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
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,36 @@ Permit.Ecto is part of the modular Permit ecosystem:

## Installation

### Using Igniter (recommended)

If you use [Igniter](https://hex.pm/packages/igniter) for project setup, add `permit_ecto` and `igniter` to your deps and run:

```bash
mix permit_ecto.install
```

This creates your `Authorization` and `Authorization.Permissions` modules pre configured for Ecto.

**Options:**

| Option | Description | Default |
|---|---|---|
| `--authorization-module` | Authorization module name | `<MyApp>.Authorization` |
| `--permissions-module` | Permissions module name | `<MyApp>.Authorization.Permissions` |
| `--actions-module` | Actions module to use | `Permit.Actions.CrudActions` |
| `--repo` | Ecto repo module | auto-detected |

Example with options:

```bash
mix permit_ecto.install \
--authorization-module MyApp.Auth \
--permissions-module MyApp.Auth.Permissions \
--repo MyApp.Repo
```

### Manual installation

The package can be installed by adding `permit_ecto` to your list of dependencies in `mix.exs`:

```elixir
Expand All @@ -125,6 +155,22 @@ def deps do
end
```

Then create your authorization module:

```elixir
defmodule MyApp.Authorization do
use Permit.Ecto,
permissions_module: MyApp.Authorization.Permissions,
repo: MyApp.Repo
end

defmodule MyApp.Authorization.Permissions do
use Permit.Ecto.Permissions, actions_module: Permit.Actions.CrudActions

def can(_user), do: permit()
end
```

For a complete setup with Phoenix or Absinthe integration, add `:permit_phoenix` or `:permit_absinthe`.

## Documentation
Expand Down
137 changes: 137 additions & 0 deletions lib/mix/tasks/permit_ecto.install.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
if Version.match?(System.version(), ">= 1.15.0") and Code.ensure_loaded?(Igniter) do
defmodule Mix.Tasks.PermitEcto.Install do
@shortdoc "Installs Permit.Ecto authorization into your project"

@moduledoc """
Installs Permit.Ecto authorization into your project, creating an authorization
module and a permissions module configured for Ecto.

## Usage

mix permit_ecto.install

## Options

- `--authorization-module` - Authorization module name (default: `<MyApp>.Authorization`)
- `--permissions-module` - Permissions module name (default: `<MyApp>.Authorization.Permissions`)
- `--actions-module` - Actions module to use in permissions (default: `Permit.Actions.CrudActions`)
- `--repo` - Ecto repo module name (auto-detected if not specified)
"""

use Igniter.Mix.Task

alias Igniter.Libs.Ecto, as: IgniterEcto
alias Igniter.Project.Module, as: ProjectModule

@impl Igniter.Mix.Task
def info(_argv, _composing_task) do
%Igniter.Mix.Task.Info{
group: :permit,
schema: [
authorization_module: :string,
permissions_module: :string,
actions_module: :string,
repo: :string
]
}
end

@impl Igniter.Mix.Task
def igniter(igniter) do
options = igniter.args.options
app_module = ProjectModule.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], nil)

{igniter, repo} = detect_repo(igniter, options[:repo])

igniter
|> create_authorization_module(authorization_module, permissions_module, repo)
|> create_permissions_module(permissions_module, actions_module)
|> Igniter.add_notice("""
Permit.Ecto has been set up!

Edit #{inspect(permissions_module)} to define your permission rules.

Example:

def can(%MyApp.User{id: user_id}) do
permit()
|> all(MyApp.Resource, owner_id: user_id)
|> read(MyApp.Resource)
end
""")
end

defp create_authorization_module(igniter, authorization_module, permissions_module, repo) do
ProjectModule.create_module(igniter, authorization_module, """
use Permit.Ecto,
permissions_module: #{inspect(permissions_module)},
repo: #{inspect(repo)}
""")
end

defp create_permissions_module(igniter, permissions_module, actions_module) do
resolved_actions = actions_module || Permit.Actions.CrudActions

ProjectModule.create_module(igniter, permissions_module, """
use Permit.Ecto.Permissions, actions_module: #{inspect(resolved_actions)}

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

defp detect_repo(igniter, repo_string) when is_binary(repo_string) do
{igniter, parse_module(repo_string, nil)}
end

defp detect_repo(igniter, nil) do
if Code.ensure_loaded?(IgniterEcto) do
{igniter, repos} = IgniterEcto.list_repos(igniter)

case repos do
[repo] ->
{igniter, repo}

[repo | _rest] ->
{Igniter.add_notice(
igniter,
"Multiple Ecto repos detected. Using #{inspect(repo)}. Pass --repo to specify a different one."
), repo}

[] ->
app_module = ProjectModule.module_name_prefix(igniter)
fallback = Module.concat(app_module, Repo)

{Igniter.add_notice(
igniter,
"Could not auto-detect Ecto repo. Using #{inspect(fallback)}. Pass --repo if this is incorrect."
), fallback}
end
else
app_module = ProjectModule.module_name_prefix(igniter)
fallback = Module.concat(app_module, Repo)
{igniter, fallback}
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
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.Ecto.MixProject do
deps: deps(),
description: "Ecto integration for the Permit authorization library.",
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.Ecto.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
14 changes: 14 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,28 @@
"ex_doc": {:hex, :ex_doc, "0.40.1", "67542e4b6dde74811cfd580e2c0149b78010fd13001fda7cfeb2b2c2ffb1344d", [: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", "bcef0e2d360d93ac19f01a85d58f91752d930c0a30e2681145feea6bd3516e00"},
"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.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"},
"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.3", "4252d5d4098da7415c390e847c814bad3764c94a814a0b4245176215615e1035", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "953297c02582a33411ac6208f2c6e55f0e870df7f80da724ed613f10e6706afd"},
"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"},
"permit": {:hex, :permit, "0.3.3", "1ed016b39983b897c92944a4ef9ca135b22adf0aabadbb29b2f30f30f770e965", [:mix], [], "hexpm", "5fdf26b5b3f455b187693ed566924eae3c939221e3dc828cf5f75d3ea2341b63"},
"postgrex": {:hex, :postgrex, "0.22.0", "fb027b58b6eab1f6de5396a2abcdaaeb168f9ed4eccbb594e6ac393b02078cbd", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "a68c4261e299597909e03e6f8ff5a13876f5caadaddd0d23af0d0a61afcc5d84"},
"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.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
"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"},
}
92 changes: 92 additions & 0 deletions test/mix/tasks/permit_ecto_install_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
if Version.match?(System.version(), ">= 1.15.0") and Code.ensure_loaded?(Igniter.Test) do
defmodule Mix.Tasks.PermitEcto.InstallTest do
use ExUnit.Case

import Igniter.Test

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

test "authorization module uses Permit.Ecto with correct options" do
igniter =
test_project()
|> Igniter.compose_task("permit_ecto.install", [])
|> apply_igniter!()

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

assert content =~ "use Permit.Ecto"
assert content =~ "permissions_module: Test.Authorization.Permissions"
assert content =~ "repo: Test.Repo"
end

test "permissions module uses Permit.Ecto.Permissions with CrudActions by default" do
igniter =
test_project()
|> Igniter.compose_task("permit_ecto.install", [])
|> apply_igniter!()

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

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

test "uses custom repo when specified" do
igniter =
test_project()
|> Igniter.compose_task("permit_ecto.install", ["--repo", "Test.CustomRepo"])
|> apply_igniter!()

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

assert content =~ "repo: Test.CustomRepo"
end

test "uses provided actions module in permissions" do
igniter =
test_project()
|> Igniter.compose_task("permit_ecto.install", [
"--actions-module",
"Test.Authorization.Actions"
])
|> apply_igniter!()

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

assert content =~ "actions_module: Test.Authorization.Actions"
end

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

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