-
-
Notifications
You must be signed in to change notification settings - Fork 19
Description
Summary
Add support for filtered policy loading to enable efficient policy enforcement in large-scale, multi-tenant applications. This feature is part of the Casbin core specification but is not yet implemented in casbin-ex.
Issue
In multi-tenant applications with thousands of organizations and millions of policy rules, loading all policies into memory creates significant performance bottlenecks:
- Memory overhead: Loading 1M+ policies can consume 500MB-2GB of RAM
- Startup time: Parsing large policy sets can take 30-60+ seconds
- Scalability: Memory usage grows linearly with tenant count
As stated in the Casbin documentation:
"Policy subset loading enables filtered policy management... This proves particularly valuable in large, multi-tenant systems where performance degradation from parsing entire policy databases becomes problematic."
Current State
The casbin-ex library currently only supports loading all policies:
# Current API - loads ALL policies from database
{:ok, policies} = PersistAdapter.load_policies(adapter)
For applications using domains (e.g., org:tenant_id), this means loading policies for ALL tenants even when only checking permissions for a single tenant.
Proposed Solution
Implement filtered policy loading following the Casbin specification:
1. Protocol Extension
defprotocol Acx.Persist.PersistAdapter do
def load_policies(adapter)
def load_filtered_policy(adapter, filter) # NEW
def load_incremental_filtered_policy(adapter, filter) # NEW (optional)
def add_policy(adapter, policy)
def remove_policy(adapter, policy)
def remove_filtered_policy(adapter, key, idx, attrs)
def save_policies(adapter, policies)
end
2. EctoAdapter Implementation
@doc """
Loads only policies matching the given filter.
## Examples
# Load policies for a specific domain
filter = %{v3: "org:tenant_123"}
PersistAdapter.load_filtered_policy(adapter, filter)
# Load policies with multiple criteria
filter = %{ptype: "p", v3: ["org:tenant_1", "org:tenant_2"]}
PersistAdapter.load_filtered_policy(adapter, filter)
"""
def load_filtered_policy(adapter, filter) when is_map(filter) do
query = build_filtered_query(filter)
policies =
adapter.repo.all(query)
|> Enum.map(&CasbinRule.changeset_to_list(&1))
{:ok, policies}
end
defp build_filtered_query(filter) do
Enum.reduce(filter, from(r in CasbinRule), fn
{:ptype, values}, query when is_list(values) ->
where(query, [r], r.ptype in ^values)
{:ptype, value}, query ->
where(query, [r], r.ptype == ^value)
{:v0, values}, query when is_list(values) ->
where(query, [r], r.v0 in ^values)
{:v0, value}, query ->
where(query, [r], r.v0 == ^value)
# ... similar for v1-v6
_, query ->
query
end)
end
3. Enforcer Module
@doc """
Loads policies from the persist adapter using the given filter.
Only matching policies are loaded into the enforcer.
## Examples
# Load only policies for a specific domain
enforcer = Enforcer.load_filtered_policies!(enforcer, %{v3: "org:abc"})
"""
@spec load_filtered_policies!(t(), map()) :: t()
def load_filtered_policies!(
%__MODULE__{model: m, persist_adapter: adapter} = enforcer,
filter
) do
case PersistAdapter.load_filtered_policy(adapter, filter) 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
4. EnforcerServer API
@doc """
Loads filtered policies from the persist adapter.
"""
def load_filtered_policies(ename, filter) do
GenServer.call(via_tuple(ename), {:load_filtered_policies, filter})
end
def handle_call({:load_filtered_policies, filter}, _from, enforcer) do
new_enforcer = Enforcer.load_filtered_policies!(enforcer, filter)
{:reply, :ok, new_enforcer}
end
Usage Example
# Multi-tenant application
defmodule MyApp.Authorization do
alias Acx.{Enforcer, EnforcerServer}
alias Acx.Persist.EctoAdapter
def start_tenant_enforcer(org_id) do
enforcer = Enforcer.new("priv/casbin/model.conf")
adapter = EctoAdapter.new(MyApp.Repo)
enforcer = Enforcer.set_persist_adapter(enforcer, adapter)
# Load ONLY policies for this organization
filter = %{v3: "org:#{org_id}"}
enforcer = Enforcer.load_filtered_policies!(enforcer, filter)
# Now enforcer contains only policies for org_id
# Memory usage: ~1KB per org vs 500MB for all orgs
enforcer
end
end