scimitar 2.7.3 → 2.9.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.
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