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
@@ -1,379 +1,16 @@
1
+ require_dependency "scimitar/application_controller"
2
+
1
3
  module Scimitar
2
4
  class SchemasController < ApplicationController
3
5
  def index
4
6
  schemas = Scimitar::Engine.schemas
5
-
6
- schemas.each do |schema|
7
- schema.meta.location = scim_schemas_url(name: schema.id)
8
- end
9
-
10
7
  schemas_by_id = schemas.reduce({}) do |hash, schema|
11
8
  hash[schema.id] = schema
12
9
  hash
13
10
  end
14
11
 
15
- list = if params.key?(:name)
16
- [ schemas_by_id[params[:name]] ]
17
- else
18
- schemas
19
- end
20
-
21
- # Now we either have a simple render method, or a complex one.
22
- #
23
- schemas_to_render = if Scimitar.engine_configuration.schema_list_from_attribute_mappings.empty?
24
- list
25
- else
26
- self.redraw_schema_list_using_mappings(list)
27
- end
28
-
29
- render(json: {
30
- schemas: [
31
- 'urn:ietf:params:scim:api:messages:2.0:ListResponse'
32
- ],
33
- totalResults: schemas_to_render.size,
34
- startIndex: 1,
35
- itemsPerPage: schemas_to_render.size,
36
- Resources: schemas_to_render
37
- })
12
+ render json: schemas_by_id[params[:name]] || schemas
38
13
  end
39
14
 
40
- # =========================================================================
41
- # PRIVATE INSTANCE METHODS
42
- # =========================================================================
43
- #
44
- private
45
-
46
- # Given a list of schema *instances*, find all Scimitar::Resources::Mixin
47
- # inclusions to obtain classes with a ::scim_resource_type implementation
48
- # that is invoked to get at the associated Scimitar resource class; each
49
- # resource class then describes the schema it uses; the list is filtered
50
- # to include only those found schemas. Then, for each, use the discovered
51
- # class' attribute maps to walk the schema attribute tree alongside the
52
- # map and render only mapped attributes. This is done via calling down to
53
- # ::redraw_schema_using_mappings for each remaining schema in the list.
54
- #
55
- # An array of new schema data is returned.
56
- #
57
- # +list+:: Array of schema instances to examine.
58
- #
59
- def redraw_schema_list_using_mappings(list)
60
-
61
- # Iterate over the configured model classes to build a mapping from
62
- # Scimitar schema class to an array of one or more model classes that
63
- # seem to use it. This is to detect the error condition wherein some
64
- # schema gets used more than once, leading to multiple possible
65
- # attribute map choices.
66
- #
67
- classes_using_scimitar_mixin = Scimitar.engine_configuration.schema_list_from_attribute_mappings
68
- schema_to_resource_map = {}
69
-
70
- classes_using_scimitar_mixin.each do | model_class |
71
- resource_class = model_class.scim_resource_type()
72
- schemas = resource_class.extended_schemas + [resource_class.schema]
73
-
74
- schemas.each do | schema_class |
75
- schema_to_resource_map[schema_class] ||= []
76
- schema_to_resource_map[schema_class] << model_class
77
- end
78
- end
79
-
80
- # Take the schema list and map to rewritten versions based on finding
81
- # out which of the above resource classes use a given schema and then
82
- # walking this schema's attribute tree while comparing against the
83
- # resource class's attribute map. Unmapped attributes are removed. The
84
- # reality of resource class attribute mutability might give different
85
- # answers for the corresponding schema attribute's mutability; for any
86
- # custom schema we'd expect a match, but for core schema where local
87
- # resources don't quite work to spec, at least the /Schemas endpoint
88
- # can try to reflect reality and aid auto-discovery.
89
- #
90
- redrawn_list = list.map do | schema_instance |
91
- resource_classes_using_schema = schema_to_resource_map[schema_instance.class]
92
-
93
- if resource_classes_using_schema.nil?
94
- next # NOTE EARLY LOOP RESTART (schema not used by a resource)
95
- elsif resource_classes_using_schema.size > 1
96
- raise "Cannot infer attribute map for engine configuration 'schema_list_from_attribute_mappings: true' because multiple resource classes use schema '#{schema_instance.class.name}': #{resource_classes_using_schema.map(&:name).join(', ')}"
97
- end
98
-
99
- found_class = classes_using_scimitar_mixin.find do | class_using_scimitar_mixin |
100
- resource_class = class_using_scimitar_mixin.scim_resource_type()
101
-
102
- resource_class.schema == schema_instance.class ||
103
- resource_class.extended_schemas.include?(schema_instance.class)
104
- end
105
-
106
- rebuilt_schema_instance = if found_class
107
- redraw_schema_using_mappings(
108
- original_schema_instance: schema_instance,
109
- instance_including_mixin: found_class.new
110
- )
111
- else
112
- nil
113
- end
114
-
115
- rebuilt_schema_instance
116
- end
117
-
118
- redrawn_list.compact!
119
- redrawn_list
120
- end
121
-
122
- # "Redraw" a schema, by walking its attribute tree alongside a related
123
- # resource class's attribute map. Only mapped attributes are included.
124
- # The mapped model is checked for a read accessor and write ability is
125
- # determined via Scimitar::Resources::Mixin#scim_mutable_attributes. This
126
- # gives the actual read/write ability of the mapped attribute; if the
127
- # schema's declared mutability differs, the *most restrictive* is chosen.
128
- # For example, if the schema says read-write but the mapped model only
129
- # has read ability, then "readOnly" is used. Conversely, if the schema
130
- # says read-only but the mapped model has read-write, the schema's
131
- # "readOnly" is chosen instead as the source of truth.
132
- #
133
- # See the implementation's comments for a table describing exactly how
134
- # all mutability conflict cases are resolved.
135
- #
136
- # The returned schema instance may be a full or partial duplicate of the
137
- # one given on input - some or all attributes and/or sub-attributes may
138
- # have been duplicated due to e.g. mutability differences. Do not assume
139
- # or rely upon this as a caller.
140
- #
141
- # Mandatory named parameters for external callers are:
142
- #
143
- # +original_schema_instance+:: The Scimitar::Schema::Base subclass
144
- # schema *instance* that is to be examined
145
- # and possibly "redrawn".
146
- #
147
- # +instance_including_mixin+:: Instance of the model class including
148
- # Scimitar::Resources::Mixin, providing
149
- # the attribute map to be examined.
150
- #
151
- # Named parameters used internally for recursive calls are:
152
- #
153
- # +scim_attributes_map+:: The fragment of the attribute map found
154
- # from +instance_including_mixin+'s class
155
- # initially, which is relevant to the
156
- # current recursion level. E.g. it might
157
- # be the sub-attributes map of "name",
158
- # for things like a "familyName" mapping.
159
- #
160
- # +schema_attributes+:: An array of schema attributes for the
161
- # current recursion level, corresponding
162
- # to +scim_attributes_map+.
163
- #
164
- # +rebuilt_attribute_array+:: Redrawn schema attributes are collected
165
- # into this array, which is altered in
166
- # place. It is usually a 'subAttributes'
167
- # property of a schema attribute that's
168
- # provoked recursion in order to examine
169
- # and rebuild its sub-attributes directly.
170
- #
171
- def redraw_schema_using_mappings(
172
- original_schema_instance:,
173
- instance_including_mixin:,
174
-
175
- scim_attributes_map: nil,
176
- schema_attributes: nil,
177
- rebuilt_attribute_array: nil
178
- )
179
- schema_attributes ||= original_schema_instance.scim_attributes
180
- scim_attributes_map ||= instance_including_mixin
181
- .class
182
- .scim_attributes_map()
183
- .with_indifferent_case_insensitive_access()
184
-
185
- rebuilt_schema_instance = nil
186
-
187
- if rebuilt_attribute_array.nil?
188
- rebuilt_schema_instance = self.duplicate_attribute(original_schema_instance)
189
- rebuilt_schema_instance.scim_attributes = []
190
- rebuilt_attribute_array = rebuilt_schema_instance.scim_attributes
191
- end
192
-
193
- schema_attributes.each do | schema_attribute |
194
- if schema_attribute.multiValued && schema_attribute.subAttributes&.any?
195
- mapped_multivalue_attribute = scim_attributes_map[schema_attribute.name]
196
-
197
- # We expect either an array in the attribute map to correspond with
198
- # a multivalued schema attribute, or nothing. If we get some other
199
- # non-Array, not-nil thing, it's just ignored.
200
- #
201
- if mapped_multivalue_attribute.is_a?(Array)
202
-
203
- # A single-entry array with "list using" semantics, for a
204
- # collection of an artbirary number of same-class items - e.g.
205
- # Groups to which a User belongs.
206
- #
207
- # If this is an up-to-date mapping, there's a "class" entry that
208
- # tells us what the collection is compromised of. If not, then we
209
- # check for ActiveRecord collections as a fallback and if that is
210
- # the case here, can use reflection to try and find the class. If
211
- # all else fails, we drop to generic schema for the collection.
212
- #
213
- if mapped_multivalue_attribute.first&.dig(:list)
214
- associated_resource_class = mapped_multivalue_attribute.first[:class]
215
-
216
- if (
217
- associated_resource_class.nil? &&
218
- instance_including_mixin.is_a?(ActiveRecord::Base)
219
- )
220
- associated_resource_class = instance_including_mixin
221
- .class
222
- .reflect_on_association(mapped_multivalue_attribute.first[:list])
223
- &.klass
224
- end
225
-
226
- if associated_resource_class.nil? || ! associated_resource_class.include?(Scimitar::Resources::Mixin)
227
- rebuilt_attribute_array << schema_attribute
228
- else
229
- rebuilt_schema_attribute = self.duplicate_attribute(schema_attribute)
230
- rebuilt_schema_attribute.subAttributes = []
231
- rebuilt_attribute_array << rebuilt_schema_attribute
232
-
233
- redraw_schema_using_mappings(
234
- original_schema_instance: original_schema_instance,
235
- instance_including_mixin: associated_resource_class.new,
236
- scim_attributes_map: mapped_multivalue_attribute.first[:using],
237
- schema_attributes: schema_attribute.subAttributes,
238
- rebuilt_attribute_array: rebuilt_schema_attribute.subAttributes
239
- )
240
- end
241
-
242
- # A one-or-more entry array with "match with" semantics, to match
243
- # discrete mapped items with a particular value in a particular
244
- # field - e.g. an e-mail of type "work" mapping the SCIM "value"
245
- # to a local attribute of "work_email_address".
246
- #
247
- # Mutability or supported attributes here might vary per matched
248
- # type. There's no way for SCIM schema to represent that so we
249
- # just merge all the "using" mappings together, in order of array
250
- # appearance, and have that combined attribute map treated as the
251
- # data the schema response will use.
252
- #
253
- elsif mapped_multivalue_attribute.first&.dig(:match)
254
- union_of_mappings = {}
255
-
256
- mapped_multivalue_attribute.each do | mapped_multivalue_attribute_description |
257
- union_of_mappings.merge!(mapped_multivalue_attribute_description[:using])
258
- end
259
-
260
- rebuilt_schema_attribute = self.duplicate_attribute(schema_attribute)
261
- rebuilt_schema_attribute.subAttributes = []
262
- rebuilt_attribute_array << rebuilt_schema_attribute
263
-
264
- redraw_schema_using_mappings(
265
- original_schema_instance: original_schema_instance,
266
- instance_including_mixin: instance_including_mixin,
267
- scim_attributes_map: union_of_mappings,
268
- schema_attributes: schema_attribute.subAttributes,
269
- rebuilt_attribute_array: rebuilt_schema_attribute.subAttributes
270
- )
271
- end
272
- end
273
-
274
- elsif schema_attribute.subAttributes&.any?
275
- mapped_subattributes = scim_attributes_map[schema_attribute.name]
276
-
277
- if mapped_subattributes.is_a?(Hash)
278
- rebuilt_schema_attribute = self.duplicate_attribute(schema_attribute)
279
- rebuilt_schema_attribute.subAttributes = []
280
- rebuilt_attribute_array << rebuilt_schema_attribute
281
-
282
- redraw_schema_using_mappings(
283
- original_schema_instance: original_schema_instance,
284
- instance_including_mixin: instance_including_mixin,
285
- scim_attributes_map: mapped_subattributes,
286
- schema_attributes: schema_attribute.subAttributes,
287
- rebuilt_attribute_array: rebuilt_schema_attribute.subAttributes
288
- )
289
- end
290
-
291
- else
292
- mapped_attribute = scim_attributes_map[schema_attribute.name]
293
-
294
- unless mapped_attribute.nil?
295
- rebuilt_schema_attribute = self.duplicate_attribute(schema_attribute)
296
- has_mapped_reader = true
297
- has_mapped_writer = false
298
-
299
- if mapped_attribute.is_a?(String) || mapped_attribute.is_a?(Symbol)
300
- has_mapped_reader = instance_including_mixin.respond_to?(mapped_attribute)
301
- has_mapped_writer = instance_including_mixin.scim_mutable_attributes().include?(mapped_attribute.to_sym)
302
- end
303
-
304
- # The schema is taken as the primary source of truth, leading to
305
- # a matrix of "do we override it or not?" based on who is the
306
- # more limited. When both have the same mutability there is no
307
- # more work to do, so we just need to consider differences:
308
- #
309
- # Actual class support Schema says Result
310
- # =============================================================
311
- # readWrite readOnly readOnly (schema wins)
312
- # readWrite writeOnly writeOnly (schema wins)
313
- # readOnly readWrite readOnly (class wins)
314
- # writeOnly readWrite writeOnly (class wins)
315
- #
316
- # Those cases are easy. But there are gnarly cases too, where we
317
- # have no good answer and the class's mapped implementation is in
318
- # essence broken compared to the schema. Since it is not useful
319
- # to insist on the schema's not-reality version, the class wins.
320
- #
321
- # Actual class support Schema says Result
322
- # ====================== =======================================
323
- # readOnly writeOnly readOnly (class "wins")
324
- # writeOnly readOnly writeOnly (class "wins")
325
-
326
- schema_attribute_mutability = schema_attribute.mutability.downcase
327
-
328
- if has_mapped_reader && has_mapped_writer
329
- #
330
- # Read-write Nothing to do. Schema always "wins" by matching or
331
- # being more restrictive than the class's actual abilities.
332
-
333
- elsif has_mapped_reader && ! has_mapped_writer
334
- #
335
- # Read-only. Class is more restrictive if schema is 'readWrite'
336
- # or if there's the broken clash of schema 'writeOnly'.
337
- #
338
- if schema_attribute_mutability == 'readwrite' || schema_attribute_mutability == 'writeonly'
339
- rebuilt_schema_attribute.mutability = 'readOnly'
340
- end
341
-
342
- elsif has_mapped_writer && ! has_mapped_reader
343
- #
344
- # Opposite to the above case.
345
- #
346
- if schema_attribute_mutability == 'readwrite' || schema_attribute_mutability == 'readonly'
347
- rebuilt_schema_attribute.mutability = 'writeOnly'
348
- end
349
-
350
- # ...else we cannot fathom how this class works - it appears to
351
- # have no read or write accessor for the mapped attribute. Keep
352
- # the schema's declaration as-is.
353
- #
354
- end
355
-
356
- rebuilt_attribute_array << rebuilt_schema_attribute
357
- end
358
- end
359
- end
360
-
361
- return rebuilt_schema_instance # (meaningless except for topmost call)
362
- end
363
-
364
- # Small helper that duplicates Scimitar::Schema::Attribute instances, but
365
- # then removes their 'errors' collection which otherwise gets initialised
366
- # to an empty value and is rendered as if part of the schema (which isn't
367
- # a valid entry in a SCIM schema representation).
368
- #
369
- # +schema_attribute+:: Scimitar::Schema::Attribute to be duplicated.
370
- # A renderable duplicate is returned.
371
- #
372
- def duplicate_attribute(schema_attribute)
373
- duplicated_schema_attribute = schema_attribute.dup()
374
- duplicated_schema_attribute.remove_instance_variable('@errors')
375
- duplicated_schema_attribute
376
- end
377
-
378
15
  end
379
16
  end
@@ -1,3 +1,4 @@
1
+ require_dependency "scimitar/application_controller"
1
2
  module Scimitar
2
3
  class ServiceProviderConfigurationsController < ApplicationController
3
4
  def show
@@ -7,6 +7,12 @@ module Scimitar
7
7
  #
8
8
  class Address < Base
9
9
  set_schema Scimitar::Schema::Address
10
+
11
+ # Returns the JSON representation of an Address.
12
+ #
13
+ def as_json(options = {})
14
+ {'type' => 'work'}.merge(super(options))
15
+ end
10
16
  end
11
17
  end
12
18
  end
@@ -7,25 +7,15 @@ module Scimitar
7
7
  class EngineConfiguration
8
8
  include ActiveModel::Model
9
9
 
10
- attr_accessor(
11
- :uses_defaults,
12
- :basic_authenticator,
13
- :token_authenticator,
14
- :application_controller_mixin,
15
- :exception_reporter,
16
- :optional_value_fields_required,
17
- :schema_list_from_attribute_mappings,
18
- )
10
+ attr_accessor :basic_authenticator,
11
+ :token_authenticator,
12
+ :application_controller_mixin
19
13
 
20
14
  def initialize(attributes = {})
21
- @uses_defaults = attributes.empty?
22
15
 
23
- # Set defaults that may be overridden by the initializer.
16
+ # No defaults yet - reserved for future use.
24
17
  #
25
- defaults = {
26
- optional_value_fields_required: true,
27
- schema_list_from_attribute_mappings: []
28
- }
18
+ defaults = {}
29
19
 
30
20
  super(defaults.merge(attributes))
31
21
  end
@@ -16,17 +16,5 @@ module Scimitar
16
16
  data['scimType'] = scimType if scimType
17
17
  data
18
18
  end
19
-
20
- # Originally Scimitar used attribute "detail" for exception text; it was
21
- # only for JSON responses at the time, but in hindsight was a bad choice.
22
- # It should have been "message" given inheritance from StandardError, which
23
- # then works properly with e.g. error reporting services.
24
- #
25
- # The "detail" attribute is still present, for backwards compatibility with
26
- # any client code that might be using this class.
27
- #
28
- def message
29
- self.detail
30
- end
31
19
  end
32
20
  end
@@ -59,18 +59,13 @@ module Scimitar
59
59
  #
60
60
  BINARY_OPERATORS = Set.new(OPERATORS.keys.reject { |op| UNARY_OPERATORS.include?(op) }).freeze
61
61
 
62
- # Precompiled expression that matches a valid attribute name according to
63
- # https://tools.ietf.org/html/rfc7643#section-2.1.
64
- #
65
- ATTRNAME = /[[:alpha:]][[:alnum:]$-_]*/
66
-
67
62
  # =======================================================================
68
63
  # Tokenizing expressions
69
64
  # =======================================================================
70
65
 
71
66
  PAREN = /[\(\)]/.freeze
72
67
  STR = /"(?:\\"|[^"])*"/.freeze
73
- OP = /(?:#{OPERATORS.keys.join('|')})\b/i.freeze
68
+ OP = /#{OPERATORS.keys.join('|')}/i.freeze
74
69
  WORD = /[\w\.]+/.freeze
75
70
  SEP = /\s?/.freeze
76
71
  NEXT_TOKEN = /\A(#{PAREN}|#{STR}|#{OP}|#{WORD})#{SEP}/.freeze
@@ -83,7 +78,7 @@ module Scimitar
83
78
  # method's return value here.
84
79
  #
85
80
  def initialize(attribute_map)
86
- @attribute_map = attribute_map.with_indifferent_case_insensitive_access()
81
+ @attribute_map = attribute_map
87
82
  end
88
83
 
89
84
  # Parse SCIM filter query into RPN stack
@@ -197,7 +192,7 @@ module Scimitar
197
192
 
198
193
  ast.push(self.start_group? ? self.parse_group() : self.pop())
199
194
 
200
- if ast.last.is_a?(String) && !UNARY_OPERATORS.include?(ast.last.downcase) || ast.last.is_a?(Array)
195
+ unless ! ast.last.is_a?(String) || UNARY_OPERATORS.include?(ast.last.downcase)
201
196
  expect_op ^= true
202
197
  end
203
198
  end
@@ -296,10 +291,6 @@ module Scimitar
296
291
  # the part before the "[" as a prefix - "emails[type" to "emails.type",
297
292
  # with similar substitutions therein.
298
293
  #
299
- # Further, via https://github.com/RIPAGlobal/scimitar/issues/115 we see
300
- # a requirement to support a broken form emitted by Microsoft; that is
301
- # supported herein.
302
- #
303
294
  # This method tries to flatten things thus. It throws exceptions if any
304
295
  # problems arise at all. Some limitations:
305
296
  #
@@ -323,9 +314,6 @@ module Scimitar
323
314
  # <- userType ne "Employee" and not (emails[value co "example.com" or (value co "example.org")]) and userName="foo"
324
315
  # => userType ne "Employee" and not (emails.value co "example.com" or (emails.value co "example.org")) and userName="foo"
325
316
  #
326
- # <- emails[type eq "work"].value eq "foo@bar.com" (Microsoft workaround)
327
- # => emails.type eq "work" and emails.value eq "foo@bar.com"
328
- #
329
317
  # +filter+:: Input filter string. Returns a possibly modified String,
330
318
  # with the hopefully equivalent but flattened filter inside.
331
319
  #
@@ -375,53 +363,9 @@ module Scimitar
375
363
  end
376
364
 
377
365
  elsif (expecting_value)
378
- matches = downcased.match(/([^\\])\](.*)/) # Contains no-backslash-then-literal (unescaped) ']'; also capture anything after
366
+ matches = downcased.match(/([^\\])\]/) # Contains no-backslash-then-literal (unescaped) ']'
379
367
  unless matches.nil? # Contains no-backslash-then-literal (unescaped) ']'
380
368
  character_before_closing_bracket = matches[1]
381
- characters_after_closing_bracket = matches[2]
382
-
383
- # https://github.com/RIPAGlobal/scimitar/issues/115 - detect
384
- # bad Microsoft filters. After the closing bracket, we expect a
385
- # dot then valid attribute characters and at least one white
386
- # space character and filter operator, but we split on spaces,
387
- # so the next item in the components array must be a recognised
388
- # operator for the special case code to kick in.
389
- #
390
- # If this all works out, we transform this section of the
391
- # filter string into a local dotted form, reconstruct the
392
- # overall filter with this substitution, and call back to this
393
- # method with that modified filter, returning the result.
394
- #
395
- # So - NOTE RECURSION AND EARLY EXIT POSSIBILITY HEREIN.
396
- #
397
- if (
398
- ! attribute_prefix.nil? &&
399
- OPERATORS.key?(components[index + 1]&.downcase) &&
400
- characters_after_closing_bracket.match?(/^\.#{ATTRNAME}$/)
401
- )
402
- # E.g. '"work"' and '.value' from input '"work"].value'
403
- #
404
- component_matches = component.match(/^(.*?[^\\])\](.*)/)
405
- part_before_closing_bracket = component_matches[1]
406
- part_after_closing_bracket = component_matches[2]
407
-
408
- # Produces e.g. '"work"] and emails.value'
409
- #
410
- dotted_version = "#{part_before_closing_bracket}] and #{attribute_prefix}#{part_after_closing_bracket}"
411
-
412
- # Overwrite the components array entry with this new version.
413
- #
414
- components[index] = dotted_version
415
-
416
- # Join it back again as a reconstructed valid filter string.
417
- #
418
- hopefully_valid_filter = components.join(' ')
419
-
420
- # NOTE EARLY EXIT
421
- #
422
- return flatten_filter(hopefully_valid_filter)
423
- end
424
-
425
369
  component.gsub!(/[^\\]\]/, character_before_closing_bracket)
426
370
 
427
371
  if expecting_closing_bracket
@@ -466,36 +410,7 @@ module Scimitar
466
410
  end
467
411
  end
468
412
 
469
- # Handle schema IDs.
470
- #
471
- # Scimitar currently has a limitation where it strips schema IDs in
472
- # things like PATCH operation path traversal; see
473
- # https://github.com/RIPAGlobal/scimitar/issues/130. At least that
474
- # makes things easy here; use the same approach and strip them out!
475
- #
476
- # We don't know which resource is being queried at this layer of the
477
- # software. If Scimitar were to bump major version, then an extension
478
- # to QueryParser#parse to include this information would be wise. In
479
- # the mean time, all we can do is enumerate all extension schema
480
- # subclasses with IDs and remove those IDs if present in the filter.
481
- #
482
- # Inbound unrecognised schema IDs will be left behind. If the client
483
- # Scimitar application hasn't defined requested schemas, it would
484
- # very likely never have been able to handle the filter either way.
485
- #
486
- rewritten_joined = rewritten.join(' ')
487
- if rewritten_joined.include?(':')
488
-
489
- # README.md notes that extensions *must* be a subclass of
490
- # Scimitar::Schema::Base and must define IDs.
491
- #
492
- known_schema_ids = Scimitar::Schema::Base.subclasses.map { |s| s.new.id }.compact
493
- known_schema_ids.each do | schema_id |
494
- rewritten_joined.gsub!(/#{schema_id}[\:\.]/, '')
495
- end
496
- end
497
-
498
- return rewritten_joined
413
+ return rewritten.join(' ')
499
414
  end
500
415
 
501
416
  # Service method to DRY up #flatten_filter a little. Applies a prefix
@@ -686,27 +601,12 @@ module Scimitar
686
601
  column_names = self.activerecord_columns(scim_attribute)
687
602
  value = self.activerecord_parameter(scim_parameter)
688
603
  value_for_like = self.sql_modified_value(scim_operator, value)
689
- arel_columns = column_names.map do |column|
690
- if base_scope.model.column_names.include?(column.to_s)
691
- arel_table[column]
692
- elsif column.is_a?(Arel::Attribute)
693
- column
694
- end
695
- end
604
+ all_supported = column_names.all? { | column_name | base_scope.model.column_names.include?(column_name.to_s) }
696
605
 
697
- raise Scimitar::FilterError unless arel_columns.all?
698
-
699
- unless case_sensitive
700
- lc_scim_attribute = scim_attribute.downcase()
701
-
702
- case_sensitive = (
703
- lc_scim_attribute == 'id' ||
704
- lc_scim_attribute == 'externalid' ||
705
- lc_scim_attribute.start_with?('meta.')
706
- )
707
- end
606
+ raise Scimitar::FilterError unless all_supported
708
607
 
709
- arel_columns.each.with_index do | arel_column, index |
608
+ column_names.each.with_index do | column_name, index |
609
+ arel_column = arel_table[column_name]
710
610
  arel_operation = case scim_operator
711
611
  when 'eq'
712
612
  if case_sensitive
@@ -731,9 +631,9 @@ module Scimitar
731
631
  when 'co', 'sw', 'ew'
732
632
  arel_column.matches(value_for_like, nil, case_sensitive)
733
633
  when 'pr'
734
- arel_column.relation.grouping(arel_column.not_eq_all(['', nil]))
634
+ arel_table.grouping(arel_column.not_eq_all(['', nil]))
735
635
  else
736
- raise Scimitar::FilterError.new("Unsupported operator: '#{scim_operator}'")
636
+ raise Scimitar::FilterError
737
637
  end
738
638
 
739
639
  if index == 0
@@ -756,10 +656,10 @@ module Scimitar
756
656
  # +scim_attribute+:: SCIM attribute from a filter string.
757
657
  #
758
658
  def activerecord_columns(scim_attribute)
759
- raise Scimitar::FilterError.new("No scim_attribute provided") if scim_attribute.blank?
659
+ raise Scimitar::FilterError if scim_attribute.blank?
760
660
 
761
661
  mapped_attribute = self.attribute_map()[scim_attribute]
762
- raise Scimitar::FilterError.new("Unable to find domain attribute from SCIM attribute: '#{scim_attribute}'") if mapped_attribute.blank?
662
+ raise Scimitar::FilterError if mapped_attribute.blank?
763
663
 
764
664
  if mapped_attribute[:ignore]
765
665
  return []
@@ -2,7 +2,7 @@ module Scimitar
2
2
  class ResourceInvalidError < ErrorResponse
3
3
 
4
4
  def initialize(error_message)
5
- super(status: 400, scimType: 'invalidValue', detail: "Operation failed since record has become invalid: #{error_message}")
5
+ super(status: 400, scimType: 'invalidValue', detail:"Operation failed since record has become invalid: #{error_message}")
6
6
  end
7
7
 
8
8
  end