seize-infra-doc/scripts/furious-to-zitadel/assign-roles.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

180 lines
5.9 KiB
Python
Executable File

#!/usr/bin/env python3
"""
assign-roles.py — Attribue les rôles RBAC aux users dans Zitadel.
Pour chaque user actif Furious :
1. Détermine son rôle (via mapping job_title)
2. Cherche son compte Zitadel par email
3. Crée le UserGrant avec le rôle dans le project "Infra Seize"
Prérequis :
- 88 users déjà importés dans Zitadel (cf. import-furious-to-zitadel.py)
- 8 rôles déjà créés dans le project Zitadel "Infra Seize"
- Clé service account avec rôle IAM_USER_MANAGER
Auteur : Hedi Kalboussi
Date : 5 juin 2026
"""
import json
import logging
import sys
import time
from pathlib import Path
import jwt
import requests
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
# Réutiliser le même mapping
from generate_roles_mapping import JOB_TITLE_TO_ROLE, determine_role # noqa
# Note : adapter selon disposition réelle des fichiers
# Si import échoue, copier le mapping ici directement
# ============================================================================
# CONFIGURATION
# ============================================================================
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"
PROJECT_ID = "375123185571463171" # ⚠️ ID du project "Infra Seize" — adapter si différent
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
log = logging.getLogger(__name__)
# ============================================================================
# AUTHENTIFICATION ZITADEL (identique au script d'import)
# ============================================================================
def get_zitadel_token():
with open(KEY_FILE) as f:
key_data = json.load(f)
user_id = key_data["userId"]
key_id = key_data["keyId"]
private_key_pem = key_data["key"]
private_key = serialization.load_pem_private_key(
private_key_pem.encode(), password=None, backend=default_backend()
)
now = int(time.time())
payload = {"iss": user_id, "sub": user_id, "aud": ZITADEL_DOMAIN, "iat": now, "exp": now + 3600}
assertion = jwt.encode(payload, private_key, algorithm="RS256", headers={"kid": key_id})
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"]
# ============================================================================
# API ZITADEL — Search user by email + create user grant
# ============================================================================
def find_user_by_email(email, token):
"""Cherche un user Zitadel par email. Retourne l'userId."""
response = requests.post(
f"{ZITADEL_DOMAIN}/v2/users",
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
json={
"queries": [
{"emailQuery": {"emailAddress": email, "method": "TEXT_QUERY_METHOD_EQUALS"}}
]
},
)
if not response.ok:
log.error(f"Erreur recherche {email} : {response.status_code}")
return None
result = response.json()
users = result.get("result", [])
if not users:
return None
return users[0].get("userId")
def assign_role_to_user(user_id, role_key, token):
"""Attribue un rôle au user dans le project Infra Seize."""
response = requests.post(
f"{ZITADEL_DOMAIN}/management/v1/users/{user_id}/grants",
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
json={
"projectId": PROJECT_ID,
"roleKeys": [role_key],
},
)
if response.status_code == 409:
log.info(f" Grant déjà existant pour user {user_id}")
return True
if not response.ok:
log.error(f"Échec attribution rôle pour {user_id} : {response.status_code} {response.text[:200]}")
return False
return True
# ============================================================================
# MAIN
# ============================================================================
def main():
log.info("===== Attribution des rôles RBAC =====")
with open(RAW_DATA_FILE) as f:
data = json.load(f)
actifs = [
u for u in data.get("data", {}).get("User", [])
if u.get("departure_date") == "0000-00-00"
]
log.info(f"Users actifs : {len(actifs)}")
token = get_zitadel_token()
log.info("✅ Token Zitadel OK")
success_count = 0
no_role_count = 0
not_found_count = 0
error_count = 0
for i, user in enumerate(actifs, 1):
email = user.get("email", "").lower()
role = determine_role(user)
log.info(f"[{i}/{len(actifs)}] {email} → rôle: {role}")
if not role:
log.warning(f" Pas de rôle déterminé (job_title='{user.get('job_title')}')")
no_role_count += 1
continue
user_id = find_user_by_email(email, token)
if not user_id:
log.error(f" User Zitadel introuvable pour {email}")
not_found_count += 1
continue
if assign_role_to_user(user_id, role, token):
success_count += 1
else:
error_count += 1
time.sleep(0.15) # Anti rate-limit
log.info(f"===== FIN =====")
log.info(f" ✅ Succès : {success_count}")
log.info(f" ⚠️ Sans rôle : {no_role_count}")
log.info(f" ❌ Non trouvés : {not_found_count}")
log.info(f" 💥 Erreurs : {error_count}")
if __name__ == "__main__":
main()