mirror of
https://github.com/hanzoai/team.git
synced 2026-10-30 14:49:35 +00:00
* MM-67592 - be changes for team admin abac channels (#35353) * MM-67592 - be changes for team admin abac channels * Revert team-scoped API routes, keep app layer business logic * move from config to permission; Add cluster-aware LRU cache for policy team scope lookup * remove unnecessary references to config value * local/remote cache invalidation consistency for policy scope * Replace policy scope cache with store-level team scope query * rename functions and add comments to query --------- Co-authored-by: Mattermost Build <[email protected]> * MM 67594 - policies CUD operations to team settings modal channels ABAC (#35590) * MM-67592 - be changes for team admin abac channels * Revert team-scoped API routes, keep app layer business logic * move from config to permission; Add cluster-aware LRU cache for policy team scope lookup * remove unnecessary references to config value * local/remote cache invalidation consistency for policy scope * Replace policy scope cache with store-level team scope query * format files correctly * fix mock expectations for store-query approach in tests * rename functions and add comments to query * revert error ids to original to prevent break tests * adjust translations * MM-67669 - add tab to team settings modal and basic listing * adjust tests and fix linter * use existing search api logic * fix style and adjust flaky test to clean up and restore orinals * address ai corabbit feedback and fix linter * fix unit tests * MM-67592 - be changes for team admin abac channels (#35353) * MM-67592 - be changes for team admin abac channels * fix linter * fix ts linter for playwright * Revert team-scoped API routes, keep app layer business logic * move from config to permission; Add cluster-aware LRU cache for policy team scope lookup * remove unnecessary references to config value * local/remote cache invalidation consistency for policy scope * Replace policy scope cache with store-level team scope query * format files correctly * fix mock expectations for store-query approach in tests * rename functions and add comments to query * revert error ids to original to prevent break tests * adjust translations --------- Co-authored-by: Mattermost Build <[email protected]> * MM-67594 - support cud operations for team abac BE changes * create the team settings policy edit section, reuse most components, add basic e2e * move optional refresh policy list button to list component * temp get team admins cud policies and sync job * enhance validation and adjust e2e * Fix testExpression permission; fix pagination of team policies; add isValidId validation * adjust styles, handling renaming and add permission migrations * update the permissions names, use the simple confirmation modal, define the delete modal * fix policy deletion flow * fix some linter issues and adjust helper tests * remove delete from list and fix e2e * code comments clean up * remove CEL editor for now, clean styles, enhance e2e * fix linter, adjust unit test * fix linter and add missing translation * fix policy deletion ownership and sanitize test expression * fixed e2e tests * rollback orphaned policy on failed channel assignment * enforce channelless check before last_team_id fallback * enforce channelless guard on assign fallback too * add translations missing * add teamId to audit payload when present * fix refresh button pagination reset * fix null safety in channel selector loadChannels * use responsive width cap for team settings modal and adjust header size * remove redundant raw term from channel search URL, add showRefreshButton prop to PolicyList component * handle error when stamping last team ID on channelless policy * replace Props-based ownership with in-memory LRU cache, disable save on zero channels * make e2e tests more reliable in CI * test skip if no license valid found * add childCount guard to cache-hit paths and reduce TTL to 5s * fix e2e, adjust translation * address review feedback: flatten permission checks and separate error types - Flatten nested permission branching in deleteAccessControlPolicy using early returns to reduce indentation (review: isacikgoz) - Validate teamID as input (400) before using it for permission checks (403) in testExpression and validateExpressionAgainstRequester handlers - Remove redundant hasSystemPermission check in searchAccessControlPolicies since system_admin role already includes manage_team_access_rules - Refactor ValidateTeamAdminPolicyOwnership to return (bool, *model.AppError) separating "not owned" from "internal error" across all 8 call sites - Update tests to assert on both return values * add persistent team scope to access control policies, replace in-memory cache * fix translation * fix case-insensitive policy search and sanitize search term input * make policies tests have a unique name * decouple scope/scopeID filter from TeamID in policy store * Fix authZ bypass searchChannelsForAccessControlPolicy by forcing TeamIds to authorized team * show unsaved changes on navigator back, and list all private channels on load * filter already applied channels to a policy * adjust the styles to dark mode; do not show added channels to the policy in the add channels modal * fix linter * MM-67967 add sync status footer to team settings (#35729) * MM-67967 add sync status footer to team settings * remove magic numbers and strings and polish the code * fix linter * fix linter: replace interface{} with any per gofmt rewrite rule * refine getJobsByType team-scoped filtering and permissions * fix sync footer stuck in syncing state on job creation error * fix team-scoped job pagination in getJobsByType * Fix authZ bypass searchChannelsForAccessControlPolicy by forcing TeamIds to authorized team * implement ux feedback, change titles font, fix marging and scroll view jump * MM-68135 - migrate add channels to policy modal to generic modal (#35907) * MM-67920 unify e2e team settings tests (#35867) * MM-67920 - extract duplicated policy editor helpers * remove duplicate team icon test file * rename Access Control to Membership Policies in e2e * replace networkidle with explicit element waits * fix attribute loading issue --------- --------- Co-authored-by: Mattermost Build <[email protected]> * fix playwright feedback issues and persist filters to the store layer in the no systemconsole path * Improve policy scope validation and team admin security checks * Renamed public channels to "AAA Public Channel %03d" and private ones to "ZZZ Private..." so the 55 public channels now fill the 50-result cap * fix e2e tests and add new unit tests to improve coverage * Improve e2e test stability: race condition handling and timeout adjustments * Improve team-scoped ABAC policies: scope preservation, input validation, shared exclusion * Add comprehensive ABAC test coverage: team admin ops and security validation to reduce flakyness * Fix team policy editor back button: preserve navigation intent through Undo * style: format import statements for better readability * Enhance access control policy creation for team admins: enforce scope stamping from query parameters to prevent unauthorized team assignments --------- Co-authored-by: Mattermost Build <[email protected]> Co-authored-by: Hanzo Dev <[email protected]>
221 lines
8.9 KiB
TypeScript
221 lines
8.9 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import type {Locator, Page} from '@playwright/test';
|
|
import {expect} from '@playwright/test';
|
|
import type {Client4} from '@mattermost/client';
|
|
|
|
import {newTestPassword} from '@mattermost/playwright-lib';
|
|
|
|
export async function enableABACConfig(client: Client4) {
|
|
await client.patchConfig({
|
|
AccessControlSettings: {
|
|
EnableAttributeBasedAccessControl: true,
|
|
EnableUserManagedAttributes: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
export async function ensureDepartmentAttribute(client: Client4) {
|
|
let fields: any[] = [];
|
|
try {
|
|
fields = await (client as any).doFetch(`${client.getBaseRoute()}/custom_profile_attributes/fields`, {
|
|
method: 'GET',
|
|
});
|
|
} catch {
|
|
// May not exist yet
|
|
}
|
|
if (!fields.find((f: any) => f.name === 'Department')) {
|
|
try {
|
|
await (client as any).doFetch(`${client.getBaseRoute()}/custom_profile_attributes/fields`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({name: 'Department', type: 'text', attrs: {visibility: 'when_set'}}),
|
|
});
|
|
} catch {
|
|
// Another parallel worker may have created the field between our GET and POST.
|
|
// Re-fetch to confirm it now exists before propagating any error.
|
|
const recheck: any[] = await (client as any).doFetch(
|
|
`${client.getBaseRoute()}/custom_profile_attributes/fields`,
|
|
{method: 'GET'},
|
|
);
|
|
if (!recheck.find((f: any) => f.name === 'Department')) {
|
|
throw new Error('Failed to create Department custom profile attribute field');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function createParentPolicy(client: Client4, name: string) {
|
|
return (client as any).doFetch(`${client.getBaseRoute()}/access_control_policies`, {
|
|
method: 'put',
|
|
body: JSON.stringify({
|
|
id: '',
|
|
name,
|
|
type: 'parent',
|
|
version: 'v0.2',
|
|
revision: 0,
|
|
rules: [{expression: 'true', actions: ['*']}],
|
|
}),
|
|
});
|
|
}
|
|
|
|
export async function assignChannelsToPolicy(client: Client4, policyId: string, channelIds: string[]) {
|
|
const url = `${client.getBaseRoute()}/access_control_policies/${policyId}/assign`;
|
|
const response = await fetch(url, {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json', Authorization: `Bearer ${client.getToken()}`},
|
|
body: JSON.stringify({channel_ids: channelIds}),
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error(`assignChannelsToPolicy failed: ${response.status}`);
|
|
}
|
|
}
|
|
|
|
export async function unassignChannelsFromPolicy(
|
|
client: Client4,
|
|
policyId: string,
|
|
channelIds: string[],
|
|
teamId?: string,
|
|
) {
|
|
const url = `${client.getBaseRoute()}/access_control_policies/${policyId}/unassign`;
|
|
const response = await fetch(url, {
|
|
method: 'DELETE',
|
|
headers: {'Content-Type': 'application/json', Authorization: `Bearer ${client.getToken()}`},
|
|
body: JSON.stringify({channel_ids: channelIds, ...(teamId && {team_id: teamId})}),
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error(`unassignChannelsFromPolicy failed: ${response.status}`);
|
|
}
|
|
}
|
|
|
|
export async function deletePolicy(client: Client4, policyId: string, teamId?: string) {
|
|
const teamParam = teamId ? `?team_id=${encodeURIComponent(teamId)}` : '';
|
|
const url = `${client.getBaseRoute()}/access_control_policies/${policyId}${teamParam}`;
|
|
const response = await fetch(url, {
|
|
method: 'DELETE',
|
|
headers: {'Content-Type': 'application/json', Authorization: `Bearer ${client.getToken()}`},
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error(`deletePolicy failed: ${response.status}`);
|
|
}
|
|
}
|
|
|
|
export async function searchPolicies(client: Client4, teamId: string): Promise<any[]> {
|
|
const result: any = await (client as any).doFetch(`${client.getBaseRoute()}/access_control_policies/search`, {
|
|
method: 'post',
|
|
body: JSON.stringify({
|
|
term: '',
|
|
type: 'parent',
|
|
cursor: {id: ''},
|
|
limit: 100,
|
|
include_children: true,
|
|
team_id: teamId,
|
|
}),
|
|
});
|
|
return result.policies || [];
|
|
}
|
|
|
|
export async function setUserAttribute(adminClient: Client4, userId: string, fieldName: string, value: string) {
|
|
// Get all fields to find the field ID
|
|
const fields: any[] = await (adminClient as any).doFetch(
|
|
`${adminClient.getBaseRoute()}/custom_profile_attributes/fields`,
|
|
{
|
|
method: 'GET',
|
|
},
|
|
);
|
|
const field = fields.find((f: any) => f.name === fieldName);
|
|
if (!field) {
|
|
throw new Error(`Field "${fieldName}" not found`);
|
|
}
|
|
await adminClient.updateUserCustomProfileAttributesValues(userId, {[field.id]: value});
|
|
}
|
|
|
|
export async function createPrivateChannel(client: Client4, teamId: string) {
|
|
const id = Date.now().toString(36) + Math.random().toString(36).substring(2, 7);
|
|
return client.createChannel({team_id: teamId, name: `abac-${id}`, display_name: `ABAC-${id}`, type: 'P'} as any);
|
|
}
|
|
|
|
export async function createGroupConstrainedPrivateChannel(client: Client4, teamId: string) {
|
|
const id = Date.now().toString(36) + Math.random().toString(36).substring(2, 7);
|
|
return client.createChannel({
|
|
team_id: teamId,
|
|
name: `gc-${id}`,
|
|
display_name: `GC-${id}`,
|
|
type: 'P',
|
|
group_constrained: true,
|
|
} as any);
|
|
}
|
|
|
|
export async function createPublicChannel(client: Client4, teamId: string) {
|
|
const id = Date.now().toString(36) + Math.random().toString(36).substring(2, 7);
|
|
return client.createChannel({team_id: teamId, name: `pub-${id}`, display_name: `PUB-${id}`, type: 'O'} as any);
|
|
}
|
|
|
|
export async function createTeamAdmin(adminClient: Client4, teamId: string) {
|
|
const id = Date.now().toString(36) + Math.random().toString(36).substring(2, 7);
|
|
const user = await adminClient.createUser(
|
|
{
|
|
email: `teamadmin-${id}@sample.mattermost.com`,
|
|
username: `teamadmin${id}`,
|
|
password: newTestPassword(),
|
|
} as any,
|
|
'',
|
|
'',
|
|
);
|
|
user.password = newTestPassword();
|
|
|
|
await adminClient.savePreferences(user.id, [
|
|
{user_id: user.id, category: 'tutorial_step', name: user.id, value: '999'},
|
|
{user_id: user.id, category: 'onboarding', name: 'complete', value: 'true'},
|
|
]);
|
|
await adminClient.addToTeam(teamId, user.id);
|
|
await (adminClient as any).doFetch(`${adminClient.getBaseRoute()}/teams/${teamId}/members/${user.id}/roles`, {
|
|
method: 'put',
|
|
body: JSON.stringify({roles: 'team_user team_admin'}),
|
|
});
|
|
|
|
return user;
|
|
}
|
|
|
|
/**
|
|
* Adds an attribute rule row in the policy editor and fills in a value.
|
|
* Handles the auto-opening attribute selector menu.
|
|
*/
|
|
export async function addAttributeRule(container: Locator, page: Page, value: string, attributeName = 'Department') {
|
|
const addAttrBtn = container.getByRole('button', {name: /Add attribute/});
|
|
await expect(addAttrBtn).toBeEnabled({timeout: 10000});
|
|
await addAttrBtn.click();
|
|
|
|
// The attribute selector menu auto-opens — select by name, not by position.
|
|
// Clicking the first item is unreliable when multiple attributes exist (e.g. Location
|
|
// sorts before Department and gets picked instead, causing self-inclusion to fail).
|
|
const attributeMenu = page.locator('[id^="attribute-selector-menu"]');
|
|
await attributeMenu.waitFor({state: 'visible', timeout: 5000});
|
|
await attributeMenu.locator('li').filter({hasText: attributeName}).first().click();
|
|
|
|
// Fill value in the simple input (text-type attribute renders direct input)
|
|
const valueInput = container.locator('.values-editor__simple-input').first();
|
|
await valueInput.waitFor({state: 'visible', timeout: 10000});
|
|
await valueInput.fill(value);
|
|
|
|
// Blur the input so React commits the onChange before the caller proceeds
|
|
await valueInput.press('Tab');
|
|
}
|
|
|
|
/**
|
|
* Adds a channel to the policy via the channel selector modal.
|
|
*/
|
|
export async function addChannelToPolicy(container: Locator, page: Page, channelDisplayName: string) {
|
|
await container.getByRole('button', {name: /Add channels/}).click();
|
|
const channelModal = page.locator('.channel-selector-modal');
|
|
await channelModal.waitFor();
|
|
await expect(channelModal.locator('.more-modal__row').first()).toBeVisible({timeout: 10000});
|
|
await channelModal.locator('.more-modal__row').filter({hasText: channelDisplayName}).click();
|
|
await channelModal.getByRole('button', {name: 'Add'}).click();
|
|
|
|
// Wait for the modal to fully close before returning — callers must not proceed
|
|
// until the channel is committed to form state, otherwise a save click races
|
|
// against the React state update and the confirmation modal never appears.
|
|
await channelModal.waitFor({state: 'hidden', timeout: 20000});
|
|
}
|