# `LetMe`
[🔗](https://github.com/woylie/let_me/blob/3.0.4/lib/let_me.ex#L1)

LetMe is a library for defining and evaluating authorization rules and
handling query scopes and field redactions.

This module only defines auxiliary functions. The main functionality lies in
the `LetMe.Policy` module.

# `filter_allowed_actions`

```elixir
@spec filter_allowed_actions([LetMe.Rule.t()], subject, object, module()) :: [
  LetMe.Rule.t()
]
when subject: any(), object: {atom(), any()} | struct()
```

Takes a list of rules and only returns the rules that would evaluate to `true`
for the given subject and object.

The object needs to be passed as a tuple, where the first element is the
object name, and the second element is the actual object, e.g.
`{:article, %Article{}}`.

If you registered the schema module with `LetMe.Policy.object/3`, you can
pass the struct without tagging it with the object name, e.g. `%Article{}`.

This function is used internally by `c:LetMe.Policy.filter_allowed_actions/3`.

## Example

    rules = MyApp.Policy.list_rules()

    filter_allowed_actions(
      rules,
      %User{},
      {:article, %Article{}},
      MyApp.Policy
    )

# `filter_rules`

```elixir
@spec filter_rules(
  [LetMe.Rule.t()],
  keyword()
) :: [LetMe.Rule.t()]
```

Takes a list of rules and a list of filter options and returns a filtered
list of rules.

This function is used by `c:LetMe.Policy.list_rules/1`.

## Filter options

- `:object` - Matches an object exactly.
- `:action` - Matches an action exactly.
- `:check` - Can be:
  - A check name as an atom
  - A 2-tuple with the check name and the `arg`
  - A 1-arity matcher function that takes a `Spek.Check` struct as an
    argument and returns a boolean.
- `:metadata` - Either a metadata name as an atom or a 2-tuple with the
  metadata name and the metadata value.

If an atom is passed as `:check`, all rules using the given check name are
returned, regardless of the argument the check is called with. If a 2-tuple
is passed, all rules are returned that use the given check with exactly that
argument. In either case, rules that have more checks in addition to the
given one will also be returned, and it does not matter whether the check
appears in an `allow/1` or a `deny/1` call.

## Examples

    iex> rules = [
    ...>   %LetMe.Rule{action: :create, name: :article_create, object: :article},
    ...>   %LetMe.Rule{action: :create, name: :category_create, object: :category}
    ...> ]
    iex> filter_rules(rules, object: :article)
    [%LetMe.Rule{action: :create, name: :article_create, object: :article}]

    iex> rules = [
    ...>   %LetMe.Rule{
    ...>     action: :create,
    ...>     name: :article_create,
    ...>     object: :article,
    ...>     expression: %Spek.Check{
    ...>       module: MyApp.Policy.Checks,
    ...>       fun: :role,
    ...>       args: [{:ctx, :subject}, {:ctx, :object}, :editor]
    ...>     }
    ...>   },
    ...>   %LetMe.Rule{
    ...>     action: :update,
    ...>     name: :article_update,
    ...>     object: :article,
    ...>     expression: %Spek.AnyOf{
    ...>       children: [
    ...>         %Spek.Check{
    ...>           module: MyApp.Policy.Checks,
    ...>           fun: :own_resource,
    ...>           args: [{:ctx, :subject}, {:ctx, :object}]
    ...>         },
    ...>         %Spek.Check{
    ...>           module: MyApp.Policy.Checks,
    ...>           fun: :role,
    ...>           args: [{:ctx, :subject}, {:ctx, :object}, :writer]
    ...>         }
    ...>       ]
    ...>     }
    ...>   }
    ...> ]
    iex> filter_rules(rules, check: :own_resource)
    [
      %LetMe.Rule{
        action: :update,
        name: :article_update,
        object: :article,
        expression: %Spek.AnyOf{children: [
          %Spek.Check{
            module: MyApp.Policy.Checks,
            fun: :own_resource,
            args: [{:ctx, :subject}, {:ctx, :object}]
          },
          %Spek.Check{
            module: MyApp.Policy.Checks,
            fun: :role,
            args: [{:ctx, :subject}, {:ctx, :object}, :writer]
          }
        ]}
      }
    ]
    iex> match?([_, _], filter_rules(rules, check: :role))
    true
    iex> filter_rules(rules, check: {:role, :editor})
    [
      %LetMe.Rule{
        action: :create,
        name: :article_create,
        object: :article,
        expression: %Spek.Check{
          module: MyApp.Policy.Checks,
          fun: :role,
          args: [{:ctx, :subject}, {:ctx, :object}, :editor]
        }
      }
    ]
    iex> filter_rules(rules, check: {:role, :writer})
    [
      %LetMe.Rule{
        action: :update,
        name: :article_update,
        object: :article,
        expression: %Spek.AnyOf{
          children: [
            %Spek.Check{
              module: MyApp.Policy.Checks,
              fun: :own_resource,
              args: [{:ctx, :subject}, {:ctx, :object}]
            },
            %Spek.Check{
              module: MyApp.Policy.Checks,
              fun: :role,
              args: [{:ctx, :subject}, {:ctx, :object}, :writer]
            }
          ]
        }
      }
    ]

# `redact`

```elixir
@spec redact(struct(), any(), keyword()) :: struct()
@spec redact([struct()], any(), keyword()) :: [struct()]
@spec redact(nil, any(), keyword()) :: nil
```

Takes a struct or a list of structs and redacts fields depending on the
subject (user).

Uses the callback implementation for `c:LetMe.Schema.redacted_fields/3` in the
struct module.

## Options

- `:redact_value` - The value to be used for redacted fields. Defaults to
  `:redacted`.

Any additional options will be passed to `c:LetMe.Schema.redacted_fields/3`.

## Example

    iex> article = %MyApp.Blog.Article{}
    iex> user = %{id: 2, role: :user}
    iex> redact(article, user)
    %MyApp.Blog.Article{like_count: :redacted, title: "Give us back our moon dust and cockroaches", user_id: 1, view_count: :redacted}

    iex> article = %MyApp.Blog.Article{}
    iex> user = %{id: 2, role: :user}
    iex> redact(article, user, redact_value: nil)
    %MyApp.Blog.Article{like_count: nil, title: "Give us back our moon dust and cockroaches", user_id: 1, view_count: nil}

    iex> articles = [
    ...>   %MyApp.Blog.Article{},
    ...>   %MyApp.Blog.Article{user_id: 2, title: "Joey Chestnut is chomp champ"}
    ...> ]
    iex> user = %{id: 2, role: :user}
    iex> redact(articles, user)
    [%MyApp.Blog.Article{like_count: :redacted, title: "Give us back our moon dust and cockroaches", user_id: 1, view_count: :redacted}, %MyApp.Blog.Article{like_count: 25, title: "Joey Chestnut is chomp champ", user_id: 2, view_count: :redacted}]

# `reject_redacted_fields`

```elixir
@spec reject_redacted_fields([atom()], struct(), any(), keyword()) :: [atom()]
```

Removes redacted fields from a given list of fields.

Uses the `c:LetMe.Schema.redacted_fields/3` callback implementation of the
struct module to determine the fields to remove.

## Examples

    iex> fields = [:like_count, :title, :user_id, :view_count]
    iex> user = %{id: 1, role: :user}
    iex> article = %MyApp.Blog.Article{}
    iex> result = reject_redacted_fields(fields, article, user)
    iex> Enum.sort(result)
    [:like_count, :title, :user_id]

This can be useful as a safeguard to prevent accidentally casting fields the
user is not allowed to see and thereby nilifying or replacing them.

    def update_changeset(%Article{} = article, attrs, %User{} = user) do
      fields = LetMe.reject_redacted_fields(
        [:title, :body, :internal_reference],
        article,
        user
      )

      article
      |> cast(attrs, fields)
      |> validate_required([:title, :body])
    end

If a keyword list is given as a fourth argument, it is passed to
`c:LetMe.Schema.redacted_fields/3`.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
