scimaenaga 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (99) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +314 -0
  4. data/Rakefile +34 -0
  5. data/app/controllers/concerns/scim_rails/exception_handler.rb +119 -0
  6. data/app/controllers/concerns/scim_rails/response.rb +94 -0
  7. data/app/controllers/scim_rails/application_controller.rb +72 -0
  8. data/app/controllers/scim_rails/scim_groups_controller.rb +96 -0
  9. data/app/controllers/scim_rails/scim_users_controller.rb +124 -0
  10. data/app/helpers/scim_rails/application_helper.rb +4 -0
  11. data/app/models/scim_rails/application_record.rb +5 -0
  12. data/app/models/scim_rails/authorize_api_request.rb +40 -0
  13. data/app/models/scim_rails/scim_count.rb +38 -0
  14. data/app/models/scim_rails/scim_query_parser.rb +47 -0
  15. data/config/initializers/mime_types.rb +5 -0
  16. data/config/routes.rb +12 -0
  17. data/lib/generators/scim_rails/USAGE +8 -0
  18. data/lib/generators/scim_rails/scim_rails_generator.rb +7 -0
  19. data/lib/generators/scim_rails/templates/initializer.rb +166 -0
  20. data/lib/scim_rails/config.rb +85 -0
  21. data/lib/scim_rails/encoder.rb +25 -0
  22. data/lib/scim_rails/engine.rb +12 -0
  23. data/lib/scim_rails/version.rb +5 -0
  24. data/lib/scim_rails.rb +6 -0
  25. data/lib/tasks/scim_rails_tasks.rake +4 -0
  26. data/spec/controllers/scim_rails/scim_groups_controller_spec.rb +494 -0
  27. data/spec/controllers/scim_rails/scim_groups_request_spec.rb +68 -0
  28. data/spec/controllers/scim_rails/scim_users_controller_spec.rb +681 -0
  29. data/spec/controllers/scim_rails/scim_users_request_spec.rb +77 -0
  30. data/spec/dummy/Rakefile +6 -0
  31. data/spec/dummy/app/assets/config/manifest.js +5 -0
  32. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  33. data/spec/dummy/app/assets/javascripts/cable.js +13 -0
  34. data/spec/dummy/app/assets/stylesheets/application.css +15 -0
  35. data/spec/dummy/app/channels/application_cable/channel.rb +4 -0
  36. data/spec/dummy/app/channels/application_cable/connection.rb +4 -0
  37. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  38. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  39. data/spec/dummy/app/jobs/application_job.rb +2 -0
  40. data/spec/dummy/app/mailers/application_mailer.rb +4 -0
  41. data/spec/dummy/app/models/application_record.rb +3 -0
  42. data/spec/dummy/app/models/company.rb +4 -0
  43. data/spec/dummy/app/models/group.rb +15 -0
  44. data/spec/dummy/app/models/group_user.rb +6 -0
  45. data/spec/dummy/app/models/user.rb +39 -0
  46. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  47. data/spec/dummy/app/views/layouts/mailer.html.erb +13 -0
  48. data/spec/dummy/app/views/layouts/mailer.text.erb +1 -0
  49. data/spec/dummy/bin/bundle +3 -0
  50. data/spec/dummy/bin/rails +4 -0
  51. data/spec/dummy/bin/rake +4 -0
  52. data/spec/dummy/bin/setup +34 -0
  53. data/spec/dummy/bin/update +29 -0
  54. data/spec/dummy/config/application.rb +15 -0
  55. data/spec/dummy/config/boot.rb +5 -0
  56. data/spec/dummy/config/cable.yml +9 -0
  57. data/spec/dummy/config/database.yml +25 -0
  58. data/spec/dummy/config/environment.rb +5 -0
  59. data/spec/dummy/config/environments/development.rb +54 -0
  60. data/spec/dummy/config/environments/production.rb +86 -0
  61. data/spec/dummy/config/environments/test.rb +42 -0
  62. data/spec/dummy/config/initializers/application_controller_renderer.rb +6 -0
  63. data/spec/dummy/config/initializers/assets.rb +11 -0
  64. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  65. data/spec/dummy/config/initializers/cookies_serializer.rb +5 -0
  66. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  67. data/spec/dummy/config/initializers/inflections.rb +16 -0
  68. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  69. data/spec/dummy/config/initializers/new_framework_defaults.rb +24 -0
  70. data/spec/dummy/config/initializers/scim_rails_config.rb +85 -0
  71. data/spec/dummy/config/initializers/session_store.rb +3 -0
  72. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  73. data/spec/dummy/config/locales/en.yml +23 -0
  74. data/spec/dummy/config/puma.rb +47 -0
  75. data/spec/dummy/config/routes.rb +3 -0
  76. data/spec/dummy/config/secrets.yml +22 -0
  77. data/spec/dummy/config/spring.rb +6 -0
  78. data/spec/dummy/config.ru +5 -0
  79. data/spec/dummy/db/migrate/20181206184304_create_users.rb +15 -0
  80. data/spec/dummy/db/migrate/20181206184313_create_companies.rb +11 -0
  81. data/spec/dummy/db/migrate/20210423075859_create_groups.rb +10 -0
  82. data/spec/dummy/db/migrate/20210423075950_create_group_users.rb +10 -0
  83. data/spec/dummy/db/schema.rb +53 -0
  84. data/spec/dummy/db/seeds.rb +14 -0
  85. data/spec/dummy/public/404.html +67 -0
  86. data/spec/dummy/public/422.html +67 -0
  87. data/spec/dummy/public/500.html +66 -0
  88. data/spec/dummy/public/apple-touch-icon-precomposed.png +0 -0
  89. data/spec/dummy/public/apple-touch-icon.png +0 -0
  90. data/spec/dummy/public/favicon.ico +0 -0
  91. data/spec/factories/company.rb +10 -0
  92. data/spec/factories/group.rb +11 -0
  93. data/spec/factories/user.rb +9 -0
  94. data/spec/lib/scim_rails/encoder_spec.rb +62 -0
  95. data/spec/spec_helper.rb +17 -0
  96. data/spec/support/auth_helper.rb +7 -0
  97. data/spec/support/factory_bot.rb +3 -0
  98. data/spec/support/scim_rails_config.rb +59 -0
  99. 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