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 +4 -4
- data/app/controllers/scimitar/application_controller.rb +0 -1
- data/app/models/scimitar/lists/query_parser.rb +11 -6
- data/app/models/scimitar/resources/base.rb +1 -1
- data/app/models/scimitar/resources/mixin.rb +24 -9
- data/lib/scimitar/version.rb +2 -2
- data/lib/scimitar.rb +0 -12
- data/spec/apps/dummy/app/models/mock_user.rb +14 -1
- data/spec/apps/dummy/config/routes.rb +6 -6
- data/spec/models/scimitar/lists/query_parser_spec.rb +67 -0
- data/spec/models/scimitar/resources/base_spec.rb +10 -1
- data/spec/models/scimitar/resources/mixin_spec.rb +8 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7bed3013773832fde9275ba69b622a8538beb2d8ff6b6ffe506e1c9453480ec5
|
4
|
+
data.tar.gz: e51c811619169e64f3a373a56670514d37ac2823b7ed8b1bedd5c9b9c282576e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 697b3299edb4752baf38b68574191b72528a5e69467aa1a097c9b8c92928d57cff9d21afa50d7c66c15fc66220e52469f2b602ef3a5051489f734717f3489955
|
7
|
+
data.tar.gz: 7d7a7bcb8fa0a9881ad7b77b6b81a4ec096e56300e8b5594fb0538adb76f7ee06b65fb56e319f1b7872738e4807d667e534f5c99b60d86b8bf5125e0d2972dc8
|
@@ -192,7 +192,7 @@ module Scimitar
|
|
192
192
|
|
193
193
|
ast.push(self.start_group? ? self.parse_group() : self.pop())
|
194
194
|
|
195
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
224
|
-
#
|
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
|
-
|
425
|
+
operations.each do |operation|
|
411
426
|
nature = operation['op' ]&.downcase
|
412
427
|
path_str = operation['path' ]
|
413
428
|
value = operation['value']
|
data/lib/scimitar/version.rb
CHANGED
@@ -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.
|
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-
|
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',
|
10
|
-
get 'Users/:id',
|
11
|
-
post 'Users',
|
12
|
-
put 'Users/:id',
|
13
|
-
patch 'Users/:id',
|
14
|
-
delete 'Users/:id',
|
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
|
-
[
|
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.
|
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-
|
12
|
+
date: 2023-09-25 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rails
|