grant/config/
role_schema.rs

1use super::role::RoleValidate;
2use anyhow::{anyhow, Result};
3use serde::{Deserialize, Serialize};
4use std::collections::HashSet;
5
6/// Role Schema Level.
7///
8/// For example:
9///
10/// ```yaml
11/// - name: role_schema_level
12///   type: SCHEMA
13///   grants:
14///     - CREATE
15///     - TEMP
16///   schemas:
17///     - schema1
18///     - schema2
19/// ```
20///
21///  The above example will grant CREATE and TEMP privileges on schema1 and schema2.
22#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
23pub struct RoleSchemaLevel {
24    pub name: String,
25    pub grants: Vec<String>,
26    pub schemas: Vec<String>,
27}
28
29impl RoleSchemaLevel {
30    /// Escape and quote a PostgreSQL identifier to prevent SQL injection
31    fn escape_identifier(ident: &str) -> String {
32        // PostgreSQL identifiers are quoted with double quotes
33        // Escape double quotes by doubling them
34        format!("\"{}\"", ident.replace("\"", "\"\""))
35    }
36
37    /// Generate role schema to sql.
38    ///
39    /// ```sql
40    /// { GRANT | REVOKE } { { CREATE | USAGE } [,...] | ALL [ PRIVILEGES ] }
41    /// ON SCHEMA schema_name [, ...]
42    /// TO { username [ WITH GRANT OPTION ] | GROUP group_name | PUBLIC } [, ...]
43    /// ```
44    pub fn to_sql(&self, user: &str) -> String {
45        // grant all privileges if no grants are specified or if grants contains "ALL"
46        let grants = if self.grants.is_empty() || self.grants.contains(&"ALL".to_string()) {
47            "ALL PRIVILEGES".to_string()
48        } else {
49            self.grants.join(", ")
50        };
51
52        // escape schema and user identifiers to prevent SQL injection
53        let escaped_schemas = self
54            .schemas
55            .iter()
56            .map(|s| Self::escape_identifier(s))
57            .collect::<Vec<_>>()
58            .join(", ");
59        let escaped_user = Self::escape_identifier(user);
60
61        // grant on schemas to user
62        let sql = format!(
63            "GRANT {} ON SCHEMA {} TO {};",
64            grants, escaped_schemas, escaped_user
65        );
66
67        sql
68    }
69}
70
71impl RoleValidate for RoleSchemaLevel {
72    fn validate(&self) -> Result<()> {
73        if self.name.is_empty() {
74            return Err(anyhow!("role name is empty"));
75        }
76
77        if self.schemas.is_empty() {
78            return Err(anyhow!("role schemas is empty"));
79        }
80
81        // Check valid grants: CREATE, USAGE, ALL
82        let valid_grants = vec!["CREATE", "USAGE", "ALL"];
83        let mut grants = HashSet::new();
84        for grant in &self.grants {
85            if !valid_grants.contains(&&grant[..]) {
86                return Err(anyhow!(
87                    "invalid grant: {}, expected: {:?}",
88                    grant,
89                    valid_grants
90                ));
91            }
92            grants.insert(grant.to_string());
93        }
94
95        if self.grants.is_empty() {
96            return Err(anyhow!("role grants is empty"));
97        }
98
99        Ok(())
100    }
101}
102
103// Test
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn test_role_schema_level() {
110        let role_schema_level = RoleSchemaLevel {
111            name: "role_schema_level".to_string(),
112            grants: vec!["CREATE".to_string(), "TEMP".to_string()],
113            schemas: vec!["schema1".to_string(), "schema2".to_string()],
114        };
115
116        role_schema_level.validate().ok();
117
118        let sql = role_schema_level.to_sql("user");
119        assert_eq!(
120            sql,
121            "GRANT CREATE, TEMP ON SCHEMA \"schema1\", \"schema2\" TO \"user\";"
122        );
123    }
124
125    #[test]
126    fn test_sql_injection_prevention() {
127        let role = RoleSchemaLevel {
128            name: "test".to_string(),
129            grants: vec!["CREATE".to_string()],
130            schemas: vec!["schema\"; DROP SCHEMA public; --".to_string()],
131        };
132
133        let sql = role.to_sql("user\"; DROP USER postgres; --");
134        // Verify the injection is properly escaped
135        assert!(sql.contains("\"schema\"\"; DROP SCHEMA public; --\""));
136        assert!(sql.contains("\"user\"\"; DROP USER postgres; --\""));
137    }
138}