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
use super::role::RoleValidate;
use anyhow::{anyhow, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;

/// Role Database Level.
///
/// For example:
///
/// ```yaml
/// - name: role_database_level
///   type: database
///   grants:
///     - CREATE
///     - TEMP
///   databases:
///     - db1
///     - db2
/// ```
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub struct RoleDatabaseLevel {
    pub name: String,
    pub grants: Vec<String>,
    pub databases: Vec<String>,
}

impl RoleDatabaseLevel {
    /// Generate role database to SQL.
    ///
    /// ```sql
    /// { GRANT | REVOKE } { { CREATE | TEMPORARY | TEMP } [,...] | ALL [ PRIVILEGES ] }
    /// ON DATABASE db_name [, ...]
    /// TO { username [ WITH GRANT OPTION ] | GROUP group_name | PUBLIC } [, ...]
    /// ```
    pub fn to_sql(&self, user: &str) -> String {
        // grant all if no grants specified or 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 databases to user
        let sql = format!(
            "GRANT {} ON DATABASE {} TO {};",
            grants,
            self.databases.join(", "),
            user
        );

        sql
    }
}

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

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

        // Check valid grants: CREATE, TEMP, TEMPORARY, ALL
        let valid_grants = vec!["CREATE", "TEMP", "TEMPORARY", "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(())
    }
}

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

    #[test]
    fn test_role_database_level() {
        let role = RoleDatabaseLevel {
            name: "role_database_level".to_string(),
            grants: vec!["CREATE".to_string(), "TEMP".to_string()],
            databases: vec!["db1".to_string(), "db2".to_string()],
        };

        assert!(role.validate().is_ok());
        assert_eq!(
            role.to_sql("user"),
            "GRANT CREATE, TEMP ON DATABASE db1, db2 TO user;"
        );
    }
}