Skip to main content

isb_daemon/daemon/
superadmin.rs

1//! Who is a superadmin ([`crate::auth::superadmin`]) on this daemon: a
2//! superadmin token, a tailnet identity on `--superadmin-tailnet`, or a
3//! verified Cloudflare Access identity on `--superadmin-access`. Nothing
4//! else grants it. One gate serves the tool endpoints (through the authn
5//! hook) and the identity endpoints (`/api/v1/auth/*`).
6
7use 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/// `--superadmin-access`: Access emails and service-token client ids.
20#[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    /// Comma-separated; an entry with `@` is an email, else a service
28    /// token's client id. Exact matches only: no wildcards or domains.
29    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    /// A user by email (case-insensitively); a service token by client id.
52    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
69/// What the gate makes of a request.
70pub enum Resolved {
71    /// Not a superadmin credential: judge the request as before.
72    None,
73    /// A superadmin token that is not valid.
74    Refused,
75    Superadmin(Arc<Superadmin>),
76}
77
78pub struct Gate {
79    store: Arc<AuthStore>,
80    tailnet: Option<Tailnet>,
81    /// The tailnet `--listen` addresses (what lets a tailnet peer in at all).
82    tailnet_listens: Vec<String>,
83    /// The validator of the listeners Access guards, and the `Host` names an
84    /// Access agent's request may carry (empty: no public URL, not checked).
85    access_agents: Option<(Arc<AccessValidator>, Vec<String>)>,
86    /// The Access validator of the loopback listeners, the allow list, and
87    /// the `Host` names an Access superadmin's request may carry.
88    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    /// Let orgs' tailnet and Access agent identities in
113    /// ([`crate::auth::agent_identities`]): the tailnet `--listen`
114    /// addresses, and Access's validator with the `Host` names to allow.
115    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    /// Which agent identities can reach this server at all.
131    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    /// The agent identity behind `req`, if an org maps it: a verified
153    /// Access identity (`id`, else the request's own assertion) on a
154    /// loopback listener Access guards, or a tailnet peer, as tailscaled
155    /// says. A bearer token decides on its own, so it is not asked here.
156    /// Superadmin sources are judged first, by [`Gate::resolve`].
157    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    /// `id` is the Access identity the listener already verified, if any.
203    /// A bearer superadmin token decides alone; any other bearer token is
204    /// not this gate's. Then Access, then the tailnet.
205    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    /// Only from a verified assertion, on the loopback listeners Access
251    /// guards.
252    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    /// As the enabled isb user with this email, else synthetic.
286    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
298/// Tools for superadmins only (not platform admins): the host itself, and
299/// what reaches further into its kernel (nesting for an org's workspace).
300pub const TOOLS: &[&str] = &[
301    "host_inventory",
302    "host_policy",
303    "superadmin_token_list",
304    "superadmin_token_revoke",
305    "org_nesting",
306];
307
308/// A `--listen` address on the tailnet (it then binds without a tunnel).
309pub 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
317/// The public URL's host, for the `Host` checks.
318fn 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
324/// The gate `cfg` describes. Refuses `--superadmin-access` without Access or a
325/// public URL (whose host the Access check needs).
326pub 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    // The tailnet check runs wherever a tailnet address is served: for the
340    // superadmin allow list, and for orgs' agent identities.
341    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
380/// What `host_policy` reports of the configuration (never a secret).
381pub 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
408/// Say at start-up which sources grant superadmin.
409pub 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
433/// The superadmin-only tools.
434pub(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        // A service token's id never matches as an email, nor the reverse.
615        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        // An ordinary API token is not the gate's.
635        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                // Acts as the isb user with that email.
667                assert_eq!(sa.principal.user.id, alice.id);
668            }
669            _ => panic!("expected a superadmin"),
670        }
671        // A forged (unsigned) assertion.
672        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        // Another email.
675        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        // DNS rebinding: another Host.
686        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        // Off the loopback listeners Access guards.
695        let tail = req("100.64.0.1:1", &[("Cf-Access-Jwt-Assertion", &token), host]);
696        assert!(label(g.resolve(&tail, None)).is_none());
697        // A service token by client id, synthetic.
698        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        // A bearer token takes the request away from the tailnet.
754        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                // A tagged node owned by the mapped login.
799                "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        // No superadmin allow list at all: agents still resolve.
812        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        // An untagged node by login: viewer in beta only.
818        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        // A tagged node by its tag: member in acme only.
823        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        // A tagged node is never its owner's login.
827        assert!(who("100.64.0.3:1", &[host]).is_none());
828        assert!(who("100.64.0.9:1", &[host]).is_none());
829        // Not a tailnet address, or another Host (DNS rebinding).
830        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        // A bearer token takes the request away from the tailnet.
834        assert!(who("100.64.0.1:1", &[host, ("Authorization", "Bearer x")]).is_none());
835        // And a tailnet agent is no superadmin.
836        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        // The isb user's email acts as that user (the authn hook's job); the
860        // gate never makes it an agent.
861        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        // The test claims' email is alice's: an isb user, so no agent.
872        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        // An unmapped client id, a forged assertion, a foreign Host, a
888        // non-loopback peer.
889        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        // Never a superadmin.
923        assert!(matches!(g.resolve(&ok, None), Resolved::None));
924    }
925}