scimitar 1.3.3 → 1.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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 +3 -3
- data/spec/apps/dummy/config/routes.rb +4 -0
- 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/models/scimitar/resources/user_spec.rb +2 -2
- data/spec/requests/active_record_backed_resources_controller_spec.rb +232 -48
- metadata +35 -37
- 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: dd0d129e52cd7cbe3fe52fec4c4033cd45f4d2071da5d495863b1f3f78685fd5
|
4
|
+
data.tar.gz: 5531cdfc4786043513f9539c2fce43bfdebbec26a6e9964d93b13c6ea00710b0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d1914b8fe68cdc6077369f22a03f7e688790f5b239738e18508e0aeaa68055ee1e197ebc4fe8b512903d07b926de64d64817b26c6ef9b9ace2594abc85f01e62
|
7
|
+
data.tar.gz: de0d305d512d646f43e581b5116c2f316eaa2b9ddb0426f1b12522a668e2a7c811a5e9d6aee82e82fadb534b5e1a5cacca303701a7f7d1791de188d73b55740c
|
@@ -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 = '1.
|
6
|
+
VERSION = '1.5.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
|
@@ -5,7 +5,7 @@ class MockUser < ActiveRecord::Base
|
|
5
5
|
# ===========================================================================
|
6
6
|
|
7
7
|
READWRITE_ATTRS = %w{
|
8
|
-
|
8
|
+
primary_key
|
9
9
|
scim_uid
|
10
10
|
username
|
11
11
|
first_name
|
@@ -38,7 +38,7 @@ class MockUser < ActiveRecord::Base
|
|
38
38
|
|
39
39
|
def self.scim_attributes_map
|
40
40
|
return {
|
41
|
-
id: :
|
41
|
+
id: :primary_key,
|
42
42
|
externalId: :scim_uid,
|
43
43
|
userName: :username,
|
44
44
|
name: {
|
@@ -92,7 +92,7 @@ class MockUser < ActiveRecord::Base
|
|
92
92
|
|
93
93
|
def self.scim_queryable_attributes
|
94
94
|
return {
|
95
|
-
'id' => { column: :
|
95
|
+
'id' => { column: :primary_key },
|
96
96
|
'externalId' => { column: :scim_uid },
|
97
97
|
'meta.lastModified' => { column: :updated_at },
|
98
98
|
'name.givenName' => { column: :first_name },
|
@@ -13,6 +13,10 @@ Rails.application.routes.draw do
|
|
13
13
|
patch 'Users/:id', to: 'mock_users#update'
|
14
14
|
delete 'Users/:id', to: 'mock_users#destroy'
|
15
15
|
|
16
|
+
get 'Groups', to: 'mock_groups#index'
|
17
|
+
get 'Groups/:id', to: 'mock_groups#show'
|
18
|
+
patch 'Groups/:id', to: 'mock_groups#update'
|
19
|
+
|
16
20
|
# For testing blocks passed to ActiveRecordBackedResourcesController#destroy
|
17
21
|
#
|
18
22
|
delete 'CustomDestroyUsers/:id', to: 'custom_destroy_mock_users#destroy'
|
@@ -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.define(version:
|
13
|
+
ActiveRecord::Schema.define(version: 2021_03_08_044214) do
|
14
14
|
|
15
15
|
# These are extensions that must be enabled in order to support this database
|
16
16
|
enable_extension "plpgsql"
|
@@ -24,12 +24,14 @@ ActiveRecord::Schema.define(version: 2023_01_09_012729) do
|
|
24
24
|
|
25
25
|
create_table "mock_groups_users", id: false, force: :cascade do |t|
|
26
26
|
t.bigint "mock_group_id", null: false
|
27
|
-
t.
|
28
|
-
t.index ["mock_group_id"
|
29
|
-
t.index ["mock_user_id"
|
27
|
+
t.uuid "mock_user_id", null: false
|
28
|
+
t.index ["mock_group_id"], name: "index_mock_groups_users_on_mock_group_id"
|
29
|
+
t.index ["mock_user_id"], name: "index_mock_groups_users_on_mock_user_id"
|
30
30
|
end
|
31
31
|
|
32
|
-
create_table "mock_users", force: :cascade do |t|
|
32
|
+
create_table "mock_users", primary_key: "primary_key", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
33
|
+
t.datetime "created_at", precision: 6, null: false
|
34
|
+
t.datetime "updated_at", precision: 6, null: false
|
33
35
|
t.text "scim_uid"
|
34
36
|
t.text "username"
|
35
37
|
t.text "first_name"
|
@@ -37,8 +39,8 @@ ActiveRecord::Schema.define(version: 2023_01_09_012729) do
|
|
37
39
|
t.text "work_email_address"
|
38
40
|
t.text "home_email_address"
|
39
41
|
t.text "work_phone_number"
|
40
|
-
t.datetime "created_at", precision: 6, null: false
|
41
|
-
t.datetime "updated_at", precision: 6, null: false
|
42
42
|
end
|
43
43
|
|
44
|
+
add_foreign_key "mock_groups_users", "mock_groups"
|
45
|
+
add_foreign_key "mock_groups_users", "mock_users", primary_key: "primary_key"
|
44
46
|
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.id])
|
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
|
|