scimitar 1.8.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/controllers/scimitar/active_record_backed_resources_controller.rb +20 -94
- data/app/controllers/scimitar/application_controller.rb +13 -41
- data/app/controllers/scimitar/schemas_controller.rb +0 -5
- data/app/models/scimitar/complex_types/address.rb +6 -0
- data/app/models/scimitar/engine_configuration.rb +5 -13
- data/app/models/scimitar/error_response.rb +0 -12
- data/app/models/scimitar/lists/query_parser.rb +10 -25
- data/app/models/scimitar/resource_invalid_error.rb +1 -1
- data/app/models/scimitar/resources/base.rb +4 -17
- data/app/models/scimitar/resources/mixin.rb +42 -539
- data/app/models/scimitar/schema/address.rb +0 -1
- data/app/models/scimitar/schema/attribute.rb +5 -14
- data/app/models/scimitar/schema/base.rb +1 -1
- data/app/models/scimitar/schema/vdtp.rb +1 -1
- data/app/models/scimitar/service_provider_configuration.rb +3 -14
- data/config/initializers/scimitar.rb +3 -28
- data/lib/scimitar/support/hash_with_indifferent_case_insensitive_access.rb +10 -140
- data/lib/scimitar/version.rb +2 -2
- data/lib/scimitar.rb +2 -7
- data/spec/apps/dummy/app/controllers/mock_groups_controller.rb +1 -1
- data/spec/apps/dummy/app/models/mock_group.rb +1 -1
- data/spec/apps/dummy/app/models/mock_user.rb +8 -36
- data/spec/apps/dummy/config/application.rb +1 -0
- data/spec/apps/dummy/config/environments/test.rb +28 -5
- data/spec/apps/dummy/config/initializers/scimitar.rb +10 -61
- data/spec/apps/dummy/config/routes.rb +7 -28
- data/spec/apps/dummy/db/migrate/20210304014602_create_mock_users.rb +1 -10
- data/spec/apps/dummy/db/migrate/20210308044214_create_join_table_mock_groups_mock_users.rb +3 -8
- data/spec/apps/dummy/db/schema.rb +4 -11
- data/spec/controllers/scimitar/application_controller_spec.rb +3 -126
- data/spec/controllers/scimitar/resource_types_controller_spec.rb +2 -2
- data/spec/controllers/scimitar/schemas_controller_spec.rb +2 -10
- data/spec/models/scimitar/complex_types/address_spec.rb +4 -3
- data/spec/models/scimitar/complex_types/email_spec.rb +2 -0
- data/spec/models/scimitar/lists/query_parser_spec.rb +9 -76
- data/spec/models/scimitar/resources/base_spec.rb +70 -216
- data/spec/models/scimitar/resources/base_validation_spec.rb +2 -27
- data/spec/models/scimitar/resources/mixin_spec.rb +129 -1447
- data/spec/models/scimitar/schema/attribute_spec.rb +3 -22
- data/spec/models/scimitar/schema/base_spec.rb +1 -1
- data/spec/models/scimitar/schema/user_spec.rb +0 -10
- data/spec/requests/active_record_backed_resources_controller_spec.rb +68 -787
- data/spec/requests/application_controller_spec.rb +3 -16
- data/spec/spec_helper.rb +0 -8
- data/spec/support/hash_with_indifferent_case_insensitive_access_spec.rb +0 -108
- metadata +14 -25
- data/LICENSE.txt +0 -21
- data/README.md +0 -710
- data/lib/scimitar/support/utilities.rb +0 -51
- data/spec/apps/dummy/app/controllers/custom_create_mock_users_controller.rb +0 -25
- data/spec/apps/dummy/app/controllers/custom_replace_mock_users_controller.rb +0 -25
- data/spec/apps/dummy/app/controllers/custom_save_mock_users_controller.rb +0 -24
- 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
|
224
|
-
#
|
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.
|
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,
|
351
|
-
resource =
|
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
|
-
#
|
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
|
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:
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
766
|
-
|
767
|
-
|
768
|
-
|
769
|
-
|
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
|
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
|
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
|
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
|
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
|
-
|
1015
|
-
|
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
|
-
|
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
|
-
|
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"
|