API de gestion des droits utilisateurs pour les outils du pôle DATA. Pour en savoir plus, découvrez la présentation.
- Python 3.13+
- uv - Gestionnaire de dépendances
- PostgreSQL 15.7
- Docker & Docker Compose
# Cloner le dépôt
git clone https://github.com/datagouv/roles.data.git
cd roles.data
# Installer les dépendances
uv sync
# Lancer les conteneurs de base de données
make docker_local
# Initialiser & migrer la base de données
make db_init
# Lancer l'application
make startNB : en mode developpement rapide, l'application n’est pas dockerisée. Seuls les containers de la base de donnée le sont.
Pour tester la configuration docker complète de l'application :
make docker
La commande lance les containers :
- nginx (cf
./nginx.conf) - app
- postgres-local
- postgres-test
- smtp-local
Ce mode permet de tester la conf nginx, le dockerfile et la logique de migration.
Cette commande est systématiquement testée dans la CI par la Github Action docker-config-test
La variable DB_ENV est utilisée pour distinguer les différents environnements :
local: developpement local (seedé)test: CI (seedé)dev: intégration (seedé)prodenvironnement de production
# lancer les DB pour les environnements local et test
docker-compose-up
# se connecter
psql -h localhost -p 5432 -U d-roles -d d-roles
# executer les migrations et la seed
make db_init
Les scripts appliqués à la base de donnée sont executés dans cet ordre :
schema.sql- creation du schema (uniquement les environnements local, test)create.sql- création de la base de donnéesmigrations/*- migrations successivesseed.sql- données de tests (uniquement les environnements local, test, dev)
Ajouter un fichier db/migrations/{YYYYMMDD}_{description}.sql avec le SQL nécessaire pour la migration
Mettre a jour le fichier seeds (selon l'environnement) dans db/seeds/{environnement}/seed.sql
Les tests d'intégration tournent sur pytest. La DB postgres-test est une DB différent de la DB de dev, pré-stubbé et isolée.
# démarrer la DB
make docker_local
# test de migrations/seed
# make db_init
# lancer les tests
make test
L'application est déployée sur différents environnements :
- [dev] https://roles.dev.data.gouv.fr : données de test. À utiliser pour en intégration.
- [prod] https://roles.data.gouv.fr
Les déploiement se font via un message de commit formaté de la manière suivante : [ENV:VERSION].
# deploy on roles.dev.data.gouv.fr
make deploy_dev
# deploy on roles.data.gouv.fr
make deploy_prod
NB : ces commandes déploient la branche main uniquement.
uv add pre-commit
pre-commit install --install-hooks
Ce projet utilise Ruff pour le formatage et le linting :
make lint
flowchart TD
U((Utilisateur))
U-->D
subgraph Dinum
subgraph Datapass:
D[1. Nouvelle demande]-->|Validation|C[2. Habilitation]
D-->|Refus|X[Demande refusée]
end
subgraph Roles.data:
C-->|3. Création d’un groupe avec les droits associés| R[(Listes des groupes)]
end
subgraph ProConnect:
L[Login]
end
end
C-->|4. Envoi d’un mail a l'utilisateur l’invitant a se connecter| U
U-->|5. Clic sur le lien contenu dans le mail|A
A<-->|6. Se ProConnecte|L
subgraph Fournisseur de Service
A[Accès au service]
A<-->|7. Récupération des droits de l’utilisateur|R
end
Le schéma ci-dessous représente la structure de la base de données (selon les migrations, ce schéma peut différer légèrement de la structure réelle) :
erDiagram
organisations ||--o{ groups : "contains"
organisations {
int id PK
char_14 siret UK "UNIQUE, format: 14 digits"
varchar_255 name "nullable"
timestamptz created_at
timestamptz updated_at
}
users ||--o{ group_user_relations : "belongs_to"
users {
int id PK
varchar_255 email UK "UNIQUE"
varchar_255 sub_pro_connect "UNIQUE when not NULL"
boolean is_verified "default: false"
timestamptz created_at
timestamptz updated_at
}
roles ||--o{ group_user_relations : "defines"
roles {
int id PK
varchar_255 role_name UK "UNIQUE"
boolean is_admin "default: false"
timestamptz created_at
timestamptz updated_at
}
group_user_relations {
int id PK
int group_id FK
int user_id FK
int role_id FK
timestamptz created_at
timestamptz updated_at
}
groups ||--o{ "parent_child_relations (pas utilisée)" : "parent"
groups ||--o{ "parent_child_relations (pas utilisée)" : "child"
groups ||--o{ group_user_relations : "has"
groups ||--o{ group_service_provider_relations : "has"
groups {
int id PK
int orga_id FK
varchar_255 name
timestamptz created_at
timestamptz updated_at
}
"parent_child_relations (pas utilisée)" {
int id PK
int parent_group_id FK
int child_group_id FK
boolean inherit_scopes "default: false"
timestamptz created_at
timestamptz updated_at
}
service_providers ||--o{ group_service_provider_relations : "provides"
service_providers ||--o{ service_accounts : "has"
service_providers {
int id PK
varchar_255 name
varchar_500 url "nullable, must be http(s)"
timestamptz created_at
timestamptz updated_at
}
group_service_provider_relations {
int id PK
int service_provider_id FK
int group_id FK
text scopes "default: empty string"
text contract_description "default: empty string"
varchar_500 contract_url "nullable, must be http(s)"
timestamptz created_at
timestamptz updated_at
}
service_accounts {
int id PK
int service_provider_id FK
boolean is_active "default: false"
varchar_255 name "UNIQUE per service_provider_id"
text hashed_password
timestamptz created_at
timestamptz updated_at
}
audit_logs {
int id PK
int service_provider_id "no FK"
int service_account_id "no FK"
varchar_50 action_type "CREATE, UPDATE, DELETE, etc."
varchar_50 resource_type "user, group, organisation, etc."
int resource_id "nullable"
jsonb new_values "nullable"
varchar_255 acting_user_sub "nullable"
timestamptz created_at
}
Notes importantes :
Datapass(id=999) est le seul fournisseur de service hardcodégroup_service_provider_relations: association many-to-many entre groupes, et fournisseurs de service, qui porte les droits(scopes)group_user_relations: association many-to-many entre groupes, utilisateurs et rôlesaudit_logsn'utilise pas de clés étrangères pour conserver l'historique même après suppression de la ressourceparent_child_relationspermet de créer une hiérarchie de groupes (la table existe mais n’est pas actuellement utilisée)
graph TB
subgraph "FastAPI Application"
subgraph "Authentification"
RSAuth[ResourceServer Credentials<br/> OAuth2 ProConnect token<br/>Fournisseur de service connectés a ProConnect uniquement]
OAuth[OAuth2 Client Credentials<br/>JWT Tokens<br/>API externes]
PCAuth[ProConnect OAuth2<br/>Session Cookies<br/>Interface Web]
DPAuth[Datapass HMAC<br/>Signature<br/>Webhooks]
end
subgraph "Routers - Couche HTTP"
RAPI[API Routers<br/>users, groups, roles, scopes]
RWebhook[Webhook Router<br/>datapass]
RWebUI[Web UI Routers<br/>admin, activation]
end
subgraph "Dependency Injection"
CTX[Context<br/>service_provider_id<br/>service_account_id<br/>acting_user_sub]
DBConn[db_conn<br/>swappable pour tests]
Logger[LogsRepository]
Repositories[Instanciation des repositories métiers]
Services[Instanciation des services métiers]
end
subgraph "Services - Logique métier"
ServicesMetiers[Utilisation des services instantiés lors de l’injection de dépendances]
end
subgraph "Repositories - appels externes"
RepositoriesMetiers[Utilisation des repositories instantiés lors de l’injection de dépendances]
end
end
subgraph "Base de données"
DB[(PostgreSQL)]
end
OAuth -->CTX
RSAuth -->CTX
PCAuth -->CTX
DPAuth -->CTX
OAuth-->RAPI
RSAuth-->RAPI
PCAuth-->RWebUI
DPAuth-->RWebhook
CTX --> Logger
DBConn --> Repositories
Logger--> Repositories
Repositories --> Services
Services -->|Injection des instances| RAPI & RWebUI & RWebhook
RAPI & RWebUI & RWebhook --> ServicesMetiers
ServicesMetiers --> RepositoriesMetiers
RepositoriesMetiers -->|Using db_conn| DB
RepositoriesMetiers-->|log écriture using LogsRepository| DB
Patterns d'authentification :
- OAuth2 Client Credentials : Service accounts avec JWT pour les API externes
- ResourceServer Credentials : Utilisation de roles par API en mode resource server
- ProConnect OAuth2 : Authentication utilisateur via OpenID Connect pour l'interface web
- Datapass HMAC : Vérification de signature pour les webhooks entrants
Injection de dépendances :
- Context : Extrait des credentials d'authentification, contient
service_provider_id,service_account_id,acting_user_sub - DB Connection : Session de base de données (
db_session), swappable pour les tests (permet d'utiliser une DB de test isolée) - LogsService : Injecté avec le context pour tracer les actions dans
audit_logs
Architecture en couches :
- Routers : Gestion des requêtes HTTP, validation des entrées, sérialisation des réponses
- Services : Logique métier, orchestration entre repositories, gestion des emails
- Repositories : Requêtes SQL directes, transactions, logging des actions via LogsService
Le resource server permet a un Fournisseur de service de récupérer des resources, par API avec le seul jeton ProConnect (passé en header authorization).
Le resource server (dans notre cas, l'API de roles sur ses endpoints /resource-server) demande a ProConnect une vérification de l'access_token que pro connect a créé pour l'utilisateur, dans le cadre du fournisseur de service (eg. annuaire des entreprises).
flowchart TD
U((Utilisateur))
U-->Login
Login -->|Redirect| LPC
LPC-->|Redirect| Callback
subgraph Fournisseur de Service
Login[Page de connexion]
Callback[Création de la session utilisateur - stockage des token ProConnect]
Callback --> Connected[L’utilisateur est connecté et poursuis sa navigation]
Connected--> GetRS[L’utilisateur a besoin d'une resource]
end
GetRS-->|Transmet l’access_token| RS[Besoin d’une resource]
RS-->|Transmet l’access_token| II
II -->|Retourne le sub, client_id et autres claims| RS
RS -->|Resource demandée| GetRS
subgraph Resource Server
RS[API]
end
subgraph ProConnect
LPC[ProConnexion]
II[Vérification du token]
end
Cas pratique : resource serveur sur Annuaire des Entreprises + Roles + ProConnect.
flowchart TD
U((Utilisateur))
U-->Login
Login -->|Redirect| LPC
LPC-->|Redirect| Callback
subgraph "Fournisseur de Service : Annuaire des Entreprises"
Login[Connexion]
Callback[Callback de connexion et de création de session utilisateur]
end
Callback-->|"API REST - Authorization : Bearer {access_token_pro_connect}"| RS
subgraph "Resource Server : roles.data"
RS["GET /resource-server/group"]
RS --> TokenValidation[Vérification du token]
TokenValidation-->Client_IDInDB[Le client_id est associé a un service provider dans la base]
TokenValidation-->Client_IDNotInDB[Le client_id n'existe pas dans la base]-->|403 Unauthrorized| Callback
Client_IDInDB --> SubInDB[Le sub est dans la base, associé à l'email de l'utilisateur]
Client_IDInDB --> SubNotInDB[Le sub n'est pas dans la base]
SubNotInDB --> GetuserEmail[Récupération du mail utilisateur]
UserInDB--> SubEmailPairing[Le sub est associé à l'email de l'utilisateur dans la base]
GetuserEmail --> UserInDB[L'email de l'utilisateur a déjà été ajouté à un groupe]
GetuserEmail --> UserNotInDB[L'email n'est pas dans la base] -->|404 Not Found| Callback
GetGroupBySub[Récupération des groupes de l’utilisateur avec le sub et le service_provider]
SubEmailPairing-->GetGroupBySub
SubInDB-->GetGroupBySub
GetGroupBySub-->Callback
end
TokenValidation-->|access_token_pro_connect| II
II-->|client_id, sub| TokenValidation
GetuserEmail-->|access_token_pro_connect| UI
UI-->|email| GetuserEmail
subgraph ProConnect
LPC[ProConnexion]
II[Route /introspect vérification du token]
UI[Route /userinfo]
end
Avec ce pattern, on fait l'économie d'un irritant majeur des Saas modernes : la validation du mail. L'ajout d'un utilisateur ce fait ainsi :
- ajout d'un utilisateur à un groupe par son email
- lors de la première connexion récupération du sub par l'access token et du mail par la route info
- sauvegarde du couple mail <> sub
- une fois le sub sauvegardé, utilisation du sub pour récuperer les groupes de l'utilisateur.
Plusieurs avantages :
- pas de mail d'activation
- permet de'identifier l'utilisateur uniquement avec son sub, contenu dans l'access_token
- cinématique très light, pas d'accès en base nécessaire pour décoder l'access_token dans la route /introspect.