diff --git a/.junie/memory/feedback.md b/.junie/memory/feedback.md index e69de29..06e631f 100644 --- a/.junie/memory/feedback.md +++ b/.junie/memory/feedback.md @@ -0,0 +1,8 @@ +[2026-03-15 19:17] - Updated by Junie +{ + "TYPE": "negative", + "CATEGORY": "Service reliability", + "EXPECTATION": "The Swagger docs endpoint should remain accessible and stable at all times.", + "NEW INSTRUCTION": "WHEN swagger/docs endpoint is down or errors THEN Diagnose cause, apply fix, and restore availability immediately" +} + diff --git a/.junie/memory/language.json b/.junie/memory/language.json index d7b90cb..62dc2e5 100644 --- a/.junie/memory/language.json +++ b/.junie/memory/language.json @@ -1 +1 @@ -[{"lang":"en","usageCount":3}] \ No newline at end of file +[{"lang":"en","usageCount":7}] \ No newline at end of file diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 08ac084..9d4cbb0 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -15,16 +15,16 @@ parties that delegate authentication decisions to it. ### Components ``` -┌──────────────────────────────────────────────────────────┐ -│ MCIAS Server (mciassrv) │ +┌─────────────────────────────────────────────────────────┐ +│ MCIAS Server (mciassrv) │ │ ┌──────────┐ ┌──────────┐ ┌───────────────────┐ │ │ │ Auth │ │ Token │ │ Account / Role │ │ │ │ Handler │ │ Manager │ │ Manager │ │ │ └────┬─────┘ └────┬─────┘ └─────────┬─────────┘ │ │ └─────────────┴─────────────────┘ │ -│ │ │ +│ │ │ │ ┌─────────▼──────────┐ │ -│ │ SQLite Database │ │ +│ │ SQLite Database │ │ │ └────────────────────┘ │ │ │ │ ┌──────────────────┐ ┌──────────────────────┐ │ @@ -32,10 +32,10 @@ parties that delegate authentication decisions to it. │ │ (net/http) │ │ (google.golang.org/ │ │ │ │ :8443 │ │ grpc) :9443 │ │ │ └──────────────────┘ └──────────────────────┘ │ -└──────────────────────────────────────────────────────────┘ - ▲ ▲ ▲ ▲ - │ HTTPS/REST │ HTTPS/REST │ gRPC/TLS │ direct file I/O - │ │ │ │ +└─────────────────────────────────────────────────────────┘ + ▲ ▲ ▲ ▲ + │ HTTPS/REST │ HTTPS/REST │ gRPC/TLS │ direct file I/O + │ │ │ │ ┌────┴──────┐ ┌────┴─────┐ ┌─────┴────────┐ ┌───┴────────┐ │ Personal │ │ mciasctl │ │ mciasgrpcctl │ │ mciasdb │ │ Apps │ │ (admin │ │ (gRPC admin │ │ (DB tool) │ @@ -424,7 +424,8 @@ value in an HTMX fragment or flash message. | Method | Path | Auth required | Description | |---|---|---|---| -| GET | `/v1/accounts/{id}/pgcreds` | admin JWT | Retrieve Postgres credentials | +| GET | `/v1/pgcreds` | bearer JWT | List all credentials accessible to the caller (owned + explicitly granted) | +| GET | `/v1/accounts/{id}/pgcreds` | admin JWT | Retrieve Postgres credentials for a specific account | | PUT | `/v1/accounts/{id}/pgcreds` | admin JWT | Set/update Postgres credentials | ### Tag Endpoints (admin only) @@ -771,30 +772,44 @@ mcias/ │ │ └── main.go │ ├── mciasctl/ # REST admin CLI │ │ └── main.go -│ ├── mciasdb/ # direct SQLite maintenance tool (Phase 6) +│ ├── mciasdb/ # direct SQLite maintenance tool │ │ └── main.go -│ └── mciasgrpcctl/ # gRPC admin CLI companion (Phase 7) +│ └── mciasgrpcctl/ # gRPC admin CLI companion │ └── main.go ├── internal/ +│ ├── audit/ # audit log event detail marshaling │ ├── auth/ # login flow, TOTP verification, account lockout │ ├── config/ # config file parsing and validation │ ├── crypto/ # key management, AES-GCM helpers, master key derivation │ ├── db/ # SQLite access layer (schema, migrations, queries) -│ ├── grpcserver/ # gRPC handler implementations (Phase 7) +│ │ └── migrations/ # numbered SQL migrations (currently 8) +│ ├── grpcserver/ # gRPC handler implementations │ ├── middleware/ # HTTP middleware (auth extraction, logging, rate-limit, policy) │ ├── model/ # shared data types (Account, Token, Role, PolicyRule, etc.) │ ├── 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 -│ └── validate/ # input validation helpers (username, password strength) +│ ├── validate/ # input validation helpers (username, password strength) +│ └── vault/ # master key lifecycle: seal/unseal state, key derivation ├── web/ -│ ├── static/ # CSS and static assets -│ └── templates/ # HTML templates (base layout, pages, HTMX fragments) +│ ├── static/ # CSS, JS, and bundled swagger-ui assets (embedded at build) +│ ├── templates/ # HTML templates (base layout, pages, HTMX fragments) +│ └── embed.go # fs.FS embedding of static files and templates ├── proto/ -│ └── mcias/v1/ # Protobuf service definitions (Phase 7) +│ └── mcias/v1/ # Protobuf service definitions ├── gen/ -│ └── mcias/v1/ # Generated Go stubs from protoc (committed; Phase 7) +│ └── mcias/v1/ # Generated Go stubs from protoc (committed) +├── clients/ +│ ├── go/ # Go client library +│ ├── python/ # Python client library +│ ├── rust/ # Rust client library +│ └── lisp/ # Common Lisp client library +├── test/ +│ ├── e2e/ # end-to-end test suite +│ └── mock/ # Go mock server for client integration tests +├── dist/ # operational artifacts: systemd unit, install script, config templates +├── man/man1/ # man pages (mciassrv.1, mciasctl.1, mciasdb.1, mciasgrpcctl.1) └── go.mod ``` @@ -1008,7 +1023,8 @@ proto/ └── v1/ ├── auth.proto # Login, Logout, Renew, TOTP enroll/confirm/remove ├── token.proto # Validate, Issue, Revoke - ├── account.proto # CRUD for accounts and roles + ├── account.proto # CRUD for accounts, roles, and credentials + ├── policy.proto # Policy rule CRUD (PolicyService) ├── admin.proto # Health, public-key retrieval └── common.proto # Shared message types (Error, Timestamp wrappers) @@ -1029,6 +1045,7 @@ in `proto/generate.go` using `protoc-gen-go` and `protoc-gen-go-grpc`. | `TokenService` | `ValidateToken`, `IssueServiceToken`, `RevokeToken` | | `AccountService` | `ListAccounts`, `CreateAccount`, `GetAccount`, `UpdateAccount`, `DeleteAccount`, `GetRoles`, `SetRoles`, `GrantRole`, `RevokeRole` | | `CredentialService` | `GetPGCreds`, `SetPGCreds` | +| `PolicyService` | `ListPolicyRules`, `CreatePolicyRule`, `GetPolicyRule`, `UpdatePolicyRule`, `DeletePolicyRule` | | `AdminService` | `Health`, `GetPublicKey` | All request/response messages follow the same credential-exclusion rules as @@ -1241,8 +1258,9 @@ The Makefile `docker` target automates the build step with the version tag. | `generate` | `go generate ./...` (re-generates proto stubs) | | `man` | Build man pages; compress to `.gz` in `man/` | | `install` | Run `dist/install.sh` | -| `docker` | `docker build -t mcias:$(VERSION) .` | -| `clean` | Remove `bin/` and compressed man pages | +| `docker` | `docker build -t mcias:$(VERSION) -t mcias:latest .` | +| `docker-clean` | Remove local `mcias:$(VERSION)` and `mcias:latest` images; prune dangling images with the mcias label | +| `clean` | Remove `bin/`, compressed man pages, and local Docker images | | `dist` | Cross-compile release tarballs for linux/amd64 and linux/arm64 | ### Upgrade Path diff --git a/PROGRESS.md b/PROGRESS.md index 28a936c..4b75358 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -4,6 +4,33 @@ Source of truth for current development state. --- All phases complete. **v1.0.0 tagged.** All packages pass `go test ./...`; `golangci-lint run ./...` clean (pre-existing warnings only). +### 2026-03-16 — Documentation sync (ARCHITECTURE.md, PROJECT_PLAN.md) + +**Task:** Full documentation audit to sync ARCHITECTURE.md and PROJECT_PLAN.md with v1.0.0 implementation. + +**ARCHITECTURE.md changes:** +- §8 Postgres Credential Endpoints: added missing `GET /v1/pgcreds` +- §12 Directory/Package Structure: added `internal/audit/`, `internal/vault/`, `web/embed.go`; added `clients/`, `test/`, `dist/`, `man/` top-level dirs; removed stale "(Phase N)" labels +- §17 Proto Package Layout: added `policy.proto` +- §17 Service Definitions: added `PolicyService` row +- §18 Makefile Targets: added `docker-clean`; corrected `docker` and `clean` descriptions + +**PROJECT_PLAN.md changes:** +- All phases 0–9 marked `[COMPLETE]` +- Added status summary at top (v1.0.0, 2026-03-15) +- Phase 4.1: added `mciasctl pgcreds list` subcommand (implemented, was missing from plan) +- Phase 7.1: added `policy.proto` to proto file list +- Phase 8.5: added `docker-clean` target; corrected `docker` and `clean` target descriptions +- Added Phase 10: Web UI (HTMX) +- Added Phase 11: Authorization Policy Engine +- Added Phase 12: Vault Seal/Unseal Lifecycle +- Added Phase 13: Token Delegation and pgcred Access Grants +- Updated implementation order to include phases 10–13 + +**No code changes.** Documentation only. + +--- + ### 2026-03-15 — Makefile: docker image cleanup **Task:** Ensure `make clean` removes Docker build images; add dedicated `docker-clean` target. diff --git a/PROJECT_PLAN.md b/PROJECT_PLAN.md index ab5018f..8f695af 100644 --- a/PROJECT_PLAN.md +++ b/PROJECT_PLAN.md @@ -5,7 +5,19 @@ See ARCHITECTURE.md for design rationale. --- -## Phase 0 — Repository Bootstrap +## Status + +**v1.0.0 tagged (2026-03-15). All phases complete.** + +All packages pass `go test ./...`; `golangci-lint run ./...` clean. +See PROGRESS.md for the detailed development log. + +Phases 0–9 match the original plan. Phases 10–13 document significant +features implemented beyond the original plan scope. + +--- + +## Phase 0 — Repository Bootstrap **[COMPLETE]** ### Step 0.1: Go module and dependency setup **Acceptance criteria:** @@ -23,7 +35,7 @@ See ARCHITECTURE.md for design rationale. --- -## Phase 1 — Foundational Packages +## Phase 1 — Foundational Packages **[COMPLETE]** ### Step 1.1: `internal/model` — shared data types **Acceptance criteria:** @@ -69,7 +81,7 @@ See ARCHITECTURE.md for design rationale. --- -## Phase 2 — Authentication Core +## Phase 2 — Authentication Core **[COMPLETE]** ### Step 2.1: `internal/token` — JWT issuance and validation **Acceptance criteria:** @@ -107,7 +119,7 @@ See ARCHITECTURE.md for design rationale. --- -## Phase 3 — HTTP Server +## Phase 3 — HTTP Server **[COMPLETE]** ### Step 3.1: `internal/middleware` — HTTP middleware **Acceptance criteria:** @@ -143,6 +155,7 @@ See ARCHITECTURE.md for design rationale. - `POST /v1/auth/totp/confirm` — confirms TOTP enrollment - `DELETE /v1/auth/totp` — admin; removes TOTP from account - `GET|PUT /v1/accounts/{id}/pgcreds` — get/set Postgres credentials +- `GET /v1/pgcreds` — list all accessible credentials (owned + granted) - Credential fields (password hash, TOTP secret, Postgres password) are **never** included in any API response - Tests: each endpoint happy path; auth middleware applied correctly; invalid @@ -160,7 +173,7 @@ See ARCHITECTURE.md for design rationale. --- -## Phase 4 — Admin CLI +## Phase 4 — Admin CLI **[COMPLETE]** ### Step 4.1: `cmd/mciasctl` — admin CLI **Acceptance criteria:** @@ -177,6 +190,7 @@ See ARCHITECTURE.md for design rationale. - `mciasctl role revoke -id UUID -role ROLE` - `mciasctl token issue -id UUID` (system accounts) - `mciasctl token revoke -jti JTI` + - `mciasctl pgcreds list` - `mciasctl pgcreds set -id UUID -host H -port P -db D -user U` - `mciasctl pgcreds get -id UUID` - `mciasctl auth login` @@ -191,7 +205,7 @@ See ARCHITECTURE.md for design rationale. --- -## Phase 5 — End-to-End Tests and Hardening +## Phase 5 — End-to-End Tests and Hardening **[COMPLETE]** ### Step 5.1: End-to-end test suite **Acceptance criteria:** @@ -228,7 +242,7 @@ See ARCHITECTURE.md for design rationale. --- -## Phase 6 — mciasdb: Database Maintenance Tool +## Phase 6 — mciasdb: Database Maintenance Tool **[COMPLETE]** See ARCHITECTURE.md §16 for full design rationale, trust model, and command surface. @@ -314,9 +328,7 @@ surface. --- ---- - -## Phase 7 — gRPC Interface +## Phase 7 — gRPC Interface **[COMPLETE]** See ARCHITECTURE.md §17 for full design rationale, proto definitions, and transport security requirements. @@ -324,7 +336,8 @@ transport security requirements. ### Step 7.1: Protobuf definitions and generated code **Acceptance criteria:** - `proto/mcias/v1/` directory contains `.proto` files for all service groups: - `auth.proto`, `token.proto`, `account.proto`, `admin.proto` + `auth.proto`, `token.proto`, `account.proto`, `policy.proto`, `admin.proto`, + `common.proto` - All RPC methods mirror the REST API surface (see ARCHITECTURE.md §8 and §17) - `proto/generate.go` contains a `//go:generate protoc ...` directive that produces Go stubs under `gen/mcias/v1/` using `protoc-gen-go` and @@ -357,10 +370,11 @@ transport security requirements. - gRPC server uses the same TLS certificate and key as the REST server (loaded from config); minimum TLS 1.2 enforced via `tls.Config` - Unary server interceptor chain: - 1. Request logger (method name, peer IP, status, duration) - 2. Auth interceptor (extracts Bearer token, validates, injects claims into + 1. Sealed interceptor (blocks all RPCs when vault sealed, except Health) + 2. Request logger (method name, peer IP, status, duration) + 3. Auth interceptor (extracts Bearer token, validates, injects claims into `context.Context`) - 3. Rate-limit interceptor (per-IP token bucket, same parameters as REST) + 4. Rate-limit interceptor (per-IP token bucket, same parameters as REST) - No credential material logged by any interceptor - Tests: interceptor chain applied correctly; rate-limit triggers after burst @@ -396,7 +410,7 @@ transport security requirements. --- -## Phase 8 — Operational Artifacts +## Phase 8 — Operational Artifacts **[COMPLETE]** See ARCHITECTURE.md §18 for full design rationale and artifact inventory. @@ -461,7 +475,10 @@ See ARCHITECTURE.md §18 for full design rationale and artifact inventory. - `generate` — `go generate ./...` (proto stubs from Phase 7) - `man` — build compressed man pages - `install` — run `dist/install.sh` - - `clean` — remove `bin/` and generated artifacts + - `docker` — `docker build -t mcias:$(VERSION) -t mcias:latest .` + - `docker-clean` — remove local `mcias:$(VERSION)` and `mcias:latest` images; + prune dangling images with the mcias label + - `clean` — remove `bin/`, compressed man pages, and local Docker images - `dist` — build release tarballs for linux/amd64 and linux/arm64 (using `GOOS`/`GOARCH` cross-compilation) - `make build` works from a clean checkout after `go mod download` @@ -483,13 +500,10 @@ See ARCHITECTURE.md §18 for full design rationale and artifact inventory. - `dist/mcias.conf.docker.example` — config template suitable for container deployment: `listen_addr = "0.0.0.0:8443"`, `grpc_addr = "0.0.0.0:9443"`, `db_path = "/data/mcias.db"`, TLS cert/key paths under `/etc/mcias/` -- `Makefile` gains a `docker` target: `docker build -t mcias:$(VERSION) .` - where `VERSION` defaults to the output of `git describe --tags --always` - Tests: - `docker build .` completes without error (run in CI if Docker available; skip gracefully if not) - `docker run --rm mcias:latest mciassrv --help` exits 0 - - Image size documented in PROGRESS.md (target: under 50 MB) ### Step 8.7: Documentation **Acceptance criteria:** @@ -501,7 +515,7 @@ See ARCHITECTURE.md §18 for full design rationale and artifact inventory. --- -## Phase 9 — Client Libraries +## Phase 9 — Client Libraries **[COMPLETE]** See ARCHITECTURE.md §19 for full design rationale, API surface, and per-language implementation notes. @@ -606,6 +620,130 @@ implementation notes. --- +## Phase 10 — Web UI (HTMX) **[COMPLETE]** + +Not in the original plan. Implemented alongside and after Phase 3. + +See ARCHITECTURE.md §8 (Web Management UI) for design details. + +### Step 10.1: `internal/ui` — HTMX web interface +**Acceptance criteria:** +- Go `html/template` pages embedded at compile time via `web/embed.go` +- CSRF protection: HMAC-signed double-submit cookie (`mcias_csrf`) +- Session: JWT stored as `HttpOnly; Secure; SameSite=Strict` cookie +- Security headers: `Content-Security-Policy: default-src 'self'`, + `X-Frame-Options: DENY`, `Referrer-Policy: strict-origin` +- Pages: login, dashboard, account list/detail, role editor, tag editor, + pgcreds, audit log viewer, policy rules, user profile, service-accounts +- HTMX partial-page updates for mutations (role updates, tag edits, policy + toggles, access grants) +- Empty-state handling on all list pages (zero records case tested) + +### Step 10.2: Swagger UI at `/docs` +**Acceptance criteria:** +- `GET /docs` serves Swagger UI for `openapi.yaml` +- swagger-ui-bundle.js and swagger-ui.css bundled locally in `web/static/` + (CDN blocked by CSP `default-src 'self'`) +- `GET /docs/openapi.yaml` serves the OpenAPI spec +- `openapi.yaml` kept in sync with REST API surface + +--- + +## Phase 11 — Authorization Policy Engine **[COMPLETE]** + +Not in the original plan (CLI subcommands for policy were planned in Phase 4, +but the engine itself was not a discrete plan phase). + +See ARCHITECTURE.md §20 for full design, evaluation algorithm, and built-in +default rules. + +### Step 11.1: `internal/policy` — in-process ABAC engine +**Acceptance criteria:** +- Pure evaluation: `Evaluate(input PolicyInput, rules []Rule) (Effect, *Rule)` +- Deny-wins: any explicit deny overrides all allows +- Default-deny: no matching rule → deny +- Built-in default rules (IDs -1 … -7) compiled in; reproduce previous + binary admin/non-admin behavior exactly; cannot be disabled via API +- Match fields: roles, account types, subject UUID, actions, resource type, + owner-matches-subject, service names, required tags (all ANDed; zero value + = wildcard) +- Temporal constraints on DB-backed rules: `not_before`, `expires_at` +- `Engine` wrapper: caches rule set in memory; reloads on policy mutations +- Tests: all built-in rules; deny-wins over allow; default-deny fallback; + temporal filtering; concurrent access + +### Step 11.2: Middleware and REST integration +**Acceptance criteria:** +- `RequirePolicy(engine, action, resourceType)` middleware replaces + `RequireRole("admin")` where policy-gated +- Every explicit deny produces a `policy_deny` audit event +- REST endpoints: `GET|POST /v1/policy/rules`, `GET|PATCH|DELETE /v1/policy/rules/{id}` +- DB schema: `policy_rules` and `account_tags` tables (migrations 000004, + 000006) +- `PATCH /v1/policy/rules/{id}` supports updating `priority`, `enabled`, + `not_before`, `expires_at` + +--- + +## Phase 12 — Vault Seal/Unseal Lifecycle **[COMPLETE]** + +Not in the original plan. + +See ARCHITECTURE.md §8 (Vault Endpoints) for the API surface. + +### Step 12.1: `internal/vault` — master key lifecycle +**Acceptance criteria:** +- Thread-safe `Vault` struct with `sync.RWMutex`-protected state +- Methods: `IsSealed()`, `Unseal(passphrase)`, `Seal()`, `MasterKey()`, + `PrivKey()`, `PubKey()` +- `Seal()` zeroes all key material before nilling (memguard-style cleanup) +- `DeriveFromPassphrase()` and `DecryptSigningKey()` extracted to `derive.go` + for reuse by unseal handlers +- Tests: state transitions; key zeroing verified; concurrent read/write safety + +### Step 12.2: REST and UI integration +**Acceptance criteria:** +- `POST /v1/vault/unseal` — rate-limited (3/s burst 5); derives key, unseals +- `GET /v1/vault/status` — always accessible; returns `{"sealed": bool}` +- `POST /v1/vault/seal` — admin only; zeroes key material +- `GET /v1/health` returns `{"status":"sealed"}` when sealed +- All other `/v1/*` endpoints return 503 `vault_sealed` when sealed +- UI redirects all paths to `/unseal` when sealed (except `/static/`) +- gRPC: `sealedInterceptor` first in chain; blocks all RPCs except Health +- Startup: server may start in sealed state if passphrase env var is absent +- Audit events: `vault_sealed`, `vault_unsealed` + +--- + +## Phase 13 — Token Delegation and pgcred Access Grants **[COMPLETE]** + +Not in the original plan. + +See ARCHITECTURE.md §21 (Token Issuance Delegation) for design details. + +### Step 13.1: Service account token delegation +**Acceptance criteria:** +- DB migration 000008: `service_account_delegates` table +- `POST /accounts/{id}/token/delegates` — admin grants delegation +- `DELETE /accounts/{id}/token/delegates/{grantee}` — admin revokes delegation +- `POST /accounts/{id}/token` — accepts admin or delegate (not admin-only) +- One-time token download: nonce stored in `sync.Map` with 5-minute TTL; + `GET /token/download/{nonce}` serves token as attachment, deletes nonce +- `/service-accounts` page for non-admin delegates +- Audit events: `token_delegate_granted`, `token_delegate_revoked` + +### Step 13.2: pgcred fine-grained access grants +**Acceptance criteria:** +- DB migration 000005: `pgcred_access_grants` table +- `POST /accounts/{id}/pgcreds/access` — owner grants read access to grantee +- `DELETE /accounts/{id}/pgcreds/access/{grantee}` — owner revokes access +- `GET /v1/pgcreds` — lists all credentials accessible to caller (owned + + granted); includes credential ID for reference +- Grantees may view connection metadata; password is never decrypted for them +- Audit events: `pgcred_access_granted`, `pgcred_access_revoked` + +--- + ## Implementation Order ``` @@ -618,6 +756,11 @@ Phase 0 → Phase 1 (1.1, 1.2, 1.3, 1.4 in parallel or sequence) → Phase 7 (7.1 → 7.2 → 7.3 → 7.4 → 7.5 → 7.6) → Phase 8 (8.1 → 8.2 → 8.3 → 8.4 → 8.5 → 8.6) → Phase 9 (9.1 → 9.2 → 9.3 → 9.4 → 9.5 → 9.6) + → Phase 10 (interleaved with Phase 3 and later phases) + → Phase 11 (interleaved with Phase 3–4) + → Phase 12 (post Phase 3) + → Phase 13 (post Phase 3 and 11) ``` Each step must have passing tests before the next step begins. +All phases complete as of v1.0.0 (2026-03-15). diff --git a/internal/ui/handlers_policy.go b/internal/ui/handlers_policy.go index eedbdfa..6795b47 100644 --- a/internal/ui/handlers_policy.go +++ b/internal/ui/handlers_policy.go @@ -129,46 +129,69 @@ func (u *UIServer) handleCreatePolicyRule(w http.ResponseWriter, r *http.Request priority = p } - effectStr := r.FormValue("effect") - if effectStr != string(policy.Allow) && effectStr != string(policy.Deny) { - u.renderError(w, r, http.StatusBadRequest, "effect must be 'allow' or 'deny'") - return - } + var ruleJSON []byte - body := policy.RuleBody{ - Effect: policy.Effect(effectStr), - } - - // Multi-value fields. - if roles := r.Form["roles"]; len(roles) > 0 { - body.Roles = roles - } - if types := r.Form["account_types"]; len(types) > 0 { - body.AccountTypes = types - } - if actions := r.Form["actions"]; len(actions) > 0 { - acts := make([]policy.Action, len(actions)) - for i, a := range actions { - acts[i] = policy.Action(a) + if rawJSON := strings.TrimSpace(r.FormValue("rule_json")); rawJSON != "" { + // JSON mode: parse and re-marshal to normalise and validate the input. + var body policy.RuleBody + if err := json.Unmarshal([]byte(rawJSON), &body); err != nil { + u.renderError(w, r, http.StatusBadRequest, fmt.Sprintf("invalid rule JSON: %v", err)) + return + } + if body.Effect != policy.Allow && body.Effect != policy.Deny { + u.renderError(w, r, http.StatusBadRequest, "rule JSON must include effect 'allow' or 'deny'") + return + } + var err error + ruleJSON, err = json.Marshal(body) + if err != nil { + u.renderError(w, r, http.StatusInternalServerError, "internal error") + return + } + } else { + // Form mode: build RuleBody from individual fields. + effectStr := r.FormValue("effect") + if effectStr != string(policy.Allow) && effectStr != string(policy.Deny) { + u.renderError(w, r, http.StatusBadRequest, "effect must be 'allow' or 'deny'") + return } - body.Actions = acts - } - if resType := r.FormValue("resource_type"); resType != "" { - body.ResourceType = policy.ResourceType(resType) - } - body.SubjectUUID = strings.TrimSpace(r.FormValue("subject_uuid")) - body.OwnerMatchesSubject = r.FormValue("owner_matches_subject") == "1" - if svcNames := r.FormValue("service_names"); svcNames != "" { - body.ServiceNames = splitCommas(svcNames) - } - if tags := r.FormValue("required_tags"); tags != "" { - body.RequiredTags = splitCommas(tags) - } - ruleJSON, err := json.Marshal(body) - if err != nil { - u.renderError(w, r, http.StatusInternalServerError, "internal error") - return + body := policy.RuleBody{ + Effect: policy.Effect(effectStr), + } + + // Multi-value fields. + if roles := r.Form["roles"]; len(roles) > 0 { + body.Roles = roles + } + if types := r.Form["account_types"]; len(types) > 0 { + body.AccountTypes = types + } + if actions := r.Form["actions"]; len(actions) > 0 { + acts := make([]policy.Action, len(actions)) + for i, a := range actions { + acts[i] = policy.Action(a) + } + body.Actions = acts + } + if resType := r.FormValue("resource_type"); resType != "" { + body.ResourceType = policy.ResourceType(resType) + } + body.SubjectUUID = strings.TrimSpace(r.FormValue("subject_uuid")) + body.OwnerMatchesSubject = r.FormValue("owner_matches_subject") == "1" + if svcNames := r.FormValue("service_names"); svcNames != "" { + body.ServiceNames = splitCommas(svcNames) + } + if tags := r.FormValue("required_tags"); tags != "" { + body.RequiredTags = splitCommas(tags) + } + + var err error + ruleJSON, err = json.Marshal(body) + if err != nil { + u.renderError(w, r, http.StatusInternalServerError, "internal error") + return + } } // Parse optional time-scoped validity window from datetime-local inputs. diff --git a/web/templates/fragments/policy_form.html b/web/templates/fragments/policy_form.html index 848de9d..6da9998 100644 --- a/web/templates/fragments/policy_form.html +++ b/web/templates/fragments/policy_form.html @@ -1,87 +1,140 @@ {{define "policy_form"}} +
+ + +
-
- - - -
-
-
- - + +
-
- - + + + + + + + +
+
+ + +
+
+
+ +
- -
- - -
-
-
- - +
+
+ + +
+
+ + +
-
- - +
+
+ + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+ + +
-
-
- - + + -
- -
-
-
- - -
-
- - +
+ + +
+
+
+ + +
+
+ + +
+ + {{end}}