fix(auth): delete the Kratos identity on account deletion
Backend CI / Test (push) Has been cancelled
Backend CI / Contract Tests (push) Has been cancelled
Backend CI / Lint (push) Has been cancelled
Backend CI / Secret Scanning (push) Has been cancelled
Backend CI / Build (push) Has been cancelled

Account deletion removed all local data but left the Ory Kratos
identity intact — an orphaned identity that can still authenticate.
Close the gap:

- kratos.Client gains the admin API: NewClient(publicURL, adminURL)
  and DeleteIdentity (DELETE /admin/identities/{id}; a 404 is treated
  as success so a retry after a partial failure is idempotent).
- AuthService.DeleteAccount deletes the Kratos identity FIRST; if that
  call fails it aborts before touching local data, so the operation is
  retryable rather than partially applied.
- KRATOS_ADMIN_URL config (default http://kratos:4434) + router wiring.
- kratos NetworkPolicy split: the api pods may now reach the admin API
  :4434 (Traefik still reaches only the public API :4433).
- kratos CORS: allow_credentials + OPTIONS so the web browser flows
  (ory_kratos_session cookie) work; origins stay an explicit allowlist.
- Regression tests: identity teardown happens, and a Kratos failure
  aborts the deletion instead of orphaning local data.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-05-18 21:55:33 -05:00
parent 81578f6e27
commit 3d3ba84df0
7 changed files with 150 additions and 12 deletions
+57
View File
@@ -3,6 +3,7 @@ package services
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
@@ -10,6 +11,8 @@ import (
"github.com/treytartt/honeydue-api/internal/config"
"github.com/treytartt/honeydue-api/internal/dto/requests"
"github.com/treytartt/honeydue-api/internal/kratos"
"github.com/treytartt/honeydue-api/internal/models"
"github.com/treytartt/honeydue-api/internal/repositories"
"github.com/treytartt/honeydue-api/internal/testutil"
)
@@ -165,6 +168,60 @@ func TestAuthService_DeleteAccount_UserNotFound(t *testing.T) {
testutil.AssertAppError(t, err, http.StatusNotFound, "error.user_not_found")
}
// Regression: deleting an account must also delete the user's Kratos
// identity, or an orphaned, still-loginable identity is left behind.
func TestAuthService_DeleteAccount_DeletesKratosIdentity(t *testing.T) {
service, userRepo := setupAuthService(t)
db := userRepo.DB()
user := testutil.CreateTestUser(t, db, "kuser", "kuser@test.com", "")
require.NoError(t, db.Model(&models.User{}).Where("id = ?", user.ID).
Update("kratos_id", "kratos-uuid-123").Error)
var gotMethod, gotPath string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotMethod, gotPath = r.Method, r.URL.Path
w.WriteHeader(http.StatusNoContent)
}))
defer srv.Close()
service.SetKratosClient(kratos.NewClient("", srv.URL))
confirmation := "DELETE"
_, err := service.DeleteAccount(context.Background(), user.ID, nil, &confirmation)
require.NoError(t, err)
assert.Equal(t, http.MethodDelete, gotMethod)
assert.Equal(t, "/admin/identities/kratos-uuid-123", gotPath)
// Local user row is gone.
_, err = userRepo.WithContext(context.Background()).FindByID(user.ID)
assert.Error(t, err)
}
// Regression: if the Kratos identity delete fails, local data must NOT be
// deleted — the operation must be retryable, not partially applied.
func TestAuthService_DeleteAccount_KratosFailureAbortsDeletion(t *testing.T) {
service, userRepo := setupAuthService(t)
db := userRepo.DB()
user := testutil.CreateTestUser(t, db, "kuser2", "kuser2@test.com", "")
require.NoError(t, db.Model(&models.User{}).Where("id = ?", user.ID).
Update("kratos_id", "kratos-uuid-456").Error)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
defer srv.Close()
service.SetKratosClient(kratos.NewClient("", srv.URL))
confirmation := "DELETE"
_, err := service.DeleteAccount(context.Background(), user.ID, nil, &confirmation)
require.Error(t, err)
// Local user row must still exist.
_, ferr := userRepo.WithContext(context.Background()).FindByID(user.ID)
assert.NoError(t, ferr)
}
// === SetNotificationRepository ===
func TestAuthService_SetNotificationRepository(t *testing.T) {