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"` Enabled *bool `json:"enabled,omitempty"` Rules []userRule `json:"rules,omitempty"` } type userRule struct { Dir string `json:"dir"` Mask string `json:"mask"` Deny bool `json:"deny,omitempty"` ChildrenOnly bool `json:"childrenOnly,omitempty"` } 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 unsafeUserPath(p string) bool { p = strings.ReplaceAll(p, "\\", "/") return p == ".." || strings.HasPrefix(p, "../") || strings.Contains(p, "/../") || strings.HasPrefix(p, "/") } 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)) } enabled := true if rec.Enabled != nil { enabled = *rec.Enabled } a := &Account{Username: rec.Username, PasswordHash: rec.Password, Mask: None, Home: home, Enabled: enabled} if len(rec.Rules) == 0 { a.Rules = append(a.Rules, PathRule{Dir: home, Mask: Read | Write | Delete}) } else { for _, rr := range rec.Rules { dir := strings.Trim(strings.ReplaceAll(rr.Dir, "\\", "/"), "/") if dir == "" || unsafeUserPath(dir) { continue } a.Rules = append(a.Rules, PathRule{Dir: dir, Mask: ParseMask(rr.Mask), Deny: rr.Deny, ChildrenOnly: rr.ChildrenOnly}) } } 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 } rec := userRecord{Username: a.Username, Password: a.PasswordHash, Home: a.Home, Enabled: &a.Enabled} for _, rr := range a.Rules { rec.Rules = append(rec.Rules, userRule{Dir: rr.Dir, Mask: maskString(rr.Mask), Deny: rr.Deny, ChildrenOnly: rr.ChildrenOnly}) } records = append(records, rec) } 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, Enabled: true} a.Rules = append(a.Rules, PathRule{Dir: home, Mask: Read | Write | Delete}) m.Accounts[username] = a return a, u.Save(m) } func maskString(p PermMask) string { var b strings.Builder if p.HasRead() { b.WriteByte('r') } if p.HasWrite() { b.WriteByte('w') } if p.HasDelete() { b.WriteByte('d') } return b.String() } func (u *UserStore) Update(m *Manager, username, password string, enabled *bool, rules []PathRule) error { a, ok := m.Accounts[username] if !ok || a.PasswordHash == "" || a.Admin { return fmt.Errorf("managed user not found") } if strings.TrimSpace(password) != "" { a.PasswordHash = PasswordHash(password) a.Password = "" } if enabled != nil { a.Enabled = *enabled } if rules != nil { a.Rules = rules } return 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) }