Planning updates.

+ Document gRPC interface, operational artifacts, and client libraries for Phases 7–9 planning.
+ Update PROGRESS.md to reflect completed design and pending implementation.
This commit is contained in:
2026-03-11 14:15:27 -07:00
parent e63d9863b6
commit 094741b56d
3 changed files with 353 additions and 2 deletions

1
.junie/guidelines.md Symbolic link
View File

@@ -0,0 +1 @@
CLAUDE.md

View File

@@ -689,3 +689,346 @@ By default all output is human-readable text. `--json` flag switches to
newline-delimited JSON for scripting. Credential fields follow the same
`json:"-"` exclusion rules as the API — they are only printed when the
specific `get` or `pgcreds get` command is invoked, never in list output.
---
## 17. gRPC Interface (Phase 7)
### Rationale
The REST API is the primary interface and will remain so. A gRPC interface is
added as an alternate transport for clients that prefer strongly-typed stubs,
streaming, or lower per-request overhead. The two interfaces are strictly
equivalent in capability and security posture; they share all business logic
in the `internal/` packages.
gRPC is not a replacement for REST. Both listeners run concurrently. Operators
may disable the gRPC listener by omitting `grpc_addr` from config.
### Proto Package Layout
```
proto/
└── mcias/
└── v1/
├── auth.proto # Login, Logout, Renew, TOTP enroll/confirm/remove
├── token.proto # Validate, Issue, Revoke
├── account.proto # CRUD for accounts and roles
├── admin.proto # Health, public-key retrieval
└── common.proto # Shared message types (Error, Timestamp wrappers)
gen/
└── mcias/
└── v1/ # Generated Go stubs (buf generate output)
```
Generated code is committed to the repository under `gen/`. `buf generate`
is idempotent and is re-run via `go generate ./...`.
### Service Definitions (summary)
| Service | RPCs |
|---|---|
| `AuthService` | `Login`, `Logout`, `RenewToken`, `EnrollTOTP`, `ConfirmTOTP`, `RemoveTOTP` |
| `TokenService` | `ValidateToken`, `IssueServiceToken`, `RevokeToken` |
| `AccountService` | `ListAccounts`, `CreateAccount`, `GetAccount`, `UpdateAccount`, `DeleteAccount`, `GetRoles`, `SetRoles` |
| `CredentialService` | `GetPGCreds`, `SetPGCreds` |
| `AdminService` | `Health`, `GetPublicKey` |
All request/response messages follow the same credential-exclusion rules as
the JSON API: `PasswordHash`, `TOTPSecret*`, and `PGPassword` fields are
never present in any response message.
### Transport Security
- The gRPC server uses the same TLS certificate and key as the REST server.
TLS 1.2 minimum is enforced via `tls.Config` (identical to the REST server).
- Mutual TLS is out of scope for v1 but is architecturally compatible (the
`tls.Config` can be extended).
- No plaintext (h2c) mode is provided. Connecting without TLS is refused.
### Authentication and Authorization
Authentication in gRPC uses the same JWT validation logic as the REST
middleware:
1. The gRPC unary interceptor extracts the `authorization` metadata key.
2. It expects the value `Bearer <token>` (case-insensitive prefix).
3. The token is validated via `internal/token.ValidateToken` — same alg-first
check, same revocation table lookup.
4. Claims are injected into the `context.Context` for downstream handlers.
5. Admin RPCs are guarded by a second interceptor that checks the `admin` role
in the injected claims — identical to the REST `RequireRole` middleware.
A missing or invalid token returns `codes.Unauthenticated`. Insufficient role
returns `codes.PermissionDenied`. No credential material is included in error
details.
### Interceptor Chain
```
[Request Logger] → [Auth Interceptor] → [Rate Limiter] → [Handler]
```
- **Request Logger**: logs method, peer IP, status code, duration; never logs
the `authorization` metadata value.
- **Auth Interceptor**: validates Bearer JWT, injects claims. Public RPCs
(`Health`, `GetPublicKey`, `ValidateToken`) bypass auth.
- **Rate Limiter**: per-IP token bucket with the same parameters as the REST
rate limiter (10 req/s burst). Exceeding the limit returns `codes.ResourceExhausted`.
### Dual-Stack Operation
mciassrv starts both listeners in the same process:
```
┌──────────────────────────────────────────────┐
│ mciassrv process │
│ │
│ ┌────────────────┐ ┌────────────────────┐ │
│ │ REST listener │ │ gRPC listener │ │
│ │ (net/http) │ │ (google.golang. │ │
│ │ :8443 │ │ org/grpc) :8444 │ │
│ └───────┬─────────┘ └──────────┬─────────┘ │
│ └──────────────┬─────────┘ │
│ ▼ │
│ ┌─────────────────────────────┐ │
│ │ Shared: signing key, DB, │ │
│ │ config, rate-limit state │ │
│ └─────────────────────────────┘ │
└──────────────────────────────────────────────┘
```
Both listeners share a single `*db.DB` connection, the same in-memory signing
key, and the same rate-limiter state. Graceful shutdown drains both within the
configured window.
### Configuration Addition
```toml
[server]
listen_addr = "0.0.0.0:8443"
grpc_addr = "0.0.0.0:8444" # optional; omit to disable gRPC
tls_cert = "/etc/mcias/server.crt"
tls_key = "/etc/mcias/server.key"
```
### `cmd/mciasgrpcctl` — gRPC Admin CLI
An optional companion CLI (`mciasgrpcctl`) provides the same subcommands as
`mciasctl` but over gRPC. It is a thin client that wraps the generated stubs.
Auth and CA-cert flags are identical to `mciasctl`. Both CLIs can coexist;
neither depends on the other.
---
## 18. Operational Artifacts (Phase 8)
### Artifact Inventory
| Artifact | Path | Purpose |
|---|---|---|
| systemd unit | `dist/mcias.service` | Production service management |
| Environment template | `dist/mcias.env.example` | Master key and other secrets |
| Reference config | `dist/mcias.conf.example` | Annotated production config |
| Dev config | `dist/mcias-dev.conf.example` | Local development defaults |
| Install script | `dist/install.sh` | First-time setup on a Linux host |
| Man page: mciassrv | `man/man1/mciassrv.1` | Server binary reference |
| Man page: mciasctl | `man/man1/mciasctl.1` | Admin CLI reference |
| Man page: mciasdb | `man/man1/mciasdb.1` | DB tool reference |
| Man page: mciasgrpcctl | `man/man1/mciasgrpcctl.1` | gRPC CLI reference |
| Makefile | `Makefile` | Build, test, lint, install, release targets |
### systemd Unit Design
The service unit applies a conservative sandboxing profile:
- `User=mcias` / `Group=mcias` — no root privileges required
- `ProtectSystem=strict` — filesystem read-only except declared `ReadWritePaths`
- `ReadWritePaths=/var/lib/mcias` — SQLite database directory only
- `PrivateTmp=true` — isolated `/tmp`
- `NoNewPrivileges=true` — seccomp/capability escalation blocked
- `CapabilityBoundingSet=` — empty; no Linux capabilities needed (port ≥ 1024)
- `EnvironmentFile=/etc/mcias/env` — secrets injected from file, not inline
The unit does not start the service on install. Operators must run
`systemctl enable --now mcias` explicitly after verifying configuration.
### Filesystem Layout (post-install)
```
/usr/local/bin/
mciassrv
mciasctl
mciasdb
mciasgrpcctl (if gRPC phase installed)
/etc/mcias/
mcias.conf (config file; mode 0640, owner root:mcias)
env (environment file with MCIAS_MASTER_PASSPHRASE; mode 0640)
server.crt (TLS certificate; mode 0644)
server.key (TLS private key; mode 0640, owner root:mcias)
/var/lib/mcias/
mcias.db (SQLite database; mode 0660, owner mcias:mcias)
/usr/share/man/man1/
mciassrv.1.gz
mciasctl.1.gz
mciasdb.1.gz
mciasgrpcctl.1.gz
```
### Makefile Targets
| Target | Action |
|---|---|
| `build` | Compile all binaries to `bin/` using current GOOS/GOARCH |
| `test` | `go test -race ./...` |
| `lint` | `golangci-lint run ./...` |
| `generate` | `go generate ./...` (re-generates proto stubs) |
| `man` | Build man pages; compress to `.gz` in `man/` |
| `install` | Run `dist/install.sh` |
| `clean` | Remove `bin/`, `gen/`, compressed man pages |
| `dist` | Cross-compile release tarballs for linux/amd64 and linux/arm64 |
### Upgrade Path
The install script is idempotent. Running it again after a new release:
1. Overwrites binaries in `/usr/local/bin/`
2. Does **not** overwrite `/etc/mcias/mcias.conf` or `/etc/mcias/env` (backs
them up with a `.bak` suffix and skips if unchanged)
3. Does **not** run `mciasdb schema migrate` automatically — the operator
must do this manually before restarting the service
---
## 19. Client Libraries (Phase 9)
### Design Goals
Client libraries exist to make it easy for relying-party applications to
authenticate users via MCIAS without needing to understand JWT handling, TLS
configuration, or the HTTP API wire format. Each library:
1. Exposes the canonical API surface (defined in `clients/README.md`).
2. Handles token storage, renewal, and error classification internally.
3. Enforces TLS (no plaintext) and validates the server certificate by default.
4. Never logs or exposes credential material.
5. Is independently versioned and testable.
### Canonical API Surface
Every language implementation must expose:
```
Client(server_url, [ca_cert], [token])
# Authentication
client.login(username, password, [totp_code]) → (token, expires_at)
client.logout() → void
client.renew_token() → (token, expires_at)
# Token operations
client.validate_token(token) → claims
client.get_public_key() → jwk
# Health
client.health() → void # raises/errors on failure
# Account management (admin)
client.create_account(username, type) → account
client.list_accounts() → [account]
client.get_account(id) → account
client.update_account(id, updates) → account
client.delete_account(id) → void
# Role management (admin)
client.get_roles(account_id) → [role]
client.set_roles(account_id, roles) → void
# Token management (admin or role-scoped)
client.issue_service_token(account_id) → (token, expires_at)
client.revoke_token(jti) → void
# PG credentials (admin or role-scoped)
client.get_pg_creds(account_id) → pg_creds
client.set_pg_creds(account_id, pg_creds) → void
```
Error types exposed by every library:
| Error | Meaning |
|---|---|
| `MciasAuthError` / `Unauthenticated` | Token missing, invalid, or expired |
| `MciasForbiddenError` / `PermissionDenied` | Insufficient role |
| `MciasNotFoundError` / `NotFound` | Resource does not exist |
| `MciasInputError` / `InvalidArgument` | Malformed request |
| `MciasServerError` / `Internal` | Unexpected server error |
| `MciasTransportError` | Network/TLS failure |
### Per-Language Implementation Notes
#### Go (`clients/go/`)
- Module: `git.wntrmute.dev/kyle/mcias/clients/go`
- Package: `mciasgoclient`
- HTTP: `net/http` with custom `*tls.Config` for CA cert
- Token state: guarded by `sync.RWMutex`
- JSON: `encoding/json` with `DisallowUnknownFields` on all decoders
- Error wrapping: `fmt.Errorf("mciasgoclient: %w", err)` preserving cause
#### Rust (`clients/rust/`)
- Crate: `mcias-client` (published to crates.io when stable)
- Runtime: `tokio`-async; `reqwest` for HTTP
- TLS: `rustls` backend (no OpenSSL dependency); custom CA via
`reqwest::Certificate`
- Error type: `MciasError` enum deriving `thiserror::Error`
- Serialization: `serde` + `serde_json`; strict unknown-field rejection
via `#[serde(deny_unknown_fields)]`
- Token state: `Arc<tokio::sync::RwLock<Option<String>>>`
#### Common Lisp (`clients/lisp/`)
- ASDF system: `mcias-client`
- HTTP: `dexador`
- JSON: `yason` (or `cl-json`; prefer `yason` for streaming)
- TLS: delegated to Dexador/Usocket; custom CA documented per platform
- API: CLOS class `mcias-client` with slot `token`; methods are generic
functions
- Conditions: `mcias-error`, `mcias-unauthenticated`, `mcias-forbidden`,
`mcias-not-found` — all subclasses of `mcias-error`
- Tests: `fiveam` test suite; mock responses via local TCP server or
Dexador's mock facility if available
- Compatibility: SBCL primary; CCL secondary
#### Python (`clients/python/`)
- Package: `mcias_client` (PEP 517 build; `pyproject.toml`)
- HTTP: `httpx` (provides both sync `MciasClient` and async `AsyncMciasClient`)
- TLS: `ssl.create_default_context(cafile=...)` for custom CA
- Types: `py.typed` marker; all public symbols fully annotated; `mypy --strict`
- Errors: `MciasError(Exception)` base with subclasses as listed above
- Token state: `_token: str | None` instance attribute; thread-safe in sync
variant via `threading.Lock`
- Python version support: 3.11+
- Linting: `ruff check` (replaces flake8/isort); `ruff format` for style
### Versioning Strategy
Each client library follows the MCIAS server's minor version. Breaking changes
to the API surface increment the major version. The proto definitions (Phase 7)
serve as the source of truth for the canonical interface; client libraries
implement a subset matching the REST API.
Client libraries are not coupled to each other. A user of the Python library
does not need the Go library installed.
### Mock Server
`test/mock/` contains a Go binary (`mcias-mock`) that implements a minimal
in-memory MCIAS server for use in client library integration tests. It
supports the full REST API surface with configurable fixture responses.
Language-specific test suites start `mcias-mock` as a subprocess and connect
to it over localhost TLS (using a bundled self-signed test certificate).

View File

@@ -4,9 +4,10 @@ Source of truth for current development state.
---
## Current Status: Phase 6 Complete — Full Implementation
## Current Status: Phase 6 Complete — Phases 79 Planned
All phases complete. 117 tests pass with zero race conditions.
117 tests pass with zero race conditions. Phases 79 are designed and
documented; implementation not yet started.
### Completed Phases
@@ -18,6 +19,12 @@ All phases complete. 117 tests pass with zero race conditions.
- [x] Phase 5: E2E tests, security hardening, commit
- [x] Phase 6: mciasdb — direct SQLite maintenance tool
### Planned Phases
- [ ] Phase 7: gRPC interface (alternate transport; dual-stack with REST)
- [ ] Phase 8: Operational artifacts (systemd unit, man pages, Makefile, install script)
- [ ] Phase 9: Client libraries (Go, Rust, Common Lisp, Python)
---
## Implementation Log