From 59f9ef9feebb19337b8a153e6cac0371ca706268 Mon Sep 17 00:00:00 2001
From: Earl Warren <109468362+earl-warren@users.noreply.github.com>
Date: Sun, 5 Nov 2023 13:48:32 +0100
Subject: [PATCH] Remove action runners on user deletion (#27902)

- On user deletion, delete action runners that the user has created.
- Add a database consistency check to remove action runners that have
nonexistent belonging owner.
- Resolves https://codeberg.org/forgejo/forgejo/issues/1720

(cherry picked from commit 009ca7223dab054f7f760b7ccae69e745eebfabb)

Co-authored-by: Gusted <postmaster@gusted.xyz>
---
 models/actions/runner.go        | 24 ++++++++++++++++++++++++
 modules/doctor/dbconsistency.go |  7 +++++++
 services/user/delete.go         |  2 ++
 3 files changed, 33 insertions(+)

diff --git a/models/actions/runner.go b/models/actions/runner.go
index ec6b49cf16..2c092c2b4a 100644
--- a/models/actions/runner.go
+++ b/models/actions/runner.go
@@ -266,3 +266,27 @@ func CreateRunner(ctx context.Context, t *ActionRunner) error {
 	_, err := db.GetEngine(ctx).Insert(t)
 	return err
 }
+
+func CountRunnersWithoutBelongingOwner(ctx context.Context) (int64, error) {
+	// Only affect action runners were a owner ID is set, as actions runners
+	// could also be created on a repository.
+	return db.GetEngine(ctx).Table("action_runner").
+		Join("LEFT", "user", "`action_runner`.owner_id = `user`.id").
+		Where("`action_runner`.owner_id != ?", 0).
+		And(builder.IsNull{"`user`.id"}).
+		Count(new(ActionRunner))
+}
+
+func FixRunnersWithoutBelongingOwner(ctx context.Context) (int64, error) {
+	subQuery := builder.Select("`action_runner`.id").
+		From("`action_runner`").
+		Join("LEFT", "user", "`action_runner`.owner_id = `user`.id").
+		Where(builder.Neq{"`action_runner`.owner_id": 0}).
+		And(builder.IsNull{"`user`.id"})
+	b := builder.Delete(builder.In("id", subQuery)).From("`action_runner`")
+	res, err := db.GetEngine(ctx).Exec(b)
+	if err != nil {
+		return 0, err
+	}
+	return res.RowsAffected()
+}
diff --git a/modules/doctor/dbconsistency.go b/modules/doctor/dbconsistency.go
index e5fc5785e8..ac983f9161 100644
--- a/modules/doctor/dbconsistency.go
+++ b/modules/doctor/dbconsistency.go
@@ -6,6 +6,7 @@ package doctor
 import (
 	"context"
 
+	actions_model "code.gitea.io/gitea/models/actions"
 	activities_model "code.gitea.io/gitea/models/activities"
 	"code.gitea.io/gitea/models/db"
 	issues_model "code.gitea.io/gitea/models/issues"
@@ -151,6 +152,12 @@ func checkDBConsistency(ctx context.Context, logger log.Logger, autofix bool) er
 			Fixer:        activities_model.FixActionCreatedUnixString,
 			FixedMessage: "Set to zero",
 		},
+		{
+			Name:         "Action Runners without existing owner",
+			Counter:      actions_model.CountRunnersWithoutBelongingOwner,
+			Fixer:        actions_model.FixRunnersWithoutBelongingOwner,
+			FixedMessage: "Removed",
+		},
 	}
 
 	// TODO: function to recalc all counters
diff --git a/services/user/delete.go b/services/user/delete.go
index 01e3c37b39..c4617e064e 100644
--- a/services/user/delete.go
+++ b/services/user/delete.go
@@ -10,6 +10,7 @@ import (
 
 	_ "image/jpeg" // Needed for jpeg support
 
+	actions_model "code.gitea.io/gitea/models/actions"
 	activities_model "code.gitea.io/gitea/models/activities"
 	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	auth_model "code.gitea.io/gitea/models/auth"
@@ -90,6 +91,7 @@ func deleteUser(ctx context.Context, u *user_model.User, purge bool) (err error)
 		&pull_model.AutoMerge{DoerID: u.ID},
 		&pull_model.ReviewState{UserID: u.ID},
 		&user_model.Redirect{RedirectUserID: u.ID},
+		&actions_model.ActionRunner{OwnerID: u.ID},
 	); err != nil {
 		return fmt.Errorf("deleteBeans: %w", err)
 	}