Future of Permit authorization library
This follow-up to our recent Permit update, analyzes the future of Elixir authorization with a look at upcoming features, optimizations, and experimental ideas that will shape how Permit evolves.
Recently, I wrote about recent updates in our Permit authorization libraries as well as introducing works in progress for Permit.Absinthe, which should make GraphQL API query & mutation authorization a lot easier and more streamlined.
As noted in my recent ElixirConf EU presentation, and mentioned in the previous article, we're working on finessing several new ideas and exploring a couple of new concepts that Permit can be adapted to support. I'll elaborate on these here, and each of these topics would be worth an article of its own. Let's explore what we plan to do to make Permit easier to use and adaptable into more different workflows.
Phoenix Scopes
An upcoming addition in Phoenix 1.8, according to the docs, a scope is a data structure used to keep information about the current request or session, such as the current user logged in, the organization/company it belongs to, permissions, and so on. Which is quite self-explanatory: this is where the likes of current_user
and authorization-related context data is supposed to live in.
This simply calls for us to think of how we could adapt Permit.Phoenix to using Phoenix scopes - what comes to mind is storing the currently used authorization context as well as defaulting to look into the scope to retrieve the authorization subject (i.e. the current user).
Policy playground & visualization
While you can use Permit to model access control using user-defined permission data in Ecto tables - as in this trivial example:
# permissions can look like:
defmodule MyApp.Permission do
use Ecto.Schema
schema "permissions" do
field :name, :string
field :resource_module, :string
field :conditions, {:array, :map} # better still, an embedded schema
end
end
# %Permission{
# name: "read",
# resource_module: "Elixir.MyApp.Article",
# conditions: [published: true]
# }
def can(%{permissions: permissions} = user) do
Enum.reduce(
permissions,
permit(),
fn permission, acc ->
. permission_to(
acc,
permission.name,
String.to_existing_atom(permission.resource_module),
permission.conditions
)
end
)
end
In reality you'd probably want a more sophisticated mechanism of converting :resource_module
to module names than String.to_existing_atom/1
, and :conditions
could be implemented with a pre-processing layer to interpolate data from user
itself.
But still, this is a structure that can straightforwardly back a UI for live permission management. This is simple enough that so far we've considered this as out of scope for core Permit libraries, specifically Permit.Phoenix - its role is to provide drop-in controller and LiveView support for the load-and-authorize pattern, not to enforce any specific access control permissions structure nor provide a UI for usage by the application's end user.
Without imposing anything on the looks of the system for the end user, we'd like to draw inspiration from Supabase's approach to making developers' lives easier building row-level security policies (more on which we'll speak about later): ![[Pasted image 20250611113154.png]] Supabase is a PostgreSQL-driven platform and it comes as no surprise that its built-in authorization is built around Postgres RLS - and they've provided a UI helps build Postgres RLS policies; it aids the user in configuring the policies' structure and validating their implementation. And this is the spirit in which we would like to work on a policy playground UI for Permit: there is certainly room for a visual tool for building Permit authorization rules based on a set of defined permissions and existing Ecto schemas. Whether it'll be a standalone web-based tool, or something that co-exists in your BEAM in development and reads out Ecto schemas and defined permissions for convenience - it can certainly help modeling authorization to the dev team's liking and matching specific business requirements.
Static permission-related code analysis
How to prove I rules are well-formed and logically consistent? How to make sure I didn't forget to cover one of my Ecto schemas, or to plug Permit into one of your controllers? Well, that's a good question to ask yourself, and we are thinking of how we can help Permit help you there.
At the rule definition level, several issues could occur - and most of them would be quite easy to detect statically as far as literals are concerned:
- Duplicate rules: Identical {action, resource, predicate_map} encountered twice - reported as redundancy.
- Redundancy: Two rules on the same
{action, resource}
pair where rule 2’s predicate sub-sumes rule 1 (it is a superset). Example:read(Invoice)
followed later byread(Invoice, status: :*)
, orall(Post)
coexisting withread(Post)
,update(Post)
, etc. - Conflicting or logically impossible predicates: Same key appears twice with different non-wildcard values in the same builder call, e.g. update(Article, published: true, published: false). A simple scan for duplicate keys with v₁ != v₂ && v₁ != : && v₂ != : is enough.
With Ecto added to the mix, we can think of these additional checks:
- Resource module not Ecto schema: If the permissions module mixes in
use Permit.Ecto.Permissions
, then we can issue warnings when non-Ecto structs are given in rules e.g.permit() |> read
- Unknown field in predicate: For every key in the keyword list, ask the compiled schema for
__schema__(:fields)
and__schema__(:virtual_fields)
, as well as__schema__(:associations)
. Flag typos early; encountered virtual fields should also emit warnings, because they can't be queried, and nested conditions can be checked against defined associations. - Type mismatch: Type of field value in condition does not match database column type - e.g.
published: "true"
instead ofpublished: true
.
Then, in Permit.Phoenix, which aims to relieve us of having to remember to write authorization-related boilerplate, there are still concerns about possible weak links. Intentionally, when plugging it into a controller or LIveView, all actions or live actions are authorized and exceptions are done on an opt-out basis. However, the developer might still want to take additional measures to ensure a strict usage policy and correctness of configuration:
- Enforced Permit.Phoenix usage: Emit warnings if
Permit.Phoenix.Controller
orPermit.Phoenix.LiveView
is not used in a controller or LiveView module. - Validation of configuration: Configuration options can be given as
:opts
touse Permit.Phoenix.Controller
or as callback implementations. In the latter case, there's at least Dialyzer checking the implementations against callback specs; when using the opts, though, there is no specific config validation yet.
We believe Credo can be configured to scan relevant ASTs for irregularities, which should suit most teams fine because it's the de facto standard tool for static code analysis in Elixir - whereas some checks might also get a bit easier (and further reinforced, coming earlier in the process) if we tackle the next idea...
Compile-time optimizations
Once we have static checks in place to clean up rule definitions and configuration, we can think of tackling possible ways of performance optimization - with a goal to leave the public API unchanged as much as possible.
Right now, that's the position we're in:
- Predicate-defining operators (
#{action_name}/3
,permission_to/5
) are already defined as macros, but they expand to code that performs runtime building of permission structures. - Predicate-checking operators (
#{action_name}?/2
,do?/3
) are generated by the Permit macro but defined as runtime functions. Permit.Ecto.accessible_by/4
are build at runtime using Ecto dynamic queries.
Regarding predicate-defining macros, to reduce the resources needed to create structures of permissions, we can likewise also try extracting literals known at compile time from these coming from the call site.
permit()
|> read(Article, status: :published) # literal
|> read(Article, user_id: user.id) # runtime-specific
This can be detected when processing ASTs at compile time, and the literal part can be extracted to something stored and retrieved in a module attribute.
As for the query building, there's also a limit to what we can do because it's evaluated per call and dynamic by design. I can imagine that we could likewise extract out the static part of the query by building it from the literal conditions. Also, the actions' acyclicity check as part of the action definitions' preprocessing can be moved to compile time as opposed to being executed on every traversal of the action forest when building queries.
Because moving things to compile-time is heavily reliant on specific API shape, it'll be best performed when the API is fully stable, so it's unlikely to be tackled before Permit 1.0 is in the works - even though I don't thing it will see a lot of changes at this point. I'd like to point out that it's on our radar, though. Before we approach this, runtime caching is more likely to be worked on.
Caching
Orthogonally to compile-time optimizations, caching authorization-related structures is another viable way to improve Permit's performance. Without diving too deep into caching implementation and tooling, let's discuss it on a conceptual level.
One thing worth noting is that the Permit.Permissions
structure built by can(user)
when defining permissions remains unchanged by nature provided that user
is intact. We could therefore cache the results of the can(...)
calls based on the argument, or a cache key derived thereof.
Then, when asking for whether a specific action is permitted on an object by a subject, just likewise we can try caching the results based on cache keys derived from {subject, action, object}
, and building queries could use the same principle.
In controllers, assuming that during a single controller action evaluation nothing changes in the data, a possible caching opportunity is in the evaluation of each call like can(user) |> read?(record)
- if one such check has already returned true
, there's no reason another one shouldn't.
Open questions
Managing access control in codebases and applications is a broad topical cluster and there are several ideas of expanding Permit's functionality beyond its current scope. We will discuss a few concepts that look interesting on paper, but - for a variety of reasons - there's still brainstorming to be done before we can work on a PoC. It's mostly down to open questions as to inclusion of certain features in the Permit ecosystem's scope, or mapping a particular situation to Permit's currently assumed permission resolution model. Let's get into the details.
New framework integrations
Permit.Absinthe has been a relatively straightforward follow-up to Permit.Phoenix and a good example that Permit can be plugged into wherever we're able to conceptually map an action to a {subject, action, object}
tuple.
What else comes to mind? Let's explore a possible interoperability with Ash policies and Commanded, Elixir's leading CQRS framework.
Ash
Ash framework's policy mechanism is quite self-explanatory. Within an Ash.Resource
, representing a business entity, you define DSL-driven policies like this:
policy action_type(:create) do
authorize_if IsSuperUser
forbid_if Deactivated
authorize_if IsAdminUser
forbid_if RegularUserCanCreate
authorize_if RegularUserAuthorized
end
Within a policy, conditions are evaluated in order and the evaluation is cut off at the first truthy evaluation of a predicate. We could say that, if we only used authorize_if
predicates, this would be equivalent to joining them with OR
. If the conditions pertain to the actor (i.e. current user), we can model it in Permit using pattern matching:
# Ash - resource module, e.g. article.ex
policy action_type(:create) do
authorize_if IsSuperUser
authorize_if IsAdminUser
end
# Permit
def can(%{superuser: true} = _user) do
permit()
|> create(Article)
end
def can(%{admin: true} = _user) do
permit()
|> create(Article)
end
And if the conditions are given for the records themselves using is Ash's expr
syntax, in Permit what we'd do is just add predicates to the Permit structures:
# Ash resource module - article.ex
policy action_type(:read) do
authorize_if expr(status == "published" and visibility == "public")
authorize_if expr(user_id == ^actor(:id))
end
# Permit
def can(%{id: user_id} = _user) do
permit()
|> read(Article, status: "published", visibility: "public")
|> read(Article, user_id: user_id)
end
But one more important characteristics of Ash policies is that, If more than one policy applies to a given request, all these policies must pass. Permit's authorization rules structure is a DNF (disjunctive normal form), which is a disjunction of conjunctions, in other words: several lists of ANDs, joined with OR. In Ash, however, and it still doesn't take into account the forbid_if
possibilities, we would be speaking of a CNF (conjunctive normal form): a conjunction of disjunctions, or several lists of OR conditions, joined with AND.
Converting Ash policies to Permit would be much harder because of the additional forbid_if
possibility, whereas conversion of Permit rules to Ash policies sounds more realistic at first glance. Since Permit intends to provide a single source of truth for authorization across multiple layers of application usage, this approach would seem in line with that. Worth exploring!
Commanded
Commanded is Elixir's most commonly used framework for building applications that follow the CQRS/ES pattern (Command Query Responsibility Segregation / Event Sourcing). We won't be describing it in much detail, but let's focus aspects of particular interest to us: commands, aggregates, queries, projections and middleware.
Recall that we are usually looking for a way to reduce a model we're working on to Permit's tuples of {subject, action, object}
. In such an architecture, commands contain all the information about the action to be performed, including potential data to identify the object. Regarding subject, in Commanded a common practice is to put the "actor" inside event metadata - with which the library annotates all events produced as a result of dispatching the action; however, in certain contexts, it can be appropriate to reason about the actor separately from the subject of an operation (e.g. an admin opening an account on someone's behalf), in which case the subject's identifier might be part of the aggregate, and the actor's identifier still lives in the metadata.
So we have at least one piece of the tuple in place: since commands are modeled as structs, the mapping can be based on the struct module, perhaps by snake_casing
the CommandModule
.
subject
's meaning could be customized to be taken from the metadata, or from the command, or even by calling a function supposed to be defined in the command.
What about the object
? Well, in essence, each command is dispatched to an aggregate, which can be thought of as stripped-down business entity that only carries data relevant to business logic of executing commands and dispatching events related to that aggregate. This gives us an impression that we could map aggregates to objects in our Permit 3-tuples.
Note, though, that it's just one side of the story: the write model. On the read model side, projectors subscribe to events occurring as a result of aggregates and using matching projections translate them into database insertions that facilitate quick readout of denormalized data in a structure of our choice. Projections are typically defined as Ecto schemas, and queried as usual; this would mean that the already established way of authorizing resources in e.g. Permit.Phoenix
could be directly applied to projections, with the action
understood to either always be :read
or customizable to the developer's liking.
Where is Permit's possible layer of action when it comes to authorizing access to commands? Well, command handlers and routing middleware come to mind here, but this is what we'd like to tackle when we actually approach doing a proof of concept; it certainly is going to be a fun topic to explore and experiment with.
Leveraging PostgreSQL row-level security (RLS)
I was pointed to this idea by fellow Elixir devs at the conference, and I must say it's an interesting point. Row-level security is a Postgres-specific feature that allows assigning tables with row security policies that restrict which rows can be returned or manipulated on a per-user basis - instead of sprinkling WHERE user_id = ...
everywhere, you define policies determining visibility or mutability of rows.
Then, the database needs to have the user authenticated in one way or another, to match the current authentication context against per-row policies:
- Canonically, this would have been mapped to the user connecting to the Postgres server, but it's rather impractical when you intend to authenticate individual users of a system in a fine-grained way - though it could be viable for multitenancy or a fixed set of role-based rules.
- Alternatively, the client connecting to Postgres can inject current user ID into the currently processed transaction like:
SELECT set_config('app.current_user_id', $1, true)
or pin this to the currently checked-out connection, with the last argument set totrue
. This means any client connected to the server with access to our database can set this ID, so it's practical for usage in polyglot environments, but requires quite a high level of trust in security of connection. Lastly, when the ID is pinned to the checked-out connection, the server must be guaranteed to reliably reset this variable when checking the connection back in - otherwise it will be leaked to a different process that checks the same connection out from the pool. - More sophisticated setups, such as that implemented in Supabase, build upon the latter approach using additional tooling (PostgREST and custom helpers) - exchanging JWTs between PostgREST and the connecting client (whether it's a frontend app or an API server). The JWT is injected into a session variable and then an
auth.uid()
helper function reads out the user ID from the token, which is then matched against the RLS policies.
As you can see, RLS is very flexible in terms of possible leverage approaches. This makes me want to step back a bit before I approach a possible interoperability between Permit and RLS. Just to name a few issues that would require extra thought:
- We'd have to get at least a little bit opinionated about what approach to database-level user authentication or identification the library would support, and so far authentication has been out of Permit's scope whatsoever.
- Permit-RLS integration would require us to design a protocol of synchronizing authorization rules between the existing Permit syntax and RLS rules living in Postgres. This could go from Postgres to Elixir code (introspection of already defined RLS policies) or the other way around (e.g. translating Permit rules to RLS policies via schema migrations).
After doing a small proof-of-concept it became clear to me that this is a broader project than initially projected. Personally, even though I acknowledge its benefits, I should say row-level security hasn't quite won me yet: there's a certain friction I've stumbled upon between Permit's "in-place", plain-Elixir policies I'm used to, and DDL-defined, SQL-heavy policies living outside the main codebase, which feel like a foreign body and taking business logic outside where it belongs. This tempts me to experiment with Permit serving as a means to relieve this friction at some point. What do you make of RLS's place and possible Permit interoperability? Comments and ideas are welcome and highly appreciated!
Field authorization
Redacting fields not deemed accessible by a certain user is a concern I've been asked about a couple of times. So far, I haven't thought of this as something that should be in Permit's scope. Adding which columns are visible effectively stretches the library from an access-control engine into a presentation filter. That can be fine, but it is a separate concern and worth being clearly delineated from the former, so users understand they are now maintaining two orthogonal rule sets inside one syntax: record-level and field-level layers.
But if we stretch it: the first thing that comes to my mind is augmenting the rule definition syntax with a clause that allows passing [Article, :id, :name]
instead of Article
as the second argument in Permit's rule definition syntax functions:
def can(user) do
permit()
|> read([Article, :id, :name])
|> read(Article, user_id: user.id)
end
This would mean exposing only :id
and :name
of articles to all users (with all other fields redacted), and exposing all fields only to the article's owner.
It looks potentially cool for readout actions. What about writes? Well, this is where I see a risk of responsibility confusion: are we not trying to duplicate the functionality of changesets and validations to some extent? I would say that, for this reason, if we were to implement field-level authorization as part of Permit, I would be in favour of only doing this for read actions, but very likely also extract this out to a separate syntactic solution for masking purposes.
Phoenix route-based authorization
Oftentimes it's considered practical to define access rules directly in the router. be it for top-level route namespacing, or specifying authorization for particular routes based on roles or even URL path parameters.
The issue with trying to conceptualize Permit's adaptability to this is that its paradigm of defining rules for {subject, action, object}
tuples is the limitation here. subject
and action
would be quite straightforward to retrieve, but object
is not there yet. In this place we can have either a resource module (e.g. Article
) or an actual record (%Article{...}
) - we could try having the routes annotated to help match a group of routes with a specific resource.
Overall, however, my stance so far is that this aspect is out of Permit.Phoenix's scope, and controller-level authorization (which it focuses on) is still clearer and more flexible in configuration; and if a developer wants to leverage Permit's rule definitions to authorize route access in one way or another, they should feel free to reference Permit in custom plugs in their pipelines. However, as always, I'm open to thoughts and ideas that might contradict the current view - if router-level authorization is an important element of your workflow, feel free to share your use cases in Permit.Phoenix GitHub repository's issues.
FAQ
What is Permit and what problem does it solve?
Permit is an open‑source, plain‑Elixir authorization library that acts as a single source of truth for permission definitions in Elixir applications. It integrates seamlessly with Phoenix (controllers, LiveView), Ecto, and optionally Absinthe for GraphQL. It aims to centralize and simplify access control logic in Elixir projects.
What are the main components in the Permit ecosystem?
Permit consists of three main packages:
permit
– the core library for defining permissions.permit_ecto
– provides Ecto integration to apply authorization rules to queries.permit_phoenix
– enables automatic authorization and data loading in Phoenix controllers and LiveViews.
How are authorization rules defined in Permit?
Authorization rules are defined in a dedicated module using use Permit.Ecto.Permissions
. Rules follow an ABAC (Attribute-Based Access Control) approach. For example:
def can(%{role: :admin}), do: permit() |> all(Article)
def can(%{id: id}), do: permit() |> all(Article, user_id: id) |> read(Article)