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.
- checksums.yaml +4 -4
- data/app/controllers/scimitar/active_record_backed_resources_controller.rb +23 -98
- data/app/controllers/scimitar/application_controller.rb +17 -44
- data/app/controllers/scimitar/resource_types_controller.rb +3 -7
- data/app/controllers/scimitar/resources_controller.rb +2 -0
- data/app/controllers/scimitar/schemas_controller.rb +3 -366
- data/app/controllers/scimitar/service_provider_configurations_controller.rb +1 -0
- data/app/models/scimitar/complex_types/address.rb +6 -0
- data/app/models/scimitar/engine_configuration.rb +5 -15
- data/app/models/scimitar/error_response.rb +0 -12
- data/app/models/scimitar/lists/query_parser.rb +13 -113
- data/app/models/scimitar/resource_invalid_error.rb +1 -1
- data/app/models/scimitar/resources/base.rb +9 -53
- data/app/models/scimitar/resources/mixin.rb +59 -646
- data/app/models/scimitar/schema/address.rb +0 -1
- data/app/models/scimitar/schema/attribute.rb +5 -14
- data/app/models/scimitar/schema/base.rb +1 -1
- data/app/models/scimitar/schema/name.rb +2 -2
- data/app/models/scimitar/schema/user.rb +10 -10
- data/app/models/scimitar/schema/vdtp.rb +1 -1
- data/app/models/scimitar/service_provider_configuration.rb +3 -14
- data/config/initializers/scimitar.rb +3 -69
- data/lib/scimitar/engine.rb +12 -57
- data/lib/scimitar/support/hash_with_indifferent_case_insensitive_access.rb +10 -140
- data/lib/scimitar/version.rb +2 -2
- data/lib/scimitar.rb +2 -7
- data/spec/apps/dummy/app/controllers/mock_groups_controller.rb +1 -1
- data/spec/apps/dummy/app/models/mock_group.rb +1 -1
- data/spec/apps/dummy/app/models/mock_user.rb +9 -52
- data/spec/apps/dummy/config/application.rb +1 -0
- data/spec/apps/dummy/config/environments/test.rb +28 -5
- data/spec/apps/dummy/config/initializers/scimitar.rb +10 -90
- data/spec/apps/dummy/config/routes.rb +7 -28
- data/spec/apps/dummy/db/migrate/20210304014602_create_mock_users.rb +1 -11
- data/spec/apps/dummy/db/migrate/20210308044214_create_join_table_mock_groups_mock_users.rb +3 -8
- data/spec/apps/dummy/db/schema.rb +4 -12
- data/spec/controllers/scimitar/application_controller_spec.rb +3 -126
- data/spec/controllers/scimitar/resource_types_controller_spec.rb +4 -8
- data/spec/controllers/scimitar/schemas_controller_spec.rb +48 -344
- data/spec/models/scimitar/complex_types/address_spec.rb +4 -3
- data/spec/models/scimitar/complex_types/email_spec.rb +2 -0
- data/spec/models/scimitar/lists/query_parser_spec.rb +9 -146
- data/spec/models/scimitar/resources/base_spec.rb +71 -217
- data/spec/models/scimitar/resources/base_validation_spec.rb +5 -43
- data/spec/models/scimitar/resources/mixin_spec.rb +129 -1508
- data/spec/models/scimitar/schema/attribute_spec.rb +3 -22
- data/spec/models/scimitar/schema/base_spec.rb +1 -1
- data/spec/models/scimitar/schema/user_spec.rb +2 -12
- data/spec/requests/active_record_backed_resources_controller_spec.rb +66 -1016
- data/spec/requests/application_controller_spec.rb +3 -16
- data/spec/requests/engine_spec.rb +0 -75
- data/spec/spec_helper.rb +1 -9
- data/spec/support/hash_with_indifferent_case_insensitive_access_spec.rb +0 -108
- metadata +26 -37
- data/LICENSE.txt +0 -21
- data/README.md +0 -717
- data/lib/scimitar/support/utilities.rb +0 -111
- data/spec/apps/dummy/app/controllers/custom_create_mock_users_controller.rb +0 -25
- data/spec/apps/dummy/app/controllers/custom_replace_mock_users_controller.rb +0 -25
- data/spec/apps/dummy/app/controllers/custom_save_mock_users_controller.rb +0 -24
- 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
|
-
|
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
|
@@ -7,25 +7,15 @@ module Scimitar
|
|
7
7
|
class EngineConfiguration
|
8
8
|
include ActiveModel::Model
|
9
9
|
|
10
|
-
attr_accessor
|
11
|
-
|
12
|
-
|
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
|
-
#
|
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 =
|
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
|
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
|
-
|
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(/([^\\])\]
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
634
|
+
arel_table.grouping(arel_column.not_eq_all(['', nil]))
|
735
635
|
else
|
736
|
-
raise Scimitar::FilterError
|
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
|
659
|
+
raise Scimitar::FilterError if scim_attribute.blank?
|
760
660
|
|
761
661
|
mapped_attribute = self.attribute_map()[scim_attribute]
|
762
|
-
raise Scimitar::FilterError
|
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:
|
5
|
+
super(status: 400, scimType: 'invalidValue', detail:"Operation failed since record has become invalid: #{error_message}")
|
6
6
|
end
|
7
7
|
|
8
8
|
end
|