1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
use super::role::RoleValidate;
use anyhow::{anyhow, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;

/// Role Schema Level.
///
/// For example:
///
/// ```yaml
/// - name: role_schema_level
///   type: SCHEMA
///   grants:
///     - CREATE
///     - TEMP
///   schemas:
///     - schema1
///     - schema2
/// ```
///
///  The above example will grant CREATE and TEMP privileges on schema1 and schema2.
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub struct RoleSchemaLevel {
    pub name: String,
    pub grants: Vec<String>,
    pub schemas: Vec<String>,
}

impl RoleSchemaLevel {
    /// Generate role schema to sql.
    ///
    /// ```sql
    /// { GRANT | REVOKE } { { CREATE | USAGE } [,...] | ALL [ PRIVILEGES ] }
    /// ON SCHEMA schema_name [, ...]
    /// TO { username [ WITH GRANT OPTION ] | GROUP group_name | PUBLIC } [, ...]
    /// ```
    pub fn to_sql(&self, user: &str) -> String {
        // grant all privileges if no grants are specified or if grants contains "ALL"
        let grants = if self.grants.is_empty() || self.grants.contains(&"ALL".to_string()) {
            "ALL PRIVILEGES".to_string()
        } else {
            self.grants.join(", ")
        };

        // grant on schemas to user
        let sql = format!(
            "GRANT {} ON SCHEMA {} TO {};",
            grants,
            self.schemas.join(", "),
            user
        );

        sql
    }
}

impl RoleValidate for RoleSchemaLevel {
    fn validate(&self) -> Result<()> {
        if self.name.is_empty() {
            return Err(anyhow!("role name is empty"));
        }

        if self.schemas.is_empty() {
            return Err(anyhow!("role schemas is empty"));
        }

        // Check valid grants: CREATE, USAGE, ALL
        let valid_grants = vec!["CREATE", "USAGE", "ALL"];
        let mut grants = HashSet::new();
        for grant in &self.grants {
            if !valid_grants.contains(&&grant[..]) {
                return Err(anyhow!(
                    "invalid grant: {}, expected: {:?}",
                    grant,
                    valid_grants
                ));
            }
            grants.insert(grant.to_string());
        }

        if self.grants.is_empty() {
            return Err(anyhow!("role grants is empty"));
        }

        Ok(())
    }
}

// Test
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_role_schema_level() {
        let role_schema_level = RoleSchemaLevel {
            name: "role_schema_level".to_string(),
            grants: vec!["CREATE".to_string(), "TEMP".to_string()],
            schemas: vec!["schema1".to_string(), "schema2".to_string()],
        };

        role_schema_level.validate().ok();

        let sql = role_schema_level.to_sql("user");
        assert_eq!(
            sql,
            "GRANT CREATE, TEMP ON SCHEMA schema1, schema2 TO user;"
        );
    }
}