scimitar 1.5.0 → 1.5.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/controllers/scimitar/application_controller.rb +2 -2
- data/app/models/scimitar/engine_configuration.rb +9 -5
- data/app/models/scimitar/resources/mixin.rb +36 -3
- data/app/models/scimitar/service_provider_configuration.rb +14 -3
- data/config/initializers/scimitar.rb +90 -86
- data/lib/scimitar/version.rb +2 -2
- data/lib/scimitar.rb +18 -2
- data/spec/apps/dummy/app/models/mock_user.rb +9 -1
- data/spec/apps/dummy/config/initializers/scimitar.rb +45 -0
- data/spec/apps/dummy/db/migrate/20210304014602_create_mock_users.rb +8 -0
- data/spec/apps/dummy/db/schema.rb +2 -0
- data/spec/controllers/scimitar/schemas_controller_spec.rb +2 -2
- data/spec/models/scimitar/resources/base_spec.rb +161 -66
- data/spec/models/scimitar/resources/mixin_spec.rb +84 -5
- data/spec/requests/active_record_backed_resources_controller_spec.rb +1 -1
- data/spec/requests/application_controller_spec.rb +10 -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: 6d57cfdaba9d48c6c193fb74baafc7c6e26c004a5a634460bc2f9ec94ad0440e
|
4
|
+
data.tar.gz: 8ff5ffbabe01c86822bd2c1dc85c535bf95b197cbc41920a995aa1801d239f32
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 91f6cba011c909de21f7c391dbfc5b18a30b7ef9cf81d9f43842452fa021484f55623034be386af6242e9a1a82efa9ef9436d8b5c17248bf0b9384587fecb50b
|
7
|
+
data.tar.gz: e72e91fa8c3dd85df64b806dd3e6cb2b4d2460b33cb7d3c241bc64e8557adbccff9e3071fd18fab676ffdcd2a8c7f375512f85919e6ba10601021d127097bef0
|
@@ -99,10 +99,10 @@ module Scimitar
|
|
99
99
|
def require_scim
|
100
100
|
scim_mime_type = Mime::Type.lookup_by_extension(:scim).to_s
|
101
101
|
|
102
|
-
if request.
|
102
|
+
if request.media_type.nil? || request.media_type.empty?
|
103
103
|
request.format = :scim
|
104
104
|
request.headers['CONTENT_TYPE'] = scim_mime_type
|
105
|
-
elsif request.
|
105
|
+
elsif request.media_type.downcase == scim_mime_type
|
106
106
|
request.format = :scim
|
107
107
|
elsif request.format == :scim
|
108
108
|
request.headers['CONTENT_TYPE'] = scim_mime_type
|
@@ -7,13 +7,17 @@ module Scimitar
|
|
7
7
|
class EngineConfiguration
|
8
8
|
include ActiveModel::Model
|
9
9
|
|
10
|
-
attr_accessor
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
10
|
+
attr_accessor(
|
11
|
+
:uses_defaults,
|
12
|
+
:basic_authenticator,
|
13
|
+
:token_authenticator,
|
14
|
+
:application_controller_mixin,
|
15
|
+
:exception_reporter,
|
16
|
+
:optional_value_fields_required,
|
17
|
+
)
|
15
18
|
|
16
19
|
def initialize(attributes = {})
|
20
|
+
@uses_defaults = attributes.empty?
|
17
21
|
|
18
22
|
# Set defaults that may be overridden by the initializer.
|
19
23
|
#
|
@@ -443,9 +443,30 @@ module Scimitar
|
|
443
443
|
ci_scim_hash = { 'root' => ci_scim_hash }.with_indifferent_case_insensitive_access()
|
444
444
|
end
|
445
445
|
|
446
|
+
# Handle extension schema. Contributed by @bettysteger and
|
447
|
+
# @MorrisFreeman via:
|
448
|
+
#
|
449
|
+
# https://github.com/RIPAGlobal/scimitar/issues/48
|
450
|
+
# https://github.com/RIPAGlobal/scimitar/pull/49
|
451
|
+
#
|
452
|
+
# Note the ":" separating the schema ID (URN) from the attribute.
|
453
|
+
# The nature of JSON rendering / other payloads might lead you to
|
454
|
+
# expect a "." as with any complex types, but that's not the case;
|
455
|
+
# see https://tools.ietf.org/html/rfc7644#section-3.10, or
|
456
|
+
# https://tools.ietf.org/html/rfc7644#section-3.5.2 of which in
|
457
|
+
# particular, https://tools.ietf.org/html/rfc7644#page-35.
|
458
|
+
#
|
459
|
+
paths = []
|
460
|
+
self.class.scim_resource_type.extended_schemas.each do |schema|
|
461
|
+
path_str.downcase.split(schema.id.downcase + ':').drop(1).each do |path|
|
462
|
+
paths += [schema.id] + path.split('.')
|
463
|
+
end
|
464
|
+
end
|
465
|
+
paths = path_str.split('.') if paths.empty?
|
466
|
+
|
446
467
|
self.from_patch_backend!(
|
447
468
|
nature: nature,
|
448
|
-
path:
|
469
|
+
path: paths,
|
449
470
|
value: value,
|
450
471
|
altering_hash: ci_scim_hash
|
451
472
|
)
|
@@ -616,7 +637,19 @@ module Scimitar
|
|
616
637
|
attrs_map_or_leaf_value.each do | scim_attribute, sub_attrs_map_or_leaf_value |
|
617
638
|
next if scim_attribute&.to_s&.downcase == 'id' && path.empty?
|
618
639
|
|
619
|
-
|
640
|
+
# Handle extension schema. Contributed by @bettysteger and
|
641
|
+
# @MorrisFreeman via:
|
642
|
+
#
|
643
|
+
# https://github.com/RIPAGlobal/scimitar/issues/48
|
644
|
+
# https://github.com/RIPAGlobal/scimitar/pull/49
|
645
|
+
#
|
646
|
+
attribute_tree = []
|
647
|
+
resource_class.extended_schemas.each do |schema|
|
648
|
+
attribute_tree << schema.id and break if schema.scim_attributes.any? { |attribute| attribute.name == scim_attribute.to_s }
|
649
|
+
end
|
650
|
+
attribute_tree << scim_attribute.to_s
|
651
|
+
|
652
|
+
sub_scim_hash_or_leaf_value = scim_hash_or_leaf_value&.dig(*attribute_tree)
|
620
653
|
|
621
654
|
self.from_scim_backend!(
|
622
655
|
attrs_map_or_leaf_value: sub_attrs_map_or_leaf_value,
|
@@ -914,7 +947,7 @@ module Scimitar
|
|
914
947
|
# which would imply "payload removes all users", there is the
|
915
948
|
# clear intent to remove just one.
|
916
949
|
#
|
917
|
-
# https://
|
950
|
+
# https://tools.ietf.org/html/rfc7644#section-3.5.2.2
|
918
951
|
# https://learn.microsoft.com/en-us/azure/active-directory/app-provisioning/use-scim-to-provision-users-and-groups#update-group-remove-members
|
919
952
|
#
|
920
953
|
# Since remove-all in the face of remove-one is destructive, we
|
@@ -9,11 +9,22 @@ module Scimitar
|
|
9
9
|
class ServiceProviderConfiguration
|
10
10
|
include ActiveModel::Model
|
11
11
|
|
12
|
-
attr_accessor
|
13
|
-
:
|
14
|
-
:
|
12
|
+
attr_accessor(
|
13
|
+
:uses_defaults,
|
14
|
+
:patch,
|
15
|
+
:bulk,
|
16
|
+
:filter,
|
17
|
+
:changePassword,
|
18
|
+
:sort,
|
19
|
+
:etag,
|
20
|
+
:authenticationSchemes,
|
21
|
+
:schemas,
|
22
|
+
:meta,
|
23
|
+
)
|
15
24
|
|
16
25
|
def initialize(attributes = {})
|
26
|
+
@uses_defaults = attributes.empty?
|
27
|
+
|
17
28
|
defaults = {
|
18
29
|
bulk: Supportable.unsupported,
|
19
30
|
changePassword: Supportable.unsupported,
|
@@ -2,101 +2,105 @@
|
|
2
2
|
#
|
3
3
|
# For supporting information and rationale, please see README.md.
|
4
4
|
|
5
|
-
#
|
6
|
-
# SERVICE PROVIDER CONFIGURATION
|
7
|
-
# =============================================================================
|
8
|
-
#
|
9
|
-
# This is a Ruby abstraction over a SCIM entity that declares the capabilities
|
10
|
-
# supported by a particular implementation.
|
11
|
-
#
|
12
|
-
# Typically this is used to declare parts of the standard unsupported, if you
|
13
|
-
# don't need them and don't want to provide subclass support.
|
14
|
-
#
|
15
|
-
Scimitar.service_provider_configuration = Scimitar::ServiceProviderConfiguration.new({
|
5
|
+
Rails.application.config.to_prepare do # (required for >= Rails 7 / Zeitwerk)
|
16
6
|
|
17
|
-
#
|
7
|
+
# ===========================================================================
|
8
|
+
# SERVICE PROVIDER CONFIGURATION
|
9
|
+
# ===========================================================================
|
18
10
|
#
|
19
|
-
#
|
20
|
-
#
|
21
|
-
# that filters are not supported so that calling clients shouldn't use them:
|
11
|
+
# This is a Ruby abstraction over a SCIM entity that declares the
|
12
|
+
# capabilities supported by a particular implementation.
|
22
13
|
#
|
23
|
-
#
|
14
|
+
# Typically this is used to declare parts of the standard unsupported, if you
|
15
|
+
# don't need them and don't want to provide subclass support.
|
16
|
+
#
|
17
|
+
Scimitar.service_provider_configuration = Scimitar::ServiceProviderConfiguration.new({
|
24
18
|
|
25
|
-
|
19
|
+
# See https://tools.ietf.org/html/rfc7643#section-8.5 for properties.
|
20
|
+
#
|
21
|
+
# See Gem file 'app/models/scimitar/service_provider_configuration.rb'
|
22
|
+
# for defaults. Define Hash keys here that override defaults; e.g. to
|
23
|
+
# declare that filters are not supported so that calling clients shouldn't
|
24
|
+
# use them:
|
25
|
+
#
|
26
|
+
# filter: Scimitar::Supported.unsupported
|
26
27
|
|
27
|
-
|
28
|
-
# ENGINE CONFIGURATION
|
29
|
-
# =============================================================================
|
30
|
-
#
|
31
|
-
# This is where you provide callbacks for things like authorisation or mixins
|
32
|
-
# that get included into all Scimitar-derived controllers (for things like
|
33
|
-
# before-actions that apply to all Scimitar controller-based routes).
|
34
|
-
#
|
35
|
-
Scimitar.engine_configuration = Scimitar::EngineConfiguration.new({
|
28
|
+
})
|
36
29
|
|
37
|
-
#
|
38
|
-
#
|
39
|
-
#
|
40
|
-
#
|
41
|
-
# For example:
|
42
|
-
#
|
43
|
-
# application_controller_mixin: Module.new do
|
44
|
-
# def self.included(base)
|
45
|
-
# base.class_eval do
|
30
|
+
# ===========================================================================
|
31
|
+
# ENGINE CONFIGURATION
|
32
|
+
# ===========================================================================
|
46
33
|
#
|
47
|
-
#
|
48
|
-
#
|
49
|
-
#
|
34
|
+
# This is where you provide callbacks for things like authorisation or mixins
|
35
|
+
# that get included into all Scimitar-derived controllers (for things like
|
36
|
+
# before-actions that apply to all Scimitar controller-based routes).
|
50
37
|
#
|
51
|
-
|
52
|
-
# prepend_before_action :setup_some_kind_of_multi_tenancy_data
|
53
|
-
# end
|
54
|
-
# end
|
55
|
-
# end, # ...other configuration entries might follow...
|
38
|
+
Scimitar.engine_configuration = Scimitar::EngineConfiguration.new({
|
56
39
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
40
|
+
# If you have filters you want to run for any Scimitar action/route, you
|
41
|
+
# can define them here. For example, you might use a before-action to set
|
42
|
+
# up some multi-tenancy related state, or skip Rails CSRF token
|
43
|
+
# verification. For example:
|
44
|
+
#
|
45
|
+
# application_controller_mixin: Module.new do
|
46
|
+
# def self.included(base)
|
47
|
+
# base.class_eval do
|
48
|
+
#
|
49
|
+
# # Anything here is written just as you'd write it at the top of
|
50
|
+
# # one of your controller classes, but it gets included in all
|
51
|
+
# # Scimitar classes too.
|
52
|
+
#
|
53
|
+
# skip_before_action :verify_authenticity_token
|
54
|
+
# prepend_before_action :setup_some_kind_of_multi_tenancy_data
|
55
|
+
# end
|
56
|
+
# end
|
57
|
+
# end, # ...other configuration entries might follow...
|
67
58
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
#
|
79
|
-
# Note that both basic and token authentication can be declared, with the
|
80
|
-
# parameters in the inbound HTTP request determining which is invoked.
|
59
|
+
# If you want to support username/password authentication:
|
60
|
+
#
|
61
|
+
# basic_authenticator: Proc.new do | username, password |
|
62
|
+
# # Check username/password and return 'true' if valid, else 'false'.
|
63
|
+
# end, # ...other configuration entries might follow...
|
64
|
+
#
|
65
|
+
# The 'username' and 'password' parameters come from Rails:
|
66
|
+
#
|
67
|
+
# https://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Basic.html
|
68
|
+
# https://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Basic/ControllerMethods.html#method-i-authenticate_with_http_basic
|
81
69
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
70
|
+
# If you want to support HTTP bearer token (OAuth-style) authentication:
|
71
|
+
#
|
72
|
+
# token_authenticator: Proc.new do | token, options |
|
73
|
+
# # Check token and return 'true' if valid, else 'false'.
|
74
|
+
# end, # ...other configuration entries might follow...
|
75
|
+
#
|
76
|
+
# The 'token' and 'options' parameters come from Rails:
|
77
|
+
#
|
78
|
+
# https://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Token.html
|
79
|
+
# https://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Token/ControllerMethods.html#method-i-authenticate_with_http_token
|
80
|
+
#
|
81
|
+
# Note that both basic and token authentication can be declared, with the
|
82
|
+
# parameters in the inbound HTTP request determining which is invoked.
|
94
83
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
84
|
+
# Scimitar rescues certain error cases and exceptions, in order to return a
|
85
|
+
# JSON response to the API caller. If you want exceptions to also be
|
86
|
+
# reported to a third party system such as sentry.io or raygun.com, you can
|
87
|
+
# configure a Proc to do so. It is passed a Ruby exception subclass object.
|
88
|
+
# For example, a minimal sentry.io reporter might do this:
|
89
|
+
#
|
90
|
+
# exception_reporter: Proc.new do | exception |
|
91
|
+
# Sentry.capture_exception(exception)
|
92
|
+
# end
|
93
|
+
#
|
94
|
+
# You will still need to configure your reporting system according to its
|
95
|
+
# documentation (e.g. via a Rails "config/initializers/<foo>.rb" file).
|
96
|
+
|
97
|
+
# Scimilar treats "VDTP" (Value, Display, Type, Primary) attribute values,
|
98
|
+
# used for e.g. e-mail addresses or phone numbers, as required by default.
|
99
|
+
# If you encounter a service which calls these with e.g. "null" value data,
|
100
|
+
# you can configure all values to be optional. You'll need to deal with
|
101
|
+
# whatever that means for you receiving system in your model code.
|
102
|
+
#
|
103
|
+
# optional_value_fields_required: false
|
104
|
+
})
|
105
|
+
|
106
|
+
end
|
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 = '1.5.
|
6
|
+
VERSION = '1.5.3'
|
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 = '2023-
|
11
|
+
DATE = '2023-09-16'
|
12
12
|
|
13
13
|
end
|
data/lib/scimitar.rb
CHANGED
@@ -4,7 +4,9 @@ require 'scimitar/engine'
|
|
4
4
|
|
5
5
|
module Scimitar
|
6
6
|
def self.service_provider_configuration=(custom_configuration)
|
7
|
-
@service_provider_configuration
|
7
|
+
if @service_provider_configuration.nil? || ! custom_configuration.uses_defaults
|
8
|
+
@service_provider_configuration = custom_configuration
|
9
|
+
end
|
8
10
|
end
|
9
11
|
|
10
12
|
def self.service_provider_configuration(location:)
|
@@ -14,11 +16,25 @@ module Scimitar
|
|
14
16
|
end
|
15
17
|
|
16
18
|
def self.engine_configuration=(custom_configuration)
|
17
|
-
@engine_configuration
|
19
|
+
if @engine_configuration.nil? || ! custom_configuration.uses_defaults
|
20
|
+
@engine_configuration = custom_configuration
|
21
|
+
end
|
18
22
|
end
|
19
23
|
|
20
24
|
def self.engine_configuration
|
21
25
|
@engine_configuration ||= EngineConfiguration.new
|
22
26
|
@engine_configuration
|
23
27
|
end
|
28
|
+
|
29
|
+
# Set in a "Rails.application.config.to_prepare" block by Scimitar itself to
|
30
|
+
# establish default values. Older Scimitar client applications might not use
|
31
|
+
# that wrapper; we don't want to overwrite settings they configured, but we
|
32
|
+
# *do* want to let them overwrite the defaults. Thus, '||=" is used here but
|
33
|
+
# not in ::service_provider_configuration=.
|
34
|
+
#
|
35
|
+
# Client applications should not call this method themselves.
|
36
|
+
#
|
37
|
+
def self.default_service_provider_configuration(default_configuration)
|
38
|
+
@service_provider_configuration ||= custom_configuration
|
39
|
+
end
|
24
40
|
end
|
@@ -13,6 +13,8 @@ class MockUser < ActiveRecord::Base
|
|
13
13
|
work_email_address
|
14
14
|
home_email_address
|
15
15
|
work_phone_number
|
16
|
+
organization
|
17
|
+
department
|
16
18
|
}
|
17
19
|
|
18
20
|
has_and_belongs_to_many :mock_groups
|
@@ -82,7 +84,13 @@ class MockUser < ActiveRecord::Base
|
|
82
84
|
}
|
83
85
|
}
|
84
86
|
],
|
85
|
-
active: :is_active
|
87
|
+
active: :is_active,
|
88
|
+
|
89
|
+
# Custom extension schema - see configuration in
|
90
|
+
# "spec/apps/dummy/config/initializers/scimitar.rb".
|
91
|
+
#
|
92
|
+
organization: :organization,
|
93
|
+
department: :department
|
86
94
|
}
|
87
95
|
end
|
88
96
|
|
@@ -1,5 +1,22 @@
|
|
1
1
|
# Test app configuration.
|
2
2
|
#
|
3
|
+
# Note that as a result of https://github.com/RIPAGlobal/scimitar/issues/48,
|
4
|
+
# tests include a custom extension of the core User schema. A shortcoming of
|
5
|
+
# some of the code from which Scimitar was originally built is that those
|
6
|
+
# extensions are done with class-level ivars, so it is largely impossible (or
|
7
|
+
# at least, impractical in tests) to avoid polluting the core class itself
|
8
|
+
# with the extension.
|
9
|
+
#
|
10
|
+
# All related schema tests are written with this in mind.
|
11
|
+
#
|
12
|
+
# Further, https://github.com/RIPAGlobal/scimitar/pull/54 fixed warning
|
13
|
+
# messages in a way that worked on Rails 6+ but, for V1 Scimitar, it would
|
14
|
+
# break existing working setups that didn't use the +to_prepare+ wrapper. Their
|
15
|
+
# application configuration would be written *first* but then *overwritten* by
|
16
|
+
# the default +to_prepare+ block in Scimitar itself, since that runs later. The
|
17
|
+
# file below does *not* use +to_prepare+ in order to test the workaround that
|
18
|
+
# was produced; it should work on all Ruby versions as-is.
|
19
|
+
#
|
3
20
|
Scimitar.engine_configuration = Scimitar::EngineConfiguration.new({
|
4
21
|
|
5
22
|
application_controller_mixin: Module.new do
|
@@ -12,3 +29,31 @@ Scimitar.engine_configuration = Scimitar::EngineConfiguration.new({
|
|
12
29
|
end
|
13
30
|
|
14
31
|
})
|
32
|
+
|
33
|
+
module ScimSchemaExtensions
|
34
|
+
module User
|
35
|
+
class Enterprise < Scimitar::Schema::Base
|
36
|
+
def initialize(options = {})
|
37
|
+
super(
|
38
|
+
name: 'ExtendedUser',
|
39
|
+
description: 'Enterprise extension for a User',
|
40
|
+
id: self.class.id,
|
41
|
+
scim_attributes: self.class.scim_attributes
|
42
|
+
)
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.id
|
46
|
+
'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.scim_attributes
|
50
|
+
[
|
51
|
+
Scimitar::Schema::Attribute.new(name: 'organization', type: 'string'),
|
52
|
+
Scimitar::Schema::Attribute.new(name: 'department', type: 'string')
|
53
|
+
]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
Scimitar::Resources::User.extend_schema ScimSchemaExtensions::User::Enterprise
|
@@ -3,6 +3,8 @@ class CreateMockUsers < ActiveRecord::Migration[6.1]
|
|
3
3
|
create_table :mock_users, id: :uuid, primary_key: :primary_key do |t|
|
4
4
|
t.timestamps
|
5
5
|
|
6
|
+
# Support part of the core schema
|
7
|
+
#
|
6
8
|
t.text :scim_uid
|
7
9
|
t.text :username
|
8
10
|
t.text :first_name
|
@@ -10,6 +12,12 @@ class CreateMockUsers < ActiveRecord::Migration[6.1]
|
|
10
12
|
t.text :work_email_address
|
11
13
|
t.text :home_email_address
|
12
14
|
t.text :work_phone_number
|
15
|
+
|
16
|
+
# Support the custom extension schema - see configuration in
|
17
|
+
# "spec/apps/dummy/config/initializers/scimitar.rb".
|
18
|
+
#
|
19
|
+
t.text :organization
|
20
|
+
t.text :department
|
13
21
|
end
|
14
22
|
end
|
15
23
|
end
|
@@ -39,6 +39,8 @@ ActiveRecord::Schema.define(version: 2021_03_08_044214) do
|
|
39
39
|
t.text "work_email_address"
|
40
40
|
t.text "home_email_address"
|
41
41
|
t.text "work_phone_number"
|
42
|
+
t.text "organization"
|
43
|
+
t.text "department"
|
42
44
|
end
|
43
45
|
|
44
46
|
add_foreign_key "mock_groups_users", "mock_groups"
|
@@ -14,9 +14,9 @@ RSpec.describe Scimitar::SchemasController do
|
|
14
14
|
get :index, params: { format: :scim }
|
15
15
|
expect(response).to be_ok
|
16
16
|
parsed_body = JSON.parse(response.body)
|
17
|
-
expect(parsed_body.length).to eql(
|
17
|
+
expect(parsed_body.length).to eql(3)
|
18
18
|
schema_names = parsed_body.map {|schema| schema['name']}
|
19
|
-
expect(schema_names).to match_array(['User', 'Group'])
|
19
|
+
expect(schema_names).to match_array(['User', 'ExtendedUser', 'Group'])
|
20
20
|
end
|
21
21
|
|
22
22
|
it 'returns only the User schema when its id is provided' do
|
@@ -250,90 +250,185 @@ RSpec.describe Scimitar::Resources::Base do
|
|
250
250
|
end # "context 'dynamic setters based on schema' do"
|
251
251
|
|
252
252
|
context 'schema extension' do
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
253
|
+
context 'of custom schema' do
|
254
|
+
ThirdCustomSchema = Class.new(Scimitar::Schema::Base) do
|
255
|
+
def self.id
|
256
|
+
'custom-id'
|
257
|
+
end
|
257
258
|
|
258
|
-
|
259
|
-
|
259
|
+
def self.scim_attributes
|
260
|
+
[ Scimitar::Schema::Attribute.new(name: 'name', type: 'string') ]
|
261
|
+
end
|
260
262
|
end
|
261
|
-
end
|
262
263
|
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
264
|
+
ExtensionSchema = Class.new(Scimitar::Schema::Base) do
|
265
|
+
def self.id
|
266
|
+
'extension-id'
|
267
|
+
end
|
267
268
|
|
268
|
-
|
269
|
-
|
269
|
+
def self.scim_attributes
|
270
|
+
[ Scimitar::Schema::Attribute.new(name: 'relationship', type: 'string', required: true) ]
|
271
|
+
end
|
270
272
|
end
|
271
|
-
end
|
272
273
|
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
274
|
+
let(:resource_class) {
|
275
|
+
Class.new(Scimitar::Resources::Base) do
|
276
|
+
set_schema ThirdCustomSchema
|
277
|
+
extend_schema ExtensionSchema
|
277
278
|
|
278
|
-
|
279
|
-
|
279
|
+
def self.endpoint
|
280
|
+
'/gaga'
|
281
|
+
end
|
282
|
+
|
283
|
+
def self.resource_type_id
|
284
|
+
'CustomResource'
|
285
|
+
end
|
280
286
|
end
|
287
|
+
}
|
281
288
|
|
282
|
-
|
283
|
-
|
289
|
+
context '#initialize' do
|
290
|
+
it 'allows setting extension attributes' do
|
291
|
+
resource = resource_class.new('extension-id' => {relationship: 'GAGA'})
|
292
|
+
expect(resource.relationship).to eql('GAGA')
|
284
293
|
end
|
285
|
-
end
|
286
|
-
|
294
|
+
end # "context '#initialize' do"
|
295
|
+
|
296
|
+
context '#as_json' do
|
297
|
+
it 'namespaces the extension attributes' do
|
298
|
+
resource = resource_class.new(relationship: 'GAGA')
|
299
|
+
hash = resource.as_json
|
300
|
+
expect(hash["schemas"]).to eql(['custom-id', 'extension-id'])
|
301
|
+
expect(hash["extension-id"]).to eql("relationship" => 'GAGA')
|
302
|
+
end
|
303
|
+
end # "context '#as_json' do"
|
287
304
|
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
305
|
+
context '.resource_type' do
|
306
|
+
it 'appends the extension schemas' do
|
307
|
+
resource_type = resource_class.resource_type('http://gaga')
|
308
|
+
expect(resource_type.meta.location).to eql('http://gaga')
|
309
|
+
expect(resource_type.schemaExtensions.count).to eql(1)
|
310
|
+
end
|
294
311
|
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
expect(hash["schemas"]).to eql(['custom-id', 'extension-id'])
|
300
|
-
expect(hash["extension-id"]).to eql("relationship" => 'GAGA')
|
301
|
-
end
|
302
|
-
end # "context '#as_json' do"
|
312
|
+
context 'validation' do
|
313
|
+
it 'validates into custom schema' do
|
314
|
+
resource = resource_class.new('extension-id' => {})
|
315
|
+
expect(resource.valid?).to eql(false)
|
303
316
|
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
end
|
317
|
+
resource = resource_class.new('extension-id' => {relationship: 'GAGA'})
|
318
|
+
expect(resource.relationship).to eql('GAGA')
|
319
|
+
expect(resource.valid?).to eql(true)
|
320
|
+
end
|
321
|
+
end # context 'validation'
|
322
|
+
end # "context '.resource_type' do"
|
310
323
|
|
311
|
-
context '
|
312
|
-
it '
|
313
|
-
|
314
|
-
expect(
|
324
|
+
context '.find_attribute' do
|
325
|
+
it 'finds in first schema' do
|
326
|
+
found = resource_class().find_attribute('name') # Defined in ThirdCustomSchema
|
327
|
+
expect(found).to be_present
|
328
|
+
expect(found.name).to eql('name')
|
329
|
+
expect(found.type).to eql('string')
|
330
|
+
end
|
315
331
|
|
316
|
-
|
317
|
-
|
318
|
-
expect(
|
332
|
+
it 'finds across schemas' do
|
333
|
+
found = resource_class().find_attribute('relationship') # Defined in ExtensionSchema
|
334
|
+
expect(found).to be_present
|
335
|
+
expect(found.name).to eql('relationship')
|
336
|
+
expect(found.type).to eql('string')
|
319
337
|
end
|
320
|
-
end # context '
|
321
|
-
end # "context '
|
338
|
+
end # "context '.find_attribute' do"
|
339
|
+
end # "context 'of custom schema' do"
|
322
340
|
|
323
|
-
context '
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
expect(found.type).to eql('string')
|
329
|
-
end
|
341
|
+
context 'of core schema' do
|
342
|
+
EnterpriseExtensionSchema = Class.new(Scimitar::Schema::Base) do
|
343
|
+
def self.id
|
344
|
+
'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'
|
345
|
+
end
|
330
346
|
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
347
|
+
def self.scim_attributes
|
348
|
+
[
|
349
|
+
Scimitar::Schema::Attribute.new(name: 'organization', type: 'string'),
|
350
|
+
Scimitar::Schema::Attribute.new(name: 'department', type: 'string')
|
351
|
+
]
|
352
|
+
end
|
336
353
|
end
|
337
|
-
|
354
|
+
|
355
|
+
let(:resource_class) {
|
356
|
+
Class.new(Scimitar::Resources::Base) do
|
357
|
+
set_schema Scimitar::Schema::User
|
358
|
+
extend_schema EnterpriseExtensionSchema
|
359
|
+
|
360
|
+
def self.endpoint
|
361
|
+
'/Users'
|
362
|
+
end
|
363
|
+
|
364
|
+
def self.resource_type_id
|
365
|
+
'User'
|
366
|
+
end
|
367
|
+
end
|
368
|
+
}
|
369
|
+
|
370
|
+
context '#initialize' do
|
371
|
+
it 'allows setting extension attributes' do
|
372
|
+
resource = resource_class.new('urn:ietf:params:scim:schemas:extension:enterprise:2.0:User' => {organization: 'SOMEORG', department: 'SOMEDPT'})
|
373
|
+
|
374
|
+
expect(resource.organization).to eql('SOMEORG')
|
375
|
+
expect(resource.department ).to eql('SOMEDPT')
|
376
|
+
end
|
377
|
+
end # "context '#initialize' do"
|
378
|
+
|
379
|
+
context '#as_json' do
|
380
|
+
it 'namespaces the extension attributes' do
|
381
|
+
resource = resource_class.new(organization: 'SOMEORG', department: 'SOMEDPT')
|
382
|
+
hash = resource.as_json
|
383
|
+
|
384
|
+
expect(hash['schemas']).to eql(['urn:ietf:params:scim:schemas:core:2.0:User', 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'])
|
385
|
+
expect(hash['urn:ietf:params:scim:schemas:extension:enterprise:2.0:User']).to eql('organization' => 'SOMEORG', 'department' => 'SOMEDPT')
|
386
|
+
end
|
387
|
+
end # "context '#as_json' do"
|
388
|
+
|
389
|
+
context '.resource_type' do
|
390
|
+
it 'appends the extension schemas' do
|
391
|
+
resource_type = resource_class.resource_type('http://example.com')
|
392
|
+
expect(resource_type.meta.location).to eql('http://example.com')
|
393
|
+
expect(resource_type.schemaExtensions.count).to eql(1)
|
394
|
+
end
|
395
|
+
|
396
|
+
context 'validation' do
|
397
|
+
it 'validates into custom schema' do
|
398
|
+
resource = resource_class.new('urn:ietf:params:scim:schemas:extension:enterprise:2.0:User' => {})
|
399
|
+
expect(resource.valid?).to eql(false)
|
400
|
+
|
401
|
+
resource = resource_class.new(
|
402
|
+
'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User' => {
|
403
|
+
userName: 'SOMEUSR',
|
404
|
+
organization: 'SOMEORG',
|
405
|
+
department: 'SOMEDPT'
|
406
|
+
}
|
407
|
+
)
|
408
|
+
|
409
|
+
expect(resource.organization).to eql('SOMEORG')
|
410
|
+
expect(resource.department ).to eql('SOMEDPT')
|
411
|
+
expect(resource.valid? ).to eql(true)
|
412
|
+
end
|
413
|
+
end # context 'validation'
|
414
|
+
end # "context '.resource_type' do"
|
415
|
+
|
416
|
+
context '.find_attribute' do
|
417
|
+
it 'finds in first schema' do
|
418
|
+
found = resource_class().find_attribute('userName') # Defined in Scimitar::Schema::User
|
419
|
+
|
420
|
+
expect(found).to be_present
|
421
|
+
expect(found.name).to eql('userName')
|
422
|
+
expect(found.type).to eql('string')
|
423
|
+
end
|
424
|
+
|
425
|
+
it 'finds across schemas' do
|
426
|
+
found = resource_class().find_attribute('organization') # Defined in EnterpriseExtensionSchema
|
427
|
+
expect(found).to be_present
|
428
|
+
expect(found.name).to eql('organization')
|
429
|
+
expect(found.type).to eql('string')
|
430
|
+
end
|
431
|
+
end # "context '.find_attribute' do"
|
432
|
+
end # "context 'of core schema' do"
|
338
433
|
end # "context 'schema extension' do"
|
339
434
|
end
|
@@ -172,6 +172,7 @@ RSpec.describe Scimitar::Resources::Mixin do
|
|
172
172
|
instance.work_email_address = 'foo.bar@test.com'
|
173
173
|
instance.home_email_address = nil
|
174
174
|
instance.work_phone_number = '+642201234567'
|
175
|
+
instance.organization = 'SOMEORG'
|
175
176
|
|
176
177
|
g1 = MockGroup.create!(display_name: 'Group 1')
|
177
178
|
g2 = MockGroup.create!(display_name: 'Group 2')
|
@@ -194,7 +195,12 @@ RSpec.describe Scimitar::Resources::Mixin do
|
|
194
195
|
'externalId' => 'AA02984',
|
195
196
|
'groups' => [{'display'=>g1.display_name, 'value'=>g1.id.to_s}, {'display'=>g3.display_name, 'value'=>g3.id.to_s}],
|
196
197
|
'meta' => {'location'=>"https://test.com/mock_users/#{uuid}", 'resourceType'=>'User'},
|
197
|
-
'schemas' => ['urn:ietf:params:scim:schemas:core:2.0:User']
|
198
|
+
'schemas' => ['urn:ietf:params:scim:schemas:core:2.0:User', 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'],
|
199
|
+
|
200
|
+
'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User' => {
|
201
|
+
'organization' => 'SOMEORG',
|
202
|
+
'department' => nil
|
203
|
+
}
|
198
204
|
})
|
199
205
|
end
|
200
206
|
end # "context 'with a UUID, renamed primary key column' do"
|
@@ -318,7 +324,9 @@ RSpec.describe Scimitar::Resources::Mixin do
|
|
318
324
|
],
|
319
325
|
|
320
326
|
'meta' => {'location'=>'https://test.com/static_map_test', 'resourceType'=>'User'},
|
321
|
-
'schemas' => ['urn:ietf:params:scim:schemas:core:2.0:User']
|
327
|
+
'schemas' => ['urn:ietf:params:scim:schemas:core:2.0:User', 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'],
|
328
|
+
|
329
|
+
'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User' => {}
|
322
330
|
})
|
323
331
|
end
|
324
332
|
end # "context 'using static mappings' do"
|
@@ -345,7 +353,9 @@ RSpec.describe Scimitar::Resources::Mixin do
|
|
345
353
|
],
|
346
354
|
|
347
355
|
'meta' => {'location'=>'https://test.com/dynamic_map_test', 'resourceType'=>'User'},
|
348
|
-
'schemas' => ['urn:ietf:params:scim:schemas:core:2.0:User']
|
356
|
+
'schemas' => ['urn:ietf:params:scim:schemas:core:2.0:User', 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'],
|
357
|
+
|
358
|
+
'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User' => {}
|
349
359
|
})
|
350
360
|
end
|
351
361
|
end # "context 'using dynamic lists' do"
|
@@ -402,7 +412,12 @@ RSpec.describe Scimitar::Resources::Mixin do
|
|
402
412
|
'id' => '42', # Note, String
|
403
413
|
'externalId' => 'AA02984',
|
404
414
|
'meta' => {'location' => 'https://test.com/mock_users/42', 'resourceType' => 'User'},
|
405
|
-
'schemas' => ['urn:ietf:params:scim:schemas:core:2.0:User']
|
415
|
+
'schemas' => ['urn:ietf:params:scim:schemas:core:2.0:User', 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'],
|
416
|
+
|
417
|
+
'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User' => {
|
418
|
+
'organization' => 'SOMEORG',
|
419
|
+
'DEPARTMENT' => 'SOMEDPT'
|
420
|
+
}
|
406
421
|
}
|
407
422
|
|
408
423
|
hash = spec_helper_hupcase(hash) if force_upper_case
|
@@ -418,6 +433,8 @@ RSpec.describe Scimitar::Resources::Mixin do
|
|
418
433
|
expect(instance.work_email_address).to eql('foo.bar@test.com')
|
419
434
|
expect(instance.home_email_address).to be_nil
|
420
435
|
expect(instance.work_phone_number ).to eql('+642201234567')
|
436
|
+
expect(instance.organization ).to eql('SOMEORG')
|
437
|
+
expect(instance.department ).to eql('SOMEDPT')
|
421
438
|
end
|
422
439
|
|
423
440
|
it 'honouring read-write lists' do
|
@@ -704,6 +721,21 @@ RSpec.describe Scimitar::Resources::Mixin do
|
|
704
721
|
expect(scim_hash['name']['familyName']).to eql('Bar')
|
705
722
|
end
|
706
723
|
|
724
|
+
it 'with schema extensions: overwrites' do
|
725
|
+
path = [ 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User', 'organization' ]
|
726
|
+
scim_hash = { 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User' => { 'organization' => 'SOMEORG' } }.with_indifferent_case_insensitive_access()
|
727
|
+
|
728
|
+
@instance.send(
|
729
|
+
:from_patch_backend!,
|
730
|
+
nature: 'add',
|
731
|
+
path: path,
|
732
|
+
value: 'OTHERORG',
|
733
|
+
altering_hash: scim_hash
|
734
|
+
)
|
735
|
+
|
736
|
+
expect(scim_hash['urn:ietf:params:scim:schemas:extension:enterprise:2.0:User']['organization' ]).to eql('OTHERORG')
|
737
|
+
end
|
738
|
+
|
707
739
|
# For 'add', filter at end-of-path is nonsensical and not
|
708
740
|
# supported by spec or Scimitar; we only test mid-path filters.
|
709
741
|
#
|
@@ -892,6 +924,21 @@ RSpec.describe Scimitar::Resources::Mixin do
|
|
892
924
|
expect(scim_hash['name']['givenName']).to eql('Baz')
|
893
925
|
end
|
894
926
|
|
927
|
+
it 'with schema extensions: adds' do
|
928
|
+
path = [ 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User', 'organization' ]
|
929
|
+
scim_hash = {}.with_indifferent_case_insensitive_access()
|
930
|
+
|
931
|
+
@instance.send(
|
932
|
+
:from_patch_backend!,
|
933
|
+
nature: 'add',
|
934
|
+
path: path,
|
935
|
+
value: 'SOMEORG',
|
936
|
+
altering_hash: scim_hash
|
937
|
+
)
|
938
|
+
|
939
|
+
expect(scim_hash['urn:ietf:params:scim:schemas:extension:enterprise:2.0:User']['organization' ]).to eql('SOMEORG')
|
940
|
+
end
|
941
|
+
|
895
942
|
context 'with filter mid-path: adds' do
|
896
943
|
it 'by string match' do
|
897
944
|
path = [ 'emails[type eq "work"]', 'value' ]
|
@@ -1233,7 +1280,7 @@ RSpec.describe Scimitar::Resources::Mixin do
|
|
1233
1280
|
|
1234
1281
|
# What we expect:
|
1235
1282
|
#
|
1236
|
-
# https://
|
1283
|
+
# https://tools.ietf.org/html/rfc7644#section-3.5.2.2
|
1237
1284
|
# https://docs.snowflake.com/en/user-guide/scim-intro.html#patch-scim-v2-groups-id
|
1238
1285
|
#
|
1239
1286
|
# ...vs accounting for the unusual payloads we sometimes get,
|
@@ -2680,6 +2727,38 @@ RSpec.describe Scimitar::Resources::Mixin do
|
|
2680
2727
|
expect(@instance.first_name).to eql('Baz')
|
2681
2728
|
end
|
2682
2729
|
|
2730
|
+
# Note odd ":" separating schema ID from first attribute, although
|
2731
|
+
# the nature of JSON rendering / other payloads might lead you to
|
2732
|
+
# expect a "." as with any other path component.
|
2733
|
+
#
|
2734
|
+
# Note the ":" separating the schema ID (URN) from the attribute.
|
2735
|
+
# The nature of JSON rendering / other payloads might lead you to
|
2736
|
+
# expect a "." as with any complex types, but that's not the case;
|
2737
|
+
# see https://tools.ietf.org/html/rfc7644#section-3.10, or
|
2738
|
+
# https://tools.ietf.org/html/rfc7644#section-3.5.2 of which in
|
2739
|
+
# particular, https://tools.ietf.org/html/rfc7644#page-35.
|
2740
|
+
#
|
2741
|
+
it 'which updates attributes defined by extension schema' do
|
2742
|
+
@instance.update!(department: 'SOMEDPT')
|
2743
|
+
|
2744
|
+
path = 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:department'
|
2745
|
+
path = path.upcase if force_upper_case
|
2746
|
+
|
2747
|
+
patch = {
|
2748
|
+
'schemas' => ['urn:ietf:params:scim:api:messages:2.0:PatchOp'],
|
2749
|
+
'Operations' => [
|
2750
|
+
{
|
2751
|
+
'op' => 'replace',
|
2752
|
+
'path' => path,
|
2753
|
+
'value' => 'OTHERDPT'
|
2754
|
+
}
|
2755
|
+
]
|
2756
|
+
}
|
2757
|
+
|
2758
|
+
@instance.from_scim_patch!(patch_hash: patch)
|
2759
|
+
expect(@instance.department).to eql('OTHERDPT')
|
2760
|
+
end
|
2761
|
+
|
2683
2762
|
it 'which updates with filter match' do
|
2684
2763
|
@instance.update!(work_email_address: 'work@test.com', home_email_address: 'home@test.com')
|
2685
2764
|
|
@@ -762,7 +762,7 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
762
762
|
end
|
763
763
|
end
|
764
764
|
|
765
|
-
# https://
|
765
|
+
# https://tools.ietf.org/html/rfc7644#section-3.5.2.2
|
766
766
|
#
|
767
767
|
context 'and using an RFC-compliant payload' do
|
768
768
|
let(:removed_user) { @u2 }
|
@@ -39,6 +39,16 @@ RSpec.describe Scimitar::ApplicationController do
|
|
39
39
|
expect(parsed_body['request']['content_type']).to eql('application/scim+json')
|
40
40
|
end
|
41
41
|
|
42
|
+
it 'translates Content-Type with charset to Rails request format' do
|
43
|
+
get '/CustomRequestVerifiers', headers: { 'CONTENT_TYPE' => 'application/scim+json; charset=utf-8' }
|
44
|
+
|
45
|
+
expect(response).to have_http_status(:ok)
|
46
|
+
parsed_body = JSON.parse(response.body)
|
47
|
+
expect(parsed_body['request']['is_scim' ]).to eql(true)
|
48
|
+
expect(parsed_body['request']['format' ]).to eql('application/scim+json')
|
49
|
+
expect(parsed_body['request']['content_type']).to eql('application/scim+json; charset=utf-8')
|
50
|
+
end
|
51
|
+
|
42
52
|
it 'translates Rails request format to header' do
|
43
53
|
get '/CustomRequestVerifiers', params: { format: :scim }
|
44
54
|
|
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: 1.5.
|
4
|
+
version: 1.5.3
|
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: 2023-
|
12
|
+
date: 2023-09-16 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rails
|