ros-core 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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