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

1
Cargo.lock generated
View File

@@ -2729,7 +2729,6 @@ dependencies = [
"lldap_domain_handlers",
"lldap_domain_model",
"lldap_ldap",
"lldap_opaque_handler",
"lldap_sql_backend_handler",
"lldap_test_utils",
"lldap_validation",

View File

@@ -55,25 +55,21 @@ version = "1"
mockall = "0.11.4"
pretty_assertions = "1"
[dev-dependencies.lldap_auth]
path = "../auth"
features = ["test"]
[dev-dependencies.lldap_domain]
path = "../domain"
features = ["test"]
[dev-dependencies.lldap_opaque_handler]
path = "../opaque-handler"
features = ["test"]
#[dev-dependencies.lldap_auth]
#path = "../auth"
#features = ["test"]
#
#[dev-dependencies.lldap_opaque_handler]
#path = "../opaque-handler"
#features = ["test"]
[dev-dependencies.lldap_test_utils]
path = "../test-utils"
[dev-dependencies.lldap_sql_backend_handler]
path = "../sql-backend-handler"
features = ["test"]
#
#[dev-dependencies.lldap_sql_backend_handler]
#path = "../sql-backend-handler"
#features = ["test"]
[dev-dependencies.tokio]
features = ["full"]
version = "1.25"
version = "1.25"

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 {
if request.base.is_empty()
&& request.scope == LdapSearchScope::Base
&& let LdapFilter::Present(attribute) = &request.filter
&& attribute.eq_ignore_ascii_case("objectclass")
{
return true;
if request.base.is_empty() && request.scope == LdapSearchScope::Base {
if let LdapFilter::Present(attribute) = &request.filter {
if attribute.eq_ignore_ascii_case("objectclass") {
return true;
}
}
}
false
}

View File

@@ -1159,30 +1159,6 @@ async fn migrate_to_v11(transaction: DatabaseTransaction) -> Result<DatabaseTran
)
.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)
}

View File

@@ -395,12 +395,12 @@ impl UserBackendHandler for SqlBackendHandler {
#[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<()> {
let user_id_owned = user_id.clone();
let user_id = user_id.clone();
self.sql_pool
.transaction::<_, _, sea_orm::DbErr>(|transaction| {
Box::pin(async move {
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),
};
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))]
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
.transaction::<_, _, sea_orm::DbErr>(|transaction| {
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)
.await?;
if res.rows_affected == 0 {
return Err(sea_orm::DbErr::Custom(format!(
"No such membership: '{user_id_owned}' -> {group_id:?}"
"No such membership: '{user_id}' -> {group_id:?}"
)));
}

View File

@@ -186,9 +186,8 @@ where
Some(token) => token,
};
if let Err(e) = super::mail::send_password_reset_email(
user.display_name
.as_deref()
.unwrap_or_else(|| user.user_id.as_str()),
user.display_name.as_deref(),
user.user_id.as_str(),
user.email.as_str(),
&token,
&data.server_url,

View File

@@ -80,6 +80,7 @@ async fn send_email(
}
pub async fn send_password_reset_email(
display_name: Option<&str>,
username: &str,
to: &str,
token: &str,
@@ -92,12 +93,16 @@ pub async fn send_password_reset_email(
.path_segments_mut()
.unwrap()
.extend(["reset-password", "step2", token]);
let greeting = format!("Hello {},", display_name.unwrap_or(username));
let body = format!(
"Hello {username},
"{greeting}
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
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}
Please contact an administrator if you did not initiate the process."

View File

@@ -125,7 +125,7 @@ async fn setup_sql_tables(database_url: &DatabaseUrl) -> Result<DatabaseConnecti
}
#[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"));
let sql_pool = setup_sql_tables(&config.database_url).await?;
@@ -214,9 +214,9 @@ async fn set_up_server(config: Configuration) -> Result<ServerBuilder> {
.await
.context("while binding the TCP server")?;
// Run every hour.
let scheduler = Scheduler::new("0 0 * * * * *", sql_pool);
let scheduler = Scheduler::new("0 0 * * * * *", sql_pool.clone());
scheduler.start();
Ok(server_builder)
Ok((server_builder, sql_pool))
}
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)?;
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<()> {
@@ -275,8 +280,11 @@ async fn create_schema_command(opts: RunOpts) -> Result<()> {
debug!("CLI: {:#?}", &opts);
let config = configuration::init(opts)?;
logging::init(&config)?;
setup_sql_tables(&config.database_url).await?;
let sql_pool = setup_sql_tables(&config.database_url).await?;
info!("Schema created successfully.");
if let Err(e) = sql_pool.close().await {
error!("Error closing database connection pool: {}", e);
}
Ok(())
}