powerhome-scimitar 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (116) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +708 -0
  4. data/Rakefile +16 -0
  5. data/app/controllers/scimitar/active_record_backed_resources_controller.rb +257 -0
  6. data/app/controllers/scimitar/application_controller.rb +157 -0
  7. data/app/controllers/scimitar/resource_types_controller.rb +28 -0
  8. data/app/controllers/scimitar/resources_controller.rb +203 -0
  9. data/app/controllers/scimitar/schemas_controller.rb +21 -0
  10. data/app/controllers/scimitar/service_provider_configurations_controller.rb +8 -0
  11. data/app/models/scimitar/authentication_error.rb +9 -0
  12. data/app/models/scimitar/authentication_scheme.rb +18 -0
  13. data/app/models/scimitar/bulk.rb +8 -0
  14. data/app/models/scimitar/complex_types/address.rb +12 -0
  15. data/app/models/scimitar/complex_types/base.rb +83 -0
  16. data/app/models/scimitar/complex_types/email.rb +12 -0
  17. data/app/models/scimitar/complex_types/entitlement.rb +12 -0
  18. data/app/models/scimitar/complex_types/ims.rb +12 -0
  19. data/app/models/scimitar/complex_types/name.rb +12 -0
  20. data/app/models/scimitar/complex_types/phone_number.rb +12 -0
  21. data/app/models/scimitar/complex_types/photo.rb +12 -0
  22. data/app/models/scimitar/complex_types/reference_group.rb +12 -0
  23. data/app/models/scimitar/complex_types/reference_member.rb +12 -0
  24. data/app/models/scimitar/complex_types/role.rb +12 -0
  25. data/app/models/scimitar/complex_types/x509_certificate.rb +12 -0
  26. data/app/models/scimitar/engine_configuration.rb +32 -0
  27. data/app/models/scimitar/error_response.rb +32 -0
  28. data/app/models/scimitar/errors.rb +14 -0
  29. data/app/models/scimitar/filter.rb +11 -0
  30. data/app/models/scimitar/filter_error.rb +22 -0
  31. data/app/models/scimitar/invalid_syntax_error.rb +9 -0
  32. data/app/models/scimitar/lists/count.rb +64 -0
  33. data/app/models/scimitar/lists/query_parser.rb +745 -0
  34. data/app/models/scimitar/meta.rb +7 -0
  35. data/app/models/scimitar/not_found_error.rb +10 -0
  36. data/app/models/scimitar/resource_invalid_error.rb +9 -0
  37. data/app/models/scimitar/resource_type.rb +29 -0
  38. data/app/models/scimitar/resources/base.rb +190 -0
  39. data/app/models/scimitar/resources/group.rb +13 -0
  40. data/app/models/scimitar/resources/mixin.rb +1524 -0
  41. data/app/models/scimitar/resources/user.rb +13 -0
  42. data/app/models/scimitar/schema/address.rb +25 -0
  43. data/app/models/scimitar/schema/attribute.rb +132 -0
  44. data/app/models/scimitar/schema/base.rb +90 -0
  45. data/app/models/scimitar/schema/derived_attributes.rb +24 -0
  46. data/app/models/scimitar/schema/email.rb +10 -0
  47. data/app/models/scimitar/schema/entitlement.rb +10 -0
  48. data/app/models/scimitar/schema/group.rb +27 -0
  49. data/app/models/scimitar/schema/ims.rb +10 -0
  50. data/app/models/scimitar/schema/name.rb +20 -0
  51. data/app/models/scimitar/schema/phone_number.rb +10 -0
  52. data/app/models/scimitar/schema/photo.rb +10 -0
  53. data/app/models/scimitar/schema/reference_group.rb +23 -0
  54. data/app/models/scimitar/schema/reference_member.rb +21 -0
  55. data/app/models/scimitar/schema/role.rb +10 -0
  56. data/app/models/scimitar/schema/user.rb +52 -0
  57. data/app/models/scimitar/schema/vdtp.rb +18 -0
  58. data/app/models/scimitar/schema/x509_certificate.rb +22 -0
  59. data/app/models/scimitar/service_provider_configuration.rb +60 -0
  60. data/app/models/scimitar/supportable.rb +14 -0
  61. data/app/views/layouts/scimitar/application.html.erb +14 -0
  62. data/config/initializers/scimitar.rb +111 -0
  63. data/config/routes.rb +6 -0
  64. data/lib/scimitar/engine.rb +63 -0
  65. data/lib/scimitar/support/hash_with_indifferent_case_insensitive_access.rb +216 -0
  66. data/lib/scimitar/support/utilities.rb +51 -0
  67. data/lib/scimitar/version.rb +13 -0
  68. data/lib/scimitar.rb +29 -0
  69. data/spec/apps/dummy/app/controllers/custom_create_mock_users_controller.rb +25 -0
  70. data/spec/apps/dummy/app/controllers/custom_destroy_mock_users_controller.rb +24 -0
  71. data/spec/apps/dummy/app/controllers/custom_replace_mock_users_controller.rb +25 -0
  72. data/spec/apps/dummy/app/controllers/custom_request_verifiers_controller.rb +30 -0
  73. data/spec/apps/dummy/app/controllers/custom_save_mock_users_controller.rb +24 -0
  74. data/spec/apps/dummy/app/controllers/custom_update_mock_users_controller.rb +25 -0
  75. data/spec/apps/dummy/app/controllers/mock_groups_controller.rb +13 -0
  76. data/spec/apps/dummy/app/controllers/mock_users_controller.rb +13 -0
  77. data/spec/apps/dummy/app/models/mock_group.rb +83 -0
  78. data/spec/apps/dummy/app/models/mock_user.rb +132 -0
  79. data/spec/apps/dummy/config/application.rb +18 -0
  80. data/spec/apps/dummy/config/boot.rb +2 -0
  81. data/spec/apps/dummy/config/environment.rb +2 -0
  82. data/spec/apps/dummy/config/environments/test.rb +38 -0
  83. data/spec/apps/dummy/config/initializers/cookies_serializer.rb +3 -0
  84. data/spec/apps/dummy/config/initializers/scimitar.rb +61 -0
  85. data/spec/apps/dummy/config/initializers/session_store.rb +3 -0
  86. data/spec/apps/dummy/config/routes.rb +45 -0
  87. data/spec/apps/dummy/db/migrate/20210304014602_create_mock_users.rb +24 -0
  88. data/spec/apps/dummy/db/migrate/20210308020313_create_mock_groups.rb +10 -0
  89. data/spec/apps/dummy/db/migrate/20210308044214_create_join_table_mock_groups_mock_users.rb +13 -0
  90. data/spec/apps/dummy/db/schema.rb +48 -0
  91. data/spec/controllers/scimitar/application_controller_spec.rb +296 -0
  92. data/spec/controllers/scimitar/resource_types_controller_spec.rb +94 -0
  93. data/spec/controllers/scimitar/resources_controller_spec.rb +247 -0
  94. data/spec/controllers/scimitar/schemas_controller_spec.rb +83 -0
  95. data/spec/controllers/scimitar/service_provider_configurations_controller_spec.rb +22 -0
  96. data/spec/models/scimitar/complex_types/address_spec.rb +18 -0
  97. data/spec/models/scimitar/complex_types/email_spec.rb +21 -0
  98. data/spec/models/scimitar/lists/count_spec.rb +147 -0
  99. data/spec/models/scimitar/lists/query_parser_spec.rb +830 -0
  100. data/spec/models/scimitar/resource_type_spec.rb +21 -0
  101. data/spec/models/scimitar/resources/base_spec.rb +485 -0
  102. data/spec/models/scimitar/resources/base_validation_spec.rb +86 -0
  103. data/spec/models/scimitar/resources/mixin_spec.rb +3562 -0
  104. data/spec/models/scimitar/resources/user_spec.rb +68 -0
  105. data/spec/models/scimitar/schema/attribute_spec.rb +99 -0
  106. data/spec/models/scimitar/schema/base_spec.rb +64 -0
  107. data/spec/models/scimitar/schema/group_spec.rb +87 -0
  108. data/spec/models/scimitar/schema/user_spec.rb +720 -0
  109. data/spec/requests/active_record_backed_resources_controller_spec.rb +1354 -0
  110. data/spec/requests/application_controller_spec.rb +61 -0
  111. data/spec/requests/controller_configuration_spec.rb +17 -0
  112. data/spec/requests/engine_spec.rb +45 -0
  113. data/spec/spec_helper.rb +101 -0
  114. data/spec/spec_helper_spec.rb +30 -0
  115. data/spec/support/hash_with_indifferent_case_insensitive_access_spec.rb +169 -0
  116. metadata +321 -0
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,257 @@
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
+ before_action :obtain_id_column_name_from_attribute_map
25
+
26
+ # GET (list)
27
+ #
28
+ def index
29
+ query = if params[:filter].blank?
30
+ self.storage_scope()
31
+ else
32
+ attribute_map = storage_class().new.scim_queryable_attributes()
33
+ parser = ::Scimitar::Lists::QueryParser.new(attribute_map)
34
+
35
+ parser.parse(params[:filter])
36
+ parser.to_activerecord_query(self.storage_scope())
37
+ end
38
+
39
+ pagination_info = scim_pagination_info(query.count())
40
+
41
+ page_of_results = query
42
+ .order(@id_column => :asc)
43
+ .offset(pagination_info.offset)
44
+ .limit(pagination_info.limit)
45
+ .to_a()
46
+
47
+ super(pagination_info, page_of_results) do | record |
48
+ record_to_scim(record)
49
+ end
50
+ end
51
+
52
+ # GET/id (show)
53
+ #
54
+ def show
55
+ super do |record_id|
56
+ record = self.find_record(record_id)
57
+ record_to_scim(record)
58
+ end
59
+ end
60
+
61
+ # POST (create)
62
+ #
63
+ # Calls #save! on the new record if no block is given, else invokes the
64
+ # block, passing it the new ActiveRecord model instance to be saved. It
65
+ # is up to the block to make any further changes and persist the record.
66
+ #
67
+ # Blocks are invoked from within a wrapping database transaction.
68
+ # ActiveRecord::RecordInvalid exceptions are handled for you, rendering
69
+ # an appropriate SCIM error.
70
+ #
71
+ def create(&block)
72
+ super do |scim_resource|
73
+ self.storage_class().transaction do
74
+ record = self.storage_class().new
75
+ record.from_scim!(scim_hash: scim_resource.as_json())
76
+ self.save!(record, &block)
77
+ record_to_scim(record)
78
+ end
79
+ end
80
+ end
81
+
82
+ # PUT (replace)
83
+ #
84
+ # Calls #save! on the updated record if no block is given, else invokes the
85
+ # block, passing the updated record which the block must persist, with the
86
+ # same rules as for #create.
87
+ #
88
+ def replace(&block)
89
+ super do |record_id, scim_resource|
90
+ self.storage_class().transaction do
91
+ record = self.find_record(record_id)
92
+ record.from_scim!(scim_hash: scim_resource.as_json())
93
+ self.save!(record, &block)
94
+ record_to_scim(record)
95
+ end
96
+ end
97
+ end
98
+
99
+ # PATCH (update)
100
+ #
101
+ # Calls #save! on the updated record if no block is given, else invokes the
102
+ # block, passing the updated record which the block must persist, with the
103
+ # same rules as for #create.
104
+ #
105
+ def update(&block)
106
+ super do |record_id, patch_hash|
107
+ self.storage_class().transaction do
108
+ record = self.find_record(record_id)
109
+ record.from_scim_patch!(patch_hash: patch_hash)
110
+ self.save!(record, &block)
111
+ record_to_scim(record)
112
+ end
113
+ end
114
+ end
115
+
116
+ # DELETE (remove)
117
+ #
118
+ # Deletion methods can vary quite a lot with ActiveRecord objects. If you
119
+ # just let this superclass handle things, it'll call:
120
+ #
121
+ # https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-destroy-21
122
+ #
123
+ # ...i.e. the standard delete-record-with-callbacks method. If you pass
124
+ # a block, then this block is invoked and passed the ActiveRecord model
125
+ # instance to be destroyed. You can then do things like soft-deletions,
126
+ # updating an "active" flag, perform audit-related operations and so-on.
127
+ #
128
+ def destroy(&block)
129
+ super do |record_id|
130
+ record = self.find_record(record_id)
131
+
132
+ if block_given?
133
+ yield(record)
134
+ else
135
+ record.destroy!
136
+ end
137
+ end
138
+ end
139
+
140
+ # =========================================================================
141
+ # PROTECTED INSTANCE METHODS
142
+ # =========================================================================
143
+ #
144
+ protected
145
+
146
+ # Return an ActiveRecord::Relation used as the starting scope for #index
147
+ # lists and any 'find by ID' operation.
148
+ #
149
+ def storage_scope
150
+ raise NotImplementedError
151
+ end
152
+
153
+ # Return an Array of exceptions that #save! can rescue and handle with a
154
+ # SCIM error automatically.
155
+ #
156
+ def scimitar_rescuable_exceptions
157
+ [
158
+ ActiveRecord::RecordInvalid,
159
+ ActiveRecord::RecordNotSaved,
160
+ ActiveRecord::RecordNotUnique,
161
+ ]
162
+ end
163
+
164
+ # Find a record by ID. Subclasses can override this if they need special
165
+ # lookup behaviour.
166
+ #
167
+ # +record_id+:: Record ID (SCIM schema 'id' value - "our" ID).
168
+ #
169
+ def find_record(record_id)
170
+ self.storage_scope().find_by!(@id_column => record_id)
171
+ end
172
+
173
+ # DRY up controller actions - pass a record; returns the SCIM
174
+ # representation, with a "show" location specified via #url_for.
175
+ #
176
+ def record_to_scim(record)
177
+ record.to_scim(
178
+ location: url_for(action: :show, id: record.send(@id_column)),
179
+ include_attributes: params.fetch(:attributes, "").split(",")
180
+ )
181
+ end
182
+
183
+ # Save a record, dealing with validation exceptions by raising SCIM
184
+ # errors.
185
+ #
186
+ # +record+:: ActiveRecord subclass to save.
187
+ #
188
+ # If you just let this superclass handle things, it'll call the standard
189
+ # +#save!+ method on the record. If you pass a block, then this block is
190
+ # invoked and passed the ActiveRecord model instance to be saved. You can
191
+ # then do things like calling a different method, using a service object
192
+ # of some kind, perform audit-related operations and so-on.
193
+ #
194
+ # The return value is not used internally, making life easier for
195
+ # overriding subclasses to "do the right thing" / avoid mistakes (instead
196
+ # of e.g. requiring that a to-SCIM representation of 'record' is returned
197
+ # and relying upon this to generate correct response payloads - an early
198
+ # version of the gem did this and it caused a confusing subclass bug).
199
+ #
200
+ def save!(record, &block)
201
+ if block_given?
202
+ yield(record)
203
+ else
204
+ record.save!
205
+ end
206
+ rescue *self.scimitar_rescuable_exceptions() => exception
207
+ handle_on_save_exception(record, exception)
208
+ end
209
+
210
+ # Deal with exceptions related to errors upon saving, by responding with
211
+ # an appropriate SCIM error. This is most effective if the record has
212
+ # validation errors defined, but falls back to the provided exception's
213
+ # message otherwise.
214
+ #
215
+ # +record+:: The record that provoked the exception. Mandatory.
216
+ # +exception+:: The exception that was raised. If omitted, a default of
217
+ # 'Unknown', in English with no I18n, is used.
218
+ #
219
+ def handle_on_save_exception(record, exception = RuntimeError.new('Unknown'))
220
+ details = if record.errors.present?
221
+ record.errors.full_messages.join('; ')
222
+ else
223
+ exception.message
224
+ end
225
+
226
+ # https://tools.ietf.org/html/rfc7644#page-12
227
+ #
228
+ # If the service provider determines that the creation of the requested
229
+ # resource conflicts with existing resources (e.g., a "User" resource
230
+ # with a duplicate "userName"), the service provider MUST return HTTP
231
+ # status code 409 (Conflict) with a "scimType" error code of
232
+ # "uniqueness"
233
+ #
234
+ if exception.is_a?(ActiveRecord::RecordNotUnique) || record.errors.any? { | e | e.type == :taken }
235
+ raise Scimitar::ErrorResponse.new(
236
+ status: 409,
237
+ scimType: 'uniqueness',
238
+ detail: "Operation failed due to a uniqueness constraint: #{details}"
239
+ )
240
+ else
241
+ raise Scimitar::ResourceInvalidError.new(details)
242
+ end
243
+ end
244
+
245
+ # Called via +before_action+ - stores in @id_column the name of whatever
246
+ # model column is used to store the record ID, via
247
+ # Scimitar::Resources::Mixin::scim_attributes_map.
248
+ #
249
+ # Default is <tt>:id</tt>.
250
+ #
251
+ def obtain_id_column_name_from_attribute_map
252
+ attrs = storage_class().scim_attributes_map() || {}
253
+ @id_column = attrs[:id] || :id
254
+ end
255
+
256
+ end
257
+ end
@@ -0,0 +1,157 @@
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, used for a configured error reporter
29
+ # via #handle_scim_error (if present).
30
+ #
31
+ def handle_resource_not_found(exception)
32
+ handle_scim_error(NotFoundError.new(params[:id]), exception)
33
+ end
34
+
35
+ # This base controller uses:
36
+ #
37
+ # rescue_from Scimitar::ErrorResponse, with: :handle_scim_error
38
+ #
39
+ # ...to "globally" invoke this handler for all Scimitar errors (including
40
+ # subclasses).
41
+ #
42
+ # Mandatory parameters are:
43
+ #
44
+ # +error_response+:: Scimitar::ErrorResponse (or subclass) instance.
45
+ #
46
+ # Optional parameters are:
47
+ #
48
+ # *exception+:: If a Ruby exception was the reason this method is being
49
+ # called, pass it here. Any configured exception reporting
50
+ # mechanism will be invokved with the given parameter.
51
+ # Otherwise, the +error_response+ value is reported.
52
+ #
53
+ def handle_scim_error(error_response, exception = error_response)
54
+ unless Scimitar.engine_configuration.exception_reporter.nil?
55
+ Scimitar.engine_configuration.exception_reporter.call(exception)
56
+ end
57
+
58
+ render json: error_response, status: error_response.status
59
+ end
60
+
61
+ # This base controller uses:
62
+ #
63
+ # rescue_from ActionDispatch::Http::Parameters::ParseError, with: :handle_bad_json_error
64
+ #
65
+ # ...to "globally" handle JSON errors implied by parse errors raised via
66
+ # the "ActionDispatch::Request.parameter_parsers" block in
67
+ # lib/scimitar/engine.rb.
68
+ #
69
+ # +exception+:: Exception instance.
70
+ #
71
+ def handle_bad_json_error(exception)
72
+ handle_scim_error(ErrorResponse.new(status: 400, detail: "Invalid JSON - #{exception.message}"), exception)
73
+ end
74
+
75
+ # This base controller uses:
76
+ #
77
+ # rescue_from StandardError, with: :handle_unexpected_error
78
+ #
79
+ # ...to "globally" handle 500-style cases with a SCIM response.
80
+ #
81
+ # +exception+:: Exception instance.
82
+ #
83
+ def handle_unexpected_error(exception)
84
+ Rails.logger.error("#{exception.message}\n#{exception.backtrace}")
85
+ handle_scim_error(ErrorResponse.new(status: 500, detail: exception.message), exception)
86
+ end
87
+
88
+ # =========================================================================
89
+ # PRIVATE INSTANCE METHODS
90
+ # =========================================================================
91
+ #
92
+ private
93
+
94
+ # Tries to be permissive in what it receives - ".scim" extensions or a
95
+ # Content-Type header (or both) lead to both being set up for the inbound
96
+ # request and subclass processing.
97
+ #
98
+ def require_scim
99
+ scim_mime_type = Mime::Type.lookup_by_extension(:scim).to_s
100
+
101
+ if request.media_type.nil? || request.media_type.empty?
102
+ request.format = :scim
103
+ request.headers['CONTENT_TYPE'] = scim_mime_type
104
+ elsif request.media_type.downcase == scim_mime_type
105
+ request.format = :scim
106
+ elsif request.format == :scim
107
+ request.headers['CONTENT_TYPE'] = scim_mime_type
108
+ else
109
+ handle_scim_error(ErrorResponse.new(status: 406, detail: "Only #{scim_mime_type} type is accepted."))
110
+ end
111
+ end
112
+
113
+ def add_mandatory_response_headers
114
+
115
+ # https://tools.ietf.org/html/rfc7644#section-2
116
+ #
117
+ # "...a SCIM service provider SHALL indicate supported HTTP
118
+ # authentication schemes via the "WWW-Authenticate" header."
119
+ #
120
+ # Rack may not handle an attempt to set two instances of the header and
121
+ # there is much debate on how to specify multiple methods in one header
122
+ # so we just let Token override Basic (since Token is much stronger, or
123
+ # at least has the potential to do so) if that's how Rack handles it.
124
+ #
125
+ # https://stackoverflow.com/questions/10239970/what-is-the-delimiter-for-www-authenticate-for-multiple-schemes
126
+ #
127
+ response.set_header('WWW-Authenticate', 'Basic' ) if Scimitar.engine_configuration.basic_authenticator.present?
128
+ response.set_header('WWW-Authenticate', 'Bearer') if Scimitar.engine_configuration.token_authenticator.present?
129
+
130
+ # No matter what a caller might request via headers, the only content
131
+ # type we can ever respond with is JSON-for-SCIM.
132
+ #
133
+ response.set_header('Content-Type', "#{Mime::Type.lookup_by_extension(:scim)}; charset=utf-8")
134
+ end
135
+
136
+ def authenticate
137
+ handle_scim_error(Scimitar::AuthenticationError.new) unless authenticated?
138
+ end
139
+
140
+ def authenticated?
141
+ result = if Scimitar.engine_configuration.basic_authenticator.present?
142
+ authenticate_with_http_basic do |username, password|
143
+ instance_exec(username, password, &Scimitar.engine_configuration.basic_authenticator)
144
+ end
145
+ end
146
+
147
+ result ||= if Scimitar.engine_configuration.token_authenticator.present?
148
+ authenticate_with_http_token do |token, options|
149
+ instance_exec(token, options, &Scimitar.engine_configuration.token_authenticator)
150
+ end
151
+ end
152
+
153
+ return result
154
+ end
155
+
156
+ end
157
+ 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
@@ -0,0 +1,21 @@
1
+ require_dependency "scimitar/application_controller"
2
+
3
+ module Scimitar
4
+ class SchemasController < ApplicationController
5
+ def index
6
+ schemas = Scimitar::Engine.schemas
7
+
8
+ schemas.each do |schema|
9
+ schema.meta.location = scim_schemas_url(name: schema.id)
10
+ end
11
+
12
+ schemas_by_id = schemas.reduce({}) do |hash, schema|
13
+ hash[schema.id] = schema
14
+ hash
15
+ end
16
+
17
+ render json: schemas_by_id[params[:name]] || schemas
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,8 @@
1
+ require_dependency "scimitar/application_controller"
2
+ module Scimitar
3
+ class ServiceProviderConfigurationsController < ApplicationController
4
+ def show
5
+ render json: Scimitar.service_provider_configuration(location: request.url)
6
+ end
7
+ end
8
+ end