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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
use crate::config::{Config, Role, User as UserInConfig};
use crate::connection::{DbConnection, User};
use ansi_term::Colour::{Green, Purple, Red};
use anyhow::{anyhow, Result};
use ascii_table::AsciiTable;
use log::{error, info};
use std::path::Path;

/// Read the config from the given path and apply it to the database.
/// If the dryrun flag is set, the changes will not be applied.
pub fn apply(target: &Path, dryrun: bool) -> Result<()> {
    let target = target.to_path_buf();

    if target.is_dir() {
        return Err(anyhow!(
            "directory is not supported yet ({})",
            target.display()
        ));
    }

    let config = Config::new(&target)?;

    info!("Applying configuration:\n{}", config);
    let mut conn = DbConnection::new(&config);

    let users_in_db = conn.get_users()?;
    let users_in_config = config.users.clone();

    // Apply users changes (new users, update password)
    create_or_update_users(&mut conn, &users_in_db, &users_in_config, dryrun)?;

    // Apply roles privileges to cluster (database role, schema role, table role)
    create_or_update_privileges(&mut conn, &config, dryrun)?;

    Ok(())
}

/// Apply all config files from the given directory.
pub fn apply_all(target: &Path, dryrun: bool) -> Result<()> {
    let target = target.to_path_buf();

    // Scan recursively for config files (.yaml for .yml) in target directory
    let mut config_files = Vec::new();
    for entry in std::fs::read_dir(target)? {
        let entry = entry?;
        let path = entry.path();
        if path.is_file() {
            let ext = path.extension().unwrap();
            if ext == "yaml" || ext == "yml" {
                config_files.push(path);
            }
        }
    }

    // Apply each config file
    for config_file in config_files {
        info!("Applying configuration from {}", config_file.display());
        apply(&config_file, dryrun)?;
    }

    Ok(())
}

/// Apply users from config to database
///
/// Get list users from database and compare with config users
/// If user is in config but not in database, create it
/// If user is in database but not in config, delete it
/// If user is in both, compare passwords and update if needed
///
/// Show the summary as table of users created, updated, deleted
fn create_or_update_users(
    conn: &mut DbConnection,
    users_in_db: &[User],
    users_in_config: &[UserInConfig],
    dryrun: bool,
) -> Result<()> {
    let mut summary = vec![vec!["User".to_string(), "Action".to_string()]];
    summary.push(vec!["---".to_string(), "---".to_string()]);

    // Create or update users in database
    for user in users_in_config {
        let user_in_db = users_in_db.iter().find(|&u| u.name == user.name);
        match user_in_db {
            // User in config and in database
            Some(user_in_db) => {
                // Update password if `update_password` is set to true
                if user.update_password.unwrap_or(false) {
                    let sql = user.to_sql_update();

                    if dryrun {
                        info!("{}: {}", Purple.paint("Dry-run"), Purple.paint(sql));
                        summary.push(vec![
                            user.name.to_string(),
                            Green.paint("would update password").to_string(),
                        ]);
                    } else {
                        conn.execute(&sql, &[])?;
                        info!("{}: {}", Green.paint("Success"), Purple.paint(sql));
                        summary.push(vec![user.name.clone(), "password updated".to_string()]);
                    }
                } else {
                    // Do nothing if user is not changed
                    summary.push(vec![
                        user_in_db.name.clone(),
                        "no action (already exists)".to_string(),
                    ]);
                }
            }

            // User in config but not in database
            None => {
                let sql = user.to_sql_create();

                if dryrun {
                    info!("{}: {}", Purple.paint("Dry-run"), sql);
                    summary.push(vec![
                        user.name.clone(),
                        format!("would create (dryrun) {}", sql),
                    ]);
                } else {
                    conn.execute(&sql, &[])?;
                    info!("{}: {}", Green.paint("Success"), sql);
                    summary.push(vec![user.name.clone(), format!("created {}", sql)]);
                }
            }
        }
    }

    // TODO: Support delete users in db that are not in config
    for user in users_in_db {
        if !users_in_config.iter().any(|u| u.name == user.name) {
            // Update summary
            summary.push(vec![
                user.name.clone(),
                "no action (not in config)".to_string(),
            ]);
        }
    }

    // Show summary
    print_summary(summary);

    Ok(())
}

/// Render role configuration to SQL and sync with database.
/// If the privileges are not in the database, they will be granted to user.
/// If the privileges are in the database, they will be updated.
/// If the privileges are not in the configuration, they will be revoked from user.
fn create_or_update_privileges(
    conn: &mut DbConnection,
    config: &Config,
    dryrun: bool,
) -> Result<()> {
    let mut summary = vec![vec![
        "User".to_string(),
        "Role Name".to_string(),
        "Detail".to_string(),
        "Status".to_string(),
    ]];
    summary.push(vec![
        "---".to_string(),
        "---".to_string(),
        "---".to_string(),
        "---".to_string(),
    ]);

    // Loop through users in config
    // Get the user Role object by the user.roles[*].name
    // Apply the Role sql privileges to the cluster
    for user in &config.users {
        // Compare privileges on config and db
        // If privileges on config are not in db, add them
        // If privileges on db are not in config, remove them
        for role_name in user.roles.iter() {
            let role = config.roles.iter().find(|&r| r.find(role_name)).unwrap();

            // TODO: revoke if privileges on db are not in configuration

            let sql = role.to_sql(&user.name);

            let mut status = if dryrun {
                "dry-run".to_string()
            } else {
                "updated".to_string()
            };

            if !dryrun {
                let nrows = conn.execute(&sql, &[]).unwrap_or_else(|e| {
                    error!("{}: {}", Red.paint("Error"), sql);
                    error!("  -> {}: {}", Red.paint("Error details"), e);
                    status = "error".to_string();

                    -1
                });

                if nrows > -1 {
                    info!(
                        "{}: {} {}",
                        Green.paint("Success"),
                        Purple.paint(sql),
                        format!("(updated {} row(s))", nrows)
                    );
                }
            } else {
                info!("{}: {}", Purple.paint("Dry-run"), sql);
            }

            let detail = match role {
                Role::Database(role) => format!("database{:?}", role.databases.clone()),
                Role::Schema(role) => format!("schema{:?}", role.schemas.clone()),
                Role::Table(role) => format!("table{:?}", role.tables.clone()),
            };

            // Update summary
            summary.push(vec![
                user.name.clone(),
                role_name.clone(),
                detail.to_string(),
                status.to_string(),
            ]);
        }
    }

    // Show summary
    print_summary(summary);

    Ok(())
}

/// Print summary table
/// TODO: Format the table, detect max size to console
fn print_summary(summary: Vec<Vec<String>>) {
    let ascii_table = AsciiTable::default();

    info!("Summary:\n{}", ascii_table.format(summary));
}