scimitar 1.1.0 → 1.3.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: 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: