@@ -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,214 @@ func TestDelete(t *testing.T) {
209
218
cancel ()
210
219
<- doneChan
211
220
})
221
+
222
+ t .Run ("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 the owner 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
+ dbUserWorkspace := setupTestDBWorkspace (t , clock , db , pb , orgID , owner .UserID , template .ID , version .ID , preset .ID )
300
+
301
+ // Ensure at least one prebuilt workspace is reported as running in the database
302
+ testutil .Eventually (ctx , t , func (ctx context.Context ) (done bool ) {
303
+ running , err := db .GetRunningPrebuiltWorkspaces (ctx )
304
+ if ! assert .NoError (t , err ) || ! assert .GreaterOrEqual (t , len (running ), 1 ) {
305
+ return false
306
+ }
307
+ return true
308
+ }, testutil .IntervalMedium , "running prebuilt workspaces timeout" )
309
+
310
+ runningWorkspaces , err := db .GetRunningPrebuiltWorkspaces (ctx )
311
+ require .NoError (t , err )
312
+ require .GreaterOrEqual (t , len (runningWorkspaces ), 1 )
313
+
314
+ // Get the full prebuilt workspace object from the DB
315
+ prebuiltWorkspace , err := db .GetWorkspaceByID (ctx , runningWorkspaces [0 ].ID )
316
+ require .NoError (t , err )
317
+
318
+ // Attempt to delete the prebuilt workspace as the test client
319
+ build , err := tc .client .CreateWorkspaceBuild (ctx , dbPrebuiltWorkspace .ID , codersdk.CreateWorkspaceBuildRequest {
320
+ Transition : codersdk .WorkspaceTransitionDelete ,
321
+ })
322
+ // Validate the result based on the expected error message
323
+ if tc .expectedPrebuiltDeleteErrMsg != "" {
324
+ require .Error (t , err )
325
+ require .Contains (t , err .Error (), tc .expectedPrebuiltDeleteErrMsg )
326
+ } else {
327
+ require .NoError (t , err , "delete the prebuilt workspace" )
328
+ coderdtest .AwaitWorkspaceBuildJobCompleted (t , client , build .ID )
329
+
330
+ // Verify that the prebuilt workspace is now marked as deleted
331
+ deletedWorkspace , err := client .DeletedWorkspace (ctx , prebuiltWorkspace .ID )
332
+ require .NoError (t , err )
333
+ require .Equal (t , prebuiltWorkspace .ID , deletedWorkspace .ID )
334
+ }
335
+
336
+ // Get the full user workspace object from the DB
337
+ userWorkspace , err := db .GetWorkspaceByID (ctx , dbUserWorkspace .ID )
338
+ require .NoError (t , err )
339
+
340
+ // Attempt to delete the prebuilt workspace as the test client
341
+ build , err = tc .client .CreateWorkspaceBuild (ctx , dbUserWorkspace .ID , codersdk.CreateWorkspaceBuildRequest {
342
+ Transition : codersdk .WorkspaceTransitionDelete ,
343
+ })
344
+ // Validate the result based on the expected error message
345
+ if tc .expectedWorkspaceDeleteErrMsg != "" {
346
+ require .Error (t , err )
347
+ require .Contains (t , err .Error (), tc .expectedWorkspaceDeleteErrMsg )
348
+ } else {
349
+ require .NoError (t , err , "delete the user Workspace" )
350
+ coderdtest .AwaitWorkspaceBuildJobCompleted (t , client , build .ID )
351
+
352
+ // Verify that the user workspace is now marked as deleted
353
+ deletedWorkspace , err := client .DeletedWorkspace (ctx , userWorkspace .ID )
354
+ require .NoError (t , err )
355
+ require .Equal (t , userWorkspace .ID , deletedWorkspace .ID )
356
+ }
357
+ })
358
+ }
359
+ })
360
+ }
361
+
362
+ func setupTestDBPreset (
363
+ t * testing.T ,
364
+ db database.Store ,
365
+ templateVersionID uuid.UUID ,
366
+ ) database.TemplateVersionPreset {
367
+ t .Helper ()
368
+
369
+ preset := dbgen .Preset (t , db , database.InsertPresetParams {
370
+ TemplateVersionID : templateVersionID ,
371
+ Name : "preset-test" ,
372
+ DesiredInstances : sql.NullInt32 {
373
+ Valid : true ,
374
+ Int32 : 1 ,
375
+ },
376
+ })
377
+ dbgen .PresetParameter (t , db , database.InsertPresetParametersParams {
378
+ TemplateVersionPresetID : preset .ID ,
379
+ Names : []string {"test" },
380
+ Values : []string {"test" },
381
+ })
382
+
383
+ return preset
384
+ }
385
+
386
+ func setupTestDBWorkspace (
387
+ t * testing.T ,
388
+ clock quartz.Clock ,
389
+ db database.Store ,
390
+ ps pubsub.Pubsub ,
391
+ orgID uuid.UUID ,
392
+ ownerID uuid.UUID ,
393
+ templateID uuid.UUID ,
394
+ templateVersionID uuid.UUID ,
395
+ presetID uuid.UUID ,
396
+ ) database.WorkspaceTable {
397
+ t .Helper ()
398
+
399
+ workspace := dbgen .Workspace (t , db , database.WorkspaceTable {
400
+ TemplateID : templateID ,
401
+ OrganizationID : orgID ,
402
+ OwnerID : ownerID ,
403
+ Deleted : false ,
404
+ CreatedAt : time .Now ().Add (- time .Hour * 2 ),
405
+ })
406
+ job := dbgen .ProvisionerJob (t , db , ps , database.ProvisionerJob {
407
+ InitiatorID : ownerID ,
408
+ CreatedAt : time .Now ().Add (- time .Hour * 2 ),
409
+ StartedAt : sql.NullTime {Time : clock .Now ().Add (- time .Hour * 2 ), Valid : true },
410
+ CompletedAt : sql.NullTime {Time : clock .Now ().Add (- time .Hour ), Valid : true },
411
+ OrganizationID : orgID ,
412
+ })
413
+ workspaceBuild := dbgen .WorkspaceBuild (t , db , database.WorkspaceBuild {
414
+ WorkspaceID : workspace .ID ,
415
+ InitiatorID : ownerID ,
416
+ TemplateVersionID : templateVersionID ,
417
+ JobID : job .ID ,
418
+ TemplateVersionPresetID : uuid.NullUUID {UUID : presetID , Valid : true },
419
+ Transition : database .WorkspaceTransitionStart ,
420
+ CreatedAt : clock .Now (),
421
+ })
422
+ dbgen .WorkspaceBuildParameters (t , db , []database.WorkspaceBuildParameter {
423
+ {
424
+ WorkspaceBuildID : workspaceBuild .ID ,
425
+ Name : "test" ,
426
+ Value : "test" ,
427
+ },
428
+ })
429
+
430
+ return workspace
212
431
}
0 commit comments