Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/system76/policy
https://github.com/system76/policy
Last synced: about 2 months ago
JSON representation
- Host: GitHub
- URL: https://github.com/system76/policy
- Owner: system76
- License: mit
- Created: 2016-08-26T22:16:52.000Z (over 8 years ago)
- Default Branch: master
- Last Pushed: 2016-08-30T20:01:40.000Z (over 8 years ago)
- Last Synced: 2024-10-10T03:38:29.599Z (2 months ago)
- Language: Elixir
- Size: 9.77 KB
- Stars: 5
- Watchers: 4
- Forks: 2
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE.md
Awesome Lists containing this project
README
# Policy
Policy is an authorization management framework for Phoenix. It aims to be
minimally invasive and secure by default.## Installation
If [available in Hex](https://hex.pm/docs/publish), the package can be installed as:
1. Add `policy` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[{:policy, "~> 1.0"}]
end
```## Usage
Permissions are specified by implementing the `Policy` protocol for each
controlled entity. Policies require two methods:* **`permit?/3`** takes the entity in question, the current user (or `nil`), and
the action to be taken as an atom and returns a boolean.
* **`scope/2`** takes the entity in question and the current user (or `nil`).
It returns an Ecto query scoped to all the entities that user can view.For example, assume we have a `Post` entity. Users can view all posts and edit
their own posts, and admins can view and edit any post. Furthermore, admins
can view all posts, but users can only view published posts or posts they own.```elixir
defimpl Policy, for: Post do
import Ecto.Query# First, anyone can read a post
def permit?(_post, _user, :read), do: true
# Second, anonymous users can't do anything else
def permit?(_post, nil, _action), do: false
# Third, admins can do anything
def permit?(_post, %User{admin: true}, _action), do: true
# Finally, users can do anything to their own posts
def permit?(%Post{user_id: user_id}, %User{id: user_id}, _action), do: true# Admins can view the whole `posts` table
def scope(_post, %User{admin: true}), do: Post
# Anonymous users can only view public posts
def scope(_post, nil), do: from(p in Post, where: p.published)
# Users can view published posts and posts they own
def scope(_post, user), do: from p in Post, where: p.published or p.user_id == ^user.id
end
````Policy` has a couple of implementations out of the box which allow it to be
invoked with either a bare module name or an Ecto changeset```elixir
# These are all equivalent
Policy.permit? %Post{}, current_user, :read
Policy.permit? Post, current_user, :read
Policy.permit? Ecto.Changeset.change(%Post{}, %{}), current_user, :read
```Bare module name permissions are determined based on the entity's default
values. Changeset permissions are determined based on an entity with all the
proposed changes applied.Policies are enforced at a controller level. Controllers can import
`Policy.Helpers` to get the `:ensure_authorization` plug and the `authorize!/2`
function.`authorize!/2` takes the `conn` and a model or list of models, throws a
`Policy.Exception` if the model (or any one of the list) is not authorized, and
returns a `conn` that is marked as authorized.The `:ensure_authorization` plug will throw a `Policy.Exception` if a Controller
tries to send without running `authorize!/2`.`Policy.Exception` is registered with `Plug` to return a 403 Unauthorized.
An example controller looks like this:
```elixir
defmodule MyApp.PostController do
use MyApp.Web, :controller
import Policy.Helpersalias MyApp.Post
plug :ensure_authorization
def index(conn, _params) do
posts = Post
|> Policy.scope(conn.assigns[:current_user])
|> Repo.allconn = conn
|> authorize!(posts)
|> render("index.html", posts: posts)
enddef new(conn, _params) do
changeset = Post.changeset(%Post{})conn
|> authorize!(changeset)
|> render("new.html", changeset: changeset)
enddef create(conn, %{"post" => post_params}) do
changeset = Post.changeset(%Post{}, post_params)conn = authorize!(conn, changeset)
case Repo.insert(changeset) do
{:ok, _post} ->
conn
|> put_flash(:info, "post created successfully.")
|> redirect(to: post_path(conn, :index))
{:error, changeset} ->
render(conn, "new.html", changeset: changeset)
end
enddef show(conn, %{"id" => id}) do
post = Repo.get!(Post, id)conn
|> authorize!(post)
|> render("show.html", post: post)
enddef edit(conn, %{"id" => id}) do
post = Repo.get!(Post, id)
changeset = Post.changeset(post)conn
|> authorize!(changeset)
|> render("edit.html", post: post, changeset: changeset)
enddef update(conn, %{"id" => id, "post" => post_params}) do
post = Repo.get!(Post, id)
changeset = Post.changeset(post, post_params)conn = authorize!(conn, changeset)
case Repo.update(changeset) do
{:ok, post} ->
conn
|> put_flash(:info, "post updated successfully.")
|> redirect(to: post_path(conn, :show, post))
{:error, changeset} ->
render(conn, "edit.html", post: post, changeset: changeset)
end
enddef delete(conn, %{"id" => id}) do
post = Repo.get!(Post, id)conn = authorize!(conn, post)
# Here we use delete! (with a bang) because we expect
# it to always work (and if it does not, it will raise).
Repo.delete!(post)conn
|> put_flash(:info, "post deleted successfully.")
|> redirect(to: post_path(conn, :index))
end
end
````authorize!/2` assumes that the current user is in `conn.assigns[:current_user]`
and that the controller action is set by Phoenix. It maps the seven RESTful
actions to `:create`, `:read`, `:update`, and `:delete`, and passes all other
actions through as-is.