scimitar 1.8.1 → 1.10.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/README.md +27 -20
- data/app/controllers/scimitar/active_record_backed_resources_controller.rb +5 -4
- data/app/controllers/scimitar/resource_types_controller.rb +0 -2
- data/app/controllers/scimitar/resources_controller.rb +0 -2
- data/app/controllers/scimitar/schemas_controller.rb +361 -3
- data/app/controllers/scimitar/service_provider_configurations_controller.rb +0 -1
- data/app/models/scimitar/engine_configuration.rb +3 -1
- data/app/models/scimitar/lists/query_parser.rb +88 -3
- data/app/models/scimitar/resources/base.rb +48 -14
- data/app/models/scimitar/resources/mixin.rb +531 -71
- data/app/models/scimitar/schema/name.rb +2 -2
- data/app/models/scimitar/schema/user.rb +10 -10
- data/config/initializers/scimitar.rb +41 -0
- data/lib/scimitar/engine.rb +57 -12
- data/lib/scimitar/support/hash_with_indifferent_case_insensitive_access.rb +140 -10
- data/lib/scimitar/support/utilities.rb +60 -0
- data/lib/scimitar/version.rb +2 -2
- data/spec/apps/dummy/app/models/mock_user.rb +18 -3
- data/spec/apps/dummy/config/initializers/scimitar.rb +31 -2
- data/spec/apps/dummy/db/migrate/20210304014602_create_mock_users.rb +1 -0
- data/spec/apps/dummy/db/schema.rb +1 -0
- data/spec/controllers/scimitar/schemas_controller_spec.rb +342 -54
- data/spec/models/scimitar/lists/query_parser_spec.rb +70 -0
- data/spec/models/scimitar/resources/base_spec.rb +20 -12
- data/spec/models/scimitar/resources/base_validation_spec.rb +16 -3
- data/spec/models/scimitar/resources/mixin_spec.rb +754 -122
- data/spec/models/scimitar/schema/user_spec.rb +2 -2
- data/spec/requests/active_record_backed_resources_controller_spec.rb +312 -5
- data/spec/requests/engine_spec.rb +75 -0
- data/spec/spec_helper.rb +1 -1
- data/spec/support/hash_with_indifferent_case_insensitive_access_spec.rb +108 -0
- metadata +22 -22
@@ -6,8 +6,8 @@ module Scimitar
|
|
6
6
|
|
7
7
|
def self.scim_attributes
|
8
8
|
@scim_attributes ||= [
|
9
|
-
Attribute.new(name: 'familyName', type: 'string'
|
10
|
-
Attribute.new(name: 'givenName', type: 'string'
|
9
|
+
Attribute.new(name: 'familyName', type: 'string'),
|
10
|
+
Attribute.new(name: 'givenName', type: 'string'),
|
11
11
|
Attribute.new(name: 'middleName', type: 'string'),
|
12
12
|
Attribute.new(name: 'formatted', type: 'string'),
|
13
13
|
Attribute.new(name: 'honorificPrefix', type: 'string'),
|
@@ -20,7 +20,7 @@ module Scimitar
|
|
20
20
|
[
|
21
21
|
Attribute.new(name: 'userName', type: 'string', uniqueness: 'server', required: true),
|
22
22
|
|
23
|
-
Attribute.new(name: 'name',
|
23
|
+
Attribute.new(name: 'name', complexType: Scimitar::ComplexTypes::Name),
|
24
24
|
|
25
25
|
Attribute.new(name: 'displayName', type: 'string'),
|
26
26
|
Attribute.new(name: 'nickName', type: 'string'),
|
@@ -35,15 +35,15 @@ module Scimitar
|
|
35
35
|
|
36
36
|
Attribute.new(name: 'password', type: 'string', mutability: 'writeOnly', returned: 'never'),
|
37
37
|
|
38
|
-
Attribute.new(name: 'emails',
|
39
|
-
Attribute.new(name: 'phoneNumbers',
|
40
|
-
Attribute.new(name: 'ims',
|
41
|
-
Attribute.new(name: 'photos',
|
42
|
-
Attribute.new(name: 'addresses',
|
43
|
-
Attribute.new(name: 'groups',
|
44
|
-
Attribute.new(name: 'entitlements',
|
45
|
-
Attribute.new(name: 'roles',
|
46
|
-
Attribute.new(name: 'x509Certificates',
|
38
|
+
Attribute.new(name: 'emails', multiValued: true, complexType: Scimitar::ComplexTypes::Email),
|
39
|
+
Attribute.new(name: 'phoneNumbers', multiValued: true, complexType: Scimitar::ComplexTypes::PhoneNumber),
|
40
|
+
Attribute.new(name: 'ims', multiValued: true, complexType: Scimitar::ComplexTypes::Ims),
|
41
|
+
Attribute.new(name: 'photos', multiValued: true, complexType: Scimitar::ComplexTypes::Photo),
|
42
|
+
Attribute.new(name: 'addresses', multiValued: true, complexType: Scimitar::ComplexTypes::Address),
|
43
|
+
Attribute.new(name: 'groups', multiValued: true, complexType: Scimitar::ComplexTypes::ReferenceGroup, mutability: 'readOnly'),
|
44
|
+
Attribute.new(name: 'entitlements', multiValued: true, complexType: Scimitar::ComplexTypes::Entitlement),
|
45
|
+
Attribute.new(name: 'roles', multiValued: true, complexType: Scimitar::ComplexTypes::Role),
|
46
|
+
Attribute.new(name: 'x509Certificates', multiValued: true, complexType: Scimitar::ComplexTypes::X509Certificate),
|
47
47
|
]
|
48
48
|
end
|
49
49
|
|
@@ -106,6 +106,47 @@ Rails.application.config.to_prepare do # (required for >= Rails 7 / Zeitwerk)
|
|
106
106
|
# whatever that means for you receiving system in your model code.
|
107
107
|
#
|
108
108
|
# optional_value_fields_required: false
|
109
|
+
|
110
|
+
# The SCIM standard `/Schemas` endpoint lists, by default, all known schema
|
111
|
+
# definitions with the mutabilty (read-write, read-only, write-only) state
|
112
|
+
# described by those definitions, and includes all defined attributes. For
|
113
|
+
# user-defined schema, this will typically exactly match your underlying
|
114
|
+
# mapped attribute and model capability - it wouldn't make sense to define
|
115
|
+
# your own schema that misrepresented the implementation! For core SCIM RFC
|
116
|
+
# schema, though, you might want to only list actually mapped attributes.
|
117
|
+
# Further, if you happen to have a non-compliant implementation especially
|
118
|
+
# in relation to mutability of some attributes, you may want to report that
|
119
|
+
# accurately in the '/Schemas' list, for auto-discovery purposes. To switch
|
120
|
+
# to a significantly slower but more accurate render method for the list,
|
121
|
+
# driven by your resource subclasses and their attribute maps, set:
|
122
|
+
#
|
123
|
+
# schema_list_from_attribute_mappings: [...array...]
|
124
|
+
#
|
125
|
+
# ...where you provide an Array of *models*, your classes that include the
|
126
|
+
# Scimitar::Resources::Mixin module and, therefore, define an attribute map
|
127
|
+
# translating SCIM schema attributes into actual implemented data. These
|
128
|
+
# must *uniquely* describe, via the Scimitar resources they each declare in
|
129
|
+
# their Scimitar::Resources::Mixin::scim_resource_type implementation, the
|
130
|
+
# set of schemas and extended schemas you want to render. Should resources
|
131
|
+
# share schema, the '/Schemas' endpoint will fail since it cannot determine
|
132
|
+
# which model attribute map it should use and it needs the map in order to
|
133
|
+
# resolve the differences (if any) between what the schema might say, and
|
134
|
+
# what the actual underlying model supports.
|
135
|
+
#
|
136
|
+
# It is further _very_ _strongly_ _recommended_ that, for any
|
137
|
+
# +scim_attributes_map+ containing a collection which has "list:" key (for
|
138
|
+
# an associative array of zero or more entities; the Groups to which a User
|
139
|
+
# might belong is a good example) then you should also specify the "class:"
|
140
|
+
# key, giving the class used for objects in that associated collection. The
|
141
|
+
# class *must* include Scimitar::Resources::Mixin, since its own attribute
|
142
|
+
# map is consulted in order to render the part of the schema describing
|
143
|
+
# those associated properties in the owning resource. If you don't do this,
|
144
|
+
# and if you're using ActiveRecord, then Scimitar attempts association
|
145
|
+
# reflection to determine the collection class - but that's more fragile
|
146
|
+
# than just being told the exact class in the attribute map. No matter how
|
147
|
+
# this class is determined, though, it must be possible to create a simple
|
148
|
+
# instance with +new+ and no parameters, since that's needed in order to
|
149
|
+
# call Scimitar::Resources::Mixin#scim_mutable_attributes.
|
109
150
|
})
|
110
151
|
|
111
152
|
end
|
data/lib/scimitar/engine.rb
CHANGED
@@ -1,15 +1,38 @@
|
|
1
|
+
require 'rails/engine'
|
2
|
+
|
1
3
|
module Scimitar
|
2
4
|
class Engine < ::Rails::Engine
|
3
5
|
isolate_namespace Scimitar
|
4
6
|
|
7
|
+
config.autoload_once_paths = %W(
|
8
|
+
#{root}/app/controllers
|
9
|
+
#{root}/app/models
|
10
|
+
)
|
11
|
+
|
5
12
|
Mime::Type.register 'application/scim+json', :scim
|
6
13
|
|
7
14
|
ActionDispatch::Request.parameter_parsers[Mime::Type.lookup('application/scim+json').symbol] = lambda do |body|
|
8
15
|
JSON.parse(body)
|
9
16
|
end
|
10
17
|
|
18
|
+
# Return an Array of all supported default and custom resource classes.
|
19
|
+
# See also :add_custom_resource and :set_default_resources.
|
20
|
+
#
|
11
21
|
def self.resources
|
12
|
-
default_resources + custom_resources
|
22
|
+
self.default_resources() + self.custom_resources()
|
23
|
+
end
|
24
|
+
|
25
|
+
# Returns a flat array of instances of all resource schema included in the
|
26
|
+
# resource classes returned by ::resources.
|
27
|
+
#
|
28
|
+
def self.schemas
|
29
|
+
self.resources().map(&:schemas).flatten.uniq.map(&:new)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns the list of custom resources, if any.
|
33
|
+
#
|
34
|
+
def self.custom_resources
|
35
|
+
@custom_resources ||= []
|
13
36
|
end
|
14
37
|
|
15
38
|
# Can be used to add a new resource type which is not provided by the gem.
|
@@ -30,7 +53,7 @@ module Scimitar
|
|
30
53
|
# Scimitar::Engine.add_custom_resource Scim::Resources::ShinyResource
|
31
54
|
#
|
32
55
|
def self.add_custom_resource(resource)
|
33
|
-
custom_resources << resource
|
56
|
+
self.custom_resources() << resource
|
34
57
|
end
|
35
58
|
|
36
59
|
# Resets the resource list to default. This is really only intended for use
|
@@ -40,23 +63,45 @@ module Scimitar
|
|
40
63
|
@custom_resources = []
|
41
64
|
end
|
42
65
|
|
43
|
-
# Returns the
|
44
|
-
#
|
45
|
-
def self.custom_resources
|
46
|
-
@custom_resources ||= []
|
47
|
-
end
|
48
|
-
|
49
|
-
# Returns the default resources added in this gem:
|
66
|
+
# Returns the default resources added in this gem - by default, these are:
|
50
67
|
#
|
51
68
|
# * Scimitar::Resources::User
|
52
69
|
# * Scimitar::Resources::Group
|
53
70
|
#
|
71
|
+
# ...but if an implementation does not e.g. support Group, it can
|
72
|
+
# be overridden via ::set_default_resources to help with service
|
73
|
+
# auto-discovery.
|
74
|
+
#
|
54
75
|
def self.default_resources
|
55
|
-
[ Resources::User, Resources::Group ]
|
76
|
+
@standard_default_resources = [ Resources::User, Resources::Group ]
|
77
|
+
@default_resources ||= @standard_default_resources.dup()
|
56
78
|
end
|
57
79
|
|
58
|
-
|
59
|
-
|
80
|
+
# Override the resources returned by ::default_resources.
|
81
|
+
#
|
82
|
+
# +resource_array+:: An Array containing one or both of
|
83
|
+
# Scimitar::Resources::User and/or
|
84
|
+
# Scimitar::Resources::Group, and nothing else.
|
85
|
+
#
|
86
|
+
def self.set_default_resources(resource_array)
|
87
|
+
self.default_resources()
|
88
|
+
unrecognised_resources = resource_array - @standard_default_resources
|
89
|
+
|
90
|
+
if unrecognised_resources.any?
|
91
|
+
raise "Scimitar::Engine::set_default_resources: Only #{@standard_default_resources.map(&:name).join(', ')} are supported"
|
92
|
+
elsif resource_array.empty?
|
93
|
+
raise 'Scimitar::Engine::set_default_resources: At least one resource must be given'
|
94
|
+
end
|
95
|
+
|
96
|
+
@default_resources = resource_array
|
97
|
+
end
|
98
|
+
|
99
|
+
# Resets the default resource list. This is really only intended for use
|
100
|
+
# during testing, to avoid one test polluting another.
|
101
|
+
#
|
102
|
+
def self.reset_default_resources
|
103
|
+
self.default_resources()
|
104
|
+
@default_resources = @standard_default_resources
|
60
105
|
end
|
61
106
|
|
62
107
|
end
|
@@ -23,8 +23,8 @@ class Hash
|
|
23
23
|
#
|
24
24
|
def self.deep_indifferent_case_insensitive_access(object)
|
25
25
|
if object.is_a?(Hash)
|
26
|
-
new_hash = Scimitar::Support::HashWithIndifferentCaseInsensitiveAccess.new
|
27
|
-
|
26
|
+
new_hash = Scimitar::Support::HashWithIndifferentCaseInsensitiveAccess.new
|
27
|
+
object.each do | key, value |
|
28
28
|
new_hash[key] = deep_indifferent_case_insensitive_access(value)
|
29
29
|
end
|
30
30
|
new_hash
|
@@ -49,34 +49,164 @@ module Scimitar
|
|
49
49
|
# in a case-insensitive fashion too.
|
50
50
|
#
|
51
51
|
# During enumeration, Hash keys will always be returned in whatever case
|
52
|
-
# they were originally set.
|
52
|
+
# they were originally set. Just as with
|
53
|
+
# ActiveSupport::HashWithIndifferentAccess, though, the type of the keys is
|
54
|
+
# always returned as a String, even if originally set as a Symbol - only
|
55
|
+
# the upper/lower case nature of the original key is preserved.
|
56
|
+
#
|
57
|
+
# If a key is written more than once with the same effective meaning in a
|
58
|
+
# to-string, to-downcase form, then whatever case was used *first* wins;
|
59
|
+
# e.g. if you did hash['User'] = 23, then hash['USER'] = 42, the result
|
60
|
+
# would be {"User" => 42}.
|
61
|
+
#
|
62
|
+
# It's important to remember that Hash#merge is shallow and replaces values
|
63
|
+
# found at existing keys in the target ("this") hash with values in the
|
64
|
+
# inbound Hash. If that new value that is itself a Hash, this *replaces*
|
65
|
+
# the value. For example:
|
66
|
+
#
|
67
|
+
# * Original: <tt>'Foo' => { 'Bar' => 42 }</tt>
|
68
|
+
# * Merge: <tt>'FOO' => { 'BAR' => 24 }</tt>
|
69
|
+
#
|
70
|
+
# ...results in "this" target hash's key +Foo+ being addressed in the merge
|
71
|
+
# by inbound key +FOO+, so the case doesn't change. But the value for +Foo+
|
72
|
+
# is _replaced_ by the merging-in Hash completely:
|
73
|
+
#
|
74
|
+
# * Result: <tt>'Foo' => { 'BAR' => 24 }</tt>
|
75
|
+
#
|
76
|
+
# ...and of course we might've replaced with a totally different type, such
|
77
|
+
# as +true+:
|
78
|
+
#
|
79
|
+
# * Original: <tt>'Foo' => { 'Bar' => 42 }</tt>
|
80
|
+
# * Merge: <tt>'FOO' => true</tt>
|
81
|
+
# * Result: <tt>'Foo' => true</tt>
|
82
|
+
#
|
83
|
+
# If you're intending to merge nested Hashes, then use ActiveSupport's
|
84
|
+
# #deep_merge or an equivalent. This will have the expected outcome, where
|
85
|
+
# the hash with 'BAR' is _merged_ into the existing value and, therefore,
|
86
|
+
# the original 'Bar' key case is preserved:
|
87
|
+
#
|
88
|
+
# * Original: <tt>'Foo' => { 'Bar' => 42 }</tt>
|
89
|
+
# * Deep merge: <tt>'FOO' => { 'BAR' => 24 }</tt>
|
90
|
+
# * Result: <tt>'Foo' => { 'Bar' => 24 }</tt>
|
53
91
|
#
|
54
92
|
class HashWithIndifferentCaseInsensitiveAccess < ActiveSupport::HashWithIndifferentAccess
|
55
93
|
def with_indifferent_case_insensitive_access
|
56
94
|
self
|
57
95
|
end
|
58
96
|
|
97
|
+
def initialize(constructor = nil)
|
98
|
+
@scimitar_hash_with_indifferent_case_insensitive_access_key_map = {}
|
99
|
+
super
|
100
|
+
end
|
101
|
+
|
102
|
+
# It's vital that the attribute map is carried over when one of these
|
103
|
+
# objects is duplicated. Duplication of this ivar state does *not* happen
|
104
|
+
# when 'dup' is called on our superclass, so we have to do that manually.
|
105
|
+
#
|
106
|
+
def dup
|
107
|
+
duplicate = super
|
108
|
+
duplicate.instance_variable_set(
|
109
|
+
'@scimitar_hash_with_indifferent_case_insensitive_access_key_map',
|
110
|
+
@scimitar_hash_with_indifferent_case_insensitive_access_key_map
|
111
|
+
)
|
112
|
+
|
113
|
+
return duplicate
|
114
|
+
end
|
115
|
+
|
116
|
+
# Override the individual key writer.
|
117
|
+
#
|
118
|
+
def []=(key, value)
|
119
|
+
string_key = scimitar_hash_with_indifferent_case_insensitive_access_string(key)
|
120
|
+
indifferent_key = scimitar_hash_with_indifferent_case_insensitive_access_downcase(string_key)
|
121
|
+
converted_value = convert_value(value, conversion: :assignment)
|
122
|
+
|
123
|
+
# Note '||=', as there might have been a prior use of the "same" key in
|
124
|
+
# a different case. The earliest one is preserved since the actual Hash
|
125
|
+
# underneath all this is already using that variant of the key.
|
126
|
+
#
|
127
|
+
key_for_writing = (
|
128
|
+
@scimitar_hash_with_indifferent_case_insensitive_access_key_map[indifferent_key] ||= string_key
|
129
|
+
)
|
130
|
+
|
131
|
+
regular_writer(key_for_writing, converted_value)
|
132
|
+
end
|
133
|
+
|
134
|
+
# Override #merge to express it in terms of #merge! (also overridden), so
|
135
|
+
# that merged hashes can have their keys treated indifferently too.
|
136
|
+
#
|
137
|
+
def merge(*other_hashes, &block)
|
138
|
+
dup.merge!(*other_hashes, &block)
|
139
|
+
end
|
140
|
+
|
141
|
+
# Modifies-self version of #merge, overriding Hash#merge!.
|
142
|
+
#
|
143
|
+
def merge!(*hashes_to_merge_to_self, &block)
|
144
|
+
if block_given?
|
145
|
+
hashes_to_merge_to_self.each do |hash_to_merge_to_self|
|
146
|
+
hash_to_merge_to_self.each_pair do |key, value|
|
147
|
+
value = block.call(key, self[key], value) if self.key?(key)
|
148
|
+
self[key] = value
|
149
|
+
end
|
150
|
+
end
|
151
|
+
else
|
152
|
+
hashes_to_merge_to_self.each do |hash_to_merge_to_self|
|
153
|
+
hash_to_merge_to_self.each_pair do |key, value|
|
154
|
+
self[key] = value
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
self
|
160
|
+
end
|
161
|
+
|
162
|
+
# =======================================================================
|
163
|
+
# PRIVATE INSTANCE METHODS
|
164
|
+
# =======================================================================
|
165
|
+
#
|
59
166
|
private
|
60
167
|
|
61
168
|
if Symbol.method_defined?(:name)
|
62
|
-
def
|
63
|
-
key.kind_of?(Symbol) ? key.name
|
169
|
+
def scimitar_hash_with_indifferent_case_insensitive_access_string(key)
|
170
|
+
key.kind_of?(Symbol) ? key.name : key
|
64
171
|
end
|
65
172
|
else
|
66
|
-
def
|
67
|
-
key.kind_of?(Symbol) ? key.to_s
|
173
|
+
def scimitar_hash_with_indifferent_case_insensitive_access_string(key)
|
174
|
+
key.kind_of?(Symbol) ? key.to_s : key
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def scimitar_hash_with_indifferent_case_insensitive_access_downcase(key)
|
179
|
+
key.kind_of?(String) ? key.downcase : key
|
180
|
+
end
|
181
|
+
|
182
|
+
def convert_key(key)
|
183
|
+
string_key = scimitar_hash_with_indifferent_case_insensitive_access_string(key)
|
184
|
+
indifferent_key = scimitar_hash_with_indifferent_case_insensitive_access_downcase(string_key)
|
185
|
+
|
186
|
+
@scimitar_hash_with_indifferent_case_insensitive_access_key_map[indifferent_key] || string_key
|
187
|
+
end
|
188
|
+
|
189
|
+
def convert_value(value, conversion: nil)
|
190
|
+
if value.is_a?(Hash)
|
191
|
+
if conversion == :to_hash
|
192
|
+
value.to_hash
|
193
|
+
else
|
194
|
+
value.with_indifferent_case_insensitive_access
|
195
|
+
end
|
196
|
+
else
|
197
|
+
super
|
68
198
|
end
|
69
199
|
end
|
70
200
|
|
71
201
|
def update_with_single_argument(other_hash, block)
|
72
|
-
if other_hash.is_a?
|
202
|
+
if other_hash.is_a?(HashWithIndifferentCaseInsensitiveAccess)
|
73
203
|
regular_update(other_hash, &block)
|
74
204
|
else
|
75
205
|
other_hash.to_hash.each_pair do |key, value|
|
76
206
|
if block && key?(key)
|
77
|
-
value = block.call(convert_key(key), self[key], value)
|
207
|
+
value = block.call(self.convert_key(key), self[key], value)
|
78
208
|
end
|
79
|
-
|
209
|
+
self.[]=(key, value)
|
80
210
|
end
|
81
211
|
end
|
82
212
|
end
|
@@ -46,6 +46,66 @@ module Scimitar
|
|
46
46
|
hash[array.shift()] = self.dot_path(array, value)
|
47
47
|
end
|
48
48
|
end
|
49
|
+
|
50
|
+
# Schema ID-aware splitter handling ":" or "." separators. Adapted from
|
51
|
+
# contribution by @bettysteger and @MorrisFreeman in:
|
52
|
+
#
|
53
|
+
# https://github.com/RIPAGlobal/scimitar/issues/48
|
54
|
+
# https://github.com/RIPAGlobal/scimitar/pull/49
|
55
|
+
#
|
56
|
+
# +schemas:: Array of extension schemas, e.g. a SCIM resource class'
|
57
|
+
# <tt>scim_resource_type.extended_schemas</tt> value. The
|
58
|
+
# Array should be empty if there are no extensions.
|
59
|
+
#
|
60
|
+
# +path_str+:: Path String, e.g. <tt>"password"</tt>, <tt>"name.givenName"</tt>,
|
61
|
+
# <tt>"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"</tt> (special case),
|
62
|
+
# <tt>"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:organization"</tt>
|
63
|
+
# (if given a Symbol, it'll be converted to a String).
|
64
|
+
#
|
65
|
+
# Returns an array of components, e.g. <tt>["password"]</tt>, <tt>["name",
|
66
|
+
# "givenName"]</tt>,
|
67
|
+
# <tt>["urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"]</tt> (special case),
|
68
|
+
# <tt>["urn:ietf:params:scim:schemas:extension:enterprise:2.0:User", "organization"]</tt>.
|
69
|
+
#
|
70
|
+
# The called-out special case is for a schema ID without any appended
|
71
|
+
# path components, which is returned as a single element ID to aid in
|
72
|
+
# traversal particularly of things like PATCH requests. There, a "value"
|
73
|
+
# attribute might have a key string that's simply a schema ID, with an
|
74
|
+
# object beneath that's got attribute-name pairs, possibly nested, in a
|
75
|
+
# path-free payload.
|
76
|
+
#
|
77
|
+
def self.path_str_to_array(schemas, path_str)
|
78
|
+
path_str = path_str.to_s
|
79
|
+
components = []
|
80
|
+
|
81
|
+
# Note the ":" separating the schema ID (URN) from the attribute.
|
82
|
+
# The nature of JSON rendering / other payloads might lead you to
|
83
|
+
# expect a "." as with any complex types, but that's not the case;
|
84
|
+
# see https://tools.ietf.org/html/rfc7644#section-3.10, or
|
85
|
+
# https://tools.ietf.org/html/rfc7644#section-3.5.2 of which in
|
86
|
+
# particular, https://tools.ietf.org/html/rfc7644#page-35.
|
87
|
+
#
|
88
|
+
if path_str.include?(':')
|
89
|
+
lower_case_path_str = path_str.downcase()
|
90
|
+
|
91
|
+
schemas.each do |schema|
|
92
|
+
lower_case_schema_id = schema.id.downcase()
|
93
|
+
attributes_after_schema_id = lower_case_path_str.split(lower_case_schema_id + ':').drop(1)
|
94
|
+
|
95
|
+
if attributes_after_schema_id.empty?
|
96
|
+
components += [schema.id] if lower_case_path_str == lower_case_schema_id
|
97
|
+
else
|
98
|
+
attributes_after_schema_id.each do |component|
|
99
|
+
components += [schema.id] + component.split('.')
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
components = path_str.split('.') if components.empty?
|
106
|
+
return components
|
107
|
+
end
|
108
|
+
|
49
109
|
end
|
50
110
|
end
|
51
111
|
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.
|
6
|
+
VERSION = '1.10.0'
|
7
7
|
|
8
8
|
# Date for VERSION. If this changes, be sure to re-run "bundle install"
|
9
9
|
# or "bundle update".
|
10
10
|
#
|
11
|
-
DATE = '2024-
|
11
|
+
DATE = '2024-06-27'
|
12
12
|
|
13
13
|
end
|
@@ -18,6 +18,7 @@ class MockUser < ActiveRecord::Base
|
|
18
18
|
work_phone_number
|
19
19
|
organization
|
20
20
|
department
|
21
|
+
manager
|
21
22
|
mock_groups
|
22
23
|
}
|
23
24
|
|
@@ -48,6 +49,7 @@ class MockUser < ActiveRecord::Base
|
|
48
49
|
externalId: :scim_uid,
|
49
50
|
userName: :username,
|
50
51
|
password: :password,
|
52
|
+
active: :is_active,
|
51
53
|
name: {
|
52
54
|
givenName: :first_name,
|
53
55
|
familyName: :last_name
|
@@ -80,8 +82,11 @@ class MockUser < ActiveRecord::Base
|
|
80
82
|
}
|
81
83
|
},
|
82
84
|
],
|
83
|
-
groups: [
|
85
|
+
groups: [
|
84
86
|
{
|
87
|
+
# Read-only, so no :find_with key. There's no 'class' specified here
|
88
|
+
# either, to help test the "/Schemas" endpoint's reflection code.
|
89
|
+
#
|
85
90
|
list: :mock_groups,
|
86
91
|
using: {
|
87
92
|
value: :id,
|
@@ -89,13 +94,16 @@ class MockUser < ActiveRecord::Base
|
|
89
94
|
}
|
90
95
|
}
|
91
96
|
],
|
92
|
-
active: :is_active,
|
93
97
|
|
94
98
|
# Custom extension schema - see configuration in
|
95
99
|
# "spec/apps/dummy/config/initializers/scimitar.rb".
|
96
100
|
#
|
97
101
|
organization: :organization,
|
98
102
|
department: :department,
|
103
|
+
primaryEmail: :scim_primary_email,
|
104
|
+
|
105
|
+
manager: :manager,
|
106
|
+
|
99
107
|
userGroups: [
|
100
108
|
{
|
101
109
|
list: :mock_groups,
|
@@ -124,9 +132,16 @@ class MockUser < ActiveRecord::Base
|
|
124
132
|
'groups.value' => { column: MockGroup.arel_table[:id] },
|
125
133
|
'emails' => { columns: [ :work_email_address, :home_email_address ] },
|
126
134
|
'emails.value' => { columns: [ :work_email_address, :home_email_address ] },
|
127
|
-
'emails.type' => { ignore: true } # We can't filter on that; it'll just search all e-mails
|
135
|
+
'emails.type' => { ignore: true }, # We can't filter on that; it'll just search all e-mails
|
136
|
+
'primaryEmail' => { column: :scim_primary_email },
|
128
137
|
}
|
129
138
|
end
|
130
139
|
|
140
|
+
# Custom attribute reader
|
141
|
+
#
|
142
|
+
def scim_primary_email
|
143
|
+
work_email_address
|
144
|
+
end
|
145
|
+
|
131
146
|
include Scimitar::Resources::Mixin
|
132
147
|
end
|
@@ -40,10 +40,13 @@ Scimitar.engine_configuration = Scimitar::EngineConfiguration.new({
|
|
40
40
|
|
41
41
|
module ScimSchemaExtensions
|
42
42
|
module User
|
43
|
+
|
44
|
+
# This "looks like" part of the standard Enterprise extension.
|
45
|
+
#
|
43
46
|
class Enterprise < Scimitar::Schema::Base
|
44
47
|
def initialize(options = {})
|
45
48
|
super(
|
46
|
-
name: '
|
49
|
+
name: 'EnterpriseExtendedUser',
|
47
50
|
description: 'Enterprise extension for a User',
|
48
51
|
id: self.class.id,
|
49
52
|
scim_attributes: self.class.scim_attributes
|
@@ -57,7 +60,32 @@ module ScimSchemaExtensions
|
|
57
60
|
def self.scim_attributes
|
58
61
|
[
|
59
62
|
Scimitar::Schema::Attribute.new(name: 'organization', type: 'string'),
|
60
|
-
Scimitar::Schema::Attribute.new(name: 'department', type: 'string')
|
63
|
+
Scimitar::Schema::Attribute.new(name: 'department', type: 'string'),
|
64
|
+
Scimitar::Schema::Attribute.new(name: 'primaryEmail', type: 'string'),
|
65
|
+
]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# In https://github.com/RIPAGlobal/scimitar/issues/122 we learn that with
|
70
|
+
# more than one extension, things can go wrong - so now we test with two.
|
71
|
+
#
|
72
|
+
class Manager < Scimitar::Schema::Base
|
73
|
+
def initialize(options = {})
|
74
|
+
super(
|
75
|
+
name: 'ManagementExtendedUser',
|
76
|
+
description: 'Management extension for a User',
|
77
|
+
id: self.class.id,
|
78
|
+
scim_attributes: self.class.scim_attributes
|
79
|
+
)
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.id
|
83
|
+
'urn:ietf:params:scim:schemas:extension:manager:1.0:User'
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.scim_attributes
|
87
|
+
[
|
88
|
+
Scimitar::Schema::Attribute.new(name: 'manager', type: 'string')
|
61
89
|
]
|
62
90
|
end
|
63
91
|
end
|
@@ -65,3 +93,4 @@ module ScimSchemaExtensions
|
|
65
93
|
end
|
66
94
|
|
67
95
|
Scimitar::Resources::User.extend_schema ScimSchemaExtensions::User::Enterprise
|
96
|
+
Scimitar::Resources::User.extend_schema ScimSchemaExtensions::User::Manager
|