use axum::http::request::Parts;
#[derive(Clone, Debug)]
pub(crate) struct RequestTenant(pub(crate) String);
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum Actor {
TenantUser {
id: i64,
is_superuser: bool,
tenant: Option<String>,
},
Operator {
id: i64,
impersonating: Option<String>,
},
Agent {
owner: Option<i64>,
agent_id: i64,
tenant: String,
},
OpenAdmin,
Anonymous,
}
impl Actor {
#[must_use]
pub fn of(parts: &Parts) -> Self {
let routed = parts
.extensions
.get::<RequestTenant>()
.map(|t| t.0.as_str());
if let Some(scoped) = parts.extensions.get::<crate::admin::session::PermsScoped>() {
return Self::admin(parts, scoped.0, routed);
}
let actor = Self::api(parts);
if actor.tenant() == routed {
actor
} else {
Self::Anonymous
}
}
fn admin(parts: &Parts, perms_scoped: bool, routed: Option<&str>) -> Self {
let Some(s) = crate::admin::session::from_extensions(&parts.extensions) else {
return if perms_scoped {
Self::Anonymous
} else {
Self::OpenAdmin
};
};
match s.impersonated_by {
Some(id) => Self::Operator {
id,
impersonating: routed.filter(|_| !perms_scoped).map(str::to_owned),
},
None => Self::TenantUser {
id: s.user_id,
is_superuser: s.is_superuser && !perms_scoped,
tenant: routed.map(str::to_owned),
},
}
}
fn api(parts: &Parts) -> Self {
#[cfg(feature = "tenancy")]
if let Some(p) = crate::tenancy::Principal::from_parts(parts) {
return match (p.kind, p.agent_id) {
(crate::tenancy::PrincipalKind::Agent, Some(agent_id)) => Self::Agent {
owner: Some(p.user_id),
agent_id,
tenant: p.tenant.unwrap_or_default(),
},
_ => Self::TenantUser {
id: p.user_id,
is_superuser: p.is_superuser,
tenant: p.tenant,
},
};
}
#[cfg(feature = "mcp")]
if let Some(a) = parts.extensions.get::<crate::mcp::auth::McpAgent>() {
return Self::Agent {
owner: a.user_id,
agent_id: a.agent_id,
tenant: a.tenant.clone(),
};
}
match parts
.extensions
.get::<crate::admin::session::AdminSession>()
{
Some(s) if s.impersonated_by.is_none() => Self::TenantUser {
id: s.user_id,
is_superuser: s.is_superuser,
tenant: None,
},
_ => Self::Anonymous,
}
}
#[must_use]
pub fn tenant(&self) -> Option<&str> {
match self {
Self::TenantUser { tenant, .. } => tenant.as_deref(),
Self::Operator { impersonating, .. } => impersonating.as_deref(),
Self::Agent { tenant, .. } => Some(tenant),
Self::OpenAdmin | Self::Anonymous => None,
}
}
#[must_use]
pub fn is_superuser(&self) -> bool {
match self {
Self::TenantUser { is_superuser, .. } => *is_superuser,
Self::Operator { impersonating, .. } => impersonating.is_some(),
Self::OpenAdmin => true,
Self::Agent { .. } | Self::Anonymous => false,
}
}
}
#[must_use]
pub fn is_superuser(parts: &Parts) -> bool {
Actor::of(parts).is_superuser()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::admin::session::{AdminSession, PermsScoped};
fn parts() -> Parts {
axum::http::Request::builder()
.body(())
.unwrap()
.into_parts()
.0
}
fn with(f: impl FnOnce(&mut axum::http::Extensions)) -> Parts {
let mut p = parts();
f(&mut p.extensions);
p
}
#[test]
fn nothing_is_anonymous_and_never_superuser() {
assert_eq!(Actor::of(&parts()), Actor::Anonymous);
assert!(!is_superuser(&parts()));
}
#[test]
fn an_admin_session_follows_the_perms_rule() {
let su = |scoped| {
with(|e| {
e.insert(PermsScoped(scoped));
e.insert(AdminSession::new(3, "a", true));
})
};
assert!(is_superuser(&su(false)));
assert!(!is_superuser(&su(true)));
let open = with(|e| {
e.insert(PermsScoped(false));
});
assert_eq!(Actor::of(&open), Actor::OpenAdmin);
let closed = with(|e| {
e.insert(PermsScoped(true));
});
assert_eq!(Actor::of(&closed), Actor::Anonymous);
}
#[test]
fn impersonation_counts_only_for_the_routed_tenant() {
let imp = |routed: Option<&str>| {
with(|e| {
e.insert(PermsScoped(false));
e.insert(AdminSession::impersonation(9));
if let Some(t) = routed {
e.insert(RequestTenant(t.to_owned()));
}
})
};
assert_eq!(
Actor::of(&imp(Some("acme"))),
Actor::Operator {
id: 9,
impersonating: Some("acme".into())
}
);
assert!(is_superuser(&imp(Some("acme"))));
assert!(!is_superuser(&imp(None)));
let api = with(|e| {
e.insert(AdminSession::impersonation(9));
});
assert_eq!(Actor::of(&api), Actor::Anonymous);
}
#[test]
fn an_admin_session_outside_the_admin_is_its_user() {
let p = with(|e| {
e.insert(AdminSession::new(4, "b", true));
});
assert!(is_superuser(&p));
let routed = with(|e| {
e.insert(AdminSession::new(4, "b", true));
e.insert(RequestTenant("acme".into()));
});
assert_eq!(Actor::of(&routed), Actor::Anonymous);
}
#[cfg(feature = "tenancy")]
#[test]
fn an_api_user_is_bound_to_its_tenant() {
use crate::tenancy::{middleware::AuthenticatedUser, TenantSlug};
let user = |slug: &str, routed: &str| {
with(|e| {
e.insert(AuthenticatedUser {
id: 7,
username: "ada".into(),
is_superuser: true,
});
e.insert(TenantSlug::new(1, slug));
e.insert(RequestTenant(routed.to_owned()));
})
};
assert_eq!(
Actor::of(&user("acme", "acme")),
Actor::TenantUser {
id: 7,
is_superuser: true,
tenant: Some("acme".into())
}
);
assert_eq!(Actor::of(&user("acme", "beta")), Actor::Anonymous);
assert!(!is_superuser(&user("acme", "beta")));
}
#[cfg(feature = "tenancy")]
#[test]
fn a_tenant_credential_off_a_tenant_route_is_anonymous() {
use crate::tenancy::{middleware::AuthenticatedUser, TenantSlug};
let p = with(|e| {
e.insert(AuthenticatedUser {
id: 7,
username: "ada".into(),
is_superuser: true,
});
e.insert(TenantSlug::new(1, "acme"));
});
assert_eq!(Actor::of(&p), Actor::Anonymous);
assert!(!is_superuser(&p));
}
#[cfg(feature = "mcp")]
#[test]
fn an_agent_is_never_superuser() {
let agent = |owner| {
with(|e| {
e.insert(crate::mcp::auth::McpAgent {
agent_id: 5,
tenant: "acme".into(),
skills: vec![],
tools: vec![],
user_id: owner,
jti: "j".into(),
secret_prefix: "p".into(),
});
e.insert(RequestTenant("acme".into()));
})
};
for owner in [Some(7), None] {
let p = agent(owner);
assert_eq!(
Actor::of(&p),
Actor::Agent {
owner,
agent_id: 5,
tenant: "acme".into()
}
);
assert!(!is_superuser(&p));
}
}
}