scimitar 1.8.2 → 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 +36 -5
- data/app/models/scimitar/resources/mixin.rb +133 -43
- 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/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 +11 -11
- data/spec/models/scimitar/resources/base_validation_spec.rb +16 -3
- data/spec/models/scimitar/resources/mixin_spec.rb +71 -10
- data/spec/models/scimitar/schema/user_spec.rb +2 -2
- data/spec/requests/active_record_backed_resources_controller_spec.rb +231 -0
- data/spec/requests/engine_spec.rb +75 -0
- data/spec/spec_helper.rb +1 -1
- metadata +22 -22
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
|
@@ -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
|