Compare commits

..

8 Commits

Author SHA1 Message Date
copilot-swe-agent[bot] c0e23f2251 Use unwrap_or logic for greeting instead of match statement
Co-authored-by: nitnelave <796633+nitnelave@users.noreply.github.com>
2025-09-04 23:06:18 +00:00
copilot-swe-agent[bot] f1d0c400aa Add display_name argument for greeting in password reset emails
Co-authored-by: nitnelave <796633+nitnelave@users.noreply.github.com>
2025-09-04 22:50:32 +00:00
copilot-swe-agent[bot] c400fac481 Revert display name changes, keep username line
Reverted all changes except adding the username line as requested. The function signature is back to the original (no display_name parameter), greeting uses username like before, but the "Your username is: {username}" line is preserved for recovery purposes.

Co-authored-by: nitnelave <796633+nitnelave@users.noreply.github.com>
2025-09-04 22:29:51 +00:00
copilot-swe-agent[bot] ccb389f2c2 Include both display name and username in password reset emails
As requested by @nitnelave, the password reset email now includes both the display name for personalization and the username for recovery. The email uses the display name in the greeting when available, but always shows the username explicitly for recovery purposes.

Co-authored-by: nitnelave <796633+nitnelave@users.noreply.github.com>
2025-09-04 22:11:42 +00:00
copilot-swe-agent[bot] 19799351db Implement username recovery in password reset emails
Co-authored-by: nitnelave <796633+nitnelave@users.noreply.github.com>
2025-09-04 20:37:24 +00:00
copilot-swe-agent[bot] ad41c04092 Initial plan 2025-09-04 20:22:29 +00:00
Copilot 775c5c716d server: gracefully shut down database connection pool 2025-09-04 09:19:03 +02:00
Kumpelinus 89cb59919b server: Add modifyTimestamp and pwdChangedTime attributes (#1265)
Add a modifyTimestamp attribute to LDAP entries for users and groups, and expose pwdChangedTime for users.
These attributes let clients track when an entry (or its password) was last changed.

 -  modifyTimestamp is a server-maintained attribute that updates on any write to user or group entries, including membership changes (on the group side).

 -  pwdChangedTime is set when a user’s password is created or changed.
2025-08-31 14:56:07 +02:00
8 changed files with 45 additions and 62 deletions
Generated
-1
View File
@@ -2729,7 +2729,6 @@ dependencies = [
"lldap_domain_handlers", "lldap_domain_handlers",
"lldap_domain_model", "lldap_domain_model",
"lldap_ldap", "lldap_ldap",
"lldap_opaque_handler",
"lldap_sql_backend_handler", "lldap_sql_backend_handler",
"lldap_test_utils", "lldap_test_utils",
"lldap_validation", "lldap_validation",
+11 -15
View File
@@ -55,24 +55,20 @@ version = "1"
mockall = "0.11.4" mockall = "0.11.4"
pretty_assertions = "1" pretty_assertions = "1"
[dev-dependencies.lldap_auth] #[dev-dependencies.lldap_auth]
path = "../auth" #path = "../auth"
features = ["test"] #features = ["test"]
#
[dev-dependencies.lldap_domain] #[dev-dependencies.lldap_opaque_handler]
path = "../domain" #path = "../opaque-handler"
features = ["test"] #features = ["test"]
[dev-dependencies.lldap_opaque_handler]
path = "../opaque-handler"
features = ["test"]
[dev-dependencies.lldap_test_utils] [dev-dependencies.lldap_test_utils]
path = "../test-utils" path = "../test-utils"
#
[dev-dependencies.lldap_sql_backend_handler] #[dev-dependencies.lldap_sql_backend_handler]
path = "../sql-backend-handler" #path = "../sql-backend-handler"
features = ["test"] #features = ["test"]
[dev-dependencies.tokio] [dev-dependencies.tokio]
features = ["full"] features = ["full"]
+6 -6
View File
@@ -291,12 +291,12 @@ pub fn make_ldap_subschema_entry(schema: PublicSchema) -> LdapOp {
} }
pub(crate) fn is_root_dse_request(request: &LdapSearchRequest) -> bool { pub(crate) fn is_root_dse_request(request: &LdapSearchRequest) -> bool {
if request.base.is_empty() if request.base.is_empty() && request.scope == LdapSearchScope::Base {
&& request.scope == LdapSearchScope::Base if let LdapFilter::Present(attribute) = &request.filter {
&& let LdapFilter::Present(attribute) = &request.filter if attribute.eq_ignore_ascii_case("objectclass") {
&& attribute.eq_ignore_ascii_case("objectclass") return true;
{ }
return true; }
} }
false false
} }
@@ -1159,30 +1159,6 @@ async fn migrate_to_v11(transaction: DatabaseTransaction) -> Result<DatabaseTran
) )
.await?; .await?;
// Initialize existing users with modified_date and password_modified_date = now
let now = chrono::Utc::now().naive_utc();
transaction
.execute(
builder.build(
Query::update()
.table(Users::Table)
.value(Users::ModifiedDate, now)
.value(Users::PasswordModifiedDate, now),
),
)
.await?;
// Initialize existing groups with modified_date = now
transaction
.execute(
builder.build(
Query::update()
.table(Groups::Table)
.value(Groups::ModifiedDate, now),
),
)
.await?;
Ok(transaction) Ok(transaction)
} }
@@ -395,12 +395,12 @@ impl UserBackendHandler for SqlBackendHandler {
#[instrument(skip_all, level = "debug", err, fields(user_id = ?user_id.as_str(), group_id))] #[instrument(skip_all, level = "debug", err, fields(user_id = ?user_id.as_str(), group_id))]
async fn add_user_to_group(&self, user_id: &UserId, group_id: GroupId) -> Result<()> { async fn add_user_to_group(&self, user_id: &UserId, group_id: GroupId) -> Result<()> {
let user_id_owned = user_id.clone(); let user_id = user_id.clone();
self.sql_pool self.sql_pool
.transaction::<_, _, sea_orm::DbErr>(|transaction| { .transaction::<_, _, sea_orm::DbErr>(|transaction| {
Box::pin(async move { Box::pin(async move {
let new_membership = model::memberships::ActiveModel { let new_membership = model::memberships::ActiveModel {
user_id: ActiveValue::Set(user_id_owned), user_id: ActiveValue::Set(user_id),
group_id: ActiveValue::Set(group_id), group_id: ActiveValue::Set(group_id),
}; };
new_membership.insert(transaction).await?; new_membership.insert(transaction).await?;
@@ -423,16 +423,16 @@ impl UserBackendHandler for SqlBackendHandler {
#[instrument(skip_all, level = "debug", err, fields(user_id = ?user_id.as_str(), group_id))] #[instrument(skip_all, level = "debug", err, fields(user_id = ?user_id.as_str(), group_id))]
async fn remove_user_from_group(&self, user_id: &UserId, group_id: GroupId) -> Result<()> { async fn remove_user_from_group(&self, user_id: &UserId, group_id: GroupId) -> Result<()> {
let user_id_owned = user_id.clone(); let user_id = user_id.clone();
self.sql_pool self.sql_pool
.transaction::<_, _, sea_orm::DbErr>(|transaction| { .transaction::<_, _, sea_orm::DbErr>(|transaction| {
Box::pin(async move { Box::pin(async move {
let res = model::Membership::delete_by_id((user_id_owned.clone(), group_id)) let res = model::Membership::delete_by_id((user_id.clone(), group_id))
.exec(transaction) .exec(transaction)
.await?; .await?;
if res.rows_affected == 0 { if res.rows_affected == 0 {
return Err(sea_orm::DbErr::Custom(format!( return Err(sea_orm::DbErr::Custom(format!(
"No such membership: '{user_id_owned}' -> {group_id:?}" "No such membership: '{user_id}' -> {group_id:?}"
))); )));
} }
+2 -3
View File
@@ -186,9 +186,8 @@ where
Some(token) => token, Some(token) => token,
}; };
if let Err(e) = super::mail::send_password_reset_email( if let Err(e) = super::mail::send_password_reset_email(
user.display_name user.display_name.as_deref(),
.as_deref() user.user_id.as_str(),
.unwrap_or_else(|| user.user_id.as_str()),
user.email.as_str(), user.email.as_str(),
&token, &token,
&data.server_url, &data.server_url,
+6 -1
View File
@@ -80,6 +80,7 @@ async fn send_email(
} }
pub async fn send_password_reset_email( pub async fn send_password_reset_email(
display_name: Option<&str>,
username: &str, username: &str,
to: &str, to: &str,
token: &str, token: &str,
@@ -92,12 +93,16 @@ pub async fn send_password_reset_email(
.path_segments_mut() .path_segments_mut()
.unwrap() .unwrap()
.extend(["reset-password", "step2", token]); .extend(["reset-password", "step2", token]);
let greeting = format!("Hello {},", display_name.unwrap_or(username));
let body = format!( let body = format!(
"Hello {username}, "{greeting}
This email has been sent to you in order to validate your identity. This email has been sent to you in order to validate your identity.
If you did not initiate the process your credentials might have been If you did not initiate the process your credentials might have been
compromised. You should reset your password and contact an administrator. compromised. You should reset your password and contact an administrator.
Your username is: {username}
To reset your password please visit the following URL: {reset_url} To reset your password please visit the following URL: {reset_url}
Please contact an administrator if you did not initiate the process." Please contact an administrator if you did not initiate the process."
+14 -6
View File
@@ -125,7 +125,7 @@ async fn setup_sql_tables(database_url: &DatabaseUrl) -> Result<DatabaseConnecti
} }
#[instrument(skip_all)] #[instrument(skip_all)]
async fn set_up_server(config: Configuration) -> Result<ServerBuilder> { async fn set_up_server(config: Configuration) -> Result<(ServerBuilder, DatabaseConnection)> {
info!("Starting LLDAP version {}", env!("CARGO_PKG_VERSION")); info!("Starting LLDAP version {}", env!("CARGO_PKG_VERSION"));
let sql_pool = setup_sql_tables(&config.database_url).await?; let sql_pool = setup_sql_tables(&config.database_url).await?;
@@ -214,9 +214,9 @@ async fn set_up_server(config: Configuration) -> Result<ServerBuilder> {
.await .await
.context("while binding the TCP server")?; .context("while binding the TCP server")?;
// Run every hour. // Run every hour.
let scheduler = Scheduler::new("0 0 * * * * *", sql_pool); let scheduler = Scheduler::new("0 0 * * * * *", sql_pool.clone());
scheduler.start(); scheduler.start();
Ok(server_builder) Ok((server_builder, sql_pool))
} }
async fn run_server_command(opts: RunOpts) -> Result<()> { async fn run_server_command(opts: RunOpts) -> Result<()> {
@@ -225,9 +225,14 @@ async fn run_server_command(opts: RunOpts) -> Result<()> {
let config = configuration::init(opts)?; let config = configuration::init(opts)?;
logging::init(&config)?; logging::init(&config)?;
let server = set_up_server(config).await?.workers(1); let (server, sql_pool) = set_up_server(config).await?;
let server = server.workers(1);
server.run().await.context("while starting the server") let result = server.run().await.context("while starting the server");
if let Err(e) = sql_pool.close().await {
error!("Error closing database connection pool: {}", e);
}
result
} }
async fn send_test_email_command(opts: TestEmailOpts) -> Result<()> { async fn send_test_email_command(opts: TestEmailOpts) -> Result<()> {
@@ -275,8 +280,11 @@ async fn create_schema_command(opts: RunOpts) -> Result<()> {
debug!("CLI: {:#?}", &opts); debug!("CLI: {:#?}", &opts);
let config = configuration::init(opts)?; let config = configuration::init(opts)?;
logging::init(&config)?; logging::init(&config)?;
setup_sql_tables(&config.database_url).await?; let sql_pool = setup_sql_tables(&config.database_url).await?;
info!("Schema created successfully."); info!("Schema created successfully.");
if let Err(e) = sql_pool.close().await {
error!("Error closing database connection pool: {}", e);
}
Ok(()) Ok(())
} }