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  }