Compare commits

..

10 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
e2d9d47623 Update group modification time when adding or removing users from groups
Co-authored-by: nitnelave <796633+nitnelave@users.noreply.github.com>
2025-08-27 19:20:21 +00:00
copilot-swe-agent[bot]
edf22afda0 Update user modification time when changing password
Added modified_date update to OPAQUE password registration to ensure both password_modified_date and user modified_date are updated when passwords change. This provides better compatibility with LDAP clients that rely on modifyTimestamp for cache invalidation.

Co-authored-by: nitnelave <796633+nitnelave@users.noreply.github.com>
2025-08-27 19:00:49 +00:00
copilot-swe-agent[bot]
7e64e061d3 Fix clippy collapsible-if warnings in LDAP search code
Co-authored-by: nitnelave <796633+nitnelave@users.noreply.github.com>
2025-08-27 17:30:15 +00:00
copilot-swe-agent[bot]
233262efa6 Fix tests and formatting for modifyTimestamp implementation
Co-authored-by: nitnelave <796633+nitnelave@users.noreply.github.com>
2025-08-27 08:03:21 +00:00
copilot-swe-agent[bot]
b8b48ebe24 Set modification timestamps for new users and groups during creation
Co-authored-by: nitnelave <796633+nitnelave@users.noreply.github.com>
2025-08-27 07:42:40 +00:00
copilot-swe-agent[bot]
8a8eb4157c Address review feedback: remove backup file, initialize timestamps with current time, move attributes to Public schema
Co-authored-by: nitnelave <796633+nitnelave@users.noreply.github.com>
2025-08-26 22:51:27 +00:00
copilot-swe-agent[bot]
1c92ae60d3 Complete modifyTimestamp implementation - fix remaining test compilation errors
Co-authored-by: nitnelave <796633+nitnelave@users.noreply.github.com>
2025-08-26 20:51:47 +00:00
copilot-swe-agent[bot]
f7ab6ded36 Fix database migration default values for modify timestamps
Co-authored-by: nitnelave <796633+nitnelave@users.noreply.github.com>
2025-08-26 20:48:37 +00:00
copilot-swe-agent[bot]
a90695a6ce Implement core modifyTimestamp functionality with database migration and backend support
Co-authored-by: nitnelave <796633+nitnelave@users.noreply.github.com>
2025-08-26 20:41:32 +00:00
copilot-swe-agent[bot]
df49d827d0 Initial plan 2025-08-26 20:11:25 +00:00
8 changed files with 62 additions and 45 deletions

1
Cargo.lock generated
View File

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

View File

@@ -55,21 +55,25 @@ version = "1"
mockall = "0.11.4"
pretty_assertions = "1"
#[dev-dependencies.lldap_auth]
#path = "../auth"
#features = ["test"]
#
#[dev-dependencies.lldap_opaque_handler]
#path = "../opaque-handler"
#features = ["test"]
[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_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 {
if let LdapFilter::Present(attribute) = &request.filter {
if attribute.eq_ignore_ascii_case("objectclass") {
return true;
}
}
if request.base.is_empty()
&& request.scope == LdapSearchScope::Base
&& let LdapFilter::Present(attribute) = &request.filter
&& attribute.eq_ignore_ascii_case("objectclass")
{
return true;
}
false
}

View File

@@ -1159,6 +1159,30 @@ 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 = user_id.clone();
let user_id_owned = 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),
user_id: ActiveValue::Set(user_id_owned),
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 = user_id.clone();
let user_id_owned = user_id.clone();
self.sql_pool
.transaction::<_, _, sea_orm::DbErr>(|transaction| {
Box::pin(async move {
let res = model::Membership::delete_by_id((user_id.clone(), group_id))
let res = model::Membership::delete_by_id((user_id_owned.clone(), group_id))
.exec(transaction)
.await?;
if res.rows_affected == 0 {
return Err(sea_orm::DbErr::Custom(format!(
"No such membership: '{user_id}' -> {group_id:?}"
"No such membership: '{user_id_owned}' -> {group_id:?}"
)));
}

View File

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

View File

@@ -80,7 +80,6 @@ async fn send_email(
}
pub async fn send_password_reset_email(
display_name: Option<&str>,
username: &str,
to: &str,
token: &str,
@@ -93,16 +92,12 @@ 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!(
"{greeting}
"Hello {username},
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, DatabaseConnection)> {
async fn set_up_server(config: Configuration) -> Result<ServerBuilder> {
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, Database
.await
.context("while binding the TCP server")?;
// Run every hour.
let scheduler = Scheduler::new("0 0 * * * * *", sql_pool.clone());
let scheduler = Scheduler::new("0 0 * * * * *", sql_pool);
scheduler.start();
Ok((server_builder, sql_pool))
Ok(server_builder)
}
async fn run_server_command(opts: RunOpts) -> Result<()> {
@@ -225,14 +225,9 @@ async fn run_server_command(opts: RunOpts) -> Result<()> {
let config = configuration::init(opts)?;
logging::init(&config)?;
let (server, sql_pool) = set_up_server(config).await?;
let server = server.workers(1);
let server = set_up_server(config).await?.workers(1);
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
server.run().await.context("while starting the server")
}
async fn send_test_email_command(opts: TestEmailOpts) -> Result<()> {
@@ -280,11 +275,8 @@ async fn create_schema_command(opts: RunOpts) -> Result<()> {
debug!("CLI: {:#?}", &opts);
let config = configuration::init(opts)?;
logging::init(&config)?;
let sql_pool = setup_sql_tables(&config.database_url).await?;
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(())
}