scimitar 1.10.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 +13 -41
  4. data/app/controllers/scimitar/resource_types_controller.rb +2 -0
  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 +2 -2
  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: db2ddb3c77cd7a482505003624275950458638efef053abe9df427b644412de4
4
- data.tar.gz: f2bc178baf10fd8cfc995114ae7dad80f822a604f9b016ae0f16cea5e1abbc9f
3
+ metadata.gz: 95a2166cc921a400959f9d8d4398f6bf8ecb772f8d7a0a0a73950892e85d808a
4
+ data.tar.gz: cdf5aab3812f10f69c96304e738a150f4208850267527b66d36eeb99548d7b1f
5
5
  SHA512:
6
- metadata.gz: 945f6073d1fa337eab6ef82644402a7c977858db3289af7480970fadd047c4ef6a55ed8f6d2756265a84c794ca957f1c83d83ecbad07d999355c23dd0cf2cbfa
7
- data.tar.gz: '0869c64c2c49128005a7163814a3ccb32e0c99209ad2a867b4c24a87c9b95e8bbb7cb39fc92bdec5c01a9d45c4316ea25c33ed3fd347e500bea02fe62922df2a'
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
@@ -25,11 +25,10 @@ module Scimitar
25
25
  #
26
26
  # ...to "globally" invoke this handler if you wish.
27
27
  #
28
- # +exception+:: Exception instance, used for a configured error reporter
29
- # via #handle_scim_error (if present).
28
+ # +_exception+:: Exception instance (currently unused).
30
29
  #
31
- def handle_resource_not_found(exception)
32
- handle_scim_error(NotFoundError.new(params[:id]), exception)
30
+ def handle_resource_not_found(_exception)
31
+ handle_scim_error(NotFoundError.new(params[:id]))
33
32
  end
34
33
 
35
34
  # This base controller uses:
@@ -39,22 +38,9 @@ module Scimitar
39
38
  # ...to "globally" invoke this handler for all Scimitar errors (including
40
39
  # subclasses).
41
40
  #
42
- # Mandatory parameters are:
43
- #
44
41
  # +error_response+:: Scimitar::ErrorResponse (or subclass) instance.
45
42
  #
46
- # Optional parameters are:
47
- #
48
- # *exception+:: If a Ruby exception was the reason this method is being
49
- # called, pass it here. Any configured exception reporting
50
- # mechanism will be invokved with the given parameter.
51
- # Otherwise, the +error_response+ value is reported.
52
- #
53
- def handle_scim_error(error_response, exception = error_response)
54
- unless Scimitar.engine_configuration.exception_reporter.nil?
55
- Scimitar.engine_configuration.exception_reporter.call(exception)
56
- end
57
-
43
+ def handle_scim_error(error_response)
58
44
  render json: error_response, status: error_response.status
59
45
  end
60
46
 
@@ -69,7 +55,7 @@ module Scimitar
69
55
  # +exception+:: Exception instance.
70
56
  #
71
57
  def handle_bad_json_error(exception)
72
- 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}"))
73
59
  end
74
60
 
75
61
  # This base controller uses:
@@ -82,7 +68,7 @@ module Scimitar
82
68
  #
83
69
  def handle_unexpected_error(exception)
84
70
  Rails.logger.error("#{exception.message}\n#{exception.backtrace}")
85
- handle_scim_error(ErrorResponse.new(status: 500, detail: exception.message), exception)
71
+ handle_scim_error(ErrorResponse.new(status: 500, detail: exception.message))
86
72
  end
87
73
 
88
74
  # =========================================================================
@@ -96,17 +82,12 @@ module Scimitar
96
82
  # request and subclass processing.
97
83
  #
98
84
  def require_scim
99
- scim_mime_type = Mime::Type.lookup_by_extension(:scim).to_s
100
-
101
- if request.media_type.nil? || request.media_type.empty?
102
- request.format = :scim
103
- request.headers['CONTENT_TYPE'] = scim_mime_type
104
- elsif request.media_type.downcase == scim_mime_type
85
+ if request.content_type&.downcase == Mime::Type.lookup_by_extension(:scim).to_s
105
86
  request.format = :scim
106
87
  elsif request.format == :scim
107
- request.headers['CONTENT_TYPE'] = scim_mime_type
88
+ request.headers['CONTENT_TYPE'] = Mime::Type.lookup_by_extension(:scim).to_s
108
89
  else
109
- 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."))
110
91
  end
111
92
  end
112
93
 
@@ -124,13 +105,8 @@ module Scimitar
124
105
  #
125
106
  # https://stackoverflow.com/questions/10239970/what-is-the-delimiter-for-www-authenticate-for-multiple-schemes
126
107
  #
127
- response.set_header('WWW-Authenticate', 'Basic' ) if Scimitar.engine_configuration.basic_authenticator.present?
128
- response.set_header('WWW-Authenticate', 'Bearer') if Scimitar.engine_configuration.token_authenticator.present?
129
-
130
- # No matter what a caller might request via headers, the only content
131
- # type we can ever respond with is JSON-for-SCIM.
132
- #
133
- 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?
134
110
  end
135
111
 
136
112
  def authenticate
@@ -139,15 +115,11 @@ module Scimitar
139
115
 
140
116
  def authenticated?
141
117
  result = if Scimitar.engine_configuration.basic_authenticator.present?
142
- authenticate_with_http_basic do |username, password|
143
- instance_exec(username, password, &Scimitar.engine_configuration.basic_authenticator)
144
- end
118
+ authenticate_with_http_basic(&Scimitar.engine_configuration.basic_authenticator)
145
119
  end
146
120
 
147
121
  result ||= if Scimitar.engine_configuration.token_authenticator.present?
148
- authenticate_with_http_token do |token, options|
149
- instance_exec(token, options, &Scimitar.engine_configuration.token_authenticator)
150
- end
122
+ authenticate_with_http_token(&Scimitar.engine_configuration.token_authenticator)
151
123
  end
152
124
 
153
125
  return result
@@ -1,3 +1,5 @@
1
+ require_dependency "scimitar/application_controller"
2
+
1
3
  module Scimitar
2
4
  class ResourceTypesController < ApplicationController
3
5
  def index
@@ -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