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:
Hexxa 2021-07-30 21:59:33 -05:00 committed by GitHub
parent 916ec7c2dc
commit aefaca98b3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 1562 additions and 478 deletions

View file

@ -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],
})
}