2022-01-20 13:46:51 +00:00
|
|
|
package coderd
|
|
|
|
|
|
|
|
import (
|
|
|
|
"crypto/sha256"
|
|
|
|
"database/sql"
|
2022-03-22 19:17:50 +00:00
|
|
|
"encoding/json"
|
2022-01-20 13:46:51 +00:00
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
2022-04-22 20:27:55 +00:00
|
|
|
"strconv"
|
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-03-22 19:17:50 +00:00
|
|
|
"github.com/moby/moby/pkg/namesgenerator"
|
2022-01-23 05:58:10 +00:00
|
|
|
"golang.org/x/xerrors"
|
2022-01-20 13:46:51 +00:00
|
|
|
|
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-01-20 13:46:51 +00:00
|
|
|
"github.com/coder/coder/coderd/userpassword"
|
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-02-10 14:33:27 +00:00
|
|
|
// Returns whether the initial user has been created or not.
|
2022-03-07 17:40:54 +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 {
|
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
|
|
Message: fmt.Sprintf("get user count: %s", err.Error()),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-04-01 19:42:36 +00:00
|
|
|
|
2022-02-10 14:33:27 +00:00
|
|
|
if userCount == 0 {
|
|
|
|
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
|
|
|
|
Message: "The initial user has not been created!",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-04-01 19:42:36 +00:00
|
|
|
|
2022-02-10 14:33:27 +00:00
|
|
|
httpapi.Write(rw, http.StatusOK, httpapi.Response{
|
|
|
|
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-03-07 17:40:54 +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 {
|
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
|
|
Message: fmt.Sprintf("get user count: %s", err.Error()),
|
|
|
|
})
|
|
|
|
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 {
|
|
|
|
httpapi.Write(rw, http.StatusConflict, httpapi.Response{
|
|
|
|
Message: "the initial user has already been created",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-04-01 19:42:36 +00:00
|
|
|
|
2022-01-20 13:46:51 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-01-23 05:58:10 +00:00
|
|
|
// Create the user, organization, and membership to the user.
|
|
|
|
var user database.User
|
2022-03-07 17:40:54 +00:00
|
|
|
var organization database.Organization
|
2022-04-06 00:18:26 +00:00
|
|
|
err = api.Database.InTx(func(db database.Store) error {
|
2022-02-01 22:15:26 +00:00
|
|
|
user, err = api.Database.InsertUser(r.Context(), database.InsertUserParams{
|
2022-04-01 19:42:36 +00:00
|
|
|
ID: uuid.New(),
|
2022-01-23 05:58:10 +00:00
|
|
|
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)
|
|
|
|
}
|
2022-04-06 00:18:26 +00:00
|
|
|
|
|
|
|
privateKey, publicKey, err := gitsshkey.Generate(api.SSHKeygenAlgorithm)
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("generate user gitsshkey: %w", err)
|
|
|
|
}
|
|
|
|
_, err = db.InsertGitSSHKey(r.Context(), 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)
|
|
|
|
}
|
|
|
|
|
2022-03-07 17:40:54 +00:00
|
|
|
organization, err = api.Database.InsertOrganization(r.Context(), database.InsertOrganizationParams{
|
2022-04-01 19:42:36 +00:00
|
|
|
ID: uuid.New(),
|
|
|
|
Name: createUser.OrganizationName,
|
2022-01-23 05:58:10 +00:00
|
|
|
CreatedAt: database.Now(),
|
|
|
|
UpdatedAt: database.Now(),
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("create organization: %w", err)
|
|
|
|
}
|
2022-02-01 22:15:26 +00:00
|
|
|
_, err = api.Database.InsertOrganizationMember(r.Context(), database.InsertOrganizationMemberParams{
|
2022-01-23 05:58:10 +00:00
|
|
|
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
|
2022-01-20 13:46:51 +00:00
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
2022-01-23 05:58:10 +00:00
|
|
|
Message: err.Error(),
|
2022-01-20 13:46:51 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-01-23 05:58:10 +00:00
|
|
|
|
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,
|
|
|
|
OrganizationID: organization.ID,
|
|
|
|
})
|
2022-01-25 19:52:58 +00:00
|
|
|
}
|
|
|
|
|
2022-04-22 20:27:55 +00:00
|
|
|
func (api *api) users(rw http.ResponseWriter, r *http.Request) {
|
|
|
|
var (
|
|
|
|
afterArg = r.URL.Query().Get("after_user")
|
|
|
|
limitArg = r.URL.Query().Get("limit")
|
|
|
|
offsetArg = r.URL.Query().Get("offset")
|
|
|
|
searchName = r.URL.Query().Get("search")
|
|
|
|
)
|
|
|
|
|
|
|
|
// createdAfter is a user uuid.
|
|
|
|
createdAfter := uuid.Nil
|
|
|
|
if afterArg != "" {
|
|
|
|
after, err := uuid.Parse(afterArg)
|
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
|
|
|
|
Message: fmt.Sprintf("after_user must be a valid uuid: %s", err.Error()),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
createdAfter = after
|
|
|
|
}
|
|
|
|
|
|
|
|
// Default to no limit and return all users.
|
|
|
|
pageLimit := -1
|
|
|
|
if limitArg != "" {
|
|
|
|
limit, err := strconv.Atoi(limitArg)
|
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
|
|
|
|
Message: fmt.Sprintf("limit must be an integer: %s", err.Error()),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
pageLimit = limit
|
|
|
|
}
|
|
|
|
|
|
|
|
// The default for empty string is 0.
|
|
|
|
offset, err := strconv.ParseInt(offsetArg, 10, 64)
|
|
|
|
if offsetArg != "" && err != nil {
|
|
|
|
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
|
|
|
|
Message: fmt.Sprintf("offset must be an integer: %s", err.Error()),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
users, err := api.Database.GetUsers(r.Context(), database.GetUsersParams{
|
|
|
|
AfterUser: createdAfter,
|
|
|
|
OffsetOpt: int32(offset),
|
|
|
|
LimitOpt: int32(pageLimit),
|
|
|
|
Search: searchName,
|
|
|
|
})
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
|
|
Message: err.Error(),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
render.Status(r, http.StatusOK)
|
|
|
|
render.JSON(rw, r, convertUsers(users))
|
|
|
|
}
|
|
|
|
|
2022-01-25 19:52:58 +00:00
|
|
|
// Creates a new user.
|
2022-02-01 22:15:26 +00:00
|
|
|
func (api *api) postUsers(rw http.ResponseWriter, r *http.Request) {
|
2022-03-07 17:40:54 +00:00
|
|
|
apiKey := httpmw.APIKey(r)
|
|
|
|
|
2022-03-22 19:17:50 +00:00
|
|
|
var createUser codersdk.CreateUserRequest
|
2022-01-25 19:52:58 +00:00
|
|
|
if !httpapi.Read(rw, r, &createUser) {
|
|
|
|
return
|
|
|
|
}
|
2022-02-01 22:15:26 +00:00
|
|
|
_, err := api.Database.GetUserByEmailOrUsername(r.Context(), database.GetUserByEmailOrUsernameParams{
|
2022-01-25 19:52:58 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-03-07 17:40:54 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-01-25 19:52:58 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-03-07 17:40:54 +00:00
|
|
|
var user database.User
|
|
|
|
err = api.Database.InTx(func(db database.Store) error {
|
|
|
|
user, err = db.InsertUser(r.Context(), database.InsertUserParams{
|
2022-04-01 19:42:36 +00:00
|
|
|
ID: uuid.New(),
|
2022-03-07 17:40:54 +00:00
|
|
|
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)
|
|
|
|
}
|
2022-04-06 00:18:26 +00:00
|
|
|
|
|
|
|
privateKey, publicKey, err := gitsshkey.Generate(api.SSHKeygenAlgorithm)
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("generate user gitsshkey: %w", err)
|
|
|
|
}
|
|
|
|
_, err = db.InsertGitSSHKey(r.Context(), 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)
|
|
|
|
}
|
|
|
|
|
2022-03-07 17:40:54 +00:00
|
|
|
_, 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
|
2022-01-25 19:52:58 +00:00
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
2022-03-07 17:40:54 +00:00
|
|
|
Message: err.Error(),
|
2022-01-25 19:52:58 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-04-12 15:17:33 +00:00
|
|
|
httpapi.Write(rw, http.StatusCreated, convertUser(user))
|
2022-01-20 13:46:51 +00:00
|
|
|
}
|
|
|
|
|
2022-01-23 05:58:10 +00:00
|
|
|
// Returns the parameterized user requested. All validation
|
|
|
|
// is completed in the middleware for this route.
|
2022-02-01 22:15:26 +00:00
|
|
|
func (*api) userByName(rw http.ResponseWriter, r *http.Request) {
|
2022-01-23 05:58:10 +00:00
|
|
|
user := httpmw.UserParam(r)
|
2022-01-20 13:46:51 +00:00
|
|
|
|
2022-04-12 15:17:33 +00:00
|
|
|
httpapi.Write(rw, http.StatusOK, convertUser(user))
|
2022-01-20 13:46:51 +00:00
|
|
|
}
|
|
|
|
|
2022-04-12 14:05:21 +00:00
|
|
|
func (api *api) putUserProfile(rw http.ResponseWriter, r *http.Request) {
|
|
|
|
user := httpmw.UserParam(r)
|
|
|
|
|
|
|
|
var params codersdk.UpdateUserProfileRequest
|
|
|
|
if !httpapi.Read(rw, r, ¶ms) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if params.Name == nil {
|
|
|
|
params.Name = &user.Name
|
|
|
|
}
|
|
|
|
|
|
|
|
existentUser, err := api.Database.GetUserByEmailOrUsername(r.Context(), database.GetUserByEmailOrUsernameParams{
|
|
|
|
Email: params.Email,
|
|
|
|
Username: params.Username,
|
|
|
|
})
|
|
|
|
isDifferentUser := existentUser.ID != user.ID
|
|
|
|
|
|
|
|
if err == nil && isDifferentUser {
|
|
|
|
responseErrors := []httpapi.Error{}
|
|
|
|
if existentUser.Email == params.Email {
|
|
|
|
responseErrors = append(responseErrors, httpapi.Error{
|
2022-04-18 16:02:54 +00:00
|
|
|
Field: "email",
|
|
|
|
Detail: "this value is already in use and should be unique",
|
2022-04-12 14:05:21 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
if existentUser.Username == params.Username {
|
|
|
|
responseErrors = append(responseErrors, httpapi.Error{
|
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
|
|
|
})
|
|
|
|
}
|
|
|
|
httpapi.Write(rw, http.StatusConflict, httpapi.Response{
|
|
|
|
Message: fmt.Sprintf("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,
|
|
|
|
Name: *params.Name,
|
|
|
|
Email: params.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
|
|
|
|
}
|
|
|
|
|
2022-04-12 16:29:07 +00:00
|
|
|
httpapi.Write(rw, http.StatusOK, convertUser(updatedUserProfile))
|
2022-04-12 14:05:21 +00:00
|
|
|
}
|
|
|
|
|
2022-01-23 05:58:10 +00:00
|
|
|
// Returns organizations the parameterized user has access to.
|
2022-02-01 22:15:26 +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 {
|
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
|
|
Message: fmt.Sprintf("get organizations: %s", err.Error()),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
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 {
|
|
|
|
publicOrganizations = append(publicOrganizations, convertOrganization(organization))
|
|
|
|
}
|
|
|
|
|
2022-04-12 15:17:33 +00:00
|
|
|
httpapi.Write(rw, http.StatusOK, publicOrganizations)
|
2022-01-23 05:58:10 +00:00
|
|
|
}
|
|
|
|
|
2022-03-07 17:40:54 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-04-12 15:17:33 +00:00
|
|
|
httpapi.Write(rw, http.StatusOK, convertOrganization(organization))
|
2022-03-07 17:40:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (api *api) postOrganizationsByUser(rw http.ResponseWriter, r *http.Request) {
|
|
|
|
user := httpmw.UserParam(r)
|
2022-03-22 19:17:50 +00:00
|
|
|
var req codersdk.CreateOrganizationRequest
|
2022-03-07 17:40:54 +00:00
|
|
|
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{
|
2022-04-01 19:42:36 +00:00
|
|
|
ID: uuid.New(),
|
2022-03-07 17:40:54 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-04-12 15:17:33 +00:00
|
|
|
httpapi.Write(rw, http.StatusCreated, 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-02-01 22:15:26 +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-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,
|
|
|
|
})
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-01-20 16:00:13 +00:00
|
|
|
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))
|
2022-01-20 13:46:51 +00:00
|
|
|
|
2022-02-01 22:15:26 +00:00
|
|
|
_, err = api.Database.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
|
2022-01-20 16:00:13 +00:00
|
|
|
ID: keyID,
|
2022-01-20 13:46:51 +00:00
|
|
|
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.
|
2022-01-20 16:00:13 +00:00
|
|
|
sessionToken := fmt.Sprintf("%s-%s", keyID, keySecret)
|
2022-01-20 13:46:51 +00:00
|
|
|
http.SetCookie(rw, &http.Cookie{
|
|
|
|
Name: httpmw.AuthCookie,
|
|
|
|
Value: sessionToken,
|
|
|
|
Path: "/",
|
|
|
|
HttpOnly: true,
|
|
|
|
SameSite: http.SameSiteLaxMode,
|
2022-03-31 17:31:06 +00:00
|
|
|
Secure: api.SecureAuthCookie,
|
2022-01-20 13:46:51 +00:00
|
|
|
})
|
|
|
|
|
2022-04-12 15:17:33 +00:00
|
|
|
httpapi.Write(rw, http.StatusCreated, codersdk.LoginWithPasswordResponse{
|
2022-01-20 13:46:51 +00:00
|
|
|
SessionToken: sessionToken,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-02-18 04:09:33 +00:00
|
|
|
// Creates a new session key, used for logging in via the CLI
|
2022-03-07 17:40:54 +00:00
|
|
|
func (api *api) postAPIKey(rw http.ResponseWriter, r *http.Request) {
|
2022-02-18 04:09:33 +00:00
|
|
|
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)
|
|
|
|
|
2022-04-12 15:17:33 +00:00
|
|
|
httpapi.Write(rw, http.StatusCreated, codersdk.GenerateAPIKeyResponse{Key: generatedAPIKey})
|
2022-02-18 04:09:33 +00:00
|
|
|
}
|
|
|
|
|
2022-01-25 01:09:39 +00:00
|
|
|
// Clear the user's session cookie
|
2022-04-12 15:17:33 +00:00
|
|
|
func (*api) postLogout(rw http.ResponseWriter, _ *http.Request) {
|
2022-01-25 01:09:39 +00:00
|
|
|
// 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)
|
2022-04-12 15:17:33 +00:00
|
|
|
httpapi.Write(rw, http.StatusOK, httpapi.Response{
|
|
|
|
Message: "Logged out!",
|
|
|
|
})
|
2022-01-25 01:09:39 +00:00
|
|
|
}
|
|
|
|
|
2022-03-07 17:40:54 +00:00
|
|
|
// Create a new workspace for the currently authenticated user.
|
|
|
|
func (api *api) postWorkspacesByUser(rw http.ResponseWriter, r *http.Request) {
|
2022-03-22 19:17:50 +00:00
|
|
|
var createWorkspace codersdk.CreateWorkspaceRequest
|
2022-03-07 17:40:54 +00:00
|
|
|
if !httpapi.Read(rw, r, &createWorkspace) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
apiKey := httpmw.APIKey(r)
|
2022-04-06 17:42:40 +00:00
|
|
|
template, err := api.Database.GetTemplateByID(r.Context(), createWorkspace.TemplateID)
|
2022-03-07 17:40:54 +00:00
|
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
|
|
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
|
2022-04-06 17:42:40 +00:00
|
|
|
Message: fmt.Sprintf("template %q doesn't exist", createWorkspace.TemplateID.String()),
|
2022-03-07 17:40:54 +00:00
|
|
|
Errors: []httpapi.Error{{
|
2022-04-18 16:02:54 +00:00
|
|
|
Field: "template_id",
|
|
|
|
Detail: "template not found",
|
2022-03-07 17:40:54 +00:00
|
|
|
}},
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
2022-04-06 17:42:40 +00:00
|
|
|
Message: fmt.Sprintf("get template: %s", err),
|
2022-03-07 17:40:54 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
_, err = api.Database.GetOrganizationMemberByUserID(r.Context(), database.GetOrganizationMemberByUserIDParams{
|
2022-04-06 17:42:40 +00:00
|
|
|
OrganizationID: template.OrganizationID,
|
2022-03-07 17:40:54 +00:00
|
|
|
UserID: apiKey.UserID,
|
|
|
|
})
|
|
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
|
|
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
|
2022-04-06 17:42:40 +00:00
|
|
|
Message: "you aren't allowed to access templates in that organization",
|
2022-03-07 17:40:54 +00:00
|
|
|
})
|
|
|
|
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.
|
2022-04-06 17:42:40 +00:00
|
|
|
template, err := api.Database.GetTemplateByID(r.Context(), workspace.TemplateID)
|
2022-03-07 17:40:54 +00:00
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
2022-04-06 17:42:40 +00:00
|
|
|
Message: fmt.Sprintf("find template for conflicting workspace name %q: %s", createWorkspace.Name, err),
|
2022-03-07 17:40:54 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-04-06 17:42:40 +00:00
|
|
|
// The template is fetched for clarity to the user on where the conflicting name may be.
|
2022-03-07 17:40:54 +00:00
|
|
|
httpapi.Write(rw, http.StatusConflict, httpapi.Response{
|
2022-04-06 17:42:40 +00:00
|
|
|
Message: fmt.Sprintf("workspace %q already exists in the %q template", createWorkspace.Name, template.Name),
|
2022-03-07 17:40:54 +00:00
|
|
|
Errors: []httpapi.Error{{
|
2022-04-18 16:02:54 +00:00
|
|
|
Field: "name",
|
|
|
|
Detail: "this value is already in use and should be unique",
|
2022-03-07 17:40:54 +00:00
|
|
|
}},
|
|
|
|
})
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-04-06 17:42:40 +00:00
|
|
|
templateVersion, err := api.Database.GetTemplateVersionByID(r.Context(), template.ActiveVersionID)
|
2022-03-22 19:17:50 +00:00
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
2022-04-06 17:42:40 +00:00
|
|
|
Message: fmt.Sprintf("get template version: %s", err),
|
2022-03-22 19:17:50 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-04-06 17:42:40 +00:00
|
|
|
templateVersionJob, err := api.Database.GetProvisionerJobByID(r.Context(), templateVersion.JobID)
|
2022-03-22 19:17:50 +00:00
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
2022-04-06 17:42:40 +00:00
|
|
|
Message: fmt.Sprintf("get template version job: %s", err),
|
2022-03-22 19:17:50 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-04-06 17:42:40 +00:00
|
|
|
templateVersionJobStatus := convertProvisionerJob(templateVersionJob).Status
|
|
|
|
switch templateVersionJobStatus {
|
2022-03-22 19:17:50 +00:00
|
|
|
case codersdk.ProvisionerJobPending, codersdk.ProvisionerJobRunning:
|
|
|
|
httpapi.Write(rw, http.StatusNotAcceptable, httpapi.Response{
|
2022-04-06 17:42:40 +00:00
|
|
|
Message: fmt.Sprintf("The provided template version is %s. Wait for it to complete importing!", templateVersionJobStatus),
|
2022-03-22 19:17:50 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
case codersdk.ProvisionerJobFailed:
|
|
|
|
httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{
|
2022-04-06 17:42:40 +00:00
|
|
|
Message: fmt.Sprintf("The provided template version %q has failed to import. You cannot create workspaces using it!", templateVersion.Name),
|
2022-03-22 19:17:50 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
case codersdk.ProvisionerJobCanceled:
|
|
|
|
httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{
|
2022-04-06 17:42:40 +00:00
|
|
|
Message: "The provided template version was canceled during import. You cannot create workspaces using it!",
|
2022-03-22 19:17:50 +00:00
|
|
|
})
|
|
|
|
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{
|
2022-04-06 17:42:40 +00:00
|
|
|
ID: uuid.New(),
|
|
|
|
CreatedAt: database.Now(),
|
|
|
|
UpdatedAt: database.Now(),
|
|
|
|
OwnerID: apiKey.UserID,
|
|
|
|
TemplateID: template.ID,
|
|
|
|
Name: createWorkspace.Name,
|
2022-03-22 19:17:50 +00:00
|
|
|
})
|
|
|
|
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,
|
2022-04-01 19:42:36 +00:00
|
|
|
ScopeID: workspace.ID,
|
2022-03-22 19:17:50 +00:00
|
|
|
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,
|
2022-04-06 17:42:40 +00:00
|
|
|
OrganizationID: template.OrganizationID,
|
|
|
|
Provisioner: template.Provisioner,
|
2022-03-22 19:17:50 +00:00
|
|
|
Type: database.ProvisionerJobTypeWorkspaceBuild,
|
2022-04-06 17:42:40 +00:00
|
|
|
StorageMethod: templateVersionJob.StorageMethod,
|
|
|
|
StorageSource: templateVersionJob.StorageSource,
|
2022-03-22 19:17:50 +00:00
|
|
|
Input: input,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("insert provisioner job: %w", err)
|
|
|
|
}
|
|
|
|
workspaceBuild, err = db.InsertWorkspaceBuild(r.Context(), database.InsertWorkspaceBuildParams{
|
2022-04-06 17:42:40 +00:00
|
|
|
ID: workspaceBuildID,
|
|
|
|
CreatedAt: database.Now(),
|
|
|
|
UpdatedAt: database.Now(),
|
|
|
|
WorkspaceID: workspace.ID,
|
|
|
|
TemplateVersionID: templateVersion.ID,
|
|
|
|
Name: namesgenerator.GetRandomName(1),
|
|
|
|
InitiatorID: apiKey.UserID,
|
|
|
|
Transition: database.WorkspaceTransitionStart,
|
|
|
|
JobID: provisionerJob.ID,
|
2022-03-22 19:17:50 +00:00
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("insert workspace build: %w", err)
|
|
|
|
}
|
|
|
|
return nil
|
2022-03-07 17:40:54 +00:00
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
2022-03-22 19:17:50 +00:00
|
|
|
Message: fmt.Sprintf("create workspace: %s", err),
|
2022-03-07 17:40:54 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-04-12 15:17:33 +00:00
|
|
|
httpapi.Write(rw, http.StatusCreated, convertWorkspace(workspace,
|
2022-04-06 17:42:40 +00:00
|
|
|
convertWorkspaceBuild(workspaceBuild, convertProvisionerJob(templateVersionJob)), template))
|
2022-03-07 17:40:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (api *api) workspacesByUser(rw http.ResponseWriter, r *http.Request) {
|
|
|
|
user := httpmw.UserParam(r)
|
2022-03-22 19:17:50 +00:00
|
|
|
workspaces, err := api.Database.GetWorkspacesByUserID(r.Context(), database.GetWorkspacesByUserIDParams{
|
|
|
|
OwnerID: user.ID,
|
|
|
|
})
|
2022-03-07 17:40:54 +00:00
|
|
|
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
|
|
|
|
}
|
2022-03-22 19:17:50 +00:00
|
|
|
workspaceIDs := make([]uuid.UUID, 0, len(workspaces))
|
2022-04-06 17:42:40 +00:00
|
|
|
templateIDs := make([]uuid.UUID, 0, len(workspaces))
|
2022-03-07 17:40:54 +00:00
|
|
|
for _, workspace := range workspaces {
|
2022-03-22 19:17:50 +00:00
|
|
|
workspaceIDs = append(workspaceIDs, workspace.ID)
|
2022-04-06 17:42:40 +00:00
|
|
|
templateIDs = append(templateIDs, workspace.TemplateID)
|
2022-03-22 19:17:50 +00:00
|
|
|
}
|
|
|
|
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
|
|
|
|
}
|
2022-04-06 17:42:40 +00:00
|
|
|
templates, err := api.Database.GetTemplatesByIDs(r.Context(), templateIDs)
|
2022-03-22 19:17:50 +00:00
|
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
|
|
err = nil
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
2022-04-06 17:42:40 +00:00
|
|
|
Message: fmt.Sprintf("get templates: %s", err),
|
2022-03-22 19:17:50 +00:00
|
|
|
})
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-04-01 19:42:36 +00:00
|
|
|
buildByWorkspaceID := map[uuid.UUID]database.WorkspaceBuild{}
|
2022-03-22 19:17:50 +00:00
|
|
|
for _, workspaceBuild := range workspaceBuilds {
|
2022-04-01 19:42:36 +00:00
|
|
|
buildByWorkspaceID[workspaceBuild.WorkspaceID] = workspaceBuild
|
2022-03-22 19:17:50 +00:00
|
|
|
}
|
2022-04-06 17:42:40 +00:00
|
|
|
templateByID := map[uuid.UUID]database.Template{}
|
|
|
|
for _, template := range templates {
|
|
|
|
templateByID[template.ID] = template
|
2022-03-22 19:17:50 +00:00
|
|
|
}
|
2022-04-01 19:42:36 +00:00
|
|
|
jobByID := map[uuid.UUID]database.ProvisionerJob{}
|
2022-03-22 19:17:50 +00:00
|
|
|
for _, job := range jobs {
|
2022-04-01 19:42:36 +00:00
|
|
|
jobByID[job.ID] = job
|
2022-03-22 19:17:50 +00:00
|
|
|
}
|
|
|
|
apiWorkspaces := make([]codersdk.Workspace, 0, len(workspaces))
|
|
|
|
for _, workspace := range workspaces {
|
2022-04-01 19:42:36 +00:00
|
|
|
build, exists := buildByWorkspaceID[workspace.ID]
|
2022-03-22 19:17:50 +00:00
|
|
|
if !exists {
|
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
|
|
Message: fmt.Sprintf("build not found for workspace %q", workspace.Name),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-04-06 17:42:40 +00:00
|
|
|
template, exists := templateByID[workspace.TemplateID]
|
2022-03-22 19:17:50 +00:00
|
|
|
if !exists {
|
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
2022-04-06 17:42:40 +00:00
|
|
|
Message: fmt.Sprintf("template not found for workspace %q", workspace.Name),
|
2022-03-22 19:17:50 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-04-01 19:42:36 +00:00
|
|
|
job, exists := jobByID[build.JobID]
|
2022-03-22 19:17:50 +00:00
|
|
|
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,
|
2022-04-06 17:42:40 +00:00
|
|
|
convertWorkspace(workspace, convertWorkspaceBuild(build, convertProvisionerJob(job)), template))
|
2022-03-07 17:40:54 +00:00
|
|
|
}
|
2022-04-12 15:17:33 +00:00
|
|
|
|
|
|
|
httpapi.Write(rw, http.StatusOK, apiWorkspaces)
|
2022-03-07 17:40:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
2022-03-22 19:17:50 +00:00
|
|
|
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
|
|
|
|
}
|
2022-04-06 17:42:40 +00:00
|
|
|
template, err := api.Database.GetTemplateByID(r.Context(), workspace.TemplateID)
|
2022-03-22 19:17:50 +00:00
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
2022-04-06 17:42:40 +00:00
|
|
|
Message: fmt.Sprintf("get template: %s", err),
|
2022-03-22 19:17:50 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-03-07 17:40:54 +00:00
|
|
|
|
2022-04-12 15:17:33 +00:00
|
|
|
httpapi.Write(rw, http.StatusOK, convertWorkspace(workspace,
|
2022-04-06 17:42:40 +00:00
|
|
|
convertWorkspaceBuild(build, convertProvisionerJob(job)), template))
|
2022-03-07 17:40:54 +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-03-22 19:17:50 +00:00
|
|
|
func convertUser(user database.User) codersdk.User {
|
|
|
|
return codersdk.User{
|
2022-01-25 19:52:58 +00:00
|
|
|
ID: user.ID,
|
|
|
|
Email: user.Email,
|
|
|
|
CreatedAt: user.CreatedAt,
|
|
|
|
Username: user.Username,
|
2022-04-12 14:05:21 +00:00
|
|
|
Name: user.Name,
|
2022-01-25 19:52:58 +00:00
|
|
|
}
|
|
|
|
}
|
2022-04-22 20:27:55 +00:00
|
|
|
|
|
|
|
func convertUsers(users []database.User) []codersdk.User {
|
|
|
|
converted := make([]codersdk.User, 0, len(users))
|
|
|
|
for _, u := range users {
|
|
|
|
converted = append(converted, convertUser(u))
|
|
|
|
}
|
|
|
|
return converted
|
|
|
|
}
|