scimitar 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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
|