scimitar 1.1.0 → 1.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 834a7c3f5dba88856dfea8fdfcc1807f49b36739d1c5886e0dc96e7ca5621642
4
- data.tar.gz: 7aaa9fae826b8142c3c3418b825ca86c7de46bb543dcdfb19b042caea15f988e
3
+ metadata.gz: 742d9f1195cc1ec3c86c17e2ca731119413925f3b5e0a3bd67fc90bf7183c2dd
4
+ data.tar.gz: c4e90cbd8d59eb564deebfd0278e96f2f1896b9fa6c0eb8697f1975115e2cf47
5
5
  SHA512:
6
- metadata.gz: c3db5de7ca04d57be95f3638183936ab1cd4621b3aba22e0bcd6eaa3b2e876dfe2e79f86711da6de1106908ae96fd444c060b6bc10055e0b8f476faff6e18ca0
7
- data.tar.gz: 701c4cb7d93f9dcfa89906bcfb222ba734d4ed83f0f2404969d8375df2a963c2376629508733d7e2852250d8652dac8e9ec07a6482f3a382fa64aaf2ac4ef9e0
6
+ metadata.gz: b1df7b83b8adaf904891983a1059a690ed7435917c8ae409a81254a2b13314e35ce45d630aecc917165c527314cd140913ce4d31494da4a8ca54c65f8b7f6d6f
7
+ data.tar.gz: 2855fcbe539ae73ff31fbae32a7e4841a31be1c48c39eae94ae34b9ab96341fdb17e53ee82d42c318a5bf928143d39181461a9bbb86d435ecb37e4fdda6876da
@@ -131,8 +131,8 @@ module Scimitar
131
131
  raise NotImplementedError
132
132
  end
133
133
 
134
- # Find a RIP user record. Subclasses can override this if they need
135
- # special lookup behaviour.
134
+ # Find a record by ID. Subclasses can override this if they need special
135
+ # lookup behaviour.
136
136
  #
137
137
  # +record_id+:: Record ID (SCIM schema 'id' value - "our" ID).
138
138
  #
@@ -25,10 +25,12 @@ module Scimitar
25
25
  #
26
26
  # ...to "globally" invoke this handler if you wish.
27
27
  #
28
- # +_exception+:: Exception instance (currently unused).
29
28
  #
30
- def handle_resource_not_found(_exception)
31
- handle_scim_error(NotFoundError.new(params[:id]))
29
+ # +exception+:: Exception instance, used for a configured error reporter
30
+ # via #handle_scim_error (if present).
31
+ #
32
+ def handle_resource_not_found(exception)
33
+ handle_scim_error(NotFoundError.new(params[:id]), exception)
32
34
  end
33
35
 
34
36
  # This base controller uses:
@@ -38,9 +40,22 @@ module Scimitar
38
40
  # ...to "globally" invoke this handler for all Scimitar errors (including
39
41
  # subclasses).
40
42
  #
43
+ # Mandatory parameters are:
44
+ #
41
45
  # +error_response+:: Scimitar::ErrorResponse (or subclass) instance.
42
46
  #
43
- def handle_scim_error(error_response)
47
+ # Optional parameters are:
48
+ #
49
+ # *exception+:: If a Ruby exception was the reason this method is being
50
+ # called, pass it here. Any configured exception reporting
51
+ # mechanism will be invokved with the given parameter.
52
+ # Otherwise, the +error_response+ value is reported.
53
+ #
54
+ def handle_scim_error(error_response, exception = error_response)
55
+ unless Scimitar.engine_configuration.exception_reporter.nil?
56
+ Scimitar.engine_configuration.exception_reporter.call(exception)
57
+ end
58
+
44
59
  render json: error_response, status: error_response.status
45
60
  end
46
61
 
@@ -55,7 +70,7 @@ module Scimitar
55
70
  # +exception+:: Exception instance.
56
71
  #
57
72
  def handle_bad_json_error(exception)
58
- handle_scim_error(ErrorResponse.new(status: 400, detail: "Invalid JSON - #{exception.message}"))
73
+ handle_scim_error(ErrorResponse.new(status: 400, detail: "Invalid JSON - #{exception.message}"), exception)
59
74
  end
60
75
 
61
76
  # This base controller uses:
@@ -68,7 +83,7 @@ module Scimitar
68
83
  #
69
84
  def handle_unexpected_error(exception)
70
85
  Rails.logger.error("#{exception.message}\n#{exception.backtrace}")
71
- handle_scim_error(ErrorResponse.new(status: 500, detail: exception.message))
86
+ handle_scim_error(ErrorResponse.new(status: 500, detail: exception.message), exception)
72
87
  end
73
88
 
74
89
  # =========================================================================
@@ -82,12 +97,17 @@ module Scimitar
82
97
  # request and subclass processing.
83
98
  #
84
99
  def require_scim
85
- if request.content_type&.downcase == Mime::Type.lookup_by_extension(:scim).to_s
100
+ scim_mime_type = Mime::Type.lookup_by_extension(:scim).to_s
101
+
102
+ if request.content_type.nil?
103
+ request.format = :scim
104
+ request.headers['CONTENT_TYPE'] = scim_mime_type
105
+ elsif request.content_type&.downcase == scim_mime_type
86
106
  request.format = :scim
87
107
  elsif request.format == :scim
88
- request.headers['CONTENT_TYPE'] = Mime::Type.lookup_by_extension(:scim).to_s
108
+ request.headers['CONTENT_TYPE'] = scim_mime_type
89
109
  else
90
- handle_scim_error(ErrorResponse.new(status: 406, detail: "Only #{Mime::Type.lookup_by_extension(:scim)} type is accepted."))
110
+ handle_scim_error(ErrorResponse.new(status: 406, detail: "Only #{scim_mime_type} type is accepted."))
91
111
  end
92
112
  end
93
113
 
@@ -9,13 +9,17 @@ module Scimitar
9
9
 
10
10
  attr_accessor :basic_authenticator,
11
11
  :token_authenticator,
12
- :application_controller_mixin
12
+ :application_controller_mixin,
13
+ :exception_reporter,
14
+ :optional_value_fields_required
13
15
 
14
16
  def initialize(attributes = {})
15
17
 
16
- # No defaults yet - reserved for future use.
18
+ # Set defaults that may be overridden by the initializer.
17
19
  #
18
- defaults = {}
20
+ defaults = {
21
+ optional_value_fields_required: true
22
+ }
19
23
 
20
24
  super(defaults.merge(attributes))
21
25
  end
@@ -16,5 +16,17 @@ module Scimitar
16
16
  data['scimType'] = scimType if scimType
17
17
  data
18
18
  end
19
+
20
+ # Originally Scimitar used attribute "detail" for exception text; it was
21
+ # only for JSON responses at the time, but in hindsight was a bad choice.
22
+ # It should have been "message" given inheritance from StandardError, which
23
+ # then works properly with e.g. error reporting services.
24
+ #
25
+ # The "detail" attribute is still present, for backwards compatibility with
26
+ # any client code that might be using this class.
27
+ #
28
+ def message
29
+ self.detail
30
+ end
19
31
  end
20
32
  end
@@ -633,7 +633,7 @@ module Scimitar
633
633
  when 'pr'
634
634
  arel_table.grouping(arel_column.not_eq_all(['', nil]))
635
635
  else
636
- raise Scimitar::FilterError
636
+ raise Scimitar::FilterError.new("Unsupported operator: '#{scim_operator}'")
637
637
  end
638
638
 
639
639
  if index == 0
@@ -656,10 +656,10 @@ module Scimitar
656
656
  # +scim_attribute+:: SCIM attribute from a filter string.
657
657
  #
658
658
  def activerecord_columns(scim_attribute)
659
- raise Scimitar::FilterError if scim_attribute.blank?
659
+ raise Scimitar::FilterError.new("No scim_attribute provided") if scim_attribute.blank?
660
660
 
661
661
  mapped_attribute = self.attribute_map()[scim_attribute]
662
- raise Scimitar::FilterError if mapped_attribute.blank?
662
+ raise Scimitar::FilterError.new("Unable to find domain attribute from SCIM attribute: '#{scim_attribute}'") if mapped_attribute.blank?
663
663
 
664
664
  if mapped_attribute[:ignore]
665
665
  return []
@@ -138,7 +138,7 @@ module Scimitar
138
138
  end
139
139
 
140
140
  def as_json(options = {})
141
- self.meta = Meta.new unless self.meta
141
+ self.meta = Meta.new unless self.meta && self.meta.is_a?(Meta)
142
142
  meta.resourceType = self.class.resource_type_id
143
143
  original_hash = super(options).except('errors')
144
144
  original_hash.merge!('schemas' => self.class.schemas.map(&:id))
@@ -10,6 +10,7 @@ module Scimitar
10
10
  def self.scim_attributes
11
11
  @scim_attributes ||= [
12
12
  Attribute.new(name: 'type', type: 'string'),
13
+ Attribute.new(name: 'primary', type: 'boolean'),
13
14
  Attribute.new(name: 'formatted', type: 'string'),
14
15
  Attribute.new(name: 'streetAddress', type: 'string'),
15
16
  Attribute.new(name: 'locality', type: 'string'),
@@ -7,7 +7,7 @@ module Scimitar
7
7
  class Vdtp < Base
8
8
  def self.scim_attributes
9
9
  @scim_attributes ||= [
10
- Attribute.new(name: 'value', type: 'string', required: true),
10
+ Attribute.new(name: 'value', type: 'string', required: Scimitar.engine_configuration.optional_value_fields_required),
11
11
  Attribute.new(name: 'display', type: 'string', mutability: 'readOnly'),
12
12
  Attribute.new(name: 'type', type: 'string'),
13
13
  Attribute.new(name: 'primary', type: 'boolean'),
@@ -79,4 +79,24 @@ Scimitar.engine_configuration = Scimitar::EngineConfiguration.new({
79
79
  # Note that both basic and token authentication can be declared, with the
80
80
  # parameters in the inbound HTTP request determining which is invoked.
81
81
 
82
+ # Scimitar rescues certain error cases and exceptions, in order to return a
83
+ # JSON response to the API caller. If you want exceptions to also be
84
+ # reported to a third party system such as sentry.io or raygun.com, you can
85
+ # configure a Proc to do so. It is passed a Ruby exception subclass object.
86
+ # For example, a minimal sentry.io reporter might do this:
87
+ #
88
+ # exception_reporter: Proc.new do | exception |
89
+ # Sentry.capture_exception(exception)
90
+ # end
91
+ #
92
+ # You will still need to configure your reporting system according to its
93
+ # documentation (e.g. via a Rails "config/initializers/<foo>.rb" file).
94
+
95
+ # Scimilar treats "VDTP" (Value, Display, Type, Primary) attribute values,
96
+ # used for e.g. e-mail addresses or phone numbers, as required by default.
97
+ # If you encounter a service which calls these with e.g. "null" value data,
98
+ # you can configure all values to be optional. You'll need to deal with
99
+ # whatever that means for you receiving system in your model code.
100
+ #
101
+ # optional_value_fields_required: false
82
102
  })
@@ -3,11 +3,11 @@ module Scimitar
3
3
  # Gem version. If this changes, be sure to re-run "bundle install" or
4
4
  # "bundle update".
5
5
  #
6
- VERSION = '1.1.0'
6
+ VERSION = '1.3.0'
7
7
 
8
8
  # Date for VERSION. If this changes, be sure to re-run "bundle install"
9
9
  # or "bundle update".
10
10
  #
11
- DATE = '2021-09-15'
11
+ DATE = '2022-07-14'
12
12
 
13
13
  end
@@ -169,5 +169,74 @@ RSpec.describe Scimitar::ApplicationController do
169
169
  expect(parsed_body).to include('status' => '500')
170
170
  expect(parsed_body).to include('detail' => 'Bang')
171
171
  end
172
- end
172
+
173
+ context 'with an exception reporter' do
174
+ around :each do | example |
175
+ original_configuration = Scimitar.engine_configuration.exception_reporter
176
+ Scimitar.engine_configuration.exception_reporter = Proc.new do | exception |
177
+ @exception = exception
178
+ end
179
+ example.run()
180
+ ensure
181
+ Scimitar.engine_configuration.exception_reporter = original_configuration
182
+ end
183
+
184
+ context 'and "internal server error"' do
185
+ it 'is invoked' do
186
+ get :index, params: { format: :scim }
187
+
188
+ expect(@exception).to be_a(RuntimeError)
189
+ expect(@exception.message).to eql('Bang')
190
+ end
191
+ end
192
+
193
+ context 'and "not found"' do
194
+ controller do
195
+ def index
196
+ handle_resource_not_found(ActiveRecord::RecordNotFound.new(42))
197
+ end
198
+ end
199
+
200
+ it 'is invoked' do
201
+ get :index, params: { format: :scim }
202
+
203
+ expect(@exception).to be_a(ActiveRecord::RecordNotFound)
204
+ expect(@exception.message).to eql('42')
205
+ end
206
+ end
207
+
208
+ context 'and bad JSON' do
209
+ controller do
210
+ def index
211
+ begin
212
+ raise 'Hello'
213
+ rescue
214
+ raise ActionDispatch::Http::Parameters::ParseError
215
+ end
216
+ end
217
+ end
218
+
219
+ it 'is invoked' do
220
+ get :index, params: { format: :scim }
221
+
222
+ expect(@exception).to be_a(ActionDispatch::Http::Parameters::ParseError)
223
+ expect(@exception.message).to eql('Hello')
224
+ end
225
+ end
226
+
227
+ context 'and a bad content type' do
228
+ controller do
229
+ def index; end
230
+ end
231
+
232
+ it 'is invoked' do
233
+ request.headers['Content-Type'] = 'text/plain'
234
+ get :index
235
+
236
+ expect(@exception).to be_a(Scimitar::ErrorResponse)
237
+ expect(@exception.message).to eql('Only application/scim+json type is accepted.')
238
+ end
239
+ end
240
+ end # "context 'exception reporter' do"
241
+ end # "context 'error handling' do"
173
242
  end
@@ -18,6 +18,4 @@ RSpec.describe Scimitar::ComplexTypes::Email do
18
18
  expect(described_class.new(value: 'a@b.c').as_json).to eq('value' => 'a@b.c')
19
19
  end
20
20
  end
21
-
22
21
  end
23
-
@@ -1,7 +1,6 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  RSpec.describe Scimitar::Resources::Base do
4
-
5
4
  context '#valid?' do
6
5
  MyCustomSchema = Class.new(Scimitar::Schema::Base) do
7
6
  def self.id
@@ -21,6 +20,9 @@ RSpec.describe Scimitar::Resources::Base do
21
20
  ),
22
21
  Scimitar::Schema::Attribute.new(
23
22
  name: 'complexNames', complexType: Scimitar::ComplexTypes::Name, multiValued:true, required: false
23
+ ),
24
+ Scimitar::Schema::Attribute.new(
25
+ name: 'vdtpTestByEmail', complexType: Scimitar::ComplexTypes::Email, required: false
24
26
  )
25
27
  ]
26
28
  end
@@ -57,5 +59,28 @@ RSpec.describe Scimitar::Resources::Base do
57
59
  expect(resource.valid?).to be(false)
58
60
  expect(resource.errors.full_messages).to match_array(["Complexnames has to follow the complexType format.", "Complexnames familyname has the wrong type. It has to be a(n) string."])
59
61
  end
60
- end
62
+
63
+ context 'configuration of required values in VDTP schema' do
64
+ around :each do | example |
65
+ original_configuration = Scimitar.engine_configuration.optional_value_fields_required
66
+ Scimitar::Schema::Email.instance_variable_set('@scim_attributes', nil)
67
+ example.run()
68
+ ensure
69
+ Scimitar.engine_configuration.optional_value_fields_required = original_configuration
70
+ Scimitar::Schema::Email.instance_variable_set('@scim_attributes', nil)
71
+ end
72
+
73
+ it 'requires a value by default' do
74
+ resource = MyCustomResource.new(vdtpTestByEmail: { value: nil }, enforce: false)
75
+ expect(resource.valid?).to be(false)
76
+ expect(resource.errors.full_messages).to match_array(['Vdtptestbyemail value is required'])
77
+ end
78
+
79
+ it 'can be configured for optional values' do
80
+ Scimitar.engine_configuration.optional_value_fields_required = false
81
+ resource = MyCustomResource.new(vdtpTestByEmail: { value: nil }, enforce: false)
82
+ expect(resource.valid?).to be(true)
83
+ end
84
+ end # "context 'configuration of required values in VDTP schema' do"
85
+ end # "context '#valid?' do"
61
86
  end
@@ -11,7 +11,7 @@ RSpec.describe Scimitar::Schema::Base do
11
11
  end
12
12
 
13
13
  context '#initialize' do
14
- it 'creates a meta' do
14
+ it 'creates "meta"' do
15
15
  schema = described_class.new
16
16
  expect(schema.meta.resourceType).to eql('Schema')
17
17
  end
@@ -419,6 +419,16 @@ RSpec.describe Scimitar::Schema::User do
419
419
  "name": "type",
420
420
  "type": "string"
421
421
  },
422
+ {
423
+ "multiValued": false,
424
+ "required": false,
425
+ "caseExact": false,
426
+ "mutability": "readWrite",
427
+ "uniqueness": "none",
428
+ "returned": "default",
429
+ "name": "primary",
430
+ "type": "boolean"
431
+ },
422
432
  {
423
433
  "multiValued": false,
424
434
  "required": false,
@@ -180,6 +180,7 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
180
180
  givenName: 'Given',
181
181
  familyName: 'Family'
182
182
  },
183
+ meta: { resourceType: 'User' },
183
184
  emails: [
184
185
  {
185
186
  type: 'work',
@@ -11,11 +11,14 @@ RSpec.describe Scimitar::ApplicationController do
11
11
  end
12
12
 
13
13
  context 'format handling' do
14
- it 'renders "not acceptable" if the request does not use SCIM type' do
14
+ it 'renders "OK" if the request does not provide any Content-Type value' do
15
15
  get '/CustomRequestVerifiers', params: { format: :html }
16
16
 
17
- expect(response).to have_http_status(:not_acceptable)
18
- expect(JSON.parse(response.body)['detail']).to eql('Only application/scim+json type is accepted.')
17
+ expect(response).to have_http_status(:ok)
18
+ parsed_body = JSON.parse(response.body)
19
+ expect(parsed_body['request']['is_scim' ]).to eql(true)
20
+ expect(parsed_body['request']['format' ]).to eql('application/scim+json')
21
+ expect(parsed_body['request']['content_type']).to eql('application/scim+json')
19
22
  end
20
23
 
21
24
  it 'renders 400 if given bad JSON' do
metadata CHANGED
@@ -1,15 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scimitar
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
- - RIP Global
7
+ - RIPA Global
8
8
  - Andrew David Hodgkinson
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2021-09-15 00:00:00.000000000 Z
12
+ date: 2022-07-14 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -115,17 +115,17 @@ dependencies:
115
115
  requirements:
116
116
  - - "~>"
117
117
  - !ruby/object:Gem::Version
118
- version: '1.1'
118
+ version: '1.2'
119
119
  type: :development
120
120
  prerelease: false
121
121
  version_requirements: !ruby/object:Gem::Requirement
122
122
  requirements:
123
123
  - - "~>"
124
124
  - !ruby/object:Gem::Version
125
- version: '1.1'
125
+ version: '1.2'
126
126
  description: SCIM v2 support for Users and Groups in Ruby On Rails
127
127
  email:
128
- - dev@ripglobal.com
128
+ - dev@ripaglobal.com
129
129
  executables: []
130
130
  extensions: []
131
131
  extra_rdoc_files: []
@@ -237,11 +237,14 @@ files:
237
237
  - spec/spec_helper.rb
238
238
  - spec/spec_helper_spec.rb
239
239
  - spec/support/hash_with_indifferent_case_insensitive_access_spec.rb
240
- homepage: https://ripglobal.com/
240
+ homepage: https://www.ripaglobal.com/
241
241
  licenses:
242
242
  - MIT
243
243
  metadata:
244
- source_code_uri: https://github.com/RIPGlobal/scimitar
244
+ homepage_uri: https://www.ripaglobal.com/
245
+ source_code_uri: https://github.com/RIPAGlobal/scimitar/
246
+ bug_tracker_uri: https://github.com/RIPAGlobal/scimitar/issues/
247
+ changelog_uri: https://github.com/RIPAGlobal/scimitar/blob/master/CHANGELOG.md
245
248
  post_install_message:
246
249
  rdoc_options: []
247
250
  require_paths: