ros-core 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +3 -0
- data/Rakefile +22 -0
- data/app/controllers/ros/application_controller.rb +43 -0
- data/app/controllers/tenants_controller.rb +4 -0
- data/app/docs/application_doc.rb +5 -0
- data/app/docs/tenants_doc.rb +9 -0
- data/app/models/concerns/api_belongs_to.rb +54 -0
- data/app/models/concerns/ros/tenant_concern.rb +104 -0
- data/app/models/ros/application_record.rb +33 -0
- data/app/policies/ros/application_policy.rb +139 -0
- data/app/policies/tenant_policy.rb +3 -0
- data/app/resources/ros/application_resource.rb +11 -0
- data/app/resources/tenant_resource.rb +6 -0
- data/config/environment.rb +0 -0
- data/config/initializers/apartment.rb +11 -0
- data/config/initializers/config.rb +55 -0
- data/config/initializers/jsonapi_resources.rb +14 -0
- data/config/routes.rb +7 -0
- data/config/settings.yml +33 -0
- data/lib/generators/endpoint/USAGE +14 -0
- data/lib/generators/endpoint/endpoint_generator.rb +34 -0
- data/lib/migrations.rb +19 -0
- data/lib/ros/api_token_strategy.rb +45 -0
- data/lib/ros/core.rb +70 -0
- data/lib/ros/core/console.rb +154 -0
- data/lib/ros/core/engine.rb +88 -0
- data/lib/ros/core/version.rb +5 -0
- data/lib/ros/tenant_middleware.rb +108 -0
- data/lib/tasks/db.rake +42 -0
- data/lib/tasks/ros/core_tasks.rake +4 -0
- metadata +313 -0
checksums.yaml
ADDED
@@ -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
|
data/MIT-LICENSE
ADDED
@@ -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.
|
data/README.md
ADDED
data/Rakefile
ADDED
@@ -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,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
|