network/TASK.md

136 lines
7.9 KiB
Markdown

# Task: Enforce API Key/JWT and Namespace in Go Client (Auto-Resolve Namespace) and Guard All Operations
Owner: To be assigned
Status: Ready to implement
## Objective
Implement strict client-side access enforcement in the Go client (`pkg/client`) so that:
- An API key or JWT is required by default to use the client.
- The client auto-resolves the namespace from the provided API key or JWT without requiring callers to pass the namespace per call.
- Per-call namespace overrides via context are still allowed for compatibility, but must match the resolved namespace; otherwise, deny the call.
- All operations (Storage, PubSub, Database/RQLite, and NetworkInfo) are guarded and return access errors when unauthenticated or namespace-mismatched.
- No backward compatibility guarantees required.
Note: This is client-side enforcement for now. Protocol-level auth/ACL for libp2p can be added later.
## High-level behavior
- `ClientConfig.RequireAPIKey` defaults to true. If true and neither `APIKey` nor `JWT` is present, `Connect()` fails.
- Namespace is automatically derived:
- From JWT: parse claims and read `Namespace` claim (no network roundtrip). Verification of signature is not required for this task; parsing is enough to derive namespace. Optionally, add a TODO hook for future verification against JWKS if provided.
- From API key: the namespace must be embedded in the key using a documented format (below). The client parses it locally and derives the namespace without any remote calls.
- All calls check that any provided per-call namespace override matches the derived namespace, else return an “access denied: namespace mismatch” error.
- All modules are guarded: Database (RQLite), Storage, PubSub, and NetworkInfo.
## API key and JWT formats
- JWT: RS256 token with claim `Namespace` (string). We will parse claims (unverified) to obtain `Namespace`.
- API key: change to an encoded format that includes the namespace so the client can parse locally. Options (pick one and implement consistently):
- Option A (dotted): `ak_<random>.<namespace>`
- Option B (colon): `ak_<random>:<namespace>`
- Option C (base64 JSON): base64url of `{ "kid": "...", "ns": "<namespace>" }` prefixed by `ak_`
For simplicity and readability, choose Option B: `ak_<random>:<namespace>`.
- Parsing rules:
- If `APIKey` contains a single colon, split and use the right side as `namespace` (trim spaces). If empty -> error.
- If more than one colon or invalid format -> error.
## Changes to implement
### 1) Client configuration and types
- File: `pkg/client/interface.go`
- Extend `ClientConfig`:
- `Namespace string` // optional; if empty, auto-derived from API key or JWT; if still empty, fallback to `AppName`.
- `RequireAPIKey bool` // default true; when true, require either `APIKey` or `JWT`.
- `JWT string` // optional bearer token; used for namespace derivation and future protocol auth.
- Update `DefaultClientConfig(appName string)` to set:
- `RequireAPIKey: true`
- `Namespace: ""` (meaning auto)
### 2) Namespace resolution and access gating
- File: `pkg/client/client.go`
- At construction or `Connect()` time:
- Implement `deriveNamespace()`:
- If `config.Namespace != ""`, use it.
- Else if `config.JWT != ""`, parse JWT claims (unverified) and read `Namespace` claim.
- Else if `config.APIKey != ""`, parse `ak_<random>:<namespace>` and extract namespace.
- Else use `config.AppName`.
- Store the resolved namespace back into `config.Namespace`.
- Enforce presence of credentials:
- If `config.RequireAPIKey` is true AND both `config.APIKey` and `config.JWT` are empty -> return error `access denied: API key or JWT required`.
- Add `func (c *Client) requireAccess(ctx context.Context) error` that:
- If `RequireAPIKey` and both `APIKey` and `JWT` are empty -> error `access denied: credentials required`.
- Resolve per-call namespace override from context (via storage/pubsub helpers below). If present and `override != c.config.Namespace` -> error `access denied: namespace mismatch`.
### 3) Guard all operations
- File: `pkg/client/implementations.go`
- At the start of each public method, call `client.requireAccess(ctx)` and return the error if any.
- DatabaseClientImpl: `Query`, `Transaction`, `CreateTable`, `DropTable`, `GetSchema`.
- StorageClientImpl: `Get`, `Put`, `Delete`, `List`, `Exists`.
- NetworkInfoImpl: `GetPeers`, `GetStatus`, `ConnectToPeer`, `DisconnectFromPeer`.
- For Storage operations, ensure we propagate the effective namespace:
- If override present and equals `config.Namespace`, pass that context through; else use `storage.WithNamespace(ctx, config.Namespace)`.
### 4) PubSub context-based namespace override (parity with Storage)
- Files: `pkg/pubsub/*`
- Add:
- `type ctxKey string`
- `const CtxKeyNamespaceOverride ctxKey = "pubsub_ns_override"`
- `func WithNamespace(ctx context.Context, ns string) context.Context`
- Update topic naming in `manager.go` and `subscriptions.go`/`publish.go`:
- Before computing `namespacedTopic`, check for ctx override; if present and non-empty, use it; else fall back to `m.namespace`.
### 5) Client context helper
- New file: `pkg/client/context.go`
- Add `func WithNamespace(ctx context.Context, ns string) context.Context` that applies both storage and pubsub overrides by chaining:
- `ctx = storage.WithNamespace(ctx, ns)`
- `ctx = pubsub.WithNamespace(ctx, ns)`
- return `ctx`
### 6) Documentation updates
- Files: `README.md`, `AI_CONTEXT.md`
- Document the new client auth behavior:
- An API key or JWT is required by default (`RequireAPIKey=true`).
- Namespace auto-derived from token:
- JWT claim `Namespace`.
- API key format `ak_<random>:<namespace>`.
- Per-call override via `client.WithNamespace(ctx, ns)` allowed but must match derived namespace.
- All modules (Storage, PubSub, Database, NetworkInfo) are guarded.
- Provide usage examples for constructing `ClientConfig` with API key or JWT and making calls.
## Helper details
- JWT parsing: implement a minimal helper to split the token and base64url-decode the payload; read `Namespace` field from JSON. Do not verify signature for this task. If parsing fails, return a clear error.
- API key parsing: simple split on `:`; trim spaces; validate non-empty.
## Error messages (standardize)
- Missing credentials: `access denied: API key or JWT required`
- Namespace mismatch: `access denied: namespace mismatch`
- Client not connected: keep existing `client not connected` error.
## Acceptance criteria
- Without credentials and `RequireAPIKey=true`, `Connect()` returns error and no operations are allowed.
- With API key `ak_abc123:myapp`, the client auto-resolves namespace `myapp`; operations succeed.
- With JWT containing `{ "Namespace": "myapp" }`, the client auto-resolves `myapp`; operations succeed.
- If a caller sets `client.WithNamespace(ctx, "otherNS")` while resolved namespace is `myapp`, any operation returns `access denied: namespace mismatch`.
- PubSub topic names use the override when present (and allowed) else the resolved namespace.
- NetworkInfo methods are also guarded and require credentials.
## Out of scope (for this task)
- Protocol-level auth or verification of JWT signatures against JWKS.
- ETH payments/subscriptions and tier enforcement. (Separate design/implementation.)
## Files to modify/add
- Modify:
- `pkg/client/interface.go`
- `pkg/client/client.go`
- `pkg/client/implementations.go`
- `pkg/pubsub/manager.go`
- `pkg/pubsub/subscriptions.go`
- `pkg/pubsub/publish.go` (if exists; add override resolution there too)
- `README.md`, `AI_CONTEXT.md`
- Add:
- `pkg/pubsub/context.go` (if not present)
- `pkg/client/context.go`
## Notes
- Keep logs concise and avoid leaking tokens in logs. You may log the resolved namespace at `INFO` level on connect.
- Ensure thread-safety when accessing `Client.config` fields (use existing locks if needed).