Skip to content

Feature request for Filtered Policy Loading #37

@sushilbansal

Description

@sushilbansal

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

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions