1use rusqlite::{OptionalExtension, Row, params};
18use serde::{Deserialize, Serialize};
19
20use super::{AuthError, AuthResult, AuthStore, Principal, Role, clean_name, org_col, role_col};
21use crate::org::OrgId;
22
23pub const MAX_PER_ORG: i64 = 100;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
28#[serde(rename_all = "snake_case")]
29pub enum AgentKind {
30 Tailnet,
31 Access,
32}
33
34impl AgentKind {
35 pub fn as_str(self) -> &'static str {
36 match self {
37 AgentKind::Tailnet => "tailnet",
38 AgentKind::Access => "access",
39 }
40 }
41
42 pub fn parse(s: &str) -> AuthResult<AgentKind> {
43 match s {
44 "tailnet" => Ok(AgentKind::Tailnet),
45 "access" => Ok(AgentKind::Access),
46 _ => Err(AuthError::Invalid(format!("kind {s:?}: tailnet or access"))),
47 }
48 }
49}
50
51#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
54pub struct AgentWays {
55 pub tailnet_listen: Vec<String>,
58 pub access: bool,
60 pub public_url: Option<String>,
62 #[serde(skip)]
65 pub superadmin_access: Vec<String>,
66 #[serde(skip)]
68 pub superadmin_tailnet: Vec<String>,
69}
70
71impl AgentWays {
72 pub fn with_public_url(mut self, url: Option<String>) -> AgentWays {
74 self.public_url = url;
75 self
76 }
77}
78
79#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
81pub struct AgentIdentity {
82 pub id: i64,
83 pub org: OrgId,
84 pub kind: AgentKind,
85 pub subject: String,
88 pub role: Role,
89 pub note: String,
90 pub created_at: i64,
91 pub created_by: String,
93}
94
95pub fn normalize_subject(kind: AgentKind, subject: &str) -> AuthResult<String> {
98 let s = subject.trim();
99 let bad = |m: &str| {
100 Err(AuthError::Invalid(format!(
101 "{} identity {s:?}: {m}",
102 kind.as_str()
103 )))
104 };
105 if s.is_empty() || s.len() > 200 {
106 return bad("1 to 200 characters");
107 }
108 if s.contains(['*', '?', ',']) || s.chars().any(|c| c.is_whitespace() || c.is_control()) {
109 return bad("exact names only: no wildcards, spaces or commas");
110 }
111 match kind {
112 AgentKind::Tailnet => {
113 if let Some(t) = s
114 .get(..4)
115 .filter(|p| p.eq_ignore_ascii_case("tag:"))
116 .map(|_| &s[4..])
117 {
118 if t.is_empty()
119 || !t
120 .bytes()
121 .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_'))
122 {
123 return bad("a tag is tag: and letters, digits, - or _");
124 }
125 Ok(format!("tag:{}", t.to_ascii_lowercase()))
126 } else if s.contains('@') && !s.starts_with('@') && !s.ends_with('@') {
127 Ok(s.to_ascii_lowercase())
128 } else {
129 bad("a login name (someone@example.com) or a tag (tag:name)")
130 }
131 }
132 AgentKind::Access => {
133 if s.contains('@') {
134 if s.starts_with('@') || s.ends_with('@') {
135 return bad("a whole email address");
136 }
137 Ok(s.to_ascii_lowercase())
138 } else {
139 Ok(s.to_string())
140 }
141 }
142 }
143}
144
145fn row(r: &Row) -> rusqlite::Result<AgentIdentity> {
146 let kind: String = r.get(2)?;
147 Ok(AgentIdentity {
148 id: r.get(0)?,
149 org: org_col(r, 1)?,
150 kind: AgentKind::parse(&kind).unwrap_or(AgentKind::Access),
151 subject: r.get(3)?,
152 role: role_col(r, 4)?,
153 note: r.get(5)?,
154 created_at: r.get(6)?,
155 created_by: r.get(7)?,
156 })
157}
158
159const COLS: &str = "id, org, kind, subject, role, note, created_at, created_by";
160
161impl AuthStore {
162 pub fn list_agent_identities(&self, org: &OrgId) -> AuthResult<Vec<AgentIdentity>> {
164 let db = self.db();
165 let mut st = db.prepare(&format!(
166 "SELECT {COLS} FROM org_agent_identities WHERE org = ?1 ORDER BY id"
167 ))?;
168 let rows = st.query_map([org.as_str()], row)?;
169 Ok(rows.collect::<rusqlite::Result<_>>()?)
170 }
171
172 pub fn set_agent_identity(
176 &self,
177 org: &OrgId,
178 kind: AgentKind,
179 subject: &str,
180 role: Role,
181 note: &str,
182 created_by: &str,
183 ) -> AuthResult<AgentIdentity> {
184 if role == Role::Owner {
185 return Err(AuthError::Invalid(
186 "an agent identity is a viewer, member or admin: never an owner".into(),
187 ));
188 }
189 let subject = normalize_subject(kind, subject)?;
190 let note = if note.trim().is_empty() {
191 String::new()
192 } else {
193 clean_name(note)?
194 };
195 if kind == AgentKind::Access
196 && subject.contains('@')
197 && self.user_by_email(&subject)?.is_some()
198 {
199 return Err(AuthError::Conflict(format!(
200 "a Cloudflare Access caller with the email {subject} already acts as the isb user with that address; add that user as a member of the org instead"
201 )));
202 }
203 let db = self.db();
204 let n: i64 = db.query_row(
205 "SELECT COUNT(*) FROM org_agent_identities WHERE org = ?1",
206 [org.as_str()],
207 |r| r.get(0),
208 )?;
209 let exists: Option<i64> = db
210 .query_row(
211 "SELECT id FROM org_agent_identities WHERE org = ?1 AND kind = ?2 AND subject = ?3",
212 params![org.as_str(), kind.as_str(), subject],
213 |r| r.get(0),
214 )
215 .optional()?;
216 let id = match exists {
217 Some(id) => {
218 db.execute(
219 "UPDATE org_agent_identities SET role = ?2, note = ?3 WHERE id = ?1",
220 params![id, role.as_str(), note],
221 )?;
222 id
223 }
224 None => {
225 if n >= MAX_PER_ORG {
226 return Err(AuthError::Invalid(format!(
227 "an org holds at most {MAX_PER_ORG} agent identities; remove one first"
228 )));
229 }
230 let r = db.execute(
231 "INSERT INTO org_agent_identities (org, kind, subject, role, note, created_at, created_by)
232 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
233 params![
234 org.as_str(),
235 kind.as_str(),
236 subject,
237 role.as_str(),
238 note,
239 self.now(),
240 created_by
241 ],
242 );
243 match r {
244 Ok(_) => db.last_insert_rowid(),
245 Err(rusqlite::Error::SqliteFailure(e, _))
246 if e.code == rusqlite::ErrorCode::ConstraintViolation =>
247 {
248 return Err(AuthError::NotFound(format!("org {org}")));
249 }
250 Err(e) => return Err(e.into()),
251 }
252 }
253 };
254 Ok(db.query_row(
255 &format!("SELECT {COLS} FROM org_agent_identities WHERE id = ?1"),
256 [id],
257 row,
258 )?)
259 }
260
261 pub fn remove_agent_identity(&self, org: &OrgId, id: i64) -> AuthResult<bool> {
263 let n = self.db().execute(
264 "DELETE FROM org_agent_identities WHERE id = ?1 AND org = ?2",
265 params![id, org.as_str()],
266 )?;
267 Ok(n > 0)
268 }
269
270 pub fn agent_orgs(
273 &self,
274 kind: AgentKind,
275 subjects: &[String],
276 ) -> AuthResult<Vec<(OrgId, Role)>> {
277 let mut out: Vec<(OrgId, Role)> = Vec::new();
278 let db = self.db();
279 let mut st = db.prepare(
280 "SELECT org, role FROM org_agent_identities WHERE kind = ?1 AND subject = ?2",
281 )?;
282 for s in subjects {
283 let rows = st.query_map(params![kind.as_str(), s], |r| {
284 Ok((org_col(r, 0)?, role_col(r, 1)?))
285 })?;
286 for r in rows {
287 let (org, role) = r?;
288 let role = role.min(Role::Admin);
290 match out.iter_mut().find(|(o, _)| *o == org) {
291 Some((_, r0)) => *r0 = (*r0).max(role),
292 None => out.push((org, role)),
293 }
294 }
295 }
296 out.sort_by(|a, b| a.0.as_str().cmp(b.0.as_str()));
297 Ok(out)
298 }
299
300 pub fn principal_for_tailnet(
303 &self,
304 login: &str,
305 node: &str,
306 tags: &[String],
307 ) -> AuthResult<Option<Principal>> {
308 let subjects: Vec<String> = if tags.is_empty() {
309 vec![login.to_ascii_lowercase()]
310 } else {
311 tags.iter().map(|t| t.to_ascii_lowercase()).collect()
312 };
313 let orgs = self.agent_orgs(AgentKind::Tailnet, &subjects)?;
314 if orgs.is_empty() {
315 return Ok(None);
316 }
317 let name = if tags.is_empty() { login } else { node };
318 Ok(Some(Principal::agent(&format!("tailnet:{name}"), orgs)))
319 }
320
321 pub fn principal_for_access_agent(
326 &self,
327 email: Option<&str>,
328 client_id: Option<&str>,
329 ) -> AuthResult<Option<Principal>> {
330 let name = match (email, client_id) {
331 (Some(e), _) => {
332 if self.user_by_email(e)?.is_some() {
333 return Ok(None);
334 }
335 e.to_ascii_lowercase()
336 }
337 (None, Some(c)) => c.to_string(),
338 (None, None) => return Ok(None),
339 };
340 let orgs = self.agent_orgs(AgentKind::Access, std::slice::from_ref(&name))?;
341 if orgs.is_empty() {
342 return Ok(None);
343 }
344 Ok(Some(Principal::agent(&format!("access:{name}"), orgs)))
345 }
346}
347
348#[cfg(test)]
349mod tests {
350 use super::*;
351 use crate::auth::AuthConfig;
352
353 fn store() -> AuthStore {
354 AuthStore::in_memory(AuthConfig {
355 password_cost: crate::auth::secret::PasswordCost::insecure_fast(),
356 ..Default::default()
357 })
358 .unwrap()
359 }
360
361 fn org(s: &AuthStore, name: &str) -> OrgId {
362 let o = OrgId::new(name).unwrap();
363 s.ensure_org(&o).unwrap();
364 o
365 }
366
367 #[test]
368 fn subjects_are_exact_and_normalized() {
369 use AgentKind::*;
370 assert_eq!(
371 normalize_subject(Tailnet, " Me@Example.com ").unwrap(),
372 "me@example.com"
373 );
374 assert_eq!(
375 normalize_subject(Tailnet, "tag:Agents").unwrap(),
376 "tag:agents"
377 );
378 for bad in [
379 "",
380 "tag:",
381 "tag:a b",
382 "*@example.com",
383 "@example.com",
384 "nobody",
385 "tag:a*",
386 ] {
387 assert!(normalize_subject(Tailnet, bad).is_err(), "{bad}");
388 }
389 assert_eq!(
390 normalize_subject(Access, "Bob@Example.com").unwrap(),
391 "bob@example.com"
392 );
393 assert_eq!(
394 normalize_subject(Access, "AbC123.access").unwrap(),
395 "AbC123.access"
396 );
397 assert!(normalize_subject(Access, "a@").is_err());
398 assert!(normalize_subject(Access, "a,b").is_err());
399 }
400
401 #[test]
402 fn mappings_upsert_cap_and_never_own() {
403 let s = store();
404 let acme = org(&s, "acme");
405 let set = |o: &OrgId, k, subj: &str, r| s.set_agent_identity(o, k, subj, r, "", "me");
406 let a = set(&acme, AgentKind::Tailnet, "tag:agents", Role::Member).unwrap();
407 assert_eq!(a.role, Role::Member);
408 let again = set(&acme, AgentKind::Tailnet, "TAG:agents", Role::Viewer).unwrap();
409 assert_eq!(again.id, a.id);
410 assert_eq!(again.role, Role::Viewer);
411 assert_eq!(s.list_agent_identities(&acme).unwrap().len(), 1);
412 assert!(set(&acme, AgentKind::Tailnet, "tag:x", Role::Owner).is_err());
413 assert!(s.remove_agent_identity(&acme, a.id).unwrap());
414 assert!(!s.remove_agent_identity(&acme, a.id).unwrap());
415 let b = set(&acme, AgentKind::Access, "svc.access", Role::Admin).unwrap();
417 let other = org(&s, "other");
418 assert!(!s.remove_agent_identity(&other, b.id).unwrap());
419 s.create_user("alice@example.com", "Alice", None, false)
421 .unwrap();
422 assert!(matches!(
423 set(&acme, AgentKind::Access, "Alice@example.com", Role::Member),
424 Err(AuthError::Conflict(_))
425 ));
426 let ghost = OrgId::new("ghost").unwrap();
428 assert!(set(&ghost, AgentKind::Access, "x.access", Role::Member).is_err());
429 }
430
431 #[test]
432 fn tailnet_tags_and_logins_never_mix() {
433 let s = store();
434 let acme = org(&s, "acme");
435 let set = |subj: &str, r| s.set_agent_identity(&acme, AgentKind::Tailnet, subj, r, "", "x");
436 set("me@example.com", Role::Admin).unwrap();
437 set("tag:agents", Role::Viewer).unwrap();
438 let p = s
440 .principal_for_tailnet("me@example.com", "laptop.t.ts.net", &[])
441 .unwrap()
442 .unwrap();
443 assert_eq!(p.user.email, "tailnet:me@example.com");
444 assert_eq!(p.orgs, vec![(acme.clone(), Role::Admin)]);
445 assert!(!p.platform_admin && p.is_agent() && p.user.id == 0);
446 let p = s
448 .principal_for_tailnet("me@example.com", "bot.t.ts.net", &["tag:agents".into()])
449 .unwrap()
450 .unwrap();
451 assert_eq!(p.user.email, "tailnet:bot.t.ts.net");
452 assert_eq!(p.orgs, vec![(acme.clone(), Role::Viewer)]);
453 assert!(
455 s.principal_for_tailnet("me@example.com", "bot.t.ts.net", &["tag:other".into()])
456 .unwrap()
457 .is_none()
458 );
459 assert!(
461 s.principal_for_tailnet("agents@example.com", "x.t.ts.net", &[])
462 .unwrap()
463 .is_none()
464 );
465 }
466
467 #[test]
468 fn identities_pin_to_the_orgs_that_map_them() {
469 let s = store();
470 let acme = org(&s, "acme");
471 let beta = org(&s, "beta");
472 let set = |o: &OrgId, k, subj: &str, r| s.set_agent_identity(o, k, subj, r, "", "x");
473 set(&acme, AgentKind::Access, "svc.access", Role::Admin).unwrap();
474 set(&beta, AgentKind::Access, "svc.access", Role::Viewer).unwrap();
475 set(
476 &beta,
477 AgentKind::Tailnet,
478 "svc.access@example.com",
479 Role::Admin,
480 )
481 .unwrap();
482 let p = s
483 .principal_for_access_agent(None, Some("svc.access"))
484 .unwrap()
485 .unwrap();
486 assert_eq!(p.role_in(&acme), Some(Role::Admin));
487 assert_eq!(p.role_in(&beta), Some(Role::Viewer));
488 assert_eq!(p.role_in(&OrgId::new("gamma").unwrap()), None);
489 assert!(!p.can_admin_org(&beta) && p.can_read_org(&beta));
490 assert!(
492 s.principal_for_access_agent(Some("svc.access@example.com"), None)
493 .unwrap()
494 .is_none()
495 );
496 set(&acme, AgentKind::Access, "eve@example.com", Role::Member).unwrap();
499 assert!(
500 s.principal_for_access_agent(Some("EVE@example.com"), None)
501 .unwrap()
502 .is_some()
503 );
504 s.create_user("eve@example.com", "Eve", None, false)
505 .unwrap();
506 assert!(
507 s.principal_for_access_agent(Some("eve@example.com"), None)
508 .unwrap()
509 .is_none()
510 );
511 }
512}