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.
Files changed (65) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +28 -0
  4. data/Rakefile +19 -0
  5. data/lib/croods.rb +58 -0
  6. data/lib/croods/action.rb +17 -0
  7. data/lib/croods/api.rb +26 -0
  8. data/lib/croods/api/initial_schema.json +52 -0
  9. data/lib/croods/attribute.rb +15 -0
  10. data/lib/croods/controller.rb +28 -0
  11. data/lib/croods/controller/actions.rb +71 -0
  12. data/lib/croods/controller/already_taken.rb +27 -0
  13. data/lib/croods/controller/authentication.rb +22 -0
  14. data/lib/croods/controller/authorization.rb +31 -0
  15. data/lib/croods/controller/collection.rb +15 -0
  16. data/lib/croods/controller/member.rb +45 -0
  17. data/lib/croods/controller/model.rb +17 -0
  18. data/lib/croods/controller/multi_tenancy.rb +54 -0
  19. data/lib/croods/controller/not_found.rb +22 -0
  20. data/lib/croods/controller/record_invalid.rb +22 -0
  21. data/lib/croods/controller/resource.rb +30 -0
  22. data/lib/croods/middleware.rb +44 -0
  23. data/lib/croods/middleware/base.rb +20 -0
  24. data/lib/croods/middleware/request_validation.rb +10 -0
  25. data/lib/croods/middleware/response_validation.rb +10 -0
  26. data/lib/croods/model.rb +41 -0
  27. data/lib/croods/policy.rb +118 -0
  28. data/lib/croods/policy/scope.rb +131 -0
  29. data/lib/croods/railtie.rb +6 -0
  30. data/lib/croods/resource.rb +39 -0
  31. data/lib/croods/resource/actions.rb +51 -0
  32. data/lib/croods/resource/attributes.rb +53 -0
  33. data/lib/croods/resource/attributes/base.rb +30 -0
  34. data/lib/croods/resource/attributes/request.rb +29 -0
  35. data/lib/croods/resource/attributes/response.rb +13 -0
  36. data/lib/croods/resource/authentication.rb +46 -0
  37. data/lib/croods/resource/authorization.rb +47 -0
  38. data/lib/croods/resource/controller.rb +50 -0
  39. data/lib/croods/resource/filters.rb +32 -0
  40. data/lib/croods/resource/identifier.rb +13 -0
  41. data/lib/croods/resource/json_schema.rb +32 -0
  42. data/lib/croods/resource/json_schema/definition.rb +34 -0
  43. data/lib/croods/resource/json_schema/definitions.rb +31 -0
  44. data/lib/croods/resource/json_schema/initial_schema.json +8 -0
  45. data/lib/croods/resource/json_schema/links.rb +40 -0
  46. data/lib/croods/resource/json_schema/links/collection.rb +70 -0
  47. data/lib/croods/resource/json_schema/links/create.rb +41 -0
  48. data/lib/croods/resource/json_schema/links/destroy.rb +40 -0
  49. data/lib/croods/resource/json_schema/links/index.rb +17 -0
  50. data/lib/croods/resource/json_schema/links/member.rb +40 -0
  51. data/lib/croods/resource/json_schema/links/show.rb +40 -0
  52. data/lib/croods/resource/json_schema/links/update.rb +40 -0
  53. data/lib/croods/resource/json_schema/properties.rb +32 -0
  54. data/lib/croods/resource/json_schema/required.rb +34 -0
  55. data/lib/croods/resource/model.rb +35 -0
  56. data/lib/croods/resource/names.rb +23 -0
  57. data/lib/croods/resource/paths.rb +16 -0
  58. data/lib/croods/resource/policy.rb +51 -0
  59. data/lib/croods/resource/services.rb +18 -0
  60. data/lib/croods/resource/sorting.rb +13 -0
  61. data/lib/croods/routes.rb +46 -0
  62. data/lib/croods/service.rb +28 -0
  63. data/lib/croods/version.rb +5 -0
  64. data/lib/tasks/croods_tasks.rake +4 -0
  65. 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,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Croods
4
+ class Controller < ActionController::API
5
+ module Model
6
+ protected
7
+
8
+ def model_name
9
+ resource_name.singularize
10
+ end
11
+
12
+ def model
13
+ model_name.constantize
14
+ end
15
+ end
16
+ end
17
+ 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
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Croods
6
+ module Middleware
7
+ class RequestValidation < Base
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Croods
6
+ module Middleware
7
+ class ResponseValidation < Base
8
+ end
9
+ end
10
+ end
@@ -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