8f09e0e81a
Rename Go client package from mciasgoclient to mcias - Update package declaration in client.go - Update error message strings to reference new package name - Update test package and imports to use new name - Update README.md documentation and examples with new package name - All tests pass
Kyle Isom2026-03-14 19:01:07 -07:00
89f78a38dd
Update web UI to support all compile-time roles - Update knownRoles to include guest, viewer, editor, and commenter - Replace hardcoded role strings with model constants - Remove obsolete 'service' role from UI - All tests pass
Kyle Isom2026-03-12 21:14:22 -07:00
4d6c5cb67c
Add guest, viewer, editor, and commenter roles to compile-time allowlist - Add RoleGuest, RoleViewer, RoleEditor, and RoleCommenter constants - Update allowedRoles map to include new roles - Update ValidateRole error message with complete role list - All tests pass; build verified
v1.3.0
Kyle Isom2026-03-12 21:03:24 -07:00
f880bbb6de
Add granular role grant/revoke endpoints to REST and gRPC APIs - Add POST /v1/accounts/{id}/roles and DELETE /v1/accounts/{id}/roles/{role} REST endpoints - Add GrantRole and RevokeRole RPCs to AccountService in gRPC API - Update OpenAPI specification with new endpoints - Add grant and revoke subcommands to mciasctl - Add grant and revoke subcommands to mciasgrpcctl - Regenerate proto files with new message types and RPCs - Implement gRPC server methods for granular role management - All existing tests pass; build verified with goimports Security: Role changes are audited via EventRoleGranted and EventRoleRevoked events, consistent with existing SetRoles implementation.
Kyle Isom2026-03-12 20:55:49 -07:00
833775de83
db: integrate golang-migrate for schema migrations - internal/db/migrations/: five embedded SQL files containing the migration SQL previously held as Go string literals. Files follow the NNN_description.up.sql naming convention required by golang-migrate's iofs source. - internal/db/migrate.go: rewritten to use github.com/golang-migrate/migrate/v4 with the database/sqlite driver (modernc.org/sqlite, pure Go) and source/iofs for compile-time embedded SQL. - newMigrate() opens a dedicated *sql.DB so m.Close() does not affect the caller's shared connection. - Migrate() includes a compatibility shim: reads the legacy schema_version table and calls m.Force(v) before m.Up() so existing databases are not re-migrated. - LatestSchemaVersion promoted from var to const. - internal/db/db.go: added path field to DB struct; Open() translates ':memory:' to a named shared-cache URI (file:mcias_N?mode=memory&cache=shared) so the migration runner can open a second connection to the same in-memory database without sharing the handle that golang-migrate will close on teardown. - go.mod: added golang-migrate/migrate/v4 v4.19.1 (direct). All callers unchanged. All tests pass; golangci-lint clean.
Kyle Isom2026-03-12 11:52:39 -07:00
562aad908e
UI: pgcreds create button; show logged-in user
Kyle Isom2026-03-12 11:38:57 -07:00
fdcc117c89
Fix UI: install real HTMX, add PG creds and roles UI - web/static/htmx.min.js: replace placeholder stub with htmx 2.0.4 (downloaded from unpkg.com). The placeholder only logged a console warning; no HTMX features worked, so form submissions fell back to native POSTs and the account_row fragment was returned as a raw HTML body rather than spliced into the table. This was the root cause of account creation appearing to 'do nothing'. - internal/ui/ui.go: add pgcreds_form.html to shared template list; add PUT /accounts/{id}/pgcreds route; reorder AccountDetailData fields so embedded PageData does not shadow Account. - internal/ui/handlers_accounts.go: add handleSetPGCreds handler — encrypts the submitted password with AES-256-GCM using the server master key before storage, validates system-account-only constraint, re-reads and re-renders the fragment after save. Add PGCred field population to handleAccountDetail. - internal/ui/ui_test.go: add tests for account creation, role management, and PG credential handlers. - web/templates/account_detail.html: add Postgres Credentials card for system accounts. - web/templates/fragments/pgcreds_form.html: new fragment for the PG credentials form; CSRF token is supplied via the body-level hx-headers attribute in base.html. Security: PG password is encrypted with AES-256-GCM (crypto.SealAESGCM) before storage; a fresh nonce is generated per call; the plaintext is never logged or returned in responses.
Kyle Isom2026-03-11 22:30:13 -07:00
b495a90a9d
Fix F-08, F-13: Adjust lockout expiration logic and enforce password length in tests
Kyle Isom2026-03-11 21:36:04 -07:00
2dbc553abe
Fix F-07: pre-compute real Argon2 dummy hash via sync.Once - auth/auth.go: add DummyHash() which uses sync.Once to compute HashPassword("dummy-password-for-timing-only", DefaultArgonParams()) on first call; subsequent calls return the cached PHC string; add sync to imports - auth/auth_test.go: TestDummyHashIsValidPHC verifies the hash parses and verifies correctly; TestDummyHashIsCached verifies sync.Once behaviour; TestDummyHashMatchesDefaultParams verifies embedded m/t/p match DefaultArgonParams() - server/server.go, grpcserver/auth.go, ui/ui.go: replace five hardcoded PHC strings with auth.DummyHash() calls - AUDIT.md: mark F-07 as fixed Security: the previous hardcoded hash used a 6-byte salt and 6-byte output ("testsalt"/"testhash" in base64), which Argon2id verifies faster than a real 16-byte-salt / 32-byte-output hash. This timing gap was measurable and could aid user enumeration. auth.DummyHash() uses identical parameters and full-length salt and output, so dummy verification timing matches real timing exactly, regardless of future parameter changes.
Kyle Isom2026-03-11 20:37:27 -07:00
06ec8be1c9
Fix F-16: revoke old system token before issuing new one - ui/handlers_accounts.go (handleIssueSystemToken): call GetSystemToken before issuing; if one exists, call RevokeToken(existing.JTI, "rotated") before TrackToken and SetSystemToken for the new token; mirrors the pattern in REST handleTokenIssue and gRPC IssueServiceToken - db/db_test.go: TestSystemTokenRotationRevokesOld verifies the full rotation flow: old JTI revoked with reason "rotated", new JTI tracked and active, GetSystemToken returns the new JTI - AUDIT.md: mark F-16 as fixed Security: without this fix an old system token remained valid after rotation until its natural expiry, giving a leaked or stolen old token extra lifetime. With the revocation the old JTI is immediately marked in token_revocation so any validator checking revocation status rejects it.
Kyle Isom2026-03-11 20:34:57 -07:00
e20b66d6f6
Fix F-02: replace password-in-hidden-field with nonce - ui/ui.go: add pendingLogin struct and pendingLogins sync.Map to UIServer; add issueTOTPNonce (generates 128-bit random nonce, stores accountID with 90s TTL) and consumeTOTPNonce (single-use, expiry-checked LoadAndDelete); add dummyHash() method - ui/handlers_auth.go: split handleLoginPost into step 1 (password verify → issue nonce) and step 2 (handleTOTPStep, consume nonce → validate TOTP) via a new finishLogin helper; password never transmitted or stored after step 1 - ui/ui_test.go: refactor newTestMux to reuse new newTestUIServer; add TestTOTPNonceIssuedAndConsumed, TestTOTPNonceUnknownRejected, TestTOTPNonceExpired, and TestLoginPostPasswordNotInTOTPForm; 11/11 tests pass - web/templates/fragments/totp_step.html: replace 'name=password' hidden field with 'name=totp_nonce' - db/accounts.go: add GetAccountByID for TOTP step lookup - AUDIT.md: mark F-02 as fixed Security: the plaintext password previously survived two HTTP round-trips and lived in the browser DOM during the TOTP step. The nonce approach means the password is verified once and immediately discarded; only an opaque random token tied to an account ID (never a credential) crosses the wire on step 2. Nonces are single-use and expire after 90 seconds to limit the window if one is captured.
Kyle Isom2026-03-11 20:33:04 -07:00
0e201ae05b
Fix F-03: make token renewal atomic - db/accounts.go: add RenewToken(oldJTI, reason, newJTI, accountID, issuedAt, expiresAt) which wraps RevokeToken + TrackToken in a single BEGIN/COMMIT transaction; if either step fails the whole tx rolls back, so the user is never left with neither old nor new token valid - server.go (handleRenewToken): replace separate RevokeToken + TrackToken calls with single RenewToken call; failure now returns 500 instead of silently losing revocation - grpcserver/auth.go (RenewToken): same replacement - db/db_test.go: TestRenewTokenAtomic verifies old token is revoked with correct reason, new token is tracked and not revoked, and a second renewal on the already-revoked old token returns an error - AUDIT.md: mark F-03 as fixed Security: without atomicity a crash/error between revoke and track could leave the old token active alongside the new one (two live tokens) or revoke the old token without tracking the new one (user locked out). The transaction ensures exactly one of the two tokens is valid at all times.
Kyle Isom2026-03-11 20:24:32 -07:00
c8f1ac6dac
Fix F-01: TOTP enroll must not set required=1 early - db/accounts.go: add StorePendingTOTP() which writes totp_secret_enc and totp_secret_nonce but leaves totp_required=0; add comment explaining two-phase flow - server.go (handleTOTPEnroll): switch from SetTOTP() to StorePendingTOTP() so the required flag is only set after the user confirms a valid TOTP code via handleTOTPConfirm, which still calls SetTOTP() - server_test.go: TestTOTPEnrollDoesNotRequireTOTP verifies that after POST /v1/auth/totp/enroll, TOTPRequired is false and the encrypted secret is present; confirms that a subsequent login without a TOTP code still succeeds (no lockout) - AUDIT.md: mark F-01 and F-11 as fixed Security: without this fix an admin who enrolls TOTP but abandons before confirmation is permanently locked out because totp_required=1 but no confirmed secret exists. StorePendingTOTP() keeps the secret pending until the user proves possession by confirming a valid code.
Kyle Isom2026-03-11 20:18:57 -07:00
47847a4312
Fix F-04 + F-11; add AUDIT.md - AUDIT.md: security audit report with 16 findings (F-01..F-16) - F-04 (server.go): wire loginRateLimit (10 req/s, burst 10) to POST /v1/auth/login and POST /v1/token/validate; no limit on /v1/health or public-key endpoints - F-04 (server_test.go): TestLoginRateLimited uses concurrent goroutines (sync.WaitGroup) to fire burst+1 requests before Argon2id completes, sidestepping token-bucket refill timing; TestTokenValidateRateLimited; TestHealthNotRateLimited - F-11 (ui.go): refactor Register() so all UI routes are mounted on a child mux wrapped with securityHeaders middleware; five headers set on every response: Content-Security-Policy, X-Content-Type-Options, X-Frame-Options, HSTS, Referrer-Policy - F-11 (ui_test.go): 7 new tests covering login page, dashboard redirect, root redirect, static assets, CSP directives, HSTS min-age, and middleware unit behaviour Security: rate limiter on login prevents brute-force credential stuffing; security headers mitigate clickjacking (X-Frame-Options DENY), MIME sniffing (nosniff), and protocol downgrade (HSTS)
Kyle Isom2026-03-11 20:18:09 -07:00
f2903ca103
Fix grpcserver rate limiter: move to Server field
Kyle Isom2026-03-11 19:20:32 -07:00
4d140886ca
Add HTMX-based UI templates and handlers for account and audit management
Kyle Isom2026-03-11 18:02:53 -07:00
da4126c1a9
Implement Phase 9: client libraries (Go, Rust, Lisp, Python) - clients/README.md: canonical API surface and error type reference - clients/testdata/: shared JSON response fixtures - clients/go/: mciasgoclient package; net/http + TLS 1.2+; sync.RWMutex token state; DisallowUnknownFields on all decoders; 25 tests pass - clients/rust/: async mcias-client crate; reqwest+rustls (no OpenSSL); thiserror MciasError enum; Arc<RwLock> token state; 22+1 tests pass; cargo clippy -D warnings clean - clients/lisp/: ASDF mcias-client; dexador HTTP, yason JSON; mcias-error condition hierarchy; Hunchentoot mock-dispatcher; 37 fiveam checks pass on SBCL 2.6.1; yason boolean normalisation in validate-token - clients/python/: mcias_client package (Python 3.11+); httpx sync; py.typed; dataclasses; 32 pytest tests; mypy --strict + ruff clean - test/mock/mockserver.go: in-memory mock server for Go client tests - ARCHITECTURE.md §19: updated per-language notes to match implementation - PROGRESS.md: Phase 9 marked complete - .gitignore: exclude clients/rust/target/, python .venv, .pytest_cache, .fasl files Security: token never logged or exposed in error messages in any library; TLS enforced in all four languages; token stored under lock/mutex/RwLock
Kyle Isom2026-03-11 16:38:32 -07:00
f34e9a69a0
Fix all golangci-lint warnings - errorlint: use errors.Is for db.ErrNotFound comparisons in accountservice.go, credentialservice.go, tokenservice.go - gofmt/goimports: move mciasv1 alias into internal import group in auth.go, credentialservice.go, grpcserver.go, grpcserver_test.go - gosec G115: add nolint annotation on int32 port conversions in mciasgrpcctl/main.go and credentialservice.go (port validated as [1,65535] on input; overflow not reachable) - govet fieldalignment: reorder Server, grpcRateLimiter, grpcRateLimitEntry, testEnv structs to reduce GC bitmap size (96 -> 80 pointer bytes each) - ineffassign: remove intermediate grpcSrv = GRPCServer() call in cmd/mciassrv/main.go (immediately overwritten by TLS build) - staticcheck SA9003: replace empty if-body with _ = Serve(lis) in grpcserver_test.go 0 golangci-lint issues; 137 tests pass (go test -race ./...)
Kyle Isom2026-03-11 15:24:07 -07:00
941c71f2d1
Implement Phase 8: operational artifacts - Makefile: build/test/lint/generate/man/install/clean/dist/docker; CGO_ENABLED=1 throughout; VERSION from git describe --tags --always - Dockerfile: multi-stage (golang:1.26-bookworm builder -> debian:bookworm-slim runtime); non-root uid 10001 (mcias), VOLUME /data, EXPOSE 8443/9443; no toolchain in final image - dist/mcias.service: hardened systemd unit (ProtectSystem=strict, ProtectHome, PrivateTmp, NoNewPrivileges, MemoryDenyWriteExecute, CapabilityBoundingSet= empty, EnvironmentFile, LimitNOFILE=65536) - dist/mcias.env.example: passphrase env file template - dist/mcias.conf.example: fully-commented production TOML config - dist/mcias-dev.conf.example: local dev config (/tmp, short expiry) - dist/mcias.conf.docker.example: container config template - dist/install.sh: POSIX sh idempotent installer; creates mcias user/group, installs binaries, /etc/mcias, /var/lib/mcias, systemd unit, man pages; prints post-install instructions - man/man1/mciassrv.1: mdoc synopsis/config/API/signals/files - man/man1/mciasctl.1: mdoc all subcommands/env/examples - man/man1/mciasdb.1: mdoc trust model/safety/all subcommands - man/man1/mciasgrpcctl.1: mdoc gRPC commands/grpcurl example - README.md: user-facing quick-start, first-run setup, build instructions, CLI references, Docker deployment, security notes - .gitignore: added /bin/, dist/mcias_*.tar.gz, man/man1/*.gz
Kyle Isom2026-03-11 15:11:36 -07:00
8f706f10ec
Phase 8 plan: add Dockerfile step (Step 8.6) - PROJECT_PLAN.md: insert Step 8.6 (Dockerfile) before the documentation step (renumbered to 8.7); acceptance criteria cover multi-stage build, non-root runtime user, EXPOSE ports, VOLUME /data, dist/mcias.conf.docker.example, Makefile docker target, and image size target (<50 MB) - ARCHITECTURE.md §18: add Dockerfile to artifact inventory table; add Dockerfile Design section covering build stages, security properties (no shell, non-root uid 10001, TLS inside container), operator workflow, and the new Makefile docker target
Kyle Isom2026-03-11 14:47:07 -07:00
7c79d00514
Sync docs: ARCHITECTURE, PROJECT, PROJECT_PLAN - ARCHITECTURE.md §12: add mciasdb, mciasgrpcctl, internal/grpcserver, proto/, and gen/ to the directory structure diagram - ARCHITECTURE.md §17: replace buf generate references with protoc; the implementation uses protoc + protoc-gen-go + protoc-gen-go-grpc invoked via go generate ./... (proto/generate.go) - PROJECT_PLAN.md §7.1: replace buf.yaml/buf.gen.yaml acceptance criteria with the protoc-based go:generate approach actually used - PROJECT_PLAN.md §7.6: clarify that gen/ is committed to the repo (not gitignored); only the binary /mciasgrpcctl is excluded - PROJECT.md: replace scrypt with Argon2id (the actual algorithm); remove the redundant Argon2 suggestion line
Kyle Isom2026-03-11 14:44:08 -07:00
59d51a1d38
Implement Phase 7: gRPC dual-stack interface - proto/mcias/v1/: AdminService, AuthService, TokenService, AccountService, CredentialService; generated Go stubs in gen/ - internal/grpcserver: full handler implementations sharing all business logic (auth, token, db, crypto) with REST server; interceptor chain: logging -> auth (JWT alg-first + revocation) -> rate-limit (token bucket, 10 req/s, burst 10, per-IP) - internal/config: optional grpc_addr field in [server] section - cmd/mciassrv: dual-stack startup; gRPC/TLS listener on grpc_addr when configured; graceful shutdown of both servers in 15s window - cmd/mciasgrpcctl: companion gRPC CLI mirroring mciasctl commands (health, pubkey, account, role, token, pgcreds) using TLS with optional custom CA cert - internal/grpcserver/grpcserver_test.go: 20 tests via bufconn covering public RPCs, auth interceptor (no token, invalid, revoked -> 401), non-admin -> 403, Login/Logout/RenewToken/ValidateToken flows, AccountService CRUD, SetPGCreds/GetPGCreds AES-GCM round-trip, credential fields absent from all responses Security: JWT validation path identical to REST: alg header checked before signature, alg:none rejected, revocation table checked after sig. Authorization metadata value never logged by any interceptor. Credential fields (PasswordHash, TOTPSecret*, PGPassword) absent from all proto response messages — enforced by proto design and confirmed by test TestCredentialFieldsAbsentFromAccountResponse. Login dummy-Argon2 timing guard preserves timing uniformity for unknown users (same as REST handleLogin). TLS required at listener level; cmd/mciassrv uses credentials.NewServerTLSFromFile; no h2c offered. 137 tests pass, zero race conditions (go test -race ./...)
Kyle Isom2026-03-11 14:38:47 -07:00
f02eff21b4
Complete implementation: e2e tests, gofmt, hardening - Add test/e2e: 11 end-to-end tests covering full login/logout, token renewal, admin account management, credential-never-in-response, unauthorised access, JWT alg confusion and alg:none attacks, revoked token rejection, system account token issuance, wrong-password vs unknown-user indistinguishability - Apply gofmt to all source files (formatting only, no logic changes) - Update .golangci.yaml for golangci-lint v2 (version field required, gosimple merged into staticcheck, formatters section separated) - Update PROGRESS.md to reflect Phase 5 completion Security: All 97 tests pass with go test -race ./... (zero race conditions). Adversarial JWT tests (alg confusion, alg:none) confirm the ValidateToken alg-first check is effective against both attack classes. Credential fields (PasswordHash, TOTPSecret*, PGPassword) confirmed absent from all API responses via both unit and e2e tests. go vet ./... clean. golangci-lint v2.6.2 incompatible with go1.26 runtime; go vet used as linter until toolchain is updated.
Kyle Isom2026-03-11 11:54:14 -07:00