scimitar 1.8.2 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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"