scimitar 1.5.3 → 1.6.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: 6d57cfdaba9d48c6c193fb74baafc7c6e26c004a5a634460bc2f9ec94ad0440e
4
- data.tar.gz: 8ff5ffbabe01c86822bd2c1dc85c535bf95b197cbc41920a995aa1801d239f32
3
+ metadata.gz: 7bed3013773832fde9275ba69b622a8538beb2d8ff6b6ffe506e1c9453480ec5
4
+ data.tar.gz: e51c811619169e64f3a373a56670514d37ac2823b7ed8b1bedd5c9b9c282576e
5
5
  SHA512:
6
- metadata.gz: 91f6cba011c909de21f7c391dbfc5b18a30b7ef9cf81d9f43842452fa021484f55623034be386af6242e9a1a82efa9ef9436d8b5c17248bf0b9384587fecb50b
7
- data.tar.gz: e72e91fa8c3dd85df64b806dd3e6cb2b4d2460b33cb7d3c241bc64e8557adbccff9e3071fd18fab676ffdcd2a8c7f375512f85919e6ba10601021d127097bef0
6
+ metadata.gz: 697b3299edb4752baf38b68574191b72528a5e69467aa1a097c9b8c92928d57cff9d21afa50d7c66c15fc66220e52469f2b602ef3a5051489f734717f3489955
7
+ data.tar.gz: 7d7a7bcb8fa0a9881ad7b77b6b81a4ec096e56300e8b5594fb0538adb76f7ee06b65fb56e319f1b7872738e4807d667e534f5c99b60d86b8bf5125e0d2972dc8
@@ -25,7 +25,6 @@ module Scimitar
25
25
  #
26
26
  # ...to "globally" invoke this handler if you wish.
27
27
  #
28
- #
29
28
  # +exception+:: Exception instance, used for a configured error reporter
30
29
  # via #handle_scim_error (if present).
31
30
  #
@@ -192,7 +192,7 @@ module Scimitar
192
192
 
193
193
  ast.push(self.start_group? ? self.parse_group() : self.pop())
194
194
 
195
- unless ! ast.last.is_a?(String) || UNARY_OPERATORS.include?(ast.last.downcase)
195
+ if ast.last.is_a?(String) && !UNARY_OPERATORS.include?(ast.last.downcase) || ast.last.is_a?(Array)
196
196
  expect_op ^= true
197
197
  end
198
198
  end
@@ -601,9 +601,15 @@ module Scimitar
601
601
  column_names = self.activerecord_columns(scim_attribute)
602
602
  value = self.activerecord_parameter(scim_parameter)
603
603
  value_for_like = self.sql_modified_value(scim_operator, value)
604
- all_supported = column_names.all? { | column_name | base_scope.model.column_names.include?(column_name.to_s) }
604
+ arel_columns = column_names.map do |column|
605
+ if base_scope.model.column_names.include?(column.to_s)
606
+ arel_table[column]
607
+ elsif column.is_a?(Arel::Attribute)
608
+ column
609
+ end
610
+ end
605
611
 
606
- raise Scimitar::FilterError unless all_supported
612
+ raise Scimitar::FilterError unless arel_columns.all?
607
613
 
608
614
  unless case_sensitive
609
615
  lc_scim_attribute = scim_attribute.downcase()
@@ -615,8 +621,7 @@ module Scimitar
615
621
  )
616
622
  end
617
623
 
618
- column_names.each.with_index do | column_name, index |
619
- arel_column = arel_table[column_name]
624
+ arel_columns.each.with_index do | arel_column, index |
620
625
  arel_operation = case scim_operator
621
626
  when 'eq'
622
627
  if case_sensitive
@@ -641,7 +646,7 @@ module Scimitar
641
646
  when 'co', 'sw', 'ew'
642
647
  arel_column.matches(value_for_like, nil, case_sensitive)
643
648
  when 'pr'
644
- arel_table.grouping(arel_column.not_eq_all(['', nil]))
649
+ arel_column.relation.grouping(arel_column.not_eq_all(['', nil]))
645
650
  else
646
651
  raise Scimitar::FilterError.new("Unsupported operator: '#{scim_operator}'")
647
652
  end
@@ -112,7 +112,7 @@ module Scimitar
112
112
  end
113
113
 
114
114
  def self.complex_scim_attributes
115
- schema.scim_attributes.select(&:complexType).group_by(&:name)
115
+ schemas.flat_map(&:scim_attributes).select(&:complexType).group_by(&:name)
116
116
  end
117
117
 
118
118
  def complex_type_from_hash(scim_attribute, attr_value)
@@ -220,13 +220,8 @@ module Scimitar
220
220
  # allow for different client searching "styles", given ambiguities in RFC
221
221
  # 7644 filter examples).
222
222
  #
223
- # Each value is a Hash with Symbol keys ':column', naming just one simple
224
- # column for a mapping; ':columns', with an Array of column names that you
225
- # want to map using 'OR' for a single search on the corresponding SCIM
226
- # attribute; or ':ignore' with value 'true', which means that a fitler on
227
- # the matching attribute is ignored rather than resulting in an "invalid
228
- # filter" exception - beware possibilities for surprised clients getting a
229
- # broader result set than expected. Example:
223
+ # Each value is a hash of queryable SCIM attribute options, described
224
+ # below - for example:
230
225
  #
231
226
  # def self.scim_queryable_attributes
232
227
  # return {
@@ -234,10 +229,27 @@ module Scimitar
234
229
  # 'name.familyName' => { column: :last_name },
235
230
  # 'emails' => { columns: [ :work_email_address, :home_email_address ] },
236
231
  # 'emails.value' => { columns: [ :work_email_address, :home_email_address ] },
237
- # 'emails.type' => { ignore: true }
232
+ # 'emails.type' => { ignore: true },
233
+ # 'groups.value' => { column: Group.arel_table[:id] }
238
234
  # }
239
235
  # end
240
236
  #
237
+ # Column references can be either a Symbol representing a column within
238
+ # the resource model table, or an <tt>Arel::Attribute</tt> instance via
239
+ # e.g. <tt>MyModel.arel_table[:my_column]</tt>.
240
+ #
241
+ # === Queryable SCIM attribute options
242
+ #
243
+ # +:column+:: Just one simple column for a mapping.
244
+ #
245
+ # +:columns+:: An Array of columns that you want to map using 'OR' for a
246
+ # single search of the corresponding entity.
247
+ #
248
+ # +:ignore+:: When set to +true+, the matching attribute is ignored rather
249
+ # than resulting in an "invalid filter" exception. Beware
250
+ # possibilities for surprised clients getting a broader result
251
+ # set than expected, since a constraint may have been ignored.
252
+ #
241
253
  # Filtering is currently limited and searching within e.g. arrays of data
242
254
  # is not supported; only simple top-level keys can be mapped.
243
255
  #
@@ -406,8 +418,11 @@ module Scimitar
406
418
  def from_scim_patch!(patch_hash:)
407
419
  frozen_ci_patch_hash = patch_hash.with_indifferent_case_insensitive_access().freeze()
408
420
  ci_scim_hash = self.to_scim(location: '(unused)').as_json().with_indifferent_case_insensitive_access()
421
+ operations = frozen_ci_patch_hash['operations']
422
+
423
+ raise Scimitar::InvalidSyntaxError.new("Missing PATCH \"operations\"") unless operations
409
424
 
410
- frozen_ci_patch_hash['operations'].each do |operation|
425
+ operations.each do |operation|
411
426
  nature = operation['op' ]&.downcase
412
427
  path_str = operation['path' ]
413
428
  value = operation['value']
@@ -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.5.3'
6
+ VERSION = '1.6.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-09-16'
11
+ DATE = '2023-09-25'
12
12
 
13
13
  end
data/lib/scimitar.rb CHANGED
@@ -25,16 +25,4 @@ module Scimitar
25
25
  @engine_configuration ||= EngineConfiguration.new
26
26
  @engine_configuration
27
27
  end
28
-
29
- # Set in a "Rails.application.config.to_prepare" block by Scimitar itself to
30
- # establish default values. Older Scimitar client applications might not use
31
- # that wrapper; we don't want to overwrite settings they configured, but we
32
- # *do* want to let them overwrite the defaults. Thus, '||=" is used here but
33
- # not in ::service_provider_configuration=.
34
- #
35
- # Client applications should not call this method themselves.
36
- #
37
- def self.default_service_provider_configuration(default_configuration)
38
- @service_provider_configuration ||= custom_configuration
39
- end
40
28
  end
@@ -15,6 +15,7 @@ class MockUser < ActiveRecord::Base
15
15
  work_phone_number
16
16
  organization
17
17
  department
18
+ mock_groups
18
19
  }
19
20
 
20
21
  has_and_belongs_to_many :mock_groups
@@ -90,7 +91,17 @@ class MockUser < ActiveRecord::Base
90
91
  # "spec/apps/dummy/config/initializers/scimitar.rb".
91
92
  #
92
93
  organization: :organization,
93
- department: :department
94
+ department: :department,
95
+ userGroups: [
96
+ {
97
+ list: :mock_groups,
98
+ find_with: ->(value) { MockGroup.find(value["value"]) },
99
+ using: {
100
+ value: :id,
101
+ display: :display_name
102
+ }
103
+ }
104
+ ]
94
105
  }
95
106
  end
96
107
 
@@ -105,6 +116,8 @@ class MockUser < ActiveRecord::Base
105
116
  'meta.lastModified' => { column: :updated_at },
106
117
  'name.givenName' => { column: :first_name },
107
118
  'name.familyName' => { column: :last_name },
119
+ 'groups' => { column: MockGroup.arel_table[:id] },
120
+ 'groups.value' => { column: MockGroup.arel_table[:id] },
108
121
  'emails' => { columns: [ :work_email_address, :home_email_address ] },
109
122
  'emails.value' => { columns: [ :work_email_address, :home_email_address ] },
110
123
  'emails.type' => { ignore: true } # We can't filter on that; it'll just search all e-mails
@@ -6,12 +6,12 @@
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
15
 
16
16
  get 'Groups', to: 'mock_groups#index'
17
17
  get 'Groups/:id', to: 'mock_groups#show'
@@ -481,6 +481,66 @@ RSpec.describe Scimitar::Lists::QueryParser do
481
481
  end
482
482
  end # "context 'when instructed to ignore an attribute' do"
483
483
 
484
+ context 'when an arel column is mapped' do
485
+ let(:scope_with_groups) { MockUser.left_joins(:mock_groups) }
486
+
487
+ context 'with binary operators' do
488
+ it 'reads across all using OR' do
489
+ @instance.parse('groups eq "12345"')
490
+ query = @instance.to_activerecord_query(scope_with_groups)
491
+
492
+ expect(query.to_sql).to eql(<<~SQL.squish)
493
+ SELECT "mock_users".*
494
+ FROM "mock_users"
495
+ LEFT OUTER JOIN "mock_groups_users" ON "mock_groups_users"."mock_user_id" = "mock_users"."primary_key"
496
+ LEFT OUTER JOIN "mock_groups" ON "mock_groups"."id" = "mock_groups_users"."mock_group_id"
497
+ WHERE "mock_groups"."id" ILIKE 12345
498
+ SQL
499
+ end
500
+
501
+ it 'works with other query elements using correct precedence' do
502
+ @instance.parse('groups eq "12345" and emails eq "any@test.com"')
503
+ query = @instance.to_activerecord_query(scope_with_groups)
504
+
505
+ expect(query.to_sql).to eql(<<~SQL.squish)
506
+ SELECT "mock_users".*
507
+ FROM "mock_users"
508
+ LEFT OUTER JOIN "mock_groups_users" ON "mock_groups_users"."mock_user_id" = "mock_users"."primary_key"
509
+ LEFT OUTER JOIN "mock_groups" ON "mock_groups"."id" = "mock_groups_users"."mock_group_id"
510
+ WHERE "mock_groups"."id" ILIKE 12345 AND ("mock_users"."work_email_address" ILIKE 'any@test.com' OR "mock_users"."home_email_address" ILIKE 'any@test.com')
511
+ SQL
512
+ end
513
+ end # "context 'with binary operators' do"
514
+
515
+ context 'with unary operators' do
516
+ it 'reads across all using OR' do
517
+ @instance.parse('groups pr')
518
+ query = @instance.to_activerecord_query(scope_with_groups)
519
+
520
+ expect(query.to_sql).to eql(<<~SQL.squish)
521
+ SELECT "mock_users".*
522
+ FROM "mock_users"
523
+ LEFT OUTER JOIN "mock_groups_users" ON "mock_groups_users"."mock_user_id" = "mock_users"."primary_key"
524
+ LEFT OUTER JOIN "mock_groups" ON "mock_groups"."id" = "mock_groups_users"."mock_group_id"
525
+ WHERE ("mock_groups"."id" != NULL AND "mock_groups"."id" IS NOT NULL)
526
+ SQL
527
+ end
528
+
529
+ it 'works with other query elements using correct precedence' do
530
+ @instance.parse('name.familyName eq "John" and groups pr')
531
+ query = @instance.to_activerecord_query(scope_with_groups)
532
+
533
+ expect(query.to_sql).to eql(<<~SQL.squish)
534
+ SELECT "mock_users".*
535
+ FROM "mock_users"
536
+ LEFT OUTER JOIN "mock_groups_users" ON "mock_groups_users"."mock_user_id" = "mock_users"."primary_key"
537
+ LEFT OUTER JOIN "mock_groups" ON "mock_groups"."id" = "mock_groups_users"."mock_group_id"
538
+ WHERE "mock_users"."last_name" ILIKE 'John' AND ("mock_groups"."id" != NULL AND "mock_groups"."id" IS NOT NULL)
539
+ SQL
540
+ end
541
+ end # "context 'with unary operators' do
542
+ end # "context 'when an arel column is mapped' do"
543
+
484
544
  context 'with complex cases' do
485
545
  context 'using AND' do
486
546
  it 'generates expected SQL' do
@@ -532,6 +592,13 @@ RSpec.describe Scimitar::Lists::QueryParser do
532
592
  expect(query.to_sql).to eql(%q{SELECT "mock_users".* FROM "mock_users" WHERE "mock_users"."first_name" ILIKE 'Jane' AND ("mock_users"."last_name" ILIKE '%avi%' OR "mock_users"."last_name" ILIKE '%ith')})
533
593
  end
534
594
 
595
+ it 'combined parentheses generates expected SQL' do
596
+ @instance.parse('(name.givenName eq "Jane" OR name.givenName eq "Jaden") and (name.familyName co "avi" or name.familyName ew "ith")')
597
+ query = @instance.to_activerecord_query(MockUser.all)
598
+
599
+ expect(query.to_sql).to eql(%q{SELECT "mock_users".* FROM "mock_users" WHERE ("mock_users"."first_name" ILIKE 'Jane' OR "mock_users"."first_name" ILIKE 'Jaden') AND ("mock_users"."last_name" ILIKE '%avi%' OR "mock_users"."last_name" ILIKE '%ith')})
600
+ end
601
+
535
602
  it 'finds expected items' do
536
603
  user_1 = MockUser.create(username: '1', first_name: 'Jane', last_name: 'Davis') # Match
537
604
  user_2 = MockUser.create(username: '2', first_name: 'Jane', last_name: 'Smith') # Match
@@ -267,7 +267,10 @@ RSpec.describe Scimitar::Resources::Base do
267
267
  end
268
268
 
269
269
  def self.scim_attributes
270
- [ Scimitar::Schema::Attribute.new(name: 'relationship', type: 'string', required: true) ]
270
+ [
271
+ Scimitar::Schema::Attribute.new(name: 'relationship', type: 'string', required: true),
272
+ Scimitar::Schema::Attribute.new(name: "userGroups", multiValued: true, complexType: Scimitar::ComplexTypes::ReferenceGroup, mutability: "writeOnly")
273
+ ]
271
274
  end
272
275
  end
273
276
 
@@ -291,6 +294,12 @@ RSpec.describe Scimitar::Resources::Base do
291
294
  resource = resource_class.new('extension-id' => {relationship: 'GAGA'})
292
295
  expect(resource.relationship).to eql('GAGA')
293
296
  end
297
+
298
+ it 'allows setting complex extension attributes' do
299
+ user_groups = [{ value: '123' }, { value: '456'}]
300
+ resource = resource_class.new('extension-id' => {userGroups: user_groups})
301
+ expect(resource.userGroups.map(&:value)).to eql(['123', '456'])
302
+ end
294
303
  end # "context '#initialize' do"
295
304
 
296
305
  context '#as_json' do
@@ -2685,6 +2685,14 @@ RSpec.describe Scimitar::Resources::Mixin do
2685
2685
  #
2686
2686
  context 'public interface' do
2687
2687
  shared_examples 'a patcher' do | force_upper_case: |
2688
+ it 'gives the user a comprehensible error when operations are missing' do
2689
+ patch = { 'schemas' => ['urn:ietf:params:scim:api:messages:2.0:PatchOp'] }
2690
+
2691
+ expect do
2692
+ @instance.from_scim_patch!(patch_hash: patch)
2693
+ end.to raise_error Scimitar::InvalidSyntaxError, "Missing PATCH \"operations\""
2694
+ end
2695
+
2688
2696
  it 'which updates simple values' do
2689
2697
  @instance.update!(username: 'foo')
2690
2698
 
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: 1.5.3
4
+ version: 1.6.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-09-16 00:00:00.000000000 Z
12
+ date: 2023-09-25 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails