use base64::Engine;
use serde::{Deserialize, Serialize};
use crate::auth::{Principal, PrincipalKind, Role, User};
use crate::error::{Error, Result};
use crate::org::OrgId;
use crate::server::Caller;
pub const SCHEME: &str = "IsbAssert";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Assertion {
pub email: String,
#[serde(default)]
pub name: String,
pub via: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub token_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub token_org: Option<OrgId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub scopes: Vec<String>,
#[serde(default)]
pub orgs: Vec<(OrgId, Role)>,
#[serde(default)]
pub platform_admin: bool,
}
impl Assertion {
pub fn control_plane() -> Self {
Assertion {
email: "control-plane".into(),
name: "isb control plane".into(),
via: "control-plane".into(),
token_name: None,
token_org: None,
scopes: Vec::new(),
orgs: Vec::new(),
platform_admin: true,
}
}
pub fn for_caller(c: &Caller) -> Option<Self> {
match c {
Caller::Local { uid } => Some(Assertion {
email: match uid {
Some(u) => format!("local(uid {u}) via control plane"),
None => "local via control plane".into(),
},
via: "local".into(),
..Self::control_plane()
}),
Caller::Unauthenticated { addr } => Some(Assertion {
email: format!("unauthenticated {addr} via control plane"),
via: "local".into(),
..Self::control_plane()
}),
Caller::Superadmin(s) => Some(Assertion {
email: format!("{} via control plane", s.label()),
via: "superadmin".into(),
..Self::control_plane()
}),
Caller::Access(_) => None,
Caller::User { principal: p } => {
let (via, token_name, token_org, scopes) = match &p.kind {
PrincipalKind::Session { .. } => ("session", None, None, Vec::new()),
PrincipalKind::Access => ("access", None, None, Vec::new()),
PrincipalKind::Superadmin { .. } => ("superadmin", None, None, Vec::new()),
PrincipalKind::Agent { label } => {
("agent", Some(label.clone()), None, Vec::new())
}
PrincipalKind::Workspace { org, name } => (
"workspace",
Some(name.clone()),
Some(org.clone()),
Vec::new(),
),
PrincipalKind::ApiToken {
org, name, scopes, ..
} => ("token", Some(name.clone()), org.clone(), scopes.clone()),
};
Some(Assertion {
email: p.user.email.clone(),
name: p.user.name.clone(),
via: via.into(),
token_name,
token_org,
scopes,
orgs: p.orgs.clone(),
platform_admin: p.platform_admin,
})
}
}
}
pub fn header(&self) -> String {
let json = serde_json::to_vec(self).unwrap_or_default();
format!(
"{SCHEME} {}",
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(json)
)
}
pub fn from_header(v: &str) -> Result<Self> {
let b64 = v
.strip_prefix(SCHEME)
.and_then(|r| r.strip_prefix(' '))
.ok_or_else(|| Error::Forbidden("not a control-plane assertion".into()))?;
let json = base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode(b64.trim())
.map_err(|_| Error::Forbidden("a malformed assertion".into()))?;
serde_json::from_slice(&json)
.map_err(|e| Error::Forbidden(format!("a malformed assertion: {e}")))
}
pub fn principal(&self) -> Principal {
let kind = match self.via.as_str() {
"token" => PrincipalKind::ApiToken {
id: 0,
org: self.token_org.clone(),
name: self.token_name.clone().unwrap_or_default(),
scopes: self.scopes.clone(),
},
"access" => PrincipalKind::Access,
"agent" => PrincipalKind::Agent {
label: self.token_name.clone().unwrap_or_default(),
},
"workspace" => match &self.token_org {
Some(org) => PrincipalKind::Workspace {
org: org.clone(),
name: self.token_name.clone().unwrap_or_default(),
},
None => PrincipalKind::Session { id: 0 },
},
_ => PrincipalKind::Session { id: 0 },
};
Principal {
user: User {
id: 0,
email: self.email.clone(),
name: self.name.clone(),
platform_admin: self.platform_admin,
created_at: 0,
disabled: false,
has_password: false,
},
kind,
orgs: self.orgs.clone(),
platform_admin: self.platform_admin,
downscoped: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
#[test]
fn a_token_round_trips_with_its_scopes() {
let p = Principal {
user: User {
id: 7,
email: "a@example.com".into(),
name: "A".into(),
platform_admin: false,
created_at: 1,
disabled: false,
has_password: true,
},
kind: PrincipalKind::ApiToken {
id: 3,
org: Some(OrgId::new("acme").unwrap()),
name: "agent".into(),
scopes: vec!["read".into()],
},
orgs: vec![(OrgId::new("acme").unwrap(), Role::Member)],
platform_admin: false,
downscoped: None,
};
let a = Assertion::for_caller(&Caller::User {
principal: Arc::new(p),
})
.unwrap();
let back = Assertion::from_header(&a.header()).unwrap();
assert_eq!(a, back);
let q = back.principal();
assert_eq!(q.scopes(), ["read".to_string()]);
assert_eq!(q.role_in(&OrgId::new("acme").unwrap()), Some(Role::Member));
assert!(!q.platform_admin);
assert!(Assertion::from_header("Bearer x").is_err());
assert!(Assertion::from_header("IsbAssert !!!").is_err());
}
#[test]
fn the_local_socket_becomes_a_platform_admin_not_a_local_caller() {
let a = Assertion::for_caller(&Caller::Local { uid: Some(1000) }).unwrap();
assert!(a.platform_admin);
assert!(a.email.contains("uid 1000"));
}
}