fix: wipe user secrets when user is soft-deleted (#24985)

Extend the delete_deleted_user_resources() trigger so that secrets
belonging to a soft-deleted user are removed in the same transaction as
the existing api_keys and user_links cleanup.

user_secrets.user_id has ON DELETE CASCADE, but Coder soft-deletes users
by flipping users.deleted rather than removing the row, so the foreign key
cascade never fires and secrets would otherwise survive deletion.

Assisted by Coder Agents.
This commit is contained in:
Zach
2026-05-11 09:07:30 -06:00
committed by GitHub
parent 81e2be69e9
commit b221632615
9 changed files with 207 additions and 27 deletions
+72
View File
@@ -7558,6 +7558,78 @@ func TestUserSecretsCRUDOperations(t *testing.T) {
})
}
// TestUserSecretsSoftDeleteTrigger verifies that a user's secrets
// are deleted when the user is soft-deleted.
func TestUserSecretsSoftDeleteTrigger(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
ctx := testutil.Context(t, testutil.WaitMedium)
// userA will be soft-deleted.
userA := dbgen.User(t, db, database.User{})
secretA1 := dbgen.UserSecret(t, db, database.UserSecret{
UserID: userA.ID,
Name: "secret-a-1",
Value: "value-a-1",
EnvName: "SECRET_A_1",
FilePath: "/secrets/a/1",
})
secretA2 := dbgen.UserSecret(t, db, database.UserSecret{
UserID: userA.ID,
Name: "secret-a-2",
Value: "value-a-2",
EnvName: "SECRET_A_2",
FilePath: "/secrets/a/2",
})
// Sanity-check the existing trigger behavior. An API key for
// userA should also be wiped on soft-delete.
_, _ = dbgen.APIKey(t, db, database.APIKey{UserID: userA.ID})
userB := dbgen.User(t, db, database.User{})
secretB := dbgen.UserSecret(t, db, database.UserSecret{
UserID: userB.ID,
Name: "secret-b",
Value: "value-b",
EnvName: "SECRET_B",
FilePath: "/secrets/b",
})
require.NoError(t, db.UpdateUserDeletedByID(ctx, userA.ID))
// userA's secrets are removed after soft-deletion.
_, err := db.GetUserSecretByID(ctx, secretA1.ID)
require.ErrorIs(t, err, sql.ErrNoRows)
_, err = db.GetUserSecretByID(ctx, secretA2.ID)
require.ErrorIs(t, err, sql.ErrNoRows)
// userA's API key is also removed.
apiKeysA, err := db.GetAPIKeysByUserID(ctx, database.GetAPIKeysByUserIDParams{
UserID: userA.ID,
LoginType: userA.LoginType,
})
require.NoError(t, err)
require.Empty(t, apiKeysA)
// userB's secret is unaffected.
got, err := db.GetUserSecretByID(ctx, secretB.ID)
require.NoError(t, err)
require.Equal(t, secretB.ID, got.ID)
// Trying to insert a new secret for the soft-deleted userA must fail.
_, err = db.CreateUserSecret(ctx, database.CreateUserSecretParams{
ID: uuid.New(),
UserID: userA.ID,
Name: "post-delete",
Value: "value",
EnvName: "POST_DELETE_ENV",
FilePath: "/secrets/post-delete",
})
require.Error(t, err)
require.Contains(t, err.Error(), "Cannot create user_secret for deleted user")
}
func TestUserSecretsAuthorization(t *testing.T) {
t.Parallel()