scimitar 2.7.2 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6898633f866963ccaacd5f16120a957f698e2e1aacd51b985c483616fe5c49d5
4
- data.tar.gz: d24f2017fab7ae3432c21d5eacb281fa8cd1ac3b43a25756ccbf105a426bf85f
3
+ metadata.gz: 7524696efa05186edcbba9876a3309d3c8a8d51772e3ac0a009a9e83d8f25d16
4
+ data.tar.gz: e6cad46dc8754981f1d85cc9bcc98f73f9edddfa0d0386497b710acd11ad666c
5
5
  SHA512:
6
- metadata.gz: cefe2b8fb441822a751ba93d54b43dc6e75281cc707095f3880c9927d413d310dbcb0676405499f46e641cb767a6d50d3d03b9a0d1b6d1218fbd59e3c036caba
7
- data.tar.gz: 34bc849312df18601c49df4f7e9019ed258b74bb84a002309a5d33b8119c4776edfb1e7cbacdcebbc92533b9cf63586c33631ac505ca1bb17bc859a9d1a65078
6
+ metadata.gz: d040bbf693140f5e62dea569d02d977cb5fd966d2464c0a722bea449428de097901471a2f4b0c2380cf935c424195336d7eed4f469a8ebd019d4277dbe90c629
7
+ data.tar.gz: 15616cedc7fffc3df69dcc1b2f35e9bd3c5408e12f8cccd536d724ce2925c72e4aa9a8f63732135ba3a71d23438dc02e8835978ff37977722cd23cab63c1ee28
@@ -483,26 +483,14 @@ module Scimitar
483
483
  ci_scim_hash = { 'root' => ci_scim_hash }.with_indifferent_case_insensitive_access()
484
484
  end
485
485
 
486
- # Handle extension schema. Contributed by @bettysteger and
487
- # @MorrisFreeman via:
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.
488
489
  #
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?
490
+ paths = ::Scimitar::Support::Utilities.path_str_to_array(
491
+ self.class.scim_resource_type.extended_schemas,
492
+ path_str
493
+ )
506
494
 
507
495
  self.from_patch_backend!(
508
496
  nature: nature,
@@ -740,9 +728,17 @@ module Scimitar
740
728
  # https://github.com/RIPAGlobal/scimitar/issues/48
741
729
  # https://github.com/RIPAGlobal/scimitar/pull/49
742
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
+ #
743
736
  attribute_tree = []
744
737
  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 }
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
746
742
  end
747
743
  attribute_tree << scim_attribute.to_s
748
744
 
@@ -950,7 +946,11 @@ module Scimitar
950
946
  end
951
947
 
952
948
  found_data_for_recursion.each do | found_data |
953
- attr_map = with_attr_map[path_component.to_sym]
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
954
 
955
955
  # Static array mappings need us to find the right map entry that
956
956
  # corresponds to the SCIM data at hand and recurse back into the
@@ -1091,9 +1091,27 @@ module Scimitar
1091
1091
  # at key 'members' with the above, rather than adding.
1092
1092
  #
1093
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
+
1094
1112
  from_patch_backend!(
1095
1113
  nature: nature,
1096
- path: path + [key],
1114
+ path: path + subpaths,
1097
1115
  value: value[key],
1098
1116
  altering_hash: altering_hash,
1099
1117
  with_attr_map: with_attr_map
@@ -1106,7 +1124,12 @@ module Scimitar
1106
1124
  when 'replace'
1107
1125
  if path_component == 'root'
1108
1126
  dot_pathed_value = value.inject({}) do |hash, (k, v)|
1109
- 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))
1110
1133
  end
1111
1134
 
1112
1135
  altering_hash[path_component].deep_merge!(dot_pathed_value)
@@ -46,6 +46,61 @@ module Scimitar
46
46
  hash[array.shift()] = self.dot_path(array, value)
47
47
  end
48
48
  end
49
+
50
+ # Schema ID-aware splitter handling ":" or "." separators. Adapted from
51
+ # contribution by @bettysteger and @MorrisFreeman in:
52
+ #
53
+ # https://github.com/RIPAGlobal/scimitar/issues/48
54
+ # https://github.com/RIPAGlobal/scimitar/pull/49
55
+ #
56
+ # +schemas:: Array of extension schemas, e.g. a SCIM resource class'
57
+ # <tt>scim_resource_type.extended_schemas</tt> value. The
58
+ # Array should be empty if there are no extensions.
59
+ #
60
+ # +path_str+:: Path string, e.g. <tt>"password"</tt>, <tt>"name.givenName"</tt>,
61
+ # <tt>"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"</tt> (special case),
62
+ # <tt>"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:organization"</tt>
63
+ #
64
+ # Returns an array of components, e.g. <tt>["password"]</tt>, <tt>["name",
65
+ # "givenName"]</tt>,
66
+ # <tt>["urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"]</tt> (special case),
67
+ # <tt>["urn:ietf:params:scim:schemas:extension:enterprise:2.0:User", "organization"]</tt>.
68
+ #
69
+ # The called-out special case is for a schema ID without any appended
70
+ # path components, which is returned as a single element ID to aid in
71
+ # traversal particularly of things like PATCH requests. There, a "value"
72
+ # attribute might have a key string that's simply a schema ID, with an
73
+ # object beneath that's got attribute-name pairs, possibly nested, in a
74
+ # path-free payload.
75
+ #
76
+ def self.path_str_to_array(schemas, path_str)
77
+ components = []
78
+
79
+ # Note the ":" separating the schema ID (URN) from the attribute.
80
+ # The nature of JSON rendering / other payloads might lead you to
81
+ # expect a "." as with any complex types, but that's not the case;
82
+ # see https://tools.ietf.org/html/rfc7644#section-3.10, or
83
+ # https://tools.ietf.org/html/rfc7644#section-3.5.2 of which in
84
+ # particular, https://tools.ietf.org/html/rfc7644#page-35.
85
+ #
86
+ if path_str.include?(':')
87
+ schemas.each do |schema|
88
+ attributes_after_schema_id = path_str.downcase.split(schema.id.downcase + ':').drop(1)
89
+
90
+ if attributes_after_schema_id.empty?
91
+ components += [schema.id]
92
+ else
93
+ attributes_after_schema_id.each do |component|
94
+ components += [schema.id] + component.split('.')
95
+ end
96
+ end
97
+ end
98
+ end
99
+
100
+ components = path_str.split('.') if components.empty?
101
+ return components
102
+ end
103
+
49
104
  end
50
105
  end
51
106
  end
@@ -3,11 +3,11 @@ module Scimitar
3
3
  # Gem version. If this changes, be sure to re-run "bundle install" or
4
4
  # "bundle update".
5
5
  #
6
- VERSION = '2.7.2'
6
+ VERSION = '2.7.3'
7
7
 
8
8
  # Date for VERSION. If this changes, be sure to re-run "bundle install"
9
9
  # or "bundle update".
10
10
  #
11
- DATE = '2024-03-27'
11
+ DATE = '2024-06-11'
12
12
 
13
13
  end
@@ -764,6 +764,106 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
764
764
  expect(@u2.password).to eql('oldpassword')
765
765
  end
766
766
 
767
+ context 'which' do
768
+ shared_examples 'it handles not-to-spec in-value Azure/Entra dotted attribute paths' do | operation |
769
+ it "and performs operation" do
770
+ payload = {
771
+ Operations: [
772
+ {
773
+ op: 'add',
774
+ value: {
775
+ 'name.givenName' => 'Foo!',
776
+ 'name.familyName' => 'Bar!',
777
+ 'name.formatted' => 'Foo! Bar!' # Unrecognised; should be ignored
778
+ },
779
+ },
780
+ ]
781
+ }
782
+
783
+ payload = spec_helper_hupcase(payload) if force_upper_case
784
+ patch "/Users/#{@u2.primary_key}", params: payload.merge(format: :scim)
785
+
786
+ expect(response.status ).to eql(200)
787
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
788
+
789
+ @u2.reload
790
+ result = JSON.parse(response.body)
791
+
792
+ expect(@u2.first_name).to eql('Foo!')
793
+ expect(@u2.last_name ).to eql('Bar!')
794
+ end
795
+ end
796
+
797
+ it_behaves_like 'it handles not-to-spec in-value Azure/Entra dotted attribute paths', 'add'
798
+ it_behaves_like 'it handles not-to-spec in-value Azure/Entra dotted attribute paths', 'replace'
799
+
800
+ shared_examples 'it handles schema ID value keys without inline attributes' do | operation |
801
+ it "and performs operation" do
802
+ payload = {
803
+ Operations: [
804
+ {
805
+ op: operation,
806
+ value: {
807
+ 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User': {
808
+ 'organization' => 'Foo Bar!',
809
+ 'department' => 'Bar Foo!'
810
+ },
811
+ },
812
+ },
813
+ ]
814
+ }
815
+
816
+ @u2.update!(organization: 'Old org')
817
+ payload = spec_helper_hupcase(payload) if force_upper_case
818
+ patch "/Users/#{@u2.primary_key}", params: payload.merge(format: :scim)
819
+
820
+ expect(response.status ).to eql(200)
821
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
822
+
823
+ @u2.reload
824
+ result = JSON.parse(response.body)
825
+
826
+ expect(@u2.organization).to eql('Foo Bar!')
827
+ expect(@u2.department ).to eql('Bar Foo!')
828
+ end
829
+ end
830
+
831
+ it_behaves_like 'it handles schema ID value keys without inline attributes', 'add'
832
+ it_behaves_like 'it handles schema ID value keys without inline attributes', 'replace'
833
+
834
+ shared_examples 'it handles schema ID value keys with inline attributes' do
835
+ it "and performs operation" do
836
+ payload = {
837
+ Operations: [
838
+ {
839
+ op: 'add',
840
+ value: {
841
+ 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:organization' => 'Foo Bar!',
842
+ 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:department' => 'Bar Foo!'
843
+ },
844
+ },
845
+ ]
846
+ }
847
+
848
+ @u2.update!(organization: 'Old org')
849
+ payload = spec_helper_hupcase(payload) if force_upper_case
850
+ patch "/Users/#{@u2.primary_key}", params: payload.merge(format: :scim)
851
+
852
+ expect(response.status ).to eql(200)
853
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
854
+
855
+ @u2.reload
856
+ result = JSON.parse(response.body)
857
+
858
+ expect(@u2.organization).to eql('Foo Bar!')
859
+ expect(@u2.department ).to eql('Bar Foo!')
860
+ end
861
+ end
862
+
863
+ it_behaves_like 'it handles schema ID value keys with inline attributes', 'add'
864
+ it_behaves_like 'it handles schema ID value keys with inline attributes', 'replace'
865
+ end
866
+
767
867
  it 'which patches "returned: \'never\'" fields' do
768
868
  payload = {
769
869
  Operations: [
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scimitar
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.7.2
4
+ version: 2.7.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - RIPA Global
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2024-03-27 00:00:00.000000000 Z
12
+ date: 2024-06-11 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails