Use strongly-validated Username type for creating users
This commit is contained in:
parent
e322da0185
commit
9c966ea7f1
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -1892,6 +1892,7 @@ dependencies = [
|
||||
"sha2",
|
||||
"sqlx",
|
||||
"subtle",
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
"tokio",
|
||||
"tower-cookies",
|
||||
|
||||
26
Cargo.toml
26
Cargo.toml
@ -13,7 +13,7 @@ repository = "https://git.emunest.net/reivilibre/idcoop"
|
||||
argon2 = "0.5.2"
|
||||
async-trait = "0.1.74"
|
||||
axum = { version = "0.8.4", features = ["tracing", "macros"] }
|
||||
axum-extra = { version = "0.10.1", features = ["typed-header"] }
|
||||
axum-extra = { version = "0.10.1", features = ["typed-header"] }
|
||||
axum-client-ip = "0.7.0"
|
||||
base64 = "0.21.5"
|
||||
blake2 = "0.10.6"
|
||||
@ -24,7 +24,9 @@ confique = { version = "0.2.4", features = ["toml"], default-features = false }
|
||||
eyre = "0.6.8"
|
||||
futures = "0.3.29"
|
||||
governor = "0.6.0"
|
||||
hornbeam = { version = "0.0.4", path = "../hornbeam/hornbeam", features = ["formbeam"] }
|
||||
hornbeam = { version = "0.0.4", path = "../hornbeam/hornbeam", features = [
|
||||
"formbeam",
|
||||
] }
|
||||
formbeam = { version = "0.0.4", path = "../hornbeam/formbeam" }
|
||||
formbeam_derive = { version = "0.0.4", path = "../hornbeam/formbeam_derive" }
|
||||
bevy_reflect = { version = "0.14.0" }
|
||||
@ -38,12 +40,25 @@ serde = { version = "1.0.188", features = ["derive"] }
|
||||
serde_json = "1.0.108"
|
||||
serde_urlencoded = "0.7.1"
|
||||
sha2 = "0.10.8"
|
||||
sqlx = { version = "0.7.2", features = ["postgres", "runtime-tokio-native-tls", "macros", "migrate", "uuid", "chrono"] }
|
||||
sqlx = { version = "0.7.2", features = [
|
||||
"postgres",
|
||||
"runtime-tokio-native-tls",
|
||||
"macros",
|
||||
"migrate",
|
||||
"uuid",
|
||||
"chrono",
|
||||
] }
|
||||
subtle = "2.5.0"
|
||||
time = "0.3.30"
|
||||
thiserror = "2.0.12"
|
||||
tokio = { version = "1.33.0", features = ["rt", "macros"] }
|
||||
tower-cookies = "0.11.0"
|
||||
tower-http = { version = "0.6.4", features = ["trace", "cors", "set-header", "fs"] }
|
||||
tower-http = { version = "0.6.4", features = [
|
||||
"trace",
|
||||
"cors",
|
||||
"set-header",
|
||||
"fs",
|
||||
] }
|
||||
tracing = "0.1.37"
|
||||
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
|
||||
uuid = { version = "1.16.0", features = ["serde"] }
|
||||
@ -59,9 +74,6 @@ rstest = "0.21.0"
|
||||
regex = "1.11.1"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Enable optimisations for some perf-sensitive libraries
|
||||
# even in dev mode
|
||||
[profile.dev.package.argon2]
|
||||
|
||||
@ -5,7 +5,7 @@ use std::io::stdin;
|
||||
|
||||
use crate::config::Configuration;
|
||||
use crate::passwords::create_password_hash;
|
||||
use crate::store::{CreateRole, CreateUser, IdCoopStore, IdCoopStoreTxn};
|
||||
use crate::store::{CreateRole, CreateUser, IdCoopStore, IdCoopStoreTxn, Username};
|
||||
use clap::Parser;
|
||||
use comfy_table::presets::UTF8_FULL;
|
||||
use comfy_table::{Attribute, Cell, Color, ContentArrangement, Row, Table};
|
||||
@ -19,8 +19,7 @@ pub enum UserCommand {
|
||||
#[clap(alias = "new", alias = "create")]
|
||||
Add {
|
||||
/// The login name of the user.
|
||||
// TODO this should be a richer newtype with validation
|
||||
username: String,
|
||||
username: Username,
|
||||
|
||||
/// Set this flag if the user should be locked.
|
||||
#[clap(long = "locked")]
|
||||
|
||||
51
src/store.rs
51
src/store.rs
@ -3,6 +3,8 @@
|
||||
//! This file contains PostgreSQL queries.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::ops::Deref;
|
||||
use std::str::FromStr;
|
||||
|
||||
use bevy_reflect::Reflect;
|
||||
use chrono::DateTime;
|
||||
@ -13,6 +15,7 @@ use eyre::eyre;
|
||||
use eyre::Context;
|
||||
use futures::future::BoxFuture;
|
||||
use sqlx::{types::Uuid, Connection, PgPool, Postgres, Transaction};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::web::login::{
|
||||
LoginSession, LOGIN_SESSION_TOKEN_HASH_BYTES, LOGIN_SESSION_XSRF_SECRET_BYTES,
|
||||
@ -22,6 +25,50 @@ use crate::web::oauth_openid::application_session_access_token::ApplicationSessi
|
||||
/// Prefix for roles that are reserved by idCoop usage.
|
||||
pub const IDCOOP_RESERVED_ROLE_PREFIX: &str = "idcoop/";
|
||||
|
||||
/// Username grammar, chosen to be fairly interoperable hopefully:
|
||||
/// 1. at least 3 chars
|
||||
/// 2. no more than 36 chars
|
||||
/// 3. all ASCII alphanumeric and lowercase
|
||||
/// 4. must start with an ASCII letter
|
||||
pub fn is_valid_username(user_id: &str) -> bool {
|
||||
user_id.len() >= 3
|
||||
&& user_id.len() <= 36
|
||||
&& user_id
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() && !c.is_ascii_uppercase())
|
||||
&& user_id.chars().next().unwrap().is_ascii_alphabetic()
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Error)]
|
||||
#[error("invalid username: {0}")]
|
||||
/// An invalid username error.
|
||||
pub struct InvalidUsername(String);
|
||||
|
||||
/// A validated username
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct Username(String);
|
||||
|
||||
impl FromStr for Username {
|
||||
type Err = InvalidUsername;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let s = s.to_owned();
|
||||
if is_valid_username(&s) {
|
||||
Ok(Username(s))
|
||||
} else {
|
||||
Err(InvalidUsername(s))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Username {
|
||||
type Target = str;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if the given role ID is valid.
|
||||
///
|
||||
/// Does NOT check whether the role ID is reserved for use by idCoop or not!
|
||||
@ -110,7 +157,7 @@ pub struct User {
|
||||
/// Representation of the action of creating a user.
|
||||
pub struct CreateUser {
|
||||
/// The system name for the user.
|
||||
pub user_login_name: String,
|
||||
pub user_login_name: Username,
|
||||
/// The password hash of the user. See [`crate::passwords`].
|
||||
pub password_hash: Option<String>,
|
||||
/// Whether the user is locked and is therefore not allowed to log in.
|
||||
@ -276,7 +323,7 @@ impl IdCoopStoreTxn<'_, '_> {
|
||||
pub async fn create_user(&mut self, cu: CreateUser) -> eyre::Result<Uuid> {
|
||||
let r = sqlx::query!(
|
||||
"INSERT INTO users (user_name, user_id, created_at_utc, password_hash, locked) VALUES ($1, gen_random_uuid(), NOW(), $2, $3) RETURNING user_id",
|
||||
&cu.user_login_name, cu.password_hash.as_ref(), cu.locked
|
||||
&*cu.user_login_name, cu.password_hash.as_ref(), cu.locked
|
||||
)
|
||||
.fetch_one(&mut **self.txn)
|
||||
.await.context("failed to create user in DB")?;
|
||||
|
||||
@ -11,7 +11,7 @@ async fn test_cli_add_user() {
|
||||
|
||||
handle_user_command(
|
||||
UserCommand::Add {
|
||||
username: "jonathan".to_owned(),
|
||||
username: "jonathan".parse().unwrap(),
|
||||
locked: true,
|
||||
},
|
||||
&sys.config,
|
||||
@ -41,7 +41,7 @@ async fn test_cli_lock_and_unlock_user() {
|
||||
|
||||
handle_user_command(
|
||||
UserCommand::Add {
|
||||
username: "jonathan".to_owned(),
|
||||
username: "jonathan".parse().unwrap(),
|
||||
locked: false,
|
||||
},
|
||||
&sys.config,
|
||||
@ -117,7 +117,7 @@ async fn test_cli_del_user() {
|
||||
|
||||
handle_user_command(
|
||||
UserCommand::Add {
|
||||
username: "jonathan".to_owned(),
|
||||
username: "jonathan".parse().unwrap(),
|
||||
locked: true,
|
||||
},
|
||||
&sys.config,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user