package admin import ( "fmt" "os" "path/filepath" "gopkg.in/yaml.v3" ) // AutheliaUserDB represents the full structure of Authelia's users_database.yml. type AutheliaUserDB struct { Users map[string]AutheliaUserEntry `yaml:"users"` } // AutheliaUserEntry represents a single user entry in Authelia's YAML. type AutheliaUserEntry struct { DisplayName string `yaml:"displayname,omitempty"` Password string `yaml:"password"` Email string `yaml:"email,omitempty"` Groups []string `yaml:"groups,omitempty"` Disabled bool `yaml:"disabled,omitempty"` } // SyncWriter handles writing the user database to Authelia's YAML format. type SyncWriter struct { usersDBPath string store *UserStore } // NewSyncWriter creates a new SyncWriter. func NewSyncWriter(usersDBPath string, store *UserStore) *SyncWriter { return &SyncWriter{ usersDBPath: usersDBPath, store: store, } } // Sync writes the current user store to Authelia's users_database.yml. func (sw *SyncWriter) Sync() error { syncUsers, err := sw.store.SyncSnapshot() if err != nil { return fmt.Errorf("get sync snapshot: %w", err) } db := AutheliaUserDB{ Users: make(map[string]AutheliaUserEntry, len(syncUsers)), } for _, u := range syncUsers { db.Users[u.Username] = AutheliaUserEntry{ DisplayName: u.DisplayName, Password: u.Password, Email: u.Email, Groups: u.Groups, Disabled: u.Disabled, } } // Ensure the target directory exists dir := filepath.Dir(sw.usersDBPath) if err := os.MkdirAll(dir, 0755); err != nil { return fmt.Errorf("create authelia data directory: %w", err) } data, err := yaml.Marshal(&db) if err != nil { return fmt.Errorf("marshal users database: %w", err) } if err := os.WriteFile(sw.usersDBPath, data, 0644); err != nil { return fmt.Errorf("write users database: %w", err) } return nil } // Bootstrap imports existing Authelia users into the SQLite store. // This runs on first initialization to adopt existing users. func (sw *SyncWriter) Bootstrap() (int, error) { data, err := os.ReadFile(sw.usersDBPath) if err != nil { if os.IsNotExist(err) { return 0, nil // No existing file, nothing to bootstrap } return 0, fmt.Errorf("read authelia users database: %w", err) } var db AutheliaUserDB if err := yaml.Unmarshal(data, &db); err != nil { return 0, fmt.Errorf("parse authelia users database: %w", err) } imported := 0 for username, entry := range db.Users { existing, _ := sw.store.GetByUsername(username) if existing != nil { continue // Already exists, skip } // Build groups string groups := "" for i, g := range entry.Groups { if i > 0 { groups += "," } groups += g } // Determine role from groups role := "user" if containsGroup(groups, "admins") { role = "admin" } _, err := sw.store.GetDB().Exec(` INSERT INTO users (username, display_name, email, role, groups, password_hash, disabled, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) `, username, entry.DisplayName, entry.Email, role, groups, entry.Password, entry.Disabled) if err != nil { return imported, fmt.Errorf("import user %s: %w", username, err) } imported++ } return imported, nil } // FixRoles updates existing users' roles based on their groups. func (sw *SyncWriter) FixRoles() (int, error) { users, err := sw.store.List() if err != nil { return 0, err } fixed := 0 for _, u := range users { expectedRole := "user" if containsGroup(u.Groups, "admins") { expectedRole = "admin" } if u.Role != expectedRole { _, err := sw.store.GetDB().Exec("UPDATE users SET role = ? WHERE username = ?", expectedRole, u.Username) if err != nil { return fixed, err } fixed++ } } return fixed, nil }