astrid_plugins/
security.rs1use async_trait::async_trait;
10
11#[async_trait]
17pub trait PluginSecurityGate: Send + Sync {
18 async fn check_http_request(
20 &self,
21 plugin_id: &str,
22 method: &str,
23 url: &str,
24 ) -> Result<(), String>;
25
26 async fn check_file_read(&self, plugin_id: &str, path: &str) -> Result<(), String>;
28
29 async fn check_file_write(&self, plugin_id: &str, path: &str) -> Result<(), String>;
31}
32
33#[derive(Debug, Clone, Copy, Default)]
35pub struct AllowAllGate;
36
37#[async_trait]
38impl PluginSecurityGate for AllowAllGate {
39 async fn check_http_request(
40 &self,
41 _plugin_id: &str,
42 _method: &str,
43 _url: &str,
44 ) -> Result<(), String> {
45 Ok(())
46 }
47
48 async fn check_file_read(&self, _plugin_id: &str, _path: &str) -> Result<(), String> {
49 Ok(())
50 }
51
52 async fn check_file_write(&self, _plugin_id: &str, _path: &str) -> Result<(), String> {
53 Ok(())
54 }
55}
56
57#[derive(Debug, Clone, Copy, Default)]
59pub struct DenyAllGate;
60
61#[async_trait]
62impl PluginSecurityGate for DenyAllGate {
63 async fn check_http_request(
64 &self,
65 plugin_id: &str,
66 method: &str,
67 url: &str,
68 ) -> Result<(), String> {
69 Err(format!(
70 "plugin '{plugin_id}' denied: {method} {url} (DenyAllGate)"
71 ))
72 }
73
74 async fn check_file_read(&self, plugin_id: &str, path: &str) -> Result<(), String> {
75 Err(format!(
76 "plugin '{plugin_id}' denied: read {path} (DenyAllGate)"
77 ))
78 }
79
80 async fn check_file_write(&self, plugin_id: &str, path: &str) -> Result<(), String> {
81 Err(format!(
82 "plugin '{plugin_id}' denied: write {path} (DenyAllGate)"
83 ))
84 }
85}
86
87#[cfg(feature = "approval")]
92mod interceptor_gate {
93 use super::{PluginSecurityGate, async_trait};
94 use astrid_approval::action::SensitiveAction;
95 use astrid_approval::interceptor::SecurityInterceptor;
96 use astrid_core::types::Permission;
97 use std::sync::Arc;
98
99 pub struct SecurityInterceptorGate {
105 interceptor: Arc<SecurityInterceptor>,
106 }
107
108 impl SecurityInterceptorGate {
109 #[must_use]
111 pub fn new(interceptor: Arc<SecurityInterceptor>) -> Self {
112 Self { interceptor }
113 }
114 }
115
116 impl std::fmt::Debug for SecurityInterceptorGate {
117 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118 f.debug_struct("SecurityInterceptorGate")
119 .finish_non_exhaustive()
120 }
121 }
122
123 #[async_trait]
124 impl PluginSecurityGate for SecurityInterceptorGate {
125 async fn check_http_request(
126 &self,
127 plugin_id: &str,
128 method: &str,
129 url: &str,
130 ) -> Result<(), String> {
131 let action = SensitiveAction::PluginHttpRequest {
132 plugin_id: plugin_id.to_string(),
133 url: url.to_string(),
134 method: method.to_string(),
135 };
136 self.interceptor
137 .intercept(&action, "plugin host function: HTTP request", None)
138 .await
139 .map(|_| ())
140 .map_err(|e| e.to_string())
141 }
142
143 async fn check_file_read(&self, plugin_id: &str, path: &str) -> Result<(), String> {
144 let action = SensitiveAction::PluginFileAccess {
145 plugin_id: plugin_id.to_string(),
146 path: path.to_string(),
147 mode: Permission::Read,
148 };
149 self.interceptor
150 .intercept(&action, "plugin host function: file read", None)
151 .await
152 .map(|_| ())
153 .map_err(|e| e.to_string())
154 }
155
156 async fn check_file_write(&self, plugin_id: &str, path: &str) -> Result<(), String> {
157 let action = SensitiveAction::PluginFileAccess {
158 plugin_id: plugin_id.to_string(),
159 path: path.to_string(),
160 mode: Permission::Write,
161 };
162 self.interceptor
163 .intercept(&action, "plugin host function: file write", None)
164 .await
165 .map(|_| ())
166 .map_err(|e| e.to_string())
167 }
168 }
169}
170
171#[cfg(feature = "approval")]
172pub use interceptor_gate::SecurityInterceptorGate;
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 #[tokio::test]
179 async fn allow_all_gate_permits_everything() {
180 let gate = AllowAllGate;
181 assert!(
182 gate.check_http_request("p", "GET", "http://x")
183 .await
184 .is_ok()
185 );
186 assert!(gate.check_file_read("p", "/tmp/f").await.is_ok());
187 assert!(gate.check_file_write("p", "/tmp/f").await.is_ok());
188 }
189
190 #[tokio::test]
191 async fn deny_all_gate_rejects_everything() {
192 let gate = DenyAllGate;
193 assert!(
194 gate.check_http_request("p", "GET", "http://x")
195 .await
196 .is_err()
197 );
198 assert!(gate.check_file_read("p", "/tmp/f").await.is_err());
199 assert!(gate.check_file_write("p", "/tmp/f").await.is_err());
200 }
201}