scimitar 2.3.0 → 2.4.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 +4 -4
- data/app/models/scimitar/resources/mixin.rb +73 -1
- data/lib/scimitar/version.rb +2 -2
- data/spec/apps/dummy/config/routes.rb +1 -0
- data/spec/models/scimitar/resources/mixin_spec.rb +590 -1
- data/spec/requests/active_record_backed_resources_controller_spec.rb +136 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bccee840f0dc82854974e8d90d19189ae133f4281142c5de89107230b413041b
|
4
|
+
data.tar.gz: 014cbf547dd861c26489aa13d5ab2c75f5826ed24ca3c98d7eb71b76d98994b1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1619af4e575d6701471820c52e39719fd96b311b45b2c5d9907bc147253cf5904926677901d0e778865d71768405774bf7082d8a641bcc4b33c9ef3c4183ec28
|
7
|
+
data.tar.gz: 3b435a7ff4343945de4436c8f6c3e52d4f714e00bdde594074f7dd064befc9d28836946195df80cdae50aa0751f0761dce8cf926b83203efd909b86538f67351
|
@@ -901,14 +901,86 @@ module Scimitar
|
|
901
901
|
else
|
902
902
|
altering_hash[path_component] = value
|
903
903
|
end
|
904
|
+
|
904
905
|
when 'replace'
|
905
906
|
if path_component == 'root'
|
906
907
|
altering_hash[path_component].merge!(value)
|
907
908
|
else
|
908
909
|
altering_hash[path_component] = value
|
909
910
|
end
|
911
|
+
|
912
|
+
# The array check handles payloads seen from e.g. Microsoft for
|
913
|
+
# remove-user-from-group, where contrary to examples in the RFC
|
914
|
+
# which would imply "payload removes all users", there is the
|
915
|
+
# clear intent to remove just one.
|
916
|
+
#
|
917
|
+
# https://www.rfc-editor.org/rfc/rfc7644#section-3.5.2.2
|
918
|
+
# https://learn.microsoft.com/en-us/azure/active-directory/app-provisioning/use-scim-to-provision-users-and-groups#update-group-remove-members
|
919
|
+
#
|
920
|
+
# Since remove-all in the face of remove-one is destructive, we
|
921
|
+
# do a special check here to see if there's an array value for
|
922
|
+
# the array path that the payload yielded. If so, we can match
|
923
|
+
# each value against array items and remove just those items.
|
924
|
+
#
|
925
|
+
# There is an additional special case to handle a bad example
|
926
|
+
# from Salesforce:
|
927
|
+
#
|
928
|
+
# https://help.salesforce.com/s/articleView?id=sf.identity_scim_manage_groups.htm&type=5
|
929
|
+
#
|
910
930
|
when 'remove'
|
911
|
-
altering_hash.
|
931
|
+
if altering_hash[path_component].is_a?(Array) && value.present?
|
932
|
+
|
933
|
+
# Handle bad Salesforce example. That might be simply a
|
934
|
+
# documentation error, but just in case...
|
935
|
+
#
|
936
|
+
value = value.values.first if (
|
937
|
+
path_component&.downcase == 'members' &&
|
938
|
+
value.is_a?(Hash) &&
|
939
|
+
value.keys.size == 1 &&
|
940
|
+
value.keys.first&.downcase == 'members'
|
941
|
+
)
|
942
|
+
|
943
|
+
# The Microsoft example provides an array of values, but we
|
944
|
+
# may as well cope with a value specified 'flat'. Promote
|
945
|
+
# such a thing to an Array to simplify the following code.
|
946
|
+
#
|
947
|
+
value = [value] unless value.is_a?(Array)
|
948
|
+
|
949
|
+
# For each value item, delete matching array entries. The
|
950
|
+
# concept of "matching" is:
|
951
|
+
#
|
952
|
+
# * For simple non-Hash values (if possible) just delete on
|
953
|
+
# an exact match
|
954
|
+
#
|
955
|
+
# * For Hash-based values, only delete if all 'patch' keys
|
956
|
+
# are present in the resource and all values thus match.
|
957
|
+
#
|
958
|
+
# Special case to ignore '$ref' from the Microsoft payload.
|
959
|
+
#
|
960
|
+
# Note coercion to strings to account for SCIM vs the usual
|
961
|
+
# tricky case of underlying implementations with (say)
|
962
|
+
# integer primary keys, which all end up as strings anyway.
|
963
|
+
#
|
964
|
+
value.each do | value_item |
|
965
|
+
altering_hash[path_component].delete_if do | item |
|
966
|
+
if item.is_a?(Hash) && value_item.is_a?(Hash)
|
967
|
+
matched_all = true
|
968
|
+
value_item.each do | value_key, value_value |
|
969
|
+
next if value_key == '$ref'
|
970
|
+
if ! item.key?(value_key) || item[value_key]&.to_s != value_value&.to_s
|
971
|
+
matched_all = false
|
972
|
+
end
|
973
|
+
end
|
974
|
+
matched_all
|
975
|
+
else
|
976
|
+
item&.to_s == value_item&.to_s
|
977
|
+
end
|
978
|
+
end
|
979
|
+
end
|
980
|
+
else
|
981
|
+
altering_hash.delete(path_component)
|
982
|
+
end
|
983
|
+
|
912
984
|
end
|
913
985
|
end
|
914
986
|
end
|
data/lib/scimitar/version.rb
CHANGED
@@ -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.
|
6
|
+
VERSION = '2.4.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-01-
|
11
|
+
DATE = '2023-01-27'
|
12
12
|
|
13
13
|
end
|
@@ -1230,6 +1230,595 @@ RSpec.describe Scimitar::Resources::Mixin do
|
|
1230
1230
|
|
1231
1231
|
expect(scim_hash).to_not have_key('emails')
|
1232
1232
|
end
|
1233
|
+
|
1234
|
+
# What we expect:
|
1235
|
+
#
|
1236
|
+
# https://www.rfc-editor.org/rfc/rfc7644#section-3.5.2.2
|
1237
|
+
# https://docs.snowflake.com/en/user-guide/scim-intro.html#patch-scim-v2-groups-id
|
1238
|
+
#
|
1239
|
+
# ...vs accounting for the unusual payloads we sometimes get,
|
1240
|
+
# tested here.
|
1241
|
+
#
|
1242
|
+
context 'special cases' do
|
1243
|
+
|
1244
|
+
# https://learn.microsoft.com/en-us/azure/active-directory/app-provisioning/use-scim-to-provision-users-and-groups#update-group-remove-members
|
1245
|
+
#
|
1246
|
+
context 'Microsoft-style payload' do
|
1247
|
+
context 'removing a user from a group' do
|
1248
|
+
it 'removes identified user' do
|
1249
|
+
path = [ 'members' ]
|
1250
|
+
value = [ { '$ref' => nil, 'value' => 'f648f8d5ea4e4cd38e9c' } ]
|
1251
|
+
scim_hash = {
|
1252
|
+
'displayname' => 'Mock group',
|
1253
|
+
'members' => [
|
1254
|
+
{
|
1255
|
+
'value' => '50ca93d04ab0c2de4772',
|
1256
|
+
'display' => 'Ingrid Smith',
|
1257
|
+
'type' => 'User'
|
1258
|
+
},
|
1259
|
+
{
|
1260
|
+
'value' => 'f648f8d5ea4e4cd38e9c',
|
1261
|
+
'display' => 'Fred Smith',
|
1262
|
+
'type' => 'User'
|
1263
|
+
},
|
1264
|
+
{
|
1265
|
+
'value' => 'a774d480e8112101375b',
|
1266
|
+
'display' => 'Taylor Smith',
|
1267
|
+
'type' => 'User'
|
1268
|
+
}
|
1269
|
+
]
|
1270
|
+
}.with_indifferent_case_insensitive_access()
|
1271
|
+
|
1272
|
+
@instance.send(
|
1273
|
+
:from_patch_backend!,
|
1274
|
+
nature: 'remove',
|
1275
|
+
path: path,
|
1276
|
+
value: value,
|
1277
|
+
altering_hash: scim_hash
|
1278
|
+
)
|
1279
|
+
|
1280
|
+
expect(scim_hash).to eql({
|
1281
|
+
'displayname' => 'Mock group',
|
1282
|
+
'members' => [
|
1283
|
+
{
|
1284
|
+
'value' => '50ca93d04ab0c2de4772',
|
1285
|
+
'display' => 'Ingrid Smith',
|
1286
|
+
'type' => 'User'
|
1287
|
+
},
|
1288
|
+
{
|
1289
|
+
'value' => 'a774d480e8112101375b',
|
1290
|
+
'display' => 'Taylor Smith',
|
1291
|
+
'type' => 'User'
|
1292
|
+
}
|
1293
|
+
]
|
1294
|
+
})
|
1295
|
+
end
|
1296
|
+
|
1297
|
+
it 'removes multiple identified users' do
|
1298
|
+
path = [ 'members' ]
|
1299
|
+
value = [
|
1300
|
+
{ '$ref' => nil, 'value' => 'f648f8d5ea4e4cd38e9c' },
|
1301
|
+
{ '$ref' => nil, 'value' => '50ca93d04ab0c2de4772' }
|
1302
|
+
]
|
1303
|
+
scim_hash = {
|
1304
|
+
'displayname' => 'Mock group',
|
1305
|
+
'members' => [
|
1306
|
+
{
|
1307
|
+
'value' => '50ca93d04ab0c2de4772',
|
1308
|
+
'display' => 'Ingrid Smith',
|
1309
|
+
'type' => 'User'
|
1310
|
+
},
|
1311
|
+
{
|
1312
|
+
'value' => 'f648f8d5ea4e4cd38e9c',
|
1313
|
+
'display' => 'Fred Smith',
|
1314
|
+
'type' => 'User'
|
1315
|
+
},
|
1316
|
+
{
|
1317
|
+
'value' => 'a774d480e8112101375b',
|
1318
|
+
'display' => 'Taylor Smith',
|
1319
|
+
'type' => 'User'
|
1320
|
+
}
|
1321
|
+
]
|
1322
|
+
}.with_indifferent_case_insensitive_access()
|
1323
|
+
|
1324
|
+
@instance.send(
|
1325
|
+
:from_patch_backend!,
|
1326
|
+
nature: 'remove',
|
1327
|
+
path: path,
|
1328
|
+
value: value,
|
1329
|
+
altering_hash: scim_hash
|
1330
|
+
)
|
1331
|
+
|
1332
|
+
expect(scim_hash).to eql({
|
1333
|
+
'displayname' => 'Mock group',
|
1334
|
+
'members' => [
|
1335
|
+
{
|
1336
|
+
'value' => 'a774d480e8112101375b',
|
1337
|
+
'display' => 'Taylor Smith',
|
1338
|
+
'type' => 'User'
|
1339
|
+
}
|
1340
|
+
]
|
1341
|
+
})
|
1342
|
+
end
|
1343
|
+
|
1344
|
+
it 'removes all users individually without error' do
|
1345
|
+
path = [ 'members' ]
|
1346
|
+
value = [ { '$ref' => nil, 'value' => 'f648f8d5ea4e4cd38e9c' } ]
|
1347
|
+
scim_hash = {
|
1348
|
+
'displayname' => 'Mock group',
|
1349
|
+
'members' => [
|
1350
|
+
{
|
1351
|
+
'value' => 'f648f8d5ea4e4cd38e9c',
|
1352
|
+
'display' => 'Fred Smith',
|
1353
|
+
'type' => 'User'
|
1354
|
+
}
|
1355
|
+
]
|
1356
|
+
}.with_indifferent_case_insensitive_access()
|
1357
|
+
|
1358
|
+
@instance.send(
|
1359
|
+
:from_patch_backend!,
|
1360
|
+
nature: 'remove',
|
1361
|
+
path: path,
|
1362
|
+
value: value,
|
1363
|
+
altering_hash: scim_hash
|
1364
|
+
)
|
1365
|
+
|
1366
|
+
expect(scim_hash).to eql({
|
1367
|
+
'displayname' => 'Mock group',
|
1368
|
+
'members' => []
|
1369
|
+
})
|
1370
|
+
end
|
1371
|
+
|
1372
|
+
it 'can match on multiple attributes' do
|
1373
|
+
path = [ 'members' ]
|
1374
|
+
value = [ { '$ref' => nil, 'value' => 'f648f8d5ea4e4cd38e9c', 'type' => 'User' } ]
|
1375
|
+
scim_hash = {
|
1376
|
+
'displayname' => 'Mock group',
|
1377
|
+
'members' => [
|
1378
|
+
{
|
1379
|
+
'value' => 'f648f8d5ea4e4cd38e9c',
|
1380
|
+
'display' => 'Fred Smith',
|
1381
|
+
'type' => 'User'
|
1382
|
+
}
|
1383
|
+
]
|
1384
|
+
}.with_indifferent_case_insensitive_access()
|
1385
|
+
|
1386
|
+
@instance.send(
|
1387
|
+
:from_patch_backend!,
|
1388
|
+
nature: 'remove',
|
1389
|
+
path: path,
|
1390
|
+
value: value,
|
1391
|
+
altering_hash: scim_hash
|
1392
|
+
)
|
1393
|
+
|
1394
|
+
expect(scim_hash).to eql({
|
1395
|
+
'displayname' => 'Mock group',
|
1396
|
+
'members' => []
|
1397
|
+
})
|
1398
|
+
end
|
1399
|
+
|
1400
|
+
it 'ignores unrecognised users' do
|
1401
|
+
path = [ 'members' ]
|
1402
|
+
value = [ { '$ref' => nil, 'value' => '11b054a9c85216ed9356' } ]
|
1403
|
+
scim_hash = {
|
1404
|
+
'displayname' => 'Mock group',
|
1405
|
+
'members' => [
|
1406
|
+
{
|
1407
|
+
'value' => 'f648f8d5ea4e4cd38e9c',
|
1408
|
+
'display' => 'Fred Smith',
|
1409
|
+
'type' => 'User'
|
1410
|
+
}
|
1411
|
+
]
|
1412
|
+
}.with_indifferent_case_insensitive_access()
|
1413
|
+
|
1414
|
+
@instance.send(
|
1415
|
+
:from_patch_backend!,
|
1416
|
+
nature: 'remove',
|
1417
|
+
path: path,
|
1418
|
+
value: value,
|
1419
|
+
altering_hash: scim_hash
|
1420
|
+
)
|
1421
|
+
|
1422
|
+
# The 'value' mismatched, so the user was not removed.
|
1423
|
+
#
|
1424
|
+
expect(scim_hash).to eql({
|
1425
|
+
'displayname' => 'Mock group',
|
1426
|
+
'members' => [
|
1427
|
+
{
|
1428
|
+
'value' => 'f648f8d5ea4e4cd38e9c',
|
1429
|
+
'display' => 'Fred Smith',
|
1430
|
+
'type' => 'User'
|
1431
|
+
}
|
1432
|
+
]
|
1433
|
+
})
|
1434
|
+
end
|
1435
|
+
|
1436
|
+
it 'ignores a mismatch on (for example) "type"' do
|
1437
|
+
path = [ 'members' ]
|
1438
|
+
value = [ { '$ref' => nil, 'value' => 'f648f8d5ea4e4cd38e9c', 'type' => 'Group' } ]
|
1439
|
+
scim_hash = {
|
1440
|
+
'displayname' => 'Mock group',
|
1441
|
+
'members' => [
|
1442
|
+
{
|
1443
|
+
'value' => 'f648f8d5ea4e4cd38e9c',
|
1444
|
+
'display' => 'Fred Smith',
|
1445
|
+
'type' => 'User'
|
1446
|
+
}
|
1447
|
+
]
|
1448
|
+
}.with_indifferent_case_insensitive_access()
|
1449
|
+
|
1450
|
+
@instance.send(
|
1451
|
+
:from_patch_backend!,
|
1452
|
+
nature: 'remove',
|
1453
|
+
path: path,
|
1454
|
+
value: value,
|
1455
|
+
altering_hash: scim_hash
|
1456
|
+
)
|
1457
|
+
|
1458
|
+
# Type 'Group' mismatches 'User', so the user was not
|
1459
|
+
# removed.
|
1460
|
+
#
|
1461
|
+
expect(scim_hash).to eql({
|
1462
|
+
'displayname' => 'Mock group',
|
1463
|
+
'members' => [
|
1464
|
+
{
|
1465
|
+
'value' => 'f648f8d5ea4e4cd38e9c',
|
1466
|
+
'display' => 'Fred Smith',
|
1467
|
+
'type' => 'User'
|
1468
|
+
}
|
1469
|
+
]
|
1470
|
+
})
|
1471
|
+
end
|
1472
|
+
|
1473
|
+
it 'matches keys case-insensitive' do
|
1474
|
+
path = [ 'members' ]
|
1475
|
+
value = [ { '$ref' => nil, 'VALUe' => 'f648f8d5ea4e4cd38e9c' } ]
|
1476
|
+
scim_hash = {
|
1477
|
+
'displayname' => 'Mock group',
|
1478
|
+
'memBERS' => [
|
1479
|
+
{
|
1480
|
+
'vaLUe' => 'f648f8d5ea4e4cd38e9c',
|
1481
|
+
'display' => 'Fred Smith',
|
1482
|
+
'type' => 'User'
|
1483
|
+
}
|
1484
|
+
]
|
1485
|
+
}.with_indifferent_case_insensitive_access()
|
1486
|
+
|
1487
|
+
@instance.send(
|
1488
|
+
:from_patch_backend!,
|
1489
|
+
nature: 'remove',
|
1490
|
+
path: path,
|
1491
|
+
value: value,
|
1492
|
+
altering_hash: scim_hash
|
1493
|
+
)
|
1494
|
+
|
1495
|
+
expect(scim_hash).to eql({
|
1496
|
+
'displayname' => 'Mock group',
|
1497
|
+
'members' => []
|
1498
|
+
})
|
1499
|
+
end
|
1500
|
+
|
1501
|
+
it 'matches values case-sensitive' do
|
1502
|
+
path = [ 'members' ]
|
1503
|
+
value = [ { '$ref' => nil, 'value' => 'f648f8d5ea4e4cd38e9c', 'type' => 'USER' } ]
|
1504
|
+
scim_hash = {
|
1505
|
+
'displayname' => 'Mock group',
|
1506
|
+
'members' => [
|
1507
|
+
{
|
1508
|
+
'value' => 'f648f8d5ea4e4cd38e9c',
|
1509
|
+
'display' => 'Fred Smith',
|
1510
|
+
'type' => 'User'
|
1511
|
+
}
|
1512
|
+
]
|
1513
|
+
}.with_indifferent_case_insensitive_access()
|
1514
|
+
|
1515
|
+
@instance.send(
|
1516
|
+
:from_patch_backend!,
|
1517
|
+
nature: 'remove',
|
1518
|
+
path: path,
|
1519
|
+
value: value,
|
1520
|
+
altering_hash: scim_hash
|
1521
|
+
)
|
1522
|
+
|
1523
|
+
# USER mismatchs User, so the user was not removed.
|
1524
|
+
#
|
1525
|
+
expect(scim_hash).to eql({
|
1526
|
+
'displayname' => 'Mock group',
|
1527
|
+
'members' => [
|
1528
|
+
{
|
1529
|
+
'value' => 'f648f8d5ea4e4cd38e9c',
|
1530
|
+
'display' => 'Fred Smith',
|
1531
|
+
'type' => 'User'
|
1532
|
+
}
|
1533
|
+
]
|
1534
|
+
})
|
1535
|
+
end
|
1536
|
+
end # "context 'removing a user from a group' do"
|
1537
|
+
|
1538
|
+
context 'generic use' do
|
1539
|
+
it 'removes matched items' do
|
1540
|
+
path = [ 'emails' ]
|
1541
|
+
value = [ { 'type' => 'work' } ]
|
1542
|
+
scim_hash = {
|
1543
|
+
'emails' => [
|
1544
|
+
{
|
1545
|
+
'type' => 'home',
|
1546
|
+
'value' => 'home@test.com'
|
1547
|
+
},
|
1548
|
+
{
|
1549
|
+
'type' => 'work',
|
1550
|
+
'value' => 'work@test.com'
|
1551
|
+
}
|
1552
|
+
]
|
1553
|
+
}.with_indifferent_case_insensitive_access()
|
1554
|
+
|
1555
|
+
@instance.send(
|
1556
|
+
:from_patch_backend!,
|
1557
|
+
nature: 'remove',
|
1558
|
+
path: path,
|
1559
|
+
value: value,
|
1560
|
+
altering_hash: scim_hash
|
1561
|
+
)
|
1562
|
+
|
1563
|
+
expect(scim_hash).to eql({
|
1564
|
+
'emails' => [
|
1565
|
+
{
|
1566
|
+
'type' => 'home',
|
1567
|
+
'value' => 'home@test.com'
|
1568
|
+
}
|
1569
|
+
]
|
1570
|
+
})
|
1571
|
+
end
|
1572
|
+
|
1573
|
+
it 'ignores unmatched items' do
|
1574
|
+
path = [ 'emails' ]
|
1575
|
+
value = [ { 'type' => 'missing' } ]
|
1576
|
+
scim_hash = {
|
1577
|
+
'emails' => [
|
1578
|
+
{
|
1579
|
+
'type' => 'home',
|
1580
|
+
'value' => 'home@test.com'
|
1581
|
+
},
|
1582
|
+
{
|
1583
|
+
'type' => 'work',
|
1584
|
+
'value' => 'work@test.com'
|
1585
|
+
}
|
1586
|
+
]
|
1587
|
+
}.with_indifferent_case_insensitive_access()
|
1588
|
+
|
1589
|
+
@instance.send(
|
1590
|
+
:from_patch_backend!,
|
1591
|
+
nature: 'remove',
|
1592
|
+
path: path,
|
1593
|
+
value: value,
|
1594
|
+
altering_hash: scim_hash
|
1595
|
+
)
|
1596
|
+
|
1597
|
+
expect(scim_hash).to eql({
|
1598
|
+
'emails' => [
|
1599
|
+
{
|
1600
|
+
'type' => 'home',
|
1601
|
+
'value' => 'home@test.com'
|
1602
|
+
},
|
1603
|
+
{
|
1604
|
+
'type' => 'work',
|
1605
|
+
'value' => 'work@test.com'
|
1606
|
+
}
|
1607
|
+
]
|
1608
|
+
})
|
1609
|
+
end
|
1610
|
+
|
1611
|
+
it 'compares string forms' do
|
1612
|
+
path = [ 'test' ]
|
1613
|
+
value = [
|
1614
|
+
{ 'active' => true, 'value' => '12' },
|
1615
|
+
{ 'active' => 'false', 'value' => 42 }
|
1616
|
+
]
|
1617
|
+
scim_hash = {
|
1618
|
+
'test' => [
|
1619
|
+
{
|
1620
|
+
'active' => 'true',
|
1621
|
+
'value' => 12
|
1622
|
+
},
|
1623
|
+
{
|
1624
|
+
'active' => false,
|
1625
|
+
'value' => '42'
|
1626
|
+
}
|
1627
|
+
]
|
1628
|
+
}.with_indifferent_case_insensitive_access()
|
1629
|
+
|
1630
|
+
@instance.send(
|
1631
|
+
:from_patch_backend!,
|
1632
|
+
nature: 'remove',
|
1633
|
+
path: path,
|
1634
|
+
value: value,
|
1635
|
+
altering_hash: scim_hash
|
1636
|
+
)
|
1637
|
+
|
1638
|
+
expect(scim_hash).to eql({'test' => []})
|
1639
|
+
end
|
1640
|
+
|
1641
|
+
it 'handles a singular to-remove value rather than an array' do
|
1642
|
+
path = [ 'emails' ]
|
1643
|
+
value = { 'type' => 'work' }
|
1644
|
+
scim_hash = {
|
1645
|
+
'emails' => [
|
1646
|
+
{
|
1647
|
+
'type' => 'home',
|
1648
|
+
'value' => 'home@test.com'
|
1649
|
+
},
|
1650
|
+
{
|
1651
|
+
'type' => 'work',
|
1652
|
+
'value' => 'work@test.com'
|
1653
|
+
}
|
1654
|
+
]
|
1655
|
+
}.with_indifferent_case_insensitive_access()
|
1656
|
+
|
1657
|
+
@instance.send(
|
1658
|
+
:from_patch_backend!,
|
1659
|
+
nature: 'remove',
|
1660
|
+
path: path,
|
1661
|
+
value: value,
|
1662
|
+
altering_hash: scim_hash
|
1663
|
+
)
|
1664
|
+
|
1665
|
+
expect(scim_hash).to eql({
|
1666
|
+
'emails' => [
|
1667
|
+
{
|
1668
|
+
'type' => 'home',
|
1669
|
+
'value' => 'home@test.com'
|
1670
|
+
}
|
1671
|
+
]
|
1672
|
+
})
|
1673
|
+
end
|
1674
|
+
|
1675
|
+
it 'handles simple values rather than object (Hash) values' do
|
1676
|
+
path = [ 'test' ]
|
1677
|
+
value = 42
|
1678
|
+
scim_hash = {
|
1679
|
+
'test' => [
|
1680
|
+
'21',
|
1681
|
+
'42',
|
1682
|
+
'15'
|
1683
|
+
]
|
1684
|
+
}.with_indifferent_case_insensitive_access()
|
1685
|
+
|
1686
|
+
@instance.send(
|
1687
|
+
:from_patch_backend!,
|
1688
|
+
nature: 'remove',
|
1689
|
+
path: path,
|
1690
|
+
value: value,
|
1691
|
+
altering_hash: scim_hash
|
1692
|
+
)
|
1693
|
+
|
1694
|
+
expect(scim_hash).to eql({
|
1695
|
+
'test' => [
|
1696
|
+
'21',
|
1697
|
+
'15'
|
1698
|
+
]
|
1699
|
+
})
|
1700
|
+
end
|
1701
|
+
end
|
1702
|
+
end # "context 'Microsoft-style payload' do"
|
1703
|
+
|
1704
|
+
# https://help.salesforce.com/s/articleView?id=sf.identity_scim_manage_groups.htm&type=5
|
1705
|
+
#
|
1706
|
+
context 'Salesforce-style payload' do
|
1707
|
+
it 'removes identified user' do
|
1708
|
+
path = [ 'members' ]
|
1709
|
+
value = { 'members' => [ { '$ref' => nil, 'value' => 'f648f8d5ea4e4cd38e9c' } ] }
|
1710
|
+
scim_hash = {
|
1711
|
+
'displayname' => 'Mock group',
|
1712
|
+
'members' => [
|
1713
|
+
{
|
1714
|
+
'value' => '50ca93d04ab0c2de4772',
|
1715
|
+
'display' => 'Ingrid Smith',
|
1716
|
+
'type' => 'User'
|
1717
|
+
},
|
1718
|
+
{
|
1719
|
+
'value' => 'f648f8d5ea4e4cd38e9c',
|
1720
|
+
'display' => 'Fred Smith',
|
1721
|
+
'type' => 'User'
|
1722
|
+
}
|
1723
|
+
]
|
1724
|
+
}.with_indifferent_case_insensitive_access()
|
1725
|
+
|
1726
|
+
@instance.send(
|
1727
|
+
:from_patch_backend!,
|
1728
|
+
nature: 'remove',
|
1729
|
+
path: path,
|
1730
|
+
value: value,
|
1731
|
+
altering_hash: scim_hash
|
1732
|
+
)
|
1733
|
+
|
1734
|
+
expect(scim_hash).to eql({
|
1735
|
+
'displayname' => 'Mock group',
|
1736
|
+
'members' => [
|
1737
|
+
{
|
1738
|
+
'value' => '50ca93d04ab0c2de4772',
|
1739
|
+
'display' => 'Ingrid Smith',
|
1740
|
+
'type' => 'User'
|
1741
|
+
}
|
1742
|
+
]
|
1743
|
+
})
|
1744
|
+
end
|
1745
|
+
|
1746
|
+
it 'matches the "members" key case-insensitive' do
|
1747
|
+
path = [ 'members' ]
|
1748
|
+
value = { 'MEMBERS' => [ { '$ref' => nil, 'value' => 'f648f8d5ea4e4cd38e9c' } ] }
|
1749
|
+
scim_hash = {
|
1750
|
+
'displayname' => 'Mock group',
|
1751
|
+
'members' => [
|
1752
|
+
{
|
1753
|
+
'value' => 'f648f8d5ea4e4cd38e9c',
|
1754
|
+
'display' => 'Fred Smith',
|
1755
|
+
'type' => 'User'
|
1756
|
+
},
|
1757
|
+
{
|
1758
|
+
'value' => 'a774d480e8112101375b',
|
1759
|
+
'display' => 'Taylor Smith',
|
1760
|
+
'type' => 'User'
|
1761
|
+
}
|
1762
|
+
]
|
1763
|
+
}.with_indifferent_case_insensitive_access()
|
1764
|
+
|
1765
|
+
@instance.send(
|
1766
|
+
:from_patch_backend!,
|
1767
|
+
nature: 'remove',
|
1768
|
+
path: path,
|
1769
|
+
value: value,
|
1770
|
+
altering_hash: scim_hash
|
1771
|
+
)
|
1772
|
+
|
1773
|
+
expect(scim_hash).to eql({
|
1774
|
+
'displayname' => 'Mock group',
|
1775
|
+
'members' => [
|
1776
|
+
{
|
1777
|
+
'value' => 'a774d480e8112101375b',
|
1778
|
+
'display' => 'Taylor Smith',
|
1779
|
+
'type' => 'User'
|
1780
|
+
}
|
1781
|
+
]
|
1782
|
+
})
|
1783
|
+
end
|
1784
|
+
|
1785
|
+
it 'ignores unrecognised users' do
|
1786
|
+
path = [ 'members' ]
|
1787
|
+
value = { 'members' => [ { '$ref' => nil, 'value' => '11b054a9c85216ed9356' } ] }
|
1788
|
+
scim_hash = {
|
1789
|
+
'displayname' => 'Mock group',
|
1790
|
+
'members' => [
|
1791
|
+
{
|
1792
|
+
'value' => 'f648f8d5ea4e4cd38e9c',
|
1793
|
+
'display' => 'Fred Smith',
|
1794
|
+
'type' => 'User'
|
1795
|
+
}
|
1796
|
+
]
|
1797
|
+
}.with_indifferent_case_insensitive_access()
|
1798
|
+
|
1799
|
+
@instance.send(
|
1800
|
+
:from_patch_backend!,
|
1801
|
+
nature: 'remove',
|
1802
|
+
path: path,
|
1803
|
+
value: value,
|
1804
|
+
altering_hash: scim_hash
|
1805
|
+
)
|
1806
|
+
|
1807
|
+
# The 'value' mismatched, so the user was not removed.
|
1808
|
+
#
|
1809
|
+
expect(scim_hash).to eql({
|
1810
|
+
'displayname' => 'Mock group',
|
1811
|
+
'members' => [
|
1812
|
+
{
|
1813
|
+
'value' => 'f648f8d5ea4e4cd38e9c',
|
1814
|
+
'display' => 'Fred Smith',
|
1815
|
+
'type' => 'User'
|
1816
|
+
}
|
1817
|
+
]
|
1818
|
+
})
|
1819
|
+
end
|
1820
|
+
end # "context 'Salesforce-style payload' do"
|
1821
|
+
end # "context 'special cases' do"
|
1233
1822
|
end # context 'when prior value already exists' do
|
1234
1823
|
|
1235
1824
|
context 'when value is not present' do
|
@@ -1954,7 +2543,7 @@ RSpec.describe Scimitar::Resources::Mixin do
|
|
1954
2543
|
:from_patch_backend!,
|
1955
2544
|
nature: 'remove',
|
1956
2545
|
path: ['complex[type eq "type1"]', 'data', 'nested[nature eq "nature2"]', 'info'],
|
1957
|
-
value:
|
2546
|
+
value: nil,
|
1958
2547
|
altering_hash: scim_hash
|
1959
2548
|
)
|
1960
2549
|
|
@@ -691,6 +691,142 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
691
691
|
result = JSON.parse(response.body)
|
692
692
|
expect(result['status']).to eql('404')
|
693
693
|
end
|
694
|
+
|
695
|
+
context 'when removing users from groups' do
|
696
|
+
before :each do
|
697
|
+
@g1.mock_users << @u1
|
698
|
+
@g1.mock_users << @u2
|
699
|
+
@g1.mock_users << @u3
|
700
|
+
|
701
|
+
# (Self-check) Verify group representation
|
702
|
+
#
|
703
|
+
get "/Groups/#{@g1.id}", params: { format: :scim }
|
704
|
+
|
705
|
+
expect(response.status).to eql(200)
|
706
|
+
result = JSON.parse(response.body)
|
707
|
+
|
708
|
+
expect(result['members'].map { |m| m['value'] }.sort()).to eql(MockUser.pluck(:primary_key).sort())
|
709
|
+
end
|
710
|
+
|
711
|
+
it 'can remove all users' do
|
712
|
+
expect {
|
713
|
+
expect {
|
714
|
+
patch "/Groups/#{@g1.id}", params: {
|
715
|
+
format: :scim,
|
716
|
+
Operations: [
|
717
|
+
{
|
718
|
+
op: 'remove',
|
719
|
+
path: 'members'
|
720
|
+
}
|
721
|
+
]
|
722
|
+
}
|
723
|
+
}.to_not change { MockUser.count }
|
724
|
+
}.to_not change { MockGroup.count }
|
725
|
+
|
726
|
+
get "/Groups/#{@g1.id}", params: { format: :scim }
|
727
|
+
|
728
|
+
expect(response.status).to eql(200)
|
729
|
+
result = JSON.parse(response.body)
|
730
|
+
|
731
|
+
expect(result['members']).to be_empty
|
732
|
+
expect(@g1.reload().mock_users).to be_empty
|
733
|
+
end
|
734
|
+
|
735
|
+
# Define via 'let':
|
736
|
+
#
|
737
|
+
# * Hash 'payload', to send via 'patch'
|
738
|
+
# * MockUser 'removed_user', which is the user that should be removed
|
739
|
+
#
|
740
|
+
shared_examples 'a user remover' do
|
741
|
+
it 'which removes the identified user' do
|
742
|
+
expect {
|
743
|
+
expect {
|
744
|
+
patch "/Groups/#{@g1.id}", params: payload()
|
745
|
+
}.to_not change { MockUser.count }
|
746
|
+
}.to_not change { MockGroup.count }
|
747
|
+
|
748
|
+
expected_remaining_user_ids = MockUser
|
749
|
+
.where.not(primary_key: removed_user().id)
|
750
|
+
.pluck(:primary_key)
|
751
|
+
.sort()
|
752
|
+
|
753
|
+
get "/Groups/#{@g1.id}", params: { format: :scim }
|
754
|
+
|
755
|
+
expect(response.status).to eql(200)
|
756
|
+
result = JSON.parse(response.body)
|
757
|
+
|
758
|
+
expect(result['members'].map { |m| m['value'] }.sort()).to eql(expected_remaining_user_ids)
|
759
|
+
expect(@g1.reload().mock_users.map(&:primary_key).sort()).to eql(expected_remaining_user_ids)
|
760
|
+
end
|
761
|
+
end
|
762
|
+
|
763
|
+
# https://www.rfc-editor.org/rfc/rfc7644#section-3.5.2.2
|
764
|
+
#
|
765
|
+
context 'and using an RFC-compliant payload' do
|
766
|
+
let(:removed_user) { @u2 }
|
767
|
+
let(:payload) do
|
768
|
+
{
|
769
|
+
format: :scim,
|
770
|
+
Operations: [
|
771
|
+
{
|
772
|
+
op: 'remove',
|
773
|
+
path: "members[value eq \"#{removed_user().primary_key}\"]",
|
774
|
+
}
|
775
|
+
]
|
776
|
+
}
|
777
|
+
end
|
778
|
+
|
779
|
+
it_behaves_like 'a user remover'
|
780
|
+
end # context 'and using an RFC-compliant payload' do
|
781
|
+
|
782
|
+
# https://learn.microsoft.com/en-us/azure/active-directory/app-provisioning/use-scim-to-provision-users-and-groups#update-group-remove-members
|
783
|
+
#
|
784
|
+
context 'and using a Microsoft variant payload' do
|
785
|
+
let(:removed_user) { @u2 }
|
786
|
+
let(:payload) do
|
787
|
+
{
|
788
|
+
format: :scim,
|
789
|
+
Operations: [
|
790
|
+
{
|
791
|
+
op: 'remove',
|
792
|
+
path: 'members',
|
793
|
+
value: [{
|
794
|
+
'$ref' => nil,
|
795
|
+
'value' => removed_user().primary_key
|
796
|
+
}]
|
797
|
+
}
|
798
|
+
]
|
799
|
+
}
|
800
|
+
end
|
801
|
+
|
802
|
+
it_behaves_like 'a user remover'
|
803
|
+
end # context 'and using a Microsoft variant payload' do
|
804
|
+
|
805
|
+
# https://help.salesforce.com/s/articleView?id=sf.identity_scim_manage_groups.htm&type=5
|
806
|
+
#
|
807
|
+
context 'and using a Salesforce variant payload' do
|
808
|
+
let(:removed_user) { @u2 }
|
809
|
+
let(:payload) do
|
810
|
+
{
|
811
|
+
format: :scim,
|
812
|
+
Operations: [
|
813
|
+
{
|
814
|
+
op: 'remove',
|
815
|
+
path: 'members',
|
816
|
+
value: {
|
817
|
+
'members' => [{
|
818
|
+
'$ref' => nil,
|
819
|
+
'value' => removed_user().primary_key
|
820
|
+
}]
|
821
|
+
}
|
822
|
+
}
|
823
|
+
]
|
824
|
+
}
|
825
|
+
end
|
826
|
+
|
827
|
+
it_behaves_like 'a user remover'
|
828
|
+
end # context 'and using a Salesforce variant payload' do
|
829
|
+
end # "context 'when removing users from groups' do"
|
694
830
|
end # "context '#update' do"
|
695
831
|
|
696
832
|
# ===========================================================================
|
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.
|
4
|
+
version: 2.4.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-01-
|
12
|
+
date: 2023-01-27 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rails
|