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_accessest 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 erreur404, pas403, 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 viafilter_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 optionnelrole. Sans lui, le rôle serait redérivé en recherchant le nom d'utilisateur de l'appelant dans la tableusers— qui n'a aucune ligne pour une identité de token API (token:<name>), faisant silencieusement retomber cette identité surreaderet 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 retourneNone(aucun filtrage) lorsque l'appelant a un accès non restreint à chaque machine activée — le cas courant, puisquemachine_accessest opt-in — et intersecte sinon toutclient_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_filtersaccepte tout nom de paquet — comportement inchangé tant qu'aucune règle n'est ajoutée. - Évaluation à la Katello. Si des règles
allowexistent 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ègledenycorrespondante 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(viafnmatch) ouregex. - 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=Truepar défaut (liste ce qui serait retiré sans rien retirer),dry_run=Falseretire 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é.