use serde::{Deserialize, Serialize};
use crate::policy::scope::PolicyScope;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PolicyScopeKind {
Global,
Org,
Team,
Agent,
Tool,
}
impl From<&PolicyScope> for PolicyScopeKind {
fn from(scope: &PolicyScope) -> Self {
match scope {
PolicyScope::Global => Self::Global,
PolicyScope::Org(_) => Self::Org,
PolicyScope::Team(_) => Self::Team,
PolicyScope::Agent(_) => Self::Agent,
PolicyScope::Tool(_) => Self::Tool,
}
}
}
impl std::fmt::Display for PolicyScopeKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Global => write!(f, "global"),
Self::Org => write!(f, "org"),
Self::Team => write!(f, "team"),
Self::Agent => write!(f, "agent"),
Self::Tool => write!(f, "tool"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MutationKind {
Create,
Update,
Delete,
}
impl std::fmt::Display for MutationKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Create => write!(f, "create"),
Self::Update => write!(f, "update"),
Self::Delete => write!(f, "delete"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CallerRole {
Auditor,
Viewer,
Developer,
TeamAdmin,
OrgAdmin,
}
impl std::fmt::Display for CallerRole {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::OrgAdmin => write!(f, "org_admin"),
Self::TeamAdmin => write!(f, "team_admin"),
Self::Developer => write!(f, "developer"),
Self::Viewer => write!(f, "viewer"),
Self::Auditor => write!(f, "auditor"),
}
}
}
impl CallerRole {
pub fn satisfies(self, required: CallerRole) -> bool {
if self == CallerRole::Auditor {
return false;
}
self >= required
}
}
pub fn required_role_for(scope: &PolicyScope, _mutation: MutationKind) -> CallerRole {
match PolicyScopeKind::from(scope) {
PolicyScopeKind::Global | PolicyScopeKind::Org => CallerRole::OrgAdmin,
PolicyScopeKind::Team => CallerRole::TeamAdmin,
PolicyScopeKind::Agent | PolicyScopeKind::Tool => CallerRole::Developer,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn global_scope_requires_org_admin() {
assert_eq!(
required_role_for(&PolicyScope::Global, MutationKind::Create),
CallerRole::OrgAdmin
);
}
#[test]
fn org_scope_requires_org_admin() {
assert_eq!(
required_role_for(&PolicyScope::Org("acme".into()), MutationKind::Update),
CallerRole::OrgAdmin
);
}
#[test]
fn team_scope_requires_team_admin() {
assert_eq!(
required_role_for(&PolicyScope::Team("platform".into()), MutationKind::Delete),
CallerRole::TeamAdmin
);
}
#[test]
fn agent_scope_requires_developer() {
use aa_core::identity::AgentId;
assert_eq!(
required_role_for(
&PolicyScope::Agent(AgentId::from_bytes([0u8; 16])),
MutationKind::Create
),
CallerRole::Developer
);
}
#[test]
fn tool_scope_requires_developer() {
assert_eq!(
required_role_for(&PolicyScope::Tool("slack-mcp".into()), MutationKind::Update),
CallerRole::Developer
);
}
#[test]
fn mutation_kind_does_not_change_required_role() {
let scope = PolicyScope::Team("x".into());
assert_eq!(
required_role_for(&scope, MutationKind::Create),
required_role_for(&scope, MutationKind::Update),
);
assert_eq!(
required_role_for(&scope, MutationKind::Update),
required_role_for(&scope, MutationKind::Delete),
);
}
#[test]
fn org_admin_satisfies_all_roles() {
for req in [
CallerRole::OrgAdmin,
CallerRole::TeamAdmin,
CallerRole::Developer,
CallerRole::Viewer,
] {
assert!(CallerRole::OrgAdmin.satisfies(req), "OrgAdmin should satisfy {req}");
}
}
#[test]
fn team_admin_satisfies_team_admin_and_below() {
assert!(CallerRole::TeamAdmin.satisfies(CallerRole::TeamAdmin));
assert!(CallerRole::TeamAdmin.satisfies(CallerRole::Developer));
assert!(CallerRole::TeamAdmin.satisfies(CallerRole::Viewer));
assert!(!CallerRole::TeamAdmin.satisfies(CallerRole::OrgAdmin));
}
#[test]
fn developer_satisfies_developer_and_viewer() {
assert!(CallerRole::Developer.satisfies(CallerRole::Developer));
assert!(CallerRole::Developer.satisfies(CallerRole::Viewer));
assert!(!CallerRole::Developer.satisfies(CallerRole::TeamAdmin));
}
#[test]
fn auditor_never_satisfies_any_write_role() {
for req in [
CallerRole::OrgAdmin,
CallerRole::TeamAdmin,
CallerRole::Developer,
CallerRole::Viewer,
] {
assert!(!CallerRole::Auditor.satisfies(req), "Auditor must not satisfy {req}");
}
}
#[test]
fn scope_kind_from_policy_scope() {
assert_eq!(PolicyScopeKind::from(&PolicyScope::Global), PolicyScopeKind::Global);
assert_eq!(
PolicyScopeKind::from(&PolicyScope::Org("x".into())),
PolicyScopeKind::Org
);
assert_eq!(
PolicyScopeKind::from(&PolicyScope::Team("y".into())),
PolicyScopeKind::Team
);
assert_eq!(
PolicyScopeKind::from(&PolicyScope::Tool("z".into())),
PolicyScopeKind::Tool
);
}
}