1
use std::collections::HashSet;
2

            
3
use indoc::indoc;
4
use nix::{libc::gid_t, unistd::Group};
5
use serde::{Deserialize, Serialize};
6
use thiserror::Error;
7

            
8
use crate::core::{common::UnixUser, types::DbOrUser};
9

            
10
#[derive(Error, Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
11
pub enum NameValidationError {
12
    #[error("Name cannot be empty.")]
13
    EmptyString,
14

            
15
    #[error(
16
        "Name contains invalid characters. Only A-Z, a-z, 0-9, _ (underscore) and - (dash) are permitted."
17
    )]
18
    InvalidCharacters,
19

            
20
    #[error("Name is too long. Maximum length is 64 characters.")]
21
    TooLong,
22
}
23

            
24
impl NameValidationError {
25
    #[must_use]
26
    pub fn to_error_message(self, db_or_user: &DbOrUser) -> String {
27
        match self {
28
            NameValidationError::EmptyString => {
29
                format!("{} name can not be empty.", db_or_user.capitalized_noun())
30
            }
31
            NameValidationError::TooLong => format!(
32
                "{} is too long, maximum length is 64 characters.",
33
                db_or_user.capitalized_noun()
34
            ),
35
            NameValidationError::InvalidCharacters => format!(
36
                indoc! {r"
37
                  Invalid characters in {} name: '{}', only A-Z, a-z, 0-9, _ (underscore) and - (dash) are permitted.
38
                "},
39
                db_or_user.lowercased_noun(),
40
                db_or_user.name(),
41
            ),
42
        }
43
    }
44

            
45
    #[must_use]
46
    pub fn error_type(&self) -> &'static str {
47
        match self {
48
            NameValidationError::EmptyString => "empty-string",
49
            NameValidationError::InvalidCharacters => "invalid-characters",
50
            NameValidationError::TooLong => "too-long",
51
        }
52
    }
53
}
54

            
55
#[derive(Error, Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
56
pub enum AuthorizationError {
57
    #[error("Illegal prefix, user is not authorized to manage this resource")]
58
    IllegalPrefix,
59

            
60
    // TODO: I don't think this should ever happen?
61
    #[error("Name cannot be empty")]
62
    StringEmpty,
63

            
64
    #[error("Group was found in denylist")]
65
    DenylistError,
66
}
67

            
68
impl AuthorizationError {
69
    #[must_use]
70
    pub fn to_error_message(self, db_or_user: &DbOrUser) -> String {
71
        match self {
72
            AuthorizationError::IllegalPrefix => format!(
73
                "Illegal {} name prefix: you are not allowed to manage databases or users prefixed with '{}'",
74
                db_or_user.lowercased_noun(),
75
                db_or_user.prefix(),
76
            )
77
            .to_owned(),
78
            // TODO: This error message could be clearer
79
            AuthorizationError::StringEmpty => {
80
                format!("{} name can not be empty.", db_or_user.capitalized_noun())
81
            }
82
            AuthorizationError::DenylistError => {
83
                format!("'{}' is denied by the group denylist", db_or_user.name())
84
            }
85
        }
86
    }
87

            
88
    #[must_use]
89
    pub fn error_type(&self) -> &'static str {
90
        match self {
91
            AuthorizationError::IllegalPrefix => "illegal-prefix",
92
            AuthorizationError::StringEmpty => "string-empty",
93
            AuthorizationError::DenylistError => "denylist-error",
94
        }
95
    }
96
}
97

            
98
#[derive(Error, Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
99
pub enum ValidationError {
100
    #[error("Name validation error: {0}")]
101
    NameValidationError(NameValidationError),
102

            
103
    #[error("Authorization error: {0}")]
104
    AuthorizationError(AuthorizationError),
105
    // AuthorizationHandlerError(String),
106
}
107

            
108
impl ValidationError {
109
    #[must_use]
110
    pub fn to_error_message(&self, db_or_user: &DbOrUser) -> String {
111
        match self {
112
            ValidationError::NameValidationError(err) => err.to_error_message(db_or_user),
113
            ValidationError::AuthorizationError(err) => err.to_error_message(db_or_user),
114
            // AuthorizationError::AuthorizationHandlerError(msg) => {
115
            //     format!(
116
            //         "Authorization handler error for '{}': {}",
117
            //         db_or_user.name(),
118
            //         msg
119
            //     )
120
            // }
121
        }
122
    }
123

            
124
    #[must_use]
125
    pub fn error_type(&self) -> String {
126
        match self {
127
            ValidationError::NameValidationError(err) => {
128
                format!("name-validation-error/{}", err.error_type())
129
            }
130
            ValidationError::AuthorizationError(err) => {
131
                format!("authorization-error/{}", err.error_type())
132
            } // AuthorizationError::AuthorizationHandlerError(_) => {
133
              //     "authorization-handler-error".to_string()
134
              // }
135
        }
136
    }
137
}
138

            
139
pub type GroupDenylist = HashSet<gid_t>;
140

            
141
const MAX_NAME_LENGTH: usize = 64;
142

            
143
35
pub fn validate_name(name: &str) -> Result<(), NameValidationError> {
144
35
    if name.is_empty() {
145
1
        Err(NameValidationError::EmptyString)
146
34
    } else if name.len() > MAX_NAME_LENGTH {
147
1
        Err(NameValidationError::TooLong)
148
33
    } else if !name
149
33
        .chars()
150
157
        .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
151
    {
152
29
        Err(NameValidationError::InvalidCharacters)
153
    } else {
154
4
        Ok(())
155
    }
156
35
}
157

            
158
pub fn validate_authorization_by_unix_user(
159
    name: &str,
160
    user: &UnixUser,
161
) -> Result<(), AuthorizationError> {
162
    let prefixes = std::iter::once(user.username.clone())
163
        .chain(user.groups.iter().cloned())
164
        .collect::<Vec<String>>();
165

            
166
    validate_authorization_by_prefixes(name, &prefixes)
167
}
168

            
169
/// Core logic for validating the ownership of a database name.
170
/// This function checks if the given name matches any of the given prefixes.
171
/// These prefixes will in most cases be the user's unix username and any
172
/// unix groups the user is a member of.
173
6
pub fn validate_authorization_by_prefixes(
174
6
    name: &str,
175
6
    prefixes: &[String],
176
6
) -> Result<(), AuthorizationError> {
177
6
    if name.is_empty() {
178
1
        return Err(AuthorizationError::StringEmpty);
179
5
    }
180

            
181
5
    if prefixes
182
5
        .iter()
183
10
        .filter(|p| name.starts_with(&((*p).clone() + "_")))
184
5
        .collect::<Vec<_>>()
185
5
        .is_empty()
186
    {
187
1
        return Err(AuthorizationError::IllegalPrefix);
188
4
    }
189

            
190
4
    Ok(())
191
6
}
192

            
193
pub fn validate_authorization_by_group_denylist(
194
    name: &str,
195
    user: &UnixUser,
196
    group_denylist: &GroupDenylist,
197
) -> Result<(), AuthorizationError> {
198
    // NOTE: if the username matches, we allow it regardless of denylist
199
    if user.username == name {
200
        return Ok(());
201
    }
202

            
203
    let user_group = Group::from_name(name)
204
        .ok()
205
        .flatten()
206
        .map(|g| g.gid.as_raw());
207

            
208
    if let Some(gid) = user_group
209
        && group_denylist.contains(&gid)
210
    {
211
        Err(AuthorizationError::DenylistError)
212
    } else {
213
        Ok(())
214
    }
215
}
216

            
217
pub fn validate_db_or_user_request(
218
    db_or_user: &DbOrUser,
219
    unix_user: &UnixUser,
220
    group_denylist: &GroupDenylist,
221
) -> Result<(), ValidationError> {
222
    validate_name(db_or_user.name()).map_err(ValidationError::NameValidationError)?;
223

            
224
    validate_authorization_by_unix_user(db_or_user.name(), unix_user)
225
        .map_err(ValidationError::AuthorizationError)?;
226

            
227
    validate_authorization_by_group_denylist(db_or_user.name(), unix_user, group_denylist)
228
        .map_err(ValidationError::AuthorizationError)?;
229

            
230
    Ok(())
231
}
232

            
233
#[cfg(test)]
234
mod tests {
235
    use super::*;
236

            
237
    #[test]
238
1
    fn test_validate_name() {
239
1
        assert_eq!(validate_name(""), Err(NameValidationError::EmptyString));
240
1
        assert_eq!(validate_name("abcdefghijklmnopqrstuvwxyz"), Ok(()));
241
1
        assert_eq!(validate_name("ABCDEFGHIJKLMNOPQRSTUVWXYZ"), Ok(()));
242
1
        assert_eq!(validate_name("0123456789_-"), Ok(()));
243

            
244
29
        for c in "\n\t\r !@#$%^&*()+=[]{}|;:,.<>?/".chars() {
245
29
            assert_eq!(
246
29
                validate_name(&c.to_string()),
247
                Err(NameValidationError::InvalidCharacters)
248
            );
249
        }
250

            
251
1
        assert_eq!(validate_name(&"a".repeat(MAX_NAME_LENGTH)), Ok(()));
252

            
253
1
        assert_eq!(
254
1
            validate_name(&"a".repeat(MAX_NAME_LENGTH + 1)),
255
            Err(NameValidationError::TooLong)
256
        );
257
1
    }
258

            
259
    #[test]
260
1
    fn test_validate_authorization_by_prefixes() {
261
1
        let prefixes = vec!["user".to_string(), "group".to_string()];
262

            
263
1
        assert_eq!(
264
1
            validate_authorization_by_prefixes("", &prefixes),
265
            Err(AuthorizationError::StringEmpty)
266
        );
267

            
268
1
        assert_eq!(
269
1
            validate_authorization_by_prefixes("user_testdb", &prefixes),
270
            Ok(())
271
        );
272
1
        assert_eq!(
273
1
            validate_authorization_by_prefixes("group_testdb", &prefixes),
274
            Ok(())
275
        );
276
1
        assert_eq!(
277
1
            validate_authorization_by_prefixes("group_test_db", &prefixes),
278
            Ok(())
279
        );
280
1
        assert_eq!(
281
1
            validate_authorization_by_prefixes("group_test-db", &prefixes),
282
            Ok(())
283
        );
284

            
285
1
        assert_eq!(
286
1
            validate_authorization_by_prefixes("nonexistent_testdb", &prefixes),
287
            Err(AuthorizationError::IllegalPrefix)
288
        );
289
1
    }
290
}