-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Migration script for LDAP sync
- Loading branch information
Showing
3 changed files
with
316 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
//! This binary is used to migrate user IDs from base64 to hex encoding. | ||
use std::{path::Path, str::FromStr}; | ||
|
||
use anyhow::{anyhow, Context, Result}; | ||
use base64::{engine::general_purpose, Engine as _}; | ||
use famedly_sync::Config; | ||
use futures::StreamExt; | ||
use tracing::level_filters::LevelFilter; | ||
use zitadel_rust_client::v2::{ | ||
users::{ | ||
ListUsersRequest, SearchQuery, SetHumanProfile, TypeQuery, UpdateHumanUserRequest, | ||
UserFieldName, Userv2Type, | ||
}, | ||
Zitadel, | ||
}; | ||
|
||
#[tokio::main] | ||
async fn main() -> Result<()> { | ||
// Config | ||
let config_path = | ||
std::env::var("FAMEDLY_SYNC_CONFIG").unwrap_or_else(|_| "./config.yaml".to_owned()); | ||
let config = Config::new(Path::new(&config_path))?; | ||
|
||
// Tracing | ||
let subscriber = tracing_subscriber::FmtSubscriber::builder() | ||
.with_max_level( | ||
config | ||
.log_level | ||
.as_ref() | ||
.map_or(Ok(LevelFilter::INFO), |s| LevelFilter::from_str(s))?, | ||
) | ||
.finish(); | ||
tracing::subscriber::set_global_default(subscriber) | ||
.context("Setting default tracing subscriber failed")?; | ||
|
||
tracing::info!("Starting migration"); | ||
tracing::debug!("Old external IDs will be base64 decoded and re-encoded as hex"); | ||
tracing::debug!("Note: External IDs are stored in the nick_name field of the user's profile in Zitadel, often referred to as uid."); | ||
|
||
// Zitadel | ||
let zitadel_config = config.zitadel.clone(); | ||
let mut zitadel = Zitadel::new(zitadel_config.url, zitadel_config.key_file).await?; | ||
|
||
// Get all users | ||
let mut stream = zitadel.list_users( | ||
ListUsersRequest::new(vec![ | ||
SearchQuery::new().with_type_query(TypeQuery::new(Userv2Type::Human)) | ||
]) | ||
.with_asc(true) | ||
.with_sorting_column(UserFieldName::NickName), | ||
)?; | ||
|
||
// Process each user | ||
while let Some(user) = stream.next().await { | ||
let user_id = user.user_id().ok_or_else(|| anyhow!("User lacks ID"))?; | ||
let human = user.human().ok_or_else(|| anyhow!("User not human"))?; | ||
let profile = human.profile().ok_or_else(|| anyhow!("User lacks profile"))?; | ||
|
||
let given_name = profile.given_name(); | ||
let family_name = profile.family_name(); | ||
let display_name = profile.display_name(); | ||
|
||
let Some(nick_name) = profile.nick_name() else { | ||
tracing::warn!(user_id = %user_id, "User lacks nick_name (external uid), skipping"); | ||
continue; | ||
}; | ||
|
||
tracing::info!(user_id = %user_id, old_uid = %nick_name, "Starting migration for user"); | ||
|
||
let new_external_id = if nick_name.is_empty() { | ||
// Keep empty string as is, will skip it later | ||
nick_name.to_string() | ||
} else { | ||
// First check if it's valid hex | ||
if nick_name.chars().all(|c| c.is_ascii_hexdigit()) && nick_name.len() % 2 == 0 { | ||
// If valid hex, keep as is | ||
tracing::debug!( user_id = %user_id, old_uid = %nick_name,"Valid hex uid detected, keeping as is"); | ||
nick_name.to_string() | ||
} else { | ||
// Try base64, if fails use plain text | ||
let decoded = general_purpose::STANDARD.decode(nick_name).unwrap_or_else(|_| { | ||
// Not base64, treat as plain text | ||
tracing::debug!( user_id = %user_id, old_uid = %nick_name,"Decoding uid failed, going with plain uid"); | ||
nick_name.as_bytes().to_vec() | ||
}); | ||
|
||
// Encode using hex | ||
hex::encode(decoded) | ||
} | ||
}; | ||
|
||
// Skip empty uid | ||
if new_external_id.is_empty() { | ||
tracing::warn!( | ||
user_id = %user_id, | ||
old_uid = %nick_name, | ||
new_uid = %new_external_id, | ||
"Skipping user due to empty uid"); | ||
continue; | ||
} | ||
|
||
// Update uid (external ID, nick_name) in Zitadel | ||
let mut request = UpdateHumanUserRequest::new(); | ||
request.set_profile( | ||
SetHumanProfile::new( | ||
given_name.ok_or_else(|| anyhow!("User lacks given name"))?.to_owned(), | ||
family_name.ok_or_else(|| anyhow!("User lacks family name"))?.to_owned(), | ||
) | ||
.with_display_name( | ||
display_name.ok_or_else(|| anyhow!("User lacks display name"))?.to_owned(), | ||
) | ||
.with_nick_name(new_external_id.clone()), | ||
); | ||
|
||
zitadel.update_human_user(user_id, request).await?; | ||
|
||
tracing::info!( | ||
user_id = %user_id, | ||
old_uid = %nick_name, | ||
new_uid = %new_external_id, | ||
"User migrated" | ||
); | ||
} | ||
|
||
tracing::info!("Migration completed."); | ||
Ok(()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters