Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,11 @@ export const RoleCreateDialog = ({ open, onOpenChange }: RoleCreateDialogProps)
<Button onClick={handleClose} variant="outline">
Cancel
</Button>
<Button disabled={!trimmed || (submitted && alreadyExists) || isSubmitting} onClick={handleSubmit}>
<Button
data-testid="create-role-submit"
disabled={!trimmed || (submitted && alreadyExists) || isSubmitting}
onClick={handleSubmit}
>
Create
</Button>
</DialogFooter>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,10 @@ export const RoleDetailPageNew = () => {
className="w-56"
clearable={false}
disabled={isSubmitting}
inputTestId="add-principal-combobox"
onChange={addMember}
options={availablePrincipalOptions}
placeholder="Add a principal..."
testId="add-principal-combobox"
value=""
/>
}
Expand Down
9 changes: 7 additions & 2 deletions frontend/src/components/pages/security/shared/acls-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ export const AclsCard = ({ acls, principal, isLoading }: AclsCardProps) => {
</Button>
)}
{principal && (
<Button onClick={() => setGrantAllOpen(true)} variant="outline">
<Button onClick={() => setGrantAllOpen(true)} testId="allow-all-operations-button" variant="outline">
Allow all operations
</Button>
)}
Expand Down Expand Up @@ -374,7 +374,12 @@ export const AclsCard = ({ acls, principal, isLoading }: AclsCardProps) => {
<Button onClick={() => setGrantAllOpen(false)} type="button" variant="outline">
Cancel
</Button>
<Button disabled={isGranting} onClick={confirmGrantAllPermissions} type="button">
<Button
disabled={isGranting}
onClick={confirmGrantAllPermissions}
testId="confirm-allow-all-button"
type="button"
>
Confirm
</Button>
</DialogFooter>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -556,12 +556,18 @@ const UserActions = ({ user }: { user: PrincipalEntry }) => {

<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button className="deleteButton" size="icon-sm" variant="ghost">
<Button
className="deleteButton"
data-testid={`user-actions-button-${user.name}`}
size="icon-sm"
variant="ghost"
>
<MoreHorizontalIcon className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
data-testid={`user-change-password-menu-item-${user.name}`}
onClick={(e) => {
e.stopPropagation();
setIsChangePasswordModalOpen(true);
Expand All @@ -571,6 +577,7 @@ const UserActions = ({ user }: { user: PrincipalEntry }) => {
Change password
</DropdownMenuItem>
<DropdownMenuItem
data-testid={`user-delete-menu-item-${user.name}`}
onClick={(e) => {
e.stopPropagation();
setIsDeleteModalOpen(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,7 @@ export const AddAclDialog = ({ open, onOpenChange, principal }: AddAclDialogProp
<Button onClick={handleClose} type="button" variant="outline">
Cancel
</Button>
<Button disabled={isPending} form="add-acl-form" type="submit">
<Button data-testid="add-acl-submit-button" disabled={isPending} form="add-acl-form" type="submit">
Add ACL
</Button>
</DialogFooter>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,16 @@ export const UserDetailsPageNew = ({ userName }: UserDetailsPageProps) => {
</div>

<div className="flex items-center gap-2">
<Button onClick={() => setIsChangePasswordModalOpen(true)} variant="outline">
<Button
data-testid="user-change-password-button"
onClick={() => setIsChangePasswordModalOpen(true)}
variant="outline"
>
<KeyRoundIcon />
Change Password
</Button>
{Boolean(isServiceAccount) && (
<Button onClick={() => setIsDeleteModalOpen(true)} variant="destructive">
<Button data-testid="user-delete-button" onClick={() => setIsDeleteModalOpen(true)} variant="destructive">
<Trash2Icon />
Delete User
</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,9 @@ export const ChangePasswordModal = ({ userName, isOpen, setIsOpen }: ChangePassw
<div className="flex items-center gap-2">
<Input
aria-invalid={!isValidPassword}
data-testid="create-user-password"
name="test"
onChange={(e) => setPassword(e.target.value)}
testId="create-user-password"
type="password"
value={password}
/>
Expand Down Expand Up @@ -150,6 +150,7 @@ export const ChangePasswordModal = ({ userName, isOpen, setIsOpen }: ChangePassw
Cancel
</Button>
<Button
data-testid="save-password-button"
disabled={!isValidPassword || mechanism === undefined || isUpdateUserPending}
onClick={onSavePassword}
type="submit"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,10 +159,10 @@ export const UserRolesCardNew = ({ roles, userName, isLoading }: UserRolesCardNe
<Combobox
className="w-56"
clearable={false}
inputTestId="assign-role-combobox"
onChange={assignRole}
options={availableRoleOptions}
placeholder="Assign a role..."
testId="assign-role-combobox"
value=""
/>
) : undefined
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { generateSecurityName, SecurityPageNew } from './utils/security-page-new';
import { expect, test } from '../fixtures';

test.use({ featureFlags: { enableNewSecurityPage: true } });

test.describe('Security › Roles', () => {
test('create role appears in list', async ({ page }) => {
const name = generateSecurityName('role');
const sp = new SecurityPageNew(page);

await sp.createRole(name);

await test.step('verify role in list', async () => {
await sp.gotoRoles();
await sp.filterRoles(name);
await expect(page.getByTestId(`role-list-item-${name}`)).toBeVisible({ timeout: 10_000 });
});

await sp.deleteRoleFromList(name);
});

test('filter roles by name regexp', async ({ page }) => {
const sp = new SecurityPageNew(page);
const suffix = Math.random().toString(36).substring(2, 6);
const nameA = `e2e-role-filter-${suffix}-a`;
const nameB = `e2e-role-filter-${suffix}-b`;
const nameC = `e2e-role-filter-${suffix}-c`;

await sp.createRole(nameA);
await sp.createRole(nameB);
await sp.createRole(nameC);

await sp.gotoRoles();
await sp.filterRoles(`e2e-role-filter-${suffix}-[ab]`);

await expect(page.getByTestId(`role-list-item-${nameA}`)).toBeVisible();
await expect(page.getByTestId(`role-list-item-${nameB}`)).toBeVisible();
await expect(page.getByTestId(`role-list-item-${nameC}`)).not.toBeVisible();

await sp.deleteRoleFromList(nameA);
await sp.deleteRoleFromList(nameB);
await sp.deleteRoleFromList(nameC);
});

test('navigate to role details', async ({ page }) => {
const name = generateSecurityName('role');
const sp = new SecurityPageNew(page);

await sp.createRole(name);

await test.step('navigate via list link', async () => {
await sp.gotoRoles();
await sp.filterRoles(name);
await page.getByTestId(`role-list-item-${name}`).click();
await page.waitForURL('**/security/roles/**/details');
await expect(page.getByRole('heading', { name })).toBeVisible();
});

await sp.deleteRoleFromList(name);
});

test('add principal to role', async ({ page }) => {
const name = generateSecurityName('role');
const sp = new SecurityPageNew(page);

await sp.createRole(name);
await sp.gotoRoleDetails(name);

await test.step('add e2euser as principal', async () => {
await sp.addPrincipalToRole('e2euser');
await expect(page.getByTestId('remove-user-e2euser-button')).toBeVisible({ timeout: 10_000 });
});

await sp.deleteRoleFromList(name);
});

test('remove principal from role', async ({ page }) => {
const name = generateSecurityName('role');
const sp = new SecurityPageNew(page);

await sp.createRole(name);
await sp.gotoRoleDetails(name);

await sp.addPrincipalToRole('e2euser');
await expect(page.getByTestId('remove-user-e2euser-button')).toBeVisible();

await test.step('remove e2euser from role', async () => {
await sp.removePrincipalFromRole('e2euser');
await expect(page.getByTestId('remove-user-e2euser-button')).not.toBeVisible({ timeout: 10_000 });
});

await sp.deleteRoleFromList(name);
});

test('delete role from list', async ({ page }) => {
const name = generateSecurityName('role');
const sp = new SecurityPageNew(page);

await sp.createRole(name);
await sp.deleteRoleFromList(name);

await sp.gotoRoles();
await sp.filterRoles(name);
await expect(page.getByTestId(`role-list-item-${name}`)).not.toBeVisible();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/** biome-ignore-all lint/performance/useTopLevelRegex: this is a test */
import { generateSecurityName, SecurityPageNew } from './utils/security-page-new';
import { expect, test } from '../fixtures';

test.use({ featureFlags: { enableNewSecurityPage: true } });

test.describe('Security › Users › advanced', () => {
// ── Role assignment during user creation ─────────────────────────────────

test('role selected at creation appears in user details', async ({ page }) => {
const roleName = generateSecurityName('role');
const userName = generateSecurityName('usr');
const sp = new SecurityPageNew(page);

await sp.createRole(roleName);
await sp.createUserWithRole(userName, roleName);

await test.step('verify role shown in user details Roles card', async () => {
await sp.gotoUserDetails(userName);
await expect(page.getByTestId(`role-name-${roleName}`)).toBeVisible({ timeout: 10_000 });
});

await sp.deleteUserFromDetails(userName);
await sp.deleteRoleFromList(roleName);
});

// ── Allow all operations ──────────────────────────────────────────────────

test('Allow all operations creates ACLs for all resource types', async ({ page }) => {
const userName = generateSecurityName('usr');
const sp = new SecurityPageNew(page);

await sp.createUser(userName);
await sp.gotoUserDetails(userName);

await sp.allowAllOperations();

await test.step('verify ACL rows appear for all resources', async () => {
// Wait for at least 4 Allow permission cells (Topic/Group/Cluster/TransactionalId,
// plus Subject/SchemaRegistry when schemaRegistryACLApi is enabled)
const allowCells = page.getByRole('cell', { name: 'Allow' });
await expect(allowCells.first()).toBeVisible({ timeout: 10_000 });
await expect(allowCells.nth(3)).toBeVisible();
// Each row should show "All" operation
await expect(page.getByRole('cell', { name: 'All' }).first()).toBeVisible();
});

await sp.deleteUserFromDetails(userName);
});

// ── Assign / remove role from user details page ───────────────────────────

test('assign role from user details', async ({ page }) => {
const roleName = generateSecurityName('role');
const userName = generateSecurityName('usr');
const sp = new SecurityPageNew(page);

await sp.createRole(roleName);
await sp.createUser(userName);
await sp.gotoUserDetails(userName);

await sp.assignRoleFromDetails(roleName);

await test.step('verify role row visible', async () => {
await expect(page.getByTestId(`role-name-${roleName}`)).toBeVisible({ timeout: 10_000 });
});

await sp.deleteUserFromDetails(userName);
await sp.deleteRoleFromList(roleName);
});

test('remove role from user details', async ({ page }) => {
const roleName = generateSecurityName('role');
const userName = generateSecurityName('usr');
const sp = new SecurityPageNew(page);

await sp.createRole(roleName);
await sp.createUser(userName);
await sp.gotoUserDetails(userName);
await sp.assignRoleFromDetails(roleName);

await sp.removeRoleFromDetails(roleName);

await test.step('verify role row gone', async () => {
await expect(page.getByTestId(`role-name-${roleName}`)).not.toBeVisible({ timeout: 10_000 });
});

await sp.deleteUserFromDetails(userName);
await sp.deleteRoleFromList(roleName);
});

// ── Role membership visible in Permissions page ───────────────────────────

test('user with role shows role-based ACLs in Permissions page', async ({ page }) => {
const roleName = generateSecurityName('role');
const userName = generateSecurityName('usr');
const topicName = generateSecurityName('topic');
const sp = new SecurityPageNew(page);

await sp.createRole(roleName);

// Add an ACL to the role so it has something to show
await sp.gotoRoleDetails(roleName);
await test.step('add ACL to role', async () => {
await page.getByTestId('add-acl-button').click();
await page.getByRole('dialog', { name: 'Add ACL' }).waitFor({ state: 'visible' });
await page.getByPlaceholder('e.g. my-topic').fill(topicName);
await page.getByTestId('add-acl-submit-button').click();
await page.getByRole('dialog').waitFor({ state: 'hidden' });
});

// Create user and assign the role
await sp.createUser(userName);
await sp.gotoUserDetails(userName);
await sp.assignRoleFromDetails(roleName);

await test.step('permissions page shows user row', async () => {
await page.goto('/security/permissions', { waitUntil: 'domcontentloaded' });
await page.getByTestId(`row-${userName}`).waitFor({ state: 'visible', timeout: 15_000 });
await expect(page.getByTestId(`row-${userName}`)).toBeVisible();
});

await sp.deleteUserFromDetails(userName);
await sp.deleteRoleFromList(roleName);
});

// ── SASL mechanism is preserved in confirmation step ─────────────────────

test('SCRAM-SHA-512 mechanism set during creation shows in user list', async ({ page }) => {
const userName = generateSecurityName('usr');
const sp = new SecurityPageNew(page);

await sp.gotoUsers();
await page.getByTestId('create-user-button').click();
await page.getByTestId('create-user-name').waitFor({ state: 'visible' });
await page.getByLabel('Username').fill(userName);

// Switch from default SCRAM-SHA-256 to SCRAM-SHA-512
await page.getByRole('combobox').click();
await page.getByRole('option', { name: 'SCRAM-SHA-512' }).click();

await page.getByTestId('create-user-submit').click();
await page.getByTestId('done-button').waitFor({ state: 'visible' });

await test.step('mechanism label shown in user list', async () => {
await sp.gotoUsers();
await sp.filterUsers(userName);
await expect(page.getByText('SCRAM-SHA-512')).toBeVisible({ timeout: 10_000 });
});

await sp.deleteUserFromList(userName);
});
});
Loading
Loading