Authorization & Access Control - ElixirConf EU 2025 presentation by Michal Buszkiewicz

Article autor
October 6, 2025
Authorization & Access Control - ElixirConf EU 2025 presentation by Michal Buszkiewicz
Elixir Newsletter
Join Elixir newsletter

Subscribe to receive Elixir news to your inbox every two weeks.

Oops! Something went wrong while submitting the form.
Elixir Newsletter
Expand your skills

Download free e-books, watch expert tech talks, and explore open-source projects. Everything you need to grow as a developer - completely free.

Table of contents

In ElixirConf EU 2025 I spoke about approaches to authorization and access control in Elixir and presented how the Permit library fits into that.

Now, here’s a treat for those of you who prefer consuming content in written form: I invite you to read this what I name a semi-transcript of the talk. Get the essence of what I talked about, with my clumsy so’s, uh’s, um’s, and yeah’s edited out. Also, I will put some additional explanation or follow-up wherever something has happened post-conference.

I’m also omitting slides except for these that illustrate syntax snippets of Permit usage, since most of them convey the ideas I was talking about.

Intro

Hello and welcome to today's presentation, in which we are going to cover the topic of managing authorization and access control in Elixir codebases. We'll, as the subtitle states, have a bit of case studies and practical solutions using Elixir, for which we'll mainly present a library that we have developed for making our lives easier.

Just to be fully clear, this talk is only going to cover authorization, not authentication. We're not going to be dealing with anything like phx.gen.auth and so on. We're also not going to be talking about API authorization, so OAuth, OpenID, and these kinds of things are out of scope of this talk. These are obviously also interesting subjects that we could touch on in the future.

Authorization Paradigms and Architectures

There are a few approaches to access control and several notable top-level architectures we can use.

  1. The first one, which might be seen as trivial, is ACL (Access Control Lists). This is a structure in which we declare who can do what on what. The advantage is that, for example, if we use ETS, we have O(1) lookups. But if we wanted to implement this in a complex system, we get a combinatorial explosion because we have to add an entry for all users and possibly all resources we need to cover.
  2. The second one is RBAC (Role-Based Access Control). Users are assigned roles, which in turn are assigned permissions. This has the advantage of auditability and clarity. But we can fall into "role creep," which means we can run into having dozens of near-duplicates for special cases.
  3. Then we have ABAC (Attribute-Based Access Control), which is basically allowing an action if a number of specific attribute conditions are satisfied. This is what we'll be showing for the Permit library because this model is effectively realized by it. The advantage is flexibility and the power of expression. But there is also a combinatorial policies problem, and we need to be careful in testing, because while the engine might be well-tested, the implementation of the rules themselves is our responsibility.
  4. And then we have PBAC (Policy-Based Access Control), implemented by quite a few libraries in the Elixir world: Bodyguard, Canary, Ash policies, and the let_me DSL, as well as OPA, which is like authorization-as-a-service. The advantage of this is readability and expressive power. But especially with DSLs, there can be a steeper learning curve and potential debugging issues.

We also have some potential dilemmas about keeping stuff inside the BEAM—which is what Bodyguard, Permit, Ash Policy do—versus offloading it. The advantages of keeping it in-app are minimal latencies and no extra infrastructure; the implementation is relatively simple but requires custom UIs and a proper data model design. It wins in monolithic and stable designs. We are also able to offload this authorization aspect to external libraries like Zed, Casbin, or OPA. This has an advantage in polyglot systems, as you don't have to rewrite rules for each part. Those tools often have integrated UIs for live updates, making governance easier. The risks, however, include network hops, extra moving parts, and potential cost or data residency issues. It wins in polyglot and multi-team settings. It's up to the developer which one to use.

Common Concerns in Elixir Projects

Some common concerns we hit in Elixir projects are repetition and the lack of a single source of truth, which is especially connected to the "load and authorize" pattern used in Phoenix controllers, LiveViews, and GraphQL resolvers. There's also Plug boilerplate, the need for expressive power when designing a system, and the complexity of multi-tenancy implementation.

To give you some practical examples: in a system for managing device pools, there was plenty of permission cascading down the resource tree. We created a hybrid system between attribute-based and role-based access control. Users were organized into teams with predefined, hardcoded roles and several levels of permissions, with checks in LiveView event handlers and a controller Plug.

Another example is a medical clinic management software where we used strict router-level separation between areas, and within the clinic-facing part, we used the Permit library to implement an RBAC system.

Streamlining Authorization with Permit

Here comes the next part, which I'm pretty excited to tell you about: the Permit library we've written. It consists of four libraries right now: the base one, Permit.Ecto for Ecto integration, Permit.Phoenix for Phoenix and LiveView integrations, and the upcoming Permit.Absinthe for Absinthe integration.

Some of the patterns and features we liked when dealing with authorization: we like stuff that can be exposed as an API in plain Elixir, without an additional DSL. We want a single source of truth for all frameworks and components, because we don't want to repeat ourselves in controllers, LiveViews, and other data-accessing layers. This relates to the "load and authorize" pattern. For single resources and lists, we want to enter a controller action or LiveView handler and have the record already loaded and authorized. If not, a handler deals with the unauthorized case.

We want it to be powered by Ecto but not tightly coupled to it, so the Ecto integration is opt-in. The same goes for Phoenix, LiveView, and Absinthe, which is why there are four libraries. We want it to be seamless with Plug and LiveView, and potentially others. There's an API you can use to produce your own integration with any other framework. It is framework-decoupled.

Our inspirations included Ruby's CanCan. We liked its convenient way of defining rules and generating queries. But as we adapted it to Elixir, we shifted to a DSL-free syntax. We also decided to only allow positive declarations (no cannot) because we found that confusing in large rule sets. Only what is explicitly declared as authorized is authorized. We also wanted it to be modular and loosely coupled.

This is how you define conditions in Permit. An authorization module uses Permit and a permissions module. You then plug in an actions module that defines your actions. If used in controllers, it reads actions from controllers and live actions from the router. For example, you can permit admins to do everything on an article, while other users can only perform actions on their own articles. They might be allowed to read all articles regardless of conditions, but other actions are only permitted for their own.

Permission definitions are loaded from a single module. Functions like read, create, update, and so on are generated corresponding to actions defined in an actions module. Conditions are given via keyword lists. The can function constructs a struct to ask whether a certain permission is given. The can function's argument is the subject, which is the current user.

The model is primarily attribute-based, implemented as "policy as code". And ACL, RBAC, and ReBAC are reducible to Permit's ABAC model. You can see examples where a rule is based on a specific user ID (ACL), a clear role (RBAC), or a relationship entity like "writer" (ReBAC).

Permit + Ecto

Permit combined with Ecto facilitates DB-backed authorization. This is not about native Postgres role-level security, though we may explore that later. Because conditions are given as keyword lists, we can automatically construct Ecto queries out of them.

The configuration uses an Ecto repository and Permit.Ecto.Permissions. The authorization syntax is the same, and conditions can be based on nested associations. The Ecto integration allows you to specify rules using ilike syntax, which gets converted to a regex for in-memory checks or used in a generated query. You can check permissions on a specific record or use the accessible_by function to generate a query that fetches only authorized records.

Permit + Phoenix: Authorization in Controllers

Next is Permit Phoenix, for DRY authorization in controllers. We want to avoid copy-pasting boilerplate and forgetting to authorize an action. We also don't want to mix authorization rules with controller code or filter records inefficiently. The "load and authorize" pattern is facilitated by plugging it into the controller and pointing to your authorization module.

To handle failures, there is an optional handle_unauthorized callback that defaults to redirecting but can be customized per-controller. It has access to the Plug conn and the action being attempted. You can also control how the query is built with base_query and finalize_query callbacks for pre- and post-processing. Other customization options include except for opting out, fallback_path, handle_not_found, and specifying ID parameter names.

Permit + Phoenix LiveView: Navigation & Events

For LiveView, we authorize navigation and events by hooking into the router with the Permit.Phoenix.LiveView.AuthorizeHook hook. It hooks into handle_event and handle_params on mount.

Permit actions are matched against the live action names from the router. You can also create a customizable mapping between event names and permit actions.

For example, the edit live action is mapped to the edit permit action. When handling events, authorization is based on this mapping, and the loaded resource is put into assigns. Failure handling follows standard LiveView conventions using cont or halt.

Editorial note: Configuration is done similarly to how it works in controllers. A standard pattern would be to use MyAppWeb's live_view block to set up common configuration across the app.

Upcoming: Permit + Absinthe

Editorial note: No longer strictly upcoming*, as Permit.Absinthe is now live: https://github.com/curiosum-dev/permit_absinthe - it’s a proof-of-concept that works for simple use cases and we’re exploring what it would take to make it relevant for production-grade systems.*

The upcoming Permit Absinthe is a tool for authorizing access in GraphQL APIs. This was challenging due to GraphQL's structure of queries and mutations. The approach is to first map an object type to an Ecto schema. Then, we map fields to actions in both queries and mutations. Query fields will mostly map to the read action.

We created simple resolver functions, load_and_authorize_all and load_and_authorize_one, to preload plural and singular resources respectively. It can also be done via middleware, allowing you to write your own resolver that consumes the preloaded, authorized records from the resolution context.

Editorial note: The resolver functions have been merged into load_and_authorize , inferring action arity from the resolved field’s definition (single object vs list(...)). A GraphQL directive has been added, too - see Q&A.

Future Directions and Contributing

In terms of future directions and ideas, we can obviously utilize Phoenix Streams and Scopes. We can think of integrating this with, for example, Commanded, or trying to figure out how it can play with Ash policies. We can research on how we can use row-level security. A policy playground and visualizer would be very handy. Audit, static checking of permissions, caching and compile-time optimizations are other ideas. And up for discussion is field authorization and Phoenix route checking.

Your ideas are very welcome. Please feel invited to contribute in any way. Test out Permit and all the related libraries in practice and do share your opinions on how stable and usable and helpful it is for you. On our GitHub, you are obviously invited to submit ideas for improvement, report bugs, contribute pull requests, and discuss.

Editorial note: See my follow-up article for more on future directions: Phoenix scopes, static analysis, compile-time optimizations and caching, Postgres RLS, Commanded, route-based authorization, and Ash.

Q&A

Did you consider Ash?

I have, but the point of this is to be an alternative that doesn't rely on any components of Ash, so there is a choice. I think using one doesn't contradict the other. I'm going to try finding a way to make them play together, so this is still up for discussion. I am open to discussing that.

Editorial note: While I admire that Ash already has its own solution to the authorization issue in Ash.Policy, with all due respect to the framework, I do not believe adding authorization logic to the system should entail loading such a heavy library.

Yes, Ash is modular. ButAsh.Policy lives in the main, big ash repository. It transitively depends on quite a few libraries including ecto , and I don’t want Permit to strictly depend on ecto: of course using Permit.Ecto is awesome and one of the main perks of Permit, but you totally don’t have to do this.

I discussed how we could reach Ash-Permit interoperability in my follow-up post on the future directions for Permit.

I assume that everything is based upon what's written in the database at the time. If somebody's permissions have been removed from the database, is that how it's resolving when the check is hit?

This is all written as code, so any change is compiled. But if you specify this based on some relationship between entities... let me come back to that thing in Permit.Ecto. If there is a relationship between articles and relations as a separate entity, and you remove a record from the relations table, then that's what you are trying to do. If you want a UI where you can remove permissions on the fly, this is the way to go.

Editorial note: Permit currently focuses on providing the backend foundation for authorization logic in your codebases. It’s up to you to design how this is supposed to work in your system - if you’d like to introduce a notion of permissions persisted in the DB, then you need to design all the related entities and create Permit rules based on that.

Therefore, any UIs have also been out of scope so far. However, we’re thinking of providing generators or some other means to conveniently bootstrap a permission system or inject one into the codebase.

Have you considered integrating Permit with directives, since they function similarly to middlewares? What is the angle there and is there an opportunity?

We are at a very early stage with this one. Honestly, I wrote this a few days ago in about 30 minutes; it was the first thing that came to my mind. It's just a simple proof of concept right now. Maybe before we even hit the 0.1 version, it's going to look like that. I'm totally open to this.

Editorial note: This has been addressed and directive support has been added. You need to add the prototype schema and use the directive in the field. I’m not a fan of how Absinthe’s directive-related syntax is documented and working, though. Maybe there’s a better way to do this - let me know if you know one.

Thanks for getting here!

Visit the https://permit.curiosum.com for useful usage cheatsheets, and see the library’s environment for discussion and open-source contributions:

Repository links:

Related posts

Dive deeper into this topic with these related posts

Looking back at ElixirConf EU 2025 - networking, speaking and Permit

It's 3 months since ElixirConf EU 2025 in Kraków, and my presentation about authorization and access control in Elixir with the Permit library is now available to watch on YouTube.

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.

Authorize access to your Phoenix app with Permit

There's arguably no non-trivial web app that doesn't need to manage resource authorization, and Curiosum’s Permit library comes in handy especially if your stack is Phoenix, LiveView and Ecto. We’ve just released it so explore its features.

You might also like

Discover more content from this category

Top 5 Elixir Skills to Learn in 2021 [for Juniors]

Elixir is not magic - despite being easy to write in and learn, you need a strong foundation. In this short article, I will give you my personal list of things that are necessary (or at least very much a “should-have”) for Junior Elixir developers.

5 top-tier companies that use Elixir

Elixir is a pretty capable language - and it consistently ranks near the top of most loved and wanted languages rankings.

Connecting Arduino to Elixir via Circuits.UART: Real-Time Serial Monitoring

While Elixir isn't the programming language most commonly associated with embedded software development, it offers features that could make it an unexpectedly good choice in this domain. The qualities for which we appreciate Elixir so much - concurrency and fault tolerance - also play a key role in more complex embedded systems, where multiple tasks must run in parallel and recover gracefully from failures.