feat(ui): add Manage Lighthouse AI role permission (#12412)

This commit is contained in:
Rubén De la Torre Vico
2026-08-12 10:19:20 +02:00
committed by GitHub
parent d41b2eaa0f
commit 68471d2a0e
19 changed files with 350 additions and 10 deletions
+53 -1
View File
@@ -21,7 +21,36 @@ vi.mock("@/lib/sentry-breadcrumbs", () => ({
addAuthEvent: vi.fn(),
}));
import { createNewUser } from "./auth";
import { createNewUser, getUserByMe } from "./auth";
const userMeResponse = (roleAttributes: Record<string, boolean>) => ({
data: {
type: "users",
id: "019b1234-5678-7abc-9def-0123456789ab",
attributes: {
name: "Jane Doe",
email: "jane@example.com",
company_name: "Prowler",
date_joined: "2026-01-01T00:00:00.000Z",
},
},
included: [
{
type: "roles",
id: "role-1",
attributes: { name: "Cloud admin", ...roleAttributes },
},
],
});
const mockUserMe = (roleAttributes: Record<string, boolean>) => {
fetchMock.mockResolvedValue(
new Response(JSON.stringify(userMeResponse(roleAttributes)), {
status: 200,
headers: { "Content-Type": "application/json" },
}),
);
};
describe("auth actions", () => {
beforeEach(() => {
@@ -102,4 +131,27 @@ describe("auth actions", () => {
expect(requestUrl.searchParams.get("promo_code")).toBe("black-hat-2026");
expect(requestUrl.searchParams.get("utm_source")).toBe("blackhat");
});
it("should carry manage_lighthouse_ai_configuration into the session permissions", async () => {
// Given
mockUserMe({ manage_lighthouse_ai_configuration: true });
// When
const result = await getUserByMe("access-token");
// Then
expect(result.permissions.manage_lighthouse_ai_configuration).toBe(true);
});
it("should default manage_lighthouse_ai_configuration to false when the role omits it", async () => {
// Given
mockUserMe({ manage_users: true });
// When
const result = await getUserByMe("access-token");
// Then
expect(result.permissions.manage_lighthouse_ai_configuration).toBe(false);
expect(result.permissions.manage_users).toBe(true);
});
});
+2
View File
@@ -181,6 +181,8 @@ export const getUserByMe = async (accessToken: string) => {
manage_integrations: userRole.attributes.manage_integrations || false,
manage_billing: userRole.attributes.manage_billing || false,
manage_alerts: userRole.attributes.manage_alerts || false,
manage_lighthouse_ai_configuration:
userRole.attributes.manage_lighthouse_ai_configuration || false,
unlimited_visibility: userRole.attributes.unlimited_visibility || false,
};
+53
View File
@@ -49,6 +49,7 @@ const makeRoleFormData = () => {
formData.set("manage_integrations", "false");
formData.set("manage_scans", "false");
formData.set("manage_alerts", "true");
formData.set("manage_lighthouse_ai_configuration", "true");
formData.set("unlimited_visibility", "false");
return formData;
};
@@ -106,4 +107,56 @@ describe("role actions", () => {
// Then
expect(lastRequestBody().data.attributes.manage_alerts).toBe(true);
});
it("includes manage_lighthouse_ai_configuration when creating a role in Prowler Cloud", async () => {
// Given
vi.stubEnv("UI_CLOUD_ENABLED", "true");
// When
await addRole(makeRoleFormData());
// Then
expect(
lastRequestBody().data.attributes.manage_lighthouse_ai_configuration,
).toBe(true);
});
it("omits manage_lighthouse_ai_configuration when creating a role outside Prowler Cloud", async () => {
// Given
vi.stubEnv("UI_CLOUD_ENABLED", "false");
// When
await addRole(makeRoleFormData());
// Then
expect(lastRequestBody().data.attributes).not.toHaveProperty(
"manage_lighthouse_ai_configuration",
);
});
it("includes manage_lighthouse_ai_configuration when updating a role in Prowler Cloud", async () => {
// Given
vi.stubEnv("UI_CLOUD_ENABLED", "true");
// When
await updateRole(makeRoleFormData(), "role-1");
// Then
expect(
lastRequestBody().data.attributes.manage_lighthouse_ai_configuration,
).toBe(true);
});
it("omits manage_lighthouse_ai_configuration when updating a role outside Prowler Cloud", async () => {
// Given
vi.stubEnv("UI_CLOUD_ENABLED", "false");
// When
await updateRole(makeRoleFormData(), "role-1");
// Then
expect(lastRequestBody().data.attributes).not.toHaveProperty(
"manage_lighthouse_ai_configuration",
);
});
});
+4
View File
@@ -114,6 +114,8 @@ export const addRole = async (formData: FormData) => {
formData.get("manage_billing") === "true";
payload.data.attributes.manage_alerts =
formData.get("manage_alerts") === "true";
payload.data.attributes.manage_lighthouse_ai_configuration =
formData.get("manage_lighthouse_ai_configuration") === "true";
}
// Add provider groups relationships only if there are items
@@ -171,6 +173,8 @@ export const updateRole = async (formData: FormData, roleId: string) => {
formData.get("manage_billing") === "true";
payload.data.attributes.manage_alerts =
formData.get("manage_alerts") === "true";
payload.data.attributes.manage_lighthouse_ai_configuration =
formData.get("manage_lighthouse_ai_configuration") === "true";
}
// Add provider groups relationships only if there are items
+1
View File
@@ -54,6 +54,7 @@ const DEFAULT_PERMISSIONS: RolePermissionAttributes = {
manage_integrations: false,
manage_billing: false,
manage_alerts: false,
manage_lighthouse_ai_configuration: false,
unlimited_visibility: false,
};
@@ -0,0 +1 @@
Manage Lighthouse AI role permission in the role forms and role details, so permission to change the Lighthouse AI configuration can be granted or restricted independently of other permissions (Prowler Cloud only)
@@ -2,6 +2,8 @@ import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { addRole } from "@/actions/roles/roles";
import { AddRoleForm } from "./add-role-form";
const routerMocks = vi.hoisted(() => ({
@@ -60,6 +62,12 @@ vi.mock("@/lib", () => ({
label: "Manage Alerts",
description: "Allows creating and managing custom alerts",
},
{
field: "manage_lighthouse_ai_configuration",
label: "Manage Lighthouse AI",
description:
"Allows configuring Lighthouse AI, including its provider credentials, default model and business context",
},
{
field: "manage_billing",
label: "Manage Billing",
@@ -88,9 +96,31 @@ beforeAll(() => {
window.ResizeObserver = ResizeObserverMock;
});
const submitRoleForm = async (
user: ReturnType<typeof userEvent.setup>,
{ grantLighthouseAi }: { grantLighthouseAi: boolean },
) => {
await user.type(screen.getByPlaceholderText("Enter role name"), "New role");
if (grantLighthouseAi) {
await user.click(
screen.getByRole("checkbox", { name: "Manage Lighthouse AI" }),
);
}
await user.click(screen.getByRole("button", { name: "Add Role" }));
};
const submittedFormData = () => {
const formData = vi.mocked(addRole).mock.calls.at(-1)?.[0];
if (!formData) throw new Error("addRole was not called");
return formData;
};
describe("AddRoleForm", () => {
afterEach(() => {
routerMocks.push.mockClear();
vi.mocked(addRole).mockClear();
vi.unstubAllEnvs();
});
@@ -103,6 +133,7 @@ describe("AddRoleForm", () => {
// Then
expect(screen.getByText("Manage Alerts")).toBeInTheDocument();
expect(screen.getByText("Manage Lighthouse AI")).toBeInTheDocument();
expect(screen.getByText("Manage Billing")).toBeInTheDocument();
});
@@ -115,9 +146,55 @@ describe("AddRoleForm", () => {
// Then
expect(screen.queryByText("Manage Alerts")).not.toBeInTheDocument();
expect(screen.queryByText("Manage Lighthouse AI")).not.toBeInTheDocument();
expect(screen.queryByText("Manage Billing")).not.toBeInTheDocument();
});
it("submits manage_lighthouse_ai_configuration when granted in Prowler Cloud", async () => {
// Given
vi.stubEnv("UI_CLOUD_ENABLED", "true");
const user = userEvent.setup();
render(<AddRoleForm groups={[]} />);
// When
await submitRoleForm(user, { grantLighthouseAi: true });
// Then
expect(submittedFormData().get("manage_lighthouse_ai_configuration")).toBe(
"true",
);
});
it("submits manage_lighthouse_ai_configuration as false when not granted in Prowler Cloud", async () => {
// Given
vi.stubEnv("UI_CLOUD_ENABLED", "true");
const user = userEvent.setup();
render(<AddRoleForm groups={[]} />);
// When
await submitRoleForm(user, { grantLighthouseAi: false });
// Then
expect(submittedFormData().get("manage_lighthouse_ai_configuration")).toBe(
"false",
);
});
it("omits manage_lighthouse_ai_configuration from the submission outside Prowler Cloud", async () => {
// Given
vi.stubEnv("UI_CLOUD_ENABLED", "false");
const user = userEvent.setup();
render(<AddRoleForm groups={[]} />);
// When
await submitRoleForm(user, { grantLighthouseAi: false });
// Then
expect(submittedFormData().has("manage_lighthouse_ai_configuration")).toBe(
false,
);
});
it("navigates back to roles when cancel is clicked", async () => {
// Given
const user = userEvent.setup();
@@ -27,6 +27,7 @@ export const AddRoleForm = ({ groups }: { groups: RoleGroupOption[] }) => {
...(isCloudEnvironment && {
manage_billing: false,
manage_alerts: false,
manage_lighthouse_ai_configuration: false,
}),
};
@@ -51,6 +52,10 @@ export const AddRoleForm = ({ groups }: { groups: RoleGroupOption[] }) => {
if (isCloudEnvironment) {
formData.append("manage_billing", String(values.manage_billing));
formData.append("manage_alerts", String(values.manage_alerts));
formData.append(
"manage_lighthouse_ai_configuration",
String(values.manage_lighthouse_ai_configuration),
);
}
if (values.groups && values.groups.length > 0) {
@@ -2,6 +2,8 @@ import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { updateRole } from "@/actions/roles/roles";
import { EditRoleForm } from "./edit-role-form";
const routerMocks = vi.hoisted(() => ({
@@ -60,6 +62,12 @@ vi.mock("@/lib", () => ({
label: "Manage Alerts",
description: "Allows creating and managing custom alerts",
},
{
field: "manage_lighthouse_ai_configuration",
label: "Manage Lighthouse AI",
description:
"Allows configuring Lighthouse AI, including its provider credentials, default model and business context",
},
{
field: "manage_billing",
label: "Manage Billing",
@@ -118,12 +126,68 @@ const renderEditRoleForm = (options?: Parameters<typeof roleData>[0]) =>
<EditRoleForm roleId="role-1" roleData={roleData(options)} groups={[]} />,
);
const submittedFormData = () => {
const formData = vi.mocked(updateRole).mock.calls.at(-1)?.[0];
if (!formData) throw new Error("updateRole was not called");
return formData;
};
describe("EditRoleForm", () => {
afterEach(() => {
routerMocks.push.mockClear();
vi.mocked(updateRole).mockClear();
vi.unstubAllEnvs();
});
it("submits manage_lighthouse_ai_configuration when granted in Prowler Cloud", async () => {
// Given
vi.stubEnv("UI_CLOUD_ENABLED", "true");
const user = userEvent.setup();
renderEditRoleForm();
// When
await user.click(
screen.getByRole("checkbox", { name: "Manage Lighthouse AI" }),
);
await user.click(screen.getByRole("button", { name: "Update Role" }));
// Then
expect(submittedFormData().get("manage_lighthouse_ai_configuration")).toBe(
"true",
);
expect(vi.mocked(updateRole).mock.calls.at(-1)?.[1]).toBe("role-1");
});
it("submits manage_lighthouse_ai_configuration as false when not granted in Prowler Cloud", async () => {
// Given
vi.stubEnv("UI_CLOUD_ENABLED", "true");
const user = userEvent.setup();
renderEditRoleForm();
// When
await user.click(screen.getByRole("button", { name: "Update Role" }));
// Then
expect(submittedFormData().get("manage_lighthouse_ai_configuration")).toBe(
"false",
);
});
it("omits manage_lighthouse_ai_configuration from the submission outside Prowler Cloud", async () => {
// Given
vi.stubEnv("UI_CLOUD_ENABLED", "false");
const user = userEvent.setup();
renderEditRoleForm();
// When
await user.click(screen.getByRole("button", { name: "Update Role" }));
// Then
expect(submittedFormData().has("manage_lighthouse_ai_configuration")).toBe(
false,
);
});
it("shows the subtle Unlimited Visibility description inside Visibility", () => {
// Given / When
renderEditRoleForm();
@@ -60,6 +60,8 @@ export const EditRoleForm = ({
if (isCloudEnvironment) {
updatedFields.manage_billing = values.manage_billing;
updatedFields.manage_alerts = values.manage_alerts;
updatedFields.manage_lighthouse_ai_configuration =
values.manage_lighthouse_ai_configuration;
}
if (
@@ -23,6 +23,7 @@ const roleDetail = {
manage_integrations: false,
manage_billing: false,
manage_alerts: true,
manage_lighthouse_ai_configuration: true,
unlimited_visibility: false,
},
} satisfies RoleDetail;
@@ -54,6 +55,28 @@ describe("RoleItem", () => {
expect(screen.queryByText("Manage Alerts")).not.toBeInTheDocument();
});
it("shows Manage Lighthouse AI in Prowler Cloud role details", () => {
// Given
vi.stubEnv("UI_CLOUD_ENABLED", "true");
// When
render(<RoleItem role={role} roleDetail={roleDetail} />);
// Then
expect(screen.getByText("Manage Lighthouse AI")).toBeInTheDocument();
});
it("hides Manage Lighthouse AI outside Prowler Cloud role details", () => {
// Given
vi.stubEnv("UI_CLOUD_ENABLED", "false");
// When
render(<RoleItem role={role} roleDetail={roleDetail} />);
// Then
expect(screen.queryByText("Manage Lighthouse AI")).not.toBeInTheDocument();
});
it("displays the permission state as a badge", () => {
// Given
vi.stubEnv("UI_CLOUD_ENABLED", "true");
+1
View File
@@ -14,6 +14,7 @@ export function useAuth() {
manage_integrations: false,
manage_billing: false,
manage_alerts: false,
manage_lighthouse_ai_configuration: false,
unlimited_visibility: false,
};
+6
View File
@@ -469,6 +469,12 @@ export const permissionFormFields: PermissionInfo[] = [
label: "Manage Alerts",
description: "Allows creating and managing custom alerts",
},
{
field: "manage_lighthouse_ai_configuration",
label: "Manage Lighthouse AI",
description:
"Allows configuring Lighthouse AI, including its provider credentials, default model and business context",
},
{
field: "manage_billing",
+31
View File
@@ -12,6 +12,7 @@ const attributes = {
manage_integrations: false,
manage_billing: false,
manage_alerts: true,
manage_lighthouse_ai_configuration: true,
unlimited_visibility: false,
} satisfies RolePermissionAttributes;
@@ -47,4 +48,34 @@ describe("getRolePermissions", () => {
permissions.some((permission) => permission.key === "manage_alerts"),
).toBe(false);
});
it("includes Manage Lighthouse AI in Prowler Cloud when role attributes provide it", () => {
// Given
vi.stubEnv("UI_CLOUD_ENABLED", "true");
// When
const permissions = getRolePermissions(attributes);
// Then
expect(permissions).toContainEqual({
key: "manage_lighthouse_ai_configuration",
label: "Manage Lighthouse AI",
enabled: true,
});
});
it("hides Manage Lighthouse AI outside Prowler Cloud", () => {
// Given
vi.stubEnv("UI_CLOUD_ENABLED", "false");
// When
const permissions = getRolePermissions(attributes);
// Then
expect(
permissions.some(
(permission) => permission.key === "manage_lighthouse_ai_configuration",
),
).toBe(false);
});
});
+5
View File
@@ -67,6 +67,11 @@ export const getRolePermissions = (attributes: RolePermissionAttributes) => {
label: "Manage Alerts",
enabled: attributes.manage_alerts ?? false,
},
{
key: "manage_lighthouse_ai_configuration",
label: "Manage Lighthouse AI",
enabled: attributes.manage_lighthouse_ai_configuration ?? false,
},
]
: []),
{
+5 -1
View File
@@ -1,6 +1,10 @@
import { permissionFormFields } from "@/lib";
const hiddenOutsideCloudFields = ["manage_billing", "manage_alerts"];
const hiddenOutsideCloudFields = [
"manage_billing",
"manage_alerts",
"manage_lighthouse_ai_configuration",
];
export const getVisiblePermissionFormFields = (isCloudEnvironment: boolean) =>
permissionFormFields.filter(
+2
View File
@@ -419,6 +419,7 @@ export interface InvitationProps {
manage_integrations?: boolean;
manage_scans?: boolean;
manage_alerts?: boolean;
manage_lighthouse_ai_configuration?: boolean;
permission_state?: PermissionState;
};
};
@@ -444,6 +445,7 @@ export interface Role {
manage_integrations: boolean;
manage_scans: boolean;
manage_alerts?: boolean;
manage_lighthouse_ai_configuration?: boolean;
unlimited_visibility: boolean;
permission_state: PermissionState;
inserted_at: string;
+1
View File
@@ -55,6 +55,7 @@ export const roleFormSchema = z.object({
manage_integrations: z.boolean().default(false),
manage_scans: z.boolean().default(false),
manage_alerts: z.boolean().default(false),
manage_lighthouse_ai_configuration: z.boolean().default(false),
unlimited_visibility: z.boolean().default(false),
groups: z.array(z.string()).optional(),
});
+14 -8
View File
@@ -82,15 +82,20 @@ export interface RoleData {
id: string;
}
export const PERMISSION_KEY = {
MANAGE_USERS: "manage_users",
MANAGE_ACCOUNT: "manage_account",
MANAGE_PROVIDERS: "manage_providers",
MANAGE_SCANS: "manage_scans",
MANAGE_INTEGRATIONS: "manage_integrations",
MANAGE_BILLING: "manage_billing",
MANAGE_ALERTS: "manage_alerts",
MANAGE_LIGHTHOUSE_AI_CONFIGURATION: "manage_lighthouse_ai_configuration",
UNLIMITED_VISIBILITY: "unlimited_visibility",
} as const;
export type PermissionKey =
| "manage_users"
| "manage_account"
| "manage_providers"
| "manage_scans"
| "manage_integrations"
| "manage_billing"
| "manage_alerts"
| "unlimited_visibility";
(typeof PERMISSION_KEY)[keyof typeof PERMISSION_KEY];
export type RolePermissionAttributes = Pick<
RoleDetail["attributes"],
@@ -117,6 +122,7 @@ export interface RoleDetail {
manage_integrations: boolean;
manage_billing?: boolean;
manage_alerts?: boolean;
manage_lighthouse_ai_configuration?: boolean;
unlimited_visibility: boolean;
permission_state?: string;
inserted_at?: string;