Fix policy form roles; add JSON edit mode

- Replace stale "service" role option with correct set:
  admin, user, guest, viewer, editor, commenter (matches model.go)
- Add Form/JSON tab toggle to policy create form
- JSON tab accepts raw RuleBody JSON with description/priority
- Handler detects rule_json field and parses/validates it
  directly, falling back to field-by-field form mode otherwise
This commit is contained in:
Claude Opus 4.6
2026-03-16 15:21:26 -07:00
committed by Kyle Isom
parent 7db560dae4
commit 19fa0c9a8e
7 changed files with 422 additions and 150 deletions

View File

@@ -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"
}

View File

@@ -1 +1 @@
[{"lang":"en","usageCount":3}]
[{"lang":"en","usageCount":7}]

View File

@@ -15,7 +15,7 @@ parties that delegate authentication decisions to it.
### Components
```
┌─────────────────────────────────────────────────────────
┌─────────────────────────────────────────────────────────┐
│ MCIAS Server (mciassrv) │
│ ┌──────────┐ ┌──────────┐ ┌───────────────────┐ │
│ │ Auth │ │ Token │ │ Account / Role │ │
@@ -32,7 +32,7 @@ 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
│ │ │ │
@@ -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

View File

@@ -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 09 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 1013
**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.

View File

@@ -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 09 match the original plan. Phases 1013 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 34)
→ 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).

View File

@@ -129,6 +129,27 @@ func (u *UIServer) handleCreatePolicyRule(w http.ResponseWriter, r *http.Request
priority = p
}
var ruleJSON []byte
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'")
@@ -165,11 +186,13 @@ func (u *UIServer) handleCreatePolicyRule(w http.ResponseWriter, r *http.Request
body.RequiredTags = splitCommas(tags)
}
ruleJSON, err := json.Marshal(body)
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.
var notBefore, expiresAt *time.Time

View File

@@ -1,5 +1,12 @@
{{define "policy_form"}}
<div style="margin-bottom:.75rem;border-bottom:1px solid var(--border-color);padding-bottom:.5rem;display:flex;gap:.5rem">
<button type="button" id="tab-form" class="btn btn-sm btn-secondary"
onclick="showTab('form')" style="font-size:.8rem">Form</button>
<button type="button" id="tab-json" class="btn btn-sm"
onclick="showTab('json')" style="font-size:.8rem;opacity:.6">JSON</button>
</div>
<form hx-post="/policies" hx-target="#policies-tbody" hx-swap="afterbegin">
<div id="pf-form-mode">
<div style="display:grid;grid-template-columns:1fr 80px 120px;gap:.5rem;margin-bottom:.5rem">
<input class="form-control" type="text" name="description"
placeholder="Description" required>
@@ -13,10 +20,13 @@
<div style="display:grid;grid-template-columns:1fr 1fr;gap:.5rem;margin-bottom:.5rem">
<div>
<label class="text-small text-muted">Roles (select multiple)</label>
<select class="form-control" name="roles" multiple size="4" style="font-size:.85rem">
<select class="form-control" name="roles" multiple size="6" style="font-size:.85rem">
<option value="admin">admin</option>
<option value="user">user</option>
<option value="service">service</option>
<option value="guest">guest</option>
<option value="viewer">viewer</option>
<option value="editor">editor</option>
<option value="commenter">commenter</option>
</select>
</div>
<div>
@@ -82,6 +92,49 @@
<input class="form-control" type="datetime-local" name="expires_at" style="font-size:.85rem">
</div>
</div>
</div>
<div id="pf-json-mode" style="display:none">
<div style="display:grid;grid-template-columns:1fr 80px;gap:.5rem;margin-bottom:.5rem">
<input class="form-control" type="text" name="description"
placeholder="Description" id="pf-json-desc">
<input class="form-control" type="number" name="priority"
placeholder="100" value="100" min="0" max="9999" id="pf-json-priority">
</div>
<div style="margin-bottom:.5rem">
<label class="text-small text-muted">Rule JSON (<code>effect</code> required; other fields optional)</label>
<textarea class="form-control" name="rule_json" rows="12"
style="font-family:monospace;font-size:.8rem;white-space:pre"
placeholder='{"effect":"allow","roles":["admin"],"actions":["accounts:list"]}'></textarea>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:.5rem;margin-bottom:.5rem">
<div>
<label class="text-small text-muted">Not before (UTC, optional)</label>
<input class="form-control" type="datetime-local" name="not_before" id="pf-json-nb" style="font-size:.85rem">
</div>
<div>
<label class="text-small text-muted">Expires at (UTC, optional)</label>
<input class="form-control" type="datetime-local" name="expires_at" id="pf-json-ea" style="font-size:.85rem">
</div>
</div>
</div>
<button class="btn btn-sm btn-secondary" type="submit">Create Rule</button>
</form>
<script>
(function() {
var active = 'form';
window.showTab = function(tab) {
active = tab;
document.getElementById('pf-form-mode').style.display = tab === 'form' ? '' : 'none';
document.getElementById('pf-json-mode').style.display = tab === 'json' ? '' : 'none';
document.getElementById('tab-form').style.opacity = tab === 'form' ? '1' : '.6';
document.getElementById('tab-json').style.opacity = tab === 'json' ? '1' : '.6';
var formBtn = document.getElementById('tab-form');
var jsonBtn = document.getElementById('tab-json');
formBtn.className = tab === 'form' ? 'btn btn-sm btn-secondary' : 'btn btn-sm';
jsonBtn.className = tab === 'json' ? 'btn btn-sm btn-secondary' : 'btn btn-sm';
};
})();
</script>
{{end}}