Voter based authorization for Elixir, inspired by Symfony.
The package can be installed by adding access_decision_manager
to your list of dependencies in mix.exs
:
def deps do
[
{:access_decision_manager, "~> 0.3.0"}
]
end
Add your configuration
# Access Decision Manager (permission voting)
config :access_decision_manager,
voters: [
MyApp.Auth.FooVoter,
MyApp.Auth.BarVoter,
]
Security voters are a granular way of checking permissions (e.g. "can this specific user edit the given item?").
All voters are called each time you use thegranted?()
function. AccessDecisionManager then takes the responses from all voters and makes the final decision (to allow or deny access to the resource) according to the strategy defined, which can be: :strategy_affirmative
, :strategy_consensus
or :strategy_unanimous
.
A custom voter needs to implement the AccessDecisionManager.Voter
behavior:
defmodule Mypp.Auth.FooVoter do
@behaviour AccessDecisionManager.Voter
def vote(_primary_subject, _attribute, _secondary_subject), do :access_abstain
end
Important: since every voter is called, every voter must return a decision. If the attribute and subjects do not apply to the voter, then abstain from voting by returning
:access_abstain
.
defmodule MyApp.Auth.FooVoter do
@behaviour AccessDecisionManager.Voter
alias MyApp.User
@doc """
Is the %User{} allowed to create %Foo{}?
"""
def vote(%User{} = user, "CREATE_FOO", nil) do
if create_allowed?(user), do: :access_granted, else: :access_denied
end
def vote(_primary_subject, _attribute, _secondary_user), do: :access_abstain
defp create_allowed?(user) do
# your permission logic goes here (db checks, etc.)
end
end
defmodule MyAppWeb.FooController do
import AccessDecisionManager
def create_foo(conn) do
if granted?(conn.assigns.current_user, "CREATE_FOO") do
# permission granted, create some foo
else
# permission denied, no foo for you
end
end
end
defmodule MyApp.Auth.FooVoter do
alias MyApp.User
alias MyApp.Foo
alias MyApp.Bar
@behaviour AccessDecisionManager.Voter
@supported_attributes [
"CREATE_BAR",
"UPDATE_BAR",
"DELETE_BAR"
]
@doc """
Is the %User{} allowed to CRUD %Bar{} on %Foo{}?
"""
def vote(%User{} = user, attribute, %Foo{} = foo) when attribute in @supported_attributes do
op_allowed(user, attribute, foo)
end
def vote(_primary_subject, _attribute, _secondary_subject), do: :access_abstain
defp op_allowed(%User{} = user, "CREATE_BAR", %Foo{} = foo) do
# your permission logic goes here (db checks, etc.)
:access_granted
end
defp op_allowed(%User{} = user, "UPDATE_BAR", %Foo{} = foo) do
# your permission logic goes here (db checks, etc.)
:access_granted
end
defp op_allowed(%User{} = user, "DELETE_BAR", %Foo{} = foo) do
# your permission logic goes here (db checks, etc.)
:access_denied
end
end
defmodule MyAppWeb.BarController do
import AccessDecisionManager
alias MyApp.Foo
def create_bar(conn, %{"foo_id" => foo_id}) do
foo = Repo.get(Foo, foo_id)
if granted?(conn.assigns.current_user, "CREATE_BAR", foo) do
# permission granted, create some foo.bar
else
# permission denied, no foo.bar for you
end
end
end
Normally, only one voter will vote at any given time (the rest will "abstain", which means they return :access_abstain
). But in theory, you could make multiple voters vote for one attribute and subject. For instance, suppose you have one voter that checks if the user is a member of the site and a second one that checks if the user is older than 18.
To handle these cases, the access decision manager uses an access decision strategy. You can configure this to suit your needs. There are three strategies available:
:strategy_affirmative
(default)
This grants access as soon as there is one voter granting access.
:strategy_consensus
This grants access if there are more voters granting access than denying.
:strategy_unanimous
This only grants access if there is no voter denying access. If all voters abstained from voting, the decision is based on the allow_if_all_abstain
config option (which defaults to false).
Support for
:strategy_consensus
is TBD.
In the above scenario, both voters should grant access in order to grant access to the user to read the post. In this case, the default strategy is no longer valid and :strategy_unanimous
should be used instead. You can set this in the security configuration:
# config.exs
config :access_decision_manager,
voters: [MyApp.Auth.FooVoter],
strategy: :strategy_unanimous,
allow_if_all_abstain: false