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    /// View apps (read-only)
30    OrgAppsView,
31    /// CRUD on apps
32    OrgAppsManage,
33    /// Dangerous app operations
34    OrgAppsDangerous,
35    /// View MCP servers (read-only)
36    OrgMcpServersView,
37    /// CRUD on MCP servers
38    OrgMcpServersManage,
39    /// Dangerous MCP server operations
40    OrgMcpServersDangerous,
41    /// View skills (read-only)
42    OrgSkillsView,
43    /// CRUD on skills
44    OrgSkillsManage,
45    /// Dangerous skill operations
46    OrgSkillsDangerous,
47    /// View capabilities (read-only)
48    OrgCapabilitiesView,
49    /// CRUD on capabilities
50    OrgCapabilitiesManage,
51    /// View agent identities (read-only)
52    OrgAgentIdentitiesView,
53    /// CRUD on agent identities
54    OrgAgentIdentitiesManage,
55    /// View marketplaces and installed plugins (read-only)
56    OrgPluginsView,
57    /// Register marketplaces and install/uninstall plugins (admin-gated per specs/plugins.md)
58    OrgPluginsManage,
59    /// CRUD on sessions
60    OrgSessionsManage,
61    /// View LLM providers (read-only)
62    OrgProvidersView,
63    /// CRUD on LLM providers
64    OrgProvidersManage,
65    /// View organization settings (read-only)
66    OrgSettingsView,
67    /// Organization settings
68    OrgSettingsManage,
69    /// View members (read-only)
70    OrgMembersView,
71    /// Invite/remove members
72    OrgMembersManage,
73    /// CRUD on API keys
74    OrgApiKeysManage,
75    /// View audit logs (read-only)
76    OrgAuditLogsView,
77    /// View semantic reports (read-only)
78    OrgReportsView,
79    /// Manage saved reports and reporting definitions
80    OrgReportsManage,
81    /// Run reporting administrative operations
82    OrgReportsAdmin,
83}
84
85impl Permission {
86    /// String identifier for this permission.
87    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    /// All defined permissions.
125    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
167// ============================================================================
168// Role → Permission mapping
169// ============================================================================
170
171/// Permissions granted to Owner role.
172const 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
207/// Permissions granted to Admin role.
208const ADMIN_PERMISSIONS: &[Permission] = &[
209    Permission::OrgHarnessesView,
210    Permission::OrgHarnessesManage,
211    Permission::OrgAgentsManage,
212    // Per-domain VIEW+MANAGE previously gated by OrgAgentsManage (EVE-656).
213    // Admin had OrgAgentsManage, so it keeps equivalent access to all six domains.
214    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
239/// Permissions granted to Member role.
240/// Members can view all resources but only manage agents and sessions.
241const MEMBER_PERMISSIONS: &[Permission] = &[
242    Permission::OrgHarnessesView,
243    Permission::OrgAgentsManage,
244    // Per-domain VIEW+MANAGE previously gated by OrgAgentsManage (EVE-656).
245    // Member had OrgAgentsManage, so it keeps equivalent access to all six domains.
246    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    // Members can view plugins but NOT manage them: PLUGIN_MANAGE has always
257    // required OrgPluginsManage, which Members never held (Admin+ only). (EVE-656)
258    Permission::OrgPluginsView,
259    Permission::OrgSessionsManage,
260    Permission::OrgProvidersView,
261    Permission::OrgSettingsView,
262    Permission::OrgMembersView,
263    Permission::OrgReportsView,
264];
265
266/// Check whether a role grants a specific permission.
267pub 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
276/// List all permissions granted to a role.
277pub 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
285/// Contract for resolving which permissions a caller has.
286///
287/// `DefaultPermissionResolver` preserves the OSS role-based mapping, while downstream
288/// consumers can implement this trait to inject billing-tier rules, database-backed
289/// grants, or external RBAC systems without patching policy evaluation itself.
290///
291/// # Contract
292///
293/// Implementations must:
294///
295/// - Fail closed. Return `true` from `has_permission()` only when the caller is
296///   actually authorized.
297/// - Be consistent. If `has_permission(caller, permission)` returns `true`, then
298///   `caller_permissions(caller)` must include that same permission.
299/// - Be safe to call from async request paths (`Send + Sync`).
300///
301/// # Example
302///
303/// ```no_run
304/// use everruns_core::{Caller, Permission, PermissionResolver};
305///
306/// struct TierAwareResolver;
307///
308/// impl PermissionResolver for TierAwareResolver {
309///     fn has_permission(&self, caller: &Caller, permission: &Permission) -> bool {
310///         caller.role == everruns_core::organization::OrgRole::Owner
311///             && permission == &Permission::OrgHarnessesDangerous
312///     }
313///
314///     fn caller_permissions(&self, caller: &Caller) -> Vec<Permission> {
315///         Permission::ALL
316///             .iter()
317///             .copied()
318///             .filter(|permission| self.has_permission(caller, permission))
319///             .collect()
320///     }
321/// }
322/// ```
323pub trait PermissionResolver: Send + Sync {
324    /// Return whether the caller currently has `permission`.
325    ///
326    /// This method is used by `Policy::evaluate_with()` for
327    /// `Rule::UserHasPermission` checks.
328    fn has_permission(&self, caller: &Caller, permission: &Permission) -> bool;
329
330    /// Return the full set of permissions currently granted to `caller`.
331    ///
332    /// Config endpoints and downstream policy-reporting code use this to expose
333    /// evaluated capabilities to a UI. Prefer deterministic ordering so callers
334    /// can compare results reliably.
335    fn caller_permissions(&self, caller: &Caller) -> Vec<Permission>;
336}
337
338/// Default `PermissionResolver` backed by the built-in role-to-permission map.
339///
340/// This preserves the existing OSS behavior by delegating to
341/// `role_has_permission()` and `role_permissions()`.
342///
343/// # Safety
344///
345/// This resolver is stateless and can be shared freely across threads.
346#[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// ============================================================================
360// Rule
361// ============================================================================
362
363/// A single predicate evaluated against a Caller context.
364/// All rules in a Policy must pass (AND logic).
365#[derive(Debug, Clone)]
366pub enum Rule {
367    /// Caller's role must grant this permission.
368    UserHasPermission(Permission),
369    /// Caller must have at least this OrgRole level.
370    UserHasRole(OrgRole),
371    /// Caller must be a platform user (allowlisted email).
372    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// ============================================================================
386// Policy
387// ============================================================================
388
389/// A named set of rules that must all pass for access.
390///
391/// Policies are defined as `const` values and attached to service methods
392/// via the `#[policy]` macro.
393#[derive(Debug, Clone)]
394pub struct Policy {
395    /// Unique identifier for this policy (e.g. "harness.manage").
396    pub id: &'static str,
397    /// Rules that must all pass. AND logic.
398    pub rules: &'static [Rule],
399}
400
401impl Policy {
402    /// Evaluate all rules against the caller. Returns `Ok(())` if all pass,
403    /// or `Err(PolicyError)` on the first failing rule.
404    pub fn evaluate(&self, caller: &Caller) -> Result<(), PolicyError> {
405        let resolver = DefaultPermissionResolver;
406        self.evaluate_with(&resolver, caller)
407    }
408
409    /// Evaluate all rules against the caller using a custom permission resolver.
410    ///
411    /// `Rule::UserHasPermission` checks are delegated to `resolver`, while other
412    /// rule types keep their current built-in behavior.
413    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// ============================================================================
442// Caller
443// ============================================================================
444
445/// Auth context passed from API handler to service layer.
446///
447/// Replaces raw `org_id: i64` parameter on service methods. Carries all
448/// information needed for policy evaluation.
449#[derive(Debug, Clone)]
450pub struct Caller {
451    /// Internal organization ID (for database queries).
452    pub org_id: i64,
453    /// External organization public ID.
454    pub org_public_id: String,
455    /// Authenticated user ID (`None` for API key auth without user context).
456    pub user_id: Option<Uuid>,
457    /// User's role in the organization.
458    pub role: OrgRole,
459    /// Whether the caller is a platform user (email allowlist).
460    pub is_platform_user: bool,
461    /// Whether the caller originates from an internal server path.
462    pub is_internal: bool,
463}
464
465impl Caller {
466    /// Create an internal/platform caller with Owner role.
467    ///
468    /// Used for gRPC service calls (worker ↔ server) and other internal
469    /// operations that should bypass all policy checks.
470    ///
471    /// THREAT[TM-AUTHZ-002]: This constructs an Owner / `is_internal` caller
472    /// that bypasses ALL policy evaluation for the given `org_id`. The worker
473    /// control plane builds it from the CLIENT-SUPPLIED `req.org_id` on every
474    /// RPC, so a single valid worker bearer token can act on ANY org — there is
475    /// no per-org token scoping. The full tenant-isolation guarantee on the
476    /// worker boundary therefore rests on (1) network isolation of that
477    /// boundary (it MUST never be reachable from untrusted networks) and (2)
478    /// the shared worker secret, which is constant-time compared in the gRPC
479    /// auth interceptor. Only ever call this from trusted gRPC/internal paths.
480    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// ============================================================================
493// PolicyError
494// ============================================================================
495
496/// Error returned when a policy evaluation fails.
497#[derive(Debug, Clone)]
498pub struct PolicyError {
499    /// Which policy failed.
500    pub policy_id: String,
501    /// Human-readable message.
502    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
525// ============================================================================
526// Policy evaluation helpers (for config endpoints)
527// ============================================================================
528
529/// Evaluate multiple policies against a caller and return a map of results.
530/// Used by `/config` endpoints to expose policy results to the UI.
531pub fn evaluate_policies(caller: &Caller, policies: &[&Policy]) -> HashMap<String, bool> {
532    let resolver = DefaultPermissionResolver;
533    evaluate_policies_with(&resolver, caller, policies)
534}
535
536/// Evaluate multiple policies using a custom permission resolver.
537///
538/// This is the config-endpoint companion to `Policy::evaluate_with()`.
539pub 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/// Response type for per-resource config endpoints.
556///
557/// Every resource exposes `GET /v1/{resource}/config` returning this type.
558/// UI uses it to gate controls (create/edit/delete buttons, admin panels).
559#[derive(Debug, Clone, Serialize)]
560#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
561pub struct ResourceConfigResponse {
562    /// Map of policy ID → whether the caller satisfies it.
563    pub policies: HashMap<String, bool>,
564}
565
566/// Backwards compat alias — prefer `ResourceConfigResponse`.
567pub type PolicyConfigResponse = ResourceConfigResponse;
568
569// ============================================================================
570// Skill-scoped permission rules
571// ============================================================================
572//
573// Decision: Skill permissions use a separate ACL system from org permissions.
574// Rules are parsed from strings like "allow Skill(commit)" or "deny Skill".
575// Specificity-based precedence: exact > wildcard > all; deny wins at same level.
576// See specs/permissions.md and EVE-140 for design.
577
578/// Action for a skill permission rule.
579#[derive(Debug, Clone, Copy, PartialEq, Eq)]
580pub enum SkillPermissionAction {
581    Allow,
582    Deny,
583}
584
585/// Pattern for matching skill invocations.
586#[derive(Debug, Clone, PartialEq, Eq)]
587pub enum SkillPermissionPattern {
588    /// Matches any skill invocation (`Skill`)
589    All,
590    /// Exact skill name match (`Skill(name)`)
591    ExactName(String),
592    /// Skill name with any args (`Skill(name *)`)
593    NameWildcard(String),
594}
595
596impl SkillPermissionPattern {
597    /// Specificity for precedence ordering. Higher = more specific.
598    fn specificity(&self) -> u8 {
599        match self {
600            SkillPermissionPattern::All => 0,
601            SkillPermissionPattern::NameWildcard(_) => 1,
602            SkillPermissionPattern::ExactName(_) => 2,
603        }
604    }
605
606    /// Check if this pattern matches a skill name.
607    ///
608    /// Note: `NameWildcard` and `ExactName` both match by skill name only.
609    /// The distinction exists for specificity-based precedence: `ExactName`
610    /// (specificity 2) overrides `NameWildcard` (specificity 1). When
611    /// argument-level matching is added, `NameWildcard` will match any
612    /// invocation args while `ExactName` will match only no-arg invocations.
613    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/// A single skill permission rule.
623#[derive(Debug, Clone, PartialEq, Eq)]
624pub struct SkillPermissionRule {
625    pub action: SkillPermissionAction,
626    pub pattern: SkillPermissionPattern,
627}
628
629/// Parse a skill permission rule from a string.
630///
631/// Supported formats:
632/// - `allow Skill` / `deny Skill` — match all skills
633/// - `allow Skill(name)` / `deny Skill(name)` — exact name
634/// - `allow Skill(name *)` / `deny Skill(name *)` — name with any args
635pub 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    // Parse pattern
646    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        // Check for wildcard: "name *" (exactly one space then asterisk)
663        let (name, is_wildcard) = if let Some(name) = inner.strip_suffix(" *") {
664            (name, true)
665        } else {
666            (inner, false)
667        };
668
669        // Validate skill name using the canonical validator
670        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
692/// Check whether a skill is allowed by the given rules.
693///
694/// Returns `true` if allowed, `false` if denied.
695/// If no rules match, defaults to allowed.
696///
697/// Precedence: higher specificity wins. At same specificity, deny wins.
698pub fn check_skill_permission(rules: &[SkillPermissionRule], skill_name: &str) -> bool {
699    let mut best_specificity: Option<u8> = None;
700    let mut best_allowed = true; // default: allow
701
702    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            // Same specificity: deny wins
719            Some(best) if spec == best && !is_allow => {
720                best_allowed = false;
721            }
722            _ => {} // lower specificity, ignore
723        }
724    }
725
726    best_allowed
727}
728
729// ============================================================================
730// Tests
731// ============================================================================
732
733#[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    // -- role_has_permission tests --
772
773    #[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    // -- EVE-656: per-domain split of OrgAgentsManage preserves behavior --
829
830    /// Per-domain VIEW+MANAGE permissions introduced in EVE-656. Every role that
831    /// previously held OrgAgentsManage (Owner, Admin, Member) gated these six
832    /// domains through it, so each role must still hold the equivalent per-domain
833    /// VIEW+MANAGE permissions. Plugins manage is excluded: it has always also
834    /// required OrgPluginsManage (Admin+ only) and is asserted separately below.
835    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        // Owner, Admin, Member all held OrgAgentsManage and could therefore
852        // view+manage all six split domains (plugins manage excepted).
853        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        // PLUGIN_MANAGE required OrgPluginsManage (Admin+ only), so Members must
867        // still be unable to manage plugins after the split.
868        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    // -- Policy evaluation tests --
883
884    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    // -- evaluate_policies (config endpoint helper) tests --
936
937    #[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        // Validates the pattern used in AuthState: Arc<dyn PermissionResolver>
1020        let resolver: Arc<dyn PermissionResolver> = Arc::new(DenyManageResolver);
1021        let caller = owner_caller();
1022
1023        // Policy::evaluate_with accepts &dyn PermissionResolver
1024        assert!(
1025            TEST_MANAGE
1026                .evaluate_with(resolver.as_ref(), &caller)
1027                .is_err()
1028        );
1029
1030        // evaluate_policies_with accepts &dyn PermissionResolver
1031        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        // Default resolver works the same way
1036        let default: Arc<dyn PermissionResolver> = Arc::new(DefaultPermissionResolver);
1037        assert!(TEST_MANAGE.evaluate_with(default.as_ref(), &caller).is_ok());
1038    }
1039
1040    // -- Permission display --
1041
1042    #[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    // -- role_permissions --
1056
1057    #[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    // -- Caller --
1077
1078    #[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        // API key callers without user_id should still evaluate policies
1089        assert!(TEST_MANAGE.evaluate(&caller).is_ok());
1090    }
1091
1092    // -- Edge cases --
1093
1094    #[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        // Internal callers should pass all policies
1111        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    // -- View permissions --
1141
1142    #[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    // -- IsPlatformUser rule --
1183
1184    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(); // is_platform_user = false
1199        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    // -- ResourceConfigResponse serialization --
1210
1211    #[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    // -- Skill permission rules --
1223
1224    #[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        // Malformed patterns rejected by skill name validation
1265        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}