Skip to main content

isb_server/servers/
wire.rs

1//! What the control plane tells an agent about the caller it forwards.
2//!
3//! The control plane authenticates and authorizes every call, then forwards
4//! it with an [`Assertion`] of who the caller is, in `Authorization:
5//! IsbAssert <base64url JSON>`. The agent believes it only because the
6//! connection is mutual TLS with the control plane's client certificate;
7//! it still runs its own authorizer over it (org scope, role, token scopes),
8//! so a forwarded call for org X cannot reach org Y.
9
10use base64::Engine;
11use serde::{Deserialize, Serialize};
12
13use crate::auth::{Principal, PrincipalKind, Role, User};
14use crate::error::{Error, Result};
15use crate::org::OrgId;
16use crate::server::Caller;
17
18/// The `Authorization` scheme of a forwarded call.
19pub const SCHEME: &str = "IsbAssert";
20
21/// A caller, as the control plane vouches for it.
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23pub struct Assertion {
24    pub email: String,
25    #[serde(default)]
26    pub name: String,
27    /// `session`, `token`, `access`, `local` (the control plane's socket)
28    /// or `control-plane` (the control plane itself: heartbeats, events).
29    pub via: String,
30    #[serde(default, skip_serializing_if = "Option::is_none")]
31    pub token_name: Option<String>,
32    #[serde(default, skip_serializing_if = "Option::is_none")]
33    pub token_org: Option<OrgId>,
34    #[serde(default, skip_serializing_if = "Vec::is_empty")]
35    pub scopes: Vec<String>,
36    #[serde(default)]
37    pub orgs: Vec<(OrgId, Role)>,
38    #[serde(default)]
39    pub platform_admin: bool,
40}
41
42impl Assertion {
43    /// The control plane acting for itself.
44    pub fn control_plane() -> Self {
45        Assertion {
46            email: "control-plane".into(),
47            name: "isb control plane".into(),
48            via: "control-plane".into(),
49            token_name: None,
50            token_org: None,
51            scopes: Vec::new(),
52            orgs: Vec::new(),
53            platform_admin: true,
54        }
55    }
56
57    /// What to assert for a caller the control plane admitted. `None` for
58    /// callers that never reach a forward (Access identities without an
59    /// account; the authorizer refuses them first).
60    pub fn for_caller(c: &Caller) -> Option<Self> {
61        match c {
62            // The control plane's own user: it could run isb against the
63            // control plane directly, so it administers every org.
64            Caller::Local { uid } => Some(Assertion {
65                email: match uid {
66                    Some(u) => format!("local(uid {u}) via control plane"),
67                    None => "local via control plane".into(),
68                },
69                via: "local".into(),
70                ..Self::control_plane()
71            }),
72            // Only reaches a forward on a control plane serving with
73            // --allow-unauthenticated, which makes every caller an admin.
74            Caller::Unauthenticated { addr } => Some(Assertion {
75                email: format!("unauthenticated {addr} via control plane"),
76                via: "local".into(),
77                ..Self::control_plane()
78            }),
79            // A superadmin of the control plane is the control plane's
80            // own user over HTTP: it administers every org, and the agent
81            // judges it as it judges the control plane (its policy holds).
82            Caller::Superadmin(s) => Some(Assertion {
83                email: format!("{} via control plane", s.label()),
84                via: "superadmin".into(),
85                ..Self::control_plane()
86            }),
87            Caller::Access(_) => None,
88            Caller::User { principal: p } => {
89                let (via, token_name, token_org, scopes) = match &p.kind {
90                    PrincipalKind::Session { .. } => ("session", None, None, Vec::new()),
91                    PrincipalKind::Access => ("access", None, None, Vec::new()),
92                    PrincipalKind::Superadmin { .. } => ("superadmin", None, None, Vec::new()),
93                    // A tailnet or Access agent identity: its label names
94                    // it, its orgs and roles say what it may do.
95                    PrincipalKind::Agent { label } => {
96                        ("agent", Some(label.clone()), None, Vec::new())
97                    }
98                    // The workspace of an org on this server: the agent
99                    // judges it as the workspace it is.
100                    PrincipalKind::Workspace { org, name } => (
101                        "workspace",
102                        Some(name.clone()),
103                        Some(org.clone()),
104                        Vec::new(),
105                    ),
106                    PrincipalKind::ApiToken {
107                        org, name, scopes, ..
108                    } => ("token", Some(name.clone()), org.clone(), scopes.clone()),
109                };
110                Some(Assertion {
111                    email: p.user.email.clone(),
112                    name: p.user.name.clone(),
113                    via: via.into(),
114                    token_name,
115                    token_org,
116                    scopes,
117                    orgs: p.orgs.clone(),
118                    platform_admin: p.platform_admin,
119                })
120            }
121        }
122    }
123
124    /// The `Authorization` header value.
125    pub fn header(&self) -> String {
126        let json = serde_json::to_vec(self).unwrap_or_default();
127        format!(
128            "{SCHEME} {}",
129            base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(json)
130        )
131    }
132
133    /// Parse an `Authorization` header value.
134    pub fn from_header(v: &str) -> Result<Self> {
135        let b64 = v
136            .strip_prefix(SCHEME)
137            .and_then(|r| r.strip_prefix(' '))
138            .ok_or_else(|| Error::Forbidden("not a control-plane assertion".into()))?;
139        let json = base64::engine::general_purpose::URL_SAFE_NO_PAD
140            .decode(b64.trim())
141            .map_err(|_| Error::Forbidden("a malformed assertion".into()))?;
142        serde_json::from_slice(&json)
143            .map_err(|e| Error::Forbidden(format!("a malformed assertion: {e}")))
144    }
145
146    /// The principal the agent's authorizer judges.
147    pub fn principal(&self) -> Principal {
148        let kind = match self.via.as_str() {
149            "token" => PrincipalKind::ApiToken {
150                id: 0,
151                org: self.token_org.clone(),
152                name: self.token_name.clone().unwrap_or_default(),
153                scopes: self.scopes.clone(),
154            },
155            "access" => PrincipalKind::Access,
156            "agent" => PrincipalKind::Agent {
157                label: self.token_name.clone().unwrap_or_default(),
158            },
159            "workspace" => match &self.token_org {
160                Some(org) => PrincipalKind::Workspace {
161                    org: org.clone(),
162                    name: self.token_name.clone().unwrap_or_default(),
163                },
164                None => PrincipalKind::Session { id: 0 },
165            },
166            _ => PrincipalKind::Session { id: 0 },
167        };
168        Principal {
169            user: User {
170                id: 0,
171                email: self.email.clone(),
172                name: self.name.clone(),
173                platform_admin: self.platform_admin,
174                created_at: 0,
175                disabled: false,
176                has_password: false,
177            },
178            kind,
179            orgs: self.orgs.clone(),
180            platform_admin: self.platform_admin,
181            downscoped: None,
182        }
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189    use std::sync::Arc;
190
191    #[test]
192    fn a_token_round_trips_with_its_scopes() {
193        let p = Principal {
194            user: User {
195                id: 7,
196                email: "a@example.com".into(),
197                name: "A".into(),
198                platform_admin: false,
199                created_at: 1,
200                disabled: false,
201                has_password: true,
202            },
203            kind: PrincipalKind::ApiToken {
204                id: 3,
205                org: Some(OrgId::new("acme").unwrap()),
206                name: "agent".into(),
207                scopes: vec!["read".into()],
208            },
209            orgs: vec![(OrgId::new("acme").unwrap(), Role::Member)],
210            platform_admin: false,
211            downscoped: None,
212        };
213        let a = Assertion::for_caller(&Caller::User {
214            principal: Arc::new(p),
215        })
216        .unwrap();
217        let back = Assertion::from_header(&a.header()).unwrap();
218        assert_eq!(a, back);
219        let q = back.principal();
220        assert_eq!(q.scopes(), ["read".to_string()]);
221        assert_eq!(q.role_in(&OrgId::new("acme").unwrap()), Some(Role::Member));
222        assert!(!q.platform_admin);
223        assert!(Assertion::from_header("Bearer x").is_err());
224        assert!(Assertion::from_header("IsbAssert !!!").is_err());
225    }
226
227    #[test]
228    fn the_local_socket_becomes_a_platform_admin_not_a_local_caller() {
229        let a = Assertion::for_caller(&Caller::Local { uid: Some(1000) }).unwrap();
230        assert!(a.platform_admin);
231        assert!(a.email.contains("uid 1000"));
232    }
233}