fix(coderd): require ssh access for workspace chats (#23094)

### Motivation
- The chat creation flow associated a workspace agent for a chat if the requester could read the workspace, enabling privilege escalation where users without SSH/app-connect permissions could cause the daemon to open privileged agent connections and execute commands.
- The intent is to ensure that attaching a workspace agent to a chat only happens when the requester has the workspace SSH permission so the chat daemon cannot be abused to bypass RBAC.

### Description
- Require request-scoped authorization for workspace agent usage by changing `validateCreateChatWorkspaceSelection` to accept the `*http.Request` and calling `api.Authorize(r, policy.ActionSSH, workspace)` before selecting the workspace for a chat.
- Pass the HTTP request into the validator from `postChats` so authorization is evaluated in the request context (`postChats` now calls `validateCreateChatWorkspaceSelection(ctx, r, req)`).
- Add a regression test `WorkspaceAccessibleButNoSSH` in `coderd/chats_test.go` which creates an org-admin-scoped user (read access but no `ActionSSH`) and asserts that creating a chat with `WorkspaceID` is denied.

### Testing
- Ran `gofmt -w coderd/chats.go coderd/chats_test.go` which succeeded.
- Attempted to run repository pre-commit checks (`make pre-commit`) and targeted `go test` invocations; these checks could not be completed in this environment due to missing local tooling and environment constraints (protobuf include resolution, containerized DB access via Docker socket, and long-running golden generation tasks), so full CI/pre-commit verification and end-to-end test runs did not complete here.
- Added a focused regression unit test (`WorkspaceAccessibleButNoSSH`) to prevent reintroduction of the authorization bypass; this test is included in the change and should be executed in CI where the full toolchain and test environment are available.

------
[Codex Task](https://chatgpt.com/codex/tasks/task_b_69b432502670832e91d14e937745de46)
This commit is contained in:
Thomas Kosiewski
2026-03-16 11:42:01 +01:00
committed by GitHub
parent aa6f301305
commit 069d3e2beb
2 changed files with 44 additions and 1 deletions
+8 -1
View File
@@ -237,7 +237,7 @@ func (api *API) postChats(rw http.ResponseWriter, r *http.Request) {
return
}
workspaceSelection, validationStatus, validationError := api.validateCreateChatWorkspaceSelection(ctx, req)
workspaceSelection, validationStatus, validationError := api.validateCreateChatWorkspaceSelection(ctx, r, req)
if validationError != nil {
httpapi.Write(ctx, rw, validationStatus, *validationError)
return
@@ -1899,6 +1899,7 @@ type createChatWorkspaceSelection struct {
func (api *API) validateCreateChatWorkspaceSelection(
ctx context.Context,
r *http.Request,
req codersdk.CreateChatRequest,
) (
createChatWorkspaceSelection,
@@ -1927,6 +1928,12 @@ func (api *API) validateCreateChatWorkspaceSelection(
Valid: true,
}
if !api.Authorize(r, policy.ActionSSH, workspace) {
return selection, http.StatusBadRequest, &codersdk.Response{
Message: "Workspace not found or you do not have access to this resource",
}
}
return selection, 0, nil
}
+36
View File
@@ -23,6 +23,7 @@ import (
"github.com/coder/coder/v2/coderd/database/dbfake"
"github.com/coder/coder/v2/coderd/externalauth"
coderdpubsub "github.com/coder/coder/v2/coderd/pubsub"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/testutil"
"github.com/coder/websocket"
@@ -162,6 +163,41 @@ func TestPostChats(t *testing.T) {
)
})
t.Run("WorkspaceAccessibleButNoSSH", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
adminClient, db := newChatClientWithDatabase(t)
firstUser := coderdtest.CreateFirstUser(t, adminClient)
orgAdminClient, _ := coderdtest.CreateAnotherUser(
t,
adminClient,
firstUser.OrganizationID,
rbac.ScopedRoleOrgAdmin(firstUser.OrganizationID),
)
workspaceBuild := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
OrganizationID: firstUser.OrganizationID,
OwnerID: firstUser.UserID,
}).WithAgent().Do()
_, err := orgAdminClient.CreateChat(ctx, codersdk.CreateChatRequest{
Content: []codersdk.ChatInputPart{
{
Type: codersdk.ChatInputPartTypeText,
Text: "hello",
},
},
WorkspaceID: &workspaceBuild.Workspace.ID,
})
sdkErr := requireSDKError(t, err, http.StatusBadRequest)
require.Equal(
t,
"Workspace not found or you do not have access to this resource",
sdkErr.Message,
)
})
t.Run("WorkspaceNotFound", func(t *testing.T) {
t.Parallel()