mysqladm/core/database_privileges/
editor.rs

1//! This module contains serialization and deserialization logic for
2//! editing database privileges in a text editor.
3
4use super::base::{
5    DATABASE_PRIVILEGE_FIELDS, DatabasePrivilegeRow, db_priv_field_human_readable_name,
6};
7use crate::core::{
8    common::{rev_yn, yn},
9    types::MySQLDatabase,
10};
11use anyhow::{Context, anyhow};
12use itertools::Itertools;
13use std::cmp::max;
14
15/// Generates a single row of the privileges table for the editor.
16pub fn format_privileges_line_for_editor(
17    privs: &DatabasePrivilegeRow,
18    username_len: usize,
19    database_name_len: usize,
20) -> String {
21    DATABASE_PRIVILEGE_FIELDS
22        .into_iter()
23        .map(|field| match field {
24            "Db" => format!("{:width$}", privs.db, width = database_name_len),
25            "User" => format!("{:width$}", privs.user, width = username_len),
26            privilege => format!(
27                "{:width$}",
28                yn(privs.get_privilege_by_name(privilege).unwrap()),
29                width = db_priv_field_human_readable_name(privilege).len()
30            ),
31        })
32        .join(" ")
33        .trim()
34        .to_string()
35}
36
37const EDITOR_COMMENT: &str = r#"
38# Welcome to the privilege editor.
39# Each line defines what privileges a single user has on a single database.
40# The first two columns respectively represent the database name and the user, and the remaining columns are the privileges.
41# If the user should have a certain privilege, write 'Y', otherwise write 'N'.
42#
43# Lines starting with '#' are comments and will be ignored.
44"#;
45
46/// Generates the content for the privilege editor.
47///
48/// The unix user is used in case there are no privileges to edit,
49/// so that the user can see an example line based on their username.
50pub fn generate_editor_content_from_privilege_data(
51    privilege_data: &[DatabasePrivilegeRow],
52    unix_user: &str,
53    database_name: Option<&MySQLDatabase>,
54) -> String {
55    let example_user = format!("{}_user", unix_user);
56    let example_db = database_name
57        .unwrap_or(&format!("{}_db", unix_user).into())
58        .to_string();
59
60    // NOTE: `.max()`` fails when the iterator is empty.
61    //       In this case, we know that the only fields in the
62    //       editor will be the example user and example db name.
63    //       Hence, it's put as the fallback value, despite not really
64    //       being a "fallback" in the normal sense.
65    let longest_username = max(
66        privilege_data
67            .iter()
68            .map(|p| p.user.len())
69            .max()
70            .unwrap_or(example_user.len()),
71        "User".len(),
72    );
73
74    let longest_database_name = max(
75        privilege_data
76            .iter()
77            .map(|p| p.db.len())
78            .max()
79            .unwrap_or(example_db.len()),
80        "Database".len(),
81    );
82
83    let mut header: Vec<_> = DATABASE_PRIVILEGE_FIELDS
84        .into_iter()
85        .map(db_priv_field_human_readable_name)
86        .collect();
87
88    // Pad the first two columns with spaces to align the privileges.
89    header[0] = format!("{:width$}", header[0], width = longest_database_name);
90    header[1] = format!("{:width$}", header[1], width = longest_username);
91
92    let example_line = format_privileges_line_for_editor(
93        &DatabasePrivilegeRow {
94            db: example_db.into(),
95            user: example_user.into(),
96            select_priv: true,
97            insert_priv: true,
98            update_priv: true,
99            delete_priv: true,
100            create_priv: false,
101            drop_priv: false,
102            alter_priv: false,
103            index_priv: false,
104            create_tmp_table_priv: false,
105            lock_tables_priv: false,
106            references_priv: false,
107        },
108        longest_username,
109        longest_database_name,
110    );
111
112    format!(
113        "{}\n{}\n{}",
114        EDITOR_COMMENT,
115        header.join(" "),
116        if privilege_data.is_empty() {
117            format!("# {}", example_line)
118        } else {
119            privilege_data
120                .iter()
121                .map(|privs| {
122                    format_privileges_line_for_editor(
123                        privs,
124                        longest_username,
125                        longest_database_name,
126                    )
127                })
128                .join("\n")
129        }
130    )
131}
132
133#[derive(Debug)]
134enum PrivilegeRowParseResult {
135    PrivilegeRow(DatabasePrivilegeRow),
136    ParserError(anyhow::Error),
137    TooFewFields(usize),
138    TooManyFields(usize),
139    Header,
140    Comment,
141    Empty,
142}
143
144#[inline]
145fn parse_privilege_cell_from_editor(yn: &str, name: &str) -> anyhow::Result<bool> {
146    rev_yn(yn)
147        .ok_or_else(|| anyhow!("Expected Y or N, found {}", yn))
148        .context(format!("Could not parse {} privilege", name))
149}
150
151#[inline]
152fn editor_row_is_header(row: &str) -> bool {
153    row.split_ascii_whitespace()
154        .zip(DATABASE_PRIVILEGE_FIELDS.iter())
155        .map(|(field, priv_name)| (field, db_priv_field_human_readable_name(priv_name)))
156        .all(|(field, header_field)| field == header_field)
157}
158
159/// Parse a single row of the privileges table from the editor.
160fn parse_privilege_row_from_editor(row: &str) -> PrivilegeRowParseResult {
161    if row.starts_with('#') || row.starts_with("//") {
162        return PrivilegeRowParseResult::Comment;
163    }
164
165    if row.trim().is_empty() {
166        return PrivilegeRowParseResult::Empty;
167    }
168
169    let parts: Vec<&str> = row.trim().split_ascii_whitespace().collect();
170
171    match parts.len() {
172        n if (n < DATABASE_PRIVILEGE_FIELDS.len()) => {
173            return PrivilegeRowParseResult::TooFewFields(n);
174        }
175        n if (n > DATABASE_PRIVILEGE_FIELDS.len()) => {
176            return PrivilegeRowParseResult::TooManyFields(n);
177        }
178        _ => {}
179    }
180
181    if editor_row_is_header(row) {
182        return PrivilegeRowParseResult::Header;
183    }
184
185    let row = DatabasePrivilegeRow {
186        db: (*parts.first().unwrap()).into(),
187        user: (*parts.get(1).unwrap()).into(),
188        select_priv: match parse_privilege_cell_from_editor(
189            parts.get(2).unwrap(),
190            DATABASE_PRIVILEGE_FIELDS[2],
191        ) {
192            Ok(p) => p,
193            Err(e) => return PrivilegeRowParseResult::ParserError(e),
194        },
195        insert_priv: match parse_privilege_cell_from_editor(
196            parts.get(3).unwrap(),
197            DATABASE_PRIVILEGE_FIELDS[3],
198        ) {
199            Ok(p) => p,
200            Err(e) => return PrivilegeRowParseResult::ParserError(e),
201        },
202        update_priv: match parse_privilege_cell_from_editor(
203            parts.get(4).unwrap(),
204            DATABASE_PRIVILEGE_FIELDS[4],
205        ) {
206            Ok(p) => p,
207            Err(e) => return PrivilegeRowParseResult::ParserError(e),
208        },
209        delete_priv: match parse_privilege_cell_from_editor(
210            parts.get(5).unwrap(),
211            DATABASE_PRIVILEGE_FIELDS[5],
212        ) {
213            Ok(p) => p,
214            Err(e) => return PrivilegeRowParseResult::ParserError(e),
215        },
216        create_priv: match parse_privilege_cell_from_editor(
217            parts.get(6).unwrap(),
218            DATABASE_PRIVILEGE_FIELDS[6],
219        ) {
220            Ok(p) => p,
221            Err(e) => return PrivilegeRowParseResult::ParserError(e),
222        },
223        drop_priv: match parse_privilege_cell_from_editor(
224            parts.get(7).unwrap(),
225            DATABASE_PRIVILEGE_FIELDS[7],
226        ) {
227            Ok(p) => p,
228            Err(e) => return PrivilegeRowParseResult::ParserError(e),
229        },
230        alter_priv: match parse_privilege_cell_from_editor(
231            parts.get(8).unwrap(),
232            DATABASE_PRIVILEGE_FIELDS[8],
233        ) {
234            Ok(p) => p,
235            Err(e) => return PrivilegeRowParseResult::ParserError(e),
236        },
237        index_priv: match parse_privilege_cell_from_editor(
238            parts.get(9).unwrap(),
239            DATABASE_PRIVILEGE_FIELDS[9],
240        ) {
241            Ok(p) => p,
242            Err(e) => return PrivilegeRowParseResult::ParserError(e),
243        },
244        create_tmp_table_priv: match parse_privilege_cell_from_editor(
245            parts.get(10).unwrap(),
246            DATABASE_PRIVILEGE_FIELDS[10],
247        ) {
248            Ok(p) => p,
249            Err(e) => return PrivilegeRowParseResult::ParserError(e),
250        },
251        lock_tables_priv: match parse_privilege_cell_from_editor(
252            parts.get(11).unwrap(),
253            DATABASE_PRIVILEGE_FIELDS[11],
254        ) {
255            Ok(p) => p,
256            Err(e) => return PrivilegeRowParseResult::ParserError(e),
257        },
258        references_priv: match parse_privilege_cell_from_editor(
259            parts.get(12).unwrap(),
260            DATABASE_PRIVILEGE_FIELDS[12],
261        ) {
262            Ok(p) => p,
263            Err(e) => return PrivilegeRowParseResult::ParserError(e),
264        },
265    };
266
267    PrivilegeRowParseResult::PrivilegeRow(row)
268}
269
270// TODO: return better errors
271
272pub fn parse_privilege_data_from_editor_content(
273    content: String,
274) -> anyhow::Result<Vec<DatabasePrivilegeRow>> {
275    content
276        .trim()
277        .split('\n')
278        .map(|line| line.trim())
279        .map(parse_privilege_row_from_editor)
280        .map(|result| match result {
281            PrivilegeRowParseResult::PrivilegeRow(row) => Ok(Some(row)),
282            PrivilegeRowParseResult::ParserError(e) => Err(e),
283            PrivilegeRowParseResult::TooFewFields(n) => Err(anyhow!(
284                "Too few fields in line. Expected to find {} fields, found {}",
285                DATABASE_PRIVILEGE_FIELDS.len(),
286                n
287            )),
288            PrivilegeRowParseResult::TooManyFields(n) => Err(anyhow!(
289                "Too many fields in line. Expected to find {} fields, found {}",
290                DATABASE_PRIVILEGE_FIELDS.len(),
291                n
292            )),
293            PrivilegeRowParseResult::Header => Ok(None),
294            PrivilegeRowParseResult::Comment => Ok(None),
295            PrivilegeRowParseResult::Empty => Ok(None),
296        })
297        .filter_map(|result| result.transpose())
298        .collect::<anyhow::Result<Vec<DatabasePrivilegeRow>>>()
299}
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304
305    #[test]
306    fn ensure_generated_and_parsed_editor_content_is_equal() {
307        let permissions = vec![
308            DatabasePrivilegeRow {
309                db: "db".into(),
310                user: "user".into(),
311                select_priv: true,
312                insert_priv: true,
313                update_priv: true,
314                delete_priv: true,
315                create_priv: true,
316                drop_priv: true,
317                alter_priv: true,
318                index_priv: true,
319                create_tmp_table_priv: true,
320                lock_tables_priv: true,
321                references_priv: true,
322            },
323            DatabasePrivilegeRow {
324                db: "db".into(),
325                user: "user".into(),
326                select_priv: false,
327                insert_priv: false,
328                update_priv: false,
329                delete_priv: false,
330                create_priv: false,
331                drop_priv: false,
332                alter_priv: false,
333                index_priv: false,
334                create_tmp_table_priv: false,
335                lock_tables_priv: false,
336                references_priv: false,
337            },
338        ];
339
340        let content = generate_editor_content_from_privilege_data(&permissions, "user", None);
341
342        let parsed_permissions = parse_privilege_data_from_editor_content(content).unwrap();
343
344        assert_eq!(permissions, parsed_permissions);
345    }
346}