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