refactor: flatten repository structure to root and archive research notes

This commit is contained in:
OpenClaw Agent
2026-09-22 03:41:27 +08:00
parent ae0becffec
commit 1a7658b1f7
40 changed files with 0 additions and 3 deletions
+230
View File
@@ -0,0 +1,230 @@
package auth
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"strings"
"time"
jwt "github.com/golang-jwt/jwt/v4"
)
type PermMask uint8
const (
None PermMask = 0
Read PermMask = 1 << iota
Write
Delete
)
func (p PermMask) HasRead() bool { return p&Read != 0 }
func (p PermMask) HasWrite() bool { return p&Write != 0 }
func (p PermMask) HasDelete() bool { return p&Delete != 0 }
func ParseMask(s string) PermMask {
var p PermMask
for _, r := range strings.ToLower(s) {
switch r {
case 'r':
p |= Read
case 'w':
p |= Read | Write
case 'd':
p |= Read | Write | Delete
}
}
return p
}
type PathRule struct {
Dir string
Mask PermMask
Deny bool
ChildrenOnly bool
}
type Account struct {
Username, Password string
PasswordHash string
Mask PermMask
Rules []PathRule
Home string
Admin bool
Enabled bool
}
type Manager struct {
Accounts map[string]*Account
Guest *Account
}
func ParseRules(values []string) (*Manager, error) {
m := &Manager{Accounts: map[string]*Account{}}
for _, value := range values {
for _, item := range strings.Split(value, "|") {
if item == "" {
continue
}
fields := strings.Split(item, ":")
if len(fields) < 3 || len(fields)%2 == 0 {
continue
}
a := &Account{Username: fields[0], Password: fields[1], Mask: ParseMask(fields[2])}
for i := 3; i+1 < len(fields); i += 2 {
a.Rules = append(a.Rules, PathRule{Dir: strings.Trim(fields[i], "/"), Mask: ParseMask(fields[i+1])})
}
if a.Username == "" {
m.Guest = a
} else {
m.Accounts[a.Username] = a
}
}
}
if m.Guest == nil {
m.Guest = &Account{Mask: Read | Write | Delete}
}
return m, nil
}
func (m *Manager) Authenticate(user, pwd string) *Account {
if a, ok := m.Accounts[user]; ok && (a.Enabled || a.PasswordHash == "") && (a.Password == pwd || a.PasswordHash == PasswordHash(pwd)) {
return a
}
if user == "" && pwd == "" {
return m.Guest
}
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)
}
// MaskForEntry returns the permission shown for a directory/file while its
// parent directory is being listed. CHFS section rules rule.r/rule.w/rule.d
// affect the contents of the named directory, but the directory entry itself
// keeps the inherited permission until it is opened.
func (a *Account) MaskForEntry(path string) PermMask {
return a.maskFor(path, true)
}
func (a *Account) maskFor(path string, entry bool) PermMask {
path = strings.Trim(path, "/")
best := -1
mask := a.Mask
for _, r := range a.Rules {
d := strings.Trim(r.Dir, "/")
if !matchPath(d, path) {
continue
}
if entry && r.ChildrenOnly && matchPathExact(d, path) {
continue
}
if len(d) >= best {
best = len(d)
if r.Deny {
mask = None
} else {
mask = r.Mask
}
}
}
return mask
}
func matchPath(pattern, path string) bool {
if pattern == "" {
return path == ""
}
pp := strings.Split(pattern, "/")
ps := strings.Split(path, "/")
if len(ps) < len(pp) {
return false
}
for i, p := range pp {
if !matchSegment(p, ps[i]) {
return false
}
}
return true
}
func matchSegment(pattern, value string) bool {
p := []rune(pattern)
v := []rune(value)
dp := make([][]bool, len(p)+1)
for i := range dp {
dp[i] = make([]bool, len(v)+1)
}
dp[0][0] = true
for i := 1; i <= len(p); i++ {
if p[i-1] == '*' {
dp[i][0] = dp[i-1][0]
}
}
for i := 1; i <= len(p); i++ {
for j := 1; j <= len(v); j++ {
switch p[i-1] {
case '*':
dp[i][j] = dp[i-1][j] || dp[i][j-1]
case '?':
dp[i][j] = dp[i-1][j-1]
default:
dp[i][j] = dp[i-1][j-1] && p[i-1] == v[j-1]
}
}
}
return dp[len(p)][len(v)]
}
func matchPathExact(pattern, path string) bool {
pp := strings.Split(strings.Trim(pattern, "/"), "/")
ps := strings.Split(strings.Trim(path, "/"), "/")
if len(pp) != len(ps) {
return false
}
return matchPath(pattern, path)
}
func MatchPath(pattern, path string) bool { return matchPath(pattern, path) }
const jwtSecret = "_pengyichen2015_"
type jwtClaims struct {
Acc string `json:"acc"`
jwt.StandardClaims
}
func NewToken(username string, _ int) (string, error) {
// CHFS v4 keeps the JWT itself valid for one year. session.timeout is
// retained as a compatibility configuration field, but it does not alter
// the JWT exp claim in the original implementation.
claims := jwtClaims{Acc: username, StandardClaims: jwt.StandardClaims{ExpiresAt: time.Now().Add(365 * 24 * time.Hour).Unix()}}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(jwtSecret))
}
func GetAccFromToken(raw string) (string, error) {
if raw == "" {
return "", fmt.Errorf("empty token")
}
var claims jwtClaims
token, err := jwt.ParseWithClaims(raw, &claims, func(t *jwt.Token) (interface{}, error) {
if t.Method != jwt.SigningMethodHS256 {
return nil, fmt.Errorf("unexpected signing method: %s", t.Method.Alg())
}
return []byte(jwtSecret), nil
})
if err != nil || token == nil || !token.Valid {
if err == nil {
err = fmt.Errorf("invalid token")
}
return "", err
}
if claims.Acc == "" {
return "", fmt.Errorf("token missing account")
}
return claims.Acc, nil
}
+117
View File
@@ -0,0 +1,117 @@
package auth
import "testing"
func TestMatchPath(t *testing.T) {
cases := []struct {
pattern, path string
want bool
}{
{"test", "test", true},
{"test", "test/sub", true},
{"test", "testing", false},
{"test*", "test2", true},
{"test*", "test/sub", true},
{"te?t", "test", true},
{"te?t", "teat", true},
{"te?t", "te", false},
{"test/sub", "test/sub/file", true},
{"test/sub", "test/other/file", false},
}
for _, tc := range cases {
if got := MatchPath(tc.pattern, tc.path); got != tc.want {
t.Errorf("MatchPath(%q,%q)=%v want %v", tc.pattern, tc.path, got, tc.want)
}
}
}
func TestMaskForLongestRule(t *testing.T) {
a := &Account{Mask: Read, Rules: []PathRule{
{Dir: "team", Mask: Write},
{Dir: "team/private", Mask: None},
}}
if got := a.MaskFor("team/file"); got != Write {
t.Fatalf("team/file mask=%d", got)
}
if got := a.MaskFor("team/private/file"); got != None {
t.Fatalf("team/private/file mask=%d", got)
}
if got := a.MaskFor("other/file"); got != Read {
t.Fatalf("other/file mask=%d", got)
}
}
func TestParseRulesGuestDefaultAndOverride(t *testing.T) {
m, err := ParseRules(nil)
if err != nil || m.Guest.Mask != (Read|Write|Delete) {
t.Fatalf("unexpected guest default: %#v err=%v", m.Guest, err)
}
m, err = ParseRules([]string{"::r|demo:pw:rw"})
if err != nil || m.Guest.Mask != Read || m.Accounts["demo"].Mask != (Read|Write) {
t.Fatalf("unexpected parsed masks: %#v %#v err=%v", m.Guest, m.Accounts["demo"], err)
}
}
func TestSectionRuleSemantics(t *testing.T) {
a := &Account{Mask: Delete, Rules: []PathRule{
{Dir: "private", Mask: None, Deny: true},
{Dir: "readonly", Mask: ParseMask("r"), ChildrenOnly: true},
{Dir: "writable", Mask: ParseMask("w"), ChildrenOnly: true},
{Dir: "full", Mask: ParseMask("d"), ChildrenOnly: true},
}}
if got := a.MaskFor("private"); got != None {
t.Fatalf("private=%v", got)
}
if got := a.MaskFor("private/file"); got != None {
t.Fatalf("private/file=%v", got)
}
if got := a.MaskForEntry("readonly"); got != Delete {
t.Fatalf("readonly entry=%v", got)
}
if got := a.MaskFor("readonly/file"); got != Read {
t.Fatalf("readonly/file=%v", got)
}
if got := a.MaskForEntry("writable"); got != Delete {
t.Fatalf("writable entry=%v", got)
}
if got := a.MaskFor("writable/file"); got != ParseMask("w") {
t.Fatalf("writable/file=%v", got)
}
if got := a.MaskForEntry("full"); got != Delete {
t.Fatalf("full entry=%v", got)
}
if got := a.MaskFor("full/file"); got != (Read | Write | Delete) {
t.Fatalf("full/file=%v", got)
}
if got := a.MaskForEntry("readonly"); got != Delete {
t.Fatalf("readonly entry mask=%v", got)
}
if got := a.MaskForEntry("private"); got != None {
t.Fatalf("private entry mask=%v", got)
}
}
func TestSectionRuleSamePathPriority(t *testing.T) {
a := &Account{Mask: Delete, Rules: []PathRule{
{Dir: "secret", Mask: None, Deny: true},
{Dir: "secret", Mask: ParseMask("r"), ChildrenOnly: true},
{Dir: "secret", Mask: ParseMask("w"), ChildrenOnly: true},
{Dir: "secret", Mask: ParseMask("d"), ChildrenOnly: true},
}}
if got := a.MaskFor("secret/file"); got != ParseMask("d") {
t.Fatalf("same-path rule priority=%v", got)
}
}
func TestWildcardSectionRules(t *testing.T) {
a := &Account{Mask: Read, Rules: []PathRule{{Dir: "project-*", Mask: Write, ChildrenOnly: true}}}
if got := a.MaskFor("project-one/file"); got != Write {
t.Fatalf("project-one/file=%v", got)
}
if got := a.MaskFor("project-two/file"); got != Write {
t.Fatalf("project-two/file=%v", got)
}
if got := a.MaskFor("project/file"); got != Read {
t.Fatalf("project/file=%v", got)
}
}
+79
View File
@@ -0,0 +1,79 @@
package auth
import "testing"
func TestPathRuleSemantics(t *testing.T) {
a := &Account{Mask: ParseMask("r")}
a.Rules = []PathRule{
{Dir: "private", Deny: true},
{Dir: "share", Mask: ParseMask("w"), ChildrenOnly: true},
{Dir: "drop", Mask: ParseMask("d"), ChildrenOnly: true},
{Dir: "pub*", Mask: ParseMask("r")},
{Dir: "team/?", Mask: ParseMask("w")},
}
cases := []struct{ path, want string }{
{"", "R"},
{"private", ""},
{"private/x.txt", ""},
{"share", "RW"},
{"share/x.txt", "RW"},
{"drop", "RWD"},
{"drop/x.txt", "RWD"},
{"public/x.txt", "R"},
{"public/sub/x.txt", "R"},
{"team/a", "RW"},
{"team/ab", "R"},
}
for _, tc := range cases {
if got := permStringForTest(a.MaskFor(tc.path)); got != tc.want {
t.Errorf("MaskFor(%q)=%q want %q", tc.path, got, tc.want)
}
}
entryCases := []struct{ path, want string }{
{"private", ""},
{"share", "R"},
{"drop", "R"},
{"public", "R"},
{"team/a", "RW"},
}
for _, tc := range entryCases {
if got := permStringForTest(a.MaskForEntry(tc.path)); got != tc.want {
t.Errorf("MaskForEntry(%q)=%q want %q", tc.path, got, tc.want)
}
}
}
func TestMoreSpecificRuleWins(t *testing.T) {
a := &Account{Mask: ParseMask("r")}
a.Rules = []PathRule{
{Dir: "share", Mask: ParseMask("w"), ChildrenOnly: true},
{Dir: "share/private", Deny: true},
{Dir: "share/public*", Mask: ParseMask("r")},
}
cases := []struct{ path, want string }{
{"share", "RW"},
{"share/file.txt", "RW"},
{"share/private", ""},
{"share/private/file.txt", ""},
{"share/public", "R"},
{"share/public/file.txt", "R"},
}
for _, tc := range cases {
if got := permStringForTest(a.MaskFor(tc.path)); got != tc.want {
t.Errorf("MaskFor(%q)=%q want %q", tc.path, got, tc.want)
}
}
}
func permStringForTest(p PermMask) string {
if p.HasDelete() {
return "RWD"
}
if p.HasWrite() {
return "RW"
}
if p.HasRead() {
return "R"
}
return ""
}
+207
View File
@@ -0,0 +1,207 @@
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 managed user's home is always their private working area. Keep the
// base RWD rule even when an administrator replaces the extra rules.
// Additional rules can grant or deny access elsewhere.
next := make([]PathRule, 0, len(rules)+1)
next = append(next, PathRule{Dir: a.Home, Mask: Read | Write | Delete})
for _, rule := range rules {
if strings.Trim(rule.Dir, "/") == strings.Trim(a.Home, "/") {
continue
}
next = append(next, rule)
}
a.Rules = next
}
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)
}