scimitar 2.0.0 → 2.1.0

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: 95a2166cc921a400959f9d8d4398f6bf8ecb772f8d7a0a0a73950892e85d808a
4
- data.tar.gz: cdf5aab3812f10f69c96304e738a150f4208850267527b66d36eeb99548d7b1f
3
+ metadata.gz: d10a10d763c621e797fbe343a113a8610f9317a06455f177c60c42062663b4da
4
+ data.tar.gz: 251c1b73b7dd51ded63d85f7b0816163a9bd567099db9ce5f57e4929301d7a49
5
5
  SHA512:
6
- metadata.gz: f0925517599b107e44fd93db9be142aebe608892c2c5069c50d22b353c51238290710474b062a002fdb010be3d783c4dea3b314f72f47b4aca3c2385a8fc1377
7
- data.tar.gz: eef6eebfc64bb2d4adabfca110f26e3a6c9e227f47387da6fb384925899ddf0ae260cf68176f24040a6bb356cf34f6576c920bd44264d4a1fee415aeadc237e6
6
+ metadata.gz: 77accce401fb9f6fbb91a0ebf53294754286867cc2becce4f8acccbbd35f90c9062174d1da0e0ad4c80a92acc053c4a0b30898260fe95dc580bf3df33453f975
7
+ data.tar.gz: 9e64bad4582b54c988f612f9a0920b291431a79f62f9656bcad2658c5a0ded291e9cf96f47cac5f2b130c47a30edbf3b68b6f8abe96ca9cb8a7bb8dcaea83bd2
@@ -25,10 +25,11 @@ module Scimitar
25
25
  #
26
26
  # ...to "globally" invoke this handler if you wish.
27
27
  #
28
- # +_exception+:: Exception instance (currently unused).
28
+ # +_exception+:: Exception instance, used for a configured error reporter
29
+ # via #handle_scim_error (if present).
29
30
  #
30
- def handle_resource_not_found(_exception)
31
- handle_scim_error(NotFoundError.new(params[:id]))
31
+ def handle_resource_not_found(exception)
32
+ handle_scim_error(NotFoundError.new(params[:id]), exception)
32
33
  end
33
34
 
34
35
  # This base controller uses:
@@ -38,9 +39,22 @@ module Scimitar
38
39
  # ...to "globally" invoke this handler for all Scimitar errors (including
39
40
  # subclasses).
40
41
  #
42
+ # Mandatory parameters are:
43
+ #
41
44
  # +error_response+:: Scimitar::ErrorResponse (or subclass) instance.
42
45
  #
43
- def handle_scim_error(error_response)
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
+
44
58
  render json: error_response, status: error_response.status
45
59
  end
46
60
 
@@ -55,7 +69,7 @@ module Scimitar
55
69
  # +exception+:: Exception instance.
56
70
  #
57
71
  def handle_bad_json_error(exception)
58
- handle_scim_error(ErrorResponse.new(status: 400, detail: "Invalid JSON - #{exception.message}"))
72
+ handle_scim_error(ErrorResponse.new(status: 400, detail: "Invalid JSON - #{exception.message}"), exception)
59
73
  end
60
74
 
61
75
  # This base controller uses:
@@ -68,7 +82,7 @@ module Scimitar
68
82
  #
69
83
  def handle_unexpected_error(exception)
70
84
  Rails.logger.error("#{exception.message}\n#{exception.backtrace}")
71
- handle_scim_error(ErrorResponse.new(status: 500, detail: exception.message))
85
+ handle_scim_error(ErrorResponse.new(status: 500, detail: exception.message), exception)
72
86
  end
73
87
 
74
88
  # =========================================================================
@@ -82,12 +96,17 @@ module Scimitar
82
96
  # request and subclass processing.
83
97
  #
84
98
  def require_scim
85
- if request.content_type&.downcase == Mime::Type.lookup_by_extension(:scim).to_s
99
+ scim_mime_type = Mime::Type.lookup_by_extension(:scim).to_s
100
+
101
+ if request.content_type.nil?
102
+ request.format = :scim
103
+ request.headers['CONTENT_TYPE'] = scim_mime_type
104
+ elsif request.content_type&.downcase == scim_mime_type
86
105
  request.format = :scim
87
106
  elsif request.format == :scim
88
- request.headers['CONTENT_TYPE'] = Mime::Type.lookup_by_extension(:scim).to_s
107
+ request.headers['CONTENT_TYPE'] = scim_mime_type
89
108
  else
90
- handle_scim_error(ErrorResponse.new(status: 406, detail: "Only #{Mime::Type.lookup_by_extension(:scim)} type is accepted."))
109
+ handle_scim_error(ErrorResponse.new(status: 406, detail: "Only #{scim_mime_type} type is accepted."))
91
110
  end
92
111
  end
93
112
 
@@ -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
+ # From v1, Scimitar used attribute "detail" for the 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'),
@@ -81,6 +81,26 @@ Rails.application.config.to_prepare do # (required for >= Rails 7 / Zeitwerk)
81
81
  # Note that both basic and token authentication can be declared, with the
82
82
  # parameters in the inbound HTTP request determining which is invoked.
83
83
 
84
+ # Scimitar rescues certain error cases and exceptions, in order to return a
85
+ # JSON response to the API caller. If you want exceptions to also be
86
+ # reported to a third party system such as sentry.io or raygun.com, you can
87
+ # configure a Proc to do so. It is passed a Ruby exception subclass object.
88
+ # For example, a minimal sentry.io reporter might do this:
89
+ #
90
+ # exception_reporter: Proc.new do | exception |
91
+ # Sentry.capture_exception(exception)
92
+ # end
93
+ #
94
+ # You will still need to configure your reporting system according to its
95
+ # documentation (e.g. via a Rails "config/initializers/<foo>.rb" file).
96
+
97
+ # Scimilar treats "VDTP" (Value, Display, Type, Primary) attribute values,
98
+ # used for e.g. e-mail addresses or phone numbers, as required by default.
99
+ # If you encounter a service which calls these with e.g. "null" value data,
100
+ # you can configure all values to be optional. You'll need to deal with
101
+ # whatever that means for you receiving system in your model code.
102
+ #
103
+ # optional_value_fields_required: false
84
104
  })
85
105
 
86
106
  end
@@ -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 = '2.0.0'
6
+ VERSION = '2.1.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 = '2022-03-04'
11
+ DATE = '2022-07-14'
12
12
 
13
13
  end
@@ -169,5 +169,70 @@ 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
+ raise ActionDispatch::Http::Parameters::ParseError.new("Hello")
212
+ end
213
+ end
214
+
215
+ it 'is invoked' do
216
+ get :index, params: { format: :scim }
217
+
218
+ expect(@exception).to be_a(ActionDispatch::Http::Parameters::ParseError)
219
+ expect(@exception.message).to eql('Hello')
220
+ end
221
+ end
222
+
223
+ context 'and a bad content type' do
224
+ controller do
225
+ def index; end
226
+ end
227
+
228
+ it 'is invoked' do
229
+ request.headers['Content-Type'] = 'text/plain'
230
+ get :index
231
+
232
+ expect(@exception).to be_a(Scimitar::ErrorResponse)
233
+ expect(@exception.message).to eql('Only application/scim+json type is accepted.')
234
+ end
235
+ end
236
+ end # "context 'exception reporter' do"
237
+ end # "context 'error handling' do"
173
238
  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') # Filled in by ApplicationController#require_scim
19
22
  end
20
23
 
21
24
  it 'renders 400 if given bad JSON' do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scimitar
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - RIPA Global
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2022-03-04 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
@@ -45,28 +45,28 @@ dependencies:
45
45
  requirements:
46
46
  - - "~>"
47
47
  - !ruby/object:Gem::Version
48
- version: '1.2'
48
+ version: '1.3'
49
49
  type: :development
50
50
  prerelease: false
51
51
  version_requirements: !ruby/object:Gem::Requirement
52
52
  requirements:
53
53
  - - "~>"
54
54
  - !ruby/object:Gem::Version
55
- version: '1.2'
55
+ version: '1.3'
56
56
  - !ruby/object:Gem::Dependency
57
57
  name: simplecov-rcov
58
58
  requirement: !ruby/object:Gem::Requirement
59
59
  requirements:
60
60
  - - "~>"
61
61
  - !ruby/object:Gem::Version
62
- version: '0.2'
62
+ version: '0.3'
63
63
  type: :development
64
64
  prerelease: false
65
65
  version_requirements: !ruby/object:Gem::Requirement
66
66
  requirements:
67
67
  - - "~>"
68
68
  - !ruby/object:Gem::Version
69
- version: '0.2'
69
+ version: '0.3'
70
70
  - !ruby/object:Gem::Dependency
71
71
  name: rdoc
72
72
  requirement: !ruby/object:Gem::Requirement
@@ -87,14 +87,14 @@ dependencies:
87
87
  requirements:
88
88
  - - "~>"
89
89
  - !ruby/object:Gem::Version
90
- version: '5.0'
90
+ version: '5.1'
91
91
  type: :development
92
92
  prerelease: false
93
93
  version_requirements: !ruby/object:Gem::Requirement
94
94
  requirements:
95
95
  - - "~>"
96
96
  - !ruby/object:Gem::Version
97
- version: '5.0'
97
+ version: '5.1'
98
98
  - !ruby/object:Gem::Dependency
99
99
  name: byebug
100
100
  requirement: !ruby/object:Gem::Requirement
@@ -260,7 +260,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
260
260
  - !ruby/object:Gem::Version
261
261
  version: '0'
262
262
  requirements: []
263
- rubygems_version: 3.3.3
263
+ rubygems_version: 3.3.7
264
264
  signing_key:
265
265
  specification_version: 4
266
266
  summary: SCIM v2 for Rails