mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix(coderd): require deployment-wide workspace read permissions for WatchAllWorkspaceBuilds endpoint (#26985)
This commit is contained in:
@@ -2249,6 +2249,20 @@ func (api *API) watchWorkspace(
|
||||
func (api *API) watchAllWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// This endpoint streams build events for every workspace in the deployment
|
||||
// with no per-item filtering, so require deployment-wide read on workspaces.
|
||||
// Without this, any authenticated user could enumerate all workspace names
|
||||
// and observe build activity across organizations they do not belong to.
|
||||
if !api.Authorize(r, policy.ActionRead, rbac.ResourceWorkspace.All()) {
|
||||
httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{
|
||||
Message: "You are not authorized to watch all workspace builds.",
|
||||
Detail: "This requires permission to read all workspaces, which is granted by the " +
|
||||
"Owner or Template Admin role. Please contact an administrator about your " +
|
||||
"permissions if you feel this is an error.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Buffer enough updates to avoid blocking the pubsub callback while we're
|
||||
// accepting the WebSocket connection. Accepting the connection signals to
|
||||
// the client that the server is subscribed and ready to forward events.
|
||||
|
||||
@@ -4079,6 +4079,49 @@ func TestWatchAllWorkspaceBuilds(t *testing.T) {
|
||||
require.Equal(t, workspace2.ID, update.WorkspaceID)
|
||||
}
|
||||
|
||||
func TestWatchAllWorkspaceBuildsAuthorization(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Enable the workspace build updates experiment.
|
||||
client := coderdtest.New(t, &coderdtest.Options{
|
||||
DeploymentValues: coderdtest.DeploymentValues(t, func(dv *codersdk.DeploymentValues) {
|
||||
dv.Experiments = []string{string(codersdk.ExperimentWorkspaceBuildUpdates)}
|
||||
}),
|
||||
})
|
||||
owner := coderdtest.CreateFirstUser(t, client)
|
||||
|
||||
t.Run("MemberForbidden", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// A plain member has no deployment-wide workspace read permission and
|
||||
// must not be able to open the all-builds stream.
|
||||
memberClient, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||
defer cancel()
|
||||
|
||||
_, err := memberClient.WatchAllWorkspaceBuilds(ctx)
|
||||
require.Error(t, err)
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusForbidden, apiErr.StatusCode())
|
||||
})
|
||||
|
||||
t.Run("TemplateAdminAllowed", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Template admins have site-wide workspace read and may open the stream.
|
||||
taClient, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleTemplateAdmin())
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||
defer cancel()
|
||||
|
||||
decoder, err := taClient.WatchAllWorkspaceBuilds(ctx)
|
||||
require.NoError(t, err)
|
||||
defer decoder.Close()
|
||||
})
|
||||
}
|
||||
|
||||
func mustLocation(t *testing.T, location string) *time.Location {
|
||||
t.Helper()
|
||||
loc, err := time.LoadLocation(location)
|
||||
|
||||
Reference in New Issue
Block a user