{"id":25655427,"url":"https://github.com/tudborg/airframe","last_synced_at":"2025-04-19T02:52:30.528Z","repository":{"id":278139372,"uuid":"810855775","full_name":"tudborg/airframe","owner":"tudborg","description":"Elixir library for managing permission checking in phoenix context modules.","archived":false,"fork":false,"pushed_at":"2025-03-03T13:43:55.000Z","size":65,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-18T01:15:25.705Z","etag":null,"topics":["authorization","ecto","elixir"],"latest_commit_sha":null,"homepage":"","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tudborg.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-06-05T13:30:37.000Z","updated_at":"2025-03-03T13:43:58.000Z","dependencies_parsed_at":"2025-02-25T08:23:20.901Z","dependency_job_id":null,"html_url":"https://github.com/tudborg/airframe","commit_stats":null,"previous_names":["tudborg/airframe"],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tudborg%2Fairframe","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tudborg%2Fairframe/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tudborg%2Fairframe/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tudborg%2Fairframe/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tudborg","download_url":"https://codeload.github.com/tudborg/airframe/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249596889,"owners_count":21297458,"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":["authorization","ecto","elixir"],"created_at":"2025-02-23T21:28:53.019Z","updated_at":"2025-04-19T02:52:30.522Z","avatar_url":"https://github.com/tudborg.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Airframe\n\n[![CI](https://github.com/tudborg/airframe/actions/workflows/elixir.yml/badge.svg)](https://github.com/tudborg/airframe/actions/workflows/elixir.yml)\n[![Hex.pm](https://img.shields.io/hexpm/v/airframe.svg)](https://hex.pm/packages/airframe)\n[![Documentation](https://img.shields.io/badge/documentation-gray)](https://hexdocs.pm/airframe/)\n\nAirframe is an authorization library.\n\nDocumentation can be found at \u003chttps://hexdocs.pm/airframe\u003e.\n\nAt it's core is the `Airframe.Policy` that you implement for all relevant \"subjects\".  \nA policy is a module implemetning `c:Airframe.Policy.allow/3`, where you implement checks against a subject and action for some \"actor\" like a `current_user` or session token.\n\nIt differentiates itself from other similar libraries by allowing the policy to change the subject, like:\n- Changing an `Ecto.Query` to narrow done the scope to the current user.\n- Removing changes from an `Ecto.Changeset` based on the current user's access level.\n- Completely replacing the subject with an alternative subject.\n- \nThis makes it possible to combine access scoping and action checking in a single policy.\n\nIt can be used anywhere in your application, but is intended to be used in your context modules to\nensure that authorization is handled at the same abstraction level everywhere, regardless of interface.\n\n## Installation\n\nThe package can be installed by adding `airframe` to your list of dependencies in `mix.exs`:\n\n```elixir\ndef deps do\n  [\n    {:airframe, \"~\u003e 0.1.0\"}\n  ]\nend\n```\n\n## Examples\n\nYou can define a Policy directly in a context:\n\n```elixir\ndefmodule MyApp.MyContext do\n  use Airframe.Policy\n\n  @spec allow(subject, action, actor)\n  def allow(_subject_, _action, _actor)do\n    true # allow everything by anyone!\n  end\nend\n```\n\nOr more typically in a larger context module, you defer the policy to it's own module:\n\n```elixir\ndefmodule MyApp.MyContext do\n  use Airframe.Policy, defer: MyApp.MyContext.MyPolicy\n\n  # ...\nend\n\ndefmodule MyApp.MyContext.MyPolicy do\n  use Airframe.Policy\n\n  @spec allow(subject, action, actor)\n  def allow(_subject_, _action, _actor)do\n    true # allow everything by anyone!\n  end\nend\n```\n\n### Authorization\n\nYou check against a policy with `Airframe.check/4`\n\n```elixir\nwith {:ok, subject} \u003c- Airframe.check(subject, action, actor, policy) do\n  # actor is allowed to perform action on subject according to policy.\nend\n```\n\nand it's `raise`ing version `Airframe.check!/4`:\n\n```elixir\nAirframe.check(subject, action, actor, policy)\n```\n\nwhich is very convenient if your subject is a schema module or ecto query:\n\n```elixir\ndef list(opts) do\n  Post\n  |\u003e Airframe.check!(:read, opts[:current_user], MyPolicy)\n  |\u003e Repo.all()\nend\n```\n\nThere is an `Airframe.check/2` and `Airframe.check!/2` macro\nthat will infer the policy and (optionally) the action from the calling context.\n\n```elixir\ndefmodule MyApp.MyContext do\n  use Airframe.Policy\n\n  def allow(_subject_, _action, _actor) do\n    true # allow everything by anyone!\n  end\n\n  def create(attr, opts) do\n    # infer the action to be the name of the calling function (`create`)\n    # and the policy to be the calling module (`MyApp.MyContext`)\n    # By using `Airframe.Policy` you automatically require Airframe\n    # such that the `Airframe.check/2` macro is available\n    with {:ok, subject} \u003c- Airframe.check(subject, actor) do\n      # actor is allowed to perform action on subject according to policy.\n    end\n  end\nend\n```\n\n### Full Example\n\n```elixir\ndefmodule MyApp.MyContext do\n  # Allow this context to be used as a policy,\n  # but defer to it's own policy module for the callback function.\n  use Airframe.Policy, defer: __MODULE__.Policy\n\n  def list(opts) do\n    # we want to list Post\n    Post\n    # we check that current_user is allowed to do so\n    # (the action is derived from the calling function - `list` in this case,\n    # you can ofc. specify the action manually as the second argument to check!/3)\n    |\u003e Airframe.check!(opts[:current_user])\n    |\u003e Repo.all()\n  end\n\n  def get!(id, opts) do\n    Post\n    |\u003e Airframe.check!(opts[:current_user])\n    |\u003e Repo.get!(id)\n  end\n  \n  def create(attrs, opts) do\n    with {:ok, attrs} \u003c- Airframe.check(attrs, opts[:current_user]) do\n      %Post{}\n      |\u003e Post.changeset(attrs)\n      |\u003e Repo.insert()\n    end\n  end\n\n  def update(post, attrs, opts) do\n    with {:ok, {post, attrs}} \u003c- Airframe.check({post, attrs}, opts[:current_user]) do\n      post\n      |\u003e Post.changeset(attrs)\n      |\u003e Repo.update()\n    end\n  end\n\n  # Airframe has special support for Ecto.Changeset.\n  # If the subject is a changeset, it will only ever be checked against the policy\n  # if it is valid. Otherwise `{:error, changeset}` is returned.\n\n  def update(post, attrs, opts) do\n    changeset = Post.changeset(post, attrs)\n\n    with {:ok, changeset} \u003c- Airframe.check(changeset, opts[:current_user]) do\n      Repo.update(changeset)\n    end\n  end\n\n  # ...\nend\n\ndefmodule MyApp.MyContext.Policy do\n  # this is the actual policy module.\n  use Airframe.Policy\n\n  # allow the `list` and `get` action on `Post`,\n  # but we narrow the scope to only posts from the actor (User, in this case)\n  @spec allow(subject, action, actor)\n  def allow(Post, action, %User{id: user_id}) when action in [:list, :get] do\n    {:ok, from(p in Post, where: p.user_id == ^user_id)}\n  end\n\n  # allow a user to create a post if the post attrs user_id matches their user's id\n  def allow(attrs, :create, %User{id: user_id}) do\n    attrs[\"user_id\"] == user_id\n  end\n\n  # allow a user to update a post if the post is their own.\n  def allow({%Post{user_id: user_id}, attrs}, :update, %User{id: user_id}), do: true\n  def allow({%Post{user_id: _}, attrs}, :update, %User{}), do: false\n\n  # or if you'd rather check on the changeset itself:\n  def allow(%Ecto.Changeset{data: %Post{user_id: user_id}}, :update, %User{id: user_id}), do: true\n  def allow(%Ecto.Changeset{data: %Post{user_id: _}}, :update, %User{id: _}), do: false\n\n  # having the changeset as the subject also makes it easy to modify it:\n  def allow(%Ecto.Changeset{data: %Post{user_id: user_id}}=changeset, :update, %User{id: user_id}) do\n    {:ok, Ecto.Changeset.delete_change(changeset, :approved_by_id)}\n  end\n\n\n  # ...\nend\n```\n\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftudborg%2Fairframe","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftudborg%2Fairframe","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftudborg%2Fairframe/lists"}