{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://caipe.cisco.com/schemas/102/audit-event.schema.json",
  "title": "Authorization Decision Audit Event",
  "description": "One document per authorization decision, written to the MongoDB collection 'authz_decisions'. Emitted by both the TypeScript BFF (logAuthzDecision) and every Python service (log_authz_decision). Append-only; never updated, never deleted. Schema mirrors the existing TS audit log shape; this file is the canonical specification both runtimes target.",
  "$comment": "Spec 102 contract. Peers: rbac-matrix.schema.json (every matrix execution writes one event per persona × route), realm-config-extras.schema.json (events with reason='OK_ROLE_FALLBACK' come from PDP-unavailable fallback rules), python-rbac-helper.md (canonical contract for the Python emitter; require_rbac_permission writes one event per decision with source='py').",
  "type": "object",
  "required": ["userId", "resource", "scope", "allowed", "reason", "source", "service", "ts"],
  "additionalProperties": false,
  "properties": {
    "_id": {
      "description": "MongoDB ObjectId, auto-assigned. Not written by application code.",
      "oneOf": [{ "type": "string" }, { "type": "object" }]
    },
    "userId": {
      "type": "string",
      "minLength": 1,
      "description": "Keycloak 'sub' claim. Use the literal string 'anonymous' when no auth context is present."
    },
    "userEmail": {
      "type": "string",
      "description": "Best-effort copy of the email or preferred_username claim, for human-readable dashboards. NEVER used for authorization. Optional."
    },
    "resource": {
      "type": "string",
      "minLength": 1,
      "pattern": "^[a-z0-9_]+(:[A-Za-z0-9_-]+)?$",
      "description": "Keycloak resource name. May include a colon-suffixed instance id, e.g. 'dynamic_agent:my-agent'."
    },
    "scope": {
      "type": "string",
      "minLength": 1,
      "pattern": "^[a-z_]+$",
      "description": "Keycloak scope name, e.g. 'view', 'read', 'write', 'invoke', 'manage'."
    },
    "allowed": {
      "type": "boolean",
      "description": "Outcome of the decision."
    },
    "reason": {
      "type": "string",
      "enum": [
        "OK",
        "OK_ROLE_FALLBACK",
        "OK_BOOTSTRAP_ADMIN",
        "DENY_NO_CAPABILITY",
        "DENY_PDP_UNAVAILABLE",
        "DENY_INVALID_TOKEN",
        "DENY_RESOURCE_UNKNOWN"
      ],
      "description": "Closed enum so dashboards can group reliably. Add new codes by amending both this schema and the TS/Py emitters in lockstep."
    },
    "source": {
      "type": "string",
      "enum": ["ts", "py"],
      "description": "Which runtime wrote the event. Lets investigators distinguish polyglot drift."
    },
    "service": {
      "type": "string",
      "minLength": 1,
      "description": "Service identifier. One of: ui, supervisor, dynamic_agents, rag_server, slack_bot, or '<agent>_mcp' (e.g. 'argocd_mcp')."
    },
    "route": {
      "type": "string",
      "description": "HTTP route or RPC method that triggered the decision. Optional. Examples: 'GET /api/admin/users', 'tools/call argocd.list_apps'."
    },
    "requestId": {
      "type": "string",
      "description": "Correlation id from inbound request, if present. Optional."
    },
    "pdp": {
      "type": "string",
      "enum": ["keycloak", "local", "cache"],
      "description": "Which evaluator produced the decision. 'keycloak' = live PDP call; 'local' = role fallback; 'cache' = decision cache hit. Optional but recommended."
    },
    "ts": {
      "oneOf": [
        { "type": "string", "format": "date-time" },
        { "type": "object" }
      ],
      "description": "Server time of decision. ISO-8601 string when serialized; BSON Date when stored in Mongo."
    }
  },
  "examples": [
    {
      "userId": "alice-keycloak-sub-uuid",
      "userEmail": "alice@example.com",
      "resource": "admin_ui",
      "scope": "view",
      "allowed": true,
      "reason": "OK",
      "source": "ts",
      "service": "ui",
      "route": "GET /api/admin/users",
      "requestId": "req-7f3b2c",
      "pdp": "keycloak",
      "ts": "2026-04-22T18:15:32.451Z"
    },
    {
      "userId": "bob-keycloak-sub-uuid",
      "userEmail": "bob@example.com",
      "resource": "argocd_mcp",
      "scope": "write",
      "allowed": false,
      "reason": "DENY_NO_CAPABILITY",
      "source": "py",
      "service": "argocd_mcp",
      "route": "tools/call argocd.delete_app",
      "requestId": "req-9d1e8a",
      "pdp": "keycloak",
      "ts": "2026-04-22T18:16:01.108Z"
    },
    {
      "userId": "alice-keycloak-sub-uuid",
      "userEmail": "alice@example.com",
      "resource": "admin_ui",
      "scope": "view",
      "allowed": true,
      "reason": "OK_ROLE_FALLBACK",
      "source": "ts",
      "service": "ui",
      "route": "GET /api/admin/users/stats",
      "pdp": "local",
      "ts": "2026-04-22T18:17:45.000Z"
    }
  ]
}
