Files
team/server/channels/app/authorization_test.go
c9759c392e MM-68952: Resolve public channel mentions for non-members under Compliance (#36815)
* MM-68952: Resolve public channel mentions for non-members under Compliance

Channel mention name resolution reused HasPermissionToReadChannel, a
content-read check that returns false for non-members of a public channel
when Compliance Monitoring is enabled (MM-45272) or when the channel is on
another team (MM-66791). As a result, the channel_mentions post prop was
stripped per-viewer (since #34235), and the webapp fell back to rendering the
raw (anonymized) channel slug instead of a clickable link.

Introduce HasPermissionToResolveChannelMention, which exposes only a public
channel display name and link (not content) and is therefore independent of
ComplianceSettings, while still requiring team membership for public channels
(blocks cross-team disclosure) and channel membership for private/DM/GM
channels. Switch the three mention call sites (FillInPostProps,
sanitizeChannelMentionsForUser, channelMentionsBroadcastHook) to the new
helper. HasPermissionToReadChannel and all content-read paths are unchanged.

* MM-68952: Add author-side and E2E coverage for channel mention resolution

Add a Go test (TestFillInPostPropsChannelMentionResolution) that exercises the
author-side persistence of the channel_mentions prop in FillInPostProps. It
locks in the new behavior: an author who is a team member but not a member of a
referenced public channel now persists the mention prop even when Compliance
Monitoring is enabled, while public channels on other teams and private
channels the author is not in are still dropped.

Add a Playwright spec (channel_mention_resolution.spec.ts) with a license-free
cross-team case (a public channel mention stays unresolved for a viewer not on
the channel's team) and a license-gated case (with Compliance enabled, a team
member who is not in the channel sees the resolved mention link).

* MM-68952: Tighten channel mention test assertions and fix lint

Use strings.Builder when assembling the test message to avoid the
golangci-lint stringsbuilder (modernize) warning about string += string
in a loop. Tighten the cross-team public and private non-member cases to
assert.Nil on the resolved mentions so the contract requires no persisted
channel_mentions map rather than merely an empty one.

---------

Co-authored-by: Hanzo Dev <[email protected]>
2026-06-03 08:42:39 -04:00

2597 lines
99 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"encoding/csv"
"fmt"
"io"
"os"
"strconv"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/plugin/plugintest/mock"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks"
)
func TestSessionHasPermissionTo(t *testing.T) {
th := Setup(t).InitBasic(t)
localSession := model.Session{
UserId: th.BasicUser.Id,
Roles: model.SystemUserRoleId,
Local: true,
}
adminSession := model.Session{
UserId: th.SystemAdminUser.Id,
Roles: model.SystemAdminRoleId,
}
session := model.Session{
UserId: th.BasicUser.Id,
Roles: model.SystemUserRoleId,
}
t.Run("basic user cannot manage system", func(t *testing.T) {
require.False(t, th.App.SessionHasPermissionTo(session, model.PermissionManageSystem))
})
t.Run("basic user generally has no global permissions", func(t *testing.T) {
require.False(t, th.App.SessionHasPermissionTo(session, model.PermissionReadPublicChannel))
})
t.Run("system admin can manage system", func(t *testing.T) {
require.True(t, th.App.SessionHasPermissionTo(adminSession, model.PermissionManageSystem))
})
t.Run("unrestricted session has all permissions", func(t *testing.T) {
require.True(t, th.App.SessionHasPermissionTo(localSession, model.PermissionManageSystem))
require.True(t, th.App.SessionHasPermissionTo(localSession, model.PermissionCreateBot))
require.True(t, th.App.SessionHasPermissionTo(localSession, model.PermissionReadPublicChannel))
})
}
func TestSessionHasPermissionToAndNotRestrictedAdmin(t *testing.T) {
th := Setup(t).InitBasic(t)
localSession := model.Session{
UserId: th.BasicUser.Id,
Roles: model.SystemUserRoleId,
Local: true,
}
adminSession := model.Session{
UserId: th.SystemAdminUser.Id,
Roles: model.SystemAdminRoleId,
}
session := model.Session{
UserId: th.BasicUser.Id,
Roles: model.SystemUserRoleId,
}
t.Run("basic user cannot manage system", func(t *testing.T) {
require.False(t, th.App.SessionHasPermissionToAndNotRestrictedAdmin(session, model.PermissionManageSystem))
})
t.Run("allow system admin when not restricted", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ExperimentalSettings.RestrictSystemAdmin = false
})
require.True(t, th.App.SessionHasPermissionToAndNotRestrictedAdmin(adminSession, model.PermissionManageSystem))
})
t.Run("reject system admin when restricted", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ExperimentalSettings.RestrictSystemAdmin = true
})
require.False(t, th.App.SessionHasPermissionToAndNotRestrictedAdmin(adminSession, model.PermissionManageSystem))
})
t.Run("always allow unrestricted session", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ExperimentalSettings.RestrictSystemAdmin = false
})
require.True(t, th.App.SessionHasPermissionToAndNotRestrictedAdmin(localSession, model.PermissionManageSystem))
require.True(t, th.App.SessionHasPermissionToAndNotRestrictedAdmin(localSession, model.PermissionCreateBot))
require.True(t, th.App.SessionHasPermissionToAndNotRestrictedAdmin(localSession, model.PermissionReadPublicChannel))
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ExperimentalSettings.RestrictSystemAdmin = true
})
require.True(t, th.App.SessionHasPermissionToAndNotRestrictedAdmin(localSession, model.PermissionManageSystem))
require.True(t, th.App.SessionHasPermissionToAndNotRestrictedAdmin(localSession, model.PermissionCreateBot))
require.True(t, th.App.SessionHasPermissionToAndNotRestrictedAdmin(localSession, model.PermissionReadPublicChannel))
})
}
func TestCheckIfRolesGrantPermission(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
cases := []struct {
roles []string
permissionID string
shouldGrant bool
}{
{[]string{model.SystemAdminRoleId}, model.PermissionManageSystem.Id, true},
{[]string{model.SystemAdminRoleId}, "non-existent-permission", false},
{[]string{model.ChannelUserRoleId}, model.PermissionReadChannel.Id, true},
{[]string{model.ChannelUserRoleId}, model.PermissionReadChannelContent.Id, true},
{[]string{model.ChannelUserRoleId}, model.PermissionManageSystem.Id, false},
{[]string{model.SystemAdminRoleId, model.ChannelUserRoleId}, model.PermissionManageSystem.Id, true},
{[]string{model.ChannelUserRoleId, model.SystemAdminRoleId}, model.PermissionManageSystem.Id, true},
{[]string{model.TeamUserRoleId, model.TeamAdminRoleId}, model.PermissionManageOwnSlashCommands.Id, true},
{[]string{model.TeamAdminRoleId, model.TeamUserRoleId}, model.PermissionManageOwnSlashCommands.Id, true},
{[]string{model.ChannelGuestRoleId}, model.PermissionReadChannelContent.Id, true},
}
for _, testcase := range cases {
require.Equal(t, th.App.RolesGrantPermission(testcase.roles, testcase.permissionID), testcase.shouldGrant)
}
}
func TestChannelRolesGrantPermission(t *testing.T) {
mainHelper.Parallel(t)
testPermissionInheritance(t, func(t *testing.T, th *TestHelper, testData permissionInheritanceTestData) {
require.Equal(t, testData.shouldHavePermission, th.App.RolesGrantPermission([]string{testData.channelRole.Name}, testData.permission.Id), "row: %+v\n", testData.truthTableRow)
})
}
func TestHasPermissionToTeam(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
assert.True(t, th.App.HasPermissionToTeam(th.Context, th.BasicUser.Id, th.BasicTeam.Id, model.PermissionListTeamChannels))
th.RemoveUserFromTeam(t, th.BasicUser, th.BasicTeam)
assert.False(t, th.App.HasPermissionToTeam(th.Context, th.BasicUser.Id, th.BasicTeam.Id, model.PermissionListTeamChannels))
assert.True(t, th.App.HasPermissionToTeam(th.Context, th.SystemAdminUser.Id, th.BasicTeam.Id, model.PermissionListTeamChannels))
th.LinkUserToTeam(t, th.SystemAdminUser, th.BasicTeam)
assert.True(t, th.App.HasPermissionToTeam(th.Context, th.SystemAdminUser.Id, th.BasicTeam.Id, model.PermissionListTeamChannels))
th.RemovePermissionFromRole(t, model.PermissionListTeamChannels.Id, model.TeamUserRoleId)
assert.True(t, th.App.HasPermissionToTeam(th.Context, th.SystemAdminUser.Id, th.BasicTeam.Id, model.PermissionListTeamChannels))
th.RemoveUserFromTeam(t, th.SystemAdminUser, th.BasicTeam)
assert.True(t, th.App.HasPermissionToTeam(th.Context, th.SystemAdminUser.Id, th.BasicTeam.Id, model.PermissionListTeamChannels))
}
func TestSessionHasPermissionToTeams(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
// Adding another team with more channels (public and private)
myTeam := th.CreateTeam(t)
bothTeams := []string{th.BasicTeam.Id, myTeam.Id}
t.Run("session with team members can access teams", func(t *testing.T) {
session := model.Session{
UserId: th.BasicUser.Id,
TeamMembers: []*model.TeamMember{
{
UserId: th.BasicUser.Id,
TeamId: th.BasicTeam.Id,
Roles: model.TeamUserRoleId,
},
{
UserId: th.BasicUser.Id,
TeamId: myTeam.Id,
Roles: model.TeamUserRoleId,
},
},
}
assert.True(t, th.App.SessionHasPermissionToTeams(th.Context, session, bothTeams, model.PermissionJoinPublicChannels))
})
t.Run("session with one team members cannot access teams", func(t *testing.T) {
session := model.Session{
UserId: th.BasicUser.Id,
TeamMembers: []*model.TeamMember{
{
UserId: th.BasicUser.Id,
TeamId: th.BasicTeam.Id,
Roles: model.TeamUserRoleId,
},
},
}
assert.False(t, th.App.SessionHasPermissionToTeams(th.Context, session, bothTeams, model.PermissionJoinPublicChannels))
})
t.Run("session role cannot access teams", func(t *testing.T) {
session := model.Session{
UserId: th.BasicUser.Id,
Roles: model.SystemUserRoleId,
}
assert.False(t, th.App.SessionHasPermissionToTeams(th.Context, session, bothTeams, model.PermissionJoinPublicChannels))
})
t.Run("session admin role can access teams", func(t *testing.T) {
session := model.Session{
UserId: th.BasicUser.Id,
Roles: model.SystemAdminRoleId,
}
assert.True(t, th.App.SessionHasPermissionToTeams(th.Context, session, bothTeams, model.PermissionJoinPublicChannels))
})
}
func TestSessionHasPermissionToChannel(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
session := model.Session{
UserId: th.BasicUser.Id,
}
t.Run("basic user can access basic channel", func(t *testing.T) {
ok, isMember := th.App.SessionHasPermissionToChannel(th.Context, session, th.BasicChannel.Id, model.PermissionAddReaction)
assert.True(t, ok)
assert.True(t, isMember)
})
t.Run("basic user can access archived channel", func(t *testing.T) {
err := th.App.DeleteChannel(th.Context, th.BasicChannel, th.SystemAdminUser.Id)
require.Nil(t, err)
ok, isMember := th.App.SessionHasPermissionToChannel(th.Context, session, th.BasicChannel.Id, model.PermissionReadChannel)
assert.True(t, ok)
assert.True(t, isMember)
})
t.Run("admin user can access channel if not a member", func(t *testing.T) {
adminSession := model.Session{
UserId: th.SystemAdminUser.Id,
Roles: model.SystemAdminRoleId,
}
ok, isMember := th.App.SessionHasPermissionToChannel(th.Context, adminSession, th.BasicChannel.Id, model.PermissionAddReaction)
assert.True(t, ok)
assert.False(t, isMember)
})
t.Run("does not panic if fetching channel causes an error", func(t *testing.T) {
// Regression test for MM-29812
// Mock the channel store so getting the channel returns with an error, as per the bug report.
mockStore := mocks.Store{}
// Playbooks DB job requires a plugin mock
pluginStore := mocks.PluginStore{}
pluginStore.On("List", mock.Anything, mock.Anything, mock.Anything).Return([]string{}, nil)
mockStore.On("Plugin").Return(&pluginStore)
mockChannelStore := mocks.ChannelStore{}
mockChannelStore.On("Get", mock.Anything, mock.Anything).Return(nil, fmt.Errorf("arbitrary error"))
mockChannelStore.On("GetAllChannelMembersForUser", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(th.App.Srv().Store().Channel().GetAllChannelMembersForUser(th.Context, th.BasicUser.Id, false, false))
mockChannelStore.On("ClearCaches").Return()
mockStore.On("Channel").Return(&mockChannelStore)
mockStore.On("FileInfo").Return(th.App.Srv().Store().FileInfo())
mockStore.On("License").Return(th.App.Srv().Store().License())
mockStore.On("Post").Return(th.App.Srv().Store().Post())
mockStore.On("Role").Return(th.App.Srv().Store().Role())
mockStore.On("System").Return(th.App.Srv().Store().System())
mockStore.On("Team").Return(th.App.Srv().Store().Team())
mockStore.On("User").Return(th.App.Srv().Store().User())
mockStore.On("Webhook").Return(th.App.Srv().Store().Webhook())
mockStore.On("Close").Return(nil)
th.App.Srv().SetStore(&mockStore)
// If there's an error returned from the GetChannel call the code should continue to cascade and since there
// are no session level permissions in this test case, the permission should be denied.
ok, isMember := th.App.SessionHasPermissionToChannel(th.Context, session, th.BasicUser.Id, model.PermissionAddReaction)
assert.False(t, ok)
assert.False(t, isMember)
})
}
func TestSessionHasPermissionToChannels(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
ch1 := th.CreateChannel(t, th.BasicTeam)
ch2 := th.CreatePrivateChannel(t, th.BasicTeam)
_, appErr := th.App.AddUserToChannel(th.Context, th.BasicUser, ch1, false)
assert.Nil(t, appErr)
_, appErr = th.App.AddUserToChannel(th.Context, th.BasicUser, ch2, false)
assert.Nil(t, appErr)
allChannels := []string{th.BasicChannel.Id, ch1.Id, ch2.Id}
t.Run("basic user can access basic channels", func(t *testing.T) {
session := model.Session{
UserId: th.BasicUser.Id,
}
assert.True(t, th.App.SessionHasPermissionToChannels(th.Context, session, allChannels, model.PermissionReadChannel))
})
t.Run("basic user removed from channel cannot access", func(t *testing.T) {
session := model.Session{
UserId: th.BasicUser.Id,
}
appErr := th.App.removeUserFromChannel(th.Context, th.BasicUser.Id, th.SystemAdminUser.Id, ch1)
assert.Nil(t, appErr)
assert.False(t, th.App.SessionHasPermissionToChannels(th.Context, session, allChannels, model.PermissionReadChannel))
})
t.Run("basic user can access archived channel", func(t *testing.T) {
session := model.Session{
UserId: th.BasicUser.Id,
}
newChannel := th.CreateChannel(t, th.BasicTeam)
_, appErr := th.App.AddUserToChannel(th.Context, th.BasicUser, newChannel, false)
assert.Nil(t, appErr)
err := th.App.DeleteChannel(th.Context, newChannel, th.SystemAdminUser.Id)
require.Nil(t, err)
assert.True(t, th.App.SessionHasPermissionToChannels(th.Context, session, []string{newChannel.Id}, model.PermissionReadChannel))
})
t.Run("basic user can access mixed archived and non-archived channels", func(t *testing.T) {
session := model.Session{
UserId: th.BasicUser.Id,
}
archivedChannel := th.CreateChannel(t, th.BasicTeam)
_, appErr := th.App.AddUserToChannel(th.Context, th.BasicUser, archivedChannel, false)
assert.Nil(t, appErr)
err := th.App.DeleteChannel(th.Context, archivedChannel, th.SystemAdminUser.Id)
require.Nil(t, err)
mixedChannels := []string{th.BasicChannel.Id, archivedChannel.Id}
assert.True(t, th.App.SessionHasPermissionToChannels(th.Context, session, mixedChannels, model.PermissionReadChannel))
})
t.Run("System Admins can access basic channels", func(t *testing.T) {
session := model.Session{
UserId: th.SystemAdminUser.Id,
Roles: model.SystemAdminRoleId,
}
assert.True(t, th.App.SessionHasPermissionToChannels(th.Context, session, allChannels, model.PermissionManagePrivateChannelMembers))
})
t.Run("does not panic if fetching channel causes an error", func(t *testing.T) {
// Regression test for MM-29812
// Mock the channel store so getting the channel returns with an error, as per the bug report.
mockStore := mocks.Store{}
mockChannelStore := mocks.ChannelStore{}
mockChannelStore.On("Get", mock.Anything, mock.Anything).Return(nil, fmt.Errorf("arbitrary error"))
mockChannelStore.On("GetAllChannelMembersForUser", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(th.App.Srv().Store().Channel().GetAllChannelMembersForUser(th.Context, th.BasicUser.Id, false, false))
mockChannelStore.On("ClearCaches").Return()
mockStore.On("Channel").Return(&mockChannelStore)
mockStore.On("FileInfo").Return(th.App.Srv().Store().FileInfo())
mockStore.On("License").Return(th.App.Srv().Store().License())
mockStore.On("Post").Return(th.App.Srv().Store().Post())
mockStore.On("Role").Return(th.App.Srv().Store().Role())
mockStore.On("System").Return(th.App.Srv().Store().System())
mockStore.On("Team").Return(th.App.Srv().Store().Team())
mockStore.On("User").Return(th.App.Srv().Store().User())
mockStore.On("Webhook").Return(th.App.Srv().Store().Webhook())
mockStore.On("Close").Return(nil)
th.App.Srv().SetStore(&mockStore)
// If there's an error returned from the GetChannel call the code should continue to cascade and since there
// are no session level permissions in this test case, the permission should be denied.
session := model.Session{
UserId: th.BasicUser.Id,
}
assert.False(t, th.App.SessionHasPermissionToChannels(th.Context, session, allChannels, model.PermissionReadChannel))
})
}
func TestHasPermissionToUser(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
assert.True(t, th.App.HasPermissionToUser(th.SystemAdminUser.Id, th.BasicUser.Id))
assert.True(t, th.App.HasPermissionToUser(th.BasicUser.Id, th.BasicUser.Id))
assert.False(t, th.App.HasPermissionToUser(th.BasicUser.Id, th.BasicUser2.Id))
}
func TestSessionHasPermissionToManageBot(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
bot, err := th.App.CreateBot(th.Context, &model.Bot{
Username: "username",
Description: "a bot",
OwnerId: th.BasicUser.Id,
})
require.Nil(t, err)
defer func() {
appErr := th.App.PermanentDeleteBot(th.Context, bot.UserId)
assert.Nil(t, appErr)
}()
assert.NotNil(t, bot)
t.Run("test my bot", func(t *testing.T) {
session := model.Session{
UserId: th.BasicUser.Id,
Roles: model.SystemUserRoleId,
}
err = th.App.SessionHasPermissionToManageBot(th.Context, session, bot.UserId)
assert.NotNil(t, err)
assert.Equal(t, "store.sql_bot.get.missing.app_error", err.Id)
assert.NoError(t, err.Unwrap())
th.AddPermissionToRole(t, model.PermissionReadBots.Id, model.SystemUserRoleId)
err = th.App.SessionHasPermissionToManageBot(th.Context, session, bot.UserId)
assert.NotNil(t, err)
assert.Equal(t, "api.context.permissions.app_error", err.Id)
assert.NoError(t, err.Unwrap())
th.AddPermissionToRole(t, model.PermissionManageBots.Id, model.SystemUserRoleId)
err = th.App.SessionHasPermissionToManageBot(th.Context, session, bot.UserId)
assert.Nil(t, err)
th.RemovePermissionFromRole(t, model.PermissionReadBots.Id, model.SystemUserRoleId)
th.RemovePermissionFromRole(t, model.PermissionManageBots.Id, model.SystemUserRoleId)
})
t.Run("test others bot", func(t *testing.T) {
session := model.Session{
UserId: th.BasicUser2.Id,
Roles: model.SystemUserRoleId,
}
err = th.App.SessionHasPermissionToManageBot(th.Context, session, bot.UserId)
assert.NotNil(t, err)
assert.Equal(t, "store.sql_bot.get.missing.app_error", err.Id)
assert.NoError(t, err.Unwrap())
th.AddPermissionToRole(t, model.PermissionReadOthersBots.Id, model.SystemUserRoleId)
err = th.App.SessionHasPermissionToManageBot(th.Context, session, bot.UserId)
assert.NotNil(t, err)
assert.Equal(t, "api.context.permissions.app_error", err.Id)
assert.NoError(t, err.Unwrap())
th.AddPermissionToRole(t, model.PermissionManageOthersBots.Id, model.SystemUserRoleId)
err = th.App.SessionHasPermissionToManageBot(th.Context, session, bot.UserId)
assert.Nil(t, err)
th.RemovePermissionFromRole(t, model.PermissionReadOthersBots.Id, model.SystemUserRoleId)
th.RemovePermissionFromRole(t, model.PermissionManageOthersBots.Id, model.SystemUserRoleId)
})
t.Run("test user manager access", func(t *testing.T) {
session := model.Session{
UserId: th.BasicUser2.Id,
Roles: model.SystemUserManagerRoleId,
}
// test non bot, contains wrapped error
err = th.App.SessionHasPermissionToManageBot(th.Context, session, "12345")
assert.NotNil(t, err)
assert.Equal(t, "store.sql_bot.get.missing.app_error", err.Id)
assert.Error(t, err.Unwrap())
// test existing bot, without PermissionManageOthersBots - no wrapped error
err = th.App.SessionHasPermissionToManageBot(th.Context, session, bot.UserId)
assert.NotNil(t, err)
assert.Equal(t, "store.sql_bot.get.missing.app_error", err.Id)
assert.NoError(t, err.Unwrap())
// test with correct permissions
th.AddPermissionToRole(t, model.PermissionManageOthersBots.Id, model.SystemUserManagerRoleId)
err = th.App.SessionHasPermissionToManageBot(th.Context, session, bot.UserId)
assert.Nil(t, err)
th.RemovePermissionFromRole(t, model.PermissionManageOthersBots.Id, model.SystemUserManagerRoleId)
})
t.Run("test sysadmin role", func(t *testing.T) {
session := model.Session{
UserId: th.SystemAdminUser.Id,
Roles: model.SystemAdminRoleId,
}
err = th.App.SessionHasPermissionToManageBot(th.Context, session, bot.UserId)
assert.Nil(t, err)
})
t.Run("test non bot ", func(t *testing.T) {
session := model.Session{
UserId: th.SystemAdminUser.Id,
Roles: model.SystemUserRoleId,
}
err = th.App.SessionHasPermissionToManageBot(th.Context, session, "12345")
assert.NotNil(t, err)
assert.Equal(t, "store.sql_bot.get.missing.app_error", err.Id)
assert.Error(t, err.Unwrap())
})
}
func TestSessionHasPermissionToUser(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
t.Run("test my user access", func(t *testing.T) {
session := model.Session{
UserId: th.BasicUser.Id,
Roles: model.SystemUserRoleId,
}
assert.True(t, th.App.SessionHasPermissionToUser(session, th.BasicUser.Id))
assert.False(t, th.App.SessionHasPermissionToUser(session, th.BasicUser2.Id))
})
t.Run("test user manager access", func(t *testing.T) {
session := model.Session{
UserId: th.BasicUser.Id,
Roles: model.SystemUserManagerRoleId,
}
assert.False(t, th.App.SessionHasPermissionToUser(session, th.BasicUser2.Id))
th.AddPermissionToRole(t, model.PermissionEditOtherUsers.Id, model.SystemUserManagerRoleId)
assert.True(t, th.App.SessionHasPermissionToUser(session, th.BasicUser2.Id))
assert.False(t, th.App.SessionHasPermissionToUser(session, th.SystemAdminUser.Id))
th.RemovePermissionFromRole(t, model.PermissionEditOtherUsers.Id, model.SystemUserManagerRoleId)
bot, err := th.App.CreateBot(th.Context, &model.Bot{
Username: "username",
Description: "a bot",
OwnerId: th.BasicUser2.Id,
})
require.Nil(t, err)
assert.NotNil(t, bot)
defer func() {
appErr := th.App.PermanentDeleteBot(th.Context, bot.UserId)
assert.Nil(t, appErr)
}()
assert.False(t, th.App.SessionHasPermissionToUser(session, bot.UserId))
})
t.Run("test admin user access", func(t *testing.T) {
session := model.Session{
UserId: th.SystemAdminUser.Id,
Roles: model.SystemAdminRoleId,
}
assert.True(t, th.App.SessionHasPermissionToUser(session, th.BasicUser.Id))
assert.True(t, th.App.SessionHasPermissionToUser(session, th.BasicUser2.Id))
})
}
func TestSessionHasPermissionToManageUserOrBot(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
bot, err := th.App.CreateBot(th.Context, &model.Bot{
Username: "username",
Description: "a bot",
OwnerId: th.BasicUser.Id,
})
require.Nil(t, err)
defer func() {
appErr := th.App.PermanentDeleteBot(th.Context, bot.UserId)
assert.Nil(t, appErr)
}()
t.Run("test basic user access", func(t *testing.T) {
session := model.Session{
UserId: th.BasicUser.Id,
Roles: model.SystemUserRoleId,
}
assert.True(t, th.App.SessionHasPermissionToUserOrBot(th.Context, session, th.BasicUser.Id))
assert.False(t, th.App.SessionHasPermissionToUserOrBot(th.Context, session, bot.UserId))
assert.False(t, th.App.SessionHasPermissionToUserOrBot(th.Context, session, th.BasicUser2.Id))
})
t.Run("test user manager access", func(t *testing.T) {
session := model.Session{
UserId: th.BasicUser2.Id,
Roles: model.SystemUserManagerRoleId,
}
assert.False(t, th.App.SessionHasPermissionToUserOrBot(th.Context, session, th.BasicUser.Id))
assert.True(t, th.App.SessionHasPermissionToUserOrBot(th.Context, session, th.BasicUser2.Id))
assert.False(t, th.App.SessionHasPermissionToUserOrBot(th.Context, session, bot.UserId))
th.AddPermissionToRole(t, model.PermissionEditOtherUsers.Id, model.SystemUserManagerRoleId)
assert.True(t, th.App.SessionHasPermissionToUserOrBot(th.Context, session, th.BasicUser.Id))
assert.False(t, th.App.SessionHasPermissionToUserOrBot(th.Context, session, bot.UserId))
th.RemovePermissionFromRole(t, model.PermissionEditOtherUsers.Id, model.SystemUserManagerRoleId)
th.AddPermissionToRole(t, model.PermissionManageOthersBots.Id, model.SystemUserManagerRoleId)
assert.False(t, th.App.SessionHasPermissionToUserOrBot(th.Context, session, th.BasicUser.Id))
assert.True(t, th.App.SessionHasPermissionToUserOrBot(th.Context, session, bot.UserId))
th.RemovePermissionFromRole(t, model.PermissionManageOthersBots.Id, model.SystemUserManagerRoleId)
})
t.Run("test system admin access", func(t *testing.T) {
session := model.Session{
UserId: th.SystemAdminUser.Id,
Roles: model.SystemAdminRoleId,
}
assert.True(t, th.App.SessionHasPermissionToUserOrBot(th.Context, session, bot.UserId))
assert.True(t, th.App.SessionHasPermissionToUserOrBot(th.Context, session, th.BasicUser.Id))
})
}
func TestHasPermissionToCategory(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
session, err := th.App.CreateSession(th.Context, &model.Session{UserId: th.BasicUser.Id, Props: model.StringMap{}})
require.Nil(t, err)
categories, err := th.App.GetSidebarCategoriesForTeamForUser(th.Context, th.BasicUser.Id, th.BasicTeam.Id)
require.Nil(t, err)
_, err = th.App.GetSession(session.Token)
require.Nil(t, err)
require.True(t, th.App.SessionHasPermissionToCategory(th.Context, *session, th.BasicUser.Id, th.BasicTeam.Id, categories.Order[0]))
categories2, err := th.App.GetSidebarCategoriesForTeamForUser(th.Context, th.BasicUser2.Id, th.BasicTeam.Id)
require.Nil(t, err)
require.False(t, th.App.SessionHasPermissionToCategory(th.Context, *session, th.BasicUser.Id, th.BasicTeam.Id, categories2.Order[0]))
}
func TestSessionHasPermissionToGroup(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
file, e := os.Open("tests/group-role-has-permission.csv")
require.NoError(t, e)
defer file.Close()
b, e := io.ReadAll(file)
require.NoError(t, e)
r := csv.NewReader(strings.NewReader(string(b)))
records, e := r.ReadAll()
require.NoError(t, e)
systemRole, err := th.App.GetRoleByName(th.Context, model.SystemUserRoleId)
require.Nil(t, err)
groupRole, err := th.App.GetRoleByName(th.Context, model.CustomGroupUserRoleId)
require.Nil(t, err)
group, err := th.App.CreateGroup(&model.Group{
Name: new(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceCustom,
AllowReference: true,
})
require.Nil(t, err)
permission := model.PermissionDeleteCustomGroup
for i, row := range records {
// skip csv header
if i == 0 {
continue
}
systemRoleHasPermission, e := strconv.ParseBool(row[0])
require.NoError(t, e)
isGroupMember, e := strconv.ParseBool(row[1])
require.NoError(t, e)
groupRoleHasPermission, e := strconv.ParseBool(row[2])
require.NoError(t, e)
permissionShouldBeGranted, e := strconv.ParseBool(row[3])
require.NoError(t, e)
if systemRoleHasPermission {
th.AddPermissionToRole(t, permission.Id, systemRole.Name)
} else {
th.RemovePermissionFromRole(t, permission.Id, systemRole.Name)
}
if isGroupMember {
_, err := th.App.UpsertGroupMember(group.Id, th.BasicUser.Id)
require.Nil(t, err)
} else {
_, err := th.App.DeleteGroupMember(group.Id, th.BasicUser.Id)
if err != nil && err.Id != "app.group.no_rows" {
t.Error(err)
}
}
if groupRoleHasPermission {
th.AddPermissionToRole(t, permission.Id, groupRole.Name)
} else {
th.RemovePermissionFromRole(t, permission.Id, groupRole.Name)
}
session, err := th.App.CreateSession(th.Context, &model.Session{UserId: th.BasicUser.Id, Props: model.StringMap{}, Roles: systemRole.Name})
require.Nil(t, err)
result := th.App.SessionHasPermissionToGroup(*session, group.Id, permission)
if permissionShouldBeGranted {
require.True(t, result, fmt.Sprintf("row: %v", row))
} else {
require.False(t, result, fmt.Sprintf("row: %v", row))
}
}
}
func TestHasPermissionToReadChannel(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
ttcc := []struct {
name string
configComplianceEnabled bool
channelDeleted bool
canReadChannel bool
channelType model.ChannelType
canReadPublicChannel bool
expected bool
isAdmin bool
}{
{
name: "Can read archived channels",
configComplianceEnabled: true,
channelDeleted: true,
canReadChannel: true,
channelType: model.ChannelTypeOpen,
canReadPublicChannel: true,
expected: true,
},
{
name: "Can read if it has permissions to read",
configComplianceEnabled: true,
channelDeleted: false,
canReadChannel: true,
channelType: model.ChannelTypePrivate,
canReadPublicChannel: true,
expected: true,
},
{
name: "Cannot read private channels if it has no permission",
configComplianceEnabled: false,
channelDeleted: false,
canReadChannel: false,
channelType: model.ChannelTypePrivate,
canReadPublicChannel: true,
expected: false,
},
{
name: "Cannot read open channels if compliance is enabled",
configComplianceEnabled: true,
channelDeleted: false,
canReadChannel: false,
channelType: model.ChannelTypeOpen,
canReadPublicChannel: true,
expected: false,
},
{
name: "Cannot read open channels if it has no team permissions",
configComplianceEnabled: false,
channelDeleted: false,
canReadChannel: false,
channelType: model.ChannelTypeOpen,
canReadPublicChannel: false,
expected: false,
},
{
name: "Can read open channels if it has team permissions and compliance is not enabled",
configComplianceEnabled: false,
channelDeleted: false,
canReadChannel: false,
channelType: model.ChannelTypeOpen,
canReadPublicChannel: true,
expected: true,
},
{
name: "Can read private channels if it is a sysadmin and it is not member of the channel",
configComplianceEnabled: false,
channelDeleted: false,
canReadChannel: false,
channelType: model.ChannelTypePrivate,
canReadPublicChannel: false,
expected: true,
isAdmin: true,
},
{
name: "Can read open board if team permissions and compliance disabled",
configComplianceEnabled: false,
channelDeleted: false,
canReadChannel: false,
channelType: model.ChannelTypeOpenBoard,
canReadPublicChannel: true,
expected: true,
},
{
name: "Cannot read open board if compliance enabled",
configComplianceEnabled: true,
channelDeleted: false,
canReadChannel: false,
channelType: model.ChannelTypeOpenBoard,
canReadPublicChannel: true,
expected: false,
},
{
name: "Cannot read private board if not member",
configComplianceEnabled: false,
channelDeleted: false,
canReadChannel: false,
channelType: model.ChannelTypePrivateBoard,
canReadPublicChannel: false,
expected: false,
},
}
for _, tc := range ttcc {
t.Run(tc.name, func(t *testing.T) {
user := th.BasicUser2
if tc.isAdmin {
user = th.SystemAdminUser
}
th.App.UpdateConfig(func(cfg *model.Config) {
configComplianceEnabled := tc.configComplianceEnabled
cfg.ComplianceSettings.Enable = &configComplianceEnabled
})
team := th.CreateTeam(t)
if tc.canReadPublicChannel {
th.LinkUserToTeam(t, th.BasicUser2, team)
}
var channel *model.Channel
switch tc.channelType {
case model.ChannelTypeOpenBoard, model.ChannelTypePrivateBoard:
kanban := &model.KanbanProps{
GroupBy: model.KanbanGroupBy{
FieldID: model.NewId(),
Columns: []model.KanbanColumn{
{ID: model.NewId(), Name: "Todo", OptionIDs: []string{model.NewId()}},
},
},
}
viewProps, _ := kanban.ToProps()
view := &model.View{CreatorId: th.SystemAdminUser.Id, Type: model.ViewTypeKanban, Title: "Board", Props: viewProps}
ch, _, storeErr := th.App.Srv().Store().Channel().SaveBoardChannel(th.Context, &model.Channel{
TeamId: team.Id,
DisplayName: "Board " + model.NewId(),
Name: "board-" + model.NewId(),
Type: tc.channelType,
CreatorId: th.SystemAdminUser.Id,
}, -1, view)
require.NoError(t, storeErr)
channel = ch
case model.ChannelTypeOpen:
channel = th.CreateChannel(t, team)
default:
channel = th.CreatePrivateChannel(t, team)
}
if tc.canReadChannel {
_, err := th.App.AddUserToChannel(th.Context, user, channel, false)
require.Nil(t, err)
}
if tc.channelDeleted {
err := th.App.DeleteChannel(th.Context, channel, th.SystemAdminUser.Id)
require.Nil(t, err)
channel, err = th.App.GetChannel(th.Context, channel.Id)
require.Nil(t, err)
}
result, isMember := th.App.HasPermissionToReadChannel(th.Context, user.Id, channel)
require.Equal(t, tc.expected, result)
if result {
require.Equal(t, tc.canReadChannel, isMember)
} else {
require.Equal(t, false, isMember)
}
})
}
}
func TestHasPermissionToResolveChannelMention(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
ttcc := []struct {
name string
configComplianceEnabled bool
channelType model.ChannelType
isChannelMember bool
isTeamMember bool
expected bool
}{
{
name: "public, team member, not channel member, compliance ON -> resolves",
configComplianceEnabled: true,
channelType: model.ChannelTypeOpen,
isChannelMember: false,
isTeamMember: true,
expected: true,
},
{
name: "public, team member, not channel member, compliance OFF -> resolves",
configComplianceEnabled: false,
channelType: model.ChannelTypeOpen,
isChannelMember: false,
isTeamMember: true,
expected: true,
},
{
name: "public, NOT team member, compliance ON -> stripped",
configComplianceEnabled: true,
channelType: model.ChannelTypeOpen,
isChannelMember: false,
isTeamMember: false,
expected: false,
},
{
name: "public, NOT team member, compliance OFF -> stripped",
configComplianceEnabled: false,
channelType: model.ChannelTypeOpen,
isChannelMember: false,
isTeamMember: false,
expected: false,
},
{
name: "open board, team member, compliance ON -> resolves",
configComplianceEnabled: true,
channelType: model.ChannelTypeOpenBoard,
isChannelMember: false,
isTeamMember: true,
expected: true,
},
{
name: "private, channel member -> resolves",
configComplianceEnabled: false,
channelType: model.ChannelTypePrivate,
isChannelMember: true,
isTeamMember: true,
expected: true,
},
{
name: "private, team member but not channel member, compliance OFF -> stripped",
configComplianceEnabled: false,
channelType: model.ChannelTypePrivate,
isChannelMember: false,
isTeamMember: true,
expected: false,
},
{
name: "private, not member, compliance ON -> stripped",
configComplianceEnabled: true,
channelType: model.ChannelTypePrivate,
isChannelMember: false,
isTeamMember: false,
expected: false,
},
}
for _, tc := range ttcc {
t.Run(tc.name, func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
enabled := tc.configComplianceEnabled
cfg.ComplianceSettings.Enable = &enabled
})
team := th.CreateTeam(t)
if tc.isTeamMember {
th.LinkUserToTeam(t, th.BasicUser2, team)
}
var channel *model.Channel
switch tc.channelType {
case model.ChannelTypeOpenBoard:
kanban := &model.KanbanProps{
GroupBy: model.KanbanGroupBy{
FieldID: model.NewId(),
Columns: []model.KanbanColumn{
{ID: model.NewId(), Name: "Todo", OptionIDs: []string{model.NewId()}},
},
},
}
viewProps, _ := kanban.ToProps()
view := &model.View{CreatorId: th.SystemAdminUser.Id, Type: model.ViewTypeKanban, Title: "Board", Props: viewProps}
ch, _, storeErr := th.App.Srv().Store().Channel().SaveBoardChannel(th.Context, &model.Channel{
TeamId: team.Id,
DisplayName: "Board " + model.NewId(),
Name: "board-" + model.NewId(),
Type: tc.channelType,
CreatorId: th.SystemAdminUser.Id,
}, -1, view)
require.NoError(t, storeErr)
channel = ch
case model.ChannelTypePrivate:
channel = th.CreatePrivateChannel(t, team)
default:
channel = th.CreateChannel(t, team)
}
if tc.isChannelMember {
_, err := th.App.AddUserToChannel(th.Context, th.BasicUser2, channel, false)
require.Nil(t, err)
}
require.Equal(t, tc.expected, th.App.HasPermissionToResolveChannelMention(th.Context, th.BasicUser2.Id, channel))
})
}
t.Run("DM, non-member -> stripped", func(t *testing.T) {
// DM between BasicUser and SystemAdminUser; BasicUser2 (the viewer) is not a participant.
dm := th.CreateDmChannel(t, th.SystemAdminUser)
require.False(t, th.App.HasPermissionToResolveChannelMention(th.Context, th.BasicUser2.Id, dm))
})
t.Run("GM, non-member -> stripped", func(t *testing.T) {
// GM between BasicUser and two fresh users; BasicUser2 (the viewer) is not a participant.
gm := th.CreateGroupChannel(t, th.CreateUser(t), th.CreateUser(t))
require.False(t, th.App.HasPermissionToResolveChannelMention(th.Context, th.BasicUser2.Id, gm))
})
}
func TestSessionHasPermissionToChannelByPost(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
session, err := th.App.CreateSession(th.Context, &model.Session{
UserId: th.BasicUser.Id,
Roles: model.SystemUserRoleId,
})
require.Nil(t, err)
session2, err := th.App.CreateSession(th.Context, &model.Session{
UserId: th.BasicUser2.Id,
Roles: model.SystemUserRoleId,
})
require.Nil(t, err)
channel := th.CreateChannel(t, th.BasicTeam)
_, appErr := th.App.AddUserToChannel(th.Context, th.BasicUser, channel, false)
assert.Nil(t, appErr)
post := th.CreatePost(t, channel)
archivedChannel := th.CreateChannel(t, th.BasicTeam)
archivedPost := th.CreatePost(t, archivedChannel)
appErr = th.App.DeleteChannel(th.Context, archivedChannel, th.SystemAdminUser.Id)
assert.Nil(t, appErr)
t.Run("read channel", func(t *testing.T) {
require.Equal(t, true, th.App.SessionHasPermissionToChannelByPost(*session, post.Id, model.PermissionReadChannel))
require.Equal(t, false, th.App.SessionHasPermissionToChannelByPost(*session2, post.Id, model.PermissionReadChannel))
})
t.Run("read archived channel", func(t *testing.T) {
require.Equal(t, true, th.App.SessionHasPermissionToChannelByPost(*session, archivedPost.Id, model.PermissionReadChannel))
require.Equal(t, false, th.App.SessionHasPermissionToChannelByPost(*session2, archivedPost.Id, model.PermissionReadChannel))
})
t.Run("read public channel", func(t *testing.T) {
require.Equal(t, true, th.App.SessionHasPermissionToChannelByPost(*session, post.Id, model.PermissionReadPublicChannel))
require.Equal(t, true, th.App.SessionHasPermissionToChannelByPost(*session2, post.Id, model.PermissionReadPublicChannel))
})
t.Run("read channel - user is admin", func(t *testing.T) {
adminSession, err := th.App.CreateSession(th.Context, &model.Session{
UserId: th.SystemAdminUser.Id,
Roles: model.SystemAdminRoleId,
})
require.Nil(t, err)
require.Equal(t, true, th.App.SessionHasPermissionToChannelByPost(*adminSession, post.Id, model.PermissionReadChannel))
})
}
func TestHasPermissionToChannelByPost(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
channel := th.CreateChannel(t, th.BasicTeam)
_, appErr := th.App.AddUserToChannel(th.Context, th.BasicUser, channel, false)
assert.Nil(t, appErr)
post := th.CreatePost(t, channel)
archivedChannel := th.CreateChannel(t, th.BasicTeam)
archivedPost := th.CreatePost(t, archivedChannel)
appErr = th.App.DeleteChannel(th.Context, archivedChannel, th.SystemAdminUser.Id)
assert.Nil(t, appErr)
t.Run("read channel", func(t *testing.T) {
require.Equal(t, true, th.App.HasPermissionToChannelByPost(th.Context, th.BasicUser.Id, post.Id, model.PermissionReadChannel))
require.Equal(t, false, th.App.HasPermissionToChannelByPost(th.Context, th.BasicUser2.Id, post.Id, model.PermissionReadChannel))
})
t.Run("read archived channel", func(t *testing.T) {
require.Equal(t, true, th.App.HasPermissionToChannelByPost(th.Context, th.BasicUser.Id, archivedPost.Id, model.PermissionReadChannel))
require.Equal(t, false, th.App.HasPermissionToChannelByPost(th.Context, th.BasicUser2.Id, archivedPost.Id, model.PermissionReadChannel))
})
t.Run("read public channel", func(t *testing.T) {
require.Equal(t, true, th.App.HasPermissionToChannelByPost(th.Context, th.BasicUser.Id, post.Id, model.PermissionReadPublicChannel))
require.Equal(t, true, th.App.HasPermissionToChannelByPost(th.Context, th.BasicUser2.Id, post.Id, model.PermissionReadPublicChannel))
})
t.Run("read channel - user is admin", func(t *testing.T) {
require.Equal(t, true, th.App.HasPermissionToChannelByPost(th.Context, th.SystemAdminUser.Id, post.Id, model.PermissionReadChannel))
})
}
func TestHasPermissionToChannel(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
channel := th.CreateChannel(t, th.BasicTeam)
_, appErr := th.App.AddUserToChannel(th.Context, th.BasicUser, channel, false)
assert.Nil(t, appErr)
archivedChannel := th.CreateChannel(t, th.BasicTeam)
_, appErr = th.App.AddUserToChannel(th.Context, th.BasicUser, archivedChannel, false)
assert.Nil(t, appErr)
appErr = th.App.DeleteChannel(th.Context, archivedChannel, th.SystemAdminUser.Id)
assert.Nil(t, appErr)
t.Run("read channel", func(t *testing.T) {
ok, isMember := th.App.HasPermissionToChannel(th.Context, th.BasicUser.Id, channel.Id, model.PermissionReadChannel)
assert.True(t, ok)
assert.True(t, isMember)
ok, isMember = th.App.HasPermissionToChannel(th.Context, th.BasicUser2.Id, channel.Id, model.PermissionReadChannel)
assert.False(t, ok)
assert.False(t, isMember)
})
t.Run("read archived channel", func(t *testing.T) {
ok, isMember := th.App.HasPermissionToChannel(th.Context, th.BasicUser.Id, archivedChannel.Id, model.PermissionReadChannel)
assert.True(t, ok)
assert.True(t, isMember)
ok, isMember = th.App.HasPermissionToChannel(th.Context, th.BasicUser2.Id, archivedChannel.Id, model.PermissionReadChannel)
assert.False(t, ok)
assert.False(t, isMember)
})
t.Run("read public channel", func(t *testing.T) {
ok, isMember := th.App.HasPermissionToChannel(th.Context, th.BasicUser.Id, channel.Id, model.PermissionReadPublicChannel)
assert.True(t, ok)
assert.True(t, isMember)
ok, isMember = th.App.HasPermissionToChannel(th.Context, th.BasicUser2.Id, channel.Id, model.PermissionReadPublicChannel)
assert.True(t, ok)
assert.False(t, isMember)
})
t.Run("read channel - user is admin", func(t *testing.T) {
ok, isMember := th.App.HasPermissionToChannel(th.Context, th.SystemAdminUser.Id, channel.Id, model.PermissionReadChannel)
assert.True(t, ok)
assert.False(t, isMember)
})
}
func TestSessionHasPermissionToReadChannel(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
channel := th.CreateChannel(t, th.BasicTeam)
_, appErr := th.App.AddUserToChannel(th.Context, th.BasicUser, channel, false)
assert.Nil(t, appErr)
archivedChannel := th.CreateChannel(t, th.BasicTeam)
_, appErr = th.App.AddUserToChannel(th.Context, th.BasicUser, archivedChannel, false)
assert.Nil(t, appErr)
appErr = th.App.DeleteChannel(th.Context, archivedChannel, th.SystemAdminUser.Id)
assert.Nil(t, appErr)
t.Run("basic user can read channel", func(t *testing.T) {
session := model.Session{
UserId: th.BasicUser.Id,
}
ok, isMember := th.App.SessionHasPermissionToReadChannel(th.Context, session, channel)
assert.True(t, ok)
assert.True(t, isMember)
})
t.Run("basic user cannot read channel if not a member and not public", func(t *testing.T) {
privateChannel := th.CreatePrivateChannel(t, th.BasicTeam)
session := model.Session{
UserId: th.BasicUser2.Id,
}
ok, isMember := th.App.SessionHasPermissionToReadChannel(th.Context, session, privateChannel)
assert.False(t, ok)
assert.False(t, isMember)
})
t.Run("basic user can read archived channel if member", func(t *testing.T) {
session := model.Session{
UserId: th.BasicUser.Id,
}
ok, isMember := th.App.SessionHasPermissionToReadChannel(th.Context, session, archivedChannel)
assert.True(t, ok)
assert.True(t, isMember)
})
t.Run("non-member can read public channel", func(t *testing.T) {
session := model.Session{
UserId: th.BasicUser2.Id,
}
ok, isMember := th.App.SessionHasPermissionToReadChannel(th.Context, session, channel)
assert.True(t, ok)
assert.False(t, isMember)
})
t.Run("admin can read any channel", func(t *testing.T) {
session := model.Session{
UserId: th.SystemAdminUser.Id,
Roles: model.SystemAdminRoleId,
}
ok, isMember := th.App.SessionHasPermissionToReadChannel(th.Context, session, channel)
assert.True(t, ok)
assert.False(t, isMember)
})
}
func TestSessionHasPermissionToReadPost(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
// Create a post in a public channel, ensure basic user can read it.
post, _, err := th.App.CreatePost(th.Context, &model.Post{
UserId: th.BasicUser.Id,
ChannelId: th.BasicChannel.Id,
Message: "hello world",
}, th.BasicChannel, model.CreatePostFlags{})
require.Nil(t, err)
t.Run("basic user can read their post", func(t *testing.T) {
session := model.Session{
UserId: th.BasicUser.Id,
}
ok, isMember := th.App.SessionHasPermissionToReadPost(th.Context, session, post.Id)
assert.True(t, ok)
assert.True(t, isMember)
})
t.Run("other member in channel can read post", func(t *testing.T) {
// Add BasicUser2 to channel
_, aerr := th.App.AddUserToChannel(th.Context, th.BasicUser2, th.BasicChannel, false)
require.Nil(t, aerr)
session := model.Session{
UserId: th.BasicUser2.Id,
}
ok, isMember := th.App.SessionHasPermissionToReadPost(th.Context, session, post.Id)
assert.True(t, ok)
assert.True(t, isMember)
})
t.Run("non-member can read post in public channel", func(t *testing.T) {
// Remove BasicUser2 from channel
aerr := th.App.removeUserFromChannel(th.Context, th.BasicUser2.Id, th.SystemAdminUser.Id, th.BasicChannel)
assert.Nil(t, aerr)
session := model.Session{
UserId: th.BasicUser2.Id,
}
ok, isMember := th.App.SessionHasPermissionToReadPost(th.Context, session, post.Id)
assert.True(t, ok)
assert.False(t, isMember)
})
t.Run("non-member cannot read post in private channel", func(t *testing.T) {
privateChan := th.CreatePrivateChannel(t, th.BasicTeam)
privatePost, _, err := th.App.CreatePost(th.Context, &model.Post{
UserId: th.BasicUser.Id,
ChannelId: privateChan.Id,
Message: "private message",
}, privateChan, model.CreatePostFlags{})
require.Nil(t, err)
session := model.Session{
UserId: th.BasicUser2.Id,
}
ok, isMember := th.App.SessionHasPermissionToReadPost(th.Context, session, privatePost.Id)
assert.False(t, ok)
assert.False(t, isMember)
})
t.Run("admin can read post even if not a channel member", func(t *testing.T) {
privateChan := th.CreatePrivateChannel(t, th.BasicTeam)
privatePost, _, err := th.App.CreatePost(th.Context, &model.Post{
UserId: th.BasicUser.Id,
ChannelId: privateChan.Id,
Message: "private admin",
}, privateChan, model.CreatePostFlags{})
require.Nil(t, err)
session := model.Session{
UserId: th.SystemAdminUser.Id,
Roles: model.SystemAdminRoleId,
}
ok, isMember := th.App.SessionHasPermissionToReadPost(th.Context, session, privatePost.Id)
assert.True(t, ok)
assert.False(t, isMember)
})
t.Run("returns false for empty postID", func(t *testing.T) {
session := model.Session{
UserId: th.BasicUser.Id,
}
ok, isMember := th.App.SessionHasPermissionToReadPost(th.Context, session, "")
assert.False(t, ok)
assert.False(t, isMember)
})
t.Run("returns permission based on system level if postID is missing", func(t *testing.T) {
// To simulate a missing post, use a postID that doesn't exist
session := model.Session{
UserId: th.SystemAdminUser.Id,
Roles: model.SystemAdminRoleId,
}
ok, isMember := th.App.SessionHasPermissionToReadPost(th.Context, session, model.NewId())
assert.True(t, ok)
assert.False(t, isMember)
// Basic user, should be false
session = model.Session{
UserId: th.BasicUser2.Id,
}
ok, isMember = th.App.SessionHasPermissionToReadPost(th.Context, session, model.NewId())
assert.False(t, ok)
assert.False(t, isMember)
})
}
func TestHasPermissionToEditPropertyField(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
cpaGroup, groupErr := th.App.GetPropertyGroup(request.TestContext(t), model.AccessControlPropertyGroupName)
require.Nil(t, groupErr)
groupID := cpaGroup.ID
testCases := []struct {
name string
userID string
field *model.PropertyField
expected bool
}{
{
name: "nil field returns false",
userID: th.BasicUser.Id,
field: nil,
expected: false,
},
{
name: "empty userID returns false",
userID: "",
field: &model.PropertyField{
GroupID: groupID,
Name: "Test Field",
Type: model.PropertyFieldTypeText,
TargetType: string(model.PropertyFieldTargetLevelSystem),
PermissionField: model.NewPointer(model.PermissionLevelMember),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelMember),
},
expected: false,
},
{
name: "protected field always returns false",
userID: th.SystemAdminUser.Id,
field: &model.PropertyField{
GroupID: groupID,
Name: "Protected Field",
Type: model.PropertyFieldTypeText,
TargetType: string(model.PropertyFieldTargetLevelSystem),
Protected: true,
PermissionField: model.NewPointer(model.PermissionLevelNone),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
},
expected: false,
},
{
name: "nil permissions returns false",
userID: th.BasicUser.Id,
field: &model.PropertyField{
GroupID: groupID,
Name: "Field Without Permissions",
Type: model.PropertyFieldTypeText,
TargetType: string(model.PropertyFieldTargetLevelSystem),
},
expected: false,
},
{
name: "admin user can edit field with admin permission",
userID: th.SystemAdminUser.Id,
field: &model.PropertyField{
GroupID: groupID,
Name: "Admin Only Field",
Type: model.PropertyFieldTypeText,
TargetType: string(model.PropertyFieldTargetLevelSystem),
PermissionField: model.NewPointer(model.PermissionLevelSysadmin),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
},
expected: true,
},
{
name: "non-admin user cannot edit field with admin permission",
userID: th.BasicUser.Id,
field: &model.PropertyField{
GroupID: groupID,
Name: "Admin Only Field",
Type: model.PropertyFieldTypeText,
TargetType: string(model.PropertyFieldTargetLevelSystem),
PermissionField: model.NewPointer(model.PermissionLevelSysadmin),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
},
expected: false,
},
{
name: "member can edit field with member permission on system field",
userID: th.BasicUser.Id,
field: &model.PropertyField{
GroupID: groupID,
Name: "Member Edit Field",
Type: model.PropertyFieldTypeText,
TargetType: string(model.PropertyFieldTargetLevelSystem),
PermissionField: model.NewPointer(model.PermissionLevelMember),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelMember),
},
expected: true,
},
{
name: "field permission none denies admin",
userID: th.SystemAdminUser.Id,
field: &model.PropertyField{
GroupID: groupID,
Name: "No Edit Field",
Type: model.PropertyFieldTypeText,
TargetType: string(model.PropertyFieldTargetLevelSystem),
PermissionField: model.NewPointer(model.PermissionLevelNone),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
},
expected: false,
},
{
name: "field permission none denies member",
userID: th.BasicUser.Id,
field: &model.PropertyField{
GroupID: groupID,
Name: "No Edit Field",
Type: model.PropertyFieldTypeText,
TargetType: string(model.PropertyFieldTargetLevelSystem),
PermissionField: model.NewPointer(model.PermissionLevelNone),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
},
expected: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, th.App.HasPermissionToEditPropertyField(th.Context, tc.userID, tc.field))
})
}
}
// TestHasPermissionToSetPropertyFieldValues exercises the field-target
// dispatch path: user-object values defer to hasPropertyFieldScopeAccess on
// the field's TargetType (channel/team/system). Per-value-target dispatch
// (channel- and post-object values) is covered separately in
// TestSessionHasPropertyFieldPermissionAdmin and
// TestSessionHasPermissionToSetPropertyFieldValues_PostMember.
func TestHasPermissionToSetPropertyFieldValues(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
cpaGroup, groupErr := th.App.GetPropertyGroup(request.TestContext(t), model.AccessControlPropertyGroupName)
require.Nil(t, groupErr)
groupID := cpaGroup.ID
// nonMember belongs to no team or channel — used for cascade-denial cases.
nonMember := th.CreateUser(t)
testCases := []struct {
name string
userID string
field *model.PropertyField
expected bool
}{
{
name: "nil field returns false",
userID: th.BasicUser.Id,
field: nil,
expected: false,
},
{
name: "empty userID returns false",
userID: "",
field: &model.PropertyField{
GroupID: groupID,
Name: "Test Field",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(model.PropertyFieldTargetLevelSystem),
PermissionField: model.NewPointer(model.PermissionLevelSysadmin),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
},
expected: false,
},
{
name: "nil permissions returns false",
userID: th.BasicUser.Id,
field: &model.PropertyField{
GroupID: groupID,
Name: "Field Without Permissions",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(model.PropertyFieldTargetLevelSystem),
},
expected: false,
},
{
name: "sysadmin can set values on channel-target field with values permission sysadmin",
userID: th.SystemAdminUser.Id,
field: &model.PropertyField{
GroupID: groupID,
Name: "Channel Field Sysadmin",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(model.PropertyFieldTargetLevelChannel),
TargetID: th.BasicChannel.Id,
PermissionField: model.NewPointer(model.PermissionLevelSysadmin),
PermissionValues: model.NewPointer(model.PermissionLevelSysadmin),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
},
expected: true,
},
{
name: "sysadmin cascades through channel scope on channel-target field with values permission member",
userID: th.SystemAdminUser.Id,
field: &model.PropertyField{
GroupID: groupID,
Name: "Channel Field Member",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(model.PropertyFieldTargetLevelChannel),
TargetID: th.BasicChannel.Id,
PermissionField: model.NewPointer(model.PermissionLevelSysadmin),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
},
expected: true,
},
{
name: "channel member can set values on channel-target field with values permission member",
userID: th.BasicUser.Id,
field: &model.PropertyField{
GroupID: groupID,
Name: "Channel Field",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(model.PropertyFieldTargetLevelChannel),
TargetID: th.BasicChannel.Id,
PermissionField: model.NewPointer(model.PermissionLevelSysadmin),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
},
expected: true,
},
{
name: "channel member can set values on protected channel-target field with values permission member",
userID: th.BasicUser.Id,
field: &model.PropertyField{
GroupID: groupID,
Name: "Channel Field Protected",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(model.PropertyFieldTargetLevelChannel),
TargetID: th.BasicChannel.Id,
Protected: true,
PermissionField: model.NewPointer(model.PermissionLevelNone),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
},
expected: true,
},
{
name: "non-member cannot set values on channel-target field with values permission member",
userID: nonMember.Id,
field: &model.PropertyField{
GroupID: groupID,
Name: "Channel Field",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(model.PropertyFieldTargetLevelChannel),
TargetID: th.BasicChannel.Id,
PermissionField: model.NewPointer(model.PermissionLevelSysadmin),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
},
expected: false,
},
{
name: "team member can set values on team-target field with values permission member",
userID: th.BasicUser.Id,
field: &model.PropertyField{
GroupID: groupID,
Name: "Team Field",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(model.PropertyFieldTargetLevelTeam),
TargetID: th.BasicTeam.Id,
PermissionField: model.NewPointer(model.PermissionLevelSysadmin),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
},
expected: true,
},
{
name: "non-team-member cannot set values on team-target field with values permission member",
userID: nonMember.Id,
field: &model.PropertyField{
GroupID: groupID,
Name: "Team Field",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(model.PropertyFieldTargetLevelTeam),
TargetID: th.BasicTeam.Id,
PermissionField: model.NewPointer(model.PermissionLevelSysadmin),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
},
expected: false,
},
{
name: "sysadmin can set values on team-target field with values permission sysadmin",
userID: th.SystemAdminUser.Id,
field: &model.PropertyField{
GroupID: groupID,
Name: "Team Field Sysadmin",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(model.PropertyFieldTargetLevelTeam),
TargetID: th.BasicTeam.Id,
PermissionField: model.NewPointer(model.PermissionLevelSysadmin),
PermissionValues: model.NewPointer(model.PermissionLevelSysadmin),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
},
expected: true,
},
{
name: "any authenticated user can set values on system-target field with values permission member",
userID: th.BasicUser.Id,
field: &model.PropertyField{
GroupID: groupID,
Name: "System Field",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(model.PropertyFieldTargetLevelSystem),
PermissionField: model.NewPointer(model.PermissionLevelSysadmin),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
},
expected: true,
},
{
name: "values permission none denies sysadmin",
userID: th.SystemAdminUser.Id,
field: &model.PropertyField{
GroupID: groupID,
Name: "System Managed Values Field",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(model.PropertyFieldTargetLevelSystem),
PermissionField: model.NewPointer(model.PermissionLevelSysadmin),
PermissionValues: model.NewPointer(model.PermissionLevelNone),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
},
expected: false,
},
{
name: "values permission none denies member",
userID: th.BasicUser.Id,
field: &model.PropertyField{
GroupID: groupID,
Name: "System Managed Values Field",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(model.PropertyFieldTargetLevelSystem),
PermissionField: model.NewPointer(model.PermissionLevelSysadmin),
PermissionValues: model.NewPointer(model.PermissionLevelNone),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
},
expected: false,
},
}
// All cases above use user-object values; valueTargetID is unused by the
// user/system/template branch of hasPropertyFieldValueScopeAccess.
const unusedValueTargetID = ""
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, th.App.HasPermissionToSetPropertyFieldValues(th.Context, tc.userID, tc.field, unusedValueTargetID))
})
}
}
func TestHasPermissionToManagePropertyFieldOptions(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
cpaGroup, groupErr := th.App.GetPropertyGroup(request.TestContext(t), model.AccessControlPropertyGroupName)
require.Nil(t, groupErr)
groupID := cpaGroup.ID
testCases := []struct {
name string
userID string
field *model.PropertyField
expected bool
}{
{
name: "nil field returns false",
userID: th.BasicUser.Id,
field: nil,
expected: false,
},
{
name: "empty userID returns false",
userID: "",
field: &model.PropertyField{
GroupID: groupID,
Name: "Test Field",
Type: model.PropertyFieldTypeSelect,
TargetType: string(model.PropertyFieldTargetLevelSystem),
PermissionField: model.NewPointer(model.PermissionLevelSysadmin),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelMember),
},
expected: false,
},
{
name: "nil permissions returns false",
userID: th.BasicUser.Id,
field: &model.PropertyField{
GroupID: groupID,
Name: "Field Without Permissions",
Type: model.PropertyFieldTypeSelect,
TargetType: string(model.PropertyFieldTargetLevelSystem),
},
expected: false,
},
{
name: "admin user can manage options with admin permission",
userID: th.SystemAdminUser.Id,
field: &model.PropertyField{
GroupID: groupID,
Name: "Admin Options Field",
Type: model.PropertyFieldTypeSelect,
TargetType: string(model.PropertyFieldTargetLevelSystem),
PermissionField: model.NewPointer(model.PermissionLevelSysadmin),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
},
expected: true,
},
{
name: "non-admin user cannot manage options with admin permission",
userID: th.BasicUser.Id,
field: &model.PropertyField{
GroupID: groupID,
Name: "Admin Options Field",
Type: model.PropertyFieldTypeSelect,
TargetType: string(model.PropertyFieldTargetLevelSystem),
PermissionField: model.NewPointer(model.PermissionLevelSysadmin),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
},
expected: false,
},
{
name: "member can manage options with member permission",
userID: th.BasicUser.Id,
field: &model.PropertyField{
GroupID: groupID,
Name: "Member Options Field",
Type: model.PropertyFieldTypeSelect,
TargetType: string(model.PropertyFieldTargetLevelSystem),
PermissionField: model.NewPointer(model.PermissionLevelSysadmin),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelMember),
},
expected: true,
},
{
name: "admin can manage options on protected field with admin permission",
userID: th.SystemAdminUser.Id,
field: &model.PropertyField{
GroupID: groupID,
Name: "Protected Options Field",
Type: model.PropertyFieldTypeSelect,
TargetType: string(model.PropertyFieldTargetLevelSystem),
Protected: true,
PermissionField: model.NewPointer(model.PermissionLevelNone),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
},
expected: true,
},
{
name: "options permission none denies admin",
userID: th.SystemAdminUser.Id,
field: &model.PropertyField{
GroupID: groupID,
Name: "Locked Options Field",
Type: model.PropertyFieldTypeSelect,
TargetType: string(model.PropertyFieldTargetLevelSystem),
PermissionField: model.NewPointer(model.PermissionLevelSysadmin),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelNone),
},
expected: false,
},
{
name: "options permission none denies member",
userID: th.BasicUser.Id,
field: &model.PropertyField{
GroupID: groupID,
Name: "Locked Options Field",
Type: model.PropertyFieldTypeSelect,
TargetType: string(model.PropertyFieldTargetLevelSystem),
PermissionField: model.NewPointer(model.PermissionLevelSysadmin),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelNone),
},
expected: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, th.App.HasPermissionToManagePropertyFieldOptions(th.Context, tc.userID, tc.field))
})
}
}
func TestSessionHasPermissionToEditPropertyField(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
cpaGroup, groupErr := th.App.GetPropertyGroup(request.TestContext(t), model.AccessControlPropertyGroupName)
require.Nil(t, groupErr)
groupID := cpaGroup.ID
testCases := []struct {
name string
session model.Session
field *model.PropertyField
expected bool
}{
{
name: "nil field returns false even for unrestricted session",
session: model.Session{UserId: th.BasicUser.Id, Local: true},
field: nil,
expected: false,
},
{
name: "protected field returns false even for unrestricted session",
session: model.Session{UserId: th.SystemAdminUser.Id, Local: true},
field: &model.PropertyField{
GroupID: groupID,
Name: "Protected Field",
Type: model.PropertyFieldTypeText,
TargetType: string(model.PropertyFieldTargetLevelSystem),
Protected: true,
PermissionField: model.NewPointer(model.PermissionLevelNone),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
},
expected: false,
},
{
name: "nil permissions returns false even for unrestricted session",
session: model.Session{UserId: th.SystemAdminUser.Id, Local: true},
field: &model.PropertyField{
GroupID: groupID,
Name: "Field Without Permissions",
Type: model.PropertyFieldTypeText,
TargetType: string(model.PropertyFieldTargetLevelSystem),
},
expected: false,
},
{
name: "unrestricted session can edit valid field",
session: model.Session{UserId: th.BasicUser.Id, Local: true},
field: &model.PropertyField{
GroupID: groupID,
Name: "Valid Field",
Type: model.PropertyFieldTypeText,
TargetType: string(model.PropertyFieldTargetLevelSystem),
PermissionField: model.NewPointer(model.PermissionLevelSysadmin),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
},
expected: true,
},
{
name: "regular session respects permission level",
session: model.Session{UserId: th.BasicUser.Id},
field: &model.PropertyField{
GroupID: groupID,
Name: "Admin Field",
Type: model.PropertyFieldTypeText,
TargetType: string(model.PropertyFieldTargetLevelSystem),
PermissionField: model.NewPointer(model.PermissionLevelSysadmin),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
},
expected: false,
},
{
name: "admin session can edit field with admin permission",
session: model.Session{UserId: th.SystemAdminUser.Id, Roles: model.SystemAdminRoleId},
field: &model.PropertyField{
GroupID: groupID,
Name: "Admin Only Field",
Type: model.PropertyFieldTypeText,
TargetType: string(model.PropertyFieldTargetLevelSystem),
PermissionField: model.NewPointer(model.PermissionLevelSysadmin),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
},
expected: true,
},
{
name: "non-admin session cannot edit field with admin permission",
session: model.Session{UserId: th.BasicUser.Id, Roles: model.SystemUserRoleId},
field: &model.PropertyField{
GroupID: groupID,
Name: "Admin Only Field",
Type: model.PropertyFieldTypeText,
TargetType: string(model.PropertyFieldTargetLevelSystem),
PermissionField: model.NewPointer(model.PermissionLevelSysadmin),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
},
expected: false,
},
{
name: "member session can edit field with member permission on system field",
session: model.Session{UserId: th.BasicUser.Id, Roles: model.SystemUserRoleId},
field: &model.PropertyField{
GroupID: groupID,
Name: "Member Edit Field",
Type: model.PropertyFieldTypeText,
TargetType: string(model.PropertyFieldTargetLevelSystem),
PermissionField: model.NewPointer(model.PermissionLevelMember),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelMember),
},
expected: true,
},
{
name: "field permission none denies admin session",
session: model.Session{UserId: th.SystemAdminUser.Id, Roles: model.SystemAdminRoleId},
field: &model.PropertyField{
GroupID: groupID,
Name: "No Edit Field",
Type: model.PropertyFieldTypeText,
TargetType: string(model.PropertyFieldTargetLevelSystem),
PermissionField: model.NewPointer(model.PermissionLevelNone),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
},
expected: false,
},
{
name: "field permission none denies member session",
session: model.Session{UserId: th.BasicUser.Id, Roles: model.SystemUserRoleId},
field: &model.PropertyField{
GroupID: groupID,
Name: "No Edit Field",
Type: model.PropertyFieldTypeText,
TargetType: string(model.PropertyFieldTargetLevelSystem),
PermissionField: model.NewPointer(model.PermissionLevelNone),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
},
expected: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, th.App.SessionHasPermissionToEditPropertyField(th.Context, tc.session, tc.field))
})
}
}
// TestSessionHasPermissionToSetPropertyFieldValues is the session-bound twin
// of TestHasPermissionToSetPropertyFieldValues — same field-target dispatch
// path via user-object values. Per-value-target dispatch is covered in
// TestSessionHasPropertyFieldPermissionAdmin and
// TestSessionHasPermissionToSetPropertyFieldValues_PostMember.
func TestSessionHasPermissionToSetPropertyFieldValues(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
cpaGroup, groupErr := th.App.GetPropertyGroup(request.TestContext(t), model.AccessControlPropertyGroupName)
require.Nil(t, groupErr)
groupID := cpaGroup.ID
// nonMember belongs to no team or channel — used for cascade-denial cases.
nonMember := th.CreateUser(t)
testCases := []struct {
name string
session model.Session
field *model.PropertyField
expected bool
}{
{
name: "nil field returns false even for unrestricted session",
session: model.Session{UserId: th.BasicUser.Id, Local: true},
field: nil,
expected: false,
},
{
name: "nil permissions returns false even for unrestricted session",
session: model.Session{UserId: th.SystemAdminUser.Id, Local: true},
field: &model.PropertyField{
GroupID: groupID,
Name: "Field Without Permissions",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(model.PropertyFieldTargetLevelSystem),
},
expected: false,
},
{
name: "unrestricted session can set values on valid field",
session: model.Session{UserId: th.BasicUser.Id, Local: true},
field: &model.PropertyField{
GroupID: groupID,
Name: "Valid Field",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(model.PropertyFieldTargetLevelSystem),
PermissionField: model.NewPointer(model.PermissionLevelSysadmin),
PermissionValues: model.NewPointer(model.PermissionLevelSysadmin),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
},
expected: true,
},
{
name: "unrestricted session can set values on protected field",
session: model.Session{UserId: th.BasicUser.Id, Local: true},
field: &model.PropertyField{
GroupID: groupID,
Name: "Protected Field",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(model.PropertyFieldTargetLevelSystem),
Protected: true,
PermissionField: model.NewPointer(model.PermissionLevelNone),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
},
expected: true,
},
{
name: "sysadmin session can set values on channel-target field with values permission sysadmin",
session: model.Session{UserId: th.SystemAdminUser.Id, Roles: model.SystemAdminRoleId},
field: &model.PropertyField{
GroupID: groupID,
Name: "Channel Field Sysadmin",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(model.PropertyFieldTargetLevelChannel),
TargetID: th.BasicChannel.Id,
PermissionField: model.NewPointer(model.PermissionLevelSysadmin),
PermissionValues: model.NewPointer(model.PermissionLevelSysadmin),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
},
expected: true,
},
{
name: "sysadmin session cascades through channel scope on channel-target field with values permission member",
session: model.Session{UserId: th.SystemAdminUser.Id, Roles: model.SystemAdminRoleId},
field: &model.PropertyField{
GroupID: groupID,
Name: "Channel Field Member",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(model.PropertyFieldTargetLevelChannel),
TargetID: th.BasicChannel.Id,
PermissionField: model.NewPointer(model.PermissionLevelSysadmin),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
},
expected: true,
},
{
name: "channel member session can set values on channel-target field with values permission member",
session: model.Session{UserId: th.BasicUser.Id, Roles: model.SystemUserRoleId},
field: &model.PropertyField{
GroupID: groupID,
Name: "Channel Field",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(model.PropertyFieldTargetLevelChannel),
TargetID: th.BasicChannel.Id,
PermissionField: model.NewPointer(model.PermissionLevelSysadmin),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
},
expected: true,
},
{
name: "non-member session cannot set values on channel-target field with values permission member",
session: model.Session{UserId: nonMember.Id, Roles: model.SystemUserRoleId},
field: &model.PropertyField{
GroupID: groupID,
Name: "Channel Field",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(model.PropertyFieldTargetLevelChannel),
TargetID: th.BasicChannel.Id,
PermissionField: model.NewPointer(model.PermissionLevelSysadmin),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
},
expected: false,
},
{
name: "team member session can set values on team-target field with values permission member",
session: model.Session{UserId: th.BasicUser.Id, Roles: model.SystemUserRoleId},
field: &model.PropertyField{
GroupID: groupID,
Name: "Team Field",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(model.PropertyFieldTargetLevelTeam),
TargetID: th.BasicTeam.Id,
PermissionField: model.NewPointer(model.PermissionLevelSysadmin),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
},
expected: true,
},
{
name: "non-team-member session cannot set values on team-target field with values permission member",
session: model.Session{UserId: nonMember.Id, Roles: model.SystemUserRoleId},
field: &model.PropertyField{
GroupID: groupID,
Name: "Team Field",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(model.PropertyFieldTargetLevelTeam),
TargetID: th.BasicTeam.Id,
PermissionField: model.NewPointer(model.PermissionLevelSysadmin),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
},
expected: false,
},
{
name: "sysadmin session can set values on team-target field with values permission sysadmin",
session: model.Session{UserId: th.SystemAdminUser.Id, Roles: model.SystemAdminRoleId},
field: &model.PropertyField{
GroupID: groupID,
Name: "Team Field Sysadmin",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(model.PropertyFieldTargetLevelTeam),
TargetID: th.BasicTeam.Id,
PermissionField: model.NewPointer(model.PermissionLevelSysadmin),
PermissionValues: model.NewPointer(model.PermissionLevelSysadmin),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
},
expected: true,
},
{
name: "any authenticated session can set values on system-target field with values permission member",
session: model.Session{UserId: th.BasicUser.Id, Roles: model.SystemUserRoleId},
field: &model.PropertyField{
GroupID: groupID,
Name: "System Field",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(model.PropertyFieldTargetLevelSystem),
PermissionField: model.NewPointer(model.PermissionLevelSysadmin),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
},
expected: true,
},
{
name: "values permission none denies sysadmin session",
session: model.Session{UserId: th.SystemAdminUser.Id, Roles: model.SystemAdminRoleId},
field: &model.PropertyField{
GroupID: groupID,
Name: "System Managed Values Field",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(model.PropertyFieldTargetLevelSystem),
PermissionField: model.NewPointer(model.PermissionLevelSysadmin),
PermissionValues: model.NewPointer(model.PermissionLevelNone),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
},
expected: false,
},
{
name: "values permission none denies member session",
session: model.Session{UserId: th.BasicUser.Id, Roles: model.SystemUserRoleId},
field: &model.PropertyField{
GroupID: groupID,
Name: "System Managed Values Field",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(model.PropertyFieldTargetLevelSystem),
PermissionField: model.NewPointer(model.PermissionLevelSysadmin),
PermissionValues: model.NewPointer(model.PermissionLevelNone),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
},
expected: false,
},
}
// All cases above use user-object values; valueTargetID is unused by the
// user/system/template branch of hasPropertyFieldValueScopeAccess.
const unusedValueTargetID = ""
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, th.App.SessionHasPermissionToSetPropertyFieldValues(th.Context, tc.session, tc.field, unusedValueTargetID))
})
}
}
func TestSessionHasPermissionToManagePropertyFieldOptions(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
cpaGroup, groupErr := th.App.GetPropertyGroup(request.TestContext(t), model.AccessControlPropertyGroupName)
require.Nil(t, groupErr)
groupID := cpaGroup.ID
testCases := []struct {
name string
session model.Session
field *model.PropertyField
expected bool
}{
{
name: "nil field returns false even for unrestricted session",
session: model.Session{UserId: th.BasicUser.Id, Local: true},
field: nil,
expected: false,
},
{
name: "nil permissions returns false even for unrestricted session",
session: model.Session{UserId: th.SystemAdminUser.Id, Local: true},
field: &model.PropertyField{
GroupID: groupID,
Name: "Field Without Permissions",
Type: model.PropertyFieldTypeSelect,
TargetType: string(model.PropertyFieldTargetLevelSystem),
},
expected: false,
},
{
name: "unrestricted session can manage options on valid field",
session: model.Session{UserId: th.BasicUser.Id, Local: true},
field: &model.PropertyField{
GroupID: groupID,
Name: "Valid Select Field",
Type: model.PropertyFieldTypeSelect,
TargetType: string(model.PropertyFieldTargetLevelSystem),
PermissionField: model.NewPointer(model.PermissionLevelSysadmin),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
},
expected: true,
},
{
name: "unrestricted session can manage options on protected field",
session: model.Session{UserId: th.BasicUser.Id, Local: true},
field: &model.PropertyField{
GroupID: groupID,
Name: "Protected Select Field",
Type: model.PropertyFieldTypeSelect,
TargetType: string(model.PropertyFieldTargetLevelSystem),
Protected: true,
PermissionField: model.NewPointer(model.PermissionLevelNone),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
},
expected: true,
},
{
name: "admin session can manage options with admin permission",
session: model.Session{UserId: th.SystemAdminUser.Id, Roles: model.SystemAdminRoleId},
field: &model.PropertyField{
GroupID: groupID,
Name: "Admin Options Field",
Type: model.PropertyFieldTypeSelect,
TargetType: string(model.PropertyFieldTargetLevelSystem),
PermissionField: model.NewPointer(model.PermissionLevelSysadmin),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
},
expected: true,
},
{
name: "non-admin session cannot manage options with admin permission",
session: model.Session{UserId: th.BasicUser.Id, Roles: model.SystemUserRoleId},
field: &model.PropertyField{
GroupID: groupID,
Name: "Admin Options Field",
Type: model.PropertyFieldTypeSelect,
TargetType: string(model.PropertyFieldTargetLevelSystem),
PermissionField: model.NewPointer(model.PermissionLevelSysadmin),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
},
expected: false,
},
{
name: "member session can manage options with member permission",
session: model.Session{UserId: th.BasicUser.Id, Roles: model.SystemUserRoleId},
field: &model.PropertyField{
GroupID: groupID,
Name: "Member Options Field",
Type: model.PropertyFieldTypeSelect,
TargetType: string(model.PropertyFieldTargetLevelSystem),
PermissionField: model.NewPointer(model.PermissionLevelSysadmin),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelMember),
},
expected: true,
},
{
name: "options permission none denies admin session",
session: model.Session{UserId: th.SystemAdminUser.Id, Roles: model.SystemAdminRoleId},
field: &model.PropertyField{
GroupID: groupID,
Name: "Locked Options Field",
Type: model.PropertyFieldTypeSelect,
TargetType: string(model.PropertyFieldTargetLevelSystem),
PermissionField: model.NewPointer(model.PermissionLevelSysadmin),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelNone),
},
expected: false,
},
{
name: "options permission none denies member session",
session: model.Session{UserId: th.BasicUser.Id, Roles: model.SystemUserRoleId},
field: &model.PropertyField{
GroupID: groupID,
Name: "Locked Options Field",
Type: model.PropertyFieldTypeSelect,
TargetType: string(model.PropertyFieldTargetLevelSystem),
PermissionField: model.NewPointer(model.PermissionLevelSysadmin),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelNone),
},
expected: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, th.App.SessionHasPermissionToManagePropertyFieldOptions(th.Context, tc.session, tc.field))
})
}
}
func TestSessionHasPropertyFieldPermissionAdmin(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
groupID := registerTestPropertyGroup(t, th)
// BasicUser2 is in the team but not the channel — add them so denial
// cases land at "in the channel, but not an admin" rather than "not a
// member at all".
require.NotEqual(t, th.BasicUser.Id, th.BasicUser2.Id)
_, appErr := th.App.AddUserToChannel(th.Context, th.BasicUser2, th.BasicChannel, false)
require.Nil(t, appErr)
t.Run("field-level admin resolves against field target", func(t *testing.T) {
// Edit and ManageOptions operate on the field definition; admin
// means admin of the field's TargetType+TargetID.
fieldFor := func(target model.PropertyFieldTargetLevel, targetID string) *model.PropertyField {
return &model.PropertyField{
GroupID: groupID,
Name: "admin only " + string(target),
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(target),
TargetID: targetID,
PermissionField: model.NewPointer(model.PermissionLevelAdmin),
PermissionValues: model.NewPointer(model.PermissionLevelSysadmin),
PermissionOptions: model.NewPointer(model.PermissionLevelAdmin),
}
}
fieldOps := func(t *testing.T, session model.Session, field *model.PropertyField, want bool) {
t.Helper()
assert.Equal(t, want, th.App.SessionHasPermissionToEditPropertyField(th.Context, session, field))
assert.Equal(t, want, th.App.SessionHasPermissionToManagePropertyFieldOptions(th.Context, session, field))
}
t.Run("channel target", func(t *testing.T) {
field := fieldFor(model.PropertyFieldTargetLevelChannel, th.BasicChannel.Id)
_, appErr := th.App.UpdateChannelMemberRoles(th.Context, th.BasicChannel.Id, th.BasicUser.Id,
model.ChannelUserRoleId+" "+model.ChannelAdminRoleId)
require.Nil(t, appErr)
t.Cleanup(func() {
_, _ = th.App.UpdateChannelMemberRoles(th.Context, th.BasicChannel.Id, th.BasicUser.Id, model.ChannelUserRoleId)
})
fieldOps(t, model.Session{UserId: th.BasicUser.Id, Roles: model.SystemUserRoleId}, field, true)
fieldOps(t, model.Session{UserId: th.BasicUser2.Id, Roles: model.SystemUserRoleId}, field, false)
})
t.Run("team target", func(t *testing.T) {
field := fieldFor(model.PropertyFieldTargetLevelTeam, th.BasicTeam.Id)
_, appErr := th.App.UpdateTeamMemberRoles(th.Context, th.BasicTeam.Id, th.BasicUser.Id,
model.TeamUserRoleId+" "+model.TeamAdminRoleId)
require.Nil(t, appErr)
t.Cleanup(func() {
_, _ = th.App.UpdateTeamMemberRoles(th.Context, th.BasicTeam.Id, th.BasicUser.Id, model.TeamUserRoleId)
})
fieldOps(t, model.Session{UserId: th.BasicUser.Id, Roles: model.SystemUserRoleId}, field, true)
fieldOps(t, model.Session{UserId: th.BasicUser2.Id, Roles: model.SystemUserRoleId}, field, false)
})
t.Run("system target", func(t *testing.T) {
field := fieldFor(model.PropertyFieldTargetLevelSystem, "")
sysadmin := model.Session{UserId: th.SystemAdminUser.Id, Roles: model.SystemUserRoleId + " " + model.SystemAdminRoleId}
fieldOps(t, sysadmin, field, true)
fieldOps(t, model.Session{UserId: th.BasicUser.Id, Roles: model.SystemUserRoleId}, field, false)
})
})
t.Run("value-level admin resolves against value target", func(t *testing.T) {
// Use a system-target field so the dispatch is purely driven by
// ObjectType + the value's TargetID — the channel classification
// shape: one global field, per-channel values.
fieldFor := func(objectType string) *model.PropertyField {
return &model.PropertyField{
GroupID: groupID,
Name: "values admin " + objectType,
Type: model.PropertyFieldTypeText,
ObjectType: objectType,
TargetType: string(model.PropertyFieldTargetLevelSystem),
PermissionField: model.NewPointer(model.PermissionLevelSysadmin),
PermissionValues: model.NewPointer(model.PermissionLevelAdmin),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
}
}
t.Run("channel-object value: admin of the value's channel passes", func(t *testing.T) {
field := fieldFor(model.PropertyFieldObjectTypeChannel)
_, appErr := th.App.UpdateChannelMemberRoles(th.Context, th.BasicChannel.Id, th.BasicUser.Id,
model.ChannelUserRoleId+" "+model.ChannelAdminRoleId)
require.Nil(t, appErr)
t.Cleanup(func() {
_, _ = th.App.UpdateChannelMemberRoles(th.Context, th.BasicChannel.Id, th.BasicUser.Id, model.ChannelUserRoleId)
})
channelAdmin := model.Session{UserId: th.BasicUser.Id, Roles: model.SystemUserRoleId}
channelMember := model.Session{UserId: th.BasicUser2.Id, Roles: model.SystemUserRoleId}
assert.True(t, th.App.SessionHasPermissionToSetPropertyFieldValues(th.Context, channelAdmin, field, th.BasicChannel.Id))
assert.False(t, th.App.SessionHasPermissionToSetPropertyFieldValues(th.Context, channelMember, field, th.BasicChannel.Id))
})
t.Run("post-object value: post-lookup yields the post's channel admin", func(t *testing.T) {
field := fieldFor(model.PropertyFieldObjectTypePost)
_, appErr := th.App.UpdateChannelMemberRoles(th.Context, th.BasicChannel.Id, th.BasicUser.Id,
model.ChannelUserRoleId+" "+model.ChannelAdminRoleId)
require.Nil(t, appErr)
t.Cleanup(func() {
_, _ = th.App.UpdateChannelMemberRoles(th.Context, th.BasicChannel.Id, th.BasicUser.Id, model.ChannelUserRoleId)
})
post := th.CreatePost(t, th.BasicChannel)
channelAdmin := model.Session{UserId: th.BasicUser.Id, Roles: model.SystemUserRoleId}
channelMember := model.Session{UserId: th.BasicUser2.Id, Roles: model.SystemUserRoleId}
assert.True(t, th.App.SessionHasPermissionToSetPropertyFieldValues(th.Context, channelAdmin, field, post.Id))
assert.False(t, th.App.SessionHasPermissionToSetPropertyFieldValues(th.Context, channelMember, field, post.Id))
})
t.Run("user-object value: no per-user admin, falls back to sysadmin", func(t *testing.T) {
field := fieldFor(model.PropertyFieldObjectTypeUser)
sysadmin := model.Session{UserId: th.SystemAdminUser.Id, Roles: model.SystemUserRoleId + " " + model.SystemAdminRoleId}
regularUser := model.Session{UserId: th.BasicUser.Id, Roles: model.SystemUserRoleId}
assert.True(t, th.App.SessionHasPermissionToSetPropertyFieldValues(th.Context, sysadmin, field, th.BasicUser2.Id))
assert.False(t, th.App.SessionHasPermissionToSetPropertyFieldValues(th.Context, regularUser, field, th.BasicUser2.Id))
})
t.Run("user-object value with channel-scoped field: falls back to channel admin", func(t *testing.T) {
// When ObjectType has no per-value admin notion (user/system/template),
// the check defers to the field's TargetType. A channel-scoped
// user-object field should require channel admin on the field's
// channel — not sysadmin.
field := &model.PropertyField{
GroupID: groupID,
Name: "values admin user-channel",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(model.PropertyFieldTargetLevelChannel),
TargetID: th.BasicChannel.Id,
PermissionField: model.NewPointer(model.PermissionLevelSysadmin),
PermissionValues: model.NewPointer(model.PermissionLevelAdmin),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
}
_, appErr := th.App.UpdateChannelMemberRoles(th.Context, th.BasicChannel.Id, th.BasicUser.Id,
model.ChannelUserRoleId+" "+model.ChannelAdminRoleId)
require.Nil(t, appErr)
t.Cleanup(func() {
_, _ = th.App.UpdateChannelMemberRoles(th.Context, th.BasicChannel.Id, th.BasicUser.Id, model.ChannelUserRoleId)
})
channelAdmin := model.Session{UserId: th.BasicUser.Id, Roles: model.SystemUserRoleId}
channelMember := model.Session{UserId: th.BasicUser2.Id, Roles: model.SystemUserRoleId}
assert.True(t, th.App.SessionHasPermissionToSetPropertyFieldValues(th.Context, channelAdmin, field, th.BasicUser2.Id))
assert.False(t, th.App.SessionHasPermissionToSetPropertyFieldValues(th.Context, channelMember, field, th.BasicUser2.Id))
})
t.Run("unrestricted session bypasses value-target check", func(t *testing.T) {
field := fieldFor(model.PropertyFieldObjectTypeChannel)
local := model.Session{UserId: th.BasicUser2.Id, Local: true}
assert.True(t, th.App.SessionHasPermissionToSetPropertyFieldValues(th.Context, local, field, th.BasicChannel.Id))
})
})
}
// Member-level scope access on post-object values requires only channel
// membership: any member of the post's channel can set a value on any post
// in that channel, regardless of whether they are the post's author.
func TestSessionHasPermissionToSetPropertyFieldValues_PostMember(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
groupID := registerTestPropertyGroup(t, th)
// BasicUser2 is a channel member but not the post's author.
_, appErr := th.App.AddUserToChannel(th.Context, th.BasicUser2, th.BasicChannel, false)
require.Nil(t, appErr)
field := &model.PropertyField{
GroupID: groupID,
Name: "post values member",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypePost,
TargetType: string(model.PropertyFieldTargetLevelSystem),
PermissionField: model.NewPointer(model.PermissionLevelSysadmin),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
}
post := th.CreatePost(t, th.BasicChannel) // authored by BasicUser
authorSession := model.Session{UserId: th.BasicUser.Id, Roles: model.SystemUserRoleId}
nonAuthorSession := model.Session{UserId: th.BasicUser2.Id, Roles: model.SystemUserRoleId}
// Author (channel member) can set the value.
assert.True(t, th.App.SessionHasPermissionToSetPropertyFieldValues(th.Context, authorSession, field, post.Id))
// Non-author who is also a channel member can set the value.
assert.True(t, th.App.SessionHasPermissionToSetPropertyFieldValues(th.Context, nonAuthorSession, field, post.Id))
}