// Copyright (c) 2017 VMware, Inc. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package api import ( "fmt" "net/http" "regexp" "strconv" "strings" "github.com/vmware/harbor/src/common/dao" "github.com/vmware/harbor/src/common/models" "github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/ui/config" ) // UserAPI handles request to /api/users/{} type UserAPI struct { BaseController currentUserID int userID int SelfRegistration bool IsAdmin bool AuthMode string } type passwordReq struct { OldPassword string `json:"old_password"` NewPassword string `json:"new_password"` } // Prepare validates the URL and parms func (ua *UserAPI) Prepare() { ua.BaseController.Prepare() mode, err := config.AuthMode() if err != nil { log.Errorf("failed to get auth mode: %v", err) ua.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) } ua.AuthMode = mode self, err := config.SelfRegistration() if err != nil { log.Errorf("failed to get self registration: %v", err) ua.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) } ua.SelfRegistration = self if !ua.SecurityCtx.IsAuthenticated() { if ua.Ctx.Input.IsPost() { return } ua.HandleUnauthorized() return } user, err := dao.GetUser(models.User{ Username: ua.SecurityCtx.GetUsername(), }) if err != nil { ua.HandleInternalServerError(fmt.Sprintf("failed to get user %s: %v", ua.SecurityCtx.GetUsername(), err)) return } ua.currentUserID = user.UserID id := ua.Ctx.Input.Param(":id") if id == "current" { ua.userID = ua.currentUserID } else if len(id) > 0 { var err error ua.userID, err = strconv.Atoi(id) if err != nil { log.Errorf("Invalid user id, error: %v", err) ua.CustomAbort(http.StatusBadRequest, "Invalid user Id") } userQuery := models.User{UserID: ua.userID} u, err := dao.GetUser(userQuery) if err != nil { log.Errorf("Error occurred in GetUser, error: %v", err) ua.CustomAbort(http.StatusInternalServerError, "Internal error.") } if u == nil { log.Errorf("User with Id: %d does not exist", ua.userID) ua.CustomAbort(http.StatusNotFound, "") } } ua.IsAdmin = ua.SecurityCtx.IsSysAdmin() } // Get ... func (ua *UserAPI) Get() { if ua.userID == ua.currentUserID || ua.IsAdmin { userQuery := models.User{UserID: ua.userID} u, err := dao.GetUser(userQuery) if err != nil { log.Errorf("Error occurred in GetUser, error: %v", err) ua.CustomAbort(http.StatusInternalServerError, "Internal error.") } ua.Data["json"] = u ua.ServeJSON() return } log.Errorf("Current user, id: %d does not have admin role, can not view other user's detail", ua.currentUserID) ua.RenderError(http.StatusForbidden, "User does not have admin role") return } // List ... func (ua *UserAPI) List() { if !ua.IsAdmin { log.Errorf("Current user, id: %d does not have admin role, can not list users", ua.currentUserID) ua.RenderError(http.StatusForbidden, "User does not have admin role") return } page, size := ua.GetPaginationParams() query := &models.UserQuery{ Username: ua.GetString("username"), Email: ua.GetString("email"), Pagination: &models.Pagination{ Page: page, Size: size, }, } total, err := dao.GetTotalOfUsers(query) if err != nil { ua.HandleInternalServerError(fmt.Sprintf("failed to get total of users: %v", err)) return } users, err := dao.ListUsers(query) if err != nil { ua.HandleInternalServerError(fmt.Sprintf("failed to get users: %v", err)) return } ua.SetPaginationHeader(total, page, size) ua.Data["json"] = users ua.ServeJSON() } // Put ... func (ua *UserAPI) Put() { ldapAdminUser := (ua.AuthMode == "ldap_auth" && ua.userID == 1 && ua.userID == ua.currentUserID) if !(ua.AuthMode == "db_auth" || ldapAdminUser) { ua.CustomAbort(http.StatusForbidden, "") } if !ua.IsAdmin { if ua.userID != ua.currentUserID { log.Warning("Guests can only change their own account.") ua.CustomAbort(http.StatusForbidden, "Guests can only change their own account.") } } user := models.User{UserID: ua.userID} ua.DecodeJSONReq(&user) err := commonValidate(user) if err != nil { log.Warningf("Bad request in change user profile: %v", err) ua.RenderError(http.StatusBadRequest, "change user profile error:"+err.Error()) return } userQuery := models.User{UserID: ua.userID} u, err := dao.GetUser(userQuery) if err != nil { log.Errorf("Error occurred in GetUser, error: %v", err) ua.CustomAbort(http.StatusInternalServerError, "Internal error.") } if u == nil { log.Errorf("User with Id: %d does not exist", ua.userID) ua.CustomAbort(http.StatusNotFound, "") } if u.Email != user.Email { emailExist, err := dao.UserExists(user, "email") if err != nil { log.Errorf("Error occurred in change user profile: %v", err) ua.CustomAbort(http.StatusInternalServerError, "Internal error.") } if emailExist { log.Warning("email has already been used!") ua.RenderError(http.StatusConflict, "email has already been used!") return } } if err := dao.ChangeUserProfile(user); err != nil { log.Errorf("Failed to update user profile, error: %v", err) ua.CustomAbort(http.StatusInternalServerError, err.Error()) } } // Post ... func (ua *UserAPI) Post() { if !(ua.AuthMode == "db_auth") { ua.CustomAbort(http.StatusForbidden, "") } if !(ua.SelfRegistration || ua.IsAdmin) { log.Warning("Registration can only be used by admin role user when self-registration is off.") ua.CustomAbort(http.StatusForbidden, "") } user := models.User{} ua.DecodeJSONReq(&user) err := validate(user) if err != nil { log.Warningf("Bad request in Register: %v", err) ua.RenderError(http.StatusBadRequest, "register error:"+err.Error()) return } userExist, err := dao.UserExists(user, "username") if err != nil { log.Errorf("Error occurred in Register: %v", err) ua.CustomAbort(http.StatusInternalServerError, "Internal error.") } if userExist { log.Warning("username has already been used!") ua.RenderError(http.StatusConflict, "username has already been used!") return } emailExist, err := dao.UserExists(user, "email") if err != nil { log.Errorf("Error occurred in change user profile: %v", err) ua.CustomAbort(http.StatusInternalServerError, "Internal error.") } if emailExist { log.Warning("email has already been used!") ua.RenderError(http.StatusConflict, "email has already been used!") return } userID, err := dao.Register(user) if err != nil { log.Errorf("Error occurred in Register: %v", err) ua.CustomAbort(http.StatusInternalServerError, "Internal error.") } ua.Redirect(http.StatusCreated, strconv.FormatInt(userID, 10)) } // Delete ... func (ua *UserAPI) Delete() { if !ua.IsAdmin { log.Warningf("current user, id: %d does not have admin role, can not remove user", ua.currentUserID) ua.RenderError(http.StatusForbidden, "User does not have admin role") return } if ua.AuthMode == "ldap_auth" { ua.CustomAbort(http.StatusForbidden, "user can not be deleted in LDAP authentication mode") } if ua.currentUserID == ua.userID { ua.CustomAbort(http.StatusForbidden, "can not delete yourself") } if ua.userID == 1 { ua.HandleForbidden(ua.SecurityCtx.GetUsername()) return } var err error err = dao.DeleteUser(ua.userID) if err != nil { log.Errorf("Failed to delete data from database, error: %v", err) ua.RenderError(http.StatusInternalServerError, "Failed to delete User") return } } // ChangePassword handles PUT to /api/users/{}/password func (ua *UserAPI) ChangePassword() { ldapAdminUser := (ua.AuthMode == "ldap_auth" && ua.userID == 1 && ua.userID == ua.currentUserID) if !(ua.AuthMode == "db_auth" || ldapAdminUser) { ua.CustomAbort(http.StatusForbidden, "") } if !ua.IsAdmin { if ua.userID != ua.currentUserID { log.Error("Guests can only change their own account.") ua.CustomAbort(http.StatusForbidden, "Guests can only change their own account.") } } var req passwordReq ua.DecodeJSONReq(&req) if req.OldPassword == "" { log.Error("Old password is blank") ua.CustomAbort(http.StatusBadRequest, "Old password is blank") } queryUser := models.User{UserID: ua.userID, Password: req.OldPassword} user, err := dao.CheckUserPassword(queryUser) if err != nil { log.Errorf("Error occurred in CheckUserPassword: %v", err) ua.CustomAbort(http.StatusInternalServerError, "Internal error.") } if user == nil { log.Warning("Password input is not correct") ua.CustomAbort(http.StatusForbidden, "old_password_is_not_correct") } if req.NewPassword == "" { ua.CustomAbort(http.StatusBadRequest, "please_input_new_password") } updateUser := models.User{UserID: ua.userID, Password: req.NewPassword, Salt: user.Salt} err = dao.ChangeUserPassword(updateUser, req.OldPassword) if err != nil { log.Errorf("Error occurred in ChangeUserPassword: %v", err) ua.CustomAbort(http.StatusInternalServerError, "Internal error.") } } // ToggleUserAdminRole handles PUT api/users/{}/sysadmin func (ua *UserAPI) ToggleUserAdminRole() { if !ua.IsAdmin { log.Warningf("current user, id: %d does not have admin role, can not update other user's role", ua.currentUserID) ua.RenderError(http.StatusForbidden, "User does not have admin role") return } userQuery := models.User{UserID: ua.userID} ua.DecodeJSONReq(&userQuery) if err := dao.ToggleUserAdminRole(userQuery.UserID, userQuery.HasAdminRole); err != nil { log.Errorf("Error occurred in ToggleUserAdminRole: %v", err) ua.CustomAbort(http.StatusInternalServerError, "Internal error.") } } // validate only validate when user register func validate(user models.User) error { if isIllegalLength(user.Username, 1, 20) { return fmt.Errorf("username with illegal length") } if isContainIllegalChar(user.Username, []string{",", "~", "#", "$", "%"}) { return fmt.Errorf("username contains illegal characters") } if isIllegalLength(user.Password, 8, 20) { return fmt.Errorf("password with illegal length") } return commonValidate(user) } //commonValidate validates email, realname, comment information when user register or change their profile func commonValidate(user models.User) error { if len(user.Email) > 0 { if m, _ := regexp.MatchString(`^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$`, user.Email); !m { return fmt.Errorf("email with illegal format") } } else { return fmt.Errorf("Email can't be empty") } if isIllegalLength(user.Realname, 1, 255) { return fmt.Errorf("realname with illegal length") } if isContainIllegalChar(user.Realname, []string{",", "~", "#", "$", "%"}) { return fmt.Errorf("realname contains illegal characters") } if isIllegalLength(user.Comment, -1, 30) { return fmt.Errorf("comment with illegal length") } return nil } func isIllegalLength(s string, min int, max int) bool { if min == -1 { return (len(s) > max) } if max == -1 { return (len(s) <= min) } return (len(s) < min || len(s) > max) } func isContainIllegalChar(s string, illegalChar []string) bool { for _, c := range illegalChar { if strings.Index(s, c) >= 0 { return true } } return false }