scimitar 1.8.2 → 1.10.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|