Skip to content

Commit 66825a3

Browse files
larcinadriguy
authored andcommitted
feat(scopes): enforce scopes restrictions on a wider range of requests (#488)
1 parent a30afbc commit 66825a3

Some content is hidden

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

43 files changed

+1142
-636
lines changed

Gemfile.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
PATH
22
remote: .
33
specs:
4-
forest_liana (7.0.0-beta.4)
4+
forest_liana (7.0.0.pre.beta.4)
55
arel-helpers
66
bcrypt
77
forestadmin-jsonapi-serializers (>= 0.14.0)
@@ -252,4 +252,4 @@ DEPENDENCIES
252252
useragent
253253

254254
BUNDLED WITH
255-
2.1.4
255+
2.2.3

app/controllers/forest_liana/actions_controller.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
module ForestLiana
2-
class ActionsController < ForestLiana::BaseController
2+
class ActionsController < ApplicationController
33

44
def get_smart_action_hook_request
55
begin
@@ -17,7 +17,7 @@ def get_collection(collection_name)
1717
def get_action(collection_name)
1818
collection = get_collection(collection_name)
1919
begin
20-
collection.actions.find {|action| ActiveSupport::Inflector.parameterize(action.name) == params[:action_name]}
20+
collection.actions.find {|action| ActiveSupport::Inflector.parameterize(action.name) == params[:action_name]}
2121
rescue => error
2222
FOREST_LOGGER.error "Smart Action get action retrieval error: #{error}"
2323
nil

app/controllers/forest_liana/associations_controller.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class AssociationsController < ForestLiana::ApplicationController
1010

1111
def index
1212
begin
13-
getter = HasManyGetter.new(@resource, @association, params)
13+
getter = HasManyGetter.new(@resource, @association, params, forest_user)
1414
getter.perform
1515

1616
respond_to do |format|
@@ -25,7 +25,7 @@ def index
2525

2626
def count
2727
begin
28-
getter = HasManyGetter.new(@resource, @association, params)
28+
getter = HasManyGetter.new(@resource, @association, params, forest_user)
2929
getter.count
3030

3131
render serializer: nil, json: { count: getter.records_count }

app/controllers/forest_liana/resources_controller.rb

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def index
2929
return head :forbidden unless checker.is_authorized?
3030
end
3131

32-
getter = ForestLiana::ResourcesGetter.new(@resource, params)
32+
getter = ForestLiana::ResourcesGetter.new(@resource, params, forest_user)
3333
getter.perform
3434

3535
respond_to do |format|
@@ -63,7 +63,7 @@ def count
6363
)
6464
return head :forbidden unless checker.is_authorized?
6565

66-
getter = ForestLiana::ResourcesGetter.new(@resource, params)
66+
getter = ForestLiana::ResourcesGetter.new(@resource, params, forest_user)
6767
getter.count
6868

6969
render serializer: nil, json: { count: getter.records_count }
@@ -89,10 +89,12 @@ def show
8989
checker = ForestLiana::PermissionsChecker.new(@resource, 'readEnabled', @rendering_id, user_id: forest_user['id'])
9090
return head :forbidden unless checker.is_authorized?
9191

92-
getter = ForestLiana::ResourceGetter.new(@resource, params)
92+
getter = ForestLiana::ResourceGetter.new(@resource, params, forest_user)
9393
getter.perform
9494

9595
render serializer: nil, json: render_record_jsonapi(getter.record)
96+
rescue ActiveRecord::RecordNotFound
97+
render serializer: nil, json: { status: 404 }, status: :not_found
9698
rescue => error
9799
FOREST_LOGGER.error "Record Show error: #{error}\n#{format_stacktrace(error)}"
98100
internal_server_error
@@ -127,7 +129,7 @@ def update
127129
checker = ForestLiana::PermissionsChecker.new(@resource, 'editEnabled', @rendering_id, user_id: forest_user['id'])
128130
return head :forbidden unless checker.is_authorized?
129131

130-
updater = ForestLiana::ResourceUpdater.new(@resource, params)
132+
updater = ForestLiana::ResourceUpdater.new(@resource, params, forest_user)
131133
updater.perform
132134

133135
if updater.errors
@@ -149,7 +151,14 @@ def destroy
149151
checker = ForestLiana::PermissionsChecker.new(@resource, 'deleteEnabled', @rendering_id, user_id: forest_user['id'])
150152
return head :forbidden unless checker.is_authorized?
151153

152-
@resource.destroy(params[:id]) if @resource.exists?(params[:id])
154+
collection_name = ForestLiana.name_for(@resource)
155+
scoped_records = ForestLiana::ScopeManager.apply_scopes_on_records(@resource, forest_user, collection_name, params[:timezone])
156+
157+
unless scoped_records.exists?(params[:id])
158+
return render serializer: nil, json: { status: 404 }, status: :not_found
159+
end
160+
161+
scoped_records.destroy(params[:id])
153162

154163
head :no_content
155164
rescue => error
@@ -161,7 +170,7 @@ def destroy_bulk
161170
checker = ForestLiana::PermissionsChecker.new(@resource, 'deleteEnabled', @rendering_id, user_id: forest_user['id'])
162171
return head :forbidden unless checker.is_authorized?
163172

164-
ids = ForestLiana::ResourcesGetter.get_ids_from_request(params)
173+
ids = ForestLiana::ResourcesGetter.get_ids_from_request(params, forest_user)
165174
@resource.destroy(ids) if ids&.any?
166175

167176
head :no_content
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
module ForestLiana
2+
class ScopesController < ForestLiana::ApplicationController
3+
def invalidate_scope_cache
4+
begin
5+
rendering_id = params[:renderingId]
6+
7+
unless rendering_id
8+
FOREST_LOGGER.error 'Missing renderingId'
9+
return render serializer: nil, json: { status: 400 }, status: :bad_request
10+
end
11+
12+
ForestLiana::ScopeManager.invalidate_scope_cache(rendering_id)
13+
return render serializer: nil, json: { status: 200 }, status: :ok
14+
rescue => error
15+
FOREST_LOGGER.error "Error during scope cache invalidation: #{error.message}"
16+
render serializer: nil, json: {status: 500 }, status: :internal_server_error
17+
end
18+
end
19+
end
20+
end

app/controllers/forest_liana/smart_actions_controller.rb

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
module ForestLiana
22
class SmartActionsController < ForestLiana::ApplicationController
33
if Rails::VERSION::MAJOR < 4
4-
before_filter :check_permission_for_smart_route
4+
before_filter :smart_action_pre_perform_checks
55
else
6-
before_action :check_permission_for_smart_route
6+
before_action :smart_action_pre_perform_checks
77
end
88

99
private
@@ -17,6 +17,41 @@ def get_smart_action_request
1717
end
1818
end
1919

20+
def smart_action_pre_perform_checks
21+
check_permission_for_smart_route
22+
ensure_record_ids_in_scope
23+
end
24+
25+
def ensure_record_ids_in_scope
26+
begin
27+
attributes = get_smart_action_request
28+
29+
# if performing a `selectAll` let the `get_ids_from_request` handle the scopes
30+
return if attributes[:all_records]
31+
32+
resource = find_resource(attributes[:collection_name])
33+
34+
# user is using the composite_primary_keys gem
35+
if resource.primary_key.kind_of?(Array)
36+
# TODO: handle primary keys
37+
return
38+
end
39+
40+
filter = JSON.generate({ 'field' => resource.primary_key, 'operator' => 'in', 'value' => attributes[:ids] })
41+
42+
resources_getter = ForestLiana::ResourcesGetter.new(resource, { :filters => filter, :timezone => attributes[:timezone] }, forest_user)
43+
44+
# resources getter will return records inside the scope. if the length differs then ids are out of scope
45+
return if resources_getter.count == attributes[:ids].length
46+
47+
# target records are out of scope
48+
render serializer: nil, json: { error: 'Smart Action: target record not found' }, status: :bad_request
49+
rescue => error
50+
FOREST_LOGGER.error "Smart Action: #{error}\n#{format_stacktrace(error)}"
51+
render serializer: nil, json: { error: 'Smart Action: failed to evaluate permissions' }, status: :internal_server_error
52+
end
53+
end
54+
2055
def check_permission_for_smart_route
2156
begin
2257

@@ -58,7 +93,8 @@ def find_resource(collection_name)
5893
# smart action permissions are retrieved from the action's endpoint and http_method
5994
def get_smart_action_request_info
6095
{
61-
endpoint: request.fullpath,
96+
# trim query params to get the endpoint
97+
endpoint: request.fullpath.split('?').first,
6298
http_method: request.request_method
6399
}
64100
end

app/controllers/forest_liana/stats_controller.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,15 @@ class StatsController < ForestLiana::ApplicationController
2727
def get
2828
case params[:type]
2929
when CHART_TYPE_VALUE
30-
stat = ValueStatGetter.new(@resource, params)
30+
stat = ValueStatGetter.new(@resource, params, forest_user)
3131
when CHART_TYPE_PIE
32-
stat = PieStatGetter.new(@resource, params)
32+
stat = PieStatGetter.new(@resource, params, forest_user)
3333
when CHART_TYPE_LINE
34-
stat = LineStatGetter.new(@resource, params)
34+
stat = LineStatGetter.new(@resource, params, forest_user)
3535
when CHART_TYPE_OBJECTIVE
36-
stat = ObjectiveStatGetter.new(@resource, params)
36+
stat = ObjectiveStatGetter.new(@resource, params, forest_user)
3737
when CHART_TYPE_LEADERBOARD
38-
stat = LeaderboardStatGetter.new(@resource, params)
38+
stat = LeaderboardStatGetter.new(@resource, params, forest_user)
3939
end
4040

4141
stat.perform

app/services/forest_liana/filters_parser.rb

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,13 +109,15 @@ def parse_condition_without_smart_field(condition)
109109
parsed_value = parse_value(operator, value)
110110
field_and_operator = "#{parsed_field} #{parsed_operator}"
111111

112+
# parenthesis around the parsed_value are required to make the `IN` operator work
113+
# and have no side effects on other requests
112114
if Rails::VERSION::MAJOR < 5
113-
"#{field_and_operator} #{ActiveRecord::Base.sanitize(parsed_value)}"
115+
"#{field_and_operator} (#{ActiveRecord::Base.sanitize(parsed_value)})"
114116
# NOTICE: sanitize method as been removed in Rails 5.1 and sanitize_sql introduced in Rails 5.2.
115117
elsif Rails::VERSION::MAJOR == 5 && Rails::VERSION::MINOR == 1
116-
"#{field_and_operator} #{ActiveRecord::Base.connection.quote(parsed_value)}"
118+
"#{field_and_operator} (#{ActiveRecord::Base.connection.quote(parsed_value)})"
117119
else
118-
ActiveRecord::Base.sanitize_sql(["#{field_and_operator} ?", parsed_value])
120+
ActiveRecord::Base.sanitize_sql(["#{field_and_operator} (?)", parsed_value])
119121
end
120122
end
121123

@@ -147,14 +149,16 @@ def parse_operator(operator)
147149
'IS'
148150
when 'present'
149151
'IS NOT'
152+
when 'in'
153+
'IN'
150154
else
151155
raise_unknown_operator_error(operator)
152156
end
153157
end
154158

155159
def parse_value(operator, value)
156160
case operator
157-
when 'not', 'greater_than', 'less_than', 'not_equal', 'equal', 'before', 'after'
161+
when 'not', 'greater_than', 'less_than', 'not_equal', 'equal', 'before', 'after', 'in'
158162
value
159163
when 'contains', 'not_contains'
160164
"%#{value}%"

app/services/forest_liana/has_many_dissociator.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
module ForestLiana
2-
class HasManyDissociator
2+
class HasManyDissociator < ForestLiana::ApplicationController
33
def initialize(resource, association, params)
44
@resource = resource
55
@association = association
@@ -17,7 +17,7 @@ def perform
1717
if @data.is_a?(Array)
1818
record_ids = @data.map { |record| record[:id] }
1919
elsif @data.dig('attributes').present?
20-
record_ids = ForestLiana::ResourcesGetter.get_ids_from_request(@params)
20+
record_ids = ForestLiana::ResourcesGetter.get_ids_from_request(@params, forest_user)
2121
else
2222
record_ids = Array.new
2323
end

app/services/forest_liana/has_many_getter.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ class HasManyGetter < BaseGetter
44
attr_reader :includes
55
attr_reader :records_count
66

7-
def initialize(resource, association, params)
7+
def initialize(resource, association, params, forest_user)
88
@resource = resource
99
@association = association
1010
@params = params
@@ -13,7 +13,7 @@ def initialize(resource, association, params)
1313
@collection = get_collection(@collection_name)
1414
compute_includes()
1515
includes_symbols = @includes.map { |include| include.to_sym }
16-
@search_query_builder = SearchQueryBuilder.new(@params, includes_symbols, @collection)
16+
@search_query_builder = SearchQueryBuilder.new(@params, includes_symbols, @collection, forest_user)
1717

1818
prepare_query()
1919
end

0 commit comments

Comments
 (0)