NextWks/src/core/admin/sync.go

151 lines
3.7 KiB
Go

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
}