This commit is contained in:
Zach Daniel 2020-01-11 00:09:52 -05:00
parent 9bb19f95ad
commit ebd8291631
No known key found for this signature in database
GPG key ID: A57053A671EE649E
34 changed files with 444 additions and 880 deletions

View file

@ -28,12 +28,12 @@ defmodule Post do
actions do
read :default,
authorization_steps: [
rules: [
authorize_if: user_is(:admin)
]
create :default,
authorization_steps: [
rules: [
authorize_if: user_is(:admin)
]
@ -151,3 +151,5 @@ end
* We need to validate incoming attributes/relationships better.
* Side load authorization testing
* Validate that params on the way in are either all strings or all atoms
* Make it `rules: :none` (or something better) than `rules: false`
* Support `read_rules`, `create_rules`, `update_rules` for attributes/relationships

View file

@ -6,17 +6,17 @@ defmodule Ash.Actions.Attributes do
attribute.name in Map.get(changeset, :__ash_skip_authorization_fields__, [])
end)
|> Enum.filter(fn attribute ->
attribute.authorization_steps != false && Map.has_key?(changeset.changes, attribute.name)
attribute.write_rules != false && Map.has_key?(changeset.changes, attribute.name)
end)
|> Enum.map(fn attribute ->
Ash.Authorization.Request.new(
api: api,
authorization_steps: attribute.authorization_steps,
rules: attribute.write_rules,
resource: resource,
changeset: changeset,
action_type: action.type,
dependencies: [[:data]],
fetcher: fn %{data: data} -> {:ok, data} end,
fetcher: fn _, %{data: data} -> {:ok, data} end,
state_key: :data,
relationship: [],
source: "change on `#{attribute.name}`"

View file

@ -59,15 +59,15 @@ defmodule Ash.Actions.Create do
create_authorization_request =
Ash.Authorization.Request.new(
api: api,
authorization_steps: action.authorization_steps,
rules: action.rules,
resource: resource,
changeset:
Ash.Actions.Relationships.authorization_changeset(
Relationships.authorization_changeset(
changeset,
relationships
),
action_type: action.type,
fetcher: fn _ ->
fetcher: fn changeset, _ ->
Ash.DataLayer.create(resource, changeset)
end,
dependencies: Map.get(changeset, :__changes_depend_on__) || [],
@ -80,7 +80,16 @@ defmodule Ash.Actions.Create do
attribute_requests =
Attributes.attribute_change_authorizations(changeset, api, resource, action)
relationship_auths = Map.get(changeset, :__authorizations__, [])
relationship_read_auths = Map.get(changeset, :__authorizations__, [])
relationship_change_auths =
Relationships.relationship_change_authorizations(
changeset,
api,
resource,
action,
relationships
)
if params[:authorization] do
strict_access? =
@ -91,7 +100,8 @@ defmodule Ash.Actions.Create do
Authorizer.authorize(
params[:authorization][:user],
[create_authorization_request | attribute_requests] ++ relationship_auths,
[create_authorization_request | attribute_requests] ++
relationship_read_auths ++ relationship_change_auths,
strict_access?: strict_access?,
log_final_report?: params[:authorization][:log_final_report?] || false
)
@ -100,7 +110,8 @@ defmodule Ash.Actions.Create do
Authorizer.authorize(
authorization[:user],
[create_authorization_request | attribute_requests] ++ relationship_auths,
[create_authorization_request | attribute_requests] ++
relationship_read_auths ++ relationship_change_auths,
fetch_only?: true
)
end

View file

@ -27,7 +27,7 @@ defmodule Ash.Actions.Destroy do
auth_request =
Ash.Authorization.Request.new(
resource: resource,
authorization_steps: action.authorization_steps,
rules: action.rules,
destroy: record,
source: "destroy request"
)

View file

@ -40,10 +40,10 @@ defmodule Ash.Actions.Read do
Ash.Authorization.Request.new(
api: api,
resource: resource,
authorization_steps: action.authorization_steps,
rules: action.rules,
filter: filter,
action_type: action.type,
fetcher: fn _ -> Ash.DataLayer.run_query(query, resource) end,
fetcher: fn _, _ -> Ash.DataLayer.run_query(query, resource) end,
must_fetch?: true,
state_key: :data,
relationship: [],

View file

@ -67,6 +67,35 @@ defmodule Ash.Actions.Relationships do
end)
end
def relationship_change_authorizations(changeset, api, resource, action, relationships) do
Enum.flat_map(relationships, fn {relationship_name, _data} ->
case Ash.relationship(resource, relationship_name) do
nil ->
[]
relationship ->
authorization =
Ash.Authorization.Request.new(
api: api,
rules: relationship.write_rules,
resource: resource,
changeset: authorization_changeset(changeset, relationships),
action_type: action.type,
fetcher: fn _, %{data: data} ->
{:ok, data}
end,
dependencies: [:data | Map.get(changeset, :__changes_depend_on__, [])],
state_key: :data,
must_fetch?: false,
relationship: [],
source: "#{relationship_name} edit"
)
[authorization]
end
end)
end
defp add_relationship_read_authorizations(changeset, api, relationship, input) do
changeset
|> add_replace_authorizations(api, relationship, input)
@ -177,13 +206,13 @@ defmodule Ash.Actions.Relationships do
authorization =
Ash.Authorization.Request.new(
api: api,
authorization_steps: default_read.authorization_steps,
rules: default_read.rules,
resource: relationship.destination,
action_type: :read,
filter: filter,
must_fetch?: true,
state_key: [:relationships, relationship_name, type],
fetcher: fn _ ->
fetcher: fn _, _ ->
case api.read(destination, filter: filter, paginate: false) do
{:ok, %{results: results}} -> {:ok, results}
{:error, error} -> {:error, error}
@ -303,48 +332,84 @@ defmodule Ash.Actions.Relationships do
end
def authorization_changeset(changeset, relationships) do
# relationships_needing_field_update =
# relationships
# |> Enum.reduce_while({:ok, []}, fn {name, data}, {:ok, relationships} ->
# case Ash.relationship(changeset.data.__struct__, name) do
# nil ->
# {:halt, {:error, name}}
# %{type: :belongs_to, destination_field: destination_field} = relationship ->
# case identifier_or_identifiers(relationship, data) do
# {:ok, identifier} ->
# if Keyword.has_key?(identifier, destination_field) do
# {:cont, {:ok, relationships}}
# else
# {:cont, {:ok, [relationship | relationships]}}
# end
# {:error, _} ->
# {:halt, {:error, name}}
# end
# _ ->
# {:cont, {:ok, relationships}}
# end
# end)
if relationships == %{} do
changeset
else
fn data ->
Enum.reduce_while(data.relationships, {:ok, changeset}, fn {relationship,
relationship_data},
{:ok, changeset} ->
data
|> Map.get(:relationships, %{})
|> Enum.reduce(changeset, fn {relationship, relationship_data}, changeset ->
relationship = Ash.relationship(changeset.data.__struct__, relationship)
case add_relationship_to_changeset(changeset, relationship, relationship_data) do
{:ok, changeset} -> {:cont, {:ok, changeset}}
{:error, error} -> {:halt, {:error, error}}
end
add_relationship_to_changeset(changeset, relationship, relationship_data)
end)
end
end
end
defp add_relationship_to_changeset(
changeset,
%{type: :belongs_to, destination: destination} = relationship,
relationship_data
) do
pkey = Ash.primary_key(destination)
case relationship_data do
%{current: [], replace: [new]} ->
changeset
|> Ecto.Changeset.put_change(
relationship.source_field,
Map.get(new, relationship.destination_field)
)
|> add_relationship_change_metadata(relationship.name, %{add: [new]})
%{current: [current], replace: []} ->
changeset
|> Ecto.Changeset.put_change(
relationship.source_field,
nil
)
|> add_relationship_change_metadata(relationship.name, %{remove: [current]})
%{current: [current], replace: [new]} ->
changeset
|> Ecto.Changeset.put_change(
relationship.source_field,
Map.get(new, relationship.destination_field)
)
|> add_relationship_change_metadata(relationship.name, %{remove: [current], add: [new]})
%{current: [current], add: [add]} ->
if Map.take(current, pkey) == Map.take(add, pkey) do
changeset
else
Ecto.Changeset.add_error(
changeset,
relationship.name,
"Can't add a value to a belongs to when something is already related."
)
end
%{current: [], remove: [_]} ->
Ecto.Changeset.add_error(
changeset,
relationship.name,
"Can't remove a value from a belongs to when nothing is related"
)
%{current: [current], remove: [remove]} ->
if Map.take(current, pkey) == Map.take(remove, pkey) do
Ecto.Changeset.put_change(changeset, relationship.source_field, nil)
else
Ecto.Changeset.add_error(
changeset,
relationship.name,
"Can't remove a related value if a different record is related"
)
end
end
end
defp add_relationship_to_changeset(changeset, relationship, relationship_data) do
IO.inspect(relationship, label: "relationship")
IO.inspect(relationship_data, label: "relationship data")
@ -352,42 +417,14 @@ defmodule Ash.Actions.Relationships do
{:ok, changeset}
end
# defp do_authorization_changeset(changeset, []) do
# changeset
# end
# defp do_authorization_changeset(
# changeset,
# relationships_being_changed
# ) do
# fn data ->
# Enum.reduce(relationships_being_changed, changeset, fn relationship, changeset ->
# end)
# # changeset
# # changeset
# # |> update_relationship_fields(data, relationships_needing_field_update)
# # |> split_relationships_being_replaced(data, relationships_being_replaced)
# end
# end
# defp update_relationship_fields(changeset, data, relationships_needing_field_update) do
# Enum.reduce(relationships_needing_field_update, changeset, fn relationship, changeset ->
# related = get_in(data, [:relationships, relationship.name, :to_add])
# if related do
# value = Map.get(related, relationship.destination_field)
# Ecto.Changeset.cast(changeset, relationship.source_field, value)
# else
# Ecto.Changeset.add_error(changeset, relationship.name, "Invalid relationship data")
# end
# end)
# end
# defp split_relationships_being_replaced(changeset, data, relationships_being_replaced) do
# # TODO: this
# changeset
# end
defp add_relationship_change_metadata(changeset, relationship_name, data) do
Map.update(
changeset,
:__ash_relationships__,
%{relationship_name => data},
&Map.put(&1, relationship_name, data)
)
end
defp fetch_string_or_atom(map, name) do
case Map.fetch(map, name) do
@ -396,101 +433,6 @@ defmodule Ash.Actions.Relationships do
end
end
# defp read_to_relate_authorization(
# api,
# %{destination: destination} = relationship,
# value
# ) do
# default_read =
# Ash.primary_action(destination, :read) ||
# raise "Need a default read action for #{destination}"
# relationship_name = relationship.name
# filter =
# case relationship.cardinality do
# :many ->
# case value do
# [single_identifier] ->
# single_identifier
# many ->
# [or: many]
# end
# :one ->
# value
# end
# Ash.Authorization.Request.new(
# api: api,
# authorization_steps: default_read.authorization_steps,
# resource: relationship.destination,
# action_type: :read,
# filter: filter,
# must_fetch?: false,
# state_key: [:relationships, relationship_name, :to_relate],
# fetcher: fn _ ->
# case api.read(destination, filter: filter, paginate: false) do
# {:ok, %{results: results}} -> {:ok, results}
# {:error, error} -> {:error, error}
# end
# end,
# relationship: [relationship.name],
# source: "read prior to write related #{relationship.name}"
# )
# end
# defp identifier_or_identifiers(relationship, data) do
# case relationship.cardinality do
# :many ->
# Ash.Actions.PrimaryKeyHelpers.values_to_primary_key_filters(
# relationship.destination,
# data
# )
# :one ->
# Ash.Actions.PrimaryKeyHelpers.value_to_primary_key_filter(
# relationship.destination,
# data
# )
# end
# end
# defp add_update_replace_authorizations(api, action, relationship, data, changeset) do
# case identifier_or_identifiers(relationship, data) do
# {:ok, identifiers} ->
# read_currently_related_authorization =
# read_currently_related_authorization(api, changeset, relationship)
# read_authorization =
# read_related_authorization(api, relationship, identifiers, :to_replace)
# changeset
# |> add_to_relationship_change_metadata(relationship, :replace, identifiers)
# |> add_authorizations(read_currently_related_authorization)
# |> add_authorizations(read_authorization)
# |> changes_depend_on([:relationships, relationship.name, :replace_result])
# {:error, error} ->
# {:error, error}
# end
# end
# defp to_replace_relationship_authorization(api, %{source: resource} = relationship, identifiers) do
# # TODO: This needs to have a state_key of `replace_result`, and this should look
# # at the `current` state for the relationship, and return %{add: add, replace: replace}
# # then in `authorization_changeset` we go through all relationships being replaced,
# # and read their `replace_results` and put them into the authorization changeset.
# # This also needs to make the required changes!
# Ash.Authorization.Request.new(
# api: api,
# authorization_steps: _read.authorization_steps,
# resource: resource,
# action_type: :update
# )
# end
defp add_relationship_currently_related_authorization(
changeset,
api,
@ -507,13 +449,13 @@ defmodule Ash.Actions.Relationships do
authorization =
Ash.Authorization.Request.new(
api: api,
authorization_steps: default_read.authorization_steps,
rules: default_read.rules,
resource: destination,
action_type: :read,
state_key: [:relationships, relationship.name, :current],
must_fetch?: true,
filter: filter,
fetcher: fn _ ->
fetcher: fn _, _ ->
case api.read(destination, filter: filter_statement) do
{:ok, %{results: results}} -> {:ok, results}
{:error, error} -> {:error, error}
@ -529,61 +471,6 @@ defmodule Ash.Actions.Relationships do
|> changes_depend_on([:relationships, relationship.name, :current])
end
# defp add_create_authorizations(api, relationship, data, changeset) do
# case identifier_or_identifiers(relationship, data) do
# {:ok, identifiers} ->
# read_authorization = read_related_authorization(api, relationship, identifiers, :to_add)
# changeset
# |> add_to_relationship_change_metadata(relationship, :add, identifiers)
# |> add_authorizations(read_authorization)
# |> add_authorizations(add_authorization)
# |> set_belongs_to_change(relationship, identifiers)
# {:error, error} ->
# {:error, error}
# end
# end
# defp add_to_relationship_change_metadata(changeset, relationship, key, identifiers) do
# changeset
# |> Map.put_new(:__ash_relationships__, %{})
# |> Map.update!(:__ash_relationships__, fn ash_relationships ->
# ash_relationships
# |> Map.put_new(relationship.name, %{})
# |> Map.update!(relationship.name, fn changes -> Map.put(changes, key, identifiers) end)
# end)
# end
# defp set_belongs_to_change(
# changeset,
# %{
# type: :belongs_to,
# source_field: source_field,
# name: name,
# destination_field: destination_field
# },
# value
# ) do
# if Keyword.has_key?(value, destination_field) do
# changeset
# |> Ecto.Changeset.put_change(
# source_field,
# Keyword.fetch!(value, destination_field)
# )
# |> Map.put_new(:__ash_skip_authorization_fields__, [])
# |> Map.update!(:__ash_skip_authorization_fields__, fn fields ->
# [source_field | fields]
# end)
# else
# changes_depend_on(changeset, [:relationships, name, :to_add])
# end
# end
# defp set_belongs_to_change(changeset, _, _) do
# changeset
# end
defp changes_depend_on(changeset, path) do
Map.update(changeset, :__changes_depend_on__, [path], fn paths -> [path | paths] end)
end
@ -592,383 +479,4 @@ defmodule Ash.Actions.Relationships do
authorizations = List.wrap(authorizations)
Map.update(changeset, :__authorizations__, authorizations, &Kernel.++(&1, authorizations))
end
# defp add_to_relationship_authorization(
# api,
# identifier,
# %{type: :has_one, name: name, destination: destination} = relationship,
# changeset
# ) do
# pkey = Ash.primary_key(destination)
# Ash.Authorization.Request.new(
# api: api,
# authorization_steps: relationship.authorization_steps,
# resource: relationship.source,
# changeset: changeset,
# action_type: :create,
# state_key: :data,
# must_fetch?: true,
# dependencies: [:data, [:relationships, name, :to_add]],
# is_fetched: fn data ->
# case data do
# %{^name => %Ecto.Association.NotLoaded{}} -> false
# _ -> true
# end
# end,
# fetcher: fn %{data: data, relationships: %{^name => %{:to_add => to_add}}} ->
# pkey_value = Keyword.take(identifier, pkey)
# related =
# Enum.find(to_add, fn to_relate ->
# to_relate
# |> Map.take(pkey)
# |> Map.to_list()
# |> Kernel.==(pkey_value)
# end)
# updated =
# api.update(related,
# attributes: %{
# relationship.destination_field => Map.get(data, relationship.source_field)
# }
# )
# case updated do
# {:ok, updated} -> {:ok, Map.put(data, relationship.name, updated)}
# {:error, error} -> {:error, error}
# end
# end,
# relationship: [],
# bypass_strict_access?: true,
# source: "Update relationship #{name}"
# )
# end
# defp add_to_relationship_authorization(
# api,
# _identifier,
# %{type: :belongs_to, name: name} = relationship,
# changeset
# ) do
# Ash.Authorization.Request.new(
# api: api,
# authorization_steps: relationship.authorization_steps,
# resource: relationship.source,
# action_type: :update,
# state_key: :data,
# is_fetched: fn data ->
# case data do
# %{^name => %Ecto.Association.NotLoaded{}} -> false
# _ -> true
# end
# end,
# must_fetch?: true,
# dependencies: [:data, [:relationships, name, :to_add]],
# bypass_strict_access?: true,
# changeset: changeset,
# fetcher: fn %{data: data, relationships: %{^name => %{:to_add => to_add}}} ->
# case to_add do
# [item] -> {:ok, Map.put(data, name, item)}
# _ -> raise "Internal relationship assumption failed."
# end
# end,
# relationship: [],
# source: "Set relationship #{relationship.name}"
# )
# end
# defp add_to_relationship_authorization(
# api,
# identifiers,
# %{destination: destination, name: name, type: :many_to_many} = relationship,
# changeset
# ) do
# Enum.flat_map(identifiers, fn identifier ->
# pkey = Ash.primary_key(destination)
# default_create =
# Ash.primary_action(relationship.through, :create) ||
# raise "Must define a default create action for #{relationship.through}"
# [
# Ash.Authorization.Request.new(
# api: api,
# authorization_steps: default_create.authorization_steps,
# resource: relationship.through,
# changeset: fn %{data: data, relationships: %{^name => %{to_add: to_add}}} ->
# pkey_value = Keyword.take(identifier, pkey)
# related =
# Enum.find(to_add, fn to_relate ->
# to_relate
# |> Map.take(pkey)
# |> Map.to_list()
# |> Kernel.==(pkey_value)
# end)
# attributes = %{
# relationship.destination_field_on_join_table =>
# Map.fetch!(related, relationship.destination_field_on_join_table),
# relationship.source_field_on_join_table =>
# Map.fetch!(data, relationship.source_field_on_join_table)
# }
# changeset = Ash.Actions.Create.changeset(api, relationship.through, attributes)
# if changeset.valid? do
# {:ok, changeset}
# else
# {:error, changeset}
# end
# end,
# action_type: :create,
# state_key: [:relationships, name, :created_join_table_rows],
# must_fetch?: true,
# dependencies: [[:relationships, name, :to_add], :data],
# fetcher: fn %{data: data, relationships: %{^name => %{to_add: to_add}}} ->
# pkey_value = Keyword.take(identifier, pkey)
# related =
# Enum.find(to_add, fn to_relate ->
# to_relate
# |> Map.take(pkey)
# |> Map.to_list()
# |> Kernel.==(pkey_value)
# end)
# attributes = %{
# relationship.destination_field_on_join_table =>
# Map.fetch!(related, relationship.destination_field),
# relationship.source_field_on_join_table =>
# Map.fetch!(data, relationship.source_field)
# }
# api.create(relationship.through, attributes: attributes)
# end,
# relationship: [],
# bypass_strict_access?: true,
# source: "Create join entry for relationship #{name}"
# ),
# Ash.Authorization.Request.new(
# api: api,
# authorization_steps: relationship.authorization_steps,
# resource: relationship.source,
# changeset: changeset,
# action_type: :create,
# state_key: :data,
# must_fetch?: true,
# dependencies: [[:relationships, name, :to_add], :data],
# is_fetched: fn data ->
# case Map.get(data, name) do
# %Ecto.Association.NotLoaded{} ->
# false
# related ->
# Enum.any?(related, fn related ->
# related
# |> Map.take(pkey)
# |> Map.to_list()
# |> Kernel.==(identifier)
# end)
# end
# end,
# fetcher: fn %{data: data, relationships: %{^name => %{to_add: to_add}}} ->
# pkey_value = Keyword.take(identifier, pkey)
# related =
# Enum.find(to_add, fn to_relate ->
# to_relate
# |> Map.take(pkey)
# |> Map.to_list()
# |> Kernel.==(pkey_value)
# end)
# data_with_related =
# Map.update!(data, name, fn
# %Ecto.Association.NotLoaded{} ->
# [related]
# items ->
# items ++ [related]
# end)
# {:ok, data_with_related}
# end,
# relationship: [],
# bypass_strict_access?: true,
# source: "Update relationship #{name}"
# )
# ]
# end)
# end
# defp add_to_relationship_authorization(
# api,
# identifiers,
# %{destination: destination, name: name, type: :has_many} = relationship,
# changeset
# ) do
# Enum.flat_map(identifiers, fn identifier ->
# pkey = Ash.primary_key(destination)
# [
# Ash.Authorization.Request.new(
# api: api,
# authorization_steps: relationship.authorization_steps,
# resource: relationship.source,
# changeset: changeset,
# action_type: :create,
# state_key: :data,
# must_fetch?: true,
# dependencies: [[:relationships, name, :to_add], :data],
# is_fetched: fn data ->
# case Map.get(data, name) do
# %Ecto.Association.NotLoaded{} ->
# false
# related ->
# Enum.any?(related, fn related ->
# related
# |> Map.take(pkey)
# |> Map.to_list()
# |> Kernel.==(identifier)
# end)
# end
# end,
# fetcher: fn %{data: data, relationships: %{^name => %{to_add: to_add}}} ->
# pkey_value = Keyword.take(identifier, pkey)
# related =
# Enum.find(to_add, fn to_relate ->
# to_relate
# |> Map.take(pkey)
# |> Map.to_list()
# |> Kernel.==(pkey_value)
# end)
# updated =
# api.update(related,
# attributes: %{
# relationship.destination_field => Map.get(data, relationship.source_field)
# }
# )
# case updated do
# {:ok, updated} ->
# updated_with_related =
# Map.update!(data, name, fn
# %Ecto.Association.NotLoaded{} ->
# [updated]
# items ->
# items ++ [updated]
# end)
# {:ok, updated_with_related}
# {:error, error} ->
# {:error, error}
# end
# end,
# relationship: [],
# bypass_strict_access?: true,
# source: "Update relationship #{name}"
# )
# ]
# end)
# end
end
# def handle_update_relationships(changeset, api, relationships_input) do
# Enum.reduce(relationships_input, changeset, fn {name, data}, changeset ->
# case Ash.relationship(changeset.data.__struct__, name) do
# nil ->
# Ecto.Changeset.add_error(changeset, name, "Invalid relationship")
# relationship ->
# cond do
# !is_map(data) ->
# add_update_replace_authorizations(api, relationship, data, changeset)
# keys_are?(data, [:add, :remove]) ->
# raise "uh oh, not there yet! 1"
# # add needs to take an action type so we can authorize it properly
# # changeset = add_update_add_authorizations(api, relationship, data, changeset)
# # add_update_remove_authorizations(api, relationships, data, changeset)
# keys_are?(data, [:add]) ->
# raise "uh oh, not there yet! 2"
# # add_update_add_authorizations(api, relationship, data, changeset)
# keys_are?(data, [:remove]) ->
# raise "uh oh, not there yet! 3"
# # add_update_remove_authorizations(api, relationships, data, changeset)
# keys_are?(data, [:replace]) ->
# add_update_replace_authorizations(api, relationship, data, changeset)
# keys_are?(data, [:add, :replace]) ->
# Ecto.Changeset.add_error(
# changeset,
# relationship.name,
# "Cannot add to a relationship and replace it at the same time."
# )
# true ->
# Ecto.Changeset.add_error(
# changeset,
# relationship.name,
# "Invalid relationship data provided 1"
# )
# end
# end
# end)
# end
# defp keys_are?(keyword, keys) do
# Enum.sort(Map.keys(keyword)) == Enum.sort(keys)
# end
# def handle_create_relationships(changeset, api, relationships_input) do
# Enum.reduce(relationships_input, changeset, fn {name, data}, changeset ->
# case Ash.relationship(changeset.data.__struct__, name) do
# nil ->
# Ecto.Changeset.add_error(changeset, name, "Invalid relationship")
# relationship ->
# cond do
# !is_map(data) ->
# add_create_authorizations(api, relationship, data, changeset)
# keys_are?(data, [:add]) ->
# add_create_authorizations(api, relationship, data[:add], changeset)
# keys_are?(data, [:replace]) ->
# add_create_authorizations(api, relationship, data[:replace], changeset)
# keys_are?(data, [:add, :replace]) ->
# Ecto.Changeset.add_error(
# changeset,
# relationship.name,
# "Cannot add to a relationship and replace it at the same time."
# )
# Keyword.has_key?(data, :remove) ->
# Ecto.Changeset.add_error(
# changeset,
# relationship.name,
# "Cannot remove from a relationship on create."
# )
# true ->
# Ecto.Changeset.add_error(
# changeset,
# relationship.name,
# "Invalid relationship data provided 2"
# )
# end
# end
# end)
# end

View file

@ -51,9 +51,9 @@ defmodule Ash.Actions.SideLoad do
auth =
Ash.Authorization.Request.new(
action_type: :read,
authorization_steps: default_read.authorization_steps,
rules: default_read.rules,
filter: filter,
fetcher: fn _ ->
fetcher: fn _, _ ->
case api.read(resource, filter: filter, paginate: false) do
{:ok, %{results: results}} -> {:ok, results}
{:error, error} -> {:error, error}

View file

@ -59,14 +59,14 @@ defmodule Ash.Actions.Update do
update_authorization_request =
Ash.Authorization.Request.new(
api: api,
authorization_steps: action.authorization_steps,
rules: action.rules,
changeset:
Ash.Actions.Relationships.authorization_changeset(
changeset,
relationships
),
action_type: action.type,
fetcher: fn _ ->
fetcher: fn changeset, _ ->
Ash.DataLayer.update(resource, changeset)
end,
dependencies: Map.get(changeset, :__changes_depend_on__) || [],

View file

@ -2,7 +2,7 @@ defmodule Ash.Authorization do
@moduledoc """
#TODO: Explain authorization
Authorization in Ash is done via declaring `authorization_steps` for actions,
Authorization in Ash is done via declaring `rules` for actions,
and in the case of stateful actions, via declaring `authoriation_steps` on attributes
and relationships.

View file

@ -22,32 +22,35 @@ defmodule Ash.Authorization.Authorizer do
def authorize(user, requests, opts \\ []) do
strict_access? = Keyword.get(opts, :strict_access?, true)
if opts[:fetch_only?] do
fetch_must_fetch(requests, %{})
else
case Enum.find(requests, fn request -> Enum.empty?(request.authorization_steps) end) do
nil ->
{new_requests, facts} = strict_check_facts(user, requests, strict_access?)
solve(
new_requests,
user,
facts,
facts,
%{user: user},
strict_access?,
opts[:log_final_report?] || false
)
request ->
exception = Ash.Error.Forbidden.exception(no_steps_configured: request)
if opts[:log_final_report?] do
Logger.info(Ash.Error.Forbidden.report_text(exception))
end
{:error, exception}
requests =
if opts[:fetch_only?] do
Enum.map(requests, &Request.authorize_always/1)
else
requests
end
case Enum.find(requests, fn request -> Enum.empty?(request.rules) end) do
nil ->
{new_requests, facts} = strict_check_facts(user, requests, strict_access?)
solve(
new_requests,
user,
facts,
facts,
%{user: user},
strict_access?,
opts[:log_final_report?] || false
)
request ->
exception = Ash.Error.Forbidden.exception(no_steps_configured: request)
if opts[:log_final_report?] do
Logger.info(Ash.Error.Forbidden.report_text(exception))
end
{:error, exception}
end
end
@ -55,42 +58,63 @@ defmodule Ash.Authorization.Authorizer do
requests,
user,
facts,
strict_check_facts,
initial_strict_check_facts,
state,
strict_access?,
log_final_report?
) do
case sat_solver(requests, facts, [], state) do
{:error, :unsatisfiable} ->
exception =
Ash.Error.Forbidden.exception(
requests: requests,
facts: facts,
strict_check_facts: strict_check_facts,
strict_access?: strict_access?,
state: state
)
if log_final_report? do
Logger.info(Ash.Error.Forbidden.report_text(exception))
requests_with_changeset =
Enum.reduce_while(requests, {:ok, []}, fn request, {:ok, requests} ->
if Request.dependencies_met?(state, request) do
case Request.fetch_changeset(state, request) do
{:ok, request} -> {:cont, {:ok, [request | requests]}}
{:error, error} -> {:halt, {:error, error}}
end
else
{:cont, {:ok, [request | requests]}}
end
end)
{:error, exception}
case requests_with_changeset do
{:error, error} ->
{:error, error}
{:ok, scenario} ->
requests
|> get_all_scenarios(scenario, facts, state)
|> Enum.uniq()
|> remove_irrelevant_clauses()
|> verify_scenarios(
user,
requests,
facts,
strict_check_facts,
state,
strict_access?,
log_final_report?
)
{:ok, requests_with_changeset} ->
{new_requests, new_facts} =
strict_check_facts(user, requests_with_changeset, strict_access?, facts)
case sat_solver(new_requests, new_facts, [], state) do
{:error, :unsatisfiable} ->
exception =
Ash.Error.Forbidden.exception(
requests: new_requests,
facts: new_facts,
strict_check_facts: initial_strict_check_facts,
strict_access?: strict_access?,
state: state
)
if log_final_report? do
Logger.info(Ash.Error.Forbidden.report_text(exception))
end
{:error, exception}
{:ok, scenario} ->
new_requests
|> get_all_scenarios(scenario, new_facts, state)
|> Enum.uniq()
|> remove_irrelevant_clauses()
|> verify_scenarios(
user,
new_requests,
new_facts,
initial_strict_check_facts,
state,
strict_access?,
log_final_report?
)
end
end
end
@ -353,8 +377,8 @@ defmodule Ash.Authorization.Authorizer do
end)
end
defp strict_check_facts(user, requests, strict_access?) do
Enum.reduce(requests, {[], %{true: true, false: false}}, fn request, {requests, facts} ->
defp strict_check_facts(user, requests, strict_access?, initial \\ %{true: true, false: false}) do
Enum.reduce(requests, {[], initial}, fn request, {requests, facts} ->
{new_request, new_facts} =
Ash.Authorization.Checker.strict_check(user, request, facts, strict_access?)

View file

@ -21,7 +21,7 @@ defmodule Ash.Authorization.Check.RelatingToUser do
def strict_check_relating?(pkey, pkey_value, changeset, opts) do
case Map.fetch(changeset.__ash_relationships__, opts[:relationship_name]) do
{:ok, %{add: relationship_change}} when is_list(relationship_change) ->
{:ok, %{add: adding}} ->
op =
if opts[:allow_additional?] do
:any?
@ -29,26 +29,16 @@ defmodule Ash.Authorization.Check.RelatingToUser do
:all?
end
relationship_change =
if Keyword.keyword?(relationship_change) do
[relationship_change]
else
relationship_change
end
found? =
apply(Enum, op, [
relationship_change,
adding,
fn relationship_change ->
Keyword.take(relationship_change, pkey) == pkey_value
Map.take(relationship_change, pkey) == Enum.into(pkey_value, %{})
end
])
found?
%{add: nil} ->
false
_ ->
false
end

View file

@ -15,7 +15,7 @@ defmodule Ash.Authorization.Checker do
def strict_check(user, request, facts, strict_access?) do
if Request.can_strict_check?(request) do
new_facts =
request.authorization_steps
request.rules
|> Enum.reduce(facts, fn {_step, clause}, facts ->
case Map.fetch(facts, {request.relationship, clause}) do
{:ok, _boolean_result} ->
@ -52,9 +52,9 @@ defmodule Ash.Authorization.Checker do
:all_scenarios_known
{[], _clauses_requiring_fetch} ->
case fetch_requests(requests, state, facts, strict_access?, user) do
{:ok, {new_requests, new_facts, new_state}} ->
{:ok, new_requests, new_facts, new_state}
case fetch_requests(requests, state, strict_access?) do
{:ok, {new_requests, new_state}} ->
{:ok, new_requests, facts, new_state}
:all_scenarios_known ->
:all_scenarios_known
@ -79,7 +79,7 @@ defmodule Ash.Authorization.Checker do
end
# TODO: We could be smart here, and likely fetch multiple requests at a time
defp fetch_requests(requests, state, facts, strict_access?, user) do
defp fetch_requests(requests, state, strict_access?) do
{fetchable_requests, other_requests} =
Enum.split_with(requests, fn request ->
bypass_strict? =
@ -93,7 +93,7 @@ defmodule Ash.Authorization.Checker do
Request.dependencies_met?(state, request)
end)
requests_with_changesets =
fetchable_requests_with_changeset =
Enum.reduce_while(fetchable_requests, {:ok, []}, fn request, {:ok, requests} ->
case Request.fetch_changeset(state, request) do
{:ok, request} -> {:cont, {:ok, [request | requests]}}
@ -101,39 +101,31 @@ defmodule Ash.Authorization.Checker do
end
end)
case requests_with_changesets do
case fetchable_requests_with_changeset do
{:error, error} ->
{:error, error}
{:ok, requests_with_changesets} ->
requests_with_changesets
{:ok, fetchable_requests_with_changeset} ->
fetchable_requests_with_changeset
|> Enum.sort_by(fn request ->
# Requests that bypass strict access should generally perform well
# as they would generally be more efficient checks
{request.strict_check_completed?, Enum.count(request.relationship),
not request.bypass_strict_access?, request.relationship}
end)
|> Enum.reduce({[], facts}, fn request, {requests, facts} ->
{request, new_facts} = strict_check(user, request, facts, strict_access?)
{[request | requests], new_facts}
{Enum.count(request.relationship), not request.bypass_strict_access?,
request.relationship}
end)
|> case do
{[request | rest] = requests, new_facts} ->
[request | rest] = requests ->
case Request.fetch(state, request) do
{:ok, new_state} ->
new_requests = [%{request | is_fetched: true} | rest] ++ other_requests
{:ok, {new_requests, new_facts, new_state}}
{:ok, {new_requests, new_state}}
:error ->
{:ok, {requests ++ other_requests, new_facts, state}}
{:ok, {requests ++ other_requests, state}}
end
{[], new_facts} ->
if new_facts == facts do
:all_scenarios_known
else
{:ok, {other_requests, new_facts, state}}
end
_ ->
:all_scenarios_known
end
end
end
@ -186,7 +178,7 @@ defmodule Ash.Authorization.Checker do
Enum.split_with(clauses, fn clause ->
Enum.any?(requests, fn request ->
Request.fetched?(state, request) && Request.contains_clause?(request, clause) &&
Request.dependencies_met?(state, request)
Request.dependencies_met?(state, request) && Request.changeset_fetched?(request)
end)
end)
end
@ -199,7 +191,7 @@ defmodule Ash.Authorization.Checker do
|> Enum.map(&elem(&1, 0))
end)
|> Enum.reject(fn clause ->
Map.has_key?(facts, clause)
match?({:ok, _}, Ash.Authorization.Clause.find(facts, clause))
end)
end

View file

@ -11,10 +11,19 @@ defmodule Ash.Authorization.Clause do
}
end
# TODO: Should we for sure special case this? I see no reason not to.
def put_new_fact(facts, _rel, _resource, {Ash.Authorization.Clause.Static, _}, _) do
facts
end
def put_new_fact(facts, rel, resource, {mod, opts}, value, pkey \\ nil) do
Map.put(facts, new(rel, resource, {mod, opts}, pkey), value)
end
def find(_clauses, %{check_module: Ash.Authorization.Check.Static, check_opts: check_opts}) do
{:ok, check_opts[:result]}
end
def find(clauses, clause) do
case Map.fetch(clauses, %{clause | pkey: nil}) do
{:ok, value} ->

View file

@ -25,7 +25,12 @@ defmodule Ash.Authorization.Report do
explained_steps =
case report.state do
%{data: data} when data not in [[], nil] ->
explain_steps_with_data(report.requests, report.facts, data, report.strict_access?)
explain_steps_with_data(
report.requests,
report.facts,
List.wrap(data),
report.strict_access?
)
_ ->
if report.strict_access? do
@ -86,8 +91,8 @@ defmodule Ash.Authorization.Report do
inner_title
end
authorization_steps_legend =
request.authorization_steps
rules_legend =
request.rules
|> Enum.with_index()
|> Enum.map_join("\n", fn {{step, check}, index} ->
"#{index + 1}| " <>
@ -109,10 +114,10 @@ defmodule Ash.Authorization.Report do
end)
|> add_header_line(indent("Record"))
|> pad()
|> add_step_info(request.authorization_steps, facts)
|> add_step_info(request.rules, facts)
full_inner_title <>
":\n" <> indent(authorization_steps_legend <> "\n\n" <> data_info <> "\n")
":\n" <> indent(rules_legend <> "\n\n" <> data_info <> "\n")
end)
title <> indent(contents)
@ -281,7 +286,7 @@ defmodule Ash.Authorization.Report do
end
contents =
request.authorization_steps
request.rules
|> Enum.sort_by(fn {_step, clause} ->
{Enum.count(clause.relationship), clause.relationship}
end)

View file

@ -1,7 +1,7 @@
defmodule Ash.Authorization.Request do
defstruct [
:resource,
:authorization_steps,
:rules,
:filter,
:action_type,
:dependencies,
@ -20,7 +20,7 @@ defmodule Ash.Authorization.Request do
@type t :: %__MODULE__{
action_type: atom,
resource: Ash.resource(),
authorization_steps: list(term),
rules: list(term),
filter: Ash.Filter.t(),
changeset: Ecto.Changeset.t(),
dependencies: list(term),
@ -40,12 +40,12 @@ defmodule Ash.Authorization.Request do
opts =
opts
|> Keyword.put_new(:relationship, [])
|> Keyword.put_new(:authorization_steps, [])
|> Keyword.put_new(:rules, [])
|> Keyword.put_new(:bypass_strict_access?, false)
|> Keyword.put_new(:dependencies, [])
|> Keyword.put_new(:strict_check_completed?, false)
|> Keyword.put_new(:is_fetched, fn _ -> true end)
|> Keyword.update!(:authorization_steps, fn steps ->
|> Keyword.update!(:rules, fn steps ->
Enum.map(steps, fn {step, fact} ->
{step, Ash.Authorization.Clause.new(opts[:relationship] || [], opts[:resource], fact)}
end)
@ -54,8 +54,23 @@ defmodule Ash.Authorization.Request do
struct!(__MODULE__, opts)
end
def authorize_always(request) do
%{
request
| rules: [
authorize_if:
Ash.Authorization.Clause.new(
request.relationship,
request.resource,
{Ash.Authorization.Check.Static, result: true}
)
]
}
end
def can_strict_check?(%{changeset: changeset}) when is_function(changeset), do: false
def can_strict_check?(_), do: true
def can_strict_check?(%{strict_check_completed?: false}), do: true
def can_strict_check?(_), do: false
def dependencies_met?(_state, %{dependencies: []}), do: true
def dependencies_met?(_state, %{dependencies: nil}), do: true
@ -70,7 +85,7 @@ defmodule Ash.Authorization.Request do
end
def contains_clause?(request, clause) do
Enum.any?(request.authorization_steps, fn {_step, request_clause} ->
Enum.any?(request.rules, fn {_step, request_clause} ->
clause == request_clause
end)
end
@ -111,14 +126,17 @@ defmodule Ash.Authorization.Request do
fetch_nested_value(state, key)
end
def fetch(state, %{fetcher: fetcher, dependencies: dependencies} = request) do
arg =
def fetch(
state,
%{fetcher: fetcher, dependencies: dependencies, changeset: changeset} = request
) do
fetcher_state =
Enum.reduce(dependencies, %{}, fn dependency, acc ->
{:ok, value} = fetch_nested_value(state, dependency)
put_nested_key(acc, dependency, value)
end)
case fetcher.(arg) do
case fetcher.(changeset, fetcher_state) do
{:ok, value} ->
{:ok, put_request_state(state, request, value)}
@ -127,6 +145,9 @@ defmodule Ash.Authorization.Request do
end
end
def changeset_fetched?(%{changeset: changeset}) when is_function(changeset), do: false
def changeset_fetched?(%{changeset: _}), do: true
def fetch_changeset(state, %{dependencies: dependencies, changeset: changeset} = request)
when is_function(changeset) do
arg =
@ -136,6 +157,9 @@ defmodule Ash.Authorization.Request do
end)
case changeset.(arg) do
%Ecto.Changeset{} = new_changeset ->
{:ok, %{request | changeset: new_changeset}}
{:ok, new_changeset} ->
{:ok, %{request | changeset: new_changeset}}

View file

@ -3,18 +3,17 @@ defmodule Ash.Authorization.SatSolver do
def solve(requests, facts, negations, ids) when is_nil(ids) do
requests
|> Enum.map(&Map.get(&1, :authorization_steps))
|> Enum.map(&Map.get(&1, :rules))
|> build_requirements_expression(facts, nil)
|> add_negations_and_solve(negations)
end
def solve(requests, facts, negations, ids) do
sets_of_authorization_steps = Enum.map(requests, &Map.get(&1, :authorization_steps))
sets_of_rules = Enum.map(requests, &Map.get(&1, :rules))
ids
|> Enum.reduce(nil, fn id, expr ->
requirements_expression =
build_requirements_expression(sets_of_authorization_steps, facts, id)
requirements_expression = build_requirements_expression(sets_of_rules, facts, id)
if expr do
{:and, expr, requirements_expression}
@ -75,15 +74,15 @@ defmodule Ash.Authorization.SatSolver do
end)
end
defp build_requirements_expression(sets_of_authorization_steps, facts, pkey) do
authorization_steps_expression =
Enum.reduce(sets_of_authorization_steps, nil, fn authorization_steps, acc ->
defp build_requirements_expression(sets_of_rules, facts, pkey) do
rules_expression =
Enum.reduce(sets_of_rules, nil, fn rules, acc ->
case acc do
nil ->
compile_authorization_steps_expression(authorization_steps, facts, pkey)
compile_rules_expression(rules, facts, pkey)
expr ->
{:and, expr, compile_authorization_steps_expression(authorization_steps, facts, pkey)}
{:and, expr, compile_rules_expression(rules, facts, pkey)}
end
end)
@ -99,9 +98,9 @@ defmodule Ash.Authorization.SatSolver do
facts_expression = facts_to_statement(facts)
if facts_expression do
{:and, facts_expression, authorization_steps_expression}
{:and, facts_expression, rules_expression}
else
authorization_steps_expression
rules_expression
end
end
@ -118,7 +117,7 @@ defmodule Ash.Authorization.SatSolver do
defp solutions_to_predicate_values({:error, error}, _), do: {:error, error}
defp compile_authorization_steps_expression([{:authorize_if, clause}], facts, pkey) do
defp compile_rules_expression([{:authorize_if, clause}], facts, pkey) do
clause = %{clause | pkey: pkey}
case Clause.find(facts, clause) do
@ -130,7 +129,7 @@ defmodule Ash.Authorization.SatSolver do
end
end
defp compile_authorization_steps_expression([{:authorize_if, clause} | rest], facts, pkey) do
defp compile_rules_expression([{:authorize_if, clause} | rest], facts, pkey) do
clause = %{clause | pkey: pkey}
case Clause.find(facts, clause) do
@ -138,21 +137,20 @@ defmodule Ash.Authorization.SatSolver do
true
{:ok, false} ->
compile_authorization_steps_expression(rest, facts, pkey)
compile_rules_expression(rest, facts, pkey)
{:ok, :irrelevant} ->
true
{:ok, :unknowable} ->
compile_authorization_steps_expression(rest, facts, pkey)
compile_rules_expression(rest, facts, pkey)
:error ->
{:or, Clause.expression(clause),
compile_authorization_steps_expression(rest, facts, pkey)}
{:or, Clause.expression(clause), compile_rules_expression(rest, facts, pkey)}
end
end
defp compile_authorization_steps_expression([{:authorize_unless, clause}], facts, pkey) do
defp compile_rules_expression([{:authorize_unless, clause}], facts, pkey) do
clause = %{clause | pkey: pkey}
case Clause.find(facts, clause) do
@ -173,12 +171,12 @@ defmodule Ash.Authorization.SatSolver do
end
end
defp compile_authorization_steps_expression([{:authorize_unless, clause} | rest], facts, pkey) do
defp compile_rules_expression([{:authorize_unless, clause} | rest], facts, pkey) do
clause = %{clause | pkey: pkey}
case Clause.find(facts, clause) do
{:ok, true} ->
compile_authorization_steps_expression(rest, facts, pkey)
compile_rules_expression(rest, facts, pkey)
{:ok, false} ->
true
@ -187,19 +185,18 @@ defmodule Ash.Authorization.SatSolver do
true
{:ok, :unknowable} ->
compile_authorization_steps_expression(rest, facts, pkey)
compile_rules_expression(rest, facts, pkey)
:error ->
{:or, {:not, Clause.expression(clause)},
compile_authorization_steps_expression(rest, facts, pkey)}
{:or, {:not, Clause.expression(clause)}, compile_rules_expression(rest, facts, pkey)}
end
end
defp compile_authorization_steps_expression([{:forbid_if, _clause}], _facts, _) do
defp compile_rules_expression([{:forbid_if, _clause}], _facts, _) do
false
end
defp compile_authorization_steps_expression([{:forbid_if, clause} | rest], facts, pkey) do
defp compile_rules_expression([{:forbid_if, clause} | rest], facts, pkey) do
clause = %{clause | pkey: pkey}
case Clause.find(facts, clause) do
@ -207,30 +204,29 @@ defmodule Ash.Authorization.SatSolver do
false
{:ok, :irrelevant} ->
compile_authorization_steps_expression(rest, facts, pkey)
compile_rules_expression(rest, facts, pkey)
{:ok, :unknowable} ->
false
{:ok, false} ->
compile_authorization_steps_expression(rest, facts, pkey)
compile_rules_expression(rest, facts, pkey)
:error ->
{:and, {:not, Clause.expression(clause)},
compile_authorization_steps_expression(rest, facts, pkey)}
{:and, {:not, Clause.expression(clause)}, compile_rules_expression(rest, facts, pkey)}
end
end
defp compile_authorization_steps_expression([{:forbid_unless, _clause}], _facts, _id) do
defp compile_rules_expression([{:forbid_unless, _clause}], _facts, _id) do
false
end
defp compile_authorization_steps_expression([{:forbid_unless, clause} | rest], facts, pkey) do
defp compile_rules_expression([{:forbid_unless, clause} | rest], facts, pkey) do
clause = %{clause | pkey: pkey}
case Clause.find(facts, clause) do
{:ok, true} ->
compile_authorization_steps_expression(rest, facts, pkey)
compile_rules_expression(rest, facts, pkey)
{:ok, false} ->
false
@ -242,8 +238,7 @@ defmodule Ash.Authorization.SatSolver do
false
:error ->
{:and, Clause.expression(clause),
compile_authorization_steps_expression(rest, facts, pkey)}
{:and, Clause.expression(clause), compile_rules_expression(rest, facts, pkey)}
end
end

View file

@ -65,9 +65,9 @@ defmodule Ash.Filter do
Ash.Authorization.Request.new(
resource: resource,
api: api,
authorization_steps: Ash.primary_action(resource, :read).authorization_steps,
rules: Ash.primary_action(resource, :read).rules,
filter: parsed_filter,
fetcher: fn _ ->
fetcher: fn _, _ ->
query = Ash.DataLayer.resource_to_query(resource)
case Ash.DataLayer.filter(query, parsed_filter, resource) do

View file

@ -128,7 +128,7 @@ defmodule Ash.Resource do
Ash.Resource.Attributes.Attribute.new(mod, :id, :uuid,
primary_key?: true,
default: &Ecto.UUID.generate/0,
authorization_steps: false
write_rules: false
)
Module.put_attribute(mod, :attributes, attribute)
@ -140,7 +140,7 @@ defmodule Ash.Resource do
{:ok, attribute} =
Ash.Resource.Attributes.Attribute.new(mod, opts[:field], opts[:type],
primary_key?: true,
authorization_steps: false
write_rules: false
)
Module.put_attribute(mod, :attributes, attribute)

View file

@ -1,28 +1,28 @@
defmodule Ash.Resource.Actions.Create do
@moduledoc "The representation of a `create` action."
defstruct [:type, :name, :primary?, :authorization_steps]
defstruct [:type, :name, :primary?, :rules]
@type t :: %__MODULE__{
type: :create,
name: atom,
primary?: boolean,
authorization_steps: Authorizer.steps()
rules: Authorizer.steps()
}
@opt_schema Ashton.schema(
opts: [
primary?: :boolean,
authorization_steps: :keyword
rules: :keyword
],
defaults: [
primary?: false,
authorization_steps: []
rules: []
],
describe: [
primary?:
"Whether or not this action should be used when no action is specified by the caller.",
# TODO: doc better
authorization_steps: "A list of authorization steps"
rules: "A list of authorization steps"
]
)
@ -33,8 +33,8 @@ defmodule Ash.Resource.Actions.Create do
def new(resource, name, opts \\ []) do
case Ashton.validate(opts, @opt_schema) do
{:ok, opts} ->
authorization_steps =
case opts[:authorization_steps] do
rules =
case opts[:rules] do
false ->
false
@ -53,7 +53,7 @@ defmodule Ash.Resource.Actions.Create do
name: name,
type: :create,
primary?: opts[:primary?],
authorization_steps: authorization_steps
rules: rules
}}
{:error, error} ->

View file

@ -1,29 +1,29 @@
defmodule Ash.Resource.Actions.Destroy do
@moduledoc "The representation of a `destroy` action"
defstruct [:type, :name, :primary?, :authorization_steps]
defstruct [:type, :name, :primary?, :rules]
@type t :: %__MODULE__{
type: :destroy,
name: atom,
primary?: boolean,
authorization_steps: Authorization.steps()
rules: Authorization.steps()
}
@opt_schema Ashton.schema(
opts: [
primary?: :boolean,
authorization_steps: :keyword
rules: :keyword
],
defaults: [
primary?: false,
authorization_steps: []
rules: []
],
describe: [
primary?:
"Whether or not this action should be used when no action is specified by the caller.",
# TODO: doc better
authorization_steps: "A list of authorization steps"
rules: "A list of authorization steps"
]
)
@ -35,8 +35,8 @@ defmodule Ash.Resource.Actions.Destroy do
# Don't call functions on the resource! We don't want it to compile here
case Ashton.validate(opts, @opt_schema) do
{:ok, opts} ->
authorization_steps =
case opts[:authorization_steps] do
rules =
case opts[:rules] do
false ->
false
@ -55,7 +55,7 @@ defmodule Ash.Resource.Actions.Destroy do
name: name,
type: :destroy,
primary?: opts[:primary?],
authorization_steps: authorization_steps
rules: rules
}}
{:error, error} ->

View file

@ -1,26 +1,26 @@
defmodule Ash.Resource.Actions.Read do
@moduledoc "The representation of a `read` action"
defstruct [:type, :name, :primary?, :authorization_steps, :paginate?]
defstruct [:type, :name, :primary?, :rules, :paginate?]
@type t :: %__MODULE__{
type: :read,
name: atom,
primary?: boolean,
paginate?: boolean,
authorization_steps: Authorization.steps()
rules: Authorization.steps()
}
@opt_schema Ashton.schema(
opts: [
primary?: :boolean,
paginate?: :boolean,
authorization_steps: :keyword
rules: :keyword
],
defaults: [
primary?: false,
paginate?: true,
authorization_steps: []
rules: []
],
describe: [
primary?:
@ -28,7 +28,7 @@ defmodule Ash.Resource.Actions.Read do
paginate?:
"If false, a page is still returned from a read action, but no limit or offset is performed.",
# TODO: doc better
authorization_steps: "A list of authorization steps"
rules: "A list of authorization steps"
]
)
@ -40,8 +40,8 @@ defmodule Ash.Resource.Actions.Read do
# Don't call functions on the resource! We don't want it to compile here
case Ashton.validate(opts, @opt_schema) do
{:ok, opts} ->
authorization_steps =
case opts[:authorization_steps] do
rules =
case opts[:rules] do
false ->
false
@ -60,7 +60,7 @@ defmodule Ash.Resource.Actions.Read do
name: name,
type: :read,
primary?: opts[:primary?],
authorization_steps: authorization_steps,
rules: rules,
paginate?: opts[:paginate?]
}}

View file

@ -1,29 +1,29 @@
defmodule Ash.Resource.Actions.Update do
@moduledoc "The representation of a `update` action"
defstruct [:type, :name, :primary?, :authorization_steps]
defstruct [:type, :name, :primary?, :rules]
@type t :: %__MODULE__{
type: :update,
name: atom,
primary?: boolean,
authorization_steps: Authorization.steps()
rules: Authorization.steps()
}
@opt_schema Ashton.schema(
opts: [
primary?: :boolean,
authorization_steps: :keyword
rules: :keyword
],
defaults: [
primary?: false,
authorization_steps: []
rules: []
],
describe: [
primary?:
"Whether or not this action should be used when no action is specified by the caller.",
# TODO: doc better
authorization_steps: "A list of authorization steps"
rules: "A list of authorization steps"
]
)
@ -35,8 +35,8 @@ defmodule Ash.Resource.Actions.Update do
# Don't call functions on the resource! We don't want it to compile here
case Ashton.validate(opts, @opt_schema) do
{:ok, opts} ->
authorization_steps =
case opts[:authorization_steps] do
rules =
case opts[:rules] do
false ->
false
@ -55,7 +55,7 @@ defmodule Ash.Resource.Actions.Update do
name: name,
type: :update,
primary?: opts[:primary?],
authorization_steps: authorization_steps
rules: rules
}}
{:error, error} ->

View file

@ -1,21 +1,21 @@
defmodule Ash.Resource.Attributes.Attribute do
@doc false
defstruct [:name, :type, :allow_nil?, :primary_key?, :default, :authorization_steps]
defstruct [:name, :type, :allow_nil?, :primary_key?, :default, :write_rules]
@type t :: %__MODULE__{
name: atom(),
type: Ash.type(),
primary_key?: boolean(),
default: (() -> term),
authorization_steps: Keyword.t()
write_rules: Keyword.t()
}
@schema Ashton.schema(
opts: [
primary_key?: :boolean,
allow_nil?: :boolean,
authorization_steps: [{:const, false}, :keyword],
write_rules: [{:const, false}, :keyword],
default: [
{:function, 0},
{:tuple, {:module, :atom}},
@ -25,7 +25,7 @@ defmodule Ash.Resource.Attributes.Attribute do
defaults: [
primary_key?: false,
allow_nil?: true,
authorization_steps: []
write_rules: []
],
describe: [
allow_nil?: """
@ -36,9 +36,9 @@ defmodule Ash.Resource.Attributes.Attribute do
"Whether this field is, or is part of, the primary key of a resource.",
default:
"A one argument function that returns a default value, an mfa that does the same, or a raw value via specifying `{:constant, value}`.",
authorization_steps: """
Rules applied on an attribute during create or update. If no rules are defined, authorization to change will fail.
If set to false, no rules are applied and any changes are allowed (assuming the action was authorized as a whole)
write_rules: """
Write_Rules applied on an attribute during create or update. If no write_rules are defined, authorization to change will fail.
If set to false, no write_rules are applied and any changes are allowed (assuming the action was authorized as a whole)
"""
]
)
@ -51,8 +51,8 @@ defmodule Ash.Resource.Attributes.Attribute do
# Don't call functions on the resource! We don't want it to compile here
with {:ok, opts} <- Ashton.validate(opts, @schema),
{:default, {:ok, default}} <- {:default, cast_default(type, opts)} do
authorization_steps =
case opts[:authorization_steps] do
write_rules =
case opts[:write_rules] do
false ->
false
@ -72,7 +72,7 @@ defmodule Ash.Resource.Attributes.Attribute do
%__MODULE__{
name: name,
type: type,
authorization_steps: authorization_steps,
write_rules: write_rules,
allow_nil?: opts[:allow_nil?],
primary_key?: opts[:primary_key?],
default: default

View file

@ -12,7 +12,7 @@ defmodule Ash.Resource.Relationships.BelongsTo do
:source_field,
:source,
:reverse_relationship,
:authorization_steps
:write_rules
]
@type t :: %__MODULE__{
@ -26,7 +26,7 @@ defmodule Ash.Resource.Relationships.BelongsTo do
field_type: Ash.Type.t(),
destination_field: atom,
source_field: atom | nil,
authorization_steps: Keyword.t()
write_rules: Keyword.t()
}
@opt_schema Ashton.schema(
@ -37,14 +37,14 @@ defmodule Ash.Resource.Relationships.BelongsTo do
define_field?: :boolean,
field_type: :atom,
reverse_relationship: :atom,
authorization_steps: :keyword
write_rules: :keyword
],
defaults: [
destination_field: :id,
primary_key?: false,
define_field?: true,
field_type: :uuid,
authorization_steps: []
write_rules: []
],
describe: [
reverse_relationship:
@ -58,7 +58,7 @@ defmodule Ash.Resource.Relationships.BelongsTo do
"The field on this resource that should match the `destination_field` on the related resource. Default: [relationship_name]_id",
primary_key?:
"Whether this field is, or is part of, the primary key of a resource.",
authorization_steps: """
write_rules: """
Steps applied on an relationship during create or update. If no steps are defined, authorization to change will fail.
If set to false, no steps are applied and any changes are allowed (assuming the action was authorized as a whole)
Remember that any changes against the destination records *will* still be authorized regardless of this setting.
@ -79,8 +79,8 @@ defmodule Ash.Resource.Relationships.BelongsTo do
# Don't call functions on the resource! We don't want it to compile here
case Ashton.validate(opts, @opt_schema) do
{:ok, opts} ->
authorization_steps =
case opts[:authorization_steps] do
write_rules =
case opts[:write_rules] do
false ->
false
@ -99,7 +99,7 @@ defmodule Ash.Resource.Relationships.BelongsTo do
{:ok,
%__MODULE__{
name: name,
authorization_steps: authorization_steps,
write_rules: write_rules,
source: resource,
type: :belongs_to,
cardinality: :one,

View file

@ -6,17 +6,17 @@ defmodule Ash.Resource.Relationships.HasMany do
:destination,
:destination_field,
:source_field,
:authorization_steps,
:write_rules,
:source,
:reverse_relationship,
:authorization_steps
:write_rules
]
@type t :: %__MODULE__{
type: :has_many,
cardinality: :many,
source: Ash.resource(),
authorization_steps: Keyword.t(),
write_rules: Keyword.t(),
name: atom,
type: Ash.Type.t(),
destination: Ash.resource(),
@ -29,12 +29,12 @@ defmodule Ash.Resource.Relationships.HasMany do
opts: [
destination_field: :atom,
source_field: :atom,
authorization_steps: :keyword,
write_rules: :keyword,
reverse_relationship: :atom
],
defaults: [
source_field: :id,
authorization_steps: []
write_rules: []
],
describe: [
reverse_relationship:
@ -43,7 +43,7 @@ defmodule Ash.Resource.Relationships.HasMany do
"The field on the related resource that should match the `source_field` on this resource. Default: [resource.name]_id",
source_field:
"The field on this resource that should match the `destination_field` on the related resource.",
authorization_steps: """
write_rules: """
Steps applied on an relationship during create or update. If no steps are defined, authorization to change will fail.
If set to false, no steps are applied and any changes are allowed (assuming the action was authorized as a whole)
Remember that any changes against the destination records *will* still be authorized regardless of this setting.
@ -64,8 +64,8 @@ defmodule Ash.Resource.Relationships.HasMany do
# Don't call functions on the resource! We don't want it to compile here
case Ashton.validate(opts, @opt_schema) do
{:ok, opts} ->
authorization_steps =
case opts[:authorization_steps] do
write_rules =
case opts[:write_rules] do
false ->
false
@ -84,7 +84,7 @@ defmodule Ash.Resource.Relationships.HasMany do
{:ok,
%__MODULE__{
name: name,
authorization_steps: authorization_steps,
write_rules: write_rules,
source: resource,
type: :has_many,
cardinality: :many,

View file

@ -9,7 +9,7 @@ defmodule Ash.Resource.Relationships.HasOne do
:destination_field,
:source_field,
:reverse_relationship,
:authorization_steps,
:write_rules,
:allow_orphans?
]
@ -19,7 +19,7 @@ defmodule Ash.Resource.Relationships.HasOne do
source: Ash.resource(),
name: atom,
type: Ash.Type.t(),
authorization_steps: Keyword.t(),
write_rules: Keyword.t(),
destination: Ash.resource(),
destination_field: atom,
source_field: atom,
@ -32,12 +32,12 @@ defmodule Ash.Resource.Relationships.HasOne do
destination_field: :atom,
source_field: :atom,
reverse_relationship: :atom,
authorization_steps: :keyword,
write_rules: :keyword,
allow_orphans?: :boolean
],
defaults: [
source_field: :id,
authorization_steps: [],
write_rules: [],
# TODO: When we add constraint expressions, we should validate this with that.
allow_orphans?: true
],
@ -51,7 +51,7 @@ defmodule Ash.Resource.Relationships.HasOne do
# TODO: Explain this better
allow_orphans:
"Whether or not to allow orphaned records that would result in replaced relationships.",
authorization_steps: """
write_rules: """
Steps applied on an relationship during create or update. If no steps are defined, authorization to change will fail.
If set to false, no steps are applied and any changes are allowed (assuming the action was authorized as a whole)
Remember that any changes against the destination records *will* still be authorized regardless of this setting.
@ -74,8 +74,8 @@ defmodule Ash.Resource.Relationships.HasOne do
# Don't call functions on the resource! We don't want it to compile here
case Ashton.validate(opts, @opt_schema) do
{:ok, opts} ->
authorization_steps =
case opts[:authorization_steps] do
write_rules =
case opts[:write_rules] do
false ->
false
@ -101,7 +101,7 @@ defmodule Ash.Resource.Relationships.HasOne do
destination_field: opts[:destination_field] || :"#{resource_type}_id",
source_field: opts[:source_field],
reverse_relationship: opts[:reverse_relationship],
authorization_steps: authorization_steps
write_rules: write_rules
}}
{:error, errors} ->

View file

@ -11,7 +11,7 @@ defmodule Ash.Resource.Relationships.ManyToMany do
:source_field_on_join_table,
:destination_field_on_join_table,
:reverse_relationship,
:authorization_steps
:write_rules
]
@type t :: %__MODULE__{
@ -26,7 +26,7 @@ defmodule Ash.Resource.Relationships.ManyToMany do
source_field_on_join_table: atom,
destination_field_on_join_table: atom,
reverse_relationship: atom,
authorization_steps: Keyword.t()
write_rules: Keyword.t()
}
@opt_schema Ashton.schema(
@ -35,15 +35,15 @@ defmodule Ash.Resource.Relationships.ManyToMany do
destination_field_on_join_table: :atom,
source_field: :atom,
destination_field: :atom,
authorization_steps: :keyword,
write_rules: :keyword,
through: :atom,
reverse_relationship: :atom,
authorization_steps: :keyword
write_rules: :keyword
],
defaults: [
source_field: :id,
destination_field: :id,
authorization_steps: []
write_rules: []
],
required: [
:through
@ -60,7 +60,7 @@ defmodule Ash.Resource.Relationships.ManyToMany do
"The field on this resource that should line up with `source_field_on_join_table` on the join table.",
destination_field:
"The field on the related resource that should line up with `destination_field_on_join_table` on the join table.",
authorization_steps: """
write_rules: """
Steps applied on an relationship during create or update. If no steps are defined, authorization to change will fail.
If set to false, no steps are applied and any changes are allowed (assuming the action was authorized as a whole)
Remember that any changes against the destination records *will* still be authorized regardless of this setting.
@ -82,8 +82,8 @@ defmodule Ash.Resource.Relationships.ManyToMany do
# Don't call functions on the resource! We don't want it to compile here
case Ashton.validate(opts, @opt_schema) do
{:ok, opts} ->
authorization_steps =
case opts[:authorization_steps] do
write_rules =
case opts[:write_rules] do
false ->
false
@ -110,7 +110,7 @@ defmodule Ash.Resource.Relationships.ManyToMany do
reverse_relationship: opts[:reverse_relationship],
source_field: opts[:source_field],
destination_field: opts[:destination_field],
authorization_steps: authorization_steps,
write_rules: write_rules,
source_field_on_join_table:
opts[:source_field_on_join_table] || :"#{resource_name}_id",
destination_field_on_join_table:

View file

@ -7,24 +7,28 @@ defmodule Ash.Test.Authorization.CreateAuthorizationTest do
actions do
read :default,
authorization_steps: [
rules: [
authorize_if: always()
]
create :default,
authorization_steps: [
rules: [
forbid_unless: setting_relationship(:author),
authorize_if: user_attribute(:author, true)
]
end
# Change rules to `rules`
# for attributes/relationship change them to `write_rules`
attributes do
attribute :contents, :string, authorization_steps: false
attribute :contents, :string, write_rules: false
attribute :color, :string, write_rules: false
attribute :size, :string, write_rules: false
end
relationships do
belongs_to :author, Ash.Test.Authorization.CreateAuthorizationTest.Author,
authorization_steps: [
write_rules: [
authorize_if: relating_to_user()
]
end
@ -35,20 +39,20 @@ defmodule Ash.Test.Authorization.CreateAuthorizationTest do
use Ash.DataLayer.Ets, private?: true
actions do
read :default, authorization_steps: [authorize_if: always()]
read :default, rules: [authorize_if: always()]
create :default,
authorization_steps: [
rules: [
authorize_if: user_attribute(:admin, true),
authorize_if: user_attribute(:manager, true)
]
end
attributes do
attribute :name, :string, authorization_steps: false
attribute :name, :string, write_rules: false
attribute :state, :string,
authorization_steps: [
write_rules: [
authorize_if: user_attribute(:admin, true),
forbid_if: setting(to: "closed"),
authorize_if: always()
@ -56,11 +60,11 @@ defmodule Ash.Test.Authorization.CreateAuthorizationTest do
attribute :bio_locked, :boolean,
default: {:constant, false},
authorization_steps: false
write_rules: false
attribute :self_manager, :boolean, authorization_steps: false
attribute :self_manager, :boolean, write_rules: false
attribute :fired, :boolean, authorization_steps: false
attribute :fired, :boolean, write_rules: false
end
relationships do
@ -68,7 +72,7 @@ defmodule Ash.Test.Authorization.CreateAuthorizationTest do
through: Ash.Test.Authorization.CreateAuthorizationTest.AuthorPost
has_one :bio, Ash.Test.Authorization.CreateAuthorizationTest.Bio,
authorization_steps: [
write_rules: [
forbid_if: attribute_equals(:bio_locked, true),
authorize_if: always()
]
@ -81,18 +85,18 @@ defmodule Ash.Test.Authorization.CreateAuthorizationTest do
actions do
read :default,
authorization_steps: [
rules: [
authorize_if: always()
]
create :default,
authorization_steps: [
rules: [
forbid_unless: setting_relationship(:author),
authorize_if: user_attribute(:author, true)
]
update :default,
authorization_steps: [
rules: [
authorize_if: always()
]
end
@ -100,14 +104,14 @@ defmodule Ash.Test.Authorization.CreateAuthorizationTest do
attributes do
attribute :admin_only?, :boolean,
default: {:constant, false},
authorization_steps: [
write_rules: [
authorize_if: always()
]
end
relationships do
belongs_to :author, Author,
authorization_steps: [
write_rules: [
authorize_if: relating_to_user()
]
end
@ -157,18 +161,18 @@ defmodule Ash.Test.Authorization.CreateAuthorizationTest do
read :default
create :default,
authorization_steps: [
rules: [
authorize_if: user_attribute(:admin, true),
authorize_if: user_attribute(:manager, true)
]
end
attributes do
attribute :title, :string, authorization_steps: false
attribute :title, :string, write_rules: false
attribute :contents, :string, authorization_steps: false
attribute :contents, :string, write_rules: false
attribute :published, :boolean, authorization_steps: false
attribute :published, :boolean, write_rules: false
end
relationships do

View file

@ -7,7 +7,7 @@ defmodule Ash.Test.Authorization.GetAuthorizationTest do
actions do
read :default,
authorization_steps: [
rules: [
# You can see yourself
authorize_if: user_attribute_matches_record(:id, :id),
# You can't see anything else unless you're a manager
@ -75,7 +75,7 @@ defmodule Ash.Test.Authorization.GetAuthorizationTest do
actions do
read :default,
authorization_steps: [
rules: [
authorize_if: attribute_equals(:published, true),
authorize_if: related_to_user_via(:authors)
]

View file

@ -7,7 +7,7 @@ defmodule Ash.Test.Authorization.ReadAuthorizationTest do
actions do
read :default,
authorization_steps: [
rules: [
# You can see yourself
authorize_if: user_attribute_matches_record(:id, :id),
# You can't see anything else unless you're a manager
@ -73,7 +73,7 @@ defmodule Ash.Test.Authorization.ReadAuthorizationTest do
actions do
read :default,
authorization_steps: [
rules: [
authorize_if: attribute_equals(:published, true),
authorize_if: related_to_user_via(:authors)
]

View file

@ -23,7 +23,7 @@ defmodule Ash.Test.Dsl.Resource.Actions.CreateTest do
%Ash.Resource.Actions.Create{
name: :default,
primary?: true,
authorization_steps: [],
rules: [],
type: :create
}
] = Ash.actions(Post)
@ -62,11 +62,11 @@ defmodule Ash.Test.Dsl.Resource.Actions.CreateTest do
test "it fails if `rules` is not a list" do
assert_raise(
Ash.Error.ResourceDslError,
"option authorization_steps at actions -> create -> default must be keyword",
"option rules at actions -> create -> default must be keyword",
fn ->
defposts do
actions do
create :default, authorization_steps: 10
create :default, rules: 10
end
end
end

View file

@ -23,7 +23,7 @@ defmodule Ash.Test.Dsl.Resource.Actions.DestroyTest do
%Ash.Resource.Actions.Destroy{
name: :default,
primary?: true,
authorization_steps: [],
rules: [],
type: :destroy
}
] = Ash.actions(Post)
@ -62,11 +62,11 @@ defmodule Ash.Test.Dsl.Resource.Actions.DestroyTest do
test "it fails if `rules` is not a list" do
assert_raise(
Ash.Error.ResourceDslError,
"option authorization_steps at actions -> destroy -> default must be keyword",
"option rules at actions -> destroy -> default must be keyword",
fn ->
defposts do
actions do
destroy :default, authorization_steps: 10
destroy :default, rules: 10
end
end
end

View file

@ -23,7 +23,7 @@ defmodule Ash.Test.Dsl.Resource.Actions.ReadTest do
%Ash.Resource.Actions.Read{
name: :default,
primary?: true,
authorization_steps: [],
rules: [],
type: :read
}
] = Ash.actions(Post)
@ -62,11 +62,11 @@ defmodule Ash.Test.Dsl.Resource.Actions.ReadTest do
test "it fails if `rules` is not a list" do
assert_raise(
Ash.Error.ResourceDslError,
"option authorization_steps at actions -> read -> default must be keyword",
"option rules at actions -> read -> default must be keyword",
fn ->
defposts do
actions do
read :default, authorization_steps: 10
read :default, rules: 10
end
end
end

View file

@ -23,7 +23,7 @@ defmodule Ash.Test.Dsl.Resource.Actions.UpdateTest do
%Ash.Resource.Actions.Update{
name: :default,
primary?: true,
authorization_steps: [],
rules: [],
type: :update
}
] = Ash.actions(Post)
@ -62,11 +62,11 @@ defmodule Ash.Test.Dsl.Resource.Actions.UpdateTest do
test "it fails if `rules` is not a list" do
assert_raise(
Ash.Error.ResourceDslError,
"option authorization_steps at actions -> update -> default must be keyword",
"option rules at actions -> update -> default must be keyword",
fn ->
defposts do
actions do
update :default, authorization_steps: 10
update :default, rules: 10
end
end
end