Automatiser le contrôle des responsables hiérarchiques dans Google Workspace : un pas de plus vers la qualité des données RH

Dans un contexte où la fiabilité des données d’entreprise est devenue un véritable enjeu de gouvernance, je vous propose un automatisme de contrôle important dans notre environnement Google Workspace : l’identification des comptes utilisateurs dont le responsable hiérarchique n’existe plus ou est inactif dans l’annuaire.

Objectif : maintenir un annuaire Google Workspace à jour et cohérent

Les profils des utilisateurs Google Workspace intègrent une information essentielle : le responsable hiérarchique. Cette donnée est non seulement utile pour les processus RH (onboarding, offboarding, organigramme…), mais elle conditionne également certains flux automatisés, droits d’accès, et workflows internes.

Or, au fil des départs ou changements de poste, il arrive que des employés conservent un responsable hiérarchique qui n’est plus actif ou dont le compte a été supprimé. Ces incohérences, si elles ne sont pas détectées rapidement, peuvent engendrer des erreurs dans les process RH, de sécurité ou de pilotage.

Ce que fait le script

Un script Google Apps Script, exécuté automatiquement chaque jour, réalise désormais les actions suivantes :

  1. Analyse complète des utilisateurs du domaine : il récupère tous les profils et vérifie la validité de l’adresse e-mail du responsable hiérarchique.
  2. Détection des incohérences : s’il identifie un responsable qui n’existe plus dans l’annuaire, il classe ce cas comme une anomalie.
  3. Envoi de rapports ciblés :
    • Quotidiennement, un rapport est adressé au service en charge de Google Workspace.
    • Hebdomadairement (le mercredi), un rapport synthétique est transmis au service RH, facilitant une prise de décision centralisée et une mise à jour dans les outils métiers.

Les fonctionnalités

Ce développement apporte plusieurs fonctionnalités concrètes :

  • Surveillance continue : chaque jour, l’annuaire est passé au crible, sans action manuelle.
  • Réactivité renforcée : en cas de problème, une alerte est envoyée immédiatement.
  • Lien RH renforcé : le script prend en compte les jours spécifiques d’envoi aux RH pour éviter la surinformation et améliorer le pilotage.
  • Exclusion ciblée : certains matricules peuvent être exclus du contrôle pour éviter les faux positifs (ex : comptes de service ou cas particuliers connus).

Pourquoi c’est important ?

Ce projet illustre une approche proactive de la qualité des données. En automatisant ce type de contrôle :

  • On fiabilise les processus RH.
  • On réduit les erreurs humaines.
  • On améliore la sécurité en évitant les comptes orphelins ou mal référencés.
  • On gagne du temps : plus besoin de vérifier manuellement des centaines de profils.

🧩 Ce projet n’est qu’un exemple parmi d’autres de ce que l’automatisation peut apporter aux équipes DSI et RH lorsqu’elles travaillent main dans la main : une meilleure fiabilité, une meilleure visibilité, et une meilleure réactivité.

Le script

/**
 * @OnlyCurrentDoc
 *
 * DESCRIPTION
 * Ce script identifie les comptes utilisateurs Google Workspace dont le responsable hiérarchique,
 * tel que défini dans leur profil, n'existe plus ou est inactif dans l'annuaire Google.
 * Il envoie un rapport quotidien à un administrateur et un rapport hebdomadaire au service RH.
 *
 */

// --- CONFIGURATION CENTRALE ---
const CONFIG = {
  // Email du destinataire du rapport HEBDOMADAIRE (ex: une liste de distribution RH)
  RECIPIENT_EMAIL_RH: "rh@votredomaine.com", //
  // NOUVEAU: Email du destinataire du rapport QUOTIDIEN
  RECIPIENT_EMAIL_QUOTIDIEN: "dsi@votredomaine.com",
  // Email utilisé pour le champ "replyTo"
  REPLY_TO_EMAIL: "dsi@votredomaine.com",
  // Nom de l'expéditeur du mail
  SENDER_NAME: "Google Workspace Alerts",
  // ID Client Google Workspace (disponible dans la console d'administration)
  CUSTOMER_ID: "xxxxxxx",
  // Liste des matricules à ignorer lors de la vérification.
  MATRICULES_A_IGNORER: ["xxxxx"],
  // Jour d'exécution du script pour le rapport RH (0=Dimanche, 1=Lundi, ..., 6=Samedi)
  JOUR_EXECUTION_RH: 3 // Mercredi
};

/**
 * Fonction principale déclenchée par un trigger temporel QUOTIDIEN.
 * Elle orchestre la récupération des données, l'analyse et l'envoi des rapports.
 */
const lancerVerificationQuotidienne = () => {
  try {
    // Étape 1 : Récupérer tous les utilisateurs et leurs informations pertinentes.
    const tousLesUtilisateurs = recupererTousLesUtilisateurs();

    // Étape 2 : Créer un ensemble (Set) d'e-mails valides pour une recherche rapide.
    const emailsValides = new Set(tousLesUtilisateurs.map(u => u.primaryEmail));

    // Étape 3 : Identifier les incohérences.
    const incoherences = [];
    for (const utilisateur of tousLesUtilisateurs) {
      const emailResponsable = utilisateur.relations?.find(rel => rel.type === 'manager')?.value;
      const matricule = utilisateur.customSchemas?.Ressources_humaines?.Matricule || "";

      if (emailResponsable && !CONFIG.MATRICULES_A_IGNORER.includes(matricule)) {
        if (!emailsValides.has(emailResponsable)) {
          incoherences.push({
            nom: utilisateur.name.fullName,
            matricule: matricule,
            emailEmploye: utilisateur.primaryEmail,
            emailResponsableIncorrect: emailResponsable
          });
        }
      }
    }

    // Étape 4 : Gérer l'envoi des e-mails si des incohérences sont trouvées.
    if (incoherences.length > 0) {
      const jourActuel = new Date().getDay();
      
      // Envoi du rapport quotidien à l'administrateur
      envoyerRapport(incoherences, CONFIG.RECIPIENT_EMAIL_QUOTIDIEN, "quotidien");
      Logger.log(`${incoherences.length} incohérences trouvées. Rapport quotidien envoyé à ${CONFIG.RECIPIENT_EMAIL_QUOTIDIEN}.`);

      // Envoi du rapport hebdomadaire aux RH, seulement le jour J
      if (jourActuel === CONFIG.JOUR_EXECUTION_RH) {
        envoyerRapport(incoherences, CONFIG.RECIPIENT_EMAIL_RH, "hebdomadaire");
        Logger.log(`Rapport hebdomadaire envoyé à ${CONFIG.RECIPIENT_EMAIL_RH}.`);
      }

    } else {
      Logger.log("Aucune incohérence de responsable détectée. Aucun rapport envoyé.");
    }

  } catch (e) {
    Logger.log(`Erreur critique lors de l'exécution du script : ${e.toString()}\n${e.stack}`);
    MailApp.sendEmail(CONFIG.RECIPIENT_EMAIL_QUOTIDIEN, "Erreur Script RH", `Le script a échoué avec l'erreur : ${e.toString()}`);
  }
};

/**
 * Récupère la liste complète des utilisateurs du domaine avec les champs nécessaires.
 * @returns {GoogleAppsScript.AdminDirectory.Schema.User[]} Un tableau d'objets utilisateur.
 */
const recupererTousLesUtilisateurs = () => {
  let utilisateurs = [];
  let pageToken;
  do {
    const page = AdminDirectory.Users.list({
      customer: CONFIG.CUSTOMER_ID,
      projection: 'full',
      maxResults: 500,
      pageToken: pageToken
    });
    if (page.users) {
      utilisateurs = utilisateurs.concat(page.users);
    }
    pageToken = page.nextPageToken;
  } while (pageToken);
  
  return utilisateurs;
};

/**
 * Construit et envoie un e-mail de rapport.
 * @param {Object[]} incoherences - Un tableau d'objets contenant les détails de chaque incohérence.
 * @param {string} destinataire - L'adresse e-mail du destinataire.
 * @param {string} typeRapport - "quotidien" ou "hebdomadaire" pour adapter le sujet.
 */
const envoyerRapport = (incoherences, destinataire, typeRapport) => {
  const dateDuJour = new Date().toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', year: 'numeric' });
  let sujetEmail = "";

  if (typeRapport === "quotidien") {
    sujetEmail = `Rapport quotidien des incohérences Annuaire (${dateDuJour})`;
  } else { // hebdomadaire
    sujetEmail = `Action requise : Mise à jour des responsables hiérarchiques (${dateDuJour})`;
  }
  
  const corpsEmailHtml = creerCorpsHtmlEmail(incoherences, typeRapport);

  GmailApp.sendEmail(destinataire, sujetEmail, "", {
    htmlBody: corpsEmailHtml,
    name: CONFIG.SENDER_NAME,
    replyTo: CONFIG.REPLY_TO_EMAIL
  });
};


/**
 * Génère le corps HTML de l'e-mail à partir des données d'incohérence.
 * @param {Object[]} incoherences - Les données à afficher.
 * @param {string} typeRapport - Le type de rapport pour adapter le texte si nécessaire.
 * @returns {string} Le corps de l'email au format HTML.
 */
const creerCorpsHtmlEmail = (incoherences, typeRapport) => {
  const lignesTableau = incoherences.map(item => `
    <tr>
      <td>${item.nom || 'N/A'}</td>
      <td>${item.matricule || 'N/A'}</td>
      <td>${item.emailEmploye || 'N/A'}</td>
      <td>${item.emailResponsableIncorrect || 'N/A'}</td>
    </tr>
  `).join('');

  const anneeActuelle = new Date().getFullYear();
  
  // Le message d'action est le même pour les deux, car il décrit la solution au problème.
  const messageAction = `Pour garantir la cohérence de nos données, veuillez s'il vous plaît mettre à jour les informations de responsable pour ces employés dans le système RH. Les modifications seront ensuite synchronisées avec Google Workspace.`;

  return `
  <!DOCTYPE html>
  <html lang="fr">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
    <style>
      body { font-family: 'Roboto', Arial, sans-serif; margin: 0; padding: 0; background-color: #f4f5f7; color: #333; }
      .wrapper { padding: 20px; }
      .container { background-color: #ffffff; border: 1px solid #e0e0e0; padding: 32px; border-radius: 8px; max-width: 700px; margin: 0 auto; box-shadow: 0 2px 4px rgba(0,0,0,0.05); }
      .header { padding-bottom: 16px; margin-bottom: 24px; border-bottom: 1px solid #e0e0e0; }
      .header h1 { font-size: 24px; font-weight: 500; color: #202124; margin: 0; }
      .content p { margin: 0 0 16px 0; line-height: 1.6; font-size: 15px; }
      .summary { background-color: #e8f0fe; color: #1967d2; padding: 12px 16px; border-radius: 6px; margin-bottom: 20px; font-size: 14px; }
      table { width: 100%; border-collapse: collapse; margin-top: 20px; margin-bottom: 20px; font-size: 14px; }
      th, td { text-align: left; padding: 12px 15px; border-bottom: 1px solid #e0e0e0; }
      th { font-weight: 500; color: #5f6368; background-color: #f8f9fa; }
      tr:last-child td { border-bottom: none; }
      .action-box { margin-top: 24px; padding: 20px; background-color: #f8f9fa; border-radius: 6px; }
      .action-box h2 { font-size: 16px; margin-top: 0; margin-bottom: 12px; color: #202124; }
      .footer { margin-top: 32px; padding-top: 16px; border-top: 1px solid #e0e0e0; font-size: 12px; color: #5f6368; text-align: center; }
    </style>
  </head>
  <body>
    <div class="wrapper">
      <div class="container">
        <div class="header">
          <h1>Anomalies de la base Annuaire</h1>
        </div>
        <div class="content">
          <p>Bonjour,</p>
          <p>Notre vérification automatisée a identifié que les employés suivants ont un responsable hiérarchique qui n'est plus présent dans l'annuaire de l'entreprise (compte supprimé ou inactif).</p>
          <div class="summary">
            <strong>${incoherences.length}</strong> compte(s) à vérifier.
          </div>
          <table>
            <thead>
              <tr>
                <th>Employé</th>
                <th>Matricule</th>
                <th>Email Employé</th>
                <th>Email du Responsable (incorrect)</th>
              </tr>
            </thead>
            <tbody>
              ${lignesTableau}
            </tbody>
          </table>
        </div>
        <div class="action-box">
          <h2>Actions recommandées</h2>
          <p>${messageAction}</p>
        </div>
        <div class="footer">
          <p>Cet e-mail est un rapport automatique. Pour toute question, veuillez répondre à cet e-mail.</p>
          <p>&copy; ${anneeActuelle} DSI</p>
        </div>
      </div>
    </div>
  </body>
  </html>
  `;
};