Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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,26 @@ 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

# +with_pks+ runs after the redaction on purpose. It only re-adds keys for relations the
# redaction kept a path through, and the serializer needs those keys to emit the readable
# column behind them — dropping them would take the permitted path down with them. A relation
# the redaction emptied contributes no path, so nothing is re-added for it.
def redacted_projection_with_pks(context, collection, args)
requested = Utils::QueryStringParser.parse_requested_projection(collection, args)

context.permissions.redact_projection(
collection, requested[:projection], named_by_caller: requested[:named_by_caller]
).with_pks(collection)
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,15 +28,14 @@ def setup_routes
def handle_request(args = {})
context = build(args)
context.permissions.can_chart?(args[:params])
condition_tree = ForestAdminAgent::Utils::QueryStringParser.parse_condition_tree(
context.collection, args
)
context.permissions.assert_can_read_query_fields(context.collection, condition_tree: condition_tree)
type = validate_and_get_type(args[:params][:type])
filter = Filter.new(
condition_tree: ConditionTreeFactory.intersect(
[
context.permissions.get_scope(context.collection),
ForestAdminAgent::Utils::QueryStringParser.parse_condition_tree(
context.collection, args
)
]
[context.permissions.get_scope(context.collection), condition_tree]
)
)

Expand All @@ -47,6 +46,15 @@ def handle_request(args = {})

private

# An empty `aggregateFieldName` is a count, and `can_chart?` cannot tell it from an absent
# one — `sanitize_chart_parameters` drops both before hashing. Normalising here keeps the
# aggregation and the guards that read it from disagreeing on what a count is.
def aggregate_field_name(args)
field = args[:params][:aggregateFieldName]

field.nil? || field.to_s.empty? ? nil : field
end

def validate_and_get_type(type)
chart_types = %w[Value Objective Pie Line Leaderboard]
unless chart_types.include?(type)
Expand Down Expand Up @@ -89,9 +97,13 @@ 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', aggregate_field_name(args)]]
)
aggregation = Aggregation.new(
operation: args[:params][:aggregator],
field: args[:params][:aggregateFieldName],
field: aggregate_field_name(args),
groups: group_field ? [{ field: group_field }] : []
)

Expand All @@ -102,6 +114,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', aggregate_field_name(args)]]
)
time_range = args[:params][:timeRange]
filter_only_with_values = filter.override(
condition_tree: ConditionTree::ConditionTreeFactory.intersect(
Expand All @@ -116,7 +133,7 @@ def make_line(context, filter, args)
filter_only_with_values,
Aggregation.new(
operation: args[:params][:aggregator],
field: args[:params][:aggregateFieldName],
field: aggregate_field_name(args),
groups: [{ field: group_by_field_name, operation: time_range }]
)
)
Expand Down Expand Up @@ -151,7 +168,7 @@ def make_leaderboard(context, filter, args)
leaderboard_filter = filter.nest(inverse)
aggregation = Aggregation.new(
operation: args[:params][:aggregator],
field: args[:params][:aggregateFieldName],
field: aggregate_field_name(args),
groups: [{ field: "#{inverse}:#{args[:params][:labelFieldName]}" }]
)
end
Expand All @@ -171,13 +188,25 @@ def make_leaderboard(context, filter, args)
leaderboard_filter = filter.nest(origin)
aggregation = Aggregation.new(
operation: args[:params][:aggregator],
field: args[:params][:aggregateFieldName] ? "#{target}:#{args[:params][:aggregateFieldName]}" : nil,
field: aggregate_field_name(args) ? "#{target}:#{aggregate_field_name(args)}" : nil,
groups: [{ field: "#{origin}:#{args[:params][:labelFieldName]}" }]
)
end
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?
Comment thread
hercemer42 marked this conversation as resolved.
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 +229,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', aggregate_field_name(args)]]
)
aggregation = Aggregation.new(operation: args[:params][:aggregator],
field: args[:params][:aggregateFieldName])
field: aggregate_field_name(args))
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,16 +19,26 @@ def handle_request(args = {})
context.permissions.can?(:browse, context.collection)

if context.collection.is_countable?
condition_tree = ForestAdminAgent::Utils::QueryStringParser.parse_condition_tree(
context.collection, args
)
search = QueryStringParser.parse_search(context.collection, args)
search_extended = QueryStringParser.parse_search_extended(args)
context.permissions.assert_can_read_query_fields(
context.collection,
condition_tree: condition_tree, search: search, search_extended: search_extended
)

filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new(
condition_tree: ConditionTreeFactory.intersect(
[
context.permissions.get_scope(context.collection),
parse_query_segment(context.collection, args, context.permissions, context.caller),
ForestAdminAgent::Utils::QueryStringParser.parse_condition_tree(context.collection, args)
condition_tree
]
),
search: QueryStringParser.parse_search(context.collection, args),
search_extended: QueryStringParser.parse_search_extended(args),
search: search,
search_extended: search_extended,
segment: QueryStringParser.parse_segment(context.collection, args)
)
aggregation = ForestAdminDatasourceToolkit::Components::Query::Aggregation.new(operation: 'Count')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,25 +22,38 @@ def handle_request(args = {})
context = build(args)
context.permissions.can?(:browse, context.collection)
context.permissions.can?(:export, context.collection)

condition_tree = QueryStringParser.parse_condition_tree(context.collection, args)
search = QueryStringParser.parse_search(context.collection, args)
search_extended = QueryStringParser.parse_search_extended(args)
sort = QueryStringParser.parse_sort(context.collection, args)
context.permissions.assert_can_read_query_fields(
context.collection,
condition_tree: condition_tree, sort: sort, search: search, search_extended: search_extended
)

filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new(
condition_tree: ConditionTreeFactory.intersect(
[
context.permissions.get_scope(context.collection),
parse_query_segment(context.collection, args, context.permissions, context.caller),
QueryStringParser.parse_condition_tree(
context.collection, args
)
condition_tree
]
),
search: QueryStringParser.parse_search(context.collection, args),
search_extended: QueryStringParser.parse_search_extended(args),
sort: QueryStringParser.parse_sort(context.collection, args),
search: search,
search_extended: search_extended,
sort: sort,
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 @@ -18,22 +18,31 @@ def handle_request(args = {})
context = build(args)
context.permissions.can?(:browse, context.collection)

condition_tree = QueryStringParser.parse_condition_tree(context.collection, args)
search = QueryStringParser.parse_search(context.collection, args)
search_extended = QueryStringParser.parse_search_extended(args)
sort = QueryStringParser.parse_sort(context.collection, args)
context.permissions.assert_can_read_query_fields(
context.collection,
condition_tree: condition_tree, sort: sort, search: search, search_extended: search_extended
)

filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new(
condition_tree: ConditionTreeFactory.intersect(
[
context.permissions.get_scope(context.collection),
QueryStringParser.parse_condition_tree(context.collection, args),
condition_tree,
parse_query_segment(context.collection, args, context.permissions, context.caller)
]
),
page: QueryStringParser.parse_pagination(args),
search: QueryStringParser.parse_search(context.collection, args),
search_extended: QueryStringParser.parse_search_extended(args),
sort: QueryStringParser.parse_sort(context.collection, args),
search: search,
search_extended: search_extended,
sort: sort,
segment: QueryStringParser.parse_segment(context.collection, args)
)

projection = QueryStringParser.parse_projection_with_pks(context.collection, args)
projection = redacted_projection_with_pks(context, context.collection, args)
records = context.collection.list(context.caller, filter, projection)

{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,26 @@ def handle_request(args = {})
context = build(args)
context.permissions.can?(:browse, context.child_collection)
context.permissions.can?(:export, context.child_collection)
condition_tree = ForestAdminAgent::Utils::QueryStringParser.parse_condition_tree(
context.child_collection, args
)
context.permissions.assert_can_read_query_fields(
context.child_collection, condition_tree: condition_tree
)

filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new(
condition_tree: ConditionTreeFactory.intersect(
[
context.permissions.get_scope(context.child_collection),
ForestAdminAgent::Utils::QueryStringParser.parse_condition_tree(context.child_collection, args)
]
[context.permissions.get_scope(context.child_collection), condition_tree]
)
)
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 +51,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,19 +23,22 @@ def setup_routes
def handle_request(args = {})
context = build(args)
context.permissions.can?(:browse, context.child_collection)
condition_tree = ForestAdminAgent::Utils::QueryStringParser.parse_condition_tree(
context.child_collection, args
)
sort = ForestAdminAgent::Utils::QueryStringParser.parse_sort(context.child_collection, args)
context.permissions.assert_can_read_query_fields(
context.child_collection, condition_tree: condition_tree, sort: sort
)

filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new(
condition_tree: ConditionTreeFactory.intersect(
[
context.permissions.get_scope(context.child_collection),
ForestAdminAgent::Utils::QueryStringParser.parse_condition_tree(context.child_collection, args)
]
[context.permissions.get_scope(context.child_collection), condition_tree]
),
page: ForestAdminAgent::Utils::QueryStringParser.parse_pagination(args),
sort: ForestAdminAgent::Utils::QueryStringParser.parse_sort(context.child_collection, args)
sort: sort
)
projection = ForestAdminAgent::Utils::QueryStringParser.parse_projection_with_pks(context.child_collection,
args)
projection = redacted_projection_with_pks(context, context.child_collection, args)
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,7 @@ def handle_request(args = {})
condition_tree: ConditionTree::ConditionTreeFactory.intersect([condition_tree, scope])
)

projection = QueryStringParser.parse_projection_with_pks(context.collection, args)
projection = redacted_projection_with_pks(context, context.collection, args)

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
Loading
Loading