scimitar 2.13.0 → 2.15.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/LICENSE.txt +1 -1
- data/README.md +31 -2
- data/Rakefile +1 -2
- data/app/controllers/scimitar/active_record_backed_resources_controller.rb +11 -5
- data/app/controllers/scimitar/application_controller.rb +31 -13
- data/app/models/scimitar/engine_configuration.rb +5 -2
- data/app/models/scimitar/lists/count.rb +6 -3
- data/app/models/scimitar/resources/mixin.rb +7 -1
- data/config/initializers/scimitar.rb +9 -1
- data/lib/scimitar/version.rb +2 -2
- data/spec/controllers/scimitar/application_controller_spec.rb +86 -13
- data/spec/models/scimitar/lists/count_spec.rb +8 -3
- data/spec/models/scimitar/resources/mixin_spec.rb +110 -2
- data/spec/requests/active_record_backed_resources_controller_spec.rb +86 -0
- metadata +8 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7c705a0369ccfd6204da2dd2a0e5a17a15f2fa658703218404d7556c1568a891
|
|
4
|
+
data.tar.gz: cd8ab1a8fc432533a6a0e4ee6045f20677c9164dfd330c416a68b5728b58947f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 11846119f8df609b6f75f95a436a78e2ca9dceed1a9d236cd08c89df052e45677efcee573943feaee719b7fff6f648bd13622a47d32f493bb69d3b8a58415363
|
|
7
|
+
data.tar.gz: 32bdc2ba6b4613c498ed988bfb2a4397c26f1d0196341f071b52d4bdb13065b9a658654dba1666bbb75c6a185afa924d6b6aa60d2f4ef9784cc9af6df4395623
|
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
|
@@ -64,7 +64,7 @@ Some aspects of configuration are handled via a `config/initializers/scimitar.rb
|
|
|
64
64
|
Rails.application.config.to_prepare do
|
|
65
65
|
Scimitar.engine_configuration = Scimitar::EngineConfiguration.new({
|
|
66
66
|
# ...see subsections below for configuration options...
|
|
67
|
-
|
|
67
|
+
})
|
|
68
68
|
end
|
|
69
69
|
```
|
|
70
70
|
|
|
@@ -184,7 +184,36 @@ Note that Okta has some [curious documentation on its use of `POST` vs `PATCH` f
|
|
|
184
184
|
|
|
185
185
|
### Google Workspace note
|
|
186
186
|
|
|
187
|
-
Using SCIM with Google Workspace might only work for a subset of applications. Since web UIs for major service providers change very often, it doesn't make sense to provide extensive documentation here as it would get out of date quickly; you may have to figure out the setup as best you can using whatever current Google documentation exists for their system. There are [some notes which were relevant around mid-2025](https://github.com/pond/scimitar/issues/142#issuecomment-2699050541) (from when a
|
|
187
|
+
Using SCIM with Google Workspace might only work for a subset of applications. Since web UIs for major service providers change very often, it doesn't make sense to provide extensive documentation here as it would get out of date quickly; you may have to figure out the setup as best you can using whatever current Google documentation exists for their system. There are [some notes which were relevant around mid-2025](https://github.com/pond/scimitar/issues/142#issuecomment-2699050541) (from when a workaround/fix was incorporated into Scimitar to allow it to work with Google Workspace) which may help you get started.
|
|
188
|
+
|
|
189
|
+
### Request content type handling
|
|
190
|
+
|
|
191
|
+
The correct content type for SCIM is `application/scim+json`. Scimitar tolerates some variants of this, rewriting things internally so that the request continues to be processed by the rest of the gem (including ending up in subclass code you write) under this media type, with a Rails request format of `:scim`. Sometimes, callers into SCIM endpoints might use a content type that Scimitar rejects. If so, you can configure a custom request sanitizer Proc (with a "z", in keeping with Rails spelling of "sanitize"):
|
|
192
|
+
|
|
193
|
+
```ruby
|
|
194
|
+
Rails.application.config.to_prepare do
|
|
195
|
+
Scimitar.engine_configuration = Scimitar::EngineConfiguration.new({
|
|
196
|
+
|
|
197
|
+
custom_request_sanitizer: Proc.new do | request |
|
|
198
|
+
#
|
|
199
|
+
# Examine e.g. 'request.media_type' and evaluate to a Symbol:
|
|
200
|
+
#
|
|
201
|
+
# :success - set the standard content type and ensure the Rails request
|
|
202
|
+
# format is :scim; continue processing normally
|
|
203
|
+
#
|
|
204
|
+
# :preserve - retain the existing content type and Rails request format;
|
|
205
|
+
# continue processing normally
|
|
206
|
+
#
|
|
207
|
+
# :fail - the request format appears to be invalid; generate a 406
|
|
208
|
+
# ("Not Acceptable") response
|
|
209
|
+
#
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
})
|
|
213
|
+
end
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
The Proc is passed the [`ActionDispatch::Request`](https://api.rubyonrails.org/classes/ActionDispatch/Request.html) instance for the current request. It **must** evaluable to a Symbol as shown above. Typically, `:preserve` is only for very special use cases where you understand that the request headers and/or format might not match what other parts of Scimitar expect, but have written appropriate custom code elsewhere to deal with that.
|
|
188
217
|
|
|
189
218
|
### Data models
|
|
190
219
|
|
data/Rakefile
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
require 'rake'
|
|
2
2
|
require 'rspec/core/rake_task'
|
|
3
3
|
require 'rdoc/task'
|
|
4
|
-
require 'sdoc'
|
|
5
4
|
|
|
6
5
|
RSpec::Core::RakeTask.new(:default) do | t |
|
|
7
6
|
end
|
|
@@ -12,5 +11,5 @@ Rake::RDocTask.new do | rd |
|
|
|
12
11
|
rd.title = 'Scimitar'
|
|
13
12
|
rd.main = 'README.md'
|
|
14
13
|
rd.rdoc_dir = 'docs/rdoc'
|
|
15
|
-
rd.generator = '
|
|
14
|
+
rd.generator = 'rdoc'
|
|
16
15
|
end
|
|
@@ -36,11 +36,17 @@ module Scimitar
|
|
|
36
36
|
|
|
37
37
|
pagination_info = scim_pagination_info(query.count())
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
# SCIM 2.0 RFC 7644: When count=0, return metadata only (no Resources).
|
|
40
|
+
# This avoids an unnecessary database query for record data.
|
|
41
|
+
page_of_results = if pagination_info.limit == 0
|
|
42
|
+
[]
|
|
43
|
+
else
|
|
44
|
+
query
|
|
45
|
+
.order(@id_column => :asc)
|
|
46
|
+
.offset(pagination_info.offset)
|
|
47
|
+
.limit(pagination_info.limit)
|
|
48
|
+
.to_a()
|
|
49
|
+
end
|
|
44
50
|
|
|
45
51
|
super(pagination_info, page_of_results) do | record |
|
|
46
52
|
record_to_scim(record)
|
|
@@ -48,7 +48,7 @@ module Scimitar
|
|
|
48
48
|
#
|
|
49
49
|
def handle_scim_error(error_response, exception = error_response)
|
|
50
50
|
unless Scimitar.engine_configuration.exception_reporter.nil?
|
|
51
|
-
Scimitar.engine_configuration.exception_reporter
|
|
51
|
+
instance_exec(exception, &Scimitar.engine_configuration.exception_reporter)
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
render json: error_response, status: error_response.status
|
|
@@ -93,19 +93,37 @@ module Scimitar
|
|
|
93
93
|
#
|
|
94
94
|
def require_scim
|
|
95
95
|
scim_mime_type = Mime::Type.lookup_by_extension(:scim).to_s
|
|
96
|
+
failure_detail = "Only #{scim_mime_type} type is accepted."
|
|
97
|
+
|
|
98
|
+
if Scimitar.engine_configuration.custom_request_sanitizer.is_a?(Proc)
|
|
99
|
+
|
|
100
|
+
result = Scimitar.engine_configuration.custom_request_sanitizer.call(request)
|
|
101
|
+
case result
|
|
102
|
+
when :fail
|
|
103
|
+
handle_scim_error(ErrorResponse.new(status: 406, detail: failure_detail))
|
|
104
|
+
when :preserve
|
|
105
|
+
# Do nothing
|
|
106
|
+
else
|
|
107
|
+
request.format = :scim
|
|
108
|
+
request.headers['CONTENT_TYPE'] = scim_mime_type
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
else # "if Scimitar.engine_configuration.custom_request_sanitizer.present?"
|
|
112
|
+
|
|
113
|
+
if request.media_type.nil? || request.media_type.empty?
|
|
114
|
+
request.format = :scim
|
|
115
|
+
request.headers['CONTENT_TYPE'] = scim_mime_type
|
|
116
|
+
elsif request.media_type.downcase == scim_mime_type
|
|
117
|
+
request.format = :scim
|
|
118
|
+
elsif request.format == :scim
|
|
119
|
+
request.headers['CONTENT_TYPE'] = scim_mime_type
|
|
120
|
+
elsif request.media_type.downcase == 'application/json' && request.user_agent&.start_with?('Google') # https://github.com/pond/scimitar/issues/142
|
|
121
|
+
request.format = :scim
|
|
122
|
+
request.headers["CONTENT_TYPE"] = scim_mime_type
|
|
123
|
+
else
|
|
124
|
+
handle_scim_error(ErrorResponse.new(status: 406, detail: failure_detail))
|
|
125
|
+
end
|
|
96
126
|
|
|
97
|
-
if request.media_type.nil? || request.media_type.empty?
|
|
98
|
-
request.format = :scim
|
|
99
|
-
request.headers['CONTENT_TYPE'] = scim_mime_type
|
|
100
|
-
elsif request.media_type.downcase == scim_mime_type
|
|
101
|
-
request.format = :scim
|
|
102
|
-
elsif request.format == :scim
|
|
103
|
-
request.headers['CONTENT_TYPE'] = scim_mime_type
|
|
104
|
-
elsif request.media_type.downcase == 'application/json' && request.user_agent.start_with?('Google') # https://github.com/pond/scimitar/issues/142
|
|
105
|
-
request.format = :scim
|
|
106
|
-
request.headers["CONTENT_TYPE"] = scim_mime_type
|
|
107
|
-
else
|
|
108
|
-
handle_scim_error(ErrorResponse.new(status: 406, detail: "Only #{scim_mime_type} type is accepted."))
|
|
109
127
|
end
|
|
110
128
|
end
|
|
111
129
|
|
|
@@ -12,9 +12,11 @@ module Scimitar
|
|
|
12
12
|
:basic_authenticator,
|
|
13
13
|
:token_authenticator,
|
|
14
14
|
:custom_authenticator,
|
|
15
|
+
:custom_request_sanitizer,
|
|
15
16
|
:application_controller_mixin,
|
|
16
17
|
:exception_reporter,
|
|
17
18
|
:optional_value_fields_required,
|
|
19
|
+
:render_mapped_nil_values_in_response,
|
|
18
20
|
:schema_list_from_attribute_mappings,
|
|
19
21
|
)
|
|
20
22
|
|
|
@@ -24,8 +26,9 @@ module Scimitar
|
|
|
24
26
|
# Set defaults that may be overridden by the initializer.
|
|
25
27
|
#
|
|
26
28
|
defaults = {
|
|
27
|
-
optional_value_fields_required:
|
|
28
|
-
|
|
29
|
+
optional_value_fields_required: true,
|
|
30
|
+
render_mapped_nil_values_in_response: true,
|
|
31
|
+
schema_list_from_attribute_mappings: []
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
super(defaults.merge(attributes))
|
|
@@ -15,9 +15,12 @@ module Scimitar
|
|
|
15
15
|
|
|
16
16
|
# Set a limit (page size) value.
|
|
17
17
|
#
|
|
18
|
-
# +value+:: Integer value held in a String. Must be >=
|
|
18
|
+
# +value+:: Integer value held in a String. Must be >= 0.
|
|
19
|
+
#
|
|
20
|
+
# Per SCIM 2.0 RFC 7644 Section 3.4.2.4: "A value of '0' indicates that
|
|
21
|
+
# no resource results are to be returned except for 'totalResults'."
|
|
19
22
|
#
|
|
20
|
-
# Raises exceptions if given non-numeric
|
|
23
|
+
# Raises exceptions if given non-numeric or negative input.
|
|
21
24
|
#
|
|
22
25
|
def limit=(value)
|
|
23
26
|
value = value&.to_s
|
|
@@ -25,7 +28,7 @@ module Scimitar
|
|
|
25
28
|
|
|
26
29
|
validate_numericality(value)
|
|
27
30
|
input = value.to_i
|
|
28
|
-
raise if input <
|
|
31
|
+
raise if input < 0
|
|
29
32
|
@limit = input
|
|
30
33
|
end
|
|
31
34
|
|
|
@@ -612,7 +612,13 @@ module Scimitar
|
|
|
612
612
|
end
|
|
613
613
|
end
|
|
614
614
|
|
|
615
|
-
|
|
615
|
+
if (
|
|
616
|
+
include_attributes.any? or
|
|
617
|
+
! Scimitar.engine_configuration.render_mapped_nil_values_in_response
|
|
618
|
+
)
|
|
619
|
+
result.compact!
|
|
620
|
+
end
|
|
621
|
+
|
|
616
622
|
result
|
|
617
623
|
|
|
618
624
|
when Array # Static or dynamic mapping against lists in data source
|
|
@@ -102,7 +102,9 @@ Rails.application.config.to_prepare do # (required for >= Rails 7 / Zeitwerk)
|
|
|
102
102
|
# JSON response to the API caller. If you want exceptions to also be
|
|
103
103
|
# reported to a third party system such as sentry.io or raygun.com, you can
|
|
104
104
|
# configure a Proc to do so. It is passed a Ruby exception subclass object.
|
|
105
|
-
#
|
|
105
|
+
# The Proc is called via 'instance_exec' in the controller context, so you
|
|
106
|
+
# have access to things like 'request', 'params' and 'action_name'. For
|
|
107
|
+
# example, a minimal sentry.io reporter might do this:
|
|
106
108
|
#
|
|
107
109
|
# exception_reporter: Proc.new do | exception |
|
|
108
110
|
# Sentry.capture_exception(exception)
|
|
@@ -119,6 +121,12 @@ Rails.application.config.to_prepare do # (required for >= Rails 7 / Zeitwerk)
|
|
|
119
121
|
#
|
|
120
122
|
# optional_value_fields_required: false
|
|
121
123
|
|
|
124
|
+
# When rendering responses, +nil+ values can either still be included via
|
|
125
|
+
# the attributes map with a JSON value of +null+, or omitted. By default,
|
|
126
|
+
# all attributes in your map are returned in responses.
|
|
127
|
+
#
|
|
128
|
+
# render_mapped_nil_values_in_response: false
|
|
129
|
+
|
|
122
130
|
# The SCIM standard `/Schemas` endpoint lists, by default, all known schema
|
|
123
131
|
# definitions with the mutabilty (read-write, read-only, write-only) state
|
|
124
132
|
# described by those definitions, and includes all defined attributes. For
|
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.15.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 = '
|
|
11
|
+
DATE = '2026-03-06'
|
|
12
12
|
|
|
13
13
|
end
|
|
@@ -352,8 +352,9 @@ RSpec.describe Scimitar::ApplicationController do
|
|
|
352
352
|
context 'with an exception reporter' do
|
|
353
353
|
around :each do | example |
|
|
354
354
|
original_configuration = Scimitar.engine_configuration.exception_reporter
|
|
355
|
+
exceptions = @exceptions = []
|
|
355
356
|
Scimitar.engine_configuration.exception_reporter = Proc.new do | exception |
|
|
356
|
-
|
|
357
|
+
exceptions << exception
|
|
357
358
|
end
|
|
358
359
|
example.run()
|
|
359
360
|
ensure
|
|
@@ -364,8 +365,8 @@ RSpec.describe Scimitar::ApplicationController do
|
|
|
364
365
|
it 'is invoked' do
|
|
365
366
|
get :index, params: { format: :scim }
|
|
366
367
|
|
|
367
|
-
expect(@
|
|
368
|
-
expect(@
|
|
368
|
+
expect(@exceptions.first).to be_a(RuntimeError)
|
|
369
|
+
expect(@exceptions.first.message).to eql('Bang')
|
|
369
370
|
end
|
|
370
371
|
end
|
|
371
372
|
|
|
@@ -379,8 +380,8 @@ RSpec.describe Scimitar::ApplicationController do
|
|
|
379
380
|
it 'is invoked' do
|
|
380
381
|
get :index, params: { format: :scim }
|
|
381
382
|
|
|
382
|
-
expect(@
|
|
383
|
-
expect(@
|
|
383
|
+
expect(@exceptions.first).to be_a(ActiveRecord::RecordNotFound)
|
|
384
|
+
expect(@exceptions.first.message).to eql('42')
|
|
384
385
|
end
|
|
385
386
|
end
|
|
386
387
|
|
|
@@ -398,8 +399,8 @@ RSpec.describe Scimitar::ApplicationController do
|
|
|
398
399
|
it 'is invoked' do
|
|
399
400
|
get :index, params: { format: :scim }
|
|
400
401
|
|
|
401
|
-
expect(@
|
|
402
|
-
expect(@
|
|
402
|
+
expect(@exceptions.first).to be_a(ActionDispatch::Http::Parameters::ParseError)
|
|
403
|
+
expect(@exceptions.first.message).to eql('Hello')
|
|
403
404
|
end
|
|
404
405
|
end
|
|
405
406
|
|
|
@@ -412,8 +413,8 @@ RSpec.describe Scimitar::ApplicationController do
|
|
|
412
413
|
request.headers['Content-Type'] = 'text/plain'
|
|
413
414
|
get :index
|
|
414
415
|
|
|
415
|
-
expect(@
|
|
416
|
-
expect(@
|
|
416
|
+
expect(@exceptions.first).to be_a(Scimitar::ErrorResponse)
|
|
417
|
+
expect(@exceptions.first.message).to eql('Only application/scim+json type is accepted.')
|
|
417
418
|
end
|
|
418
419
|
end
|
|
419
420
|
|
|
@@ -423,18 +424,90 @@ RSpec.describe Scimitar::ApplicationController do
|
|
|
423
424
|
request.headers['User-Agent' ] = 'Google-Auto-Provisioning'
|
|
424
425
|
get :index
|
|
425
426
|
|
|
426
|
-
expect(@
|
|
427
|
-
expect(@
|
|
427
|
+
expect(@exceptions.first).to be_a(RuntimeError)
|
|
428
|
+
expect(@exceptions.first.message).to eql('Bang')
|
|
428
429
|
end
|
|
429
430
|
|
|
430
431
|
it 'is invoked early for unrecognised agents' do
|
|
431
432
|
request.headers['Content-Type'] = 'application/json'
|
|
432
433
|
get :index
|
|
433
434
|
|
|
434
|
-
expect(@
|
|
435
|
-
expect(@
|
|
435
|
+
expect(@exceptions.first).to be_a(Scimitar::ErrorResponse)
|
|
436
|
+
expect(@exceptions.first.message).to eql('Only application/scim+json type is accepted.')
|
|
436
437
|
end
|
|
437
438
|
end # "context 'and with Google SCIM calls' do"
|
|
439
|
+
|
|
440
|
+
context 'and with a custom request sanitizer' do
|
|
441
|
+
around :each do | example |
|
|
442
|
+
original_configuration = Scimitar.engine_configuration.custom_request_sanitizer
|
|
443
|
+
Scimitar.engine_configuration.custom_request_sanitizer = Proc.new do | request |
|
|
444
|
+
case request.media_type
|
|
445
|
+
when 'application/json+success'
|
|
446
|
+
:success
|
|
447
|
+
when 'application/json+preserve'
|
|
448
|
+
:preserve
|
|
449
|
+
else
|
|
450
|
+
:fail
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
example.run()
|
|
454
|
+
ensure
|
|
455
|
+
Scimitar.engine_configuration.custom_request_sanitizer = original_configuration
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
context 'returning "success"' do
|
|
459
|
+
it 'reaches the controller action with cleaned up request data' do
|
|
460
|
+
request.headers['Content-Type'] = 'application/json+success'
|
|
461
|
+
get :index
|
|
462
|
+
|
|
463
|
+
expect(@exceptions.first).to be_a(RuntimeError)
|
|
464
|
+
expect(@exceptions.first.message).to eql('Bang')
|
|
465
|
+
|
|
466
|
+
expect(request.format == :scim).to eql(true)
|
|
467
|
+
expect(request.headers['CONTENT_TYPE']).to eql('application/scim+json')
|
|
468
|
+
end
|
|
469
|
+
end # "context 'returning "success"' do"
|
|
470
|
+
|
|
471
|
+
context 'returning "preserve"' do
|
|
472
|
+
it 'reaches the controller action with unmodified request data' do
|
|
473
|
+
request.headers['Content-Type'] = 'application/json+preserve'
|
|
474
|
+
get :index
|
|
475
|
+
|
|
476
|
+
expect(@exceptions.first).to be_a(RuntimeError)
|
|
477
|
+
expect(@exceptions.first.message).to eql('Bang')
|
|
478
|
+
|
|
479
|
+
expect(request.format == :html).to eql(true)
|
|
480
|
+
expect(request.headers['CONTENT_TYPE']).to eql('application/json+preserve')
|
|
481
|
+
end
|
|
482
|
+
end # "context 'returning "keep"' do"
|
|
483
|
+
|
|
484
|
+
context 'returning "fail"' do
|
|
485
|
+
it 'is invoked' do
|
|
486
|
+
request.headers['Content-Type'] = 'application/json+fail'
|
|
487
|
+
get :index
|
|
488
|
+
|
|
489
|
+
expect(@exceptions.first).to be_a(Scimitar::ErrorResponse)
|
|
490
|
+
expect(@exceptions.first.message).to eql('Only application/scim+json type is accepted.')
|
|
491
|
+
end
|
|
492
|
+
end # "context 'returning "fail"' do"
|
|
493
|
+
end # "context 'and with a custom request sanitizer' do"
|
|
494
|
+
|
|
495
|
+
context 'evaluated in controller context' do
|
|
496
|
+
it 'has access to controller methods like request and params' do
|
|
497
|
+
reported_request_method = nil
|
|
498
|
+
reported_params = nil
|
|
499
|
+
Scimitar.engine_configuration.exception_reporter = Proc.new do | exception |
|
|
500
|
+
reported_request_method = request.method
|
|
501
|
+
reported_params = params
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
get :index, params: { format: :scim, foo: 'bar' }
|
|
505
|
+
|
|
506
|
+
expect(reported_request_method).to eql('GET')
|
|
507
|
+
expect(reported_params['format']).to eql('scim')
|
|
508
|
+
expect(reported_params['foo']).to eql('bar')
|
|
509
|
+
end
|
|
510
|
+
end
|
|
438
511
|
end # "context 'exception reporter' do"
|
|
439
512
|
end # "context 'error handling' do"
|
|
440
513
|
end
|
|
@@ -34,12 +34,17 @@ RSpec.describe Scimitar::Lists::Count do
|
|
|
34
34
|
expect { @instance.limit = 'A' }.to raise_error(RuntimeError)
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
-
it '
|
|
38
|
-
expect { @instance.limit = '0' }.
|
|
37
|
+
it 'allows count=0 per SCIM 2.0 specification (RFC 7644)' do
|
|
38
|
+
expect { @instance.limit = '0' }.to_not raise_error
|
|
39
|
+
expect(@instance.limit).to eql(0)
|
|
39
40
|
end
|
|
40
41
|
|
|
41
|
-
it '
|
|
42
|
+
it 'allows count=0 as integer' do
|
|
43
|
+
expect { @instance.limit = 0 }.to_not raise_error
|
|
44
|
+
expect(@instance.limit).to eql(0)
|
|
45
|
+
end
|
|
42
46
|
|
|
47
|
+
it 'complains about attempts to set negative values' do
|
|
43
48
|
expect { @instance.limit = '-10' }.to raise_error(RuntimeError)
|
|
44
49
|
end
|
|
45
50
|
end # "context 'on-read error checking' do"
|
|
@@ -267,7 +267,7 @@ RSpec.describe Scimitar::Resources::Mixin do
|
|
|
267
267
|
instance.first_name = 'Foo'
|
|
268
268
|
instance.last_name = 'Bar'
|
|
269
269
|
instance.work_email_address = 'foo.bar@test.com'
|
|
270
|
-
instance.home_email_address =
|
|
270
|
+
instance.home_email_address = 'foo.bar@example.com'
|
|
271
271
|
instance.work_phone_number = '+642201234567'
|
|
272
272
|
instance.organization = 'SOMEORG'
|
|
273
273
|
|
|
@@ -299,6 +299,49 @@ RSpec.describe Scimitar::Resources::Mixin do
|
|
|
299
299
|
'urn:ietf:params:scim:schemas:extension:manager:1.0:User' => {},
|
|
300
300
|
})
|
|
301
301
|
end
|
|
302
|
+
|
|
303
|
+
it 'hides "nil" value attributes' do
|
|
304
|
+
uuid = SecureRandom.uuid
|
|
305
|
+
|
|
306
|
+
instance = MockUser.new
|
|
307
|
+
instance.primary_key = uuid
|
|
308
|
+
instance.scim_uid = 'AA02984'
|
|
309
|
+
instance.username = nil
|
|
310
|
+
instance.password = 'correcthorsebatterystaple'
|
|
311
|
+
instance.first_name = nil
|
|
312
|
+
instance.last_name = 'Bar'
|
|
313
|
+
instance.work_email_address = 'foo.bar@test.com'
|
|
314
|
+
instance.home_email_address = 'foo.bar@example.com'
|
|
315
|
+
instance.work_phone_number = '+642201234567'
|
|
316
|
+
instance.organization = 'SOMEORG'
|
|
317
|
+
|
|
318
|
+
g1 = MockGroup.create!(display_name: 'Group 1')
|
|
319
|
+
g2 = MockGroup.create!(display_name: 'Group 2')
|
|
320
|
+
g3 = MockGroup.create!(display_name: 'Group 3')
|
|
321
|
+
|
|
322
|
+
g1.mock_users << instance
|
|
323
|
+
g3.mock_users << instance
|
|
324
|
+
|
|
325
|
+
scim = instance.to_scim(location: "https://test.com/mock_users/#{uuid}", include_attributes: %w[id userName name groups.display groups.value organization])
|
|
326
|
+
json = scim.to_json()
|
|
327
|
+
hash = JSON.parse(json)
|
|
328
|
+
|
|
329
|
+
expect(hash).to eql({
|
|
330
|
+
'id' => uuid,
|
|
331
|
+
'name' => {'familyName'=>'Bar'},
|
|
332
|
+
'groups' => [{'display'=>g1.display_name, 'value'=>g1.id.to_s}, {'display'=>g3.display_name, 'value'=>g3.id.to_s}],
|
|
333
|
+
'meta' => {'location'=>"https://test.com/mock_users/#{uuid}", 'resourceType'=>'User'},
|
|
334
|
+
'schemas' => [
|
|
335
|
+
'urn:ietf:params:scim:schemas:core:2.0:User',
|
|
336
|
+
'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User',
|
|
337
|
+
'urn:ietf:params:scim:schemas:extension:manager:1.0:User',
|
|
338
|
+
],
|
|
339
|
+
'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User' => {
|
|
340
|
+
'organization' => 'SOMEORG',
|
|
341
|
+
},
|
|
342
|
+
'urn:ietf:params:scim:schemas:extension:manager:1.0:User' => {},
|
|
343
|
+
})
|
|
344
|
+
end
|
|
302
345
|
end # "context 'with list of requested attributes' do"
|
|
303
346
|
|
|
304
347
|
context 'with a UUID, renamed primary key column' do
|
|
@@ -332,7 +375,7 @@ RSpec.describe Scimitar::Resources::Mixin do
|
|
|
332
375
|
'userName' => 'foo',
|
|
333
376
|
'name' => {'givenName'=>'Foo', 'familyName'=>'Bar'},
|
|
334
377
|
'active' => true,
|
|
335
|
-
'emails' => [{'type'=>'work', 'primary'=>true, 'value'=>'foo.bar@test.com'}, {
|
|
378
|
+
'emails' => [{'type'=>'work', 'primary'=>true, 'value'=>'foo.bar@test.com'}, {'primary'=>false, 'type'=>'home', 'value'=>nil}],
|
|
336
379
|
'phoneNumbers'=> [{'type'=>'work', 'primary'=>false, 'value'=>'+642201234567'}],
|
|
337
380
|
'id' => uuid,
|
|
338
381
|
'externalId' => 'AA02984',
|
|
@@ -353,6 +396,71 @@ RSpec.describe Scimitar::Resources::Mixin do
|
|
|
353
396
|
},
|
|
354
397
|
})
|
|
355
398
|
end
|
|
399
|
+
|
|
400
|
+
context 'and when configured to omit "nil" values in the response' do
|
|
401
|
+
around :each do | example |
|
|
402
|
+
original_configuration = Scimitar.engine_configuration.render_mapped_nil_values_in_response
|
|
403
|
+
Scimitar.engine_configuration.render_mapped_nil_values_in_response = false
|
|
404
|
+
example.run()
|
|
405
|
+
ensure
|
|
406
|
+
Scimitar.engine_configuration.render_mapped_nil_values_in_response = original_configuration
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
it 'omits "nil" values as expected' do
|
|
410
|
+
uuid = SecureRandom.uuid
|
|
411
|
+
|
|
412
|
+
instance = MockUser.new
|
|
413
|
+
instance.primary_key = uuid
|
|
414
|
+
instance.scim_uid = 'AA02984'
|
|
415
|
+
instance.username = 'foo'
|
|
416
|
+
instance.password = 'correcthorsebatterystaple'
|
|
417
|
+
instance.first_name = nil
|
|
418
|
+
instance.last_name = 'Bar'
|
|
419
|
+
instance.work_email_address = 'foo.bar@test.com'
|
|
420
|
+
instance.home_email_address = nil
|
|
421
|
+
instance.work_phone_number = '+642201234567'
|
|
422
|
+
instance.organization = 'SOMEORG'
|
|
423
|
+
|
|
424
|
+
g1 = MockGroup.create!(display_name: 'Group 1')
|
|
425
|
+
g2 = MockGroup.create!(display_name: 'Group 2')
|
|
426
|
+
g3 = MockGroup.create!(display_name: 'Group 3')
|
|
427
|
+
|
|
428
|
+
g1.mock_users << instance
|
|
429
|
+
g3.mock_users << instance
|
|
430
|
+
|
|
431
|
+
scim = instance.to_scim(location: "https://test.com/mock_users/#{uuid}")
|
|
432
|
+
json = scim.to_json()
|
|
433
|
+
hash = JSON.parse(json)
|
|
434
|
+
|
|
435
|
+
# Note currently limited implementation for things like static maps,
|
|
436
|
+
# where part of the returned value is included; in this case, the
|
|
437
|
+
# "primary" value is "false" for e-mail of type "home", so the
|
|
438
|
+
# structure for that *does* appear in the output array even though
|
|
439
|
+
# the source dynamic data field from the NockUser instance is "nil".
|
|
440
|
+
#
|
|
441
|
+
expect(hash).to eql({
|
|
442
|
+
'userName' => 'foo',
|
|
443
|
+
'name' => {'familyName'=>'Bar'},
|
|
444
|
+
'active' => true,
|
|
445
|
+
'emails' => [{'type'=>'work', 'primary'=>true, 'value'=>'foo.bar@test.com'}, {'primary' => false, 'type' => 'home'}],
|
|
446
|
+
'phoneNumbers'=> [{'type'=>'work', 'primary'=>false, 'value'=>'+642201234567'}],
|
|
447
|
+
'id' => uuid,
|
|
448
|
+
'externalId' => 'AA02984',
|
|
449
|
+
'groups' => [{'display'=>g1.display_name, 'value'=>g1.id.to_s}, {'display'=>g3.display_name, 'value'=>g3.id.to_s}],
|
|
450
|
+
'meta' => {'location'=>"https://test.com/mock_users/#{uuid}", 'resourceType'=>'User'},
|
|
451
|
+
'schemas' => [
|
|
452
|
+
'urn:ietf:params:scim:schemas:core:2.0:User',
|
|
453
|
+
'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User',
|
|
454
|
+
'urn:ietf:params:scim:schemas:extension:manager:1.0:User',
|
|
455
|
+
],
|
|
456
|
+
'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User' => {
|
|
457
|
+
'organization' => 'SOMEORG',
|
|
458
|
+
'primaryEmail' => instance.work_email_address,
|
|
459
|
+
},
|
|
460
|
+
'urn:ietf:params:scim:schemas:extension:manager:1.0:User' => {}
|
|
461
|
+
})
|
|
462
|
+
end
|
|
463
|
+
end # "context 'and when configured to omit "nil" values in the response'" do"
|
|
356
464
|
end # "context 'with a UUID, renamed primary key column' do"
|
|
357
465
|
|
|
358
466
|
context 'with an integer, conventionally named primary key column' do
|
|
@@ -292,6 +292,92 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
|
292
292
|
usernames = result['Resources'].map { |resource| resource['userName'] }
|
|
293
293
|
expect(usernames).to match_array(['2', '3'])
|
|
294
294
|
end
|
|
295
|
+
|
|
296
|
+
# SCIM 2.0 RFC 7644 Section 3.4.2.4: count=0 support
|
|
297
|
+
context 'with count=0' do
|
|
298
|
+
it 'returns 200 OK' do
|
|
299
|
+
get '/Users', params: {
|
|
300
|
+
format: :scim,
|
|
301
|
+
count: 0
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
expect(response.status).to eql(200)
|
|
305
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
it 'returns totalResults with actual count' do
|
|
309
|
+
get '/Users', params: {
|
|
310
|
+
format: :scim,
|
|
311
|
+
count: 0
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
result = JSON.parse(response.body)
|
|
315
|
+
expect(result['totalResults']).to eql(3)
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
it 'returns itemsPerPage as 0' do
|
|
319
|
+
get '/Users', params: {
|
|
320
|
+
format: :scim,
|
|
321
|
+
count: 0
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
result = JSON.parse(response.body)
|
|
325
|
+
expect(result['itemsPerPage']).to eql(0)
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
it 'returns empty Resources array' do
|
|
329
|
+
get '/Users', params: {
|
|
330
|
+
format: :scim,
|
|
331
|
+
count: 0
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
result = JSON.parse(response.body)
|
|
335
|
+
expect(result['Resources']).to eql([])
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
it 'respects startIndex parameter' do
|
|
339
|
+
get '/Users', params: {
|
|
340
|
+
format: :scim,
|
|
341
|
+
count: 0,
|
|
342
|
+
startIndex: 5
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
result = JSON.parse(response.body)
|
|
346
|
+
expect(result['startIndex']).to eql(5)
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
it 'applies filters when calculating totalResults' do
|
|
350
|
+
get '/Users', params: {
|
|
351
|
+
format: :scim,
|
|
352
|
+
count: 0,
|
|
353
|
+
filter: 'name.familyName eq "Bar"'
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
result = JSON.parse(response.body)
|
|
357
|
+
expect(result['totalResults']).to eql(1)
|
|
358
|
+
expect(result['Resources']).to eql([])
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
it 'does not query for records (performance optimization)' do
|
|
362
|
+
# We should get the count but not fetch records
|
|
363
|
+
query_double = instance_double(ActiveRecord::Relation)
|
|
364
|
+
allow(MockUser).to receive(:all).and_return(query_double)
|
|
365
|
+
allow(query_double).to receive(:count).and_return(3)
|
|
366
|
+
|
|
367
|
+
# Should NOT call order, offset, limit, or to_a when count=0
|
|
368
|
+
expect(query_double).not_to receive(:order)
|
|
369
|
+
expect(query_double).not_to receive(:offset)
|
|
370
|
+
expect(query_double).not_to receive(:limit)
|
|
371
|
+
expect(query_double).not_to receive(:to_a)
|
|
372
|
+
|
|
373
|
+
get '/Users', params: {
|
|
374
|
+
format: :scim,
|
|
375
|
+
count: 0
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
expect(response.status).to eql(200)
|
|
379
|
+
end
|
|
380
|
+
end # "context 'with count=0' do"
|
|
295
381
|
end # "context 'with items' do"
|
|
296
382
|
|
|
297
383
|
context 'with bad calls' do
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: scimitar
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.15.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
|
-
- RIPA Global
|
|
8
7
|
- Andrew David Hodgkinson
|
|
8
|
+
- RIPA Global
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-03-06 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -44,14 +44,14 @@ dependencies:
|
|
|
44
44
|
requirements:
|
|
45
45
|
- - "~>"
|
|
46
46
|
- !ruby/object:Gem::Version
|
|
47
|
-
version: '13.
|
|
47
|
+
version: '13.3'
|
|
48
48
|
type: :development
|
|
49
49
|
prerelease: false
|
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
|
51
51
|
requirements:
|
|
52
52
|
- - "~>"
|
|
53
53
|
- !ruby/object:Gem::Version
|
|
54
|
-
version: '13.
|
|
54
|
+
version: '13.3'
|
|
55
55
|
- !ruby/object:Gem::Dependency
|
|
56
56
|
name: pg
|
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -86,14 +86,14 @@ dependencies:
|
|
|
86
86
|
requirements:
|
|
87
87
|
- - "~>"
|
|
88
88
|
- !ruby/object:Gem::Version
|
|
89
|
-
version: '
|
|
89
|
+
version: '7.2'
|
|
90
90
|
type: :development
|
|
91
91
|
prerelease: false
|
|
92
92
|
version_requirements: !ruby/object:Gem::Requirement
|
|
93
93
|
requirements:
|
|
94
94
|
- - "~>"
|
|
95
95
|
- !ruby/object:Gem::Version
|
|
96
|
-
version: '
|
|
96
|
+
version: '7.2'
|
|
97
97
|
- !ruby/object:Gem::Dependency
|
|
98
98
|
name: warden
|
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -296,7 +296,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
296
296
|
- !ruby/object:Gem::Version
|
|
297
297
|
version: '0'
|
|
298
298
|
requirements: []
|
|
299
|
-
rubygems_version:
|
|
299
|
+
rubygems_version: 4.0.3
|
|
300
300
|
specification_version: 4
|
|
301
301
|
summary: SCIM v2 for Rails
|
|
302
302
|
test_files:
|