scimitar 1.10.0 → 2.0.0

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