From f59364be906998d144bd982cbc9e02a49d4c4c38 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Oct 2025 07:09:09 +0000 Subject: [PATCH 1/8] Initial plan From 2c7824640bbc81e316cf11ed5e96d686c58d3621 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Oct 2025 07:19:18 +0000 Subject: [PATCH 2/8] Add load_policies_from_adapter to EnforcerServer and Enforcer Co-authored-by: hsluoyz <3787410+hsluoyz@users.noreply.github.com> --- lib/acx/enforcer_server.ex | 39 +++++++ test/persist/ecto_adapter_load_test.exs | 129 ++++++++++++++++++++++++ 2 files changed, 168 insertions(+) create mode 100644 test/persist/ecto_adapter_load_test.exs diff --git a/lib/acx/enforcer_server.ex b/lib/acx/enforcer_server.ex index adfada1..c86c63c 100644 --- a/lib/acx/enforcer_server.ex +++ b/lib/acx/enforcer_server.ex @@ -68,6 +68,33 @@ defmodule Acx.EnforcerServer do GenServer.call(via_tuple(ename), {:load_policies, pfile}) end + @doc """ + Loads policy rules from the configured persist adapter and adds them + to the enforcer. + + This function loads both regular policies and mapping policies (role + inheritances) from the database into the enforcer's memory. + + Returns `:ok` on success, or `{:error, reason}` if the adapter is not + configured or if loading fails. + + ## Examples + + # Configure adapter + adapter = EctoAdapter.new(Repo) + EnforcerServer.set_persist_adapter("my_enforcer", adapter) + + # Load policies from the adapter + EnforcerServer.load_policies_from_adapter("my_enforcer") + # => :ok + + See `Enforcer.load_policies!/1` and `Enforcer.load_mapping_policies!/1` + for more details. + """ + def load_policies_from_adapter(ename) do + GenServer.call(via_tuple(ename), {:load_policies_from_adapter}) + end + @doc """ Returns a list of policies in the given enforcer that match the given criteria. @@ -245,6 +272,18 @@ defmodule Acx.EnforcerServer do {:reply, :ok, new_enforcer} end + def handle_call({:load_policies_from_adapter}, _from, enforcer) do + case Enforcer.load_policies!(enforcer) do + {:error, reason} -> + {:reply, {:error, reason}, enforcer} + + new_enforcer -> + new_enforcer = Enforcer.load_mapping_policies!(new_enforcer) + :ets.insert(:enforcers_table, {self_name(), new_enforcer}) + {:reply, :ok, new_enforcer} + end + end + def handle_call({:list_policies, criteria}, _from, enforcer) do policies = enforcer |> Enforcer.list_policies(criteria) {:reply, policies, enforcer} diff --git a/test/persist/ecto_adapter_load_test.exs b/test/persist/ecto_adapter_load_test.exs new file mode 100644 index 0000000..c11dd72 --- /dev/null +++ b/test/persist/ecto_adapter_load_test.exs @@ -0,0 +1,129 @@ +defmodule Acx.Persist.EctoAdapterLoadTest do + use ExUnit.Case, async: false + alias Acx.EnforcerServer + alias Acx.Enforcer + alias Acx.Persist.EctoAdapter + + defmodule MockAclRepo do + use Acx.Persist.MockRepo, pfile: "../data/acl.csv" |> Path.expand(__DIR__) + end + + defmodule MockRbacRepo do + use Acx.Persist.MockRepo, pfile: "../data/rbac.csv" |> Path.expand(__DIR__) + end + + @acl_cfile "../data/acl.conf" |> Path.expand(__DIR__) + @rbac_cfile "../data/rbac.conf" |> Path.expand(__DIR__) + + describe "load_policies_from_adapter/1 for ACL model" do + setup do + # Start a fresh enforcer for each test + enforcer_name = :"test_enforcer_#{:erlang.unique_integer([:positive])}" + {:ok, _pid} = EnforcerServer.start_link(enforcer_name, @acl_cfile) + + on_exit(fn -> + Process.exit( + Process.whereis({:via, Registry, {Acx.EnforcerRegistry, enforcer_name}}), + :kill + ) + end) + + {:ok, enforcer_name: enforcer_name} + end + + test "loads policies from adapter into memory", %{enforcer_name: enforcer_name} do + # Set the persist adapter + adapter = EctoAdapter.new(MockAclRepo) + :ok = EnforcerServer.set_persist_adapter(enforcer_name, adapter) + + # Load policies from the adapter + :ok = EnforcerServer.load_policies_from_adapter(enforcer_name) + + # Verify that policies are loaded into memory by checking permissions + assert EnforcerServer.allow?(enforcer_name, ["alice", "blog_post", "read"]) === true + assert EnforcerServer.allow?(enforcer_name, ["alice", "blog_post", "create"]) === true + assert EnforcerServer.allow?(enforcer_name, ["bob", "blog_post", "read"]) === true + assert EnforcerServer.allow?(enforcer_name, ["bob", "blog_post", "create"]) === false + assert EnforcerServer.allow?(enforcer_name, ["peter", "blog_post", "modify"]) === true + end + + test "returns error when adapter is not set", %{enforcer_name: enforcer_name} do + # Try to load without setting an adapter + result = EnforcerServer.load_policies_from_adapter(enforcer_name) + assert result === {:error, "No adapter set and no policy file provided"} + end + + test "policies are available after restart simulation", %{enforcer_name: enforcer_name} do + # Simulate the issue scenario: configure adapter, add policy, restart + adapter = EctoAdapter.new(MockAclRepo) + :ok = EnforcerServer.set_persist_adapter(enforcer_name, adapter) + + # Load policies from adapter (simulating startup after restart) + :ok = EnforcerServer.load_policies_from_adapter(enforcer_name) + + # Verify the loaded policies work + assert EnforcerServer.allow?(enforcer_name, ["alice", "blog_post", "delete"]) === true + end + end + + describe "load_policies_from_adapter/1 for RBAC model" do + setup do + # Start a fresh enforcer for each test + enforcer_name = :"test_enforcer_rbac_#{:erlang.unique_integer([:positive])}" + {:ok, _pid} = EnforcerServer.start_link(enforcer_name, @rbac_cfile) + + on_exit(fn -> + Process.exit( + Process.whereis({:via, Registry, {Acx.EnforcerRegistry, enforcer_name}}), + :kill + ) + end) + + {:ok, enforcer_name: enforcer_name} + end + + test "loads both policies and mapping policies from adapter", %{enforcer_name: enforcer_name} do + # Set the persist adapter + adapter = EctoAdapter.new(MockRbacRepo) + :ok = EnforcerServer.set_persist_adapter(enforcer_name, adapter) + + # Load policies and mapping policies from the adapter + :ok = EnforcerServer.load_policies_from_adapter(enforcer_name) + + # Verify that policies are loaded + assert EnforcerServer.allow?(enforcer_name, ["bob", "blog_post", "read"]) === true + + # Verify that role mappings are loaded (bob has reader role, which has read permission) + # peter is author who inherits from reader + assert EnforcerServer.allow?(enforcer_name, ["peter", "blog_post", "read"]) === true + assert EnforcerServer.allow?(enforcer_name, ["peter", "blog_post", "create"]) === true + + # alice is admin who inherits from author (and reader through author) + assert EnforcerServer.allow?(enforcer_name, ["alice", "blog_post", "read"]) === true + assert EnforcerServer.allow?(enforcer_name, ["alice", "blog_post", "create"]) === true + assert EnforcerServer.allow?(enforcer_name, ["alice", "blog_post", "delete"]) === true + end + end + + describe "Enforcer.load_policies!/1 (direct usage)" do + test "loads policies from adapter directly on Enforcer struct" do + adapter = EctoAdapter.new(MockAclRepo) + {:ok, e} = Enforcer.init(@acl_cfile, adapter) + + # Load policies using the adapter + e = Enforcer.load_policies!(e) + + # Verify policies are loaded + assert Enforcer.allow?(e, ["alice", "blog_post", "read"]) === true + assert Enforcer.allow?(e, ["bob", "blog_post", "create"]) === false + end + + test "returns error when no adapter is set" do + {:ok, e} = Enforcer.init(@acl_cfile) + + # Try to load without an adapter + result = Enforcer.load_policies!(e) + assert result === {:error, "No adapter set and no policy file provided"} + end + end +end From a5152fa373cfb689c0baf4edac4fc4c1607a7dcc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Oct 2025 07:21:49 +0000 Subject: [PATCH 3/8] Add integration tests and improve documentation Co-authored-by: hsluoyz <3787410+hsluoyz@users.noreply.github.com> --- lib/acx/enforcer.ex | 27 +++++ ...r_server_adapter_load_integration_test.exs | 109 ++++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100644 test/persist/enforcer_server_adapter_load_integration_test.exs diff --git a/lib/acx/enforcer.ex b/lib/acx/enforcer.ex index dc836d4..c8dc76e 100644 --- a/lib/acx/enforcer.ex +++ b/lib/acx/enforcer.ex @@ -320,6 +320,33 @@ defmodule Acx.Enforcer do ] """ + @doc """ + Loads policy rules from the configured persist adapter and adds them + to the enforcer. + + This is useful for loading policies from a database or other storage + backend after application startup. The adapter must be configured using + `set_persist_adapter/2` or by passing it to `init/2`. + + Returns the updated enforcer with policies loaded from the adapter, or + `{:error, reason}` if no adapter is configured. + + ## Examples + + # Initialize enforcer with adapter + adapter = Acx.Persist.EctoAdapter.new(MyRepo) + {:ok, e} = Enforcer.init("path/to/model.conf", adapter) + + # Load policies from the adapter + e = Enforcer.load_policies!(e) + + # Now the enforcer has policies from the database + Enforcer.allow?(e, ["alice", "blog_post", "read"]) + # => true + + See also `load_policies!/2` for loading from a file, and + `load_mapping_policies!/1` for loading role mappings from the adapter. + """ @spec load_policies!(t()) :: t() | {:error, any()} def load_policies!(%__MODULE__{persist_adapter: nil}) do {:error, "No adapter set and no policy file provided"} diff --git a/test/persist/enforcer_server_adapter_load_integration_test.exs b/test/persist/enforcer_server_adapter_load_integration_test.exs new file mode 100644 index 0000000..6368557 --- /dev/null +++ b/test/persist/enforcer_server_adapter_load_integration_test.exs @@ -0,0 +1,109 @@ +defmodule Acx.Persist.EnforcerServerAdapterLoadIntegrationTest do + @moduledoc """ + Integration test that demonstrates the exact use case from the GitHub issue: + Loading policies from the database on application startup. + """ + use ExUnit.Case, async: false + alias Acx.EnforcerServer + alias Acx.Persist.EctoAdapter + + defmodule MockRepo do + use Acx.Persist.MockRepo, pfile: "../data/rbac.csv" |> Path.expand(__DIR__) + + # Add transaction support for save_policies if needed + def transaction(fun) do + {:ok, fun.()} + end + end + + @cfile "../data/rbac.conf" |> Path.expand(__DIR__) + + describe "Issue scenario: Load policies from database on startup" do + test "demonstrates the complete workflow from the issue" do + # Step 1: Start a new enforcer (simulating application startup) + enforcer_name = :my_enforcer + {:ok, _pid} = EnforcerServer.start_link(enforcer_name, @cfile) + + on_exit(fn -> + Process.exit(Process.whereis({:via, Registry, {Acx.EnforcerRegistry, enforcer_name}}), :kill) + end) + + # Step 2: Configure the adapter + adapter = EctoAdapter.new(MockRepo) + :ok = EnforcerServer.set_persist_adapter(enforcer_name, adapter) + + # Step 3: Load policies from the database (NEW FUNCTIONALITY) + # This is what was missing before - a clean way to load policies + :ok = EnforcerServer.load_policies_from_adapter(enforcer_name) + + # Step 4: Verify policies are loaded and working + # Admin has delete permission (from the database) + assert EnforcerServer.allow?(enforcer_name, ["alice", "blog_post", "delete"]) === true + + # Author has create and modify permissions (from the database) + assert EnforcerServer.allow?(enforcer_name, ["peter", "blog_post", "create"]) === true + assert EnforcerServer.allow?(enforcer_name, ["peter", "blog_post", "modify"]) === true + + # Reader has read permission (from the database) + assert EnforcerServer.allow?(enforcer_name, ["bob", "blog_post", "read"]) === true + + # Negative test: bob (reader) should not have create permission + assert EnforcerServer.allow?(enforcer_name, ["bob", "blog_post", "create"]) === false + end + + test "demonstrates the issue scenario - before and after" do + enforcer_name = :my_enforcer_2 + {:ok, _pid} = EnforcerServer.start_link(enforcer_name, @cfile) + + on_exit(fn -> + Process.exit(Process.whereis({:via, Registry, {Acx.EnforcerRegistry, enforcer_name}}), :kill) + end) + + # BEFORE: Without load_policies_from_adapter + # Configure adapter + adapter = EctoAdapter.new(MockRepo) + :ok = EnforcerServer.set_persist_adapter(enforcer_name, adapter) + + # At this point, policies are NOT loaded + # The allow? call will return false because policies are not in memory + assert EnforcerServer.allow?(enforcer_name, ["alice", "blog_post", "delete"]) === false + + # AFTER: With load_policies_from_adapter + # Now load policies from the adapter + :ok = EnforcerServer.load_policies_from_adapter(enforcer_name) + + # Now the policies ARE loaded and the allow? call returns the correct result + assert EnforcerServer.allow?(enforcer_name, ["alice", "blog_post", "delete"]) === true + end + + test "simulates application restart scenario" do + # Scenario: Application starts up, needs to load policies from DB + + # Application startup: Create enforcer + enforcer_name = :my_enforcer_restart + {:ok, _pid} = EnforcerServer.start_link(enforcer_name, @cfile) + + on_exit(fn -> + Process.exit(Process.whereis({:via, Registry, {Acx.EnforcerRegistry, enforcer_name}}), :kill) + end) + + # Application startup: Configure persistence + adapter = EctoAdapter.new(MockRepo) + :ok = EnforcerServer.set_persist_adapter(enforcer_name, adapter) + + # Application startup: Load policies from database + # This is the NEW, clean way to load policies on startup + :ok = EnforcerServer.load_policies_from_adapter(enforcer_name) + + # Application is ready to use + # Verify the enforcer has the policies from the database + policies = EnforcerServer.list_policies(enforcer_name, %{key: :p}) + assert length(policies) > 0 + + # Verify role mappings are also loaded + assert EnforcerServer.allow?(enforcer_name, ["alice", "blog_post", "read"]) === true + assert EnforcerServer.allow?(enforcer_name, ["alice", "blog_post", "create"]) === true + assert EnforcerServer.allow?(enforcer_name, ["alice", "blog_post", "delete"]) === true + end + end +end From 310801e65efc5cd8534bdd683f72bebafbc45c9b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Oct 2025 07:23:05 +0000 Subject: [PATCH 4/8] Add comprehensive database persistence documentation to README Co-authored-by: hsluoyz <3787410+hsluoyz@users.noreply.github.com> --- README.md | 98 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/README.md b/README.md index bc02a87..9400d3c 100644 --- a/README.md +++ b/README.md @@ -289,6 +289,104 @@ case EnforcerServer.allow?(ename, new_req) do end ``` +## Database Persistence + +Acx supports persisting policies to a database using the `EctoAdapter`. This allows you to store policies in a database and load them back into memory on application startup. + +### Setup + +First, add the `casbin_rule` table to your database: + +```elixir +defmodule MyApp.Repo.Migrations.CreateCasbinRule do + use Ecto.Migration + + def change do + create table(:casbin_rule) do + add :ptype, :string, null: false + add :v0, :string + add :v1, :string + add :v2, :string + add :v3, :string + add :v4, :string + add :v5, :string + add :v6, :string + end + + create index(:casbin_rule, [:ptype]) + end +end +``` + +### Usage + +```elixir +alias Acx.{EnforcerServer, Persist.EctoAdapter} + +# Start your enforcer with a model configuration +{:ok, _pid} = EnforcerServer.start_link("my_enforcer", "path/to/model.conf") + +# Configure the database adapter +adapter = EctoAdapter.new(MyApp.Repo) +:ok = EnforcerServer.set_persist_adapter("my_enforcer", adapter) + +# Load policies from the database +# This loads both regular policies and role mappings +:ok = EnforcerServer.load_policies_from_adapter("my_enforcer") + +# Now your enforcer is ready with policies from the database +EnforcerServer.allow?("my_enforcer", ["alice", "blog_post", "read"]) +# => true or false based on policies in the database +``` + +### Automatic Persistence + +When you add or remove policies, they are automatically saved to the database: + +```elixir +# This automatically saves to the database +EnforcerServer.add_policy("my_enforcer", {:p, ["admin", "data", "write"]}) + +# This also automatically saves to the database +EnforcerServer.add_mapping_policy("my_enforcer", {:g, "alice", "admin"}) +``` + +### Application Startup Pattern + +A typical pattern for loading policies on application startup: + +```elixir +defmodule MyApp.Application do + use Application + + def start(_type, _args) do + children = [ + MyApp.Repo, + # ... other children + {Acx.EnforcerSupervisor, []}, + ] + + opts = [strategy: :one_for_one, name: MyApp.Supervisor] + result = Supervisor.start_link(children, opts) + + # Initialize the enforcer after the repo is started + setup_enforcer() + + result + end + + defp setup_enforcer do + {:ok, _pid} = Acx.EnforcerServer.start_link("my_enforcer", "priv/model.conf") + + adapter = Acx.Persist.EctoAdapter.new(MyApp.Repo) + :ok = Acx.EnforcerServer.set_persist_adapter("my_enforcer", adapter) + + # Load policies from database on startup + :ok = Acx.EnforcerServer.load_policies_from_adapter("my_enforcer") + end +end +``` + ## TODO ### Global From 7e0af032a54e91df781a6cb5241f493c82054c5b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Oct 2025 07:29:11 +0000 Subject: [PATCH 5/8] Apply code formatting Co-authored-by: hsluoyz <3787410+hsluoyz@users.noreply.github.com> --- ...orcer_server_adapter_load_integration_test.exs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/test/persist/enforcer_server_adapter_load_integration_test.exs b/test/persist/enforcer_server_adapter_load_integration_test.exs index 6368557..4c9eef3 100644 --- a/test/persist/enforcer_server_adapter_load_integration_test.exs +++ b/test/persist/enforcer_server_adapter_load_integration_test.exs @@ -25,7 +25,10 @@ defmodule Acx.Persist.EnforcerServerAdapterLoadIntegrationTest do {:ok, _pid} = EnforcerServer.start_link(enforcer_name, @cfile) on_exit(fn -> - Process.exit(Process.whereis({:via, Registry, {Acx.EnforcerRegistry, enforcer_name}}), :kill) + Process.exit( + Process.whereis({:via, Registry, {Acx.EnforcerRegistry, enforcer_name}}), + :kill + ) end) # Step 2: Configure the adapter @@ -56,7 +59,10 @@ defmodule Acx.Persist.EnforcerServerAdapterLoadIntegrationTest do {:ok, _pid} = EnforcerServer.start_link(enforcer_name, @cfile) on_exit(fn -> - Process.exit(Process.whereis({:via, Registry, {Acx.EnforcerRegistry, enforcer_name}}), :kill) + Process.exit( + Process.whereis({:via, Registry, {Acx.EnforcerRegistry, enforcer_name}}), + :kill + ) end) # BEFORE: Without load_policies_from_adapter @@ -84,7 +90,10 @@ defmodule Acx.Persist.EnforcerServerAdapterLoadIntegrationTest do {:ok, _pid} = EnforcerServer.start_link(enforcer_name, @cfile) on_exit(fn -> - Process.exit(Process.whereis({:via, Registry, {Acx.EnforcerRegistry, enforcer_name}}), :kill) + Process.exit( + Process.whereis({:via, Registry, {Acx.EnforcerRegistry, enforcer_name}}), + :kill + ) end) # Application startup: Configure persistence From ade04372f8556683f3fc6e41711e49aeef35874b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Oct 2025 07:33:17 +0000 Subject: [PATCH 6/8] Improve error handling in test teardown Co-authored-by: hsluoyz <3787410+hsluoyz@users.noreply.github.com> --- test/persist/ecto_adapter_load_test.exs | 16 ++++++------- ...r_server_adapter_load_integration_test.exs | 24 +++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/test/persist/ecto_adapter_load_test.exs b/test/persist/ecto_adapter_load_test.exs index c11dd72..059ab24 100644 --- a/test/persist/ecto_adapter_load_test.exs +++ b/test/persist/ecto_adapter_load_test.exs @@ -22,10 +22,10 @@ defmodule Acx.Persist.EctoAdapterLoadTest do {:ok, _pid} = EnforcerServer.start_link(enforcer_name, @acl_cfile) on_exit(fn -> - Process.exit( - Process.whereis({:via, Registry, {Acx.EnforcerRegistry, enforcer_name}}), - :kill - ) + case Process.whereis({:via, Registry, {Acx.EnforcerRegistry, enforcer_name}}) do + nil -> :ok + pid -> Process.exit(pid, :kill) + end end) {:ok, enforcer_name: enforcer_name} @@ -73,10 +73,10 @@ defmodule Acx.Persist.EctoAdapterLoadTest do {:ok, _pid} = EnforcerServer.start_link(enforcer_name, @rbac_cfile) on_exit(fn -> - Process.exit( - Process.whereis({:via, Registry, {Acx.EnforcerRegistry, enforcer_name}}), - :kill - ) + case Process.whereis({:via, Registry, {Acx.EnforcerRegistry, enforcer_name}}) do + nil -> :ok + pid -> Process.exit(pid, :kill) + end end) {:ok, enforcer_name: enforcer_name} diff --git a/test/persist/enforcer_server_adapter_load_integration_test.exs b/test/persist/enforcer_server_adapter_load_integration_test.exs index 4c9eef3..20035d6 100644 --- a/test/persist/enforcer_server_adapter_load_integration_test.exs +++ b/test/persist/enforcer_server_adapter_load_integration_test.exs @@ -25,10 +25,10 @@ defmodule Acx.Persist.EnforcerServerAdapterLoadIntegrationTest do {:ok, _pid} = EnforcerServer.start_link(enforcer_name, @cfile) on_exit(fn -> - Process.exit( - Process.whereis({:via, Registry, {Acx.EnforcerRegistry, enforcer_name}}), - :kill - ) + case Process.whereis({:via, Registry, {Acx.EnforcerRegistry, enforcer_name}}) do + nil -> :ok + pid -> Process.exit(pid, :kill) + end end) # Step 2: Configure the adapter @@ -59,10 +59,10 @@ defmodule Acx.Persist.EnforcerServerAdapterLoadIntegrationTest do {:ok, _pid} = EnforcerServer.start_link(enforcer_name, @cfile) on_exit(fn -> - Process.exit( - Process.whereis({:via, Registry, {Acx.EnforcerRegistry, enforcer_name}}), - :kill - ) + case Process.whereis({:via, Registry, {Acx.EnforcerRegistry, enforcer_name}}) do + nil -> :ok + pid -> Process.exit(pid, :kill) + end end) # BEFORE: Without load_policies_from_adapter @@ -90,10 +90,10 @@ defmodule Acx.Persist.EnforcerServerAdapterLoadIntegrationTest do {:ok, _pid} = EnforcerServer.start_link(enforcer_name, @cfile) on_exit(fn -> - Process.exit( - Process.whereis({:via, Registry, {Acx.EnforcerRegistry, enforcer_name}}), - :kill - ) + case Process.whereis({:via, Registry, {Acx.EnforcerRegistry, enforcer_name}}) do + nil -> :ok + pid -> Process.exit(pid, :kill) + end end) # Application startup: Configure persistence From 5b7412f853f845a6971150c95e09bafef986080e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Oct 2025 07:35:19 +0000 Subject: [PATCH 7/8] Add exception handling to load_policies_from_adapter handler Co-authored-by: hsluoyz <3787410+hsluoyz@users.noreply.github.com> --- lib/acx/enforcer_server.ex | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/lib/acx/enforcer_server.ex b/lib/acx/enforcer_server.ex index c86c63c..e40e331 100644 --- a/lib/acx/enforcer_server.ex +++ b/lib/acx/enforcer_server.ex @@ -273,14 +273,18 @@ defmodule Acx.EnforcerServer do end def handle_call({:load_policies_from_adapter}, _from, enforcer) do - case Enforcer.load_policies!(enforcer) do - {:error, reason} -> - {:reply, {:error, reason}, enforcer} - - new_enforcer -> - new_enforcer = Enforcer.load_mapping_policies!(new_enforcer) - :ets.insert(:enforcers_table, {self_name(), new_enforcer}) - {:reply, :ok, new_enforcer} + try do + case Enforcer.load_policies!(enforcer) do + {:error, reason} -> + {:reply, {:error, reason}, enforcer} + + new_enforcer -> + new_enforcer = Enforcer.load_mapping_policies!(new_enforcer) + :ets.insert(:enforcers_table, {self_name(), new_enforcer}) + {:reply, :ok, new_enforcer} + end + rescue + e -> {:reply, {:error, Exception.message(e)}, enforcer} end end From 6d9950ba663cd005bf279fe4e280a2b064a5a099 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Oct 2025 09:16:50 +0000 Subject: [PATCH 8/8] Fix duplicate @doc warning by reordering function definitions Move load_policies!/2 before load_policies!/1 so each arity has its own @doc block without conflicts. Co-authored-by: hsluoyz <3787410+hsluoyz@users.noreply.github.com> --- lib/acx/enforcer.ex | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/lib/acx/enforcer.ex b/lib/acx/enforcer.ex index c8dc76e..d0f8bd6 100644 --- a/lib/acx/enforcer.ex +++ b/lib/acx/enforcer.ex @@ -319,6 +319,21 @@ defmodule Acx.Enforcer do } ] """ + @spec load_policies!(t(), String.t()) :: t() + def load_policies!(%__MODULE__{model: m} = enforcer, pfile) + when is_binary(pfile) do + adapter = ReadonlyFileAdapter.new(pfile) + enforcer = %{enforcer | persist_adapter: adapter} + + case PersistAdapter.load_policies(adapter) do + {:ok, policies} -> + policies + |> Enum.map(fn [key | attrs] -> [String.to_atom(key) | attrs] end) + |> Enum.filter(fn [key | _] -> Model.has_policy_key?(m, key) end) + |> Enum.map(fn [key | attrs] -> {key, attrs} end) + |> Enum.reduce(enforcer, &load_policy!(&2, &1)) + end + end @doc """ Loads policy rules from the configured persist adapter and adds them @@ -364,22 +379,6 @@ defmodule Acx.Enforcer do end end - @spec load_policies!(t(), String.t()) :: t() - def load_policies!(%__MODULE__{model: m} = enforcer, pfile) - when is_binary(pfile) do - adapter = ReadonlyFileAdapter.new(pfile) - enforcer = %{enforcer | persist_adapter: adapter} - - case PersistAdapter.load_policies(adapter) do - {:ok, policies} -> - policies - |> Enum.map(fn [key | attrs] -> [String.to_atom(key) | attrs] end) - |> Enum.filter(fn [key | _] -> Model.has_policy_key?(m, key) end) - |> Enum.map(fn [key | attrs] -> {key, attrs} end) - |> Enum.reduce(enforcer, &load_policy!(&2, &1)) - end - end - @doc """ Returns a list of policies in the given enforcer that match the given criteria.