1use crate::organization::OrgRole;
9use serde::Serialize;
10use std::collections::HashMap;
11use std::fmt;
12use uuid::Uuid;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18pub enum Permission {
19 OrgHarnessesView,
21 OrgHarnessesManage,
23 OrgHarnessesDangerous,
25 OrgAgentsManage,
27 OrgAgentsDangerous,
29 OrgSkillsDangerous,
31 OrgMcpServersDangerous,
33 OrgAppsDangerous,
35 OrgSessionsManage,
37 OrgLlmProvidersView,
39 OrgLlmProvidersManage,
41 OrgSettingsView,
43 OrgSettingsManage,
45 OrgMembersView,
47 OrgMembersManage,
49 OrgApiKeysManage,
51 OrgAuditLogsView,
53 OrgReportsView,
55 OrgReportsManage,
57 OrgReportsAdmin,
59}
60
61impl Permission {
62 pub const fn as_str(&self) -> &'static str {
64 match self {
65 Permission::OrgHarnessesView => "org:harnesses:view",
66 Permission::OrgHarnessesManage => "org:harnesses:manage",
67 Permission::OrgHarnessesDangerous => "org:harnesses:dangerous",
68 Permission::OrgAgentsManage => "org:agents:manage",
69 Permission::OrgAgentsDangerous => "org:agents:dangerous",
70 Permission::OrgSkillsDangerous => "org:skills:dangerous",
71 Permission::OrgMcpServersDangerous => "org:mcp-servers:dangerous",
72 Permission::OrgAppsDangerous => "org:apps:dangerous",
73 Permission::OrgSessionsManage => "org:sessions:manage",
74 Permission::OrgLlmProvidersView => "org:llm-providers:view",
75 Permission::OrgLlmProvidersManage => "org:llm-providers:manage",
76 Permission::OrgSettingsView => "org:settings:view",
77 Permission::OrgSettingsManage => "org:settings:manage",
78 Permission::OrgMembersView => "org:members:view",
79 Permission::OrgMembersManage => "org:members:manage",
80 Permission::OrgApiKeysManage => "org:api-keys:manage",
81 Permission::OrgAuditLogsView => "org:audit-logs:view",
82 Permission::OrgReportsView => "org:reports:view",
83 Permission::OrgReportsManage => "org:reports:manage",
84 Permission::OrgReportsAdmin => "org:reports:admin",
85 }
86 }
87
88 pub const ALL: &'static [Permission] = &[
90 Permission::OrgHarnessesView,
91 Permission::OrgHarnessesManage,
92 Permission::OrgHarnessesDangerous,
93 Permission::OrgAgentsManage,
94 Permission::OrgAgentsDangerous,
95 Permission::OrgSkillsDangerous,
96 Permission::OrgMcpServersDangerous,
97 Permission::OrgAppsDangerous,
98 Permission::OrgSessionsManage,
99 Permission::OrgLlmProvidersView,
100 Permission::OrgLlmProvidersManage,
101 Permission::OrgSettingsView,
102 Permission::OrgSettingsManage,
103 Permission::OrgMembersView,
104 Permission::OrgMembersManage,
105 Permission::OrgApiKeysManage,
106 Permission::OrgAuditLogsView,
107 Permission::OrgReportsView,
108 Permission::OrgReportsManage,
109 Permission::OrgReportsAdmin,
110 ];
111}
112
113impl fmt::Display for Permission {
114 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115 f.write_str(self.as_str())
116 }
117}
118
119const OWNER_PERMISSIONS: &[Permission] = &[
125 Permission::OrgHarnessesView,
126 Permission::OrgHarnessesManage,
127 Permission::OrgHarnessesDangerous,
128 Permission::OrgAgentsManage,
129 Permission::OrgAgentsDangerous,
130 Permission::OrgSkillsDangerous,
131 Permission::OrgMcpServersDangerous,
132 Permission::OrgAppsDangerous,
133 Permission::OrgSessionsManage,
134 Permission::OrgLlmProvidersView,
135 Permission::OrgLlmProvidersManage,
136 Permission::OrgSettingsView,
137 Permission::OrgSettingsManage,
138 Permission::OrgMembersView,
139 Permission::OrgMembersManage,
140 Permission::OrgApiKeysManage,
141 Permission::OrgAuditLogsView,
142 Permission::OrgReportsView,
143 Permission::OrgReportsManage,
144 Permission::OrgReportsAdmin,
145];
146
147const ADMIN_PERMISSIONS: &[Permission] = &[
149 Permission::OrgHarnessesView,
150 Permission::OrgHarnessesManage,
151 Permission::OrgAgentsManage,
152 Permission::OrgSessionsManage,
153 Permission::OrgLlmProvidersView,
154 Permission::OrgLlmProvidersManage,
155 Permission::OrgSettingsView,
156 Permission::OrgSettingsManage,
157 Permission::OrgMembersView,
158 Permission::OrgMembersManage,
159 Permission::OrgApiKeysManage,
160 Permission::OrgAuditLogsView,
161 Permission::OrgReportsView,
162 Permission::OrgReportsManage,
163];
164
165const MEMBER_PERMISSIONS: &[Permission] = &[
168 Permission::OrgHarnessesView,
169 Permission::OrgAgentsManage,
170 Permission::OrgSessionsManage,
171 Permission::OrgLlmProvidersView,
172 Permission::OrgSettingsView,
173 Permission::OrgMembersView,
174 Permission::OrgReportsView,
175];
176
177pub fn role_has_permission(role: OrgRole, permission: &Permission) -> bool {
179 let perms = match role {
180 OrgRole::Owner => OWNER_PERMISSIONS,
181 OrgRole::Admin => ADMIN_PERMISSIONS,
182 OrgRole::Member => MEMBER_PERMISSIONS,
183 };
184 perms.contains(permission)
185}
186
187pub fn role_permissions(role: OrgRole) -> &'static [Permission] {
189 match role {
190 OrgRole::Owner => OWNER_PERMISSIONS,
191 OrgRole::Admin => ADMIN_PERMISSIONS,
192 OrgRole::Member => MEMBER_PERMISSIONS,
193 }
194}
195
196pub trait PermissionResolver: Send + Sync {
235 fn has_permission(&self, caller: &Caller, permission: &Permission) -> bool;
240
241 fn caller_permissions(&self, caller: &Caller) -> Vec<Permission>;
247}
248
249#[derive(Debug, Clone, Copy, Default)]
258pub struct DefaultPermissionResolver;
259
260impl PermissionResolver for DefaultPermissionResolver {
261 fn has_permission(&self, caller: &Caller, permission: &Permission) -> bool {
262 role_has_permission(caller.role, permission)
263 }
264
265 fn caller_permissions(&self, caller: &Caller) -> Vec<Permission> {
266 role_permissions(caller.role).to_vec()
267 }
268}
269
270#[derive(Debug, Clone)]
277pub enum Rule {
278 UserHasPermission(Permission),
280 UserHasRole(OrgRole),
282 IsPlatformUser,
284}
285
286impl fmt::Display for Rule {
287 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
288 match self {
289 Rule::UserHasPermission(p) => write!(f, "UserHasPermission({})", p),
290 Rule::UserHasRole(r) => write!(f, "UserHasRole({})", r),
291 Rule::IsPlatformUser => write!(f, "IsPlatformUser"),
292 }
293 }
294}
295
296#[derive(Debug, Clone)]
305pub struct Policy {
306 pub id: &'static str,
308 pub rules: &'static [Rule],
310}
311
312impl Policy {
313 pub fn evaluate(&self, caller: &Caller) -> Result<(), PolicyError> {
316 let resolver = DefaultPermissionResolver;
317 self.evaluate_with(&resolver, caller)
318 }
319
320 pub fn evaluate_with(
325 &self,
326 resolver: &dyn PermissionResolver,
327 caller: &Caller,
328 ) -> Result<(), PolicyError> {
329 for rule in self.rules {
330 match rule {
331 Rule::UserHasPermission(perm) => {
332 if !resolver.has_permission(caller, perm) {
333 return Err(PolicyError::denied(self.id, perm.as_str()));
334 }
335 }
336 Rule::UserHasRole(required) => {
337 if !caller.role.has_permission(*required) {
338 return Err(PolicyError::denied(self.id, &format!("role:{}", required)));
339 }
340 }
341 Rule::IsPlatformUser => {
342 if !caller.is_platform_user {
343 return Err(PolicyError::denied(self.id, "platform_user"));
344 }
345 }
346 }
347 }
348 Ok(())
349 }
350}
351
352#[derive(Debug, Clone)]
361pub struct Caller {
362 pub org_id: i64,
364 pub org_public_id: String,
366 pub user_id: Option<Uuid>,
368 pub role: OrgRole,
370 pub is_platform_user: bool,
372 pub is_internal: bool,
374}
375
376impl Caller {
377 pub fn internal(org_id: i64) -> Self {
383 Self {
384 org_id,
385 org_public_id: crate::organization::org_public_id_from_internal(org_id),
386 user_id: None,
387 role: OrgRole::Owner,
388 is_platform_user: true,
389 is_internal: true,
390 }
391 }
392}
393
394#[derive(Debug, Clone)]
400pub struct PolicyError {
401 pub policy_id: String,
403 pub message: String,
405}
406
407impl PolicyError {
408 pub fn denied(policy_id: &str, detail: &str) -> Self {
409 Self {
410 policy_id: policy_id.to_string(),
411 message: format!(
412 "Access denied: policy '{}' requires '{}'",
413 policy_id, detail
414 ),
415 }
416 }
417}
418
419impl fmt::Display for PolicyError {
420 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
421 write!(f, "{}", self.message)
422 }
423}
424
425impl std::error::Error for PolicyError {}
426
427pub fn evaluate_policies(caller: &Caller, policies: &[&Policy]) -> HashMap<String, bool> {
434 let resolver = DefaultPermissionResolver;
435 evaluate_policies_with(&resolver, caller, policies)
436}
437
438pub fn evaluate_policies_with(
442 resolver: &dyn PermissionResolver,
443 caller: &Caller,
444 policies: &[&Policy],
445) -> HashMap<String, bool> {
446 policies
447 .iter()
448 .map(|policy| {
449 (
450 policy.id.to_string(),
451 policy.evaluate_with(resolver, caller).is_ok(),
452 )
453 })
454 .collect()
455}
456
457#[derive(Debug, Clone, Serialize)]
462#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
463pub struct ResourceConfigResponse {
464 pub policies: HashMap<String, bool>,
466}
467
468pub type PolicyConfigResponse = ResourceConfigResponse;
470
471#[derive(Debug, Clone, Copy, PartialEq, Eq)]
482pub enum SkillPermissionAction {
483 Allow,
484 Deny,
485}
486
487#[derive(Debug, Clone, PartialEq, Eq)]
489pub enum SkillPermissionPattern {
490 All,
492 ExactName(String),
494 NameWildcard(String),
496}
497
498impl SkillPermissionPattern {
499 fn specificity(&self) -> u8 {
501 match self {
502 SkillPermissionPattern::All => 0,
503 SkillPermissionPattern::NameWildcard(_) => 1,
504 SkillPermissionPattern::ExactName(_) => 2,
505 }
506 }
507
508 fn matches(&self, skill_name: &str) -> bool {
516 match self {
517 SkillPermissionPattern::All => true,
518 SkillPermissionPattern::ExactName(name) => name == skill_name,
519 SkillPermissionPattern::NameWildcard(name) => name == skill_name,
520 }
521 }
522}
523
524#[derive(Debug, Clone, PartialEq, Eq)]
526pub struct SkillPermissionRule {
527 pub action: SkillPermissionAction,
528 pub pattern: SkillPermissionPattern,
529}
530
531pub fn parse_skill_permission_rule(input: &str) -> Result<SkillPermissionRule, String> {
538 let input = input.trim();
539 let (action, rest) = if let Some(rest) = input.strip_prefix("allow ") {
540 (SkillPermissionAction::Allow, rest.trim())
541 } else if let Some(rest) = input.strip_prefix("deny ") {
542 (SkillPermissionAction::Deny, rest.trim())
543 } else {
544 return Err(format!("Rule must start with 'allow' or 'deny': {input}"));
545 };
546
547 if rest == "Skill" {
549 return Ok(SkillPermissionRule {
550 action,
551 pattern: SkillPermissionPattern::All,
552 });
553 }
554
555 if let Some(inner) = rest
556 .strip_prefix("Skill(")
557 .and_then(|s| s.strip_suffix(')'))
558 {
559 let inner = inner.trim();
560 if inner.is_empty() {
561 return Err("Skill name cannot be empty in Skill()".to_string());
562 }
563
564 let (name, is_wildcard) = if let Some(name) = inner.strip_suffix(" *") {
566 (name, true)
567 } else {
568 (inner, false)
569 };
570
571 if let Err(errors) = crate::skill::validate_skill_name(name) {
573 return Err(format!(
574 "Invalid skill name '{}': {}",
575 name,
576 errors.join(", ")
577 ));
578 }
579
580 let pattern = if is_wildcard {
581 SkillPermissionPattern::NameWildcard(name.to_string())
582 } else {
583 SkillPermissionPattern::ExactName(name.to_string())
584 };
585
586 return Ok(SkillPermissionRule { action, pattern });
587 }
588
589 Err(format!(
590 "Invalid skill permission pattern: {rest}. Expected 'Skill', 'Skill(name)', or 'Skill(name *)'"
591 ))
592}
593
594pub fn check_skill_permission(rules: &[SkillPermissionRule], skill_name: &str) -> bool {
601 let mut best_specificity: Option<u8> = None;
602 let mut best_allowed = true; for rule in rules {
605 if !rule.pattern.matches(skill_name) {
606 continue;
607 }
608 let spec = rule.pattern.specificity();
609 let is_allow = rule.action == SkillPermissionAction::Allow;
610
611 match best_specificity {
612 None => {
613 best_specificity = Some(spec);
614 best_allowed = is_allow;
615 }
616 Some(best) if spec > best => {
617 best_specificity = Some(spec);
618 best_allowed = is_allow;
619 }
620 Some(best) if spec == best && !is_allow => {
622 best_allowed = false;
623 }
624 _ => {} }
626 }
627
628 best_allowed
629}
630
631#[cfg(test)]
636mod tests {
637 use super::*;
638 use std::sync::Arc;
639
640 fn owner_caller() -> Caller {
641 Caller {
642 org_id: 1,
643 org_public_id: "org_00000000000000000000000000000001".to_string(),
644 user_id: Some(Uuid::new_v4()),
645 role: OrgRole::Owner,
646 is_platform_user: false,
647 is_internal: false,
648 }
649 }
650
651 fn admin_caller() -> Caller {
652 Caller {
653 org_id: 1,
654 org_public_id: "org_00000000000000000000000000000001".to_string(),
655 user_id: Some(Uuid::new_v4()),
656 role: OrgRole::Admin,
657 is_platform_user: false,
658 is_internal: false,
659 }
660 }
661
662 fn member_caller() -> Caller {
663 Caller {
664 org_id: 1,
665 org_public_id: "org_00000000000000000000000000000001".to_string(),
666 user_id: Some(Uuid::new_v4()),
667 role: OrgRole::Member,
668 is_platform_user: false,
669 is_internal: false,
670 }
671 }
672
673 #[test]
676 fn owner_has_all_permissions() {
677 for perm in Permission::ALL {
678 assert!(
679 role_has_permission(OrgRole::Owner, perm),
680 "Owner should have {:?}",
681 perm
682 );
683 }
684 }
685
686 #[test]
687 fn admin_has_manage_but_not_dangerous() {
688 assert!(role_has_permission(
689 OrgRole::Admin,
690 &Permission::OrgHarnessesManage
691 ));
692 assert!(!role_has_permission(
693 OrgRole::Admin,
694 &Permission::OrgHarnessesDangerous
695 ));
696 assert!(role_has_permission(
697 OrgRole::Admin,
698 &Permission::OrgAgentsManage
699 ));
700 assert!(role_has_permission(
701 OrgRole::Admin,
702 &Permission::OrgSettingsManage
703 ));
704 }
705
706 #[test]
707 fn member_has_only_basic_permissions() {
708 assert!(role_has_permission(
709 OrgRole::Member,
710 &Permission::OrgAgentsManage
711 ));
712 assert!(role_has_permission(
713 OrgRole::Member,
714 &Permission::OrgSessionsManage
715 ));
716 assert!(!role_has_permission(
717 OrgRole::Member,
718 &Permission::OrgHarnessesManage
719 ));
720 assert!(!role_has_permission(
721 OrgRole::Member,
722 &Permission::OrgSettingsManage
723 ));
724 assert!(!role_has_permission(
725 OrgRole::Member,
726 &Permission::OrgApiKeysManage
727 ));
728 }
729
730 const TEST_MANAGE: Policy = Policy {
733 id: "harness.manage",
734 rules: &[Rule::UserHasPermission(Permission::OrgHarnessesManage)],
735 };
736
737 const TEST_DANGEROUS: Policy = Policy {
738 id: "harness.dangerous",
739 rules: &[
740 Rule::UserHasPermission(Permission::OrgHarnessesManage),
741 Rule::UserHasPermission(Permission::OrgHarnessesDangerous),
742 ],
743 };
744
745 const TEST_ROLE_ADMIN: Policy = Policy {
746 id: "require.admin",
747 rules: &[Rule::UserHasRole(OrgRole::Admin)],
748 };
749
750 #[test]
751 fn owner_passes_all_policies() {
752 let caller = owner_caller();
753 assert!(TEST_MANAGE.evaluate(&caller).is_ok());
754 assert!(TEST_DANGEROUS.evaluate(&caller).is_ok());
755 assert!(TEST_ROLE_ADMIN.evaluate(&caller).is_ok());
756 }
757
758 #[test]
759 fn admin_passes_manage_but_not_dangerous() {
760 let caller = admin_caller();
761 assert!(TEST_MANAGE.evaluate(&caller).is_ok());
762 assert!(TEST_DANGEROUS.evaluate(&caller).is_err());
763 assert!(TEST_ROLE_ADMIN.evaluate(&caller).is_ok());
764 }
765
766 #[test]
767 fn member_fails_manage_and_dangerous() {
768 let caller = member_caller();
769 assert!(TEST_MANAGE.evaluate(&caller).is_err());
770 assert!(TEST_DANGEROUS.evaluate(&caller).is_err());
771 assert!(TEST_ROLE_ADMIN.evaluate(&caller).is_err());
772 }
773
774 #[test]
775 fn policy_error_contains_useful_info() {
776 let caller = member_caller();
777 let err = TEST_MANAGE.evaluate(&caller).unwrap_err();
778 assert_eq!(err.policy_id, "harness.manage");
779 assert!(err.message.contains("org:harnesses:manage"));
780 assert!(err.to_string().contains("Access denied"));
781 }
782
783 #[test]
786 fn evaluate_policies_returns_correct_map() {
787 let caller = admin_caller();
788 let result = evaluate_policies(&caller, &[&TEST_MANAGE, &TEST_DANGEROUS]);
789
790 assert_eq!(result.get("harness.manage"), Some(&true));
791 assert_eq!(result.get("harness.dangerous"), Some(&false));
792 }
793
794 #[test]
795 fn evaluate_policies_owner_all_true() {
796 let caller = owner_caller();
797 let result = evaluate_policies(&caller, &[&TEST_MANAGE, &TEST_DANGEROUS, &TEST_ROLE_ADMIN]);
798
799 assert!(result.values().all(|&v| v));
800 }
801
802 #[test]
803 fn evaluate_policies_member_all_false_for_admin_policies() {
804 let caller = member_caller();
805 let result = evaluate_policies(&caller, &[&TEST_MANAGE, &TEST_DANGEROUS, &TEST_ROLE_ADMIN]);
806
807 assert!(result.values().all(|&v| !v));
808 }
809
810 #[test]
811 fn default_permission_resolver_matches_hardcoded_role_mapping() {
812 let resolver = DefaultPermissionResolver;
813
814 for caller in [owner_caller(), admin_caller(), member_caller()] {
815 for permission in Permission::ALL {
816 assert_eq!(
817 resolver.has_permission(&caller, permission),
818 role_has_permission(caller.role, permission)
819 );
820 }
821
822 assert_eq!(
823 resolver.caller_permissions(&caller),
824 role_permissions(caller.role).to_vec()
825 );
826 }
827 }
828
829 struct DenyManageResolver;
830
831 impl PermissionResolver for DenyManageResolver {
832 fn has_permission(&self, _caller: &Caller, permission: &Permission) -> bool {
833 permission != &Permission::OrgHarnessesManage
834 }
835
836 fn caller_permissions(&self, _caller: &Caller) -> Vec<Permission> {
837 Permission::ALL
838 .iter()
839 .copied()
840 .filter(|permission| permission != &Permission::OrgHarnessesManage)
841 .collect()
842 }
843 }
844
845 #[test]
846 fn evaluate_with_uses_custom_permission_resolver() {
847 let caller = owner_caller();
848 let resolver = DenyManageResolver;
849
850 assert!(TEST_MANAGE.evaluate(&caller).is_ok());
851 assert!(TEST_MANAGE.evaluate_with(&resolver, &caller).is_err());
852 }
853
854 #[test]
855 fn evaluate_policies_with_uses_custom_permission_resolver() {
856 let caller = owner_caller();
857 let resolver = DenyManageResolver;
858
859 let result = evaluate_policies_with(&resolver, &caller, &[&TEST_MANAGE, &TEST_DANGEROUS]);
860
861 assert_eq!(result.get("harness.manage"), Some(&false));
862 assert_eq!(result.get("harness.dangerous"), Some(&false));
863 }
864
865 #[test]
866 fn arc_resolver_works_as_trait_object() {
867 let resolver: Arc<dyn PermissionResolver> = Arc::new(DenyManageResolver);
869 let caller = owner_caller();
870
871 assert!(
873 TEST_MANAGE
874 .evaluate_with(resolver.as_ref(), &caller)
875 .is_err()
876 );
877
878 let result =
880 evaluate_policies_with(resolver.as_ref(), &caller, &[&TEST_MANAGE, &TEST_DANGEROUS]);
881 assert_eq!(result.get("harness.manage"), Some(&false));
882
883 let default: Arc<dyn PermissionResolver> = Arc::new(DefaultPermissionResolver);
885 assert!(TEST_MANAGE.evaluate_with(default.as_ref(), &caller).is_ok());
886 }
887
888 #[test]
891 fn permission_display() {
892 assert_eq!(
893 Permission::OrgHarnessesManage.to_string(),
894 "org:harnesses:manage"
895 );
896 assert_eq!(
897 Permission::OrgHarnessesDangerous.to_string(),
898 "org:harnesses:dangerous"
899 );
900 assert_eq!(Permission::OrgAgentsManage.to_string(), "org:agents:manage");
901 }
902
903 #[test]
906 fn role_permissions_returns_correct_sets() {
907 assert_eq!(role_permissions(OrgRole::Owner).len(), 20);
908 assert_eq!(role_permissions(OrgRole::Admin).len(), 14);
909 assert_eq!(role_permissions(OrgRole::Member).len(), 7);
910 assert!(role_has_permission(
911 OrgRole::Owner,
912 &Permission::OrgReportsAdmin
913 ));
914 assert!(role_has_permission(
915 OrgRole::Admin,
916 &Permission::OrgReportsManage
917 ));
918 assert!(role_has_permission(
919 OrgRole::Member,
920 &Permission::OrgReportsView
921 ));
922 }
923
924 #[test]
927 fn caller_without_user_id() {
928 let caller = Caller {
929 org_id: 1,
930 org_public_id: "org_00000000000000000000000000000001".to_string(),
931 user_id: None,
932 role: OrgRole::Admin,
933 is_platform_user: false,
934 is_internal: false,
935 };
936 assert!(TEST_MANAGE.evaluate(&caller).is_ok());
938 }
939
940 #[test]
943 fn empty_policy_always_passes() {
944 const EMPTY: Policy = Policy {
945 id: "empty",
946 rules: &[],
947 };
948 let caller = member_caller();
949 assert!(EMPTY.evaluate(&caller).is_ok());
950 }
951
952 #[test]
953 fn caller_internal_has_owner_role() {
954 let caller = Caller::internal(42);
955 assert_eq!(caller.org_id, 42);
956 assert_eq!(caller.role, OrgRole::Owner);
957 assert!(caller.user_id.is_none());
958 assert!(TEST_MANAGE.evaluate(&caller).is_ok());
960 assert!(TEST_DANGEROUS.evaluate(&caller).is_ok());
961 assert!(TEST_ROLE_ADMIN.evaluate(&caller).is_ok());
962 }
963
964 #[test]
965 fn caller_internal_generates_public_id() {
966 let caller = Caller::internal(1);
967 assert_eq!(caller.org_public_id, "org_00000000000000000000000000000001");
968
969 let caller = Caller::internal(99);
970 assert!(caller.org_public_id.starts_with("org_"));
971 }
972
973 #[test]
974 fn policy_error_is_std_error() {
975 let err = PolicyError::denied("test", "detail");
976 let _: &dyn std::error::Error = &err;
977 }
978
979 #[test]
980 fn policy_error_downcast_from_anyhow() {
981 let err = PolicyError::denied("test.policy", "org:harnesses:manage");
982 let anyhow_err: anyhow::Error = err.into();
983 let downcasted = anyhow_err.downcast_ref::<PolicyError>();
984 assert!(downcasted.is_some());
985 assert_eq!(downcasted.unwrap().policy_id, "test.policy");
986 }
987
988 #[test]
991 fn member_has_view_permissions() {
992 assert!(role_has_permission(
993 OrgRole::Member,
994 &Permission::OrgHarnessesView
995 ));
996 assert!(role_has_permission(
997 OrgRole::Member,
998 &Permission::OrgLlmProvidersView
999 ));
1000 assert!(role_has_permission(
1001 OrgRole::Member,
1002 &Permission::OrgSettingsView
1003 ));
1004 assert!(role_has_permission(
1005 OrgRole::Member,
1006 &Permission::OrgMembersView
1007 ));
1008 }
1009
1010 #[test]
1011 fn member_lacks_manage_for_restricted_resources() {
1012 assert!(!role_has_permission(
1013 OrgRole::Member,
1014 &Permission::OrgHarnessesManage
1015 ));
1016 assert!(!role_has_permission(
1017 OrgRole::Member,
1018 &Permission::OrgLlmProvidersManage
1019 ));
1020 assert!(!role_has_permission(
1021 OrgRole::Member,
1022 &Permission::OrgSettingsManage
1023 ));
1024 assert!(!role_has_permission(
1025 OrgRole::Member,
1026 &Permission::OrgMembersManage
1027 ));
1028 }
1029
1030 const TEST_PLATFORM: Policy = Policy {
1033 id: "durable.manage",
1034 rules: &[Rule::IsPlatformUser],
1035 };
1036
1037 #[test]
1038 fn platform_user_passes_platform_policy() {
1039 let mut caller = owner_caller();
1040 caller.is_platform_user = true;
1041 assert!(TEST_PLATFORM.evaluate(&caller).is_ok());
1042 }
1043
1044 #[test]
1045 fn non_platform_user_fails_platform_policy() {
1046 let caller = owner_caller(); assert!(TEST_PLATFORM.evaluate(&caller).is_err());
1048 }
1049
1050 #[test]
1051 fn internal_caller_is_platform_user() {
1052 let caller = Caller::internal(1);
1053 assert!(caller.is_platform_user);
1054 assert!(TEST_PLATFORM.evaluate(&caller).is_ok());
1055 }
1056
1057 #[test]
1060 fn resource_config_response_serializes() {
1061 let mut policies = HashMap::new();
1062 policies.insert("harness.manage".to_string(), true);
1063 policies.insert("harness.dangerous".to_string(), false);
1064 let response = ResourceConfigResponse { policies };
1065 let json = serde_json::to_value(&response).unwrap();
1066 assert_eq!(json["policies"]["harness.manage"], true);
1067 assert_eq!(json["policies"]["harness.dangerous"], false);
1068 }
1069
1070 #[test]
1073 fn parse_skill_permission_allow_all() {
1074 let rule = parse_skill_permission_rule("allow Skill").unwrap();
1075 assert_eq!(rule.action, SkillPermissionAction::Allow);
1076 assert_eq!(rule.pattern, SkillPermissionPattern::All);
1077 }
1078
1079 #[test]
1080 fn parse_skill_permission_deny_all() {
1081 let rule = parse_skill_permission_rule("deny Skill").unwrap();
1082 assert_eq!(rule.action, SkillPermissionAction::Deny);
1083 assert_eq!(rule.pattern, SkillPermissionPattern::All);
1084 }
1085
1086 #[test]
1087 fn parse_skill_permission_exact_name() {
1088 let rule = parse_skill_permission_rule("allow Skill(commit)").unwrap();
1089 assert_eq!(rule.action, SkillPermissionAction::Allow);
1090 assert_eq!(
1091 rule.pattern,
1092 SkillPermissionPattern::ExactName("commit".to_string())
1093 );
1094 }
1095
1096 #[test]
1097 fn parse_skill_permission_name_wildcard() {
1098 let rule = parse_skill_permission_rule("deny Skill(deploy *)").unwrap();
1099 assert_eq!(rule.action, SkillPermissionAction::Deny);
1100 assert_eq!(
1101 rule.pattern,
1102 SkillPermissionPattern::NameWildcard("deploy".to_string())
1103 );
1104 }
1105
1106 #[test]
1107 fn parse_skill_permission_invalid() {
1108 assert!(parse_skill_permission_rule("allow Something").is_err());
1109 assert!(parse_skill_permission_rule("Skill(commit)").is_err());
1110 assert!(parse_skill_permission_rule("allow Skill()").is_err());
1111 assert!(parse_skill_permission_rule("deny Skill( *)").is_err());
1112 assert!(parse_skill_permission_rule("allow Skill(deploy **)").is_err());
1114 assert!(parse_skill_permission_rule("allow Skill(Deploy)").is_err());
1115 assert!(parse_skill_permission_rule("allow Skill(foo bar)").is_err());
1116 assert!(parse_skill_permission_rule("allow Skill(-deploy)").is_err());
1117 }
1118
1119 #[test]
1120 fn skill_permission_deny_all_blocks_everything() {
1121 let rules = vec![parse_skill_permission_rule("deny Skill").unwrap()];
1122 assert!(!check_skill_permission(&rules, "commit"));
1123 assert!(!check_skill_permission(&rules, "deploy"));
1124 assert!(!check_skill_permission(&rules, "anything"));
1125 }
1126
1127 #[test]
1128 fn skill_permission_no_rules_allows() {
1129 assert!(check_skill_permission(&[], "commit"));
1130 }
1131
1132 #[test]
1133 fn skill_permission_exact_overrides_deny_all() {
1134 let rules = vec![
1135 parse_skill_permission_rule("deny Skill").unwrap(),
1136 parse_skill_permission_rule("allow Skill(commit)").unwrap(),
1137 ];
1138 assert!(check_skill_permission(&rules, "commit"));
1139 assert!(!check_skill_permission(&rules, "deploy"));
1140 }
1141
1142 #[test]
1143 fn skill_permission_deny_specific_with_allow_all() {
1144 let rules = vec![
1145 parse_skill_permission_rule("allow Skill").unwrap(),
1146 parse_skill_permission_rule("deny Skill(deploy)").unwrap(),
1147 ];
1148 assert!(check_skill_permission(&rules, "commit"));
1149 assert!(!check_skill_permission(&rules, "deploy"));
1150 }
1151
1152 #[test]
1153 fn skill_permission_wildcard_overrides_all() {
1154 let rules = vec![
1155 parse_skill_permission_rule("deny Skill").unwrap(),
1156 parse_skill_permission_rule("allow Skill(review-pr *)").unwrap(),
1157 ];
1158 assert!(check_skill_permission(&rules, "review-pr"));
1159 assert!(!check_skill_permission(&rules, "deploy"));
1160 }
1161
1162 #[test]
1163 fn skill_permission_deny_wins_at_same_specificity() {
1164 let rules = vec![
1165 parse_skill_permission_rule("allow Skill(deploy)").unwrap(),
1166 parse_skill_permission_rule("deny Skill(deploy)").unwrap(),
1167 ];
1168 assert!(!check_skill_permission(&rules, "deploy"));
1169 }
1170}