diff --git a/Cargo.lock b/Cargo.lock index 661bea7..218a790 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2572,6 +2572,7 @@ dependencies = [ "opaque-ke", "rand 0.8.5", "rust-argon2", + "sea-orm", "serde", "sha2 0.9.9", "thiserror", diff --git a/app/src/components/change_password.rs b/app/src/components/change_password.rs index dd29623..296fd9d 100644 --- a/app/src/components/change_password.rs +++ b/app/src/components/change_password.rs @@ -97,7 +97,7 @@ impl CommonComponent for ChangePasswordForm { .context("Could not initialize login")?; self.opaque_data = OpaqueData::Login(login_start_request.state); let req = login::ClientLoginStartRequest { - username: ctx.props().username.clone(), + username: ctx.props().username.clone().into(), login_start_request: login_start_request.message, }; self.common.call_backend( @@ -134,7 +134,7 @@ impl CommonComponent for ChangePasswordForm { ) .context("Could not initiate password change")?; let req = registration::ClientRegistrationStartRequest { - username: ctx.props().username.clone(), + username: ctx.props().username.clone().into(), registration_start_request: registration_start_request.message, }; self.opaque_data = OpaqueData::Registration(registration_start_request.state); diff --git a/app/src/components/create_user.rs b/app/src/components/create_user.rs index ba74b80..c8c8939 100644 --- a/app/src/components/create_user.rs +++ b/app/src/components/create_user.rs @@ -123,7 +123,7 @@ impl CommonComponent for CreateUserForm { &mut rng, )?; let req = registration::ClientRegistrationStartRequest { - username: user_id, + username: user_id.into(), registration_start_request: message, }; self.common diff --git a/app/src/components/login.rs b/app/src/components/login.rs index 91bf41f..8d35be8 100644 --- a/app/src/components/login.rs +++ b/app/src/components/login.rs @@ -66,7 +66,7 @@ impl CommonComponent for LoginForm { opaque::client::login::start_login(&password, &mut rng) .context("Could not initialize login")?; let req = login::ClientLoginStartRequest { - username, + username: username.into(), login_start_request: message, }; self.common diff --git a/app/src/components/reset_password_step2.rs b/app/src/components/reset_password_step2.rs index c46d1f6..7dfece6 100644 --- a/app/src/components/reset_password_step2.rs +++ b/app/src/components/reset_password_step2.rs @@ -68,7 +68,7 @@ impl CommonComponent for ResetPasswordStep2Form { opaque_registration::start_registration(new_password.as_bytes(), &mut rng) .context("Could not initiate password change")?; let req = registration::ClientRegistrationStartRequest { - username: self.username.clone().unwrap(), + username: self.username.as_ref().unwrap().into(), registration_start_request: registration_start_request.message, }; self.opaque_data = Some(registration_start_request.state); diff --git a/auth/Cargo.toml b/auth/Cargo.toml index 4d53823..1f8cc10 100644 --- a/auth/Cargo.toml +++ b/auth/Cargo.toml @@ -13,6 +13,7 @@ default = ["opaque_server", "opaque_client"] opaque_server = [] opaque_client = [] js = [] +sea_orm = ["dep:sea-orm"] [dependencies] rust-argon2 = "0.8" @@ -31,6 +32,12 @@ version = "0.6" version = "*" features = [ "serde" ] +[dependencies.sea-orm] +version= "0.12" +default-features = false +features = ["macros"] +optional = true + # For WASM targets, use the JS getrandom. [target.'cfg(not(target_arch = "wasm32"))'.dependencies.getrandom] version = "0.2" diff --git a/auth/src/lib.rs b/auth/src/lib.rs index d51af8a..411d6bd 100644 --- a/auth/src/lib.rs +++ b/auth/src/lib.rs @@ -9,17 +9,17 @@ pub mod opaque; /// The messages for the 3-step OPAQUE and simple login process. pub mod login { - use super::*; + use super::{types::UserId, *}; #[derive(Serialize, Deserialize, Clone)] pub struct ServerData { - pub username: String, + pub username: UserId, pub server_login: opaque::server::login::ServerLogin, } #[derive(Serialize, Deserialize, Clone)] pub struct ClientLoginStartRequest { - pub username: String, + pub username: UserId, pub login_start_request: opaque::server::login::CredentialRequest, } @@ -39,14 +39,14 @@ pub mod login { #[derive(Serialize, Deserialize, Clone)] pub struct ClientSimpleLoginRequest { - pub username: String, + pub username: UserId, pub password: String, } impl fmt::Debug for ClientSimpleLoginRequest { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("ClientSimpleLoginRequest") - .field("username", &self.username) + .field("username", &self.username.as_str()) .field("password", &"***********") .finish() } @@ -63,16 +63,16 @@ pub mod login { /// The messages for the 3-step OPAQUE registration process. /// It is used to reset a user's password. pub mod registration { - use super::*; + use super::{types::UserId, *}; #[derive(Serialize, Deserialize, Clone)] pub struct ServerData { - pub username: String, + pub username: UserId, } #[derive(Serialize, Deserialize, Clone)] pub struct ClientRegistrationStartRequest { - pub username: String, + pub username: UserId, pub registration_start_request: opaque::server::registration::RegistrationRequest, } @@ -104,6 +104,100 @@ pub mod password_reset { } } +pub mod types { + use serde::{Deserialize, Serialize}; + + #[cfg(feature = "sea_orm")] + use sea_orm::{DbErr, DeriveValueType, QueryResult, TryFromU64, Value}; + + #[derive( + PartialEq, Eq, PartialOrd, Ord, Clone, Debug, Default, Hash, Serialize, Deserialize, + )] + #[cfg_attr(feature = "sea_orm", derive(DeriveValueType))] + #[serde(from = "String")] + pub struct CaseInsensitiveString(String); + + impl CaseInsensitiveString { + pub fn new(s: &str) -> Self { + Self(s.to_ascii_lowercase()) + } + + pub fn as_str(&self) -> &str { + self.0.as_str() + } + + pub fn into_string(self) -> String { + self.0 + } + } + + impl From for CaseInsensitiveString { + fn from(mut s: String) -> Self { + s.make_ascii_lowercase(); + Self(s) + } + } + + impl From<&String> for CaseInsensitiveString { + fn from(s: &String) -> Self { + Self::new(s.as_str()) + } + } + + impl From<&str> for CaseInsensitiveString { + fn from(s: &str) -> Self { + Self::new(s) + } + } + + #[derive( + PartialEq, Eq, PartialOrd, Ord, Clone, Debug, Default, Hash, Serialize, Deserialize, + )] + #[cfg_attr(feature = "sea_orm", derive(DeriveValueType))] + #[serde(from = "CaseInsensitiveString")] + pub struct UserId(CaseInsensitiveString); + + impl UserId { + pub fn new(s: &str) -> Self { + s.into() + } + pub fn as_str(&self) -> &str { + self.0.as_str() + } + pub fn into_string(self) -> String { + self.0.into_string() + } + } + impl From for UserId + where + T: Into, + { + fn from(s: T) -> Self { + Self(s.into()) + } + } + impl std::fmt::Display for UserId { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self.0.as_str()) + } + } + + #[cfg(feature = "sea_orm")] + impl From<&UserId> for Value { + fn from(user_id: &UserId) -> Self { + user_id.as_str().into() + } + } + #[cfg(feature = "sea_orm")] + impl TryFromU64 for UserId { + fn try_from_u64(_n: u64) -> Result { + Err(DbErr::ConvertFromU64( + "UserId cannot be constructed from u64", + )) + } + } +} + #[derive(Clone, Serialize, Deserialize)] pub struct JWTClaims { pub exp: DateTime, diff --git a/auth/src/opaque.rs b/auth/src/opaque.rs index cfdd396..a2dfa39 100644 --- a/auth/src/opaque.rs +++ b/auth/src/opaque.rs @@ -1,3 +1,4 @@ +use crate::types::UserId; use opaque_ke::ciphersuite::CipherSuite; use rand::{CryptoRng, RngCore}; @@ -145,12 +146,12 @@ pub mod server { pub fn start_registration( server_setup: &ServerSetup, registration_request: RegistrationRequest, - username: &str, + username: &UserId, ) -> AuthenticationResult { Ok(ServerRegistration::start( server_setup, registration_request, - username.as_bytes(), + username.as_str().as_bytes(), )?) } @@ -178,14 +179,14 @@ pub mod server { server_setup: &ServerSetup, password_file: Option, credential_request: CredentialRequest, - username: &str, + username: &UserId, ) -> AuthenticationResult { Ok(ServerLogin::start( rng, server_setup, password_file, credential_request, - username.as_bytes(), + username.as_str().as_bytes(), ServerLoginStartParameters::default(), )?) } diff --git a/migration-tool/src/lldap.rs b/migration-tool/src/lldap.rs index 7f61c57..5e4ce35 100644 --- a/migration-tool/src/lldap.rs +++ b/migration-tool/src/lldap.rs @@ -136,7 +136,7 @@ fn try_login( let ClientLoginStartResult { state, message } = start_login(password, &mut rng).context("Could not initialize login")?; let req = ClientLoginStartRequest { - username: username.to_owned(), + username: username.into(), login_start_request: message, }; let response = client diff --git a/server/Cargo.toml b/server/Cargo.toml index 28b164e..bbf013d 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -79,6 +79,7 @@ version = "0.10.1" [dependencies.lldap_auth] path = "../auth" +features = ["opaque_server", "opaque_client", "sea_orm"] [dependencies.opaque-ke] version = "0.6" diff --git a/server/src/domain/sql_backend_handler.rs b/server/src/domain/sql_backend_handler.rs index e742696..5963e7d 100644 --- a/server/src/domain/sql_backend_handler.rs +++ b/server/src/domain/sql_backend_handler.rs @@ -63,7 +63,7 @@ pub mod tests { opaque::client::registration::start_registration(pass.as_bytes(), &mut rng).unwrap(); let response = handler .registration_start(registration::ClientRegistrationStartRequest { - username: name.to_string(), + username: name.into(), registration_start_request: client_registration_start.message, }) .await diff --git a/server/src/domain/sql_opaque_handler.rs b/server/src/domain/sql_opaque_handler.rs index 9bda7e6..b296615 100644 --- a/server/src/domain/sql_opaque_handler.rs +++ b/server/src/domain/sql_opaque_handler.rs @@ -33,7 +33,7 @@ fn passwords_match( server_setup, Some(password_file), client_login_start_result.message, - username.as_str(), + username, )?; client::login::finish_login( client_login_start_result.state, @@ -100,15 +100,13 @@ impl OpaqueHandler for SqlOpaqueHandler { &self, request: login::ClientLoginStartRequest, ) -> Result { + let user_id = request.username; let maybe_password_file = self - .get_password_file_for_user(UserId::new(&request.username)) + .get_password_file_for_user(user_id.clone()) .await? .map(|bytes| { opaque::server::ServerRegistration::deserialize(&bytes).map_err(|_| { - DomainError::InternalError(format!( - "Corrupted password file for {}", - &request.username - )) + DomainError::InternalError(format!("Corrupted password file for {}", &user_id)) }) }) .transpose()?; @@ -120,11 +118,11 @@ impl OpaqueHandler for SqlOpaqueHandler { self.config.get_server_setup(), maybe_password_file, request.login_start_request, - &request.username, + &user_id, )?; let secret_key = self.get_orion_secret_key()?; let server_data = login::ServerData { - username: request.username, + username: user_id, server_login: start_response.state, }; let encrypted_state = orion::aead::seal(&secret_key, &bincode::serialize(&server_data)?)?; @@ -151,7 +149,7 @@ impl OpaqueHandler for SqlOpaqueHandler { opaque::server::login::finish_login(server_login, request.credential_finalization)? .session_key; - Ok(UserId::new(&username)) + Ok(username) } #[instrument(skip_all, level = "debug", err)] @@ -191,7 +189,7 @@ impl OpaqueHandler for SqlOpaqueHandler { opaque::server::registration::get_password_file(request.registration_upload); // Set the user password to the new password. let user_update = model::users::ActiveModel { - user_id: ActiveValue::Set(UserId::new(&username)), + user_id: ActiveValue::Set(username), password_hash: ActiveValue::Set(Some(password_file.serialize())), ..Default::default() }; @@ -204,7 +202,7 @@ impl OpaqueHandler for SqlOpaqueHandler { #[instrument(skip_all, level = "debug", err, fields(username = %username.as_str()))] pub(crate) async fn register_password( opaque_handler: &SqlOpaqueHandler, - username: &UserId, + username: UserId, password: &SecUtf8, ) -> Result<()> { let mut rng = rand::rngs::OsRng; @@ -213,7 +211,7 @@ pub(crate) async fn register_password( opaque::client::registration::start_registration(password.unsecure().as_bytes(), &mut rng)?; let start_response = opaque_handler .registration_start(ClientRegistrationStartRequest { - username: username.to_string(), + username, registration_start_request: registration_start.message, }) .await?; @@ -245,7 +243,7 @@ mod tests { let login_start = opaque::client::login::start_login(password, &mut rng)?; let start_response = opaque_handler .login_start(ClientLoginStartRequest { - username: username.to_string(), + username: UserId::new(username), login_start_request: login_start.message, }) .await?; @@ -276,7 +274,7 @@ mod tests { .unwrap_err(); register_password( &opaque_handler, - &UserId::new("bob"), + UserId::new("bob"), &secstr::SecUtf8::from("bob00"), ) .await?; diff --git a/server/src/domain/types.rs b/server/src/domain/types.rs index a22e487..1f498e8 100644 --- a/server/src/domain/types.rs +++ b/server/src/domain/types.rs @@ -2,6 +2,7 @@ use std::cmp::Ordering; use base64::Engine; use chrono::{NaiveDateTime, TimeZone}; +use lldap_auth::types::CaseInsensitiveString; use sea_orm::{ entity::IntoActiveValue, sea_query::{value::ValueType, ArrayType, BlobSize, ColumnType, Nullable, ValueTypeErr}, @@ -11,6 +12,7 @@ use serde::{Deserialize, Serialize}; use strum::{EnumString, IntoStaticStr}; pub use super::model::UserColumn; +pub use lldap_auth::types::UserId; #[derive(PartialEq, Hash, Eq, Clone, Debug, Default, Serialize, Deserialize, DeriveValueType)] #[serde(try_from = "&str")] @@ -122,112 +124,6 @@ impl Serialized { } } -#[derive( - PartialEq, - Eq, - PartialOrd, - Ord, - Clone, - Debug, - Default, - Hash, - Serialize, - Deserialize, - DeriveValueType, -)] -#[serde(from = "String")] -pub struct CaseInsensitiveString(String); - -impl CaseInsensitiveString { - pub fn new(user_id: &str) -> Self { - Self(user_id.to_lowercase()) - } - - pub fn as_str(&self) -> &str { - self.0.as_str() - } - - pub fn into_string(self) -> String { - self.0 - } -} - -impl From for CaseInsensitiveString { - fn from(s: String) -> Self { - Self::new(&s) - } -} - -macro_rules! make_case_insensitive_string { - ($c:ident) => { - #[derive( - PartialEq, - Eq, - PartialOrd, - Ord, - Clone, - Debug, - Default, - Hash, - Serialize, - Deserialize, - DeriveValueType, - )] - #[serde(from = "CaseInsensitiveString")] - pub struct $c(CaseInsensitiveString); - - impl $c { - pub fn new(raw: &str) -> Self { - Self(CaseInsensitiveString::new(raw)) - } - - pub fn as_str(&self) -> &str { - self.0.as_str() - } - - pub fn into_string(self) -> String { - self.0.into_string() - } - } - - impl From for $c { - fn from(s: CaseInsensitiveString) -> Self { - Self(s) - } - } - - impl From for $c { - fn from(s: String) -> Self { - Self(CaseInsensitiveString::from(s)) - } - } - - impl From<&str> for $c { - fn from(s: &str) -> Self { - Self::new(s) - } - } - - impl std::fmt::Display for $c { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}", self.0.as_str()) - } - } - - impl From<&$c> for Value { - fn from(user_id: &$c) -> Self { - user_id.as_str().into() - } - } - - impl TryFromU64 for $c { - fn try_from_u64(_n: u64) -> Result { - Err(DbErr::ConvertFromU64("$c cannot be constructed from u64")) - } - } - }; -} - fn compare_str_case_insensitive(s1: &str, s2: &str) -> Ordering { let mut it_1 = s1.chars().flat_map(|c| c.to_lowercase()); let mut it_2 = s2.chars().flat_map(|c| c.to_lowercase()); @@ -323,8 +219,58 @@ macro_rules! make_case_insensitive_comparable_string { }; } -make_case_insensitive_string!(UserId); -make_case_insensitive_string!(AttributeName); +#[derive( + PartialEq, + Eq, + PartialOrd, + Ord, + Clone, + Debug, + Default, + Hash, + Serialize, + Deserialize, + DeriveValueType, +)] +#[serde(from = "CaseInsensitiveString")] +pub struct AttributeName(CaseInsensitiveString); + +impl AttributeName { + pub fn new(s: &str) -> Self { + s.into() + } + pub fn as_str(&self) -> &str { + self.0.as_str() + } + pub fn into_string(self) -> String { + self.0.into_string() + } +} +impl From for AttributeName +where + T: Into, +{ + fn from(s: T) -> Self { + Self(s.into()) + } +} +impl std::fmt::Display for AttributeName { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self.0.as_str()) + } +} +impl From<&AttributeName> for Value { + fn from(attribute_name: &AttributeName) -> Self { + attribute_name.as_str().into() + } +} +impl TryFromU64 for AttributeName { + fn try_from_u64(_n: u64) -> Result { + Err(DbErr::ConvertFromU64( + "AttributeName cannot be constructed from u64", + )) + } +} make_case_insensitive_comparable_string!(Email); make_case_insensitive_comparable_string!(GroupName); diff --git a/server/src/infra/auth_service.rs b/server/src/infra/auth_service.rs index b65b9d0..78ee47b 100644 --- a/server/src/infra/auth_service.rs +++ b/server/src/infra/auth_service.rs @@ -428,13 +428,13 @@ async fn simple_login( where Backend: TcpBackendHandler + BackendHandler + OpaqueHandler + LoginHandler + 'static, { - let user_id = UserId::new(&request.username); + let login::ClientSimpleLoginRequest { username, password } = request.into_inner(); let bind_request = BindRequest { - name: user_id.clone(), - password: request.password.clone(), + name: username.clone(), + password, }; data.get_login_handler().bind(bind_request).await?; - get_login_successful_response(&data, &user_id).await + get_login_successful_response(&data, &username).await } async fn simple_login_handler( @@ -500,14 +500,14 @@ where .await .map_err(|e| TcpError::BadRequest(format!("{:#?}", e)))? .into_inner(); - let user_id = UserId::new(®istration_start_request.username); + let user_id = ®istration_start_request.username; let user_is_admin = data .get_readonly_handler() - .get_user_groups(&user_id) + .get_user_groups(user_id) .await? .iter() .any(|g| g.display_name == "lldap_admin".into()); - if !validation_result.can_change_password(&user_id, user_is_admin) { + if !validation_result.can_change_password(user_id, user_is_admin) { return Err(TcpError::UnauthorizedError( "Not authorized to change the user's password".to_string(), )); diff --git a/server/src/infra/ldap_handler.rs b/server/src/infra/ldap_handler.rs index 02b901c..2b05e23 100644 --- a/server/src/infra/ldap_handler.rs +++ b/server/src/infra/ldap_handler.rs @@ -306,7 +306,7 @@ impl LdapHandler( &self, backend_handler: &B, - user: &UserId, + user: UserId, password: &[u8], ) -> Result<()> { use lldap_auth::*; @@ -314,7 +314,7 @@ impl LdapHandler LdapHandler LdapHandler LdapHandler LdapHandler Result { warn!("Forcing admin password reset to the config-provided password"); register_password( &backend_handler, - &config.ldap_user_dn, + config.ldap_user_dn.clone(), &config.ldap_user_pass, ) .await diff --git a/server/tests/common/auth.rs b/server/tests/common/auth.rs index 3414398..eff4e02 100644 --- a/server/tests/common/auth.rs +++ b/server/tests/common/auth.rs @@ -10,7 +10,7 @@ pub fn get_token(client: &Client) -> String { .header(reqwest::header::CONTENT_TYPE, "application/json") .body( serde_json::to_string(&lldap_auth::login::ClientSimpleLoginRequest { - username, + username: username.into(), password, }) .expect("Failed to encode the username/password as json to log in"), diff --git a/set-password/src/main.rs b/set-password/src/main.rs index 74d3f85..9ed9cd7 100644 --- a/set-password/src/main.rs +++ b/set-password/src/main.rs @@ -49,7 +49,7 @@ fn get_token(base_url: &Url, username: &str, password: &str) -> Result { .header(reqwest::header::CONTENT_TYPE, "application/json") .body( serde_json::to_string(&lldap_auth::login::ClientSimpleLoginRequest { - username: username.to_string(), + username: username.into(), password: password.to_string(), }) .expect("Failed to encode the username/password as json to log in"), @@ -121,7 +121,7 @@ fn main() -> Result<()> { opaque::client::registration::start_registration(opts.password.as_bytes(), &mut rng) .context("Could not initiate password change")?; let start_request = registration::ClientRegistrationStartRequest { - username: opts.username.to_string(), + username: opts.username.clone().into(), registration_start_request: registration_start_request.message, }; let res = register_start(&opts.base_url, &token, start_request)?;