github.com/bluestoneag/bluephish@v0.1.0/controllers/api/user.go (about) 1 package api 2 3 import ( 4 "encoding/json" 5 "errors" 6 "net/http" 7 "strconv" 8 9 "github.com/bluestoneag/bluephish/auth" 10 ctx "github.com/bluestoneag/bluephish/context" 11 log "github.com/bluestoneag/bluephish/logger" 12 "github.com/bluestoneag/bluephish/models" 13 "github.com/gorilla/mux" 14 "github.com/jinzhu/gorm" 15 ) 16 17 // ErrUsernameTaken is thrown when a user attempts to register a username that is taken. 18 var ErrUsernameTaken = errors.New("Username already taken") 19 20 // ErrEmptyUsername is thrown when a user attempts to register a username that is taken. 21 var ErrEmptyUsername = errors.New("No username provided") 22 23 // ErrEmptyRole is throws when no role is provided when creating or modifying a user. 24 var ErrEmptyRole = errors.New("No role specified") 25 26 // ErrInsufficientPermission is thrown when a user attempts to change an 27 // attribute (such as the role) for which they don't have permission. 28 var ErrInsufficientPermission = errors.New("Permission denied") 29 30 // userRequest is the payload which represents the creation of a new user. 31 type userRequest struct { 32 Username string `json:"username"` 33 Password string `json:"password"` 34 Role string `json:"role"` 35 PasswordChangeRequired bool `json:"password_change_required"` 36 AccountLocked bool `json:"account_locked"` 37 } 38 39 func (ur *userRequest) Validate(existingUser *models.User) error { 40 switch { 41 case ur.Username == "": 42 return ErrEmptyUsername 43 case ur.Role == "": 44 return ErrEmptyRole 45 } 46 // Verify that the username isn't already taken. We consider two cases: 47 // * We're creating a new user, in which case any match is a conflict 48 // * We're modifying a user, in which case any match with a different ID is 49 // a conflict. 50 possibleConflict, err := models.GetUserByUsername(ur.Username) 51 if err == nil { 52 if existingUser == nil { 53 return ErrUsernameTaken 54 } 55 if possibleConflict.Id != existingUser.Id { 56 return ErrUsernameTaken 57 } 58 } 59 // If we have an error which is not simply indicating that no user was found, report it 60 if err != nil && err != gorm.ErrRecordNotFound { 61 return err 62 } 63 return nil 64 } 65 66 // Users contains functions to retrieve a list of existing users or create a 67 // new user. Users with the ModifySystem permissions can view and create users. 68 func (as *Server) Users(w http.ResponseWriter, r *http.Request) { 69 switch { 70 case r.Method == "GET": 71 us, err := models.GetUsers() 72 if err != nil { 73 JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError) 74 return 75 } 76 JSONResponse(w, us, http.StatusOK) 77 return 78 case r.Method == "POST": 79 ur := &userRequest{} 80 err := json.NewDecoder(r.Body).Decode(ur) 81 if err != nil { 82 JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest) 83 return 84 } 85 err = ur.Validate(nil) 86 if err != nil { 87 JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest) 88 return 89 } 90 err = auth.CheckPasswordPolicy(ur.Password) 91 if err != nil { 92 JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest) 93 return 94 } 95 hash, err := auth.GeneratePasswordHash(ur.Password) 96 if err != nil { 97 JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError) 98 return 99 } 100 role, err := models.GetRoleBySlug(ur.Role) 101 if err != nil { 102 JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError) 103 return 104 } 105 user := models.User{ 106 Username: ur.Username, 107 Hash: hash, 108 ApiKey: auth.GenerateSecureKey(auth.APIKeyLength), 109 Role: role, 110 RoleID: role.ID, 111 PasswordChangeRequired: ur.PasswordChangeRequired, 112 } 113 err = models.PutUser(&user) 114 if err != nil { 115 JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError) 116 return 117 } 118 JSONResponse(w, user, http.StatusOK) 119 return 120 } 121 } 122 123 // User contains functions to retrieve or delete a single user. Users with 124 // the ModifySystem permission can view and modify any user. Otherwise, users 125 // may only view or delete their own account. 126 func (as *Server) User(w http.ResponseWriter, r *http.Request) { 127 vars := mux.Vars(r) 128 id, _ := strconv.ParseInt(vars["id"], 0, 64) 129 // If the user doesn't have ModifySystem permissions, we need to verify 130 // that they're only taking action on their account. 131 currentUser := ctx.Get(r, "user").(models.User) 132 hasSystem, err := currentUser.HasPermission(models.PermissionModifySystem) 133 if err != nil { 134 JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError) 135 return 136 } 137 if !hasSystem && currentUser.Id != id { 138 JSONResponse(w, models.Response{Success: false, Message: http.StatusText(http.StatusForbidden)}, http.StatusForbidden) 139 return 140 } 141 existingUser, err := models.GetUser(id) 142 if err != nil { 143 JSONResponse(w, models.Response{Success: false, Message: "User not found"}, http.StatusNotFound) 144 return 145 } 146 switch { 147 case r.Method == "GET": 148 JSONResponse(w, existingUser, http.StatusOK) 149 case r.Method == "DELETE": 150 err = models.DeleteUser(id) 151 if err != nil { 152 JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError) 153 return 154 } 155 log.Infof("Deleted user account for %s", existingUser.Username) 156 JSONResponse(w, models.Response{Success: true, Message: "User deleted Successfully!"}, http.StatusOK) 157 case r.Method == "PUT": 158 ur := &userRequest{} 159 err = json.NewDecoder(r.Body).Decode(ur) 160 if err != nil { 161 log.Errorf("error decoding user request: %v", err) 162 JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest) 163 return 164 } 165 err = ur.Validate(&existingUser) 166 if err != nil { 167 log.Errorf("invalid user request received: %v", err) 168 JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest) 169 return 170 } 171 existingUser.Username = ur.Username 172 // Only users with the ModifySystem permission are able to update a 173 // user's role. This prevents a privilege escalation letting users 174 // upgrade their own account. 175 if !hasSystem && ur.Role != existingUser.Role.Slug { 176 JSONResponse(w, models.Response{Success: false, Message: ErrInsufficientPermission.Error()}, http.StatusBadRequest) 177 return 178 } 179 role, err := models.GetRoleBySlug(ur.Role) 180 if err != nil { 181 JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError) 182 return 183 } 184 // If our user is trying to change the role of an admin, we need to 185 // ensure that it isn't the last user account with the Admin role. 186 if existingUser.Role.Slug == models.RoleAdmin && existingUser.Role.ID != role.ID { 187 err = models.EnsureEnoughAdmins() 188 if err != nil { 189 JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError) 190 return 191 } 192 } 193 existingUser.Role = role 194 existingUser.RoleID = role.ID 195 // We don't force the password to be provided, since it may be an admin 196 // managing the user's account, and making a simple change like 197 // updating the username or role. However, if it _is_ provided, we'll 198 // update the stored hash after validating the new password meets our 199 // password policy. 200 // 201 // Note that we don't force the current password to be provided. The 202 // assumption here is that the API key is a proper bearer token proving 203 // authenticated access to the account. 204 existingUser.PasswordChangeRequired = ur.PasswordChangeRequired 205 if ur.Password != "" { 206 err = auth.CheckPasswordPolicy(ur.Password) 207 if err != nil { 208 JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest) 209 return 210 } 211 hash, err := auth.GeneratePasswordHash(ur.Password) 212 if err != nil { 213 JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError) 214 return 215 } 216 existingUser.Hash = hash 217 } 218 err = models.PutUser(&existingUser) 219 if err != nil { 220 JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError) 221 return 222 } 223 JSONResponse(w, existingUser, http.StatusOK) 224 } 225 }