From bb97ba727f7dfe01e9fa69c7a170b126b7b4fe7a Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Fri, 27 Feb 2026 15:31:04 -0500 Subject: [PATCH] fix(coderd): allow non-admin users to list chat model configs (#22407) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem Non-admin users of the Agents (chat) feature send `model_config_id: "00000000-0000-0000-0000-000000000000"` (nil UUID) when creating chats, because the `GET /api/experimental/chats/model-configs` endpoint requires `policy.ActionRead` on `rbac.ResourceDeploymentConfig`, which is only granted to admins. The flow: 1. `AgentsPage.tsx` calls `useQuery(chatModelConfigs())` → hits `listChatModelConfigs` 2. Non-admin users get a **403 Forbidden** response 3. `chatModelConfigsQuery.data` is `undefined`, so the `modelConfigIDByModelID` map is empty 4. `handleCreateChat` falls back to `nilUUID` for `model_config_id` 5. The backend rejects the nil UUID: `"Invalid model config ID."` ## Fix Changed `listChatModelConfigs` to allow all authenticated users to read model configs: - **Admin users** continue to see all configs (including disabled ones) for management via `GetChatModelConfigs` - **Non-admin users** now see only enabled configs via `GetEnabledChatModelConfigs` with a system context, which is sufficient for using the chat feature This follows the same pattern as `listChatModels`, which already uses `dbauthz.AsSystemRestricted(ctx)` to allow all authenticated users to see available models. Write endpoints (create/update/delete) retain their existing `ResourceDeploymentConfig` authorization. ## Testing - Updated `TestListChatModelConfigs/ForbiddenForOrganizationMember` → `SuccessForOrganizationMember` to verify non-admin users can list enabled model configs - All existing chat tests continue to pass --- coderd/chats.go | 18 +++++++++++++----- coderd/chats_test.go | 19 ++++++++++++++++--- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/coderd/chats.go b/coderd/chats.go index 501f0296a0..b30e62c2b5 100644 --- a/coderd/chats.go +++ b/coderd/chats.go @@ -2444,12 +2444,20 @@ func (api *API) deleteChatProvider(rw http.ResponseWriter, r *http.Request) { func (api *API) listChatModelConfigs(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() - if !api.Authorize(r, policy.ActionRead, rbac.ResourceDeploymentConfig) { - httpapi.Forbidden(rw) - return - } - configs, err := api.Database.GetChatModelConfigs(ctx) + // Admin users can see all model configs (including disabled ones) + // for management purposes. Non-admin users see only enabled + // configs, which is sufficient for using the chat feature. + isAdmin := api.Authorize(r, policy.ActionRead, rbac.ResourceDeploymentConfig) + + var configs []database.ChatModelConfig + var err error + if isAdmin { + configs, err = api.Database.GetChatModelConfigs(ctx) + } else { + //nolint:gocritic // All authenticated users need to read enabled model configs to use the chat feature. + configs, err = api.Database.GetEnabledChatModelConfigs(dbauthz.AsSystemRestricted(ctx)) + } if err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ Message: "Failed to list chat model configs.", diff --git a/coderd/chats_test.go b/coderd/chats_test.go index 34b34b5176..41129109bb 100644 --- a/coderd/chats_test.go +++ b/coderd/chats_test.go @@ -818,16 +818,29 @@ func TestListChatModelConfigs(t *testing.T) { require.True(t, found) }) - t.Run("ForbiddenForOrganizationMember", func(t *testing.T) { + t.Run("SuccessForOrganizationMember", func(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitLong) adminClient := newChatClient(t) firstUser := coderdtest.CreateFirstUser(t, adminClient) + modelConfig := createChatModelConfig(t, adminClient) memberClient, _ := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID) - _, err := memberClient.ListChatModelConfigs(ctx) - requireSDKError(t, err, http.StatusForbidden) + // Non-admin users should see only enabled model configs. + configs, err := memberClient.ListChatModelConfigs(ctx) + require.NoError(t, err) + require.NotEmpty(t, configs) + + found := false + for _, config := range configs { + if config.ID == modelConfig.ID { + found = true + require.Equal(t, "openai", config.Provider) + require.Equal(t, "gpt-4o-mini", config.Model) + } + } + require.True(t, found) }) }