Checkpoint: fix all lint warnings

- errorlint: use errors.Is for ErrSealed comparisons in vault_test.go
- gofmt: reformat config, config_test, middleware_test with goimports
- govet/fieldalignment: reorder struct fields in vault.go, csrf.go,
  detail_test.go, middleware_test.go for optimal alignment
- unused: remove unused newCSRFManager in csrf.go (superseded by
  newCSRFManagerFromVault)
- revive/early-return: invert sealed-vault condition in main.go

Security: no auth/crypto logic changed; struct reordering and error
comparison fixes only. newCSRFManager removal is safe — it was never
called; all CSRF construction goes through newCSRFManagerFromVault.

Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
2026-03-15 16:40:11 -07:00
parent 9657f18784
commit cb661bb8f5
12 changed files with 708 additions and 41 deletions

View File

@@ -367,7 +367,25 @@ All endpoints use JSON request/response bodies. All responses include a
| POST | `/v1/token/issue` | admin JWT | Issue service account token |
| DELETE | `/v1/token/{jti}` | admin JWT | Revoke token by JTI |
### Account Endpoints (admin only)
### Token Download Endpoint
| Method | Path | Auth required | Description |
|---|---|---|---|
| GET | `/token/download/{nonce}` | bearer JWT | Download a previously issued token via one-time nonce (5-min TTL, single-use) |
The token download flow issues a short-lived nonce when a service token is created
via `POST /accounts/{id}/token`. The bearer must be authenticated; the nonce is
deleted on first download to prevent replay. This avoids exposing the raw token
value in an HTMX fragment or flash message.
### Token Delegation Endpoints (admin only)
| Method | Path | Auth required | Description |
|---|---|---|---|
| POST | `/accounts/{id}/token/delegates` | admin JWT | Grant a human account permission to issue tokens for a system account |
| DELETE | `/accounts/{id}/token/delegates/{grantee}` | admin JWT | Revoke token-issue delegation |
### Account Endpoints
| Method | Path | Auth required | Description |
|---|---|---|---|
@@ -376,6 +394,7 @@ All endpoints use JSON request/response bodies. All responses include a
| GET | `/v1/accounts/{id}` | admin JWT | Get account details |
| PATCH | `/v1/accounts/{id}` | admin JWT | Update account (status, roles, etc.) |
| DELETE | `/v1/accounts/{id}` | admin JWT | Soft-delete account |
| POST | `/v1/accounts/{id}/token` | bearer JWT (admin or delegate) | Issue/rotate service account token |
### Password Endpoints
@@ -479,6 +498,7 @@ cookie pattern (`mcias_csrf`).
| `/policies` | Policy rules management — create, enable/disable, delete |
| `/audit` | Audit log viewer |
| `/profile` | User profile — self-service password change (any authenticated user) |
| `/service-accounts` | Delegated service account list for non-admin users; issue/rotate token with one-time download |
**HTMX fragments:** Mutating operations (role updates, tag edits, credential
saves, policy toggles, access grants) use HTMX partial-page updates for a
@@ -658,6 +678,22 @@ CREATE TABLE policy_rules (
not_before TEXT DEFAULT NULL, -- optional: earliest activation time (RFC3339)
expires_at TEXT DEFAULT NULL -- optional: expiry time (RFC3339)
);
-- Token issuance delegation: tracks which human accounts may issue tokens for
-- a given system account without holding the global admin role. Admins manage
-- delegates; delegates can issue/rotate tokens for the specific system account
-- only and cannot modify any other account settings.
CREATE TABLE service_account_delegates (
id INTEGER PRIMARY KEY,
account_id INTEGER NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, -- target system account
grantee_id INTEGER NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, -- human account granted access
granted_by INTEGER REFERENCES accounts(id), -- admin who granted access
granted_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')),
UNIQUE (account_id, grantee_id)
);
CREATE INDEX idx_sa_delegates_account ON service_account_delegates (account_id);
CREATE INDEX idx_sa_delegates_grantee ON service_account_delegates (grantee_id);
```
### Schema Notes
@@ -810,6 +846,8 @@ The `cmd/` packages are thin wrappers that wire dependencies and call into
| `policy_rule_updated` | Policy rule updated (priority, enabled, description) |
| `policy_rule_deleted` | Policy rule deleted |
| `policy_deny` | Policy engine denied a request (logged for every explicit deny) |
| `token_delegate_granted` | Admin granted a human account permission to issue tokens for a system account |
| `token_delegate_revoked` | Admin revoked a human account's token-issue delegation |
| `vault_unsealed` | Vault unsealed via REST API or web UI; details include `source` (api\|ui) and `ip` |
| `vault_sealed` | Vault sealed via REST API; details include actor ID, `source`, and `ip` |
@@ -1373,6 +1411,8 @@ needed:
- A human account should be able to access credentials for one specific service
without being a full admin.
- A human account should be able to issue/rotate tokens for one specific service
account without holding the global `admin` role (see token delegation, §21).
- A system account (`deploy-agent`) should only operate on hosts tagged
`env:staging`, not `env:production`.
- A "secrets reader" role should read pgcreds for any service but change nothing.
@@ -1465,7 +1505,7 @@ type Resource struct {
// Rule is a single policy statement. All populated fields are ANDed.
// A zero/empty field is a wildcard (matches anything).
type Rule struct {
ID int64 // database primary key; 0 for built-in rules
ID int64 // database primary key; negative for built-in rules (-1 … -7)
Description string
// Principal match conditions
@@ -1681,3 +1721,94 @@ introduced.
| `policy_rule_deleted` | Rule deleted |
| `tag_added` | Tag added to an account |
| `tag_removed` | Tag removed from an account |
---
## 21. Token Issuance Delegation
### Motivation
The initial design required the `admin` role to issue a service account token.
This blocks a common workflow: a developer who owns one personal app (e.g.
`payments-api`) wants to rotate its service token without granting another
person full admin access to all of MCIAS.
Token issuance delegation solves this by allowing admins to grant specific
human accounts the right to issue/rotate tokens for specific system accounts —
and nothing else.
### Model
The `service_account_delegates` table stores the delegation relationship:
```
service_account_delegates(account_id, grantee_id, granted_by, granted_at)
```
- `account_id` — the **system account** whose token the delegate may issue
- `grantee_id` — the **human account** granted the right
- `granted_by` — the admin who created the grant (for audit purposes)
A human account is a delegate if a row exists with their ID as `grantee_id`.
Delegates may:
- Issue/rotate the token for the specific system account
- Download the newly issued token via the one-time nonce endpoint
- View the system account on their `/service-accounts` page
Delegates may **not**:
- Modify roles, tags, or status on the system account
- Read or modify pgcreds for the system account
- List other accounts or perform any other admin operation
### Token Download Flow
Issuing a service token via `POST /accounts/{id}/token` (admin or delegate)
stores the raw token string in an in-memory `sync.Map` under a random nonce
with a 5-minute TTL. The handler returns the nonce in the HTMX fragment.
The caller redeems the nonce via `GET /token/download/{nonce}`, which:
1. Looks up the nonce in the map (missing → 404).
2. Deletes the nonce immediately (prevents replay).
3. Returns the token as `Content-Disposition: attachment; filename=token.txt`.
The nonce is not stored in the database and is lost on server restart. This
is intentional: if the download window is missed, the operator simply issues
a new token.
### Authorization Check
`POST /accounts/{id}/token` is authenticated (bearer JWT + CSRF) but not
admin-only. The handler performs an explicit check:
```
if claims.HasRole("admin") OR db.HasTokenIssueAccess(targetID, callerID):
proceed
else:
403 Forbidden
```
This check is done in the handler rather than middleware because the
delegation relationship requires a DB lookup that depends on the caller's
identity and the specific target account.
### Admin Management
| Endpoint | Description |
|---|---|
| `POST /accounts/{id}/token/delegates` | Grant delegation (admin only) |
| `DELETE /accounts/{id}/token/delegates/{grantee}` | Revoke delegation (admin only) |
Both operations produce audit events (`token_delegate_granted`,
`token_delegate_revoked`) and are visible in the account detail UI under
the "Token Issue Access" section.
### Audit Events
| Event | Trigger |
|---|---|
| `token_delegate_granted` | Admin granted a human account token-issue access for a system account |
| `token_delegate_revoked` | Admin revoked token-issue delegation |
| `token_issued` | Token issued (existing event, also fires for delegate-issued tokens) |