mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix(coderd): prevent cross-tenant workspace app rebinding (#26103)
This commit is contained in:
@@ -259,8 +259,9 @@ func (a *SubAgentAPI) CreateSubAgent(ctx context.Context, req *agentproto.Create
|
||||
slugHashEnc := base32.HexEncoding.WithPadding(base32.NoPadding).EncodeToString(slugHash[:])
|
||||
computedSlug := strings.ToLower(slugHashEnc[:8]) + "-" + app.Slug
|
||||
|
||||
appID := uuid.New()
|
||||
_, err := a.Database.UpsertWorkspaceApp(ctx, database.UpsertWorkspaceAppParams{
|
||||
ID: uuid.New(), // NOTE: we may need to maintain the app's ID here for stability, but for now we'll leave this as-is.
|
||||
ID: appID, // NOTE: we may need to maintain the app's ID here for stability, but for now we'll leave this as-is.
|
||||
CreatedAt: createdAt,
|
||||
AgentID: subAgent.ID,
|
||||
Slug: computedSlug,
|
||||
@@ -291,6 +292,12 @@ func (a *SubAgentAPI) CreateSubAgent(ctx context.Context, req *agentproto.Create
|
||||
Tooltip: "", // tooltips are not currently supported in subagent workspaces, default to empty string
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
// The upsert's ON CONFLICT guard refused to rebind an
|
||||
// existing workspace-owned app to an agent outside that
|
||||
// workspace, including agents that resolve to no workspace.
|
||||
return xerrors.Errorf("workspace app slug %q with ID %q is already bound to a workspace-owned agent and cannot be rebound to an agent in another workspace or to an agent without a workspace; refusing to rebind to agent ID %q", computedSlug, appID, subAgent.ID)
|
||||
}
|
||||
return xerrors.Errorf("insert workspace app: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/mock/gomock"
|
||||
|
||||
"cdr.dev/slog/v3"
|
||||
"github.com/coder/coder/v2/agent/proto"
|
||||
@@ -19,6 +20,7 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
"github.com/coder/coder/v2/coderd/database/dbgen"
|
||||
"github.com/coder/coder/v2/coderd/database/dbmock"
|
||||
"github.com/coder/coder/v2/coderd/database/dbtestutil"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
"github.com/coder/coder/v2/coderd/util/ptr"
|
||||
@@ -804,6 +806,81 @@ func TestSubAgentAPI(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("CreateSubAgentWithAppRebindRejected", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
clock := quartz.NewMock(t)
|
||||
createdAt := clock.Now()
|
||||
parentAgent := database.WorkspaceAgent{
|
||||
ID: uuid.New(),
|
||||
ResourceID: uuid.New(),
|
||||
ConnectionTimeoutSeconds: 30,
|
||||
TroubleshootingURL: "https://example.com/troubleshoot",
|
||||
APIKeyScope: database.AgentKeyScopeEnumAll,
|
||||
}
|
||||
workspace := database.Workspace{
|
||||
ID: uuid.New(),
|
||||
TemplateID: uuid.New(),
|
||||
}
|
||||
template := database.Template{
|
||||
ID: workspace.TemplateID,
|
||||
MaxPortSharingLevel: database.AppSharingLevelPublic,
|
||||
}
|
||||
insertedSubAgent := database.WorkspaceAgent{
|
||||
ID: uuid.New(),
|
||||
ParentID: uuid.NullUUID{UUID: parentAgent.ID, Valid: true},
|
||||
ResourceID: parentAgent.ResourceID,
|
||||
Name: "child-agent",
|
||||
AuthToken: uuid.New(),
|
||||
}
|
||||
|
||||
dbM := dbmock.NewMockStore(gomock.NewController(t))
|
||||
dbM.EXPECT().GetWorkspaceByAgentID(gomock.Any(), parentAgent.ID).Return(workspace, nil)
|
||||
dbM.EXPECT().GetTemplateByID(gomock.Any(), workspace.TemplateID).Return(template, nil)
|
||||
dbM.EXPECT().InsertWorkspaceAgent(gomock.Any(), gomock.Cond(func(params database.InsertWorkspaceAgentParams) bool {
|
||||
return params.ParentID.Valid && params.ParentID.UUID == parentAgent.ID &&
|
||||
params.ResourceID == parentAgent.ResourceID &&
|
||||
params.Name == insertedSubAgent.Name
|
||||
})).Return(insertedSubAgent, nil)
|
||||
dbM.EXPECT().UpsertWorkspaceApp(gomock.Any(), gomock.Cond(func(params database.UpsertWorkspaceAppParams) bool {
|
||||
return params.ID != uuid.Nil &&
|
||||
params.AgentID == insertedSubAgent.ID &&
|
||||
params.CreatedAt.Equal(createdAt) &&
|
||||
params.Slug == "fdqf0lpd-code-server" &&
|
||||
params.DisplayName == "VS Code"
|
||||
})).Return(database.WorkspaceApp{}, sql.ErrNoRows)
|
||||
|
||||
api := &agentapi.SubAgentAPI{
|
||||
OwnerID: uuid.New(),
|
||||
OrganizationID: uuid.New(),
|
||||
AgentFn: func(context.Context) (database.WorkspaceAgent, error) { return parentAgent, nil },
|
||||
Clock: clock,
|
||||
Database: dbM,
|
||||
Log: testutil.Logger(t),
|
||||
}
|
||||
|
||||
createResp, err := api.CreateSubAgent(context.Background(), &proto.CreateSubAgentRequest{
|
||||
Name: insertedSubAgent.Name,
|
||||
Directory: "/workspaces/coder",
|
||||
Architecture: "amd64",
|
||||
OperatingSystem: "linux",
|
||||
Apps: []*proto.CreateSubAgentRequest_App{
|
||||
{
|
||||
Slug: "code-server",
|
||||
DisplayName: ptr.Ref("VS Code"),
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, createResp.AppCreationErrors, 1)
|
||||
require.Equal(t, int32(0), createResp.AppCreationErrors[0].Index)
|
||||
require.Nil(t, createResp.AppCreationErrors[0].Field)
|
||||
require.Contains(t, createResp.AppCreationErrors[0].Error, "workspace app slug \"fdqf0lpd-code-server\"")
|
||||
require.Contains(t, createResp.AppCreationErrors[0].Error, "already bound to a workspace-owned agent")
|
||||
require.Contains(t, createResp.AppCreationErrors[0].Error, "cannot be rebound to an agent in another workspace or to an agent without a workspace")
|
||||
require.NotContains(t, createResp.AppCreationErrors[0].Error, "sql: no rows in result set")
|
||||
})
|
||||
|
||||
t.Run("DeleteSubAgent", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user