feat: migrate instance_oidc into auth_providers (0005)

This commit is contained in:
2026-08-03 10:38:52 +01:00
parent e2b01b62a5
commit e22faebfcd
2 changed files with 105 additions and 0 deletions
+7
View File
@@ -101,6 +101,13 @@ func runSchemaSetup() {
log.Fatalf("failed to ensure auth indexes: %v", err)
}
// 0005 runs AFTER EnsureAuthIndexes: the unique (instance_id, provider_id)
// index must exist before anything inserts providers, or a concurrent
// re-run could double-insert before the index is there to refuse it.
if err := services.MigrateAuthProviders(); err != nil {
log.Fatalf("auth provider migration failed: %v", err)
}
if err := services.EnsureSecretIndexes(); err != nil {
log.Printf("warning: failed to ensure secret indexes: %v", err)
}
@@ -0,0 +1,98 @@
package services
import (
"context"
"log"
"time"
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/db"
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/models"
"go.mongodb.org/mongo-driver/v2/bson"
)
// legacyInstanceOIDC is the pre-0005 shape: one document per instance, in a
// collection this migration is the last thing to read.
type legacyInstanceOIDC struct {
InstanceID string `bson:"instance_id"`
Issuer string `bson:"issuer"`
ClientID string `bson:"client_id"`
ClientSecretEnc string `bson:"client_secret_enc"`
Enabled bool `bson:"enabled"`
UpdatedAt time.Time `bson:"updated_at"`
}
// MigrateAuthProviders copies each instance_oidc document into auth_providers.
//
// The ciphertext is copied verbatim rather than decrypted and re-encrypted: a
// migration that needs KEY_ENCRYPTION_KEY fails on an instance that has none
// and strands the SSO configuration it was supposed to preserve.
//
// instance_oidc is left in place and no longer read. Nothing deletes it — a
// migration that drops the only copy of a client secret has no undo.
func MigrateAuthProviders() error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
const marker = "0005_auth_providers"
if n, _ := db.Col("migrations").CountDocuments(ctx, bson.M{"_id": marker}); n > 0 {
return nil
}
cur, err := db.Col("instance_oidc").Find(ctx, bson.M{})
if err != nil {
return err
}
var legacy []legacyInstanceOIDC
if err := cur.All(ctx, &legacy); err != nil {
return err
}
migrated := 0
for _, l := range legacy {
if l.InstanceID == "" {
continue
}
// Idempotent by skipping an instance that already has a provider, so a
// re-run after a partial failure completes rather than duplicating.
n, err := db.Col(authProviderCol).CountDocuments(ctx, bson.M{"instance_id": l.InstanceID})
if err != nil {
return err
}
if n > 0 {
continue
}
providerID, err := randomProviderID()
if err != nil {
return err
}
now := time.Now()
p := models.AuthProvider{
InstanceID: l.InstanceID,
ProviderID: providerID,
Name: "Single sign-on",
Kind: models.KindOIDC,
Preset: "",
Issuer: l.Issuer,
ClientID: l.ClientID,
ClientSecretEnc: l.ClientSecretEnc,
Scopes: []string{"openid", "profile", "email"},
Enabled: l.Enabled,
// This provider's redirect URI has changed and nobody has been told
// yet. The settings card raises it until an administrator dismisses.
CallbackNotice: true,
Order: 0,
CreatedAt: now,
UpdatedAt: now,
}
if _, err := db.Col(authProviderCol).InsertOne(ctx, p); err != nil {
return err
}
migrated++
}
if migrated > 0 {
log.Printf("0005: migrated %d OIDC configuration(s) to auth_providers", migrated)
}
_, err = db.Col("migrations").InsertOne(ctx, bson.M{"_id": marker, "applied_at": time.Now()})
return err
}