a3a96fe2cc
CI / Check (push) Has been skipped
CI / Detect Changes (push) Successful in 5s
CI / Deploy Agent (push) Successful in 8m13s
CI / Deploy Dashboard (push) Successful in 7m3s
CI / Deploy Docs (push) Has been skipped
CI / Deploy MCP (push) Successful in 1m50s
MCP server validates per-tenant bearer tokens on incoming calls and routes each tool to the caller's tenant DB. Closes the cross-tenant data leak in the MCP path identified in M7.3.
70 lines
2.7 KiB
Rust
70 lines
2.7 KiB
Rust
//! Per-tenant API tokens used by `compliance-mcp` to authenticate MCP
|
|
//! HTTP requests on behalf of LLM clients (Claude Desktop, Cursor,
|
|
//! ChatGPT, etc.) that can't run a Keycloak OIDC flow.
|
|
//!
|
|
//! Tokens are opaque strings of the form `mcpt_<44 url-safe random
|
|
//! chars>`. The raw value is shown to the user exactly once at
|
|
//! creation; the database only ever sees the SHA-256 hash. Lookups go
|
|
//! through the cross-tenant `<prefix>__admin.mcp_tokens` collection
|
|
//! and return the `tenant_id` the MCP server should route to.
|
|
|
|
use chrono::{DateTime, Utc};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// Persisted token metadata. `token_hash` is the SHA-256 hex of the
|
|
/// raw token; the raw token itself is never stored.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct McpToken {
|
|
#[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
|
|
pub id: Option<bson::oid::ObjectId>,
|
|
/// SHA-256 hex of the raw token. Unique index in the collection.
|
|
pub token_hash: String,
|
|
/// First 8 chars of the raw token — purely for UI display so users
|
|
/// can identify which token is which without re-issuing.
|
|
pub token_prefix: String,
|
|
/// Routes to `<db_prefix>_<tenant_id>` on MCP requests.
|
|
pub tenant_id: String,
|
|
/// User-given label, e.g. "Claude Desktop" or "Sharang's laptop".
|
|
pub name: String,
|
|
/// Keycloak `sub` of the user who created this token, for audit.
|
|
pub created_by: String,
|
|
#[serde(with = "super::serde_helpers::bson_datetime")]
|
|
pub created_at: DateTime<Utc>,
|
|
#[serde(default, with = "super::serde_helpers::opt_bson_datetime")]
|
|
pub last_used_at: Option<DateTime<Utc>>,
|
|
/// Soft-delete flag. A revoked token doc stays around for audit
|
|
/// but never authenticates.
|
|
#[serde(default)]
|
|
pub revoked: bool,
|
|
}
|
|
|
|
/// Public projection of a token — never includes the hash.
|
|
/// Returned by `GET /api/v1/mcp-tokens`.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct McpTokenView {
|
|
pub id: String,
|
|
pub name: String,
|
|
/// `mcpt_xxxx…` so the user can identify which row is which.
|
|
pub token_prefix: String,
|
|
pub created_by: String,
|
|
#[serde(with = "super::serde_helpers::bson_datetime")]
|
|
pub created_at: DateTime<Utc>,
|
|
#[serde(default, with = "super::serde_helpers::opt_bson_datetime")]
|
|
pub last_used_at: Option<DateTime<Utc>>,
|
|
pub revoked: bool,
|
|
}
|
|
|
|
impl From<&McpToken> for McpTokenView {
|
|
fn from(t: &McpToken) -> Self {
|
|
Self {
|
|
id: t.id.map(|o| o.to_hex()).unwrap_or_default(),
|
|
name: t.name.clone(),
|
|
token_prefix: t.token_prefix.clone(),
|
|
created_by: t.created_by.clone(),
|
|
created_at: t.created_at,
|
|
last_used_at: t.last_used_at,
|
|
revoked: t.revoked,
|
|
}
|
|
}
|
|
}
|