scimitar 1.5.2 → 1.6.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: b55b1f0a0a700b57690cb05fc02241cf47f60a4ad8c92946a32366c90ee56d8c
4
- data.tar.gz: a7143dcd7d1381250e66529c70288659a2018fb26fc16639c14ba796acc805d5
3
+ metadata.gz: 7bed3013773832fde9275ba69b622a8538beb2d8ff6b6ffe506e1c9453480ec5
4
+ data.tar.gz: e51c811619169e64f3a373a56670514d37ac2823b7ed8b1bedd5c9b9c282576e
5
5
  SHA512:
6
- metadata.gz: fb740b528770a918fc3237cabdc953affcde23fa7787cc9e9383a6d71b104e2d6325c2829545d367983a3bb64435179a0523b6d785d8e6d3eb7b2bfdb6c82748
7
- data.tar.gz: 9f73bcafefbd7de57f258d1dc1c18e020b8dd7bca6330f7b288e308168daf7cb698051a9f2d4eaee9717139978df4e7a9fe854ba269ab94b2b46f1030d2ab9db
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
  #
@@ -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,
@@ -2,101 +2,105 @@
2
2
  #
3
3
  # For supporting information and rationale, please see README.md.
4
4
 
5
- # =============================================================================
6
- # SERVICE PROVIDER CONFIGURATION
7
- # =============================================================================
8
- #
9
- # This is a Ruby abstraction over a SCIM entity that declares the capabilities
10
- # supported by a particular implementation.
11
- #
12
- # Typically this is used to declare parts of the standard unsupported, if you
13
- # don't need them and don't want to provide subclass support.
14
- #
15
- Scimitar.service_provider_configuration = Scimitar::ServiceProviderConfiguration.new({
5
+ Rails.application.config.to_prepare do # (required for >= Rails 7 / Zeitwerk)
16
6
 
17
- # See https://tools.ietf.org/html/rfc7643#section-8.5 for properties.
7
+ # ===========================================================================
8
+ # SERVICE PROVIDER CONFIGURATION
9
+ # ===========================================================================
18
10
  #
19
- # See Gem source file 'app/models/scimitar/service_provider_configuration.rb'
20
- # for defaults. Define Hash keys here that override defaults; e.g. to declare
21
- # that filters are not supported so that calling clients shouldn't use them:
11
+ # This is a Ruby abstraction over a SCIM entity that declares the
12
+ # capabilities supported by a particular implementation.
22
13
  #
23
- # filter: Scimitar::Supported.unsupported
14
+ # Typically this is used to declare parts of the standard unsupported, if you
15
+ # don't need them and don't want to provide subclass support.
16
+ #
17
+ Scimitar.service_provider_configuration = Scimitar::ServiceProviderConfiguration.new({
24
18
 
25
- })
19
+ # See https://tools.ietf.org/html/rfc7643#section-8.5 for properties.
20
+ #
21
+ # See Gem file 'app/models/scimitar/service_provider_configuration.rb'
22
+ # for defaults. Define Hash keys here that override defaults; e.g. to
23
+ # declare that filters are not supported so that calling clients shouldn't
24
+ # use them:
25
+ #
26
+ # filter: Scimitar::Supported.unsupported
26
27
 
27
- # =============================================================================
28
- # ENGINE CONFIGURATION
29
- # =============================================================================
30
- #
31
- # This is where you provide callbacks for things like authorisation or mixins
32
- # that get included into all Scimitar-derived controllers (for things like
33
- # before-actions that apply to all Scimitar controller-based routes).
34
- #
35
- Scimitar.engine_configuration = Scimitar::EngineConfiguration.new({
28
+ })
36
29
 
37
- # If you have filters you want to run for any Scimitar action/route, you can
38
- # define them here. For example, you might use a before-action to set up some
39
- # multi-tenancy related state, or skip Rails CSRF token verification/
40
- #
41
- # For example:
42
- #
43
- # application_controller_mixin: Module.new do
44
- # def self.included(base)
45
- # base.class_eval do
30
+ # ===========================================================================
31
+ # ENGINE CONFIGURATION
32
+ # ===========================================================================
46
33
  #
47
- # # Anything here is written just as you'd write it at the top of
48
- # # one of your controller classes, but it gets included in all
49
- # # Scimitar classes too.
34
+ # This is where you provide callbacks for things like authorisation or mixins
35
+ # that get included into all Scimitar-derived controllers (for things like
36
+ # before-actions that apply to all Scimitar controller-based routes).
50
37
  #
51
- # skip_before_action :verify_authenticity_token
52
- # prepend_before_action :setup_some_kind_of_multi_tenancy_data
53
- # end
54
- # end
55
- # end, # ...other configuration entries might follow...
38
+ Scimitar.engine_configuration = Scimitar::EngineConfiguration.new({
56
39
 
57
- # If you want to support username/password authentication:
58
- #
59
- # basic_authenticator: Proc.new do | username, password |
60
- # # Check username/password and return 'true' if valid, else 'false'.
61
- # end, # ...other configuration entries might follow...
62
- #
63
- # The 'username' and 'password' parameters come from Rails:
64
- #
65
- # https://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Basic.html
66
- # https://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Basic/ControllerMethods.html#method-i-authenticate_with_http_basic
40
+ # If you have filters you want to run for any Scimitar action/route, you
41
+ # can define them here. For example, you might use a before-action to set
42
+ # up some multi-tenancy related state, or skip Rails CSRF token
43
+ # verification. For example:
44
+ #
45
+ # application_controller_mixin: Module.new do
46
+ # def self.included(base)
47
+ # base.class_eval do
48
+ #
49
+ # # Anything here is written just as you'd write it at the top of
50
+ # # one of your controller classes, but it gets included in all
51
+ # # Scimitar classes too.
52
+ #
53
+ # skip_before_action :verify_authenticity_token
54
+ # prepend_before_action :setup_some_kind_of_multi_tenancy_data
55
+ # end
56
+ # end
57
+ # end, # ...other configuration entries might follow...
67
58
 
68
- # If you want to support HTTP bearer token (OAuth-style) authentication:
69
- #
70
- # token_authenticator: Proc.new do | token, options |
71
- # # Check token and return 'true' if valid, else 'false'.
72
- # end, # ...other configuration entries might follow...
73
- #
74
- # The 'token' and 'options' parameters come from Rails:
75
- #
76
- # https://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Token.html
77
- # https://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Token/ControllerMethods.html#method-i-authenticate_with_http_token
78
- #
79
- # Note that both basic and token authentication can be declared, with the
80
- # parameters in the inbound HTTP request determining which is invoked.
59
+ # If you want to support username/password authentication:
60
+ #
61
+ # basic_authenticator: Proc.new do | username, password |
62
+ # # Check username/password and return 'true' if valid, else 'false'.
63
+ # end, # ...other configuration entries might follow...
64
+ #
65
+ # The 'username' and 'password' parameters come from Rails:
66
+ #
67
+ # https://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Basic.html
68
+ # https://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Basic/ControllerMethods.html#method-i-authenticate_with_http_basic
81
69
 
82
- # Scimitar rescues certain error cases and exceptions, in order to return a
83
- # JSON response to the API caller. If you want exceptions to also be
84
- # reported to a third party system such as sentry.io or raygun.com, you can
85
- # configure a Proc to do so. It is passed a Ruby exception subclass object.
86
- # For example, a minimal sentry.io reporter might do this:
87
- #
88
- # exception_reporter: Proc.new do | exception |
89
- # Sentry.capture_exception(exception)
90
- # end
91
- #
92
- # You will still need to configure your reporting system according to its
93
- # documentation (e.g. via a Rails "config/initializers/<foo>.rb" file).
70
+ # If you want to support HTTP bearer token (OAuth-style) authentication:
71
+ #
72
+ # token_authenticator: Proc.new do | token, options |
73
+ # # Check token and return 'true' if valid, else 'false'.
74
+ # end, # ...other configuration entries might follow...
75
+ #
76
+ # The 'token' and 'options' parameters come from Rails:
77
+ #
78
+ # https://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Token.html
79
+ # https://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Token/ControllerMethods.html#method-i-authenticate_with_http_token
80
+ #
81
+ # Note that both basic and token authentication can be declared, with the
82
+ # parameters in the inbound HTTP request determining which is invoked.
94
83
 
95
- # Scimilar treats "VDTP" (Value, Display, Type, Primary) attribute values,
96
- # used for e.g. e-mail addresses or phone numbers, as required by default.
97
- # If you encounter a service which calls these with e.g. "null" value data,
98
- # you can configure all values to be optional. You'll need to deal with
99
- # whatever that means for you receiving system in your model code.
100
- #
101
- # optional_value_fields_required: false
102
- })
84
+ # Scimitar rescues certain error cases and exceptions, in order to return a
85
+ # JSON response to the API caller. If you want exceptions to also be
86
+ # reported to a third party system such as sentry.io or raygun.com, you can
87
+ # configure a Proc to do so. It is passed a Ruby exception subclass object.
88
+ # For example, a minimal sentry.io reporter might do this:
89
+ #
90
+ # exception_reporter: Proc.new do | exception |
91
+ # Sentry.capture_exception(exception)
92
+ # end
93
+ #
94
+ # You will still need to configure your reporting system according to its
95
+ # documentation (e.g. via a Rails "config/initializers/<foo>.rb" file).
96
+
97
+ # Scimilar treats "VDTP" (Value, Display, Type, Primary) attribute values,
98
+ # used for e.g. e-mail addresses or phone numbers, as required by default.
99
+ # If you encounter a service which calls these with e.g. "null" value data,
100
+ # you can configure all values to be optional. You'll need to deal with
101
+ # whatever that means for you receiving system in your model code.
102
+ #
103
+ # optional_value_fields_required: false
104
+ })
105
+
106
+ 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.5.2'
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-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
@@ -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
@@ -9,6 +9,14 @@
9
9
  #
10
10
  # All related schema tests are written with this in mind.
11
11
  #
12
+ # Further, https://github.com/RIPAGlobal/scimitar/pull/54 fixed warning
13
+ # messages in a way that worked on Rails 6+ but, for V1 Scimitar, it would
14
+ # break existing working setups that didn't use the +to_prepare+ wrapper. Their
15
+ # application configuration would be written *first* but then *overwritten* by
16
+ # the default +to_prepare+ block in Scimitar itself, since that runs later. The
17
+ # file below does *not* use +to_prepare+ in order to test the workaround that
18
+ # was produced; it should work on all Ruby versions as-is.
19
+ #
12
20
  Scimitar.engine_configuration = Scimitar::EngineConfiguration.new({
13
21
 
14
22
  application_controller_mixin: Module.new do
@@ -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.2
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-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