Skip to content

Commit ca99939

Browse files
committed
feat(conditional-approval): users want conditional triggers of conditional approval (#600)
Co-authored-by: Nicolas Alexandre <[email protected]> BREAKING CHANGE: Introduction of a new permission module call Ability. The previous permission system PermissionChecker doesn't exist anymore.
1 parent 449c18b commit ca99939

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+1445
-2702
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
/spec/dummy/tmp/
1515
/tmp/
1616
.byebug_history
17+
/out
1718

1819
## Specific to RubyMotion:
1920
.dat*

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,4 @@ gem 'ipaddress', '0.8.3'
3535
gem 'openid_connect', '1.4.2'
3636
gem 'json'
3737
gem 'json-jwt', '1.15.0'
38+
gem 'deepsort'

Gemfile.lock

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ PATH
44
forest_liana (7.8.0)
55
arel-helpers
66
bcrypt
7+
deepsort
78
forestadmin-jsonapi-serializers (>= 0.14.0)
89
groupdate (>= 5.0.0)
910
httparty
@@ -89,6 +90,7 @@ GEM
8990
concurrent-ruby (1.1.10)
9091
crass (1.0.6)
9192
date (3.3.3)
93+
deepsort (0.4.5)
9294
diff-lcs (1.5.0)
9395
docile (1.4.0)
9496
erubi (1.12.0)
@@ -254,6 +256,7 @@ DEPENDENCIES
254256
arel-helpers (= 2.14.0)
255257
bcrypt
256258
byebug
259+
deepsort
257260
forest_liana!
258261
forestadmin-jsonapi-serializers
259262
groupdate (= 5.2.2)

app/controllers/forest_liana/actions_controller.rb

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ module ForestLiana
22
class ActionsController < ApplicationController
33

44
def get_smart_action_hook_request
5-
begin
5+
if params[:data] && params[:data][:attributes] && params[:data][:attributes][:collection_name]
66
params[:data][:attributes]
7-
rescue => error
7+
else
8+
error = 'parameters data attributes missing'
89
FOREST_REPORTER.report error
910
FOREST_LOGGER.error "Smart Action hook request error: #{error}"
10-
{}
11+
12+
raise ForestLiana::Errors::HTTP422Error.new("Error in smart action load hook: cannot retrieve action from collection")
1113
end
1214
end
1315

app/controllers/forest_liana/application_controller.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33

44
module ForestLiana
55
class ApplicationController < ForestLiana::BaseController
6+
rescue_from ForestLiana::Ability::Exceptions::AccessDenied, with: :render_error
7+
rescue_from ForestLiana::Errors::HTTP422Error, with: :render_error
8+
69
def self.papertrail?
710
Object.const_get('PaperTrail::Version').is_a?(Class) rescue false
811
end
@@ -96,6 +99,18 @@ def deactivate_count_response
9699

97100
private
98101

102+
def render_error(exception)
103+
errors = {
104+
status: exception.error_code,
105+
detail: exception.message,
106+
}
107+
108+
errors['name'] = exception.name if exception.try(:name)
109+
errors['data'] = exception.data if exception.try(:data)
110+
111+
render json: { errors: [errors] }, status: exception.status
112+
end
113+
99114
def force_utf8_encoding(json)
100115
if json['data'].class == Array
101116
# NOTICE: Collection of records case

app/controllers/forest_liana/resources_controller.rb

Lines changed: 31 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
module ForestLiana
22
class ResourcesController < ForestLiana::ApplicationController
3+
include ForestLiana::Ability
34
begin
45
prepend ResourcesExtensions
56
rescue NameError
@@ -14,24 +15,11 @@ class ResourcesController < ForestLiana::ApplicationController
1415
end
1516

1617
def index
18+
action = request.format == 'csv' ? 'export' : 'browse'
19+
forest_authorize!(action, forest_user, @resource)
1720
begin
18-
if request.format == 'csv'
19-
checker = ForestLiana::PermissionsChecker.new(@resource, 'exportEnabled', @rendering_id, user: forest_user)
20-
return head :forbidden unless checker.is_authorized?
21-
else
22-
checker = ForestLiana::PermissionsChecker.new(
23-
@resource,
24-
'browseEnabled',
25-
@rendering_id,
26-
user: forest_user,
27-
collection_list_parameters: get_collection_list_permission_info(forest_user, request)
28-
)
29-
return head :forbidden unless checker.is_authorized?
30-
end
31-
3221
getter = ForestLiana::ResourcesGetter.new(@resource, params, forest_user)
3322
getter.perform
34-
3523
respond_to do |format|
3624
format.json { render_jsonapi(getter) }
3725
format.csv { render_csv(getter, @resource) }
@@ -55,16 +43,8 @@ def index
5543

5644
def count
5745
find_resource
46+
forest_authorize!('browse', forest_user, @resource)
5847
begin
59-
checker = ForestLiana::PermissionsChecker.new(
60-
@resource,
61-
'browseEnabled',
62-
@rendering_id,
63-
user: forest_user,
64-
collection_list_parameters: get_collection_list_permission_info(forest_user, request)
65-
)
66-
return head :forbidden unless checker.is_authorized?
67-
6848
getter = ForestLiana::ResourcesGetter.new(@resource, params, forest_user)
6949
getter.count
7050

@@ -88,10 +68,8 @@ def count
8868
end
8969

9070
def show
71+
forest_authorize!('read', forest_user, @resource)
9172
begin
92-
checker = ForestLiana::PermissionsChecker.new(@resource, 'readEnabled', @rendering_id, user: forest_user)
93-
return head :forbidden unless checker.is_authorized?
94-
9573
getter = ForestLiana::ResourceGetter.new(@resource, params, forest_user)
9674
getter.perform
9775

@@ -106,10 +84,8 @@ def show
10684
end
10785

10886
def create
87+
forest_authorize!('add', forest_user, @resource)
10988
begin
110-
checker = ForestLiana::PermissionsChecker.new(@resource, 'addEnabled', @rendering_id, user: forest_user)
111-
return head :forbidden unless checker.is_authorized?
112-
11389
creator = ForestLiana::ResourceCreator.new(@resource, params)
11490
creator.perform
11591

@@ -130,10 +106,8 @@ def create
130106
end
131107

132108
def update
109+
forest_authorize!('edit', forest_user, @resource)
133110
begin
134-
checker = ForestLiana::PermissionsChecker.new(@resource, 'editEnabled', @rendering_id, user: forest_user)
135-
return head :forbidden unless checker.is_authorized?
136-
137111
updater = ForestLiana::ResourceUpdater.new(@resource, params, forest_user)
138112
updater.perform
139113

@@ -154,37 +128,37 @@ def update
154128
end
155129

156130
def destroy
157-
checker = ForestLiana::PermissionsChecker.new(@resource, 'deleteEnabled', @rendering_id, user: forest_user)
158-
return head :forbidden unless checker.is_authorized?
131+
forest_authorize!('delete', forest_user, @resource)
132+
begin
133+
collection_name = ForestLiana.name_for(@resource)
134+
scoped_records = ForestLiana::ScopeManager.apply_scopes_on_records(@resource, forest_user, collection_name, params[:timezone])
159135

160-
collection_name = ForestLiana.name_for(@resource)
161-
scoped_records = ForestLiana::ScopeManager.apply_scopes_on_records(@resource, forest_user, collection_name, params[:timezone])
162-
163-
unless scoped_records.exists?(params[:id])
164-
return render serializer: nil, json: { status: 404 }, status: :not_found
165-
end
136+
unless scoped_records.exists?(params[:id])
137+
return render serializer: nil, json: { status: 404 }, status: :not_found
138+
end
166139

167-
scoped_records.destroy(params[:id])
140+
scoped_records.destroy(params[:id])
168141

169-
head :no_content
170-
rescue => error
171-
FOREST_REPORTER.report error
172-
FOREST_LOGGER.error "Record Destroy error: #{error}\n#{format_stacktrace(error)}"
173-
internal_server_error
142+
head :no_content
143+
rescue => error
144+
FOREST_REPORTER.report error
145+
FOREST_LOGGER.error "Record Destroy error: #{error}\n#{format_stacktrace(error)}"
146+
internal_server_error
147+
end
174148
end
175149

176150
def destroy_bulk
177-
checker = ForestLiana::PermissionsChecker.new(@resource, 'deleteEnabled', @rendering_id, user: forest_user)
178-
return head :forbidden unless checker.is_authorized?
179-
180-
ids = ForestLiana::ResourcesGetter.get_ids_from_request(params, forest_user)
181-
@resource.destroy(ids) if ids&.any?
151+
forest_authorize!('delete', forest_user, @resource)
152+
begin
153+
ids = ForestLiana::ResourcesGetter.get_ids_from_request(params, forest_user)
154+
@resource.destroy(ids) if ids&.any?
182155

183-
head :no_content
184-
rescue => error
185-
FOREST_REPORTER.report error
186-
FOREST_LOGGER.error "Records Destroy error: #{error}\n#{format_stacktrace(error)}"
187-
internal_server_error
156+
head :no_content
157+
rescue => error
158+
FOREST_REPORTER.report error
159+
FOREST_LOGGER.error "Records Destroy error: #{error}\n#{format_stacktrace(error)}"
160+
internal_server_error
161+
end
188162
end
189163

190164
private
Lines changed: 44 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
module ForestLiana
22
class SmartActionsController < ForestLiana::ApplicationController
3+
rescue_from ForestLiana::Ability::Exceptions::TriggerForbidden, with: :render_error
4+
rescue_from ForestLiana::Ability::Exceptions::RequireApproval, with: :render_error
5+
rescue_from ForestLiana::Ability::Exceptions::ActionConditionError, with: :render_error
6+
include ForestLiana::Ability
37
if Rails::VERSION::MAJOR < 4
48
before_filter :smart_action_pre_perform_checks
59
else
@@ -8,39 +12,70 @@ class SmartActionsController < ForestLiana::ApplicationController
812

913
private
1014

15+
def smart_action_pre_perform_checks
16+
get_smart_action_request
17+
find_resource
18+
check_permission_for_smart_route
19+
ensure_record_ids_in_scope
20+
end
21+
1122
def get_smart_action_request
1223
begin
1324
params[:data][:attributes]
25+
@parameters = ForestLiana::Ability::Permission::RequestPermission::decodeSignedApprovalRequest(params.permit!)
1426
rescue => error
1527
FOREST_REPORTER.report error
1628
FOREST_LOGGER.error "Smart Action execution error: #{error}"
1729
{}
1830
end
1931
end
2032

21-
def smart_action_pre_perform_checks
22-
check_permission_for_smart_route
23-
ensure_record_ids_in_scope
33+
def find_resource
34+
begin
35+
@resource = SchemaUtils.find_model_from_collection_name(@parameters[:data][:attributes][:collection_name])
36+
if @resource.nil? || !SchemaUtils.model_included?(@resource) ||
37+
!@resource.ancestors.include?(ActiveRecord::Base)
38+
render serializer: nil, json: { status: 404 }, status: :not_found
39+
end
40+
@resource
41+
rescue => error
42+
FOREST_REPORTER.report error
43+
FOREST_LOGGER.error "Find Collection error: #{error}\n#{format_stacktrace(error)}"
44+
render serializer: nil, json: { status: 404 }, status: :not_found
45+
end
46+
end
47+
48+
def check_permission_for_smart_route
49+
smart_action_request = @parameters[:data][:attributes]
50+
if !smart_action_request.nil? && smart_action_request.has_key?(:smart_action_id)
51+
forest_authorize!(
52+
'action',
53+
forest_user,
54+
@resource,
55+
{parameters: params, endpoint: request.fullpath.split('?').first, http_method: request.request_method}
56+
)
57+
else
58+
FOREST_LOGGER.error 'Smart action execution error: Unable to retrieve the smart action id.'
59+
render serializer: nil, json: { status: 400 }, status: :bad_request
60+
end
2461
end
2562

2663
def ensure_record_ids_in_scope
2764
begin
28-
attributes = get_smart_action_request
65+
attributes = @parameters[:data][:attributes]
2966

3067
# if performing a `selectAll` let the `get_ids_from_request` handle the scopes
3168
return if attributes[:all_records]
3269

33-
resource = find_resource(attributes[:collection_name])
34-
3570
# user is using the composite_primary_keys gem
36-
if resource.primary_key.kind_of?(Array)
71+
if @resource.primary_key.kind_of?(Array)
3772
# TODO: handle primary keys
3873
return
3974
end
4075

41-
filter = JSON.generate({ 'field' => resource.primary_key, 'operator' => 'in', 'value' => attributes[:ids] })
76+
filter = JSON.generate({ 'field' => @resource.primary_key, 'operator' => 'in', 'value' => attributes[:ids] })
4277

43-
resources_getter = ForestLiana::ResourcesGetter.new(resource, { :filters => filter, :timezone => attributes[:timezone] }, forest_user)
78+
resources_getter = ForestLiana::ResourcesGetter.new(@resource, { :filters => filter, :timezone => attributes[:timezone] }, forest_user)
4479

4580
# resources getter will return records inside the scope. if the length differs then ids are out of scope
4681
return if resources_getter.count == attributes[:ids].length
@@ -53,54 +88,5 @@ def ensure_record_ids_in_scope
5388
render serializer: nil, json: { error: 'Smart Action: failed to evaluate permissions' }, status: :internal_server_error
5489
end
5590
end
56-
57-
def check_permission_for_smart_route
58-
begin
59-
60-
smart_action_request = get_smart_action_request
61-
if !smart_action_request.nil? && smart_action_request.has_key?(:smart_action_id)
62-
checker = ForestLiana::PermissionsChecker.new(
63-
find_resource(smart_action_request[:collection_name]),
64-
'actions',
65-
@rendering_id,
66-
user: forest_user,
67-
smart_action_request_info: get_smart_action_request_info
68-
)
69-
return head :forbidden unless checker.is_authorized?
70-
else
71-
FOREST_LOGGER.error 'Smart action execution error: Unable to retrieve the smart action id.'
72-
render serializer: nil, json: { status: 400 }, status: :bad_request
73-
end
74-
rescue => error
75-
FOREST_REPORTER.report error
76-
FOREST_LOGGER.error "Smart Action execution error: #{error}"
77-
render serializer: nil, json: { status: 400 }, status: :bad_request
78-
end
79-
end
80-
81-
def find_resource(collection_name)
82-
begin
83-
resource = SchemaUtils.find_model_from_collection_name(collection_name)
84-
85-
if resource.nil? || !SchemaUtils.model_included?(resource) ||
86-
!resource.ancestors.include?(ActiveRecord::Base)
87-
render serializer: nil, json: { status: 404 }, status: :not_found
88-
end
89-
resource
90-
rescue => error
91-
FOREST_REPORTER.report error
92-
FOREST_LOGGER.error "Find Collection error: #{error}\n#{format_stacktrace(error)}"
93-
render serializer: nil, json: { status: 404 }, status: :not_found
94-
end
95-
end
96-
97-
# smart action permissions are retrieved from the action's endpoint and http_method
98-
def get_smart_action_request_info
99-
{
100-
# trim query params to get the endpoint
101-
endpoint: request.fullpath.split('?').first,
102-
http_method: request.request_method
103-
}
104-
end
10591
end
10692
end

0 commit comments

Comments
 (0)