Files
mattermost/server/channels/app/session_scope_test.go
T
Cursor AgentandChristopher Speller fc59cb3cd5 Grant scoped sessions the public channels of listed teams and open name lookups
Adds SessionScopeResources.PublicChannelTeamIDs: every public channel on
the listed teams, whether or not the team is open, so the plugin can
always include the triggering channel's team. Compliance mode turns it off;
deleted and policy-enforced teams and channels stay excluded.
SessionScopeAllowsChannel now takes SessionScopeLimits so compliance mode
and join_public_teams can limit the two public-channel grants separately.

Flags channel/team lookups by name, channel stats, pinned posts, post
reactions and user search for scoped sessions, each gated by the scope.

Co-authored-by: Christopher Speller <crspeller@users.noreply.github.com>
2026-11-02 07:51:38 +00:00

671 lines
31 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/gorilla/mux"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/plugin"
einterfacesmocks "github.com/mattermost/mattermost/server/v8/einterfaces/mocks"
)
const scopeTestPluginID = "com.example.scoped"
type scopeFixture struct {
bot *model.User
openTeam *model.Team
closedTeam *model.Team
triggering *model.Channel // private, closed team, listed, bot not a member
extra *model.Channel // private, open team, listed, bot not a member
publicOpen *model.Channel // public, open team, bot not a member
publicClosed *model.Channel // public, closed team
memberPriv *model.Channel // private, bot is a member, not listed
botDM *model.Channel // the bot's DM with a user
}
func setTeamOpen(t *testing.T, th *TestHelper, team *model.Team, open bool) *model.Team {
t.Helper()
team.AllowOpenInvite = open
if !open {
team.Type = model.TeamInvite
}
updated, err := th.App.Srv().Store().Team().Update(team)
require.NoError(t, err)
return updated
}
func newScopeFixture(t *testing.T, th *TestHelper) *scopeFixture {
t.Helper()
f := &scopeFixture{}
bot := th.CreateBot(t)
var appErr *model.AppError
f.bot, appErr = th.App.GetUser(th.Context, bot.UserId)
require.Nil(t, appErr)
f.openTeam = setTeamOpen(t, th, th.CreateTeam(t), true)
f.closedTeam = setTeamOpen(t, th, th.CreateTeam(t), false)
f.triggering = th.CreatePrivateChannel(t, f.closedTeam)
f.extra = th.CreatePrivateChannel(t, f.openTeam)
f.publicOpen = th.CreateChannel(t, f.openTeam)
f.publicClosed = th.CreateChannel(t, f.closedTeam)
f.memberPriv = th.CreatePrivateChannel(t, f.openTeam)
th.LinkUserToTeam(t, f.bot, f.openTeam)
th.AddUserToChannel(t, f.bot, f.memberPriv)
f.botDM, appErr = th.App.GetOrCreateDirectChannel(th.Context, th.BasicUser.Id, f.bot.Id)
require.Nil(t, appErr)
return f
}
func (f *scopeFixture) scope() *model.SessionScope {
return &model.SessionScope{
Permissions: []string{model.PermissionReadChannel.Id, model.PermissionReadChannelContent.Id, model.PermissionViewTeam.Id},
Resources: model.SessionScopeResources{
ChannelIDs: []string{f.triggering.Id, f.extra.Id},
OpenTeamPublicChannels: true,
},
OnBehalfOfUserID: model.NewId(),
}
}
func mintScoped(t *testing.T, th *TestHelper, userID string, scope *model.SessionScope) *model.Session {
t.Helper()
session, appErr := th.App.CreateScopedSession(th.Context, scopeTestPluginID, userID, scope, model.GetMillis()+30*60*1000)
require.Nil(t, appErr)
return session
}
// forgeScoped builds a scoped session without validation, as if the stored
// scope had been tampered with.
func forgeScoped(t *testing.T, userID string, scope *model.SessionScope) *model.Session {
t.Helper()
b, err := json.Marshal(scope)
require.NoError(t, err)
return &model.Session{Id: model.NewId(), UserId: userID, Props: model.StringMap{model.SessionPropScope: string(b)}}
}
func TestSessionScopeChannelReads(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
f := newScopeFixture(t, th)
session := mintScoped(t, th, f.bot.Id, f.scope())
rctx := th.Context.WithSession(session)
inScope := []*model.Channel{f.triggering, f.extra, f.publicOpen}
outOfScope := map[string]*model.Channel{
"public channel on closed team": f.publicClosed,
"unlisted private channel on the listed channel's team": th.CreatePrivateChannel(t, f.closedTeam),
"private member channel": f.memberPriv,
"bot DM": f.botDM,
}
check := func(t *testing.T, channel *model.Channel, permission *model.Permission, want bool) {
t.Helper()
got, isMember := th.App.SessionHasPermissionToChannel(rctx, *session, channel.Id, permission)
assert.Equal(t, want, got, "SessionHasPermissionToChannel")
assert.False(t, isMember)
if permission.Id == model.PermissionReadChannelContent.Id {
got, _ = th.App.SessionHasPermissionToReadChannel(rctx, *session, channel)
assert.Equal(t, want, got, "SessionHasPermissionToReadChannel")
got, _ = th.App.HasPermissionToReadChannel(rctx, f.bot.Id, channel)
assert.Equal(t, want, got, "HasPermissionToReadChannel with scoped request")
post := &model.Post{ChannelId: channel.Id, UserId: th.BasicUser.Id, Message: "m"}
_, err := th.App.Srv().Store().Post().Save(th.Context, post)
require.NoError(t, err)
got, _ = th.App.SessionHasPermissionToReadPost(rctx, *session, post.Id)
assert.Equal(t, want, got, "SessionHasPermissionToReadPost")
assert.Equal(t, want, th.App.SessionHasPermissionToChannelByPost(*session, post.Id, permission), "SessionHasPermissionToChannelByPost")
assert.Equal(t, want, th.App.HasPermissionToChannelByPost(rctx, f.bot.Id, post.Id, permission), "HasPermissionToChannelByPost with scoped request")
}
got, _ = th.App.HasPermissionToChannel(rctx, f.bot.Id, channel.Id, permission)
assert.Equal(t, want, got, "HasPermissionToChannel with scoped request")
}
for _, permission := range []*model.Permission{model.PermissionReadChannel, model.PermissionReadChannelContent} {
for _, channel := range inScope {
t.Run(permission.Id+" in scope "+channel.Name, func(t *testing.T) {
check(t, channel, permission, true)
})
}
for name, channel := range outOfScope {
t.Run(permission.Id+" "+name, func(t *testing.T) {
check(t, channel, permission, false)
})
}
}
t.Run("permission outside the scope's subset is denied on in-scope channels", func(t *testing.T) {
for _, channel := range inScope {
check(t, channel, model.PermissionReadPublicChannel, false)
check(t, channel, model.PermissionCreatePost, false)
check(t, channel, model.PermissionManagePublicChannelProperties, false)
}
})
t.Run("multi-channel check needs every channel in scope", func(t *testing.T) {
assert.True(t, th.App.SessionHasPermissionToChannels(rctx, *session, []string{f.triggering.Id, f.publicOpen.Id}, model.PermissionReadChannel))
assert.False(t, th.App.SessionHasPermissionToChannels(rctx, *session, []string{f.triggering.Id, f.botDM.Id}, model.PermissionReadChannel))
})
t.Run("user-based checks for other users are unaffected", func(t *testing.T) {
ok, _ := th.App.HasPermissionToReadChannel(rctx, th.BasicUser.Id, f.botDM)
assert.True(t, ok)
})
t.Run("the same bot unscoped keeps its normal access", func(t *testing.T) {
unscoped := &model.Session{Id: model.NewId(), UserId: f.bot.Id, Roles: f.bot.Roles}
ok, _ := th.App.SessionHasPermissionToChannel(th.Context, *unscoped, f.memberPriv.Id, model.PermissionReadChannel)
assert.True(t, ok)
ok, _ = th.App.SessionHasPermissionToChannel(th.Context, *unscoped, f.triggering.Id, model.PermissionReadChannel)
assert.False(t, ok)
})
t.Run("listing a DM in a stored scope still denies it", func(t *testing.T) {
scope := f.scope()
scope.Resources.ChannelIDs = append(scope.Resources.ChannelIDs, f.botDM.Id)
forged := forgeScoped(t, f.bot.Id, scope)
ok, _ := th.App.SessionHasPermissionToChannel(th.Context.WithSession(forged), *forged, f.botDM.Id, model.PermissionReadChannelContent)
assert.False(t, ok)
})
t.Run("policy-enforced channel is denied even when listed", func(t *testing.T) {
policyChannel := *f.extra
policyChannel.PolicyEnforced = true
scoped, allowed := th.App.EvaluateSessionScope(rctx, session, model.PermissionReadChannelContent.Id, SessionScopeTarget{Channel: &policyChannel})
assert.True(t, scoped)
assert.False(t, allowed)
})
t.Run("archived channel is denied", func(t *testing.T) {
archived := th.CreateChannel(t, f.openTeam)
require.Nil(t, th.App.DeleteChannel(th.Context, archived, th.BasicUser.Id))
ok, _ := th.App.SessionHasPermissionToChannel(rctx, *session, archived.Id, model.PermissionReadChannel)
assert.False(t, ok)
})
t.Run("malformed scope denies everything", func(t *testing.T) {
malformed := &model.Session{Id: model.NewId(), UserId: f.bot.Id, Props: model.StringMap{model.SessionPropScope: "{"}}
ok, _ := th.App.SessionHasPermissionToChannel(th.Context, *malformed, f.triggering.Id, model.PermissionReadChannel)
assert.False(t, ok)
assert.False(t, th.App.SessionHasPermissionToTeam(*malformed, f.openTeam.Id, model.PermissionViewTeam))
})
}
func TestSessionScopeComplianceDisablesOpenTeamGrant(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
f := newScopeFixture(t, th)
session := mintScoped(t, th, f.bot.Id, f.scope())
rctx := th.Context.WithSession(session)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ComplianceSettings.Enable = true })
defer th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ComplianceSettings.Enable = false })
ok, _ := th.App.SessionHasPermissionToChannel(rctx, *session, f.publicOpen.Id, model.PermissionReadChannel)
assert.False(t, ok, "open-team public channel")
ok, _ = th.App.SessionHasPermissionToChannel(rctx, *session, f.triggering.Id, model.PermissionReadChannel)
assert.True(t, ok, "listed channel")
}
func TestSessionScopeOpenTeamGrantNeedsJoinPublicTeams(t *testing.T) {
th := Setup(t).InitBasic(t)
f := newScopeFixture(t, th)
session := mintScoped(t, th, f.bot.Id, f.scope())
rctx := th.Context.WithSession(session)
otherOpen := setTeamOpen(t, th, th.CreateTeam(t), true)
require.True(t, th.App.SessionHasPermissionToTeam(*session, otherOpen.Id, model.PermissionViewTeam))
ok, _ := th.App.SessionHasPermissionToChannel(rctx, *session, f.publicOpen.Id, model.PermissionReadChannel)
require.True(t, ok)
th.RemovePermissionFromRole(t, model.PermissionJoinPublicTeams.Id, model.SystemUserRoleId)
defer th.AddPermissionToRole(t, model.PermissionJoinPublicTeams.Id, model.SystemUserRoleId)
ok, _ = th.App.SessionHasPermissionToChannel(rctx, *session, f.publicOpen.Id, model.PermissionReadChannel)
assert.False(t, ok, "open-team public channel")
assert.False(t, th.App.SessionHasPermissionToTeam(*session, otherOpen.Id, model.PermissionViewTeam), "open team without a listed channel")
ok, _ = th.App.SessionHasPermissionToChannel(rctx, *session, f.triggering.Id, model.PermissionReadChannel)
assert.True(t, ok, "listed channel")
assert.True(t, th.App.SessionHasPermissionToTeam(*session, f.closedTeam.Id, model.PermissionViewTeam), "team holding a listed channel")
}
func TestSessionScopePublicChannelsOnListedTeams(t *testing.T) {
th := Setup(t).InitBasic(t)
f := newScopeFixture(t, th)
scope := f.scope()
scope.Permissions = append(scope.Permissions, model.PermissionListTeamChannels.Id)
scope.Resources.OpenTeamPublicChannels = false
scope.Resources.PublicChannelTeamIDs = []string{f.closedTeam.Id}
session := mintScoped(t, th, f.bot.Id, scope)
rctx := th.Context.WithSession(session)
canRead := func(channel *model.Channel) bool {
ok, _ := th.App.SessionHasPermissionToChannel(rctx, *session, channel.Id, model.PermissionReadChannelContent)
return ok
}
otherClosed := setTeamOpen(t, th, th.CreateTeam(t), false)
assert.True(t, canRead(f.publicClosed), "public channel on the listed invite-only team, bot not a member")
assert.True(t, canRead(f.triggering), "listed channel")
assert.False(t, canRead(th.CreatePrivateChannel(t, f.closedTeam)), "unlisted private channel on the listed team")
assert.False(t, canRead(f.publicOpen), "public channel on an open team without the open-team grant")
assert.False(t, canRead(th.CreateChannel(t, otherClosed)), "public channel on another invite-only team")
assert.False(t, canRead(f.botDM))
assert.True(t, th.App.SessionHasPermissionToTeam(*session, f.closedTeam.Id, model.PermissionViewTeam))
assert.True(t, th.App.SessionHasPermissionToTeam(*session, f.closedTeam.Id, model.PermissionListTeamChannels))
assert.False(t, th.App.SessionHasPermissionToTeam(*session, otherClosed.Id, model.PermissionViewTeam))
assert.False(t, th.App.SessionHasPermissionToTeam(*session, f.closedTeam.Id, model.PermissionManageTeam))
t.Run("not tied to join_public_teams", func(t *testing.T) {
th.RemovePermissionFromRole(t, model.PermissionJoinPublicTeams.Id, model.SystemUserRoleId)
defer th.AddPermissionToRole(t, model.PermissionJoinPublicTeams.Id, model.SystemUserRoleId)
assert.True(t, canRead(f.publicClosed))
})
t.Run("compliance mode turns it off", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ComplianceSettings.Enable = true })
defer th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ComplianceSettings.Enable = false })
assert.False(t, canRead(f.publicClosed))
assert.True(t, canRead(f.triggering), "listed channel")
})
t.Run("policy-enforced channels and teams are excluded", func(t *testing.T) {
policyChannel := th.CreateChannel(t, f.closedTeam)
policyChannel.PolicyEnforced = true
assert.False(t, th.App.ScopeAllowsChannel(rctx, scope, policyChannel))
policyTeam := setTeamOpen(t, th, th.CreateTeam(t), false)
policyPublic := th.CreateChannel(t, policyTeam)
_, err := th.App.Srv().Store().AccessControlPolicy().Save(th.Context, &model.AccessControlPolicy{
ID: policyTeam.Id,
Name: "Scoped team policy",
Type: model.AccessControlPolicyTypeTeam,
Active: true,
Version: model.AccessControlPolicyVersionV0_3,
Rules: []model.AccessControlPolicyRule{{
Actions: []string{model.AccessControlPolicyActionMembership},
Expression: `user.attributes.program == "engineering"`,
}},
})
require.NoError(t, err)
policyScope := f.scope()
policyScope.Resources.PublicChannelTeamIDs = []string{policyTeam.Id}
policySession := mintScoped(t, th, f.bot.Id, policyScope)
team, appErr := th.App.GetTeam(policyTeam.Id)
require.Nil(t, appErr)
require.True(t, team.PolicyEnforced)
ok, _ := th.App.SessionHasPermissionToChannel(th.Context.WithSession(policySession), *policySession, policyPublic.Id, model.PermissionReadChannelContent)
assert.False(t, ok)
assert.False(t, th.App.SessionHasPermissionToTeam(*policySession, policyTeam.Id, model.PermissionViewTeam))
})
t.Run("a deleted team grants nothing", func(t *testing.T) {
team := setTeamOpen(t, th, th.CreateTeam(t), false)
public := th.CreateChannel(t, team)
teamScope := f.scope()
teamScope.Resources.PublicChannelTeamIDs = []string{team.Id}
teamSession := mintScoped(t, th, f.bot.Id, teamScope)
require.Nil(t, th.App.SoftDeleteTeam(team.Id))
ok, _ := th.App.SessionHasPermissionToChannel(th.Context.WithSession(teamSession), *teamSession, public.Id, model.PermissionReadChannelContent)
assert.False(t, ok)
assert.False(t, th.App.SessionHasPermissionToTeam(*teamSession, team.Id, model.PermissionViewTeam))
})
}
func TestSessionScopeViewUsersRestrictions(t *testing.T) {
th := Setup(t).InitBasic(t)
f := newScopeFixture(t, th)
session := mintScoped(t, th, f.bot.Id, f.scope())
rctx := th.Context.WithSession(session)
restrictions, appErr := th.App.GetViewUsersRestrictions(rctx, f.bot.Id)
require.Nil(t, appErr)
require.Nil(t, restrictions, "a bot that can see every user still can")
th.RemovePermissionFromRole(t, model.PermissionViewMembers.Id, model.SystemUserRoleId)
defer th.AddPermissionToRole(t, model.PermissionViewMembers.Id, model.SystemUserRoleId)
th.AddPermissionToRole(t, model.PermissionViewMembers.Id, model.TeamUserRoleId)
defer th.RemovePermissionFromRole(t, model.PermissionViewMembers.Id, model.TeamUserRoleId)
unlistedClosed := setTeamOpen(t, th, th.CreateTeam(t), false)
th.LinkUserToTeam(t, f.bot, unlistedClosed)
restrictions, appErr = th.App.GetViewUsersRestrictions(rctx, f.bot.Id)
require.Nil(t, appErr)
require.NotNil(t, restrictions)
assert.ElementsMatch(t, []string{f.openTeam.Id}, restrictions.Teams, "only teams the bot is on and the scope can view")
assert.ElementsMatch(t, []string{f.triggering.Id, f.extra.Id}, restrictions.Channels, "listed channels, not the bot's DMs or other memberships")
dmOnly := th.CreateUser(t)
_, appErr = th.App.GetOrCreateDirectChannel(th.Context, dmOnly.Id, f.bot.Id)
require.Nil(t, appErr)
canSee, appErr := th.App.UserCanSeeOtherUser(rctx, f.bot.Id, dmOnly.Id)
require.Nil(t, appErr)
assert.False(t, canSee, "sharing a DM with the bot doesn't make a user visible")
canSee, appErr = th.App.UserCanSeeOtherUser(th.Context, f.bot.Id, dmOnly.Id)
require.Nil(t, appErr)
assert.True(t, canSee, "outside a scoped request the bot's memberships still count")
}
func TestSessionScopeTeamAndSystemChecks(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
f := newScopeFixture(t, th)
session := mintScoped(t, th, f.bot.Id, f.scope())
assert.True(t, th.App.SessionHasPermissionToTeam(*session, f.openTeam.Id, model.PermissionViewTeam))
assert.True(t, th.App.SessionHasPermissionToTeam(*session, f.closedTeam.Id, model.PermissionViewTeam), "invite-only team holding a listed channel")
unlistedClosed := setTeamOpen(t, th, th.CreateTeam(t), false)
th.CreateChannel(t, unlistedClosed)
assert.False(t, th.App.SessionHasPermissionToTeam(*session, unlistedClosed.Id, model.PermissionViewTeam), "invite-only team without a listed channel")
listedOnly := f.scope()
listedOnly.Resources.OpenTeamPublicChannels = false
listedOnlySession := mintScoped(t, th, f.bot.Id, listedOnly)
otherOpen := setTeamOpen(t, th, th.CreateTeam(t), true)
assert.False(t, th.App.SessionHasPermissionToTeam(*listedOnlySession, otherOpen.Id, model.PermissionViewTeam), "open team without the open-team grant")
assert.True(t, th.App.SessionHasPermissionToTeam(*listedOnlySession, f.openTeam.Id, model.PermissionViewTeam), "open team holding a listed channel")
t.Run("a listed channel that is archived no longer makes its team visible", func(t *testing.T) {
team := setTeamOpen(t, th, th.CreateTeam(t), false)
archived := th.CreatePrivateChannel(t, team)
scope := f.scope()
scope.Resources.ChannelIDs = []string{archived.Id}
archivedSession := mintScoped(t, th, f.bot.Id, scope)
require.True(t, th.App.SessionHasPermissionToTeam(*archivedSession, team.Id, model.PermissionViewTeam))
require.Nil(t, th.App.DeleteChannel(th.Context, archived, th.BasicUser.Id))
assert.False(t, th.App.SessionHasPermissionToTeam(*archivedSession, team.Id, model.PermissionViewTeam))
})
assert.False(t, th.App.SessionHasPermissionToTeam(*session, f.openTeam.Id, model.PermissionListTeamChannels), "permission not in this scope")
assert.False(t, th.App.SessionHasPermissionToTeam(*session, f.openTeam.Id, model.PermissionReadPublicChannel), "channel permission at team level")
assert.False(t, th.App.SessionHasPermissionToTeam(*session, f.openTeam.Id, model.PermissionManageTeam))
assert.True(t, th.App.SessionHasPermissionToTeams(th.Context, *session, []string{f.openTeam.Id, f.closedTeam.Id}, model.PermissionViewTeam))
assert.False(t, th.App.SessionHasPermissionToTeams(th.Context, *session, []string{f.openTeam.Id, unlistedClosed.Id}, model.PermissionViewTeam))
for _, permission := range []*model.Permission{model.PermissionManageSystem, model.PermissionCreatePostPublic, model.PermissionViewMembers, model.PermissionReadChannelContent} {
assert.False(t, th.App.SessionHasPermissionTo(*session, permission), permission.Id)
assert.False(t, th.App.SessionHasPermissionToAndNotRestrictedAdmin(*session, permission), permission.Id)
}
assert.False(t, th.App.SessionHasPermissionToUser(th.Context, *session, f.bot.Id), "not even itself")
assert.False(t, th.App.SessionHasPermissionToUserOrBot(th.Context, *session, f.bot.Id))
assert.NotNil(t, th.App.SessionHasPermissionToManageBot(th.Context, *session, f.bot.Id))
assert.False(t, th.App.SessionHasPermissionToGroup(*session, model.NewId(), model.PermissionReadChannel))
}
func TestSessionScopeAdminAndPostAllBot(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
f := newScopeFixture(t, th)
_, appErr := th.App.UpdateUserRoles(th.Context, f.bot.Id, model.SystemUserRoleId+" "+model.SystemPostAllRoleId, false)
require.Nil(t, appErr)
session := mintScoped(t, th, f.bot.Id, f.scope())
rctx := th.Context.WithSession(session)
t.Run("system_post_all bot cannot post", func(t *testing.T) {
assert.False(t, th.App.SessionHasPermissionTo(*session, model.PermissionCreatePost))
for _, channel := range []*model.Channel{f.triggering, f.publicOpen, f.memberPriv} {
ok, _ := th.App.SessionHasPermissionToChannel(rctx, *session, channel.Id, model.PermissionCreatePost)
assert.False(t, ok)
}
})
t.Run("system admin scoped session gets nothing from its roles", func(t *testing.T) {
adminSession := mintScoped(t, th, th.SystemAdminUser.Id, f.scope())
adminDM, appErr := th.App.GetOrCreateDirectChannel(th.Context, th.BasicUser.Id, th.SystemAdminUser.Id)
require.Nil(t, appErr)
assert.False(t, th.App.SessionHasPermissionTo(*adminSession, model.PermissionManageSystem))
ok, _ := th.App.SessionHasPermissionToChannel(th.Context, *adminSession, adminDM.Id, model.PermissionReadChannel)
assert.False(t, ok)
ok, _ = th.App.SessionHasPermissionToChannel(th.Context, *adminSession, f.publicClosed.Id, model.PermissionReadChannel)
assert.False(t, ok)
})
t.Run("scoped session is never unrestricted", func(t *testing.T) {
local := session.DeepCopy()
local.Local = true
assert.False(t, local.IsUnrestricted())
assert.False(t, th.App.SessionHasPermissionTo(*local, model.PermissionManageSystem))
ok, _ := th.App.SessionHasPermissionToChannel(th.Context, *local, f.botDM.Id, model.PermissionReadChannel)
assert.False(t, ok)
})
}
func TestSessionScopeIsFinalThroughAccessEvaluation(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
f := newScopeFixture(t, th)
session := mintScoped(t, th, f.bot.Id, f.scope())
rctx := th.Context.WithSession(session)
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.FeatureFlags.PermissionPolicies = true
cfg.AccessControlSettings.EnableAttributeBasedAccessControl = model.NewPointer(true)
})
mockACS := &einterfacesmocks.AccessControlServiceInterface{}
mockACS.On("AccessEvaluation", mock.Anything, mock.Anything).Return(model.AccessDecision{Decision: true}, (*model.AppError)(nil))
original := th.App.Srv().Channels().AccessControl
th.App.Srv().Channels().AccessControl = mockACS
t.Cleanup(func() { th.App.Srv().Channels().AccessControl = original })
download := model.AccessControlPolicyActionDownloadFileAttachment
assert.False(t, th.App.HasPermissionToChannelAction(rctx, f.bot.Id, f.bot.Roles, f.botDM.Id, download), "ABAC allow cannot override scope denial")
assert.False(t, th.App.HasPermissionToChannelAction(rctx, f.bot.Id, f.bot.Roles, f.memberPriv.Id, download))
assert.False(t, th.App.HasPermissionToChannelAction(rctx, f.bot.Id, f.bot.Roles, f.triggering.Id, model.AccessControlPolicyActionUploadFileAttachment), "writes are denied")
assert.True(t, th.App.HasPermissionToChannelAction(rctx, f.bot.Id, f.bot.Roles, f.triggering.Id, download))
mockDeny := &einterfacesmocks.AccessControlServiceInterface{}
mockDeny.On("AccessEvaluation", mock.Anything, mock.Anything).Return(model.AccessDecision{Decision: false}, (*model.AppError)(nil))
th.App.Srv().Channels().AccessControl = mockDeny
assert.False(t, th.App.HasPermissionToChannelAction(rctx, f.bot.Id, f.bot.Roles, f.triggering.Id, download), "ABAC can still deny in-scope channels")
th.App.Srv().Channels().AccessControl = nil
assert.False(t, th.App.HasPermissionToChannelAction(rctx, f.bot.Id, f.bot.Roles, f.botDM.Id, download), "scope applies without ABAC")
}
func TestCreateScopedSession(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
f := newScopeFixture(t, th)
expiresAt := model.GetMillis() + 30*60*1000
t.Run("success", func(t *testing.T) {
scope := f.scope()
scope.Resources.PublicChannelTeamIDs = []string{f.closedTeam.Id}
scope.CreatedByPluginID = "forged"
session, appErr := th.App.CreateScopedSession(th.Context, scopeTestPluginID, f.bot.Id, scope, expiresAt)
require.Nil(t, appErr)
assert.NotEmpty(t, session.Token)
assert.Equal(t, expiresAt, session.ExpiresAt)
assert.False(t, session.Local)
assert.True(t, session.IsBotUser())
stored, appErr := th.App.GetSession(session.Token)
require.Nil(t, appErr)
got, scoped := stored.Scope()
require.True(t, scoped)
assert.Equal(t, scopeTestPluginID, got.CreatedByPluginID, "server sets the plugin ID")
assert.Equal(t, scope.Resources, got.Resources)
assert.Equal(t, scope.OnBehalfOfUserID, got.OnBehalfOfUserID)
assert.Equal(t, "forged", scope.CreatedByPluginID, "caller's scope is not mutated")
})
for name, tc := range map[string]struct {
pluginID string
userID string
scope func() *model.SessionScope
expiresAt int64
}{
"no plugin": {"", f.bot.Id, f.scope, expiresAt},
"nil scope": {scopeTestPluginID, f.bot.Id, func() *model.SessionScope { return nil }, expiresAt},
"write permission": {scopeTestPluginID, f.bot.Id, func() *model.SessionScope {
s := f.scope()
s.Permissions = append(s.Permissions, model.PermissionCreatePost.Id)
return s
}, expiresAt},
"no permissions": {scopeTestPluginID, f.bot.Id, func() *model.SessionScope { s := f.scope(); s.Permissions = nil; return s }, expiresAt},
"DM channel": {scopeTestPluginID, f.bot.Id, func() *model.SessionScope { s := f.scope(); s.Resources.ChannelIDs = []string{f.botDM.Id}; return s }, expiresAt},
"GM channel": {scopeTestPluginID, f.bot.Id, func() *model.SessionScope {
s := f.scope()
s.Resources.ChannelIDs = []string{th.CreateGroupChannel(t, th.BasicUser2, f.bot).Id}
return s
}, expiresAt},
"missing channel": {scopeTestPluginID, f.bot.Id, func() *model.SessionScope { s := f.scope(); s.Resources.ChannelIDs = []string{model.NewId()}; return s }, expiresAt},
"expired": {scopeTestPluginID, f.bot.Id, f.scope, model.GetMillis() - 1},
"beyond 24h": {scopeTestPluginID, f.bot.Id, f.scope, model.GetMillis() + model.SessionScopeMaxLifetimeMillis + 60*1000},
"missing user": {scopeTestPluginID, model.NewId(), f.scope, expiresAt},
"missing team": {scopeTestPluginID, f.bot.Id, func() *model.SessionScope {
s := f.scope()
s.Resources.PublicChannelTeamIDs = []string{f.closedTeam.Id, model.NewId()}
return s
}, expiresAt},
"invalid team ID": {scopeTestPluginID, f.bot.Id, func() *model.SessionScope {
s := f.scope()
s.Resources.PublicChannelTeamIDs = []string{"x"}
return s
}, expiresAt},
"invalid behalf of": {scopeTestPluginID, f.bot.Id, func() *model.SessionScope { s := f.scope(); s.OnBehalfOfUserID = "x"; return s }, expiresAt},
} {
t.Run(name, func(t *testing.T) {
session, appErr := th.App.CreateScopedSession(th.Context, tc.pluginID, tc.userID, tc.scope(), tc.expiresAt)
require.NotNil(t, appErr)
assert.Nil(t, session)
})
}
t.Run("deactivated user", func(t *testing.T) {
user := th.CreateUser(t)
_, appErr := th.App.UpdateActive(th.Context, user, false)
require.Nil(t, appErr)
_, appErr = th.App.CreateScopedSession(th.Context, scopeTestPluginID, user.Id, f.scope(), expiresAt)
require.NotNil(t, appErr)
})
t.Run("CreateSession rejects a scope prop", func(t *testing.T) {
session := &model.Session{UserId: f.bot.Id, Props: model.StringMap{model.SessionPropScope: `{"permissions":["read_channel"],"created_by_plugin_id":"x"}`}}
_, appErr := th.App.CreateSession(th.Context, session)
require.NotNil(t, appErr)
assert.Equal(t, "app.session.create.scope_prop.app_error", appErr.Id)
assert.Equal(t, http.StatusBadRequest, appErr.StatusCode)
})
t.Run("activity does not extend a scoped session", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ExtendSessionLengthWithActivity = true })
session := mintScoped(t, th, f.bot.Id, f.scope())
assert.False(t, th.App.ExtendSessionExpiryIfNeeded(th.Context, session))
})
}
func TestRevokeScopedSessionsForPlugin(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
f := newScopeFixture(t, th)
expiresAt := model.GetMillis() + 30*60*1000
mine, appErr := th.App.CreateScopedSession(th.Context, scopeTestPluginID, f.bot.Id, f.scope(), expiresAt)
require.Nil(t, appErr)
theirs, appErr := th.App.CreateScopedSession(th.Context, "com.example.other", f.bot.Id, f.scope(), expiresAt)
require.Nil(t, appErr)
unscoped, appErr := th.App.CreateSession(th.Context, &model.Session{UserId: f.bot.Id})
require.Nil(t, appErr)
require.Nil(t, th.App.RevokeScopedSessionsForPlugin(th.Context, scopeTestPluginID))
_, appErr = th.App.GetSession(mine.Token)
assert.NotNil(t, appErr, "the plugin's scoped session is revoked")
_, appErr = th.App.GetSession(theirs.Token)
assert.Nil(t, appErr, "another plugin's scoped session survives")
_, appErr = th.App.GetSession(unscoped.Token)
assert.Nil(t, appErr, "unscoped sessions survive")
t.Run("a failed revocation doesn't stop the rest", func(t *testing.T) {
// An OAuth session without access data fails to revoke. It sorts first.
broken := &model.Session{UserId: f.bot.Id, IsOAuth: true, ExpiresAt: expiresAt}
broken.AddProp(model.SessionPropScope, mine.Props[model.SessionPropScope])
_, err := th.App.Srv().Store().Session().Save(th.Context, broken)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
next := mintScoped(t, th, f.bot.Id, f.scope())
appErr := th.App.RevokeScopedSessionsForPlugin(th.Context, scopeTestPluginID)
require.NotNil(t, appErr)
assert.Equal(t, "app.session.revoke_scoped.app_error", appErr.Id)
_, appErr = th.App.GetSession(next.Token)
assert.NotNil(t, appErr, "the session after the failure is revoked")
})
t.Run("all scoped sessions", func(t *testing.T) {
require.Nil(t, th.App.RevokeAllScopedSessions(th.Context))
_, appErr := th.App.GetSession(theirs.Token)
assert.NotNil(t, appErr)
_, appErr = th.App.GetSession(unscoped.Token)
assert.Nil(t, appErr, "unscoped sessions survive")
})
}
func TestScopedSessionsRevokedWhenPluginsAreTurnedOff(t *testing.T) {
th := Setup(t).InitBasic(t)
f := newScopeFixture(t, th)
session := mintScoped(t, th, f.bot.Id, f.scope())
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PluginSettings.Enable = false })
require.Eventually(t, func() bool {
_, appErr := th.App.GetSession(session.Token)
return appErr != nil
}, 10*time.Second, 100*time.Millisecond)
}
func TestScopedSessionIsUnauthenticatedOnPluginRoutes(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
f := newScopeFixture(t, th)
session := mintScoped(t, th, f.bot.Id, f.scope())
for name, setAuth := range map[string]func(r *http.Request){
"bearer header": func(r *http.Request) { r.Header.Set(model.HeaderAuth, model.HeaderBearer+" "+session.Token) },
"cookie": func(r *http.Request) { r.AddCookie(&http.Cookie{Name: model.SessionCookieToken, Value: session.Token}) },
"query": func(r *http.Request) { r.URL.RawQuery = "access_token=" + session.Token },
} {
t.Run(name, func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/plugins/testplugin/endpoint", nil)
req = mux.SetURLVars(req, map[string]string{"plugin_id": "testplugin"})
setAuth(req)
handlerCalled := false
th.App.ch.servePluginRequest(httptest.NewRecorder(), req, func(ctx *plugin.Context, w http.ResponseWriter, r *http.Request) {
handlerCalled = true
assert.Empty(t, r.Header.Get("Mattermost-User-Id"))
assert.Empty(t, ctx.SessionId)
assert.Empty(t, r.Header.Get(model.HeaderAuth), "the token is not forwarded to the plugin")
assert.Empty(t, r.URL.Query().Get("access_token"))
})
require.True(t, handlerCalled)
})
}
}