croods 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 +28 -0
- data/Rakefile +19 -0
- data/lib/croods.rb +58 -0
- data/lib/croods/action.rb +17 -0
- data/lib/croods/api.rb +26 -0
- data/lib/croods/api/initial_schema.json +52 -0
- data/lib/croods/attribute.rb +15 -0
- data/lib/croods/controller.rb +28 -0
- data/lib/croods/controller/actions.rb +71 -0
- data/lib/croods/controller/already_taken.rb +27 -0
- data/lib/croods/controller/authentication.rb +22 -0
- data/lib/croods/controller/authorization.rb +31 -0
- data/lib/croods/controller/collection.rb +15 -0
- data/lib/croods/controller/member.rb +45 -0
- data/lib/croods/controller/model.rb +17 -0
- data/lib/croods/controller/multi_tenancy.rb +54 -0
- data/lib/croods/controller/not_found.rb +22 -0
- data/lib/croods/controller/record_invalid.rb +22 -0
- data/lib/croods/controller/resource.rb +30 -0
- data/lib/croods/middleware.rb +44 -0
- data/lib/croods/middleware/base.rb +20 -0
- data/lib/croods/middleware/request_validation.rb +10 -0
- data/lib/croods/middleware/response_validation.rb +10 -0
- data/lib/croods/model.rb +41 -0
- data/lib/croods/policy.rb +118 -0
- data/lib/croods/policy/scope.rb +131 -0
- data/lib/croods/railtie.rb +6 -0
- data/lib/croods/resource.rb +39 -0
- data/lib/croods/resource/actions.rb +51 -0
- data/lib/croods/resource/attributes.rb +53 -0
- data/lib/croods/resource/attributes/base.rb +30 -0
- data/lib/croods/resource/attributes/request.rb +29 -0
- data/lib/croods/resource/attributes/response.rb +13 -0
- data/lib/croods/resource/authentication.rb +46 -0
- data/lib/croods/resource/authorization.rb +47 -0
- data/lib/croods/resource/controller.rb +50 -0
- data/lib/croods/resource/filters.rb +32 -0
- data/lib/croods/resource/identifier.rb +13 -0
- data/lib/croods/resource/json_schema.rb +32 -0
- data/lib/croods/resource/json_schema/definition.rb +34 -0
- data/lib/croods/resource/json_schema/definitions.rb +31 -0
- data/lib/croods/resource/json_schema/initial_schema.json +8 -0
- data/lib/croods/resource/json_schema/links.rb +40 -0
- data/lib/croods/resource/json_schema/links/collection.rb +70 -0
- data/lib/croods/resource/json_schema/links/create.rb +41 -0
- data/lib/croods/resource/json_schema/links/destroy.rb +40 -0
- data/lib/croods/resource/json_schema/links/index.rb +17 -0
- data/lib/croods/resource/json_schema/links/member.rb +40 -0
- data/lib/croods/resource/json_schema/links/show.rb +40 -0
- data/lib/croods/resource/json_schema/links/update.rb +40 -0
- data/lib/croods/resource/json_schema/properties.rb +32 -0
- data/lib/croods/resource/json_schema/required.rb +34 -0
- data/lib/croods/resource/model.rb +35 -0
- data/lib/croods/resource/names.rb +23 -0
- data/lib/croods/resource/paths.rb +16 -0
- data/lib/croods/resource/policy.rb +51 -0
- data/lib/croods/resource/services.rb +18 -0
- data/lib/croods/resource/sorting.rb +13 -0
- data/lib/croods/routes.rb +46 -0
- data/lib/croods/service.rb +28 -0
- data/lib/croods/version.rb +5 -0
- data/lib/tasks/croods_tasks.rake +4 -0
- metadata +316 -0
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Croods
|
4
|
+
class Controller < ActionController::API
|
5
|
+
module Member
|
6
|
+
protected
|
7
|
+
|
8
|
+
def member
|
9
|
+
return @member ||= member_by_id if resource.identifier == :id
|
10
|
+
|
11
|
+
@member ||= member_by_identifier
|
12
|
+
end
|
13
|
+
|
14
|
+
def member_by_id
|
15
|
+
policy_scope(model).find(params[:id])
|
16
|
+
end
|
17
|
+
|
18
|
+
def member_by_identifier
|
19
|
+
policy_scope(model).find_by!(resource.identifier => identifier)
|
20
|
+
end
|
21
|
+
|
22
|
+
def identifier
|
23
|
+
params[resource.identifier]
|
24
|
+
end
|
25
|
+
|
26
|
+
def member_params
|
27
|
+
params
|
28
|
+
.permit(resource.request_attributes.keys)
|
29
|
+
.merge(
|
30
|
+
params
|
31
|
+
.require(resource.resource_name)
|
32
|
+
.permit(resource.request_attributes.keys)
|
33
|
+
)
|
34
|
+
end
|
35
|
+
|
36
|
+
def new_member
|
37
|
+
policy_scope(model).new(
|
38
|
+
member_params
|
39
|
+
.merge(tenant_params(model))
|
40
|
+
.merge(user_params(model))
|
41
|
+
)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Croods
|
4
|
+
class Controller < ActionController::API
|
5
|
+
module MultiTenancy
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
before_action :authorize_multi_tenancy, unless: :devise_controller?
|
10
|
+
end
|
11
|
+
|
12
|
+
protected
|
13
|
+
|
14
|
+
def tenant_model
|
15
|
+
return unless Croods.multi_tenancy?
|
16
|
+
|
17
|
+
Croods.multi_tenancy_by.to_s.titleize.constantize
|
18
|
+
end
|
19
|
+
|
20
|
+
def header_tenant
|
21
|
+
return unless Croods.multi_tenancy?
|
22
|
+
|
23
|
+
tenant_model.find_by!(slug: request.headers['Tenant'])
|
24
|
+
end
|
25
|
+
|
26
|
+
def current_tenant
|
27
|
+
return unless Croods.multi_tenancy?
|
28
|
+
|
29
|
+
@current_tenant ||= current_user&.tenant
|
30
|
+
end
|
31
|
+
|
32
|
+
def tenant_params(model)
|
33
|
+
return {} unless Croods.multi_tenancy?
|
34
|
+
|
35
|
+
return {} unless model.has_attribute? Croods.tenant_attribute
|
36
|
+
|
37
|
+
{ Croods.tenant_attribute => current_tenant.id }
|
38
|
+
end
|
39
|
+
|
40
|
+
def authorize_multi_tenancy
|
41
|
+
return unless Croods.multi_tenancy?
|
42
|
+
|
43
|
+
return unless current_user
|
44
|
+
|
45
|
+
return if request.headers['Tenant'] == current_tenant.slug
|
46
|
+
|
47
|
+
raise(
|
48
|
+
Pundit::NotAuthorizedError,
|
49
|
+
'You are not authorized to access this organization'
|
50
|
+
)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Croods
|
4
|
+
class Controller < ActionController::API
|
5
|
+
module NotFound
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
rescue_from ActiveRecord::RecordNotFound, with: :not_found
|
10
|
+
end
|
11
|
+
|
12
|
+
protected
|
13
|
+
|
14
|
+
def not_found(exception)
|
15
|
+
render status: :not_found, json: {
|
16
|
+
id: 'not_found',
|
17
|
+
message: exception.message
|
18
|
+
}
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Croods
|
4
|
+
class Controller < ActionController::API
|
5
|
+
module RecordInvalid
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
rescue_from ActiveRecord::RecordInvalid, with: :record_invalid
|
10
|
+
end
|
11
|
+
|
12
|
+
protected
|
13
|
+
|
14
|
+
def record_invalid(exception)
|
15
|
+
render status: :unprocessable_entity, json: {
|
16
|
+
id: 'record_invalid',
|
17
|
+
message: exception.message
|
18
|
+
}
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Croods
|
4
|
+
class Controller < ActionController::API
|
5
|
+
module Resource
|
6
|
+
protected
|
7
|
+
|
8
|
+
def resource_name
|
9
|
+
*names, _last = self.class.to_s.titleize.split
|
10
|
+
names.join
|
11
|
+
end
|
12
|
+
|
13
|
+
def resource
|
14
|
+
"#{resource_name}::Resource".constantize
|
15
|
+
end
|
16
|
+
|
17
|
+
def action
|
18
|
+
@action ||= resource.actions.find do |action|
|
19
|
+
action.name.to_s == action_name
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def execute_service(member_or_collection, params, &block)
|
24
|
+
return instance_eval(&block) unless action&.service
|
25
|
+
|
26
|
+
action.service.execute(member_or_collection, params, current_user)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'middleware/request_validation'
|
4
|
+
require_relative 'middleware/response_validation'
|
5
|
+
|
6
|
+
module Croods
|
7
|
+
module Middleware
|
8
|
+
METHODS = %i[get post put patch delete options head].freeze
|
9
|
+
|
10
|
+
EXPOSE_HEADERS = %w[
|
11
|
+
access-token expiry token-type uid client Authorization Link Total
|
12
|
+
Per-Page
|
13
|
+
].freeze
|
14
|
+
|
15
|
+
def self.insert!
|
16
|
+
insert_cors!
|
17
|
+
insert_request_validation!
|
18
|
+
insert_response_validation!
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.insert_cors!
|
22
|
+
Rails.application.config.middleware.insert_before 0, Rack::Cors do
|
23
|
+
allow do
|
24
|
+
origins '*'
|
25
|
+
resource '*', headers: :any, expose: EXPOSE_HEADERS, methods: METHODS
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.insert_request_validation!
|
31
|
+
Rails.application.config.middleware.insert_before(
|
32
|
+
ActionDispatch::Executor,
|
33
|
+
Middleware::RequestValidation
|
34
|
+
)
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.insert_response_validation!
|
38
|
+
Rails.application.config.middleware.insert_after(
|
39
|
+
ActionDispatch::Callbacks,
|
40
|
+
Middleware::ResponseValidation
|
41
|
+
)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Croods
|
4
|
+
module Middleware
|
5
|
+
class Base
|
6
|
+
attr_accessor :app, :options
|
7
|
+
|
8
|
+
def initialize(app, **options)
|
9
|
+
self.app = app
|
10
|
+
self.options = options
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(env)
|
14
|
+
committee = self.class.name.gsub('Croods', 'Committee').constantize
|
15
|
+
.new(app, options.merge(schema: Croods.json_schema))
|
16
|
+
committee.call(env)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lib/croods/model.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Croods
|
4
|
+
class Model < ActiveRecord::Base
|
5
|
+
self.abstract_class = true
|
6
|
+
|
7
|
+
def self.resource_name
|
8
|
+
to_s.pluralize
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.resource
|
12
|
+
"#{resource_name}::Resource".constantize
|
13
|
+
end
|
14
|
+
|
15
|
+
def as_json(_options = {})
|
16
|
+
attributes = {}
|
17
|
+
|
18
|
+
resource.response_attributes.each do |name, attribute|
|
19
|
+
value = send(name)
|
20
|
+
value = value.iso8601 if value && attribute.type == :datetime
|
21
|
+
attributes[name] = value
|
22
|
+
end
|
23
|
+
|
24
|
+
attributes
|
25
|
+
end
|
26
|
+
|
27
|
+
def tenant
|
28
|
+
return unless Croods.multi_tenancy?
|
29
|
+
|
30
|
+
public_send(Croods.multi_tenancy_by)
|
31
|
+
end
|
32
|
+
|
33
|
+
def resource_name
|
34
|
+
self.class.to_s.pluralize
|
35
|
+
end
|
36
|
+
|
37
|
+
def resource
|
38
|
+
"#{resource_name}::Resource".constantize
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'policy/scope'
|
4
|
+
|
5
|
+
module Croods
|
6
|
+
class Policy
|
7
|
+
DEFAULT_ROLES = %i[owner admin].freeze
|
8
|
+
|
9
|
+
def initialize(user, member)
|
10
|
+
self.user = user
|
11
|
+
self.member = member
|
12
|
+
end
|
13
|
+
|
14
|
+
protected
|
15
|
+
|
16
|
+
cattr_writer :roles
|
17
|
+
attr_accessor :user, :member
|
18
|
+
|
19
|
+
def super?(role)
|
20
|
+
return role?(role) unless Croods.multi_tenancy? && user && member_user
|
21
|
+
|
22
|
+
role?(role) && member_user.tenant == user.tenant
|
23
|
+
end
|
24
|
+
|
25
|
+
def role?(role)
|
26
|
+
user&.public_send("#{role}?")
|
27
|
+
end
|
28
|
+
|
29
|
+
def owner?
|
30
|
+
return true unless member_user
|
31
|
+
|
32
|
+
return false unless user
|
33
|
+
|
34
|
+
member_user == user
|
35
|
+
end
|
36
|
+
|
37
|
+
def member_user
|
38
|
+
return @member_user if @member_user
|
39
|
+
|
40
|
+
return if member.instance_of?(Class)
|
41
|
+
|
42
|
+
@member_user = reflection_user(member)
|
43
|
+
end
|
44
|
+
|
45
|
+
def user_is_the_owner?(record)
|
46
|
+
record.respond_to?(:user) && record.resource.user_is_the_owner?
|
47
|
+
end
|
48
|
+
|
49
|
+
def reflection_user(record)
|
50
|
+
return unless record
|
51
|
+
|
52
|
+
return record.user if user_is_the_owner?(record)
|
53
|
+
|
54
|
+
associations = list_associations(record)
|
55
|
+
|
56
|
+
return if associations.empty?
|
57
|
+
|
58
|
+
associations.each do |association|
|
59
|
+
association_user = reflection_user(record.public_send(association.name))
|
60
|
+
return association_user if association_user
|
61
|
+
end
|
62
|
+
|
63
|
+
nil
|
64
|
+
end
|
65
|
+
|
66
|
+
def list_associations(record)
|
67
|
+
record.class.reflect_on_all_associations(:belongs_to)
|
68
|
+
end
|
69
|
+
|
70
|
+
def other_tenant?(user_to_compare)
|
71
|
+
user.tenant != user_to_compare.tenant
|
72
|
+
end
|
73
|
+
|
74
|
+
def skip_associations_authorization?
|
75
|
+
!Croods.multi_tenancy? || member.instance_of?(Class)
|
76
|
+
end
|
77
|
+
|
78
|
+
def other_tenant_user?
|
79
|
+
member.respond_to?(:user) && other_tenant?(member.user)
|
80
|
+
end
|
81
|
+
|
82
|
+
def authorize_associations
|
83
|
+
return true if skip_associations_authorization?
|
84
|
+
return false if other_tenant_user?
|
85
|
+
|
86
|
+
associations = list_associations(member)
|
87
|
+
|
88
|
+
return true if associations.empty?
|
89
|
+
|
90
|
+
associations.each do |association|
|
91
|
+
association_user = reflection_user(member.public_send(association.name))
|
92
|
+
return false if association_user && other_tenant?(association_user)
|
93
|
+
end
|
94
|
+
|
95
|
+
true
|
96
|
+
end
|
97
|
+
|
98
|
+
def authorize_action(action)
|
99
|
+
return true if action.public
|
100
|
+
|
101
|
+
return false unless authorize_associations
|
102
|
+
|
103
|
+
roles = action.roles || DEFAULT_ROLES
|
104
|
+
|
105
|
+
roles.each do |role|
|
106
|
+
return true if authorize_role(role)
|
107
|
+
end
|
108
|
+
|
109
|
+
false
|
110
|
+
end
|
111
|
+
|
112
|
+
def authorize_role(role)
|
113
|
+
return owner? if role.to_sym == :owner
|
114
|
+
|
115
|
+
super?(role)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|