snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-16 00:29:55 +05:00
commit 240db4e2a7
15309 changed files with 2173406 additions and 0 deletions

View File

@@ -0,0 +1,226 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, test, enableABAC, navigateToABACPage} from '@mattermost/playwright-lib';
import type {CustomProfileAttribute} from '../../../channels/custom_profile_attributes/helpers';
import {setupCustomProfileAttributeFields} from '../../../channels/custom_profile_attributes/helpers';
import {createPrivateChannelForABAC, createBasicPolicy, enableUserManagedAttributes} from '../support';
/**
* ABAC Policy Management - Delete Policies
* Tests for deleting ABAC policies
*/
test.describe('ABAC Policy Management - Delete Policies', () => {
/**
* MM-T5793: Attribute-based access policy cannot be deleted if it is applied to any channels
*
* Step 1:
* 1. As system admin, go to ABAC page and click three-dot menu on a policy APPLIED to a channel
* 2. Observe Delete is DISABLED
* 3. Click three-dot menu on a policy NOT applied to any channels
* 4. Click Delete
*
* Expected:
* - Policy applied to a channel CANNOT be deleted (Delete is disabled)
* - Policy NOT applied to any channels CAN be deleted
*/
test('MM-T5793 Policy with channels cannot be deleted, policy without channels can be deleted', async ({pw}) => {
test.setTimeout(120000);
await pw.skipIfNoLicense();
const {adminUser, adminClient} = await pw.initSetup();
// Enable user-managed attributes
await enableUserManagedAttributes(adminClient);
// Set up a basic attribute field
const attributeFields: CustomProfileAttribute[] = [{name: 'Department', type: 'text', value: ''}];
await setupCustomProfileAttributeFields(adminClient, attributeFields);
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
const page = systemConsolePage.page;
await navigateToABACPage(page);
await enableABAC(page);
// ===========================================
// Create two policies:
// 1. policyWithChannel - has a channel assigned
// 2. policyWithoutChannel - has NO channels assigned
// ===========================================
const uniqueId = pw.random.id();
const policyWithChannelName = `ABAC-WithChannel-${uniqueId}`;
const policyWithoutChannelName = `ABAC-NoChannel-${uniqueId}`;
// Create a channel for the first policy
const team = (await adminClient.getMyTeams())[0];
const privateChannel = await createPrivateChannelForABAC(adminClient, team.id);
// Create policy WITH channel
await createBasicPolicy(page, {
name: policyWithChannelName,
attribute: 'Department',
operator: '==',
value: 'Engineering',
autoSync: false,
channels: [privateChannel.display_name],
});
// Navigate back to ABAC page
await navigateToABACPage(page);
await page.waitForTimeout(1000);
// Create policy WITHOUT channel using UI (Advanced mode)
// We'll create the policy, save it without channels, then remove channels via UI
// Click Add policy
const addPolicyButton = page.getByRole('button', {name: 'Add policy'});
await addPolicyButton.click();
await page.waitForLoadState('networkidle');
// Fill policy name
const nameInput = page.locator('#admin\\.access_control\\.policy\\.edit_policy\\.policyName');
await nameInput.waitFor({state: 'visible', timeout: 10000});
await nameInput.fill(policyWithoutChannelName);
// Switch to Advanced mode to create minimal policy
const advancedModeButton = page.getByRole('button', {name: /advanced/i});
if (await advancedModeButton.isVisible({timeout: 2000})) {
await advancedModeButton.click();
await page.waitForTimeout(1000);
}
// Fill CEL expression in Monaco editor
const monacoContainer = page.locator('.monaco-editor').first();
await monacoContainer.waitFor({state: 'visible', timeout: 5000});
const editorLines = page.locator('.monaco-editor .view-lines').first();
await editorLines.click({force: true});
await page.waitForTimeout(300);
// Type a simple expression
const isMac = process.platform === 'darwin';
await page.keyboard.press(isMac ? 'Meta+a' : 'Control+a');
await page.waitForTimeout(100);
await page.keyboard.type('user.attributes.Department == "Sales"', {delay: 10});
await page.waitForTimeout(1000);
// Save policy WITHOUT assigning any channels
const saveButton = page.getByRole('button', {name: 'Save'});
await saveButton.click();
// The "Apply policy" modal should NOT appear since there are no channels
// The webapp will call handleSubmit() directly and navigate back automatically
// Wait for navigation to complete
await page.waitForURL('**/membership_policies', {timeout: 10000});
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1500);
// ===========================================
// STEP 1-2: Verify Delete is DISABLED for policy WITH channel
// ===========================================
// Clear any existing search and verify both policies exist
const searchInput = page.locator('input[placeholder*="Search" i]').first();
await searchInput.waitFor({state: 'visible', timeout: 5000});
await searchInput.clear();
await page.waitForTimeout(500);
// Verify both policies are visible
await page.locator('.policy-name, tr.clickable').count();
// Now search for the policy with channel
await searchInput.fill(policyWithChannelName);
await page.waitForTimeout(1000);
// Find and click the three-dot menu for the policy with channel
const policyWithChannelRow = page
.locator('tr.clickable, .DataGrid_row')
.filter({hasText: policyWithChannelName})
.first();
await policyWithChannelRow.waitFor({state: 'visible', timeout: 10000});
const menuButtonWithChannel = policyWithChannelRow
.locator('button[id*="policy-menu"], button[aria-label*="menu" i], .menu-button, button:has(svg)')
.first();
await menuButtonWithChannel.click();
await page.waitForTimeout(500);
// Check if Delete is disabled
const deleteMenuItemWithChannel = page.getByRole('menuitem', {name: /delete/i});
const isDeleteDisabled = await deleteMenuItemWithChannel.isDisabled();
// Close the menu
await page.keyboard.press('Escape');
await page.waitForTimeout(300);
expect(isDeleteDisabled).toBe(true);
// ===========================================
// STEP 3-4: Verify Delete is ENABLED for policy WITHOUT channel and delete it
// ===========================================
// Clear search first to ensure we're seeing all policies
await searchInput.clear();
await page.waitForTimeout(500);
// Verify policy without channel exists in the list
const policyWithoutChannelExists = await page.locator('text=' + policyWithoutChannelName).count();
if (policyWithoutChannelExists === 0) {
// Try reloading if policy not visible
await page.reload();
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
}
// Now search for the policy without channel
await searchInput.fill(policyWithoutChannelName);
await page.waitForTimeout(1000);
// Find and click the three-dot menu for the policy without channel
const policyWithoutChannelRow = page
.locator('tr.clickable, .DataGrid_row')
.filter({hasText: policyWithoutChannelName})
.first();
await policyWithoutChannelRow.waitFor({state: 'visible', timeout: 10000});
const menuButtonWithoutChannel = policyWithoutChannelRow
.locator('button[id*="policy-menu"], button[aria-label*="menu" i], .menu-button, button:has(svg)')
.first();
await menuButtonWithoutChannel.click();
await page.waitForTimeout(500);
// Check if Delete is enabled
const deleteMenuItemWithoutChannel = page.getByRole('menuitem', {name: /delete/i});
const isDeleteEnabled = !(await deleteMenuItemWithoutChannel.isDisabled());
expect(isDeleteEnabled).toBe(true);
// Click Delete
await deleteMenuItemWithoutChannel.click();
await page.waitForTimeout(500);
// Handle confirmation modal if it appears
const confirmDeleteButton = page.getByRole('button', {name: /delete|confirm/i});
if (await confirmDeleteButton.isVisible({timeout: 2000})) {
await confirmDeleteButton.click();
await page.waitForTimeout(1000);
}
await page.waitForLoadState('networkidle');
// Verify the policy was deleted
await searchInput.clear();
await searchInput.fill(policyWithoutChannelName);
await page.waitForTimeout(1000);
const policyStillExists = await page
.locator('tr.clickable, .DataGrid_row')
.filter({hasText: policyWithoutChannelName})
.isVisible({timeout: 3000});
expect(policyStillExists).toBe(false);
});
});

View File

@@ -0,0 +1,179 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, test, enableABAC, navigateToABACPage} from '@mattermost/playwright-lib';
import {createBasicPolicy, getPolicyIdByName} from '../support';
/**
* @objective E2E coverage for membership policy edit action navigation:
* - Clicking Edit in the policy row action menu navigates to the membership policy editor
*
* @reference MM-68958: Fix membership policy edit action navigation
*/
test.describe('ABAC Policy Management - Edit Action Navigation', () => {
/**
* MM-68958: Edit action in policy row menu navigates to membership policy editor
*
* Steps:
* 1. Enable ABAC and create a membership policy
* 2. Navigate to System Console > Membership Policies
* 3. Open a policy row's three-dot action menu
* 4. Click Edit
* 5. Verify the URL is /admin_console/system_attributes/membership_policies/edit_policy/<policy_id>
*/
test('MM-68958 Edit action navigates to membership policy editor', async ({pw}) => {
test.setTimeout(120000);
await pw.skipIfNoLicense();
const {adminUser, adminClient, team} = await pw.initSetup();
// Create a test channel for the policy
const channelName = `abac-edit-nav-test-${pw.random.id()}`;
const privateChannel = await adminClient.createChannel({
team_id: team.id,
name: channelName.toLowerCase().replace(/[^a-z0-9-]/g, ''),
display_name: channelName,
type: 'P',
});
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
const page = systemConsolePage.page;
await navigateToABACPage(page);
await enableABAC(page);
// Create a basic membership policy
const policyName = `Edit-Nav-Test-${pw.random.id()}`;
await createBasicPolicy(page, {
name: policyName,
attribute: 'Department',
operator: '==',
value: 'Engineering',
autoSync: false,
channels: [privateChannel.display_name],
});
// Get the policy ID from the backend
const policyId = await getPolicyIdByName(adminClient, policyName);
expect(policyId, 'Policy should be created and have an ID').toBeTruthy();
// Navigate to Membership Policies list page
await page.goto('/admin_console/system_attributes/membership_policies', {waitUntil: 'networkidle'});
await page.waitForTimeout(1000);
// Search for the policy to ensure it's visible
const policySearchInput = page.locator('input[placeholder*="Search" i]').first();
if (await policySearchInput.isVisible({timeout: 3000})) {
await policySearchInput.fill(policyName);
await page.waitForTimeout(1000);
}
// Find the policy row
const policyRowLocator = page.locator('tr.clickable, .DataGrid_row').filter({hasText: policyName}).first();
await policyRowLocator.waitFor({state: 'visible', timeout: 10000});
// Open the three-dot action menu for the policy
const actionMenuButton = policyRowLocator
.locator('button[aria-label*="Actions" i], button:has(i.icon-dots-vertical)')
.first();
await actionMenuButton.waitFor({state: 'visible', timeout: 5000});
await actionMenuButton.click();
await page.waitForTimeout(500);
// Click the Edit menu item
const editMenuItem = page.locator(`[id*="policy-menu-edit-${policyId}"]`).first();
await editMenuItem.waitFor({state: 'visible', timeout: 5000});
// Click Edit and wait for navigation
await editMenuItem.click();
// Wait for the URL to change to the edit policy page
await page.waitForURL(`**/admin_console/system_attributes/membership_policies/edit_policy/${policyId}`, {
timeout: 10000,
});
// Verify we're on the edit policy page by checking the URL
const currentURL = page.url();
expect(currentURL).toContain(`/admin_console/system_attributes/membership_policies/edit_policy/${policyId}`);
// Additional verification: Check that the policy editor is loaded
// The policy editor should have the policy name visible
const policyNameInput = page.locator('input[placeholder*="name" i], input[value*="Edit-Nav-Test"]').first();
await expect(policyNameInput).toBeVisible({timeout: 10000});
});
/**
* MM-68958: Row click also navigates to membership policy editor
*
* This test verifies that clicking the row (not just the Edit action) also navigates correctly.
* This behavior should have been working before, but we verify it still works after the fix.
*/
test('Row click navigates to membership policy editor', async ({pw}) => {
test.setTimeout(120000);
await pw.skipIfNoLicense();
const {adminUser, adminClient, team} = await pw.initSetup();
// Create a test channel for the policy
const channelName = `abac-row-click-test-${pw.random.id()}`;
const privateChannel = await adminClient.createChannel({
team_id: team.id,
name: channelName.toLowerCase().replace(/[^a-z0-9-]/g, ''),
display_name: channelName,
type: 'P',
});
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
const page = systemConsolePage.page;
await navigateToABACPage(page);
await enableABAC(page);
// Create a basic membership policy
const policyName = `Row-Click-Test-${pw.random.id()}`;
await createBasicPolicy(page, {
name: policyName,
attribute: 'Department',
operator: '==',
value: 'Sales',
autoSync: false,
channels: [privateChannel.display_name],
});
// Get the policy ID from the backend
const policyId = await getPolicyIdByName(adminClient, policyName);
expect(policyId, 'Policy should be created and have an ID').toBeTruthy();
// Navigate to Membership Policies list page
await page.goto('/admin_console/system_attributes/membership_policies', {waitUntil: 'networkidle'});
await page.waitForTimeout(1000);
// Search for the policy to ensure it's visible
const policySearchInput = page.locator('input[placeholder*="Search" i]').first();
if (await policySearchInput.isVisible({timeout: 3000})) {
await policySearchInput.fill(policyName);
await page.waitForTimeout(1000);
}
// Find the policy row
const policyRowLocator = page.locator('tr.clickable, .DataGrid_row').filter({hasText: policyName}).first();
await policyRowLocator.waitFor({state: 'visible', timeout: 10000});
// Click the row (not the action menu)
await policyRowLocator.click();
// Wait for the URL to change to the edit policy page
await page.waitForURL(`**/admin_console/system_attributes/membership_policies/edit_policy/${policyId}`, {
timeout: 10000,
});
// Verify we're on the edit policy page
const currentURL = page.url();
expect(currentURL).toContain(`/admin_console/system_attributes/membership_policies/edit_policy/${policyId}`);
});
});

View File

@@ -0,0 +1,939 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {
expect,
test,
enableABAC,
navigateToABACPage,
verifyUserInChannel,
verifyUserNotInChannel,
} from '@mattermost/playwright-lib';
import type {CustomProfileAttribute} from '../../../channels/custom_profile_attributes/helpers';
import {setupCustomProfileAttributeFields} from '../../../channels/custom_profile_attributes/helpers';
import {
createUserForABAC,
testAccessRule,
createPrivateChannelForABAC,
createBasicPolicy,
createAdvancedPolicy,
waitForPolicySyncJob,
getPolicyIdByName,
enableUserManagedAttributes,
} from '../support';
/**
* ABAC Policy Management - Edit Policies
* Tests for editing existing ABAC policies
*/
test.describe('ABAC Policy Management - Edit Policies', () => {
/**
* MM-T5790: Editing value of existing attribute-based access policy applies access control as specified (without auto-add)
*
* Step 1:
* 1. Go to ABAC page, click a policy to edit. Ensure Auto-add is False
* 2. Edit an existing policy rule to a different value (same attribute and operator)
* 3. Click Test Access Rule, observe users who satisfy the policy
* 4. Save the changes
* 5. User who satisfies NEW policy but not in channel → NOT auto-added
*/
test('MM-T5790 Editing policy value applies access control without auto-add', async ({pw}) => {
test.setTimeout(180000);
await pw.skipIfNoLicense();
const {adminUser, adminClient, team} = await pw.initSetup();
// Enable user-managed attributes FIRST (same order as MM-T5783)
await enableUserManagedAttributes(adminClient);
// Set up the Department attribute field
const attributeFields: CustomProfileAttribute[] = [{name: 'Department', type: 'text', value: ''}];
const attributeFieldsMap = await setupCustomProfileAttributeFields(adminClient, attributeFields);
// Create users with proper CPA attributes
// User A: Department=Engineering (will satisfy ORIGINAL policy)
// User B: Department=Sales (will satisfy EDITED policy)
const engineerUser = await createUserForABAC(adminClient, attributeFieldsMap, [
{name: 'Department', value: 'Engineering', type: 'text'},
]);
const salesUser = await createUserForABAC(adminClient, attributeFieldsMap, [
{name: 'Department', value: 'Sales', type: 'text'},
]);
await adminClient.addToTeam(team.id, engineerUser.id);
await adminClient.addToTeam(team.id, salesUser.id);
// Create channel - use direct API call for more control
const channelName = `abac-edit-test-${pw.random.id()}`;
const privateChannel = await adminClient.createChannel({
team_id: team.id,
name: channelName.toLowerCase().replace(/[^a-z0-9-]/g, ''),
display_name: channelName,
type: 'P',
});
// Admin user is automatically added as channel creator, but let's add the test users
// Note: addToChannel(userId, channelId) - user first, then channel
await adminClient.addToChannel(engineerUser.id, privateChannel.id);
// Verify we can access the channel
// const channelCheck = await adminClient.getChannel(privateChannel.id);
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
const page = systemConsolePage.page;
await navigateToABACPage(page);
await enableABAC(page);
// Check membership BEFORE policy creation
// Using library helper verifyUserInChannel(client, userId, channelId)
await verifyUserInChannel(adminClient, engineerUser.id, privateChannel.id);
// Debug: Show user attributes BEFORE policy creation
try {
await (adminClient as any).doFetch(
`${adminClient.getBaseRoute()}/users/${engineerUser.id}/custom_profile_attributes`,
{method: 'GET'},
);
} catch {
// Ignore errors
}
// ===========================================
// SETUP: Create policy with ORIGINAL value (Engineering), Auto-add OFF
// ===========================================
const policyName = `ABAC-Edit-Test-${pw.random.id()}`;
await createBasicPolicy(page, {
name: policyName,
attribute: 'Department',
operator: '==',
value: 'Engineering',
autoSync: false, // Auto-add is OFF
channels: [privateChannel.display_name],
});
const policyId = await getPolicyIdByName(adminClient, policyName);
// Check membership AFTER policy creation (before explicit sync)
await verifyUserInChannel(adminClient, engineerUser.id, privateChannel.id);
// Wait for the automatic sync (triggered by createBasicPolicy's "Apply Policy") to complete
await page.waitForTimeout(3000); // Give time for sync job to run
// Check membership AFTER automatic sync
const engineerAfterSync = await verifyUserInChannel(adminClient, engineerUser.id, privateChannel.id);
const salesAfterSync = await verifyUserInChannel(adminClient, salesUser.id, privateChannel.id);
// Debug: Fetch user attributes to verify they're set
try {
await (adminClient as any).doFetch(
`${adminClient.getBaseRoute()}/users/${engineerUser.id}/custom_profile_attributes`,
{method: 'GET'},
);
} catch {
// Ignore errors
}
expect(engineerAfterSync).toBe(true);
expect(salesAfterSync).toBe(false);
// ===========================================
// STEP 1-2: Edit policy to different value (Sales instead of Engineering)
// ===========================================
// Navigate to ABAC page and find the policy
await navigateToABACPage(page);
await page.waitForTimeout(1000);
// Search for and click the policy
const policySearchInput = page.locator('input[placeholder*="Search" i]').first();
if (await policySearchInput.isVisible({timeout: 3000})) {
await policySearchInput.fill(policyName);
await page.waitForTimeout(1000);
}
const policyRowLocator = page.locator('tr.clickable, .DataGrid_row').filter({hasText: policyName}).first();
await policyRowLocator.waitFor({state: 'visible', timeout: 10000});
await policyRowLocator.click();
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
// Verify Auto-add is OFF (check the header checkbox)
const autoAddCheckbox = page.locator('#auto-add-header-checkbox');
if (await autoAddCheckbox.isVisible({timeout: 3000})) {
const isChecked = await autoAddCheckbox.isChecked();
if (isChecked) {
// Uncheck it
await autoAddCheckbox.click();
await page.waitForTimeout(500);
}
} else {
// Checkbox not visible
}
// Edit the value: Change from "Engineering" to "Sales"
// Strategy 1: Try simple input field (for text attributes)
const simpleValueInput = page.locator('.values-editor__simple-input').first();
if (await simpleValueInput.isVisible({timeout: 3000})) {
// Clear and fill the new value
await simpleValueInput.click();
await simpleValueInput.fill('');
await simpleValueInput.fill('Sales');
// Press Tab or click elsewhere to confirm
await page.keyboard.press('Tab');
await page.waitForTimeout(500);
} else {
// Strategy 2: Try value selector menu button
const valueButton = page.locator('[data-testid="valueSelectorMenuButton"]').first();
if (await valueButton.isVisible({timeout: 3000})) {
await valueButton.click();
await page.waitForTimeout(500);
// Look for input in the dropdown
const menuInput = page
.locator('#value-selector-menu input[type="text"], .value-selector-menu input')
.first();
if (await menuInput.isVisible({timeout: 2000})) {
await menuInput.fill('Sales');
await page.waitForTimeout(500);
// Click on the option or press Enter
const salesOption = page.locator('#value-selector-menu').getByText('Sales', {exact: true}).first();
if (await salesOption.isVisible({timeout: 2000})) {
await salesOption.click();
} else {
await page.keyboard.press('Enter');
}
await page.waitForTimeout(500);
}
} else {
// Strategy 3: Use Advanced mode to edit CEL expression directly
const advancedModeButton = page.getByRole('button', {name: /advanced|switch to advanced/i});
if (await advancedModeButton.isVisible({timeout: 3000})) {
await advancedModeButton.click();
await page.waitForTimeout(1000);
// Find Monaco editor - use view-lines with force click to bypass overlay
const monacoContainer = page.locator('.monaco-editor').first();
if (await monacoContainer.isVisible({timeout: 3000})) {
const editorLines = page.locator('.monaco-editor .view-lines').first();
await editorLines.click({force: true});
await page.waitForTimeout(300);
// Platform-specific select all
const isMac = process.platform === 'darwin';
await page.keyboard.press(isMac ? 'Meta+a' : 'Control+a');
await page.waitForTimeout(100);
await page.keyboard.type('user.attributes.Department == "Sales"', {delay: 10});
await page.waitForTimeout(500);
}
}
}
}
// ===========================================
// STEP 3: Click Test Access Rule
// ===========================================
await testAccessRule(page);
// ===========================================
// STEP 4: Save the changes
// ===========================================
const saveButton = page.getByRole('button', {name: 'Save'});
await saveButton.waitFor({state: 'visible', timeout: 5000});
await saveButton.click();
await page.waitForTimeout(1000);
// Handle "Apply policy" confirmation if it appears
const applyPolicyButton = page.getByRole('button', {name: /apply policy/i});
if (await applyPolicyButton.isVisible({timeout: 3000})) {
await applyPolicyButton.click();
await page.waitForTimeout(1000);
}
// Wait for sync to complete (race-safe: polls exact policy, not UI table)
await navigateToABACPage(page);
if (!policyId) {
throw new Error('Policy ID not found after creation');
}
await waitForPolicySyncJob(adminClient, policyId);
// ===========================================
// STEP 5 & 6: Verify channel membership after policy edit
// ===========================================
// Poll under PW_WORKERS>=2: another shard's sync job may flip membership.
await expect
.poll(async () => verifyUserInChannel(adminClient, salesUser.id, privateChannel.id), {
timeout: 30_000,
intervals: [500, 1000, 2000],
message: 'salesUser should NOT be in channel (auto-add is off)',
})
.toBe(false);
await expect
.poll(async () => verifyUserInChannel(adminClient, engineerUser.id, privateChannel.id), {
timeout: 30_000,
intervals: [500, 1000, 2000],
message: 'engineerUser should be REMOVED (no longer satisfies policy)',
})
.toBe(false);
// ===========================================
// STEP 7: Admin can manually add satisfying user
// ===========================================
try {
// Note: addToChannel(userId, channelId) - user first, then channel
await adminClient.addToChannel(salesUser.id, privateChannel.id);
// Verify the user was actually added
const salesInChannelAfterManualAdd = await verifyUserInChannel(
adminClient,
salesUser.id,
privateChannel.id,
);
expect(salesInChannelAfterManualAdd).toBe(true);
} catch (error) {
throw new Error(`Step 7 FAILED: Admin should be able to manually add satisfying user. Error: ${error}`);
}
});
/**
* MM-T5791: Editing existing access policy to add another attribute applies access control as specified (with auto-add)
*
* Precondition: At least one policy in existence
*
* Step 1:
* 1. Go to ABAC page, click a policy to edit. Ensure Auto-add is TRUE
* 2. Edit an existing policy rule to add another attribute/value
* 3. Click Test Access Rule, observe users who satisfy the policy
* 4. Save the changes
* 5. User who satisfies NEWLY EDITED policy but not in channel → auto-ADDED
* 6. User who doesn't satisfy NEWLY EDITED policy and is in channel → auto-REMOVED
*
* Expected:
* - User satisfying new multi-attribute policy IS auto-added
* - User not satisfying new policy IS auto-removed
*/
test('MM-T5791 Editing policy to add attribute with auto-add enabled', async ({pw}) => {
test.setTimeout(180000);
await pw.skipIfNoLicense();
const {adminUser, adminClient, team} = await pw.initSetup();
// Delete ALL existing custom attributes to start fresh
try {
const existingFields = await adminClient.getCustomProfileAttributeFields();
for (const field of existingFields) {
try {
await adminClient.deleteCustomProfileAttributeField(field.id);
} catch {
// Ignore deletion errors
}
}
await new Promise((resolve) => setTimeout(resolve, 2000));
} catch {
// Ignore if no fields exist
}
// Enable user-managed attributes FIRST (same pattern as MM-T5783)
await enableUserManagedAttributes(adminClient);
// create attributes using direct API
const attributeFieldsMap: Record<string, any> = {};
const departmentField = await adminClient.createCustomProfileAttributeField({
name: 'Department',
type: 'text',
attrs: {managed: 'admin', visibility: 'when_set', sort_order: 0},
} as any);
attributeFieldsMap[departmentField.id] = departmentField;
const officeField = await adminClient.createCustomProfileAttributeField({
name: 'Office',
type: 'text',
attrs: {managed: 'admin', visibility: 'when_set', sort_order: 1},
} as any);
attributeFieldsMap[officeField.id] = officeField;
// Wait for attributes to be indexed
await new Promise((resolve) => setTimeout(resolve, 2000));
// Create users:
// 1. engineerRemoteUser: Dept=Engineering, Office=Remote → satisfies BOTH (after edit)
// 2. engineerOfficeUser: Dept=Engineering, Office=HQ → satisfies ORIGINAL only, NOT the edited policy
// 3. salesUser: Dept=Sales → doesn't satisfy any policy
const engineerRemoteUser = await createUserForABAC(adminClient, attributeFieldsMap, [
{name: 'Department', type: 'text', value: 'Engineering'},
{name: 'Office', type: 'text', value: 'Remote'},
]);
const engineerOfficeUser = await createUserForABAC(adminClient, attributeFieldsMap, [
{name: 'Department', type: 'text', value: 'Engineering'},
{name: 'Office', type: 'text', value: 'HQ'},
]);
const salesUser = await createUserForABAC(adminClient, attributeFieldsMap, [
{name: 'Department', type: 'text', value: 'Sales'},
]);
// Add users to team
await adminClient.addToTeam(team.id, engineerRemoteUser.id);
await adminClient.addToTeam(team.id, engineerOfficeUser.id);
await adminClient.addToTeam(team.id, salesUser.id);
// Create channel and add engineerOfficeUser (satisfies original policy)
const privateChannel = await createPrivateChannelForABAC(adminClient, team.id);
await adminClient.addToChannel(engineerOfficeUser.id, privateChannel.id);
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
const page = systemConsolePage.page;
await navigateToABACPage(page);
await enableABAC(page);
// ===========================================
// PRECONDITION: Create ORIGINAL policy with ONE attribute (Department=Engineering)
// Auto-add ON so users are auto-added
// ===========================================
const policyName = `ABAC-AddAttr-Test-${pw.random.id()}`;
await createBasicPolicy(page, {
name: policyName,
attribute: 'Department',
operator: '==',
value: 'Engineering',
autoSync: true, // Auto-add is ON
channels: [privateChannel.display_name],
});
const policyId = await getPolicyIdByName(adminClient, policyName);
// Wait for automatic sync to complete
await page.waitForTimeout(3000);
// Verify initial state after original policy sync
await verifyUserInChannel(adminClient, engineerRemoteUser.id, privateChannel.id);
await verifyUserInChannel(adminClient, engineerOfficeUser.id, privateChannel.id);
// ===========================================
// STEP 1-2: Edit policy to ADD another attribute (Office=Remote)
// New expression: Department=Engineering AND Office=Remote
// ===========================================
// Navigate back to ABAC list page
await page.goto('/admin_console/system_attributes/membership_policies', {waitUntil: 'networkidle'});
await page.waitForTimeout(2000);
// Verify we're on the list page by checking for "Add policy" button
const addPolicyButton = page.getByRole('button', {name: 'Add policy'});
await addPolicyButton.waitFor({state: 'visible', timeout: 10000});
// Try to find the policy row first without search
const policyRowLocator = page.locator('tr.clickable, .DataGrid_row').filter({hasText: policyName}).first();
const isPolicyVisible = await policyRowLocator.isVisible({timeout: 3000}).catch(() => false);
// If not visible, use search
if (!isPolicyVisible) {
const policySearchInput = page
.locator('.DataGrid input[type="text"], input[placeholder*="Search policies" i]')
.first();
if (await policySearchInput.isVisible({timeout: 3000})) {
await policySearchInput.click();
await policySearchInput.fill(policyName);
await page.waitForTimeout(1500);
}
}
// Click policy to edit
await policyRowLocator.waitFor({state: 'visible', timeout: 15000});
await policyRowLocator.click();
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
// Check if "Add attribute" button is disabled (means attributes not loaded)
// If so, reload to fetch the Office attribute
const addAttributeButtonCheck = page.getByRole('button', {name: /add attribute/i});
if (await addAttributeButtonCheck.isVisible({timeout: 2000})) {
const isDisabled = await addAttributeButtonCheck.isDisabled();
if (isDisabled) {
await page.reload();
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
}
}
// Stay in Simple Mode and add a second attribute row
const addAttributeButton = page.getByRole('button', {name: /add attribute/i});
await addAttributeButton.waitFor({state: 'visible', timeout: 5000});
await addAttributeButton.click();
await page.waitForTimeout(1000);
// The attribute dropdown opens automatically after clicking "Add attribute"
// Wait for the menu to be visible and select "Office"
const attributeMenu = page.locator('[id^="attribute-selector-menu"]');
await attributeMenu.waitFor({state: 'visible', timeout: 5000});
const officeOption = attributeMenu.locator('li:has-text("Office")').first();
await officeOption.waitFor({state: 'visible', timeout: 5000});
await officeOption.click({force: true});
await page.waitForTimeout(500);
// Select operator "==" (is)
const operatorButton = page.locator('[data-testid="operatorSelectorMenuButton"]').last();
await operatorButton.waitFor({state: 'visible', timeout: 5000});
await operatorButton.click({force: true});
await page.waitForTimeout(500);
const operatorOption = page.locator('[id^="operator-selector-menu"] li:has-text("is")').first();
await operatorOption.click({force: true});
await page.waitForTimeout(500);
// Fill value "Remote"
const valueInput = page.locator('.values-editor__simple-input').last();
await valueInput.waitFor({state: 'visible', timeout: 5000});
await valueInput.fill('Remote');
await page.waitForTimeout(500);
// ===========================================
// STEP 3: Test Access Rule
// ===========================================
await testAccessRule(page);
// ===========================================
// STEP 4: Save the changes
// ===========================================
const saveButton = page.getByRole('button', {name: 'Save'});
await saveButton.waitFor({state: 'visible', timeout: 5000});
await saveButton.click();
await page.waitForTimeout(1000);
// Handle "Apply policy" confirmation if it appears
const applyPolicyButton = page.getByRole('button', {name: /apply policy/i});
if (await applyPolicyButton.isVisible({timeout: 3000})) {
await applyPolicyButton.click();
await page.waitForTimeout(1000);
}
// Navigate to ABAC page and wait for auto-triggered sync job
await navigateToABACPage(page);
await page.waitForTimeout(2000);
// Wait for the auto-triggered sync job to complete (policy edit triggers sync automatically)
if (!policyId) {
throw new Error('Policy ID not found after creation');
}
await waitForPolicySyncJob(adminClient, policyId);
// Additional wait for membership changes to propagate
// ===========================================
// STEP 5 & 6: Verify channel membership after edit
// ===========================================
// Poll under PW_WORKERS>=2: another shard's sync job may interleave.
await expect
.poll(async () => verifyUserInChannel(adminClient, engineerRemoteUser.id, privateChannel.id), {
timeout: 30_000,
intervals: [500, 1000, 2000],
message: 'engineerRemoteUser should be in channel (satisfies BOTH attributes)',
})
.toBe(true);
await expect
.poll(async () => verifyUserInChannel(adminClient, engineerOfficeUser.id, privateChannel.id), {
timeout: 30_000,
intervals: [500, 1000, 2000],
message: 'engineerOfficeUser should be REMOVED (only satisfies original, not new policy)',
})
.toBe(false);
await expect
.poll(async () => verifyUserInChannel(adminClient, salesUser.id, privateChannel.id), {
timeout: 30_000,
intervals: [500, 1000, 2000],
message: 'salesUser should not be in channel',
})
.toBe(false);
});
/**
* MM-T5792: Editing existing access policy to remove one of the rules applies access control as specified (with auto-add)
*
* Precondition: At least one policy with MULTIPLE rules in existence
*
* Step 1:
* 1. Go to ABAC page, click a policy to edit. Ensure Auto-add is TRUE
* 2. Edit policy to REMOVE one of the rules (attribute/value)
* 3. Click Test Access Rule, observe users who satisfy the policy
* 4. Save the changes
* 5. User who satisfies newly edited (simpler) policy but not in channel → auto-ADDED
* 6. User who no longer satisfies newly edited policy and is in channel → auto-REMOVED
*
* Expected:
* - User satisfying new simpler policy IS auto-added
* - User not satisfying new policy IS auto-removed
*
* This is the OPPOSITE of MM-T5791:
* - MM-T5791: ADD rule → policy MORE restrictive
* - MM-T5792: REMOVE rule → policy LESS restrictive
*/
test('MM-T5792 Editing policy to remove attribute rule with auto-add enabled', async ({pw}) => {
test.setTimeout(180000);
await pw.skipIfNoLicense();
const {adminUser, adminClient, team} = await pw.initSetup();
// Delete ALL existing custom attributes to start fresh
try {
const existingFields = await adminClient.getCustomProfileAttributeFields();
for (const field of existingFields) {
try {
await adminClient.deleteCustomProfileAttributeField(field.id);
} catch {
// Ignore deletion errors
}
}
await new Promise((resolve) => setTimeout(resolve, 2000));
} catch {
// Ignore if no fields exist
}
// Enable user-managed attributes FIRST (same pattern as MM-T5783)
await enableUserManagedAttributes(adminClient);
// create attributes using direct API
const attributeFieldsMap: Record<string, any> = {};
const departmentField = await adminClient.createCustomProfileAttributeField({
name: 'Department',
type: 'text',
attrs: {managed: 'admin', visibility: 'when_set', sort_order: 0},
} as any);
attributeFieldsMap[departmentField.id] = departmentField;
const officeField = await adminClient.createCustomProfileAttributeField({
name: 'Office',
type: 'text',
attrs: {managed: 'admin', visibility: 'when_set', sort_order: 1},
} as any);
attributeFieldsMap[officeField.id] = officeField;
// Wait for attributes to be indexed
await new Promise((resolve) => setTimeout(resolve, 2000));
// Create users:
// 1. engineerRemoteUser: Dept=Engineering, Office=Remote → satisfies ORIGINAL (both rules)
// 2. engineerOfficeUser: Dept=Engineering, Office=HQ → satisfies EDITED policy (Dept only)
// 3. salesRemoteUser: Dept=Sales, Office=Remote → doesn't satisfy (wrong Dept)
const engineerRemoteUser = await createUserForABAC(adminClient, attributeFieldsMap, [
{name: 'Department', type: 'text', value: 'Engineering'},
{name: 'Office', type: 'text', value: 'Remote'},
]);
const engineerOfficeUser = await createUserForABAC(adminClient, attributeFieldsMap, [
{name: 'Department', type: 'text', value: 'Engineering'},
{name: 'Office', type: 'text', value: 'HQ'},
]);
const salesRemoteUser = await createUserForABAC(adminClient, attributeFieldsMap, [
{name: 'Department', type: 'text', value: 'Sales'},
{name: 'Office', type: 'text', value: 'Remote'},
]);
// Add users to team
await adminClient.addToTeam(team.id, engineerRemoteUser.id);
await adminClient.addToTeam(team.id, engineerOfficeUser.id);
await adminClient.addToTeam(team.id, salesRemoteUser.id);
// Create channel and add salesRemoteUser (does NOT satisfy any policy)
// This user will be REMOVED after we edit policy (to verify removal behavior)
const privateChannel = await createPrivateChannelForABAC(adminClient, team.id);
await adminClient.addToChannel(salesRemoteUser.id, privateChannel.id);
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
const page = systemConsolePage.page;
await navigateToABACPage(page);
await enableABAC(page);
// ===========================================
// PRECONDITION: Create ORIGINAL policy with TWO attributes
// Department=Engineering AND Office=Remote
// Auto-add ON
// ===========================================
const policyName = `ABAC-RemoveRule-${pw.random.id()}`;
await adminClient.patchConfig({
AccessControlSettings: {
EnableAttributeBasedAccessControl: true,
EnableUserManagedAttributes: true,
},
} as any);
// Use advanced mode for multi-attribute policy
await createAdvancedPolicy(page, {
name: policyName,
celExpression: 'user.attributes.Department == "Engineering" && user.attributes.Office == "Remote"',
autoSync: true, // Auto-add is ON
channels: [privateChannel.display_name],
});
const policyId = await getPolicyIdByName(adminClient, policyName);
// Wait for automatic sync to complete
await page.waitForTimeout(3000);
// Verify initial state after original policy sync
// Original policy: Department=Engineering AND Office=Remote
// - engineerRemoteUser: satisfies → should be auto-added
// - engineerOfficeUser: does NOT satisfy (HQ != Remote) → should NOT be in channel
// - salesRemoteUser: does NOT satisfy (Sales != Engineering) → should be auto-removed
await verifyUserInChannel(adminClient, engineerRemoteUser.id, privateChannel.id);
await verifyUserNotInChannel(adminClient, engineerOfficeUser.id, privateChannel.id);
await verifyUserNotInChannel(adminClient, salesRemoteUser.id, privateChannel.id);
// ===========================================
// STEP 1-2: Edit policy to REMOVE Location rule
// New expression: Department=Engineering (only)
// This makes policy LESS restrictive
// ===========================================
await adminClient.patchConfig({
AccessControlSettings: {
EnableAttributeBasedAccessControl: true,
EnableUserManagedAttributes: true,
},
} as any);
// Navigate back to ABAC list page
await page.goto('/admin_console/system_attributes/membership_policies', {waitUntil: 'networkidle'});
await page.waitForTimeout(2000);
// Verify we're on the list page by checking for "Add policy" button
const addPolicyButton = page.getByRole('button', {name: 'Add policy'});
await addPolicyButton.waitFor({state: 'visible', timeout: 10000});
// Try to find the policy row first without search
const policyRowLocator = page.locator('tr.clickable, .DataGrid_row').filter({hasText: policyName}).first();
const isPolicyVisible = await policyRowLocator.isVisible({timeout: 3000}).catch(() => false);
// If not visible, try with search
if (!isPolicyVisible) {
// Use a more specific selector for the search input in the policies table
const policySearchInput = page
.locator('.DataGrid input[type="text"], input[placeholder*="Search policies" i]')
.first();
if (await policySearchInput.isVisible({timeout: 3000})) {
await policySearchInput.click();
await policySearchInput.fill(policyName);
// DON'T press Enter - just wait for the search to filter
await page.waitForTimeout(1500);
}
}
// Wait for policy row to be visible
await policyRowLocator.waitFor({state: 'visible', timeout: 15000});
await policyRowLocator.click();
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
// Verify Auto-add is ON
const autoAddCheckbox = page.locator('#auto-add-header-checkbox');
if (await autoAddCheckbox.isVisible({timeout: 3000})) {
const isChecked = await autoAddCheckbox.isChecked();
if (!isChecked) {
await autoAddCheckbox.click();
await page.waitForTimeout(500);
}
}
// Check if Monaco editor is visible - if not, switch to Advanced mode
// Policy may open in Simple mode even if created in Advanced mode
let monacoContainer = page.locator('.monaco-editor').first();
const isMonacoVisible = await monacoContainer.isVisible({timeout: 2000}).catch(() => false);
if (!isMonacoVisible) {
const advancedModeButton = page.getByRole('button', {name: /advanced|switch to advanced/i});
if (await advancedModeButton.isVisible({timeout: 5000})) {
await advancedModeButton.click();
await page.waitForTimeout(2000); // Wait for Monaco to fully initialize
}
}
// Find Monaco editor and update expression to REMOVE Office rule
monacoContainer = page.locator('.monaco-editor').first();
await monacoContainer.waitFor({state: 'visible', timeout: 10000});
const editorLines = page.locator('.monaco-editor .view-lines').first();
await editorLines.waitFor({state: 'visible', timeout: 5000});
await page.waitForTimeout(500);
// Click to focus the editor
await editorLines.click({force: true});
await page.waitForTimeout(300);
// Use platform-specific select all (Meta+a on Mac, Control+a on others)
const isMac = process.platform === 'darwin';
await page.keyboard.press(isMac ? 'Meta+a' : 'Control+a');
await page.waitForTimeout(200);
// Type the new CEL expression (REMOVING Office rule)
const newExpression = 'user.attributes.Department == "Engineering"';
await page.keyboard.type(newExpression, {delay: 10});
await page.waitForTimeout(1000);
// Wait for the "Valid" indicator to confirm the expression is valid
const validIndicator = page.locator('text=Valid').first();
try {
await validIndicator.waitFor({state: 'visible', timeout: 10000});
} catch {
// Ignore if Valid indicator doesn't appear
}
// ===========================================
// STEP 3: Test Access Rule
// ===========================================
await testAccessRule(page);
// ===========================================
// STEP 4: Save the changes
// ===========================================
const saveButton = page.getByRole('button', {name: 'Save'});
await saveButton.waitFor({state: 'visible', timeout: 5000});
await saveButton.click();
await page.waitForTimeout(1000);
// Handle "Apply policy" confirmation if it appears
const applyPolicyButton = page.getByRole('button', {name: /apply policy/i});
if (await applyPolicyButton.isVisible({timeout: 3000})) {
await applyPolicyButton.click();
await page.waitForTimeout(1000);
}
// Navigate to ABAC page and wait for sync job to complete (race-safe by policyId)
await navigateToABACPage(page);
if (!policyId) {
throw new Error('Policy ID not found after creation');
}
await waitForPolicySyncJob(adminClient, policyId);
// ===========================================
// STEP 5 & 6: Verify channel membership after edit
// ===========================================
// Poll under PW_WORKERS>=2: another shard's sync job may interleave.
await expect
.poll(async () => verifyUserInChannel(adminClient, engineerOfficeUser.id, privateChannel.id), {
timeout: 30_000,
intervals: [500, 1000, 2000],
message: 'engineerOfficeUser should be AUTO-ADDED (satisfies simpler Dept-only policy)',
})
.toBe(true);
await expect
.poll(async () => verifyUserInChannel(adminClient, engineerRemoteUser.id, privateChannel.id), {
timeout: 30_000,
intervals: [500, 1000, 2000],
message: 'engineerRemoteUser should still be in channel',
})
.toBe(true);
await expect
.poll(async () => verifyUserInChannel(adminClient, salesRemoteUser.id, privateChannel.id), {
timeout: 30_000,
intervals: [500, 1000, 2000],
message: 'salesRemoteUser should NOT be in channel',
})
.toBe(false);
});
/**
* MM-63848: Renaming a policy to a name that already exists should show an error
*/
test('MM-63848 Should show error when renaming policy to an existing name', async ({pw}) => {
await pw.skipIfNoLicense();
const {adminUser, adminClient, team} = await pw.initSetup();
await enableUserManagedAttributes(adminClient);
const departmentAttribute: CustomProfileAttribute[] = [{name: 'Department', type: 'text', value: ''}];
await setupCustomProfileAttributeFields(adminClient, departmentAttribute);
const privateChannel = await createPrivateChannelForABAC(adminClient, team.id);
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
const page = systemConsolePage.page;
await navigateToABACPage(page);
await enableABAC(page);
// Create two policies with different names
const policyName1 = `Edit Dup Test A ${pw.random.id()}`;
await createBasicPolicy(page, {
name: policyName1,
attribute: 'Department',
operator: '==',
value: 'Engineering',
autoSync: false,
channels: [privateChannel.display_name],
});
await navigateToABACPage(page);
const privateChannel2 = await createPrivateChannelForABAC(adminClient, team.id);
const policyName2 = `Edit Dup Test B ${pw.random.id()}`;
await createBasicPolicy(page, {
name: policyName2,
attribute: 'Department',
operator: '==',
value: 'Sales',
autoSync: false,
channels: [privateChannel2.display_name],
});
// Navigate back and edit policy2's name to match policy1
await navigateToABACPage(page);
await page.waitForTimeout(1000);
// Search for the second policy
const policySearchInput = page.locator('input[placeholder*="Search" i]').first();
if (await policySearchInput.isVisible({timeout: 3000})) {
await policySearchInput.fill(policyName2);
await page.waitForTimeout(1000);
}
const policyRow = page.locator('tr.clickable, .DataGrid_row').filter({hasText: policyName2}).first();
await policyRow.waitFor({state: 'visible', timeout: 10000});
await policyRow.click();
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
// Change the name to match the first policy
const nameInput = page.locator('#admin\\.access_control\\.policy\\.edit_policy\\.policyName');
await nameInput.waitFor({state: 'visible', timeout: 10000});
await nameInput.fill('');
await nameInput.fill(policyName1);
// Save and expect failure
const saveButton = page.getByRole('button', {name: 'Save'});
await saveButton.click();
await page.waitForTimeout(2000);
// Handle confirmation modal if it appears
const applyPolicyButton = page.getByRole('button', {name: /apply policy/i});
if (await applyPolicyButton.isVisible({timeout: 3000}).catch(() => false)) {
await applyPolicyButton.click();
await page.waitForTimeout(2000);
}
// Verify error message is shown
const errorMessage = page.locator('.EditPolicy__error');
await expect(errorMessage).toBeVisible({timeout: 5000});
const errorText = await errorMessage.textContent();
expect(errorText).toContain('A policy with this name already exists');
});
});

View File

@@ -0,0 +1,549 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {
expect,
getAdminClient,
test,
navigateToABACPage,
verifyUserInChannel,
verifyUserNotInChannel,
} from '@mattermost/playwright-lib';
import {
createUserForABAC,
testAccessRule,
createPrivateChannelForABAC,
createBasicPolicy,
createAdvancedPolicy,
waitForLatestSyncJob,
getPolicyIdByName,
enableUserManagedAttributes,
} from '../support';
import {deleteFieldFromDB} from '../masking/masking_db_setup';
// Restore AccessControlSettings to the shared baseline expected by
// `specs/test_setup.ts` (ABAC enabled) after this file's tests complete, so
// later files on the same worker see the expected setup-state.
test.afterAll(async () => {
try {
const {adminClient} = await getAdminClient({skipLog: true});
await adminClient.patchConfig({
AccessControlSettings: {
EnableAttributeBasedAccessControl: true,
EnableUserManagedAttributes: true,
},
} as any);
} catch {
// Best-effort cleanup.
}
});
/**
* MM-T5791: Editing existing access policy to add another attribute applies access control as specified (with auto-add)
*
* Precondition: At least one policy in existence
*
* Step 1:
* 1. Go to ABAC page, click a policy to edit. Ensure Auto-add is TRUE
* 2. Edit an existing policy rule to add another attribute/value
* 3. Click Test Access Rule, observe users who satisfy the policy
* 4. Save the changes
* 5. User who satisfies NEWLY EDITED policy but not in channel → auto-ADDED
* 6. User who doesn't satisfy NEWLY EDITED policy and is in channel → auto-REMOVED
*
* Expected:
* - User satisfying new multi-attribute policy IS auto-added
* - User not satisfying new policy IS auto-removed
*/
test('MM-T5791 Editing policy to add attribute with auto-add enabled', async ({pw}) => {
test.setTimeout(180000);
await pw.skipIfNoLicense();
const {adminUser, adminClient, team} = await pw.initSetup();
// Ensure Office field exists with correct managed:admin permissions.
// Fields created by older server versions may lack permission_values, causing 403
// when setting attribute values — delete and recreate if permission_values is missing.
// Legacy fields with permission_field=null reject API deletion (403), so fall back to DB.
const existingFields = await adminClient.getCustomProfileAttributeFields();
const attributeFieldsMap: Record<string, any> = {};
for (const field of existingFields) {
attributeFieldsMap[field.id] = field;
}
const existingOffice = existingFields.find((f: any) => f.name === 'Office') as any;
if (!existingOffice || !(existingOffice as any).permission_values) {
if (existingOffice) {
try {
await adminClient.deleteCustomProfileAttributeField(existingOffice.id);
} catch {
await deleteFieldFromDB(existingOffice.id);
}
delete attributeFieldsMap[existingOffice.id];
}
const officeField = await adminClient.createCustomProfileAttributeField({
name: 'Office',
type: 'text',
attrs: {managed: 'admin', visibility: 'when_set', sort_order: 1},
} as any);
attributeFieldsMap[officeField.id] = officeField;
}
// Wait for attributes to be indexed
await new Promise((resolve) => setTimeout(resolve, 2000));
// Create users:
// 1. engineerRemoteUser: Dept=Engineering, Office=Remote → satisfies BOTH (after edit)
// 2. engineerOfficeUser: Dept=Engineering, Office=HQ → satisfies ORIGINAL only, NOT the edited policy
// 3. salesUser: Dept=Sales → doesn't satisfy any policy
const engineerRemoteUser = await createUserForABAC(adminClient, attributeFieldsMap, [
{name: 'Department', type: 'text', value: 'Engineering'},
{name: 'Office', type: 'text', value: 'Remote'},
]);
const engineerOfficeUser = await createUserForABAC(adminClient, attributeFieldsMap, [
{name: 'Department', type: 'text', value: 'Engineering'},
{name: 'Office', type: 'text', value: 'HQ'},
]);
const salesUser = await createUserForABAC(adminClient, attributeFieldsMap, [
{name: 'Department', type: 'text', value: 'Sales'},
]);
// Add users to team
await adminClient.addToTeam(team.id, engineerRemoteUser.id);
await adminClient.addToTeam(team.id, engineerOfficeUser.id);
await adminClient.addToTeam(team.id, salesUser.id);
// Create channel and add engineerOfficeUser (satisfies original policy)
const privateChannel = await createPrivateChannelForABAC(adminClient, team.id);
await adminClient.addToChannel(engineerOfficeUser.id, privateChannel.id);
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
const page = systemConsolePage.page;
await navigateToABACPage(page);
// ===========================================
// PRECONDITION: Create ORIGINAL policy with ONE attribute (Department=Engineering)
// Auto-add ON so users are auto-added
// ===========================================
const policyName = `ABAC-AddAttr-Test-${await pw.random.id()}`;
await createBasicPolicy(page, {
name: policyName,
attribute: 'Department',
operator: '==',
value: 'Engineering',
autoSync: true, // Auto-add is ON
channels: [privateChannel.display_name],
});
(await getPolicyIdByName(adminClient, policyName))!;
// Wait for automatic sync to complete
await page.waitForTimeout(500);
// Verify initial state after original policy sync
await verifyUserInChannel(adminClient, engineerRemoteUser.id, privateChannel.id);
await verifyUserInChannel(adminClient, engineerOfficeUser.id, privateChannel.id);
// ===========================================
// STEP 1-2: Edit policy to ADD another attribute (Office=Remote)
// New expression: Department=Engineering AND Office=Remote
// ===========================================
// Navigate back to ABAC list page
await page.goto('/admin_console/system_attributes/membership_policies', {waitUntil: 'networkidle'});
await page.waitForTimeout(2000);
// Verify we're on the list page by checking for "Add policy" button
const addPolicyButton = page.getByRole('button', {name: 'Add policy'});
await addPolicyButton.waitFor({state: 'visible', timeout: 10000});
// Try to find the policy row first without search
const policyRowLocator = page.locator('tr.clickable, .DataGrid_row').filter({hasText: policyName}).first();
let isPolicyVisible = await policyRowLocator.isVisible({timeout: 3000}).catch(() => false);
// If not visible, use search
if (!isPolicyVisible) {
const policySearchInput = page
.locator('.DataGrid input[type="text"], input[placeholder*="Search policies" i]')
.first();
if (await policySearchInput.isVisible({timeout: 3000})) {
await policySearchInput.fill(policyName);
}
// Re-bind and poll — grid refresh under parallel load may be delayed.
await expect
.poll(() => policyRowLocator.isVisible(), {
timeout: 20_000,
message: `policy "${policyName}" should appear in grid after search`,
})
.toBe(true);
isPolicyVisible = true;
}
// Click policy to edit
await policyRowLocator.waitFor({state: 'visible', timeout: 15000});
await policyRowLocator.click();
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
// Check if "Add attribute" button is disabled (means attributes not loaded)
const addAttributeButtonCheck = page.getByRole('button', {name: /add attribute/i});
if (await addAttributeButtonCheck.isVisible({timeout: 2000})) {
const isDisabled = await addAttributeButtonCheck.isDisabled();
if (isDisabled) {
await page.reload();
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
}
}
// Stay in Simple Mode and add a second attribute row
const addAttributeButton = page.getByRole('button', {name: /add attribute/i});
await addAttributeButton.waitFor({state: 'visible', timeout: 5000});
await addAttributeButton.click();
await page.waitForTimeout(1000);
// The attribute dropdown opens automatically after clicking "Add attribute"
const attributeMenu = page.locator('[id^="attribute-selector-menu"]');
await expect
.poll(() => attributeMenu.isVisible(), {
timeout: 15_000,
message: 'attribute dropdown should appear',
})
.toBe(true);
const officeOption = attributeMenu.locator('li:has-text("Office")').first();
await expect
.poll(() => officeOption.isVisible(), {
timeout: 15_000,
message: 'Office option should be visible in attribute dropdown',
})
.toBe(true);
await officeOption.click({force: true});
await page.waitForTimeout(500);
// Select operator "==" (is)
const operatorButton = page.locator('[data-testid="operatorSelectorMenuButton"]').last();
await operatorButton.waitFor({state: 'visible', timeout: 10_000});
await operatorButton.click({force: true});
await page.waitForTimeout(500);
const operatorOption = page.locator('[id^="operator-selector-menu"] li:has-text("is")').first();
await operatorOption.click({force: true});
await page.waitForTimeout(500);
// Fill value "Remote"
const valueInput = page.locator('.values-editor__simple-input').last();
await valueInput.waitFor({state: 'visible', timeout: 10_000});
await valueInput.fill('Remote');
await page.waitForTimeout(500);
// ===========================================
// STEP 3: Test Access Rule
// ===========================================
await testAccessRule(page);
// ===========================================
// STEP 4: Save the changes
// ===========================================
// Intercept the sync-job POST triggered by "Apply policy" so we can poll the
// exact job ID instead of using the racy UI-table path.
const editSyncJobIdPromise = page
.waitForResponse((r) => r.url().includes('/api/v4/jobs') && r.request().method() === 'POST', {timeout: 15_000})
.then(async (r) => (r.ok() ? (((await r.json()) as {id?: string}).id ?? null) : null))
.catch(() => null);
const saveButton = page.getByRole('button', {name: 'Save'});
await saveButton.waitFor({state: 'visible', timeout: 5000});
await saveButton.click();
await page.waitForTimeout(1000);
// Handle "Apply policy" confirmation if it appears
const applyPolicyButton = page.getByRole('button', {name: /apply policy/i});
if (await applyPolicyButton.isVisible({timeout: 3000})) {
await applyPolicyButton.click();
await page.waitForTimeout(1000);
}
// Navigate to ABAC page and wait for auto-triggered sync job
await navigateToABACPage(page);
await page.waitForTimeout(500);
// Wait for the auto-triggered sync job to complete (policy edit triggers sync automatically)
const editSyncJobId = await editSyncJobIdPromise;
await waitForLatestSyncJob(page, 10, editSyncJobId);
// ===========================================
// STEP 5 & 6: Verify channel membership after edit
// ===========================================
// Poll under PW_WORKERS>=2: another shard's sync job may briefly change
// membership after we read it, so we retry until stable.
await expect
.poll(async () => verifyUserInChannel(adminClient, engineerRemoteUser.id, privateChannel.id), {
timeout: 30_000,
intervals: [500, 1000, 2000],
message: 'engineerRemoteUser should be in channel (satisfies BOTH attributes)',
})
.toBe(true);
await expect
.poll(async () => verifyUserInChannel(adminClient, engineerOfficeUser.id, privateChannel.id), {
timeout: 30_000,
intervals: [500, 1000, 2000],
message: 'engineerOfficeUser should be REMOVED (does not satisfy new policy)',
})
.toBe(false);
await expect
.poll(async () => verifyUserInChannel(adminClient, salesUser.id, privateChannel.id), {
timeout: 30_000,
intervals: [500, 1000, 2000],
message: 'salesUser should not be in channel',
})
.toBe(false);
});
/**
* MM-T5792: Editing existing access policy to remove one of the rules applies access control as specified (with auto-add)
*
* Precondition: At least one policy with MULTIPLE rules in existence
*
* Step 1:
* 1. Go to ABAC page, click a policy to edit. Ensure Auto-add is TRUE
* 2. Edit policy to REMOVE one of the rules (attribute/value)
* 3. Click Test Access Rule, observe users who satisfy the policy
* 4. Save the changes
* 5. User who satisfies newly edited (simpler) policy but not in channel → auto-ADDED
* 6. User who no longer satisfies newly edited policy and is in channel → auto-REMOVED
*
* This is the OPPOSITE of MM-T5791:
* - MM-T5791: ADD rule → policy MORE restrictive
* - MM-T5792: REMOVE rule → policy LESS restrictive
*/
test('MM-T5792 Editing policy to remove attribute rule with auto-add enabled', async ({pw}) => {
test.setTimeout(180000);
await pw.skipIfNoLicense();
const {adminUser, adminClient, team} = await pw.initSetup();
// Ensure Office field exists with correct managed:admin permissions.
// Fields created by older server versions may lack permission_values, causing 403
// when setting attribute values — delete and recreate if permission_values is missing.
// Legacy fields with permission_field=null reject API deletion (403), so fall back to DB.
const existingFields = await adminClient.getCustomProfileAttributeFields();
const attributeFieldsMap: Record<string, any> = {};
for (const field of existingFields) {
attributeFieldsMap[field.id] = field;
}
const existingOffice = existingFields.find((f: any) => f.name === 'Office') as any;
if (!existingOffice || !(existingOffice as any).permission_values) {
if (existingOffice) {
try {
await adminClient.deleteCustomProfileAttributeField(existingOffice.id);
} catch {
await deleteFieldFromDB(existingOffice.id);
}
delete attributeFieldsMap[existingOffice.id];
}
const officeField = await adminClient.createCustomProfileAttributeField({
name: 'Office',
type: 'text',
attrs: {managed: 'admin', visibility: 'when_set', sort_order: 1},
} as any);
attributeFieldsMap[officeField.id] = officeField;
}
// Wait for attributes to be indexed
await new Promise((resolve) => setTimeout(resolve, 2000));
await enableUserManagedAttributes(adminClient);
// Create users:
// 1. engineerRemoteUser: Dept=Engineering, Office=Remote → satisfies ORIGINAL (both rules)
// 2. engineerOfficeUser: Dept=Engineering, Office=HQ → satisfies EDITED policy (Dept only)
// 3. salesRemoteUser: Dept=Sales, Office=Remote → doesn't satisfy (wrong Dept)
const engineerRemoteUser = await createUserForABAC(adminClient, attributeFieldsMap, [
{name: 'Department', type: 'text', value: 'Engineering'},
{name: 'Office', type: 'text', value: 'Remote'},
]);
const engineerOfficeUser = await createUserForABAC(adminClient, attributeFieldsMap, [
{name: 'Department', type: 'text', value: 'Engineering'},
{name: 'Office', type: 'text', value: 'HQ'},
]);
const salesRemoteUser = await createUserForABAC(adminClient, attributeFieldsMap, [
{name: 'Department', type: 'text', value: 'Sales'},
{name: 'Office', type: 'text', value: 'Remote'},
]);
// Add users to team
await adminClient.addToTeam(team.id, engineerRemoteUser.id);
await adminClient.addToTeam(team.id, engineerOfficeUser.id);
await adminClient.addToTeam(team.id, salesRemoteUser.id);
// Create channel and add salesRemoteUser (does NOT satisfy any policy)
const privateChannel = await createPrivateChannelForABAC(adminClient, team.id);
await adminClient.addToChannel(salesRemoteUser.id, privateChannel.id);
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
const page = systemConsolePage.page;
await navigateToABACPage(page);
// ===========================================
// PRECONDITION: Create ORIGINAL policy with TWO attributes
// Department=Engineering AND Office=Remote, Auto-add ON
// ===========================================
const policyName = `ABAC-RemoveRule-${await pw.random.id()}`;
await createAdvancedPolicy(page, {
name: policyName,
celExpression: 'user.attributes.Department == "Engineering" && user.attributes.Office == "Remote"',
autoSync: true, // Auto-add is ON
channels: [privateChannel.display_name],
});
(await getPolicyIdByName(adminClient, policyName))!;
// Wait for automatic sync to complete
await page.waitForTimeout(500);
// Verify initial state after original policy sync
await verifyUserInChannel(adminClient, engineerRemoteUser.id, privateChannel.id);
await verifyUserNotInChannel(adminClient, engineerOfficeUser.id, privateChannel.id);
await verifyUserNotInChannel(adminClient, salesRemoteUser.id, privateChannel.id);
// ===========================================
// STEP 1-2: Edit policy to REMOVE Location rule
// New expression: Department=Engineering (only)
// ===========================================
await page.goto('/admin_console/system_attributes/membership_policies', {waitUntil: 'networkidle'});
await page.waitForTimeout(500);
const addPolicyButton = page.getByRole('button', {name: 'Add policy'});
await addPolicyButton.waitFor({state: 'visible', timeout: 10000});
// Wait for the exact policy row to appear with retry — under parallel load the
// grid update from the server may lag behind the page load.
const policyRowLocator = page.locator('tr.clickable, .DataGrid_row').filter({hasText: policyName}).first();
const found = await policyRowLocator.isVisible({timeout: 3000}).catch(() => false);
if (!found) {
const policySearchInput = page
.locator('.DataGrid input[type="text"], input[placeholder*="Search policies" i]')
.first();
if (await policySearchInput.isVisible({timeout: 3000})) {
await policySearchInput.fill(policyName);
}
const policyRow = () => page.locator('tr.clickable, .DataGrid_row').filter({hasText: policyName}).first();
await expect
.poll(() => policyRow().isVisible(), {
timeout: 45_000,
intervals: [500, 1000, 2000, 3000],
message: `policy "${policyName}" should appear in grid after search`,
})
.toBe(true);
}
await page.locator('tr.clickable, .DataGrid_row').filter({hasText: policyName}).first().click();
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
// Verify Auto-add is ON
const autoAddCheckbox = page.locator('#auto-add-header-checkbox');
if (await autoAddCheckbox.isVisible({timeout: 3000})) {
const isChecked = await autoAddCheckbox.isChecked();
if (!isChecked) {
await autoAddCheckbox.click();
await page.waitForTimeout(500);
}
}
// Remove the Office rule in Simple mode (table editor). Opening an Advanced-created policy
// can leave the UI in CEL mode; "Switch to Advanced Mode" stays disabled while attributes load.
const switchToSimpleButton = page.getByRole('button', {name: /switch to simple mode/i});
if (await switchToSimpleButton.isVisible({timeout: 5000}).catch(() => false)) {
await expect(switchToSimpleButton).toBeEnabled({timeout: 60_000});
await switchToSimpleButton.click();
await page.waitForTimeout(500);
}
const officeRowRemove = page
.locator('.table-editor__row')
.filter({hasText: 'Office'})
.getByRole('button', {name: 'Remove row'});
await expect(officeRowRemove).toBeVisible({timeout: 15_000});
await officeRowRemove.click();
await page.waitForTimeout(500);
// ===========================================
// STEP 3: Test Access Rule
// ===========================================
await testAccessRule(page);
// ===========================================
// STEP 4: Save the changes
// ===========================================
// Intercept the sync-job POST triggered by "Apply policy" so we can poll the
// exact job ID instead of using the racy UI-table path.
const editSyncJobIdPromiseT5792 = page
.waitForResponse((r) => r.url().includes('/api/v4/jobs') && r.request().method() === 'POST', {timeout: 15_000})
.then(async (r) => (r.ok() ? (((await r.json()) as {id?: string}).id ?? null) : null))
.catch(() => null);
const saveButton = page.getByRole('button', {name: 'Save'});
await saveButton.waitFor({state: 'visible', timeout: 5000});
await saveButton.click();
await page.waitForTimeout(1000);
const applyPolicyButton = page.getByRole('button', {name: /apply policy/i});
if (await applyPolicyButton.isVisible({timeout: 3000})) {
await applyPolicyButton.click();
await page.waitForTimeout(1000);
}
await navigateToABACPage(page);
const editSyncJobIdT5792 = await editSyncJobIdPromiseT5792;
await waitForLatestSyncJob(page, 10, editSyncJobIdT5792);
// ===========================================
// STEP 5 & 6: Verify channel membership after edit
// ===========================================
// Re-apply guard: a concurrent initSetup() may have reset ABAC between the policy save
// and the sync job completing. Without ABAC enabled the sync job is a no-op.
await adminClient.patchConfig({
AccessControlSettings: {
EnableAttributeBasedAccessControl: true,
EnableUserManagedAttributes: true,
},
} as any);
// Poll under PW_WORKERS>=2: other shards' sync jobs may briefly change membership.
await expect
.poll(async () => verifyUserInChannel(adminClient, engineerOfficeUser.id, privateChannel.id), {
timeout: 30_000,
intervals: [500, 1000, 2000],
message: 'engineerOfficeUser should be AUTO-ADDED (satisfies simpler Dept-only policy)',
})
.toBe(true);
await expect
.poll(async () => verifyUserInChannel(adminClient, engineerRemoteUser.id, privateChannel.id), {
timeout: 30_000,
intervals: [500, 1000, 2000],
message: 'engineerRemoteUser should still be in channel',
})
.toBe(true);
await expect
.poll(async () => verifyUserInChannel(adminClient, salesRemoteUser.id, privateChannel.id), {
timeout: 30_000,
intervals: [500, 1000, 2000],
message: 'salesRemoteUser should NOT be in channel',
})
.toBe(false);
});