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.
- 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
|