Skip to main content

weft_core/package/
permissions.rs

1use crate::package::config::PackagePermissions;
2use anyhow::{bail, Result};
3
4/// Enumeration of all permission categories.
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum Permission {
7    Process,
8    Network,
9    Auth,
10    Storage,
11    Pipeline,
12    Routes,
13    Events,
14    Log,
15    Config,
16    Scheduler,
17    Ui,
18}
19
20impl std::fmt::Display for Permission {
21    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22        match self {
23            Self::Process => write!(f, "process"),
24            Self::Network => write!(f, "network"),
25            Self::Auth => write!(f, "auth"),
26            Self::Storage => write!(f, "storage"),
27            Self::Pipeline => write!(f, "pipeline"),
28            Self::Routes => write!(f, "routes"),
29            Self::Events => write!(f, "events"),
30            Self::Log => write!(f, "log"),
31            Self::Config => write!(f, "config"),
32            Self::Scheduler => write!(f, "scheduler"),
33            Self::Ui => write!(f, "ui"),
34        }
35    }
36}
37
38/// Checks whether a package has a given permission.
39pub struct PermissionChecker {
40    package_name: String,
41    permissions: PackagePermissions,
42}
43
44impl PermissionChecker {
45    pub fn new(package_name: &str, permissions: PackagePermissions) -> Self {
46        Self {
47            package_name: package_name.to_string(),
48            permissions,
49        }
50    }
51
52    /// Returns Ok(()) if allowed, Err with descriptive message if denied.
53    pub fn check(&self, perm: Permission) -> Result<()> {
54        let allowed = match perm {
55            Permission::Process => self.permissions.process,
56            Permission::Network => self.permissions.network,
57            Permission::Auth => self.permissions.auth,
58            Permission::Storage => self.permissions.storage,
59            Permission::Pipeline => self.permissions.pipeline,
60            Permission::Routes => self.permissions.routes,
61            Permission::Events => self.permissions.events,
62            Permission::Log => self.permissions.log,
63            Permission::Config => self.permissions.config,
64            Permission::Scheduler => self.permissions.scheduler,
65            Permission::Ui => self.permissions.ui,
66        };
67
68        if allowed {
69            Ok(())
70        } else {
71            bail!(
72                "Package '{}' does not have '{}' permission. Declare it in package.toml [permissions].",
73                self.package_name,
74                perm
75            )
76        }
77    }
78}
79
80impl Permission {
81    /// All permission variants, for exhaustive iteration (e.g. escalation diff).
82    pub const ALL: [Permission; 11] = [
83        Permission::Process,
84        Permission::Network,
85        Permission::Auth,
86        Permission::Storage,
87        Permission::Pipeline,
88        Permission::Routes,
89        Permission::Events,
90        Permission::Log,
91        Permission::Config,
92        Permission::Scheduler,
93        Permission::Ui,
94    ];
95}
96
97/// Returns the permissions present in `new` but absent in `old` — i.e. the
98/// escalation a package upgrade would introduce (B4). Empty means the upgrade
99/// requests no new capabilities and is safe to apply without re-approval.
100pub fn escalated_permissions(
101    old: &PackagePermissions,
102    new: &PackagePermissions,
103) -> Vec<Permission> {
104    let granted = |perms: &PackagePermissions, perm: Permission| -> bool {
105        let checker = PermissionChecker::new("", perms.clone());
106        checker.check(perm).is_ok()
107    };
108    Permission::ALL
109        .into_iter()
110        .filter(|&perm| !granted(old, perm) && granted(new, perm))
111        .collect()
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[test]
119    fn test_check_allowed() {
120        let perms = PackagePermissions {
121            process: true,
122            network: true,
123            ..Default::default()
124        };
125        let checker = PermissionChecker::new("test-package", perms);
126        assert!(checker.check(Permission::Process).is_ok());
127        assert!(checker.check(Permission::Network).is_ok());
128    }
129
130    #[test]
131    fn test_check_denied() {
132        let perms = PackagePermissions::default();
133        let checker = PermissionChecker::new("test-package", perms);
134        let err = checker.check(Permission::Process).unwrap_err();
135        assert!(err.to_string().contains("test-package"));
136        assert!(err.to_string().contains("process"));
137    }
138
139    #[test]
140    fn no_escalation_when_permissions_unchanged() {
141        let p = PackagePermissions {
142            storage: true,
143            network: true,
144            ..Default::default()
145        };
146        assert!(escalated_permissions(&p, &p).is_empty());
147    }
148
149    #[test]
150    fn detects_added_permissions() {
151        let old = PackagePermissions {
152            storage: true,
153            ..Default::default()
154        };
155        let new = PackagePermissions {
156            storage: true,
157            process: true, // newly requested
158            network: true, // newly requested
159            ..Default::default()
160        };
161        let esc = escalated_permissions(&old, &new);
162        assert_eq!(esc.len(), 2);
163        assert!(esc.contains(&Permission::Process));
164        assert!(esc.contains(&Permission::Network));
165    }
166
167    #[test]
168    fn dropping_permissions_is_not_escalation() {
169        let old = PackagePermissions {
170            storage: true,
171            process: true,
172            ..Default::default()
173        };
174        let new = PackagePermissions {
175            storage: true,
176            ..Default::default()
177        };
178        assert!(escalated_permissions(&old, &new).is_empty());
179    }
180}