Aller au contenu

Le modèle RBAC

Repod contrôle l'accès via trois couches indépendantes : un rôle global (ce qu'un utilisateur peut faire), une portée de distribution (sur quels dépôts un utilisateur peut agir), et une portée de machine (sur quelles machines d'inventaire un utilisateur peut agir). Cette page explique comment les trois couches se combinent, et comment chacune des deux couches de portée — ainsi que le verrou associé de filtres de contenu sur les noms de paquets — fonctionne réellement. Pour les définitions de rôle de base et la matrice de permissions complète, voir Rôles & permissions ; cette page ne répète pas cette matrice.

Pourquoi trois couches

Un RBAC global à 5 rôles (reader < uploader/auditor < maintainer < admin) répond à la question « cet utilisateur peut-il uploader des paquets, en général ? ». Il ne peut pas répondre à « cet utilisateur peut-il uploader spécifiquement vers la distribution finance-internal ? » ni à « cet utilisateur peut-il voir la posture CVE des machines appartenant à une autre équipe ? ». Ce sont des questions de portée, pas des questions de capacité, et Repod y répond avec deux couches opt-in distinctes, en plus du rôle global :

  • distribution_access — restreint à quels codenames de distribution (jammy, almalinux9, …) un utilisateur peut accéder.
  • machine_access — restreint à quelles machines d'inventaire un utilisateur peut accéder.

Un troisième mécanisme, les filtres de contenu, n'est pas du RBAC du tout — c'est une politique d'allow/deny sur les noms de paquets, par distribution, évaluée indépendamment de qui effectue l'upload. Il est documenté ici car il se situe aux mêmes points d'entrée que distribution_access et est facile à confondre avec lui.

Comment les couches se combinent

La portée de distribution et la portée de machine se combinent avec le rôle global en utilisant ET, jamais OU. La dépendance de rôle global (get_uploader_user, get_maintainer_user, get_admin_user, get_auditor_user) continue de conditionner l'action — un reader ne peut pas uploader un paquet, quoi que dise distribution_access. Les couches de portée ajoutent une restriction par-dessus une action déjà autorisée ; elles n'accordent jamais une capacité que le rôle global ne possède pas déjà.

flowchart TD
    Req["Requête : installer un paquet\nsur la machine M dans la distribution D"] --> Role{"Vérification du rôle global\nget_uploader_user / get_maintainer_user / …"}
    Role -->|"rôle insuffisant"| R403["403 Forbidden"]
    Role -->|"rôle suffisant"| Admin{"rôle == admin ?"}
    Admin -->|"oui"| Allow["Poursuivre\n(admin contourne les deux couches de portée)"]
    Admin -->|"non"| Dist{"distribution_access :\ncheck_distribution_access(user, D)"}
    Dist -->|"refusé"| D404["404 Not Found\n(anti-fuite : identique à D inexistant)"]
    Dist -->|"autorisé"| Mach{"machine_access :\ncheck_machine_access(user, M)"}
    Mach -->|"refusé"| M404["404 Not Found\n(anti-fuite : identique à M inexistant)"]
    Mach -->|"autorisé"| Allow

Les deux vérifications de portée sont ouvertes par défaut lorsqu'aucune règle n'a jamais été créée pour la ressource en question — une installation neuve, ou une installation existante qui n'a jamais configuré distribution_access/machine_access, se comporte exactement comme si ces couches n'existaient pas. Elles ne commencent à restreindre l'accès qu'au moment où un administrateur ajoute la première ligne pour une distribution ou une machine donnée. Il s'agit d'une garantie de compatibilité délibérée : aucune mise à niveau vers une version livrant ces couches ne peut verrouiller une installation existante hors de ses propres dépôts ou machines.

admin contourne toujours distribution_access et machine_access de manière inconditionnelle. C'est intentionnel, pas un oubli : sans un contournement garanti, une distribution ou une machine pourrait se retrouver sans aucun utilisateur autorisé capable de la gérer (par exemple, si un rôle/groupe référencé par une règle d'accès est ultérieurement supprimé).

Portée au niveau distribution (distribution_access)

distribution_access restreint à quels codenames de distribution un utilisateur peut accéder. Les distributions n'ont pas de ligne en base de données propre — les codenames forment une liste fixe et codée en dur par format (services/distributions_apt.py, distributions_rpm.py, distributions_apk.py) — donc les règles d'accès vivent dans leur propre table, une ligne par (codename, principal_type, principal_id), où principal_type vaut role ou group et principal_id référence un rôle personnalisé ou un groupe.

  • Ouvert par défaut. Un codename sans aucune ligne dans distribution_access est accessible à tout utilisateur authentifié dont le rôle global autorise déjà l'action. Ajouter la première ligne pour un codename est ce qui active la restriction.
  • Combinaison de plusieurs règles. Toutes les lignes pour un codename se combinent en union — une correspondance de rôle ou de groupe suffit ; il n'y a pas de sémantique d'intersection ici.
  • 404, pas 403, en cas de refus. ensure_distribution_access() lève une erreur 404, pas 403, de sorte qu'un codename restreint et inaccessible est indistinguable d'un codename qui n'existe pas. Les endpoints de listing (GET /distributions/) filtrent silencieusement les codenames restreints via filter_accessible_codenames() plutôt que de générer une erreur sur eux.
  • Identités par token API. check_distribution_access() accepte un paramètre optionnel role. Sans lui, le rôle serait redérivé en recherchant le nom d'utilisateur de l'appelant dans la table users — qui n'a aucune ligne pour une identité de token API (token:<name>), faisant silencieusement retomber cette identité sur reader et perdant son rôle réellement accordé. Les appelants qui connaissent déjà le rôle résolu (les chemins d'authentification par token API) le transmettent explicitement ; les points d'entrée authentifiés par JWT ne sont pas affectés.

Appliqué sur : GET/POST/DELETE /distributions/{codename}/access (gestion de l'ACL elle-même, réservée à admin), GET /distributions/{codename}/packages, POST /distributions/promote et /distributions/migrate (vérifié à la fois sur from_dist et to_dist), POST /upload/ et /upload/stream, et les endpoints d'import (POST /import/fetch, /import/batch, plus le filtre de distribution sur GET /import/search?distro=).

Les téléchargements de paquets sont eux aussi verrouillés, avec une exception documentée

Les téléchargements RPM et APK sont entièrement fermés par distribution_access via auth_request de nginx, car l'index par distribution généré par chaque format est autonome — un client DNF/apk n'a jamais besoin de sortir de l'arborescence de sa propre distribution. Le répertoire pool/ partagé d'APT n'est fermé que partiellement : un .deb qui a un jour été promu dans une distribution via reprepro copy reste accessible depuis pool/ ensuite, quelle que soit la distribution contre laquelle il est actuellement enregistré, car l'opération de promotion de reprepro laisse le fichier présent à la fois dans la distribution source et la distribution de destination, sans séparation de stockage par distribution. Un paquet uploadé ou importé directement dans une distribution restreinte et jamais promu est intégralement protégé.

Portée au niveau machine (machine_access)

machine_access restreint à quelles machines d'inventaire un utilisateur peut accéder. Il étend le même modèle conceptuel que distribution_access (ouvert par défaut, contournement admin, ET avec le rôle global, anti-fuite 404) à un second type de ressource, en utilisant sa propre table plutôt qu'en réutilisant le schéma de distribution_access.

machine_access possède deux axes de principaux indépendants :

Axe Valeurs Signification
user_principal_type role | group À qui la règle accorde l'accès
machine_principal_type tag | client À quelle(s) machine(s) la règle s'applique

Il n'existe pas d'entité « groupe de machines » dédiée — un groupe de machines est simplement un tag d'inventaire (inventory_clients.tags, en texte libre). Une règle de type client référence directement l'id d'une machine.

  • Une surcharge client remplace les règles de tag, elle ne fusionne jamais avec elles. Si une règle référence directement l'id d'une machine, cet ensemble de règles constitue l'intégralité de l'ensemble de règles effectif pour cette machine — les règles dérivées des tags de la machine sont totalement ignorées, pas combinées. En l'absence de surcharge au niveau client, toutes les règles issues de chaque tag porté par la machine se combinent en union : un seul rôle ou groupe correspondant suffit. Ce choix d'union est délibéré — une intersection signifierait qu'étiqueter une machine avec un second tag, sans rapport, pourrait silencieusement révoquer l'accès d'une équipe existante, une surprise pire que le compromis de l'union (qu'un tag multi-usage élargit l'accès à une machine).
  • resolve_scope_for_aggregates() calcule la portée effective pour les endpoints d'agrégation/résumé (inventaire à l'échelle du parc, conformité, tableaux de bord exécutifs). Elle retourne None (aucun filtrage) lorsque l'appelant a un accès non restreint à chaque machine activée — le cas courant, puisque machine_access est opt-in — et intersecte sinon tout client_ids/tags explicitement demandé avec ce que l'appelant peut réellement voir. Un appelant ne peut jamais élargir sa portée effective en passant une requête plus large ; l'intersection ne fait que la restreindre.

Appliqué sur : chaque endpoint scopé à un client dans les routeurs d'inventaire, de dérive et de dépendances applicatives — gestion de l'empreinte SSH, déclenchement/statut/annulation de scan, paquets, mises à jour, résultats CVE, conformité, et endpoints de conteneurs — plus les endpoints de listing (GET /inventory/clients, filtré silencieusement plutôt que bloqué), POST /inventory/scan-all (403 si l'appelant ne peut pas voir l'intégralité du parc actif, puisque le scan sous-jacent s'exécute de manière inconditionnelle et qu'un scan partiel ne peut pas être substitué silencieusement), et la création/lecture/liste/ confirmation/annulation de job d'installation (un job dont une seule machine cible est inaccessible est rejeté, ou retourne 404 à la lecture).

Filtres de contenu (content_filters)

Les filtres de contenu sont des règles d'allow/deny sur les noms de paquets, par distribution — pas une couche de contrôle d'accès sur les utilisateurs, mais un verrou de politique sur les noms de paquets que tout upload, import ou promotion autorisé doit franchir. Ils existent indépendamment de qui effectue l'action.

  • Ouvert par défaut. Une distribution sans aucune règle dans content_filters accepte tout nom de paquet — comportement inchangé tant qu'aucune règle n'est ajoutée.
  • Évaluation à la Katello. Si des règles allow existent pour la distribution, le nom du paquet doit correspondre à au moins une d'entre elles, ou il est rejeté d'office. Qu'une règle allow ait correspondu ou non, toute règle deny correspondante l'emporte toujours — un paquet correspondant à la fois à une règle allow et à une règle deny est rejeté.
  • Types de correspondance. Chaque règle est exact, glob (via fnmatch) ou regex.
  • Appliqué à l'entrée, avant toute écriture disque. Le filtre est évalué avant que les octets du paquet n'atteignent pool/ ou l'index de manifestes — à l'upload, à l'import (avant même que l'artefact soit téléchargé, puisque le nom/la version sont déjà connus depuis l'index source), et à la promotion/migration entre distributions (verrouillé sur la distribution de destination).
  • Jamais rétroactif de lui-même. Ajouter ou modifier une règle n'a aucun effet sur les paquets déjà présents. evaluate_existing_distribution() peut explicitement passer en revue le contenu actuel d'une seule distribution par rapport à ses règles actives — dry_run=True par défaut (liste ce qui serait retiré sans rien retirer), dry_run=False retire réellement les paquets non conformes, restreint à cette seule distribution.

Les filtres de contenu constituent un mécanisme distinct de la file de revue CVE (pending_review) : un filtre est une décision de politique binaire prise automatiquement au point d'entrée, sans étape d'approbation humaine, tandis que pending_review retient un paquet qui a par ailleurs passé la validation, en attente d'une décision explicite d'acceptation ou de rejet.

Résumé

Couche Ressource Table Défaut Contournement Réponse de refus
Rôle global Action users.role N/A — toujours appliqué N/A 403 Forbidden
distribution_access Codename de distribution distribution_access Ouvert (aucune ligne = ouvert) admin 404 Not Found
machine_access Machine d'inventaire machine_access Ouvert (aucune ligne = ouvert) admin 404 Not Found
content_filters Nom de paquet content_filters Ouvert (aucune ligne = ouvert) aucun (ce n'est pas une couche de portée sur l'utilisateur) Rejeté à l'entrée (pas de sémantique HTTP 404 — c'est un échec de validation, pas une décision d'accès)

Chaque couche au-delà du rôle de base est opt-in : aucune d'elles ne peut faire régresser l'accès d'une installation existante, car aucune d'elles ne fait quoi que ce soit tant qu'un administrateur n'a pas explicitement créé la première règle pour une distribution, une machine ou un motif de nom de paquet donné.