1use std::sync::Arc;
8
9use serde_json::{Value, json};
10
11use crate::auth::agent_identities::AgentWays;
12use crate::auth::{AuthStore, Principal, Superadmin, SuperadminSource};
13use crate::error::{Error, Result};
14use crate::server::access::{ASSERTION_HEADER, AccessValidator, Identity};
15use crate::server::http::{Peer, Request};
16use crate::server::tailnet::{Tailnet, host_only};
17use crate::server::{Registry, Tool};
18
19#[derive(Debug, Clone, PartialEq, Eq, Default)]
21pub struct AccessAllowList {
22 pub emails: Vec<String>,
23 pub client_ids: Vec<String>,
24}
25
26impl AccessAllowList {
27 pub fn parse(list: &str) -> Result<AccessAllowList> {
30 let mut a = AccessAllowList::default();
31 for e in list.split(',').map(str::trim).filter(|e| !e.is_empty()) {
32 if e.contains(['*', '?', ' ', '\t']) || e.starts_with('@') || e.ends_with('@') {
33 return Err(Error::invalid(format!(
34 "--superadmin-access: {e:?}: exact emails or service token client ids only"
35 )));
36 }
37 if e.contains('@') {
38 a.emails.push(e.to_ascii_lowercase());
39 } else {
40 a.client_ids.push(e.to_string());
41 }
42 }
43 if a.emails.is_empty() && a.client_ids.is_empty() {
44 return Err(Error::invalid(
45 "--superadmin-access needs at least one email or service token client id",
46 ));
47 }
48 Ok(a)
49 }
50
51 pub fn admits(&self, id: &Identity) -> bool {
53 match (&id.email, &id.common_name) {
54 (Some(e), _) => self.emails.iter().any(|x| x.eq_ignore_ascii_case(e)),
55 (None, Some(cn)) => self.client_ids.iter().any(|x| x == cn),
56 (None, None) => false,
57 }
58 }
59
60 pub fn entries(&self) -> Vec<String> {
61 self.emails
62 .iter()
63 .chain(&self.client_ids)
64 .cloned()
65 .collect()
66 }
67}
68
69pub enum Resolved {
71 None,
73 Refused,
75 Superadmin(Arc<Superadmin>),
76}
77
78pub struct Gate {
79 store: Arc<AuthStore>,
80 tailnet: Option<Tailnet>,
81 tailnet_listens: Vec<String>,
83 access_agents: Option<(Arc<AccessValidator>, Vec<String>)>,
86 access: Option<(Arc<AccessValidator>, AccessAllowList, Vec<String>)>,
89}
90
91impl Gate {
92 pub fn new(
93 store: Arc<AuthStore>,
94 tailnet: Option<Tailnet>,
95 access: Option<(Arc<AccessValidator>, AccessAllowList, Vec<String>)>,
96 ) -> Gate {
97 let access = access.map(|(v, a, hosts)| {
98 let mut hosts: Vec<String> = hosts.iter().map(|h| host_only(h)).collect();
99 hosts.sort();
100 hosts.dedup();
101 (v, a, hosts)
102 });
103 Gate {
104 store,
105 tailnet,
106 tailnet_listens: Vec::new(),
107 access_agents: None,
108 access,
109 }
110 }
111
112 pub fn with_agents(
116 mut self,
117 tailnet_listens: Vec<String>,
118 access: Option<(Arc<AccessValidator>, Vec<String>)>,
119 ) -> Gate {
120 self.tailnet_listens = tailnet_listens;
121 self.access_agents = access.map(|(v, hosts)| {
122 let mut hosts: Vec<String> = hosts.iter().map(|h| host_only(h)).collect();
123 hosts.sort();
124 hosts.dedup();
125 (v, hosts)
126 });
127 self
128 }
129
130 pub fn agent_ways(&self) -> AgentWays {
132 AgentWays {
133 tailnet_listen: if self.tailnet.is_some() {
134 self.tailnet_listens.clone()
135 } else {
136 Vec::new()
137 },
138 access: self.access_agents.is_some(),
139 public_url: None,
140 superadmin_access: self
141 .access_list()
142 .map(AccessAllowList::entries)
143 .unwrap_or_default(),
144 superadmin_tailnet: self
145 .tailnet
146 .as_ref()
147 .map(|t| t.allow().entries())
148 .unwrap_or_default(),
149 }
150 }
151
152 pub fn agent(&self, req: &Request, id: Option<&Identity>) -> Option<Principal> {
158 if req.header("authorization").is_some() {
159 return None;
160 }
161 let looked = if matches!(&req.peer, Peer::Tcp(a) if a.ip().is_loopback()) {
162 let (v, hosts) = self.access_agents.as_ref()?;
163 let verified;
164 let id = match id {
165 Some(id) => id,
166 None => {
167 let t = req.header(ASSERTION_HEADER)?.trim();
168 verified = v.validate(t).ok()?;
169 &verified
170 }
171 };
172 if !hosts.is_empty() {
173 let host = req.header("host").map(host_only).unwrap_or_default();
174 if !hosts.contains(&host) {
175 eprintln!(
176 "isb serve: Access agent {} sent Host {host:?}, not one of this server's names; not an agent",
177 id.name()
178 );
179 return None;
180 }
181 }
182 self.store
183 .principal_for_access_agent(id.email.as_deref(), id.common_name.as_deref())
184 } else {
185 let w = self.tailnet.as_ref()?.identify(req)?;
186 self.store.principal_for_tailnet(&w.login, &w.node, &w.tags)
187 };
188 looked.unwrap_or_else(|e| {
189 eprintln!("isb serve: agent identities: {e}");
190 None
191 })
192 }
193
194 pub fn tailnet(&self) -> Option<&Tailnet> {
195 self.tailnet.as_ref()
196 }
197
198 pub fn access_list(&self) -> Option<&AccessAllowList> {
199 self.access.as_ref().map(|(_, a, _)| a)
200 }
201
202 pub fn resolve(&self, req: &Request, id: Option<&Identity>) -> Resolved {
206 if let Some(a) = req.header("authorization") {
207 let token = a
208 .trim()
209 .split_once(' ')
210 .filter(|(s, _)| s.eq_ignore_ascii_case("bearer"))
211 .map(|(_, t)| t.trim());
212 return match token {
213 Some(t) if t.starts_with(crate::auth::secret::TokenKind::Superadmin.prefix()) => {
214 match self.store.authenticate_superadmin_token(t) {
215 Ok(Some(info)) => Resolved::Superadmin(Arc::new(Superadmin::synthetic(
216 SuperadminSource::Token {
217 id: info.id,
218 name: info.name,
219 },
220 ))),
221 Ok(None) => Resolved::Refused,
222 Err(e) => {
223 eprintln!("isb serve: superadmin token: {e}");
224 Resolved::Refused
225 }
226 }
227 }
228 _ => Resolved::None,
229 };
230 }
231 if let Some(s) = self.access_superadmin(req, id) {
232 return Resolved::Superadmin(s);
233 }
234 if let Some(w) = self.tailnet.as_ref().and_then(|t| t.superadmin(req)) {
235 let source = SuperadminSource::Tailnet {
236 login: w.login.clone(),
237 node: w.node,
238 tags: w.tags.clone(),
239 };
240 let as_user = if w.tags.is_empty() {
241 Some(w.login.as_str())
242 } else {
243 None
244 };
245 return Resolved::Superadmin(Arc::new(self.acting_as(source, as_user)));
246 }
247 Resolved::None
248 }
249
250 fn access_superadmin(&self, req: &Request, id: Option<&Identity>) -> Option<Arc<Superadmin>> {
253 let (v, allow, hosts) = self.access.as_ref()?;
254 let loopback = matches!(&req.peer, Peer::Tcp(a) if a.ip().is_loopback());
255 if !loopback {
256 return None;
257 }
258 let verified;
259 let id = match id {
260 Some(id) => id,
261 None => {
262 let t = req.header(ASSERTION_HEADER)?.trim();
263 verified = v.validate(t).ok()?;
264 &verified
265 }
266 };
267 if !allow.admits(id) {
268 return None;
269 }
270 let host = req.header("host").map(host_only).unwrap_or_default();
271 if !hosts.contains(&host) {
272 eprintln!(
273 "isb serve: Access superadmin {} sent Host {host:?}, not one of this server's names; not a superadmin",
274 id.name()
275 );
276 return None;
277 }
278 let source = SuperadminSource::Access {
279 name: id.name().to_string(),
280 service_token: id.is_service_token(),
281 };
282 Some(Arc::new(self.acting_as(source, id.email.as_deref())))
283 }
284
285 fn acting_as(&self, source: SuperadminSource, email: Option<&str>) -> Superadmin {
287 let user = email
288 .and_then(|e| self.store.user_by_email(e).ok().flatten())
289 .filter(|u| !u.disabled);
290 match user {
291 Some(u) => Superadmin::as_user(source.clone(), u, &self.store)
292 .unwrap_or_else(|_| Superadmin::synthetic(source)),
293 None => Superadmin::synthetic(source),
294 }
295 }
296}
297
298pub const TOOLS: &[&str] = &[
301 "host_inventory",
302 "host_policy",
303 "superadmin_token_list",
304 "superadmin_token_revoke",
305 "org_nesting",
306];
307
308pub fn is_tailnet_listen(addr: &str) -> bool {
310 use std::net::ToSocketAddrs;
311 addr.to_socket_addrs().is_ok_and(|mut a| {
312 a.next()
313 .is_some_and(|a| crate::server::tailnet::is_tailnet_ip(a.ip()))
314 })
315}
316
317fn public_host(url: &str) -> Option<String> {
319 let rest = url.trim().split_once("://")?.1;
320 let host = rest.split(['/', '?', '#']).next()?;
321 (!host.is_empty()).then(|| host.to_string())
322}
323
324pub fn gate(
327 cfg: &super::ServeConfig,
328 store: Arc<AuthStore>,
329 access: Option<Arc<AccessValidator>>,
330) -> Result<Gate> {
331 let tailnet_listens: Vec<&String> =
332 cfg.listen.iter().filter(|a| is_tailnet_listen(a)).collect();
333 let public = cfg.public_url.as_deref().and_then(public_host);
334 if cfg.superadmin_tailnet.is_some() && tailnet_listens.is_empty() {
335 eprintln!(
336 "isb serve: WARNING: --superadmin-tailnet without a tailnet --listen address: no tailnet peer can reach this daemon"
337 );
338 }
339 let tailnet = (cfg.superadmin_tailnet.is_some() || !tailnet_listens.is_empty()).then(|| {
342 let allow = cfg.superadmin_tailnet.clone().unwrap_or_default();
343 let mut hosts: Vec<String> = tailnet_listens.iter().map(|a| a.to_string()).collect();
344 hosts.extend(crate::server::tailnet::self_names());
345 hosts.extend(public.clone());
346 crate::server::tailnet::Tailnet::new(allow, crate::server::tailnet::system_fetcher(), hosts)
347 });
348 let mut access_hosts: Vec<String> = public.iter().cloned().collect();
349 access_hosts.extend(cfg.listen.iter().filter(|a| !is_tailnet_listen(a)).cloned());
350 let agent_access = access.clone().map(|v| {
351 let hosts = if public.is_some() {
352 access_hosts
353 } else {
354 Vec::new()
355 };
356 (v, hosts)
357 });
358 let access = match (&cfg.superadmin_access, access) {
359 (None, _) => None,
360 (Some(_), None) => {
361 return Err(Error::invalid(
362 "--superadmin-access needs Cloudflare Access (CF_ACCESS_TEAM_DOMAIN and CF_ACCESS_AUD): it trusts only a verified assertion",
363 ));
364 }
365 (Some(list), Some(v)) => {
366 let Some(host) = public.clone() else {
367 return Err(Error::invalid(
368 "--superadmin-access needs --public-url: an Access superadmin's request must name this server's host",
369 ));
370 };
371 let mut hosts = vec![host];
372 hosts.extend(cfg.listen.iter().filter(|a| !is_tailnet_listen(a)).cloned());
373 Some((v, list.clone(), hosts))
374 }
375 };
376 let listens = tailnet_listens.iter().map(|a| a.to_string()).collect();
377 Ok(Gate::new(store, tailnet, access).with_agents(listens, agent_access))
378}
379
380pub fn host_summary(cfg: &super::ServeConfig, gate: &Gate) -> Value {
382 json!({
383 "isb": env!("CARGO_PKG_VERSION"),
384 "listen": cfg.listen,
385 "socket": cfg.socket,
386 "state_dir": cfg.state_dir,
387 "public_url": cfg.public_url,
388 "access": cfg.access.as_ref().map(|(team, aud)| json!({"team_domain": team, "aud": aud})),
389 "allow_unauthenticated": cfg.allow_unauthenticated,
390 "tools": {"allow": cfg.remote_tools.allow, "deny": cfg.remote_tools.deny},
391 "policy": {
392 "allow_privileged": cfg.policy.allow_privileged,
393 "allow_raw": cfg.policy.allow_raw,
394 "bind_roots": cfg.policy.bind_roots,
395 "publish_addresses": cfg.policy.publish_addresses,
396 "any_instance": cfg.policy.any_instance,
397 },
398 "superadmin": {
399 "socket": cfg.socket,
400 "tokens": true,
401 "tailnet": cfg.superadmin_tailnet.as_ref().and(gate.tailnet()).map(|t| t.allow().entries()),
402 "tailnet_hosts": gate.tailnet().map(|t| t.hosts().to_vec()),
403 "access": gate.access_list().map(AccessAllowList::entries),
404 },
405 })
406}
407
408pub fn announce(cfg: &super::ServeConfig, gate: &Gate, store: &AuthStore) {
410 let mut v = vec![format!("the unix socket {}", cfg.socket.display())];
411 if !cfg.listen.is_empty() {
412 let n = store.list_superadmin_tokens().map(|t| t.len()).unwrap_or(0);
413 v.push(format!(
414 "superadmin tokens ({n}; minted on this host with `isb token create NAME --superadmin`)"
415 ));
416 }
417 if let Some(t) = cfg.superadmin_tailnet.as_ref().and(gate.tailnet()) {
418 v.push(format!(
419 "tailnet identities {} (Host: {})",
420 t.allow().entries().join(", "),
421 t.hosts().join(", ")
422 ));
423 }
424 if let Some(a) = gate.access_list() {
425 v.push(format!(
426 "Cloudflare Access identities {}",
427 a.entries().join(", ")
428 ));
429 }
430 eprintln!("isb serve: superadmins: {}", v.join("; "));
431}
432
433pub(super) fn register(r: &mut Registry, d: Arc<super::Daemon>) -> Result<()> {
435 let ro = json!({"readOnlyHint": true, "openWorldHint": false});
436 let destructive = json!({"destructiveHint": true, "openWorldHint": false});
437 let empty = || json!({"type": "object", "properties": {"org": {"type": "string"}}, "additionalProperties": false});
438 let dd = d.clone();
439 r.register(
440 Tool::new(
441 "host_inventory",
442 "Every incus project and instance on the host, isb's or not: projects with the org each one is (if any); instances with project, type, status, addresses, and isb's labels (stack, owner). Superadmins only.",
443 empty(),
444 move |_a, _c| inventory(&dd),
445 )
446 .title("Host inventory")
447 .annotations(ro.clone()),
448 )?;
449 let dd = d.clone();
450 r.register(
451 Tool::new(
452 "host_policy",
453 "How this daemon serves: listen addresses, Cloudflare Access, the tools remote callers see, what a remote caller's specs may ask for (bind roots, publish addresses, privileged, raw, any instance), and every superadmin source with its allow lists. Superadmins only.",
454 empty(),
455 move |_a, _c| {
456 let mut v = dd.host.clone();
457 v["superadmin"]["token_count"] =
458 json!(dd.users.list_superadmin_tokens().map(|t| t.len()).unwrap_or(0));
459 Ok(v)
460 },
461 )
462 .title("Host policy")
463 .annotations(ro.clone()),
464 )?;
465 let dd = d.clone();
466 r.register(
467 Tool::new(
468 "superadmin_token_list",
469 "Superadmin tokens: id, name, created, last used, expiry (never the token). They are minted only on the host: isb token create NAME --superadmin. Superadmins only.",
470 empty(),
471 move |_a, _c| Ok(json!({"tokens": dd.users.list_superadmin_tokens()?})),
472 )
473 .title("Superadmin tokens")
474 .annotations(ro),
475 )?;
476 let dd = d;
477 r.register(
478 Tool::new(
479 "superadmin_token_revoke",
480 "Revoke a superadmin token by id; it stops working at once. Superadmins only.",
481 json!({"type": "object", "properties": {"id": {"type": "integer"}, "org": {"type": "string"}}, "required": ["id"], "additionalProperties": false}),
482 move |a, _c| {
483 let id = a
484 .get("id")
485 .and_then(Value::as_i64)
486 .ok_or_else(|| Error::invalid("id: an integer"))?;
487 let t = dd.users.superadmin_token(id)?;
488 dd.users.revoke_superadmin_token(id)?;
489 Ok(json!({"revoked": t}))
490 },
491 )
492 .title("Revoke a superadmin token")
493 .annotations(destructive),
494 )?;
495 Ok(())
496}
497
498fn inventory(d: &super::Daemon) -> Result<Value> {
499 let projects = d.client.get("/1.0/projects?recursion=1")?;
500 let projects: Vec<Value> = projects
501 .as_array()
502 .map(|a| {
503 a.iter()
504 .map(|p| {
505 let name = p["name"].as_str().unwrap_or("");
506 json!({
507 "name": name,
508 "description": p["description"],
509 "org": crate::org::OrgId::from_incus_project(name).map(|o| o.to_string()),
510 "instances": p["used_by"].as_array().map(|u| u.iter().filter(|x| x.as_str().is_some_and(|s| s.starts_with("/1.0/instances/"))).count()).unwrap_or(0),
511 })
512 })
513 .collect()
514 })
515 .unwrap_or_default();
516 let instances = d
517 .client
518 .get("/1.0/instances?recursion=2&all-projects=true")?;
519 let instances: Vec<Value> = instances
520 .as_array()
521 .map(|a| {
522 a.iter()
523 .map(|i| {
524 let project = i["project"].as_str().unwrap_or("default");
525 let cfg = &i["config"];
526 let label = |k: &str| cfg.get(format!("user.{k}")).cloned().unwrap_or(Value::Null);
527 let addresses: Vec<String> = i["state"]["network"]
528 .as_object()
529 .map(|n| {
530 n.iter()
531 .filter(|(k, _)| k.as_str() != "lo")
532 .flat_map(|(_, v)| v["addresses"].as_array().cloned().unwrap_or_default())
533 .filter(|a| a["scope"] == "global")
534 .filter_map(|a| a["address"].as_str().map(String::from))
535 .collect()
536 })
537 .unwrap_or_default();
538 let stack = label("isb.stack");
539 let owner = label(super::LABEL_OWNER);
540 json!({
541 "name": i["name"],
542 "project": project,
543 "org": crate::org::OrgId::from_incus_project(project).map(|o| o.to_string()),
544 "type": i["type"],
545 "status": i["status"],
546 "created_at": i["created_at"],
547 "image": cfg.get("image.description").cloned().unwrap_or(Value::Null),
548 "addresses": addresses,
549 "stack": stack,
550 "owner": owner,
551 "managed": !stack.is_null() || !owner.is_null(),
552 })
553 })
554 .collect()
555 })
556 .unwrap_or_default();
557 Ok(json!({"projects": projects, "instances": instances}))
558}
559
560#[cfg(test)]
561mod tests {
562 use super::*;
563 use crate::auth::AuthConfig;
564 use crate::server::access::tests as at;
565 use crate::server::tailnet::{AllowList, Whois, WhoisFetcher};
566 use serde_json::json;
567
568 fn req(peer: &str, headers: &[(&str, &str)]) -> Request {
569 Request {
570 method: "GET".into(),
571 path: "/mcp".into(),
572 query: None,
573 headers: headers
574 .iter()
575 .map(|(k, v)| (k.to_string(), v.to_string()))
576 .collect(),
577 body: Vec::new(),
578 peer: Peer::Tcp(peer.parse().unwrap()),
579 }
580 }
581
582 fn store() -> Arc<AuthStore> {
583 let c = AuthConfig {
584 password_cost: crate::auth::secret::PasswordCost::insecure_fast(),
585 ..Default::default()
586 };
587 Arc::new(AuthStore::in_memory(c).unwrap())
588 }
589
590 fn label(r: Resolved) -> Option<String> {
591 match r {
592 Resolved::Superadmin(s) => Some(s.label()),
593 _ => None,
594 }
595 }
596
597 #[test]
598 fn access_allow_lists() {
599 assert!(AccessAllowList::parse("").is_err());
600 assert!(AccessAllowList::parse("*@example.com").is_err());
601 assert!(AccessAllowList::parse("@example.com").is_err());
602 let a = AccessAllowList::parse("Alice@Example.com, abc123.access").unwrap();
603 let id = |email: Option<&str>, cn: Option<&str>| Identity {
604 email: email.map(String::from),
605 sub: "s".into(),
606 common_name: cn.map(String::from),
607 };
608 assert!(a.admits(&id(Some("alice@example.com"), None)));
609 assert!(a.admits(&id(Some("ALICE@example.com"), None)));
610 assert!(!a.admits(&id(Some("bob@example.com"), None)));
611 assert!(!a.admits(&id(Some("alice@example.com.evil"), None)));
612 assert!(a.admits(&id(None, Some("abc123.access"))));
613 assert!(!a.admits(&id(None, Some("other.access"))));
614 let b = AccessAllowList::parse("alice@example.com").unwrap();
616 assert!(!b.admits(&id(None, Some("alice@example.com"))));
617 }
618
619 #[test]
620 fn tokens_decide_alone() {
621 let s = store();
622 let t = s.create_superadmin_token("agent", None).unwrap();
623 let g = Gate::new(s.clone(), None, None);
624 let auth = format!("Bearer {}", t.token);
625 assert_eq!(
626 label(g.resolve(&req("127.0.0.1:1", &[("Authorization", &auth)]), None)).as_deref(),
627 Some("token:agent")
628 );
629 let bad = format!("Bearer {}x", t.token);
630 assert!(matches!(
631 g.resolve(&req("127.0.0.1:1", &[("Authorization", &bad)]), None),
632 Resolved::Refused
633 ));
634 assert!(matches!(
636 g.resolve(
637 &req("127.0.0.1:1", &[("Authorization", "Bearer isb_tok_x")]),
638 None
639 ),
640 Resolved::None
641 ));
642 }
643
644 #[test]
645 fn access_needs_a_verified_listed_identity_and_this_host() {
646 let s = store();
647 let alice = s
648 .create_user("alice@example.com", "Alice", None, false)
649 .unwrap();
650 let (v, _) = at::validator();
651 let g = Gate::new(
652 s.clone(),
653 None,
654 Some((
655 Arc::new(v),
656 AccessAllowList::parse("alice@example.com,svc.access").unwrap(),
657 vec!["https://isb.example.com".replace("https://", "")],
658 )),
659 );
660 let token = at::sign(&at::header(), &at::claims());
661 let host = ("Host", "isb.example.com");
662 let ok = req("127.0.0.1:1", &[("Cf-Access-Jwt-Assertion", &token), host]);
663 match g.resolve(&ok, None) {
664 Resolved::Superadmin(sa) => {
665 assert_eq!(sa.label(), "access:alice@example.com");
666 assert_eq!(sa.principal.user.id, alice.id);
668 }
669 _ => panic!("expected a superadmin"),
670 }
671 let forged = req("127.0.0.1:1", &[("Cf-Access-Jwt-Assertion", "a.b.c"), host]);
673 assert!(label(g.resolve(&forged, None)).is_none());
674 let mut c = at::claims();
676 c["email"] = json!("bob@example.com");
677 let bob = at::sign(&at::header(), &c);
678 assert!(
679 label(g.resolve(
680 &req("127.0.0.1:1", &[("Cf-Access-Jwt-Assertion", &bob), host]),
681 None
682 ))
683 .is_none()
684 );
685 let evil = req(
687 "127.0.0.1:1",
688 &[
689 ("Cf-Access-Jwt-Assertion", &token),
690 ("Host", "evil.example"),
691 ],
692 );
693 assert!(label(g.resolve(&evil, None)).is_none());
694 let tail = req("100.64.0.1:1", &[("Cf-Access-Jwt-Assertion", &token), host]);
696 assert!(label(g.resolve(&tail, None)).is_none());
697 let mut c = at::claims();
699 c.as_object_mut().unwrap().remove("email");
700 c["common_name"] = json!("svc.access");
701 let svc = at::sign(&at::header(), &c);
702 match g.resolve(
703 &req("127.0.0.1:1", &[("Cf-Access-Jwt-Assertion", &svc), host]),
704 None,
705 ) {
706 Resolved::Superadmin(sa) => {
707 assert_eq!(sa.label(), "access:svc.access");
708 assert!(!sa.has_account());
709 }
710 _ => panic!("expected a superadmin"),
711 }
712 }
713
714 #[test]
715 fn tailnet_acts_as_the_user_or_synthetic() {
716 let s = store();
717 let me = s.create_user("me@example.com", "Me", None, false).unwrap();
718 let fetch: WhoisFetcher = Arc::new(|p: std::net::SocketAddr| {
719 Ok(match p.ip().to_string().as_str() {
720 "100.64.0.1" => Whois {
721 login: "me@example.com".into(),
722 node: "laptop.t.ts.net".into(),
723 tags: vec![],
724 },
725 _ => Whois {
726 login: "tagged-devices".into(),
727 node: "agent.t.ts.net".into(),
728 tags: vec!["tag:agents".into()],
729 },
730 })
731 });
732 let t = Tailnet::new(
733 AllowList::parse("me@example.com,tag:agents").unwrap(),
734 fetch,
735 vec!["100.86.22.100".into()],
736 );
737 let g = Gate::new(s, Some(t), None);
738 let host = ("Host", "100.86.22.100:18995");
739 match g.resolve(&req("100.64.0.1:1", &[host]), None) {
740 Resolved::Superadmin(sa) => {
741 assert_eq!(sa.label(), "tailnet:me@example.com");
742 assert_eq!(sa.principal.user.id, me.id);
743 }
744 _ => panic!(),
745 }
746 match g.resolve(&req("100.64.0.2:1", &[host]), None) {
747 Resolved::Superadmin(sa) => {
748 assert_eq!(sa.label(), "tailnet:agent.t.ts.net");
749 assert!(!sa.has_account());
750 }
751 _ => panic!(),
752 }
753 assert!(matches!(
755 g.resolve(
756 &req(
757 "100.64.0.1:1",
758 &[host, ("Authorization", "Bearer isb_tok_x")]
759 ),
760 None
761 ),
762 Resolved::None
763 ));
764 }
765
766 fn agent_store() -> (Arc<AuthStore>, crate::org::OrgId, crate::org::OrgId) {
767 let s = store();
768 let acme = crate::org::OrgId::new("acme").unwrap();
769 let beta = crate::org::OrgId::new("beta").unwrap();
770 s.ensure_org(&acme).unwrap();
771 s.ensure_org(&beta).unwrap();
772 (s, acme, beta)
773 }
774
775 #[test]
776 fn tailnet_agents_are_pinned_to_the_orgs_that_map_them() {
777 use crate::auth::Role;
778 use crate::auth::agent_identities::AgentKind;
779 let (s, acme, beta) = agent_store();
780 let map = |o: &crate::org::OrgId, subj: &str, r| {
781 s.set_agent_identity(o, AgentKind::Tailnet, subj, r, "", "t")
782 .unwrap()
783 };
784 map(&acme, "tag:agents", Role::Member);
785 map(&beta, "me@example.com", Role::Viewer);
786 let fetch: WhoisFetcher = Arc::new(|p: std::net::SocketAddr| {
787 Ok(match p.ip().to_string().as_str() {
788 "100.64.0.1" => Whois {
789 login: "me@example.com".into(),
790 node: "laptop.t.ts.net".into(),
791 tags: vec![],
792 },
793 "100.64.0.2" => Whois {
794 login: "tagged-devices".into(),
795 node: "bot.t.ts.net".into(),
796 tags: vec!["tag:agents".into()],
797 },
798 "100.64.0.3" => Whois {
800 login: "me@example.com".into(),
801 node: "owned.t.ts.net".into(),
802 tags: vec!["tag:other".into()],
803 },
804 _ => Whois {
805 login: "stranger@example.com".into(),
806 node: "x.t.ts.net".into(),
807 tags: vec![],
808 },
809 })
810 });
811 let t = Tailnet::new(AllowList::default(), fetch, vec!["100.86.22.100".into()]);
813 let g = Gate::new(s, Some(t), None).with_agents(vec!["100.86.22.100:18995".into()], None);
814 assert_eq!(g.agent_ways().tailnet_listen, vec!["100.86.22.100:18995"]);
815 let host = ("Host", "100.86.22.100:18995");
816 let who = |peer: &str, h: &[(&str, &str)]| g.agent(&req(peer, h), None);
817 let p = who("100.64.0.1:1", &[host]).unwrap();
819 assert_eq!(p.user.email, "tailnet:me@example.com");
820 assert_eq!(p.orgs, vec![(beta.clone(), Role::Viewer)]);
821 assert!(p.role_in(&acme).is_none() && !p.platform_admin && p.user.id == 0);
822 let p = who("100.64.0.2:1", &[host]).unwrap();
824 assert_eq!(p.user.email, "tailnet:bot.t.ts.net");
825 assert_eq!(p.orgs, vec![(acme, Role::Member)]);
826 assert!(who("100.64.0.3:1", &[host]).is_none());
828 assert!(who("100.64.0.9:1", &[host]).is_none());
829 assert!(who("192.168.1.5:1", &[host]).is_none());
831 assert!(who("100.64.0.1:1", &[("Host", "evil.example")]).is_none());
832 assert!(who("100.64.0.1:1", &[]).is_none());
833 assert!(who("100.64.0.1:1", &[host, ("Authorization", "Bearer x")]).is_none());
835 assert!(matches!(
837 g.resolve(&req("100.64.0.1:1", &[host]), None),
838 Resolved::None
839 ));
840 assert!(
841 g.tailnet()
842 .unwrap()
843 .superadmin(&req("100.64.0.1:1", &[host]))
844 .is_none()
845 );
846 }
847
848 #[test]
849 fn access_agents_need_a_verified_mapped_identity_that_is_not_a_user() {
850 use crate::auth::Role;
851 use crate::auth::agent_identities::AgentKind;
852 let (s, acme, _beta) = agent_store();
853 let map = |subj: &str| {
854 s.set_agent_identity(&acme, AgentKind::Access, subj, Role::Admin, "", "t")
855 .unwrap()
856 };
857 map("svc.access");
858 map("alice@example.com");
859 s.create_user("alice@example.com", "Alice", None, false)
862 .ok();
863 let (v, _) = at::validator();
864 let g = Gate::new(s, None, None).with_agents(
865 Vec::new(),
866 Some((Arc::new(v), vec!["isb.example.com".into()])),
867 );
868 assert!(g.agent_ways().access);
869 let host = ("Host", "isb.example.com");
870 let token = at::sign(&at::header(), &at::claims());
871 assert!(
873 g.agent(
874 &req("127.0.0.1:1", &[("Cf-Access-Jwt-Assertion", &token), host]),
875 None
876 )
877 .is_none()
878 );
879 let mut c = at::claims();
880 c.as_object_mut().unwrap().remove("email");
881 c["common_name"] = json!("svc.access");
882 let svc = at::sign(&at::header(), &c);
883 let ok = req("127.0.0.1:1", &[("Cf-Access-Jwt-Assertion", &svc), host]);
884 let p = g.agent(&ok, None).unwrap();
885 assert_eq!(p.user.email, "access:svc.access");
886 assert_eq!(p.orgs, vec![(acme.clone(), Role::Admin)]);
887 c["common_name"] = json!("other.access");
890 let other = at::sign(&at::header(), &c);
891 assert!(
892 g.agent(
893 &req("127.0.0.1:1", &[("Cf-Access-Jwt-Assertion", &other), host]),
894 None
895 )
896 .is_none()
897 );
898 assert!(
899 g.agent(
900 &req("127.0.0.1:1", &[("Cf-Access-Jwt-Assertion", "a.b.c"), host]),
901 None
902 )
903 .is_none()
904 );
905 assert!(
906 g.agent(
907 &req(
908 "127.0.0.1:1",
909 &[("Cf-Access-Jwt-Assertion", &svc), ("Host", "evil.example")]
910 ),
911 None
912 )
913 .is_none()
914 );
915 assert!(
916 g.agent(
917 &req("100.64.0.1:1", &[("Cf-Access-Jwt-Assertion", &svc), host]),
918 None
919 )
920 .is_none()
921 );
922 assert!(matches!(g.resolve(&ok, None), Resolved::None));
924 }
925}