Update docs for recent changes

- ARCHITECTURE.md: add gRPC listener, mciasgrpcctl, new roles,
  granular role endpoints, profile page, audit events, policy actions,
  trusted_proxy config, validate package, schema force command
- PROGRESS.md: document role expansion and UI privilege escalation fix
- PROJECT_PLAN.md: align mciasctl subcommands with implementation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 23:07:41 -07:00
parent 8d9d9da6f5
commit 4e544665d2
3 changed files with 125 additions and 49 deletions

View File

@@ -15,36 +15,46 @@ parties that delegate authentication decisions to it.
### Components ### Components
``` ```
┌────────────────────────────────────────────────────┐ ┌──────────────────────────────────────────────────────────
│ MCIAS Server (mciassrv) │ MCIAS Server (mciassrv)
│ ┌──────────┐ ┌──────────┐ ┌───────────────────┐ │ │ ┌──────────┐ ┌──────────┐ ┌───────────────────┐
│ │ Auth │ │ Token │ │ Account / Role │ │ │ │ Auth │ │ Token │ │ Account / Role │
│ │ Handler │ │ Manager │ │ Manager │ │ │ │ Handler │ │ Manager │ │ Manager │
│ └────┬─────┘ └────┬─────┘ └─────────┬─────────┘ │ │ └────┬─────┘ └────┬─────┘ └─────────┬─────────┘
│ └─────────────┴─────────────────┘ │ │ └─────────────┴─────────────────┘
│ │ │ │ │
│ ┌─────────▼──────────┐ │ │ ┌─────────▼──────────┐
│ │ SQLite Database │ │ │ │ SQLite Database │
│ └────────────────────┘ │ │ └────────────────────┘
└────────────────────────────────────────────────────┘ │ │
▲ ▲ ┌──────────────────┐ ┌──────────────────────┐
│ HTTPS/REST │ HTTPS/REST │ direct file I/O REST listener │ │ gRPC listener │ │
│ │ (net/http) (google.golang.org/ │
┌──────┴──────┐ ┌────┴─────┐ ┌──────┴──────┐ │ │ :8443 │ │ grpc) :9443 │ │
Personal │ │ mciasctl │ │ mciasdb └──────────────────┘ └──────────────────────┘
│ Apps │ │ (admin │ │ (DB tool) │ └──────────────────────────────────────────────────────────┘
└─────────────┘ │ CLI) └─────────────┘ ▲ ▲ ▲
└──────────┘ │ HTTPS/REST │ HTTPS/REST │ gRPC/TLS │ direct file I/O
│ │ │ │
┌────┴──────┐ ┌────┴─────┐ ┌─────┴────────┐ ┌───┴────────┐
│ Personal │ │ mciasctl │ │ mciasgrpcctl │ │ mciasdb │
│ Apps │ │ (admin │ │ (gRPC admin │ │ (DB tool) │
└───────────┘ │ CLI) │ │ CLI) │ └────────────┘
└──────────┘ └──────────────┘
``` ```
**mciassrv** — The authentication server. Exposes a REST API over HTTPS/TLS. **mciassrv** — The authentication server. Exposes a REST API and gRPC API over
Handles login, token issuance, token validation, token renewal, and token HTTPS/TLS (dual-stack; see §17). Handles login, token issuance, token
revocation. validation, token renewal, and token revocation.
**mciasctl** — The administrator CLI. Communicates with mciassrv's REST API **mciasctl** — The administrator CLI. Communicates with mciassrv's REST API
using an admin JWT. Creates/manages human accounts, system accounts, roles, using an admin JWT. Creates/manages human accounts, system accounts, roles,
and Postgres credential records. and Postgres credential records.
**mciasgrpcctl** — The gRPC administrator CLI. Mirrors mciasctl's subcommands
but communicates over gRPC/TLS instead of REST. Both CLIs can coexist; neither
depends on the other.
**mciasdb** — The database maintenance tool. Operates directly on the SQLite **mciasdb** — The database maintenance tool. Operates directly on the SQLite
file, bypassing the server API. Intended for break-glass recovery, offline file, bypassing the server API. Intended for break-glass recovery, offline
inspection, schema verification, and maintenance tasks that cannot be inspection, schema verification, and maintenance tasks that cannot be
@@ -127,13 +137,21 @@ mciassrv (passphrase or keyfile) to decrypt secrets at rest.
### Roles ### Roles
Roles are simple string labels stored in the `account_roles` table. Roles are simple string labels stored in the `account_roles` table. Only
compile-time allowlisted role names are accepted; attempting to grant an
unknown role returns an error (prevents typos like "admim" from silently
creating a useless role).
Reserved roles: Compile-time allowlisted roles:
- `admin` — superuser; can manage all accounts, tokens, and credentials - `admin` — superuser; can manage all accounts, tokens, and credentials
- `user` — standard user role
- `guest` — limited read-only access
- `viewer` — read-only access
- `editor` — create/modify access
- `commenter` — comment/annotate access
- Any role named identically to a system account — grants that human account - Any role named identically to a system account — grants that human account
the ability to issue/revoke tokens and retrieve Postgres credentials for that the ability to issue/revoke tokens and retrieve Postgres credentials for that
system account system account (via policy rules, not the allowlist)
Role assignment requires admin privileges. Role assignment requires admin privileges.
@@ -340,7 +358,6 @@ All endpoints use JSON request/response bodies. All responses include a
| POST | `/v1/auth/login` | none | Username/password (+TOTP) login → JWT | | POST | `/v1/auth/login` | none | Username/password (+TOTP) login → JWT |
| POST | `/v1/auth/logout` | bearer JWT | Revoke current token | | POST | `/v1/auth/logout` | bearer JWT | Revoke current token |
| POST | `/v1/auth/renew` | bearer JWT | Exchange token for new token | | POST | `/v1/auth/renew` | bearer JWT | Exchange token for new token |
| PUT | `/v1/auth/password` | bearer JWT | Self-service password change (requires current password) |
### Token Endpoints ### Token Endpoints
@@ -372,7 +389,9 @@ All endpoints use JSON request/response bodies. All responses include a
| Method | Path | Auth required | Description | | Method | Path | Auth required | Description |
|---|---|---|---| |---|---|---|---|
| GET | `/v1/accounts/{id}/roles` | admin JWT | List roles for account | | GET | `/v1/accounts/{id}/roles` | admin JWT | List roles for account |
| PUT | `/v1/accounts/{id}/roles` | admin JWT | Replace role set | | PUT | `/v1/accounts/{id}/roles` | admin JWT | Replace role set (atomic) |
| POST | `/v1/accounts/{id}/roles` | admin JWT | Grant a single role |
| DELETE | `/v1/accounts/{id}/roles/{role}` | admin JWT | Revoke a single role |
### TOTP Endpoints ### TOTP Endpoints
@@ -446,6 +465,7 @@ cookie pattern (`mcias_csrf`).
| `/pgcreds` | Postgres credentials list (owned + granted) with create form | | `/pgcreds` | Postgres credentials list (owned + granted) with create form |
| `/policies` | Policy rules management — create, enable/disable, delete | | `/policies` | Policy rules management — create, enable/disable, delete |
| `/audit` | Audit log viewer | | `/audit` | Audit log viewer |
| `/profile` | User profile — self-service password change (any authenticated user) |
**HTMX fragments:** Mutating operations (role updates, tag edits, credential **HTMX fragments:** Mutating operations (role updates, tag edits, credential
saves, policy toggles, access grants) use HTMX partial-page updates for a saves, policy toggles, access grants) use HTMX partial-page updates for a
@@ -490,6 +510,9 @@ CREATE TABLE accounts (
-- AES-256-GCM encrypted TOTP secret; NULL if not enrolled -- AES-256-GCM encrypted TOTP secret; NULL if not enrolled
totp_secret_enc BLOB, totp_secret_enc BLOB,
totp_secret_nonce BLOB, totp_secret_nonce BLOB,
-- Last accepted TOTP counter value; prevents replay attacks within the
-- ±1 time-step window (RFC 6238 §5.2). NULL = no code accepted yet.
last_totp_counter INTEGER DEFAULT NULL,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')), created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')),
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')), updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')),
deleted_at TEXT deleted_at TEXT
@@ -661,10 +684,13 @@ or a keyfile path — never inline in the config file.
```toml ```toml
[server] [server]
listen_addr = "0.0.0.0:8443" listen_addr = "0.0.0.0:8443"
grpc_addr = "0.0.0.0:9443" # optional; omit to disable gRPC grpc_addr = "0.0.0.0:9443" # optional; omit to disable gRPC
tls_cert = "/etc/mcias/server.crt" tls_cert = "/etc/mcias/server.crt"
tls_key = "/etc/mcias/server.key" tls_key = "/etc/mcias/server.key"
# trusted_proxy = "127.0.0.1" # optional; IP of reverse proxy — when set,
# X-Forwarded-For is trusted only from this IP
# for rate limiting and audit log IP extraction
[database] [database]
path = "/var/lib/mcias/mcias.db" path = "/var/lib/mcias/mcias.db"
@@ -711,7 +737,8 @@ mcias/
│ ├── policy/ # in-process authorization policy engine (§20) │ ├── policy/ # in-process authorization policy engine (§20)
│ ├── server/ # HTTP handlers, router setup │ ├── server/ # HTTP handlers, router setup
│ ├── token/ # JWT issuance, validation, revocation │ ├── token/ # JWT issuance, validation, revocation
── ui/ # web UI context, CSRF, session, template handlers ── ui/ # web UI context, CSRF, session, template handlers
│ └── validate/ # input validation helpers (username, password strength)
├── web/ ├── web/
│ ├── static/ # CSS and static assets │ ├── static/ # CSS and static assets
│ └── templates/ # HTML templates (base layout, pages, HTMX fragments) │ └── templates/ # HTML templates (base layout, pages, HTMX fragments)
@@ -761,6 +788,9 @@ The `cmd/` packages are thin wrappers that wire dependencies and call into
| `totp_removed` | TOTP removed from account | | `totp_removed` | TOTP removed from account |
| `pgcred_accessed` | Postgres credentials retrieved | | `pgcred_accessed` | Postgres credentials retrieved |
| `pgcred_updated` | Postgres credentials stored/updated | | `pgcred_updated` | Postgres credentials stored/updated |
| `pgcred_access_granted` | Read access to PG credentials granted to another account |
| `pgcred_access_revoked` | Read access to PG credentials revoked from an account |
| `password_changed` | Account password changed (self-service or admin reset) |
| `tag_added` | Tag added to account | | `tag_added` | Tag added to account |
| `tag_removed` | Tag removed from account | | `tag_removed` | Tag removed from account |
| `policy_rule_created` | Policy rule created | | `policy_rule_created` | Policy rule created |
@@ -838,6 +868,7 @@ mciasdb --config PATH <subcommand> [flags]
|---|---| |---|---|
| `mciasdb schema verify` | Open DB, run migrations in dry-run mode, report version | | `mciasdb schema verify` | Open DB, run migrations in dry-run mode, report version |
| `mciasdb schema migrate` | Apply any pending migrations and exit | | `mciasdb schema migrate` | Apply any pending migrations and exit |
| `mciasdb schema force --version N` | Force schema version (clears dirty state); break-glass recovery |
| `mciasdb prune tokens` | Delete expired rows from `token_revocation` and `system_tokens` | | `mciasdb prune tokens` | Delete expired rows from `token_revocation` and `system_tokens` |
**Account management (offline):** **Account management (offline):**
@@ -943,7 +974,7 @@ in `proto/generate.go` using `protoc-gen-go` and `protoc-gen-go-grpc`.
|---|---| |---|---|
| `AuthService` | `Login`, `Logout`, `RenewToken`, `EnrollTOTP`, `ConfirmTOTP`, `RemoveTOTP` | | `AuthService` | `Login`, `Logout`, `RenewToken`, `EnrollTOTP`, `ConfirmTOTP`, `RemoveTOTP` |
| `TokenService` | `ValidateToken`, `IssueServiceToken`, `RevokeToken` | | `TokenService` | `ValidateToken`, `IssueServiceToken`, `RevokeToken` |
| `AccountService` | `ListAccounts`, `CreateAccount`, `GetAccount`, `UpdateAccount`, `DeleteAccount`, `GetRoles`, `SetRoles` | | `AccountService` | `ListAccounts`, `CreateAccount`, `GetAccount`, `UpdateAccount`, `DeleteAccount`, `GetRoles`, `SetRoles`, `GrantRole`, `RevokeRole` |
| `CredentialService` | `GetPGCreds`, `SetPGCreds` | | `CredentialService` | `GetPGCreds`, `SetPGCreds` |
| `AdminService` | `Health`, `GetPublicKey` | | `AdminService` | `Health`, `GetPublicKey` |
@@ -1374,9 +1405,10 @@ const (
ActionReadAudit Action = "audit:read" ActionReadAudit Action = "audit:read"
ActionEnrollTOTP Action = "totp:enroll" // self-service ActionEnrollTOTP Action = "totp:enroll" // self-service
ActionRemoveTOTP Action = "totp:remove" // admin ActionRemoveTOTP Action = "totp:remove" // admin
ActionLogin Action = "auth:login" // public ActionLogin Action = "auth:login" // public
ActionLogout Action = "auth:logout" // self-service ActionLogout Action = "auth:logout" // self-service
ActionListRules Action = "policy:list" ActionChangePassword Action = "auth:change_password" // self-service
ActionListRules Action = "policy:list"
ActionManageRules Action = "policy:manage" ActionManageRules Action = "policy:manage"
// Resource types // Resource types
@@ -1476,8 +1508,10 @@ at the same priority level.
``` ```
Priority 0, Allow: roles=[admin], actions=<all> — admin wildcard Priority 0, Allow: roles=[admin], actions=<all> — admin wildcard
Priority 0, Allow: actions=[tokens:renew, auth:logout] — self-service logout/renew Priority 0, Allow: actions=[auth:logout, tokens:renew] — self-service logout/renew
Priority 0, Allow: actions=[totp:enroll] — self-service TOTP enrollment Priority 0, Allow: actions=[totp:enroll] — self-service TOTP enrollment
Priority 0, Allow: accountTypes=[human], actions=[auth:change_password]
— self-service password change
Priority 0, Allow: accountTypes=[system], actions=[pgcreds:read], Priority 0, Allow: accountTypes=[system], actions=[pgcreds:read],
resourceType=pgcreds, ownerMatchesSubject=true resourceType=pgcreds, ownerMatchesSubject=true
— system account reads own creds — system account reads own creds

View File

@@ -4,6 +4,39 @@ Source of truth for current development state.
--- ---
All phases complete. **v1.0.0 tagged.** All packages pass `go test ./...`; `golangci-lint run ./...` clean. All phases complete. **v1.0.0 tagged.** All packages pass `go test ./...`; `golangci-lint run ./...` clean.
### 2026-03-12 — Update web UI and model for all compile-time roles
- `internal/model/model.go`: added `RoleGuest`, `RoleViewer`, `RoleEditor`, and
`RoleCommenter` constants; updated `allowedRoles` map and `ValidateRole` error
message to include the full set of recognised roles.
- `internal/ui/`: updated `knownRoles` to include guest, viewer, editor, and
commenter; replaced hardcoded role strings with model constants; removed
obsolete "service" role from UI dropdowns.
- All tests pass; build verified.
### 2026-03-12 — Fix UI privilege escalation vulnerability
**internal/ui/ui.go**
- Added `requireAdminRole` middleware that checks `claims.HasRole("admin")`
and returns 403 if absent
- Updated `admin` and `adminGet` middleware wrappers to include
`requireAdminRole` in the chain — previously only `requireCookieAuth`
was applied, allowing any authenticated user to access admin endpoints
- Profile routes correctly use only `requireCookieAuth` (not admin-gated)
**internal/ui/handlers_accounts.go**
- Removed redundant inline admin check from `handleAdminResetPassword`
(now handled by route-level middleware)
**Full audit performed across all three API surfaces:**
- REST (`internal/server/server.go`): all admin routes use
`requireAuth → RequireRole("admin")` — correct
- gRPC (all service files): every admin RPC calls `requireAdmin(ctx)` as
first statement — correct
- UI: was vulnerable, now fixed with `requireAdminRole` middleware
All tests pass; `go vet ./...` clean.
### 2026-03-12 — Checkpoint: password change UI enforcement + migration recovery ### 2026-03-12 — Checkpoint: password change UI enforcement + migration recovery
**internal/ui/handlers_accounts.go** **internal/ui/handlers_accounts.go**

View File

@@ -165,18 +165,27 @@ See ARCHITECTURE.md for design rationale.
### Step 4.1: `cmd/mciasctl` — admin CLI ### Step 4.1: `cmd/mciasctl` — admin CLI
**Acceptance criteria:** **Acceptance criteria:**
- Subcommands: - Subcommands:
- `mciasctl account create --username NAME --type human|system` - `mciasctl account create -username NAME -type human|system`
- `mciasctl account list` - `mciasctl account list`
- `mciasctl account suspend --id UUID` - `mciasctl account update -id UUID -status active|inactive`
- `mciasctl account delete --id UUID` - `mciasctl account delete -id UUID`
- `mciasctl role grant --account UUID --role ROLE` - `mciasctl account get -id UUID`
- `mciasctl role revoke --account UUID --role ROLE` - `mciasctl account set-password -id UUID`
- `mciasctl token issue --account UUID` (system accounts) - `mciasctl role list -id UUID`
- `mciasctl token revoke --jti JTI` - `mciasctl role set -id UUID -roles role1,role2`
- `mciasctl pgcreds set --account UUID --host H --port P --db D --user U --password P` - `mciasctl role grant -id UUID -role ROLE`
- `mciasctl pgcreds get --account UUID` - `mciasctl role revoke -id UUID -role ROLE`
- CLI reads admin JWT from `MCIAS_ADMIN_TOKEN` env var or `--token` flag - `mciasctl token issue -id UUID` (system accounts)
- All commands make HTTPS requests to mciassrv (base URL from `--server` flag - `mciasctl token revoke -jti JTI`
- `mciasctl pgcreds set -id UUID -host H -port P -db D -user U`
- `mciasctl pgcreds get -id UUID`
- `mciasctl auth login`
- `mciasctl auth change-password`
- `mciasctl tag list -id UUID`
- `mciasctl tag set -id UUID -tags tag1,tag2`
- `mciasctl policy list|create|get|update|delete`
- CLI reads admin JWT from `MCIAS_TOKEN` env var or `-token` flag
- All commands make HTTPS requests to mciassrv (base URL from `-server` flag
or `MCIAS_SERVER` env var) or `MCIAS_SERVER` env var)
- Tests: flag parsing; missing required flags → error; help text complete - Tests: flag parsing; missing required flags → error; help text complete