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