scimitar 1.5.0 → 1.5.3
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/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
|