mirror of
https://github.com/coder/coder.git
synced 2025-07-09 11:45:56 +00:00
* feat: Prevent role changing on yourself. Only allow changing roles on other users. Not much value in self changing at the moment
870 lines
25 KiB
Go
870 lines
25 KiB
Go
package coderd
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/go-chi/render"
|
|
"github.com/google/uuid"
|
|
"golang.org/x/xerrors"
|
|
|
|
"github.com/coder/coder/coderd/database"
|
|
"github.com/coder/coder/coderd/gitsshkey"
|
|
"github.com/coder/coder/coderd/httpapi"
|
|
"github.com/coder/coder/coderd/httpmw"
|
|
"github.com/coder/coder/coderd/rbac"
|
|
"github.com/coder/coder/coderd/userpassword"
|
|
"github.com/coder/coder/codersdk"
|
|
"github.com/coder/coder/cryptorand"
|
|
)
|
|
|
|
// Returns whether the initial user has been created or not.
|
|
func (api *API) firstUser(rw http.ResponseWriter, r *http.Request) {
|
|
userCount, err := api.Database.GetUserCount(r.Context())
|
|
if err != nil {
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
Message: fmt.Sprintf("get user count: %s", err.Error()),
|
|
})
|
|
return
|
|
}
|
|
|
|
if userCount == 0 {
|
|
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
|
|
Message: "The initial user has not been created!",
|
|
})
|
|
return
|
|
}
|
|
|
|
httpapi.Write(rw, http.StatusOK, httpapi.Response{
|
|
Message: "The initial user has already been created!",
|
|
})
|
|
}
|
|
|
|
// Creates the initial user for a Coder deployment.
|
|
func (api *API) postFirstUser(rw http.ResponseWriter, r *http.Request) {
|
|
var createUser codersdk.CreateFirstUserRequest
|
|
if !httpapi.Read(rw, r, &createUser) {
|
|
return
|
|
}
|
|
|
|
// This should only function for the first user.
|
|
userCount, err := api.Database.GetUserCount(r.Context())
|
|
if err != nil {
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
Message: fmt.Sprintf("get user count: %s", err.Error()),
|
|
})
|
|
return
|
|
}
|
|
|
|
// If a user already exists, the initial admin user no longer can be created.
|
|
if userCount != 0 {
|
|
httpapi.Write(rw, http.StatusConflict, httpapi.Response{
|
|
Message: "the initial user has already been created",
|
|
})
|
|
return
|
|
}
|
|
|
|
user, organizationID, err := api.createUser(r.Context(), codersdk.CreateUserRequest{
|
|
Email: createUser.Email,
|
|
Username: createUser.Username,
|
|
Password: createUser.Password,
|
|
})
|
|
if err != nil {
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
Message: err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
// TODO: @emyrk this currently happens outside the database tx used to create
|
|
// the user. Maybe I add this ability to grant roles in the createUser api
|
|
// and add some rbac bypass when calling api functions this way??
|
|
// Add the admin role to this first user.
|
|
_, err = api.Database.UpdateUserRoles(r.Context(), database.UpdateUserRolesParams{
|
|
GrantedRoles: []string{rbac.RoleAdmin(), rbac.RoleMember()},
|
|
ID: user.ID,
|
|
})
|
|
if err != nil {
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
Message: err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
httpapi.Write(rw, http.StatusCreated, codersdk.CreateFirstUserResponse{
|
|
UserID: user.ID,
|
|
OrganizationID: organizationID,
|
|
})
|
|
}
|
|
|
|
func (api *API) users(rw http.ResponseWriter, r *http.Request) {
|
|
var (
|
|
searchName = r.URL.Query().Get("search")
|
|
statusFilters = r.URL.Query().Get("status")
|
|
)
|
|
|
|
statuses := make([]database.UserStatus, 0)
|
|
|
|
if statusFilters != "" {
|
|
// Split on commas if present to account for it being a list
|
|
for _, filter := range strings.Split(statusFilters, ",") {
|
|
switch database.UserStatus(filter) {
|
|
case database.UserStatusSuspended, database.UserStatusActive:
|
|
statuses = append(statuses, database.UserStatus(filter))
|
|
default:
|
|
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
|
|
Message: fmt.Sprintf("%q is not a valid user status", filter),
|
|
})
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// Reading all users across the site.
|
|
if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceUser) {
|
|
return
|
|
}
|
|
|
|
paginationParams, ok := parsePagination(rw, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
users, err := api.Database.GetUsers(r.Context(), database.GetUsersParams{
|
|
AfterID: paginationParams.AfterID,
|
|
OffsetOpt: int32(paginationParams.Offset),
|
|
LimitOpt: int32(paginationParams.Limit),
|
|
Search: searchName,
|
|
Status: statuses,
|
|
})
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
httpapi.Write(rw, http.StatusOK, []codersdk.User{})
|
|
return
|
|
}
|
|
if err != nil {
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
Message: err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
userIDs := make([]uuid.UUID, 0, len(users))
|
|
for _, user := range users {
|
|
userIDs = append(userIDs, user.ID)
|
|
}
|
|
organizationIDsByMemberIDsRows, err := api.Database.GetOrganizationIDsByMemberIDs(r.Context(), userIDs)
|
|
if xerrors.Is(err, sql.ErrNoRows) {
|
|
err = nil
|
|
}
|
|
if err != nil {
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
Message: err.Error(),
|
|
})
|
|
return
|
|
}
|
|
organizationIDsByUserID := map[uuid.UUID][]uuid.UUID{}
|
|
for _, organizationIDsByMemberIDsRow := range organizationIDsByMemberIDsRows {
|
|
organizationIDsByUserID[organizationIDsByMemberIDsRow.UserID] = organizationIDsByMemberIDsRow.OrganizationIDs
|
|
}
|
|
|
|
render.Status(r, http.StatusOK)
|
|
render.JSON(rw, r, convertUsers(users, organizationIDsByUserID))
|
|
}
|
|
|
|
// Creates a new user.
|
|
func (api *API) postUser(rw http.ResponseWriter, r *http.Request) {
|
|
// Create the user on the site.
|
|
if !api.Authorize(rw, r, rbac.ActionCreate, rbac.ResourceUser) {
|
|
return
|
|
}
|
|
|
|
var createUser codersdk.CreateUserRequest
|
|
if !httpapi.Read(rw, r, &createUser) {
|
|
return
|
|
}
|
|
|
|
// Create the organization member in the org.
|
|
if !api.Authorize(rw, r, rbac.ActionCreate,
|
|
rbac.ResourceOrganizationMember.InOrg(createUser.OrganizationID)) {
|
|
return
|
|
}
|
|
|
|
// TODO: @emyrk Authorize the organization create if the createUser will do that.
|
|
|
|
_, err := api.Database.GetUserByEmailOrUsername(r.Context(), database.GetUserByEmailOrUsernameParams{
|
|
Username: createUser.Username,
|
|
Email: createUser.Email,
|
|
})
|
|
if err == nil {
|
|
httpapi.Write(rw, http.StatusConflict, httpapi.Response{
|
|
Message: "user already exists",
|
|
})
|
|
return
|
|
}
|
|
if !errors.Is(err, sql.ErrNoRows) {
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
Message: fmt.Sprintf("get user: %s", err),
|
|
})
|
|
return
|
|
}
|
|
|
|
_, err = api.Database.GetOrganizationByID(r.Context(), createUser.OrganizationID)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
|
|
Message: "organization does not exist with the provided id",
|
|
})
|
|
return
|
|
}
|
|
if err != nil {
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
Message: fmt.Sprintf("get organization: %s", err),
|
|
})
|
|
return
|
|
}
|
|
|
|
user, _, err := api.createUser(r.Context(), createUser)
|
|
if err != nil {
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
Message: err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
httpapi.Write(rw, http.StatusCreated, convertUser(user, []uuid.UUID{createUser.OrganizationID}))
|
|
}
|
|
|
|
// Returns the parameterized user requested. All validation
|
|
// is completed in the middleware for this route.
|
|
func (api *API) userByName(rw http.ResponseWriter, r *http.Request) {
|
|
user := httpmw.UserParam(r)
|
|
organizationIDs, err := userOrganizationIDs(r.Context(), api, user)
|
|
|
|
if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceUser.WithID(user.ID.String())) {
|
|
return
|
|
}
|
|
|
|
if err != nil {
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
Message: fmt.Sprintf("get organization IDs: %s", err.Error()),
|
|
})
|
|
return
|
|
}
|
|
|
|
httpapi.Write(rw, http.StatusOK, convertUser(user, organizationIDs))
|
|
}
|
|
|
|
func (api *API) putUserProfile(rw http.ResponseWriter, r *http.Request) {
|
|
user := httpmw.UserParam(r)
|
|
|
|
if !api.Authorize(rw, r, rbac.ActionUpdate, rbac.ResourceUser.WithID(user.ID.String())) {
|
|
return
|
|
}
|
|
|
|
var params codersdk.UpdateUserProfileRequest
|
|
if !httpapi.Read(rw, r, ¶ms) {
|
|
return
|
|
}
|
|
existentUser, err := api.Database.GetUserByEmailOrUsername(r.Context(), database.GetUserByEmailOrUsernameParams{
|
|
Username: params.Username,
|
|
})
|
|
isDifferentUser := existentUser.ID != user.ID
|
|
|
|
if err == nil && isDifferentUser {
|
|
responseErrors := []httpapi.Error{}
|
|
if existentUser.Username == params.Username {
|
|
responseErrors = append(responseErrors, httpapi.Error{
|
|
Field: "username",
|
|
Detail: "this value is already in use and should be unique",
|
|
})
|
|
}
|
|
httpapi.Write(rw, http.StatusConflict, httpapi.Response{
|
|
Message: "user already exists",
|
|
Errors: responseErrors,
|
|
})
|
|
return
|
|
}
|
|
if !errors.Is(err, sql.ErrNoRows) && isDifferentUser {
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
Message: fmt.Sprintf("get user: %s", err),
|
|
})
|
|
return
|
|
}
|
|
|
|
updatedUserProfile, err := api.Database.UpdateUserProfile(r.Context(), database.UpdateUserProfileParams{
|
|
ID: user.ID,
|
|
Email: user.Email,
|
|
Username: params.Username,
|
|
UpdatedAt: database.Now(),
|
|
})
|
|
|
|
if err != nil {
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
Message: fmt.Sprintf("patch user: %s", err.Error()),
|
|
})
|
|
return
|
|
}
|
|
|
|
organizationIDs, err := userOrganizationIDs(r.Context(), api, user)
|
|
if err != nil {
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
Message: fmt.Sprintf("get organization IDs: %s", err.Error()),
|
|
})
|
|
return
|
|
}
|
|
|
|
httpapi.Write(rw, http.StatusOK, convertUser(updatedUserProfile, organizationIDs))
|
|
}
|
|
|
|
func (api *API) putUserStatus(status database.UserStatus) func(rw http.ResponseWriter, r *http.Request) {
|
|
return func(rw http.ResponseWriter, r *http.Request) {
|
|
user := httpmw.UserParam(r)
|
|
apiKey := httpmw.APIKey(r)
|
|
|
|
if !api.Authorize(rw, r, rbac.ActionDelete, rbac.ResourceUser.WithID(user.ID.String())) {
|
|
return
|
|
}
|
|
|
|
if status == database.UserStatusSuspended && user.ID == apiKey.UserID {
|
|
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
|
|
Message: "You cannot suspend yourself",
|
|
})
|
|
return
|
|
}
|
|
|
|
suspendedUser, err := api.Database.UpdateUserStatus(r.Context(), database.UpdateUserStatusParams{
|
|
ID: user.ID,
|
|
Status: status,
|
|
UpdatedAt: database.Now(),
|
|
})
|
|
|
|
if err != nil {
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
Message: fmt.Sprintf("put user suspended: %s", err.Error()),
|
|
})
|
|
return
|
|
}
|
|
|
|
organizations, err := userOrganizationIDs(r.Context(), api, user)
|
|
if err != nil {
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
Message: fmt.Sprintf("get organization IDs: %s", err.Error()),
|
|
})
|
|
return
|
|
}
|
|
|
|
httpapi.Write(rw, http.StatusOK, convertUser(suspendedUser, organizations))
|
|
}
|
|
}
|
|
|
|
func (api *API) putUserPassword(rw http.ResponseWriter, r *http.Request) {
|
|
var (
|
|
user = httpmw.UserParam(r)
|
|
apiKey = httpmw.APIKey(r)
|
|
params codersdk.UpdateUserPasswordRequest
|
|
)
|
|
|
|
if !api.Authorize(rw, r, rbac.ActionUpdate, rbac.ResourceUserData.WithOwner(user.ID.String())) {
|
|
return
|
|
}
|
|
|
|
if !httpapi.Read(rw, r, ¶ms) {
|
|
return
|
|
}
|
|
|
|
err := userpassword.Validate(params.Password)
|
|
if err != nil {
|
|
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
|
|
Errors: []httpapi.Error{
|
|
{
|
|
Field: "password",
|
|
Detail: err.Error(),
|
|
},
|
|
},
|
|
})
|
|
return
|
|
}
|
|
|
|
// we want to require old_password field if the user is changing their
|
|
// own password. This is to prevent a compromised session from being able
|
|
// to change password and lock out the user.
|
|
if user.ID == apiKey.UserID {
|
|
ok, err := userpassword.Compare(string(user.HashedPassword), params.OldPassword)
|
|
if err != nil {
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
Message: fmt.Sprintf("compare user password: %s", err.Error()),
|
|
})
|
|
return
|
|
}
|
|
if !ok {
|
|
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
|
|
Errors: []httpapi.Error{
|
|
{
|
|
Field: "old_password",
|
|
Detail: "Old password is incorrect.",
|
|
},
|
|
},
|
|
})
|
|
return
|
|
}
|
|
}
|
|
|
|
hashedPassword, err := userpassword.Hash(params.Password)
|
|
if err != nil {
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
Message: fmt.Sprintf("hash password: %s", err.Error()),
|
|
})
|
|
return
|
|
}
|
|
err = api.Database.UpdateUserHashedPassword(r.Context(), database.UpdateUserHashedPasswordParams{
|
|
ID: user.ID,
|
|
HashedPassword: []byte(hashedPassword),
|
|
})
|
|
if err != nil {
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
Message: fmt.Sprintf("put user password: %s", err.Error()),
|
|
})
|
|
return
|
|
}
|
|
|
|
httpapi.Write(rw, http.StatusNoContent, nil)
|
|
}
|
|
|
|
func (api *API) userRoles(rw http.ResponseWriter, r *http.Request) {
|
|
user := httpmw.UserParam(r)
|
|
|
|
if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceUserData.
|
|
WithOwner(user.ID.String())) {
|
|
return
|
|
}
|
|
|
|
resp := codersdk.UserRoles{
|
|
Roles: user.RBACRoles,
|
|
OrganizationRoles: make(map[uuid.UUID][]string),
|
|
}
|
|
|
|
memberships, err := api.Database.GetOrganizationMembershipsByUserID(r.Context(), user.ID)
|
|
if err != nil {
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
Message: fmt.Sprintf("get user memberships: %s", err),
|
|
})
|
|
return
|
|
}
|
|
|
|
// Only include ones we can read from RBAC.
|
|
memberships = AuthorizeFilter(api, r, rbac.ActionRead, memberships)
|
|
|
|
for _, mem := range memberships {
|
|
// If we can read the org member, include the roles.
|
|
if err == nil {
|
|
resp.OrganizationRoles[mem.OrganizationID] = mem.Roles
|
|
}
|
|
}
|
|
|
|
httpapi.Write(rw, http.StatusOK, resp)
|
|
}
|
|
|
|
func (api *API) putUserRoles(rw http.ResponseWriter, r *http.Request) {
|
|
// User is the user to modify.
|
|
user := httpmw.UserParam(r)
|
|
roles := httpmw.UserRoles(r)
|
|
apiKey := httpmw.APIKey(r)
|
|
|
|
if apiKey.UserID == user.ID {
|
|
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
|
|
Message: "You cannot change your own roles.",
|
|
})
|
|
return
|
|
}
|
|
|
|
var params codersdk.UpdateRoles
|
|
if !httpapi.Read(rw, r, ¶ms) {
|
|
return
|
|
}
|
|
|
|
added, removed := rbac.ChangeRoleSet(roles.Roles, params.Roles)
|
|
for _, roleName := range added {
|
|
// Assigning a role requires the create permission.
|
|
if !api.Authorize(rw, r, rbac.ActionCreate, rbac.ResourceRoleAssignment.WithID(roleName)) {
|
|
return
|
|
}
|
|
}
|
|
for _, roleName := range removed {
|
|
// Removing a role requires the delete permission.
|
|
if !api.Authorize(rw, r, rbac.ActionDelete, rbac.ResourceRoleAssignment.WithID(roleName)) {
|
|
return
|
|
}
|
|
}
|
|
|
|
updatedUser, err := api.updateSiteUserRoles(r.Context(), database.UpdateUserRolesParams{
|
|
GrantedRoles: params.Roles,
|
|
ID: user.ID,
|
|
})
|
|
if err != nil {
|
|
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
|
|
Message: err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
organizationIDs, err := userOrganizationIDs(r.Context(), api, user)
|
|
if err != nil {
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
Message: fmt.Sprintf("get organization IDs: %s", err.Error()),
|
|
})
|
|
return
|
|
}
|
|
|
|
httpapi.Write(rw, http.StatusOK, convertUser(updatedUser, organizationIDs))
|
|
}
|
|
|
|
// updateSiteUserRoles will ensure only site wide roles are passed in as arguments.
|
|
// If an organization role is included, an error is returned.
|
|
func (api *API) updateSiteUserRoles(ctx context.Context, args database.UpdateUserRolesParams) (database.User, error) {
|
|
// Enforce only site wide roles.
|
|
for _, r := range args.GrantedRoles {
|
|
if _, ok := rbac.IsOrgRole(r); ok {
|
|
return database.User{}, xerrors.Errorf("must only update site wide roles")
|
|
}
|
|
|
|
if _, err := rbac.RoleByName(r); err != nil {
|
|
return database.User{}, xerrors.Errorf("%q is not a supported role", r)
|
|
}
|
|
}
|
|
|
|
updatedUser, err := api.Database.UpdateUserRoles(ctx, args)
|
|
if err != nil {
|
|
return database.User{}, xerrors.Errorf("update site roles: %w", err)
|
|
}
|
|
return updatedUser, nil
|
|
}
|
|
|
|
// Returns organizations the parameterized user has access to.
|
|
func (api *API) organizationsByUser(rw http.ResponseWriter, r *http.Request) {
|
|
user := httpmw.UserParam(r)
|
|
|
|
organizations, err := api.Database.GetOrganizationsByUserID(r.Context(), user.ID)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
err = nil
|
|
organizations = []database.Organization{}
|
|
}
|
|
if err != nil {
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
Message: fmt.Sprintf("get organizations: %s", err.Error()),
|
|
})
|
|
return
|
|
}
|
|
|
|
// Only return orgs the user can read.
|
|
organizations = AuthorizeFilter(api, r, rbac.ActionRead, organizations)
|
|
|
|
publicOrganizations := make([]codersdk.Organization, 0, len(organizations))
|
|
for _, organization := range organizations {
|
|
publicOrganizations = append(publicOrganizations, convertOrganization(organization))
|
|
}
|
|
|
|
httpapi.Write(rw, http.StatusOK, publicOrganizations)
|
|
}
|
|
|
|
func (api *API) organizationByUserAndName(rw http.ResponseWriter, r *http.Request) {
|
|
organizationName := chi.URLParam(r, "organizationname")
|
|
organization, err := api.Database.GetOrganizationByName(r.Context(), organizationName)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
// Return unauthorized rather than a 404 to not leak if the organization
|
|
// exists.
|
|
httpapi.Forbidden(rw)
|
|
return
|
|
}
|
|
if err != nil {
|
|
httpapi.Forbidden(rw)
|
|
return
|
|
}
|
|
|
|
if !api.Authorize(rw, r, rbac.ActionRead,
|
|
rbac.ResourceOrganization.
|
|
InOrg(organization.ID).
|
|
WithID(organization.ID.String())) {
|
|
return
|
|
}
|
|
|
|
httpapi.Write(rw, http.StatusOK, convertOrganization(organization))
|
|
}
|
|
|
|
// Authenticates the user with an email and password.
|
|
func (api *API) postLogin(rw http.ResponseWriter, r *http.Request) {
|
|
var loginWithPassword codersdk.LoginWithPasswordRequest
|
|
if !httpapi.Read(rw, r, &loginWithPassword) {
|
|
return
|
|
}
|
|
|
|
user, err := api.Database.GetUserByEmailOrUsername(r.Context(), database.GetUserByEmailOrUsernameParams{
|
|
Email: loginWithPassword.Email,
|
|
})
|
|
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
Message: fmt.Sprintf("get user: %s", err.Error()),
|
|
})
|
|
return
|
|
}
|
|
|
|
// If the user doesn't exist, it will be a default struct.
|
|
equal, err := userpassword.Compare(string(user.HashedPassword), loginWithPassword.Password)
|
|
if err != nil {
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
Message: fmt.Sprintf("compare: %s", err.Error()),
|
|
})
|
|
}
|
|
if !equal {
|
|
// This message is the same as above to remove ease in detecting whether
|
|
// users are registered or not. Attackers still could with a timing attack.
|
|
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
|
|
Message: "Incorrect email or password.",
|
|
})
|
|
return
|
|
}
|
|
|
|
// If the user logged into a suspended account, reject the login request.
|
|
if user.Status != database.UserStatusActive {
|
|
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
|
|
Message: "You are suspended, contact an admin to reactivate your account",
|
|
})
|
|
return
|
|
}
|
|
|
|
sessionToken, created := api.createAPIKey(rw, r, database.InsertAPIKeyParams{
|
|
UserID: user.ID,
|
|
LoginType: database.LoginTypePassword,
|
|
})
|
|
if !created {
|
|
return
|
|
}
|
|
|
|
httpapi.Write(rw, http.StatusCreated, codersdk.LoginWithPasswordResponse{
|
|
SessionToken: sessionToken,
|
|
})
|
|
}
|
|
|
|
// Creates a new session key, used for logging in via the CLI.
|
|
func (api *API) postAPIKey(rw http.ResponseWriter, r *http.Request) {
|
|
user := httpmw.UserParam(r)
|
|
|
|
if !api.Authorize(rw, r, rbac.ActionCreate, rbac.ResourceAPIKey.WithOwner(user.ID.String())) {
|
|
return
|
|
}
|
|
|
|
sessionToken, created := api.createAPIKey(rw, r, database.InsertAPIKeyParams{
|
|
UserID: user.ID,
|
|
LoginType: database.LoginTypePassword,
|
|
})
|
|
if !created {
|
|
return
|
|
}
|
|
|
|
httpapi.Write(rw, http.StatusCreated, codersdk.GenerateAPIKeyResponse{Key: sessionToken})
|
|
}
|
|
|
|
// Clear the user's session cookie.
|
|
func (api *API) postLogout(rw http.ResponseWriter, r *http.Request) {
|
|
// Get a blank token cookie.
|
|
cookie := &http.Cookie{
|
|
// MaxAge < 0 means to delete the cookie now.
|
|
MaxAge: -1,
|
|
Name: httpmw.SessionTokenKey,
|
|
Path: "/",
|
|
}
|
|
|
|
http.SetCookie(rw, cookie)
|
|
|
|
// Delete the session token from database.
|
|
apiKey := httpmw.APIKey(r)
|
|
err := api.Database.DeleteAPIKeyByID(r.Context(), apiKey.ID)
|
|
if err != nil {
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
Message: fmt.Sprintf("delete api key: %s", err.Error()),
|
|
})
|
|
return
|
|
}
|
|
|
|
httpapi.Write(rw, http.StatusOK, httpapi.Response{
|
|
Message: "Logged out!",
|
|
})
|
|
}
|
|
|
|
// Generates a new ID and secret for an API key.
|
|
func generateAPIKeyIDSecret() (id string, secret string, err error) {
|
|
// Length of an API Key ID.
|
|
id, err = cryptorand.String(10)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
// Length of an API Key secret.
|
|
secret, err = cryptorand.String(22)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
return id, secret, nil
|
|
}
|
|
|
|
func (api *API) createAPIKey(rw http.ResponseWriter, r *http.Request, params database.InsertAPIKeyParams) (string, bool) {
|
|
keyID, keySecret, err := generateAPIKeyIDSecret()
|
|
if err != nil {
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
Message: fmt.Sprintf("generate api key parts: %s", err.Error()),
|
|
})
|
|
return "", false
|
|
}
|
|
hashed := sha256.Sum256([]byte(keySecret))
|
|
|
|
_, err = api.Database.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
|
|
ID: keyID,
|
|
UserID: params.UserID,
|
|
ExpiresAt: database.Now().Add(24 * time.Hour),
|
|
CreatedAt: database.Now(),
|
|
UpdatedAt: database.Now(),
|
|
HashedSecret: hashed[:],
|
|
LoginType: params.LoginType,
|
|
OAuthAccessToken: params.OAuthAccessToken,
|
|
OAuthRefreshToken: params.OAuthRefreshToken,
|
|
OAuthIDToken: params.OAuthIDToken,
|
|
OAuthExpiry: params.OAuthExpiry,
|
|
})
|
|
if err != nil {
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
Message: fmt.Sprintf("insert api key: %s", err.Error()),
|
|
})
|
|
return "", false
|
|
}
|
|
|
|
// This format is consumed by the APIKey middleware.
|
|
sessionToken := fmt.Sprintf("%s-%s", keyID, keySecret)
|
|
http.SetCookie(rw, &http.Cookie{
|
|
Name: httpmw.SessionTokenKey,
|
|
Value: sessionToken,
|
|
Path: "/",
|
|
HttpOnly: true,
|
|
SameSite: http.SameSiteLaxMode,
|
|
Secure: api.SecureAuthCookie,
|
|
})
|
|
return sessionToken, true
|
|
}
|
|
|
|
func (api *API) createUser(ctx context.Context, req codersdk.CreateUserRequest) (database.User, uuid.UUID, error) {
|
|
var user database.User
|
|
return user, req.OrganizationID, api.Database.InTx(func(db database.Store) error {
|
|
var orgRoles []string
|
|
// If no organization is provided, create a new one for the user.
|
|
if req.OrganizationID == uuid.Nil {
|
|
organization, err := db.InsertOrganization(ctx, database.InsertOrganizationParams{
|
|
ID: uuid.New(),
|
|
Name: req.Username,
|
|
CreatedAt: database.Now(),
|
|
UpdatedAt: database.Now(),
|
|
})
|
|
if err != nil {
|
|
return xerrors.Errorf("create organization: %w", err)
|
|
}
|
|
req.OrganizationID = organization.ID
|
|
orgRoles = append(orgRoles, rbac.RoleOrgAdmin(req.OrganizationID))
|
|
}
|
|
// Always also be a member.
|
|
orgRoles = append(orgRoles, rbac.RoleOrgMember(req.OrganizationID))
|
|
|
|
params := database.InsertUserParams{
|
|
ID: uuid.New(),
|
|
Email: req.Email,
|
|
Username: req.Username,
|
|
CreatedAt: database.Now(),
|
|
UpdatedAt: database.Now(),
|
|
// All new users are defaulted to members of the site.
|
|
RBACRoles: []string{rbac.RoleMember()},
|
|
}
|
|
// If a user signs up with OAuth, they can have no password!
|
|
if req.Password != "" {
|
|
hashedPassword, err := userpassword.Hash(req.Password)
|
|
if err != nil {
|
|
return xerrors.Errorf("hash password: %w", err)
|
|
}
|
|
params.HashedPassword = []byte(hashedPassword)
|
|
}
|
|
|
|
var err error
|
|
user, err = db.InsertUser(ctx, params)
|
|
if err != nil {
|
|
return xerrors.Errorf("create user: %w", err)
|
|
}
|
|
|
|
privateKey, publicKey, err := gitsshkey.Generate(api.SSHKeygenAlgorithm)
|
|
if err != nil {
|
|
return xerrors.Errorf("generate user gitsshkey: %w", err)
|
|
}
|
|
_, err = db.InsertGitSSHKey(ctx, database.InsertGitSSHKeyParams{
|
|
UserID: user.ID,
|
|
CreatedAt: database.Now(),
|
|
UpdatedAt: database.Now(),
|
|
PrivateKey: privateKey,
|
|
PublicKey: publicKey,
|
|
})
|
|
if err != nil {
|
|
return xerrors.Errorf("insert user gitsshkey: %w", err)
|
|
}
|
|
_, err = db.InsertOrganizationMember(ctx, database.InsertOrganizationMemberParams{
|
|
OrganizationID: req.OrganizationID,
|
|
UserID: user.ID,
|
|
CreatedAt: database.Now(),
|
|
UpdatedAt: database.Now(),
|
|
// By default give them membership to the organization.
|
|
Roles: orgRoles,
|
|
})
|
|
if err != nil {
|
|
return xerrors.Errorf("create organization member: %w", err)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func convertUser(user database.User, organizationIDs []uuid.UUID) codersdk.User {
|
|
convertedUser := codersdk.User{
|
|
ID: user.ID,
|
|
Email: user.Email,
|
|
CreatedAt: user.CreatedAt,
|
|
Username: user.Username,
|
|
Status: codersdk.UserStatus(user.Status),
|
|
OrganizationIDs: organizationIDs,
|
|
Roles: make([]codersdk.Role, 0),
|
|
}
|
|
|
|
for _, roleName := range user.RBACRoles {
|
|
rbacRole, _ := rbac.RoleByName(roleName)
|
|
convertedUser.Roles = append(convertedUser.Roles, convertRole(rbacRole))
|
|
}
|
|
|
|
return convertedUser
|
|
}
|
|
|
|
func convertUsers(users []database.User, organizationIDsByUserID map[uuid.UUID][]uuid.UUID) []codersdk.User {
|
|
converted := make([]codersdk.User, 0, len(users))
|
|
for _, u := range users {
|
|
userOrganizationIDs := organizationIDsByUserID[u.ID]
|
|
converted = append(converted, convertUser(u, userOrganizationIDs))
|
|
}
|
|
return converted
|
|
}
|
|
|
|
func userOrganizationIDs(ctx context.Context, api *API, user database.User) ([]uuid.UUID, error) {
|
|
organizationIDsByMemberIDsRows, err := api.Database.GetOrganizationIDsByMemberIDs(ctx, []uuid.UUID{user.ID})
|
|
if errors.Is(err, sql.ErrNoRows) || len(organizationIDsByMemberIDsRows) == 0 {
|
|
return []uuid.UUID{}, nil
|
|
}
|
|
if err != nil {
|
|
return []uuid.UUID{}, err
|
|
}
|
|
member := organizationIDsByMemberIDsRows[0]
|
|
return member.OrganizationIDs, nil
|
|
}
|