From e4b26a98022e34f0fbf5c3c3b3263968ff69a4a4 Mon Sep 17 00:00:00 2001 From: Duyet Le Date: Thu, 20 Jan 2022 11:39:57 +0700 Subject: [PATCH 1/2] feat: support flag users[*].update_password to force update password --- Cargo.toml | 2 +- examples/example.yaml | 1 + src/apply.rs | 55 ++++++++++++++++++++++++------------- src/cli.rs | 2 +- src/config/connection.rs | 8 ++---- src/config/role_database.rs | 11 +++++--- src/config/role_schema.rs | 10 +++++-- src/config/role_table.rs | 11 +++++--- src/config/user.rs | 36 ++++++++++++++++++++++-- src/gen.rs | 7 ++++- tests/cli-apply.rs | 2 +- 11 files changed, 104 insertions(+), 41 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 602902a..dd78a29 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "grant" -version = "0.0.1-beta.3" +version = "0.0.1-beta.4" edition = "2021" authors = ["Duyet Le "] license = "MIT" diff --git a/examples/example.yaml b/examples/example.yaml index 852c63d..8f8f7a8 100644 --- a/examples/example.yaml +++ b/examples/example.yaml @@ -41,6 +41,7 @@ users: - role_table_level - name: duyet2 password: 1234567890 + update_password: true roles: - role_database_level - role_schema_level diff --git a/src/apply.rs b/src/apply.rs index ff0f4c0..2bdfd6b 100644 --- a/src/apply.rs +++ b/src/apply.rs @@ -10,7 +10,10 @@ use std::path::PathBuf; /// If the dryrun flag is set, the changes will not be applied. pub fn apply(target: &PathBuf, dryrun: bool) -> Result<()> { if target.is_dir() { - return Err(anyhow!("The target is a directory")); + return Err(anyhow!( + "directory is not supported yet ({})", + target.display() + )); } let config = Config::new(&target)?; @@ -22,10 +25,10 @@ pub fn apply(target: &PathBuf, dryrun: bool) -> Result<()> { let users_in_config = config.users.clone(); // Apply users changes (new users, update password) - apply_users(&mut conn, &users_in_db, &users_in_config, dryrun)?; + create_or_update_users(&mut conn, &users_in_db, &users_in_config, dryrun)?; // Apply roles privileges to cluster (database role, schema role, table role) - apply_privileges(&mut conn, &config, dryrun)?; + create_or_update_privileges(&mut conn, &config, dryrun)?; Ok(()) } @@ -62,7 +65,7 @@ pub fn apply_all(target: &PathBuf, dryrun: bool) -> Result<()> { /// If user is in both, compare passwords and update if needed /// /// Show the summary as table of users created, updated, deleted -fn apply_users( +fn create_or_update_users( conn: &mut DbConnection, users_in_db: &[User], users_in_config: &[UserInConfig], @@ -77,13 +80,28 @@ fn apply_users( match user_in_db { // User in config and in database Some(user_in_db) => { - // TODO: Update password if needed, currently we can't compare the password - - // Do nothing if user is not changed - summary.push(vec![ - user_in_db.name.clone(), - "no action (already exists)".to_string(), - ]); + // 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![ + format!("{}", user.name), + format!("{}", Green.paint("would update password")), + ]); + } 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 @@ -91,17 +109,16 @@ fn apply_users( 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)]); } - - // Update summary - summary.push(vec![user.name.clone(), "created".to_string()]); } } } @@ -127,7 +144,11 @@ fn apply_users( /// 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 apply_privileges(conn: &mut DbConnection, config: &Config, dryrun: bool) -> Result<()> { +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(), @@ -141,10 +162,6 @@ fn apply_privileges(conn: &mut DbConnection, config: &Config, dryrun: bool) -> R "---".to_string(), ]); - // let database_privileges = conn.get_user_database_privileges()?; - // let schema_privileges = conn.get_user_schema_privileges()?; - // let table_privileges = conn.get_user_table_privileges()?; - // Loop through users in config // Get the user Role object by the user.roles[*].name // Apply the Role sql privileges to the cluster diff --git a/src/cli.rs b/src/cli.rs index 5cee8e7..ef39c7e 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -39,7 +39,7 @@ pub enum Command { /// Apply a configuration to a redshift by file name. /// Yaml format are accepted. Apply { - /// The path to the file to read + /// The path to the file to read, directory is not supported yet. #[structopt(short, long, parse(from_os_str))] file: PathBuf, diff --git a/src/config/connection.rs b/src/config/connection.rs index 1cce500..0989143 100644 --- a/src/config/connection.rs +++ b/src/config/connection.rs @@ -35,9 +35,8 @@ impl Connection { } } - // xpaned environtment variables in the `url` field. - // Expand environment variables in the `url` field. - // For example: postgres://user:${PASSWORD}@host:port/database + /// Expand environment variables in the `url` field. + /// For example: postgres://user:${PASSWORD}@host:port/database pub fn expand_env_vars(&self) -> Result { let mut connection = self.clone(); @@ -61,7 +60,7 @@ impl Connection { } } -// Implement default values for connection type and url. +/// Implement default values for connection type and url. impl Default for Connection { fn default() -> Self { Self { @@ -71,7 +70,6 @@ impl Default for Connection { } } -// Test Connection. #[cfg(test)] mod tests { use super::*; diff --git a/src/config/role_database.rs b/src/config/role_database.rs index 1ad18cc..21fa631 100644 --- a/src/config/role_database.rs +++ b/src/config/role_database.rs @@ -25,9 +25,13 @@ pub struct RoleDatabaseLevel { } impl RoleDatabaseLevel { - // { GRANT | REVOKE } { { CREATE | TEMPORARY | TEMP } [,...] | ALL [ PRIVILEGES ] } - // ON DATABASE db_name [, ...] - // TO { username [ WITH GRANT OPTION ] | GROUP group_name | PUBLIC } [, ...] + /// 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()) { @@ -80,7 +84,6 @@ impl RoleValidate for RoleDatabaseLevel { } } -// Test #[cfg(test)] mod tests { use super::*; diff --git a/src/config/role_schema.rs b/src/config/role_schema.rs index 3f5405c..0bac3e9 100644 --- a/src/config/role_schema.rs +++ b/src/config/role_schema.rs @@ -27,9 +27,13 @@ pub struct RoleSchemaLevel { } impl RoleSchemaLevel { - // { GRANT | REVOKE } { { CREATE | USAGE } [,...] | ALL [ PRIVILEGES ] } - // ON SCHEMA schema_name [, ...] - // TO { username [ WITH GRANT OPTION ] | GROUP group_name | PUBLIC } [, ...] + /// 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()) { diff --git a/src/config/role_table.rs b/src/config/role_table.rs index f4e1f18..8031cae 100644 --- a/src/config/role_table.rs +++ b/src/config/role_table.rs @@ -55,9 +55,13 @@ impl Table { } impl RoleTableLevel { - // {GRANT | REVOKE} { { SELECT | INSERT | UPDATE | DELETE | DROP | REFERENCES } [,...] | ALL [ PRIVILEGES ] } - // ON { [ TABLE ] table_name [, ...] | ALL TABLES IN SCHEMA schema_name [, ...] } - // TO { username [ WITH GRANT OPTION ] | GROUP group_name | PUBLIC } [, ...] + /// Generate role table to sql. + /// + /// ```sql + /// {GRANT | REVOKE} { { SELECT | INSERT | UPDATE | DELETE | DROP | REFERENCES } [,...] | ALL [ PRIVILEGES ] } + /// ON { [ TABLE ] table_name [, ...] | ALL TABLES IN SCHEMA schema_name [, ...] } + /// TO { username [ WITH GRANT OPTION ] | GROUP group_name | PUBLIC } [, ...] + /// ``` pub fn to_sql(&self, user: &str) -> String { let mut sqls = vec![]; let mut tables = self @@ -204,7 +208,6 @@ impl RoleValidate for RoleTableLevel { } } -// Test #[cfg(test)] mod tests { use super::*; diff --git a/src/config/user.rs b/src/config/user.rs index 2a3cdbf..1fd08bf 100644 --- a/src/config/user.rs +++ b/src/config/user.rs @@ -6,6 +6,8 @@ pub struct User { pub name: String, // password is optional pub password: Option, + // Need to update password at anytime? by default is false + pub update_password: Option, pub roles: Vec, } @@ -19,9 +21,17 @@ impl User { format!("CREATE USER {}{};", self.name, password) } + pub fn to_sql_update(&self) -> String { + let password = match &self.password { + Some(p) => format!(" WITH PASSWORD '{}'", p), + None => "".to_string(), + }; + + format!("ALTER USER {}{};", self.name, password) + } + pub fn to_sql_drop(&self) -> String { - let sql = format!("DROP USER IF EXISTS {};", self.name); - sql + format!("DROP USER IF EXISTS {};", self.name) } pub fn validate(&self) -> Result<()> { @@ -58,6 +68,7 @@ mod tests { let user = User { name: "test".to_string(), password: Some("test".to_string()), + update_password: Some(true), roles: vec!["test".to_string()], }; @@ -65,11 +76,25 @@ mod tests { assert_eq!(sql, "CREATE USER test WITH PASSWORD 'test';"); } + #[test] + fn test_user_to_sql_update() { + let user = User { + name: "test".to_string(), + password: Some("test".to_string()), + update_password: Some(true), + roles: vec!["test".to_string()], + }; + + let sql = user.to_sql_update(); + assert_eq!(sql, "ALTER USER test WITH PASSWORD 'test';"); + } + #[test] fn test_user_to_sql_drop() { let user = User { name: "test".to_string(), password: Some("test".to_string()), + update_password: Some(true), roles: vec!["test".to_string()], }; @@ -82,6 +107,7 @@ mod tests { let user = User { name: "test".to_string(), password: Some("test".to_string()), + update_password: Some(true), roles: vec!["test".to_string()], }; @@ -93,6 +119,7 @@ mod tests { let user = User { name: "".to_string(), password: Some("test".to_string()), + update_password: Some(true), roles: vec!["test".to_string()], }; @@ -104,6 +131,7 @@ mod tests { let user = User { name: "test".to_string(), password: None, + update_password: Some(true), roles: vec!["test".to_string()], }; @@ -115,6 +143,7 @@ mod tests { let user = User { name: "test".to_string(), password: Some("test".to_string()), + update_password: Some(true), roles: vec![], }; @@ -126,6 +155,7 @@ mod tests { let user = User { name: "test".to_string(), password: Some("test".to_string()), + update_password: Some(true), roles: vec!["test".to_string()], }; @@ -137,6 +167,7 @@ mod tests { let user = User { name: "test".to_string(), password: Some("test".to_string()), + update_password: Some(true), roles: vec!["test".to_string()], }; @@ -148,6 +179,7 @@ mod tests { let user = User { name: "test".to_string(), password: Some("test".to_string()), + update_password: Some(true), roles: vec!["test".to_string()], }; diff --git a/src/gen.rs b/src/gen.rs index 8b3ce0b..a230a13 100644 --- a/src/gen.rs +++ b/src/gen.rs @@ -98,7 +98,12 @@ mod tests { gen_password(10, true, Some("test".to_string()), Some("test".to_string())); gen_password(10, false, None, None); gen_password(10, false, Some("test".to_string()), None); - gen_password(10, false, Some("test".to_string()), Some("test".to_string())); + gen_password( + 10, + false, + Some("test".to_string()), + Some("test".to_string()), + ); } // Test gen_md5_password diff --git a/tests/cli-apply.rs b/tests/cli-apply.rs index 714c583..d73617d 100644 --- a/tests/cli-apply.rs +++ b/tests/cli-apply.rs @@ -34,7 +34,7 @@ fn apply_target_is_directory() { .arg(dir.path()) .assert() .failure() - .stderr(predicate::str::contains("is a directory")); + .stderr(predicate::str::contains("directory is not supported")); // cleanup dir.close().unwrap(); From e3b704d7685baf0bf0cd3f6690190b2a8d3a41f1 Mon Sep 17 00:00:00 2001 From: Duyet Le Date: Thu, 20 Jan 2022 11:49:48 +0700 Subject: [PATCH 2/2] chore: add homebrew build script --- homebrew.sh | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100755 homebrew.sh diff --git a/homebrew.sh b/homebrew.sh new file mode 100755 index 0000000..1512fa3 --- /dev/null +++ b/homebrew.sh @@ -0,0 +1,7 @@ +name=grant +version=0.0.1-beta.4 + +cargo build --release +cd target/release +tar -czf $name-$version-x86_64-apple-darwin.tar.gz $name +shasum -a 256 $name-$version-x86_64-apple-darwin.tar.gz