scimaenaga 0.4.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +314 -0
- data/Rakefile +34 -0
- data/app/controllers/concerns/scim_rails/exception_handler.rb +119 -0
- data/app/controllers/concerns/scim_rails/response.rb +94 -0
- data/app/controllers/scim_rails/application_controller.rb +72 -0
- data/app/controllers/scim_rails/scim_groups_controller.rb +96 -0
- data/app/controllers/scim_rails/scim_users_controller.rb +124 -0
- data/app/helpers/scim_rails/application_helper.rb +4 -0
- data/app/models/scim_rails/application_record.rb +5 -0
- data/app/models/scim_rails/authorize_api_request.rb +40 -0
- data/app/models/scim_rails/scim_count.rb +38 -0
- data/app/models/scim_rails/scim_query_parser.rb +47 -0
- data/config/initializers/mime_types.rb +5 -0
- data/config/routes.rb +12 -0
- data/lib/generators/scim_rails/USAGE +8 -0
- data/lib/generators/scim_rails/scim_rails_generator.rb +7 -0
- data/lib/generators/scim_rails/templates/initializer.rb +166 -0
- data/lib/scim_rails/config.rb +85 -0
- data/lib/scim_rails/encoder.rb +25 -0
- data/lib/scim_rails/engine.rb +12 -0
- data/lib/scim_rails/version.rb +5 -0
- data/lib/scim_rails.rb +6 -0
- data/lib/tasks/scim_rails_tasks.rake +4 -0
- data/spec/controllers/scim_rails/scim_groups_controller_spec.rb +494 -0
- data/spec/controllers/scim_rails/scim_groups_request_spec.rb +68 -0
- data/spec/controllers/scim_rails/scim_users_controller_spec.rb +681 -0
- data/spec/controllers/scim_rails/scim_users_request_spec.rb +77 -0
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/app/assets/config/manifest.js +5 -0
- data/spec/dummy/app/assets/javascripts/application.js +13 -0
- data/spec/dummy/app/assets/javascripts/cable.js +13 -0
- data/spec/dummy/app/assets/stylesheets/application.css +15 -0
- data/spec/dummy/app/channels/application_cable/channel.rb +4 -0
- data/spec/dummy/app/channels/application_cable/connection.rb +4 -0
- data/spec/dummy/app/controllers/application_controller.rb +3 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/jobs/application_job.rb +2 -0
- data/spec/dummy/app/mailers/application_mailer.rb +4 -0
- data/spec/dummy/app/models/application_record.rb +3 -0
- data/spec/dummy/app/models/company.rb +4 -0
- data/spec/dummy/app/models/group.rb +15 -0
- data/spec/dummy/app/models/group_user.rb +6 -0
- data/spec/dummy/app/models/user.rb +39 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/app/views/layouts/mailer.html.erb +13 -0
- data/spec/dummy/app/views/layouts/mailer.text.erb +1 -0
- data/spec/dummy/bin/bundle +3 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/bin/setup +34 -0
- data/spec/dummy/bin/update +29 -0
- data/spec/dummy/config/application.rb +15 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/cable.yml +9 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +54 -0
- data/spec/dummy/config/environments/production.rb +86 -0
- data/spec/dummy/config/environments/test.rb +42 -0
- data/spec/dummy/config/initializers/application_controller_renderer.rb +6 -0
- data/spec/dummy/config/initializers/assets.rb +11 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/cookies_serializer.rb +5 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/dummy/config/initializers/inflections.rb +16 -0
- data/spec/dummy/config/initializers/mime_types.rb +4 -0
- data/spec/dummy/config/initializers/new_framework_defaults.rb +24 -0
- data/spec/dummy/config/initializers/scim_rails_config.rb +85 -0
- data/spec/dummy/config/initializers/session_store.rb +3 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +23 -0
- data/spec/dummy/config/puma.rb +47 -0
- data/spec/dummy/config/routes.rb +3 -0
- data/spec/dummy/config/secrets.yml +22 -0
- data/spec/dummy/config/spring.rb +6 -0
- data/spec/dummy/config.ru +5 -0
- data/spec/dummy/db/migrate/20181206184304_create_users.rb +15 -0
- data/spec/dummy/db/migrate/20181206184313_create_companies.rb +11 -0
- data/spec/dummy/db/migrate/20210423075859_create_groups.rb +10 -0
- data/spec/dummy/db/migrate/20210423075950_create_group_users.rb +10 -0
- data/spec/dummy/db/schema.rb +53 -0
- data/spec/dummy/db/seeds.rb +14 -0
- data/spec/dummy/public/404.html +67 -0
- data/spec/dummy/public/422.html +67 -0
- data/spec/dummy/public/500.html +66 -0
- data/spec/dummy/public/apple-touch-icon-precomposed.png +0 -0
- data/spec/dummy/public/apple-touch-icon.png +0 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/factories/company.rb +10 -0
- data/spec/factories/group.rb +11 -0
- data/spec/factories/user.rb +9 -0
- data/spec/lib/scim_rails/encoder_spec.rb +62 -0
- data/spec/spec_helper.rb +17 -0
- data/spec/support/auth_helper.rb +7 -0
- data/spec/support/factory_bot.rb +3 -0
- data/spec/support/scim_rails_config.rb +59 -0
- metadata +339 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: dea244dd3c4735fe8f156571c07c630dbae4cee67cf46c53757aa77d74afb135
|
4
|
+
data.tar.gz: 52cd9e9e2459231d3b415083f28bbac20beac60952d4e2dc6531412ee419492a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e48ad1a2a9108a211f9bb5d15a258292212556281f30403d0d6d5e19a2a3b3dda882f635e11bbde0260fb16bfde43b926e79ae77faac713e7c494189928609eb
|
7
|
+
data.tar.gz: 9d737fe588c932bae532792c141f54a67f5d952874ea7e32151c17d01cc1d6abf3ef4dd25a8639e029be0781b721c23d70cf468a9945d1e33f84a627552acb1d
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2018 Lessonly
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,314 @@
|
|
1
|
+
[![Tests](https://github.com/StudistCorporation/scim_rails/actions/workflows/test.yaml/badge.svg)](https://github.com/StudistCorporation/scim_rails/actions/workflows/test.yaml)
|
2
|
+
[![Inline docs](http://inch-ci.org/github/lessonly/scim_rails.svg?branch=master)](http://inch-ci.org/github/lessonly/scim_rails)
|
3
|
+
[![Maintainability](https://api.codeclimate.com/v1/badges/ddfb6a891d2f0d1122ae/maintainability)](https://codeclimate.com/github/lessonly/scim_rails/maintainability)
|
4
|
+
|
5
|
+
# ScimRails
|
6
|
+
|
7
|
+
NOTE: This Gem is not yet fully SCIM complaint. It was developed with the main function of interfacing with Okta. There are features of SCIM that this Gem does not implement as described in the SCIM documentation or that have been left out completely.
|
8
|
+
|
9
|
+
#### What is SCIM?
|
10
|
+
|
11
|
+
SCIM stands for System for Cross-domain Identity Management. At its core, it is a set of rules defining how apps should interact for the purpose of creating, updating, and deprovisioning users. SCIM requests and responses can be sent in XML or JSON and this Gem uses JSON for ease of readability.
|
12
|
+
|
13
|
+
To learn more about SCIM 2.0 you can read the documentation at [RFC 7643](https://tools.ietf.org/html/rfc7643) and [RFC 7644](https://tools.ietf.org/html/rfc7644).
|
14
|
+
|
15
|
+
The goal of the Gem is to offer a relatively painless way of adding SCIM 2.0 to your app. This Gem should be fully compatible with Okta's SCIM implementation. This project is ongoing and will hopefully be fully SCIM compliant in time. Pull requests that assist in meeting that goal are welcome!
|
16
|
+
|
17
|
+
## Installation
|
18
|
+
|
19
|
+
Add this line to your application's Gemfile:
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
gem 'scim_rails'
|
23
|
+
```
|
24
|
+
|
25
|
+
And then execute:
|
26
|
+
|
27
|
+
```bash
|
28
|
+
$ bundle
|
29
|
+
```
|
30
|
+
|
31
|
+
Or install it yourself as:
|
32
|
+
|
33
|
+
```bash
|
34
|
+
$ gem install scim_rails
|
35
|
+
```
|
36
|
+
|
37
|
+
Generate the config file with:
|
38
|
+
|
39
|
+
```bash
|
40
|
+
$ rails generate scim_rails config
|
41
|
+
```
|
42
|
+
|
43
|
+
The config file will be located at:
|
44
|
+
|
45
|
+
```
|
46
|
+
config/initializers/scim_rails_config.rb
|
47
|
+
```
|
48
|
+
|
49
|
+
Please update the config file with the models and attributes of your app.
|
50
|
+
|
51
|
+
Mount the gem in your routes file:
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
Application.routes.draw do
|
55
|
+
mount ScimRails::Engine => "/"
|
56
|
+
end
|
57
|
+
```
|
58
|
+
|
59
|
+
This will enable the following routes for the Gem to use:
|
60
|
+
|
61
|
+
| Request | Route |
|
62
|
+
|:-------:|:-------------------:|
|
63
|
+
| get | 'scim/v2/Users' |
|
64
|
+
| post | 'scim/v2/Users' |
|
65
|
+
| get | 'scim/v2/Users/:id' |
|
66
|
+
| put | 'scim/v2/Users/:id' |
|
67
|
+
| patch | 'scim/v2/Users/:id' |
|
68
|
+
|
69
|
+
Note: This Gem can be mounted to any path. For example:
|
70
|
+
|
71
|
+
```
|
72
|
+
https://scim.example.com/scim/v2/Users
|
73
|
+
https://www.example.com/scim/v2/Users
|
74
|
+
https://example.com/example/scim/v2/Users
|
75
|
+
```
|
76
|
+
|
77
|
+
## Usage
|
78
|
+
|
79
|
+
#### Content-Type
|
80
|
+
|
81
|
+
When sending requests to the server the `Content-Type` should be set to `application/scim+json` but will also respond to `application/json`.
|
82
|
+
|
83
|
+
All responses will be sent with a `Content-Type` of `application/scim+json`.
|
84
|
+
|
85
|
+
#### Authentication
|
86
|
+
|
87
|
+
This gem supports both basic and OAuth bearer authentication.
|
88
|
+
|
89
|
+
##### Basic Auth
|
90
|
+
###### Username
|
91
|
+
The config setting `basic_auth_model_searchable_attribute` is the model attribute used to authenticate as the `username`. It defaults to `:subdomain`.
|
92
|
+
|
93
|
+
Ensure it is unique to the model records.
|
94
|
+
|
95
|
+
###### Password
|
96
|
+
The config setting `basic_auth_model_authenticatable_attribute` is the model attribute used to authenticate as `password`. Defaults to `:api_token`.
|
97
|
+
|
98
|
+
Assuming the attribute is `:api_token`, generate the password using:
|
99
|
+
```ruby
|
100
|
+
token = ScimRails::Encoder.encode(company)
|
101
|
+
# use the token as password for requests
|
102
|
+
company.api_token = token # required
|
103
|
+
company.save! # don't forget to persist the company record
|
104
|
+
```
|
105
|
+
|
106
|
+
This is necessary irrespective of your authentication choice(s) - basic auth, oauth bearer or both.
|
107
|
+
|
108
|
+
###### Sample Request
|
109
|
+
|
110
|
+
```bash
|
111
|
+
$ curl -X GET 'http://username:password@localhost:3000/scim/v2/Users'
|
112
|
+
```
|
113
|
+
|
114
|
+
##### OAuth Bearer
|
115
|
+
|
116
|
+
###### Signing Algorithm
|
117
|
+
In the config settings, ensure you set `signing_algorithm` to a valid JWT signing algorithm, e.g "HS256". Defaults to `"none"` when not set.
|
118
|
+
|
119
|
+
###### Signing Secret
|
120
|
+
In the config settings, ensure you set `signing_secret` to a secret key that will be used to encode and decode tokens. Defaults to `nil` when not set.
|
121
|
+
|
122
|
+
If you have already generated the `api_token` in the "Basic Auth" section, then use that as your bearer token and ignore the steps below:
|
123
|
+
```ruby
|
124
|
+
token = ScimRails::Encoder.encode(company)
|
125
|
+
# use the token as bearer token for requests
|
126
|
+
company.api_token = token #required
|
127
|
+
company.save! # don't forget to persist the company record
|
128
|
+
```
|
129
|
+
|
130
|
+
##### Sample Request
|
131
|
+
|
132
|
+
```bash
|
133
|
+
$ curl -H 'Authorization: Bearer xxxxxxx.xxxxxx' -X GET 'http://localhost:3000/scim/v2/Users'
|
134
|
+
```
|
135
|
+
|
136
|
+
### List
|
137
|
+
|
138
|
+
##### All
|
139
|
+
|
140
|
+
Sample request:
|
141
|
+
|
142
|
+
```bash
|
143
|
+
$ curl -X GET 'http://username:password@localhost:3000/scim/v2/Users'
|
144
|
+
```
|
145
|
+
|
146
|
+
##### Pagination
|
147
|
+
|
148
|
+
This Gem provides two pagination filters; `startIndex` and `count`.
|
149
|
+
|
150
|
+
`startIndex` is the positional number you would like to start at. This parameter can accept any integer but anything less than 1 will be interpreted as 1. If you visualize an array with all your user records in the array, `startIndex` is basically what element you would like to start at. If you are familiar with SQL this parameter is directly correlated to the query offset. **The default value for this filter is 1.**
|
151
|
+
|
152
|
+
`count` is the number of records you would like present in the response. **The default value for this filter is 100.**
|
153
|
+
|
154
|
+
Sample request:
|
155
|
+
|
156
|
+
```bash
|
157
|
+
$ curl -X GET 'http://username:password@localhost:3000/scim/v2/Users?startIndex=38&count=44'
|
158
|
+
```
|
159
|
+
|
160
|
+
Pagination only really works with a determinate order. What that means is, every time you call the database you need to get the results in the exact same order. So the 4th record is _always_ the 4th record and never appears in a different position. If there is no order then records might show up on multiple pages. **The default order is by id** but this can be configured with `scim_users_list_order`.
|
161
|
+
|
162
|
+
The pagination filters may be used on their own or in addition to the query filters listed in the next section.
|
163
|
+
|
164
|
+
##### Querying
|
165
|
+
|
166
|
+
Currently the only filter supported is a single level `eq`. More operators can be added fairly easily in future releases. The SCIM RFC documents nested querying which is something we would like to implement in the future.
|
167
|
+
|
168
|
+
**Queryable attributes can be mapped in the configuration file.**
|
169
|
+
|
170
|
+
Supported filters:
|
171
|
+
|
172
|
+
```
|
173
|
+
filter=email eq test@example.com
|
174
|
+
fitler=userName eq test@example.com
|
175
|
+
filter=formattedName eq Test User
|
176
|
+
filter=id eq 1
|
177
|
+
```
|
178
|
+
|
179
|
+
Unsupported filter:
|
180
|
+
|
181
|
+
```
|
182
|
+
filter=(email eq test@example.com) or (userName eq test@example.com)
|
183
|
+
```
|
184
|
+
|
185
|
+
Sample request:
|
186
|
+
|
187
|
+
```bash
|
188
|
+
$ curl -X GET 'http://username:password@localhost:3000/scim/v2/Users?filter=formattedName%20eq%20%22Test%20User%22'
|
189
|
+
```
|
190
|
+
|
191
|
+
### Show
|
192
|
+
|
193
|
+
This response can be modified in the configuration file. The `user_schema` configuration supports any JSON structure and will transform any values by calling symbols against the user model. A sample SCIM compliant response looks like:
|
194
|
+
|
195
|
+
```
|
196
|
+
{
|
197
|
+
schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"],
|
198
|
+
id: "1",
|
199
|
+
userName: "test@example.com",
|
200
|
+
name: {
|
201
|
+
givenName: "Test",
|
202
|
+
familyName: "User"
|
203
|
+
},
|
204
|
+
emails: [
|
205
|
+
{
|
206
|
+
value: "test@example.com"
|
207
|
+
},
|
208
|
+
],
|
209
|
+
active: "true"
|
210
|
+
}
|
211
|
+
```
|
212
|
+
|
213
|
+
Sample request:
|
214
|
+
|
215
|
+
```bash
|
216
|
+
$ curl -X GET 'http://username:password@localhost:3000/scim/v2/Users/1'
|
217
|
+
```
|
218
|
+
|
219
|
+
### Create
|
220
|
+
|
221
|
+
The create request can receive any SCIM compliant JSON but can only be parsed with the configuration schema provided. What that means is that if your app receives a request to modify an attribute that is not listed in your `mutable_user_attributes` configuration it will ignore the parameter. In addition to needing to be included in the mutable attributes it also requires `mutable_user_attributes_schema` which defines where the Gem should look for a given attribute.
|
222
|
+
|
223
|
+
**Do not include attributes that you do not want modified** such as `id`. Any attributes can be provided in the `user_schema` configuration to be returned as part of the response but if they are not part of the `mutable_user_attributes_schema` then they cannot be modified.
|
224
|
+
|
225
|
+
Sample request:
|
226
|
+
|
227
|
+
```bash
|
228
|
+
$ curl -X POST 'http://username:password@localhost:3000/scim/v2/Users/' -d '{"schemas":["urn:ietf:params:scim:schemas:core:2.0:User"],"userName":"test@example.com","name":{"givenName":"Test","familyName":"User"},"emails":[{"primary":true,"value":"test@example.com","type":"work"}],"displayName":"Test User","active":true}' -H 'Content-Type: application/scim+json'
|
229
|
+
```
|
230
|
+
|
231
|
+
### Update
|
232
|
+
|
233
|
+
Update requests follow the same guidelines as create requests. The request is parsed for the mutable attributes provided in the configuration file and sent to the user model to update those attributes. This request expects a full representation of the object and any missing mutable attributes will send `nil` to the user model. If the attribute cannot be blank and sends a validation error, that error will be rescued and the response will be an appropriate SCIM error.
|
234
|
+
|
235
|
+
Sample request:
|
236
|
+
|
237
|
+
```bash
|
238
|
+
$ curl -X PUT 'http://username:password@localhost:3000/scim/v2/Users/1' -d '{"schemas":["urn:ietf:params:scim:schemas:core:2.0:User"],"userName":"test@example.com","name":{"givenName":"Test","familyName":"User"},"emails":[{"primary":true,"value":"test@example.com","type":"work"}],"displayName":"Test User","active":true}' -H 'Content-Type: application/scim+json'
|
239
|
+
```
|
240
|
+
|
241
|
+
### Deprovision / Reprovision
|
242
|
+
|
243
|
+
The PATCH request was implemented to work with Okta. Okta updates profiles with PUT and deprovisions / reprovisions with PATCH. This implementation of PATCH is not SCIM compliant as it does not update a single attribute on the user profile but instead only sends a status update request to the record.
|
244
|
+
|
245
|
+
We would like to implement PATCH to be fully SCIM compliant in future releases.
|
246
|
+
|
247
|
+
Sample request:
|
248
|
+
|
249
|
+
```bash
|
250
|
+
$ curl -X PATCH 'http://username:password@localhost:3000/scim/v2/Users/1' -d '{"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], "Operations": [{"op": "replace", "value": { "active": false }}]}' -H 'Content-Type: application/scim+json'
|
251
|
+
```
|
252
|
+
|
253
|
+
### Error Handling
|
254
|
+
|
255
|
+
By default, scim_rails will output any unhandled exceptions to your configured rails logs.
|
256
|
+
|
257
|
+
If you would like, you can supply a custom handler for exceptions in the initializer. The only requirement is that the value you supply responds to `#call`.
|
258
|
+
|
259
|
+
For example, you might want to notify Honeybadger:
|
260
|
+
|
261
|
+
```ruby
|
262
|
+
ScimRails.configure do |config|
|
263
|
+
config.on_error = ->(e) { Honeybadger.notify(e) }
|
264
|
+
end
|
265
|
+
```
|
266
|
+
|
267
|
+
## Contributing
|
268
|
+
|
269
|
+
### [Code of Conduct](https://github.com/lessonly/scim_rails/blob/master/CODE_OF_CONDUCT.md)
|
270
|
+
|
271
|
+
### Pull Requests
|
272
|
+
|
273
|
+
Pull requests are welcome and encouraged! Please follow the default template format.
|
274
|
+
|
275
|
+
[How to create a pull request from a fork.](https://help.github.com/articles/creating-a-pull-request-from-a-fork/)
|
276
|
+
|
277
|
+
### Getting Started
|
278
|
+
|
279
|
+
Clone (or fork) the project.
|
280
|
+
|
281
|
+
Navigate to the top level of the project directory in your console and run `bundle install`.
|
282
|
+
|
283
|
+
Proceed to setting up the dummy app.
|
284
|
+
|
285
|
+
#### Dummy App
|
286
|
+
|
287
|
+
This Gem contains a fully functional Rails application that lives in `/spec/dummy`.
|
288
|
+
|
289
|
+
In the console, navigate to the dummy app at `/spec/dummy`.
|
290
|
+
|
291
|
+
Next run `bin/setup` to setup the app. This will set up the gems and build the databases. The databases are local to the project.
|
292
|
+
|
293
|
+
Last run `bundle exec rails server`.
|
294
|
+
|
295
|
+
If you wish you may send CURL requests to the dummy server or send requests to it via Postman.
|
296
|
+
|
297
|
+
### Specs
|
298
|
+
|
299
|
+
Specs can be run with `rspec` at the top level of the project (if you run `rspec` and it shows zero specs try running `rspec` from a different directory).
|
300
|
+
|
301
|
+
All specs should be passing. (The dummy app will need to be setup first.)
|
302
|
+
|
303
|
+
## Current Maintainers
|
304
|
+
|
305
|
+
Maintainers:
|
306
|
+
- Are active contributors
|
307
|
+
- Help set project direction
|
308
|
+
- Merge contributions from contributors
|
309
|
+
|
310
|
+
- [@wernull](https://github.com/wernull)
|
311
|
+
- [@rreinhardt9](https://github.com/rreinhardt9)
|
312
|
+
|
313
|
+
## License
|
314
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'ScimRails'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.md')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
|
18
|
+
load 'rails/tasks/engine.rake'
|
19
|
+
|
20
|
+
|
21
|
+
load 'rails/tasks/statistics.rake'
|
22
|
+
|
23
|
+
|
24
|
+
Bundler::GemHelper.install_tasks
|
25
|
+
|
26
|
+
Dir[File.join(File.dirname(__FILE__), 'tasks/**/*.rake')].each {|f| load f }
|
27
|
+
|
28
|
+
require 'rspec/core'
|
29
|
+
require 'rspec/core/rake_task'
|
30
|
+
|
31
|
+
desc "Run all specs in spec directory (excluding plugin specs)"
|
32
|
+
RSpec::Core::RakeTask.new(:spec => 'app:db:test:prepare')
|
33
|
+
|
34
|
+
task :default => :spec
|
@@ -0,0 +1,119 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ScimRails
|
4
|
+
module ExceptionHandler
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
class InvalidCredentials < StandardError
|
8
|
+
end
|
9
|
+
|
10
|
+
class InvalidQuery < StandardError
|
11
|
+
end
|
12
|
+
|
13
|
+
class UnsupportedPatchRequest < StandardError
|
14
|
+
end
|
15
|
+
|
16
|
+
class UnsupportedDeleteRequest < StandardError
|
17
|
+
end
|
18
|
+
|
19
|
+
included do
|
20
|
+
if Rails.env.production?
|
21
|
+
rescue_from StandardError do |exception|
|
22
|
+
on_error = ScimRails.config.on_error
|
23
|
+
if on_error.respond_to?(:call)
|
24
|
+
on_error.call(exception)
|
25
|
+
else
|
26
|
+
Rails.logger.error(exception.inspect)
|
27
|
+
end
|
28
|
+
|
29
|
+
json_response(
|
30
|
+
{
|
31
|
+
schemas: ["urn:ietf:params:scim:api:messages:2.0:Error"],
|
32
|
+
status: "500"
|
33
|
+
},
|
34
|
+
:internal_server_error
|
35
|
+
)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
rescue_from ScimRails::ExceptionHandler::InvalidCredentials do
|
40
|
+
json_response(
|
41
|
+
{
|
42
|
+
schemas: ["urn:ietf:params:scim:api:messages:2.0:Error"],
|
43
|
+
detail: "Authorization failure. The authorization header is invalid or missing.",
|
44
|
+
status: "401"
|
45
|
+
},
|
46
|
+
:unauthorized
|
47
|
+
)
|
48
|
+
end
|
49
|
+
|
50
|
+
rescue_from ScimRails::ExceptionHandler::InvalidQuery do
|
51
|
+
json_response(
|
52
|
+
{
|
53
|
+
schemas: ["urn:ietf:params:scim:api:messages:2.0:Error"],
|
54
|
+
scimType: "invalidFilter",
|
55
|
+
detail: "The specified filter syntax was invalid, or the specified attribute and filter comparison combination is not supported.",
|
56
|
+
status: "400"
|
57
|
+
},
|
58
|
+
:bad_request
|
59
|
+
)
|
60
|
+
end
|
61
|
+
|
62
|
+
rescue_from ScimRails::ExceptionHandler::UnsupportedPatchRequest do
|
63
|
+
json_response(
|
64
|
+
{
|
65
|
+
schemas: ["urn:ietf:params:scim:api:messages:2.0:Error"],
|
66
|
+
detail: "Invalid PATCH request. This PATCH endpoint only supports deprovisioning and reprovisioning records.",
|
67
|
+
status: "422"
|
68
|
+
},
|
69
|
+
:unprocessable_entity
|
70
|
+
)
|
71
|
+
end
|
72
|
+
|
73
|
+
rescue_from ScimRails::ExceptionHandler::UnsupportedDeleteRequest do
|
74
|
+
json_response(
|
75
|
+
{
|
76
|
+
schemas: ["urn:ietf:params:scim:api:messages:2.0:Error"],
|
77
|
+
detail: "Delete operation is disabled for the requested resource.",
|
78
|
+
status: "501"
|
79
|
+
},
|
80
|
+
:not_implemented
|
81
|
+
)
|
82
|
+
end
|
83
|
+
|
84
|
+
rescue_from ActiveRecord::RecordNotFound do |e|
|
85
|
+
json_response(
|
86
|
+
{
|
87
|
+
schemas: ["urn:ietf:params:scim:api:messages:2.0:Error"],
|
88
|
+
detail: "Resource #{e.id} not found.",
|
89
|
+
status: "404"
|
90
|
+
},
|
91
|
+
:not_found
|
92
|
+
)
|
93
|
+
end
|
94
|
+
|
95
|
+
rescue_from ActiveRecord::RecordInvalid do |e|
|
96
|
+
case e.message
|
97
|
+
when /has already been taken/
|
98
|
+
json_response(
|
99
|
+
{
|
100
|
+
schemas: ["urn:ietf:params:scim:api:messages:2.0:Error"],
|
101
|
+
detail: e.message,
|
102
|
+
status: "409"
|
103
|
+
},
|
104
|
+
:conflict
|
105
|
+
)
|
106
|
+
else
|
107
|
+
json_response(
|
108
|
+
{
|
109
|
+
schemas: ["urn:ietf:params:scim:api:messages:2.0:Error"],
|
110
|
+
detail: e.message,
|
111
|
+
status: "422"
|
112
|
+
},
|
113
|
+
:unprocessable_entity
|
114
|
+
)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ScimRails
|
4
|
+
module Response
|
5
|
+
CONTENT_TYPE = "application/scim+json"
|
6
|
+
|
7
|
+
def json_response(object, status = :ok)
|
8
|
+
render \
|
9
|
+
json: object,
|
10
|
+
status: status,
|
11
|
+
content_type: CONTENT_TYPE
|
12
|
+
end
|
13
|
+
|
14
|
+
def json_scim_response(object:, status: :ok, counts: nil)
|
15
|
+
case params[:action]
|
16
|
+
when "index"
|
17
|
+
render \
|
18
|
+
json: list_response(object, counts),
|
19
|
+
status: status,
|
20
|
+
content_type: CONTENT_TYPE
|
21
|
+
when "show", "create", "put_update", "patch_update"
|
22
|
+
render \
|
23
|
+
json: object_response(object),
|
24
|
+
status: status,
|
25
|
+
content_type: CONTENT_TYPE
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def list_response(object, counts)
|
32
|
+
object = object
|
33
|
+
.order(:id)
|
34
|
+
.offset(counts.offset)
|
35
|
+
.limit(counts.limit)
|
36
|
+
{
|
37
|
+
schemas: [
|
38
|
+
"urn:ietf:params:scim:api:messages:2.0:ListResponse"
|
39
|
+
],
|
40
|
+
totalResults: counts.total,
|
41
|
+
startIndex: counts.start_index,
|
42
|
+
itemsPerPage: counts.limit,
|
43
|
+
Resources: list_objects(object)
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
def list_objects(objects)
|
48
|
+
objects.map do |object|
|
49
|
+
object_response(object)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def object_response(object)
|
54
|
+
schema = case object
|
55
|
+
when ScimRails.config.scim_users_model
|
56
|
+
ScimRails.config.user_schema
|
57
|
+
when ScimRails.config.scim_groups_model
|
58
|
+
ScimRails.config.group_schema
|
59
|
+
else
|
60
|
+
raise ScimRails::ExceptionHandler::InvalidQuery,
|
61
|
+
"Unknown model: #{object}"
|
62
|
+
end
|
63
|
+
find_value(object, schema)
|
64
|
+
end
|
65
|
+
|
66
|
+
# `find_value` is a recursive method that takes a "user" and a
|
67
|
+
# "user schema" and replaces any symbols in the schema with the
|
68
|
+
# corresponding value from the user. Given a schema with symbols,
|
69
|
+
# `find_value` will search through the object for the symbols,
|
70
|
+
# send those symbols to the model, and replace the symbol with
|
71
|
+
# the return value.
|
72
|
+
|
73
|
+
def find_value(object, schema)
|
74
|
+
case schema
|
75
|
+
when Hash
|
76
|
+
schema.each.with_object({}) do |(key, value), hash|
|
77
|
+
hash[key] = find_value(object, value)
|
78
|
+
end
|
79
|
+
when Array, ActiveRecord::Associations::CollectionProxy
|
80
|
+
schema.map do |value|
|
81
|
+
find_value(object, value)
|
82
|
+
end
|
83
|
+
when ScimRails.config.scim_users_model
|
84
|
+
find_value(schema, ScimRails.config.user_abbreviated_schema)
|
85
|
+
when ScimRails.config.scim_groups_model
|
86
|
+
find_value(schema, ScimRails.config.group_abbreviated_schema)
|
87
|
+
when Symbol
|
88
|
+
find_value(object, object.public_send(schema))
|
89
|
+
else
|
90
|
+
schema
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ScimRails
|
4
|
+
class ApplicationController < ActionController::API
|
5
|
+
include ActionController::HttpAuthentication::Basic::ControllerMethods
|
6
|
+
include ExceptionHandler
|
7
|
+
include Response
|
8
|
+
|
9
|
+
before_action :authorize_request
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def authorize_request
|
14
|
+
send(authentication_strategy) do |searchable_attribute, authentication_attribute|
|
15
|
+
authorization = AuthorizeApiRequest.new(
|
16
|
+
searchable_attribute: searchable_attribute,
|
17
|
+
authentication_attribute: authentication_attribute
|
18
|
+
)
|
19
|
+
@company = authorization.company
|
20
|
+
end
|
21
|
+
raise ScimRails::ExceptionHandler::InvalidCredentials if @company.blank?
|
22
|
+
end
|
23
|
+
|
24
|
+
def authentication_strategy
|
25
|
+
if request.headers["Authorization"]&.include?("Bearer")
|
26
|
+
:authenticate_with_oauth_bearer
|
27
|
+
else
|
28
|
+
:authenticate_with_http_basic
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def authenticate_with_oauth_bearer
|
33
|
+
authentication_attribute = request.headers["Authorization"].split.last
|
34
|
+
payload = ScimRails::Encoder.decode(authentication_attribute).with_indifferent_access
|
35
|
+
searchable_attribute = payload[ScimRails.config.basic_auth_model_searchable_attribute]
|
36
|
+
|
37
|
+
yield searchable_attribute, authentication_attribute
|
38
|
+
end
|
39
|
+
|
40
|
+
def find_value_for(attribute)
|
41
|
+
params.dig(*path_for(attribute))
|
42
|
+
end
|
43
|
+
|
44
|
+
# `path_for` is a recursive method used to find the "path" for
|
45
|
+
# `.dig` to take when looking for a given attribute in the
|
46
|
+
# params.
|
47
|
+
#
|
48
|
+
# Example: `path_for(:name)` should return an array that looks
|
49
|
+
# like [:names, 0, :givenName]. `.dig` can then use that path
|
50
|
+
# against the params to translate the :name attribute to "John".
|
51
|
+
|
52
|
+
def path_for(attribute, object = controller_schema, path = [])
|
53
|
+
at_path = path.empty? ? object : object.dig(*path)
|
54
|
+
return path if at_path == attribute
|
55
|
+
|
56
|
+
case at_path
|
57
|
+
when Hash
|
58
|
+
at_path.each do |key, _value|
|
59
|
+
found_path = path_for(attribute, object, [*path, key])
|
60
|
+
return found_path if found_path
|
61
|
+
end
|
62
|
+
nil
|
63
|
+
when Array
|
64
|
+
at_path.each_with_index do |_value, index|
|
65
|
+
found_path = path_for(attribute, object, [*path, index])
|
66
|
+
return found_path if found_path
|
67
|
+
end
|
68
|
+
nil
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|