Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,24 @@ import BaseModal from '@/ui/components/system_console/base_modal';
*/
export class ManageRolesModal extends BaseModal {
readonly saveButton: Locator;
readonly systemAdminRadio: Locator;
readonly delegatedRolesSection: Locator;
readonly delegatedRolesTitle: Locator;

constructor(container: Locator) {
super(container);
this.saveButton = container.getByRole('button', {name: 'Save'});
this.systemAdminRadio = container.locator('input[name="systemadmin"]');
this.delegatedRolesSection = container.locator('.manage-roles-modal__delegated-roles');
this.delegatedRolesTitle = container.getByText('Delegated Administration Roles', {exact: true});
}

/**
* Get the checkbox for a delegated administration role by its display name
* (e.g. "User Manager", "System Manager", "Viewer").
*/
getDelegatedRoleCheckbox(roleDisplayName: string): Locator {
return this.delegatedRolesSection.locator('label').filter({hasText: roleDisplayName}).getByRole('checkbox');
}

async save() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

import type {Client4} from '@mattermost/client';
import type {UserProfile} from '@mattermost/types/users';

import type {SystemConsolePage} from '@mattermost/playwright-lib';
import {expect, test} from '@mattermost/playwright-lib';

/**
* Delegated (granular) administration roles surfaced in the Manage Roles modal.
* Mirrors DELEGATED_ROLE_NAMES in the manage_roles_modal component.
*/
const USER_MANAGER_ROLE = 'system_user_manager';
const USER_MANAGER_LABEL = 'User Manager';
const SYSTEM_MANAGER_ROLE = 'system_manager';
const SYSTEM_MANAGER_LABEL = 'System Manager';

/**
* Open the Manage Roles modal for the given user from System Console > User Management > Users.
*/
async function openManageRolesModal(systemConsolePage: SystemConsolePage, user: UserProfile) {
await systemConsolePage.goto();
await systemConsolePage.sidebar.users.click();
await systemConsolePage.users.toBeVisible();

await systemConsolePage.users.searchUsers(user.email);
const userRow = systemConsolePage.users.usersTable.getRowByIndex(0);
await expect(userRow.container.getByText(user.email)).toBeVisible();

const actionMenu = await userRow.openActionMenu();
await actionMenu.clickManageRoles();

const {manageRolesModal} = systemConsolePage.users;
await manageRolesModal.toBeVisible();
return manageRolesModal;
}

/**
* Skip when the server license does not enable delegated granular administration.
* The feature requires an Enterprise/Enterprise Advanced license with the LDAP Groups
* feature and is not available on the Entry SKU (see utils/license_utils.ts).
*/
async function skipIfNoDelegatedAdminLicense(adminClient: Client4) {
const license = await adminClient.getClientLicenseOld();
test.skip(
license.IsLicensed !== 'true' || license.LDAPGroups !== 'true' || license.SkuShortName === 'entry',
'Skipping test - server not licensed for delegated granular administration',
);
}

/**
* @objective Verify a delegated administration role can be granted from the Manage Roles modal and persists.
*/
test(
'grants a delegated administration role from the Manage Roles modal',
{tag: ['@system_console', '@user_management']},
async ({pw}) => {
const {adminUser, adminClient, user} = await pw.initSetup();
await skipIfNoDelegatedAdminLicense(adminClient);

// # Login as admin and open the Manage Roles modal for a regular user
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
const manageRolesModal = await openManageRolesModal(systemConsolePage, user);

// * Verify the Delegated Administration Roles section is shown for a regular member
await expect(manageRolesModal.delegatedRolesTitle).toBeVisible();

// # Grant the User Manager role and save
const userManagerCheckbox = manageRolesModal.getDelegatedRoleCheckbox(USER_MANAGER_LABEL);
await expect(userManagerCheckbox).not.toBeChecked();
await userManagerCheckbox.check();
await manageRolesModal.save();

// * Verify the role was persisted via the API
const updatedUser = await adminClient.getUser(user.id);
expect(updatedUser.roles).toContain(USER_MANAGER_ROLE);
},
);

/**
* @objective Verify the modal pre-selects delegated administration roles the user already has.
*/
test(
'pre-selects delegated administration roles the user already has',
{tag: ['@system_console', '@user_management']},
async ({pw}) => {
const {adminUser, adminClient, user} = await pw.initSetup();
await skipIfNoDelegatedAdminLicense(adminClient);

// # Grant the System Manager role to the user up front
await adminClient.updateUserRoles(user.id, `system_user ${SYSTEM_MANAGER_ROLE}`);

// # Login as admin and open the Manage Roles modal for that user
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
const manageRolesModal = await openManageRolesModal(systemConsolePage, user);

// * Verify the already-granted role is pre-checked and others are not
await expect(manageRolesModal.getDelegatedRoleCheckbox(SYSTEM_MANAGER_LABEL)).toBeChecked();
await expect(manageRolesModal.getDelegatedRoleCheckbox(USER_MANAGER_LABEL)).not.toBeChecked();
},
);

/**
* @objective Verify the Delegated Administration Roles section is hidden when System Admin is selected.
*/
test(
'hides the delegated administration roles section when System Admin is selected',
{tag: ['@system_console', '@user_management']},
async ({pw}) => {
const {adminUser, adminClient, user} = await pw.initSetup();
await skipIfNoDelegatedAdminLicense(adminClient);

// # Login as admin and open the Manage Roles modal for a regular user
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
const manageRolesModal = await openManageRolesModal(systemConsolePage, user);

// * Verify the section is initially visible for a member
await expect(manageRolesModal.delegatedRolesTitle).toBeVisible();

// # Promote the account to System Admin within the modal
await manageRolesModal.systemAdminRadio.check();

// * Verify the delegated roles section is hidden since System Admins already have full access
await expect(manageRolesModal.delegatedRolesTitle).not.toBeVisible();
},
);
16 changes: 0 additions & 16 deletions server/public/model/slack_compatibility.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,6 @@ import (
"strings"
)

// Deprecated: Use MessageAttachment instead.
type SlackAttachment = MessageAttachment

// Deprecated: Use MessageAttachmentField instead.
type SlackAttachmentField = MessageAttachmentField

// Deprecated: Use ParseMessageAttachment instead.
func ParseSlackAttachment(post *Post, attachments []*MessageAttachment) {
ParseMessageAttachment(post, attachments)
}

// Deprecated: Use StringifyMessageAttachmentFieldValue instead.
func StringifySlackFieldValue(a []*MessageAttachment) []*MessageAttachment {
return StringifyMessageAttachmentFieldValue(a)
}

// SlackCompatibleBool is an alias for bool that implements json.Unmarshaler
type SlackCompatibleBool bool

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,162 @@ describe('custom plugin sections and settings', () => {
expect(screen.getByText('In order to view this setting, enable the plugin and click Save.')).toBeInTheDocument();
});

it('mixed custom section fallback with plugin disabled keeps fallback sections visible', () => {
const state = {
...baseState,
entities: {
admin: {
plugins: {
testplugin: {
...plugin,
settings_schema: {
...plugin.settings_schema,
sections: [
{
key: 'section1',
title: 'Fallback Section',
settings: [
{
key: 'fallbacknumbersetting',
label: 'Fallback Number Setting',
type: 'number' as const,
help_text: 'Fallback Number Setting Help Text',
},
],
custom: true,
fallback: true,
},
{
key: 'section2',
title: 'No Fallback Section',
settings: [
{
key: 'nofallbacknumbersetting',
label: 'No Fallback Number Setting',
type: 'number' as const,
help_text: 'No Fallback Number Setting Help Text',
},
],
custom: true,
fallback: false,
},
],
},
},
},
},
},
};

const props = {
...baseProps,
config: {
...baseProps.config,
PluginStates: {
testplugin: {
Enabled: false,
},
},
},
};

renderWithContext(
<CustomPluginSettings
{...props}
patchConfig={jest.fn()}
/>,
{...state});

expectPluginPageTitle('testplugin', 'testplugin');
expect(screen.getByTestId('PluginSettings.PluginStates.testplugin.Enable')).toBeInTheDocument();

// The single collapse warning must not replace the whole page when at least one section allows a fallback.
expect(screen.queryByText('In order to view and configure plugin settings, enable the plugin and click Save.')).not.toBeInTheDocument();

// The fallback-enabled section stays configurable.
expect(screen.getByText('Fallback Section')).toBeInTheDocument();
expect(screen.getByText('Fallback Number Setting Help Text')).toBeInTheDocument();

// The non-fallback section is hidden behind its own per-section warning.
expect(screen.getByText('No Fallback Section')).toBeInTheDocument();
expect(screen.getByText('In order to view this section, enable the plugin and click Save.')).toBeInTheDocument();
expect(screen.queryByText('No Fallback Number Setting Help Text')).not.toBeInTheDocument();
});

it('mixed custom section fallback is order-independent', () => {
const state = {
...baseState,
entities: {
admin: {
plugins: {
testplugin: {
...plugin,
settings_schema: {
...plugin.settings_schema,
sections: [
{
key: 'section1',
title: 'No Fallback Section',
settings: [
{
key: 'nofallbacknumbersetting',
label: 'No Fallback Number Setting',
type: 'number' as const,
help_text: 'No Fallback Number Setting Help Text',
},
],
custom: true,
fallback: false,
},
{
key: 'section2',
title: 'Fallback Section',
settings: [
{
key: 'fallbacknumbersetting',
label: 'Fallback Number Setting',
type: 'number' as const,
help_text: 'Fallback Number Setting Help Text',
},
],
custom: true,
fallback: true,
},
],
},
},
},
},
},
};

const props = {
...baseProps,
config: {
...baseProps.config,
PluginStates: {
testplugin: {
Enabled: false,
},
},
},
};

renderWithContext(
<CustomPluginSettings
{...props}
patchConfig={jest.fn()}
/>,
{...state});

expect(screen.queryByText('In order to view and configure plugin settings, enable the plugin and click Save.')).not.toBeInTheDocument();
expect(screen.getByText('Fallback Section')).toBeInTheDocument();
expect(screen.getByText('Fallback Number Setting Help Text')).toBeInTheDocument();
expect(screen.getByText('No Fallback Section')).toBeInTheDocument();
expect(screen.getByText('In order to view this section, enable the plugin and click Save.')).toBeInTheDocument();
expect(screen.queryByText('No Fallback Number Setting Help Text')).not.toBeInTheDocument();
});

it('custom sections with plugin enabled should render as expected', () => {
const CustomSection1 = () => {
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,10 +137,10 @@ function makeGetPluginSchema() {
const pluginEnableSetting = getEnablePluginSetting(plugin) as AdminDefinitionSetting;

const hasAllCustomSectionsDisabled = plugin.settings_schema?.sections?.every((s) => s.custom && !customSections[s.key.toLowerCase()]);
const allCustomSectionsAllowFallback = plugin.settings_schema?.sections?.every((s) => s.custom && s.fallback);
const anyCustomSectionAllowsFallback = plugin.settings_schema?.sections?.some((s) => s.custom && s.fallback);

if (plugin.settings_schema && hasAllCustomSectionsDisabled && !allCustomSectionsAllowFallback) {
// If the plugin is composed of purely custom sections (e.g. Calls), it's disabled (custom components are not found), and they don't allow a fallback, we show a single warning.
if (plugin.settings_schema && hasAllCustomSectionsDisabled && !anyCustomSectionAllowsFallback) {
// If the plugin is composed of purely custom sections (e.g. Calls), it's disabled (custom components are not found), and none allow a fallback, we show a single warning. When a section allows a fallback we render the sections instead, so fallback-enabled ones stay configurable.
const warningBanner = {
key: 'admin.plugin.customSections.pluginDisabledWarning',
type: Constants.SettingsTypes.TYPE_BANNER,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import type {Dispatch} from 'redux';

import {loadRolesIfNeeded} from 'mattermost-redux/actions/roles';
import {updateUserRoles} from 'mattermost-redux/actions/users';
import {getLicense} from 'mattermost-redux/selectors/entities/general';
import {getRoles} from 'mattermost-redux/selectors/entities/roles_helpers';

import {isLicensedForDelegatedAdministration} from 'utils/license_utils';

import type {GlobalState} from 'types/store';

Expand All @@ -14,13 +19,16 @@ import ManageRolesModal from './manage_roles_modal';
function mapStateToProps(state: GlobalState) {
return {
userAccessTokensEnabled: state.entities.admin.config.ServiceSettings!.EnableUserAccessTokens,
roles: getRoles(state),
isLicensedForDelegatedAdmin: isLicensedForDelegatedAdministration(getLicense(state)),
};
}

function mapDispatchToProps(dispatch: Dispatch) {
return {
actions: bindActionCreators({
updateUserRoles,
loadRolesIfNeeded,
}, dispatch),
};
}
Expand Down
Loading
Loading