(Nuclear Access Restriction Policies)
Narp is an easy and flexible way to authorize function calls in elixir. It has partly been inspired by the pundit project and can be used with plain elixir or beneath a phoenix project.
- Authorize function calls easily
- Convention based syntax for clean and neat function definitions
- More complex definitions and centralized authorization policies possible
- Default policies, when no matching policy was found
Add
{:narp, git: "https://github.com/ftaebi/narp.git"},
to the deps section of your projects mix file. Alternatively you can use the available hex package by using.
{:narp, "~> 0.0.1"},
Narp can now be enabled by importing the Narp module.
Plain Elixir:
defmodule YourModule do
import Narp
...
end
Phoenix: import in WebController module to make Narp accessible in every controller:
defmodule YourProject.Web do
def controller do
quote do
import Narp
...
end
end
end
In order to guard a function Narp provides the defg (define guarded) keyword. A guarded function would be defined like this:
defmodule YourProject.YourModule
defg your_function(params) do
...
end
end
In this case Narp will look for a policy module called YourModulePolicy and call a function with the same name as the guarded function: YourModulePolicy.your_function(params). Guard functions in policy modules should return matchable data which you can use to implement conditional behavior in your guarded function.
Example:
defmodule YourProject.YourRessourceController do
defg show(conn, %{"user_id" => user_id}) do
:valid_user_id ->
render conn, "show.json", data: %{hello: "World"}
:invalid_user_id ->
conn
|> put_status(:forbidden)
|> put_error("Viewing or editing other users data is not allowed.")
end
end
defmodule YourProject.YourRessourceControllerPolicy do
def show(conn, %{"user_id" => user_id}) do
if valid_user_id?(user_id) do
:valid_user_id
else
:invalid_user_id
end
end
defp valid_user_id?(user_id) do
user_id == 15
end
end
Sometimes you may want to use a single policy module to guard multiple functions in different modules. In this case you can tell Narp which policy module and which policy function to use:
defmodule YourProject.YourModule
defg your_function(params), with: {YourAwesomePolicyModule, :your_awesome_policy_function} do
...
end
end
Example:
defmodule YourProject.YourRessourceController do
defg show(conn, %{"user_id" => user_id}), with: {YourAwesomePolicyModule, :user_has_id_15} do
:valid_user_id ->
render conn, "show.json", data: %{hello: "World"}
:invalid_user_id ->
conn
|> put_status(:forbidden)
|> put_error("Viewing or editing other users data is not allowed.")
end
end
defmodule YourProject.YourOtherRessourceController do
defg get(conn, %{"user_id" => user_id}), with: {YourAwesomePolicyModule, :user_has_id_15} do
:valid_user_id ->
render conn, "get.json", data: %{hello: "World"}
:invalid_user_id ->
conn
|> put_status(:forbidden)
|> put_error("Viewing or editing other users data is not allowed.")
end
end
defmodule YourProject.YourAwesomePolicy do
def user_has_id_15(conn, %{"user_id" => user_id}) do
if valid_user_id?(user_id) do
:valid_user_id
else
:invalid_user_id
end
end
defp valid_user_id?(user_id) do
user_id == 15
end
end
Sometimes you may want to use one policy to guard many functions by default. In this cases you can define a default policy by using the function name default_policy() in the corresponding policy module. It is called, whenever no matching policy function could be found.
Example:
defmodule YourProject.YourRessourceController do
defg show(conn, %{"user_id" => user_id}) do
:ok ->
render conn, "show.json", data: %{hello: "World"}
{:error, msg} ->
conn
|> put_status(:forbidden)
|> put_error(msg)
end
end
defmodule YourProject.YourRessourceControllerPolicy do
def default_policy(conn, %{"user_id" => user_id}) do
case user_id do
15 -> :ok
_ -> {:error, "only 15 allowed"}
end
end
end
Special thanks to:
- Slawomir Dabek
- Matthias Lindhorst