scimitar 2.2.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/controllers/scimitar/active_record_backed_resources_controller.rb +27 -7
- data/app/models/scimitar/resources/mixin.rb +73 -1
- data/lib/scimitar/version.rb +2 -2
- data/spec/apps/dummy/app/controllers/mock_groups_controller.rb +1 -1
- data/spec/apps/dummy/app/models/mock_group.rb +1 -1
- data/spec/apps/dummy/app/models/mock_user.rb +5 -3
- data/spec/apps/dummy/config/routes.rb +10 -6
- data/spec/apps/dummy/db/migrate/20210304014602_create_mock_users.rb +2 -2
- data/spec/apps/dummy/db/migrate/20210308044214_create_join_table_mock_groups_mock_users.rb +8 -3
- data/spec/apps/dummy/db/schema.rb +9 -7
- data/spec/models/scimitar/lists/query_parser_spec.rb +8 -8
- data/spec/models/scimitar/resources/mixin_spec.rb +657 -40
- data/spec/requests/active_record_backed_resources_controller_spec.rb +230 -48
- metadata +3 -5
- data/spec/apps/dummy/db/migrate/20230109012729_add_timestamps_to_mock_user.rb +0 -5
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
|
@@ -21,6 +21,8 @@ module Scimitar
|
|
21
21
|
|
22
22
|
rescue_from ActiveRecord::RecordNotFound, with: :handle_resource_not_found # See Scimitar::ApplicationController
|
23
23
|
|
24
|
+
before_action :obtain_id_column_name_from_attribute_map
|
25
|
+
|
24
26
|
# GET (list)
|
25
27
|
#
|
26
28
|
def index
|
@@ -37,13 +39,13 @@ module Scimitar
|
|
37
39
|
pagination_info = scim_pagination_info(query.count())
|
38
40
|
|
39
41
|
page_of_results = query
|
40
|
-
.order(
|
42
|
+
.order(@id_column => :asc)
|
41
43
|
.offset(pagination_info.offset)
|
42
44
|
.limit(pagination_info.limit)
|
43
45
|
.to_a()
|
44
46
|
|
45
47
|
super(pagination_info, page_of_results) do | record |
|
46
|
-
|
48
|
+
record_to_scim(record)
|
47
49
|
end
|
48
50
|
end
|
49
51
|
|
@@ -52,7 +54,7 @@ module Scimitar
|
|
52
54
|
def show
|
53
55
|
super do |record_id|
|
54
56
|
record = self.find_record(record_id)
|
55
|
-
record
|
57
|
+
record_to_scim(record)
|
56
58
|
end
|
57
59
|
end
|
58
60
|
|
@@ -64,7 +66,7 @@ module Scimitar
|
|
64
66
|
record = self.storage_class().new
|
65
67
|
record.from_scim!(scim_hash: scim_resource.as_json())
|
66
68
|
self.save!(record)
|
67
|
-
|
69
|
+
record_to_scim(record)
|
68
70
|
end
|
69
71
|
end
|
70
72
|
end
|
@@ -77,7 +79,7 @@ module Scimitar
|
|
77
79
|
record = self.find_record(record_id)
|
78
80
|
record.from_scim!(scim_hash: scim_resource.as_json())
|
79
81
|
self.save!(record)
|
80
|
-
|
82
|
+
record_to_scim(record)
|
81
83
|
end
|
82
84
|
end
|
83
85
|
end
|
@@ -90,7 +92,7 @@ module Scimitar
|
|
90
92
|
record = self.find_record(record_id)
|
91
93
|
record.from_scim_patch!(patch_hash: patch_hash)
|
92
94
|
self.save!(record)
|
93
|
-
|
95
|
+
record_to_scim(record)
|
94
96
|
end
|
95
97
|
end
|
96
98
|
end
|
@@ -138,7 +140,14 @@ module Scimitar
|
|
138
140
|
# +record_id+:: Record ID (SCIM schema 'id' value - "our" ID).
|
139
141
|
#
|
140
142
|
def find_record(record_id)
|
141
|
-
self.storage_scope().
|
143
|
+
self.storage_scope().find_by!(@id_column => record_id)
|
144
|
+
end
|
145
|
+
|
146
|
+
# DRY up controller actions - pass a record; returns the SCIM
|
147
|
+
# representation, with a "show" location specified via #url_for.
|
148
|
+
#
|
149
|
+
def record_to_scim(record)
|
150
|
+
record.to_scim(location: url_for(action: :show, id: record.send(@id_column)))
|
142
151
|
end
|
143
152
|
|
144
153
|
# Save a record, dealing with validation exceptions by raising SCIM
|
@@ -177,5 +186,16 @@ module Scimitar
|
|
177
186
|
end
|
178
187
|
end
|
179
188
|
|
189
|
+
# Called via +before_action+ - stores in @id_column the name of whatever
|
190
|
+
# model column is used to store the record ID, via
|
191
|
+
# Scimitar::Resources::Mixin::scim_attributes_map.
|
192
|
+
#
|
193
|
+
# Default is <tt>:id</tt>.
|
194
|
+
#
|
195
|
+
def obtain_id_column_name_from_attribute_map
|
196
|
+
attrs = storage_class().scim_attributes_map() || {}
|
197
|
+
@id_column = attrs[:id] || :id
|
198
|
+
end
|
199
|
+
|
180
200
|
end
|
181
201
|
end
|
@@ -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
|
@@ -1,11 +1,13 @@
|
|
1
1
|
class MockUser < ActiveRecord::Base
|
2
2
|
|
3
|
+
self.primary_key = :primary_key
|
4
|
+
|
3
5
|
# ===========================================================================
|
4
6
|
# TEST ATTRIBUTES - see db/migrate/20210304014602_create_mock_users.rb etc.
|
5
7
|
# ===========================================================================
|
6
8
|
|
7
9
|
READWRITE_ATTRS = %w{
|
8
|
-
|
10
|
+
primary_key
|
9
11
|
scim_uid
|
10
12
|
username
|
11
13
|
first_name
|
@@ -38,7 +40,7 @@ class MockUser < ActiveRecord::Base
|
|
38
40
|
|
39
41
|
def self.scim_attributes_map
|
40
42
|
return {
|
41
|
-
id: :
|
43
|
+
id: :primary_key,
|
42
44
|
externalId: :scim_uid,
|
43
45
|
userName: :username,
|
44
46
|
name: {
|
@@ -92,7 +94,7 @@ class MockUser < ActiveRecord::Base
|
|
92
94
|
|
93
95
|
def self.scim_queryable_attributes
|
94
96
|
return {
|
95
|
-
'id' => { column: :
|
97
|
+
'id' => { column: :primary_key },
|
96
98
|
'externalId' => { column: :scim_uid },
|
97
99
|
'meta.lastModified' => { column: :updated_at },
|
98
100
|
'name.givenName' => { column: :first_name },
|
@@ -6,12 +6,16 @@
|
|
6
6
|
Rails.application.routes.draw do
|
7
7
|
mount Scimitar::Engine, at: '/'
|
8
8
|
|
9
|
-
get 'Users',
|
10
|
-
get 'Users/:id',
|
11
|
-
post 'Users',
|
12
|
-
put 'Users/:id',
|
13
|
-
patch 'Users/:id',
|
14
|
-
delete 'Users/:id',
|
9
|
+
get 'Users', to: 'mock_users#index'
|
10
|
+
get 'Users/:id', to: 'mock_users#show'
|
11
|
+
post 'Users', to: 'mock_users#create'
|
12
|
+
put 'Users/:id', to: 'mock_users#replace'
|
13
|
+
patch 'Users/:id', to: 'mock_users#update'
|
14
|
+
delete 'Users/:id', to: 'mock_users#destroy'
|
15
|
+
|
16
|
+
get 'Groups', to: 'mock_groups#index'
|
17
|
+
get 'Groups/:id', to: 'mock_groups#show'
|
18
|
+
patch 'Groups/:id', to: 'mock_groups#update'
|
15
19
|
|
16
20
|
# For testing blocks passed to ActiveRecordBackedResourcesController#destroy
|
17
21
|
#
|
@@ -1,6 +1,7 @@
|
|
1
1
|
class CreateMockUsers < ActiveRecord::Migration[6.1]
|
2
2
|
def change
|
3
|
-
create_table :mock_users do |t|
|
3
|
+
create_table :mock_users, id: :uuid, primary_key: :primary_key do |t|
|
4
|
+
t.timestamps
|
4
5
|
|
5
6
|
t.text :scim_uid
|
6
7
|
t.text :username
|
@@ -9,7 +10,6 @@ class CreateMockUsers < ActiveRecord::Migration[6.1]
|
|
9
10
|
t.text :work_email_address
|
10
11
|
t.text :home_email_address
|
11
12
|
t.text :work_phone_number
|
12
|
-
|
13
13
|
end
|
14
14
|
end
|
15
15
|
end
|
@@ -1,8 +1,13 @@
|
|
1
1
|
class CreateJoinTableMockGroupsMockUsers < ActiveRecord::Migration[6.1]
|
2
2
|
def change
|
3
|
-
|
4
|
-
t.
|
5
|
-
t.index
|
3
|
+
create_table :mock_groups_users, id: false do | t |
|
4
|
+
t.references :mock_group, foreign_key: true, type: :int8, index: true, null: false
|
5
|
+
t.references :mock_user, type: :uuid, index: true, null: false, primary_key: :primary_key
|
6
|
+
|
7
|
+
# The 'foreign_key:' option (used above) only works for 'id' column names
|
8
|
+
# but the test data has a column named 'primary_key' for 'mock_users'.
|
9
|
+
#
|
10
|
+
t.foreign_key :mock_users, primary_key: :primary_key
|
6
11
|
end
|
7
12
|
end
|
8
13
|
end
|
@@ -10,7 +10,7 @@
|
|
10
10
|
#
|
11
11
|
# It's strongly recommended that you check this file into your version control system.
|
12
12
|
|
13
|
-
ActiveRecord::Schema[7.0].define(version:
|
13
|
+
ActiveRecord::Schema[7.0].define(version: 2021_03_08_044214) do
|
14
14
|
# These are extensions that must be enabled in order to support this database
|
15
15
|
enable_extension "plpgsql"
|
16
16
|
|
@@ -23,12 +23,14 @@ ActiveRecord::Schema[7.0].define(version: 2023_01_09_012729) do
|
|
23
23
|
|
24
24
|
create_table "mock_groups_users", id: false, force: :cascade do |t|
|
25
25
|
t.bigint "mock_group_id", null: false
|
26
|
-
t.
|
27
|
-
t.index ["mock_group_id"
|
28
|
-
t.index ["mock_user_id"
|
26
|
+
t.uuid "mock_user_id", null: false
|
27
|
+
t.index ["mock_group_id"], name: "index_mock_groups_users_on_mock_group_id"
|
28
|
+
t.index ["mock_user_id"], name: "index_mock_groups_users_on_mock_user_id"
|
29
29
|
end
|
30
30
|
|
31
|
-
create_table "mock_users", force: :cascade do |t|
|
31
|
+
create_table "mock_users", primary_key: "primary_key", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
32
|
+
t.datetime "created_at", null: false
|
33
|
+
t.datetime "updated_at", null: false
|
32
34
|
t.text "scim_uid"
|
33
35
|
t.text "username"
|
34
36
|
t.text "first_name"
|
@@ -36,8 +38,8 @@ ActiveRecord::Schema[7.0].define(version: 2023_01_09_012729) do
|
|
36
38
|
t.text "work_email_address"
|
37
39
|
t.text "home_email_address"
|
38
40
|
t.text "work_phone_number"
|
39
|
-
t.datetime "created_at", null: false
|
40
|
-
t.datetime "updated_at", null: false
|
41
41
|
end
|
42
42
|
|
43
|
+
add_foreign_key "mock_groups_users", "mock_groups"
|
44
|
+
add_foreign_key "mock_groups_users", "mock_users", primary_key: "primary_key"
|
43
45
|
end
|
@@ -405,19 +405,19 @@ RSpec.describe Scimitar::Lists::QueryParser do
|
|
405
405
|
query = @instance.to_activerecord_query(MockUser.all)
|
406
406
|
|
407
407
|
expect(query.count).to eql(1)
|
408
|
-
expect(query.pluck(:
|
408
|
+
expect(query.pluck(:primary_key)).to eql([user_1.primary_key])
|
409
409
|
|
410
410
|
@instance.parse('name.givenName sw J') # First name starts with 'J'
|
411
411
|
query = @instance.to_activerecord_query(MockUser.all)
|
412
412
|
|
413
413
|
expect(query.count).to eql(2)
|
414
|
-
expect(query.pluck(:
|
414
|
+
expect(query.pluck(:primary_key)).to match_array([user_1.primary_key, user_2.primary_key])
|
415
415
|
|
416
416
|
@instance.parse('name.familyName ew he') # Last name ends with 'he'
|
417
417
|
query = @instance.to_activerecord_query(MockUser.all)
|
418
418
|
|
419
419
|
expect(query.count).to eql(1)
|
420
|
-
expect(query.pluck(:
|
420
|
+
expect(query.pluck(:primary_key)).to eql([user_2.primary_key])
|
421
421
|
|
422
422
|
# Test presence
|
423
423
|
|
@@ -425,7 +425,7 @@ RSpec.describe Scimitar::Lists::QueryParser do
|
|
425
425
|
query = @instance.to_activerecord_query(MockUser.all)
|
426
426
|
|
427
427
|
expect(query.count).to eql(2)
|
428
|
-
expect(query.pluck(:
|
428
|
+
expect(query.pluck(:primary_key)).to match_array([user_1.primary_key, user_2.primary_key])
|
429
429
|
|
430
430
|
# Test a simple not-equals, but use a custom starting scope. Note that
|
431
431
|
# the query would find "user_3" *except* there is no first name defined
|
@@ -435,7 +435,7 @@ RSpec.describe Scimitar::Lists::QueryParser do
|
|
435
435
|
query = @instance.to_activerecord_query(MockUser.where.not('first_name' => 'John'))
|
436
436
|
|
437
437
|
expect(query.count).to eql(1)
|
438
|
-
expect(query.pluck(:
|
438
|
+
expect(query.pluck(:primary_key)).to match_array([user_1.primary_key])
|
439
439
|
end
|
440
440
|
|
441
441
|
context 'when mapped to multiple columns' do
|
@@ -499,7 +499,7 @@ RSpec.describe Scimitar::Lists::QueryParser do
|
|
499
499
|
query = @instance.to_activerecord_query(MockUser.all)
|
500
500
|
|
501
501
|
expect(query.count).to eql(1)
|
502
|
-
expect(query.pluck(:
|
502
|
+
expect(query.pluck(:primary_key)).to match_array([user_2.primary_key])
|
503
503
|
end
|
504
504
|
end # "context 'simple AND' do"
|
505
505
|
|
@@ -520,7 +520,7 @@ RSpec.describe Scimitar::Lists::QueryParser do
|
|
520
520
|
query = @instance.to_activerecord_query(MockUser.all)
|
521
521
|
|
522
522
|
expect(query.count).to eql(2)
|
523
|
-
expect(query.pluck(:
|
523
|
+
expect(query.pluck(:primary_key)).to match_array([user_1.primary_key, user_2.primary_key])
|
524
524
|
end
|
525
525
|
end # "context 'simple OR' do"
|
526
526
|
|
@@ -546,7 +546,7 @@ RSpec.describe Scimitar::Lists::QueryParser do
|
|
546
546
|
query = @instance.to_activerecord_query(MockUser.all)
|
547
547
|
|
548
548
|
expect(query.count).to eql(3)
|
549
|
-
expect(query.pluck(:
|
549
|
+
expect(query.pluck(:primary_key)).to match_array([user_1.primary_key, user_2.primary_key, user_3.primary_key])
|
550
550
|
end
|
551
551
|
end # "context 'combined AND and OR' do"
|
552
552
|
|