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.
491 lines
20 KiB
Go
491 lines
20 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package api4
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"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"
|
|
)
|
|
|
|
// enableTeamABAC turns on the license + config the team ABAC paths require and
|
|
// injects a mock PDP, returning it for per-test stubbing. Feature flags are
|
|
// read-only by default in tests, so unlock them first — otherwise the
|
|
// TeamMembershipAccessControl toggle below is silently dropped.
|
|
func enableTeamABAC(t *testing.T, th *TestHelper) *mocks.AccessControlServiceInterface {
|
|
t.Helper()
|
|
require.True(t, th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced)))
|
|
th.ConfigStore.SetReadOnlyFF(false)
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.AccessControlSettings.EnableAttributeBasedAccessControl = true
|
|
cfg.FeatureFlags.TeamMembershipAccessControl = true
|
|
})
|
|
m := &mocks.AccessControlServiceInterface{}
|
|
th.App.Srv().Channels().AccessControl = m
|
|
t.Cleanup(func() {
|
|
th.App.Srv().Channels().AccessControl = nil
|
|
th.App.Srv().SetLicense(nil)
|
|
})
|
|
return m
|
|
}
|
|
|
|
// saveTeamMembershipPolicy persists an active team-type policy for teamID so
|
|
// PolicyEnforced/HasMembershipPolicyAction resolve true through the store.
|
|
func saveTeamMembershipPolicy(t *testing.T, th *TestHelper, teamID string) *model.AccessControlPolicy {
|
|
t.Helper()
|
|
policy := &model.AccessControlPolicy{
|
|
ID: teamID,
|
|
Type: model.AccessControlPolicyTypeTeam,
|
|
Name: "policy-" + teamID,
|
|
Active: true,
|
|
Revision: 1,
|
|
Version: model.AccessControlPolicyVersionV0_3,
|
|
Imports: []string{},
|
|
Rules: []model.AccessControlPolicyRule{
|
|
{Actions: []string{model.AccessControlPolicyActionMembership}, Expression: "true"},
|
|
},
|
|
}
|
|
_, err := th.App.Srv().Store().AccessControlPolicy().Save(th.Context, policy)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { _ = th.App.Srv().Store().AccessControlPolicy().Delete(th.Context, teamID) })
|
|
return policy
|
|
}
|
|
|
|
// assignBody is the additive request shape for the assign/unassign handlers.
|
|
type assignBody struct {
|
|
ChannelIds []string `json:"channel_ids,omitempty"`
|
|
TeamID string `json:"team_id,omitempty"`
|
|
TeamIds []string `json:"team_ids,omitempty"`
|
|
}
|
|
|
|
// doAssign/doUnassign return the HTTP status. The raw client helpers surface an
|
|
// error for any status >= 300, so we read the code off the response rather than
|
|
// asserting on err.
|
|
func doAssign(t *testing.T, client *model.Client4, policyID string, body assignBody) int {
|
|
t.Helper()
|
|
resp, _ := client.DoAPIPostJSON(context.Background(), "/access_control_policies/"+policyID+"/assign", body)
|
|
require.NotNil(t, resp)
|
|
defer resp.Body.Close()
|
|
return resp.StatusCode
|
|
}
|
|
|
|
func doUnassign(t *testing.T, client *model.Client4, policyID string, body assignBody) int {
|
|
t.Helper()
|
|
resp, _ := client.DoAPIDeleteJSON(context.Background(), "/access_control_policies/"+policyID+"/unassign", body)
|
|
require.NotNil(t, resp)
|
|
defer resp.Body.Close()
|
|
return resp.StatusCode
|
|
}
|
|
|
|
func TestAssignAccessPolicyTeamIds(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
parent := &model.AccessControlPolicy{
|
|
ID: model.NewId(),
|
|
Type: model.AccessControlPolicyTypeParent,
|
|
Version: model.AccessControlPolicyVersionV0_3,
|
|
Revision: 1,
|
|
Rules: []model.AccessControlPolicyRule{
|
|
{Expression: "true", Actions: []string{model.AccessControlPolicyActionMembership}},
|
|
},
|
|
}
|
|
|
|
t.Run("non-sysadmin sending team_ids is forbidden", func(t *testing.T) {
|
|
enableTeamABAC(t, th)
|
|
status := doAssign(t, th.Client, parent.ID, assignBody{TeamIds: []string{th.BasicTeam.Id}})
|
|
require.Equal(t, http.StatusForbidden, status)
|
|
})
|
|
|
|
t.Run("all-invalid team_ids returns 400 (not 404)", func(t *testing.T) {
|
|
enableTeamABAC(t, th)
|
|
status := doAssign(t, th.SystemAdminClient, parent.ID, assignBody{TeamIds: []string{model.NewId(), model.NewId()}})
|
|
require.Equal(t, http.StatusBadRequest, status)
|
|
})
|
|
|
|
t.Run("one invalid id among valid ones returns 400", func(t *testing.T) {
|
|
enableTeamABAC(t, th)
|
|
status := doAssign(t, th.SystemAdminClient, parent.ID, assignBody{TeamIds: []string{th.BasicTeam.Id, model.NewId()}})
|
|
require.Equal(t, http.StatusBadRequest, status)
|
|
})
|
|
|
|
t.Run("sysadmin team_ids assigns the policy", func(t *testing.T) {
|
|
m := enableTeamABAC(t, th)
|
|
notFound := model.NewAppError("GetPolicy", "app.access_control.not_found.app_error", nil, "", http.StatusNotFound)
|
|
child := &model.AccessControlPolicy{
|
|
ID: th.BasicTeam.Id,
|
|
Type: model.AccessControlPolicyTypeTeam,
|
|
Version: model.AccessControlPolicyVersionV0_3,
|
|
Imports: []string{parent.ID},
|
|
Props: map[string]any{},
|
|
}
|
|
m.On("GetPolicy", mock.AnythingOfType("*request.Context"), parent.ID).Return(parent, nil).Once()
|
|
m.On("GetPolicy", mock.AnythingOfType("*request.Context"), th.BasicTeam.Id).Return(nil, notFound).Once()
|
|
m.On("SavePolicy", mock.AnythingOfType("*request.Context"), mock.AnythingOfType("*model.AccessControlPolicy")).Return(child, nil).Once()
|
|
|
|
status := doAssign(t, th.SystemAdminClient, parent.ID, assignBody{TeamIds: []string{th.BasicTeam.Id}})
|
|
require.Equal(t, http.StatusOK, status)
|
|
m.AssertExpectations(t)
|
|
})
|
|
|
|
t.Run("team_ids is rejected with 501 when team membership ABAC is disabled", func(t *testing.T) {
|
|
enableTeamABAC(t, th)
|
|
// Dark-launch guard: even a sysadmin cannot create team child policy rows
|
|
// while the sub-flag is off, so they can't go live on a later flag flip.
|
|
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.TeamMembershipAccessControl = false })
|
|
status := doAssign(t, th.SystemAdminClient, parent.ID, assignBody{TeamIds: []string{th.BasicTeam.Id}})
|
|
require.Equal(t, http.StatusNotImplemented, status)
|
|
})
|
|
}
|
|
|
|
func TestUnassignAccessPolicyTeamIds(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
policyID := model.NewId()
|
|
|
|
t.Run("non-sysadmin sending team_ids is forbidden", func(t *testing.T) {
|
|
enableTeamABAC(t, th)
|
|
status := doUnassign(t, th.Client, policyID, assignBody{TeamIds: []string{th.BasicTeam.Id}})
|
|
require.Equal(t, http.StatusForbidden, status)
|
|
})
|
|
|
|
t.Run("unknown team_ids returns 400", func(t *testing.T) {
|
|
enableTeamABAC(t, th)
|
|
status := doUnassign(t, th.SystemAdminClient, policyID, assignBody{TeamIds: []string{model.NewId()}})
|
|
require.Equal(t, http.StatusBadRequest, status)
|
|
})
|
|
|
|
t.Run("sysadmin team_ids with no assigned child is a no-op", func(t *testing.T) {
|
|
enableTeamABAC(t, th)
|
|
// No child policy exists for the team, so UnassignPoliciesFromTeams skips it
|
|
// (warn + continue) and returns success.
|
|
status := doUnassign(t, th.SystemAdminClient, policyID, assignBody{TeamIds: []string{th.BasicTeam.Id}})
|
|
require.Equal(t, http.StatusOK, status)
|
|
})
|
|
|
|
t.Run("team_ids is rejected with 501 when team membership ABAC is disabled", func(t *testing.T) {
|
|
enableTeamABAC(t, th)
|
|
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.TeamMembershipAccessControl = false })
|
|
status := doUnassign(t, th.SystemAdminClient, policyID, assignBody{TeamIds: []string{th.BasicTeam.Id}})
|
|
require.Equal(t, http.StatusNotImplemented, status)
|
|
})
|
|
}
|
|
|
|
func TestGetTeamAccessControlPolicy(t *testing.T) {
|
|
th := SetupConfig(t, func(cfg *model.Config) {
|
|
cfg.FeatureFlags.TeamMembershipAccessControl = true
|
|
}).InitBasic(t)
|
|
|
|
getPolicy := func(t *testing.T, client *model.Client4, teamID string) (*http.Response, error) {
|
|
t.Helper()
|
|
return client.DoAPIGet(context.Background(), "/teams/"+teamID+"/access_control/policy", "")
|
|
}
|
|
|
|
t.Run("regular member without permission is forbidden", func(t *testing.T) {
|
|
enableTeamABAC(t, th)
|
|
resp, _ := getPolicy(t, th.Client, th.BasicTeam.Id)
|
|
require.NotNil(t, resp)
|
|
defer resp.Body.Close()
|
|
require.Equal(t, http.StatusForbidden, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("sysadmin sees enforced=false when no policy is assigned", func(t *testing.T) {
|
|
m := enableTeamABAC(t, th)
|
|
notFound := model.NewAppError("GetPolicy", "app.access_control.not_found.app_error", nil, "", http.StatusNotFound)
|
|
m.On("GetPolicy", mock.AnythingOfType("*request.Context"), th.BasicTeam.Id).Return(nil, notFound)
|
|
|
|
resp, err := getPolicy(t, th.SystemAdminClient, th.BasicTeam.Id)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
require.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
var body struct {
|
|
Policy *model.AccessControlPolicy `json:"policy"`
|
|
Enforced bool `json:"enforced"`
|
|
}
|
|
require.NoError(t, json.NewDecoder(resp.Body).Decode(&body))
|
|
require.Nil(t, body.Policy)
|
|
require.False(t, body.Enforced)
|
|
})
|
|
|
|
t.Run("sysadmin sees enforced=true with the policy when assigned", func(t *testing.T) {
|
|
m := enableTeamABAC(t, th)
|
|
policy := saveTeamMembershipPolicy(t, th, th.BasicTeam.Id)
|
|
m.On("GetPolicy", mock.AnythingOfType("*request.Context"), th.BasicTeam.Id).Return(policy, nil)
|
|
|
|
resp, err := getPolicy(t, th.SystemAdminClient, th.BasicTeam.Id)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
require.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
var body struct {
|
|
Policy *model.AccessControlPolicy `json:"policy"`
|
|
Enforced bool `json:"enforced"`
|
|
}
|
|
require.NoError(t, json.NewDecoder(resp.Body).Decode(&body))
|
|
require.NotNil(t, body.Policy)
|
|
require.Equal(t, th.BasicTeam.Id, body.Policy.ID)
|
|
require.True(t, body.Enforced)
|
|
})
|
|
|
|
t.Run("team admin of the team is allowed", func(t *testing.T) {
|
|
m := enableTeamABAC(t, th)
|
|
notFound := model.NewAppError("GetPolicy", "app.access_control.not_found.app_error", nil, "", http.StatusNotFound)
|
|
m.On("GetPolicy", mock.AnythingOfType("*request.Context"), th.BasicTeam.Id).Return(nil, notFound)
|
|
|
|
th.AddPermissionToRole(t, model.PermissionManageTeamAccessRules.Id, model.TeamAdminRoleId)
|
|
defer th.RemovePermissionFromRole(t, model.PermissionManageTeamAccessRules.Id, model.TeamAdminRoleId)
|
|
th.LinkUserToTeam(t, th.TeamAdminUser, th.BasicTeam)
|
|
th.UpdateUserToTeamAdmin(t, th.TeamAdminUser, th.BasicTeam)
|
|
th.LoginTeamAdmin(t)
|
|
defer th.LoginBasic(t)
|
|
|
|
resp, err := getPolicy(t, th.Client, th.BasicTeam.Id)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
require.Equal(t, http.StatusOK, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("resolves imported parent policies into parent_policies for the banner", func(t *testing.T) {
|
|
m := enableTeamABAC(t, th)
|
|
|
|
// Team child policy that imports a system-level parent policy.
|
|
parentID := model.NewId()
|
|
teamPolicy := saveTeamMembershipPolicy(t, th, th.BasicTeam.Id)
|
|
teamPolicy.Imports = []string{parentID}
|
|
_, err := th.App.Srv().Store().AccessControlPolicy().Save(th.Context, teamPolicy)
|
|
require.NoError(t, err)
|
|
|
|
// The stored parent carries assignment metadata (scope, child_ids) that spans
|
|
// other teams — the response must not leak any of it.
|
|
parentPolicy := &model.AccessControlPolicy{
|
|
ID: parentID,
|
|
Type: model.AccessControlPolicyTypeParent,
|
|
Name: "Engineering",
|
|
Active: true,
|
|
Revision: 1,
|
|
Version: model.AccessControlPolicyVersionV0_3,
|
|
Scope: model.AccessControlPolicyScopeTeam,
|
|
ScopeID: model.NewId(),
|
|
Props: map[string]any{"child_ids": []string{model.NewId(), model.NewId()}},
|
|
Rules: []model.AccessControlPolicyRule{
|
|
{Actions: []string{model.AccessControlPolicyActionMembership}, Expression: `user.attributes.Department == "Engineering"`},
|
|
},
|
|
}
|
|
|
|
m.On("GetPolicy", mock.AnythingOfType("*request.Context"), th.BasicTeam.Id).Return(teamPolicy, nil)
|
|
m.On("GetPolicy", mock.AnythingOfType("*request.Context"), parentID).Return(parentPolicy, nil)
|
|
|
|
// AttributeValueMasking is on by default, so the handler masks parent
|
|
// expressions through the PDP. Pass them through unchanged here.
|
|
m.On("MaskExpressionForCaller", mock.Anything, mock.Anything, mock.Anything).
|
|
Return(`user.attributes.Department == "Engineering"`, false, (*model.AppError)(nil))
|
|
|
|
assertBanner := func(t *testing.T, client *model.Client4) {
|
|
t.Helper()
|
|
resp, err := getPolicy(t, client, th.BasicTeam.Id)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
require.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
var body struct {
|
|
Policy *model.AccessControlPolicy `json:"policy"`
|
|
Enforced bool `json:"enforced"`
|
|
ParentPolicies []*model.AccessControlPolicy `json:"parent_policies"`
|
|
}
|
|
require.NoError(t, json.NewDecoder(resp.Body).Decode(&body))
|
|
require.Len(t, body.ParentPolicies, 1)
|
|
got := body.ParentPolicies[0]
|
|
require.Equal(t, parentID, got.ID)
|
|
require.Equal(t, "Engineering", got.Name)
|
|
require.Len(t, got.Rules, 1)
|
|
require.Equal(t, `user.attributes.Department == "Engineering"`, got.Rules[0].Expression)
|
|
|
|
// Assignment metadata must be stripped.
|
|
require.Empty(t, got.Scope)
|
|
require.Empty(t, got.ScopeID)
|
|
require.Nil(t, got.Props)
|
|
}
|
|
|
|
// System admin gets the resolved parent — the pre-existing behavior.
|
|
t.Run("system admin", func(t *testing.T) {
|
|
assertBanner(t, th.SystemAdminClient)
|
|
})
|
|
|
|
// Team admin gets the SAME banner data, without needing to fetch the parent
|
|
// policy directly (which requires ManageSystem). This is the MM-70056 fix.
|
|
t.Run("team admin", func(t *testing.T) {
|
|
th.AddPermissionToRole(t, model.PermissionManageTeamAccessRules.Id, model.TeamAdminRoleId)
|
|
defer th.RemovePermissionFromRole(t, model.PermissionManageTeamAccessRules.Id, model.TeamAdminRoleId)
|
|
th.LinkUserToTeam(t, th.TeamAdminUser, th.BasicTeam)
|
|
th.UpdateUserToTeamAdmin(t, th.TeamAdminUser, th.BasicTeam)
|
|
th.LoginTeamAdmin(t)
|
|
defer th.LoginBasic(t)
|
|
|
|
assertBanner(t, th.Client)
|
|
})
|
|
})
|
|
|
|
t.Run("disabled flag hides a policy row created while dark", func(t *testing.T) {
|
|
enableTeamABAC(t, th)
|
|
// A child policy persisted while the feature was on must not leak through
|
|
// this endpoint once the sub-flag is turned off again (dark-launch/rollback).
|
|
saveTeamMembershipPolicy(t, th, th.BasicTeam.Id)
|
|
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.TeamMembershipAccessControl = false })
|
|
|
|
resp, err := getPolicy(t, th.SystemAdminClient, th.BasicTeam.Id)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
require.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
var body struct {
|
|
Policy *model.AccessControlPolicy `json:"policy"`
|
|
Enforced bool `json:"enforced"`
|
|
}
|
|
require.NoError(t, json.NewDecoder(resp.Body).Decode(&body))
|
|
require.Nil(t, body.Policy)
|
|
require.False(t, body.Enforced)
|
|
})
|
|
}
|
|
|
|
func TestGetUsersNotInTeamAbacMatchOnly(t *testing.T) {
|
|
th := SetupConfig(t, func(cfg *model.Config) {
|
|
cfg.FeatureFlags.TeamMembershipAccessControl = true
|
|
}).InitBasic(t)
|
|
|
|
t.Run("without abac_match_only the listing is unchanged", func(t *testing.T) {
|
|
users, resp, err := th.SystemAdminClient.GetUsersNotInTeam(context.Background(), th.BasicTeam.Id, 0, 200, "")
|
|
require.NoError(t, err)
|
|
CheckOKStatus(t, resp)
|
|
// BasicUser/BasicUser2 are team members, so they must not appear.
|
|
require.NotContains(t, pluckIDs(users, func(u *model.User) string { return u.Id }), th.BasicUser.Id)
|
|
})
|
|
|
|
t.Run("abac_match_only=true routes to the policy-matched candidates", func(t *testing.T) {
|
|
m := enableTeamABAC(t, th)
|
|
saveTeamMembershipPolicy(t, th, th.BasicTeam.Id)
|
|
|
|
qualifying := th.CreateUser(t)
|
|
m.On("QueryUsersForResource", mock.AnythingOfType("*request.Context"), th.BasicTeam.Id, model.AccessControlPolicyActionMembership, mock.Anything).
|
|
Return([]*model.User{qualifying}, int64(1), (*model.AppError)(nil))
|
|
|
|
resp, err := th.SystemAdminClient.DoAPIGet(context.Background(), "/users?not_in_team="+th.BasicTeam.Id+"&abac_match_only=true&per_page=200", "")
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
require.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
var users []*model.User
|
|
require.NoError(t, json.NewDecoder(resp.Body).Decode(&users))
|
|
require.Equal(t, []string{qualifying.Id}, pluckIDs(users, func(u *model.User) string { return u.Id }))
|
|
m.AssertExpectations(t)
|
|
})
|
|
|
|
t.Run("term query param is forwarded to the policy query", func(t *testing.T) {
|
|
m := enableTeamABAC(t, th)
|
|
saveTeamMembershipPolicy(t, th, th.BasicTeam.Id)
|
|
|
|
qualifying := th.CreateUser(t)
|
|
m.On("QueryUsersForResource", mock.AnythingOfType("*request.Context"), th.BasicTeam.Id, model.AccessControlPolicyActionMembership, mock.MatchedBy(func(opts model.SubjectSearchOptions) bool {
|
|
return opts.Term == "eng"
|
|
})).Return([]*model.User{qualifying}, int64(1), (*model.AppError)(nil))
|
|
|
|
resp, err := th.SystemAdminClient.DoAPIGet(context.Background(), "/users?not_in_team="+th.BasicTeam.Id+"&abac_match_only=true&term=eng&per_page=200", "")
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
require.Equal(t, http.StatusOK, resp.StatusCode)
|
|
m.AssertExpectations(t)
|
|
})
|
|
|
|
// The term search must not let a non-admin probe users' real names when
|
|
// PrivacySettings.ShowFullName is off — mirroring the normal user-search gate.
|
|
// System admin (asAdmin) is never restricted.
|
|
privacyCases := []struct {
|
|
name string
|
|
asAdmin bool
|
|
showFullName bool
|
|
wantExcludeNames bool
|
|
}{
|
|
{"non-admin with ShowFullName off excludes full names", false, false, true},
|
|
{"non-admin with ShowFullName on allows full names", false, true, false},
|
|
{"system admin always allows full names", true, false, false},
|
|
}
|
|
for _, tc := range privacyCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
m := enableTeamABAC(t, th)
|
|
saveTeamMembershipPolicy(t, th, th.BasicTeam.Id)
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.PrivacySettings.ShowFullName = model.NewPointer(tc.showFullName)
|
|
})
|
|
|
|
m.On("QueryUsersForResource", mock.AnythingOfType("*request.Context"), th.BasicTeam.Id, model.AccessControlPolicyActionMembership, mock.MatchedBy(func(opts model.SubjectSearchOptions) bool {
|
|
return opts.ExcludeFullNames == tc.wantExcludeNames
|
|
})).Return([]*model.User{}, int64(0), (*model.AppError)(nil))
|
|
|
|
// BasicUser is a member of BasicTeam, so it holds ViewTeam but is not a sysadmin.
|
|
client := th.Client
|
|
if tc.asAdmin {
|
|
client = th.SystemAdminClient
|
|
}
|
|
resp, err := client.DoAPIGet(context.Background(), "/users?not_in_team="+th.BasicTeam.Id+"&abac_match_only=true&term=smith&per_page=200", "")
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
require.Equal(t, http.StatusOK, resp.StatusCode)
|
|
m.AssertExpectations(t)
|
|
})
|
|
}
|
|
|
|
t.Run("caller without view_team permission is forbidden", func(t *testing.T) {
|
|
// Owned by the admin so BasicUser is a genuine non-member without ViewTeam.
|
|
otherTeam := th.CreateTeamWithClient(t, th.SystemAdminClient)
|
|
_, resp, err := th.Client.GetUsersNotInTeam(context.Background(), otherTeam.Id, 0, 60, "")
|
|
require.Error(t, err)
|
|
CheckForbiddenStatus(t, resp)
|
|
})
|
|
}
|
|
|
|
// TestAddTeamMembersGracefulABACError pins P1-21: a per-user ABAC denial in the
|
|
// batch add surfaces as that user's Error in graceful mode without aborting the
|
|
// batch; qualifying users in the same batch are still added.
|
|
func TestAddTeamMembersGracefulABACError(t *testing.T) {
|
|
th := SetupConfig(t, func(cfg *model.Config) {
|
|
cfg.FeatureFlags.TeamMembershipAccessControl = true
|
|
}).InitBasic(t)
|
|
|
|
m := enableTeamABAC(t, th)
|
|
team := th.CreateTeam(t)
|
|
saveTeamMembershipPolicy(t, th, team.Id)
|
|
|
|
admitted := th.CreateUser(t)
|
|
denied := th.CreateUser(t)
|
|
|
|
m.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))
|
|
m.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))
|
|
|
|
result, resp, err := th.SystemAdminClient.AddTeamMembersGracefully(context.Background(), team.Id, []string{admitted.Id, denied.Id})
|
|
require.NoError(t, err)
|
|
CheckCreatedStatus(t, resp)
|
|
|
|
var admittedOK, deniedErr bool
|
|
for _, r := range result {
|
|
switch r.UserId {
|
|
case admitted.Id:
|
|
admittedOK = r.Error == nil && r.Member != nil
|
|
case denied.Id:
|
|
deniedErr = r.Error != nil
|
|
}
|
|
}
|
|
require.True(t, admittedOK, "qualifying user must be added")
|
|
require.True(t, deniedErr, "non-qualifying user must carry an error without aborting the batch")
|
|
}
|