Changeset 0.29.0 (#195)
This commit is contained in:
@@ -20,7 +20,6 @@ import (
|
||||
"git.gobha.me/xcaliber/chat-switchboard/auth"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/config"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/crypto"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/database"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
)
|
||||
@@ -184,10 +183,7 @@ func (h *AuthHandler) OIDCLogin(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Store state for callback verification
|
||||
_, err := database.DB.ExecContext(c.Request.Context(), database.Q(`
|
||||
INSERT INTO oidc_auth_state (state, nonce, redirect_to) VALUES ($1, $2, $3)
|
||||
`), state, nonce, c.Query("redirect"))
|
||||
if err != nil {
|
||||
if err := h.stores.GlobalConfig.SaveOIDCState(c.Request.Context(), state, nonce, c.Query("redirect")); err != nil {
|
||||
log.Printf("[auth/oidc] warn: could not store state: %v", err)
|
||||
}
|
||||
|
||||
@@ -220,29 +216,15 @@ func (h *AuthHandler) OIDCCallback(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Verify state
|
||||
var nonce, redirectTo string
|
||||
err := database.DB.QueryRowContext(c.Request.Context(), database.Q(`
|
||||
SELECT nonce, COALESCE(redirect_to, '') FROM oidc_auth_state WHERE state = $1
|
||||
`), state).Scan(&nonce, &redirectTo)
|
||||
// Verify state (nonce + redirectTo retrieved but not yet validated — TODO)
|
||||
_, _, err := h.stores.GlobalConfig.ConsumeOIDCState(c.Request.Context(), state)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid or expired state"})
|
||||
return
|
||||
}
|
||||
|
||||
// Clean up state (one-time use)
|
||||
database.DB.ExecContext(c.Request.Context(), database.Q(`
|
||||
DELETE FROM oidc_auth_state WHERE state = $1
|
||||
`), state)
|
||||
|
||||
// Also clean up stale states (> 10 minutes old)
|
||||
if database.IsSQLite() {
|
||||
database.DB.ExecContext(c.Request.Context(),
|
||||
`DELETE FROM oidc_auth_state WHERE created_at < datetime('now', '-10 minutes')`)
|
||||
} else {
|
||||
database.DB.ExecContext(c.Request.Context(),
|
||||
`DELETE FROM oidc_auth_state WHERE created_at < NOW() - INTERVAL '10 minutes'`)
|
||||
}
|
||||
// Clean up stale states (> 10 minutes old)
|
||||
_ = h.stores.GlobalConfig.CleanupOIDCState(c.Request.Context())
|
||||
|
||||
// Determine redirect URI (must match what was sent in the login request)
|
||||
redirectURI := h.cfg.OIDCRedirectURL
|
||||
@@ -363,24 +345,17 @@ func hashToken(token string) string {
|
||||
// - AdminHandler.destroyVault (admin-initiated reset)
|
||||
//
|
||||
// Does NOT evict from UEK cache or write audit logs — callers handle that.
|
||||
func DestroyVaultDB(ctx context.Context, userID string) (providersDeleted int64) {
|
||||
_, err := database.DB.ExecContext(ctx, database.Q(`
|
||||
UPDATE users
|
||||
SET encrypted_uek = NULL, uek_salt = NULL, uek_nonce = NULL, vault_set = false
|
||||
WHERE id = $1
|
||||
`), userID)
|
||||
if err != nil {
|
||||
// v0.29.0: accepts stores instead of using database.DB directly.
|
||||
func DestroyVaultDB(ctx context.Context, stores store.Stores, userID string) (providersDeleted int64) {
|
||||
if err := stores.Users.ClearVaultKeys(ctx, userID); err != nil {
|
||||
log.Printf("⚠ DestroyVaultDB: failed to clear vault columns for user %s: %v", userID, err)
|
||||
}
|
||||
|
||||
result, err := database.DB.ExecContext(ctx, database.Q(`
|
||||
DELETE FROM provider_configs WHERE scope = 'personal' AND owner_id = $1
|
||||
`), userID)
|
||||
rows, err := stores.Providers.DeletePersonalByOwner(ctx, userID)
|
||||
if err != nil {
|
||||
log.Printf("⚠ DestroyVaultDB: failed to delete personal providers for user %s: %v", userID, err)
|
||||
return 0
|
||||
}
|
||||
rows, _ := result.RowsAffected()
|
||||
return rows
|
||||
}
|
||||
|
||||
@@ -392,14 +367,9 @@ func DestroyVaultDB(ctx context.Context, userID string) (providersDeleted int64)
|
||||
//
|
||||
// Used by BootstrapAdmin and SeedUsers where the password is known at
|
||||
// startup but the UEK cache is not available.
|
||||
func ProbeAndRepairVault(ctx context.Context, userID, password string) {
|
||||
var vaultSet bool
|
||||
var encryptedUEK, salt, nonce []byte
|
||||
|
||||
err := database.DB.QueryRowContext(ctx, database.Q(`
|
||||
SELECT vault_set, encrypted_uek, uek_salt, uek_nonce
|
||||
FROM users WHERE id = $1
|
||||
`), userID).Scan(&vaultSet, &encryptedUEK, &salt, &nonce)
|
||||
// v0.29.0: accepts stores instead of using database.DB directly.
|
||||
func ProbeAndRepairVault(ctx context.Context, stores store.Stores, userID, password string) {
|
||||
vaultSet, encryptedUEK, salt, nonce, err := stores.Users.GetVaultKeys(ctx, userID)
|
||||
if err != nil || !vaultSet {
|
||||
return // no vault to probe
|
||||
}
|
||||
@@ -410,7 +380,7 @@ func ProbeAndRepairVault(ctx context.Context, userID, password string) {
|
||||
}
|
||||
|
||||
// Stale seal: password has actually changed since the vault was sealed
|
||||
deleted := DestroyVaultDB(ctx, userID)
|
||||
deleted := DestroyVaultDB(ctx, stores, userID)
|
||||
if deleted > 0 {
|
||||
log.Printf(" 🔐 Vault stale-seal repair for user %s: cleared %d personal provider(s)", userID, deleted)
|
||||
} else {
|
||||
@@ -442,12 +412,7 @@ func (h *AuthHandler) initVault(ctx context.Context, userID, password string) er
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = database.DB.ExecContext(ctx, database.Q(`
|
||||
UPDATE users
|
||||
SET encrypted_uek = $1, uek_salt = $2, uek_nonce = $3, vault_set = true
|
||||
WHERE id = $4
|
||||
`), encryptedUEK, salt, nonce, userID)
|
||||
if err != nil {
|
||||
if err := h.stores.Users.InitVaultKeys(ctx, userID, encryptedUEK, salt, nonce); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -464,13 +429,7 @@ func (h *AuthHandler) unlockVault(ctx context.Context, user *models.User, passwo
|
||||
return
|
||||
}
|
||||
|
||||
var vaultSet bool
|
||||
var encryptedUEK, salt, nonce []byte
|
||||
|
||||
err := database.DB.QueryRowContext(ctx, database.Q(`
|
||||
SELECT vault_set, encrypted_uek, uek_salt, uek_nonce
|
||||
FROM users WHERE id = $1
|
||||
`), user.ID).Scan(&vaultSet, &encryptedUEK, &salt, &nonce)
|
||||
vaultSet, encryptedUEK, salt, nonce, err := h.stores.Users.GetVaultKeys(ctx, user.ID)
|
||||
if err != nil {
|
||||
log.Printf("⚠ Vault query failed for user %s: %v", user.ID, err)
|
||||
return
|
||||
@@ -489,10 +448,7 @@ func (h *AuthHandler) unlockVault(ctx context.Context, user *models.User, passwo
|
||||
uek, err := crypto.UnwrapUEK(encryptedUEK, nonce, pdk)
|
||||
if err != nil {
|
||||
// Stale seal: encrypted_uek was wrapped with a different password
|
||||
// (e.g. admin reset, BootstrapAdmin rotation, or pre-UEK migration).
|
||||
// The old UEK is irrecoverable. Destroy the vault and re-initialize
|
||||
// with the current password so the user isn't permanently locked out.
|
||||
deleted := DestroyVaultDB(ctx, user.ID)
|
||||
deleted := DestroyVaultDB(ctx, h.stores, user.ID)
|
||||
if deleted > 0 {
|
||||
log.Printf("⚠ Vault stale-seal recovery for user %s: cleared %d personal provider(s)", user.ID, deleted)
|
||||
} else {
|
||||
@@ -536,7 +492,7 @@ func BootstrapAdmin(cfg *config.Config, s store.Stores) {
|
||||
handle := auth.UniqueHandle(ctx, s.Users, models.HandleFromName(cfg.AdminUsername))
|
||||
s.Users.Update(ctx, existing.ID, map[string]interface{}{"handle": handle})
|
||||
}
|
||||
ProbeAndRepairVault(ctx, existing.ID, cfg.AdminPassword)
|
||||
ProbeAndRepairVault(ctx, s, existing.ID, cfg.AdminPassword)
|
||||
log.Printf(" ✅ Admin user '%s' updated", cfg.AdminUsername)
|
||||
return
|
||||
}
|
||||
@@ -626,7 +582,7 @@ func SeedUsers(cfg *config.Config, s store.Stores) {
|
||||
handle := auth.UniqueHandle(ctx, s.Users, models.HandleFromName(username))
|
||||
s.Users.Update(ctx, existing.ID, map[string]interface{}{"handle": handle})
|
||||
}
|
||||
ProbeAndRepairVault(ctx, existing.ID, password)
|
||||
ProbeAndRepairVault(ctx, s, existing.ID, password)
|
||||
log.Printf(" 🌱 Seed user '%s' updated (role=%s)", username, role)
|
||||
continue
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user