package auth import ( "encoding/json" "fmt" "os" "path/filepath" "regexp" "strings" "sync" ) type UserStore struct { Path string Mu sync.Mutex } type userRecord struct { Username string `json:"username"` Password string `json:"passwordHash"` Home string `json:"home"` } var validUsername = regexp.MustCompile("^[A-Za-z0-9][A-Za-z0-9._-]{0,31}$") func ValidateUsername(username string) error { if !validUsername.MatchString(username) { return fmt.Errorf("invalid username") } return nil } func (u *UserStore) Load(m *Manager, homeRoot string) error { if u.Path == "" { return nil } data, err := os.ReadFile(u.Path) if os.IsNotExist(err) { return nil } if err != nil { return err } var records []userRecord if err := json.Unmarshal(data, &records); err != nil { return fmt.Errorf("invalid user store: %w", err) } for _, rec := range records { if err := ValidateUsername(rec.Username); err != nil || rec.Password == "" { continue } if _, exists := m.Accounts[rec.Username]; exists { continue } home := filepath.ToSlash(strings.Trim(strings.TrimSpace(rec.Home), "/")) if home == "" { home = filepath.ToSlash(filepath.Join(homeRoot, rec.Username)) } a := &Account{Username: rec.Username, PasswordHash: rec.Password, Mask: None, Home: home} a.Rules = append(a.Rules, PathRule{Dir: home, Mask: Read | Write | Delete}) m.Accounts[rec.Username] = a } return nil } func (u *UserStore) Save(m *Manager) error { if u.Path == "" { return nil } u.Mu.Lock() defer u.Mu.Unlock() records := make([]userRecord, 0) for _, a := range m.Accounts { if a.PasswordHash == "" || a.Home == "" || a.Admin { continue } records = append(records, userRecord{Username: a.Username, Password: a.PasswordHash, Home: a.Home}) } data, err := json.MarshalIndent(records, "", " ") if err != nil { return err } if dir := filepath.Dir(u.Path); dir != "." { if err := os.MkdirAll(dir, 0700); err != nil { return err } } tmp := u.Path + ".tmp" if err := os.WriteFile(tmp, data, 0600); err != nil { return err } return os.Rename(tmp, u.Path) } func (u *UserStore) Add(m *Manager, username, password, home string) (*Account, error) { if err := ValidateUsername(username); err != nil { return nil, err } if strings.TrimSpace(password) == "" { return nil, fmt.Errorf("password required") } if _, ok := m.Accounts[username]; ok { return nil, fmt.Errorf("user already exists") } home = strings.Trim(home, "/") a := &Account{Username: username, PasswordHash: PasswordHash(password), Mask: None, Home: home} a.Rules = append(a.Rules, PathRule{Dir: home, Mask: Read | Write | Delete}) m.Accounts[username] = a return a, u.Save(m) } func (u *UserStore) UpdatePassword(m *Manager, username, password string) error { if strings.TrimSpace(password) == "" { return fmt.Errorf("password required") } a, ok := m.Accounts[username] if !ok || a.PasswordHash == "" { return fmt.Errorf("managed user not found") } a.PasswordHash = PasswordHash(password) a.Password = "" return u.Save(m) } func (u *UserStore) Delete(m *Manager, username string) error { a, ok := m.Accounts[username] if !ok || a.Admin || a.PasswordHash == "" { return fmt.Errorf("managed user not found") } delete(m.Accounts, username) return u.Save(m) }