Skip to content

Commit 3801701

Browse files
authored
fix(coderd): disallow POSTing a workspace build on a deleted workspace (#20584)
- Adds a check on /api/v2/workspacebuilds to disallow creating a START or STOP build if the workspace is deleted. - DELETEs are still allowed.
1 parent 984a834 commit 3801701

File tree

2 files changed

+71
-0
lines changed

2 files changed

+71
-0
lines changed

coderd/workspacebuilds.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,15 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
335335
return
336336
}
337337

338+
// We want to allow a delete build for a deleted workspace, but not a start or stop build.
339+
if workspace.Deleted && createBuild.Transition != codersdk.WorkspaceTransitionDelete {
340+
httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{
341+
Message: fmt.Sprintf("Cannot %s a deleted workspace!", createBuild.Transition),
342+
Detail: "This workspace has been deleted and cannot be modified.",
343+
})
344+
return
345+
}
346+
338347
apiBuild, err := api.postWorkspaceBuildsInternal(
339348
ctx,
340349
apiKey,

coderd/workspacebuilds_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1840,6 +1840,68 @@ func TestPostWorkspaceBuild(t *testing.T) {
18401840
require.NoError(t, err)
18411841
require.Equal(t, codersdk.BuildReasonDashboard, build.Reason)
18421842
})
1843+
t.Run("DeletedWorkspace", func(t *testing.T) {
1844+
t.Parallel()
1845+
1846+
// Given: a workspace that has already been deleted
1847+
var (
1848+
ctx = testutil.Context(t, testutil.WaitShort)
1849+
logger = slogtest.Make(t, &slogtest.Options{}).Leveled(slog.LevelError)
1850+
adminClient, db = coderdtest.NewWithDatabase(t, &coderdtest.Options{
1851+
Logger: &logger,
1852+
})
1853+
admin = coderdtest.CreateFirstUser(t, adminClient)
1854+
workspaceOwnerClient, member1 = coderdtest.CreateAnotherUser(t, adminClient, admin.OrganizationID)
1855+
otherMemberClient, _ = coderdtest.CreateAnotherUser(t, adminClient, admin.OrganizationID)
1856+
ws = dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{OwnerID: member1.ID, OrganizationID: admin.OrganizationID}).
1857+
Seed(database.WorkspaceBuild{Transition: database.WorkspaceTransitionDelete}).
1858+
Do()
1859+
)
1860+
1861+
// This needs to be done separately as provisionerd handles marking the workspace as deleted
1862+
// and we're skipping provisionerd here for speed.
1863+
require.NoError(t, db.UpdateWorkspaceDeletedByID(dbauthz.AsProvisionerd(ctx), database.UpdateWorkspaceDeletedByIDParams{
1864+
ID: ws.Workspace.ID,
1865+
Deleted: true,
1866+
}))
1867+
1868+
// Assert test invariant: Workspace should be deleted
1869+
dbWs, err := db.GetWorkspaceByID(dbauthz.AsProvisionerd(ctx), ws.Workspace.ID)
1870+
require.NoError(t, err)
1871+
require.True(t, dbWs.Deleted, "workspace should be deleted")
1872+
1873+
for _, tc := range []struct {
1874+
user *codersdk.Client
1875+
tr codersdk.WorkspaceTransition
1876+
expectStatus int
1877+
}{
1878+
// You should not be allowed to mess with a workspace you don't own, regardless of its deleted state.
1879+
{otherMemberClient, codersdk.WorkspaceTransitionStart, http.StatusNotFound},
1880+
{otherMemberClient, codersdk.WorkspaceTransitionStop, http.StatusNotFound},
1881+
{otherMemberClient, codersdk.WorkspaceTransitionDelete, http.StatusNotFound},
1882+
// Starting or stopping a workspace is not allowed when it is deleted.
1883+
{workspaceOwnerClient, codersdk.WorkspaceTransitionStart, http.StatusConflict},
1884+
{workspaceOwnerClient, codersdk.WorkspaceTransitionStop, http.StatusConflict},
1885+
// We allow a delete just in case a retry is required. In most cases, this will be a no-op.
1886+
// Note: this is the last test case because it will change the state of the workspace.
1887+
{workspaceOwnerClient, codersdk.WorkspaceTransitionDelete, http.StatusOK},
1888+
} {
1889+
// When: we create a workspace build with the given transition
1890+
_, err = tc.user.CreateWorkspaceBuild(ctx, ws.Workspace.ID, codersdk.CreateWorkspaceBuildRequest{
1891+
Transition: tc.tr,
1892+
})
1893+
1894+
// Then: we allow ONLY a delete build for a deleted workspace.
1895+
if tc.expectStatus < http.StatusBadRequest {
1896+
require.NoError(t, err, "creating a %s build for a deleted workspace should not error", tc.tr)
1897+
} else {
1898+
var apiError *codersdk.Error
1899+
require.Error(t, err, "creating a %s build for a deleted workspace should return an error", tc.tr)
1900+
require.ErrorAs(t, err, &apiError)
1901+
require.Equal(t, tc.expectStatus, apiError.StatusCode())
1902+
}
1903+
}
1904+
})
18431905
}
18441906

18451907
func TestWorkspaceBuildTimings(t *testing.T) {

0 commit comments

Comments
 (0)