croods 0.1.0

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