scimitar 1.10.0 → 2.0.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/app/controllers/scimitar/active_record_backed_resources_controller.rb +23 -98
- data/app/controllers/scimitar/application_controller.rb +13 -41
- data/app/controllers/scimitar/resource_types_controller.rb +2 -0
- data/app/controllers/scimitar/resources_controller.rb +2 -0
- data/app/controllers/scimitar/schemas_controller.rb +3 -366
- data/app/controllers/scimitar/service_provider_configurations_controller.rb +1 -0
- data/app/models/scimitar/complex_types/address.rb +6 -0
- data/app/models/scimitar/engine_configuration.rb +5 -15
- data/app/models/scimitar/error_response.rb +0 -12
- data/app/models/scimitar/lists/query_parser.rb +13 -113
- data/app/models/scimitar/resource_invalid_error.rb +1 -1
- data/app/models/scimitar/resources/base.rb +9 -53
- data/app/models/scimitar/resources/mixin.rb +59 -646
- data/app/models/scimitar/schema/address.rb +0 -1
- data/app/models/scimitar/schema/attribute.rb +5 -14
- data/app/models/scimitar/schema/base.rb +1 -1
- data/app/models/scimitar/schema/name.rb +2 -2
- data/app/models/scimitar/schema/user.rb +10 -10
- data/app/models/scimitar/schema/vdtp.rb +1 -1
- data/app/models/scimitar/service_provider_configuration.rb +3 -14
- data/config/initializers/scimitar.rb +3 -69
- data/lib/scimitar/engine.rb +12 -57
- data/lib/scimitar/support/hash_with_indifferent_case_insensitive_access.rb +10 -140
- data/lib/scimitar/version.rb +2 -2
- data/lib/scimitar.rb +2 -7
- data/spec/apps/dummy/app/controllers/mock_groups_controller.rb +1 -1
- data/spec/apps/dummy/app/models/mock_group.rb +1 -1
- data/spec/apps/dummy/app/models/mock_user.rb +9 -52
- data/spec/apps/dummy/config/application.rb +1 -0
- data/spec/apps/dummy/config/environments/test.rb +28 -5
- data/spec/apps/dummy/config/initializers/scimitar.rb +10 -90
- data/spec/apps/dummy/config/routes.rb +7 -28
- data/spec/apps/dummy/db/migrate/20210304014602_create_mock_users.rb +1 -11
- data/spec/apps/dummy/db/migrate/20210308044214_create_join_table_mock_groups_mock_users.rb +3 -8
- data/spec/apps/dummy/db/schema.rb +4 -12
- data/spec/controllers/scimitar/application_controller_spec.rb +3 -126
- data/spec/controllers/scimitar/resource_types_controller_spec.rb +2 -2
- data/spec/controllers/scimitar/schemas_controller_spec.rb +48 -344
- data/spec/models/scimitar/complex_types/address_spec.rb +4 -3
- data/spec/models/scimitar/complex_types/email_spec.rb +2 -0
- data/spec/models/scimitar/lists/query_parser_spec.rb +9 -146
- data/spec/models/scimitar/resources/base_spec.rb +71 -217
- data/spec/models/scimitar/resources/base_validation_spec.rb +5 -43
- data/spec/models/scimitar/resources/mixin_spec.rb +129 -1508
- data/spec/models/scimitar/schema/attribute_spec.rb +3 -22
- data/spec/models/scimitar/schema/base_spec.rb +1 -1
- data/spec/models/scimitar/schema/user_spec.rb +2 -12
- data/spec/requests/active_record_backed_resources_controller_spec.rb +66 -1016
- data/spec/requests/application_controller_spec.rb +3 -16
- data/spec/requests/engine_spec.rb +0 -75
- data/spec/spec_helper.rb +1 -9
- data/spec/support/hash_with_indifferent_case_insensitive_access_spec.rb +0 -108
- metadata +26 -37
- data/LICENSE.txt +0 -21
- data/README.md +0 -717
- data/lib/scimitar/support/utilities.rb +0 -111
- data/spec/apps/dummy/app/controllers/custom_create_mock_users_controller.rb +0 -25
- data/spec/apps/dummy/app/controllers/custom_replace_mock_users_controller.rb +0 -25
- data/spec/apps/dummy/app/controllers/custom_save_mock_users_controller.rb +0 -24
- data/spec/apps/dummy/app/controllers/custom_update_mock_users_controller.rb +0 -25
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 95a2166cc921a400959f9d8d4398f6bf8ecb772f8d7a0a0a73950892e85d808a
|
4
|
+
data.tar.gz: cdf5aab3812f10f69c96304e738a150f4208850267527b66d36eeb99548d7b1f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f0925517599b107e44fd93db9be142aebe608892c2c5069c50d22b353c51238290710474b062a002fdb010be3d783c4dea3b314f72f47b4aca3c2385a8fc1377
|
7
|
+
data.tar.gz: eef6eebfc64bb2d4adabfca110f26e3a6c9e227f47387da6fb384925899ddf0ae260cf68176f24040a6bb356cf34f6576c920bd44264d4a1fee415aeadc237e6
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require_dependency "scimitar/application_controller"
|
2
|
+
|
1
3
|
module Scimitar
|
2
4
|
|
3
5
|
# An ActiveRecord-centric subclass of Scimitar::ResourcesController. See that
|
@@ -17,9 +19,7 @@ module Scimitar
|
|
17
19
|
#
|
18
20
|
class ActiveRecordBackedResourcesController < ResourcesController
|
19
21
|
|
20
|
-
rescue_from
|
21
|
-
|
22
|
-
before_action :obtain_id_column_name_from_attribute_map
|
22
|
+
rescue_from ActiveRecord::RecordNotFound, with: :handle_resource_not_found # See Scimitar::ApplicationController
|
23
23
|
|
24
24
|
# GET (list)
|
25
25
|
#
|
@@ -37,13 +37,12 @@ module Scimitar
|
|
37
37
|
pagination_info = scim_pagination_info(query.count())
|
38
38
|
|
39
39
|
page_of_results = query
|
40
|
-
.order(@id_column => :asc)
|
41
40
|
.offset(pagination_info.offset)
|
42
41
|
.limit(pagination_info.limit)
|
43
42
|
.to_a()
|
44
43
|
|
45
44
|
super(pagination_info, page_of_results) do | record |
|
46
|
-
|
45
|
+
record.to_scim(location: url_for(action: :show, id: record.id))
|
47
46
|
end
|
48
47
|
end
|
49
48
|
|
@@ -52,61 +51,45 @@ module Scimitar
|
|
52
51
|
def show
|
53
52
|
super do |record_id|
|
54
53
|
record = self.find_record(record_id)
|
55
|
-
|
54
|
+
record.to_scim(location: url_for(action: :show, id: record_id))
|
56
55
|
end
|
57
56
|
end
|
58
57
|
|
59
58
|
# POST (create)
|
60
59
|
#
|
61
|
-
|
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)
|
60
|
+
def create
|
70
61
|
super do |scim_resource|
|
71
62
|
self.storage_class().transaction do
|
72
63
|
record = self.storage_class().new
|
73
64
|
record.from_scim!(scim_hash: scim_resource.as_json())
|
74
|
-
self.save!(record
|
75
|
-
|
65
|
+
self.save!(record)
|
66
|
+
record.to_scim(location: url_for(action: :show, id: record.id))
|
76
67
|
end
|
77
68
|
end
|
78
69
|
end
|
79
70
|
|
80
71
|
# PUT (replace)
|
81
72
|
#
|
82
|
-
|
83
|
-
# block, passing the updated record which the block must persist, with the
|
84
|
-
# same rules as for #create.
|
85
|
-
#
|
86
|
-
def replace(&block)
|
73
|
+
def replace
|
87
74
|
super do |record_id, scim_resource|
|
88
75
|
self.storage_class().transaction do
|
89
76
|
record = self.find_record(record_id)
|
90
77
|
record.from_scim!(scim_hash: scim_resource.as_json())
|
91
|
-
self.save!(record
|
92
|
-
|
78
|
+
self.save!(record)
|
79
|
+
record.to_scim(location: url_for(action: :show, id: record.id))
|
93
80
|
end
|
94
81
|
end
|
95
82
|
end
|
96
83
|
|
97
84
|
# PATCH (update)
|
98
85
|
#
|
99
|
-
|
100
|
-
# block, passing the updated record which the block must persist, with the
|
101
|
-
# same rules as for #create.
|
102
|
-
#
|
103
|
-
def update(&block)
|
86
|
+
def update
|
104
87
|
super do |record_id, patch_hash|
|
105
88
|
self.storage_class().transaction do
|
106
89
|
record = self.find_record(record_id)
|
107
90
|
record.from_scim_patch!(patch_hash: patch_hash)
|
108
|
-
self.save!(record
|
109
|
-
|
91
|
+
self.save!(record)
|
92
|
+
record.to_scim(location: url_for(action: :show, id: record.id))
|
110
93
|
end
|
111
94
|
end
|
112
95
|
end
|
@@ -148,46 +131,19 @@ module Scimitar
|
|
148
131
|
raise NotImplementedError
|
149
132
|
end
|
150
133
|
|
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
|
-
|
162
134
|
# Find a record by ID. Subclasses can override this if they need special
|
163
135
|
# lookup behaviour.
|
164
136
|
#
|
165
137
|
# +record_id+:: Record ID (SCIM schema 'id' value - "our" ID).
|
166
138
|
#
|
167
139
|
def find_record(record_id)
|
168
|
-
self.storage_scope().
|
169
|
-
end
|
170
|
-
|
171
|
-
# DRY up controller actions - pass a record; returns the SCIM
|
172
|
-
# representation, with a "show" location specified via #url_for.
|
173
|
-
#
|
174
|
-
def record_to_scim(record)
|
175
|
-
record.to_scim(
|
176
|
-
location: url_for(action: :show, id: record.send(@id_column)),
|
177
|
-
include_attributes: params.fetch(:attributes, "").split(",")
|
178
|
-
)
|
140
|
+
self.storage_scope().find(record_id)
|
179
141
|
end
|
180
142
|
|
181
143
|
# Save a record, dealing with validation exceptions by raising SCIM
|
182
144
|
# errors.
|
183
145
|
#
|
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.
|
146
|
+
# +record+:: ActiveRecord subclass to save (via #save!).
|
191
147
|
#
|
192
148
|
# The return value is not used internally, making life easier for
|
193
149
|
# overriding subclasses to "do the right thing" / avoid mistakes (instead
|
@@ -195,31 +151,11 @@ module Scimitar
|
|
195
151
|
# and relying upon this to generate correct response payloads - an early
|
196
152
|
# version of the gem did this and it caused a confusing subclass bug).
|
197
153
|
#
|
198
|
-
def save!(record
|
199
|
-
|
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
|
154
|
+
def save!(record)
|
155
|
+
record.save!
|
207
156
|
|
208
|
-
|
209
|
-
|
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
|
157
|
+
rescue ActiveRecord::RecordInvalid => exception
|
158
|
+
joined_errors = record.errors.full_messages.join('; ')
|
223
159
|
|
224
160
|
# https://tools.ietf.org/html/rfc7644#page-12
|
225
161
|
#
|
@@ -229,27 +165,16 @@ module Scimitar
|
|
229
165
|
# status code 409 (Conflict) with a "scimType" error code of
|
230
166
|
# "uniqueness"
|
231
167
|
#
|
232
|
-
if
|
168
|
+
if record.errors.any? { | e | e.type == :taken }
|
233
169
|
raise Scimitar::ErrorResponse.new(
|
234
170
|
status: 409,
|
235
171
|
scimType: 'uniqueness',
|
236
|
-
detail:
|
172
|
+
detail: joined_errors
|
237
173
|
)
|
238
174
|
else
|
239
|
-
raise Scimitar::ResourceInvalidError.new(
|
175
|
+
raise Scimitar::ResourceInvalidError.new(joined_errors)
|
240
176
|
end
|
241
177
|
end
|
242
178
|
|
243
|
-
# Called via +before_action+ - stores in @id_column the name of whatever
|
244
|
-
# model column is used to store the record ID, via
|
245
|
-
# Scimitar::Resources::Mixin::scim_attributes_map.
|
246
|
-
#
|
247
|
-
# Default is <tt>:id</tt>.
|
248
|
-
#
|
249
|
-
def obtain_id_column_name_from_attribute_map
|
250
|
-
attrs = storage_class().scim_attributes_map() || {}
|
251
|
-
@id_column = attrs[:id] || :id
|
252
|
-
end
|
253
|
-
|
254
179
|
end
|
255
180
|
end
|
@@ -25,11 +25,10 @@ module Scimitar
|
|
25
25
|
#
|
26
26
|
# ...to "globally" invoke this handler if you wish.
|
27
27
|
#
|
28
|
-
# +
|
29
|
-
# via #handle_scim_error (if present).
|
28
|
+
# +_exception+:: Exception instance (currently unused).
|
30
29
|
#
|
31
|
-
def handle_resource_not_found(
|
32
|
-
handle_scim_error(NotFoundError.new(params[:id])
|
30
|
+
def handle_resource_not_found(_exception)
|
31
|
+
handle_scim_error(NotFoundError.new(params[:id]))
|
33
32
|
end
|
34
33
|
|
35
34
|
# This base controller uses:
|
@@ -39,22 +38,9 @@ module Scimitar
|
|
39
38
|
# ...to "globally" invoke this handler for all Scimitar errors (including
|
40
39
|
# subclasses).
|
41
40
|
#
|
42
|
-
# Mandatory parameters are:
|
43
|
-
#
|
44
41
|
# +error_response+:: Scimitar::ErrorResponse (or subclass) instance.
|
45
42
|
#
|
46
|
-
|
47
|
-
#
|
48
|
-
# *exception+:: If a Ruby exception was the reason this method is being
|
49
|
-
# called, pass it here. Any configured exception reporting
|
50
|
-
# mechanism will be invokved with the given parameter.
|
51
|
-
# Otherwise, the +error_response+ value is reported.
|
52
|
-
#
|
53
|
-
def handle_scim_error(error_response, exception = error_response)
|
54
|
-
unless Scimitar.engine_configuration.exception_reporter.nil?
|
55
|
-
Scimitar.engine_configuration.exception_reporter.call(exception)
|
56
|
-
end
|
57
|
-
|
43
|
+
def handle_scim_error(error_response)
|
58
44
|
render json: error_response, status: error_response.status
|
59
45
|
end
|
60
46
|
|
@@ -69,7 +55,7 @@ module Scimitar
|
|
69
55
|
# +exception+:: Exception instance.
|
70
56
|
#
|
71
57
|
def handle_bad_json_error(exception)
|
72
|
-
handle_scim_error(ErrorResponse.new(status: 400, detail: "Invalid JSON - #{exception.message}")
|
58
|
+
handle_scim_error(ErrorResponse.new(status: 400, detail: "Invalid JSON - #{exception.message}"))
|
73
59
|
end
|
74
60
|
|
75
61
|
# This base controller uses:
|
@@ -82,7 +68,7 @@ module Scimitar
|
|
82
68
|
#
|
83
69
|
def handle_unexpected_error(exception)
|
84
70
|
Rails.logger.error("#{exception.message}\n#{exception.backtrace}")
|
85
|
-
handle_scim_error(ErrorResponse.new(status: 500, detail: exception.message)
|
71
|
+
handle_scim_error(ErrorResponse.new(status: 500, detail: exception.message))
|
86
72
|
end
|
87
73
|
|
88
74
|
# =========================================================================
|
@@ -96,17 +82,12 @@ module Scimitar
|
|
96
82
|
# request and subclass processing.
|
97
83
|
#
|
98
84
|
def require_scim
|
99
|
-
|
100
|
-
|
101
|
-
if request.media_type.nil? || request.media_type.empty?
|
102
|
-
request.format = :scim
|
103
|
-
request.headers['CONTENT_TYPE'] = scim_mime_type
|
104
|
-
elsif request.media_type.downcase == scim_mime_type
|
85
|
+
if request.content_type&.downcase == Mime::Type.lookup_by_extension(:scim).to_s
|
105
86
|
request.format = :scim
|
106
87
|
elsif request.format == :scim
|
107
|
-
request.headers['CONTENT_TYPE'] =
|
88
|
+
request.headers['CONTENT_TYPE'] = Mime::Type.lookup_by_extension(:scim).to_s
|
108
89
|
else
|
109
|
-
handle_scim_error(ErrorResponse.new(status: 406, detail: "Only #{
|
90
|
+
handle_scim_error(ErrorResponse.new(status: 406, detail: "Only #{Mime::Type.lookup_by_extension(:scim)} type is accepted."))
|
110
91
|
end
|
111
92
|
end
|
112
93
|
|
@@ -124,13 +105,8 @@ module Scimitar
|
|
124
105
|
#
|
125
106
|
# https://stackoverflow.com/questions/10239970/what-is-the-delimiter-for-www-authenticate-for-multiple-schemes
|
126
107
|
#
|
127
|
-
response.set_header('
|
128
|
-
response.set_header('
|
129
|
-
|
130
|
-
# No matter what a caller might request via headers, the only content
|
131
|
-
# type we can ever respond with is JSON-for-SCIM.
|
132
|
-
#
|
133
|
-
response.set_header('Content-Type', "#{Mime::Type.lookup_by_extension(:scim)}; charset=utf-8")
|
108
|
+
response.set_header('WWW_AUTHENTICATE', 'Basic' ) if Scimitar.engine_configuration.basic_authenticator.present?
|
109
|
+
response.set_header('WWW_AUTHENTICATE', 'Bearer') if Scimitar.engine_configuration.token_authenticator.present?
|
134
110
|
end
|
135
111
|
|
136
112
|
def authenticate
|
@@ -139,15 +115,11 @@ module Scimitar
|
|
139
115
|
|
140
116
|
def authenticated?
|
141
117
|
result = if Scimitar.engine_configuration.basic_authenticator.present?
|
142
|
-
authenticate_with_http_basic
|
143
|
-
instance_exec(username, password, &Scimitar.engine_configuration.basic_authenticator)
|
144
|
-
end
|
118
|
+
authenticate_with_http_basic(&Scimitar.engine_configuration.basic_authenticator)
|
145
119
|
end
|
146
120
|
|
147
121
|
result ||= if Scimitar.engine_configuration.token_authenticator.present?
|
148
|
-
authenticate_with_http_token
|
149
|
-
instance_exec(token, options, &Scimitar.engine_configuration.token_authenticator)
|
150
|
-
end
|
122
|
+
authenticate_with_http_token(&Scimitar.engine_configuration.token_authenticator)
|
151
123
|
end
|
152
124
|
|
153
125
|
return result
|