From ebd829163177d1680f495103faec40e6802459a3 Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Sat, 11 Jan 2020 00:09:52 -0500 Subject: [PATCH] WIP --- README.md | 6 +- lib/ash/actions/attributes.ex | 6 +- lib/ash/actions/create.ex | 23 +- lib/ash/actions/destroy.ex | 2 +- lib/ash/actions/read.ex | 4 +- lib/ash/actions/relationships.ex | 708 +++--------------- lib/ash/actions/side_load.ex | 4 +- lib/ash/actions/update.ex | 4 +- lib/ash/authorization/authorization.ex | 2 +- lib/ash/authorization/authorizer.ex | 136 ++-- .../authorization/check/relating_to_user.ex | 16 +- lib/ash/authorization/checker.ex | 44 +- lib/ash/authorization/clause.ex | 9 + lib/ash/authorization/report.ex | 17 +- lib/ash/authorization/request.ex | 42 +- lib/ash/authorization/sat_solver.ex | 63 +- lib/ash/filter/filter.ex | 4 +- lib/ash/resource.ex | 4 +- lib/ash/resource/actions/create.ex | 16 +- lib/ash/resource/actions/destroy.ex | 16 +- lib/ash/resource/actions/read.ex | 16 +- lib/ash/resource/actions/update.ex | 16 +- lib/ash/resource/attributes/attribute.ex | 20 +- lib/ash/resource/relationships/belongs_to.ex | 16 +- lib/ash/resource/relationships/has_many.ex | 18 +- lib/ash/resource/relationships/has_one.ex | 16 +- .../resource/relationships/many_to_many.ex | 18 +- .../create_authorization_test.exs | 46 +- test/authorization/get_authorization_test.exs | 4 +- .../authorization/read_authorization_test.exs | 4 +- test/resource/actions/create_test.exs | 6 +- test/resource/actions/destroy_test.exs | 6 +- test/resource/actions/read_test.exs | 6 +- test/resource/actions/update_test.exs | 6 +- 34 files changed, 444 insertions(+), 880 deletions(-) diff --git a/README.md b/README.md index 2803943e..b3b195b7 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/lib/ash/actions/attributes.ex b/lib/ash/actions/attributes.ex index 37c36765..0f846770 100644 --- a/lib/ash/actions/attributes.ex +++ b/lib/ash/actions/attributes.ex @@ -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}`" diff --git a/lib/ash/actions/create.ex b/lib/ash/actions/create.ex index 14a53ae9..06efba01 100644 --- a/lib/ash/actions/create.ex +++ b/lib/ash/actions/create.ex @@ -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 diff --git a/lib/ash/actions/destroy.ex b/lib/ash/actions/destroy.ex index a0256a7a..e040acee 100644 --- a/lib/ash/actions/destroy.ex +++ b/lib/ash/actions/destroy.ex @@ -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" ) diff --git a/lib/ash/actions/read.ex b/lib/ash/actions/read.ex index 2eaed630..8becd31f 100644 --- a/lib/ash/actions/read.ex +++ b/lib/ash/actions/read.ex @@ -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: [], diff --git a/lib/ash/actions/relationships.ex b/lib/ash/actions/relationships.ex index aa857c31..18a92ed2 100644 --- a/lib/ash/actions/relationships.ex +++ b/lib/ash/actions/relationships.ex @@ -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 diff --git a/lib/ash/actions/side_load.ex b/lib/ash/actions/side_load.ex index e9b57f2a..cc1eb5ef 100644 --- a/lib/ash/actions/side_load.ex +++ b/lib/ash/actions/side_load.ex @@ -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} diff --git a/lib/ash/actions/update.ex b/lib/ash/actions/update.ex index 48cb6f34..eafccc5f 100644 --- a/lib/ash/actions/update.ex +++ b/lib/ash/actions/update.ex @@ -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__) || [], diff --git a/lib/ash/authorization/authorization.ex b/lib/ash/authorization/authorization.ex index 4d3b7d45..f502eb66 100644 --- a/lib/ash/authorization/authorization.ex +++ b/lib/ash/authorization/authorization.ex @@ -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. diff --git a/lib/ash/authorization/authorizer.ex b/lib/ash/authorization/authorizer.ex index 14460840..d52f262b 100644 --- a/lib/ash/authorization/authorizer.ex +++ b/lib/ash/authorization/authorizer.ex @@ -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?) diff --git a/lib/ash/authorization/check/relating_to_user.ex b/lib/ash/authorization/check/relating_to_user.ex index 154abb1d..eaa63299 100644 --- a/lib/ash/authorization/check/relating_to_user.ex +++ b/lib/ash/authorization/check/relating_to_user.ex @@ -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 diff --git a/lib/ash/authorization/checker.ex b/lib/ash/authorization/checker.ex index 32993dd3..2419eb60 100644 --- a/lib/ash/authorization/checker.ex +++ b/lib/ash/authorization/checker.ex @@ -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 diff --git a/lib/ash/authorization/clause.ex b/lib/ash/authorization/clause.ex index 3dd4fc7b..27b15db7 100644 --- a/lib/ash/authorization/clause.ex +++ b/lib/ash/authorization/clause.ex @@ -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} -> diff --git a/lib/ash/authorization/report.ex b/lib/ash/authorization/report.ex index a3ccda95..dab516a0 100644 --- a/lib/ash/authorization/report.ex +++ b/lib/ash/authorization/report.ex @@ -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) diff --git a/lib/ash/authorization/request.ex b/lib/ash/authorization/request.ex index 3c9bf8c1..aa669324 100644 --- a/lib/ash/authorization/request.ex +++ b/lib/ash/authorization/request.ex @@ -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}} diff --git a/lib/ash/authorization/sat_solver.ex b/lib/ash/authorization/sat_solver.ex index 71fe7904..82c0cd1e 100644 --- a/lib/ash/authorization/sat_solver.ex +++ b/lib/ash/authorization/sat_solver.ex @@ -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 diff --git a/lib/ash/filter/filter.ex b/lib/ash/filter/filter.ex index d30fb501..688910aa 100644 --- a/lib/ash/filter/filter.ex +++ b/lib/ash/filter/filter.ex @@ -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 diff --git a/lib/ash/resource.ex b/lib/ash/resource.ex index 6b1c2d0a..9762e7ec 100644 --- a/lib/ash/resource.ex +++ b/lib/ash/resource.ex @@ -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) diff --git a/lib/ash/resource/actions/create.ex b/lib/ash/resource/actions/create.ex index e8c5d6c9..d24dd792 100644 --- a/lib/ash/resource/actions/create.ex +++ b/lib/ash/resource/actions/create.ex @@ -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} -> diff --git a/lib/ash/resource/actions/destroy.ex b/lib/ash/resource/actions/destroy.ex index 75118059..17bed747 100644 --- a/lib/ash/resource/actions/destroy.ex +++ b/lib/ash/resource/actions/destroy.ex @@ -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} -> diff --git a/lib/ash/resource/actions/read.ex b/lib/ash/resource/actions/read.ex index ffeef3df..e5c3bbe0 100644 --- a/lib/ash/resource/actions/read.ex +++ b/lib/ash/resource/actions/read.ex @@ -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?] }} diff --git a/lib/ash/resource/actions/update.ex b/lib/ash/resource/actions/update.ex index 2f3fdffd..e88c595d 100644 --- a/lib/ash/resource/actions/update.ex +++ b/lib/ash/resource/actions/update.ex @@ -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} -> diff --git a/lib/ash/resource/attributes/attribute.ex b/lib/ash/resource/attributes/attribute.ex index 87383bbb..7ec67df6 100644 --- a/lib/ash/resource/attributes/attribute.ex +++ b/lib/ash/resource/attributes/attribute.ex @@ -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 diff --git a/lib/ash/resource/relationships/belongs_to.ex b/lib/ash/resource/relationships/belongs_to.ex index 1a890091..3c5ca0df 100644 --- a/lib/ash/resource/relationships/belongs_to.ex +++ b/lib/ash/resource/relationships/belongs_to.ex @@ -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, diff --git a/lib/ash/resource/relationships/has_many.ex b/lib/ash/resource/relationships/has_many.ex index 8bebba5c..0c86aac1 100644 --- a/lib/ash/resource/relationships/has_many.ex +++ b/lib/ash/resource/relationships/has_many.ex @@ -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, diff --git a/lib/ash/resource/relationships/has_one.ex b/lib/ash/resource/relationships/has_one.ex index 657692fb..86e0aa17 100644 --- a/lib/ash/resource/relationships/has_one.ex +++ b/lib/ash/resource/relationships/has_one.ex @@ -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} -> diff --git a/lib/ash/resource/relationships/many_to_many.ex b/lib/ash/resource/relationships/many_to_many.ex index 59377baf..79222bb4 100644 --- a/lib/ash/resource/relationships/many_to_many.ex +++ b/lib/ash/resource/relationships/many_to_many.ex @@ -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: diff --git a/test/authorization/create_authorization_test.exs b/test/authorization/create_authorization_test.exs index 9999e6ae..c4f57941 100644 --- a/test/authorization/create_authorization_test.exs +++ b/test/authorization/create_authorization_test.exs @@ -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 diff --git a/test/authorization/get_authorization_test.exs b/test/authorization/get_authorization_test.exs index 2a37c4ef..2d3e3404 100644 --- a/test/authorization/get_authorization_test.exs +++ b/test/authorization/get_authorization_test.exs @@ -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) ] diff --git a/test/authorization/read_authorization_test.exs b/test/authorization/read_authorization_test.exs index 141f7656..1f7afccd 100644 --- a/test/authorization/read_authorization_test.exs +++ b/test/authorization/read_authorization_test.exs @@ -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) ] diff --git a/test/resource/actions/create_test.exs b/test/resource/actions/create_test.exs index 61ce3aca..b8df7183 100644 --- a/test/resource/actions/create_test.exs +++ b/test/resource/actions/create_test.exs @@ -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 diff --git a/test/resource/actions/destroy_test.exs b/test/resource/actions/destroy_test.exs index 453e6bb2..55808ec1 100644 --- a/test/resource/actions/destroy_test.exs +++ b/test/resource/actions/destroy_test.exs @@ -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 diff --git a/test/resource/actions/read_test.exs b/test/resource/actions/read_test.exs index aafe61d7..6b4ee3f2 100644 --- a/test/resource/actions/read_test.exs +++ b/test/resource/actions/read_test.exs @@ -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 diff --git a/test/resource/actions/update_test.exs b/test/resource/actions/update_test.exs index 840c3dc0..fb5e8880 100644 --- a/test/resource/actions/update_test.exs +++ b/test/resource/actions/update_test.exs @@ -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