From 3e6ee6ddd3ec89284aa7b30552f443c241634ff4 Mon Sep 17 00:00:00 2001 From: veypi Date: Mon, 4 Nov 2024 15:41:05 +0800 Subject: [PATCH] feat: fs auth --- oa/api/access/access.go | 44 +++++++++++++++++++++++++ oa/api/app/resource.go | 31 ++++++++++++++++++ oa/api/token/token.go | 5 ++- oa/builtin/fs/app.go | 64 ++++++++++++++++++++++++++++++++---- oa/builtin/fs/fs.go | 4 --- oa/builtin/fs/user.go | 71 ++++++++++++++++++++++++++++++++++++++-- oa/builtin/init.go | 2 +- oa/cfg/db.go | 14 ++++---- oa/libs/auth/access.go | 23 +++++++++++-- oa/libs/webdav/webdav.go | 19 ++++++++--- oa/models/access.gen.go | 12 +++++++ oa/models/access.go | 2 +- oa/models/app.gen.go | 3 ++ oa/models/app.go | 2 +- 14 files changed, 265 insertions(+), 31 deletions(-) diff --git a/oa/api/access/access.go b/oa/api/access/access.go index bf1e50e..9835f0a 100644 --- a/oa/api/access/access.go +++ b/oa/api/access/access.go @@ -9,6 +9,9 @@ import ( func useAccess(r rest.Router) { r.Get("/", accessList) 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) { opts := &M.AccessList{} @@ -57,3 +60,44 @@ func accessPost(x *rest.X) (any, error) { 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 +} diff --git a/oa/api/app/resource.go b/oa/api/app/resource.go index a525442..38eaa16 100644 --- a/oa/api/app/resource.go +++ b/oa/api/app/resource.go @@ -11,6 +11,8 @@ func useResource(r rest.Router) { r.Delete("/", resourceDelete) r.Get("/", resourceList) r.Delete("/:resource_id", resourceDelete) + r.Get("/:resource_id", resourceGet) + r.Patch("/:resource_id", resourcePatch) } func resourcePost(x *rest.X) (any, error) { opts := &M.ResourcePost{} @@ -62,3 +64,32 @@ func resourceList(x *rest.X) (any, error) { 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 +} diff --git a/oa/api/token/token.go b/oa/api/token/token.go index ad05c1c..042949a 100644 --- a/oa/api/token/token.go +++ b/oa/api/token/token.go @@ -59,9 +59,12 @@ func tokenPost(x *rest.X) (any, error) { if opts.Refresh != nil { // for other app redirect refresh, err := auth.ParseJwt(*opts.Refresh) - if err != nil || refresh.ID == "" { + if err != nil { return nil, err } + if refresh.ID == "" { + return nil, errs.AuthInvalid + } err = cfg.DB().Where("id = ?", refresh.ID).First(data).Error if err != nil { return nil, err diff --git a/oa/builtin/fs/app.go b/oa/builtin/fs/app.go index 29e988d..e64bbb0 100644 --- a/oa/builtin/fs/app.go +++ b/oa/builtin/fs/app.go @@ -10,23 +10,75 @@ package fs import ( "net/http" "oa/cfg" + "oa/errs" + "oa/libs/auth" "oa/libs/webdav" "os" + "strings" "github.com/veypi/utils" "github.com/veypi/utils/logv" ) func NewAppFs(prefix string) func(http.ResponseWriter, *http.Request) { - apPath := utils.PathJoin(cfg.Config.FsPath, "app") - if !utils.FileExists(apPath) { - logv.AssertError(os.MkdirAll(apPath, 0744)) + if strings.HasSuffix(prefix, "/") { + prefix = prefix[:len(prefix)-1] + } + 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.GenSubPathFunc = func(r *http.Request) string { - return "" + client.GenSubPathFunc = func(r *http.Request) (string, error) { + // /: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 } diff --git a/oa/builtin/fs/fs.go b/oa/builtin/fs/fs.go index 1a84627..f495340 100644 --- a/oa/builtin/fs/fs.go +++ b/oa/builtin/fs/fs.go @@ -8,15 +8,11 @@ package fs import ( - "net/http" "oa/libs/webdav" ) func NewFs(dir_path, prefix string) *webdav.Handler { client := webdav.NewWebdav(dir_path) client.Prefix = prefix - client.GenSubPathFunc = func(r *http.Request) string { - return "" - } return client } diff --git a/oa/builtin/fs/user.go b/oa/builtin/fs/user.go index de8bb21..4393450 100644 --- a/oa/builtin/fs/user.go +++ b/oa/builtin/fs/user.go @@ -8,16 +8,24 @@ package fs import ( + "bufio" + "encoding/base64" "net/http" "oa/cfg" + "oa/errs" + "oa/libs/auth" "oa/libs/webdav" "os" + "strings" "github.com/veypi/utils" "github.com/veypi/utils/logv" ) 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") if !utils.FileExists(tmp) { logv.AssertError(os.MkdirAll(tmp, 0744)) @@ -25,8 +33,67 @@ func NewUserFs(prefix string) func(http.ResponseWriter, *http.Request) { client := webdav.NewWebdav(tmp) client.Prefix = prefix - client.GenSubPathFunc = func(r *http.Request) string { - return "" + client.Logger = func(r *http.Request, err error) { + } + 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 } + +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 +} diff --git a/oa/builtin/init.go b/oa/builtin/init.go index bd5cd53..09f3f10 100644 --- a/oa/builtin/init.go +++ b/oa/builtin/init.go @@ -21,7 +21,7 @@ import ( func Enable(app *rest.Application) { if cfg.Config.FsPath != "" { 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")) } tsPorxy := httputil.NewSingleHostReverseProxy(logv.AssertFuncErr(url.Parse("http://v.v:8428"))) diff --git a/oa/cfg/db.go b/oa/cfg/db.go index 4ce3886..e8357ff 100644 --- a/oa/cfg/db.go +++ b/oa/cfg/db.go @@ -8,7 +8,7 @@ package cfg import ( - "gorm.io/driver/postgres" + "gorm.io/driver/mysql" "gorm.io/gorm" "gorm.io/gorm/logger" ) @@ -39,14 +39,14 @@ func init() { func DB() *gorm.DB { if db == nil { var err error - // db, err = gorm.Open(mysql.New(mysql.Config{ - // DSN: Config.DSN, - // }), &gorm.Config{ - // Logger: logger.Default.LogMode(logger.Silent), - // }) - db, err = gorm.Open(postgres.Open(Config.DSN), &gorm.Config{ + db, err = gorm.Open(mysql.New(mysql.Config{ + DSN: Config.DSN, + }), &gorm.Config{ 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 { panic(err) } diff --git a/oa/libs/auth/access.go b/oa/libs/auth/access.go index 9508ec6..2bea64f 100644 --- a/oa/libs/auth/access.go +++ b/oa/libs/auth/access.go @@ -7,9 +7,13 @@ package auth -import "github.com/golang-jwt/jwt/v5" +import ( + "strings" -type AuthLevel uint + "github.com/golang-jwt/jwt/v5" +) + +type AuthLevel = int const ( DoNone = 0 @@ -32,7 +36,7 @@ func (a *Access) Check(target string, tid string, l AuthLevel) bool { return true } for _, line := range *a { - if line.Name == target && line.Level > l { + if line.Name == target && line.Level >= l { if line.TID == "" || line.TID == tid { return true } @@ -40,6 +44,19 @@ func (a *Access) Check(target string, tid string, l AuthLevel) bool { } 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 { UID string `json:"uid"` diff --git a/oa/libs/webdav/webdav.go b/oa/libs/webdav/webdav.go index ee52e32..715a0ac 100644 --- a/oa/libs/webdav/webdav.go +++ b/oa/libs/webdav/webdav.go @@ -23,8 +23,8 @@ func NewWebdav(p string) *Handler { FileSystem: Dir(p), LockSystem: NewMemLS(), EnableDirRender: true, - GenSubPathFunc: func(r *http.Request) string { - return "" + GenSubPathFunc: func(r *http.Request) (string, error) { + return "", nil }, } return fs @@ -34,7 +34,7 @@ type Handler struct { // Prefix is the URL path prefix to strip from WebDAV resource paths. Prefix string EnableDirRender bool - GenSubPathFunc func(*http.Request) string + GenSubPathFunc func(*http.Request) (string, error) // FileSystem is the virtual file system. FileSystem FileSystem // 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 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 } } @@ -95,7 +101,10 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if status != 0 { 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))) } } diff --git a/oa/models/access.gen.go b/oa/models/access.gen.go index 04da31a..78f86fb 100644 --- a/oa/models/access.gen.go +++ b/oa/models/access.gen.go @@ -19,3 +19,15 @@ type AccessPost struct { TID string `json:"tid" 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"` +} diff --git a/oa/models/access.go b/oa/models/access.go index 51da649..a35ce88 100644 --- a/oa/models/access.go +++ b/oa/models/access.go @@ -8,7 +8,7 @@ package models type Access struct { - BaseDate + BaseModel AppID string `json:"app_id" gorm:"index;type:varchar(32)" methods:"post,list" parse:"json"` App *App `json:"-" gorm:"foreignKey:AppID;references:ID"` diff --git a/oa/models/app.gen.go b/oa/models/app.gen.go index 27aa4a0..d4df61f 100644 --- a/oa/models/app.gen.go +++ b/oa/models/app.gen.go @@ -76,12 +76,15 @@ type ResourcePost struct { type ResourceDelete struct { Name string `json:"name" gorm:"primaryKey" parse:"json"` 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 { AppID string `json:"app_id" parse:"path"` + ID string `json:"id" gorm:"primaryKey;type:varchar(32)" parse:"path@resource_id"` } type ResourcePatch struct { AppID string `json:"app_id" parse:"path"` + ID string `json:"id" gorm:"primaryKey;type:varchar(32)" parse:"path@resource_id"` } diff --git a/oa/models/app.go b/oa/models/app.go index 6ca2d43..0e7dbdc 100644 --- a/oa/models/app.go +++ b/oa/models/app.go @@ -68,7 +68,7 @@ func (m *AppUser) AfterUpdate(tx *gorm.DB) error { } type Resource struct { - BaseDate + BaseModel AppID string `json:"app_id" methods:"get,list,post,patch,delete" parse:"path"` App *App `json:"-" gorm:"foreignKey:AppID;references:ID"` Name string `json:"name" gorm:"primaryKey" methods:"post,delete" parse:"json"`