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,
OrgSkillsDangerous,
OrgMcpServersDangerous,
OrgAppsDangerous,
OrgSessionsManage,
OrgLlmProvidersView,
OrgLlmProvidersManage,
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::OrgSkillsDangerous => "org:skills:dangerous",
Permission::OrgMcpServersDangerous => "org:mcp-servers:dangerous",
Permission::OrgAppsDangerous => "org:apps:dangerous",
Permission::OrgSessionsManage => "org:sessions:manage",
Permission::OrgLlmProvidersView => "org:llm-providers:view",
Permission::OrgLlmProvidersManage => "org:llm-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::OrgSkillsDangerous,
Permission::OrgMcpServersDangerous,
Permission::OrgAppsDangerous,
Permission::OrgSessionsManage,
Permission::OrgLlmProvidersView,
Permission::OrgLlmProvidersManage,
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::OrgSkillsDangerous,
Permission::OrgMcpServersDangerous,
Permission::OrgAppsDangerous,
Permission::OrgSessionsManage,
Permission::OrgLlmProvidersView,
Permission::OrgLlmProvidersManage,
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::OrgSessionsManage,
Permission::OrgLlmProvidersView,
Permission::OrgLlmProvidersManage,
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::OrgSessionsManage,
Permission::OrgLlmProvidersView,
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,
}
}
#[test]
fn owner_has_all_permissions() {
for perm in Permission::ALL {
assert!(
role_has_permission(OrgRole::Owner, perm),
"Owner should have {:?}",
perm
);
}
}
#[test]
fn admin_has_manage_but_not_dangerous() {
assert!(role_has_permission(
OrgRole::Admin,
&Permission::OrgHarnessesManage
));
assert!(!role_has_permission(
OrgRole::Admin,
&Permission::OrgHarnessesDangerous
));
assert!(role_has_permission(
OrgRole::Admin,
&Permission::OrgAgentsManage
));
assert!(role_has_permission(
OrgRole::Admin,
&Permission::OrgSettingsManage
));
}
#[test]
fn member_has_only_basic_permissions() {
assert!(role_has_permission(
OrgRole::Member,
&Permission::OrgAgentsManage
));
assert!(role_has_permission(
OrgRole::Member,
&Permission::OrgSessionsManage
));
assert!(!role_has_permission(
OrgRole::Member,
&Permission::OrgHarnessesManage
));
assert!(!role_has_permission(
OrgRole::Member,
&Permission::OrgSettingsManage
));
assert!(!role_has_permission(
OrgRole::Member,
&Permission::OrgApiKeysManage
));
}
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 owner_passes_all_policies() {
let caller = owner_caller();
assert!(TEST_MANAGE.evaluate(&caller).is_ok());
assert!(TEST_DANGEROUS.evaluate(&caller).is_ok());
assert!(TEST_ROLE_ADMIN.evaluate(&caller).is_ok());
}
#[test]
fn admin_passes_manage_but_not_dangerous() {
let caller = admin_caller();
assert!(TEST_MANAGE.evaluate(&caller).is_ok());
assert!(TEST_DANGEROUS.evaluate(&caller).is_err());
assert!(TEST_ROLE_ADMIN.evaluate(&caller).is_ok());
}
#[test]
fn member_fails_manage_and_dangerous() {
let caller = member_caller();
assert!(TEST_MANAGE.evaluate(&caller).is_err());
assert!(TEST_DANGEROUS.evaluate(&caller).is_err());
assert!(TEST_ROLE_ADMIN.evaluate(&caller).is_err());
}
#[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!(err.message.contains("org:harnesses:manage"));
assert!(err.to_string().contains("Access denied"));
}
#[test]
fn evaluate_policies_returns_correct_map() {
let caller = admin_caller();
let result = evaluate_policies(&caller, &[&TEST_MANAGE, &TEST_DANGEROUS]);
assert_eq!(result.get("harness.manage"), Some(&true));
assert_eq!(result.get("harness.dangerous"), Some(&false));
}
#[test]
fn evaluate_policies_owner_all_true() {
let caller = owner_caller();
let result = evaluate_policies(&caller, &[&TEST_MANAGE, &TEST_DANGEROUS, &TEST_ROLE_ADMIN]);
assert!(result.values().all(|&v| v));
}
#[test]
fn evaluate_policies_member_all_false_for_admin_policies() {
let caller = member_caller();
let result = evaluate_policies(&caller, &[&TEST_MANAGE, &TEST_DANGEROUS, &TEST_ROLE_ADMIN]);
assert!(result.values().all(|&v| !v));
}
#[test]
fn default_permission_resolver_matches_hardcoded_role_mapping() {
let resolver = DefaultPermissionResolver;
for caller in [owner_caller(), admin_caller(), member_caller()] {
for permission in Permission::ALL {
assert_eq!(
resolver.has_permission(&caller, permission),
role_has_permission(caller.role, permission)
);
}
assert_eq!(
resolver.caller_permissions(&caller),
role_permissions(caller.role).to_vec()
);
}
}
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 permission_display() {
assert_eq!(
Permission::OrgHarnessesManage.to_string(),
"org:harnesses:manage"
);
assert_eq!(
Permission::OrgHarnessesDangerous.to_string(),
"org:harnesses:dangerous"
);
assert_eq!(Permission::OrgAgentsManage.to_string(), "org:agents:manage");
}
#[test]
fn role_permissions_returns_correct_sets() {
assert_eq!(role_permissions(OrgRole::Owner).len(), 20);
assert_eq!(role_permissions(OrgRole::Admin).len(), 14);
assert_eq!(role_permissions(OrgRole::Member).len(), 7);
assert!(role_has_permission(
OrgRole::Owner,
&Permission::OrgReportsAdmin
));
assert!(role_has_permission(
OrgRole::Admin,
&Permission::OrgReportsManage
));
assert!(role_has_permission(
OrgRole::Member,
&Permission::OrgReportsView
));
}
#[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 caller_internal_has_owner_role() {
let caller = Caller::internal(42);
assert_eq!(caller.org_id, 42);
assert_eq!(caller.role, OrgRole::Owner);
assert!(caller.user_id.is_none());
assert!(TEST_MANAGE.evaluate(&caller).is_ok());
assert!(TEST_DANGEROUS.evaluate(&caller).is_ok());
assert!(TEST_ROLE_ADMIN.evaluate(&caller).is_ok());
}
#[test]
fn caller_internal_generates_public_id() {
let caller = Caller::internal(1);
assert_eq!(caller.org_public_id, "org_00000000000000000000000000000001");
let caller = Caller::internal(99);
assert!(caller.org_public_id.starts_with("org_"));
}
#[test]
fn policy_error_is_std_error() {
let err = PolicyError::denied("test", "detail");
let _: &dyn std::error::Error = &err;
}
#[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");
}
#[test]
fn member_has_view_permissions() {
assert!(role_has_permission(
OrgRole::Member,
&Permission::OrgHarnessesView
));
assert!(role_has_permission(
OrgRole::Member,
&Permission::OrgLlmProvidersView
));
assert!(role_has_permission(
OrgRole::Member,
&Permission::OrgSettingsView
));
assert!(role_has_permission(
OrgRole::Member,
&Permission::OrgMembersView
));
}
#[test]
fn member_lacks_manage_for_restricted_resources() {
assert!(!role_has_permission(
OrgRole::Member,
&Permission::OrgHarnessesManage
));
assert!(!role_has_permission(
OrgRole::Member,
&Permission::OrgLlmProvidersManage
));
assert!(!role_has_permission(
OrgRole::Member,
&Permission::OrgSettingsManage
));
assert!(!role_has_permission(
OrgRole::Member,
&Permission::OrgMembersManage
));
}
const TEST_PLATFORM: Policy = Policy {
id: "durable.manage",
rules: &[Rule::IsPlatformUser],
};
#[test]
fn platform_user_passes_platform_policy() {
let mut caller = owner_caller();
caller.is_platform_user = true;
assert!(TEST_PLATFORM.evaluate(&caller).is_ok());
}
#[test]
fn non_platform_user_fails_platform_policy() {
let caller = owner_caller(); assert!(TEST_PLATFORM.evaluate(&caller).is_err());
}
#[test]
fn internal_caller_is_platform_user() {
let caller = Caller::internal(1);
assert!(caller.is_platform_user);
assert!(TEST_PLATFORM.evaluate(&caller).is_ok());
}
#[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["policies"]["harness.manage"], true);
assert_eq!(json["policies"]["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() {
let rules = vec![
parse_skill_permission_rule("allow Skill(deploy)").unwrap(),
parse_skill_permission_rule("deny Skill(deploy)").unwrap(),
];
assert!(!check_skill_permission(&rules, "deploy"));
}
}