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,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: fa0abdc1ded91db064f338783e2d106403613a1bb1e3eb5fe895cdf417ecd682
4
+ data.tar.gz: 2a10b4e370a34c8b099a576b198d92129e6a4e6077531aa9c7d7b0b6bc915e6c
5
+ SHA512:
6
+ metadata.gz: 62f0fd52ac519d27eca58a2eb13069376771b14169b521ca06576c5797984559da98071fdd1cb1b74366819659b57f50873cb8e143c2b95114b480928cdba43d
7
+ data.tar.gz: 0707dfdb6114e8e8fa34ede206c37324195ea33d8bd3c3b994315c4939179d3ef576091918f0a27b456e02843e896f6b41e6e24727862aee70ae8c41e5e25617
@@ -0,0 +1,20 @@
1
+ Copyright 2019 Robert Roach
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,3 @@
1
+ ## Summary
2
+
3
+ Created by rails-templates
@@ -0,0 +1,22 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'Ros::Core'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.md')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
18
+ load 'rails/tasks/engine.rake'
19
+
20
+ load 'rails/tasks/statistics.rake'
21
+
22
+ require 'bundler/gem_tasks'
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ros
4
+ class ApplicationController < ::ApplicationController
5
+ include JSONAPI::ActsAsResourceController
6
+ rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
7
+ include OpenApi::DSL
8
+
9
+ before_action :authenticate_it!
10
+ before_action :set_raven_context, if: -> { Settings.credentials.sentry_dsn }
11
+
12
+ def authenticate_it!
13
+ return unless @current_user = request.env['warden'].authenticate!(:api_token)
14
+ # set_jwt if request.env['HTTP_AUTHORIZATION'].starts_with?('Basic')
15
+ response.set_header('AUTHORIZATION', "Bearer #{jwt}") if request.env['HTTP_AUTHORIZATION'].starts_with?('Basic')
16
+
17
+ # render(status: :unauthorized, json: { errors: [{
18
+ # status: 401, code: 'unauthorized', title: 'Unauthorized'
19
+ # }]})
20
+ # throw(:abort) unless @current_user
21
+ end
22
+
23
+ # def set_jwt; response.set_header('AUTHORIZATION', "Bearer #{jwt}") end
24
+
25
+ def jwt; Jwt.encode(current_user.jwt_payload) end
26
+
27
+ def current_user; @current_user end
28
+
29
+ # Methods for Pundit
30
+ def context; { user: current_user } end
31
+ def user_not_authorized; head :forbidden end
32
+
33
+ def set_raven_context
34
+ # Raven.user_context(id: session[:current_user_id]) # or anything else in session
35
+ Raven.extra_context(params: params.to_unsafe_h, url: request.url, tenant: Apartment::Tenant.current)
36
+ end
37
+
38
+ # Documentation
39
+ api_dry [:index, :show] do
40
+ query :page, Integer
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class TenantsController < Ros::ApplicationController
4
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ApplicationDoc
4
+ include OpenApi::DSL
5
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class TenantsDoc < ApplicationDoc
4
+ route_base '/v1/tenants'
5
+
6
+ api :index do
7
+ # ...
8
+ end
9
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ApiBelongsTo
4
+ # NOTE: Allows microservices to talk to each other using the api-client library
5
+ # fail ArgumentError,
6
+ # "Column #{model_id} does not exist on #{name}" unless column_names.include?(foreign_key_column)
7
+ extend ActiveSupport::Concern
8
+
9
+ class_methods do
10
+ # Reads a GID, caches and returns the result with a simple declaration
11
+ # Example: api_belongs_to :user, class_name: 'Ros::IAM::User'
12
+ def api_belongs_to(model_name, class_name: nil, foreign_key: nil, polymorphic: nil)
13
+ model_name = model_name.to_s
14
+ class_name = class_name || model_name.classify
15
+ attr_type = polymorphic ? "#{model_name}_type" : model_name
16
+ attr_id = "#{model_name}_id"
17
+ gid_name = "#{model_name}_gid"
18
+
19
+ # defines a method that returns a GlobalID in format: gid://internal/Service::Model/:id
20
+ # Example: api_belongs_to :user, class_name: 'Ros::IAM::User'
21
+ # creates a method named #user_gid that returns a GlobalID representing that instance's gid referencing :user
22
+ define_method(gid_name) do
23
+ current_id = send(attr_id)&.to_s
24
+ unless instance_variable_get("@#{gid_name}")&.model_id == current_id
25
+ gid_string = "gid://internal/#{polymorphic ? send(attr_type) : class_name}/#{current_id}"
26
+ instance_variable_set("@#{gid_name}", current_id.blank? ? nil : GlobalID.new(gid_string))
27
+ end
28
+ instance_variable_get("@#{gid_name}")
29
+ end
30
+
31
+ # defines a method that returns a model from a remote service
32
+ # Example: api_belongs_to :user, class_name: 'Ros::IAM::User'
33
+ # creates a method named #user that returns an object from the GlobalID
34
+ define_method(model_name) do
35
+ current_id = send(attr_id)&.to_s
36
+ unless instance_variable_get("@#{model_name}")&.id == current_id
37
+ instance_variable_set("@#{model_name}", current_id.blank? ? nil : GlobalID::Locator.locate(send(gid_name)).first)
38
+ end
39
+ instance_variable_get("@#{model_name}")
40
+ end
41
+
42
+ # defines a method that takes an object and updates the associated _id and _gid values
43
+ # Example: api_belongs_to :user, class_name: 'Ros::IAM::User'
44
+ # creates a method named #user= that takes an object of type Ros::IAM::User and sets it on the model
45
+ define_method("#{model_name}=") do |obj|
46
+ fail ArgumentError, "Must be of type #{obj}" unless polymorphic || obj.class.name.eql?(class_name)
47
+ send("#{attr_id}=", obj.id)
48
+ send("#{attr_type}=", obj.class.name) if polymorphic
49
+ instance_variable_set("@#{model_name}", obj)
50
+ instance_variable_set("@#{gid_name}", obj.to_gid)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ros
4
+ module TenantConcern
5
+ extend ActiveSupport::Concern
6
+
7
+ class_methods do
8
+ def excluded_models
9
+ %w(Tenant)
10
+ end
11
+
12
+ def urn_id; :account_id end
13
+
14
+ def public_schema_endpoints; [] end
15
+
16
+ def schema_name_for(id:)
17
+ Tenant.find_by(id: id)&.schema_name || public_schema
18
+ end
19
+
20
+ def schema_name_from(account_id: nil, id: nil)
21
+ if account_id and (tenant = Tenant.find_by(schema_name: account_id_to_schema(account_id)))
22
+ return tenant.schema_name
23
+ elsif id and (tenant = Tenant.find_by(id: id))
24
+ return tenant.schema_name
25
+ end
26
+ end
27
+
28
+ def account_id_to_schema(account_id)
29
+ account_id.to_s.scan(/.{3}/).join('_')
30
+ end
31
+
32
+ def public_schema
33
+ case ActiveRecord::Base.connection.class.name
34
+ when 'ActiveRecord::ConnectionAdapters::SQLite3Adapter'
35
+ nil
36
+ when 'ActiveRecord::ConnectionAdapters::PostgreSQLAdapter'
37
+ 'public'
38
+ end
39
+ end
40
+ end
41
+
42
+ included do
43
+ attr_reader :account_id
44
+
45
+ validates :schema_name, presence: true, length: { is: 11 }
46
+
47
+ validate :fixed_values_unchanged, if: :persisted?
48
+
49
+ after_create :create_schema
50
+
51
+ after_destroy :destroy_schema
52
+
53
+ def fixed_values_unchanged
54
+ errors.add(:schema_name, 'schema_name cannot be changed') if schema_name_changed?
55
+ end
56
+
57
+ def account_id
58
+ @account_id ||= schema_name.remove('_')
59
+ end
60
+
61
+ def current_tenant
62
+ self
63
+ end
64
+
65
+ def switch!
66
+ Apartment::Tenant.switch!(schema_name)
67
+ end
68
+
69
+ def switch
70
+ Apartment::Tenant.switch(schema_name) do
71
+ yield
72
+ end
73
+ end
74
+
75
+ # NOTE: This method is very important!
76
+ # Called by RpcWorker#receive and TenantMiddleware#parse_tenant_name
77
+ # It parses a request hash and sets the necessary RequestStere settings
78
+ # The caller then uses these settings to select the appropriate schema for the request to operate on
79
+ # TODO: This is probably where the JWT will be processed; Or it will already be decrypted and values put into the header
80
+ # NOTE: Either way, the request header needs to put the tenant somewhere so logging can be done per tenant
81
+ def self.set_request_store(request_hash)
82
+ request = RequestStore.store[:tenant_request] = ApiAll::TenantRequest.new(request_hash)
83
+ raise ArgumentError, 'Tenant schema is nil!' unless request.schema_name
84
+ RequestStore.store[:tenant] = find_by!(schema_name: request.schema_name)
85
+ end
86
+
87
+ def create_schema
88
+ Apartment::Tenant.create(schema_name)
89
+ Rails.logger.info("Tenant created: #{schema_name}")
90
+ rescue Apartment::TenantExists => e
91
+ Rails.logger.warn("Failed to create tenant (already exists): #{schema_name}")
92
+ raise e if Rails.env.production? # Don't raise an exception in dev mode so to allow seeds to work
93
+ end
94
+
95
+ def destroy_schema
96
+ Apartment::Tenant.drop(schema_name)
97
+ Rails.logger.info("Tenant dropped: #{schema_name}")
98
+ rescue Apartment::TenantNotFound => e
99
+ Rails.logger.warn("Failed to drop tenant (not found): #{schema_name}")
100
+ raise e if Rails.env.production? # Don't raise an exception in dev mode so to allow seeds to work
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ros
4
+ # ALL models inherit from this class
5
+ class ApplicationRecord < ::ApplicationRecord
6
+ self.abstract_class = true
7
+ include ApiBelongsTo
8
+
9
+ # urn:partition:service:region:account_id:resource_type
10
+ # def self.to_urn; "#{urn_base}:#{current_tenant.try(:account_id)}:#{name.underscore}" end
11
+ def self.to_urn; "#{urn_base}:#{account_id}:#{name.underscore}" end
12
+
13
+ def self.account_id
14
+ Apartment::Tenant.current.eql?('public') ? '' : Apartment::Tenant.current.remove('_')
15
+ end
16
+
17
+ def self.current_tenant; Tenant.find_by(schema_name: Apartment::Tenant.current) end
18
+
19
+ # Universal Resource Name (URNs) and Service Namespaces
20
+ # urn:partition:service:region
21
+ def self.urn_base; "urn:#{Settings.service.partition_name}:#{Settings.service.name}:#{Settings.service.region}" end
22
+
23
+ def self.find_by_urn(value); find_by(urn_id => value) end
24
+
25
+ # urn:partition:service:region:account_id:resource_type/id
26
+ def to_urn; "#{self.class.to_urn}/#{send(self.class.urn_id)}" end
27
+
28
+ def current_tenant; self.class.current_tenant end
29
+
30
+ # NOTE: Override in model to provide a custom id
31
+ def self.urn_id; :id end
32
+ end
33
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ros
4
+ class ApplicationPolicy
5
+ attr_reader :user, :record
6
+
7
+ def self.actions
8
+ descendants.reject{ |d| d.name.eql? 'ApplicationPolicy' }.each_with_object([]) do |policy, ary|
9
+ ary.concat(policy.accepted_actions.values.flatten)
10
+ end.uniq
11
+ end
12
+
13
+ def self.policies
14
+ descendants.reject{ |d| d.name.eql? 'ApplicationPolicy' }.each_with_object([]) do |policy, ary|
15
+ ary.concat(policy.accepted_policies.values.flatten)
16
+ end.uniq
17
+ end
18
+
19
+ def initialize(user, record)
20
+ @user = user
21
+ @record = record
22
+ end
23
+
24
+ # UserPolicy.new({ policies: ['IamFullAccess'] }, nil).index?
25
+ def index?
26
+ standard_check?
27
+ end
28
+
29
+ def show?
30
+ standard_check?
31
+ end
32
+
33
+ def create?
34
+ standard_check?
35
+ end
36
+
37
+ def new?
38
+ create?
39
+ end
40
+
41
+ def update?
42
+ standard_check?
43
+ end
44
+
45
+ def edit?
46
+ update?
47
+ end
48
+
49
+ def destroy?
50
+ standard_check?
51
+ end
52
+
53
+ class Scope
54
+ attr_reader :user, :scope
55
+
56
+ def initialize(user, scope)
57
+ @user = user
58
+ @scope = scope
59
+ end
60
+
61
+ def resolve
62
+ scope.all
63
+ end
64
+ end
65
+
66
+ # def self.policies
67
+ # {
68
+ # "#{policy_name}FullAccess": {
69
+ # Effect: 'Allow',
70
+ # Action: "#{policy_name}:*",
71
+ # Resource: '*'
72
+ # },
73
+ # "#{policy_name}ReadOnlyAccess": {
74
+ # Effect: 'Allow',
75
+ # Action: ["#{policy_name}:Get*", "#{policy_name}:List*"],
76
+ # Resource: '*'
77
+ # }
78
+ # }
79
+ # end
80
+ #
81
+ def service; Settings.service.name.eql?('iam') ? :local : :remote end
82
+
83
+ def standard_check?
84
+ action = caller_locations(1,1)[0].label.to_sym
85
+ # Just like apartment, this will need code for if in IAM or in remote
86
+ if service.eql?(:local)
87
+ (user.policies.pluck(:name) & accepted_policies(action)).any? || (user.actions.pluck(:name) & accepted_actions(action)).any?
88
+ else
89
+ (user.policies & accepted_policies(action)).any? || (user.actions.pluck(:name) & accepted_actions(action)).any?
90
+ end
91
+ end
92
+
93
+ def accepted_policies(action); self.class.accepted_policies[action] || [] end
94
+ def accepted_actions(action); self.class.accepted_actions[action] || [] end
95
+
96
+ def self.accepted_policies
97
+ {
98
+ index?: [
99
+ "AdministratorAccess",
100
+ "#{policy_name}FullAccess",
101
+ "#{policy_name}ReadOnlyAccess",
102
+ ],
103
+ show?: [
104
+ "AdministratorAccess",
105
+ "#{policy_name}ReadOnlyAccess",
106
+ ],
107
+ create?: [
108
+ "AdministratorAccess",
109
+ "#{policy_name}FullAccess",
110
+ ],
111
+ update?: [
112
+ "AdministratorAccess",
113
+ "#{policy_name}FullAccess",
114
+ ],
115
+ destroy?: [
116
+ "AdministratorAccess",
117
+ "#{policy_name}FullAccess",
118
+ ]
119
+ }
120
+ end
121
+
122
+ def self.accepted_actions
123
+ {
124
+ index?: [
125
+ "#{policy_name}List#{model_name.pluralize}"
126
+ ],
127
+ create?: [
128
+ "#{policy_name}Create#{model_name}"
129
+ ]
130
+ }
131
+ end
132
+
133
+ def self.policy_name; Settings.service.policy_name end
134
+
135
+ def self.model_name
136
+ "#{name.gsub('Policy', '')}"
137
+ end
138
+ end
139
+ end