{"id":18485406,"url":"https://github.com/system76/policy","last_synced_at":"2025-08-01T00:06:35.792Z","repository":{"id":57535460,"uuid":"66682581","full_name":"system76/policy","owner":"system76","description":null,"archived":false,"fork":false,"pushed_at":"2016-08-30T20:01:40.000Z","size":10,"stargazers_count":5,"open_issues_count":0,"forks_count":2,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-03-23T17:45:48.648Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/system76.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2016-08-26T22:16:52.000Z","updated_at":"2023-09-01T12:15:24.000Z","dependencies_parsed_at":"2022-08-29T00:40:55.263Z","dependency_job_id":null,"html_url":"https://github.com/system76/policy","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/system76%2Fpolicy","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/system76%2Fpolicy/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/system76%2Fpolicy/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/system76%2Fpolicy/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/system76","download_url":"https://codeload.github.com/system76/policy/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247912760,"owners_count":21017043,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-06T12:45:06.644Z","updated_at":"2025-04-08T19:32:36.561Z","avatar_url":"https://github.com/system76.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Policy\n\nPolicy is an authorization management framework for Phoenix.  It aims to be\nminimally invasive and secure by default.\n\n## Installation\n\nIf [available in Hex](https://hex.pm/docs/publish), the package can be installed as:\n\n  1. Add `policy` to your list of dependencies in `mix.exs`:\n\n    ```elixir\n    def deps do\n      [{:policy, \"~\u003e 1.0\"}]\n    end\n    ```\n\n## Usage\n\nPermissions are specified by implementing the `Policy` protocol for each\ncontrolled entity.  Policies require two methods:\n\n* **`permit?/3`** takes the entity in question, the current user (or `nil`), and\n  the action to be taken as an atom and returns a boolean.\n* **`scope/2`** takes the entity in question and the current user (or `nil`).\n  It returns an Ecto query scoped to all the entities that user can view.\n\nFor example, assume we have a `Post` entity.  Users can view all posts and edit\ntheir own posts, and admins can view and edit any post.  Furthermore, admins\ncan view all posts, but users can only view published posts or posts they own.\n\n```elixir\ndefimpl Policy, for: Post do\n  import Ecto.Query\n\n  # First, anyone can read a post\n  def permit?(_post, _user, :read), do: true\n  # Second, anonymous users can't do anything else\n  def permit?(_post, nil, _action), do: false\n  # Third, admins can do anything\n  def permit?(_post, %User{admin: true}, _action), do: true\n  # Finally, users can do anything to their own posts\n  def permit?(%Post{user_id: user_id}, %User{id: user_id}, _action), do: true\n\n  # Admins can view the whole `posts` table\n  def scope(_post, %User{admin: true}), do: Post\n  # Anonymous users can only view public posts\n  def scope(_post, nil), do: from(p in Post, where: p.published)\n  # Users can view published posts and posts they own\n  def scope(_post, user), do: from p in Post, where: p.published or p.user_id == ^user.id\nend\n```\n\n`Policy` has a couple of implementations out of the box which allow it to be\ninvoked with either a bare module name or an Ecto changeset\n\n```elixir\n# These are all equivalent\nPolicy.permit? %Post{}, current_user, :read\nPolicy.permit? Post, current_user, :read\nPolicy.permit? Ecto.Changeset.change(%Post{}, %{}), current_user, :read\n```\n\nBare module name permissions are determined based on the entity's default\nvalues.  Changeset permissions are determined based on an entity with all the\nproposed changes applied.\n\nPolicies are enforced at a controller level.  Controllers can import\n`Policy.Helpers` to get the `:ensure_authorization` plug and the `authorize!/2`\nfunction.\n\n`authorize!/2` takes the `conn` and a model or list of models, throws a\n`Policy.Exception` if the model (or any one of the list) is not authorized, and\nreturns a `conn` that is marked as authorized.\n\nThe `:ensure_authorization` plug will throw a `Policy.Exception` if a Controller\ntries to send without running `authorize!/2`.\n\n`Policy.Exception` is registered with `Plug` to return a 403 Unauthorized.\n\nAn example controller looks like this:\n\n```elixir\ndefmodule MyApp.PostController do\n  use MyApp.Web, :controller\n  import Policy.Helpers\n\n  alias MyApp.Post\n\n  plug :ensure_authorization\n\n  def index(conn, _params) do\n    posts = Post\n    |\u003e Policy.scope(conn.assigns[:current_user])\n    |\u003e Repo.all\n\n    conn = conn\n    |\u003e authorize!(posts)\n    |\u003e render(\"index.html\", posts: posts)\n  end\n\n  def new(conn, _params) do\n    changeset = Post.changeset(%Post{})\n\n    conn\n    |\u003e authorize!(changeset)\n    |\u003e render(\"new.html\", changeset: changeset)\n  end\n\n  def create(conn, %{\"post\" =\u003e post_params}) do\n    changeset = Post.changeset(%Post{}, post_params)\n\n    conn = authorize!(conn, changeset)\n\n    case Repo.insert(changeset) do\n      {:ok, _post} -\u003e\n        conn\n        |\u003e put_flash(:info, \"post created successfully.\")\n        |\u003e redirect(to: post_path(conn, :index))\n      {:error, changeset} -\u003e\n        render(conn, \"new.html\", changeset: changeset)\n    end\n  end\n\n  def show(conn, %{\"id\" =\u003e id}) do\n    post = Repo.get!(Post, id)\n\n    conn\n    |\u003e authorize!(post)\n    |\u003e render(\"show.html\", post: post)\n  end\n\n  def edit(conn, %{\"id\" =\u003e id}) do\n    post = Repo.get!(Post, id)\n    changeset = Post.changeset(post)\n\n    conn\n    |\u003e authorize!(changeset)\n    |\u003e render(\"edit.html\", post: post, changeset: changeset)\n  end\n\n  def update(conn, %{\"id\" =\u003e id, \"post\" =\u003e post_params}) do\n    post = Repo.get!(Post, id)\n    changeset = Post.changeset(post, post_params)\n\n    conn = authorize!(conn, changeset)\n\n    case Repo.update(changeset) do\n      {:ok, post} -\u003e\n        conn\n        |\u003e put_flash(:info, \"post updated successfully.\")\n        |\u003e redirect(to: post_path(conn, :show, post))\n      {:error, changeset} -\u003e\n        render(conn, \"edit.html\", post: post, changeset: changeset)\n    end\n  end\n\n  def delete(conn, %{\"id\" =\u003e id}) do\n    post = Repo.get!(Post, id)\n\n    conn = authorize!(conn, post)\n\n    # Here we use delete! (with a bang) because we expect\n    # it to always work (and if it does not, it will raise).\n    Repo.delete!(post)\n\n    conn\n    |\u003e put_flash(:info, \"post deleted successfully.\")\n    |\u003e redirect(to: post_path(conn, :index))\n  end\nend\n```\n\n`authorize!/2` assumes that the current user is in `conn.assigns[:current_user]`\nand that the controller action is set by Phoenix.  It maps the seven RESTful\nactions to `:create`, `:read`, `:update`, and `:delete`, and passes all other\nactions through as-is.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsystem76%2Fpolicy","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsystem76%2Fpolicy","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsystem76%2Fpolicy/lists"}