scimitar 1.11.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 +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