seize-infra-doc/scripts/furious-to-zitadel/merge-duplicates.py
Hedi Kalboussi 1e90a85359 Documentation initiale infra Seize — passation J7
- 6 docs racine (README, HANDOVER, ARCHITECTURE, DECISIONS, INSTALL, OPERATIONS)
- 9 docs thématiques dans docs/
- 10 templates docker-compose dans configs/
- 7 scripts Python/Shell d'import Furious vers Zitadel
- 2 matrices RBAC

Etat infra au 5 juin 2026 :
- 88 users Furious importes dans Zitadel
- 8 roles RBAC crees et attribues
- Intranet filtre les cartes selon role (Approche A)
- 7 outils SSO operationnels
2026-06-05 17:49:43 +00:00

178 lines
5.5 KiB
Python
Executable File

#!/usr/bin/env python3
"""
merge-duplicates.py — Fusionne les doublons après import.
Pour chaque doublon :
1. Lit les metadata Furious sur le compte "nouveau"
2. Les copie sur le compte "ancien" (à garder)
3. Supprime le compte "nouveau"
⚠️ Adapter la variable DUPLICATES pour ton cas.
Auteur : Hedi Kalboussi
Date : 5 juin 2026
"""
import base64
import json
import logging
import time
from pathlib import Path
import jwt
import requests
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
# ============================================================================
# CONFIGURATION — À ADAPTER
# ============================================================================
# Liste des doublons à fusionner
# Format : { "keep_user_id": <id_ancien>, "delete_email": <email_du_nouveau>, "furious_id": <id_furious> }
DUPLICATES = [
# Exemple :
# {
# "keep_user_id": "375119988404518915",
# "delete_email": "hedi.kalboussi@seizeconsulting.com",
# "furious_id": 5,
# },
]
SCRIPT_DIR = Path(__file__).parent
KEY_FILE = SCRIPT_DIR / "zitadelfuriousimportkey.json"
RAW_DATA_FILE = SCRIPT_DIR / "furious-users-raw.json"
ZITADEL_DOMAIN = "https://sso.seizeconsulting.com"
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
log = logging.getLogger(__name__)
# ============================================================================
# AUTH
# ============================================================================
def get_zitadel_token():
with open(KEY_FILE) as f:
key_data = json.load(f)
private_key = serialization.load_pem_private_key(
key_data["key"].encode(), password=None, backend=default_backend()
)
now = int(time.time())
payload = {
"iss": key_data["userId"],
"sub": key_data["userId"],
"aud": ZITADEL_DOMAIN,
"iat": now,
"exp": now + 3600,
}
assertion = jwt.encode(
payload, private_key, algorithm="RS256", headers={"kid": key_data["keyId"]}
)
response = requests.post(
f"{ZITADEL_DOMAIN}/oauth/v2/token",
data={
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
"scope": "openid profile urn:zitadel:iam:org:project:id:zitadel:aud",
"assertion": assertion,
},
)
response.raise_for_status()
return response.json()["access_token"]
# ============================================================================
# OPÉRATIONS
# ============================================================================
def find_user_by_email(email, token):
response = requests.post(
f"{ZITADEL_DOMAIN}/v2/users",
headers={"Authorization": f"Bearer {token}"},
json={
"queries": [
{"emailQuery": {"emailAddress": email, "method": "TEXT_QUERY_METHOD_EQUALS"}}
]
},
)
response.raise_for_status()
users = response.json().get("result", [])
return users[0].get("userId") if users else None
def get_user_metadata(user_id, token):
"""Récupère toutes les metadata d'un user."""
response = requests.get(
f"{ZITADEL_DOMAIN}/management/v1/users/{user_id}/metadata/_search",
headers={"Authorization": f"Bearer {token}"},
)
if not response.ok:
return {}
result = response.json().get("result", [])
return {
m["key"]: base64.b64decode(m["value"]).decode()
for m in result
}
def set_user_metadata(user_id, key, value, token):
value_b64 = base64.b64encode(str(value).encode()).decode()
response = requests.post(
f"{ZITADEL_DOMAIN}/management/v1/users/{user_id}/metadata/{key}",
headers={"Authorization": f"Bearer {token}"},
json={"value": value_b64},
)
return response.ok
def delete_user(user_id, token):
response = requests.delete(
f"{ZITADEL_DOMAIN}/v2/users/{user_id}",
headers={"Authorization": f"Bearer {token}"},
)
return response.ok
# ============================================================================
# MAIN
# ============================================================================
def main():
if not DUPLICATES:
log.error("Variable DUPLICATES vide. Adapter le script avant exécution.")
return
token = get_zitadel_token()
log.info("✅ Token Zitadel OK")
for dup in DUPLICATES:
keep_id = dup["keep_user_id"]
delete_email = dup["delete_email"]
log.info(f"\n=== Doublon : keep={keep_id}, delete={delete_email} ===")
# 1. Trouver le user "nouveau" à supprimer
delete_id = find_user_by_email(delete_email, token)
if not delete_id:
log.warning(f"User {delete_email} pas trouvé (peut-être déjà fusionné)")
continue
# 2. Récupérer ses metadata
metadata = get_user_metadata(delete_id, token)
log.info(f" Metadata récupérées : {list(metadata.keys())}")
# 3. Les copier sur l'ancien
for key, value in metadata.items():
if value and set_user_metadata(keep_id, key, value, token):
log.info(f" → Copié {key}={value[:30]}...")
# 4. Supprimer le nouveau
if delete_user(delete_id, token):
log.info(f" ✅ Supprimé : {delete_email}")
else:
log.error(f" 💥 Échec suppression : {delete_email}")
time.sleep(0.5)
if __name__ == "__main__":
main()