scimitar 1.0.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 (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