diff --git a/cli/delete_test.go b/cli/delete_test.go index 1d4dc8dfb40ad..ecd1c6996df1d 100644 --- a/cli/delete_test.go +++ b/cli/delete_test.go @@ -2,9 +2,18 @@ package cli_test import ( "context" + "database/sql" "fmt" "io" "testing" + "time" + + "github.com/google/uuid" + + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/dbgen" + "github.com/coder/coder/v2/coderd/database/pubsub" + "github.com/coder/quartz" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -209,4 +218,225 @@ func TestDelete(t *testing.T) { cancel() <-doneChan }) + + t.Run("Prebuilt workspace delete permissions", func(t *testing.T) { + t.Parallel() + if !dbtestutil.WillUsePostgres() { + t.Skip("this test requires postgres") + } + + clock := quartz.NewMock(t) + ctx := testutil.Context(t, testutil.WaitSuperLong) + + // Setup + db, pb := dbtestutil.NewDB(t, dbtestutil.WithDumpOnFailure()) + client, _ := coderdtest.NewWithProvisionerCloser(t, &coderdtest.Options{ + Database: db, + Pubsub: pb, + IncludeProvisionerDaemon: true, + }) + owner := coderdtest.CreateFirstUser(t, client) + orgID := owner.OrganizationID + + // Given a template version with a preset and a template + version := coderdtest.CreateTemplateVersion(t, client, orgID, nil) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + preset := setupTestDBPreset(t, db, version.ID) + template := coderdtest.CreateTemplate(t, client, orgID, version.ID) + + cases := []struct { + name string + client *codersdk.Client + expectedPrebuiltDeleteErrMsg string + expectedWorkspaceDeleteErrMsg string + }{ + // Users with the OrgAdmin role should be able to delete both normal and prebuilt workspaces + { + name: "OrgAdmin", + client: func() *codersdk.Client { + client, _ := coderdtest.CreateAnotherUser(t, client, orgID, rbac.ScopedRoleOrgAdmin(orgID)) + return client + }(), + }, + // Users with the TemplateAdmin role should be able to delete prebuilt workspaces, but not normal workspaces + { + name: "TemplateAdmin", + client: func() *codersdk.Client { + client, _ := coderdtest.CreateAnotherUser(t, client, orgID, rbac.RoleTemplateAdmin()) + return client + }(), + expectedWorkspaceDeleteErrMsg: "unexpected status code 403: You do not have permission to delete this workspace.", + }, + // Users with the OrgTemplateAdmin role should be able to delete prebuilt workspaces, but not normal workspaces + { + name: "OrgTemplateAdmin", + client: func() *codersdk.Client { + client, _ := coderdtest.CreateAnotherUser(t, client, orgID, rbac.ScopedRoleOrgTemplateAdmin(orgID)) + return client + }(), + expectedWorkspaceDeleteErrMsg: "unexpected status code 403: You do not have permission to delete this workspace.", + }, + // Users with the Member role should not be able to delete prebuilt or normal workspaces + { + name: "Member", + client: func() *codersdk.Client { + client, _ := coderdtest.CreateAnotherUser(t, client, orgID, rbac.RoleMember()) + return client + }(), + expectedPrebuiltDeleteErrMsg: "unexpected status code 404: Resource not found or you do not have access to this resource", + expectedWorkspaceDeleteErrMsg: "unexpected status code 404: Resource not found or you do not have access to this resource", + }, + } + + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // Create one prebuilt workspace (owned by system user) and one normal workspace (owned by a user) + // Each workspace is persisted in the DB along with associated workspace jobs and builds. + dbPrebuiltWorkspace := setupTestDBWorkspace(t, clock, db, pb, orgID, database.PrebuildsSystemUserID, template.ID, version.ID, preset.ID) + userWorkspaceOwner, err := client.User(context.Background(), "testUser") + require.NoError(t, err) + dbUserWorkspace := setupTestDBWorkspace(t, clock, db, pb, orgID, userWorkspaceOwner.ID, template.ID, version.ID, preset.ID) + + assertWorkspaceDelete := func( + runClient *codersdk.Client, + workspace database.Workspace, + workspaceOwner string, + expectedErr string, + ) { + t.Helper() + + // Attempt to delete the workspace as the test client + inv, root := clitest.New(t, "delete", workspaceOwner+"/"+workspace.Name, "-y") + clitest.SetupConfig(t, runClient, root) + doneChan := make(chan struct{}) + pty := ptytest.New(t).Attach(inv) + var runErr error + go func() { + defer close(doneChan) + runErr = inv.Run() + }() + + // Validate the result based on the expected error message + if expectedErr != "" { + <-doneChan + require.Error(t, runErr) + require.Contains(t, runErr.Error(), expectedErr) + } else { + pty.ExpectMatch("has been deleted") + <-doneChan + + // When running with the race detector on, we sometimes get an EOF. + if runErr != nil { + assert.ErrorIs(t, runErr, io.EOF) + } + + // Verify that the workspace is now marked as deleted + _, err := client.Workspace(context.Background(), workspace.ID) + require.ErrorContains(t, err, "was deleted") + } + } + + // Ensure at least one prebuilt workspace is reported as running in the database + testutil.Eventually(ctx, t, func(ctx context.Context) (done bool) { + running, err := db.GetRunningPrebuiltWorkspaces(ctx) + if !assert.NoError(t, err) || !assert.GreaterOrEqual(t, len(running), 1) { + return false + } + return true + }, testutil.IntervalMedium, "running prebuilt workspaces timeout") + + runningWorkspaces, err := db.GetRunningPrebuiltWorkspaces(ctx) + require.NoError(t, err) + require.GreaterOrEqual(t, len(runningWorkspaces), 1) + + // Get the full prebuilt workspace object from the DB + prebuiltWorkspace, err := db.GetWorkspaceByID(ctx, dbPrebuiltWorkspace.ID) + require.NoError(t, err) + + // Assert the prebuilt workspace deletion + assertWorkspaceDelete(tc.client, prebuiltWorkspace, "prebuilds", tc.expectedPrebuiltDeleteErrMsg) + + // Get the full user workspace object from the DB + userWorkspace, err := db.GetWorkspaceByID(ctx, dbUserWorkspace.ID) + require.NoError(t, err) + + // Assert the user workspace deletion + assertWorkspaceDelete(tc.client, userWorkspace, userWorkspaceOwner.Username, tc.expectedWorkspaceDeleteErrMsg) + }) + } + }) +} + +func setupTestDBPreset( + t *testing.T, + db database.Store, + templateVersionID uuid.UUID, +) database.TemplateVersionPreset { + t.Helper() + + preset := dbgen.Preset(t, db, database.InsertPresetParams{ + TemplateVersionID: templateVersionID, + Name: "preset-test", + DesiredInstances: sql.NullInt32{ + Valid: true, + Int32: 1, + }, + }) + dbgen.PresetParameter(t, db, database.InsertPresetParametersParams{ + TemplateVersionPresetID: preset.ID, + Names: []string{"test"}, + Values: []string{"test"}, + }) + + return preset +} + +func setupTestDBWorkspace( + t *testing.T, + clock quartz.Clock, + db database.Store, + ps pubsub.Pubsub, + orgID uuid.UUID, + ownerID uuid.UUID, + templateID uuid.UUID, + templateVersionID uuid.UUID, + presetID uuid.UUID, +) database.WorkspaceTable { + t.Helper() + + workspace := dbgen.Workspace(t, db, database.WorkspaceTable{ + TemplateID: templateID, + OrganizationID: orgID, + OwnerID: ownerID, + Deleted: false, + CreatedAt: time.Now().Add(-time.Hour * 2), + }) + job := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{ + InitiatorID: ownerID, + CreatedAt: time.Now().Add(-time.Hour * 2), + StartedAt: sql.NullTime{Time: clock.Now().Add(-time.Hour * 2), Valid: true}, + CompletedAt: sql.NullTime{Time: clock.Now().Add(-time.Hour), Valid: true}, + OrganizationID: orgID, + }) + workspaceBuild := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ + WorkspaceID: workspace.ID, + InitiatorID: ownerID, + TemplateVersionID: templateVersionID, + JobID: job.ID, + TemplateVersionPresetID: uuid.NullUUID{UUID: presetID, Valid: true}, + Transition: database.WorkspaceTransitionStart, + CreatedAt: clock.Now(), + }) + dbgen.WorkspaceBuildParameters(t, db, []database.WorkspaceBuildParameter{ + { + WorkspaceBuildID: workspaceBuild.ID, + Name: "test", + Value: "test", + }, + }) + + return workspace } diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 05e61dbec9296..1d175333c1271 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -15259,6 +15259,7 @@ const docTemplate = `{ "oauth2_app_secret", "organization", "organization_member", + "prebuilt_workspace", "provisioner_daemon", "provisioner_jobs", "replicas", @@ -15298,6 +15299,7 @@ const docTemplate = `{ "ResourceOauth2AppSecret", "ResourceOrganization", "ResourceOrganizationMember", + "ResourcePrebuiltWorkspace", "ResourceProvisionerDaemon", "ResourceProvisionerJobs", "ResourceReplicas", diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 8577c080a7ecf..9d00a7ba34c30 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -13851,6 +13851,7 @@ "oauth2_app_secret", "organization", "organization_member", + "prebuilt_workspace", "provisioner_daemon", "provisioner_jobs", "replicas", @@ -13890,6 +13891,7 @@ "ResourceOauth2AppSecret", "ResourceOrganization", "ResourceOrganizationMember", + "ResourcePrebuiltWorkspace", "ResourceProvisionerDaemon", "ResourceProvisionerJobs", "ResourceReplicas", diff --git a/coderd/database/constants.go b/coderd/database/constants.go new file mode 100644 index 0000000000000..931e0d7e0983d --- /dev/null +++ b/coderd/database/constants.go @@ -0,0 +1,5 @@ +package database + +import "github.com/google/uuid" + +var PrebuildsSystemUserID = uuid.MustParse("c42fdf75-3097-471c-8c33-fb52454d81c0") diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 8d470aa13473b..adb2007918f8d 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -21,7 +21,6 @@ import ( "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/httpapi/httpapiconstraints" "github.com/coder/coder/v2/coderd/httpmw/loggermw" - "github.com/coder/coder/v2/coderd/prebuilds" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/rbac/policy" "github.com/coder/coder/v2/coderd/rbac/rolestore" @@ -150,6 +149,30 @@ func (q *querier) authorizeContext(ctx context.Context, action policy.Action, ob return nil } +// authorizePrebuiltWorkspace handles authorization for workspace resource types. +// prebuilt_workspaces are a subset of workspaces, currently limited to +// supporting delete operations. Therefore, if the action is delete or +// update and the workspace is a prebuild, a prebuilt-specific authorization +// is attempted first. If that fails, it falls back to normal workspace +// authorization. +// Note: Delete operations of workspaces requires both update and delete +// permissions. +func (q *querier) authorizePrebuiltWorkspace(ctx context.Context, action policy.Action, workspace database.Workspace) error { + var prebuiltErr error + // Special handling for prebuilt_workspace deletion authorization check + if (action == policy.ActionUpdate || action == policy.ActionDelete) && workspace.IsPrebuild() { + // Try prebuilt-specific authorization first + if prebuiltErr = q.authorizeContext(ctx, action, workspace.AsPrebuild()); prebuiltErr == nil { + return nil + } + } + // Fallback to normal workspace authorization check + if err := q.authorizeContext(ctx, action, workspace); err != nil { + return xerrors.Errorf("authorize context: %w", errors.Join(prebuiltErr, err)) + } + return nil +} + type authContextKey struct{} // ActorFromContext returns the authorization subject from the context. @@ -399,7 +422,7 @@ var ( subjectPrebuildsOrchestrator = rbac.Subject{ Type: rbac.SubjectTypePrebuildsOrchestrator, FriendlyName: "Prebuilds Orchestrator", - ID: prebuilds.SystemUserID.String(), + ID: database.PrebuildsSystemUserID.String(), Roles: rbac.Roles([]rbac.Role{ { Identifier: rbac.RoleIdentifier{Name: "prebuilds-orchestrator"}, @@ -412,6 +435,12 @@ var ( policy.ActionCreate, policy.ActionDelete, policy.ActionRead, policy.ActionUpdate, policy.ActionWorkspaceStart, policy.ActionWorkspaceStop, }, + // PrebuiltWorkspaces are a subset of Workspaces. + // Explicitly setting PrebuiltWorkspace permissions for clarity. + // Note: even without PrebuiltWorkspace permissions, access is still granted via Workspace permissions. + rbac.ResourcePrebuiltWorkspace.Type: { + policy.ActionUpdate, policy.ActionDelete, + }, // Should be able to add the prebuilds system user as a member to any organization that needs prebuilds. rbac.ResourceOrganizationMember.Type: { policy.ActionCreate, @@ -3953,8 +3982,9 @@ func (q *querier) InsertWorkspaceBuild(ctx context.Context, arg database.InsertW action = policy.ActionWorkspaceStop } - if err = q.authorizeContext(ctx, action, w); err != nil { - return xerrors.Errorf("authorize context: %w", err) + // Special handling for prebuilt workspace deletion + if err := q.authorizePrebuiltWorkspace(ctx, action, w); err != nil { + return err } // If we're starting a workspace we need to check the template. @@ -3993,8 +4023,8 @@ func (q *querier) InsertWorkspaceBuildParameters(ctx context.Context, arg databa return err } - err = q.authorizeContext(ctx, policy.ActionUpdate, workspace) - if err != nil { + // Special handling for prebuilt workspace deletion + if err := q.authorizePrebuiltWorkspace(ctx, policy.ActionUpdate, workspace); err != nil { return err } diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index ba9d1ddf0d7d2..dd649ce636a71 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -5564,3 +5564,63 @@ func (s *MethodTestSuite) TestChat() { }).Asserts(c, policy.ActionUpdate) })) } + +func (s *MethodTestSuite) TestAuthorizePrebuiltWorkspace() { + s.Run("PrebuildDelete/InsertWorkspaceBuild", s.Subtest(func(db database.Store, check *expects) { + u := dbgen.User(s.T(), db, database.User{}) + o := dbgen.Organization(s.T(), db, database.Organization{}) + tpl := dbgen.Template(s.T(), db, database.Template{ + OrganizationID: o.ID, + CreatedBy: u.ID, + }) + w := dbgen.Workspace(s.T(), db, database.WorkspaceTable{ + TemplateID: tpl.ID, + OrganizationID: o.ID, + OwnerID: database.PrebuildsSystemUserID, + }) + pj := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{ + OrganizationID: o.ID, + }) + tv := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{ + TemplateID: uuid.NullUUID{UUID: tpl.ID, Valid: true}, + OrganizationID: o.ID, + CreatedBy: u.ID, + }) + check.Args(database.InsertWorkspaceBuildParams{ + WorkspaceID: w.ID, + Transition: database.WorkspaceTransitionDelete, + Reason: database.BuildReasonInitiator, + TemplateVersionID: tv.ID, + JobID: pj.ID, + }).Asserts(w.AsPrebuild(), policy.ActionDelete) + })) + s.Run("PrebuildUpdate/InsertWorkspaceBuildParameters", s.Subtest(func(db database.Store, check *expects) { + u := dbgen.User(s.T(), db, database.User{}) + o := dbgen.Organization(s.T(), db, database.Organization{}) + tpl := dbgen.Template(s.T(), db, database.Template{ + OrganizationID: o.ID, + CreatedBy: u.ID, + }) + w := dbgen.Workspace(s.T(), db, database.WorkspaceTable{ + TemplateID: tpl.ID, + OrganizationID: o.ID, + OwnerID: database.PrebuildsSystemUserID, + }) + pj := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{ + OrganizationID: o.ID, + }) + tv := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{ + TemplateID: uuid.NullUUID{UUID: tpl.ID, Valid: true}, + OrganizationID: o.ID, + CreatedBy: u.ID, + }) + wb := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{ + JobID: pj.ID, + WorkspaceID: w.ID, + TemplateVersionID: tv.ID, + }) + check.Args(database.InsertWorkspaceBuildParametersParams{ + WorkspaceBuildID: wb.ID, + }).Asserts(w.AsPrebuild(), policy.ActionUpdate) + })) +} diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index ee1c7471808d5..b631a6f6a9d04 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -23,11 +23,9 @@ import ( "golang.org/x/exp/maps" "golang.org/x/xerrors" - "github.com/coder/coder/v2/coderd/notifications/types" - "github.com/coder/coder/v2/coderd/prebuilds" - "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbtime" + "github.com/coder/coder/v2/coderd/notifications/types" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/rbac/regosql" "github.com/coder/coder/v2/coderd/util/slice" @@ -159,7 +157,7 @@ func New() database.Store { q.mutex.Lock() // We can't insert this user using the interface, because it's a system user. q.data.users = append(q.data.users, database.User{ - ID: prebuilds.SystemUserID, + ID: database.PrebuildsSystemUserID, Email: "prebuilds@coder.com", Username: "prebuilds", CreatedAt: dbtime.Now(), diff --git a/coderd/database/modelmethods.go b/coderd/database/modelmethods.go index b3f6deed9eff0..cb16d8c4995b6 100644 --- a/coderd/database/modelmethods.go +++ b/coderd/database/modelmethods.go @@ -229,6 +229,24 @@ func (w Workspace) RBACObject() rbac.Object { return w.WorkspaceTable().RBACObject() } +// IsPrebuild returns true if the workspace is a prebuild workspace. +// A workspace is considered a prebuild if its owner is the prebuild system user. +func (w Workspace) IsPrebuild() bool { + return w.OwnerID == PrebuildsSystemUserID +} + +// AsPrebuild returns the RBAC object corresponding to the workspace type. +// If the workspace is a prebuild, it returns a prebuilt_workspace RBAC object. +// Otherwise, it returns a normal workspace RBAC object. +func (w Workspace) AsPrebuild() rbac.Object { + if w.IsPrebuild() { + return rbac.ResourcePrebuiltWorkspace.WithID(w.ID). + InOrg(w.OrganizationID). + WithOwner(w.OwnerID.String()) + } + return w.RBACObject() +} + func (w WorkspaceTable) RBACObject() rbac.Object { if w.DormantAt.Valid { return w.DormantRBAC() @@ -246,6 +264,24 @@ func (w WorkspaceTable) DormantRBAC() rbac.Object { WithOwner(w.OwnerID.String()) } +// IsPrebuild returns true if the workspace is a prebuild workspace. +// A workspace is considered a prebuild if its owner is the prebuild system user. +func (w WorkspaceTable) IsPrebuild() bool { + return w.OwnerID == PrebuildsSystemUserID +} + +// AsPrebuild returns the RBAC object corresponding to the workspace type. +// If the workspace is a prebuild, it returns a prebuilt_workspace RBAC object. +// Otherwise, it returns a normal workspace RBAC object. +func (w WorkspaceTable) AsPrebuild() rbac.Object { + if w.IsPrebuild() { + return rbac.ResourcePrebuiltWorkspace.WithID(w.ID). + InOrg(w.OrganizationID). + WithOwner(w.OwnerID.String()) + } + return w.RBACObject() +} + func (m OrganizationMember) RBACObject() rbac.Object { return rbac.ResourceOrganizationMember. WithID(m.UserID). diff --git a/coderd/database/querier_test.go b/coderd/database/querier_test.go index 74ac5b0a20caf..600fb8269909a 100644 --- a/coderd/database/querier_test.go +++ b/coderd/database/querier_test.go @@ -27,7 +27,6 @@ import ( "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/database/migrations" "github.com/coder/coder/v2/coderd/httpmw" - "github.com/coder/coder/v2/coderd/prebuilds" "github.com/coder/coder/v2/coderd/provisionerdserver" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/rbac/policy" @@ -1418,7 +1417,7 @@ func TestGetUsers_IncludeSystem(t *testing.T) { for _, u := range users { if u.IsSystem { foundSystemUser = true - require.Equal(t, prebuilds.SystemUserID, u.ID) + require.Equal(t, database.PrebuildsSystemUserID, u.ID) } else { foundRegularUser = true require.Equalf(t, other.ID.String(), u.ID.String(), "found unexpected regular user") diff --git a/coderd/httpmw/organizationparam.go b/coderd/httpmw/organizationparam.go index efedc3a764591..c12772a4de4e4 100644 --- a/coderd/httpmw/organizationparam.go +++ b/coderd/httpmw/organizationparam.go @@ -180,7 +180,7 @@ func ExtractOrganizationMember(ctx context.Context, auth func(r *http.Request, a organizationMembers, err := db.OrganizationMembers(ctx, database.OrganizationMembersParams{ OrganizationID: orgID, UserID: user.ID, - IncludeSystem: false, + IncludeSystem: true, }) if httpapi.Is404Error(err) { httpapi.ResourceNotFound(rw) diff --git a/coderd/prebuilds/id.go b/coderd/prebuilds/id.go deleted file mode 100644 index 7c2bbe79b7a6f..0000000000000 --- a/coderd/prebuilds/id.go +++ /dev/null @@ -1,5 +0,0 @@ -package prebuilds - -import "github.com/google/uuid" - -var SystemUserID = uuid.MustParse("c42fdf75-3097-471c-8c33-fb52454d81c0") diff --git a/coderd/rbac/object_gen.go b/coderd/rbac/object_gen.go index f19d90894dd55..a5c696fb2a491 100644 --- a/coderd/rbac/object_gen.go +++ b/coderd/rbac/object_gen.go @@ -222,6 +222,14 @@ var ( Type: "organization_member", } + // ResourcePrebuiltWorkspace + // Valid Actions + // - "ActionDelete" :: delete prebuilt workspace + // - "ActionUpdate" :: update prebuilt workspace settings + ResourcePrebuiltWorkspace = Object{ + Type: "prebuilt_workspace", + } + // ResourceProvisionerDaemon // Valid Actions // - "ActionCreate" :: create a provisioner daemon/key @@ -389,6 +397,7 @@ func AllResources() []Objecter { ResourceOauth2AppSecret, ResourceOrganization, ResourceOrganizationMember, + ResourcePrebuiltWorkspace, ResourceProvisionerDaemon, ResourceProvisionerJobs, ResourceReplicas, diff --git a/coderd/rbac/policy/policy.go b/coderd/rbac/policy/policy.go index 160062283f857..733a70bcafd0e 100644 --- a/coderd/rbac/policy/policy.go +++ b/coderd/rbac/policy/policy.go @@ -102,6 +102,20 @@ var RBACPermissions = map[string]PermissionDefinition{ "workspace_dormant": { Actions: workspaceActions, }, + "prebuilt_workspace": { + // Prebuilt_workspace actions currently apply only to delete operations. + // To successfully delete a prebuilt workspace, a user must have the following permissions: + // * workspace.read: to read the current workspace state + // * update: to modify workspace metadata and related resources during deletion + // (e.g., updating the deleted field in the database) + // * delete: to perform the actual deletion of the workspace + // If the user lacks prebuilt_workspace update or delete permissions, + // the authorization will always fall back to the corresponding permissions on workspace. + Actions: map[Action]ActionDefinition{ + ActionUpdate: actDef("update prebuilt workspace settings"), + ActionDelete: actDef("delete prebuilt workspace"), + }, + }, "workspace_proxy": { Actions: map[Action]ActionDefinition{ ActionCreate: actDef("create a workspace proxy"), diff --git a/coderd/rbac/roles.go b/coderd/rbac/roles.go index 28ddc38462ce9..8acdf7486ddd2 100644 --- a/coderd/rbac/roles.go +++ b/coderd/rbac/roles.go @@ -270,11 +270,15 @@ func ReloadBuiltinRoles(opts *RoleOptions) { Site: append( // Workspace dormancy and workspace are omitted. // Workspace is specifically handled based on the opts.NoOwnerWorkspaceExec - allPermsExcept(ResourceWorkspaceDormant, ResourceWorkspace), + allPermsExcept(ResourceWorkspaceDormant, ResourcePrebuiltWorkspace, ResourceWorkspace), // This adds back in the Workspace permissions. Permissions(map[string][]policy.Action{ ResourceWorkspace.Type: ownerWorkspaceActions, ResourceWorkspaceDormant.Type: {policy.ActionRead, policy.ActionDelete, policy.ActionCreate, policy.ActionUpdate, policy.ActionWorkspaceStop, policy.ActionCreateAgent, policy.ActionDeleteAgent}, + // PrebuiltWorkspaces are a subset of Workspaces. + // Explicitly setting PrebuiltWorkspace permissions for clarity. + // Note: even without PrebuiltWorkspace permissions, access is still granted via Workspace permissions. + ResourcePrebuiltWorkspace.Type: {policy.ActionUpdate, policy.ActionDelete}, })...), Org: map[string][]Permission{}, User: []Permission{}, @@ -290,7 +294,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) { ResourceWorkspaceProxy.Type: {policy.ActionRead}, }), Org: map[string][]Permission{}, - User: append(allPermsExcept(ResourceWorkspaceDormant, ResourceUser, ResourceOrganizationMember), + User: append(allPermsExcept(ResourceWorkspaceDormant, ResourcePrebuiltWorkspace, ResourceUser, ResourceOrganizationMember), Permissions(map[string][]policy.Action{ // Reduced permission set on dormant workspaces. No build, ssh, or exec ResourceWorkspaceDormant.Type: {policy.ActionRead, policy.ActionDelete, policy.ActionCreate, policy.ActionUpdate, policy.ActionWorkspaceStop, policy.ActionCreateAgent, policy.ActionDeleteAgent}, @@ -335,8 +339,9 @@ func ReloadBuiltinRoles(opts *RoleOptions) { ResourceAssignOrgRole.Type: {policy.ActionRead}, ResourceTemplate.Type: ResourceTemplate.AvailableActions(), // CRUD all files, even those they did not upload. - ResourceFile.Type: {policy.ActionCreate, policy.ActionRead}, - ResourceWorkspace.Type: {policy.ActionRead}, + ResourceFile.Type: {policy.ActionCreate, policy.ActionRead}, + ResourceWorkspace.Type: {policy.ActionRead}, + ResourcePrebuiltWorkspace.Type: {policy.ActionUpdate, policy.ActionDelete}, // CRUD to provisioner daemons for now. ResourceProvisionerDaemon.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, // Needs to read all organizations since @@ -413,9 +418,13 @@ func ReloadBuiltinRoles(opts *RoleOptions) { }), Org: map[string][]Permission{ // Org admins should not have workspace exec perms. - organizationID.String(): append(allPermsExcept(ResourceWorkspace, ResourceWorkspaceDormant, ResourceAssignRole), Permissions(map[string][]policy.Action{ + organizationID.String(): append(allPermsExcept(ResourceWorkspace, ResourceWorkspaceDormant, ResourcePrebuiltWorkspace, ResourceAssignRole), Permissions(map[string][]policy.Action{ ResourceWorkspaceDormant.Type: {policy.ActionRead, policy.ActionDelete, policy.ActionCreate, policy.ActionUpdate, policy.ActionWorkspaceStop, policy.ActionCreateAgent, policy.ActionDeleteAgent}, ResourceWorkspace.Type: slice.Omit(ResourceWorkspace.AvailableActions(), policy.ActionApplicationConnect, policy.ActionSSH), + // PrebuiltWorkspaces are a subset of Workspaces. + // Explicitly setting PrebuiltWorkspace permissions for clarity. + // Note: even without PrebuiltWorkspace permissions, access is still granted via Workspace permissions. + ResourcePrebuiltWorkspace.Type: {policy.ActionUpdate, policy.ActionDelete}, })...), }, User: []Permission{}, @@ -493,9 +502,10 @@ func ReloadBuiltinRoles(opts *RoleOptions) { Site: []Permission{}, Org: map[string][]Permission{ organizationID.String(): Permissions(map[string][]policy.Action{ - ResourceTemplate.Type: ResourceTemplate.AvailableActions(), - ResourceFile.Type: {policy.ActionCreate, policy.ActionRead}, - ResourceWorkspace.Type: {policy.ActionRead}, + ResourceTemplate.Type: ResourceTemplate.AvailableActions(), + ResourceFile.Type: {policy.ActionCreate, policy.ActionRead}, + ResourceWorkspace.Type: {policy.ActionRead}, + ResourcePrebuiltWorkspace.Type: {policy.ActionUpdate, policy.ActionDelete}, // Assigning template perms requires this permission. ResourceOrganization.Type: {policy.ActionRead}, ResourceOrganizationMember.Type: {policy.ActionRead}, diff --git a/coderd/rbac/roles_test.go b/coderd/rbac/roles_test.go index 5738edfe8caa2..a1b7c7c15d03a 100644 --- a/coderd/rbac/roles_test.go +++ b/coderd/rbac/roles_test.go @@ -5,6 +5,8 @@ import ( "fmt" "testing" + "github.com/coder/coder/v2/coderd/database" + "github.com/google/uuid" "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/assert" @@ -496,6 +498,15 @@ func TestRolePermissions(t *testing.T) { false: {setOtherOrg, userAdmin, templateAdmin, memberMe, orgTemplateAdmin, orgUserAdmin, orgAuditor}, }, }, + { + Name: "PrebuiltWorkspace", + Actions: []policy.Action{policy.ActionUpdate, policy.ActionDelete}, + Resource: rbac.ResourcePrebuiltWorkspace.WithID(uuid.New()).InOrg(orgID).WithOwner(database.PrebuildsSystemUserID.String()), + AuthorizeMap: map[bool][]hasAuthSubjects{ + true: {owner, orgAdmin, templateAdmin, orgTemplateAdmin}, + false: {setOtherOrg, userAdmin, memberMe, orgUserAdmin, orgAuditor, orgMemberMe}, + }, + }, // Some admin style resources { Name: "Licenses", diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index b05f69bb0ad9a..74946d46dcd9f 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -392,6 +392,16 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { ctx, tx, func(action policy.Action, object rbac.Objecter) bool { + // Special handling for prebuilt workspace deletion + if object.RBACObject().Type == rbac.ResourceWorkspace.Type && action == policy.ActionDelete { + if workspaceObj, ok := object.(database.Workspace); ok { + // Try prebuilt-specific authorization first + if auth := api.Authorize(r, action, workspaceObj.AsPrebuild()); auth { + return auth + } + } + } + // Fallback to default authorization return api.Authorize(r, action, object) }, audit.WorkspaceBuildBaggageFromRequest(r), diff --git a/coderd/wsbuilder/wsbuilder.go b/coderd/wsbuilder/wsbuilder.go index 9605df58014de..b52a20ac1e9db 100644 --- a/coderd/wsbuilder/wsbuilder.go +++ b/coderd/wsbuilder/wsbuilder.go @@ -918,7 +918,18 @@ func (b *Builder) authorize(authFunc func(action policy.Action, object rbac.Obje msg := fmt.Sprintf("Transition %q not supported.", b.trans) return BuildError{http.StatusBadRequest, msg, xerrors.New(msg)} } - if !authFunc(action, b.workspace) { + + // Special handling for prebuilt workspace deletion + authorized := false + if action == policy.ActionDelete && b.workspace.IsPrebuild() && authFunc(action, b.workspace.AsPrebuild()) { + authorized = true + } + // Fallback to default authorization + if !authorized && authFunc(action, b.workspace) { + authorized = true + } + + if !authorized { if authFunc(policy.ActionRead, b.workspace) { // If the user can read the workspace, but not delete/create/update. Show // a more helpful error. They are allowed to know the workspace exists. diff --git a/codersdk/rbacresources_gen.go b/codersdk/rbacresources_gen.go index 95792bb8e2a7b..1304218ad7bea 100644 --- a/codersdk/rbacresources_gen.go +++ b/codersdk/rbacresources_gen.go @@ -28,6 +28,7 @@ const ( ResourceOauth2AppSecret RBACResource = "oauth2_app_secret" ResourceOrganization RBACResource = "organization" ResourceOrganizationMember RBACResource = "organization_member" + ResourcePrebuiltWorkspace RBACResource = "prebuilt_workspace" ResourceProvisionerDaemon RBACResource = "provisioner_daemon" ResourceProvisionerJobs RBACResource = "provisioner_jobs" ResourceReplicas RBACResource = "replicas" @@ -91,6 +92,7 @@ var RBACResourceActions = map[RBACResource][]RBACAction{ ResourceOauth2AppSecret: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, ResourceOrganization: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, ResourceOrganizationMember: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, + ResourcePrebuiltWorkspace: {ActionDelete, ActionUpdate}, ResourceProvisionerDaemon: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, ResourceProvisionerJobs: {ActionCreate, ActionRead, ActionUpdate}, ResourceReplicas: {ActionRead}, diff --git a/docs/reference/api/members.md b/docs/reference/api/members.md index 6b5d124753bc0..40921e40b70ee 100644 --- a/docs/reference/api/members.md +++ b/docs/reference/api/members.md @@ -206,6 +206,7 @@ Status Code **200** | `resource_type` | `oauth2_app_secret` | | `resource_type` | `organization` | | `resource_type` | `organization_member` | +| `resource_type` | `prebuilt_workspace` | | `resource_type` | `provisioner_daemon` | | `resource_type` | `provisioner_jobs` | | `resource_type` | `replicas` | @@ -375,6 +376,7 @@ Status Code **200** | `resource_type` | `oauth2_app_secret` | | `resource_type` | `organization` | | `resource_type` | `organization_member` | +| `resource_type` | `prebuilt_workspace` | | `resource_type` | `provisioner_daemon` | | `resource_type` | `provisioner_jobs` | | `resource_type` | `replicas` | @@ -544,6 +546,7 @@ Status Code **200** | `resource_type` | `oauth2_app_secret` | | `resource_type` | `organization` | | `resource_type` | `organization_member` | +| `resource_type` | `prebuilt_workspace` | | `resource_type` | `provisioner_daemon` | | `resource_type` | `provisioner_jobs` | | `resource_type` | `replicas` | @@ -682,6 +685,7 @@ Status Code **200** | `resource_type` | `oauth2_app_secret` | | `resource_type` | `organization` | | `resource_type` | `organization_member` | +| `resource_type` | `prebuilt_workspace` | | `resource_type` | `provisioner_daemon` | | `resource_type` | `provisioner_jobs` | | `resource_type` | `replicas` | @@ -1042,6 +1046,7 @@ Status Code **200** | `resource_type` | `oauth2_app_secret` | | `resource_type` | `organization` | | `resource_type` | `organization_member` | +| `resource_type` | `prebuilt_workspace` | | `resource_type` | `provisioner_daemon` | | `resource_type` | `provisioner_jobs` | | `resource_type` | `replicas` | diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 44f4665ba6f49..8f548478e27a6 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -6329,6 +6329,7 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith | `oauth2_app_secret` | | `organization` | | `organization_member` | +| `prebuilt_workspace` | | `provisioner_daemon` | | `provisioner_jobs` | | `replicas` | diff --git a/enterprise/coderd/groups_test.go b/enterprise/coderd/groups_test.go index 028aa3328535f..f87a9193f5fa4 100644 --- a/enterprise/coderd/groups_test.go +++ b/enterprise/coderd/groups_test.go @@ -6,8 +6,6 @@ import ( "testing" "time" - "github.com/coder/coder/v2/coderd/prebuilds" - "github.com/google/uuid" "github.com/stretchr/testify/require" @@ -833,7 +831,7 @@ func TestGroup(t *testing.T) { ctx := testutil.Context(t, testutil.WaitLong) // nolint:gocritic // "This client is operating as the owner user" is fine in this case. - prebuildsUser, err := client.User(ctx, prebuilds.SystemUserID.String()) + prebuildsUser, err := client.User(ctx, database.PrebuildsSystemUserID.String()) require.NoError(t, err) // The 'Everyone' group always has an ID that matches the organization ID. group, err := userAdminClient.Group(ctx, user.OrganizationID) diff --git a/enterprise/coderd/prebuilds/claim.go b/enterprise/coderd/prebuilds/claim.go index f040ee756e678..b6a85ae1fc094 100644 --- a/enterprise/coderd/prebuilds/claim.go +++ b/enterprise/coderd/prebuilds/claim.go @@ -47,7 +47,7 @@ func (c EnterpriseClaimer) Claim( } func (EnterpriseClaimer) Initiator() uuid.UUID { - return prebuilds.SystemUserID + return database.PrebuildsSystemUserID } var _ prebuilds.Claimer = &EnterpriseClaimer{} diff --git a/enterprise/coderd/prebuilds/membership_test.go b/enterprise/coderd/prebuilds/membership_test.go index 6caa7178d9d60..229814e5bf764 100644 --- a/enterprise/coderd/prebuilds/membership_test.go +++ b/enterprise/coderd/prebuilds/membership_test.go @@ -12,7 +12,6 @@ import ( "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbgen" "github.com/coder/coder/v2/coderd/database/dbtestutil" - agplprebuilds "github.com/coder/coder/v2/coderd/prebuilds" "github.com/coder/coder/v2/enterprise/coderd/prebuilds" ) @@ -74,14 +73,14 @@ func TestReconcileAll(t *testing.T) { // dbmem doesn't ensure membership to the default organization dbgen.OrganizationMember(t, db, database.OrganizationMember{ OrganizationID: defaultOrg.ID, - UserID: agplprebuilds.SystemUserID, + UserID: database.PrebuildsSystemUserID, }) } - dbgen.OrganizationMember(t, db, database.OrganizationMember{OrganizationID: unrelatedOrg.ID, UserID: agplprebuilds.SystemUserID}) + dbgen.OrganizationMember(t, db, database.OrganizationMember{OrganizationID: unrelatedOrg.ID, UserID: database.PrebuildsSystemUserID}) if tc.preExistingMembership { // System user already a member of both orgs. - dbgen.OrganizationMember(t, db, database.OrganizationMember{OrganizationID: targetOrg.ID, UserID: agplprebuilds.SystemUserID}) + dbgen.OrganizationMember(t, db, database.OrganizationMember{OrganizationID: targetOrg.ID, UserID: database.PrebuildsSystemUserID}) } presets := []database.GetTemplatePresetsWithPrebuildsRow{newPresetRow(unrelatedOrg.ID)} @@ -91,7 +90,7 @@ func TestReconcileAll(t *testing.T) { // Verify memberships before reconciliation. preReconcileMemberships, err := db.GetOrganizationsByUserID(ctx, database.GetOrganizationsByUserIDParams{ - UserID: agplprebuilds.SystemUserID, + UserID: database.PrebuildsSystemUserID, }) require.NoError(t, err) expectedMembershipsBefore := []uuid.UUID{defaultOrg.ID, unrelatedOrg.ID} @@ -102,11 +101,11 @@ func TestReconcileAll(t *testing.T) { // Reconcile reconciler := prebuilds.NewStoreMembershipReconciler(db, clock) - require.NoError(t, reconciler.ReconcileAll(ctx, agplprebuilds.SystemUserID, presets)) + require.NoError(t, reconciler.ReconcileAll(ctx, database.PrebuildsSystemUserID, presets)) // Verify memberships after reconciliation. postReconcileMemberships, err := db.GetOrganizationsByUserID(ctx, database.GetOrganizationsByUserIDParams{ - UserID: agplprebuilds.SystemUserID, + UserID: database.PrebuildsSystemUserID, }) require.NoError(t, err) expectedMembershipsAfter := expectedMembershipsBefore diff --git a/enterprise/coderd/prebuilds/metricscollector_test.go b/enterprise/coderd/prebuilds/metricscollector_test.go index dce9e07dd110f..7befffe9d3a05 100644 --- a/enterprise/coderd/prebuilds/metricscollector_test.go +++ b/enterprise/coderd/prebuilds/metricscollector_test.go @@ -20,7 +20,6 @@ import ( "github.com/coder/coder/v2/coderd/database/dbgen" "github.com/coder/coder/v2/coderd/database/dbtestutil" "github.com/coder/coder/v2/coderd/database/dbtime" - agplprebuilds "github.com/coder/coder/v2/coderd/prebuilds" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/enterprise/coderd/prebuilds" "github.com/coder/coder/v2/testutil" @@ -55,8 +54,8 @@ func TestMetricsCollector(t *testing.T) { name: "prebuild provisioned but not completed", transitions: allTransitions, jobStatuses: allJobStatusesExcept(database.ProvisionerJobStatusPending, database.ProvisionerJobStatusRunning, database.ProvisionerJobStatusCanceling), - initiatorIDs: []uuid.UUID{agplprebuilds.SystemUserID}, - ownerIDs: []uuid.UUID{agplprebuilds.SystemUserID}, + initiatorIDs: []uuid.UUID{database.PrebuildsSystemUserID}, + ownerIDs: []uuid.UUID{database.PrebuildsSystemUserID}, metrics: []metricCheck{ {prebuilds.MetricCreatedCount, ptr.To(1.0), true}, {prebuilds.MetricClaimedCount, ptr.To(0.0), true}, @@ -72,8 +71,8 @@ func TestMetricsCollector(t *testing.T) { name: "prebuild running", transitions: []database.WorkspaceTransition{database.WorkspaceTransitionStart}, jobStatuses: []database.ProvisionerJobStatus{database.ProvisionerJobStatusSucceeded}, - initiatorIDs: []uuid.UUID{agplprebuilds.SystemUserID}, - ownerIDs: []uuid.UUID{agplprebuilds.SystemUserID}, + initiatorIDs: []uuid.UUID{database.PrebuildsSystemUserID}, + ownerIDs: []uuid.UUID{database.PrebuildsSystemUserID}, metrics: []metricCheck{ {prebuilds.MetricCreatedCount, ptr.To(1.0), true}, {prebuilds.MetricClaimedCount, ptr.To(0.0), true}, @@ -89,8 +88,8 @@ func TestMetricsCollector(t *testing.T) { name: "prebuild failed", transitions: allTransitions, jobStatuses: []database.ProvisionerJobStatus{database.ProvisionerJobStatusFailed}, - initiatorIDs: []uuid.UUID{agplprebuilds.SystemUserID}, - ownerIDs: []uuid.UUID{agplprebuilds.SystemUserID, uuid.New()}, + initiatorIDs: []uuid.UUID{database.PrebuildsSystemUserID}, + ownerIDs: []uuid.UUID{database.PrebuildsSystemUserID, uuid.New()}, metrics: []metricCheck{ {prebuilds.MetricCreatedCount, ptr.To(1.0), true}, {prebuilds.MetricFailedCount, ptr.To(1.0), true}, @@ -105,8 +104,8 @@ func TestMetricsCollector(t *testing.T) { name: "prebuild eligible", transitions: []database.WorkspaceTransition{database.WorkspaceTransitionStart}, jobStatuses: []database.ProvisionerJobStatus{database.ProvisionerJobStatusSucceeded}, - initiatorIDs: []uuid.UUID{agplprebuilds.SystemUserID}, - ownerIDs: []uuid.UUID{agplprebuilds.SystemUserID}, + initiatorIDs: []uuid.UUID{database.PrebuildsSystemUserID}, + ownerIDs: []uuid.UUID{database.PrebuildsSystemUserID}, metrics: []metricCheck{ {prebuilds.MetricCreatedCount, ptr.To(1.0), true}, {prebuilds.MetricClaimedCount, ptr.To(0.0), true}, @@ -122,8 +121,8 @@ func TestMetricsCollector(t *testing.T) { name: "prebuild ineligible", transitions: allTransitions, jobStatuses: allJobStatusesExcept(database.ProvisionerJobStatusSucceeded), - initiatorIDs: []uuid.UUID{agplprebuilds.SystemUserID}, - ownerIDs: []uuid.UUID{agplprebuilds.SystemUserID}, + initiatorIDs: []uuid.UUID{database.PrebuildsSystemUserID}, + ownerIDs: []uuid.UUID{database.PrebuildsSystemUserID}, metrics: []metricCheck{ {prebuilds.MetricCreatedCount, ptr.To(1.0), true}, {prebuilds.MetricClaimedCount, ptr.To(0.0), true}, @@ -139,7 +138,7 @@ func TestMetricsCollector(t *testing.T) { name: "prebuild claimed", transitions: allTransitions, jobStatuses: allJobStatuses, - initiatorIDs: []uuid.UUID{agplprebuilds.SystemUserID}, + initiatorIDs: []uuid.UUID{database.PrebuildsSystemUserID}, ownerIDs: []uuid.UUID{uuid.New()}, metrics: []metricCheck{ {prebuilds.MetricCreatedCount, ptr.To(1.0), true}, @@ -169,8 +168,8 @@ func TestMetricsCollector(t *testing.T) { name: "deleted templates should not be included in exported metrics", transitions: allTransitions, jobStatuses: allJobStatuses, - initiatorIDs: []uuid.UUID{agplprebuilds.SystemUserID}, - ownerIDs: []uuid.UUID{agplprebuilds.SystemUserID, uuid.New()}, + initiatorIDs: []uuid.UUID{database.PrebuildsSystemUserID}, + ownerIDs: []uuid.UUID{database.PrebuildsSystemUserID, uuid.New()}, metrics: nil, templateDeleted: []bool{true}, eligible: []bool{false}, @@ -209,7 +208,7 @@ func TestMetricsCollector(t *testing.T) { reconciler := prebuilds.NewStoreReconciler(db, pubsub, codersdk.PrebuildsConfig{}, logger, quartz.NewMock(t), prometheus.NewRegistry(), newNoopEnqueuer()) ctx := testutil.Context(t, testutil.WaitLong) - createdUsers := []uuid.UUID{agplprebuilds.SystemUserID} + createdUsers := []uuid.UUID{database.PrebuildsSystemUserID} for _, user := range slices.Concat(test.ownerIDs, test.initiatorIDs) { if !slices.Contains(createdUsers, user) { dbgen.User(t, db, database.User{ @@ -327,8 +326,8 @@ func TestMetricsCollector_DuplicateTemplateNames(t *testing.T) { test := testCase{ transition: database.WorkspaceTransitionStart, jobStatus: database.ProvisionerJobStatusSucceeded, - initiatorID: agplprebuilds.SystemUserID, - ownerID: agplprebuilds.SystemUserID, + initiatorID: database.PrebuildsSystemUserID, + ownerID: database.PrebuildsSystemUserID, metrics: []metricCheck{ {prebuilds.MetricCreatedCount, ptr.To(1.0), true}, {prebuilds.MetricClaimedCount, ptr.To(0.0), true}, diff --git a/enterprise/coderd/prebuilds/reconcile.go b/enterprise/coderd/prebuilds/reconcile.go index a9f8bd014b3e9..b1fe5b34c5ca5 100644 --- a/enterprise/coderd/prebuilds/reconcile.go +++ b/enterprise/coderd/prebuilds/reconcile.go @@ -265,7 +265,7 @@ func (c *StoreReconciler) ReconcileAll(ctx context.Context) error { } membershipReconciler := NewStoreMembershipReconciler(c.store, c.clock) - err = membershipReconciler.ReconcileAll(ctx, prebuilds.SystemUserID, snapshot.Presets) + err = membershipReconciler.ReconcileAll(ctx, database.PrebuildsSystemUserID, snapshot.Presets) if err != nil { return xerrors.Errorf("reconcile prebuild membership: %w", err) } @@ -676,7 +676,7 @@ func (c *StoreReconciler) createPrebuiltWorkspace(ctx context.Context, prebuiltW ID: prebuiltWorkspaceID, CreatedAt: now, UpdatedAt: now, - OwnerID: prebuilds.SystemUserID, + OwnerID: database.PrebuildsSystemUserID, OrganizationID: template.OrganizationID, TemplateID: template.ID, Name: name, @@ -718,7 +718,7 @@ func (c *StoreReconciler) deletePrebuiltWorkspace(ctx context.Context, prebuiltW return xerrors.Errorf("failed to get template: %w", err) } - if workspace.OwnerID != prebuilds.SystemUserID { + if workspace.OwnerID != database.PrebuildsSystemUserID { return xerrors.Errorf("prebuilt workspace is not owned by prebuild user anymore, probably it was claimed") } @@ -761,7 +761,7 @@ func (c *StoreReconciler) provision( builder := wsbuilder.New(workspace, transition). Reason(database.BuildReasonInitiator). - Initiator(prebuilds.SystemUserID). + Initiator(database.PrebuildsSystemUserID). MarkPrebuild() if transition != database.WorkspaceTransitionDelete { diff --git a/enterprise/coderd/prebuilds/reconcile_test.go b/enterprise/coderd/prebuilds/reconcile_test.go index 702a0769b548f..540ec1088ac0c 100644 --- a/enterprise/coderd/prebuilds/reconcile_test.go +++ b/enterprise/coderd/prebuilds/reconcile_test.go @@ -33,7 +33,6 @@ import ( "github.com/coder/coder/v2/coderd/database/dbgen" "github.com/coder/coder/v2/coderd/database/dbtestutil" "github.com/coder/coder/v2/coderd/database/pubsub" - agplprebuilds "github.com/coder/coder/v2/coderd/prebuilds" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/enterprise/coderd/prebuilds" "github.com/coder/coder/v2/testutil" @@ -2021,7 +2020,7 @@ func setupTestDBPrebuild( opts ...prebuildOption, ) (database.WorkspaceTable, database.WorkspaceBuild) { t.Helper() - return setupTestDBWorkspace(t, clock, db, ps, transition, prebuildStatus, orgID, preset, templateID, templateVersionID, agplprebuilds.SystemUserID, agplprebuilds.SystemUserID, opts...) + return setupTestDBWorkspace(t, clock, db, ps, transition, prebuildStatus, orgID, preset, templateID, templateVersionID, database.PrebuildsSystemUserID, database.PrebuildsSystemUserID, opts...) } func setupTestDBWorkspace( diff --git a/enterprise/coderd/workspaces_test.go b/enterprise/coderd/workspaces_test.go index ce86151f9b883..a1be6a3701393 100644 --- a/enterprise/coderd/workspaces_test.go +++ b/enterprise/coderd/workspaces_test.go @@ -32,7 +32,6 @@ import ( "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/httpmw" "github.com/coder/coder/v2/coderd/notifications" - "github.com/coder/coder/v2/coderd/prebuilds" "github.com/coder/coder/v2/coderd/provisionerdserver" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/rbac/policy" @@ -496,7 +495,7 @@ func TestCreateUserWorkspace(t *testing.T) { }).Do() r := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ - OwnerID: prebuilds.SystemUserID, + OwnerID: database.PrebuildsSystemUserID, TemplateID: tv.Template.ID, }).Seed(database.WorkspaceBuild{ TemplateVersionID: tv.TemplateVersion.ID, diff --git a/site/src/api/rbacresourcesGenerated.ts b/site/src/api/rbacresourcesGenerated.ts index 885f603c1eb82..3ec6a3accee32 100644 --- a/site/src/api/rbacresourcesGenerated.ts +++ b/site/src/api/rbacresourcesGenerated.ts @@ -123,6 +123,10 @@ export const RBACResourceActions: Partial< read: "read member", update: "update an organization member", }, + prebuilt_workspace: { + delete: "delete prebuilt workspace", + update: "update prebuilt workspace settings", + }, provisioner_daemon: { create: "create a provisioner daemon/key", delete: "delete a provisioner daemon/key", diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index d668018976f1e..98338c24bb2d8 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -2192,6 +2192,7 @@ export type RBACResource = | "oauth2_app_secret" | "organization" | "organization_member" + | "prebuilt_workspace" | "provisioner_daemon" | "provisioner_jobs" | "replicas" @@ -2231,6 +2232,7 @@ export const RBACResources: RBACResource[] = [ "oauth2_app_secret", "organization", "organization_member", + "prebuilt_workspace", "provisioner_daemon", "provisioner_jobs", "replicas",
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: