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:
@@ -1704,6 +1704,7 @@ func (s *server) completeTemplateImportJob(ctx context.Context, job database.Pro
|
||||
slog.F("transition", transition))
|
||||
|
||||
if err := InsertWorkspaceResource(ctx, db, jobID, transition, resource, telemetrySnapshot); err != nil {
|
||||
s.warnWorkspaceAppRebindRejected(ctx, jobID, err)
|
||||
return xerrors.Errorf("insert resource: %w", err)
|
||||
}
|
||||
}
|
||||
@@ -2122,6 +2123,7 @@ func (s *server) completeWorkspaceBuildJob(ctx context.Context, job database.Pro
|
||||
InsertWorkspaceResourceWithAgentIDsFromProto(),
|
||||
)
|
||||
if err != nil {
|
||||
s.warnWorkspaceAppRebindRejected(ctx, jobID, err)
|
||||
return xerrors.Errorf("insert provisioner job: %w", err)
|
||||
}
|
||||
}
|
||||
@@ -2590,6 +2592,7 @@ func (s *server) completeTemplateDryRunJob(ctx context.Context, job database.Pro
|
||||
|
||||
err := InsertWorkspaceResource(ctx, db, jobID, database.WorkspaceTransitionStart, resource, telemetrySnapshot)
|
||||
if err != nil {
|
||||
s.warnWorkspaceAppRebindRejected(ctx, jobID, err)
|
||||
return xerrors.Errorf("insert resource: %w", err)
|
||||
}
|
||||
}
|
||||
@@ -3614,6 +3617,32 @@ func insertAgentScriptsAndLogSources(ctx context.Context, db database.Store, age
|
||||
return nil
|
||||
}
|
||||
|
||||
type workspaceAppRebindError struct {
|
||||
slug string
|
||||
appID uuid.UUID
|
||||
agentID uuid.UUID
|
||||
}
|
||||
|
||||
func (e *workspaceAppRebindError) Error() string {
|
||||
return fmt.Sprintf("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", e.slug, e.appID, e.agentID)
|
||||
}
|
||||
|
||||
func (s *server) warnWorkspaceAppRebindRejected(ctx context.Context, jobID uuid.UUID, err error) {
|
||||
slog.Helper()
|
||||
|
||||
var rebindErr *workspaceAppRebindError
|
||||
if !errors.As(err, &rebindErr) {
|
||||
return
|
||||
}
|
||||
|
||||
s.Logger.Warn(ctx, "workspace app rebind rejected by SQL guard",
|
||||
slog.F("job_id", jobID.String()),
|
||||
slog.F("app_id", rebindErr.appID.String()),
|
||||
slog.F("agent_id", rebindErr.agentID.String()),
|
||||
slog.F("app_slug", rebindErr.slug),
|
||||
)
|
||||
}
|
||||
|
||||
func insertAgentApp(ctx context.Context, db database.Store, agentID uuid.UUID, app *sdkproto.App, appSlugs map[string]struct{}, snapshot *telemetry.Snapshot) error {
|
||||
// Similar logic is duplicated in terraform/resources.go.
|
||||
slug := app.Slug
|
||||
@@ -3702,6 +3731,17 @@ func insertAgentApp(ctx context.Context, db database.Store, agentID uuid.UUID, a
|
||||
Tooltip: app.Tooltip,
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
// The upsert's ON CONFLICT guard refused to rebind an app
|
||||
// owned by a workspace to an agent outside that workspace,
|
||||
// including agents from import or dry-run jobs that resolve
|
||||
// to no workspace (SEC-91).
|
||||
return &workspaceAppRebindError{
|
||||
slug: slug,
|
||||
appID: id,
|
||||
agentID: agentID,
|
||||
}
|
||||
}
|
||||
return xerrors.Errorf("upsert app: %w", err)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user