Permissions Python : comparer deux CSV avec XOR et IntFlag

Deux cartes de permissions reliées à XOR, avec un ajout turquoise et un retrait orange.

Deux fichiers CSV décrivent les permissions de vos utilisateurs avant et après une modification. Pour comparer des permissions Python avec XOR, l’opérateur ^ repère les bits qui ont changé. Il ne dit cependant pas, à lui seul, quels droits ont été ajoutés ou retirés. Ce cas pratique produit les deux listes, rejette les bits inconnus et conserve la différence entre une ligne absente et une permission nulle.

Exemple : Alice passe du masque 3 au masque 5. Le XOR vaut 6, mais le résultat utile est plus précis : EXECUTER ajouté, ECRIRE retiré, LIRE conservé. Le script fourni compare uniquement des fichiers locaux ; il ne modifie aucun compte ni aucune permission réelle.

Télécharger le ZIP gratuit The Pingouin — code et tests. Sans inscription. Archive extraite et rejouée, 10 tests de référence réussis. Les données sont fictives ; les résultats présentés proviennent du code exécuté.

Fixer le sens des trois bits et des deux inventaires

Nous utilisons trois droits fictifs : LIRE vaut 1, ECRIRE vaut 2 et EXECUTER vaut 4. Chaque droit occupe donc un bit distinct. Le masque 3 correspond à LIRE et ECRIRE ; le masque 5 à LIRE et EXECUTER. La valeur 0 signifie qu’aucun de ces trois droits n’est présent.

Les CSV sont deux inventaires complets du même périmètre. Une ligne présente seulement dans le premier est signalée comme retirée de l’inventaire ; une ligne présente seulement dans le second est signalée comme ajoutée. Cette convention ne convient pas à un fichier contenant uniquement les dernières modifications : dans un export partiel, une absence ne signifie pas nécessairement un retrait.

Voici avant.csv :

utilisateur,permissions
alice,3
bilal,5
chloe,1
eric,0

Et apres.csv :

utilisateur,permissions
alice,5
bilal,1
dina,3
eric,0

Ces noms et permissions sont fabriqués pour l’exemple. Les nombres sont écrits en décimal : le lecteur n’accepte ni préfixe binaire comme 0b011, ni noms de droits à la place du masque. Pour revoir les opérations sur les bits, consultez le guide de XOR en Python.

Séparer les changements, les ajouts et les retraits

Notons a le masque avant modification et b le masque après modification. Les trois calculs sont :

changes = a ^ b
ajoutes = (~a & b) & 7
retires = (a & ~b) & 7

Le masque connu vaut 7, soit les trois bits autorisés à 1. Le calcul des ajouts conserve les droits présents après et absents avant. Celui des retraits fait l’inverse. Pour Alice, 011 ^ 101 = 110 : le bit de valeur 4 est ajouté, celui de valeur 2 est retiré et celui de valeur 1 reste inchangé.

Inverser les deux états ne change pas leur XOR : 3 ^ 5 et 5 ^ 3 valent tous deux 6. En revanche, les ajouts et les retraits sont inversés. C’est pourquoi un rapport contenant seulement « masque changé : 6 » ne suffit pas pour comprendre la modification.

Les opérations sont réalisées ici sur des entiers validés. En Python, le complément ~ d’un entier n’est pas limité implicitement à trois bits ; le masque final explicite le périmètre retenu. La documentation des opérations binaires sur les entiers décrit ces opérateurs.

Utiliser IntFlag sans accepter silencieusement un bit inconnu

IntFlag donne des noms aux droits et permet de combiner leurs valeurs. Attention à sa politique par défaut : KEEP conserve les bits inconnus. Déclarer une énumération ne suffit donc pas à valider automatiquement un fichier externe. Notre classe utilise boundary=STRICT, disponible depuis Python 3.11. Le lecteur contrôle aussi explicitement les bits autorisés afin de produire un message compréhensible. Voir la documentation de FlagBoundary.

Un masque 8 ou 9 est rejeté : le bit de valeur 8 n’a aucun sens dans notre convention. Le script refuse également les valeurs négatives, les décimaux et les cellules vides. Une valeur vide n’est jamais transformée en zéro. Pour ajouter un quatrième droit, il faut faire évoluer la convention des deux inventaires et le code de validation ensemble.

Les fichiers doivent être en UTF-8, avec ou sans BOM, séparés par des virgules et commencer par l’en-tête utilisateur,permissions. Les identifiants sont uniques, commencent par une lettre ASCII et contiennent au plus 40 caractères parmi lettres ASCII, chiffres, tirets et soulignements. Le lecteur retire les espaces autour des cellules et refuse les lignes ayant un nombre de colonnes incorrect.

Lire le rapport sans confondre absence et zéro

Le script produit les cinq lignes suivantes. « AUCUNE » désigne une liste de droits vide, pas une donnée manquante.

Utilisateur Avant → après XOR Ajoutés Retirés Statut
alice 3 → 5 6 EXECUTER ECRIRE modifie
bilal 5 → 1 4 AUCUNE EXECUTER modifie
chloe 1 → 0 1 AUCUNE LIRE ligne retiree
dina 0 → 3 3 LIRE|ECRIRE AUCUNE ligne ajoutee
eric 0 → 0 0 AUCUNE AUCUNE inchange

Chloe est absente du second inventaire ; Dina apparaît uniquement dans celui-ci. Le programme emploie zéro pour calculer les différences de droits sur ces lignes, mais conserve aussi present_avant, present_apres et le statut. La disparition d’une ligne ne prouve pas qu’un compte a été supprimé dans une application : elle décrit seulement la comparaison des deux fichiers.

Eric est présent dans les deux fichiers avec un masque nul. Il reste donc « inchangé ». Si une nouvelle ligne apparaissait avec zéro permission, son XOR vaudrait aussi zéro, mais son statut serait « ligne ajoutée ». Ce cas montre pourquoi il faut comparer la présence de l’entrée et son masque, pas seulement les bits.

Exécuter le script et récupérer les deux exports

L’exemple utilise uniquement la bibliothèque standard de Python. Après extraction du ZIP, ouvrez un terminal dans le dossier et lancez :

python permissions_csv.py
python test_exemple.py

La première commande utilise les deux CSV fournis et écrit sortie/comparaison.json ainsi que sortie/comparaison.csv. La seconde exécute les tests. Pour choisir vos propres fichiers et un dossier distinct :

python permissions_csv.py avant.csv apres.csv --sortie comparaison-octobre

Les deux fichiers de résultat sont remplacés s’ils existent déjà dans le dossier choisi. Le programme valide les deux entrées avant d’écrire le rapport. Un masque inconnu fait échouer la commande avec un code de sortie 2. Un fichier contenant uniquement l’en-tête est accepté comme un inventaire vide ; cette interprétation suppose toujours un export complet.

Afficher le script Python complet et commenté
"""Comparer deux inventaires complets de permissions, sans modifier de compte."""
import argparse
import csv
import json
import re
from enum import IntFlag, STRICT
from pathlib import Path


class Permission(IntFlag, boundary=STRICT):
    LIRE = 1
    ECRIRE = 2
    EXECUTER = 4


MASQUE_CONNU = int(Permission.LIRE | Permission.ECRIRE | Permission.EXECUTER)
IDENTIFIANT = re.compile(r"[A-Za-z][A-Za-z0-9_-]{0,39}\Z")


def valider_masque(valeur):
    if type(valeur) is not int or valeur < 0:
        raise ValueError("Le masque doit être un entier positif ou nul.")
    if valeur & ~MASQUE_CONNU:
        raise ValueError(f"Bits inconnus dans le masque {valeur}.")
    return Permission(valeur)


def lire_inventaire(chemin):
    with Path(chemin).open(encoding="utf-8-sig", newline="") as fichier:
        lecteur = csv.reader(fichier, strict=True)
        entete = next(lecteur, None)
        if entete is None or [c.strip() for c in entete] != ["utilisateur", "permissions"]:
            raise ValueError("En-tête attendu : utilisateur,permissions")
        inventaire = {}
        for numero, ligne in enumerate(lecteur, 2):
            if len(ligne) != 2:
                raise ValueError(f"Ligne {numero} : deux colonnes attendues.")
            utilisateur, texte = [c.strip() for c in ligne]
            if not IDENTIFIANT.fullmatch(utilisateur) or utilisateur in inventaire:
                raise ValueError(f"Ligne {numero} : identifiant invalide ou répété.")
            if not re.fullmatch(r"[0-9]{1,3}", texte):
                raise ValueError(f"Ligne {numero} : masque décimal entier attendu.")
            inventaire[utilisateur] = valider_masque(int(texte))
    return inventaire


def noms(masque):
    permission = valider_masque(int(masque))
    return "|".join(p.name for p in Permission if p & permission) or "AUCUNE"


def changements(avant, apres):
    a, b = int(valider_masque(avant)), int(valider_masque(apres))
    return {
        "changes": a ^ b,
        "ajoutes": (~a & b) & MASQUE_CONNU,
        "retires": (a & ~b) & MASQUE_CONNU,
    }


def comparer(avant, apres):
    """Une absence signifie une ligne absente de cet inventaire complet."""
    resultat = []
    for utilisateur in sorted(set(avant) | set(apres)):
        a, b = int(avant.get(utilisateur, 0)), int(apres.get(utilisateur, 0))
        delta = changements(a, b)
        if utilisateur not in avant:
            statut = "ligne_ajoutee"
        elif utilisateur not in apres:
            statut = "ligne_retiree"
        else:
            statut = "modifie" if delta["changes"] else "inchange"
        resultat.append({
            "utilisateur": utilisateur, "present_avant": utilisateur in avant,
            "present_apres": utilisateur in apres, "avant": a, "apres": b,
            "xor": delta["changes"], "ajoutes": noms(delta["ajoutes"]),
            "retires": noms(delta["retires"]), "statut": statut,
        })
    return resultat


def enregistrer(lignes, dossier):
    dossier = Path(dossier)
    dossier.mkdir(parents=True, exist_ok=True)
    (dossier / "comparaison.json").write_text(
        json.dumps(lignes, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    colonnes = ["utilisateur", "present_avant", "present_apres", "avant", "apres",
                "xor", "ajoutes", "retires", "statut"]
    with (dossier / "comparaison.csv").open("w", encoding="utf-8", newline="") as fichier:
        writer = csv.DictWriter(fichier, fieldnames=colonnes)
        writer.writeheader()
        writer.writerows(lignes)


def main():
    ici = Path(__file__).resolve().parent
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("avant", nargs="?", default=str(ici / "avant.csv"))
    parser.add_argument("apres", nargs="?", default=str(ici / "apres.csv"))
    parser.add_argument("--sortie", default="sortie")
    args = parser.parse_args()
    try:
        lignes = comparer(lire_inventaire(args.avant), lire_inventaire(args.apres))
    except (ValueError, OSError, csv.Error) as erreur:
        parser.exit(2, f"Erreur : {erreur}\n")
    enregistrer(lignes, args.sortie)
    print(json.dumps(lignes, ensure_ascii=False, indent=2))


if __name__ == "__main__":
    main()

Vérifier les 64 transitions possibles

Trois bits définissent huit masques valides, de 0 à 7. Il existe donc 64 paires avant/après. Un des dix tests les parcourt toutes et compare les résultats binaires à un témoin utilisant des ensembles de droits : différence pour les ajouts et retraits, différence symétrique pour les changements. Il vérifie aussi que les droits ajoutés et retirés ne se recouvrent pas.

Les autres tests couvrent notamment les bits inconnus, les doublons d’identifiants, les cellules vides, l’inventaire vide, la présence avec zéro droit et les exports. L’archive a été extraite et ses programmes rejoués. Cela vérifie le comportement du cas fourni ; ce n’est pas un audit des règles d’accès d’un système réel.

Adapter la comparaison à votre convention

Avant d’utiliser vos données, vérifiez que les deux exports décrivent les mêmes droits et le même périmètre. Deux systèmes peuvent donner un sens différent au bit 4. Des autorisations héritées, des groupes, des refus explicites ou des droits propres à une ressource demandent un modèle plus riche que trois bits par utilisateur.

Pour prolonger le travail sur les fichiers, consultez les exercices CSV gratuits ou le cas d’affectation de tâches depuis un CSV avec contraintes. Le point commun est la validation du fichier avant d’interpréter son résultat.

Exemple original exécuté le 23 octobre 2026 avec Python 3.12.14, bibliothèque standard uniquement. Versions et consignes dans le téléchargement. Miniature illustrative générée avec image_gen. Notre méthode éditoriale.

About Salah YAHIAOUI

Salah YAHIAOUI publie sur The Pingouin des tutoriels Python et des décryptages sur l’intelligence artificielle. Les cas pratiques récents précisent leurs données, leurs tests et leurs limites. À propos, méthode éditoriale et contact.

View all posts by Salah YAHIAOUI →

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. Découvrez comment les données de vos commentaires sont traitées.