ros-core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ class TenantPolicy < Ros::ApplicationPolicy; end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ros
4
+ class ApplicationResource < JSONAPI::Resource
5
+ include JSONAPI::Authorization::PunditScopedResource
6
+ abstract
7
+ attributes :urn
8
+
9
+ def urn; @model.to_urn end
10
+ end
11
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ class TenantResource < Ros::ApplicationResource
4
+ # attributes :account_id, :alias, :name, :schema_name # :locale
5
+ attributes :schema_name, :properties
6
+ end
File without changes
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ Apartment.configure do |config|
4
+ # Provide list of schemas to be migrated when rails db:migrate is invoked
5
+ # SEE: https://github.com/influitive/apartment#managing-migrations
6
+ config.tenant_names = proc { Tenant.pluck(:schema_name) }
7
+
8
+ # List of models that are NOT multi-tenanted
9
+ # See: https://github.com/influitive/apartment#excluding-models
10
+ config.excluded_models = Tenant.excluded_models
11
+ end
@@ -0,0 +1,55 @@
1
+ Config.setup do |config|
2
+ # Name of the constant exposing loaded settings
3
+ config.const_name = 'Settings'
4
+
5
+ # Ability to remove elements of the array set in earlier loaded settings file. For example value: '--'.
6
+ #
7
+ # config.knockout_prefix = nil
8
+
9
+ # Overwrite an existing value when merging a `nil` value.
10
+ # When set to `false`, the existing value is retained after merge.
11
+ #
12
+ # config.merge_nil_values = true
13
+
14
+ # Overwrite arrays found in previously loaded settings file. When set to `false`, arrays will be merged.
15
+ #
16
+ # config.overwrite_arrays = true
17
+ config.overwrite_arrays = false
18
+
19
+ # Load environment variables from the `ENV` object and override any settings defined in files.
20
+ #
21
+ # config.use_env = false
22
+ config.use_env = true
23
+
24
+ # Define ENV variable prefix deciding which variables to load into config.
25
+ #
26
+ # config.env_prefix = 'Settings'
27
+ config.env_prefix = 'PLATFORM'
28
+
29
+ # What string to use as level separator for settings loaded from ENV variables. Default value of '.' works well
30
+ # with Heroku, but you might want to change it for example for '__' to easy override settings from command line, where
31
+ # using dots in variable names might not be allowed (eg. Bash).
32
+ #
33
+ # config.env_separator = '.'
34
+ config.env_separator = '__'
35
+
36
+ # Ability to process variables names:
37
+ # * nil - no change
38
+ # * :downcase - convert to lower case
39
+ #
40
+ # config.env_converter = :downcase
41
+
42
+ # Parse numeric values as integers instead of strings.
43
+ #
44
+ # config.env_parse_values = true
45
+
46
+ # Validate presence and type of specific config values. Check https://github.com/dry-rb/dry-validation for details.
47
+ #
48
+ # config.schema do
49
+ # required(:name).filled
50
+ # required(:age).maybe(:int?)
51
+ # required(:email).filled(format?: EMAIL_REGEX)
52
+ # end
53
+
54
+ end
55
+ Settings.load_env!
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ JSONAPI.configure do |config|
4
+ # http://jsonapi-resources.com/v0.9/guide/resource_caching.html
5
+ config.resource_cache = Rails.cache
6
+
7
+ #:underscored_key, :camelized_key, :dasherized_key, or custom
8
+ config.json_key_format = :underscored_key
9
+
10
+ #:underscored_route, :camelized_route, :dasherized_route, or custom
11
+ config.route_format = :underscored_route
12
+ end
13
+
14
+ Mime::Type.register 'application/json-patch+json', :json_patch
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ Rails.application.routes.draw do
4
+ jsonapi_resources :tenants
5
+ # TODO: This uses a non-model backed resource
6
+ # jsonapi_resources :policies
7
+ end
@@ -0,0 +1,33 @@
1
+ # This service's specific configuration; calculate URNs, etc
2
+ service:
3
+ # name is set by the service
4
+ # name:
5
+ # policy_name is set by the service
6
+ # policy_name:
7
+ partition_name: ros
8
+ region: ''
9
+ auth_type: Internal
10
+ # The options for configuring internal connections to services
11
+ services:
12
+ connection:
13
+ type: port
14
+ # 'host' is the default. Each service runs on port 3000 and is uniquely
15
+ # addressed by it's host name which is taken from the module name of the Client
16
+ # Can override values here with an ENV. For example, to use https instead of http:
17
+ # export PLATFORM_SERVICES_CONNECTION_HOST_SCHEME=https
18
+ host:
19
+ scheme: http
20
+ port: 3000
21
+ # port based means that each configured service has its own port on localhost
22
+ # port number starts at value of 'port' and increments by 1 for each additional service
23
+ # The port to service mapping is in alphabetical order by the service name
24
+ # e.g. 'comm' is a lower port number than 'iam'
25
+ # To use this connection type:
26
+ # export PLATFORM_SERVICES_CONNECTION_TYPE=port
27
+ port:
28
+ scheme: http
29
+ host: localhost
30
+ port: 3000
31
+ jwt:
32
+ iss: ros
33
+ alg: HS256
@@ -0,0 +1,14 @@
1
+ Description:
2
+ Generates jsonapi controller and resource, pundit policy, model, spec and factory
3
+
4
+ Example:
5
+ rails generate endpoint Thing
6
+
7
+ This will create:
8
+ app/controllers/things_controller.rb
9
+ app/resources/thing_resource.rb
10
+ app/policies/thing_policy.rb
11
+ app/models/thing.rb
12
+ spec/models/thing_spec.rb
13
+ spec/factories/things.rb
14
+ db/migrate/XXX_create_things.rb
@@ -0,0 +1,34 @@
1
+ class EndpointGenerator < Rails::Generators::NamedBase
2
+ source_root File.expand_path('templates', __dir__)
3
+
4
+ def create_files
5
+ parent_module = Dir.pwd.split('/').last.remove('ros-').classify
6
+ invoke(:model)
7
+ gsub_file("app/models/#{name}.rb", 'ApplicationRecord', "#{parent_module}::ApplicationRecord")
8
+ insert_into_file 'config/routes.rb', after: "Rails.application.routes.draw do\n" do
9
+ " jsonapi_resources :#{plural_name}\n"
10
+ end
11
+ create_file "app/controllers/#{plural_name}_controller.rb", <<-FILE
12
+ # frozen_string_literal: true
13
+
14
+ class #{name.classify.pluralize}Controller < #{parent_module}::ApplicationController
15
+ end
16
+ FILE
17
+
18
+ create_file "app/resources/#{name}_resource.rb", <<-FILE
19
+ # frozen_string_literal: true
20
+
21
+ class #{name.classify}Resource < #{parent_module}::ApplicationResource
22
+ attributes #{args.map { |e| ':' + e.split(':').first }.join(', ')}
23
+ end
24
+ FILE
25
+
26
+ create_file "app/policies/#{name}_policy.rb", <<-FILE
27
+ # frozen_string_literal: true
28
+
29
+ class #{name.classify}Policy < Cognito::ApplicationPolicy
30
+ end
31
+ FILE
32
+
33
+ end
34
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record'
4
+
5
+ module Ros
6
+ module Migrations
7
+ module Create
8
+ module Tenant
9
+ def change
10
+ create_table :tenants do |t|
11
+ t.string :schema_name, null: false, index: { unique: true }
12
+
13
+ t.timestamps null: false
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ros
4
+ class ApiTokenStrategy < Warden::Strategies::Base
5
+ attr_accessor :auth_string, :auth_type, :token, :access_key_id, :secret_access_key, :urn
6
+
7
+ def auth_string; @auth_string ||= env['HTTP_AUTHORIZATION'] end
8
+
9
+ def auth_type; @auth_type ||= auth_string.split.first.downcase end
10
+
11
+ def token; @token ||= auth_string.split.last end
12
+
13
+ def access_key_id; @access_key_id ||= token.split(':').first end
14
+
15
+ def secret_access_key; @secret_access_key ||= token.split(':').last end
16
+
17
+ def valid?; token.present? end
18
+
19
+ def authenticate!
20
+ user = send("authenticate_#{auth_type}") if auth_type.in? %w(basic bearer)
21
+ return success!(user) if user
22
+ # This is returned to IAM service
23
+ fail!({ errors: [{ status: 401, code: 'unauthorized', title: 'Unauthorized' }] }.to_json)
24
+ end
25
+
26
+ def authenticate_basic
27
+ # TODO: Credential.authorization must be an instance variable
28
+ Ros::Sdk::Credential.authorization = auth_string
29
+ return unless credential = Ros::IAM::Credential.where(access_key_id: access_key_id).first
30
+ "Ros::IAM::#{credential.owner_type}".constantize.find(credential.owner_id).first
31
+ # NOTE: Swallow the auth error and return nil which causes user to be nil, which cuases FailureApp to be invoked
32
+ rescue JsonApiClient::Errors::NotAuthorized => e
33
+ end
34
+
35
+ def authenticate_bearer
36
+ return unless urn = Urn.from_jwt(token)
37
+ return unless urn.model_name.in? %w(Root User)
38
+ # TODO: Credential.authorization must be an instance variable
39
+ Ros::Sdk::Credential.authorization = auth_string
40
+ "Ros::IAM::#{urn.model_name}".constantize.find_by_urn(urn.resource_id)
41
+ # NOTE: Swallow the auth error and return nil which causes user to be nil, which cuases FailureApp to be invoked
42
+ rescue JsonApiClient::Errors::NotAuthorized => e
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Require gems from gemspec so they are available to the gems that depend on ros-core
4
+ require 'seedbank'
5
+ require 'apartment'
6
+ require 'jsonapi-resources'
7
+ require 'jsonapi/authorization'
8
+ require 'attr_encrypted'
9
+ require 'pry'
10
+ require 'jwt'
11
+ require 'warden'
12
+ require 'config'
13
+ require 'ros_sdk'
14
+
15
+ require_relative 'tenant_middleware'
16
+ require_relative 'api_token_strategy'
17
+
18
+ require 'ros/core/engine'
19
+
20
+ module Ros
21
+ class Jwt
22
+ def self.encode(payload)
23
+ # TODO: This is called twice by IAM on basic auth
24
+ JWT.encode(payload, Settings.credentials.jwt_encryption_key, Settings.jwt.alg)
25
+ end
26
+
27
+ def self.decode(payload)
28
+ JWT.decode(payload, Settings.credentials.jwt_encryption_key, Settings.jwt.alg)
29
+ end
30
+ end
31
+
32
+ Urn = Struct.new(:txt, :partition_name, :service_name, :region, :account_id, :resource) do
33
+ def to_s; to_a.join(':') end
34
+
35
+ def self.from_jwt(token)
36
+ jwt = Jwt.decode(token)
37
+ return unless urn_string = jwt[0]['urn']
38
+ urn_array = urn_string.split(':')
39
+ new(*urn_array)
40
+ rescue JWT::DecodeError
41
+ end
42
+
43
+ def resource_type; resource.split('/').first end
44
+ def resource_id; resource.split('/').last end
45
+
46
+ def model_name; resource_type.classify end
47
+ def model; model_name.constantize end
48
+ def instance; model.find_by_urn(resource_id) end
49
+ end
50
+
51
+ # Failure response to return JSONAPI error message when authentication failse
52
+ class FailureApp
53
+ def self.call(env)
54
+ [
55
+ '401',
56
+ { 'Content-Type' => 'application/vnd.api+json' },
57
+ [
58
+ { errors: [{ status: '401', title: 'Unauthorized' }] }.to_json
59
+ ]
60
+ ]
61
+ end
62
+ end
63
+
64
+ module Core
65
+ class << self
66
+ def load_factories
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Add commands to the Pry command set for all services
4
+ # Change the pry cli prompt to displace the current tenant
5
+ # "[#{PryRails::Prompt.project_name}][#{PryRails::Prompt.formatted_env}][#{Apartment::Tenant.current}] " \
6
+
7
+ def ab
8
+ Apartment::Tenant.current
9
+ rescue ActiveRecord::ConnectionNotEstablished
10
+ 'n/a'
11
+ end
12
+
13
+ if Pry::Prompt.respond_to?(:add)
14
+ desc = "Includes the current Rails environment and project folder name.\n" \
15
+ "[1] [project_name][Rails.env][Apartment::Tenant.current] pry(main)>"
16
+ Pry::Prompt.add 'ros', desc, %w(> *) do |target_self, nest_level, pry, sep|
17
+ "[#{pry.input_ring.size}] " \
18
+ "[#{PryRails::Prompt.project_name}][#{PryRails::Prompt.formatted_env}][#{ab}] " \
19
+ "#{pry.config.prompt_name}(#{Pry.view_clip(target_self)})" \
20
+ "#{":#{nest_level}" unless nest_level.zero?}#{sep} "
21
+ end
22
+
23
+ Pry.config.prompt = Pry::Prompt[:ros][:value]
24
+ end
25
+
26
+ module Ros
27
+ module Console
28
+ module Methods
29
+ def fbc(type)
30
+ try_count ||= 0
31
+ FactoryBot.create(type)
32
+ rescue KeyError
33
+ try_count += 1
34
+ Dir[Pathname.new(Dir.pwd).join('spec', 'factories', '**', '*.rb')].each { |f| require f }
35
+ retry if try_count < 2
36
+ end
37
+ class << self
38
+ def models
39
+ Dir[Pathname.new(Dir.pwd).join('app', 'models', '**', '*.rb')]
40
+ end unless Ros::Console::Methods.methods.include? :models
41
+ def init
42
+ models.each do |model|
43
+ name = File.basename(model).gsub('.rb', '')
44
+ id = name[0]
45
+ define_method("#{id}a") { name.classify.constantize.all }
46
+ define_method("#{id}c") { fbc(name) }
47
+ define_method("#{id}f") { Rails.configuration.x.memoized_shortcuts["#{id}f"] ||= name.classify.constantize.first }
48
+ define_method("#{id}l") { Rails.configuration.x.memoized_shortcuts["#{id}l"] ||= name.classify.constantize.last }
49
+ define_method("#{id}p") { |column| name.classify.constantize.pluck(column) }
50
+ end
51
+ end
52
+ end
53
+ Ros::Console::Methods.init
54
+ def ct; Rails.configuration.x.memoized_shortcuts[:ct] ||= Tenant.find_by(schema_name: Apartment::Tenant.current) end
55
+ end
56
+ end
57
+ end
58
+
59
+ Ros::PryCommandSet = Pry::CommandSet.new
60
+
61
+ module Ros::Console::Commands
62
+ class TenantSelect < Pry::ClassCommand
63
+ match 'select-tenant'
64
+ group 'ros'
65
+ description 'Select Tenant'
66
+ banner <<-BANNER
67
+ Usage: select-tenant [id]
68
+
69
+ 'id' is the numerical id returned from `select-tenant` when no id is passed
70
+ If the id is passed that tenant's schema will become the active schema
71
+ If the id that is passed doesn't exist then the default schema 'public' will become the active schema
72
+ BANNER
73
+
74
+ def process(id = nil)
75
+ if id.nil?
76
+ # NOTE: This is dumb, but passing an array of field names to #pluck results in a noisy DEPRECATION WARNING
77
+ if Tenant.column_names.include? 'name'
78
+ output.puts Tenant.order(:id).pluck(:id, :schema_name, :name).each_with_object([]) { |a, ary| ary << a.join(' ') }
79
+ else
80
+ output.puts Tenant.order(:id).pluck(:id, :schema_name).each_with_object([]) { |a, ary| ary << a.join(' ') }
81
+ end
82
+ return
83
+ end
84
+ Apartment::Tenant.switch! Tenant.schema_name_for(id: id)
85
+ Rails.configuration.x.memoized_shortcuts = {}
86
+ end
87
+
88
+ Ros::PryCommandSet.add_command(self)
89
+ end
90
+
91
+ class Reload < Pry::ClassCommand
92
+ match 'reload'
93
+ group 'ros'
94
+ description 'reload rails and reset memoized'
95
+
96
+ def process
97
+ Rails.configuration.x.memoized_shortcuts = {}
98
+ TOPLEVEL_BINDING.eval('self').reload!
99
+ end
100
+
101
+ Ros::PryCommandSet.add_command(self)
102
+ end
103
+
104
+ class RabbitMQ < Pry::ClassCommand
105
+ match 'mq-send'
106
+ group 'ros'
107
+ description 'send a message on the mq bus'
108
+
109
+ # TODO: refactor
110
+ def process
111
+ return unless ENV['AMQP_URL']
112
+ record = { bucket: 'test', key: 'path/to/object' }
113
+ conn = Bunny.new(ENV['AMQP_URL'])
114
+ conn.start
115
+ ch = conn.create_channel
116
+ puts "#{record[:bucket]}/#{record[:key]}"
117
+ puts ENV['AMQP_QUEUE_NAME']
118
+ puts record.merge!({ tenant: 'hsbc', environment: 'development' })
119
+
120
+ res = ch.default_exchange.publish("#{record[:bucket]}/#{record[:key]}",
121
+ routing_key: ENV['AMQP_QUEUE_NAME'],
122
+ headers: record.merge({ version: ENV['AMQP_VERSION'].to_s }))
123
+
124
+ puts 'Here is output from bunny'
125
+ puts res
126
+ conn.close
127
+ end
128
+ end
129
+
130
+ class ToggleLogger < Pry::ClassCommand
131
+ match 'toggle-logger'
132
+ group 'ros'
133
+ description 'Toggle the Rails Logger on/off'
134
+
135
+ def process(state = nil)
136
+ unless state.nil?
137
+ return if (state == 'off' and ActiveRecord::Base.logger.nil?) or (state == 'on' and not ActiveRecord::Base.logger.nil?)
138
+ end
139
+ if ActiveRecord::Base.logger.nil?
140
+ ActiveRecord::Base.logger = Rails.configuration.x.old_logger
141
+ else
142
+ Rails.configuration.x.old_logger = ActiveRecord::Base.logger
143
+ ActiveRecord::Base.logger = nil
144
+ end
145
+ end
146
+
147
+ Ros::PryCommandSet.add_command(self)
148
+ end
149
+ end
150
+
151
+ Pry.config.commands.import Ros::PryCommandSet
152
+ Pry.config.commands.alias_command 'r', 'reload'
153
+ Pry.config.commands.alias_command 'st', 'select-tenant'
154
+ Pry.config.commands.alias_command 'to', 'toggle-logger'