feat(m7.3): cross-tenant admin HTTP endpoints (#95)
CI / Check (push) Has been skipped
CI / Detect Changes (push) Successful in 4s
CI / Deploy Dashboard (push) Has been cancelled
CI / Deploy Docs (push) Has been cancelled
CI / Deploy MCP (push) Has been cancelled
CI / Deploy Agent (push) Has been cancelled
CI / Check (push) Has been skipped
CI / Detect Changes (push) Successful in 4s
CI / Deploy Dashboard (push) Has been cancelled
CI / Deploy Docs (push) Has been cancelled
CI / Deploy MCP (push) Has been cancelled
CI / Deploy Agent (push) Has been cancelled
GET /api/admin/tenants lists tenant DBs; DELETE /api/admin/tenants/{tenant_id} drops them (GDPR). Behind a separate auth path that rejects customer realm tokens.
This commit was merged in pull request #95.
This commit is contained in:
@@ -63,16 +63,24 @@ struct Claims {
|
||||
|
||||
const PUBLIC_ENDPOINTS: &[&str] = &["/api/v1/health"];
|
||||
|
||||
/// Path prefixes that bypass JWT validation. The admin sub-router
|
||||
/// (`/api/v1/admin/*`) has its own static-bearer middleware and must
|
||||
/// not be routed through the customer-JWT path — a Keycloak token
|
||||
/// always carries a single tenant_id and would semantically conflict
|
||||
/// with cross-tenant admin operations.
|
||||
const PUBLIC_PREFIXES: &[&str] = &["/api/v1/admin/"];
|
||||
|
||||
/// Middleware that validates Bearer JWT tokens against Keycloak's JWKS
|
||||
/// and attaches a `TenantContext` extension on success.
|
||||
///
|
||||
/// Skips validation for the health endpoint.
|
||||
/// If `JwksState` is not present (Keycloak not configured), requests
|
||||
/// pass through and downstream code must handle the missing context.
|
||||
/// Skips validation for the health endpoint and any path under one of
|
||||
/// the [`PUBLIC_PREFIXES`]. If `JwksState` is not present (Keycloak
|
||||
/// not configured), requests pass through and downstream code must
|
||||
/// handle the missing context.
|
||||
pub async fn require_jwt_auth(mut request: Request, next: Next) -> Response {
|
||||
let path = request.uri().path();
|
||||
|
||||
if PUBLIC_ENDPOINTS.contains(&path) {
|
||||
if PUBLIC_ENDPOINTS.contains(&path) || PUBLIC_PREFIXES.iter().any(|p| path.starts_with(p)) {
|
||||
return next.run(request).await;
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,15 @@ pub struct AgentConfig {
|
||||
pub pentest_imap_tls: bool,
|
||||
pub pentest_imap_username: Option<String>,
|
||||
pub pentest_imap_password: Option<SecretString>,
|
||||
/// Static bearer for the cross-tenant admin endpoints under
|
||||
/// `/api/v1/admin/*`. When `None`, those endpoints are not
|
||||
/// mounted at all (defense-in-depth: ops endpoints never reach
|
||||
/// any auth path if no operator has explicitly opted in).
|
||||
pub admin_api_token: Option<SecretString>,
|
||||
/// Live tenant-registry URL the scheduler consults for the list
|
||||
/// of tenants to iterate. When `None` or unreachable, scheduler
|
||||
/// falls back to `SCHEDULER_TENANT_IDS` env (M7.2-C).
|
||||
pub tenant_registry_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
|
||||
Reference in New Issue
Block a user