From cc9f6f9d2372c39e7bfa950ac802f7dba3e7eb5a Mon Sep 17 00:00:00 2001 From: McKayla Washburn-Love Date: Fri, 24 Jul 2026 03:12:06 +0000 Subject: [PATCH 1/6] fix: don't show admin settings dropdown to everyone --- .../dashboard/Navbar/AdminSettings.tsx | 80 +++++++++++++++++++ .../dashboard/Navbar/DeploymentDropdown.tsx | 25 ++---- .../dashboard/Navbar/MobileMenu.stories.tsx | 45 ++++------- .../modules/dashboard/Navbar/MobileMenu.tsx | 46 +++++------ site/src/modules/dashboard/Navbar/Navbar.tsx | 19 ++--- .../dashboard/Navbar/NavbarView.stories.tsx | 63 ++++++--------- .../modules/dashboard/Navbar/NavbarView.tsx | 69 +++++----------- .../modules/dashboard/Navbar/adminSettings.ts | 55 ------------- site/src/modules/navigation.ts | 6 -- site/src/modules/permissions/index.ts | 3 +- 10 files changed, 178 insertions(+), 233 deletions(-) create mode 100644 site/src/modules/dashboard/Navbar/AdminSettings.tsx delete mode 100644 site/src/modules/dashboard/Navbar/adminSettings.ts diff --git a/site/src/modules/dashboard/Navbar/AdminSettings.tsx b/site/src/modules/dashboard/Navbar/AdminSettings.tsx new file mode 100644 index 0000000000000..11d2df7dbe37e --- /dev/null +++ b/site/src/modules/dashboard/Navbar/AdminSettings.tsx @@ -0,0 +1,80 @@ +import { Link } from "react-router"; +import { DropdownMenuItem } from "#/components/DropdownMenu/DropdownMenu"; + +/** + * Permissions that determine which items appear in the Admin settings menu. + * Shared by the desktop `DeploymentDropdown` and the mobile `MobileMenu` so + * both surfaces render the same set of items from a single source of truth. + */ +export type AdminSettingsItemsProps = { + itemClassName?: string; + permissions: AdminSettingsPermissions; +}; + +export type AdminSettingsPermissions = { + canViewDeployment?: boolean; + canViewOrganizations?: boolean; + canViewAISettings?: boolean; + canViewAuditLog?: boolean; + canViewConnectionLog?: boolean; + canViewAIBridge?: boolean; + canViewHealth?: boolean; +}; + +/** + * Builds the ordered list of Admin settings menu items for the given + * permissions. Organizations is always available; the rest are gated behind + * their respective permissions. + */ +export const AdminSettingsItems: React.FC = ({ + itemClassName, + permissions, +}) => { + return ( + <> + {permissions.canViewDeployment && ( + + Deployment + + )} + {permissions.canViewOrganizations && ( + + Organizations + + )} + {permissions.canViewAISettings && ( + + AI + + )} + {permissions.canViewAuditLog && ( + + Audit logs + + )} + {permissions.canViewConnectionLog && ( + + Connection logs + + )} + {permissions.canViewAIBridge && ( + + AI sessions + + )} + {permissions.canViewHealth && ( + + Healthcheck + + )} + + ); +}; + +/** + * Whether the user has any permission that should surface the Admin settings + * menu. Organizations alone does not gate visibility, matching prior behavior. + */ +export const canViewAdminSettings = ( + permissions: AdminSettingsPermissions, +): boolean => Object.values(permissions).some((canView) => canView); diff --git a/site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx b/site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx index d10e8f8870002..6ccaf52469979 100644 --- a/site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx +++ b/site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx @@ -1,30 +1,21 @@ import type { FC } from "react"; -import { Link } from "react-router"; import { ChevronDownIcon } from "#/components/AnimatedIcons/ChevronDown"; import { Button } from "#/components/Button/Button"; import { DropdownMenu, DropdownMenuContent, - DropdownMenuItem, DropdownMenuTrigger, } from "#/components/DropdownMenu/DropdownMenu"; import { + AdminSettingsItems, type AdminSettingsPermissions, - canViewAdminSettings, - getAdminSettingsItems, -} from "./adminSettings"; +} from "./AdminSettings"; -type DeploymentDropdownProps = AdminSettingsPermissions; +type AdminSettingsDropdownProps = { permissions: AdminSettingsPermissions }; -export const DeploymentDropdown: FC = ( +export const AdminSettingsDropdown: FC = ({ permissions, -) => { - if (!canViewAdminSettings(permissions)) { - return null; - } - - const items = getAdminSettingsItems(permissions); - +}) => { return ( @@ -36,11 +27,7 @@ export const DeploymentDropdown: FC = ( diff --git a/site/src/modules/dashboard/Navbar/MobileMenu.stories.tsx b/site/src/modules/dashboard/Navbar/MobileMenu.stories.tsx index e6b3af2ec5812..00193cdcbc0b4 100644 --- a/site/src/modules/dashboard/Navbar/MobileMenu.stories.tsx +++ b/site/src/modules/dashboard/Navbar/MobileMenu.stories.tsx @@ -40,13 +40,15 @@ const meta: Meta = { supportLinks: MockSupportLinks, onSignOut: fn(), isDefaultOpen: true, - canViewAuditLog: true, - canViewConnectionLog: true, - canViewDeployment: true, - canViewHealth: true, - canViewOrganizations: true, - canViewAIBridge: true, - canViewAISettings: true, + adminPermissions: { + canViewDeployment: true, + canViewOrganizations: true, + canViewAISettings: true, + canViewAuditLog: true, + canViewConnectionLog: true, + canViewAIBridge: true, + canViewHealth: true, + }, }, decorators: [withNavbarMock], }; @@ -67,13 +69,9 @@ export const Admin: Story = { export const Auditor: Story = { args: { user: MockUserMember, - canViewAuditLog: true, - canViewConnectionLog: false, - canViewDeployment: false, - canViewHealth: false, - canViewOrganizations: false, - canViewAIBridge: false, - canViewAISettings: false, + adminPermissions: { + canViewAuditLog: true, + }, }, play: openAdminSettings, }; @@ -81,13 +79,10 @@ export const Auditor: Story = { export const OrgAdmin: Story = { args: { user: MockUserMember, - canViewAuditLog: true, - canViewConnectionLog: false, - canViewDeployment: false, - canViewHealth: false, - canViewOrganizations: true, - canViewAIBridge: false, - canViewAISettings: false, + adminPermissions: { + canViewAuditLog: true, + canViewOrganizations: true, + }, }, play: openAdminSettings, }; @@ -95,13 +90,7 @@ export const OrgAdmin: Story = { export const Member: Story = { args: { user: MockUserMember, - canViewAuditLog: false, - canViewConnectionLog: false, - canViewDeployment: false, - canViewHealth: false, - canViewOrganizations: false, - canViewAIBridge: false, - canViewAISettings: false, + adminPermissions: {}, }, }; diff --git a/site/src/modules/dashboard/Navbar/MobileMenu.tsx b/site/src/modules/dashboard/Navbar/MobileMenu.tsx index 906e50a4d3a34..661341830c71b 100644 --- a/site/src/modules/dashboard/Navbar/MobileMenu.tsx +++ b/site/src/modules/dashboard/Navbar/MobileMenu.tsx @@ -27,9 +27,10 @@ import { Latency } from "#/components/Latency/Latency"; import type { ProxyContextValue } from "#/contexts/ProxyContext"; import { cn } from "#/utils/cn"; import { + AdminSettingsItems, type AdminSettingsPermissions, - getAdminSettingsItems, -} from "./adminSettings"; + canViewAdminSettings, +} from "./AdminSettings"; import { sortProxiesByLatency } from "./proxyUtils"; const itemStyles = { @@ -38,8 +39,9 @@ const itemStyles = { open: "text-content-primary", }; -type MobileMenuProps = AdminSettingsPermissions & { +type MobileMenuProps = { proxyContextValue?: ProxyContextValue; + adminPermissions: AdminSettingsPermissions; user?: TypesGen.User; supportLinks?: readonly TypesGen.LinkConfig[]; onSignOut: () => void; @@ -47,15 +49,14 @@ type MobileMenuProps = AdminSettingsPermissions & { }; export const MobileMenu: FC = ({ - isDefaultOpen, + adminPermissions, proxyContextValue, user, supportLinks, onSignOut, - ...permissions + isDefaultOpen, }) => { const [open, setOpen] = useState(isDefaultOpen); - const hasSomePermission = Object.values(permissions).some((p) => p); return ( @@ -87,10 +88,10 @@ export const MobileMenu: FC = ({ - {hasSomePermission && ( + {canViewAdminSettings(adminPermissions) && ( <> - + )} @@ -123,7 +124,7 @@ const ProxySettingsSub: FC = ({ proxyContextValue }) => { { e.preventDefault(); setOpen((prev) => !prev); @@ -191,7 +192,8 @@ const ProxySettingsSub: FC = ({ proxyContextValue }) => { { + onClick={(event) => { + event.stopPropagation(); proxyContextValue.refetchProxyLatencies(); }} > @@ -202,15 +204,18 @@ const ProxySettingsSub: FC = ({ proxyContextValue }) => { ); }; -const AdminSettingsSub: FC = (permissions) => { +type AdminSettingsSubProps = { + permissions: AdminSettingsPermissions; +}; + +const AdminSettingsSub: FC = ({ permissions }) => { const [open, setOpen] = useState(false); - const items = getAdminSettingsItems(permissions); return ( { e.preventDefault(); setOpen((prev) => !prev); @@ -223,15 +228,10 @@ const AdminSettingsSub: FC = (permissions) => { - {items.map((item) => ( - - {item.label} - - ))} + ); @@ -254,7 +254,7 @@ const UserSettingsSub: FC = ({ { e.preventDefault(); setOpen((prev) => !prev); diff --git a/site/src/modules/dashboard/Navbar/Navbar.tsx b/site/src/modules/dashboard/Navbar/Navbar.tsx index ea98ebf2b2fa7..75417d5e9c648 100644 --- a/site/src/modules/dashboard/Navbar/Navbar.tsx +++ b/site/src/modules/dashboard/Navbar/Navbar.tsx @@ -1,4 +1,3 @@ -import type { FC } from "react"; import { useQuery } from "react-query"; import { buildInfo } from "#/api/queries/buildInfo"; import type { LinkConfig } from "#/api/typesGenerated"; @@ -10,7 +9,7 @@ import { canViewDeploymentSettings } from "#/modules/permissions"; import { useFeatureVisibility } from "../useFeatureVisibility"; import { NavbarView } from "./NavbarView"; -export const Navbar: FC = () => { +export const Navbar: React.FC = () => { const { metadata } = useEmbeddedMetadata(); const buildInfoQuery = useQuery(buildInfo(metadata["build-info"])); const { appearance, canViewOrganizationSettings } = useDashboard(); @@ -45,13 +44,15 @@ export const Navbar: FC = () => { buildInfo={buildInfoQuery.data} supportLinks={Array.from(uniqueLinks.values())} onSignOut={signOut} - canViewDeployment={canViewDeployment} - canViewOrganizations={canViewOrganizations} - canViewHealth={canViewHealth} - canViewAuditLog={canViewAuditLog} - canViewConnectionLog={canViewConnectionLog} - canViewAIBridge={canViewAIBridge} - canViewAISettings={canViewAISettings} + adminPermissions={{ + canViewDeployment, + canViewOrganizations, + canViewAISettings, + canViewAuditLog, + canViewConnectionLog, + canViewAIBridge, + canViewHealth, + }} canCreateChat={canCreateChat} proxyContextValue={proxyContextValue} /> diff --git a/site/src/modules/dashboard/Navbar/NavbarView.stories.tsx b/site/src/modules/dashboard/Navbar/NavbarView.stories.tsx index 5cd6a50bbb169..9ec29a4f93e4f 100644 --- a/site/src/modules/dashboard/Navbar/NavbarView.stories.tsx +++ b/site/src/modules/dashboard/Navbar/NavbarView.stories.tsx @@ -30,11 +30,15 @@ const meta: Meta = { component: NavbarView, args: { user: MockUserOwner, - canViewAuditLog: true, - canViewDeployment: true, - canViewHealth: true, - canViewAISettings: true, - canViewOrganizations: true, + adminPermissions: { + canViewDeployment: true, + canViewOrganizations: true, + canViewAISettings: true, + canViewAuditLog: true, + canViewConnectionLog: true, + canViewAIBridge: true, + canViewHealth: true, + }, canCreateChat: true, supportLinks: [], }, @@ -56,11 +60,9 @@ export const ForAdmin: Story = { export const ForAuditor: Story = { args: { user: MockUserMember, - canViewAuditLog: true, - canViewDeployment: false, - canViewHealth: false, - canViewAISettings: false, - canViewOrganizations: false, + adminPermissions: { + canViewAuditLog: true, + }, }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); @@ -73,11 +75,10 @@ export const ForAuditor: Story = { export const ForOrgAdmin: Story = { args: { user: MockUserMember, - canViewAuditLog: true, - canViewDeployment: false, - canViewHealth: false, - canViewAISettings: false, - canViewOrganizations: true, + adminPermissions: { + canViewAuditLog: true, + canViewOrganizations: true, + }, }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); @@ -89,11 +90,9 @@ export const ForOrgAdmin: Story = { export const ForSingleOrgOSSAdmin: Story = { args: { - canViewAuditLog: false, - canViewOrganizations: false, - canViewConnectionLog: false, - canViewAIBridge: false, - canViewAISettings: false, + adminPermissions: { + canViewDeployment: true, + }, }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); @@ -106,11 +105,7 @@ export const ForSingleOrgOSSAdmin: Story = { export const ForMember: Story = { args: { user: MockUserMember, - canViewAuditLog: false, - canViewDeployment: false, - canViewHealth: false, - canViewAISettings: false, - canViewOrganizations: false, + adminPermissions: {}, canCreateChat: false, }, }; @@ -118,11 +113,7 @@ export const ForMember: Story = { export const ForMemberWithAgentsAccess: Story = { args: { user: MockUserMember, - canViewAuditLog: false, - canViewDeployment: false, - canViewHealth: false, - canViewAISettings: false, - canViewOrganizations: false, + adminPermissions: {}, canCreateChat: true, }, }; @@ -141,11 +132,7 @@ export const IdleTasks: Story = { export const SupportLinks: Story = { args: { user: MockUserMember, - canViewAuditLog: false, - canViewDeployment: false, - canViewHealth: false, - canViewAISettings: false, - canViewOrganizations: false, + adminPermissions: {}, supportLinks: [ { name: "This is a bug", @@ -182,11 +169,7 @@ export const SupportLinks: Story = { export const DefaultSupportLinks: Story = { args: { user: MockUserMember, - canViewAuditLog: false, - canViewDeployment: false, - canViewHealth: false, - canViewAISettings: false, - canViewOrganizations: false, + adminPermissions: {}, supportLinks: [ { icon: "docs", name: "Documentation", target: "" }, { icon: "bug", name: "Report a bug", target: "" }, diff --git a/site/src/modules/dashboard/Navbar/NavbarView.tsx b/site/src/modules/dashboard/Navbar/NavbarView.tsx index 967815c25cb22..486c302637109 100644 --- a/site/src/modules/dashboard/Navbar/NavbarView.tsx +++ b/site/src/modules/dashboard/Navbar/NavbarView.tsx @@ -16,7 +16,8 @@ import { useEmbeddedMetadata } from "#/hooks/useEmbeddedMetadata"; import { NotificationsInbox } from "#/modules/notifications/NotificationsInbox/NotificationsInbox"; import { getPrereleaseFlag } from "#/utils/buildInfo"; import { cn } from "#/utils/cn"; -import { DeploymentDropdown } from "./DeploymentDropdown"; +import { canViewAdminSettings, type AdminSettingsPermissions } from "./AdminSettings"; +import { AdminSettingsDropdown } from "./DeploymentDropdown"; import { MobileMenu } from "./MobileMenu"; import { ProxyMenu } from "./ProxyMenu"; import { SupportIcon } from "./SupportIcon"; @@ -27,13 +28,7 @@ interface NavbarViewProps { buildInfo?: TypesGen.BuildInfoResponse; supportLinks: readonly TypesGen.LinkConfig[]; onSignOut: () => void; - canViewDeployment: boolean; - canViewOrganizations: boolean; - canViewAuditLog: boolean; - canViewConnectionLog: boolean; - canViewHealth: boolean; - canViewAIBridge: boolean; - canViewAISettings: boolean; + adminPermissions: AdminSettingsPermissions; canCreateChat: boolean; proxyContextValue?: ProxyContextValue; } @@ -49,13 +44,7 @@ export const NavbarView: FC = ({ buildInfo, supportLinks, onSignOut, - canViewDeployment, - canViewOrganizations, - canViewHealth, - canViewAuditLog, - canViewConnectionLog, - canViewAIBridge, - canViewAISettings, + adminPermissions, canCreateChat, proxyContextValue, }) => { @@ -125,17 +114,9 @@ export const NavbarView: FC = ({ )} -
- -
+ {canViewAdminSettings(adminPermissions) &&
+ +
} = ({
@@ -207,7 +182,16 @@ const NavItems: FC = ({ className, user, canCreateChat }) => { Templates - + {canCreateChat && ( + { + return cn(linkStyles.default, { [linkStyles.active]: isActive }); + }} + to="/agents" + > + Agents + + )} ); }; @@ -272,23 +256,6 @@ function idleTasksLabel(count: number) { return `You have ${count} ${count === 1 ? "task" : "tasks"} waiting for input`; } -const AgentsNavItem: FC<{ canCreateChat: boolean }> = ({ canCreateChat }) => { - if (!canCreateChat) { - return null; - } - - return ( - { - return cn(linkStyles.default, { [linkStyles.active]: isActive }); - }} - to="/agents" - > - Agents - - ); -}; - function isNavbarLink(link: TypesGen.LinkConfig): boolean { return link.location === "navbar"; } diff --git a/site/src/modules/dashboard/Navbar/adminSettings.ts b/site/src/modules/dashboard/Navbar/adminSettings.ts deleted file mode 100644 index c22011fa96f61..0000000000000 --- a/site/src/modules/dashboard/Navbar/adminSettings.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { linkToAuditing } from "#/modules/navigation"; - -/** - * Permissions that determine which items appear in the Admin settings menu. - * Shared by the desktop `DeploymentDropdown` and the mobile `MobileMenu` so - * both surfaces render the same set of items from a single source of truth. - */ -export type AdminSettingsPermissions = { - canViewDeployment: boolean; - canViewOrganizations: boolean; - canViewAuditLog: boolean; - canViewConnectionLog: boolean; - canViewAIBridge: boolean; - canViewAISettings: boolean; - canViewHealth: boolean; -}; - -type AdminSettingsItem = { - label: string; - to: string; -}; - -/** - * Builds the ordered list of Admin settings menu items for the given - * permissions. Organizations is always available; the rest are gated behind - * their respective permissions. - */ -export const getAdminSettingsItems = ({ - canViewDeployment, - canViewAuditLog, - canViewConnectionLog, - canViewAIBridge, - canViewAISettings, - canViewHealth, -}: AdminSettingsPermissions): AdminSettingsItem[] => [ - ...(canViewDeployment ? [{ label: "Deployment", to: "/deployment" }] : []), - { label: "Organizations", to: "/organizations" }, - ...(canViewAISettings ? [{ label: "AI", to: "/ai/settings" }] : []), - ...(canViewAuditLog ? [{ label: "Audit logs", to: linkToAuditing }] : []), - ...(canViewConnectionLog - ? [{ label: "Connection logs", to: "/connectionlog" }] - : []), - ...(canViewAIBridge - ? [{ label: "AI sessions", to: "/ai-gateway/sessions" }] - : []), - ...(canViewHealth ? [{ label: "Healthcheck", to: "/health" }] : []), -]; - -/** - * Whether the user has any permission that should surface the Admin settings - * menu. Organizations alone does not gate visibility, matching prior behavior. - */ -export const canViewAdminSettings = ( - permissions: AdminSettingsPermissions, -): boolean => Object.values(permissions).some((canView) => canView); diff --git a/site/src/modules/navigation.ts b/site/src/modules/navigation.ts index 487c0e755d700..645a8f06759f8 100644 --- a/site/src/modules/navigation.ts +++ b/site/src/modules/navigation.ts @@ -17,14 +17,8 @@ export function useLinks() { return get; } -function withFilter(path: string, filter: string) { - return path + (filter ? `?filter=${encodeURIComponent(filter)}` : ""); -} - export const linkToAuditing = "/audit"; -const _linkToUsers = withFilter("/deployment/users", "status:active"); - export const linkToTemplate = (organizationName: string, templateName: string): LinkThunk => (dashboard) => diff --git a/site/src/modules/permissions/index.ts b/site/src/modules/permissions/index.ts index be97417f9c5a5..06a8d631ff278 100644 --- a/site/src/modules/permissions/index.ts +++ b/site/src/modules/permissions/index.ts @@ -40,8 +40,7 @@ export const canViewAnyOrganization = ( ): permissions is Permissions => { return ( permissions !== undefined && - (permissions.viewAnyMembers || - permissions.editAnyGroups || + (permissions.editAnyGroups || permissions.assignAnyRoles || permissions.viewAnyIdpSyncSettings || permissions.editAnySettings) From 4a78b64ecaa2eb98fff588af4ba47ae1d2b07b17 Mon Sep 17 00:00:00 2001 From: McKayla Washburn-Love Date: Fri, 24 Jul 2026 03:22:28 +0000 Subject: [PATCH 2/6] Update NavbarView.tsx --- site/src/modules/dashboard/Navbar/NavbarView.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/site/src/modules/dashboard/Navbar/NavbarView.tsx b/site/src/modules/dashboard/Navbar/NavbarView.tsx index 486c302637109..6634521970173 100644 --- a/site/src/modules/dashboard/Navbar/NavbarView.tsx +++ b/site/src/modules/dashboard/Navbar/NavbarView.tsx @@ -16,7 +16,10 @@ import { useEmbeddedMetadata } from "#/hooks/useEmbeddedMetadata"; import { NotificationsInbox } from "#/modules/notifications/NotificationsInbox/NotificationsInbox"; import { getPrereleaseFlag } from "#/utils/buildInfo"; import { cn } from "#/utils/cn"; -import { canViewAdminSettings, type AdminSettingsPermissions } from "./AdminSettings"; +import { + type AdminSettingsPermissions, + canViewAdminSettings, +} from "./AdminSettings"; import { AdminSettingsDropdown } from "./DeploymentDropdown"; import { MobileMenu } from "./MobileMenu"; import { ProxyMenu } from "./ProxyMenu"; @@ -114,9 +117,11 @@ export const NavbarView: FC = ({ )} - {canViewAdminSettings(adminPermissions) &&
- -
} + {canViewAdminSettings(adminPermissions) && ( +
+ +
+ )} Date: Fri, 24 Jul 2026 03:33:28 +0000 Subject: [PATCH 3/6] test(site/e2e): cover admin settings dropdown for licensed members --- site/e2e/tests/roles.spec.ts | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/site/e2e/tests/roles.spec.ts b/site/e2e/tests/roles.spec.ts index a1d39c7c42a11..a8607e65987d6 100644 --- a/site/e2e/tests/roles.spec.ts +++ b/site/e2e/tests/roles.spec.ts @@ -4,7 +4,7 @@ import { createOrganizationMember, setupApiCalls, } from "../api"; -import { license, users } from "../constants"; +import { defaultOrganizationId, license, users } from "../constants"; import { login, requiresLicense } from "../helpers"; import { beforeCoderTest } from "../hooks"; @@ -103,6 +103,25 @@ test.describe("org-scoped roles admin settings access", () => { await setupApiCalls(page); }); + test("member cannot see admin settings", async ({ page }) => { + // The unlicensed member test above cannot catch regressions here. + // Organizations are only shown when the deployment has the + // `multiple_organizations` entitlement, and every member can view the + // other members of their organizations, so the dropdown was visible to + // everyone. See https://github.com/coder/coder/issues/26695 + const member = await createOrganizationMember({ + orgRoles: { + [defaultOrganizationId]: [], + }, + }); + + await login(page, member); + await page.goto("/", { waitUntil: "domcontentloaded" }); + + // None, "Admin settings" button should not be visible + await hasAccessToAdminSettings(page, []); + }); + test("org template admin can see admin settings", async ({ page }) => { const org = await createOrganization(); const orgTemplateAdmin = await createOrganizationMember({ From 74e91dea4ae7a8d2b853f02f1365fea953c6dfc9 Mon Sep 17 00:00:00 2001 From: McKayla Washburn-Love Date: Fri, 24 Jul 2026 04:26:39 +0000 Subject: [PATCH 4/6] more lint --- site/e2e/tests/roles.spec.ts | 12 +++++------- site/src/modules/dashboard/Navbar/AdminSettings.tsx | 2 +- site/src/modules/navigation.ts | 2 -- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/site/e2e/tests/roles.spec.ts b/site/e2e/tests/roles.spec.ts index a8607e65987d6..c8bd30e80968f 100644 --- a/site/e2e/tests/roles.spec.ts +++ b/site/e2e/tests/roles.spec.ts @@ -4,7 +4,7 @@ import { createOrganizationMember, setupApiCalls, } from "../api"; -import { defaultOrganizationId, license, users } from "../constants"; +import { license, users } from "../constants"; import { login, requiresLicense } from "../helpers"; import { beforeCoderTest } from "../hooks"; @@ -104,14 +104,12 @@ test.describe("org-scoped roles admin settings access", () => { }); test("member cannot see admin settings", async ({ page }) => { - // The unlicensed member test above cannot catch regressions here. - // Organizations are only shown when the deployment has the - // `multiple_organizations` entitlement, and every member can view the - // other members of their organizations, so the dropdown was visible to - // everyone. See https://github.com/coder/coder/issues/26695 + // The unlicensed member test above cannot catch all regressions here. + // Many admin settings are locked behind a license and wouldn't be shown. + const org = await createOrganization(); const member = await createOrganizationMember({ orgRoles: { - [defaultOrganizationId]: [], + [org.id]: [], }, }); diff --git a/site/src/modules/dashboard/Navbar/AdminSettings.tsx b/site/src/modules/dashboard/Navbar/AdminSettings.tsx index 11d2df7dbe37e..4844c302b5fdb 100644 --- a/site/src/modules/dashboard/Navbar/AdminSettings.tsx +++ b/site/src/modules/dashboard/Navbar/AdminSettings.tsx @@ -6,7 +6,7 @@ import { DropdownMenuItem } from "#/components/DropdownMenu/DropdownMenu"; * Shared by the desktop `DeploymentDropdown` and the mobile `MobileMenu` so * both surfaces render the same set of items from a single source of truth. */ -export type AdminSettingsItemsProps = { +type AdminSettingsItemsProps = { itemClassName?: string; permissions: AdminSettingsPermissions; }; diff --git a/site/src/modules/navigation.ts b/site/src/modules/navigation.ts index 645a8f06759f8..70bdb302126ad 100644 --- a/site/src/modules/navigation.ts +++ b/site/src/modules/navigation.ts @@ -17,8 +17,6 @@ export function useLinks() { return get; } -export const linkToAuditing = "/audit"; - export const linkToTemplate = (organizationName: string, templateName: string): LinkThunk => (dashboard) => From 21cf028c410443854ac2a17e95bf99dd3da032ee Mon Sep 17 00:00:00 2001 From: McKayla Washburn-Love Date: Fri, 24 Jul 2026 04:46:38 +0000 Subject: [PATCH 5/6] =?UTF-8?q?=E6=83=A8=E3=82=81=E3=81=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/e2e/tests/roles.spec.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/site/e2e/tests/roles.spec.ts b/site/e2e/tests/roles.spec.ts index c8bd30e80968f..1120d7687acac 100644 --- a/site/e2e/tests/roles.spec.ts +++ b/site/e2e/tests/roles.spec.ts @@ -61,14 +61,14 @@ test.describe("roles admin settings access", () => { await login(page, users.templateAdmin); await page.goto("/", { waitUntil: "domcontentloaded" }); - await hasAccessToAdminSettings(page, ["Deployment", "Organizations"]); + await hasAccessToAdminSettings(page, ["Deployment"]); }); test("user admin can see admin settings", async ({ page }) => { await login(page, users.userAdmin); await page.goto("/", { waitUntil: "domcontentloaded" }); - await hasAccessToAdminSettings(page, ["Deployment", "Organizations"]); + await hasAccessToAdminSettings(page, ["Deployment"]); }); test("auditor can see admin settings", async ({ page }) => { @@ -77,7 +77,6 @@ test.describe("roles admin settings access", () => { await hasAccessToAdminSettings(page, [ "Deployment", - "Organizations", "Audit Logs", ]); }); @@ -88,7 +87,6 @@ test.describe("roles admin settings access", () => { await hasAccessToAdminSettings(page, [ "Deployment", - "Organizations", "Healthcheck", "Audit Logs", ]); From a69f4c77bb3b6d61bee29f3eb7b445b2743f1df4 Mon Sep 17 00:00:00 2001 From: McKayla Washburn-Love Date: Fri, 24 Jul 2026 04:50:53 +0000 Subject: [PATCH 6/6] =?UTF-8?q?=E6=83=A8=E3=82=81=E3=81=95=E3=82=88?= =?UTF-8?q?=E3=82=8A=E3=82=82=E3=81=AA=E3=81=8A=E3=81=B2=E3=81=A9=E3=81=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/e2e/tests/roles.spec.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/site/e2e/tests/roles.spec.ts b/site/e2e/tests/roles.spec.ts index 1120d7687acac..973b815991d17 100644 --- a/site/e2e/tests/roles.spec.ts +++ b/site/e2e/tests/roles.spec.ts @@ -75,10 +75,7 @@ test.describe("roles admin settings access", () => { await login(page, users.auditor); await page.goto("/", { waitUntil: "domcontentloaded" }); - await hasAccessToAdminSettings(page, [ - "Deployment", - "Audit Logs", - ]); + await hasAccessToAdminSettings(page, ["Deployment", "Audit Logs"]); }); test("owner can see admin settings", async ({ page }) => {