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 Nightly Race / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (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,486 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {Page} from '@playwright/test';
import {
expect,
test,
enableABAC,
navigateToABACPage,
runSyncJob,
verifyUserInChannel,
createUserWithAttributes,
getAdminClient,
TestBrowser,
getRandomId,
} from '@mattermost/playwright-lib';
import type {CustomProfileAttribute} from '../../../channels/custom_profile_attributes/helpers';
import {setupCustomProfileAttributeFields} from '../../../channels/custom_profile_attributes/helpers';
import {
ensureUserAttributes,
createUserForABAC,
testAccessRule,
createPrivateChannelForABAC,
createAdvancedPolicy,
activatePolicy,
waitForLatestSyncJob,
waitForPolicySyncJob,
getJobDetailsFromRecentJobs,
enableUserManagedAttributes,
assertAccessControlAutocompleteContains,
} from '../support';
/**
* ABAC Policies - Advanced Policies
*
* The previous monolithic MM-T5785 and MM-T5786 tests each did 3–5 independent
* assertions wrapped around one very-expensive sync-job cycle. They are now
* split into separate `test(...)` blocks — either sharing a single beforeAll
* (MM-T5785) or split into one-operator-per-test (MM-T5786) so the whole
* test-file parallelises cleanly under sharding.
*/
test.describe('ABAC Policies - Advanced Policies - MM-T5785 all attribute types (auto-add)', () => {
let sharedAdminClient: any;
let user1: Awaited<ReturnType<typeof createUserWithAttributes>>; // qualifying, NOT in channel
let user2: Awaited<ReturnType<typeof createUserWithAttributes>>; // qualifying, IN channel
let user3: Awaited<ReturnType<typeof createUserWithAttributes>>; // non-qualifying, IN channel
let privateChannel: any;
let licensed = true;
test.beforeAll(async ({browser}) => {
test.setTimeout(240000);
const {adminClient, adminUser} = await getAdminClient();
if (!adminUser) {
throw new Error('Admin user not found');
}
sharedAdminClient = adminClient;
// License gate
try {
const lic = await adminClient.getClientLicenseOld();
if (!lic || lic.IsLicensed !== 'true') {
licensed = false;
return;
}
} catch {
licensed = false;
return;
}
await ensureUserAttributes(adminClient);
const suffix = getRandomId();
const team = await adminClient.createTeam({
name: `abac-${suffix}`,
display_name: `ABAC ${suffix}`,
type: 'O',
} as any);
// Three users with different attribute combinations
user1 = await createUserWithAttributes(adminClient, {Department: 'Engineering'});
user2 = await createUserWithAttributes(adminClient, {Department: 'Engineering'});
user3 = await createUserWithAttributes(adminClient, {Department: 'Sales'});
await adminClient.addToTeam(team.id, user1.id);
await adminClient.addToTeam(team.id, user2.id);
await adminClient.addToTeam(team.id, user3.id);
// Attribute indexing settle
await new Promise((resolve) => setTimeout(resolve, 2000));
privateChannel = await createPrivateChannelForABAC(adminClient, team.id);
await adminClient.addToChannel(user2.id, privateChannel.id);
await adminClient.addToChannel(user3.id, privateChannel.id);
// Drive policy creation via the system-console UI exactly once.
const tb = new TestBrowser(browser);
try {
const {systemConsolePage} = await tb.login(adminUser);
await navigateToABACPage(systemConsolePage.page);
await enableABAC(systemConsolePage.page);
const policyName = `Multi-Attr Policy ${getRandomId()}`;
const celExpression = 'user.attributes.Department == "Engineering"';
await createAdvancedPolicy(systemConsolePage.page, {
name: policyName,
celExpression,
autoSync: true,
channels: [privateChannel.display_name],
});
await systemConsolePage.page.waitForTimeout(1000);
const searchInput = systemConsolePage.page.locator('input[placeholder*="Search" i]').first();
await searchInput.waitFor({state: 'visible', timeout: 5000});
const idMatch = policyName.match(/([a-z0-9]+)$/i);
const uniqueId = idMatch ? idMatch[1] : policyName;
await searchInput.fill(uniqueId);
await systemConsolePage.page.waitForTimeout(1000);
const policyRow = systemConsolePage.page.locator('.policy-name').first();
const policyElementId = await policyRow.getAttribute('id');
const policyId = policyElementId?.replace('customDescription-', '');
if (!policyId) {
throw new Error('Could not get policy ID');
}
await searchInput.clear();
await activatePolicy(adminClient, policyId);
await waitForPolicySyncJob(adminClient, policyId);
const jobId1 = await runSyncJob(systemConsolePage.page);
await waitForLatestSyncJob(systemConsolePage.page, 10, jobId1);
try {
await getJobDetailsFromRecentJobs(systemConsolePage.page, privateChannel.display_name);
} catch {
// non-fatal
}
// Optional extra sync if user1 not added yet
const added = await verifyUserInChannel(adminClient, user1.id, privateChannel.id);
if (!added) {
const jobId2 = await runSyncJob(systemConsolePage.page);
await waitForLatestSyncJob(systemConsolePage.page, 10, jobId2);
await systemConsolePage.page.waitForTimeout(2000);
}
} finally {
await tb.close().catch(() => {});
}
});
test('MM-T5785_a auto-adds qualifying user who was not in channel', async () => {
test.setTimeout(60000);
test.skip(!licensed, 'No ABAC license');
await expect
.poll(async () => verifyUserInChannel(sharedAdminClient, user1.id, privateChannel.id), {
timeout: 30_000,
intervals: [500, 1000, 2000],
message: 'user1 should be auto-added to channel',
})
.toBe(true);
});
test('MM-T5785_b keeps qualifying user who was already in channel', async () => {
test.setTimeout(60000);
test.skip(!licensed, 'No ABAC license');
await expect
.poll(async () => verifyUserInChannel(sharedAdminClient, user2.id, privateChannel.id), {
timeout: 30_000,
intervals: [500, 1000, 2000],
message: 'user2 should stay in channel',
})
.toBe(true);
});
test('MM-T5785_c auto-removes non-qualifying user who was in channel', async () => {
test.setTimeout(60000);
test.skip(!licensed, 'No ABAC license');
await expect
.poll(async () => verifyUserInChannel(sharedAdminClient, user3.id, privateChannel.id), {
timeout: 30_000,
intervals: [500, 1000, 2000],
message: 'user3 should be auto-removed from channel',
})
.toBe(false);
});
});
/**
* MM-T5786: Attribute-based access policy using operator variations in Simple mode.
*
* Splits the original test's 5 operator sequences into 5 independent tests
* that share a single beforeAll (which creates the shared team, attributes,
* engineer/sales users, and admin-logged-in system-console page). Each
* operator then adds its own channel + policy and verifies independently.
*
* Tests run serially within the same worker for this file, so mutations on
* the shared page don't race.
*/
test.describe('ABAC Policies - Advanced Policies - MM-T5786 operator variants', () => {
test.describe.configure({mode: 'serial'});
let sharedAdminClient: any;
let sharedTeamId: string;
let engineerUser: Awaited<ReturnType<typeof createUserForABAC>>;
let salesUser: Awaited<ReturnType<typeof createUserForABAC>>;
let deptFieldName: string;
let systemConsolePage: {page: Page};
let sharedTestBrowser: TestBrowser | null = null;
let licensed = true;
test.beforeAll(async ({browser}) => {
test.setTimeout(180000);
const {adminClient, adminUser} = await getAdminClient();
if (!adminUser) {
throw new Error('Admin user not found');
}
sharedAdminClient = adminClient;
try {
const lic = await adminClient.getClientLicenseOld();
if (!lic || lic.IsLicensed !== 'true') {
licensed = false;
return;
}
} catch {
licensed = false;
return;
}
await enableUserManagedAttributes(adminClient);
const suffix = getRandomId();
deptFieldName = `MM5786_Dept_${suffix}`;
const attributeFields: CustomProfileAttribute[] = [
{name: deptFieldName, type: 'text', value: '', attrs: {managed: 'admin', visibility: 'when_set'}},
];
const attributeFieldsMap = await setupCustomProfileAttributeFields(adminClient, attributeFields);
await assertAccessControlAutocompleteContains(adminClient, [deptFieldName]);
engineerUser = await createUserForABAC(adminClient, attributeFieldsMap, [
{name: deptFieldName, type: 'text', value: 'Engineering'},
]);
salesUser = await createUserForABAC(adminClient, attributeFieldsMap, [
{name: deptFieldName, type: 'text', value: 'Sales'},
]);
const teamSuffix = getRandomId();
const team = await adminClient.createTeam({
name: `abac-ops-${teamSuffix}`,
display_name: `ABAC-Ops ${teamSuffix}`,
type: 'O',
} as any);
sharedTeamId = team.id;
await adminClient.addToTeam(sharedTeamId, engineerUser.id);
await adminClient.addToTeam(sharedTeamId, salesUser.id);
sharedTestBrowser = new TestBrowser(browser);
const loggedIn = await sharedTestBrowser.login(adminUser);
systemConsolePage = loggedIn.systemConsolePage;
await navigateToABACPage(systemConsolePage.page);
await enableABAC(systemConsolePage.page);
});
test.afterAll(async () => {
await sharedTestBrowser?.close().catch(() => {});
});
async function runOperatorCase(celExpression: string, namePrefix: string, searchTerm: string) {
const channel = await createPrivateChannelForABAC(sharedAdminClient, sharedTeamId);
await sharedAdminClient.addToChannel(salesUser.id, channel.id);
await navigateToABACPage(systemConsolePage.page);
const policyName = `${namePrefix} Policy ${getRandomId()}`;
await createAdvancedPolicy(systemConsolePage.page, {
name: policyName,
celExpression: celExpression.replaceAll('user.attributes.Department', `user.attributes.${deptFieldName}`),
autoSync: true,
channels: [channel.display_name],
});
await systemConsolePage.page.waitForTimeout(1000);
const policyRowForTest = systemConsolePage.page.locator('.policy-name').filter({hasText: policyName}).first();
if (await policyRowForTest.isVisible({timeout: 3000})) {
await policyRowForTest.click();
await systemConsolePage.page.waitForLoadState('networkidle');
await testAccessRule(systemConsolePage.page, {
expectedMatchingUsers: [engineerUser.username],
expectedNonMatchingUsers: [salesUser.username],
});
await navigateToABACPage(systemConsolePage.page);
}
const searchInput = systemConsolePage.page.locator('input[placeholder*="Search" i]').first();
await searchInput.fill(searchTerm);
// Wait for the exact policy row to appear instead of grabbing .first() blindly.
// Under parallel load the grid update may be delayed, and .first() can return a
// policy created by another concurrent test.
const policyRow = systemConsolePage.page.locator('.policy-name').filter({hasText: policyName}).first();
await expect
.poll(() => policyRow.isVisible(), {
timeout: 45_000,
intervals: [200, 500, 1000, 2000],
message: `policy row for "${policyName}" should appear in search results`,
})
.toBe(true);
const policyId = (await policyRow.getAttribute('id'))?.replace('customDescription-', '');
if (policyId) {
await activatePolicy(sharedAdminClient, policyId);
const jobId3 = await runSyncJob(systemConsolePage.page);
await waitForLatestSyncJob(systemConsolePage.page, 15, jobId3);
}
await searchInput.clear();
// Poll under PW_WORKERS>=2: another shard's sync job may interleave.
await expect
.poll(async () => verifyUserInChannel(sharedAdminClient, engineerUser.id, channel.id), {
timeout: 90_000,
intervals: [500, 1000, 2000, 4000],
message: 'engineerUser should be in channel',
})
.toBe(true);
await expect
.poll(async () => verifyUserInChannel(sharedAdminClient, salesUser.id, channel.id), {
timeout: 90_000,
intervals: [500, 1000, 2000, 4000],
message: 'salesUser should not be in channel',
})
.toBe(false);
}
test('MM-T5786_a is-not (!=) operator', async () => {
test.setTimeout(90000);
test.skip(!licensed, 'No ABAC license');
await runOperatorCase('user.attributes.Department != "Sales"', 'IsNot', 'IsNot');
});
test('MM-T5786_b in operator', async () => {
test.setTimeout(90000);
test.skip(!licensed, 'No ABAC license');
await runOperatorCase('user.attributes.Department in ["Engineering", "DevOps"]', 'In', 'In Policy');
});
test('MM-T5786_c starts-with operator', async () => {
test.setTimeout(90000);
test.skip(!licensed, 'No ABAC license');
await runOperatorCase('user.attributes.Department.startsWith("Eng")', 'StartsWith', 'StartsWith');
});
test('MM-T5786_d ends-with operator', async () => {
test.setTimeout(90000);
test.skip(!licensed, 'No ABAC license');
await runOperatorCase('user.attributes.Department.endsWith("ing")', 'EndsWith', 'EndsWith');
});
test('MM-T5786_e contains operator', async () => {
test.setTimeout(90000);
test.skip(!licensed, 'No ABAC license');
await runOperatorCase('user.attributes.Department.contains("gineer")', 'Contains', 'Contains');
});
});
/**
* MM-T5787: Complex CEL expressions with || and grouping ().
* Kept as a single test because its three verifications are cheap once
* the single sync-job cycle completes.
*/
test.describe('ABAC Policies - Advanced Policies', () => {
test('MM-T5787 Test policy with complex rules in Advanced Mode', async ({pw}) => {
test.setTimeout(120000);
await pw.skipIfNoLicense();
const {adminUser, adminClient, team} = await pw.initSetup();
await enableUserManagedAttributes(adminClient);
const idSuffix = pw.random.id();
const deptFieldName = `MM5787_Dept_${idSuffix}`;
const locationFieldName = `MM5787_Loc_${idSuffix}`;
const attributeFieldsMap = await setupCustomProfileAttributeFields(adminClient, [
{
name: deptFieldName,
type: 'text',
attrs: {managed: 'admin', visibility: 'when_set'},
},
{
name: locationFieldName,
type: 'text',
attrs: {managed: 'admin', visibility: 'when_set'},
},
]);
await assertAccessControlAutocompleteContains(adminClient, [deptFieldName, locationFieldName]);
const engineerUser = await createUserForABAC(adminClient, attributeFieldsMap, [
{name: deptFieldName, value: 'Engineering', type: 'text'},
{name: locationFieldName, value: 'Office', type: 'text'},
]);
const salesRemoteUser = await createUserForABAC(adminClient, attributeFieldsMap, [
{name: deptFieldName, value: 'Sales', type: 'text'},
{name: locationFieldName, value: 'Remote', type: 'text'},
]);
const salesOfficeUser = await createUserForABAC(adminClient, attributeFieldsMap, [
{name: deptFieldName, value: 'Sales', type: 'text'},
{name: locationFieldName, value: 'Office', type: 'text'},
]);
await adminClient.addToTeam(team.id, engineerUser.id);
await adminClient.addToTeam(team.id, salesRemoteUser.id);
await adminClient.addToTeam(team.id, salesOfficeUser.id);
const channel = await createPrivateChannelForABAC(adminClient, team.id);
await adminClient.addToChannel(salesOfficeUser.id, channel.id);
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
await navigateToABACPage(systemConsolePage.page);
await enableABAC(systemConsolePage.page);
await systemConsolePage.page.reload();
await systemConsolePage.page.waitForLoadState('networkidle');
const policyName = `Complex Policy ${pw.random.id()}`;
const complexExpression = `user.attributes.${deptFieldName} == "Engineering" || (user.attributes.${deptFieldName} == "Sales" && user.attributes.${locationFieldName} == "Remote")`;
await createAdvancedPolicy(systemConsolePage.page, {
name: policyName,
celExpression: complexExpression,
autoSync: true,
channels: [channel.display_name],
});
await navigateToABACPage(systemConsolePage.page);
await systemConsolePage.page.waitForTimeout(500);
const searchInput = systemConsolePage.page.locator('input[placeholder*="Search" i]').first();
const policyIdMatch = policyName.match(/([a-z0-9]+)$/i);
const searchTerm = policyIdMatch ? policyIdMatch[1] : policyName;
await searchInput.fill(searchTerm);
await systemConsolePage.page.waitForTimeout(500);
const foundPolicy = systemConsolePage.page.locator('.policy-name').filter({hasText: policyName}).first();
await expect
.poll(() => foundPolicy.isVisible(), {
timeout: 15_000,
message: `policy "${policyName}" should appear after search`,
})
.toBe(true);
const policyId = (await foundPolicy.getAttribute('id'))?.replace('customDescription-', '');
expect(policyId, 'policy row should expose id').toBeTruthy();
await activatePolicy(adminClient, policyId!);
const jobId4 = await runSyncJob(systemConsolePage.page);
await waitForLatestSyncJob(systemConsolePage.page, 5, jobId4);
await searchInput.clear();
// Poll under PW_WORKERS>=2: another shard's sync job may interleave.
await expect
.poll(async () => verifyUserInChannel(adminClient, engineerUser.id, channel.id), {
timeout: 90_000,
intervals: [500, 1000, 2000, 4000],
message: 'engineerUser should be in channel',
})
.toBe(true);
await expect
.poll(async () => verifyUserInChannel(adminClient, salesRemoteUser.id, channel.id), {
timeout: 90_000,
intervals: [500, 1000, 2000, 4000],
message: 'salesRemoteUser should be in channel',
})
.toBe(true);
await expect
.poll(async () => verifyUserInChannel(adminClient, salesOfficeUser.id, channel.id), {
timeout: 90_000,
intervals: [500, 1000, 2000, 4000],
message: 'salesOfficeUser should not be in channel',
})
.toBe(false);
});
});

View File

@@ -0,0 +1,218 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, test, navigateToABACPage, runSyncJob, verifyUserInChannel} from '@mattermost/playwright-lib';
import type {CustomProfileAttribute} from '../../../channels/custom_profile_attributes/helpers';
import {setupCustomProfileAttributeFields} from '../../../channels/custom_profile_attributes/helpers';
import {waitForAttributeViewToInclude} from '../../../channels/team_settings/helpers';
import {
ensureUserAttributes,
createUserForABAC,
testAccessRule,
createPrivateChannelForABAC,
createAdvancedPolicy,
activatePolicy,
waitForPolicySyncJob,
getPolicyIdByName,
} from '../support';
/**
* MM-T5786 (1/5): "is not" (!=) operator — Department != "Sales" with auto-add
*
* @reference https://github.com/mattermost/mattermost-test-management/blob/main/data/test-cases/channels/abac-attribute-based-access/abac-system-admin/MM-T5786.md
*/
test('MM-T5786 Test "is not" (!=) operator in Simple mode', async ({pw}) => {
test.setTimeout(120000);
await pw.skipIfNoLicense();
const {adminUser, adminClient, team} = await pw.initSetup();
const attributeFields: CustomProfileAttribute[] = [{name: 'Department', type: 'text', value: ''}];
const attributeFieldsMap = await setupCustomProfileAttributeFields(adminClient, attributeFields);
const engineerUser = await createUserForABAC(adminClient, attributeFieldsMap, [
{name: 'Department', type: 'text', value: 'Engineering'},
]);
const salesUser = await createUserForABAC(adminClient, attributeFieldsMap, [
{name: 'Department', type: 'text', value: 'Sales'},
]);
await adminClient.addToTeam(team.id, engineerUser.id);
await adminClient.addToTeam(team.id, salesUser.id);
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
await navigateToABACPage(systemConsolePage.page);
const channel = await createPrivateChannelForABAC(adminClient, team.id);
await adminClient.addToChannel(salesUser.id, channel.id);
await ensureUserAttributes(adminClient);
const policyName = `IsNot Policy ${await pw.random.id()}`;
await createAdvancedPolicy(systemConsolePage.page, {
name: policyName,
celExpression: 'user.attributes.Department != "Sales"',
autoSync: true,
channels: [channel.display_name],
});
const policyId = (await getPolicyIdByName(adminClient, policyName))!;
await systemConsolePage.page.waitForTimeout(1000);
const policyRowForTest = systemConsolePage.page.locator('.policy-name').filter({hasText: policyName}).first();
if (await policyRowForTest.isVisible({timeout: 3000})) {
await policyRowForTest.click();
await systemConsolePage.page.waitForLoadState('networkidle');
await testAccessRule(systemConsolePage.page, {
expectedMatchingUsers: [engineerUser.username],
expectedNonMatchingUsers: [salesUser.username],
});
await navigateToABACPage(systemConsolePage.page);
}
await waitForAttributeViewToInclude(adminClient, 'user.attributes.Department != "Sales"', [engineerUser.id]);
await activatePolicy(adminClient, policyId);
await runSyncJob(systemConsolePage.page);
await waitForPolicySyncJob(adminClient, policyId);
await expect
.poll(() => verifyUserInChannel(adminClient, engineerUser.id, channel.id), {
timeout: 15_000,
intervals: [500, 1000, 2000],
message: 'engineerUser should be in channel after sync',
})
.toBe(true);
expect(await verifyUserInChannel(adminClient, salesUser.id, channel.id)).toBe(false);
});
/**
* MM-T5786 (2/5): "in" operator — Department in ["Engineering", "DevOps"] with auto-add
*
* @reference https://github.com/mattermost/mattermost-test-management/blob/main/data/test-cases/channels/abac-attribute-based-access/abac-system-admin/MM-T5786.md
*/
test('MM-T5786 Test "in" operator in Simple mode', async ({pw}) => {
test.setTimeout(120000);
await pw.skipIfNoLicense();
const {adminUser, adminClient, team} = await pw.initSetup();
const attributeFields: CustomProfileAttribute[] = [{name: 'Department', type: 'text', value: ''}];
const attributeFieldsMap = await setupCustomProfileAttributeFields(adminClient, attributeFields);
const engineerUser = await createUserForABAC(adminClient, attributeFieldsMap, [
{name: 'Department', type: 'text', value: 'Engineering'},
]);
const salesUser = await createUserForABAC(adminClient, attributeFieldsMap, [
{name: 'Department', type: 'text', value: 'Sales'},
]);
await adminClient.addToTeam(team.id, engineerUser.id);
await adminClient.addToTeam(team.id, salesUser.id);
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
await navigateToABACPage(systemConsolePage.page);
const channel = await createPrivateChannelForABAC(adminClient, team.id);
await adminClient.addToChannel(salesUser.id, channel.id);
await ensureUserAttributes(adminClient);
const policyName = `In Policy ${await pw.random.id()}`;
await createAdvancedPolicy(systemConsolePage.page, {
name: policyName,
celExpression: 'user.attributes.Department in ["Engineering", "DevOps"]',
autoSync: true,
channels: [channel.display_name],
});
const policyId = (await getPolicyIdByName(adminClient, policyName))!;
await systemConsolePage.page.waitForTimeout(1000);
const policyRowForTest = systemConsolePage.page.locator('.policy-name').filter({hasText: policyName}).first();
if (await policyRowForTest.isVisible({timeout: 3000})) {
await policyRowForTest.click();
await systemConsolePage.page.waitForLoadState('networkidle');
await testAccessRule(systemConsolePage.page, {
expectedMatchingUsers: [engineerUser.username],
expectedNonMatchingUsers: [salesUser.username],
});
await navigateToABACPage(systemConsolePage.page);
}
await waitForAttributeViewToInclude(adminClient, 'user.attributes.Department in ["Engineering", "DevOps"]', [
engineerUser.id,
]);
await activatePolicy(adminClient, policyId);
await runSyncJob(systemConsolePage.page);
await waitForPolicySyncJob(adminClient, policyId);
await expect
.poll(() => verifyUserInChannel(adminClient, engineerUser.id, channel.id), {
timeout: 15_000,
intervals: [500, 1000, 2000],
message: 'engineerUser should be in channel after sync',
})
.toBe(true);
expect(await verifyUserInChannel(adminClient, salesUser.id, channel.id)).toBe(false);
});
/**
* MM-T5786 (3/5): "starts with" operator — Department.startsWith("Eng") with auto-add
*
* @reference https://github.com/mattermost/mattermost-test-management/blob/main/data/test-cases/channels/abac-attribute-based-access/abac-system-admin/MM-T5786.md
*/
test('MM-T5786 Test "starts with" operator in Simple mode', async ({pw}) => {
test.setTimeout(120000);
await pw.skipIfNoLicense();
const {adminUser, adminClient, team} = await pw.initSetup();
const attributeFields: CustomProfileAttribute[] = [{name: 'Department', type: 'text', value: ''}];
const attributeFieldsMap = await setupCustomProfileAttributeFields(adminClient, attributeFields);
const engineerUser = await createUserForABAC(adminClient, attributeFieldsMap, [
{name: 'Department', type: 'text', value: 'Engineering'},
]);
const salesUser = await createUserForABAC(adminClient, attributeFieldsMap, [
{name: 'Department', type: 'text', value: 'Sales'},
]);
await adminClient.addToTeam(team.id, engineerUser.id);
await adminClient.addToTeam(team.id, salesUser.id);
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
await navigateToABACPage(systemConsolePage.page);
const channel = await createPrivateChannelForABAC(adminClient, team.id);
await adminClient.addToChannel(salesUser.id, channel.id);
await ensureUserAttributes(adminClient);
const policyName = `StartsWith Policy ${await pw.random.id()}`;
await createAdvancedPolicy(systemConsolePage.page, {
name: policyName,
celExpression: 'user.attributes.Department.startsWith("Eng")',
autoSync: true,
channels: [channel.display_name],
});
const policyId = (await getPolicyIdByName(adminClient, policyName))!;
await systemConsolePage.page.waitForTimeout(1000);
const policyRowForTest = systemConsolePage.page.locator('.policy-name').filter({hasText: policyName}).first();
if (await policyRowForTest.isVisible({timeout: 3000})) {
await policyRowForTest.click();
await systemConsolePage.page.waitForLoadState('networkidle');
await testAccessRule(systemConsolePage.page, {
expectedMatchingUsers: [engineerUser.username],
expectedNonMatchingUsers: [salesUser.username],
});
await navigateToABACPage(systemConsolePage.page);
}
await waitForAttributeViewToInclude(adminClient, 'user.attributes.Department.startsWith("Eng")', [engineerUser.id]);
await activatePolicy(adminClient, policyId);
await runSyncJob(systemConsolePage.page);
await waitForPolicySyncJob(adminClient, policyId);
await expect
.poll(() => verifyUserInChannel(adminClient, engineerUser.id, channel.id), {
timeout: 15_000,
intervals: [500, 1000, 2000],
message: 'engineerUser should be in channel after sync',
})
.toBe(true);
expect(await verifyUserInChannel(adminClient, salesUser.id, channel.id)).toBe(false);
});

View File

@@ -0,0 +1,181 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/**
* E2E tests for MM-64357 in the Membership Policy editor.
*
* A rule value containing a quote character (e.g. the apostrophe in
* "Matt's Department") must still be recognized as a simple expression. Before
* the fix the quoted-value matcher forbade any inner quote, so such a rule was
* misclassified as complex: the "Switch to Simple Mode" toggle became disabled
* and the admin was trapped in Advanced mode after a save/reopen.
*
* These specs drive the rendered UI (button enabled/disabled state and the
* save -> reopen round-trip) rather than the helper functions directly, which
* is the coverage gap flagged in review.
*/
import type {Client4} from '@mattermost/client';
import type {UserProfile} from '@mattermost/types/users';
import type {UserPropertyField} from '@mattermost/types/properties_user';
import {expect, getRandomId, test} from '@mattermost/playwright-lib';
import {deleteCustomProfileAttributes} from '../../../channels/custom_profile_attributes/helpers';
const APOSTROPHE_VALUE = "Matt's Department";
test.describe('System Console - Membership Policy apostrophe values (MM-64357)', () => {
let adminClient: Client4;
let adminUser: UserProfile;
let field: UserPropertyField | undefined;
test.beforeEach(async ({pw}) => {
await pw.ensureLicense();
await pw.skipIfNoLicense();
const clientInfo = await pw.getAdminClient();
adminClient = clientInfo.adminClient;
adminUser = clientInfo.adminUser!;
// # Create an admin-managed text attribute (admin-managed → usable in ABAC)
field = await adminClient.createCustomProfileAttributeField({
name: `department_${getRandomId()}`,
type: 'text',
attrs: {
sort_order: 0,
managed: 'admin',
},
} as any);
// # Make sure ABAC stays enabled (a concurrent initSetup can reset config)
await adminClient.patchConfig({
AccessControlSettings: {EnableAttributeBasedAccessControl: true},
} as any);
});
test.afterEach(async () => {
if (field) {
await deleteCustomProfileAttributes(adminClient, {
[field.id]: field,
__ownedIds: new Set([field.id]),
} as any);
field = undefined;
}
});
/**
* @objective A rule whose value contains an apostrophe survives a
* save/reopen round-trip: the stored expression rehydrates as a simple
* expression so the "Switch to Simple Mode" toggle stays enabled (the admin
* is not trapped in Advanced mode) and the table editor re-renders the same
* value.
*
* @precondition
* An admin-managed text attribute exists.
*/
test('round-trips an apostrophe "is" value and keeps the mode toggle switchable', {tag: '@abac'}, async ({pw}) => {
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
const {page} = systemConsolePage;
const policyName = `Apostrophe RT ${getRandomId()}`;
let policyId: string | null = null;
try {
await page.goto('/admin_console/system_attributes/membership_policies');
await page.waitForLoadState('networkidle');
// # Create a policy and add a text attribute row
await page.getByRole('button', {name: 'Add policy'}).click();
await page.waitForLoadState('networkidle');
const nameInput = page.locator('#admin\\.access_control\\.policy\\.edit_policy\\.policyName');
await nameInput.waitFor({state: 'visible', timeout: 10000});
await nameInput.fill(policyName);
const addAttributeButton = page.getByRole('button', {name: /add attribute/i});
await addAttributeButton.waitFor({state: 'visible', timeout: 10000});
if (await addAttributeButton.isDisabled()) {
await page.reload();
await page.waitForLoadState('networkidle');
await nameInput.fill(policyName);
await expect(addAttributeButton).toBeEnabled({timeout: 15000});
}
await addAttributeButton.click();
// # Select the text attribute by name
if (!(await page.locator('[id^="attribute-selector-menu"]').isVisible({timeout: 2000}))) {
await page.locator('[data-testid="attributeSelectorMenuButton"]').first().click();
}
await page
.locator(`[id^="attribute-selector-menu"] li:has-text("${field!.name}")`)
.first()
.click({force: true});
// # Dismiss the attribute menu
await page.keyboard.press('Escape');
// * Verify the attribute menu is hidden
await expect(page.locator('[id^="attribute-selector-menu"]')).toBeHidden();
// * A text attribute defaults to the "is" operator
await expect(page.locator('[data-testid="operatorSelectorMenuButton"]').first()).toContainText('is');
// # Type a value containing an apostrophe
const valueInput = page.locator('.values-editor__simple-input, input[placeholder*="Add value" i]').first();
await valueInput.waitFor({state: 'visible', timeout: 10000});
await valueInput.fill(APOSTROPHE_VALUE);
await expect(valueInput).toHaveValue(APOSTROPHE_VALUE);
// # Save the policy
await page.getByRole('button', {name: 'Save'}).last().click();
await page.waitForLoadState('networkidle');
// # Reopen the policy from the list
await page.goto('/admin_console/system_attributes/membership_policies');
await page.waitForLoadState('networkidle');
const policyRow = page.locator('.policy-name').filter({hasText: policyName}).first();
await expect(policyRow).toBeVisible({timeout: 10000});
const rowId = await policyRow.getAttribute('id');
policyId = rowId?.replace('customDescription-', '') ?? null;
await policyRow.click();
await page.waitForLoadState('networkidle');
// # Switch to Advanced mode so the "Switch to Simple Mode" toggle is present.
const toAdvanced = page.getByRole('button', {name: 'Switch to Advanced Mode'});
if (await toAdvanced.isVisible({timeout: 5000}).catch(() => false)) {
await expect(toAdvanced).toBeEnabled({timeout: 60_000});
await toAdvanced.click();
}
// * The stored apostrophe expression classifies as simple, so the
// toggle back to Simple mode stays enabled (before the fix it was
// disabled, trapping the admin in Advanced mode).
const toSimpleMode = page.getByRole('button', {name: 'Switch to Simple Mode'});
await expect(toSimpleMode).toBeVisible({timeout: 10_000});
await expect(toSimpleMode).toBeEnabled();
// # Return to Simple mode and confirm the value round-tripped intact
await toSimpleMode.click();
const reopenedValue = page
.locator('.values-editor__simple-input, input[placeholder*="Add value" i]')
.first();
await expect(reopenedValue).toHaveValue(APOSTROPHE_VALUE, {timeout: 10_000});
} finally {
// Clean up the policy. If we didn't capture the ID from the DOM (e.g. because
// save succeeded but navigation/assertion failed), search for it by name.
let idToDelete = policyId;
if (!idToDelete) {
try {
const searchResult = await adminClient.searchAccessControlPolicies(policyName, 'parent', '', 10);
const foundPolicy = searchResult.policies?.find((p) => p.name === policyName);
if (foundPolicy) {
idToDelete = foundPolicy.id;
}
} catch {
// Search failed; skip cleanup
}
}
if (idToDelete) {
await adminClient.deleteAccessControlPolicy(idToDelete).catch(() => {});
}
}
});
});

View File

@@ -0,0 +1,406 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {
expect,
test,
enableABAC,
navigateToABACPage,
runSyncJob,
verifyUserInChannel,
} from '@mattermost/playwright-lib';
import {setupCustomProfileAttributeFields} from '../../../channels/custom_profile_attributes/helpers';
import {
ensureUserAttributes,
createUserForABAC,
createPrivateChannelForABAC,
createBasicPolicy,
waitForLatestSyncJob,
} from '../support';
/**
* ABAC Policies - Channel Integration
* Tests for managing ABAC policies through Channel Configuration
*/
test.describe('ABAC Policies - Channel Integration', () => {
/**
* MM-T5788: Add attribute-based policy to a channel from Channel Configuration page
* @objective Verify that a policy can be added to a channel from the Channel Configuration page
*
* Steps:
* 1. As admin go to System Console > User Management > Channels
*/
test('MM-T5788 Add policy to channel from Channel Configuration page', async ({pw}) => {
test.setTimeout(120000);
await pw.skipIfNoLicense();
// ============================================================
// SETUP: Create users, channel, and policy
// ============================================================
const {adminUser, adminClient, team} = await pw.initSetup();
await ensureUserAttributes(adminClient);
const attributeFieldsMap = await setupCustomProfileAttributeFields(adminClient, [
{name: 'Department', type: 'text'},
]);
// Create satisfying user (Department=Engineering) - NOT in channel initially
const satisfyingUser = await createUserForABAC(adminClient, attributeFieldsMap, [
{name: 'Department', value: 'Engineering', type: 'text'},
]);
await adminClient.addToTeam(team.id, satisfyingUser.id);
// Create non-satisfying user (Department=Sales) - will be IN channel initially
const nonSatisfyingUser = await createUserForABAC(adminClient, attributeFieldsMap, [
{name: 'Department', value: 'Sales', type: 'text'},
]);
await adminClient.addToTeam(team.id, nonSatisfyingUser.id);
// Create private channel and add non-satisfying user
const channel = await createPrivateChannelForABAC(adminClient, team.id);
await adminClient.addToChannel(nonSatisfyingUser.id, channel.id);
// Login and setup ABAC
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
await navigateToABACPage(systemConsolePage.page);
await enableABAC(systemConsolePage.page);
// Create policy without channels (we'll link via Channel Config)
const policyName = `Channel Config Policy ${pw.random.id()}`;
await createBasicPolicy(systemConsolePage.page, {
name: policyName,
attribute: 'Department',
operator: '==',
value: 'Engineering',
});
// Get search term for policy
const policyIdMatch = policyName.match(/([a-z0-9]+)$/i);
const searchTerm = policyIdMatch ? policyIdMatch[1] : policyName;
// ============================================================
// STEP 1-2: Navigate to Channel Configuration
// ============================================================
await systemConsolePage.page.goto('/admin_console/user_management/channels');
await systemConsolePage.page.waitForLoadState('networkidle');
// Search and find our channel
const channelSearchInput = systemConsolePage.page.locator('input[placeholder*="Search" i]').first();
await channelSearchInput.fill(channel.display_name);
await systemConsolePage.page.waitForTimeout(1000);
// Verify channel shows "Manual Invites" management
const channelRow = systemConsolePage.page
.locator('.DataGrid_row')
.filter({hasText: channel.display_name})
.first();
// const managementText = await channelRow.textContent();
// Click Edit
await channelRow.getByText('Edit').click();
await systemConsolePage.page.waitForLoadState('networkidle');
// ============================================================
// STEP 3: Toggle on "Enable attribute based channel access"
// ============================================================
const abacToggle = systemConsolePage.page.locator('[data-testid="policy-enforce-toggle-button"]');
await abacToggle.waitFor({state: 'visible', timeout: 5000});
const isEnabled = await abacToggle.getAttribute('aria-pressed');
if (isEnabled !== 'true') {
await abacToggle.click();
}
await systemConsolePage.page.waitForTimeout(500);
// ============================================================
// STEP 4: Link to policy and Save
// ============================================================
// Click "Link to a policy"
const linkButton = systemConsolePage.page.locator('[data-testid="link-to-a-policy"]');
await linkButton.waitFor({state: 'visible', timeout: 5000});
await linkButton.click();
await systemConsolePage.page.waitForTimeout(500);
// Select policy in modal
const modal = systemConsolePage.page.locator('[role="dialog"]').filter({hasText: 'Select a Membership Policy'});
await modal.waitFor({state: 'visible', timeout: 5000});
const modalSearch = modal.locator('[data-testid="searchInput"]');
await modalSearch.fill(searchTerm);
await systemConsolePage.page.waitForTimeout(1000);
const policyOption = modal.locator('.DataGrid_row').filter({hasText: policyName}).first();
await policyOption.click();
await systemConsolePage.page.waitForTimeout(500);
// Save
await systemConsolePage.page.getByRole('button', {name: 'Save'}).click();
await systemConsolePage.page.waitForLoadState('networkidle');
// ============================================================
// Run sync to apply policy
// ============================================================
await systemConsolePage.page.waitForTimeout(2000);
await navigateToABACPage(systemConsolePage.page);
const jobId1 = await runSyncJob(systemConsolePage.page);
await waitForLatestSyncJob(systemConsolePage.page, 5, jobId1);
// ============================================================
// VERIFY: Channel membership
// ============================================================
// 1. Non-satisfying user should be REMOVED
const nonSatisfyingInChannel = await verifyUserInChannel(adminClient, nonSatisfyingUser.id, channel.id);
expect(nonSatisfyingInChannel).toBe(false);
// 2. Satisfying user should NOT be auto-added (per requirement)
const satisfyingInChannel = await verifyUserInChannel(adminClient, satisfyingUser.id, channel.id);
// Note: If implementation auto-adds, this will fail. Adjust if needed.
expect(satisfyingInChannel).toBe(false);
// 3. Satisfying user CAN be manually added
await adminClient.addToChannel(satisfyingUser.id, channel.id);
const afterManualAdd = await verifyUserInChannel(adminClient, satisfyingUser.id, channel.id);
expect(afterManualAdd).toBe(true);
// 4. Non-satisfying user CANNOT be added (blocked by policy)
let blocked = false;
try {
await adminClient.addToChannel(nonSatisfyingUser.id, channel.id);
} catch {
// Expected to fail - policy blocks non-qualifying users
blocked = true;
}
expect(blocked).toBe(true);
});
/**
* MM-T5789: Channel cannot use attribute-based policies if already constrained by LDAP group sync
*
* Preconditions:
* - At least one policy exists on the server
* - At least one channel configured to be constrained by LDAP group sync
*
* Step 1:
* 1. As admin go to System Console > User Management > Channels
* 2. Click a channel with Management = "Group Sync" (private channel)
* 3. Observe "Enable attribute based channel access" is NOT available
* 4. Toggle off "Sync Group Members", observe ABAC becomes available
*
* Step 2:
* 1. Go to System Console > User Management > Attribute-Based Access
* 2. Click a policy to edit it, click Add channels
* 3. Select a channel constrained by LDAP group sync
*
* Expected: ABAC not available for channels using LDAP group sync
*
* Test Data Note: Current UI behavior is uncertain - may allow save but channel
* not added, or may show error. This test observes and documents actual behavior.
*
* Implementation Note: We mock Group Sync by setting group_constrained=true via API.
* This works without LDAP - the server accepts it and UI shows "Group Sync".
*/
test('MM-T5789 Channel with LDAP group sync cannot use ABAC', async ({pw}) => {
test.setTimeout(120000);
await pw.skipIfNoLicense();
const {adminUser, adminClient, team} = await pw.initSetup();
await ensureUserAttributes(adminClient);
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
const page = systemConsolePage.page;
// ===========================================
// PRECONDITION 1: Create a policy first
// ===========================================
await navigateToABACPage(page);
await enableABAC(page);
const policyName = `ABAC-GroupSync-Test-${pw.random.id()}`;
await createBasicPolicy(page, {
name: policyName,
attribute: 'Department',
operator: '==',
value: 'Engineering',
autoSync: false,
});
// ===========================================
// PRECONDITION 2: Create a Group Sync channel via API
// We mock Group Sync by setting group_constrained=true
// This works without LDAP configuration
// ===========================================
const groupSyncChannelName = `ABAC-GroupSync-${pw.random.id()}`;
const groupSyncChannel = await adminClient.createChannel({
team_id: team.id,
name: groupSyncChannelName.toLowerCase().replace(/[^a-z0-9]/g, ''),
display_name: groupSyncChannelName,
type: 'P', // Private
});
// Set group_constrained=true via API to mock Group Sync
await adminClient.patchChannel(groupSyncChannel.id, {
group_constrained: true,
} as any);
// ===========================================
// STEP 1: Navigate to Group Sync channel config
// ===========================================
await page.goto('/admin_console/user_management/channels');
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
// Search for our channel
const searchInput = page.locator('input[placeholder*="Search" i]').first();
if (await searchInput.isVisible({timeout: 3000})) {
await searchInput.fill(groupSyncChannelName);
await page.waitForTimeout(1000);
}
// Verify channel shows as "Group Sync"
const channelRow = page.locator('.DataGrid_row').filter({hasText: groupSyncChannelName}).first();
await channelRow.waitFor({state: 'visible', timeout: 10000});
const rowText = await channelRow.textContent();
expect(rowText).toContain('Group Sync');
// Click Edit to open channel configuration
await channelRow.getByText('Edit').click();
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
// STEP 1.3: Verify ABAC toggle is NOT available when Group Sync is enabled
const abacToggle = page.locator('[data-testid="policy-enforce-toggle-button"]');
const abacVisibleWithGroupSync = await abacToggle.isVisible({timeout: 5000}).catch(() => false);
expect(abacVisibleWithGroupSync).toBe(false);
// STEP 1.4: Toggle off Group Sync and verify ABAC becomes available
// Disable Group Sync via API (more reliable than UI toggle)
await adminClient.patchChannel(groupSyncChannel.id, {
group_constrained: false,
} as any);
// Reload page to see updated UI
await page.reload();
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
// Verify ABAC toggle is now available
const abacToggleAfter = page.locator('[data-testid="policy-enforce-toggle-button"]');
const abacVisibleAfterDisable = await abacToggleAfter.isVisible({timeout: 5000}).catch(() => false);
expect(abacVisibleAfterDisable).toBe(true);
// Re-enable Group Sync for Step 2
await adminClient.patchChannel(groupSyncChannel.id, {
group_constrained: true,
} as any);
// ===========================================
// STEP 2: Try to add Group Sync channel to existing policy
// ===========================================
await navigateToABACPage(page);
await page.waitForTimeout(1000);
// Step 2.2: Click the policy to edit it
// Search for the policy first
const policySearchInput = page.locator('input[placeholder*="Search" i]').first();
if (await policySearchInput.isVisible({timeout: 3000})) {
await policySearchInput.fill(policyName);
await page.waitForTimeout(1000);
}
// Click on the policy row (use text-based locator)
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);
// Click Add channels button
const addChannelsButton = page.getByRole('button', {name: /add channel/i});
await addChannelsButton.waitFor({state: 'visible', timeout: 10000});
await addChannelsButton.click();
await page.waitForTimeout(1000);
// Step 2.3: Try to select the Group Sync channel
const channelModal = page.locator('[role="dialog"]').filter({hasText: /channel/i});
await channelModal.waitFor({state: 'visible', timeout: 5000});
// Search for the Group Sync channel
const modalSearchInput = channelModal.locator('[data-testid="searchInput"], input[type="text"]').first();
if (await modalSearchInput.isVisible({timeout: 3000})) {
await modalSearchInput.fill(groupSyncChannelName);
await page.waitForTimeout(1000);
}
// Document actual behavior (requirement notes uncertainty)
const channelRows = channelModal.locator('.DataGrid_row, .more-modal__row');
const rowCount = await channelRows.count();
if (rowCount === 0) {
// Group Sync channel is filtered out - good behavior
} else {
// Channel is shown - try to select it
const channelRowToSelect = channelRows.first();
await channelRowToSelect.textContent();
// Try to click/select the channel
await channelRowToSelect.click({timeout: 5000}).catch(() => {
// Ignore click errors
});
await page.waitForTimeout(500);
// Try to click Add button
const addButton = channelModal.getByRole('button', {name: 'Add'});
if (await addButton.isVisible({timeout: 3000})) {
const addButtonDisabled = await addButton.isDisabled();
if (addButtonDisabled) {
// Add button is disabled
} else {
await addButton.click();
await page.waitForTimeout(1000);
}
}
// Close modal
const closeButton = channelModal.getByRole('button', {name: /close|cancel|×/i});
if (await closeButton.isVisible({timeout: 2000})) {
await closeButton.click();
await page.waitForTimeout(500);
}
// Check if we're back on edit page - try to save
const saveButton = page.getByRole('button', {name: 'Save'});
if (await saveButton.isVisible({timeout: 3000})) {
const saveEnabled = await saveButton.isEnabled();
if (saveEnabled) {
await saveButton.click();
await page.waitForTimeout(2000);
// Check for error message
const errorMessage = page.locator('.error-message, [class*="error"], .alert-danger');
const hasError = await errorMessage.isVisible({timeout: 3000}).catch(() => false);
if (hasError) {
await errorMessage.textContent();
} else {
// Check if channel was actually added
}
}
}
}
});
});

View File

@@ -0,0 +1,425 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {
expect,
test,
enableABAC,
navigateToABACPage,
runSyncJob,
verifyUserInChannel,
} 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,
activatePolicy,
waitForLatestSyncJob,
waitForPolicySyncJob,
enableUserManagedAttributes,
} from '../support';
/**
* ABAC Policies - Create Policies
* Tests for creating ABAC policies with different auto-add settings
*/
test.describe('ABAC Policies - Create Policies', () => {
test('MM-T5783 Create and test policy with auto-add disabled', async ({pw}) => {
// # Skip test if no license for ABAC
await pw.skipIfNoLicense();
// ============================================================
// SETUP: Create users and channel BEFORE creating policy
// ============================================================
const {adminUser, adminClient, team} = await pw.initSetup();
// Enable user-managed attributes
await enableUserManagedAttributes(adminClient);
// Define and create the Department attribute field
const departmentAttribute: CustomProfileAttribute[] = [{name: 'Department', type: 'text', value: ''}];
const attributeFieldsMap = await setupCustomProfileAttributeFields(adminClient, departmentAttribute);
// Create 3 users as per test case:
// 1. satisfyingUserNotInChannel - Department=Engineering, NOT in channel initially
// 2. satisfyingUserInChannel - Department=Engineering, IN channel initially
// 3. nonSatisfyingUserInChannel - Department=Sales, IN channel initially
const satisfyingUserNotInChannel = await createUserForABAC(adminClient, attributeFieldsMap, [
{name: 'Department', type: 'text', value: 'Engineering'},
]);
const satisfyingUserInChannel = await createUserForABAC(adminClient, attributeFieldsMap, [
{name: 'Department', type: 'text', value: 'Engineering'},
]);
const nonSatisfyingUserInChannel = await createUserForABAC(adminClient, attributeFieldsMap, [
{name: 'Department', type: 'text', value: 'Sales'},
]);
// Add all users to the team
await adminClient.addToTeam(team.id, satisfyingUserNotInChannel.id);
await adminClient.addToTeam(team.id, satisfyingUserInChannel.id);
await adminClient.addToTeam(team.id, nonSatisfyingUserInChannel.id);
// Create private channel and add users 2 and 3 (but NOT user 1)
const privateChannel = await createPrivateChannelForABAC(adminClient, team.id);
await adminClient.addToChannel(satisfyingUserInChannel.id, privateChannel.id);
await adminClient.addToChannel(nonSatisfyingUserInChannel.id, privateChannel.id);
// Verify initial channel state
const initialUser1InChannel = await verifyUserInChannel(
adminClient,
satisfyingUserNotInChannel.id,
privateChannel.id,
);
const initialUser2InChannel = await verifyUserInChannel(
adminClient,
satisfyingUserInChannel.id,
privateChannel.id,
);
const initialUser3InChannel = await verifyUserInChannel(
adminClient,
nonSatisfyingUserInChannel.id,
privateChannel.id,
);
expect(initialUser1InChannel).toBe(false);
expect(initialUser2InChannel).toBe(true);
expect(initialUser3InChannel).toBe(true);
// ============================================================
// STEP 1-4: Login, navigate to ABAC, create policy with rule and channel
// ============================================================
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
await navigateToABACPage(systemConsolePage.page);
await enableABAC(systemConsolePage.page);
// Re-apply guard: concurrent initSetup() resets ABAC between enableABAC() UI call and policy creation
await adminClient.patchConfig({
AccessControlSettings: {EnableAttributeBasedAccessControl: true},
});
// Use the working createBasicPolicy helper (same as MM-T5784)
const policyName = `Engineering Policy ${pw.random.id()}`;
const jobIdMM5783 = await createBasicPolicy(systemConsolePage.page, {
name: policyName,
attribute: 'Department',
operator: '==',
value: 'Engineering',
autoSync: false, // Auto-add DISABLED for this test
channels: [privateChannel.display_name],
});
// ============================================================
// STEP 3: Test Access Rule (navigate back to policy to test)
// ============================================================
// Navigate back to policy to test the access rule
await systemConsolePage.page.waitForTimeout(1000);
const policyRowForTest = systemConsolePage.page.locator('.policy-name').filter({hasText: policyName}).first();
if (await policyRowForTest.isVisible({timeout: 3000})) {
await policyRowForTest.click();
await systemConsolePage.page.waitForLoadState('networkidle');
const testResult = await testAccessRule(systemConsolePage.page, {
expectedMatchingUsers: [satisfyingUserNotInChannel.username, satisfyingUserInChannel.username],
expectedNonMatchingUsers: [nonSatisfyingUserInChannel.username],
});
expect(testResult.expectedUsersMatch).toBe(true);
expect(testResult.unexpectedUsersMatch).toBe(false);
// Navigate back to ABAC page
await navigateToABACPage(systemConsolePage.page);
}
// Wait for sync job to complete (triggered by createBasicPolicy)
await waitForLatestSyncJob(systemConsolePage.page, undefined, jobIdMM5783);
// ============================================================
// STEP 5-7: Verify channel membership after sync
// ============================================================
// Step 5: User who satisfies policy but NOT in channel → should NOT be auto-added
const user1AfterSync = await verifyUserInChannel(adminClient, satisfyingUserNotInChannel.id, privateChannel.id);
expect(user1AfterSync).toBe(false); // NOT auto-added because auto-add is FALSE
// Step 6: User who satisfies policy and IS in channel → no change (stays in channel)
const user2AfterSync = await verifyUserInChannel(adminClient, satisfyingUserInChannel.id, privateChannel.id);
expect(user2AfterSync).toBe(true); // Stays in channel
// Step 7: User who does NOT satisfy policy and IS in channel → auto-removed
const user3AfterSync = await verifyUserInChannel(adminClient, nonSatisfyingUserInChannel.id, privateChannel.id);
expect(user3AfterSync).toBe(false); // AUTO-REMOVED
// ============================================================
// STEP 8: Admin can manually add the satisfying user to channel
// Validate: satisfying user CAN be added, non-satisfying user CANNOT
// ============================================================
// 8a. Add user who SATISFIES the policy - should succeed
await adminClient.addToChannel(satisfyingUserNotInChannel.id, privateChannel.id);
const user1AfterManualAdd = await verifyUserInChannel(
adminClient,
satisfyingUserNotInChannel.id,
privateChannel.id,
);
expect(user1AfterManualAdd).toBe(true); // Successfully added by admin
// 8b. Try to add user who does NOT satisfy the policy - should FAIL
try {
await adminClient.addToChannel(nonSatisfyingUserInChannel.id, privateChannel.id);
} catch {
// Expected to fail - policy prevents non-compliant users
}
// Verify the non-satisfying user is NOT in the channel
const user3AfterAttempt = await verifyUserInChannel(
adminClient,
nonSatisfyingUserInChannel.id,
privateChannel.id,
);
expect(user3AfterAttempt).toBe(false); // Policy prevents non-compliant users
});
/**
* MM-T5784: Attribute-based access policy created in System Console controls access as specified
* (one attribute, = is, with auto-add)
*
* @reference https://github.com/mattermost/mattermost-test-management/blob/main/data/test-cases/channels/abac-attribute-based-access/abac-system-admin/MM-T5784.md
*
* Test Steps:
* 1. As system admin, go to ABAC page, click Add policy, enter name, set Auto-add = TRUE
* 2. Select policy values: Attribute, Operator, and Value (just one)
* 3. Click Test Access Rule, observe users who satisfy the policy are listed
* 4. Click Add channels and select a channel, then save
* 5. User who satisfies policy but NOT in channel → should be AUTO-ADDED
* 6. User who satisfies policy and IS in channel → no change (stays in channel)
* 7. User who does NOT satisfy policy and IS in channel → auto-removed
*
* Expected:
* - User who satisfies the policy is auto-added
* - User who does not satisfy the policy is auto-removed
*/
test('MM-T5784 Create and test policy with auto-add enabled', async ({pw}) => {
// Increase timeout for this complex test to prevent trace file race conditions
test.setTimeout(120000); // 2 minutes instead of default 1 minute
// # Skip test if no license for ABAC
await pw.skipIfNoLicense();
// ============================================================
// SETUP: Create users and channel BEFORE creating policy
// ============================================================
const {adminUser, adminClient, team} = await pw.initSetup();
// Enable user-managed attributes
await enableUserManagedAttributes(adminClient);
// Define and create the Department attribute field
const departmentAttribute: CustomProfileAttribute[] = [{name: 'Department', type: 'text', value: ''}];
const attributeFieldsMap = await setupCustomProfileAttributeFields(adminClient, departmentAttribute);
// Create 3 users as per test case:
// 1. satisfyingUserNotInChannel - Department=Engineering, NOT in channel initially
// 2. satisfyingUserInChannel - Department=Engineering, IN channel initially
// 3. nonSatisfyingUserInChannel - Department=Sales, IN channel initially
const satisfyingUserNotInChannel = await createUserForABAC(adminClient, attributeFieldsMap, [
{name: 'Department', type: 'text', value: 'Engineering'},
]);
const satisfyingUserInChannel = await createUserForABAC(adminClient, attributeFieldsMap, [
{name: 'Department', type: 'text', value: 'Engineering'},
]);
const nonSatisfyingUserInChannel = await createUserForABAC(adminClient, attributeFieldsMap, [
{name: 'Department', type: 'text', value: 'Sales'},
]);
// Add all users to the team
await adminClient.addToTeam(team.id, satisfyingUserNotInChannel.id);
await adminClient.addToTeam(team.id, satisfyingUserInChannel.id);
await adminClient.addToTeam(team.id, nonSatisfyingUserInChannel.id);
// Wait for user attributes to be indexed before creating policy
await new Promise((resolve) => setTimeout(resolve, 2000));
// Create private channel and add users 2 and 3 (but NOT user 1)
const privateChannel = await createPrivateChannelForABAC(adminClient, team.id);
await adminClient.addToChannel(satisfyingUserInChannel.id, privateChannel.id);
await adminClient.addToChannel(nonSatisfyingUserInChannel.id, privateChannel.id);
// Verify initial channel state
const initialUser1InChannel = await verifyUserInChannel(
adminClient,
satisfyingUserNotInChannel.id,
privateChannel.id,
);
const initialUser2InChannel = await verifyUserInChannel(
adminClient,
satisfyingUserInChannel.id,
privateChannel.id,
);
const initialUser3InChannel = await verifyUserInChannel(
adminClient,
nonSatisfyingUserInChannel.id,
privateChannel.id,
);
expect(initialUser1InChannel).toBe(false);
expect(initialUser2InChannel).toBe(true);
expect(initialUser3InChannel).toBe(true);
// ============================================================
// STEP 1-4: Login, navigate to ABAC, create policy with auto-add TRUE
// ============================================================
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
await navigateToABACPage(systemConsolePage.page);
await enableABAC(systemConsolePage.page);
// Use createBasicPolicy with autoSync: true
const policyName = `Auto-Add Policy ${pw.random.id()}`;
const jobIdMM5784 = await createBasicPolicy(systemConsolePage.page, {
name: policyName,
attribute: 'Department',
operator: '==',
value: 'Engineering',
autoSync: true, // Auto-add ENABLED for this test
channels: [privateChannel.display_name],
});
// ============================================================
// STEP 3: Test Access Rule (navigate back to policy to test)
// ============================================================
await systemConsolePage.page.waitForTimeout(1000);
const policyRowForTest = systemConsolePage.page.locator('.policy-name').filter({hasText: policyName}).first();
if (await policyRowForTest.isVisible({timeout: 3000})) {
await policyRowForTest.click();
await systemConsolePage.page.waitForLoadState('networkidle');
const testResult = await testAccessRule(systemConsolePage.page, {
expectedMatchingUsers: [satisfyingUserNotInChannel.username, satisfyingUserInChannel.username],
expectedNonMatchingUsers: [nonSatisfyingUserInChannel.username],
});
expect(testResult.expectedUsersMatch).toBe(true);
expect(testResult.unexpectedUsersMatch).toBe(false);
await navigateToABACPage(systemConsolePage.page);
}
// Wait for initial sync job to complete
await waitForLatestSyncJob(systemConsolePage.page, undefined, jobIdMM5784);
// Get policy ID and activate it for auto-add to work
const searchInput = systemConsolePage.page.locator('input[placeholder*="Search" i]').first();
await searchInput.waitFor({state: 'visible', timeout: 5000});
const idMatch = policyName.match(/([a-z0-9]+)$/i);
const uniqueId = idMatch ? idMatch[1] : policyName;
await searchInput.fill(uniqueId);
await systemConsolePage.page.waitForTimeout(1000);
const policyRow = systemConsolePage.page.locator('.policy-name').first();
const policyElementId = await policyRow.getAttribute('id');
const policyId = policyElementId?.replace('customDescription-', '');
if (!policyId) {
throw new Error('Could not get policy ID');
}
await searchInput.clear();
// Activate the policy so auto-add works
await activatePolicy(adminClient, policyId);
// Run sync job with active policy; poll by policyId to avoid picking up a
// concurrent shard's job completing first
await runSyncJob(systemConsolePage.page);
await waitForPolicySyncJob(adminClient, policyId);
// ============================================================
// STEP 5-7: Verify via API
// ============================================================
// Step 5: User who satisfies policy but NOT in channel → should be AUTO-ADDED
const user1AfterSync = await verifyUserInChannel(adminClient, satisfyingUserNotInChannel.id, privateChannel.id);
expect(user1AfterSync).toBe(true); // AUTO-ADDED because auto-add is TRUE
// Step 6: User who satisfies policy and IS in channel → no change (stays in channel)
const user2AfterSync = await verifyUserInChannel(adminClient, satisfyingUserInChannel.id, privateChannel.id);
expect(user2AfterSync).toBe(true); // Stays in channel
// Step 7: User who does NOT satisfy policy and IS in channel → auto-removed
const user3AfterSync = await verifyUserInChannel(adminClient, nonSatisfyingUserInChannel.id, privateChannel.id);
expect(user3AfterSync).toBe(false); // AUTO-REMOVED
});
/**
* MM-63848: Creating a policy with a name that already exists should show an error
*/
test('MM-63848 Should show error when creating policy with duplicate 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);
// Re-apply guard: concurrent initSetup() resets ABAC between enableABAC() UI call and policy creation
await adminClient.patchConfig({
AccessControlSettings: {EnableAttributeBasedAccessControl: true},
});
// Create the first policy
const policyName = `Duplicate Test ${pw.random.id()}`;
await createBasicPolicy(page, {
name: policyName,
attribute: 'Department',
operator: '==',
value: 'Engineering',
autoSync: false,
channels: [privateChannel.display_name],
});
// Navigate back and try to create another policy with the same name.
// Re-apply guards: a concurrent initSetup() may have reset EnableAttributeBasedAccessControl
// AND deleted custom profile attributes between the first createBasicPolicy call and now.
// Without the Department attribute the attributeSelectorMenuButton has no items and times out.
await navigateToABACPage(page);
await setupCustomProfileAttributeFields(adminClient, departmentAttribute);
await adminClient.patchConfig({
AccessControlSettings: {EnableAttributeBasedAccessControl: true},
});
await createBasicPolicy(page, {
name: policyName,
attribute: 'Department',
operator: '==',
value: 'Sales',
autoSync: false,
channels: [],
});
// 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,199 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, test, enableABAC} from '@mattermost/playwright-lib';
import {ensureUserAttributes, navigateToPermissionPoliciesPage} from '../support';
/**
* Permission Policies - Create Policy form UI and validation (MM-64508)
*
* Covers the create-form UI elements (name input, role dropdown, CEL editor mode
* toggle, info banner) and inline validation (empty name / expression / permissions).
* See permission_policies_create_save.spec.ts for the save/cancel flows that
* actually persist policies.
*/
test.describe('Permission Policies - Create Policy', () => {
test('MM-T5804 admin can open the create permission policy form', async ({pw}) => {
await pw.skipIfNoLicense();
const {adminUser, adminClient} = await pw.initSetup();
await ensureUserAttributes(adminClient);
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
await enableABAC(systemConsolePage.page);
await navigateToPermissionPoliciesPage(systemConsolePage.page);
await systemConsolePage.page.getByRole('button', {name: 'Add policy'}).click();
await systemConsolePage.page.waitForLoadState('networkidle');
// * Detail page heading and name input visible
await expect(
systemConsolePage.page.getByText('Attribute Based Permission Policy', {exact: true}),
).toBeVisible();
await expect(systemConsolePage.page.getByPlaceholder('Add a unique policy name')).toBeVisible();
});
test('MM-T5805 create policy form shows evaluation order info banner', async ({pw}) => {
await pw.skipIfNoLicense();
const {adminUser, adminClient} = await pw.initSetup();
await ensureUserAttributes(adminClient);
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
await enableABAC(systemConsolePage.page);
await navigateToPermissionPoliciesPage(systemConsolePage.page);
await systemConsolePage.page.getByRole('button', {name: 'Add policy'}).click();
await systemConsolePage.page.waitForLoadState('networkidle');
// * Banner explains that permission policies override system permission schemes
await expect(
systemConsolePage.page.getByText('The permissions defined in this policy override the', {exact: false}),
).toBeVisible();
await expect(systemConsolePage.page.getByText('system permission schemes', {exact: false})).toBeVisible();
await expect(systemConsolePage.page.getByText('Permissions evaluation order', {exact: false})).toBeVisible();
});
test('MM-T5806 create policy form shows role dropdown defaulting to Members', async ({pw}) => {
await pw.skipIfNoLicense();
const {adminUser, adminClient} = await pw.initSetup();
await ensureUserAttributes(adminClient);
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
await enableABAC(systemConsolePage.page);
await navigateToPermissionPoliciesPage(systemConsolePage.page);
await systemConsolePage.page.getByRole('button', {name: 'Add policy'}).click();
await systemConsolePage.page.waitForLoadState('networkidle');
await expect(systemConsolePage.page.getByText('Who this policy applies to')).toBeVisible();
await expect(
systemConsolePage.page.getByText('Select a role from the predefined list of system roles'),
).toBeVisible();
// * The dropdown button is visible and shows the default role
// (system_user). The label was shortened from "Members and
// system administrators" to just "Members" in the UX pass;
// the "system admins fall back when no admin-specific rule
// exists" semantics moved into the role's description copy.
// Use an exact-text matcher so a regression to the longer
// "Members and system administrators" label fails the test
// instead of silently passing the substring check.
const roleButton = systemConsolePage.page.locator('#pp-role-selector-btn');
await expect(roleButton).toBeVisible();
await expect(roleButton).toHaveText('Members');
});
test('MM-T5807 admin can change role selection to System administrators via dropdown', async ({pw}) => {
await pw.skipIfNoLicense();
const {adminUser, adminClient} = await pw.initSetup();
await ensureUserAttributes(adminClient);
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
await enableABAC(systemConsolePage.page);
await navigateToPermissionPoliciesPage(systemConsolePage.page);
await systemConsolePage.page.getByRole('button', {name: 'Add policy'}).click();
await systemConsolePage.page.waitForLoadState('networkidle');
// # Open the role dropdown and select System administrators
await systemConsolePage.page.locator('#pp-role-selector-btn').click();
await systemConsolePage.page.locator('#pp-role-option-system_admin').click();
// * Dropdown button now shows the selected role
await expect(systemConsolePage.page.locator('#pp-role-selector-btn')).toContainText('System administrators');
});
test('MM-T5808 admin can toggle between Simple and Advanced CEL editor modes', async ({pw}) => {
await pw.skipIfNoLicense();
const {adminUser, adminClient} = await pw.initSetup();
await ensureUserAttributes(adminClient);
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
await enableABAC(systemConsolePage.page);
await navigateToPermissionPoliciesPage(systemConsolePage.page);
await systemConsolePage.page.getByRole('button', {name: 'Add policy'}).click();
await systemConsolePage.page.waitForLoadState('networkidle');
const switchToAdvanced = systemConsolePage.page.getByRole('button', {name: 'Switch to Advanced Mode'});
await expect(switchToAdvanced).toBeVisible();
await switchToAdvanced.click();
// * Button label flips to Simple Mode
const switchToSimple = systemConsolePage.page.getByRole('button', {name: 'Switch to Simple Mode'});
await expect(switchToSimple).toBeVisible();
await switchToSimple.click();
await expect(switchToAdvanced).toBeVisible();
});
test('MM-T5809 Save is blocked when policy name is empty', async ({pw}) => {
await pw.skipIfNoLicense();
const {adminUser, adminClient} = await pw.initSetup();
await ensureUserAttributes(adminClient);
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
await enableABAC(systemConsolePage.page);
await navigateToPermissionPoliciesPage(systemConsolePage.page);
await systemConsolePage.page.getByRole('button', {name: 'Add policy'}).click();
await systemConsolePage.page.waitForLoadState('networkidle');
// # Type then clear the name to mark the form dirty, enabling the Save button
const nameInput = systemConsolePage.page.getByPlaceholder('Add a unique policy name');
await nameInput.fill('x');
await nameInput.clear();
await systemConsolePage.page.getByRole('button', {name: 'Save'}).last().click();
await expect(systemConsolePage.page.getByText('Please add a name to the policy')).toBeVisible();
});
test('MM-T5810 Save is blocked when CEL expression is empty', async ({pw}) => {
await pw.skipIfNoLicense();
const {adminUser, adminClient} = await pw.initSetup();
await ensureUserAttributes(adminClient);
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
await enableABAC(systemConsolePage.page);
await navigateToPermissionPoliciesPage(systemConsolePage.page);
await systemConsolePage.page.getByRole('button', {name: 'Add policy'}).click();
await systemConsolePage.page.waitForLoadState('networkidle');
await systemConsolePage.page
.getByPlaceholder('Add a unique policy name')
.fill(`PP Expr Validate ${pw.random.id()}`);
await systemConsolePage.page.getByRole('button', {name: 'Save'}).last().click();
await expect(systemConsolePage.page.getByText('Please add an expression to the policy')).toBeVisible();
});
test('MM-T5811 Save is blocked when no permission is selected', async ({pw}) => {
test.setTimeout(120000);
await pw.skipIfNoLicense();
const {adminUser, adminClient} = await pw.initSetup();
await ensureUserAttributes(adminClient);
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
await enableABAC(systemConsolePage.page);
await navigateToPermissionPoliciesPage(systemConsolePage.page);
await systemConsolePage.page.getByRole('button', {name: 'Add policy'}).click();
await systemConsolePage.page.waitForLoadState('networkidle');
await systemConsolePage.page
.getByPlaceholder('Add a unique policy name')
.fill(`PP Perm Validate ${pw.random.id()}`);
// # Enter a valid CEL expression but add no permissions
await systemConsolePage.page.getByRole('button', {name: 'Switch to Advanced Mode'}).click();
const monacoContainer = systemConsolePage.page.locator('.monaco-editor').first();
await monacoContainer.waitFor({state: 'visible', timeout: 5000});
const editorLines = systemConsolePage.page.locator('.monaco-editor .view-lines').first();
await editorLines.click({force: true});
await systemConsolePage.page.waitForTimeout(300);
const isMac = process.platform === 'darwin';
await systemConsolePage.page.keyboard.press(isMac ? 'Meta+a' : 'Control+a');
await systemConsolePage.page.waitForTimeout(100);
await systemConsolePage.page.keyboard.type('user.attributes.Department == "Engineering"', {delay: 10});
await systemConsolePage.page.getByRole('button', {name: 'Save'}).last().click();
await expect(systemConsolePage.page.getByText('Please select at least one permission')).toBeVisible();
});
});

View File

@@ -0,0 +1,135 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, test, enableABAC} from '@mattermost/playwright-lib';
import {
ensureUserAttributes,
createPermissionPolicy,
deletePermissionPolicyByName,
navigateToPermissionPoliciesPage,
} from '../support';
/**
* Permission Policies - Create Policy save/cancel flows (MM-64508)
*
* Covers end-to-end creation flows that persist a policy (download-only,
* combined download+upload, guest role) plus the cancel path. Paired with
* permission_policies_create_form.spec.ts which covers form UI + validation.
*/
test.describe('Permission Policies - Create Policy', () => {
test('MM-T5812 admin can create a permission policy restricting file downloads', async ({pw}) => {
test.setTimeout(120000);
await pw.skipIfNoLicense();
const {adminUser, adminClient} = await pw.initSetup();
await ensureUserAttributes(adminClient);
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
await enableABAC(systemConsolePage.page);
// Re-apply via API: a concurrent initSetup() on another shard may have
// disabled ABAC between the enableABAC UI call and the navigation to
// permission_policies, causing a redirect to the license page.
await adminClient.patchConfig({AccessControlSettings: {EnableAttributeBasedAccessControl: true}});
const policyName = `PP Download ${pw.random.id()}`;
try {
await createPermissionPolicy(systemConsolePage.page, {
name: policyName,
celExpression: 'user.attributes.Department == "Engineering"',
permissions: ['Download Files'],
});
// * List page shows the new policy with correct role and permissions
await expect(systemConsolePage.page.getByRole('heading', {name: 'Permission Policies'})).toBeVisible();
const policyRow = systemConsolePage.page.locator('.DataGrid_row').filter({hasText: policyName});
await expect(policyRow).toBeVisible();
// Role label was shortened from "Members and system administrators"
// to just "Members" in the UX pass; the "system admins fall back
// when no admin-specific rule exists" semantics moved into the
// role's description copy.
await expect(policyRow.getByText('Members', {exact: true})).toBeVisible();
await expect(policyRow.getByText('Download Files')).toBeVisible();
} finally {
await deletePermissionPolicyByName(adminClient, policyName);
}
});
test('MM-T5813 admin can create a permission policy with both Download and Upload permissions', async ({pw}) => {
test.setTimeout(120000);
await pw.skipIfNoLicense();
const {adminUser, adminClient} = await pw.initSetup();
await ensureUserAttributes(adminClient);
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
await enableABAC(systemConsolePage.page);
await adminClient.patchConfig({AccessControlSettings: {EnableAttributeBasedAccessControl: true}});
const policyName = `PP Both Perms ${pw.random.id()}`;
try {
await createPermissionPolicy(systemConsolePage.page, {
name: policyName,
celExpression: 'user.attributes.Department == "Legal"',
permissions: ['Download Files', 'Upload Files'],
});
const policyRow = systemConsolePage.page.locator('.DataGrid_row').filter({hasText: policyName});
await expect(policyRow.getByText(/Download Files/)).toBeVisible();
await expect(policyRow.getByText(/Upload Files/)).toBeVisible();
} finally {
await deletePermissionPolicyByName(adminClient, policyName);
}
});
test('MM-T5814 created policy appears in list with correct name, role, and permissions', async ({pw}) => {
test.setTimeout(120000);
await pw.skipIfNoLicense();
const {adminUser, adminClient} = await pw.initSetup();
await ensureUserAttributes(adminClient);
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
await enableABAC(systemConsolePage.page);
await adminClient.patchConfig({AccessControlSettings: {EnableAttributeBasedAccessControl: true}});
const policyName = `PP List Check ${pw.random.id()}`;
try {
await createPermissionPolicy(systemConsolePage.page, {
name: policyName,
celExpression: 'user.attributes.Department == "Legal"',
permissions: ['Download Files'],
role: 'system_guest',
});
// * Row shows name, Guest role, and Download Files permission
const policyRow = systemConsolePage.page.locator('.DataGrid_row').filter({hasText: policyName});
await expect(policyRow).toBeVisible();
await expect(policyRow.getByText('Guest users')).toBeVisible();
await expect(policyRow.getByText('Download Files')).toBeVisible();
} finally {
await deletePermissionPolicyByName(adminClient, policyName);
}
});
test('MM-T5815 admin can cancel policy creation and return to list', async ({pw}) => {
await pw.skipIfNoLicense();
const {adminUser, adminClient} = await pw.initSetup();
await ensureUserAttributes(adminClient);
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
await enableABAC(systemConsolePage.page);
await adminClient.patchConfig({AccessControlSettings: {EnableAttributeBasedAccessControl: true}});
await navigateToPermissionPoliciesPage(systemConsolePage.page);
await systemConsolePage.page.getByRole('button', {name: 'Add policy'}).click();
await systemConsolePage.page.waitForLoadState('networkidle');
await expect(
systemConsolePage.page.getByText('Attribute Based Permission Policy', {exact: true}),
).toBeVisible();
// # Cancel navigates back to list without saving
await systemConsolePage.page.getByRole('link', {name: 'Cancel'}).click();
await systemConsolePage.page.waitForLoadState('networkidle');
await expect(systemConsolePage.page.getByRole('heading', {name: 'Permission Policies'})).toBeVisible();
});
});

View File

@@ -0,0 +1,145 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, test, enableABAC} from '@mattermost/playwright-lib';
import {
ensureUserAttributes,
createPermissionPolicy,
deletePermissionPolicyByName,
navigateToPermissionPoliciesPage,
} from '../support';
/**
* Permission Policies - System Console (MM-64508)
*
* Tests the Permission Policies page under System Attributes > Permission Policies.
* Requires Enterprise Advanced license and ABAC enabled.
*
* Sidebar items (Membership Policies, Permission Policies) are only rendered when
* ABAC is enabled — all tests call enableABAC() first.
*
* This file covers list-page UI and management (delete / search) of existing policies.
*/
test.describe('Permission Policies - List Page', () => {
test('MM-T5801 admin can navigate to Permission Policies page via sidebar', async ({pw}) => {
await pw.skipIfNoLicense();
const {adminUser, adminClient} = await pw.initSetup();
await ensureUserAttributes(adminClient);
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
// # Enable ABAC — sidebar items only appear when ABAC is on.
// enableABAC lands on /membership_policies so the sidebar is already expanded.
await enableABAC(systemConsolePage.page);
// # Click Permission Policies in the sidebar
await systemConsolePage.sidebar.systemAttributes.permissionPolicies.click();
await systemConsolePage.page.waitForLoadState('networkidle');
// * Correct URL and heading
await expect(systemConsolePage.page).toHaveURL(/permission_policies/);
await expect(systemConsolePage.page.getByRole('heading', {name: 'Permission Policies'})).toBeVisible();
// * List columns are present
const section = systemConsolePage.page.getByTestId('sysconsole_section_PermissionPolicies');
await expect(section.getByText('Name')).toBeVisible();
await expect(section.getByText('Role')).toBeVisible();
await expect(section.getByText('Permissions', {exact: true})).toBeVisible();
});
test('MM-T5802 Permission Policies list page has Add policy button and search input', async ({pw}) => {
await pw.skipIfNoLicense();
const {adminUser, adminClient} = await pw.initSetup();
await ensureUserAttributes(adminClient);
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
await enableABAC(systemConsolePage.page);
await navigateToPermissionPoliciesPage(systemConsolePage.page);
await expect(systemConsolePage.page.getByRole('button', {name: 'Add policy'})).toBeVisible();
await expect(systemConsolePage.page.getByPlaceholder('Search')).toBeVisible();
});
test('MM-T5803 Permission Policies list page subtitle describes file permission scope', async ({pw}) => {
await pw.skipIfNoLicense();
const {adminUser, adminClient} = await pw.initSetup();
await ensureUserAttributes(adminClient);
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
await enableABAC(systemConsolePage.page);
await navigateToPermissionPoliciesPage(systemConsolePage.page);
await expect(
systemConsolePage.page.getByText(
'Create policies to control file upload and download permissions based on user attributes',
{exact: false},
),
).toBeVisible();
});
});
test.describe('Permission Policies - Manage Existing Policies', () => {
test('MM-T5816 admin can delete a permission policy', async ({pw}) => {
test.setTimeout(120000);
await pw.skipIfNoLicense();
const {adminUser, adminClient} = await pw.initSetup();
await ensureUserAttributes(adminClient);
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
await enableABAC(systemConsolePage.page);
const policyName = `PP Delete ${pw.random.id()}`;
await createPermissionPolicy(systemConsolePage.page, {
name: policyName,
celExpression: 'user.attributes.Department == "Delete"',
permissions: ['Download Files'],
});
await expect(systemConsolePage.page.getByText(policyName)).toBeVisible();
// # Open the row's action menu and delete
const policyRow = systemConsolePage.page.locator('.DataGrid_row').filter({hasText: policyName});
await policyRow.locator('button[id*="policy-menu"], button[aria-label*="menu" i], button').last().click();
const deleteOption = systemConsolePage.page.getByRole('menuitem', {name: /delete/i});
await deleteOption.click();
// # Deletion from the list fires immediately — no confirmation modal
await systemConsolePage.page.waitForLoadState('networkidle');
// * Policy no longer in list
await expect(systemConsolePage.page.getByText(policyName)).not.toBeVisible();
// # Safety net: API cleanup in case UI deletion failed
await deletePermissionPolicyByName(adminClient, policyName);
});
test('MM-T5817 admin can search for a permission policy by name', async ({pw}) => {
test.setTimeout(120000);
await pw.skipIfNoLicense();
const {adminUser, adminClient} = await pw.initSetup();
await ensureUserAttributes(adminClient);
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
await enableABAC(systemConsolePage.page);
const policyName = `PP Search ${pw.random.id()}`;
try {
await createPermissionPolicy(systemConsolePage.page, {
name: policyName,
celExpression: 'user.attributes.Department == "Search"',
permissions: ['Download Files'],
});
// # Search by the exact name
await systemConsolePage.page.getByPlaceholder('Search').fill(policyName);
await systemConsolePage.page.waitForLoadState('networkidle');
// * Only the matching policy is visible
await expect(systemConsolePage.page.getByText(policyName)).toBeVisible();
} finally {
await deletePermissionPolicyByName(adminClient, policyName);
}
});
});

View File

@@ -0,0 +1,235 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/**
* E2E tests for ranked comparison operators in the Membership Policy editor.
*
* When the selected attribute is of type `rank`, the simple-mode operator
* dropdown replaces the standard set with the ordinal comparison operators
* (is exactly, is not, is at least, is greater than, is at most, is less than).
*/
import type {Page} from '@playwright/test';
import type {Client4} from '@mattermost/client';
import type {UserProfile} from '@mattermost/types/users';
import type {UserPropertyField} from '@mattermost/types/properties_user';
import {expect, getRandomId, test} from '@mattermost/playwright-lib';
import {deleteCustomProfileAttributes} from '../../../channels/custom_profile_attributes/helpers';
import {assertAccessControlAutocompleteContains} from '../support';
// Filter + real click so an off-screen ranked field is selected; click the
// policy name to dismiss a leftover MUI overlay (the filter swallows Escape).
async function selectAttributeInEditor(page: Page, attributeName: string) {
const attributeMenu = page.locator('[id^="attribute-selector-menu"]');
const attributeButton = page.locator('[data-testid="attributeSelectorMenuButton"]').first();
if (!(await attributeMenu.isVisible({timeout: 2000}).catch(() => false))) {
await attributeButton.click();
}
await expect(attributeMenu).toBeVisible();
const filterInput = attributeMenu.locator('.attribute-selector-search input');
await filterInput.waitFor({state: 'visible'});
await filterInput.fill(attributeName);
const option = attributeMenu.getByRole('menuitemradio', {name: attributeName, exact: true});
await expect(option).toBeVisible();
await option.click();
await expect(attributeButton).toContainText(attributeName);
if (await attributeMenu.isVisible().catch(() => false)) {
await page.locator('#admin\\.access_control\\.policy\\.edit_policy\\.policyName').click();
}
await expect(attributeMenu).toBeHidden();
}
test.describe('System Console - Membership Policy ranked operators', () => {
let adminClient: Client4;
let adminUser: UserProfile;
let field: UserPropertyField | undefined;
test.beforeEach(async ({pw}) => {
await pw.ensureLicense();
await pw.skipIfNoLicense();
const clientInfo = await pw.getAdminClient();
adminClient = clientInfo.adminClient;
adminUser = clientInfo.adminUser!;
// # Create an admin-managed ranked attribute (admin-managed → usable in ABAC)
field = await adminClient.createCustomProfileAttributeField({
name: `clearance_${getRandomId()}`,
type: 'rank',
attrs: {
sort_order: 0,
managed: 'admin',
options: [
{name: 'Unclassified', rank: 1},
{name: 'Secret', rank: 2},
{name: 'TopSecret', rank: 3},
],
},
} as any);
// # Make sure ABAC stays enabled (a concurrent initSetup can reset config)
await adminClient.patchConfig({
AccessControlSettings: {EnableAttributeBasedAccessControl: true},
} as any);
await assertAccessControlAutocompleteContains(adminClient, [field.name]);
});
test.afterEach(async () => {
if (field) {
await deleteCustomProfileAttributes(adminClient, {
[field.id]: field,
ownedIds: new Set([field.id]),
} as any);
field = undefined;
}
});
/**
* @objective Selecting a ranked attribute in the policy editor surfaces the
* ordinal operators and removes the equality/string/list operators; the row
* defaults to "is at least".
*
* @precondition
* An admin-managed ranked attribute (Unclassified/Secret/TopSecret) exists.
*/
test('shows ranked comparison operators for a ranked attribute', {tag: '@abac'}, async ({pw}) => {
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
const {page} = systemConsolePage;
// # ABAC is already enabled (see beforeEach) — go straight to Membership Policies
await page.goto('/admin_console/system_attributes/membership_policies');
await page.waitForLoadState('networkidle');
// # Open the new-policy editor and name it
await page.getByRole('button', {name: 'Add policy'}).click();
await page.waitForLoadState('networkidle');
const nameInput = page.locator('#admin\\.access_control\\.policy\\.edit_policy\\.policyName');
await nameInput.waitFor({state: 'visible', timeout: 10000});
await nameInput.fill(`Ranked Policy ${getRandomId()}`);
// # Add an attribute row (reload once if attributes haven't loaded yet)
const addAttributeButton = page.getByRole('button', {name: /add attribute/i});
await addAttributeButton.waitFor({state: 'visible', timeout: 10000});
if (await addAttributeButton.isDisabled()) {
await page.reload();
await page.waitForLoadState('networkidle');
await nameInput.fill(`Ranked Policy ${getRandomId()}`);
await expect(addAttributeButton).toBeEnabled({timeout: 15000});
}
await addAttributeButton.click();
await selectAttributeInEditor(page, field!.name);
// * The row defaults to the canonical ranked operator, "is at least"
const operatorButton = page.locator('[data-testid="operatorSelectorMenuButton"]').first();
await expect(operatorButton).toContainText('is at least');
// # Open the operator dropdown
await operatorButton.click();
// * All six ranked operators are offered (menu items auto-wait for the open menu)
for (const label of ['is exactly', 'is not', 'is at least', 'is greater than', 'is at most', 'is less than']) {
await expect(page.getByRole('menuitemradio', {name: label, exact: true})).toBeVisible();
}
// * The standard (non-ranked) operators are not offered
for (const label of ['is', 'in', 'has any of', 'has all of', 'starts with', 'ends with', 'contains']) {
await expect(page.getByRole('menuitemradio', {name: label, exact: true})).toHaveCount(0);
}
});
/**
* @objective An "is at least <option>" rule built in the editor survives a
* save/reopen round-trip: the stored marker form rehydrates back to the
* operator form and the table editor re-renders the same operator and value.
*
* @precondition
* An admin-managed ranked attribute (Unclassified/Secret/TopSecret) exists.
*/
test('round-trips an "is at least" ranked rule through save and reopen', {tag: '@abac'}, async ({pw}) => {
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
const {page} = systemConsolePage;
const policyName = `Ranked RT ${getRandomId()}`;
let policyId: string | null = null;
try {
await page.goto('/admin_console/system_attributes/membership_policies');
await page.waitForLoadState('networkidle');
// # Create a policy and add a ranked attribute row
await page.getByRole('button', {name: 'Add policy'}).click();
await page.waitForLoadState('networkidle');
const nameInput = page.locator('#admin\\.access_control\\.policy\\.edit_policy\\.policyName');
await nameInput.waitFor({state: 'visible', timeout: 10000});
await nameInput.fill(policyName);
const addAttributeButton = page.getByRole('button', {name: /add attribute/i});
await addAttributeButton.waitFor({state: 'visible', timeout: 10000});
if (await addAttributeButton.isDisabled()) {
await page.reload();
await page.waitForLoadState('networkidle');
await nameInput.fill(policyName);
await expect(addAttributeButton).toBeEnabled({timeout: 15000});
}
await addAttributeButton.click();
// # Select the ranked attribute by name
await selectAttributeInEditor(page, field!.name);
// * Defaults to "is at least"
await expect(page.locator('[data-testid="operatorSelectorMenuButton"]').first()).toContainText(
'is at least',
);
// # Pick the value "Secret"
await page.locator('[data-testid="valueSelectorMenuButton"]').first().click();
await page.getByRole('menuitemradio', {name: 'Secret', exact: true}).click();
await expect(page.locator('[data-testid="valueSelectorMenuButton"]').first()).toContainText('Secret');
// # Save the policy
await page.getByRole('button', {name: 'Save'}).last().click();
await page.waitForLoadState('networkidle');
// # Reopen the policy from the list
await page.goto('/admin_console/system_attributes/membership_policies');
await page.waitForLoadState('networkidle');
const policyRow = page.locator('.policy-name').filter({hasText: policyName}).first();
await expect(policyRow).toBeVisible({timeout: 10000});
const rowId = await policyRow.getAttribute('id');
policyId = rowId?.replace('customDescription-', '') ?? null;
await policyRow.click();
await page.waitForLoadState('networkidle');
// Existing policies open in Advanced mode. The "Switch to Simple Mode"
// toggle is enabled only when isSimpleExpression accepts the stored
// expression — proving the marker form rehydrated to `>= "Secret"`
// (a non-rehydrated `_rank_ge(...)` marker would disable the toggle).
const toSimpleMode = page.getByRole('button', {name: 'Switch to Simple Mode'});
if (await toSimpleMode.isVisible({timeout: 5000}).catch(() => false)) {
await expect(toSimpleMode).toBeEnabled();
await toSimpleMode.click();
}
// * The table editor re-parses the rehydrated rule to the same operator
// and value (no rank integer surfaced in the value chip)
await expect(page.locator('[data-testid="operatorSelectorMenuButton"]').first()).toContainText(
'is at least',
);
const valueButton = page.locator('[data-testid="valueSelectorMenuButton"]').first();
await expect(valueButton).toContainText('Secret');
await expect(valueButton).not.toContainText('2');
} finally {
if (policyId) {
await adminClient.deleteAccessControlPolicy(policyId).catch(() => {});
}
}
});
});