2022-01-20 13:46:51 +00:00
|
|
|
package coderd
|
|
|
|
|
|
|
|
import (
|
2022-08-25 19:32:35 +00:00
|
|
|
"bytes"
|
2022-04-23 22:58:57 +00:00
|
|
|
"context"
|
2022-01-20 13:46:51 +00:00
|
|
|
"crypto/sha256"
|
|
|
|
"database/sql"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
2022-06-22 17:32:21 +00:00
|
|
|
"net"
|
2022-01-20 13:46:51 +00:00
|
|
|
"net/http"
|
2022-06-24 15:02:23 +00:00
|
|
|
"net/url"
|
2022-08-25 19:32:35 +00:00
|
|
|
"os"
|
2022-05-31 13:06:42 +00:00
|
|
|
"strings"
|
2022-01-20 13:46:51 +00:00
|
|
|
"time"
|
|
|
|
|
2022-03-07 17:40:54 +00:00
|
|
|
"github.com/go-chi/chi/v5"
|
2022-04-22 20:27:55 +00:00
|
|
|
"github.com/go-chi/render"
|
2022-01-20 13:46:51 +00:00
|
|
|
"github.com/google/uuid"
|
2022-06-22 17:32:21 +00:00
|
|
|
"github.com/tabbed/pqtype"
|
2022-01-23 05:58:10 +00:00
|
|
|
"golang.org/x/xerrors"
|
2022-01-20 13:46:51 +00:00
|
|
|
|
2022-08-25 19:32:35 +00:00
|
|
|
"cdr.dev/slog"
|
2022-09-09 02:16:16 +00:00
|
|
|
"github.com/coder/coder/coderd/audit"
|
2022-03-25 21:07:45 +00:00
|
|
|
"github.com/coder/coder/coderd/database"
|
2022-04-06 00:18:26 +00:00
|
|
|
"github.com/coder/coder/coderd/gitsshkey"
|
2022-03-25 21:07:45 +00:00
|
|
|
"github.com/coder/coder/coderd/httpapi"
|
|
|
|
"github.com/coder/coder/coderd/httpmw"
|
2022-04-29 14:04:19 +00:00
|
|
|
"github.com/coder/coder/coderd/rbac"
|
2022-06-17 05:26:40 +00:00
|
|
|
"github.com/coder/coder/coderd/telemetry"
|
2022-01-20 13:46:51 +00:00
|
|
|
"github.com/coder/coder/coderd/userpassword"
|
2022-08-31 15:26:36 +00:00
|
|
|
"github.com/coder/coder/coderd/util/slice"
|
2022-03-22 19:17:50 +00:00
|
|
|
"github.com/coder/coder/codersdk"
|
2022-01-20 13:46:51 +00:00
|
|
|
"github.com/coder/coder/cryptorand"
|
2022-08-25 19:32:35 +00:00
|
|
|
"github.com/coder/coder/examples"
|
2022-01-20 13:46:51 +00:00
|
|
|
)
|
|
|
|
|
2022-02-10 14:33:27 +00:00
|
|
|
// Returns whether the initial user has been created or not.
|
2022-05-26 03:14:08 +00:00
|
|
|
func (api *API) firstUser(rw http.ResponseWriter, r *http.Request) {
|
2022-02-10 14:33:27 +00:00
|
|
|
userCount, err := api.Database.GetUserCount(r.Context())
|
|
|
|
if err != nil {
|
2022-07-13 00:15:02 +00:00
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
|
2022-06-07 14:33:06 +00:00
|
|
|
Message: "Internal error fetching user count.",
|
2022-06-03 21:48:09 +00:00
|
|
|
Detail: err.Error(),
|
2022-02-10 14:33:27 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-04-01 19:42:36 +00:00
|
|
|
|
2022-02-10 14:33:27 +00:00
|
|
|
if userCount == 0 {
|
2022-07-13 00:15:02 +00:00
|
|
|
httpapi.Write(rw, http.StatusNotFound, codersdk.Response{
|
2022-02-10 14:33:27 +00:00
|
|
|
Message: "The initial user has not been created!",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-04-01 19:42:36 +00:00
|
|
|
|
2022-07-13 00:15:02 +00:00
|
|
|
httpapi.Write(rw, http.StatusOK, codersdk.Response{
|
2022-02-10 14:33:27 +00:00
|
|
|
Message: "The initial user has already been created!",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-01-20 13:46:51 +00:00
|
|
|
// Creates the initial user for a Coder deployment.
|
2022-05-26 03:14:08 +00:00
|
|
|
func (api *API) postFirstUser(rw http.ResponseWriter, r *http.Request) {
|
2022-03-22 19:17:50 +00:00
|
|
|
var createUser codersdk.CreateFirstUserRequest
|
2022-01-20 13:46:51 +00:00
|
|
|
if !httpapi.Read(rw, r, &createUser) {
|
|
|
|
return
|
|
|
|
}
|
2022-04-01 19:42:36 +00:00
|
|
|
|
2022-01-20 13:46:51 +00:00
|
|
|
// This should only function for the first user.
|
2022-02-01 22:15:26 +00:00
|
|
|
userCount, err := api.Database.GetUserCount(r.Context())
|
2022-01-20 13:46:51 +00:00
|
|
|
if err != nil {
|
2022-07-13 00:15:02 +00:00
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
|
2022-06-07 14:33:06 +00:00
|
|
|
Message: "Internal error fetching user count.",
|
2022-06-03 21:48:09 +00:00
|
|
|
Detail: err.Error(),
|
2022-01-20 13:46:51 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-04-01 19:42:36 +00:00
|
|
|
|
2022-01-20 13:46:51 +00:00
|
|
|
// If a user already exists, the initial admin user no longer can be created.
|
|
|
|
if userCount != 0 {
|
2022-07-13 00:15:02 +00:00
|
|
|
httpapi.Write(rw, http.StatusConflict, codersdk.Response{
|
2022-06-07 14:33:06 +00:00
|
|
|
Message: "The initial user has already been created.",
|
2022-01-20 13:46:51 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-04-01 19:42:36 +00:00
|
|
|
|
2022-09-20 20:16:26 +00:00
|
|
|
user, organizationID, err := api.CreateUser(r.Context(), api.Database, CreateUserRequest{
|
2022-08-17 23:00:53 +00:00
|
|
|
CreateUserRequest: codersdk.CreateUserRequest{
|
|
|
|
Email: createUser.Email,
|
|
|
|
Username: createUser.Username,
|
|
|
|
Password: createUser.Password,
|
2022-08-25 19:32:35 +00:00
|
|
|
// Create an org for the first user.
|
|
|
|
OrganizationID: uuid.Nil,
|
2022-08-17 23:00:53 +00:00
|
|
|
},
|
|
|
|
LoginType: database.LoginTypePassword,
|
2022-01-20 13:46:51 +00:00
|
|
|
})
|
|
|
|
if err != nil {
|
2022-07-13 00:15:02 +00:00
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
|
2022-06-07 14:33:06 +00:00
|
|
|
Message: "Internal error creating user.",
|
2022-06-03 21:48:09 +00:00
|
|
|
Detail: err.Error(),
|
2022-01-20 13:46:51 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-01-23 05:58:10 +00:00
|
|
|
|
2022-06-17 05:26:40 +00:00
|
|
|
telemetryUser := telemetry.ConvertUser(user)
|
|
|
|
// Send the initial users email address!
|
|
|
|
telemetryUser.Email = &user.Email
|
|
|
|
api.Telemetry.Report(&telemetry.Snapshot{
|
|
|
|
Users: []telemetry.User{telemetryUser},
|
|
|
|
})
|
|
|
|
|
2022-04-29 14:04:19 +00:00
|
|
|
// 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??
|
2022-05-27 20:47:03 +00:00
|
|
|
// Add the admin role to this first user.
|
2022-04-29 14:04:19 +00:00
|
|
|
_, err = api.Database.UpdateUserRoles(r.Context(), database.UpdateUserRolesParams{
|
2022-08-15 19:40:19 +00:00
|
|
|
GrantedRoles: []string{rbac.RoleOwner()},
|
2022-04-29 14:04:19 +00:00
|
|
|
ID: user.ID,
|
|
|
|
})
|
|
|
|
if err != nil {
|
2022-07-13 00:15:02 +00:00
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
|
2022-06-07 14:33:06 +00:00
|
|
|
Message: "Internal error updating user's roles.",
|
2022-06-03 21:48:09 +00:00
|
|
|
Detail: err.Error(),
|
2022-04-29 14:04:19 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-08-25 19:32:35 +00:00
|
|
|
// Auto-import any designated templates into the new organization.
|
|
|
|
for _, template := range api.AutoImportTemplates {
|
|
|
|
archive, err := examples.Archive(string(template))
|
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
|
|
|
|
Message: "Internal error importing template.",
|
|
|
|
Detail: xerrors.Errorf("load template archive for %q: %w", template, err).Error(),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Determine which parameter values to use.
|
|
|
|
parameters := map[string]string{}
|
|
|
|
switch template {
|
|
|
|
case AutoImportTemplateKubernetes:
|
|
|
|
|
|
|
|
// Determine the current namespace we're in.
|
|
|
|
const namespaceFile = "/var/run/secrets/kubernetes.io/serviceaccount/namespace"
|
|
|
|
namespace, err := os.ReadFile(namespaceFile)
|
|
|
|
if err != nil {
|
|
|
|
parameters["use_kubeconfig"] = "true" // use ~/.config/kubeconfig
|
|
|
|
parameters["namespace"] = "coder-workspaces"
|
|
|
|
} else {
|
|
|
|
parameters["use_kubeconfig"] = "false" // use SA auth
|
|
|
|
parameters["namespace"] = string(bytes.TrimSpace(namespace))
|
|
|
|
}
|
|
|
|
|
|
|
|
default:
|
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
|
|
|
|
Message: "Internal error importing template.",
|
|
|
|
Detail: fmt.Sprintf("cannot auto-import %q template", template),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
tpl, err := api.autoImportTemplate(r.Context(), autoImportTemplateOpts{
|
|
|
|
name: string(template),
|
|
|
|
archive: archive,
|
|
|
|
params: parameters,
|
|
|
|
userID: user.ID,
|
|
|
|
orgID: organizationID,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
api.Logger.Warn(r.Context(), "failed to auto-import template", slog.F("template", template), slog.F("parameters", parameters), slog.Error(err))
|
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
|
|
|
|
Message: "Internal error importing template.",
|
|
|
|
Detail: xerrors.Errorf("failed to import template %q: %w", template, err).Error(),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
api.Logger.Info(r.Context(), "auto-imported template", slog.F("id", tpl.ID), slog.F("template", template), slog.F("parameters", parameters))
|
|
|
|
}
|
|
|
|
|
2022-04-12 15:17:33 +00:00
|
|
|
httpapi.Write(rw, http.StatusCreated, codersdk.CreateFirstUserResponse{
|
2022-03-07 17:40:54 +00:00
|
|
|
UserID: user.ID,
|
2022-04-23 22:58:57 +00:00
|
|
|
OrganizationID: organizationID,
|
2022-03-07 17:40:54 +00:00
|
|
|
})
|
2022-01-25 19:52:58 +00:00
|
|
|
}
|
|
|
|
|
2022-05-26 03:14:08 +00:00
|
|
|
func (api *API) users(rw http.ResponseWriter, r *http.Request) {
|
2022-06-24 15:02:23 +00:00
|
|
|
query := r.URL.Query().Get("q")
|
|
|
|
params, errs := userSearchQuery(query)
|
|
|
|
if len(errs) > 0 {
|
2022-07-13 00:15:02 +00:00
|
|
|
httpapi.Write(rw, http.StatusBadRequest, codersdk.Response{
|
2022-06-24 15:02:23 +00:00
|
|
|
Message: "Invalid user search query.",
|
|
|
|
Validations: errs,
|
|
|
|
})
|
2022-06-24 23:55:28 +00:00
|
|
|
return
|
2022-05-17 18:43:19 +00:00
|
|
|
}
|
|
|
|
|
2022-05-10 07:44:09 +00:00
|
|
|
paginationParams, ok := parsePagination(rw, r)
|
|
|
|
if !ok {
|
2022-04-22 20:27:55 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
users, err := api.Database.GetUsers(r.Context(), database.GetUsersParams{
|
2022-05-10 07:44:09 +00:00
|
|
|
AfterID: paginationParams.AfterID,
|
|
|
|
OffsetOpt: int32(paginationParams.Offset),
|
|
|
|
LimitOpt: int32(paginationParams.Limit),
|
2022-06-24 15:02:23 +00:00
|
|
|
Search: params.Search,
|
|
|
|
Status: params.Status,
|
|
|
|
RbacRole: params.RbacRole,
|
2022-04-22 20:27:55 +00:00
|
|
|
})
|
2022-05-10 07:44:09 +00:00
|
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
|
|
httpapi.Write(rw, http.StatusOK, []codersdk.User{})
|
|
|
|
return
|
|
|
|
}
|
2022-04-28 14:10:17 +00:00
|
|
|
if err != nil {
|
2022-07-13 00:15:02 +00:00
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
|
2022-06-07 14:33:06 +00:00
|
|
|
Message: "Internal error fetching users.",
|
2022-06-03 21:48:09 +00:00
|
|
|
Detail: err.Error(),
|
2022-04-28 14:10:17 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-04-22 20:27:55 +00:00
|
|
|
|
2022-09-20 04:11:01 +00:00
|
|
|
users, err = AuthorizeFilter(api.HTTPAuth, r, rbac.ActionRead, users)
|
2022-08-11 22:07:48 +00:00
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
|
|
|
|
Message: "Internal error fetching users.",
|
|
|
|
Detail: err.Error(),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-04-28 14:10:17 +00:00
|
|
|
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
|
|
|
|
}
|
2022-04-22 20:27:55 +00:00
|
|
|
if err != nil {
|
2022-07-13 00:15:02 +00:00
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
|
2022-06-07 14:33:06 +00:00
|
|
|
Message: "Internal error fetching user's organizations.",
|
2022-06-03 21:48:09 +00:00
|
|
|
Detail: err.Error(),
|
2022-04-22 20:27:55 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-04-28 14:10:17 +00:00
|
|
|
organizationIDsByUserID := map[uuid.UUID][]uuid.UUID{}
|
|
|
|
for _, organizationIDsByMemberIDsRow := range organizationIDsByMemberIDsRows {
|
|
|
|
organizationIDsByUserID[organizationIDsByMemberIDsRow.UserID] = organizationIDsByMemberIDsRow.OrganizationIDs
|
|
|
|
}
|
2022-04-22 20:27:55 +00:00
|
|
|
|
|
|
|
render.Status(r, http.StatusOK)
|
2022-04-28 14:10:17 +00:00
|
|
|
render.JSON(rw, r, convertUsers(users, organizationIDsByUserID))
|
2022-04-22 20:27:55 +00:00
|
|
|
}
|
|
|
|
|
2022-01-25 19:52:58 +00:00
|
|
|
// Creates a new user.
|
2022-05-26 03:14:08 +00:00
|
|
|
func (api *API) postUser(rw http.ResponseWriter, r *http.Request) {
|
2022-09-20 04:11:01 +00:00
|
|
|
auditor := *api.Auditor.Load()
|
2022-09-09 02:16:16 +00:00
|
|
|
aReq, commitAudit := audit.InitRequest[database.User](rw, &audit.RequestParams{
|
2022-09-20 04:11:01 +00:00
|
|
|
Audit: auditor,
|
|
|
|
Log: api.Logger,
|
|
|
|
Request: r,
|
|
|
|
Action: database.AuditActionCreate,
|
2022-09-09 02:16:16 +00:00
|
|
|
})
|
|
|
|
defer commitAudit()
|
|
|
|
|
2022-05-27 20:47:03 +00:00
|
|
|
// Create the user on the site.
|
2022-06-14 15:14:05 +00:00
|
|
|
if !api.Authorize(r, rbac.ActionCreate, rbac.ResourceUser) {
|
|
|
|
httpapi.Forbidden(rw)
|
2022-05-17 18:43:19 +00:00
|
|
|
return
|
|
|
|
}
|
2022-03-07 17:40:54 +00:00
|
|
|
|
2022-08-17 23:00:53 +00:00
|
|
|
var req codersdk.CreateUserRequest
|
|
|
|
if !httpapi.Read(rw, r, &req) {
|
2022-01-25 19:52:58 +00:00
|
|
|
return
|
|
|
|
}
|
2022-05-17 18:43:19 +00:00
|
|
|
|
|
|
|
// Create the organization member in the org.
|
2022-06-14 15:14:05 +00:00
|
|
|
if !api.Authorize(r, rbac.ActionCreate,
|
2022-08-17 23:00:53 +00:00
|
|
|
rbac.ResourceOrganizationMember.InOrg(req.OrganizationID)) {
|
2022-06-14 15:14:05 +00:00
|
|
|
httpapi.ResourceNotFound(rw)
|
2022-05-17 18:43:19 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: @emyrk Authorize the organization create if the createUser will do that.
|
|
|
|
|
2022-02-01 22:15:26 +00:00
|
|
|
_, err := api.Database.GetUserByEmailOrUsername(r.Context(), database.GetUserByEmailOrUsernameParams{
|
2022-08-17 23:00:53 +00:00
|
|
|
Username: req.Username,
|
|
|
|
Email: req.Email,
|
2022-01-25 19:52:58 +00:00
|
|
|
})
|
|
|
|
if err == nil {
|
2022-07-13 00:15:02 +00:00
|
|
|
httpapi.Write(rw, http.StatusConflict, codersdk.Response{
|
2022-06-07 14:33:06 +00:00
|
|
|
Message: "User already exists.",
|
2022-01-25 19:52:58 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if !errors.Is(err, sql.ErrNoRows) {
|
2022-07-13 00:15:02 +00:00
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
|
2022-06-07 14:33:06 +00:00
|
|
|
Message: "Internal error fetching user.",
|
2022-06-03 21:48:09 +00:00
|
|
|
Detail: err.Error(),
|
2022-01-25 19:52:58 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-08-17 23:00:53 +00:00
|
|
|
_, err = api.Database.GetOrganizationByID(r.Context(), req.OrganizationID)
|
2022-03-07 17:40:54 +00:00
|
|
|
if errors.Is(err, sql.ErrNoRows) {
|
2022-07-13 00:15:02 +00:00
|
|
|
httpapi.Write(rw, http.StatusNotFound, codersdk.Response{
|
2022-08-17 23:00:53 +00:00
|
|
|
Message: fmt.Sprintf("Organization does not exist with the provided id %q.", req.OrganizationID),
|
2022-03-07 17:40:54 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if err != nil {
|
2022-07-13 00:15:02 +00:00
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
|
2022-06-07 14:33:06 +00:00
|
|
|
Message: "Internal error fetching organization.",
|
2022-06-03 21:48:09 +00:00
|
|
|
Detail: err.Error(),
|
2022-03-07 17:40:54 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-09-20 20:16:26 +00:00
|
|
|
user, _, err := api.CreateUser(r.Context(), api.Database, CreateUserRequest{
|
2022-08-17 23:00:53 +00:00
|
|
|
CreateUserRequest: req,
|
|
|
|
LoginType: database.LoginTypePassword,
|
|
|
|
})
|
2022-01-25 19:52:58 +00:00
|
|
|
if err != nil {
|
2022-07-13 00:15:02 +00:00
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
|
2022-06-07 14:33:06 +00:00
|
|
|
Message: "Internal error creating user.",
|
2022-06-03 21:48:09 +00:00
|
|
|
Detail: err.Error(),
|
2022-01-25 19:52:58 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-06-17 05:26:40 +00:00
|
|
|
|
2022-09-09 02:16:16 +00:00
|
|
|
aReq.New = user
|
|
|
|
|
2022-06-17 05:26:40 +00:00
|
|
|
// Report when users are added!
|
|
|
|
api.Telemetry.Report(&telemetry.Snapshot{
|
|
|
|
Users: []telemetry.User{telemetry.ConvertUser(user)},
|
|
|
|
})
|
2022-01-25 19:52:58 +00:00
|
|
|
|
2022-08-17 23:00:53 +00:00
|
|
|
httpapi.Write(rw, http.StatusCreated, convertUser(user, []uuid.UUID{req.OrganizationID}))
|
2022-01-20 13:46:51 +00:00
|
|
|
}
|
|
|
|
|
2022-09-12 23:24:20 +00:00
|
|
|
func (api *API) deleteUser(rw http.ResponseWriter, r *http.Request) {
|
2022-09-20 04:11:01 +00:00
|
|
|
auditor := *api.Auditor.Load()
|
2022-09-12 23:24:20 +00:00
|
|
|
user := httpmw.UserParam(r)
|
|
|
|
aReq, commitAudit := audit.InitRequest[database.User](rw, &audit.RequestParams{
|
2022-09-20 04:11:01 +00:00
|
|
|
Audit: auditor,
|
|
|
|
Log: api.Logger,
|
|
|
|
Request: r,
|
|
|
|
Action: database.AuditActionDelete,
|
2022-09-12 23:24:20 +00:00
|
|
|
})
|
|
|
|
aReq.Old = user
|
|
|
|
defer commitAudit()
|
|
|
|
|
|
|
|
if !api.Authorize(r, rbac.ActionDelete, rbac.ResourceUser) {
|
|
|
|
httpapi.Forbidden(rw)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
workspaces, err := api.Database.GetWorkspaces(r.Context(), database.GetWorkspacesParams{
|
|
|
|
OwnerID: user.ID,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
|
|
|
|
Message: "Internal error fetching workspaces.",
|
|
|
|
Detail: err.Error(),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if len(workspaces) > 0 {
|
|
|
|
httpapi.Write(rw, http.StatusExpectationFailed, codersdk.Response{
|
|
|
|
Message: "You cannot delete a user that has workspaces. Delete their workspaces and try again!",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
err = api.Database.UpdateUserDeletedByID(r.Context(), database.UpdateUserDeletedByIDParams{
|
|
|
|
ID: user.ID,
|
|
|
|
Deleted: true,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
|
|
|
|
Message: "Internal error deleting user.",
|
|
|
|
Detail: err.Error(),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
user.Deleted = true
|
|
|
|
aReq.New = user
|
|
|
|
httpapi.Write(rw, http.StatusOK, codersdk.Response{
|
|
|
|
Message: "User has been deleted!",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-01-23 05:58:10 +00:00
|
|
|
// Returns the parameterized user requested. All validation
|
|
|
|
// is completed in the middleware for this route.
|
2022-05-26 03:14:08 +00:00
|
|
|
func (api *API) userByName(rw http.ResponseWriter, r *http.Request) {
|
2022-01-23 05:58:10 +00:00
|
|
|
user := httpmw.UserParam(r)
|
2022-04-28 14:10:17 +00:00
|
|
|
organizationIDs, err := userOrganizationIDs(r.Context(), api, user)
|
|
|
|
|
2022-08-09 18:16:53 +00:00
|
|
|
if !api.Authorize(r, rbac.ActionRead, rbac.ResourceUser) {
|
2022-06-14 15:14:05 +00:00
|
|
|
httpapi.ResourceNotFound(rw)
|
2022-05-17 18:43:19 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-04-28 14:10:17 +00:00
|
|
|
if err != nil {
|
2022-07-13 00:15:02 +00:00
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
|
2022-06-07 14:33:06 +00:00
|
|
|
Message: "Internal error fetching user's organizations.",
|
2022-06-03 21:48:09 +00:00
|
|
|
Detail: err.Error(),
|
2022-04-28 14:10:17 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-01-20 13:46:51 +00:00
|
|
|
|
2022-04-28 14:10:17 +00:00
|
|
|
httpapi.Write(rw, http.StatusOK, convertUser(user, organizationIDs))
|
2022-01-20 13:46:51 +00:00
|
|
|
}
|
|
|
|
|
2022-05-26 03:14:08 +00:00
|
|
|
func (api *API) putUserProfile(rw http.ResponseWriter, r *http.Request) {
|
2022-09-09 02:16:16 +00:00
|
|
|
var (
|
|
|
|
user = httpmw.UserParam(r)
|
2022-09-20 04:11:01 +00:00
|
|
|
auditor = *api.Auditor.Load()
|
2022-09-09 02:16:16 +00:00
|
|
|
aReq, commitAudit = audit.InitRequest[database.User](rw, &audit.RequestParams{
|
2022-09-20 04:11:01 +00:00
|
|
|
Audit: auditor,
|
|
|
|
Log: api.Logger,
|
|
|
|
Request: r,
|
|
|
|
Action: database.AuditActionWrite,
|
2022-09-09 02:16:16 +00:00
|
|
|
})
|
|
|
|
)
|
|
|
|
defer commitAudit()
|
|
|
|
aReq.Old = user
|
2022-04-12 14:05:21 +00:00
|
|
|
|
2022-08-09 18:16:53 +00:00
|
|
|
if !api.Authorize(r, rbac.ActionUpdate, rbac.ResourceUser) {
|
2022-06-14 15:14:05 +00:00
|
|
|
httpapi.ResourceNotFound(rw)
|
2022-05-17 18:43:19 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-04-12 14:05:21 +00:00
|
|
|
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 {
|
2022-07-13 00:15:02 +00:00
|
|
|
responseErrors := []codersdk.ValidationError{}
|
2022-04-12 14:05:21 +00:00
|
|
|
if existentUser.Username == params.Username {
|
2022-07-13 00:15:02 +00:00
|
|
|
responseErrors = append(responseErrors, codersdk.ValidationError{
|
2022-04-18 16:02:54 +00:00
|
|
|
Field: "username",
|
|
|
|
Detail: "this value is already in use and should be unique",
|
2022-04-12 14:05:21 +00:00
|
|
|
})
|
|
|
|
}
|
2022-07-13 00:15:02 +00:00
|
|
|
httpapi.Write(rw, http.StatusConflict, codersdk.Response{
|
2022-06-07 14:33:06 +00:00
|
|
|
Message: "User already exists.",
|
2022-06-03 21:48:09 +00:00
|
|
|
Validations: responseErrors,
|
2022-04-12 14:05:21 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if !errors.Is(err, sql.ErrNoRows) && isDifferentUser {
|
2022-07-13 00:15:02 +00:00
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
|
2022-06-07 14:33:06 +00:00
|
|
|
Message: "Internal error fetching user.",
|
2022-06-03 21:48:09 +00:00
|
|
|
Detail: err.Error(),
|
2022-04-12 14:05:21 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
updatedUserProfile, err := api.Database.UpdateUserProfile(r.Context(), database.UpdateUserProfileParams{
|
|
|
|
ID: user.ID,
|
2022-05-27 22:25:04 +00:00
|
|
|
Email: user.Email,
|
2022-09-04 16:44:27 +00:00
|
|
|
AvatarURL: user.AvatarURL,
|
2022-04-12 14:05:21 +00:00
|
|
|
Username: params.Username,
|
|
|
|
UpdatedAt: database.Now(),
|
|
|
|
})
|
2022-09-09 02:16:16 +00:00
|
|
|
aReq.New = updatedUserProfile
|
2022-04-12 14:05:21 +00:00
|
|
|
|
|
|
|
if err != nil {
|
2022-07-13 00:15:02 +00:00
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
|
2022-06-07 14:33:06 +00:00
|
|
|
Message: "Internal error updating user.",
|
2022-06-03 21:48:09 +00:00
|
|
|
Detail: err.Error(),
|
2022-04-12 14:05:21 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-04-28 14:10:17 +00:00
|
|
|
organizationIDs, err := userOrganizationIDs(r.Context(), api, user)
|
|
|
|
if err != nil {
|
2022-07-13 00:15:02 +00:00
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
|
2022-06-07 14:33:06 +00:00
|
|
|
Message: "Internal error fetching user's organizations.",
|
2022-06-03 21:48:09 +00:00
|
|
|
Detail: err.Error(),
|
2022-04-28 14:10:17 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
httpapi.Write(rw, http.StatusOK, convertUser(updatedUserProfile, organizationIDs))
|
2022-04-12 14:05:21 +00:00
|
|
|
}
|
|
|
|
|
2022-05-26 03:14:08 +00:00
|
|
|
func (api *API) putUserStatus(status database.UserStatus) func(rw http.ResponseWriter, r *http.Request) {
|
2022-05-16 20:29:27 +00:00
|
|
|
return func(rw http.ResponseWriter, r *http.Request) {
|
2022-09-09 02:16:16 +00:00
|
|
|
var (
|
|
|
|
user = httpmw.UserParam(r)
|
|
|
|
apiKey = httpmw.APIKey(r)
|
2022-09-20 04:11:01 +00:00
|
|
|
auditor = *api.Auditor.Load()
|
2022-09-09 02:16:16 +00:00
|
|
|
aReq, commitAudit = audit.InitRequest[database.User](rw, &audit.RequestParams{
|
2022-09-20 04:11:01 +00:00
|
|
|
Audit: auditor,
|
|
|
|
Log: api.Logger,
|
|
|
|
Request: r,
|
|
|
|
Action: database.AuditActionWrite,
|
2022-09-09 02:16:16 +00:00
|
|
|
})
|
|
|
|
)
|
|
|
|
defer commitAudit()
|
|
|
|
aReq.Old = user
|
2022-05-17 18:43:19 +00:00
|
|
|
|
2022-08-09 18:16:53 +00:00
|
|
|
if !api.Authorize(r, rbac.ActionDelete, rbac.ResourceUser) {
|
2022-06-14 15:14:05 +00:00
|
|
|
httpapi.ResourceNotFound(rw)
|
2022-05-17 18:43:19 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-08-31 15:26:36 +00:00
|
|
|
if status == database.UserStatusSuspended {
|
|
|
|
// There are some manual protections when suspending a user to
|
|
|
|
// prevent certain situations.
|
|
|
|
switch {
|
|
|
|
case user.ID == apiKey.UserID:
|
|
|
|
// Suspending yourself is not allowed, as you can lock yourself
|
|
|
|
// out of the system.
|
|
|
|
httpapi.Write(rw, http.StatusBadRequest, codersdk.Response{
|
|
|
|
Message: "You cannot suspend yourself.",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
case slice.Contains(user.RBACRoles, rbac.RoleOwner()):
|
|
|
|
// You may not suspend an owner
|
|
|
|
httpapi.Write(rw, http.StatusBadRequest, codersdk.Response{
|
|
|
|
Message: fmt.Sprintf("You cannot suspend a user with the %q role. You must remove the role first.", rbac.RoleOwner()),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-05-16 20:29:27 +00:00
|
|
|
}
|
2022-04-26 14:00:07 +00:00
|
|
|
|
2022-05-16 20:29:27 +00:00
|
|
|
suspendedUser, err := api.Database.UpdateUserStatus(r.Context(), database.UpdateUserStatusParams{
|
|
|
|
ID: user.ID,
|
|
|
|
Status: status,
|
|
|
|
UpdatedAt: database.Now(),
|
2022-04-26 14:00:07 +00:00
|
|
|
})
|
2022-05-16 20:29:27 +00:00
|
|
|
if err != nil {
|
2022-07-13 00:15:02 +00:00
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
|
2022-06-07 14:33:06 +00:00
|
|
|
Message: fmt.Sprintf("Internal error updating user's status to %q.", status),
|
2022-06-03 21:48:09 +00:00
|
|
|
Detail: err.Error(),
|
2022-05-16 20:29:27 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-09-09 02:16:16 +00:00
|
|
|
aReq.New = suspendedUser
|
2022-04-28 14:10:17 +00:00
|
|
|
|
2022-05-16 20:29:27 +00:00
|
|
|
organizations, err := userOrganizationIDs(r.Context(), api, user)
|
|
|
|
if err != nil {
|
2022-07-13 00:15:02 +00:00
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
|
2022-06-07 14:33:06 +00:00
|
|
|
Message: "Internal error fetching user's organizations.",
|
2022-06-03 21:48:09 +00:00
|
|
|
Detail: err.Error(),
|
2022-05-16 20:29:27 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
httpapi.Write(rw, http.StatusOK, convertUser(suspendedUser, organizations))
|
|
|
|
}
|
2022-04-26 14:00:07 +00:00
|
|
|
}
|
|
|
|
|
2022-05-26 03:14:08 +00:00
|
|
|
func (api *API) putUserPassword(rw http.ResponseWriter, r *http.Request) {
|
2022-05-06 18:23:03 +00:00
|
|
|
var (
|
2022-09-09 02:16:16 +00:00
|
|
|
user = httpmw.UserParam(r)
|
|
|
|
params codersdk.UpdateUserPasswordRequest
|
2022-09-20 04:11:01 +00:00
|
|
|
auditor = *api.Auditor.Load()
|
2022-09-09 02:16:16 +00:00
|
|
|
aReq, commitAudit = audit.InitRequest[database.User](rw, &audit.RequestParams{
|
2022-09-20 04:11:01 +00:00
|
|
|
Audit: auditor,
|
|
|
|
Log: api.Logger,
|
|
|
|
Request: r,
|
|
|
|
Action: database.AuditActionWrite,
|
2022-09-09 02:16:16 +00:00
|
|
|
})
|
2022-05-06 18:23:03 +00:00
|
|
|
)
|
2022-09-09 02:16:16 +00:00
|
|
|
defer commitAudit()
|
|
|
|
aReq.Old = user
|
2022-05-17 18:43:19 +00:00
|
|
|
|
2022-06-14 15:14:05 +00:00
|
|
|
if !api.Authorize(r, rbac.ActionUpdate, rbac.ResourceUserData.WithOwner(user.ID.String())) {
|
|
|
|
httpapi.ResourceNotFound(rw)
|
2022-05-17 18:43:19 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-05-06 14:20:08 +00:00
|
|
|
if !httpapi.Read(rw, r, ¶ms) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-05-27 17:29:55 +00:00
|
|
|
err := userpassword.Validate(params.Password)
|
|
|
|
if err != nil {
|
2022-07-13 00:15:02 +00:00
|
|
|
httpapi.Write(rw, http.StatusBadRequest, codersdk.Response{
|
2022-06-07 14:33:06 +00:00
|
|
|
Message: "Invalid password.",
|
2022-07-13 00:15:02 +00:00
|
|
|
Validations: []codersdk.ValidationError{
|
2022-05-27 17:29:55 +00:00
|
|
|
{
|
|
|
|
Field: "password",
|
|
|
|
Detail: err.Error(),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-06-10 01:43:54 +00:00
|
|
|
// admins can change passwords without sending old_password
|
|
|
|
if params.OldPassword == "" {
|
2022-08-09 18:16:53 +00:00
|
|
|
if !api.Authorize(r, rbac.ActionUpdate, rbac.ResourceUser) {
|
2022-06-14 15:14:05 +00:00
|
|
|
httpapi.Forbidden(rw)
|
2022-06-10 01:43:54 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// if they send something let's validate it
|
2022-05-27 17:29:55 +00:00
|
|
|
ok, err := userpassword.Compare(string(user.HashedPassword), params.OldPassword)
|
|
|
|
if err != nil {
|
2022-07-13 00:15:02 +00:00
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
|
2022-06-07 14:33:06 +00:00
|
|
|
Message: "Internal error with passwords.",
|
2022-06-03 21:48:09 +00:00
|
|
|
Detail: err.Error(),
|
2022-05-27 17:29:55 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if !ok {
|
2022-07-13 00:15:02 +00:00
|
|
|
httpapi.Write(rw, http.StatusBadRequest, codersdk.Response{
|
2022-06-07 14:33:06 +00:00
|
|
|
Message: "Old password is incorrect.",
|
2022-07-13 00:15:02 +00:00
|
|
|
Validations: []codersdk.ValidationError{
|
2022-05-27 17:29:55 +00:00
|
|
|
{
|
|
|
|
Field: "old_password",
|
|
|
|
Detail: "Old password is incorrect.",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-06 14:20:08 +00:00
|
|
|
hashedPassword, err := userpassword.Hash(params.Password)
|
|
|
|
if err != nil {
|
2022-07-13 00:15:02 +00:00
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
|
2022-06-07 14:33:06 +00:00
|
|
|
Message: "Internal error hashing new password.",
|
2022-06-03 21:48:09 +00:00
|
|
|
Detail: err.Error(),
|
2022-05-06 14:20:08 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
err = api.Database.UpdateUserHashedPassword(r.Context(), database.UpdateUserHashedPasswordParams{
|
|
|
|
ID: user.ID,
|
|
|
|
HashedPassword: []byte(hashedPassword),
|
|
|
|
})
|
|
|
|
if err != nil {
|
2022-07-13 00:15:02 +00:00
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
|
2022-06-07 14:33:06 +00:00
|
|
|
Message: "Internal error updating user's password.",
|
2022-06-03 21:48:09 +00:00
|
|
|
Detail: err.Error(),
|
2022-05-06 14:20:08 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-09-09 02:16:16 +00:00
|
|
|
newUser := user
|
|
|
|
newUser.HashedPassword = []byte(hashedPassword)
|
|
|
|
aReq.New = newUser
|
|
|
|
|
2022-05-06 14:20:08 +00:00
|
|
|
httpapi.Write(rw, http.StatusNoContent, nil)
|
|
|
|
}
|
|
|
|
|
2022-05-26 03:14:08 +00:00
|
|
|
func (api *API) userRoles(rw http.ResponseWriter, r *http.Request) {
|
2022-04-29 14:04:19 +00:00
|
|
|
user := httpmw.UserParam(r)
|
2022-05-17 18:43:19 +00:00
|
|
|
|
2022-06-14 15:14:05 +00:00
|
|
|
if !api.Authorize(r, rbac.ActionRead, rbac.ResourceUserData.WithOwner(user.ID.String())) {
|
|
|
|
httpapi.ResourceNotFound(rw)
|
2022-05-17 18:43:19 +00:00
|
|
|
return
|
|
|
|
}
|
2022-04-29 14:04:19 +00:00
|
|
|
|
|
|
|
resp := codersdk.UserRoles{
|
|
|
|
Roles: user.RBACRoles,
|
|
|
|
OrganizationRoles: make(map[uuid.UUID][]string),
|
|
|
|
}
|
|
|
|
|
|
|
|
memberships, err := api.Database.GetOrganizationMembershipsByUserID(r.Context(), user.ID)
|
|
|
|
if err != nil {
|
2022-07-13 00:15:02 +00:00
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
|
2022-06-07 14:33:06 +00:00
|
|
|
Message: "Internal error fetching user's organization memberships.",
|
2022-06-03 21:48:09 +00:00
|
|
|
Detail: err.Error(),
|
2022-04-29 14:04:19 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-05-27 20:47:03 +00:00
|
|
|
// Only include ones we can read from RBAC.
|
2022-09-20 04:11:01 +00:00
|
|
|
memberships, err = AuthorizeFilter(api.HTTPAuth, r, rbac.ActionRead, memberships)
|
2022-08-11 22:07:48 +00:00
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
|
|
|
|
Message: "Internal error fetching memberships.",
|
|
|
|
Detail: err.Error(),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-05-17 18:43:19 +00:00
|
|
|
|
2022-05-24 13:43:34 +00:00
|
|
|
for _, mem := range memberships {
|
2022-05-27 20:47:03 +00:00
|
|
|
// If we can read the org member, include the roles.
|
2022-05-17 18:43:19 +00:00
|
|
|
if err == nil {
|
|
|
|
resp.OrganizationRoles[mem.OrganizationID] = mem.Roles
|
|
|
|
}
|
2022-04-29 14:04:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
httpapi.Write(rw, http.StatusOK, resp)
|
|
|
|
}
|
|
|
|
|
2022-05-26 03:14:08 +00:00
|
|
|
func (api *API) putUserRoles(rw http.ResponseWriter, r *http.Request) {
|
2022-09-09 02:16:16 +00:00
|
|
|
var (
|
|
|
|
// User is the user to modify.
|
|
|
|
user = httpmw.UserParam(r)
|
2022-09-19 17:39:02 +00:00
|
|
|
actorRoles = httpmw.UserAuthorization(r)
|
2022-09-09 02:16:16 +00:00
|
|
|
apiKey = httpmw.APIKey(r)
|
2022-09-20 04:11:01 +00:00
|
|
|
auditor = *api.Auditor.Load()
|
2022-09-09 02:16:16 +00:00
|
|
|
aReq, commitAudit = audit.InitRequest[database.User](rw, &audit.RequestParams{
|
2022-09-20 04:11:01 +00:00
|
|
|
Audit: auditor,
|
|
|
|
Log: api.Logger,
|
|
|
|
Request: r,
|
|
|
|
Action: database.AuditActionWrite,
|
2022-09-09 02:16:16 +00:00
|
|
|
})
|
|
|
|
)
|
|
|
|
defer commitAudit()
|
|
|
|
aReq.Old = user
|
2022-05-31 20:50:38 +00:00
|
|
|
|
|
|
|
if apiKey.UserID == user.ID {
|
2022-07-13 00:15:02 +00:00
|
|
|
httpapi.Write(rw, http.StatusBadRequest, codersdk.Response{
|
2022-05-31 20:50:38 +00:00
|
|
|
Message: "You cannot change your own roles.",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-04-29 14:04:19 +00:00
|
|
|
|
|
|
|
var params codersdk.UpdateRoles
|
|
|
|
if !httpapi.Read(rw, r, ¶ms) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-08-09 18:16:53 +00:00
|
|
|
if !api.Authorize(r, rbac.ActionRead, rbac.ResourceUser) {
|
2022-06-14 15:14:05 +00:00
|
|
|
httpapi.ResourceNotFound(rw)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-06-01 14:07:50 +00:00
|
|
|
// The member role is always implied.
|
|
|
|
impliedTypes := append(params.Roles, rbac.RoleMember())
|
2022-08-09 18:16:53 +00:00
|
|
|
added, removed := rbac.ChangeRoleSet(user.RBACRoles, impliedTypes)
|
|
|
|
|
|
|
|
// Assigning a role requires the create permission.
|
|
|
|
if len(added) > 0 && !api.Authorize(r, rbac.ActionCreate, rbac.ResourceRoleAssignment) {
|
|
|
|
httpapi.Forbidden(rw)
|
|
|
|
return
|
2022-05-17 18:43:19 +00:00
|
|
|
}
|
2022-08-09 18:16:53 +00:00
|
|
|
|
|
|
|
// Removing a role requires the delete permission.
|
|
|
|
if len(removed) > 0 && !api.Authorize(r, rbac.ActionDelete, rbac.ResourceRoleAssignment) {
|
|
|
|
httpapi.Forbidden(rw)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Just treat adding & removing as "assigning" for now.
|
|
|
|
for _, roleName := range append(added, removed...) {
|
|
|
|
if !rbac.CanAssignRole(actorRoles.Roles, roleName) {
|
2022-06-14 15:14:05 +00:00
|
|
|
httpapi.Forbidden(rw)
|
2022-05-17 18:43:19 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-29 14:04:19 +00:00
|
|
|
updatedUser, err := api.updateSiteUserRoles(r.Context(), database.UpdateUserRolesParams{
|
|
|
|
GrantedRoles: params.Roles,
|
|
|
|
ID: user.ID,
|
|
|
|
})
|
|
|
|
if err != nil {
|
2022-07-13 00:15:02 +00:00
|
|
|
httpapi.Write(rw, http.StatusBadRequest, codersdk.Response{
|
2022-04-29 14:04:19 +00:00
|
|
|
Message: err.Error(),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-09-09 02:16:16 +00:00
|
|
|
aReq.New = updatedUser
|
2022-04-29 14:04:19 +00:00
|
|
|
|
|
|
|
organizationIDs, err := userOrganizationIDs(r.Context(), api, user)
|
|
|
|
if err != nil {
|
2022-07-13 00:15:02 +00:00
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
|
2022-06-07 14:33:06 +00:00
|
|
|
Message: "Internal error fetching user's organizations.",
|
2022-06-03 21:48:09 +00:00
|
|
|
Detail: err.Error(),
|
2022-04-29 14:04:19 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
httpapi.Write(rw, http.StatusOK, convertUser(updatedUser, organizationIDs))
|
|
|
|
}
|
|
|
|
|
2022-05-17 18:43:19 +00:00
|
|
|
// updateSiteUserRoles will ensure only site wide roles are passed in as arguments.
|
|
|
|
// If an organization role is included, an error is returned.
|
2022-05-26 03:14:08 +00:00
|
|
|
func (api *API) updateSiteUserRoles(ctx context.Context, args database.UpdateUserRolesParams) (database.User, error) {
|
2022-05-27 20:47:03 +00:00
|
|
|
// Enforce only site wide roles.
|
2022-04-29 14:04:19 +00:00
|
|
|
for _, r := range args.GrantedRoles {
|
|
|
|
if _, ok := rbac.IsOrgRole(r); ok {
|
2022-06-03 21:48:09 +00:00
|
|
|
return database.User{}, xerrors.Errorf("Must only update site wide roles")
|
2022-04-29 14:04:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-01-23 05:58:10 +00:00
|
|
|
// Returns organizations the parameterized user has access to.
|
2022-05-26 03:14:08 +00:00
|
|
|
func (api *API) organizationsByUser(rw http.ResponseWriter, r *http.Request) {
|
2022-01-23 05:58:10 +00:00
|
|
|
user := httpmw.UserParam(r)
|
|
|
|
|
2022-02-01 22:15:26 +00:00
|
|
|
organizations, err := api.Database.GetOrganizationsByUserID(r.Context(), user.ID)
|
2022-02-06 00:24:51 +00:00
|
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
|
|
err = nil
|
|
|
|
organizations = []database.Organization{}
|
|
|
|
}
|
2022-01-23 05:58:10 +00:00
|
|
|
if err != nil {
|
2022-07-13 00:15:02 +00:00
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
|
2022-06-07 14:33:06 +00:00
|
|
|
Message: "Internal error fetching user's organizations.",
|
2022-06-03 21:48:09 +00:00
|
|
|
Detail: err.Error(),
|
2022-01-23 05:58:10 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-05-27 20:47:03 +00:00
|
|
|
// Only return orgs the user can read.
|
2022-09-20 04:11:01 +00:00
|
|
|
organizations, err = AuthorizeFilter(api.HTTPAuth, r, rbac.ActionRead, organizations)
|
2022-08-11 22:07:48 +00:00
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
|
|
|
|
Message: "Internal error fetching organizations.",
|
|
|
|
Detail: err.Error(),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-05-24 13:43:34 +00:00
|
|
|
|
2022-03-22 19:17:50 +00:00
|
|
|
publicOrganizations := make([]codersdk.Organization, 0, len(organizations))
|
2022-01-23 05:58:10 +00:00
|
|
|
for _, organization := range organizations {
|
2022-05-24 13:43:34 +00:00
|
|
|
publicOrganizations = append(publicOrganizations, convertOrganization(organization))
|
2022-01-23 05:58:10 +00:00
|
|
|
}
|
|
|
|
|
2022-04-12 15:17:33 +00:00
|
|
|
httpapi.Write(rw, http.StatusOK, publicOrganizations)
|
2022-01-23 05:58:10 +00:00
|
|
|
}
|
|
|
|
|
2022-05-26 03:14:08 +00:00
|
|
|
func (api *API) organizationByUserAndName(rw http.ResponseWriter, r *http.Request) {
|
2022-03-07 17:40:54 +00:00
|
|
|
organizationName := chi.URLParam(r, "organizationname")
|
|
|
|
organization, err := api.Database.GetOrganizationByName(r.Context(), organizationName)
|
|
|
|
if errors.Is(err, sql.ErrNoRows) {
|
2022-06-14 15:14:05 +00:00
|
|
|
httpapi.ResourceNotFound(rw)
|
2022-03-07 17:40:54 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
if err != nil {
|
2022-07-13 00:15:02 +00:00
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
|
2022-06-14 15:14:05 +00:00
|
|
|
Message: "Internal error fetching organization.",
|
|
|
|
Detail: err.Error(),
|
|
|
|
})
|
2022-03-07 17:40:54 +00:00
|
|
|
return
|
|
|
|
}
|
2022-05-17 18:43:19 +00:00
|
|
|
|
2022-06-14 15:14:05 +00:00
|
|
|
if !api.Authorize(r, rbac.ActionRead,
|
2022-05-17 18:43:19 +00:00
|
|
|
rbac.ResourceOrganization.
|
2022-08-09 18:16:53 +00:00
|
|
|
InOrg(organization.ID)) {
|
2022-06-14 15:14:05 +00:00
|
|
|
httpapi.ResourceNotFound(rw)
|
2022-03-07 17:40:54 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-04-12 15:17:33 +00:00
|
|
|
httpapi.Write(rw, http.StatusOK, convertOrganization(organization))
|
2022-03-07 17:40:54 +00:00
|
|
|
}
|
|
|
|
|
2022-01-20 13:46:51 +00:00
|
|
|
// Authenticates the user with an email and password.
|
2022-05-26 03:14:08 +00:00
|
|
|
func (api *API) postLogin(rw http.ResponseWriter, r *http.Request) {
|
2022-03-22 19:17:50 +00:00
|
|
|
var loginWithPassword codersdk.LoginWithPasswordRequest
|
2022-01-20 13:46:51 +00:00
|
|
|
if !httpapi.Read(rw, r, &loginWithPassword) {
|
|
|
|
return
|
|
|
|
}
|
2022-04-28 18:22:38 +00:00
|
|
|
|
2022-02-01 22:15:26 +00:00
|
|
|
user, err := api.Database.GetUserByEmailOrUsername(r.Context(), database.GetUserByEmailOrUsernameParams{
|
2022-01-20 13:46:51 +00:00
|
|
|
Email: loginWithPassword.Email,
|
|
|
|
})
|
2022-04-28 18:22:38 +00:00
|
|
|
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
|
2022-07-13 00:15:02 +00:00
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
|
2022-06-07 14:33:06 +00:00
|
|
|
Message: "Internal error.",
|
2022-01-20 13:46:51 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-04-28 18:22:38 +00:00
|
|
|
|
|
|
|
// If the user doesn't exist, it will be a default struct.
|
2022-01-20 13:46:51 +00:00
|
|
|
equal, err := userpassword.Compare(string(user.HashedPassword), loginWithPassword.Password)
|
|
|
|
if err != nil {
|
2022-07-13 00:15:02 +00:00
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
|
2022-06-07 14:33:06 +00:00
|
|
|
Message: "Internal error.",
|
2022-01-20 13:46:51 +00:00
|
|
|
})
|
2022-06-28 19:13:37 +00:00
|
|
|
return
|
2022-01-20 13:46:51 +00:00
|
|
|
}
|
|
|
|
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.
|
2022-07-13 00:15:02 +00:00
|
|
|
httpapi.Write(rw, http.StatusUnauthorized, codersdk.Response{
|
2022-05-31 13:06:42 +00:00
|
|
|
Message: "Incorrect email or password.",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-08-17 23:00:53 +00:00
|
|
|
if user.LoginType != database.LoginTypePassword {
|
|
|
|
httpapi.Write(rw, http.StatusForbidden, codersdk.Response{
|
|
|
|
Message: fmt.Sprintf("Incorrect login type, attempting to use %q but user is of login type %q", database.LoginTypePassword, user.LoginType),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-05-31 13:06:42 +00:00
|
|
|
// If the user logged into a suspended account, reject the login request.
|
|
|
|
if user.Status != database.UserStatusActive {
|
2022-07-13 00:15:02 +00:00
|
|
|
httpapi.Write(rw, http.StatusUnauthorized, codersdk.Response{
|
2022-06-03 21:48:09 +00:00
|
|
|
Message: "Your account is suspended. Contact an admin to reactivate your account.",
|
2022-01-20 13:46:51 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-08-22 23:13:46 +00:00
|
|
|
cookie, err := api.createAPIKey(r, createAPIKeyParams{
|
2022-04-23 22:58:57 +00:00
|
|
|
UserID: user.ID,
|
|
|
|
LoginType: database.LoginTypePassword,
|
2022-01-20 13:46:51 +00:00
|
|
|
})
|
2022-08-22 23:13:46 +00:00
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
|
|
|
|
Message: "Failed to create API key.",
|
|
|
|
Detail: err.Error(),
|
|
|
|
})
|
2022-01-20 13:46:51 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-09-13 17:31:33 +00:00
|
|
|
api.setAuthCookie(rw, cookie)
|
2022-08-22 23:13:46 +00:00
|
|
|
|
2022-04-12 15:17:33 +00:00
|
|
|
httpapi.Write(rw, http.StatusCreated, codersdk.LoginWithPasswordResponse{
|
2022-08-22 23:13:46 +00:00
|
|
|
SessionToken: cookie.Value,
|
2022-01-20 13:46:51 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-05-27 20:47:03 +00:00
|
|
|
// Creates a new session key, used for logging in via the CLI.
|
2022-05-26 03:14:08 +00:00
|
|
|
func (api *API) postAPIKey(rw http.ResponseWriter, r *http.Request) {
|
2022-02-18 04:09:33 +00:00
|
|
|
user := httpmw.UserParam(r)
|
|
|
|
|
2022-06-14 15:14:05 +00:00
|
|
|
if !api.Authorize(r, rbac.ActionCreate, rbac.ResourceAPIKey.WithOwner(user.ID.String())) {
|
|
|
|
httpapi.ResourceNotFound(rw)
|
2022-02-18 04:09:33 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-06-01 19:58:55 +00:00
|
|
|
lifeTime := time.Hour * 24 * 7
|
2022-08-22 23:13:46 +00:00
|
|
|
cookie, err := api.createAPIKey(r, createAPIKeyParams{
|
2022-04-23 22:58:57 +00:00
|
|
|
UserID: user.ID,
|
|
|
|
LoginType: database.LoginTypePassword,
|
2022-06-01 19:58:55 +00:00
|
|
|
// All api generated keys will last 1 week. Browser login tokens have
|
|
|
|
// a shorter life.
|
|
|
|
ExpiresAt: database.Now().Add(lifeTime),
|
|
|
|
LifetimeSeconds: int64(lifeTime.Seconds()),
|
2022-02-18 04:09:33 +00:00
|
|
|
})
|
2022-08-22 23:13:46 +00:00
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
|
|
|
|
Message: "Failed to create API key.",
|
|
|
|
Detail: err.Error(),
|
|
|
|
})
|
2022-02-18 04:09:33 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-08-22 23:13:46 +00:00
|
|
|
// We intentionally do not set the cookie on the response here.
|
|
|
|
// Setting the cookie will couple the browser sesion to the API
|
|
|
|
// key we return here, meaning logging out of the website would
|
|
|
|
// invalid your CLI key.
|
|
|
|
httpapi.Write(rw, http.StatusCreated, codersdk.GenerateAPIKeyResponse{Key: cookie.Value})
|
2022-02-18 04:09:33 +00:00
|
|
|
}
|
|
|
|
|
2022-06-27 18:50:52 +00:00
|
|
|
func (api *API) apiKey(rw http.ResponseWriter, r *http.Request) {
|
|
|
|
var (
|
|
|
|
ctx = r.Context()
|
|
|
|
user = httpmw.UserParam(r)
|
|
|
|
)
|
|
|
|
|
|
|
|
if !api.Authorize(r, rbac.ActionRead, rbac.ResourceAPIKey.WithOwner(user.ID.String())) {
|
|
|
|
httpapi.ResourceNotFound(rw)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
keyID := chi.URLParam(r, "keyid")
|
|
|
|
key, err := api.Database.GetAPIKeyByID(ctx, keyID)
|
|
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
|
|
httpapi.ResourceNotFound(rw)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if err != nil {
|
2022-07-13 00:15:02 +00:00
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
|
2022-06-27 18:50:52 +00:00
|
|
|
Message: "Internal error fetching API key.",
|
|
|
|
Detail: err.Error(),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
httpapi.Write(rw, http.StatusOK, convertAPIKey(key))
|
|
|
|
}
|
|
|
|
|
2022-05-27 20:47:03 +00:00
|
|
|
// Clear the user's session cookie.
|
|
|
|
func (api *API) postLogout(rw http.ResponseWriter, r *http.Request) {
|
|
|
|
// Get a blank token cookie.
|
2022-01-25 01:09:39 +00:00
|
|
|
cookie := &http.Cookie{
|
2022-05-27 20:47:03 +00:00
|
|
|
// MaxAge < 0 means to delete the cookie now.
|
2022-01-25 01:09:39 +00:00
|
|
|
MaxAge: -1,
|
2022-07-13 00:15:02 +00:00
|
|
|
Name: codersdk.SessionTokenKey,
|
2022-01-25 01:09:39 +00:00
|
|
|
Path: "/",
|
|
|
|
}
|
2022-09-13 17:31:33 +00:00
|
|
|
api.setAuthCookie(rw, cookie)
|
2022-05-27 20:47:03 +00:00
|
|
|
|
|
|
|
// Delete the session token from database.
|
|
|
|
apiKey := httpmw.APIKey(r)
|
|
|
|
err := api.Database.DeleteAPIKeyByID(r.Context(), apiKey.ID)
|
|
|
|
if err != nil {
|
2022-07-13 00:15:02 +00:00
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
|
2022-06-07 14:33:06 +00:00
|
|
|
Message: "Internal error deleting API key.",
|
2022-06-03 21:48:09 +00:00
|
|
|
Detail: err.Error(),
|
2022-05-27 20:47:03 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-07-13 00:15:02 +00:00
|
|
|
httpapi.Write(rw, http.StatusOK, codersdk.Response{
|
2022-04-12 15:17:33 +00:00
|
|
|
Message: "Logged out!",
|
|
|
|
})
|
2022-01-25 01:09:39 +00:00
|
|
|
}
|
|
|
|
|
2022-01-20 13:46:51 +00:00
|
|
|
// Generates a new ID and secret for an API key.
|
2022-01-20 16:00:13 +00:00
|
|
|
func generateAPIKeyIDSecret() (id string, secret string, err error) {
|
2022-01-20 13:46:51 +00:00
|
|
|
// Length of an API Key ID.
|
2022-01-20 16:00:13 +00:00
|
|
|
id, err = cryptorand.String(10)
|
2022-01-20 13:46:51 +00:00
|
|
|
if err != nil {
|
|
|
|
return "", "", err
|
|
|
|
}
|
|
|
|
// Length of an API Key secret.
|
2022-01-20 16:00:13 +00:00
|
|
|
secret, err = cryptorand.String(22)
|
2022-01-20 13:46:51 +00:00
|
|
|
if err != nil {
|
|
|
|
return "", "", err
|
|
|
|
}
|
|
|
|
return id, secret, nil
|
|
|
|
}
|
2022-01-25 19:52:58 +00:00
|
|
|
|
2022-08-17 23:00:53 +00:00
|
|
|
type createAPIKeyParams struct {
|
|
|
|
UserID uuid.UUID
|
|
|
|
LoginType database.LoginType
|
|
|
|
|
|
|
|
// Optional.
|
|
|
|
ExpiresAt time.Time
|
|
|
|
LifetimeSeconds int64
|
|
|
|
}
|
|
|
|
|
2022-08-22 23:13:46 +00:00
|
|
|
func (api *API) createAPIKey(r *http.Request, params createAPIKeyParams) (*http.Cookie, error) {
|
2022-04-23 22:58:57 +00:00
|
|
|
keyID, keySecret, err := generateAPIKeyIDSecret()
|
|
|
|
if err != nil {
|
2022-08-22 23:13:46 +00:00
|
|
|
return nil, xerrors.Errorf("generate API key: %w", err)
|
2022-04-23 22:58:57 +00:00
|
|
|
}
|
|
|
|
hashed := sha256.Sum256([]byte(keySecret))
|
|
|
|
|
2022-06-01 19:58:55 +00:00
|
|
|
// Default expires at to now+lifetime, or just 24hrs if not set
|
|
|
|
if params.ExpiresAt.IsZero() {
|
|
|
|
if params.LifetimeSeconds != 0 {
|
|
|
|
params.ExpiresAt = database.Now().Add(time.Duration(params.LifetimeSeconds) * time.Second)
|
|
|
|
} else {
|
|
|
|
params.ExpiresAt = database.Now().Add(24 * time.Hour)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-26 21:22:03 +00:00
|
|
|
host, _, _ := net.SplitHostPort(r.RemoteAddr)
|
|
|
|
ip := net.ParseIP(host)
|
2022-06-22 17:32:21 +00:00
|
|
|
if ip == nil {
|
|
|
|
ip = net.IPv4(0, 0, 0, 0)
|
|
|
|
}
|
2022-06-27 20:31:18 +00:00
|
|
|
bitlen := len(ip) * 8
|
2022-06-22 17:32:21 +00:00
|
|
|
key, err := api.Database.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
|
2022-06-01 19:58:55 +00:00
|
|
|
ID: keyID,
|
|
|
|
UserID: params.UserID,
|
|
|
|
LifetimeSeconds: params.LifetimeSeconds,
|
2022-06-22 17:32:21 +00:00
|
|
|
IPAddress: pqtype.Inet{
|
|
|
|
IPNet: net.IPNet{
|
|
|
|
IP: ip,
|
2022-06-27 20:31:18 +00:00
|
|
|
Mask: net.CIDRMask(bitlen, bitlen),
|
2022-06-22 17:32:21 +00:00
|
|
|
},
|
|
|
|
Valid: true,
|
|
|
|
},
|
2022-06-01 19:58:55 +00:00
|
|
|
// Make sure in UTC time for common time zone
|
2022-08-17 23:00:53 +00:00
|
|
|
ExpiresAt: params.ExpiresAt.UTC(),
|
|
|
|
CreatedAt: database.Now(),
|
|
|
|
UpdatedAt: database.Now(),
|
|
|
|
HashedSecret: hashed[:],
|
|
|
|
LoginType: params.LoginType,
|
2022-09-19 17:39:02 +00:00
|
|
|
Scope: database.APIKeyScopeAll,
|
2022-04-23 22:58:57 +00:00
|
|
|
})
|
|
|
|
if err != nil {
|
2022-08-22 23:13:46 +00:00
|
|
|
return nil, xerrors.Errorf("insert API key: %w", err)
|
2022-04-23 22:58:57 +00:00
|
|
|
}
|
|
|
|
|
2022-06-22 17:32:21 +00:00
|
|
|
api.Telemetry.Report(&telemetry.Snapshot{
|
|
|
|
APIKeys: []telemetry.APIKey{telemetry.ConvertAPIKey(key)},
|
|
|
|
})
|
|
|
|
|
2022-04-23 22:58:57 +00:00
|
|
|
// This format is consumed by the APIKey middleware.
|
|
|
|
sessionToken := fmt.Sprintf("%s-%s", keyID, keySecret)
|
2022-08-22 23:13:46 +00:00
|
|
|
return &http.Cookie{
|
2022-07-13 00:15:02 +00:00
|
|
|
Name: codersdk.SessionTokenKey,
|
2022-04-23 22:58:57 +00:00
|
|
|
Value: sessionToken,
|
|
|
|
Path: "/",
|
|
|
|
HttpOnly: true,
|
|
|
|
SameSite: http.SameSiteLaxMode,
|
|
|
|
Secure: api.SecureAuthCookie,
|
2022-08-22 23:13:46 +00:00
|
|
|
}, nil
|
2022-04-23 22:58:57 +00:00
|
|
|
}
|
|
|
|
|
2022-09-20 20:16:26 +00:00
|
|
|
type CreateUserRequest struct {
|
2022-08-17 23:00:53 +00:00
|
|
|
codersdk.CreateUserRequest
|
|
|
|
LoginType database.LoginType
|
|
|
|
}
|
|
|
|
|
2022-09-20 20:16:26 +00:00
|
|
|
func (api *API) CreateUser(ctx context.Context, store database.Store, req CreateUserRequest) (database.User, uuid.UUID, error) {
|
2022-04-23 22:58:57 +00:00
|
|
|
var user database.User
|
2022-08-22 23:13:46 +00:00
|
|
|
return user, req.OrganizationID, store.InTx(func(tx database.Store) error {
|
2022-06-01 14:07:50 +00:00
|
|
|
orgRoles := make([]string, 0)
|
2022-04-23 22:58:57 +00:00
|
|
|
// If no organization is provided, create a new one for the user.
|
|
|
|
if req.OrganizationID == uuid.Nil {
|
2022-08-22 23:13:46 +00:00
|
|
|
organization, err := tx.InsertOrganization(ctx, database.InsertOrganizationParams{
|
2022-04-23 22:58:57 +00:00
|
|
|
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
|
2022-04-29 14:04:19 +00:00
|
|
|
orgRoles = append(orgRoles, rbac.RoleOrgAdmin(req.OrganizationID))
|
2022-04-23 22:58:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
params := database.InsertUserParams{
|
|
|
|
ID: uuid.New(),
|
|
|
|
Email: req.Email,
|
|
|
|
Username: req.Username,
|
|
|
|
CreatedAt: database.Now(),
|
|
|
|
UpdatedAt: database.Now(),
|
2022-04-29 14:04:19 +00:00
|
|
|
// All new users are defaulted to members of the site.
|
2022-06-01 14:07:50 +00:00
|
|
|
RBACRoles: []string{},
|
2022-08-17 23:00:53 +00:00
|
|
|
LoginType: req.LoginType,
|
2022-04-23 22:58:57 +00:00
|
|
|
}
|
|
|
|
// 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
|
2022-08-22 23:13:46 +00:00
|
|
|
user, err = tx.InsertUser(ctx, params)
|
2022-04-23 22:58:57 +00:00
|
|
|
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)
|
|
|
|
}
|
2022-08-22 23:13:46 +00:00
|
|
|
_, err = tx.InsertGitSSHKey(ctx, database.InsertGitSSHKeyParams{
|
2022-04-23 22:58:57 +00:00
|
|
|
UserID: user.ID,
|
|
|
|
CreatedAt: database.Now(),
|
|
|
|
UpdatedAt: database.Now(),
|
|
|
|
PrivateKey: privateKey,
|
|
|
|
PublicKey: publicKey,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("insert user gitsshkey: %w", err)
|
|
|
|
}
|
2022-08-22 23:13:46 +00:00
|
|
|
_, err = tx.InsertOrganizationMember(ctx, database.InsertOrganizationMemberParams{
|
2022-04-23 22:58:57 +00:00
|
|
|
OrganizationID: req.OrganizationID,
|
|
|
|
UserID: user.ID,
|
|
|
|
CreatedAt: database.Now(),
|
|
|
|
UpdatedAt: database.Now(),
|
2022-05-27 20:47:03 +00:00
|
|
|
// By default give them membership to the organization.
|
2022-04-29 14:04:19 +00:00
|
|
|
Roles: orgRoles,
|
2022-04-23 22:58:57 +00:00
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("create organization member: %w", err)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-09-13 17:31:33 +00:00
|
|
|
func (api *API) setAuthCookie(rw http.ResponseWriter, cookie *http.Cookie) {
|
|
|
|
http.SetCookie(rw, cookie)
|
|
|
|
|
2022-09-20 04:11:01 +00:00
|
|
|
appCookie := api.applicationCookie(cookie)
|
|
|
|
if appCookie != nil {
|
|
|
|
http.SetCookie(rw, appCookie)
|
2022-09-13 17:31:33 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-28 14:10:17 +00:00
|
|
|
func convertUser(user database.User, organizationIDs []uuid.UUID) codersdk.User {
|
2022-05-09 16:38:14 +00:00
|
|
|
convertedUser := codersdk.User{
|
2022-04-28 14:10:17 +00:00
|
|
|
ID: user.ID,
|
|
|
|
Email: user.Email,
|
|
|
|
CreatedAt: user.CreatedAt,
|
|
|
|
Username: user.Username,
|
|
|
|
Status: codersdk.UserStatus(user.Status),
|
|
|
|
OrganizationIDs: organizationIDs,
|
2022-08-24 19:58:57 +00:00
|
|
|
Roles: make([]codersdk.Role, 0, len(user.RBACRoles)),
|
2022-09-04 16:44:27 +00:00
|
|
|
AvatarURL: user.AvatarURL.String,
|
2022-01-25 19:52:58 +00:00
|
|
|
}
|
2022-05-09 16:38:14 +00:00
|
|
|
|
|
|
|
for _, roleName := range user.RBACRoles {
|
|
|
|
rbacRole, _ := rbac.RoleByName(roleName)
|
|
|
|
convertedUser.Roles = append(convertedUser.Roles, convertRole(rbacRole))
|
|
|
|
}
|
|
|
|
|
|
|
|
return convertedUser
|
2022-01-25 19:52:58 +00:00
|
|
|
}
|
2022-04-22 20:27:55 +00:00
|
|
|
|
2022-04-28 14:10:17 +00:00
|
|
|
func convertUsers(users []database.User, organizationIDsByUserID map[uuid.UUID][]uuid.UUID) []codersdk.User {
|
2022-04-22 20:27:55 +00:00
|
|
|
converted := make([]codersdk.User, 0, len(users))
|
|
|
|
for _, u := range users {
|
2022-04-28 14:10:17 +00:00
|
|
|
userOrganizationIDs := organizationIDsByUserID[u.ID]
|
|
|
|
converted = append(converted, convertUser(u, userOrganizationIDs))
|
2022-04-22 20:27:55 +00:00
|
|
|
}
|
|
|
|
return converted
|
|
|
|
}
|
2022-04-28 14:10:17 +00:00
|
|
|
|
2022-05-26 03:14:08 +00:00
|
|
|
func userOrganizationIDs(ctx context.Context, api *API, user database.User) ([]uuid.UUID, error) {
|
2022-04-28 14:10:17 +00:00
|
|
|
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
|
|
|
|
}
|
2022-06-09 01:23:35 +00:00
|
|
|
|
|
|
|
func findUser(id uuid.UUID, users []database.User) *database.User {
|
|
|
|
for _, u := range users {
|
|
|
|
if u.ID == id {
|
|
|
|
return &u
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
2022-06-24 15:02:23 +00:00
|
|
|
|
2022-07-13 00:15:02 +00:00
|
|
|
func userSearchQuery(query string) (database.GetUsersParams, []codersdk.ValidationError) {
|
2022-06-24 15:02:23 +00:00
|
|
|
searchParams := make(url.Values)
|
|
|
|
if query == "" {
|
|
|
|
// No filter
|
|
|
|
return database.GetUsersParams{}, nil
|
|
|
|
}
|
2022-06-24 23:55:28 +00:00
|
|
|
query = strings.ToLower(query)
|
2022-06-24 15:02:23 +00:00
|
|
|
// Because we do this in 2 passes, we want to maintain quotes on the first
|
|
|
|
// pass.Further splitting occurs on the second pass and quotes will be
|
|
|
|
// dropped.
|
|
|
|
elements := splitQueryParameterByDelimiter(query, ' ', true)
|
|
|
|
for _, element := range elements {
|
|
|
|
parts := splitQueryParameterByDelimiter(element, ':', false)
|
|
|
|
switch len(parts) {
|
|
|
|
case 1:
|
|
|
|
// No key:value pair.
|
|
|
|
searchParams.Set("search", parts[0])
|
|
|
|
case 2:
|
|
|
|
searchParams.Set(parts[0], parts[1])
|
|
|
|
default:
|
2022-07-13 00:15:02 +00:00
|
|
|
return database.GetUsersParams{}, []codersdk.ValidationError{
|
2022-06-24 15:02:23 +00:00
|
|
|
{Field: "q", Detail: fmt.Sprintf("Query element %q can only contain 1 ':'", element)},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
parser := httpapi.NewQueryParamParser()
|
|
|
|
filter := database.GetUsersParams{
|
|
|
|
Search: parser.String(searchParams, "", "search"),
|
|
|
|
Status: httpapi.ParseCustom(parser, searchParams, []database.UserStatus{}, "status", parseUserStatus),
|
|
|
|
RbacRole: parser.Strings(searchParams, []string{}, "role"),
|
|
|
|
}
|
|
|
|
|
|
|
|
return filter, parser.Errors
|
|
|
|
}
|
|
|
|
|
|
|
|
// parseUserStatus ensures proper enums are used for user statuses
|
|
|
|
func parseUserStatus(v string) ([]database.UserStatus, error) {
|
|
|
|
var statuses []database.UserStatus
|
|
|
|
if v == "" {
|
|
|
|
return statuses, nil
|
|
|
|
}
|
|
|
|
parts := strings.Split(v, ",")
|
|
|
|
for _, part := range parts {
|
|
|
|
switch database.UserStatus(part) {
|
|
|
|
case database.UserStatusActive, database.UserStatusSuspended:
|
|
|
|
statuses = append(statuses, database.UserStatus(part))
|
|
|
|
default:
|
|
|
|
return []database.UserStatus{}, xerrors.Errorf("%q is not a valid user status", part)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return statuses, nil
|
|
|
|
}
|
2022-06-27 18:50:52 +00:00
|
|
|
|
|
|
|
func convertAPIKey(k database.APIKey) codersdk.APIKey {
|
|
|
|
return codersdk.APIKey{
|
|
|
|
ID: k.ID,
|
|
|
|
UserID: k.UserID,
|
|
|
|
LastUsed: k.LastUsed,
|
|
|
|
ExpiresAt: k.ExpiresAt,
|
|
|
|
CreatedAt: k.CreatedAt,
|
|
|
|
UpdatedAt: k.UpdatedAt,
|
|
|
|
LoginType: codersdk.LoginType(k.LoginType),
|
|
|
|
LifetimeSeconds: k.LifetimeSeconds,
|
|
|
|
}
|
|
|
|
}
|