diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 59c3ddd..b7e14a9 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -15,36 +15,46 @@ parties that delegate authentication decisions to it. ### Components ``` -┌────────────────────────────────────────────────────┐ -│ MCIAS Server (mciassrv) │ -│ ┌──────────┐ ┌──────────┐ ┌───────────────────┐ │ -│ │ Auth │ │ Token │ │ Account / Role │ │ -│ │ Handler │ │ Manager │ │ Manager │ │ -│ └────┬─────┘ └────┬─────┘ └─────────┬─────────┘ │ -│ └─────────────┴─────────────────┘ │ -│ │ │ -│ ┌─────────▼──────────┐ │ -│ │ SQLite Database │ │ -│ └────────────────────┘ │ -└────────────────────────────────────────────────────┘ - ▲ ▲ ▲ - │ HTTPS/REST │ HTTPS/REST │ direct file I/O - │ │ │ - ┌──────┴──────┐ ┌────┴─────┐ ┌──────┴──────┐ - │ Personal │ │ mciasctl │ │ mciasdb │ - │ Apps │ │ (admin │ │ (DB tool) │ - └─────────────┘ │ CLI) │ └─────────────┘ - └──────────┘ +┌──────────────────────────────────────────────────────────┐ +│ MCIAS Server (mciassrv) │ +│ ┌──────────┐ ┌──────────┐ ┌───────────────────┐ │ +│ │ Auth │ │ Token │ │ Account / Role │ │ +│ │ Handler │ │ Manager │ │ Manager │ │ +│ └────┬─────┘ └────┬─────┘ └─────────┬─────────┘ │ +│ └─────────────┴─────────────────┘ │ +│ │ │ +│ ┌─────────▼──────────┐ │ +│ │ SQLite Database │ │ +│ └────────────────────┘ │ +│ │ +│ ┌──────────────────┐ ┌──────────────────────┐ │ +│ │ REST listener │ │ gRPC listener │ │ +│ │ (net/http) │ │ (google.golang.org/ │ │ +│ │ :8443 │ │ grpc) :9443 │ │ +│ └──────────────────┘ └──────────────────────┘ │ +└──────────────────────────────────────────────────────────┘ + ▲ ▲ ▲ ▲ + │ 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. -Handles login, token issuance, token validation, token renewal, and token -revocation. +**mciassrv** — The authentication server. Exposes a REST API and gRPC API over +HTTPS/TLS (dual-stack; see §17). Handles login, token issuance, token +validation, token renewal, and token revocation. **mciasctl** — The administrator CLI. Communicates with mciassrv's REST API using an admin JWT. Creates/manages human accounts, system accounts, roles, 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 file, bypassing the server API. Intended for break-glass recovery, offline inspection, schema verification, and maintenance tasks that cannot be @@ -127,13 +137,21 @@ mciassrv (passphrase or keyfile) to decrypt secrets at rest. ### 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 +- `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 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. @@ -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/logout` | bearer JWT | Revoke current 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 @@ -372,7 +389,9 @@ All endpoints use JSON request/response bodies. All responses include a | Method | Path | Auth required | Description | |---|---|---|---| | 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 @@ -446,6 +465,7 @@ cookie pattern (`mcias_csrf`). | `/pgcreds` | Postgres credentials list (owned + granted) with create form | | `/policies` | Policy rules management — create, enable/disable, delete | | `/audit` | Audit log viewer | +| `/profile` | User profile — self-service password change (any authenticated user) | **HTMX fragments:** Mutating operations (role updates, tag edits, credential 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 totp_secret_enc 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')), updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')), deleted_at TEXT @@ -661,10 +684,13 @@ or a keyfile path — never inline in the config file. ```toml [server] -listen_addr = "0.0.0.0:8443" -grpc_addr = "0.0.0.0:9443" # optional; omit to disable gRPC -tls_cert = "/etc/mcias/server.crt" -tls_key = "/etc/mcias/server.key" +listen_addr = "0.0.0.0:8443" +grpc_addr = "0.0.0.0:9443" # optional; omit to disable gRPC +tls_cert = "/etc/mcias/server.crt" +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] path = "/var/lib/mcias/mcias.db" @@ -711,7 +737,8 @@ mcias/ │ ├── policy/ # in-process authorization policy engine (§20) │ ├── server/ # HTTP handlers, router setup │ ├── 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/ │ ├── static/ # CSS and static assets │ └── 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 | | `pgcred_accessed` | Postgres credentials retrieved | | `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_removed` | Tag removed from account | | `policy_rule_created` | Policy rule created | @@ -838,6 +868,7 @@ mciasdb --config PATH [flags] |---|---| | `mciasdb schema verify` | Open DB, run migrations in dry-run mode, report version | | `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` | **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` | | `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` | | `AdminService` | `Health`, `GetPublicKey` | @@ -1374,9 +1405,10 @@ const ( ActionReadAudit Action = "audit:read" ActionEnrollTOTP Action = "totp:enroll" // self-service ActionRemoveTOTP Action = "totp:remove" // admin - ActionLogin Action = "auth:login" // public - ActionLogout Action = "auth:logout" // self-service - ActionListRules Action = "policy:list" + ActionLogin Action = "auth:login" // public + ActionLogout Action = "auth:logout" // self-service + ActionChangePassword Action = "auth:change_password" // self-service + ActionListRules Action = "policy:list" ActionManageRules Action = "policy:manage" // Resource types @@ -1476,8 +1508,10 @@ at the same priority level. ``` Priority 0, Allow: roles=[admin], actions= — 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: accountTypes=[human], actions=[auth:change_password] + — self-service password change Priority 0, Allow: accountTypes=[system], actions=[pgcreds:read], resourceType=pgcreds, ownerMatchesSubject=true — system account reads own creds diff --git a/PROGRESS.md b/PROGRESS.md index 7e5d3d4..227b6cb 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -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. +### 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 **internal/ui/handlers_accounts.go** diff --git a/PROJECT_PLAN.md b/PROJECT_PLAN.md index cafea3e..ab5018f 100644 --- a/PROJECT_PLAN.md +++ b/PROJECT_PLAN.md @@ -165,18 +165,27 @@ See ARCHITECTURE.md for design rationale. ### Step 4.1: `cmd/mciasctl` — admin CLI **Acceptance criteria:** - Subcommands: - - `mciasctl account create --username NAME --type human|system` + - `mciasctl account create -username NAME -type human|system` - `mciasctl account list` - - `mciasctl account suspend --id UUID` - - `mciasctl account delete --id UUID` - - `mciasctl role grant --account UUID --role ROLE` - - `mciasctl role revoke --account UUID --role ROLE` - - `mciasctl token issue --account UUID` (system accounts) - - `mciasctl token revoke --jti JTI` - - `mciasctl pgcreds set --account UUID --host H --port P --db D --user U --password P` - - `mciasctl pgcreds get --account UUID` -- CLI reads admin JWT from `MCIAS_ADMIN_TOKEN` env var or `--token` flag -- All commands make HTTPS requests to mciassrv (base URL from `--server` flag + - `mciasctl account update -id UUID -status active|inactive` + - `mciasctl account delete -id UUID` + - `mciasctl account get -id UUID` + - `mciasctl account set-password -id UUID` + - `mciasctl role list -id UUID` + - `mciasctl role set -id UUID -roles role1,role2` + - `mciasctl role grant -id UUID -role ROLE` + - `mciasctl role revoke -id UUID -role ROLE` + - `mciasctl token issue -id UUID` (system accounts) + - `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) - Tests: flag parsing; missing required flags → error; help text complete