scimitar 2.5.0 → 2.6.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: 9a1f1bf2bb09d47ecd354dd72ec9ecbaab96b17b8e1165df6f1ae4fe26200d8b
4
- data.tar.gz: 3ed9ed5b0bb4612ee6213b86c73fb51a5ad2be1a19cffc360037b29c504b61cb
3
+ metadata.gz: 13a1cc0fad873f6524c65d4d24c6556d22b75abcd13ecdbb422d074a8ce94afc
4
+ data.tar.gz: 9244ab9bcf9f32b3a8e9b6b16653c92f0e7a228320f7c3fefe751e7bee290372
5
5
  SHA512:
6
- metadata.gz: 4353efc5d7acb49b23d69478e75de4fcc932d59331f9428b53f22e72cf91df2e3db8b8ee1c70af242b93fb64cf338cde0c1e5ee4f2557f79800768bca4ab2b4f
7
- data.tar.gz: f57d58f5e6ef4eb0181d29f7ad46612eada045cd2b1a92edfeb663aa622e400b4474cb06707826c13ea8ade2d0dd51ebdad1b5edb063ce147abf51d62f1e3815
6
+ metadata.gz: bc24687beb3d360e5b548b617b699a9b0a6384f3b1477336b18a95fc4647d1364727dd4bf66984a05efc7d06210689a7ad33f662c82ff42b81abae4e1aa26584
7
+ data.tar.gz: 9291068aae0c3eb047eedaaf9d2d7587aa49bfd7703b8d4db0235a15a43b2d8d0e3ddeaf5784f432f3f0df0edb6bc8099e85d1eb5937ca262399634d3fc51965
@@ -153,7 +153,13 @@ module Scimitar
153
153
  # Save a record, dealing with validation exceptions by raising SCIM
154
154
  # errors.
155
155
  #
156
- # +record+:: ActiveRecord subclass to save (via #save!).
156
+ # +record+:: ActiveRecord subclass to save.
157
+ #
158
+ # If you just let this superclass handle things, it'll call the standard
159
+ # +#save!+ method on the record. If you pass a block, then this block is
160
+ # invoked and passed the ActiveRecord model instance to be saved. You can
161
+ # then do things like calling a different method, using a service object of
162
+ # some kind, perform audit-related operations and so-on.
157
163
  #
158
164
  # The return value is not used internally, making life easier for
159
165
  # overriding subclasses to "do the right thing" / avoid mistakes (instead
@@ -161,10 +167,21 @@ module Scimitar
161
167
  # and relying upon this to generate correct response payloads - an early
162
168
  # version of the gem did this and it caused a confusing subclass bug).
163
169
  #
164
- def save!(record)
165
- record.save!
166
-
170
+ def save!(record, &block)
171
+ if block_given?
172
+ yield(record)
173
+ else
174
+ record.save!
175
+ end
167
176
  rescue ActiveRecord::RecordInvalid => exception
177
+ handle_invalid_record(exception.record)
178
+ end
179
+
180
+ # Deal with validation errors by responding with an appropriate SCIM error.
181
+ #
182
+ # +record+:: The record with validation errors.
183
+ #
184
+ def handle_invalid_record(record)
168
185
  joined_errors = record.errors.full_messages.join('; ')
169
186
 
170
187
  # https://tools.ietf.org/html/rfc7644#page-12
@@ -4,6 +4,11 @@ module Scimitar
4
4
  class SchemasController < ApplicationController
5
5
  def index
6
6
  schemas = Scimitar::Engine.schemas
7
+
8
+ schemas.each do |schema|
9
+ schema.meta.location = scim_schemas_url(name: schema.id)
10
+ end
11
+
7
12
  schemas_by_id = schemas.reduce({}) do |hash, schema|
8
13
  hash[schema.id] = schema
9
14
  hash
@@ -139,13 +139,23 @@ module Scimitar
139
139
 
140
140
  def as_json(options = {})
141
141
  self.meta = Meta.new unless self.meta && self.meta.is_a?(Meta)
142
- meta.resourceType = self.class.resource_type_id
143
- original_hash = super(options).except('errors')
142
+ self.meta.resourceType = self.class.resource_type_id
143
+
144
+ non_returnable_attributes = self.class
145
+ .schemas
146
+ .flat_map(&:scim_attributes)
147
+ .filter_map { |attribute| attribute.name if attribute.returned == 'never' }
148
+
149
+ non_returnable_attributes << 'errors'
150
+
151
+ original_hash = super(options).except(*non_returnable_attributes)
144
152
  original_hash.merge!('schemas' => self.class.schemas.map(&:id))
153
+
145
154
  self.class.extended_schemas.each do |extension_schema|
146
155
  extension_attributes = extension_schema.scim_attributes.map(&:name)
147
156
  original_hash.merge!(extension_schema.id => original_hash.extract!(*extension_attributes))
148
157
  end
158
+
149
159
  original_hash
150
160
  end
151
161
 
@@ -93,12 +93,21 @@ module Scimitar
93
93
  end
94
94
 
95
95
  def valid_simple_type?(value)
96
- valid = (type == 'string' && value.is_a?(String)) ||
96
+ if multiValued
97
+ valid = value.is_a?(Array) && value.all? { |v| simple_type?(v) }
98
+ errors.add(self.name, "or one of its elements has the wrong type. It has to be an array of #{self.type}s.") unless valid
99
+ else
100
+ valid = simple_type?(value)
101
+ errors.add(self.name, "has the wrong type. It has to be a(n) #{self.type}.") unless valid
102
+ end
103
+ valid
104
+ end
105
+
106
+ def simple_type?(value)
107
+ (type == 'string' && value.is_a?(String)) ||
97
108
  (type == 'boolean' && (value.is_a?(TrueClass) || value.is_a?(FalseClass))) ||
98
109
  (type == 'integer' && (value.is_a?(Integer))) ||
99
110
  (type == 'dateTime' && valid_date_time?(value))
100
- errors.add(self.name, "has the wrong type. It has to be a(n) #{self.type}.") unless valid
101
- valid
102
111
  end
103
112
 
104
113
  def valid_date_time?(value)
@@ -13,7 +13,7 @@ module Scimitar
13
13
 
14
14
  # Converts the schema to its json representation that will be returned by /SCHEMAS end-point of a SCIM service provider.
15
15
  def as_json(options = {})
16
- @meta.location = Scimitar::Engine.routes.url_helpers.scim_schemas_path(name: id)
16
+ @meta.location ||= Scimitar::Engine.routes.url_helpers.scim_schemas_path(name: id)
17
17
  original = super
18
18
  original.merge('attributes' => original.delete('scim_attributes'))
19
19
  end
@@ -37,10 +37,11 @@ Rails.application.config.to_prepare do # (required for >= Rails 7 / Zeitwerk)
37
37
  #
38
38
  Scimitar.engine_configuration = Scimitar::EngineConfiguration.new({
39
39
 
40
- # If you have filters you want to run for any Scimitar action/route, you
41
- # can define them here. For example, you might use a before-action to set
42
- # up some multi-tenancy related state, or skip Rails CSRF token
43
- # verification. For example:
40
+ # If you have filters you want to run for any Scimitar action/route, you can
41
+ # define them here. You can also override any shared controller methods
42
+ # here. For example, you might use a before-action to set up some
43
+ # multi-tenancy related state, skip Rails CSRF token verification, or
44
+ # customize how Scimitar generates URLs:
44
45
  #
45
46
  # application_controller_mixin: Module.new do
46
47
  # def self.included(base)
@@ -54,6 +55,10 @@ Rails.application.config.to_prepare do # (required for >= Rails 7 / Zeitwerk)
54
55
  # prepend_before_action :setup_some_kind_of_multi_tenancy_data
55
56
  # end
56
57
  # end
58
+ #
59
+ # def scim_schemas_url(options)
60
+ # super(custom_param: 'value', **options)
61
+ # end
57
62
  # end, # ...other configuration entries might follow...
58
63
 
59
64
  # If you want to support username/password authentication:
@@ -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.5.0'
6
+ VERSION = '2.6.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 = '2023-09-25'
11
+ DATE = '2023-11-14'
12
12
 
13
13
  end
@@ -0,0 +1,24 @@
1
+ # For tests only - uses custom 'save!' implementation which passes a block to
2
+ # Scimitar::ActiveRecordBackedResourcesController#save!.
3
+ #
4
+ class CustomSaveMockUsersController < Scimitar::ActiveRecordBackedResourcesController
5
+
6
+ CUSTOM_SAVE_BLOCK_USERNAME_INDICATOR = 'Custom save-block invoked'
7
+
8
+ protected
9
+
10
+ def save!(_record)
11
+ super do | record |
12
+ record.update!(username: CUSTOM_SAVE_BLOCK_USERNAME_INDICATOR)
13
+ end
14
+ end
15
+
16
+ def storage_class
17
+ MockUser
18
+ end
19
+
20
+ def storage_scope
21
+ MockUser.all
22
+ end
23
+
24
+ end
@@ -10,6 +10,7 @@ class MockUser < ActiveRecord::Base
10
10
  primary_key
11
11
  scim_uid
12
12
  username
13
+ password
13
14
  first_name
14
15
  last_name
15
16
  work_email_address
@@ -46,6 +47,7 @@ class MockUser < ActiveRecord::Base
46
47
  id: :primary_key,
47
48
  externalId: :scim_uid,
48
49
  userName: :username,
50
+ password: :password,
49
51
  name: {
50
52
  givenName: :first_name,
51
53
  familyName: :last_name
@@ -19,6 +19,14 @@ Rails.application.config.to_prepare do
19
19
  before_action :test_hook
20
20
  end
21
21
  end
22
+
23
+ def scim_schemas_url(options)
24
+ super(test: 1, **options)
25
+ end
26
+
27
+ def scim_resource_type_url(options)
28
+ super(test: 1, **options)
29
+ end
22
30
  end
23
31
 
24
32
  })
@@ -21,6 +21,11 @@ Rails.application.routes.draw do
21
21
  #
22
22
  delete 'CustomDestroyUsers/:id', to: 'custom_destroy_mock_users#destroy'
23
23
 
24
+ # For testing blocks passed to ActiveRecordBackedResourcesController#save!
25
+ #
26
+ post 'CustomSaveUsers', to: 'custom_save_mock_users#create'
27
+ get 'CustomSaveUsers/:id', to: 'custom_save_mock_users#show'
28
+
24
29
  # For testing environment inside Scimitar::ApplicationController subclasses.
25
30
  #
26
31
  get 'CustomRequestVerifiers', to: 'custom_request_verifiers#index'
@@ -7,6 +7,7 @@ class CreateMockUsers < ActiveRecord::Migration[6.1]
7
7
  #
8
8
  t.text :scim_uid
9
9
  t.text :username
10
+ t.text :password
10
11
  t.text :first_name
11
12
  t.text :last_name
12
13
  t.text :work_email_address
@@ -10,7 +10,7 @@
10
10
  #
11
11
  # It's strongly recommended that you check this file into your version control system.
12
12
 
13
- ActiveRecord::Schema[7.0].define(version: 2021_03_08_044214) do
13
+ ActiveRecord::Schema[7.1].define(version: 2021_03_08_044214) do
14
14
  # These are extensions that must be enabled in order to support this database
15
15
  enable_extension "plpgsql"
16
16
 
@@ -33,6 +33,7 @@ ActiveRecord::Schema[7.0].define(version: 2021_03_08_044214) do
33
33
  t.datetime "updated_at", null: false
34
34
  t.text "scim_uid"
35
35
  t.text "username"
36
+ t.text "password"
36
37
  t.text "first_name"
37
38
  t.text "last_name"
38
39
  t.text "work_email_address"
@@ -9,8 +9,8 @@ RSpec.describe Scimitar::ResourceTypesController do
9
9
  it 'renders the resource type for user' do
10
10
  get :index, format: :scim
11
11
  response_hash = JSON.parse(response.body)
12
- expected_response = [ Scimitar::Resources::User.resource_type(scim_resource_type_url(name: 'User')),
13
- Scimitar::Resources::Group.resource_type(scim_resource_type_url(name: 'Group'))
12
+ expected_response = [ Scimitar::Resources::User.resource_type(scim_resource_type_url(name: 'User', test: 1)),
13
+ Scimitar::Resources::Group.resource_type(scim_resource_type_url(name: 'Group', test: 1))
14
14
  ].to_json
15
15
 
16
16
  response_hash = JSON.parse(response.body)
@@ -1,6 +1,7 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  RSpec.describe Scimitar::SchemasController do
4
+ routes { Scimitar::Engine.routes }
4
5
 
5
6
  before(:each) { allow(controller).to receive(:authenticated?).and_return(true) }
6
7
 
@@ -26,6 +27,13 @@ RSpec.describe Scimitar::SchemasController do
26
27
  expect(parsed_body['name']).to eql('User')
27
28
  end
28
29
 
30
+ it 'includes the controller customized schema location' do
31
+ get :index, params: { name: Scimitar::Schema::User.id, format: :scim }
32
+ expect(response).to be_ok
33
+ parsed_body = JSON.parse(response.body)
34
+ expect(parsed_body.dig('meta', 'location')).to eq scim_schemas_url(name: Scimitar::Schema::User.id, test: 1)
35
+ end
36
+
29
37
  it 'returns only the Group schema when its id is provided' do
30
38
  get :index, params: { name: Scimitar::Schema::Group.id, format: :scim }
31
39
  expect(response).to be_ok
@@ -14,7 +14,10 @@ RSpec.describe Scimitar::Resources::Base do
14
14
  ),
15
15
  Scimitar::Schema::Attribute.new(
16
16
  name: 'names', multiValued: true, complexType: Scimitar::ComplexTypes::Name, required: false
17
- )
17
+ ),
18
+ Scimitar::Schema::Attribute.new(
19
+ name: 'privateName', complexType: Scimitar::ComplexTypes::Name, required: false, returned: false
20
+ ),
18
21
  ]
19
22
  end
20
23
  end
@@ -30,6 +33,10 @@ RSpec.describe Scimitar::Resources::Base do
30
33
  name: {
31
34
  givenName: 'John',
32
35
  familyName: 'Smith'
36
+ },
37
+ privateName: {
38
+ givenName: 'Alt John',
39
+ familyName: 'Alt Smith'
33
40
  }
34
41
  }
35
42
 
@@ -39,6 +46,9 @@ RSpec.describe Scimitar::Resources::Base do
39
46
  expect(resource.name.is_a?(Scimitar::ComplexTypes::Name)).to be(true)
40
47
  expect(resource.name.givenName).to eql('John')
41
48
  expect(resource.name.familyName).to eql('Smith')
49
+ expect(resource.privateName.is_a?(Scimitar::ComplexTypes::Name)).to be(true)
50
+ expect(resource.privateName.givenName).to eql('Alt John')
51
+ expect(resource.privateName.familyName).to eql('Alt Smith')
42
52
  end
43
53
 
44
54
  it 'which builds an array of nested resources' do
@@ -101,14 +111,38 @@ RSpec.describe Scimitar::Resources::Base do
101
111
  context '#as_json' do
102
112
  it 'renders the json with the resourceType' do
103
113
  resource = CustomResourse.new(name: {
104
- givenName: 'John',
114
+ givenName: 'John',
105
115
  familyName: 'Smith'
106
116
  })
107
117
 
108
118
  result = resource.as_json
109
- expect(result['schemas']).to eql(['custom-id'])
119
+
120
+ expect(result['schemas'] ).to eql(['custom-id'])
121
+ expect(result['meta']['resourceType']).to eql('CustomResourse')
122
+ expect(result['errors'] ).to be_nil
123
+ end
124
+
125
+ it 'excludes attributes that are flagged as do-not-return' do
126
+ resource = CustomResourse.new(
127
+ name: {
128
+ givenName: 'John',
129
+ familyName: 'Smith'
130
+ },
131
+ privateName: {
132
+ givenName: 'Alt John',
133
+ familyName: 'Alt Smith'
134
+ }
135
+ )
136
+
137
+ result = resource.as_json
138
+
139
+ expect(result['schemas'] ).to eql(['custom-id'])
110
140
  expect(result['meta']['resourceType']).to eql('CustomResourse')
111
- expect(result['errors']).to be_nil
141
+ expect(result['errors'] ).to be_nil
142
+ expect(result['name'] ).to be_present
143
+ expect(result['name']['givenName'] ).to eql('John')
144
+ expect(result['name']['familyName'] ).to eql('Smith')
145
+ expect(result['privateName'] ).to be_present
112
146
  end
113
147
  end # "context '#as_json' do"
114
148
 
@@ -160,13 +160,14 @@ RSpec.describe Scimitar::Resources::Mixin do
160
160
 
161
161
  context '#to_scim' do
162
162
  context 'with a UUID, renamed primary key column' do
163
- it 'compiles instance attribute values into a SCIM representation' do
163
+ it 'compiles instance attribute values into a SCIM representation, but omits do-not-return fields' do
164
164
  uuid = SecureRandom.uuid
165
165
 
166
166
  instance = MockUser.new
167
167
  instance.primary_key = uuid
168
168
  instance.scim_uid = 'AA02984'
169
169
  instance.username = 'foo'
170
+ instance.password = 'correcthorsebatterystaple'
170
171
  instance.first_name = 'Foo'
171
172
  instance.last_name = 'Bar'
172
173
  instance.work_email_address = 'foo.bar@test.com'
@@ -404,6 +405,7 @@ RSpec.describe Scimitar::Resources::Mixin do
404
405
  it 'ignoring read-only lists' do
405
406
  hash = {
406
407
  'userName' => 'foo',
408
+ 'password' => 'staplebatteryhorsecorrect',
407
409
  'name' => {'givenName' => 'Foo', 'familyName' => 'Bar'},
408
410
  'active' => true,
409
411
  'emails' => [{'type' => 'work', 'primary' => true, 'value' => 'foo.bar@test.com'}],
@@ -428,6 +430,7 @@ RSpec.describe Scimitar::Resources::Mixin do
428
430
 
429
431
  expect(instance.scim_uid ).to eql('AA02984')
430
432
  expect(instance.username ).to eql('foo')
433
+ expect(instance.password ).to eql('staplebatteryhorsecorrect')
431
434
  expect(instance.first_name ).to eql('Foo')
432
435
  expect(instance.last_name ).to eql('Bar')
433
436
  expect(instance.work_email_address).to eql('foo.bar@test.com')
@@ -46,6 +46,28 @@ RSpec.describe Scimitar::Schema::Attribute do
46
46
  expect(attribute.errors.messages.to_h).to eql({userName: ['has the wrong type. It has to be a(n) string.']})
47
47
  end
48
48
 
49
+ it 'is valid if multi-valued and type is string and given value is an array of strings' do
50
+ attribute = described_class.new(name: 'scopes', multiValued: true, type: 'string')
51
+ expect(attribute.valid?(['something', 'something else'])).to be(true)
52
+ end
53
+
54
+ it 'is valid if multi-valued and type is string and given value is an empty array' do
55
+ attribute = described_class.new(name: 'scopes', multiValued: true, type: 'string')
56
+ expect(attribute.valid?([])).to be(true)
57
+ end
58
+
59
+ it 'is invalid if multi-valued and type is string and given value is not an array' do
60
+ attribute = described_class.new(name: 'scopes', multiValued: true, type: 'string')
61
+ expect(attribute.valid?('something')).to be(false)
62
+ expect(attribute.errors.messages.to_h).to eql({scopes: ['or one of its elements has the wrong type. It has to be an array of strings.']})
63
+ end
64
+
65
+ it 'is invalid if multi-valued and type is string and given value is an array containing another type' do
66
+ attribute = described_class.new(name: 'scopes', multiValued: true, type: 'string')
67
+ expect(attribute.valid?(['something', 123])).to be(false)
68
+ expect(attribute.errors.messages.to_h).to eql({scopes: ['or one of its elements has the wrong type. It has to be an array of strings.']})
69
+ end
70
+
49
71
  it 'is valid if type is boolean and given value is boolean' do
50
72
  expect(described_class.new(name: 'name', type: 'boolean').valid?(false)).to be(true)
51
73
  expect(described_class.new(name: 'name', type: 'boolean').valid?(true)).to be(true)
@@ -397,6 +397,22 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
397
397
  expect(result['scimType']).to eql('invalidValue')
398
398
  expect(result['detail']).to include('is reserved')
399
399
  end
400
+
401
+ it 'invokes a block if given one' do
402
+ mock_before = MockUser.all.to_a
403
+ attributes = { userName: '5' } # Minimum required by schema
404
+
405
+ expect_any_instance_of(CustomSaveMockUsersController).to receive(:create).once.and_call_original
406
+ expect {
407
+ post "/CustomSaveUsers", params: attributes.merge(format: :scim)
408
+ }.to change { MockUser.count }.by(1)
409
+
410
+ mock_after = MockUser.all.to_a
411
+ new_mock = (mock_after - mock_before).first
412
+
413
+ expect(response.status).to eql(201)
414
+ expect(new_mock.username).to eql(CustomSaveMockUsersController::CUSTOM_SAVE_BLOCK_USERNAME_INDICATOR)
415
+ end
400
416
  end # "context '#create' do"
401
417
 
402
418
  # ===========================================================================
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.5.0
4
+ version: 2.6.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: 2023-09-25 00:00:00.000000000 Z
12
+ date: 2023-11-14 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -196,6 +196,7 @@ files:
196
196
  - lib/scimitar/version.rb
197
197
  - spec/apps/dummy/app/controllers/custom_destroy_mock_users_controller.rb
198
198
  - spec/apps/dummy/app/controllers/custom_request_verifiers_controller.rb
199
+ - spec/apps/dummy/app/controllers/custom_save_mock_users_controller.rb
199
200
  - spec/apps/dummy/app/controllers/mock_groups_controller.rb
200
201
  - spec/apps/dummy/app/controllers/mock_users_controller.rb
201
202
  - spec/apps/dummy/app/models/mock_group.rb
@@ -260,13 +261,14 @@ required_rubygems_version: !ruby/object:Gem::Requirement
260
261
  - !ruby/object:Gem::Version
261
262
  version: '0'
262
263
  requirements: []
263
- rubygems_version: 3.4.4
264
+ rubygems_version: 3.4.10
264
265
  signing_key:
265
266
  specification_version: 4
266
267
  summary: SCIM v2 for Rails
267
268
  test_files:
268
269
  - spec/apps/dummy/app/controllers/custom_destroy_mock_users_controller.rb
269
270
  - spec/apps/dummy/app/controllers/custom_request_verifiers_controller.rb
271
+ - spec/apps/dummy/app/controllers/custom_save_mock_users_controller.rb
270
272
  - spec/apps/dummy/app/controllers/mock_groups_controller.rb
271
273
  - spec/apps/dummy/app/controllers/mock_users_controller.rb
272
274
  - spec/apps/dummy/app/models/mock_group.rb