scimitar 1.8.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/scimitar/active_record_backed_resources_controller.rb +20 -94
  3. data/app/controllers/scimitar/application_controller.rb +13 -41
  4. data/app/controllers/scimitar/schemas_controller.rb +0 -5
  5. data/app/models/scimitar/complex_types/address.rb +6 -0
  6. data/app/models/scimitar/engine_configuration.rb +5 -13
  7. data/app/models/scimitar/error_response.rb +0 -12
  8. data/app/models/scimitar/lists/query_parser.rb +10 -25
  9. data/app/models/scimitar/resource_invalid_error.rb +1 -1
  10. data/app/models/scimitar/resources/base.rb +4 -17
  11. data/app/models/scimitar/resources/mixin.rb +42 -539
  12. data/app/models/scimitar/schema/address.rb +0 -1
  13. data/app/models/scimitar/schema/attribute.rb +5 -14
  14. data/app/models/scimitar/schema/base.rb +1 -1
  15. data/app/models/scimitar/schema/vdtp.rb +1 -1
  16. data/app/models/scimitar/service_provider_configuration.rb +3 -14
  17. data/config/initializers/scimitar.rb +3 -28
  18. data/lib/scimitar/support/hash_with_indifferent_case_insensitive_access.rb +10 -140
  19. data/lib/scimitar/version.rb +2 -2
  20. data/lib/scimitar.rb +2 -7
  21. data/spec/apps/dummy/app/controllers/mock_groups_controller.rb +1 -1
  22. data/spec/apps/dummy/app/models/mock_group.rb +1 -1
  23. data/spec/apps/dummy/app/models/mock_user.rb +8 -36
  24. data/spec/apps/dummy/config/application.rb +1 -0
  25. data/spec/apps/dummy/config/environments/test.rb +28 -5
  26. data/spec/apps/dummy/config/initializers/scimitar.rb +10 -61
  27. data/spec/apps/dummy/config/routes.rb +7 -28
  28. data/spec/apps/dummy/db/migrate/20210304014602_create_mock_users.rb +1 -10
  29. data/spec/apps/dummy/db/migrate/20210308044214_create_join_table_mock_groups_mock_users.rb +3 -8
  30. data/spec/apps/dummy/db/schema.rb +4 -11
  31. data/spec/controllers/scimitar/application_controller_spec.rb +3 -126
  32. data/spec/controllers/scimitar/resource_types_controller_spec.rb +2 -2
  33. data/spec/controllers/scimitar/schemas_controller_spec.rb +2 -10
  34. data/spec/models/scimitar/complex_types/address_spec.rb +4 -3
  35. data/spec/models/scimitar/complex_types/email_spec.rb +2 -0
  36. data/spec/models/scimitar/lists/query_parser_spec.rb +9 -76
  37. data/spec/models/scimitar/resources/base_spec.rb +70 -216
  38. data/spec/models/scimitar/resources/base_validation_spec.rb +2 -27
  39. data/spec/models/scimitar/resources/mixin_spec.rb +129 -1447
  40. data/spec/models/scimitar/schema/attribute_spec.rb +3 -22
  41. data/spec/models/scimitar/schema/base_spec.rb +1 -1
  42. data/spec/models/scimitar/schema/user_spec.rb +0 -10
  43. data/spec/requests/active_record_backed_resources_controller_spec.rb +68 -787
  44. data/spec/requests/application_controller_spec.rb +3 -16
  45. data/spec/spec_helper.rb +0 -8
  46. data/spec/support/hash_with_indifferent_case_insensitive_access_spec.rb +0 -108
  47. metadata +14 -25
  48. data/LICENSE.txt +0 -21
  49. data/README.md +0 -710
  50. data/lib/scimitar/support/utilities.rb +0 -51
  51. data/spec/apps/dummy/app/controllers/custom_create_mock_users_controller.rb +0 -25
  52. data/spec/apps/dummy/app/controllers/custom_replace_mock_users_controller.rb +0 -25
  53. data/spec/apps/dummy/app/controllers/custom_save_mock_users_controller.rb +0 -24
  54. data/spec/apps/dummy/app/controllers/custom_update_mock_users_controller.rb +0 -25
@@ -220,8 +220,13 @@ module Scimitar
220
220
  # allow for different client searching "styles", given ambiguities in RFC
221
221
  # 7644 filter examples).
222
222
  #
223
- # Each value is a hash of queryable SCIM attribute options, described
224
- # below - for example:
223
+ # Each value is a Hash with Symbol keys ':column', naming just one simple
224
+ # column for a mapping; ':columns', with an Array of column names that you
225
+ # want to map using 'OR' for a single search on the corresponding SCIM
226
+ # attribute; or ':ignore' with value 'true', which means that a fitler on
227
+ # the matching attribute is ignored rather than resulting in an "invalid
228
+ # filter" exception - beware possibilities for surprised clients getting a
229
+ # broader result set than expected. Example:
225
230
  #
226
231
  # def self.scim_queryable_attributes
227
232
  # return {
@@ -229,27 +234,10 @@ module Scimitar
229
234
  # 'name.familyName' => { column: :last_name },
230
235
  # 'emails' => { columns: [ :work_email_address, :home_email_address ] },
231
236
  # 'emails.value' => { columns: [ :work_email_address, :home_email_address ] },
232
- # 'emails.type' => { ignore: true },
233
- # 'groups.value' => { column: Group.arel_table[:id] }
237
+ # 'emails.type' => { ignore: true }
234
238
  # }
235
239
  # end
236
240
  #
237
- # Column references can be either a Symbol representing a column within
238
- # the resource model table, or an <tt>Arel::Attribute</tt> instance via
239
- # e.g. <tt>MyModel.arel_table[:my_column]</tt>.
240
- #
241
- # === Queryable SCIM attribute options
242
- #
243
- # +:column+:: Just one simple column for a mapping.
244
- #
245
- # +:columns+:: An Array of columns that you want to map using 'OR' for a
246
- # single search of the corresponding entity.
247
- #
248
- # +:ignore+:: When set to +true+, the matching attribute is ignored rather
249
- # than resulting in an "invalid filter" exception. Beware
250
- # possibilities for surprised clients getting a broader result
251
- # set than expected, since a constraint may have been ignored.
252
- #
253
241
  # Filtering is currently limited and searching within e.g. arrays of data
254
242
  # is not supported; only simple top-level keys can be mapped.
255
243
  #
@@ -335,8 +323,7 @@ module Scimitar
335
323
  @scim_queryable_attributes ||= self.class.scim_queryable_attributes()
336
324
  end
337
325
 
338
- # Render self as a SCIM object using ::scim_attributes_map. Fields that
339
- # are marked as <tt>returned: 'never'</tt> are excluded.
326
+ # Render self as a SCIM object using ::scim_attributes_map.
340
327
  #
341
328
  # +location+:: The location (HTTP(S) full URI) of this resource, in the
342
329
  # domain of the object including this mixin - "your" IDs,
@@ -345,10 +332,9 @@ module Scimitar
345
332
  #
346
333
  def to_scim(location:)
347
334
  map = self.class.scim_attributes_map()
348
- resource_type = self.class.scim_resource_type()
349
335
  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)
351
- resource = resource_type.new(attrs_hash)
336
+ attrs_hash = self.to_scim_backend(data_source: self, attrs_map_or_leaf_value: map)
337
+ resource = self.class.scim_resource_type().new(attrs_hash)
352
338
  meta_attrs_hash = { location: location }
353
339
 
354
340
  meta_attrs_hash[:created ] = self.send(timestamps_map[:created ])&.iso8601(0) if timestamps_map&.key?(:created)
@@ -375,39 +361,16 @@ module Scimitar
375
361
  #
376
362
  # Call ONLY for POST or PUT. For PATCH, see #from_scim_patch!.
377
363
  #
378
- # Mandatory named parameters:
379
- #
380
- # +scim_hash+:: A Hash that's the result of parsing a JSON payload
381
- # from an inbound POST or PUT request.
382
- #
383
- # Optional named parameters:
384
- #
385
- # +with_clearing+:: According to RFC 7644 section 3.5.1, PUT operations
386
- # MAY default or clear any attribute missing from
387
- # +scim_hash+ as this is deemed "not asserted by the
388
- # client" (see
389
- # https://tools.ietf.org/html/rfc7644#section-3.5.1).
390
- # This parameter controls such behaviour. It defaults
391
- # to +true+, so clearing is applied - single value
392
- # attributes are set to +nil+ and arrays are emptied.
393
- # If +false+, an unusual <b>preservation</b> mode is
394
- # applied and anything absent from +scim_hash+ will
395
- # have no impact on the target object (any mapped
396
- # attributes in the local data model with existing
397
- # non-nil values will retain those values).
364
+ # +scim_hash+:: A Hash that's the result of parsing a JSON payload
365
+ # from an inbound POST or PUT request.
398
366
  #
399
367
  # Returns 'self', for convenience of e.g. chaining other methods.
400
368
  #
401
- def from_scim!(scim_hash:, with_clearing: true)
369
+ def from_scim!(scim_hash:)
402
370
  scim_hash.freeze()
403
371
  map = self.class.scim_attributes_map().freeze()
404
372
 
405
- self.from_scim_backend!(
406
- attrs_map_or_leaf_value: map,
407
- scim_hash_or_leaf_value: scim_hash,
408
- with_clearing: with_clearing
409
- )
410
-
373
+ self.from_scim_backend!(attrs_map_or_leaf_value: map, scim_hash_or_leaf_value: scim_hash)
411
374
  return self
412
375
  end
413
376
 
@@ -443,11 +406,8 @@ module Scimitar
443
406
  def from_scim_patch!(patch_hash:)
444
407
  frozen_ci_patch_hash = patch_hash.with_indifferent_case_insensitive_access().freeze()
445
408
  ci_scim_hash = self.to_scim(location: '(unused)').as_json().with_indifferent_case_insensitive_access()
446
- operations = frozen_ci_patch_hash['operations']
447
-
448
- raise Scimitar::InvalidSyntaxError.new("Missing PATCH \"operations\"") unless operations
449
409
 
450
- operations.each do |operation|
410
+ frozen_ci_patch_hash['operations'].each do |operation|
451
411
  nature = operation['op' ]&.downcase
452
412
  path_str = operation['path' ]
453
413
  value = operation['value']
@@ -483,33 +443,11 @@ module Scimitar
483
443
  ci_scim_hash = { 'root' => ci_scim_hash }.with_indifferent_case_insensitive_access()
484
444
  end
485
445
 
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
491
- #
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?
506
-
507
446
  self.from_patch_backend!(
508
447
  nature: nature,
509
- path: paths,
448
+ path: (path_str || '').split('.'),
510
449
  value: value,
511
- altering_hash: ci_scim_hash,
512
- with_attr_map: self.class.scim_attributes_map()
450
+ altering_hash: ci_scim_hash
513
451
  )
514
452
 
515
453
  if extract_root
@@ -517,7 +455,7 @@ module Scimitar
517
455
  end
518
456
  end
519
457
 
520
- self.from_scim!(scim_hash: ci_scim_hash, with_clearing: false)
458
+ self.from_scim!(scim_hash: ci_scim_hash)
521
459
  return self
522
460
  end
523
461
 
@@ -535,48 +473,14 @@ module Scimitar
535
473
  # this is "self" (an instance of the
536
474
  # class mixing in this module).
537
475
  #
538
- # +resource_type+:: The resource type carrying the schemas
539
- # describing the SCIM object. If at the
540
- # top level when +data_source+ is +self+,
541
- # this would be sent as
542
- # <tt>self.class.scim_resource_type()</tt>.
543
- #
544
476
  # +attrs_map_or_leaf_value+:: The attribute map. At the top level,
545
477
  # this is from ::scim_attributes_map.
546
478
  #
547
- # Internal recursive calls also send:
548
- #
549
- # +attribute_path+:: Array of path components to the
550
- # attribute, which can be found through
551
- # +resource_type+ so that things like the
552
- # "+returned+" state can be checked.
553
- #
554
- def to_scim_backend(
555
- data_source:,
556
- resource_type:,
557
- attrs_map_or_leaf_value:,
558
- attribute_path: []
559
- )
560
-
561
- # On assumption of a top-level attributes list, the 'return never'
562
- # state is only checked on the recursive call from a Hash type. The
563
- # other handled types are assumed to only happen when called
564
- # recursively, so no need to check as no such call is made for a
565
- # 'return never' attribute.
566
- #
479
+ def to_scim_backend(data_source:, attrs_map_or_leaf_value:)
567
480
  case attrs_map_or_leaf_value
568
481
  when Hash # Expected at top-level of any map, or nested within
569
482
  attrs_map_or_leaf_value.each.with_object({}) do |(key, value), hash|
570
- nested_attribute_path = attribute_path + [key]
571
-
572
- if resource_type.find_attribute(*nested_attribute_path)&.returned != "never"
573
- hash[key] = to_scim_backend(
574
- data_source: data_source,
575
- resource_type: resource_type,
576
- attribute_path: nested_attribute_path,
577
- attrs_map_or_leaf_value: value
578
- )
579
- end
483
+ hash[key] = to_scim_backend(data_source: data_source, attrs_map_or_leaf_value: value)
580
484
  end
581
485
 
582
486
  when Array # Static or dynamic mapping against lists in data source
@@ -587,26 +491,14 @@ module Scimitar
587
491
 
588
492
  elsif value.key?(:match) # Static map
589
493
  static_hash = { value[:match] => value[:with] }
590
- static_hash.merge!(
591
- to_scim_backend(
592
- data_source: data_source,
593
- resource_type: resource_type,
594
- attribute_path: attribute_path,
595
- attrs_map_or_leaf_value: value[:using]
596
- )
597
- )
494
+ static_hash.merge!(to_scim_backend(data_source: data_source, attrs_map_or_leaf_value: value[:using]))
598
495
  static_hash
599
496
 
600
497
  elsif value.key?(:list) # Dynamic mapping of each complex list item
601
498
  built_dynamic_list = true
602
499
  list = data_source.public_send(value[:list])
603
500
  list.map do |list_entry|
604
- to_scim_backend(
605
- data_source: list_entry,
606
- resource_type: resource_type,
607
- attribute_path: attribute_path,
608
- attrs_map_or_leaf_value: value[:using]
609
- )
501
+ to_scim_backend(data_source: list_entry, attrs_map_or_leaf_value: value[:using])
610
502
  end
611
503
 
612
504
  else # Unknown type, just treat as flat values
@@ -697,15 +589,6 @@ module Scimitar
697
589
  # read as input source material (left
698
590
  # hand side of the ASCII art diagram).
699
591
  #
700
- # +with_clearing+:: If +true+, attributes absent in
701
- # +scim_hash_or_leaf_value+ but present
702
- # in +attrs_map_or_leaf_value+ will be
703
- # cleared (+nil+ or empty array), for PUT
704
- # ("replace") semantics. If +false+, such
705
- # missing attribute values are left
706
- # untouched - whatever mapped value is in
707
- # +self+ is preserved.
708
- #
709
592
  # +path+:: Array of SCIM attribute names giving a
710
593
  # path into the SCIM schema where
711
594
  # iteration has reached. Used to find the
@@ -715,7 +598,6 @@ module Scimitar
715
598
  def from_scim_backend!(
716
599
  attrs_map_or_leaf_value:,
717
600
  scim_hash_or_leaf_value:,
718
- with_clearing:,
719
601
  path: []
720
602
  )
721
603
  scim_hash_or_leaf_value = scim_hash_or_leaf_value.with_indifferent_case_insensitive_access() if scim_hash_or_leaf_value.is_a?(Hash)
@@ -734,41 +616,13 @@ module Scimitar
734
616
  attrs_map_or_leaf_value.each do | scim_attribute, sub_attrs_map_or_leaf_value |
735
617
  next if scim_attribute&.to_s&.downcase == 'id' && path.empty?
736
618
 
737
- # Handle extension schema. Contributed by @bettysteger and
738
- # @MorrisFreeman via:
739
- #
740
- # https://github.com/RIPAGlobal/scimitar/issues/48
741
- # https://github.com/RIPAGlobal/scimitar/pull/49
742
- #
743
- attribute_tree = []
744
- 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 }
746
- end
747
- attribute_tree << scim_attribute.to_s
748
-
749
- continue_processing = if with_clearing
750
- true
751
- else
752
- most_of_attribute_tree = attribute_tree[...-1]
753
- last_attribute_in_tree = attribute_tree.last
754
-
755
- if most_of_attribute_tree.empty?
756
- scim_hash_or_leaf_value&.key?(last_attribute_in_tree)
757
- else
758
- scim_hash_or_leaf_value&.dig(*most_of_attribute_tree)&.key?(last_attribute_in_tree)
759
- end
760
- end
761
-
762
- if continue_processing
763
- sub_scim_hash_or_leaf_value = scim_hash_or_leaf_value&.dig(*attribute_tree)
619
+ sub_scim_hash_or_leaf_value = scim_hash_or_leaf_value&.dig(scim_attribute.to_s)
764
620
 
765
- self.from_scim_backend!(
766
- attrs_map_or_leaf_value: sub_attrs_map_or_leaf_value,
767
- scim_hash_or_leaf_value: sub_scim_hash_or_leaf_value, # May be 'nil'
768
- with_clearing: with_clearing,
769
- path: path + [scim_attribute]
770
- )
771
- end
621
+ self.from_scim_backend!(
622
+ attrs_map_or_leaf_value: sub_attrs_map_or_leaf_value,
623
+ scim_hash_or_leaf_value: sub_scim_hash_or_leaf_value, # May be 'nil'
624
+ path: path + [scim_attribute]
625
+ )
772
626
  end
773
627
 
774
628
  when Array # Static or dynamic maps
@@ -790,7 +644,6 @@ module Scimitar
790
644
  self.from_scim_backend!(
791
645
  attrs_map_or_leaf_value: sub_attrs_map,
792
646
  scim_hash_or_leaf_value: found_source_list_entry, # May be 'nil'
793
- with_clearing: with_clearing,
794
647
  path: path
795
648
  )
796
649
 
@@ -846,9 +699,7 @@ module Scimitar
846
699
  # +path+:: Operation path, as a series of array entries (so
847
700
  # an inbound dot-separated path string would first
848
701
  # be split into an array by the caller). For
849
- # internal recursive calls, this will be a subset
850
- # of array entries from an index somewhere into the
851
- # top-level array, through to its end.
702
+ # internal recursive calls, this will
852
703
  #
853
704
  # +value+:: The value to apply at the attribute(s) identified
854
705
  # by +path+. Ignored for 'remove' operations.
@@ -864,7 +715,7 @@ module Scimitar
864
715
  # own wrapping Hash with a single key addressing the SCIM object of
865
716
  # interest and supply this key as the sole array entry in +path+.
866
717
  #
867
- def from_patch_backend!(nature:, path:, value:, altering_hash:, with_attr_map:)
718
+ def from_patch_backend!(nature:, path:, value:, altering_hash:)
868
719
  raise 'Case sensitivity violation' unless altering_hash.is_a?(Scimitar::Support::HashWithIndifferentCaseInsensitiveAccess)
869
720
 
870
721
  # These all throw exceptions if data is not as expected / required,
@@ -875,16 +726,14 @@ module Scimitar
875
726
  nature: nature,
876
727
  path: path,
877
728
  value: value,
878
- altering_hash: altering_hash,
879
- with_attr_map: with_attr_map
729
+ altering_hash: altering_hash
880
730
  )
881
731
  else
882
732
  from_patch_backend_traverse!(
883
733
  nature: nature,
884
734
  path: path,
885
735
  value: value,
886
- altering_hash: altering_hash,
887
- with_attr_map: with_attr_map
736
+ altering_hash: altering_hash
888
737
  )
889
738
  end
890
739
 
@@ -904,7 +753,7 @@ module Scimitar
904
753
  #
905
754
  # Happily throws exceptions if data is not as expected / required.
906
755
  #
907
- def from_patch_backend_traverse!(nature:, path:, value:, altering_hash:, with_attr_map:)
756
+ def from_patch_backend_traverse!(nature:, path:, value:, altering_hash:)
908
757
  raise 'Case sensitivity violation' unless altering_hash.is_a?(Scimitar::Support::HashWithIndifferentCaseInsensitiveAccess)
909
758
 
910
759
  path_component, filter = extract_filter_from(path_component: path.first)
@@ -950,23 +799,11 @@ module Scimitar
950
799
  end
951
800
 
952
801
  found_data_for_recursion.each do | found_data |
953
- attr_map = with_attr_map[path_component.to_sym]
954
-
955
- # Static array mappings need us to find the right map entry that
956
- # corresponds to the SCIM data at hand and recurse back into the
957
- # patch engine with the ":using" attribute map data.
958
- #
959
- if attr_map.is_a?(Array)
960
- array_attr_map = find_matching_static_attr_map(data: found_data, with_attr_map: attr_map)
961
- attr_map = array_attr_map unless array_attr_map.nil?
962
- end
963
-
964
802
  self.from_patch_backend!(
965
803
  nature: nature,
966
- path: path[1..],
804
+ path: path[1..-1],
967
805
  value: value,
968
- altering_hash: found_data,
969
- with_attr_map: attr_map
806
+ altering_hash: found_data
970
807
  )
971
808
  end
972
809
  end
@@ -981,7 +818,7 @@ module Scimitar
981
818
  #
982
819
  # Happily throws exceptions if data is not as expected / required.
983
820
  #
984
- def from_patch_backend_apply!(nature:, path:, value:, altering_hash:, with_attr_map:)
821
+ def from_patch_backend_apply!(nature:, path:, value:, altering_hash:)
985
822
  raise 'Case sensitivity violation' unless altering_hash.is_a?(Scimitar::Support::HashWithIndifferentCaseInsensitiveAccess)
986
823
 
987
824
  path_component, filter = extract_filter_from(path_component: path.first)
@@ -1011,48 +848,11 @@ module Scimitar
1011
848
 
1012
849
  case nature
1013
850
  when 'remove'
1014
- handled = false
1015
- attr_map_path = path[..-2] + [path_component]
1016
- attr_map_entry = with_attr_map.dig(*attr_map_path.map(&:to_sym))
1017
-
1018
- # Deal with arrays specially; static maps require specific
1019
- # treatment, but dynamic or actual array values do not.
1020
- #
1021
- if attr_map_entry.is_a?(Array)
1022
- array_attr_map = find_matching_static_attr_map(
1023
- data: matched_hash,
1024
- with_attr_map: attr_map_entry
1025
- )
1026
-
1027
- # Found? Run through the mapped attributes. Anything that
1028
- # has an associated model attribute (i.e. some property
1029
- # that must be to be written into local data in response
1030
- # to the SCIM attribute being changed) is 'removed' by
1031
- # setting the corresponding value in "altering_hash" (of
1032
- # which "matched_hash" referenced fragment) to "nil".
1033
- #
1034
- handled = clear_data_for_removal!(
1035
- altering_hash: matched_hash,
1036
- with_attr_map: array_attr_map
1037
- )
1038
- end
1039
-
1040
- # For dynamic arrays or other value types, we assume that
1041
- # just clearing the item from the array or setting its SCIM
1042
- # attribute to "nil" will result in an appropriate update
1043
- # to the local data model (e.g. by a change in an Rails
1044
- # associated collection or clearing a local model attribute
1045
- # directly to "nil").
1046
- #
1047
- if handled == false
1048
- current_data_at_path[matched_index] = nil
1049
- compact_after = true
1050
- end
1051
-
851
+ current_data_at_path[matched_index] = nil
852
+ compact_after = true
1052
853
  when 'replace'
1053
854
  matched_hash.reject! { true }
1054
855
  matched_hash.merge!(value)
1055
-
1056
856
  end
1057
857
  end
1058
858
 
@@ -1095,142 +895,16 @@ module Scimitar
1095
895
  nature: nature,
1096
896
  path: path + [key],
1097
897
  value: value[key],
1098
- altering_hash: altering_hash,
1099
- with_attr_map: with_attr_map
898
+ altering_hash: altering_hash
1100
899
  )
1101
900
  end
1102
901
  else
1103
902
  altering_hash[path_component] = value
1104
903
  end
1105
-
1106
904
  when 'replace'
1107
- if path_component == 'root'
1108
- dot_pathed_value = value.inject({}) do |hash, (k, v)|
1109
- hash.deep_merge!(::Scimitar::Support::Utilities.dot_path(k.split('.'), v))
1110
- end
1111
-
1112
- altering_hash[path_component].deep_merge!(dot_pathed_value)
1113
- else
1114
- altering_hash[path_component] = value
1115
- end
1116
-
1117
- # The array check handles payloads seen from e.g. Microsoft for
1118
- # remove-user-from-group, where contrary to examples in the RFC
1119
- # which would imply "payload removes all users", there is the
1120
- # clear intent to remove just one.
1121
- #
1122
- # https://tools.ietf.org/html/rfc7644#section-3.5.2.2
1123
- # https://learn.microsoft.com/en-us/azure/active-directory/app-provisioning/use-scim-to-provision-users-and-groups#update-group-remove-members
1124
- #
1125
- # Since remove-all in the face of remove-one is destructive, we
1126
- # do a special check here to see if there's an array value for
1127
- # the array path that the payload yielded. If so, we can match
1128
- # each value against array items and remove just those items.
1129
- #
1130
- # There is an additional special case to handle a bad example
1131
- # from Salesforce:
1132
- #
1133
- # https://help.salesforce.com/s/articleView?id=sf.identity_scim_manage_groups.htm&type=5
1134
- #
905
+ altering_hash[path_component] = value
1135
906
  when 'remove'
1136
- if altering_hash[path_component].is_a?(Array) && value.present?
1137
-
1138
- # Handle bad Salesforce example. That might be simply a
1139
- # documentation error, but just in case...
1140
- #
1141
- value = value.values.first if (
1142
- path_component&.downcase == 'members' &&
1143
- value.is_a?(Hash) &&
1144
- value.keys.size == 1 &&
1145
- value.keys.first&.downcase == 'members'
1146
- )
1147
-
1148
- # The Microsoft example provides an array of values, but we
1149
- # may as well cope with a value specified 'flat'. Promote
1150
- # such a thing to an Array to simplify the following code.
1151
- #
1152
- value = [value] unless value.is_a?(Array)
1153
-
1154
- # For each value item, delete matching array entries. The
1155
- # concept of "matching" is:
1156
- #
1157
- # * For simple non-Hash values (if possible) just delete on
1158
- # an exact match
1159
- #
1160
- # * For Hash-based values, only delete if all 'patch' keys
1161
- # are present in the resource and all values thus match.
1162
- #
1163
- # Special case to ignore '$ref' from the Microsoft payload.
1164
- #
1165
- # Note coercion to strings to account for SCIM vs the usual
1166
- # tricky case of underlying implementations with (say)
1167
- # integer primary keys, which all end up as strings anyway.
1168
- #
1169
- value.each do | value_item |
1170
- altering_hash[path_component].map! do | item |
1171
- item_is_matched = if item.is_a?(Hash) && value_item.is_a?(Hash)
1172
- matched_all = true
1173
- value_item.each do | value_key, value_value |
1174
- next if value_key == '$ref'
1175
- if ! item.key?(value_key) || item[value_key]&.to_s != value_value&.to_s
1176
- matched_all = false
1177
- end
1178
- end
1179
- matched_all
1180
- else
1181
- item&.to_s == value_item&.to_s
1182
- end
1183
-
1184
- if item_is_matched
1185
- handled = false
1186
- attr_map_path = path[..-2] + [path_component]
1187
- attr_map_entry = with_attr_map.dig(*attr_map_path.map(&:to_sym))
1188
- array_attr_map = find_matching_static_attr_map(
1189
- data: item,
1190
- with_attr_map: attr_map_entry
1191
- )
1192
-
1193
- handled = clear_data_for_removal!(
1194
- altering_hash: item,
1195
- with_attr_map: array_attr_map
1196
- )
1197
-
1198
- handled ? item : nil
1199
- else
1200
- item
1201
- end
1202
- end
1203
-
1204
- altering_hash[path_component].compact!
1205
- end
1206
-
1207
- elsif altering_hash[path_component].is_a?(Array)
1208
- handled = false
1209
- attr_map_path = path[..-2] + [path_component]
1210
- attr_map_entry = with_attr_map.dig(*attr_map_path.map(&:to_sym))
1211
-
1212
- if attr_map_entry.is_a?(Array) # Array mapping
1213
- altering_hash[path_component].each do | data_to_check |
1214
- array_attr_map = find_matching_static_attr_map(
1215
- data: data_to_check,
1216
- with_attr_map: attr_map_entry
1217
- )
1218
-
1219
- handled = clear_data_for_removal!(
1220
- altering_hash: data_to_check,
1221
- with_attr_map: array_attr_map
1222
- )
1223
- end
1224
- end
1225
-
1226
- if handled == false
1227
- altering_hash[path_component] = []
1228
- end
1229
-
1230
- else
1231
- altering_hash[path_component] = nil
1232
- end
1233
-
907
+ altering_hash.delete(path_component)
1234
908
  end
1235
909
  end
1236
910
  end
@@ -1307,177 +981,6 @@ module Scimitar
1307
981
  end
1308
982
  end
1309
983
 
1310
- # Static attribute maps are used where SCIM attributes include some
1311
- # kind of array, but it's not an arbitrary collection (dynamic maps
1312
- # handle those). Instead, specific matched values inside the SCIM
1313
- # data are mapped to specific attributes in the local data model.
1314
- #
1315
- # A typical example is for e-mails, where the SCIM "type" field in an
1316
- # array of e-mail addresses might get mapped to detect specific types
1317
- # of address such as "work" and "home", which happen to be stored
1318
- # locally in dedicated attributes (e.g. "work_email_address").
1319
- #
1320
- # During certain processing operations we end up with a set of data
1321
- # sent in from some SCIM operation and need to make modifications
1322
- # (e.g. for a PATCH) that require the attribute map corresponding to
1323
- # each part of the inbound SCIM data to be known. That's where this
1324
- # method comes in. Usually, it's not hard to traverse a path of SCIM
1325
- # data and dig a corresponding path through the attribute map Hash,
1326
- # except for static arrays. There, we need to know which of the
1327
- # static map entries matches a piece of SCIM data *from entries* in
1328
- # the array of SCIM data corresponding to the static map.
1329
- #
1330
- # Call here with a piece of SCIM data from an array, along with an
1331
- # attribute map fragment that must be the Array containing mappings.
1332
- # Static mapping entries from this are compared with the data and if
1333
- # a match is found, the sub-attribute map from the static entry's
1334
- # <tt>:using</tt> key is returned; else +nil+.
1335
- #
1336
- # Named parameters are:
1337
- #
1338
- # +data+:: A SCIM data entry from a SCIM data array which is
1339
- # mapped via the data given in the +with_attr_map+
1340
- # parameter.
1341
- #
1342
- # +with_attr_map+:: The attributes map fragment which must be an
1343
- # Array of mappings for the corresponding array
1344
- # in the SCIM data from which +data+ was drawn.
1345
- #
1346
- # For example, if SCIM data consisted of:
1347
- #
1348
- # {
1349
- # 'emails' => [
1350
- # {
1351
- # 'type' => 'work',
1352
- # 'value' => 'work_1@test.com'
1353
- # },
1354
- # {
1355
- # 'type' => 'work',
1356
- # 'value' => 'work_2@test.com'
1357
- # }
1358
- # ]
1359
- # }
1360
- #
1361
- # ...which was mapped to the local data model using the following
1362
- # attribute map:
1363
- #
1364
- # {
1365
- # emails: [
1366
- # { match: 'type', with: 'home', using: { value: :home_email } },
1367
- # { match: 'type', with: 'work', using: { value: :work_email } },
1368
- # ]
1369
- # }
1370
- #
1371
- # ...then when it came to processing the SCIM 'emails' entry, one of
1372
- # the array _entries_ therein would be passed in +data+, while the
1373
- # attribute map's <tt>:emails</tt> key's value (the _array_ of map
1374
- # data) would be given in <tt>:with_attr_map</tt>. The first SCIM
1375
- # array entry matches +work+ so the <tt>:using</tt> part of the map
1376
- # for that match would be returned:
1377
- #
1378
- # { value: :work_email }
1379
- #
1380
- # If there was a SCIM entry with a type of something unrecognised,
1381
- # such as 'holday', then +nil+ would be returned since there is no
1382
- # matching attribute map entry.
1383
- #
1384
- # Note that the <tt>:with_attr_map</tt> array can contain dynamic
1385
- # mappings or even be just a simple fixed array - only things that
1386
- # "look like" static mapping entries are processed (i.e. Hashes with
1387
- # a Symbol key of <tt>:match</tt> present), with the rest ignored.
1388
- #
1389
- def find_matching_static_attr_map(data:, with_attr_map:)
1390
- matched_map = with_attr_map.find do | static_or_dynamic_mapping |
1391
-
1392
- # Only interested in Static Array mappings.
1393
- #
1394
- if static_or_dynamic_mapping.is_a?(Hash) && static_or_dynamic_mapping.key?(:match)
1395
-
1396
- attr_to_match = static_or_dynamic_mapping[:match].to_s
1397
- value_to_match = static_or_dynamic_mapping[:with]
1398
- sub_attrs_map = static_or_dynamic_mapping[:using]
1399
-
1400
- # If this mapping refers to the matched data at hand,
1401
- # then we can process it further (see later below.
1402
- #
1403
- found = data[attr_to_match] == value_to_match
1404
-
1405
- # Not found? No static map match perhaps; this could be
1406
- # because a filter worked on a value which is fixed in
1407
- # the static map. For example, a filter might check for
1408
- # emails with "primary true", and the emergence of the
1409
- # value for "primary" might not be in the data model -
1410
- # it could be a constant declared in the 'using' part
1411
- # of a static map. Ugh! Check for that.
1412
- #
1413
- unless found
1414
- sub_attrs_map.each do | scim_attr, model_attr_or_constant |
1415
-
1416
- # Only want constants such as 'true' or 'false'.
1417
- #
1418
- next if model_attr_or_constant.is_a?(Symbol)
1419
-
1420
- # Does the static value match in the source data?
1421
- # E.g. a SCIM attribute :primary with value 'true'.
1422
- #
1423
- if data[scim_attr] == model_attr_or_constant
1424
- found = true
1425
- break
1426
- end
1427
- end
1428
- end
1429
-
1430
- found
1431
- else
1432
- false
1433
- end
1434
- end
1435
-
1436
- return matched_map&.dig(:using)
1437
- end
1438
-
1439
- # Related to #find_matching_static_attr_map - often, the reason to
1440
- # find a static array entry related to some inbound SCIM data is for
1441
- # a removal operation, where the way to "remove" the data in the
1442
- # local data model is to set an attribute to "nil". This means you
1443
- # need to know if there is an attribute writer related to the SCIM
1444
- # data being removed - and #find_matching_static_attr_map helps.
1445
- #
1446
- # With that done, you can call here with the hash data to be changed
1447
- # and fragment of attribute map that #find_matching_static_attr_map
1448
- # (or something like it) found.
1449
- #
1450
- # +altering_hash+:: The fragment of SCIM data that might be updated
1451
- # with +nil+ to ultimately lead to an atttribute
1452
- # writer identified through +with_attr_map+ being
1453
- # called with that value. This is often the same
1454
- # that was passed in the +data+ attribute in a
1455
- # prior #find_matching_static_attr_map call.
1456
- #
1457
- # +with_attr_map:: The map fragment that corresponds exactly to the
1458
- # +altering_hash+ data - e.g. the return value of a
1459
- # prior #find_matching_static_attr_map call.
1460
- #
1461
- # Update +altering_hash+ in place if the map finds a relevant local
1462
- # data model attribute and returns +true+. If no changes are made,
1463
- # returns +false+.
1464
- #
1465
- def clear_data_for_removal!(altering_hash:, with_attr_map:)
1466
- handled = false
1467
-
1468
- with_attr_map&.each do | scim_attr, model_attr_or_constant |
1469
-
1470
- # Only process attribute names, not constants.
1471
- #
1472
- next unless model_attr_or_constant.is_a?(Symbol)
1473
-
1474
- altering_hash[scim_attr] = nil
1475
- handled = true
1476
- end
1477
-
1478
- return handled
1479
- end
1480
-
1481
984
  end # "included do"
1482
985
  end # "module Mixin"
1483
986
  end # "module Resources"