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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ce356c067dc4636a1540eec905c21730c6d37d7831725ed5fd914b0a62e7f1e7
4
- data.tar.gz: 979360b39861dc14e5d202ab580774e6164f9818b0358a8bcc47e06c6a6462c3
3
+ metadata.gz: bccee840f0dc82854974e8d90d19189ae133f4281142c5de89107230b413041b
4
+ data.tar.gz: 014cbf547dd861c26489aa13d5ab2c75f5826ed24ca3c98d7eb71b76d98994b1
5
5
  SHA512:
6
- metadata.gz: 78eeeab146e8a18f265643bc99a32a461a7972b2971309421f4cbfa1751fabe0ad99a84037f8f2000ef5da90ae7282bf16c66fa34e26ed83e4ae2510248df651
7
- data.tar.gz: 15af818f707e5a7db410abd57fe342258c016fdf6fef79af12e384481f7730bdea7a22983e3fd662317e73437fdb0c491a651639479539a8029eebe3dd8d3ab6
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(id: :asc)
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
- record.to_scim(location: url_for(action: :show, id: record.id))
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.to_scim(location: url_for(action: :show, id: record_id))
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
- record.to_scim(location: url_for(action: :show, id: record.id))
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
- record.to_scim(location: url_for(action: :show, id: record.id))
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
- record.to_scim(location: url_for(action: :show, id: record.id))
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().find(record_id)
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.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.2.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-13'
11
+ DATE = '2023-01-27'
12
12
 
13
13
  end
@@ -1,4 +1,4 @@
1
- class MockUsersController < Scimitar::ActiveRecordBackedResourcesController
1
+ class MockGroupsController < Scimitar::ActiveRecordBackedResourcesController
2
2
 
3
3
  protected
4
4
 
@@ -58,7 +58,7 @@ class MockGroup < ActiveRecord::Base
58
58
 
59
59
  case type.downcase
60
60
  when 'user'
61
- MockUser.find_by_id(id)
61
+ MockUser.find_by_primary_key(id)
62
62
  when 'group'
63
63
  MockGroup.find_by_id(id)
64
64
  else
@@ -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
- id
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: :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: :id },
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', 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'
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
- create_join_table :mock_groups, :mock_users do |t|
4
- t.index [:mock_group_id, :mock_user_id]
5
- t.index [:mock_user_id, :mock_group_id]
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: 2023_01_09_012729) do
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.bigint "mock_user_id", null: false
27
- t.index ["mock_group_id", "mock_user_id"], name: "index_mock_groups_users_on_mock_group_id_and_mock_user_id"
28
- t.index ["mock_user_id", "mock_group_id"], name: "index_mock_groups_users_on_mock_user_id_and_mock_group_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(:id)).to eql([user_1.id])
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(:id)).to match_array([user_1.id, user_2.id])
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(:id)).to eql([user_2.id])
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(:id)).to match_array([user_1.id, user_2.id])
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(:id)).to match_array([user_1.id])
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(:id)).to match_array([user_2.id])
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(:id)).to match_array([user_1.id, user_2.id])
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(:id)).to match_array([user_1.id, user_2.id, user_3.id])
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