opendev_runtime/permissions/
mod.rs1mod glob;
7
8pub use glob::{glob_matches, glob_matches_path};
9
10use serde::{Deserialize, Serialize};
11use std::path::Path;
12
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(rename_all = "snake_case")]
16pub enum PermissionAction {
17 Allow,
19 Deny,
21 Prompt,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct PermissionRule {
28 pub pattern: String,
30 pub action: PermissionAction,
32 pub priority: i32,
34 #[serde(skip_serializing_if = "Option::is_none")]
37 pub directory_scope: Option<String>,
38}
39
40#[derive(Debug, Clone, Default, Serialize, Deserialize)]
42pub struct PermissionRuleSet {
43 rules: Vec<PermissionRule>,
44}
45
46pub fn is_sensitive_file(path: &str) -> bool {
50 let filename = path.rsplit('/').next().unwrap_or(path);
51 let lower = filename.to_lowercase();
52
53 if lower == ".env" {
55 return true;
56 }
57 if let Some(suffix) = lower.strip_prefix(".env.") {
58 return !matches!(suffix, "example" | "sample" | "template");
59 }
60
61 matches!(
63 lower.as_str(),
64 "credentials.json"
65 | "service-account.json"
66 | "id_rsa"
67 | "id_ed25519"
68 | ".npmrc"
69 | ".pypirc"
70 )
71}
72
73impl PermissionRuleSet {
74 pub fn new() -> Self {
76 Self { rules: Vec::new() }
77 }
78
79 pub fn with_defaults() -> Self {
84 let mut rs = Self::new();
85
86 rs.add_rule(PermissionRule {
88 pattern: "read_file:*.env".into(),
89 action: PermissionAction::Deny,
90 priority: 1000,
91 directory_scope: None,
92 });
93 rs.add_rule(PermissionRule {
94 pattern: "read_file:*.env.*".into(),
95 action: PermissionAction::Deny,
96 priority: 1000,
97 directory_scope: None,
98 });
99 rs.add_rule(PermissionRule {
101 pattern: "read_file:*.env.example".into(),
102 action: PermissionAction::Allow,
103 priority: 1001,
104 directory_scope: None,
105 });
106 rs.add_rule(PermissionRule {
107 pattern: "read_file:*.env.sample".into(),
108 action: PermissionAction::Allow,
109 priority: 1001,
110 directory_scope: None,
111 });
112 rs.add_rule(PermissionRule {
113 pattern: "read_file:*.env.template".into(),
114 action: PermissionAction::Allow,
115 priority: 1001,
116 directory_scope: None,
117 });
118
119 rs.add_rule(PermissionRule {
121 pattern: "edit_file:*.env".into(),
122 action: PermissionAction::Deny,
123 priority: 1000,
124 directory_scope: None,
125 });
126 rs.add_rule(PermissionRule {
127 pattern: "edit_file:*.env.*".into(),
128 action: PermissionAction::Deny,
129 priority: 1000,
130 directory_scope: None,
131 });
132 rs.add_rule(PermissionRule {
133 pattern: "write_file:*.env".into(),
134 action: PermissionAction::Deny,
135 priority: 1000,
136 directory_scope: None,
137 });
138 rs.add_rule(PermissionRule {
139 pattern: "write_file:*.env.*".into(),
140 action: PermissionAction::Deny,
141 priority: 1000,
142 directory_scope: None,
143 });
144
145 rs
146 }
147
148 pub fn add_rule(&mut self, rule: PermissionRule) {
150 self.rules.push(rule);
151 }
152
153 pub fn remove_rules<F: Fn(&PermissionRule) -> bool>(&mut self, predicate: F) {
155 self.rules.retain(|r| !predicate(r));
156 }
157
158 pub fn rules(&self) -> &[PermissionRule] {
160 &self.rules
161 }
162
163 pub fn evaluate(
172 &self,
173 tool_name: &str,
174 args: &str,
175 working_dir: Option<&Path>,
176 ) -> Option<PermissionAction> {
177 let input = format!("{tool_name}:{args}");
178
179 let mut sorted: Vec<&PermissionRule> = self.rules.iter().collect();
180 sorted.sort_by(|a, b| b.priority.cmp(&a.priority));
181
182 for rule in sorted {
183 if let Some(ref scope) = rule.directory_scope {
185 match working_dir {
186 Some(dir) => {
187 if !glob_matches_path(scope, &dir.to_string_lossy()) {
188 continue;
189 }
190 }
191 None => continue, }
193 }
194
195 if glob_matches(&rule.pattern, &input) {
196 return Some(rule.action.clone());
197 }
198 }
199
200 None
201 }
202
203 pub fn evaluate_simple(&self, tool_name: &str, args: &str) -> Option<PermissionAction> {
205 self.evaluate(tool_name, args, None)
206 }
207}
208
209#[cfg(test)]
210mod tests;