scimitar 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Rakefile +16 -0
- data/app/controllers/scimitar/active_record_backed_resources_controller.rb +180 -0
- data/app/controllers/scimitar/application_controller.rb +129 -0
- data/app/controllers/scimitar/resource_types_controller.rb +28 -0
- data/app/controllers/scimitar/resources_controller.rb +203 -0
- data/app/controllers/scimitar/schemas_controller.rb +16 -0
- data/app/controllers/scimitar/service_provider_configurations_controller.rb +8 -0
- data/app/models/scimitar/authentication_error.rb +9 -0
- data/app/models/scimitar/authentication_scheme.rb +18 -0
- data/app/models/scimitar/bulk.rb +8 -0
- data/app/models/scimitar/complex_types/address.rb +18 -0
- data/app/models/scimitar/complex_types/base.rb +41 -0
- data/app/models/scimitar/complex_types/email.rb +12 -0
- data/app/models/scimitar/complex_types/entitlement.rb +12 -0
- data/app/models/scimitar/complex_types/ims.rb +12 -0
- data/app/models/scimitar/complex_types/name.rb +12 -0
- data/app/models/scimitar/complex_types/phone_number.rb +12 -0
- data/app/models/scimitar/complex_types/photo.rb +12 -0
- data/app/models/scimitar/complex_types/reference_group.rb +12 -0
- data/app/models/scimitar/complex_types/reference_member.rb +12 -0
- data/app/models/scimitar/complex_types/role.rb +12 -0
- data/app/models/scimitar/complex_types/x509_certificate.rb +12 -0
- data/app/models/scimitar/engine_configuration.rb +24 -0
- data/app/models/scimitar/error_response.rb +20 -0
- data/app/models/scimitar/errors.rb +14 -0
- data/app/models/scimitar/filter.rb +11 -0
- data/app/models/scimitar/filter_error.rb +22 -0
- data/app/models/scimitar/invalid_syntax_error.rb +9 -0
- data/app/models/scimitar/lists/count.rb +64 -0
- data/app/models/scimitar/lists/query_parser.rb +730 -0
- data/app/models/scimitar/meta.rb +7 -0
- data/app/models/scimitar/not_found_error.rb +10 -0
- data/app/models/scimitar/resource_invalid_error.rb +9 -0
- data/app/models/scimitar/resource_type.rb +29 -0
- data/app/models/scimitar/resources/base.rb +159 -0
- data/app/models/scimitar/resources/group.rb +13 -0
- data/app/models/scimitar/resources/mixin.rb +964 -0
- data/app/models/scimitar/resources/user.rb +13 -0
- data/app/models/scimitar/schema/address.rb +24 -0
- data/app/models/scimitar/schema/attribute.rb +123 -0
- data/app/models/scimitar/schema/base.rb +86 -0
- data/app/models/scimitar/schema/derived_attributes.rb +24 -0
- data/app/models/scimitar/schema/email.rb +10 -0
- data/app/models/scimitar/schema/entitlement.rb +10 -0
- data/app/models/scimitar/schema/group.rb +27 -0
- data/app/models/scimitar/schema/ims.rb +10 -0
- data/app/models/scimitar/schema/name.rb +20 -0
- data/app/models/scimitar/schema/phone_number.rb +10 -0
- data/app/models/scimitar/schema/photo.rb +10 -0
- data/app/models/scimitar/schema/reference_group.rb +23 -0
- data/app/models/scimitar/schema/reference_member.rb +21 -0
- data/app/models/scimitar/schema/role.rb +10 -0
- data/app/models/scimitar/schema/user.rb +52 -0
- data/app/models/scimitar/schema/vdtp.rb +18 -0
- data/app/models/scimitar/schema/x509_certificate.rb +22 -0
- data/app/models/scimitar/service_provider_configuration.rb +49 -0
- data/app/models/scimitar/supportable.rb +14 -0
- data/app/views/layouts/scimitar/application.html.erb +14 -0
- data/config/initializers/scimitar.rb +82 -0
- data/config/routes.rb +6 -0
- data/lib/scimitar.rb +23 -0
- data/lib/scimitar/engine.rb +63 -0
- data/lib/scimitar/version.rb +13 -0
- data/spec/apps/dummy/app/controllers/custom_destroy_mock_users_controller.rb +24 -0
- data/spec/apps/dummy/app/controllers/custom_request_verifiers_controller.rb +30 -0
- data/spec/apps/dummy/app/controllers/mock_groups_controller.rb +13 -0
- data/spec/apps/dummy/app/controllers/mock_users_controller.rb +13 -0
- data/spec/apps/dummy/app/models/mock_group.rb +83 -0
- data/spec/apps/dummy/app/models/mock_user.rb +104 -0
- data/spec/apps/dummy/config/application.rb +17 -0
- data/spec/apps/dummy/config/boot.rb +2 -0
- data/spec/apps/dummy/config/environment.rb +2 -0
- data/spec/apps/dummy/config/environments/test.rb +15 -0
- data/spec/apps/dummy/config/initializers/cookies_serializer.rb +3 -0
- data/spec/apps/dummy/config/initializers/scimitar.rb +14 -0
- data/spec/apps/dummy/config/initializers/session_store.rb +3 -0
- data/spec/apps/dummy/config/routes.rb +24 -0
- data/spec/apps/dummy/db/migrate/20210304014602_create_mock_users.rb +15 -0
- data/spec/apps/dummy/db/migrate/20210308020313_create_mock_groups.rb +10 -0
- data/spec/apps/dummy/db/migrate/20210308044214_create_join_table_mock_groups_mock_users.rb +8 -0
- data/spec/apps/dummy/db/schema.rb +42 -0
- data/spec/controllers/scimitar/application_controller_spec.rb +173 -0
- data/spec/controllers/scimitar/resource_types_controller_spec.rb +94 -0
- data/spec/controllers/scimitar/resources_controller_spec.rb +247 -0
- data/spec/controllers/scimitar/schemas_controller_spec.rb +75 -0
- data/spec/controllers/scimitar/service_provider_configurations_controller_spec.rb +22 -0
- data/spec/models/scimitar/complex_types/address_spec.rb +19 -0
- data/spec/models/scimitar/complex_types/email_spec.rb +23 -0
- data/spec/models/scimitar/lists/count_spec.rb +147 -0
- data/spec/models/scimitar/lists/query_parser_spec.rb +763 -0
- data/spec/models/scimitar/resource_type_spec.rb +21 -0
- data/spec/models/scimitar/resources/base_spec.rb +289 -0
- data/spec/models/scimitar/resources/base_validation_spec.rb +61 -0
- data/spec/models/scimitar/resources/mixin_spec.rb +2127 -0
- data/spec/models/scimitar/resources/user_spec.rb +55 -0
- data/spec/models/scimitar/schema/attribute_spec.rb +80 -0
- data/spec/models/scimitar/schema/base_spec.rb +64 -0
- data/spec/models/scimitar/schema/group_spec.rb +87 -0
- data/spec/models/scimitar/schema/user_spec.rb +710 -0
- data/spec/requests/active_record_backed_resources_controller_spec.rb +569 -0
- data/spec/requests/application_controller_spec.rb +49 -0
- data/spec/requests/controller_configuration_spec.rb +17 -0
- data/spec/requests/engine_spec.rb +20 -0
- data/spec/spec_helper.rb +66 -0
- metadata +315 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: d7c0cb7d5ff4346954d9aab0d83ae22fcd75fcc456b9be54d74e22334cf4e273
|
4
|
+
data.tar.gz: 47adaec88f0418f18294fb19374be773cbfafa6c94b8870e38a349f70a4c53b4
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 59a7f529e86667e14de8a6c0a0b0bc9a26cd53b21b53656be8b2f37d31e4cddcc1d0fccdefe2ff695d31baf19764489903a59e61f31e863e78c544f5dd24d550
|
7
|
+
data.tar.gz: '09d32ab29c325fa047f9e77daababa2bc763b79ee97e3b0b961202b60901b3ace406f5f66f63fd5c56b015a86e368294d4ec1e4b98b3588e7f4b75c72280e9e3'
|
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rspec/core/rake_task'
|
3
|
+
require 'rdoc/task'
|
4
|
+
require 'sdoc'
|
5
|
+
|
6
|
+
RSpec::Core::RakeTask.new(:default) do | t |
|
7
|
+
end
|
8
|
+
|
9
|
+
Rake::RDocTask.new do | rd |
|
10
|
+
rd.rdoc_files.include('README.md', 'lib/**/*.rb', 'app/**/*.rb')
|
11
|
+
|
12
|
+
rd.title = 'Scimitar'
|
13
|
+
rd.main = 'README.md'
|
14
|
+
rd.rdoc_dir = 'docs/rdoc'
|
15
|
+
rd.generator = 'sdoc'
|
16
|
+
end
|
@@ -0,0 +1,180 @@
|
|
1
|
+
require_dependency "scimitar/application_controller"
|
2
|
+
|
3
|
+
module Scimitar
|
4
|
+
|
5
|
+
# An ActiveRecord-centric subclass of Scimitar::ResourcesController. See that
|
6
|
+
# class's documentation first, as it describes things that your subclass must
|
7
|
+
# do which apply equally to subclasses of this ActiveRecord-focused code.
|
8
|
+
#
|
9
|
+
# In addition to requirements mentioned above, your subclass MUST override
|
10
|
+
# protected method #storage_scope, returning an ActiveRecord::Relation which
|
11
|
+
# is used as a starting scope for any 'index' (list) views. This gives you an
|
12
|
+
# opportunity to apply things like is-active filters, apply soft deletion
|
13
|
+
# scopes, apply security scopes and so-on. For example:
|
14
|
+
#
|
15
|
+
# protected
|
16
|
+
# def storage_scope
|
17
|
+
# self.storage_class().where(is_deleted: false)
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
class ActiveRecordBackedResourcesController < ResourcesController
|
21
|
+
|
22
|
+
rescue_from ActiveRecord::RecordNotFound, with: :handle_resource_not_found # See Scimitar::ApplicationController
|
23
|
+
|
24
|
+
# GET (list)
|
25
|
+
#
|
26
|
+
def index
|
27
|
+
query = if params[:filter].blank?
|
28
|
+
self.storage_scope()
|
29
|
+
else
|
30
|
+
attribute_map = storage_class().new.scim_queryable_attributes()
|
31
|
+
parser = ::Scimitar::Lists::QueryParser.new(attribute_map)
|
32
|
+
|
33
|
+
parser.parse(params[:filter])
|
34
|
+
parser.to_activerecord_query(self.storage_scope())
|
35
|
+
end
|
36
|
+
|
37
|
+
pagination_info = scim_pagination_info(query.count())
|
38
|
+
|
39
|
+
page_of_results = query
|
40
|
+
.offset(pagination_info.offset)
|
41
|
+
.limit(pagination_info.limit)
|
42
|
+
.to_a()
|
43
|
+
|
44
|
+
super(pagination_info, page_of_results) do | record |
|
45
|
+
record.to_scim(location: url_for(action: :show, id: record.id))
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# GET/id (show)
|
50
|
+
#
|
51
|
+
def show
|
52
|
+
super do |record_id|
|
53
|
+
record = self.find_record(record_id)
|
54
|
+
record.to_scim(location: url_for(action: :show, id: record_id))
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# POST (create)
|
59
|
+
#
|
60
|
+
def create
|
61
|
+
super do |scim_resource|
|
62
|
+
self.storage_class().transaction do
|
63
|
+
record = self.storage_class().new
|
64
|
+
record.from_scim!(scim_hash: scim_resource.as_json())
|
65
|
+
self.save!(record)
|
66
|
+
record.to_scim(location: url_for(action: :show, id: record.id))
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# PUT (replace)
|
72
|
+
#
|
73
|
+
def replace
|
74
|
+
super do |record_id, scim_resource|
|
75
|
+
self.storage_class().transaction do
|
76
|
+
record = self.find_record(record_id)
|
77
|
+
record.from_scim!(scim_hash: scim_resource.as_json())
|
78
|
+
self.save!(record)
|
79
|
+
record.to_scim(location: url_for(action: :show, id: record.id))
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# PATCH (update)
|
85
|
+
#
|
86
|
+
def update
|
87
|
+
super do |record_id, patch_hash|
|
88
|
+
self.storage_class().transaction do
|
89
|
+
record = self.find_record(record_id)
|
90
|
+
record.from_scim_patch!(patch_hash: patch_hash)
|
91
|
+
self.save!(record)
|
92
|
+
record.to_scim(location: url_for(action: :show, id: record.id))
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# DELETE (remove)
|
98
|
+
#
|
99
|
+
# Deletion methods can vary quite a lot with ActiveRecord objects. If you
|
100
|
+
# just let this superclass handle things, it'll call:
|
101
|
+
#
|
102
|
+
# https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-destroy-21
|
103
|
+
#
|
104
|
+
# ...i.e. the standard delete-record-with-callbacks method. If you pass
|
105
|
+
# a block, then this block is invoked and passed the ActiveRecord model
|
106
|
+
# instance to be destroyed. You can then do things like soft-deletions,
|
107
|
+
# updating an "active" flag, perform audit-related operations and so-on.
|
108
|
+
#
|
109
|
+
def destroy(&block)
|
110
|
+
super do |record_id|
|
111
|
+
record = self.find_record(record_id)
|
112
|
+
|
113
|
+
if block_given?
|
114
|
+
yield(record)
|
115
|
+
else
|
116
|
+
record.destroy!
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# =========================================================================
|
122
|
+
# PROTECTED INSTANCE METHODS
|
123
|
+
# =========================================================================
|
124
|
+
#
|
125
|
+
protected
|
126
|
+
|
127
|
+
# Return an ActiveRecord::Relation used as the starting scope for #index
|
128
|
+
# lists and any 'find by ID' operation.
|
129
|
+
#
|
130
|
+
def storage_scope
|
131
|
+
raise NotImplementedError
|
132
|
+
end
|
133
|
+
|
134
|
+
# Find a RIP user record. Subclasses can override this if they need
|
135
|
+
# special lookup behaviour.
|
136
|
+
#
|
137
|
+
# +record_id+:: Record ID (SCIM schema 'id' value - "our" ID).
|
138
|
+
#
|
139
|
+
def find_record(record_id)
|
140
|
+
self.storage_scope().find(record_id)
|
141
|
+
end
|
142
|
+
|
143
|
+
# Save a record, dealing with validation exceptions by raising SCIM
|
144
|
+
# errors.
|
145
|
+
#
|
146
|
+
# +record+:: ActiveRecord subclass to save (via #save!).
|
147
|
+
#
|
148
|
+
# The return value is not used internally, making life easier for
|
149
|
+
# overriding subclasses to "do the right thing" / avoid mistakes (instead
|
150
|
+
# of e.g. requiring that a to-SCIM representation of 'record' is returned
|
151
|
+
# and relying upon this to generate correct response payloads - an early
|
152
|
+
# version of the gem did this and it caused a confusing subclass bug).
|
153
|
+
#
|
154
|
+
def save!(record)
|
155
|
+
record.save!
|
156
|
+
|
157
|
+
rescue ActiveRecord::RecordInvalid => exception
|
158
|
+
joined_errors = record.errors.full_messages.join('; ')
|
159
|
+
|
160
|
+
# https://tools.ietf.org/html/rfc7644#page-12
|
161
|
+
#
|
162
|
+
# If the service provider determines that the creation of the requested
|
163
|
+
# resource conflicts with existing resources (e.g., a "User" resource
|
164
|
+
# with a duplicate "userName"), the service provider MUST return HTTP
|
165
|
+
# status code 409 (Conflict) with a "scimType" error code of
|
166
|
+
# "uniqueness"
|
167
|
+
#
|
168
|
+
if record.errors.any? { | e | e.type == :taken }
|
169
|
+
raise Scimitar::ErrorResponse.new(
|
170
|
+
status: 409,
|
171
|
+
scimType: 'uniqueness',
|
172
|
+
detail: joined_errors
|
173
|
+
)
|
174
|
+
else
|
175
|
+
raise Scimitar::ResourceInvalidError.new(joined_errors)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
end
|
180
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
module Scimitar
|
2
|
+
class ApplicationController < ActionController::Base
|
3
|
+
|
4
|
+
rescue_from StandardError, with: :handle_unexpected_error
|
5
|
+
rescue_from ActionDispatch::Http::Parameters::ParseError, with: :handle_bad_json_error # Via "ActionDispatch::Request.parameter_parsers" block in lib/scimitar/engine.rb
|
6
|
+
rescue_from Scimitar::ErrorResponse, with: :handle_scim_error
|
7
|
+
|
8
|
+
before_action :require_scim
|
9
|
+
before_action :add_mandatory_response_headers
|
10
|
+
before_action :authenticate
|
11
|
+
|
12
|
+
if Scimitar.engine_configuration.application_controller_mixin
|
13
|
+
include Scimitar.engine_configuration.application_controller_mixin
|
14
|
+
end
|
15
|
+
|
16
|
+
# =========================================================================
|
17
|
+
# PROTECTED INSTANCE METHODS
|
18
|
+
# =========================================================================
|
19
|
+
#
|
20
|
+
protected
|
21
|
+
|
22
|
+
# You can use:
|
23
|
+
#
|
24
|
+
# rescue_from SomeException, with: :handle_resource_not_found
|
25
|
+
#
|
26
|
+
# ...to "globally" invoke this handler if you wish.
|
27
|
+
#
|
28
|
+
# +_exception+:: Exception instance (currently unused).
|
29
|
+
#
|
30
|
+
def handle_resource_not_found(_exception)
|
31
|
+
handle_scim_error(NotFoundError.new(params[:id]))
|
32
|
+
end
|
33
|
+
|
34
|
+
# This base controller uses:
|
35
|
+
#
|
36
|
+
# rescue_from Scimitar::ErrorResponse, with: :handle_scim_error
|
37
|
+
#
|
38
|
+
# ...to "globally" invoke this handler for all Scimitar errors (including
|
39
|
+
# subclasses).
|
40
|
+
#
|
41
|
+
# +error_response+:: Scimitar::ErrorResponse (or subclass) instance.
|
42
|
+
#
|
43
|
+
def handle_scim_error(error_response)
|
44
|
+
render json: error_response, status: error_response.status
|
45
|
+
end
|
46
|
+
|
47
|
+
# This base controller uses:
|
48
|
+
#
|
49
|
+
# rescue_from ActionDispatch::Http::Parameters::ParseError, with: :handle_bad_json_error
|
50
|
+
#
|
51
|
+
# ...to "globally" handle JSON errors implied by parse errors raised via
|
52
|
+
# the "ActionDispatch::Request.parameter_parsers" block in
|
53
|
+
# lib/scimitar/engine.rb.
|
54
|
+
#
|
55
|
+
# +exception+:: Exception instance.
|
56
|
+
#
|
57
|
+
def handle_bad_json_error(exception)
|
58
|
+
handle_scim_error(ErrorResponse.new(status: 400, detail: "Invalid JSON - #{exception.message}"))
|
59
|
+
end
|
60
|
+
|
61
|
+
# This base controller uses:
|
62
|
+
#
|
63
|
+
# rescue_from StandardError, with: :handle_unexpected_error
|
64
|
+
#
|
65
|
+
# ...to "globally" handle 500-style cases with a SCIM response.
|
66
|
+
#
|
67
|
+
# +exception+:: Exception instance.
|
68
|
+
#
|
69
|
+
def handle_unexpected_error(exception)
|
70
|
+
Rails.logger.error("#{exception.message}\n#{exception.backtrace}")
|
71
|
+
handle_scim_error(ErrorResponse.new(status: 500, detail: exception.message))
|
72
|
+
end
|
73
|
+
|
74
|
+
# =========================================================================
|
75
|
+
# PRIVATE INSTANCE METHODS
|
76
|
+
# =========================================================================
|
77
|
+
#
|
78
|
+
private
|
79
|
+
|
80
|
+
# Tries to be permissive in what it receives - ".scim" extensions or a
|
81
|
+
# Content-Type header (or both) lead to both being set up for the inbound
|
82
|
+
# request and subclass processing.
|
83
|
+
#
|
84
|
+
def require_scim
|
85
|
+
if request.content_type&.downcase == Mime::Type.lookup_by_extension(:scim).to_s
|
86
|
+
request.format = :scim
|
87
|
+
elsif request.format == :scim
|
88
|
+
request.headers['CONTENT_TYPE'] = Mime::Type.lookup_by_extension(:scim).to_s
|
89
|
+
else
|
90
|
+
handle_scim_error(ErrorResponse.new(status: 406, detail: "Only #{Mime::Type.lookup_by_extension(:scim)} type is accepted."))
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def add_mandatory_response_headers
|
95
|
+
|
96
|
+
# https://tools.ietf.org/html/rfc7644#section-2
|
97
|
+
#
|
98
|
+
# "...a SCIM service provider SHALL indicate supported HTTP
|
99
|
+
# authentication schemes via the "WWW-Authenticate" header."
|
100
|
+
#
|
101
|
+
# Rack may not handle an attempt to set two instances of the header and
|
102
|
+
# there is much debate on how to specify multiple methods in one header
|
103
|
+
# so we just let Token override Basic (since Token is much stronger, or
|
104
|
+
# at least has the potential to do so) if that's how Rack handles it.
|
105
|
+
#
|
106
|
+
# https://stackoverflow.com/questions/10239970/what-is-the-delimiter-for-www-authenticate-for-multiple-schemes
|
107
|
+
#
|
108
|
+
response.set_header('WWW_AUTHENTICATE', 'Basic' ) if Scimitar.engine_configuration.basic_authenticator.present?
|
109
|
+
response.set_header('WWW_AUTHENTICATE', 'Bearer') if Scimitar.engine_configuration.token_authenticator.present?
|
110
|
+
end
|
111
|
+
|
112
|
+
def authenticate
|
113
|
+
handle_scim_error(Scimitar::AuthenticationError.new) unless authenticated?
|
114
|
+
end
|
115
|
+
|
116
|
+
def authenticated?
|
117
|
+
result = if Scimitar.engine_configuration.basic_authenticator.present?
|
118
|
+
authenticate_with_http_basic(&Scimitar.engine_configuration.basic_authenticator)
|
119
|
+
end
|
120
|
+
|
121
|
+
result ||= if Scimitar.engine_configuration.token_authenticator.present?
|
122
|
+
authenticate_with_http_token(&Scimitar.engine_configuration.token_authenticator)
|
123
|
+
end
|
124
|
+
|
125
|
+
return result
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require_dependency "scimitar/application_controller"
|
2
|
+
|
3
|
+
module Scimitar
|
4
|
+
class ResourceTypesController < ApplicationController
|
5
|
+
def index
|
6
|
+
resource_types = Scimitar::Engine.resources.map do |resource|
|
7
|
+
resource.resource_type(scim_resource_type_url(name: resource.resource_type_id))
|
8
|
+
end
|
9
|
+
|
10
|
+
render json: resource_types
|
11
|
+
end
|
12
|
+
|
13
|
+
def show
|
14
|
+
resource_types = Scimitar::Engine.resources.reduce({}) do |hash, resource|
|
15
|
+
hash[resource.resource_type_id] = resource.resource_type(scim_resource_type_url(name: resource.resource_type_id))
|
16
|
+
hash
|
17
|
+
end
|
18
|
+
|
19
|
+
resource_type = resource_types[params[:name]]
|
20
|
+
|
21
|
+
if resource_type.nil?
|
22
|
+
raise Scimitar::NotFoundError.new(params[:name])
|
23
|
+
else
|
24
|
+
render json: resource_type
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,203 @@
|
|
1
|
+
require_dependency "scimitar/application_controller"
|
2
|
+
|
3
|
+
module Scimitar
|
4
|
+
|
5
|
+
# A Rails controller which is mostly idiomatic, with #index, #show, #create
|
6
|
+
# and #destroy methods mapping to the conventional HTTP methods in Rails.
|
7
|
+
# The #update method is used for partial-update PATCH calls, while the
|
8
|
+
# #replace method is used for whole-update PUT calls.
|
9
|
+
#
|
10
|
+
# Subclass this controller to deal with resource-specific API calls, for
|
11
|
+
# endpoints such as Users and Groups. Any one controller is assumed to be
|
12
|
+
# related to one class in your application which has mixed in
|
13
|
+
# Scimitar::Resources::Mixin. Your subclass MUST override protected method
|
14
|
+
# #storage_class, which returns that class. For example, if you had a class
|
15
|
+
# User with the mixin included, then:
|
16
|
+
#
|
17
|
+
# protected
|
18
|
+
# def storage_class
|
19
|
+
# User
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# ...is sufficient.
|
23
|
+
#
|
24
|
+
# The controller makes no assumptions about storage method - it does not have
|
25
|
+
# any ActiveRecord specialisations, for example. If you do use ActiveRecord,
|
26
|
+
# consider subclassing Scimitar::ActiveRecordBackedResourcesController
|
27
|
+
# instead as it does most of the mapping, persistence and error handling work
|
28
|
+
# for you.
|
29
|
+
#
|
30
|
+
class ResourcesController < ApplicationController
|
31
|
+
|
32
|
+
# GET (list)
|
33
|
+
#
|
34
|
+
# Pass a Scimitar::Lists::Count object providing pagination data along with
|
35
|
+
# a page of results in "your" data domain as an Enumerable, along with a
|
36
|
+
# block. Renders as "list" result by calling your block with each of the
|
37
|
+
# results, allowing you to use something like
|
38
|
+
# Scimitar::Resources::Mixin#to_scim to convert to a SCIM representation.
|
39
|
+
#
|
40
|
+
# +pagination_info+:: A Scimitar::Lists::Count instance with #total set.
|
41
|
+
# See e.g. protected method #scim_pagination_info to
|
42
|
+
# assist with this.
|
43
|
+
#
|
44
|
+
# +page_of_results+:: An Enumerable single page of results.
|
45
|
+
#
|
46
|
+
def index(pagination_info, page_of_results, &block)
|
47
|
+
render(json: {
|
48
|
+
schemas: [
|
49
|
+
'urn:ietf:params:scim:api:messages:2.0:ListResponse'
|
50
|
+
],
|
51
|
+
totalResults: pagination_info.total,
|
52
|
+
startIndex: pagination_info.start_index,
|
53
|
+
itemsPerPage: pagination_info.limit,
|
54
|
+
Resources: page_of_results.map(&block)
|
55
|
+
})
|
56
|
+
end
|
57
|
+
|
58
|
+
# GET/id (show)
|
59
|
+
#
|
60
|
+
# Call with a block that is passed an ID to find in "your" domain. Evaluate
|
61
|
+
# to the SCIM representation of the arising found record.
|
62
|
+
#
|
63
|
+
def show(&block)
|
64
|
+
scim_resource = yield(self.safe_params()[:id])
|
65
|
+
render(json: scim_resource)
|
66
|
+
end
|
67
|
+
|
68
|
+
# POST (create)
|
69
|
+
#
|
70
|
+
# Call with a block that is passed a SCIM resource instance - e.g a
|
71
|
+
# Scimitar::Resources::User instance - representing an item to be created.
|
72
|
+
# Your ::storage_class class's ::scim_resource_type method determines the
|
73
|
+
# kind of object you'll be given.
|
74
|
+
#
|
75
|
+
# See also e.g. Scimitar::Resources::Mixin#from_scim!.
|
76
|
+
#
|
77
|
+
# Evaluate to the SCIM representation of the arising created record.
|
78
|
+
#
|
79
|
+
def create(&block)
|
80
|
+
with_scim_resource() do |resource|
|
81
|
+
render(json: yield(resource, :create), status: :created)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# PUT (replace)
|
86
|
+
#
|
87
|
+
# Similar to #create, but you're passed an ID to find as well as the
|
88
|
+
# resource details to then use for all replacement attributes in that found
|
89
|
+
# resource. See also e.g. Scimitar::Resources::Mixin#from_scim!.
|
90
|
+
#
|
91
|
+
# Evaluate to the SCIM representation of the arising created record.
|
92
|
+
#
|
93
|
+
def replace(&block)
|
94
|
+
with_scim_resource() do |resource|
|
95
|
+
render(json: yield(self.safe_params()[:id], resource))
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# PATCH (update)
|
100
|
+
#
|
101
|
+
# A variant of #create where you're again passed the resource ID (in "your"
|
102
|
+
# domain) to look up, but then a Hash with patch operation details from the
|
103
|
+
# calling client. This can be passed to e.g.
|
104
|
+
# Scimitar::Resources::Mixin#from_scim_patch!.
|
105
|
+
#
|
106
|
+
# Evaluate to the SCIM representation of the arising created record.
|
107
|
+
#
|
108
|
+
def update(&block)
|
109
|
+
validate_request()
|
110
|
+
|
111
|
+
# Params includes all of the PATCH data at the top level along with other
|
112
|
+
# other Rails-injected params like 'id', 'action', 'controller'. These
|
113
|
+
# are harmless given no namespace collision and we're only interested in
|
114
|
+
# the 'Operations' key for the actual patch data.
|
115
|
+
#
|
116
|
+
render(json: yield(self.safe_params()[:id], self.safe_params().to_hash()))
|
117
|
+
end
|
118
|
+
|
119
|
+
# DELETE (remove)
|
120
|
+
#
|
121
|
+
def destroy
|
122
|
+
if yield(self.safe_params()[:id]) != false
|
123
|
+
head :no_content
|
124
|
+
else
|
125
|
+
raise ErrorResponse.new(
|
126
|
+
status: 500,
|
127
|
+
detail: "Failed to delete the resource with id '#{params[:id]}'. Please try again later."
|
128
|
+
)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# =========================================================================
|
133
|
+
# PROTECTED INSTANCE METHODS
|
134
|
+
# =========================================================================
|
135
|
+
#
|
136
|
+
protected
|
137
|
+
|
138
|
+
# The class including Scimitar::Resources::Mixin which declares mappings
|
139
|
+
# to the entity you return in #resource_type.
|
140
|
+
#
|
141
|
+
def storage_class
|
142
|
+
raise NotImplementedError
|
143
|
+
end
|
144
|
+
|
145
|
+
# For #index actions, returns a Scimitar::Lists::Count instance which can
|
146
|
+
# be used to access offset-vs-start-index (0-indexed or 1-indexed),
|
147
|
+
# per-page limit and also holds the total number-of-items count which you
|
148
|
+
# can optionally pass up-front here, or set via #total= later.
|
149
|
+
#
|
150
|
+
# +total_count+:: Optional integer total record count across all pages,
|
151
|
+
# else must be set later - BEFORE passing an instance to
|
152
|
+
# the #index implementation in this class.
|
153
|
+
#
|
154
|
+
def scim_pagination_info(total_count = nil)
|
155
|
+
::Scimitar::Lists::Count.new(
|
156
|
+
start_index: params[:startIndex],
|
157
|
+
limit: params[:count] || Scimitar.service_provider_configuration(location: nil).filter.maxResults,
|
158
|
+
total: total_count
|
159
|
+
)
|
160
|
+
end
|
161
|
+
|
162
|
+
# =========================================================================
|
163
|
+
# PRIVATE INSTANCE METHODS
|
164
|
+
# =========================================================================
|
165
|
+
#
|
166
|
+
private
|
167
|
+
|
168
|
+
def validate_request
|
169
|
+
if request.raw_post.blank?
|
170
|
+
raise Scimitar::ErrorResponse.new(status: 400, detail: 'must provide a request body')
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def with_scim_resource
|
175
|
+
validate_request()
|
176
|
+
|
177
|
+
resource_type = storage_class().scim_resource_type() # See Scimitar::Resources::Mixin
|
178
|
+
resource = resource_type.new(self.safe_params().to_h)
|
179
|
+
|
180
|
+
if resource.valid?
|
181
|
+
yield(resource)
|
182
|
+
else
|
183
|
+
raise Scimitar::ErrorResponse.new(
|
184
|
+
status: 400,
|
185
|
+
detail: "Invalid resource: #{resource.errors.full_messages.join(', ')}.",
|
186
|
+
scimType: 'invalidValue'
|
187
|
+
)
|
188
|
+
end
|
189
|
+
|
190
|
+
# Gem bugs aside - if this happens, we couldn't create "resource"; bad
|
191
|
+
# (or unsupported) attributes encountered in inbound payload data.
|
192
|
+
#
|
193
|
+
rescue NoMethodError => exception
|
194
|
+
Rails.logger.error("#{exception.message}\n#{exception.backtrace}")
|
195
|
+
raise Scimitar::ErrorResponse.new(status: 400, detail: 'Invalid request')
|
196
|
+
end
|
197
|
+
|
198
|
+
def safe_params
|
199
|
+
params.permit!
|
200
|
+
end
|
201
|
+
|
202
|
+
end
|
203
|
+
end
|