mirror of
https://github.com/coder/coder.git
synced 2026-06-03 04:58:23 +00:00
344 lines
10 KiB
Go
344 lines
10 KiB
Go
package coderd
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/google/uuid"
|
|
"golang.org/x/xerrors"
|
|
|
|
"github.com/coder/coder/v2/coderd/audit"
|
|
"github.com/coder/coder/v2/coderd/database"
|
|
"github.com/coder/coder/v2/coderd/database/db2sdk"
|
|
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
|
"github.com/coder/coder/v2/coderd/database/dbtime"
|
|
"github.com/coder/coder/v2/coderd/httpapi"
|
|
"github.com/coder/coder/v2/coderd/httpmw"
|
|
"github.com/coder/coder/v2/coderd/rbac/rolestore"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
)
|
|
|
|
// @Summary Update organization
|
|
// @ID update-organization
|
|
// @Security CoderSessionToken
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Tags Organizations
|
|
// @Param organization path string true "Organization ID or name"
|
|
// @Param request body codersdk.UpdateOrganizationRequest true "Patch organization request"
|
|
// @Success 200 {object} codersdk.Organization
|
|
// @Router /api/v2/organizations/{organization} [patch]
|
|
func (api *API) patchOrganization(rw http.ResponseWriter, r *http.Request) {
|
|
var (
|
|
ctx = r.Context()
|
|
organization = httpmw.OrganizationParam(r)
|
|
auditor = api.AGPL.Auditor.Load()
|
|
aReq, commitAudit = audit.InitRequest[database.Organization](rw, &audit.RequestParams{
|
|
Audit: *auditor,
|
|
Log: api.Logger,
|
|
Request: r,
|
|
Action: database.AuditActionWrite,
|
|
OrganizationID: organization.ID,
|
|
})
|
|
)
|
|
aReq.Old = organization
|
|
defer commitAudit()
|
|
|
|
var req codersdk.UpdateOrganizationRequest
|
|
if !httpapi.Read(ctx, rw, r, &req) {
|
|
return
|
|
}
|
|
|
|
// "default" is a reserved name that always refers to the default org (much like the way we
|
|
// use "me" for users).
|
|
if req.Name == codersdk.DefaultOrganization {
|
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
|
Message: fmt.Sprintf("Organization name %q is reserved.", codersdk.DefaultOrganization),
|
|
})
|
|
return
|
|
}
|
|
|
|
err := database.ReadModifyUpdate(api.Database, func(tx database.Store) error {
|
|
var err error
|
|
organization, err = tx.GetOrganizationByID(ctx, organization.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
updateOrgParams := database.UpdateOrganizationParams{
|
|
UpdatedAt: dbtime.Now(),
|
|
ID: organization.ID,
|
|
Name: organization.Name,
|
|
DisplayName: organization.DisplayName,
|
|
Description: organization.Description,
|
|
Icon: organization.Icon,
|
|
}
|
|
|
|
if req.Name != "" {
|
|
updateOrgParams.Name = req.Name
|
|
}
|
|
if req.DisplayName != "" {
|
|
updateOrgParams.DisplayName = req.DisplayName
|
|
}
|
|
if req.Description != nil {
|
|
updateOrgParams.Description = *req.Description
|
|
}
|
|
if req.Icon != nil {
|
|
updateOrgParams.Icon = *req.Icon
|
|
}
|
|
|
|
organization, err = tx.UpdateOrganization(ctx, updateOrgParams)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
})
|
|
|
|
if httpapi.Is404Error(err) {
|
|
httpapi.ResourceNotFound(rw)
|
|
return
|
|
}
|
|
if database.IsUniqueViolation(err) {
|
|
httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{
|
|
Message: fmt.Sprintf("Organization already exists with the name %q.", req.Name),
|
|
Validations: []codersdk.ValidationError{{
|
|
Field: "name",
|
|
Detail: "This value is already in use and should be unique.",
|
|
}},
|
|
})
|
|
return
|
|
}
|
|
if err != nil {
|
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
|
Message: "Internal error updating organization.",
|
|
Detail: fmt.Sprintf("update organization: %s", err.Error()),
|
|
})
|
|
return
|
|
}
|
|
|
|
aReq.New = organization
|
|
httpapi.Write(ctx, rw, http.StatusOK, db2sdk.Organization(organization))
|
|
}
|
|
|
|
// @Summary Delete organization
|
|
// @ID delete-organization
|
|
// @Security CoderSessionToken
|
|
// @Produce json
|
|
// @Tags Organizations
|
|
// @Param organization path string true "Organization ID or name"
|
|
// @Success 200 {object} codersdk.Response
|
|
// @Router /api/v2/organizations/{organization} [delete]
|
|
func (api *API) deleteOrganization(rw http.ResponseWriter, r *http.Request) {
|
|
var (
|
|
ctx = r.Context()
|
|
organization = httpmw.OrganizationParam(r)
|
|
auditor = api.AGPL.Auditor.Load()
|
|
aReq, commitAudit = audit.InitRequest[database.Organization](rw, &audit.RequestParams{
|
|
Audit: *auditor,
|
|
Log: api.Logger,
|
|
Request: r,
|
|
Action: database.AuditActionDelete,
|
|
OrganizationID: organization.ID,
|
|
})
|
|
)
|
|
aReq.Old = organization
|
|
defer commitAudit()
|
|
|
|
if organization.IsDefault {
|
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
|
Message: "Default organization cannot be deleted.",
|
|
})
|
|
return
|
|
}
|
|
|
|
err := api.Database.InTx(func(tx database.Store) error {
|
|
err := tx.UpdateOrganizationDeletedByID(ctx, database.UpdateOrganizationDeletedByIDParams{
|
|
ID: organization.ID,
|
|
UpdatedAt: dbtime.Now(),
|
|
})
|
|
if err != nil {
|
|
return xerrors.Errorf("delete organization: %w", err)
|
|
}
|
|
return nil
|
|
}, nil)
|
|
if err != nil {
|
|
orgResourcesRow, queryErr := api.Database.GetOrganizationResourceCountByID(ctx, organization.ID)
|
|
if queryErr != nil {
|
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
|
Message: "Internal error deleting organization.",
|
|
Detail: fmt.Sprintf("delete organization: %s", err.Error()),
|
|
})
|
|
|
|
return
|
|
}
|
|
|
|
detailParts := make([]string, 0)
|
|
|
|
addDetailPart := func(resource string, count int64) {
|
|
if count == 1 {
|
|
detailParts = append(detailParts, fmt.Sprintf("1 %s", resource))
|
|
} else if count > 1 {
|
|
detailParts = append(detailParts, fmt.Sprintf("%d %ss", count, resource))
|
|
}
|
|
}
|
|
|
|
addDetailPart("workspace", orgResourcesRow.WorkspaceCount)
|
|
addDetailPart("template", orgResourcesRow.TemplateCount)
|
|
|
|
// There will always be one member and group so instead we need to check that
|
|
// the count is greater than one.
|
|
addDetailPart("member", orgResourcesRow.MemberCount-1)
|
|
addDetailPart("group", orgResourcesRow.GroupCount-1)
|
|
|
|
addDetailPart("provisioner key", orgResourcesRow.ProvisionerKeyCount)
|
|
|
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
|
Message: "Error deleting organization.",
|
|
Detail: fmt.Sprintf("This organization has %s that must be deleted first.", strings.Join(detailParts, ", ")),
|
|
})
|
|
|
|
return
|
|
}
|
|
|
|
aReq.New = database.Organization{}
|
|
httpapi.Write(ctx, rw, http.StatusOK, codersdk.Response{
|
|
Message: "Organization has been deleted.",
|
|
})
|
|
}
|
|
|
|
// @Summary Create organization
|
|
// @ID create-organization
|
|
// @Security CoderSessionToken
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Tags Organizations
|
|
// @Param request body codersdk.CreateOrganizationRequest true "Create organization request"
|
|
// @Success 201 {object} codersdk.Organization
|
|
// @Router /api/v2/organizations [post]
|
|
func (api *API) postOrganizations(rw http.ResponseWriter, r *http.Request) {
|
|
var (
|
|
// organizationID is required before the audit log entry is created.
|
|
organizationID = uuid.New()
|
|
ctx = r.Context()
|
|
apiKey = httpmw.APIKey(r)
|
|
auditor = api.AGPL.Auditor.Load()
|
|
aReq, commitAudit = audit.InitRequest[database.Organization](rw, &audit.RequestParams{
|
|
Audit: *auditor,
|
|
Log: api.Logger,
|
|
Request: r,
|
|
Action: database.AuditActionCreate,
|
|
OrganizationID: organizationID,
|
|
})
|
|
)
|
|
aReq.Old = database.Organization{}
|
|
defer commitAudit()
|
|
|
|
var req codersdk.CreateOrganizationRequest
|
|
if !httpapi.Read(ctx, rw, r, &req) {
|
|
return
|
|
}
|
|
|
|
if req.Name == codersdk.DefaultOrganization {
|
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
|
Message: fmt.Sprintf("Organization name %q is reserved.", codersdk.DefaultOrganization),
|
|
})
|
|
return
|
|
}
|
|
|
|
_, err := api.Database.GetOrganizationByName(ctx, database.GetOrganizationByNameParams{
|
|
Name: req.Name,
|
|
Deleted: false,
|
|
})
|
|
if err == nil {
|
|
httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{
|
|
Message: "Organization already exists with that name.",
|
|
})
|
|
return
|
|
}
|
|
if !xerrors.Is(err, sql.ErrNoRows) {
|
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
|
Message: fmt.Sprintf("Internal error fetching organization %q.", req.Name),
|
|
Detail: err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
var organization database.Organization
|
|
err = api.Database.InTx(func(tx database.Store) error {
|
|
// Serialize creation and reconciliation of the org-member
|
|
// system role across coderd instances (e.g. during rolling
|
|
// restarts).
|
|
err := tx.AcquireLock(ctx, database.LockIDReconcileSystemRoles)
|
|
if err != nil {
|
|
return xerrors.Errorf("acquire system roles reconciliation lock: %w", err)
|
|
}
|
|
|
|
if req.DisplayName == "" {
|
|
req.DisplayName = req.Name
|
|
}
|
|
|
|
organization, err = tx.InsertOrganization(ctx, database.InsertOrganizationParams{
|
|
ID: organizationID,
|
|
Name: req.Name,
|
|
DisplayName: req.DisplayName,
|
|
Description: req.Description,
|
|
Icon: req.Icon,
|
|
CreatedAt: dbtime.Now(),
|
|
UpdatedAt: dbtime.Now(),
|
|
})
|
|
if err != nil {
|
|
return xerrors.Errorf("create organization: %w", err)
|
|
}
|
|
|
|
// Populate the placeholder system role(s) that the DB trigger
|
|
// created for us.
|
|
//nolint:gocritic // ReconcileOrgMemberRole needs the system:update
|
|
// permission that user doesn't have.
|
|
sysCtx := dbauthz.AsSystemRestricted(ctx)
|
|
for roleName := range rolestore.SystemRoleNames {
|
|
_, _, err = rolestore.ReconcileSystemRole(sysCtx, tx, database.CustomRole{
|
|
Name: roleName,
|
|
OrganizationID: uuid.NullUUID{UUID: organizationID, Valid: true},
|
|
}, organization)
|
|
if err != nil {
|
|
return xerrors.Errorf("reconcile %s role for organization %s: %w",
|
|
roleName, organizationID, err)
|
|
}
|
|
}
|
|
|
|
_, err = tx.InsertOrganizationMember(ctx, database.InsertOrganizationMemberParams{
|
|
OrganizationID: organization.ID,
|
|
UserID: apiKey.UserID,
|
|
CreatedAt: dbtime.Now(),
|
|
UpdatedAt: dbtime.Now(),
|
|
Roles: []string{
|
|
// TODO: When organizations are allowed to be created, we should
|
|
// come back to determining the default role of the person who
|
|
// creates the org. Until that happens, all users in an organization
|
|
// should be just regular members.
|
|
},
|
|
})
|
|
if err != nil {
|
|
return xerrors.Errorf("create organization admin: %w", err)
|
|
}
|
|
|
|
_, err = tx.InsertAllUsersGroup(ctx, organization.ID)
|
|
if err != nil {
|
|
return xerrors.Errorf("create %q group: %w", database.EveryoneGroup, err)
|
|
}
|
|
return nil
|
|
}, nil)
|
|
if err != nil {
|
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
|
Message: "Internal error inserting organization member.",
|
|
Detail: err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
aReq.New = organization
|
|
httpapi.Write(ctx, rw, http.StatusCreated, db2sdk.Organization(organization))
|
|
}
|