scimitar 2.5.0 → 2.11.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/LICENSE.txt +21 -0
- data/README.md +721 -0
- data/app/controllers/scimitar/active_record_backed_resources_controller.rb +72 -18
- data/app/controllers/scimitar/application_controller.rb +17 -9
- data/app/controllers/scimitar/resource_types_controller.rb +7 -3
- data/app/controllers/scimitar/resources_controller.rb +0 -2
- data/app/controllers/scimitar/schemas_controller.rb +366 -3
- data/app/controllers/scimitar/service_provider_configurations_controller.rb +3 -2
- data/app/models/scimitar/complex_types/address.rb +0 -6
- data/app/models/scimitar/complex_types/base.rb +2 -2
- data/app/models/scimitar/engine_configuration.rb +3 -1
- data/app/models/scimitar/lists/query_parser.rb +97 -12
- data/app/models/scimitar/resource_invalid_error.rb +1 -1
- data/app/models/scimitar/resource_type.rb +4 -6
- data/app/models/scimitar/resources/base.rb +52 -8
- data/app/models/scimitar/resources/mixin.rb +539 -76
- data/app/models/scimitar/schema/attribute.rb +18 -8
- data/app/models/scimitar/schema/base.rb +2 -2
- data/app/models/scimitar/schema/name.rb +2 -2
- data/app/models/scimitar/schema/user.rb +10 -10
- data/config/initializers/scimitar.rb +49 -3
- 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 +111 -0
- data/lib/scimitar/version.rb +2 -2
- data/lib/scimitar.rb +1 -0
- data/spec/apps/dummy/app/controllers/custom_create_mock_users_controller.rb +25 -0
- data/spec/apps/dummy/app/controllers/custom_replace_mock_users_controller.rb +25 -0
- data/spec/apps/dummy/app/controllers/custom_save_mock_users_controller.rb +24 -0
- data/spec/apps/dummy/app/controllers/custom_update_mock_users_controller.rb +25 -0
- data/spec/apps/dummy/app/models/mock_user.rb +20 -3
- data/spec/apps/dummy/config/application.rb +8 -0
- data/spec/apps/dummy/config/initializers/scimitar.rb +40 -3
- data/spec/apps/dummy/config/routes.rb +18 -1
- data/spec/apps/dummy/db/migrate/20210304014602_create_mock_users.rb +2 -0
- data/spec/apps/dummy/db/schema.rb +3 -1
- data/spec/controllers/scimitar/application_controller_spec.rb +56 -2
- data/spec/controllers/scimitar/resource_types_controller_spec.rb +8 -4
- data/spec/controllers/scimitar/schemas_controller_spec.rb +344 -48
- data/spec/controllers/scimitar/service_provider_configurations_controller_spec.rb +1 -0
- data/spec/models/scimitar/complex_types/address_spec.rb +3 -4
- data/spec/models/scimitar/lists/query_parser_spec.rb +70 -0
- data/spec/models/scimitar/resources/base_spec.rb +55 -13
- data/spec/models/scimitar/resources/base_validation_spec.rb +16 -3
- data/spec/models/scimitar/resources/mixin_spec.rb +781 -124
- data/spec/models/scimitar/schema/attribute_spec.rb +22 -0
- data/spec/models/scimitar/schema/user_spec.rb +2 -2
- data/spec/requests/active_record_backed_resources_controller_spec.rb +723 -40
- data/spec/requests/engine_spec.rb +75 -0
- data/spec/spec_helper.rb +10 -2
- data/spec/support/hash_with_indifferent_case_insensitive_access_spec.rb +108 -0
- metadata +42 -34
@@ -1,5 +1,3 @@
|
|
1
|
-
require_dependency "scimitar/application_controller"
|
2
|
-
|
3
1
|
module Scimitar
|
4
2
|
|
5
3
|
# An ActiveRecord-centric subclass of Scimitar::ResourcesController. See that
|
@@ -19,7 +17,7 @@ module Scimitar
|
|
19
17
|
#
|
20
18
|
class ActiveRecordBackedResourcesController < ResourcesController
|
21
19
|
|
22
|
-
rescue_from ActiveRecord::RecordNotFound, with: :handle_resource_not_found # See Scimitar::ApplicationController
|
20
|
+
rescue_from 'ActiveRecord::RecordNotFound', with: :handle_resource_not_found # See Scimitar::ApplicationController
|
23
21
|
|
24
22
|
before_action :obtain_id_column_name_from_attribute_map
|
25
23
|
|
@@ -60,12 +58,20 @@ module Scimitar
|
|
60
58
|
|
61
59
|
# POST (create)
|
62
60
|
#
|
63
|
-
|
61
|
+
# Calls #save! on the new record if no block is given, else invokes the
|
62
|
+
# block, passing it the new ActiveRecord model instance to be saved. It
|
63
|
+
# is up to the block to make any further changes and persist the record.
|
64
|
+
#
|
65
|
+
# Blocks are invoked from within a wrapping database transaction.
|
66
|
+
# ActiveRecord::RecordInvalid exceptions are handled for you, rendering
|
67
|
+
# an appropriate SCIM error.
|
68
|
+
#
|
69
|
+
def create(&block)
|
64
70
|
super do |scim_resource|
|
65
71
|
self.storage_class().transaction do
|
66
72
|
record = self.storage_class().new
|
67
73
|
record.from_scim!(scim_hash: scim_resource.as_json())
|
68
|
-
self.save!(record)
|
74
|
+
self.save!(record, &block)
|
69
75
|
record_to_scim(record)
|
70
76
|
end
|
71
77
|
end
|
@@ -73,12 +79,16 @@ module Scimitar
|
|
73
79
|
|
74
80
|
# PUT (replace)
|
75
81
|
#
|
76
|
-
|
82
|
+
# Calls #save! on the updated record if no block is given, else invokes the
|
83
|
+
# block, passing the updated record which the block must persist, with the
|
84
|
+
# same rules as for #create.
|
85
|
+
#
|
86
|
+
def replace(&block)
|
77
87
|
super do |record_id, scim_resource|
|
78
88
|
self.storage_class().transaction do
|
79
89
|
record = self.find_record(record_id)
|
80
90
|
record.from_scim!(scim_hash: scim_resource.as_json())
|
81
|
-
self.save!(record)
|
91
|
+
self.save!(record, &block)
|
82
92
|
record_to_scim(record)
|
83
93
|
end
|
84
94
|
end
|
@@ -86,12 +96,16 @@ module Scimitar
|
|
86
96
|
|
87
97
|
# PATCH (update)
|
88
98
|
#
|
89
|
-
|
99
|
+
# Calls #save! on the updated record if no block is given, else invokes the
|
100
|
+
# block, passing the updated record which the block must persist, with the
|
101
|
+
# same rules as for #create.
|
102
|
+
#
|
103
|
+
def update(&block)
|
90
104
|
super do |record_id, patch_hash|
|
91
105
|
self.storage_class().transaction do
|
92
106
|
record = self.find_record(record_id)
|
93
107
|
record.from_scim_patch!(patch_hash: patch_hash)
|
94
|
-
self.save!(record)
|
108
|
+
self.save!(record, &block)
|
95
109
|
record_to_scim(record)
|
96
110
|
end
|
97
111
|
end
|
@@ -134,6 +148,17 @@ module Scimitar
|
|
134
148
|
raise NotImplementedError
|
135
149
|
end
|
136
150
|
|
151
|
+
# Return an Array of exceptions that #save! can rescue and handle with a
|
152
|
+
# SCIM error automatically.
|
153
|
+
#
|
154
|
+
def scimitar_rescuable_exceptions
|
155
|
+
[
|
156
|
+
ActiveRecord::RecordInvalid,
|
157
|
+
ActiveRecord::RecordNotSaved,
|
158
|
+
ActiveRecord::RecordNotUnique,
|
159
|
+
]
|
160
|
+
end
|
161
|
+
|
137
162
|
# Find a record by ID. Subclasses can override this if they need special
|
138
163
|
# lookup behaviour.
|
139
164
|
#
|
@@ -147,13 +172,22 @@ module Scimitar
|
|
147
172
|
# representation, with a "show" location specified via #url_for.
|
148
173
|
#
|
149
174
|
def record_to_scim(record)
|
150
|
-
record.to_scim(
|
175
|
+
record.to_scim(
|
176
|
+
location: url_for(action: :show, id: record.send(@id_column)),
|
177
|
+
include_attributes: params.fetch(:attributes, "").split(",")
|
178
|
+
)
|
151
179
|
end
|
152
180
|
|
153
181
|
# Save a record, dealing with validation exceptions by raising SCIM
|
154
182
|
# errors.
|
155
183
|
#
|
156
|
-
# +record+:: ActiveRecord subclass to save
|
184
|
+
# +record+:: ActiveRecord subclass to save.
|
185
|
+
#
|
186
|
+
# If you just let this superclass handle things, it'll call the standard
|
187
|
+
# +#save!+ method on the record. If you pass a block, then this block is
|
188
|
+
# invoked and passed the ActiveRecord model instance to be saved. You can
|
189
|
+
# then do things like calling a different method, using a service object
|
190
|
+
# of some kind, perform audit-related operations and so-on.
|
157
191
|
#
|
158
192
|
# The return value is not used internally, making life easier for
|
159
193
|
# overriding subclasses to "do the right thing" / avoid mistakes (instead
|
@@ -161,11 +195,31 @@ module Scimitar
|
|
161
195
|
# and relying upon this to generate correct response payloads - an early
|
162
196
|
# version of the gem did this and it caused a confusing subclass bug).
|
163
197
|
#
|
164
|
-
def save!(record)
|
165
|
-
|
198
|
+
def save!(record, &block)
|
199
|
+
if block_given?
|
200
|
+
yield(record)
|
201
|
+
else
|
202
|
+
record.save!
|
203
|
+
end
|
204
|
+
rescue *self.scimitar_rescuable_exceptions() => exception
|
205
|
+
handle_on_save_exception(record, exception)
|
206
|
+
end
|
166
207
|
|
167
|
-
|
168
|
-
|
208
|
+
# Deal with exceptions related to errors upon saving, by responding with
|
209
|
+
# an appropriate SCIM error. This is most effective if the record has
|
210
|
+
# validation errors defined, but falls back to the provided exception's
|
211
|
+
# message otherwise.
|
212
|
+
#
|
213
|
+
# +record+:: The record that provoked the exception. Mandatory.
|
214
|
+
# +exception+:: The exception that was raised. If omitted, a default of
|
215
|
+
# 'Unknown', in English with no I18n, is used.
|
216
|
+
#
|
217
|
+
def handle_on_save_exception(record, exception = RuntimeError.new('Unknown'))
|
218
|
+
details = if record.errors.present?
|
219
|
+
record.errors.full_messages.join('; ')
|
220
|
+
else
|
221
|
+
exception.message
|
222
|
+
end
|
169
223
|
|
170
224
|
# https://tools.ietf.org/html/rfc7644#page-12
|
171
225
|
#
|
@@ -175,14 +229,14 @@ module Scimitar
|
|
175
229
|
# status code 409 (Conflict) with a "scimType" error code of
|
176
230
|
# "uniqueness"
|
177
231
|
#
|
178
|
-
if record.errors.any? { | e | e.type == :taken }
|
232
|
+
if exception.is_a?(ActiveRecord::RecordNotUnique) || record.errors.any? { | e | e.type == :taken }
|
179
233
|
raise Scimitar::ErrorResponse.new(
|
180
234
|
status: 409,
|
181
235
|
scimType: 'uniqueness',
|
182
|
-
detail:
|
236
|
+
detail: "Operation failed due to a uniqueness constraint: #{details}"
|
183
237
|
)
|
184
238
|
else
|
185
|
-
raise Scimitar::ResourceInvalidError.new(
|
239
|
+
raise Scimitar::ResourceInvalidError.new(details)
|
186
240
|
end
|
187
241
|
end
|
188
242
|
|
@@ -9,10 +9,6 @@ module Scimitar
|
|
9
9
|
before_action :add_mandatory_response_headers
|
10
10
|
before_action :authenticate
|
11
11
|
|
12
|
-
if Scimitar.engine_configuration.application_controller_mixin
|
13
|
-
include Scimitar.engine_configuration.application_controller_mixin
|
14
|
-
end
|
15
|
-
|
16
12
|
# =========================================================================
|
17
13
|
# PROTECTED INSTANCE METHODS
|
18
14
|
# =========================================================================
|
@@ -47,7 +43,7 @@ module Scimitar
|
|
47
43
|
#
|
48
44
|
# *exception+:: If a Ruby exception was the reason this method is being
|
49
45
|
# called, pass it here. Any configured exception reporting
|
50
|
-
# mechanism will be
|
46
|
+
# mechanism will be invoked with the given parameter.
|
51
47
|
# Otherwise, the +error_response+ value is reported.
|
52
48
|
#
|
53
49
|
def handle_scim_error(error_response, exception = error_response)
|
@@ -124,8 +120,13 @@ module Scimitar
|
|
124
120
|
#
|
125
121
|
# https://stackoverflow.com/questions/10239970/what-is-the-delimiter-for-www-authenticate-for-multiple-schemes
|
126
122
|
#
|
127
|
-
response.set_header('
|
128
|
-
response.set_header('
|
123
|
+
response.set_header('WWW-Authenticate', 'Basic' ) if Scimitar.engine_configuration.basic_authenticator.present?
|
124
|
+
response.set_header('WWW-Authenticate', 'Bearer') if Scimitar.engine_configuration.token_authenticator.present?
|
125
|
+
|
126
|
+
# No matter what a caller might request via headers, the only content
|
127
|
+
# type we can ever respond with is JSON-for-SCIM.
|
128
|
+
#
|
129
|
+
response.set_header('Content-Type', "#{Mime::Type.lookup_by_extension(:scim)}; charset=utf-8")
|
129
130
|
end
|
130
131
|
|
131
132
|
def authenticate
|
@@ -134,15 +135,22 @@ module Scimitar
|
|
134
135
|
|
135
136
|
def authenticated?
|
136
137
|
result = if Scimitar.engine_configuration.basic_authenticator.present?
|
137
|
-
authenticate_with_http_basic
|
138
|
+
authenticate_with_http_basic do |username, password|
|
139
|
+
instance_exec(username, password, &Scimitar.engine_configuration.basic_authenticator)
|
140
|
+
end
|
138
141
|
end
|
139
142
|
|
140
143
|
result ||= if Scimitar.engine_configuration.token_authenticator.present?
|
141
|
-
authenticate_with_http_token
|
144
|
+
authenticate_with_http_token do |token, options|
|
145
|
+
instance_exec(token, options, &Scimitar.engine_configuration.token_authenticator)
|
146
|
+
end
|
142
147
|
end
|
143
148
|
|
144
149
|
return result
|
145
150
|
end
|
146
151
|
|
152
|
+
if Scimitar.engine_configuration.application_controller_mixin
|
153
|
+
include Scimitar.engine_configuration.application_controller_mixin
|
154
|
+
end
|
147
155
|
end
|
148
156
|
end
|
@@ -1,5 +1,3 @@
|
|
1
|
-
require_dependency "scimitar/application_controller"
|
2
|
-
|
3
1
|
module Scimitar
|
4
2
|
class ResourceTypesController < ApplicationController
|
5
3
|
def index
|
@@ -7,7 +5,13 @@ module Scimitar
|
|
7
5
|
resource.resource_type(scim_resource_type_url(name: resource.resource_type_id))
|
8
6
|
end
|
9
7
|
|
10
|
-
render json:
|
8
|
+
render json: {
|
9
|
+
schemas: [
|
10
|
+
'urn:ietf:params:scim:api:messages:2.0:ListResponse'
|
11
|
+
],
|
12
|
+
totalResults: resource_types.size,
|
13
|
+
Resources: resource_types
|
14
|
+
}
|
11
15
|
end
|
12
16
|
|
13
17
|
def show
|
@@ -1,16 +1,379 @@
|
|
1
|
-
require_dependency "scimitar/application_controller"
|
2
|
-
|
3
1
|
module Scimitar
|
4
2
|
class SchemasController < ApplicationController
|
5
3
|
def index
|
6
4
|
schemas = Scimitar::Engine.schemas
|
5
|
+
|
6
|
+
schemas.each do |schema|
|
7
|
+
schema.meta.location = scim_schemas_url(name: schema.id)
|
8
|
+
end
|
9
|
+
|
7
10
|
schemas_by_id = schemas.reduce({}) do |hash, schema|
|
8
11
|
hash[schema.id] = schema
|
9
12
|
hash
|
10
13
|
end
|
11
14
|
|
12
|
-
|
15
|
+
list = if params.key?(:name)
|
16
|
+
[ schemas_by_id[params[:name]] ]
|
17
|
+
else
|
18
|
+
schemas
|
19
|
+
end
|
20
|
+
|
21
|
+
# Now we either have a simple render method, or a complex one.
|
22
|
+
#
|
23
|
+
schemas_to_render = if Scimitar.engine_configuration.schema_list_from_attribute_mappings.empty?
|
24
|
+
list
|
25
|
+
else
|
26
|
+
self.redraw_schema_list_using_mappings(list)
|
27
|
+
end
|
28
|
+
|
29
|
+
render(json: {
|
30
|
+
schemas: [
|
31
|
+
'urn:ietf:params:scim:api:messages:2.0:ListResponse'
|
32
|
+
],
|
33
|
+
totalResults: schemas_to_render.size,
|
34
|
+
startIndex: 1,
|
35
|
+
itemsPerPage: schemas_to_render.size,
|
36
|
+
Resources: schemas_to_render
|
37
|
+
})
|
13
38
|
end
|
14
39
|
|
40
|
+
# =========================================================================
|
41
|
+
# PRIVATE INSTANCE METHODS
|
42
|
+
# =========================================================================
|
43
|
+
#
|
44
|
+
private
|
45
|
+
|
46
|
+
# Given a list of schema *instances*, find all Scimitar::Resources::Mixin
|
47
|
+
# inclusions to obtain classes with a ::scim_resource_type implementation
|
48
|
+
# that is invoked to get at the associated Scimitar resource class; each
|
49
|
+
# resource class then describes the schema it uses; the list is filtered
|
50
|
+
# to include only those found schemas. Then, for each, use the discovered
|
51
|
+
# class' attribute maps to walk the schema attribute tree alongside the
|
52
|
+
# map and render only mapped attributes. This is done via calling down to
|
53
|
+
# ::redraw_schema_using_mappings for each remaining schema in the list.
|
54
|
+
#
|
55
|
+
# An array of new schema data is returned.
|
56
|
+
#
|
57
|
+
# +list+:: Array of schema instances to examine.
|
58
|
+
#
|
59
|
+
def redraw_schema_list_using_mappings(list)
|
60
|
+
|
61
|
+
# Iterate over the configured model classes to build a mapping from
|
62
|
+
# Scimitar schema class to an array of one or more model classes that
|
63
|
+
# seem to use it. This is to detect the error condition wherein some
|
64
|
+
# schema gets used more than once, leading to multiple possible
|
65
|
+
# attribute map choices.
|
66
|
+
#
|
67
|
+
classes_using_scimitar_mixin = Scimitar.engine_configuration.schema_list_from_attribute_mappings
|
68
|
+
schema_to_resource_map = {}
|
69
|
+
|
70
|
+
classes_using_scimitar_mixin.each do | model_class |
|
71
|
+
resource_class = model_class.scim_resource_type()
|
72
|
+
schemas = resource_class.extended_schemas + [resource_class.schema]
|
73
|
+
|
74
|
+
schemas.each do | schema_class |
|
75
|
+
schema_to_resource_map[schema_class] ||= []
|
76
|
+
schema_to_resource_map[schema_class] << model_class
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Take the schema list and map to rewritten versions based on finding
|
81
|
+
# out which of the above resource classes use a given schema and then
|
82
|
+
# walking this schema's attribute tree while comparing against the
|
83
|
+
# resource class's attribute map. Unmapped attributes are removed. The
|
84
|
+
# reality of resource class attribute mutability might give different
|
85
|
+
# answers for the corresponding schema attribute's mutability; for any
|
86
|
+
# custom schema we'd expect a match, but for core schema where local
|
87
|
+
# resources don't quite work to spec, at least the /Schemas endpoint
|
88
|
+
# can try to reflect reality and aid auto-discovery.
|
89
|
+
#
|
90
|
+
redrawn_list = list.map do | schema_instance |
|
91
|
+
resource_classes_using_schema = schema_to_resource_map[schema_instance.class]
|
92
|
+
|
93
|
+
if resource_classes_using_schema.nil?
|
94
|
+
next # NOTE EARLY LOOP RESTART (schema not used by a resource)
|
95
|
+
elsif resource_classes_using_schema.size > 1
|
96
|
+
raise "Cannot infer attribute map for engine configuration 'schema_list_from_attribute_mappings: true' because multiple resource classes use schema '#{schema_instance.class.name}': #{resource_classes_using_schema.map(&:name).join(', ')}"
|
97
|
+
end
|
98
|
+
|
99
|
+
found_class = classes_using_scimitar_mixin.find do | class_using_scimitar_mixin |
|
100
|
+
resource_class = class_using_scimitar_mixin.scim_resource_type()
|
101
|
+
|
102
|
+
resource_class.schema == schema_instance.class ||
|
103
|
+
resource_class.extended_schemas.include?(schema_instance.class)
|
104
|
+
end
|
105
|
+
|
106
|
+
rebuilt_schema_instance = if found_class
|
107
|
+
redraw_schema_using_mappings(
|
108
|
+
original_schema_instance: schema_instance,
|
109
|
+
instance_including_mixin: found_class.new
|
110
|
+
)
|
111
|
+
else
|
112
|
+
nil
|
113
|
+
end
|
114
|
+
|
115
|
+
rebuilt_schema_instance
|
116
|
+
end
|
117
|
+
|
118
|
+
redrawn_list.compact!
|
119
|
+
redrawn_list
|
120
|
+
end
|
121
|
+
|
122
|
+
# "Redraw" a schema, by walking its attribute tree alongside a related
|
123
|
+
# resource class's attribute map. Only mapped attributes are included.
|
124
|
+
# The mapped model is checked for a read accessor and write ability is
|
125
|
+
# determined via Scimitar::Resources::Mixin#scim_mutable_attributes. This
|
126
|
+
# gives the actual read/write ability of the mapped attribute; if the
|
127
|
+
# schema's declared mutability differs, the *most restrictive* is chosen.
|
128
|
+
# For example, if the schema says read-write but the mapped model only
|
129
|
+
# has read ability, then "readOnly" is used. Conversely, if the schema
|
130
|
+
# says read-only but the mapped model has read-write, the schema's
|
131
|
+
# "readOnly" is chosen instead as the source of truth.
|
132
|
+
#
|
133
|
+
# See the implementation's comments for a table describing exactly how
|
134
|
+
# all mutability conflict cases are resolved.
|
135
|
+
#
|
136
|
+
# The returned schema instance may be a full or partial duplicate of the
|
137
|
+
# one given on input - some or all attributes and/or sub-attributes may
|
138
|
+
# have been duplicated due to e.g. mutability differences. Do not assume
|
139
|
+
# or rely upon this as a caller.
|
140
|
+
#
|
141
|
+
# Mandatory named parameters for external callers are:
|
142
|
+
#
|
143
|
+
# +original_schema_instance+:: The Scimitar::Schema::Base subclass
|
144
|
+
# schema *instance* that is to be examined
|
145
|
+
# and possibly "redrawn".
|
146
|
+
#
|
147
|
+
# +instance_including_mixin+:: Instance of the model class including
|
148
|
+
# Scimitar::Resources::Mixin, providing
|
149
|
+
# the attribute map to be examined.
|
150
|
+
#
|
151
|
+
# Named parameters used internally for recursive calls are:
|
152
|
+
#
|
153
|
+
# +scim_attributes_map+:: The fragment of the attribute map found
|
154
|
+
# from +instance_including_mixin+'s class
|
155
|
+
# initially, which is relevant to the
|
156
|
+
# current recursion level. E.g. it might
|
157
|
+
# be the sub-attributes map of "name",
|
158
|
+
# for things like a "familyName" mapping.
|
159
|
+
#
|
160
|
+
# +schema_attributes+:: An array of schema attributes for the
|
161
|
+
# current recursion level, corresponding
|
162
|
+
# to +scim_attributes_map+.
|
163
|
+
#
|
164
|
+
# +rebuilt_attribute_array+:: Redrawn schema attributes are collected
|
165
|
+
# into this array, which is altered in
|
166
|
+
# place. It is usually a 'subAttributes'
|
167
|
+
# property of a schema attribute that's
|
168
|
+
# provoked recursion in order to examine
|
169
|
+
# and rebuild its sub-attributes directly.
|
170
|
+
#
|
171
|
+
def redraw_schema_using_mappings(
|
172
|
+
original_schema_instance:,
|
173
|
+
instance_including_mixin:,
|
174
|
+
|
175
|
+
scim_attributes_map: nil,
|
176
|
+
schema_attributes: nil,
|
177
|
+
rebuilt_attribute_array: nil
|
178
|
+
)
|
179
|
+
schema_attributes ||= original_schema_instance.scim_attributes
|
180
|
+
scim_attributes_map ||= instance_including_mixin
|
181
|
+
.class
|
182
|
+
.scim_attributes_map()
|
183
|
+
.with_indifferent_case_insensitive_access()
|
184
|
+
|
185
|
+
rebuilt_schema_instance = nil
|
186
|
+
|
187
|
+
if rebuilt_attribute_array.nil?
|
188
|
+
rebuilt_schema_instance = self.duplicate_attribute(original_schema_instance)
|
189
|
+
rebuilt_schema_instance.scim_attributes = []
|
190
|
+
rebuilt_attribute_array = rebuilt_schema_instance.scim_attributes
|
191
|
+
end
|
192
|
+
|
193
|
+
schema_attributes.each do | schema_attribute |
|
194
|
+
if schema_attribute.multiValued && schema_attribute.subAttributes&.any?
|
195
|
+
mapped_multivalue_attribute = scim_attributes_map[schema_attribute.name]
|
196
|
+
|
197
|
+
# We expect either an array in the attribute map to correspond with
|
198
|
+
# a multivalued schema attribute, or nothing. If we get some other
|
199
|
+
# non-Array, not-nil thing, it's just ignored.
|
200
|
+
#
|
201
|
+
if mapped_multivalue_attribute.is_a?(Array)
|
202
|
+
|
203
|
+
# A single-entry array with "list using" semantics, for a
|
204
|
+
# collection of an arbitrary number of same-class items - e.g.
|
205
|
+
# Groups to which a User belongs.
|
206
|
+
#
|
207
|
+
# If this is an up-to-date mapping, there's a "class" entry that
|
208
|
+
# tells us what the collection is compromised of. If not, then we
|
209
|
+
# check for ActiveRecord collections as a fallback and if that is
|
210
|
+
# the case here, can use reflection to try and find the class. If
|
211
|
+
# all else fails, we drop to generic schema for the collection.
|
212
|
+
#
|
213
|
+
if mapped_multivalue_attribute.first&.dig(:list)
|
214
|
+
associated_resource_class = mapped_multivalue_attribute.first[:class]
|
215
|
+
|
216
|
+
if (
|
217
|
+
associated_resource_class.nil? &&
|
218
|
+
instance_including_mixin.is_a?(ActiveRecord::Base)
|
219
|
+
)
|
220
|
+
associated_resource_class = instance_including_mixin
|
221
|
+
.class
|
222
|
+
.reflect_on_association(mapped_multivalue_attribute.first[:list])
|
223
|
+
&.klass
|
224
|
+
end
|
225
|
+
|
226
|
+
if associated_resource_class.nil? || ! associated_resource_class.include?(Scimitar::Resources::Mixin)
|
227
|
+
rebuilt_attribute_array << schema_attribute
|
228
|
+
else
|
229
|
+
rebuilt_schema_attribute = self.duplicate_attribute(schema_attribute)
|
230
|
+
rebuilt_schema_attribute.subAttributes = []
|
231
|
+
rebuilt_attribute_array << rebuilt_schema_attribute
|
232
|
+
|
233
|
+
redraw_schema_using_mappings(
|
234
|
+
original_schema_instance: original_schema_instance,
|
235
|
+
instance_including_mixin: associated_resource_class.new,
|
236
|
+
scim_attributes_map: mapped_multivalue_attribute.first[:using],
|
237
|
+
schema_attributes: schema_attribute.subAttributes,
|
238
|
+
rebuilt_attribute_array: rebuilt_schema_attribute.subAttributes
|
239
|
+
)
|
240
|
+
end
|
241
|
+
|
242
|
+
# A one-or-more entry array with "match with" semantics, to match
|
243
|
+
# discrete mapped items with a particular value in a particular
|
244
|
+
# field - e.g. an e-mail of type "work" mapping the SCIM "value"
|
245
|
+
# to a local attribute of "work_email_address".
|
246
|
+
#
|
247
|
+
# Mutability or supported attributes here might vary per matched
|
248
|
+
# type. There's no way for SCIM schema to represent that so we
|
249
|
+
# just merge all the "using" mappings together, in order of array
|
250
|
+
# appearance, and have that combined attribute map treated as the
|
251
|
+
# data the schema response will use.
|
252
|
+
#
|
253
|
+
elsif mapped_multivalue_attribute.first&.dig(:match)
|
254
|
+
union_of_mappings = {}
|
255
|
+
|
256
|
+
mapped_multivalue_attribute.each do | mapped_multivalue_attribute_description |
|
257
|
+
union_of_mappings.merge!(mapped_multivalue_attribute_description[:using])
|
258
|
+
end
|
259
|
+
|
260
|
+
rebuilt_schema_attribute = self.duplicate_attribute(schema_attribute)
|
261
|
+
rebuilt_schema_attribute.subAttributes = []
|
262
|
+
rebuilt_attribute_array << rebuilt_schema_attribute
|
263
|
+
|
264
|
+
redraw_schema_using_mappings(
|
265
|
+
original_schema_instance: original_schema_instance,
|
266
|
+
instance_including_mixin: instance_including_mixin,
|
267
|
+
scim_attributes_map: union_of_mappings,
|
268
|
+
schema_attributes: schema_attribute.subAttributes,
|
269
|
+
rebuilt_attribute_array: rebuilt_schema_attribute.subAttributes
|
270
|
+
)
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
elsif schema_attribute.subAttributes&.any?
|
275
|
+
mapped_subattributes = scim_attributes_map[schema_attribute.name]
|
276
|
+
|
277
|
+
if mapped_subattributes.is_a?(Hash)
|
278
|
+
rebuilt_schema_attribute = self.duplicate_attribute(schema_attribute)
|
279
|
+
rebuilt_schema_attribute.subAttributes = []
|
280
|
+
rebuilt_attribute_array << rebuilt_schema_attribute
|
281
|
+
|
282
|
+
redraw_schema_using_mappings(
|
283
|
+
original_schema_instance: original_schema_instance,
|
284
|
+
instance_including_mixin: instance_including_mixin,
|
285
|
+
scim_attributes_map: mapped_subattributes,
|
286
|
+
schema_attributes: schema_attribute.subAttributes,
|
287
|
+
rebuilt_attribute_array: rebuilt_schema_attribute.subAttributes
|
288
|
+
)
|
289
|
+
end
|
290
|
+
|
291
|
+
else
|
292
|
+
mapped_attribute = scim_attributes_map[schema_attribute.name]
|
293
|
+
|
294
|
+
unless mapped_attribute.nil?
|
295
|
+
rebuilt_schema_attribute = self.duplicate_attribute(schema_attribute)
|
296
|
+
has_mapped_reader = true
|
297
|
+
has_mapped_writer = false
|
298
|
+
|
299
|
+
if mapped_attribute.is_a?(String) || mapped_attribute.is_a?(Symbol)
|
300
|
+
has_mapped_reader = instance_including_mixin.respond_to?(mapped_attribute)
|
301
|
+
has_mapped_writer = instance_including_mixin.scim_mutable_attributes().include?(mapped_attribute.to_sym)
|
302
|
+
end
|
303
|
+
|
304
|
+
# The schema is taken as the primary source of truth, leading to
|
305
|
+
# a matrix of "do we override it or not?" based on who is the
|
306
|
+
# more limited. When both have the same mutability there is no
|
307
|
+
# more work to do, so we just need to consider differences:
|
308
|
+
#
|
309
|
+
# Actual class support Schema says Result
|
310
|
+
# =============================================================
|
311
|
+
# readWrite readOnly readOnly (schema wins)
|
312
|
+
# readWrite writeOnly writeOnly (schema wins)
|
313
|
+
# readOnly readWrite readOnly (class wins)
|
314
|
+
# writeOnly readWrite writeOnly (class wins)
|
315
|
+
#
|
316
|
+
# Those cases are easy. But there are gnarly cases too, where we
|
317
|
+
# have no good answer and the class's mapped implementation is in
|
318
|
+
# essence broken compared to the schema. Since it is not useful
|
319
|
+
# to insist on the schema's not-reality version, the class wins.
|
320
|
+
#
|
321
|
+
# Actual class support Schema says Result
|
322
|
+
# ====================== =======================================
|
323
|
+
# readOnly writeOnly readOnly (class "wins")
|
324
|
+
# writeOnly readOnly writeOnly (class "wins")
|
325
|
+
|
326
|
+
schema_attribute_mutability = schema_attribute.mutability.downcase
|
327
|
+
|
328
|
+
if has_mapped_reader && has_mapped_writer
|
329
|
+
#
|
330
|
+
# Read-write Nothing to do. Schema always "wins" by matching or
|
331
|
+
# being more restrictive than the class's actual abilities.
|
332
|
+
|
333
|
+
elsif has_mapped_reader && ! has_mapped_writer
|
334
|
+
#
|
335
|
+
# Read-only. Class is more restrictive if schema is 'readWrite'
|
336
|
+
# or if there's the broken clash of schema 'writeOnly'.
|
337
|
+
#
|
338
|
+
if schema_attribute_mutability == 'readwrite' || schema_attribute_mutability == 'writeonly'
|
339
|
+
rebuilt_schema_attribute.mutability = 'readOnly'
|
340
|
+
end
|
341
|
+
|
342
|
+
elsif has_mapped_writer && ! has_mapped_reader
|
343
|
+
#
|
344
|
+
# Opposite to the above case.
|
345
|
+
#
|
346
|
+
if schema_attribute_mutability == 'readwrite' || schema_attribute_mutability == 'readonly'
|
347
|
+
rebuilt_schema_attribute.mutability = 'writeOnly'
|
348
|
+
end
|
349
|
+
|
350
|
+
# ...else we cannot fathom how this class works - it appears to
|
351
|
+
# have no read or write accessor for the mapped attribute. Keep
|
352
|
+
# the schema's declaration as-is.
|
353
|
+
#
|
354
|
+
end
|
355
|
+
|
356
|
+
rebuilt_attribute_array << rebuilt_schema_attribute
|
357
|
+
end
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
return rebuilt_schema_instance # (meaningless except for topmost call)
|
362
|
+
end
|
363
|
+
|
364
|
+
# Small helper that duplicates Scimitar::Schema::Attribute instances, but
|
365
|
+
# then removes their 'errors' collection which otherwise gets initialised
|
366
|
+
# to an empty value and is rendered as if part of the schema (which isn't
|
367
|
+
# a valid entry in a SCIM schema representation).
|
368
|
+
#
|
369
|
+
# +schema_attribute+:: Scimitar::Schema::Attribute to be duplicated.
|
370
|
+
# A renderable duplicate is returned.
|
371
|
+
#
|
372
|
+
def duplicate_attribute(schema_attribute)
|
373
|
+
duplicated_schema_attribute = schema_attribute.dup()
|
374
|
+
duplicated_schema_attribute.remove_instance_variable('@errors')
|
375
|
+
duplicated_schema_attribute
|
376
|
+
end
|
377
|
+
|
15
378
|
end
|
16
379
|
end
|
@@ -1,8 +1,9 @@
|
|
1
|
-
require_dependency "scimitar/application_controller"
|
2
1
|
module Scimitar
|
3
2
|
class ServiceProviderConfigurationsController < ApplicationController
|
4
3
|
def show
|
5
|
-
|
4
|
+
service_provider_configuration = Scimitar.service_provider_configuration(location: request.url).as_json
|
5
|
+
service_provider_configuration.delete("uses_defaults")
|
6
|
+
render json: service_provider_configuration
|
6
7
|
end
|
7
8
|
end
|
8
9
|
end
|
@@ -75,8 +75,8 @@ module Scimitar
|
|
75
75
|
# attributes of this complex type object.
|
76
76
|
#
|
77
77
|
def as_json(options={})
|
78
|
-
options[:except]
|
79
|
-
super.except
|
78
|
+
exclusions = options[:except] || ['errors']
|
79
|
+
super(options.merge(except: exclusions))
|
80
80
|
end
|
81
81
|
end
|
82
82
|
end
|