scimitar 1.8.2 → 1.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +27 -20
  3. data/app/controllers/scimitar/active_record_backed_resources_controller.rb +5 -4
  4. data/app/controllers/scimitar/resource_types_controller.rb +0 -2
  5. data/app/controllers/scimitar/resources_controller.rb +0 -2
  6. data/app/controllers/scimitar/schemas_controller.rb +361 -3
  7. data/app/controllers/scimitar/service_provider_configurations_controller.rb +0 -1
  8. data/app/models/scimitar/engine_configuration.rb +3 -1
  9. data/app/models/scimitar/lists/query_parser.rb +88 -3
  10. data/app/models/scimitar/resources/base.rb +36 -5
  11. data/app/models/scimitar/resources/mixin.rb +133 -43
  12. data/app/models/scimitar/schema/name.rb +2 -2
  13. data/app/models/scimitar/schema/user.rb +10 -10
  14. data/config/initializers/scimitar.rb +41 -0
  15. data/lib/scimitar/engine.rb +57 -12
  16. data/lib/scimitar/support/utilities.rb +60 -0
  17. data/lib/scimitar/version.rb +2 -2
  18. data/spec/apps/dummy/app/models/mock_user.rb +18 -3
  19. data/spec/apps/dummy/config/initializers/scimitar.rb +31 -2
  20. data/spec/apps/dummy/db/migrate/20210304014602_create_mock_users.rb +1 -0
  21. data/spec/apps/dummy/db/schema.rb +1 -0
  22. data/spec/controllers/scimitar/schemas_controller_spec.rb +342 -54
  23. data/spec/models/scimitar/lists/query_parser_spec.rb +70 -0
  24. data/spec/models/scimitar/resources/base_spec.rb +11 -11
  25. data/spec/models/scimitar/resources/base_validation_spec.rb +16 -3
  26. data/spec/models/scimitar/resources/mixin_spec.rb +71 -10
  27. data/spec/models/scimitar/schema/user_spec.rb +2 -2
  28. data/spec/requests/active_record_backed_resources_controller_spec.rb +231 -0
  29. data/spec/requests/engine_spec.rb +75 -0
  30. data/spec/spec_helper.rb +1 -1
  31. 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 = /#{OPERATORS.keys.join('|')}/i.freeze
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
- return rewritten.join(' ')
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 = options.dup
40
- self.class.extended_schemas.each do |extended_schema|
41
- if extension_attrs = flattened.delete(extended_schema.id)
42
- flattened.merge!(extension_attrs)
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
- flattened
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: :users, # <-- i.e. Team.users,
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 _must+ implement the read accessor identified by the
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's passed the SCIM entry at each
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
- # ResouceController subclass which declares that it maps to the mixing-in
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
- # entites are columns and that's expressed in the names of keys described
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
- # mustsupport #iso8601 to convert to a String representation. Example for a
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+:: The location (HTTP(S) full URI) of this resource, in the
342
- # domain of the object including this mixin - "your" IDs,
343
- # not the remote SCIM client's external IDs. #url_for is a
344
- # good way to generate this.
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
- def to_scim(location:)
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(data_source: self, resource_type: resource_type, attrs_map_or_leaf_value: map)
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
- # Handle extension schema. Contributed by @bettysteger and
487
- # @MorrisFreeman via:
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
- # Note the ":" separating the schema ID (URN) from the attribute.
493
- # The nature of JSON rendering / other payloads might lead you to
494
- # expect a "." as with any complex types, but that's not the case;
495
- # see https://tools.ietf.org/html/rfc7644#section-3.10, or
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
- attribute_tree << schema.id and break if schema.scim_attributes.any? { |attribute| attribute.name == scim_attribute.to_s }
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 = with_attr_map[path_component.to_sym]
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 + [key],
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
- hash.deep_merge!(::Scimitar::Support::Utilities.dot_path(k.split('.'), v))
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', required: true),
10
- Attribute.new(name: 'givenName', type: 'string', required: true),
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', complexType: Scimitar::ComplexTypes::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', 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),
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