Skip to main content

tl_errors/
security.rs

1// ThinkingLanguage — Security Policy
2// Licensed under Apache-2.0
3//
4// Phase 23: Connector permissions, path restrictions, sandbox mode.
5// Phase C3: Moved from tl-compiler to tl-errors for shared access.
6
7use std::collections::HashSet;
8
9/// Security policy controlling access to files, network, and connectors.
10#[derive(Debug, Clone)]
11pub struct SecurityPolicy {
12    pub allowed_connectors: HashSet<String>,
13    pub denied_paths: Vec<String>,
14    pub allow_network: bool,
15    pub allow_file_read: bool,
16    pub allow_file_write: bool,
17    pub sandbox_mode: bool,
18}
19
20impl SecurityPolicy {
21    pub fn permissive() -> Self {
22        SecurityPolicy {
23            allowed_connectors: HashSet::new(),
24            denied_paths: Vec::new(),
25            allow_network: true,
26            allow_file_read: true,
27            allow_file_write: true,
28            sandbox_mode: false,
29        }
30    }
31
32    pub fn sandbox() -> Self {
33        SecurityPolicy {
34            allowed_connectors: HashSet::new(),
35            denied_paths: Vec::new(),
36            allow_network: false,
37            allow_file_read: true,
38            allow_file_write: false,
39            sandbox_mode: true,
40        }
41    }
42
43    /// Check if a permission is allowed.
44    pub fn check(&self, permission: &str) -> bool {
45        if !self.sandbox_mode {
46            return true;
47        }
48        match permission {
49            "network" => self.allow_network,
50            "file_read" => self.allow_file_read,
51            "file_write" => self.allow_file_write,
52            "python" => false,
53            "env_write" => false,
54            p if p.starts_with("connector:") => {
55                let conn_type = &p["connector:".len()..];
56                self.allowed_connectors.is_empty() || self.allowed_connectors.contains(conn_type)
57            }
58            _ => true,
59        }
60    }
61
62    /// Check if a file path is allowed.
63    pub fn check_path(&self, path: &str) -> bool {
64        if !self.sandbox_mode {
65            return true;
66        }
67        !self
68            .denied_paths
69            .iter()
70            .any(|denied| path.starts_with(denied))
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn test_permissive_allows_all() {
80        let policy = SecurityPolicy::permissive();
81        assert!(policy.check("network"));
82        assert!(policy.check("file_read"));
83        assert!(policy.check("file_write"));
84        assert!(policy.check("connector:postgres"));
85    }
86
87    #[test]
88    fn test_sandbox_restricts() {
89        let policy = SecurityPolicy::sandbox();
90        assert!(!policy.check("network"));
91        assert!(policy.check("file_read"));
92        assert!(!policy.check("file_write"));
93    }
94
95    #[test]
96    fn test_connector_whitelist() {
97        let mut policy = SecurityPolicy::sandbox();
98        policy.allowed_connectors.insert("postgres".to_string());
99        assert!(policy.check("connector:postgres"));
100        assert!(!policy.check("connector:mysql"));
101    }
102
103    #[test]
104    fn test_denied_paths() {
105        let mut policy = SecurityPolicy::sandbox();
106        policy.denied_paths.push("/etc/".to_string());
107        assert!(!policy.check_path("/etc/passwd"));
108        assert!(policy.check_path("/home/user/file.txt"));
109    }
110}