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 OrgAppsView,
31 OrgAppsManage,
33 OrgAppsDangerous,
35 OrgMcpServersView,
37 OrgMcpServersManage,
39 OrgMcpServersDangerous,
41 OrgSkillsView,
43 OrgSkillsManage,
45 OrgSkillsDangerous,
47 OrgCapabilitiesView,
49 OrgCapabilitiesManage,
51 OrgAgentIdentitiesView,
53 OrgAgentIdentitiesManage,
55 OrgPluginsView,
57 OrgPluginsManage,
59 OrgSessionsManage,
61 OrgProvidersView,
63 OrgProvidersManage,
65 OrgSettingsView,
67 OrgSettingsManage,
69 OrgMembersView,
71 OrgMembersManage,
73 OrgApiKeysManage,
75 OrgAuditLogsView,
77 OrgReportsView,
79 OrgReportsManage,
81 OrgReportsAdmin,
83}
84
85impl Permission {
86 pub const fn as_str(&self) -> &'static str {
88 match self {
89 Permission::OrgHarnessesView => "org:harnesses:view",
90 Permission::OrgHarnessesManage => "org:harnesses:manage",
91 Permission::OrgHarnessesDangerous => "org:harnesses:dangerous",
92 Permission::OrgAgentsManage => "org:agents:manage",
93 Permission::OrgAgentsDangerous => "org:agents:dangerous",
94 Permission::OrgAppsView => "org:apps:view",
95 Permission::OrgAppsManage => "org:apps:manage",
96 Permission::OrgAppsDangerous => "org:apps:dangerous",
97 Permission::OrgMcpServersView => "org:mcp-servers:view",
98 Permission::OrgMcpServersManage => "org:mcp-servers:manage",
99 Permission::OrgMcpServersDangerous => "org:mcp-servers:dangerous",
100 Permission::OrgSkillsView => "org:skills:view",
101 Permission::OrgSkillsManage => "org:skills:manage",
102 Permission::OrgSkillsDangerous => "org:skills:dangerous",
103 Permission::OrgCapabilitiesView => "org:capabilities:view",
104 Permission::OrgCapabilitiesManage => "org:capabilities:manage",
105 Permission::OrgAgentIdentitiesView => "org:agent-identities:view",
106 Permission::OrgAgentIdentitiesManage => "org:agent-identities:manage",
107 Permission::OrgPluginsView => "org:plugins:view",
108 Permission::OrgPluginsManage => "org:plugins:manage",
109 Permission::OrgSessionsManage => "org:sessions:manage",
110 Permission::OrgProvidersView => "org:providers:view",
111 Permission::OrgProvidersManage => "org:providers:manage",
112 Permission::OrgSettingsView => "org:settings:view",
113 Permission::OrgSettingsManage => "org:settings:manage",
114 Permission::OrgMembersView => "org:members:view",
115 Permission::OrgMembersManage => "org:members:manage",
116 Permission::OrgApiKeysManage => "org:api-keys:manage",
117 Permission::OrgAuditLogsView => "org:audit-logs:view",
118 Permission::OrgReportsView => "org:reports:view",
119 Permission::OrgReportsManage => "org:reports:manage",
120 Permission::OrgReportsAdmin => "org:reports:admin",
121 }
122 }
123
124 pub const ALL: &'static [Permission] = &[
126 Permission::OrgHarnessesView,
127 Permission::OrgHarnessesManage,
128 Permission::OrgHarnessesDangerous,
129 Permission::OrgAgentsManage,
130 Permission::OrgAgentsDangerous,
131 Permission::OrgAppsView,
132 Permission::OrgAppsManage,
133 Permission::OrgAppsDangerous,
134 Permission::OrgMcpServersView,
135 Permission::OrgMcpServersManage,
136 Permission::OrgMcpServersDangerous,
137 Permission::OrgSkillsView,
138 Permission::OrgSkillsManage,
139 Permission::OrgSkillsDangerous,
140 Permission::OrgCapabilitiesView,
141 Permission::OrgCapabilitiesManage,
142 Permission::OrgAgentIdentitiesView,
143 Permission::OrgAgentIdentitiesManage,
144 Permission::OrgPluginsView,
145 Permission::OrgPluginsManage,
146 Permission::OrgSessionsManage,
147 Permission::OrgProvidersView,
148 Permission::OrgProvidersManage,
149 Permission::OrgSettingsView,
150 Permission::OrgSettingsManage,
151 Permission::OrgMembersView,
152 Permission::OrgMembersManage,
153 Permission::OrgApiKeysManage,
154 Permission::OrgAuditLogsView,
155 Permission::OrgReportsView,
156 Permission::OrgReportsManage,
157 Permission::OrgReportsAdmin,
158 ];
159}
160
161impl fmt::Display for Permission {
162 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163 f.write_str(self.as_str())
164 }
165}
166
167const OWNER_PERMISSIONS: &[Permission] = &[
173 Permission::OrgHarnessesView,
174 Permission::OrgHarnessesManage,
175 Permission::OrgHarnessesDangerous,
176 Permission::OrgAgentsManage,
177 Permission::OrgAgentsDangerous,
178 Permission::OrgAppsView,
179 Permission::OrgAppsManage,
180 Permission::OrgAppsDangerous,
181 Permission::OrgMcpServersView,
182 Permission::OrgMcpServersManage,
183 Permission::OrgMcpServersDangerous,
184 Permission::OrgSkillsView,
185 Permission::OrgSkillsManage,
186 Permission::OrgSkillsDangerous,
187 Permission::OrgCapabilitiesView,
188 Permission::OrgCapabilitiesManage,
189 Permission::OrgAgentIdentitiesView,
190 Permission::OrgAgentIdentitiesManage,
191 Permission::OrgPluginsView,
192 Permission::OrgPluginsManage,
193 Permission::OrgSessionsManage,
194 Permission::OrgProvidersView,
195 Permission::OrgProvidersManage,
196 Permission::OrgSettingsView,
197 Permission::OrgSettingsManage,
198 Permission::OrgMembersView,
199 Permission::OrgMembersManage,
200 Permission::OrgApiKeysManage,
201 Permission::OrgAuditLogsView,
202 Permission::OrgReportsView,
203 Permission::OrgReportsManage,
204 Permission::OrgReportsAdmin,
205];
206
207const ADMIN_PERMISSIONS: &[Permission] = &[
209 Permission::OrgHarnessesView,
210 Permission::OrgHarnessesManage,
211 Permission::OrgAgentsManage,
212 Permission::OrgAppsView,
215 Permission::OrgAppsManage,
216 Permission::OrgMcpServersView,
217 Permission::OrgMcpServersManage,
218 Permission::OrgSkillsView,
219 Permission::OrgSkillsManage,
220 Permission::OrgCapabilitiesView,
221 Permission::OrgCapabilitiesManage,
222 Permission::OrgAgentIdentitiesView,
223 Permission::OrgAgentIdentitiesManage,
224 Permission::OrgPluginsView,
225 Permission::OrgPluginsManage,
226 Permission::OrgSessionsManage,
227 Permission::OrgProvidersView,
228 Permission::OrgProvidersManage,
229 Permission::OrgSettingsView,
230 Permission::OrgSettingsManage,
231 Permission::OrgMembersView,
232 Permission::OrgMembersManage,
233 Permission::OrgApiKeysManage,
234 Permission::OrgAuditLogsView,
235 Permission::OrgReportsView,
236 Permission::OrgReportsManage,
237];
238
239const MEMBER_PERMISSIONS: &[Permission] = &[
242 Permission::OrgHarnessesView,
243 Permission::OrgAgentsManage,
244 Permission::OrgAppsView,
247 Permission::OrgAppsManage,
248 Permission::OrgMcpServersView,
249 Permission::OrgMcpServersManage,
250 Permission::OrgSkillsView,
251 Permission::OrgSkillsManage,
252 Permission::OrgCapabilitiesView,
253 Permission::OrgCapabilitiesManage,
254 Permission::OrgAgentIdentitiesView,
255 Permission::OrgAgentIdentitiesManage,
256 Permission::OrgPluginsView,
259 Permission::OrgSessionsManage,
260 Permission::OrgProvidersView,
261 Permission::OrgSettingsView,
262 Permission::OrgMembersView,
263 Permission::OrgReportsView,
264];
265
266pub fn role_has_permission(role: OrgRole, permission: &Permission) -> bool {
268 let perms = match role {
269 OrgRole::Owner => OWNER_PERMISSIONS,
270 OrgRole::Admin => ADMIN_PERMISSIONS,
271 OrgRole::Member => MEMBER_PERMISSIONS,
272 };
273 perms.contains(permission)
274}
275
276pub fn role_permissions(role: OrgRole) -> &'static [Permission] {
278 match role {
279 OrgRole::Owner => OWNER_PERMISSIONS,
280 OrgRole::Admin => ADMIN_PERMISSIONS,
281 OrgRole::Member => MEMBER_PERMISSIONS,
282 }
283}
284
285pub trait PermissionResolver: Send + Sync {
324 fn has_permission(&self, caller: &Caller, permission: &Permission) -> bool;
329
330 fn caller_permissions(&self, caller: &Caller) -> Vec<Permission>;
336}
337
338#[derive(Debug, Clone, Copy, Default)]
347pub struct DefaultPermissionResolver;
348
349impl PermissionResolver for DefaultPermissionResolver {
350 fn has_permission(&self, caller: &Caller, permission: &Permission) -> bool {
351 role_has_permission(caller.role, permission)
352 }
353
354 fn caller_permissions(&self, caller: &Caller) -> Vec<Permission> {
355 role_permissions(caller.role).to_vec()
356 }
357}
358
359#[derive(Debug, Clone)]
366pub enum Rule {
367 UserHasPermission(Permission),
369 UserHasRole(OrgRole),
371 IsPlatformUser,
373}
374
375impl fmt::Display for Rule {
376 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
377 match self {
378 Rule::UserHasPermission(p) => write!(f, "UserHasPermission({})", p),
379 Rule::UserHasRole(r) => write!(f, "UserHasRole({})", r),
380 Rule::IsPlatformUser => write!(f, "IsPlatformUser"),
381 }
382 }
383}
384
385#[derive(Debug, Clone)]
394pub struct Policy {
395 pub id: &'static str,
397 pub rules: &'static [Rule],
399}
400
401impl Policy {
402 pub fn evaluate(&self, caller: &Caller) -> Result<(), PolicyError> {
405 let resolver = DefaultPermissionResolver;
406 self.evaluate_with(&resolver, caller)
407 }
408
409 pub fn evaluate_with(
414 &self,
415 resolver: &dyn PermissionResolver,
416 caller: &Caller,
417 ) -> Result<(), PolicyError> {
418 for rule in self.rules {
419 match rule {
420 Rule::UserHasPermission(perm) => {
421 if !resolver.has_permission(caller, perm) {
422 return Err(PolicyError::denied(self.id, perm.as_str()));
423 }
424 }
425 Rule::UserHasRole(required) => {
426 if !caller.role.has_permission(*required) {
427 return Err(PolicyError::denied(self.id, &format!("role:{}", required)));
428 }
429 }
430 Rule::IsPlatformUser => {
431 if !caller.is_platform_user {
432 return Err(PolicyError::denied(self.id, "platform_user"));
433 }
434 }
435 }
436 }
437 Ok(())
438 }
439}
440
441#[derive(Debug, Clone)]
450pub struct Caller {
451 pub org_id: i64,
453 pub org_public_id: String,
455 pub user_id: Option<Uuid>,
457 pub role: OrgRole,
459 pub is_platform_user: bool,
461 pub is_internal: bool,
463}
464
465impl Caller {
466 pub fn internal(org_id: i64) -> Self {
481 Self {
482 org_id,
483 org_public_id: crate::organization::org_public_id_from_internal(org_id),
484 user_id: None,
485 role: OrgRole::Owner,
486 is_platform_user: true,
487 is_internal: true,
488 }
489 }
490}
491
492#[derive(Debug, Clone)]
498pub struct PolicyError {
499 pub policy_id: String,
501 pub message: String,
503}
504
505impl PolicyError {
506 pub fn denied(policy_id: &str, detail: &str) -> Self {
507 Self {
508 policy_id: policy_id.to_string(),
509 message: format!(
510 "Access denied: policy '{}' requires '{}'",
511 policy_id, detail
512 ),
513 }
514 }
515}
516
517impl fmt::Display for PolicyError {
518 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
519 write!(f, "{}", self.message)
520 }
521}
522
523impl std::error::Error for PolicyError {}
524
525pub fn evaluate_policies(caller: &Caller, policies: &[&Policy]) -> HashMap<String, bool> {
532 let resolver = DefaultPermissionResolver;
533 evaluate_policies_with(&resolver, caller, policies)
534}
535
536pub fn evaluate_policies_with(
540 resolver: &dyn PermissionResolver,
541 caller: &Caller,
542 policies: &[&Policy],
543) -> HashMap<String, bool> {
544 policies
545 .iter()
546 .map(|policy| {
547 (
548 policy.id.to_string(),
549 policy.evaluate_with(resolver, caller).is_ok(),
550 )
551 })
552 .collect()
553}
554
555#[derive(Debug, Clone, Serialize)]
560#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
561pub struct ResourceConfigResponse {
562 pub policies: HashMap<String, bool>,
564}
565
566pub type PolicyConfigResponse = ResourceConfigResponse;
568
569#[derive(Debug, Clone, Copy, PartialEq, Eq)]
580pub enum SkillPermissionAction {
581 Allow,
582 Deny,
583}
584
585#[derive(Debug, Clone, PartialEq, Eq)]
587pub enum SkillPermissionPattern {
588 All,
590 ExactName(String),
592 NameWildcard(String),
594}
595
596impl SkillPermissionPattern {
597 fn specificity(&self) -> u8 {
599 match self {
600 SkillPermissionPattern::All => 0,
601 SkillPermissionPattern::NameWildcard(_) => 1,
602 SkillPermissionPattern::ExactName(_) => 2,
603 }
604 }
605
606 fn matches(&self, skill_name: &str) -> bool {
614 match self {
615 SkillPermissionPattern::All => true,
616 SkillPermissionPattern::ExactName(name) => name == skill_name,
617 SkillPermissionPattern::NameWildcard(name) => name == skill_name,
618 }
619 }
620}
621
622#[derive(Debug, Clone, PartialEq, Eq)]
624pub struct SkillPermissionRule {
625 pub action: SkillPermissionAction,
626 pub pattern: SkillPermissionPattern,
627}
628
629pub fn parse_skill_permission_rule(input: &str) -> Result<SkillPermissionRule, String> {
636 let input = input.trim();
637 let (action, rest) = if let Some(rest) = input.strip_prefix("allow ") {
638 (SkillPermissionAction::Allow, rest.trim())
639 } else if let Some(rest) = input.strip_prefix("deny ") {
640 (SkillPermissionAction::Deny, rest.trim())
641 } else {
642 return Err(format!("Rule must start with 'allow' or 'deny': {input}"));
643 };
644
645 if rest == "Skill" {
647 return Ok(SkillPermissionRule {
648 action,
649 pattern: SkillPermissionPattern::All,
650 });
651 }
652
653 if let Some(inner) = rest
654 .strip_prefix("Skill(")
655 .and_then(|s| s.strip_suffix(')'))
656 {
657 let inner = inner.trim();
658 if inner.is_empty() {
659 return Err("Skill name cannot be empty in Skill()".to_string());
660 }
661
662 let (name, is_wildcard) = if let Some(name) = inner.strip_suffix(" *") {
664 (name, true)
665 } else {
666 (inner, false)
667 };
668
669 if let Err(errors) = crate::skill::validate_skill_name(name) {
671 return Err(format!(
672 "Invalid skill name '{}': {}",
673 name,
674 errors.join(", ")
675 ));
676 }
677
678 let pattern = if is_wildcard {
679 SkillPermissionPattern::NameWildcard(name.to_string())
680 } else {
681 SkillPermissionPattern::ExactName(name.to_string())
682 };
683
684 return Ok(SkillPermissionRule { action, pattern });
685 }
686
687 Err(format!(
688 "Invalid skill permission pattern: {rest}. Expected 'Skill', 'Skill(name)', or 'Skill(name *)'"
689 ))
690}
691
692pub fn check_skill_permission(rules: &[SkillPermissionRule], skill_name: &str) -> bool {
699 let mut best_specificity: Option<u8> = None;
700 let mut best_allowed = true; for rule in rules {
703 if !rule.pattern.matches(skill_name) {
704 continue;
705 }
706 let spec = rule.pattern.specificity();
707 let is_allow = rule.action == SkillPermissionAction::Allow;
708
709 match best_specificity {
710 None => {
711 best_specificity = Some(spec);
712 best_allowed = is_allow;
713 }
714 Some(best) if spec > best => {
715 best_specificity = Some(spec);
716 best_allowed = is_allow;
717 }
718 Some(best) if spec == best && !is_allow => {
720 best_allowed = false;
721 }
722 _ => {} }
724 }
725
726 best_allowed
727}
728
729#[cfg(test)]
734mod tests {
735 use super::*;
736 use std::sync::Arc;
737
738 fn owner_caller() -> Caller {
739 Caller {
740 org_id: 1,
741 org_public_id: "org_00000000000000000000000000000001".to_string(),
742 user_id: Some(Uuid::new_v4()),
743 role: OrgRole::Owner,
744 is_platform_user: false,
745 is_internal: false,
746 }
747 }
748
749 fn admin_caller() -> Caller {
750 Caller {
751 org_id: 1,
752 org_public_id: "org_00000000000000000000000000000001".to_string(),
753 user_id: Some(Uuid::new_v4()),
754 role: OrgRole::Admin,
755 is_platform_user: false,
756 is_internal: false,
757 }
758 }
759
760 fn member_caller() -> Caller {
761 Caller {
762 org_id: 1,
763 org_public_id: "org_00000000000000000000000000000001".to_string(),
764 user_id: Some(Uuid::new_v4()),
765 role: OrgRole::Member,
766 is_platform_user: false,
767 is_internal: false,
768 }
769 }
770
771 #[test]
774 fn owner_has_all_permissions() {
775 for perm in Permission::ALL {
776 assert!(
777 role_has_permission(OrgRole::Owner, perm),
778 "Owner should have {:?}",
779 perm
780 );
781 }
782 }
783
784 #[test]
785 fn admin_has_manage_but_not_dangerous() {
786 assert!(role_has_permission(
787 OrgRole::Admin,
788 &Permission::OrgHarnessesManage
789 ));
790 assert!(!role_has_permission(
791 OrgRole::Admin,
792 &Permission::OrgHarnessesDangerous
793 ));
794 assert!(role_has_permission(
795 OrgRole::Admin,
796 &Permission::OrgAgentsManage
797 ));
798 assert!(role_has_permission(
799 OrgRole::Admin,
800 &Permission::OrgSettingsManage
801 ));
802 }
803
804 #[test]
805 fn member_has_only_basic_permissions() {
806 assert!(role_has_permission(
807 OrgRole::Member,
808 &Permission::OrgAgentsManage
809 ));
810 assert!(role_has_permission(
811 OrgRole::Member,
812 &Permission::OrgSessionsManage
813 ));
814 assert!(!role_has_permission(
815 OrgRole::Member,
816 &Permission::OrgHarnessesManage
817 ));
818 assert!(!role_has_permission(
819 OrgRole::Member,
820 &Permission::OrgSettingsManage
821 ));
822 assert!(!role_has_permission(
823 OrgRole::Member,
824 &Permission::OrgApiKeysManage
825 ));
826 }
827
828 const SPLIT_DOMAIN_VIEW_MANAGE: &[Permission] = &[
836 Permission::OrgAppsView,
837 Permission::OrgAppsManage,
838 Permission::OrgMcpServersView,
839 Permission::OrgMcpServersManage,
840 Permission::OrgSkillsView,
841 Permission::OrgSkillsManage,
842 Permission::OrgCapabilitiesView,
843 Permission::OrgCapabilitiesManage,
844 Permission::OrgAgentIdentitiesView,
845 Permission::OrgAgentIdentitiesManage,
846 Permission::OrgPluginsView,
847 ];
848
849 #[test]
850 fn eve656_roles_with_agents_manage_keep_per_domain_access() {
851 for role in [OrgRole::Owner, OrgRole::Admin, OrgRole::Member] {
854 assert!(role_has_permission(role, &Permission::OrgAgentsManage));
855 for perm in SPLIT_DOMAIN_VIEW_MANAGE {
856 assert!(
857 role_has_permission(role, perm),
858 "{role:?} must retain {perm:?} after the OrgAgentsManage split"
859 );
860 }
861 }
862 }
863
864 #[test]
865 fn eve656_plugin_manage_stays_admin_plus_only() {
866 assert!(role_has_permission(
869 OrgRole::Owner,
870 &Permission::OrgPluginsManage
871 ));
872 assert!(role_has_permission(
873 OrgRole::Admin,
874 &Permission::OrgPluginsManage
875 ));
876 assert!(!role_has_permission(
877 OrgRole::Member,
878 &Permission::OrgPluginsManage
879 ));
880 }
881
882 const TEST_MANAGE: Policy = Policy {
885 id: "harness.manage",
886 rules: &[Rule::UserHasPermission(Permission::OrgHarnessesManage)],
887 };
888
889 const TEST_DANGEROUS: Policy = Policy {
890 id: "harness.dangerous",
891 rules: &[
892 Rule::UserHasPermission(Permission::OrgHarnessesManage),
893 Rule::UserHasPermission(Permission::OrgHarnessesDangerous),
894 ],
895 };
896
897 const TEST_ROLE_ADMIN: Policy = Policy {
898 id: "require.admin",
899 rules: &[Rule::UserHasRole(OrgRole::Admin)],
900 };
901
902 #[test]
903 fn owner_passes_all_policies() {
904 let caller = owner_caller();
905 assert!(TEST_MANAGE.evaluate(&caller).is_ok());
906 assert!(TEST_DANGEROUS.evaluate(&caller).is_ok());
907 assert!(TEST_ROLE_ADMIN.evaluate(&caller).is_ok());
908 }
909
910 #[test]
911 fn admin_passes_manage_but_not_dangerous() {
912 let caller = admin_caller();
913 assert!(TEST_MANAGE.evaluate(&caller).is_ok());
914 assert!(TEST_DANGEROUS.evaluate(&caller).is_err());
915 assert!(TEST_ROLE_ADMIN.evaluate(&caller).is_ok());
916 }
917
918 #[test]
919 fn member_fails_manage_and_dangerous() {
920 let caller = member_caller();
921 assert!(TEST_MANAGE.evaluate(&caller).is_err());
922 assert!(TEST_DANGEROUS.evaluate(&caller).is_err());
923 assert!(TEST_ROLE_ADMIN.evaluate(&caller).is_err());
924 }
925
926 #[test]
927 fn policy_error_contains_useful_info() {
928 let caller = member_caller();
929 let err = TEST_MANAGE.evaluate(&caller).unwrap_err();
930 assert_eq!(err.policy_id, "harness.manage");
931 assert!(err.message.contains("org:harnesses:manage"));
932 assert!(err.to_string().contains("Access denied"));
933 }
934
935 #[test]
938 fn evaluate_policies_returns_correct_map() {
939 let caller = admin_caller();
940 let result = evaluate_policies(&caller, &[&TEST_MANAGE, &TEST_DANGEROUS]);
941
942 assert_eq!(result.get("harness.manage"), Some(&true));
943 assert_eq!(result.get("harness.dangerous"), Some(&false));
944 }
945
946 #[test]
947 fn evaluate_policies_owner_all_true() {
948 let caller = owner_caller();
949 let result = evaluate_policies(&caller, &[&TEST_MANAGE, &TEST_DANGEROUS, &TEST_ROLE_ADMIN]);
950
951 assert!(result.values().all(|&v| v));
952 }
953
954 #[test]
955 fn evaluate_policies_member_all_false_for_admin_policies() {
956 let caller = member_caller();
957 let result = evaluate_policies(&caller, &[&TEST_MANAGE, &TEST_DANGEROUS, &TEST_ROLE_ADMIN]);
958
959 assert!(result.values().all(|&v| !v));
960 }
961
962 #[test]
963 fn default_permission_resolver_matches_hardcoded_role_mapping() {
964 let resolver = DefaultPermissionResolver;
965
966 for caller in [owner_caller(), admin_caller(), member_caller()] {
967 for permission in Permission::ALL {
968 assert_eq!(
969 resolver.has_permission(&caller, permission),
970 role_has_permission(caller.role, permission)
971 );
972 }
973
974 assert_eq!(
975 resolver.caller_permissions(&caller),
976 role_permissions(caller.role).to_vec()
977 );
978 }
979 }
980
981 struct DenyManageResolver;
982
983 impl PermissionResolver for DenyManageResolver {
984 fn has_permission(&self, _caller: &Caller, permission: &Permission) -> bool {
985 permission != &Permission::OrgHarnessesManage
986 }
987
988 fn caller_permissions(&self, _caller: &Caller) -> Vec<Permission> {
989 Permission::ALL
990 .iter()
991 .copied()
992 .filter(|permission| permission != &Permission::OrgHarnessesManage)
993 .collect()
994 }
995 }
996
997 #[test]
998 fn evaluate_with_uses_custom_permission_resolver() {
999 let caller = owner_caller();
1000 let resolver = DenyManageResolver;
1001
1002 assert!(TEST_MANAGE.evaluate(&caller).is_ok());
1003 assert!(TEST_MANAGE.evaluate_with(&resolver, &caller).is_err());
1004 }
1005
1006 #[test]
1007 fn evaluate_policies_with_uses_custom_permission_resolver() {
1008 let caller = owner_caller();
1009 let resolver = DenyManageResolver;
1010
1011 let result = evaluate_policies_with(&resolver, &caller, &[&TEST_MANAGE, &TEST_DANGEROUS]);
1012
1013 assert_eq!(result.get("harness.manage"), Some(&false));
1014 assert_eq!(result.get("harness.dangerous"), Some(&false));
1015 }
1016
1017 #[test]
1018 fn arc_resolver_works_as_trait_object() {
1019 let resolver: Arc<dyn PermissionResolver> = Arc::new(DenyManageResolver);
1021 let caller = owner_caller();
1022
1023 assert!(
1025 TEST_MANAGE
1026 .evaluate_with(resolver.as_ref(), &caller)
1027 .is_err()
1028 );
1029
1030 let result =
1032 evaluate_policies_with(resolver.as_ref(), &caller, &[&TEST_MANAGE, &TEST_DANGEROUS]);
1033 assert_eq!(result.get("harness.manage"), Some(&false));
1034
1035 let default: Arc<dyn PermissionResolver> = Arc::new(DefaultPermissionResolver);
1037 assert!(TEST_MANAGE.evaluate_with(default.as_ref(), &caller).is_ok());
1038 }
1039
1040 #[test]
1043 fn permission_display() {
1044 assert_eq!(
1045 Permission::OrgHarnessesManage.to_string(),
1046 "org:harnesses:manage"
1047 );
1048 assert_eq!(
1049 Permission::OrgHarnessesDangerous.to_string(),
1050 "org:harnesses:dangerous"
1051 );
1052 assert_eq!(Permission::OrgAgentsManage.to_string(), "org:agents:manage");
1053 }
1054
1055 #[test]
1058 fn role_permissions_returns_correct_sets() {
1059 assert_eq!(role_permissions(OrgRole::Owner).len(), 32);
1060 assert_eq!(role_permissions(OrgRole::Admin).len(), 26);
1061 assert_eq!(role_permissions(OrgRole::Member).len(), 18);
1062 assert!(role_has_permission(
1063 OrgRole::Owner,
1064 &Permission::OrgReportsAdmin
1065 ));
1066 assert!(role_has_permission(
1067 OrgRole::Admin,
1068 &Permission::OrgReportsManage
1069 ));
1070 assert!(role_has_permission(
1071 OrgRole::Member,
1072 &Permission::OrgReportsView
1073 ));
1074 }
1075
1076 #[test]
1079 fn caller_without_user_id() {
1080 let caller = Caller {
1081 org_id: 1,
1082 org_public_id: "org_00000000000000000000000000000001".to_string(),
1083 user_id: None,
1084 role: OrgRole::Admin,
1085 is_platform_user: false,
1086 is_internal: false,
1087 };
1088 assert!(TEST_MANAGE.evaluate(&caller).is_ok());
1090 }
1091
1092 #[test]
1095 fn empty_policy_always_passes() {
1096 const EMPTY: Policy = Policy {
1097 id: "empty",
1098 rules: &[],
1099 };
1100 let caller = member_caller();
1101 assert!(EMPTY.evaluate(&caller).is_ok());
1102 }
1103
1104 #[test]
1105 fn caller_internal_has_owner_role() {
1106 let caller = Caller::internal(42);
1107 assert_eq!(caller.org_id, 42);
1108 assert_eq!(caller.role, OrgRole::Owner);
1109 assert!(caller.user_id.is_none());
1110 assert!(TEST_MANAGE.evaluate(&caller).is_ok());
1112 assert!(TEST_DANGEROUS.evaluate(&caller).is_ok());
1113 assert!(TEST_ROLE_ADMIN.evaluate(&caller).is_ok());
1114 }
1115
1116 #[test]
1117 fn caller_internal_generates_public_id() {
1118 let caller = Caller::internal(1);
1119 assert_eq!(caller.org_public_id, "org_00000000000000000000000000000001");
1120
1121 let caller = Caller::internal(99);
1122 assert!(caller.org_public_id.starts_with("org_"));
1123 }
1124
1125 #[test]
1126 fn policy_error_is_std_error() {
1127 let err = PolicyError::denied("test", "detail");
1128 let _: &dyn std::error::Error = &err;
1129 }
1130
1131 #[test]
1132 fn policy_error_downcast_from_anyhow() {
1133 let err = PolicyError::denied("test.policy", "org:harnesses:manage");
1134 let anyhow_err: anyhow::Error = err.into();
1135 let downcasted = anyhow_err.downcast_ref::<PolicyError>();
1136 assert!(downcasted.is_some());
1137 assert_eq!(downcasted.unwrap().policy_id, "test.policy");
1138 }
1139
1140 #[test]
1143 fn member_has_view_permissions() {
1144 assert!(role_has_permission(
1145 OrgRole::Member,
1146 &Permission::OrgHarnessesView
1147 ));
1148 assert!(role_has_permission(
1149 OrgRole::Member,
1150 &Permission::OrgProvidersView
1151 ));
1152 assert!(role_has_permission(
1153 OrgRole::Member,
1154 &Permission::OrgSettingsView
1155 ));
1156 assert!(role_has_permission(
1157 OrgRole::Member,
1158 &Permission::OrgMembersView
1159 ));
1160 }
1161
1162 #[test]
1163 fn member_lacks_manage_for_restricted_resources() {
1164 assert!(!role_has_permission(
1165 OrgRole::Member,
1166 &Permission::OrgHarnessesManage
1167 ));
1168 assert!(!role_has_permission(
1169 OrgRole::Member,
1170 &Permission::OrgProvidersManage
1171 ));
1172 assert!(!role_has_permission(
1173 OrgRole::Member,
1174 &Permission::OrgSettingsManage
1175 ));
1176 assert!(!role_has_permission(
1177 OrgRole::Member,
1178 &Permission::OrgMembersManage
1179 ));
1180 }
1181
1182 const TEST_PLATFORM: Policy = Policy {
1185 id: "durable.manage",
1186 rules: &[Rule::IsPlatformUser],
1187 };
1188
1189 #[test]
1190 fn platform_user_passes_platform_policy() {
1191 let mut caller = owner_caller();
1192 caller.is_platform_user = true;
1193 assert!(TEST_PLATFORM.evaluate(&caller).is_ok());
1194 }
1195
1196 #[test]
1197 fn non_platform_user_fails_platform_policy() {
1198 let caller = owner_caller(); assert!(TEST_PLATFORM.evaluate(&caller).is_err());
1200 }
1201
1202 #[test]
1203 fn internal_caller_is_platform_user() {
1204 let caller = Caller::internal(1);
1205 assert!(caller.is_platform_user);
1206 assert!(TEST_PLATFORM.evaluate(&caller).is_ok());
1207 }
1208
1209 #[test]
1212 fn resource_config_response_serializes() {
1213 let mut policies = HashMap::new();
1214 policies.insert("harness.manage".to_string(), true);
1215 policies.insert("harness.dangerous".to_string(), false);
1216 let response = ResourceConfigResponse { policies };
1217 let json = serde_json::to_value(&response).unwrap();
1218 assert_eq!(json["policies"]["harness.manage"], true);
1219 assert_eq!(json["policies"]["harness.dangerous"], false);
1220 }
1221
1222 #[test]
1225 fn parse_skill_permission_allow_all() {
1226 let rule = parse_skill_permission_rule("allow Skill").unwrap();
1227 assert_eq!(rule.action, SkillPermissionAction::Allow);
1228 assert_eq!(rule.pattern, SkillPermissionPattern::All);
1229 }
1230
1231 #[test]
1232 fn parse_skill_permission_deny_all() {
1233 let rule = parse_skill_permission_rule("deny Skill").unwrap();
1234 assert_eq!(rule.action, SkillPermissionAction::Deny);
1235 assert_eq!(rule.pattern, SkillPermissionPattern::All);
1236 }
1237
1238 #[test]
1239 fn parse_skill_permission_exact_name() {
1240 let rule = parse_skill_permission_rule("allow Skill(commit)").unwrap();
1241 assert_eq!(rule.action, SkillPermissionAction::Allow);
1242 assert_eq!(
1243 rule.pattern,
1244 SkillPermissionPattern::ExactName("commit".to_string())
1245 );
1246 }
1247
1248 #[test]
1249 fn parse_skill_permission_name_wildcard() {
1250 let rule = parse_skill_permission_rule("deny Skill(deploy *)").unwrap();
1251 assert_eq!(rule.action, SkillPermissionAction::Deny);
1252 assert_eq!(
1253 rule.pattern,
1254 SkillPermissionPattern::NameWildcard("deploy".to_string())
1255 );
1256 }
1257
1258 #[test]
1259 fn parse_skill_permission_invalid() {
1260 assert!(parse_skill_permission_rule("allow Something").is_err());
1261 assert!(parse_skill_permission_rule("Skill(commit)").is_err());
1262 assert!(parse_skill_permission_rule("allow Skill()").is_err());
1263 assert!(parse_skill_permission_rule("deny Skill( *)").is_err());
1264 assert!(parse_skill_permission_rule("allow Skill(deploy **)").is_err());
1266 assert!(parse_skill_permission_rule("allow Skill(Deploy)").is_err());
1267 assert!(parse_skill_permission_rule("allow Skill(foo bar)").is_err());
1268 assert!(parse_skill_permission_rule("allow Skill(-deploy)").is_err());
1269 }
1270
1271 #[test]
1272 fn skill_permission_deny_all_blocks_everything() {
1273 let rules = vec![parse_skill_permission_rule("deny Skill").unwrap()];
1274 assert!(!check_skill_permission(&rules, "commit"));
1275 assert!(!check_skill_permission(&rules, "deploy"));
1276 assert!(!check_skill_permission(&rules, "anything"));
1277 }
1278
1279 #[test]
1280 fn skill_permission_no_rules_allows() {
1281 assert!(check_skill_permission(&[], "commit"));
1282 }
1283
1284 #[test]
1285 fn skill_permission_exact_overrides_deny_all() {
1286 let rules = vec![
1287 parse_skill_permission_rule("deny Skill").unwrap(),
1288 parse_skill_permission_rule("allow Skill(commit)").unwrap(),
1289 ];
1290 assert!(check_skill_permission(&rules, "commit"));
1291 assert!(!check_skill_permission(&rules, "deploy"));
1292 }
1293
1294 #[test]
1295 fn skill_permission_deny_specific_with_allow_all() {
1296 let rules = vec![
1297 parse_skill_permission_rule("allow Skill").unwrap(),
1298 parse_skill_permission_rule("deny Skill(deploy)").unwrap(),
1299 ];
1300 assert!(check_skill_permission(&rules, "commit"));
1301 assert!(!check_skill_permission(&rules, "deploy"));
1302 }
1303
1304 #[test]
1305 fn skill_permission_wildcard_overrides_all() {
1306 let rules = vec![
1307 parse_skill_permission_rule("deny Skill").unwrap(),
1308 parse_skill_permission_rule("allow Skill(review-pr *)").unwrap(),
1309 ];
1310 assert!(check_skill_permission(&rules, "review-pr"));
1311 assert!(!check_skill_permission(&rules, "deploy"));
1312 }
1313
1314 #[test]
1315 fn skill_permission_deny_wins_at_same_specificity() {
1316 let rules = vec![
1317 parse_skill_permission_rule("allow Skill(deploy)").unwrap(),
1318 parse_skill_permission_rule("deny Skill(deploy)").unwrap(),
1319 ];
1320 assert!(!check_skill_permission(&rules, "deploy"));
1321 }
1322}