Per-tool authorization: scopes, capabilities, policy as code

Move fine-grained decisions out of scopes and into OPA, Cedar, or OpenFGA with the token claims as input.

⚖️ Module 6 8 min read Not started

Why this matters

A token saying "the agent may call any tool" is barely better than no auth. Real agent products check each tool call against fine-grained policy that considers the subject, actor, tenant, resource, action, time, context, and risk. This lesson turns that requirement into running code with OPA, Cedar, and OpenFGA — and shows how capability tokens reduce blast radius further.

Learning objectives

  1. Distinguish RBAC, ABAC, and ReBAC and pick per use case.
  2. Express policies in Rego (OPA) and Cedar.
  3. Model relationships in OpenFGA.
  4. Issue capability tokens for narrow, single-use authority.
  5. Wire it all into the broker / MCP server.

1. RBAC, ABAC, ReBAC

  • RBAC (Role-Based) — "users in role support_agent may read tickets". Simple; coarse; struggles with multi-tenant + per-resource ownership.
  • ABAC (Attribute-Based) — "if subject.department == resource.department and action == read and time < 18:00 then allow". Flexible; harder to audit.
  • ReBAC (Relationship-Based) — "user is editor of document; document is in folder; folder is owned by tenant". Models real-world ownership graphs. Google Zanzibar's heritage.

In practice you'll mix:

  • RBAC for org-wide roles (tenant_admin, support).
  • ABAC for context (time, IP, risk score, agent purpose).
  • ReBAC for resource ownership chains.

Authoring policy as code in OPA (Rego) or AWS Cedar + relationships in OpenFGA gives you a flexible, testable system.


2. Policy as code with OPA

Centralised decision point. Resource servers send a JSON input; OPA returns allow: true|false plus optional reasons.

rego
# policies/mcp/allow.rego
package mcp

default allow := false

# Read tickets: same tenant, has scope
allow {
    input.action == "tickets:read"
    input.subject.tenant == input.resource.tenant
    input.scope[_] == "tickets:read"
}

# Write tickets: above + agent has 'write' purpose declared
allow {
    input.action == "tickets:write"
    input.subject.tenant == input.resource.tenant
    input.scope[_] == "tickets:write"
    input.actor.purpose == "ticket_resolution"
}

# Refunds: require HITL + risk_score < 0.8 + binding token (RAR)
allow {
    input.action == "payments:refund"
    input.approval.granted == true
    input.risk_score < 0.8
    valid_rar(input.token.authorization_details, input.args)
}

valid_rar(details, args) {
    some d
    d := details[_]
    d.type == "payment_initiation"
    d.instructedAmount.amount == args.amount
}

Resource server:

python
def authorize(action: str, claims: dict, args: dict, resource: dict) -> bool:
    inp = {
        "action": action,
        "scope": claims.get("scope","").split(),
        "subject": {"sub": claims["sub"], "tenant": claims["tenant"]},
        "actor":   claims.get("act", {}),
        "resource": resource, "args": args,
        "approval": current_approval(claims),
        "risk_score": risk_score(claims, args),
        "token": claims,
    }
    r = httpx.post("http://opa:8181/v1/data/mcp/allow",
                   json={"input": inp}, timeout=0.2).json()
    return r.get("result", False)

OPA bundles + decision logs are first-class: ship policy as a signed bundle from CI, send each decision to a sink for audit.


3. AWS Cedar

Cedar is a more recent policy language (AWS, 2023) designed for application-level authz with formal analysis. Strongly typed; static reasoner can prove policies don't grant unintended access.

cedar
permit (
  principal,
  action == Action::"tickets:read",
  resource
) when {
  principal.tenant == resource.tenant &&
  context.token.scope.contains("tickets:read")
};

forbid (
  principal,
  action,
  resource
) unless {
  context.token.exp > context.now
};

The Cedar SDK runs in your app process (no network hop). Tradeoffs vs OPA:

  • Cedar: typed schema, formally analysable, single-process latency, smaller community.
  • OPA: untyped JSON, separate process, huge community, broader ecosystem (Kubernetes, Envoy, Terraform).

Either is fine for agent IAM; pick one and standardise.


4. OpenFGA — relationship-based access control

For "who can read this document?", encode relationships, not roles. Inspired by Google Zanzibar.

Schema:

fga
model
  schema 1.1

type user
type org
  relations
    define member: [user]
type folder
  relations
    define owner: [org]
    define editor: [user, org#member]
    define viewer: [user, org#member, folder#editor]
type doc
  relations
    define parent: [folder]
    define editor: [user, folder#editor]
    define viewer: [user, folder#viewer]

Write tuples (facts):

bash
fga tuple write user:alice editor doc:42
fga tuple write user:agent.helpdesk-v1 viewer doc:42  # the agent can read
fga tuple write org:acme member user:alice
fga tuple write folder:engineering owner org:acme
fga tuple write doc:42 parent folder:engineering

Check:

bash
fga query check user:agent.helpdesk-v1 viewer doc:42
# {"allowed": true}

Excellent for B2B SaaS agents where access is determined by org membership + sharing rules. Pair with OPA / Cedar for action-level (RBAC/ABAC) checks; OpenFGA handles the resource graph.


5. Capability tokens

A capability is a transferable, narrow permission to perform a specific action on a specific resource. Capability tokens are the operational form: short-lived, narrowly-scoped, single-purpose tokens issued just-in-time.

The pattern:

  1. Agent decides it needs to call tickets-api.update(ticket_id=42, status="closed").
  2. Agent runtime asks the capability service for a capability.
  3. Capability service checks policy (OPA), HITL approval if required, and mints a token:
    json
    {
      "iss": "https://cap.acme.com",
      "sub": "alice@acme.com",
      "act": { "sub": "agent.helpdesk-v1" },
      "aud": "tickets-api",
      "cap": {
        "resource": "ticket:42",
        "action": "update",
        "fields": ["status"],
        "expected_value": "closed"
      },
      "exp": "+60s",
      "jti": "cap_4f12...",
      "cnf": { "jkt": "<dpop key thumbprint>" }
    }
  4. Agent calls tickets-api with the capability token + DPoP.
  5. tickets-api validates and enforces: must match cap.resource, cap.action, cap.fields, cap.expected_value. Reject anything else.

Even a fully-compromised agent runtime that grabs the token can only do the one thing. Capability tokens are basically RAR (lesson 3.1) for internal APIs.

This is the highest leverage technique in agent IAM. Use it for any high-stakes action.


6. Composing policy + capability

Workflow per high-stakes tool call:

1. Agent: "I want to refund $200 to order #1234 for alice@acme.com"
2. Broker / capability service:
   a. OPA decision: subject=alice, actor=helpdesk-v1, action=refund, amount=200, time=now
      → "needs approval"
   b. Trigger CIBA (lesson 3.1) with binding_message:
      "Refund $200 to order #1234?"
   c. User approves on phone
   d. Mint capability token bound to (order=1234, amount=200, currency=USD), TTL 60s
3. Agent calls payments-api with the capability token
4. payments-api validates: cap matches args? token live? subject + actor OK?
   → execute, log, return

The agent never has the authority to refund any other amount or any other order. The user explicitly approved this one. Audit trail names both. This is what an EU AI Act high-risk system looks like in practice.


7. ABAC inputs: context the agent doesn't control

Some attributes should come from your infrastructure, not the agent:

  • IP / geo of the calling agent (workload identity tag).
  • Time of day + business hours.
  • Risk score (computed by a separate service from session signals, anomaly detectors).
  • Tenant compliance tier.
  • Per-action cost so far for cost-aware throttling.

Inject these in the policy input on the resource server, not from the agent. Anything the agent provides could be lied to it by injected content.


8. Testing policy

Treat policies like code:

rego
# tests/mcp_test.rego
package mcp

test_allow_tickets_read {
  allow with input as {
    "action": "tickets:read",
    "scope": ["tickets:read"],
    "subject": {"sub":"alice","tenant":"acme"},
    "resource": {"tenant":"acme"},
  }
}

test_deny_cross_tenant {
  not allow with input as {
    "action": "tickets:read",
    "scope": ["tickets:read"],
    "subject": {"sub":"alice","tenant":"acme"},
    "resource": {"tenant":"globex"},
  }
}
bash
opa test policies/ -v

CI runs opa test on every PR; failing tests block merge. Same for Cedar (cedar test) and OpenFGA assertions.

Coverage > 90% for any policy that protects regulated data.


9. Hands-on lab (4 h)

  1. Stand up OPA + OpenFGA (lesson 0.1).
  2. Write a Rego policy with at least 5 rules covering read, write, cross-tenant deny, business-hours restriction, and HITL gate.
  3. Model an OpenFGA schema for user, org, folder, doc; write tuples for two tenants; verify cross-tenant queries deny.
  4. Build a capability service (/cap) that:
    • Takes (action, args) from an agent.
    • Consults OPA.
    • If approval needed, triggers CIBA (mock if needed).
    • Returns a short-lived JWT bound to the agent's DPoP key.
  5. Build a payments-api that enforces capability tokens (validate cap matches request).
  6. Write opa test cases covering each policy branch; run in CI.

10. Common pitfalls

  1. Mixing ABAC and ReBAC in one policy language without clear lanes — pick OPA for action, OpenFGA for relationships.
  2. Trusting subject-supplied attributes (tenant, role) instead of resolving from claims server-side.
  3. Capability tokens too broad ("any refund up to $1000") — defeats the purpose.
  4. No tests for deny cases — only positive paths reviewed.
  5. Policy bundles unsigned — supply chain compromise rewrites policy silently.
  6. Decisions not logged → no post-hoc analysis.

11. Self-check

  1. RBAC vs ABAC vs ReBAC — pick per scenario.
  2. Capability token design — 3 must-have claims.
  3. OpenFGA in 2 sentences.
  4. Two attributes that must come from infra, not the agent.
  5. How CIBA + capability token + DPoP compose.

12. References

  • OPA + Rego docs.
  • AWS Cedar docs + Cedar by Example.
  • OpenFGA docs (openfga.dev).
  • Google "Zanzibar: Google's Consistent, Global Authorization System" (paper).
  • "OAuth 2.0 Rich Authorization Requests" (RFC 9396).
  • OpenID Foundation "AuthZEN" working group.
  • "Capability-based Security" — Mark Miller (Combex / E language).

Sign in to save your progress and earn badges.