Unreleased

[2.0.0] - 2026-03-31

Added

Changed

  • Evaluate authorization checks lazily.
  • Change 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 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:

- 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.

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.

@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 LetMe.Policy.list_rules/1 with the check option. The value is unchanged.

- 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 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 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

[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

[1.0.0] - 2022-11-06

Added

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 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 LetMe.Schema.redacted_fields/3 to allow passing additional options, and to be consistent with 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