snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {
|
||||
expect,
|
||||
test,
|
||||
enableABAC,
|
||||
navigateToABACPage,
|
||||
runSyncJob,
|
||||
verifyUserInChannel,
|
||||
updateUserAttributes,
|
||||
createUserWithAttributes,
|
||||
} from '@mattermost/playwright-lib';
|
||||
|
||||
import {
|
||||
ensureUserAttributes,
|
||||
createPrivateChannelForABAC,
|
||||
createBasicPolicy,
|
||||
activatePolicy,
|
||||
waitForLatestSyncJob,
|
||||
} from '../support';
|
||||
|
||||
/**
|
||||
* ABAC User Attributes - Attribute Changes
|
||||
* Tests for user attribute changes affecting ABAC policies
|
||||
*/
|
||||
test.describe('ABAC User Attributes - Attribute Changes', () => {
|
||||
/**
|
||||
* MM-T5794: User is auto-added to channel when a qualifying attribute is added to their profile (auto-add true)
|
||||
*
|
||||
* Step 1:
|
||||
* With at least one access policy in existence on the server, set to auto-add, and applied to a channel:
|
||||
* 1. As system admin make a note of the attribute needed for a user to be auto-added to a channel
|
||||
* 2. As a user not in the channel and not having the required attribute
|
||||
* 3. Click user's own profile picture top right and select Profile
|
||||
* 4. Scroll down to the required custom attribute, click Edit, and add the required value
|
||||
*/
|
||||
test('MM-T5794 User auto-added when qualifying attribute is added to profile', async ({pw}) => {
|
||||
test.setTimeout(120000);
|
||||
|
||||
await pw.skipIfNoLicense();
|
||||
|
||||
// ============================================================
|
||||
// SETUP: Create attribute, policy, and channel
|
||||
// ============================================================
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
|
||||
// Setup attributes (using ensureUserAttributes like MM-T5800 does)
|
||||
await ensureUserAttributes(adminClient);
|
||||
|
||||
// Create test user with NON-qualifying Department attribute (same pattern as MM-T5800)
|
||||
// MM-T5800 creates user with Department=Sales, then changes to Engineering
|
||||
// We do the same: Start with Sales (non-qualifying), then change to Engineering (qualifying)
|
||||
const testUser = await createUserWithAttributes(adminClient, {Department: 'Sales'});
|
||||
await adminClient.addToTeam(team.id, testUser.id);
|
||||
|
||||
// Create private channel
|
||||
const privateChannel = await createPrivateChannelForABAC(adminClient, team.id);
|
||||
|
||||
// ============================================================
|
||||
// STEP 1: Create ABAC policy with auto-add enabled
|
||||
// Policy requirement: Department == "Engineering"
|
||||
// ============================================================
|
||||
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
|
||||
await navigateToABACPage(systemConsolePage.page);
|
||||
await enableABAC(systemConsolePage.page);
|
||||
|
||||
const policyName = `Engineering Access ${pw.random.id()}`;
|
||||
const jobId = await createBasicPolicy(systemConsolePage.page, {
|
||||
name: policyName,
|
||||
attribute: 'Department',
|
||||
operator: '==',
|
||||
value: 'Engineering',
|
||||
autoSync: true, // ✅ Auto-add enabled
|
||||
channels: [privateChannel.display_name],
|
||||
});
|
||||
|
||||
// Activate policy (EXACT same pattern as MM-T5800)
|
||||
await waitForLatestSyncJob(systemConsolePage.page, undefined, jobId);
|
||||
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 policyId = (await policyRow.getAttribute('id'))?.replace('customDescription-', '');
|
||||
|
||||
if (policyId) {
|
||||
await activatePolicy(adminClient, policyId);
|
||||
}
|
||||
await searchInput.clear();
|
||||
|
||||
// Re-apply guard: concurrent initSetup() resets ABAC between enableABAC() UI call and sync
|
||||
await adminClient.patchConfig({
|
||||
AccessControlSettings: {EnableAttributeBasedAccessControl: true},
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// STEP 2: Verify user is NOT in channel initially
|
||||
// ============================================================
|
||||
const syncJob1 = await runSyncJob(systemConsolePage.page);
|
||||
await waitForLatestSyncJob(systemConsolePage.page, undefined, syncJob1);
|
||||
|
||||
const initialInChannel = await verifyUserInChannel(adminClient, testUser.id, privateChannel.id);
|
||||
expect(initialInChannel).toBe(false);
|
||||
|
||||
// ============================================================
|
||||
// STEPS 3-5: Add qualifying attribute to user's profile
|
||||
// Note: Using API for attribute update. UI testing for profile editing
|
||||
// is covered in separate user profile test suite.
|
||||
// ============================================================
|
||||
await updateUserAttributes(adminClient, testUser.id, {Department: 'Engineering'});
|
||||
|
||||
// ============================================================
|
||||
// STEP 6: Run sync job to trigger auto-add
|
||||
// ============================================================
|
||||
|
||||
// DEBUG: Verify attribute was updated before sync
|
||||
await adminClient.getUserCustomProfileAttributesValues(testUser.id);
|
||||
|
||||
// Get the Department field to check its value
|
||||
await adminClient.getCustomProfileAttributeFields();
|
||||
|
||||
const syncJob2 = await runSyncJob(systemConsolePage.page);
|
||||
await waitForLatestSyncJob(systemConsolePage.page, undefined, syncJob2);
|
||||
|
||||
// ============================================================
|
||||
// VERIFICATION: User should now be auto-added to channel
|
||||
// ============================================================
|
||||
|
||||
// DEBUG: Check all channel members
|
||||
await adminClient.getChannelMembers(privateChannel.id);
|
||||
|
||||
const finalInChannel = await verifyUserInChannel(adminClient, testUser.id, privateChannel.id);
|
||||
|
||||
if (!finalInChannel) {
|
||||
// console.error('\n[ERROR] User NOT in channel after sync!');
|
||||
// console.error('[ERROR] This means the ABAC sync did not add the user.');
|
||||
// console.error('[ERROR] Possible causes:');
|
||||
// console.error('[ERROR] 1. Policy not active');
|
||||
// console.error('[ERROR] 2. Attribute value not matching policy');
|
||||
// console.error('[ERROR] 3. Sync job failed silently');
|
||||
}
|
||||
|
||||
expect(finalInChannel).toBe(true);
|
||||
|
||||
// ============================================================
|
||||
// VERIFICATION: Check for "User added" system message
|
||||
// ============================================================
|
||||
|
||||
// Get recent posts from the channel
|
||||
const posts = await adminClient.getPosts(privateChannel.id, 0, 10);
|
||||
const postList = posts.order.map((postId: string) => posts.posts[postId]);
|
||||
|
||||
// Find system message for user being added
|
||||
const userAddedMessage = postList.find((post: any) => {
|
||||
return (
|
||||
post.type === 'system_add_to_channel' &&
|
||||
post.props?.addedUserId === testUser.id &&
|
||||
post.user_id === 'system'
|
||||
);
|
||||
});
|
||||
|
||||
if (userAddedMessage) {
|
||||
// System message found
|
||||
} else {
|
||||
// System message not found (may be disabled in test env)
|
||||
}
|
||||
|
||||
// System messages might be disabled in test env, so we don't fail the test
|
||||
// The important verification is that the user was added
|
||||
expect(finalInChannel).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,139 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {
|
||||
expect,
|
||||
test,
|
||||
enableABAC,
|
||||
navigateToABACPage,
|
||||
verifyUserInChannel,
|
||||
updateUserAttributes,
|
||||
} from '@mattermost/playwright-lib';
|
||||
|
||||
import type {CustomProfileAttribute} from '../../../channels/custom_profile_attributes/helpers';
|
||||
import {setupCustomProfileAttributeFields} from '../../../channels/custom_profile_attributes/helpers';
|
||||
import {
|
||||
createUserForABAC,
|
||||
createPrivateChannelForABAC,
|
||||
createBasicPolicy,
|
||||
enableUserManagedAttributes,
|
||||
} from '../support';
|
||||
|
||||
/**
|
||||
* ABAC User Attributes - Attribute Changes
|
||||
* Tests for user attribute changes affecting ABAC policies
|
||||
*/
|
||||
test.describe('ABAC User Attributes - Attribute Changes', () => {
|
||||
/**
|
||||
* MM-T5795: User can be added to channel by system admin after a qualifying attribute is added to their profile (auto-add false)
|
||||
*
|
||||
* Preconditions:
|
||||
* - Access policy with auto-add set to FALSE
|
||||
*
|
||||
* Steps:
|
||||
* 1. As system admin, note the required attribute for channel access
|
||||
* 2. As a user not in the channel and lacking the required attribute:
|
||||
* - Add the required attribute value to user profile
|
||||
* 3. As system admin, go to the channel and add the user
|
||||
*
|
||||
* Expected:
|
||||
* - User who now meets the policy CAN be added to the channel by the admin
|
||||
* - "User added" message is posted in the channel by System
|
||||
*/
|
||||
test('MM-T5795 User can be added by admin after attribute added (auto-add false)', async ({pw}) => {
|
||||
test.setTimeout(120000);
|
||||
|
||||
await pw.skipIfNoLicense();
|
||||
|
||||
// ============================================================
|
||||
// SETUP: Create attribute, policy with auto-add FALSE, and channel
|
||||
// ============================================================
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
|
||||
await enableUserManagedAttributes(adminClient);
|
||||
|
||||
const attributeFields: CustomProfileAttribute[] = [{name: 'Department', type: 'text', value: ''}];
|
||||
const attributeFieldsMap = await setupCustomProfileAttributeFields(adminClient, attributeFields);
|
||||
|
||||
// Create test user WITHOUT the qualifying attribute
|
||||
const testUser = await createUserForABAC(adminClient, attributeFieldsMap, []);
|
||||
await adminClient.addToTeam(team.id, testUser.id);
|
||||
|
||||
const privateChannel = await createPrivateChannelForABAC(adminClient, team.id);
|
||||
|
||||
// ============================================================
|
||||
// STEP 1: Create policy with auto-add DISABLED
|
||||
// ============================================================
|
||||
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
|
||||
await navigateToABACPage(systemConsolePage.page);
|
||||
await enableABAC(systemConsolePage.page);
|
||||
|
||||
await adminClient.patchConfig({
|
||||
AccessControlSettings: {EnableAttributeBasedAccessControl: true},
|
||||
});
|
||||
await pw.waitUntil(async () => {
|
||||
const cfg = await adminClient.getConfig();
|
||||
return cfg.AccessControlSettings?.EnableAttributeBasedAccessControl === true;
|
||||
});
|
||||
|
||||
const policyName = `Engineering Manual Add ${pw.random.id()}`;
|
||||
await createBasicPolicy(systemConsolePage.page, {
|
||||
name: policyName,
|
||||
attribute: 'Department',
|
||||
operator: '==',
|
||||
value: 'Engineering',
|
||||
autoSync: false, // ✅ Auto-add DISABLED
|
||||
channels: [privateChannel.display_name],
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// STEP 2: Add qualifying attribute to user
|
||||
// ============================================================
|
||||
await updateUserAttributes(adminClient, testUser.id, {Department: 'Engineering'});
|
||||
|
||||
// ============================================================
|
||||
// STEP 3: Admin manually adds user to channel
|
||||
// ============================================================
|
||||
|
||||
await adminClient.patchConfig({
|
||||
AccessControlSettings: {EnableAttributeBasedAccessControl: true},
|
||||
});
|
||||
await pw.waitUntil(async () => {
|
||||
const cfg = await adminClient.getConfig();
|
||||
return cfg.AccessControlSettings?.EnableAttributeBasedAccessControl === true;
|
||||
});
|
||||
|
||||
// Re-create the Department CPA field if a concurrent initSetup() deleted it.
|
||||
// Without the field the server returns "An attribute is missing from the expression"
|
||||
// on addToChannel, because the policy references a field id that no longer exists.
|
||||
await setupCustomProfileAttributeFields(adminClient, attributeFields);
|
||||
|
||||
// Verify user can be added (policy allows it since user has qualifying attribute)
|
||||
await adminClient.addToChannel(testUser.id, privateChannel.id);
|
||||
|
||||
// Membership + ABAC evaluation can lag behind the REST response in CI.
|
||||
await expect
|
||||
.poll(async () => verifyUserInChannel(adminClient, testUser.id, privateChannel.id), {
|
||||
timeout: 60000,
|
||||
intervals: [1000, 2000, 3000],
|
||||
})
|
||||
.toBe(true);
|
||||
|
||||
// ============================================================
|
||||
// VERIFICATION: Check for "User added" system message
|
||||
// ============================================================
|
||||
|
||||
const posts = await adminClient.getPosts(privateChannel.id, 0, 10);
|
||||
const postList = posts.order.map((postId: string) => posts.posts[postId]);
|
||||
|
||||
const userAddedMessage = postList.find((post: any) => {
|
||||
return post.type === 'system_add_to_channel' && post.props?.addedUserId === testUser.id;
|
||||
});
|
||||
|
||||
if (userAddedMessage) {
|
||||
// System message found
|
||||
} else {
|
||||
// System message not found (may be disabled in test env)
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,215 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {
|
||||
expect,
|
||||
test,
|
||||
enableABAC,
|
||||
navigateToABACPage,
|
||||
runSyncJob,
|
||||
verifyUserInChannel,
|
||||
updateUserAttributes,
|
||||
} from '@mattermost/playwright-lib';
|
||||
|
||||
import type {CustomProfileAttribute} from '../../../channels/custom_profile_attributes/helpers';
|
||||
import {setupCustomProfileAttributeFields} from '../../../channels/custom_profile_attributes/helpers';
|
||||
import {
|
||||
createUserForABAC,
|
||||
createPrivateChannelForABAC,
|
||||
createBasicPolicy,
|
||||
activatePolicy,
|
||||
waitForLatestSyncJob,
|
||||
enableUserManagedAttributes,
|
||||
} from '../support';
|
||||
|
||||
/**
|
||||
* ABAC User Attributes - Attribute Changes
|
||||
* Tests for user attribute changes affecting ABAC policies
|
||||
*/
|
||||
test.describe('ABAC User Attributes - Attribute Changes', () => {
|
||||
/**
|
||||
* MM-T5796: User is auto-removed from channel when required attribute is removed
|
||||
*
|
||||
* Test Scenario 1 & 2 (Auto-add: False & True):
|
||||
* Steps:
|
||||
* 1. As system admin, identify the required attribute for channel access
|
||||
* 2. Log in as a user currently in the channel with the required attribute
|
||||
* 3. Edit user's profile
|
||||
* 4. Remove or change the required attribute value
|
||||
* 5. Save changes
|
||||
*
|
||||
* Expected:
|
||||
* - User is automatically removed from the channel
|
||||
* - System posts a "User removed" message in the channel
|
||||
*/
|
||||
test('MM-T5796 User auto-removed when required attribute is removed', async ({pw}) => {
|
||||
test.setTimeout(180000);
|
||||
|
||||
await pw.skipIfNoLicense();
|
||||
|
||||
// ============================================================
|
||||
// SETUP
|
||||
// ============================================================
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
|
||||
await enableUserManagedAttributes(adminClient);
|
||||
|
||||
const attributeFields: CustomProfileAttribute[] = [{name: 'Department', type: 'text', value: ''}];
|
||||
const attributeFieldsMap = await setupCustomProfileAttributeFields(adminClient, attributeFields);
|
||||
|
||||
// Create test user WITH the qualifying attribute (starts with Department=Engineering)
|
||||
const testUser = await createUserForABAC(adminClient, attributeFieldsMap, [
|
||||
{name: 'Department', type: 'text', value: 'Engineering'},
|
||||
]);
|
||||
await adminClient.addToTeam(team.id, testUser.id);
|
||||
|
||||
const privateChannel = await createPrivateChannelForABAC(adminClient, team.id);
|
||||
|
||||
// ============================================================
|
||||
// TEST SCENARIO 1: Auto-add FALSE
|
||||
// ============================================================
|
||||
|
||||
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
|
||||
await navigateToABACPage(systemConsolePage.page);
|
||||
await enableABAC(systemConsolePage.page);
|
||||
|
||||
const policy1Name = `Engineering Access NoAutoAdd ${pw.random.id()}`;
|
||||
const jobId1 = await createBasicPolicy(systemConsolePage.page, {
|
||||
name: policy1Name,
|
||||
attribute: 'Department',
|
||||
operator: '==',
|
||||
value: 'Engineering',
|
||||
autoSync: false, // Auto-add FALSE
|
||||
channels: [privateChannel.display_name],
|
||||
});
|
||||
|
||||
// Manually add user to channel
|
||||
await adminClient.addToChannel(testUser.id, privateChannel.id);
|
||||
const initialInChannel = await verifyUserInChannel(adminClient, testUser.id, privateChannel.id);
|
||||
expect(initialInChannel).toBe(true);
|
||||
|
||||
// Get policy ID and activate
|
||||
await waitForLatestSyncJob(systemConsolePage.page, undefined, jobId1);
|
||||
const searchInput = systemConsolePage.page.locator('input[placeholder*="Search" i]').first();
|
||||
await searchInput.waitFor({state: 'visible', timeout: 5000});
|
||||
const idMatch = policy1Name.match(/([a-z0-9]+)$/i);
|
||||
const uniqueId = idMatch ? idMatch[1] : policy1Name;
|
||||
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) {
|
||||
await activatePolicy(adminClient, policyId);
|
||||
}
|
||||
await searchInput.clear();
|
||||
|
||||
// Remove the qualifying attribute
|
||||
await updateUserAttributes(adminClient, testUser.id, {Department: 'Sales'});
|
||||
|
||||
// Wait for attribute change to propagate
|
||||
await systemConsolePage.page.waitForTimeout(1000);
|
||||
|
||||
await adminClient.patchConfig({
|
||||
AccessControlSettings: {EnableAttributeBasedAccessControl: true},
|
||||
});
|
||||
|
||||
// Run sync job
|
||||
const syncJob1 = await runSyncJob(systemConsolePage.page);
|
||||
await waitForLatestSyncJob(systemConsolePage.page, undefined, syncJob1);
|
||||
|
||||
// Wait for membership updates to apply
|
||||
await systemConsolePage.page.waitForTimeout(1000);
|
||||
|
||||
// Verify user is removed
|
||||
const userInChannelAfterRemoval = await verifyUserInChannel(adminClient, testUser.id, privateChannel.id);
|
||||
expect(userInChannelAfterRemoval).toBe(false);
|
||||
|
||||
// Check for removal system message
|
||||
const posts = await adminClient.getPosts(privateChannel.id, 0, 10);
|
||||
const postList = posts.order.map((postId: string) => posts.posts[postId]);
|
||||
|
||||
const userRemovedMessage = postList.find((post: any) => {
|
||||
return (
|
||||
(post.type === 'system_remove_from_channel' || post.type === 'system_leave_channel') &&
|
||||
(post.props?.removedUserId === testUser.id || post.user_id === testUser.id)
|
||||
);
|
||||
});
|
||||
|
||||
if (userRemovedMessage) {
|
||||
// System message found
|
||||
} else {
|
||||
// System message not found (may be disabled in test env)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// TEST SCENARIO 2: Auto-add TRUE
|
||||
// ============================================================
|
||||
|
||||
// Restore user attribute and create new policy with auto-add=true
|
||||
await updateUserAttributes(adminClient, testUser.id, {Department: 'Engineering'});
|
||||
|
||||
const channel2 = await createPrivateChannelForABAC(adminClient, team.id);
|
||||
|
||||
await navigateToABACPage(systemConsolePage.page);
|
||||
|
||||
const policy2Name = `Engineering Access WithAutoAdd ${pw.random.id()}`;
|
||||
const jobId2 = await createBasicPolicy(systemConsolePage.page, {
|
||||
name: policy2Name,
|
||||
attribute: 'Department',
|
||||
operator: '==',
|
||||
value: 'Engineering',
|
||||
autoSync: true, // Auto-add TRUE
|
||||
channels: [channel2.display_name],
|
||||
});
|
||||
|
||||
// Activate and run sync to auto-add user
|
||||
await waitForLatestSyncJob(systemConsolePage.page, undefined, jobId2);
|
||||
await searchInput.fill(policy2Name.match(/([a-z0-9]+)$/i)?.[1] || policy2Name);
|
||||
await systemConsolePage.page.waitForTimeout(1000);
|
||||
|
||||
const policyRow2 = systemConsolePage.page.locator('.policy-name').first();
|
||||
const policyId2 = (await policyRow2.getAttribute('id'))?.replace('customDescription-', '');
|
||||
|
||||
if (policyId2) {
|
||||
await activatePolicy(adminClient, policyId2);
|
||||
}
|
||||
await searchInput.clear();
|
||||
|
||||
// Re-apply ABAC enable guard: a concurrent initSetup() on another shard may have
|
||||
// disabled ABAC between the initial enableABAC call and this sync job.
|
||||
// Without ABAC enabled the server skips policy evaluation and won't auto-add the user.
|
||||
await adminClient.patchConfig({
|
||||
AccessControlSettings: {EnableAttributeBasedAccessControl: true},
|
||||
});
|
||||
|
||||
const syncJob2 = await runSyncJob(systemConsolePage.page);
|
||||
await waitForLatestSyncJob(systemConsolePage.page, undefined, syncJob2);
|
||||
|
||||
const userAutoAdded = await verifyUserInChannel(adminClient, testUser.id, channel2.id);
|
||||
expect(userAutoAdded).toBe(true);
|
||||
|
||||
// Remove attribute again
|
||||
await updateUserAttributes(adminClient, testUser.id, {Department: 'Marketing'});
|
||||
|
||||
// Wait for attribute change to propagate
|
||||
await systemConsolePage.page.waitForTimeout(1000);
|
||||
|
||||
await adminClient.patchConfig({
|
||||
AccessControlSettings: {EnableAttributeBasedAccessControl: true},
|
||||
});
|
||||
|
||||
// Run sync
|
||||
const syncJob3 = await runSyncJob(systemConsolePage.page);
|
||||
await waitForLatestSyncJob(systemConsolePage.page, undefined, syncJob3);
|
||||
|
||||
// Small delay for channel membership update
|
||||
await systemConsolePage.page.waitForTimeout(1000);
|
||||
|
||||
// Verify user is removed
|
||||
const userRemovedFromChannel2 = await verifyUserInChannel(adminClient, testUser.id, channel2.id);
|
||||
expect(userRemovedFromChannel2).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,204 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {Client4} from '@mattermost/client';
|
||||
import type {UserPropertyField} from '@mattermost/types/properties_user';
|
||||
|
||||
import {expect, test, enableABAC, navigateToABACPage} from '@mattermost/playwright-lib';
|
||||
|
||||
import type {CustomProfileAttribute} from '../../../channels/custom_profile_attributes/helpers';
|
||||
import {
|
||||
deleteCustomProfileAttributes,
|
||||
setupCustomProfileAttributeFields,
|
||||
} from '../../../channels/custom_profile_attributes/helpers';
|
||||
import {fillSingleConditionValue, getPolicyIdByName} from '../support';
|
||||
|
||||
type FieldsMap = Record<string, UserPropertyField>;
|
||||
|
||||
async function clearExistingFields(client: Client4): Promise<void> {
|
||||
try {
|
||||
const existing = await client.getCustomProfileAttributeFields();
|
||||
if (existing?.length) {
|
||||
const map: FieldsMap = {};
|
||||
for (const f of existing) {
|
||||
map[f.id] = f;
|
||||
}
|
||||
await deleteCustomProfileAttributes(client, map);
|
||||
}
|
||||
} catch {
|
||||
// No fields to clean up
|
||||
}
|
||||
}
|
||||
|
||||
test.describe('ABAC Attribute Selector - display_name rendering and filtering', () => {
|
||||
/**
|
||||
* @objective Verify the attribute selector renders display_name when set,
|
||||
* falls back to `name`, filters on both, and persists the CEL identifier
|
||||
* in saved policy expressions.
|
||||
*
|
||||
* @precondition
|
||||
* Two admin-managed CPA fields seeded via REST: `dept_head` with
|
||||
* display_name 'Department Head', and `office` with no display_name.
|
||||
*/
|
||||
test(
|
||||
'renders and filters by display_name while persisting CEL identifier',
|
||||
{tag: '@user_attributes'},
|
||||
async ({pw}) => {
|
||||
test.setTimeout(120000);
|
||||
|
||||
await pw.ensureLicense();
|
||||
await pw.skipIfNoLicense();
|
||||
|
||||
const {adminUser, adminClient} = await pw.initSetup();
|
||||
|
||||
await clearExistingFields(adminClient);
|
||||
|
||||
const seedAttributes: CustomProfileAttribute[] = [
|
||||
{
|
||||
name: 'dept_head',
|
||||
type: 'text',
|
||||
attrs: {
|
||||
display_name: 'Department Head',
|
||||
visibility: 'when_set',
|
||||
managed: 'admin',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'office',
|
||||
type: 'text',
|
||||
attrs: {
|
||||
visibility: 'when_set',
|
||||
managed: 'admin',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const fieldsMap = await setupCustomProfileAttributeFields(adminClient, seedAttributes);
|
||||
|
||||
const policyName = `Display Name Selector ${pw.random.id()}`;
|
||||
let policyId: string | null = null;
|
||||
|
||||
try {
|
||||
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
|
||||
const {page} = systemConsolePage;
|
||||
|
||||
await navigateToABACPage(page);
|
||||
await enableABAC(page);
|
||||
|
||||
// # Open the new-policy form
|
||||
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);
|
||||
|
||||
// # Add an attribute rule and open its selector
|
||||
const addAttributeButton = page.getByRole('button', {name: /add attribute/i});
|
||||
await expect(addAttributeButton).toBeEnabled({timeout: 10000});
|
||||
await addAttributeButton.click();
|
||||
|
||||
const attributeButton = page.locator('[data-testid="attributeSelectorMenuButton"]').first();
|
||||
await attributeButton.waitFor({state: 'visible', timeout: 5000});
|
||||
|
||||
const attributeMenu = page.locator('[id^="attribute-selector-menu"]');
|
||||
|
||||
if (!(await attributeMenu.isVisible({timeout: 1000}).catch(() => false))) {
|
||||
await attributeButton.click();
|
||||
}
|
||||
await attributeMenu.waitFor({state: 'visible', timeout: 5000});
|
||||
|
||||
const deptHeadItem = page.locator('[id^="attribute-selector-menu"] li:has-text("Department Head")');
|
||||
const officeItem = page.locator('[id^="attribute-selector-menu"] li:has-text("office")');
|
||||
|
||||
// * Both fields render: 'Department Head' (display_name) and 'office' (name fallback)
|
||||
await expect(deptHeadItem).toBeVisible();
|
||||
await expect(officeItem).toBeVisible();
|
||||
|
||||
const filterInput = attributeMenu.locator('.attribute-selector-search input');
|
||||
await filterInput.waitFor({state: 'visible', timeout: 5000});
|
||||
|
||||
// * Filter by display_name keeps only 'Department Head'
|
||||
await filterInput.fill('department');
|
||||
await expect(deptHeadItem).toBeVisible();
|
||||
await expect(officeItem).toHaveCount(0);
|
||||
|
||||
// * Filter by CEL identifier keeps only 'Department Head'
|
||||
await filterInput.fill('');
|
||||
await filterInput.fill('dept_head');
|
||||
await expect(deptHeadItem).toBeVisible();
|
||||
await expect(officeItem).toHaveCount(0);
|
||||
|
||||
// * Filter on the no-display_name field's `name` keeps only 'office'
|
||||
await filterInput.fill('');
|
||||
await filterInput.fill('office');
|
||||
await expect(officeItem).toBeVisible();
|
||||
await expect(deptHeadItem).toHaveCount(0);
|
||||
|
||||
// # Select 'Department Head'
|
||||
await filterInput.fill('');
|
||||
await deptHeadItem.first().click({force: true});
|
||||
|
||||
// * The trigger button shows display_name, not the CEL identifier
|
||||
await expect(attributeButton).toContainText('Department Head', {timeout: 5000});
|
||||
|
||||
// # Wait for the attribute-selector popover to close before opening the next menu
|
||||
const attributeMenuPopover = page.locator('[id^="attribute-selector-menu"]');
|
||||
await attributeMenuPopover.waitFor({state: 'hidden', timeout: 5000});
|
||||
|
||||
const operatorButton = page.locator('[data-testid="operatorSelectorMenuButton"]').first();
|
||||
await operatorButton.waitFor({state: 'visible', timeout: 5000});
|
||||
await operatorButton.click();
|
||||
|
||||
const operatorMenu = page.locator('[id^="operator-selector-menu"]');
|
||||
await operatorMenu.waitFor({state: 'visible', timeout: 5000});
|
||||
await operatorMenu.locator('li:has-text("is")').first().click();
|
||||
|
||||
await fillSingleConditionValue(page, 'engineering');
|
||||
|
||||
const saveButton = page.getByRole('button', {name: 'Save'});
|
||||
await expect(saveButton).toBeEnabled({timeout: 10000});
|
||||
|
||||
// # Click Save and wait on the create-policy PUT (button unmounts on navigate)
|
||||
const createPolicyResponse = page.waitForResponse(
|
||||
(r) => /\/access_control_policies(?:\?|$)/.test(r.url()) && r.request().method() === 'PUT',
|
||||
{timeout: 15000},
|
||||
);
|
||||
await saveButton.click();
|
||||
const createResponse = await createPolicyResponse;
|
||||
expect(createResponse.ok()).toBe(true);
|
||||
|
||||
await page.waitForURL(/\/admin_console\/system_attributes\/membership_policies/, {timeout: 10000});
|
||||
|
||||
// * The persisted CEL uses the canonical identifier, not display_name
|
||||
policyId = await getPolicyIdByName(adminClient, policyName);
|
||||
expect(policyId).not.toBeNull();
|
||||
|
||||
const policy = await (adminClient as any).doFetch(
|
||||
`${adminClient.getBaseRoute()}/access_control_policies/${policyId}`,
|
||||
{method: 'GET'},
|
||||
);
|
||||
|
||||
const rules = (policy?.rules || []) as Array<{actions?: string[]; expression?: string}>;
|
||||
const membershipRule = rules.find((r) => r.actions?.includes('membership')) || rules[0];
|
||||
|
||||
expect(membershipRule).toBeDefined();
|
||||
expect(membershipRule?.expression || '').toContain('user.attributes.dept_head');
|
||||
expect(membershipRule?.expression || '').not.toContain('Department Head');
|
||||
} finally {
|
||||
if (policyId) {
|
||||
try {
|
||||
await (adminClient as any).doFetch(
|
||||
`${adminClient.getBaseRoute()}/access_control_policies/${policyId}`,
|
||||
{method: 'DELETE'},
|
||||
);
|
||||
} catch {
|
||||
// best-effort cleanup
|
||||
}
|
||||
}
|
||||
|
||||
await deleteCustomProfileAttributes(adminClient, fieldsMap);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user