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:
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
|
||||
`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).
|
||||
|
||||
Reference in New Issue
Block a user