Definition
Authorization is the process of determining whether an authenticated entity has permission to perform a specific action on a specific resource. While authentication answers "Who are you?", authorization answers "Are you allowed to do this?" It is the second critical gate in the security pipeline: after identity is established, the authorization system evaluates the entity's roles, permissions, scopes, and contextual attributes against the access requirements of the requested operation.
In the Prismatic Platform, authorization operates at two distinct levels: human authorization through RBAC (role-based access control) for users and API consumers, and agent authorization through authority levels for AIAD agents. Both systems share the same architectural principles -- explicit permission checks, deny-by-default, and immutable audit trails -- but differ in their policy evaluation mechanisms.
Overview
Authorization is the enforcement layer that translates security policy into runtime behavior. A well-designed authorization system ensures that:
- Principle of least privilege: Every entity can access only what it needs, nothing more
- Separation of duties: Critical operations require multiple authorized parties
- Deny by default: Any action not explicitly permitted is denied
- Auditability: Every authorization decision is logged with full context
The Prismatic Platform's authorization architecture processes thousands of authorization decisions per second across its 115-application umbrella. Performance is critical: authorization checks must complete in microseconds to meet the platform's 250ms page load target. This is achieved through ETS-cached policy evaluation, compile-time permission resolution where possible, and scope-based short-circuit evaluation.
Authorization Decision Flow
┌─────────────┐ ┌──────────────┐ ┌──────────────┐ ┌─────────┐
│ Authenticated│────>│ Extract │────>│ Evaluate │────>│ Grant / │
│ Request │ │ Identity + │ │ Policy │ │ Deny │
│ │ │ Requested │ │ │ │ │
│ │ │ Action │ │ - RBAC roles │ │ │
│ │ │ │ │ - Scopes │ │ │
│ │ │ │ │ - Context │ │ │
└─────────────┘ └──────────────┘ └──────────────┘ └─────────┘
│
┌─────▼─────┐
│ Audit Log │
└───────────┘Technical Details
Authorization Models
The platform supports multiple authorization models, selected based on the granularity and dynamism requirements of each subsystem:
| Model | Description | Granularity | Performance | Prismatic Usage |
|---|---|---|---|---|
| RBAC | Role-based access control | Role-level | Fast (ETS lookup) | Primary for human users |
| Scope-Based | OAuth2-style scoped permissions | Action-level | Fast (set membership) | API consumers |
| Authority Level | L1-L5 hierarchical capabilities | Level + capability | Very fast (integer comparison) | AIAD agents |
| Policy-Based | Rule engine evaluation | Arbitrary | Medium (rule evaluation) | Complex cross-domain decisions |
| ABAC | Attribute-based access control | Attribute-level | Slow (attribute evaluation) | Future consideration |
RBAC Implementation
defmodule Prismatic.Authorization.RBAC do
@moduledoc """
Role-based access control implementation for the Prismatic Platform.
Roles define sets of permissions. Users are assigned roles.
Authorization checks verify that the user's roles include
the permission required for the requested action.
Roles are hierarchical: an admin role includes all permissions
of the viewer role plus additional admin-specific permissions.
"""
@type role :: :admin | :operator | :analyst | :viewer | :api_consumer
@type permission :: {atom(), atom()} # {resource, action}
@type identity :: %{roles: [role()], scopes: [String.t()]}
@role_hierarchy %{
admin: [:operator, :analyst, :viewer],
operator: [:analyst, :viewer],
analyst: [:viewer],
viewer: [],
api_consumer: []
}
@role_permissions %{
admin: [
{:users, :create}, {:users, :read}, {:users, :update}, {:users, :delete},
{:agents, :create}, {:agents, :read}, {:agents, :update}, {:agents, :delete},
{:config, :read}, {:config, :update},
{:audit, :read}, {:audit, :export},
{:perimeter, :read}, {:perimeter, :scan}, {:perimeter, :configure}
],
operator: [
{:agents, :read}, {:agents, :update},
{:config, :read},
{:perimeter, :read}, {:perimeter, :scan},
{:deployments, :create}, {:deployments, :read}
],
analyst: [
{:agents, :read},
{:perimeter, :read},
{:reports, :create}, {:reports, :read},
{:osint, :read}, {:osint, :search}
],
viewer: [
{:agents, :read},
{:perimeter, :read},
{:reports, :read},
{:dashboard, :read}
],
api_consumer: [] # Permissions determined by scopes
}
@spec authorize(identity(), atom(), atom()) ::
{:ok, :authorized} | {:error, :forbidden, String.t()}
def authorize(identity, resource, action) do
required = {resource, action}
effective_permissions = effective_permissions(identity)
if required in effective_permissions do
log_authorization(:granted, identity, resource, action)
{:ok, :authorized}
else
reason = "No role grants #{}:#{}"
log_authorization(:denied, identity, resource, action, reason)
{:error, :forbidden, reason}
end
end
@spec effective_permissions(identity()) :: MapSet.t(permission())
def effective_permissions(%{roles: roles, scopes: scopes}) do
role_perms =
roles
|> Enum.flat_map(fn role ->
direct = Map.get(@role_permissions, role, [])
inherited = inherited_permissions(role)
direct ++ inherited
end)
|> MapSet.new()
scope_perms =
scopes
|> Enum.flat_map(&scope_to_permissions/1)
|> MapSet.new()
MapSet.union(role_perms, scope_perms)
end
@spec inherited_permissions(role()) :: [permission()]
defp inherited_permissions(role) do
@role_hierarchy
|> Map.get(role, [])
|> Enum.flat_map(fn inherited_role ->
Map.get(@role_permissions, inherited_role, []) ++
inherited_permissions(inherited_role)
end)
end
@spec scope_to_permissions(String.t()) :: [permission()]
defp scope_to_permissions("perimeter:read"), do: [{:perimeter, :read}]
defp scope_to_permissions("perimeter:scan"), do: [{:perimeter, :read}, {:perimeter, :scan}]
defp scope_to_permissions("osint:search"), do: [{:osint, :read}, {:osint, :search}]
defp scope_to_permissions("agents:read"), do: [{:agents, :read}]
defp scope_to_permissions(_scope), do: []
endAuthorization Plug Pipeline
defmodule PrismaticWeb.Plugs.Authorization do
@moduledoc """
Plug-based authorization enforcement for Phoenix routes.
Must be placed AFTER the Authentication plug in the pipeline.
Reads the authenticated identity from conn.assigns and evaluates
the required permission for the current route.
## Usage
plug PrismaticWeb.Plugs.Authorization,
resource: :perimeter,
action: :scan
## Scope-Based (API routes)
plug PrismaticWeb.Plugs.Authorization,
scope: "perimeter:scan"
"""
import Plug.Conn
alias Prismatic.Authorization.RBAC
alias Prismatic.Authorization.ScopeResolver
@behaviour Plug
@spec init(keyword()) :: keyword()
@impl true
def init(opts), do: opts
@spec call(Plug.Conn.t(), keyword()) :: Plug.Conn.t()
@impl true
def call(conn, opts) do
identity = conn.assigns[:current_identity]
case identity do
nil ->
conn
|> put_status(:unauthorized)
|> Phoenix.Controller.json(%{error: "authentication_required"})
|> halt()
identity ->
evaluate_authorization(conn, identity, opts)
end
end
@spec evaluate_authorization(Plug.Conn.t(), map(), keyword()) :: Plug.Conn.t()
defp evaluate_authorization(conn, identity, opts) do
result =
cond do
Keyword.has_key?(opts, :scope) ->
scope = Keyword.fetch!(opts, :scope)
ScopeResolver.check_scope(identity, scope)
Keyword.has_key?(opts, :resource) ->
resource = Keyword.fetch!(opts, :resource)
action = Keyword.fetch!(opts, :action)
RBAC.authorize(identity, resource, action)
true ->
{:error, :forbidden, "No authorization requirement specified"}
end
case result do
{:ok, :authorized} ->
conn
{:error, :forbidden, reason} ->
conn
|> put_status(:forbidden)
|> Phoenix.Controller.json(%{
error: "forbidden",
message: "Insufficient permissions",
detail: reason
})
|> halt()
end
end
endPolicy Engine for Complex Authorization
defmodule Prismatic.Authorization.PolicyEngine do
@moduledoc """
Rule-based policy engine for complex authorization decisions
that cannot be expressed through simple RBAC or scope checks.
Policies are defined as Elixir modules implementing the
`Policy` behaviour. Each policy evaluates a context and
returns an explicit allow/deny decision with rationale.
## Example Policy
defmodule Prismatic.Policies.CrossDomainAccess do
@behaviour Prismatic.Authorization.PolicyEngine.Policy
@impl true
def evaluate(%{requester_domain: req, target_domain: target}) do
if req == target do
{:allow, "Same domain access"}
else
{:deny, "Cross-domain access requires L3+ authority"}
end
end
end
"""
@type decision :: {:allow, String.t()} | {:deny, String.t()}
@type context :: map()
@callback evaluate(context()) :: decision()
@spec evaluate_policies([module()], context()) ::
{:ok, :authorized, [String.t()]} | {:error, :forbidden, [String.t()]}
def evaluate_policies(policies, context) do
results = Enum.map(policies, fn policy -> policy.evaluate(context) end)
denials = for {:deny, reason} <- results, do: reason
case denials do
[] ->
approvals = for {:allow, reason} <- results, do: reason
{:ok, :authorized, approvals}
reasons ->
{:error, :forbidden, reasons}
end
end
endAgent Authorization
AIAD agents use a distinct authorization mechanism based on authority levels:
defmodule Prismatic.AIAD.AgentAuthorization do
@moduledoc """
Authorization module for AIAD agents. Unlike human RBAC,
agent authorization is based on authority levels (L1-L5)
and domain scope assignments.
Agent authorization checks are ultra-fast (ETS lookup +
integer comparison) because they execute on every agent
operation in the 530-agent ecosystem.
"""
alias Prismatic.AIAD.{AuthorityLevel, AuthorityGuard}
@type agent_context :: %{
agent_id: String.t(),
authority_level: AuthorityLevel.t(),
domain: atom(),
requested_capability: atom(),
target_domain: atom() | nil
}
@spec authorize_agent_operation(agent_context()) ::
{:ok, :authorized} | {:error, :forbidden, String.t()}
def authorize_agent_operation(context) do
with :ok <- check_authority_level(context),
:ok <- check_domain_scope(context),
:ok <- check_capability(context) do
{:ok, :authorized}
else
{:error, reason} -> {:error, :forbidden, reason}
end
end
@spec check_authority_level(agent_context()) :: :ok | {:error, String.t()}
defp check_authority_level(%{authority_level: level, requested_capability: cap}) do
min_level = AuthorityLevel.minimum_for_capability(cap)
if AuthorityLevel.compare(level, min_level) in [:eq, :gt] do
:ok
else
{:error, "#{} insufficient for #{} (requires #{})"}
end
end
@spec check_domain_scope(agent_context()) :: :ok | {:error, String.t()}
defp check_domain_scope(%{domain: domain, target_domain: nil}), do: :ok
defp check_domain_scope(%{domain: domain, target_domain: domain}), do: :ok
defp check_domain_scope(%{authority_level: level} = ctx) do
if AuthorityLevel.numeric_value(level) >= 4 do
:ok
else
{:error, "Agent in #{.} cannot access #{.} without L4+ authority"}
end
end
@spec check_capability(agent_context()) :: :ok | {:error, String.t()}
defp check_capability(%{agent_id: agent_id, requested_capability: cap}) do
case AuthorityGuard.authorize(agent_id, cap) do
{:ok, :authorized} -> :ok
{:error, :unauthorized, reason} -> {:error, reason}
end
end
endAuthorization Telemetry
| Event | Measurements | Metadata |
|---|---|---|
[:prismatic, :authz, :check] | %{duration: integer} | %{model: atom, result: atom} |
[:prismatic, :authz, :rbac, :granted] | %{count: 1} | %{role: atom, resource: atom, action: atom} |
[:prismatic, :authz, :rbac, :denied] | %{count: 1} | %{role: atom, resource: atom, action: atom, reason: string} |
[:prismatic, :authz, :agent, :granted] | %{count: 1} | %{agent_id: string, capability: atom, level: atom} |
[:prismatic, :authz, :agent, :denied] | %{count: 1} | %{agent_id: string, capability: atom, reason: string} |
[:prismatic, :authz, :policy, :evaluated] | %{duration: integer} | %{policies: integer, result: atom} |
Implementation in Prismatic Platform
Router Integration
Authorization plugs are applied at the router scope level, ensuring every route has explicit access requirements:
scope "/api/v1/perimeter", PrismaticAPI.Perimeter do
pipe_through [:api, :api_auth]
# Read-only endpoints require viewer-level access
get "/assets", AssetController, :index
get "/ratings", RatingController, :index
# Scan operations require operator-level access
scope "/" do
pipe_through [PrismaticWeb.Plugs.Authorization, resource: :perimeter, action: :scan]
post "/scan", ScanController, :create
end
# Configuration requires admin access
scope "/config" do
pipe_through [PrismaticWeb.Plugs.Authorization, resource: :perimeter, action: :configure]
put "/", ConfigController, :update
end
endLiveView Authorization
LiveView pages enforce authorization in the mount/3 callback:
defmodule PrismaticWeb.PerimeterLive do
use PrismaticWeb, :live_view
alias Prismatic.Authorization.RBAC
@impl true
def mount(_params, session, socket) do
identity = session["current_identity"]
case RBAC.authorize(identity, :perimeter, :read) do
{:ok, :authorized} ->
{:ok, assign(socket, :identity, identity)}
{:error, :forbidden, _reason} ->
{:ok, socket |> put_flash(:error, "Access denied") |> redirect(to: "/")}
end
end
endComparison with Alternatives
| Approach | Flexibility | Performance | Auditability | Complexity | When to Use |
|---|---|---|---|---|---|
| RBAC (current) | Moderate | Excellent | Good | Low | Known role structures |
| ABAC | Very High | Poor | Excellent | Very High | Complex attribute conditions |
| ACL | Low | Excellent | Good | Low | Simple resource lists |
| ReBAC | High | Good | Good | High | Relationship-heavy domains |
| Policy-as-Code (OPA) | Very High | Good | Excellent | High | Multi-service policies |
| Capability-Based | High | Excellent | Moderate | Moderate | Distributed systems |
The Prismatic Platform uses RBAC for human users because roles map naturally to organizational structure. Agent authorization uses the simpler authority level model because agent capabilities are more uniform and hierarchical.
Best Practices
-
Deny by default: Every authorization check must return
:forbiddenunless an explicit allow rule matches. Never implement authorization as a blocklist. -
Separate authentication from authorization: These are distinct concerns with distinct modules. The authentication plug establishes identity; the authorization plug checks permissions. Never conflate them.
-
Cache permission lookups: Role-to-permission mappings change infrequently. Cache the effective permission set in ETS after first computation to avoid repeated traversal of role hierarchies.
-
Test denied cases as thoroughly as allowed cases: Authorization tests should verify that unauthorized entities receive
:forbiddenresponses. Testing only the happy path leaves authorization bypasses undetected. -
Log all authorization denials: Denied authorization attempts are security-relevant events. Log them with full context (identity, resource, action, reason) for incident investigation.
-
Use the most specific authorization model: RBAC for role-based decisions, scopes for API consumers, authority levels for agents. Mixing models unnecessarily adds complexity.
-
Avoid inline authorization checks: Authorization logic should live in dedicated modules (plugs, policy engines), not scattered across controller actions. Inline checks are easy to forget.
Common Pitfalls
-
Authorization bypass via direct database access: If a service bypasses the authorization layer and queries the database directly, all permission checks are meaningless. Enforce authorization at the service boundary.
-
Inconsistent authorization across channels: A resource that requires admin access via the REST API but is unrestricted via LiveView WebSocket is a vulnerability. Apply consistent authorization across all access channels.
-
Privilege escalation through role assignment: If a user can assign roles to themselves, they can escalate to admin. Role assignment must itself be an authorized operation requiring higher privilege.
-
Stale permission cache: When roles or permissions change, cached effective permissions must be invalidated. Serving stale permissions after a role revocation is a security breach window.
-
Overly broad scopes: API scopes like
admin:alldefeat the purpose of scoped authorization. Define narrow, action-specific scopes (perimeter:read,osint:search). -
Missing authorization on mutation operations: Read operations are often well-protected, but mutation operations (create, update, delete) are sometimes left unguarded. Every state-changing operation needs explicit authorization.
Use Cases
API Gateway Authorization
External API consumers receive OAuth2 access tokens with specific scopes. The authorization layer verifies that the token's scopes include the permission required for each endpoint, enabling fine-grained API access control without full RBAC.
Multi-Tenant Resource Isolation
In multi-tenant deployments, authorization ensures that Tenant A cannot access Tenant B's resources even with valid authentication. The policy engine evaluates tenant membership as an additional authorization constraint beyond role checks.
AIAD Agent Domain Boundaries
The 530+ AIAD agents are authorized within their assigned domains. A security agent cannot modify deployment configurations; a documentation agent cannot trigger security scans. Domain boundary enforcement prevents capability creep across the agent ecosystem.
Compliance-Driven Access Control
For NIS2 and ZKB compliance (Prismatic Perimeter), authorization policies enforce that only certified operators can initiate compliance assessments and that assessment results are accessible only to authorized reviewers.
Graduated Access for New Users
New users start with viewer role (read-only dashboard access). As they complete onboarding and verification, their role is upgraded through analyst to operator, gradually expanding their authorized operations.
Related Concepts
- Authentication -- identity verification that precedes authorization
- RBAC -- role-based access control, the primary human authorization model
- Authority Level -- hierarchical agent authorization (L1-L5)
- Privilege Escalation -- unauthorized authority increase
- Policy -- governance rules evaluated during authorization
- Security Operations -- monitoring authorization events for threats
- Plug -- composable pipeline where authorization checks are enforced
- AIAD -- agent framework with built-in authorization model
- Agent Tier -- functional classification informing authorization scope
- Credential Management -- managing credentials that carry authorization claims
See Also
- NIST SP 800-162: Guide to ABAC -- attribute-based access control standard
- OWASP Authorization Cheat Sheet -- security best practices
- Phoenix Security Guide -- framework-level security documentation
- Glossary Index -- complete glossary of Prismatic Platform terminology
Connect & Contribute
Created by Tomas Korcak (korczis) | Open Source under GHL