feat(admin): enable multi-users (#67)
* feat(userstore): support ListUsers * feat(userstore): support del users * feat(multiusers): support list users and delete user apis * feat(client/web): add new apis to web client * fix(ui/panes): move each pane out of the container * feat(ui): add admin pane * feat(users): support force set password api * feat(ui/admin-pane): add functions to admin pane * feat(users): support self API and move uploading folder to home * fix(users): remove home folder when deleting user * fix(ui): remove useless function * feat(ui/panes): hide admin menu if user is not admin * fix(server/files): list home path is incorrect * fix(server): 1.listHome return incorrect cwd 2.addUser init folder with incorrect uid 3.check ns before using * test(server): add regression test cases * test(users, files): add e2e test for concurrent operations * fix(test): clean ups
This commit is contained in:
parent
916ec7c2dc
commit
aefaca98b3
28 changed files with 1562 additions and 478 deletions
|
@ -118,7 +118,7 @@ func (h *FileHandlers) Create(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
tmpFilePath := q.GetTmpPath(userID, req.Path)
|
||||
tmpFilePath := q.UploadPath(userID, req.Path)
|
||||
locker := h.NewAutoLocker(c, lockName(tmpFilePath))
|
||||
locker.Exec(func() {
|
||||
err := h.deps.FS().Create(tmpFilePath)
|
||||
|
@ -295,7 +295,7 @@ func (h *FileHandlers) UploadChunk(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
tmpFilePath := q.GetTmpPath(userID, req.Path)
|
||||
tmpFilePath := q.UploadPath(userID, req.Path)
|
||||
locker := h.NewAutoLocker(c, lockName(tmpFilePath))
|
||||
locker.Exec(func() {
|
||||
var err error
|
||||
|
@ -407,7 +407,7 @@ func (h *FileHandlers) UploadStatus(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
tmpFilePath := q.GetTmpPath(userID, filePath)
|
||||
tmpFilePath := q.UploadPath(userID, filePath)
|
||||
locker := h.NewAutoLocker(c, lockName(tmpFilePath))
|
||||
locker.Exec(func() {
|
||||
_, fileSize, uploaded, err := h.uploadMgr.GetInfo(userID, tmpFilePath)
|
||||
|
@ -549,7 +549,8 @@ func (h *FileHandlers) List(c *gin.Context) {
|
|||
|
||||
func (h *FileHandlers) ListHome(c *gin.Context) {
|
||||
userID := c.MustGet(q.UserIDParam).(string)
|
||||
infos, err := h.deps.FS().ListDir(userID)
|
||||
fsPath := q.FsRootPath(userID, "/")
|
||||
infos, err := h.deps.FS().ListDir(fsPath)
|
||||
if err != nil {
|
||||
c.JSON(q.ErrResp(c, 500, err))
|
||||
return
|
||||
|
@ -565,7 +566,7 @@ func (h *FileHandlers) ListHome(c *gin.Context) {
|
|||
}
|
||||
|
||||
c.JSON(200, &ListResp{
|
||||
Cwd: userID,
|
||||
Cwd: fsPath,
|
||||
Metadatas: metadatas,
|
||||
})
|
||||
}
|
||||
|
@ -606,7 +607,7 @@ func (h *FileHandlers) DelUploading(c *gin.Context) {
|
|||
userID := c.MustGet(q.UserIDParam).(string)
|
||||
|
||||
var err error
|
||||
tmpFilePath := q.GetTmpPath(userID, filePath)
|
||||
tmpFilePath := q.UploadPath(userID, filePath)
|
||||
locker := h.NewAutoLocker(c, lockName(tmpFilePath))
|
||||
locker.Exec(func() {
|
||||
err = h.deps.FS().Remove(tmpFilePath)
|
||||
|
|
|
@ -102,7 +102,12 @@ func (um *UploadMgr) DelInfo(user, filePath string) error {
|
|||
}
|
||||
|
||||
func (um *UploadMgr) ListInfo(user string) ([]*UploadInfo, error) {
|
||||
infoMap, err := um.kv.ListStringsIn(UploadNS(user))
|
||||
ns := UploadNS(user)
|
||||
if !um.kv.HasNamespace(ns) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
infoMap, err := um.kv.ListStringsIn(ns)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -33,36 +33,41 @@ func NewMultiUsersSvc(cfg gocfg.ICfg, deps *depidx.Deps) (*MultiUsersSvc, error)
|
|||
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,
|
||||
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, "PATCH", "/v1/users/pwd/force-set"): true,
|
||||
apiRuleCname(userstore.AdminRole, "POST", "/v1/users/"): true,
|
||||
apiRuleCname(userstore.AdminRole, "DELETE", "/v1/users/"): true,
|
||||
apiRuleCname(userstore.AdminRole, "GET", "/v1/users/list"): true,
|
||||
apiRuleCname(userstore.AdminRole, "GET", "/v1/users/self"): true,
|
||||
apiRuleCname(userstore.AdminRole, "POST", "/v1/roles/"): true,
|
||||
apiRuleCname(userstore.AdminRole, "DELETE", "/v1/roles/"): true,
|
||||
apiRuleCname(userstore.AdminRole, "GET", "/v1/roles/list"): 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, "GET", "/v1/users/self"): 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,
|
||||
|
@ -82,6 +87,7 @@ func NewMultiUsersSvc(cfg gocfg.ICfg, deps *depidx.Deps) (*MultiUsersSvc, error)
|
|||
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/users/self"): true,
|
||||
apiRuleCname(userstore.VisitorRole, "GET", "/v1/fs/files"): true,
|
||||
apiRuleCname(userstore.VisitorRole, "OPTIONS", "/v1/settings/health"): true,
|
||||
}
|
||||
|
@ -97,12 +103,12 @@ func (h *MultiUsersSvc) Init(adminName, adminPwd string) (string, error) {
|
|||
var err error
|
||||
|
||||
userID := "0"
|
||||
fsPath := q.HomePath(userID, "/")
|
||||
fsPath := q.FsRootPath(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 {
|
||||
uploadFolder := q.UploadFolder(userID)
|
||||
if err = h.deps.FS().MkdirAll(uploadFolder); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
|
@ -231,6 +237,57 @@ func (h *MultiUsersSvc) SetPwd(c *gin.Context) {
|
|||
c.JSON(q.Resp(200))
|
||||
}
|
||||
|
||||
type ForceSetPwdReq struct {
|
||||
ID string `json:"id"`
|
||||
NewPwd string `json:"newPwd"`
|
||||
}
|
||||
|
||||
func (h *MultiUsersSvc) ForceSetPwd(c *gin.Context) {
|
||||
req := &ForceSetPwdReq{}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(q.ErrResp(c, 400, err))
|
||||
return
|
||||
}
|
||||
|
||||
claims, err := h.getUserInfo(c)
|
||||
if err != nil {
|
||||
c.JSON(q.ErrResp(c, 401, err))
|
||||
return
|
||||
}
|
||||
if claims[q.RoleParam] != userstore.AdminRole {
|
||||
c.JSON(q.ErrResp(c, 403, errors.New("operation denied")))
|
||||
return
|
||||
}
|
||||
targetUID, err := strconv.ParseUint(req.ID, 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(q.ErrResp(c, 500, err))
|
||||
return
|
||||
}
|
||||
targetUser, err := h.deps.Users().GetUser(targetUID)
|
||||
if err != nil {
|
||||
c.JSON(q.ErrResp(c, 500, err))
|
||||
return
|
||||
}
|
||||
if targetUser.Role == userstore.AdminRole {
|
||||
c.JSON(q.ErrResp(c, 403, errors.New("can not set admin's password")))
|
||||
return
|
||||
}
|
||||
|
||||
newHash, err := bcrypt.GenerateFromPassword([]byte(req.NewPwd), 10)
|
||||
if err != nil {
|
||||
c.JSON(q.ErrResp(c, 500, errors.New("fail to set password")))
|
||||
return
|
||||
}
|
||||
|
||||
err = h.deps.Users().SetPwd(targetUser.ID, string(newHash))
|
||||
if err != nil {
|
||||
c.JSON(q.ErrResp(c, 500, err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(q.Resp(200))
|
||||
}
|
||||
|
||||
type AddUserReq struct {
|
||||
Name string `json:"name"`
|
||||
Pwd string `json:"pwd"`
|
||||
|
@ -267,14 +324,14 @@ func (h *MultiUsersSvc) AddUser(c *gin.Context) {
|
|||
|
||||
// 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 {
|
||||
uidStr := fmt.Sprint(uid)
|
||||
fsRootFolder := q.FsRootPath(uidStr, "/")
|
||||
if err = h.deps.FS().MkdirAll(fsRootFolder); err != nil {
|
||||
c.JSON(q.ErrResp(c, 500, err))
|
||||
return
|
||||
}
|
||||
uploadingsPath := q.GetTmpPath(userID, "/")
|
||||
if err = h.deps.FS().MkdirAll(uploadingsPath); err != nil {
|
||||
uploadFolder := q.UploadFolder(uidStr)
|
||||
if err = h.deps.FS().MkdirAll(uploadFolder); err != nil {
|
||||
c.JSON(q.ErrResp(c, 500, err))
|
||||
return
|
||||
}
|
||||
|
@ -293,6 +350,71 @@ func (h *MultiUsersSvc) AddUser(c *gin.Context) {
|
|||
c.JSON(200, &AddUserResp{ID: fmt.Sprint(uid)})
|
||||
}
|
||||
|
||||
type DelUserResp struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
func (h *MultiUsersSvc) DelUser(c *gin.Context) {
|
||||
userIDStr := c.Query(q.UserIDParam)
|
||||
userID, err := strconv.ParseUint(userIDStr, 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(q.ErrResp(c, 400, fmt.Errorf("invalid users ID %w", err)))
|
||||
return
|
||||
} else if userID == 0 {
|
||||
c.JSON(q.ErrResp(c, 400, errors.New("It is not allowed to delete root")))
|
||||
return
|
||||
}
|
||||
|
||||
claims, err := h.getUserInfo(c)
|
||||
if err != nil {
|
||||
c.JSON(q.ErrResp(c, 401, err))
|
||||
return
|
||||
}
|
||||
if claims[q.UserIDParam] == userIDStr {
|
||||
c.JSON(q.ErrResp(c, 403, errors.New("can not delete self")))
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: try to make following atomic
|
||||
err = h.deps.Users().DelUser(userID)
|
||||
if err != nil {
|
||||
c.JSON(q.ErrResp(c, 500, err))
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: move the folder to recycle bin when it failed to remove it
|
||||
homePath := userIDStr
|
||||
if err = h.deps.FS().Remove(homePath); err != nil {
|
||||
c.JSON(q.ErrResp(c, 500, err))
|
||||
return
|
||||
}
|
||||
c.JSON(200, &DelUserResp{ID: userIDStr})
|
||||
}
|
||||
|
||||
type ListUsersResp struct {
|
||||
Users []*userstore.User `json:"users"`
|
||||
}
|
||||
|
||||
func (h *MultiUsersSvc) ListUsers(c *gin.Context) {
|
||||
// TODO: pagination is not enabled
|
||||
// lastID := 0
|
||||
// lastIDStr := c.Query(q.LastID)
|
||||
// if lastIDStr != "" {
|
||||
// lastID, err := strconv.Atoi(lastIDStr)
|
||||
// if err != nil {
|
||||
// c.JSON(q.ErrResp(c, 400, fmt.Errorf("invalid param %w", err)))
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
|
||||
users, err := h.deps.Users().ListUsers()
|
||||
if err != nil {
|
||||
c.JSON(q.ErrResp(c, 500, err))
|
||||
return
|
||||
}
|
||||
c.JSON(200, &ListUsersResp{Users: users})
|
||||
}
|
||||
|
||||
type AddRoleReq struct {
|
||||
Role string `json:"role"`
|
||||
}
|
||||
|
@ -405,3 +527,23 @@ func (h *MultiUsersSvc) isValidRole(role string) error {
|
|||
}
|
||||
return h.isValidUserName(role)
|
||||
}
|
||||
|
||||
type SelfResp struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Role string `json:"role"`
|
||||
}
|
||||
|
||||
func (h *MultiUsersSvc) Self(c *gin.Context) {
|
||||
claims, err := h.getUserInfo(c)
|
||||
if err != nil {
|
||||
c.JSON(q.ErrResp(c, 401, err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, &SelfResp{
|
||||
ID: claims[q.UserIDParam],
|
||||
Name: claims[q.UserParam],
|
||||
Role: claims[q.RoleParam],
|
||||
})
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ var (
|
|||
// dirs
|
||||
UploadDir = "uploadings"
|
||||
FsDir = "files"
|
||||
FsRootDir = "files"
|
||||
|
||||
UserIDParam = "uid"
|
||||
UserParam = "user"
|
||||
|
@ -21,6 +22,7 @@ var (
|
|||
RoleParam = "role"
|
||||
ExpireParam = "expire"
|
||||
TokenCookie = "tk"
|
||||
LastID = "lid"
|
||||
|
||||
ErrAccessDenied = errors.New("access denied")
|
||||
ErrUnauthorized = errors.New("unauthorized")
|
||||
|
@ -131,6 +133,18 @@ func HomePath(userID, relFilePath string) string {
|
|||
return filepath.Join(userID, relFilePath)
|
||||
}
|
||||
|
||||
func FsRootPath(userID, relFilePath string) string {
|
||||
return filepath.Join(userID, FsRootDir, relFilePath)
|
||||
}
|
||||
|
||||
func GetTmpPath(userID, relFilePath string) string {
|
||||
return filepath.Join(UploadDir, userID, fmt.Sprintf("%x", sha1.Sum([]byte(relFilePath))))
|
||||
}
|
||||
|
||||
func UploadPath(userID, relFilePath string) string {
|
||||
return filepath.Join(UploadFolder(userID), fmt.Sprintf("%x", sha1.Sum([]byte(relFilePath))))
|
||||
}
|
||||
|
||||
func UploadFolder(userID string) string {
|
||||
return filepath.Join(userID, UploadDir)
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue