Skip to content

Commit df9dd93

Browse files
chore: add validation to user and role creation (#1435)
below validations are added to both user and role names at creation - 1. the length should be between 3-64 characters 2. the name should not be any of these - admin, user, role, null, undefined, none, empty, password, username 3. first and the last character must be alphanumeric 4. allow any of these special characters - `_, -,.` 5. no two consecutive characters should be special character
1 parent 1a674f9 commit df9dd93

File tree

5 files changed

+92
-26
lines changed

5 files changed

+92
-26
lines changed

src/handlers/http/modal/query/querier_rbac.rs

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,9 @@ pub async fn post_user(
4444
body: Option<web::Json<serde_json::Value>>,
4545
) -> Result<impl Responder, RBACError> {
4646
let username = username.into_inner();
47-
47+
validator::user_role_name(&username)?;
4848
let mut metadata = get_metadata().await?;
4949

50-
validator::user_name(&username)?;
5150
let user_roles: HashSet<String> = if let Some(body) = body {
5251
serde_json::from_value(body.into_inner())?
5352
} else {
@@ -173,17 +172,17 @@ pub async fn add_roles_to_user(
173172
return Err(RBACError::UserDoesNotExist);
174173
};
175174

176-
let mut non_existant_roles = Vec::new();
175+
let mut non_existent_roles = Vec::new();
177176

178177
// check if the role exists
179178
roles_to_add.iter().for_each(|r| {
180179
if roles().get(r).is_none() {
181-
non_existant_roles.push(r.clone());
180+
non_existent_roles.push(r.clone());
182181
}
183182
});
184183

185-
if !non_existant_roles.is_empty() {
186-
return Err(RBACError::RolesDoNotExist(non_existant_roles));
184+
if !non_existent_roles.is_empty() {
185+
return Err(RBACError::RolesDoNotExist(non_existent_roles));
187186
}
188187

189188
// update parseable.json first
@@ -229,17 +228,17 @@ pub async fn remove_roles_from_user(
229228
return Err(RBACError::UserDoesNotExist);
230229
};
231230

232-
let mut non_existant_roles = Vec::new();
231+
let mut non_existent_roles = Vec::new();
233232

234233
// check if the role exists
235234
roles_to_remove.iter().for_each(|r| {
236235
if roles().get(r).is_none() {
237-
non_existant_roles.push(r.clone());
236+
non_existent_roles.push(r.clone());
238237
}
239238
});
240239

241-
if !non_existant_roles.is_empty() {
242-
return Err(RBACError::RolesDoNotExist(non_existant_roles));
240+
if !non_existent_roles.is_empty() {
241+
return Err(RBACError::RolesDoNotExist(non_existent_roles));
243242
}
244243

245244
// check for role not present with user

src/handlers/http/modal/query/querier_role.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ use crate::{
3333
map::{mut_roles, mut_sessions, read_user_groups, users},
3434
role::model::DefaultPrivilege,
3535
},
36+
validator,
3637
};
3738

3839
// Handler for PUT /api/v1/role/{name}
@@ -42,6 +43,8 @@ pub async fn put(
4243
Json(privileges): Json<Vec<DefaultPrivilege>>,
4344
) -> Result<impl Responder, RoleError> {
4445
let name = name.into_inner();
46+
// validate the role name
47+
validator::user_role_name(&name).map_err(RoleError::ValidationError)?;
4548
let mut metadata = get_metadata().await?;
4649
metadata.roles.insert(name.clone(), privileges.clone());
4750

src/handlers/http/rbac.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,9 @@ pub async fn post_user(
101101
body: Option<web::Json<serde_json::Value>>,
102102
) -> Result<impl Responder, RBACError> {
103103
let username = username.into_inner();
104-
104+
validator::user_role_name(&username)?;
105105
let mut metadata = get_metadata().await?;
106106

107-
validator::user_name(&username)?;
108107
let user_roles: HashSet<String> = if let Some(body) = body {
109108
serde_json::from_value(body.into_inner())?
110109
} else {
@@ -121,7 +120,7 @@ pub async fn post_user(
121120
return Err(RBACError::RolesDoNotExist(non_existent_roles));
122121
}
123122
let _guard = UPDATE_LOCK.lock().await;
124-
if Users.contains(&username) && Users.contains(&username)
123+
if Users.contains(&username)
125124
|| metadata.users.iter().any(|user| match &user.ty {
126125
UserType::Native(basic) => basic.username == username,
127126
UserType::OAuth(_) => false, // OAuth users should be created differently

src/handlers/http/role.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ use crate::{
3232
role::model::DefaultPrivilege,
3333
},
3434
storage::{self, ObjectStorageError, StorageMetadata},
35+
validator::{self, error::UsernameValidationError},
3536
};
3637

3738
// Handler for PUT /api/v1/role/{name}
@@ -41,6 +42,8 @@ pub async fn put(
4142
Json(privileges): Json<Vec<DefaultPrivilege>>,
4243
) -> Result<impl Responder, RoleError> {
4344
let name = name.into_inner();
45+
// validate the role name
46+
validator::user_role_name(&name).map_err(RoleError::ValidationError)?;
4447
let mut metadata = get_metadata().await?;
4548
metadata.roles.insert(name.clone(), privileges.clone());
4649

@@ -168,6 +171,8 @@ pub enum RoleError {
168171
SerdeError(#[from] serde_json::Error),
169172
#[error("Network Error: {0}")]
170173
Network(#[from] reqwest::Error),
174+
#[error("Validation Error: {0}")]
175+
ValidationError(#[from] UsernameValidationError),
171176
}
172177

173178
impl actix_web::ResponseError for RoleError {
@@ -178,6 +183,7 @@ impl actix_web::ResponseError for RoleError {
178183
Self::Anyhow(_) => StatusCode::INTERNAL_SERVER_ERROR,
179184
Self::SerdeError(_) => StatusCode::BAD_REQUEST,
180185
Self::Network(_) => StatusCode::BAD_GATEWAY,
186+
Self::ValidationError(_) => StatusCode::BAD_REQUEST,
181187
}
182188
}
183189

src/validator.rs

Lines changed: 72 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@
1616
*
1717
*/
1818

19+
use std::collections::HashSet;
20+
1921
use error::HotTierValidationError;
22+
use once_cell::sync::Lazy;
2023

2124
use self::error::{StreamNameValidationError, UsernameValidationError};
2225
use crate::hottier::MIN_STREAM_HOT_TIER_SIZE_BYTES;
@@ -67,21 +70,65 @@ pub fn stream_name(
6770
Ok(())
6871
}
6972

70-
// validate if username is valid
71-
// username should be between 3 and 64 characters long
72-
// username should contain only alphanumeric characters or underscores
73-
// username should be lowercase
74-
pub fn user_name(username: &str) -> Result<(), UsernameValidationError> {
75-
// Check if the username meets the required criteria
76-
if username.len() < 3 || username.len() > 64 {
73+
static RESERVED_NAMES: Lazy<HashSet<&'static str>> = Lazy::new(|| {
74+
[
75+
"admin",
76+
"user",
77+
"role",
78+
"null",
79+
"undefined",
80+
"none",
81+
"empty",
82+
"password",
83+
"username",
84+
]
85+
.into_iter()
86+
.collect()
87+
});
88+
89+
pub fn user_role_name(name: &str) -> Result<(), UsernameValidationError> {
90+
// Normalize username to lowercase for validation
91+
let name = name.to_lowercase();
92+
93+
// Check length constraints
94+
if name.len() < 3 || name.len() > 64 {
7795
return Err(UsernameValidationError::InvalidLength);
7896
}
79-
// Username should contain only alphanumeric characters or underscores
80-
if !username
81-
.chars()
82-
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
97+
98+
// Check if name is reserved
99+
if RESERVED_NAMES.contains(name.as_str()) {
100+
return Err(UsernameValidationError::ReservedName);
101+
}
102+
103+
let chars: Vec<char> = name.chars().collect();
104+
105+
// Check last character (must be alphanumeric)
106+
if let Some(last_char) = chars.last()
107+
&& !last_char.is_ascii_alphanumeric()
83108
{
84-
return Err(UsernameValidationError::SpecialChar);
109+
return Err(UsernameValidationError::InvalidEndChar);
110+
}
111+
112+
// Check all characters and consecutive special chars
113+
let mut prev_was_special = false;
114+
for &ch in &chars {
115+
match ch {
116+
// Allow alphanumeric
117+
c if c.is_ascii_alphanumeric() => {
118+
prev_was_special = false;
119+
}
120+
// Allow specific special characters
121+
'_' | '-' | '.' => {
122+
if prev_was_special {
123+
return Err(UsernameValidationError::ConsecutiveSpecialChars);
124+
}
125+
prev_was_special = true;
126+
}
127+
// Reject any other character
128+
_ => {
129+
return Err(UsernameValidationError::InvalidCharacter);
130+
}
131+
}
85132
}
86133

87134
Ok(())
@@ -138,9 +185,21 @@ pub mod error {
138185
#[error("Username should be between 3 and 64 chars long")]
139186
InvalidLength,
140187
#[error(
141-
"Username contains invalid characters. Please use lowercase, alphanumeric or underscore"
188+
"Username contains invalid characters. Only alphanumeric characters and special characters (underscore, hyphen and dot) are allowed"
142189
)]
143190
SpecialChar,
191+
#[error("Username should start with an alphanumeric character")]
192+
InvalidStartChar,
193+
#[error("Username should end with an alphanumeric character")]
194+
InvalidEndChar,
195+
#[error(
196+
"Username contains invalid characters. Only alphanumeric characters and special characters (underscore, hyphen and dot) are allowed"
197+
)]
198+
InvalidCharacter,
199+
#[error("Username contains consecutive special characters")]
200+
ConsecutiveSpecialChars,
201+
#[error("Username is reserved")]
202+
ReservedName,
144203
}
145204

146205
#[derive(Debug, thiserror::Error)]

0 commit comments

Comments
 (0)