@@ -2,9 +2,18 @@ package cli_test
22
33import (
44"context"
5+ "database/sql"
56"fmt"
67"io"
78"testing"
9+ "time"
10+
11+ "github.com/google/uuid"
12+
13+ "github.com/coder/coder/v2/coderd/database"
14+ "github.com/coder/coder/v2/coderd/database/dbgen"
15+ "github.com/coder/coder/v2/coderd/database/pubsub"
16+ "github.com/coder/quartz"
817
918"github.com/stretchr/testify/assert"
1019"github.com/stretchr/testify/require"
@@ -209,4 +218,225 @@ func TestDelete(t *testing.T) {
209218cancel ()
210219<- doneChan
211220})
221+
222+ t .Run ("Prebuilt workspace delete permissions" ,func (t * testing.T ) {
223+ t .Parallel ()
224+ if ! dbtestutil .WillUsePostgres () {
225+ t .Skip ("this test requires postgres" )
226+ }
227+
228+ clock := quartz .NewMock (t )
229+ ctx := testutil .Context (t ,testutil .WaitSuperLong )
230+
231+ // Setup
232+ db ,pb := dbtestutil .NewDB (t ,dbtestutil .WithDumpOnFailure ())
233+ client ,_ := coderdtest .NewWithProvisionerCloser (t ,& coderdtest.Options {
234+ Database :db ,
235+ Pubsub :pb ,
236+ IncludeProvisionerDaemon :true ,
237+ })
238+ owner := coderdtest .CreateFirstUser (t ,client )
239+ orgID := owner .OrganizationID
240+
241+ // Given a template version with a preset and a template
242+ version := coderdtest .CreateTemplateVersion (t ,client ,orgID ,nil )
243+ coderdtest .AwaitTemplateVersionJobCompleted (t ,client ,version .ID )
244+ preset := setupTestDBPreset (t ,db ,version .ID )
245+ template := coderdtest .CreateTemplate (t ,client ,orgID ,version .ID )
246+
247+ cases := []struct {
248+ name string
249+ client * codersdk.Client
250+ expectedPrebuiltDeleteErrMsg string
251+ expectedWorkspaceDeleteErrMsg string
252+ }{
253+ // Users with the OrgAdmin role should be able to delete both normal and prebuilt workspaces
254+ {
255+ name :"OrgAdmin" ,
256+ client :func ()* codersdk.Client {
257+ client ,_ := coderdtest .CreateAnotherUser (t ,client ,orgID ,rbac .ScopedRoleOrgAdmin (orgID ))
258+ return client
259+ }(),
260+ },
261+ // Users with the TemplateAdmin role should be able to delete prebuilt workspaces, but not normal workspaces
262+ {
263+ name :"TemplateAdmin" ,
264+ client :func ()* codersdk.Client {
265+ client ,_ := coderdtest .CreateAnotherUser (t ,client ,orgID ,rbac .RoleTemplateAdmin ())
266+ return client
267+ }(),
268+ expectedWorkspaceDeleteErrMsg :"unexpected status code 403: You do not have permission to delete this workspace." ,
269+ },
270+ // Users with the OrgTemplateAdmin role should be able to delete prebuilt workspaces, but not normal workspaces
271+ {
272+ name :"OrgTemplateAdmin" ,
273+ client :func ()* codersdk.Client {
274+ client ,_ := coderdtest .CreateAnotherUser (t ,client ,orgID ,rbac .ScopedRoleOrgTemplateAdmin (orgID ))
275+ return client
276+ }(),
277+ expectedWorkspaceDeleteErrMsg :"unexpected status code 403: You do not have permission to delete this workspace." ,
278+ },
279+ // Users with the Member role should not be able to delete prebuilt or normal workspaces
280+ {
281+ name :"Member" ,
282+ client :func ()* codersdk.Client {
283+ client ,_ := coderdtest .CreateAnotherUser (t ,client ,orgID ,rbac .RoleMember ())
284+ return client
285+ }(),
286+ expectedPrebuiltDeleteErrMsg :"unexpected status code 404: Resource not found or you do not have access to this resource" ,
287+ expectedWorkspaceDeleteErrMsg :"unexpected status code 404: Resource not found or you do not have access to this resource" ,
288+ },
289+ }
290+
291+ for _ ,tc := range cases {
292+ tc := tc
293+ t .Run (tc .name ,func (t * testing.T ) {
294+ t .Parallel ()
295+
296+ // Create one prebuilt workspace (owned by system user) and one normal workspace (owned by a user)
297+ // Each workspace is persisted in the DB along with associated workspace jobs and builds.
298+ dbPrebuiltWorkspace := setupTestDBWorkspace (t ,clock ,db ,pb ,orgID ,database .PrebuildsSystemUserID ,template .ID ,version .ID ,preset .ID )
299+ userWorkspaceOwner ,err := client .User (context .Background (),"testUser" )
300+ require .NoError (t ,err )
301+ dbUserWorkspace := setupTestDBWorkspace (t ,clock ,db ,pb ,orgID ,userWorkspaceOwner .ID ,template .ID ,version .ID ,preset .ID )
302+
303+ assertWorkspaceDelete := func (
304+ runClient * codersdk.Client ,
305+ workspace database.Workspace ,
306+ workspaceOwner string ,
307+ expectedErr string ,
308+ ) {
309+ t .Helper ()
310+
311+ // Attempt to delete the workspace as the test client
312+ inv ,root := clitest .New (t ,"delete" ,workspaceOwner + "/" + workspace .Name ,"-y" )
313+ clitest .SetupConfig (t ,runClient ,root )
314+ doneChan := make (chan struct {})
315+ pty := ptytest .New (t ).Attach (inv )
316+ var runErr error
317+ go func () {
318+ defer close (doneChan )
319+ runErr = inv .Run ()
320+ }()
321+
322+ // Validate the result based on the expected error message
323+ if expectedErr != "" {
324+ <- doneChan
325+ require .Error (t ,runErr )
326+ require .Contains (t ,runErr .Error (),expectedErr )
327+ }else {
328+ pty .ExpectMatch ("has been deleted" )
329+ <- doneChan
330+
331+ // When running with the race detector on, we sometimes get an EOF.
332+ if runErr != nil {
333+ assert .ErrorIs (t ,runErr ,io .EOF )
334+ }
335+
336+ // Verify that the workspace is now marked as deleted
337+ _ ,err := client .Workspace (context .Background (),workspace .ID )
338+ require .ErrorContains (t ,err ,"was deleted" )
339+ }
340+ }
341+
342+ // Ensure at least one prebuilt workspace is reported as running in the database
343+ testutil .Eventually (ctx ,t ,func (ctx context.Context ) (done bool ) {
344+ running ,err := db .GetRunningPrebuiltWorkspaces (ctx )
345+ if ! assert .NoError (t ,err )|| ! assert .GreaterOrEqual (t ,len (running ),1 ) {
346+ return false
347+ }
348+ return true
349+ },testutil .IntervalMedium ,"running prebuilt workspaces timeout" )
350+
351+ runningWorkspaces ,err := db .GetRunningPrebuiltWorkspaces (ctx )
352+ require .NoError (t ,err )
353+ require .GreaterOrEqual (t ,len (runningWorkspaces ),1 )
354+
355+ // Get the full prebuilt workspace object from the DB
356+ prebuiltWorkspace ,err := db .GetWorkspaceByID (ctx ,dbPrebuiltWorkspace .ID )
357+ require .NoError (t ,err )
358+
359+ // Assert the prebuilt workspace deletion
360+ assertWorkspaceDelete (tc .client ,prebuiltWorkspace ,"prebuilds" ,tc .expectedPrebuiltDeleteErrMsg )
361+
362+ // Get the full user workspace object from the DB
363+ userWorkspace ,err := db .GetWorkspaceByID (ctx ,dbUserWorkspace .ID )
364+ require .NoError (t ,err )
365+
366+ // Assert the user workspace deletion
367+ assertWorkspaceDelete (tc .client ,userWorkspace ,userWorkspaceOwner .Username ,tc .expectedWorkspaceDeleteErrMsg )
368+ })
369+ }
370+ })
371+ }
372+
373+ func setupTestDBPreset (
374+ t * testing.T ,
375+ db database.Store ,
376+ templateVersionID uuid.UUID ,
377+ ) database.TemplateVersionPreset {
378+ t .Helper ()
379+
380+ preset := dbgen .Preset (t ,db , database.InsertPresetParams {
381+ TemplateVersionID :templateVersionID ,
382+ Name :"preset-test" ,
383+ DesiredInstances : sql.NullInt32 {
384+ Valid :true ,
385+ Int32 :1 ,
386+ },
387+ })
388+ dbgen .PresetParameter (t ,db , database.InsertPresetParametersParams {
389+ TemplateVersionPresetID :preset .ID ,
390+ Names : []string {"test" },
391+ Values : []string {"test" },
392+ })
393+
394+ return preset
395+ }
396+
397+ func setupTestDBWorkspace (
398+ t * testing.T ,
399+ clock quartz.Clock ,
400+ db database.Store ,
401+ ps pubsub.Pubsub ,
402+ orgID uuid.UUID ,
403+ ownerID uuid.UUID ,
404+ templateID uuid.UUID ,
405+ templateVersionID uuid.UUID ,
406+ presetID uuid.UUID ,
407+ ) database.WorkspaceTable {
408+ t .Helper ()
409+
410+ workspace := dbgen .Workspace (t ,db , database.WorkspaceTable {
411+ TemplateID :templateID ,
412+ OrganizationID :orgID ,
413+ OwnerID :ownerID ,
414+ Deleted :false ,
415+ CreatedAt :time .Now ().Add (- time .Hour * 2 ),
416+ })
417+ job := dbgen .ProvisionerJob (t ,db ,ps , database.ProvisionerJob {
418+ InitiatorID :ownerID ,
419+ CreatedAt :time .Now ().Add (- time .Hour * 2 ),
420+ StartedAt : sql.NullTime {Time :clock .Now ().Add (- time .Hour * 2 ),Valid :true },
421+ CompletedAt : sql.NullTime {Time :clock .Now ().Add (- time .Hour ),Valid :true },
422+ OrganizationID :orgID ,
423+ })
424+ workspaceBuild := dbgen .WorkspaceBuild (t ,db , database.WorkspaceBuild {
425+ WorkspaceID :workspace .ID ,
426+ InitiatorID :ownerID ,
427+ TemplateVersionID :templateVersionID ,
428+ JobID :job .ID ,
429+ TemplateVersionPresetID : uuid.NullUUID {UUID :presetID ,Valid :true },
430+ Transition :database .WorkspaceTransitionStart ,
431+ CreatedAt :clock .Now (),
432+ })
433+ dbgen .WorkspaceBuildParameters (t ,db , []database.WorkspaceBuildParameter {
434+ {
435+ WorkspaceBuildID :workspaceBuild .ID ,
436+ Name :"test" ,
437+ Value :"test" ,
438+ },
439+ })
440+
441+ return workspace
212442}