Files
CHFS-NG/chfs-ng/internal/auth/auth.go
T

224 lines
4.9 KiB
Go

package auth
import (
"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
Mask PermMask
Rules []PathRule
}
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.Password == pwd {
return a
}
if user == "" && pwd == "" {
return m.Guest
}
return nil
}
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) (string, error) {
return NewTokenWithTTL(username, 365*24*time.Hour)
}
func NewTokenWithTTL(username string, ttl time.Duration) (string, error) {
if ttl <= 0 {
return "", fmt.Errorf("invalid token ttl")
}
claims := jwtClaims{Acc: username, StandardClaims: jwt.StandardClaims{ExpiresAt: time.Now().Add(ttl).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
}