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
@@ -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