Skip to main content

surql/connection/
auth.rs

1//! Authentication types for SurrealDB connections.
2//!
3//! Port of `surql/connection/auth.py`. Covers the four SurrealDB auth
4//! levels (root / namespace / database / scope) plus JWT token auth, as
5//! pure data types. The Python `AuthManager` ties these to the async
6//! client; that wrapper lands with the runtime client in a later PR.
7
8use std::collections::BTreeMap;
9
10use serde::{Deserialize, Serialize};
11use serde_json::{Map, Value};
12
13/// SurrealDB authentication level.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
15#[serde(rename_all = "lowercase")]
16pub enum AuthType {
17    /// Server root user.
18    Root,
19    /// Namespace user.
20    Namespace,
21    /// Database user.
22    Database,
23    /// Scope/record-level user.
24    Scope,
25}
26
27impl std::fmt::Display for AuthType {
28    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29        let s = match self {
30            Self::Root => "root",
31            Self::Namespace => "namespace",
32            Self::Database => "database",
33            Self::Scope => "scope",
34        };
35        f.write_str(s)
36    }
37}
38
39/// Trait implemented by every credential type so the runtime client can
40/// serialise them to the SurrealDB SDK's `signin`/`signup` payload.
41pub trait Credentials {
42    /// SurrealDB auth level this credential targets.
43    fn auth_type(&self) -> AuthType;
44
45    /// Flatten the credential to a JSON object for the SurrealDB SDK.
46    fn to_signin_payload(&self) -> Map<String, Value>;
47}
48
49/// Root-level credentials.
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
51pub struct RootCredentials {
52    /// Root username.
53    pub username: String,
54    /// Root password.
55    #[serde(skip_serializing_if = "Option::is_none", default)]
56    pub password: Option<String>,
57}
58
59impl RootCredentials {
60    /// Construct root credentials.
61    pub fn new(username: impl Into<String>, password: impl Into<String>) -> Self {
62        Self {
63            username: username.into(),
64            password: Some(password.into()),
65        }
66    }
67}
68
69impl Credentials for RootCredentials {
70    fn auth_type(&self) -> AuthType {
71        AuthType::Root
72    }
73
74    fn to_signin_payload(&self) -> Map<String, Value> {
75        let mut m = Map::new();
76        m.insert("username".into(), Value::String(self.username.clone()));
77        if let Some(p) = &self.password {
78            m.insert("password".into(), Value::String(p.clone()));
79        }
80        m
81    }
82}
83
84/// Namespace-level credentials.
85#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
86pub struct NamespaceCredentials {
87    /// Target namespace.
88    pub namespace: String,
89    /// Namespace username.
90    pub username: String,
91    /// Namespace password.
92    #[serde(skip_serializing_if = "Option::is_none", default)]
93    pub password: Option<String>,
94}
95
96impl NamespaceCredentials {
97    /// Construct namespace credentials.
98    pub fn new(
99        namespace: impl Into<String>,
100        username: impl Into<String>,
101        password: impl Into<String>,
102    ) -> Self {
103        Self {
104            namespace: namespace.into(),
105            username: username.into(),
106            password: Some(password.into()),
107        }
108    }
109}
110
111impl Credentials for NamespaceCredentials {
112    fn auth_type(&self) -> AuthType {
113        AuthType::Namespace
114    }
115
116    fn to_signin_payload(&self) -> Map<String, Value> {
117        let mut m = Map::new();
118        m.insert("namespace".into(), Value::String(self.namespace.clone()));
119        m.insert("username".into(), Value::String(self.username.clone()));
120        if let Some(p) = &self.password {
121            m.insert("password".into(), Value::String(p.clone()));
122        }
123        m
124    }
125}
126
127/// Database-level credentials.
128#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
129pub struct DatabaseCredentials {
130    /// Target namespace.
131    pub namespace: String,
132    /// Target database.
133    pub database: String,
134    /// Database username.
135    pub username: String,
136    /// Database password.
137    #[serde(skip_serializing_if = "Option::is_none", default)]
138    pub password: Option<String>,
139}
140
141impl DatabaseCredentials {
142    /// Construct database credentials.
143    pub fn new(
144        namespace: impl Into<String>,
145        database: impl Into<String>,
146        username: impl Into<String>,
147        password: impl Into<String>,
148    ) -> Self {
149        Self {
150            namespace: namespace.into(),
151            database: database.into(),
152            username: username.into(),
153            password: Some(password.into()),
154        }
155    }
156}
157
158impl Credentials for DatabaseCredentials {
159    fn auth_type(&self) -> AuthType {
160        AuthType::Database
161    }
162
163    fn to_signin_payload(&self) -> Map<String, Value> {
164        let mut m = Map::new();
165        m.insert("namespace".into(), Value::String(self.namespace.clone()));
166        m.insert("database".into(), Value::String(self.database.clone()));
167        m.insert("username".into(), Value::String(self.username.clone()));
168        if let Some(p) = &self.password {
169            m.insert("password".into(), Value::String(p.clone()));
170        }
171        m
172    }
173}
174
175/// Scope-level (record access) credentials.
176///
177/// `variables` holds the scope-defined fields (commonly `email`,
178/// `password`, etc.). They are flattened into the top-level payload at
179/// signin time to match the SDK contract.
180#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
181pub struct ScopeCredentials {
182    /// Target namespace.
183    pub namespace: String,
184    /// Target database.
185    pub database: String,
186    /// Access/scope name.
187    pub access: String,
188    /// Scope-defined variables (stored sorted for deterministic output).
189    #[serde(default)]
190    pub variables: BTreeMap<String, Value>,
191}
192
193impl ScopeCredentials {
194    /// Construct scope credentials (with an empty variable set).
195    pub fn new(
196        namespace: impl Into<String>,
197        database: impl Into<String>,
198        access: impl Into<String>,
199    ) -> Self {
200        Self {
201            namespace: namespace.into(),
202            database: database.into(),
203            access: access.into(),
204            variables: BTreeMap::new(),
205        }
206    }
207
208    /// Attach a scope variable (e.g. `"email"`, `"password"`).
209    pub fn with(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
210        self.variables.insert(key.into(), value.into());
211        self
212    }
213}
214
215impl Credentials for ScopeCredentials {
216    fn auth_type(&self) -> AuthType {
217        AuthType::Scope
218    }
219
220    fn to_signin_payload(&self) -> Map<String, Value> {
221        let mut m = Map::new();
222        m.insert("namespace".into(), Value::String(self.namespace.clone()));
223        m.insert("database".into(), Value::String(self.database.clone()));
224        m.insert("access".into(), Value::String(self.access.clone()));
225        for (k, v) in &self.variables {
226            m.insert(k.clone(), v.clone());
227        }
228        m
229    }
230}
231
232/// Pre-existing JWT token authentication.
233#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
234pub struct TokenAuth {
235    /// JWT authentication token.
236    pub token: String,
237}
238
239impl TokenAuth {
240    /// Construct token auth.
241    pub fn new(token: impl Into<String>) -> Self {
242        Self {
243            token: token.into(),
244        }
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251    use serde_json::json;
252
253    #[test]
254    fn auth_type_display() {
255        assert_eq!(AuthType::Root.to_string(), "root");
256        assert_eq!(AuthType::Namespace.to_string(), "namespace");
257        assert_eq!(AuthType::Database.to_string(), "database");
258        assert_eq!(AuthType::Scope.to_string(), "scope");
259    }
260
261    #[test]
262    fn root_payload() {
263        let creds = RootCredentials::new("root", "secret");
264        let p = creds.to_signin_payload();
265        assert_eq!(p.get("username").unwrap(), &json!("root"));
266        assert_eq!(p.get("password").unwrap(), &json!("secret"));
267        assert_eq!(creds.auth_type(), AuthType::Root);
268    }
269
270    #[test]
271    fn namespace_payload() {
272        let creds = NamespaceCredentials::new("prod", "u", "p");
273        let p = creds.to_signin_payload();
274        assert_eq!(p.get("namespace").unwrap(), &json!("prod"));
275        assert_eq!(p.get("username").unwrap(), &json!("u"));
276        assert_eq!(p.get("password").unwrap(), &json!("p"));
277        assert_eq!(creds.auth_type(), AuthType::Namespace);
278    }
279
280    #[test]
281    fn database_payload() {
282        let creds = DatabaseCredentials::new("prod", "app", "u", "p");
283        let p = creds.to_signin_payload();
284        assert_eq!(p.get("namespace").unwrap(), &json!("prod"));
285        assert_eq!(p.get("database").unwrap(), &json!("app"));
286        assert_eq!(p.get("username").unwrap(), &json!("u"));
287        assert_eq!(p.get("password").unwrap(), &json!("p"));
288        assert_eq!(creds.auth_type(), AuthType::Database);
289    }
290
291    #[test]
292    fn scope_payload_flattens_variables() {
293        let creds = ScopeCredentials::new("prod", "app", "user")
294            .with("email", "a@example.com")
295            .with("password", "secret");
296        let p = creds.to_signin_payload();
297        assert_eq!(p.get("namespace").unwrap(), &json!("prod"));
298        assert_eq!(p.get("database").unwrap(), &json!("app"));
299        assert_eq!(p.get("access").unwrap(), &json!("user"));
300        assert_eq!(p.get("email").unwrap(), &json!("a@example.com"));
301        assert_eq!(p.get("password").unwrap(), &json!("secret"));
302        assert_eq!(creds.auth_type(), AuthType::Scope);
303    }
304
305    #[test]
306    fn token_auth_debug_does_not_panic() {
307        let t = TokenAuth::new("eyJhbGciOiJIUzI1NiJ9.abc");
308        // Ensure Debug is implemented; this will print the token since
309        // we keep Serialize for wire use. Users should avoid logging.
310        let _ = format!("{t:?}");
311    }
312
313    #[test]
314    fn auth_type_serde() {
315        let json = serde_json::to_string(&AuthType::Root).unwrap();
316        assert_eq!(json, "\"root\"");
317        let back: AuthType = serde_json::from_str("\"scope\"").unwrap();
318        assert_eq!(back, AuthType::Scope);
319    }
320
321    #[test]
322    fn root_credentials_skip_missing_password() {
323        let creds = RootCredentials {
324            username: "root".into(),
325            password: None,
326        };
327        let json = serde_json::to_string(&creds).unwrap();
328        assert!(!json.contains("password"));
329    }
330}