coder/coderd/users.go

839 lines
26 KiB
Go
Raw Normal View History

package coderd
import (
"crypto/sha256"
"database/sql"
"encoding/json"
"errors"
"fmt"
"net/http"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
"github.com/google/uuid"
"github.com/moby/moby/pkg/namesgenerator"
"golang.org/x/xerrors"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/coderd/httpmw"
"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
}
hashedPassword, err := userpassword.Hash(createUser.Password)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("hash password: %s", err.Error()),
})
return
}
// Create the user, organization, and membership to the user.
var user database.User
var organization database.Organization
err = api.Database.InTx(func(s database.Store) error {
user, err = api.Database.InsertUser(r.Context(), database.InsertUserParams{
ID: uuid.NewString(),
Email: createUser.Email,
HashedPassword: []byte(hashedPassword),
Username: createUser.Username,
LoginType: database.LoginTypeBuiltIn,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
})
if err != nil {
return xerrors.Errorf("create user: %w", err)
}
organization, err = api.Database.InsertOrganization(r.Context(), database.InsertOrganizationParams{
ID: uuid.NewString(),
Name: createUser.Organization,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
})
if err != nil {
return xerrors.Errorf("create organization: %w", err)
}
_, err = api.Database.InsertOrganizationMember(r.Context(), database.InsertOrganizationMemberParams{
OrganizationID: organization.ID,
UserID: user.ID,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
Roles: []string{"organization-admin"},
})
if err != nil {
return xerrors.Errorf("create organization member: %w", err)
}
return nil
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: err.Error(),
})
return
}
render.Status(r, http.StatusCreated)
render.JSON(rw, r, codersdk.CreateFirstUserResponse{
UserID: user.ID,
OrganizationID: organization.ID,
})
}
// Creates a new user.
func (api *api) postUsers(rw http.ResponseWriter, r *http.Request) {
apiKey := httpmw.APIKey(r)
var createUser codersdk.CreateUserRequest
if !httpapi.Read(rw, r, &createUser) {
return
}
_, 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
}
organization, 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
}
// Check if the caller has permissions to the organization requested.
_, err = api.Database.GetOrganizationMemberByUserID(r.Context(), database.GetOrganizationMemberByUserIDParams{
OrganizationID: organization.ID,
UserID: apiKey.UserID,
})
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
Message: "you are not authorized to add members to that organization",
})
return
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get organization member: %s", err),
})
return
}
hashedPassword, err := userpassword.Hash(createUser.Password)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("hash password: %s", err.Error()),
})
return
}
var user database.User
err = api.Database.InTx(func(db database.Store) error {
user, err = db.InsertUser(r.Context(), database.InsertUserParams{
ID: uuid.NewString(),
Email: createUser.Email,
HashedPassword: []byte(hashedPassword),
Username: createUser.Username,
LoginType: database.LoginTypeBuiltIn,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
})
if err != nil {
return xerrors.Errorf("create user: %w", err)
}
_, err = db.InsertOrganizationMember(r.Context(), database.InsertOrganizationMemberParams{
OrganizationID: organization.ID,
UserID: user.ID,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
Roles: []string{},
})
if err != nil {
return xerrors.Errorf("create organization member: %w", err)
}
return nil
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: err.Error(),
})
return
}
render.Status(r, http.StatusCreated)
render.JSON(rw, r, convertUser(user))
}
// Returns the parameterized user requested. All validation
// is completed in the middleware for this route.
func (*api) userByName(rw http.ResponseWriter, r *http.Request) {
user := httpmw.UserParam(r)
render.JSON(rw, r, convertUser(user))
}
// 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
}
publicOrganizations := make([]codersdk.Organization, 0, len(organizations))
for _, organization := range organizations {
publicOrganizations = append(publicOrganizations, convertOrganization(organization))
}
render.Status(r, http.StatusOK)
render.JSON(rw, r, publicOrganizations)
}
func (api *api) organizationByUserAndName(rw http.ResponseWriter, r *http.Request) {
user := httpmw.UserParam(r)
organizationName := chi.URLParam(r, "organizationname")
organization, err := api.Database.GetOrganizationByName(r.Context(), organizationName)
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
Message: fmt.Sprintf("no organization found by name %q", organizationName),
})
return
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get organization by name: %s", err),
})
return
}
_, err = api.Database.GetOrganizationMemberByUserID(r.Context(), database.GetOrganizationMemberByUserIDParams{
OrganizationID: organization.ID,
UserID: user.ID,
})
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
Message: "you are not a member of that organization",
})
return
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get organization member: %s", err),
})
return
}
render.Status(r, http.StatusOK)
render.JSON(rw, r, convertOrganization(organization))
}
func (api *api) postOrganizationsByUser(rw http.ResponseWriter, r *http.Request) {
user := httpmw.UserParam(r)
var req codersdk.CreateOrganizationRequest
if !httpapi.Read(rw, r, &req) {
return
}
_, err := api.Database.GetOrganizationByName(r.Context(), req.Name)
if err == nil {
httpapi.Write(rw, http.StatusConflict, httpapi.Response{
Message: "organization already exists with that name",
})
return
}
if !errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get organization: %s", err.Error()),
})
return
}
var organization database.Organization
err = api.Database.InTx(func(db database.Store) error {
organization, err = api.Database.InsertOrganization(r.Context(), database.InsertOrganizationParams{
ID: uuid.NewString(),
Name: req.Name,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
})
if err != nil {
return xerrors.Errorf("create organization: %w", err)
}
_, err = api.Database.InsertOrganizationMember(r.Context(), database.InsertOrganizationMemberParams{
OrganizationID: organization.ID,
UserID: user.ID,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
Roles: []string{"organization-admin"},
})
if err != nil {
return xerrors.Errorf("create organization member: %w", err)
}
return nil
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: err.Error(),
})
return
}
render.Status(r, http.StatusCreated)
render.JSON(rw, r, 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 errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
Message: "invalid email or password",
})
return
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get user: %s", err.Error()),
})
return
}
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: "invalid email or password",
})
return
}
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
}
hashed := sha256.Sum256([]byte(keySecret))
_, err = api.Database.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
ID: keyID,
UserID: user.ID,
ExpiresAt: database.Now().Add(24 * time.Hour),
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
HashedSecret: hashed[:],
LoginType: database.LoginTypeBuiltIn,
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("insert api key: %s", err.Error()),
})
return
}
// This format is consumed by the APIKey middleware.
sessionToken := fmt.Sprintf("%s-%s", keyID, keySecret)
http.SetCookie(rw, &http.Cookie{
Name: httpmw.AuthCookie,
Value: sessionToken,
Path: "/",
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
Secure: api.SecureAuthCookie,
})
render.Status(r, http.StatusCreated)
render.JSON(rw, r, 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)
apiKey := httpmw.APIKey(r)
if user.ID != apiKey.UserID {
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
Message: "Keys can only be generated for the authenticated user",
})
return
}
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
}
hashed := sha256.Sum256([]byte(keySecret))
_, err = api.Database.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
ID: keyID,
UserID: apiKey.UserID,
ExpiresAt: database.Now().AddDate(1, 0, 0), // Expire after 1 year (same as v1)
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
HashedSecret: hashed[:],
LoginType: database.LoginTypeBuiltIn,
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("insert api key: %s", err.Error()),
})
return
}
// This format is consumed by the APIKey middleware.
generatedAPIKey := fmt.Sprintf("%s-%s", keyID, keySecret)
render.Status(r, http.StatusCreated)
render.JSON(rw, r, codersdk.GenerateAPIKeyResponse{Key: generatedAPIKey})
}
// Clear the user's session cookie
func (*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.AuthCookie,
Path: "/",
}
http.SetCookie(rw, cookie)
render.Status(r, http.StatusOK)
}
// Create a new workspace for the currently authenticated user.
func (api *api) postWorkspacesByUser(rw http.ResponseWriter, r *http.Request) {
var createWorkspace codersdk.CreateWorkspaceRequest
if !httpapi.Read(rw, r, &createWorkspace) {
return
}
apiKey := httpmw.APIKey(r)
project, err := api.Database.GetProjectByID(r.Context(), createWorkspace.ProjectID)
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: fmt.Sprintf("project %q doesn't exist", createWorkspace.ProjectID.String()),
Errors: []httpapi.Error{{
Field: "project_id",
Code: "not_found",
}},
})
return
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get project: %s", err),
})
return
}
_, err = api.Database.GetOrganizationMemberByUserID(r.Context(), database.GetOrganizationMemberByUserIDParams{
OrganizationID: project.OrganizationID,
UserID: apiKey.UserID,
})
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
Message: "you aren't allowed to access projects in that organization",
})
return
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get organization member: %s", err),
})
return
}
workspace, err := api.Database.GetWorkspaceByUserIDAndName(r.Context(), database.GetWorkspaceByUserIDAndNameParams{
OwnerID: apiKey.UserID,
Name: createWorkspace.Name,
})
if err == nil {
// If the workspace already exists, don't allow creation.
project, err := api.Database.GetProjectByID(r.Context(), workspace.ProjectID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("find project for conflicting workspace name %q: %s", createWorkspace.Name, err),
})
return
}
// The project is fetched for clarity to the user on where the conflicting name may be.
httpapi.Write(rw, http.StatusConflict, httpapi.Response{
Message: fmt.Sprintf("workspace %q already exists in the %q project", createWorkspace.Name, project.Name),
Errors: []httpapi.Error{{
Field: "name",
Code: "exists",
}},
})
return
}
if !errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get workspace by name: %s", err.Error()),
})
return
}
projectVersion, err := api.Database.GetProjectVersionByID(r.Context(), project.ActiveVersionID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get project version: %s", err),
})
return
}
projectVersionJob, err := api.Database.GetProvisionerJobByID(r.Context(), projectVersion.JobID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get project version job: %s", err),
})
return
}
projectVersionJobStatus := convertProvisionerJob(projectVersionJob).Status
switch projectVersionJobStatus {
case codersdk.ProvisionerJobPending, codersdk.ProvisionerJobRunning:
httpapi.Write(rw, http.StatusNotAcceptable, httpapi.Response{
Message: fmt.Sprintf("The provided project version is %s. Wait for it to complete importing!", projectVersionJobStatus),
})
return
case codersdk.ProvisionerJobFailed:
httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{
Message: fmt.Sprintf("The provided project version %q has failed to import. You cannot create workspaces using it!", projectVersion.Name),
})
return
case codersdk.ProvisionerJobCanceled:
httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{
Message: "The provided project version was canceled during import. You cannot create workspaces using it!",
})
return
}
var provisionerJob database.ProvisionerJob
var workspaceBuild database.WorkspaceBuild
err = api.Database.InTx(func(db database.Store) error {
workspaceBuildID := uuid.New()
// Workspaces are created without any versions.
workspace, err = db.InsertWorkspace(r.Context(), database.InsertWorkspaceParams{
ID: uuid.New(),
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
OwnerID: apiKey.UserID,
ProjectID: project.ID,
Name: createWorkspace.Name,
})
if err != nil {
return xerrors.Errorf("insert workspace: %w", err)
}
for _, parameterValue := range createWorkspace.ParameterValues {
_, err = db.InsertParameterValue(r.Context(), database.InsertParameterValueParams{
ID: uuid.New(),
Name: parameterValue.Name,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
Scope: database.ParameterScopeWorkspace,
ScopeID: workspace.ID.String(),
SourceScheme: parameterValue.SourceScheme,
SourceValue: parameterValue.SourceValue,
DestinationScheme: parameterValue.DestinationScheme,
})
if err != nil {
return xerrors.Errorf("insert parameter value: %w", err)
}
}
input, err := json.Marshal(workspaceProvisionJob{
WorkspaceBuildID: workspaceBuildID,
})
if err != nil {
return xerrors.Errorf("marshal provision job: %w", err)
}
provisionerJob, err = db.InsertProvisionerJob(r.Context(), database.InsertProvisionerJobParams{
ID: uuid.New(),
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
InitiatorID: apiKey.UserID,
OrganizationID: project.OrganizationID,
Provisioner: project.Provisioner,
Type: database.ProvisionerJobTypeWorkspaceBuild,
StorageMethod: projectVersionJob.StorageMethod,
StorageSource: projectVersionJob.StorageSource,
Input: input,
})
if err != nil {
return xerrors.Errorf("insert provisioner job: %w", err)
}
workspaceBuild, err = db.InsertWorkspaceBuild(r.Context(), database.InsertWorkspaceBuildParams{
ID: workspaceBuildID,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
WorkspaceID: workspace.ID,
ProjectVersionID: projectVersion.ID,
Name: namesgenerator.GetRandomName(1),
Initiator: apiKey.UserID,
Transition: database.WorkspaceTransitionStart,
JobID: provisionerJob.ID,
})
if err != nil {
return xerrors.Errorf("insert workspace build: %w", err)
}
return nil
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("create workspace: %s", err),
})
return
}
render.Status(r, http.StatusCreated)
render.JSON(rw, r, convertWorkspace(workspace,
convertWorkspaceBuild(workspaceBuild, convertProvisionerJob(projectVersionJob)), project))
}
func (api *api) workspacesByUser(rw http.ResponseWriter, r *http.Request) {
user := httpmw.UserParam(r)
workspaces, err := api.Database.GetWorkspacesByUserID(r.Context(), database.GetWorkspacesByUserIDParams{
OwnerID: user.ID,
})
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get workspaces: %s", err),
})
return
}
workspaceIDs := make([]uuid.UUID, 0, len(workspaces))
projectIDs := make([]uuid.UUID, 0, len(workspaces))
for _, workspace := range workspaces {
workspaceIDs = append(workspaceIDs, workspace.ID)
projectIDs = append(projectIDs, workspace.ProjectID)
}
workspaceBuilds, err := api.Database.GetWorkspaceBuildsByWorkspaceIDsWithoutAfter(r.Context(), workspaceIDs)
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get workspace builds: %s", err),
})
return
}
projects, err := api.Database.GetProjectsByIDs(r.Context(), projectIDs)
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get projects: %s", err),
})
return
}
jobIDs := make([]uuid.UUID, 0, len(workspaceBuilds))
for _, build := range workspaceBuilds {
jobIDs = append(jobIDs, build.JobID)
}
jobs, err := api.Database.GetProvisionerJobsByIDs(r.Context(), jobIDs)
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get provisioner jobs: %s", err),
})
return
}
buildByWorkspaceID := map[string]database.WorkspaceBuild{}
for _, workspaceBuild := range workspaceBuilds {
buildByWorkspaceID[workspaceBuild.WorkspaceID.String()] = workspaceBuild
}
projectByID := map[string]database.Project{}
for _, project := range projects {
projectByID[project.ID.String()] = project
}
jobByID := map[string]database.ProvisionerJob{}
for _, job := range jobs {
jobByID[job.ID.String()] = job
}
apiWorkspaces := make([]codersdk.Workspace, 0, len(workspaces))
for _, workspace := range workspaces {
build, exists := buildByWorkspaceID[workspace.ID.String()]
if !exists {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("build not found for workspace %q", workspace.Name),
})
return
}
project, exists := projectByID[workspace.ProjectID.String()]
if !exists {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("project not found for workspace %q", workspace.Name),
})
return
}
job, exists := jobByID[build.JobID.String()]
if !exists {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("build job not found for workspace %q", workspace.Name),
})
return
}
apiWorkspaces = append(apiWorkspaces,
convertWorkspace(workspace, convertWorkspaceBuild(build, convertProvisionerJob(job)), project))
}
render.Status(r, http.StatusOK)
render.JSON(rw, r, apiWorkspaces)
}
func (api *api) workspaceByUserAndName(rw http.ResponseWriter, r *http.Request) {
user := httpmw.UserParam(r)
workspaceName := chi.URLParam(r, "workspacename")
workspace, err := api.Database.GetWorkspaceByUserIDAndName(r.Context(), database.GetWorkspaceByUserIDAndNameParams{
OwnerID: user.ID,
Name: workspaceName,
})
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
Message: fmt.Sprintf("no workspace found by name %q", workspaceName),
})
return
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get workspace by name: %s", err),
})
return
}
build, err := api.Database.GetWorkspaceBuildByWorkspaceIDWithoutAfter(r.Context(), workspace.ID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get workspace build: %s", err),
})
return
}
job, err := api.Database.GetProvisionerJobByID(r.Context(), build.JobID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get provisioner job: %s", err),
})
return
}
project, err := api.Database.GetProjectByID(r.Context(), workspace.ProjectID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get project: %s", err),
})
return
}
render.Status(r, http.StatusOK)
render.JSON(rw, r, convertWorkspace(workspace,
convertWorkspaceBuild(build, convertProvisionerJob(job)), project))
}
// 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 convertUser(user database.User) codersdk.User {
return codersdk.User{
ID: user.ID,
Email: user.Email,
CreatedAt: user.CreatedAt,
Username: user.Username,
}
}