1use std::time::Duration;
21
22use rusqlite::{OptionalExtension, params};
23use serde::Serialize;
24
25mod identities;
26pub use identities::{MAX_SUPERADMIN_IDENTITIES, SuperadminIdentity};
27
28use super::secret::{self, TokenKind};
29use super::{AuthError, AuthResult, AuthStore, Principal, PrincipalKind, TOUCH_EVERY, User};
30
31#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
33#[serde(tag = "kind", rename_all = "snake_case")]
34pub enum SuperadminSource {
35 Token {
36 id: i64,
37 name: String,
38 },
39 Tailnet {
40 login: String,
42 node: String,
44 #[serde(default, skip_serializing_if = "Vec::is_empty")]
45 tags: Vec<String>,
46 },
47 Access {
48 name: String,
50 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
51 service_token: bool,
52 },
53 Dev {
56 email: String,
57 },
58}
59
60impl SuperadminSource {
61 pub fn label(&self) -> String {
64 match self {
65 SuperadminSource::Token { name, .. } => format!("token:{name}"),
66 SuperadminSource::Tailnet { login, node, tags } => {
67 if tags.is_empty() {
68 format!("tailnet:{login}")
69 } else {
70 format!("tailnet:{node}")
71 }
72 }
73 SuperadminSource::Access { name, .. } => format!("access:{name}"),
74 SuperadminSource::Dev { email } => format!("dev:{email}"),
75 }
76 }
77
78 pub fn is_ambient(&self) -> bool {
82 matches!(
83 self,
84 SuperadminSource::Tailnet { .. }
85 | SuperadminSource::Access { .. }
86 | SuperadminSource::Dev { .. }
87 )
88 }
89}
90
91#[derive(Debug, Clone, PartialEq, Eq)]
93pub struct Superadmin {
94 pub source: SuperadminSource,
95 pub principal: Principal,
99}
100
101impl Superadmin {
102 pub fn synthetic(source: SuperadminSource) -> Superadmin {
104 let label = source.label();
105 Superadmin {
106 principal: Principal {
107 user: User {
108 id: 0,
109 email: label.clone(),
110 name: label,
111 platform_admin: true,
112 created_at: 0,
113 disabled: false,
114 has_password: false,
115 },
116 kind: PrincipalKind::Superadmin {
117 source: source.clone(),
118 },
119 orgs: Vec::new(),
120 platform_admin: true,
121 downscoped: None,
122 },
123 source,
124 }
125 }
126
127 pub fn as_user(source: SuperadminSource, user: User, store: &AuthStore) -> AuthResult<Self> {
129 let orgs = store
130 .memberships(user.id)?
131 .into_iter()
132 .map(|m| (m.org, m.role))
133 .collect();
134 Ok(Superadmin {
135 principal: Principal {
136 user,
137 kind: PrincipalKind::Superadmin {
138 source: source.clone(),
139 },
140 orgs,
141 platform_admin: true,
142 downscoped: None,
143 },
144 source,
145 })
146 }
147
148 pub fn has_account(&self) -> bool {
150 self.principal.user.id > 0
151 }
152
153 pub fn label(&self) -> String {
154 self.source.label()
155 }
156}
157
158#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
160pub struct SuperadminToken {
161 pub id: i64,
162 pub name: String,
163 pub created_at: i64,
164 pub last_used: Option<i64>,
165 pub expires_at: Option<i64>,
166}
167
168#[derive(Debug, Clone)]
169pub struct NewSuperadminToken {
170 pub token: String,
171 pub info: SuperadminToken,
172}
173
174const COLS: &str = "id, name, created_at, last_used, expires_at";
175
176fn row(r: &rusqlite::Row) -> rusqlite::Result<SuperadminToken> {
177 Ok(SuperadminToken {
178 id: r.get(0)?,
179 name: r.get(1)?,
180 created_at: r.get(2)?,
181 last_used: r.get(3)?,
182 expires_at: r.get(4)?,
183 })
184}
185
186impl AuthStore {
187 pub fn create_superadmin_token(
190 &self,
191 name: &str,
192 expires: Option<Duration>,
193 ) -> AuthResult<NewSuperadminToken> {
194 let name = name.trim();
195 if name.is_empty()
196 || name.chars().count() > 64
197 || !name
198 .bytes()
199 .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.'))
200 {
201 return Err(AuthError::Invalid(
202 "superadmin token name: 1 to 64 of [A-Za-z0-9._-]".into(),
203 ));
204 }
205 let (token, hash) = secret::new_token(TokenKind::Superadmin)?;
206 let now = self.now();
207 let expires_at = expires.map(|d| now + d.as_secs() as i64);
208 let db = self.db();
209 let r = db.execute(
210 "INSERT INTO superadmin_tokens (token_hash, name, created_at, expires_at)
211 VALUES (?1, ?2, ?3, ?4)",
212 params![hash, name, now, expires_at],
213 );
214 match r {
215 Ok(_) => {}
216 Err(rusqlite::Error::SqliteFailure(e, _))
217 if e.code == rusqlite::ErrorCode::ConstraintViolation =>
218 {
219 return Err(AuthError::Conflict(format!(
220 "a superadmin token named {name} exists; revoke it or pick another name"
221 )));
222 }
223 Err(e) => return Err(e.into()),
224 }
225 Ok(NewSuperadminToken {
226 token,
227 info: SuperadminToken {
228 id: db.last_insert_rowid(),
229 name: name.to_string(),
230 created_at: now,
231 last_used: None,
232 expires_at,
233 },
234 })
235 }
236
237 pub fn authenticate_superadmin_token(
239 &self,
240 token: &str,
241 ) -> AuthResult<Option<SuperadminToken>> {
242 if !secret::well_formed(token, TokenKind::Superadmin) {
243 return Ok(None);
244 }
245 let hash = secret::hash_token(token);
246 let now = self.now();
247 let found = self
248 .db()
249 .query_row(
250 &format!("SELECT token_hash, {COLS} FROM superadmin_tokens WHERE token_hash = ?1"),
251 [&hash],
252 |r| Ok((r.get::<_, Vec<u8>>(0)?, row_at(r)?)),
253 )
254 .optional()?;
255 let Some((stored, t)) = found else {
256 return Ok(None);
257 };
258 if !secret::ct_eq(&stored, &hash) || t.expires_at.is_some_and(|e| now >= e) {
259 return Ok(None);
260 }
261 if t.last_used.is_none_or(|l| now - l >= TOUCH_EVERY) {
262 self.db().execute(
263 "UPDATE superadmin_tokens SET last_used = ?2 WHERE id = ?1",
264 params![t.id, now],
265 )?;
266 }
267 Ok(Some(t))
268 }
269
270 pub fn list_superadmin_tokens(&self) -> AuthResult<Vec<SuperadminToken>> {
271 let db = self.db();
272 let mut st = db.prepare(&format!("SELECT {COLS} FROM superadmin_tokens ORDER BY id"))?;
273 let rows = st.query_map([], row)?;
274 Ok(rows.collect::<rusqlite::Result<_>>()?)
275 }
276
277 pub fn superadmin_token(&self, id: i64) -> AuthResult<SuperadminToken> {
278 self.db()
279 .query_row(
280 &format!("SELECT {COLS} FROM superadmin_tokens WHERE id = ?1"),
281 [id],
282 row,
283 )
284 .optional()?
285 .ok_or_else(|| AuthError::NotFound(format!("superadmin token {id}")))
286 }
287
288 pub fn revoke_superadmin_token(&self, id: i64) -> AuthResult<bool> {
290 let n = self
291 .db()
292 .execute("DELETE FROM superadmin_tokens WHERE id = ?1", [id])?;
293 Ok(n > 0)
294 }
295}
296
297fn row_at(r: &rusqlite::Row) -> rusqlite::Result<SuperadminToken> {
299 Ok(SuperadminToken {
300 id: r.get(1)?,
301 name: r.get(2)?,
302 created_at: r.get(3)?,
303 last_used: r.get(4)?,
304 expires_at: r.get(5)?,
305 })
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311 use crate::auth::AuthConfig;
312
313 #[test]
314 fn tokens_mint_authenticate_expire_and_revoke() {
315 let s = AuthStore::in_memory(AuthConfig::default()).unwrap();
316 let t = s.create_superadmin_token("agent", None).unwrap();
317 assert!(t.token.starts_with("isb_sa_"));
318 let got = s.authenticate_superadmin_token(&t.token).unwrap().unwrap();
319 assert_eq!(got.name, "agent");
320 assert!(got.last_used.is_some() || s.superadmin_token(got.id).unwrap().last_used.is_some());
321 assert!(matches!(
323 s.create_superadmin_token("agent", None),
324 Err(AuthError::Conflict(_))
325 ));
326 assert!(s.create_superadmin_token("has space", None).is_err());
327 assert!(s.create_superadmin_token("", None).is_err());
328 assert!(
330 s.authenticate_superadmin_token(&t.token.replace("isb_sa_", "isb_tok_"))
331 .unwrap()
332 .is_none()
333 );
334 let mut bad = t.token.clone();
335 bad.pop();
336 bad.push(if t.token.ends_with('A') { 'B' } else { 'A' });
337 assert!(s.authenticate_superadmin_token(&bad).unwrap().is_none());
338 let e = s
340 .create_superadmin_token("short", Some(Duration::from_secs(0)))
341 .unwrap();
342 assert!(s.authenticate_superadmin_token(&e.token).unwrap().is_none());
343 assert_eq!(s.list_superadmin_tokens().unwrap().len(), 2);
344 assert!(s.revoke_superadmin_token(got.id).unwrap());
345 assert!(!s.revoke_superadmin_token(got.id).unwrap());
346 assert!(s.authenticate_superadmin_token(&t.token).unwrap().is_none());
347 }
348
349 #[test]
350 fn labels_and_synthetic_principals() {
351 let tok = SuperadminSource::Token {
352 id: 1,
353 name: "ci".into(),
354 };
355 assert_eq!(tok.label(), "token:ci");
356 assert!(!tok.is_ambient());
357 let person = SuperadminSource::Tailnet {
358 login: "a@example.com".into(),
359 node: "laptop.tail1.ts.net".into(),
360 tags: vec![],
361 };
362 assert_eq!(person.label(), "tailnet:a@example.com");
363 assert!(person.is_ambient());
364 let tagged = SuperadminSource::Tailnet {
365 login: "tagged-devices".into(),
366 node: "agent-1.tail1.ts.net".into(),
367 tags: vec!["tag:agents".into()],
368 };
369 assert_eq!(tagged.label(), "tailnet:agent-1.tail1.ts.net");
370 let access = SuperadminSource::Access {
371 name: "a@example.com".into(),
372 service_token: false,
373 };
374 assert_eq!(access.label(), "access:a@example.com");
375 assert!(access.is_ambient());
376 let dev = SuperadminSource::Dev {
377 email: "dev@dev.com".into(),
378 };
379 assert_eq!(dev.label(), "dev:dev@dev.com");
380 assert!(dev.is_ambient());
381 let s = Superadmin::synthetic(tagged);
382 assert!(!s.has_account());
383 assert!(s.principal.platform_admin);
384 assert_eq!(s.principal.user.email, "tailnet:agent-1.tail1.ts.net");
385 }
386}