feat: add web ui and dynamic private users

This commit is contained in:
OpenClaw Agent
2026-09-22 01:14:15 +08:00
parent 035c3600bf
commit 77572e610b
7 changed files with 418 additions and 7 deletions
+11 -1
View File
@@ -1,6 +1,8 @@
package auth
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"strings"
"time"
@@ -43,8 +45,11 @@ type PathRule struct {
}
type Account struct {
Username, Password string
PasswordHash string
Mask PermMask
Rules []PathRule
Home string
Admin bool
}
type Manager struct {
Accounts map[string]*Account
@@ -80,7 +85,7 @@ func ParseRules(values []string) (*Manager, error) {
}
func (m *Manager) Authenticate(user, pwd string) *Account {
if a, ok := m.Accounts[user]; ok && a.Password == pwd {
if a, ok := m.Accounts[user]; ok && (a.Password == pwd || a.PasswordHash == PasswordHash(pwd)) {
return a
}
if user == "" && pwd == "" {
@@ -88,6 +93,11 @@ func (m *Manager) Authenticate(user, pwd string) *Account {
}
return nil
}
func PasswordHash(password string) string {
h := sha256.Sum256([]byte(password))
return hex.EncodeToString(h[:])
}
func (a *Account) MaskFor(path string) PermMask {
return a.maskFor(path, false)
}
+119
View File
@@ -0,0 +1,119 @@
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) 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)
}