scimitar 1.5.0 → 1.5.2

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: dd0d129e52cd7cbe3fe52fec4c4033cd45f4d2071da5d495863b1f3f78685fd5
4
- data.tar.gz: 5531cdfc4786043513f9539c2fce43bfdebbec26a6e9964d93b13c6ea00710b0
3
+ metadata.gz: b55b1f0a0a700b57690cb05fc02241cf47f60a4ad8c92946a32366c90ee56d8c
4
+ data.tar.gz: a7143dcd7d1381250e66529c70288659a2018fb26fc16639c14ba796acc805d5
5
5
  SHA512:
6
- metadata.gz: d1914b8fe68cdc6077369f22a03f7e688790f5b239738e18508e0aeaa68055ee1e197ebc4fe8b512903d07b926de64d64817b26c6ef9b9ace2594abc85f01e62
7
- data.tar.gz: de0d305d512d646f43e581b5116c2f316eaa2b9ddb0426f1b12522a668e2a7c811a5e9d6aee82e82fadb534b5e1a5cacca303701a7f7d1791de188d73b55740c
6
+ metadata.gz: fb740b528770a918fc3237cabdc953affcde23fa7787cc9e9383a6d71b104e2d6325c2829545d367983a3bb64435179a0523b6d785d8e6d3eb7b2bfdb6c82748
7
+ data.tar.gz: 9f73bcafefbd7de57f258d1dc1c18e020b8dd7bca6330f7b288e308168daf7cb698051a9f2d4eaee9717139978df4e7a9fe854ba269ab94b2b46f1030d2ab9db
@@ -99,10 +99,10 @@ module Scimitar
99
99
  def require_scim
100
100
  scim_mime_type = Mime::Type.lookup_by_extension(:scim).to_s
101
101
 
102
- if request.content_type.nil?
102
+ if request.media_type.nil? || request.media_type.empty?
103
103
  request.format = :scim
104
104
  request.headers['CONTENT_TYPE'] = scim_mime_type
105
- elsif request.content_type&.downcase == scim_mime_type
105
+ elsif request.media_type.downcase == scim_mime_type
106
106
  request.format = :scim
107
107
  elsif request.format == :scim
108
108
  request.headers['CONTENT_TYPE'] = scim_mime_type
@@ -443,9 +443,30 @@ module Scimitar
443
443
  ci_scim_hash = { 'root' => ci_scim_hash }.with_indifferent_case_insensitive_access()
444
444
  end
445
445
 
446
+ # Handle extension schema. Contributed by @bettysteger and
447
+ # @MorrisFreeman via:
448
+ #
449
+ # https://github.com/RIPAGlobal/scimitar/issues/48
450
+ # https://github.com/RIPAGlobal/scimitar/pull/49
451
+ #
452
+ # Note the ":" separating the schema ID (URN) from the attribute.
453
+ # The nature of JSON rendering / other payloads might lead you to
454
+ # expect a "." as with any complex types, but that's not the case;
455
+ # see https://tools.ietf.org/html/rfc7644#section-3.10, or
456
+ # https://tools.ietf.org/html/rfc7644#section-3.5.2 of which in
457
+ # particular, https://tools.ietf.org/html/rfc7644#page-35.
458
+ #
459
+ paths = []
460
+ self.class.scim_resource_type.extended_schemas.each do |schema|
461
+ path_str.downcase.split(schema.id.downcase + ':').drop(1).each do |path|
462
+ paths += [schema.id] + path.split('.')
463
+ end
464
+ end
465
+ paths = path_str.split('.') if paths.empty?
466
+
446
467
  self.from_patch_backend!(
447
468
  nature: nature,
448
- path: (path_str || '').split('.'),
469
+ path: paths,
449
470
  value: value,
450
471
  altering_hash: ci_scim_hash
451
472
  )
@@ -616,7 +637,19 @@ module Scimitar
616
637
  attrs_map_or_leaf_value.each do | scim_attribute, sub_attrs_map_or_leaf_value |
617
638
  next if scim_attribute&.to_s&.downcase == 'id' && path.empty?
618
639
 
619
- sub_scim_hash_or_leaf_value = scim_hash_or_leaf_value&.dig(scim_attribute.to_s)
640
+ # Handle extension schema. Contributed by @bettysteger and
641
+ # @MorrisFreeman via:
642
+ #
643
+ # https://github.com/RIPAGlobal/scimitar/issues/48
644
+ # https://github.com/RIPAGlobal/scimitar/pull/49
645
+ #
646
+ attribute_tree = []
647
+ resource_class.extended_schemas.each do |schema|
648
+ attribute_tree << schema.id and break if schema.scim_attributes.any? { |attribute| attribute.name == scim_attribute.to_s }
649
+ end
650
+ attribute_tree << scim_attribute.to_s
651
+
652
+ sub_scim_hash_or_leaf_value = scim_hash_or_leaf_value&.dig(*attribute_tree)
620
653
 
621
654
  self.from_scim_backend!(
622
655
  attrs_map_or_leaf_value: sub_attrs_map_or_leaf_value,
@@ -914,7 +947,7 @@ module Scimitar
914
947
  # which would imply "payload removes all users", there is the
915
948
  # clear intent to remove just one.
916
949
  #
917
- # https://www.rfc-editor.org/rfc/rfc7644#section-3.5.2.2
950
+ # https://tools.ietf.org/html/rfc7644#section-3.5.2.2
918
951
  # https://learn.microsoft.com/en-us/azure/active-directory/app-provisioning/use-scim-to-provision-users-and-groups#update-group-remove-members
919
952
  #
920
953
  # Since remove-all in the face of remove-one is destructive, we
@@ -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.5.0'
6
+ VERSION = '1.5.2'
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-01-27'
11
+ DATE = '2023-03-21'
12
12
 
13
13
  end
@@ -13,6 +13,8 @@ class MockUser < ActiveRecord::Base
13
13
  work_email_address
14
14
  home_email_address
15
15
  work_phone_number
16
+ organization
17
+ department
16
18
  }
17
19
 
18
20
  has_and_belongs_to_many :mock_groups
@@ -82,7 +84,13 @@ class MockUser < ActiveRecord::Base
82
84
  }
83
85
  }
84
86
  ],
85
- active: :is_active
87
+ active: :is_active,
88
+
89
+ # Custom extension schema - see configuration in
90
+ # "spec/apps/dummy/config/initializers/scimitar.rb".
91
+ #
92
+ organization: :organization,
93
+ department: :department
86
94
  }
87
95
  end
88
96
 
@@ -1,5 +1,14 @@
1
1
  # Test app configuration.
2
2
  #
3
+ # Note that as a result of https://github.com/RIPAGlobal/scimitar/issues/48,
4
+ # tests include a custom extension of the core User schema. A shortcoming of
5
+ # some of the code from which Scimitar was originally built is that those
6
+ # extensions are done with class-level ivars, so it is largely impossible (or
7
+ # at least, impractical in tests) to avoid polluting the core class itself
8
+ # with the extension.
9
+ #
10
+ # All related schema tests are written with this in mind.
11
+ #
3
12
  Scimitar.engine_configuration = Scimitar::EngineConfiguration.new({
4
13
 
5
14
  application_controller_mixin: Module.new do
@@ -12,3 +21,31 @@ Scimitar.engine_configuration = Scimitar::EngineConfiguration.new({
12
21
  end
13
22
 
14
23
  })
24
+
25
+ module ScimSchemaExtensions
26
+ module User
27
+ class Enterprise < Scimitar::Schema::Base
28
+ def initialize(options = {})
29
+ super(
30
+ name: 'ExtendedUser',
31
+ description: 'Enterprise extension for a User',
32
+ id: self.class.id,
33
+ scim_attributes: self.class.scim_attributes
34
+ )
35
+ end
36
+
37
+ def self.id
38
+ 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'
39
+ end
40
+
41
+ def self.scim_attributes
42
+ [
43
+ Scimitar::Schema::Attribute.new(name: 'organization', type: 'string'),
44
+ Scimitar::Schema::Attribute.new(name: 'department', type: 'string')
45
+ ]
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ Scimitar::Resources::User.extend_schema ScimSchemaExtensions::User::Enterprise
@@ -3,6 +3,8 @@ class CreateMockUsers < ActiveRecord::Migration[6.1]
3
3
  create_table :mock_users, id: :uuid, primary_key: :primary_key do |t|
4
4
  t.timestamps
5
5
 
6
+ # Support part of the core schema
7
+ #
6
8
  t.text :scim_uid
7
9
  t.text :username
8
10
  t.text :first_name
@@ -10,6 +12,12 @@ class CreateMockUsers < ActiveRecord::Migration[6.1]
10
12
  t.text :work_email_address
11
13
  t.text :home_email_address
12
14
  t.text :work_phone_number
15
+
16
+ # Support the custom extension schema - see configuration in
17
+ # "spec/apps/dummy/config/initializers/scimitar.rb".
18
+ #
19
+ t.text :organization
20
+ t.text :department
13
21
  end
14
22
  end
15
23
  end
@@ -39,6 +39,8 @@ ActiveRecord::Schema.define(version: 2021_03_08_044214) do
39
39
  t.text "work_email_address"
40
40
  t.text "home_email_address"
41
41
  t.text "work_phone_number"
42
+ t.text "organization"
43
+ t.text "department"
42
44
  end
43
45
 
44
46
  add_foreign_key "mock_groups_users", "mock_groups"
@@ -14,9 +14,9 @@ RSpec.describe Scimitar::SchemasController do
14
14
  get :index, params: { format: :scim }
15
15
  expect(response).to be_ok
16
16
  parsed_body = JSON.parse(response.body)
17
- expect(parsed_body.length).to eql(2)
17
+ expect(parsed_body.length).to eql(3)
18
18
  schema_names = parsed_body.map {|schema| schema['name']}
19
- expect(schema_names).to match_array(['User', 'Group'])
19
+ expect(schema_names).to match_array(['User', 'ExtendedUser', 'Group'])
20
20
  end
21
21
 
22
22
  it 'returns only the User schema when its id is provided' do
@@ -250,90 +250,185 @@ RSpec.describe Scimitar::Resources::Base do
250
250
  end # "context 'dynamic setters based on schema' do"
251
251
 
252
252
  context 'schema extension' do
253
- ThirdCustomSchema = Class.new(Scimitar::Schema::Base) do
254
- def self.id
255
- 'custom-id'
256
- end
253
+ context 'of custom schema' do
254
+ ThirdCustomSchema = Class.new(Scimitar::Schema::Base) do
255
+ def self.id
256
+ 'custom-id'
257
+ end
257
258
 
258
- def self.scim_attributes
259
- [ Scimitar::Schema::Attribute.new(name: 'name', type: 'string') ]
259
+ def self.scim_attributes
260
+ [ Scimitar::Schema::Attribute.new(name: 'name', type: 'string') ]
261
+ end
260
262
  end
261
- end
262
263
 
263
- ExtensionSchema = Class.new(Scimitar::Schema::Base) do
264
- def self.id
265
- 'extension-id'
266
- end
264
+ ExtensionSchema = Class.new(Scimitar::Schema::Base) do
265
+ def self.id
266
+ 'extension-id'
267
+ end
267
268
 
268
- def self.scim_attributes
269
- [ Scimitar::Schema::Attribute.new(name: 'relationship', type: 'string', required: true) ]
269
+ def self.scim_attributes
270
+ [ Scimitar::Schema::Attribute.new(name: 'relationship', type: 'string', required: true) ]
271
+ end
270
272
  end
271
- end
272
273
 
273
- let(:resource_class) {
274
- Class.new(Scimitar::Resources::Base) do
275
- set_schema ThirdCustomSchema
276
- extend_schema ExtensionSchema
274
+ let(:resource_class) {
275
+ Class.new(Scimitar::Resources::Base) do
276
+ set_schema ThirdCustomSchema
277
+ extend_schema ExtensionSchema
277
278
 
278
- def self.endpoint
279
- '/gaga'
279
+ def self.endpoint
280
+ '/gaga'
281
+ end
282
+
283
+ def self.resource_type_id
284
+ 'CustomResource'
285
+ end
280
286
  end
287
+ }
281
288
 
282
- def self.resource_type_id
283
- 'CustomResource'
289
+ context '#initialize' do
290
+ it 'allows setting extension attributes' do
291
+ resource = resource_class.new('extension-id' => {relationship: 'GAGA'})
292
+ expect(resource.relationship).to eql('GAGA')
284
293
  end
285
- end
286
- }
294
+ end # "context '#initialize' do"
295
+
296
+ context '#as_json' do
297
+ it 'namespaces the extension attributes' do
298
+ resource = resource_class.new(relationship: 'GAGA')
299
+ hash = resource.as_json
300
+ expect(hash["schemas"]).to eql(['custom-id', 'extension-id'])
301
+ expect(hash["extension-id"]).to eql("relationship" => 'GAGA')
302
+ end
303
+ end # "context '#as_json' do"
287
304
 
288
- context '#initialize' do
289
- it 'allows setting extension attributes' do
290
- resource = resource_class.new('extension-id' => {relationship: 'GAGA'})
291
- expect(resource.relationship).to eql('GAGA')
292
- end
293
- end # "context '#initialize' do"
305
+ context '.resource_type' do
306
+ it 'appends the extension schemas' do
307
+ resource_type = resource_class.resource_type('http://gaga')
308
+ expect(resource_type.meta.location).to eql('http://gaga')
309
+ expect(resource_type.schemaExtensions.count).to eql(1)
310
+ end
294
311
 
295
- context '#as_json' do
296
- it 'namespaces the extension attributes' do
297
- resource = resource_class.new(relationship: 'GAGA')
298
- hash = resource.as_json
299
- expect(hash["schemas"]).to eql(['custom-id', 'extension-id'])
300
- expect(hash["extension-id"]).to eql("relationship" => 'GAGA')
301
- end
302
- end # "context '#as_json' do"
312
+ context 'validation' do
313
+ it 'validates into custom schema' do
314
+ resource = resource_class.new('extension-id' => {})
315
+ expect(resource.valid?).to eql(false)
303
316
 
304
- context '.resource_type' do
305
- it 'appends the extension schemas' do
306
- resource_type = resource_class.resource_type('http://gaga')
307
- expect(resource_type.meta.location).to eql('http://gaga')
308
- expect(resource_type.schemaExtensions.count).to eql(1)
309
- end
317
+ resource = resource_class.new('extension-id' => {relationship: 'GAGA'})
318
+ expect(resource.relationship).to eql('GAGA')
319
+ expect(resource.valid?).to eql(true)
320
+ end
321
+ end # context 'validation'
322
+ end # "context '.resource_type' do"
310
323
 
311
- context 'validation' do
312
- it 'validates into custom schema' do
313
- resource = resource_class.new('extension-id' => {})
314
- expect(resource.valid?).to eql(false)
324
+ context '.find_attribute' do
325
+ it 'finds in first schema' do
326
+ found = resource_class().find_attribute('name') # Defined in ThirdCustomSchema
327
+ expect(found).to be_present
328
+ expect(found.name).to eql('name')
329
+ expect(found.type).to eql('string')
330
+ end
315
331
 
316
- resource = resource_class.new('extension-id' => {relationship: 'GAGA'})
317
- expect(resource.relationship).to eql('GAGA')
318
- expect(resource.valid?).to eql(true)
332
+ it 'finds across schemas' do
333
+ found = resource_class().find_attribute('relationship') # Defined in ExtensionSchema
334
+ expect(found).to be_present
335
+ expect(found.name).to eql('relationship')
336
+ expect(found.type).to eql('string')
319
337
  end
320
- end # context 'validation'
321
- end # "context '.resource_type' do"
338
+ end # "context '.find_attribute' do"
339
+ end # "context 'of custom schema' do"
322
340
 
323
- context '.find_attribute' do
324
- it 'finds in first schema' do
325
- found = resource_class().find_attribute('name') # Defined in ThirdCustomSchema
326
- expect(found).to be_present
327
- expect(found.name).to eql('name')
328
- expect(found.type).to eql('string')
329
- end
341
+ context 'of core schema' do
342
+ EnterpriseExtensionSchema = Class.new(Scimitar::Schema::Base) do
343
+ def self.id
344
+ 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'
345
+ end
330
346
 
331
- it 'finds across schemas' do
332
- found = resource_class().find_attribute('relationship') # Defined in ExtensionSchema
333
- expect(found).to be_present
334
- expect(found.name).to eql('relationship')
335
- expect(found.type).to eql('string')
347
+ def self.scim_attributes
348
+ [
349
+ Scimitar::Schema::Attribute.new(name: 'organization', type: 'string'),
350
+ Scimitar::Schema::Attribute.new(name: 'department', type: 'string')
351
+ ]
352
+ end
336
353
  end
337
- end # "context '.find_attribute' do"
354
+
355
+ let(:resource_class) {
356
+ Class.new(Scimitar::Resources::Base) do
357
+ set_schema Scimitar::Schema::User
358
+ extend_schema EnterpriseExtensionSchema
359
+
360
+ def self.endpoint
361
+ '/Users'
362
+ end
363
+
364
+ def self.resource_type_id
365
+ 'User'
366
+ end
367
+ end
368
+ }
369
+
370
+ context '#initialize' do
371
+ it 'allows setting extension attributes' do
372
+ resource = resource_class.new('urn:ietf:params:scim:schemas:extension:enterprise:2.0:User' => {organization: 'SOMEORG', department: 'SOMEDPT'})
373
+
374
+ expect(resource.organization).to eql('SOMEORG')
375
+ expect(resource.department ).to eql('SOMEDPT')
376
+ end
377
+ end # "context '#initialize' do"
378
+
379
+ context '#as_json' do
380
+ it 'namespaces the extension attributes' do
381
+ resource = resource_class.new(organization: 'SOMEORG', department: 'SOMEDPT')
382
+ hash = resource.as_json
383
+
384
+ expect(hash['schemas']).to eql(['urn:ietf:params:scim:schemas:core:2.0:User', 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'])
385
+ expect(hash['urn:ietf:params:scim:schemas:extension:enterprise:2.0:User']).to eql('organization' => 'SOMEORG', 'department' => 'SOMEDPT')
386
+ end
387
+ end # "context '#as_json' do"
388
+
389
+ context '.resource_type' do
390
+ it 'appends the extension schemas' do
391
+ resource_type = resource_class.resource_type('http://example.com')
392
+ expect(resource_type.meta.location).to eql('http://example.com')
393
+ expect(resource_type.schemaExtensions.count).to eql(1)
394
+ end
395
+
396
+ context 'validation' do
397
+ it 'validates into custom schema' do
398
+ resource = resource_class.new('urn:ietf:params:scim:schemas:extension:enterprise:2.0:User' => {})
399
+ expect(resource.valid?).to eql(false)
400
+
401
+ resource = resource_class.new(
402
+ 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User' => {
403
+ userName: 'SOMEUSR',
404
+ organization: 'SOMEORG',
405
+ department: 'SOMEDPT'
406
+ }
407
+ )
408
+
409
+ expect(resource.organization).to eql('SOMEORG')
410
+ expect(resource.department ).to eql('SOMEDPT')
411
+ expect(resource.valid? ).to eql(true)
412
+ end
413
+ end # context 'validation'
414
+ end # "context '.resource_type' do"
415
+
416
+ context '.find_attribute' do
417
+ it 'finds in first schema' do
418
+ found = resource_class().find_attribute('userName') # Defined in Scimitar::Schema::User
419
+
420
+ expect(found).to be_present
421
+ expect(found.name).to eql('userName')
422
+ expect(found.type).to eql('string')
423
+ end
424
+
425
+ it 'finds across schemas' do
426
+ found = resource_class().find_attribute('organization') # Defined in EnterpriseExtensionSchema
427
+ expect(found).to be_present
428
+ expect(found.name).to eql('organization')
429
+ expect(found.type).to eql('string')
430
+ end
431
+ end # "context '.find_attribute' do"
432
+ end # "context 'of core schema' do"
338
433
  end # "context 'schema extension' do"
339
434
  end
@@ -172,6 +172,7 @@ RSpec.describe Scimitar::Resources::Mixin do
172
172
  instance.work_email_address = 'foo.bar@test.com'
173
173
  instance.home_email_address = nil
174
174
  instance.work_phone_number = '+642201234567'
175
+ instance.organization = 'SOMEORG'
175
176
 
176
177
  g1 = MockGroup.create!(display_name: 'Group 1')
177
178
  g2 = MockGroup.create!(display_name: 'Group 2')
@@ -194,7 +195,12 @@ RSpec.describe Scimitar::Resources::Mixin do
194
195
  'externalId' => 'AA02984',
195
196
  'groups' => [{'display'=>g1.display_name, 'value'=>g1.id.to_s}, {'display'=>g3.display_name, 'value'=>g3.id.to_s}],
196
197
  'meta' => {'location'=>"https://test.com/mock_users/#{uuid}", 'resourceType'=>'User'},
197
- 'schemas' => ['urn:ietf:params:scim:schemas:core:2.0:User']
198
+ 'schemas' => ['urn:ietf:params:scim:schemas:core:2.0:User', 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'],
199
+
200
+ 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User' => {
201
+ 'organization' => 'SOMEORG',
202
+ 'department' => nil
203
+ }
198
204
  })
199
205
  end
200
206
  end # "context 'with a UUID, renamed primary key column' do"
@@ -318,7 +324,9 @@ RSpec.describe Scimitar::Resources::Mixin do
318
324
  ],
319
325
 
320
326
  'meta' => {'location'=>'https://test.com/static_map_test', 'resourceType'=>'User'},
321
- 'schemas' => ['urn:ietf:params:scim:schemas:core:2.0:User']
327
+ 'schemas' => ['urn:ietf:params:scim:schemas:core:2.0:User', 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'],
328
+
329
+ 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User' => {}
322
330
  })
323
331
  end
324
332
  end # "context 'using static mappings' do"
@@ -345,7 +353,9 @@ RSpec.describe Scimitar::Resources::Mixin do
345
353
  ],
346
354
 
347
355
  'meta' => {'location'=>'https://test.com/dynamic_map_test', 'resourceType'=>'User'},
348
- 'schemas' => ['urn:ietf:params:scim:schemas:core:2.0:User']
356
+ 'schemas' => ['urn:ietf:params:scim:schemas:core:2.0:User', 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'],
357
+
358
+ 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User' => {}
349
359
  })
350
360
  end
351
361
  end # "context 'using dynamic lists' do"
@@ -402,7 +412,12 @@ RSpec.describe Scimitar::Resources::Mixin do
402
412
  'id' => '42', # Note, String
403
413
  'externalId' => 'AA02984',
404
414
  'meta' => {'location' => 'https://test.com/mock_users/42', 'resourceType' => 'User'},
405
- 'schemas' => ['urn:ietf:params:scim:schemas:core:2.0:User']
415
+ 'schemas' => ['urn:ietf:params:scim:schemas:core:2.0:User', 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'],
416
+
417
+ 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User' => {
418
+ 'organization' => 'SOMEORG',
419
+ 'DEPARTMENT' => 'SOMEDPT'
420
+ }
406
421
  }
407
422
 
408
423
  hash = spec_helper_hupcase(hash) if force_upper_case
@@ -418,6 +433,8 @@ RSpec.describe Scimitar::Resources::Mixin do
418
433
  expect(instance.work_email_address).to eql('foo.bar@test.com')
419
434
  expect(instance.home_email_address).to be_nil
420
435
  expect(instance.work_phone_number ).to eql('+642201234567')
436
+ expect(instance.organization ).to eql('SOMEORG')
437
+ expect(instance.department ).to eql('SOMEDPT')
421
438
  end
422
439
 
423
440
  it 'honouring read-write lists' do
@@ -704,6 +721,21 @@ RSpec.describe Scimitar::Resources::Mixin do
704
721
  expect(scim_hash['name']['familyName']).to eql('Bar')
705
722
  end
706
723
 
724
+ it 'with schema extensions: overwrites' do
725
+ path = [ 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User', 'organization' ]
726
+ scim_hash = { 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User' => { 'organization' => 'SOMEORG' } }.with_indifferent_case_insensitive_access()
727
+
728
+ @instance.send(
729
+ :from_patch_backend!,
730
+ nature: 'add',
731
+ path: path,
732
+ value: 'OTHERORG',
733
+ altering_hash: scim_hash
734
+ )
735
+
736
+ expect(scim_hash['urn:ietf:params:scim:schemas:extension:enterprise:2.0:User']['organization' ]).to eql('OTHERORG')
737
+ end
738
+
707
739
  # For 'add', filter at end-of-path is nonsensical and not
708
740
  # supported by spec or Scimitar; we only test mid-path filters.
709
741
  #
@@ -892,6 +924,21 @@ RSpec.describe Scimitar::Resources::Mixin do
892
924
  expect(scim_hash['name']['givenName']).to eql('Baz')
893
925
  end
894
926
 
927
+ it 'with schema extensions: adds' do
928
+ path = [ 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User', 'organization' ]
929
+ scim_hash = {}.with_indifferent_case_insensitive_access()
930
+
931
+ @instance.send(
932
+ :from_patch_backend!,
933
+ nature: 'add',
934
+ path: path,
935
+ value: 'SOMEORG',
936
+ altering_hash: scim_hash
937
+ )
938
+
939
+ expect(scim_hash['urn:ietf:params:scim:schemas:extension:enterprise:2.0:User']['organization' ]).to eql('SOMEORG')
940
+ end
941
+
895
942
  context 'with filter mid-path: adds' do
896
943
  it 'by string match' do
897
944
  path = [ 'emails[type eq "work"]', 'value' ]
@@ -1233,7 +1280,7 @@ RSpec.describe Scimitar::Resources::Mixin do
1233
1280
 
1234
1281
  # What we expect:
1235
1282
  #
1236
- # https://www.rfc-editor.org/rfc/rfc7644#section-3.5.2.2
1283
+ # https://tools.ietf.org/html/rfc7644#section-3.5.2.2
1237
1284
  # https://docs.snowflake.com/en/user-guide/scim-intro.html#patch-scim-v2-groups-id
1238
1285
  #
1239
1286
  # ...vs accounting for the unusual payloads we sometimes get,
@@ -2680,6 +2727,38 @@ RSpec.describe Scimitar::Resources::Mixin do
2680
2727
  expect(@instance.first_name).to eql('Baz')
2681
2728
  end
2682
2729
 
2730
+ # Note odd ":" separating schema ID from first attribute, although
2731
+ # the nature of JSON rendering / other payloads might lead you to
2732
+ # expect a "." as with any other path component.
2733
+ #
2734
+ # Note the ":" separating the schema ID (URN) from the attribute.
2735
+ # The nature of JSON rendering / other payloads might lead you to
2736
+ # expect a "." as with any complex types, but that's not the case;
2737
+ # see https://tools.ietf.org/html/rfc7644#section-3.10, or
2738
+ # https://tools.ietf.org/html/rfc7644#section-3.5.2 of which in
2739
+ # particular, https://tools.ietf.org/html/rfc7644#page-35.
2740
+ #
2741
+ it 'which updates attributes defined by extension schema' do
2742
+ @instance.update!(department: 'SOMEDPT')
2743
+
2744
+ path = 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:department'
2745
+ path = path.upcase if force_upper_case
2746
+
2747
+ patch = {
2748
+ 'schemas' => ['urn:ietf:params:scim:api:messages:2.0:PatchOp'],
2749
+ 'Operations' => [
2750
+ {
2751
+ 'op' => 'replace',
2752
+ 'path' => path,
2753
+ 'value' => 'OTHERDPT'
2754
+ }
2755
+ ]
2756
+ }
2757
+
2758
+ @instance.from_scim_patch!(patch_hash: patch)
2759
+ expect(@instance.department).to eql('OTHERDPT')
2760
+ end
2761
+
2683
2762
  it 'which updates with filter match' do
2684
2763
  @instance.update!(work_email_address: 'work@test.com', home_email_address: 'home@test.com')
2685
2764
 
@@ -762,7 +762,7 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
762
762
  end
763
763
  end
764
764
 
765
- # https://www.rfc-editor.org/rfc/rfc7644#section-3.5.2.2
765
+ # https://tools.ietf.org/html/rfc7644#section-3.5.2.2
766
766
  #
767
767
  context 'and using an RFC-compliant payload' do
768
768
  let(:removed_user) { @u2 }
@@ -39,6 +39,16 @@ RSpec.describe Scimitar::ApplicationController do
39
39
  expect(parsed_body['request']['content_type']).to eql('application/scim+json')
40
40
  end
41
41
 
42
+ it 'translates Content-Type with charset to Rails request format' do
43
+ get '/CustomRequestVerifiers', headers: { 'CONTENT_TYPE' => 'application/scim+json; charset=utf-8' }
44
+
45
+ expect(response).to have_http_status(:ok)
46
+ parsed_body = JSON.parse(response.body)
47
+ expect(parsed_body['request']['is_scim' ]).to eql(true)
48
+ expect(parsed_body['request']['format' ]).to eql('application/scim+json')
49
+ expect(parsed_body['request']['content_type']).to eql('application/scim+json; charset=utf-8')
50
+ end
51
+
42
52
  it 'translates Rails request format to header' do
43
53
  get '/CustomRequestVerifiers', params: { format: :scim }
44
54
 
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: 1.5.0
4
+ version: 1.5.2
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-01-27 00:00:00.000000000 Z
12
+ date: 2023-03-21 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails