Skip to main content

cognee_http_server/auth/
extractor.rs

1//! `AuthenticatedUser` + `OptionalAuthenticatedUser` extractors for axum
2//! handlers — OSS slim variant.
3//!
4//! Resolution order:
5//! 1. `AuthResolver` on `AppState` (closed-injected — JWT/cookie/API key
6//!    chain or external validator) when present and it returns `Some`.
7//! 2. If `require_authentication == false` → synthetic default user (id
8//!    = all-zeros).
9//! 3. Else → 401 Unauthorized.
10//!
11//! The OSS build keeps no JWT/cookie/API-key parsing state — those moved
12//! into the closed `cognee-http-cloud` crate. Closed embedders install
13//! an `AuthResolver` via `RouterBuilder::with_auth_resolver(...)` or an
14//! `ExtraAuthValidator` via `RouterBuilder::with_extra_validator(...)`.
15
16use axum::{extract::FromRequestParts, http::request::Parts};
17use uuid::Uuid;
18
19use crate::error::ApiError;
20use crate::state::AppState;
21
22// ─── AuthMethod ───────────────────────────────────────────────────────────────
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum AuthMethod {
26    ApiKey,
27    BearerJwt,
28    CookieJwt,
29    DefaultUser,
30}
31
32// ─── AuthenticatedUser ────────────────────────────────────────────────────────
33
34#[derive(Debug, Clone)]
35pub struct AuthenticatedUser {
36    pub id: Uuid,
37    pub email: String,
38    pub is_superuser: bool,
39    pub is_verified: bool,
40    pub is_active: bool,
41    pub tenant_id: Option<Uuid>,
42    pub auth_method: AuthMethod,
43}
44
45impl FromRequestParts<AppState> for AuthenticatedUser {
46    type Rejection = ApiError;
47
48    async fn from_request_parts(
49        parts: &mut Parts,
50        state: &AppState,
51    ) -> Result<Self, Self::Rejection> {
52        if let Some(resolver) = state.auth_resolver.as_ref()
53            && let Some(user) = resolver.resolve(parts).await
54        {
55            if !user.is_active {
56                return Err(ApiError::LoginBadCredentials);
57            }
58            return Ok(user);
59        }
60        if state.config.require_authentication {
61            Err(ApiError::Unauthorized)
62        } else {
63            Ok(default_user_from_state(state))
64        }
65    }
66}
67
68/// Synthetic default user used when no auth resolver is wired and
69/// `require_authentication=false`.
70///
71/// **UUID5 content-addressing invariant** (Python parity): the returned
72/// `id` MUST equal `Uuid::new_v5(&Uuid::NAMESPACE_OID, email.as_bytes())`
73/// for the configured `default_user_email`. This matches
74/// [`cognee_lib::api::user::get_or_create_default_user`] and the Python
75/// reference SDK (`uuid5(NAMESPACE_OID, email)`), so the HTTP server
76/// produces the same owner id as the bindings/CLI for the same email.
77/// Without this, data added via bindings (uuid5-derived owner) would
78/// not be visible to queries via the HTTP server (previously hardcoded
79/// `Uuid::nil()`), breaking the cross-SDK content-addressed UUID5
80/// invariants asserted by `e2e-cross-sdk`.
81pub fn default_user_from_state(state: &AppState) -> AuthenticatedUser {
82    // `state.config` is a plain `Arc<HttpServerConfig>` — no lock guard
83    // to drop. The clone is cheap and keeps this function synchronous.
84    let email = state.config.default_user_email.clone();
85    let id = Uuid::new_v5(&Uuid::NAMESPACE_OID, email.as_bytes());
86    AuthenticatedUser {
87        id,
88        email,
89        is_superuser: true,
90        is_verified: true,
91        is_active: true,
92        tenant_id: None,
93        auth_method: AuthMethod::DefaultUser,
94    }
95}
96
97// ─── OptionalAuthenticatedUser ────────────────────────────────────────────────
98
99/// Same resolution as `AuthenticatedUser` but never errors — returns
100/// `None` when authentication fails (instead of 401).
101#[derive(Debug, Clone)]
102pub struct OptionalAuthenticatedUser(pub Option<AuthenticatedUser>);
103
104impl FromRequestParts<AppState> for OptionalAuthenticatedUser {
105    type Rejection = std::convert::Infallible;
106
107    async fn from_request_parts(
108        parts: &mut Parts,
109        state: &AppState,
110    ) -> Result<Self, Self::Rejection> {
111        if let Some(resolver) = state.auth_resolver.as_ref()
112            && let Some(user) = resolver.resolve(parts).await
113        {
114            if user.is_active {
115                return Ok(Self(Some(user)));
116            }
117            return Ok(Self(None));
118        }
119        if state.config.require_authentication {
120            Ok(Self(None))
121        } else {
122            Ok(Self(Some(default_user_from_state(state))))
123        }
124    }
125}
126
127// ─── Tests ────────────────────────────────────────────────────────────────────
128
129#[cfg(test)]
130#[allow(
131    clippy::unwrap_used,
132    clippy::expect_used,
133    reason = "test code — panics are acceptable failures"
134)]
135mod tests {
136    use super::*;
137    use crate::config::HttpServerConfig;
138
139    /// `default_user_from_state` MUST derive the owner id as
140    /// `uuid5(NAMESPACE_OID, default_user_email)` so it matches
141    /// `cognee_lib::api::user::get_or_create_default_user` and the
142    /// Python reference SDK. Locks down the Python parity invariant
143    /// referenced in Plan §7.
144    #[tokio::test]
145    async fn default_user_id_matches_uuid5_of_configured_email() {
146        let cfg = HttpServerConfig {
147            default_user_email: "alice@example.com".to_string(),
148            ..HttpServerConfig::default()
149        };
150        let state = AppState::build(cfg)
151            .await
152            .expect("AppState::build with default config must succeed");
153
154        let user = default_user_from_state(&state);
155
156        let expected_id = Uuid::new_v5(&Uuid::NAMESPACE_OID, "alice@example.com".as_bytes());
157        assert_eq!(
158            user.id, expected_id,
159            "owner id must be uuid5(NAMESPACE_OID, email)"
160        );
161        assert_eq!(user.email, "alice@example.com");
162        assert!(user.is_active);
163        assert!(user.is_superuser);
164        assert_eq!(user.auth_method, AuthMethod::DefaultUser);
165        assert!(user.tenant_id.is_none());
166    }
167
168    /// The default-config email derivation must also match what the
169    /// bindings/CLI compute via `get_or_create_default_user` for the
170    /// out-of-the-box `default_user@example.com`.
171    #[tokio::test]
172    async fn default_user_id_for_default_email_is_stable() {
173        let state = AppState::build(HttpServerConfig::default())
174            .await
175            .expect("AppState::build with default config must succeed");
176
177        let user = default_user_from_state(&state);
178
179        let expected_id = Uuid::new_v5(&Uuid::NAMESPACE_OID, "default_user@example.com".as_bytes());
180        assert_eq!(user.id, expected_id);
181        assert_ne!(user.id, Uuid::nil(), "must not regress to the old nil-UUID");
182    }
183}