scimitar 1.2.1 → 1.3.1

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: 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: