isb_server/auth/superadmin/
identities.rs1use rusqlite::{OptionalExtension, params};
15use serde::Serialize;
16
17use super::super::agent_identities::{AgentKind, normalize_subject};
18use super::super::{AuthError, AuthResult, AuthStore};
19
20pub const MAX_SUPERADMIN_IDENTITIES: i64 = 200;
23
24#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
26pub struct SuperadminIdentity {
27 pub id: i64,
28 pub kind: AgentKind,
31 pub value: String,
32 pub added_at: i64,
33 pub added_by: String,
35}
36
37impl SuperadminIdentity {
38 pub fn admits_tailnet(&self, login: &str, tags: &[String]) -> bool {
41 if self.kind != AgentKind::Tailnet {
42 return false;
43 }
44 if tags.is_empty() {
45 !self.value.starts_with("tag:") && self.value.eq_ignore_ascii_case(login)
46 } else {
47 tags.iter().any(|t| t.eq_ignore_ascii_case(&self.value))
48 }
49 }
50
51 pub fn admits_access(&self, email: Option<&str>, client_id: Option<&str>) -> bool {
54 if self.kind != AgentKind::Access {
55 return false;
56 }
57 match (email, client_id) {
58 (Some(e), _) => self.value.contains('@') && self.value.eq_ignore_ascii_case(e),
59 (None, Some(cn)) => !self.value.contains('@') && self.value == cn,
60 (None, None) => false,
61 }
62 }
63}
64
65fn row(r: &rusqlite::Row) -> rusqlite::Result<SuperadminIdentity> {
66 let kind: String = r.get(1)?;
67 Ok(SuperadminIdentity {
68 id: r.get(0)?,
69 kind: AgentKind::parse(&kind).map_err(|e| {
70 rusqlite::Error::FromSqlConversionFailure(1, rusqlite::types::Type::Text, Box::new(e))
71 })?,
72 value: r.get(2)?,
73 added_at: r.get(3)?,
74 added_by: r.get(4)?,
75 })
76}
77
78const COLS: &str = "id, kind, value, added_at, added_by";
79
80impl AuthStore {
81 pub fn add_superadmin_identity(
83 &self,
84 kind: AgentKind,
85 value: &str,
86 added_by: &str,
87 ) -> AuthResult<SuperadminIdentity> {
88 let value = normalize_subject(kind, value)?;
89 let db = self.db();
90 let n: i64 = db.query_row("SELECT COUNT(*) FROM superadmin_identities", [], |r| {
91 r.get(0)
92 })?;
93 if n >= MAX_SUPERADMIN_IDENTITIES {
94 return Err(AuthError::Invalid(format!(
95 "{n} superadmin identities, the most isb keeps: remove one first"
96 )));
97 }
98 let now = self.now();
99 let r = db.execute(
100 "INSERT INTO superadmin_identities (kind, value, added_at, added_by)
101 VALUES (?1, ?2, ?3, ?4)",
102 params![kind.as_str(), value, now, added_by],
103 );
104 match r {
105 Ok(_) => {}
106 Err(rusqlite::Error::SqliteFailure(e, _))
107 if e.code == rusqlite::ErrorCode::ConstraintViolation =>
108 {
109 return Err(AuthError::Conflict(format!(
110 "{} {value} is a superadmin already",
111 kind.as_str()
112 )));
113 }
114 Err(e) => return Err(e.into()),
115 }
116 Ok(SuperadminIdentity {
117 id: db.last_insert_rowid(),
118 kind,
119 value,
120 added_at: now,
121 added_by: added_by.to_string(),
122 })
123 }
124
125 pub fn remove_superadmin_identity(
127 &self,
128 kind: AgentKind,
129 value: &str,
130 ) -> AuthResult<SuperadminIdentity> {
131 let value = normalize_subject(kind, value)?;
132 let db = self.db();
133 let found = db
134 .query_row(
135 &format!("SELECT {COLS} FROM superadmin_identities WHERE kind = ?1 AND value = ?2"),
136 params![kind.as_str(), value],
137 row,
138 )
139 .optional()?;
140 let Some(found) = found else {
141 return Err(AuthError::NotFound(format!(
142 "{} {value} is not a superadmin in isb.db (one from --superadmin-{} is removed from that flag)",
143 kind.as_str(),
144 kind.as_str()
145 )));
146 };
147 db.execute(
148 "DELETE FROM superadmin_identities WHERE id = ?1",
149 [found.id],
150 )?;
151 Ok(found)
152 }
153
154 pub fn list_superadmin_identities(&self) -> AuthResult<Vec<SuperadminIdentity>> {
155 let db = self.db();
156 let mut st = db.prepare(&format!(
157 "SELECT {COLS} FROM superadmin_identities ORDER BY kind, value"
158 ))?;
159 let rows = st.query_map([], row)?;
160 Ok(rows.collect::<rusqlite::Result<_>>()?)
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167 use crate::auth::AuthConfig;
168
169 #[test]
170 fn add_list_remove_and_validate() {
171 let s = AuthStore::in_memory(AuthConfig::default()).unwrap();
172 let a = s
173 .add_superadmin_identity(AgentKind::Access, " Alice@Example.com ", "local(uid 1)")
174 .unwrap();
175 assert_eq!(
176 (a.value.as_str(), a.added_by.as_str()),
177 ("alice@example.com", "local(uid 1)")
178 );
179 s.add_superadmin_identity(AgentKind::Access, "Svc.Access", "t")
180 .unwrap();
181 s.add_superadmin_identity(AgentKind::Tailnet, "TAG:Agents", "t")
182 .unwrap();
183 s.add_superadmin_identity(AgentKind::Tailnet, "me@example.com", "t")
184 .unwrap();
185 assert!(matches!(
187 s.add_superadmin_identity(AgentKind::Access, "alice@EXAMPLE.com", "t"),
188 Err(AuthError::Conflict(_))
189 ));
190 for (k, v) in [
192 (AgentKind::Access, ""),
193 (AgentKind::Access, " "),
194 (AgentKind::Access, "*@example.com"),
195 (AgentKind::Access, "@example.com"),
196 (AgentKind::Access, "a b@example.com"),
197 (AgentKind::Tailnet, "nobody"),
198 (AgentKind::Tailnet, "tag:"),
199 (AgentKind::Tailnet, "tag:a.b"),
200 (AgentKind::Tailnet, "a@x.io,b@x.io"),
201 ] {
202 assert!(
203 matches!(
204 s.add_superadmin_identity(k, v, "t"),
205 Err(AuthError::Invalid(_))
206 ),
207 "{k:?} {v:?}"
208 );
209 }
210 let l = s.list_superadmin_identities().unwrap();
211 let got: Vec<_> = l.iter().map(|i| (i.kind, i.value.as_str())).collect();
212 assert_eq!(
213 got,
214 vec![
215 (AgentKind::Access, "Svc.Access"),
216 (AgentKind::Access, "alice@example.com"),
217 (AgentKind::Tailnet, "me@example.com"),
218 (AgentKind::Tailnet, "tag:agents"),
219 ]
220 );
221 let find = |v: &str| l.iter().find(|i| i.value == v).unwrap();
224 assert!(find("alice@example.com").admits_access(Some("ALICE@example.com"), None));
225 assert!(!find("alice@example.com").admits_access(None, Some("alice@example.com")));
226 assert!(find("Svc.Access").admits_access(None, Some("Svc.Access")));
227 assert!(!find("Svc.Access").admits_access(None, Some("svc.access")));
228 assert!(!find("Svc.Access").admits_tailnet("Svc.Access", &[]));
229 assert!(find("tag:agents").admits_tailnet("tagged-devices", &["tag:agents".into()]));
230 assert!(!find("tag:agents").admits_tailnet("tag:agents", &[]));
231 assert!(find("me@example.com").admits_tailnet("Me@example.com", &[]));
232 assert!(!find("me@example.com").admits_tailnet("me@example.com", &["tag:x".into()]));
233 assert!(!find("me@example.com").admits_access(Some("me@example.com"), None));
234 let r = s
236 .remove_superadmin_identity(AgentKind::Access, "ALICE@example.com")
237 .unwrap();
238 assert_eq!(r.id, a.id);
239 assert!(matches!(
240 s.remove_superadmin_identity(AgentKind::Access, "alice@example.com"),
241 Err(AuthError::NotFound(_))
242 ));
243 assert_eq!(s.list_superadmin_identities().unwrap().len(), 3);
244 }
245}