Skip to content
Closed
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
39 changes: 39 additions & 0 deletions lib/acx/enforcer_server.ex
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,19 @@ 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's memory.

This function is useful for loading policies from a database adapter
(like EctoAdapter) on application startup.

See `Enforcer.load_policies!/1` for more details.
"""
def load_policies_from_adapter(ename) do
GenServer.call(via_tuple(ename), {:load_policies_from_adapter})
end

@doc """
Loads filtered policies from the persist adapter.
Only policies matching the filter are loaded into the enforcer.
Expand Down Expand Up @@ -173,6 +186,19 @@ defmodule Acx.EnforcerServer do
GenServer.call(via_tuple(ename), {:load_mapping_policies, fname})
end

@doc """
Loads mapping policies from the configured persist adapter and adds them
to the enforcer's memory.

This function is useful for loading role mappings from a database adapter
(like EctoAdapter) on application startup.

See `Enforcer.load_mapping_policies!/1` for more details.
"""
def load_mapping_policies_from_adapter(ename) do
GenServer.call(via_tuple(ename), {:load_mapping_policies_from_adapter})
end

@doc """
Return a fresh enforcer.

Expand Down Expand Up @@ -255,6 +281,12 @@ defmodule Acx.EnforcerServer do
{:reply, :ok, new_enforcer}
end

def handle_call({:load_policies_from_adapter}, _from, enforcer) do
new_enforcer = enforcer |> Enforcer.load_policies!()
:ets.insert(:enforcers_table, {self_name(), new_enforcer})
{:reply, :ok, new_enforcer}
end

def handle_call({:load_filtered_policies, filter}, _from, enforcer) do
new_enforcer = enforcer |> Enforcer.load_filtered_policies!(filter)
:ets.insert(:enforcers_table, {self_name(), new_enforcer})
Expand Down Expand Up @@ -292,6 +324,12 @@ defmodule Acx.EnforcerServer do
{:reply, :ok, new_enforcer}
end

def handle_call({:load_mapping_policies_from_adapter}, _from, enforcer) do
new_enforcer = enforcer |> Enforcer.load_mapping_policies!()
:ets.insert(:enforcers_table, {self_name(), new_enforcer})
{:reply, :ok, new_enforcer}
end

def handle_call({:remove_mapping_policy, mapping}, _from, enforcer) do
case Enforcer.remove_mapping_policy(enforcer, mapping) do
{:error, reason} ->
Expand Down Expand Up @@ -374,3 +412,4 @@ defmodule Acx.EnforcerServer do
end
end
end

130 changes: 130 additions & 0 deletions test/enforcer_server_load_from_adapter_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
defmodule Acx.EnforcerServerLoadFromAdapterTest do
use ExUnit.Case, async: true
alias Acx.EnforcerServer
alias Acx.Persist.EctoAdapter

@enforcer_name "test_enforcer_load_from_adapter"
@model_file "../data/rbac.conf" |> Path.expand(__DIR__)
@policy_file "../data/acl.csv" |> Path.expand(__DIR__)

defmodule MockTestRepo 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

setup do
# Start the enforcer with just the model
{:ok, _pid} = EnforcerServer.start_link(@enforcer_name, @model_file)

# Clean up after the test
on_exit(fn ->
# The enforcer process will be terminated automatically
# Remove from ETS table if needed
try do
GenServer.stop(
{:via, Registry, {Acx.EnforcerRegistry, @enforcer_name}},
:normal,
100
)
catch
:exit, _ -> :ok
end
end)

:ok
end

describe "load_policies_from_adapter/1" do
test "loads policies from EctoAdapter into enforcer memory" do
# Configure adapter
adapter = EctoAdapter.new(MockTestRepo)
:ok = EnforcerServer.set_persist_adapter(@enforcer_name, adapter)

# Load policies from adapter
:ok = EnforcerServer.load_policies_from_adapter(@enforcer_name)

# Verify policies are loaded into memory
assert EnforcerServer.allow?(@enforcer_name, ["alice", "blog_post", "read"])
assert EnforcerServer.allow?(@enforcer_name, ["alice", "blog_post", "create"])
assert EnforcerServer.allow?(@enforcer_name, ["bob", "blog_post", "read"])
refute EnforcerServer.allow?(@enforcer_name, ["bob", "blog_post", "create"])
end

test "policies persist in memory after application restart simulation" do
# Initial setup - configure adapter and load policies
adapter = EctoAdapter.new(MockTestRepo)
:ok = EnforcerServer.set_persist_adapter(@enforcer_name, adapter)
:ok = EnforcerServer.load_policies_from_adapter(@enforcer_name)

# Verify initial state
assert EnforcerServer.allow?(@enforcer_name, ["alice", "blog_post", "read"])

# Add a new policy (should be persisted to "database")
:ok = EnforcerServer.add_policy(@enforcer_name, {:p, ["charlie", "blog_post", "read"]})

# Verify new policy works
assert EnforcerServer.allow?(@enforcer_name, ["charlie", "blog_post", "read"])
end

test "works with empty adapter" do
# Create a mock repo with an empty file
empty_file = "/tmp/empty_policies.csv"
File.write!(empty_file, "")

defmodule EmptyMockRepo do
use Acx.Persist.MockRepo, pfile: "/tmp/empty_policies.csv"
end

adapter = EctoAdapter.new(EmptyMockRepo)
:ok = EnforcerServer.set_persist_adapter(@enforcer_name, adapter)

# Should not raise error
:ok = EnforcerServer.load_policies_from_adapter(@enforcer_name)

# No policies should be loaded
refute EnforcerServer.allow?(@enforcer_name, ["alice", "blog_post", "read"])

# Clean up
File.rm(empty_file)
end
end

describe "load_mapping_policies_from_adapter/1" do
test "loads role mappings from EctoAdapter into enforcer memory" do
# Configure adapter with RBAC policies
adapter = EctoAdapter.new(MockRbacRepo)
:ok = EnforcerServer.set_persist_adapter(@enforcer_name, adapter)

# Load both policies and mapping policies
:ok = EnforcerServer.load_policies_from_adapter(@enforcer_name)
:ok = EnforcerServer.load_mapping_policies_from_adapter(@enforcer_name)

# Verify role-based access works
# bob is reader (via role mapping in rbac.csv)
assert EnforcerServer.allow?(@enforcer_name, ["bob", "blog_post", "read"])

# alice is admin (via role mapping in rbac.csv)
assert EnforcerServer.allow?(@enforcer_name, ["alice", "blog_post", "write"])
assert EnforcerServer.allow?(@enforcer_name, ["alice", "blog_post", "read"])
end
end

describe "integration with load_filtered_policies/2" do
test "can use filtered loading after setting adapter" do
adapter = EctoAdapter.new(MockTestRepo)
:ok = EnforcerServer.set_persist_adapter(@enforcer_name, adapter)

# Load only policies for alice (filter by subject)
:ok = EnforcerServer.load_filtered_policies(@enforcer_name, %{v0: "alice"})

# alice's policies should be loaded
assert EnforcerServer.allow?(@enforcer_name, ["alice", "blog_post", "read"])

# bob's policies should NOT be loaded (filtered out)
refute EnforcerServer.allow?(@enforcer_name, ["bob", "blog_post", "read"])
end
end
end
Loading