Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

feat: allow TemplateAdmin to delete prebuilds via auth layer#18333

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
ssncferreira merged 15 commits intomainfromssncferreira/poc-prebuild-rbac-authz
Jun 20, 2025
Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
Show all changes
15 commits
Select commitHold shift + click to select a range
2ba15c5
feat: POC for allowing TemplateAdmin to delete prebuild workspaces vi…
ssncferreiraJun 11, 2025
a043f92
fix: role permissions tests
ssncferreiraJun 12, 2025
6cae769
fix: exclude prebuiltWorkspace permissions from orgAdmin role
ssncferreiraJun 12, 2025
afc5359
fix: explicitly set prebuild_workspace permissions
ssncferreiraJun 12, 2025
b2c7bdd
chore: improve code quality and add clarifying comments
ssncferreiraJun 17, 2025
f24e4ab
refactor: move PrebuildsSystemUserID constant to database package to …
ssncferreiraJun 18, 2025
98576c6
chore: remove unnecessary comment
ssncferreiraJun 18, 2025
9d2bf8b
Merge remote-tracking branch 'origin/main' into ssncferreira/poc-preb…
ssncferreiraJun 18, 2025
d461a3d
test: fix roles_test PrebuiltWorkspace to have the PrebuildsSystemUse…
ssncferreiraJun 18, 2025
5f896f0
test: improve end-to-end permission tests for deletion of prebuilt wo…
ssncferreiraJun 18, 2025
f4ae6bc
test: fix end-to-end cli tests Workspace delete permissions
ssncferreiraJun 20, 2025
998fa3b
test: add dbauthz tests for authorize prebuilt workspaces
ssncferreiraJun 20, 2025
007f3fd
chore: address comments - add IsPrebuild and AsPrebuild methods to Wo…
ssncferreiraJun 20, 2025
f811778
chore: address comments - update cli test name
ssncferreiraJun 20, 2025
ed8af65
Merge remote-tracking branch 'origin/main' into ssncferreira/poc-preb…
ssncferreiraJun 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
230 changes: 230 additions & 0 deletionscli/delete_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -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"
Expand DownExpand Up@@ -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
}
2 changes: 2 additions & 0 deletionscoderd/apidoc/docs.go
View file
Open in desktop

Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.

2 changes: 2 additions & 0 deletionscoderd/apidoc/swagger.json
View file
Open in desktop

Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.

5 changes: 5 additions & 0 deletionscoderd/database/constants.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
package database

import "github.com/google/uuid"

var PrebuildsSystemUserID = uuid.MustParse("c42fdf75-3097-471c-8c33-fb52454d81c0")
42 changes: 36 additions & 6 deletionscoderd/database/dbauthz/dbauthz.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -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"
Expand DownExpand Up@@ -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))
}
Comment on lines +163 to +172
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I think the normal workspace check is more likely to succeed then the prebuild. So it might be a slight optimization to flip these checks.

But that will probably break some of the unit tests? No need to change this, just a thought.

ssncferreira reacted with thumbs up emoji
Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Good point, I had considered that optimization as well. I believe it might require some test updates, so I'd prefer to merge this PR and address the optimization in a follow-up PR.

return nil
}

type authContextKey struct{}

// ActorFromContext returns the authorization subject from the context.
Expand DownExpand Up@@ -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"},
Expand All@@ -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,
Expand DownExpand Up@@ -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.
Expand DownExpand Up@@ -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 prebuiltworkspace deletion
if err:= q.authorizePrebuiltWorkspace(ctx, policy.ActionUpdate, workspace); err!= nil {
return err
}

Expand Down
Loading
Loading

[8]ページ先頭

©2009-2025 Movatter.jp