scimitar 2.4.2 → 2.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: 610a1d6294e74d1a4540bd30e7cc01d85b25f184891ec8acad01368feb67fb6e
4
- data.tar.gz: 382185bb81b19a3c2cc31aafbaa144d18877f3fc1f93676456c656499fea79ee
3
+ metadata.gz: 9a1f1bf2bb09d47ecd354dd72ec9ecbaab96b17b8e1165df6f1ae4fe26200d8b
4
+ data.tar.gz: 3ed9ed5b0bb4612ee6213b86c73fb51a5ad2be1a19cffc360037b29c504b61cb
5
5
  SHA512:
6
- metadata.gz: 50bdeb5492bcc461f2fb550b3ce7d8f3e03c8e3d9b0ca66691702f47e91f3e3868745351a37b953d1416d1225be635f8bc01a61de3e8e017c86c9e471b219dee
7
- data.tar.gz: 2afddc9337f6810f8eaf6e932c14098ed56c473895716e7a21d5ae4eafc04831ecb8c6997cd128ab99a633a5f39debaa34fd3e3d59f629d4dc972737a7c3e95d
6
+ metadata.gz: 4353efc5d7acb49b23d69478e75de4fcc932d59331f9428b53f22e72cf91df2e3db8b8ee1c70af242b93fb64cf338cde0c1e5ee4f2557f79800768bca4ab2b4f
7
+ data.tar.gz: f57d58f5e6ef4eb0181d29f7ad46612eada045cd2b1a92edfeb663aa622e400b4474cb06707826c13ea8ade2d0dd51ebdad1b5edb063ce147abf51d62f1e3815
@@ -98,7 +98,7 @@ module Scimitar
98
98
  def require_scim
99
99
  scim_mime_type = Mime::Type.lookup_by_extension(:scim).to_s
100
100
 
101
- if request.media_type.nil?
101
+ if request.media_type.nil? || request.media_type.empty?
102
102
  request.format = :scim
103
103
  request.headers['CONTENT_TYPE'] = scim_mime_type
104
104
  elsif request.media_type.downcase == scim_mime_type
@@ -7,13 +7,17 @@ module Scimitar
7
7
  class EngineConfiguration
8
8
  include ActiveModel::Model
9
9
 
10
- attr_accessor :basic_authenticator,
11
- :token_authenticator,
12
- :application_controller_mixin,
13
- :exception_reporter,
14
- :optional_value_fields_required
10
+ attr_accessor(
11
+ :uses_defaults,
12
+ :basic_authenticator,
13
+ :token_authenticator,
14
+ :application_controller_mixin,
15
+ :exception_reporter,
16
+ :optional_value_fields_required,
17
+ )
15
18
 
16
19
  def initialize(attributes = {})
20
+ @uses_defaults = attributes.empty?
17
21
 
18
22
  # Set defaults that may be overridden by the initializer.
19
23
  #
@@ -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']
@@ -9,11 +9,22 @@ module Scimitar
9
9
  class ServiceProviderConfiguration
10
10
  include ActiveModel::Model
11
11
 
12
- attr_accessor :patch, :bulk, :filter, :changePassword,
13
- :sort, :etag, :authenticationSchemes,
14
- :schemas, :meta
12
+ attr_accessor(
13
+ :uses_defaults,
14
+ :patch,
15
+ :bulk,
16
+ :filter,
17
+ :changePassword,
18
+ :sort,
19
+ :etag,
20
+ :authenticationSchemes,
21
+ :schemas,
22
+ :meta,
23
+ )
15
24
 
16
25
  def initialize(attributes = {})
26
+ @uses_defaults = attributes.empty?
27
+
17
28
  defaults = {
18
29
  bulk: Supportable.unsupported,
19
30
  changePassword: Supportable.unsupported,
@@ -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.4.2'
6
+ VERSION = '2.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-03-21'
11
+ DATE = '2023-09-25'
12
12
 
13
13
  end
data/lib/scimitar.rb CHANGED
@@ -4,7 +4,9 @@ require 'scimitar/engine'
4
4
 
5
5
  module Scimitar
6
6
  def self.service_provider_configuration=(custom_configuration)
7
- @service_provider_configuration = custom_configuration
7
+ if @service_provider_configuration.nil? || ! custom_configuration.uses_defaults
8
+ @service_provider_configuration = custom_configuration
9
+ end
8
10
  end
9
11
 
10
12
  def self.service_provider_configuration(location:)
@@ -14,7 +16,9 @@ module Scimitar
14
16
  end
15
17
 
16
18
  def self.engine_configuration=(custom_configuration)
17
- @engine_configuration = custom_configuration
19
+ if @engine_configuration.nil? || ! custom_configuration.uses_defaults
20
+ @engine_configuration = custom_configuration
21
+ end
18
22
  end
19
23
 
20
24
  def self.engine_configuration
@@ -17,6 +17,7 @@ class MockUser < ActiveRecord::Base
17
17
  work_phone_number
18
18
  organization
19
19
  department
20
+ mock_groups
20
21
  }
21
22
 
22
23
  has_and_belongs_to_many :mock_groups
@@ -92,7 +93,17 @@ class MockUser < ActiveRecord::Base
92
93
  # "spec/apps/dummy/config/initializers/scimitar.rb".
93
94
  #
94
95
  organization: :organization,
95
- department: :department
96
+ department: :department,
97
+ userGroups: [
98
+ {
99
+ list: :mock_groups,
100
+ find_with: ->(value) { MockGroup.find(value["value"]) },
101
+ using: {
102
+ value: :id,
103
+ display: :display_name
104
+ }
105
+ }
106
+ ]
96
107
  }
97
108
  end
98
109
 
@@ -107,6 +118,8 @@ class MockUser < ActiveRecord::Base
107
118
  'meta.lastModified' => { column: :updated_at },
108
119
  'name.givenName' => { column: :first_name },
109
120
  'name.familyName' => { column: :last_name },
121
+ 'groups' => { column: MockGroup.arel_table[:id] },
122
+ 'groups.value' => { column: MockGroup.arel_table[:id] },
110
123
  'emails' => { columns: [ :work_email_address, :home_email_address ] },
111
124
  'emails.value' => { columns: [ :work_email_address, :home_email_address ] },
112
125
  'emails.type' => { ignore: true } # We can't filter on that; it'll just search all e-mails
@@ -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: 2.4.2
4
+ version: 2.5.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-03-21 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