1
//! This module contains serialization and deserialization logic for
2
//! editing database privileges in a text editor.
3

            
4
use super::base::{
5
    DATABASE_PRIVILEGE_FIELDS, DatabasePrivilegeRow, db_priv_field_human_readable_name,
6
};
7
use crate::core::{
8
    common::{rev_yn, yn},
9
    types::MySQLDatabase,
10
};
11
use anyhow::{Context, anyhow};
12
use itertools::Itertools;
13
use std::cmp::max;
14

            
15
/// Generates a single row of the privileges table for the editor.
16
#[must_use]
17
6
pub fn format_privileges_line_for_editor(
18
6
    privs: &DatabasePrivilegeRow,
19
6
    database_name_len: usize,
20
6
    username_len: usize,
21
6
) -> String {
22
    DATABASE_PRIVILEGE_FIELDS
23
6
        .into_iter()
24
78
        .map(|field| match field {
25
78
            "Db" => format!("{:width$}", privs.db, width = database_name_len),
26
72
            "User" => format!("{:width$}", privs.user, width = username_len),
27
66
            privilege => format!(
28
                "{:width$}",
29
                // SAFETY: unwrap is safe here because the field names are static
30
66
                yn(privs.get_privilege_by_name(privilege).unwrap()),
31
66
                width = db_priv_field_human_readable_name(privilege).len()
32
            ),
33
78
        })
34
6
        .join(" ")
35
6
        .trim()
36
6
        .to_string()
37
6
}
38

            
39
const EDITOR_COMMENT: &str = r"
40
# Welcome to the privilege editor.
41
# Each line defines what privileges a single user has on a single database.
42
# The first two columns respectively represent the database name and the user, and the remaining columns are the privileges.
43
# If the user should have a certain privilege, write 'Y', otherwise write 'N'.
44
#
45
# Lines starting with '#' are comments and will be ignored.
46
";
47

            
48
/// Generates the content for the privilege editor.
49
///
50
/// The unix user is used in case there are no privileges to edit,
51
/// so that the user can see an example line based on their username.
52
2
pub fn generate_editor_content_from_privilege_data(
53
2
    privilege_data: &[DatabasePrivilegeRow],
54
2
    unix_user: &str,
55
2
    database_name: Option<&MySQLDatabase>,
56
2
) -> String {
57
2
    let example_user = format!("{unix_user}_user");
58
2
    let example_db = database_name
59
2
        .unwrap_or(&format!("{unix_user}_db").into())
60
2
        .to_string();
61

            
62
    // NOTE: `.max()`` fails when the iterator is empty.
63
    //       In this case, we know that the only fields in the
64
    //       editor will be the example user and example db name.
65
    //       Hence, it's put as the fallback value, despite not really
66
    //       being a "fallback" in the normal sense.
67
2
    let longest_username = max(
68
2
        privilege_data
69
2
            .iter()
70
4
            .map(|p| p.user.len())
71
2
            .max()
72
2
            .unwrap_or(example_user.len()),
73
2
        "User".len(),
74
    );
75

            
76
2
    let longest_database_name = max(
77
2
        privilege_data
78
2
            .iter()
79
4
            .map(|p| p.db.len())
80
2
            .max()
81
2
            .unwrap_or(example_db.len()),
82
2
        "Database".len(),
83
    );
84

            
85
2
    let mut header: Vec<_> = DATABASE_PRIVILEGE_FIELDS
86
2
        .into_iter()
87
2
        .map(db_priv_field_human_readable_name)
88
2
        .collect();
89

            
90
    // Pad the first two columns with spaces to align the privileges.
91
2
    header[0] = format!("{:width$}", header[0], width = longest_database_name);
92
2
    header[1] = format!("{:width$}", header[1], width = longest_username);
93

            
94
2
    let example_line = format_privileges_line_for_editor(
95
2
        &DatabasePrivilegeRow {
96
2
            db: example_db.into(),
97
2
            user: example_user.into(),
98
2
            select_priv: true,
99
2
            insert_priv: true,
100
2
            update_priv: true,
101
2
            delete_priv: true,
102
2
            create_priv: false,
103
2
            drop_priv: false,
104
2
            alter_priv: false,
105
2
            index_priv: false,
106
2
            create_tmp_table_priv: false,
107
2
            lock_tables_priv: false,
108
2
            references_priv: false,
109
2
        },
110
2
        longest_database_name,
111
2
        longest_username,
112
    );
113

            
114
2
    format!(
115
        "{}\n{}\n{}",
116
        EDITOR_COMMENT,
117
2
        header.join(" "),
118
2
        if privilege_data.is_empty() {
119
            format!("# {example_line}")
120
        } else {
121
2
            privilege_data
122
2
                .iter()
123
4
                .map(|privs| {
124
4
                    format_privileges_line_for_editor(
125
4
                        privs,
126
4
                        longest_database_name,
127
4
                        longest_username,
128
                    )
129
4
                })
130
2
                .join("\n")
131
        }
132
    )
133
2
}
134

            
135
#[derive(Debug)]
136
enum PrivilegeRowParseResult {
137
    PrivilegeRow(DatabasePrivilegeRow),
138
    ParserError(anyhow::Error),
139
    TooFewFields(usize),
140
    TooManyFields(usize),
141
    Header,
142
    Comment,
143
    Empty,
144
}
145

            
146
#[inline]
147
22
fn parse_privilege_cell_from_editor(yn: &str, name: &str) -> anyhow::Result<bool> {
148
22
    let human_readable_name = db_priv_field_human_readable_name(name);
149
22
    rev_yn(yn)
150
22
        .ok_or_else(|| anyhow!("Expected Y or N, found {yn}"))
151
22
        .context(format!("Could not parse '{human_readable_name}' privilege"))
152
22
}
153

            
154
#[inline]
155
3
fn editor_row_is_header(row: &str) -> bool {
156
3
    row.split_ascii_whitespace()
157
3
        .zip(DATABASE_PRIVILEGE_FIELDS.iter())
158
15
        .map(|(field, priv_name)| (field, db_priv_field_human_readable_name(priv_name)))
159
15
        .all(|(field, header_field)| field == header_field)
160
3
}
161

            
162
/// Parse a single row of the privileges table from the editor.
163
10
fn parse_privilege_row_from_editor(row: &str) -> PrivilegeRowParseResult {
164
10
    if row.starts_with('#') || row.starts_with("//") {
165
6
        return PrivilegeRowParseResult::Comment;
166
4
    }
167

            
168
4
    if row.trim().is_empty() {
169
1
        return PrivilegeRowParseResult::Empty;
170
3
    }
171

            
172
3
    let parts: Vec<&str> = row.trim().split_ascii_whitespace().collect();
173

            
174
3
    match parts.len() {
175
3
        n if (n < DATABASE_PRIVILEGE_FIELDS.len()) => {
176
            return PrivilegeRowParseResult::TooFewFields(n);
177
        }
178
3
        n if (n > DATABASE_PRIVILEGE_FIELDS.len()) => {
179
            return PrivilegeRowParseResult::TooManyFields(n);
180
        }
181
3
        _ => {}
182
    }
183

            
184
3
    if editor_row_is_header(row) {
185
1
        return PrivilegeRowParseResult::Header;
186
2
    }
187

            
188
2
    let row = DatabasePrivilegeRow {
189
2
        db: (*parts.first().unwrap()).into(),
190
2
        user: (*parts.get(1).unwrap()).into(),
191
2
        select_priv: match parse_privilege_cell_from_editor(
192
2
            parts.get(2).unwrap(),
193
2
            DATABASE_PRIVILEGE_FIELDS[2],
194
2
        ) {
195
2
            Ok(p) => p,
196
            Err(e) => return PrivilegeRowParseResult::ParserError(e),
197
        },
198
2
        insert_priv: match parse_privilege_cell_from_editor(
199
2
            parts.get(3).unwrap(),
200
2
            DATABASE_PRIVILEGE_FIELDS[3],
201
2
        ) {
202
2
            Ok(p) => p,
203
            Err(e) => return PrivilegeRowParseResult::ParserError(e),
204
        },
205
2
        update_priv: match parse_privilege_cell_from_editor(
206
2
            parts.get(4).unwrap(),
207
2
            DATABASE_PRIVILEGE_FIELDS[4],
208
2
        ) {
209
2
            Ok(p) => p,
210
            Err(e) => return PrivilegeRowParseResult::ParserError(e),
211
        },
212
2
        delete_priv: match parse_privilege_cell_from_editor(
213
2
            parts.get(5).unwrap(),
214
2
            DATABASE_PRIVILEGE_FIELDS[5],
215
2
        ) {
216
2
            Ok(p) => p,
217
            Err(e) => return PrivilegeRowParseResult::ParserError(e),
218
        },
219
2
        create_priv: match parse_privilege_cell_from_editor(
220
2
            parts.get(6).unwrap(),
221
2
            DATABASE_PRIVILEGE_FIELDS[6],
222
2
        ) {
223
2
            Ok(p) => p,
224
            Err(e) => return PrivilegeRowParseResult::ParserError(e),
225
        },
226
2
        drop_priv: match parse_privilege_cell_from_editor(
227
2
            parts.get(7).unwrap(),
228
2
            DATABASE_PRIVILEGE_FIELDS[7],
229
2
        ) {
230
2
            Ok(p) => p,
231
            Err(e) => return PrivilegeRowParseResult::ParserError(e),
232
        },
233
2
        alter_priv: match parse_privilege_cell_from_editor(
234
2
            parts.get(8).unwrap(),
235
2
            DATABASE_PRIVILEGE_FIELDS[8],
236
2
        ) {
237
2
            Ok(p) => p,
238
            Err(e) => return PrivilegeRowParseResult::ParserError(e),
239
        },
240
2
        index_priv: match parse_privilege_cell_from_editor(
241
2
            parts.get(9).unwrap(),
242
2
            DATABASE_PRIVILEGE_FIELDS[9],
243
2
        ) {
244
2
            Ok(p) => p,
245
            Err(e) => return PrivilegeRowParseResult::ParserError(e),
246
        },
247
2
        create_tmp_table_priv: match parse_privilege_cell_from_editor(
248
2
            parts.get(10).unwrap(),
249
2
            DATABASE_PRIVILEGE_FIELDS[10],
250
2
        ) {
251
2
            Ok(p) => p,
252
            Err(e) => return PrivilegeRowParseResult::ParserError(e),
253
        },
254
2
        lock_tables_priv: match parse_privilege_cell_from_editor(
255
2
            parts.get(11).unwrap(),
256
2
            DATABASE_PRIVILEGE_FIELDS[11],
257
2
        ) {
258
2
            Ok(p) => p,
259
            Err(e) => return PrivilegeRowParseResult::ParserError(e),
260
        },
261
2
        references_priv: match parse_privilege_cell_from_editor(
262
2
            parts.get(12).unwrap(),
263
2
            DATABASE_PRIVILEGE_FIELDS[12],
264
2
        ) {
265
2
            Ok(p) => p,
266
            Err(e) => return PrivilegeRowParseResult::ParserError(e),
267
        },
268
    };
269

            
270
2
    PrivilegeRowParseResult::PrivilegeRow(row)
271
10
}
272

            
273
1
pub fn parse_privilege_data_from_editor_content(
274
1
    content: &str,
275
1
) -> anyhow::Result<Vec<DatabasePrivilegeRow>> {
276
1
    content
277
1
        .trim()
278
1
        .lines()
279
1
        .map(str::trim)
280
1
        .enumerate()
281
10
        .map(|(i, line)| {
282
10
            let mut header: Vec<_> = DATABASE_PRIVILEGE_FIELDS
283
10
                .into_iter()
284
10
                .map(db_priv_field_human_readable_name)
285
10
                .collect();
286

            
287
10
            let splitline = line.split_ascii_whitespace().collect::<Vec<&str>>();
288
10
            let dbname = splitline.first().unwrap_or(&"");
289
10
            let username = splitline.get(1).unwrap_or(&"");
290

            
291
            // Pad the first two columns with spaces to align the privileges.
292
10
            header[0] = format!("{:width$}", header[0], width = dbname.len());
293
10
            header[1] = format!("{:width$}", header[1], width = username.len());
294

            
295
10
            let header: String = header.join(" ");
296

            
297
10
            match parse_privilege_row_from_editor(line) {
298
2
                PrivilegeRowParseResult::PrivilegeRow(row) => Ok(Some(row)),
299
                PrivilegeRowParseResult::ParserError(e) => Err(anyhow!(
300
                    "Could not parse privilege row from line {i}:\n  {header}\n  {line}\n  {e}",
301
                )),
302

            
303
                PrivilegeRowParseResult::TooFewFields(n) => Err(anyhow!(
304
                    "Too few fields in line {i}:\n  {header}\n  {line}\n  Expected to find {} fields, found {n}",
305
                    DATABASE_PRIVILEGE_FIELDS.len(),
306
                )),
307
                PrivilegeRowParseResult::TooManyFields(n) => Err(anyhow!(
308
                    "Too many fields in line {i}:\n  {header}\n  {line}\n  Expected to find {} fields, found {n}",
309
                    DATABASE_PRIVILEGE_FIELDS.len(),
310
                )),
311
1
                PrivilegeRowParseResult::Header => Ok(None),
312
6
                PrivilegeRowParseResult::Comment => Ok(None),
313
1
                PrivilegeRowParseResult::Empty => Ok(None),
314
            }
315
10
        })
316
1
        .filter_map(std::result::Result::transpose)
317
1
        .collect::<anyhow::Result<Vec<DatabasePrivilegeRow>>>()
318
1
}
319

            
320
#[cfg(test)]
321
mod tests {
322
    use super::*;
323

            
324
    use pretty_assertions::assert_eq;
325

            
326
    #[test]
327
1
    fn test_generate_editor_content_from_privilege_data() {
328
1
        let permissions = vec![
329
1
            DatabasePrivilegeRow {
330
1
                db: "test_abcdef".into(),
331
1
                user: "test_abcdef".into(),
332
1
                select_priv: true,
333
1
                insert_priv: false,
334
1
                update_priv: true,
335
1
                delete_priv: false,
336
1
                create_priv: true,
337
1
                drop_priv: false,
338
1
                alter_priv: true,
339
1
                index_priv: false,
340
1
                create_tmp_table_priv: true,
341
1
                lock_tables_priv: false,
342
1
                references_priv: true,
343
1
            },
344
1
            DatabasePrivilegeRow {
345
1
                db: "test_abcdefghijlkmno".into(),
346
1
                user: "test_abcdef".into(),
347
1
                select_priv: true,
348
1
                insert_priv: false,
349
1
                update_priv: true,
350
1
                delete_priv: false,
351
1
                create_priv: true,
352
1
                drop_priv: false,
353
1
                alter_priv: true,
354
1
                index_priv: false,
355
1
                create_tmp_table_priv: true,
356
1
                lock_tables_priv: false,
357
1
                references_priv: true,
358
1
            },
359
        ];
360

            
361
1
        let content = generate_editor_content_from_privilege_data(&permissions, "test", None);
362

            
363
1
        let expected_lines = vec![
364
            "",
365
1
            "# Welcome to the privilege editor.",
366
1
            "# Each line defines what privileges a single user has on a single database.",
367
1
            "# The first two columns respectively represent the database name and the user, and the remaining columns are the privileges.",
368
1
            "# If the user should have a certain privilege, write 'Y', otherwise write 'N'.",
369
1
            "#",
370
1
            "# Lines starting with '#' are comments and will be ignored.",
371
1
            "",
372
1
            "Database             User        Select Insert Update Delete Create Drop Alter Index Temp Lock References",
373
1
            "test_abcdef          test_abcdef Y      N      Y      N      Y      N    Y     N     Y    N    Y",
374
1
            "test_abcdefghijlkmno test_abcdef Y      N      Y      N      Y      N    Y     N     Y    N    Y",
375
        ];
376

            
377
1
        let generated_lines: Vec<&str> = content.lines().collect();
378

            
379
1
        assert_eq!(generated_lines, expected_lines);
380
1
    }
381

            
382
    #[test]
383
1
    fn ensure_generated_and_parsed_editor_content_is_equal() {
384
1
        let permissions = vec![
385
1
            DatabasePrivilegeRow {
386
1
                db: "db".into(),
387
1
                user: "user".into(),
388
1
                select_priv: true,
389
1
                insert_priv: true,
390
1
                update_priv: true,
391
1
                delete_priv: true,
392
1
                create_priv: true,
393
1
                drop_priv: true,
394
1
                alter_priv: true,
395
1
                index_priv: true,
396
1
                create_tmp_table_priv: true,
397
1
                lock_tables_priv: true,
398
1
                references_priv: true,
399
1
            },
400
1
            DatabasePrivilegeRow {
401
1
                db: "db".into(),
402
1
                user: "user".into(),
403
1
                select_priv: false,
404
1
                insert_priv: false,
405
1
                update_priv: false,
406
1
                delete_priv: false,
407
1
                create_priv: false,
408
1
                drop_priv: false,
409
1
                alter_priv: false,
410
1
                index_priv: false,
411
1
                create_tmp_table_priv: false,
412
1
                lock_tables_priv: false,
413
1
                references_priv: false,
414
1
            },
415
        ];
416

            
417
1
        let content = generate_editor_content_from_privilege_data(&permissions, "user", None);
418

            
419
1
        let parsed_permissions = parse_privilege_data_from_editor_content(&content).unwrap();
420

            
421
1
        assert_eq!(permissions, parsed_permissions);
422
1
    }
423
}