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));
}