From 3569e56cf912aac565d7357b4efeb7ecd37a643f Mon Sep 17 00:00:00 2001 From: "gcp-cherry-pick-bot[bot]" <98988430+gcp-cherry-pick-bot[bot]@users.noreply.github.com> Date: Mon, 17 Mar 2025 11:13:40 -0500 Subject: [PATCH 1/2] fix: hide deleted users from org members query (cherry-pick #16830) (#16832) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cherry-picked fix: hide deleted users from org members query (#16830) Co-authored-by: ケイラ --- coderd/database/queries.sql.go | 2 +- coderd/database/queries/organizationmembers.sql | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 2f28b563e98b8..37c79537c622f 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -4967,7 +4967,7 @@ SELECT FROM organization_members INNER JOIN - users ON organization_members.user_id = users.id + users ON organization_members.user_id = users.id AND users.deleted = false WHERE -- Filter by organization id CASE diff --git a/coderd/database/queries/organizationmembers.sql b/coderd/database/queries/organizationmembers.sql index 71304c8883602..8685e71129ac9 100644 --- a/coderd/database/queries/organizationmembers.sql +++ b/coderd/database/queries/organizationmembers.sql @@ -9,7 +9,7 @@ SELECT FROM organization_members INNER JOIN - users ON organization_members.user_id = users.id + users ON organization_members.user_id = users.id AND users.deleted = false WHERE -- Filter by organization id CASE From 8a3ebaca0112703297af94d9f65dedaa832e2b4e Mon Sep 17 00:00:00 2001 From: "gcp-cherry-pick-bot[bot]" <98988430+gcp-cherry-pick-bot[bot]@users.noreply.github.com> Date: Mon, 17 Mar 2025 16:00:39 -0500 Subject: [PATCH 2/2] fix: fix deployment settings navigation issues (cherry-pick #16780) (#16790) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cherry-picked fix: fix deployment settings navigation issues (#16780) Co-authored-by: ケイラ --- site/e2e/tests/roles.spec.ts | 157 +++++++++++++++++ site/src/api/queries/organizations.ts | 17 -- site/src/contexts/auth/AuthProvider.tsx | 6 +- site/src/contexts/auth/permissions.tsx | 159 ++++++++++++------ .../modules/dashboard/DashboardProvider.tsx | 21 +-- .../dashboard/Navbar/DeploymentDropdown.tsx | 2 +- .../modules/dashboard/Navbar/MobileMenu.tsx | 2 +- site/src/modules/dashboard/Navbar/Navbar.tsx | 11 +- .../dashboard/Navbar/NavbarView.test.tsx | 2 +- .../dashboard/Navbar/ProxyMenu.stories.tsx | 4 +- .../management/DeploymentSettingsLayout.tsx | 26 ++- .../management/DeploymentSettingsProvider.tsx | 25 +-- .../management/organizationPermissions.tsx | 62 ------- .../TerminalPage/TerminalPage.stories.tsx | 6 +- site/src/router.tsx | 5 +- site/src/testHelpers/entities.ts | 58 ++++--- site/src/testHelpers/handlers.ts | 4 +- site/src/testHelpers/storybook.tsx | 4 +- 18 files changed, 350 insertions(+), 221 deletions(-) create mode 100644 site/e2e/tests/roles.spec.ts diff --git a/site/e2e/tests/roles.spec.ts b/site/e2e/tests/roles.spec.ts new file mode 100644 index 0000000000000..482436c9c9b2d --- /dev/null +++ b/site/e2e/tests/roles.spec.ts @@ -0,0 +1,157 @@ +import { type Page, expect, test } from "@playwright/test"; +import { + createOrganization, + createOrganizationMember, + setupApiCalls, +} from "../api"; +import { license, users } from "../constants"; +import { login, requiresLicense } from "../helpers"; +import { beforeCoderTest } from "../hooks"; + +test.beforeEach(async ({ page }) => { + beforeCoderTest(page); +}); + +type AdminSetting = (typeof adminSettings)[number]; + +const adminSettings = [ + "Deployment", + "Organizations", + "Healthcheck", + "Audit Logs", +] as const; + +async function hasAccessToAdminSettings(page: Page, settings: AdminSetting[]) { + // Organizations and Audit Logs both require a license to be visible + const visibleSettings = license + ? settings + : settings.filter((it) => it !== "Organizations" && it !== "Audit Logs"); + const adminSettingsButton = page.getByRole("button", { + name: "Admin settings", + }); + if (visibleSettings.length < 1) { + await expect(adminSettingsButton).not.toBeVisible(); + return; + } + + await adminSettingsButton.click(); + + for (const name of visibleSettings) { + await expect(page.getByText(name, { exact: true })).toBeVisible(); + } + + const hiddenSettings = adminSettings.filter( + (it) => !visibleSettings.includes(it), + ); + for (const name of hiddenSettings) { + await expect(page.getByText(name, { exact: true })).not.toBeVisible(); + } +} + +test.describe("roles admin settings access", () => { + test("member cannot see admin settings", async ({ page }) => { + await login(page, users.member); + await page.goto("/", { waitUntil: "domcontentloaded" }); + + // None, "Admin settings" button should not be visible + await hasAccessToAdminSettings(page, []); + }); + + test("template admin can see admin settings", async ({ page }) => { + await login(page, users.templateAdmin); + await page.goto("/", { waitUntil: "domcontentloaded" }); + + await hasAccessToAdminSettings(page, ["Deployment", "Organizations"]); + }); + + test("user admin can see admin settings", async ({ page }) => { + await login(page, users.userAdmin); + await page.goto("/", { waitUntil: "domcontentloaded" }); + + await hasAccessToAdminSettings(page, ["Deployment", "Organizations"]); + }); + + test("auditor can see admin settings", async ({ page }) => { + await login(page, users.auditor); + await page.goto("/", { waitUntil: "domcontentloaded" }); + + await hasAccessToAdminSettings(page, [ + "Deployment", + "Organizations", + "Audit Logs", + ]); + }); + + test("admin can see admin settings", async ({ page }) => { + await login(page, users.admin); + await page.goto("/", { waitUntil: "domcontentloaded" }); + + await hasAccessToAdminSettings(page, [ + "Deployment", + "Organizations", + "Healthcheck", + "Audit Logs", + ]); + }); +}); + +test.describe("org-scoped roles admin settings access", () => { + requiresLicense(); + + test.beforeEach(async ({ page }) => { + await login(page); + await setupApiCalls(page); + }); + + test("org template admin can see admin settings", async ({ page }) => { + const org = await createOrganization(); + const orgTemplateAdmin = await createOrganizationMember({ + [org.id]: ["organization-template-admin"], + }); + + await login(page, orgTemplateAdmin); + await page.goto("/", { waitUntil: "domcontentloaded" }); + + await hasAccessToAdminSettings(page, ["Organizations"]); + }); + + test("org user admin can see admin settings", async ({ page }) => { + const org = await createOrganization(); + const orgUserAdmin = await createOrganizationMember({ + [org.id]: ["organization-user-admin"], + }); + + await login(page, orgUserAdmin); + await page.goto("/", { waitUntil: "domcontentloaded" }); + + await hasAccessToAdminSettings(page, ["Deployment", "Organizations"]); + }); + + test("org auditor can see admin settings", async ({ page }) => { + const org = await createOrganization(); + const orgAuditor = await createOrganizationMember({ + [org.id]: ["organization-auditor"], + }); + + await login(page, orgAuditor); + await page.goto("/", { waitUntil: "domcontentloaded" }); + + await hasAccessToAdminSettings(page, ["Organizations", "Audit Logs"]); + }); + + test("org admin can see admin settings", async ({ page }) => { + const org = await createOrganization(); + const orgAdmin = await createOrganizationMember({ + [org.id]: ["organization-admin"], + }); + + await login(page, orgAdmin); + await page.goto("/", { waitUntil: "domcontentloaded" }); + + await hasAccessToAdminSettings(page, [ + "Deployment", + "Organizations", + "Audit Logs", + ]); + }); +}); diff --git a/site/src/api/queries/organizations.ts b/site/src/api/queries/organizations.ts index a27514a03c161..374f9e7eacf4e 100644 --- a/site/src/api/queries/organizations.ts +++ b/site/src/api/queries/organizations.ts @@ -6,10 +6,8 @@ import type { UpdateOrganizationRequest, } from "api/typesGenerated"; import { - type AnyOrganizationPermissions, type OrganizationPermissionName, type OrganizationPermissions, - anyOrganizationPermissionChecks, organizationPermissionChecks, } from "modules/management/organizationPermissions"; import type { QueryClient } from "react-query"; @@ -266,21 +264,6 @@ export const organizationsPermissions = ( }; }; -export const anyOrganizationPermissionsKey = [ - "authorization", - "anyOrganization", -]; - -export const anyOrganizationPermissions = () => { - return { - queryKey: anyOrganizationPermissionsKey, - 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..7418691a291e5 100644 --- a/site/src/contexts/auth/AuthProvider.tsx +++ b/site/src/contexts/auth/AuthProvider.tsx @@ -18,7 +18,7 @@ import { useContext, } from "react"; import { useMutation, useQuery, useQueryClient } from "react-query"; -import { type Permissions, permissionsToCheck } from "./permissions"; +import { type Permissions, permissionChecks } from "./permissions"; export type AuthContextValue = { isLoading: boolean; @@ -50,13 +50,13 @@ export const AuthProvider: FC = ({ children }) => { const hasFirstUserQuery = useQuery(hasFirstUser(userMetadataState)); const permissionsQuery = useQuery({ - ...checkAuthorization({ checks: permissionsToCheck }), + ...checkAuthorization({ checks: permissionChecks }), enabled: userQuery.data !== undefined, }); const queryClient = useQueryClient(); const loginMutation = useMutation( - login({ checks: permissionsToCheck }, queryClient), + login({ checks: permissionChecks }, queryClient), ); const logoutMutation = useMutation(logout(queryClient)); diff --git a/site/src/contexts/auth/permissions.tsx b/site/src/contexts/auth/permissions.tsx index 1043862942edb..0d8957627c36d 100644 --- a/site/src/contexts/auth/permissions.tsx +++ b/site/src/contexts/auth/permissions.tsx @@ -1,156 +1,205 @@ import type { AuthorizationCheck } from "api/typesGenerated"; -export const checks = { - viewAllUsers: "viewAllUsers", - updateUsers: "updateUsers", - createUser: "createUser", - createTemplates: "createTemplates", - updateTemplates: "updateTemplates", - deleteTemplates: "deleteTemplates", - viewAnyAuditLog: "viewAnyAuditLog", - viewDeploymentValues: "viewDeploymentValues", - editDeploymentValues: "editDeploymentValues", - viewUpdateCheck: "viewUpdateCheck", - viewExternalAuthConfig: "viewExternalAuthConfig", - viewDeploymentStats: "viewDeploymentStats", - readWorkspaceProxies: "readWorkspaceProxies", - editWorkspaceProxies: "editWorkspaceProxies", - createOrganization: "createOrganization", - viewAnyGroup: "viewAnyGroup", - createGroup: "createGroup", - viewAllLicenses: "viewAllLicenses", - viewNotificationTemplate: "viewNotificationTemplate", - viewOrganizationIDPSyncSettings: "viewOrganizationIDPSyncSettings", -} as const satisfies Record; +export type Permissions = { + [k in PermissionName]: boolean; +}; -// Type expression seems a little redundant (`keyof typeof checks` has the same -// result), just because each key-value pair is currently symmetrical; this may -// change down the line -type PermissionValue = (typeof checks)[keyof typeof checks]; +export type PermissionName = keyof typeof permissionChecks; -export const permissionsToCheck = { - [checks.viewAllUsers]: { +export const permissionChecks = { + viewAllUsers: { object: { resource_type: "user", }, action: "read", }, - [checks.updateUsers]: { + updateUsers: { object: { resource_type: "user", }, action: "update", }, - [checks.createUser]: { + createUser: { object: { resource_type: "user", }, action: "create", }, - [checks.createTemplates]: { + createTemplates: { object: { resource_type: "template", any_org: true, }, action: "update", }, - [checks.updateTemplates]: { + updateTemplates: { object: { resource_type: "template", }, action: "update", }, - [checks.deleteTemplates]: { + deleteTemplates: { object: { resource_type: "template", }, action: "delete", }, - [checks.viewAnyAuditLog]: { - object: { - resource_type: "audit_log", - any_org: true, - }, - action: "read", - }, - [checks.viewDeploymentValues]: { + viewDeploymentValues: { object: { resource_type: "deployment_config", }, action: "read", }, - [checks.editDeploymentValues]: { + editDeploymentValues: { object: { resource_type: "deployment_config", }, action: "update", }, - [checks.viewUpdateCheck]: { + viewUpdateCheck: { object: { resource_type: "deployment_config", }, action: "read", }, - [checks.viewExternalAuthConfig]: { + viewExternalAuthConfig: { object: { resource_type: "deployment_config", }, action: "read", }, - [checks.viewDeploymentStats]: { + viewDeploymentStats: { object: { resource_type: "deployment_stats", }, action: "read", }, - [checks.readWorkspaceProxies]: { + readWorkspaceProxies: { object: { resource_type: "workspace_proxy", }, action: "read", }, - [checks.editWorkspaceProxies]: { + editWorkspaceProxies: { object: { resource_type: "workspace_proxy", }, action: "create", }, - [checks.createOrganization]: { + createOrganization: { object: { resource_type: "organization", }, action: "create", }, - [checks.viewAnyGroup]: { + viewAnyGroup: { object: { resource_type: "group", }, action: "read", }, - [checks.createGroup]: { + createGroup: { object: { resource_type: "group", }, action: "create", }, - [checks.viewAllLicenses]: { + viewAllLicenses: { object: { resource_type: "license", }, action: "read", }, - [checks.viewNotificationTemplate]: { + viewNotificationTemplate: { object: { resource_type: "notification_template", }, action: "read", }, - [checks.viewOrganizationIDPSyncSettings]: { + viewOrganizationIDPSyncSettings: { object: { resource_type: "idpsync_settings", }, action: "read", }, -} as const satisfies Record; -export type Permissions = Record; + viewAnyMembers: { + object: { + resource_type: "organization_member", + any_org: true, + }, + action: "read", + }, + editAnyGroups: { + object: { + resource_type: "group", + any_org: true, + }, + action: "update", + }, + assignAnyRoles: { + object: { + resource_type: "assign_org_role", + any_org: true, + }, + action: "assign", + }, + viewAnyIdpSyncSettings: { + object: { + resource_type: "idpsync_settings", + any_org: true, + }, + action: "read", + }, + editAnySettings: { + object: { + resource_type: "organization", + any_org: true, + }, + action: "update", + }, + viewAnyAuditLog: { + object: { + resource_type: "audit_log", + any_org: true, + }, + action: "read", + }, + viewDebugInfo: { + object: { + resource_type: "debug_info", + }, + action: "read", + }, +} as const satisfies Record; + +export const canViewDeploymentSettings = ( + permissions: Permissions | undefined, +): permissions is Permissions => { + return ( + permissions !== undefined && + (permissions.viewDeploymentValues || + permissions.viewAllLicenses || + permissions.viewAllUsers || + permissions.viewAnyGroup || + permissions.viewNotificationTemplate || + permissions.viewOrganizationIDPSyncSettings) + ); +}; + +/** + * Checks if the user can view or edit members or groups for the organization + * that produced the given OrganizationPermissions. + */ +export const canViewAnyOrganization = ( + permissions: Permissions | undefined, +): permissions is Permissions => { + return ( + permissions !== undefined && + (permissions.viewAnyMembers || + permissions.editAnyGroups || + permissions.assignAnyRoles || + permissions.viewAnyIdpSyncSettings || + permissions.editAnySettings) + ); +}; diff --git a/site/src/modules/dashboard/DashboardProvider.tsx b/site/src/modules/dashboard/DashboardProvider.tsx index bf8e307206aea..bb5987d6546be 100644 --- a/site/src/modules/dashboard/DashboardProvider.tsx +++ b/site/src/modules/dashboard/DashboardProvider.tsx @@ -1,10 +1,7 @@ import { appearance } from "api/queries/appearance"; import { entitlements } from "api/queries/entitlements"; import { experiments } from "api/queries/experiments"; -import { - anyOrganizationPermissions, - organizations, -} from "api/queries/organizations"; +import { organizations } from "api/queries/organizations"; import type { AppearanceConfig, Entitlements, @@ -13,8 +10,9 @@ import type { } from "api/typesGenerated"; import { ErrorAlert } from "components/Alert/ErrorAlert"; import { Loader } from "components/Loader/Loader"; +import { useAuthenticated } from "contexts/auth/RequireAuth"; +import { canViewAnyOrganization } from "contexts/auth/permissions"; import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata"; -import { canViewAnyOrganization } from "modules/management/organizationPermissions"; import { type FC, type PropsWithChildren, createContext } from "react"; import { useQuery } from "react-query"; import { selectFeatureVisibility } from "./entitlements"; @@ -34,20 +32,17 @@ export const DashboardContext = createContext( export const DashboardProvider: FC = ({ children }) => { const { metadata } = useEmbeddedMetadata(); + const { permissions } = useAuthenticated(); const entitlementsQuery = useQuery(entitlements(metadata.entitlements)); 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 || - anyOrganizationPermissionsQuery.error; + organizationsQuery.error; if (error) { return ; @@ -57,8 +52,7 @@ export const DashboardProvider: FC = ({ children }) => { !entitlementsQuery.data || !appearanceQuery.data || !experimentsQuery.data || - !organizationsQuery.data || - !anyOrganizationPermissionsQuery.data; + !organizationsQuery.data; if (isLoading) { return ; @@ -79,8 +73,7 @@ export const DashboardProvider: FC = ({ children }) => { organizations: organizationsQuery.data, showOrganizations, canViewOrganizationSettings: - showOrganizations && - canViewAnyOrganization(anyOrganizationPermissionsQuery.data), + showOrganizations && canViewAnyOrganization(permissions), }} > {children} diff --git a/site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx b/site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx index 746ddc8f89e78..876a3eb441cf1 100644 --- a/site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx +++ b/site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx @@ -82,7 +82,7 @@ const DeploymentDropdownContent: FC = ({ {canViewDeployment && ( diff --git a/site/src/modules/dashboard/Navbar/MobileMenu.tsx b/site/src/modules/dashboard/Navbar/MobileMenu.tsx index 20058335eb8e5..ae5f600ba68de 100644 --- a/site/src/modules/dashboard/Navbar/MobileMenu.tsx +++ b/site/src/modules/dashboard/Navbar/MobileMenu.tsx @@ -220,7 +220,7 @@ const AdminSettingsSub: FC = ({ asChild className={cn(itemStyles.default, itemStyles.sub)} > - Deployment + Deployment )} {canViewOrganizations && ( diff --git a/site/src/modules/dashboard/Navbar/Navbar.tsx b/site/src/modules/dashboard/Navbar/Navbar.tsx index f80887e1f1aec..7dc96c791e7ca 100644 --- a/site/src/modules/dashboard/Navbar/Navbar.tsx +++ b/site/src/modules/dashboard/Navbar/Navbar.tsx @@ -1,6 +1,7 @@ import { buildInfo } from "api/queries/buildInfo"; import { useProxy } from "contexts/ProxyContext"; import { useAuthenticated } from "contexts/auth/RequireAuth"; +import { canViewDeploymentSettings } from "contexts/auth/permissions"; import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata"; import { useDashboard } from "modules/dashboard/useDashboard"; import type { FC } from "react"; @@ -11,16 +12,16 @@ import { NavbarView } from "./NavbarView"; export const Navbar: FC = () => { const { metadata } = useEmbeddedMetadata(); const buildInfoQuery = useQuery(buildInfo(metadata["build-info"])); - const { appearance, canViewOrganizationSettings } = useDashboard(); const { user: me, permissions, signOut } = useAuthenticated(); const featureVisibility = useFeatureVisibility(); + const proxyContextValue = useProxy(); + + const canViewDeployment = canViewDeploymentSettings(permissions); + const canViewOrganizations = canViewOrganizationSettings; + const canViewHealth = permissions.viewDebugInfo; const canViewAuditLog = featureVisibility.audit_log && permissions.viewAnyAuditLog; - const canViewDeployment = permissions.viewDeploymentValues; - const canViewOrganizations = canViewOrganizationSettings; - const proxyContextValue = useProxy(); - const canViewHealth = canViewDeployment; return ( { await userEvent.click(deploymentMenu); const deploymentSettingsLink = await screen.findByText(/deployment/i); - expect(deploymentSettingsLink.href).toContain("/deployment/general"); + expect(deploymentSettingsLink.href).toContain("/deployment"); }); }); diff --git a/site/src/modules/dashboard/Navbar/ProxyMenu.stories.tsx b/site/src/modules/dashboard/Navbar/ProxyMenu.stories.tsx index 883bbd0dd2f61..8e8cf7fcb8951 100644 --- a/site/src/modules/dashboard/Navbar/ProxyMenu.stories.tsx +++ b/site/src/modules/dashboard/Navbar/ProxyMenu.stories.tsx @@ -3,7 +3,7 @@ import { fn, userEvent, within } from "@storybook/test"; import { getAuthorizationKey } from "api/queries/authCheck"; import { getPreferredProxy } from "contexts/ProxyContext"; import { AuthProvider } from "contexts/auth/AuthProvider"; -import { permissionsToCheck } from "contexts/auth/permissions"; +import { permissionChecks } from "contexts/auth/permissions"; import { MockAuthMethodsAll, MockPermissions, @@ -45,7 +45,7 @@ const meta: Meta = { { key: ["authMethods"], data: MockAuthMethodsAll }, { key: ["hasFirstUser"], data: true }, { - key: getAuthorizationKey({ checks: permissionsToCheck }), + key: getAuthorizationKey({ checks: permissionChecks }), data: MockPermissions, }, ], diff --git a/site/src/modules/management/DeploymentSettingsLayout.tsx b/site/src/modules/management/DeploymentSettingsLayout.tsx index 676a24c936246..c40b6440a81c3 100644 --- a/site/src/modules/management/DeploymentSettingsLayout.tsx +++ b/site/src/modules/management/DeploymentSettingsLayout.tsx @@ -8,19 +8,31 @@ import { import { Loader } from "components/Loader/Loader"; import { useAuthenticated } from "contexts/auth/RequireAuth"; import { RequirePermission } from "contexts/auth/RequirePermission"; +import { canViewDeploymentSettings } from "contexts/auth/permissions"; import { type FC, Suspense } from "react"; -import { Outlet } from "react-router-dom"; +import { Navigate, Outlet, useLocation } from "react-router-dom"; import { DeploymentSidebar } from "./DeploymentSidebar"; const DeploymentSettingsLayout: FC = () => { const { permissions } = useAuthenticated(); + const location = useLocation(); - // The deployment settings page also contains users, audit logs, and groups - // so this page must be visible if you can see any of these. - const canViewDeploymentSettingsPage = - permissions.viewDeploymentValues || - permissions.viewAllUsers || - permissions.viewAnyAuditLog; + if (location.pathname === "/deployment") { + return ( + + ); + } + + // The deployment settings page also contains users and groups and more so + // this page must be visible if you can see any of these. + const canViewDeploymentSettingsPage = canViewDeploymentSettings(permissions); return ( diff --git a/site/src/modules/management/DeploymentSettingsProvider.tsx b/site/src/modules/management/DeploymentSettingsProvider.tsx index 633c67d67fe44..766d75aacd216 100644 --- a/site/src/modules/management/DeploymentSettingsProvider.tsx +++ b/site/src/modules/management/DeploymentSettingsProvider.tsx @@ -2,8 +2,6 @@ import type { DeploymentConfig } from "api/api"; import { deploymentConfig } from "api/queries/deployment"; import { ErrorAlert } from "components/Alert/ErrorAlert"; import { Loader } from "components/Loader/Loader"; -import { useAuthenticated } from "contexts/auth/RequireAuth"; -import { RequirePermission } from "contexts/auth/RequirePermission"; import { type FC, createContext, useContext } from "react"; import { useQuery } from "react-query"; import { Outlet } from "react-router-dom"; @@ -28,19 +26,8 @@ export const useDeploymentSettings = (): DeploymentSettingsValue => { }; const DeploymentSettingsProvider: FC = () => { - const { permissions } = useAuthenticated(); const deploymentConfigQuery = useQuery(deploymentConfig()); - // The deployment settings page also contains users, audit logs, and groups - // so this page must be visible if you can see any of these. - const canViewDeploymentSettingsPage = - permissions.viewDeploymentValues || - permissions.viewAllUsers || - permissions.viewAnyAuditLog; - - // Not a huge problem to unload the content in the event of an error, - // because the sidebar rendering isn't tied to this. Even if the user hits - // a 403 error, they'll still have navigation options if (deploymentConfigQuery.error) { return ; } @@ -50,13 +37,11 @@ const DeploymentSettingsProvider: FC = () => { } return ( - - - - - + + + ); }; diff --git a/site/src/modules/management/organizationPermissions.tsx b/site/src/modules/management/organizationPermissions.tsx index 2059d8fd6f76f..1b79e11e68ca0 100644 --- a/site/src/modules/management/organizationPermissions.tsx +++ b/site/src/modules/management/organizationPermissions.tsx @@ -135,65 +135,3 @@ export const canEditOrganization = ( permissions.createOrgRoles) ); }; - -export type AnyOrganizationPermissions = { - [k in AnyOrganizationPermissionName]: boolean; -}; - -export type AnyOrganizationPermissionName = - keyof typeof anyOrganizationPermissionChecks; - -export const anyOrganizationPermissionChecks = { - viewAnyMembers: { - object: { - resource_type: "organization_member", - any_org: true, - }, - action: "read", - }, - editAnyGroups: { - object: { - resource_type: "group", - any_org: true, - }, - action: "update", - }, - assignAnyRoles: { - object: { - resource_type: "assign_org_role", - any_org: true, - }, - action: "assign", - }, - viewAnyIdpSyncSettings: { - object: { - resource_type: "idpsync_settings", - any_org: true, - }, - action: "read", - }, - editAnySettings: { - object: { - resource_type: "organization", - any_org: true, - }, - 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.viewAnyIdpSyncSettings || - permissions.editAnySettings) - ); -}; diff --git a/site/src/pages/TerminalPage/TerminalPage.stories.tsx b/site/src/pages/TerminalPage/TerminalPage.stories.tsx index b9dfeba1d811d..f50b75bac4a26 100644 --- a/site/src/pages/TerminalPage/TerminalPage.stories.tsx +++ b/site/src/pages/TerminalPage/TerminalPage.stories.tsx @@ -1,11 +1,10 @@ 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"; import { RequireAuth } from "contexts/auth/RequireAuth"; -import { permissionsToCheck } from "contexts/auth/permissions"; +import { permissionChecks } from "contexts/auth/permissions"; import { reactRouterOutlet, reactRouterParameters, @@ -74,10 +73,9 @@ const meta = { { key: ["appearance"], data: MockAppearanceConfig }, { key: ["organizations"], data: [MockDefaultOrganization] }, { - key: getAuthorizationKey({ checks: permissionsToCheck }), + key: getAuthorizationKey({ checks: permissionChecks }), data: { editWorkspaceProxies: true }, }, - { key: anyOrganizationPermissionsKey, data: {} }, ], chromatic: { delay: 300 }, }, diff --git a/site/src/router.tsx b/site/src/router.tsx index 66d37f92aeaf1..ebb9e6763d058 100644 --- a/site/src/router.tsx +++ b/site/src/router.tsx @@ -453,8 +453,6 @@ export const router = createBrowserRouter( path="notifications" element={} /> - } /> - } /> @@ -476,6 +474,9 @@ export const router = createBrowserRouter( } /> {groupsRouter()} + + } /> + } /> }> diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 12654bc064fee..aa87ac7fbf6fc 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -2856,6 +2856,41 @@ export const MockPermissions: Permissions = { viewAllLicenses: true, viewNotificationTemplate: true, viewOrganizationIDPSyncSettings: true, + viewDebugInfo: true, + assignAnyRoles: true, + editAnyGroups: true, + editAnySettings: true, + viewAnyIdpSyncSettings: true, + viewAnyMembers: true, +}; + +export const MockNoPermissions: Permissions = { + createTemplates: false, + createUser: false, + deleteTemplates: false, + updateTemplates: false, + viewAllUsers: false, + updateUsers: false, + viewAnyAuditLog: false, + viewDeploymentValues: false, + editDeploymentValues: false, + viewUpdateCheck: false, + viewDeploymentStats: false, + viewExternalAuthConfig: false, + readWorkspaceProxies: false, + editWorkspaceProxies: false, + createOrganization: false, + viewAnyGroup: false, + createGroup: false, + viewAllLicenses: false, + viewNotificationTemplate: false, + viewOrganizationIDPSyncSettings: false, + viewDebugInfo: false, + assignAnyRoles: false, + editAnyGroups: false, + editAnySettings: false, + viewAnyIdpSyncSettings: false, + viewAnyMembers: false, }; export const MockOrganizationPermissions: OrganizationPermissions = { @@ -2890,29 +2925,6 @@ export const MockNoOrganizationPermissions: OrganizationPermissions = { editIdpSyncSettings: false, }; -export const MockNoPermissions: Permissions = { - createTemplates: false, - createUser: false, - deleteTemplates: false, - updateTemplates: false, - viewAllUsers: false, - updateUsers: false, - viewAnyAuditLog: false, - viewDeploymentValues: false, - editDeploymentValues: false, - viewUpdateCheck: false, - viewDeploymentStats: false, - viewExternalAuthConfig: false, - readWorkspaceProxies: false, - editWorkspaceProxies: false, - createOrganization: false, - viewAnyGroup: false, - createGroup: false, - viewAllLicenses: false, - viewNotificationTemplate: false, - viewOrganizationIDPSyncSettings: false, -}; - export const MockDeploymentConfig: DeploymentConfig = { config: { enable_terraform_debug_mode: true, diff --git a/site/src/testHelpers/handlers.ts b/site/src/testHelpers/handlers.ts index b458956b17a1d..71e67697572e2 100644 --- a/site/src/testHelpers/handlers.ts +++ b/site/src/testHelpers/handlers.ts @@ -1,7 +1,7 @@ import fs from "node:fs"; import path from "node:path"; import type { CreateWorkspaceBuildRequest } from "api/typesGenerated"; -import { permissionsToCheck } from "contexts/auth/permissions"; +import { permissionChecks } from "contexts/auth/permissions"; import { http, HttpResponse } from "msw"; import * as M from "./entities"; import { MockGroup, MockWorkspaceQuota } from "./entities"; @@ -173,7 +173,7 @@ export const handlers = [ }), http.post("/api/v2/authcheck", () => { const permissions = [ - ...Object.keys(permissionsToCheck), + ...Object.keys(permissionChecks), "canUpdateTemplate", "updateWorkspace", ]; diff --git a/site/src/testHelpers/storybook.tsx b/site/src/testHelpers/storybook.tsx index 2b81bf16cd40f..fdaeda69f15c1 100644 --- a/site/src/testHelpers/storybook.tsx +++ b/site/src/testHelpers/storybook.tsx @@ -6,7 +6,7 @@ import { hasFirstUserKey, meKey } from "api/queries/users"; import type { Entitlements } from "api/typesGenerated"; import { GlobalSnackbar } from "components/GlobalSnackbar/GlobalSnackbar"; import { AuthProvider } from "contexts/auth/AuthProvider"; -import { permissionsToCheck } from "contexts/auth/permissions"; +import { permissionChecks } from "contexts/auth/permissions"; import { DashboardContext } from "modules/dashboard/DashboardProvider"; import { DeploymentSettingsContext } from "modules/management/DeploymentSettingsProvider"; import { OrganizationSettingsContext } from "modules/management/OrganizationSettingsLayout"; @@ -114,7 +114,7 @@ export const withAuthProvider = (Story: FC, { parameters }: StoryContext) => { queryClient.setQueryData(meKey, parameters.user); queryClient.setQueryData(hasFirstUserKey, true); queryClient.setQueryData( - getAuthorizationKey({ checks: permissionsToCheck }), + getAuthorizationKey({ checks: permissionChecks }), parameters.permissions ?? {}, ); 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