1
use std::{collections::HashSet, path::Path, str::Lines};
2

            
3
use anyhow::Context;
4
use nix::unistd::Group;
5

            
6
use crate::core::{
7
    common::UnixUser,
8
    protocol::{
9
        CheckAuthorizationError,
10
        request_validation::{GroupDenylist, validate_db_or_user_request},
11
    },
12
    types::DbOrUser,
13
};
14

            
15
pub async fn check_authorization(
16
    dbs_or_users: &[DbOrUser],
17
    unix_user: &UnixUser,
18
    group_denylist: &GroupDenylist,
19
) -> std::collections::BTreeMap<DbOrUser, Result<(), CheckAuthorizationError>> {
20
    dbs_or_users
21
        .iter()
22
        .cloned()
23
        .map(|db_or_user| {
24
            let result = validate_db_or_user_request(&db_or_user, unix_user, group_denylist)
25
                .map_err(CheckAuthorizationError);
26
            (db_or_user, result)
27
        })
28
        .collect()
29
}
30

            
31
/// Reads and parses a group denylist file, returning a set of GUIDs
32
///
33
/// The format of the denylist file is expected to be one group name or GID per line.
34
/// Lines starting with '#' are treated as comments and ignored.
35
/// Empty lines are also ignored.
36
///
37
/// Each line looks like one of the following:
38
/// - `gid:1001`
39
/// - `group:admins`
40
pub fn read_and_parse_group_denylist(denylist_path: &Path) -> anyhow::Result<GroupDenylist> {
41
    let content = std::fs::read_to_string(denylist_path)
42
        .context(format!("Failed to read denylist file at {denylist_path:?}"))?;
43

            
44
    let lines = content.lines();
45

            
46
    let groups = parse_group_denylist(denylist_path, lines);
47

            
48
    Ok(groups)
49
}
50

            
51
1
fn parse_group_denylist(denylist_path: &Path, lines: Lines) -> GroupDenylist {
52
1
    let mut groups = HashSet::<u32>::new();
53

            
54
8
    for (line_number, line) in lines.enumerate() {
55
8
        let trimmed_line = if let Some(comment_start) = line.find('#') {
56
4
            &line[..comment_start]
57
        } else {
58
4
            line
59
        }
60
8
        .trim();
61

            
62
8
        if trimmed_line.is_empty() {
63
3
            continue;
64
5
        }
65

            
66
5
        let parts: Vec<&str> = trimmed_line.splitn(2, ':').collect();
67
5
        if parts.len() != 2 {
68
1
            tracing::warn!(
69
                "Invalid format in denylist file at {:?} on line {}: {}",
70
                denylist_path,
71
                line_number + 1,
72
                line
73
            );
74
1
            continue;
75
4
        }
76

            
77
4
        match parts[0] {
78
4
            "gid" => {
79
2
                let gid: u32 = match parts[1].parse() {
80
1
                    Ok(gid) => gid,
81
1
                    Err(err) => {
82
1
                        tracing::warn!(
83
                            "Invalid GID '{}' in denylist file at {:?} on line {}: {}",
84
                            parts[1],
85
                            denylist_path,
86
                            line_number + 1,
87
                            err
88
                        );
89
1
                        continue;
90
                    }
91
                };
92
1
                let group = match Group::from_gid(nix::unistd::Gid::from_raw(gid)) {
93
1
                    Ok(Some(g)) => g,
94
                    Ok(None) => {
95
                        tracing::warn!(
96
                            "No group found for GID {} in denylist file at {:?} on line {}",
97
                            gid,
98
                            denylist_path,
99
                            line_number + 1
100
                        );
101
                        continue;
102
                    }
103
                    Err(err) => {
104
                        tracing::warn!(
105
                            "Failed to get group for GID {} in denylist file at {:?} on line {}: {}",
106
                            gid,
107
                            denylist_path,
108
                            line_number + 1,
109
                            err
110
                        );
111
                        continue;
112
                    }
113
                };
114

            
115
1
                groups.insert(group.gid.as_raw());
116
            }
117
2
            "group" => match Group::from_name(parts[1]) {
118
1
                Ok(Some(group)) => {
119
1
                    groups.insert(group.gid.as_raw());
120
1
                }
121
                Ok(None) => {
122
1
                    tracing::warn!(
123
                        "No group found for name '{}' in denylist file at {:?} on line {}",
124
                        parts[1],
125
                        denylist_path,
126
                        line_number + 1
127
                    );
128
1
                    continue;
129
                }
130
                Err(err) => {
131
                    tracing::warn!(
132
                        "Failed to get group for name '{}' in denylist file at {:?} on line {}: {}",
133
                        parts[1],
134
                        denylist_path,
135
                        line_number + 1,
136
                        err
137
                    );
138
                }
139
            },
140
            _ => {
141
                tracing::warn!(
142
                    "Invalid prefix '{}' in denylist file at {:?} on line {}: {}",
143
                    parts[0],
144
                    denylist_path,
145
                    line_number + 1,
146
                    line
147
                );
148
                continue;
149
            }
150
        }
151
    }
152

            
153
1
    groups
154
1
}
155

            
156
#[cfg(test)]
157
mod tests {
158
    use indoc::indoc;
159

            
160
    use super::*;
161

            
162
    #[test]
163
1
    fn test_parse_group_denylist() {
164
1
        let denylist_content = indoc! {"
165
1
            # Valid entries
166
1
            gid:0 # This is usually the 'root' group
167
1
            group:root # This is also the 'root' group, should deduplicate
168
1

            
169
1
            # Invalid entries
170
1
            invalid_line
171
1
            gid:not_a_number
172
1
            group:nonexistent_group
173
1
        "};
174

            
175
1
        let lines = denylist_content.lines();
176
1
        let group_denylist = parse_group_denylist(Path::new("test_denylist"), lines);
177

            
178
1
        assert_eq!(group_denylist.len(), 1);
179
1
        assert!(group_denylist.contains(&0));
180
1
    }
181
}