diff --git a/pkg/controller/cleanup/handle_cleanup.go b/pkg/controller/cleanup/handle_cleanup.go index 5d1ecb331..08c08cca9 100644 --- a/pkg/controller/cleanup/handle_cleanup.go +++ b/pkg/controller/cleanup/handle_cleanup.go @@ -57,10 +57,23 @@ func (c *Controller) HandleCleanup() http.Handler { defer enobs.RecordLatency(ctx, time.Now(), mLatencyMs, &result, &item) item = tag.Upsert(itemTagKey, "API_KEYS") if count, err := c.db.PurgeAuthorizedApps(c.config.AuthorizedAppMaxAge); err != nil { - merr = multierror.Append(merr, fmt.Errorf("failed to purge authorized apps: %w", err)) + merr = multierror.Append(merr, fmt.Errorf("failed to purge api keys: %w", err)) result = enobs.ResultError("FAILED") } else { - logger.Infow("purged authorized apps", "count", count) + logger.Infow("purged api keys", "count", count) + result = enobs.ResultOK + } + }() + + // e2e API keys + func() { + defer enobs.RecordLatency(ctx, time.Now(), mLatencyMs, &result, &item) + item = tag.Upsert(itemTagKey, "E2E_API_KEYS") + if count, err := c.db.PurgeE2EAuthorizedApps(); err != nil { + merr = multierror.Append(merr, fmt.Errorf("failed to purge e2e api keys: %w", err)) + result = enobs.ResultError("FAILED") + } else { + logger.Infow("purged e2e api keys", "count", count) result = enobs.ResultOK } }() diff --git a/pkg/controller/cleanup/handle_cleanup_test.go b/pkg/controller/cleanup/handle_cleanup_test.go index b61009909..f4dd7d11e 100644 --- a/pkg/controller/cleanup/handle_cleanup_test.go +++ b/pkg/controller/cleanup/handle_cleanup_test.go @@ -87,6 +87,41 @@ func TestHandleCleanup(t *testing.T) { } }) + t.Run("e2e_api_keys", func(t *testing.T) { + t.Parallel() + + db, _ := testDatabaseInstance.NewDatabase(t, nil) + + realm := database.NewRealmWithDefaults("e2e-test-realm") + realm.RegionCode = "E2E-TEST" + if err := db.SaveRealm(realm, database.SystemTest); err != nil { + t.Fatal(err) + } + + c := New(config, db, keyManagerSigner, h) + + authApp := &database.AuthorizedApp{ + Name: "e2e-test-admin", + Model: gorm.Model{ + CreatedAt: time.Now().UTC().Add(-25 * time.Hour), + }, + } + if _, err := realm.CreateAuthorizedApp(db, authApp, database.SystemTest); err != nil { + t.Fatal(err) + } + + w, r := envstest.BuildJSONRequest(ctx, t, http.MethodGet, "/", nil) + c.HandleCleanup().ServeHTTP(w, r) + + apps, _, err := realm.ListAuthorizedApps(db, nil) + if err != nil { + t.Fatal(err) + } + if got, want := len(apps), 0; got != want { + t.Errorf("got %d apps, expected %d", got, want) + } + }) + t.Run("verification_codes", func(t *testing.T) { t.Parallel() diff --git a/pkg/database/authorized_app.go b/pkg/database/authorized_app.go index 72e033f0a..2d061ef20 100644 --- a/pkg/database/authorized_app.go +++ b/pkg/database/authorized_app.go @@ -474,3 +474,24 @@ func (db *Database) PurgeAuthorizedApps(maxAge time.Duration) (int64, error) { Delete(&AuthorizedApp{}) return result.RowsAffected, result.Error } + +// PurgeE2EAuthorizedApps purges any API keys for the e2e-runner that have +// existed for more than 24 hours. In cases where the e2e-runner fails, it can +// leave behind API keys that are never deleted. +func (db *Database) PurgeE2EAuthorizedApps() (int64, error) { + deleteBefore := time.Now().UTC().Add(-24 * time.Hour) + + realm, err := db.E2ERealm() + if err != nil { + if IsNotFound(err) { + return 0, nil + } + return 0, fmt.Errorf("failed to lookup e2e realm: %w", err) + } + + result := db.db. + Unscoped(). + Where("realm_id = ? AND created_at < ?", realm.ID, deleteBefore). + Delete(&AuthorizedApp{}) + return result.RowsAffected, result.Error +} diff --git a/pkg/database/authorized_app_test.go b/pkg/database/authorized_app_test.go index ce7043465..b242feebd 100644 --- a/pkg/database/authorized_app_test.go +++ b/pkg/database/authorized_app_test.go @@ -321,6 +321,53 @@ func TestDatabase_PurgeAuthorizedApps(t *testing.T) { } } +func TestDatabase_PurgeE2EAuthorizedApps(t *testing.T) { + t.Parallel() + + db, _ := testDatabaseInstance.NewDatabase(t, nil) + + // No realm + { + n, err := db.PurgeE2EAuthorizedApps() + if err != nil { + t.Fatal(err) + } + if got, want := n, int64(0); got != want { + t.Errorf("expected %d to purge, got %d", want, got) + } + } + + realm := NewRealmWithDefaults("e2e-test-realm") + realm.RegionCode = "E2E-TEST" + if err := db.SaveRealm(realm, SystemTest); err != nil { + t.Fatal(err) + } + + for i := 0; i < 5; i++ { + if err := db.SaveAuthorizedApp(&AuthorizedApp{ + RealmID: realm.ID, + Name: fmt.Sprintf("e2e-test-%d", i), + APIKey: fmt.Sprintf("%d", i), + Model: gorm.Model{ + CreatedAt: time.Now().UTC().Add(-25 * time.Hour), + }, + }, SystemTest); err != nil { + t.Fatal(err) + } + } + + // Purges entries. + { + n, err := db.PurgeE2EAuthorizedApps() + if err != nil { + t.Fatal(err) + } + if got, want := n, int64(5); got != want { + t.Errorf("expected %d to purge, got %d", want, got) + } + } +} + func TestAuthorizedApp_Audits(t *testing.T) { t.Parallel() diff --git a/pkg/database/realm.go b/pkg/database/realm.go index 198e54608..423f7c82e 100644 --- a/pkg/database/realm.go +++ b/pkg/database/realm.go @@ -306,6 +306,24 @@ func NewRealmWithDefaults(name string) *Realm { } } +// E2ERealm gets the end-to-end realm. The end-to-end realm is defined as the +// realm that has a region_code beginning with E2E-* or a name beginning with +// e2e-test-*. +// +// If no e2e realm is defined, it returns NotFound. +func (db *Database) E2ERealm() (*Realm, error) { + var realm Realm + if err := db.db. + Model(&Realm{}). + Where("region_code ILIKE 'E2E-%' OR name ILIKE 'e2e-test-%'"). + Order("created_at ASC"). + First(&realm). + Error; err != nil { + return nil, fmt.Errorf("failed to find e2e realm: %w", err) + } + return &realm, nil +} + // AllowsUserReport returns true if this realm has enabled user initiated // test reporting. func (r *Realm) AllowsUserReport() bool { diff --git a/pkg/database/realm_test.go b/pkg/database/realm_test.go index f2e56b4aa..2c9b112c2 100644 --- a/pkg/database/realm_test.go +++ b/pkg/database/realm_test.go @@ -393,6 +393,78 @@ func TestRealm_BeforeSave(t *testing.T) { } } +func TestDatabase_E2ERealm(t *testing.T) { + t.Parallel() + + t.Run("none", func(t *testing.T) { + t.Parallel() + + db, _ := testDatabaseInstance.NewDatabase(t, nil) + + if _, err := db.E2ERealm(); !IsNotFound(err) { + t.Errorf("expected not found, got %#v", err) + } + }) + + t.Run("none_matching", func(t *testing.T) { + t.Parallel() + + db, _ := testDatabaseInstance.NewDatabase(t, nil) + + realm := NewRealmWithDefaults("not-the-e2e-realm") + realm.RegionCode = "NOT-E2E" + if err := db.SaveRealm(realm, SystemTest); err != nil { + t.Fatal(err) + } + + if _, err := db.E2ERealm(); !IsNotFound(err) { + t.Errorf("expected not found, got %#v", err) + } + }) + + t.Run("by_region_code", func(t *testing.T) { + t.Parallel() + + db, _ := testDatabaseInstance.NewDatabase(t, nil) + + realm := NewRealmWithDefaults("apple") + realm.RegionCode = "E2E-TEST" + if err := db.SaveRealm(realm, SystemTest); err != nil { + t.Fatal(err) + } + + record, err := db.E2ERealm() + if err != nil { + t.Fatal(err) + } + + if record.ID != realm.ID { + t.Errorf("expected %#v to be %#v", record, realm) + } + }) + + t.Run("by_name", func(t *testing.T) { + t.Parallel() + + db, _ := testDatabaseInstance.NewDatabase(t, nil) + + realm := NewRealmWithDefaults("e2e-test-realm") + realm.RegionCode = "TEST" + if err := db.SaveRealm(realm, SystemTest); err != nil { + t.Fatal(err) + } + + record, err := db.E2ERealm() + if err != nil { + t.Fatal(err) + } + + if record.ID != realm.ID { + t.Errorf("expected %#v to be %#v", record, realm) + } + }) +} + func TestRealm_BuildSMSText(t *testing.T) { t.Parallel()