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 @@ -365,4 +365,56 @@ test.describe('ABAC Policies - Create Policies', () => {
const user3AfterSync = await verifyUserInChannel(adminClient, nonSatisfyingUserInChannel.id, privateChannel.id);
expect(user3AfterSync).toBe(false); // AUTO-REMOVED
});

/**
* MM-63848: Creating a policy with a name that already exists should show an error
*/
test('MM-63848 Should show error when creating policy with duplicate name', async ({pw}) => {
await pw.skipIfNoLicense();

const {adminUser, adminClient, team} = await pw.initSetup();

await enableUserManagedAttributes(adminClient);

const departmentAttribute: CustomProfileAttribute[] = [{name: 'Department', type: 'text', value: ''}];
await setupCustomProfileAttributeFields(adminClient, departmentAttribute);

const privateChannel = await createPrivateChannelForABAC(adminClient, team.id);

const {systemConsolePage} = await pw.testBrowser.login(adminUser);
const page = systemConsolePage.page;

await navigateToABACPage(page);
await enableABAC(page);

// Create the first policy
const policyName = `Duplicate Test ${await pw.random.id()}`;
await createBasicPolicy(page, {
name: policyName,
attribute: 'Department',
operator: '==',
value: 'Engineering',
autoSync: false,
channels: [privateChannel.display_name],
});

// Navigate back and try to create another policy with the same name
await navigateToABACPage(page);

await createBasicPolicy(page, {
name: policyName,
attribute: 'Department',
operator: '==',
value: 'Sales',
autoSync: false,
channels: [],
});

// Verify error message is shown
const errorMessage = page.locator('.EditPolicy__error');
await expect(errorMessage).toBeVisible({timeout: 5000});

const errorText = await errorMessage.textContent();
expect(errorText).toContain('A policy with this name already exists');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -814,4 +814,92 @@ test.describe('ABAC Policy Management - Edit Policies', () => {
// Step 6: salesRemoteUser should NOT be in channel (never satisfied Dept requirement)
expect(salesRemoteAfterEdit).toBe(false);
});

/**
* MM-63848: Renaming a policy to a name that already exists should show an error
*/
test('MM-63848 Should show error when renaming policy to an existing name', async ({pw}) => {
await pw.skipIfNoLicense();

const {adminUser, adminClient, team} = await pw.initSetup();

await enableUserManagedAttributes(adminClient);

const departmentAttribute: CustomProfileAttribute[] = [{name: 'Department', type: 'text', value: ''}];
await setupCustomProfileAttributeFields(adminClient, departmentAttribute);

const privateChannel = await createPrivateChannelForABAC(adminClient, team.id);

const {systemConsolePage} = await pw.testBrowser.login(adminUser);
const page = systemConsolePage.page;

await navigateToABACPage(page);
await enableABAC(page);

// Create two policies with different names
const policyName1 = `Edit Dup Test A ${await pw.random.id()}`;
await createBasicPolicy(page, {
name: policyName1,
attribute: 'Department',
operator: '==',
value: 'Engineering',
autoSync: false,
channels: [privateChannel.display_name],
});

await navigateToABACPage(page);

const privateChannel2 = await createPrivateChannelForABAC(adminClient, team.id);
const policyName2 = `Edit Dup Test B ${await pw.random.id()}`;
await createBasicPolicy(page, {
name: policyName2,
attribute: 'Department',
operator: '==',
value: 'Sales',
autoSync: false,
channels: [privateChannel2.display_name],
});

// Navigate back and edit policy2's name to match policy1
await navigateToABACPage(page);
await page.waitForTimeout(1000);

// Search for the second policy
const policySearchInput = page.locator('input[placeholder*="Search" i]').first();
if (await policySearchInput.isVisible({timeout: 3000})) {
await policySearchInput.fill(policyName2);
await page.waitForTimeout(1000);
}

const policyRow = page.locator('tr.clickable, .DataGrid_row').filter({hasText: policyName2}).first();
await policyRow.waitFor({state: 'visible', timeout: 10000});
await policyRow.click();
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);

// Change the name to match the first policy
const nameInput = page.locator('#admin\\.access_control\\.policy\\.edit_policy\\.policyName');
await nameInput.waitFor({state: 'visible', timeout: 10000});
await nameInput.fill('');
await nameInput.fill(policyName1);

// Save and expect failure
const saveButton = page.getByRole('button', {name: 'Save'});
await saveButton.click();
await page.waitForTimeout(2000);

// Handle confirmation modal if it appears
const applyPolicyButton = page.getByRole('button', {name: /apply policy/i});
if (await applyPolicyButton.isVisible({timeout: 3000}).catch(() => false)) {
await applyPolicyButton.click();
await page.waitForTimeout(2000);
}

// Verify error message is shown
const errorMessage = page.locator('.EditPolicy__error');
await expect(errorMessage).toBeVisible({timeout: 5000});

const errorText = await errorMessage.textContent();
expect(errorText).toContain('A policy with this name already exists');
});
});
4 changes: 2 additions & 2 deletions server/channels/api4/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (
"strings"
"time"

"github.com/blang/semver/v4"
"github.com/Masterminds/semver/v3"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"

Expand Down Expand Up @@ -2538,7 +2538,7 @@ func handleDeviceProps(c *Context, w http.ResponseWriter, r *http.Request) {

mobileVersion := receivedProps[model.SessionPropMobileVersion]
if mobileVersion != "" {
if _, err := semver.Parse(mobileVersion); err != nil {
if _, err := semver.StrictNewVersion(mobileVersion); err != nil {
c.SetInvalidParam(model.SessionPropMobileVersion)
return
}
Expand Down
8 changes: 4 additions & 4 deletions server/channels/app/agents.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ package app
import (
"net/http"

"github.com/blang/semver/v4"
"github.com/Masterminds/semver/v3"

agentclient "github.com/mattermost/mattermost-plugin-ai/public/bridgeclient"
"github.com/mattermost/mattermost/server/public/model"
Expand Down Expand Up @@ -45,7 +45,7 @@ func (a *App) GetAIPluginBridgeStatus(rctx request.CTX) (bool, string) {
plugins := pluginsEnvironment.Active()
for _, plugin := range plugins {
if plugin.Manifest != nil && plugin.Manifest.Id == aiPluginID {
pluginVersion, err := semver.Parse(plugin.Manifest.Version)
pluginVersion, err := semver.StrictNewVersion(plugin.Manifest.Version)
if err != nil {
rctx.Logger().Debug("AI plugin bridge not available - failed to parse plugin version",
mlog.String("plugin_id", aiPluginID),
Expand All @@ -55,12 +55,12 @@ func (a *App) GetAIPluginBridgeStatus(rctx request.CTX) (bool, string) {
return false, "app.agents.bridge.not_available.plugin_version_parse_failed"
}

minVersion, err := semver.Parse(minAIPluginVersionForBridge)
minVersion, err := semver.StrictNewVersion(minAIPluginVersionForBridge)
if err != nil {
return false, "app.agents.bridge.not_available.min_version_parse_failed"
}

if pluginVersion.LT(minVersion) {
if pluginVersion.LessThan(minVersion) {
rctx.Logger().Debug("AI plugin bridge not available - plugin version is too old",
mlog.String("plugin_id", aiPluginID),
mlog.String("current_version", plugin.Manifest.Version),
Expand Down
31 changes: 31 additions & 0 deletions server/channels/app/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -1604,6 +1604,18 @@ func (a *App) DeleteChannel(rctx request.CTX, channel *model.Channel, userID str
return err
}

var archiveRejectionReason string
pluginContext := pluginContext(rctx)
a.ch.RunMultiHook(func(hooks plugin.Hooks, _ *model.Manifest) bool {
archiveRejectionReason = hooks.ChannelWillBeArchived(pluginContext, channel)
return archiveRejectionReason == ""
}, plugin.ChannelWillBeArchivedID)

if archiveRejectionReason != "" {
return model.NewAppError("DeleteChannel", "app.channel.delete_channel.rejected_by_plugin",
map[string]any{"Reason": archiveRejectionReason}, "", http.StatusBadRequest)
}

if user != nil {
T := i18n.GetUserTranslations(user.Locale)

Expand Down Expand Up @@ -1756,6 +1768,25 @@ func (a *App) addUserToChannel(rctx request.CTX, user *model.User, channel *mode
}
}

var rejectionReason string
pluginContext := pluginContext(rctx)
a.ch.RunMultiHook(func(hooks plugin.Hooks, _ *model.Manifest) bool {
updatedMember, reason := hooks.ChannelMemberWillBeAdded(pluginContext, newMember)
if reason != "" {
rejectionReason = reason
return false
}
if updatedMember != nil {
newMember = updatedMember
}
return true
}, plugin.ChannelMemberWillBeAddedID)

if rejectionReason != "" {
return nil, model.NewAppError("AddUserToChannel", "app.channel.add_user.to.channel.rejected_by_plugin",
map[string]any{"Reason": rejectionReason}, "", http.StatusBadRequest)
}

newMember, nErr = a.Srv().Store().Channel().SaveMember(rctx, newMember)
if nErr != nil {
return nil, model.NewAppError("AddUserToChannel", "api.channel.add_user.to.channel.failed.app_error", nil,
Expand Down
14 changes: 7 additions & 7 deletions server/channels/app/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (
"strings"
"sync"

"github.com/blang/semver/v4"
"github.com/Masterminds/semver/v3"
svg "github.com/h2non/go-is-svg"
"github.com/pkg/errors"

Expand Down Expand Up @@ -687,18 +687,18 @@ func (a *App) mergePrepackagedPlugins(remoteMarketplacePlugins map[string]*model
}

// If available in the marketplace, only overwrite if newer.
prepackagedVersion, err := semver.Parse(prepackaged.Manifest.Version)
prepackagedVersion, err := semver.StrictNewVersion(prepackaged.Manifest.Version)
if err != nil {
return model.NewAppError("mergePrepackagedPlugins", "app.plugin.invalid_version.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}

marketplacePlugin := remoteMarketplacePlugins[prepackaged.Manifest.Id]
marketplaceVersion, err := semver.Parse(marketplacePlugin.Manifest.Version)
marketplaceVersion, err := semver.StrictNewVersion(marketplacePlugin.Manifest.Version)
if err != nil {
return model.NewAppError("mergePrepackagedPlugins", "app.plugin.invalid_version.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}

if prepackagedVersion.GT(marketplaceVersion) {
if prepackagedVersion.GreaterThan(marketplaceVersion) {
remoteMarketplacePlugins[prepackaged.Manifest.Id] = prepackagedMarketplace
}
}
Expand Down Expand Up @@ -1075,22 +1075,22 @@ func (ch *Channels) shouldPersistTransitionallyPrepackagedPlugin(availablePlugin
return true
}

prepackagedVersion, err := semver.Parse(p.Manifest.Version)
prepackagedVersion, err := semver.StrictNewVersion(p.Manifest.Version)
if err != nil {
logger.Error("Should not persist transitionally prepackged plugin: invalid prepackaged version", mlog.Err(err))
return false
}

logger = logger.With(mlog.String("existing_version", existing.Manifest.Version))

existingVersion, err := semver.Parse(existing.Manifest.Version)
existingVersion, err := semver.StrictNewVersion(existing.Manifest.Version)
if err != nil {
// Consider this an old version and replace with the prepackaged version instead.
logger.Warn("Should persist transitionally prepackged plugin: invalid existing version", mlog.Err(err))
return true
}

if prepackagedVersion.GT(existingVersion) {
if prepackagedVersion.GreaterThan(existingVersion) {
logger.Info("Should persist transitionally prepackged plugin: newer version")
return true
}
Expand Down
Loading
Loading