1use crate::config::{Config, Role, User as UserInConfig};
2use crate::connection::{DbConnection, User};
3use ansi_term::Colour::{Green, Purple, Red};
4use anyhow::{anyhow, Context, Result};
5use ascii_table::AsciiTable;
6use log::{error, info};
7use std::path::Path;
8
9pub fn apply(target: &Path, dryrun: bool) -> Result<()> {
12 let target = target.to_path_buf();
13
14 if target.is_dir() {
15 return Err(anyhow!(
16 "directory is not supported yet ({})",
17 target.display()
18 ));
19 }
20
21 let config = Config::new(&target)?;
22
23 info!("Applying configuration:\n{}", config);
24 let mut conn = DbConnection::new(&config)?;
25
26 let users_in_db = conn.get_users()?;
27 let users_in_config = config.users.clone();
28
29 create_or_update_users(&mut conn, &users_in_db, &users_in_config, dryrun)?;
31
32 create_or_update_privileges(&mut conn, &config, dryrun)?;
34
35 Ok(())
36}
37
38pub fn apply_all(target: &Path, dryrun: bool) -> Result<()> {
40 let target = target.to_path_buf();
41
42 let mut config_files = Vec::new();
44 for entry in std::fs::read_dir(target)? {
45 let entry = entry?;
46 let path = entry.path();
47 if path.is_file() {
48 if let Some(ext) = path.extension() {
49 if ext == "yaml" || ext == "yml" {
50 config_files.push(path);
51 }
52 }
53 }
54 }
55
56 for config_file in config_files {
58 info!("Applying configuration from {}", config_file.display());
59 apply(&config_file, dryrun)?;
60 }
61
62 Ok(())
63}
64
65fn create_or_update_users(
74 conn: &mut DbConnection,
75 users_in_db: &[User],
76 users_in_config: &[UserInConfig],
77 dryrun: bool,
78) -> Result<()> {
79 let mut summary = vec![vec!["User".to_string(), "Action".to_string()]];
80 summary.push(vec!["---".to_string(), "---".to_string()]);
81
82 for user in users_in_config {
84 let user_in_db = users_in_db.iter().find(|&u| u.name == user.name);
85 match user_in_db {
86 Some(user_in_db) => {
88 if user.update_password.unwrap_or(false) {
90 let sql = user.to_sql_update()?;
91
92 if dryrun {
93 info!(
94 "{}: {}",
95 Purple.paint("Dry-run"),
96 Purple.paint(sanitize_sql_for_logging(&sql))
97 );
98 summary.push(vec![
99 user.name.to_string(),
100 Green.paint("would update password").to_string(),
101 ]);
102 } else {
103 conn.execute(&sql, &[])?;
104 info!(
105 "{}: {}",
106 Green.paint("Success"),
107 Purple.paint(sanitize_sql_for_logging(&sql))
108 );
109 summary.push(vec![user.name.clone(), "password updated".to_string()]);
110 }
111 } else {
112 summary.push(vec![
114 user_in_db.name.clone(),
115 "no action (already exists)".to_string(),
116 ]);
117 }
118 }
119
120 None => {
122 let sql = user.to_sql_create()?;
123
124 if dryrun {
125 info!(
126 "{}: {}",
127 Purple.paint("Dry-run"),
128 sanitize_sql_for_logging(&sql)
129 );
130 summary.push(vec![
131 user.name.clone(),
132 format!("would create (dryrun) {}", sanitize_sql_for_logging(&sql)),
133 ]);
134 } else {
135 conn.execute(&sql, &[])?;
136 info!(
137 "{}: {}",
138 Green.paint("Success"),
139 sanitize_sql_for_logging(&sql)
140 );
141 summary.push(vec![
142 user.name.clone(),
143 format!("created {}", sanitize_sql_for_logging(&sql)),
144 ]);
145 }
146 }
147 }
148 }
149
150 for user in users_in_db {
152 if !users_in_config.iter().any(|u| u.name == user.name) {
153 summary.push(vec![
155 user.name.clone(),
156 "no action (not in config)".to_string(),
157 ]);
158 }
159 }
160
161 print_summary(summary);
163
164 Ok(())
165}
166
167fn create_or_update_privileges(
172 conn: &mut DbConnection,
173 config: &Config,
174 dryrun: bool,
175) -> Result<()> {
176 let mut summary = vec![vec![
177 "User".to_string(),
178 "Role Name".to_string(),
179 "Detail".to_string(),
180 "Status".to_string(),
181 ]];
182 summary.push(vec![
183 "---".to_string(),
184 "---".to_string(),
185 "---".to_string(),
186 "---".to_string(),
187 ]);
188
189 for user in &config.users {
193 for role_name in user.roles.iter() {
197 let role = config
198 .roles
199 .iter()
200 .find(|&r| r.find(role_name))
201 .ok_or_else(|| {
202 anyhow!("Role '{}' not found for user '{}'", role_name, user.name)
203 })?;
204
205 let sql = role.to_sql(&user.name);
208
209 let mut status = if dryrun {
210 "dry-run".to_string()
211 } else {
212 "updated".to_string()
213 };
214
215 if !dryrun {
216 match conn.execute(&sql, &[]) {
217 Ok(nrows) => {
218 info!(
219 "{}: {} {}",
220 Green.paint("Success"),
221 Purple.paint(&sql),
222 format!("(updated {} row(s))", nrows)
223 );
224 status = "updated".to_string();
225 }
226 Err(e) => {
227 error!("{}: {}", Red.paint("Error"), sql);
228 error!(" -> {}: {}", Red.paint("Error details"), e);
229 status = "error".to_string();
230 return Err(e).context(format!(
232 "Failed to execute privilege grant for user '{}' role '{}'",
233 user.name, role_name
234 ));
235 }
236 }
237 } else {
238 info!("{}: {}", Purple.paint("Dry-run"), sql);
239 }
240
241 let detail = match role {
242 Role::Database(role) => format!("database{:?}", role.databases.clone()),
243 Role::Schema(role) => format!("schema{:?}", role.schemas.clone()),
244 Role::Table(role) => format!("table{:?}", role.tables.clone()),
245 };
246
247 summary.push(vec![
249 user.name.clone(),
250 role_name.clone(),
251 detail.to_string(),
252 status.to_string(),
253 ]);
254 }
255 }
256
257 print_summary(summary);
259
260 Ok(())
261}
262
263fn sanitize_sql_for_logging(sql: &str) -> String {
266 let mut result = String::new();
268 let bytes = sql.as_bytes();
269 let mut i = 0;
270
271 while i < bytes.len() {
272 if i + 8 < bytes.len()
274 && &bytes[i..i + 8].to_ascii_uppercase() == b"PASSWORD"
275 && (i == 0 || !bytes[i - 1].is_ascii_alphanumeric())
276 {
277 result.push_str("PASSWORD");
278 i += 8;
279
280 while i < bytes.len() && bytes[i].is_ascii_whitespace() {
282 result.push(bytes[i] as char);
283 i += 1;
284 }
285
286 if i < bytes.len() && bytes[i] == b'\'' {
288 result.push('\'');
289 i += 1;
290
291 while i < bytes.len() {
293 if bytes[i] == b'\'' {
294 if i + 1 < bytes.len() && bytes[i + 1] == b'\'' {
296 i += 2; continue;
298 }
299 result.push_str("[REDACTED]'");
301 i += 1;
302 break;
303 }
304 i += 1;
305 }
306 }
307 } else {
308 result.push(bytes[i] as char);
309 i += 1;
310 }
311 }
312
313 result
314}
315
316fn print_summary(summary: Vec<Vec<String>>) {
319 let ascii_table = AsciiTable::default();
320
321 info!("Summary:\n{}", ascii_table.format(summary));
322}