scimitar 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
+