Skip to content

Commit 01f4543

Browse files
committed
feat(ui): add users and authentication support
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
1 parent cfb7641 commit 01f4543

76 files changed

Lines changed: 9343 additions & 544 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ RUN apt-get update && \
256256

257257
FROM build-requirements AS builder-base
258258

259-
ARG GO_TAGS=""
259+
ARG GO_TAGS="auth"
260260
ARG GRPC_BACKENDS
261261
ARG MAKEFLAGS
262262
ARG LD_FLAGS="-s -w"

core/application/application.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/mudler/LocalAI/core/templates"
1212
"github.com/mudler/LocalAI/pkg/model"
1313
"github.com/mudler/xlog"
14+
"gorm.io/gorm"
1415
)
1516

1617
type Application struct {
@@ -22,6 +23,7 @@ type Application struct {
2223
galleryService *services.GalleryService
2324
agentJobService *services.AgentJobService
2425
agentPoolService atomic.Pointer[services.AgentPoolService]
26+
authDB *gorm.DB
2527
watchdogMutex sync.Mutex
2628
watchdogStop chan bool
2729
p2pMutex sync.Mutex
@@ -74,6 +76,11 @@ func (a *Application) AgentPoolService() *services.AgentPoolService {
7476
return a.agentPoolService.Load()
7577
}
7678

79+
// AuthDB returns the auth database connection, or nil if auth is not enabled.
80+
func (a *Application) AuthDB() *gorm.DB {
81+
return a.authDB
82+
}
83+
7784
// StartupConfig returns the original startup configuration (from env vars, before file loading)
7885
func (a *Application) StartupConfig() *config.ApplicationConfig {
7986
return a.startupConfig
@@ -118,9 +125,23 @@ func (a *Application) StartAgentPool() {
118125
xlog.Error("Failed to create agent pool service", "error", err)
119126
return
120127
}
128+
if a.authDB != nil {
129+
aps.SetAuthDB(a.authDB)
130+
}
121131
if err := aps.Start(a.applicationConfig.Context); err != nil {
122132
xlog.Error("Failed to start agent pool", "error", err)
123133
return
124134
}
135+
136+
// Wire per-user scoped services so collections, skills, and jobs are isolated per user
137+
usm := services.NewUserServicesManager(
138+
aps.UserStorage(),
139+
a.applicationConfig,
140+
a.modelLoader,
141+
a.backendLoader,
142+
a.templatesEvaluator,
143+
)
144+
aps.SetUserServicesManager(usm)
145+
125146
a.agentPoolService.Store(aps)
126147
}

core/application/startup.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/mudler/LocalAI/core/backend"
1111
"github.com/mudler/LocalAI/core/config"
1212
"github.com/mudler/LocalAI/core/gallery"
13+
"github.com/mudler/LocalAI/core/http/auth"
1314
"github.com/mudler/LocalAI/core/services"
1415
coreStartup "github.com/mudler/LocalAI/core/startup"
1516
"github.com/mudler/LocalAI/internal"
@@ -81,6 +82,32 @@ func New(opts ...config.AppOption) (*Application, error) {
8182
}
8283
}
8384

85+
// Initialize auth database if auth is enabled
86+
if options.Auth.Enabled {
87+
authDB, err := auth.InitDB(options.Auth.DatabaseURL)
88+
if err != nil {
89+
return nil, fmt.Errorf("failed to initialize auth database: %w", err)
90+
}
91+
application.authDB = authDB
92+
xlog.Info("Auth enabled", "database", options.Auth.DatabaseURL)
93+
94+
// Start session cleanup goroutine
95+
go func() {
96+
ticker := time.NewTicker(1 * time.Hour)
97+
defer ticker.Stop()
98+
for {
99+
select {
100+
case <-options.Context.Done():
101+
return
102+
case <-ticker.C:
103+
if err := auth.CleanExpiredSessions(authDB); err != nil {
104+
xlog.Error("failed to clean expired sessions", "error", err)
105+
}
106+
}
107+
}
108+
}()
109+
}
110+
84111
if err := coreStartup.InstallModels(options.Context, application.GalleryService(), options.Galleries, options.BackendGalleries, options.SystemState, application.ModelLoader(), options.EnforcePredownloadScans, options.AutoloadBackendGalleries, nil, options.ModelsURL...); err != nil {
85112
xlog.Error("error installing models", "error", err)
86113
}

core/cli/run.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,15 @@ type RunCMD struct {
121121
AgentPoolCollectionDBPath string `env:"LOCALAI_AGENT_POOL_COLLECTION_DB_PATH" help:"Database path for agent collections" group:"agents"`
122122
AgentHubURL string `env:"LOCALAI_AGENT_HUB_URL" default:"https://agenthub.localai.io" help:"URL for the agent hub where users can browse and download agent configurations" group:"agents"`
123123

124+
// Authentication
125+
AuthEnabled bool `env:"LOCALAI_AUTH" default:"false" help:"Enable user authentication and authorization" group:"auth"`
126+
AuthDatabaseURL string `env:"LOCALAI_AUTH_DATABASE_URL,DATABASE_URL" help:"Database URL for auth (postgres:// or file path for SQLite). Defaults to {DataPath}/database.db" group:"auth"`
127+
GitHubClientID string `env:"GITHUB_CLIENT_ID" help:"GitHub OAuth App Client ID (auto-enables auth when set)" group:"auth"`
128+
GitHubClientSecret string `env:"GITHUB_CLIENT_SECRET" help:"GitHub OAuth App Client Secret" group:"auth"`
129+
AuthBaseURL string `env:"LOCALAI_BASE_URL" help:"Base URL for OAuth callbacks (e.g. http://localhost:8080)" group:"auth"`
130+
AuthAdminEmail string `env:"LOCALAI_ADMIN_EMAIL" help:"Email address to auto-promote to admin role" group:"auth"`
131+
AuthRegistrationMode string `env:"LOCALAI_REGISTRATION_MODE" default:"open" help:"Registration mode: 'open' (default) or 'approval'" group:"auth"`
132+
124133
Version bool
125134
}
126135

@@ -311,6 +320,32 @@ func (r *RunCMD) Run(ctx *cliContext.Context) error {
311320
opts = append(opts, config.WithAgentHubURL(r.AgentHubURL))
312321
}
313322

323+
// Authentication
324+
authEnabled := r.AuthEnabled || r.GitHubClientID != ""
325+
if authEnabled {
326+
opts = append(opts, config.WithAuthEnabled(true))
327+
328+
dbURL := r.AuthDatabaseURL
329+
if dbURL == "" {
330+
dbURL = filepath.Join(r.DataPath, "database.db")
331+
}
332+
opts = append(opts, config.WithAuthDatabaseURL(dbURL))
333+
334+
if r.GitHubClientID != "" {
335+
opts = append(opts, config.WithAuthGitHubClientID(r.GitHubClientID))
336+
opts = append(opts, config.WithAuthGitHubClientSecret(r.GitHubClientSecret))
337+
}
338+
if r.AuthBaseURL != "" {
339+
opts = append(opts, config.WithAuthBaseURL(r.AuthBaseURL))
340+
}
341+
if r.AuthAdminEmail != "" {
342+
opts = append(opts, config.WithAuthAdminEmail(r.AuthAdminEmail))
343+
}
344+
if r.AuthRegistrationMode != "" {
345+
opts = append(opts, config.WithAuthRegistrationMode(r.AuthRegistrationMode))
346+
}
347+
}
348+
314349
if idleWatchDog || busyWatchDog {
315350
opts = append(opts, config.EnableWatchDog)
316351
if idleWatchDog {

core/config/application_config.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,20 @@ type ApplicationConfig struct {
9696

9797
// Agent Pool (LocalAGI integration)
9898
AgentPool AgentPoolConfig
99+
100+
// Authentication & Authorization
101+
Auth AuthConfig
102+
}
103+
104+
// AuthConfig holds configuration for user authentication and authorization.
105+
type AuthConfig struct {
106+
Enabled bool
107+
DatabaseURL string // "postgres://..." or file path for SQLite
108+
GitHubClientID string
109+
GitHubClientSecret string
110+
BaseURL string // for OAuth callback URLs (e.g. "http://localhost:8080")
111+
AdminEmail string // auto-promote to admin on login
112+
RegistrationMode string // "open" (default), "approval", "invite"
99113
}
100114

101115
// AgentPoolConfig holds configuration for the LocalAGI agent pool integration.
@@ -150,6 +164,8 @@ func NewApplicationConfig(o ...AppOption) *ApplicationConfig {
150164
"/favicon.svg",
151165
"/readyz",
152166
"/healthz",
167+
"/api/auth/",
168+
"/assets/",
153169
},
154170
}
155171
for _, oo := range o {
@@ -711,6 +727,50 @@ func WithAgentHubURL(url string) AppOption {
711727
}
712728
}
713729

730+
// Auth options
731+
732+
func WithAuthEnabled(enabled bool) AppOption {
733+
return func(o *ApplicationConfig) {
734+
o.Auth.Enabled = enabled
735+
}
736+
}
737+
738+
func WithAuthDatabaseURL(url string) AppOption {
739+
return func(o *ApplicationConfig) {
740+
o.Auth.DatabaseURL = url
741+
}
742+
}
743+
744+
func WithAuthGitHubClientID(clientID string) AppOption {
745+
return func(o *ApplicationConfig) {
746+
o.Auth.GitHubClientID = clientID
747+
}
748+
}
749+
750+
func WithAuthGitHubClientSecret(clientSecret string) AppOption {
751+
return func(o *ApplicationConfig) {
752+
o.Auth.GitHubClientSecret = clientSecret
753+
}
754+
}
755+
756+
func WithAuthBaseURL(baseURL string) AppOption {
757+
return func(o *ApplicationConfig) {
758+
o.Auth.BaseURL = baseURL
759+
}
760+
}
761+
762+
func WithAuthAdminEmail(email string) AppOption {
763+
return func(o *ApplicationConfig) {
764+
o.Auth.AdminEmail = email
765+
}
766+
}
767+
768+
func WithAuthRegistrationMode(mode string) AppOption {
769+
return func(o *ApplicationConfig) {
770+
o.Auth.RegistrationMode = mode
771+
}
772+
}
773+
714774
// ToConfigLoaderOptions returns a slice of ConfigLoader Option.
715775
// Some options defined at the application level are going to be passed as defaults for
716776
// all the configuration for the models.

core/http/app.go

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/labstack/echo/v4"
1515
"github.com/labstack/echo/v4/middleware"
1616

17+
"github.com/mudler/LocalAI/core/http/auth"
1718
"github.com/mudler/LocalAI/core/http/endpoints/localai"
1819
httpMiddleware "github.com/mudler/LocalAI/core/http/middleware"
1920
"github.com/mudler/LocalAI/core/http/routes"
@@ -170,11 +171,9 @@ func API(application *application.Application) (*echo.Echo, error) {
170171
// Health Checks should always be exempt from auth, so register these first
171172
routes.HealthRoutes(e)
172173

173-
// Get key auth middleware
174-
keyAuthMiddleware, err := httpMiddleware.GetKeyAuthConfig(application.ApplicationConfig())
175-
if err != nil {
176-
return nil, fmt.Errorf("failed to create key auth config: %w", err)
177-
}
174+
// Build auth middleware: use the new auth.Middleware when auth is enabled or
175+
// as a unified replacement for the legacy key-auth middleware.
176+
authMiddleware := auth.Middleware(application.AuthDB(), application.ApplicationConfig())
178177

179178
// Favicon handler
180179
e.GET("/favicon.svg", func(c echo.Context) error {
@@ -209,8 +208,14 @@ func API(application *application.Application) (*echo.Echo, error) {
209208
e.Static("/generated-videos", videoPath)
210209
}
211210

212-
// Auth is applied to _all_ endpoints. No exceptions. Filtering out endpoints to bypass is the role of the Skipper property of the KeyAuth Configuration
213-
e.Use(keyAuthMiddleware)
211+
// Initialize usage recording when auth DB is available
212+
if application.AuthDB() != nil {
213+
httpMiddleware.InitUsageRecorder(application.AuthDB())
214+
}
215+
216+
// Auth is applied to _all_ endpoints. Filtering out endpoints to bypass is
217+
// the role of the exempt-path logic inside the middleware.
218+
e.Use(authMiddleware)
214219

215220
// CORS middleware
216221
if application.ApplicationConfig().CORS {
@@ -229,8 +234,25 @@ func API(application *application.Application) (*echo.Echo, error) {
229234
e.Use(middleware.CSRF())
230235
}
231236

237+
// Admin middleware: enforces admin role when auth is enabled, no-op otherwise
238+
var adminMiddleware echo.MiddlewareFunc
239+
if application.AuthDB() != nil {
240+
adminMiddleware = auth.RequireAdmin()
241+
} else {
242+
adminMiddleware = auth.NoopMiddleware()
243+
}
244+
245+
// Feature middlewares: per-feature access control
246+
agentsMw := auth.RequireFeature(application.AuthDB(), auth.FeatureAgents)
247+
skillsMw := auth.RequireFeature(application.AuthDB(), auth.FeatureSkills)
248+
collectionsMw := auth.RequireFeature(application.AuthDB(), auth.FeatureCollections)
249+
mcpJobsMw := auth.RequireFeature(application.AuthDB(), auth.FeatureMCPJobs)
250+
232251
requestExtractor := httpMiddleware.NewRequestExtractor(application.ModelConfigLoader(), application.ModelLoader(), application.ApplicationConfig())
233252

253+
// Register auth routes (login, callback, API keys, user management)
254+
routes.RegisterAuthRoutes(e, application)
255+
234256
routes.RegisterElevenLabsRoutes(e, requestExtractor, application.ModelConfigLoader(), application.ModelLoader(), application.ApplicationConfig())
235257

236258
// Create opcache for tracking UI operations (used by both UI and LocalAI routes)
@@ -239,14 +261,14 @@ func API(application *application.Application) (*echo.Echo, error) {
239261
opcache = services.NewOpCache(application.GalleryService())
240262
}
241263

242-
routes.RegisterLocalAIRoutes(e, requestExtractor, application.ModelConfigLoader(), application.ModelLoader(), application.ApplicationConfig(), application.GalleryService(), opcache, application.TemplatesEvaluator(), application)
243-
routes.RegisterAgentPoolRoutes(e, application)
264+
routes.RegisterLocalAIRoutes(e, requestExtractor, application.ModelConfigLoader(), application.ModelLoader(), application.ApplicationConfig(), application.GalleryService(), opcache, application.TemplatesEvaluator(), application, adminMiddleware, mcpJobsMw)
265+
routes.RegisterAgentPoolRoutes(e, application, agentsMw, skillsMw, collectionsMw)
244266
routes.RegisterOpenAIRoutes(e, requestExtractor, application)
245267
routes.RegisterAnthropicRoutes(e, requestExtractor, application)
246268
routes.RegisterOpenResponsesRoutes(e, requestExtractor, application)
247269
if !application.ApplicationConfig().DisableWebUI {
248-
routes.RegisterUIAPIRoutes(e, application.ModelConfigLoader(), application.ModelLoader(), application.ApplicationConfig(), application.GalleryService(), opcache, application)
249-
routes.RegisterUIRoutes(e, application.ModelConfigLoader(), application.ModelLoader(), application.ApplicationConfig(), application.GalleryService())
270+
routes.RegisterUIAPIRoutes(e, application.ModelConfigLoader(), application.ModelLoader(), application.ApplicationConfig(), application.GalleryService(), opcache, application, adminMiddleware)
271+
routes.RegisterUIRoutes(e, application.ModelConfigLoader(), application.ModelLoader(), application.ApplicationConfig(), application.GalleryService(), adminMiddleware)
250272

251273
// Serve React SPA from / with SPA fallback via 404 handler
252274
reactFS, fsErr := fs.Sub(reactUI, "react-ui/dist")

0 commit comments

Comments
 (0)