scimitar 1.0.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 (106) hide show
  1. checksums.yaml +7 -0
  2. data/Rakefile +16 -0
  3. data/app/controllers/scimitar/active_record_backed_resources_controller.rb +180 -0
  4. data/app/controllers/scimitar/application_controller.rb +129 -0
  5. data/app/controllers/scimitar/resource_types_controller.rb +28 -0
  6. data/app/controllers/scimitar/resources_controller.rb +203 -0
  7. data/app/controllers/scimitar/schemas_controller.rb +16 -0
  8. data/app/controllers/scimitar/service_provider_configurations_controller.rb +8 -0
  9. data/app/models/scimitar/authentication_error.rb +9 -0
  10. data/app/models/scimitar/authentication_scheme.rb +18 -0
  11. data/app/models/scimitar/bulk.rb +8 -0
  12. data/app/models/scimitar/complex_types/address.rb +18 -0
  13. data/app/models/scimitar/complex_types/base.rb +41 -0
  14. data/app/models/scimitar/complex_types/email.rb +12 -0
  15. data/app/models/scimitar/complex_types/entitlement.rb +12 -0
  16. data/app/models/scimitar/complex_types/ims.rb +12 -0
  17. data/app/models/scimitar/complex_types/name.rb +12 -0
  18. data/app/models/scimitar/complex_types/phone_number.rb +12 -0
  19. data/app/models/scimitar/complex_types/photo.rb +12 -0
  20. data/app/models/scimitar/complex_types/reference_group.rb +12 -0
  21. data/app/models/scimitar/complex_types/reference_member.rb +12 -0
  22. data/app/models/scimitar/complex_types/role.rb +12 -0
  23. data/app/models/scimitar/complex_types/x509_certificate.rb +12 -0
  24. data/app/models/scimitar/engine_configuration.rb +24 -0
  25. data/app/models/scimitar/error_response.rb +20 -0
  26. data/app/models/scimitar/errors.rb +14 -0
  27. data/app/models/scimitar/filter.rb +11 -0
  28. data/app/models/scimitar/filter_error.rb +22 -0
  29. data/app/models/scimitar/invalid_syntax_error.rb +9 -0
  30. data/app/models/scimitar/lists/count.rb +64 -0
  31. data/app/models/scimitar/lists/query_parser.rb +730 -0
  32. data/app/models/scimitar/meta.rb +7 -0
  33. data/app/models/scimitar/not_found_error.rb +10 -0
  34. data/app/models/scimitar/resource_invalid_error.rb +9 -0
  35. data/app/models/scimitar/resource_type.rb +29 -0
  36. data/app/models/scimitar/resources/base.rb +159 -0
  37. data/app/models/scimitar/resources/group.rb +13 -0
  38. data/app/models/scimitar/resources/mixin.rb +964 -0
  39. data/app/models/scimitar/resources/user.rb +13 -0
  40. data/app/models/scimitar/schema/address.rb +24 -0
  41. data/app/models/scimitar/schema/attribute.rb +123 -0
  42. data/app/models/scimitar/schema/base.rb +86 -0
  43. data/app/models/scimitar/schema/derived_attributes.rb +24 -0
  44. data/app/models/scimitar/schema/email.rb +10 -0
  45. data/app/models/scimitar/schema/entitlement.rb +10 -0
  46. data/app/models/scimitar/schema/group.rb +27 -0
  47. data/app/models/scimitar/schema/ims.rb +10 -0
  48. data/app/models/scimitar/schema/name.rb +20 -0
  49. data/app/models/scimitar/schema/phone_number.rb +10 -0
  50. data/app/models/scimitar/schema/photo.rb +10 -0
  51. data/app/models/scimitar/schema/reference_group.rb +23 -0
  52. data/app/models/scimitar/schema/reference_member.rb +21 -0
  53. data/app/models/scimitar/schema/role.rb +10 -0
  54. data/app/models/scimitar/schema/user.rb +52 -0
  55. data/app/models/scimitar/schema/vdtp.rb +18 -0
  56. data/app/models/scimitar/schema/x509_certificate.rb +22 -0
  57. data/app/models/scimitar/service_provider_configuration.rb +49 -0
  58. data/app/models/scimitar/supportable.rb +14 -0
  59. data/app/views/layouts/scimitar/application.html.erb +14 -0
  60. data/config/initializers/scimitar.rb +82 -0
  61. data/config/routes.rb +6 -0
  62. data/lib/scimitar.rb +23 -0
  63. data/lib/scimitar/engine.rb +63 -0
  64. data/lib/scimitar/version.rb +13 -0
  65. data/spec/apps/dummy/app/controllers/custom_destroy_mock_users_controller.rb +24 -0
  66. data/spec/apps/dummy/app/controllers/custom_request_verifiers_controller.rb +30 -0
  67. data/spec/apps/dummy/app/controllers/mock_groups_controller.rb +13 -0
  68. data/spec/apps/dummy/app/controllers/mock_users_controller.rb +13 -0
  69. data/spec/apps/dummy/app/models/mock_group.rb +83 -0
  70. data/spec/apps/dummy/app/models/mock_user.rb +104 -0
  71. data/spec/apps/dummy/config/application.rb +17 -0
  72. data/spec/apps/dummy/config/boot.rb +2 -0
  73. data/spec/apps/dummy/config/environment.rb +2 -0
  74. data/spec/apps/dummy/config/environments/test.rb +15 -0
  75. data/spec/apps/dummy/config/initializers/cookies_serializer.rb +3 -0
  76. data/spec/apps/dummy/config/initializers/scimitar.rb +14 -0
  77. data/spec/apps/dummy/config/initializers/session_store.rb +3 -0
  78. data/spec/apps/dummy/config/routes.rb +24 -0
  79. data/spec/apps/dummy/db/migrate/20210304014602_create_mock_users.rb +15 -0
  80. data/spec/apps/dummy/db/migrate/20210308020313_create_mock_groups.rb +10 -0
  81. data/spec/apps/dummy/db/migrate/20210308044214_create_join_table_mock_groups_mock_users.rb +8 -0
  82. data/spec/apps/dummy/db/schema.rb +42 -0
  83. data/spec/controllers/scimitar/application_controller_spec.rb +173 -0
  84. data/spec/controllers/scimitar/resource_types_controller_spec.rb +94 -0
  85. data/spec/controllers/scimitar/resources_controller_spec.rb +247 -0
  86. data/spec/controllers/scimitar/schemas_controller_spec.rb +75 -0
  87. data/spec/controllers/scimitar/service_provider_configurations_controller_spec.rb +22 -0
  88. data/spec/models/scimitar/complex_types/address_spec.rb +19 -0
  89. data/spec/models/scimitar/complex_types/email_spec.rb +23 -0
  90. data/spec/models/scimitar/lists/count_spec.rb +147 -0
  91. data/spec/models/scimitar/lists/query_parser_spec.rb +763 -0
  92. data/spec/models/scimitar/resource_type_spec.rb +21 -0
  93. data/spec/models/scimitar/resources/base_spec.rb +289 -0
  94. data/spec/models/scimitar/resources/base_validation_spec.rb +61 -0
  95. data/spec/models/scimitar/resources/mixin_spec.rb +2127 -0
  96. data/spec/models/scimitar/resources/user_spec.rb +55 -0
  97. data/spec/models/scimitar/schema/attribute_spec.rb +80 -0
  98. data/spec/models/scimitar/schema/base_spec.rb +64 -0
  99. data/spec/models/scimitar/schema/group_spec.rb +87 -0
  100. data/spec/models/scimitar/schema/user_spec.rb +710 -0
  101. data/spec/requests/active_record_backed_resources_controller_spec.rb +569 -0
  102. data/spec/requests/application_controller_spec.rb +49 -0
  103. data/spec/requests/controller_configuration_spec.rb +17 -0
  104. data/spec/requests/engine_spec.rb +20 -0
  105. data/spec/spec_helper.rb +66 -0
  106. metadata +315 -0
@@ -0,0 +1,94 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Scimitar::ResourceTypesController do
4
+ routes { Scimitar::Engine.routes }
5
+
6
+ before(:each) { allow(controller).to receive(:authenticated?).and_return(true) }
7
+
8
+ context 'GET index' do
9
+ it 'renders the resource type for user' do
10
+ get :index, format: :scim
11
+ response_hash = JSON.parse(response.body)
12
+ expected_response = [ Scimitar::Resources::User.resource_type(scim_resource_type_url(name: 'User')),
13
+ Scimitar::Resources::Group.resource_type(scim_resource_type_url(name: 'Group'))
14
+ ].to_json
15
+
16
+ response_hash = JSON.parse(response.body)
17
+ expect(response_hash).to eql(JSON.parse(expected_response))
18
+ end
19
+
20
+ context 'with custom resource types' do
21
+ around :each do | example |
22
+ example.run()
23
+ ensure
24
+ Scimitar::Engine.reset_custom_resources
25
+ end
26
+
27
+ it 'renders them' do
28
+ custom_resource = Class.new(Scimitar::Resources::Base) do
29
+ set_schema Scimitar::Schema::User
30
+
31
+ def self.endpoint
32
+ "/Gaga"
33
+ end
34
+
35
+ def self.resource_type_id
36
+ 'Gaga'
37
+ end
38
+ end
39
+
40
+ Scimitar::Engine.add_custom_resource(custom_resource)
41
+
42
+ get :index, params: { format: :scim }
43
+ response_hash = JSON.parse(response.body)
44
+ expect(response_hash.size).to eql(3)
45
+ end
46
+ end
47
+ end
48
+
49
+ context 'GET show' do
50
+ it 'renders the resource type for user' do
51
+ get :show, params: { name: 'User', format: :scim }
52
+ response_hash = JSON.parse(response.body)
53
+ expected_response = Scimitar::Resources::User.resource_type(scim_resource_type_url(name: 'User')).to_json
54
+ expect(response_hash).to eql(JSON.parse(expected_response))
55
+ end
56
+
57
+ it 'renders the resource type for group' do
58
+ get :show, params: { name: 'Group', format: :scim }
59
+ response_hash = JSON.parse(response.body)
60
+ expected_response = Scimitar::Resources::Group.resource_type(scim_resource_type_url(name: 'Group')).to_json
61
+ expect(response_hash).to eql(JSON.parse(expected_response))
62
+ end
63
+
64
+ it 'renders custom resource type' do
65
+ custom_resource = Class.new(Scimitar::Resources::Base) do
66
+ set_schema Scimitar::Schema::User
67
+
68
+ def self.endpoint
69
+ "/Gaga"
70
+ end
71
+
72
+ def self.resource_type_id
73
+ 'Gaga'
74
+ end
75
+ end
76
+
77
+ allow(Scimitar::Engine).to receive(:custom_resources) {[ custom_resource ]}
78
+
79
+ get :show, params: { name: 'Gaga', format: :scim }
80
+ response_hash = JSON.parse(response.body)
81
+ expected_response = custom_resource.resource_type(scim_resource_type_url(name: 'Gaga')).to_json
82
+ expect(response_hash).to eql(JSON.parse(expected_response))
83
+ end
84
+
85
+ it 'renders 404 if not recognised' do
86
+ get :show, params: { name: 'Foo', format: :scim }
87
+ expect(response).to have_http_status(:not_found)
88
+ response_hash = JSON.parse(response.body)
89
+ expect(response_hash['schemas']).to eql(['urn:ietf:params:scim:api:messages:2.0:Error'])
90
+ expect(response_hash['status' ]).to eql('404')
91
+ expect(response_hash['detail' ]).to eql('Resource "Foo" not found')
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,247 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Scimitar::ResourcesController do
4
+ class FakeGroup
5
+ include ActiveModel::Model
6
+
7
+ attr_accessor :scim_id
8
+ attr_accessor :display_name
9
+ attr_accessor :member_names
10
+
11
+ def self.scim_resource_type
12
+ return Scimitar::Resources::Group
13
+ end
14
+
15
+ def self.scim_attributes_map
16
+ return {
17
+ id: :id,
18
+ externalId: :scim_id,
19
+ displayName: :display_name,
20
+ members: :member_names
21
+ }
22
+ end
23
+
24
+ def self.scim_mutable_attributes
25
+ return nil
26
+ end
27
+
28
+ def self.scim_queryable_attributes
29
+ return { displayName: display_name }
30
+ end
31
+
32
+ include Scimitar::Resources::Mixin
33
+ end
34
+
35
+ let(:parsed_response) { JSON.parse(response.body, symbolize_names: true) }
36
+
37
+ before(:each) do
38
+ allow(controller()).to receive(:authenticated?).and_return(true)
39
+ allow(FakeGroup ).to receive(:all).and_return(double('ActiveRecord::Relation', where: []))
40
+ end
41
+
42
+ controller do
43
+ def index
44
+ super(FakeGroup.all) do | fake_group |
45
+ fake_group
46
+ end
47
+ end
48
+
49
+ def show
50
+ super do |id|
51
+ Scimitar::Resources::Group.new(id: id)
52
+ end
53
+ end
54
+
55
+ def create
56
+ super do |resource|
57
+ resource
58
+ end
59
+ end
60
+
61
+ def replace
62
+ super do |record_id, resource|
63
+ resource
64
+ end
65
+ end
66
+
67
+ # PATCH is more easily (and comprehensively) tested via
68
+ # spec/requests/active_record_backed_resources_controller_spec.rb.
69
+ #
70
+ def update # PATCH
71
+ raise NotImplementedError
72
+ end
73
+
74
+ def destroy
75
+ super do |id|
76
+ successful_delete?
77
+ end
78
+ end
79
+
80
+ def successful_delete? # Just a test hook
81
+ true
82
+ end
83
+
84
+ protected
85
+
86
+ def storage_class
87
+ FakeGroup
88
+ end
89
+ end
90
+
91
+ context 'GET show' do
92
+ it 'renders the resource' do
93
+ get :show, params: { id: '10', format: :scim }
94
+ expect(response.status).to eql(200)
95
+ expect(parsed_response()).to include(id: '10')
96
+ end
97
+ end
98
+
99
+ context 'POST create' do
100
+ it 'returns error if body is missing' do
101
+ post :create, params: { format: :scim }
102
+ expect(response.status).to eql(400)
103
+ expect(parsed_response()[:detail]).to eql('must provide a request body')
104
+ end
105
+
106
+ it 'works if the request is valid' do
107
+ post :create, params: { displayName: 'Sauron biz', format: :scim }
108
+ expect(response).to have_http_status(:created)
109
+ expect(parsed_response()[:displayName]).to eql('Sauron biz')
110
+ end
111
+
112
+ it 'renders error if resource object cannot be built from the params' do
113
+ @routes.draw do
114
+ put 'scimitar/resources/:id', action: 'replace', controller: 'scimitar/resources'
115
+ end
116
+ put :replace, params: { id: 'group-id', name: {email: 'a@b.com'}, format: :scim }
117
+
118
+ expect(response.status).to eql(400)
119
+ expect(parsed_response()[:detail]).to match(/^Invalid/)
120
+ end
121
+
122
+ it 'renders application side error' do
123
+ expect_any_instance_of(Scimitar::Resources::Group).to receive(:to_json).and_raise(Scimitar::ErrorResponse.new(status: 400, detail: 'gaga'))
124
+
125
+ @routes.draw do
126
+ put 'scimitar/resources/:id', action: 'replace', controller: 'scimitar/resources'
127
+ end
128
+ put :replace, params: { id: 'group-id', displayName: 'invalid name', format: :scim }
129
+
130
+ expect(response.status).to eql(400)
131
+ expect(parsed_response()[:detail]).to eql('gaga')
132
+ end
133
+
134
+ it 'renders externalId if provided' do
135
+ post :create, params: { externalId: 'some-id', displayName: 'sauron', format: :scim }
136
+
137
+ expect(response).to have_http_status(:created)
138
+
139
+ expect(parsed_response()[:displayName]).to eql('sauron')
140
+ expect(parsed_response()[:externalId]).to eql('some-id')
141
+ end
142
+
143
+ it 'maps internal NoMethodError failures to "Invalid request"' do
144
+ expect(controller()).to receive(:validate_request) { raise NoMethodError.new }
145
+
146
+ post :create, params: { externalId: 'some-id', displayName: 'sauron', format: :scim }
147
+
148
+ expect(response.status).to eql(400)
149
+ expect(parsed_response()[:detail]).to eql('Invalid request')
150
+ end
151
+ end
152
+
153
+ context 'PUT update' do
154
+ it 'returns error if body is missing' do
155
+ @routes.draw do
156
+ put 'scimitar/resources/:id', action: 'replace', controller: 'scimitar/resources'
157
+ end
158
+ put :replace, params: { id: 'group-id', format: :scim }
159
+
160
+ expect(response.status).to eql(400)
161
+ expect(parsed_response()[:detail]).to eql('must provide a request body')
162
+ end
163
+
164
+ it 'works if the request is valid' do
165
+ @routes.draw do
166
+ put 'scimitar/resources/:id', action: 'replace', controller: 'scimitar/resources'
167
+ end
168
+ put :replace, params: { id: 'group-id', displayName: 'sauron', format: :scim }
169
+
170
+ expect(response.status).to eql(200)
171
+ expect(parsed_response()[:displayName]).to eql('sauron')
172
+ end
173
+
174
+ it 'renders error if resource object cannot be built from the params' do
175
+ @routes.draw do
176
+ put 'scimitar/resources/:id', action: 'replace', controller: 'scimitar/resources'
177
+ end
178
+ put :replace, params: { id: 'group-id', name: {email: 'a@b.com'}, format: :scim }
179
+
180
+ expect(response.status).to eql(400)
181
+ expect(parsed_response()[:detail]).to match(/^Invalid/)
182
+ end
183
+
184
+ it 'renders application side error' do
185
+ allow_any_instance_of(Scimitar::Resources::Group).to receive(:to_json).and_raise(Scimitar::ErrorResponse.new(status: 400, detail: 'gaga'))
186
+
187
+ @routes.draw do
188
+ put 'scimitar/resources/:id', action: 'replace', controller: 'scimitar/resources'
189
+ end
190
+ put :replace, params: { id: 'group-id', displayName: 'invalid name', format: :scim }
191
+
192
+ expect(response.status).to eql(400)
193
+ expect(parsed_response()[:detail]).to eql('gaga')
194
+ end
195
+
196
+ end
197
+
198
+ context 'DELETE destroy' do
199
+ it 'returns an empty response with no content status if deletion is successful' do
200
+ delete :destroy, params: { id: 'group-id', format: :scim }
201
+ expect(response).to have_http_status(:no_content)
202
+ expect(response.body).to be_empty
203
+ end
204
+
205
+ it 'renders error if deletion fails' do
206
+ allow(controller()).to receive(:successful_delete?).and_return(false)
207
+ delete :destroy, params: { id: 'group-id', format: :scim }
208
+ expect(response).to have_http_status(:internal_server_error)
209
+ expect(parsed_response()[:detail]).to eql("Failed to delete the resource with id 'group-id'. Please try again later.")
210
+ end
211
+ end
212
+
213
+ context 'service methods' do
214
+ context '#scim_pagination_info' do
215
+ it 'applies defaults' do
216
+ result = controller().send(:scim_pagination_info)
217
+
218
+ expect(result.limit).to eql(Scimitar.service_provider_configuration(location: nil).filter.maxResults)
219
+ expect(result.offset).to eql(0)
220
+ expect(result.start_index).to eql(1)
221
+ expect(result.total).to be_nil
222
+ end
223
+
224
+ it 'reads parameters' do
225
+ allow(controller()).to receive(:params).and_return({count: '10', startIndex: '5'})
226
+
227
+ result = controller().send(:scim_pagination_info)
228
+
229
+ expect(result.limit).to eql(10)
230
+ expect(result.offset).to eql(4)
231
+ expect(result.start_index).to eql(5)
232
+ end
233
+
234
+ it 'accepts an up-front total' do
235
+ result = controller().send(:scim_pagination_info, 150)
236
+
237
+ expect(result.total).to eql(150)
238
+ end
239
+ end # "context '#scim_pagination_info' do"
240
+
241
+ context '#storage_class' do
242
+ it 'raises "not implemented" to warn subclass authors' do
243
+ expect { described_class.new.send(:storage_class) }.to raise_error(NotImplementedError)
244
+ end
245
+ end # "context '#storage_class' do"
246
+ end # "context 'service methods' do"
247
+ end
@@ -0,0 +1,75 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Scimitar::SchemasController do
4
+
5
+ before(:each) { allow(controller).to receive(:authenticated?).and_return(true) }
6
+
7
+ controller do
8
+ def index
9
+ super
10
+ end
11
+ end
12
+ context '#index' do
13
+ it 'returns a collection of supported schemas' do
14
+ get :index, params: { format: :scim }
15
+ expect(response).to be_ok
16
+ parsed_body = JSON.parse(response.body)
17
+ expect(parsed_body.length).to eql(2)
18
+ schema_names = parsed_body.map {|schema| schema['name']}
19
+ expect(schema_names).to match_array(['User', 'Group'])
20
+ end
21
+
22
+ it 'returns only the User schema when its id is provided' do
23
+ get :index, params: { name: Scimitar::Schema::User.id, format: :scim }
24
+ expect(response).to be_ok
25
+ parsed_body = JSON.parse(response.body)
26
+ expect(parsed_body['name']).to eql('User')
27
+ end
28
+
29
+ it 'returns only the Group schema when its id is provided' do
30
+ get :index, params: { name: Scimitar::Schema::Group.id, format: :scim }
31
+ expect(response).to be_ok
32
+ parsed_body = JSON.parse(response.body)
33
+ expect(parsed_body['name']).to eql('Group')
34
+ end
35
+
36
+ context 'with custom resource types' do
37
+ around :each do | example |
38
+ example.run()
39
+ ensure
40
+ Scimitar::Engine.reset_custom_resources
41
+ end
42
+
43
+ it 'returns only the License schemas when its id is provided' do
44
+ license_schema = Class.new(Scimitar::Schema::Base) do
45
+ def initialize(options = {})
46
+ super(name: 'License',
47
+ id: self.class.id,
48
+ description: 'Represents a License')
49
+ end
50
+ def self.id
51
+ 'License'
52
+ end
53
+ def self.scim_attributes
54
+ []
55
+ end
56
+ end
57
+
58
+ license_resource = Class.new(Scimitar::Resources::Base) do
59
+ set_schema license_schema
60
+ def self.endopint
61
+ '/Gaga'
62
+ end
63
+ end
64
+
65
+ Scimitar::Engine.add_custom_resource(license_resource)
66
+
67
+ get :index, params: { name: license_schema.id, format: :scim }
68
+ expect(response).to be_ok
69
+ parsed_body = JSON.parse(response.body)
70
+ expect(parsed_body['name']).to eql('License')
71
+ end
72
+ end
73
+ end
74
+ end
75
+
@@ -0,0 +1,22 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Scimitar::ServiceProviderConfigurationsController do
4
+
5
+ before(:each) { allow(controller).to receive(:authenticated?).and_return(true) }
6
+
7
+ controller do
8
+ def show
9
+ super
10
+ end
11
+ end
12
+ context '#show' do
13
+ it 'renders the servive provider configurations' do
14
+ get :show, params: { id: 'fake', format: :scim }
15
+
16
+ expect(response).to be_ok
17
+ expect(JSON.parse(response.body)).to include('patch' => {'supported' => true})
18
+ expect(JSON.parse(response.body)).to include('filter' => {'maxResults' => Scimitar::Filter::MAX_RESULTS_DEFAULT, 'supported' => true})
19
+ end
20
+ end
21
+
22
+ end
@@ -0,0 +1,19 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Scimitar::ComplexTypes::Address do
4
+ context '#as_json' do
5
+ it 'assumes a type of "work" as a default' do
6
+ expect(described_class.new.as_json).to eq('type' => 'work')
7
+ end
8
+
9
+ it 'allows a custom address type' do
10
+ expect(described_class.new(type: 'home').as_json).to eq('type' => 'home')
11
+ end
12
+
13
+ it 'shows the set address' do
14
+ expect(described_class.new(country: 'NZ').as_json).to eq('type' => 'work', 'country' => 'NZ')
15
+ end
16
+ end
17
+
18
+ end
19
+