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:
parent
9748d0cab4
commit
81da97650b
18 changed files with 527 additions and 212 deletions
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue