scimitar 2.3.0 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3904e389b3b00b4c49df6d9267cc0b5413613ed56b544ee888dd4b3bfa9d7489
4
- data.tar.gz: 93b14676bda6d86d4add6c0bcbf8ae6415ff49177b384f2b75571a304485d87f
3
+ metadata.gz: bccee840f0dc82854974e8d90d19189ae133f4281142c5de89107230b413041b
4
+ data.tar.gz: 014cbf547dd861c26489aa13d5ab2c75f5826ed24ca3c98d7eb71b76d98994b1
5
5
  SHA512:
6
- metadata.gz: f1fad29d6ba090ad00b55bdf59f1050aacd1858d4e511e726d3fa501a37389e57777cd2cb2253c20972106e567a3f4b5e07ecd53a17723dcab2517f0c8ab99cf
7
- data.tar.gz: 30a1c3ad4c6653b347cf00228cad99b88a3dc4a2da45fe311b61d0a0f2d08db723fabf2041e3d72f1d1cea28d633c540322394841723bdd8db7d805e49819eb4
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.delete(path_component)
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
@@ -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.3.0'
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-17'
11
+ DATE = '2023-01-27'
12
12
 
13
13
  end
@@ -15,6 +15,7 @@ Rails.application.routes.draw do
15
15
 
16
16
  get 'Groups', to: 'mock_groups#index'
17
17
  get 'Groups/:id', to: 'mock_groups#show'
18
+ patch 'Groups/:id', to: 'mock_groups#update'
18
19
 
19
20
  # For testing blocks passed to ActiveRecordBackedResourcesController#destroy
20
21
  #
@@ -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: [{ 'deeper' => 'addition' }],
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.3.0
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-17 00:00:00.000000000 Z
12
+ date: 2023-01-27 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails