mirror of
https://github.com/mudler/LocalAI.git
synced 2026-04-01 13:42:20 -04:00
* feat(ui): add users and authentication support Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat: allow the admin user to impersonificate users Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * chore: ui improvements, disable 'Users' button in navbar when no auth is configured Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat: add OIDC support Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix: gate models Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * chore: cache requests to optimize speed Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * small UI enhancements Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * chore(ui): style improvements Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix: cover other paths by auth Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * chore: separate local auth, refactor Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * security hardening, approval mode Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix: fix tests and expectations Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * chore: update localagi/localrecall Signed-off-by: Ettore Di Giacinto <mudler@localai.io> --------- Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
156 lines
4.0 KiB
Go
156 lines
4.0 KiB
Go
//go:build auth
|
|
|
|
package auth_test
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
|
|
"github.com/labstack/echo/v4"
|
|
"github.com/mudler/LocalAI/core/config"
|
|
"github.com/mudler/LocalAI/core/http/auth"
|
|
. "github.com/onsi/gomega"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// testDB creates an in-memory SQLite GORM instance with auto-migration.
|
|
func testDB() *gorm.DB {
|
|
db, err := auth.InitDB(":memory:")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
return db
|
|
}
|
|
|
|
// createTestUser inserts a user directly into the DB for test setup.
|
|
func createTestUser(db *gorm.DB, email, role, provider string) *auth.User {
|
|
user := &auth.User{
|
|
ID: generateTestID(),
|
|
Email: email,
|
|
Name: "Test User",
|
|
Provider: provider,
|
|
Subject: generateTestID(),
|
|
Role: role,
|
|
Status: auth.StatusActive,
|
|
}
|
|
err := db.Create(user).Error
|
|
Expect(err).ToNot(HaveOccurred())
|
|
return user
|
|
}
|
|
|
|
// createTestSession creates a session for a user, returns plaintext session token.
|
|
func createTestSession(db *gorm.DB, userID string) string {
|
|
sessionID, err := auth.CreateSession(db, userID, "")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
return sessionID
|
|
}
|
|
|
|
var testIDCounter int
|
|
|
|
func generateTestID() string {
|
|
testIDCounter++
|
|
return "test-id-" + string(rune('a'+testIDCounter))
|
|
}
|
|
|
|
// ok is a simple handler that returns 200 OK.
|
|
func ok(c echo.Context) error {
|
|
return c.String(http.StatusOK, "ok")
|
|
}
|
|
|
|
// newAuthTestApp creates a minimal Echo app with the new auth middleware.
|
|
func newAuthTestApp(db *gorm.DB, appConfig *config.ApplicationConfig) *echo.Echo {
|
|
e := echo.New()
|
|
e.Use(auth.Middleware(db, appConfig))
|
|
|
|
// API routes (require auth)
|
|
e.GET("/v1/models", ok)
|
|
e.POST("/v1/chat/completions", ok)
|
|
e.GET("/api/settings", ok)
|
|
e.POST("/api/settings", ok)
|
|
|
|
// Auth routes (exempt)
|
|
e.GET("/api/auth/status", ok)
|
|
e.GET("/api/auth/github/login", ok)
|
|
|
|
// Static routes
|
|
e.GET("/app", ok)
|
|
e.GET("/app/*", ok)
|
|
|
|
return e
|
|
}
|
|
|
|
// newAdminTestApp creates an Echo app with admin-protected routes.
|
|
func newAdminTestApp(db *gorm.DB, appConfig *config.ApplicationConfig) *echo.Echo {
|
|
e := echo.New()
|
|
e.Use(auth.Middleware(db, appConfig))
|
|
|
|
// Regular routes
|
|
e.GET("/v1/models", ok)
|
|
e.POST("/v1/chat/completions", ok)
|
|
|
|
// Admin-only routes
|
|
adminMw := auth.RequireAdmin()
|
|
e.POST("/api/settings", ok, adminMw)
|
|
e.POST("/models/apply", ok, adminMw)
|
|
e.POST("/backends/apply", ok, adminMw)
|
|
e.GET("/api/agents", ok, adminMw)
|
|
|
|
// Trace/log endpoints (admin only)
|
|
e.GET("/api/traces", ok, adminMw)
|
|
e.POST("/api/traces/clear", ok, adminMw)
|
|
e.GET("/api/backend-logs", ok, adminMw)
|
|
e.GET("/api/backend-logs/:modelId", ok, adminMw)
|
|
|
|
// Gallery/management reads (admin only)
|
|
e.GET("/api/operations", ok, adminMw)
|
|
e.GET("/api/models", ok, adminMw)
|
|
e.GET("/api/backends", ok, adminMw)
|
|
e.GET("/api/resources", ok, adminMw)
|
|
e.GET("/api/p2p/workers", ok, adminMw)
|
|
|
|
// Agent task/job routes (admin only)
|
|
e.POST("/api/agent/tasks", ok, adminMw)
|
|
e.GET("/api/agent/tasks", ok, adminMw)
|
|
e.GET("/api/agent/jobs", ok, adminMw)
|
|
|
|
// System info (admin only)
|
|
e.GET("/system", ok, adminMw)
|
|
e.GET("/backend/monitor", ok, adminMw)
|
|
|
|
return e
|
|
}
|
|
|
|
// doRequest performs an HTTP request against the given Echo app and returns the recorder.
|
|
func doRequest(e *echo.Echo, method, path string, opts ...func(*http.Request)) *httptest.ResponseRecorder {
|
|
req := httptest.NewRequest(method, path, nil)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
for _, opt := range opts {
|
|
opt(req)
|
|
}
|
|
rec := httptest.NewRecorder()
|
|
e.ServeHTTP(rec, req)
|
|
return rec
|
|
}
|
|
|
|
func withBearerToken(token string) func(*http.Request) {
|
|
return func(req *http.Request) {
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
}
|
|
}
|
|
|
|
func withXApiKey(key string) func(*http.Request) {
|
|
return func(req *http.Request) {
|
|
req.Header.Set("x-api-key", key)
|
|
}
|
|
}
|
|
|
|
func withSessionCookie(sessionID string) func(*http.Request) {
|
|
return func(req *http.Request) {
|
|
req.AddCookie(&http.Cookie{Name: "session", Value: sessionID})
|
|
}
|
|
}
|
|
|
|
func withTokenCookie(token string) func(*http.Request) {
|
|
return func(req *http.Request) {
|
|
req.AddCookie(&http.Cookie{Name: "token", Value: token})
|
|
}
|
|
}
|