use crate::organization::OrgRole;
use serde::Serialize;
use std::collections::HashMap;
use std::fmt;
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Permission {
OrgHarnessesView,
OrgHarnessesManage,
OrgHarnessesDangerous,
OrgAgentsManage,
OrgAgentsDangerous,
OrgAppsView,
OrgAppsManage,
OrgAppsDangerous,
OrgMcpServersView,
OrgMcpServersManage,
OrgMcpServersDangerous,
OrgSkillsView,
OrgSkillsManage,
OrgSkillsDangerous,
OrgCapabilitiesView,
OrgCapabilitiesManage,
OrgAgentIdentitiesView,
OrgAgentIdentitiesManage,
OrgPluginsView,
OrgPluginsManage,
OrgSessionsManage,
OrgProvidersView,
OrgProvidersManage,
OrgSettingsView,
OrgSettingsManage,
OrgMembersView,
OrgMembersManage,
OrgApiKeysManage,
OrgAuditLogsView,
OrgReportsView,
OrgReportsManage,
OrgReportsAdmin,
}
impl Permission {
pub const fn as_str(&self) -> &'static str {
match self {
Permission::OrgHarnessesView => "org:harnesses:view",
Permission::OrgHarnessesManage => "org:harnesses:manage",
Permission::OrgHarnessesDangerous => "org:harnesses:dangerous",
Permission::OrgAgentsManage => "org:agents:manage",
Permission::OrgAgentsDangerous => "org:agents:dangerous",
Permission::OrgAppsView => "org:apps:view",
Permission::OrgAppsManage => "org:apps:manage",
Permission::OrgAppsDangerous => "org:apps:dangerous",
Permission::OrgMcpServersView => "org:mcp-servers:view",
Permission::OrgMcpServersManage => "org:mcp-servers:manage",
Permission::OrgMcpServersDangerous => "org:mcp-servers:dangerous",
Permission::OrgSkillsView => "org:skills:view",
Permission::OrgSkillsManage => "org:skills:manage",
Permission::OrgSkillsDangerous => "org:skills:dangerous",
Permission::OrgCapabilitiesView => "org:capabilities:view",
Permission::OrgCapabilitiesManage => "org:capabilities:manage",
Permission::OrgAgentIdentitiesView => "org:agent-identities:view",
Permission::OrgAgentIdentitiesManage => "org:agent-identities:manage",
Permission::OrgPluginsView => "org:plugins:view",
Permission::OrgPluginsManage => "org:plugins:manage",
Permission::OrgSessionsManage => "org:sessions:manage",
Permission::OrgProvidersView => "org:providers:view",
Permission::OrgProvidersManage => "org:providers:manage",
Permission::OrgSettingsView => "org:settings:view",
Permission::OrgSettingsManage => "org:settings:manage",
Permission::OrgMembersView => "org:members:view",
Permission::OrgMembersManage => "org:members:manage",
Permission::OrgApiKeysManage => "org:api-keys:manage",
Permission::OrgAuditLogsView => "org:audit-logs:view",
Permission::OrgReportsView => "org:reports:view",
Permission::OrgReportsManage => "org:reports:manage",
Permission::OrgReportsAdmin => "org:reports:admin",
}
}
pub const ALL: &'static [Permission] = &[
Permission::OrgHarnessesView,
Permission::OrgHarnessesManage,
Permission::OrgHarnessesDangerous,
Permission::OrgAgentsManage,
Permission::OrgAgentsDangerous,
Permission::OrgAppsView,
Permission::OrgAppsManage,
Permission::OrgAppsDangerous,
Permission::OrgMcpServersView,
Permission::OrgMcpServersManage,
Permission::OrgMcpServersDangerous,
Permission::OrgSkillsView,
Permission::OrgSkillsManage,
Permission::OrgSkillsDangerous,
Permission::OrgCapabilitiesView,
Permission::OrgCapabilitiesManage,
Permission::OrgAgentIdentitiesView,
Permission::OrgAgentIdentitiesManage,
Permission::OrgPluginsView,
Permission::OrgPluginsManage,
Permission::OrgSessionsManage,
Permission::OrgProvidersView,
Permission::OrgProvidersManage,
Permission::OrgSettingsView,
Permission::OrgSettingsManage,
Permission::OrgMembersView,
Permission::OrgMembersManage,
Permission::OrgApiKeysManage,
Permission::OrgAuditLogsView,
Permission::OrgReportsView,
Permission::OrgReportsManage,
Permission::OrgReportsAdmin,
];
}
impl fmt::Display for Permission {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
const OWNER_PERMISSIONS: &[Permission] = &[
Permission::OrgHarnessesView,
Permission::OrgHarnessesManage,
Permission::OrgHarnessesDangerous,
Permission::OrgAgentsManage,
Permission::OrgAgentsDangerous,
Permission::OrgAppsView,
Permission::OrgAppsManage,
Permission::OrgAppsDangerous,
Permission::OrgMcpServersView,
Permission::OrgMcpServersManage,
Permission::OrgMcpServersDangerous,
Permission::OrgSkillsView,
Permission::OrgSkillsManage,
Permission::OrgSkillsDangerous,
Permission::OrgCapabilitiesView,
Permission::OrgCapabilitiesManage,
Permission::OrgAgentIdentitiesView,
Permission::OrgAgentIdentitiesManage,
Permission::OrgPluginsView,
Permission::OrgPluginsManage,
Permission::OrgSessionsManage,
Permission::OrgProvidersView,
Permission::OrgProvidersManage,
Permission::OrgSettingsView,
Permission::OrgSettingsManage,
Permission::OrgMembersView,
Permission::OrgMembersManage,
Permission::OrgApiKeysManage,
Permission::OrgAuditLogsView,
Permission::OrgReportsView,
Permission::OrgReportsManage,
Permission::OrgReportsAdmin,
];
const ADMIN_PERMISSIONS: &[Permission] = &[
Permission::OrgHarnessesView,
Permission::OrgHarnessesManage,
Permission::OrgAgentsManage,
Permission::OrgAppsView,
Permission::OrgAppsManage,
Permission::OrgMcpServersView,
Permission::OrgMcpServersManage,
Permission::OrgSkillsView,
Permission::OrgSkillsManage,
Permission::OrgCapabilitiesView,
Permission::OrgCapabilitiesManage,
Permission::OrgAgentIdentitiesView,
Permission::OrgAgentIdentitiesManage,
Permission::OrgPluginsView,
Permission::OrgPluginsManage,
Permission::OrgSessionsManage,
Permission::OrgProvidersView,
Permission::OrgProvidersManage,
Permission::OrgSettingsView,
Permission::OrgSettingsManage,
Permission::OrgMembersView,
Permission::OrgMembersManage,
Permission::OrgApiKeysManage,
Permission::OrgAuditLogsView,
Permission::OrgReportsView,
Permission::OrgReportsManage,
];
const MEMBER_PERMISSIONS: &[Permission] = &[
Permission::OrgHarnessesView,
Permission::OrgAgentsManage,
Permission::OrgAppsView,
Permission::OrgAppsManage,
Permission::OrgMcpServersView,
Permission::OrgMcpServersManage,
Permission::OrgSkillsView,
Permission::OrgSkillsManage,
Permission::OrgCapabilitiesView,
Permission::OrgCapabilitiesManage,
Permission::OrgAgentIdentitiesView,
Permission::OrgAgentIdentitiesManage,
Permission::OrgPluginsView,
Permission::OrgSessionsManage,
Permission::OrgProvidersView,
Permission::OrgSettingsView,
Permission::OrgMembersView,
Permission::OrgReportsView,
];
pub fn role_has_permission(role: OrgRole, permission: &Permission) -> bool {
let perms = match role {
OrgRole::Owner => OWNER_PERMISSIONS,
OrgRole::Admin => ADMIN_PERMISSIONS,
OrgRole::Member => MEMBER_PERMISSIONS,
};
perms.contains(permission)
}
pub fn role_permissions(role: OrgRole) -> &'static [Permission] {
match role {
OrgRole::Owner => OWNER_PERMISSIONS,
OrgRole::Admin => ADMIN_PERMISSIONS,
OrgRole::Member => MEMBER_PERMISSIONS,
}
}
pub trait PermissionResolver: Send + Sync {
fn has_permission(&self, caller: &Caller, permission: &Permission) -> bool;
fn caller_permissions(&self, caller: &Caller) -> Vec<Permission>;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct DefaultPermissionResolver;
impl PermissionResolver for DefaultPermissionResolver {
fn has_permission(&self, caller: &Caller, permission: &Permission) -> bool {
role_has_permission(caller.role, permission)
}
fn caller_permissions(&self, caller: &Caller) -> Vec<Permission> {
role_permissions(caller.role).to_vec()
}
}
#[derive(Debug, Clone)]
pub enum Rule {
UserHasPermission(Permission),
UserHasRole(OrgRole),
IsPlatformUser,
}
impl fmt::Display for Rule {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Rule::UserHasPermission(p) => write!(f, "UserHasPermission({})", p),
Rule::UserHasRole(r) => write!(f, "UserHasRole({})", r),
Rule::IsPlatformUser => write!(f, "IsPlatformUser"),
}
}
}
#[derive(Debug, Clone)]
pub struct Policy {
pub id: &'static str,
pub rules: &'static [Rule],
}
impl Policy {
pub fn evaluate(&self, caller: &Caller) -> Result<(), PolicyError> {
let resolver = DefaultPermissionResolver;
self.evaluate_with(&resolver, caller)
}
pub fn evaluate_with(
&self,
resolver: &dyn PermissionResolver,
caller: &Caller,
) -> Result<(), PolicyError> {
for rule in self.rules {
match rule {
Rule::UserHasPermission(perm) => {
if !resolver.has_permission(caller, perm) {
return Err(PolicyError::denied(self.id, perm.as_str()));
}
}
Rule::UserHasRole(required) => {
if !caller.role.has_permission(*required) {
return Err(PolicyError::denied(self.id, &format!("role:{}", required)));
}
}
Rule::IsPlatformUser => {
if !caller.is_platform_user {
return Err(PolicyError::denied(self.id, "platform_user"));
}
}
}
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct Caller {
pub org_id: i64,
pub org_public_id: String,
pub user_id: Option<Uuid>,
pub role: OrgRole,
pub is_platform_user: bool,
pub is_internal: bool,
}
impl Caller {
pub fn internal(org_id: i64) -> Self {
Self {
org_id,
org_public_id: crate::organization::org_public_id_from_internal(org_id),
user_id: None,
role: OrgRole::Owner,
is_platform_user: true,
is_internal: true,
}
}
}
#[derive(Debug, Clone)]
pub struct PolicyError {
pub policy_id: String,
pub message: String,
}
impl PolicyError {
pub fn denied(policy_id: &str, detail: &str) -> Self {
Self {
policy_id: policy_id.to_string(),
message: format!(
"Access denied: policy '{}' requires '{}'",
policy_id, detail
),
}
}
}
impl fmt::Display for PolicyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for PolicyError {}
pub fn evaluate_policies(caller: &Caller, policies: &[&Policy]) -> HashMap<String, bool> {
let resolver = DefaultPermissionResolver;
evaluate_policies_with(&resolver, caller, policies)
}
pub fn evaluate_policies_with(
resolver: &dyn PermissionResolver,
caller: &Caller,
policies: &[&Policy],
) -> HashMap<String, bool> {
policies
.iter()
.map(|policy| {
(
policy.id.to_string(),
policy.evaluate_with(resolver, caller).is_ok(),
)
})
.collect()
}
#[derive(Debug, Clone, Serialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct ResourceConfigResponse {
pub policies: HashMap<String, bool>,
}
pub type PolicyConfigResponse = ResourceConfigResponse;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SkillPermissionAction {
Allow,
Deny,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SkillPermissionPattern {
All,
ExactName(String),
NameWildcard(String),
}
impl SkillPermissionPattern {
fn specificity(&self) -> u8 {
match self {
SkillPermissionPattern::All => 0,
SkillPermissionPattern::NameWildcard(_) => 1,
SkillPermissionPattern::ExactName(_) => 2,
}
}
fn matches(&self, skill_name: &str) -> bool {
match self {
SkillPermissionPattern::All => true,
SkillPermissionPattern::ExactName(name) => name == skill_name,
SkillPermissionPattern::NameWildcard(name) => name == skill_name,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SkillPermissionRule {
pub action: SkillPermissionAction,
pub pattern: SkillPermissionPattern,
}
pub fn parse_skill_permission_rule(input: &str) -> Result<SkillPermissionRule, String> {
let input = input.trim();
let (action, rest) = if let Some(rest) = input.strip_prefix("allow ") {
(SkillPermissionAction::Allow, rest.trim())
} else if let Some(rest) = input.strip_prefix("deny ") {
(SkillPermissionAction::Deny, rest.trim())
} else {
return Err(format!("Rule must start with 'allow' or 'deny': {input}"));
};
if rest == "Skill" {
return Ok(SkillPermissionRule {
action,
pattern: SkillPermissionPattern::All,
});
}
if let Some(inner) = rest
.strip_prefix("Skill(")
.and_then(|s| s.strip_suffix(')'))
{
let inner = inner.trim();
if inner.is_empty() {
return Err("Skill name cannot be empty in Skill()".to_string());
}
let (name, is_wildcard) = if let Some(name) = inner.strip_suffix(" *") {
(name, true)
} else {
(inner, false)
};
if let Err(errors) = crate::skill::validate_skill_name(name) {
return Err(format!(
"Invalid skill name '{}': {}",
name,
errors.join(", ")
));
}
let pattern = if is_wildcard {
SkillPermissionPattern::NameWildcard(name.to_string())
} else {
SkillPermissionPattern::ExactName(name.to_string())
};
return Ok(SkillPermissionRule { action, pattern });
}
Err(format!(
"Invalid skill permission pattern: {rest}. Expected 'Skill', 'Skill(name)', or 'Skill(name *)'"
))
}
pub fn check_skill_permission(rules: &[SkillPermissionRule], skill_name: &str) -> bool {
let mut best_specificity: Option<u8> = None;
let mut best_allowed = true;
for rule in rules {
if !rule.pattern.matches(skill_name) {
continue;
}
let spec = rule.pattern.specificity();
let is_allow = rule.action == SkillPermissionAction::Allow;
match best_specificity {
None => {
best_specificity = Some(spec);
best_allowed = is_allow;
}
Some(best) if spec > best => {
best_specificity = Some(spec);
best_allowed = is_allow;
}
Some(best) if spec == best && !is_allow => {
best_allowed = false;
}
_ => {} }
}
best_allowed
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
fn owner_caller() -> Caller {
Caller {
org_id: 1,
org_public_id: "org_00000000000000000000000000000001".to_string(),
user_id: Some(Uuid::new_v4()),
role: OrgRole::Owner,
is_platform_user: false,
is_internal: false,
}
}
fn admin_caller() -> Caller {
Caller {
org_id: 1,
org_public_id: "org_00000000000000000000000000000001".to_string(),
user_id: Some(Uuid::new_v4()),
role: OrgRole::Admin,
is_platform_user: false,
is_internal: false,
}
}
fn member_caller() -> Caller {
Caller {
org_id: 1,
org_public_id: "org_00000000000000000000000000000001".to_string(),
user_id: Some(Uuid::new_v4()),
role: OrgRole::Member,
is_platform_user: false,
is_internal: false,
}
}
const TEST_MANAGE: Policy = Policy {
id: "harness.manage",
rules: &[Rule::UserHasPermission(Permission::OrgHarnessesManage)],
};
const TEST_DANGEROUS: Policy = Policy {
id: "harness.dangerous",
rules: &[
Rule::UserHasPermission(Permission::OrgHarnessesManage),
Rule::UserHasPermission(Permission::OrgHarnessesDangerous),
],
};
const TEST_ROLE_ADMIN: Policy = Policy {
id: "require.admin",
rules: &[Rule::UserHasRole(OrgRole::Admin)],
};
#[test]
fn policy_error_contains_useful_info() {
let caller = member_caller();
let err = TEST_MANAGE.evaluate(&caller).unwrap_err();
assert_eq!(err.policy_id, "harness.manage");
assert_eq!(
err.message,
"Access denied: policy 'harness.manage' requires 'org:harnesses:manage'"
);
assert_eq!(err.to_string(), err.message);
}
struct DenyManageResolver;
impl PermissionResolver for DenyManageResolver {
fn has_permission(&self, _caller: &Caller, permission: &Permission) -> bool {
permission != &Permission::OrgHarnessesManage
}
fn caller_permissions(&self, _caller: &Caller) -> Vec<Permission> {
Permission::ALL
.iter()
.copied()
.filter(|permission| permission != &Permission::OrgHarnessesManage)
.collect()
}
}
#[test]
fn evaluate_with_uses_custom_permission_resolver() {
let caller = owner_caller();
let resolver = DenyManageResolver;
assert!(TEST_MANAGE.evaluate(&caller).is_ok());
assert!(TEST_MANAGE.evaluate_with(&resolver, &caller).is_err());
}
#[test]
fn evaluate_policies_with_uses_custom_permission_resolver() {
let caller = owner_caller();
let resolver = DenyManageResolver;
let result = evaluate_policies_with(&resolver, &caller, &[&TEST_MANAGE, &TEST_DANGEROUS]);
assert_eq!(result.get("harness.manage"), Some(&false));
assert_eq!(result.get("harness.dangerous"), Some(&false));
}
#[test]
fn arc_resolver_works_as_trait_object() {
let resolver: Arc<dyn PermissionResolver> = Arc::new(DenyManageResolver);
let caller = owner_caller();
assert!(
TEST_MANAGE
.evaluate_with(resolver.as_ref(), &caller)
.is_err()
);
let result =
evaluate_policies_with(resolver.as_ref(), &caller, &[&TEST_MANAGE, &TEST_DANGEROUS]);
assert_eq!(result.get("harness.manage"), Some(&false));
let default: Arc<dyn PermissionResolver> = Arc::new(DefaultPermissionResolver);
assert!(TEST_MANAGE.evaluate_with(default.as_ref(), &caller).is_ok());
}
#[test]
fn caller_without_user_id() {
let caller = Caller {
org_id: 1,
org_public_id: "org_00000000000000000000000000000001".to_string(),
user_id: None,
role: OrgRole::Admin,
is_platform_user: false,
is_internal: false,
};
assert!(TEST_MANAGE.evaluate(&caller).is_ok());
}
#[test]
fn empty_policy_always_passes() {
const EMPTY: Policy = Policy {
id: "empty",
rules: &[],
};
let caller = member_caller();
assert!(EMPTY.evaluate(&caller).is_ok());
}
#[test]
fn policy_error_downcast_from_anyhow() {
let err = PolicyError::denied("test.policy", "org:harnesses:manage");
let anyhow_err: anyhow::Error = err.into();
let downcasted = anyhow_err.downcast_ref::<PolicyError>();
assert!(downcasted.is_some());
assert_eq!(downcasted.unwrap().policy_id, "test.policy");
}
const TEST_PLATFORM: Policy = Policy {
id: "durable.manage",
rules: &[Rule::IsPlatformUser],
};
#[test]
fn resource_config_response_serializes() {
let mut policies = HashMap::new();
policies.insert("harness.manage".to_string(), true);
policies.insert("harness.dangerous".to_string(), false);
let response = ResourceConfigResponse { policies };
let json = serde_json::to_value(&response).unwrap();
assert_eq!(
json,
serde_json::json!({"policies":{"harness.manage":true,"harness.dangerous":false}})
);
}
#[test]
fn parse_skill_permission_allow_all() {
let rule = parse_skill_permission_rule("allow Skill").unwrap();
assert_eq!(rule.action, SkillPermissionAction::Allow);
assert_eq!(rule.pattern, SkillPermissionPattern::All);
}
#[test]
fn parse_skill_permission_deny_all() {
let rule = parse_skill_permission_rule("deny Skill").unwrap();
assert_eq!(rule.action, SkillPermissionAction::Deny);
assert_eq!(rule.pattern, SkillPermissionPattern::All);
}
#[test]
fn parse_skill_permission_exact_name() {
let rule = parse_skill_permission_rule("allow Skill(commit)").unwrap();
assert_eq!(rule.action, SkillPermissionAction::Allow);
assert_eq!(
rule.pattern,
SkillPermissionPattern::ExactName("commit".to_string())
);
}
#[test]
fn parse_skill_permission_name_wildcard() {
let rule = parse_skill_permission_rule("deny Skill(deploy *)").unwrap();
assert_eq!(rule.action, SkillPermissionAction::Deny);
assert_eq!(
rule.pattern,
SkillPermissionPattern::NameWildcard("deploy".to_string())
);
}
#[test]
fn parse_skill_permission_invalid() {
assert!(parse_skill_permission_rule("allow Something").is_err());
assert!(parse_skill_permission_rule("Skill(commit)").is_err());
assert!(parse_skill_permission_rule("allow Skill()").is_err());
assert!(parse_skill_permission_rule("deny Skill( *)").is_err());
assert!(parse_skill_permission_rule("allow Skill(deploy **)").is_err());
assert!(parse_skill_permission_rule("allow Skill(Deploy)").is_err());
assert!(parse_skill_permission_rule("allow Skill(foo bar)").is_err());
assert!(parse_skill_permission_rule("allow Skill(-deploy)").is_err());
}
#[test]
fn skill_permission_deny_all_blocks_everything() {
let rules = vec![parse_skill_permission_rule("deny Skill").unwrap()];
assert!(!check_skill_permission(&rules, "commit"));
assert!(!check_skill_permission(&rules, "deploy"));
assert!(!check_skill_permission(&rules, "anything"));
}
#[test]
fn skill_permission_no_rules_allows() {
assert!(check_skill_permission(&[], "commit"));
}
#[test]
fn skill_permission_exact_overrides_deny_all() {
let rules = vec![
parse_skill_permission_rule("deny Skill").unwrap(),
parse_skill_permission_rule("allow Skill(commit)").unwrap(),
];
assert!(check_skill_permission(&rules, "commit"));
assert!(!check_skill_permission(&rules, "deploy"));
}
#[test]
fn skill_permission_deny_specific_with_allow_all() {
let rules = vec![
parse_skill_permission_rule("allow Skill").unwrap(),
parse_skill_permission_rule("deny Skill(deploy)").unwrap(),
];
assert!(check_skill_permission(&rules, "commit"));
assert!(!check_skill_permission(&rules, "deploy"));
}
#[test]
fn skill_permission_wildcard_overrides_all() {
let rules = vec![
parse_skill_permission_rule("deny Skill").unwrap(),
parse_skill_permission_rule("allow Skill(review-pr *)").unwrap(),
];
assert!(check_skill_permission(&rules, "review-pr"));
assert!(!check_skill_permission(&rules, "deploy"));
}
#[test]
fn skill_permission_deny_wins_at_same_specificity() {
for pattern in ["Skill", "Skill(deploy *)", "Skill(deploy)"] {
let mut rules = vec![
parse_skill_permission_rule(&format!("allow {pattern}")).unwrap(),
parse_skill_permission_rule(&format!("deny {pattern}")).unwrap(),
];
assert!(!check_skill_permission(&rules, "deploy"));
rules.reverse();
assert!(!check_skill_permission(&rules, "deploy"));
}
}
#[test]
fn role_matrix_pins_permissions_and_resolver_results() {
let cases = [
(
Permission::OrgHarnessesView,
"org:harnesses:view",
true,
true,
),
(
Permission::OrgHarnessesManage,
"org:harnesses:manage",
true,
false,
),
(
Permission::OrgHarnessesDangerous,
"org:harnesses:dangerous",
false,
false,
),
(Permission::OrgAgentsManage, "org:agents:manage", true, true),
(
Permission::OrgAgentsDangerous,
"org:agents:dangerous",
false,
false,
),
(Permission::OrgAppsView, "org:apps:view", true, true),
(Permission::OrgAppsManage, "org:apps:manage", true, true),
(
Permission::OrgAppsDangerous,
"org:apps:dangerous",
false,
false,
),
(
Permission::OrgMcpServersView,
"org:mcp-servers:view",
true,
true,
),
(
Permission::OrgMcpServersManage,
"org:mcp-servers:manage",
true,
true,
),
(
Permission::OrgMcpServersDangerous,
"org:mcp-servers:dangerous",
false,
false,
),
(Permission::OrgSkillsView, "org:skills:view", true, true),
(Permission::OrgSkillsManage, "org:skills:manage", true, true),
(
Permission::OrgSkillsDangerous,
"org:skills:dangerous",
false,
false,
),
(
Permission::OrgCapabilitiesView,
"org:capabilities:view",
true,
true,
),
(
Permission::OrgCapabilitiesManage,
"org:capabilities:manage",
true,
true,
),
(
Permission::OrgAgentIdentitiesView,
"org:agent-identities:view",
true,
true,
),
(
Permission::OrgAgentIdentitiesManage,
"org:agent-identities:manage",
true,
true,
),
(Permission::OrgPluginsView, "org:plugins:view", true, true),
(
Permission::OrgPluginsManage,
"org:plugins:manage",
true,
false,
),
(
Permission::OrgSessionsManage,
"org:sessions:manage",
true,
true,
),
(
Permission::OrgProvidersView,
"org:providers:view",
true,
true,
),
(
Permission::OrgProvidersManage,
"org:providers:manage",
true,
false,
),
(Permission::OrgSettingsView, "org:settings:view", true, true),
(
Permission::OrgSettingsManage,
"org:settings:manage",
true,
false,
),
(Permission::OrgMembersView, "org:members:view", true, true),
(
Permission::OrgMembersManage,
"org:members:manage",
true,
false,
),
(
Permission::OrgApiKeysManage,
"org:api-keys:manage",
true,
false,
),
(
Permission::OrgAuditLogsView,
"org:audit-logs:view",
true,
false,
),
(Permission::OrgReportsView, "org:reports:view", true, true),
(
Permission::OrgReportsManage,
"org:reports:manage",
true,
false,
),
(
Permission::OrgReportsAdmin,
"org:reports:admin",
false,
false,
),
];
let resolver = DefaultPermissionResolver;
assert_eq!(Permission::ALL, cases.map(|(p, _, _, _)| p));
for caller in [owner_caller(), admin_caller(), member_caller()] {
let mut expected = Vec::new();
for (permission, wire, admin, member) in cases {
let allowed = match caller.role {
OrgRole::Owner => true,
OrgRole::Admin => admin,
OrgRole::Member => member,
};
assert_eq!(permission.to_string(), wire);
assert_eq!(
role_has_permission(caller.role, &permission),
allowed,
"{:?}/{wire}",
caller.role
);
assert_eq!(
resolver.has_permission(&caller, &permission),
allowed,
"resolver {:?}/{wire}",
caller.role
);
if allowed {
expected.push(permission);
}
}
assert_eq!(role_permissions(caller.role), expected);
assert_eq!(resolver.caller_permissions(&caller), expected);
}
}
#[test]
fn default_policy_results_match_the_complete_config_response() {
for (caller, manage, dangerous, admin) in [
(owner_caller(), true, true, true),
(admin_caller(), true, false, true),
(member_caller(), false, false, false),
] {
assert_eq!(TEST_MANAGE.evaluate(&caller).is_ok(), manage);
assert_eq!(TEST_DANGEROUS.evaluate(&caller).is_ok(), dangerous);
assert_eq!(TEST_ROLE_ADMIN.evaluate(&caller).is_ok(), admin);
assert_eq!(
evaluate_policies(&caller, &[&TEST_MANAGE, &TEST_DANGEROUS, &TEST_ROLE_ADMIN]),
HashMap::from([
("harness.manage".into(), manage),
("harness.dangerous".into(), dangerous),
("require.admin".into(), admin)
])
);
assert!(evaluate_policies(&caller, &[]).is_empty());
}
}
#[test]
fn internal_caller_preserves_identity_and_satisfies_internal_policies() {
for (id, public_id) in [
(1, "org_00000000000000000000000000000001"),
(42, "org_0000000000000000000000000000002a"),
(99, "org_00000000000000000000000000000063"),
] {
let caller = Caller::internal(id);
assert_eq!(caller.org_id, id);
assert_eq!(caller.org_public_id, public_id);
assert_eq!(caller.role, OrgRole::Owner);
assert_eq!(caller.user_id, None);
assert!(caller.is_internal);
assert!(caller.is_platform_user);
for policy in [
&TEST_MANAGE,
&TEST_DANGEROUS,
&TEST_ROLE_ADMIN,
&TEST_PLATFORM,
] {
assert!(policy.evaluate(&caller).is_ok());
}
}
}
#[test]
fn platform_policy_depends_on_platform_flag_independently_of_role() {
for mut caller in [owner_caller(), admin_caller(), member_caller()] {
for flag in [false, true] {
caller.is_platform_user = flag;
assert_eq!(TEST_PLATFORM.evaluate(&caller).is_ok(), flag);
}
}
}
#[test]
fn skill_specificity_is_order_independent_and_exact_beats_wildcard() {
for (texts, expected) in [
(
["deny Skill", "allow Skill(deploy *)", "deny Skill(deploy)"],
false,
),
(
["allow Skill", "deny Skill(deploy *)", "allow Skill(deploy)"],
true,
),
] {
for order in [
[0, 1, 2],
[0, 2, 1],
[1, 0, 2],
[1, 2, 0],
[2, 0, 1],
[2, 1, 0],
] {
let rules: Vec<_> = order
.into_iter()
.map(|i| parse_skill_permission_rule(texts[i]).unwrap())
.collect();
assert_eq!(
check_skill_permission(&rules, "deploy"),
expected,
"{order:?}"
);
assert_eq!(check_skill_permission(&rules, "deploy-prod"), expected);
}
}
}
#[test]
fn mixed_policy_short_circuits_and_preserves_denied_rule_identity() {
struct RecordingResolver(std::sync::Mutex<Vec<Permission>>);
impl PermissionResolver for RecordingResolver {
fn has_permission(&self, caller: &Caller, permission: &Permission) -> bool {
assert_eq!(caller.org_id, 42);
assert_eq!(caller.user_id, Some(Uuid::from_u128(7)));
self.0.lock().unwrap().push(*permission);
*permission != Permission::OrgHarnessesDangerous
}
fn caller_permissions(&self, _: &Caller) -> Vec<Permission> {
panic!("evaluation must ask the specific permission")
}
}
const MIXED: Policy = Policy {
id: "mixed",
rules: &[
Rule::IsPlatformUser,
Rule::UserHasRole(OrgRole::Admin),
Rule::UserHasPermission(Permission::OrgHarnessesManage),
Rule::UserHasPermission(Permission::OrgHarnessesDangerous),
Rule::UserHasPermission(Permission::OrgSettingsManage),
],
};
let mut caller = member_caller();
caller.org_id = 42;
caller.user_id = Some(Uuid::from_u128(7));
let resolver = RecordingResolver(Default::default());
let err = MIXED.evaluate_with(&resolver, &caller).unwrap_err();
assert_eq!(
err.message,
"Access denied: policy 'mixed' requires 'platform_user'"
);
assert!(resolver.0.lock().unwrap().is_empty());
caller.is_platform_user = true;
assert_eq!(
MIXED.evaluate_with(&resolver, &caller).unwrap_err().message,
"Access denied: policy 'mixed' requires 'role:admin'"
);
assert!(resolver.0.lock().unwrap().is_empty());
caller.role = OrgRole::Admin;
let err = MIXED.evaluate_with(&resolver, &caller).unwrap_err();
assert_eq!(err.policy_id, "mixed");
assert_eq!(
err.message,
"Access denied: policy 'mixed' requires 'org:harnesses:dangerous'"
);
assert_eq!(
*resolver.0.lock().unwrap(),
vec![
Permission::OrgHarnessesManage,
Permission::OrgHarnessesDangerous
]
);
}
}