forest_liana 9.22.14 → 9.22.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/controllers/forest_liana/devise_controller.rb +16 -7
- data/app/controllers/forest_liana/smart_actions_controller.rb +6 -0
- data/app/services/forest_liana/schema_adapter.rb +1 -0
- data/lib/forest_liana/version.rb +1 -1
- data/spec/requests/actions_controller_spec.rb +11 -0
- data/spec/requests/devise_controller_spec.rb +271 -0
- data/spec/services/forest_liana/schema_adapter_spec.rb +26 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dc9e247d2d65125925e14127a256acb70326504b4c1e2fc90daaf3b792736025
|
|
4
|
+
data.tar.gz: 7039ebbb67be595efd366b94d57a35881448f9747cba39cf2e8e35b1f88a083e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3bf2d66f16eaedc067bfca8422a0274255bc9bc6d86057e251bdb65dd41191859e02344f2712356511c437bf03a525c5ff11b2a7f4ebb972104420baafcd1fbc
|
|
7
|
+
data.tar.gz: 07b0b79be5105719f3cffbb590717731031d3e1cd0fc81eb6e5d2c72a60f7413e411661f4f301a97294fc08e4cadac9e1d547ceb9c9cec1b9b0bbeadae569b3b
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
module ForestLiana
|
|
2
|
-
class DeviseController < ForestLiana::
|
|
3
|
-
|
|
4
|
-
resource = SchemaUtils.find_model_from_collection_name(
|
|
5
|
-
params['data']['attributes']['collection_name'])
|
|
2
|
+
class DeviseController < ForestLiana::SmartActionsController
|
|
3
|
+
rescue_from ActiveRecord::RecordNotFound, with: :render_target_not_found
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
def change_password
|
|
6
|
+
attributes = @parameters[:data][:attributes]
|
|
7
|
+
# The inherited scope check skips composite primary keys.
|
|
8
|
+
scoped_records = ForestLiana::ScopeManager.apply_scopes_on_records(
|
|
9
|
+
@resource, forest_user, ForestLiana.name_for(@resource), @parameters[:timezone]
|
|
10
|
+
)
|
|
11
|
+
user = scoped_records.find(attributes[:ids].first)
|
|
12
|
+
user.password = attributes[:values]['New password']
|
|
9
13
|
user.save
|
|
10
14
|
|
|
11
15
|
if user.valid?
|
|
@@ -16,6 +20,11 @@ module ForestLiana
|
|
|
16
20
|
}
|
|
17
21
|
end
|
|
18
22
|
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def render_target_not_found
|
|
27
|
+
render serializer: nil, json: { error: 'Smart Action: target record not found' }, status: :bad_request
|
|
28
|
+
end
|
|
19
29
|
end
|
|
20
30
|
end
|
|
21
|
-
|
|
@@ -6,11 +6,13 @@ module ForestLiana
|
|
|
6
6
|
rescue_from ForestLiana::Ability::Exceptions::ActionConditionError, with: :render_error
|
|
7
7
|
include ForestLiana::Ability
|
|
8
8
|
if Rails::VERSION::MAJOR < 4
|
|
9
|
+
before_filter :refuse_requester_id_outside_signed_request
|
|
9
10
|
before_filter :get_smart_action_request
|
|
10
11
|
before_filter :find_resource
|
|
11
12
|
before_filter :check_permission_for_smart_route
|
|
12
13
|
before_filter :ensure_record_ids_in_scope
|
|
13
14
|
else
|
|
15
|
+
before_action :refuse_requester_id_outside_signed_request
|
|
14
16
|
before_action :get_smart_action_request
|
|
15
17
|
before_action :find_resource
|
|
16
18
|
before_action :check_permission_for_smart_route
|
|
@@ -19,6 +21,10 @@ module ForestLiana
|
|
|
19
21
|
|
|
20
22
|
private
|
|
21
23
|
|
|
24
|
+
def refuse_requester_id_outside_signed_request
|
|
25
|
+
raise ForestLiana::Errors::HTTP422Error.new if params.dig(:data, :attributes, :requester_id)
|
|
26
|
+
end
|
|
27
|
+
|
|
22
28
|
def get_smart_action_request
|
|
23
29
|
begin
|
|
24
30
|
params[:data][:attributes]
|
data/lib/forest_liana/version.rb
CHANGED
|
@@ -563,6 +563,17 @@ describe 'Requesting Actions routes', :type => :request do
|
|
|
563
563
|
end
|
|
564
564
|
end
|
|
565
565
|
|
|
566
|
+
describe 'with a requester named outside a signed approval request' do
|
|
567
|
+
it 'should respond 422 and NOT perform the action' do
|
|
568
|
+
params[:data][:attributes][:requester_id] = 99
|
|
569
|
+
expect_any_instance_of(Forest::IslandsController).not_to receive(:test)
|
|
570
|
+
|
|
571
|
+
post '/forest/actions/test', params: JSON.dump(params), headers: headers
|
|
572
|
+
|
|
573
|
+
expect(response.status).to eq(422)
|
|
574
|
+
end
|
|
575
|
+
end
|
|
576
|
+
|
|
566
577
|
describe 'with scopes' do
|
|
567
578
|
before(:each) do
|
|
568
579
|
allow_any_instance_of(ForestLiana::Ability).to receive(:forest_authorize!) { true }
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
require 'rails_helper'
|
|
2
|
+
|
|
3
|
+
describe 'Change password action', type: :request do
|
|
4
|
+
let(:rendering_id) { 13 }
|
|
5
|
+
let(:role_id) { 1 }
|
|
6
|
+
let(:trigger_roles) { [role_id] }
|
|
7
|
+
let(:approval_roles) { [] }
|
|
8
|
+
let(:approver_roles) { [] }
|
|
9
|
+
let(:scope_filters) { { 'scopes' => {}, 'team' => { 'id' => '1', 'name' => 'Operations' } } }
|
|
10
|
+
let(:cache) { ActiveSupport::Cache::MemoryStore.new }
|
|
11
|
+
let(:island_collection) { ForestLiana.apimap.find { |collection| collection.name.to_s == 'Island' } }
|
|
12
|
+
let(:change_password_action) do
|
|
13
|
+
ForestLiana::Model::Action.new(
|
|
14
|
+
id: 'Island.Change password',
|
|
15
|
+
name: 'Change password',
|
|
16
|
+
type: 'single',
|
|
17
|
+
fields: [{ field: 'New password', type: 'String' }]
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
let(:token) do
|
|
22
|
+
JWT.encode({
|
|
23
|
+
id: 38,
|
|
24
|
+
email: 'michael.kelso@that70.show',
|
|
25
|
+
first_name: 'Michael',
|
|
26
|
+
last_name: 'Kelso',
|
|
27
|
+
team: 'Operations',
|
|
28
|
+
rendering_id: rendering_id,
|
|
29
|
+
exp: Time.now.to_i + 2.weeks.to_i,
|
|
30
|
+
permission_level: 'user'
|
|
31
|
+
}, ForestLiana.auth_secret, 'HS256')
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
let(:headers) do
|
|
35
|
+
{
|
|
36
|
+
'Accept' => 'application/json',
|
|
37
|
+
'Content-Type' => 'application/json',
|
|
38
|
+
'Authorization' => "Bearer #{token}"
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
let(:attributes) do
|
|
43
|
+
{
|
|
44
|
+
collection_name: 'Island',
|
|
45
|
+
ids: ['1'],
|
|
46
|
+
all_records: false,
|
|
47
|
+
values: { 'New password' => 'n3w-p4ssw0rd' },
|
|
48
|
+
smart_action_id: 'Island-Change password'
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
let(:params) { { data: { attributes: attributes, type: 'custom-action-requests' }, timezone: 'Europe/Paris' } }
|
|
53
|
+
|
|
54
|
+
def environment_permissions
|
|
55
|
+
enabled = { 'roles' => [role_id] }
|
|
56
|
+
crud = %w[browseEnabled readEnabled editEnabled addEnabled deleteEnabled exportEnabled].to_h { |key| [key, enabled] }
|
|
57
|
+
change_password = {
|
|
58
|
+
'triggerEnabled' => { 'roles' => trigger_roles },
|
|
59
|
+
'triggerConditions' => [],
|
|
60
|
+
'approvalRequired' => { 'roles' => approval_roles },
|
|
61
|
+
'approvalRequiredConditions' => [],
|
|
62
|
+
'userApprovalEnabled' => { 'roles' => approver_roles },
|
|
63
|
+
'userApprovalConditions' => [],
|
|
64
|
+
'selfApprovalEnabled' => { 'roles' => [] }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
{ 'collections' => { 'Island' => { 'collection' => crud, 'actions' => { 'Change password' => change_password } } } }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
before(:all) { Island.send(:attr_accessor, :password) }
|
|
71
|
+
after(:all) { Island.send(:remove_method, :password, :password=) }
|
|
72
|
+
|
|
73
|
+
before do
|
|
74
|
+
island_collection.actions << change_password_action
|
|
75
|
+
Island.create!(id: 1, name: 'Corsica')
|
|
76
|
+
|
|
77
|
+
allow(Rails).to receive(:cache).and_return(cache)
|
|
78
|
+
allow_any_instance_of(ForestLiana::Ability::Fetch).to receive(:get_permissions) do |_, path|
|
|
79
|
+
case path
|
|
80
|
+
when '/liana/v4/permissions/environment' then environment_permissions
|
|
81
|
+
when '/liana/v4/permissions/users' then [{ 'id' => 38, 'roleId' => role_id }]
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
ForestLiana::ScopeManager.invalidate_scope_cache(rendering_id)
|
|
86
|
+
allow(ForestLiana::ScopeManager).to receive(:fetch_scopes).and_return(scope_filters)
|
|
87
|
+
allow(ForestLiana::IpWhitelist).to receive(:is_ip_whitelist_retrieved) { true }
|
|
88
|
+
allow(ForestLiana::IpWhitelist).to receive(:is_ip_valid) { true }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
after do
|
|
92
|
+
island_collection.actions.delete(change_password_action)
|
|
93
|
+
Island.destroy_all
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def change_password
|
|
97
|
+
post '/forest/actions/change-password', params: JSON.dump(params), headers: headers
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
context 'when the user is allowed to trigger the action on the collection' do
|
|
101
|
+
it 'responds 204 and sets the new password on the targeted record' do
|
|
102
|
+
expect_any_instance_of(Island).to receive(:password=).with('n3w-p4ssw0rd').and_call_original
|
|
103
|
+
|
|
104
|
+
change_password
|
|
105
|
+
|
|
106
|
+
expect(response.status).to eq(204)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
context 'when the user scope excludes the targeted record at lookup' do
|
|
111
|
+
it 'resolves the record within the user scope and leaves the password unchanged' do
|
|
112
|
+
expect(ForestLiana::ScopeManager).to receive(:apply_scopes_on_records)
|
|
113
|
+
.with(Island, hash_including('id' => 38, 'rendering_id' => rendering_id), 'Island', 'Europe/Paris')
|
|
114
|
+
.and_return(Island.none)
|
|
115
|
+
expect_any_instance_of(Island).not_to receive(:password=)
|
|
116
|
+
|
|
117
|
+
change_password
|
|
118
|
+
|
|
119
|
+
expect(response.status).to eq(400)
|
|
120
|
+
expect(JSON.parse(response.body)).to eq('error' => 'Smart Action: target record not found')
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
context 'when the user is not allowed to trigger the action on the collection' do
|
|
125
|
+
let(:trigger_roles) { [] }
|
|
126
|
+
|
|
127
|
+
it 'responds 403 and leaves the password unchanged' do
|
|
128
|
+
expect_any_instance_of(Island).not_to receive(:password=)
|
|
129
|
+
|
|
130
|
+
change_password
|
|
131
|
+
|
|
132
|
+
expect(response.status).to eq(403)
|
|
133
|
+
expect(JSON.parse(response.body)['errors'].first).to include(
|
|
134
|
+
'name' => 'CustomActionTriggerForbiddenError',
|
|
135
|
+
'detail' => "You don't have the permission to trigger this action"
|
|
136
|
+
)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
context 'when the user role must have the action approved' do
|
|
141
|
+
let(:approval_roles) { [role_id] }
|
|
142
|
+
|
|
143
|
+
it 'responds 403 asking for approval and leaves the password unchanged' do
|
|
144
|
+
expect_any_instance_of(Island).not_to receive(:password=)
|
|
145
|
+
|
|
146
|
+
change_password
|
|
147
|
+
|
|
148
|
+
expect(response.status).to eq(403)
|
|
149
|
+
expect(JSON.parse(response.body)['errors'].first).to include(
|
|
150
|
+
'name' => 'CustomActionRequiresApprovalError',
|
|
151
|
+
'detail' => 'This action requires to be approved.'
|
|
152
|
+
)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
context 'when the request carries no smart action id' do
|
|
157
|
+
let(:attributes) { super().except(:smart_action_id) }
|
|
158
|
+
|
|
159
|
+
it 'responds 400 and leaves the password unchanged' do
|
|
160
|
+
expect_any_instance_of(Island).not_to receive(:password=)
|
|
161
|
+
|
|
162
|
+
change_password
|
|
163
|
+
|
|
164
|
+
expect(response.status).to eq(400)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
context 'when the targeted record is outside the user scope' do
|
|
169
|
+
let(:scope_filters) do
|
|
170
|
+
{
|
|
171
|
+
'scopes' => {
|
|
172
|
+
'Island' => {
|
|
173
|
+
'aggregator' => 'and',
|
|
174
|
+
'conditions' => [{ 'field' => 'name', 'operator' => 'equal', 'value' => 'Ré' }]
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
'team' => { 'id' => 43, 'name' => 'Operations' }
|
|
178
|
+
}
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
it 'responds 400 and leaves the password unchanged' do
|
|
182
|
+
expect_any_instance_of(Island).not_to receive(:password=)
|
|
183
|
+
|
|
184
|
+
change_password
|
|
185
|
+
|
|
186
|
+
expect(response.status).to eq(400)
|
|
187
|
+
expect(JSON.parse(response.body)).to eq('error' => 'Smart Action: target record not found')
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
context 'when the request selects all records' do
|
|
192
|
+
let(:attributes) { super().merge(all_records: true, all_records_ids_excluded: []) }
|
|
193
|
+
|
|
194
|
+
it 'responds 422 and leaves the password unchanged' do
|
|
195
|
+
expect_any_instance_of(Island).not_to receive(:password=)
|
|
196
|
+
|
|
197
|
+
change_password
|
|
198
|
+
|
|
199
|
+
expect(response.status).to eq(422)
|
|
200
|
+
expect(JSON.parse(response.body)['errors'].first).to include('detail' => ForestLiana::SingleRecordSelection::MESSAGE)
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
context 'when the request targets several records' do
|
|
205
|
+
let(:attributes) { super().merge(ids: %w[1 2]) }
|
|
206
|
+
|
|
207
|
+
it 'responds 422 and leaves the password unchanged' do
|
|
208
|
+
expect_any_instance_of(Island).not_to receive(:password=)
|
|
209
|
+
|
|
210
|
+
change_password
|
|
211
|
+
|
|
212
|
+
expect(response.status).to eq(422)
|
|
213
|
+
expect(JSON.parse(response.body)['errors'].first).to include('detail' => ForestLiana::SingleRecordSelection::MESSAGE)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
context 'when the request names a requester outside a signed approval request' do
|
|
218
|
+
let(:attributes) { super().merge(requester_id: 99) }
|
|
219
|
+
|
|
220
|
+
it 'responds 422 and leaves the password unchanged' do
|
|
221
|
+
expect_any_instance_of(Island).not_to receive(:password=)
|
|
222
|
+
|
|
223
|
+
change_password
|
|
224
|
+
|
|
225
|
+
expect(response.status).to eq(422)
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
context 'when an approver executes a signed approval request' do
|
|
230
|
+
let(:approval_roles) { [role_id] }
|
|
231
|
+
let(:approver_roles) { [role_id] }
|
|
232
|
+
let(:approved_ids) { ['1'] }
|
|
233
|
+
let(:signed_approval_request) do
|
|
234
|
+
JWT.encode(
|
|
235
|
+
{ data: { attributes: attributes.merge(ids: approved_ids, requester_id: 99) } },
|
|
236
|
+
ForestLiana.env_secret,
|
|
237
|
+
'HS256'
|
|
238
|
+
)
|
|
239
|
+
end
|
|
240
|
+
let(:params) do
|
|
241
|
+
{
|
|
242
|
+
data: {
|
|
243
|
+
attributes: attributes.except(:values).merge(signed_approval_request: signed_approval_request),
|
|
244
|
+
type: 'custom-action-requests'
|
|
245
|
+
},
|
|
246
|
+
timezone: 'Europe/Paris'
|
|
247
|
+
}
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
it 'responds 204 and sets the approved password on the targeted record' do
|
|
251
|
+
expect_any_instance_of(Island).to receive(:password=).with('n3w-p4ssw0rd').and_call_original
|
|
252
|
+
|
|
253
|
+
change_password
|
|
254
|
+
|
|
255
|
+
expect(response.status).to eq(204)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
context 'and the approved request targets several records' do
|
|
259
|
+
let(:approved_ids) { %w[1 2] }
|
|
260
|
+
|
|
261
|
+
it 'responds 422 and leaves the password unchanged' do
|
|
262
|
+
expect_any_instance_of(Island).not_to receive(:password=)
|
|
263
|
+
|
|
264
|
+
change_password
|
|
265
|
+
|
|
266
|
+
expect(response.status).to eq(422)
|
|
267
|
+
expect(JSON.parse(response.body)['errors'].first).to include('detail' => ForestLiana::SingleRecordSelection::MESSAGE)
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end
|
|
@@ -122,6 +122,32 @@ module ForestLiana
|
|
|
122
122
|
expect(uri_regex_validation[:value].match('\n')).to eq(nil)
|
|
123
123
|
end
|
|
124
124
|
end
|
|
125
|
+
|
|
126
|
+
context 'with a Devise model' do
|
|
127
|
+
let(:devise_model) do
|
|
128
|
+
stub_const('DeviseAccount', Class.new(ActiveRecord::Base) do
|
|
129
|
+
self.table_name = 'isle'
|
|
130
|
+
|
|
131
|
+
def self.devise_modules?
|
|
132
|
+
true
|
|
133
|
+
end
|
|
134
|
+
end)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
after { ForestLiana.apimap.delete_if { |collection| collection.name.to_s == 'DeviseAccount' } }
|
|
138
|
+
|
|
139
|
+
it 'declares Change password as a single-record action' do
|
|
140
|
+
collection = described_class.new(devise_model).perform
|
|
141
|
+
action = collection.actions.find { |candidate| candidate.name == 'Change password' }
|
|
142
|
+
|
|
143
|
+
expect(action).to have_attributes(
|
|
144
|
+
id: 'DeviseAccount.Change password',
|
|
145
|
+
type: 'single',
|
|
146
|
+
endpoint: 'forest/actions/change-password',
|
|
147
|
+
http_method: 'POST'
|
|
148
|
+
)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
125
151
|
end
|
|
126
152
|
end
|
|
127
153
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: forest_liana
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 9.22.
|
|
4
|
+
version: 9.22.15
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sandro Munda
|
|
@@ -486,6 +486,7 @@ files:
|
|
|
486
486
|
- spec/requests/capabilities_spec.rb
|
|
487
487
|
- spec/requests/cors_spec.rb
|
|
488
488
|
- spec/requests/count_spec.rb
|
|
489
|
+
- spec/requests/devise_controller_spec.rb
|
|
489
490
|
- spec/requests/missing_attribute_valve_spec.rb
|
|
490
491
|
- spec/requests/permissions_cache_footprint_spec.rb
|
|
491
492
|
- spec/requests/projection_header_spec.rb
|
|
@@ -850,6 +851,7 @@ test_files:
|
|
|
850
851
|
- spec/requests/capabilities_spec.rb
|
|
851
852
|
- spec/requests/cors_spec.rb
|
|
852
853
|
- spec/requests/count_spec.rb
|
|
854
|
+
- spec/requests/devise_controller_spec.rb
|
|
853
855
|
- spec/requests/missing_attribute_valve_spec.rb
|
|
854
856
|
- spec/requests/permissions_cache_footprint_spec.rb
|
|
855
857
|
- spec/requests/projection_header_spec.rb
|