Skip to main content

kellnr_common/
account_name.rs

1//! Validation of the names of users, groups and auth tokens.
2
3use std::fmt;
4
5use sea_orm::Value;
6use serde::{Deserialize, Serialize};
7use thiserror::Error;
8use utoipa::ToSchema;
9
10/// Maximal length of user and group names in characters.
11pub const MAX_NAME_LEN: usize = 64;
12
13/// Maximal length of auth token names in characters.
14pub const MAX_TOKEN_NAME_LEN: usize = 128;
15
16#[derive(Debug, Error, PartialEq, Eq)]
17pub enum AccountNameError {
18    #[error("The name must not be empty")]
19    Empty,
20    #[error("The name must not be longer than {0} characters")]
21    TooLong(usize),
22    #[error("The name must not contain control characters")]
23    ControlCharacter,
24    #[error("The user name must not contain ':'")]
25    Colon,
26}
27
28/// Checks a new user name. A `:` is rejected because HTTP Basic auth splits
29/// the credentials at the first `:`, so such a user could not log in.
30pub fn validate_user_name(name: &str) -> Result<(), AccountNameError> {
31    validate(name, MAX_NAME_LEN)?;
32    if name.contains(':') {
33        return Err(AccountNameError::Colon);
34    }
35    Ok(())
36}
37
38/// Checks a new group name.
39pub fn validate_group_name(name: &str) -> Result<(), AccountNameError> {
40    validate(name, MAX_NAME_LEN)
41}
42
43/// Checks a new auth token name.
44pub fn validate_token_name(name: &str) -> Result<(), AccountNameError> {
45    validate(name, MAX_TOKEN_NAME_LEN)
46}
47
48/// Defines a name type. The names of existing accounts are taken as they
49/// are, as older versions did not check them, so only names of new accounts
50/// are validated. Deserializing only rejects an empty name.
51macro_rules! account_name_type {
52    ($(#[$doc:meta])* $name:ident, $new_fn:ident, $validate:path) => {
53        $(#[$doc])*
54        #[derive(
55            Default,
56            Debug,
57            Clone,
58            PartialEq,
59            Eq,
60            PartialOrd,
61            Ord,
62            Hash,
63            Serialize,
64            Deserialize,
65            ToSchema,
66        )]
67        #[serde(try_from = "String", into = "String")]
68        pub struct $name(String);
69
70        impl $name {
71            /// Validates the name of a new account.
72            pub fn $new_fn(name: impl Into<String>) -> Result<Self, AccountNameError> {
73                let name = name.into();
74                $validate(&name)?;
75                Ok(Self(name))
76            }
77
78            /// Takes the name of an existing account to look it up. Only an
79            /// empty name is rejected.
80            pub fn new(name: impl Into<String>) -> Result<Self, AccountNameError> {
81                let name = name.into();
82                if name.is_empty() {
83                    return Err(AccountNameError::Empty);
84                }
85                Ok(Self(name))
86            }
87
88            /// Takes a name without checks, for names stored in the database
89            /// or generated by Kellnr.
90            pub fn from_unchecked(name: String) -> Self {
91                Self(name)
92            }
93
94            pub fn as_str(&self) -> &str {
95                &self.0
96            }
97
98            pub fn into_inner(self) -> String {
99                self.0
100            }
101        }
102
103        impl TryFrom<String> for $name {
104            type Error = AccountNameError;
105
106            fn try_from(name: String) -> Result<Self, Self::Error> {
107                Self::new(name)
108            }
109        }
110
111        impl TryFrom<&str> for $name {
112            type Error = AccountNameError;
113
114            fn try_from(name: &str) -> Result<Self, Self::Error> {
115                Self::new(name)
116            }
117        }
118
119        impl From<$name> for String {
120            fn from(name: $name) -> Self {
121                name.0
122            }
123        }
124
125        impl From<&$name> for Value {
126            fn from(name: &$name) -> Self {
127                Value::String(Some(name.0.clone()))
128            }
129        }
130
131        impl AsRef<str> for $name {
132            fn as_ref(&self) -> &str {
133                &self.0
134            }
135        }
136
137        impl PartialEq<str> for $name {
138            fn eq(&self, other: &str) -> bool {
139                self.0 == other
140            }
141        }
142
143        impl PartialEq<&str> for $name {
144            fn eq(&self, other: &&str) -> bool {
145                self.0 == *other
146            }
147        }
148
149        impl PartialEq<$name> for str {
150            fn eq(&self, other: &$name) -> bool {
151                self == other.0
152            }
153        }
154
155        impl PartialEq<$name> for &str {
156            fn eq(&self, other: &$name) -> bool {
157                *self == other.0
158            }
159        }
160
161        impl fmt::Display for $name {
162            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163                f.write_str(&self.0)
164            }
165        }
166    };
167}
168
169account_name_type!(
170    /// The name of a user.
171    UserName,
172    for_new_user,
173    validate_user_name
174);
175
176account_name_type!(
177    /// The name of a group.
178    GroupName,
179    for_new_group,
180    validate_group_name
181);
182
183fn validate(name: &str, max_len: usize) -> Result<(), AccountNameError> {
184    if name.trim().is_empty() {
185        return Err(AccountNameError::Empty);
186    }
187    if name.chars().count() > max_len {
188        return Err(AccountNameError::TooLong(max_len));
189    }
190    if name.chars().any(char::is_control) {
191        return Err(AccountNameError::ControlCharacter);
192    }
193    Ok(())
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199
200    #[test]
201    fn valid_names_are_accepted() {
202        assert_eq!(validate_user_name("alice.smith"), Ok(()));
203        assert_eq!(validate_user_name(&"a".repeat(MAX_NAME_LEN)), Ok(()));
204        assert_eq!(validate_group_name("dev team"), Ok(()));
205        assert_eq!(validate_token_name("CI token: main"), Ok(()));
206    }
207
208    #[test]
209    fn empty_and_whitespace_names_are_rejected() {
210        assert_eq!(validate_user_name(""), Err(AccountNameError::Empty));
211        assert_eq!(validate_group_name("  "), Err(AccountNameError::Empty));
212        assert_eq!(validate_token_name("\t"), Err(AccountNameError::Empty));
213    }
214
215    #[test]
216    fn long_names_are_rejected() {
217        assert_eq!(
218            validate_user_name(&"a".repeat(MAX_NAME_LEN + 1)),
219            Err(AccountNameError::TooLong(MAX_NAME_LEN))
220        );
221        assert_eq!(
222            validate_token_name(&"a".repeat(MAX_TOKEN_NAME_LEN + 1)),
223            Err(AccountNameError::TooLong(MAX_TOKEN_NAME_LEN))
224        );
225    }
226
227    #[test]
228    fn control_characters_are_rejected() {
229        assert_eq!(
230            validate_user_name("ali\nce"),
231            Err(AccountNameError::ControlCharacter)
232        );
233    }
234
235    #[test]
236    fn existing_names_are_taken_as_they_are() {
237        let name = "a:b".repeat(30);
238        assert_eq!(name, UserName::new(name.as_str()).unwrap().as_str());
239        assert_eq!(name, GroupName::new(name.as_str()).unwrap().as_str());
240        assert_eq!(Err(AccountNameError::Empty), UserName::new(""));
241    }
242
243    #[test]
244    fn new_names_are_validated() {
245        assert_eq!(Err(AccountNameError::Colon), UserName::for_new_user("a:b"));
246        assert_eq!(
247            Err(AccountNameError::TooLong(MAX_NAME_LEN)),
248            GroupName::for_new_group("a".repeat(MAX_NAME_LEN + 1))
249        );
250        assert_eq!(UserName::for_new_user("alice").unwrap(), "alice");
251    }
252
253    #[test]
254    fn names_are_strings_in_json() {
255        let name: UserName = serde_json::from_str(r#""a:b""#).unwrap();
256        assert_eq!(name, "a:b");
257        assert_eq!(r#""a:b""#, serde_json::to_string(&name).unwrap());
258        assert!(serde_json::from_str::<GroupName>(r#""""#).is_err());
259    }
260
261    #[test]
262    fn colon_is_rejected_in_user_names_only() {
263        assert_eq!(validate_user_name("a:b"), Err(AccountNameError::Colon));
264        assert_eq!(validate_group_name("a:b"), Ok(()));
265    }
266}