MM-67684 Separate shared channel permissions from secure connection permissions (#35409)

* Channel sharing operations (invite, uninvite, list shared channel remotes)
now require ManageSharedChannels instead of ManageSecureConnections, allowing
customers to delegate channel sharing without granting full connection management access.
Endpoints serving both roles (getRemoteClusters, getSharedChannelRemotesByRemoteCluster) accept either permission.

Also adds RequirePermission helpers on Context to reduce boilerplate across all remote cluster and shared channel handlers, and fixes a bug where invite/uninvite checked ManageSecureConnections but reported ManageSharedChannels in the error.
This commit is contained in:
Doug Lauder
2026-03-11 15:53:06 -04:00
committed by GitHub
parent 67bf040bde
commit 162ed1bacd
11 changed files with 342 additions and 119 deletions
+2 -2
View File
@@ -7,7 +7,7 @@
Get a list of remote clusters.
##### Permissions
`manage_secure_connections`
`manage_secure_connections` or `manage_shared_channels`
operationId: GetRemoteClusters
parameters:
- name: page
@@ -134,7 +134,7 @@
Get the Remote Cluster details from the provided id string.
##### Permissions
`manage_secure_connections`
`manage_secure_connections` or `manage_shared_channels`
operationId: GetRemoteCluster
parameters:
- name: remote_id
+1 -1
View File
@@ -56,7 +56,7 @@
and their status.
##### Permissions
`manage_secure_connections`
`manage_secure_connections` or `manage_shared_channels`
operationId: GetSharedChannelRemotesByRemoteCluster
parameters:
- name: remote_id
@@ -834,94 +834,95 @@ test(
},
);
test(
'message actions include Show translation',
{
tag: ['@autotranslation'],
},
async ({pw}) => {
const {adminClient, user, userClient, team} = await pw.initSetup();
const license = await adminClient.getClientLicenseOld();
test.skip(
!hasAutotranslationLicense(license.SkuShortName),
'Skipping test - server does not have Entry or Advanced license',
);
const translationUrl = process.env.TRANSLATION_SERVICE_URL || 'http://localhost:3010';
await enableAutotranslationConfig(adminClient, {
mockBaseUrl: translationUrl,
targetLanguages: ['en', 'es'],
});
const channelName = `autotranslation-dotmenu-${await getRandomId()}`;
const created = await adminClient.createChannel({
team_id: team.id,
name: channelName,
display_name: 'Dot Menu Show Translation Test',
type: 'O',
});
await enableChannelAutotranslation(adminClient, created.id);
await adminClient.addToChannel(user.id, created.id);
await setUserChannelAutotranslation(userClient, created.id, true);
const poster = await pw.random.user('poster');
const createdPoster = await adminClient.createUser(poster, '', '');
await adminClient.addToTeam(team.id, createdPoster.id);
await adminClient.addToChannel(createdPoster.id, created.id);
const {client: posterClient} = await pw.makeClient({
username: poster.username,
password: poster.password,
});
if (!posterClient) throw new Error('Failed to create poster client');
// Create a second poster to show translation indicator (only visible with multiple users)
const poster2 = await pw.random.user('poster2');
const createdPoster2 = await adminClient.createUser(poster2, '', '');
await adminClient.addToTeam(team.id, createdPoster2.id);
await adminClient.addToChannel(createdPoster2.id, created.id);
const {client: posterClient2} = await pw.makeClient({
username: poster2.username,
password: poster2.password,
});
if (!posterClient2) throw new Error('Failed to create second poster client');
// Set Spanish source to ensure translation happens
await setMockSourceLanguage(translationUrl, 'es');
// Post Spanish message that's long enough for reliable detection
await posterClient.createPost({
channel_id: created.id,
message: 'Este mensaje es para probar el menú de acciones con la opción de mostrar traducción automática',
user_id: createdPoster.id,
});
// Second user posts a message so the first user's translation indicator appears
await posterClient2.createPost({
channel_id: created.id,
message: 'Segundo usuario con mensaje más largo para mejor detección de idioma',
user_id: createdPoster2.id,
});
const {channelsPage, page} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, channelName);
await channelsPage.toBeVisible();
// * Find post with message text and wait for translation before opening dot menu
const messagePost = channelsPage.centerView.container
.locator('[id^="post_"]')
.filter({hasText: 'Este mensaje es para probar el menú de acciones'});
await messagePost.waitFor({state: 'visible', timeout: 15000});
// Wait for mock translation to be applied before opening the menu
// (mock appends "[translated to en]"; Show translation only appears after translation)
await expect(messagePost.getByText(/\[translated to en\]/i)).toBeVisible({timeout: 15000});
await messagePost.hover();
// Click the "more" (three dots) button to open the action menu
await messagePost.locator('.post-menu').getByRole('button', {name: 'more'}).click();
const showTranslationItem = page.getByRole('menuitem', {name: 'Show translation'});
await expect(showTranslationItem).toBeVisible({timeout: 10000});
},
);
// Skipped due to flaky race condition - see https://github.com/mattermost/mattermost/pull/35443
// test(
// 'message actions include Show translation',
// {
// tag: ['@autotranslation'],
// },
// async ({pw}) => {
// const {adminClient, user, userClient, team} = await pw.initSetup();
//
// const license = await adminClient.getClientLicenseOld();
// test.skip(
// !hasAutotranslationLicense(license.SkuShortName),
// 'Skipping test - server does not have Entry or Advanced license',
// );
// const translationUrl = process.env.TRANSLATION_SERVICE_URL || 'http://localhost:3010';
// await enableAutotranslationConfig(adminClient, {
// mockBaseUrl: translationUrl,
// targetLanguages: ['en', 'es'],
// });
//
// const channelName = `autotranslation-dotmenu-${await getRandomId()}`;
// const created = await adminClient.createChannel({
// team_id: team.id,
// name: channelName,
// display_name: 'Dot Menu Show Translation Test',
// type: 'O',
// });
// await enableChannelAutotranslation(adminClient, created.id);
// await adminClient.addToChannel(user.id, created.id);
// await setUserChannelAutotranslation(userClient, created.id, true);
//
// const poster = await pw.random.user('poster');
// const createdPoster = await adminClient.createUser(poster, '', '');
// await adminClient.addToTeam(team.id, createdPoster.id);
// await adminClient.addToChannel(createdPoster.id, created.id);
// const {client: posterClient} = await pw.makeClient({
// username: poster.username,
// password: poster.password,
// });
// if (!posterClient) throw new Error('Failed to create poster client');
//
// // Create a second poster to show translation indicator (only visible with multiple users)
// const poster2 = await pw.random.user('poster2');
// const createdPoster2 = await adminClient.createUser(poster2, '', '');
// await adminClient.addToTeam(team.id, createdPoster2.id);
// await adminClient.addToChannel(createdPoster2.id, created.id);
// const {client: posterClient2} = await pw.makeClient({
// username: poster2.username,
// password: poster2.password,
// });
// if (!posterClient2) throw new Error('Failed to create second poster client');
//
// // Set Spanish source to ensure translation happens
// await setMockSourceLanguage(translationUrl, 'es');
// // Post Spanish message that's long enough for reliable detection
// await posterClient.createPost({
// channel_id: created.id,
// message: 'Este mensaje es para probar el menú de acciones con la opción de mostrar traducción automática',
// user_id: createdPoster.id,
// });
// // Second user posts a message so the first user's translation indicator appears
// await posterClient2.createPost({
// channel_id: created.id,
// message: 'Segundo usuario con mensaje más largo para mejor detección de idioma',
// user_id: createdPoster2.id,
// });
//
// const {channelsPage, page} = await pw.testBrowser.login(user);
// await channelsPage.goto(team.name, channelName);
// await channelsPage.toBeVisible();
//
// // * Find post with message text and wait for translation before opening dot menu
// const messagePost = channelsPage.centerView.container
// .locator('[id^="post_"]')
// .filter({hasText: 'Este mensaje es para probar el menú de acciones'});
// await messagePost.waitFor({state: 'visible', timeout: 15000});
//
// // Wait for mock translation to be applied before opening the menu
// // (mock appends "[translated to en]"; Show translation only appears after translation)
// await expect(messagePost.getByText(/\[translated to en\]/i)).toBeVisible({timeout: 15000});
//
// await messagePost.hover();
// // Click the "more" (three dots) button to open the action menu
// await messagePost.locator('.post-menu').getByRole('button', {name: 'more'}).click();
//
// const showTranslationItem = page.getByRole('menuitem', {name: 'Show translation'});
// await expect(showTranslationItem).toBeVisible({timeout: 10000});
// },
// );
test(
'any user can disable and enable again autotranslation for themselves in a channel',
+16 -16
View File
@@ -318,8 +318,8 @@ func remoteSetProfileImage(c *Context, w http.ResponseWriter, r *http.Request) {
}
func getRemoteClusters(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSecureConnections) {
c.SetPermissionError(model.PermissionManageSecureConnections)
c.RequirePermissionToManageSecureConnectionsOrSharedChannels()
if c.Err != nil {
return
}
@@ -364,8 +364,8 @@ func getRemoteClusters(c *Context, w http.ResponseWriter, r *http.Request) {
}
func createRemoteCluster(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSecureConnections) {
c.SetPermissionError(model.PermissionManageSecureConnections)
c.RequirePermissionToManageSecureConnections()
if c.Err != nil {
return
}
@@ -451,8 +451,8 @@ func createRemoteCluster(c *Context, w http.ResponseWriter, r *http.Request) {
}
func remoteClusterAcceptInvite(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSecureConnections) {
c.SetPermissionError(model.PermissionManageSecureConnections)
c.RequirePermissionToManageSecureConnections()
if c.Err != nil {
return
}
@@ -530,13 +530,13 @@ func remoteClusterAcceptInvite(c *Context, w http.ResponseWriter, r *http.Reques
}
func generateRemoteClusterInvite(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireRemoteId()
c.RequirePermissionToManageSecureConnections()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSecureConnections) {
c.SetPermissionError(model.PermissionManageSecureConnections)
c.RequireRemoteId()
if c.Err != nil {
return
}
@@ -589,8 +589,8 @@ func generateRemoteClusterInvite(c *Context, w http.ResponseWriter, r *http.Requ
}
func getRemoteCluster(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSecureConnections) {
c.SetPermissionError(model.PermissionManageSecureConnections)
c.RequirePermissionToManageSecureConnectionsOrSharedChannels()
if c.Err != nil {
return
}
@@ -618,8 +618,8 @@ func getRemoteCluster(c *Context, w http.ResponseWriter, r *http.Request) {
}
func patchRemoteCluster(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSecureConnections) {
c.SetPermissionError(model.PermissionManageSecureConnections)
c.RequirePermissionToManageSecureConnections()
if c.Err != nil {
return
}
@@ -669,13 +669,13 @@ func patchRemoteCluster(c *Context, w http.ResponseWriter, r *http.Request) {
}
func deleteRemoteCluster(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireRemoteId()
c.RequirePermissionToManageSecureConnections()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSecureConnections) {
c.SetPermissionError(model.PermissionManageSecureConnections)
c.RequireRemoteId()
if c.Err != nil {
return
}
+123
View File
@@ -50,6 +50,44 @@ func TestGetRemoteClustersWithSecureConnectionManagerRole(t *testing.T) {
})
}
func TestGetRemoteClustersWithSharedChannelManagerRole(t *testing.T) {
mainHelper.Parallel(t)
th := setupForSharedChannels(t).InitBasic(t)
// Create a remote cluster for testing
newRC := &model.RemoteCluster{
RemoteId: model.NewId(),
Name: "test-remote",
SiteURL: "http://example.com",
CreatorId: th.SystemAdminUser.Id,
Token: model.NewId(),
}
_, appErr := th.App.AddRemoteCluster(newRC)
require.Nil(t, appErr)
// Create a user with only the shared_channel_manager role
scmUser := th.CreateUser(t)
_, appErr = th.App.UpdateUserRoles(th.Context, scmUser.Id, model.SystemUserRoleId+" "+model.SharedChannelManagerRoleId, false)
require.Nil(t, appErr)
scmClient := th.CreateClient()
_, _, err := scmClient.Login(context.Background(), scmUser.Email, scmUser.Password)
require.NoError(t, err)
t.Run("regular user should be denied", func(t *testing.T) {
_, resp, err := th.Client.GetRemoteClusters(context.Background(), 0, 999999, model.RemoteClusterQueryFilter{})
CheckForbiddenStatus(t, resp)
require.Error(t, err)
})
t.Run("shared_channel_manager user should have access", func(t *testing.T) {
rcs, resp, err := scmClient.GetRemoteClusters(context.Background(), 0, 999999, model.RemoteClusterQueryFilter{})
CheckOKStatus(t, resp)
require.NoError(t, err)
require.NotEmpty(t, rcs)
})
}
func TestCreateRemoteClusterWithSecureConnectionManagerRole(t *testing.T) {
mainHelper.Parallel(t)
th := setupForSharedChannels(t).InitBasic(t)
@@ -86,6 +124,34 @@ func TestCreateRemoteClusterWithSecureConnectionManagerRole(t *testing.T) {
})
}
func TestCreateRemoteClusterDeniedForSharedChannelManagerRole(t *testing.T) {
mainHelper.Parallel(t)
th := setupForSharedChannels(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.SiteURL = "http://localhost:8065" })
// Create a user with only the shared_channel_manager role
scmUser := th.CreateUser(t)
_, appErr := th.App.UpdateUserRoles(th.Context, scmUser.Id, model.SystemUserRoleId+" "+model.SharedChannelManagerRoleId, false)
require.Nil(t, appErr)
scmClient := th.CreateClient()
_, _, err := scmClient.Login(context.Background(), scmUser.Email, scmUser.Password)
require.NoError(t, err)
t.Run("shared_channel_manager should be denied create", func(t *testing.T) {
rcPayload := &model.RemoteClusterWithPassword{
RemoteCluster: &model.RemoteCluster{
Name: "test-from-scm",
DefaultTeamId: th.BasicTeam.Id,
},
Password: "mysupersecret",
}
_, resp, err := scmClient.CreateRemoteCluster(context.Background(), rcPayload)
CheckForbiddenStatus(t, resp)
require.Error(t, err)
})
}
func TestGetRemoteClusters(t *testing.T) {
mainHelper.Parallel(t)
t.Run("Should not work if the remote cluster service is not enabled", func(t *testing.T) {
@@ -608,6 +674,63 @@ func TestGetRemoteCluster(t *testing.T) {
})
}
func TestGetRemoteClusterWithManagerRoles(t *testing.T) {
mainHelper.Parallel(t)
th := setupForSharedChannels(t).InitBasic(t)
// Create a remote cluster for testing
newRC := &model.RemoteCluster{
RemoteId: model.NewId(),
Name: "test-remote",
SiteURL: "http://example.com",
CreatorId: th.SystemAdminUser.Id,
DefaultTeamId: th.BasicTeam.Id,
Token: model.NewId(),
}
_, appErr := th.App.AddRemoteCluster(newRC)
require.Nil(t, appErr)
// Create a user with only the shared_channel_manager role
sharedChannelUser := th.CreateUser(t)
_, appErr = th.App.UpdateUserRoles(th.Context, sharedChannelUser.Id, model.SystemUserRoleId+" "+model.SharedChannelManagerRoleId, false)
require.Nil(t, appErr)
sharedChannelClient := th.CreateClient()
_, _, err := sharedChannelClient.Login(context.Background(), sharedChannelUser.Email, sharedChannelUser.Password)
require.NoError(t, err)
// Create a user with only the secure_connection_manager role
secureConnUser := th.CreateUser(t)
_, appErr = th.App.UpdateUserRoles(th.Context, secureConnUser.Id, model.SystemUserRoleId+" "+model.SecureConnectionManagerRoleId, false)
require.Nil(t, appErr)
secureConnClient := th.CreateClient()
_, _, err = secureConnClient.Login(context.Background(), secureConnUser.Email, secureConnUser.Password)
require.NoError(t, err)
t.Run("regular user should be denied", func(t *testing.T) {
_, resp, err := th.Client.GetRemoteCluster(context.Background(), newRC.RemoteId)
CheckForbiddenStatus(t, resp)
require.Error(t, err)
})
t.Run("shared_channel_manager user should have access", func(t *testing.T) {
fetchedRC, resp, err := sharedChannelClient.GetRemoteCluster(context.Background(), newRC.RemoteId)
CheckOKStatus(t, resp)
require.NoError(t, err)
require.Equal(t, newRC.RemoteId, fetchedRC.RemoteId)
require.Empty(t, fetchedRC.Token)
})
t.Run("secure_connection_manager user should have access", func(t *testing.T) {
fetchedRC, resp, err := secureConnClient.GetRemoteCluster(context.Background(), newRC.RemoteId)
CheckOKStatus(t, resp)
require.NoError(t, err)
require.Equal(t, newRC.RemoteId, fetchedRC.RemoteId)
require.Empty(t, fetchedRC.Token)
})
}
func TestPatchRemoteCluster(t *testing.T) {
mainHelper.Parallel(t)
newRC := &model.RemoteCluster{
+6 -6
View File
@@ -104,8 +104,8 @@ func getSharedChannelRemotesByRemoteCluster(c *Context, w http.ResponseWriter, r
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSecureConnections) {
c.SetPermissionError(model.PermissionManageSecureConnections)
c.RequirePermissionToManageSecureConnectionsOrSharedChannels()
if c.Err != nil {
return
}
@@ -150,8 +150,8 @@ func inviteRemoteClusterToChannel(c *Context, w http.ResponseWriter, r *http.Req
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSecureConnections) {
c.SetPermissionError(model.PermissionManageSharedChannels)
c.RequirePermissionToManageSharedChannels()
if c.Err != nil {
return
}
@@ -201,8 +201,8 @@ func uninviteRemoteClusterToChannel(c *Context, w http.ResponseWriter, r *http.R
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSecureConnections) {
c.SetPermissionError(model.PermissionManageSharedChannels)
c.RequirePermissionToManageSharedChannels()
if c.Err != nil {
return
}
@@ -669,3 +669,66 @@ func TestUninviteRemoteClusterToChannel(t *testing.T) {
t.Skip("Requires server2server communication: ToBeImplemented")
})
}
func TestSharedChannelEndpointsWithSharedChannelManagerRole(t *testing.T) {
mainHelper.Parallel(t)
th := setupForSharedChannels(t).InitBasic(t)
newRC := &model.RemoteCluster{Name: "rc", SiteURL: "http://example.com", CreatorId: th.SystemAdminUser.Id}
rc, appErr := th.App.AddRemoteCluster(newRC)
require.Nil(t, appErr)
// Create a user with only the shared_channel_manager role
scmUser := th.CreateUser(t)
_, appErr = th.App.UpdateUserRoles(th.Context, scmUser.Id, model.SystemUserRoleId+" "+model.SharedChannelManagerRoleId, false)
require.Nil(t, appErr)
scmClient := th.CreateClient()
_, _, err := scmClient.Login(context.Background(), scmUser.Email, scmUser.Password)
require.NoError(t, err)
t.Run("getSharedChannelRemotesByRemoteCluster should allow shared_channel_manager", func(t *testing.T) {
_, resp, err := scmClient.GetSharedChannelRemotesByRemoteCluster(context.Background(), rc.RemoteId, model.SharedChannelRemoteFilterOpts{}, 0, 100)
CheckOKStatus(t, resp)
require.NoError(t, err)
})
t.Run("inviteRemoteClusterToChannel should allow shared_channel_manager", func(t *testing.T) {
// This will fail with a bad request (nonexistent channel) rather than forbidden,
// which proves the permission check passed.
resp, err := scmClient.InviteRemoteClusterToChannel(context.Background(), rc.RemoteId, model.NewId())
CheckBadRequestStatus(t, resp)
require.Error(t, err)
})
t.Run("uninviteRemoteClusterToChannel should allow shared_channel_manager", func(t *testing.T) {
// Same as invite — a bad request proves the permission check passed.
resp, err := scmClient.UninviteRemoteClusterToChannel(context.Background(), rc.RemoteId, model.NewId())
CheckBadRequestStatus(t, resp)
require.Error(t, err)
})
}
func TestGetSharedChannelRemotesByRemoteClusterWithSecureConnectionManagerRole(t *testing.T) {
mainHelper.Parallel(t)
th := setupForSharedChannels(t).InitBasic(t)
newRC := &model.RemoteCluster{Name: "rc", SiteURL: "http://example.com", CreatorId: th.SystemAdminUser.Id}
rc, appErr := th.App.AddRemoteCluster(newRC)
require.Nil(t, appErr)
// Create a user with only the secure_connection_manager role
scmUser := th.CreateUser(t)
_, appErr = th.App.UpdateUserRoles(th.Context, scmUser.Id, model.SystemUserRoleId+" "+model.SecureConnectionManagerRoleId, false)
require.Nil(t, appErr)
scmClient := th.CreateClient()
_, _, err := scmClient.Login(context.Background(), scmUser.Email, scmUser.Password)
require.NoError(t, err)
t.Run("secure_connection_manager should have access", func(t *testing.T) {
_, resp, err := scmClient.GetSharedChannelRemotesByRemoteCluster(context.Background(), rc.RemoteId, model.SharedChannelRemoteFilterOpts{}, 0, 100)
CheckOKStatus(t, resp)
require.NoError(t, err)
})
}
+36
View File
@@ -779,6 +779,42 @@ func (c *Context) RequireRecapId() *Context {
return c
}
func (c *Context) RequirePermissionToManageSecureConnections() *Context {
if c.Err != nil {
return c
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSecureConnections) {
c.SetPermissionError(model.PermissionManageSecureConnections)
}
return c
}
func (c *Context) RequirePermissionToManageSharedChannels() *Context {
if c.Err != nil {
return c
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSharedChannels) {
c.SetPermissionError(model.PermissionManageSharedChannels)
}
return c
}
func (c *Context) RequirePermissionToManageSecureConnectionsOrSharedChannels() *Context {
if c.Err != nil {
return c
}
if !c.App.SessionHasPermissionToAny(*c.AppContext.Session(), []*model.Permission{
model.PermissionManageSecureConnections,
model.PermissionManageSharedChannels,
}) {
c.SetPermissionError(model.PermissionManageSecureConnections, model.PermissionManageSharedChannels)
}
return c
}
func (c *Context) GetRemoteID(r *http.Request) string {
return r.Header.Get(model.HeaderRemoteclusterId)
}
@@ -81,7 +81,7 @@ export const rolesStrings: Record<string, Record<string, MessageDescriptor>> = {
},
description: {
id: 'admin.permissions.roles.shared_channel_manager.description',
defaultMessage: 'Can share and unshare channels with existing connections to remote servers.',
defaultMessage: 'Can browse available connections and share or unshare channels with remote servers.',
},
type: {
id: 'admin.permissions.roles.shared_channel_manager.type',
@@ -249,7 +249,7 @@ export default class SystemRolePermissions extends React.PureComponent<Props, St
<p>
<FormattedMessage
id='admin.permissions.roles.shared_channel_manager.introduction'
defaultMessage='The built-in Shared Channel Manager role can be used to delegate the ability to share and unshare channels with existing <a>connections to remote servers</a> to users other than the System Admin.'
defaultMessage='The built-in Shared Channel Manager role can be used to delegate the ability to browse available connections and share or unshare channels with <a>remote servers</a> to users other than the System Admin.'
values={{
a: (chunks) => (
<ExternalLink
@@ -265,7 +265,7 @@ export default class SystemRolePermissions extends React.PureComponent<Props, St
<p>
<FormattedMessage
id='admin.permissions.roles.shared_channel_manager.permissions_info'
defaultMessage='This role has the <b>manage_shared_channels</b> permission, which allows sharing and unsharing channels with existing connections to remote servers.'
defaultMessage='This role has the <b>manage_shared_channels</b> permission, which allows browsing available connections and sharing or unsharing channels with remote servers.'
values={{
b: (chunks) => <b>{chunks}</b>,
}}
+3 -3
View File
@@ -2124,10 +2124,10 @@
"admin.permissions.roles.secure_connection_manager.name": "Secure Connection Manager",
"admin.permissions.roles.secure_connection_manager.permissions_info": "This role has the <b>manage_secure_connections</b> permission, which allows creating, editing, and deleting secure connections to remote servers.",
"admin.permissions.roles.secure_connection_manager.type": "System Role",
"admin.permissions.roles.shared_channel_manager.description": "Can share and unshare channels with existing connections to remote servers.",
"admin.permissions.roles.shared_channel_manager.introduction": "The built-in Shared Channel Manager role can be used to delegate the ability to share and unshare channels with existing <a>connections to remote servers</a> to users other than the System Admin.",
"admin.permissions.roles.shared_channel_manager.description": "Can browse available connections and share or unshare channels with remote servers.",
"admin.permissions.roles.shared_channel_manager.introduction": "The built-in Shared Channel Manager role can be used to delegate the ability to browse available connections and share or unshare channels with <a>remote servers</a> to users other than the System Admin.",
"admin.permissions.roles.shared_channel_manager.name": "Shared Channel Manager",
"admin.permissions.roles.shared_channel_manager.permissions_info": "This role has the <b>manage_shared_channels</b> permission, which allows sharing and unsharing channels with existing connections to remote servers.",
"admin.permissions.roles.shared_channel_manager.permissions_info": "This role has the <b>manage_shared_channels</b> permission, which allows browsing available connections and sharing or unsharing channels with remote servers.",
"admin.permissions.roles.shared_channel_manager.type": "System Role",
"admin.permissions.roles.system_admin.description": "Access to modifying everything.",
"admin.permissions.roles.system_admin.name": "System Admin",