scimitar 2.7.3 → 2.9.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/README.md +27 -18
- data/app/controllers/scimitar/active_record_backed_resources_controller.rb +5 -4
- data/app/controllers/scimitar/resource_types_controller.rb +0 -2
- data/app/controllers/scimitar/resources_controller.rb +0 -2
- data/app/controllers/scimitar/schemas_controller.rb +361 -3
- data/app/controllers/scimitar/service_provider_configurations_controller.rb +0 -1
- data/app/models/scimitar/engine_configuration.rb +3 -1
- data/app/models/scimitar/lists/query_parser.rb +88 -3
- data/app/models/scimitar/resources/base.rb +36 -5
- data/app/models/scimitar/resources/mixin.rb +87 -20
- data/app/models/scimitar/schema/name.rb +2 -2
- data/app/models/scimitar/schema/user.rb +10 -10
- data/config/initializers/scimitar.rb +41 -0
- data/lib/scimitar/engine.rb +57 -12
- data/lib/scimitar/support/utilities.rb +8 -3
- data/lib/scimitar/version.rb +2 -2
- data/spec/apps/dummy/app/models/mock_user.rb +18 -3
- data/spec/apps/dummy/config/initializers/scimitar.rb +31 -2
- data/spec/apps/dummy/db/migrate/20210304014602_create_mock_users.rb +1 -0
- data/spec/apps/dummy/db/schema.rb +1 -0
- data/spec/controllers/scimitar/schemas_controller_spec.rb +342 -54
- data/spec/models/scimitar/lists/query_parser_spec.rb +70 -0
- data/spec/models/scimitar/resources/base_spec.rb +11 -11
- data/spec/models/scimitar/resources/base_validation_spec.rb +16 -3
- data/spec/models/scimitar/resources/mixin_spec.rb +71 -10
- data/spec/models/scimitar/schema/user_spec.rb +2 -2
- data/spec/requests/active_record_backed_resources_controller_spec.rb +132 -1
- data/spec/requests/engine_spec.rb +75 -0
- data/spec/spec_helper.rb +1 -1
- metadata +20 -20
@@ -59,13 +59,18 @@ module Scimitar
|
|
59
59
|
#
|
60
60
|
BINARY_OPERATORS = Set.new(OPERATORS.keys.reject { |op| UNARY_OPERATORS.include?(op) }).freeze
|
61
61
|
|
62
|
+
# Precompiled expression that matches a valid attribute name according to
|
63
|
+
# https://tools.ietf.org/html/rfc7643#section-2.1.
|
64
|
+
#
|
65
|
+
ATTRNAME = /[[:alpha:]][[:alnum:]$-_]*/
|
66
|
+
|
62
67
|
# =======================================================================
|
63
68
|
# Tokenizing expressions
|
64
69
|
# =======================================================================
|
65
70
|
|
66
71
|
PAREN = /[\(\)]/.freeze
|
67
72
|
STR = /"(?:\\"|[^"])*"/.freeze
|
68
|
-
OP =
|
73
|
+
OP = /(?:#{OPERATORS.keys.join('|')})\b/i.freeze
|
69
74
|
WORD = /[\w\.]+/.freeze
|
70
75
|
SEP = /\s?/.freeze
|
71
76
|
NEXT_TOKEN = /\A(#{PAREN}|#{STR}|#{OP}|#{WORD})#{SEP}/.freeze
|
@@ -291,6 +296,10 @@ module Scimitar
|
|
291
296
|
# the part before the "[" as a prefix - "emails[type" to "emails.type",
|
292
297
|
# with similar substitutions therein.
|
293
298
|
#
|
299
|
+
# Further, via https://github.com/RIPAGlobal/scimitar/issues/115 we see
|
300
|
+
# a requirement to support a broken form emitted by Microsoft; that is
|
301
|
+
# supported herein.
|
302
|
+
#
|
294
303
|
# This method tries to flatten things thus. It throws exceptions if any
|
295
304
|
# problems arise at all. Some limitations:
|
296
305
|
#
|
@@ -314,6 +323,9 @@ module Scimitar
|
|
314
323
|
# <- userType ne "Employee" and not (emails[value co "example.com" or (value co "example.org")]) and userName="foo"
|
315
324
|
# => userType ne "Employee" and not (emails.value co "example.com" or (emails.value co "example.org")) and userName="foo"
|
316
325
|
#
|
326
|
+
# <- emails[type eq "work"].value eq "foo@bar.com" (Microsoft workaround)
|
327
|
+
# => emails.type eq "work" and emails.value eq "foo@bar.com"
|
328
|
+
#
|
317
329
|
# +filter+:: Input filter string. Returns a possibly modified String,
|
318
330
|
# with the hopefully equivalent but flattened filter inside.
|
319
331
|
#
|
@@ -363,9 +375,53 @@ module Scimitar
|
|
363
375
|
end
|
364
376
|
|
365
377
|
elsif (expecting_value)
|
366
|
-
matches = downcased.match(/([^\\])\]/) # Contains no-backslash-then-literal (unescaped) ']'
|
378
|
+
matches = downcased.match(/([^\\])\](.*)/) # Contains no-backslash-then-literal (unescaped) ']'; also capture anything after
|
367
379
|
unless matches.nil? # Contains no-backslash-then-literal (unescaped) ']'
|
368
380
|
character_before_closing_bracket = matches[1]
|
381
|
+
characters_after_closing_bracket = matches[2]
|
382
|
+
|
383
|
+
# https://github.com/RIPAGlobal/scimitar/issues/115 - detect
|
384
|
+
# bad Microsoft filters. After the closing bracket, we expect a
|
385
|
+
# dot then valid attribute characters and at least one white
|
386
|
+
# space character and filter operator, but we split on spaces,
|
387
|
+
# so the next item in the components array must be a recognised
|
388
|
+
# operator for the special case code to kick in.
|
389
|
+
#
|
390
|
+
# If this all works out, we transform this section of the
|
391
|
+
# filter string into a local dotted form, reconstruct the
|
392
|
+
# overall filter with this substitution, and call back to this
|
393
|
+
# method with that modified filter, returning the result.
|
394
|
+
#
|
395
|
+
# So - NOTE RECURSION AND EARLY EXIT POSSIBILITY HEREIN.
|
396
|
+
#
|
397
|
+
if (
|
398
|
+
! attribute_prefix.nil? &&
|
399
|
+
OPERATORS.key?(components[index + 1]&.downcase) &&
|
400
|
+
characters_after_closing_bracket.match?(/^\.#{ATTRNAME}$/)
|
401
|
+
)
|
402
|
+
# E.g. '"work"' and '.value' from input '"work"].value'
|
403
|
+
#
|
404
|
+
component_matches = component.match(/^(.*?[^\\])\](.*)/)
|
405
|
+
part_before_closing_bracket = component_matches[1]
|
406
|
+
part_after_closing_bracket = component_matches[2]
|
407
|
+
|
408
|
+
# Produces e.g. '"work"] and emails.value'
|
409
|
+
#
|
410
|
+
dotted_version = "#{part_before_closing_bracket}] and #{attribute_prefix}#{part_after_closing_bracket}"
|
411
|
+
|
412
|
+
# Overwrite the components array entry with this new version.
|
413
|
+
#
|
414
|
+
components[index] = dotted_version
|
415
|
+
|
416
|
+
# Join it back again as a reconstructed valid filter string.
|
417
|
+
#
|
418
|
+
hopefully_valid_filter = components.join(' ')
|
419
|
+
|
420
|
+
# NOTE EARLY EXIT
|
421
|
+
#
|
422
|
+
return flatten_filter(hopefully_valid_filter)
|
423
|
+
end
|
424
|
+
|
369
425
|
component.gsub!(/[^\\]\]/, character_before_closing_bracket)
|
370
426
|
|
371
427
|
if expecting_closing_bracket
|
@@ -410,7 +466,36 @@ module Scimitar
|
|
410
466
|
end
|
411
467
|
end
|
412
468
|
|
413
|
-
|
469
|
+
# Handle schema IDs.
|
470
|
+
#
|
471
|
+
# Scimitar currently has a limitation where it strips schema IDs in
|
472
|
+
# things like PATCH operation path traversal; see
|
473
|
+
# https://github.com/RIPAGlobal/scimitar/issues/130. At least that
|
474
|
+
# makes things easy here; use the same approach and strip them out!
|
475
|
+
#
|
476
|
+
# We don't know which resource is being queried at this layer of the
|
477
|
+
# software. If Scimitar were to bump major version, then an extension
|
478
|
+
# to QueryParser#parse to include this information would be wise. In
|
479
|
+
# the mean time, all we can do is enumerate all extension schema
|
480
|
+
# subclasses with IDs and remove those IDs if present in the filter.
|
481
|
+
#
|
482
|
+
# Inbound unrecognised schema IDs will be left behind. If the client
|
483
|
+
# Scimitar application hasn't defined requested schemas, it would
|
484
|
+
# very likely never have been able to handle the filter either way.
|
485
|
+
#
|
486
|
+
rewritten_joined = rewritten.join(' ')
|
487
|
+
if rewritten_joined.include?(':')
|
488
|
+
|
489
|
+
# README.md notes that extensions *must* be a subclass of
|
490
|
+
# Scimitar::Schema::Base and must define IDs.
|
491
|
+
#
|
492
|
+
known_schema_ids = Scimitar::Schema::Base.subclasses.map { |s| s.new.id }.compact
|
493
|
+
known_schema_ids.each do | schema_id |
|
494
|
+
rewritten_joined.gsub!(/#{schema_id}[\:\.]/, '')
|
495
|
+
end
|
496
|
+
end
|
497
|
+
|
498
|
+
return rewritten_joined
|
414
499
|
end
|
415
500
|
|
416
501
|
# Service method to DRY up #flatten_filter a little. Applies a prefix
|
@@ -35,14 +35,45 @@ module Scimitar
|
|
35
35
|
@errors = ActiveModel::Errors.new(self)
|
36
36
|
end
|
37
37
|
|
38
|
+
# Scimitar has at present a general limitation in handling schema IDs,
|
39
|
+
# which just involves stripping them and requiring attributes across all
|
40
|
+
# extension schemas to be overall unique.
|
41
|
+
#
|
42
|
+
# This method takes an options payload for the initializer and strips out
|
43
|
+
# *recognised* schema IDs, so that the resulting attribute data matches
|
44
|
+
# the resource attribute map.
|
45
|
+
#
|
46
|
+
# +attributes+:: Attributes to assign via initializer; typically a POST
|
47
|
+
# payload of attributes that has been run through Rails
|
48
|
+
# strong parameters for safety.
|
49
|
+
#
|
50
|
+
# Returns a new object of the same class as +options+ with recognised
|
51
|
+
# schema IDs removed.
|
52
|
+
#
|
38
53
|
def flatten_extension_attributes(options)
|
39
|
-
flattened
|
40
|
-
self.class.extended_schemas.
|
41
|
-
|
42
|
-
|
54
|
+
flattened = options.class.new
|
55
|
+
lower_case_schema_ids = self.class.extended_schemas.map do | schema |
|
56
|
+
schema.id.downcase()
|
57
|
+
end
|
58
|
+
|
59
|
+
options.each do | key, value |
|
60
|
+
path = Scimitar::Support::Utilities::path_str_to_array(
|
61
|
+
self.class.extended_schemas,
|
62
|
+
key
|
63
|
+
)
|
64
|
+
|
65
|
+
if path.first.include?(':') && lower_case_schema_ids.include?(path.first.downcase)
|
66
|
+
path.shift()
|
67
|
+
end
|
68
|
+
|
69
|
+
if path.empty?
|
70
|
+
flattened.merge!(value)
|
71
|
+
else
|
72
|
+
flattened[path.join('.')] = value
|
43
73
|
end
|
44
74
|
end
|
45
|
-
|
75
|
+
|
76
|
+
return flattened
|
46
77
|
end
|
47
78
|
|
48
79
|
# Can be used to extend an existing resource type's schema. For example:
|
@@ -139,27 +139,31 @@ module Scimitar
|
|
139
139
|
# # ...
|
140
140
|
# groups: [
|
141
141
|
# {
|
142
|
-
# list:
|
142
|
+
# list: :users, # <-- i.e. Team.users,
|
143
143
|
# using: {
|
144
144
|
# value: :id, # <-- i.e. Team.users[n].id
|
145
145
|
# display: :full_name # <-- i.e. Team.users[n].full_name
|
146
146
|
# },
|
147
|
+
# class: Team, # Optional; see below
|
147
148
|
# find_with: -> (scim_list_entry) {...} # See below
|
148
149
|
# }
|
149
150
|
# ],
|
150
151
|
# #...
|
151
152
|
# end
|
152
153
|
#
|
153
|
-
# The mixing-in class
|
154
|
+
# The mixing-in class _must_ implement the read accessor identified by the
|
154
155
|
# value of the "list" key, returning any indexed, Enumerable collection
|
155
156
|
# (e.g. an Array or ActiveRecord::Relation instance). The optional key
|
156
|
-
# ":find_with" is defined with a Proc that
|
157
|
+
# ":find_with" is defined with a Proc that is passed the SCIM entry at each
|
157
158
|
# list position. It must use this to look up the equivalent entry for
|
158
159
|
# association via the write accessor described by the ":list" key. In the
|
159
160
|
# example above, "find_with"'s Proc might look at a SCIM entry value which
|
160
161
|
# is expected to be a user ID and find that User. The mapped set of User
|
161
162
|
# data thus found would be written back with "#users=", due to the ":list"
|
162
|
-
# key declaring the method name ":users".
|
163
|
+
# key declaring the method name ":users". The optional "class" key is
|
164
|
+
# recommended but not really *needed* unless the configuration option
|
165
|
+
# Scimitar::EngineConfiguration::schema_list_from_attribute_mappings is
|
166
|
+
# defined; see documentation of that option for more information.
|
163
167
|
#
|
164
168
|
# Note that you can only use either:
|
165
169
|
#
|
@@ -176,7 +180,8 @@ module Scimitar
|
|
176
180
|
# == scim_mutable_attributes
|
177
181
|
#
|
178
182
|
# Define this method to return a Set (preferred) or Array of names of
|
179
|
-
# attributes which may be written in the mixing-in class.
|
183
|
+
# attributes which may be written in the mixing-in class. The names MUST be
|
184
|
+
# expressed as Symbols, *not* Strings.
|
180
185
|
#
|
181
186
|
# If you return +nil+, it is assumed that +any+ attribute mapped by
|
182
187
|
# ::scim_attributes_map which has a write accessor will be eligible for
|
@@ -204,12 +209,12 @@ module Scimitar
|
|
204
209
|
# Define this method to return a Hash that maps field names you wish to
|
205
210
|
# support in SCIM filter queries to corresponding attributes in the in the
|
206
211
|
# mixing-in class. If +nil+ then filtering is not supported in the
|
207
|
-
#
|
212
|
+
# ResourceController subclass which declares that it maps to the mixing-in
|
208
213
|
# class. If not +nil+ but a SCIM filter enquiry is made for an unmapped
|
209
214
|
# attribute, an 'invalid filter' exception is raised.
|
210
215
|
#
|
211
216
|
# If using ActiveRecord support in Scimitar::Lists::QueryParser, the mapped
|
212
|
-
#
|
217
|
+
# entities are columns and that's expressed in the names of keys described
|
213
218
|
# below; if you have other approaches to searching, these might be virtual
|
214
219
|
# attributes or other such constructs rather than columns. That would be up
|
215
220
|
# to your non-ActiveRecord's implementation to decide.
|
@@ -262,8 +267,8 @@ module Scimitar
|
|
262
267
|
# both of the keys 'created' and 'lastModified', as Symbols. The values
|
263
268
|
# should be methods that the including method supports which return a
|
264
269
|
# creation or most-recently-updated time, respectively. The returned object
|
265
|
-
#
|
266
|
-
# typical ActiveRecord object with standard timestamps:
|
270
|
+
# must support #iso8601 to convert to a String representation. Example for
|
271
|
+
# a typical ActiveRecord object with standard timestamps:
|
267
272
|
#
|
268
273
|
# def self.scim_timestamps_map
|
269
274
|
# {
|
@@ -291,7 +296,7 @@ module Scimitar
|
|
291
296
|
# the result in an instance variable.
|
292
297
|
#
|
293
298
|
def scim_mutable_attributes
|
294
|
-
@scim_mutable_attributes ||= self.class.scim_mutable_attributes()
|
299
|
+
@scim_mutable_attributes ||= self.class.scim_mutable_attributes()&.map(&:to_sym)
|
295
300
|
|
296
301
|
if @scim_mutable_attributes.nil?
|
297
302
|
@scim_mutable_attributes = Set.new
|
@@ -338,16 +343,32 @@ module Scimitar
|
|
338
343
|
# Render self as a SCIM object using ::scim_attributes_map. Fields that
|
339
344
|
# are marked as <tt>returned: 'never'</tt> are excluded.
|
340
345
|
#
|
341
|
-
# +location+::
|
342
|
-
#
|
343
|
-
#
|
344
|
-
#
|
346
|
+
# +location+:: The location (HTTP(S) full URI) of this
|
347
|
+
# resource in the domain of the object including
|
348
|
+
# this mixin - "your" IDs, not the remote SCIM
|
349
|
+
# client's external IDs. #url_for is a good way
|
350
|
+
# to generate this.
|
345
351
|
#
|
346
|
-
|
352
|
+
# +include_attributes+:: The attributes that should be included in the
|
353
|
+
# response, in the form of a list of full
|
354
|
+
# attribute paths. Schema IDs are not supported.
|
355
|
+
# See RFC 7644 section 3.9 and section 3.10 for
|
356
|
+
# more. When a collection is given, +nil+ value
|
357
|
+
# items are also excluded from the response. If
|
358
|
+
# omitted or given an empty collection, all
|
359
|
+
# attributes are included.
|
360
|
+
#
|
361
|
+
def to_scim(location:, include_attributes: [])
|
347
362
|
map = self.class.scim_attributes_map()
|
348
363
|
resource_type = self.class.scim_resource_type()
|
349
364
|
timestamps_map = self.class.scim_timestamps_map() if self.class.respond_to?(:scim_timestamps_map)
|
350
|
-
attrs_hash = self.to_scim_backend(
|
365
|
+
attrs_hash = self.to_scim_backend(
|
366
|
+
data_source: self,
|
367
|
+
resource_type: resource_type,
|
368
|
+
attrs_map_or_leaf_value: map,
|
369
|
+
include_attributes: include_attributes
|
370
|
+
)
|
371
|
+
|
351
372
|
resource = resource_type.new(attrs_hash)
|
352
373
|
meta_attrs_hash = { location: location }
|
353
374
|
|
@@ -532,6 +553,16 @@ module Scimitar
|
|
532
553
|
# +attrs_map_or_leaf_value+:: The attribute map. At the top level,
|
533
554
|
# this is from ::scim_attributes_map.
|
534
555
|
#
|
556
|
+
# +include_attributes+:: The attributes that should be included
|
557
|
+
# in the response, in the form of a list
|
558
|
+
# of full attribute paths. Schema IDs are
|
559
|
+
# not supported. See RFC 7644 section
|
560
|
+
# 3.9 and section 3.10 for more. When a
|
561
|
+
# collection is given, +nil+ value items
|
562
|
+
# are also excluded from the response. If
|
563
|
+
# omitted or given an empty collection,
|
564
|
+
# all attributes are included.
|
565
|
+
#
|
535
566
|
# Internal recursive calls also send:
|
536
567
|
#
|
537
568
|
# +attribute_path+:: Array of path components to the
|
@@ -543,8 +574,15 @@ module Scimitar
|
|
543
574
|
data_source:,
|
544
575
|
resource_type:,
|
545
576
|
attrs_map_or_leaf_value:,
|
577
|
+
include_attributes:,
|
546
578
|
attribute_path: []
|
547
579
|
)
|
580
|
+
# NOTE EARLY EXIT
|
581
|
+
#
|
582
|
+
return unless scim_attribute_included?(
|
583
|
+
include_attributes: include_attributes,
|
584
|
+
attribute_path: attribute_path
|
585
|
+
)
|
548
586
|
|
549
587
|
# On assumption of a top-level attributes list, the 'return never'
|
550
588
|
# state is only checked on the recursive call from a Hash type. The
|
@@ -554,7 +592,7 @@ module Scimitar
|
|
554
592
|
#
|
555
593
|
case attrs_map_or_leaf_value
|
556
594
|
when Hash # Expected at top-level of any map, or nested within
|
557
|
-
attrs_map_or_leaf_value.each.with_object({}) do |(key, value), hash|
|
595
|
+
result = attrs_map_or_leaf_value.each.with_object({}) do |(key, value), hash|
|
558
596
|
nested_attribute_path = attribute_path + [key]
|
559
597
|
|
560
598
|
if resource_type.find_attribute(*nested_attribute_path)&.returned != "never"
|
@@ -562,11 +600,15 @@ module Scimitar
|
|
562
600
|
data_source: data_source,
|
563
601
|
resource_type: resource_type,
|
564
602
|
attribute_path: nested_attribute_path,
|
565
|
-
attrs_map_or_leaf_value: value
|
603
|
+
attrs_map_or_leaf_value: value,
|
604
|
+
include_attributes: include_attributes
|
566
605
|
)
|
567
606
|
end
|
568
607
|
end
|
569
608
|
|
609
|
+
result.compact! if include_attributes.any?
|
610
|
+
result
|
611
|
+
|
570
612
|
when Array # Static or dynamic mapping against lists in data source
|
571
613
|
built_dynamic_list = false
|
572
614
|
mapped_array = attrs_map_or_leaf_value.map do |value|
|
@@ -580,7 +622,8 @@ module Scimitar
|
|
580
622
|
data_source: data_source,
|
581
623
|
resource_type: resource_type,
|
582
624
|
attribute_path: attribute_path,
|
583
|
-
attrs_map_or_leaf_value: value[:using]
|
625
|
+
attrs_map_or_leaf_value: value[:using],
|
626
|
+
include_attributes: include_attributes
|
584
627
|
)
|
585
628
|
)
|
586
629
|
static_hash
|
@@ -593,7 +636,8 @@ module Scimitar
|
|
593
636
|
data_source: list_entry,
|
594
637
|
resource_type: resource_type,
|
595
638
|
attribute_path: attribute_path,
|
596
|
-
attrs_map_or_leaf_value: value[:using]
|
639
|
+
attrs_map_or_leaf_value: value[:using],
|
640
|
+
include_attributes: include_attributes
|
597
641
|
)
|
598
642
|
end
|
599
643
|
|
@@ -1501,6 +1545,29 @@ module Scimitar
|
|
1501
1545
|
return handled
|
1502
1546
|
end
|
1503
1547
|
|
1548
|
+
# Related to to_scim_backend, this methods tells whether +attribute_path+
|
1549
|
+
# should be included in the current +include_attributes+. This method
|
1550
|
+
# implements the attributes request from RFC 7644, section 3.9 and 3.10.
|
1551
|
+
#
|
1552
|
+
# +include_attributes+:: The attributes that should be included
|
1553
|
+
# in the response, in the form of a list of
|
1554
|
+
# full attribute paths. See RFC 7644 section
|
1555
|
+
# 3.9 and section 3.10. An empty collection
|
1556
|
+
# will include all attributes.
|
1557
|
+
#
|
1558
|
+
# +attribute_path+:: Array of path components to the attribute,
|
1559
|
+
# e.g. <tt>["name", "givenName"]</tt>.
|
1560
|
+
#
|
1561
|
+
def scim_attribute_included?(include_attributes:, attribute_path:)
|
1562
|
+
return true unless attribute_path.any? && include_attributes.any?
|
1563
|
+
|
1564
|
+
full_path = attribute_path.join(".")
|
1565
|
+
attribute_included = full_path.start_with?(*include_attributes)
|
1566
|
+
will_include_nested = include_attributes.any? { |att| att.start_with?(full_path) }
|
1567
|
+
|
1568
|
+
attribute_included || will_include_nested
|
1569
|
+
end
|
1570
|
+
|
1504
1571
|
end # "included do"
|
1505
1572
|
end # "module Mixin"
|
1506
1573
|
end # "module Resources"
|
@@ -6,8 +6,8 @@ module Scimitar
|
|
6
6
|
|
7
7
|
def self.scim_attributes
|
8
8
|
@scim_attributes ||= [
|
9
|
-
Attribute.new(name: 'familyName', type: 'string'
|
10
|
-
Attribute.new(name: 'givenName', type: 'string'
|
9
|
+
Attribute.new(name: 'familyName', type: 'string'),
|
10
|
+
Attribute.new(name: 'givenName', type: 'string'),
|
11
11
|
Attribute.new(name: 'middleName', type: 'string'),
|
12
12
|
Attribute.new(name: 'formatted', type: 'string'),
|
13
13
|
Attribute.new(name: 'honorificPrefix', type: 'string'),
|
@@ -20,7 +20,7 @@ module Scimitar
|
|
20
20
|
[
|
21
21
|
Attribute.new(name: 'userName', type: 'string', uniqueness: 'server', required: true),
|
22
22
|
|
23
|
-
Attribute.new(name: 'name',
|
23
|
+
Attribute.new(name: 'name', complexType: Scimitar::ComplexTypes::Name),
|
24
24
|
|
25
25
|
Attribute.new(name: 'displayName', type: 'string'),
|
26
26
|
Attribute.new(name: 'nickName', type: 'string'),
|
@@ -35,15 +35,15 @@ module Scimitar
|
|
35
35
|
|
36
36
|
Attribute.new(name: 'password', type: 'string', mutability: 'writeOnly', returned: 'never'),
|
37
37
|
|
38
|
-
Attribute.new(name: 'emails',
|
39
|
-
Attribute.new(name: 'phoneNumbers',
|
40
|
-
Attribute.new(name: 'ims',
|
41
|
-
Attribute.new(name: 'photos',
|
42
|
-
Attribute.new(name: 'addresses',
|
43
|
-
Attribute.new(name: 'groups',
|
44
|
-
Attribute.new(name: 'entitlements',
|
45
|
-
Attribute.new(name: 'roles',
|
46
|
-
Attribute.new(name: 'x509Certificates',
|
38
|
+
Attribute.new(name: 'emails', multiValued: true, complexType: Scimitar::ComplexTypes::Email),
|
39
|
+
Attribute.new(name: 'phoneNumbers', multiValued: true, complexType: Scimitar::ComplexTypes::PhoneNumber),
|
40
|
+
Attribute.new(name: 'ims', multiValued: true, complexType: Scimitar::ComplexTypes::Ims),
|
41
|
+
Attribute.new(name: 'photos', multiValued: true, complexType: Scimitar::ComplexTypes::Photo),
|
42
|
+
Attribute.new(name: 'addresses', multiValued: true, complexType: Scimitar::ComplexTypes::Address),
|
43
|
+
Attribute.new(name: 'groups', multiValued: true, complexType: Scimitar::ComplexTypes::ReferenceGroup, mutability: 'readOnly'),
|
44
|
+
Attribute.new(name: 'entitlements', multiValued: true, complexType: Scimitar::ComplexTypes::Entitlement),
|
45
|
+
Attribute.new(name: 'roles', multiValued: true, complexType: Scimitar::ComplexTypes::Role),
|
46
|
+
Attribute.new(name: 'x509Certificates', multiValued: true, complexType: Scimitar::ComplexTypes::X509Certificate),
|
47
47
|
]
|
48
48
|
end
|
49
49
|
|
@@ -106,6 +106,47 @@ Rails.application.config.to_prepare do # (required for >= Rails 7 / Zeitwerk)
|
|
106
106
|
# whatever that means for you receiving system in your model code.
|
107
107
|
#
|
108
108
|
# optional_value_fields_required: false
|
109
|
+
|
110
|
+
# The SCIM standard `/Schemas` endpoint lists, by default, all known schema
|
111
|
+
# definitions with the mutabilty (read-write, read-only, write-only) state
|
112
|
+
# described by those definitions, and includes all defined attributes. For
|
113
|
+
# user-defined schema, this will typically exactly match your underlying
|
114
|
+
# mapped attribute and model capability - it wouldn't make sense to define
|
115
|
+
# your own schema that misrepresented the implementation! For core SCIM RFC
|
116
|
+
# schema, though, you might want to only list actually mapped attributes.
|
117
|
+
# Further, if you happen to have a non-compliant implementation especially
|
118
|
+
# in relation to mutability of some attributes, you may want to report that
|
119
|
+
# accurately in the '/Schemas' list, for auto-discovery purposes. To switch
|
120
|
+
# to a significantly slower but more accurate render method for the list,
|
121
|
+
# driven by your resource subclasses and their attribute maps, set:
|
122
|
+
#
|
123
|
+
# schema_list_from_attribute_mappings: [...array...]
|
124
|
+
#
|
125
|
+
# ...where you provide an Array of *models*, your classes that include the
|
126
|
+
# Scimitar::Resources::Mixin module and, therefore, define an attribute map
|
127
|
+
# translating SCIM schema attributes into actual implemented data. These
|
128
|
+
# must *uniquely* describe, via the Scimitar resources they each declare in
|
129
|
+
# their Scimitar::Resources::Mixin::scim_resource_type implementation, the
|
130
|
+
# set of schemas and extended schemas you want to render. Should resources
|
131
|
+
# share schema, the '/Schemas' endpoint will fail since it cannot determine
|
132
|
+
# which model attribute map it should use and it needs the map in order to
|
133
|
+
# resolve the differences (if any) between what the schema might say, and
|
134
|
+
# what the actual underlying model supports.
|
135
|
+
#
|
136
|
+
# It is further _very_ _strongly_ _recommended_ that, for any
|
137
|
+
# +scim_attributes_map+ containing a collection which has "list:" key (for
|
138
|
+
# an associative array of zero or more entities; the Groups to which a User
|
139
|
+
# might belong is a good example) then you should also specify the "class:"
|
140
|
+
# key, giving the class used for objects in that associated collection. The
|
141
|
+
# class *must* include Scimitar::Resources::Mixin, since its own attribute
|
142
|
+
# map is consulted in order to render the part of the schema describing
|
143
|
+
# those associated properties in the owning resource. If you don't do this,
|
144
|
+
# and if you're using ActiveRecord, then Scimitar attempts association
|
145
|
+
# reflection to determine the collection class - but that's more fragile
|
146
|
+
# than just being told the exact class in the attribute map. No matter how
|
147
|
+
# this class is determined, though, it must be possible to create a simple
|
148
|
+
# instance with +new+ and no parameters, since that's needed in order to
|
149
|
+
# call Scimitar::Resources::Mixin#scim_mutable_attributes.
|
109
150
|
})
|
110
151
|
|
111
152
|
end
|
data/lib/scimitar/engine.rb
CHANGED
@@ -1,15 +1,38 @@
|
|
1
|
+
require 'rails/engine'
|
2
|
+
|
1
3
|
module Scimitar
|
2
4
|
class Engine < ::Rails::Engine
|
3
5
|
isolate_namespace Scimitar
|
4
6
|
|
7
|
+
config.autoload_once_paths = %W(
|
8
|
+
#{root}/app/controllers
|
9
|
+
#{root}/app/models
|
10
|
+
)
|
11
|
+
|
5
12
|
Mime::Type.register 'application/scim+json', :scim
|
6
13
|
|
7
14
|
ActionDispatch::Request.parameter_parsers[Mime::Type.lookup('application/scim+json').symbol] = lambda do |body|
|
8
15
|
JSON.parse(body)
|
9
16
|
end
|
10
17
|
|
18
|
+
# Return an Array of all supported default and custom resource classes.
|
19
|
+
# See also :add_custom_resource and :set_default_resources.
|
20
|
+
#
|
11
21
|
def self.resources
|
12
|
-
default_resources + custom_resources
|
22
|
+
self.default_resources() + self.custom_resources()
|
23
|
+
end
|
24
|
+
|
25
|
+
# Returns a flat array of instances of all resource schema included in the
|
26
|
+
# resource classes returned by ::resources.
|
27
|
+
#
|
28
|
+
def self.schemas
|
29
|
+
self.resources().map(&:schemas).flatten.uniq.map(&:new)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns the list of custom resources, if any.
|
33
|
+
#
|
34
|
+
def self.custom_resources
|
35
|
+
@custom_resources ||= []
|
13
36
|
end
|
14
37
|
|
15
38
|
# Can be used to add a new resource type which is not provided by the gem.
|
@@ -30,7 +53,7 @@ module Scimitar
|
|
30
53
|
# Scimitar::Engine.add_custom_resource Scim::Resources::ShinyResource
|
31
54
|
#
|
32
55
|
def self.add_custom_resource(resource)
|
33
|
-
custom_resources << resource
|
56
|
+
self.custom_resources() << resource
|
34
57
|
end
|
35
58
|
|
36
59
|
# Resets the resource list to default. This is really only intended for use
|
@@ -40,23 +63,45 @@ module Scimitar
|
|
40
63
|
@custom_resources = []
|
41
64
|
end
|
42
65
|
|
43
|
-
# Returns the
|
44
|
-
#
|
45
|
-
def self.custom_resources
|
46
|
-
@custom_resources ||= []
|
47
|
-
end
|
48
|
-
|
49
|
-
# Returns the default resources added in this gem:
|
66
|
+
# Returns the default resources added in this gem - by default, these are:
|
50
67
|
#
|
51
68
|
# * Scimitar::Resources::User
|
52
69
|
# * Scimitar::Resources::Group
|
53
70
|
#
|
71
|
+
# ...but if an implementation does not e.g. support Group, it can
|
72
|
+
# be overridden via ::set_default_resources to help with service
|
73
|
+
# auto-discovery.
|
74
|
+
#
|
54
75
|
def self.default_resources
|
55
|
-
[ Resources::User, Resources::Group ]
|
76
|
+
@standard_default_resources = [ Resources::User, Resources::Group ]
|
77
|
+
@default_resources ||= @standard_default_resources.dup()
|
56
78
|
end
|
57
79
|
|
58
|
-
|
59
|
-
|
80
|
+
# Override the resources returned by ::default_resources.
|
81
|
+
#
|
82
|
+
# +resource_array+:: An Array containing one or both of
|
83
|
+
# Scimitar::Resources::User and/or
|
84
|
+
# Scimitar::Resources::Group, and nothing else.
|
85
|
+
#
|
86
|
+
def self.set_default_resources(resource_array)
|
87
|
+
self.default_resources()
|
88
|
+
unrecognised_resources = resource_array - @standard_default_resources
|
89
|
+
|
90
|
+
if unrecognised_resources.any?
|
91
|
+
raise "Scimitar::Engine::set_default_resources: Only #{@standard_default_resources.map(&:name).join(', ')} are supported"
|
92
|
+
elsif resource_array.empty?
|
93
|
+
raise 'Scimitar::Engine::set_default_resources: At least one resource must be given'
|
94
|
+
end
|
95
|
+
|
96
|
+
@default_resources = resource_array
|
97
|
+
end
|
98
|
+
|
99
|
+
# Resets the default resource list. This is really only intended for use
|
100
|
+
# during testing, to avoid one test polluting another.
|
101
|
+
#
|
102
|
+
def self.reset_default_resources
|
103
|
+
self.default_resources()
|
104
|
+
@default_resources = @standard_default_resources
|
60
105
|
end
|
61
106
|
|
62
107
|
end
|
@@ -57,9 +57,10 @@ module Scimitar
|
|
57
57
|
# <tt>scim_resource_type.extended_schemas</tt> value. The
|
58
58
|
# Array should be empty if there are no extensions.
|
59
59
|
#
|
60
|
-
# +path_str+:: Path
|
60
|
+
# +path_str+:: Path String, e.g. <tt>"password"</tt>, <tt>"name.givenName"</tt>,
|
61
61
|
# <tt>"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"</tt> (special case),
|
62
62
|
# <tt>"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:organization"</tt>
|
63
|
+
# (if given a Symbol, it'll be converted to a String).
|
63
64
|
#
|
64
65
|
# Returns an array of components, e.g. <tt>["password"]</tt>, <tt>["name",
|
65
66
|
# "givenName"]</tt>,
|
@@ -74,6 +75,7 @@ module Scimitar
|
|
74
75
|
# path-free payload.
|
75
76
|
#
|
76
77
|
def self.path_str_to_array(schemas, path_str)
|
78
|
+
path_str = path_str.to_s
|
77
79
|
components = []
|
78
80
|
|
79
81
|
# Note the ":" separating the schema ID (URN) from the attribute.
|
@@ -84,11 +86,14 @@ module Scimitar
|
|
84
86
|
# particular, https://tools.ietf.org/html/rfc7644#page-35.
|
85
87
|
#
|
86
88
|
if path_str.include?(':')
|
89
|
+
lower_case_path_str = path_str.downcase()
|
90
|
+
|
87
91
|
schemas.each do |schema|
|
88
|
-
|
92
|
+
lower_case_schema_id = schema.id.downcase()
|
93
|
+
attributes_after_schema_id = lower_case_path_str.split(lower_case_schema_id + ':').drop(1)
|
89
94
|
|
90
95
|
if attributes_after_schema_id.empty?
|
91
|
-
components += [schema.id]
|
96
|
+
components += [schema.id] if lower_case_path_str == lower_case_schema_id
|
92
97
|
else
|
93
98
|
attributes_after_schema_id.each do |component|
|
94
99
|
components += [schema.id] + component.split('.')
|
data/lib/scimitar/version.rb
CHANGED
@@ -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.
|
6
|
+
VERSION = '2.9.0'
|
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-06-
|
11
|
+
DATE = '2024-06-27'
|
12
12
|
|
13
13
|
end
|