Search documentation of ash

# Authorize Access to Resources

View Source

    Mix.install(
      [
        {:ash, "~> 3.0"},
        {:simple_sat, "~> 0.1"},
        {:kino, "~> 0.12"}
      ],
      consolidate_protocols: false
    )

    Logger.configure(level: :warning)
    Application.put_env(:ash, :policies, show_policy_breakdowns?: true)

## Introduction

A key feature of Ash is the ability to build security directly into your resources. We do this with policies.

Because how you write policies is extremely situational, this how-to guide provides a list of "considerations" as opposed to "instructions".

For more context, read the policies guide.

## Writing Policies

1. Consider whether or not you want to adopt a specific style of authorization, like ACL, or RBAC. For standard RBAC, look into AshRbac, and you may not need to write any of your own policies at that point
2. Determine if there are any `bypass` policies to add (admin users, super users, etc.). Consider placing this on the domain, instead of the resource
3. Begin by making an inventory of each action on your resource, and under what conditions a given actor may be allowed to perform them. If all actions of a given type have the same criteria, we will typically use the `action_type(:type)` condition
4. Armed with this inventory, begin to write policies. Start simple, write a policy per action type, and add a description of what your policy accomplishes.
5. Find patterns, like cross-cutting checks that exist in all policies, that can be expressed as smaller, simpler policies
6. Determine if any field policies are required to prohibit access to attributes/calculations/aggregates
7. Finally, you can confirm your understanding of the authorization flow for a given resource by generating policy charts with `mix ash.generate_policy_charts` (field policies are not currently included in the generated charts)

## Example

    defmodule User do
      use Ash.Resource,
        domain: Domain,
        data_layer: Ash.DataLayer.Ets

      actions do
        defaults [:read, create: [:admin?]]
      end

      attributes do
        uuid_primary_key :id
        attribute :admin?, :boolean do
          allow_nil? false
          default false
        end
      end
    end

    defmodule Tweet do
      use Ash.Resource,
        domain: Domain,
        data_layer: Ash.DataLayer.Ets,
        authorizers: [Ash.Policy.Authorizer]

      attributes do
        uuid_primary_key :id
        attribute :text, :string do
          allow_nil? false
          constraints max_length: 144
          public? true
        end

        attribute :hidden?, :boolean do
          allow_nil? false
          default false
          public? true
        end

        attribute :private_note, :string do
          sensitive? true
          public? true
        end
      end

      calculations do
        calculate :tweet_length, :integer, expr(string_length(text)) do
          public? true
        end
      end

      relationships do
        belongs_to :user, User, allow_nil?: false
      end

      actions do
        defaults [:read, update: [:text, :hidden?, :private_note]]

        create :create do
          primary? true
          accept [:text, :hidden?, :private_note]
          change relate_actor(:user)
        end
      end

      policies do
        # action_type-based policies
        policy action_type(:read) do
          # each policy has a description
          description "If a tweet is hidden, only the author can read it. Otherwise, anyone can."
          # first check this. If true, then this policy passes
          authorize_if relates_to_actor_via(:user)
          # then check this. If false, then this policy fails
          forbid_if expr(hidden? == true)
          # otherwise, this policy passes
          authorize_if always()
        end

        # blanket allow-listing of creates
        policy action_type(:create) do
          description "Anyone can create a tweet"
          authorize_if always()
        end

        policy action_type(:update) do
          description "Only an admin or the user who tweeted can edit their tweet"
          # first check this. If true, then this policy passes
          authorize_if actor_attribute_equals(:admin?, true)
          # then check this. If true, then this policy passes
          authorize_if relates_to_actor_via(:user)
          # otherwise, there is nothing left to check and no decision, so *this policy fails*
        end
      end

      field_policies do
        # anyone can see these fields
        field_policy [:text, :tweet_length] do
          description "Public tweet fields are visible"
          authorize_if always()
        end

        field_policy [:hidden?, :private_note] do
          description "hidden? and private_note are only visible to the author"
          authorize_if relates_to_actor_via(:user)
        end
      end
    end

    defmodule Domain do
      use Ash.Domain,
        validate_config_inclusion?: false

      resources do
        resource Tweet do
          define :create_tweet, action: :create, args: [:text]
          define :update_tweet, action: :update, args: [:text]
          define :list_tweets, action: :read
          define :get_tweet, action: :read, get_by: [:id]
        end

        resource User do
          define :create_user, action: :create
        end
      end
    end

    {:module, Domain, <<70, 79, 82, 49, 0, 2, 117, ...>>,
     [
       Ash.Domain.Dsl.Resources.Resource,
       Ash.Domain.Dsl.Resources.Options,
       Ash.Domain.Dsl,
       %{opts: [], entities: [...]},
       Ash.Domain.Dsl,
       Ash.Domain.Dsl.Resources.Options,
       ...
     ]}

## Interacting with resources that have policies

    # doing forbidden things produces an `Ash.Error.Forbidden`
    user = Domain.create_user!()
    other_user = Domain.create_user!()

    tweet = Domain.create_tweet!("hello world!", actor: user)
    Domain.update_tweet!(tweet, "Goodbye world", actor: other_user)

    # Reading data applies policies as filters

    user = Domain.create_user!()
    other_user = Domain.create_user!()

    my_hidden_tweet = Domain.create_tweet!("hello world!", %{hidden?: true}, actor: user)

    other_users_hidden_tweet =
      Domain.create_tweet!("hello world!", %{hidden?: true}, actor: other_user)

    my_tweet = Domain.create_tweet!("hello world!", actor: user)
    other_users_tweet = Domain.create_tweet!("hello world!", actor: other_user)

    tweet_ids = Domain.list_tweets!(actor: user) |> Enum.map(& &1.id)

    # I see my own hidden tweets, and other users non-hidden tweets
    true = my_hidden_tweet.id in tweet_ids
    true = other_users_tweet.id in tweet_ids

    # but not other users hidden tweets
    false = other_users_hidden_tweet.id in tweet_ids

    :ok

    :ok

    # Field policies return hidden fields as `%Ash.ForbiddenField{}`

    user = Domain.create_user!()
    other_user = Domain.create_user!()

    other_users_tweet =
      Domain.create_tweet!("hello world!", %{private_note: "you can't see this!"}, actor: other_user)

    %Ash.ForbiddenField{} = Domain.get_tweet!(other_users_tweet.id, actor: user).private_note

    #Ash.ForbiddenField<field: :private_note, type: :attribute, ...>

    Tweet
    |> Ash.Policy.Chart.Mermaid.chart()
    |> Kino.Shorts.mermaid()

    flowchart TB
    subgraph at least one policy applies
    direction TB
    at_least_one_policy["action.type == :read
    or action.type == :create
    or action.type == :update"]
    end
    at_least_one_policy--False-->Forbidden
    at_least_one_policy--True-->0_conditions
    subgraph Policy 1[If a tweet is hidden, only the author can read it. Otherwise, anyone can.]
    direction TB
    0_conditions{"action.type == :read"}
    0_checks_0{"record.user == actor"}
    0_checks_1{"hidden? == true"}
    end
    0_conditions--True-->0_checks_0
    0_conditions--False-->1_conditions
    0_checks_0--True-->1_conditions
    0_checks_0--False-->0_checks_1
    0_checks_1--True-->Forbidden
    0_checks_1--False-->1_conditions
    subgraph Policy 2[Anyone can create a tweet]
    direction TB
    1_conditions{"action.type == :create"}
    end
    subgraph Policy 3[Only an admin or the user who tweeted can edit their tweet]
    direction TB
    2_conditions{"action.type == :update"}
    2_checks_0{"actor.admin? == true"}
    2_checks_1{"record.user == actor"}
    end
    2_conditions--True-->2_checks_0
    2_conditions--False-->Authorized
    2_checks_0--True-->Authorized
    2_checks_0--False-->2_checks_1
    2_checks_1--True-->Authorized
    2_checks_1--False-->Forbidden
    subgraph results[Results]
    Authorized([Authorized])
    Forbidden([Forbidden])
    end
    1_conditions--Or-->2_conditions

← Previous Page Test Resources
Next Page → Encrypt Attributes