115 lines
4.5 KiB
Go
115 lines
4.5 KiB
Go
//go:build auth
|
|
|
|
package auth_test
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/mudler/LocalAI/core/http/auth"
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// Regression coverage for the production bug where deleting a user from the
|
|
// distributed-mode (PostgreSQL) admin UI returned "user not found" because the
|
|
// old delete path ignored result.Error and left several tables uncleaned.
|
|
var _ = Describe("DeleteUserCascade", Label("auth"), func() {
|
|
var db *gorm.DB
|
|
|
|
BeforeEach(func() {
|
|
db = testDB()
|
|
})
|
|
|
|
It("returns ErrRecordNotFound when the user does not exist", func() {
|
|
err := auth.DeleteUserCascade(db, uuid.New().String())
|
|
Expect(err).To(Equal(gorm.ErrRecordNotFound))
|
|
})
|
|
|
|
It("removes invite codes the user authored", func() {
|
|
target := createTestUser(db, "author@test.com", auth.RoleAdmin, auth.ProviderLocal)
|
|
Expect(db.Create(&auth.InviteCode{
|
|
ID: uuid.New().String(), Code: "code-author-1", CodePrefix: "code-aut",
|
|
CreatedBy: target.ID, ExpiresAt: time.Now().Add(time.Hour),
|
|
}).Error).ToNot(HaveOccurred())
|
|
|
|
Expect(auth.DeleteUserCascade(db, target.ID)).ToNot(HaveOccurred())
|
|
|
|
var count int64
|
|
db.Model(&auth.InviteCode{}).Where("created_by = ?", target.ID).Count(&count)
|
|
Expect(count).To(Equal(int64(0)))
|
|
})
|
|
|
|
It("nulls used_by on invite codes the user consumed but keeps the audit row", func() {
|
|
admin := createTestUser(db, "admin-keep@test.com", auth.RoleAdmin, auth.ProviderLocal)
|
|
target := createTestUser(db, "consumer@test.com", auth.RoleUser, auth.ProviderLocal)
|
|
|
|
usedBy := target.ID
|
|
now := time.Now()
|
|
invite := &auth.InviteCode{
|
|
ID: uuid.New().String(), Code: "code-used-1", CodePrefix: "code-use",
|
|
CreatedBy: admin.ID, UsedBy: &usedBy, UsedAt: &now,
|
|
ExpiresAt: now.Add(time.Hour),
|
|
}
|
|
Expect(db.Create(invite).Error).ToNot(HaveOccurred())
|
|
|
|
Expect(auth.DeleteUserCascade(db, target.ID)).ToNot(HaveOccurred())
|
|
|
|
var refreshed auth.InviteCode
|
|
Expect(db.First(&refreshed, "id = ?", invite.ID).Error).ToNot(HaveOccurred())
|
|
Expect(refreshed.UsedBy).To(BeNil(), "used_by should be cleared so the FK no longer points to the deleted user")
|
|
})
|
|
|
|
It("wipes sessions, api keys, permissions, quotas, and usage metrics", func() {
|
|
target := createTestUser(db, "owns-data@test.com", auth.RoleUser, auth.ProviderLocal)
|
|
|
|
_ = createTestSession(db, target.ID)
|
|
_, _, err := auth.CreateAPIKey(db, target.ID, "k1", auth.RoleUser, "", nil)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(auth.UpdateUserPermissions(db, target.ID, auth.PermissionMap{auth.FeatureChat: true})).ToNot(HaveOccurred())
|
|
max := int64(100)
|
|
_, err = auth.CreateOrUpdateQuotaRule(db, target.ID, "", &max, nil, 3600)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(auth.RecordUsage(db, &auth.UsageRecord{
|
|
UserID: target.ID, UserName: target.Name, Model: "test-model",
|
|
Endpoint: "/v1/chat/completions", PromptTokens: 5, CompletionTokens: 10, TotalTokens: 15,
|
|
})).ToNot(HaveOccurred())
|
|
|
|
Expect(auth.DeleteUserCascade(db, target.ID)).ToNot(HaveOccurred())
|
|
|
|
var sessions, keys, perms, quotas, usage int64
|
|
db.Model(&auth.Session{}).Where("user_id = ?", target.ID).Count(&sessions)
|
|
db.Model(&auth.UserAPIKey{}).Where("user_id = ?", target.ID).Count(&keys)
|
|
db.Model(&auth.UserPermission{}).Where("user_id = ?", target.ID).Count(&perms)
|
|
db.Model(&auth.QuotaRule{}).Where("user_id = ?", target.ID).Count("as)
|
|
db.Model(&auth.UsageRecord{}).Where("user_id = ?", target.ID).Count(&usage)
|
|
|
|
Expect(sessions).To(Equal(int64(0)))
|
|
Expect(keys).To(Equal(int64(0)))
|
|
Expect(perms).To(Equal(int64(0)))
|
|
Expect(quotas).To(Equal(int64(0)))
|
|
Expect(usage).To(Equal(int64(0)), "usage metrics must be removed alongside the user")
|
|
})
|
|
|
|
It("succeeds with foreign keys enforced — the production failure mode", func() {
|
|
// Mirror PostgreSQL's strict FK behavior on the SQLite test DB. Without
|
|
// the cleanup of invite_codes.created_by, the engine would reject the
|
|
// user delete with a constraint violation, which the old handler then
|
|
// surfaced as a misleading 404.
|
|
Expect(db.Exec("PRAGMA foreign_keys = ON").Error).ToNot(HaveOccurred())
|
|
|
|
target := createTestUser(db, "fk-author@test.com", auth.RoleAdmin, auth.ProviderLocal)
|
|
Expect(db.Create(&auth.InviteCode{
|
|
ID: uuid.New().String(), Code: "code-fk-1", CodePrefix: "code-fk1",
|
|
CreatedBy: target.ID, ExpiresAt: time.Now().Add(time.Hour),
|
|
}).Error).ToNot(HaveOccurred())
|
|
|
|
Expect(auth.DeleteUserCascade(db, target.ID)).ToNot(HaveOccurred())
|
|
|
|
var users int64
|
|
db.Model(&auth.User{}).Where("id = ?", target.ID).Count(&users)
|
|
Expect(users).To(Equal(int64(0)))
|
|
})
|
|
})
|