1use 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
18pub const SCHEME: &str = "IsbAssert";
20
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23pub struct Assertion {
24 pub email: String,
25 #[serde(default)]
26 pub name: String,
27 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 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 pub fn for_caller(c: &Caller) -> Option<Self> {
61 match c {
62 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 Caller::Unauthenticated { addr } => Some(Assertion {
75 email: format!("unauthenticated {addr} via control plane"),
76 via: "local".into(),
77 ..Self::control_plane()
78 }),
79 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 PrincipalKind::Agent { label } => {
96 ("agent", Some(label.clone()), None, Vec::new())
97 }
98 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 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 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 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}