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
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:
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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(() => {});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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(() => {});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user