231 lines
5.3 KiB
Go
231 lines
5.3 KiB
Go
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
|
|
}
|