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:
1
.junie/guidelines.md
Symbolic link
1
.junie/guidelines.md
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
CLAUDE.md
|
||||||
343
ARCHITECTURE.md
343
ARCHITECTURE.md
@@ -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
|
newline-delimited JSON for scripting. Credential fields follow the same
|
||||||
`json:"-"` exclusion rules as the API — they are only printed when the
|
`json:"-"` exclusion rules as the API — they are only printed when the
|
||||||
specific `get` or `pgcreds get` command is invoked, never in list output.
|
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).
|
||||||
|
|||||||
11
PROGRESS.md
11
PROGRESS.md
@@ -4,9 +4,10 @@ Source of truth for current development state.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Current Status: Phase 6 Complete — Full Implementation
|
## Current Status: Phase 6 Complete — Phases 7–9 Planned
|
||||||
|
|
||||||
All phases complete. 117 tests pass with zero race conditions.
|
117 tests pass with zero race conditions. Phases 7–9 are designed and
|
||||||
|
documented; implementation not yet started.
|
||||||
|
|
||||||
### Completed Phases
|
### 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 5: E2E tests, security hardening, commit
|
||||||
- [x] Phase 6: mciasdb — direct SQLite maintenance tool
|
- [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
|
## Implementation Log
|
||||||
|
|||||||
Reference in New Issue
Block a user