scimitar 2.6.0 → 2.6.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/LICENSE.txt +21 -0
- data/README.md +671 -0
- data/app/controllers/scimitar/active_record_backed_resources_controller.rb +4 -3
- data/app/controllers/scimitar/application_controller.rb +7 -2
- data/app/models/scimitar/schema/attribute.rb +3 -3
- data/config/initializers/scimitar.rb +3 -3
- data/lib/scimitar/version.rb +2 -2
- data/spec/apps/dummy/config/routes.rb +2 -2
- data/spec/controllers/scimitar/application_controller_spec.rb +2 -2
- data/spec/controllers/scimitar/schemas_controller_spec.rb +1 -1
- data/spec/requests/active_record_backed_resources_controller_spec.rb +110 -34
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e763d8f162583b44983db37fe3d8f0e8c9fa4f39f841813e9c3df247ffcd8cf9
|
4
|
+
data.tar.gz: cfc7680b8f12d928a8975882109027f0c72f13884b438f04b90dcadc8d8a582c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4a4d50b204fa662b9661d258686c22296c07bb88fd5cd2cbebfdbfef3455ca1385877ac66af5c3d6d22de6bfb6b43ae12ce74e21a9b5b279fa640b1042002a0b
|
7
|
+
data.tar.gz: 795e29ab7736f0e40fd3bc39e055146935427a13a4a2e10cbbee5b9ddd6980c164cc9d187131d43895a602b0c44a2d950e4cb3c87af3ed7f9959bd1f8dfbb6fa
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2021 RIPA Global
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,671 @@
|
|
1
|
+
# Scimitar
|
2
|
+
|
3
|
+
[![Gem Version](https://badge.fury.io/rb/scimitar.svg)](https://badge.fury.io/rb/scimitar)
|
4
|
+
[![Build Status](https://app.travis-ci.com/RIPAGlobal/scimitar.svg?branch=main)](https://app.travis-ci.com/RIPAGlobal/scimitar)
|
5
|
+
[![License](https://img.shields.io/badge/license-mit-blue.svg)](https://opensource.org/licenses/MIT)
|
6
|
+
|
7
|
+
A SCIM v2 API endpoint implementation for Ruby On Rails.
|
8
|
+
|
9
|
+
For a list of changes and information on major version upgrades, please see `CHANGELOG.md`.
|
10
|
+
|
11
|
+
|
12
|
+
|
13
|
+
## Overview
|
14
|
+
|
15
|
+
System for Cross-domain Identity Management (SCIM) is a protocol that helps systems synchronise user data between different business systems. A _service provider_ hosts a SCIM API endpoint implementation and the Scimitar gem is used to help quickly build this implementation. One or more _enterprise subscribers_ use these APIs to let that service know about changes in the enterprise's user (employee) list.
|
16
|
+
|
17
|
+
In the context of the names used by the SCIM standard, the service that is provided is some kind of software-as-a-service solution that the enterprise subscriber uses to assist with their day to day business. The enterprise maintains its user (employee) list via whatever means it wants, but includes SCIM support so that any third party services it uses can be kept up to date with adds, removals or changes to employee data.
|
18
|
+
|
19
|
+
* [Overview](https://en.wikipedia.org/wiki/System_for_Cross-domain_Identity_Management) at Wikipedia
|
20
|
+
* [More detailed introduction](http://www.simplecloud.info) at SimpleCloud
|
21
|
+
* SCIM v2 RFC [7642](https://tools.ietf.org/html/rfc7642): Concepts
|
22
|
+
* SCIM v2 RFC [7643](https://tools.ietf.org/html/rfc7643): Core schema
|
23
|
+
* SCIM v2 RFC [7644](https://tools.ietf.org/html/rfc7644): Protocol
|
24
|
+
|
25
|
+
|
26
|
+
|
27
|
+
## Installation
|
28
|
+
|
29
|
+
Install using:
|
30
|
+
|
31
|
+
```shell
|
32
|
+
gem install scimitar
|
33
|
+
```
|
34
|
+
|
35
|
+
In your Gemfile:
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
gem 'scimitar', '~> 2.0'
|
39
|
+
```
|
40
|
+
|
41
|
+
Scimitar uses [semantic versioning](https://semver.org) so you can be confident that patch and minor version updates for features, bug fixes and/or security patches will not break your application.
|
42
|
+
|
43
|
+
|
44
|
+
|
45
|
+
## Heritage
|
46
|
+
|
47
|
+
Scimitar borrows heavily - to the point of cut-and-paste - from:
|
48
|
+
|
49
|
+
* [ScimEngine](https://github.com/Cisco-AMP/scim_engine) for the Rails controllers and resource-agnostic subclassing approach that makes supporting User and/or Group, along with custom resource types if you need them, quite easy.
|
50
|
+
* [ScimRails](https://github.com/lessonly/scim_rails) for the bearer token support, 'index' actions and filter support.
|
51
|
+
* [SCIM Query Filter Parser](https://github.com/ingydotnet/scim-query-filter-parser-rb) for advanced filter handling.
|
52
|
+
|
53
|
+
All three are provided under the MIT license. Scimitar is too.
|
54
|
+
|
55
|
+
|
56
|
+
|
57
|
+
## Usage
|
58
|
+
|
59
|
+
Scimitar is best used with Rails and ActiveRecord, but it can be used with other persistence back-ends too - you just have to do more of the work in controllers using Scimitar's lower level controller subclasses, rather than relying on Scimitar's higher level ActiveRecord abstractions.
|
60
|
+
|
61
|
+
### Authentication
|
62
|
+
|
63
|
+
Noting the _Security_ section later - to set up an authentication method, create a `config/initializers/scimitar.rb` in your Rails application and define a token-based authenticator and/or a username-password authenticator in the [engine configuration section documented in the sample file](https://github.com/RIPAGlobal/scimitar/blob/main/config/initializers/scimitar.rb). For example:
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
Scimitar.engine_configuration = Scimitar::EngineConfiguration.new({
|
67
|
+
token_authenticator: Proc.new do | token, options |
|
68
|
+
|
69
|
+
# This is where you'd write the code to validate `token` - the means by
|
70
|
+
# which your application issues tokens to SCIM clients, or validates them,
|
71
|
+
# is outside the scope of the gem; the required mechanisms vary by client.
|
72
|
+
# More on this can be found in the 'Security' section later.
|
73
|
+
#
|
74
|
+
SomeLibraryModule.validate_access_token(token)
|
75
|
+
|
76
|
+
end
|
77
|
+
})
|
78
|
+
```
|
79
|
+
|
80
|
+
When it comes to token access, Scimitar neither enforces nor presumes any kind of encoding for bearer tokens. You can use anything you like, including encoding/encrypting JWTs if you so wish - https://rubygems.org/gems/jwt may be useful. The way in which a client might integrate with your SCIM service varies by client and you will have to check documentation to see how a token gets conveyed to that client in the first place (e.g. a full OAuth flow with your application, or just a static token generated in some UI which an administrator copies and pastes into their client's SCIM configuration UI).
|
81
|
+
|
82
|
+
**Important:** Under Rails 7 or later, you may need to wrap any Scimitar configuration with `Rails.application.config.to_prepare do...` to avoid `NameError: uninitialized constant...` exceptions arising due to autoloader problems:
|
83
|
+
|
84
|
+
```ruby
|
85
|
+
Rails.application.config.to_prepare do
|
86
|
+
Scimitar.engine_configuration = Scimitar::EngineConfiguration.new({
|
87
|
+
# ...
|
88
|
+
end
|
89
|
+
end
|
90
|
+
```
|
91
|
+
|
92
|
+
### Routes
|
93
|
+
|
94
|
+
For each resource you support, add these lines to your `routes.rb`:
|
95
|
+
|
96
|
+
```ruby
|
97
|
+
namespace :scim_v2 do
|
98
|
+
mount Scimitar::Engine, at: '/'
|
99
|
+
|
100
|
+
get 'Users', to: 'users#index'
|
101
|
+
get 'Users/:id', to: 'users#show'
|
102
|
+
post 'Users', to: 'users#create'
|
103
|
+
put 'Users/:id', to: 'users#replace'
|
104
|
+
patch 'Users/:id', to: 'users#update'
|
105
|
+
delete 'Users/:id', to: 'users#destroy'
|
106
|
+
end
|
107
|
+
```
|
108
|
+
|
109
|
+
All routes then will be available at `https://.../scim_v2/...` via controllers you write in `app/controllers/scim_v2/...`, e.g. `app/controllers/scim_v2/users_controller.rb`. More on controllers later.
|
110
|
+
|
111
|
+
#### URL helpers
|
112
|
+
|
113
|
+
Internally Scimitar always invokes URL helpers in the controller layer. I.e. any variable path parameters will be resolved by Rails automatically. If you need more control over the way URLs are generated you can override any URL helper by redefining it in the application controller mixin. See the [`application_controller_mixin` engine configuration option](https://github.com/RIPAGlobal/scimitar/blob/main/config/initializers/scimitar.rb).
|
114
|
+
|
115
|
+
### Data models
|
116
|
+
|
117
|
+
Scimitar assumes that each SCIM resource maps to a single corresponding class in your system. This might be an abstraction over more complex underpinings, but either way, a 1:1 relationship is expected. For example, a SCIM User might map to a User ActiveRecord model in your Rails application, while a SCIM Group might map to some custom class called Team which operates on a more complex set of data "under the hood".
|
118
|
+
|
119
|
+
Before writing any controllers, it's a good idea to examine the SCIM specification and figure out how you intend to map SCIM attributes in any resources of interest, to your local data. A [mixin is provided](https://github.com/RIPAGlobal/scimitar/blob/main/app/models/scimitar/resources/mixin.rb) which you can include in any plain old Ruby class (including, but not limited to ActiveRecord model classes) - a more readable form of the comments in this file [is in the RDoc output](https://www.rubydoc.info/gems/scimitar/Scimitar/Resources/Mixin).
|
120
|
+
|
121
|
+
The functionality exposed by the mixin is relatively complicated because the range of operations that the SCIM API supports is quite extensive. Rather than duplicate all the information here, please see the extensive comments in the mixin linked above for more information. There are examples in the [test suite's Rails models](https://github.com/RIPAGlobal/scimitar/tree/main/spec/apps/dummy/app/models), or for another example:
|
122
|
+
|
123
|
+
```ruby
|
124
|
+
class User < ActiveRecord::Base
|
125
|
+
|
126
|
+
# The attributes in the SCIM section below include a reference to this
|
127
|
+
# hypothesised 'groups' HABTM relationship. All of the other "leaf node"
|
128
|
+
# Symbols - e.g. ":first_name", ":last_name" - are expected to be defined as
|
129
|
+
# accessors e.g. via ActiveRecord and your related database table columns,
|
130
|
+
# "attr_accessor" declarations, or bespoke "def foo"/"def foo=(value)". If a
|
131
|
+
# write accessor is not present, the attribute will not be writable via SCIM.
|
132
|
+
#
|
133
|
+
has_and_belongs_to_many :groups
|
134
|
+
|
135
|
+
# ===========================================================================
|
136
|
+
# SCIM MIXIN AND REQUIRED METHODS
|
137
|
+
# ===========================================================================
|
138
|
+
#
|
139
|
+
# All class methods shown below are mandatory unless otherwise commented.
|
140
|
+
|
141
|
+
def self.scim_resource_type
|
142
|
+
return Scimitar::Resources::User
|
143
|
+
end
|
144
|
+
|
145
|
+
def self.scim_attributes_map
|
146
|
+
return {
|
147
|
+
id: :id,
|
148
|
+
externalId: :scim_uid,
|
149
|
+
userName: :username,
|
150
|
+
name: {
|
151
|
+
givenName: :first_name,
|
152
|
+
familyName: :last_name
|
153
|
+
},
|
154
|
+
emails: [
|
155
|
+
{
|
156
|
+
match: 'type',
|
157
|
+
with: 'work',
|
158
|
+
using: {
|
159
|
+
value: :work_email_address,
|
160
|
+
primary: true
|
161
|
+
}
|
162
|
+
},
|
163
|
+
{
|
164
|
+
match: 'type',
|
165
|
+
with: 'home',
|
166
|
+
using: {
|
167
|
+
value: :home_email_address,
|
168
|
+
primary: false
|
169
|
+
}
|
170
|
+
},
|
171
|
+
],
|
172
|
+
phoneNumbers: [
|
173
|
+
{
|
174
|
+
match: 'type',
|
175
|
+
with: 'work',
|
176
|
+
using: {
|
177
|
+
value: :work_phone_number,
|
178
|
+
primary: false
|
179
|
+
}
|
180
|
+
},
|
181
|
+
],
|
182
|
+
|
183
|
+
# NB The 'groups' collection in a SCIM User resource is read-only, so
|
184
|
+
# we provide no ":find_with" key for looking up records for writing
|
185
|
+
# updates to the associated collection.
|
186
|
+
#
|
187
|
+
groups: [
|
188
|
+
{
|
189
|
+
list: :groups,
|
190
|
+
using: {
|
191
|
+
value: :id,
|
192
|
+
display: :display_name
|
193
|
+
}
|
194
|
+
}
|
195
|
+
],
|
196
|
+
active: :is_active
|
197
|
+
}
|
198
|
+
end
|
199
|
+
|
200
|
+
def self.scim_mutable_attributes
|
201
|
+
return nil
|
202
|
+
end
|
203
|
+
|
204
|
+
# The attributes in this example include a reference to the same hypothesised
|
205
|
+
# 'Group' model as in the HABTM relationship above. In this case, in order to
|
206
|
+
# filter by "groups" or "groups.value", the 'column' entry must reference the
|
207
|
+
# Group model's ID column as an AREL attribute as shown below, and the SCIM
|
208
|
+
# controller's #storage_scope implementation must also introduce a #join with
|
209
|
+
# ':groups' - see the "Queries & Optimisations" section below.
|
210
|
+
#
|
211
|
+
def self.scim_queryable_attributes
|
212
|
+
return {
|
213
|
+
givenName: { column: :first_name },
|
214
|
+
familyName: { column: :last_name },
|
215
|
+
emails: { column: :work_email_address },
|
216
|
+
groups: { column: Group.arel_table[:id] },
|
217
|
+
"groups.value" => { column: Group.arel_table[:id] },
|
218
|
+
}
|
219
|
+
end
|
220
|
+
|
221
|
+
# Optional but recommended.
|
222
|
+
#
|
223
|
+
def self.scim_timestamps_map
|
224
|
+
{
|
225
|
+
created: :created_at,
|
226
|
+
lastModified: :updated_at
|
227
|
+
}
|
228
|
+
end
|
229
|
+
|
230
|
+
# If you omit any mandatory declarations, you'll get an exception raised by
|
231
|
+
# this inclusion which tells you which method(s) need(s) to be added.
|
232
|
+
#
|
233
|
+
include Scimitar::Resources::Mixin
|
234
|
+
end
|
235
|
+
```
|
236
|
+
|
237
|
+
### Controllers
|
238
|
+
|
239
|
+
#### ActiveRecord
|
240
|
+
|
241
|
+
If you use ActiveRecord, your controllers can potentially be extremely simple by subclassing [`Scimitar::ActiveRecordBackedResourcesController`](https://www.rubydoc.info/gems/scimitar/Scimitar/ActiveRecordBackedResourcesController) - at a minimum:
|
242
|
+
|
243
|
+
```ruby
|
244
|
+
module Scim
|
245
|
+
class UsersController < Scimitar::ActiveRecordBackedResourcesController
|
246
|
+
|
247
|
+
skip_before_action :verify_authenticity_token
|
248
|
+
|
249
|
+
protected
|
250
|
+
|
251
|
+
def storage_class
|
252
|
+
User
|
253
|
+
end
|
254
|
+
|
255
|
+
def storage_scope
|
256
|
+
User.all # Or e.g. "User.where(is_deleted: false)" - whatever base scope you require
|
257
|
+
end
|
258
|
+
|
259
|
+
end
|
260
|
+
end
|
261
|
+
```
|
262
|
+
|
263
|
+
All data-layer actions are taken via `#find` or `#save!`, with exceptions such as `ActiveRecord::RecordNotFound`, `ActiveRecord::RecordInvalid` or generalised SCIM exceptions handled by various superclasses. For a real Rails example of this, see the [test suite's controllers](https://github.com/RIPAGlobal/scimitar/tree/main/spec/apps/dummy/app/controllers) which are invoked via its [routing declarations](https://github.com/RIPAGlobal/scimitar/blob/main/spec/apps/dummy/config/routes.rb).
|
264
|
+
|
265
|
+
#### Queries & Optimisations
|
266
|
+
|
267
|
+
The scope can be optimised to eager load the data exposed by the SCIM interface, i.e.:
|
268
|
+
|
269
|
+
```ruby
|
270
|
+
def storage_scope
|
271
|
+
User.eager_load(:groups)
|
272
|
+
end
|
273
|
+
```
|
274
|
+
|
275
|
+
In cases where you have references to related columns in your `scim_queryable_attributes`, your `storage_scope` must join the relation:
|
276
|
+
|
277
|
+
```ruby
|
278
|
+
def storage_scope
|
279
|
+
User.left_join(:groups)
|
280
|
+
end
|
281
|
+
```
|
282
|
+
|
283
|
+
#### Other source types
|
284
|
+
|
285
|
+
If you do _not_ use ActiveRecord to store data, or if you have very esoteric read-write requirements, you can subclass [`Scimigar::ResourcesController`](https://www.rubydoc.info/gems/scimitar/Scimitar/ResourcesController) in a manner similar to this:
|
286
|
+
|
287
|
+
```ruby
|
288
|
+
class UsersController < Scimitar::ResourcesController
|
289
|
+
|
290
|
+
# SCIM clients don't use Rails CSRF tokens.
|
291
|
+
#
|
292
|
+
skip_before_action :verify_authenticity_token
|
293
|
+
|
294
|
+
# If you have any filters you need to run BEFORE authentication done in
|
295
|
+
# the superclass (typically set up in config/initializers/scimitar.rb),
|
296
|
+
# then use "prepend_before_filter to declare these - else Scimitar's
|
297
|
+
# own authorisation before-action filter would always run first.
|
298
|
+
|
299
|
+
def index
|
300
|
+
# There's a degree of heavy lifting for arbitrary storage engines.
|
301
|
+
query = if params[:filter].present?
|
302
|
+
attribute_map = User.new.scim_queryable_attributes() # Note use of *instance* method
|
303
|
+
parser = Scimitar::Lists::QueryParser.new(attribute_map)
|
304
|
+
|
305
|
+
parser.parse(params[:filter])
|
306
|
+
# Then use 'parser' to read e.g. #tree or #rpn and turn this into a
|
307
|
+
# query object for your storage engine. With ActiveRecord, you could
|
308
|
+
# just do: parser.to_activerecord_query(base_scope)
|
309
|
+
else
|
310
|
+
# Return a query object for 'all results' (e.g. User.all).
|
311
|
+
end
|
312
|
+
|
313
|
+
# Assuming the 'query' object above had ActiveRecord-like semantics,
|
314
|
+
# you'd create a Scimitar::Lists::Count object with total count filled in
|
315
|
+
# via #scim_pagination_info and obtain a page of results with something
|
316
|
+
# like the code shown below.
|
317
|
+
pagination_info = scim_pagination_info(query.count())
|
318
|
+
page_of_results = query.offset(pagination_info.offset).limit(pagination_info.limit).to_a
|
319
|
+
|
320
|
+
super(pagination_info, page_of_results) do | record |
|
321
|
+
# Return each instance as a SCIM object, e.g. via Scimitar::Resources::Mixin#to_scim
|
322
|
+
record.to_scim(location: url_for(action: :show, id: record.id))
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
def show
|
327
|
+
super do |user_id|
|
328
|
+
user = find_user(user_id)
|
329
|
+
# Evaluate to the record as a SCIM object, e.g. via Scimitar::Resources::Mixin#to_scim
|
330
|
+
user.to_scim(location: url_for(action: :show, id: user_id))
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
def create
|
335
|
+
super do |scim_resource|
|
336
|
+
# Create an instance based on the Scimitar::Resources::User in
|
337
|
+
# "scim_resource" (or whatever your ::storage_class() defines via its
|
338
|
+
# ::scim_resource_type class method).
|
339
|
+
record = self.storage_class().new
|
340
|
+
record.from_scim!(scim_hash: scim_resource.as_json())
|
341
|
+
self.save!(record)
|
342
|
+
# Evaluate to the record as a SCIM object (or do that via "self.save!")
|
343
|
+
user.to_scim(location: url_for(action: :show, id: record.id))
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
def replace
|
348
|
+
super do |record_id, scim_resource|
|
349
|
+
# Fully update an instance based on the Scimitar::Resources::User in
|
350
|
+
# "scim_resource" (or whatever your ::storage_class() defines via its
|
351
|
+
# ::scim_resource_type class method). For example:
|
352
|
+
record = self.find_record(record_id)
|
353
|
+
record.from_scim!(scim_hash: scim_resource.as_json())
|
354
|
+
self.save!(record)
|
355
|
+
# Evaluate to the record as a SCIM object (or do that via "self.save!")
|
356
|
+
user.to_scim(location: url_for(action: :show, id: record_id))
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
def update
|
361
|
+
super do |record_id, patch_hash|
|
362
|
+
# Partially update an instance based on the PATCH payload *Hash* given
|
363
|
+
# in "patch_hash" (note that unlike the "scim_resource" parameter given
|
364
|
+
# to blocks in #create or #replace, this is *not* a high-level object).
|
365
|
+
record = self.find_record(record_id)
|
366
|
+
record.from_scim_patch!(patch_hash: patch_hash)
|
367
|
+
self.save!(record)
|
368
|
+
# Evaluate to the record as a SCIM object (or do that via "self.save!")
|
369
|
+
user.to_scim(location: url_for(action: :show, id: record_id))
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
def destroy
|
374
|
+
super do |user_id|
|
375
|
+
user = find_user(user_id)
|
376
|
+
user.delete
|
377
|
+
end
|
378
|
+
end
|
379
|
+
|
380
|
+
protected
|
381
|
+
|
382
|
+
# The class including Scimitar::Resources::Mixin which declares mappings
|
383
|
+
# to the entity you return in #resource_type.
|
384
|
+
#
|
385
|
+
def storage_class
|
386
|
+
User
|
387
|
+
end
|
388
|
+
|
389
|
+
# Find your user. The +id+ parameter is one of YOUR identifiers, which
|
390
|
+
# are returned in "id" fields in JSON responses via SCIM schema. If the
|
391
|
+
# remote caller (client) doesn't want to remember your IDs and hold a
|
392
|
+
# mapping to their IDs, then they do an index with filter on their own
|
393
|
+
# "externalId" value and retrieve your "id" from that response.
|
394
|
+
#
|
395
|
+
def find_user(id)
|
396
|
+
# Find records by your ID here.
|
397
|
+
end
|
398
|
+
|
399
|
+
# Persist 'user' - for example, if we *were* using ActiveRecord...
|
400
|
+
#
|
401
|
+
def save!(user)
|
402
|
+
user.save!
|
403
|
+
rescue ActiveRecord::RecordInvalid => exception
|
404
|
+
raise Scimitar::ResourceInvalidError.new(record.errors.full_messages.join('; '))
|
405
|
+
end
|
406
|
+
|
407
|
+
end
|
408
|
+
|
409
|
+
```
|
410
|
+
|
411
|
+
Note that the [`Scimitar::ApplicationController` parent class](https://www.rubydoc.info/gems/scimitar/Scimitar/ApplicationController) of `Scimitar::ResourcesController` has a few methods to help with handling exceptions and rendering them as SCIM responses; for example, if a resource were not found by ID, you might wish to use [`Scimitar::ApplicationController#handle_resource_not_found`](https://github.com/RIPAGlobal/scimitar/blob/v1.0.3/app/controllers/scimitar/application_controller.rb#L22).
|
412
|
+
|
413
|
+
### Extension schema
|
414
|
+
|
415
|
+
You can extend schema with custom data by defining an extension class and calling `::extend_schema` on the SCIM resource class to which the extension applies. These extension classes:
|
416
|
+
|
417
|
+
* Must subclass `Scimitar::Schema::Base`
|
418
|
+
* Must call `super` in `def initialize`, providing data as shown in the example below
|
419
|
+
* Must define class methods for `::id` and `::scim_attributes`
|
420
|
+
|
421
|
+
The `::id` class method defines a unique schema ID that is used to namespace payloads or paths in JSON responses describing extended resources, JSON payloads creating them or PATCH paths modifying them. The SCIM RFCs would refer to this as the URN. For example, we might choose to use the [RFC-defined User extension schema](https://tools.ietf.org/html/rfc7643#section-4.3) to define a couple of extra fields our User model happens to support:
|
422
|
+
|
423
|
+
```ruby
|
424
|
+
class UserEnterpriseExtension < Scimitar::Schema::Base
|
425
|
+
def initialize(options = {})
|
426
|
+
super(
|
427
|
+
name: 'ExtendedUser',
|
428
|
+
description: 'Enterprise extension for a User',
|
429
|
+
id: self.class.id,
|
430
|
+
scim_attributes: self.class.scim_attributes
|
431
|
+
)
|
432
|
+
end
|
433
|
+
|
434
|
+
def self.id
|
435
|
+
'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'
|
436
|
+
end
|
437
|
+
|
438
|
+
def self.scim_attributes
|
439
|
+
[
|
440
|
+
Scimitar::Schema::Attribute.new(name: 'organization', type: 'string'),
|
441
|
+
Scimitar::Schema::Attribute.new(name: 'department', type: 'string')
|
442
|
+
]
|
443
|
+
end
|
444
|
+
end
|
445
|
+
```
|
446
|
+
|
447
|
+
...with the `super` call providing your choice of `name` and `description`, but also always providing `id` and `scim_attributes` as shown above. The class name chosen here is just an example and the class can be put inside any level of wrapping namespaces you choose - it's *your* class that can be named however you like. The extension class is then applied to the SCIM User resource _globally in your application_ by calling:
|
448
|
+
|
449
|
+
```ruby
|
450
|
+
Scimitar::Resources::User.extend_schema(UserEnterpriseExtension)
|
451
|
+
```
|
452
|
+
|
453
|
+
This is often done in `config/initializers/scimitar.rb` to help make it very clear that extensions are globally available and remove the risk of SCIM resources somehow being referenced before schema extensions have been applied.
|
454
|
+
|
455
|
+
In `def self.scim_attributes_map` in the underlying data model, add any new fields - `organization` and `department` in this example - to map them to whatever the equivalent data model attributes are, just as you would do with any other resource fields. These are declared without any special nesting - for example:
|
456
|
+
|
457
|
+
```ruby
|
458
|
+
def self.scim_attributes_map
|
459
|
+
return {
|
460
|
+
id: :id,
|
461
|
+
externalId: :scim_uid,
|
462
|
+
userName: :username,
|
463
|
+
# ...etc...
|
464
|
+
organization: :company,
|
465
|
+
department: :team
|
466
|
+
}
|
467
|
+
end
|
468
|
+
```
|
469
|
+
|
470
|
+
Whatever you provide in the `::id` method in your extension class will be used as a namespace in JSON data. This means that, for example, a SCIM representation of the above resource would look something like this:
|
471
|
+
|
472
|
+
```json
|
473
|
+
{
|
474
|
+
"schemas": [
|
475
|
+
"urn:ietf:params:scim:schemas:core:2.0:User",
|
476
|
+
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"
|
477
|
+
],
|
478
|
+
"id": "2819c223-7f76-453a-413861904646",
|
479
|
+
"externalId": "701984",
|
480
|
+
"userName": "bjensen@example.com",
|
481
|
+
// ...
|
482
|
+
|
483
|
+
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": {
|
484
|
+
"organization": "Corporation Incorporated",
|
485
|
+
"department": "Marketing",
|
486
|
+
},
|
487
|
+
// ...
|
488
|
+
}
|
489
|
+
```
|
490
|
+
|
491
|
+
...and likewise, creation via `POST` would require the same nesting if a caller wanted to create a resource instance with those extended properties set (and RFC-compliant consumers of your SCIM API should already be doing this). For `PATCH` operations, [the `path` uses a _colon_ to separate the ID/URN part from the path](https://tools.ietf.org/html/rfc7644#section-3.10) rather than just using a dot as you might expect from the JSON nesting above:
|
492
|
+
|
493
|
+
```json
|
494
|
+
{
|
495
|
+
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
|
496
|
+
"Operations": [
|
497
|
+
{
|
498
|
+
"op": "replace",
|
499
|
+
"path": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:organization",
|
500
|
+
"value": "Sales"
|
501
|
+
}
|
502
|
+
]
|
503
|
+
}
|
504
|
+
```
|
505
|
+
|
506
|
+
Resource extensions can provide any fields you choose, under any ID/URN you choose, to either RFC-described resources or entirely custom SCIM resources. There are no hard-coded assumptions or other "magic" that might require you to only extend RFC-described resources with RFC-described extensions. Of course, if you use custom resources or custom extensions that are not described by the SCIM RFCs, then the SCIM API you provide may only work with custom-written API callers that are aware of your bespoke resources and/or extensions.
|
507
|
+
|
508
|
+
Extensions can also contain complex attributes such as groups. For instance, if you want the ability to write to groups from the User resource perspective (since 'groups' collection in a SCIM User resource is read-only), you can add one attribute to your extension like this:
|
509
|
+
|
510
|
+
```ruby
|
511
|
+
Scimitar::Schema::Attribute.new(name: "userGroups", multiValued: true, complexType: Scimitar::ComplexTypes::ReferenceGroup, mutability: "writeOnly"),
|
512
|
+
```
|
513
|
+
|
514
|
+
Then map it in your `scim_attributes_map`:
|
515
|
+
|
516
|
+
```ruby
|
517
|
+
userGroups: [
|
518
|
+
{
|
519
|
+
list: :groups,
|
520
|
+
find_with: ->(value) { Group.find(value["value"]) },
|
521
|
+
using: {
|
522
|
+
value: :id,
|
523
|
+
display: :name
|
524
|
+
}
|
525
|
+
}
|
526
|
+
]
|
527
|
+
```
|
528
|
+
|
529
|
+
And write to it like this:
|
530
|
+
|
531
|
+
```json
|
532
|
+
{
|
533
|
+
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
|
534
|
+
"Operations": [
|
535
|
+
{
|
536
|
+
"op": "replace",
|
537
|
+
"path": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:userGroups",
|
538
|
+
"value": [{ "value": "1" }]
|
539
|
+
}
|
540
|
+
]
|
541
|
+
}
|
542
|
+
```
|
543
|
+
|
544
|
+
## Security
|
545
|
+
|
546
|
+
One vital feature of SCIM is its authorisation and security model. The best resource I've found to describe this in any detail is [section 2 of the protocol RFC, 7644](https://tools.ietf.org/html/rfc7644#section-2).
|
547
|
+
|
548
|
+
Often, you'll find that bearer tokens are in use by SCIM API consumers, but the way in which this is used by that consumer in practice can vary a great deal. For example, suppose a corporation uses Microsoft Azure Active Directory to maintain a master database of employee details. Azure lets administrators [connect to SCIM endpoints](https://docs.microsoft.com/en-us/azure/active-directory/app-provisioning/how-provisioning-works) for services that this corporation might use. In all cases, bearer tokens are used.
|
549
|
+
|
550
|
+
* When the third party integration builds an app that it gets hosted in the Azure Marketplace, the token is obtained via full OAuth flow of some kind - the enterprise corporation would sign into your app by some OAuth UI mechanism you provide, which leads to a Bearer token being issued. Thereafter, the Azure system would quote this back to you in API calls via the `Authorization` HTTP header.
|
551
|
+
|
552
|
+
* If you are providing SCIM services as part of some wider service offering it might not make sense to go to the trouble of adding all the extra features and requirements for Marketplace inclusion. Fortunately, Microsoft support [addition of 'user-defined' enterprise "app" integrations](https://docs.microsoft.com/en-us/azure/active-directory/app-provisioning/use-scim-to-provision-users-and-groups#integrate-your-scim-endpoint-with-the-aad-scim-client) in Azure, so the administrator can set up and 'provision' your SCIM API endpoint. In _this_ case, the bearer token is just some string that you generate which they paste into the Azure AD UI. Clearly, then, this amounts to little more than a glorified password, but you can take steps to make sure that it's long, unguessable and potentially be some encrypted/encoded structure that allows you to make additional security checks on "your side" when you unpack the token as part of API request handling.
|
553
|
+
|
554
|
+
* HTTPS is obviously a given here and localhost integration during development is difficult; perhaps search around for things like POSTman collections to assist with development testing. Scimitar has a reasonably comprehensive internal test suite but it's only as good as the accuracy and reliability of the subclass code you write to "bridge the gap" between SCIM schema and actions, and your User/Group equivalent records and the operations you perform upon them. Microsoft provide [additional information](https://techcommunity.microsoft.com/t5/identity-standards-blog/provisioning-with-scim-design-build-and-test-your-scim-endpoint/ba-p/1204883) to help guide service provider implementors with best practice.
|
555
|
+
|
556
|
+
|
557
|
+
|
558
|
+
## Limitations
|
559
|
+
|
560
|
+
### Specification versus implementation
|
561
|
+
|
562
|
+
* The `name` complex type of a User has `givenName` and `familyName` fields which [the RFC 7643 core schema](https://tools.ietf.org/html/rfc7643#section-8.7.1) describes as optional. Scimitar marks these as required, in the belief that most user synchronisation scenarios between clients and a Scimitar-based provider would require at least those names for basic user management on the provider side, in conjunction with the in-spec-required `userName` field. That's only if the whole `name` type is given at all - at the top level, this itself remains optional per spec, but if you're going to bother specifying names at all, Scimitar wants at least those two pieces of data.
|
563
|
+
|
564
|
+
* Several complex types for User contain the same set of `value`, `display`, `type` and `primary` fields, all used in synonymous ways.
|
565
|
+
|
566
|
+
- The `value` field - which is e.g. an e-mail address or phone number - is described as optional by [the RFC 7643 core schema](https://tools.ietf.org/html/rfc7643#section-8.7.1), also using "SHOULD" rather than "MUST" in field descriptions elsewhere. Scimitar marks this as required by default, since there's not much point being sent (say) an e-mail section which has entries that don't provide the e-mail address. Some services might send `null` values here regardless so, if you need to be able to accept such data, you can set [engine configuration option `optional_value_fields_required`](https://github.com/RIPAGlobal/scimitar/blob/main/config/initializers/scimitar.rb) to `false`.
|
567
|
+
|
568
|
+
- The schema _descriptions_ for `display` declare that the field is something optionally sent by the service provider and state clearly that it is read-only - yet the formal schema declares it `readWrite`. Scimitar marks it as read-only.
|
569
|
+
|
570
|
+
* The `displayName` of a Group is described in [RFC 7643 section 4.2](https://tools.ietf.org/html/rfc7643#section-4.2) and in the free-text schema `description` field as required, but the schema nonetheless states `"required" : false` in the formal definition. We consider this to be an error and mark the property as `"required" : true`.
|
571
|
+
|
572
|
+
* In the `members` section of a [`Group` in the RFC 7643 core schema](https://tools.ietf.org/html/rfc7643#page-69), any member's `value` is noted as _not_ required but [the RFC also says](https://tools.ietf.org/html/rfc7643#section-4.2) "Service providers MAY require clients to provide a non-empty value by setting the "required" attribute characteristic of a sub-attribute of the "members" attribute in the "Group" resource schema". Scimitar does this. The `value` field would contain the `id` of a SCIM resource, which is the primary key on "our side" as a service provider. Just as we must store `externalId` values to maintain a mapping on "our side", we in turn _do_ require clients to provide our ID in group member lists via the `value` field.
|
573
|
+
|
574
|
+
* While the gem attempts to support difficult/complex filter strings via incorporating code and ideas in [SCIM Query Filter Parser](https://github.com/ingydotnet/scim-query-filter-parser-rb), it is possible that ActiveRecord / Rails precedence on some query operations in complex cases might not exactly match the SCIM specification. Please do submit a bug report if you encounter this. You may also wish to view [`query_parser_spec.rb`](https://github.com/RIPAGlobal/scimitar/blob/main/spec/models/scimitar/lists/query_parser_spec.rb) to get an idea of the tested examples - more interesting test cases are in the "`context 'with complex cases' do`" section.
|
575
|
+
|
576
|
+
* Group resource examples show the `members` array including field `display`, but this is not in the [formal schema](https://tools.ietf.org/html/rfc7643#page-69); Scimitar includes it in the Group definition.
|
577
|
+
|
578
|
+
* `POST` actions with only a subset of attributes specified treat missing attributes "to be cleared" for anything that's mapped for the target model. If you have defaults established at instantiation rather than (say) before-validation, you'll need to override `Scimitar::ActiveRecordBackedResourcesController#create` (if using that controller as a base class) as normally the controller just instantiates a model, applies _all_ attributes (with any mapped attribute values without an inbound value set to `nil`), then saves the record. This might cause default values to be overwritten. For consistency, `PUT` operations apply the same behaviour. The decision on this optional specification aspect is in part constrained by the difficulties of implementing `PATCH`.
|
579
|
+
|
580
|
+
* [RFC 7644 indicates](https://tools.ietf.org/html/rfc7644#page-35) that a resource might only return its core schema in the `schemas` attribute if it was created without any extension fields used. Only if e.g. a subsequent `PATCH` operation added data provided by extension schema, would that extension also appear in `schemas`. This behaviour is extremely difficult to implement and Scimitar does not try - it will always return a resource's core schema and any/all defined extension schemas in the `schemas` array at all times.
|
581
|
+
|
582
|
+
If you believe choices made in this section may be incorrect, please [create a GitHub issue](https://github.com/RIPAGlobal/scimitar/issues/new) describing the problem.
|
583
|
+
|
584
|
+
### Omissions
|
585
|
+
|
586
|
+
* Bulk operations are not supported.
|
587
|
+
|
588
|
+
* List ("index") endpoint [filters in SCIM](https://tools.ietf.org/html/rfc7644#section-3.4.2.2) are _extremely_ complicated. There is a syntax for specifying equals, not-equals, precedence through parentheses and things like "and"/"or"/"not" along the lines of "attribute operator value", which Scimitar supports to a reasonably comprehensive degree but with some limitations discussed shortly. That aside, it isn't at all clear what some of the [examples in the RFC](https://tools.ietf.org/html/rfc7644#page-23) are even meant to mean. Consider:
|
589
|
+
|
590
|
+
- `filter=userType eq "Employee" and (emails co "example.com" or emails.value co "example.org")`
|
591
|
+
|
592
|
+
It's very strange just specifying `emails co...`, since this is an Array which contains complex types. Is the filter there meant to try and match every attribute of the nested types in all array entries? I.e. if `type` happened to contain `example.com`, is that meant to match? It's strongly implied, because the next part of the filter specifically says `emails.value`. Again, we have to reach a little and assume that `emails.value` means "in _any_ of the objects in the `emails` Array, match all things where `value` contains `example.org`. It seems likely that this is a specification error and both of the specifiers should be `emails.value`.
|
593
|
+
|
594
|
+
Adding even more complexity - the specification shows filters _which include filters within them_. In the same way that PATCH operations use paths to identify attributes not just by name, but by filter matches within collections - e.g. `emails[type eq "work"]`, for all e-mail objects inside the `emails` array with a `type` attribute that has a value of `work`) - so also can a filter _contain a filter_, which isn't supported. So, this [example from the RFC](https://tools.ietf.org/html/rfc7644#page-23) is not supported by Scimitar:
|
595
|
+
|
596
|
+
- `filter=userType eq "Employee" and emails[type eq "work" and value co "@example.com"]`
|
597
|
+
|
598
|
+
Another filter shows a potential workaround:
|
599
|
+
|
600
|
+
- `filter=userType eq "Employee" and (emails.type eq "work")`
|
601
|
+
|
602
|
+
...which is just a match on `emails.type`, so if you have a queryable attribute mapping defined for `emails.type`, that would become queryable. Likewise, you could rewrite the more complex prior example thus:
|
603
|
+
|
604
|
+
- `filter=userType eq "Employee" and emails.type eq "work" and emails.value co "@example.com"`
|
605
|
+
|
606
|
+
...so adding a mapping for `emails.value` would then allow a database query to be constructed.
|
607
|
+
|
608
|
+
* Currently filtering for lists is always matched case-insensitive regardless of schema declarations that might indicate otherwise, for `eq`, `ne`, `co`, `sw` and `ew` operators; for greater/less-thank style filters, case is maintained with simple `>`, `<` etc. database operations in use. The standard Group and User schema have `caseExact` set to `false` for just about anything readily queryable, so this hopefully would only ever potentially be an issue for custom schema.
|
609
|
+
|
610
|
+
* As an exception to the above, attributes `id`, `externalId` and `meta.*` are matched case-sensitive. Filters that use `eq` on such attributes will end up a comparison using `=` rather than e.g. `ILIKE` (arising from https://github.com/RIPAGlobal/scimitar/issues/36).
|
611
|
+
|
612
|
+
* The `PATCH` mechanism is supported, but where filters are included, only a single "attribute eq value" is permitted - no other operators or combinations. For example, a work e-mail address's value could be replaced by a PATCH patch of `emails[type eq "work"].value`. For in-path filters such as this, other operators such as `ne` are not supported; combinations with "and"/"or" are not supported; negation with "not" is not supported.
|
613
|
+
|
614
|
+
If you would like to see something listed in the session implemented, please [create a GitHub issue](https://github.com/RIPAGlobal/scimitar/issues/new) asking for it to be implemented, or if possible, implement the feature and send a Pull Request.
|
615
|
+
|
616
|
+
|
617
|
+
|
618
|
+
## Development
|
619
|
+
|
620
|
+
Install Ruby dependencies first:
|
621
|
+
|
622
|
+
```
|
623
|
+
bundle install
|
624
|
+
```
|
625
|
+
|
626
|
+
### Tests
|
627
|
+
|
628
|
+
For testing, two main options are available:
|
629
|
+
|
630
|
+
* The first option is running the project locally. This is also the recommended way, as running the tests on a variety of setups and platforms increases he chance of finding platform-specific or setup-specific bugs.
|
631
|
+
* The second option is utilising the existing Docker Compose setup provided in the project. You can use this if getting the project to work locally is hard or not feasible.
|
632
|
+
|
633
|
+
#### Testing on your machine
|
634
|
+
|
635
|
+
You will need to have PostgreSQL running. This database is chosen for tests to prove case-insensitive behaviour via detection of ILIKE in generated queries. Using SQLite would have resulted in a more conceptually self-contained test suite, but SQLite is case-insensitive by default and uses "LIKE" either way, making it hard to "see" if the query system is doing the right thing.
|
636
|
+
|
637
|
+
After `bundle install` and with PostgreSQL up, set up the test database with:
|
638
|
+
|
639
|
+
```shell
|
640
|
+
pushd spec/apps/dummy
|
641
|
+
RAILS_ENV=test bundle exec bin/rails db:drop db:create db:migrate
|
642
|
+
popd
|
643
|
+
```
|
644
|
+
|
645
|
+
...and thereafter, run tests with:
|
646
|
+
|
647
|
+
```
|
648
|
+
bundle exec rspec
|
649
|
+
```
|
650
|
+
|
651
|
+
You can get an idea of arising test coverage by opening `coverage/index.html` in your preferred web browser.
|
652
|
+
|
653
|
+
#### Testing with Docker (Compose)
|
654
|
+
|
655
|
+
In order to be able to utilise the Docker Compose setup, you will need to have Docker installed with the Compose plugin. For an easy installation of Docker (with a GUI and the Compose plugin preinstalled) please see [Docker Desktop](https://www.docker.com/products/docker-desktop/).
|
656
|
+
|
657
|
+
In order to configure the Docker image, run `docker compose build` in a terminal of your choice, in the root of this project. This will download the required image and install the required libraries. After this is complete, running the tests is as easy as running the command `docker compose up test`.
|
658
|
+
|
659
|
+
As mentioned in the previous section, test coverage may be analysed using `coverage/index.html` after running the project.
|
660
|
+
|
661
|
+
You can also open a raw terminal in this test container by running `docker run --rm test sh`. For more Compose commands, please refer to [the Docker Compose reference manual](https://docs.docker.com/compose/reference/).
|
662
|
+
|
663
|
+
### Internal documentation
|
664
|
+
|
665
|
+
Locally generated RDoc HTML seems to contain a more comprehensive and inter-linked set of pages than those available from `rubydoc.info`. You can (re)generate the internal [`rdoc` documentation](https://ruby-doc.org/stdlib-2.4.1/libdoc/rdoc/rdoc/RDoc/Markup.html#label-Supported+Formats) with:
|
666
|
+
|
667
|
+
```shell
|
668
|
+
bundle exec rake rerdoc
|
669
|
+
```
|
670
|
+
|
671
|
+
...yes, that's `rerdoc` - Re-R-Doc - then open `docs/rdoc/index.html`.
|
@@ -158,8 +158,8 @@ module Scimitar
|
|
158
158
|
# If you just let this superclass handle things, it'll call the standard
|
159
159
|
# +#save!+ method on the record. If you pass a block, then this block is
|
160
160
|
# invoked and passed the ActiveRecord model instance to be saved. You can
|
161
|
-
# then do things like calling a different method, using a service object
|
162
|
-
# some kind, perform audit-related operations and so-on.
|
161
|
+
# then do things like calling a different method, using a service object
|
162
|
+
# of some kind, perform audit-related operations and so-on.
|
163
163
|
#
|
164
164
|
# The return value is not used internally, making life easier for
|
165
165
|
# overriding subclasses to "do the right thing" / avoid mistakes (instead
|
@@ -177,7 +177,8 @@ module Scimitar
|
|
177
177
|
handle_invalid_record(exception.record)
|
178
178
|
end
|
179
179
|
|
180
|
-
# Deal with validation errors by responding with an appropriate SCIM
|
180
|
+
# Deal with validation errors by responding with an appropriate SCIM
|
181
|
+
# error.
|
181
182
|
#
|
182
183
|
# +record+:: The record with validation errors.
|
183
184
|
#
|
@@ -124,8 +124,13 @@ module Scimitar
|
|
124
124
|
#
|
125
125
|
# https://stackoverflow.com/questions/10239970/what-is-the-delimiter-for-www-authenticate-for-multiple-schemes
|
126
126
|
#
|
127
|
-
response.set_header('
|
128
|
-
response.set_header('
|
127
|
+
response.set_header('WWW-Authenticate', 'Basic' ) if Scimitar.engine_configuration.basic_authenticator.present?
|
128
|
+
response.set_header('WWW-Authenticate', 'Bearer') if Scimitar.engine_configuration.token_authenticator.present?
|
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")
|
129
134
|
end
|
130
135
|
|
131
136
|
def authenticate
|
@@ -105,9 +105,9 @@ module Scimitar
|
|
105
105
|
|
106
106
|
def simple_type?(value)
|
107
107
|
(type == 'string' && value.is_a?(String)) ||
|
108
|
-
|
109
|
-
|
110
|
-
|
108
|
+
(type == 'boolean' && (value.is_a?(TrueClass) || value.is_a?(FalseClass))) ||
|
109
|
+
(type == 'integer' && (value.is_a?(Integer))) ||
|
110
|
+
(type == 'dateTime' && valid_date_time?(value))
|
111
111
|
end
|
112
112
|
|
113
113
|
def valid_date_time?(value)
|
@@ -37,11 +37,11 @@ Rails.application.config.to_prepare do # (required for >= Rails 7 / Zeitwerk)
|
|
37
37
|
#
|
38
38
|
Scimitar.engine_configuration = Scimitar::EngineConfiguration.new({
|
39
39
|
|
40
|
-
# If you have filters you want to run for any Scimitar action/route, you
|
41
|
-
# define them here. You can also override any shared controller methods
|
40
|
+
# If you have filters you want to run for any Scimitar action/route, you
|
41
|
+
# can define them here. You can also override any shared controller methods
|
42
42
|
# here. For example, you might use a before-action to set up some
|
43
43
|
# multi-tenancy related state, skip Rails CSRF token verification, or
|
44
|
-
#
|
44
|
+
# customise how Scimitar generates URLs:
|
45
45
|
#
|
46
46
|
# application_controller_mixin: Module.new do
|
47
47
|
# def self.included(base)
|
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 = '2.6.
|
6
|
+
VERSION = '2.6.1'
|
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 = '2023-11-
|
11
|
+
DATE = '2023-11-15'
|
12
12
|
|
13
13
|
end
|
@@ -23,8 +23,8 @@ Rails.application.routes.draw do
|
|
23
23
|
|
24
24
|
# For testing blocks passed to ActiveRecordBackedResourcesController#save!
|
25
25
|
#
|
26
|
-
post 'CustomSaveUsers',
|
27
|
-
get
|
26
|
+
post 'CustomSaveUsers', to: 'custom_save_mock_users#create'
|
27
|
+
get 'CustomSaveUsers/:id', to: 'custom_save_mock_users#show'
|
28
28
|
|
29
29
|
# For testing environment inside Scimitar::ApplicationController subclasses.
|
30
30
|
#
|
@@ -24,7 +24,7 @@ RSpec.describe Scimitar::ApplicationController do
|
|
24
24
|
get :index, params: { format: :scim }
|
25
25
|
expect(response).to be_ok
|
26
26
|
expect(JSON.parse(response.body)).to eql({ 'message' => 'cool, cool!' })
|
27
|
-
expect(response.headers['
|
27
|
+
expect(response.headers['WWW-Authenticate']).to eql('Basic')
|
28
28
|
end
|
29
29
|
|
30
30
|
it 'renders failure with bad password' do
|
@@ -84,7 +84,7 @@ RSpec.describe Scimitar::ApplicationController do
|
|
84
84
|
get :index, params: { format: :scim }
|
85
85
|
expect(response).to be_ok
|
86
86
|
expect(JSON.parse(response.body)).to eql({ 'message' => 'cool, cool!' })
|
87
|
-
expect(response.headers['
|
87
|
+
expect(response.headers['WWW-Authenticate']).to eql('Bearer')
|
88
88
|
end
|
89
89
|
|
90
90
|
it 'renders failure with bad token' do
|
@@ -27,7 +27,7 @@ RSpec.describe Scimitar::SchemasController do
|
|
27
27
|
expect(parsed_body['name']).to eql('User')
|
28
28
|
end
|
29
29
|
|
30
|
-
it 'includes the controller
|
30
|
+
it 'includes the controller customised schema location' do
|
31
31
|
get :index, params: { name: Scimitar::Schema::User.id, format: :scim }
|
32
32
|
expect(response).to be_ok
|
33
33
|
parsed_body = JSON.parse(response.body)
|
@@ -26,13 +26,17 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
26
26
|
|
27
27
|
context '#index' do
|
28
28
|
context 'with no items' do
|
29
|
-
|
29
|
+
before :each do
|
30
30
|
MockUser.delete_all
|
31
|
+
end
|
31
32
|
|
33
|
+
it 'returns empty list' do
|
32
34
|
expect_any_instance_of(MockUsersController).to receive(:index).once.and_call_original
|
33
35
|
get '/Users', params: { format: :scim }
|
34
36
|
|
35
|
-
expect(response.status).to eql(200)
|
37
|
+
expect(response.status ).to eql(200)
|
38
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
39
|
+
|
36
40
|
result = JSON.parse(response.body)
|
37
41
|
|
38
42
|
expect(result['totalResults']).to eql(0)
|
@@ -46,7 +50,9 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
46
50
|
it 'returns all items' do
|
47
51
|
get '/Users', params: { format: :scim }
|
48
52
|
|
49
|
-
expect(response.status).to eql(200)
|
53
|
+
expect(response.status ).to eql(200)
|
54
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
55
|
+
|
50
56
|
result = JSON.parse(response.body)
|
51
57
|
|
52
58
|
expect(result['totalResults']).to eql(3)
|
@@ -64,7 +70,9 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
64
70
|
it 'returns all items' do
|
65
71
|
get '/Groups', params: { format: :scim }
|
66
72
|
|
67
|
-
expect(response.status).to eql(200)
|
73
|
+
expect(response.status ).to eql(200)
|
74
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
75
|
+
|
68
76
|
result = JSON.parse(response.body)
|
69
77
|
|
70
78
|
expect(result['totalResults']).to eql(3)
|
@@ -84,7 +92,9 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
84
92
|
filter: 'name.givenName eq "FOO" and name.familyName pr and emails ne "home_1@test.com"'
|
85
93
|
}
|
86
94
|
|
87
|
-
expect(response.status).to eql(200)
|
95
|
+
expect(response.status ).to eql(200)
|
96
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
97
|
+
|
88
98
|
result = JSON.parse(response.body)
|
89
99
|
|
90
100
|
expect(result['totalResults']).to eql(1)
|
@@ -103,7 +113,9 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
103
113
|
filter: 'name.GIVENNAME eq "Foo" and name.Familyname pr and emails ne "home_1@test.com"'
|
104
114
|
}
|
105
115
|
|
106
|
-
expect(response.status).to eql(200)
|
116
|
+
expect(response.status ).to eql(200)
|
117
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
118
|
+
|
107
119
|
result = JSON.parse(response.body)
|
108
120
|
|
109
121
|
expect(result['totalResults']).to eql(1)
|
@@ -126,7 +138,9 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
126
138
|
filter: "id eq \"#{@u3.primary_key}\""
|
127
139
|
}
|
128
140
|
|
129
|
-
expect(response.status).to eql(200)
|
141
|
+
expect(response.status ).to eql(200)
|
142
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
143
|
+
|
130
144
|
result = JSON.parse(response.body)
|
131
145
|
|
132
146
|
expect(result['totalResults']).to eql(1)
|
@@ -145,7 +159,9 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
145
159
|
filter: "externalID eq \"#{@u2.scim_uid}\""
|
146
160
|
}
|
147
161
|
|
148
|
-
expect(response.status).to eql(200)
|
162
|
+
expect(response.status ).to eql(200)
|
163
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
164
|
+
|
149
165
|
result = JSON.parse(response.body)
|
150
166
|
|
151
167
|
expect(result['totalResults']).to eql(1)
|
@@ -164,7 +180,9 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
164
180
|
filter: "Meta.LastModified eq \"#{@u3.updated_at}\""
|
165
181
|
}
|
166
182
|
|
167
|
-
expect(response.status).to eql(200)
|
183
|
+
expect(response.status ).to eql(200)
|
184
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
185
|
+
|
168
186
|
result = JSON.parse(response.body)
|
169
187
|
|
170
188
|
expect(result['totalResults']).to eql(1)
|
@@ -184,7 +202,9 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
184
202
|
count: 2
|
185
203
|
}
|
186
204
|
|
187
|
-
expect(response.status).to eql(200)
|
205
|
+
expect(response.status ).to eql(200)
|
206
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
207
|
+
|
188
208
|
result = JSON.parse(response.body)
|
189
209
|
|
190
210
|
expect(result['totalResults']).to eql(3)
|
@@ -203,7 +223,9 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
203
223
|
startIndex: 2
|
204
224
|
}
|
205
225
|
|
206
|
-
expect(response.status).to eql(200)
|
226
|
+
expect(response.status ).to eql(200)
|
227
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
228
|
+
|
207
229
|
result = JSON.parse(response.body)
|
208
230
|
|
209
231
|
expect(result['totalResults']).to eql(3)
|
@@ -224,8 +246,11 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
224
246
|
filter: 'name.givenName'
|
225
247
|
}
|
226
248
|
|
227
|
-
expect(response.status).to eql(400)
|
249
|
+
expect(response.status ).to eql(400)
|
250
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
251
|
+
|
228
252
|
result = JSON.parse(response.body)
|
253
|
+
|
229
254
|
expect(result['scimType']).to eql('invalidFilter')
|
230
255
|
end
|
231
256
|
end # "context 'with bad calls' do"
|
@@ -239,7 +264,9 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
239
264
|
expect_any_instance_of(MockUsersController).to receive(:show).once.and_call_original
|
240
265
|
get "/Users/#{@u2.primary_key}", params: { format: :scim }
|
241
266
|
|
242
|
-
expect(response.status).to eql(200)
|
267
|
+
expect(response.status ).to eql(200)
|
268
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
269
|
+
|
243
270
|
result = JSON.parse(response.body)
|
244
271
|
|
245
272
|
expect(result['id']).to eql(@u2.primary_key.to_s)
|
@@ -254,7 +281,9 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
254
281
|
expect_any_instance_of(MockGroupsController).to receive(:show).once.and_call_original
|
255
282
|
get "/Groups/#{@g2.id}", params: { format: :scim }
|
256
283
|
|
257
|
-
expect(response.status).to eql(200)
|
284
|
+
expect(response.status ).to eql(200)
|
285
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
286
|
+
|
258
287
|
result = JSON.parse(response.body)
|
259
288
|
|
260
289
|
expect(result['id']).to eql(@g2.id.to_s) # Note - ID was converted String; not Integer
|
@@ -266,8 +295,11 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
266
295
|
it 'renders 404' do
|
267
296
|
get '/Users/xyz', params: { format: :scim }
|
268
297
|
|
269
|
-
expect(response.status).to eql(404)
|
298
|
+
expect(response.status ).to eql(404)
|
299
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
300
|
+
|
270
301
|
result = JSON.parse(response.body)
|
302
|
+
|
271
303
|
expect(result['status']).to eql('404')
|
272
304
|
end
|
273
305
|
end # "context '#show' do"
|
@@ -291,7 +323,9 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
291
323
|
mock_after = MockUser.all.to_a
|
292
324
|
new_mock = (mock_after - mock_before).first
|
293
325
|
|
294
|
-
expect(response.status).to eql(201)
|
326
|
+
expect(response.status ).to eql(201)
|
327
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
328
|
+
|
295
329
|
result = JSON.parse(response.body)
|
296
330
|
|
297
331
|
expect(result['id']).to eql(new_mock.primary_key.to_s)
|
@@ -332,7 +366,9 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
332
366
|
mock_after = MockUser.all.to_a
|
333
367
|
new_mock = (mock_after - mock_before).first
|
334
368
|
|
335
|
-
expect(response.status).to eql(201)
|
369
|
+
expect(response.status ).to eql(201)
|
370
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
371
|
+
|
336
372
|
result = JSON.parse(response.body)
|
337
373
|
|
338
374
|
expect(result['id']).to eql(new_mock.id.to_s)
|
@@ -363,8 +399,11 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
363
399
|
}
|
364
400
|
}.to_not change { MockUser.count }
|
365
401
|
|
366
|
-
expect(response.status).to eql(409)
|
402
|
+
expect(response.status ).to eql(409)
|
403
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
404
|
+
|
367
405
|
result = JSON.parse(response.body)
|
406
|
+
|
368
407
|
expect(result['scimType']).to eql('uniqueness')
|
369
408
|
expect(result['detail']).to include('already been taken')
|
370
409
|
end
|
@@ -377,8 +416,11 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
377
416
|
}
|
378
417
|
}.to_not change { MockUser.count }
|
379
418
|
|
380
|
-
expect(response.status).to eql(400)
|
419
|
+
expect(response.status ).to eql(400)
|
420
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
421
|
+
|
381
422
|
result = JSON.parse(response.body)
|
423
|
+
|
382
424
|
expect(result['scimType']).to eql('invalidValue')
|
383
425
|
expect(result['detail']).to include('is required')
|
384
426
|
end
|
@@ -391,7 +433,9 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
391
433
|
}
|
392
434
|
}.to_not change { MockUser.count }
|
393
435
|
|
394
|
-
expect(response.status).to eql(400)
|
436
|
+
expect(response.status ).to eql(400)
|
437
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
438
|
+
|
395
439
|
result = JSON.parse(response.body)
|
396
440
|
|
397
441
|
expect(result['scimType']).to eql('invalidValue')
|
@@ -410,7 +454,9 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
410
454
|
mock_after = MockUser.all.to_a
|
411
455
|
new_mock = (mock_after - mock_before).first
|
412
456
|
|
413
|
-
expect(response.status).to eql(201)
|
457
|
+
expect(response.status ).to eql(201)
|
458
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
459
|
+
|
414
460
|
expect(new_mock.username).to eql(CustomSaveMockUsersController::CUSTOM_SAVE_BLOCK_USERNAME_INDICATOR)
|
415
461
|
end
|
416
462
|
end # "context '#create' do"
|
@@ -428,7 +474,9 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
428
474
|
put "/Users/#{@u2.primary_key}", params: attributes.merge(format: :scim)
|
429
475
|
}.to_not change { MockUser.count }
|
430
476
|
|
431
|
-
expect(response.status).to eql(200)
|
477
|
+
expect(response.status ).to eql(200)
|
478
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
479
|
+
|
432
480
|
result = JSON.parse(response.body)
|
433
481
|
|
434
482
|
expect(result['id']).to eql(@u2.primary_key.to_s)
|
@@ -459,8 +507,11 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
459
507
|
}
|
460
508
|
}.to_not change { MockUser.count }
|
461
509
|
|
462
|
-
expect(response.status).to eql(400)
|
510
|
+
expect(response.status ).to eql(400)
|
511
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
512
|
+
|
463
513
|
result = JSON.parse(response.body)
|
514
|
+
|
464
515
|
expect(result['scimType']).to eql('invalidValue')
|
465
516
|
expect(result['detail']).to include('is required')
|
466
517
|
|
@@ -480,7 +531,9 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
480
531
|
}
|
481
532
|
}.to_not change { MockUser.count }
|
482
533
|
|
483
|
-
expect(response.status).to eql(400)
|
534
|
+
expect(response.status ).to eql(400)
|
535
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
536
|
+
|
484
537
|
result = JSON.parse(response.body)
|
485
538
|
|
486
539
|
expect(result['scimType']).to eql('invalidValue')
|
@@ -502,8 +555,11 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
502
555
|
}
|
503
556
|
}.to_not change { MockUser.count }
|
504
557
|
|
505
|
-
expect(response.status).to eql(404)
|
558
|
+
expect(response.status ).to eql(404)
|
559
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
560
|
+
|
506
561
|
result = JSON.parse(response.body)
|
562
|
+
|
507
563
|
expect(result['status']).to eql('404')
|
508
564
|
end
|
509
565
|
end # "context '#replace' do"
|
@@ -535,7 +591,9 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
535
591
|
patch "/Users/#{@u2.primary_key}", params: payload.merge(format: :scim)
|
536
592
|
}.to_not change { MockUser.count }
|
537
593
|
|
538
|
-
expect(response.status).to eql(200)
|
594
|
+
expect(response.status ).to eql(200)
|
595
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
596
|
+
|
539
597
|
result = JSON.parse(response.body)
|
540
598
|
|
541
599
|
expect(result['id']).to eql(@u2.primary_key.to_s)
|
@@ -572,7 +630,9 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
572
630
|
patch "/Users/#{@u2.primary_key}", params: payload.merge(format: :scim)
|
573
631
|
}.to_not change { MockUser.count }
|
574
632
|
|
575
|
-
expect(response.status).to eql(200)
|
633
|
+
expect(response.status ).to eql(200)
|
634
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
635
|
+
|
576
636
|
result = JSON.parse(response.body)
|
577
637
|
|
578
638
|
expect(result['id']).to eql(@u2.primary_key.to_s)
|
@@ -604,7 +664,9 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
604
664
|
patch "/Users/#{@u2.primary_key}", params: payload.merge(format: :scim)
|
605
665
|
}.to_not change { MockUser.count }
|
606
666
|
|
607
|
-
expect(response.status).to eql(200)
|
667
|
+
expect(response.status ).to eql(200)
|
668
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
669
|
+
|
608
670
|
result = JSON.parse(response.body)
|
609
671
|
|
610
672
|
expect(result['id']).to eql(@u2.primary_key.to_s)
|
@@ -636,7 +698,9 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
636
698
|
patch "/Users/#{@u2.primary_key}", params: payload.merge(format: :scim)
|
637
699
|
}.to_not change { MockUser.count }
|
638
700
|
|
639
|
-
expect(response.status).to eql(200)
|
701
|
+
expect(response.status ).to eql(200)
|
702
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
703
|
+
|
640
704
|
result = JSON.parse(response.body)
|
641
705
|
|
642
706
|
expect(result['id']).to eql(@u2.primary_key.to_s)
|
@@ -675,7 +739,9 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
675
739
|
}
|
676
740
|
}.to_not change { MockUser.count }
|
677
741
|
|
678
|
-
expect(response.status).to eql(400)
|
742
|
+
expect(response.status ).to eql(400)
|
743
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
744
|
+
|
679
745
|
result = JSON.parse(response.body)
|
680
746
|
|
681
747
|
expect(result['scimType']).to eql('invalidValue')
|
@@ -703,8 +769,11 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
703
769
|
}
|
704
770
|
}.to_not change { MockUser.count }
|
705
771
|
|
706
|
-
expect(response.status).to eql(404)
|
772
|
+
expect(response.status ).to eql(404)
|
773
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
774
|
+
|
707
775
|
result = JSON.parse(response.body)
|
776
|
+
|
708
777
|
expect(result['status']).to eql('404')
|
709
778
|
end
|
710
779
|
|
@@ -741,7 +810,9 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
741
810
|
|
742
811
|
get "/Groups/#{@g1.id}", params: { format: :scim }
|
743
812
|
|
744
|
-
expect(response.status).to eql(200)
|
813
|
+
expect(response.status ).to eql(200)
|
814
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
815
|
+
|
745
816
|
result = JSON.parse(response.body)
|
746
817
|
|
747
818
|
expect(result['members']).to be_empty
|
@@ -768,7 +839,9 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
768
839
|
|
769
840
|
get "/Groups/#{@g1.id}", params: { format: :scim }
|
770
841
|
|
771
|
-
expect(response.status).to eql(200)
|
842
|
+
expect(response.status ).to eql(200)
|
843
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
844
|
+
|
772
845
|
result = JSON.parse(response.body)
|
773
846
|
|
774
847
|
expect(result['members'].map { |m| m['value'] }.sort()).to eql(expected_remaining_user_ids)
|
@@ -879,8 +952,11 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
879
952
|
delete '/Users/xyz', params: { format: :scim }
|
880
953
|
}.to_not change { MockUser.count }
|
881
954
|
|
882
|
-
expect(response.status).to eql(404)
|
955
|
+
expect(response.status ).to eql(404)
|
956
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
957
|
+
|
883
958
|
result = JSON.parse(response.body)
|
959
|
+
|
884
960
|
expect(result['status']).to eql('404')
|
885
961
|
end
|
886
962
|
end # "context '#destroy' do"
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: scimitar
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.6.
|
4
|
+
version: 2.6.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- RIPA Global
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2023-11-
|
12
|
+
date: 2023-11-15 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rails
|
@@ -130,6 +130,8 @@ executables: []
|
|
130
130
|
extensions: []
|
131
131
|
extra_rdoc_files: []
|
132
132
|
files:
|
133
|
+
- LICENSE.txt
|
134
|
+
- README.md
|
133
135
|
- Rakefile
|
134
136
|
- app/controllers/scimitar/active_record_backed_resources_controller.rb
|
135
137
|
- app/controllers/scimitar/application_controller.rb
|