mirror of
https://github.com/ash-project/ash.git
synced 2024-09-20 05:23:03 +12:00
fix: properly authorize access to query aggregates in all cases
This commit is contained in:
parent
ea51d4d096
commit
6e0be43c51
4 changed files with 80 additions and 26 deletions
|
@ -6,8 +6,9 @@ defmodule Ash.Actions.Aggregate do
|
||||||
query = %{query | api: api}
|
query = %{query | api: api}
|
||||||
{query, opts} = Ash.Actions.Helpers.add_process_context(query.api, query, opts)
|
{query, opts} = Ash.Actions.Helpers.add_process_context(query.api, query, opts)
|
||||||
action = query.action || Ash.Resource.Info.primary_action!(query.resource, :read)
|
action = query.action || Ash.Resource.Info.primary_action!(query.resource, :read)
|
||||||
|
opts = Keyword.put_new(opts, :read_action, action.name)
|
||||||
|
|
||||||
case validate_aggregates(query, aggregates) do
|
case validate_aggregates(query, aggregates, opts) do
|
||||||
{:ok, aggregates} ->
|
{:ok, aggregates} ->
|
||||||
aggregates
|
aggregates
|
||||||
|> Enum.group_by(fn aggregate ->
|
|> Enum.group_by(fn aggregate ->
|
||||||
|
@ -120,14 +121,19 @@ defmodule Ash.Actions.Aggregate do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp validate_aggregates(query, aggregates) do
|
defp validate_aggregates(query, aggregates, opts) do
|
||||||
aggregates
|
aggregates
|
||||||
|> Enum.reduce_while({:ok, []}, fn
|
|> Enum.reduce_while({:ok, []}, fn
|
||||||
%Ash.Query.Aggregate{} = aggregate, {:ok, aggregates} ->
|
%Ash.Query.Aggregate{} = aggregate, {:ok, aggregates} ->
|
||||||
{:cont, {:ok, [aggregate | aggregates]}}
|
{:cont, {:ok, [aggregate | aggregates]}}
|
||||||
|
|
||||||
{name, kind}, {:ok, aggregates} ->
|
{name, kind}, {:ok, aggregates} ->
|
||||||
case Ash.Query.Aggregate.new(query.resource, name, kind) do
|
case Ash.Query.Aggregate.new(
|
||||||
|
query.resource,
|
||||||
|
name,
|
||||||
|
kind,
|
||||||
|
set_opts([], opts)
|
||||||
|
) do
|
||||||
{:ok, aggregate} ->
|
{:ok, aggregate} ->
|
||||||
{:cont, {:ok, [aggregate | aggregates]}}
|
{:cont, {:ok, [aggregate | aggregates]}}
|
||||||
|
|
||||||
|
@ -135,8 +141,8 @@ defmodule Ash.Actions.Aggregate do
|
||||||
{:halt, {:error, error}}
|
{:halt, {:error, error}}
|
||||||
end
|
end
|
||||||
|
|
||||||
{name, kind, opts}, {:ok, aggregates} ->
|
{name, kind, agg_opts}, {:ok, aggregates} ->
|
||||||
case Ash.Query.Aggregate.new(query.resource, name, kind, opts) do
|
case Ash.Query.Aggregate.new(query.resource, name, kind, set_opts(agg_opts, opts)) do
|
||||||
{:ok, aggregate} ->
|
{:ok, aggregate} ->
|
||||||
{:cont, {:ok, [aggregate | aggregates]}}
|
{:cont, {:ok, [aggregate | aggregates]}}
|
||||||
|
|
||||||
|
@ -145,4 +151,9 @@ defmodule Ash.Actions.Aggregate do
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp set_opts(specified, others) do
|
||||||
|
{agg_opts, _} = Ash.Query.Aggregate.split_aggregate_opts(others)
|
||||||
|
Keyword.merge(agg_opts, specified)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -79,7 +79,7 @@ defmodule Ash.Api.Interface do
|
||||||
opts
|
opts
|
||||||
end
|
end
|
||||||
|
|
||||||
{aggregate_opts, opts} = split_aggregate_opts(opts)
|
{aggregate_opts, opts} = Ash.Query.Aggregate.split_aggregate_opts(opts)
|
||||||
|
|
||||||
case Ash.Query.Aggregate.new(query.resource, :count, :count, aggregate_opts) do
|
case Ash.Query.Aggregate.new(query.resource, :count, :count, aggregate_opts) do
|
||||||
{:ok, aggregate} ->
|
{:ok, aggregate} ->
|
||||||
|
@ -106,7 +106,7 @@ defmodule Ash.Api.Interface do
|
||||||
opts
|
opts
|
||||||
end
|
end
|
||||||
|
|
||||||
{aggregate_opts, opts} = split_aggregate_opts(opts)
|
{aggregate_opts, opts} = Ash.Query.Aggregate.split_aggregate_opts(opts)
|
||||||
|
|
||||||
case Ash.Query.Aggregate.new(query.resource, :count, :count, aggregate_opts) do
|
case Ash.Query.Aggregate.new(query.resource, :count, :count, aggregate_opts) do
|
||||||
{:ok, aggregate} ->
|
{:ok, aggregate} ->
|
||||||
|
@ -133,7 +133,7 @@ defmodule Ash.Api.Interface do
|
||||||
opts
|
opts
|
||||||
end
|
end
|
||||||
|
|
||||||
{aggregate_opts, opts} = split_aggregate_opts(opts)
|
{aggregate_opts, opts} = Ash.Query.Aggregate.split_aggregate_opts(opts)
|
||||||
|
|
||||||
case Ash.Query.Aggregate.new(query.resource, :exists, :exists, aggregate_opts) do
|
case Ash.Query.Aggregate.new(query.resource, :exists, :exists, aggregate_opts) do
|
||||||
{:ok, aggregate} ->
|
{:ok, aggregate} ->
|
||||||
|
@ -160,7 +160,7 @@ defmodule Ash.Api.Interface do
|
||||||
opts
|
opts
|
||||||
end
|
end
|
||||||
|
|
||||||
{aggregate_opts, opts} = split_aggregate_opts(opts)
|
{aggregate_opts, opts} = Ash.Query.Aggregate.split_aggregate_opts(opts)
|
||||||
|
|
||||||
case Ash.Query.Aggregate.new(query.resource, :exists, :exists, aggregate_opts) do
|
case Ash.Query.Aggregate.new(query.resource, :exists, :exists, aggregate_opts) do
|
||||||
{:ok, aggregate} ->
|
{:ok, aggregate} ->
|
||||||
|
@ -188,7 +188,7 @@ defmodule Ash.Api.Interface do
|
||||||
opts
|
opts
|
||||||
end
|
end
|
||||||
|
|
||||||
{aggregate_opts, opts} = split_aggregate_opts(opts)
|
{aggregate_opts, opts} = Ash.Query.Aggregate.split_aggregate_opts(opts)
|
||||||
|
|
||||||
case Ash.Query.Aggregate.new(
|
case Ash.Query.Aggregate.new(
|
||||||
query.resource,
|
query.resource,
|
||||||
|
@ -214,7 +214,7 @@ defmodule Ash.Api.Interface do
|
||||||
def unquote(:"#{kind}!")(query, field, opts \\ []) do
|
def unquote(:"#{kind}!")(query, field, opts \\ []) do
|
||||||
query = Ash.Query.to_query(query)
|
query = Ash.Query.to_query(query)
|
||||||
|
|
||||||
{aggregate_opts, opts} = split_aggregate_opts(opts)
|
{aggregate_opts, opts} = Ash.Query.Aggregate.split_aggregate_opts(opts)
|
||||||
|
|
||||||
opts =
|
opts =
|
||||||
if query.action do
|
if query.action do
|
||||||
|
@ -244,18 +244,6 @@ defmodule Ash.Api.Interface do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp split_aggregate_opts(opts) do
|
|
||||||
{left, right} = Keyword.split(opts, Ash.Query.Aggregate.opt_keys())
|
|
||||||
|
|
||||||
case Keyword.fetch(left, :authorize?) do
|
|
||||||
{:ok, value} ->
|
|
||||||
{left, Keyword.put(right, :authorize?, value)}
|
|
||||||
|
|
||||||
:error ->
|
|
||||||
{left, right}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def aggregate(query, aggregate_or_aggregates, opts \\ []) do
|
def aggregate(query, aggregate_or_aggregates, opts \\ []) do
|
||||||
case Api.aggregate(__MODULE__, query, aggregate_or_aggregates, opts) do
|
case Api.aggregate(__MODULE__, query, aggregate_or_aggregates, opts) do
|
||||||
{:ok, result} ->
|
{:ok, result} ->
|
||||||
|
|
|
@ -123,12 +123,22 @@ defmodule Ash.Query.Aggregate do
|
||||||
end)
|
end)
|
||||||
|
|
||||||
with {:ok, opts} <- Spark.OptionsHelpers.validate(opts, @schema) do
|
with {:ok, opts} <- Spark.OptionsHelpers.validate(opts, @schema) do
|
||||||
|
query =
|
||||||
|
opts[:query] || Ash.Query.new(Ash.Resource.Info.related(resource, opts[:path] || []))
|
||||||
|
|
||||||
|
query =
|
||||||
|
if opts[:read_action] && query.__validated_for_action__ != opts[:read_action] do
|
||||||
|
Ash.Query.for_read(query, opts[:read_action], %{})
|
||||||
|
else
|
||||||
|
query
|
||||||
|
end
|
||||||
|
|
||||||
new(
|
new(
|
||||||
resource,
|
resource,
|
||||||
name,
|
name,
|
||||||
kind,
|
kind,
|
||||||
opts[:path] || [],
|
opts[:path] || [],
|
||||||
opts[:query] || Ash.Query.new(Ash.Resource.Info.related(resource, opts[:path] || [])),
|
query,
|
||||||
opts[:field],
|
opts[:field],
|
||||||
opts[:default],
|
opts[:default],
|
||||||
Keyword.get(opts, :filterable?, true),
|
Keyword.get(opts, :filterable?, true),
|
||||||
|
@ -267,6 +277,28 @@ defmodule Ash.Query.Aggregate do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
def split_aggregate_opts(opts) do
|
||||||
|
{left, right} = Keyword.split(opts, opt_keys())
|
||||||
|
|
||||||
|
right =
|
||||||
|
case Keyword.fetch(left, :authorize?) do
|
||||||
|
{:ok, value} ->
|
||||||
|
Keyword.put(right, :authorize?, value)
|
||||||
|
|
||||||
|
:error ->
|
||||||
|
right
|
||||||
|
end
|
||||||
|
|
||||||
|
case Keyword.fetch(right, :action) do
|
||||||
|
{:ok, action} ->
|
||||||
|
{Keyword.put(left, :read_action, action), right}
|
||||||
|
|
||||||
|
:error ->
|
||||||
|
{left, right}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def default_value(:count), do: 0
|
def default_value(:count), do: 0
|
||||||
def default_value(:first), do: nil
|
def default_value(:first), do: nil
|
||||||
def default_value(:sum), do: nil
|
def default_value(:sum), do: nil
|
||||||
|
@ -476,6 +508,13 @@ defmodule Ash.Query.Aggregate do
|
||||||
request_path ++
|
request_path ++
|
||||||
[:aggregate, relationship_path, {authorize?, read_action}, :authorization_filter]
|
[:aggregate, relationship_path, {authorize?, read_action}, :authorization_filter]
|
||||||
|
|
||||||
|
action =
|
||||||
|
if read_action do
|
||||||
|
Ash.Resource.Info.action(related, read_action)
|
||||||
|
else
|
||||||
|
Ash.Resource.Info.primary_action!(related, :read)
|
||||||
|
end
|
||||||
|
|
||||||
Request.new(
|
Request.new(
|
||||||
resource: related,
|
resource: related,
|
||||||
api: initial_query.api,
|
api: initial_query.api,
|
||||||
|
@ -483,7 +522,7 @@ defmodule Ash.Query.Aggregate do
|
||||||
query: related_query,
|
query: related_query,
|
||||||
path: request_path ++ [:aggregate, relationship_path, {authorize?, read_action}],
|
path: request_path ++ [:aggregate, relationship_path, {authorize?, read_action}],
|
||||||
strict_check_only?: true,
|
strict_check_only?: true,
|
||||||
action: Ash.Resource.Info.primary_action(related, :read),
|
action: action,
|
||||||
name: "authorize aggregate: #{Enum.join(relationship_path, ".")}",
|
name: "authorize aggregate: #{Enum.join(relationship_path, ".")}",
|
||||||
data:
|
data:
|
||||||
Request.resolve([auth_filter_path], fn data ->
|
Request.resolve([auth_filter_path], fn data ->
|
||||||
|
|
|
@ -46,6 +46,7 @@ defmodule Ash.Test.Actions.AggregateTest do
|
||||||
|
|
||||||
actions do
|
actions do
|
||||||
defaults [:create, :read, :update, :destroy]
|
defaults [:create, :read, :update, :destroy]
|
||||||
|
read :unpublic
|
||||||
end
|
end
|
||||||
|
|
||||||
attributes do
|
attributes do
|
||||||
|
@ -70,9 +71,13 @@ defmodule Ash.Test.Actions.AggregateTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
policies do
|
policies do
|
||||||
policy always() do
|
policy action(:read) do
|
||||||
authorize_if expr(public == true)
|
authorize_if expr(public == true)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
policy action(:unpublic) do
|
||||||
|
authorize_if expr(public == false)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -124,6 +129,17 @@ defmodule Ash.Test.Actions.AggregateTest do
|
||||||
assert %{count: 0} = Api.aggregate!(Post, {:count, :count}, authorize?: true)
|
assert %{count: 0} = Api.aggregate!(Post, {:count, :count}, authorize?: true)
|
||||||
assert 0 = Api.count!(Post, authorize?: true)
|
assert 0 = Api.count!(Post, authorize?: true)
|
||||||
|
|
||||||
|
assert %{count: 1} =
|
||||||
|
Post
|
||||||
|
|> Ash.Query.for_read(:unpublic)
|
||||||
|
|> Api.aggregate!({:count, :count}, actor: nil)
|
||||||
|
|
||||||
|
assert %{count: 1} =
|
||||||
|
Api.aggregate!(Post, {:count, :count}, actor: nil, action: :unpublic)
|
||||||
|
|
||||||
|
assert 1 = Api.count!(Post, actor: nil, action: :unpublic)
|
||||||
|
assert 0 = Api.count!(Post, authorize?: true)
|
||||||
|
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.for_create(:create, %{title: "title", public: true})
|
|> Ash.Changeset.for_create(:create, %{title: "title", public: true})
|
||||||
|> Api.create!()
|
|> Api.create!()
|
||||||
|
|
Loading…
Reference in a new issue