Files
LocalAI/core/http/auth/models.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

149 lines
4.5 KiB
Go

package auth
import (
"database/sql/driver"
"encoding/json"
"fmt"
"time"
)
// Auth provider constants.
const (
ProviderLocal = "local"
ProviderGitHub = "github"
ProviderOIDC = "oidc"
)
// User represents an authenticated user.
type User struct {
ID string `gorm:"primaryKey;size:36"`
Email string `gorm:"size:255;index"`
Name string `gorm:"size:255"`
AvatarURL string `gorm:"size:512"`
Provider string `gorm:"size:50"` // ProviderLocal, ProviderGitHub, ProviderOIDC
Subject string `gorm:"size:255"` // provider-specific user ID
PasswordHash string `json:"-"` // bcrypt hash, empty for OAuth-only users
Role string `gorm:"size:20;default:user"`
Status string `gorm:"size:20;default:active"` // "active", "pending"
CreatedAt time.Time
UpdatedAt time.Time
}
// Session represents a user login session.
type Session struct {
ID string `gorm:"primaryKey;size:64"` // HMAC-SHA256 hash of session token
UserID string `gorm:"size:36;index"`
ExpiresAt time.Time
RotatedAt time.Time
CreatedAt time.Time
User User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE"`
}
// UserAPIKey represents a user-generated API key for programmatic access.
type UserAPIKey struct {
ID string `gorm:"primaryKey;size:36"`
UserID string `gorm:"size:36;index"`
Name string `gorm:"size:255"` // user-provided label
KeyHash string `gorm:"size:64;uniqueIndex"`
KeyPrefix string `gorm:"size:12"` // first 8 chars of key for display
Role string `gorm:"size:20"`
CreatedAt time.Time
ExpiresAt *time.Time `gorm:"index"`
LastUsed *time.Time
User User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE"`
}
// PermissionMap is a flexible map of feature -> enabled, stored as JSON text.
// Known features: "agents", "skills", "collections", "mcp_jobs".
// New features can be added without schema changes.
type PermissionMap map[string]bool
// Value implements driver.Valuer for GORM JSON serialization.
func (p PermissionMap) Value() (driver.Value, error) {
if p == nil {
return "{}", nil
}
b, err := json.Marshal(p)
if err != nil {
return nil, fmt.Errorf("failed to marshal PermissionMap: %w", err)
}
return string(b), nil
}
// Scan implements sql.Scanner for GORM JSON deserialization.
func (p *PermissionMap) Scan(value any) error {
if value == nil {
*p = PermissionMap{}
return nil
}
var bytes []byte
switch v := value.(type) {
case string:
bytes = []byte(v)
case []byte:
bytes = v
default:
return fmt.Errorf("cannot scan %T into PermissionMap", value)
}
return json.Unmarshal(bytes, p)
}
// InviteCode represents an admin-generated invitation for user registration.
type InviteCode struct {
ID string `gorm:"primaryKey;size:36"`
Code string `gorm:"uniqueIndex;not null;size:64"` // HMAC-SHA256 hash of invite code
CodePrefix string `gorm:"size:12"` // first 8 chars for admin display
CreatedBy string `gorm:"size:36;not null"`
UsedBy *string `gorm:"size:36"`
UsedAt *time.Time
ExpiresAt time.Time `gorm:"not null;index"`
CreatedAt time.Time
Creator User `gorm:"foreignKey:CreatedBy"`
Consumer *User `gorm:"foreignKey:UsedBy"`
}
// ModelAllowlist controls which models a user can access.
// When Enabled is false (default), all models are allowed.
type ModelAllowlist struct {
Enabled bool `json:"enabled"`
Models []string `json:"models,omitempty"`
}
// Value implements driver.Valuer for GORM JSON serialization.
func (m ModelAllowlist) Value() (driver.Value, error) {
b, err := json.Marshal(m)
if err != nil {
return nil, fmt.Errorf("failed to marshal ModelAllowlist: %w", err)
}
return string(b), nil
}
// Scan implements sql.Scanner for GORM JSON deserialization.
func (m *ModelAllowlist) Scan(value any) error {
if value == nil {
*m = ModelAllowlist{}
return nil
}
var bytes []byte
switch v := value.(type) {
case string:
bytes = []byte(v)
case []byte:
bytes = v
default:
return fmt.Errorf("cannot scan %T into ModelAllowlist", value)
}
return json.Unmarshal(bytes, m)
}
// UserPermission stores per-user feature permissions.
type UserPermission struct {
ID string `gorm:"primaryKey;size:36"`
UserID string `gorm:"size:36;uniqueIndex"`
Permissions PermissionMap `gorm:"type:text"`
AllowedModels ModelAllowlist `gorm:"type:text"`
CreatedAt time.Time
UpdatedAt time.Time
User User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE"`
}