Skip to content

chore: perform several small frontend permissions refactors #16735

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Mar 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions enterprise/coderd/groups.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,6 @@ func (api *API) patchGroup(rw http.ResponseWriter, r *http.Request) {
})
return
}
// TODO: It would be nice to enforce this at the schema level
// but unfortunately our org_members table does not have an ID.
_, err := database.ExpectOne(api.Database.OrganizationMembers(ctx, database.OrganizationMembersParams{
OrganizationID: group.OrganizationID,
UserID: uuid.MustParse(id),
Expand Down
8 changes: 4 additions & 4 deletions site/e2e/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ export const defaultPassword = "SomeSecurePassword!";

// Credentials for users
export const users = {
admin: {
username: "admin",
owner: {
username: "owner",
password: defaultPassword,
email: "admin@coder.com",
email: "owner@coder.com",
},
templateAdmin: {
username: "template-admin",
Expand All @@ -41,7 +41,7 @@ export const users = {
username: "auditor",
password: defaultPassword,
email: "auditor@coder.com",
roles: ["Template Admin", "Auditor"],
roles: ["Auditor"],
},
member: {
username: "member",
Expand Down
2 changes: 1 addition & 1 deletion site/e2e/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export type LoginOptions = {
password: string;
};

export async function login(page: Page, options: LoginOptions = users.admin) {
export async function login(page: Page, options: LoginOptions = users.owner) {
const ctx = page.context();
// biome-ignore lint/suspicious/noExplicitAny: reset the current user
(ctx as any)[Symbol.for("currentUser")] = undefined;
Expand Down
6 changes: 3 additions & 3 deletions site/e2e/setup/addUsersAndLicense.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,16 @@ test("setup deployment", async ({ page }) => {
}

// Setup first user
await page.getByLabel(Language.emailLabel).fill(users.admin.email);
await page.getByLabel(Language.passwordLabel).fill(users.admin.password);
await page.getByLabel(Language.emailLabel).fill(users.owner.email);
await page.getByLabel(Language.passwordLabel).fill(users.owner.password);
await page.getByTestId("create").click();

await expectUrl(page).toHavePathName("/workspaces");
await page.getByTestId("button-select-template").isVisible();

for (const user of Object.values(users)) {
// Already created as first user
if (user.username === "admin") {
if (user.username === "owner") {
continue;
}

Expand Down
40 changes: 21 additions & 19 deletions site/e2e/tests/auditLogs.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,62 +13,63 @@ test.describe.configure({ mode: "parallel" });

test.beforeEach(async ({ page }) => {
beforeCoderTest(page);
await login(page, users.auditor);
});

async function resetSearch(page: Page) {
async function resetSearch(page: Page, username: string) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I think a better name for this function could be filterByUser or searchByUser.

const clearButton = page.getByLabel("Clear search");
if (await clearButton.isVisible()) {
await clearButton.click();
}

// Filter by the auditor test user to prevent race conditions
const user = currentUser(page);
await expect(page.getByText("All users")).toBeVisible();
await page.getByPlaceholder("Search...").fill(`username:${user.username}`);
await page.getByPlaceholder("Search...").fill(`username:${username}`);
await expect(page.getByText("All users")).not.toBeVisible();
}

test("logins are logged", async ({ page }) => {
requiresLicense();

// Go to the audit history
await login(page, users.auditor);
await page.goto("/audit");
const username = users.auditor.username;

const user = currentUser(page);
const loginMessage = `${user.username} logged in`;
const loginMessage = `${username} logged in`;
// Make sure those things we did all actually show up
await resetSearch(page);
await resetSearch(page, username);
await expect(page.getByText(loginMessage).first()).toBeVisible();
});

test("creating templates and workspaces is logged", async ({ page }) => {
requiresLicense();

// Do some stuff that should show up in the audit logs
await login(page, users.templateAdmin);
const username = users.templateAdmin.username;
const templateName = await createTemplate(page);
const workspaceName = await createWorkspace(page, templateName);

// Go to the audit history
await login(page, users.auditor);
await page.goto("/audit");

const user = currentUser(page);

// Make sure those things we did all actually show up
await resetSearch(page);
await resetSearch(page, username);
await expect(
page.getByText(`${user.username} created template ${templateName}`),
page.getByText(`${username} created template ${templateName}`),
).toBeVisible();
await expect(
page.getByText(`${user.username} created workspace ${workspaceName}`),
page.getByText(`${username} created workspace ${workspaceName}`),
).toBeVisible();
await expect(
page.getByText(`${user.username} started workspace ${workspaceName}`),
page.getByText(`${username} started workspace ${workspaceName}`),
).toBeVisible();

// Make sure we can inspect the details of the log item
const createdWorkspace = page.locator(".MuiTableRow-root", {
hasText: `${user.username} created workspace ${workspaceName}`,
hasText: `${username} created workspace ${workspaceName}`,
});
await createdWorkspace.getByLabel("open-dropdown").click();
await expect(
Expand All @@ -83,18 +84,19 @@ test("inspecting and filtering audit logs", async ({ page }) => {
requiresLicense();

// Do some stuff that should show up in the audit logs
await login(page, users.templateAdmin);
const username = users.templateAdmin.username;
const templateName = await createTemplate(page);
const workspaceName = await createWorkspace(page, templateName);

// Go to the audit history
await login(page, users.auditor);
await page.goto("/audit");

const user = currentUser(page);
const loginMessage = `${user.username} logged in`;
const startedWorkspaceMessage = `${user.username} started workspace ${workspaceName}`;
const loginMessage = `${username} logged in`;
const startedWorkspaceMessage = `${username} started workspace ${workspaceName}`;

// Filter by resource type
await resetSearch(page);
await resetSearch(page, username);
await page.getByText("All resource types").click();
const workspaceBuildsOption = page.getByText("Workspace Build");
await workspaceBuildsOption.scrollIntoViewIfNeeded({ timeout: 5000 });
Expand All @@ -107,7 +109,7 @@ test("inspecting and filtering audit logs", async ({ page }) => {
await expect(page.getByText("All resource types")).toBeVisible();

// Filter by action type
await resetSearch(page);
await resetSearch(page, username);
await page.getByText("All actions").click();
await page.getByText("Login", { exact: true }).click();
// Logins should be visible
Expand Down
2 changes: 1 addition & 1 deletion site/e2e/tests/deployment/general.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ test("experiments", async ({ page }) => {
const availableExperiments = await API.getAvailableExperiments();

// Verify if the site lists the same experiments
await page.goto("/deployment/general", { waitUntil: "networkidle" });
await page.goto("/deployment/overview", { waitUntil: "domcontentloaded" });

const experimentsLocator = page.locator(
"div.options-table tr.option-experiments ul.option-array",
Expand Down
4 changes: 2 additions & 2 deletions site/e2e/tests/roles.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ test.describe("roles admin settings access", () => {
]);
});

test("admin can see admin settings", async ({ page }) => {
await login(page, users.admin);
test("owner can see admin settings", async ({ page }) => {
await login(page, users.owner);
await page.goto("/", { waitUntil: "domcontentloaded" });

await hasAccessToAdminSettings(page, [
Expand Down
2 changes: 1 addition & 1 deletion site/src/@types/storybook.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type {
SerpentOption,
User,
} from "api/typesGenerated";
import type { Permissions } from "contexts/auth/permissions";
import type { Permissions } from "modules/permissions";
import type { QueryKey } from "react-query";

declare module "@storybook/react" {
Expand Down
2 changes: 1 addition & 1 deletion site/src/api/queries/organizations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
type OrganizationPermissionName,
type OrganizationPermissions,
organizationPermissionChecks,
} from "modules/management/organizationPermissions";
} from "modules/permissions/organizations";
import type { QueryClient } from "react-query";
import { meKey } from "./users";

Expand Down
2 changes: 1 addition & 1 deletion site/src/contexts/auth/AuthProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import type { UpdateUserProfileRequest, User } from "api/typesGenerated";
import { displaySuccess } from "components/GlobalSnackbar/utils";
import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata";
import { type Permissions, permissionChecks } from "modules/permissions";
import {
type FC,
type PropsWithChildren,
Expand All @@ -18,7 +19,6 @@ import {
useContext,
} from "react";
import { useMutation, useQuery, useQueryClient } from "react-query";
import { type Permissions, permissionChecks } from "./permissions";

export type AuthContextValue = {
isLoading: boolean;
Expand Down
4 changes: 2 additions & 2 deletions site/src/modules/dashboard/DashboardLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import { useUpdateCheck } from "./useUpdateCheck";

export const DashboardLayout: FC = () => {
const { permissions } = useAuthenticated();
const updateCheck = useUpdateCheck(permissions.viewUpdateCheck);
const canViewDeployment = Boolean(permissions.viewDeploymentValues);
const updateCheck = useUpdateCheck(permissions.viewDeploymentConfig);
const canViewDeployment = Boolean(permissions.viewDeploymentConfig);

return (
<>
Expand Down
2 changes: 1 addition & 1 deletion site/src/modules/dashboard/DashboardProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import type {
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/permissions";
import { type FC, type PropsWithChildren, createContext } from "react";
import { useQuery } from "react-query";
import { selectFeatureVisibility } from "./entitlements";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ export const DeploymentBanner: FC = () => {
const deploymentStatsQuery = useQuery(deploymentStats());
const healthQuery = useQuery({
...health(),
enabled: permissions.viewDeploymentValues,
enabled: permissions.viewDeploymentConfig,
});

if (!permissions.viewDeploymentValues || !deploymentStatsQuery.data) {
if (!permissions.viewDeploymentConfig || !deploymentStatsQuery.data) {
return null;
}

Expand Down
2 changes: 1 addition & 1 deletion site/src/modules/dashboard/Navbar/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
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 { canViewDeploymentSettings } from "modules/permissions";
import type { FC } from "react";
import { useQuery } from "react-query";
import { useFeatureVisibility } from "../useFeatureVisibility";
Expand Down
2 changes: 1 addition & 1 deletion site/src/modules/dashboard/Navbar/ProxyMenu.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 { permissionChecks } from "contexts/auth/permissions";
import { permissionChecks } from "modules/permissions";
import {
MockAuthMethodsAll,
MockPermissions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,26 @@ import { type FC, createContext, useContext } from "react";
import { useQuery } from "react-query";
import { Outlet } from "react-router-dom";

export const DeploymentSettingsContext = createContext<
DeploymentSettingsValue | undefined
export const DeploymentConfigContext = createContext<
DeploymentConfigValue | undefined
>(undefined);

type DeploymentSettingsValue = Readonly<{
type DeploymentConfigValue = Readonly<{
deploymentConfig: DeploymentConfig;
}>;

export const useDeploymentSettings = (): DeploymentSettingsValue => {
const context = useContext(DeploymentSettingsContext);
export const useDeploymentConfig = (): DeploymentConfigValue => {
const context = useContext(DeploymentConfigContext);
if (!context) {
throw new Error(
`${useDeploymentSettings.name} should be used inside of ${DeploymentSettingsProvider.name}`,
`${useDeploymentConfig.name} should be used inside of ${DeploymentConfigProvider.name}`,
);
}

return context;
};

const DeploymentSettingsProvider: FC = () => {
const DeploymentConfigProvider: FC = () => {
const deploymentConfigQuery = useQuery(deploymentConfig());

if (deploymentConfigQuery.error) {
Expand All @@ -37,12 +37,12 @@ const DeploymentSettingsProvider: FC = () => {
}

return (
<DeploymentSettingsContext.Provider
<DeploymentConfigContext.Provider
value={{ deploymentConfig: deploymentConfigQuery.data }}
>
<Outlet />
</DeploymentSettingsContext.Provider>
</DeploymentConfigContext.Provider>
);
};

export default DeploymentSettingsProvider;
export default DeploymentConfigProvider;
8 changes: 4 additions & 4 deletions site/src/modules/management/DeploymentSettingsLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import {
} from "components/Breadcrumb/Breadcrumb";
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 { canViewDeploymentSettings } from "modules/permissions";
import { RequirePermission } from "modules/permissions/RequirePermission";
import { type FC, Suspense } from "react";
import { Navigate, Outlet, useLocation } from "react-router-dom";
import { DeploymentSidebar } from "./DeploymentSidebar";
Expand All @@ -21,8 +21,8 @@ const DeploymentSettingsLayout: FC = () => {
return (
<Navigate
to={
permissions.viewDeploymentValues
? "/deployment/general"
permissions.viewDeploymentConfig
? "/deployment/overview"
: "/deployment/users"
}
replace
Expand Down
4 changes: 2 additions & 2 deletions site/src/modules/management/DeploymentSidebarView.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ export const NoDeploymentValues: Story = {
args: {
permissions: {
...MockPermissions,
viewDeploymentValues: false,
editDeploymentValues: false,
viewDeploymentConfig: false,
editDeploymentConfig: false,
},
},
};
Expand Down
Loading
Loading