use serde_json::{Value, json};
use super::{accounts, audit, superadmin};
use crate::error::{Error, Result};
use crate::server::Caller;
pub(super) const PLATFORM_TOOLS: &[&str] = &[
"server_status",
"org_list",
"org_create",
"org_update",
"org_delete",
"registry_gc",
"notification_settings",
"template_catalog_add",
"template_catalog_remove",
"audit_verify",
"server_add",
"server_list",
"server_show",
"server_remove",
"server_rotate_cert",
"server_provision_get",
"server_upgrade",
"user_list",
"user_update",
];
pub(super) const CROSS_ORG_READS: &[&str] = &["overview", "events", "stack_list", "ingress_status"];
pub(super) fn tool_listed(c: &Caller, tool: &str, scope: Option<&crate::org::OrgId>) -> bool {
scope.is_none()
|| c.is_local()
|| !(superadmin::TOOLS.contains(&tool) || PLATFORM_TOOLS.contains(&tool))
}
pub(super) fn scoped(mut details: Value, c: &Caller) -> Value {
if let Some(org) = c.downscope() {
details["scope"] = json!(format!("org {org}"));
}
details
}
pub(super) fn arg_org(args: &Value) -> Result<crate::org::OrgId> {
match args.get("org").and_then(Value::as_str) {
Some(o) => crate::org::OrgId::new(o),
None => Ok(crate::org::OrgId::default_org()),
}
}
pub(super) fn authorize_class(
c: &Caller,
tool: &str,
cls: audit::Class,
mut args: Value,
scope: Option<&crate::org::OrgId>,
allow_anonymous: bool,
) -> Result<Value> {
if let Some(org) = scope {
match args.get("org").and_then(Value::as_str) {
Some(o) if o != org.as_str() => {
return Err(Error::Forbidden(format!(
"this endpoint acts in org {org}, not {o}"
)));
}
_ => args["org"] = json!(org.as_str()),
}
}
match c {
Caller::Local { .. } | Caller::Superadmin(_) => Ok(args),
_ if superadmin::TOOLS.contains(&tool) => Err(Error::Forbidden(format!(
"{tool} is for superadmins (the unix socket, a superadmin token, or a listed tailnet or Access identity)"
))),
Caller::Unauthenticated { .. } if allow_anonymous => Ok(args),
Caller::Unauthenticated { .. } => Err(Error::Forbidden(
"sign in: send an API token as Authorization: Bearer (isb token create)".into(),
)),
Caller::Access(id) => Err(Error::Forbidden(format!(
"{} has no isb account; ask an org admin to invite you",
id.name()
))),
Caller::User { principal: p } => {
if !audit::scope_allows(p.scopes(), tool, cls) {
return Err(Error::Forbidden(format!(
"this token's scopes ({}) do not cover {tool}",
p.scopes().join(", ")
)));
}
if accounts::TOOLS.contains(&tool) {
accounts::authorize(p, tool, cls)?;
if PLATFORM_TOOLS.contains(&tool) && !p.platform_admin {
return Err(Error::Forbidden(format!("{tool} is for platform admins")));
}
return Ok(args);
}
if PLATFORM_TOOLS.contains(&tool) && !p.platform_admin {
return Err(Error::Forbidden(format!("{} is for platform admins", tool)));
}
if tool == "secret_reencrypt"
&& args.get("all").and_then(Value::as_bool) == Some(true)
&& !p.platform_admin
{
return Err(Error::Forbidden(
"re-encrypting every org is for platform admins".into(),
));
}
if (CROSS_ORG_READS.contains(&tool) && scope.is_none())
|| tool == "audit_list"
|| tool == "history_query"
{
return Ok(args);
}
let org = arg_org(&args)?;
if p.platform_admin {
return Ok(args);
}
match p.role_in(&org) {
None => Err(Error::Forbidden(format!("no access to org {org}"))),
Some(_) if cls.read_only => Ok(args),
Some(_) if p.can_admin_org(&org) => Ok(args),
Some(r) => Err(Error::Forbidden(format!(
"{tool} changes things or reads secrets; a {r} in org {org} only reads"
))),
}
}
}
}