@@ -2,9 +2,18 @@ package cli_test
2
2
3
3
import (
4
4
"context"
5
+ "database/sql"
5
6
"fmt"
6
7
"io"
7
8
"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"
8
17
9
18
"github.com/stretchr/testify/assert"
10
19
"github.com/stretchr/testify/require"
@@ -209,4 +218,225 @@ func TestDelete(t *testing.T) {
209
218
cancel ()
210
219
<- doneChan
211
220
})
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
212
442
}
0 commit comments