fix(ui): improve profile and role visibility UX (#11956)
|
Before Width: | Height: | Size: 372 KiB After Width: | Height: | Size: 256 KiB |
|
Before Width: | Height: | Size: 427 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 255 KiB |
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 257 KiB |
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 353 KiB |
@@ -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
|
||||
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||