scimitar 1.2.1 → 1.3.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 69cae1bb9cbd4ddcdf7914e35b4ac23ad7359c2d566d99071dfb346ae8b2f9d2
4
- data.tar.gz: c1636e01e98e1938a1f62ffd27b8aa0c00a787860f93a95f445158cb8e7e6064
3
+ metadata.gz: 9faabfad9cae931ab19f765393d9f307fa5d44773f943d2286fbd92302c2e9e1
4
+ data.tar.gz: 2bc23d4e3b123103efd0fa87375547dcdf9e3684ea272a1e4941c43e7845937b
5
5
  SHA512:
6
- metadata.gz: 321cf5943bf587c925c827d909cb8dda138be977a03e211b1a7496e0c608da1055a0e4fb9aaa1d8f9877d5631a34dbba5f16c00b50098ca08b5bdfa6fed2cff2
7
- data.tar.gz: 910f2e4f85c7722f71229cdbd680af6af64aa7d1186e8464ad6050b338f97bf6849042407b241acfc409a63ce8fe48023f1f251d3712d1ec8298cb91d911ce0f
6
+ metadata.gz: bb74810f72d7b806b7e6aef1a02d363be1135dbf190e2ae0e88a727f94ec9b90e643a13883cd64951d4879fe542fb5936621ec96c44d0f192050f9f6453b58ef
7
+ data.tar.gz: b8ea496f490696379236a971070f6e04188fbfcff27f0dd484e18c01416193c625173c72618e123cbc39b1521ef4e6137553396d981db6366861f98f172cee2c
@@ -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
  # =========================================================================
@@ -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 []
@@ -902,7 +902,11 @@ module Scimitar
902
902
  altering_hash[path_component] = value
903
903
  end
904
904
  when 'replace'
905
- altering_hash[path_component] = value
905
+ if path_component == 'root'
906
+ altering_hash[path_component].merge!(value)
907
+ else
908
+ altering_hash[path_component] = value
909
+ end
906
910
  when 'remove'
907
911
  altering_hash.delete(path_component)
908
912
  end
@@ -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.2.1'
6
+ VERSION = '1.3.1'
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 = '2022-06-15'
11
+ DATE = '2022-11-04'
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
@@ -1747,6 +1747,24 @@ RSpec.describe Scimitar::Resources::Mixin do
1747
1747
  expect(scim_hash['emails'][0]['type' ]).to eql('work')
1748
1748
  expect(scim_hash['emails'][0]['value']).to eql('work@test.com')
1749
1749
  end
1750
+
1751
+ context 'when prior value already exists, and no path' do
1752
+ it 'simple value: overwrites' do
1753
+ path = [ 'root' ]
1754
+ scim_hash = { 'root' => { 'userName' => 'bar', 'active' => true } }.with_indifferent_case_insensitive_access()
1755
+
1756
+ @instance.send(
1757
+ :from_patch_backend!,
1758
+ nature: 'replace',
1759
+ path: path,
1760
+ value: { 'active' => false }.with_indifferent_case_insensitive_access(),
1761
+ altering_hash: scim_hash
1762
+ )
1763
+
1764
+ expect(scim_hash['root']['userName']).to eql('bar')
1765
+ expect(scim_hash['root']['active']).to eql(false)
1766
+ end
1767
+ end
1750
1768
  end # context 'when value is not present' do
1751
1769
  end # "context 'replace' do"
1752
1770
 
@@ -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,
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.2.1
4
+ version: 1.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - RIPA Global
8
8
  - Andrew David Hodgkinson
9
- autorequire:
9
+ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2022-06-15 00:00:00.000000000 Z
12
+ date: 2022-11-04 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -245,7 +245,7 @@ metadata:
245
245
  source_code_uri: https://github.com/RIPAGlobal/scimitar/
246
246
  bug_tracker_uri: https://github.com/RIPAGlobal/scimitar/issues/
247
247
  changelog_uri: https://github.com/RIPAGlobal/scimitar/blob/master/CHANGELOG.md
248
- post_install_message:
248
+ post_install_message:
249
249
  rdoc_options: []
250
250
  require_paths:
251
251
  - lib
@@ -261,7 +261,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
261
261
  version: '0'
262
262
  requirements: []
263
263
  rubygems_version: 3.1.6
264
- signing_key:
264
+ signing_key:
265
265
  specification_version: 4
266
266
  summary: SCIM v2 for Rails
267
267
  test_files: