scimitar 1.5.0 → 1.5.2

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