use std::collections::HashSet; use indoc::indoc; use nix::{libc::gid_t, unistd::Group}; use serde::{Deserialize, Serialize}; use thiserror::Error; use crate::core::{common::UnixUser, types::DbOrUser}; #[derive(Error, Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] pub enum NameValidationError { #[error("Name cannot be empty.")] EmptyString, #[error( "Name contains invalid characters. Only A-Z, a-z, 0-9, _ (underscore) and - (dash) are permitted." )] InvalidCharacters, #[error("Name is too long. Maximum length is 64 characters.")] TooLong, } impl NameValidationError { #[must_use] pub fn to_error_message(self, db_or_user: &DbOrUser) -> String { match self { NameValidationError::EmptyString => { format!("{} name can not be empty.", db_or_user.capitalized_noun()) } NameValidationError::TooLong => format!( "{} is too long, maximum length is 64 characters.", db_or_user.capitalized_noun() ), NameValidationError::InvalidCharacters => format!( indoc! {r" Invalid characters in {} name: '{}', only A-Z, a-z, 0-9, _ (underscore) and - (dash) are permitted. "}, db_or_user.lowercased_noun(), db_or_user.name(), ), } } #[must_use] pub fn error_type(&self) -> &'static str { match self { NameValidationError::EmptyString => "empty-string", NameValidationError::InvalidCharacters => "invalid-characters", NameValidationError::TooLong => "too-long", } } } #[derive(Error, Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] pub enum AuthorizationError { #[error("Illegal prefix, user is not authorized to manage this resource")] IllegalPrefix, // TODO: I don't think this should ever happen? #[error("Name cannot be empty")] StringEmpty, #[error("Group was found in denylist")] DenylistError, } impl AuthorizationError { #[must_use] pub fn to_error_message(self, db_or_user: &DbOrUser) -> String { match self { AuthorizationError::IllegalPrefix => format!( "Illegal {} name prefix: you are not allowed to manage databases or users prefixed with '{}'", db_or_user.lowercased_noun(), db_or_user.prefix(), ) .to_owned(), // TODO: This error message could be clearer AuthorizationError::StringEmpty => { format!("{} name can not be empty.", db_or_user.capitalized_noun()) } AuthorizationError::DenylistError => { format!("'{}' is denied by the group denylist", db_or_user.name()) } } } #[must_use] pub fn error_type(&self) -> &'static str { match self { AuthorizationError::IllegalPrefix => "illegal-prefix", AuthorizationError::StringEmpty => "string-empty", AuthorizationError::DenylistError => "denylist-error", } } } #[derive(Error, Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] pub enum ValidationError { #[error("Name validation error: {0}")] NameValidationError(NameValidationError), #[error("Authorization error: {0}")] AuthorizationError(AuthorizationError), // AuthorizationHandlerError(String), } impl ValidationError { #[must_use] pub fn to_error_message(&self, db_or_user: &DbOrUser) -> String { match self { ValidationError::NameValidationError(err) => err.to_error_message(db_or_user), ValidationError::AuthorizationError(err) => err.to_error_message(db_or_user), // AuthorizationError::AuthorizationHandlerError(msg) => { // format!( // "Authorization handler error for '{}': {}", // db_or_user.name(), // msg // ) // } } } #[must_use] pub fn error_type(&self) -> String { match self { ValidationError::NameValidationError(err) => { format!("name-validation-error/{}", err.error_type()) } ValidationError::AuthorizationError(err) => { format!("authorization-error/{}", err.error_type()) } // AuthorizationError::AuthorizationHandlerError(_) => { // "authorization-handler-error".to_string() // } } } } pub type GroupDenylist = HashSet; const MAX_NAME_LENGTH: usize = 64; pub fn validate_name(name: &str) -> Result<(), NameValidationError> { if name.is_empty() { Err(NameValidationError::EmptyString) } else if name.len() > MAX_NAME_LENGTH { Err(NameValidationError::TooLong) } else if !name .chars() .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-') { Err(NameValidationError::InvalidCharacters) } else { Ok(()) } } pub fn validate_authorization_by_unix_user( name: &str, user: &UnixUser, ) -> Result<(), AuthorizationError> { let prefixes = std::iter::once(user.username.clone()) .chain(user.groups.iter().cloned()) .collect::>(); validate_authorization_by_prefixes(name, &prefixes) } /// Core logic for validating the ownership of a database name. /// This function checks if the given name matches any of the given prefixes. /// These prefixes will in most cases be the user's unix username and any /// unix groups the user is a member of. pub fn validate_authorization_by_prefixes( name: &str, prefixes: &[String], ) -> Result<(), AuthorizationError> { if name.is_empty() { return Err(AuthorizationError::StringEmpty); } if prefixes .iter() .filter(|p| name.starts_with(&((*p).clone() + "_"))) .collect::>() .is_empty() { return Err(AuthorizationError::IllegalPrefix); } Ok(()) } pub fn validate_authorization_by_group_denylist( name: &str, user: &UnixUser, group_denylist: &GroupDenylist, ) -> Result<(), AuthorizationError> { // NOTE: if the username matches, we allow it regardless of denylist if user.username == name { return Ok(()); } let user_group = Group::from_name(name) .ok() .flatten() .map(|g| g.gid.as_raw()); if let Some(gid) = user_group && group_denylist.contains(&gid) { Err(AuthorizationError::DenylistError) } else { Ok(()) } } pub fn validate_db_or_user_request( db_or_user: &DbOrUser, unix_user: &UnixUser, group_denylist: &GroupDenylist, ) -> Result<(), ValidationError> { validate_name(db_or_user.name()).map_err(ValidationError::NameValidationError)?; validate_authorization_by_unix_user(db_or_user.name(), unix_user) .map_err(ValidationError::AuthorizationError)?; validate_authorization_by_group_denylist(db_or_user.name(), unix_user, group_denylist) .map_err(ValidationError::AuthorizationError)?; Ok(()) } #[cfg(test)] mod tests { use super::*; #[test] fn test_validate_name() { assert_eq!(validate_name(""), Err(NameValidationError::EmptyString)); assert_eq!(validate_name("abcdefghijklmnopqrstuvwxyz"), Ok(())); assert_eq!(validate_name("ABCDEFGHIJKLMNOPQRSTUVWXYZ"), Ok(())); assert_eq!(validate_name("0123456789_-"), Ok(())); for c in "\n\t\r !@#$%^&*()+=[]{}|;:,.<>?/".chars() { assert_eq!( validate_name(&c.to_string()), Err(NameValidationError::InvalidCharacters) ); } assert_eq!(validate_name(&"a".repeat(MAX_NAME_LENGTH)), Ok(())); assert_eq!( validate_name(&"a".repeat(MAX_NAME_LENGTH + 1)), Err(NameValidationError::TooLong) ); } #[test] fn test_validate_authorization_by_prefixes() { let prefixes = vec!["user".to_string(), "group".to_string()]; assert_eq!( validate_authorization_by_prefixes("", &prefixes), Err(AuthorizationError::StringEmpty) ); assert_eq!( validate_authorization_by_prefixes("user_testdb", &prefixes), Ok(()) ); assert_eq!( validate_authorization_by_prefixes("group_testdb", &prefixes), Ok(()) ); assert_eq!( validate_authorization_by_prefixes("group_test_db", &prefixes), Ok(()) ); assert_eq!( validate_authorization_by_prefixes("group_test-db", &prefixes), Ok(()) ); assert_eq!( validate_authorization_by_prefixes("nonexistent_testdb", &prefixes), Err(AuthorizationError::IllegalPrefix) ); } }