scimitar 2.7.1 → 2.7.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -335,7 +335,8 @@ module Scimitar
335
335
  @scim_queryable_attributes ||= self.class.scim_queryable_attributes()
336
336
  end
337
337
 
338
- # Render self as a SCIM object using ::scim_attributes_map.
338
+ # Render self as a SCIM object using ::scim_attributes_map. Fields that
339
+ # are marked as <tt>returned: 'never'</tt> are excluded.
339
340
  #
340
341
  # +location+:: The location (HTTP(S) full URI) of this resource, in the
341
342
  # domain of the object including this mixin - "your" IDs,
@@ -344,9 +345,10 @@ module Scimitar
344
345
  #
345
346
  def to_scim(location:)
346
347
  map = self.class.scim_attributes_map()
348
+ resource_type = self.class.scim_resource_type()
347
349
  timestamps_map = self.class.scim_timestamps_map() if self.class.respond_to?(:scim_timestamps_map)
348
- attrs_hash = self.to_scim_backend(data_source: self, attrs_map_or_leaf_value: map)
349
- resource = self.class.scim_resource_type().new(attrs_hash)
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)
350
352
  meta_attrs_hash = { location: location }
351
353
 
352
354
  meta_attrs_hash[:created ] = self.send(timestamps_map[:created ])&.iso8601(0) if timestamps_map&.key?(:created)
@@ -373,16 +375,39 @@ module Scimitar
373
375
  #
374
376
  # Call ONLY for POST or PUT. For PATCH, see #from_scim_patch!.
375
377
  #
376
- # +scim_hash+:: A Hash that's the result of parsing a JSON payload
377
- # from an inbound POST or PUT request.
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).
378
398
  #
379
399
  # Returns 'self', for convenience of e.g. chaining other methods.
380
400
  #
381
- def from_scim!(scim_hash:)
401
+ def from_scim!(scim_hash:, with_clearing: true)
382
402
  scim_hash.freeze()
383
403
  map = self.class.scim_attributes_map().freeze()
384
404
 
385
- self.from_scim_backend!(attrs_map_or_leaf_value: map, scim_hash_or_leaf_value: scim_hash)
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
+
386
411
  return self
387
412
  end
388
413
 
@@ -458,32 +483,21 @@ module Scimitar
458
483
  ci_scim_hash = { 'root' => ci_scim_hash }.with_indifferent_case_insensitive_access()
459
484
  end
460
485
 
461
- # Handle extension schema. Contributed by @bettysteger and
462
- # @MorrisFreeman via:
463
- #
464
- # https://github.com/RIPAGlobal/scimitar/issues/48
465
- # https://github.com/RIPAGlobal/scimitar/pull/49
466
- #
467
- # Note the ":" separating the schema ID (URN) from the attribute.
468
- # The nature of JSON rendering / other payloads might lead you to
469
- # expect a "." as with any complex types, but that's not the case;
470
- # see https://tools.ietf.org/html/rfc7644#section-3.10, or
471
- # https://tools.ietf.org/html/rfc7644#section-3.5.2 of which in
472
- # particular, https://tools.ietf.org/html/rfc7644#page-35.
486
+ # Split the path into an array of path components, in a way
487
+ # which is aware of extension schemas. See documentation of
488
+ # Scimitar::Support::Utilities.path_str_to_array for details.
473
489
  #
474
- paths = []
475
- self.class.scim_resource_type.extended_schemas.each do |schema|
476
- path_str.downcase.split(schema.id.downcase + ':').drop(1).each do |path|
477
- paths += [schema.id] + path.split('.')
478
- end
479
- end
480
- paths = path_str.split('.') if paths.empty?
490
+ paths = ::Scimitar::Support::Utilities.path_str_to_array(
491
+ self.class.scim_resource_type.extended_schemas,
492
+ path_str
493
+ )
481
494
 
482
495
  self.from_patch_backend!(
483
496
  nature: nature,
484
497
  path: paths,
485
498
  value: value,
486
- altering_hash: ci_scim_hash
499
+ altering_hash: ci_scim_hash,
500
+ with_attr_map: self.class.scim_attributes_map()
487
501
  )
488
502
 
489
503
  if extract_root
@@ -491,7 +505,7 @@ module Scimitar
491
505
  end
492
506
  end
493
507
 
494
- self.from_scim!(scim_hash: ci_scim_hash)
508
+ self.from_scim!(scim_hash: ci_scim_hash, with_clearing: false)
495
509
  return self
496
510
  end
497
511
 
@@ -509,14 +523,48 @@ module Scimitar
509
523
  # this is "self" (an instance of the
510
524
  # class mixing in this module).
511
525
  #
526
+ # +resource_type+:: The resource type carrying the schemas
527
+ # describing the SCIM object. If at the
528
+ # top level when +data_source+ is +self+,
529
+ # this would be sent as
530
+ # <tt>self.class.scim_resource_type()</tt>.
531
+ #
512
532
  # +attrs_map_or_leaf_value+:: The attribute map. At the top level,
513
533
  # this is from ::scim_attributes_map.
514
534
  #
515
- def to_scim_backend(data_source:, attrs_map_or_leaf_value:)
535
+ # Internal recursive calls also send:
536
+ #
537
+ # +attribute_path+:: Array of path components to the
538
+ # attribute, which can be found through
539
+ # +resource_type+ so that things like the
540
+ # "+returned+" state can be checked.
541
+ #
542
+ def to_scim_backend(
543
+ data_source:,
544
+ resource_type:,
545
+ attrs_map_or_leaf_value:,
546
+ attribute_path: []
547
+ )
548
+
549
+ # On assumption of a top-level attributes list, the 'return never'
550
+ # state is only checked on the recursive call from a Hash type. The
551
+ # other handled types are assumed to only happen when called
552
+ # recursively, so no need to check as no such call is made for a
553
+ # 'return never' attribute.
554
+ #
516
555
  case attrs_map_or_leaf_value
517
556
  when Hash # Expected at top-level of any map, or nested within
518
557
  attrs_map_or_leaf_value.each.with_object({}) do |(key, value), hash|
519
- hash[key] = to_scim_backend(data_source: data_source, attrs_map_or_leaf_value: value)
558
+ nested_attribute_path = attribute_path + [key]
559
+
560
+ if resource_type.find_attribute(*nested_attribute_path)&.returned != "never"
561
+ hash[key] = to_scim_backend(
562
+ data_source: data_source,
563
+ resource_type: resource_type,
564
+ attribute_path: nested_attribute_path,
565
+ attrs_map_or_leaf_value: value
566
+ )
567
+ end
520
568
  end
521
569
 
522
570
  when Array # Static or dynamic mapping against lists in data source
@@ -527,14 +575,26 @@ module Scimitar
527
575
 
528
576
  elsif value.key?(:match) # Static map
529
577
  static_hash = { value[:match] => value[:with] }
530
- static_hash.merge!(to_scim_backend(data_source: data_source, attrs_map_or_leaf_value: value[:using]))
578
+ static_hash.merge!(
579
+ to_scim_backend(
580
+ data_source: data_source,
581
+ resource_type: resource_type,
582
+ attribute_path: attribute_path,
583
+ attrs_map_or_leaf_value: value[:using]
584
+ )
585
+ )
531
586
  static_hash
532
587
 
533
588
  elsif value.key?(:list) # Dynamic mapping of each complex list item
534
589
  built_dynamic_list = true
535
590
  list = data_source.public_send(value[:list])
536
591
  list.map do |list_entry|
537
- to_scim_backend(data_source: list_entry, attrs_map_or_leaf_value: value[:using])
592
+ to_scim_backend(
593
+ data_source: list_entry,
594
+ resource_type: resource_type,
595
+ attribute_path: attribute_path,
596
+ attrs_map_or_leaf_value: value[:using]
597
+ )
538
598
  end
539
599
 
540
600
  else # Unknown type, just treat as flat values
@@ -625,6 +685,15 @@ module Scimitar
625
685
  # read as input source material (left
626
686
  # hand side of the ASCII art diagram).
627
687
  #
688
+ # +with_clearing+:: If +true+, attributes absent in
689
+ # +scim_hash_or_leaf_value+ but present
690
+ # in +attrs_map_or_leaf_value+ will be
691
+ # cleared (+nil+ or empty array), for PUT
692
+ # ("replace") semantics. If +false+, such
693
+ # missing attribute values are left
694
+ # untouched - whatever mapped value is in
695
+ # +self+ is preserved.
696
+ #
628
697
  # +path+:: Array of SCIM attribute names giving a
629
698
  # path into the SCIM schema where
630
699
  # iteration has reached. Used to find the
@@ -634,6 +703,7 @@ module Scimitar
634
703
  def from_scim_backend!(
635
704
  attrs_map_or_leaf_value:,
636
705
  scim_hash_or_leaf_value:,
706
+ with_clearing:,
637
707
  path: []
638
708
  )
639
709
  scim_hash_or_leaf_value = scim_hash_or_leaf_value.with_indifferent_case_insensitive_access() if scim_hash_or_leaf_value.is_a?(Hash)
@@ -658,19 +728,43 @@ module Scimitar
658
728
  # https://github.com/RIPAGlobal/scimitar/issues/48
659
729
  # https://github.com/RIPAGlobal/scimitar/pull/49
660
730
  #
731
+ # Note the shortcoming that attribute names within extensions
732
+ # must be unique, as this mechanism basically just pulls out
733
+ # extension attributes to the top level, losing what amounts
734
+ # to the namespace that the extension schema ID provides.
735
+ #
661
736
  attribute_tree = []
662
737
  resource_class.extended_schemas.each do |schema|
663
- attribute_tree << schema.id and break if schema.scim_attributes.any? { |attribute| attribute.name == scim_attribute.to_s }
738
+ if schema.scim_attributes.any? { |attribute| attribute.name == scim_attribute.to_s }
739
+ attribute_tree << schema.id
740
+ break # NOTE EARLY LOOP EXIT
741
+ end
664
742
  end
665
743
  attribute_tree << scim_attribute.to_s
666
744
 
667
- sub_scim_hash_or_leaf_value = scim_hash_or_leaf_value&.dig(*attribute_tree)
745
+ continue_processing = if with_clearing
746
+ true
747
+ else
748
+ most_of_attribute_tree = attribute_tree[...-1]
749
+ last_attribute_in_tree = attribute_tree.last
750
+
751
+ if most_of_attribute_tree.empty?
752
+ scim_hash_or_leaf_value&.key?(last_attribute_in_tree)
753
+ else
754
+ scim_hash_or_leaf_value&.dig(*most_of_attribute_tree)&.key?(last_attribute_in_tree)
755
+ end
756
+ end
757
+
758
+ if continue_processing
759
+ sub_scim_hash_or_leaf_value = scim_hash_or_leaf_value&.dig(*attribute_tree)
668
760
 
669
- self.from_scim_backend!(
670
- attrs_map_or_leaf_value: sub_attrs_map_or_leaf_value,
671
- scim_hash_or_leaf_value: sub_scim_hash_or_leaf_value, # May be 'nil'
672
- path: path + [scim_attribute]
673
- )
761
+ self.from_scim_backend!(
762
+ attrs_map_or_leaf_value: sub_attrs_map_or_leaf_value,
763
+ scim_hash_or_leaf_value: sub_scim_hash_or_leaf_value, # May be 'nil'
764
+ with_clearing: with_clearing,
765
+ path: path + [scim_attribute]
766
+ )
767
+ end
674
768
  end
675
769
 
676
770
  when Array # Static or dynamic maps
@@ -692,6 +786,7 @@ module Scimitar
692
786
  self.from_scim_backend!(
693
787
  attrs_map_or_leaf_value: sub_attrs_map,
694
788
  scim_hash_or_leaf_value: found_source_list_entry, # May be 'nil'
789
+ with_clearing: with_clearing,
695
790
  path: path
696
791
  )
697
792
 
@@ -747,7 +842,9 @@ module Scimitar
747
842
  # +path+:: Operation path, as a series of array entries (so
748
843
  # an inbound dot-separated path string would first
749
844
  # be split into an array by the caller). For
750
- # internal recursive calls, this will
845
+ # internal recursive calls, this will be a subset
846
+ # of array entries from an index somewhere into the
847
+ # top-level array, through to its end.
751
848
  #
752
849
  # +value+:: The value to apply at the attribute(s) identified
753
850
  # by +path+. Ignored for 'remove' operations.
@@ -763,7 +860,7 @@ module Scimitar
763
860
  # own wrapping Hash with a single key addressing the SCIM object of
764
861
  # interest and supply this key as the sole array entry in +path+.
765
862
  #
766
- def from_patch_backend!(nature:, path:, value:, altering_hash:)
863
+ def from_patch_backend!(nature:, path:, value:, altering_hash:, with_attr_map:)
767
864
  raise 'Case sensitivity violation' unless altering_hash.is_a?(Scimitar::Support::HashWithIndifferentCaseInsensitiveAccess)
768
865
 
769
866
  # These all throw exceptions if data is not as expected / required,
@@ -774,14 +871,16 @@ module Scimitar
774
871
  nature: nature,
775
872
  path: path,
776
873
  value: value,
777
- altering_hash: altering_hash
874
+ altering_hash: altering_hash,
875
+ with_attr_map: with_attr_map
778
876
  )
779
877
  else
780
878
  from_patch_backend_traverse!(
781
879
  nature: nature,
782
880
  path: path,
783
881
  value: value,
784
- altering_hash: altering_hash
882
+ altering_hash: altering_hash,
883
+ with_attr_map: with_attr_map
785
884
  )
786
885
  end
787
886
 
@@ -801,7 +900,7 @@ module Scimitar
801
900
  #
802
901
  # Happily throws exceptions if data is not as expected / required.
803
902
  #
804
- def from_patch_backend_traverse!(nature:, path:, value:, altering_hash:)
903
+ def from_patch_backend_traverse!(nature:, path:, value:, altering_hash:, with_attr_map:)
805
904
  raise 'Case sensitivity violation' unless altering_hash.is_a?(Scimitar::Support::HashWithIndifferentCaseInsensitiveAccess)
806
905
 
807
906
  path_component, filter = extract_filter_from(path_component: path.first)
@@ -847,11 +946,27 @@ module Scimitar
847
946
  end
848
947
 
849
948
  found_data_for_recursion.each do | found_data |
949
+ attr_map = if path_component.to_sym == :root
950
+ with_attr_map
951
+ else
952
+ with_attr_map[path_component.to_sym]
953
+ end
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
+
850
964
  self.from_patch_backend!(
851
965
  nature: nature,
852
- path: path[1..-1],
966
+ path: path[1..],
853
967
  value: value,
854
- altering_hash: found_data
968
+ altering_hash: found_data,
969
+ with_attr_map: attr_map
855
970
  )
856
971
  end
857
972
  end
@@ -866,7 +981,7 @@ module Scimitar
866
981
  #
867
982
  # Happily throws exceptions if data is not as expected / required.
868
983
  #
869
- def from_patch_backend_apply!(nature:, path:, value:, altering_hash:)
984
+ def from_patch_backend_apply!(nature:, path:, value:, altering_hash:, with_attr_map:)
870
985
  raise 'Case sensitivity violation' unless altering_hash.is_a?(Scimitar::Support::HashWithIndifferentCaseInsensitiveAccess)
871
986
 
872
987
  path_component, filter = extract_filter_from(path_component: path.first)
@@ -896,11 +1011,48 @@ module Scimitar
896
1011
 
897
1012
  case nature
898
1013
  when 'remove'
899
- current_data_at_path[matched_index] = nil
900
- compact_after = true
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
+
901
1052
  when 'replace'
902
1053
  matched_hash.reject! { true }
903
1054
  matched_hash.merge!(value)
1055
+
904
1056
  end
905
1057
  end
906
1058
 
@@ -939,11 +1091,30 @@ module Scimitar
939
1091
  # at key 'members' with the above, rather than adding.
940
1092
  #
941
1093
  value.keys.each do | key |
1094
+
1095
+ # Handle the Azure (Entra) case where keys might use
1096
+ # dotted paths - see:
1097
+ #
1098
+ # https://github.com/RIPAGlobal/scimitar/issues/123
1099
+ #
1100
+ # ...along with keys containing schema IDs - see:
1101
+ #
1102
+ # https://is.docs.wso2.com/en/next/apis/scim2-patch-operations/#add-user-attributes
1103
+ #
1104
+ # ...and scroll down to example 3 of "Complex singular
1105
+ # attributes".
1106
+ #
1107
+ subpaths = ::Scimitar::Support::Utilities.path_str_to_array(
1108
+ self.class.scim_resource_type.extended_schemas,
1109
+ key
1110
+ )
1111
+
942
1112
  from_patch_backend!(
943
1113
  nature: nature,
944
- path: path + [key],
1114
+ path: path + subpaths,
945
1115
  value: value[key],
946
- altering_hash: altering_hash
1116
+ altering_hash: altering_hash,
1117
+ with_attr_map: with_attr_map
947
1118
  )
948
1119
  end
949
1120
  else
@@ -953,8 +1124,14 @@ module Scimitar
953
1124
  when 'replace'
954
1125
  if path_component == 'root'
955
1126
  dot_pathed_value = value.inject({}) do |hash, (k, v)|
956
- hash.deep_merge!(::Scimitar::Support::Utilities.dot_path(k.split('.'), v))
1127
+ subpaths = ::Scimitar::Support::Utilities.path_str_to_array(
1128
+ self.class.scim_resource_type.extended_schemas,
1129
+ k
1130
+ )
1131
+
1132
+ hash.deep_merge!(::Scimitar::Support::Utilities.dot_path(subpaths, v))
957
1133
  end
1134
+
958
1135
  altering_hash[path_component].deep_merge!(dot_pathed_value)
959
1136
  else
960
1137
  altering_hash[path_component] = value
@@ -1013,8 +1190,8 @@ module Scimitar
1013
1190
  # integer primary keys, which all end up as strings anyway.
1014
1191
  #
1015
1192
  value.each do | value_item |
1016
- altering_hash[path_component].delete_if do | item |
1017
- if item.is_a?(Hash) && value_item.is_a?(Hash)
1193
+ altering_hash[path_component].map! do | item |
1194
+ item_is_matched = if item.is_a?(Hash) && value_item.is_a?(Hash)
1018
1195
  matched_all = true
1019
1196
  value_item.each do | value_key, value_value |
1020
1197
  next if value_key == '$ref'
@@ -1026,10 +1203,55 @@ module Scimitar
1026
1203
  else
1027
1204
  item&.to_s == value_item&.to_s
1028
1205
  end
1206
+
1207
+ if item_is_matched
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
+ array_attr_map = find_matching_static_attr_map(
1212
+ data: item,
1213
+ with_attr_map: attr_map_entry
1214
+ )
1215
+
1216
+ handled = clear_data_for_removal!(
1217
+ altering_hash: item,
1218
+ with_attr_map: array_attr_map
1219
+ )
1220
+
1221
+ handled ? item : nil
1222
+ else
1223
+ item
1224
+ end
1029
1225
  end
1226
+
1227
+ altering_hash[path_component].compact!
1228
+ end
1229
+
1230
+ elsif altering_hash[path_component].is_a?(Array)
1231
+ handled = false
1232
+ attr_map_path = path[..-2] + [path_component]
1233
+ attr_map_entry = with_attr_map.dig(*attr_map_path.map(&:to_sym))
1234
+
1235
+ if attr_map_entry.is_a?(Array) # Array mapping
1236
+ altering_hash[path_component].each do | data_to_check |
1237
+ array_attr_map = find_matching_static_attr_map(
1238
+ data: data_to_check,
1239
+ with_attr_map: attr_map_entry
1240
+ )
1241
+
1242
+ handled = clear_data_for_removal!(
1243
+ altering_hash: data_to_check,
1244
+ with_attr_map: array_attr_map
1245
+ )
1246
+ end
1247
+ end
1248
+
1249
+ if handled == false
1250
+ altering_hash[path_component] = []
1030
1251
  end
1252
+
1031
1253
  else
1032
- altering_hash.delete(path_component)
1254
+ altering_hash[path_component] = nil
1033
1255
  end
1034
1256
 
1035
1257
  end
@@ -1108,6 +1330,177 @@ module Scimitar
1108
1330
  end
1109
1331
  end
1110
1332
 
1333
+ # Static attribute maps are used where SCIM attributes include some
1334
+ # kind of array, but it's not an arbitrary collection (dynamic maps
1335
+ # handle those). Instead, specific matched values inside the SCIM
1336
+ # data are mapped to specific attributes in the local data model.
1337
+ #
1338
+ # A typical example is for e-mails, where the SCIM "type" field in an
1339
+ # array of e-mail addresses might get mapped to detect specific types
1340
+ # of address such as "work" and "home", which happen to be stored
1341
+ # locally in dedicated attributes (e.g. "work_email_address").
1342
+ #
1343
+ # During certain processing operations we end up with a set of data
1344
+ # sent in from some SCIM operation and need to make modifications
1345
+ # (e.g. for a PATCH) that require the attribute map corresponding to
1346
+ # each part of the inbound SCIM data to be known. That's where this
1347
+ # method comes in. Usually, it's not hard to traverse a path of SCIM
1348
+ # data and dig a corresponding path through the attribute map Hash,
1349
+ # except for static arrays. There, we need to know which of the
1350
+ # static map entries matches a piece of SCIM data *from entries* in
1351
+ # the array of SCIM data corresponding to the static map.
1352
+ #
1353
+ # Call here with a piece of SCIM data from an array, along with an
1354
+ # attribute map fragment that must be the Array containing mappings.
1355
+ # Static mapping entries from this are compared with the data and if
1356
+ # a match is found, the sub-attribute map from the static entry's
1357
+ # <tt>:using</tt> key is returned; else +nil+.
1358
+ #
1359
+ # Named parameters are:
1360
+ #
1361
+ # +data+:: A SCIM data entry from a SCIM data array which is
1362
+ # mapped via the data given in the +with_attr_map+
1363
+ # parameter.
1364
+ #
1365
+ # +with_attr_map+:: The attributes map fragment which must be an
1366
+ # Array of mappings for the corresponding array
1367
+ # in the SCIM data from which +data+ was drawn.
1368
+ #
1369
+ # For example, if SCIM data consisted of:
1370
+ #
1371
+ # {
1372
+ # 'emails' => [
1373
+ # {
1374
+ # 'type' => 'work',
1375
+ # 'value' => 'work_1@test.com'
1376
+ # },
1377
+ # {
1378
+ # 'type' => 'work',
1379
+ # 'value' => 'work_2@test.com'
1380
+ # }
1381
+ # ]
1382
+ # }
1383
+ #
1384
+ # ...which was mapped to the local data model using the following
1385
+ # attribute map:
1386
+ #
1387
+ # {
1388
+ # emails: [
1389
+ # { match: 'type', with: 'home', using: { value: :home_email } },
1390
+ # { match: 'type', with: 'work', using: { value: :work_email } },
1391
+ # ]
1392
+ # }
1393
+ #
1394
+ # ...then when it came to processing the SCIM 'emails' entry, one of
1395
+ # the array _entries_ therein would be passed in +data+, while the
1396
+ # attribute map's <tt>:emails</tt> key's value (the _array_ of map
1397
+ # data) would be given in <tt>:with_attr_map</tt>. The first SCIM
1398
+ # array entry matches +work+ so the <tt>:using</tt> part of the map
1399
+ # for that match would be returned:
1400
+ #
1401
+ # { value: :work_email }
1402
+ #
1403
+ # If there was a SCIM entry with a type of something unrecognised,
1404
+ # such as 'holday', then +nil+ would be returned since there is no
1405
+ # matching attribute map entry.
1406
+ #
1407
+ # Note that the <tt>:with_attr_map</tt> array can contain dynamic
1408
+ # mappings or even be just a simple fixed array - only things that
1409
+ # "look like" static mapping entries are processed (i.e. Hashes with
1410
+ # a Symbol key of <tt>:match</tt> present), with the rest ignored.
1411
+ #
1412
+ def find_matching_static_attr_map(data:, with_attr_map:)
1413
+ matched_map = with_attr_map.find do | static_or_dynamic_mapping |
1414
+
1415
+ # Only interested in Static Array mappings.
1416
+ #
1417
+ if static_or_dynamic_mapping.is_a?(Hash) && static_or_dynamic_mapping.key?(:match)
1418
+
1419
+ attr_to_match = static_or_dynamic_mapping[:match].to_s
1420
+ value_to_match = static_or_dynamic_mapping[:with]
1421
+ sub_attrs_map = static_or_dynamic_mapping[:using]
1422
+
1423
+ # If this mapping refers to the matched data at hand,
1424
+ # then we can process it further (see later below.
1425
+ #
1426
+ found = data[attr_to_match] == value_to_match
1427
+
1428
+ # Not found? No static map match perhaps; this could be
1429
+ # because a filter worked on a value which is fixed in
1430
+ # the static map. For example, a filter might check for
1431
+ # emails with "primary true", and the emergence of the
1432
+ # value for "primary" might not be in the data model -
1433
+ # it could be a constant declared in the 'using' part
1434
+ # of a static map. Ugh! Check for that.
1435
+ #
1436
+ unless found
1437
+ sub_attrs_map.each do | scim_attr, model_attr_or_constant |
1438
+
1439
+ # Only want constants such as 'true' or 'false'.
1440
+ #
1441
+ next if model_attr_or_constant.is_a?(Symbol)
1442
+
1443
+ # Does the static value match in the source data?
1444
+ # E.g. a SCIM attribute :primary with value 'true'.
1445
+ #
1446
+ if data[scim_attr] == model_attr_or_constant
1447
+ found = true
1448
+ break
1449
+ end
1450
+ end
1451
+ end
1452
+
1453
+ found
1454
+ else
1455
+ false
1456
+ end
1457
+ end
1458
+
1459
+ return matched_map&.dig(:using)
1460
+ end
1461
+
1462
+ # Related to #find_matching_static_attr_map - often, the reason to
1463
+ # find a static array entry related to some inbound SCIM data is for
1464
+ # a removal operation, where the way to "remove" the data in the
1465
+ # local data model is to set an attribute to "nil". This means you
1466
+ # need to know if there is an attribute writer related to the SCIM
1467
+ # data being removed - and #find_matching_static_attr_map helps.
1468
+ #
1469
+ # With that done, you can call here with the hash data to be changed
1470
+ # and fragment of attribute map that #find_matching_static_attr_map
1471
+ # (or something like it) found.
1472
+ #
1473
+ # +altering_hash+:: The fragment of SCIM data that might be updated
1474
+ # with +nil+ to ultimately lead to an atttribute
1475
+ # writer identified through +with_attr_map+ being
1476
+ # called with that value. This is often the same
1477
+ # that was passed in the +data+ attribute in a
1478
+ # prior #find_matching_static_attr_map call.
1479
+ #
1480
+ # +with_attr_map:: The map fragment that corresponds exactly to the
1481
+ # +altering_hash+ data - e.g. the return value of a
1482
+ # prior #find_matching_static_attr_map call.
1483
+ #
1484
+ # Update +altering_hash+ in place if the map finds a relevant local
1485
+ # data model attribute and returns +true+. If no changes are made,
1486
+ # returns +false+.
1487
+ #
1488
+ def clear_data_for_removal!(altering_hash:, with_attr_map:)
1489
+ handled = false
1490
+
1491
+ with_attr_map&.each do | scim_attr, model_attr_or_constant |
1492
+
1493
+ # Only process attribute names, not constants.
1494
+ #
1495
+ next unless model_attr_or_constant.is_a?(Symbol)
1496
+
1497
+ altering_hash[scim_attr] = nil
1498
+ handled = true
1499
+ end
1500
+
1501
+ return handled
1502
+ end
1503
+
1111
1504
  end # "included do"
1112
1505
  end # "module Mixin"
1113
1506
  end # "module Resources"