scimitar 2.7.3 → 2.8.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 +5 -1
- 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 +0 -2
- data/app/controllers/scimitar/service_provider_configurations_controller.rb +0 -1
- data/app/models/scimitar/lists/query_parser.rb +88 -3
- data/app/models/scimitar/resources/mixin.rb +78 -16
- data/app/models/scimitar/schema/name.rb +2 -2
- data/app/models/scimitar/schema/user.rb +10 -10
- data/lib/scimitar/engine.rb +7 -0
- data/lib/scimitar/version.rb +2 -2
- data/spec/apps/dummy/app/models/mock_user.rb +8 -1
- data/spec/apps/dummy/config/initializers/scimitar.rb +2 -1
- data/spec/models/scimitar/lists/query_parser_spec.rb +65 -0
- data/spec/models/scimitar/resources/base_validation_spec.rb +15 -2
- data/spec/models/scimitar/resources/mixin_spec.rb +43 -1
- data/spec/models/scimitar/schema/user_spec.rb +2 -2
- data/spec/requests/active_record_backed_resources_controller_spec.rb +47 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 207cb3e880d58e44ed4c995fd85e165a4a5890ff2974cef418ed605cf4ddba0b
|
4
|
+
data.tar.gz: b1aade94993912f93f783e64b1142447b6d2394f1e9fccd1fc8708116af8542b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9c9ecf7480dd0a18df82ad0c2b18b9122540d13c44a9bc7a5a2e8d1b5fe8091faa1d57c2fa7791c7509e4736ded919c5dde6c7ff5a52252ef05c5eac9a057c5d
|
7
|
+
data.tar.gz: 3153843636fca6bcf4d2b719eef31d32188b9f1c2b2a4f645f76302bd0be316454a941fb1d50f315cf5ec6118d5553e46eff83c92aecf168152d0d9620ad56d9
|
data/README.md
CHANGED
@@ -79,7 +79,7 @@ Scimitar.engine_configuration = Scimitar::EngineConfiguration.new({
|
|
79
79
|
|
80
80
|
When it comes to token access, Scimitar neither enforces nor presumes any kind of encoding for bearer tokens. You can use anything you like, including encoding/encrypting JWTs if you so wish - https://rubygems.org/gems/jwt may be useful. The way in which a client might integrate with your SCIM service varies by client and you will have to check documentation to see how a token gets conveyed to that client in the first place (e.g. a full OAuth flow with your application, or just a static token generated in some UI which an administrator copies and pastes into their client's SCIM configuration UI).
|
81
81
|
|
82
|
-
**
|
82
|
+
**Strongly recommended:** You should wrap any Scimitar configuration with `Rails.application.config.to_prepare do...` so that any changes you make to configuration during local development are reflected via auto-reload, rather than requiring a server restart.
|
83
83
|
|
84
84
|
```ruby
|
85
85
|
Rails.application.config.to_prepare do
|
@@ -89,6 +89,8 @@ Rails.application.config.to_prepare do
|
|
89
89
|
end
|
90
90
|
```
|
91
91
|
|
92
|
+
In general, Scimitar's own development and tests assume this approach. If you choose to put the configuration directly into an initializer file without the `to_prepare` wrapper, you will be at a _slightly_ higher risk of tripping over unrecognised Scimitar bugs; please make sure that your own application test coverage is reasonably comprehensive.
|
93
|
+
|
92
94
|
### Routes
|
93
95
|
|
94
96
|
For each resource you support, add these lines to your `routes.rb`:
|
@@ -540,6 +542,8 @@ Whatever you provide in the `::id` method in your extension class will be used a
|
|
540
542
|
}
|
541
543
|
```
|
542
544
|
|
545
|
+
**IMPORTANT: Attribute names must be unique** across your entire combined schema, regardless of URNs used. This is because of a limitation in Scimitar's implementation. [This GitHub issue](https://github.com/RIPAGlobal/scimitar/issues/130) explains more. If this is a problem for you, please comment on the GitHub issue to help the maintainers understand the level of demand for remediation.
|
546
|
+
|
543
547
|
Resource extensions can provide any fields you choose, under any ID/URN you choose, to either RFC-described resources or entirely custom SCIM resources. There are no hard-coded assumptions or other "magic" that might require you to only extend RFC-described resources with RFC-described extensions. Of course, if you use custom resources or custom extensions that are not described by the SCIM RFCs, then the SCIM API you provide may only work with custom-written API callers that are aware of your bespoke resources and/or extensions.
|
544
548
|
|
545
549
|
Extensions can also contain complex attributes such as groups. For instance, if you want the ability to write to groups from the User resource perspective (since 'groups' collection in a SCIM User resource is read-only), you can add one attribute to your extension like this:
|
@@ -1,5 +1,3 @@
|
|
1
|
-
require_dependency "scimitar/application_controller"
|
2
|
-
|
3
1
|
module Scimitar
|
4
2
|
|
5
3
|
# An ActiveRecord-centric subclass of Scimitar::ResourcesController. See that
|
@@ -19,7 +17,7 @@ module Scimitar
|
|
19
17
|
#
|
20
18
|
class ActiveRecordBackedResourcesController < ResourcesController
|
21
19
|
|
22
|
-
rescue_from ActiveRecord::RecordNotFound, with: :handle_resource_not_found # See Scimitar::ApplicationController
|
20
|
+
rescue_from 'ActiveRecord::RecordNotFound', with: :handle_resource_not_found # See Scimitar::ApplicationController
|
23
21
|
|
24
22
|
before_action :obtain_id_column_name_from_attribute_map
|
25
23
|
|
@@ -174,7 +172,10 @@ module Scimitar
|
|
174
172
|
# representation, with a "show" location specified via #url_for.
|
175
173
|
#
|
176
174
|
def record_to_scim(record)
|
177
|
-
record.to_scim(
|
175
|
+
record.to_scim(
|
176
|
+
location: url_for(action: :show, id: record.send(@id_column)),
|
177
|
+
include_attributes: params.fetch(:attributes, "").split(",")
|
178
|
+
)
|
178
179
|
end
|
179
180
|
|
180
181
|
# Save a record, dealing with validation exceptions by raising SCIM
|
@@ -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
|
@@ -150,10 +150,10 @@ module Scimitar
|
|
150
150
|
# #...
|
151
151
|
# end
|
152
152
|
#
|
153
|
-
# The mixing-in class
|
153
|
+
# The mixing-in class _must_ implement the read accessor identified by the
|
154
154
|
# value of the "list" key, returning any indexed, Enumerable collection
|
155
155
|
# (e.g. an Array or ActiveRecord::Relation instance). The optional key
|
156
|
-
# ":find_with" is defined with a Proc that
|
156
|
+
# ":find_with" is defined with a Proc that is passed the SCIM entry at each
|
157
157
|
# list position. It must use this to look up the equivalent entry for
|
158
158
|
# association via the write accessor described by the ":list" key. In the
|
159
159
|
# example above, "find_with"'s Proc might look at a SCIM entry value which
|
@@ -204,12 +204,12 @@ module Scimitar
|
|
204
204
|
# Define this method to return a Hash that maps field names you wish to
|
205
205
|
# support in SCIM filter queries to corresponding attributes in the in the
|
206
206
|
# mixing-in class. If +nil+ then filtering is not supported in the
|
207
|
-
#
|
207
|
+
# ResourceController subclass which declares that it maps to the mixing-in
|
208
208
|
# class. If not +nil+ but a SCIM filter enquiry is made for an unmapped
|
209
209
|
# attribute, an 'invalid filter' exception is raised.
|
210
210
|
#
|
211
211
|
# If using ActiveRecord support in Scimitar::Lists::QueryParser, the mapped
|
212
|
-
#
|
212
|
+
# entities are columns and that's expressed in the names of keys described
|
213
213
|
# below; if you have other approaches to searching, these might be virtual
|
214
214
|
# attributes or other such constructs rather than columns. That would be up
|
215
215
|
# to your non-ActiveRecord's implementation to decide.
|
@@ -262,8 +262,8 @@ module Scimitar
|
|
262
262
|
# both of the keys 'created' and 'lastModified', as Symbols. The values
|
263
263
|
# should be methods that the including method supports which return a
|
264
264
|
# creation or most-recently-updated time, respectively. The returned object
|
265
|
-
#
|
266
|
-
# typical ActiveRecord object with standard timestamps:
|
265
|
+
# must support #iso8601 to convert to a String representation. Example for
|
266
|
+
# a typical ActiveRecord object with standard timestamps:
|
267
267
|
#
|
268
268
|
# def self.scim_timestamps_map
|
269
269
|
# {
|
@@ -338,16 +338,32 @@ module Scimitar
|
|
338
338
|
# Render self as a SCIM object using ::scim_attributes_map. Fields that
|
339
339
|
# are marked as <tt>returned: 'never'</tt> are excluded.
|
340
340
|
#
|
341
|
-
# +location+::
|
342
|
-
#
|
343
|
-
#
|
344
|
-
#
|
341
|
+
# +location+:: The location (HTTP(S) full URI) of this
|
342
|
+
# resource in the domain of the object including
|
343
|
+
# this mixin - "your" IDs, not the remote SCIM
|
344
|
+
# client's external IDs. #url_for is a good way
|
345
|
+
# to generate this.
|
345
346
|
#
|
346
|
-
|
347
|
+
# +include_attributes+:: The attributes that should be included in the
|
348
|
+
# response, in the form of a list of full
|
349
|
+
# attribute paths. Schema IDs are not supported.
|
350
|
+
# See RFC 7644 section 3.9 and section 3.10 for
|
351
|
+
# more. When a collection is given, +nil+ value
|
352
|
+
# items are also excluded from the response. If
|
353
|
+
# omitted or given an empty collection, all
|
354
|
+
# attributes are included.
|
355
|
+
#
|
356
|
+
def to_scim(location:, include_attributes: [])
|
347
357
|
map = self.class.scim_attributes_map()
|
348
358
|
resource_type = self.class.scim_resource_type()
|
349
359
|
timestamps_map = self.class.scim_timestamps_map() if self.class.respond_to?(:scim_timestamps_map)
|
350
|
-
attrs_hash = self.to_scim_backend(
|
360
|
+
attrs_hash = self.to_scim_backend(
|
361
|
+
data_source: self,
|
362
|
+
resource_type: resource_type,
|
363
|
+
attrs_map_or_leaf_value: map,
|
364
|
+
include_attributes: include_attributes
|
365
|
+
)
|
366
|
+
|
351
367
|
resource = resource_type.new(attrs_hash)
|
352
368
|
meta_attrs_hash = { location: location }
|
353
369
|
|
@@ -532,6 +548,16 @@ module Scimitar
|
|
532
548
|
# +attrs_map_or_leaf_value+:: The attribute map. At the top level,
|
533
549
|
# this is from ::scim_attributes_map.
|
534
550
|
#
|
551
|
+
# +include_attributes+:: The attributes that should be included
|
552
|
+
# in the response, in the form of a list
|
553
|
+
# of full attribute paths. Schema IDs are
|
554
|
+
# not supported. See RFC 7644 section
|
555
|
+
# 3.9 and section 3.10 for more. When a
|
556
|
+
# collection is given, +nil+ value items
|
557
|
+
# are also excluded from the response. If
|
558
|
+
# omitted or given an empty collection,
|
559
|
+
# all attributes are included.
|
560
|
+
#
|
535
561
|
# Internal recursive calls also send:
|
536
562
|
#
|
537
563
|
# +attribute_path+:: Array of path components to the
|
@@ -543,8 +569,15 @@ module Scimitar
|
|
543
569
|
data_source:,
|
544
570
|
resource_type:,
|
545
571
|
attrs_map_or_leaf_value:,
|
572
|
+
include_attributes:,
|
546
573
|
attribute_path: []
|
547
574
|
)
|
575
|
+
# NOTE EARLY EXIT
|
576
|
+
#
|
577
|
+
return unless scim_attribute_included?(
|
578
|
+
include_attributes: include_attributes,
|
579
|
+
attribute_path: attribute_path
|
580
|
+
)
|
548
581
|
|
549
582
|
# On assumption of a top-level attributes list, the 'return never'
|
550
583
|
# state is only checked on the recursive call from a Hash type. The
|
@@ -554,7 +587,7 @@ module Scimitar
|
|
554
587
|
#
|
555
588
|
case attrs_map_or_leaf_value
|
556
589
|
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|
|
590
|
+
result = attrs_map_or_leaf_value.each.with_object({}) do |(key, value), hash|
|
558
591
|
nested_attribute_path = attribute_path + [key]
|
559
592
|
|
560
593
|
if resource_type.find_attribute(*nested_attribute_path)&.returned != "never"
|
@@ -562,11 +595,15 @@ module Scimitar
|
|
562
595
|
data_source: data_source,
|
563
596
|
resource_type: resource_type,
|
564
597
|
attribute_path: nested_attribute_path,
|
565
|
-
attrs_map_or_leaf_value: value
|
598
|
+
attrs_map_or_leaf_value: value,
|
599
|
+
include_attributes: include_attributes
|
566
600
|
)
|
567
601
|
end
|
568
602
|
end
|
569
603
|
|
604
|
+
result.compact! if include_attributes.any?
|
605
|
+
result
|
606
|
+
|
570
607
|
when Array # Static or dynamic mapping against lists in data source
|
571
608
|
built_dynamic_list = false
|
572
609
|
mapped_array = attrs_map_or_leaf_value.map do |value|
|
@@ -580,7 +617,8 @@ module Scimitar
|
|
580
617
|
data_source: data_source,
|
581
618
|
resource_type: resource_type,
|
582
619
|
attribute_path: attribute_path,
|
583
|
-
attrs_map_or_leaf_value: value[:using]
|
620
|
+
attrs_map_or_leaf_value: value[:using],
|
621
|
+
include_attributes: include_attributes
|
584
622
|
)
|
585
623
|
)
|
586
624
|
static_hash
|
@@ -593,7 +631,8 @@ module Scimitar
|
|
593
631
|
data_source: list_entry,
|
594
632
|
resource_type: resource_type,
|
595
633
|
attribute_path: attribute_path,
|
596
|
-
attrs_map_or_leaf_value: value[:using]
|
634
|
+
attrs_map_or_leaf_value: value[:using],
|
635
|
+
include_attributes: include_attributes
|
597
636
|
)
|
598
637
|
end
|
599
638
|
|
@@ -1501,6 +1540,29 @@ module Scimitar
|
|
1501
1540
|
return handled
|
1502
1541
|
end
|
1503
1542
|
|
1543
|
+
# Related to to_scim_backend, this methods tells whether +attribute_path+
|
1544
|
+
# should be included in the current +include_attributes+. This method
|
1545
|
+
# implements the attributes request from RFC 7644, section 3.9 and 3.10.
|
1546
|
+
#
|
1547
|
+
# +include_attributes+:: The attributes that should be included
|
1548
|
+
# in the response, in the form of a list of
|
1549
|
+
# full attribute paths. See RFC 7644 section
|
1550
|
+
# 3.9 and section 3.10. An empty collection
|
1551
|
+
# will include all attributes.
|
1552
|
+
#
|
1553
|
+
# +attribute_path+:: Array of path components to the attribute,
|
1554
|
+
# e.g. <tt>["name", "givenName"]</tt>.
|
1555
|
+
#
|
1556
|
+
def scim_attribute_included?(include_attributes:, attribute_path:)
|
1557
|
+
return true unless attribute_path.any? && include_attributes.any?
|
1558
|
+
|
1559
|
+
full_path = attribute_path.join(".")
|
1560
|
+
attribute_included = full_path.start_with?(*include_attributes)
|
1561
|
+
will_include_nested = include_attributes.any? { |att| att.start_with?(full_path) }
|
1562
|
+
|
1563
|
+
attribute_included || will_include_nested
|
1564
|
+
end
|
1565
|
+
|
1504
1566
|
end # "included do"
|
1505
1567
|
end # "module Mixin"
|
1506
1568
|
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
|
|
data/lib/scimitar/engine.rb
CHANGED
@@ -1,7 +1,14 @@
|
|
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|
|
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.8.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-13'
|
12
12
|
|
13
13
|
end
|
@@ -96,6 +96,7 @@ class MockUser < ActiveRecord::Base
|
|
96
96
|
#
|
97
97
|
organization: :organization,
|
98
98
|
department: :department,
|
99
|
+
primaryEmail: :scim_primary_email,
|
99
100
|
userGroups: [
|
100
101
|
{
|
101
102
|
list: :mock_groups,
|
@@ -124,9 +125,15 @@ class MockUser < ActiveRecord::Base
|
|
124
125
|
'groups.value' => { column: MockGroup.arel_table[:id] },
|
125
126
|
'emails' => { columns: [ :work_email_address, :home_email_address ] },
|
126
127
|
'emails.value' => { columns: [ :work_email_address, :home_email_address ] },
|
127
|
-
'emails.type' => { ignore: true } # We can't filter on that; it'll just search all e-mails
|
128
|
+
'emails.type' => { ignore: true }, # We can't filter on that; it'll just search all e-mails
|
129
|
+
'primaryEmail' => { column: :scim_primary_email },
|
128
130
|
}
|
129
131
|
end
|
130
132
|
|
133
|
+
# reader
|
134
|
+
def scim_primary_email
|
135
|
+
work_email_address
|
136
|
+
end
|
137
|
+
|
131
138
|
include Scimitar::Resources::Mixin
|
132
139
|
end
|
@@ -50,7 +50,8 @@ Rails.application.config.to_prepare do
|
|
50
50
|
def self.scim_attributes
|
51
51
|
[
|
52
52
|
Scimitar::Schema::Attribute.new(name: 'organization', type: 'string'),
|
53
|
-
Scimitar::Schema::Attribute.new(name: 'department', type: 'string')
|
53
|
+
Scimitar::Schema::Attribute.new(name: 'department', type: 'string'),
|
54
|
+
Scimitar::Schema::Attribute.new(name: 'primaryEmail', type: 'string'),
|
54
55
|
]
|
55
56
|
end
|
56
57
|
end
|
@@ -60,6 +60,15 @@ RSpec.describe Scimitar::Lists::QueryParser do
|
|
60
60
|
expect(%Q("O'Malley")).to eql(tree[2])
|
61
61
|
end
|
62
62
|
|
63
|
+
it "extended attribute equals" do
|
64
|
+
@instance.parse(%Q(primaryEmail eq "foo@bar.com"))
|
65
|
+
|
66
|
+
rpn = @instance.rpn
|
67
|
+
expect('primaryEmail').to eql(rpn[0])
|
68
|
+
expect(%Q("foo@bar.com")).to eql(rpn[1])
|
69
|
+
expect('eq').to eql(rpn[2])
|
70
|
+
end
|
71
|
+
|
63
72
|
it "user name starts with" do
|
64
73
|
@instance.parse(%Q(userName sw "J"))
|
65
74
|
|
@@ -337,6 +346,62 @@ RSpec.describe Scimitar::Lists::QueryParser do
|
|
337
346
|
result = @instance.send(:flatten_filter, 'emails[type eq "work" and value co "@example.com" ] or userType eq "Admin" or ims[type eq "xmpp" and value co "@foo.com"]')
|
338
347
|
expect(result).to eql('emails.type eq "work" and emails.value co "@example.com" or userType eq "Admin" or ims.type eq "xmpp" and ims.value co "@foo.com"')
|
339
348
|
end
|
349
|
+
|
350
|
+
# https://github.com/RIPAGlobal/scimitar/issues/116
|
351
|
+
#
|
352
|
+
context 'with schema IDs (GitHub issue #116)' do
|
353
|
+
it 'handles simple attributes' do
|
354
|
+
result = @instance.send(:flatten_filter, 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:employeeId eq "gsar"')
|
355
|
+
expect(result).to eql('employeeId eq "gsar"')
|
356
|
+
end
|
357
|
+
|
358
|
+
it 'handles dotted attribute paths' do
|
359
|
+
result = @instance.send(:flatten_filter, 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:imaginary.path eq "gsar"')
|
360
|
+
expect(result).to eql('imaginary.path eq "gsar"')
|
361
|
+
end
|
362
|
+
|
363
|
+
it 'replaces all examples' do
|
364
|
+
result = @instance.send(:flatten_filter, 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:employeeId eq "gsar" or urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:imaginary.path eq "gsar"')
|
365
|
+
expect(result).to eql('employeeId eq "gsar" or imaginary.path eq "gsar"')
|
366
|
+
end
|
367
|
+
|
368
|
+
it 'handles the square bracket form with schema ID at the root' do
|
369
|
+
result = @instance.send(:flatten_filter, 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User[employeeId eq "gsar"')
|
370
|
+
expect(result).to eql('employeeId eq "gsar"')
|
371
|
+
end
|
372
|
+
|
373
|
+
it 'handles the square bracket form with schema ID and attribute at the root' do
|
374
|
+
result = @instance.send(:flatten_filter, 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:imaginary[path eq "gsar"')
|
375
|
+
expect(result).to eql('imaginary.path eq "gsar"')
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
# https://github.com/RIPAGlobal/scimitar/issues/115
|
380
|
+
#
|
381
|
+
context 'broken filters from Microsoft (GitHub issue #115)' do
|
382
|
+
it 'work with "eq"' do
|
383
|
+
result = @instance.send(:flatten_filter, 'emails[type eq "work"].value eq "foo@bar.com"')
|
384
|
+
expect(result).to eql('emails.type eq "work" and emails.value eq "foo@bar.com"')
|
385
|
+
end
|
386
|
+
|
387
|
+
it 'work with "ne"' do # (just check a couple of operators, not all!)
|
388
|
+
result = @instance.send(:flatten_filter, 'emails[type eq "work"].value ne "foo@bar.com"')
|
389
|
+
expect(result).to eql('emails.type eq "work" and emails.value ne "foo@bar.com"')
|
390
|
+
end
|
391
|
+
|
392
|
+
it 'preserve input case' do
|
393
|
+
result = @instance.send(:flatten_filter, 'emaiLs[TYPE eq "work"].valUE eq "FOO@bar.com"')
|
394
|
+
expect(result).to eql('emaiLs.TYPE eq "work" and emaiLs.valUE eq "FOO@bar.com"')
|
395
|
+
end
|
396
|
+
|
397
|
+
# At the time of writing, this was used in a "belt and braces" request
|
398
|
+
# spec in 'active_record_backed_resources_controller_spec.rb'.
|
399
|
+
#
|
400
|
+
it 'handles more complex, hypothetical cases' do
|
401
|
+
result = @instance.send(:flatten_filter, 'name[givenName eq "FOO"].familyName pr and emails ne "home_1@test.com"')
|
402
|
+
expect(result).to eql('name.givenName eq "FOO" and name.familyName pr and emails ne "home_1@test.com"')
|
403
|
+
end
|
404
|
+
end # "context 'broken filters from Microsoft' do"
|
340
405
|
end # "context 'when flattening is needed' do"
|
341
406
|
|
342
407
|
context 'with bad filters' do
|
@@ -7,6 +7,19 @@ RSpec.describe Scimitar::Resources::Base do
|
|
7
7
|
'custom-id'
|
8
8
|
end
|
9
9
|
|
10
|
+
class NameWithRequirementsSchema < Scimitar::Schema::Base
|
11
|
+
def self.scim_attributes
|
12
|
+
@scim_attributes ||= [
|
13
|
+
Scimitar::Schema::Attribute.new(name: 'familyName', type: 'string', required: true),
|
14
|
+
Scimitar::Schema::Attribute.new(name: 'givenName', type: 'string', required: true),
|
15
|
+
]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class NameWithRequirementsComplexType < Scimitar::ComplexTypes::Base
|
20
|
+
set_schema NameWithRequirementsSchema
|
21
|
+
end
|
22
|
+
|
10
23
|
def self.scim_attributes
|
11
24
|
[
|
12
25
|
Scimitar::Schema::Attribute.new(
|
@@ -16,10 +29,10 @@ RSpec.describe Scimitar::Resources::Base do
|
|
16
29
|
name: 'enforce', type: 'boolean', required: true
|
17
30
|
),
|
18
31
|
Scimitar::Schema::Attribute.new(
|
19
|
-
name: 'complexName', complexType:
|
32
|
+
name: 'complexName', complexType: NameWithRequirementsComplexType, required: false
|
20
33
|
),
|
21
34
|
Scimitar::Schema::Attribute.new(
|
22
|
-
name: 'complexNames', complexType: Scimitar::ComplexTypes::Name, multiValued:true, required: false
|
35
|
+
name: 'complexNames', complexType: Scimitar::ComplexTypes::Name, multiValued: true, required: false
|
23
36
|
),
|
24
37
|
Scimitar::Schema::Attribute.new(
|
25
38
|
name: 'vdtpTestByEmail', complexType: Scimitar::ComplexTypes::Email, required: false
|
@@ -255,6 +255,47 @@ RSpec.describe Scimitar::Resources::Mixin do
|
|
255
255
|
# =========================================================================
|
256
256
|
|
257
257
|
context '#to_scim' do
|
258
|
+
context 'with list of requested attributes' do
|
259
|
+
it 'compiles instance attribute values into a SCIM representation, including only the requested attributes' do
|
260
|
+
uuid = SecureRandom.uuid
|
261
|
+
|
262
|
+
instance = MockUser.new
|
263
|
+
instance.primary_key = uuid
|
264
|
+
instance.scim_uid = 'AA02984'
|
265
|
+
instance.username = 'foo'
|
266
|
+
instance.password = 'correcthorsebatterystaple'
|
267
|
+
instance.first_name = 'Foo'
|
268
|
+
instance.last_name = 'Bar'
|
269
|
+
instance.work_email_address = 'foo.bar@test.com'
|
270
|
+
instance.home_email_address = nil
|
271
|
+
instance.work_phone_number = '+642201234567'
|
272
|
+
instance.organization = 'SOMEORG'
|
273
|
+
|
274
|
+
g1 = MockGroup.create!(display_name: 'Group 1')
|
275
|
+
g2 = MockGroup.create!(display_name: 'Group 2')
|
276
|
+
g3 = MockGroup.create!(display_name: 'Group 3')
|
277
|
+
|
278
|
+
g1.mock_users << instance
|
279
|
+
g3.mock_users << instance
|
280
|
+
|
281
|
+
scim = instance.to_scim(location: "https://test.com/mock_users/#{uuid}", include_attributes: %w[id userName name groups.display groups.value organization])
|
282
|
+
json = scim.to_json()
|
283
|
+
hash = JSON.parse(json)
|
284
|
+
|
285
|
+
expect(hash).to eql({
|
286
|
+
'id' => uuid,
|
287
|
+
'userName' => 'foo',
|
288
|
+
'name' => {'givenName'=>'Foo', 'familyName'=>'Bar'},
|
289
|
+
'groups' => [{'display'=>g1.display_name, 'value'=>g1.id.to_s}, {'display'=>g3.display_name, 'value'=>g3.id.to_s}],
|
290
|
+
'meta' => {'location'=>"https://test.com/mock_users/#{uuid}", 'resourceType'=>'User'},
|
291
|
+
'schemas' => ['urn:ietf:params:scim:schemas:core:2.0:User', 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'],
|
292
|
+
'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User' => {
|
293
|
+
'organization' => 'SOMEORG',
|
294
|
+
},
|
295
|
+
})
|
296
|
+
end
|
297
|
+
end # "context 'with list of requested attributes' do"
|
298
|
+
|
258
299
|
context 'with a UUID, renamed primary key column' do
|
259
300
|
it 'compiles instance attribute values into a SCIM representation, but omits do-not-return fields' do
|
260
301
|
uuid = SecureRandom.uuid
|
@@ -296,7 +337,8 @@ RSpec.describe Scimitar::Resources::Mixin do
|
|
296
337
|
|
297
338
|
'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User' => {
|
298
339
|
'organization' => 'SOMEORG',
|
299
|
-
'department' => nil
|
340
|
+
'department' => nil,
|
341
|
+
'primaryEmail' => instance.work_email_address
|
300
342
|
}
|
301
343
|
})
|
302
344
|
end
|
@@ -27,7 +27,7 @@ RSpec.describe Scimitar::Schema::User do
|
|
27
27
|
"subAttributes": [
|
28
28
|
{
|
29
29
|
"multiValued": false,
|
30
|
-
"required":
|
30
|
+
"required": false,
|
31
31
|
"caseExact": false,
|
32
32
|
"mutability": "readWrite",
|
33
33
|
"uniqueness": "none",
|
@@ -37,7 +37,7 @@ RSpec.describe Scimitar::Schema::User do
|
|
37
37
|
},
|
38
38
|
{
|
39
39
|
"multiValued": false,
|
40
|
-
"required":
|
40
|
+
"required": false,
|
41
41
|
"caseExact": false,
|
42
42
|
"mutability": "readWrite",
|
43
43
|
"uniqueness": "none",
|
@@ -107,6 +107,29 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
107
107
|
expect(usernames).to match_array(['2'])
|
108
108
|
end
|
109
109
|
|
110
|
+
it 'returns only the requested attributes' do
|
111
|
+
get '/Users', params: {
|
112
|
+
format: :scim,
|
113
|
+
attributes: "id,name"
|
114
|
+
}
|
115
|
+
|
116
|
+
expect(response.status ).to eql(200)
|
117
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
118
|
+
|
119
|
+
result = JSON.parse(response.body)
|
120
|
+
|
121
|
+
expect(result['totalResults']).to eql(3)
|
122
|
+
expect(result['Resources'].size).to eql(3)
|
123
|
+
|
124
|
+
keys = result['Resources'].map { |resource| resource.keys }.flatten.uniq
|
125
|
+
expect(keys).to match_array(%w[id meta name schemas urn:ietf:params:scim:schemas:extension:enterprise:2.0:User])
|
126
|
+
expect(result.dig('Resources', 0, 'id')).to eql @u1.primary_key.to_s
|
127
|
+
expect(result.dig('Resources', 0, 'name', 'givenName')).to eql 'Foo'
|
128
|
+
expect(result.dig('Resources', 0, 'name', 'familyName')).to eql 'Ark'
|
129
|
+
end
|
130
|
+
|
131
|
+
# https://github.com/RIPAGlobal/scimitar/issues/37
|
132
|
+
#
|
110
133
|
it 'applies a filter, with case-insensitive attribute matching (GitHub issue #37)' do
|
111
134
|
get '/Users', params: {
|
112
135
|
format: :scim,
|
@@ -128,6 +151,30 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
128
151
|
expect(usernames).to match_array(['2'])
|
129
152
|
end
|
130
153
|
|
154
|
+
# https://github.com/RIPAGlobal/scimitar/issues/115
|
155
|
+
#
|
156
|
+
it 'handles broken Microsoft filters (GitHub issue #115)' do
|
157
|
+
get '/Users', params: {
|
158
|
+
format: :scim,
|
159
|
+
filter: 'name[givenName eq "FOO"].familyName pr and emails ne "home_1@test.com"'
|
160
|
+
}
|
161
|
+
|
162
|
+
expect(response.status ).to eql(200)
|
163
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
164
|
+
|
165
|
+
result = JSON.parse(response.body)
|
166
|
+
|
167
|
+
expect(result['totalResults']).to eql(1)
|
168
|
+
expect(result['Resources'].size).to eql(1)
|
169
|
+
|
170
|
+
ids = result['Resources'].map { |resource| resource['id'] }
|
171
|
+
expect(ids).to match_array([@u2.primary_key.to_s])
|
172
|
+
|
173
|
+
usernames = result['Resources'].map { |resource| resource['userName'] }
|
174
|
+
expect(usernames).to match_array(['2'])
|
175
|
+
end
|
176
|
+
|
177
|
+
|
131
178
|
# Strange attribute capitalisation in tests here builds on test coverage
|
132
179
|
# for now-fixed GitHub issue #37.
|
133
180
|
#
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: scimitar
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- RIPA Global
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2024-06-
|
12
|
+
date: 2024-06-13 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rails
|