scimitar 1.11.0 → 2.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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/scimitar/active_record_backed_resources_controller.rb +23 -98
  3. data/app/controllers/scimitar/application_controller.rb +17 -44
  4. data/app/controllers/scimitar/resource_types_controller.rb +3 -7
  5. data/app/controllers/scimitar/resources_controller.rb +2 -0
  6. data/app/controllers/scimitar/schemas_controller.rb +3 -366
  7. data/app/controllers/scimitar/service_provider_configurations_controller.rb +1 -0
  8. data/app/models/scimitar/complex_types/address.rb +6 -0
  9. data/app/models/scimitar/engine_configuration.rb +5 -15
  10. data/app/models/scimitar/error_response.rb +0 -12
  11. data/app/models/scimitar/lists/query_parser.rb +13 -113
  12. data/app/models/scimitar/resource_invalid_error.rb +1 -1
  13. data/app/models/scimitar/resources/base.rb +9 -53
  14. data/app/models/scimitar/resources/mixin.rb +59 -646
  15. data/app/models/scimitar/schema/address.rb +0 -1
  16. data/app/models/scimitar/schema/attribute.rb +5 -14
  17. data/app/models/scimitar/schema/base.rb +1 -1
  18. data/app/models/scimitar/schema/name.rb +2 -2
  19. data/app/models/scimitar/schema/user.rb +10 -10
  20. data/app/models/scimitar/schema/vdtp.rb +1 -1
  21. data/app/models/scimitar/service_provider_configuration.rb +3 -14
  22. data/config/initializers/scimitar.rb +3 -69
  23. data/lib/scimitar/engine.rb +12 -57
  24. data/lib/scimitar/support/hash_with_indifferent_case_insensitive_access.rb +10 -140
  25. data/lib/scimitar/version.rb +2 -2
  26. data/lib/scimitar.rb +2 -7
  27. data/spec/apps/dummy/app/controllers/mock_groups_controller.rb +1 -1
  28. data/spec/apps/dummy/app/models/mock_group.rb +1 -1
  29. data/spec/apps/dummy/app/models/mock_user.rb +9 -52
  30. data/spec/apps/dummy/config/application.rb +1 -0
  31. data/spec/apps/dummy/config/environments/test.rb +28 -5
  32. data/spec/apps/dummy/config/initializers/scimitar.rb +10 -90
  33. data/spec/apps/dummy/config/routes.rb +7 -28
  34. data/spec/apps/dummy/db/migrate/20210304014602_create_mock_users.rb +1 -11
  35. data/spec/apps/dummy/db/migrate/20210308044214_create_join_table_mock_groups_mock_users.rb +3 -8
  36. data/spec/apps/dummy/db/schema.rb +4 -12
  37. data/spec/controllers/scimitar/application_controller_spec.rb +3 -126
  38. data/spec/controllers/scimitar/resource_types_controller_spec.rb +4 -8
  39. data/spec/controllers/scimitar/schemas_controller_spec.rb +48 -344
  40. data/spec/models/scimitar/complex_types/address_spec.rb +4 -3
  41. data/spec/models/scimitar/complex_types/email_spec.rb +2 -0
  42. data/spec/models/scimitar/lists/query_parser_spec.rb +9 -146
  43. data/spec/models/scimitar/resources/base_spec.rb +71 -217
  44. data/spec/models/scimitar/resources/base_validation_spec.rb +5 -43
  45. data/spec/models/scimitar/resources/mixin_spec.rb +129 -1508
  46. data/spec/models/scimitar/schema/attribute_spec.rb +3 -22
  47. data/spec/models/scimitar/schema/base_spec.rb +1 -1
  48. data/spec/models/scimitar/schema/user_spec.rb +2 -12
  49. data/spec/requests/active_record_backed_resources_controller_spec.rb +66 -1016
  50. data/spec/requests/application_controller_spec.rb +3 -16
  51. data/spec/requests/engine_spec.rb +0 -75
  52. data/spec/spec_helper.rb +1 -9
  53. data/spec/support/hash_with_indifferent_case_insensitive_access_spec.rb +0 -108
  54. metadata +26 -37
  55. data/LICENSE.txt +0 -21
  56. data/README.md +0 -717
  57. data/lib/scimitar/support/utilities.rb +0 -111
  58. data/spec/apps/dummy/app/controllers/custom_create_mock_users_controller.rb +0 -25
  59. data/spec/apps/dummy/app/controllers/custom_replace_mock_users_controller.rb +0 -25
  60. data/spec/apps/dummy/app/controllers/custom_save_mock_users_controller.rb +0 -24
  61. data/spec/apps/dummy/app/controllers/custom_update_mock_users_controller.rb +0 -25
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5860adcfd6c793922be88ef8bb7bb878571e8fcd34ea89ead9d0278f09582cff
4
- data.tar.gz: 2f7bf25e40156c0f006255c0c015f6302c8de39fc1634be789dba462d43f8c4a
3
+ metadata.gz: 95a2166cc921a400959f9d8d4398f6bf8ecb772f8d7a0a0a73950892e85d808a
4
+ data.tar.gz: cdf5aab3812f10f69c96304e738a150f4208850267527b66d36eeb99548d7b1f
5
5
  SHA512:
6
- metadata.gz: 8047867c9365db25571693526e765d1a1a6b6d9cd17278f4bca2037815b5f9e0b9945390cc5ffba0228b9fabd3b15deae253519bbbe74ada2c262718d1c54989
7
- data.tar.gz: 6ef777d96c932404c1ae0209804010fee98e993ce289d4eac09e0c8c3395b0de390fb423611d599fb6750a5d46e34545e0685e9563441fcc2d582fb5b6d1618f
6
+ metadata.gz: f0925517599b107e44fd93db9be142aebe608892c2c5069c50d22b353c51238290710474b062a002fdb010be3d783c4dea3b314f72f47b4aca3c2385a8fc1377
7
+ data.tar.gz: eef6eebfc64bb2d4adabfca110f26e3a6c9e227f47387da6fb384925899ddf0ae260cf68176f24040a6bb356cf34f6576c920bd44264d4a1fee415aeadc237e6
@@ -1,3 +1,5 @@
1
+ require_dependency "scimitar/application_controller"
2
+
1
3
  module Scimitar
2
4
 
3
5
  # An ActiveRecord-centric subclass of Scimitar::ResourcesController. See that
@@ -17,9 +19,7 @@ module Scimitar
17
19
  #
18
20
  class ActiveRecordBackedResourcesController < ResourcesController
19
21
 
20
- rescue_from 'ActiveRecord::RecordNotFound', with: :handle_resource_not_found # See Scimitar::ApplicationController
21
-
22
- before_action :obtain_id_column_name_from_attribute_map
22
+ rescue_from ActiveRecord::RecordNotFound, with: :handle_resource_not_found # See Scimitar::ApplicationController
23
23
 
24
24
  # GET (list)
25
25
  #
@@ -37,13 +37,12 @@ module Scimitar
37
37
  pagination_info = scim_pagination_info(query.count())
38
38
 
39
39
  page_of_results = query
40
- .order(@id_column => :asc)
41
40
  .offset(pagination_info.offset)
42
41
  .limit(pagination_info.limit)
43
42
  .to_a()
44
43
 
45
44
  super(pagination_info, page_of_results) do | record |
46
- record_to_scim(record)
45
+ record.to_scim(location: url_for(action: :show, id: record.id))
47
46
  end
48
47
  end
49
48
 
@@ -52,61 +51,45 @@ module Scimitar
52
51
  def show
53
52
  super do |record_id|
54
53
  record = self.find_record(record_id)
55
- record_to_scim(record)
54
+ record.to_scim(location: url_for(action: :show, id: record_id))
56
55
  end
57
56
  end
58
57
 
59
58
  # POST (create)
60
59
  #
61
- # Calls #save! on the new record if no block is given, else invokes the
62
- # block, passing it the new ActiveRecord model instance to be saved. It
63
- # is up to the block to make any further changes and persist the record.
64
- #
65
- # Blocks are invoked from within a wrapping database transaction.
66
- # ActiveRecord::RecordInvalid exceptions are handled for you, rendering
67
- # an appropriate SCIM error.
68
- #
69
- def create(&block)
60
+ def create
70
61
  super do |scim_resource|
71
62
  self.storage_class().transaction do
72
63
  record = self.storage_class().new
73
64
  record.from_scim!(scim_hash: scim_resource.as_json())
74
- self.save!(record, &block)
75
- record_to_scim(record)
65
+ self.save!(record)
66
+ record.to_scim(location: url_for(action: :show, id: record.id))
76
67
  end
77
68
  end
78
69
  end
79
70
 
80
71
  # PUT (replace)
81
72
  #
82
- # Calls #save! on the updated record if no block is given, else invokes the
83
- # block, passing the updated record which the block must persist, with the
84
- # same rules as for #create.
85
- #
86
- def replace(&block)
73
+ def replace
87
74
  super do |record_id, scim_resource|
88
75
  self.storage_class().transaction do
89
76
  record = self.find_record(record_id)
90
77
  record.from_scim!(scim_hash: scim_resource.as_json())
91
- self.save!(record, &block)
92
- record_to_scim(record)
78
+ self.save!(record)
79
+ record.to_scim(location: url_for(action: :show, id: record.id))
93
80
  end
94
81
  end
95
82
  end
96
83
 
97
84
  # PATCH (update)
98
85
  #
99
- # Calls #save! on the updated record if no block is given, else invokes the
100
- # block, passing the updated record which the block must persist, with the
101
- # same rules as for #create.
102
- #
103
- def update(&block)
86
+ def update
104
87
  super do |record_id, patch_hash|
105
88
  self.storage_class().transaction do
106
89
  record = self.find_record(record_id)
107
90
  record.from_scim_patch!(patch_hash: patch_hash)
108
- self.save!(record, &block)
109
- record_to_scim(record)
91
+ self.save!(record)
92
+ record.to_scim(location: url_for(action: :show, id: record.id))
110
93
  end
111
94
  end
112
95
  end
@@ -148,46 +131,19 @@ module Scimitar
148
131
  raise NotImplementedError
149
132
  end
150
133
 
151
- # Return an Array of exceptions that #save! can rescue and handle with a
152
- # SCIM error automatically.
153
- #
154
- def scimitar_rescuable_exceptions
155
- [
156
- ActiveRecord::RecordInvalid,
157
- ActiveRecord::RecordNotSaved,
158
- ActiveRecord::RecordNotUnique,
159
- ]
160
- end
161
-
162
134
  # Find a record by ID. Subclasses can override this if they need special
163
135
  # lookup behaviour.
164
136
  #
165
137
  # +record_id+:: Record ID (SCIM schema 'id' value - "our" ID).
166
138
  #
167
139
  def find_record(record_id)
168
- self.storage_scope().find_by!(@id_column => record_id)
169
- end
170
-
171
- # DRY up controller actions - pass a record; returns the SCIM
172
- # representation, with a "show" location specified via #url_for.
173
- #
174
- def record_to_scim(record)
175
- record.to_scim(
176
- location: url_for(action: :show, id: record.send(@id_column)),
177
- include_attributes: params.fetch(:attributes, "").split(",")
178
- )
140
+ self.storage_scope().find(record_id)
179
141
  end
180
142
 
181
143
  # Save a record, dealing with validation exceptions by raising SCIM
182
144
  # errors.
183
145
  #
184
- # +record+:: ActiveRecord subclass to save.
185
- #
186
- # If you just let this superclass handle things, it'll call the standard
187
- # +#save!+ method on the record. If you pass a block, then this block is
188
- # invoked and passed the ActiveRecord model instance to be saved. You can
189
- # then do things like calling a different method, using a service object
190
- # of some kind, perform audit-related operations and so-on.
146
+ # +record+:: ActiveRecord subclass to save (via #save!).
191
147
  #
192
148
  # The return value is not used internally, making life easier for
193
149
  # overriding subclasses to "do the right thing" / avoid mistakes (instead
@@ -195,31 +151,11 @@ module Scimitar
195
151
  # and relying upon this to generate correct response payloads - an early
196
152
  # version of the gem did this and it caused a confusing subclass bug).
197
153
  #
198
- def save!(record, &block)
199
- if block_given?
200
- yield(record)
201
- else
202
- record.save!
203
- end
204
- rescue *self.scimitar_rescuable_exceptions() => exception
205
- handle_on_save_exception(record, exception)
206
- end
154
+ def save!(record)
155
+ record.save!
207
156
 
208
- # Deal with exceptions related to errors upon saving, by responding with
209
- # an appropriate SCIM error. This is most effective if the record has
210
- # validation errors defined, but falls back to the provided exception's
211
- # message otherwise.
212
- #
213
- # +record+:: The record that provoked the exception. Mandatory.
214
- # +exception+:: The exception that was raised. If omitted, a default of
215
- # 'Unknown', in English with no I18n, is used.
216
- #
217
- def handle_on_save_exception(record, exception = RuntimeError.new('Unknown'))
218
- details = if record.errors.present?
219
- record.errors.full_messages.join('; ')
220
- else
221
- exception.message
222
- end
157
+ rescue ActiveRecord::RecordInvalid => exception
158
+ joined_errors = record.errors.full_messages.join('; ')
223
159
 
224
160
  # https://tools.ietf.org/html/rfc7644#page-12
225
161
  #
@@ -229,27 +165,16 @@ module Scimitar
229
165
  # status code 409 (Conflict) with a "scimType" error code of
230
166
  # "uniqueness"
231
167
  #
232
- if exception.is_a?(ActiveRecord::RecordNotUnique) || record.errors.any? { | e | e.type == :taken }
168
+ if record.errors.any? { | e | e.type == :taken }
233
169
  raise Scimitar::ErrorResponse.new(
234
170
  status: 409,
235
171
  scimType: 'uniqueness',
236
- detail: "Operation failed due to a uniqueness constraint: #{details}"
172
+ detail: joined_errors
237
173
  )
238
174
  else
239
- raise Scimitar::ResourceInvalidError.new(details)
175
+ raise Scimitar::ResourceInvalidError.new(joined_errors)
240
176
  end
241
177
  end
242
178
 
243
- # Called via +before_action+ - stores in @id_column the name of whatever
244
- # model column is used to store the record ID, via
245
- # Scimitar::Resources::Mixin::scim_attributes_map.
246
- #
247
- # Default is <tt>:id</tt>.
248
- #
249
- def obtain_id_column_name_from_attribute_map
250
- attrs = storage_class().scim_attributes_map() || {}
251
- @id_column = attrs[:id] || :id
252
- end
253
-
254
179
  end
255
180
  end
@@ -9,6 +9,10 @@ module Scimitar
9
9
  before_action :add_mandatory_response_headers
10
10
  before_action :authenticate
11
11
 
12
+ if Scimitar.engine_configuration.application_controller_mixin
13
+ include Scimitar.engine_configuration.application_controller_mixin
14
+ end
15
+
12
16
  # =========================================================================
13
17
  # PROTECTED INSTANCE METHODS
14
18
  # =========================================================================
@@ -21,11 +25,10 @@ module Scimitar
21
25
  #
22
26
  # ...to "globally" invoke this handler if you wish.
23
27
  #
24
- # +exception+:: Exception instance, used for a configured error reporter
25
- # via #handle_scim_error (if present).
28
+ # +_exception+:: Exception instance (currently unused).
26
29
  #
27
- def handle_resource_not_found(exception)
28
- handle_scim_error(NotFoundError.new(params[:id]), exception)
30
+ def handle_resource_not_found(_exception)
31
+ handle_scim_error(NotFoundError.new(params[:id]))
29
32
  end
30
33
 
31
34
  # This base controller uses:
@@ -35,22 +38,9 @@ module Scimitar
35
38
  # ...to "globally" invoke this handler for all Scimitar errors (including
36
39
  # subclasses).
37
40
  #
38
- # Mandatory parameters are:
39
- #
40
41
  # +error_response+:: Scimitar::ErrorResponse (or subclass) instance.
41
42
  #
42
- # Optional parameters are:
43
- #
44
- # *exception+:: If a Ruby exception was the reason this method is being
45
- # called, pass it here. Any configured exception reporting
46
- # mechanism will be invokved with the given parameter.
47
- # Otherwise, the +error_response+ value is reported.
48
- #
49
- def handle_scim_error(error_response, exception = error_response)
50
- unless Scimitar.engine_configuration.exception_reporter.nil?
51
- Scimitar.engine_configuration.exception_reporter.call(exception)
52
- end
53
-
43
+ def handle_scim_error(error_response)
54
44
  render json: error_response, status: error_response.status
55
45
  end
56
46
 
@@ -65,7 +55,7 @@ module Scimitar
65
55
  # +exception+:: Exception instance.
66
56
  #
67
57
  def handle_bad_json_error(exception)
68
- handle_scim_error(ErrorResponse.new(status: 400, detail: "Invalid JSON - #{exception.message}"), exception)
58
+ handle_scim_error(ErrorResponse.new(status: 400, detail: "Invalid JSON - #{exception.message}"))
69
59
  end
70
60
 
71
61
  # This base controller uses:
@@ -78,7 +68,7 @@ module Scimitar
78
68
  #
79
69
  def handle_unexpected_error(exception)
80
70
  Rails.logger.error("#{exception.message}\n#{exception.backtrace}")
81
- handle_scim_error(ErrorResponse.new(status: 500, detail: exception.message), exception)
71
+ handle_scim_error(ErrorResponse.new(status: 500, detail: exception.message))
82
72
  end
83
73
 
84
74
  # =========================================================================
@@ -92,17 +82,12 @@ module Scimitar
92
82
  # request and subclass processing.
93
83
  #
94
84
  def require_scim
95
- scim_mime_type = Mime::Type.lookup_by_extension(:scim).to_s
96
-
97
- if request.media_type.nil? || request.media_type.empty?
98
- request.format = :scim
99
- request.headers['CONTENT_TYPE'] = scim_mime_type
100
- elsif request.media_type.downcase == scim_mime_type
85
+ if request.content_type&.downcase == Mime::Type.lookup_by_extension(:scim).to_s
101
86
  request.format = :scim
102
87
  elsif request.format == :scim
103
- request.headers['CONTENT_TYPE'] = scim_mime_type
88
+ request.headers['CONTENT_TYPE'] = Mime::Type.lookup_by_extension(:scim).to_s
104
89
  else
105
- handle_scim_error(ErrorResponse.new(status: 406, detail: "Only #{scim_mime_type} type is accepted."))
90
+ handle_scim_error(ErrorResponse.new(status: 406, detail: "Only #{Mime::Type.lookup_by_extension(:scim)} type is accepted."))
106
91
  end
107
92
  end
108
93
 
@@ -120,13 +105,8 @@ module Scimitar
120
105
  #
121
106
  # https://stackoverflow.com/questions/10239970/what-is-the-delimiter-for-www-authenticate-for-multiple-schemes
122
107
  #
123
- response.set_header('WWW-Authenticate', 'Basic' ) if Scimitar.engine_configuration.basic_authenticator.present?
124
- response.set_header('WWW-Authenticate', 'Bearer') if Scimitar.engine_configuration.token_authenticator.present?
125
-
126
- # No matter what a caller might request via headers, the only content
127
- # type we can ever respond with is JSON-for-SCIM.
128
- #
129
- response.set_header('Content-Type', "#{Mime::Type.lookup_by_extension(:scim)}; charset=utf-8")
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?
130
110
  end
131
111
 
132
112
  def authenticate
@@ -135,22 +115,15 @@ module Scimitar
135
115
 
136
116
  def authenticated?
137
117
  result = if Scimitar.engine_configuration.basic_authenticator.present?
138
- authenticate_with_http_basic do |username, password|
139
- instance_exec(username, password, &Scimitar.engine_configuration.basic_authenticator)
140
- end
118
+ authenticate_with_http_basic(&Scimitar.engine_configuration.basic_authenticator)
141
119
  end
142
120
 
143
121
  result ||= if Scimitar.engine_configuration.token_authenticator.present?
144
- authenticate_with_http_token do |token, options|
145
- instance_exec(token, options, &Scimitar.engine_configuration.token_authenticator)
146
- end
122
+ authenticate_with_http_token(&Scimitar.engine_configuration.token_authenticator)
147
123
  end
148
124
 
149
125
  return result
150
126
  end
151
127
 
152
- if Scimitar.engine_configuration.application_controller_mixin
153
- include Scimitar.engine_configuration.application_controller_mixin
154
- end
155
128
  end
156
129
  end
@@ -1,3 +1,5 @@
1
+ require_dependency "scimitar/application_controller"
2
+
1
3
  module Scimitar
2
4
  class ResourceTypesController < ApplicationController
3
5
  def index
@@ -5,13 +7,7 @@ module Scimitar
5
7
  resource.resource_type(scim_resource_type_url(name: resource.resource_type_id))
6
8
  end
7
9
 
8
- render json: {
9
- schemas: [
10
- 'urn:ietf:params:scim:api:messages:2.0:ListResponse'
11
- ],
12
- totalResults: resource_types.size,
13
- Resources: resource_types
14
- }
10
+ render json: resource_types
15
11
  end
16
12
 
17
13
  def show
@@ -1,3 +1,5 @@
1
+ require_dependency "scimitar/application_controller"
2
+
1
3
  module Scimitar
2
4
 
3
5
  # A Rails controller which is mostly idiomatic, with #index, #show, #create