From f3828b430e17c3feb1a51535aeda5dca533318f8 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Wed, 5 Feb 2025 22:41:33 +0000 Subject: [PATCH 01/21] oh man what I have gotten myself into --- site/src/api/queries/organizations.ts | 110 ++---------- .../management/OrganizationSettingsLayout.tsx | 57 +++++-- .../management/OrganizationSidebar.tsx | 62 ++----- .../OrganizationSidebarView.stories.tsx | 158 +++--------------- .../management/OrganizationSidebarView.tsx | 115 +++++-------- .../management/organizationPermissions.tsx | 73 ++++++++ .../NotificationsPage/storybookUtils.ts | 4 +- site/src/pages/GroupsPage/GroupsPage.tsx | 17 +- .../pages/GroupsPage/GroupsPageProvider.tsx | 11 +- .../CustomRolesPage/CreateEditRolePage.tsx | 10 +- .../CustomRolesPage/CustomRolesPage.tsx | 19 +-- .../DefaultOrganizationRedirect.tsx | 38 +++++ .../OrganizationMembersPage.tsx | 22 +-- .../OrganizationMembersPageView.tsx | 1 - .../OrganizationSettingsPage.stories.tsx | 4 +- .../OrganizationSettingsPage.tsx | 26 +-- .../OrganizationSummaryPageView.stories.tsx | 23 --- .../OrganizationSummaryPageView.tsx | 49 ------ site/src/router.tsx | 6 +- site/src/testHelpers/entities.ts | 25 +++ site/src/testHelpers/storybook.tsx | 8 +- 21 files changed, 329 insertions(+), 509 deletions(-) create mode 100644 site/src/modules/management/organizationPermissions.tsx create mode 100644 site/src/pages/OrganizationSettingsPage/DefaultOrganizationRedirect.tsx delete mode 100644 site/src/pages/OrganizationSettingsPage/OrganizationSummaryPageView.stories.tsx delete mode 100644 site/src/pages/OrganizationSettingsPage/OrganizationSummaryPageView.tsx diff --git a/site/src/api/queries/organizations.ts b/site/src/api/queries/organizations.ts index 33ef19f0d2654..904a5702aaf36 100644 --- a/site/src/api/queries/organizations.ts +++ b/site/src/api/queries/organizations.ts @@ -1,6 +1,5 @@ import { API } from "api/api"; import type { - AuthorizationResponse, CreateOrganizationRequest, GroupSyncSettings, RoleSyncSettings, @@ -8,6 +7,11 @@ import type { } from "api/typesGenerated"; import type { QueryClient } from "react-query"; import { meKey } from "./users"; +import { + organizationPermissionChecks, + type OrganizationPermissions, + type OrganizationPermissionName, +} from "modules/management/organizationPermissions"; export const createOrganization = (queryClient: QueryClient) => { return { @@ -197,53 +201,6 @@ export const patchRoleSyncSettings = ( }; }; -/** - * Fetch permissions for a single organization. - * - * If the ID is undefined, return a disabled query. - */ -export const organizationPermissions = (organizationId: string | undefined) => { - if (!organizationId) { - return { enabled: false }; - } - return { - queryKey: ["organization", organizationId, "permissions"], - queryFn: () => - // Only request what we use on individual org settings, members, and group - // pages, which at the moment is whether you can edit the members on the - // members page, create roles on the roles page, and create groups on the - // groups page. The edit organization check for the settings page is - // covered by the multi-org query at the moment, and the edit group check - // on the group page is done on the group itself, not the org, so neither - // show up here. - API.checkAuthorization({ - checks: { - editMembers: { - object: { - resource_type: "organization_member", - organization_id: organizationId, - }, - action: "update", - }, - createGroup: { - object: { - resource_type: "group", - organization_id: organizationId, - }, - action: "create", - }, - assignOrgRole: { - object: { - resource_type: "assign_org_role", - organization_id: organizationId, - }, - action: "create", - }, - }, - }), - }; -}; - /** * Fetch permissions for all provided organizations. * @@ -263,58 +220,13 @@ export const organizationsPermissions = ( // per sub-link (settings, groups, roles, and members pages) that tells us // whether to show that page, since we only show them if you can edit (and // not, at the moment if you can only view). - const checks = (organizationId: string) => ({ - editMembers: { - object: { - resource_type: "organization_member", - organization_id: organizationId, - }, - action: "update", - }, - editGroups: { - object: { - resource_type: "group", - organization_id: organizationId, - }, - action: "update", - }, - editOrganization: { - object: { - resource_type: "organization", - organization_id: organizationId, - }, - action: "update", - }, - assignOrgRole: { - object: { - resource_type: "assign_org_role", - organization_id: organizationId, - }, - action: "create", - }, - viewProvisioners: { - object: { - resource_type: "provisioner_daemon", - organization_id: organizationId, - }, - action: "read", - }, - viewIdpSyncSettings: { - object: { - resource_type: "idpsync_settings", - organization_id: organizationId, - }, - action: "read", - }, - }); // The endpoint takes a flat array, so to avoid collisions prepend each // check with the org ID (the key can be anything we want). const prefixedChecks = organizationIds.flatMap((orgId) => - Object.entries(checks(orgId)).map(([key, val]) => [ - `${orgId}.${key}`, - val, - ]), + Object.entries(organizationPermissionChecks(orgId)).map( + ([key, val]) => [`${orgId}.${key}`, val], + ), ); const response = await API.checkAuthorization({ @@ -330,11 +242,11 @@ export const organizationsPermissions = ( if (!acc[orgId]) { acc[orgId] = {}; } - acc[orgId][perm] = value; + acc[orgId][perm as OrganizationPermissionName] = value; return acc; }, - {} as Record, - ); + {} as Record>, + ) as Record; }, }; }; diff --git a/site/src/modules/management/OrganizationSettingsLayout.tsx b/site/src/modules/management/OrganizationSettingsLayout.tsx index 3e9e2537a0ec2..f7f7da3e674c9 100644 --- a/site/src/modules/management/OrganizationSettingsLayout.tsx +++ b/site/src/modules/management/OrganizationSettingsLayout.tsx @@ -1,20 +1,23 @@ +import { organizationsPermissions } from "api/queries/organizations"; import type { AuthorizationResponse, Organization } from "api/typesGenerated"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; import { Avatar } from "components/Avatar/Avatar"; import { Breadcrumb, BreadcrumbItem, - BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, } from "components/Breadcrumb/Breadcrumb"; -import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge"; import { Loader } from "components/Loader/Loader"; import { useAuthenticated } from "contexts/auth/RequireAuth"; import { RequirePermission } from "contexts/auth/RequirePermission"; import { useDashboard } from "modules/dashboard/useDashboard"; +import NotFoundPage from "pages/404Page/404Page"; import { type FC, Suspense, createContext, useContext } from "react"; +import { useQuery } from "react-query"; import { Outlet, useParams } from "react-router-dom"; +import type { OrganizationPermissions } from "./organizationPermissions"; export const OrganizationSettingsContext = createContext< OrganizationSettingsValue | undefined @@ -22,7 +25,9 @@ export const OrganizationSettingsContext = createContext< type OrganizationSettingsValue = Readonly<{ organizations: readonly Organization[]; + permissionsByOrganizationId: Record; organization?: Organization; + organizationPermissions?: OrganizationPermissions; }>; export const useOrganizationSettings = (): OrganizationSettingsValue => { @@ -37,16 +42,19 @@ export const useOrganizationSettings = (): OrganizationSettingsValue => { }; /** - * Return true if the user can edit the organization settings or its members. + * Checks if the user can view or edit members or groups for the organization + * that produced the given OrganizationPermissions. */ -export const canEditOrganization = ( - permissions: AuthorizationResponse | undefined, -) => { +const canViewOrganization = ( + permissions: OrganizationPermissions | undefined, +): permissions is OrganizationPermissions => { return ( permissions !== undefined && (permissions.editOrganization || permissions.editMembers || - permissions.editGroups) + permissions.viewMembers || + permissions.editGroups || + permissions.viewGroups) ); }; @@ -57,19 +65,46 @@ const OrganizationSettingsLayout: FC = () => { organization?: string; }; - const canViewOrganizationSettingsPage = - permissions.viewDeploymentValues || permissions.editAnyOrganization; + const canViewOrganizationSettingsPage = permissions.editAnyOrganization; const organization = orgName ? organizations.find((org) => org.name === orgName) : undefined; + const orgPermissionsQuery = useQuery( + organizationsPermissions(organizations?.map((o) => o.id)), + ); + + if (orgPermissionsQuery.isLoading) { + return ; + } + + if (!orgPermissionsQuery.data) { + return ; + } + + const viewableOrganizations = organizations.filter((org) => + canViewOrganization(orgPermissionsQuery.data?.[org.id]), + ); + + // It's currently up to each individual page to show an empty state if there + // is no matching organization. This is weird and we should probably fix it + // eventually, but if we handled it here it would break the /new route, and + // refactoring to fix _that_ is a non-trivial amount of work. + const organizationPermissions = + organization && orgPermissionsQuery.data?.[organization.id]; + if (organization && !canViewOrganization(organizationPermissions)) { + return ; + } + return (
@@ -95,7 +130,7 @@ const OrganizationSettingsLayout: FC = () => { fallback={organization.display_name} src={organization.icon} /> - {organization?.name} + {organization.display_name} diff --git a/site/src/modules/management/OrganizationSidebar.tsx b/site/src/modules/management/OrganizationSidebar.tsx index 8ef14f9baf165..40f29146a3981 100644 --- a/site/src/modules/management/OrganizationSidebar.tsx +++ b/site/src/modules/management/OrganizationSidebar.tsx @@ -1,59 +1,25 @@ -import { organizationsPermissions } from "api/queries/organizations"; import { useAuthenticated } from "contexts/auth/RequireAuth"; -import { - canEditOrganization, - useOrganizationSettings, -} from "modules/management/OrganizationSettingsLayout"; +import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout"; import type { FC } from "react"; -import { useQuery } from "react-query"; -import { useParams } from "react-router-dom"; -import { - OrganizationSidebarView, - type OrganizationWithPermissions, -} from "./OrganizationSidebarView"; +import { OrganizationSidebarView } from "./OrganizationSidebarView"; +import { Sidebar as BaseSidebar } from "components/Sidebar/Sidebar"; /** - * A combined deployment settings and organization menu. - * - * This should only be used with multi-org support. If multi-org support is - * disabled or not licensed, this is the wrong sidebar to use. See - * DeploySettingsPage/Sidebar instead. + * Sidebar for the OrganizationSettingsLayout */ export const OrganizationSidebar: FC = () => { const { permissions } = useAuthenticated(); - const { organizations } = useOrganizationSettings(); - const { organization: organizationName } = useParams() as { - organization?: string; - }; - - const orgPermissionsQuery = useQuery( - organizationsPermissions(organizations?.map((o) => o.id)), - ); - - // Sometimes a user can read an organization but cannot actually do anything - // with it. For now, these are filtered out so you only see organizations you - // can manage in some way. - const editableOrgs = organizations - ?.map((org) => { - return { - ...org, - permissions: orgPermissionsQuery.data?.[org.id], - }; - }) - // TypeScript is not able to infer whether permissions are defined on the - // object even if we explicitly check org.permissions here, so add the `is` - // here to help out (canEditOrganization does the actual check). - .filter((org): org is OrganizationWithPermissions => { - return canEditOrganization(org.permissions); - }); - - const organization = editableOrgs?.find((o) => o.name === organizationName); + const { organizations, organization, organizationPermissions } = + useOrganizationSettings(); return ( - + + + ); }; diff --git a/site/src/modules/management/OrganizationSidebarView.stories.tsx b/site/src/modules/management/OrganizationSidebarView.stories.tsx index 4f1b17a27c181..f6c7c204c451c 100644 --- a/site/src/modules/management/OrganizationSidebarView.stories.tsx +++ b/site/src/modules/management/OrganizationSidebarView.stories.tsx @@ -1,9 +1,11 @@ import type { Meta, StoryObj } from "@storybook/react"; import { expect, userEvent, waitFor, within } from "@storybook/test"; import { + MockNoOrganizationPermissions, MockNoPermissions, MockOrganization, MockOrganization2, + MockOrganizationPermissions, MockPermissions, } from "testHelpers/entities"; import { withDashboardProvider } from "testHelpers/storybook"; @@ -16,26 +18,7 @@ const meta: Meta = { parameters: { showOrganizations: true }, args: { activeOrganization: undefined, - organizations: [ - { - ...MockOrganization, - permissions: { - editOrganization: true, - editMembers: true, - editGroups: true, - auditOrganization: true, - }, - }, - { - ...MockOrganization2, - permissions: { - editOrganization: true, - editMembers: true, - editGroups: true, - auditOrganization: true, - }, - }, - ], + organizations: [MockOrganization, MockOrganization2], permissions: MockPermissions, }, }; @@ -51,10 +34,8 @@ export const LoadingOrganizations: Story = { export const NoCreateOrg: Story = { args: { - activeOrganization: { - ...MockOrganization, - permissions: { createOrganization: false }, - }, + activeOrganization: MockOrganization, + orgPermissions: MockNoOrganizationPermissions, permissions: { ...MockPermissions, createOrganization: false, @@ -73,23 +54,15 @@ export const NoCreateOrg: Story = { export const OverflowDropdown: Story = { args: { - activeOrganization: { - ...MockOrganization, - permissions: { createOrganization: true }, - }, + activeOrganization: MockOrganization, + orgPermissions: MockOrganizationPermissions, permissions: { ...MockPermissions, createOrganization: true, }, organizations: [ - { - ...MockOrganization, - permissions: {}, - }, - { - ...MockOrganization2, - permissions: {}, - }, + MockOrganization, + MockOrganization2, { id: "my-organization-3-id", name: "my-organization-3", @@ -99,7 +72,6 @@ export const OverflowDropdown: Story = { created_at: "", updated_at: "", is_default: false, - permissions: {}, }, { id: "my-organization-4-id", @@ -110,7 +82,6 @@ export const OverflowDropdown: Story = { created_at: "", updated_at: "", is_default: false, - permissions: {}, }, { id: "my-organization-5-id", @@ -121,7 +92,6 @@ export const OverflowDropdown: Story = { created_at: "", updated_at: "", is_default: false, - permissions: {}, }, { id: "my-organization-6-id", @@ -132,7 +102,6 @@ export const OverflowDropdown: Story = { created_at: "", updated_at: "", is_default: false, - permissions: {}, }, { id: "my-organization-7-id", @@ -143,7 +112,6 @@ export const OverflowDropdown: Story = { created_at: "", updated_at: "", is_default: false, - permissions: {}, }, ], }, @@ -157,127 +125,53 @@ export const OverflowDropdown: Story = { export const NoPermissions: Story = { args: { - activeOrganization: { - ...MockOrganization, - permissions: MockNoPermissions, - }, + activeOrganization: MockOrganization, + orgPermissions: MockNoOrganizationPermissions, permissions: MockNoPermissions, }, }; export const AllPermissions: Story = { args: { - activeOrganization: { - ...MockOrganization, - permissions: { - editOrganization: true, - editMembers: true, - editGroups: true, - auditOrganization: true, - assignOrgRole: true, - viewProvisioners: true, - viewIdpSyncSettings: true, - }, - }, - organizations: [ - { - ...MockOrganization, - permissions: { - editOrganization: true, - editMembers: true, - editGroups: true, - auditOrganization: true, - assignOrgRole: true, - viewProvisioners: true, - viewIdpSyncSettings: true, - }, - }, - ], + activeOrganization: MockOrganization, + orgPermissions: MockOrganizationPermissions, + organizations: [MockOrganization], }, }; export const SelectedOrgAdmin: Story = { args: { - activeOrganization: { - ...MockOrganization, - permissions: { - editOrganization: true, - editMembers: true, - editGroups: true, - auditOrganization: true, - assignOrgRole: true, - }, - }, - organizations: [ - { - ...MockOrganization, - permissions: { - editOrganization: true, - editMembers: true, - editGroups: true, - auditOrganization: true, - assignOrgRole: true, - }, - }, - ], + activeOrganization: MockOrganization, + orgPermissions: MockOrganizationPermissions, + organizations: [MockOrganization], }, }; export const SelectedOrgAuditor: Story = { args: { - activeOrganization: { - ...MockOrganization, - permissions: { - editOrganization: false, - editMembers: false, - editGroups: false, - auditOrganization: true, - }, - }, + activeOrganization: MockOrganization, + orgPermissions: MockNoOrganizationPermissions, permissions: { ...MockPermissions, createOrganization: false, }, - organizations: [ - { - ...MockOrganization, - permissions: { - editOrganization: false, - editMembers: false, - editGroups: false, - auditOrganization: true, - }, - }, - ], + organizations: [MockOrganization], }, }; export const SelectedOrgUserAdmin: Story = { args: { - activeOrganization: { - ...MockOrganization, - permissions: { - editOrganization: false, - editMembers: true, - editGroups: true, - auditOrganization: false, - }, + activeOrganization: MockOrganization, + orgPermissions: { + ...MockNoOrganizationPermissions, + editMembers: true, + editGroups: true, }, permissions: { ...MockPermissions, createOrganization: false, }, - organizations: [ - { - ...MockOrganization, - permissions: { - editOrganization: false, - editMembers: true, - editGroups: true, - auditOrganization: false, - }, - }, - ], + organizations: [MockOrganization], }, }; diff --git a/site/src/modules/management/OrganizationSidebarView.tsx b/site/src/modules/management/OrganizationSidebarView.tsx index 8d913edf87df3..b86121f7cde93 100644 --- a/site/src/modules/management/OrganizationSidebarView.tsx +++ b/site/src/modules/management/OrganizationSidebarView.tsx @@ -7,31 +7,25 @@ import { CommandItem, CommandList, } from "components/Command/Command"; -import { Loader } from "components/Loader/Loader"; import { Popover, PopoverContent, PopoverTrigger, } from "components/Popover/Popover"; -import { - Sidebar as BaseSidebar, - SettingsSidebarNavItem, -} from "components/Sidebar/Sidebar"; +import { SettingsSidebarNavItem } from "components/Sidebar/Sidebar"; import type { Permissions } from "contexts/auth/permissions"; import { Check, ChevronDown, Plus } from "lucide-react"; -import { useDashboard } from "modules/dashboard/useDashboard"; import { type FC, useState } from "react"; import { useNavigate } from "react-router-dom"; - -export interface OrganizationWithPermissions extends Organization { - permissions: AuthorizationResponse; -} +import type { OrganizationPermissions } from "./organizationPermissions"; interface SidebarProps { /** The active org name, if any. Overrides activeSettings. */ - activeOrganization: OrganizationWithPermissions | undefined; + activeOrganization: Organization | undefined; + /** Permissions for the active organization */ + orgPermissions: OrganizationPermissions | undefined; /** Organizations and their permissions or undefined if still fetching. */ - organizations: OrganizationWithPermissions[] | undefined; + organizations: readonly Organization[]; /** Site-wide permissions. */ permissions: Permissions; } @@ -41,58 +35,17 @@ interface SidebarProps { */ export const OrganizationSidebarView: FC = ({ activeOrganization, + orgPermissions, organizations, permissions, }) => { - const { showOrganizations } = useDashboard(); - - return ( - - {showOrganizations && ( - - )} - - ); -}; - -function urlForSubpage(organizationName: string, subpage = ""): string { - return [`/organizations/${organizationName}`, subpage] - .filter(Boolean) - .join("/"); -} - -interface OrganizationsSettingsNavigationProps { - /** The active org name if an org is being viewed. */ - activeOrganization: OrganizationWithPermissions | undefined; - /** Organizations and their permissions or undefined if still fetching. */ - organizations: OrganizationWithPermissions[] | undefined; - /** Site-wide permissions. */ - permissions: Permissions; -} - -/** - * Displays navigation items for the active organization and a combobox to - * switch between organizations. - * - * If organizations or their permissions are still loading, show a loader. - */ -const OrganizationsSettingsNavigation: FC< - OrganizationsSettingsNavigationProps -> = ({ activeOrganization, organizations, permissions }) => { - // Wait for organizations and their permissions to load - if (!organizations || !activeOrganization) { - return ; - } - // Sort organizations to put active organization first - const sortedOrganizations = [ - activeOrganization, - ...organizations.filter((org) => org.id !== activeOrganization.id), - ]; + const sortedOrganizations = activeOrganization + ? [ + activeOrganization, + ...organizations.filter((org) => org.id !== activeOrganization.id), + ] + : organizations; const [isPopoverOpen, setIsPopoverOpen] = useState(false); const navigate = useNavigate(); @@ -125,7 +78,7 @@ const OrganizationsSettingsNavigation: FC< - {sortedOrganizations.length > 1 && ( + {sortedOrganizations.length > 1 ? (
{sortedOrganizations.map((organization) => ( {organization?.display_name || organization?.name} - {activeOrganization.name === organization.name && ( + {activeOrganization?.name === organization.name && ( ))}
+ ) : ( + + No more organizations + )} {permissions.createOrganization && ( <> @@ -181,58 +138,68 @@ const OrganizationsSettingsNavigation: FC<
- + {activeOrganization && orgPermissions && ( + + )} ); }; +function urlForSubpage(organizationName: string, subpage = ""): string { + return [`/organizations/${organizationName}`, subpage] + .filter(Boolean) + .join("/"); +} + interface OrganizationSettingsNavigationProps { - organization: OrganizationWithPermissions; + organization: Organization; + orgPermissions: AuthorizationResponse; } const OrganizationSettingsNavigation: FC< OrganizationSettingsNavigationProps -> = ({ organization }) => { +> = ({ organization, orgPermissions }) => { return ( <>
- {organization.permissions.editMembers && ( + {orgPermissions.viewMembers && ( Members )} - {organization.permissions.editGroups && ( + {orgPermissions.viewGroups && ( Groups )} - {organization.permissions.assignOrgRole && ( + {orgPermissions.assignOrgRole && ( Roles )} - {organization.permissions.viewProvisioners && ( + {orgPermissions.viewProvisioners && ( Provisioners )} - {organization.permissions.viewIdpSyncSettings && ( + {orgPermissions.viewIdpSyncSettings && ( IdP Sync )} - {organization.permissions.editOrganization && ( + {orgPermissions.editOrganization && ( diff --git a/site/src/modules/management/organizationPermissions.tsx b/site/src/modules/management/organizationPermissions.tsx new file mode 100644 index 0000000000000..d004af990b3ce --- /dev/null +++ b/site/src/modules/management/organizationPermissions.tsx @@ -0,0 +1,73 @@ +export type OrganizationPermissions = { + [k in OrganizationPermissionName]: boolean; +}; + +export type OrganizationPermissionName = keyof ReturnType< + typeof organizationPermissionChecks +>; + +export const organizationPermissionChecks = (organizationId: string) => ({ + viewMembers: { + object: { + resource_type: "organization_member", + organization_id: organizationId, + }, + action: "read", + }, + editMembers: { + object: { + resource_type: "organization_member", + organization_id: organizationId, + }, + action: "update", + }, + createGroup: { + object: { + resource_type: "group", + organization_id: organizationId, + }, + action: "create", + }, + viewGroups: { + object: { + resource_type: "group", + organization_id: organizationId, + }, + action: "read", + }, + editGroups: { + object: { + resource_type: "group", + organization_id: organizationId, + }, + action: "update", + }, + editOrganization: { + object: { + resource_type: "organization", + organization_id: organizationId, + }, + action: "update", + }, + assignOrgRole: { + object: { + resource_type: "assign_org_role", + organization_id: organizationId, + }, + action: "create", + }, + viewProvisioners: { + object: { + resource_type: "provisioner_daemon", + organization_id: organizationId, + }, + action: "read", + }, + viewIdpSyncSettings: { + object: { + resource_type: "idpsync_settings", + organization_id: organizationId, + }, + action: "read", + }, +}); diff --git a/site/src/pages/DeploymentSettingsPage/NotificationsPage/storybookUtils.ts b/site/src/pages/DeploymentSettingsPage/NotificationsPage/storybookUtils.ts index 4906a5ab54496..fc500efd847d6 100644 --- a/site/src/pages/DeploymentSettingsPage/NotificationsPage/storybookUtils.ts +++ b/site/src/pages/DeploymentSettingsPage/NotificationsPage/storybookUtils.ts @@ -13,7 +13,7 @@ import { withAuthProvider, withDashboardProvider, withGlobalSnackbar, - withManagementSettingsProvider, + withOrganizationSettingsProvider, } from "testHelpers/storybook"; import type { NotificationsPage } from "./NotificationsPage"; @@ -213,6 +213,6 @@ export const baseMeta = { withGlobalSnackbar, withAuthProvider, withDashboardProvider, - withManagementSettingsProvider, + withOrganizationSettingsProvider, ], } satisfies Meta; diff --git a/site/src/pages/GroupsPage/GroupsPage.tsx b/site/src/pages/GroupsPage/GroupsPage.tsx index 5e33e232227ef..b94063fd74e75 100644 --- a/site/src/pages/GroupsPage/GroupsPage.tsx +++ b/site/src/pages/GroupsPage/GroupsPage.tsx @@ -1,7 +1,7 @@ import GroupAdd from "@mui/icons-material/GroupAddOutlined"; import { getErrorMessage } from "api/errors"; import { groupsByOrganization } from "api/queries/groups"; -import { organizationPermissions } from "api/queries/organizations"; +import { organizationsPermissions } from "api/queries/organizations"; import { Button } from "components/Button/Button"; import { EmptyState } from "components/EmptyState/EmptyState"; import { displayError } from "components/GlobalSnackbar/utils"; @@ -16,6 +16,7 @@ import { Link as RouterLink } from "react-router-dom"; import { pageTitle } from "utils/page"; import { useGroupsSettings } from "./GroupsPageProvider"; import GroupsPageView from "./GroupsPageView"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; export const GroupsPage: FC = () => { const { template_rbac: groupsEnabled } = useFeatureVisibility(); @@ -23,7 +24,11 @@ export const GroupsPage: FC = () => { const groupsQuery = useQuery( organization ? groupsByOrganization(organization.name) : { enabled: false }, ); - const permissionsQuery = useQuery(organizationPermissions(organization?.id)); + const permissionsQuery = useQuery( + organization + ? organizationsPermissions([organization.id]) + : { enabled: false }, + ); useEffect(() => { if (groupsQuery.error) { @@ -45,11 +50,15 @@ export const GroupsPage: FC = () => { return ; } - const permissions = permissionsQuery.data; - if (!permissions) { + if (permissionsQuery.isLoading) { return ; } + const permissions = permissionsQuery.data?.[organization.id]; + if (!permissions) { + return ; + } + return ( <> diff --git a/site/src/pages/GroupsPage/GroupsPageProvider.tsx b/site/src/pages/GroupsPage/GroupsPageProvider.tsx index 85ccd763be10a..3697705aebc4b 100644 --- a/site/src/pages/GroupsPage/GroupsPageProvider.tsx +++ b/site/src/pages/GroupsPage/GroupsPageProvider.tsx @@ -1,13 +1,6 @@ -import type { AuthorizationResponse, Organization } from "api/typesGenerated"; -import { useAuthenticated } from "contexts/auth/RequireAuth"; -import { RequirePermission } from "contexts/auth/RequirePermission"; +import type { Organization } from "api/typesGenerated"; import { useDashboard } from "modules/dashboard/useDashboard"; -import { - type FC, - type PropsWithChildren, - createContext, - useContext, -} from "react"; +import { type FC, createContext, useContext } from "react"; import { Navigate, Outlet, useParams } from "react-router-dom"; export const GroupsPageContext = createContext< diff --git a/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CreateEditRolePage.tsx b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CreateEditRolePage.tsx index 9bb27679689fa..aa678f5a3e277 100644 --- a/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CreateEditRolePage.tsx +++ b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CreateEditRolePage.tsx @@ -1,5 +1,4 @@ import { getErrorMessage } from "api/errors"; -import { organizationPermissions } from "api/queries/organizations"; import { createOrganizationRole, organizationRoles, @@ -24,9 +23,7 @@ export const CreateEditRolePage: FC = () => { organization: string; roleName: string; }; - const { organizations } = useOrganizationSettings(); - const organization = organizations?.find((o) => o.name === organizationName); - const permissionsQuery = useQuery(organizationPermissions(organization?.id)); + const { organizationPermissions } = useOrganizationSettings(); const createOrganizationRoleMutation = useMutation( createOrganizationRole(queryClient, organizationName), ); @@ -37,9 +34,8 @@ export const CreateEditRolePage: FC = () => { organizationRoles(organizationName), ); const role = roleData?.find((role) => role.name === roleName); - const permissions = permissionsQuery.data; - if (isLoading || !permissions) { + if (isLoading || !organizationPermissions) { return ; } @@ -80,7 +76,7 @@ export const CreateEditRolePage: FC = () => { : createOrganizationRoleMutation.isLoading } organizationName={organizationName} - canAssignOrgRole={permissions.assignOrgRole} + canAssignOrgRole={organizationPermissions.assignOrgRole} /> ); diff --git a/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPage.tsx b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPage.tsx index 905e67ebd26e3..26fa8e45d8eb6 100644 --- a/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPage.tsx @@ -1,5 +1,4 @@ import { getErrorMessage } from "api/errors"; -import { organizationPermissions } from "api/queries/organizations"; import { deleteOrganizationRole, organizationRoles } from "api/queries/roles"; import type { Role } from "api/typesGenerated"; import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog"; @@ -22,13 +21,10 @@ export const CustomRolesPage: FC = () => { const { organization: organizationName } = useParams() as { organization: string; }; - const { organizations } = useOrganizationSettings(); - const organization = organizations?.find((o) => o.name === organizationName); - const permissionsQuery = useQuery(organizationPermissions(organization?.id)); - const deleteRoleMutation = useMutation( - deleteOrganizationRole(queryClient, organizationName), - ); + const { organizationPermissions } = useOrganizationSettings(); + const [roleToDelete, setRoleToDelete] = useState(); + const organizationRolesQuery = useQuery(organizationRoles(organizationName)); const builtInRoles = organizationRolesQuery.data?.filter( (role) => role.built_in, @@ -36,7 +32,10 @@ export const CustomRolesPage: FC = () => { const customRoles = organizationRolesQuery.data?.filter( (role) => !role.built_in, ); - const permissions = permissionsQuery.data; + + const deleteRoleMutation = useMutation( + deleteOrganizationRole(queryClient, organizationName), + ); useEffect(() => { if (organizationRolesQuery.error) { @@ -49,7 +48,7 @@ export const CustomRolesPage: FC = () => { } }, [organizationRolesQuery.error]); - if (!permissions) { + if (!organizationPermissions) { return ; } @@ -74,7 +73,7 @@ export const CustomRolesPage: FC = () => { builtInRoles={builtInRoles} customRoles={customRoles} onDeleteRole={setRoleToDelete} - canAssignOrgRole={permissions.assignOrgRole} + canAssignOrgRole={organizationPermissions.assignOrgRole} isCustomRolesEnabled={isCustomRolesEnabled} /> diff --git a/site/src/pages/OrganizationSettingsPage/DefaultOrganizationRedirect.tsx b/site/src/pages/OrganizationSettingsPage/DefaultOrganizationRedirect.tsx new file mode 100644 index 0000000000000..719ed357d3d45 --- /dev/null +++ b/site/src/pages/OrganizationSettingsPage/DefaultOrganizationRedirect.tsx @@ -0,0 +1,38 @@ +import type { FC } from "react"; +import { EmptyState } from "components/EmptyState/EmptyState"; +import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout"; +import { Navigate } from "react-router-dom"; +import type { OrganizationPermissions } from "modules/management/organizationPermissions"; + +/** + * Return true if the user can edit the organization settings or its members. + */ +const canEditOrganization = ( + permissions: OrganizationPermissions | undefined, +): permissions is OrganizationPermissions => { + return ( + permissions !== undefined && + (permissions.editOrganization || + permissions.editMembers || + permissions.editGroups) + ); +}; + +const DefaultOrganizationRedirect: FC = () => { + const { organizations, permissionsByOrganizationId } = + useOrganizationSettings(); + + // Redirect /organizations => /organizations/default-org, or if they cannot edit + // the default org, then the first org they can edit, if any. + // .find will stop at the first match found; make sure default + // organizations are placed first + const editableOrg = [...organizations] + .sort((a, b) => (b.is_default ? 1 : 0) - (a.is_default ? 1 : 0)) + .find((org) => canEditOrganization(permissionsByOrganizationId[org.id])); + if (editableOrg) { + return ; + } + return ; +}; + +export default DefaultOrganizationRedirect; diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.tsx index ac90365ea4d43..29ed27eb04942 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.tsx @@ -4,7 +4,6 @@ import { groupsByUserIdInOrganization } from "api/queries/groups"; import { addOrganizationMember, organizationMembers, - organizationPermissions, removeOrganizationMember, updateOrganizationMemberRoles, } from "api/queries/organizations"; @@ -19,24 +18,24 @@ import { useOrganizationSettings } from "modules/management/OrganizationSettings import { type FC, useState } from "react"; import { Helmet } from "react-helmet-async"; import { useMutation, useQuery, useQueryClient } from "react-query"; -import { useParams } from "react-router-dom"; import { pageTitle } from "utils/page"; import { OrganizationMembersPageView } from "./OrganizationMembersPageView"; +import { useParams } from "react-router-dom"; const OrganizationMembersPage: FC = () => { const queryClient = useQueryClient(); + const { user: me } = useAuthenticated(); const { organization: organizationName } = useParams() as { organization: string; }; - const { user: me } = useAuthenticated(); + const { organization, organizationPermissions } = useOrganizationSettings(); + const membersQuery = useQuery(organizationMembers(organizationName)); + const organizationRolesQuery = useQuery(organizationRoles(organizationName)); const groupsByUserIdQuery = useQuery( groupsByUserIdInOrganization(organizationName), ); - const membersQuery = useQuery(organizationMembers(organizationName)); - const organizationRolesQuery = useQuery(organizationRoles(organizationName)); - const members = membersQuery.data?.map((member) => { const groups = groupsByUserIdQuery.data?.get(member.user_id) ?? []; return { ...member, groups }; @@ -52,15 +51,10 @@ const OrganizationMembersPage: FC = () => { updateOrganizationMemberRoles(queryClient, organizationName), ); - const { organizations } = useOrganizationSettings(); - const organization = organizations?.find((o) => o.name === organizationName); - const permissionsQuery = useQuery(organizationPermissions(organization?.id)); - const [memberToDelete, setMemberToDelete] = useState(); - const permissions = permissionsQuery.data; - if (!permissions) { + if (!organizationPermissions) { return ; } @@ -77,9 +71,11 @@ const OrganizationMembersPage: FC = () => { {helmet} = { decorators: [ withAuthProvider, withDashboardProvider, - withManagementSettingsProvider, + withOrganizationSettingsProvider, ], parameters: { showOrganizations: true, diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx index 698f2ee75822f..5f29b71e63ad4 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx @@ -7,21 +7,18 @@ import { ErrorAlert } from "components/Alert/ErrorAlert"; import { EmptyState } from "components/EmptyState/EmptyState"; import { displaySuccess } from "components/GlobalSnackbar/utils"; import { Loader } from "components/Loader/Loader"; -import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility"; -import { canEditOrganization } from "modules/management/OrganizationSettingsLayout"; import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout"; import type { FC } from "react"; import { useMutation, useQuery, useQueryClient } from "react-query"; import { Navigate, useNavigate, useParams } from "react-router-dom"; import { OrganizationSettingsPageView } from "./OrganizationSettingsPageView"; -import { OrganizationSummaryPageView } from "./OrganizationSummaryPageView"; +import type { AuthorizationResponse } from "api/typesGenerated"; const OrganizationSettingsPage: FC = () => { const { organization: organizationName } = useParams() as { organization?: string; }; const { organizations } = useOrganizationSettings(); - const feats = useFeatureVisibility(); const navigate = useNavigate(); const queryClient = useQueryClient(); @@ -46,20 +43,6 @@ const OrganizationSettingsPage: FC = () => { return ; } - // Redirect /organizations => /organizations/default-org, or if they cannot edit - // the default org, then the first org they can edit, if any. - if (!organizationName) { - // .find will stop at the first match found; make sure default - // organizations are placed first - const editableOrg = [...organizations] - .sort((a, b) => (b.is_default ? 1 : 0) - (a.is_default ? 1 : 0)) - .find((org) => canEditOrganization(permissions[org.id])); - if (editableOrg) { - return ; - } - return ; - } - if (!organization) { return ; } @@ -69,11 +52,8 @@ const OrganizationSettingsPage: FC = () => { // summary page instead of the settings form. // Similarly, if the feature is not entitled then the user will not be able to // edit the organization. - if ( - !permissions[organization.id]?.editOrganization || - !feats.multiple_organizations - ) { - return ; + if (!permissions[organization.id]?.editOrganization) { + return ; } const error = diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationSummaryPageView.stories.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationSummaryPageView.stories.tsx deleted file mode 100644 index 92567ad99fac4..0000000000000 --- a/site/src/pages/OrganizationSettingsPage/OrganizationSummaryPageView.stories.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import { - MockDefaultOrganization, - MockOrganization, -} from "testHelpers/entities"; -import { OrganizationSummaryPageView } from "./OrganizationSummaryPageView"; - -const meta: Meta = { - title: "pages/OrganizationSummaryPageView", - component: OrganizationSummaryPageView, - args: { - organization: MockOrganization, - }, -}; - -export default meta; -type Story = StoryObj; - -export const DefaultOrg: Story = { - args: { - organization: MockDefaultOrganization, - }, -}; diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationSummaryPageView.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationSummaryPageView.tsx deleted file mode 100644 index c12b3c13a416c..0000000000000 --- a/site/src/pages/OrganizationSettingsPage/OrganizationSummaryPageView.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import type { Organization } from "api/typesGenerated"; -import { Avatar } from "components/Avatar/Avatar"; -import { - PageHeader, - PageHeaderSubtitle, - PageHeaderTitle, -} from "components/PageHeader/PageHeader"; -import { Stack } from "components/Stack/Stack"; -import type { FC } from "react"; - -interface OrganizationSummaryPageViewProps { - organization: Organization; -} - -export const OrganizationSummaryPageView: FC< - OrganizationSummaryPageViewProps -> = ({ organization }) => { - return ( -
- - - - -
- - {organization.display_name || organization.name} - - {organization.description && ( - - {organization.description} - - )} -
-
-
- You are a member of this organization. -
- ); -}; diff --git a/site/src/router.tsx b/site/src/router.tsx index acaf417cecbcd..c2a460844af78 100644 --- a/site/src/router.tsx +++ b/site/src/router.tsx @@ -228,6 +228,10 @@ const AddNewLicensePage = lazy( "./pages/DeploymentSettingsPage/LicensesSettingsPage/AddNewLicensePage" ), ); +const DefaultOrganizationRedirect = lazy( + () => import("./pages/OrganizationSettingsPage/DefaultOrganizationRedirect"), +); + const CreateOrganizationPage = lazy( () => import("./pages/OrganizationSettingsPage/CreateOrganizationPage"), ); @@ -412,7 +416,7 @@ export const router = createBrowserRouter( } /> {/* General settings for the default org can omit the organization name */} - } /> + } /> }> } /> diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index c522457a63c1d..d1ef4f124a989 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -8,6 +8,7 @@ import type * as TypesGen from "api/typesGenerated"; import type { Permissions } from "contexts/auth/permissions"; import type { ProxyLatencyReport } from "contexts/useProxyLatency"; import range from "lodash/range"; +import type { OrganizationPermissions } from "modules/management/organizationPermissions"; import type { FileTree } from "utils/filetree"; import type { TemplateVersionFiles } from "utils/templateVersion"; @@ -2823,6 +2824,30 @@ export const MockPermissions: Permissions = { viewOrganizationIDPSyncSettings: true, }; +export const MockOrganizationPermissions: OrganizationPermissions = { + viewMembers: true, + editMembers: true, + createGroup: true, + viewGroups: true, + editGroups: true, + editOrganization: true, + assignOrgRole: true, + viewProvisioners: true, + viewIdpSyncSettings: true, +}; + +export const MockNoOrganizationPermissions: OrganizationPermissions = { + viewMembers: false, + editMembers: false, + createGroup: false, + viewGroups: false, + editGroups: false, + editOrganization: false, + assignOrgRole: false, + viewProvisioners: false, + viewIdpSyncSettings: false, +}; + export const MockNoPermissions: Permissions = { createTemplates: false, createUser: false, diff --git a/site/src/testHelpers/storybook.tsx b/site/src/testHelpers/storybook.tsx index f1bdc8fadd0f0..0b73445ca2a44 100644 --- a/site/src/testHelpers/storybook.tsx +++ b/site/src/testHelpers/storybook.tsx @@ -17,6 +17,8 @@ import { MockDefaultOrganization, MockDeploymentConfig, MockEntitlements, + MockOrganizationPermissions, + MockPermissions, } from "./entities"; export const withDashboardProvider = ( @@ -153,12 +155,16 @@ export const withGlobalSnackbar = (Story: FC) => ( ); -export const withManagementSettingsProvider = (Story: FC) => { +export const withOrganizationSettingsProvider = (Story: FC) => { return ( Date: Wed, 5 Feb 2025 23:11:56 +0000 Subject: [PATCH 02/21] fix even more-er permissions --- site/src/modules/dashboard/Navbar/Navbar.tsx | 7 ++++--- site/src/modules/management/OrganizationSettingsLayout.tsx | 5 +++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/site/src/modules/dashboard/Navbar/Navbar.tsx b/site/src/modules/dashboard/Navbar/Navbar.tsx index fa249f3a7f004..684b940e2ffa1 100644 --- a/site/src/modules/dashboard/Navbar/Navbar.tsx +++ b/site/src/modules/dashboard/Navbar/Navbar.tsx @@ -16,10 +16,11 @@ export const Navbar: FC = () => { const { user: me, permissions, signOut } = useAuthenticated(); const featureVisibility = useFeatureVisibility(); const canViewAuditLog = - featureVisibility.audit_log && Boolean(permissions.viewAnyAuditLog); - const canViewDeployment = Boolean(permissions.viewDeploymentValues); + featureVisibility.audit_log && permissions.viewAnyAuditLog; + const canViewDeployment = permissions.viewDeploymentValues; const canViewOrganizations = - Boolean(permissions.editAnyOrganization) && showOrganizations; + (permissions.viewDeploymentValues || permissions.editAnyOrganization) && + showOrganizations; const proxyContextValue = useProxy(); const canViewHealth = canViewDeployment; diff --git a/site/src/modules/management/OrganizationSettingsLayout.tsx b/site/src/modules/management/OrganizationSettingsLayout.tsx index f7f7da3e674c9..da7c2cde2ee33 100644 --- a/site/src/modules/management/OrganizationSettingsLayout.tsx +++ b/site/src/modules/management/OrganizationSettingsLayout.tsx @@ -65,7 +65,8 @@ const OrganizationSettingsLayout: FC = () => { organization?: string; }; - const canViewOrganizationSettingsPage = permissions.editAnyOrganization; + const canViewOrganizationSettings = + permissions.viewDeploymentValues || permissions.editAnyOrganization; const organization = orgName ? organizations.find((org) => org.name === orgName) @@ -98,7 +99,7 @@ const OrganizationSettingsLayout: FC = () => { } return ( - + Date: Fri, 7 Feb 2025 23:04:16 +0000 Subject: [PATCH 03/21] oh yeah --- site/src/api/queries/organizations.ts | 12 ++ site/src/contexts/auth/AuthProvider.tsx | 2 + site/src/contexts/auth/permissions.tsx | 8 - .../modules/dashboard/DashboardProvider.tsx | 22 +- site/src/modules/dashboard/Navbar/Navbar.tsx | 6 +- .../management/OrganizationSettingsLayout.tsx | 28 +-- .../management/organizationPermissions.tsx | 198 ++++++++++++++---- .../CustomRolesPage/CreateEditRolePage.tsx | 2 +- .../CustomRolesPage/CustomRolesPage.tsx | 2 +- .../DefaultOrganizationRedirect.tsx | 16 +- .../WorkspacePage/WorkspacePage.test.tsx | 1 + site/src/testHelpers/entities.ts | 10 +- site/src/testHelpers/storybook.tsx | 5 +- 13 files changed, 207 insertions(+), 105 deletions(-) diff --git a/site/src/api/queries/organizations.ts b/site/src/api/queries/organizations.ts index b575a3125dec3..45e44d8d18ab0 100644 --- a/site/src/api/queries/organizations.ts +++ b/site/src/api/queries/organizations.ts @@ -11,6 +11,8 @@ import { organizationPermissionChecks, type OrganizationPermissions, type OrganizationPermissionName, + anyOrganizationPermissionChecks, + type AnyOrganizationPermissions, } from "modules/management/organizationPermissions"; export const createOrganization = (queryClient: QueryClient) => { @@ -251,6 +253,16 @@ export const organizationsPermissions = ( }; }; +export const anyOrganizationPermissions = () => { + return { + queryKey: ["authorization", "anyOrganization"], + queryFn: () => + API.checkAuthorization({ + checks: anyOrganizationPermissionChecks, + }) as Promise, + }; +}; + export const getOrganizationIdpSyncClaimFieldValuesKey = ( organization: string, field: string, diff --git a/site/src/contexts/auth/AuthProvider.tsx b/site/src/contexts/auth/AuthProvider.tsx index ad475bddcbfb7..fd0820e8d91c9 100644 --- a/site/src/contexts/auth/AuthProvider.tsx +++ b/site/src/contexts/auth/AuthProvider.tsx @@ -101,6 +101,8 @@ export const AuthProvider: FC = ({ children }) => { [updateProfileMutation], ); + console.log(permissionsQuery.data); + return ( ( @@ -33,12 +38,16 @@ export const DashboardProvider: FC = ({ children }) => { const experimentsQuery = useQuery(experiments(metadata.experiments)); const appearanceQuery = useQuery(appearance(metadata.appearance)); const organizationsQuery = useQuery(organizations()); + const anyOrganizationPermissionsQuery = useQuery( + anyOrganizationPermissions(), + ); const error = entitlementsQuery.error || appearanceQuery.error || experimentsQuery.error || - organizationsQuery.error; + organizationsQuery.error || + anyOrganizationPermissionsQuery.error; if (error) { return ; @@ -48,7 +57,8 @@ export const DashboardProvider: FC = ({ children }) => { !entitlementsQuery.data || !appearanceQuery.data || !experimentsQuery.data || - !organizationsQuery.data; + !organizationsQuery.data || + !anyOrganizationPermissionsQuery.data; if (isLoading) { return ; @@ -58,6 +68,7 @@ export const DashboardProvider: FC = ({ children }) => { const organizationsEnabled = selectFeatureVisibility( entitlementsQuery.data, ).multiple_organizations; + const showOrganizations = hasMultipleOrganizations || organizationsEnabled; return ( = ({ children }) => { experiments: experimentsQuery.data, appearance: appearanceQuery.data, organizations: organizationsQuery.data, - showOrganizations: hasMultipleOrganizations || organizationsEnabled, + showOrganizations, + canViewOrganizationSettings: + showOrganizations && + canViewAnyOrganization(anyOrganizationPermissionsQuery.data), }} > {children} diff --git a/site/src/modules/dashboard/Navbar/Navbar.tsx b/site/src/modules/dashboard/Navbar/Navbar.tsx index 684b940e2ffa1..f80887e1f1aec 100644 --- a/site/src/modules/dashboard/Navbar/Navbar.tsx +++ b/site/src/modules/dashboard/Navbar/Navbar.tsx @@ -12,15 +12,13 @@ export const Navbar: FC = () => { const { metadata } = useEmbeddedMetadata(); const buildInfoQuery = useQuery(buildInfo(metadata["build-info"])); - const { appearance, showOrganizations } = useDashboard(); + const { appearance, canViewOrganizationSettings } = useDashboard(); const { user: me, permissions, signOut } = useAuthenticated(); const featureVisibility = useFeatureVisibility(); const canViewAuditLog = featureVisibility.audit_log && permissions.viewAnyAuditLog; const canViewDeployment = permissions.viewDeploymentValues; - const canViewOrganizations = - (permissions.viewDeploymentValues || permissions.editAnyOrganization) && - showOrganizations; + const canViewOrganizations = canViewOrganizationSettings; const proxyContextValue = useProxy(); const canViewHealth = canViewDeployment; diff --git a/site/src/modules/management/OrganizationSettingsLayout.tsx b/site/src/modules/management/OrganizationSettingsLayout.tsx index da7c2cde2ee33..6a277c880ec2f 100644 --- a/site/src/modules/management/OrganizationSettingsLayout.tsx +++ b/site/src/modules/management/OrganizationSettingsLayout.tsx @@ -17,7 +17,10 @@ import NotFoundPage from "pages/404Page/404Page"; import { type FC, Suspense, createContext, useContext } from "react"; import { useQuery } from "react-query"; import { Outlet, useParams } from "react-router-dom"; -import type { OrganizationPermissions } from "./organizationPermissions"; +import { + canViewOrganization, + type OrganizationPermissions, +} from "./organizationPermissions"; export const OrganizationSettingsContext = createContext< OrganizationSettingsValue | undefined @@ -41,33 +44,12 @@ export const useOrganizationSettings = (): OrganizationSettingsValue => { return context; }; -/** - * Checks if the user can view or edit members or groups for the organization - * that produced the given OrganizationPermissions. - */ -const canViewOrganization = ( - permissions: OrganizationPermissions | undefined, -): permissions is OrganizationPermissions => { - return ( - permissions !== undefined && - (permissions.editOrganization || - permissions.editMembers || - permissions.viewMembers || - permissions.editGroups || - permissions.viewGroups) - ); -}; - const OrganizationSettingsLayout: FC = () => { - const { permissions } = useAuthenticated(); - const { organizations } = useDashboard(); + const { organizations, canViewOrganizationSettings } = useDashboard(); const { organization: orgName } = useParams() as { organization?: string; }; - const canViewOrganizationSettings = - permissions.viewDeploymentValues || permissions.editAnyOrganization; - const organization = orgName ? organizations.find((org) => org.name === orgName) : undefined; diff --git a/site/src/modules/management/organizationPermissions.tsx b/site/src/modules/management/organizationPermissions.tsx index d004af990b3ce..cc5860f470ab1 100644 --- a/site/src/modules/management/organizationPermissions.tsx +++ b/site/src/modules/management/organizationPermissions.tsx @@ -1,3 +1,5 @@ +import type { AuthorizationCheck } from "api/typesGenerated"; + export type OrganizationPermissions = { [k in OrganizationPermissionName]: boolean; }; @@ -6,68 +8,178 @@ export type OrganizationPermissionName = keyof ReturnType< typeof organizationPermissionChecks >; -export const organizationPermissionChecks = (organizationId: string) => ({ - viewMembers: { - object: { - resource_type: "organization_member", - organization_id: organizationId, +export const organizationPermissionChecks = (organizationId: string) => + ({ + viewMembers: { + object: { + resource_type: "organization_member", + organization_id: organizationId, + }, + action: "read", }, - action: "read", - }, - editMembers: { - object: { - resource_type: "organization_member", - organization_id: organizationId, + editMembers: { + object: { + resource_type: "organization_member", + organization_id: organizationId, + }, + action: "update", }, - action: "update", - }, - createGroup: { - object: { - resource_type: "group", - organization_id: organizationId, + createGroup: { + object: { + resource_type: "group", + organization_id: organizationId, + }, + action: "create", }, - action: "create", - }, - viewGroups: { + viewGroups: { + object: { + resource_type: "group", + organization_id: organizationId, + }, + action: "read", + }, + editGroups: { + object: { + resource_type: "group", + organization_id: organizationId, + }, + action: "update", + }, + editOrganization: { + object: { + resource_type: "organization", + organization_id: organizationId, + }, + action: "update", + }, + assignOrgRoles: { + object: { + resource_type: "assign_org_role", + organization_id: organizationId, + }, + action: "assign", + }, + viewOrgRoles: { + object: { + resource_type: "assign_org_role", + organization_id: organizationId, + }, + action: "read", + }, + createOrgRoles: { + object: { + resource_type: "assign_org_role", + organization_id: organizationId, + }, + action: "create", + }, + viewProvisioners: { + object: { + resource_type: "provisioner_daemon", + organization_id: organizationId, + }, + action: "read", + }, + viewIdpSyncSettings: { + object: { + resource_type: "idpsync_settings", + organization_id: organizationId, + }, + action: "read", + }, + }) as const satisfies Record; + +/** + * Checks if the user can view or edit members or groups for the organization + * that produced the given OrganizationPermissions. + */ +export const canViewOrganization = ( + permissions: OrganizationPermissions | undefined, +): permissions is OrganizationPermissions => { + return ( + permissions !== undefined && + (permissions.viewMembers || + permissions.viewGroups || + permissions.viewOrgRoles || + permissions.viewProvisioners || + permissions.viewIdpSyncSettings) + ); +}; + +/** + * Return true if the user can edit the organization settings or its members. + */ +export const canEditOrganization = ( + permissions: OrganizationPermissions | undefined, +): permissions is OrganizationPermissions => { + return ( + permissions !== undefined && + (permissions.editMembers || + permissions.editGroups || + permissions.editOrganization || + permissions.assignOrgRoles || + permissions.createOrgRoles) + ); +}; + +export type AnyOrganizationPermissions = { + [k in AnyOrganizationPermissionName]: boolean; +}; + +export type AnyOrganizationPermissionName = + keyof typeof anyOrganizationPermissionChecks; + +export const anyOrganizationPermissionChecks = { + viewAnyMembers: { object: { - resource_type: "group", - organization_id: organizationId, + resource_type: "organization_member", + any_org: true, }, action: "read", }, - editGroups: { + editAnyGroups: { object: { resource_type: "group", - organization_id: organizationId, - }, - action: "update", - }, - editOrganization: { - object: { - resource_type: "organization", - organization_id: organizationId, + any_org: true, }, action: "update", }, - assignOrgRole: { + assignAnyRoles: { object: { resource_type: "assign_org_role", - organization_id: organizationId, + any_org: true, }, - action: "create", + action: "assign", }, - viewProvisioners: { + editAnyIdpSyncSettings: { object: { - resource_type: "provisioner_daemon", - organization_id: organizationId, + resource_type: "idpsync_settings", + any_org: true, }, - action: "read", + action: "update", }, - viewIdpSyncSettings: { + editAnySettings: { object: { - resource_type: "idpsync_settings", - organization_id: organizationId, + resource_type: "organization", + any_org: true, }, - action: "read", + action: "update", }, -}); +} as const satisfies Record; + +/** + * Checks if the user can view or edit members or groups for the organization + * that produced the given OrganizationPermissions. + */ +export const canViewAnyOrganization = ( + permissions: AnyOrganizationPermissions | undefined, +): permissions is AnyOrganizationPermissions => { + return ( + permissions !== undefined && + (permissions.viewAnyMembers || + permissions.editAnyGroups || + permissions.assignAnyRoles || + permissions.editAnyIdpSyncSettings || + permissions.editAnySettings) + ); +}; diff --git a/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CreateEditRolePage.tsx b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CreateEditRolePage.tsx index aa678f5a3e277..d073ed698ec66 100644 --- a/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CreateEditRolePage.tsx +++ b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CreateEditRolePage.tsx @@ -76,7 +76,7 @@ export const CreateEditRolePage: FC = () => { : createOrganizationRoleMutation.isLoading } organizationName={organizationName} - canAssignOrgRole={organizationPermissions.assignOrgRole} + canAssignOrgRole={organizationPermissions.assignOrgRoles} /> ); diff --git a/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPage.tsx b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPage.tsx index 26fa8e45d8eb6..096675adb1da5 100644 --- a/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPage.tsx @@ -73,7 +73,7 @@ export const CustomRolesPage: FC = () => { builtInRoles={builtInRoles} customRoles={customRoles} onDeleteRole={setRoleToDelete} - canAssignOrgRole={organizationPermissions.assignOrgRole} + canAssignOrgRole={organizationPermissions.assignOrgRoles} isCustomRolesEnabled={isCustomRolesEnabled} /> diff --git a/site/src/pages/OrganizationSettingsPage/DefaultOrganizationRedirect.tsx b/site/src/pages/OrganizationSettingsPage/DefaultOrganizationRedirect.tsx index 719ed357d3d45..f071b2c8bb086 100644 --- a/site/src/pages/OrganizationSettingsPage/DefaultOrganizationRedirect.tsx +++ b/site/src/pages/OrganizationSettingsPage/DefaultOrganizationRedirect.tsx @@ -2,21 +2,7 @@ import type { FC } from "react"; import { EmptyState } from "components/EmptyState/EmptyState"; import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout"; import { Navigate } from "react-router-dom"; -import type { OrganizationPermissions } from "modules/management/organizationPermissions"; - -/** - * Return true if the user can edit the organization settings or its members. - */ -const canEditOrganization = ( - permissions: OrganizationPermissions | undefined, -): permissions is OrganizationPermissions => { - return ( - permissions !== undefined && - (permissions.editOrganization || - permissions.editMembers || - permissions.editGroups) - ); -}; +import { canEditOrganization } from "modules/management/organizationPermissions"; const DefaultOrganizationRedirect: FC = () => { const { organizations, permissionsByOrganizationId } = diff --git a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx index 1c644f981d7a6..50f47a4721320 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx @@ -565,6 +565,7 @@ describe("WorkspacePage", () => { experiments: [], organizations: [MockOrganization], showOrganizations: true, + canViewOrganizationSettings: true, }} > {children} diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index b9dea64d9ee75..a8df56dc5affd 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -2823,7 +2823,6 @@ export const MockPermissions: Permissions = { readWorkspaceProxies: true, editWorkspaceProxies: true, createOrganization: true, - editAnyOrganization: true, viewAnyGroup: true, createGroup: true, viewAllLicenses: true, @@ -2838,7 +2837,9 @@ export const MockOrganizationPermissions: OrganizationPermissions = { viewGroups: true, editGroups: true, editOrganization: true, - assignOrgRole: true, + viewOrgRoles: true, + createOrgRoles: true, + assignOrgRoles: true, viewProvisioners: true, viewIdpSyncSettings: true, }; @@ -2850,7 +2851,9 @@ export const MockNoOrganizationPermissions: OrganizationPermissions = { viewGroups: false, editGroups: false, editOrganization: false, - assignOrgRole: false, + viewOrgRoles: false, + createOrgRoles: false, + assignOrgRoles: false, viewProvisioners: false, viewIdpSyncSettings: false, }; @@ -2871,7 +2874,6 @@ export const MockNoPermissions: Permissions = { readWorkspaceProxies: false, editWorkspaceProxies: false, createOrganization: false, - editAnyOrganization: false, viewAnyGroup: false, createGroup: false, viewAllLicenses: false, diff --git a/site/src/testHelpers/storybook.tsx b/site/src/testHelpers/storybook.tsx index 0b73445ca2a44..a483696a8cf46 100644 --- a/site/src/testHelpers/storybook.tsx +++ b/site/src/testHelpers/storybook.tsx @@ -18,7 +18,6 @@ import { MockDeploymentConfig, MockEntitlements, MockOrganizationPermissions, - MockPermissions, } from "./entities"; export const withDashboardProvider = ( @@ -30,6 +29,7 @@ export const withDashboardProvider = ( experiments = [], showOrganizations = false, organizations = [MockDefaultOrganization], + canViewOrganizationSettings = false, } = parameters; const entitlements: Entitlements = { @@ -50,9 +50,10 @@ export const withDashboardProvider = ( value={{ entitlements, experiments, + appearance: MockAppearanceConfig, organizations, showOrganizations, - appearance: MockAppearanceConfig, + canViewOrganizationSettings, }} > From 379bacd1556803a71f286121e16b3b7d63612812 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Fri, 7 Feb 2025 23:05:33 +0000 Subject: [PATCH 04/21] oh yeah --- .../management/OrganizationSettingsLayout.tsx | 9 +++++++-- .../management/OrganizationSidebarView.tsx | 8 +++++--- .../DefaultOrganizationRedirect.tsx | 20 ++++++++++++------- site/src/testHelpers/storybook.tsx | 2 +- 4 files changed, 26 insertions(+), 13 deletions(-) diff --git a/site/src/modules/management/OrganizationSettingsLayout.tsx b/site/src/modules/management/OrganizationSettingsLayout.tsx index 6a277c880ec2f..2c310be2a7d6d 100644 --- a/site/src/modules/management/OrganizationSettingsLayout.tsx +++ b/site/src/modules/management/OrganizationSettingsLayout.tsx @@ -28,7 +28,10 @@ export const OrganizationSettingsContext = createContext< type OrganizationSettingsValue = Readonly<{ organizations: readonly Organization[]; - permissionsByOrganizationId: Record; + organizationPermissionsByOrganizationId: Record< + string, + OrganizationPermissions + >; organization?: Organization; organizationPermissions?: OrganizationPermissions; }>; @@ -80,12 +83,14 @@ const OrganizationSettingsLayout: FC = () => { return ; } + console.log(orgPermissionsQuery.data); + return ( = ({ ))}
) : ( - - No more organizations - + !permissions.createOrganization && ( + + No more organizations + + ) )} {permissions.createOrganization && ( <> diff --git a/site/src/pages/OrganizationSettingsPage/DefaultOrganizationRedirect.tsx b/site/src/pages/OrganizationSettingsPage/DefaultOrganizationRedirect.tsx index f071b2c8bb086..d65a105b991e1 100644 --- a/site/src/pages/OrganizationSettingsPage/DefaultOrganizationRedirect.tsx +++ b/site/src/pages/OrganizationSettingsPage/DefaultOrganizationRedirect.tsx @@ -5,19 +5,25 @@ import { Navigate } from "react-router-dom"; import { canEditOrganization } from "modules/management/organizationPermissions"; const DefaultOrganizationRedirect: FC = () => { - const { organizations, permissionsByOrganizationId } = - useOrganizationSettings(); + const { + organizations, + organizationPermissionsByOrganizationId: organizationPermissions, + } = useOrganizationSettings(); - // Redirect /organizations => /organizations/default-org, or if they cannot edit - // the default org, then the first org they can edit, if any. - // .find will stop at the first match found; make sure default - // organizations are placed first + // Redirect /organizations => /organizations/some-organization-name + // If they can edit the default org, we should redirect to the default. + // If they cannot edit the default, we should redirect to the first org that + // they can edit. const editableOrg = [...organizations] .sort((a, b) => (b.is_default ? 1 : 0) - (a.is_default ? 1 : 0)) - .find((org) => canEditOrganization(permissionsByOrganizationId[org.id])); + .find((org) => canEditOrganization(organizationPermissions[org.id])); if (editableOrg) { return ; } + // If they cannot edit any org, just redirect to an org they can read. + if (organizations.length > 0) { + return ; + } return ; }; diff --git a/site/src/testHelpers/storybook.tsx b/site/src/testHelpers/storybook.tsx index a483696a8cf46..2b81bf16cd40f 100644 --- a/site/src/testHelpers/storybook.tsx +++ b/site/src/testHelpers/storybook.tsx @@ -161,7 +161,7 @@ export const withOrganizationSettingsProvider = (Story: FC) => { Date: Fri, 7 Feb 2025 23:06:21 +0000 Subject: [PATCH 05/21] =?UTF-8?q?=F0=9F=94=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/src/api/queries/organizations.ts | 10 +++++----- site/src/contexts/auth/AuthProvider.tsx | 2 -- site/src/modules/dashboard/DashboardProvider.tsx | 2 +- .../modules/management/OrganizationSettingsLayout.tsx | 4 +--- site/src/modules/management/OrganizationSidebar.tsx | 2 +- site/src/pages/GroupsPage/GroupsPage.tsx | 2 +- .../DefaultOrganizationRedirect.tsx | 4 ++-- .../OrganizationMembersPage.tsx | 2 +- .../OrganizationSettingsPage.tsx | 2 +- 9 files changed, 13 insertions(+), 17 deletions(-) diff --git a/site/src/api/queries/organizations.ts b/site/src/api/queries/organizations.ts index 45e44d8d18ab0..13914006c370a 100644 --- a/site/src/api/queries/organizations.ts +++ b/site/src/api/queries/organizations.ts @@ -5,15 +5,15 @@ import type { RoleSyncSettings, UpdateOrganizationRequest, } from "api/typesGenerated"; -import type { QueryClient } from "react-query"; -import { meKey } from "./users"; import { - organizationPermissionChecks, - type OrganizationPermissions, + type AnyOrganizationPermissions, type OrganizationPermissionName, + type OrganizationPermissions, anyOrganizationPermissionChecks, - type AnyOrganizationPermissions, + organizationPermissionChecks, } from "modules/management/organizationPermissions"; +import type { QueryClient } from "react-query"; +import { meKey } from "./users"; export const createOrganization = (queryClient: QueryClient) => { return { diff --git a/site/src/contexts/auth/AuthProvider.tsx b/site/src/contexts/auth/AuthProvider.tsx index fd0820e8d91c9..ad475bddcbfb7 100644 --- a/site/src/contexts/auth/AuthProvider.tsx +++ b/site/src/contexts/auth/AuthProvider.tsx @@ -101,8 +101,6 @@ export const AuthProvider: FC = ({ children }) => { [updateProfileMutation], ); - console.log(permissionsQuery.data); - return ( { return ; } - console.log(orgPermissionsQuery.data); - return ( { const { template_rbac: groupsEnabled } = useFeatureVisibility(); diff --git a/site/src/pages/OrganizationSettingsPage/DefaultOrganizationRedirect.tsx b/site/src/pages/OrganizationSettingsPage/DefaultOrganizationRedirect.tsx index d65a105b991e1..d69fc50aa1491 100644 --- a/site/src/pages/OrganizationSettingsPage/DefaultOrganizationRedirect.tsx +++ b/site/src/pages/OrganizationSettingsPage/DefaultOrganizationRedirect.tsx @@ -1,8 +1,8 @@ -import type { FC } from "react"; import { EmptyState } from "components/EmptyState/EmptyState"; import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout"; -import { Navigate } from "react-router-dom"; import { canEditOrganization } from "modules/management/organizationPermissions"; +import type { FC } from "react"; +import { Navigate } from "react-router-dom"; const DefaultOrganizationRedirect: FC = () => { const { diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.tsx index 29ed27eb04942..7d3aa534ca4a0 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.tsx @@ -18,9 +18,9 @@ import { useOrganizationSettings } from "modules/management/OrganizationSettings import { type FC, useState } from "react"; import { Helmet } from "react-helmet-async"; import { useMutation, useQuery, useQueryClient } from "react-query"; +import { useParams } from "react-router-dom"; import { pageTitle } from "utils/page"; import { OrganizationMembersPageView } from "./OrganizationMembersPageView"; -import { useParams } from "react-router-dom"; const OrganizationMembersPage: FC = () => { const queryClient = useQueryClient(); diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx index 5f29b71e63ad4..06f286f0298ed 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx @@ -3,6 +3,7 @@ import { organizationsPermissions, updateOrganization, } from "api/queries/organizations"; +import type { AuthorizationResponse } from "api/typesGenerated"; import { ErrorAlert } from "components/Alert/ErrorAlert"; import { EmptyState } from "components/EmptyState/EmptyState"; import { displaySuccess } from "components/GlobalSnackbar/utils"; @@ -12,7 +13,6 @@ import type { FC } from "react"; import { useMutation, useQuery, useQueryClient } from "react-query"; import { Navigate, useNavigate, useParams } from "react-router-dom"; import { OrganizationSettingsPageView } from "./OrganizationSettingsPageView"; -import type { AuthorizationResponse } from "api/typesGenerated"; const OrganizationSettingsPage: FC = () => { const { organization: organizationName } = useParams() as { From b841aacae17c8c4f8a2bf53d3b300b7d74637e9d Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Sat, 8 Feb 2025 00:12:14 +0000 Subject: [PATCH 06/21] yes --- site/src/contexts/auth/RequirePermission.tsx | 3 +- .../management/OrganizationSettingsLayout.tsx | 98 +++++++++---------- .../management/OrganizationSidebarView.tsx | 26 +++-- .../management/organizationPermissions.tsx | 18 +++- .../OrganizationMembersPage.tsx | 7 +- ...test.tsx => OrganizationRedirect.test.tsx} | 58 ++++++----- ...nRedirect.tsx => OrganizationRedirect.tsx} | 4 +- .../OrganizationSettingsPage.tsx | 32 +----- .../OrganizationSettingsPageView.tsx | 1 - site/src/router.tsx | 6 +- site/src/testHelpers/entities.ts | 6 +- 11 files changed, 131 insertions(+), 128 deletions(-) rename site/src/pages/OrganizationSettingsPage/{OrganizationSettingsPage.test.tsx => OrganizationRedirect.test.tsx} (58%) rename site/src/pages/OrganizationSettingsPage/{DefaultOrganizationRedirect.tsx => OrganizationRedirect.tsx} (92%) diff --git a/site/src/contexts/auth/RequirePermission.tsx b/site/src/contexts/auth/RequirePermission.tsx index 50dbd0232ab88..d1c68ae50b919 100644 --- a/site/src/contexts/auth/RequirePermission.tsx +++ b/site/src/contexts/auth/RequirePermission.tsx @@ -14,7 +14,8 @@ export const RequirePermission: FC = ({ isFeatureVisible, }) => { if (!isFeatureVisible) { - return ; + // return ; + return

oh fuck

; } return <>{children}; diff --git a/site/src/modules/management/OrganizationSettingsLayout.tsx b/site/src/modules/management/OrganizationSettingsLayout.tsx index 6c7114037af76..fd7a1f062a9d0 100644 --- a/site/src/modules/management/OrganizationSettingsLayout.tsx +++ b/site/src/modules/management/OrganizationSettingsLayout.tsx @@ -61,12 +61,12 @@ const OrganizationSettingsLayout: FC = () => { organizationsPermissions(organizations?.map((o) => o.id)), ); - if (orgPermissionsQuery.isLoading) { - return ; + if (orgPermissionsQuery.isError) { + return ; } if (!orgPermissionsQuery.data) { - return ; + return ; } const viewableOrganizations = organizations.filter((org) => @@ -84,54 +84,52 @@ const OrganizationSettingsLayout: FC = () => { } return ( - - -
- - - - Admin Settings - - - - - Organizations - - - {organization && ( - <> - - - - - {organization.display_name} - - - - )} - - -
-
- }> - - -
+ +
+ + + + Admin Settings + + + + + Organizations + + + {organization && ( + <> + + + + + {organization.display_name} + + + + )} + + +
+
+ }> + +
- - +
+
); }; diff --git a/site/src/modules/management/OrganizationSidebarView.tsx b/site/src/modules/management/OrganizationSidebarView.tsx index 31dab067aebc0..e0f6ffb1f504e 100644 --- a/site/src/modules/management/OrganizationSidebarView.tsx +++ b/site/src/modules/management/OrganizationSidebarView.tsx @@ -47,6 +47,8 @@ export const OrganizationSidebarView: FC = ({ ] : organizations; + console.log(organizations); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); const navigate = useNavigate(); @@ -60,16 +62,20 @@ export const OrganizationSidebarView: FC = ({ className="w-60 justify-between p-2 h-11" >
- {activeOrganization && ( - + {activeOrganization ? ( + <> + + + {activeOrganization.display_name || activeOrganization.name} + + + ) : ( + No organization selected )} - - {activeOrganization?.display_name || activeOrganization?.name} -
@@ -78,7 +84,7 @@ export const OrganizationSidebarView: FC = ({ - {sortedOrganizations.length > 1 ? ( + {sortedOrganizations.length > (activeOrganization ? 1 : 0) ? (
{sortedOrganizations.map((organization) => ( }, action: "update", }, - editOrganization: { + editSettings: { object: { resource_type: "organization", organization_id: organizationId, @@ -87,6 +87,13 @@ export const organizationPermissionChecks = (organizationId: string) => }, action: "read", }, + editIdpSyncSettings: { + object: { + resource_type: "idpsync_settings", + organization_id: organizationId, + }, + action: "update", + }, }) as const satisfies Record; /** @@ -116,8 +123,9 @@ export const canEditOrganization = ( permissions !== undefined && (permissions.editMembers || permissions.editGroups || - permissions.editOrganization || + permissions.editSettings || permissions.assignOrgRoles || + permissions.editIdpSyncSettings || permissions.createOrgRoles) ); }; @@ -151,12 +159,12 @@ export const anyOrganizationPermissionChecks = { }, action: "assign", }, - editAnyIdpSyncSettings: { + viewAnyIdpSyncSettings: { object: { resource_type: "idpsync_settings", any_org: true, }, - action: "update", + action: "read", }, editAnySettings: { object: { @@ -179,7 +187,7 @@ export const canViewAnyOrganization = ( (permissions.viewAnyMembers || permissions.editAnyGroups || permissions.assignAnyRoles || - permissions.editAnyIdpSyncSettings || + permissions.viewAnyIdpSyncSettings || permissions.editAnySettings) ); }; diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.tsx index 7d3aa534ca4a0..196bbf410e73d 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.tsx @@ -21,6 +21,7 @@ import { useMutation, useQuery, useQueryClient } from "react-query"; import { useParams } from "react-router-dom"; import { pageTitle } from "utils/page"; import { OrganizationMembersPageView } from "./OrganizationMembersPageView"; +import { EmptyState } from "components/EmptyState/EmptyState"; const OrganizationMembersPage: FC = () => { const queryClient = useQueryClient(); @@ -54,11 +55,11 @@ const OrganizationMembersPage: FC = () => { const [memberToDelete, setMemberToDelete] = useState(); - if (!organizationPermissions) { - return ; + if (!organization || !organizationPermissions) { + return ; } - const helmet = organization && ( + const helmet = ( {pageTitle("Members", organization.display_name || organization.name)} diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.test.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationRedirect.test.tsx similarity index 58% rename from site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.test.tsx rename to site/src/pages/OrganizationSettingsPage/OrganizationRedirect.test.tsx index 2978702ab9651..96e0110d21a80 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.test.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationRedirect.test.tsx @@ -10,19 +10,29 @@ import { waitForLoaderToBeRemoved, } from "testHelpers/renderHelpers"; import { server } from "testHelpers/server"; -import OrganizationSettingsPage from "./OrganizationSettingsPage"; +import OrganizationRedirect from "./OrganizationRedirect"; jest.spyOn(console, "error").mockImplementation(() => {}); const renderPage = async () => { - renderWithOrganizationSettingsLayout(<OrganizationSettingsPage />, { - route: "/organizations", - path: "/organizations/:organization?", - }); + const { router } = renderWithOrganizationSettingsLayout( + <OrganizationRedirect />, + { + route: "/organizations", + path: "/organizations", + extraRoutes: [ + { + path: "/organizations/:organization", + element: <h1>Organization Settings</h1>, + }, + ], + }, + ); await waitForLoaderToBeRemoved(); + return router; }; -describe("OrganizationSettingsPage", () => { +describe("OrganizationRedirect", () => { it("has no editable organizations", async () => { server.use( http.get("/api/v2/entitlements", () => { @@ -32,9 +42,7 @@ describe("OrganizationSettingsPage", () => { return HttpResponse.json([MockDefaultOrganization, MockOrganization2]); }), http.post("/api/v2/authcheck", async () => { - return HttpResponse.json({ - viewDeploymentValues: true, - }); + return HttpResponse.json({}); }), ); await renderPage(); @@ -52,16 +60,19 @@ describe("OrganizationSettingsPage", () => { }), http.post("/api/v2/authcheck", async () => { return HttpResponse.json({ - [`${MockDefaultOrganization.id}.editOrganization`]: true, - [`${MockOrganization2.id}.editOrganization`]: true, - viewDeploymentValues: true, + viewAnyMembers: true, + [`${MockDefaultOrganization.id}.viewMembers`]: true, + [`${MockDefaultOrganization.id}.editMembers`]: true, + [`${MockOrganization2.id}.viewMembers`]: true, + [`${MockOrganization2.id}.editMembers`]: true, }); }), ); - await renderPage(); - const form = screen.getByTestId("org-settings-form"); - expect(within(form).getByRole("textbox", { name: "Slug" })).toHaveValue( - MockDefaultOrganization.name, + const router = await renderPage(); + const form = screen.getByText("Organization Settings"); + expect(form).toBeInTheDocument(); + expect(router.state.location.pathname).toBe( + `/organizations/${MockDefaultOrganization.name}`, ); }); @@ -75,15 +86,18 @@ describe("OrganizationSettingsPage", () => { }), http.post("/api/v2/authcheck", async () => { return HttpResponse.json({ - [`${MockOrganization2.id}.editOrganization`]: true, - viewDeploymentValues: true, + viewAnyMembers: true, + [`${MockDefaultOrganization.id}.viewMembers`]: true, + [`${MockOrganization2.id}.viewMembers`]: true, + [`${MockOrganization2.id}.editMembers`]: true, }); }), ); - await renderPage(); - const form = screen.getByTestId("org-settings-form"); - expect(within(form).getByRole("textbox", { name: "Slug" })).toHaveValue( - MockOrganization2.name, + const router = await renderPage(); + const form = screen.getByText("Organization Settings"); + expect(form).toBeInTheDocument(); + expect(router.state.location.pathname).toBe( + `/organizations/${MockOrganization2.name}`, ); }); }); diff --git a/site/src/pages/OrganizationSettingsPage/DefaultOrganizationRedirect.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationRedirect.tsx similarity index 92% rename from site/src/pages/OrganizationSettingsPage/DefaultOrganizationRedirect.tsx rename to site/src/pages/OrganizationSettingsPage/OrganizationRedirect.tsx index d69fc50aa1491..b862ad41dc883 100644 --- a/site/src/pages/OrganizationSettingsPage/DefaultOrganizationRedirect.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationRedirect.tsx @@ -4,7 +4,7 @@ import { canEditOrganization } from "modules/management/organizationPermissions" import type { FC } from "react"; import { Navigate } from "react-router-dom"; -const DefaultOrganizationRedirect: FC = () => { +const OrganizationRedirect: FC = () => { const { organizations, organizationPermissionsByOrganizationId: organizationPermissions, @@ -27,4 +27,4 @@ const DefaultOrganizationRedirect: FC = () => { return <EmptyState message="No organizations found" />; }; -export default DefaultOrganizationRedirect; +export default OrganizationRedirect; diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx index 06f286f0298ed..8119bb94b7e0a 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx @@ -15,13 +15,10 @@ import { Navigate, useNavigate, useParams } from "react-router-dom"; import { OrganizationSettingsPageView } from "./OrganizationSettingsPageView"; const OrganizationSettingsPage: FC = () => { - const { organization: organizationName } = useParams() as { - organization?: string; - }; - const { organizations } = useOrganizationSettings(); - const navigate = useNavigate(); const queryClient = useQueryClient(); + const { organization, organizationPermissions } = useOrganizationSettings(); + const updateOrganizationMutation = useMutation( updateOrganization(queryClient), ); @@ -29,33 +26,10 @@ const OrganizationSettingsPage: FC = () => { deleteOrganization(queryClient), ); - const organization = organizations?.find((o) => o.name === organizationName); - const permissionsQuery = useQuery( - organizationsPermissions(organizations?.map((o) => o.id)), - ); - - if (permissionsQuery.isLoading) { - return <Loader />; - } - - const permissions = permissionsQuery.data; - if (permissionsQuery.error || !permissions) { - return <ErrorAlert error={permissionsQuery.error} />; - } - - if (!organization) { + if (!organization || !organizationPermissions?.editSettings) { return <EmptyState message="Organization not found" />; } - // The user may not be able to edit this org but they can still see it because - // they can edit members, etc. In this case they will be shown a read-only - // summary page instead of the settings form. - // Similarly, if the feature is not entitled then the user will not be able to - // edit the organization. - if (!permissions[organization.id]?.editOrganization) { - return <Navigate to=".." replace />; - } - const error = updateOrganizationMutation.error ?? deleteOrganizationMutation.error; diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPageView.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPageView.tsx index 7dcf23bf4a4a6..6ce41354973fa 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPageView.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPageView.tsx @@ -75,7 +75,6 @@ export const OrganizationSettingsPageView: FC< )} <HorizontalForm - data-testid="org-settings-form" onSubmit={form.handleSubmit} aria-label="Organization settings form" > diff --git a/site/src/router.tsx b/site/src/router.tsx index c2a460844af78..60b461fdba2ec 100644 --- a/site/src/router.tsx +++ b/site/src/router.tsx @@ -228,8 +228,8 @@ const AddNewLicensePage = lazy( "./pages/DeploymentSettingsPage/LicensesSettingsPage/AddNewLicensePage" ), ); -const DefaultOrganizationRedirect = lazy( - () => import("./pages/OrganizationSettingsPage/DefaultOrganizationRedirect"), +const OrganizationRedirect = lazy( + () => import("./pages/OrganizationSettingsPage/OrganizationRedirect"), ); const CreateOrganizationPage = lazy( @@ -416,7 +416,7 @@ export const router = createBrowserRouter( <Route path="new" element={<CreateOrganizationPage />} /> {/* General settings for the default org can omit the organization name */} - <Route index element={<DefaultOrganizationRedirect />} /> + <Route index element={<OrganizationRedirect />} /> <Route path=":organization" element={<OrganizationSidebarLayout />}> <Route index element={<OrganizationMembersPage />} /> diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index a8df56dc5affd..37ae681977c2e 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -2836,12 +2836,13 @@ export const MockOrganizationPermissions: OrganizationPermissions = { createGroup: true, viewGroups: true, editGroups: true, - editOrganization: true, + editSettings: true, viewOrgRoles: true, createOrgRoles: true, assignOrgRoles: true, viewProvisioners: true, viewIdpSyncSettings: true, + editIdpSyncSettings: true, }; export const MockNoOrganizationPermissions: OrganizationPermissions = { @@ -2850,12 +2851,13 @@ export const MockNoOrganizationPermissions: OrganizationPermissions = { createGroup: false, viewGroups: false, editGroups: false, - editOrganization: false, + editSettings: false, viewOrgRoles: false, createOrgRoles: false, assignOrgRoles: false, viewProvisioners: false, viewIdpSyncSettings: false, + editIdpSyncSettings: false, }; export const MockNoPermissions: Permissions = { From d7990cfcbc2aeb97da6d2e8e30c526c2d805b501 Mon Sep 17 00:00:00 2001 From: McKayla Washburn <mckayla@hey.com> Date: Sat, 8 Feb 2025 00:22:29 +0000 Subject: [PATCH 07/21] =?UTF-8?q?=F0=9F=A7=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/src/modules/management/OrganizationSettingsLayout.tsx | 6 ++---- .../CustomRolesPage/CustomRolesPage.tsx | 1 + .../CustomRolesPage/CustomRolesPageView.tsx | 4 +++- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/site/src/modules/management/OrganizationSettingsLayout.tsx b/site/src/modules/management/OrganizationSettingsLayout.tsx index fd7a1f062a9d0..c8b88e35952c2 100644 --- a/site/src/modules/management/OrganizationSettingsLayout.tsx +++ b/site/src/modules/management/OrganizationSettingsLayout.tsx @@ -1,5 +1,5 @@ import { organizationsPermissions } from "api/queries/organizations"; -import type { AuthorizationResponse, Organization } from "api/typesGenerated"; +import type { Organization } from "api/typesGenerated"; import { ErrorAlert } from "components/Alert/ErrorAlert"; import { Avatar } from "components/Avatar/Avatar"; import { @@ -10,8 +10,6 @@ import { BreadcrumbSeparator, } from "components/Breadcrumb/Breadcrumb"; import { Loader } from "components/Loader/Loader"; -import { useAuthenticated } from "contexts/auth/RequireAuth"; -import { RequirePermission } from "contexts/auth/RequirePermission"; import { useDashboard } from "modules/dashboard/useDashboard"; import NotFoundPage from "pages/404Page/404Page"; import { type FC, Suspense, createContext, useContext } from "react"; @@ -48,7 +46,7 @@ export const useOrganizationSettings = (): OrganizationSettingsValue => { }; const OrganizationSettingsLayout: FC = () => { - const { organizations, canViewOrganizationSettings } = useDashboard(); + const { organizations } = useDashboard(); const { organization: orgName } = useParams() as { organization?: string; }; diff --git a/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPage.tsx b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPage.tsx index 096675adb1da5..362448368d1a6 100644 --- a/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPage.tsx @@ -74,6 +74,7 @@ export const CustomRolesPage: FC = () => { customRoles={customRoles} onDeleteRole={setRoleToDelete} canAssignOrgRole={organizationPermissions.assignOrgRoles} + canCreateOrgRole={organizationPermissions.createOrgRoles} isCustomRolesEnabled={isCustomRolesEnabled} /> diff --git a/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPageView.tsx b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPageView.tsx index c1aa2223703d2..1bb1f049aa804 100644 --- a/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPageView.tsx +++ b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPageView.tsx @@ -35,6 +35,7 @@ interface CustomRolesPageViewProps { customRoles: AssignableRoles[] | undefined; onDeleteRole: (role: Role) => void; canAssignOrgRole: boolean; + canCreateOrgRole: boolean; isCustomRolesEnabled: boolean; } @@ -43,6 +44,7 @@ export const CustomRolesPageView: FC<CustomRolesPageViewProps> = ({ customRoles, onDeleteRole, canAssignOrgRole, + canCreateOrgRole, isCustomRolesEnabled, }) => { return ( @@ -66,7 +68,7 @@ export const CustomRolesPageView: FC<CustomRolesPageViewProps> = ({ permissions. </span> </span> - {canAssignOrgRole && isCustomRolesEnabled && ( + {canCreateOrgRole && isCustomRolesEnabled && ( <Button component={RouterLink} startIcon={<AddIcon />} to="create"> Create custom role </Button> From acf5cc7e033f9218a3127759a1ce90037e3bedf6 Mon Sep 17 00:00:00 2001 From: McKayla Washburn <mckayla@hey.com> Date: Sat, 8 Feb 2025 00:25:33 +0000 Subject: [PATCH 08/21] :) --- site/src/modules/management/OrganizationSidebarView.tsx | 2 -- .../pages/OrganizationSettingsPage/OrganizationMembersPage.tsx | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/site/src/modules/management/OrganizationSidebarView.tsx b/site/src/modules/management/OrganizationSidebarView.tsx index e0f6ffb1f504e..2e281641cead1 100644 --- a/site/src/modules/management/OrganizationSidebarView.tsx +++ b/site/src/modules/management/OrganizationSidebarView.tsx @@ -47,8 +47,6 @@ export const OrganizationSidebarView: FC<SidebarProps> = ({ ] : organizations; - console.log(organizations); - const [isPopoverOpen, setIsPopoverOpen] = useState(false); const navigate = useNavigate(); diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.tsx index 196bbf410e73d..a236e2bc44c37 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.tsx @@ -10,6 +10,7 @@ import { import { organizationRoles } from "api/queries/roles"; import type { OrganizationMemberWithUserData, User } from "api/typesGenerated"; import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"; +import { EmptyState } from "components/EmptyState/EmptyState"; import { displayError, displaySuccess } from "components/GlobalSnackbar/utils"; import { Loader } from "components/Loader/Loader"; import { Stack } from "components/Stack/Stack"; @@ -21,7 +22,6 @@ import { useMutation, useQuery, useQueryClient } from "react-query"; import { useParams } from "react-router-dom"; import { pageTitle } from "utils/page"; import { OrganizationMembersPageView } from "./OrganizationMembersPageView"; -import { EmptyState } from "components/EmptyState/EmptyState"; const OrganizationMembersPage: FC = () => { const queryClient = useQueryClient(); From c8db4f73a20ed9c85a06ec62802a1efd38f64596 Mon Sep 17 00:00:00 2001 From: McKayla Washburn <mckayla@hey.com> Date: Tue, 11 Feb 2025 22:35:01 +0000 Subject: [PATCH 09/21] yay polish time --- coderd/rbac/roles.go | 17 ++++++++--------- .../Navbar/UserDropdown/UserDropdownContent.tsx | 7 ++++--- .../management/OrganizationSidebarView.tsx | 6 +++--- .../OrganizationSettingsPage.tsx | 8 ++------ 4 files changed, 17 insertions(+), 21 deletions(-) diff --git a/coderd/rbac/roles.go b/coderd/rbac/roles.go index 7fb141e557e96..d9023ae785d5f 100644 --- a/coderd/rbac/roles.go +++ b/coderd/rbac/roles.go @@ -297,18 +297,17 @@ func ReloadBuiltinRoles(opts *RoleOptions) { Identifier: RoleAuditor(), DisplayName: "Auditor", Site: Permissions(map[string][]policy.Action{ - // Should be able to read all template details, even in orgs they - // are not in. - ResourceTemplate.Type: {policy.ActionRead, policy.ActionViewInsights}, - ResourceAuditLog.Type: {policy.ActionRead}, - ResourceUser.Type: {policy.ActionRead}, - ResourceGroup.Type: {policy.ActionRead}, - ResourceGroupMember.Type: {policy.ActionRead}, + ResourceAuditLog.Type: {policy.ActionRead}, + // Allow auditors to see the resources that audit logs reflect. + ResourceTemplate.Type: {policy.ActionRead, policy.ActionViewInsights}, + ResourceUser.Type: {policy.ActionRead}, + ResourceGroup.Type: {policy.ActionRead}, + ResourceGroupMember.Type: {policy.ActionRead}, + ResourceOrganization.Type: {policy.ActionRead}, + ResourceOrganizationMember.Type: {policy.ActionRead}, // Allow auditors to query deployment stats and insights. ResourceDeploymentStats.Type: {policy.ActionRead}, ResourceDeploymentConfig.Type: {policy.ActionRead}, - // Org roles are not really used yet, so grant the perm at the site level. - ResourceOrganizationMember.Type: {policy.ActionRead}, }), Org: map[string][]Permission{}, User: []Permission{}, diff --git a/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdownContent.tsx b/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdownContent.tsx index 90ea1dab74a67..9eb89407dea31 100644 --- a/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdownContent.tsx +++ b/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdownContent.tsx @@ -22,6 +22,7 @@ import { Stack } from "components/Stack/Stack"; import { usePopover } from "components/deprecated/Popover/Popover"; import type { FC } from "react"; import { Link } from "react-router-dom"; + export const Language = { accountLabel: "Account", signOutLabel: "Sign Out", @@ -129,7 +130,7 @@ export const UserDropdownContent: FC<UserDropdownContentProps> = ({ </a> </Tooltip> - {Boolean(buildInfo?.deployment_id) && ( + {buildInfo?.deployment_id && ( <div css={css` font-size: 12px; @@ -145,11 +146,11 @@ export const UserDropdownContent: FC<UserDropdownContentProps> = ({ text-overflow: ellipsis; `} > - {buildInfo?.deployment_id} + {buildInfo.deployment_id} </div> </Tooltip> <CopyButton - text={buildInfo!.deployment_id} + text={buildInfo.deployment_id} buttonStyles={css` width: 16px; height: 16px; diff --git a/site/src/modules/management/OrganizationSidebarView.tsx b/site/src/modules/management/OrganizationSidebarView.tsx index 2e281641cead1..473935628aeb5 100644 --- a/site/src/modules/management/OrganizationSidebarView.tsx +++ b/site/src/modules/management/OrganizationSidebarView.tsx @@ -163,7 +163,7 @@ function urlForSubpage(organizationName: string, subpage = ""): string { interface OrganizationSettingsNavigationProps { organization: Organization; - orgPermissions: AuthorizationResponse; + orgPermissions: OrganizationPermissions; } const OrganizationSettingsNavigation: FC< @@ -184,7 +184,7 @@ const OrganizationSettingsNavigation: FC< Groups </SettingsSidebarNavItem> )} - {orgPermissions.assignOrgRole && ( + {orgPermissions.assignOrgRoles && ( <SettingsSidebarNavItem href={urlForSubpage(organization.name, "roles")} > @@ -205,7 +205,7 @@ const OrganizationSettingsNavigation: FC< IdP Sync </SettingsSidebarNavItem> )} - {orgPermissions.editOrganization && ( + {orgPermissions.editSettings && ( <SettingsSidebarNavItem href={urlForSubpage(organization.name, "settings")} > diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx index 8119bb94b7e0a..13c339dcc3c09 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx @@ -1,17 +1,13 @@ import { deleteOrganization, - organizationsPermissions, updateOrganization, } from "api/queries/organizations"; -import type { AuthorizationResponse } from "api/typesGenerated"; -import { ErrorAlert } from "components/Alert/ErrorAlert"; import { EmptyState } from "components/EmptyState/EmptyState"; import { displaySuccess } from "components/GlobalSnackbar/utils"; -import { Loader } from "components/Loader/Loader"; import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout"; import type { FC } from "react"; -import { useMutation, useQuery, useQueryClient } from "react-query"; -import { Navigate, useNavigate, useParams } from "react-router-dom"; +import { useMutation, useQueryClient } from "react-query"; +import { useNavigate } from "react-router-dom"; import { OrganizationSettingsPageView } from "./OrganizationSettingsPageView"; const OrganizationSettingsPage: FC = () => { From 1bb613722f1eea6984c4f07ab8d567d5a0b16b64 Mon Sep 17 00:00:00 2001 From: McKayla Washburn <mckayla@hey.com> Date: Tue, 11 Feb 2025 22:35:33 +0000 Subject: [PATCH 10/21] =?UTF-8?q?=F0=9F=92=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/src/contexts/auth/RequirePermission.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/site/src/contexts/auth/RequirePermission.tsx b/site/src/contexts/auth/RequirePermission.tsx index d1c68ae50b919..50dbd0232ab88 100644 --- a/site/src/contexts/auth/RequirePermission.tsx +++ b/site/src/contexts/auth/RequirePermission.tsx @@ -14,8 +14,7 @@ export const RequirePermission: FC<RequirePermissionProps> = ({ isFeatureVisible, }) => { if (!isFeatureVisible) { - // return <Navigate to="/workspaces" />; - return <h1>oh fuck</h1>; + return <Navigate to="/workspaces" />; } return <>{children}</>; From 5cd198bb067acee80ade32d3f483581c67537ec0 Mon Sep 17 00:00:00 2001 From: McKayla Washburn <mckayla@hey.com> Date: Tue, 18 Feb 2025 19:33:39 +0000 Subject: [PATCH 11/21] =?UTF-8?q?=F0=9F=A7=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CustomRolesPage/CreateEditRolePage.tsx | 7 ++++++- .../OrganizationMembersPage.test.tsx | 13 +++++++++---- .../OrganizationMembersPage.tsx | 1 - .../OrganizationMembersPageView.tsx | 1 + 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CreateEditRolePage.tsx b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CreateEditRolePage.tsx index d073ed698ec66..cc275cc75e703 100644 --- a/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CreateEditRolePage.tsx +++ b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CreateEditRolePage.tsx @@ -14,6 +14,7 @@ import { useMutation, useQuery, useQueryClient } from "react-query"; import { useNavigate, useParams } from "react-router-dom"; import { pageTitle } from "utils/page"; import CreateEditRolePageView from "./CreateEditRolePageView"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; export const CreateEditRolePage: FC = () => { const queryClient = useQueryClient(); @@ -35,10 +36,14 @@ export const CreateEditRolePage: FC = () => { ); const role = roleData?.find((role) => role.name === roleName); - if (isLoading || !organizationPermissions) { + if (isLoading) { return <Loader />; } + if (!organizationPermissions) { + return <ErrorAlert error="Failed to load organization permissions" />; + } + return ( <> <Helmet> diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.test.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.test.tsx index 0c9c7d44bd15a..1270f78484dc7 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.test.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.test.tsx @@ -6,6 +6,7 @@ import { MockEntitlementsWithMultiOrg, MockOrganization, MockOrganizationAuditorRole, + MockOrganizationPermissions, MockUser, } from "testHelpers/entities"; import { @@ -23,10 +24,14 @@ beforeEach(() => { return HttpResponse.json(MockEntitlementsWithMultiOrg); }), http.post("/api/v2/authcheck", async () => { - return HttpResponse.json({ - editMembers: true, - viewDeploymentValues: true, - }); + return HttpResponse.json( + Object.fromEntries( + Object.entries(MockOrganizationPermissions).map(([key, value]) => [ + `${MockOrganization.id}.${key}`, + value, + ]), + ), + ); }), ); }); diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.tsx index a236e2bc44c37..078ae1a0cbba8 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.tsx @@ -12,7 +12,6 @@ import type { OrganizationMemberWithUserData, User } from "api/typesGenerated"; import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"; import { EmptyState } from "components/EmptyState/EmptyState"; import { displayError, displaySuccess } from "components/GlobalSnackbar/utils"; -import { Loader } from "components/Loader/Loader"; import { Stack } from "components/Stack/Stack"; import { useAuthenticated } from "contexts/auth/RequireAuth"; import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout"; diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationMembersPageView.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPageView.tsx index 72737a92c3ebe..f6c791484e425 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationMembersPageView.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPageView.tsx @@ -79,6 +79,7 @@ export const OrganizationMembersPageView: FC< onSubmit={addMember} /> )} + <Table> <TableHeader> <TableRow> From 0cb6c4e6397c1940a2a58c30d385a756a327b988 Mon Sep 17 00:00:00 2001 From: McKayla Washburn <mckayla@hey.com> Date: Tue, 18 Feb 2025 19:43:56 +0000 Subject: [PATCH 12/21] of course --- .../CustomRolesPage/CreateEditRolePage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CreateEditRolePage.tsx b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CreateEditRolePage.tsx index cc275cc75e703..b9adbb44feb26 100644 --- a/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CreateEditRolePage.tsx +++ b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CreateEditRolePage.tsx @@ -5,6 +5,7 @@ import { updateOrganizationRole, } from "api/queries/roles"; import type { CustomRoleRequest } from "api/typesGenerated"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; import { displayError } from "components/GlobalSnackbar/utils"; import { Loader } from "components/Loader/Loader"; import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout"; @@ -14,7 +15,6 @@ import { useMutation, useQuery, useQueryClient } from "react-query"; import { useNavigate, useParams } from "react-router-dom"; import { pageTitle } from "utils/page"; import CreateEditRolePageView from "./CreateEditRolePageView"; -import { ErrorAlert } from "components/Alert/ErrorAlert"; export const CreateEditRolePage: FC = () => { const queryClient = useQueryClient(); From b4227b2e4466bb2c8e7b3fe329df912075894b94 Mon Sep 17 00:00:00 2001 From: McKayla Washburn <mckayla@hey.com> Date: Tue, 18 Feb 2025 22:16:41 +0000 Subject: [PATCH 13/21] fix some stories --- .../OrganizationSidebarView.stories.tsx | 13 +-- .../management/OrganizationSidebarView.tsx | 2 +- .../CustomRolesPageView.stories.tsx | 33 ++----- .../OrganizationSettingsPage.stories.tsx | 98 ------------------- 4 files changed, 16 insertions(+), 130 deletions(-) delete mode 100644 site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.stories.tsx diff --git a/site/src/modules/management/OrganizationSidebarView.stories.tsx b/site/src/modules/management/OrganizationSidebarView.stories.tsx index f6c7c204c451c..21869081a49c9 100644 --- a/site/src/modules/management/OrganizationSidebarView.stories.tsx +++ b/site/src/modules/management/OrganizationSidebarView.stories.tsx @@ -26,12 +26,6 @@ const meta: Meta<typeof OrganizationSidebarView> = { export default meta; type Story = StoryObj<typeof OrganizationSidebarView>; -export const LoadingOrganizations: Story = { - args: { - organizations: undefined, - }, -}; - export const NoCreateOrg: Story = { args: { activeOrganization: MockOrganization, @@ -164,8 +158,11 @@ export const SelectedOrgUserAdmin: Story = { activeOrganization: MockOrganization, orgPermissions: { ...MockNoOrganizationPermissions, - editMembers: true, - editGroups: true, + viewMembers: true, + viewGroups: true, + viewOrgRoles: true, + viewProvisioners: true, + viewIdpSyncSettings: true, }, permissions: { ...MockPermissions, diff --git a/site/src/modules/management/OrganizationSidebarView.tsx b/site/src/modules/management/OrganizationSidebarView.tsx index 473935628aeb5..06b148016bf36 100644 --- a/site/src/modules/management/OrganizationSidebarView.tsx +++ b/site/src/modules/management/OrganizationSidebarView.tsx @@ -184,7 +184,7 @@ const OrganizationSettingsNavigation: FC< Groups </SettingsSidebarNavItem> )} - {orgPermissions.assignOrgRoles && ( + {orgPermissions.viewOrgRoles && ( <SettingsSidebarNavItem href={urlForSubpage(organization.name, "roles")} > diff --git a/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPageView.stories.tsx b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPageView.stories.tsx index f37e23a1e989a..79319c888647f 100644 --- a/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPageView.stories.tsx +++ b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPageView.stories.tsx @@ -8,44 +8,38 @@ import { CustomRolesPageView } from "./CustomRolesPageView"; const meta: Meta<typeof CustomRolesPageView> = { title: "pages/OrganizationCustomRolesPage", component: CustomRolesPageView, + args: { + builtInRoles: [MockRoleWithOrgPermissions], + customRoles: [MockRoleWithOrgPermissions], + canAssignOrgRole: true, + canCreateOrgRole: true, + isCustomRolesEnabled: true, + }, }; export default meta; type Story = StoryObj<typeof CustomRolesPageView>; +export const Enabled: Story = {}; + export const NotEnabled: Story = { args: { - builtInRoles: [MockRoleWithOrgPermissions], - customRoles: [MockRoleWithOrgPermissions], - canAssignOrgRole: true, isCustomRolesEnabled: false, }, }; export const NotEnabledEmptyTable: Story = { args: { - builtInRoles: [MockRoleWithOrgPermissions], customRoles: [], canAssignOrgRole: true, isCustomRolesEnabled: false, }, }; -export const Enabled: Story = { - args: { - builtInRoles: [MockRoleWithOrgPermissions], - customRoles: [MockRoleWithOrgPermissions], - canAssignOrgRole: true, - isCustomRolesEnabled: true, - }, -}; - export const RoleWithoutPermissions: Story = { args: { builtInRoles: [MockOrganizationAuditorRole], customRoles: [MockOrganizationAuditorRole], - canAssignOrgRole: true, - isCustomRolesEnabled: true, }, }; @@ -58,26 +52,19 @@ export const EmptyDisplayName: Story = { display_name: "", }, ], - builtInRoles: [MockRoleWithOrgPermissions], - canAssignOrgRole: true, - isCustomRolesEnabled: true, }, }; export const EmptyTableUserWithoutPermission: Story = { args: { - builtInRoles: [MockRoleWithOrgPermissions], customRoles: [], canAssignOrgRole: false, - isCustomRolesEnabled: true, + canCreateOrgRole: false, }, }; export const EmptyTableUserWithPermission: Story = { args: { - builtInRoles: [MockRoleWithOrgPermissions], customRoles: [], - canAssignOrgRole: true, - isCustomRolesEnabled: true, }, }; diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.stories.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.stories.tsx deleted file mode 100644 index f1b0f8c93e81b..0000000000000 --- a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.stories.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import { reactRouterParameters } from "storybook-addon-remix-react-router"; -import { - MockDefaultOrganization, - MockOrganization, - MockOrganization2, - MockUser, -} from "testHelpers/entities"; -import { - withAuthProvider, - withDashboardProvider, - withOrganizationSettingsProvider, -} from "testHelpers/storybook"; -import OrganizationSettingsPage from "./OrganizationSettingsPage"; - -const meta: Meta<typeof OrganizationSettingsPage> = { - title: "pages/OrganizationSettingsPage", - component: OrganizationSettingsPage, - decorators: [ - withAuthProvider, - withDashboardProvider, - withOrganizationSettingsProvider, - ], - parameters: { - showOrganizations: true, - user: MockUser, - features: ["multiple_organizations"], - permissions: { viewDeploymentValues: true }, - queries: [ - { - key: ["organizations", [MockDefaultOrganization.id], "permissions"], - data: {}, - }, - ], - }, -}; - -export default meta; -type Story = StoryObj<typeof OrganizationSettingsPage>; - -export const NoRedirectableOrganizations: Story = {}; - -export const OrganizationDoesNotExist: Story = { - parameters: { - reactRouter: reactRouterParameters({ - location: { pathParams: { organization: "does-not-exist" } }, - routing: { path: "/organizations/:organization" }, - }), - }, -}; - -export const CannotEditOrganization: Story = { - parameters: { - reactRouter: reactRouterParameters({ - location: { pathParams: { organization: MockDefaultOrganization.name } }, - routing: { path: "/organizations/:organization" }, - }), - }, -}; - -export const CanEditOrganization: Story = { - parameters: { - reactRouter: reactRouterParameters({ - location: { pathParams: { organization: MockDefaultOrganization.name } }, - routing: { path: "/organizations/:organization" }, - }), - queries: [ - { - key: ["organizations", [MockDefaultOrganization.id], "permissions"], - data: { - [MockDefaultOrganization.id]: { - editOrganization: true, - }, - }, - }, - ], - }, -}; - -export const CanEditOrganizationNotEntitled: Story = { - parameters: { - reactRouter: reactRouterParameters({ - location: { pathParams: { organization: MockDefaultOrganization.name } }, - routing: { path: "/organizations/:organization" }, - }), - features: [], - queries: [ - { - key: ["organizations", [MockDefaultOrganization.id], "permissions"], - data: { - [MockDefaultOrganization.id]: { - editOrganization: true, - }, - }, - }, - ], - }, -}; From 5f6b24820546bf2a9ceda424aad52bb1e82ac25c Mon Sep 17 00:00:00 2001 From: McKayla Washburn <mckayla@hey.com> Date: Tue, 18 Feb 2025 23:17:58 +0000 Subject: [PATCH 14/21] a few more permissions fixes --- coderd/rbac/roles.go | 14 +++++++------- .../management/OrganizationSettingsLayout.tsx | 14 ++++++++++++-- .../modules/management/OrganizationSidebarView.tsx | 2 +- .../modules/management/organizationPermissions.tsx | 7 +++++++ .../ProvisionersPage/ProvisionersPage.tsx | 4 ++-- site/src/testHelpers/entities.ts | 2 ++ 6 files changed, 31 insertions(+), 12 deletions(-) diff --git a/coderd/rbac/roles.go b/coderd/rbac/roles.go index d9023ae785d5f..e1399aded95d0 100644 --- a/coderd/rbac/roles.go +++ b/coderd/rbac/roles.go @@ -324,11 +324,10 @@ func ReloadBuiltinRoles(opts *RoleOptions) { // CRUD to provisioner daemons for now. ResourceProvisionerDaemon.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, // Needs to read all organizations since - ResourceOrganization.Type: {policy.ActionRead}, - ResourceUser.Type: {policy.ActionRead}, - ResourceGroup.Type: {policy.ActionRead}, - ResourceGroupMember.Type: {policy.ActionRead}, - // Org roles are not really used yet, so grant the perm at the site level. + ResourceUser.Type: {policy.ActionRead}, + ResourceGroup.Type: {policy.ActionRead}, + ResourceGroupMember.Type: {policy.ActionRead}, + ResourceOrganization.Type: {policy.ActionRead}, ResourceOrganizationMember.Type: {policy.ActionRead}, }), Org: map[string][]Permission{}, @@ -347,10 +346,11 @@ func ReloadBuiltinRoles(opts *RoleOptions) { policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete, policy.ActionUpdatePersonal, policy.ActionReadPersonal, }, + ResourceGroup.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, + ResourceGroupMember.Type: {policy.ActionRead}, + ResourceOrganization.Type: {policy.ActionRead}, // Full perms to manage org members ResourceOrganizationMember.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, - ResourceGroup.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, - ResourceGroupMember.Type: {policy.ActionRead}, // Manage org membership based on OIDC claims ResourceIdpsyncSettings.Type: {policy.ActionRead, policy.ActionUpdate}, }), diff --git a/site/src/modules/management/OrganizationSettingsLayout.tsx b/site/src/modules/management/OrganizationSettingsLayout.tsx index c8b88e35952c2..906a8aaf42ff5 100644 --- a/site/src/modules/management/OrganizationSettingsLayout.tsx +++ b/site/src/modules/management/OrganizationSettingsLayout.tsx @@ -19,6 +19,8 @@ import { type OrganizationPermissions, canViewOrganization, } from "./organizationPermissions"; +import { Paywall } from "components/Paywall/Paywall"; +import { docs } from "utils/docs"; export const OrganizationSettingsContext = createContext< OrganizationSettingsValue | undefined @@ -46,7 +48,7 @@ export const useOrganizationSettings = (): OrganizationSettingsValue => { }; const OrganizationSettingsLayout: FC = () => { - const { organizations } = useDashboard(); + const { organizations, showOrganizations } = useDashboard(); const { organization: orgName } = useParams() as { organization?: string; }; @@ -123,7 +125,15 @@ const OrganizationSettingsLayout: FC = () => { <hr className="h-px border-none bg-border" /> <div className="px-10 max-w-screen-2xl"> <Suspense fallback={<Loader />}> - <Outlet /> + {showOrganizations ? ( + <Outlet /> + ) : ( + <Paywall + message="Organizations" + description="Organizations can be used to segment and isolate resources inside a Coder deployment. You need a Premium license to use this feature." + documentationLink={docs("/admin/users/organizations")} + /> + )} </Suspense> </div> </div> diff --git a/site/src/modules/management/OrganizationSidebarView.tsx b/site/src/modules/management/OrganizationSidebarView.tsx index 06b148016bf36..a875579ceab67 100644 --- a/site/src/modules/management/OrganizationSidebarView.tsx +++ b/site/src/modules/management/OrganizationSidebarView.tsx @@ -191,7 +191,7 @@ const OrganizationSettingsNavigation: FC< Roles </SettingsSidebarNavItem> )} - {orgPermissions.viewProvisioners && ( + {orgPermissions.viewProvisionerJobs && ( <SettingsSidebarNavItem href={urlForSubpage(organization.name, "provisioners")} > diff --git a/site/src/modules/management/organizationPermissions.tsx b/site/src/modules/management/organizationPermissions.tsx index a646d492f0640..2a414856105a4 100644 --- a/site/src/modules/management/organizationPermissions.tsx +++ b/site/src/modules/management/organizationPermissions.tsx @@ -80,6 +80,13 @@ export const organizationPermissionChecks = (organizationId: string) => }, action: "read", }, + viewProvisionerJobs: { + object: { + resource_type: "provisioner_jobs", + organization_id: organizationId, + }, + action: "read", + }, viewIdpSyncSettings: { object: { resource_type: "idpsync_settings", diff --git a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionersPage.tsx b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionersPage.tsx index 871eb7b91fa0f..051f916c3ad99 100644 --- a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionersPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionersPage.tsx @@ -9,13 +9,13 @@ import { ProvisionerDaemonsPage } from "./ProvisionerDaemonsPage"; import { ProvisionerJobsPage } from "./ProvisionerJobsPage"; const ProvisionersPage: FC = () => { - const { organization } = useOrganizationSettings(); + const { organization, organizationPermissions } = useOrganizationSettings(); const tab = useSearchParamsKey({ key: "tab", defaultValue: "jobs", }); - if (!organization) { + if (!organization || !organizationPermissions?.viewProvisionerJobs) { return ( <> <Helmet> diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 7ef77e73579b5..74d4de9121e2e 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -2855,6 +2855,7 @@ export const MockOrganizationPermissions: OrganizationPermissions = { createOrgRoles: true, assignOrgRoles: true, viewProvisioners: true, + viewProvisionerJobs: true, viewIdpSyncSettings: true, editIdpSyncSettings: true, }; @@ -2870,6 +2871,7 @@ export const MockNoOrganizationPermissions: OrganizationPermissions = { createOrgRoles: false, assignOrgRoles: false, viewProvisioners: false, + viewProvisionerJobs: false, viewIdpSyncSettings: false, editIdpSyncSettings: false, }; From 81fbdf4116ff9aa219efeea1543f0fabf665b677 Mon Sep 17 00:00:00 2001 From: McKayla Washburn <mckayla@hey.com> Date: Tue, 18 Feb 2025 23:19:35 +0000 Subject: [PATCH 15/21] mmmmm bep --- .../management/OrganizationSidebarView.tsx | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/site/src/modules/management/OrganizationSidebarView.tsx b/site/src/modules/management/OrganizationSidebarView.tsx index a875579ceab67..b5b1ae09122ec 100644 --- a/site/src/modules/management/OrganizationSidebarView.tsx +++ b/site/src/modules/management/OrganizationSidebarView.tsx @@ -191,13 +191,14 @@ const OrganizationSettingsNavigation: FC< Roles </SettingsSidebarNavItem> )} - {orgPermissions.viewProvisionerJobs && ( - <SettingsSidebarNavItem - href={urlForSubpage(organization.name, "provisioners")} - > - Provisioners - </SettingsSidebarNavItem> - )} + {orgPermissions.viewProvisioners && + orgPermissions.viewProvisionerJobs && ( + <SettingsSidebarNavItem + href={urlForSubpage(organization.name, "provisioners")} + > + Provisioners + </SettingsSidebarNavItem> + )} {orgPermissions.viewIdpSyncSettings && ( <SettingsSidebarNavItem href={urlForSubpage(organization.name, "idp-sync")} From 41ff221676dc8c1d6c6b0804c239fb81a6b741e6 Mon Sep 17 00:00:00 2001 From: McKayla Washburn <mckayla@hey.com> Date: Tue, 18 Feb 2025 23:21:23 +0000 Subject: [PATCH 16/21] :| --- site/src/modules/management/OrganizationSettingsLayout.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site/src/modules/management/OrganizationSettingsLayout.tsx b/site/src/modules/management/OrganizationSettingsLayout.tsx index 906a8aaf42ff5..ffca37beddef2 100644 --- a/site/src/modules/management/OrganizationSettingsLayout.tsx +++ b/site/src/modules/management/OrganizationSettingsLayout.tsx @@ -10,17 +10,17 @@ import { BreadcrumbSeparator, } from "components/Breadcrumb/Breadcrumb"; import { Loader } from "components/Loader/Loader"; +import { Paywall } from "components/Paywall/Paywall"; import { useDashboard } from "modules/dashboard/useDashboard"; import NotFoundPage from "pages/404Page/404Page"; import { type FC, Suspense, createContext, useContext } from "react"; import { useQuery } from "react-query"; import { Outlet, useParams } from "react-router-dom"; +import { docs } from "utils/docs"; import { type OrganizationPermissions, canViewOrganization, } from "./organizationPermissions"; -import { Paywall } from "components/Paywall/Paywall"; -import { docs } from "utils/docs"; export const OrganizationSettingsContext = createContext< OrganizationSettingsValue | undefined From 58a08768ce7174a465f2767298cc98eabe468917 Mon Sep 17 00:00:00 2001 From: McKayla Washburn <mckayla@hey.com> Date: Tue, 18 Feb 2025 23:33:38 +0000 Subject: [PATCH 17/21] testin' --- coderd/rbac/roles_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/coderd/rbac/roles_test.go b/coderd/rbac/roles_test.go index db0d9832579fc..cb43b1b1751d6 100644 --- a/coderd/rbac/roles_test.go +++ b/coderd/rbac/roles_test.go @@ -117,6 +117,7 @@ func TestRolePermissions(t *testing.T) { owner := authSubject{Name: "owner", Actor: rbac.Subject{ID: adminID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.RoleOwner()}}} templateAdmin := authSubject{Name: "template-admin", Actor: rbac.Subject{ID: templateAdminID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.RoleTemplateAdmin()}}} userAdmin := authSubject{Name: "user-admin", Actor: rbac.Subject{ID: userAdminID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.RoleUserAdmin()}}} + auditor := authSubject{Name: "auditor", Actor: rbac.Subject{ID: auditorID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.RoleAuditor()}}} orgAdmin := authSubject{Name: "org_admin", Actor: rbac.Subject{ID: adminID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.ScopedRoleOrgMember(orgID), rbac.ScopedRoleOrgAdmin(orgID)}}} orgAuditor := authSubject{Name: "org_auditor", Actor: rbac.Subject{ID: auditorID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.ScopedRoleOrgMember(orgID), rbac.ScopedRoleOrgAuditor(orgID)}}} @@ -286,8 +287,8 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionRead}, Resource: rbac.ResourceOrganization.WithID(orgID).InOrg(orgID), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner, orgAdmin, orgMemberMe, templateAdmin, orgTemplateAdmin, orgAuditor, orgUserAdmin}, - false: {setOtherOrg, memberMe, userAdmin}, + true: {owner, orgAdmin, orgMemberMe, templateAdmin, orgTemplateAdmin, auditor, orgAuditor, userAdmin, orgUserAdmin}, + false: {setOtherOrg, memberMe}, }, }, { From 71b317074004155ce4238c7b6a926e2fb9bca144 Mon Sep 17 00:00:00 2001 From: McKayla Washburn <mckayla@hey.com> Date: Tue, 18 Feb 2025 23:56:22 +0000 Subject: [PATCH 18/21] ok lets do this separately --- .../management/OrganizationSettingsLayout.tsx | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/site/src/modules/management/OrganizationSettingsLayout.tsx b/site/src/modules/management/OrganizationSettingsLayout.tsx index ffca37beddef2..ae1ce597641ae 100644 --- a/site/src/modules/management/OrganizationSettingsLayout.tsx +++ b/site/src/modules/management/OrganizationSettingsLayout.tsx @@ -10,13 +10,11 @@ import { BreadcrumbSeparator, } from "components/Breadcrumb/Breadcrumb"; import { Loader } from "components/Loader/Loader"; -import { Paywall } from "components/Paywall/Paywall"; import { useDashboard } from "modules/dashboard/useDashboard"; import NotFoundPage from "pages/404Page/404Page"; import { type FC, Suspense, createContext, useContext } from "react"; import { useQuery } from "react-query"; import { Outlet, useParams } from "react-router-dom"; -import { docs } from "utils/docs"; import { type OrganizationPermissions, canViewOrganization, @@ -125,15 +123,7 @@ const OrganizationSettingsLayout: FC = () => { <hr className="h-px border-none bg-border" /> <div className="px-10 max-w-screen-2xl"> <Suspense fallback={<Loader />}> - {showOrganizations ? ( - <Outlet /> - ) : ( - <Paywall - message="Organizations" - description="Organizations can be used to segment and isolate resources inside a Coder deployment. You need a Premium license to use this feature." - documentationLink={docs("/admin/users/organizations")} - /> - )} + <Outlet /> </Suspense> </div> </div> From 37d87d3790e5aaa8bed4d87a11c619e510767b9f Mon Sep 17 00:00:00 2001 From: McKayla Washburn <mckayla@hey.com> Date: Wed, 19 Feb 2025 00:37:33 +0000 Subject: [PATCH 19/21] add missing query to terminal page story --- site/src/api/queries/organizations.ts | 7 ++++++- site/src/pages/TerminalPage/TerminalPage.stories.tsx | 2 ++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/site/src/api/queries/organizations.ts b/site/src/api/queries/organizations.ts index 58fb9230c9396..a27514a03c161 100644 --- a/site/src/api/queries/organizations.ts +++ b/site/src/api/queries/organizations.ts @@ -266,9 +266,14 @@ export const organizationsPermissions = ( }; }; +export const anyOrganizationPermissionsKey = [ + "authorization", + "anyOrganization", +]; + export const anyOrganizationPermissions = () => { return { - queryKey: ["authorization", "anyOrganization"], + queryKey: anyOrganizationPermissionsKey, queryFn: () => API.checkAuthorization({ checks: anyOrganizationPermissionChecks, diff --git a/site/src/pages/TerminalPage/TerminalPage.stories.tsx b/site/src/pages/TerminalPage/TerminalPage.stories.tsx index 4fae86ff8b8ca..ded6047122932 100644 --- a/site/src/pages/TerminalPage/TerminalPage.stories.tsx +++ b/site/src/pages/TerminalPage/TerminalPage.stories.tsx @@ -22,6 +22,7 @@ import { } from "testHelpers/entities"; import { withWebSocket } from "testHelpers/storybook"; import TerminalPage from "./TerminalPage"; +import { anyOrganizationPermissionsKey } from "api/queries/organizations"; const createWorkspaceWithAgent = (lifecycle: WorkspaceAgentLifecycle) => { return { @@ -76,6 +77,7 @@ const meta = { key: getAuthorizationKey({ checks: permissionsToCheck }), data: { editWorkspaceProxies: true }, }, + { key: anyOrganizationPermissionsKey, data: {} }, ], chromatic: { delay: 300 }, }, From 660cbda086ca16001cdffac6e19d3d97aac77982 Mon Sep 17 00:00:00 2001 From: McKayla Washburn <mckayla@hey.com> Date: Wed, 19 Feb 2025 00:45:11 +0000 Subject: [PATCH 20/21] =?UTF-8?q?=F0=9F=A7=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/src/pages/TerminalPage/TerminalPage.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/pages/TerminalPage/TerminalPage.stories.tsx b/site/src/pages/TerminalPage/TerminalPage.stories.tsx index ded6047122932..b9dfeba1d811d 100644 --- a/site/src/pages/TerminalPage/TerminalPage.stories.tsx +++ b/site/src/pages/TerminalPage/TerminalPage.stories.tsx @@ -1,5 +1,6 @@ import type { Meta, StoryObj } from "@storybook/react"; import { getAuthorizationKey } from "api/queries/authCheck"; +import { anyOrganizationPermissionsKey } from "api/queries/organizations"; import { workspaceByOwnerAndNameKey } from "api/queries/workspaces"; import type { Workspace, WorkspaceAgentLifecycle } from "api/typesGenerated"; import { AuthProvider } from "contexts/auth/AuthProvider"; @@ -22,7 +23,6 @@ import { } from "testHelpers/entities"; import { withWebSocket } from "testHelpers/storybook"; import TerminalPage from "./TerminalPage"; -import { anyOrganizationPermissionsKey } from "api/queries/organizations"; const createWorkspaceWithAgent = (lifecycle: WorkspaceAgentLifecycle) => { return { From f0f3859f0a06e48b591d8c67b760dce0b70b560f Mon Sep 17 00:00:00 2001 From: McKayla Washburn <mckayla@hey.com> Date: Wed, 19 Feb 2025 18:36:44 +0000 Subject: [PATCH 21/21] story fixes --- .../OrganizationSidebarView.stories.tsx | 32 ++++++++- .../management/OrganizationSidebarView.tsx | 66 ++++++++----------- 2 files changed, 58 insertions(+), 40 deletions(-) diff --git a/site/src/modules/management/OrganizationSidebarView.stories.tsx b/site/src/modules/management/OrganizationSidebarView.stories.tsx index ae0402d5b8758..0a3ebef493239 100644 --- a/site/src/modules/management/OrganizationSidebarView.stories.tsx +++ b/site/src/modules/management/OrganizationSidebarView.stories.tsx @@ -118,6 +118,36 @@ export const OverflowDropdown: Story = { }, }; +export const NoOrganizations: Story = { + args: { + organizations: [], + activeOrganization: undefined, + orgPermissions: MockNoOrganizationPermissions, + permissions: MockNoPermissions, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.click( + canvas.getByRole("button", { name: /No organization selected/i }), + ); + }, +}; + +export const NoOtherOrganizations: Story = { + args: { + organizations: [MockOrganization], + activeOrganization: MockOrganization, + orgPermissions: MockNoOrganizationPermissions, + permissions: MockNoPermissions, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.click( + canvas.getByRole("button", { name: /My Organization/i }), + ); + }, +}; + export const NoPermissions: Story = { args: { activeOrganization: MockOrganization, @@ -263,7 +293,7 @@ export const SearchForOrg: Story = { // dropdown is not in #storybook-root so must query full document const globalScreen = within(document.body); const searchInput = - await globalScreen.getByPlaceholderText("Find organization"); + await globalScreen.findByPlaceholderText("Find organization"); await userEvent.type(searchInput, "ALPHA"); diff --git a/site/src/modules/management/OrganizationSidebarView.tsx b/site/src/modules/management/OrganizationSidebarView.tsx index 2c05a8d1663bc..7f3b697766563 100644 --- a/site/src/modules/management/OrganizationSidebarView.tsx +++ b/site/src/modules/management/OrganizationSidebarView.tsx @@ -89,45 +89,33 @@ export const OrganizationSidebarView: FC< <CommandList> <CommandEmpty>No organization found.</CommandEmpty> <CommandGroup className="pb-2"> - {sortedOrganizations.length > (activeOrganization ? 1 : 0) ? ( - <div className="flex flex-col max-h-[260px] overflow-y-auto"> - {sortedOrganizations.map((organization) => ( - <CommandItem - key={organization.id} - value={`${organization.display_name} ${organization.name}`} - onSelect={() => { - setIsPopoverOpen(false); - navigate(urlForSubpage(organization.name)); - }} - // There is currently an issue with the cmdk component for keyboard navigation - // https://github.com/pacocoursey/cmdk/issues/322 - tabIndex={0} - > - <Avatar - size="sm" - src={organization.icon} - fallback={organization.display_name} - /> - <span className="truncate"> - {organization?.display_name || organization?.name} - </span> - {activeOrganization?.name === organization.name && ( - <Check - size={16} - strokeWidth={2} - className="ml-auto" - /> - )} - </CommandItem> - ))} - </div> - ) : ( - !permissions.createOrganization && ( - <span className="select-none text-content-disabled text-center rounded-sm px-2 py-2 text-sm font-medium"> - No more organizations - </span> - ) - )} + <div className="flex flex-col max-h-[260px] overflow-y-auto"> + {sortedOrganizations.map((organization) => ( + <CommandItem + key={organization.id} + value={`${organization.display_name} ${organization.name}`} + onSelect={() => { + setIsPopoverOpen(false); + navigate(urlForSubpage(organization.name)); + }} + // There is currently an issue with the cmdk component for keyboard navigation + // https://github.com/pacocoursey/cmdk/issues/322 + tabIndex={0} + > + <Avatar + size="sm" + src={organization.icon} + fallback={organization.display_name} + /> + <span className="truncate"> + {organization?.display_name || organization?.name} + </span> + {activeOrganization?.name === organization.name && ( + <Check size={16} strokeWidth={2} className="ml-auto" /> + )} + </CommandItem> + ))} + </div> </CommandGroup> {permissions.createOrganization && ( <> <!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'> <html xmlns='http://www.w3.org/1999/xhtml'> <head> <title>pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy