server: Improve the error message in case of duplicate emails
This commit is contained in:
committed by
nitnelave
parent
96b7dbb1c5
commit
e5ce98c874
58
docs/migration_guides/v0.5.md
Normal file
58
docs/migration_guides/v0.5.md
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# Migration from 0.4 to 0.5
|
||||||
|
|
||||||
|
Welcome! If you're here, it's probably that the migration from 0.4.x to 0.5
|
||||||
|
didn't go smoothly for you. Don't worry, we can fix that.
|
||||||
|
|
||||||
|
## Multiple users with the same email
|
||||||
|
|
||||||
|
This is the most common case. You can see in the LLDAP logs that there are
|
||||||
|
several users with the same email, and they are listed.
|
||||||
|
|
||||||
|
This is not allowed anymore in v0.5, to prevent a user from setting their email
|
||||||
|
to someone else's email and gaining access to systems that identify by email.
|
||||||
|
|
||||||
|
The problem is that you currently have several users with the same email, so the
|
||||||
|
constraint cannot be enforced.
|
||||||
|
|
||||||
|
### Step 1: Take a note of the users with duplicate emails
|
||||||
|
|
||||||
|
In the LLDAP logs when you tried to start v0.5+, you'll see some warnings with
|
||||||
|
the list of users with the same emails. Take note of them.
|
||||||
|
|
||||||
|
### Step 2: Downgrade to v0.4.3
|
||||||
|
|
||||||
|
If using docker, switch to the `lldap/lldap:v0.4.3` image. Alternatively, grab
|
||||||
|
the binaries at https://github.com/lldap/lldap/releases/tag/v0.4.3.
|
||||||
|
|
||||||
|
This downgrade is safe and supported.
|
||||||
|
|
||||||
|
### Step 3: Remove duplicate emails
|
||||||
|
|
||||||
|
Restart LLDAP with the v0.4.3 version, and using your notes from step 1, change
|
||||||
|
the email of users with duplicate emails to make sure that each email is unique.
|
||||||
|
|
||||||
|
### Step 4: Upgrade again
|
||||||
|
|
||||||
|
You can now revert to the initial version.
|
||||||
|
|
||||||
|
## Multiple users/groups with the same UUID
|
||||||
|
|
||||||
|
This should be extremely rare. In this case, you'll need to find which users
|
||||||
|
have the same UUID, revert to v0.4.3 to be able to apply the changes, and delete
|
||||||
|
one of the duplicates.
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
### What if I want several users to be controlled by the same email?
|
||||||
|
|
||||||
|
You can use plus codes to set "the same" email to several users, while ensuring
|
||||||
|
that they can't identify as each other. For instance:
|
||||||
|
|
||||||
|
- Admin: `admin@example.com`
|
||||||
|
- Read-only admin: `admin+readonly@example.com`
|
||||||
|
- Jellyfin admin: `admin+jellyfin@example.com`
|
||||||
|
|
||||||
|
### I'm upgrading to a higher version than v0.5.
|
||||||
|
|
||||||
|
This guide is still relevant: you can use whatever later version in place of
|
||||||
|
v0.5. You'll still need to revert to v0.4.3 to apply the changes.
|
||||||
@@ -3,9 +3,12 @@ use crate::domain::{
|
|||||||
types::{GroupId, UserId, Uuid},
|
types::{GroupId, UserId, Uuid},
|
||||||
};
|
};
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
|
use itertools::Itertools;
|
||||||
use sea_orm::{
|
use sea_orm::{
|
||||||
sea_query::{self, ColumnDef, Expr, ForeignKey, ForeignKeyAction, Index, Query, Table, Value},
|
sea_query::{
|
||||||
ConnectionTrait, FromQueryResult, Iden, Statement, TransactionTrait,
|
self, all, ColumnDef, Expr, ForeignKey, ForeignKeyAction, Func, Index, Query, Table, Value,
|
||||||
|
},
|
||||||
|
ConnectionTrait, FromQueryResult, Iden, Order, Statement, TransactionTrait,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tracing::{info, instrument, warn};
|
use tracing::{info, instrument, warn};
|
||||||
@@ -462,20 +465,73 @@ async fn migrate_to_v3(pool: &DbConnection) -> anyhow::Result<()> {
|
|||||||
async fn migrate_to_v4(pool: &DbConnection) -> anyhow::Result<()> {
|
async fn migrate_to_v4(pool: &DbConnection) -> anyhow::Result<()> {
|
||||||
let builder = pool.get_database_backend();
|
let builder = pool.get_database_backend();
|
||||||
// Make emails and UUIDs unique.
|
// Make emails and UUIDs unique.
|
||||||
|
if let Err(e) = pool
|
||||||
|
.execute(
|
||||||
|
builder.build(
|
||||||
|
Index::create()
|
||||||
|
.if_not_exists()
|
||||||
|
.name("unique-user-email")
|
||||||
|
.table(Users::Table)
|
||||||
|
.col(Users::Email)
|
||||||
|
.unique(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.context(
|
||||||
|
r#"while enforcing unicity on emails (2 users have the same email).
|
||||||
|
|
||||||
|
See https://github.com/lldap/lldap/blob/main/docs/migration_guides/v0.5.md for details.
|
||||||
|
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
{
|
||||||
|
warn!("Found several users with the same email:");
|
||||||
|
for (email, users) in &pool
|
||||||
|
.query_all(
|
||||||
|
builder.build(
|
||||||
|
Query::select()
|
||||||
|
.from(Users::Table)
|
||||||
|
.columns([Users::Email, Users::UserId])
|
||||||
|
.order_by_columns([(Users::Email, Order::Asc), (Users::UserId, Order::Asc)])
|
||||||
|
.and_where(
|
||||||
|
Expr::col(Users::Email).in_subquery(
|
||||||
|
Query::select()
|
||||||
|
.from(Users::Table)
|
||||||
|
.column(Users::Email)
|
||||||
|
.group_by_col(Users::Email)
|
||||||
|
.cond_having(all![Expr::gt(
|
||||||
|
Expr::expr(Func::count(Expr::col(Users::Email))),
|
||||||
|
1
|
||||||
|
)])
|
||||||
|
.take(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("Could not check duplicate users")
|
||||||
|
.into_iter()
|
||||||
|
.map(|row| {
|
||||||
|
(
|
||||||
|
row.try_get::<UserId>("", &Users::UserId.to_string())
|
||||||
|
.unwrap(),
|
||||||
|
row.try_get::<String>("", &Users::Email.to_string())
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.group_by(|(_user, email)| email.to_owned())
|
||||||
|
{
|
||||||
|
warn!("Email: {email}");
|
||||||
|
for (user, _email) in users {
|
||||||
|
warn!(" User: {}", user.as_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
pool.execute(
|
pool.execute(
|
||||||
builder.build(
|
builder.build(
|
||||||
Index::create()
|
Index::create()
|
||||||
.name("unique-user-email")
|
.if_not_exists()
|
||||||
.table(Users::Table)
|
|
||||||
.col(Users::Email)
|
|
||||||
.unique(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.context("while enforcing unicity on emails (2 users have the same email)")?;
|
|
||||||
pool.execute(
|
|
||||||
builder.build(
|
|
||||||
Index::create()
|
|
||||||
.name("unique-user-uuid")
|
.name("unique-user-uuid")
|
||||||
.table(Users::Table)
|
.table(Users::Table)
|
||||||
.col(Users::Uuid)
|
.col(Users::Uuid)
|
||||||
@@ -487,6 +543,7 @@ async fn migrate_to_v4(pool: &DbConnection) -> anyhow::Result<()> {
|
|||||||
pool.execute(
|
pool.execute(
|
||||||
builder.build(
|
builder.build(
|
||||||
Index::create()
|
Index::create()
|
||||||
|
.if_not_exists()
|
||||||
.name("unique-group-uuid")
|
.name("unique-group-uuid")
|
||||||
.table(Groups::Table)
|
.table(Groups::Table)
|
||||||
.col(Groups::Uuid)
|
.col(Groups::Uuid)
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use chrono::prelude::*;
|
use chrono::prelude::*;
|
||||||
use sea_orm::{ConnectionTrait, Database, DbBackend, FromQueryResult};
|
use sea_orm::{ConnectionTrait, Database, DbBackend, FromQueryResult};
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
async fn get_in_memory_db() -> DbConnection {
|
async fn get_in_memory_db() -> DbConnection {
|
||||||
let mut sql_opt = sea_orm::ConnectOptions::new("sqlite::memory:".to_owned());
|
let mut sql_opt = sea_orm::ConnectOptions::new("sqlite::memory:".to_owned());
|
||||||
@@ -208,6 +209,7 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_migration_to_v4() {
|
async fn test_migration_to_v4() {
|
||||||
|
crate::infra::logging::init_for_tests();
|
||||||
let sql_pool = get_in_memory_db().await;
|
let sql_pool = get_in_memory_db().await;
|
||||||
upgrade_to_v1(&sql_pool).await.unwrap();
|
upgrade_to_v1(&sql_pool).await.unwrap();
|
||||||
migrate_from_version(&sql_pool, SchemaVersion(1), SchemaVersion(3))
|
migrate_from_version(&sql_pool, SchemaVersion(1), SchemaVersion(3))
|
||||||
@@ -227,9 +229,12 @@ mod tests {
|
|||||||
))
|
))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
migrate_from_version(&sql_pool, SchemaVersion(3), SchemaVersion(4))
|
error!(
|
||||||
.await
|
"{}",
|
||||||
.expect_err("migration should fail");
|
migrate_from_version(&sql_pool, SchemaVersion(3), SchemaVersion(4))
|
||||||
|
.await
|
||||||
|
.expect_err("migration should fail")
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
sql_migrations::JustSchemaVersion::find_by_statement(raw_statement(
|
sql_migrations::JustSchemaVersion::find_by_statement(raw_statement(
|
||||||
r#"SELECT version FROM metadata"#
|
r#"SELECT version FROM metadata"#
|
||||||
|
|||||||
Reference in New Issue
Block a user