Skip to content

Commit

Permalink
Add configuration option for non-ASCII usernames (#417)
Browse files Browse the repository at this point in the history
* Add configuration option for non-ASCII usernames

* change function name
  • Loading branch information
aumetra authored Nov 11, 2023
1 parent 110b4b3 commit 66fc706
Show file tree
Hide file tree
Showing 5 changed files with 61 additions and 8 deletions.
7 changes: 7 additions & 0 deletions config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,13 @@ max-connections = 20
# These are all the values you can use to customize your instance
# Stuff like the name of your server, description, character limit, configuration, etc.
[instance]
# Allow users to create accounts with non-ASCII usernames
#
# These usernames can, for example, contains Hangul, umlauts, etc.
# We use a technique to prevent some impersonation cases by making Kitsune consider, for example, "a" and "ä" as the same character
#
# This is set to "false" by default since we are not quite sure yet how this interacts with other software
allow-non-ascii-usernames = false
# Name of your instance
#
# This name is shown on the front page, in Mastodon clients, and will show up on statistics scrapers
Expand Down
1 change: 1 addition & 0 deletions crates/kitsune-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ pub enum FederationFilterConfiguration {
#[derive(Clone, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct InstanceConfiguration {
pub allow_non_ascii_usernames: bool,
pub name: SmolStr,
pub description: SmolStr,
pub webfinger_domain: Option<SmolStr>,
Expand Down
1 change: 1 addition & 0 deletions crates/kitsune-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,7 @@ pub async fn prepare_state(
let timeline_service = TimelineService::builder().db_pool(db_pool.clone()).build();

let user_service = UserService::builder()
.allow_non_ascii_usernames(config.instance.allow_non_ascii_usernames)
.captcha_service(captcha_service.clone())
.db_pool(db_pool.clone())
.job_service(job_service.clone())
Expand Down
3 changes: 1 addition & 2 deletions crates/kitsune-core/src/service/attachment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ use typed_builder::TypedBuilder;

const ALLOWED_FILETYPES: &[mime::Name<'_>] = &[mime::IMAGE, mime::VIDEO, mime::AUDIO];

#[allow(clippy::trivially_copy_pass_by_ref)]
fn is_allowed_filetype(value: &str, _ctx: &()) -> garde::Result {
fn is_allowed_filetype<T>(value: &str, _ctx: &T) -> garde::Result {
let content_type: mime::Mime = value
.parse()
.map_err(|err: mime::FromStrError| garde::Error::new(err.to_string()))?;
Expand Down
57 changes: 51 additions & 6 deletions crates/kitsune-core/src/service/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,16 @@ use zxcvbn::zxcvbn;

const MIN_PASSWORD_STRENGTH: u8 = 3;

#[allow(clippy::trivially_copy_pass_by_ref)]
fn is_strong_password(value: &Option<String>, _context: &()) -> garde::Result {
#[inline]
fn conditional_ascii_check(value: &str, ctx: &RegisterContext) -> garde::Result {
if ctx.allow_non_ascii {
return Ok(());
}

garde::rules::ascii::apply(&value, ())
}

fn is_strong_password<T>(value: &Option<String>, _context: &T) -> garde::Result {
let Some(ref value) = value else {
return Ok(());
};
Expand Down Expand Up @@ -70,10 +78,19 @@ fn is_strong_password(value: &Option<String>, _context: &()) -> garde::Result {
Ok(())
}

pub struct RegisterContext {
allow_non_ascii: bool,
}

#[derive(Clone, TypedBuilder, Validate)]
#[garde(context(RegisterContext))]
pub struct Register {
/// Username of the new user
#[garde(length(min = 1, max = 64), pattern(r"^[\p{L}\p{N}\.]+$"))]
#[garde(
custom(conditional_ascii_check),
length(min = 1, max = 64),
pattern(r"^[\p{L}\p{N}\.]+$")
)]
username: String,

/// Email address of the new user
Expand Down Expand Up @@ -103,6 +120,7 @@ pub struct Register {

#[derive(Clone, TypedBuilder)]
pub struct UserService {
allow_non_ascii_usernames: bool,
db_pool: PgPool,
job_service: JobService,
registrations_open: bool,
Expand Down Expand Up @@ -141,12 +159,15 @@ impl UserService {
Ok(())
}

#[allow(clippy::too_many_lines)] // TODO: Refactor to get under the limit
pub async fn register(&self, register: Register) -> Result<User> {
if !self.registrations_open && !register.force_registration {
return Err(ApiError::RegistrationsClosed.into());
}

register.validate(&())?;
register.validate(&RegisterContext {
allow_non_ascii: self.allow_non_ascii_usernames,
})?;

if self.captcha_service.enabled() {
let token = register.captcha_token.ok_or(ApiError::InvalidCaptcha)?;
Expand Down Expand Up @@ -265,6 +286,7 @@ impl UserService {
#[cfg(test)]
mod test {
use super::Register;
use crate::service::user::RegisterContext;
use garde::Validate;

#[test]
Expand All @@ -286,7 +308,11 @@ mod test {
.build();

assert!(
register.validate(&()).is_ok(),
register
.validate(&RegisterContext {
allow_non_ascii: true
})
.is_ok(),
"{username} is considered invalid",
);
}
Expand All @@ -301,9 +327,28 @@ mod test {
.build();

assert!(
register.validate(&()).is_err(),
register
.validate(&RegisterContext {
allow_non_ascii: true,
})
.is_err(),
"{username} is considered valid",
);
}
}

#[test]
fn deny_non_ascii() {
let register = Register::builder()
.email("[email protected]".into())
.password("verysecurepassword123".into())
.username("äumeträ".into())
.build();

assert!(register
.validate(&RegisterContext {
allow_non_ascii: false
})
.is_err());
}
}

0 comments on commit 66fc706

Please sign in to comment.