diff --git a/site/src/api/queries/organizations.ts b/site/src/api/queries/organizations.ts index 1dcaac36596f6..6caf0d6f958bf 100644 --- a/site/src/api/queries/organizations.ts +++ b/site/src/api/queries/organizations.ts @@ -294,6 +294,12 @@ export const provisionerJobs = ( }; }; +export const permittedOrganizationsKey = (check: AuthorizationCheck) => [ + "organizations", + "permitted", + check, +]; + /** * Fetch organizations the current user is permitted to use for a given * action. Fetches all organizations, runs a per-org authorization @@ -301,7 +307,7 @@ export const provisionerJobs = ( */ export const permittedOrganizations = (check: AuthorizationCheck) => { return { - queryKey: ["organizations", "permitted", check], + queryKey: permittedOrganizationsKey(check), queryFn: async (): Promise => { const orgs = await API.getOrganizations(); const checks = Object.fromEntries( diff --git a/site/src/pages/AgentsPage/components/AgentCreateForm.stories.tsx b/site/src/pages/AgentsPage/components/AgentCreateForm.stories.tsx index e38c344494d62..46fb4784803af 100644 --- a/site/src/pages/AgentsPage/components/AgentCreateForm.stories.tsx +++ b/site/src/pages/AgentsPage/components/AgentCreateForm.stories.tsx @@ -9,6 +9,7 @@ import { within, } from "storybook/test"; import { API } from "#/api/api"; +import { permittedOrganizationsKey } from "#/api/queries/organizations"; import type * as TypesGen from "#/api/typesGenerated"; import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog"; import { MockChatModelConfig } from "#/testHelpers/chatModels"; @@ -24,12 +25,10 @@ import { } from "../utils/reasoningEffort"; import { AgentCreateForm } from "./AgentCreateForm"; -// Query key used by permittedOrganizations() in the form. -const permittedOrgsKey = [ - "organizations", - "permitted", - { object: { resource_type: "chat" }, action: "create" }, -]; +const permittedOrgsKey = permittedOrganizationsKey({ + object: { resource_type: "chat", owner_id: "me" }, + action: "create", +}); const modelConfigID = "model-config-1"; const claudeModelConfigID = "model-config-claude"; @@ -1061,3 +1060,49 @@ export const PermittedOrgsResolvesToSubset: Story = { expect(options.organizationId).toBe(MockOrganization2.id); }, }; + +/** + * Member-scoped roles like agents-access grant chat:create only on + * chats the user owns, so the per-org check must carry owner context + * for the picker to render. + */ +export const MemberScopedPermissionsShowOrgPicker: Story = { + parameters: { + showOrganizations: true, + organizations: [MockDefaultOrganization, MockOrganization2], + }, + beforeEach: () => { + spyOn(API, "getOrganizations").mockResolvedValue([ + MockDefaultOrganization, + MockOrganization2, + ]); + spyOn(API, "checkAuthorization").mockImplementation(async ({ checks }) => + Object.fromEntries( + Object.entries(checks).map(([id, check]) => [ + id, + check.object.owner_id === "me", + ]), + ), + ); + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const picker = await canvas.findByRole( + "button", + { name: /^Organization:/ }, + { timeout: 3000 }, + ); + await userEvent.click(picker); + await screen.findByRole("option", { + name: MockDefaultOrganization.display_name, + }); + await userEvent.click( + screen.getByRole("option", { name: MockOrganization2.display_name }), + ); + expect( + canvas.getByRole("button", { + name: `Organization: ${MockOrganization2.display_name}`, + }), + ).toBeInTheDocument(); + }, +}; diff --git a/site/src/pages/AgentsPage/components/AgentCreateForm.tsx b/site/src/pages/AgentsPage/components/AgentCreateForm.tsx index 0a494c01dcf16..22165f04b54ee 100644 --- a/site/src/pages/AgentsPage/components/AgentCreateForm.tsx +++ b/site/src/pages/AgentsPage/components/AgentCreateForm.tsx @@ -461,7 +461,7 @@ export const AgentCreateForm: FC = ({ const permittedOrgsQuery = useQuery({ ...permittedOrganizations({ - object: { resource_type: "chat" }, + object: { resource_type: "chat", owner_id: "me" }, action: "create", }), enabled: showOrganizations,