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,246 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test, enableABAC, expectFilesRedacted, getRandomId, testConfig} from '@mattermost/playwright-lib';
|
||||
|
||||
import {createPermissionPolicy, deletePermissionPolicyByName, navigateToPermissionPoliciesPage} from '../support';
|
||||
|
||||
import {ensureABACEnabled, setupUserAndChannel, waitForPolicy} from './helpers';
|
||||
|
||||
// Each surface renders attachments through a different React tree, so one that forgets to
|
||||
// honour RedactedFileCount is invisible to server-side coverage.
|
||||
//
|
||||
// Denies via a comparison no test user satisfies: policy creation rejects a bare `false`
|
||||
// literal, and these users have no Department value.
|
||||
const DENY_ALL_CEL = "user.attributes.Department == 'no-such-value-deny-all'";
|
||||
const THREAD_ROOT_MESSAGE = 'thread root with file';
|
||||
|
||||
test.describe('ABAC file permissions - redaction across surfaces', () => {
|
||||
let lastPolicyName = '';
|
||||
let savedAdminClient: any = null;
|
||||
|
||||
test.beforeEach(async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
});
|
||||
|
||||
test.afterEach(async () => {
|
||||
if (lastPolicyName && savedAdminClient) {
|
||||
await deletePermissionPolicyByName(savedAdminClient, lastPolicyName);
|
||||
lastPolicyName = '';
|
||||
savedAdminClient = null;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @objective Verify a Burn-on-Read message's attachment is redacted after the recipient
|
||||
* reveals it, so the reveal cannot be used to sidestep the download policy.
|
||||
*/
|
||||
test(
|
||||
'MM-T5827 redacts the attachment of a revealed Burn-on-Read message',
|
||||
{tag: '@abac_file_permissions'},
|
||||
async ({pw}) => {
|
||||
test.setTimeout(pw.duration.four_min);
|
||||
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
savedAdminClient = adminClient;
|
||||
const {testUser: deniedUser, channelName} = await setupUserAndChannel(adminClient, team);
|
||||
|
||||
// # Admin sends a Burn-on-Read message carrying a file
|
||||
const {channelsPage: adminChannelsPage} = await pw.testBrowser.login(adminUser);
|
||||
await adminChannelsPage.goto(team.name, channelName);
|
||||
await adminChannelsPage.toBeVisible();
|
||||
await adminChannelsPage.centerView.postCreate.toggleBurnOnRead();
|
||||
await adminChannelsPage.centerView.postCreate.postMessage('BOR with file', ['sample_text_file.txt']);
|
||||
|
||||
// # Enable ABAC and deny download for everyone
|
||||
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
|
||||
await enableABAC(systemConsolePage.page);
|
||||
await navigateToPermissionPoliciesPage(systemConsolePage.page);
|
||||
|
||||
lastPolicyName = `BOR Download Deny ${getRandomId()}`;
|
||||
await createPermissionPolicy(systemConsolePage.page, {
|
||||
name: lastPolicyName,
|
||||
celExpression: DENY_ALL_CEL,
|
||||
permissions: ['Download Files'],
|
||||
adminClient,
|
||||
});
|
||||
await waitForPolicy(adminClient, lastPolicyName);
|
||||
await ensureABACEnabled(adminClient);
|
||||
|
||||
// # The denied user opens the channel and reveals the concealed message
|
||||
const {channelsPage} = await pw.testBrowser.login(deniedUser);
|
||||
await channelsPage.goto(team.name, channelName);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const post = await channelsPage.centerView.getLastPost();
|
||||
await post.concealedPlaceholder.toBeVisible();
|
||||
await post.concealedPlaceholder.clickToReveal();
|
||||
await post.concealedPlaceholder.waitForReveal(pw.duration.ten_sec);
|
||||
|
||||
// * Verify the revealed message's file is redacted
|
||||
await post.toHaveFilesRedacted();
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @objective Verify the file inside an embedded permalink preview is redacted, not just the
|
||||
* file on the original post — the preview is built from a separately fetched post.
|
||||
*/
|
||||
test(
|
||||
'MM-T5828 redacts the attachment inside an embedded permalink preview',
|
||||
{tag: '@abac_file_permissions'},
|
||||
async ({pw}) => {
|
||||
test.setTimeout(pw.duration.four_min);
|
||||
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
savedAdminClient = adminClient;
|
||||
const {testUser: deniedUser, channelName, channelId} = await setupUserAndChannel(adminClient, team);
|
||||
|
||||
// # Admin posts a file, then posts a permalink to it in the same channel
|
||||
const {channelsPage: adminChannelsPage} = await pw.testBrowser.login(adminUser);
|
||||
await adminChannelsPage.goto(team.name, channelName);
|
||||
await adminChannelsPage.toBeVisible();
|
||||
await adminChannelsPage.centerView.postCreate.postMessage('original with file', ['sample_text_file.txt']);
|
||||
|
||||
const originalPosts = await adminClient.getPosts(channelId, 0, 1);
|
||||
const originalPostId = originalPosts.order[0];
|
||||
|
||||
// Use testConfig.internalBaseURL rather than the admin client's host-mapped route:
|
||||
// the server must recognize this URL as its own SiteURL to embed it through an
|
||||
// internal permalink lookup, instead of fetching it back over HTTP as a link
|
||||
// (which fails under testcontainers).
|
||||
const permalinkUrl = `${testConfig.internalBaseURL}/${team.name}/pl/${originalPostId}`;
|
||||
await adminChannelsPage.centerView.postCreate.postMessage(permalinkUrl);
|
||||
|
||||
// Both posts are addressed by ID: the embed repeats the original's text, so filtering
|
||||
// by text cannot tell the two of them apart.
|
||||
const permalinkPosts = await adminClient.getPosts(channelId, 0, 1);
|
||||
const permalinkPostId = permalinkPosts.order[0];
|
||||
|
||||
// # Enable ABAC and deny download for everyone
|
||||
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
|
||||
await enableABAC(systemConsolePage.page);
|
||||
await navigateToPermissionPoliciesPage(systemConsolePage.page);
|
||||
|
||||
lastPolicyName = `Permalink Download Deny ${getRandomId()}`;
|
||||
await createPermissionPolicy(systemConsolePage.page, {
|
||||
name: lastPolicyName,
|
||||
celExpression: DENY_ALL_CEL,
|
||||
permissions: ['Download Files'],
|
||||
adminClient,
|
||||
});
|
||||
await waitForPolicy(adminClient, lastPolicyName);
|
||||
await ensureABACEnabled(adminClient);
|
||||
|
||||
// # The denied user opens the channel
|
||||
const {channelsPage} = await pw.testBrowser.login(deniedUser);
|
||||
await channelsPage.goto(team.name, channelName);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// * Verify the standalone original post is redacted
|
||||
const originalPost = await channelsPage.centerView.getPostById(originalPostId);
|
||||
await originalPost.toHaveFilesRedacted();
|
||||
|
||||
// * Verify the embedded preview is redacted too, and not merely file-less
|
||||
const permalinkPost = await channelsPage.centerView.getPostById(permalinkPostId);
|
||||
await expect(permalinkPost.postPreview).toBeVisible();
|
||||
await permalinkPost.toHaveFilesRedacted(permalinkPost.postPreview);
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @objective Verify a denied user sees the redacted placeholder when the same post is opened
|
||||
* in a thread in the right-hand sidebar, which renders through its own post list.
|
||||
*/
|
||||
test(
|
||||
'redacts the attachment when the post is opened in a thread',
|
||||
{tag: '@abac_file_permissions'},
|
||||
async ({pw}) => {
|
||||
test.setTimeout(pw.duration.four_min);
|
||||
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
savedAdminClient = adminClient;
|
||||
const {testUser: deniedUser, channelName} = await setupUserAndChannel(adminClient, team);
|
||||
|
||||
// # Admin posts a file
|
||||
const {channelsPage: adminChannelsPage} = await pw.testBrowser.login(adminUser);
|
||||
await adminChannelsPage.goto(team.name, channelName);
|
||||
await adminChannelsPage.toBeVisible();
|
||||
await adminChannelsPage.centerView.postCreate.postMessage(THREAD_ROOT_MESSAGE, ['sample_text_file.txt']);
|
||||
|
||||
// # Enable ABAC and deny download for everyone
|
||||
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
|
||||
await enableABAC(systemConsolePage.page);
|
||||
await navigateToPermissionPoliciesPage(systemConsolePage.page);
|
||||
|
||||
lastPolicyName = `Thread Download Deny ${getRandomId()}`;
|
||||
await createPermissionPolicy(systemConsolePage.page, {
|
||||
name: lastPolicyName,
|
||||
celExpression: DENY_ALL_CEL,
|
||||
permissions: ['Download Files'],
|
||||
adminClient,
|
||||
});
|
||||
await waitForPolicy(adminClient, lastPolicyName);
|
||||
await ensureABACEnabled(adminClient);
|
||||
|
||||
// # The denied user opens the post in a thread
|
||||
const {channelsPage} = await pw.testBrowser.login(deniedUser);
|
||||
await channelsPage.goto(team.name, channelName);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const centerPost = await channelsPage.centerView.getPostByText(THREAD_ROOT_MESSAGE);
|
||||
await centerPost.toHaveFilesRedacted();
|
||||
await centerPost.openAThread();
|
||||
await channelsPage.sidebarRight.toBeVisible();
|
||||
|
||||
// * Verify the thread's copy of the post is redacted as well
|
||||
const rhsPost = await channelsPage.sidebarRight.getFirstPost();
|
||||
await rhsPost.toHaveFilesRedacted();
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @objective Verify a denied user sees the redacted placeholder in search results, which
|
||||
* render posts through the search panel rather than a channel post list.
|
||||
*/
|
||||
test('redacts the attachment in search results', {tag: '@abac_file_permissions'}, async ({pw}) => {
|
||||
test.setTimeout(pw.duration.four_min);
|
||||
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
savedAdminClient = adminClient;
|
||||
const {testUser: deniedUser, channelName} = await setupUserAndChannel(adminClient, team);
|
||||
|
||||
// # Admin posts a file with a searchable, unique term
|
||||
const searchTerm = `redactme${getRandomId()}`;
|
||||
const {channelsPage: adminChannelsPage} = await pw.testBrowser.login(adminUser);
|
||||
await adminChannelsPage.goto(team.name, channelName);
|
||||
await adminChannelsPage.toBeVisible();
|
||||
await adminChannelsPage.centerView.postCreate.postMessage(searchTerm, ['sample_text_file.txt']);
|
||||
|
||||
// # Enable ABAC and deny download for everyone
|
||||
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
|
||||
await enableABAC(systemConsolePage.page);
|
||||
await navigateToPermissionPoliciesPage(systemConsolePage.page);
|
||||
|
||||
lastPolicyName = `Search Download Deny ${getRandomId()}`;
|
||||
await createPermissionPolicy(systemConsolePage.page, {
|
||||
name: lastPolicyName,
|
||||
celExpression: DENY_ALL_CEL,
|
||||
permissions: ['Download Files'],
|
||||
adminClient,
|
||||
});
|
||||
await waitForPolicy(adminClient, lastPolicyName);
|
||||
await ensureABACEnabled(adminClient);
|
||||
|
||||
// # The denied user searches for the post
|
||||
const {channelsPage} = await pw.testBrowser.login(deniedUser);
|
||||
await channelsPage.goto(team.name, channelName);
|
||||
await channelsPage.toBeVisible();
|
||||
await channelsPage.searchFor(searchTerm);
|
||||
|
||||
// * Verify the search result shows the redacted placeholder, not the file
|
||||
const result = channelsPage.searchResultsPanel.getResultByText(searchTerm).first();
|
||||
await expect(result).toBeVisible();
|
||||
await expectFilesRedacted(result);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,293 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {Client4} from '@mattermost/client';
|
||||
|
||||
import {expect, test, enableABAC, getAdminClient, getRandomId, TestBrowser} from '@mattermost/playwright-lib';
|
||||
|
||||
import type {CustomProfileAttribute} from '../../../channels/custom_profile_attributes/helpers';
|
||||
import {
|
||||
setupCustomProfileAttributeFields,
|
||||
setupCustomProfileAttributeValuesForUser,
|
||||
} from '../../../channels/custom_profile_attributes/helpers';
|
||||
import {
|
||||
createPermissionPolicy,
|
||||
createPrivateChannelForABAC,
|
||||
createUserForABAC,
|
||||
deletePermissionPolicyByName,
|
||||
enableUserManagedAttributes,
|
||||
navigateToPermissionPoliciesPage,
|
||||
updatePermissionPolicyExpression,
|
||||
} from '../support';
|
||||
|
||||
import {ensureABACEnabled, waitForPolicy} from './helpers';
|
||||
|
||||
// Each test adds a member, and each addition appends a join system message after the fixture
|
||||
// post, so the post under test is never the last one.
|
||||
const FIXTURE_MESSAGE = 'File for live tests';
|
||||
|
||||
const GRANTING_EXPRESSION = 'user.attributes.Department == "Engineering"';
|
||||
const REVOKING_EXPRESSION = 'user.attributes.Department == "Finance"';
|
||||
|
||||
// Access changes reaching an already-open page. Two triggers, two websocket events: an
|
||||
// attribute change sends custom_profile_attributes_values_updated, a policy edit sends
|
||||
// permission_policy_updated. Each test creates its own user so a revoke cannot leak.
|
||||
test.describe('ABAC file permissions - live updates', () => {
|
||||
let sharedAdminClient: Client4;
|
||||
let policyName = '';
|
||||
let team: any;
|
||||
let channelName = '';
|
||||
let channelId = '';
|
||||
let attributeFieldsMap: Record<string, any>;
|
||||
let licensed = true;
|
||||
let sharedBrowser: TestBrowser | null = null;
|
||||
|
||||
test.beforeAll(async ({browser}) => {
|
||||
test.setTimeout(240000);
|
||||
|
||||
const {adminClient, adminUser} = await getAdminClient();
|
||||
if (!adminUser) {
|
||||
throw new Error('Admin user not found — cannot proceed with ABAC file-access tests');
|
||||
}
|
||||
sharedAdminClient = adminClient;
|
||||
|
||||
try {
|
||||
const lic = await adminClient.getClientLicenseOld();
|
||||
if (!lic || lic.IsLicensed !== 'true') {
|
||||
licensed = false;
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
licensed = false;
|
||||
return;
|
||||
}
|
||||
|
||||
await enableUserManagedAttributes(adminClient);
|
||||
const departmentAttr: CustomProfileAttribute[] = [{name: 'Department', type: 'text', value: ''}];
|
||||
attributeFieldsMap = await setupCustomProfileAttributeFields(adminClient, departmentAttr);
|
||||
|
||||
const suffix = getRandomId();
|
||||
team = await adminClient.createTeam({
|
||||
name: `abac-live-${suffix}`,
|
||||
display_name: `ABAC Live ${suffix}`,
|
||||
type: 'O',
|
||||
} as any);
|
||||
|
||||
const channel = await createPrivateChannelForABAC(adminClient, team.id);
|
||||
channelName = channel.name;
|
||||
channelId = channel.id;
|
||||
|
||||
sharedBrowser = new TestBrowser(browser);
|
||||
|
||||
// ABAC off while the admin posts the fixture file, so a policy left behind by an
|
||||
// earlier spec cannot block the upload this whole describe depends on.
|
||||
await adminClient.patchConfig({
|
||||
AccessControlSettings: {EnableAttributeBasedAccessControl: false},
|
||||
} as any);
|
||||
|
||||
const {channelsPage: adminChannelsPage} = await sharedBrowser.login(adminUser);
|
||||
await adminChannelsPage.goto(team.name, channelName);
|
||||
await adminChannelsPage.toBeVisible();
|
||||
await adminChannelsPage.centerView.postCreate.postMessage(FIXTURE_MESSAGE, ['sample_text_file.txt']);
|
||||
|
||||
const {systemConsolePage} = await sharedBrowser.login(adminUser);
|
||||
await enableABAC(systemConsolePage.page);
|
||||
await navigateToPermissionPoliciesPage(systemConsolePage.page);
|
||||
|
||||
policyName = `Dept Live Policy ${getRandomId()}`;
|
||||
await createPermissionPolicy(systemConsolePage.page, {
|
||||
name: policyName,
|
||||
celExpression: GRANTING_EXPRESSION,
|
||||
permissions: ['Upload Files', 'Download Files'],
|
||||
adminClient,
|
||||
});
|
||||
await waitForPolicy(adminClient, policyName);
|
||||
});
|
||||
|
||||
test.beforeEach(async () => {
|
||||
test.skip(!licensed, 'No ABAC license');
|
||||
await ensureABACEnabled(sharedAdminClient);
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
if (policyName && sharedAdminClient) {
|
||||
await deletePermissionPolicyByName(sharedAdminClient, policyName).catch(() => {});
|
||||
}
|
||||
await sharedBrowser?.close().catch(() => {});
|
||||
});
|
||||
|
||||
/**
|
||||
* Create a user who currently satisfies the policy and drop them into the fixture channel.
|
||||
* Each test gets its own so that revoking access is not visible to the others.
|
||||
*/
|
||||
async function createAllowedMember() {
|
||||
const user = await createUserForABAC(sharedAdminClient, attributeFieldsMap, [
|
||||
{name: 'Department', type: 'text', value: 'Engineering'},
|
||||
]);
|
||||
await sharedAdminClient.addToTeam(team.id, user.id);
|
||||
await sharedAdminClient.addToChannel(user.id, channelId);
|
||||
return user;
|
||||
}
|
||||
|
||||
/** Move a user out of the department the policy grants access to. */
|
||||
async function revokeByAttribute(userId: string) {
|
||||
await setupCustomProfileAttributeValuesForUser(
|
||||
sharedAdminClient,
|
||||
[{name: 'Department', type: 'text', value: 'Sales'}],
|
||||
attributeFieldsMap,
|
||||
userId,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @objective Verify the upload control flips from enabled to disabled on an open page when
|
||||
* the user's attributes stop satisfying the policy, without a reload.
|
||||
*
|
||||
* @precondition
|
||||
* A licensed server with a permission policy granting Upload Files to Department == "Engineering"
|
||||
*/
|
||||
test(
|
||||
'disables the upload control on an open page when the user attribute changes',
|
||||
{tag: '@abac_file_permissions'},
|
||||
async ({pw}) => {
|
||||
test.setTimeout(pw.duration.two_min);
|
||||
|
||||
// # A user who currently matches the policy opens the channel
|
||||
const user = await createAllowedMember();
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, channelName);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// * Verify upload starts out available
|
||||
await expect(channelsPage.centerView.postCreate.attachmentButton).toBeEnabled({
|
||||
timeout: pw.duration.half_min,
|
||||
});
|
||||
|
||||
// # Admin moves the user out of the granted department
|
||||
await revokeByAttribute(user.id);
|
||||
|
||||
// * Verify the control becomes unavailable without the page being reloaded
|
||||
await expect(channelsPage.centerView.postCreate.attachmentButton).toBeDisabled({
|
||||
timeout: pw.duration.half_min,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @objective Verify file attachments already rendered on screen are replaced by the redacted
|
||||
* placeholder when the user's attributes stop satisfying the policy, without a reload.
|
||||
*
|
||||
* @precondition
|
||||
* A licensed server with a permission policy granting Download Files to Department == "Engineering"
|
||||
*/
|
||||
test(
|
||||
'redacts files already on screen when the user attribute changes',
|
||||
{tag: '@abac_file_permissions'},
|
||||
async ({pw}) => {
|
||||
test.setTimeout(pw.duration.two_min);
|
||||
|
||||
// # A user who currently matches the policy opens the channel
|
||||
const user = await createAllowedMember();
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, channelName);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// * Verify the file starts out visible
|
||||
const post = await channelsPage.centerView.getPostByText(FIXTURE_MESSAGE);
|
||||
await post.toHaveFilesVisible();
|
||||
|
||||
// # Admin moves the user out of the granted department
|
||||
await revokeByAttribute(user.id);
|
||||
|
||||
// * Verify the already-loaded post is re-rendered as redacted, with no reload
|
||||
await post.toHaveFilesRedacted();
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @objective Verify a revoked user stays revoked across a reload — the post-list ETag epoch
|
||||
* moved, so the refetch cannot be served from a cache populated while access was allowed.
|
||||
*
|
||||
* @precondition
|
||||
* A licensed server with a permission policy granting both file permissions to Department == "Engineering"
|
||||
*/
|
||||
test(
|
||||
'keeps files redacted and upload blocked after a page reload',
|
||||
{tag: '@abac_file_permissions'},
|
||||
async ({pw}) => {
|
||||
test.setTimeout(pw.duration.two_min);
|
||||
|
||||
// # A user who currently matches the policy opens the channel
|
||||
const user = await createAllowedMember();
|
||||
const {channelsPage, page} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, channelName);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// * Verify access starts out granted
|
||||
await expect(channelsPage.centerView.postCreate.attachmentButton).toBeEnabled({
|
||||
timeout: pw.duration.half_min,
|
||||
});
|
||||
const post = await channelsPage.centerView.getPostByText(FIXTURE_MESSAGE);
|
||||
await post.toHaveFilesVisible();
|
||||
|
||||
// # Admin revokes access, then the user reloads
|
||||
await revokeByAttribute(user.id);
|
||||
await page.reload();
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// * Verify the fresh fetch is sanitized rather than served from the stale cache
|
||||
await expect(channelsPage.centerView.postCreate.attachmentButton).toBeDisabled({
|
||||
timeout: pw.duration.half_min,
|
||||
});
|
||||
const reloadedPost = await channelsPage.centerView.getPostByText(FIXTURE_MESSAGE);
|
||||
await reloadedPost.toHaveFilesRedacted();
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @objective Verify editing the policy itself — rather than the user's attributes — reaches
|
||||
* an already-open page and withdraws the upload affordance.
|
||||
*
|
||||
* Distinct path from the tests above: a policy edit is broadcast system-wide as
|
||||
* permission_policy_updated, with no per-user event to key off.
|
||||
*
|
||||
* Revokes by narrowing the rule's expression, not by removing the action. An action that no
|
||||
* rule grants is implicitly allowed, so dropping it from the policy would widen access.
|
||||
*
|
||||
* @precondition
|
||||
* A licensed server with a permission policy granting both file permissions to Department == "Engineering"
|
||||
*/
|
||||
test(
|
||||
'disables the upload control on an open page when the policy stops matching the user',
|
||||
{tag: '@abac_file_permissions'},
|
||||
async ({pw}) => {
|
||||
test.setTimeout(pw.duration.two_min);
|
||||
|
||||
// # A user who matches the policy opens the channel
|
||||
const user = await createAllowedMember();
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, channelName);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// * Verify upload starts out available
|
||||
await expect(channelsPage.centerView.postCreate.attachmentButton).toBeEnabled({
|
||||
timeout: pw.duration.half_min,
|
||||
});
|
||||
|
||||
// # Restore the fixture even if the assertion below fails, so the shared policy does
|
||||
// not leak a revoking expression into the tests that run after this one.
|
||||
try {
|
||||
// # Admin repoints the policy at a department the user is not in, leaving the
|
||||
// user's own attributes untouched
|
||||
await updatePermissionPolicyExpression(sharedAdminClient, policyName, REVOKING_EXPRESSION);
|
||||
|
||||
// * Verify the upload affordance is withdrawn without the page being reloaded
|
||||
await expect(channelsPage.centerView.postCreate.attachmentButton).toBeDisabled({
|
||||
timeout: pw.duration.half_min,
|
||||
});
|
||||
} finally {
|
||||
await updatePermissionPolicyExpression(sharedAdminClient, policyName, GRANTING_EXPRESSION);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,365 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {Client4} from '@mattermost/client';
|
||||
|
||||
import {expect, test, enableABAC, getAdminClient, getRandomId, TestBrowser} from '@mattermost/playwright-lib';
|
||||
|
||||
import type {CustomProfileAttribute} from '../../../channels/custom_profile_attributes/helpers';
|
||||
import {setupCustomProfileAttributeFields} from '../../../channels/custom_profile_attributes/helpers';
|
||||
import {
|
||||
cleanupAllPermissionPolicies,
|
||||
createPermissionPolicy,
|
||||
createPrivateChannelForABAC,
|
||||
createUserForABAC,
|
||||
deletePermissionPolicyByName,
|
||||
enableUserManagedAttributes,
|
||||
navigateToPermissionPoliciesPage,
|
||||
} from '../support';
|
||||
|
||||
import {ensureABACEnabled, setupUserAndChannel, waitForPolicy} from './helpers';
|
||||
|
||||
// Targeted by text, not position: joining a channel appends a system message and later tests
|
||||
// append their own posts, so the fixture post is not the last one.
|
||||
const FIXTURE_MESSAGE = 'File for render tests';
|
||||
const NO_POLICY_MESSAGE = 'File attachment post';
|
||||
|
||||
// Load-time assertions only. Changes to an already-open page live in
|
||||
// file_permissions_live_updates.spec.ts; non-center-channel surfaces in
|
||||
// file_permissions_edge_cases.spec.ts.
|
||||
test.describe('ABAC file permissions - attribute-based policy', () => {
|
||||
let sharedAdminClient: Client4;
|
||||
let policyName = '';
|
||||
let team: any;
|
||||
let channelName = '';
|
||||
let allowedUser: any;
|
||||
let deniedUser: any;
|
||||
let licensed = true;
|
||||
let sharedBrowser: TestBrowser | null = null;
|
||||
|
||||
test.beforeAll(async ({browser}) => {
|
||||
test.setTimeout(240000);
|
||||
|
||||
const {adminClient, adminUser} = await getAdminClient();
|
||||
if (!adminUser) {
|
||||
throw new Error('Admin user not found — cannot proceed with ABAC file-access tests');
|
||||
}
|
||||
sharedAdminClient = adminClient;
|
||||
|
||||
try {
|
||||
const lic = await adminClient.getClientLicenseOld();
|
||||
if (!lic || lic.IsLicensed !== 'true') {
|
||||
licensed = false;
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
licensed = false;
|
||||
return;
|
||||
}
|
||||
|
||||
await enableUserManagedAttributes(adminClient);
|
||||
const departmentAttr: CustomProfileAttribute[] = [{name: 'Department', type: 'text', value: ''}];
|
||||
const attributeFieldsMap = await setupCustomProfileAttributeFields(adminClient, departmentAttr);
|
||||
|
||||
allowedUser = await createUserForABAC(adminClient, attributeFieldsMap, [
|
||||
{name: 'Department', type: 'text', value: 'Engineering'},
|
||||
]);
|
||||
deniedUser = await createUserForABAC(adminClient, attributeFieldsMap, [
|
||||
{name: 'Department', type: 'text', value: 'Sales'},
|
||||
]);
|
||||
|
||||
const suffix = getRandomId();
|
||||
team = await adminClient.createTeam({
|
||||
name: `abac-render-${suffix}`,
|
||||
display_name: `ABAC Render ${suffix}`,
|
||||
type: 'O',
|
||||
} as any);
|
||||
await adminClient.addToTeam(team.id, allowedUser.id);
|
||||
await adminClient.addToTeam(team.id, deniedUser.id);
|
||||
|
||||
const channel = await createPrivateChannelForABAC(adminClient, team.id);
|
||||
channelName = channel.name;
|
||||
await adminClient.addToChannel(allowedUser.id, channel.id);
|
||||
await adminClient.addToChannel(deniedUser.id, channel.id);
|
||||
|
||||
sharedBrowser = new TestBrowser(browser);
|
||||
|
||||
// Turn ABAC off while the admin posts the fixture file, so a policy left behind by an
|
||||
// earlier spec cannot block the upload this whole describe depends on.
|
||||
await adminClient.patchConfig({
|
||||
AccessControlSettings: {EnableAttributeBasedAccessControl: false},
|
||||
} as any);
|
||||
|
||||
const {channelsPage: adminChannelsPage} = await sharedBrowser.login(adminUser);
|
||||
await adminChannelsPage.goto(team.name, channelName);
|
||||
await adminChannelsPage.toBeVisible();
|
||||
await adminChannelsPage.centerView.postCreate.postMessage(FIXTURE_MESSAGE, ['sample_text_file.txt']);
|
||||
|
||||
const {systemConsolePage} = await sharedBrowser.login(adminUser);
|
||||
await enableABAC(systemConsolePage.page);
|
||||
await navigateToPermissionPoliciesPage(systemConsolePage.page);
|
||||
|
||||
policyName = `Dept Render Policy ${getRandomId()}`;
|
||||
await createPermissionPolicy(systemConsolePage.page, {
|
||||
name: policyName,
|
||||
celExpression: 'user.attributes.Department == "Engineering"',
|
||||
permissions: ['Upload Files', 'Download Files'],
|
||||
adminClient,
|
||||
});
|
||||
await waitForPolicy(adminClient, policyName);
|
||||
});
|
||||
|
||||
test.beforeEach(async () => {
|
||||
test.skip(!licensed, 'No ABAC license');
|
||||
await ensureABACEnabled(sharedAdminClient);
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
if (policyName && sharedAdminClient) {
|
||||
await deletePermissionPolicyByName(sharedAdminClient, policyName).catch(() => {});
|
||||
}
|
||||
await sharedBrowser?.close().catch(() => {});
|
||||
});
|
||||
|
||||
/**
|
||||
* @objective Verify a user whose attributes satisfy the policy sees real file attachments.
|
||||
*
|
||||
* @precondition
|
||||
* A licensed server with a permission policy granting Download Files to Department == "Engineering"
|
||||
*/
|
||||
test(
|
||||
'MM-T5826_b shows file attachments to a user whose attributes match the policy',
|
||||
{tag: '@abac_file_permissions'},
|
||||
async ({pw}) => {
|
||||
test.setTimeout(pw.duration.one_min);
|
||||
|
||||
// # Log in as the user whose Department matches the policy
|
||||
const {channelsPage} = await pw.testBrowser.login(allowedUser);
|
||||
await channelsPage.goto(team.name, channelName);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// * Verify the post renders its real attachments, not the redacted placeholder
|
||||
const post = await channelsPage.centerView.getPostByText(FIXTURE_MESSAGE);
|
||||
await post.toHaveFilesVisible();
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @objective Verify a user whose attributes fail the policy sees the redacted placeholder
|
||||
* instead of the file.
|
||||
*
|
||||
* @precondition
|
||||
* A licensed server with a permission policy granting Download Files to Department == "Engineering"
|
||||
*/
|
||||
test(
|
||||
'MM-T5826_a redacts file attachments for a user whose attributes fail the policy',
|
||||
{tag: '@abac_file_permissions'},
|
||||
async ({pw}) => {
|
||||
test.setTimeout(pw.duration.one_min);
|
||||
|
||||
// # Log in as the user whose Department does not match the policy
|
||||
const {channelsPage} = await pw.testBrowser.login(deniedUser);
|
||||
await channelsPage.goto(team.name, channelName);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// * Verify the file is replaced by the redacted placeholder
|
||||
const post = await channelsPage.centerView.getPostByText(FIXTURE_MESSAGE);
|
||||
await post.toHaveFilesRedacted();
|
||||
await expect(post.redactedFilesPlaceholder).toContainText('Files not available');
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @objective Verify the upload affordance is offered to a user whose attributes satisfy the policy.
|
||||
*
|
||||
* @precondition
|
||||
* A licensed server with a permission policy granting Upload Files to Department == "Engineering"
|
||||
*/
|
||||
test(
|
||||
'enables the upload control for a user whose attributes match the policy',
|
||||
{tag: '@abac_file_permissions'},
|
||||
async ({pw}) => {
|
||||
test.setTimeout(pw.duration.one_min);
|
||||
|
||||
// # Log in as the user whose Department matches the policy
|
||||
const {channelsPage} = await pw.testBrowser.login(allowedUser);
|
||||
await channelsPage.goto(team.name, channelName);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// * Verify the attachment control is offered
|
||||
await expect(channelsPage.centerView.postCreate.attachmentButton).toBeEnabled({
|
||||
timeout: pw.duration.half_min,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @objective Verify the upload affordance is disabled rather than hidden for a user whose
|
||||
* attributes fail the policy, so the restriction is visible instead of surfacing as a
|
||||
* server error after the fact.
|
||||
*
|
||||
* @precondition
|
||||
* A licensed server with a permission policy granting Upload Files to Department == "Engineering"
|
||||
*/
|
||||
test(
|
||||
'MM-T5822 disables the upload control for a user whose attributes fail the policy',
|
||||
{tag: '@abac_file_permissions'},
|
||||
async ({pw}) => {
|
||||
test.setTimeout(pw.duration.one_min);
|
||||
|
||||
// # Log in as the user whose Department does not match the policy
|
||||
const {channelsPage} = await pw.testBrowser.login(deniedUser);
|
||||
await channelsPage.goto(team.name, channelName);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// * Verify the attachment control stays visible but is not actionable
|
||||
await expect(channelsPage.centerView.postCreate.attachmentButton).toBeVisible({
|
||||
timeout: pw.duration.half_min,
|
||||
});
|
||||
await expect(channelsPage.centerView.postCreate.attachmentButton).toBeDisabled();
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @objective Verify a single denied user is restricted on both file actions at once — files
|
||||
* redacted and upload blocked in the same view.
|
||||
*
|
||||
* @precondition
|
||||
* A licensed server with a permission policy granting both file permissions to Department == "Engineering"
|
||||
*/
|
||||
test(
|
||||
'MM-T5824 redacts files and blocks upload together for a denied user',
|
||||
{tag: '@abac_file_permissions'},
|
||||
async ({pw}) => {
|
||||
test.setTimeout(pw.duration.one_min);
|
||||
|
||||
// # Log in as the user whose Department does not match the policy
|
||||
const {channelsPage} = await pw.testBrowser.login(deniedUser);
|
||||
await channelsPage.goto(team.name, channelName);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// * Verify existing files are redacted
|
||||
const post = await channelsPage.centerView.getPostByText(FIXTURE_MESSAGE);
|
||||
await post.toHaveFilesRedacted();
|
||||
|
||||
// * Verify new uploads are not offered
|
||||
await expect(channelsPage.centerView.postCreate.attachmentButton).toBeDisabled({
|
||||
timeout: pw.duration.half_min,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @objective Verify a rendered "allowed" is backed by live enforcement — the user the UI
|
||||
* offers upload to can actually complete one, so the affordance and the server agree.
|
||||
*
|
||||
* @precondition
|
||||
* A licensed server with a permission policy granting Upload Files to Department == "Engineering"
|
||||
*/
|
||||
test(
|
||||
'accepts a real upload from a user the render decision allows',
|
||||
{tag: '@abac_file_permissions'},
|
||||
async ({pw}) => {
|
||||
test.setTimeout(pw.duration.two_min);
|
||||
|
||||
// # Log in as the user whose Department matches the policy
|
||||
const {channelsPage} = await pw.testBrowser.login(allowedUser);
|
||||
await channelsPage.goto(team.name, channelName);
|
||||
await channelsPage.toBeVisible();
|
||||
await expect(channelsPage.centerView.postCreate.attachmentButton).toBeEnabled({
|
||||
timeout: pw.duration.half_min,
|
||||
});
|
||||
|
||||
// # Upload a file
|
||||
const message = `Upload from allowed user ${getRandomId()}`;
|
||||
await channelsPage.centerView.postCreate.postMessage(message, ['sample_text_file.txt']);
|
||||
|
||||
// * Verify the post was accepted with its attachment intact
|
||||
const post = await channelsPage.centerView.getPostByText(message);
|
||||
await post.toContainText(message);
|
||||
await post.toHaveFilesVisible();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* With ABAC enabled but no permission policy in existence, every file action is implicitly
|
||||
* allowed. Separate fixture from the describe above because it needs the policy table empty.
|
||||
*/
|
||||
test.describe('ABAC file permissions - implicit allow with no policy', () => {
|
||||
test.beforeEach(async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
});
|
||||
|
||||
/**
|
||||
* @objective Verify existing file attachments render normally when no permission policy
|
||||
* restricts the user.
|
||||
*/
|
||||
test(
|
||||
'MM-T5821 shows file attachments when no permission policy exists',
|
||||
{tag: '@abac_file_permissions'},
|
||||
async ({pw}) => {
|
||||
test.setTimeout(pw.duration.two_min);
|
||||
|
||||
// # Set up a team, a private channel, and a member of it
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
const {testUser, channelName} = await setupUserAndChannel(adminClient, team);
|
||||
await cleanupAllPermissionPolicies(adminClient);
|
||||
|
||||
// # Admin posts a file into the channel
|
||||
const {channelsPage: adminChannelsPage} = await pw.testBrowser.login(adminUser);
|
||||
await adminChannelsPage.goto(team.name, channelName);
|
||||
await adminChannelsPage.toBeVisible();
|
||||
await adminChannelsPage.centerView.postCreate.postMessage(NO_POLICY_MESSAGE, ['sample_text_file.txt']);
|
||||
|
||||
// # Enable ABAC without creating any policy
|
||||
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
|
||||
await enableABAC(systemConsolePage.page);
|
||||
await ensureABACEnabled(adminClient);
|
||||
|
||||
// # The member opens the channel
|
||||
const {channelsPage} = await pw.testBrowser.login(testUser);
|
||||
await channelsPage.goto(team.name, channelName);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// * Verify the file renders rather than being redacted
|
||||
const post = await channelsPage.centerView.getPostByText(NO_POLICY_MESSAGE);
|
||||
await post.toHaveFilesVisible();
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @objective Verify a user can attach and send a file when no permission policy restricts
|
||||
* uploads.
|
||||
*/
|
||||
test(
|
||||
'MM-T5823 accepts a file upload when no permission policy exists',
|
||||
{tag: '@abac_file_permissions'},
|
||||
async ({pw}) => {
|
||||
test.setTimeout(pw.duration.two_min);
|
||||
|
||||
// # Set up a team, a private channel, and a member of it
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
const {testUser, channelName} = await setupUserAndChannel(adminClient, team);
|
||||
await cleanupAllPermissionPolicies(adminClient);
|
||||
|
||||
// # Enable ABAC without creating any policy
|
||||
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
|
||||
await enableABAC(systemConsolePage.page);
|
||||
await ensureABACEnabled(adminClient);
|
||||
|
||||
// # The member uploads a file
|
||||
const {channelsPage} = await pw.testBrowser.login(testUser);
|
||||
await channelsPage.goto(team.name, channelName);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const message = `Upload test ${getRandomId()}`;
|
||||
await channelsPage.centerView.postCreate.postMessage(message, ['sample_text_file.txt']);
|
||||
|
||||
// * Verify the post was accepted with its attachment intact
|
||||
const post = await channelsPage.centerView.getPostByText(message);
|
||||
await post.toContainText(message);
|
||||
await post.toHaveFilesVisible();
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {Client4} from '@mattermost/client';
|
||||
|
||||
import {expect, getRandomId, newTestPassword} from '@mattermost/playwright-lib';
|
||||
|
||||
import {createPrivateChannelForABAC, ensureUserAttributes, getPolicyIdByName} from '../support';
|
||||
|
||||
export async function setupUserAndChannel(
|
||||
adminClient: any,
|
||||
team: any,
|
||||
): Promise<{
|
||||
testUser: any;
|
||||
channelName: string;
|
||||
channelId: string;
|
||||
}> {
|
||||
// Ensure at least one user attribute field exists so the permission policy
|
||||
// CEL editor's "Switch to Advanced Mode" button is enabled in the UI.
|
||||
await ensureUserAttributes(adminClient, ['Department']);
|
||||
|
||||
const username = `user${getRandomId()}`;
|
||||
const password = newTestPassword();
|
||||
const testUser = await adminClient.createUser(
|
||||
{email: `${username}@example.com`, username, password} as any,
|
||||
'',
|
||||
'',
|
||||
);
|
||||
(testUser as any).password = password;
|
||||
|
||||
await adminClient.addToTeam(team.id, testUser.id);
|
||||
|
||||
const channel = await createPrivateChannelForABAC(adminClient, team.id);
|
||||
await adminClient.addToChannel(testUser.id, channel.id);
|
||||
|
||||
return {testUser, channelName: channel.name, channelId: channel.id};
|
||||
}
|
||||
|
||||
/**
|
||||
* Every initSetup() rewrites the shared server config, so an earlier spec can leave ABAC off.
|
||||
* Without this the policy is created but never enforced, and the allow-side tests pass blindly.
|
||||
*/
|
||||
export async function ensureABACEnabled(adminClient: Client4): Promise<void> {
|
||||
await adminClient.patchConfig({
|
||||
AccessControlSettings: {EnableAttributeBasedAccessControl: true},
|
||||
} as any);
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const cfg = await adminClient.getConfig();
|
||||
return cfg.AccessControlSettings?.EnableAttributeBasedAccessControl === true;
|
||||
},
|
||||
{timeout: 15000, intervals: [500, 1000, 2000]},
|
||||
)
|
||||
.toBe(true);
|
||||
}
|
||||
|
||||
/** Block until a just-created policy is readable through the search API. */
|
||||
export async function waitForPolicy(adminClient: Client4, policyName: string): Promise<void> {
|
||||
await expect
|
||||
.poll(async () => Boolean(await getPolicyIdByName(adminClient, policyName, 1)), {
|
||||
timeout: 30000,
|
||||
intervals: [500, 1000, 2000],
|
||||
})
|
||||
.toBe(true);
|
||||
}
|
||||
Reference in New Issue
Block a user