Unreleased

[3.0.0] - 2026-05-27

Added

Changed

  • The expressions, evaluation logic, and optimization logic introduced in LetMe 2.0.0 were extracted into a separate library called Spek. This version replaces the expression structs and logic with the new library.

How to upgrade

The DSL and callback functions are unchanged compared to version 2.0.0. Only the representation of expressions under the expression key in the LetMe.Rule and LetMe.UnauthorizedError structs was changed to use the Spek structs.

The structs have mostly the same structure, except for these differences:

  • All structs: passed? -> satisfied?
  • Spek.Literal: additional result key
  • Spek.Check:
    • name -> fun
    • additional module key, which contains the name of the check module.
    • arg -> args
    • Because of the way Spek maps the evaluation context to function arguments, the args key holds a list in the format [{:ctx, :subject}, {:ctx, :object}, arg], where arg is the value that was previously under the arg key.

[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, 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 LetMe.Policy.list_rules/1.

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