Files
LocalAI/core/services/user_services.go
Ettore Di Giacinto aea21951a2 feat: add users and authentication support (#9061)
* 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>
2026-03-19 21:40:51 +01:00

184 lines
4.6 KiB
Go

package services
import (
"sync"
"github.com/mudler/LocalAI/core/config"
"github.com/mudler/LocalAI/core/templates"
"github.com/mudler/LocalAI/pkg/model"
"github.com/mudler/LocalAGI/services/skills"
"github.com/mudler/LocalAGI/webui/collections"
"github.com/mudler/xlog"
)
// UserServicesManager lazily creates per-user service instances for
// collections, skills, and jobs.
type UserServicesManager struct {
mu sync.RWMutex
storage *UserScopedStorage
appConfig *config.ApplicationConfig
modelLoader *model.ModelLoader
configLoader *config.ModelConfigLoader
evaluator *templates.Evaluator
collectionsCache map[string]collections.Backend
skillsCache map[string]*skills.Service
jobsCache map[string]*AgentJobService
}
// NewUserServicesManager creates a new UserServicesManager.
func NewUserServicesManager(
storage *UserScopedStorage,
appConfig *config.ApplicationConfig,
modelLoader *model.ModelLoader,
configLoader *config.ModelConfigLoader,
evaluator *templates.Evaluator,
) *UserServicesManager {
return &UserServicesManager{
storage: storage,
appConfig: appConfig,
modelLoader: modelLoader,
configLoader: configLoader,
evaluator: evaluator,
collectionsCache: make(map[string]collections.Backend),
skillsCache: make(map[string]*skills.Service),
jobsCache: make(map[string]*AgentJobService),
}
}
// GetCollections returns the collections backend for a user, creating it lazily.
func (m *UserServicesManager) GetCollections(userID string) (collections.Backend, error) {
m.mu.RLock()
if backend, ok := m.collectionsCache[userID]; ok {
m.mu.RUnlock()
return backend, nil
}
m.mu.RUnlock()
m.mu.Lock()
defer m.mu.Unlock()
// Double-check after acquiring write lock
if backend, ok := m.collectionsCache[userID]; ok {
return backend, nil
}
if err := m.storage.EnsureUserDirs(userID); err != nil {
return nil, err
}
cfg := m.appConfig.AgentPool
apiURL := cfg.APIURL
if apiURL == "" {
apiURL = "http://127.0.0.1:" + getPort(m.appConfig)
}
apiKey := cfg.APIKey
if apiKey == "" && len(m.appConfig.ApiKeys) > 0 {
apiKey = m.appConfig.ApiKeys[0]
}
collectionsCfg := &collections.Config{
LLMAPIURL: apiURL,
LLMAPIKey: apiKey,
LLMModel: cfg.DefaultModel,
CollectionDBPath: m.storage.CollectionsDir(userID),
FileAssets: m.storage.AssetsDir(userID),
VectorEngine: cfg.VectorEngine,
EmbeddingModel: cfg.EmbeddingModel,
MaxChunkingSize: cfg.MaxChunkingSize,
ChunkOverlap: cfg.ChunkOverlap,
DatabaseURL: cfg.DatabaseURL,
}
backend, _ := collections.NewInProcessBackend(collectionsCfg)
m.collectionsCache[userID] = backend
return backend, nil
}
// GetSkills returns the skills service for a user, creating it lazily.
func (m *UserServicesManager) GetSkills(userID string) (*skills.Service, error) {
m.mu.RLock()
if svc, ok := m.skillsCache[userID]; ok {
m.mu.RUnlock()
return svc, nil
}
m.mu.RUnlock()
m.mu.Lock()
defer m.mu.Unlock()
if svc, ok := m.skillsCache[userID]; ok {
return svc, nil
}
if err := m.storage.EnsureUserDirs(userID); err != nil {
return nil, err
}
skillsDir := m.storage.SkillsDir(userID)
svc, err := skills.NewService(skillsDir)
if err != nil {
return nil, err
}
m.skillsCache[userID] = svc
return svc, nil
}
// GetJobs returns the agent job service for a user, creating it lazily.
func (m *UserServicesManager) GetJobs(userID string) (*AgentJobService, error) {
m.mu.RLock()
if svc, ok := m.jobsCache[userID]; ok {
m.mu.RUnlock()
return svc, nil
}
m.mu.RUnlock()
m.mu.Lock()
defer m.mu.Unlock()
if svc, ok := m.jobsCache[userID]; ok {
return svc, nil
}
if err := m.storage.EnsureUserDirs(userID); err != nil {
return nil, err
}
svc := NewAgentJobServiceWithPaths(
m.appConfig,
m.modelLoader,
m.configLoader,
m.evaluator,
m.storage.TasksFile(userID),
m.storage.JobsFile(userID),
)
m.jobsCache[userID] = svc
return svc, nil
}
// ListAllUserIDs returns all user IDs that have scoped data directories.
func (m *UserServicesManager) ListAllUserIDs() ([]string, error) {
return m.storage.ListUserDirs()
}
// getPort extracts the port from the API address config.
func getPort(appConfig *config.ApplicationConfig) string {
addr := appConfig.APIAddress
for i := len(addr) - 1; i >= 0; i-- {
if addr[i] == ':' {
return addr[i+1:]
}
}
return addr
}
// StopAll stops all cached job services.
func (m *UserServicesManager) StopAll() {
m.mu.Lock()
defer m.mu.Unlock()
for _, svc := range m.jobsCache {
if err := svc.Stop(); err != nil {
xlog.Error("Failed to stop user job service", "error", err)
}
}
}