Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,33 @@ This list of planned items relates to the main Permit repository as well as to [

## Installation

If [available in Hex](https://hex.pm/docs/publish), the package can be installed by adding `permit` to your list of dependencies in `mix.exs`:
### Using Igniter (recommended)

The easiest way to set up Permit is with [Igniter](https://hex.pm/packages/igniter), which will add the dependencies, generate authorization modules, and patch your web module automatically:

```bash
# Full setup with Ecto and Phoenix (LiveView + controllers)
mix igniter.install permit --phoenix

# With Absinthe/GraphQL integration
mix igniter.install permit --phoenix --absinthe

# Base Permit only, no Ecto
mix igniter.install permit --no-ecto
```

After installation, use the patch tasks to wire Permit into existing controllers and LiveViews:

```bash
mix permit.patch.controller MyAppWeb.ArticleController MyApp.Blog.Article
mix permit.patch.live_view MyAppWeb.ArticleLive.Index MyApp.Blog.Article
```

See the [Igniter documentation](https://hexdocs.pm/igniter) for more details.

### Manual installation

Alternatively, add `permit` to your list of dependencies in `mix.exs`:

```elixir
def deps do
Expand All @@ -286,7 +312,7 @@ def deps do
{:permit, "~> 0.3.3"},
{:permit_ecto, "~> 0.2.4"}, # For Ecto integration
{:permit_phoenix, "~> 0.3.0"}, # For Phoenix & LiveView
{:permit_absinthe, "~> 0.1.0"} # For GraphQL (Absinthe)
{:permit_absinthe, "~> 0.1.0"} # For GraphQL (Absinthe)
]
end
```
Expand Down
160 changes: 160 additions & 0 deletions lib/mix/tasks/permit.install.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
if Version.match?(System.version(), ">= 1.15.0") and Code.ensure_loaded?(Igniter) do
defmodule Mix.Tasks.Permit.Install do
@shortdoc "Installs Permit authorization into your project"

@moduledoc """
Installs Permit authorization into your project.

## Usage

mix permit.install

## Options

- `--phoenix` - Include Phoenix integration (Permit.Phoenix)
- `--absinthe` - Include Absinthe/GraphQL integration (Permit.Absinthe)
- `--no-ecto` - Do not include Ecto integration (use only base Permit)
- `--authorization-module` - Authorization module name (default: `<MyApp>.Authorization`)
- `--permissions-module` - Permissions module name (default: `<MyApp>.Authorization.Permissions`)
- `--actions-module` - Actions module name (default: `<MyApp>.Authorization.Actions`)
- `--repo` - Ecto repo module name (auto-detected if not specified)
- `--router` - Phoenix router module (auto-detected if not specified)
- `--schema-module` - Absinthe schema module (auto-detected if not specified)
"""

use Igniter.Mix.Task

@impl Igniter.Mix.Task
def info(_argv, _composing_task) do
%Igniter.Mix.Task.Info{
group: :permit,
schema: [
phoenix: :boolean,
absinthe: :boolean,
no_ecto: :boolean,
authorization_module: :string,
permissions_module: :string,
actions_module: :string,
repo: :string,
router: :string,
schema_module: :string
],
defaults: [
phoenix: false,
absinthe: false,
no_ecto: false
],
composes: [
"permit_ecto.install",
"permit_phoenix.install",
"permit_absinthe.install"
]
}
end

@impl Igniter.Mix.Task
def igniter(igniter) do
options = igniter.args.options
app_module = Igniter.Project.Module.module_name_prefix(igniter)

authorization_module =
parse_module(options[:authorization_module], Module.concat(app_module, Authorization))

permissions_module =
parse_module(
options[:permissions_module],
Module.concat(authorization_module, Permissions)
)

actions_module =
parse_module(options[:actions_module], Module.concat(authorization_module, Actions))

no_ecto? = Keyword.get(options, :no_ecto, false)
phoenix? = Keyword.get(options, :phoenix, false)
absinthe? = Keyword.get(options, :absinthe, false)

igniter =
if no_ecto? do
igniter
|> create_base_authorization_module(authorization_module, permissions_module)
|> create_base_permissions_module(permissions_module)
else
compose_args =
[
"--authorization-module",
inspect(authorization_module),
"--permissions-module",
inspect(permissions_module)
]
|> maybe_add_option(options, :repo)
|> maybe_add_option_value("--actions-module", phoenix? && inspect(actions_module))

Igniter.compose_task(igniter, "permit_ecto.install", compose_args)
end

igniter =
if phoenix? do
compose_args =
[
"--authorization-module",
inspect(authorization_module),
"--actions-module",
inspect(actions_module)
]
|> maybe_add_option(options, :router)

Igniter.compose_task(igniter, "permit_phoenix.install", compose_args)
else
igniter
end

igniter =
if absinthe? do
compose_args =
["--authorization-module", inspect(authorization_module)]
|> maybe_add_option(options, :schema_module)

Igniter.compose_task(igniter, "permit_absinthe.install", compose_args)
else
igniter
end

igniter
end

defp create_base_authorization_module(igniter, authorization_module, permissions_module) do
Igniter.Project.Module.create_module(igniter, authorization_module, """
use Permit, permissions_module: #{inspect(permissions_module)}
""")
end

defp create_base_permissions_module(igniter, permissions_module) do
Igniter.Project.Module.create_module(igniter, permissions_module, """
use Permit.Permissions, actions_module: Permit.Actions.CrudActions

def can(_user) do
permit()
end
""")
end

defp parse_module(nil, default), do: default

defp parse_module(string, _default) when is_binary(string) do
string
|> String.split(".")
|> Module.concat()
end

defp maybe_add_option(args, options, key) do
case Keyword.get(options, key) do
nil -> args
value -> args ++ ["--#{key}", value]
end
end

defp maybe_add_option_value(args, _flag, false), do: args
defp maybe_add_option_value(args, _flag, nil), do: args
defp maybe_add_option_value(args, flag, value), do: args ++ [flag, value]
end
end
12 changes: 10 additions & 2 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ defmodule Permit.MixProject do
consolidate_protocols: Mix.env() not in [:dev, :test],
description: "Plain-Elixir, DSL-less, extensible authorization library for Elixir.",
package: package(),
dialyzer: [plt_add_apps: [:ex_unit]],
dialyzer: [plt_add_apps: [:ex_unit, :mix, :igniter]],
docs: docs(),
test_coverage: [tool: ExCoveralls],
preferred_cli_env: [
Expand Down Expand Up @@ -63,7 +63,15 @@ defmodule Permit.MixProject do
{:git_cli, "~> 0.3.0", only: [:dev, :test], runtime: false},
{:excoveralls, "~> 0.18", only: :test, runtime: false},
{:castore, "~> 1.0", only: :test, runtime: false}
]
] ++ igniter_dep()
end

defp igniter_dep do
if Version.match?(System.version(), ">= 1.15.0") do
[{:igniter, "~> 0.5", only: [:dev, :test], runtime: false}]
else
[]
end
end

defp docs do
Expand Down
17 changes: 16 additions & 1 deletion mix.lock
Original file line number Diff line number Diff line change
@@ -1,18 +1,33 @@
%{
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
"castore": {:hex, :castore, "1.0.14", "4582dd7d630b48cf5e1ca8d3d42494db51e406b7ba704e81fbd401866366896a", [:mix], [], "hexpm", "7bc1b65249d31701393edaaac18ec8398d8974d52c647b7904d01b964137b9f4"},
"castore": {:hex, :castore, "1.0.18", "5e43ef0ec7d31195dfa5a65a86e6131db999d074179d2ba5a8de11fe14570f55", [:mix], [], "hexpm", "f393e4fe6317829b158fb74d86eb681f737d2fe326aa61ccf6293c4104957e34"},
"credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"},
"dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"},
"earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"},
"erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"},
"ex_doc": {:hex, :ex_doc, "0.38.2", "504d25eef296b4dec3b8e33e810bc8b5344d565998cd83914ffe1b8503737c02", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "732f2d972e42c116a70802f9898c51b54916e542cc50968ac6980512ec90f42b"},
"excoveralls": {:hex, :excoveralls, "0.18.5", "e229d0a65982613332ec30f07940038fe451a2e5b29bce2a5022165f0c9b157e", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "523fe8a15603f86d64852aab2abe8ddbd78e68579c8525ae765facc5eae01562"},
"file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"},
"finch": {:hex, :finch, "0.21.0", "b1c3b2d48af02d0c66d2a9ebfb5622be5c5ecd62937cf79a88a7f98d48a8290c", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "87dc6e169794cb2570f75841a19da99cfde834249568f2a5b121b809588a4377"},
"git_cli": {:hex, :git_cli, "0.3.0", "a5422f9b95c99483385b976f5d43f7e8233283a47cda13533d7c16131cb14df5", [:mix], [], "hexpm", "78cb952f4c86a41f4d3511f1d3ecb28edb268e3a7df278de2faa1bd4672eaf9b"},
"glob_ex": {:hex, :glob_ex, "0.1.11", "cb50d3f1ef53f6ca04d6252c7fde09fd7a1cf63387714fe96f340a1349e62c93", [:mix], [], "hexpm", "342729363056e3145e61766b416769984c329e4378f1d558b63e341020525de4"},
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
"igniter": {:hex, :igniter, "0.7.7", "08bae07b7b610100bc7c676e6b18130fe12bb90617982023cc798346879c2c5f", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "caeb1227887362b22038ff8419a7e6ddd3888f3d7e6cffacb14c73abbce17600"},
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
"makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"},
"makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"},
"makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"},
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
"mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"},
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
"nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
"owl": {:hex, :owl, "0.13.0", "26010e066d5992774268f3163506972ddac0a7e77bfe57fa42a250f24d6b876e", [:mix], [{:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: true]}], "hexpm", "59bf9d11ce37a4db98f57cb68fbfd61593bf419ec4ed302852b6683d3d2f7475"},
"req": {:hex, :req, "0.5.17", "0096ddd5b0ed6f576a03dde4b158a0c727215b15d2795e59e0916c6971066ede", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0b8bc6ffdfebbc07968e59d3ff96d52f2202d0536f10fef4dc11dc02a2a43e39"},
"rewrite": {:hex, :rewrite, "1.3.0", "67448ba7975690b35ba7e7f35717efcce317dbd5963cb0577aa7325c1923121a", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "d111ac7ff3a58a802ef4f193bbd1831e00a9c57b33276e5068e8390a212714a5"},
"sourceror": {:hex, :sourceror, "1.12.0", "da354c5f35aad3cc1132f5d5b0d8437d865e2661c263260480bab51b5eedb437", [:mix], [], "hexpm", "755703683bd014ebcd5de9acc24b68fb874a660a568d1d63f8f98cd8a6ef9cd0"},
"spitfire": {:hex, :spitfire, "0.3.10", "19aea9914132456515e8f7d592f63ab9f3130876b0252e834d2390bdd8becb24", [:mix], [], "hexpm", "6a6a5f77eb4165249c76199cd2d01fb595bac9207aed3de551918ac1c2bc9267"},
"telemetry": {:hex, :telemetry, "1.4.1", "ab6de178e2b29b58e8256b92b382ea3f590a47152ca3651ea857a6cae05ac423", [:rebar3], [], "hexpm", "2172e05a27531d3d31dd9782841065c50dd5c3c7699d95266b2edd54c2dafa1c"},
"text_diff": {:hex, :text_diff, "0.1.0", "1caf3175e11a53a9a139bc9339bd607c47b9e376b073d4571c031913317fecaa", [:mix], [], "hexpm", "d1ffaaecab338e49357b6daa82e435f877e0649041ace7755583a0ea3362dbd7"},
"versioce": {:hex, :versioce, "2.0.0", "a31b5e7b744d0d4a3694dd6fe4c0ee403e969631789e73cbd2a3367246404948", [:mix], [{:git_cli, "~> 0.3.0", [hex: :git_cli, repo: "hexpm", optional: true]}], "hexpm", "b2112ce621cd40fe23ad957a3dd82bccfdfa33c9a7f1e710a44b75ae772186cc"},
}
78 changes: 78 additions & 0 deletions test/mix/tasks/permit_install_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
if Version.match?(System.version(), ">= 1.15.0") and Code.ensure_loaded?(Igniter.Test) do
defmodule Mix.Tasks.Permit.InstallTest do
use ExUnit.Case

import Igniter.Test

describe "permit.install --no-ecto" do
test "creates authorization and permissions modules" do
test_project()
|> Igniter.compose_task("permit.install", ["--no-ecto"])
|> assert_creates("lib/test/authorization.ex")
|> assert_creates("lib/test/authorization/permissions.ex")
end

test "generated authorization module uses Permit with correct permissions module" do
igniter =
test_project()
|> Igniter.compose_task("permit.install", ["--no-ecto"])
|> apply_igniter!()

source = Rewrite.source!(igniter.rewrite, "lib/test/authorization.ex")
content = Rewrite.Source.get(source, :content)

assert content =~ "use Permit, permissions_module: Test.Authorization.Permissions"
end

test "generated permissions module uses Permit.Permissions with CrudActions" do
igniter =
test_project()
|> Igniter.compose_task("permit.install", ["--no-ecto"])
|> apply_igniter!()

source = Rewrite.source!(igniter.rewrite, "lib/test/authorization/permissions.ex")
content = Rewrite.Source.get(source, :content)

assert content =~ "use Permit.Permissions, actions_module: Permit.Actions.CrudActions"
assert content =~ "def can(_user) do"
assert content =~ "permit()"
end

test "uses custom authorization module name" do
test_project()
|> Igniter.compose_task("permit.install", [
"--no-ecto",
"--authorization-module",
"Test.Auth"
])
|> assert_creates("lib/test/auth.ex")
end

test "uses custom permissions module name" do
test_project()
|> Igniter.compose_task("permit.install", [
"--no-ecto",
"--authorization-module",
"Test.Auth",
"--permissions-module",
"Test.Auth.Perms"
])
|> assert_creates("lib/test/auth.ex")
|> assert_creates("lib/test/auth/perms.ex")
end
end

describe "permit.install defaults (with ecto)" do
test "runs without error when ecto task is unavailable" do
# When permit_ecto.install task is not available (different package),
# compose_task should handle it gracefully
igniter =
test_project()
|> Igniter.compose_task("permit.install", [])

# Should not raise - the task handles missing sub-tasks gracefully
assert %Igniter{} = igniter
end
end
end
end
Loading