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>
143 lines
4.2 KiB
Go
143 lines
4.2 KiB
Go
package services
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// UserScopedStorage resolves per-user storage directories.
|
|
// When userID is empty, paths resolve to root-level (backward compat).
|
|
// When userID is set, paths resolve to {baseDir}/users/{userID}/...
|
|
type UserScopedStorage struct {
|
|
baseDir string // State directory
|
|
dataDir string // Data directory (for jobs files)
|
|
}
|
|
|
|
// NewUserScopedStorage creates a new UserScopedStorage.
|
|
func NewUserScopedStorage(baseDir, dataDir string) *UserScopedStorage {
|
|
return &UserScopedStorage{
|
|
baseDir: baseDir,
|
|
dataDir: dataDir,
|
|
}
|
|
}
|
|
|
|
// resolve returns baseDir for empty userID, or baseDir/users/{userID} otherwise.
|
|
func (s *UserScopedStorage) resolve(userID string) string {
|
|
if userID == "" {
|
|
return s.baseDir
|
|
}
|
|
return filepath.Join(s.baseDir, "users", userID)
|
|
}
|
|
|
|
// resolveData returns dataDir for empty userID, or baseDir/users/{userID} otherwise.
|
|
func (s *UserScopedStorage) resolveData(userID string) string {
|
|
if userID == "" {
|
|
return s.dataDir
|
|
}
|
|
return filepath.Join(s.baseDir, "users", userID)
|
|
}
|
|
|
|
// UserDir returns the root directory for a user's scoped data.
|
|
func (s *UserScopedStorage) UserDir(userID string) string {
|
|
return s.resolve(userID)
|
|
}
|
|
|
|
// CollectionsDir returns the collections directory for a user.
|
|
func (s *UserScopedStorage) CollectionsDir(userID string) string {
|
|
return filepath.Join(s.resolve(userID), "collections")
|
|
}
|
|
|
|
// AssetsDir returns the assets directory for a user.
|
|
func (s *UserScopedStorage) AssetsDir(userID string) string {
|
|
return filepath.Join(s.resolve(userID), "assets")
|
|
}
|
|
|
|
// OutputsDir returns the outputs directory for a user.
|
|
func (s *UserScopedStorage) OutputsDir(userID string) string {
|
|
return filepath.Join(s.resolve(userID), "outputs")
|
|
}
|
|
|
|
// SkillsDir returns the skills directory for a user.
|
|
func (s *UserScopedStorage) SkillsDir(userID string) string {
|
|
return filepath.Join(s.resolve(userID), "skills")
|
|
}
|
|
|
|
// TasksFile returns the path to the agent_tasks.json for a user.
|
|
func (s *UserScopedStorage) TasksFile(userID string) string {
|
|
return filepath.Join(s.resolveData(userID), "agent_tasks.json")
|
|
}
|
|
|
|
// JobsFile returns the path to the agent_jobs.json for a user.
|
|
func (s *UserScopedStorage) JobsFile(userID string) string {
|
|
return filepath.Join(s.resolveData(userID), "agent_jobs.json")
|
|
}
|
|
|
|
// EnsureUserDirs creates all subdirectories for a user.
|
|
func (s *UserScopedStorage) EnsureUserDirs(userID string) error {
|
|
dirs := []string{
|
|
s.CollectionsDir(userID),
|
|
s.AssetsDir(userID),
|
|
s.OutputsDir(userID),
|
|
s.SkillsDir(userID),
|
|
}
|
|
for _, d := range dirs {
|
|
if err := os.MkdirAll(d, 0750); err != nil {
|
|
return fmt.Errorf("failed to create directory %s: %w", d, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var uuidRegex = regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`)
|
|
|
|
// ListUserDirs scans {baseDir}/users/ and returns sorted UUIDs matching uuidRegex.
|
|
// Returns an empty slice if the directory doesn't exist.
|
|
func (s *UserScopedStorage) ListUserDirs() ([]string, error) {
|
|
usersDir := filepath.Join(s.baseDir, "users")
|
|
entries, err := os.ReadDir(usersDir)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return []string{}, nil
|
|
}
|
|
return nil, fmt.Errorf("failed to read users directory: %w", err)
|
|
}
|
|
var ids []string
|
|
for _, e := range entries {
|
|
if e.IsDir() && uuidRegex.MatchString(e.Name()) {
|
|
ids = append(ids, e.Name())
|
|
}
|
|
}
|
|
sort.Strings(ids)
|
|
return ids, nil
|
|
}
|
|
|
|
// ValidateUserID validates that a userID is safe for use in filesystem paths.
|
|
// Empty string is allowed (maps to root storage). Otherwise must be a valid UUID.
|
|
func ValidateUserID(id string) error {
|
|
if id == "" {
|
|
return nil
|
|
}
|
|
if strings.ContainsAny(id, "/\\") || strings.Contains(id, "..") {
|
|
return fmt.Errorf("invalid user ID: contains path traversal characters")
|
|
}
|
|
if !uuidRegex.MatchString(id) {
|
|
return fmt.Errorf("invalid user ID: must be a valid UUID")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ValidateAgentName validates that an agent name is safe (no namespace escape or path traversal).
|
|
func ValidateAgentName(name string) error {
|
|
if name == "" {
|
|
return fmt.Errorf("agent name is required")
|
|
}
|
|
if strings.ContainsAny(name, ":/\\\x00") || strings.Contains(name, "..") {
|
|
return fmt.Errorf("agent name contains invalid characters (: / \\ .. or null bytes are not allowed)")
|
|
}
|
|
return nil
|
|
}
|