mirror of
https://github.com/hanzoai/team.git
synced 2026-09-30 01:57:35 +00:00
The server module named github.com/hanzoai/team/server/v8 — a path that resolves to the retired workspace's repository, not this one — while server/public still published github.com/mattermost/mattermost/server/public. Both are github.com/hanzoteam/server/... now, which is where this code actually lives. That second half is what made the binary unbootable. Upstream's public path stayed claimed by upstream, mattermost-plugin-ai imported it, and the build linked public/model twice; public/plugin's gob.Register then ran twice under one name and panicked at init, before main. Reverting the rename traded the brand for a working binary. Rebranding the plugin alongside it needs no such trade: the agents fork is github.com/hanzoteam/agents/v2 against our public module, so no module named mattermost/mattermost is in the graph at all. Measured: `go list -deps ./cmd/mattermost` names exactly one public/model, and it is ours.
500 lines
20 KiB
Go
500 lines
20 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package app
|
|
|
|
import (
|
|
"net/http"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/mock"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
"github.com/hanzoteam/server/server/v8/einterfaces/mocks"
|
|
)
|
|
|
|
// setupTeamABACEnforcement returns a TestHelper with the team ABAC kill switch,
|
|
// license, and config all enabled — the only state under which the JoinUserToTeam
|
|
// gate engages. The feature flag must be set at setup time because the config
|
|
// store treats the FeatureFlags section as read-only at runtime.
|
|
func setupTeamABACEnforcement(t *testing.T) *TestHelper {
|
|
th := SetupConfig(t, func(cfg *model.Config) {
|
|
cfg.FeatureFlags.TeamMembershipAccessControl = true
|
|
}).InitBasic(t)
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.AccessControlSettings.EnableAttributeBasedAccessControl = true
|
|
})
|
|
require.True(t, th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced)))
|
|
t.Cleanup(func() { th.App.Srv().SetLicense(nil) })
|
|
return th
|
|
}
|
|
|
|
// setupTeamABACPrereqsFlagOff enables the license and config prerequisites but
|
|
// leaves the TeamMembershipAccessControl flag off, so a governed team exercises
|
|
// the kill-switch branch with everything else in place.
|
|
func setupTeamABACPrereqsFlagOff(t *testing.T) *TestHelper {
|
|
th := SetupConfig(t, func(cfg *model.Config) {
|
|
cfg.FeatureFlags.TeamMembershipAccessControl = false
|
|
}).InitBasic(t)
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.AccessControlSettings.EnableAttributeBasedAccessControl = true
|
|
})
|
|
require.True(t, th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced)))
|
|
t.Cleanup(func() { th.App.Srv().SetLicense(nil) })
|
|
return th
|
|
}
|
|
|
|
func setMockACS(t *testing.T, th *TestHelper) *mocks.AccessControlServiceInterface {
|
|
t.Helper()
|
|
mockACS := &mocks.AccessControlServiceInterface{}
|
|
th.App.Srv().ch.AccessControl = mockACS
|
|
t.Cleanup(func() { th.App.Srv().ch.AccessControl = nil })
|
|
return mockACS
|
|
}
|
|
|
|
func TestJoinUserToTeamAccessControlEnforcement(t *testing.T) {
|
|
th := setupTeamABACEnforcement(t)
|
|
|
|
// newEnforcedTeam creates a team governed by an active membership policy.
|
|
newEnforcedTeam := func(t *testing.T) *model.Team {
|
|
t.Helper()
|
|
team := th.CreateTeam(t)
|
|
saveTestTeamPolicy(t, th, team.Id, model.AccessControlPolicyActionMembership)
|
|
return team
|
|
}
|
|
|
|
t.Run("qualifying user is admitted", func(t *testing.T) {
|
|
team := newEnforcedTeam(t)
|
|
user := th.CreateUser(t)
|
|
acs := setMockACS(t, th)
|
|
acs.On("AccessEvaluation", mock.Anything, mock.Anything).Return(model.AccessDecision{Decision: true}, (*model.AppError)(nil))
|
|
|
|
_, _, appErr := th.App.AddUserToTeam(th.Context, team.Id, user.Id, "")
|
|
require.Nil(t, appErr)
|
|
acs.AssertExpectations(t)
|
|
})
|
|
|
|
t.Run("non-qualifying user is denied with a generic 403 carrying no policy detail", func(t *testing.T) {
|
|
team := newEnforcedTeam(t)
|
|
user := th.CreateUser(t)
|
|
acs := setMockACS(t, th)
|
|
acs.On("AccessEvaluation", mock.Anything, mock.Anything).Return(model.AccessDecision{Decision: false}, (*model.AppError)(nil))
|
|
|
|
_, _, appErr := th.App.AddUserToTeam(th.Context, team.Id, user.Id, "")
|
|
require.NotNil(t, appErr)
|
|
require.Equal(t, "api.team.add_user.to.team.rejected", appErr.Id)
|
|
require.Equal(t, http.StatusForbidden, appErr.StatusCode)
|
|
// No-enumeration: the denial must carry no resource- or policy-specific
|
|
// detail that could be used to probe attributes.
|
|
require.Empty(t, appErr.DetailedError)
|
|
})
|
|
|
|
t.Run("two distinct failing policies produce byte-identical denials (no enumeration)", func(t *testing.T) {
|
|
teamA := newEnforcedTeam(t)
|
|
teamB := newEnforcedTeam(t)
|
|
userA := th.CreateUser(t)
|
|
userB := th.CreateUser(t)
|
|
acs := setMockACS(t, th)
|
|
acs.On("AccessEvaluation", mock.Anything, mock.Anything).Return(model.AccessDecision{Decision: false}, (*model.AppError)(nil))
|
|
|
|
_, _, errA := th.App.AddUserToTeam(th.Context, teamA.Id, userA.Id, "")
|
|
_, _, errB := th.App.AddUserToTeam(th.Context, teamB.Id, userB.Id, "")
|
|
require.NotNil(t, errA)
|
|
require.NotNil(t, errB)
|
|
require.Equal(t, errA.Id, errB.Id)
|
|
require.Equal(t, errA.Message, errB.Message)
|
|
require.Equal(t, errA.DetailedError, errB.DetailedError)
|
|
require.Equal(t, errA.StatusCode, errB.StatusCode)
|
|
})
|
|
|
|
t.Run("PDP evaluation error is propagated (fail-closed, never open)", func(t *testing.T) {
|
|
team := newEnforcedTeam(t)
|
|
user := th.CreateUser(t)
|
|
acs := setMockACS(t, th)
|
|
evalErr := model.NewAppError("AccessEvaluation", "pdp.boom", nil, "", http.StatusInternalServerError)
|
|
acs.On("AccessEvaluation", mock.Anything, mock.Anything).Return(model.AccessDecision{}, evalErr)
|
|
|
|
_, _, appErr := th.App.AddUserToTeam(th.Context, team.Id, user.Id, "")
|
|
require.NotNil(t, appErr, "an eval error must deny the join, not admit it")
|
|
|
|
// The user must not have been added.
|
|
_, memberErr := th.App.GetTeamMember(th.Context, team.Id, user.Id)
|
|
require.NotNil(t, memberErr)
|
|
})
|
|
|
|
t.Run("unavailable PDP denies a governed join (no silent allow)", func(t *testing.T) {
|
|
team := newEnforcedTeam(t)
|
|
user := th.CreateUser(t)
|
|
// No AccessControl service wired, yet license/config/flag and a policy
|
|
// are all present: the team is governed but unevaluable → deny.
|
|
th.App.Srv().ch.AccessControl = nil
|
|
|
|
_, _, appErr := th.App.AddUserToTeam(th.Context, team.Id, user.Id, "")
|
|
require.NotNil(t, appErr)
|
|
require.Equal(t, "api.team.add_user.to.team.rejected", appErr.Id)
|
|
require.Equal(t, http.StatusForbidden, appErr.StatusCode)
|
|
})
|
|
|
|
t.Run("system admin who fails the policy is denied (no role bypass)", func(t *testing.T) {
|
|
team := newEnforcedTeam(t)
|
|
acs := setMockACS(t, th)
|
|
acs.On("AccessEvaluation", mock.Anything, mock.Anything).Return(model.AccessDecision{Decision: false}, (*model.AppError)(nil))
|
|
|
|
_, _, appErr := th.App.AddUserToTeam(th.Context, team.Id, th.SystemAdminUser.Id, "")
|
|
require.NotNil(t, appErr)
|
|
require.Equal(t, "api.team.add_user.to.team.rejected", appErr.Id)
|
|
})
|
|
|
|
t.Run("every programmatic add entry point funnels through the gate", func(t *testing.T) {
|
|
acs := setMockACS(t, th)
|
|
acs.On("AccessEvaluation", mock.Anything, mock.Anything).Return(model.AccessDecision{Decision: false}, (*model.AppError)(nil))
|
|
|
|
t.Run("AddUserToTeam", func(t *testing.T) {
|
|
team := newEnforcedTeam(t)
|
|
user := th.CreateUser(t)
|
|
_, _, appErr := th.App.AddUserToTeam(th.Context, team.Id, user.Id, "")
|
|
require.NotNil(t, appErr)
|
|
require.Equal(t, "api.team.add_user.to.team.rejected", appErr.Id)
|
|
})
|
|
|
|
t.Run("AddUserToTeamByTeamId", func(t *testing.T) {
|
|
team := newEnforcedTeam(t)
|
|
user := th.CreateUser(t)
|
|
appErr := th.App.AddUserToTeamByTeamId(th.Context, team.Id, user)
|
|
require.NotNil(t, appErr)
|
|
require.Equal(t, "api.team.add_user.to.team.rejected", appErr.Id)
|
|
})
|
|
|
|
t.Run("AddUserToTeamByToken", func(t *testing.T) {
|
|
team := newEnforcedTeam(t)
|
|
user := th.CreateUser(t)
|
|
token := model.NewToken(
|
|
model.TokenTypeTeamInvitation,
|
|
model.MapToJSON(map[string]string{"teamId": team.Id}),
|
|
)
|
|
require.NoError(t, th.App.Srv().Store().Token().Save(token))
|
|
defer func() { _ = th.App.DeleteToken(token) }()
|
|
|
|
_, _, appErr := th.App.AddUserToTeamByToken(th.Context, user.Id, token.Token)
|
|
require.NotNil(t, appErr)
|
|
require.Equal(t, "api.team.add_user.to.team.rejected", appErr.Id)
|
|
})
|
|
|
|
t.Run("AddUserToTeamByInviteId", func(t *testing.T) {
|
|
team := newEnforcedTeam(t)
|
|
user := th.CreateUser(t)
|
|
_, _, appErr := th.App.AddUserToTeamByInviteId(th.Context, team.InviteId, user.Id)
|
|
require.NotNil(t, appErr)
|
|
require.Equal(t, "api.team.add_user.to.team.rejected", appErr.Id)
|
|
})
|
|
})
|
|
|
|
t.Run("batch add surfaces ABAC denial per-user without aborting the batch (graceful)", func(t *testing.T) {
|
|
team := newEnforcedTeam(t)
|
|
denied := th.CreateUser(t)
|
|
admitted := th.CreateUser(t)
|
|
acs := setMockACS(t, th)
|
|
acs.On("AccessEvaluation", mock.Anything, mock.MatchedBy(func(req model.AccessRequest) bool {
|
|
return req.Subject.ID == admitted.Id
|
|
})).Return(model.AccessDecision{Decision: true}, (*model.AppError)(nil))
|
|
acs.On("AccessEvaluation", mock.Anything, mock.MatchedBy(func(req model.AccessRequest) bool {
|
|
return req.Subject.ID == denied.Id
|
|
})).Return(model.AccessDecision{Decision: false}, (*model.AppError)(nil))
|
|
|
|
results, appErr := th.App.AddTeamMembers(th.Context, team.Id, []string{denied.Id, admitted.Id}, "", true)
|
|
require.Nil(t, appErr, "graceful batch must not abort on a per-user denial")
|
|
|
|
byUser := map[string]*model.TeamMemberWithError{}
|
|
for _, r := range results {
|
|
byUser[r.UserId] = r
|
|
}
|
|
require.NotNil(t, byUser[denied.Id].Error, "denied user must carry an error entry")
|
|
require.Equal(t, "api.team.add_user.to.team.rejected", byUser[denied.Id].Error.Id)
|
|
require.Nil(t, byUser[admitted.Id].Error, "qualifying user must be added")
|
|
})
|
|
|
|
t.Run("join to a non-ABAC team is unaffected (no PDP call)", func(t *testing.T) {
|
|
team := th.CreateTeam(t) // no policy assigned
|
|
user := th.CreateUser(t)
|
|
acs := setMockACS(t, th)
|
|
// AccessEvaluation intentionally not stubbed: it must never be called.
|
|
|
|
_, _, appErr := th.App.AddUserToTeam(th.Context, team.Id, user.Id, "")
|
|
require.Nil(t, appErr)
|
|
acs.AssertNotCalled(t, "AccessEvaluation", mock.Anything, mock.Anything)
|
|
})
|
|
|
|
t.Run("public governed team admits any user without consulting the PDP (advisory mode)", func(t *testing.T) {
|
|
team := th.CreateTeam(t)
|
|
team.AllowOpenInvite = true // public → advisory: the policy must not gate join
|
|
team, appErr := th.App.UpdateTeam(team)
|
|
require.Nil(t, appErr)
|
|
saveTestTeamPolicy(t, th, team.Id, model.AccessControlPolicyActionMembership)
|
|
user := th.CreateUser(t)
|
|
acs := setMockACS(t, th)
|
|
// AccessEvaluation intentionally not stubbed: advisory mode must never call it.
|
|
|
|
_, _, appErr = th.App.AddUserToTeam(th.Context, team.Id, user.Id, "")
|
|
require.Nil(t, appErr)
|
|
acs.AssertNotCalled(t, "AccessEvaluation", mock.Anything, mock.Anything)
|
|
})
|
|
}
|
|
|
|
func TestFilterNonQualifyingTeamsForUser(t *testing.T) {
|
|
th := setupTeamABACEnforcement(t)
|
|
|
|
governedTeam := func(t *testing.T) *model.Team {
|
|
t.Helper()
|
|
team := th.CreateTeam(t)
|
|
saveTestTeamPolicy(t, th, team.Id, model.AccessControlPolicyActionMembership)
|
|
reloaded, err := th.App.Srv().Store().Team().Get(team.Id)
|
|
require.NoError(t, err)
|
|
require.True(t, reloaded.PolicyEnforced, "store must report the team as policy-enforced")
|
|
return reloaded
|
|
}
|
|
|
|
t.Run("feature flag off is a no-op pass-through", func(t *testing.T) {
|
|
// Prerequisites on, flag off, team governed by a policy: only the
|
|
// kill switch keeps this a pass-through.
|
|
thOff := setupTeamABACPrereqsFlagOff(t)
|
|
team := thOff.CreateTeam(t)
|
|
saveTestTeamPolicy(t, thOff, team.Id, model.AccessControlPolicyActionMembership)
|
|
reloaded, err := thOff.App.Srv().Store().Team().Get(team.Id)
|
|
require.NoError(t, err)
|
|
require.True(t, reloaded.PolicyEnforced, "team must be policy-governed so only the flag keeps it a pass-through")
|
|
user := thOff.CreateUser(t)
|
|
out, dropped, appErr := thOff.App.FilterNonQualifyingTeamsForUser(thOff.Context, []*model.Team{reloaded}, user.Id)
|
|
require.Nil(t, appErr)
|
|
require.Zero(t, dropped)
|
|
require.Len(t, out, 1)
|
|
})
|
|
|
|
t.Run("non-governed teams are always returned", func(t *testing.T) {
|
|
team := th.CreateTeam(t)
|
|
user := th.CreateUser(t)
|
|
acs := setMockACS(t, th)
|
|
out, dropped, appErr := th.App.FilterNonQualifyingTeamsForUser(th.Context, []*model.Team{team}, user.Id)
|
|
require.Nil(t, appErr)
|
|
require.Zero(t, dropped)
|
|
require.Len(t, out, 1)
|
|
acs.AssertNotCalled(t, "AccessEvaluation", mock.Anything, mock.Anything)
|
|
})
|
|
|
|
t.Run("qualifying non-member sees a governed team", func(t *testing.T) {
|
|
team := governedTeam(t)
|
|
user := th.CreateUser(t)
|
|
acs := setMockACS(t, th)
|
|
acs.On("AccessEvaluation", mock.Anything, mock.Anything).Return(model.AccessDecision{Decision: true}, (*model.AppError)(nil))
|
|
|
|
out, dropped, appErr := th.App.FilterNonQualifyingTeamsForUser(th.Context, []*model.Team{team}, user.Id)
|
|
require.Nil(t, appErr)
|
|
require.Zero(t, dropped)
|
|
require.Len(t, out, 1)
|
|
})
|
|
|
|
t.Run("non-qualifying non-member has the governed team hidden", func(t *testing.T) {
|
|
team := governedTeam(t)
|
|
user := th.CreateUser(t)
|
|
acs := setMockACS(t, th)
|
|
acs.On("AccessEvaluation", mock.Anything, mock.Anything).Return(model.AccessDecision{Decision: false}, (*model.AppError)(nil))
|
|
|
|
out, dropped, appErr := th.App.FilterNonQualifyingTeamsForUser(th.Context, []*model.Team{team}, user.Id)
|
|
require.Nil(t, appErr)
|
|
require.Equal(t, 1, dropped)
|
|
require.Empty(t, out)
|
|
})
|
|
|
|
t.Run("existing member keeps visibility even when they no longer qualify", func(t *testing.T) {
|
|
team := governedTeam(t)
|
|
user := th.CreateUser(t)
|
|
// Add the user directly through the store to bypass the join gate.
|
|
_, err := th.App.Srv().Store().Team().SaveMember(th.Context, &model.TeamMember{TeamId: team.Id, UserId: user.Id}, *th.App.Config().TeamSettings.MaxUsersPerTeam)
|
|
require.NoError(t, err)
|
|
|
|
acs := setMockACS(t, th)
|
|
// Decision intentionally not stubbed for this team: members short-circuit
|
|
// before any PDP call.
|
|
|
|
out, dropped, appErr := th.App.FilterNonQualifyingTeamsForUser(th.Context, []*model.Team{team}, user.Id)
|
|
require.Nil(t, appErr)
|
|
require.Zero(t, dropped)
|
|
require.Len(t, out, 1)
|
|
acs.AssertNotCalled(t, "AccessEvaluation", mock.Anything, mock.Anything)
|
|
})
|
|
|
|
t.Run("PDP error hides the team (fail-secure)", func(t *testing.T) {
|
|
team := governedTeam(t)
|
|
user := th.CreateUser(t)
|
|
acs := setMockACS(t, th)
|
|
acs.On("AccessEvaluation", mock.Anything, mock.Anything).
|
|
Return(model.AccessDecision{}, model.NewAppError("AccessEvaluation", "boom", nil, "", http.StatusInternalServerError))
|
|
|
|
out, dropped, appErr := th.App.FilterNonQualifyingTeamsForUser(th.Context, []*model.Team{team}, user.Id)
|
|
require.Nil(t, appErr, "a PDP error must not fail the whole listing")
|
|
require.Equal(t, 1, dropped)
|
|
require.Empty(t, out)
|
|
})
|
|
|
|
t.Run("public governed team stays visible to a non-qualifying non-member (advisory mode)", func(t *testing.T) {
|
|
team := th.CreateTeam(t)
|
|
team.AllowOpenInvite = true // public → advisory: the team is never hidden
|
|
updated, appErr := th.App.UpdateTeam(team)
|
|
require.Nil(t, appErr)
|
|
saveTestTeamPolicy(t, th, updated.Id, model.AccessControlPolicyActionMembership)
|
|
reloaded, err := th.App.Srv().Store().Team().Get(updated.Id)
|
|
require.NoError(t, err)
|
|
require.True(t, reloaded.PolicyEnforced)
|
|
require.True(t, reloaded.AllowOpenInvite)
|
|
|
|
user := th.CreateUser(t)
|
|
acs := setMockACS(t, th)
|
|
// Decision intentionally not stubbed: a public team is never evaluated for hiding.
|
|
|
|
out, dropped, appErr := th.App.FilterNonQualifyingTeamsForUser(th.Context, []*model.Team{reloaded}, user.Id)
|
|
require.Nil(t, appErr)
|
|
require.Zero(t, dropped)
|
|
require.Len(t, out, 1)
|
|
acs.AssertNotCalled(t, "AccessEvaluation", mock.Anything, mock.Anything)
|
|
})
|
|
|
|
t.Run("decisions are memoised per request (one eval for a repeated governed team)", func(t *testing.T) {
|
|
team := governedTeam(t)
|
|
user := th.CreateUser(t)
|
|
acs := setMockACS(t, th)
|
|
acs.On("AccessEvaluation", mock.Anything, mock.Anything).Return(model.AccessDecision{Decision: true}, (*model.AppError)(nil))
|
|
|
|
out, dropped, appErr := th.App.FilterNonQualifyingTeamsForUser(th.Context, []*model.Team{team, team, team}, user.Id)
|
|
require.Nil(t, appErr)
|
|
require.Zero(t, dropped)
|
|
require.Len(t, out, 3)
|
|
acs.AssertNumberOfCalls(t, "AccessEvaluation", 1)
|
|
})
|
|
}
|
|
|
|
func TestFilterNonQualifyingTeamsForUserErrorIsNotSwallowed(t *testing.T) {
|
|
// A subject-build/membership-lookup failure that is not a PDP error must
|
|
// surface so the handler fails the request rather than silently returning a
|
|
// partial list.
|
|
th := setupTeamABACEnforcement(t)
|
|
team := th.CreateTeam(t)
|
|
saveTestTeamPolicy(t, th, team.Id, model.AccessControlPolicyActionMembership)
|
|
reloaded, err := th.App.Srv().Store().Team().Get(team.Id)
|
|
require.NoError(t, err)
|
|
|
|
acs := setMockACS(t, th)
|
|
acs.On("AccessEvaluation", mock.Anything, mock.Anything).Return(model.AccessDecision{Decision: true}, (*model.AppError)(nil))
|
|
|
|
// A non-existent requesting user makes GetUser fail; that error must bubble.
|
|
_, _, appErr := th.App.FilterNonQualifyingTeamsForUser(th.Context, []*model.Team{reloaded}, model.NewId())
|
|
require.NotNil(t, appErr)
|
|
}
|
|
|
|
func TestAnnotateRecommendedTeamsForUser(t *testing.T) {
|
|
th := setupTeamABACEnforcement(t)
|
|
|
|
publicGoverned := func(t *testing.T) *model.Team {
|
|
t.Helper()
|
|
team := th.CreateTeam(t)
|
|
team.AllowOpenInvite = true // public → advisory: eligible for the recommended tag
|
|
updated, appErr := th.App.UpdateTeam(team)
|
|
require.Nil(t, appErr)
|
|
saveTestTeamPolicy(t, th, updated.Id, model.AccessControlPolicyActionMembership)
|
|
reloaded, err := th.App.Srv().Store().Team().Get(updated.Id)
|
|
require.NoError(t, err)
|
|
require.True(t, reloaded.PolicyEnforced)
|
|
require.True(t, reloaded.AllowOpenInvite)
|
|
return reloaded
|
|
}
|
|
|
|
privateGoverned := func(t *testing.T) *model.Team {
|
|
t.Helper()
|
|
team := th.CreateTeam(t) // AllowOpenInvite defaults false → private/strict
|
|
saveTestTeamPolicy(t, th, team.Id, model.AccessControlPolicyActionMembership)
|
|
reloaded, err := th.App.Srv().Store().Team().Get(team.Id)
|
|
require.NoError(t, err)
|
|
require.True(t, reloaded.PolicyEnforced)
|
|
require.False(t, reloaded.AllowOpenInvite)
|
|
return reloaded
|
|
}
|
|
|
|
t.Run("feature flag off is a no-op", func(t *testing.T) {
|
|
// Prerequisites on, flag off, public governed team (otherwise
|
|
// recommend-eligible): only the kill switch suppresses the tag.
|
|
thOff := setupTeamABACPrereqsFlagOff(t)
|
|
team := thOff.CreateTeam(t)
|
|
team.AllowOpenInvite = true
|
|
updated, appErr := thOff.App.UpdateTeam(team)
|
|
require.Nil(t, appErr)
|
|
saveTestTeamPolicy(t, thOff, updated.Id, model.AccessControlPolicyActionMembership)
|
|
reloaded, err := thOff.App.Srv().Store().Team().Get(updated.Id)
|
|
require.NoError(t, err)
|
|
require.True(t, reloaded.PolicyEnforced, "team must be policy-governed so only the flag suppresses the tag")
|
|
require.True(t, reloaded.AllowOpenInvite)
|
|
user := thOff.CreateUser(t)
|
|
thOff.App.AnnotateRecommendedTeamsForUser(thOff.Context, []*model.Team{reloaded}, user.Id)
|
|
require.False(t, reloaded.Recommended)
|
|
})
|
|
|
|
t.Run("non-governed team is never recommended and never evaluated", func(t *testing.T) {
|
|
team := th.CreateTeam(t)
|
|
user := th.CreateUser(t)
|
|
acs := setMockACS(t, th)
|
|
th.App.AnnotateRecommendedTeamsForUser(th.Context, []*model.Team{team}, user.Id)
|
|
require.False(t, team.Recommended)
|
|
acs.AssertNotCalled(t, "AccessEvaluation", mock.Anything, mock.Anything)
|
|
})
|
|
|
|
t.Run("private governed team is never recommended and never evaluated", func(t *testing.T) {
|
|
team := privateGoverned(t)
|
|
user := th.CreateUser(t)
|
|
acs := setMockACS(t, th)
|
|
th.App.AnnotateRecommendedTeamsForUser(th.Context, []*model.Team{team}, user.Id)
|
|
require.False(t, team.Recommended)
|
|
acs.AssertNotCalled(t, "AccessEvaluation", mock.Anything, mock.Anything)
|
|
})
|
|
|
|
t.Run("public governed team is recommended to a qualifying non-member", func(t *testing.T) {
|
|
team := publicGoverned(t)
|
|
user := th.CreateUser(t)
|
|
acs := setMockACS(t, th)
|
|
acs.On("AccessEvaluation", mock.Anything, mock.Anything).Return(model.AccessDecision{Decision: true}, (*model.AppError)(nil))
|
|
|
|
th.App.AnnotateRecommendedTeamsForUser(th.Context, []*model.Team{team}, user.Id)
|
|
require.True(t, team.Recommended)
|
|
})
|
|
|
|
t.Run("public governed team is not recommended to a non-qualifying non-member", func(t *testing.T) {
|
|
team := publicGoverned(t)
|
|
user := th.CreateUser(t)
|
|
acs := setMockACS(t, th)
|
|
acs.On("AccessEvaluation", mock.Anything, mock.Anything).Return(model.AccessDecision{Decision: false}, (*model.AppError)(nil))
|
|
|
|
th.App.AnnotateRecommendedTeamsForUser(th.Context, []*model.Team{team}, user.Id)
|
|
require.False(t, team.Recommended)
|
|
})
|
|
|
|
t.Run("PDP error leaves the team not recommended (fail-secure)", func(t *testing.T) {
|
|
team := publicGoverned(t)
|
|
user := th.CreateUser(t)
|
|
acs := setMockACS(t, th)
|
|
acs.On("AccessEvaluation", mock.Anything, mock.Anything).
|
|
Return(model.AccessDecision{}, model.NewAppError("AccessEvaluation", "boom", nil, "", http.StatusInternalServerError))
|
|
|
|
th.App.AnnotateRecommendedTeamsForUser(th.Context, []*model.Team{team}, user.Id)
|
|
require.False(t, team.Recommended)
|
|
})
|
|
|
|
t.Run("existing member is not recommended and is never evaluated", func(t *testing.T) {
|
|
team := publicGoverned(t)
|
|
user := th.CreateUser(t)
|
|
_, err := th.App.Srv().Store().Team().SaveMember(th.Context, &model.TeamMember{TeamId: team.Id, UserId: user.Id}, *th.App.Config().TeamSettings.MaxUsersPerTeam)
|
|
require.NoError(t, err)
|
|
|
|
acs := setMockACS(t, th)
|
|
th.App.AnnotateRecommendedTeamsForUser(th.Context, []*model.Team{team}, user.Id)
|
|
require.False(t, team.Recommended)
|
|
acs.AssertNotCalled(t, "AccessEvaluation", mock.Anything, mock.Anything)
|
|
})
|
|
}
|