Hooks gitrust — Référence

Ce document décrit les hooks exposés par crates/gitrust-hooks/, quand ils sont appelés, leur payload, leurs effets de bord, et comment les tester localement.

Vue d'ensemble

gitrust-hooks implémente le trait RustwardenHooks de rustwarden-core. Ce trait est un mécanisme de lifecycle : le framework appelle les hooks aux moments clés du cycle de vie des ressources (inscription utilisateur, suppression, partage).

// crates/gitrust-hooks/src/lib.rs
#[async_trait]
impl RustwardenHooks for GitrustHooks {
    async fn on_user_registered(...) -> anyhow::Result<()>;
    async fn on_resource_shared(...) -> anyhow::Result<()>;
    async fn on_user_deleted(...) -> anyhow::Result<()>;
    async fn on_resource_unshared(...) -> anyhow::Result<()>;
}

Les hooks Git serveur (receive-pack, upload-pack) sont implémentés directement dans gitrust-web/src/handlers/git_http.rs et dans le serveur SSH (gitrust-ssh).

Hooks de lifecycle (RustwardenHooks)

on_user_registered

Déclencheur : après la création réussie d'un compte utilisateur (via /register ou l'API /api/v1/auth/register).

Payload :

async fn on_user_registered(
    db: &DatabaseConnection,
    user_id: Uuid,
    username: &str,
) -> anyhow::Result<()>

Effet de bord : crée le répertoire de dépôts de l'utilisateur sur le système de fichiers :

{REPOS_BASE_PATH}/{username}/

Validation de sécurité : le username est validé contre la traversal de chemin (.., /, \) avant toute opération disque. Un username suspect déclenche un log WARN et retourne une erreur — le compte n'est pas créé.

Idempotence : si le répertoire existe déjà, l'appel réussit silencieusement.


on_user_deleted

Déclencheur : juste avant la suppression d'un utilisateur depuis le panel admin.

Payload :

async fn on_user_deleted(
    db: &DatabaseConnection,
    user_id: Uuid,
    username: &str,
) -> anyhow::Result<()>

Effets de bord : 1. Supprime le répertoire disque {REPOS_BASE_PATH}/{username}/ et tous les bare repos qu'il contient. 2. Enregistre un événement user_deleted dans audit_logs (niveau WARN).

Note : la cascade DB (suppression des lignes repositories) est gérée par la contrainte FK ON DELETE CASCADE. Ce hook gère uniquement le système de fichiers.

En cas d'erreur disque (permissions, NFS indisponible), l'erreur est loggée en WARN mais ne bloque pas la suppression en base.


on_resource_shared

Déclencheur : après un appel réussi à ResourceService::share() — quand un dépôt est partagé avec un collaborateur.

Payload :

async fn on_resource_shared(
    db: &DatabaseConnection,
    resource_type: &str,
    resource_id: Uuid,
    shared_with_user_id: Uuid,
    permission_level: &str,
    shared_by_user_id: Uuid,
) -> anyhow::Result<()>

Effet de bord : si resource_type == "repository", enregistre l'événement REPO_SHARED dans audit_logs avec les détails du partage.


on_resource_unshared

Déclencheur : après un appel réussi à ResourceService::revoke_share() ou revoke_user_access().

Payload :

async fn on_resource_unshared(
    db: &DatabaseConnection,
    resource_type: &str,
    resource_id: Uuid,
    user_id: Uuid,
) -> anyhow::Result<()>

Effet de bord : si resource_type == "repository", enregistre l'événement REPO_UNSHARED dans audit_logs.

Hooks Git serveur (receive-pack / upload-pack)

Ces hooks ne sont pas dans gitrust-hooks mais dans les handlers HTTP et SSH.

Receive-pack (après un git push)

Déclencheur : POST /{owner}/{repo}/git-receive-pack (HTTP) ou connexion SSH suivie d'un git-receive-pack.

Séquence d'effets :

sequenceDiagram
    participant Client as git push
    participant Handler as receive_pack handler
    participant DB as Base de données
    participant CI as CI Worker
    participant WH as Webhook Dispatcher

    Client->>Handler: objets packés
    Handler->>DB: RepositoryService::mark_non_empty() si premier push
    Handler->>DB: CiService::create_pipeline() si .gitrust-ci.yml détecté
    Handler->>CI: mpsc::send(CiTask)
    Handler->>DB: WebhookService::list_by_repo_and_event("push")
    Handler->>WH: tokio::spawn → deliver(webhook, "push", payload)
    Handler-->>Client: 200 OK (pack-protocol)

Payload webhook push :

{
    "repository": {
        "id": "...",
        "slug": "myrepo",
        "owner": "alice"
    },
    "pusher": { "username": "alice" },
    "ref": "refs/heads/main",
    "before": "0000000000000000000000000000000000000000",
    "after": "abc123...",
    "commits": [
        {
            "sha": "abc123...",
            "message": "feat: ajouter la signature HMAC",
            "author": { "name": "Alice", "email": "alice@example.com" }
        }
    ]
}

Upload-pack (après un git fetch / git clone)

Déclencheur : GET /{owner}/{repo}/info/refs?service=git-upload-pack + POST /{owner}/{repo}/git-upload-pack.

Effets de bord : aucun effet de bord métier. Vérification des permissions d'accès (public → lecture libre ; privé → authentification requise).

Hooks SBOM (post-push)

Après chaque push sur un dépôt avec SBOM_ENABLED=true, un job SBOM est déclenché via SbomService :

  1. SbomService::trigger_scan(db, repo_id, commit_sha) crée un job dans ci_jobs.
  2. Le worker CI exécute syft sur le répertoire de travail.
  3. Le résultat est uploadé vers Dependency-Track via HTTP.
  4. Les findings critiques peuvent bloquer la CI selon la configuration sbom_block_on_violation.

Tester les hooks localement

Tester on_user_registered

// tests/hooks_test.rs
#[tokio::test]
async fn on_user_registered_creates_dir() {
    let tmp = tempfile::TempDir::new().unwrap();
    let hooks = GitrustHooks::new(tmp.path().to_path_buf());
    let db = sea_orm::Database::connect("sqlite::memory:").await.unwrap();

    hooks
        .on_user_registered(&db, Uuid::new_v4(), "alice")
        .await
        .unwrap();

    assert!(tmp.path().join("alice").exists());
}

#[tokio::test]
async fn on_user_registered_rejects_traversal() {
    let tmp = tempfile::TempDir::new().unwrap();
    let hooks = GitrustHooks::new(tmp.path().to_path_buf());
    let db = sea_orm::Database::connect("sqlite::memory:").await.unwrap();

    assert!(hooks
        .on_user_registered(&db, Uuid::new_v4(), "../etc/passwd")
        .await
        .is_err());
}

Simuler un push en local

Pour tester le cycle complet receive-pack → CI → webhook sans client réel :

# 1. Démarrer gitrust localement
cargo run

# 2. Créer un dépôt via l'UI, cloner en SSH ou HTTP
git clone http://localhost:4000/alice/myrepo.git

# 3. Ajouter un fichier et pusher
cd myrepo
echo "hello" > README.md
git add README.md && git commit -m "test push"
git push origin main

# 4. Vérifier les effets de bord dans les logs
# → Log "Created pipeline" si .gitrust-ci.yml présent
# → Log "Delivered webhook" si webhook configuré

Ajouter un nouveau hook

Pour réagir à un nouvel événement de lifecycle :

  1. Définissez la méthode dans le trait RustwardenHooks (dans rustwarden-core/src/hooks.rs).
  2. Implémentez la méthode dans GitrustHooks (dans gitrust-hooks/src/lib.rs).
  3. Appelez le hook depuis le service rustwarden-core approprié.
  4. Écrivez un test unitaire avec tempfile::TempDir et sea_orm::Database::connect("sqlite::memory:").

Règle : les hooks ne doivent jamais bloquer un flux utilisateur critique. En cas d'erreur non fatale (écriture disque, audit log), loggez en WARN et retournez Ok(()).

Voir aussi