mirror of
https://github.com/coder/coder.git
synced 2026-09-21 20:51:01 +08:00
fix(coderd): enforce required external auth on workspace create (#26314)
Required external auth (`optional = false`) was only enforced by client-side preflight checks, so creating a workspace via the REST API succeeded even when the owner had never authenticated, producing a broken workspace. `createWorkspace` now validates the workspace owner's external auth server-side and returns 403 before any row is inserted or prebuild is claimed. The owner (not the initiator) is checked because build-time token injection uses their links, so this also covers admin-on-behalf-of creates and prebuild claims. Use `optional = true` to allow pre-provisioning for unauthenticated users. Fixes PLAT-241. > This PR was generated by Coder Agents on behalf of @dylanhuff-at-coder.
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
"net/http"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
@@ -595,6 +596,27 @@ func createWorkspace(
|
||||
})
|
||||
}
|
||||
|
||||
// Required external auth is otherwise only enforced by client-side preflight
|
||||
// checks in the CLI and UI, so API-created workspaces must be validated here
|
||||
// before any workspace row is inserted or prebuilt workspace is claimed.
|
||||
templateVersionID := req.TemplateVersionID
|
||||
if templateVersionID == uuid.Nil {
|
||||
templateVersionID = template.ActiveVersionID
|
||||
}
|
||||
templateVersion, err := api.Database.GetTemplateVersionByID(ctx, templateVersionID)
|
||||
if err != nil {
|
||||
if httpapi.Is404Error(err) {
|
||||
return codersdk.Workspace{}, httperror.ErrResourceNotFound
|
||||
}
|
||||
return codersdk.Workspace{}, httperror.NewResponseError(http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error fetching template version.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
}
|
||||
if err := api.requireWorkspaceOwnerExternalAuth(ctx, templateVersion, owner.ID); err != nil {
|
||||
return codersdk.Workspace{}, err
|
||||
}
|
||||
|
||||
dbAutostartSchedule, err := validWorkspaceSchedule(req.AutostartSchedule)
|
||||
if err != nil {
|
||||
return codersdk.Workspace{}, httperror.NewResponseError(http.StatusBadRequest, codersdk.Response{
|
||||
@@ -892,6 +914,50 @@ func createWorkspace(
|
||||
return w, nil
|
||||
}
|
||||
|
||||
// requireWorkspaceOwnerExternalAuth returns a 403 response error when the
|
||||
// workspace owner has not authenticated with every required (non-optional)
|
||||
// external auth provider referenced by the template version. Token injection
|
||||
// at build time uses the owner's external auth links, so the owner is the
|
||||
// subject of the check even when another user initiates the build.
|
||||
func (api *API) requireWorkspaceOwnerExternalAuth(ctx context.Context, templateVersion database.TemplateVersion, ownerID uuid.UUID) error {
|
||||
//nolint:gocritic // System access is required to validate the workspace owner's external auth links because admins and API clients may create workspaces for other users.
|
||||
providers, err := api.templateVersionExternalAuthForUser(dbauthz.AsSystemRestricted(ctx), templateVersion, ownerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var (
|
||||
missingNames []string
|
||||
validations []codersdk.ValidationError
|
||||
)
|
||||
for _, provider := range providers {
|
||||
if provider.Optional || provider.Authenticated {
|
||||
continue
|
||||
}
|
||||
name := provider.DisplayName
|
||||
if name == "" {
|
||||
name = provider.ID
|
||||
}
|
||||
missingNames = append(missingNames, name)
|
||||
validations = append(validations, codersdk.ValidationError{
|
||||
Field: "external_auth",
|
||||
Detail: provider.ID,
|
||||
})
|
||||
}
|
||||
if len(missingNames) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return httperror.NewResponseError(http.StatusForbidden, codersdk.Response{
|
||||
Message: "External authentication is required to create a workspace with this template.",
|
||||
Detail: fmt.Sprintf(
|
||||
"The workspace owner must authenticate with the following external auth providers: %s.",
|
||||
strings.Join(missingNames, ", "),
|
||||
),
|
||||
Validations: validations,
|
||||
})
|
||||
}
|
||||
|
||||
func requestTemplate(ctx context.Context, req codersdk.CreateWorkspaceRequest, db database.Store) (database.Template, error) {
|
||||
// If we were given a `TemplateVersionID`, we need to determine the `TemplateID` from it.
|
||||
templateID := req.TemplateID
|
||||
|
||||
Reference in New Issue
Block a user