Construction du processus de gouvernance du cycle de vie des identités
Quand l'utiliser
- L'organisation manque de processus automatisés d'arrivée-mutation-départ (JML) pour la gestion des identités
- Le provisionnement des accès est manuel et prend des jours, créant des pertes de productivité et des failles de sécurité
- Les anciens employés conservent des accès aux systèmes après leur départ (comptes orphelins)
- L'explosion des rôles a créé des milliers de rôles avec un propriétaire flou et des droits qui se chevauchent
- Les exigences de conformité imposent des processus de cycle de vie des identités documentés (SOX, HIPAA, RGPD)
- Aucune visibilité centralisée sur qui a accès à quoi dans l'entreprise
Ne pas utiliser pour la gestion des utilisateurs d'une seule application ; la gouvernance des identités traite de la gestion du cycle de vie intersystèmes, nécessitant la corrélation des sources RH faisant autorité avec le provisionnement en aval des applications.
Prérequis
- Système RH faisant autorité (Workday, SAP SuccessFactors, BambooHR) comme source de vérité des identités
- Plateforme IGA (SailPoint, Saviynt, One Identity) ou Microsoft Entra ID Governance
- Active Directory et/ou Azure AD comme services d'annuaire principaux
- Connecteurs d'application pour les systèmes cibles nécessitant un provisionnement automatisé
- Structure de rôles organisationnels définie et hiérarchie hiérarchique
- Adhésion des parties prenantes (RH, DSI, sécurité, responsables métier)
Workflow
Étape 1 : Définir les états et transitions du cycle de vie des identités
Cartographier le cycle de vie des identités, de l'embauche à la rupture de contrat :
"""
Machine à états du cycle de vie des identités
Définit tous les états d'identité et les transitions valides avec des actions automatisées.
"""
IDENTITY_LIFECYCLE = {
"states": {
"PRE_HIRE": {
"description": "Identité créée à partir du flux RH avant la date de début",
"automated_actions": [
"Créer un enregistrement d'identité dans la plateforme IGA",
"Générer un identifiant employé unique",
"Réserver une boîte aux lettres",
"Attribuer les rôles de naissance en fonction du code métier",
"Lancer le workflow de vérification des antécédents"
],
"valid_transitions": ["ACTIVE", "CANCELLED"]
},
"ACTIVE": {
"description": "L'employé a commencé, accès complet provisionné",
"automated_actions": [
"Créer un compte Active Directory",
"Créer une boîte aux lettres électronique",
"Provisionner l'accès aux applications de naissance",
"Attribuer les rôles spécifiques au département",
"Ajouter aux groupes de distribution",
"Délivrer un jeton MFA / clé de sécurité",
"Créer un compte VPN si le travailleur est à distance"
],
"valid_transitions": ["ROLE_CHANGE", "LEAVE_OF_ABSENCE", "TERMINATED"]
},
"ROLE_CHANGE": {
"description": "Employé muté, promu ou ayant changé de département",
"automated_actions": [
"Recalculer les attributions de rôles en fonction du nouveau code métier",
"Supprimer les accès aux applications du département précédent",
"Provisionner l'accès aux applications du nouveau département",
"Mettre à jour les appartenances aux groupes",
"Transférer le gestionnaire dans l'annuaire",
"Déclencher une révision des accès pour les droits conservés",
"Notifier le nouveau gestionnaire des accès hérités"
],
"valid_transitions": ["ACTIVE", "LEAVE_OF_ABSENCE", "TERMINATED"]
},
"LEAVE_OF_ABSENCE": {
"description": "Employé en congé prolongé (médical, parental, sabbatique)",
"automated_actions": [
"Désactiver la connexion interactive (conserver le compte)",
"Suspendre l'accès VPN",
"Configurer la réponse automatique d'absence",
"Déléguer la boîte aux lettres au gestionnaire",
"Conserver toutes les attributions de rôles pour le retour",
"Définir la date de réactivation à partir du flux RH"
],
"valid_transitions": ["ACTIVE", "TERMINATED"]
},
"TERMINATED": {
"description": "L'employé a quitté l'organisation",
"automated_actions": [
"Désactiver le compte AD immédiatement",
"Révoquer tous les accès aux applications",
"Révoquer l'accès VPN et à distance",
"Convertir la boîte aux lettres en boîte partagée (accès gestionnaire pendant 90 jours)",
"Transférer les fichiers OneDrive au gestionnaire",
"Supprimer de tous les groupes de sécurité et de distribution",
"Révoquer les jetons OAuth et les clés API",
"Effacer les données d'entreprise des appareils mobiles",
"Archiver l'enregistrement d'identité",
"Planifier la suppression du compte après la période de conservation"
],
"valid_transitions": ["REHIRE", "DELETED"]
},
"REHIRE": {
"description": "Ancien employé réembauché",
"automated_actions": [
"Réactiver l'enregistrement d'identité existant",
"Réinitialiser les identifiants et exiger une nouvelle inscription MFA",
"Provisionner en fonction du nouveau code métier (pas de l'accès précédent)",
"Marquer pour une révision d'accès renforcée dans les 30 premiers jours"
],
"valid_transitions": ["ACTIVE"]
},
"DELETED": {
"description": "Compte supprimé définitivement après la période de conservation",
"automated_actions": [
"Supprimer le compte AD",
"Supprimer l'archive de la boîte aux lettres électronique",
"Supprimer l'enregistrement d'identité de l'IGA",
"Générer le journal d'audit de suppression"
],
"valid_transitions": []
}
},
"retention_periods": {
"terminated_to_deleted": "90 jours (par défaut)",
"mailbox_retention": "90 jours en tant que boîte partagée",
"onedrive_retention": "30 jours d'accès gestionnaire, puis archivé",
"audit_log_retention": "7 ans pour la conformité"
}
}
Étape 2 : Implémenter l'intégration de la source faisant autorité
Connecter le système RH comme source unique de vérité pour les données d'identité :
"""
Intégration source RH - Connecteur Workday vers plateforme IGA
Interroge Workday pour les événements du cycle de vie des employés et déclenche le provisionnement.
"""
import requests
from datetime import datetime, timedelta
import logging
class WorkdayIdentityConnector:
def __init__(self, config):
self.base_url = config["workday_api_url"]
self.tenant = config["tenant"]
self.client_id = config["client_id"]
self.client_secret = config["client_secret"]
self.session = requests.Session()
self.logger = logging.getLogger("workday_connector")
def get_access_token(self):
"""S'authentifier à l'API REST Workday."""
token_url = f"{self.base_url}/ccx/oauth2/{self.tenant}/token"
response = self.session.post(token_url, data={
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret
})
response.raise_for_status()
return response.json()["access_token"]
def fetch_worker_changes(self, since_datetime):
"""Récupérer tous les événements du cycle de vie des travailleurs depuis la dernière synchronisation."""
headers = {"Authorization": f"Bearer {self.get_access_token()}"}
params = {
"Updated_From": since_datetime.isoformat(),
"Updated_Through": datetime.utcnow().isoformat(),
"Count": 100
}
workers = []
url = f"{self.base_url}/ccx/api/v1/{self.tenant}/workers"
while url:
response = self.session.get(url, headers=headers, params=params)
response.raise_for_status()
data = response.json()
workers.extend(data.get("data", []))
url = data.get("next", None)
params = {}
return workers
def map_lifecycle_event(self, worker):
"""Mapper les données d'un travailleur Workday à un événement du cycle de vie des identités."""
worker_data = worker.get("workerData", {})
employment = worker_data.get("employmentData", {})
personal = worker_data.get("personalData", {})
event = {
"employee_id": worker.get("id"),
"first_name": personal.get("legalName", {}).get("firstName"),
"last_name": personal.get("legalName", {}).get("lastName"),
"email": worker_data.get("emailAddress"),
"job_code": employment.get("jobProfile", {}).get("id"),
"job_title": employment.get("jobProfile", {}).get("name"),
"department": employment.get("organization", {}).get("name"),
"department_code": employment.get("organization", {}).get("id"),
"manager_id": employment.get("managerId"),
"location": employment.get("location", {}).get("name"),
"cost_center": employment.get("costCenter", {}).get("id"),
"hire_date": employment.get("hireDate"),
"termination_date": employment.get("terminationDate"),
"status": employment.get("status"),
"worker_type": employment.get("workerType"),
}
# Déterminer la transition du cycle de vie
if event["status"] == "Active" and event["hire_date"]:
hire_date = datetime.fromisoformat(event["hire_date"])
if hire_date > datetime.utcnow():
event["lifecycle_event"] = "PRE_HIRE"
else:
event["lifecycle_event"] = "JOINER"
elif event["status"] == "Active":
event["lifecycle_event"] = "MOVER" # Changement de département ou de rôle
elif event["status"] == "Terminated":
event["lifecycle_event"] = "LEAVER"
elif event["status"] == "On Leave":
event["lifecycle_event"] = "LEAVE_OF_ABSENCE"
return event
def process_lifecycle_events(self, since_datetime):
"""Boucle de traitement principale pour les événements du cycle de vie des identités."""
workers = self.fetch_worker_changes(since_datetime)
events = []
for worker in workers:
event = self.map_lifecycle_event(worker)
events.append(event)
self.logger.info(
f"Lifecycle event: {event['lifecycle_event']} for "
f"{event['first_name']} {event['last_name']} "
f"(EmpID: {event['employee_id']})"
)
return events
Étape 3 : Implémenter l'exploration des rôles et l'accès de naissance
Définir les rôles en fonction des fonctions pour un provisionnement automatisé :
"""
Moteur d'exploration des rôles
Analyse les modèles d'accès existants pour dériver les définitions de rôles
pour le provisionnement de naissance (automatique).
"""
import pandas as pd
from collections import Counter
from itertools import combinations
class RoleMiningEngine:
def __init__(self, access_data):
"""
access_data: DataFrame avec les colonnes
[employee_id, job_code, department, application, entitlement]
"""
self.access_data = access_data
def mine_birthright_roles(self, min_assignment_pct=0.8):
"""
Identifier les droits qui doivent être attribués automatiquement
en fonction du code métier. Si 80%+ des utilisateurs avec le même code métier
ont un droit, il devient un accès de naissance.
"""
birthright_roles = {}
for job_code, group in self.access_data.groupby("job_code"):
total_users = group["employee_id"].nunique()
entitlement_counts = group.groupby(
["application", "entitlement"]
)["employee_id"].nunique()
birthright_entitlements = []
for (app, ent), count in entitlement_counts.items():
pct = count / total_users
if pct >= min_assignment_pct:
birthright_entitlements.append({
"application": app,
"entitlement": ent,
"assignment_percentage": round(pct * 100, 1),
"user_count": count
})
if birthright_entitlements:
birthright_roles[job_code] = {
"job_code": job_code,
"total_users": total_users,
"birthright_entitlements": birthright_entitlements
}
return birthright_roles
def detect_role_explosion(self):
"""Identifier les rôles avec un chevauchement excessif indiquant un besoin de consolidation."""
roles = self.access_data.groupby("job_code").apply(
lambda x: set(zip(x["application"], x["entitlement"]))
)
overlap_report = []
for (role1, ents1), (role2, ents2) in combinations(roles.items(), 2):
if len(ents1) == 0 or len(ents2) == 0:
continue
overlap = len(ents1 & ents2)
max_size = max(len(ents1), len(ents2))
overlap_pct = overlap / max_size * 100
if overlap_pct > 70:
overlap_report.append({
"role_1": role1,
"role_2": role2,
"role_1_entitlements": len(ents1),
"role_2_entitlements": len(ents2),
"overlapping_entitlements": overlap,
"overlap_percentage": round(overlap_pct, 1),
"recommendation": "CONSOLIDATE" if overlap_pct > 90 else "REVIEW"
})
return sorted(overlap_report, key=lambda x: x["overlap_percentage"], reverse=True)
def find_orphaned_access(self):
"""
Trouver les droits qui ne correspondent plus à aucune définition de rôle.
Ce sont des exceptions qui se sont accumulées au fil du temps.
"""
# Obtenir les définitions de naissance
birthright = self.mine_birthright_roles(min_assignment_pct=0.5)
orphaned = []
for _, row in self.access_data.iterrows():
job_birthright = birthright.get(row["job_code"], {})
expected_ents = set()
for ent in job_birthright.get("birthright_entitlements", []):
expected_ents.add((ent["application"], ent["entitlement"]))
current_ent = (row["application"], row["entitlement"])
…
[truncated for translation]