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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 382f2feb979c655de91c1d30bf1fa8bb5347059abe376839758a9b34992f8616
4
- data.tar.gz: d1d8f8c32a92f4b879699a995b4e1746b7b778d782ab3101695e59891a107606
3
+ metadata.gz: dd0d129e52cd7cbe3fe52fec4c4033cd45f4d2071da5d495863b1f3f78685fd5
4
+ data.tar.gz: 5531cdfc4786043513f9539c2fce43bfdebbec26a6e9964d93b13c6ea00710b0
5
5
  SHA512:
6
- metadata.gz: 87373dddf17fe6791e7464c228e688338267bf1a230cd2ab1d69372185ea3adcf165b969872e0e05556ba28b1e9e1581ddaac5639f89235bd7f19da081ff3e7f
7
- data.tar.gz: ae79056e07500c1b195807a9a3f12755895f47a4383c7451208a2956b79ec02e600a8c915fa8a600cccce559c16b5c62f73e081ced2e09e8bee5da63910a2a1e
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(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 = '1.3.3'
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-10'
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
@@ -5,7 +5,7 @@ class MockUser < ActiveRecord::Base
5
5
  # ===========================================================================
6
6
 
7
7
  READWRITE_ATTRS = %w{
8
- id
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: :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: :id },
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
- 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.define(version: 2023_01_09_012729) do
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.bigint "mock_user_id", null: false
28
- t.index ["mock_group_id", "mock_user_id"], name: "index_mock_groups_users_on_mock_group_id_and_mock_user_id"
29
- t.index ["mock_user_id", "mock_group_id"], name: "index_mock_groups_users_on_mock_user_id_and_mock_group_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(:id)).to eql([user_1.id])
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(: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