Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ def build(args = {})
context
end

# Never refused: a write must not 403 because the row it just wrote carries a relation the
# caller cannot read.
def redacted_full_projection(context)
all = ForestAdminDatasourceToolkit::Components::Query::ProjectionFactory.all(context.collection)

context.permissions.redact_projection(context.collection, all, named_by_caller: false)
end

def format_attributes(args, collection)
record = args[:params][:data][:attributes] || {}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ def setup_routes
def handle_request(args = {})
context = build(args)
context.permissions.can_chart?(args[:params])
context.permissions.assert_can_read_query_fields(context.collection, args, consumes: %i[filter])
type = validate_and_get_type(args[:params][:type])
filter = Filter.new(
condition_tree: ConditionTreeFactory.intersect(
Expand Down Expand Up @@ -89,6 +90,10 @@ def make_objective(context, filter, args)

def make_pie(context, filter, args)
group_field = args[:params][:groupByFieldName]
assert_can_read_aggregated_fields(
context, context.collection,
[['group a chart by', group_field], ['aggregate a chart on', args[:params][:aggregateFieldName]]]
)
aggregation = Aggregation.new(
operation: args[:params][:aggregator],
field: args[:params][:aggregateFieldName],
Expand All @@ -102,6 +107,11 @@ def make_pie(context, filter, args)

def make_line(context, filter, args)
group_by_field_name = args[:params][:groupByFieldName]
assert_can_read_aggregated_fields(
context, context.collection,
[['group a chart by', group_by_field_name],
['aggregate a chart on', args[:params][:aggregateFieldName]]]
)
time_range = args[:params][:timeRange]
filter_only_with_values = filter.override(
condition_tree: ConditionTree::ConditionTreeFactory.intersect(
Expand Down Expand Up @@ -178,6 +188,18 @@ def make_leaderboard(context, filter, args)
end

if collection && leaderboard_filter && aggregation
assert_can_read_aggregated_fields(
context, context.datasource.get_collection(collection),
[['group a leaderboard by', aggregation.groups[0][:field]],
['aggregate a leaderboard on', aggregation.field]]
)

# A count exposes the cardinality of the relation, which `/relationships/<name>/count`
# puts behind `browse`. No path names it, so nothing above sees it.
if aggregation.field.nil?
context.permissions.can?(:browse, context.datasource.get_collection(field.foreign_collection))
end

rows = context.datasource.get_collection(collection).aggregate(
context.caller,
leaderboard_filter,
Expand All @@ -200,12 +222,33 @@ def make_leaderboard(context, filter, args)
end

def compute_value(context, filter, args)
assert_can_read_aggregated_fields(
context, context.collection,
[['aggregate a chart on', args[:params][:aggregateFieldName]]]
)
aggregation = Aggregation.new(operation: args[:params][:aggregator],
field: args[:params][:aggregateFieldName])
result = context.collection.aggregate(context.caller, filter, aggregation)

result[0]['value'] || 0
end

# The permission root stays the chart's own collection, which the leaderboard call site does
# not share with the collection its paths resolve against.
def assert_can_read_aggregated_fields(context, path_collection, fields)
usages = fields.reject { |_action, path| path.nil? || path.to_s.empty? }
.map do |action, path|
{
action: action,
path: path,
collections: ForestAdminDatasourceToolkit::Utils::FieldPath.leaf_collection_names(
path_collection, path
)
}
end

context.permissions.assert_can_read_usages(context.collection.name, usages)
end
end
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ def handle_request(args = {})
context.permissions.can?(:browse, context.collection)

if context.collection.is_countable?
context.permissions.assert_can_read_query_fields(context.collection, args, consumes: %i[filter search])

filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new(
condition_tree: ConditionTreeFactory.intersect(
[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ def handle_request(args = {})
context = build(args)
context.permissions.can?(:browse, context.collection)
context.permissions.can?(:export, context.collection)
context.permissions.assert_can_read_query_fields(context.collection, args)
filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new(
condition_tree: ConditionTreeFactory.intersect(
[
Expand All @@ -37,10 +38,15 @@ def handle_request(args = {})
sort: QueryStringParser.parse_sort(context.collection, args),
segment: QueryStringParser.parse_segment(context.collection, args)
)
projection = QueryStringParser.parse_projection_from_request(context.collection, args)
requested = QueryStringParser.parse_requested_projection(context.collection, args)
projection = context.permissions.redact_projection(
context.collection,
requested[:projection],
named_by_caller: requested[:named_by_caller]
)
filename = args[:params][:filename] || args[:params]['collection_name']
filename += '.csv' unless /\.csv$/i.match?(filename)
header = args[:params][:header]
header = Utils::CsvGenerator.filter_header(args[:params][:header], requested[:projection], projection)

# Generate timestamp for filename
now = Time.now.strftime('%Y%m%d_%H%M%S')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ def setup_routes
def handle_request(args = {})
context = build(args)
context.permissions.can?(:browse, context.collection)
context.permissions.assert_can_read_query_fields(context.collection, args)

filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new(
condition_tree: ConditionTreeFactory.intersect(
Expand All @@ -33,7 +34,12 @@ def handle_request(args = {})
segment: QueryStringParser.parse_segment(context.collection, args)
)

projection = QueryStringParser.parse_projection_with_pks(context.collection, args)
requested = QueryStringParser.parse_requested_projection(context.collection, args)
projection = context.permissions.redact_projection(
context.collection,
requested[:projection],
named_by_caller: requested[:named_by_caller]
).with_pks(context.collection)
records = context.collection.list(context.caller, filter, projection)

{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def handle_request(args = {})
context = build(args)
context.permissions.can?(:browse, context.child_collection)
context.permissions.can?(:export, context.child_collection)
context.permissions.assert_can_read_query_fields(context.child_collection, args, consumes: %i[filter])

filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new(
condition_tree: ConditionTreeFactory.intersect(
Expand All @@ -32,9 +33,14 @@ def handle_request(args = {})
]
)
)
projection = ForestAdminAgent::Utils::QueryStringParser.parse_projection_from_request(
requested = ForestAdminAgent::Utils::QueryStringParser.parse_requested_projection(
context.child_collection, args
)
projection = context.permissions.redact_projection(
context.child_collection,
requested[:projection],
named_by_caller: requested[:named_by_caller]
)

# Get the parent record primary keys
primary_key_values = Utils::Id.unpack_id(context.collection, args[:params]['id'], with_key: true)
Expand All @@ -43,7 +49,9 @@ def handle_request(args = {})
# Generate timestamp for filename
now = Time.now.strftime('%Y%m%d_%H%M%S')
collection_name = args.dig(:params, 'collection_name')
header = args.dig(:params, 'header')
header = ForestAdminAgent::Utils::CsvGenerator.filter_header(
args.dig(:params, 'header'), requested[:projection], projection
)
filename_with_timestamp = "#{collection_name}_#{relation_name}_export_#{now}.csv"

# Create a callable to fetch related records
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def setup_routes
def handle_request(args = {})
context = build(args)
context.permissions.can?(:browse, context.child_collection)
context.permissions.assert_can_read_query_fields(context.child_collection, args, consumes: %i[filter sort])

filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new(
condition_tree: ConditionTreeFactory.intersect(
Expand All @@ -34,8 +35,14 @@ def handle_request(args = {})
page: ForestAdminAgent::Utils::QueryStringParser.parse_pagination(args),
sort: ForestAdminAgent::Utils::QueryStringParser.parse_sort(context.child_collection, args)
)
projection = ForestAdminAgent::Utils::QueryStringParser.parse_projection_with_pks(context.child_collection,
args)
requested = ForestAdminAgent::Utils::QueryStringParser.parse_requested_projection(
context.child_collection, args
)
projection = context.permissions.redact_projection(
context.child_collection,
requested[:projection],
named_by_caller: requested[:named_by_caller]
).with_pks(context.child_collection)
primary_key_values = Utils::Id.unpack_id(context.collection, args[:params]['id'], with_key: true)
records = Collection.list_relation(
context.collection,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@ def handle_request(args = {})
condition_tree: ConditionTree::ConditionTreeFactory.intersect([condition_tree, scope])
)

projection = QueryStringParser.parse_projection_with_pks(context.collection, args)
requested = QueryStringParser.parse_requested_projection(context.collection, args)
projection = context.permissions.redact_projection(
context.collection,
requested[:projection],
named_by_caller: requested[:named_by_caller]
).with_pks(context.collection)

records = context.collection.list(context.caller, filter, projection)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ def handle_request(args = {})
filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new(
condition_tree: ConditionTree::ConditionTreeFactory.match_ids(context.collection, [id])
)
records = context.collection.list(context.caller, filter, ProjectionFactory.all(context.collection))
projection = redacted_full_projection(context)
records = context.collection.list(context.caller, filter, projection)

{
name: args[:params]['collection_name'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ def handle_request(args = {})
drop_relationships!(args)
data = format_attributes(args, context.collection)
context.collection.update(context.caller, filter, data)
records = context.collection.list(context.caller, filter, ProjectionFactory.all(context.collection))
projection = redacted_full_projection(context)
records = context.collection.list(context.caller, filter, projection)

{
name: args[:params]['collection_name'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ def handle_request(args = {})
)
context.collection.update(context.caller, filter, { field_name => updated_array })

records = context.collection.list(context.caller, filter, ProjectionFactory.all(context.collection))
projection = redacted_full_projection(context)
records = context.collection.list(context.caller, filter, projection)

{
name: args[:params]['collection_name'],
Expand Down Expand Up @@ -93,7 +94,8 @@ def fetch_record(primary_key_values, context)
filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new(
condition_tree: ConditionTree::ConditionTreeFactory.intersect([condition_tree, scope])
)
records = context.collection.list(context.caller, filter, ProjectionFactory.all(context.collection))
projection = redacted_full_projection(context)
records = context.collection.list(context.caller, filter, projection)

raise Http::Exceptions::NotFoundError, 'Record not found' unless records&.any?

Expand Down
Loading
Loading