CHANGELOG.md

# Changelog

## Unreleased

## [2.0.0] - 2026-03-31

### Added

- Add `LetMe.AllOf`, `LetMe.AnyOf`, `LetMe.Check`, `LetMe.Literal`, and
  `LetMe.Not` structs and `t:LetMe.expression/0` type to represent policy
  expressions.
- Add `error` option to `use LetMe.Policy`, `c:LetMe.Policy.authorize/4` to
  switch between error structs without evaluation details, error structs with
  evaluation details, and arbitrary custom error values.
- Support check matcher function as argument for `LetMe.filter_rules/2` and
  `c:LetMe.Policy.list_rules/1`.

### Changed

- Evaluate authorization checks lazily.
- Change `c:LetMe.Policy.authorize/4` to always return an
  `{:error, LetMe.UnauthorizedError.t()}` tuple when authorization checks
  fail.
- Add `expression` field to `LetMe.UnauthorizedError.t()`, which contains the
  policy expression and evaluation results until a decision was made.
- Replace `allow` and `deny` fields on `LetMe.Rule` struct with a single
  `expression` field that contains a combined logical expression.
- Optimize the combined logical expression at compile time.
- Support check functions that return `:ok`, `:error`, `{:ok, term}`, or
  `{:error, term}`. These return values can be read from the expression in the
  `LetMe.UnauthorizedError` struct.
- Replace the `allow` and `deny` options in `LetMe.filter_rules/2` and
  `c:LetMe.Policy.list_rules/1` with a single `check` option.

### Removed

- Remove `error_reason` and `error_message` options from `LetMe.Policy`.

### How to upgrade

Replace the `error_reason` and `error_message` options with the `error` option:

```diff
- use LetMe.Policy, error_reason: :forbidden, error_message: "Forbidden"
+ use LetMe.Policy, error: :forbidden
```

You can opt-in to detailed error structs by setting the value to `:detailed`
or simple error structs by setting the value to `:simple`.

```elixir
use LetMe.Policy, error: :detailed
```

If you do that, change all pattern matches on `{:error, :unauthorized}` or your
custom error reason and update your type specifications accordingly.

```diff
@spec update_article(Scope.t(), Article.t(), map) ::
-   {:ok, Article.t()} | {:error, :unauthorized}
+   {:ok, Article.t()} | {:error, LetMe.Unauthorized.t()}
def update_article(scope, article, params)
  with MyApp.Policy.authorize(:article_update, scope, article) do
    # ...
  end
end

case update_article(scope, article, params) do
  {:ok, article} ->
    # ...

-   {:error, :unauthorized} ->
+   {:error, %LetMe.UnauthorizedError{}} ->
    # ...
end
```

Replace the `allow` and `deny` option in `LetMe.filter_rules/2` and
`c:LetMe.Policy.list_rules/1` with the `check` option. The value is unchanged.

```diff
- MyApp.Policy.filter_rules(allow: {:role, :admin})
+ MyApp.Policy.filter_rules(check: {:role, :admin})

- MyApp.Policy.filter_rules(deny: :suspended)
+ MyApp.Policy.filter_rules(check: :suspended)
```

If you were working directly with the `allow` and `deny` fields of the
`LetMe.Rule` struct, update your code to work with the `expression` field and
`t:LetMe.expression/0` type instead.

## [1.2.5] - 2025-03-26

### Changed

- Improve documentation.

## [1.2.4] - 2024-04-22

### Fixed

- Nested lists within structs resulted in a `CaseClauseError` during redaction.

## [1.2.3] - 2023-11-11

### Changed

- Updated documentation.

## [1.2.2] - 2023-06-28

### Changed

- You can now override the exception message used by
  `c:LetMe.Policy.authorize!/4` (e.g.
  `use LetMe.Policy, error_message: "Not today, chap."`).

## [1.2.1] - 2023-06-28

### Changed

- Define `action` type when you `use LetMe.Policy`.
- Add type specifications for generated `authorize` functions.

## [1.2.0] - 2023-06-19

### Added

- Added an optional `opts` argument to the authorize functions, so that
  additional options can be passed to pre-hooks.
- Updated `LetMe.filter_rules/2` to allow filtering by meta data.

### Changed

- Pre-hook options are now expected to be passed as a keyword list.

### Fixed

- Fix deprecation warning about `Logger.warn/2` in Elixir 1.15.

## [1.1.0] - 2023-05-08

### Added

- Added a `metadata` macro to add metadata to actions. The metadata can be read
  from the `LetMe.Rule` struct.

## [1.0.3] - 2023-03-21

### Changed

- Update `ex_doc` and other dev dependencies.

## [1.0.2] - 2023-01-05

### Added

- Added a cheat sheet for rules and checks.

### Fixed

- Fixed a code example for rule introspection in the readme.

## [1.0.1] - 2022-11-06

### Changed

- Use `Keyword.pop/3` with default value instead of `Keyword.pop!/2`, so that
  you can pass options to `LetMe.redact/3` without passing the `redact_value`
  option.

## [1.0.0] - 2022-11-06

### Added

- Added `c:LetMe.Policy.filter_allowed_actions/3` and
  `LetMe.filter_allowed_actions/4`.
- Added `c:LetMe.Policy.get_object_name/1`.

### Changed

- Renamed `c:LetMe.Policy.authorized?/3` to `c:LetMe.Policy.authorize?/3`,
  because consistency is more important than grammar, maybe.
- The `c:LetMe.Schema.scope/2` callback was removed in favour of
  `c:LetMe.Schema.scope/3`. The `__using__` macro defined default
  implementations for both functions that returned the given query unchanged, in
  case you only needed the `redact` callback of the behaviour. In practice, this
  made it all too easy to call the 2-arity version when only the 3-arity
  version was defined, and vice versa, which would lead the query to not be
  scoped. So in order to reduce the room for error at the cost of a minor
  inconvenience, you will now always need to implement the 3-arity function,
  even if you don't need the third argument.
- Changed `c:LetMe.Schema.redacted_fields/2` to
  `c:LetMe.Schema.redacted_fields/3` to allow passing additional options, and to
  be consistent with `c:LetMe.Schema.scope/3`.

## [0.2.0] - 2022-07-12

### Changed

- Added support for nested field redactions, either by explicitly listing the
  fields or by referencing a module that also implements `LetMe.Schema`.

### Fixed

- `reject_redacted_fields/3` called `redact/2` callback with the wrong argument
  order.

## [0.1.0] - 2022-07-11

initial release