scimitar 1.8.2 → 1.10.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/README.md +27 -20
- data/app/controllers/scimitar/active_record_backed_resources_controller.rb +5 -4
- data/app/controllers/scimitar/resource_types_controller.rb +0 -2
- data/app/controllers/scimitar/resources_controller.rb +0 -2
- data/app/controllers/scimitar/schemas_controller.rb +361 -3
- data/app/controllers/scimitar/service_provider_configurations_controller.rb +0 -1
- data/app/models/scimitar/engine_configuration.rb +3 -1
- data/app/models/scimitar/lists/query_parser.rb +88 -3
- data/app/models/scimitar/resources/base.rb +36 -5
- data/app/models/scimitar/resources/mixin.rb +133 -43
- data/app/models/scimitar/schema/name.rb +2 -2
- data/app/models/scimitar/schema/user.rb +10 -10
- data/config/initializers/scimitar.rb +41 -0
- data/lib/scimitar/engine.rb +57 -12
- data/lib/scimitar/support/utilities.rb +60 -0
- data/lib/scimitar/version.rb +2 -2
- data/spec/apps/dummy/app/models/mock_user.rb +18 -3
- data/spec/apps/dummy/config/initializers/scimitar.rb +31 -2
- data/spec/apps/dummy/db/migrate/20210304014602_create_mock_users.rb +1 -0
- data/spec/apps/dummy/db/schema.rb +1 -0
- data/spec/controllers/scimitar/schemas_controller_spec.rb +342 -54
- data/spec/models/scimitar/lists/query_parser_spec.rb +70 -0
- data/spec/models/scimitar/resources/base_spec.rb +11 -11
- data/spec/models/scimitar/resources/base_validation_spec.rb +16 -3
- data/spec/models/scimitar/resources/mixin_spec.rb +71 -10
- data/spec/models/scimitar/schema/user_spec.rb +2 -2
- data/spec/requests/active_record_backed_resources_controller_spec.rb +231 -0
- data/spec/requests/engine_spec.rb +75 -0
- data/spec/spec_helper.rb +1 -1
- metadata +22 -22
@@ -59,13 +59,18 @@ 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
|
+
|
62
67
|
# =======================================================================
|
63
68
|
# Tokenizing expressions
|
64
69
|
# =======================================================================
|
65
70
|
|
66
71
|
PAREN = /[\(\)]/.freeze
|
67
72
|
STR = /"(?:\\"|[^"])*"/.freeze
|
68
|
-
OP =
|
73
|
+
OP = /(?:#{OPERATORS.keys.join('|')})\b/i.freeze
|
69
74
|
WORD = /[\w\.]+/.freeze
|
70
75
|
SEP = /\s?/.freeze
|
71
76
|
NEXT_TOKEN = /\A(#{PAREN}|#{STR}|#{OP}|#{WORD})#{SEP}/.freeze
|
@@ -291,6 +296,10 @@ module Scimitar
|
|
291
296
|
# the part before the "[" as a prefix - "emails[type" to "emails.type",
|
292
297
|
# with similar substitutions therein.
|
293
298
|
#
|
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
|
+
#
|
294
303
|
# This method tries to flatten things thus. It throws exceptions if any
|
295
304
|
# problems arise at all. Some limitations:
|
296
305
|
#
|
@@ -314,6 +323,9 @@ module Scimitar
|
|
314
323
|
# <- userType ne "Employee" and not (emails[value co "example.com" or (value co "example.org")]) and userName="foo"
|
315
324
|
# => userType ne "Employee" and not (emails.value co "example.com" or (emails.value co "example.org")) and userName="foo"
|
316
325
|
#
|
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
|
+
#
|
317
329
|
# +filter+:: Input filter string. Returns a possibly modified String,
|
318
330
|
# with the hopefully equivalent but flattened filter inside.
|
319
331
|
#
|
@@ -363,9 +375,53 @@ module Scimitar
|
|
363
375
|
end
|
364
376
|
|
365
377
|
elsif (expecting_value)
|
366
|
-
matches = downcased.match(/([^\\])\]/) # Contains no-backslash-then-literal (unescaped) ']'
|
378
|
+
matches = downcased.match(/([^\\])\](.*)/) # Contains no-backslash-then-literal (unescaped) ']'; also capture anything after
|
367
379
|
unless matches.nil? # Contains no-backslash-then-literal (unescaped) ']'
|
368
380
|
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
|
+
|
369
425
|
component.gsub!(/[^\\]\]/, character_before_closing_bracket)
|
370
426
|
|
371
427
|
if expecting_closing_bracket
|
@@ -410,7 +466,36 @@ module Scimitar
|
|
410
466
|
end
|
411
467
|
end
|
412
468
|
|
413
|
-
|
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
|
414
499
|
end
|
415
500
|
|
416
501
|
# Service method to DRY up #flatten_filter a little. Applies a prefix
|
@@ -35,14 +35,45 @@ module Scimitar
|
|
35
35
|
@errors = ActiveModel::Errors.new(self)
|
36
36
|
end
|
37
37
|
|
38
|
+
# Scimitar has at present a general limitation in handling schema IDs,
|
39
|
+
# which just involves stripping them and requiring attributes across all
|
40
|
+
# extension schemas to be overall unique.
|
41
|
+
#
|
42
|
+
# This method takes an options payload for the initializer and strips out
|
43
|
+
# *recognised* schema IDs, so that the resulting attribute data matches
|
44
|
+
# the resource attribute map.
|
45
|
+
#
|
46
|
+
# +attributes+:: Attributes to assign via initializer; typically a POST
|
47
|
+
# payload of attributes that has been run through Rails
|
48
|
+
# strong parameters for safety.
|
49
|
+
#
|
50
|
+
# Returns a new object of the same class as +options+ with recognised
|
51
|
+
# schema IDs removed.
|
52
|
+
#
|
38
53
|
def flatten_extension_attributes(options)
|
39
|
-
flattened
|
40
|
-
self.class.extended_schemas.
|
41
|
-
|
42
|
-
|
54
|
+
flattened = options.class.new
|
55
|
+
lower_case_schema_ids = self.class.extended_schemas.map do | schema |
|
56
|
+
schema.id.downcase()
|
57
|
+
end
|
58
|
+
|
59
|
+
options.each do | key, value |
|
60
|
+
path = Scimitar::Support::Utilities::path_str_to_array(
|
61
|
+
self.class.extended_schemas,
|
62
|
+
key
|
63
|
+
)
|
64
|
+
|
65
|
+
if path.first.include?(':') && lower_case_schema_ids.include?(path.first.downcase)
|
66
|
+
path.shift()
|
67
|
+
end
|
68
|
+
|
69
|
+
if path.empty?
|
70
|
+
flattened.merge!(value)
|
71
|
+
else
|
72
|
+
flattened[path.join('.')] = value
|
43
73
|
end
|
44
74
|
end
|
45
|
-
|
75
|
+
|
76
|
+
return flattened
|
46
77
|
end
|
47
78
|
|
48
79
|
# Can be used to extend an existing resource type's schema. For example:
|
@@ -139,27 +139,31 @@ module Scimitar
|
|
139
139
|
# # ...
|
140
140
|
# groups: [
|
141
141
|
# {
|
142
|
-
# list:
|
142
|
+
# list: :users, # <-- i.e. Team.users,
|
143
143
|
# using: {
|
144
144
|
# value: :id, # <-- i.e. Team.users[n].id
|
145
145
|
# display: :full_name # <-- i.e. Team.users[n].full_name
|
146
146
|
# },
|
147
|
+
# class: Team, # Optional; see below
|
147
148
|
# find_with: -> (scim_list_entry) {...} # See below
|
148
149
|
# }
|
149
150
|
# ],
|
150
151
|
# #...
|
151
152
|
# end
|
152
153
|
#
|
153
|
-
# The mixing-in class
|
154
|
+
# The mixing-in class _must_ implement the read accessor identified by the
|
154
155
|
# value of the "list" key, returning any indexed, Enumerable collection
|
155
156
|
# (e.g. an Array or ActiveRecord::Relation instance). The optional key
|
156
|
-
# ":find_with" is defined with a Proc that
|
157
|
+
# ":find_with" is defined with a Proc that is passed the SCIM entry at each
|
157
158
|
# list position. It must use this to look up the equivalent entry for
|
158
159
|
# association via the write accessor described by the ":list" key. In the
|
159
160
|
# example above, "find_with"'s Proc might look at a SCIM entry value which
|
160
161
|
# is expected to be a user ID and find that User. The mapped set of User
|
161
162
|
# data thus found would be written back with "#users=", due to the ":list"
|
162
|
-
# key declaring the method name ":users".
|
163
|
+
# key declaring the method name ":users". The optional "class" key is
|
164
|
+
# recommended but not really *needed* unless the configuration option
|
165
|
+
# Scimitar::EngineConfiguration::schema_list_from_attribute_mappings is
|
166
|
+
# defined; see documentation of that option for more information.
|
163
167
|
#
|
164
168
|
# Note that you can only use either:
|
165
169
|
#
|
@@ -176,7 +180,8 @@ module Scimitar
|
|
176
180
|
# == scim_mutable_attributes
|
177
181
|
#
|
178
182
|
# Define this method to return a Set (preferred) or Array of names of
|
179
|
-
# attributes which may be written in the mixing-in class.
|
183
|
+
# attributes which may be written in the mixing-in class. The names MUST be
|
184
|
+
# expressed as Symbols, *not* Strings.
|
180
185
|
#
|
181
186
|
# If you return +nil+, it is assumed that +any+ attribute mapped by
|
182
187
|
# ::scim_attributes_map which has a write accessor will be eligible for
|
@@ -204,12 +209,12 @@ module Scimitar
|
|
204
209
|
# Define this method to return a Hash that maps field names you wish to
|
205
210
|
# support in SCIM filter queries to corresponding attributes in the in the
|
206
211
|
# mixing-in class. If +nil+ then filtering is not supported in the
|
207
|
-
#
|
212
|
+
# ResourceController subclass which declares that it maps to the mixing-in
|
208
213
|
# class. If not +nil+ but a SCIM filter enquiry is made for an unmapped
|
209
214
|
# attribute, an 'invalid filter' exception is raised.
|
210
215
|
#
|
211
216
|
# If using ActiveRecord support in Scimitar::Lists::QueryParser, the mapped
|
212
|
-
#
|
217
|
+
# entities are columns and that's expressed in the names of keys described
|
213
218
|
# below; if you have other approaches to searching, these might be virtual
|
214
219
|
# attributes or other such constructs rather than columns. That would be up
|
215
220
|
# to your non-ActiveRecord's implementation to decide.
|
@@ -262,8 +267,8 @@ module Scimitar
|
|
262
267
|
# both of the keys 'created' and 'lastModified', as Symbols. The values
|
263
268
|
# should be methods that the including method supports which return a
|
264
269
|
# creation or most-recently-updated time, respectively. The returned object
|
265
|
-
#
|
266
|
-
# typical ActiveRecord object with standard timestamps:
|
270
|
+
# must support #iso8601 to convert to a String representation. Example for
|
271
|
+
# a typical ActiveRecord object with standard timestamps:
|
267
272
|
#
|
268
273
|
# def self.scim_timestamps_map
|
269
274
|
# {
|
@@ -291,7 +296,7 @@ module Scimitar
|
|
291
296
|
# the result in an instance variable.
|
292
297
|
#
|
293
298
|
def scim_mutable_attributes
|
294
|
-
@scim_mutable_attributes ||= self.class.scim_mutable_attributes()
|
299
|
+
@scim_mutable_attributes ||= self.class.scim_mutable_attributes()&.map(&:to_sym)
|
295
300
|
|
296
301
|
if @scim_mutable_attributes.nil?
|
297
302
|
@scim_mutable_attributes = Set.new
|
@@ -338,16 +343,32 @@ module Scimitar
|
|
338
343
|
# Render self as a SCIM object using ::scim_attributes_map. Fields that
|
339
344
|
# are marked as <tt>returned: 'never'</tt> are excluded.
|
340
345
|
#
|
341
|
-
# +location+::
|
342
|
-
#
|
343
|
-
#
|
344
|
-
#
|
346
|
+
# +location+:: The location (HTTP(S) full URI) of this
|
347
|
+
# resource in the domain of the object including
|
348
|
+
# this mixin - "your" IDs, not the remote SCIM
|
349
|
+
# client's external IDs. #url_for is a good way
|
350
|
+
# to generate this.
|
345
351
|
#
|
346
|
-
|
352
|
+
# +include_attributes+:: The attributes that should be included in the
|
353
|
+
# response, in the form of a list of full
|
354
|
+
# attribute paths. Schema IDs are not supported.
|
355
|
+
# See RFC 7644 section 3.9 and section 3.10 for
|
356
|
+
# more. When a collection is given, +nil+ value
|
357
|
+
# items are also excluded from the response. If
|
358
|
+
# omitted or given an empty collection, all
|
359
|
+
# attributes are included.
|
360
|
+
#
|
361
|
+
def to_scim(location:, include_attributes: [])
|
347
362
|
map = self.class.scim_attributes_map()
|
348
363
|
resource_type = self.class.scim_resource_type()
|
349
364
|
timestamps_map = self.class.scim_timestamps_map() if self.class.respond_to?(:scim_timestamps_map)
|
350
|
-
attrs_hash = self.to_scim_backend(
|
365
|
+
attrs_hash = self.to_scim_backend(
|
366
|
+
data_source: self,
|
367
|
+
resource_type: resource_type,
|
368
|
+
attrs_map_or_leaf_value: map,
|
369
|
+
include_attributes: include_attributes
|
370
|
+
)
|
371
|
+
|
351
372
|
resource = resource_type.new(attrs_hash)
|
352
373
|
meta_attrs_hash = { location: location }
|
353
374
|
|
@@ -483,26 +504,14 @@ module Scimitar
|
|
483
504
|
ci_scim_hash = { 'root' => ci_scim_hash }.with_indifferent_case_insensitive_access()
|
484
505
|
end
|
485
506
|
|
486
|
-
#
|
487
|
-
#
|
488
|
-
#
|
489
|
-
# https://github.com/RIPAGlobal/scimitar/issues/48
|
490
|
-
# https://github.com/RIPAGlobal/scimitar/pull/49
|
507
|
+
# Split the path into an array of path components, in a way
|
508
|
+
# which is aware of extension schemas. See documentation of
|
509
|
+
# Scimitar::Support::Utilities.path_str_to_array for details.
|
491
510
|
#
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
# https://tools.ietf.org/html/rfc7644#section-3.5.2 of which in
|
497
|
-
# particular, https://tools.ietf.org/html/rfc7644#page-35.
|
498
|
-
#
|
499
|
-
paths = []
|
500
|
-
self.class.scim_resource_type.extended_schemas.each do |schema|
|
501
|
-
path_str.downcase.split(schema.id.downcase + ':').drop(1).each do |path|
|
502
|
-
paths += [schema.id] + path.split('.')
|
503
|
-
end
|
504
|
-
end
|
505
|
-
paths = path_str.split('.') if paths.empty?
|
511
|
+
paths = ::Scimitar::Support::Utilities.path_str_to_array(
|
512
|
+
self.class.scim_resource_type.extended_schemas,
|
513
|
+
path_str
|
514
|
+
)
|
506
515
|
|
507
516
|
self.from_patch_backend!(
|
508
517
|
nature: nature,
|
@@ -544,6 +553,16 @@ module Scimitar
|
|
544
553
|
# +attrs_map_or_leaf_value+:: The attribute map. At the top level,
|
545
554
|
# this is from ::scim_attributes_map.
|
546
555
|
#
|
556
|
+
# +include_attributes+:: The attributes that should be included
|
557
|
+
# in the response, in the form of a list
|
558
|
+
# of full attribute paths. Schema IDs are
|
559
|
+
# not supported. See RFC 7644 section
|
560
|
+
# 3.9 and section 3.10 for more. When a
|
561
|
+
# collection is given, +nil+ value items
|
562
|
+
# are also excluded from the response. If
|
563
|
+
# omitted or given an empty collection,
|
564
|
+
# all attributes are included.
|
565
|
+
#
|
547
566
|
# Internal recursive calls also send:
|
548
567
|
#
|
549
568
|
# +attribute_path+:: Array of path components to the
|
@@ -555,8 +574,15 @@ module Scimitar
|
|
555
574
|
data_source:,
|
556
575
|
resource_type:,
|
557
576
|
attrs_map_or_leaf_value:,
|
577
|
+
include_attributes:,
|
558
578
|
attribute_path: []
|
559
579
|
)
|
580
|
+
# NOTE EARLY EXIT
|
581
|
+
#
|
582
|
+
return unless scim_attribute_included?(
|
583
|
+
include_attributes: include_attributes,
|
584
|
+
attribute_path: attribute_path
|
585
|
+
)
|
560
586
|
|
561
587
|
# On assumption of a top-level attributes list, the 'return never'
|
562
588
|
# state is only checked on the recursive call from a Hash type. The
|
@@ -566,7 +592,7 @@ module Scimitar
|
|
566
592
|
#
|
567
593
|
case attrs_map_or_leaf_value
|
568
594
|
when Hash # Expected at top-level of any map, or nested within
|
569
|
-
attrs_map_or_leaf_value.each.with_object({}) do |(key, value), hash|
|
595
|
+
result = attrs_map_or_leaf_value.each.with_object({}) do |(key, value), hash|
|
570
596
|
nested_attribute_path = attribute_path + [key]
|
571
597
|
|
572
598
|
if resource_type.find_attribute(*nested_attribute_path)&.returned != "never"
|
@@ -574,11 +600,15 @@ module Scimitar
|
|
574
600
|
data_source: data_source,
|
575
601
|
resource_type: resource_type,
|
576
602
|
attribute_path: nested_attribute_path,
|
577
|
-
attrs_map_or_leaf_value: value
|
603
|
+
attrs_map_or_leaf_value: value,
|
604
|
+
include_attributes: include_attributes
|
578
605
|
)
|
579
606
|
end
|
580
607
|
end
|
581
608
|
|
609
|
+
result.compact! if include_attributes.any?
|
610
|
+
result
|
611
|
+
|
582
612
|
when Array # Static or dynamic mapping against lists in data source
|
583
613
|
built_dynamic_list = false
|
584
614
|
mapped_array = attrs_map_or_leaf_value.map do |value|
|
@@ -592,7 +622,8 @@ module Scimitar
|
|
592
622
|
data_source: data_source,
|
593
623
|
resource_type: resource_type,
|
594
624
|
attribute_path: attribute_path,
|
595
|
-
attrs_map_or_leaf_value: value[:using]
|
625
|
+
attrs_map_or_leaf_value: value[:using],
|
626
|
+
include_attributes: include_attributes
|
596
627
|
)
|
597
628
|
)
|
598
629
|
static_hash
|
@@ -605,7 +636,8 @@ module Scimitar
|
|
605
636
|
data_source: list_entry,
|
606
637
|
resource_type: resource_type,
|
607
638
|
attribute_path: attribute_path,
|
608
|
-
attrs_map_or_leaf_value: value[:using]
|
639
|
+
attrs_map_or_leaf_value: value[:using],
|
640
|
+
include_attributes: include_attributes
|
609
641
|
)
|
610
642
|
end
|
611
643
|
|
@@ -740,9 +772,17 @@ module Scimitar
|
|
740
772
|
# https://github.com/RIPAGlobal/scimitar/issues/48
|
741
773
|
# https://github.com/RIPAGlobal/scimitar/pull/49
|
742
774
|
#
|
775
|
+
# Note the shortcoming that attribute names within extensions
|
776
|
+
# must be unique, as this mechanism basically just pulls out
|
777
|
+
# extension attributes to the top level, losing what amounts
|
778
|
+
# to the namespace that the extension schema ID provides.
|
779
|
+
#
|
743
780
|
attribute_tree = []
|
744
781
|
resource_class.extended_schemas.each do |schema|
|
745
|
-
|
782
|
+
if schema.scim_attributes.any? { |attribute| attribute.name == scim_attribute.to_s }
|
783
|
+
attribute_tree << schema.id
|
784
|
+
break # NOTE EARLY LOOP EXIT
|
785
|
+
end
|
746
786
|
end
|
747
787
|
attribute_tree << scim_attribute.to_s
|
748
788
|
|
@@ -950,7 +990,11 @@ module Scimitar
|
|
950
990
|
end
|
951
991
|
|
952
992
|
found_data_for_recursion.each do | found_data |
|
953
|
-
attr_map =
|
993
|
+
attr_map = if path_component.to_sym == :root
|
994
|
+
with_attr_map
|
995
|
+
else
|
996
|
+
with_attr_map[path_component.to_sym]
|
997
|
+
end
|
954
998
|
|
955
999
|
# Static array mappings need us to find the right map entry that
|
956
1000
|
# corresponds to the SCIM data at hand and recurse back into the
|
@@ -1091,9 +1135,27 @@ module Scimitar
|
|
1091
1135
|
# at key 'members' with the above, rather than adding.
|
1092
1136
|
#
|
1093
1137
|
value.keys.each do | key |
|
1138
|
+
|
1139
|
+
# Handle the Azure (Entra) case where keys might use
|
1140
|
+
# dotted paths - see:
|
1141
|
+
#
|
1142
|
+
# https://github.com/RIPAGlobal/scimitar/issues/123
|
1143
|
+
#
|
1144
|
+
# ...along with keys containing schema IDs - see:
|
1145
|
+
#
|
1146
|
+
# https://is.docs.wso2.com/en/next/apis/scim2-patch-operations/#add-user-attributes
|
1147
|
+
#
|
1148
|
+
# ...and scroll down to example 3 of "Complex singular
|
1149
|
+
# attributes".
|
1150
|
+
#
|
1151
|
+
subpaths = ::Scimitar::Support::Utilities.path_str_to_array(
|
1152
|
+
self.class.scim_resource_type.extended_schemas,
|
1153
|
+
key
|
1154
|
+
)
|
1155
|
+
|
1094
1156
|
from_patch_backend!(
|
1095
1157
|
nature: nature,
|
1096
|
-
path: path +
|
1158
|
+
path: path + subpaths,
|
1097
1159
|
value: value[key],
|
1098
1160
|
altering_hash: altering_hash,
|
1099
1161
|
with_attr_map: with_attr_map
|
@@ -1106,7 +1168,12 @@ module Scimitar
|
|
1106
1168
|
when 'replace'
|
1107
1169
|
if path_component == 'root'
|
1108
1170
|
dot_pathed_value = value.inject({}) do |hash, (k, v)|
|
1109
|
-
|
1171
|
+
subpaths = ::Scimitar::Support::Utilities.path_str_to_array(
|
1172
|
+
self.class.scim_resource_type.extended_schemas,
|
1173
|
+
k
|
1174
|
+
)
|
1175
|
+
|
1176
|
+
hash.deep_merge!(::Scimitar::Support::Utilities.dot_path(subpaths, v))
|
1110
1177
|
end
|
1111
1178
|
|
1112
1179
|
altering_hash[path_component].deep_merge!(dot_pathed_value)
|
@@ -1478,6 +1545,29 @@ module Scimitar
|
|
1478
1545
|
return handled
|
1479
1546
|
end
|
1480
1547
|
|
1548
|
+
# Related to to_scim_backend, this methods tells whether +attribute_path+
|
1549
|
+
# should be included in the current +include_attributes+. This method
|
1550
|
+
# implements the attributes request from RFC 7644, section 3.9 and 3.10.
|
1551
|
+
#
|
1552
|
+
# +include_attributes+:: The attributes that should be included
|
1553
|
+
# in the response, in the form of a list of
|
1554
|
+
# full attribute paths. See RFC 7644 section
|
1555
|
+
# 3.9 and section 3.10. An empty collection
|
1556
|
+
# will include all attributes.
|
1557
|
+
#
|
1558
|
+
# +attribute_path+:: Array of path components to the attribute,
|
1559
|
+
# e.g. <tt>["name", "givenName"]</tt>.
|
1560
|
+
#
|
1561
|
+
def scim_attribute_included?(include_attributes:, attribute_path:)
|
1562
|
+
return true unless attribute_path.any? && include_attributes.any?
|
1563
|
+
|
1564
|
+
full_path = attribute_path.join(".")
|
1565
|
+
attribute_included = full_path.start_with?(*include_attributes)
|
1566
|
+
will_include_nested = include_attributes.any? { |att| att.start_with?(full_path) }
|
1567
|
+
|
1568
|
+
attribute_included || will_include_nested
|
1569
|
+
end
|
1570
|
+
|
1481
1571
|
end # "included do"
|
1482
1572
|
end # "module Mixin"
|
1483
1573
|
end # "module Resources"
|
@@ -6,8 +6,8 @@ module Scimitar
|
|
6
6
|
|
7
7
|
def self.scim_attributes
|
8
8
|
@scim_attributes ||= [
|
9
|
-
Attribute.new(name: 'familyName', type: 'string'
|
10
|
-
Attribute.new(name: 'givenName', type: 'string'
|
9
|
+
Attribute.new(name: 'familyName', type: 'string'),
|
10
|
+
Attribute.new(name: 'givenName', type: 'string'),
|
11
11
|
Attribute.new(name: 'middleName', type: 'string'),
|
12
12
|
Attribute.new(name: 'formatted', type: 'string'),
|
13
13
|
Attribute.new(name: 'honorificPrefix', type: 'string'),
|
@@ -20,7 +20,7 @@ module Scimitar
|
|
20
20
|
[
|
21
21
|
Attribute.new(name: 'userName', type: 'string', uniqueness: 'server', required: true),
|
22
22
|
|
23
|
-
Attribute.new(name: 'name',
|
23
|
+
Attribute.new(name: 'name', complexType: Scimitar::ComplexTypes::Name),
|
24
24
|
|
25
25
|
Attribute.new(name: 'displayName', type: 'string'),
|
26
26
|
Attribute.new(name: 'nickName', type: 'string'),
|
@@ -35,15 +35,15 @@ module Scimitar
|
|
35
35
|
|
36
36
|
Attribute.new(name: 'password', type: 'string', mutability: 'writeOnly', returned: 'never'),
|
37
37
|
|
38
|
-
Attribute.new(name: 'emails',
|
39
|
-
Attribute.new(name: 'phoneNumbers',
|
40
|
-
Attribute.new(name: 'ims',
|
41
|
-
Attribute.new(name: 'photos',
|
42
|
-
Attribute.new(name: 'addresses',
|
43
|
-
Attribute.new(name: 'groups',
|
44
|
-
Attribute.new(name: 'entitlements',
|
45
|
-
Attribute.new(name: 'roles',
|
46
|
-
Attribute.new(name: 'x509Certificates',
|
38
|
+
Attribute.new(name: 'emails', multiValued: true, complexType: Scimitar::ComplexTypes::Email),
|
39
|
+
Attribute.new(name: 'phoneNumbers', multiValued: true, complexType: Scimitar::ComplexTypes::PhoneNumber),
|
40
|
+
Attribute.new(name: 'ims', multiValued: true, complexType: Scimitar::ComplexTypes::Ims),
|
41
|
+
Attribute.new(name: 'photos', multiValued: true, complexType: Scimitar::ComplexTypes::Photo),
|
42
|
+
Attribute.new(name: 'addresses', multiValued: true, complexType: Scimitar::ComplexTypes::Address),
|
43
|
+
Attribute.new(name: 'groups', multiValued: true, complexType: Scimitar::ComplexTypes::ReferenceGroup, mutability: 'readOnly'),
|
44
|
+
Attribute.new(name: 'entitlements', multiValued: true, complexType: Scimitar::ComplexTypes::Entitlement),
|
45
|
+
Attribute.new(name: 'roles', multiValued: true, complexType: Scimitar::ComplexTypes::Role),
|
46
|
+
Attribute.new(name: 'x509Certificates', multiValued: true, complexType: Scimitar::ComplexTypes::X509Certificate),
|
47
47
|
]
|
48
48
|
end
|
49
49
|
|
@@ -106,6 +106,47 @@ Rails.application.config.to_prepare do # (required for >= Rails 7 / Zeitwerk)
|
|
106
106
|
# whatever that means for you receiving system in your model code.
|
107
107
|
#
|
108
108
|
# optional_value_fields_required: false
|
109
|
+
|
110
|
+
# The SCIM standard `/Schemas` endpoint lists, by default, all known schema
|
111
|
+
# definitions with the mutabilty (read-write, read-only, write-only) state
|
112
|
+
# described by those definitions, and includes all defined attributes. For
|
113
|
+
# user-defined schema, this will typically exactly match your underlying
|
114
|
+
# mapped attribute and model capability - it wouldn't make sense to define
|
115
|
+
# your own schema that misrepresented the implementation! For core SCIM RFC
|
116
|
+
# schema, though, you might want to only list actually mapped attributes.
|
117
|
+
# Further, if you happen to have a non-compliant implementation especially
|
118
|
+
# in relation to mutability of some attributes, you may want to report that
|
119
|
+
# accurately in the '/Schemas' list, for auto-discovery purposes. To switch
|
120
|
+
# to a significantly slower but more accurate render method for the list,
|
121
|
+
# driven by your resource subclasses and their attribute maps, set:
|
122
|
+
#
|
123
|
+
# schema_list_from_attribute_mappings: [...array...]
|
124
|
+
#
|
125
|
+
# ...where you provide an Array of *models*, your classes that include the
|
126
|
+
# Scimitar::Resources::Mixin module and, therefore, define an attribute map
|
127
|
+
# translating SCIM schema attributes into actual implemented data. These
|
128
|
+
# must *uniquely* describe, via the Scimitar resources they each declare in
|
129
|
+
# their Scimitar::Resources::Mixin::scim_resource_type implementation, the
|
130
|
+
# set of schemas and extended schemas you want to render. Should resources
|
131
|
+
# share schema, the '/Schemas' endpoint will fail since it cannot determine
|
132
|
+
# which model attribute map it should use and it needs the map in order to
|
133
|
+
# resolve the differences (if any) between what the schema might say, and
|
134
|
+
# what the actual underlying model supports.
|
135
|
+
#
|
136
|
+
# It is further _very_ _strongly_ _recommended_ that, for any
|
137
|
+
# +scim_attributes_map+ containing a collection which has "list:" key (for
|
138
|
+
# an associative array of zero or more entities; the Groups to which a User
|
139
|
+
# might belong is a good example) then you should also specify the "class:"
|
140
|
+
# key, giving the class used for objects in that associated collection. The
|
141
|
+
# class *must* include Scimitar::Resources::Mixin, since its own attribute
|
142
|
+
# map is consulted in order to render the part of the schema describing
|
143
|
+
# those associated properties in the owning resource. If you don't do this,
|
144
|
+
# and if you're using ActiveRecord, then Scimitar attempts association
|
145
|
+
# reflection to determine the collection class - but that's more fragile
|
146
|
+
# than just being told the exact class in the attribute map. No matter how
|
147
|
+
# this class is determined, though, it must be possible to create a simple
|
148
|
+
# instance with +new+ and no parameters, since that's needed in order to
|
149
|
+
# call Scimitar::Resources::Mixin#scim_mutable_attributes.
|
109
150
|
})
|
110
151
|
|
111
152
|
end
|