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
|
@ -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],
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue