fix(ui): improve profile and role visibility UX (#11956)

This commit is contained in:
Alejandro Bailo
2026-07-13 16:50:43 +02:00
committed by GitHub
parent 0f6137dd04
commit 488f3d1bed
17 changed files with 179 additions and 61 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 372 KiB

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 427 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 255 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 353 KiB

+36 -33
View File
@@ -124,7 +124,7 @@ To resend the invitation to the user, it is necessary to explicitly **delete the
## Managing Groups and Roles
The Roles section in Prowler is designed to facilitate the assignment of custom user privileges. This section allows administrators to define roles with specific permissions for Prowler administrative tasks and Account visibility.
Roles combine administrative permissions with provider visibility. Administrative permissions control the actions a role can perform. Provider Groups and Unlimited Visibility control the providers, resources, findings, scans, and compliance results the role can access.
<Note>
**Only users that have the _Manage Account_ or _admin_ permission can access this section.**
@@ -132,47 +132,53 @@ The Roles section in Prowler is designed to facilitate the assignment of custom
</Note>
### Provider Groups
Provider Groups control visibility across specific providers. When creating a new role, you can assign specific groups to define their Provider visibility. This ensures that users with that role have access only to the Providers that are required.
Provider Groups limit visibility to selected providers. Assigning one or more Provider Groups to a role grants access to the providers in those groups and their resources, findings, scans, and compliance results.
By default, a new user role does not have visibility into any group.
New roles have no provider visibility by default. Assign at least one Provider Group or enable **Unlimited Visibility** before assigning the role to users who need access to provider data.
Alternatively, to grant the role unlimited visibility across all providers, check the Grant Unlimited Visibility checkbox.
**Unlimited Visibility** grants organization-wide visibility across every provider, regardless of the Provider Groups assigned to the role. It does not grant administrative permissions.
#### Creating a Provider Group
Follow these steps to create a provider group in your account:
1. Navigate to **Provider Groups** from the side menu..
1. Click **Providers** in the side menu.
2. In this view you can select the provider groups you want to assign to one or more roles.
2. Select the **Provider Groups** tab.
3. Click the **Create Group** button on the center of the screen.
3. Enter a group name in the **Create a new provider group** form.
<img src="/images/prowler-app/rbac/provider_group.png" alt="Create Provider Group" width="700" />
4. Select the providers that the group controls. Optionally, select the roles that should use the group.
5. Click **Create Group**.
<img src="/images/prowler-app/rbac/provider_group.png" alt="Create a Provider Group" width="700" />
#### Editing a Provider Group
Follow these steps to edit a provider group on your account:
1. Navigate to **Provider Groups** from the side menu.
1. Click **Providers** in the side menu and select the **Provider Groups** tab.
2. Click the edit button of the provider group you want to modify.
2. Open the actions menu for the Provider Group and click **Edit Provider Group**.
<img src="/images/prowler-app/rbac/provider_group_edit.png" alt="Edit Provider Group" width="700" />
<img src="/images/prowler-app/rbac/provider_group_edit.png" alt="Edit Provider Group action" width="300" />
3. Change the provider group parameters you need and save the changes.
3. Update the group name, providers, or roles, and save the changes.
<img src="/images/prowler-app/rbac/provider_group_edit_1.png" alt="Edit Provider Group Details" width="700" />
<img src="/images/prowler-app/rbac/provider_group_edit_1.png" alt="Edit Provider Group form" width="700" />
#### Removing a Provider Group
Follow these steps to remove a provider group of your account:
Follow these steps to remove a provider group from your account:
1. Navigate to **Provider Groups** from the side menu.
1. Click **Providers** in the side menu and select the **Provider Groups** tab.
2. Click the delete button of the provider group you want to remove.
2. Open the actions menu for the Provider Group and click **Delete Provider Group**.
<img src="/images/prowler-app/rbac/provider_group_remove.png" alt="Remove Provider Group" width="700" />
3. Confirm the deletion.
<img src="/images/prowler-app/rbac/provider_group_remove.png" alt="Delete Provider Group confirmation" width="700" />
### Roles
@@ -182,19 +188,20 @@ Follow these steps to create a role for your account:
1. Navigate to **Roles** from the side menu.
2. Click the **Add Role** button on the top right-hand corner of the screen.
2. Click **Add Role**.
<img src="/images/prowler-app/rbac/role_create.png" alt="Create Role" width="700" />
3. Enter the role name and select the required administrative permissions.
3. In the Add Role screen, enter the role name, the administration permissions and the groups of providers to which the Role will have access to.
4. In the Groups and Account Visibility section, you will see a list of available groups with checkboxes next to them. To assign a group to the user role, simply click the checkbox next to the group name. If you need to assign multiple groups, repeat the process for each group you wish to add.
4. Configure **Visibility**:
- To grant organization-wide visibility, select **Enable Unlimited Visibility for this role**.
- To limit visibility, leave Unlimited Visibility cleared and select one or more Provider Groups.
<img src="/images/prowler-app/rbac/role_create_1.png" alt="Role parameters" width="700" />
5. Click **Add Role**.
<Note>
To assign read-only access, select only the `Unlimited Visibility` permission when creating the role. Then, go to the Users page and assign this role to the appropriate user.
To grant read-only access across the organization, enable **Unlimited Visibility** without selecting administrative permissions. Then, assign the role from the **Users** page.
</Note>
#### Editing a Role
@@ -203,25 +210,21 @@ Follow these steps to edit a role on your account:
1. Navigate to **Roles** from the side menu.
2. Click the edit button of the role you want to modify.
2. Open the actions menu for the role and click **Edit Role**.
<img src="/images/prowler-app/rbac/role_edit.png" alt="Edit Role" width="700" />
3. Adjust the settings as needed and save the changes.
<img src="/images/prowler-app/rbac/role_edit_details.png" alt="Edit Role Details" width="700" />
3. Update the role name, administrative permissions, Unlimited Visibility setting, or Provider Groups.
4. Save the changes.
#### Removing a Role
Follow these steps to remove a role of your account:
Follow these steps to remove a role from your account:
1. Navigate to **Roles** from the side menu.
2. Click the delete button of the role you want to remove.
<img src="/images/prowler-app/rbac/role_remove.png" alt="Remove Role" width="700" />
2. Open the actions menu for the role and click **Delete Role**.
3. Confirm the deletion.
## RBAC Administrative Permissions
+4 -2
View File
@@ -9,18 +9,20 @@ describe("profile page layout", () => {
const pagePath = path.join(currentDir, "page.tsx");
const source = readFileSync(pagePath, "utf8");
it("uses one large card with profile sections stacked vertically", () => {
it("places roles before API Keys and exposes its deep-link target", () => {
expect(source).toContain('aria-label="User profile settings"');
expect(source).toContain('className="w-full gap-4 p-4 md:p-5"');
expect(source).toContain('id="api-keys"');
expect(source).not.toContain("xl:grid-cols");
expect(source).not.toContain('className="flex w-full flex-col gap-6"');
const sectionOrder = [
"<UserBasicInfoCard",
"<RolesCard",
'id="api-keys"',
"<ApiKeysCard",
"<SamlIntegrationCard",
"<MembershipsCard",
"<ApiKeysCard",
];
const sectionIndexes = sectionOrder.map((section) =>
+5 -1
View File
@@ -106,6 +106,11 @@ const SSRDataUser = async ({
>
<UserBasicInfoCard user={userData} tenantId={userTenantId || ""} />
<RolesCard roles={roleDetails} roleDetails={roleDetailsMap} />
{hasManageAccount && (
<div id="api-keys" className="scroll-mt-6">
<ApiKeysCard searchParams={searchParams} />
</div>
)}
{hasManageIntegrations && (
<SamlIntegrationCard samlConfig={samlConfig?.data?.[0]} />
)}
@@ -115,7 +120,6 @@ const SSRDataUser = async ({
hasManageAccount={hasManageAccount}
sessionTenantId={session?.tenantId}
/>
{hasManageAccount && <ApiKeysCard searchParams={searchParams} />}
</Card>
);
};
@@ -0,0 +1,16 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { ApiKeyLinkCard } from "./api-key-link-card";
describe("ApiKeyLinkCard", () => {
it("links directly to the API Keys section in the user profile", () => {
// Given / When
render(<ApiKeyLinkCard />);
// Then
expect(
screen.getByRole("link", { name: /go to profile/i }),
).toHaveAttribute("href", "/profile#api-keys");
});
});
@@ -13,7 +13,7 @@ export const ApiKeyLinkCard = () => {
learnMoreUrl="https://docs.prowler.com/user-guide/tutorials/prowler-app-api-keys"
learnMoreAriaLabel="Learn more about API Keys"
bodyText="API Key management is available in your User Profile. Create and manage API keys to authenticate with the Prowler API for automation and integrations."
linkHref="/profile"
linkHref="/profile#api-keys"
linkText="Go to Profile"
/>
);
@@ -137,13 +137,19 @@ describe("AddRoleForm", () => {
// Then
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
expect(
screen.getByText(/tenant-wide visibility setting/i),
).toHaveTextContent(
/grants visibility into every provider, account, resource, finding, scan, and compliance result.*required to use the Jira integration/i,
);
screen.getByText(
"Checking the box below grants visibility into every provider: resources, findings, scans, and compliance results, regardless of the provider groups selected.",
),
).toBeInTheDocument();
expect(
screen.getByText(/required to use the Jira integration/i),
).toHaveProperty("tagName", "STRONG");
expect(
screen.getByRole("link", { name: /learn more about provider groups/i }),
).toHaveAttribute(
"href",
"https://docs.prowler.com/user-guide/tutorials/prowler-app-rbac#provider-groups",
);
expect(
screen.queryByRole("heading", { name: "Unlimited Visibility" }),
).not.toBeInTheDocument();
@@ -154,12 +160,12 @@ describe("AddRoleForm", () => {
).not.toBeInTheDocument();
expect(
screen.queryByText(
/enable it only for roles that need tenant-wide security visibility/i,
/enable it only for roles that need organization-wide security visibility/i,
),
).not.toBeInTheDocument();
expect(
screen.queryByText(
/manage providers enables unlimited visibility in this form because provider administration needs tenant-wide provider-group context/i,
/manage providers enables unlimited visibility in this form because provider administration needs organization-wide provider-group context/i,
),
).not.toBeInTheDocument();
@@ -194,7 +200,7 @@ describe("AddRoleForm", () => {
}),
).toBeChecked();
expect(
screen.getByText(/tenant-wide visibility setting/i),
screen.getByText(/checking the box below grants visibility/i),
).toBeInTheDocument();
expect(screen.queryByTestId("group-select")).not.toBeInTheDocument();
expect(
@@ -131,16 +131,22 @@ describe("EditRoleForm", () => {
// Then
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
expect(
screen.getByText(/tenant-wide visibility setting/i),
).toHaveTextContent(
/grants visibility into every provider, account, resource, finding, scan, and compliance result.*required to use the Jira integration/i,
);
screen.getByText(
"Checking the box below grants visibility into every provider: resources, findings, scans, and compliance results, regardless of the provider groups selected.",
),
).toBeInTheDocument();
expect(
screen.getByText(/required to use the Jira integration/i),
).toHaveProperty("tagName", "STRONG");
expect(
screen.getByRole("link", { name: /learn more about provider groups/i }),
).toHaveAttribute(
"href",
"https://docs.prowler.com/user-guide/tutorials/prowler-app-rbac#provider-groups",
);
expect(
screen.queryByText(
/manage providers enables unlimited visibility in this form because provider administration needs tenant-wide provider-group context/i,
/manage providers enables unlimited visibility in this form because provider administration needs organization-wide provider-group context/i,
),
).not.toBeInTheDocument();
@@ -181,7 +187,7 @@ describe("EditRoleForm", () => {
}),
).toBeChecked();
expect(
screen.getByText(/tenant-wide visibility setting/i),
screen.getByText(/checking the box below grants visibility/i),
).toBeInTheDocument();
expect(screen.queryByTestId("group-select")).not.toBeInTheDocument();
expect(
@@ -2,6 +2,10 @@ import { InfoIcon } from "lucide-react";
import { ReactNode } from "react";
import { Checkbox } from "@/components/shadcn/checkbox/checkbox";
import { CustomLink } from "@/components/shadcn/custom/custom-link";
const PROVIDER_GROUPS_DOCS_URL =
"https://docs.prowler.com/user-guide/tutorials/prowler-app-rbac#provider-groups";
export const UnlimitedVisibilitySection = ({
children,
@@ -15,12 +19,20 @@ export const UnlimitedVisibilitySection = ({
aria-hidden="true"
className="text-bg-data-info mt-0.5 h-4 w-4 shrink-0"
/>
<p>
This is a tenant-wide visibility setting. It grants visibility into
every provider, account, resource, finding, scan, and compliance
result, regardless of the groups selected below. It is also{" "}
<strong>required to use the Jira integration</strong>.
</p>
<div className="flex flex-col gap-1">
<p>
Checking the box below grants visibility into every provider:
resources, findings, scans, and compliance results, regardless of
the provider groups selected.
</p>
<p>
Unlimited Visibility is also{" "}
<strong>required to use the Jira integration</strong>.{" "}
<CustomLink href={PROVIDER_GROUPS_DOCS_URL} size="sm">
Learn more about Provider Groups
</CustomLink>
</p>
</div>
</div>
<div>{children}</div>
</section>
@@ -67,6 +67,23 @@ const memberships = [
},
] satisfies MembershipDetailData[];
const inactiveMembership = {
id: "membership-2",
type: "memberships",
attributes: {
role: "owner",
date_joined: "2026-06-23T10:00:00Z",
},
relationships: {
tenant: {
data: {
type: "tenants",
id: "tenant-2",
},
},
},
} satisfies MembershipDetailData;
const tenantsMap = {
"tenant-1": {
id: "tenant-1",
@@ -83,6 +100,21 @@ const tenantsMap = {
},
},
},
"tenant-2": {
id: "tenant-2",
type: "tenants",
attributes: {
name: "Prowler Sandbox",
},
relationships: {
memberships: {
meta: {
count: 1,
},
data: [],
},
},
},
} satisfies Record<string, TenantDetailData>;
describe("MembershipsCardClient", () => {
@@ -119,6 +151,25 @@ describe("MembershipsCardClient", () => {
expect(cells[2]).toHaveTextContent("Prowler Labs");
});
it("leaves inactive organization status empty", () => {
// Given / When
render(
<MembershipsCardClient
memberships={[...memberships, inactiveMembership]}
tenantsMap={tenantsMap}
hasManageAccount
sessionTenantId="tenant-1"
/>,
);
// Then
const inactiveRow = screen.getByRole("row", {
name: /owner prowler sandbox/i,
});
expect(within(inactiveRow).queryByText("Inactive")).not.toBeInTheDocument();
expect(screen.getByText("Active")).toBeInTheDocument();
});
it("keeps organization edit and delete actions inside the actions menu", async () => {
// Given
const user = userEvent.setup();
@@ -166,9 +166,7 @@ const membershipColumns: ColumnDef<MembershipRow>[] = [
cell: ({ row }) =>
row.original.isActiveTenant ? (
<Badge variant="success">Active</Badge>
) : (
<Badge variant="outline">Inactive</Badge>
),
) : null,
enableSorting: false,
},
{
+21 -1
View File
@@ -1,6 +1,10 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { downloadScanZip, getErrorMessage } from "./helper";
import {
downloadScanZip,
getErrorMessage,
permissionFormFields,
} from "./helper";
vi.mock("@/actions/scans", () => ({
getComplianceCsv: vi.fn(),
@@ -135,3 +139,19 @@ describe("getErrorMessage", () => {
);
});
});
describe("permissionFormFields", () => {
it("describes Unlimited Visibility as organization-wide", () => {
// Given
const field = permissionFormFields.find(
({ field }) => field === "unlimited_visibility",
);
// When
const description = field?.description;
// Then
expect(description).toContain("organization-wide visibility");
expect(description).not.toContain("tenant-wide");
});
});
+1 -1
View File
@@ -438,7 +438,7 @@ export const permissionFormFields: PermissionInfo[] = [
field: "unlimited_visibility",
label: "Unlimited Visibility",
description:
"Grants tenant-wide visibility across all providers, accounts, resources, findings, scans, and compliance results without granting admin actions.",
"Grants organization-wide visibility across all providers, resources, findings, scans, and compliance results without granting admin actions.",
},
{
field: "manage_providers",