feat: fs auth

v3
veypi 3 weeks ago
parent 38582d2148
commit 3e6ee6ddd3

@ -9,6 +9,9 @@ import (
func useAccess(r rest.Router) { func useAccess(r rest.Router) {
r.Get("/", accessList) r.Get("/", accessList)
r.Post("/", accessPost) r.Post("/", accessPost)
r.Get("/:access_id", accessGet)
r.Patch("/:access_id", accessPatch)
r.Delete("/:access_id", accessDelete)
} }
func accessList(x *rest.X) (any, error) { func accessList(x *rest.X) (any, error) {
opts := &M.AccessList{} opts := &M.AccessList{}
@ -57,3 +60,44 @@ func accessPost(x *rest.X) (any, error) {
return data, err return data, err
} }
func accessGet(x *rest.X) (any, error) {
opts := &M.AccessGet{}
err := x.Parse(opts)
if err != nil {
return nil, err
}
data := &M.Access{}
err = cfg.DB().Where("id = ?", opts.ID).First(data).Error
return data, err
}
func accessPatch(x *rest.X) (any, error) {
opts := &M.AccessPatch{}
err := x.Parse(opts)
if err != nil {
return nil, err
}
data := &M.Access{}
err = cfg.DB().Where("id = ?", opts.ID).First(data).Error
if err != nil {
return nil, err
}
optsMap := make(map[string]interface{})
err = cfg.DB().Model(data).Updates(optsMap).Error
return data, err
}
func accessDelete(x *rest.X) (any, error) {
opts := &M.AccessDelete{}
err := x.Parse(opts)
if err != nil {
return nil, err
}
data := &M.Access{}
err = cfg.DB().Where("id = ?", opts.ID).Delete(data).Error
return data, err
}

@ -11,6 +11,8 @@ func useResource(r rest.Router) {
r.Delete("/", resourceDelete) r.Delete("/", resourceDelete)
r.Get("/", resourceList) r.Get("/", resourceList)
r.Delete("/:resource_id", resourceDelete) r.Delete("/:resource_id", resourceDelete)
r.Get("/:resource_id", resourceGet)
r.Patch("/:resource_id", resourcePatch)
} }
func resourcePost(x *rest.X) (any, error) { func resourcePost(x *rest.X) (any, error) {
opts := &M.ResourcePost{} opts := &M.ResourcePost{}
@ -62,3 +64,32 @@ func resourceList(x *rest.X) (any, error) {
return data, err return data, err
} }
func resourceGet(x *rest.X) (any, error) {
opts := &M.ResourceGet{}
err := x.Parse(opts)
if err != nil {
return nil, err
}
data := &M.Resource{}
err = cfg.DB().Where("id = ?", opts.ID).First(data).Error
return data, err
}
func resourcePatch(x *rest.X) (any, error) {
opts := &M.ResourcePatch{}
err := x.Parse(opts)
if err != nil {
return nil, err
}
data := &M.Resource{}
err = cfg.DB().Where("id = ?", opts.ID).First(data).Error
if err != nil {
return nil, err
}
optsMap := make(map[string]interface{})
err = cfg.DB().Model(data).Updates(optsMap).Error
return data, err
}

@ -59,9 +59,12 @@ func tokenPost(x *rest.X) (any, error) {
if opts.Refresh != nil { if opts.Refresh != nil {
// for other app redirect // for other app redirect
refresh, err := auth.ParseJwt(*opts.Refresh) refresh, err := auth.ParseJwt(*opts.Refresh)
if err != nil || refresh.ID == "" { if err != nil {
return nil, err return nil, err
} }
if refresh.ID == "" {
return nil, errs.AuthInvalid
}
err = cfg.DB().Where("id = ?", refresh.ID).First(data).Error err = cfg.DB().Where("id = ?", refresh.ID).First(data).Error
if err != nil { if err != nil {
return nil, err return nil, err

@ -10,23 +10,75 @@ package fs
import ( import (
"net/http" "net/http"
"oa/cfg" "oa/cfg"
"oa/errs"
"oa/libs/auth"
"oa/libs/webdav" "oa/libs/webdav"
"os" "os"
"strings"
"github.com/veypi/utils" "github.com/veypi/utils"
"github.com/veypi/utils/logv" "github.com/veypi/utils/logv"
) )
func NewAppFs(prefix string) func(http.ResponseWriter, *http.Request) { func NewAppFs(prefix string) func(http.ResponseWriter, *http.Request) {
apPath := utils.PathJoin(cfg.Config.FsPath, "app") if strings.HasSuffix(prefix, "/") {
if !utils.FileExists(apPath) { prefix = prefix[:len(prefix)-1]
logv.AssertError(os.MkdirAll(apPath, 0744)) }
tmp := utils.PathJoin(cfg.Config.FsPath, "app")
if !utils.FileExists(tmp) {
logv.AssertError(os.MkdirAll(tmp, 0744))
} }
client := webdav.NewWebdav(apPath) client := webdav.NewWebdav(tmp)
client.Prefix = prefix client.Prefix = prefix
client.GenSubPathFunc = func(r *http.Request) string { client.GenSubPathFunc = func(r *http.Request) (string, error) {
return "" // /:aid/*p
dir := strings.TrimPrefix(r.URL.Path, prefix)
dirs := strings.Split(dir[1:], "/")
aid := ""
root := "/"
if len(dirs) > 0 {
aid = dirs[0]
}
if len(dirs) > 1 {
root = "/" + strings.Join(dirs[1:], "/")
}
if root == "/" {
if !utils.FileExists(tmp + "/" + aid) {
os.MkdirAll(tmp+"/"+aid, 0744)
}
}
logv.Warn().Msgf("aid: %v, root: %v", aid, root)
if aid == "" {
return "", errs.AuthNoPerm
}
if root == "/pub" || strings.HasPrefix(root, "/pub/") {
switch r.Method {
case "OPTIONS", "GET", "HEAD", "POST":
return dir, nil
default:
}
}
// appfs权限等于app权限
// TODO: 存在空文件覆盖重要文件的风险
handlerLevle := auth.Do
switch r.Method {
case "PUT", "MKCOL", "COPY", "MOVE":
handlerLevle = auth.DoCreate
case "DELETE":
handlerLevle = auth.DoDelete
default:
handlerLevle = auth.DoRead
}
payload, err := getToken(r)
if err != nil {
return "", err
}
if payload.Access.CheckPrefix("app", aid, handlerLevle) {
return "/" + payload.UID + dir, nil
}
return "", errs.AuthNoPerm
} }
return client.ServeHTTP return client.ServeHTTP
} }

@ -8,15 +8,11 @@
package fs package fs
import ( import (
"net/http"
"oa/libs/webdav" "oa/libs/webdav"
) )
func NewFs(dir_path, prefix string) *webdav.Handler { func NewFs(dir_path, prefix string) *webdav.Handler {
client := webdav.NewWebdav(dir_path) client := webdav.NewWebdav(dir_path)
client.Prefix = prefix client.Prefix = prefix
client.GenSubPathFunc = func(r *http.Request) string {
return ""
}
return client return client
} }

@ -8,16 +8,24 @@
package fs package fs
import ( import (
"bufio"
"encoding/base64"
"net/http" "net/http"
"oa/cfg" "oa/cfg"
"oa/errs"
"oa/libs/auth"
"oa/libs/webdav" "oa/libs/webdav"
"os" "os"
"strings"
"github.com/veypi/utils" "github.com/veypi/utils"
"github.com/veypi/utils/logv" "github.com/veypi/utils/logv"
) )
func NewUserFs(prefix string) func(http.ResponseWriter, *http.Request) { func NewUserFs(prefix string) func(http.ResponseWriter, *http.Request) {
if strings.HasSuffix(prefix, "/") {
prefix = prefix[:len(prefix)-1]
}
tmp := utils.PathJoin(cfg.Config.FsPath, "u") tmp := utils.PathJoin(cfg.Config.FsPath, "u")
if !utils.FileExists(tmp) { if !utils.FileExists(tmp) {
logv.AssertError(os.MkdirAll(tmp, 0744)) logv.AssertError(os.MkdirAll(tmp, 0744))
@ -25,8 +33,67 @@ func NewUserFs(prefix string) func(http.ResponseWriter, *http.Request) {
client := webdav.NewWebdav(tmp) client := webdav.NewWebdav(tmp)
client.Prefix = prefix client.Prefix = prefix
client.GenSubPathFunc = func(r *http.Request) string { client.Logger = func(r *http.Request, err error) {
return "" }
client.GenSubPathFunc = func(r *http.Request) (string, error) {
dir := strings.TrimPrefix(r.URL.Path, prefix)
logv.Warn().Msg(dir)
payload, err := getToken(r)
if err != nil {
return "", err
}
if !strings.HasPrefix(dir, "/") {
dir = "/" + dir
}
if payload.Access.CheckPrefix("fs", dir, auth.Do) {
if dir == "/" {
if !utils.FileExists(tmp + "/" + payload.UID) {
os.MkdirAll(tmp+"/"+payload.UID, 0744)
}
}
return "/" + payload.UID + dir, nil
}
return "", errs.AuthNoPerm
} }
return client.ServeHTTP return client.ServeHTTP
} }
func getToken(r *http.Request) (*auth.Claims, error) {
authHeader := r.Header.Get("Authorization")
token := ""
if authHeader != "" {
if strings.HasPrefix(authHeader, "Basic ") {
decodedAuth, err := base64.StdEncoding.DecodeString(authHeader[6:])
if err != nil {
return nil, errs.AuthInvalid
}
// 通常认证信息格式为 username:password
credentials := string(decodedAuth)
reader := bufio.NewReader(strings.NewReader(credentials))
username, _ := reader.ReadString(':')
password := credentials[len(username):]
username = strings.TrimSuffix(username, "\n")
username = strings.TrimSuffix(username, ":")
token = strings.TrimPrefix(password, "\n")
logv.Warn().Msgf("username: %s, password: %s", username, token)
}
if strings.HasPrefix(authHeader, "Bearer ") {
token = strings.TrimPrefix(authHeader, "Bearer ")
}
} else {
acookie, err := r.Cookie("fstoken")
if err == nil {
token = acookie.Value
}
}
if token == "" {
return nil, errs.AuthNotFound
}
payload, err := auth.ParseJwt(token)
if err != nil {
return nil, err
}
return payload, nil
}

@ -21,7 +21,7 @@ import (
func Enable(app *rest.Application) { func Enable(app *rest.Application) {
if cfg.Config.FsPath != "" { if cfg.Config.FsPath != "" {
r := app.Router().SubRouter("fs") r := app.Router().SubRouter("fs")
r.Any("/a/*", fs.NewAppFs("/fs/a")) r.Any("/a/:aid/*p", fs.NewAppFs("/fs/a"))
r.Any("/u/*", fs.NewUserFs("/fs/u")) r.Any("/u/*", fs.NewUserFs("/fs/u"))
} }
tsPorxy := httputil.NewSingleHostReverseProxy(logv.AssertFuncErr(url.Parse("http://v.v:8428"))) tsPorxy := httputil.NewSingleHostReverseProxy(logv.AssertFuncErr(url.Parse("http://v.v:8428")))

@ -8,7 +8,7 @@
package cfg package cfg
import ( import (
"gorm.io/driver/postgres" "gorm.io/driver/mysql"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/logger" "gorm.io/gorm/logger"
) )
@ -39,14 +39,14 @@ func init() {
func DB() *gorm.DB { func DB() *gorm.DB {
if db == nil { if db == nil {
var err error var err error
// db, err = gorm.Open(mysql.New(mysql.Config{ db, err = gorm.Open(mysql.New(mysql.Config{
// DSN: Config.DSN, DSN: Config.DSN,
// }), &gorm.Config{ }), &gorm.Config{
// Logger: logger.Default.LogMode(logger.Silent),
// })
db, err = gorm.Open(postgres.Open(Config.DSN), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent), Logger: logger.Default.LogMode(logger.Silent),
}) })
// db, err = gorm.Open(postgres.Open(Config.DSN), &gorm.Config{
// Logger: logger.Default.LogMode(logger.Silent),
// })
if err != nil { if err != nil {
panic(err) panic(err)
} }

@ -7,9 +7,13 @@
package auth package auth
import "github.com/golang-jwt/jwt/v5" import (
"strings"
type AuthLevel uint "github.com/golang-jwt/jwt/v5"
)
type AuthLevel = int
const ( const (
DoNone = 0 DoNone = 0
@ -32,7 +36,7 @@ func (a *Access) Check(target string, tid string, l AuthLevel) bool {
return true return true
} }
for _, line := range *a { for _, line := range *a {
if line.Name == target && line.Level > l { if line.Name == target && line.Level >= l {
if line.TID == "" || line.TID == tid { if line.TID == "" || line.TID == tid {
return true return true
} }
@ -40,6 +44,19 @@ func (a *Access) Check(target string, tid string, l AuthLevel) bool {
} }
return false return false
} }
func (a *Access) CheckPrefix(target string, tid string, l AuthLevel) bool {
if l == DoNone {
return true
}
for _, line := range *a {
if line.Name == target && line.Level >= l {
if line.TID == "" || strings.HasPrefix(tid, line.TID) {
return true
}
}
}
return false
}
type Claims struct { type Claims struct {
UID string `json:"uid"` UID string `json:"uid"`

@ -23,8 +23,8 @@ func NewWebdav(p string) *Handler {
FileSystem: Dir(p), FileSystem: Dir(p),
LockSystem: NewMemLS(), LockSystem: NewMemLS(),
EnableDirRender: true, EnableDirRender: true,
GenSubPathFunc: func(r *http.Request) string { GenSubPathFunc: func(r *http.Request) (string, error) {
return "" return "", nil
}, },
} }
return fs return fs
@ -34,7 +34,7 @@ type Handler struct {
// Prefix is the URL path prefix to strip from WebDAV resource paths. // Prefix is the URL path prefix to strip from WebDAV resource paths.
Prefix string Prefix string
EnableDirRender bool EnableDirRender bool
GenSubPathFunc func(*http.Request) string GenSubPathFunc func(*http.Request) (string, error)
// FileSystem is the virtual file system. // FileSystem is the virtual file system.
FileSystem FileSystem FileSystem FileSystem
// LockSystem is the lock management system. // LockSystem is the lock management system.
@ -55,7 +55,13 @@ func (h *Handler) stripPrefix(p string, r *http.Request) (np string, ns int, ne
} }
} }
if h.GenSubPathFunc != nil { if h.GenSubPathFunc != nil {
if tmp := h.GenSubPathFunc(r); tmp != "" && tmp != "/" { tmp, e := h.GenSubPathFunc(r)
if e != nil {
ne = e
ns = http.StatusUnauthorized
return
}
if tmp != "" && tmp != "/" {
np = tmp + np np = tmp + np
} }
} }
@ -95,7 +101,10 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if status != 0 { if status != 0 {
w.WriteHeader(status) w.WriteHeader(status)
if status != http.StatusNoContent { if status == http.StatusUnauthorized {
w.Header().Set("WWW-Authenticate", "Basic realm=\"file\"")
w.Write([]byte("Unauthorized"))
} else if status != http.StatusNoContent {
w.Write([]byte(StatusText(status))) w.Write([]byte(StatusText(status)))
} }
} }

@ -19,3 +19,15 @@ type AccessPost struct {
TID string `json:"tid" parse:"json"` TID string `json:"tid" parse:"json"`
Level uint `json:"level" parse:"json"` Level uint `json:"level" parse:"json"`
} }
type AccessGet struct {
ID string `json:"id" gorm:"primaryKey;type:varchar(32)" parse:"path@access_id"`
}
type AccessPatch struct {
ID string `json:"id" gorm:"primaryKey;type:varchar(32)" parse:"path@access_id"`
}
type AccessDelete struct {
ID string `json:"id" gorm:"primaryKey;type:varchar(32)" parse:"path@access_id"`
}

@ -8,7 +8,7 @@
package models package models
type Access struct { type Access struct {
BaseDate BaseModel
AppID string `json:"app_id" gorm:"index;type:varchar(32)" methods:"post,list" parse:"json"` AppID string `json:"app_id" gorm:"index;type:varchar(32)" methods:"post,list" parse:"json"`
App *App `json:"-" gorm:"foreignKey:AppID;references:ID"` App *App `json:"-" gorm:"foreignKey:AppID;references:ID"`

@ -76,12 +76,15 @@ type ResourcePost struct {
type ResourceDelete struct { type ResourceDelete struct {
Name string `json:"name" gorm:"primaryKey" parse:"json"` Name string `json:"name" gorm:"primaryKey" parse:"json"`
AppID string `json:"app_id" gorm:"primaryKey;type:varchar(32)" parse:"path"` AppID string `json:"app_id" gorm:"primaryKey;type:varchar(32)" parse:"path"`
ID string `json:"id" gorm:"primaryKey;type:varchar(32)" parse:"path@resource_id"`
} }
type ResourceGet struct { type ResourceGet struct {
AppID string `json:"app_id" parse:"path"` AppID string `json:"app_id" parse:"path"`
ID string `json:"id" gorm:"primaryKey;type:varchar(32)" parse:"path@resource_id"`
} }
type ResourcePatch struct { type ResourcePatch struct {
AppID string `json:"app_id" parse:"path"` AppID string `json:"app_id" parse:"path"`
ID string `json:"id" gorm:"primaryKey;type:varchar(32)" parse:"path@resource_id"`
} }

@ -68,7 +68,7 @@ func (m *AppUser) AfterUpdate(tx *gorm.DB) error {
} }
type Resource struct { type Resource struct {
BaseDate BaseModel
AppID string `json:"app_id" methods:"get,list,post,patch,delete" parse:"path"` AppID string `json:"app_id" methods:"get,list,post,patch,delete" parse:"path"`
App *App `json:"-" gorm:"foreignKey:AppID;references:ID"` App *App `json:"-" gorm:"foreignKey:AppID;references:ID"`
Name string `json:"name" gorm:"primaryKey" methods:"post,delete" parse:"json"` Name string `json:"name" gorm:"primaryKey" methods:"post,delete" parse:"json"`

Loading…
Cancel
Save