scimitar 1.8.1 → 1.10.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/README.md +27 -20
- data/app/controllers/scimitar/active_record_backed_resources_controller.rb +5 -4
- data/app/controllers/scimitar/resource_types_controller.rb +0 -2
- data/app/controllers/scimitar/resources_controller.rb +0 -2
- data/app/controllers/scimitar/schemas_controller.rb +361 -3
- data/app/controllers/scimitar/service_provider_configurations_controller.rb +0 -1
- data/app/models/scimitar/engine_configuration.rb +3 -1
- data/app/models/scimitar/lists/query_parser.rb +88 -3
- data/app/models/scimitar/resources/base.rb +48 -14
- data/app/models/scimitar/resources/mixin.rb +531 -71
- data/app/models/scimitar/schema/name.rb +2 -2
- data/app/models/scimitar/schema/user.rb +10 -10
- data/config/initializers/scimitar.rb +41 -0
- data/lib/scimitar/engine.rb +57 -12
- data/lib/scimitar/support/hash_with_indifferent_case_insensitive_access.rb +140 -10
- data/lib/scimitar/support/utilities.rb +60 -0
- data/lib/scimitar/version.rb +2 -2
- data/spec/apps/dummy/app/models/mock_user.rb +18 -3
- data/spec/apps/dummy/config/initializers/scimitar.rb +31 -2
- data/spec/apps/dummy/db/migrate/20210304014602_create_mock_users.rb +1 -0
- data/spec/apps/dummy/db/schema.rb +1 -0
- data/spec/controllers/scimitar/schemas_controller_spec.rb +342 -54
- data/spec/models/scimitar/lists/query_parser_spec.rb +70 -0
- data/spec/models/scimitar/resources/base_spec.rb +20 -12
- data/spec/models/scimitar/resources/base_validation_spec.rb +16 -3
- data/spec/models/scimitar/resources/mixin_spec.rb +754 -122
- data/spec/models/scimitar/schema/user_spec.rb +2 -2
- data/spec/requests/active_record_backed_resources_controller_spec.rb +312 -5
- data/spec/requests/engine_spec.rb +75 -0
- data/spec/spec_helper.rb +1 -1
- data/spec/support/hash_with_indifferent_case_insensitive_access_spec.rb +108 -0
- metadata +22 -22
@@ -10,74 +10,362 @@ RSpec.describe Scimitar::SchemasController do
|
|
10
10
|
super
|
11
11
|
end
|
12
12
|
end
|
13
|
+
|
13
14
|
context '#index' do
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
expect(parsed_body.length).to eql(3)
|
19
|
-
schema_names = parsed_body.map {|schema| schema['name']}
|
20
|
-
expect(schema_names).to match_array(['User', 'ExtendedUser', 'Group'])
|
21
|
-
end
|
15
|
+
shared_examples 'a Schema list which' do
|
16
|
+
it 'returns a valid ListResponse' do
|
17
|
+
get :index, params: { format: :scim }
|
18
|
+
expect(response).to be_ok
|
22
19
|
|
23
|
-
|
24
|
-
|
25
|
-
expect(response).to be_ok
|
26
|
-
parsed_body = JSON.parse(response.body)
|
27
|
-
expect(parsed_body['name']).to eql('User')
|
28
|
-
end
|
20
|
+
parsed_body = JSON.parse(response.body)
|
21
|
+
schema_count = parsed_body['Resources']&.size
|
29
22
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
23
|
+
expect(parsed_body['schemas' ]).to match_array(['urn:ietf:params:scim:api:messages:2.0:ListResponse'])
|
24
|
+
expect(parsed_body['totalResults']).to eql(schema_count)
|
25
|
+
expect(parsed_body['itemsPerPage']).to eql(schema_count)
|
26
|
+
expect(parsed_body['startIndex' ]).to eql(1)
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'returns a collection of supported schemas' do
|
30
|
+
get :index, params: { format: :scim }
|
31
|
+
expect(response).to be_ok
|
32
|
+
|
33
|
+
parsed_body = JSON.parse(response.body)
|
34
|
+
expect(parsed_body['Resources']&.size).to eql(4)
|
35
|
+
|
36
|
+
schema_names = parsed_body['Resources'].map {|schema| schema['name']}
|
37
|
+
expect(schema_names).to match_array(['User', 'EnterpriseExtendedUser', 'ManagementExtendedUser', 'Group'])
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'returns only the User schema when its ID is provided' do
|
41
|
+
get :index, params: { name: Scimitar::Schema::User.id, format: :scim }
|
42
|
+
expect(response).to be_ok
|
36
43
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
44
|
+
parsed_body = JSON.parse(response.body)
|
45
|
+
expect(parsed_body.dig('Resources', 0, 'name')).to eql('User')
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'includes the controller customised schema location' do
|
49
|
+
get :index, params: { name: Scimitar::Schema::User.id, format: :scim }
|
50
|
+
expect(response).to be_ok
|
51
|
+
|
52
|
+
parsed_body = JSON.parse(response.body)
|
53
|
+
expect(parsed_body.dig('Resources', 0, 'meta', 'location')).to eq scim_schemas_url(name: Scimitar::Schema::User.id, test: 1)
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'returns only the Group schema when its ID is provided' do
|
57
|
+
get :index, params: { name: Scimitar::Schema::Group.id, format: :scim }
|
58
|
+
expect(response).to be_ok
|
59
|
+
|
60
|
+
parsed_body = JSON.parse(response.body)
|
61
|
+
|
62
|
+
expect(parsed_body['Resources' ]&.size).to eql(1)
|
63
|
+
expect(parsed_body['totalResults'] ).to eql(1)
|
64
|
+
expect(parsed_body['itemsPerPage'] ).to eql(1)
|
65
|
+
expect(parsed_body['startIndex' ] ).to eql(1)
|
66
|
+
|
67
|
+
expect(parsed_body.dig('Resources', 0, 'name')).to eql('Group')
|
68
|
+
end
|
42
69
|
end
|
43
70
|
|
44
|
-
context 'with
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
Scimitar::
|
71
|
+
context 'with default engine configuration of schema_list_from_attribute_mappings undefined' do
|
72
|
+
it_behaves_like 'a Schema list which'
|
73
|
+
|
74
|
+
it 'returns all attributes' do
|
75
|
+
get :index, params: { name: Scimitar::Schema::User.id, format: :scim }
|
76
|
+
expect(response).to be_ok
|
77
|
+
|
78
|
+
parsed_body = JSON.parse(response.body)
|
79
|
+
user_attrs = parsed_body['Resources'].find { | r | r['name'] == 'User' }
|
80
|
+
|
81
|
+
expect(user_attrs['attributes'].find { | a | a['name'] == 'ims' }).to be_present
|
82
|
+
expect(user_attrs['attributes'].find { | a | a['name'] == 'entitlements' }).to be_present
|
83
|
+
expect(user_attrs['attributes'].find { | a | a['name'] == 'x509Certificates' }).to be_present
|
84
|
+
|
85
|
+
name_attr = user_attrs['attributes'].find { | a | a['name'] == 'name' }
|
86
|
+
|
87
|
+
expect(name_attr['subAttributes'].find { | s | s['name'] == 'honorificPrefix' }).to be_present
|
88
|
+
expect(name_attr['subAttributes'].find { | s | s['name'] == 'honorificSuffix' }).to be_present
|
49
89
|
end
|
50
90
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
91
|
+
context 'with custom resource types' do
|
92
|
+
around :each do | example |
|
93
|
+
example.run()
|
94
|
+
ensure
|
95
|
+
Scimitar::Engine.reset_custom_resources
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'returns only the License schemas when its ID is provided' do
|
99
|
+
license_schema = Class.new(Scimitar::Schema::Base) do
|
100
|
+
def initialize(options = {})
|
101
|
+
super(name: 'License', id: self.class.id(), description: 'Represents a License')
|
102
|
+
end
|
103
|
+
def self.id; 'urn:ietf:params:scim:schemas:license'; end
|
104
|
+
def self.scim_attributes; []; end
|
60
105
|
end
|
61
|
-
|
62
|
-
|
106
|
+
|
107
|
+
license_resource = Class.new(Scimitar::Resources::Base) do
|
108
|
+
set_schema(license_schema)
|
109
|
+
def self.endpoint; '/License'; end
|
63
110
|
end
|
111
|
+
|
112
|
+
Scimitar::Engine.add_custom_resource(license_resource)
|
113
|
+
|
114
|
+
get :index, params: { name: license_schema.id, format: :scim }
|
115
|
+
expect(response).to be_ok
|
116
|
+
|
117
|
+
parsed_body = JSON.parse(response.body)
|
118
|
+
expect(parsed_body.dig('Resources', 0, 'name')).to eql('License')
|
64
119
|
end
|
120
|
+
end # "context 'with custom resource types' do"
|
121
|
+
end # "context 'with default engine configuration of schema_list_from_attribute_mappings undefined' do"
|
65
122
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
123
|
+
context 'with engine configuration of schema_list_from_attribute_mappings set' do
|
124
|
+
context 'standard resources' do
|
125
|
+
around :each do | example |
|
126
|
+
old_config = Scimitar.engine_configuration.schema_list_from_attribute_mappings
|
127
|
+
Scimitar.engine_configuration.schema_list_from_attribute_mappings = [
|
128
|
+
MockUser,
|
129
|
+
MockGroup
|
130
|
+
]
|
131
|
+
example.run()
|
132
|
+
ensure
|
133
|
+
Scimitar.engine_configuration.schema_list_from_attribute_mappings = old_config
|
71
134
|
end
|
72
135
|
|
73
|
-
|
136
|
+
it_behaves_like 'a Schema list which'
|
74
137
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
138
|
+
it 'returns only mapped attributes' do
|
139
|
+
get :index, params: { name: Scimitar::Schema::User.id, format: :scim }
|
140
|
+
expect(response).to be_ok
|
141
|
+
|
142
|
+
parsed_body = JSON.parse(response.body)
|
143
|
+
user_attrs = parsed_body['Resources'].find { | r | r['name'] == 'User' }
|
144
|
+
password_attr = user_attrs['attributes'].find { | a | a['name'] == 'password' }
|
145
|
+
|
146
|
+
expect(password_attr['mutability']).to eql('writeOnly')
|
147
|
+
|
148
|
+
expect(user_attrs['attributes'].find { | a | a['name'] == 'ims' }).to_not be_present
|
149
|
+
expect(user_attrs['attributes'].find { | a | a['name'] == 'entitlements' }).to_not be_present
|
150
|
+
expect(user_attrs['attributes'].find { | a | a['name'] == 'x509Certificates' }).to_not be_present
|
151
|
+
|
152
|
+
name_attr = user_attrs['attributes'].find { | a | a['name'] == 'name' }
|
153
|
+
|
154
|
+
expect(name_attr['subAttributes'].find { | s | s['name'] == 'givenName' }).to be_present
|
155
|
+
expect(name_attr['subAttributes'].find { | s | s['name'] == 'familyName' }).to be_present
|
156
|
+
expect(name_attr['subAttributes'].find { | s | s['name'] == 'honorificPrefix' }).to_not be_present
|
157
|
+
expect(name_attr['subAttributes'].find { | s | s['name'] == 'honorificSuffix' }).to_not be_present
|
158
|
+
|
159
|
+
emails_attr = user_attrs['attributes' ].find { | a | a['name'] == 'emails' }
|
160
|
+
value_attr = emails_attr['subAttributes'].find { | a | a['name'] == 'value' }
|
161
|
+
primary_attr = emails_attr['subAttributes'].find { | a | a['name'] == 'primary' }
|
162
|
+
|
163
|
+
expect( value_attr['mutability']).to eql('readWrite')
|
164
|
+
expect(primary_attr['mutability']).to eql('readOnly')
|
165
|
+
|
166
|
+
expect(emails_attr['subAttributes'].find { | s | s['name'] == 'type' }).to_not be_present
|
167
|
+
expect(emails_attr['subAttributes'].find { | s | s['name'] == 'display' }).to_not be_present
|
168
|
+
|
169
|
+
groups_attr = user_attrs['attributes' ].find { | a | a['name'] == 'groups' }
|
170
|
+
value_attr = groups_attr['subAttributes'].find { | a | a['name'] == 'value' }
|
171
|
+
display_attr = groups_attr['subAttributes'].find { | a | a['name'] == 'display' }
|
172
|
+
|
173
|
+
expect( value_attr['mutability']).to eql('readOnly')
|
174
|
+
expect(display_attr['mutability']).to eql('readOnly')
|
175
|
+
end
|
79
176
|
end
|
80
|
-
end
|
81
|
-
end
|
82
|
-
end
|
83
177
|
|
178
|
+
context 'with custom resource types' do
|
179
|
+
let(:license_schema) {
|
180
|
+
Class.new(Scimitar::Schema::Base) do
|
181
|
+
def initialize(options = {})
|
182
|
+
super(
|
183
|
+
id: self.class.id(),
|
184
|
+
name: 'License',
|
185
|
+
description: 'Represents a license',
|
186
|
+
scim_attributes: self.class.scim_attributes
|
187
|
+
)
|
188
|
+
end
|
189
|
+
def self.id; 'urn:ietf:params:scim:schemas:license'; end
|
190
|
+
def self.scim_attributes
|
191
|
+
[
|
192
|
+
Scimitar::Schema::Attribute.new(name: 'licenseNumber', type: 'string'),
|
193
|
+
Scimitar::Schema::Attribute.new(name: 'licenseExpired', type: 'boolean', mutability: 'readOnly'),
|
194
|
+
]
|
195
|
+
end
|
196
|
+
end
|
197
|
+
}
|
198
|
+
|
199
|
+
let(:license_resource) {
|
200
|
+
local_var_license_schema = license_schema()
|
201
|
+
|
202
|
+
Class.new(Scimitar::Resources::Base) do
|
203
|
+
set_schema(local_var_license_schema)
|
204
|
+
def self.endpoint; '/License'; end
|
205
|
+
end
|
206
|
+
}
|
207
|
+
|
208
|
+
let(:license_model_base) {
|
209
|
+
local_var_license_resource = license_resource()
|
210
|
+
|
211
|
+
Class.new do
|
212
|
+
singleton_class.class_eval do
|
213
|
+
define_method(:scim_resource_type) { local_var_license_resource }
|
214
|
+
end
|
215
|
+
end
|
216
|
+
}
|
217
|
+
|
218
|
+
around :each do | example |
|
219
|
+
old_config = Scimitar.engine_configuration.schema_list_from_attribute_mappings
|
220
|
+
Scimitar::Engine.add_custom_resource(license_resource())
|
221
|
+
example.run()
|
222
|
+
ensure
|
223
|
+
Scimitar.engine_configuration.schema_list_from_attribute_mappings = old_config
|
224
|
+
Scimitar::Engine.reset_custom_resources
|
225
|
+
end
|
226
|
+
|
227
|
+
context 'with an empty attribute map' do
|
228
|
+
it 'returns no attributes' do
|
229
|
+
license_model = Class.new(license_model_base()) do
|
230
|
+
attr_accessor :license_number
|
231
|
+
|
232
|
+
def self.scim_mutable_attributes; nil; end
|
233
|
+
def self.scim_queryable_attributes; nil; end
|
234
|
+
def self.scim_attributes_map; {}; end # Empty map
|
235
|
+
|
236
|
+
include Scimitar::Resources::Mixin
|
237
|
+
end
|
238
|
+
|
239
|
+
Scimitar.engine_configuration.schema_list_from_attribute_mappings = [license_model]
|
240
|
+
|
241
|
+
get :index, params: { format: :scim }
|
242
|
+
expect(response).to be_ok
|
243
|
+
|
244
|
+
parsed_body = JSON.parse(response.body)
|
245
|
+
|
246
|
+
expect(parsed_body.dig('Resources', 0, 'name' )).to eql('License')
|
247
|
+
expect(parsed_body.dig('Resources', 0, 'attributes')).to be_empty
|
248
|
+
end
|
249
|
+
end # "context 'with an empty attribute map' do"
|
250
|
+
|
251
|
+
context 'with a defined attribute map' do
|
252
|
+
it 'returns only the License schemas when its ID is provided' do
|
253
|
+
license_model = Class.new(license_model_base()) do
|
254
|
+
attr_accessor :license_number
|
255
|
+
|
256
|
+
def self.scim_mutable_attributes; nil; end
|
257
|
+
def self.scim_queryable_attributes; nil; end
|
258
|
+
def self.scim_attributes_map # Simple map
|
259
|
+
{ licenseNumber: :license_number }
|
260
|
+
end
|
261
|
+
|
262
|
+
include Scimitar::Resources::Mixin
|
263
|
+
end
|
264
|
+
|
265
|
+
Scimitar.engine_configuration.schema_list_from_attribute_mappings = [license_model]
|
266
|
+
|
267
|
+
get :index, params: { format: :scim }
|
268
|
+
expect(response).to be_ok
|
269
|
+
|
270
|
+
parsed_body = JSON.parse(response.body)
|
271
|
+
|
272
|
+
expect(parsed_body.dig('Resources', 0, 'name' )).to eql('License')
|
273
|
+
expect(parsed_body.dig('Resources', 0, 'attributes').size ).to eql(1)
|
274
|
+
expect(parsed_body.dig('Resources', 0, 'attributes', 0, 'name' )).to eql('licenseNumber')
|
275
|
+
expect(parsed_body.dig('Resources', 0, 'attributes', 0, 'mutability')).to eql('readWrite')
|
276
|
+
end
|
277
|
+
end # "context 'with a defined attribute map' do"
|
278
|
+
|
279
|
+
context 'with mutability overridden' do
|
280
|
+
it 'returns read-only when expected' do
|
281
|
+
license_model = Class.new(license_model_base()) do
|
282
|
+
attr_accessor :license_number
|
283
|
+
|
284
|
+
def self.scim_mutable_attributes; []; end # Note empty array, NOT "nil" - no mutable attributes
|
285
|
+
def self.scim_queryable_attributes; nil; end
|
286
|
+
def self.scim_attributes_map
|
287
|
+
{ licenseNumber: :license_number }
|
288
|
+
end
|
289
|
+
|
290
|
+
include Scimitar::Resources::Mixin
|
291
|
+
end
|
292
|
+
|
293
|
+
Scimitar.engine_configuration.schema_list_from_attribute_mappings = [license_model]
|
294
|
+
|
295
|
+
get :index, params: { format: :scim }
|
296
|
+
expect(response).to be_ok
|
297
|
+
|
298
|
+
parsed_body = JSON.parse(response.body)
|
299
|
+
|
300
|
+
expect(parsed_body.dig('Resources', 0, 'name' )).to eql('License')
|
301
|
+
expect(parsed_body.dig('Resources', 0, 'attributes').size ).to eql(1)
|
302
|
+
expect(parsed_body.dig('Resources', 0, 'attributes', 0, 'name' )).to eql('licenseNumber')
|
303
|
+
expect(parsed_body.dig('Resources', 0, 'attributes', 0, 'mutability')).to eql('readOnly')
|
304
|
+
end
|
305
|
+
|
306
|
+
it 'returns write-only when expected' do
|
307
|
+
license_model = Class.new(license_model_base()) do
|
308
|
+
attr_writer :license_number # Writer only, no reader
|
309
|
+
|
310
|
+
def self.scim_mutable_attributes; nil; end
|
311
|
+
def self.scim_queryable_attributes; nil; end
|
312
|
+
def self.scim_attributes_map
|
313
|
+
{ licenseNumber: :license_number }
|
314
|
+
end
|
315
|
+
|
316
|
+
include Scimitar::Resources::Mixin
|
317
|
+
end
|
318
|
+
|
319
|
+
Scimitar.engine_configuration.schema_list_from_attribute_mappings = [license_model]
|
320
|
+
|
321
|
+
get :index, params: { format: :scim }
|
322
|
+
expect(response).to be_ok
|
323
|
+
|
324
|
+
parsed_body = JSON.parse(response.body)
|
325
|
+
|
326
|
+
expect(parsed_body.dig('Resources', 0, 'name' )).to eql('License')
|
327
|
+
expect(parsed_body.dig('Resources', 0, 'attributes').size ).to eql(1)
|
328
|
+
expect(parsed_body.dig('Resources', 0, 'attributes', 0, 'name' )).to eql('licenseNumber')
|
329
|
+
expect(parsed_body.dig('Resources', 0, 'attributes', 0, 'mutability')).to eql('writeOnly')
|
330
|
+
end
|
331
|
+
|
332
|
+
it 'handles conflicts via reality-wins' do
|
333
|
+
license_model = Class.new(license_model_base()) do
|
334
|
+
def self.scim_mutable_attributes; [:licence_expired]; end
|
335
|
+
def self.scim_queryable_attributes; nil; end
|
336
|
+
def self.scim_attributes_map
|
337
|
+
{ licenseNumber: :license_number, licenseExpired: :licence_expired }
|
338
|
+
end
|
339
|
+
|
340
|
+
include Scimitar::Resources::Mixin
|
341
|
+
end
|
342
|
+
|
343
|
+
Scimitar.engine_configuration.schema_list_from_attribute_mappings = [license_model]
|
344
|
+
|
345
|
+
get :index, params: { format: :scim }
|
346
|
+
expect(response).to be_ok
|
347
|
+
|
348
|
+
parsed_body = JSON.parse(response.body)
|
349
|
+
attributes = parsed_body.dig('Resources', 0, 'attributes')
|
350
|
+
|
351
|
+
expect(parsed_body.dig('Resources', 0, 'name')).to eql('License')
|
352
|
+
expect(attributes.size).to eql(2)
|
353
|
+
|
354
|
+
number_attr = attributes.find { | a | a['name'] == 'licenseNumber' }
|
355
|
+
expiry_attr = attributes.find { | a | a['name'] == 'licenseExpired' }
|
356
|
+
|
357
|
+
# Number attribute - no reader or writer, so code has to shrug and
|
358
|
+
# say "it's broken, so I'll quote the schema verbatim'.
|
359
|
+
#
|
360
|
+
# Expiry attribute - is read-only in schema, but we declare it as a
|
361
|
+
# writable attribute and provide no reader. This clashes badly; the
|
362
|
+
# schema read-only declaration is ignored in favour of reality.
|
363
|
+
#
|
364
|
+
expect(number_attr['mutability']).to eql('readWrite')
|
365
|
+
expect(expiry_attr['mutability']).to eql('writeOnly')
|
366
|
+
end
|
367
|
+
end # "context 'with mutability overridden' do"
|
368
|
+
end # "context 'with custom resource types' do"
|
369
|
+
end # "context 'with engine configuration of schema_list_from_attribute_mappings: true' do"
|
370
|
+
end # "context '#index' do
|
371
|
+
end
|
@@ -60,6 +60,15 @@ RSpec.describe Scimitar::Lists::QueryParser do
|
|
60
60
|
expect(%Q("O'Malley")).to eql(tree[2])
|
61
61
|
end
|
62
62
|
|
63
|
+
it "extended attribute equals" do
|
64
|
+
@instance.parse(%Q(primaryEmail eq "foo@bar.com"))
|
65
|
+
|
66
|
+
rpn = @instance.rpn
|
67
|
+
expect('primaryEmail').to eql(rpn[0])
|
68
|
+
expect(%Q("foo@bar.com")).to eql(rpn[1])
|
69
|
+
expect('eq').to eql(rpn[2])
|
70
|
+
end
|
71
|
+
|
63
72
|
it "user name starts with" do
|
64
73
|
@instance.parse(%Q(userName sw "J"))
|
65
74
|
|
@@ -337,6 +346,67 @@ RSpec.describe Scimitar::Lists::QueryParser do
|
|
337
346
|
result = @instance.send(:flatten_filter, 'emails[type eq "work" and value co "@example.com" ] or userType eq "Admin" or ims[type eq "xmpp" and value co "@foo.com"]')
|
338
347
|
expect(result).to eql('emails.type eq "work" and emails.value co "@example.com" or userType eq "Admin" or ims.type eq "xmpp" and ims.value co "@foo.com"')
|
339
348
|
end
|
349
|
+
|
350
|
+
it 'handles an example previously described as unsupported in README.md' do
|
351
|
+
result = @instance.send(:flatten_filter, 'filter=userType eq "Employee" and emails[type eq "work" and value co "@example.com"]')
|
352
|
+
expect(result).to eql('filter=userType eq "Employee" and emails.type eq "work" and emails.value co "@example.com"')
|
353
|
+
end
|
354
|
+
|
355
|
+
# https://github.com/RIPAGlobal/scimitar/issues/116
|
356
|
+
#
|
357
|
+
context 'with schema IDs (GitHub issue #116)' do
|
358
|
+
it 'handles simple attributes' do
|
359
|
+
result = @instance.send(:flatten_filter, 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:employeeId eq "gsar"')
|
360
|
+
expect(result).to eql('employeeId eq "gsar"')
|
361
|
+
end
|
362
|
+
|
363
|
+
it 'handles dotted attribute paths' do
|
364
|
+
result = @instance.send(:flatten_filter, 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:imaginary.path eq "gsar"')
|
365
|
+
expect(result).to eql('imaginary.path eq "gsar"')
|
366
|
+
end
|
367
|
+
|
368
|
+
it 'replaces all examples' do
|
369
|
+
result = @instance.send(:flatten_filter, 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:employeeId eq "gsar" or urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:imaginary.path eq "gsar"')
|
370
|
+
expect(result).to eql('employeeId eq "gsar" or imaginary.path eq "gsar"')
|
371
|
+
end
|
372
|
+
|
373
|
+
it 'handles the square bracket form with schema ID at the root' do
|
374
|
+
result = @instance.send(:flatten_filter, 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User[employeeId eq "gsar"')
|
375
|
+
expect(result).to eql('employeeId eq "gsar"')
|
376
|
+
end
|
377
|
+
|
378
|
+
it 'handles the square bracket form with schema ID and attribute at the root' do
|
379
|
+
result = @instance.send(:flatten_filter, 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:imaginary[path eq "gsar"')
|
380
|
+
expect(result).to eql('imaginary.path eq "gsar"')
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|
384
|
+
# https://github.com/RIPAGlobal/scimitar/issues/115
|
385
|
+
#
|
386
|
+
context 'broken filters from Microsoft (GitHub issue #115)' do
|
387
|
+
it 'work with "eq"' do
|
388
|
+
result = @instance.send(:flatten_filter, 'emails[type eq "work"].value eq "foo@bar.com"')
|
389
|
+
expect(result).to eql('emails.type eq "work" and emails.value eq "foo@bar.com"')
|
390
|
+
end
|
391
|
+
|
392
|
+
it 'work with "ne"' do # (just check a couple of operators, not all!)
|
393
|
+
result = @instance.send(:flatten_filter, 'emails[type eq "work"].value ne "foo@bar.com"')
|
394
|
+
expect(result).to eql('emails.type eq "work" and emails.value ne "foo@bar.com"')
|
395
|
+
end
|
396
|
+
|
397
|
+
it 'preserve input case' do
|
398
|
+
result = @instance.send(:flatten_filter, 'emaiLs[TYPE eq "work"].valUE eq "FOO@bar.com"')
|
399
|
+
expect(result).to eql('emaiLs.TYPE eq "work" and emaiLs.valUE eq "FOO@bar.com"')
|
400
|
+
end
|
401
|
+
|
402
|
+
# At the time of writing, this was used in a "belt and braces" request
|
403
|
+
# spec in 'active_record_backed_resources_controller_spec.rb'.
|
404
|
+
#
|
405
|
+
it 'handles more complex, hypothetical cases' do
|
406
|
+
result = @instance.send(:flatten_filter, 'name[givenName eq "FOO"].familyName pr and emails ne "home_1@test.com"')
|
407
|
+
expect(result).to eql('name.givenName eq "FOO" and name.familyName pr and emails ne "home_1@test.com"')
|
408
|
+
end
|
409
|
+
end # "context 'broken filters from Microsoft' do"
|
340
410
|
end # "context 'when flattening is needed' do"
|
341
411
|
|
342
412
|
context 'with bad filters' do
|
@@ -4,7 +4,7 @@ RSpec.describe Scimitar::Resources::Base do
|
|
4
4
|
context 'basic operation' do
|
5
5
|
FirstCustomSchema = Class.new(Scimitar::Schema::Base) do
|
6
6
|
def self.id
|
7
|
-
'custom-id'
|
7
|
+
'urn:ietf:params:scim:schemas:custom-id'
|
8
8
|
end
|
9
9
|
|
10
10
|
def self.scim_attributes
|
@@ -16,7 +16,7 @@ RSpec.describe Scimitar::Resources::Base do
|
|
16
16
|
name: 'names', multiValued: true, complexType: Scimitar::ComplexTypes::Name, required: false
|
17
17
|
),
|
18
18
|
Scimitar::Schema::Attribute.new(
|
19
|
-
name: 'privateName', complexType: Scimitar::ComplexTypes::Name, required: false, returned:
|
19
|
+
name: 'privateName', complexType: Scimitar::ComplexTypes::Name, required: false, returned: 'never'
|
20
20
|
),
|
21
21
|
]
|
22
22
|
end
|
@@ -27,6 +27,14 @@ RSpec.describe Scimitar::Resources::Base do
|
|
27
27
|
end
|
28
28
|
|
29
29
|
context '#initialize' do
|
30
|
+
it 'accepts nil for non-required attributes' do
|
31
|
+
resource = CustomResourse.new(name: nil, names: nil, privateName: nil)
|
32
|
+
|
33
|
+
expect(resource.name).to be_nil
|
34
|
+
expect(resource.names).to be_nil
|
35
|
+
expect(resource.privateName).to be_nil
|
36
|
+
end
|
37
|
+
|
30
38
|
shared_examples 'an initializer' do | force_upper_case: |
|
31
39
|
it 'which builds the nested type' do
|
32
40
|
attributes = {
|
@@ -117,7 +125,7 @@ RSpec.describe Scimitar::Resources::Base do
|
|
117
125
|
|
118
126
|
result = resource.as_json
|
119
127
|
|
120
|
-
expect(result['schemas'] ).to eql(['custom-id'])
|
128
|
+
expect(result['schemas'] ).to eql(['urn:ietf:params:scim:schemas:custom-id'])
|
121
129
|
expect(result['meta']['resourceType']).to eql('CustomResourse')
|
122
130
|
expect(result['errors'] ).to be_nil
|
123
131
|
end
|
@@ -136,7 +144,7 @@ RSpec.describe Scimitar::Resources::Base do
|
|
136
144
|
|
137
145
|
result = resource.as_json
|
138
146
|
|
139
|
-
expect(result['schemas'] ).to eql(['custom-id'])
|
147
|
+
expect(result['schemas'] ).to eql(['urn:ietf:params:scim:schemas:custom-id'])
|
140
148
|
expect(result['meta']['resourceType']).to eql('CustomResourse')
|
141
149
|
expect(result['errors'] ).to be_nil
|
142
150
|
expect(result['name'] ).to be_present
|
@@ -287,7 +295,7 @@ RSpec.describe Scimitar::Resources::Base do
|
|
287
295
|
context 'of custom schema' do
|
288
296
|
ThirdCustomSchema = Class.new(Scimitar::Schema::Base) do
|
289
297
|
def self.id
|
290
|
-
'custom-id'
|
298
|
+
'urn:ietf:params:scim:schemas:custom-id'
|
291
299
|
end
|
292
300
|
|
293
301
|
def self.scim_attributes
|
@@ -297,7 +305,7 @@ RSpec.describe Scimitar::Resources::Base do
|
|
297
305
|
|
298
306
|
ExtensionSchema = Class.new(Scimitar::Schema::Base) do
|
299
307
|
def self.id
|
300
|
-
'extension
|
308
|
+
'urn:ietf:params:scim:schemas:extension'
|
301
309
|
end
|
302
310
|
|
303
311
|
def self.scim_attributes
|
@@ -325,13 +333,13 @@ RSpec.describe Scimitar::Resources::Base do
|
|
325
333
|
|
326
334
|
context '#initialize' do
|
327
335
|
it 'allows setting extension attributes' do
|
328
|
-
resource = resource_class.new('extension
|
336
|
+
resource = resource_class.new('urn:ietf:params:scim:schemas:extension' => {relationship: 'GAGA'})
|
329
337
|
expect(resource.relationship).to eql('GAGA')
|
330
338
|
end
|
331
339
|
|
332
340
|
it 'allows setting complex extension attributes' do
|
333
341
|
user_groups = [{ value: '123' }, { value: '456'}]
|
334
|
-
resource = resource_class.new('extension
|
342
|
+
resource = resource_class.new('urn:ietf:params:scim:schemas:extension' => {userGroups: user_groups})
|
335
343
|
expect(resource.userGroups.map(&:value)).to eql(['123', '456'])
|
336
344
|
end
|
337
345
|
end # "context '#initialize' do"
|
@@ -340,8 +348,8 @@ RSpec.describe Scimitar::Resources::Base do
|
|
340
348
|
it 'namespaces the extension attributes' do
|
341
349
|
resource = resource_class.new(relationship: 'GAGA')
|
342
350
|
hash = resource.as_json
|
343
|
-
expect(hash["schemas"]).to eql(['custom-id', 'extension
|
344
|
-
expect(hash["extension
|
351
|
+
expect(hash["schemas"]).to eql(['urn:ietf:params:scim:schemas:custom-id', 'urn:ietf:params:scim:schemas:extension'])
|
352
|
+
expect(hash["urn:ietf:params:scim:schemas:extension"]).to eql("relationship" => 'GAGA')
|
345
353
|
end
|
346
354
|
end # "context '#as_json' do"
|
347
355
|
|
@@ -354,10 +362,10 @@ RSpec.describe Scimitar::Resources::Base do
|
|
354
362
|
|
355
363
|
context 'validation' do
|
356
364
|
it 'validates into custom schema' do
|
357
|
-
resource = resource_class.new('extension
|
365
|
+
resource = resource_class.new('urn:ietf:params:scim:schemas:extension' => {})
|
358
366
|
expect(resource.valid?).to eql(false)
|
359
367
|
|
360
|
-
resource = resource_class.new('extension
|
368
|
+
resource = resource_class.new('urn:ietf:params:scim:schemas:extension' => {relationship: 'GAGA'})
|
361
369
|
expect(resource.relationship).to eql('GAGA')
|
362
370
|
expect(resource.valid?).to eql(true)
|
363
371
|
end
|
@@ -4,7 +4,20 @@ RSpec.describe Scimitar::Resources::Base do
|
|
4
4
|
context '#valid?' do
|
5
5
|
MyCustomSchema = Class.new(Scimitar::Schema::Base) do
|
6
6
|
def self.id
|
7
|
-
'custom-id'
|
7
|
+
'urn:ietf:params:scim:schemas:custom-id'
|
8
|
+
end
|
9
|
+
|
10
|
+
class NameWithRequirementsSchema < Scimitar::Schema::Base
|
11
|
+
def self.scim_attributes
|
12
|
+
@scim_attributes ||= [
|
13
|
+
Scimitar::Schema::Attribute.new(name: 'familyName', type: 'string', required: true),
|
14
|
+
Scimitar::Schema::Attribute.new(name: 'givenName', type: 'string', required: true),
|
15
|
+
]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class NameWithRequirementsComplexType < Scimitar::ComplexTypes::Base
|
20
|
+
set_schema NameWithRequirementsSchema
|
8
21
|
end
|
9
22
|
|
10
23
|
def self.scim_attributes
|
@@ -16,10 +29,10 @@ RSpec.describe Scimitar::Resources::Base do
|
|
16
29
|
name: 'enforce', type: 'boolean', required: true
|
17
30
|
),
|
18
31
|
Scimitar::Schema::Attribute.new(
|
19
|
-
name: 'complexName', complexType:
|
32
|
+
name: 'complexName', complexType: NameWithRequirementsComplexType, required: false
|
20
33
|
),
|
21
34
|
Scimitar::Schema::Attribute.new(
|
22
|
-
name: 'complexNames', complexType: Scimitar::ComplexTypes::Name, multiValued:true, required: false
|
35
|
+
name: 'complexNames', complexType: Scimitar::ComplexTypes::Name, multiValued: true, required: false
|
23
36
|
),
|
24
37
|
Scimitar::Schema::Attribute.new(
|
25
38
|
name: 'vdtpTestByEmail', complexType: Scimitar::ComplexTypes::Email, required: false
|