scimitar 2.7.1 → 2.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +0 -2
- data/app/models/scimitar/resources/base.rb +12 -9
- data/app/models/scimitar/resources/mixin.rb +448 -55
- data/lib/scimitar/support/hash_with_indifferent_case_insensitive_access.rb +140 -10
- data/lib/scimitar/support/utilities.rb +55 -0
- data/lib/scimitar/version.rb +2 -2
- data/spec/models/scimitar/resources/base_spec.rb +9 -1
- data/spec/models/scimitar/resources/mixin_spec.rb +683 -112
- data/spec/requests/active_record_backed_resources_controller_spec.rb +181 -5
- data/spec/support/hash_with_indifferent_case_insensitive_access_spec.rb +108 -0
- metadata +2 -2
@@ -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 =
|
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
|
-
#
|
377
|
-
#
|
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!(
|
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
|
-
#
|
462
|
-
#
|
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
|
-
|
476
|
-
path_str
|
477
|
-
|
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
|
-
|
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
|
-
|
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!(
|
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(
|
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
|
-
|
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
|
-
|
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
|
-
|
670
|
-
|
671
|
-
|
672
|
-
|
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
|
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
|
-
|
900
|
-
|
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 +
|
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
|
-
|
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].
|
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
|
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"
|