openapi: "3.1.0" info: title: MCIAS Authentication API version: "1.0" description: | MCIAS (Metacircular Identity and Access System) provides JWT-based authentication, account management, TOTP, and Postgres credential storage. All tokens are Ed25519-signed JWTs (algorithm `EdDSA`). Bearer tokens must be sent in the `Authorization` header as `Bearer `. Rate limiting applies to `/v1/auth/login` and `/v1/token/validate`: 10 requests per second per IP, burst of 10. servers: - url: https://auth.example.com:8443 description: Production components: securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: JWT schemas: Error: type: object required: [error, code] properties: error: type: string description: Human-readable error message. example: invalid credentials code: type: string description: Machine-readable error code. example: unauthorized TokenResponse: type: object required: [token, expires_at] properties: token: type: string description: Ed25519-signed JWT (EdDSA). example: eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9... expires_at: type: string format: date-time description: Token expiry in RFC 3339 format. example: "2026-04-10T12:34:56Z" Account: type: object required: [id, username, account_type, status, created_at, updated_at, totp_enabled] properties: id: type: string format: uuid description: Account UUID (use this in all API calls). example: 550e8400-e29b-41d4-a716-446655440000 username: type: string example: alice account_type: type: string enum: [human, system] example: human status: type: string enum: [active, inactive, deleted] example: active created_at: type: string format: date-time example: "2026-03-11T09:00:00Z" updated_at: type: string format: date-time example: "2026-03-11T09:00:00Z" totp_enabled: type: boolean description: Whether TOTP is enrolled and required for this account. example: false AuditEvent: type: object required: [id, event_type, event_time, ip_address] properties: id: type: integer example: 42 event_type: type: string example: login_ok event_time: type: string format: date-time example: "2026-03-11T09:01:23Z" actor_id: type: string format: uuid nullable: true description: UUID of the account that performed the action. Null for bootstrap events. example: 550e8400-e29b-41d4-a716-446655440000 target_id: type: string format: uuid nullable: true description: UUID of the affected account, if applicable. ip_address: type: string example: "192.0.2.1" details: type: string description: JSON blob with event-specific metadata. Never contains credentials. example: '{"jti":"f47ac10b-..."}' TagsResponse: type: object required: [tags] properties: tags: type: array items: type: string description: Current tag list for the account. example: ["env:production", "svc:payments-api"] RuleBody: type: object required: [effect] description: | The match conditions and effect of a policy rule. All fields except `effect` are optional; an omitted field acts as a wildcard. properties: effect: type: string enum: [allow, deny] example: allow roles: type: array items: type: string description: Subject must have at least one of these roles. example: ["svc:payments-api"] account_types: type: array items: type: string enum: [human, system] description: Subject account type must be one of these. example: ["system"] subject_uuid: type: string format: uuid description: Match only this specific subject UUID. example: 550e8400-e29b-41d4-a716-446655440000 actions: type: array items: type: string description: | One of the defined action constants, e.g. `pgcreds:read`, `accounts:list`. Subject action must be in this list. example: ["pgcreds:read"] resource_type: type: string description: Resource type the rule applies to. example: pgcreds owner_matches_subject: type: boolean description: Resource owner UUID must equal the subject UUID. example: true service_names: type: array items: type: string description: Resource service name must be one of these. example: ["payments-api"] required_tags: type: array items: type: string description: Resource must have ALL of these tags. example: ["env:staging"] PolicyRule: type: object required: [id, priority, description, rule, enabled, created_at, updated_at] properties: id: type: integer example: 1 priority: type: integer description: Lower number = evaluated first. example: 100 description: type: string example: Allow payments-api to read its own pgcreds rule: $ref: "#/components/schemas/RuleBody" enabled: type: boolean example: true not_before: type: string format: date-time nullable: true description: | Earliest time the rule becomes active. NULL means no constraint (always active). Rules where `not_before > now()` are skipped during evaluation. example: "2026-04-01T00:00:00Z" expires_at: type: string format: date-time nullable: true description: | Time after which the rule is no longer active. NULL means no constraint (never expires). Rules where `expires_at <= now()` are skipped during evaluation. example: "2026-06-01T00:00:00Z" created_at: type: string format: date-time example: "2026-03-11T09:00:00Z" updated_at: type: string format: date-time example: "2026-03-11T09:00:00Z" PGCreds: type: object required: [host, port, database, username, password] properties: host: type: string example: db.example.com port: type: integer example: 5432 database: type: string example: mydb username: type: string example: myuser password: type: string description: > Plaintext password (sent over TLS, stored encrypted at rest with AES-256-GCM). Only returned to admin callers. example: hunter2 responses: Unauthorized: description: Token missing, invalid, expired, or credentials incorrect. content: application/json: schema: $ref: "#/components/schemas/Error" example: error: invalid credentials code: unauthorized Forbidden: description: Token valid but lacks the required role. content: application/json: schema: $ref: "#/components/schemas/Error" example: error: forbidden code: forbidden NotFound: description: Requested resource does not exist. content: application/json: schema: $ref: "#/components/schemas/Error" example: error: account not found code: not_found BadRequest: description: Malformed request or missing required fields. content: application/json: schema: $ref: "#/components/schemas/Error" example: error: username and password are required code: bad_request RateLimited: description: Rate limit exceeded. content: application/json: schema: $ref: "#/components/schemas/Error" example: error: rate limit exceeded code: rate_limited paths: # ── Public ──────────────────────────────────────────────────────────────── /v1/health: get: summary: Health check description: Returns `{"status":"ok"}` if the server is running. No auth required. operationId: getHealth tags: [Public] responses: "200": description: Server is healthy. content: application/json: schema: type: object properties: status: type: string example: ok /v1/keys/public: get: summary: Ed25519 public key (JWK) description: | Returns the server's Ed25519 public key in JWK format (RFC 8037). Relying parties use this to verify JWT signatures offline. Cache this key at startup. Refresh it if signature verification begins failing (indicates key rotation). **Important:** Always validate the `alg` header of the JWT (`EdDSA`) before calling the signature verification routine. Never accept `none`. operationId: getPublicKey tags: [Public] responses: "200": description: Ed25519 public key in JWK format. content: application/json: schema: type: object required: [kty, crv, use, alg, x] properties: kty: type: string example: OKP crv: type: string example: Ed25519 use: type: string example: sig alg: type: string example: EdDSA x: type: string description: Base64url-encoded public key bytes. example: 11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo /v1/auth/login: post: summary: Login description: | Authenticate with username + password and optionally a TOTP code. Returns an Ed25519-signed JWT. Rate limited to 10 requests per second per IP (burst 10). Error responses always use the generic message `"invalid credentials"` regardless of whether the user exists, the password is wrong, or the account is inactive. This prevents user enumeration. If the account has TOTP enrolled, `totp_code` is required. Omitting it returns HTTP 401 with code `totp_required`. operationId: login tags: [Public] requestBody: required: true content: application/json: schema: type: object required: [username, password] properties: username: type: string example: alice password: type: string example: s3cr3t totp_code: type: string description: Current 6-digit TOTP code. Required if TOTP is enrolled. example: "123456" responses: "200": description: Login successful. Returns JWT and expiry. content: application/json: schema: $ref: "#/components/schemas/TokenResponse" "400": $ref: "#/components/responses/BadRequest" "401": description: Invalid credentials, inactive account, or missing TOTP code. content: application/json: schema: $ref: "#/components/schemas/Error" examples: invalid_credentials: value: {error: invalid credentials, code: unauthorized} totp_required: value: {error: TOTP code required, code: totp_required} "429": $ref: "#/components/responses/RateLimited" /v1/token/validate: post: summary: Validate a JWT description: | Validate a JWT and return its claims. Reflects revocations immediately (online validation). Use this for high-security paths where offline verification is insufficient. The token may be supplied either as a Bearer header or in the JSON body. **Always inspect the `valid` field.** The response is always HTTP 200; do not branch on the status code. Rate limited to 10 requests per second per IP (burst 10). operationId: validateToken tags: [Public] security: - bearerAuth: [] - {} requestBody: description: Optionally supply the token in the body instead of the header. required: false content: application/json: schema: type: object properties: token: type: string example: eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9... responses: "200": description: Validation result. Always HTTP 200; check `valid`. content: application/json: schema: type: object required: [valid] properties: valid: type: boolean sub: type: string format: uuid description: Subject (account UUID). Present when valid=true. example: 550e8400-e29b-41d4-a716-446655440000 roles: type: array items: type: string description: Role list. Present when valid=true. example: [editor] expires_at: type: string format: date-time description: Expiry. Present when valid=true. example: "2026-04-10T12:34:56Z" examples: valid: value: {valid: true, sub: "550e8400-...", roles: [editor], expires_at: "2026-04-10T12:34:56Z"} invalid: value: {valid: false} "429": $ref: "#/components/responses/RateLimited" # ── Authenticated ────────────────────────────────────────────────────────── /v1/auth/logout: post: summary: Logout description: | Revoke the current bearer token immediately. The JTI is recorded in the revocation table; subsequent validation calls will return `valid=false`. operationId: logout tags: [Auth] security: - bearerAuth: [] responses: "204": description: Token revoked. "401": $ref: "#/components/responses/Unauthorized" /v1/auth/renew: post: summary: Renew token description: | Exchange the current token for a fresh one. The old token is revoked. The new token reflects any role changes made since the original login. Token expiry is recalculated: 30 days for regular users, 8 hours for admins. operationId: renewToken tags: [Auth] security: - bearerAuth: [] responses: "200": description: New token issued. Old token revoked. content: application/json: schema: $ref: "#/components/schemas/TokenResponse" "401": $ref: "#/components/responses/Unauthorized" /v1/auth/totp/enroll: post: summary: Begin TOTP enrollment description: | Generate a TOTP secret for the authenticated account and return it as a bare secret and as an `otpauth://` URI (scan with any authenticator app). The secret is shown **once**. It is stored encrypted at rest and is not retrievable after this call. TOTP is not required until the enrollment is confirmed via `POST /v1/auth/totp/confirm`. Abandoning after this call does not lock the account. operationId: enrollTOTP tags: [Auth] security: - bearerAuth: [] responses: "200": description: TOTP secret generated. content: application/json: schema: type: object required: [secret, otpauth_uri] properties: secret: type: string description: Base32-encoded TOTP secret. Store in an authenticator app. example: JBSWY3DPEHPK3PXP otpauth_uri: type: string description: Standard otpauth URI for QR-code generation. example: "otpauth://totp/MCIAS:alice?secret=JBSWY3DPEHPK3PXP&issuer=MCIAS" "401": $ref: "#/components/responses/Unauthorized" /v1/auth/totp/confirm: post: summary: Confirm TOTP enrollment description: | Verify the provided TOTP code against the pending secret. On success, TOTP becomes required for all future logins for this account. operationId: confirmTOTP tags: [Auth] security: - bearerAuth: [] requestBody: required: true content: application/json: schema: type: object required: [code] properties: code: type: string description: Current 6-digit TOTP code. example: "123456" responses: "204": description: TOTP confirmed. Required for future logins. "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" /v1/auth/password: put: summary: Change own password (self-service) description: | Change the password of the currently authenticated human account. The caller must supply the correct `current_password` to prevent token-theft attacks: possession of a valid JWT alone is not sufficient. On success: - The stored Argon2id hash is replaced with the new password hash. - All active sessions *except* the caller's current token are revoked. - The lockout failure counter is cleared. On failure (wrong current password): - A login failure is recorded against the account, subject to the same lockout rules as `POST /v1/auth/login`. Only applies to human accounts. System accounts have no password. operationId: changePassword tags: [Auth] security: - bearerAuth: [] requestBody: required: true content: application/json: schema: type: object required: [current_password, new_password] properties: current_password: type: string description: The account's current password (required for verification). example: old-s3cr3t new_password: type: string description: The new password. Minimum 12 characters. example: new-s3cr3t-long responses: "204": description: Password changed. Other active sessions revoked. "400": $ref: "#/components/responses/BadRequest" "401": description: Current password is incorrect. content: application/json: schema: $ref: "#/components/schemas/Error" example: error: current password is incorrect code: unauthorized "429": description: Account temporarily locked due to too many failed attempts. content: application/json: schema: $ref: "#/components/schemas/Error" example: error: account temporarily locked code: account_locked # ── Admin ────────────────────────────────────────────────────────────────── /v1/auth/totp: delete: summary: Remove TOTP from account (admin) description: | Clear TOTP enrollment for an account. Use for account recovery when a user loses their TOTP device. The account can log in with password only after this call. operationId: removeTOTP tags: [Admin — Auth] security: - bearerAuth: [] requestBody: required: true content: application/json: schema: type: object required: [account_id] properties: account_id: type: string format: uuid example: 550e8400-e29b-41d4-a716-446655440000 responses: "204": description: TOTP removed. "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" /v1/token/issue: post: summary: Issue service account token (admin) description: | Issue a long-lived bearer token for a system account. If the account already has an active token, it is revoked and replaced. Only one active token exists per system account at a time. Issued tokens expire after 1 year (configurable via `tokens.service_expiry`). operationId: issueServiceToken tags: [Admin — Tokens] security: - bearerAuth: [] requestBody: required: true content: application/json: schema: type: object required: [account_id] properties: account_id: type: string format: uuid example: 550e8400-e29b-41d4-a716-446655440000 responses: "200": description: Token issued. content: application/json: schema: $ref: "#/components/schemas/TokenResponse" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" /v1/token/{jti}: delete: summary: Revoke token by JTI (admin) description: | Revoke any token by its JWT ID (`jti` claim). The token is immediately invalid for all future validation calls. operationId: revokeToken tags: [Admin — Tokens] security: - bearerAuth: [] parameters: - name: jti in: path required: true schema: type: string format: uuid example: f47ac10b-58cc-4372-a567-0e02b2c3d479 responses: "204": description: Token revoked. "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" /v1/accounts: get: summary: List accounts (admin) operationId: listAccounts tags: [Admin — Accounts] security: - bearerAuth: [] responses: "200": description: Array of accounts. content: application/json: schema: type: array items: $ref: "#/components/schemas/Account" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" post: summary: Create account (admin) description: | Create a human or system account. - `human` accounts require a `password`. - `system` accounts must not include a `password`; authenticate via tokens issued by `POST /v1/token/issue`. operationId: createAccount tags: [Admin — Accounts] security: - bearerAuth: [] requestBody: required: true content: application/json: schema: type: object required: [username, account_type] properties: username: type: string example: alice account_type: type: string enum: [human, system] example: human password: type: string description: Required for human accounts. Hashed with Argon2id. example: s3cr3t responses: "201": description: Account created. content: application/json: schema: $ref: "#/components/schemas/Account" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "409": description: Username already taken. content: application/json: schema: $ref: "#/components/schemas/Error" example: error: username already exists code: conflict /v1/accounts/{id}: parameters: - name: id in: path required: true description: Account UUID. schema: type: string format: uuid example: 550e8400-e29b-41d4-a716-446655440000 get: summary: Get account (admin) operationId: getAccount tags: [Admin — Accounts] security: - bearerAuth: [] responses: "200": description: Account details. content: application/json: schema: $ref: "#/components/schemas/Account" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" patch: summary: Update account (admin) description: Update mutable account fields. Currently only `status` is patchable. operationId: updateAccount tags: [Admin — Accounts] security: - bearerAuth: [] requestBody: required: true content: application/json: schema: type: object properties: status: type: string enum: [active, inactive] example: inactive responses: "204": description: Account updated. "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" delete: summary: Delete account (admin) description: | Soft-delete an account. Sets status to `deleted` and revokes all active tokens. The account record is retained for audit purposes. operationId: deleteAccount tags: [Admin — Accounts] security: - bearerAuth: [] responses: "204": description: Account deleted. "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" /v1/accounts/{id}/roles: parameters: - name: id in: path required: true schema: type: string format: uuid example: 550e8400-e29b-41d4-a716-446655440000 get: summary: Get account roles (admin) operationId: getRoles tags: [Admin — Accounts] security: - bearerAuth: [] responses: "200": description: Current role list. content: application/json: schema: type: object required: [roles] properties: roles: type: array items: type: string example: [editor, readonly] "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" put: summary: Set account roles (admin) description: | Replace the account's full role list. Roles take effect in the **next** token issued or renewed; existing tokens continue to carry the roles embedded at issuance time. operationId: setRoles tags: [Admin — Accounts] security: - bearerAuth: [] requestBody: required: true content: application/json: schema: type: object required: [roles] properties: roles: type: array items: type: string example: [editor, readonly] responses: "204": description: Roles updated. "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" /v1/accounts/{id}/pgcreds: parameters: - name: id in: path required: true schema: type: string format: uuid example: 550e8400-e29b-41d4-a716-446655440000 get: summary: Get Postgres credentials (admin) description: | Retrieve stored Postgres connection credentials. Password is returned in plaintext over TLS. Stored encrypted at rest with AES-256-GCM. operationId: getPGCreds tags: [Admin — Credentials] security: - bearerAuth: [] responses: "200": description: Postgres credentials. content: application/json: schema: $ref: "#/components/schemas/PGCreds" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" put: summary: Set Postgres credentials (admin) description: Store or replace Postgres credentials for an account. operationId: setPGCreds tags: [Admin — Credentials] security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/PGCreds" responses: "204": description: Credentials stored. "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" /v1/audit: get: summary: Query audit log (admin) description: | Retrieve audit log entries, newest first. Supports pagination and filtering. The log is append-only and never contains credentials. Event types include: `login_ok`, `login_fail`, `login_totp_fail`, `token_issued`, `token_renewed`, `token_revoked`, `token_expired`, `account_created`, `account_updated`, `account_deleted`, `role_granted`, `role_revoked`, `totp_enrolled`, `totp_removed`, `pgcred_accessed`, `pgcred_updated`, `pgcred_access_granted`, `pgcred_access_revoked`, `tag_added`, `tag_removed`, `policy_rule_created`, `policy_rule_updated`, `policy_rule_deleted`, `policy_deny`. operationId: listAudit tags: [Admin — Audit] security: - bearerAuth: [] parameters: - name: limit in: query schema: type: integer default: 50 minimum: 1 maximum: 1000 example: 50 - name: offset in: query schema: type: integer default: 0 example: 0 - name: event_type in: query schema: type: string description: Filter by event type. example: login_fail - name: actor_id in: query schema: type: string format: uuid description: Filter by actor account UUID. example: 550e8400-e29b-41d4-a716-446655440000 responses: "200": description: Paginated audit log. content: application/json: schema: type: object required: [events, total, limit, offset] properties: events: type: array items: $ref: "#/components/schemas/AuditEvent" total: type: integer description: Total number of matching events (for pagination). example: 142 limit: type: integer example: 50 offset: type: integer example: 0 "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" /v1/accounts/{id}/tags: parameters: - name: id in: path required: true schema: type: string format: uuid example: 550e8400-e29b-41d4-a716-446655440000 get: summary: Get account tags (admin) description: | Return the current tag set for an account. Tags are used by the policy engine for machine/service gating (e.g. `env:production`, `svc:payments-api`). operationId: getAccountTags tags: [Admin — Policy] security: - bearerAuth: [] responses: "200": description: Tag list. content: application/json: schema: $ref: "#/components/schemas/TagsResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" put: summary: Set account tags (admin) description: | Replace the account's full tag set atomically. Pass an empty array to clear all tags. Changes take effect immediately for new policy evaluations; no token renewal is required. operationId: setAccountTags tags: [Admin — Policy] security: - bearerAuth: [] requestBody: required: true content: application/json: schema: type: object required: [tags] properties: tags: type: array items: type: string example: ["env:production", "svc:payments-api"] responses: "200": description: Updated tag list. content: application/json: schema: $ref: "#/components/schemas/TagsResponse" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" /v1/accounts/{id}/password: parameters: - name: id in: path required: true schema: type: string format: uuid example: 550e8400-e29b-41d4-a716-446655440000 put: summary: Admin password reset (admin) description: | Reset the password for a human account without requiring the current password. This is intended for account recovery (e.g. a user forgot their password). On success: - The stored Argon2id hash is replaced with the new password hash. - All active sessions for the target account are revoked. Only applies to human accounts. The new password must be at least 12 characters. operationId: adminSetPassword tags: [Admin — Accounts] security: - bearerAuth: [] requestBody: required: true content: application/json: schema: type: object required: [new_password] properties: new_password: type: string description: The new password. Minimum 12 characters. example: new-s3cr3t-long responses: "204": description: Password reset. All active sessions for the account revoked. "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" /v1/policy/rules: get: summary: List policy rules (admin) description: | Return all operator-defined policy rules ordered by priority (ascending). Built-in default rules (IDs -1 to -7) are not included. operationId: listPolicyRules tags: [Admin — Policy] security: - bearerAuth: [] responses: "200": description: Array of policy rules. content: application/json: schema: type: array items: $ref: "#/components/schemas/PolicyRule" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" post: summary: Create policy rule (admin) description: | Create a new operator policy rule. Rules are evaluated in priority order (lower number = evaluated first, default 100). Deny-wins: if any matching rule has effect `deny`, access is denied regardless of allow rules. operationId: createPolicyRule tags: [Admin — Policy] security: - bearerAuth: [] requestBody: required: true content: application/json: schema: type: object required: [description, rule] properties: description: type: string example: Allow payments-api to read its own pgcreds priority: type: integer description: Evaluation priority. Lower = first. Default 100. example: 50 rule: $ref: "#/components/schemas/RuleBody" not_before: type: string format: date-time description: Earliest activation time (RFC3339, optional). example: "2026-04-01T00:00:00Z" expires_at: type: string format: date-time description: Expiry time (RFC3339, optional). example: "2026-06-01T00:00:00Z" responses: "201": description: Rule created. content: application/json: schema: $ref: "#/components/schemas/PolicyRule" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" /v1/policy/rules/{id}: parameters: - name: id in: path required: true schema: type: integer example: 1 get: summary: Get policy rule (admin) operationId: getPolicyRule tags: [Admin — Policy] security: - bearerAuth: [] responses: "200": description: Policy rule. content: application/json: schema: $ref: "#/components/schemas/PolicyRule" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" patch: summary: Update policy rule (admin) description: | Update one or more fields of an existing policy rule. All fields are optional; omitted fields are left unchanged. operationId: updatePolicyRule tags: [Admin — Policy] security: - bearerAuth: [] requestBody: required: true content: application/json: schema: type: object properties: description: type: string example: Updated description priority: type: integer example: 75 enabled: type: boolean example: false rule: $ref: "#/components/schemas/RuleBody" not_before: type: string format: date-time description: Set earliest activation time (RFC3339). example: "2026-04-01T00:00:00Z" expires_at: type: string format: date-time description: Set expiry time (RFC3339). example: "2026-06-01T00:00:00Z" clear_not_before: type: boolean description: Set to true to remove not_before constraint. clear_expires_at: type: boolean description: Set to true to remove expires_at constraint. responses: "200": description: Updated rule. content: application/json: schema: $ref: "#/components/schemas/PolicyRule" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" delete: summary: Delete policy rule (admin) description: Permanently delete a policy rule. This action cannot be undone. operationId: deletePolicyRule tags: [Admin — Policy] security: - bearerAuth: [] responses: "204": description: Rule deleted. "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" tags: - name: Public description: No authentication required. - name: Auth description: Requires a valid bearer token. - name: Admin — Auth description: Requires admin role. - name: Admin — Tokens description: Requires admin role. - name: Admin — Accounts description: Requires admin role. - name: Admin — Credentials description: Requires admin role. - name: Admin — Audit description: Requires admin role. - name: Admin — Policy description: Requires admin role. Manage policy rules and account tags.