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