feat(multi-home): enable separated home dir for each user (#64)

* feat(files): make files service supporting home dir

* fix(files): add path access control and avoid redirecting path in the backend

* feat(files): add ListHome API

* fix(server): fix access control issues

* feat(client/web): support multi-home

* feat(server): cleanup

* fix(server): failed to init admin folder
This commit is contained in:
Hexxa 2021-07-24 21:05:36 -05:00 committed by GitHub
parent 9748d0cab4
commit 81da97650b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 527 additions and 212 deletions

View file

@ -1,15 +1,16 @@
package fileshdr
import (
"crypto/sha1"
"encoding/base64"
"errors"
"fmt"
"github.com/ihexxa/quickshare/src/userstore"
"io"
"net/http"
"os"
"path"
"path/filepath"
"strings"
"time"
"github.com/gin-gonic/gin"
@ -18,14 +19,9 @@ import (
"github.com/ihexxa/quickshare/src/depidx"
q "github.com/ihexxa/quickshare/src/handlers"
"github.com/ihexxa/quickshare/src/handlers/singleuserhdr"
)
var (
// dirs
UploadDir = "uploadings"
FsDir = "files"
// queries
FilePathQuery = "fp"
ListDirQuery = "dp"
@ -45,20 +41,11 @@ type FileHandlers struct {
}
func NewFileHandlers(cfg gocfg.ICfg, deps *depidx.Deps) (*FileHandlers, error) {
var err error
if err = deps.FS().MkdirAll(UploadDir); err != nil {
return nil, err
}
if err = deps.FS().MkdirAll(FsDir); err != nil {
return nil, err
}
return &FileHandlers{
cfg: cfg,
deps: deps,
uploadMgr: NewUploadMgr(deps.KV()),
}, err
}, nil
}
type AutoLocker struct {
@ -95,12 +82,24 @@ func (lk *AutoLocker) Exec(handler func()) {
lk.c.JSON(q.ErrResp(lk.c, 500, errors.New("fail to lock the file")))
return
}
locked = true
locked = true
handler()
}
func (h *FileHandlers) canAccess(userID, role, path string) bool {
if role == userstore.AdminRole {
return true
}
// the file path must start with userID: <userID>/...
parts := strings.Split(path, "/")
if len(parts) < 1 {
return false
}
return parts[0] == userID
}
type CreateReq struct {
Path string `json:"path"`
FileSize int64 `json:"fileSize"`
@ -112,10 +111,15 @@ func (h *FileHandlers) Create(c *gin.Context) {
c.JSON(q.ErrResp(c, 500, err))
return
}
userName := c.MustGet(singleuserhdr.UserParam).(string)
role := c.MustGet(q.RoleParam).(string)
userID := c.MustGet(q.UserIDParam).(string)
if !h.canAccess(userID, role, req.Path) {
c.JSON(q.ErrResp(c, 403, q.ErrAccessDenied))
return
}
tmpFilePath := h.getTmpPath(req.Path)
locker := h.NewAutoLocker(c, lockName(userName, tmpFilePath))
tmpFilePath := q.GetTmpPath(userID, req.Path)
locker := h.NewAutoLocker(c, lockName(tmpFilePath))
locker.Exec(func() {
err := h.deps.FS().Create(tmpFilePath)
if err != nil {
@ -126,14 +130,14 @@ func (h *FileHandlers) Create(c *gin.Context) {
}
return
}
err = h.uploadMgr.AddInfo(userName, req.Path, tmpFilePath, req.FileSize)
err = h.uploadMgr.AddInfo(userID, req.Path, tmpFilePath, req.FileSize)
if err != nil {
c.JSON(q.ErrResp(c, 500, err))
return
}
fileDir := h.FsPath(filepath.Dir(req.Path))
err = h.deps.FS().MkdirAll(fileDir)
// fileDir := q.FsPath(userID, filepath.Dir(req.Path))
err = h.deps.FS().MkdirAll(filepath.Dir(req.Path))
if err != nil {
c.JSON(q.ErrResp(c, 500, err))
return
@ -149,8 +153,14 @@ func (h *FileHandlers) Delete(c *gin.Context) {
c.JSON(q.ErrResp(c, 400, errors.New("invalid file path")))
return
}
role := c.MustGet(q.RoleParam).(string)
userID := c.MustGet(q.UserIDParam).(string)
if !h.canAccess(userID, role, filePath) {
c.JSON(q.ErrResp(c, 403, q.ErrAccessDenied))
return
}
filePath = h.FsPath(filePath)
// filePath = q.FsPath(userID, filePath)
err := h.deps.FS().Remove(filePath)
if err != nil {
c.JSON(q.ErrResp(c, 500, err))
@ -173,8 +183,14 @@ func (h *FileHandlers) Metadata(c *gin.Context) {
c.JSON(q.ErrResp(c, 400, errors.New("invalid file path")))
return
}
role := c.MustGet(q.RoleParam).(string)
userID := c.MustGet(q.UserIDParam).(string)
if !h.canAccess(userID, role, filePath) {
c.JSON(q.ErrResp(c, 403, q.ErrAccessDenied))
return
}
filePath = h.FsPath(filePath)
// filePath = q.FsPath(userID, filePath)
info, err := h.deps.FS().Stat(filePath)
if err != nil {
c.JSON(q.ErrResp(c, 500, err))
@ -199,9 +215,15 @@ func (h *FileHandlers) Mkdir(c *gin.Context) {
c.JSON(q.ErrResp(c, 400, err))
return
}
role := c.MustGet(q.RoleParam).(string)
userID := c.MustGet(q.UserIDParam).(string)
if !h.canAccess(userID, role, req.Path) {
c.JSON(q.ErrResp(c, 403, q.ErrAccessDenied))
return
}
dirPath := h.FsPath(req.Path)
err := h.deps.FS().MkdirAll(dirPath)
// dirPath := q.FsPath(userID, req.Path)
err := h.deps.FS().MkdirAll(req.Path)
if err != nil {
c.JSON(q.ErrResp(c, 500, err))
return
@ -221,15 +243,21 @@ func (h *FileHandlers) Move(c *gin.Context) {
c.JSON(q.ErrResp(c, 400, err))
return
}
role := c.MustGet(q.RoleParam).(string)
userID := c.MustGet(q.UserIDParam).(string)
if !h.canAccess(userID, role, req.OldPath) || !h.canAccess(userID, role, req.NewPath) {
c.JSON(q.ErrResp(c, 403, q.ErrAccessDenied))
return
}
oldPath := h.FsPath(req.OldPath)
newPath := h.FsPath(req.NewPath)
_, err := h.deps.FS().Stat(oldPath)
// oldPath := q.FsPath(userID, req.OldPath)
// newPath := q.FsPath(userID, req.NewPath)
_, err := h.deps.FS().Stat(req.OldPath)
if err != nil {
c.JSON(q.ErrResp(c, 500, err))
return
}
_, err = h.deps.FS().Stat(newPath)
_, err = h.deps.FS().Stat(req.NewPath)
if err != nil && !os.IsNotExist(err) {
c.JSON(q.ErrResp(c, 500, err))
return
@ -239,7 +267,7 @@ func (h *FileHandlers) Move(c *gin.Context) {
return
}
err = h.deps.FS().Rename(oldPath, newPath)
err = h.deps.FS().Rename(req.OldPath, req.NewPath)
if err != nil {
c.JSON(q.ErrResp(c, 500, err))
return
@ -260,14 +288,19 @@ func (h *FileHandlers) UploadChunk(c *gin.Context) {
c.JSON(q.ErrResp(c, 500, err))
return
}
userName := c.MustGet(singleuserhdr.UserParam).(string)
role := c.MustGet(q.RoleParam).(string)
userID := c.MustGet(q.UserIDParam).(string)
if !h.canAccess(userID, role, req.Path) {
c.JSON(q.ErrResp(c, 403, q.ErrAccessDenied))
return
}
tmpFilePath := h.getTmpPath(req.Path)
locker := h.NewAutoLocker(c, lockName(userName, tmpFilePath))
tmpFilePath := q.GetTmpPath(userID, req.Path)
locker := h.NewAutoLocker(c, lockName(tmpFilePath))
locker.Exec(func() {
var err error
_, fileSize, uploaded, err := h.uploadMgr.GetInfo(userName, tmpFilePath)
_, fileSize, uploaded, err := h.uploadMgr.GetInfo(userID, tmpFilePath)
if err != nil {
c.JSON(q.ErrResp(c, 500, err))
return
@ -288,7 +321,7 @@ func (h *FileHandlers) UploadChunk(c *gin.Context) {
return
}
err = h.uploadMgr.SetInfo(userName, tmpFilePath, req.Offset+int64(wrote))
err = h.uploadMgr.SetInfo(userID, tmpFilePath, req.Offset+int64(wrote))
if err != nil {
c.JSON(q.ErrResp(c, 500, err))
return
@ -296,7 +329,7 @@ func (h *FileHandlers) UploadChunk(c *gin.Context) {
// move the file from uploading dir to uploaded dir
if uploaded+int64(wrote) == fileSize {
fsFilePath, err := h.getFSFilePath(req.Path)
fsFilePath, err := h.getFSFilePath(userID, req.Path)
if err != nil {
c.JSON(q.ErrResp(c, 500, err))
return
@ -307,7 +340,7 @@ func (h *FileHandlers) UploadChunk(c *gin.Context) {
c.JSON(q.ErrResp(c, 500, fmt.Errorf("%s error: %w", req.Path, err)))
return
}
err = h.uploadMgr.DelInfo(userName, tmpFilePath)
err = h.uploadMgr.DelInfo(userID, tmpFilePath)
if err != nil {
c.JSON(q.ErrResp(c, 500, err))
return
@ -323,8 +356,8 @@ func (h *FileHandlers) UploadChunk(c *gin.Context) {
})
}
func (h *FileHandlers) getFSFilePath(reqPath string) (string, error) {
fsFilePath := h.FsPath(reqPath)
func (h *FileHandlers) getFSFilePath(userID, fsFilePath string) (string, error) {
// fsFilePath := q.FsPath(userID, reqPath)
_, err := h.deps.FS().Stat(fsFilePath)
if err != nil {
if os.IsNotExist(err) {
@ -367,12 +400,17 @@ func (h *FileHandlers) UploadStatus(c *gin.Context) {
if filePath == "" {
c.JSON(q.ErrResp(c, 400, errors.New("invalid file name")))
}
userName := c.MustGet(singleuserhdr.UserParam).(string)
role := c.MustGet(q.RoleParam).(string)
userID := c.MustGet(q.UserIDParam).(string)
if !h.canAccess(userID, role, filePath) {
c.JSON(q.ErrResp(c, 403, q.ErrAccessDenied))
return
}
tmpFilePath := h.getTmpPath(filePath)
locker := h.NewAutoLocker(c, lockName(userName, tmpFilePath))
tmpFilePath := q.GetTmpPath(userID, filePath)
locker := h.NewAutoLocker(c, lockName(tmpFilePath))
locker.Exec(func() {
_, fileSize, uploaded, err := h.uploadMgr.GetInfo(userName, tmpFilePath)
_, fileSize, uploaded, err := h.uploadMgr.GetInfo(userID, tmpFilePath)
if err != nil {
if os.IsNotExist(err) {
c.JSON(q.ErrResp(c, 404, err))
@ -400,9 +438,16 @@ func (h *FileHandlers) Download(c *gin.Context) {
c.JSON(q.ErrResp(c, 400, errors.New("invalid file name")))
return
}
role := c.MustGet(q.RoleParam).(string)
userID := c.MustGet(q.UserIDParam).(string)
if !h.canAccess(userID, role, filePath) {
c.JSON(q.ErrResp(c, 403, q.ErrAccessDenied))
return
}
// TODO: when sharing is introduced, move following logics to a separeted method
// concurrently file accessing is managed by os
filePath = h.FsPath(filePath)
// filePath = q.FsPath(userID, filePath)
info, err := h.deps.FS().Stat(filePath)
if err != nil {
if os.IsNotExist(err) {
@ -446,7 +491,7 @@ func (h *FileHandlers) Download(c *gin.Context) {
// respond to range requests
parts, err := multipart.RangeToParts(rangeVal, contentType, fmt.Sprintf("%d", info.Size()))
if err != nil {
c.JSON(q.ErrResp(c, 401, err))
c.JSON(q.ErrResp(c, 400, err))
return
}
@ -463,17 +508,24 @@ func (h *FileHandlers) Download(c *gin.Context) {
}
type ListResp struct {
Cwd string `json:"cwd"`
Metadatas []*MetadataResp `json:"metadatas"`
}
func (h *FileHandlers) List(c *gin.Context) {
dirPath := c.Query(ListDirQuery)
if dirPath == "" {
c.JSON(q.ErrResp(c, 402, errors.New("incorrect path name")))
c.JSON(q.ErrResp(c, 400, errors.New("incorrect path name")))
return
}
role := c.MustGet(q.RoleParam).(string)
userID := c.MustGet(q.UserIDParam).(string)
if !h.canAccess(userID, role, dirPath) {
c.JSON(q.ErrResp(c, 403, q.ErrAccessDenied))
return
}
dirPath = h.FsPath(dirPath)
// dirPath = q.FsPath(userID, dirPath)
infos, err := h.deps.FS().ListDir(dirPath)
if err != nil {
c.JSON(q.ErrResp(c, 500, err))
@ -489,7 +541,33 @@ func (h *FileHandlers) List(c *gin.Context) {
})
}
c.JSON(200, &ListResp{Metadatas: metadatas})
c.JSON(200, &ListResp{
Cwd: dirPath,
Metadatas: metadatas,
})
}
func (h *FileHandlers) ListHome(c *gin.Context) {
userID := c.MustGet(q.UserIDParam).(string)
infos, err := h.deps.FS().ListDir(userID)
if err != nil {
c.JSON(q.ErrResp(c, 500, err))
return
}
metadatas := []*MetadataResp{}
for _, info := range infos {
metadatas = append(metadatas, &MetadataResp{
Name: info.Name(),
Size: info.Size(),
ModTime: info.ModTime(),
IsDir: info.IsDir(),
})
}
c.JSON(200, &ListResp{
Cwd: userID,
Metadatas: metadatas,
})
}
func (h *FileHandlers) Copy(c *gin.Context) {
@ -500,16 +578,8 @@ func (h *FileHandlers) CopyDir(c *gin.Context) {
c.JSON(q.NewMsgResp(501, "Not Implemented"))
}
func (h *FileHandlers) getTmpPath(filePath string) string {
return path.Join(UploadDir, fmt.Sprintf("%x", sha1.Sum([]byte(filePath))))
}
func lockName(user, filePath string) string {
return fmt.Sprintf("%s/%s", user, filePath)
}
func (h *FileHandlers) FsPath(filePath string) string {
return path.Join(FsDir, filePath)
func lockName(filePath string) string {
return filePath
}
type ListUploadingsResp struct {
@ -517,9 +587,9 @@ type ListUploadingsResp struct {
}
func (h *FileHandlers) ListUploadings(c *gin.Context) {
userName := c.MustGet(singleuserhdr.UserParam).(string)
userID := c.MustGet(q.UserIDParam).(string)
infos, err := h.uploadMgr.ListInfo(userName)
infos, err := h.uploadMgr.ListInfo(userID)
if err != nil {
c.JSON(q.ErrResp(c, 500, err))
return
@ -533,11 +603,11 @@ func (h *FileHandlers) DelUploading(c *gin.Context) {
c.JSON(q.ErrResp(c, 400, errors.New("invalid file path")))
return
}
userName := c.MustGet(singleuserhdr.UserParam).(string)
userID := c.MustGet(q.UserIDParam).(string)
var err error
tmpFilePath := h.getTmpPath(filePath)
locker := h.NewAutoLocker(c, lockName(userName, tmpFilePath))
tmpFilePath := q.GetTmpPath(userID, filePath)
locker := h.NewAutoLocker(c, lockName(tmpFilePath))
locker.Exec(func() {
err = h.deps.FS().Remove(tmpFilePath)
if err != nil {
@ -545,7 +615,7 @@ func (h *FileHandlers) DelUploading(c *gin.Context) {
return
}
err = h.uploadMgr.DelInfo(userName, tmpFilePath)
err = h.uploadMgr.DelInfo(userID, tmpFilePath)
if err != nil {
c.JSON(q.ErrResp(c, 500, err))
return

View file

@ -3,6 +3,7 @@ package multiusers
import (
"errors"
"fmt"
"path/filepath"
"strconv"
"time"
@ -18,30 +19,95 @@ import (
var (
ErrInvalidUser = errors.New("invalid user name or password")
ErrInvalidConfig = errors.New("invalid user config")
UserIDParam = "uid"
UserParam = "user"
PwdParam = "pwd"
NewPwdParam = "newpwd"
RoleParam = "role"
ExpireParam = "expire"
TokenCookie = "tk"
)
type MultiUsersSvc struct {
cfg gocfg.ICfg
deps *depidx.Deps
cfg gocfg.ICfg
deps *depidx.Deps
apiACRules map[string]bool
}
func NewMultiUsersSvc(cfg gocfg.ICfg, deps *depidx.Deps) (*MultiUsersSvc, error) {
publicPath := filepath.Join("/", cfg.GrabString("Server.PublicPath"))
apiACRules := map[string]bool{
// TODO: make these configurable
// admin rules
apiRuleCname(userstore.AdminRole, "GET", "/"): true,
apiRuleCname(userstore.AdminRole, "GET", publicPath): true,
apiRuleCname(userstore.AdminRole, "POST", "/v1/users/login"): true,
apiRuleCname(userstore.AdminRole, "POST", "/v1/users/logout"): true,
apiRuleCname(userstore.AdminRole, "GET", "/v1/users/isauthed"): true,
apiRuleCname(userstore.AdminRole, "PATCH", "/v1/users/pwd"): true,
apiRuleCname(userstore.AdminRole, "POST", "/v1/users/"): true,
apiRuleCname(userstore.AdminRole, "POST", "/v1/roles/"): true,
apiRuleCname(userstore.AdminRole, "DELETE", "/v1/roles/"): true,
apiRuleCname(userstore.AdminRole, "GET", "/v1/roles/"): true,
apiRuleCname(userstore.AdminRole, "POST", "/v1/fs/files"): true,
apiRuleCname(userstore.AdminRole, "DELETE", "/v1/fs/files"): true,
apiRuleCname(userstore.AdminRole, "GET", "/v1/fs/files"): true,
apiRuleCname(userstore.AdminRole, "PATCH", "/v1/fs/files/chunks"): true,
apiRuleCname(userstore.AdminRole, "GET", "/v1/fs/files/chunks"): true,
apiRuleCname(userstore.AdminRole, "PATCH", "/v1/fs/files/copy"): true,
apiRuleCname(userstore.AdminRole, "PATCH", "/v1/fs/files/move"): true,
apiRuleCname(userstore.AdminRole, "GET", "/v1/fs/dirs"): true,
apiRuleCname(userstore.AdminRole, "GET", "/v1/fs/dirs/home"): true,
apiRuleCname(userstore.AdminRole, "POST", "/v1/fs/dirs"): true,
apiRuleCname(userstore.AdminRole, "GET", "/v1/fs/uploadings"): true,
apiRuleCname(userstore.AdminRole, "DELETE", "/v1/fs/uploadings"): true,
apiRuleCname(userstore.AdminRole, "GET", "/v1/fs/metadata"): true,
apiRuleCname(userstore.AdminRole, "OPTIONS", "/v1/settings/health"): true,
// user rules
apiRuleCname(userstore.UserRole, "GET", "/"): true,
apiRuleCname(userstore.UserRole, "GET", publicPath): true,
apiRuleCname(userstore.UserRole, "POST", "/v1/users/logout"): true,
apiRuleCname(userstore.UserRole, "GET", "/v1/users/isauthed"): true,
apiRuleCname(userstore.UserRole, "PATCH", "/v1/users/pwd"): true,
apiRuleCname(userstore.UserRole, "POST", "/v1/fs/files"): true,
apiRuleCname(userstore.UserRole, "DELETE", "/v1/fs/files"): true,
apiRuleCname(userstore.UserRole, "GET", "/v1/fs/files"): true,
apiRuleCname(userstore.UserRole, "PATCH", "/v1/fs/files/chunks"): true,
apiRuleCname(userstore.UserRole, "GET", "/v1/fs/files/chunks"): true,
apiRuleCname(userstore.UserRole, "PATCH", "/v1/fs/files/copy"): true,
apiRuleCname(userstore.UserRole, "PATCH", "/v1/fs/files/move"): true,
apiRuleCname(userstore.UserRole, "GET", "/v1/fs/dirs"): true,
apiRuleCname(userstore.UserRole, "GET", "/v1/fs/dirs/home"): true,
apiRuleCname(userstore.UserRole, "POST", "/v1/fs/dirs"): true,
apiRuleCname(userstore.UserRole, "GET", "/v1/fs/uploadings"): true,
apiRuleCname(userstore.UserRole, "DELETE", "/v1/fs/uploadings"): true,
apiRuleCname(userstore.UserRole, "GET", "/v1/fs/metadata"): true,
apiRuleCname(userstore.UserRole, "OPTIONS", "/v1/settings/health"): true,
// visitor rules
apiRuleCname(userstore.VisitorRole, "GET", "/"): true,
apiRuleCname(userstore.VisitorRole, "GET", publicPath): true,
apiRuleCname(userstore.VisitorRole, "POST", "/v1/users/login"): true,
apiRuleCname(userstore.VisitorRole, "GET", "/v1/users/isauthed"): true,
apiRuleCname(userstore.VisitorRole, "GET", "/v1/fs/files"): true,
apiRuleCname(userstore.VisitorRole, "OPTIONS", "/v1/settings/health"): true,
}
return &MultiUsersSvc{
cfg: cfg,
deps: deps,
cfg: cfg,
deps: deps,
apiACRules: apiACRules,
}, nil
}
func (h *MultiUsersSvc) Init(adminName, adminPwd string) (string, error) {
var err error
userID := "0"
fsPath := q.HomePath(userID, "/")
if err = h.deps.FS().MkdirAll(fsPath); err != nil {
return "", err
}
uploadingsPath := q.GetTmpPath(userID, "/")
if err = h.deps.FS().MkdirAll(uploadingsPath); err != nil {
return "", err
}
// TODO: return "" for being compatible with singleuser service, should remove this
err := h.deps.Users().Init(adminName, adminPwd)
err = h.deps.Users().Init(adminName, adminPwd)
return "", err
}
@ -75,10 +141,10 @@ func (h *MultiUsersSvc) Login(c *gin.Context) {
ttl := h.cfg.GrabInt("Users.CookieTTL")
token, err := h.deps.Token().ToToken(map[string]string{
UserIDParam: fmt.Sprint(user.ID),
UserParam: user.Name,
RoleParam: user.Role,
ExpireParam: fmt.Sprintf("%d", time.Now().Unix()+int64(ttl)),
q.UserIDParam: fmt.Sprint(user.ID),
q.UserParam: user.Name,
q.RoleParam: user.Role,
q.ExpireParam: fmt.Sprintf("%d", time.Now().Unix()+int64(ttl)),
})
if err != nil {
c.JSON(q.ErrResp(c, 500, err))
@ -87,7 +153,7 @@ func (h *MultiUsersSvc) Login(c *gin.Context) {
secure := h.cfg.GrabBool("Users.CookieSecure")
httpOnly := h.cfg.GrabBool("Users.CookieHttpOnly")
c.SetCookie(TokenCookie, token, ttl, "/", "", secure, httpOnly)
c.SetCookie(q.TokenCookie, token, ttl, "/", "", secure, httpOnly)
c.JSON(q.Resp(200))
}
@ -98,12 +164,17 @@ func (h *MultiUsersSvc) Logout(c *gin.Context) {
// token alreay verified in the authn middleware
secure := h.cfg.GrabBool("Users.CookieSecure")
httpOnly := h.cfg.GrabBool("Users.CookieHttpOnly")
c.SetCookie(TokenCookie, "", 0, "/", "", secure, httpOnly)
c.SetCookie(q.TokenCookie, "", 0, "/", "", secure, httpOnly)
c.JSON(q.Resp(200))
}
func (h *MultiUsersSvc) IsAuthed(c *gin.Context) {
// token alreay verified in the authn middleware
role := c.MustGet(q.RoleParam).(string)
if role == userstore.VisitorRole {
c.JSON(q.ErrResp(c, 401, q.ErrUnauthorized))
return
}
c.JSON(q.Resp(200))
}
@ -128,7 +199,7 @@ func (h *MultiUsersSvc) SetPwd(c *gin.Context) {
return
}
uid, err := strconv.ParseUint(claims[UserIDParam], 10, 64)
uid, err := strconv.ParseUint(claims[q.UserIDParam], 10, 64)
if err != nil {
c.JSON(q.ErrResp(c, 500, err))
return
@ -177,13 +248,13 @@ func (h *MultiUsersSvc) AddUser(c *gin.Context) {
return
}
// TODO: do more comprehensive validation
// Role and duplicated name will be validated by the store
if len(req.Name) < 2 {
c.JSON(q.ErrResp(c, 400, errors.New("name length must be greater than 2")))
var err error
if err = h.isValidUserName(req.Name); err != nil {
c.JSON(q.ErrResp(c, 400, err))
return
} else if len(req.Name) < 3 {
c.JSON(q.ErrResp(c, 400, errors.New("password length must be greater than 2")))
} else if err = h.isValidPwd(req.Pwd); err != nil {
c.JSON(q.ErrResp(c, 400, err))
return
}
@ -194,6 +265,20 @@ func (h *MultiUsersSvc) AddUser(c *gin.Context) {
return
}
// TODO: following operations must be atomic
// TODO: check if the folders already exists
userID := c.MustGet(q.UserIDParam).(string)
homePath := q.HomePath(userID, "/")
if err = h.deps.FS().MkdirAll(homePath); err != nil {
c.JSON(q.ErrResp(c, 500, err))
return
}
uploadingsPath := q.GetTmpPath(userID, "/")
if err = h.deps.FS().MkdirAll(uploadingsPath); err != nil {
c.JSON(q.ErrResp(c, 500, err))
return
}
err = h.deps.Users().AddUser(&userstore.User{
ID: uid,
Name: req.Name,
@ -213,19 +298,19 @@ type AddRoleReq struct {
}
func (h *MultiUsersSvc) AddRole(c *gin.Context) {
var err error
req := &AddRoleReq{}
if err := c.ShouldBindJSON(&req); err != nil {
if err = c.ShouldBindJSON(&req); err != nil {
c.JSON(q.ErrResp(c, 400, err))
return
}
// TODO: do more comprehensive validation
if len(req.Role) < 2 {
c.JSON(q.ErrResp(c, 400, errors.New("name length must be greater than 2")))
if err = h.isValidRole(req.Role); err != nil {
c.JSON(q.ErrResp(c, 400, err))
return
}
err := h.deps.Users().AddRole(req.Role)
err = h.deps.Users().AddRole(req.Role)
if err != nil {
c.JSON(q.ErrResp(c, 500, err))
return
@ -239,19 +324,19 @@ type DelRoleReq struct {
}
func (h *MultiUsersSvc) DelRole(c *gin.Context) {
var err error
req := &DelRoleReq{}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(q.ErrResp(c, 400, err))
return
}
// TODO: do more comprehensive validation
if len(req.Role) < 2 {
c.JSON(q.ErrResp(c, 400, errors.New("name length must be greater than 2")))
if err = h.isValidRole(req.Role); err != nil {
c.JSON(q.ErrResp(c, 400, err))
return
}
err := h.deps.Users().DelRole(req.Role)
err = h.deps.Users().DelRole(req.Role)
if err != nil {
c.JSON(q.ErrResp(c, 500, err))
return
@ -276,24 +361,47 @@ func (h *MultiUsersSvc) ListRoles(c *gin.Context) {
}
func (h *MultiUsersSvc) getUserInfo(c *gin.Context) (map[string]string, error) {
tokenStr, err := c.Cookie(TokenCookie)
tokenStr, err := c.Cookie(q.TokenCookie)
if err != nil {
return nil, err
}
claims, err := h.deps.Token().FromToken(
tokenStr,
map[string]string{
UserIDParam: "",
UserParam: "",
RoleParam: "",
ExpireParam: "",
q.UserIDParam: "",
q.UserParam: "",
q.RoleParam: "",
q.ExpireParam: "",
},
)
if err != nil {
return nil, err
} else if claims[UserIDParam] == "" || claims[UserParam] == "" {
} else if claims[q.UserIDParam] == "" || claims[q.UserParam] == "" {
return nil, ErrInvalidConfig
}
return claims, nil
}
func (h *MultiUsersSvc) isValidUserName(userName string) error {
minUserNameLen := h.cfg.GrabInt("Users.MinUserNameLen")
if len(userName) < minUserNameLen {
return errors.New("name is too short")
}
return nil
}
func (h *MultiUsersSvc) isValidPwd(pwd string) error {
minPwdLen := h.cfg.GrabInt("Users.MinPwdLen")
if len(pwd) < minPwdLen {
return errors.New("password is too short")
}
return nil
}
func (h *MultiUsersSvc) isValidRole(role string) error {
if role == userstore.AdminRole || role == userstore.UserRole || role == userstore.VisitorRole {
return errors.New("predefined roles can not be added/deleted")
}
return h.isValidUserName(role)
}

View file

@ -1,81 +1,84 @@
package multiusers
import (
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
q "github.com/ihexxa/quickshare/src/handlers"
"github.com/ihexxa/quickshare/src/userstore"
)
var exposedAPIs = map[string]bool{
"Login-fm": true,
"Health-fm": true,
func apiRuleCname(role, method, path string) string {
return fmt.Sprintf("%s-%s-%s", role, method, path)
}
var publicRootPath = "/"
var publicStaticPath = "/static"
func IsPublicPath(accessPath string) bool {
return accessPath == publicRootPath || strings.HasPrefix(accessPath, publicStaticPath)
}
func GetHandlerName(fullname string) (string, error) {
parts := strings.Split(fullname, ".")
if len(parts) == 0 {
return "", errors.New("invalid handler name")
}
return parts[len(parts)-1], nil
}
func (h *MultiUsersSvc) Auth() gin.HandlerFunc {
func (h *MultiUsersSvc) AuthN() gin.HandlerFunc {
return func(c *gin.Context) {
handlerName, err := GetHandlerName(c.HandlerName())
if err != nil {
c.JSON(q.ErrResp(c, 401, err))
return
}
accessPath := c.Request.URL.String()
enableAuth := h.cfg.GrabBool("Users.EnableAuth")
if enableAuth && !exposedAPIs[handlerName] && !IsPublicPath(accessPath) {
token, err := c.Cookie(TokenCookie)
if err != nil {
c.AbortWithStatusJSON(q.ErrResp(c, 401, err))
return
}
claims := map[string]string{
UserIDParam: "",
UserParam: "",
RoleParam: "",
ExpireParam: "",
}
claims, err = h.deps.Token().FromToken(token, claims)
if err != nil {
c.AbortWithStatusJSON(q.ErrResp(c, 401, err))
return
}
for key, val := range claims {
c.Set(key, val)
}
now := time.Now().Unix()
expire, err := strconv.ParseInt(claims[ExpireParam], 10, 64)
if err != nil || expire <= now {
c.AbortWithStatusJSON(q.ErrResp(c, 401, err))
return
}
// no one is allowed to download
} else {
// this is for UploadMgr to get user info to get related namespace
c.Set(UserParam, "quickshare_anonymous")
claims := map[string]string{
q.UserIDParam: "",
q.UserParam: "",
q.RoleParam: userstore.VisitorRole,
q.ExpireParam: "",
}
if enableAuth {
token, err := c.Cookie(q.TokenCookie)
if err != nil {
if err != http.ErrNoCookie {
c.AbortWithStatusJSON(q.ErrResp(c, 401, err))
return
}
// set default values if no cookie is found
} else if token != "" {
claims, err = h.deps.Token().FromToken(token, claims)
if err != nil {
c.AbortWithStatusJSON(q.ErrResp(c, 401, err))
return
}
now := time.Now().Unix()
expire, err := strconv.ParseInt(claims[q.ExpireParam], 10, 64)
if err != nil || expire <= now {
c.AbortWithStatusJSON(q.ErrResp(c, 401, err))
return
}
}
// set default values if token is empty
} else {
claims[q.UserIDParam] = "0"
claims[q.UserParam] = "admin"
claims[q.RoleParam] = userstore.AdminRole
claims[q.ExpireParam] = ""
}
for key, val := range claims {
c.Set(key, val)
}
c.Next()
}
}
func (h *MultiUsersSvc) APIAccessControl() gin.HandlerFunc {
return func(c *gin.Context) {
role := c.MustGet(q.RoleParam).(string)
method := c.Request.Method
accessPath := c.Request.URL.Path
// we don't lock the map because we only read it
if h.apiACRules[apiRuleCname(role, method, accessPath)] {
c.Next()
return
} else if accessPath == "/" || // TODO: temporarily allow accessing static resources
accessPath == "/favicon.ico" ||
strings.HasPrefix(accessPath, "/static") {
c.Next()
return
}
c.AbortWithStatusJSON(q.ErrResp(c, 403, q.ErrAccessDenied))
}
}

View file

@ -1,11 +1,31 @@
package handlers
import (
"crypto/sha1"
"errors"
"fmt"
"path/filepath"
"github.com/gin-gonic/gin"
)
var (
// dirs
UploadDir = "uploadings"
FsDir = "files"
UserIDParam = "uid"
UserParam = "user"
PwdParam = "pwd"
NewPwdParam = "newpwd"
RoleParam = "role"
ExpireParam = "expire"
TokenCookie = "tk"
ErrAccessDenied = errors.New("access denied")
ErrUnauthorized = errors.New("unauthorized")
)
var statusCodes = map[int]string{
100: "Continue", // RFC 7231, 6.2.1
101: "SwitchingProtocols", // RFC 7231, 6.2.2
@ -102,3 +122,15 @@ func ErrResp(c *gin.Context, code int, err error) (int, interface{}) {
return code, gErr.JSON()
}
func FsPath(userID, relFilePath string) string {
return filepath.Join(userID, FsDir, relFilePath)
}
func HomePath(userID, relFilePath string) string {
return filepath.Join(userID, relFilePath)
}
func GetTmpPath(userID, relFilePath string) string {
return filepath.Join(UploadDir, userID, fmt.Sprintf("%x", sha1.Sum([]byte(relFilePath))))
}