scimitar 1.10.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/controllers/scimitar/active_record_backed_resources_controller.rb +23 -98
- data/app/controllers/scimitar/application_controller.rb +13 -41
- data/app/controllers/scimitar/resource_types_controller.rb +2 -0
- 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 +2 -2
- 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
|