1
use std::{collections::HashSet, sync::OnceLock};
2

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            
140
#[derive(Debug, Clone, PartialEq, Eq)]
141
pub struct GroupNamePattern(String);
142

            
143
impl GroupNamePattern {
144
19
    pub fn new(pattern: impl Into<String>) -> Self {
145
19
        Self(pattern.into()).normalize()
146
19
    }
147

            
148
    /// Collapses runs of consecutive `*`/`?` wildcards into a canonical form.
149
    ///
150
    /// - `***` -> `*`
151
    /// - `*?*?` -> `??*`
152
19
    fn normalize(&self) -> Self {
153
19
        let mut result = String::with_capacity(self.0.len());
154
19
        let mut chars = self.0.chars().peekable();
155

            
156
60
        while let Some(c) = chars.next() {
157
41
            if c == '*' || c == '?' {
158
17
                let mut question_marks = usize::from(c == '?');
159
17
                let mut has_star = c == '*';
160

            
161
33
                while let Some(&next) = chars.peek() {
162
18
                    match next {
163
6
                        '?' => question_marks += 1,
164
10
                        '*' => has_star = true,
165
2
                        _ => break,
166
                    }
167
16
                    chars.next();
168
                }
169

            
170
17
                result.extend(std::iter::repeat_n('?', question_marks));
171
17
                if has_star {
172
11
                    result.push('*');
173
11
                }
174
24
            } else {
175
24
                result.push(c);
176
24
            }
177
        }
178

            
179
19
        Self(result)
180
19
    }
181

            
182
19
    pub fn to_regex(&self) -> Result<Regex, regex::Error> {
183
19
        let mut regex_str = String::from("^");
184
44
        for c in self.0.chars() {
185
44
            match c {
186
9
                '*' => regex_str.push_str(".*"),
187
13
                '?' => regex_str.push('.'),
188
22
                _ => regex_str.push_str(&regex::escape(&c.to_string())),
189
            }
190
        }
191
19
        regex_str.push('$');
192
19
        Regex::new(&regex_str)
193
19
    }
194
}
195

            
196
#[derive(Debug, Default)]
197
pub struct GroupDenylist {
198
    gids: HashSet<gid_t>,
199
    name_patterns: Vec<GroupNamePattern>,
200
    compiled_name_patterns: OnceLock<Vec<Regex>>,
201
}
202

            
203
impl Clone for GroupDenylist {
204
    fn clone(&self) -> Self {
205
        Self {
206
            gids: self.gids.clone(),
207
            name_patterns: self.name_patterns.clone(),
208
            compiled_name_patterns: OnceLock::new(),
209
        }
210
    }
211
}
212

            
213
impl GroupDenylist {
214
6
    pub fn new() -> Self {
215
6
        Self::default()
216
6
    }
217

            
218
2
    pub fn insert_gid(&mut self, gid: gid_t) {
219
2
        self.gids.insert(gid);
220
2
    }
221

            
222
4
    pub fn insert_name_pattern(&mut self, pattern: GroupNamePattern) {
223
4
        self.name_patterns.push(pattern);
224
4
        self.compiled_name_patterns = OnceLock::new();
225
4
    }
226

            
227
1
    pub fn is_empty(&self) -> bool {
228
1
        self.gids.is_empty() && self.name_patterns.is_empty()
229
1
    }
230

            
231
4
    pub fn len(&self) -> usize {
232
4
        self.gids.len() + self.name_patterns.len()
233
4
    }
234

            
235
12
    fn compiled_name_pattern_regexes(&self) -> &[Regex] {
236
12
        self.compiled_name_patterns.get_or_init(|| {
237
3
            self.name_patterns
238
3
                .iter()
239
4
                .filter_map(|pattern| pattern.to_regex().ok())
240
3
                .collect()
241
3
        })
242
12
    }
243

            
244
13
    pub fn matches(&self, group: &Group) -> bool {
245
13
        self.gids.contains(&group.gid.as_raw())
246
12
            || self
247
12
                .compiled_name_pattern_regexes()
248
12
                .iter()
249
16
                .any(|regex| regex.is_match(&group.name))
250
13
    }
251
}
252

            
253
const MAX_NAME_LENGTH: usize = 64;
254

            
255
35
pub fn validate_name(name: &str) -> Result<(), NameValidationError> {
256
35
    if name.is_empty() {
257
1
        Err(NameValidationError::EmptyString)
258
34
    } else if name.len() > MAX_NAME_LENGTH {
259
1
        Err(NameValidationError::TooLong)
260
33
    } else if !name
261
33
        .chars()
262
157
        .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
263
    {
264
29
        Err(NameValidationError::InvalidCharacters)
265
    } else {
266
4
        Ok(())
267
    }
268
35
}
269

            
270
pub fn validate_authorization_by_unix_user(
271
    name: &str,
272
    user: &UnixUser,
273
) -> Result<(), AuthorizationError> {
274
    let prefixes = std::iter::once(user.username.clone())
275
        .chain(user.groups.iter().cloned())
276
        .collect::<Vec<String>>();
277

            
278
    validate_authorization_by_prefixes(name, &prefixes)
279
}
280

            
281
/// Core logic for validating the ownership of a database name.
282
/// This function checks if the given name matches any of the given prefixes.
283
/// These prefixes will in most cases be the user's unix username and any
284
/// unix groups the user is a member of.
285
6
pub fn validate_authorization_by_prefixes(
286
6
    name: &str,
287
6
    prefixes: &[String],
288
6
) -> Result<(), AuthorizationError> {
289
6
    if name.is_empty() {
290
1
        return Err(AuthorizationError::StringEmpty);
291
5
    }
292

            
293
5
    if prefixes
294
5
        .iter()
295
10
        .filter(|p| name.starts_with(&((*p).clone() + "_")))
296
5
        .collect::<Vec<_>>()
297
5
        .is_empty()
298
    {
299
1
        return Err(AuthorizationError::IllegalPrefix);
300
4
    }
301

            
302
4
    Ok(())
303
6
}
304

            
305
pub fn validate_authorization_by_group_denylist(
306
    name: &str,
307
    user: &UnixUser,
308
    group_denylist: &GroupDenylist,
309
) -> Result<(), AuthorizationError> {
310
    // NOTE: if the username matches, we allow it regardless of denylist
311
    if user.username == name {
312
        return Ok(());
313
    }
314

            
315
    let user_group = Group::from_name(name).ok().flatten();
316

            
317
    if let Some(group) = user_group
318
        && group_denylist.matches(&group)
319
    {
320
        Err(AuthorizationError::DenylistError)
321
    } else {
322
        Ok(())
323
    }
324
}
325

            
326
pub fn validate_db_or_user_request(
327
    db_or_user: &DbOrUser,
328
    unix_user: &UnixUser,
329
    group_denylist: &GroupDenylist,
330
) -> Result<(), ValidationError> {
331
    validate_name(db_or_user.name()).map_err(ValidationError::NameValidationError)?;
332

            
333
    validate_authorization_by_unix_user(db_or_user.name(), unix_user)
334
        .map_err(ValidationError::AuthorizationError)?;
335

            
336
    validate_authorization_by_group_denylist(db_or_user.name(), unix_user, group_denylist)
337
        .map_err(ValidationError::AuthorizationError)?;
338

            
339
    Ok(())
340
}
341

            
342
#[cfg(test)]
343
mod tests {
344
    use super::*;
345

            
346
    #[test]
347
1
    fn test_validate_name() {
348
1
        assert_eq!(validate_name(""), Err(NameValidationError::EmptyString));
349
1
        assert_eq!(validate_name("abcdefghijklmnopqrstuvwxyz"), Ok(()));
350
1
        assert_eq!(validate_name("ABCDEFGHIJKLMNOPQRSTUVWXYZ"), Ok(()));
351
1
        assert_eq!(validate_name("0123456789_-"), Ok(()));
352

            
353
29
        for c in "\n\t\r !@#$%^&*()+=[]{}|;:,.<>?/".chars() {
354
29
            assert_eq!(
355
29
                validate_name(&c.to_string()),
356
                Err(NameValidationError::InvalidCharacters)
357
            );
358
        }
359

            
360
1
        assert_eq!(validate_name(&"a".repeat(MAX_NAME_LENGTH)), Ok(()));
361

            
362
1
        assert_eq!(
363
1
            validate_name(&"a".repeat(MAX_NAME_LENGTH + 1)),
364
            Err(NameValidationError::TooLong)
365
        );
366
1
    }
367

            
368
    #[test]
369
1
    fn test_group_name_pattern_normalize() {
370
1
        assert_eq!(GroupNamePattern::new("*").0, "*");
371
1
        assert_eq!(GroupNamePattern::new("?").0, "?");
372
1
        assert_eq!(GroupNamePattern::new("**").0, "*");
373
1
        assert_eq!(GroupNamePattern::new("***").0, "*");
374
1
        assert_eq!(GroupNamePattern::new("??").0, "??");
375
1
        assert_eq!(GroupNamePattern::new("?*").0, "?*");
376
1
        assert_eq!(GroupNamePattern::new("*?").0, "?*");
377
1
        assert_eq!(GroupNamePattern::new("*?*?*").0, "??*");
378
1
        assert_eq!(GroupNamePattern::new("admin**?*svc").0, "admin?*svc");
379
1
        assert_eq!(GroupNamePattern::new("admin").0, "admin");
380
1
        assert_eq!(GroupNamePattern::new("").0, "");
381
1
    }
382

            
383
    #[test]
384
1
    fn test_group_name_pattern_only_wildcards() {
385
1
        let star = GroupNamePattern::new("*");
386
1
        assert!(star.to_regex().unwrap().is_match(""));
387
1
        assert!(star.to_regex().unwrap().is_match("a"));
388
1
        assert!(star.to_regex().unwrap().is_match("anything"));
389

            
390
1
        let question_mark = GroupNamePattern::new("?");
391
1
        assert!(!question_mark.to_regex().unwrap().is_match(""));
392
1
        assert!(question_mark.to_regex().unwrap().is_match("a"));
393
1
        assert!(!question_mark.to_regex().unwrap().is_match("ab"));
394

            
395
1
        let double_question_mark = GroupNamePattern::new("??");
396
1
        assert!(!double_question_mark.to_regex().unwrap().is_match("a"));
397
1
        assert!(double_question_mark.to_regex().unwrap().is_match("ab"));
398
1
        assert!(!double_question_mark.to_regex().unwrap().is_match("abc"));
399

            
400
1
        let collapsed_stars = GroupNamePattern::new("***");
401
1
        assert!(collapsed_stars.to_regex().unwrap().is_match(""));
402
1
        assert!(collapsed_stars.to_regex().unwrap().is_match("anything"));
403
1
    }
404

            
405
    #[test]
406
1
    fn test_validate_authorization_by_prefixes() {
407
1
        let prefixes = vec!["user".to_string(), "group".to_string()];
408

            
409
1
        assert_eq!(
410
1
            validate_authorization_by_prefixes("", &prefixes),
411
            Err(AuthorizationError::StringEmpty)
412
        );
413

            
414
1
        assert_eq!(
415
1
            validate_authorization_by_prefixes("user_testdb", &prefixes),
416
            Ok(())
417
        );
418
1
        assert_eq!(
419
1
            validate_authorization_by_prefixes("group_testdb", &prefixes),
420
            Ok(())
421
        );
422
1
        assert_eq!(
423
1
            validate_authorization_by_prefixes("group_test_db", &prefixes),
424
            Ok(())
425
        );
426
1
        assert_eq!(
427
1
            validate_authorization_by_prefixes("group_test-db", &prefixes),
428
            Ok(())
429
        );
430

            
431
1
        assert_eq!(
432
1
            validate_authorization_by_prefixes("nonexistent_testdb", &prefixes),
433
            Err(AuthorizationError::IllegalPrefix)
434
        );
435
1
    }
436
}