scimitar 2.7.3 → 2.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +27 -18
  3. data/app/controllers/scimitar/active_record_backed_resources_controller.rb +5 -4
  4. data/app/controllers/scimitar/resource_types_controller.rb +0 -2
  5. data/app/controllers/scimitar/resources_controller.rb +0 -2
  6. data/app/controllers/scimitar/schemas_controller.rb +361 -3
  7. data/app/controllers/scimitar/service_provider_configurations_controller.rb +0 -1
  8. data/app/models/scimitar/engine_configuration.rb +3 -1
  9. data/app/models/scimitar/lists/query_parser.rb +88 -3
  10. data/app/models/scimitar/resources/base.rb +36 -5
  11. data/app/models/scimitar/resources/mixin.rb +87 -20
  12. data/app/models/scimitar/schema/name.rb +2 -2
  13. data/app/models/scimitar/schema/user.rb +10 -10
  14. data/config/initializers/scimitar.rb +41 -0
  15. data/lib/scimitar/engine.rb +57 -12
  16. data/lib/scimitar/support/utilities.rb +8 -3
  17. data/lib/scimitar/version.rb +2 -2
  18. data/spec/apps/dummy/app/models/mock_user.rb +18 -3
  19. data/spec/apps/dummy/config/initializers/scimitar.rb +31 -2
  20. data/spec/apps/dummy/db/migrate/20210304014602_create_mock_users.rb +1 -0
  21. data/spec/apps/dummy/db/schema.rb +1 -0
  22. data/spec/controllers/scimitar/schemas_controller_spec.rb +342 -54
  23. data/spec/models/scimitar/lists/query_parser_spec.rb +70 -0
  24. data/spec/models/scimitar/resources/base_spec.rb +11 -11
  25. data/spec/models/scimitar/resources/base_validation_spec.rb +16 -3
  26. data/spec/models/scimitar/resources/mixin_spec.rb +71 -10
  27. data/spec/models/scimitar/schema/user_spec.rb +2 -2
  28. data/spec/requests/active_record_backed_resources_controller_spec.rb +132 -1
  29. data/spec/requests/engine_spec.rb +75 -0
  30. data/spec/spec_helper.rb +1 -1
  31. metadata +20 -20
@@ -18,6 +18,7 @@ class MockUser < ActiveRecord::Base
18
18
  work_phone_number
19
19
  organization
20
20
  department
21
+ manager
21
22
  mock_groups
22
23
  }
23
24
 
@@ -48,6 +49,7 @@ class MockUser < ActiveRecord::Base
48
49
  externalId: :scim_uid,
49
50
  userName: :username,
50
51
  password: :password,
52
+ active: :is_active,
51
53
  name: {
52
54
  givenName: :first_name,
53
55
  familyName: :last_name
@@ -80,8 +82,11 @@ class MockUser < ActiveRecord::Base
80
82
  }
81
83
  },
82
84
  ],
83
- groups: [ # NB read-only, so no :find_with key
85
+ groups: [
84
86
  {
87
+ # Read-only, so no :find_with key. There's no 'class' specified here
88
+ # either, to help test the "/Schemas" endpoint's reflection code.
89
+ #
85
90
  list: :mock_groups,
86
91
  using: {
87
92
  value: :id,
@@ -89,13 +94,16 @@ class MockUser < ActiveRecord::Base
89
94
  }
90
95
  }
91
96
  ],
92
- active: :is_active,
93
97
 
94
98
  # Custom extension schema - see configuration in
95
99
  # "spec/apps/dummy/config/initializers/scimitar.rb".
96
100
  #
97
101
  organization: :organization,
98
102
  department: :department,
103
+ primaryEmail: :scim_primary_email,
104
+
105
+ manager: :manager,
106
+
99
107
  userGroups: [
100
108
  {
101
109
  list: :mock_groups,
@@ -124,9 +132,16 @@ class MockUser < ActiveRecord::Base
124
132
  'groups.value' => { column: MockGroup.arel_table[:id] },
125
133
  'emails' => { columns: [ :work_email_address, :home_email_address ] },
126
134
  'emails.value' => { columns: [ :work_email_address, :home_email_address ] },
127
- 'emails.type' => { ignore: true } # We can't filter on that; it'll just search all e-mails
135
+ 'emails.type' => { ignore: true }, # We can't filter on that; it'll just search all e-mails
136
+ 'primaryEmail' => { column: :scim_primary_email },
128
137
  }
129
138
  end
130
139
 
140
+ # Custom attribute reader
141
+ #
142
+ def scim_primary_email
143
+ work_email_address
144
+ end
145
+
131
146
  include Scimitar::Resources::Mixin
132
147
  end
@@ -33,10 +33,13 @@ Rails.application.config.to_prepare do
33
33
 
34
34
  module ScimSchemaExtensions
35
35
  module User
36
+
37
+ # This "looks like" part of the standard Enterprise extension.
38
+ #
36
39
  class Enterprise < Scimitar::Schema::Base
37
40
  def initialize(options = {})
38
41
  super(
39
- name: 'ExtendedUser',
42
+ name: 'EnterpriseExtendedUser',
40
43
  description: 'Enterprise extension for a User',
41
44
  id: self.class.id,
42
45
  scim_attributes: self.class.scim_attributes
@@ -50,7 +53,32 @@ Rails.application.config.to_prepare do
50
53
  def self.scim_attributes
51
54
  [
52
55
  Scimitar::Schema::Attribute.new(name: 'organization', type: 'string'),
53
- Scimitar::Schema::Attribute.new(name: 'department', type: 'string')
56
+ Scimitar::Schema::Attribute.new(name: 'department', type: 'string'),
57
+ Scimitar::Schema::Attribute.new(name: 'primaryEmail', type: 'string'),
58
+ ]
59
+ end
60
+ end
61
+
62
+ # In https://github.com/RIPAGlobal/scimitar/issues/122 we learn that with
63
+ # more than one extension, things can go wrong - so now we test with two.
64
+ #
65
+ class Manager < Scimitar::Schema::Base
66
+ def initialize(options = {})
67
+ super(
68
+ name: 'ManagementExtendedUser',
69
+ description: 'Management extension for a User',
70
+ id: self.class.id,
71
+ scim_attributes: self.class.scim_attributes
72
+ )
73
+ end
74
+
75
+ def self.id
76
+ 'urn:ietf:params:scim:schemas:extension:manager:1.0:User'
77
+ end
78
+
79
+ def self.scim_attributes
80
+ [
81
+ Scimitar::Schema::Attribute.new(name: 'manager', type: 'string')
54
82
  ]
55
83
  end
56
84
  end
@@ -58,4 +86,5 @@ Rails.application.config.to_prepare do
58
86
  end
59
87
 
60
88
  Scimitar::Resources::User.extend_schema ScimSchemaExtensions::User::Enterprise
89
+ Scimitar::Resources::User.extend_schema ScimSchemaExtensions::User::Manager
61
90
  end
@@ -19,6 +19,7 @@ class CreateMockUsers < ActiveRecord::Migration[6.1]
19
19
  #
20
20
  t.text :organization
21
21
  t.text :department
22
+ t.text :manager
22
23
  end
23
24
  end
24
25
  end
@@ -41,6 +41,7 @@ ActiveRecord::Schema[7.1].define(version: 2021_03_08_044214) do
41
41
  t.text "work_phone_number"
42
42
  t.text "organization"
43
43
  t.text "department"
44
+ t.text "manager"
44
45
  end
45
46
 
46
47
  add_foreign_key "mock_groups_users", "mock_groups"
@@ -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
- it 'returns a collection of supported schemas' do
15
- get :index, params: { format: :scim }
16
- expect(response).to be_ok
17
- parsed_body = JSON.parse(response.body)
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
- it 'returns only the User schema when its id is provided' do
24
- get :index, params: { name: Scimitar::Schema::User.id, format: :scim }
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
- it 'includes the controller customised schema location' do
31
- get :index, params: { name: Scimitar::Schema::User.id, format: :scim }
32
- expect(response).to be_ok
33
- parsed_body = JSON.parse(response.body)
34
- expect(parsed_body.dig('meta', 'location')).to eq scim_schemas_url(name: Scimitar::Schema::User.id, test: 1)
35
- end
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
- it 'returns only the Group schema when its id is provided' do
38
- get :index, params: { name: Scimitar::Schema::Group.id, format: :scim }
39
- expect(response).to be_ok
40
- parsed_body = JSON.parse(response.body)
41
- expect(parsed_body['name']).to eql('Group')
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 custom resource types' do
45
- around :each do | example |
46
- example.run()
47
- ensure
48
- Scimitar::Engine.reset_custom_resources
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
- it 'returns only the License schemas when its id is provided' do
52
- license_schema = Class.new(Scimitar::Schema::Base) do
53
- def initialize(options = {})
54
- super(name: 'License',
55
- id: self.class.id,
56
- description: 'Represents a License')
57
- end
58
- def self.id
59
- 'License'
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
- def self.scim_attributes
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
- license_resource = Class.new(Scimitar::Resources::Base) do
67
- set_schema license_schema
68
- def self.endopint
69
- '/Gaga'
70
- end
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
- Scimitar::Engine.add_custom_resource(license_resource)
136
+ it_behaves_like 'a Schema list which'
74
137
 
75
- get :index, params: { name: license_schema.id, format: :scim }
76
- expect(response).to be_ok
77
- parsed_body = JSON.parse(response.body)
78
- expect(parsed_body['name']).to eql('License')
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