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.

Table of contents

    What is Permit?

    Permit is an authorization library, available as three Hex packages:

    • permit - the core application, providing the syntax for authorization rules
    • permit_ecto - depending on Permit core, providing automatic conversion of authorization rules to Ecto queries
    • permit_phoenix - depending on Permit core and optionally Permit.Ecto, it preloads and checks authorization for records in controller and LiveView actions

    You can use all three, or only those which you need - only the core is always required.

    What makes Permit interesting?

    While many of the previously existing authorization libraries for Elixir have been a great job and are great in their own right, our experiences using them has rarely been satisfying. Not being fond of Ruby on Rails altogether, some of us have had experience pairing it with CanCanCan which was seamlessly integrable with controllers and able to convert conditions into SQL queries. This served as an inspiration, although Permit is obviously not a port. It’s got a vastly different syntax and is written with extensibility in mind.

    When exploring possibiliities, out of many available options, Bodyguard looked quite promising, but still required the developer to manually define Ecto query scopes for accessible records, which means it is twice the job.

    A similar approach is employed in LetMe, but its API is a DSL and we generally tend to be skeptical about DSLs at Curiosum.

    Canada paired with Canary provides Plug integration, but LiveView was still missing from the feature set and still conditions were defined using a function-based syntax, which meant no way to convert them to Ecto queries.

    Permit is Elixir’s first authorization library that automatically converts authorization conditions to Ecto queries and provides a mechanism of automatically running them in controller actions as well as LiveView live actions.

    Usage examples

    A lot of magic happens under the hood, but the project was conceived with being as plain-Elixir as possible, so authorization rules can be written as simple as this:

    defmodule MyApp.Authorization do
      use Permit.Ecto, permissions_module: MyApp.Permissions, repo: MyApp.Repo
    end
    
    defmodule MyApp.Permissions do
      use Permit.Ecto.Permissions, actions_module: Permit.Actions.PhoenixActions
    
      def can(%{role: :admin} = user) do
        permit()
        |> all(MyApp.Blog.Article)
      end
    
      def can(%{id: user_id} = user) do
        permit()
        |> all(MyApp.Blog.Article, user_id: user_id)
        |> read(MyApp.Blog.Article) # allows :index and :show
      end
    
      def can(user), do: permit()
    end

    Typically, in Phoenix applications using standard controller action naming, the :index and :show actions are authorized when the :read permission is given, and the Permit.Actions.PhoenixActions module is responsible for that. Refer to documentation for Permit.Actions to understand how to use Permit with custom action names.

    Checking authorization of a given user to perform an action on a resource is as simple as this:

    # article = %Article{user_id: 1}
    # user1 = %User{id: 1}
    # user2 = %User{id: 2}
    # admin = %User{id: 3, role: :admin}
    
    iex> can(user1) |> index?(article)
    true
    
    iex> can(user1) |> update?(article)
    true
    
    iex> can(user2) |> show?(article)
    true
    
    iex> can(user2) |> update?(article)
    false
    
    iex> can(admin) |> update?(article)
    true

    With Permit.Ecto, converting applicable authorization conditions to an Ecto query only takes this:

    iex> MyApp.Authorization.accessible_by!(user, :update, Article)
    #Ecto.Query<from a0 in MyApp.Blog.Article, where: a0.user_id == ^1>
    
    iex> MyApp.Authorization.accessible_by!(admin, :update, Article)
    #Ecto.Query<from a0 in MyApp.Blog.Article, where: ^true>

    Worth noting is that permissions can also be given as functions, but it is not recommended since you will have to provide a way to convert them to Ecto queries manually if you want to use Permit.Ecto’s automatic query conversions.

    And with Permit.Phoenix, guarding all actions in a controller against defined permissions and making it automatically preload records into assigns is just about this:

    defmodule MyAppWeb.ArticleController do
      use MyAppWeb, :controller
    
      use Permit.Phoenix.Controller,
        authorization_module: MyApp.Authorization,
        resource_module: MyApp.Blog.Article
    
      def show(conn, params) do
        # If @current_user is authorized to :show an Article loaded by
            # params[:id], then it is assigned to @loaded_resource
      end
    
        def index(conn, params) do
            # If @current_user is authorized to :index Articles, then it calls
            # MyApp.Authorization.accessible_by(conn.assigns[:current_user], :index, Article)
            # then executes the query and assigns the result to @loaded_resources
      end
    
      @impl true
      def handle_unauthorized(action, conn) do
            # Executed when authorization is not granted, defaults to redirecting
        # to `/`.
      end
    end

    If you ask about an analogous example for Phoenix LiveView, there you go!

    # To use Permit.Phoenix with LiveView, put your routes under a live_session
    defmodule MyAppWeb.Router do
      # ...
      scope "/", MyAppWeb do
        live_session :permit_session, on_mount: Permit.Phoenix.LiveView.AuthorizeHook do
                live "/live/articles", ArticleLive.Index, :index
                # ...
        end
      end
    end
    
    defmodule MyAppWeb.ArticleLive.Index do
      use MyAppWeb, :live_view
    
      use Permit.Phoenix.LiveView,
        authorization_module: MyApp.Authorization,
        resource_module: MyApp.Blog.Article
      
      @impl true
      def mount(_params, _session, socket) do
        # @loaded_resources is available if @current_user is authorized to :index Articles
        # (:index is taken from @live_action)
      end
    
      @impl true
        def handle_params(params, url, socket) do
        # authorization and preloading into @loaded_resource or @loaded_resources
        # is performed again if @live_action changes
      end
    
      @impl true
      def fetch_subject(_socket, session) do
            # Mandatory - can be put in MyAppWeb
        MyAppWeb.Accounts.get_user_by_session_token(session["user_token"])
      end
    
      @impl true
      def handle_unauthorized(action, socket) do
        # Defaults to:
        # {:halt, push_redirect(socket, to: fallback_path(action, socket, opts))}
        #
        # Alternatively, you can redirect differently or {:cont, do_anything(conn)}.
      end
    end

    For more on configuration and usage examples, see documentation for Permit, Permit.Ecto and Permit.Phoenix.

    Ideas for the future

    In a similar way to Permit.Phoenix, the core package and Permit.Ecto can also potentially be adapted to be plugged into other frameworks. Just for example, it could be interesting to try aligning it with Ash, even though it has its own idea on how to manage authorization.

    Another interesting topic to explore would be integrating Permit with Absinthe and reasoning on how to apply or extend Permit’s model for usage with GraphQL.

    Something not covered by Permit as of now is authorization at record field level, and while it is not currently in the plans, it might be worth exploring as well.

    Would you like to contribute to the library's development? If so, you're welcome to fork the repositories and share your ideas with us!

    How to contribute?

    • Do test it out! We've already used the library in a few production apps, and there's no better way to gather insights about possible improvements than practical usage.
    • Feel invited to report bugs and feature suggestions in the library's GitHub repositories, and of course your code contributions are more than welcome! There's always stuff to do in the project's issue list, and we'll make sure to tag “good first issue” tasks when possible.
    • If one of our “Ideas for the future” resonates with you, or you have your own ideas on how to make the library better, we will be happy to discuss it with us or review your proposed solutions - the best place for such a discussion is the library’s issue list.

    Acknowledgements

    I'd like to call for applause for Piotr Lisowski, who spearheaded the works on the project for a long time and, most importantly, made the Ecto integration possible!

    I've had invaluable support from our awesome Curiosum team throughout the works on the project, so let me express a great deal of gratitude for all the fruitful discussions we've had over time!

    Summing up

    Permit has already helped us simplify and reinforce permission management in a number of applications we've been developing, and we encourage you to try it out, include the library https://hex.pm/packages/permit in your application's Mixfile and play around a bit.

    There is surely a lot more ground to cover in the topic of Elixir authorization, and we are committed to maintaining the library to make developers’ lives easier.

    FAQ

    What is Permit in Phoenix?

    Permit is an authorization library for Phoenix apps, providing a syntax for defining authorization rules and converting them to Ecto queries.

    How does Permit enhance resource authorization?

    It automates the conversion of authorization conditions to Ecto queries and seamlessly integrates with Phoenix controller and LiveView actions for efficient permission checks.

    What are the components of Permit?

    Permit consists of three packages: Permit core, Permit.Ecto for Ecto queries, and Permit.Phoenix for Phoenix integration.

    How do you define authorization rules with Permit?

    Authorization rules are defined in Elixir syntax within a designated module, specifying permissions based on user roles or attributes.

    How does Permit handle permissions in Phoenix applications?

    It checks permissions automatically in Phoenix controllers and LiveViews, preloading records into assigns based on authorization.

    Can Permit be integrated with other frameworks or technologies?

    While primarily designed for Phoenix, the library could potentially be adapted for other frameworks or used with GraphQL.

    How can you contribute to the development of Permit?

    Contributions can include testing, bug reporting, feature suggestions, and code contributions, with an open invitation for community collaboration.

    Michał Buszkiewicz, Elixir Developer
    Michał Buszkiewicz Curiosum Founder & CTO

    Read more
    on #curiosum blog