scimitar 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (106) hide show
  1. checksums.yaml +7 -0
  2. data/Rakefile +16 -0
  3. data/app/controllers/scimitar/active_record_backed_resources_controller.rb +180 -0
  4. data/app/controllers/scimitar/application_controller.rb +129 -0
  5. data/app/controllers/scimitar/resource_types_controller.rb +28 -0
  6. data/app/controllers/scimitar/resources_controller.rb +203 -0
  7. data/app/controllers/scimitar/schemas_controller.rb +16 -0
  8. data/app/controllers/scimitar/service_provider_configurations_controller.rb +8 -0
  9. data/app/models/scimitar/authentication_error.rb +9 -0
  10. data/app/models/scimitar/authentication_scheme.rb +18 -0
  11. data/app/models/scimitar/bulk.rb +8 -0
  12. data/app/models/scimitar/complex_types/address.rb +18 -0
  13. data/app/models/scimitar/complex_types/base.rb +41 -0
  14. data/app/models/scimitar/complex_types/email.rb +12 -0
  15. data/app/models/scimitar/complex_types/entitlement.rb +12 -0
  16. data/app/models/scimitar/complex_types/ims.rb +12 -0
  17. data/app/models/scimitar/complex_types/name.rb +12 -0
  18. data/app/models/scimitar/complex_types/phone_number.rb +12 -0
  19. data/app/models/scimitar/complex_types/photo.rb +12 -0
  20. data/app/models/scimitar/complex_types/reference_group.rb +12 -0
  21. data/app/models/scimitar/complex_types/reference_member.rb +12 -0
  22. data/app/models/scimitar/complex_types/role.rb +12 -0
  23. data/app/models/scimitar/complex_types/x509_certificate.rb +12 -0
  24. data/app/models/scimitar/engine_configuration.rb +24 -0
  25. data/app/models/scimitar/error_response.rb +20 -0
  26. data/app/models/scimitar/errors.rb +14 -0
  27. data/app/models/scimitar/filter.rb +11 -0
  28. data/app/models/scimitar/filter_error.rb +22 -0
  29. data/app/models/scimitar/invalid_syntax_error.rb +9 -0
  30. data/app/models/scimitar/lists/count.rb +64 -0
  31. data/app/models/scimitar/lists/query_parser.rb +730 -0
  32. data/app/models/scimitar/meta.rb +7 -0
  33. data/app/models/scimitar/not_found_error.rb +10 -0
  34. data/app/models/scimitar/resource_invalid_error.rb +9 -0
  35. data/app/models/scimitar/resource_type.rb +29 -0
  36. data/app/models/scimitar/resources/base.rb +159 -0
  37. data/app/models/scimitar/resources/group.rb +13 -0
  38. data/app/models/scimitar/resources/mixin.rb +964 -0
  39. data/app/models/scimitar/resources/user.rb +13 -0
  40. data/app/models/scimitar/schema/address.rb +24 -0
  41. data/app/models/scimitar/schema/attribute.rb +123 -0
  42. data/app/models/scimitar/schema/base.rb +86 -0
  43. data/app/models/scimitar/schema/derived_attributes.rb +24 -0
  44. data/app/models/scimitar/schema/email.rb +10 -0
  45. data/app/models/scimitar/schema/entitlement.rb +10 -0
  46. data/app/models/scimitar/schema/group.rb +27 -0
  47. data/app/models/scimitar/schema/ims.rb +10 -0
  48. data/app/models/scimitar/schema/name.rb +20 -0
  49. data/app/models/scimitar/schema/phone_number.rb +10 -0
  50. data/app/models/scimitar/schema/photo.rb +10 -0
  51. data/app/models/scimitar/schema/reference_group.rb +23 -0
  52. data/app/models/scimitar/schema/reference_member.rb +21 -0
  53. data/app/models/scimitar/schema/role.rb +10 -0
  54. data/app/models/scimitar/schema/user.rb +52 -0
  55. data/app/models/scimitar/schema/vdtp.rb +18 -0
  56. data/app/models/scimitar/schema/x509_certificate.rb +22 -0
  57. data/app/models/scimitar/service_provider_configuration.rb +49 -0
  58. data/app/models/scimitar/supportable.rb +14 -0
  59. data/app/views/layouts/scimitar/application.html.erb +14 -0
  60. data/config/initializers/scimitar.rb +82 -0
  61. data/config/routes.rb +6 -0
  62. data/lib/scimitar.rb +23 -0
  63. data/lib/scimitar/engine.rb +63 -0
  64. data/lib/scimitar/version.rb +13 -0
  65. data/spec/apps/dummy/app/controllers/custom_destroy_mock_users_controller.rb +24 -0
  66. data/spec/apps/dummy/app/controllers/custom_request_verifiers_controller.rb +30 -0
  67. data/spec/apps/dummy/app/controllers/mock_groups_controller.rb +13 -0
  68. data/spec/apps/dummy/app/controllers/mock_users_controller.rb +13 -0
  69. data/spec/apps/dummy/app/models/mock_group.rb +83 -0
  70. data/spec/apps/dummy/app/models/mock_user.rb +104 -0
  71. data/spec/apps/dummy/config/application.rb +17 -0
  72. data/spec/apps/dummy/config/boot.rb +2 -0
  73. data/spec/apps/dummy/config/environment.rb +2 -0
  74. data/spec/apps/dummy/config/environments/test.rb +15 -0
  75. data/spec/apps/dummy/config/initializers/cookies_serializer.rb +3 -0
  76. data/spec/apps/dummy/config/initializers/scimitar.rb +14 -0
  77. data/spec/apps/dummy/config/initializers/session_store.rb +3 -0
  78. data/spec/apps/dummy/config/routes.rb +24 -0
  79. data/spec/apps/dummy/db/migrate/20210304014602_create_mock_users.rb +15 -0
  80. data/spec/apps/dummy/db/migrate/20210308020313_create_mock_groups.rb +10 -0
  81. data/spec/apps/dummy/db/migrate/20210308044214_create_join_table_mock_groups_mock_users.rb +8 -0
  82. data/spec/apps/dummy/db/schema.rb +42 -0
  83. data/spec/controllers/scimitar/application_controller_spec.rb +173 -0
  84. data/spec/controllers/scimitar/resource_types_controller_spec.rb +94 -0
  85. data/spec/controllers/scimitar/resources_controller_spec.rb +247 -0
  86. data/spec/controllers/scimitar/schemas_controller_spec.rb +75 -0
  87. data/spec/controllers/scimitar/service_provider_configurations_controller_spec.rb +22 -0
  88. data/spec/models/scimitar/complex_types/address_spec.rb +19 -0
  89. data/spec/models/scimitar/complex_types/email_spec.rb +23 -0
  90. data/spec/models/scimitar/lists/count_spec.rb +147 -0
  91. data/spec/models/scimitar/lists/query_parser_spec.rb +763 -0
  92. data/spec/models/scimitar/resource_type_spec.rb +21 -0
  93. data/spec/models/scimitar/resources/base_spec.rb +289 -0
  94. data/spec/models/scimitar/resources/base_validation_spec.rb +61 -0
  95. data/spec/models/scimitar/resources/mixin_spec.rb +2127 -0
  96. data/spec/models/scimitar/resources/user_spec.rb +55 -0
  97. data/spec/models/scimitar/schema/attribute_spec.rb +80 -0
  98. data/spec/models/scimitar/schema/base_spec.rb +64 -0
  99. data/spec/models/scimitar/schema/group_spec.rb +87 -0
  100. data/spec/models/scimitar/schema/user_spec.rb +710 -0
  101. data/spec/requests/active_record_backed_resources_controller_spec.rb +569 -0
  102. data/spec/requests/application_controller_spec.rb +49 -0
  103. data/spec/requests/controller_configuration_spec.rb +17 -0
  104. data/spec/requests/engine_spec.rb +20 -0
  105. data/spec/spec_helper.rb +66 -0
  106. metadata +315 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d7c0cb7d5ff4346954d9aab0d83ae22fcd75fcc456b9be54d74e22334cf4e273
4
+ data.tar.gz: 47adaec88f0418f18294fb19374be773cbfafa6c94b8870e38a349f70a4c53b4
5
+ SHA512:
6
+ metadata.gz: 59a7f529e86667e14de8a6c0a0b0bc9a26cd53b21b53656be8b2f37d31e4cddcc1d0fccdefe2ff695d31baf19764489903a59e61f31e863e78c544f5dd24d550
7
+ data.tar.gz: '09d32ab29c325fa047f9e77daababa2bc763b79ee97e3b0b961202b60901b3ace406f5f66f63fd5c56b015a86e368294d4ec1e4b98b3588e7f4b75c72280e9e3'
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ require 'rake'
2
+ require 'rspec/core/rake_task'
3
+ require 'rdoc/task'
4
+ require 'sdoc'
5
+
6
+ RSpec::Core::RakeTask.new(:default) do | t |
7
+ end
8
+
9
+ Rake::RDocTask.new do | rd |
10
+ rd.rdoc_files.include('README.md', 'lib/**/*.rb', 'app/**/*.rb')
11
+
12
+ rd.title = 'Scimitar'
13
+ rd.main = 'README.md'
14
+ rd.rdoc_dir = 'docs/rdoc'
15
+ rd.generator = 'sdoc'
16
+ end
@@ -0,0 +1,180 @@
1
+ require_dependency "scimitar/application_controller"
2
+
3
+ module Scimitar
4
+
5
+ # An ActiveRecord-centric subclass of Scimitar::ResourcesController. See that
6
+ # class's documentation first, as it describes things that your subclass must
7
+ # do which apply equally to subclasses of this ActiveRecord-focused code.
8
+ #
9
+ # In addition to requirements mentioned above, your subclass MUST override
10
+ # protected method #storage_scope, returning an ActiveRecord::Relation which
11
+ # is used as a starting scope for any 'index' (list) views. This gives you an
12
+ # opportunity to apply things like is-active filters, apply soft deletion
13
+ # scopes, apply security scopes and so-on. For example:
14
+ #
15
+ # protected
16
+ # def storage_scope
17
+ # self.storage_class().where(is_deleted: false)
18
+ # end
19
+ #
20
+ class ActiveRecordBackedResourcesController < ResourcesController
21
+
22
+ rescue_from ActiveRecord::RecordNotFound, with: :handle_resource_not_found # See Scimitar::ApplicationController
23
+
24
+ # GET (list)
25
+ #
26
+ def index
27
+ query = if params[:filter].blank?
28
+ self.storage_scope()
29
+ else
30
+ attribute_map = storage_class().new.scim_queryable_attributes()
31
+ parser = ::Scimitar::Lists::QueryParser.new(attribute_map)
32
+
33
+ parser.parse(params[:filter])
34
+ parser.to_activerecord_query(self.storage_scope())
35
+ end
36
+
37
+ pagination_info = scim_pagination_info(query.count())
38
+
39
+ page_of_results = query
40
+ .offset(pagination_info.offset)
41
+ .limit(pagination_info.limit)
42
+ .to_a()
43
+
44
+ super(pagination_info, page_of_results) do | record |
45
+ record.to_scim(location: url_for(action: :show, id: record.id))
46
+ end
47
+ end
48
+
49
+ # GET/id (show)
50
+ #
51
+ def show
52
+ super do |record_id|
53
+ record = self.find_record(record_id)
54
+ record.to_scim(location: url_for(action: :show, id: record_id))
55
+ end
56
+ end
57
+
58
+ # POST (create)
59
+ #
60
+ def create
61
+ super do |scim_resource|
62
+ self.storage_class().transaction do
63
+ record = self.storage_class().new
64
+ record.from_scim!(scim_hash: scim_resource.as_json())
65
+ self.save!(record)
66
+ record.to_scim(location: url_for(action: :show, id: record.id))
67
+ end
68
+ end
69
+ end
70
+
71
+ # PUT (replace)
72
+ #
73
+ def replace
74
+ super do |record_id, scim_resource|
75
+ self.storage_class().transaction do
76
+ record = self.find_record(record_id)
77
+ record.from_scim!(scim_hash: scim_resource.as_json())
78
+ self.save!(record)
79
+ record.to_scim(location: url_for(action: :show, id: record.id))
80
+ end
81
+ end
82
+ end
83
+
84
+ # PATCH (update)
85
+ #
86
+ def update
87
+ super do |record_id, patch_hash|
88
+ self.storage_class().transaction do
89
+ record = self.find_record(record_id)
90
+ record.from_scim_patch!(patch_hash: patch_hash)
91
+ self.save!(record)
92
+ record.to_scim(location: url_for(action: :show, id: record.id))
93
+ end
94
+ end
95
+ end
96
+
97
+ # DELETE (remove)
98
+ #
99
+ # Deletion methods can vary quite a lot with ActiveRecord objects. If you
100
+ # just let this superclass handle things, it'll call:
101
+ #
102
+ # https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-destroy-21
103
+ #
104
+ # ...i.e. the standard delete-record-with-callbacks method. If you pass
105
+ # a block, then this block is invoked and passed the ActiveRecord model
106
+ # instance to be destroyed. You can then do things like soft-deletions,
107
+ # updating an "active" flag, perform audit-related operations and so-on.
108
+ #
109
+ def destroy(&block)
110
+ super do |record_id|
111
+ record = self.find_record(record_id)
112
+
113
+ if block_given?
114
+ yield(record)
115
+ else
116
+ record.destroy!
117
+ end
118
+ end
119
+ end
120
+
121
+ # =========================================================================
122
+ # PROTECTED INSTANCE METHODS
123
+ # =========================================================================
124
+ #
125
+ protected
126
+
127
+ # Return an ActiveRecord::Relation used as the starting scope for #index
128
+ # lists and any 'find by ID' operation.
129
+ #
130
+ def storage_scope
131
+ raise NotImplementedError
132
+ end
133
+
134
+ # Find a RIP user record. Subclasses can override this if they need
135
+ # special lookup behaviour.
136
+ #
137
+ # +record_id+:: Record ID (SCIM schema 'id' value - "our" ID).
138
+ #
139
+ def find_record(record_id)
140
+ self.storage_scope().find(record_id)
141
+ end
142
+
143
+ # Save a record, dealing with validation exceptions by raising SCIM
144
+ # errors.
145
+ #
146
+ # +record+:: ActiveRecord subclass to save (via #save!).
147
+ #
148
+ # The return value is not used internally, making life easier for
149
+ # overriding subclasses to "do the right thing" / avoid mistakes (instead
150
+ # of e.g. requiring that a to-SCIM representation of 'record' is returned
151
+ # and relying upon this to generate correct response payloads - an early
152
+ # version of the gem did this and it caused a confusing subclass bug).
153
+ #
154
+ def save!(record)
155
+ record.save!
156
+
157
+ rescue ActiveRecord::RecordInvalid => exception
158
+ joined_errors = record.errors.full_messages.join('; ')
159
+
160
+ # https://tools.ietf.org/html/rfc7644#page-12
161
+ #
162
+ # If the service provider determines that the creation of the requested
163
+ # resource conflicts with existing resources (e.g., a "User" resource
164
+ # with a duplicate "userName"), the service provider MUST return HTTP
165
+ # status code 409 (Conflict) with a "scimType" error code of
166
+ # "uniqueness"
167
+ #
168
+ if record.errors.any? { | e | e.type == :taken }
169
+ raise Scimitar::ErrorResponse.new(
170
+ status: 409,
171
+ scimType: 'uniqueness',
172
+ detail: joined_errors
173
+ )
174
+ else
175
+ raise Scimitar::ResourceInvalidError.new(joined_errors)
176
+ end
177
+ end
178
+
179
+ end
180
+ end
@@ -0,0 +1,129 @@
1
+ module Scimitar
2
+ class ApplicationController < ActionController::Base
3
+
4
+ rescue_from StandardError, with: :handle_unexpected_error
5
+ rescue_from ActionDispatch::Http::Parameters::ParseError, with: :handle_bad_json_error # Via "ActionDispatch::Request.parameter_parsers" block in lib/scimitar/engine.rb
6
+ rescue_from Scimitar::ErrorResponse, with: :handle_scim_error
7
+
8
+ before_action :require_scim
9
+ before_action :add_mandatory_response_headers
10
+ before_action :authenticate
11
+
12
+ if Scimitar.engine_configuration.application_controller_mixin
13
+ include Scimitar.engine_configuration.application_controller_mixin
14
+ end
15
+
16
+ # =========================================================================
17
+ # PROTECTED INSTANCE METHODS
18
+ # =========================================================================
19
+ #
20
+ protected
21
+
22
+ # You can use:
23
+ #
24
+ # rescue_from SomeException, with: :handle_resource_not_found
25
+ #
26
+ # ...to "globally" invoke this handler if you wish.
27
+ #
28
+ # +_exception+:: Exception instance (currently unused).
29
+ #
30
+ def handle_resource_not_found(_exception)
31
+ handle_scim_error(NotFoundError.new(params[:id]))
32
+ end
33
+
34
+ # This base controller uses:
35
+ #
36
+ # rescue_from Scimitar::ErrorResponse, with: :handle_scim_error
37
+ #
38
+ # ...to "globally" invoke this handler for all Scimitar errors (including
39
+ # subclasses).
40
+ #
41
+ # +error_response+:: Scimitar::ErrorResponse (or subclass) instance.
42
+ #
43
+ def handle_scim_error(error_response)
44
+ render json: error_response, status: error_response.status
45
+ end
46
+
47
+ # This base controller uses:
48
+ #
49
+ # rescue_from ActionDispatch::Http::Parameters::ParseError, with: :handle_bad_json_error
50
+ #
51
+ # ...to "globally" handle JSON errors implied by parse errors raised via
52
+ # the "ActionDispatch::Request.parameter_parsers" block in
53
+ # lib/scimitar/engine.rb.
54
+ #
55
+ # +exception+:: Exception instance.
56
+ #
57
+ def handle_bad_json_error(exception)
58
+ handle_scim_error(ErrorResponse.new(status: 400, detail: "Invalid JSON - #{exception.message}"))
59
+ end
60
+
61
+ # This base controller uses:
62
+ #
63
+ # rescue_from StandardError, with: :handle_unexpected_error
64
+ #
65
+ # ...to "globally" handle 500-style cases with a SCIM response.
66
+ #
67
+ # +exception+:: Exception instance.
68
+ #
69
+ def handle_unexpected_error(exception)
70
+ Rails.logger.error("#{exception.message}\n#{exception.backtrace}")
71
+ handle_scim_error(ErrorResponse.new(status: 500, detail: exception.message))
72
+ end
73
+
74
+ # =========================================================================
75
+ # PRIVATE INSTANCE METHODS
76
+ # =========================================================================
77
+ #
78
+ private
79
+
80
+ # Tries to be permissive in what it receives - ".scim" extensions or a
81
+ # Content-Type header (or both) lead to both being set up for the inbound
82
+ # request and subclass processing.
83
+ #
84
+ def require_scim
85
+ if request.content_type&.downcase == Mime::Type.lookup_by_extension(:scim).to_s
86
+ request.format = :scim
87
+ elsif request.format == :scim
88
+ request.headers['CONTENT_TYPE'] = Mime::Type.lookup_by_extension(:scim).to_s
89
+ else
90
+ handle_scim_error(ErrorResponse.new(status: 406, detail: "Only #{Mime::Type.lookup_by_extension(:scim)} type is accepted."))
91
+ end
92
+ end
93
+
94
+ def add_mandatory_response_headers
95
+
96
+ # https://tools.ietf.org/html/rfc7644#section-2
97
+ #
98
+ # "...a SCIM service provider SHALL indicate supported HTTP
99
+ # authentication schemes via the "WWW-Authenticate" header."
100
+ #
101
+ # Rack may not handle an attempt to set two instances of the header and
102
+ # there is much debate on how to specify multiple methods in one header
103
+ # so we just let Token override Basic (since Token is much stronger, or
104
+ # at least has the potential to do so) if that's how Rack handles it.
105
+ #
106
+ # https://stackoverflow.com/questions/10239970/what-is-the-delimiter-for-www-authenticate-for-multiple-schemes
107
+ #
108
+ response.set_header('WWW_AUTHENTICATE', 'Basic' ) if Scimitar.engine_configuration.basic_authenticator.present?
109
+ response.set_header('WWW_AUTHENTICATE', 'Bearer') if Scimitar.engine_configuration.token_authenticator.present?
110
+ end
111
+
112
+ def authenticate
113
+ handle_scim_error(Scimitar::AuthenticationError.new) unless authenticated?
114
+ end
115
+
116
+ def authenticated?
117
+ result = if Scimitar.engine_configuration.basic_authenticator.present?
118
+ authenticate_with_http_basic(&Scimitar.engine_configuration.basic_authenticator)
119
+ end
120
+
121
+ result ||= if Scimitar.engine_configuration.token_authenticator.present?
122
+ authenticate_with_http_token(&Scimitar.engine_configuration.token_authenticator)
123
+ end
124
+
125
+ return result
126
+ end
127
+
128
+ end
129
+ end
@@ -0,0 +1,28 @@
1
+ require_dependency "scimitar/application_controller"
2
+
3
+ module Scimitar
4
+ class ResourceTypesController < ApplicationController
5
+ def index
6
+ resource_types = Scimitar::Engine.resources.map do |resource|
7
+ resource.resource_type(scim_resource_type_url(name: resource.resource_type_id))
8
+ end
9
+
10
+ render json: resource_types
11
+ end
12
+
13
+ def show
14
+ resource_types = Scimitar::Engine.resources.reduce({}) do |hash, resource|
15
+ hash[resource.resource_type_id] = resource.resource_type(scim_resource_type_url(name: resource.resource_type_id))
16
+ hash
17
+ end
18
+
19
+ resource_type = resource_types[params[:name]]
20
+
21
+ if resource_type.nil?
22
+ raise Scimitar::NotFoundError.new(params[:name])
23
+ else
24
+ render json: resource_type
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,203 @@
1
+ require_dependency "scimitar/application_controller"
2
+
3
+ module Scimitar
4
+
5
+ # A Rails controller which is mostly idiomatic, with #index, #show, #create
6
+ # and #destroy methods mapping to the conventional HTTP methods in Rails.
7
+ # The #update method is used for partial-update PATCH calls, while the
8
+ # #replace method is used for whole-update PUT calls.
9
+ #
10
+ # Subclass this controller to deal with resource-specific API calls, for
11
+ # endpoints such as Users and Groups. Any one controller is assumed to be
12
+ # related to one class in your application which has mixed in
13
+ # Scimitar::Resources::Mixin. Your subclass MUST override protected method
14
+ # #storage_class, which returns that class. For example, if you had a class
15
+ # User with the mixin included, then:
16
+ #
17
+ # protected
18
+ # def storage_class
19
+ # User
20
+ # end
21
+ #
22
+ # ...is sufficient.
23
+ #
24
+ # The controller makes no assumptions about storage method - it does not have
25
+ # any ActiveRecord specialisations, for example. If you do use ActiveRecord,
26
+ # consider subclassing Scimitar::ActiveRecordBackedResourcesController
27
+ # instead as it does most of the mapping, persistence and error handling work
28
+ # for you.
29
+ #
30
+ class ResourcesController < ApplicationController
31
+
32
+ # GET (list)
33
+ #
34
+ # Pass a Scimitar::Lists::Count object providing pagination data along with
35
+ # a page of results in "your" data domain as an Enumerable, along with a
36
+ # block. Renders as "list" result by calling your block with each of the
37
+ # results, allowing you to use something like
38
+ # Scimitar::Resources::Mixin#to_scim to convert to a SCIM representation.
39
+ #
40
+ # +pagination_info+:: A Scimitar::Lists::Count instance with #total set.
41
+ # See e.g. protected method #scim_pagination_info to
42
+ # assist with this.
43
+ #
44
+ # +page_of_results+:: An Enumerable single page of results.
45
+ #
46
+ def index(pagination_info, page_of_results, &block)
47
+ render(json: {
48
+ schemas: [
49
+ 'urn:ietf:params:scim:api:messages:2.0:ListResponse'
50
+ ],
51
+ totalResults: pagination_info.total,
52
+ startIndex: pagination_info.start_index,
53
+ itemsPerPage: pagination_info.limit,
54
+ Resources: page_of_results.map(&block)
55
+ })
56
+ end
57
+
58
+ # GET/id (show)
59
+ #
60
+ # Call with a block that is passed an ID to find in "your" domain. Evaluate
61
+ # to the SCIM representation of the arising found record.
62
+ #
63
+ def show(&block)
64
+ scim_resource = yield(self.safe_params()[:id])
65
+ render(json: scim_resource)
66
+ end
67
+
68
+ # POST (create)
69
+ #
70
+ # Call with a block that is passed a SCIM resource instance - e.g a
71
+ # Scimitar::Resources::User instance - representing an item to be created.
72
+ # Your ::storage_class class's ::scim_resource_type method determines the
73
+ # kind of object you'll be given.
74
+ #
75
+ # See also e.g. Scimitar::Resources::Mixin#from_scim!.
76
+ #
77
+ # Evaluate to the SCIM representation of the arising created record.
78
+ #
79
+ def create(&block)
80
+ with_scim_resource() do |resource|
81
+ render(json: yield(resource, :create), status: :created)
82
+ end
83
+ end
84
+
85
+ # PUT (replace)
86
+ #
87
+ # Similar to #create, but you're passed an ID to find as well as the
88
+ # resource details to then use for all replacement attributes in that found
89
+ # resource. See also e.g. Scimitar::Resources::Mixin#from_scim!.
90
+ #
91
+ # Evaluate to the SCIM representation of the arising created record.
92
+ #
93
+ def replace(&block)
94
+ with_scim_resource() do |resource|
95
+ render(json: yield(self.safe_params()[:id], resource))
96
+ end
97
+ end
98
+
99
+ # PATCH (update)
100
+ #
101
+ # A variant of #create where you're again passed the resource ID (in "your"
102
+ # domain) to look up, but then a Hash with patch operation details from the
103
+ # calling client. This can be passed to e.g.
104
+ # Scimitar::Resources::Mixin#from_scim_patch!.
105
+ #
106
+ # Evaluate to the SCIM representation of the arising created record.
107
+ #
108
+ def update(&block)
109
+ validate_request()
110
+
111
+ # Params includes all of the PATCH data at the top level along with other
112
+ # other Rails-injected params like 'id', 'action', 'controller'. These
113
+ # are harmless given no namespace collision and we're only interested in
114
+ # the 'Operations' key for the actual patch data.
115
+ #
116
+ render(json: yield(self.safe_params()[:id], self.safe_params().to_hash()))
117
+ end
118
+
119
+ # DELETE (remove)
120
+ #
121
+ def destroy
122
+ if yield(self.safe_params()[:id]) != false
123
+ head :no_content
124
+ else
125
+ raise ErrorResponse.new(
126
+ status: 500,
127
+ detail: "Failed to delete the resource with id '#{params[:id]}'. Please try again later."
128
+ )
129
+ end
130
+ end
131
+
132
+ # =========================================================================
133
+ # PROTECTED INSTANCE METHODS
134
+ # =========================================================================
135
+ #
136
+ protected
137
+
138
+ # The class including Scimitar::Resources::Mixin which declares mappings
139
+ # to the entity you return in #resource_type.
140
+ #
141
+ def storage_class
142
+ raise NotImplementedError
143
+ end
144
+
145
+ # For #index actions, returns a Scimitar::Lists::Count instance which can
146
+ # be used to access offset-vs-start-index (0-indexed or 1-indexed),
147
+ # per-page limit and also holds the total number-of-items count which you
148
+ # can optionally pass up-front here, or set via #total= later.
149
+ #
150
+ # +total_count+:: Optional integer total record count across all pages,
151
+ # else must be set later - BEFORE passing an instance to
152
+ # the #index implementation in this class.
153
+ #
154
+ def scim_pagination_info(total_count = nil)
155
+ ::Scimitar::Lists::Count.new(
156
+ start_index: params[:startIndex],
157
+ limit: params[:count] || Scimitar.service_provider_configuration(location: nil).filter.maxResults,
158
+ total: total_count
159
+ )
160
+ end
161
+
162
+ # =========================================================================
163
+ # PRIVATE INSTANCE METHODS
164
+ # =========================================================================
165
+ #
166
+ private
167
+
168
+ def validate_request
169
+ if request.raw_post.blank?
170
+ raise Scimitar::ErrorResponse.new(status: 400, detail: 'must provide a request body')
171
+ end
172
+ end
173
+
174
+ def with_scim_resource
175
+ validate_request()
176
+
177
+ resource_type = storage_class().scim_resource_type() # See Scimitar::Resources::Mixin
178
+ resource = resource_type.new(self.safe_params().to_h)
179
+
180
+ if resource.valid?
181
+ yield(resource)
182
+ else
183
+ raise Scimitar::ErrorResponse.new(
184
+ status: 400,
185
+ detail: "Invalid resource: #{resource.errors.full_messages.join(', ')}.",
186
+ scimType: 'invalidValue'
187
+ )
188
+ end
189
+
190
+ # Gem bugs aside - if this happens, we couldn't create "resource"; bad
191
+ # (or unsupported) attributes encountered in inbound payload data.
192
+ #
193
+ rescue NoMethodError => exception
194
+ Rails.logger.error("#{exception.message}\n#{exception.backtrace}")
195
+ raise Scimitar::ErrorResponse.new(status: 400, detail: 'Invalid request')
196
+ end
197
+
198
+ def safe_params
199
+ params.permit!
200
+ end
201
+
202
+ end
203
+ end