MM-64330 - filter abac users in channel invite (#31219)

* MM-64330 - filter abac users in channel invite

* implement cursor functionality for abac user filtering

* remove unnecessary comments

* refactor the backend implementation simplifying the functions

* refactor api to use opts as parameters, rename function

* add missing translation

* remove unnecesary test code

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
Pablo Vélez
2025-06-20 10:53:14 +02:00
коммит произвёл GitHub
родитель 968550d275
Коммит 5fc74cd401
11 изменённых файлов: 346 добавлений и 21 удалений

Просмотреть файл

@@ -840,7 +840,13 @@ func getUsers(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
profiles, appErr = c.App.GetUsersNotInChannelPage(inTeamId, notInChannelId, groupConstrainedBool, c.Params.Page, c.Params.PerPage, c.IsSystemAdmin(), restrictions)
if ok, _ := c.App.ChannelAccessControlled(c.AppContext, notInChannelId); ok {
// Get cursor_id from query parameters for cursor-based pagination
cursorId := r.URL.Query().Get("cursor_id")
profiles, appErr = c.App.GetUsersNotInAbacChannel(c.AppContext, inTeamId, notInChannelId, groupConstrainedBool, cursorId, c.Params.PerPage, c.IsSystemAdmin(), restrictions)
} else {
profiles, appErr = c.App.GetUsersNotInChannelPage(inTeamId, notInChannelId, groupConstrainedBool, c.Params.Page, c.Params.PerPage, c.IsSystemAdmin(), restrictions)
}
} else if notInTeamId != "" {
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), notInTeamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)

Просмотреть файл

@@ -3965,7 +3965,7 @@ func (a *App) ChannelAccessControlled(c request.CTX, channelID string) (bool, *m
return false, nil
}
_, err := a.Srv().Store().AccessControlPolicy().Get(c, channelID)
channel, err := a.Srv().Store().Channel().Get(channelID, true)
var nfErr *store.ErrNotFound
if err != nil && !errors.As(err, &nfErr) {
return false, model.NewAppError("ChannelIsAccessControlled", "app.channel.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
@@ -3973,7 +3973,7 @@ func (a *App) ChannelAccessControlled(c request.CTX, channelID string) (bool, *m
return false, nil
}
return true, nil
return channel.PolicyEnforced, nil
}
func (a *App) handleChannelCategoryName(channel *model.Channel) {

Просмотреть файл

@@ -667,6 +667,28 @@ func (a *App) GetUsersNotInChannelPage(teamID string, channelID string, groupCon
return a.sanitizeProfiles(users, asAdmin), nil
}
func (a *App) GetUsersNotInAbacChannel(ctx request.CTX, teamID string, channelID string, groupConstrained bool, cursorID string, limit int, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) {
// Get the AccessControl service
acs := a.Srv().Channels().AccessControl
if acs == nil {
return nil, model.NewAppError("GetUsersNotInAbacChannel", "api.user.get_users_not_in_abac_channel.access_control_unavailable.app_error", nil, "", http.StatusInternalServerError)
}
// Use cursor-based pagination for ABAC channels
users, _, appErr := acs.QueryUsersForResource(ctx, channelID, "*", model.SubjectSearchOptions{
TeamID: teamID,
Limit: limit,
Cursor: model.SubjectCursor{
TargetID: cursorID, // Empty string means start from beginning
},
})
if appErr != nil {
return nil, appErr
}
return a.sanitizeProfiles(users, asAdmin), nil
}
func (a *App) GetUsersWithoutTeamPage(options *model.UserGetOptions, asAdmin bool) ([]*model.User, *model.AppError) {
users, err := a.ch.srv.userService.GetUsersWithoutTeamPage(options, asAdmin)
if err != nil {

Просмотреть файл

@@ -880,6 +880,126 @@ func TestGetUsersByStatus(t *testing.T) {
})
}
func TestGetUsersNotInAbacChannel(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
// Set license to EnterpriseAdvanced
th.App.Srv().SetLicense(model.NewTestLicense("enterprise.advanced"))
// Enable ABAC in config
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.AccessControlSettings.EnableAttributeBasedAccessControl = true
})
// Create an ABAC channel
abacChannel := th.CreatePrivateChannel(th.Context, th.BasicTeam)
// Create three test users and add them to the team
user1 := th.CreateUser() // Will have matching attributes for ABAC
user2 := th.CreateUser() // Won't have matching attributes
user3 := th.CreateUser() // Won't have matching attributes
th.LinkUserToTeam(user1, th.BasicTeam)
th.LinkUserToTeam(user2, th.BasicTeam)
th.LinkUserToTeam(user3, th.BasicTeam)
// Create a policy with the same ID as the ABAC channel
channelPolicy := &model.AccessControlPolicy{
Type: model.AccessControlPolicyTypeChannel,
ID: abacChannel.Id,
Name: "Test Channel Policy",
Revision: 1,
Version: model.AccessControlPolicyVersionV0_1,
Rules: []model.AccessControlPolicyRule{
{
Actions: []string{"view", "join_channel"},
Expression: "user.attributes.program == \"test-program\"",
},
},
}
// Save the channel policy
var storeErr error
channelPolicy, storeErr = th.App.Srv().Store().AccessControlPolicy().Save(th.Context, channelPolicy)
require.NoError(t, storeErr)
require.NotNil(t, channelPolicy)
t.Cleanup(func() {
dErr := th.App.Srv().Store().AccessControlPolicy().Delete(th.Context, channelPolicy.ID)
require.NoError(t, dErr)
})
// Mock the AccessControl service
mockAccessControl := &mocks.AccessControlServiceInterface{}
originalAccessControl := th.App.Srv().ch.AccessControl
th.App.Srv().ch.AccessControl = mockAccessControl
defer func() {
th.App.Srv().ch.AccessControl = originalAccessControl
}()
t.Run("Returns users with matching attributes using cursor pagination", func(t *testing.T) {
// Set up the mock to return user1 when querying for users
mockAccessControl.On("QueryUsersForResource",
mock.Anything,
abacChannel.Id,
"*",
mock.MatchedBy(func(opts model.SubjectSearchOptions) bool {
return opts.TeamID == th.BasicTeam.Id &&
opts.Limit == 50 &&
opts.Cursor.TargetID == ""
})).Return([]*model.User{user1}, int64(1), nil).Once()
// Call the new ABAC-specific function with th.Context as first parameter
users, appErr := th.App.GetUsersNotInAbacChannel(th.Context, th.BasicTeam.Id, abacChannel.Id, false, "", 50, true, nil)
require.Nil(t, appErr)
// Create a map of user IDs for easier lookup
userMap := make(map[string]bool)
for _, u := range users {
userMap[u.Id] = true
}
// Verify only user1 is returned
assert.True(t, userMap[user1.Id], "User1 should be returned for ABAC channel")
assert.False(t, userMap[user2.Id], "User2 should not be returned for ABAC channel")
assert.False(t, userMap[user3.Id], "User3 should not be returned for ABAC channel")
assert.Len(t, users, 1, "Should return exactly 1 user")
})
t.Run("Works with cursor-based pagination", func(t *testing.T) {
cursorID := "some-cursor-id"
// Set up the mock to return user1 when querying with cursor
mockAccessControl.On("QueryUsersForResource",
mock.Anything,
abacChannel.Id,
"*",
mock.MatchedBy(func(opts model.SubjectSearchOptions) bool {
return opts.TeamID == th.BasicTeam.Id &&
opts.Limit == 25 &&
opts.Cursor.TargetID == cursorID
})).Return([]*model.User{user1}, int64(1), nil).Once()
// Call with cursor ID and th.Context as first parameter
users, appErr := th.App.GetUsersNotInAbacChannel(th.Context, th.BasicTeam.Id, abacChannel.Id, false, cursorID, 25, true, nil)
require.Nil(t, appErr)
assert.Len(t, users, 1, "Should return exactly 1 user with cursor pagination")
})
t.Run("Returns error when AccessControl service is unavailable", func(t *testing.T) {
// Temporarily set AccessControl to nil
th.App.Srv().ch.AccessControl = nil
defer func() {
th.App.Srv().ch.AccessControl = mockAccessControl
}()
// Call should return error with th.Context as first parameter
users, appErr := th.App.GetUsersNotInAbacChannel(th.Context, th.BasicTeam.Id, abacChannel.Id, false, "", 50, true, nil)
require.NotNil(t, appErr)
require.Nil(t, users)
assert.Equal(t, "api.user.get_users_not_in_abac_channel.access_control_unavailable.app_error", appErr.Id)
})
}
func TestCreateUserWithInviteId(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic()