grant/config/
config_base.rs

1use anyhow::{anyhow, Context, Result};
2use serde::{Deserialize, Serialize};
3use std::collections::HashSet;
4use std::path::Path;
5use std::{fmt, fs};
6
7pub use super::connection::Connection;
8pub use super::User;
9pub use super::{Role, RoleLevelType};
10
11/// Configuration contains all the information needed to connect to a database, the roles and
12/// users.
13///  - `connection`: the connection to the database, including the type of connection and the URL.
14///  - `roles`: the roles of the users. The roles are used to determine the permissions of the
15///  users. A role can be a [RoleDatabaseLevel], [RoleSchemaLevel] or [RoleTableLevel].
16///  - `users`: the users.
17///
18/// [RoleDatabaseLevel]: crate::config::role::RoleDatabaseLevel
19/// [RoleSchemaLevel]: crate::config::role::RoleSchemaLevel
20/// [RoleTableLevel]: crate::config::role::RoleTableLevel
21///
22/// For example:
23///
24/// ```yaml
25/// connection:
26///   type: postgres
27///   url: postgres://user:password@host:port/database
28///
29/// roles:
30///   - name: role_database_level
31///     type: databases
32///     grants:
33///     - CREATE
34///     - TEMP
35///     databases:
36///     - db1
37///     - db2
38///     - db3
39///  users:
40///  - name: user1
41///    password: password1
42///    roles:
43///    - role_database_level
44///    - role_schema_level
45///    - role_table_level
46/// ```
47#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
48pub struct Config {
49    pub connection: Connection,
50    pub roles: Vec<Role>,
51    pub users: Vec<User>,
52}
53
54impl fmt::Display for Config {
55    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
56        write!(f, "{}", serde_yaml::to_string(&self).unwrap())
57    }
58}
59
60impl std::str::FromStr for Config {
61    type Err = anyhow::Error;
62
63    fn from_str(s: &str) -> Result<Self> {
64        let config: Config = serde_yaml::from_str(s)?;
65
66        // Validate
67        config.validate()?;
68
69        Ok(config)
70    }
71}
72
73impl Config {
74    pub fn new(config_path: &Path) -> Result<Self> {
75        let config_path = config_path.to_path_buf();
76        let config_str = fs::read_to_string(&config_path).context("failed to read config file")?;
77        let config: Config = serde_yaml::from_str(&config_str)?;
78
79        config.validate()?;
80
81        // expand env variables
82        let config = config.expand_env_vars()?;
83
84        Ok(config)
85    }
86
87    pub fn validate(&self) -> Result<()> {
88        // Validate connection
89        self.connection.validate()?;
90
91        // Validate roles
92        for role in &self.roles {
93            role.validate()?;
94        }
95        // Validate role name are unique by name
96        let mut role_names = HashSet::new();
97        for role in &self.roles {
98            if role_names.contains(&role.get_name()) {
99                return Err(anyhow!("duplicated role name: {}", role.get_name()));
100            }
101            role_names.insert(role.get_name());
102        }
103
104        // Validate users
105        for user in &self.users {
106            user.validate()?;
107        }
108        // Validate users are unique by name
109        let mut user_names: HashSet<String> = HashSet::new();
110        for user in &self.users {
111            if user_names.contains(&user.name) {
112                return Err(anyhow!("duplicated user: {}", user.name));
113            }
114            user_names.insert(user.name.clone());
115        }
116        // Validate users roles are available in roles
117        for user in &self.users {
118            for role in &user.roles {
119                // role name can contain '-' at the first position
120                let role_name = if let Some(without_sign) = role.strip_prefix('-') {
121                    without_sign
122                } else {
123                    role
124                };
125
126                if !self.roles.iter().any(|r| r.get_name() == role_name) {
127                    return Err(anyhow!("user role {} is not available", role));
128                }
129            }
130        }
131
132        Ok(())
133    }
134
135    // Expand env variables in config
136    fn expand_env_vars(&self) -> Result<Self> {
137        let mut config = self.clone();
138
139        // expand connection
140        config.connection = config.connection.expand_env_vars()?;
141
142        Ok(config)
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149    use indoc::indoc;
150    use std::io::Write;
151    use std::path::PathBuf;
152    use std::str::FromStr;
153    use tempfile::NamedTempFile;
154
155    #[test]
156    #[should_panic(expected = "failed to get content: invalid type: string")]
157    fn test_with_basic_config() {
158        let _text = "bad yaml content";
159        let mut file = NamedTempFile::new().expect("failed to create temp file");
160        file.write(_text.as_bytes())
161            .expect("failed to write to temp file");
162        let path = PathBuf::from(file.path().to_str().unwrap());
163
164        Config::new(&path).expect("failed to get content");
165    }
166
167    // Test config with minimum valid YAML
168    #[test]
169    fn test_read_config_basic_config() {
170        let _text = indoc! {"
171                 connection:
172                   type: postgres
173                   url: postgres://localhost:5432/postgres
174                 roles: []
175                 users: []
176             "};
177
178        let mut file = NamedTempFile::new().expect("failed to create temp file");
179        file.write(_text.as_bytes())
180            .expect("failed to write to temp file");
181        let path = PathBuf::from(file.path().to_str().unwrap());
182
183        Config::new(&path).expect("failed to get content");
184    }
185
186    // Test Config::from_str
187    #[test]
188    fn test_read_config_from_str() {
189        let _text = indoc! {"
190                 connection:
191                   type: postgres
192                   url: postgres://localhost:5432/postgres
193                 roles: []
194                 users: []
195             "};
196
197        Config::from_str(_text).expect("failed to get content");
198    }
199
200    // Config::from_str and Config::new should return the same result
201    #[test]
202    fn test_read_config_from_str_and_new() {
203        let _text = indoc! {"
204             connection:
205               type: postgres
206               url: postgres://localhost:5432/postgres
207             roles: []
208             users: []
209        "};
210
211        let config_1 = Config::from_str(_text).expect("failed to get content");
212
213        let mut file = NamedTempFile::new().expect("failed to create temp file");
214        file.write(_text.as_bytes())
215            .expect("failed to write to temp file");
216        let path = PathBuf::from(file.path().to_str().unwrap());
217        let config_2 = Config::new(&path).expect("failed to get content");
218
219        assert_eq!(config_1, config_2);
220    }
221
222    // Test config with url contains environement variable
223    #[test]
224    fn test_read_config_with_env_var() {
225        envmnt::set("POSTGRES_HOST", "duyet");
226
227        let _text = indoc! {"
228                 connection:
229                   type: postgres
230                   url: postgres://${POSTGRES_HOST}:5432/postgres
231                 roles: []
232                 users: []
233             "};
234
235        let mut file = NamedTempFile::new().expect("failed to create temp file");
236        file.write(_text.as_bytes())
237            .expect("failed to write to temp file");
238        let path = PathBuf::from(file.path().to_str().unwrap());
239
240        let config = Config::new(&path).expect("failed to get content");
241
242        assert_eq!(config.connection.url, "postgres://duyet:5432/postgres");
243
244        envmnt::remove("POSTGRES_HOST");
245    }
246
247    // Test expand environement variables but not available
248    #[test]
249    fn test_read_config_with_env_var_not_available() {
250        let _text = indoc! {"
251                 connection:
252                   type: postgres
253                   url: postgres://${POSTGRES_HOST:duyet}:5432/${POSTGRES_ABC}
254                 roles: []
255                 users: []
256             "};
257
258        let mut file = NamedTempFile::new().expect("failed to create temp file");
259        file.write(_text.as_bytes())
260            .expect("failed to write to temp file");
261        let path = PathBuf::from(file.path().to_str().unwrap());
262
263        let config = Config::new(&path).expect("failed to get content");
264
265        assert_eq!(
266            config.connection.url,
267            "postgres://duyet:5432/${POSTGRES_ABC}"
268        );
269    }
270
271    // Test config with invalid connection type
272    #[test]
273    #[should_panic(expected = "connection.type: unknown variant `invalid`")]
274    fn test_read_config_invalid_connection_type() {
275        let _text = indoc! {"
276                 connection:
277                   type: invalid
278                   url: postgres://postgres@localhost:5432/postgres
279                 roles: []
280                 users: []
281             "};
282
283        let mut file = NamedTempFile::new().expect("failed to create temp file");
284        file.write(_text.as_bytes())
285            .expect("failed to write to temp file");
286        let path = PathBuf::from(file.path().to_str().unwrap());
287
288        Config::new(&path).expect("failed to parse config");
289    }
290
291    // Test config with role database level
292    #[test]
293    fn test_read_config_one_role_database_level() {
294        let _text = indoc! {"
295                 connection:
296                   type: postgres
297                   url: postgres://localhost:5432/postgres
298                 roles:
299                 - type: database
300                   name: role_database_level_1
301                   grants:
302                   - CREATE
303                   - TEMP
304                   databases:
305                   - db1
306                   - db2
307                   - db3
308                 - type: database
309                   name: role_database_level_2
310                   grants:
311                   - ALL
312                   databases:
313                   - db1
314                   - db2
315                   - db3
316                 users: []
317             "};
318
319        let mut file = NamedTempFile::new().expect("failed to create temp file");
320        file.write(_text.as_bytes())
321            .expect("failed to write to temp file");
322        let path = PathBuf::from(file.path().to_str().unwrap());
323
324        let config = Config::new(&path).expect("failed to parse config");
325        assert_eq!(config.roles.len(), 2);
326
327        // Test role 1
328        assert_eq!(config.roles[0].get_name(), "role_database_level_1");
329        assert_eq!(config.roles[0].get_level(), RoleLevelType::Database);
330        assert_eq!(config.roles[0].get_grants().len(), 2);
331        assert_eq!(config.roles[0].get_grants()[0], "CREATE");
332        assert_eq!(config.roles[0].get_grants()[1], "TEMP");
333        assert_eq!(config.roles[0].get_databases().len(), 3);
334        assert_eq!(config.roles[0].get_databases()[0], "db1");
335        assert_eq!(config.roles[0].get_databases()[1], "db2");
336        assert_eq!(config.roles[0].get_databases()[2], "db3");
337        assert_eq!(
338            config.roles[0].to_sql("duyet"),
339            "GRANT CREATE, TEMP ON DATABASE \"db1\", \"db2\", \"db3\" TO \"duyet\";".to_string()
340        );
341
342        // Test role 2
343        assert_eq!(config.roles[1].get_name(), "role_database_level_2");
344        assert_eq!(config.roles[1].get_level(), RoleLevelType::Database);
345        assert_eq!(config.roles[1].get_grants().len(), 1);
346        assert_eq!(config.roles[1].get_grants()[0], "ALL");
347        assert_eq!(config.roles[1].get_databases().len(), 3);
348        assert_eq!(config.roles[1].get_databases()[0], "db1");
349        assert_eq!(config.roles[1].get_databases()[1], "db2");
350        assert_eq!(config.roles[1].get_databases()[2], "db3");
351        assert_eq!(
352            config.roles[1].to_sql("duyet"),
353            "GRANT ALL PRIVILEGES ON DATABASE \"db1\", \"db2\", \"db3\" TO \"duyet\";".to_string()
354        );
355    }
356
357    // Test config role type database level with invalid grants
358    #[test]
359    #[should_panic(expected = "invalid grant: invalid")]
360    fn test_read_config_role_type_database_level_invalid_grants() {
361        let _text = indoc! {"
362                 connection:
363                   type: postgres
364                   url: postgres://localhost:5432/postgres
365                 roles:
366                 - type: database
367                   name: role_database_level
368                   grants:
369                   - invalid
370                   databases:
371                   - db1
372                   - db2
373                   - db3
374                 users: []
375             "};
376
377        let mut file = NamedTempFile::new().expect("failed to create temp file");
378        file.write(_text.as_bytes())
379            .expect("failed to write to temp file");
380        let path = PathBuf::from(file.path().to_str().unwrap());
381
382        Config::new(&path).expect("failed to parse config");
383    }
384
385    // Test config with role schema level
386    #[test]
387    fn test_read_config_one_role_schema_level() {
388        let _text = indoc! {"
389                 connection:
390                   type: postgres
391                   url: postgres://localhost:5432/postgres
392                 roles:
393                 - type: schema
394                   name: role_schema_level_1
395                   grants:
396                   - CREATE
397                   - USAGE
398                   schemas:
399                   - schema1
400                   - schema2
401                   - schema3
402                 - type: schema
403                   name: role_schema_level_2
404                   grants:
405                   - ALL
406                   schemas:
407                   - schema1
408                   - schema2
409                   - schema3
410                 users: []
411             "};
412
413        let mut file = NamedTempFile::new().expect("failed to create temp file");
414        file.write(_text.as_bytes())
415            .expect("failed to write to temp file");
416        let path = PathBuf::from(file.path().to_str().unwrap());
417
418        let config = Config::new(&path).expect("failed to parse config");
419        assert_eq!(config.roles.len(), 2);
420
421        // Test role 1
422        assert_eq!(config.roles[0].get_name(), "role_schema_level_1");
423        assert_eq!(config.roles[0].get_level(), RoleLevelType::Schema);
424        assert_eq!(config.roles[0].get_grants().len(), 2);
425        assert_eq!(config.roles[0].get_grants()[0], "CREATE");
426        assert_eq!(config.roles[0].get_grants()[1], "USAGE");
427        assert_eq!(config.roles[0].get_schemas().len(), 3);
428        assert_eq!(config.roles[0].get_schemas()[0], "schema1");
429        assert_eq!(config.roles[0].get_schemas()[1], "schema2");
430        assert_eq!(config.roles[0].get_schemas()[2], "schema3");
431        assert_eq!(
432            config.roles[0].to_sql("duyet"),
433            "GRANT CREATE, USAGE ON SCHEMA \"schema1\", \"schema2\", \"schema3\" TO \"duyet\";"
434                .to_string()
435        );
436
437        // Test role 2
438        assert_eq!(config.roles[1].get_name(), "role_schema_level_2");
439        assert_eq!(config.roles[1].get_level(), RoleLevelType::Schema);
440        assert_eq!(config.roles[1].get_grants().len(), 1);
441        assert_eq!(config.roles[1].get_grants()[0], "ALL");
442        assert_eq!(config.roles[1].get_schemas().len(), 3);
443        assert_eq!(config.roles[1].get_schemas()[0], "schema1");
444        assert_eq!(config.roles[1].get_schemas()[1], "schema2");
445        assert_eq!(config.roles[1].get_schemas()[2], "schema3");
446        assert_eq!(
447            config.roles[1].to_sql("duyet"),
448            "GRANT ALL PRIVILEGES ON SCHEMA \"schema1\", \"schema2\", \"schema3\" TO \"duyet\";"
449                .to_string()
450        );
451    }
452
453    // Test config role type schema level with invalid grants
454    #[test]
455    #[should_panic(expected = "invalid grant: invalid")]
456    fn test_read_config_role_type_schema_level_invalid_grants() {
457        let _text = indoc! {"
458                 connection:
459                   type: postgres
460                   url: postgres://localhost:5432/postgres
461                 roles:
462                 - type: schema
463                   name: role_schema_level
464                   grants:
465                   - invalid
466                   schemas:
467                   - schema1
468                   - schema2
469                   - schema3
470                 users: []
471             "};
472
473        let mut file = NamedTempFile::new().expect("failed to create temp file");
474        file.write(_text.as_bytes())
475            .expect("failed to write to temp file");
476        let path = PathBuf::from(file.path().to_str().unwrap());
477
478        Config::new(&path).expect("failed to parse config");
479    }
480
481    // Test config with one role table level
482    #[test]
483    fn test_read_config_one_role_table_level() {
484        let _text = indoc! {"
485                 connection:
486                   type: postgres
487                   url: postgres://localhost:5432/postgres
488                 roles:
489                 - type: table
490                   name: role_table_level_1
491                   grants:
492                     - SELECT
493                     - INSERT
494                   schemas:
495                     - schema1
496                   tables:
497                     - table1
498                     - table2
499                     - table3
500                 - type: table
501                   name: role_table_level_2
502                   grants:
503                     - ALL
504                   schemas:
505                     - schema1
506                   tables:
507                     - table1
508                     - table2
509                     - table3
510                 users: []
511             "};
512
513        let mut file = NamedTempFile::new().expect("failed to create temp file");
514        file.write(_text.as_bytes())
515            .expect("failed to write to temp file");
516        let path = PathBuf::from(file.path().to_str().unwrap());
517
518        let config = Config::new(&path).expect("failed to parse config");
519        assert_eq!(config.roles.len(), 2);
520
521        // Test role 1
522        assert_eq!(config.roles[0].get_name(), "role_table_level_1");
523        assert_eq!(config.roles[0].get_level(), RoleLevelType::Table);
524        assert_eq!(config.roles[0].get_grants().len(), 2);
525        assert_eq!(config.roles[0].get_grants()[0], "SELECT");
526        assert_eq!(config.roles[0].get_grants()[1], "INSERT");
527        assert_eq!(config.roles[0].get_schemas().len(), 1);
528        assert_eq!(config.roles[0].get_schemas()[0], "schema1");
529        assert_eq!(config.roles[0].get_tables().len(), 3);
530        assert_eq!(config.roles[0].get_tables()[0], "table1");
531        assert_eq!(config.roles[0].get_tables()[1], "table2");
532        assert_eq!(config.roles[0].get_tables()[2], "table3");
533        assert_eq!(
534            config.roles[0].to_sql("duyet"),
535            "GRANT SELECT, INSERT ON \"schema1\".\"table1\", \"schema1\".\"table2\", \"schema1\".\"table3\" TO \"duyet\";"
536        );
537
538        // Test role 2
539        assert_eq!(config.roles[1].get_name(), "role_table_level_2");
540        assert_eq!(config.roles[1].get_level(), RoleLevelType::Table);
541        assert_eq!(config.roles[1].get_grants().len(), 1);
542        assert_eq!(config.roles[1].get_grants()[0], "ALL");
543        assert_eq!(config.roles[1].get_schemas().len(), 1);
544        assert_eq!(config.roles[1].get_schemas()[0], "schema1");
545        assert_eq!(config.roles[1].get_tables().len(), 3);
546        assert_eq!(config.roles[1].get_tables()[0], "table1");
547        assert_eq!(config.roles[1].get_tables()[1], "table2");
548        assert_eq!(config.roles[1].get_tables()[2], "table3");
549        assert_eq!(
550            config.roles[1].to_sql("duyet"),
551            "GRANT ALL PRIVILEGES ON \"schema1\".\"table1\", \"schema1\".\"table2\", \"schema1\".\"table3\" TO \"duyet\";"
552                .to_string()
553        );
554    }
555
556    // Test config role type table level with table name is `ALL`
557    #[test]
558    fn test_read_config_role_type_table_level_all_tables() {
559        let _text = indoc! {"
560                 connection:
561                   type: postgres
562                   url: postgres://localhost:5432/postgres
563                 roles:
564                 - type: table
565                   name: role_table_level_1
566                   grants:
567                     - SELECT
568                   schemas:
569                     - schema1
570                   tables:
571                     - ALL
572                 - type: table
573                   name: role_table_level_2
574                   grants:
575                     - SELECT
576                   schemas:
577                     - schema1
578                   tables:
579                     - ALL
580                     - another_table_should_be_included_in_all_too
581                 - type: table
582                   name: role_table_level_3
583                   grants:
584                     - SELECT
585                   schemas:
586                     - schema1
587                   tables:
588                     - ALL
589                     - -but_excluded_me
590                 - type: table
591                   name: role_table_level_4
592                   grants:
593                     - SELECT
594                   schemas:
595                     - schema1
596                   tables:
597                     - table_a
598                     - -table_b
599                 - type: table
600                   name: role_table_level_5
601                   grants:
602                     - SELECT
603                   schemas:
604                     - schema1
605                   tables:
606                     - -table_a
607                     - -table_b
608                 - type: table
609                   name: role_table_level_6
610                   grants:
611                     - SELECT
612                   schemas:
613                     - schema1
614                   tables:
615                     - -ALL
616                 users: []
617             "};
618
619        let mut file = NamedTempFile::new().expect("failed to create temp file");
620        file.write(_text.as_bytes())
621            .expect("failed to write to temp file");
622        let path = PathBuf::from(file.path().to_str().unwrap());
623
624        let config = Config::new(&path).expect("failed to parse config");
625        assert_eq!(config.roles.len(), 6);
626
627        assert_eq!(
628            config.roles[0].to_sql("duyet"),
629            "GRANT SELECT ON ALL TABLES IN SCHEMA \"schema1\" TO \"duyet\";"
630        );
631        assert_eq!(
632            config.roles[1].to_sql("duyet"),
633            "GRANT SELECT ON ALL TABLES IN SCHEMA \"schema1\" TO \"duyet\";"
634        );
635        assert_eq!(
636            config.roles[2].to_sql("duyet"),
637            "GRANT SELECT ON ALL TABLES IN SCHEMA \"schema1\" TO \"duyet\"; REVOKE SELECT ON \"schema1\".\"but_excluded_me\" FROM \"duyet\";"
638        );
639        assert_eq!(
640            config.roles[3].to_sql("duyet"),
641            "GRANT SELECT ON \"schema1\".\"table_a\" TO \"duyet\"; REVOKE SELECT ON \"schema1\".\"table_b\" FROM \"duyet\";"
642        );
643        assert_eq!(
644            config.roles[4].to_sql("duyet"),
645            "REVOKE SELECT ON \"schema1\".\"table_a\", \"schema1\".\"table_b\" FROM \"duyet\";"
646        );
647        assert_eq!(
648            config.roles[5].to_sql("duyet"),
649            "REVOKE SELECT ON ALL TABLES IN SCHEMA \"schema1\" FROM \"duyet\";"
650        );
651    }
652
653    // Test config role type table level with invalid grants
654    #[test]
655    #[should_panic(expected = "role.grants invalid")]
656    fn test_read_config_role_type_table_level_invalid_grants() {
657        let _text = indoc! {"
658                 connection:
659                   type: postgres
660                   url: postgres://localhost:5432/postgres
661                 roles:
662                 - type: table
663                   name: role_table_level
664                   grants:
665                   - invalid
666                   schemas:
667                   - schema1
668                   tables:
669                   - table1
670                   - table2
671                   - table3
672                 users: []
673             "};
674
675        let mut file = NamedTempFile::new().expect("failed to create temp file");
676        file.write(_text.as_bytes())
677            .expect("failed to write to temp file");
678        let path = PathBuf::from(file.path().to_str().unwrap());
679
680        Config::new(&path).expect("failed to parse config");
681    }
682
683    // Test two role duplicated name error
684    #[test]
685    #[should_panic(expected = "duplicated role name: role_table_level")]
686    fn test_read_config_two_role_duplicated_name() {
687        let _text = indoc! {"
688                 connection:
689                   type: postgres
690                   url: postgres://localhost:5432/postgres
691                 roles:
692                 - type: table
693                   name: role_table_level
694                   grants:
695                   - SELECT
696                   - INSERT
697                   schemas:
698                   - schema1
699                   tables:
700                   - table1
701                   - table2
702                   - table3
703                 - type: table
704                   name: role_table_level
705                   grants:
706                   - ALL
707                   schemas:
708                   - schema1
709                   tables:
710                   - table1
711                   - table2
712                   - table3
713                 users: []
714             "};
715
716        let mut file = NamedTempFile::new().expect("failed to create temp file");
717        file.write(_text.as_bytes())
718            .expect("failed to write to temp file");
719        let path = PathBuf::from(file.path().to_str().unwrap());
720
721        Config::new(&path).expect("failed to parse config");
722    }
723
724    // Test users config
725    #[test]
726    fn test_read_config_users() {
727        let _text = indoc! {"
728                 connection:
729                   type: postgres
730                   url: postgres://postgres:postgres@localhost:5432/postgres
731                 roles:
732                 - type: database
733                   name: role_database_level
734                   grants:
735                   - CREATE
736                   - TEMP
737                   databases:
738                   - db1
739                   - db2
740                   - db3
741                 - type: schema
742                   name: role_schema_level
743                   grants:
744                   - ALL
745                   schemas:
746                   - schema1
747                   - schema2
748                   - schema3
749                 - type: table
750                   name: role_table_level
751                   grants:
752                   - SELECT
753                   - INSERT
754                   schemas:
755                   - schema1
756                   tables:
757                   - table1
758                   - table2
759                   - table3
760                 users:
761                 - name: duyet
762                   password: 123456
763                   roles:
764                   - role_database_level
765                   - role_schema_level
766                   - role_table_level
767                 - name: duyet_without_password
768                   roles:
769                   - role_database_level
770                   - role_schema_level
771                   - role_table_level
772             "};
773
774        let mut file = NamedTempFile::new().expect("failed to create temp file");
775        file.write(_text.as_bytes())
776            .expect("failed to write to temp file");
777        let path = PathBuf::from(file.path().to_str().unwrap());
778
779        let config = Config::new(&path).expect("failed to parse config");
780        assert_eq!(config.users.len(), 2);
781
782        // Test user 1
783        assert_eq!(config.users[0].get_name(), "duyet");
784        assert_eq!(config.users[0].get_password(), "123456");
785        assert_eq!(config.users[0].get_roles().len(), 3);
786        assert_eq!(config.users[0].get_roles()[0], "role_database_level");
787        assert_eq!(config.users[0].get_roles()[1], "role_schema_level");
788        assert_eq!(config.users[0].get_roles()[2], "role_table_level");
789
790        // Test sql create user
791        assert_eq!(
792            config.users[0].to_sql_create().unwrap(),
793            "CREATE USER duyet WITH PASSWORD '123456';"
794        );
795
796        // Test sql create user without password
797        assert_eq!(
798            config.users[1].to_sql_create().unwrap(),
799            "CREATE USER duyet_without_password;"
800        );
801
802        // Test sql drop user
803        assert_eq!(
804            config.users[0].to_sql_drop().unwrap(),
805            "DROP USER IF EXISTS duyet;"
806        );
807    }
808
809    // Test users config with revoke role by `-role_name`
810    #[test]
811    fn test_read_config_users_exclude_role_by_minus_role_name() {
812        let _text = indoc! {"
813             connection:
814               type: postgres
815               url: postgres://postgres:postgres@localhost:5432/postgres
816             roles:
817             - type: database
818               name: role_database_level
819               grants:
820               - CREATE
821               - TEMP
822               databases:
823               - db1
824               - db2
825               - db3
826             - type: schema
827               name: role_schema_level
828               grants:
829               - ALL
830               schemas:
831               - schema1
832               - schema2
833               - schema3
834             - type: table
835               name: role_table_level
836               grants:
837               - SELECT
838               - INSERT
839               schemas:
840               - schema1
841               tables:
842               - table1
843               - table2
844               - table3
845             users:
846             - name: duyet
847               password: 123456
848               roles:
849               - -role_database_level
850               - -role_schema_level
851               - -role_table_level
852             - name: duyet_without_password
853               roles:
854               - role_database_level
855               - role_schema_level
856               - role_table_level
857        "};
858
859        let mut file = NamedTempFile::new().expect("failed to create temp file");
860        file.write(_text.as_bytes())
861            .expect("failed to write to temp file");
862        let path = PathBuf::from(file.path().to_str().unwrap());
863
864        let config = Config::new(&path).expect("failed to parse config");
865        assert_eq!(config.users.len(), 2);
866
867        // Test user 1
868        assert_eq!(config.users[0].get_name(), "duyet");
869        assert_eq!(config.users[0].get_password(), "123456");
870        assert_eq!(config.users[0].get_roles().len(), 3);
871    }
872
873    /// Test find role by name, name can contains `-` in case of exclude role
874    #[test]
875    fn test_find_role_by_name() {
876        let _text = indoc! {"
877             connection:
878               type: postgres
879               url: postgres://postgres:postgres@localhost:5432/postgres
880             roles:
881             - type: database
882               name: role_database_level
883               grants:
884               - CREATE
885               databases:
886               - db1
887             users: []
888        "};
889
890        let mut file = NamedTempFile::new().expect("failed to create temp file");
891        file.write(_text.as_bytes())
892            .expect("failed to write to temp file");
893        let path = PathBuf::from(file.path().to_str().unwrap());
894
895        let config = Config::new(&path).expect("failed to parse config");
896
897        assert!(config
898            .roles
899            .iter()
900            .find(|r| r.find("role_database_level"))
901            .is_some());
902
903        // Test find role by name with `-`
904        assert!(config
905            .roles
906            .iter()
907            .find(|r| r.find("-role_database_level"))
908            .is_some());
909    }
910}