Skip to main content

everruns_core/
permissions.rs

1// Permissions model for fine-grained access control
2//
3// Decision: Default permissions are hardcoded per OrgRole; custom resolvers can override evaluation.
4// Decision: Policies are const values evaluated at service method entry via #[policy] macro.
5// Decision: Permission format is `org:<resource>:<action>`.
6// See specs/permissions.md for full design.
7
8use crate::organization::OrgRole;
9use serde::Serialize;
10use std::collections::HashMap;
11use std::fmt;
12use uuid::Uuid;
13
14/// A permission identifier representing an action on a resource.
15///
16/// Format: `org:<resource>:<action>`
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18pub enum Permission {
19    /// View harnesses (read-only)
20    OrgHarnessesView,
21    /// CRUD on harnesses
22    OrgHarnessesManage,
23    /// Delete, reset, and other dangerous harness operations
24    OrgHarnessesDangerous,
25    /// CRUD on agents
26    OrgAgentsManage,
27    /// Delete and other dangerous agent operations
28    OrgAgentsDangerous,
29    /// Dangerous skill operations
30    OrgSkillsDangerous,
31    /// Dangerous MCP server operations
32    OrgMcpServersDangerous,
33    /// Dangerous app operations
34    OrgAppsDangerous,
35    /// CRUD on sessions
36    OrgSessionsManage,
37    /// View LLM providers (read-only)
38    OrgLlmProvidersView,
39    /// CRUD on LLM providers
40    OrgLlmProvidersManage,
41    /// View organization settings (read-only)
42    OrgSettingsView,
43    /// Organization settings
44    OrgSettingsManage,
45    /// View members (read-only)
46    OrgMembersView,
47    /// Invite/remove members
48    OrgMembersManage,
49    /// CRUD on API keys
50    OrgApiKeysManage,
51    /// View audit logs (read-only)
52    OrgAuditLogsView,
53    /// View semantic reports (read-only)
54    OrgReportsView,
55    /// Manage saved reports and reporting definitions
56    OrgReportsManage,
57    /// Run reporting administrative operations
58    OrgReportsAdmin,
59}
60
61impl Permission {
62    /// String identifier for this permission.
63    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    /// All defined permissions.
89    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
119// ============================================================================
120// Role → Permission mapping
121// ============================================================================
122
123/// Permissions granted to Owner role.
124const 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
147/// Permissions granted to Admin role.
148const 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
165/// Permissions granted to Member role.
166/// Members can view all resources but only manage agents and sessions.
167const 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
177/// Check whether a role grants a specific permission.
178pub 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
187/// List all permissions granted to a role.
188pub 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
196/// Contract for resolving which permissions a caller has.
197///
198/// `DefaultPermissionResolver` preserves the OSS role-based mapping, while downstream
199/// consumers can implement this trait to inject billing-tier rules, database-backed
200/// grants, or external RBAC systems without patching policy evaluation itself.
201///
202/// # Contract
203///
204/// Implementations must:
205///
206/// - Fail closed. Return `true` from `has_permission()` only when the caller is
207///   actually authorized.
208/// - Be consistent. If `has_permission(caller, permission)` returns `true`, then
209///   `caller_permissions(caller)` must include that same permission.
210/// - Be safe to call from async request paths (`Send + Sync`).
211///
212/// # Example
213///
214/// ```no_run
215/// use everruns_core::{Caller, Permission, PermissionResolver};
216///
217/// struct TierAwareResolver;
218///
219/// impl PermissionResolver for TierAwareResolver {
220///     fn has_permission(&self, caller: &Caller, permission: &Permission) -> bool {
221///         caller.role == everruns_core::organization::OrgRole::Owner
222///             && permission == &Permission::OrgHarnessesDangerous
223///     }
224///
225///     fn caller_permissions(&self, caller: &Caller) -> Vec<Permission> {
226///         Permission::ALL
227///             .iter()
228///             .copied()
229///             .filter(|permission| self.has_permission(caller, permission))
230///             .collect()
231///     }
232/// }
233/// ```
234pub trait PermissionResolver: Send + Sync {
235    /// Return whether the caller currently has `permission`.
236    ///
237    /// This method is used by `Policy::evaluate_with()` for
238    /// `Rule::UserHasPermission` checks.
239    fn has_permission(&self, caller: &Caller, permission: &Permission) -> bool;
240
241    /// Return the full set of permissions currently granted to `caller`.
242    ///
243    /// Config endpoints and downstream policy-reporting code use this to expose
244    /// evaluated capabilities to a UI. Prefer deterministic ordering so callers
245    /// can compare results reliably.
246    fn caller_permissions(&self, caller: &Caller) -> Vec<Permission>;
247}
248
249/// Default `PermissionResolver` backed by the built-in role-to-permission map.
250///
251/// This preserves the existing OSS behavior by delegating to
252/// `role_has_permission()` and `role_permissions()`.
253///
254/// # Safety
255///
256/// This resolver is stateless and can be shared freely across threads.
257#[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// ============================================================================
271// Rule
272// ============================================================================
273
274/// A single predicate evaluated against a Caller context.
275/// All rules in a Policy must pass (AND logic).
276#[derive(Debug, Clone)]
277pub enum Rule {
278    /// Caller's role must grant this permission.
279    UserHasPermission(Permission),
280    /// Caller must have at least this OrgRole level.
281    UserHasRole(OrgRole),
282    /// Caller must be a platform user (allowlisted email).
283    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// ============================================================================
297// Policy
298// ============================================================================
299
300/// A named set of rules that must all pass for access.
301///
302/// Policies are defined as `const` values and attached to service methods
303/// via the `#[policy]` macro.
304#[derive(Debug, Clone)]
305pub struct Policy {
306    /// Unique identifier for this policy (e.g. "harness.manage").
307    pub id: &'static str,
308    /// Rules that must all pass. AND logic.
309    pub rules: &'static [Rule],
310}
311
312impl Policy {
313    /// Evaluate all rules against the caller. Returns `Ok(())` if all pass,
314    /// or `Err(PolicyError)` on the first failing rule.
315    pub fn evaluate(&self, caller: &Caller) -> Result<(), PolicyError> {
316        let resolver = DefaultPermissionResolver;
317        self.evaluate_with(&resolver, caller)
318    }
319
320    /// Evaluate all rules against the caller using a custom permission resolver.
321    ///
322    /// `Rule::UserHasPermission` checks are delegated to `resolver`, while other
323    /// rule types keep their current built-in behavior.
324    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// ============================================================================
353// Caller
354// ============================================================================
355
356/// Auth context passed from API handler to service layer.
357///
358/// Replaces raw `org_id: i64` parameter on service methods. Carries all
359/// information needed for policy evaluation.
360#[derive(Debug, Clone)]
361pub struct Caller {
362    /// Internal organization ID (for database queries).
363    pub org_id: i64,
364    /// External organization public ID.
365    pub org_public_id: String,
366    /// Authenticated user ID (`None` for API key auth without user context).
367    pub user_id: Option<Uuid>,
368    /// User's role in the organization.
369    pub role: OrgRole,
370    /// Whether the caller is a platform user (email allowlist).
371    pub is_platform_user: bool,
372    /// Whether the caller originates from an internal server path.
373    pub is_internal: bool,
374}
375
376impl Caller {
377    /// Create an internal/platform caller with Owner role.
378    ///
379    /// Used for gRPC service calls (worker ↔ server) and other internal
380    /// operations that should bypass all policy checks.
381    // THREAT[TM-AUTHZ-002]: Internal caller bypasses policies; only use for gRPC/internal paths
382    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// ============================================================================
395// PolicyError
396// ============================================================================
397
398/// Error returned when a policy evaluation fails.
399#[derive(Debug, Clone)]
400pub struct PolicyError {
401    /// Which policy failed.
402    pub policy_id: String,
403    /// Human-readable message.
404    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
427// ============================================================================
428// Policy evaluation helpers (for config endpoints)
429// ============================================================================
430
431/// Evaluate multiple policies against a caller and return a map of results.
432/// Used by `/config` endpoints to expose policy results to the UI.
433pub fn evaluate_policies(caller: &Caller, policies: &[&Policy]) -> HashMap<String, bool> {
434    let resolver = DefaultPermissionResolver;
435    evaluate_policies_with(&resolver, caller, policies)
436}
437
438/// Evaluate multiple policies using a custom permission resolver.
439///
440/// This is the config-endpoint companion to `Policy::evaluate_with()`.
441pub 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/// Response type for per-resource config endpoints.
458///
459/// Every resource exposes `GET /v1/{resource}/config` returning this type.
460/// UI uses it to gate controls (create/edit/delete buttons, admin panels).
461#[derive(Debug, Clone, Serialize)]
462#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
463pub struct ResourceConfigResponse {
464    /// Map of policy ID → whether the caller satisfies it.
465    pub policies: HashMap<String, bool>,
466}
467
468/// Backwards compat alias — prefer `ResourceConfigResponse`.
469pub type PolicyConfigResponse = ResourceConfigResponse;
470
471// ============================================================================
472// Skill-scoped permission rules
473// ============================================================================
474//
475// Decision: Skill permissions use a separate ACL system from org permissions.
476// Rules are parsed from strings like "allow Skill(commit)" or "deny Skill".
477// Specificity-based precedence: exact > wildcard > all; deny wins at same level.
478// See specs/permissions.md and EVE-140 for design.
479
480/// Action for a skill permission rule.
481#[derive(Debug, Clone, Copy, PartialEq, Eq)]
482pub enum SkillPermissionAction {
483    Allow,
484    Deny,
485}
486
487/// Pattern for matching skill invocations.
488#[derive(Debug, Clone, PartialEq, Eq)]
489pub enum SkillPermissionPattern {
490    /// Matches any skill invocation (`Skill`)
491    All,
492    /// Exact skill name match (`Skill(name)`)
493    ExactName(String),
494    /// Skill name with any args (`Skill(name *)`)
495    NameWildcard(String),
496}
497
498impl SkillPermissionPattern {
499    /// Specificity for precedence ordering. Higher = more specific.
500    fn specificity(&self) -> u8 {
501        match self {
502            SkillPermissionPattern::All => 0,
503            SkillPermissionPattern::NameWildcard(_) => 1,
504            SkillPermissionPattern::ExactName(_) => 2,
505        }
506    }
507
508    /// Check if this pattern matches a skill name.
509    ///
510    /// Note: `NameWildcard` and `ExactName` both match by skill name only.
511    /// The distinction exists for specificity-based precedence: `ExactName`
512    /// (specificity 2) overrides `NameWildcard` (specificity 1). When
513    /// argument-level matching is added, `NameWildcard` will match any
514    /// invocation args while `ExactName` will match only no-arg invocations.
515    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/// A single skill permission rule.
525#[derive(Debug, Clone, PartialEq, Eq)]
526pub struct SkillPermissionRule {
527    pub action: SkillPermissionAction,
528    pub pattern: SkillPermissionPattern,
529}
530
531/// Parse a skill permission rule from a string.
532///
533/// Supported formats:
534/// - `allow Skill` / `deny Skill` — match all skills
535/// - `allow Skill(name)` / `deny Skill(name)` — exact name
536/// - `allow Skill(name *)` / `deny Skill(name *)` — name with any args
537pub 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    // Parse pattern
548    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        // Check for wildcard: "name *" (exactly one space then asterisk)
565        let (name, is_wildcard) = if let Some(name) = inner.strip_suffix(" *") {
566            (name, true)
567        } else {
568            (inner, false)
569        };
570
571        // Validate skill name using the canonical validator
572        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
594/// Check whether a skill is allowed by the given rules.
595///
596/// Returns `true` if allowed, `false` if denied.
597/// If no rules match, defaults to allowed.
598///
599/// Precedence: higher specificity wins. At same specificity, deny wins.
600pub fn check_skill_permission(rules: &[SkillPermissionRule], skill_name: &str) -> bool {
601    let mut best_specificity: Option<u8> = None;
602    let mut best_allowed = true; // default: allow
603
604    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            // Same specificity: deny wins
621            Some(best) if spec == best && !is_allow => {
622                best_allowed = false;
623            }
624            _ => {} // lower specificity, ignore
625        }
626    }
627
628    best_allowed
629}
630
631// ============================================================================
632// Tests
633// ============================================================================
634
635#[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    // -- role_has_permission tests --
674
675    #[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    // -- Policy evaluation tests --
731
732    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    // -- evaluate_policies (config endpoint helper) tests --
784
785    #[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        // Validates the pattern used in AuthState: Arc<dyn PermissionResolver>
868        let resolver: Arc<dyn PermissionResolver> = Arc::new(DenyManageResolver);
869        let caller = owner_caller();
870
871        // Policy::evaluate_with accepts &dyn PermissionResolver
872        assert!(
873            TEST_MANAGE
874                .evaluate_with(resolver.as_ref(), &caller)
875                .is_err()
876        );
877
878        // evaluate_policies_with accepts &dyn PermissionResolver
879        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        // Default resolver works the same way
884        let default: Arc<dyn PermissionResolver> = Arc::new(DefaultPermissionResolver);
885        assert!(TEST_MANAGE.evaluate_with(default.as_ref(), &caller).is_ok());
886    }
887
888    // -- Permission display --
889
890    #[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    // -- role_permissions --
904
905    #[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    // -- Caller --
925
926    #[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        // API key callers without user_id should still evaluate policies
937        assert!(TEST_MANAGE.evaluate(&caller).is_ok());
938    }
939
940    // -- Edge cases --
941
942    #[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        // Internal callers should pass all policies
959        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    // -- View permissions --
989
990    #[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    // -- IsPlatformUser rule --
1031
1032    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(); // is_platform_user = false
1047        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    // -- ResourceConfigResponse serialization --
1058
1059    #[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    // -- Skill permission rules --
1071
1072    #[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        // Malformed patterns rejected by skill name validation
1113        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}