Files
sentryagent-idp/policies/authz.rego
SentryAgent.ai Developer 03b5de300c feat(phase-3): workstream 4 — AGNTCY Federation
Implements cross-IdP token verification for the AGNTCY ecosystem:

- Migration 015: federation_partners table (issuer, jwks_uri,
  allowed_organizations JSONB, status, expires_at)
- FederationService: registerPartner (JWKS validation at registration),
  listPartners, getPartner, updatePartner, deletePartner,
  verifyFederatedToken (alg:none rejected, RS256/ES256 only,
  allowedOrganizations filter, expiry enforcement)
- JWKS caching in Redis (TTL: FEDERATION_JWKS_CACHE_TTL_SECONDS);
  cache invalidated on partner delete and jwks_uri change
- FederationController + routes: 5 admin:orgs endpoints +
  POST /federation/verify (agents:read)
- OPA policy: 5 federation admin endpoint → admin:orgs mappings
- 499 unit tests passing; 94.69% statement coverage on FederationService

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 10:13:49 +00:00

111 lines
3.6 KiB
Rego

package authz
import rego.v1
# ─── Data ─────────────────────────────────────────────────────────────────────
# data.endpoint_permissions is loaded from policies/data/scopes.json
# Structure: { "METHOD:/path/pattern": ["scope1", ...], ... }
# ─── Default ──────────────────────────────────────────────────────────────────
default allow := false
default reason := "insufficient_scope"
# ─── Path pattern normalisation ───────────────────────────────────────────────
# Converts a concrete request path to a pattern key by replacing UUID-like
# segments with named placeholders.
#
# Supported patterns (longest-match wins via iteration):
# /api/v1/agents/{uuid}/credentials/{uuid}/rotate
# /api/v1/agents/{uuid}/credentials/{uuid}
# /api/v1/agents/{uuid}/credentials
# /api/v1/agents/{uuid}
# /api/v1/agents
# /api/v1/token/introspect
# /api/v1/token/revoke
# /api/v1/audit/{uuid}
# /api/v1/audit
# Build the lookup key from method + normalised path.
lookup_key(method, path) := key if {
normalised := normalise_path(path)
key := concat(":", [method, normalised])
}
# Normalise a concrete path to its pattern form.
normalise_path(path) := "/api/v1/agents/:id/credentials/:credId/rotate" if {
regex.match(`^/api/v1/agents/[^/]+/credentials/[^/]+/rotate$`, path)
}
normalise_path(path) := "/api/v1/agents/:id/credentials/:credId" if {
regex.match(`^/api/v1/agents/[^/]+/credentials/[^/]+$`, path)
}
normalise_path(path) := "/api/v1/agents/:id/credentials" if {
regex.match(`^/api/v1/agents/[^/]+/credentials$`, path)
}
normalise_path(path) := "/api/v1/agents/:id" if {
regex.match(`^/api/v1/agents/[^/]+$`, path)
}
normalise_path(path) := "/api/v1/agents" if {
path == "/api/v1/agents"
}
normalise_path(path) := "/api/v1/token/introspect" if {
path == "/api/v1/token/introspect"
}
normalise_path(path) := "/api/v1/token/revoke" if {
path == "/api/v1/token/revoke"
}
normalise_path(path) := "/api/v1/audit/:id" if {
regex.match(`^/api/v1/audit/[^/]+$`, path)
}
normalise_path(path) := "/api/v1/audit" if {
path == "/api/v1/audit"
}
normalise_path(path) := "/api/v1/organizations/:id/members" if {
regex.match(`^/api/v1/organizations/[^/]+/members$`, path)
}
normalise_path(path) := "/api/v1/organizations/:id" if {
regex.match(`^/api/v1/organizations/[^/]+$`, path)
}
normalise_path(path) := "/api/v1/organizations" if {
path == "/api/v1/organizations"
}
normalise_path(path) := "/api/v1/federation/partners/:id" if {
regex.match(`^/api/v1/federation/partners/[^/]+$`, path)
}
normalise_path(path) := "/api/v1/federation/partners" if {
path == "/api/v1/federation/partners"
}
normalise_path(path) := "/api/v1/federation/trust" if {
path == "/api/v1/federation/trust"
}
# ─── Core allow rule ──────────────────────────────────────────────────────────
# allow = true if every required scope for the endpoint is present in input.scopes.
allow if {
key := lookup_key(input.method, input.path)
required := data.endpoint_permissions[key]
every req_scope in required {
req_scope in input.scopes
}
}
# reason is populated only on deny.
reason := "missing required scope for this endpoint" if {
not allow
}