Definition

A role is an abstraction that groups a set of permissions under a named identity, simplifying access control management. Instead of assigning individual permissions to each user, administrators assign roles, and the system derives permissions from role membership. This indirection reduces management complexity from O(users x permissions) to O(users x roles) + O(roles x permissions), and ensures consistent authorization across users with similar responsibilities.

RBAC (Role-Based Access Control) is the standard authorization model for enterprise systems, formalized in NIST INCITS 359-2004. It defines a hierarchy: users are assigned roles, roles contain permissions, and permissions grant access to resources and operations. Advanced RBAC models support role hierarchies (senior roles inherit junior role permissions), constraints (mutual exclusion between roles), and dynamic activation (users activate specific roles per session). The Prismatic Platform extends this model with a dual-layer system: human user roles for web interface and API access, and AIAD agent tiers for automated operation authorization.

The distinction between authentication ("who are you?") and authorization ("what can you do?") is fundamental. Roles operate entirely in the authorization domain -- they assume identity has already been established through authentication (tokens, sessions, certificates). A properly designed role system makes authorization decisions based solely on role membership, never on identity directly. This separation enables role-based auditing, delegation, and principle-of-least-privilege enforcement.

Core Concepts

Role Hierarchy: Human Users

RoleLevelPermissionsTypical UserAccess Scope
viewer1Read-only access to dashboards and reportsExternal stakeholders, clientsView KPIs, read DD reports, browse glossary
analyst2Read + execute investigations and toolsIntelligence analysts, DD specialistsRun OSINT tools, create DD cases, generate reports
operator3Analyst + manage pipelines and configurationsDevOps, platform operatorsStart/stop pipelines, manage tool configs, view logs
admin4Full access to all platform featuresPlatform administratorsUser management, system config, deployment controls
superadmin5Admin + doctrine modification + destructive opsPlatform ownerModify doctrine rules, purge data, manage AIAD agents

Role Hierarchy: AIAD Agents

TierDesignationAuthorization LevelCapabilitiesExample Agents
L1OperationalBasic task executionSingle-domain operations, non-destructive readsLog analyzers, metric collectors
L2TacticalMulti-step operationsCross-module coordination, limited writesTest runners, code formatters
L3StrategicDomain-spanning operationsArchitecture decisions, schema modificationsRefactoring agents, migration planners
L4SpecialistFull domain authorityDestructive operations within domain, policy enforcementSecurity auditors, quality enforcers
L5SupremePlatform-wide authorityCross-domain operations, infrastructure changesArcher Supreme, deployment orchestrators
COSMICCosmic ClearanceUnrestrictedDoctrine modification, meta-evolution, system overrideMeta-evolution agent (unique)

Casbin Policy Model

ComponentDefinitionExample
Subject (sub)Who is requesting accessuser:admin, agent:archer-supreme, role:analyst
Object (obj)What resource is being accessed/api/v1/dd/cases, /hub/osint/tools, doctrine:nmnd
Action (act)What operation is requestedread, write, execute, delete, modify
Effect (eft)Allow or denyallow, deny
ConditionAdditional constraintstime_of_day, ip_range, data_classification

Permission Enforcement Points

LayerMechanismTimingScope
API GatewayPrismaticWeb.Plugs.RequireRoleBefore controller actionHTTP request
LiveView Mounton_mount callback with role checkDuring LiveView mountWebSocket connection
Channel Joinjoin/3 callback with role verificationDuring channel subscriptionPubSub topic
GenServer CallPattern match on caller roleDuring business logic executionInter-process call
AIAD CommandTier check before command dispatchBefore agent executionAgent operation
Casbin EvaluationPolicy engine queryAt any enforcement pointFlexible

Technical Deep Dive

Phoenix Plug-Based Enforcement

Role enforcement in Phoenix applications typically involves three components: role storage (database or configuration), role assignment (associating users with roles), and role enforcement (checking permissions before allowing operations). Phoenix plugs provide the ideal mechanism for HTTP-level enforcement because they execute before controller actions and can short-circuit the request pipeline.

The Prismatic Platform implements role enforcement as composable plugs. The RequireRole plug extracts the user's roles from the connection assigns (populated by the authentication plug earlier in the pipeline) and compares them against the required roles for the endpoint. If the user lacks the required role, the plug returns a 403 Forbidden response and halts the pipeline.

For LiveView pages, role enforcement happens in the on_mount callback. Since LiveView operates over WebSocket, the initial mount is the security boundary -- once mounted, subsequent events within the same LiveView session inherit the mount-time authorization. This means role changes take effect on the next page load, not during an active session.

Casbin Policy Engine Integration

Casbin is a powerful authorization library that supports multiple access control models (RBAC, ABAC, ACL) through a model-policy separation. The model defines the structure of authorization rules; the policy defines the specific rules. This separation allows the authorization model to evolve without changing application code.

The Prismatic Platform uses Casbin for complex authorization decisions that go beyond simple role checks:

  • Multi-factor authorization: "Analyst can access DD case only if they are assigned to the case AND the case is not sealed"
  • Time-based constraints: "OSINT tools can only be executed during business hours for non-admin users"
  • Data-level access control: "Viewer can see case summary but not raw entity data"
  • Agent tier enforcement: "L2 agent cannot execute L3 strategic commands"

Casbin policies are stored in a PostgreSQL table and cached in ETS for sub-millisecond evaluation. Policy updates are distributed across cluster nodes via PubSub.

Role Assignment and Session Management

Roles are stored in the user_roles join table with optional metadata (assigned_by, assigned_at, expires_at). Role assignments are audited -- every grant and revocation is logged with the performing user, timestamp, and reason.

Session management interacts with roles through the authentication token. JWT tokens carry role claims, and the authentication plug decodes these claims into connection assigns. Token refresh validates that roles have not been revoked since the token was issued.

For agents, roles (tiers) are defined in the agent specification file (.aiad/agents/*.agent.md) and are immutable at runtime. An agent's tier determines what AIAD commands it can execute and what system resources it can access.

Role Inheritance and Composition

The platform supports role inheritance: admin inherits all operator permissions, which inherits all analyst permissions, which inherits all viewer permissions. This is implemented through a role hierarchy map rather than duplicating permissions.

Role composition (a user having multiple roles) follows a union model: the effective permission set is the union of all assigned role permissions. Deny rules take precedence over allow rules to enable "except" patterns (e.g., "analyst can access all tools EXCEPT classified ones").

Usage in Prismatic Platform

API Gateway Enforcement

The API gateway enforces roles through the PrismaticWeb.Plugs.APIAuth plug, which extracts the user's role from the authentication token and verifies it against the endpoint's required permissions. Each router scope declares its minimum role:

# In router.ex
scope "/api/v1/dd", PrismaticWeb.API.V1 do
  pipe_through [:api, :require_auth, :require_role_analyst]
  resources "/cases", DDCaseController
end

scope "/api/v1/admin", PrismaticWeb.API.V1 do
  pipe_through [:api, :require_auth, :require_role_admin]
  resources "/users", UserController
end

LiveView Mount Guards

LiveView pages check roles in the on_mount/4 callback, redirecting unauthorized users to an appropriate error page. The mount guard pattern is standardized across all protected LiveViews:

# Applied via on_mount hook in router
defmodule PrismaticWeb.RequireAnalystRole do
  def on_mount(:default, _params, session, socket) do
    case Map.get(session, "user_roles", []) do
      roles when is_list(roles) ->
        if "analyst" in roles or "admin" in roles or "superadmin" in roles do
          {:cont, Phoenix.Component.assign(socket, :current_user_roles, roles)}
        else
          {:halt, Phoenix.LiveView.redirect(socket, to: "/unauthorized")}
        end

      _ ->
        {:halt, Phoenix.LiveView.redirect(socket, to: "/login")}
    end
  end
end

AIAD Tier Authorization

Agent roles (AIAD tiers) are defined in agent specification files and enforced at the command execution level. An L2 tactical agent cannot execute L3 strategic commands, and no agent below COSMIC CLEARANCE can modify doctrine-level configurations:

# Before executing any AIAD command
case PrismaticAgents.TierAuthorization.authorized?(agent.tier, command.required_tier) do
  true -> execute_command(agent, command)
  false ->
    Logger.warning("Agent #{agent.name} (#{agent.tier}) unauthorized for #{command.name} (requires #{command.required_tier})")
    {:error, :insufficient_tier}
end

DD Dataroom RBAC

The DD dataroom feature implements fine-grained RBAC with four roles specific to the M&A context:

Dataroom RoleRead DocumentsUpload DocumentsApprove DocumentsManage Access
dd_viewerYes (non-confidential)NoNoNo
dd_contributorYes (all)YesNoNo
dd_reviewerYes (all)YesYesNo
dd_adminYes (all)YesYesYes

Code Examples

defmodule PrismaticWeb.Plugs.RequireRole do
  @moduledoc """
  Plug that enforces role-based access control on routes.

  Checks the current user's roles (from conn.assigns) against
  the required roles for the route. Supports role inheritance
  through the role hierarchy.

  ## Usage in Router

      pipe_through [RequireRole.init(:admin)]
      pipe_through [RequireRole.init([:analyst, :operator])]

  ## Role Hierarchy

  Higher roles inherit permissions from lower roles:
  superadmin > admin > operator > analyst > viewer
  """

  import Plug.Conn
  import Phoenix.Controller

  @behaviour Plug

  @role_hierarchy %{
    viewer: 1,
    analyst: 2,
    operator: 3,
    admin: 4,
    superadmin: 5
  }

  @type role :: :viewer | :analyst | :operator | :admin | :superadmin

  @impl true
  @spec init(role() | list(role())) :: list(role())
  def init(roles) when is_list(roles), do: roles
  def init(role) when is_atom(role), do: [role]

  @impl true
  @spec call(Plug.Conn.t(), list(role())) :: Plug.Conn.t()
  def call(conn, required_roles) do
    user_roles = conn.assigns[:current_user_roles] || []

    if authorized?(user_roles, required_roles) do
      conn
    else
      conn
      |> put_status(:forbidden)
      |> json(%{
        error: "insufficient_permissions",
        required: required_roles,
        message: "You do not have the required role to access this resource"
      })
      |> halt()
    end
  end

  @doc """
  Checks if any of the user's roles satisfies the required role,
  accounting for role hierarchy (admin satisfies analyst requirement).

  ## Examples

      iex> PrismaticWeb.Plugs.RequireRole.authorized?([:admin], [:analyst])
      true

      iex> PrismaticWeb.Plugs.RequireRole.authorized?([:viewer], [:analyst])
      false

      iex> PrismaticWeb.Plugs.RequireRole.authorized?([:analyst, :viewer], [:operator])
      false
  """
  @spec authorized?(list(atom()), list(atom())) :: boolean()
  def authorized?(user_roles, required_roles) do
    max_user_level =
      user_roles
      |> Enum.map(&Map.get(@role_hierarchy, &1, 0))
      |> Enum.max(fn -> 0 end)

    min_required_level =
      required_roles
      |> Enum.map(&Map.get(@role_hierarchy, &1, 999))
      |> Enum.min(fn -> 999 end)

    max_user_level >= min_required_level
  end
end
defmodule PrismaticAgents.TierAuthorization do
  @moduledoc """
  Enforces AIAD tier-based authorization for agent operations.

  The tier system provides a hierarchical authorization model where
  higher-tier agents can perform all operations available to lower tiers
  plus their own tier-specific operations.

  ## Tier Hierarchy

  - L1 (Operational): Basic task execution
  - L2 (Tactical): Multi-step operations
  - L3 (Strategic): Domain-spanning operations
  - L4 (Specialist): Full domain authority
  - L5 (Supreme): Platform-wide authority
  - COSMIC: Unrestricted access (doctrine modification)

  ## Usage

      if TierAuthorization.authorized?(:l3_strategic, :l2_tactical) do
        # L3 agent can execute L2 commands
      end
  """

  @tier_hierarchy %{
    l1_operational: 1,
    l2_tactical: 2,
    l3_strategic: 3,
    l4_specialist: 4,
    l5_supreme: 5,
    cosmic: 6
  }

  @type tier :: :l1_operational | :l2_tactical | :l3_strategic | :l4_specialist | :l5_supreme | :cosmic

  @doc """
  Checks whether an agent with the given tier is authorized to perform
  an operation requiring the specified tier.

  ## Examples

      iex> PrismaticAgents.TierAuthorization.authorized?(:l3_strategic, :l2_tactical)
      true

      iex> PrismaticAgents.TierAuthorization.authorized?(:l1_operational, :l3_strategic)
      false

      iex> PrismaticAgents.TierAuthorization.authorized?(:cosmic, :l5_supreme)
      true
  """
  @spec authorized?(tier(), tier()) :: boolean()
  def authorized?(agent_tier, required_tier) do
    agent_level = Map.get(@tier_hierarchy, agent_tier, 0)
    required_level = Map.get(@tier_hierarchy, required_tier, 0)
    agent_level >= required_level
  end

  @doc """
  Returns the numeric level for a tier.

  ## Examples

      iex> PrismaticAgents.TierAuthorization.level(:l3_strategic)
      3

      iex> PrismaticAgents.TierAuthorization.level(:cosmic)
      6
  """
  @spec level(tier()) :: pos_integer()
  def level(tier) do
    Map.get(@tier_hierarchy, tier, 0)
  end

  @doc """
  Returns all tiers that are authorized for the given required tier,
  sorted from lowest to highest.

  ## Examples

      iex> PrismaticAgents.TierAuthorization.authorized_tiers(:l2_tactical)
      [:l2_tactical, :l3_strategic, :l4_specialist, :l5_supreme, :cosmic]
  """
  @spec authorized_tiers(tier()) :: list(tier())
  def authorized_tiers(required_tier) do
    required_level = Map.get(@tier_hierarchy, required_tier, 0)

    @tier_hierarchy
    |> Enum.filter(fn {_tier, level} -> level >= required_level end)
    |> Enum.sort_by(fn {_tier, level} -> level end)
    |> Enum.map(fn {tier, _level} -> tier end)
  end
end
defmodule PrismaticAuth.CasbinAdapter do
  @moduledoc """
  Casbin policy adapter for the Prismatic Platform.

  Loads authorization policies from PostgreSQL and caches them in ETS
  for sub-millisecond evaluation. Supports RBAC with role hierarchy
  and attribute-based conditions.

  ## Policy Format

      {subject, object, action, effect}

  ## Examples

      # Allow analysts to read DD cases
      {"role:analyst", "/api/v1/dd/cases/*", "read", "allow"}

      # Deny viewers from accessing raw entity data
      {"role:viewer", "/api/v1/dd/entities/*/raw", "*", "deny"}

      # Allow L3+ agents to execute strategic commands
      {"tier:l3+", "command:strategic:*", "execute", "allow"}
  """

  @type policy :: {String.t(), String.t(), String.t(), String.t()}
  @type evaluation_result :: :allow | :deny | :not_found

  @ets_table :casbin_policies

  @doc """
  Initializes the policy cache from the database.

  ## Examples

      iex> PrismaticAuth.CasbinAdapter.load_policies()
      {:ok, 42}
  """
  @spec load_policies() :: {:ok, non_neg_integer()}
  def load_policies do
    :ets.new(@ets_table, [:named_table, :bag, :public, read_concurrency: true])
    # In production, load from PostgreSQL
    count = 0
    {:ok, count}
  end

  @doc """
  Evaluates whether the given subject can perform the action on the object.

  Deny rules take precedence over allow rules.

  ## Examples

      iex> PrismaticAuth.CasbinAdapter.enforce("role:admin", "/api/v1/dd/cases", "read")
      :allow

      iex> PrismaticAuth.CasbinAdapter.enforce("role:viewer", "/api/v1/admin/users", "write")
      :deny
  """
  @spec enforce(String.t(), String.t(), String.t()) :: evaluation_result()
  def enforce(subject, object, action) do
    policies = :ets.lookup(@ets_table, subject)

    deny_match =
      Enum.any?(policies, fn {_sub, obj, act, "deny"} ->
        matches_pattern?(object, obj) and matches_pattern?(action, act)
      end)

    if deny_match do
      :deny
    else
      allow_match =
        Enum.any?(policies, fn {_sub, obj, act, "allow"} ->
          matches_pattern?(object, obj) and matches_pattern?(action, act)
        end)

      if allow_match, do: :allow, else: :not_found
    end
  end

  defp matches_pattern?(value, pattern) do
    cond do
      pattern == "*" -> true
      String.ends_with?(pattern, "/*") ->
        prefix = String.trim_trailing(pattern, "/*")
        String.starts_with?(value, prefix)
      true -> value == pattern
    end
  end
end

Common Pitfalls

PitfallProblemSolution
Checking roles deep in business logicAuthorization scattered across codebase, hard to auditEnforce at boundary only: plugs, mount callbacks, channel joins
Hardcoded role checks (if user.role == "admin")Brittle, no hierarchy support, hard to modifyUse role hierarchy map with level comparison
Missing role check on LiveView eventsLiveView mount checks role, but subsequent events are unprotectedRe-validate role on sensitive events, not just mount
Atom creation from role stringsString.to_atom(role) causes atom table exhaustionUse String.to_existing_atom/1 or string comparison
No role assignment audit trailCannot determine who granted a role or whenLog all role grants/revocations with actor, timestamp, reason
Stale roles in long-lived JWT tokensUser role revoked but token still carries old role claimsToken refresh validates role currency; set short token TTL
Same roles for all environmentsDev/staging roles leak into productionEnvironment-specific role configurations
Over-privileged default rolesNew users get too many permissions by defaultDefault to viewer (least privilege); require explicit escalation
Agent tier bypass via direct GenServer callsAgents call GenServers directly, bypassing tier checksEnforce tier checks in GenServer handle_call guards
No separation between role and permissionRoles contain implicit permissions that are hard to enumerateDefine explicit permission sets per role; use Casbin for complex policies

Best Practices

  1. Principle of least privilege -- assign the minimum role necessary for each user or agent to perform their duties. Default new users to viewer and require explicit role escalation.

  2. Separate role definition from assignment -- roles should be defined once in configuration and assigned independently through a management interface. Never hardcode role assignments per user.

  3. Audit role assignments regularly -- stale role assignments create security risks. Implement quarterly role review and automatic expiration for temporary elevated roles.

  4. Use role hierarchies -- avoid permission duplication by having senior roles inherit from junior roles. An admin should automatically have all analyst capabilities without explicit configuration.

  5. Enforce at the boundary -- check roles at API entry points (plugs) and LiveView mounts, not deep in business logic. This makes the authorization surface auditable and testable.

  6. Log all authorization decisions -- both grants and denials should be logged with context (who, what, when, why). This supports security auditing and incident investigation.

  7. Use Casbin for complex policies -- when authorization depends on multiple factors (role + resource ownership + time + data classification), use a policy engine rather than custom code.

  8. Implement role-based rate limiting -- different roles may have different API rate limits. Analysts might get 100 req/min while admins get 1000 req/min.

  9. Test authorization at every level -- unit test the role hierarchy logic, integration test the plug enforcement, and E2E test the full authentication + authorization flow.

  10. Handle cross-session boundaries carefully -- sidebar links in LiveView must stay within the same live_session to avoid layout switching. Role checks at the router level must account for session boundaries.

  • RBAC -- the access control model that roles implement
  • Scope -- fine-grained permission boundaries within a role
  • Session -- the authenticated context carrying role information
  • Token -- the authentication artifact that carries role claims
  • User -- the entity to which roles are assigned
  • Casbin -- policy engine for complex authorization decisions
  • Authorization -- the broader concept of access control
  • Authentication -- identity verification that precedes role checks
  • Permission -- individual access rights grouped into roles
  • Policy -- declarative rules governing authorization decisions
  • Plug -- Phoenix middleware for request pipeline processing
  • Guardian -- Elixir authentication library for token management

See Also


Connect & Contribute

Created by Tomas Korcak (korczis) | Open Source under GHL