[MM-61756] Attribute Based Access Control - Phase 1 (#30785)

Attribute Based Access Control - Base
* MM-63662

* MM-63919

* MM-63954

* MM-63955 

* MM-63425

* MM-63426

* MM-63458

* MM-63459

* MM-63603

* MM-63845

* MM-64146

* MM-64199

* MM-64201

* MM-64233

* MM-64247

* MM-64268

---------

Co-authored-by: Harshil Sharma <harshilsharma63@gmail.com>
Co-authored-by: Pablo Andrés Vélez Vidal <pablovv2012@gmail.com>
Co-authored-by: abhijit-singh <abhijitsingh0702@gmail.com>
Co-authored-by: Harrison Healey <harrisonmhealey@gmail.com>
Этот коммит содержится в:
Ibrahim Serdar Acikgoz
2025-05-15 11:33:08 +02:00
коммит произвёл GitHub
родитель 4b445cbf16
Коммит a344b3225b
156 изменённых файлов: 14382 добавлений и 621 удалений

293
server/channels/app/access_control.go Обычный файл
Просмотреть файл

@@ -0,0 +1,293 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"net/http"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
)
func (a *App) GetChannelsForPolicy(rctx request.CTX, policyID string, cursor model.AccessControlPolicyCursor, limit int) ([]*model.ChannelWithTeamData, int64, *model.AppError) {
policy, appErr := a.GetAccessControlPolicy(rctx, policyID)
if appErr != nil {
return nil, 0, appErr
}
switch policy.Type {
case model.AccessControlPolicyTypeParent:
policies, total, err := a.Srv().Store().AccessControlPolicy().SearchPolicies(rctx, model.AccessControlPolicySearch{
Type: model.AccessControlPolicyTypeChannel,
ParentID: policyID,
Cursor: cursor,
Limit: limit,
})
if err != nil {
return nil, 0, model.NewAppError("GetChannelsForPolicy", "app.pap.get_all_access_control_policies.app_error", nil, err.Error(), http.StatusInternalServerError)
}
channelIDs := make([]string, 0, len(policies))
for _, p := range policies {
channelIDs = append(channelIDs, p.ID)
}
chs, err := a.Srv().Store().Channel().GetChannelsWithTeamDataByIds(channelIDs, true)
if err != nil {
return nil, 0, model.NewAppError("GetChannelsForPolicy", "app.pap.get_all_access_control_policies.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return chs, total, nil
case model.AccessControlPolicyTypeChannel:
chs, err := a.Srv().Store().Channel().GetChannelsWithTeamDataByIds([]string{policyID}, true)
if err != nil {
return nil, 0, model.NewAppError("GetChannelsForPolicy", "app.pap.get_all_access_control_policies.app_error", nil, err.Error(), http.StatusInternalServerError)
}
total := int64(len(chs))
return chs, total, nil
default:
return nil, 0, model.NewAppError("GetChannelsForPolicy", "app.pap.get_all_access_control_policies.app_error", nil, "Invalid policy type", http.StatusBadRequest)
}
}
func (a *App) GetAccessControlPolicy(rctx request.CTX, id string) (*model.AccessControlPolicy, *model.AppError) {
acs := a.Srv().ch.AccessControl
if acs == nil {
return nil, model.NewAppError("GetPolicy", "app.pap.get_policy.app_error", nil, "Policy Administration Point is not initialized", http.StatusNotImplemented)
}
policy, appErr := acs.GetPolicy(rctx, id)
if appErr != nil {
return nil, appErr
}
return policy, nil
}
func (a *App) CreateOrUpdateAccessControlPolicy(rctx request.CTX, policy *model.AccessControlPolicy) (*model.AccessControlPolicy, *model.AppError) {
acs := a.Srv().ch.AccessControl
if acs == nil {
return nil, model.NewAppError("CreateAccessControlPolicy", "app.pap.create_access_control_policy.app_error", nil, "Policy Administration Point is not initialized", http.StatusNotImplemented)
}
if policy.ID == "" {
policy.ID = model.NewId()
}
var appErr *model.AppError
policy, appErr = acs.SavePolicy(rctx, policy)
if appErr != nil {
return nil, appErr
}
return policy, nil
}
func (a *App) DeleteAccessControlPolicy(rctx request.CTX, id string) *model.AppError {
acs := a.Srv().ch.AccessControl
if acs == nil {
return model.NewAppError("DeleteAccessControlPolicy", "app.pap.delete_access_control_policy.app_error", nil, "Policy Administration Point is not initialized", http.StatusNotImplemented)
}
appErr := acs.DeletePolicy(rctx, id)
if appErr != nil {
return appErr
}
return nil
}
func (a *App) CheckExpression(rctx request.CTX, expression string) ([]model.CELExpressionError, *model.AppError) {
acs := a.Srv().ch.AccessControl
if acs == nil {
return nil, model.NewAppError("CheckExpression", "app.pap.check_expression.app_error", nil, "Policy Administration Point is not initialized", http.StatusNotImplemented)
}
errs, appErr := acs.CheckExpression(rctx, expression)
if appErr != nil {
return nil, model.NewAppError("CheckExpression", "app.pap.check_expression.app_error", nil, appErr.Error(), http.StatusInternalServerError)
}
return errs, nil
}
func (a *App) TestExpression(rctx request.CTX, expression string, opts model.SubjectSearchOptions) ([]*model.User, int64, *model.AppError) {
acs := a.Srv().ch.AccessControl
if acs == nil {
return nil, 0, model.NewAppError("TestExpression", "app.pap.check_expression.app_error", nil, "Policy Administration Point is not initialized", http.StatusNotImplemented)
}
res, count, err := acs.QueryUsersForExpression(rctx, expression, opts)
if err != nil {
return nil, 0, model.NewAppError("TestExpression", "app.pap.check_expression.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return res, count, nil
}
func (a *App) AssignAccessControlPolicyToChannels(rctx request.CTX, parentID string, channelIDs []string) ([]*model.AccessControlPolicy, *model.AppError) {
acs := a.Srv().ch.AccessControl
if acs == nil {
return nil, model.NewAppError("AssignAccessControlPolicyToChannels", "app.pap.assign_access_control_policy_to_channels.app_error", nil, "Policy Administration Point is not initialized", http.StatusNotImplemented)
}
policy, appErr := a.GetAccessControlPolicy(rctx, parentID)
if appErr != nil {
return nil, appErr
}
if policy.Type != model.AccessControlPolicyTypeParent {
return nil, model.NewAppError("AssignAccessControlPolicyToChannels", "app.pap.assign_access_control_policy_to_channels.app_error", nil, "Policy is not of type parent", http.StatusBadRequest)
}
channels, err := a.GetChannels(rctx, channelIDs)
if err != nil {
return nil, appErr
}
policies := make([]*model.AccessControlPolicy, 0, len(channelIDs))
for _, channel := range channels {
if channel.Type != model.ChannelTypePrivate || channel.IsGroupConstrained() {
return nil, model.NewAppError("AssignAccessControlPolicyToChannels", "app.pap.assign_access_control_policy_to_channels.app_error", nil, "Channel is not of type private", http.StatusBadRequest)
}
if channel.IsShared() {
return nil, model.NewAppError("AssignAccessControlPolicyToChannels", "app.pap.assign_access_control_policy_to_channels.app_error", nil, "Channel is shared", http.StatusBadRequest)
}
newPolicy, appErr := policy.Inherit(channel.Id, model.AccessControlPolicyTypeChannel)
if appErr != nil {
return nil, appErr
}
newPolicy, appErr = acs.SavePolicy(rctx, newPolicy)
if appErr != nil {
return nil, appErr
}
policies = append(policies, newPolicy)
}
return policies, nil
}
func (a *App) UnAssignPoliciesFromChannels(rctx request.CTX, policyID string, channelIDs []string) *model.AppError {
acs := a.Srv().ch.AccessControl
if acs == nil {
return model.NewAppError("UnAssignPoliciesFromChannels", "app.pap.unassign_access_control_policy_from_channels.app_error", nil, "Policy Administration Point is not initialized", http.StatusNotImplemented)
}
cps, _, err := a.Srv().Store().AccessControlPolicy().SearchPolicies(rctx, model.AccessControlPolicySearch{
Type: model.AccessControlPolicyTypeChannel,
ParentID: policyID,
})
if err != nil {
return model.NewAppError("UnAssignPoliciesFromChannels", "app.pap.unassign_access_control_policy_from_channels.app_error", nil, err.Error(), http.StatusInternalServerError)
}
childPolicies := make(map[string]bool)
for _, p := range cps {
childPolicies[p.ID] = true
}
for _, channelID := range channelIDs {
if _, ok := childPolicies[channelID]; !ok {
mlog.Warn("Policy is not assigned to the parent policy", mlog.String("channel_id", channelID), mlog.String("parent_policy_id", policyID))
continue
}
appErr := acs.DeletePolicy(rctx, channelID)
if appErr != nil {
return appErr
}
}
return nil
}
func (a *App) SearchAccessControlPolicies(rctx request.CTX, opts model.AccessControlPolicySearch) ([]*model.AccessControlPolicy, int64, *model.AppError) {
acs := a.Srv().ch.AccessControl
if acs == nil {
return nil, 0, model.NewAppError("SearchAccessControlPolicies", "app.pap.search_access_control_policies.app_error", nil, "Policy Administration Point is not initialized", http.StatusNotImplemented)
}
policies, total, err := a.Srv().Store().AccessControlPolicy().SearchPolicies(rctx, opts)
if err != nil {
return nil, 0, model.NewAppError("SearchAccessControlPolicies", "app.pap.search_access_control_policies.app_error", nil, err.Error(), http.StatusInternalServerError)
}
for i, policy := range policies {
if policy.Type != model.AccessControlPolicyTypeParent {
continue
}
normlizedPolicy, appErr := acs.NormalizePolicy(rctx, policy)
if appErr != nil {
mlog.Error("Failed to normalize policy", mlog.String("policy_id", policy.ID), mlog.Err(appErr))
continue
}
policies[i] = normlizedPolicy
}
return policies, total, nil
}
func (a *App) GetAccessControlPolicyAttributes(rctx request.CTX, channelID string, action string) (map[string][]string, *model.AppError) {
acs := a.Srv().ch.AccessControl
if acs == nil {
return nil, model.NewAppError("GetChannelAccessControlAttributes", "app.pap.get_channel_access_control_attributes.app_error", nil, "Policy Administration Point is not initialized", http.StatusNotImplemented)
}
attributes, appErr := acs.GetPolicyRuleAttributes(rctx, channelID, action)
if appErr != nil {
return nil, appErr
}
return attributes, nil
}
func (a *App) GetAccessControlFieldsAutocomplete(rctx request.CTX, after string, limit int) ([]*model.PropertyField, *model.AppError) {
cpaGroupID, err := a.CpaGroupID()
if err != nil {
return nil, model.NewAppError("GetAccessControlAutoComplete", "app.pap.get_access_control_auto_complete.app_error", nil, err.Error(), http.StatusInternalServerError)
}
fields, err := a.Srv().Store().PropertyField().SearchPropertyFields(model.PropertyFieldSearchOpts{
GroupID: cpaGroupID,
Cursor: model.PropertyFieldSearchCursor{
PropertyFieldID: after,
CreateAt: 1,
},
PerPage: limit,
})
if err != nil {
return nil, model.NewAppError("GetAccessControlAutoComplete", "app.pap.get_access_control_auto_complete.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return fields, nil
}
func (a *App) UpdateAccessControlPolicyActive(rctx request.CTX, policyID string, active bool) *model.AppError {
_, err := a.Srv().Store().AccessControlPolicy().SetActiveStatus(rctx, policyID, active)
if err != nil {
return model.NewAppError("UpdateAccessControlPolicyActive", "app.pap.update_access_control_policy_active.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return nil
}
func (a *App) ExpressionToVisualAST(rctx request.CTX, expression string) (*model.VisualExpression, *model.AppError) {
acs := a.Srv().ch.AccessControl
if acs == nil {
return nil, model.NewAppError("ExpressionToVisualAST", "app.pap.expression_to_visual_ast.app_error", nil, "Policy Administration Point is not initialized", http.StatusNotImplemented)
}
visualAST, appErr := acs.ExpressionToVisualAST(rctx, expression)
if appErr != nil {
return nil, appErr
}
return visualAST, nil
}

455
server/channels/app/access_control_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,455 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"net/http"
"testing"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
mocks "github.com/mattermost/mattermost/server/v8/einterfaces/mocks"
)
func TestGetChannelsForPolicy(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
rctx := request.TestContext(t)
policyID := "policyID"
cursor := model.AccessControlPolicyCursor{}
limit := 10
t.Run("Feature not enabled", func(t *testing.T) {
th.App.Srv().ch.AccessControl = nil
channels, total, err := th.App.GetChannelsForPolicy(rctx, policyID, cursor, limit)
require.NotNil(t, err)
assert.Nil(t, channels)
assert.Equal(t, int64(0), total)
})
t.Run("Invalid policy type", func(t *testing.T) {
mockAccessControl := &mocks.AccessControlServiceInterface{}
th.App.Srv().ch.AccessControl = mockAccessControl
mockAccessControl.On("GetPolicy", mock.AnythingOfType("*request.Context"), policyID).Return(&model.AccessControlPolicy{Type: "invalid"}, nil)
channels, total, err := th.App.GetChannelsForPolicy(rctx, policyID, cursor, limit)
require.NotNil(t, err)
require.Nil(t, channels)
require.Equal(t, int64(0), total)
})
t.Run("Valid policy type - no channels", func(t *testing.T) {
pID := model.NewId()
parentPolicy := &model.AccessControlPolicy{
Type: model.AccessControlPolicyTypeParent,
ID: pID,
Name: "parentPolicy",
Revision: 1,
Version: model.AccessControlPolicyVersionV0_1,
Rules: []model.AccessControlPolicyRule{
{
Actions: []string{"*"},
Expression: "user.attributes.program == \"non-existent-program\"",
},
},
}
mockAccessControl := &mocks.AccessControlServiceInterface{}
th.App.Srv().ch.AccessControl = mockAccessControl
mockAccessControl.On("GetPolicy", rctx, pID).Return(parentPolicy, nil)
channels, total, err := th.App.GetChannelsForPolicy(rctx, pID, cursor, limit)
require.Nil(t, err)
require.NotNil(t, channels)
require.Equal(t, int64(0), total)
})
t.Run("Valid policy type - with channels", func(t *testing.T) {
pID := model.NewId()
parentPolicy := &model.AccessControlPolicy{
Type: model.AccessControlPolicyTypeParent,
ID: pID,
Name: "parentPolicy",
Revision: 1,
Version: model.AccessControlPolicyVersionV0_1,
Rules: []model.AccessControlPolicyRule{
{
Actions: []string{"*"},
Expression: "user.attributes.program == \"non-existent-program\"",
},
},
}
ch := th.CreatePrivateChannel(rctx, th.BasicTeam)
childPolicy, appErr := parentPolicy.Inherit(ch.Id, model.AccessControlPolicyTypeChannel)
require.Nil(t, appErr)
var err error
childPolicy, err = th.App.Srv().Store().AccessControlPolicy().Save(rctx, childPolicy)
require.NoError(t, err)
require.NotNil(t, childPolicy)
mockAccessControl := &mocks.AccessControlServiceInterface{}
th.App.Srv().ch.AccessControl = mockAccessControl
mockAccessControl.On("GetPolicy", rctx, pID).Return(parentPolicy, nil)
channels, total, appErr := th.App.GetChannelsForPolicy(rctx, pID, cursor, limit)
require.Nil(t, appErr)
require.NotNil(t, channels)
require.Equal(t, int64(1), total)
assert.Equal(t, ch.Id, channels[0].Id)
mockAccessControl.On("GetPolicy", rctx, ch.Id).Return(childPolicy, nil)
channels, total, appErr = th.App.GetChannelsForPolicy(rctx, ch.Id, cursor, limit)
require.Nil(t, appErr)
require.NotNil(t, channels)
require.Equal(t, int64(1), total)
assert.Equal(t, ch.Id, channels[0].Id)
})
}
func TestSearchAccessControlPolicies(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
rctx := request.TestContext(t)
t.Run("Feature not enabled", func(t *testing.T) {
policies, total, err := th.App.SearchAccessControlPolicies(rctx, model.AccessControlPolicySearch{})
require.NotNil(t, err)
require.Empty(t, policies)
require.Equal(t, int64(0), total)
})
t.Run("Empty search result", func(t *testing.T) {
mockAccessControl := &mocks.AccessControlServiceInterface{}
th.App.Srv().ch.AccessControl = mockAccessControl
policies, total, err := th.App.SearchAccessControlPolicies(rctx, model.AccessControlPolicySearch{})
require.Nil(t, err)
require.Empty(t, policies)
require.Equal(t, int64(0), total)
})
t.Run("Single search result", func(t *testing.T) {
pID := model.NewId()
parentPolicy := &model.AccessControlPolicy{
Type: model.AccessControlPolicyTypeParent,
ID: pID,
Name: "parentPolicy",
Revision: 1,
Version: model.AccessControlPolicyVersionV0_1,
Rules: []model.AccessControlPolicyRule{
{
Actions: []string{"*"},
Expression: "user.attributes.program == \"non-existent-program\"",
},
},
}
var err error
parentPolicy, err = th.App.Srv().Store().AccessControlPolicy().Save(rctx, parentPolicy)
require.NoError(t, err)
require.NotNil(t, parentPolicy)
defer func() {
dErr := th.App.Srv().Store().AccessControlPolicy().Delete(rctx, parentPolicy.ID)
require.NoError(t, dErr)
}()
mockAccessControl := &mocks.AccessControlServiceInterface{}
th.App.Srv().ch.AccessControl = mockAccessControl
mockAccessControl.On("NormalizePolicy", rctx, parentPolicy).Return(parentPolicy, nil)
t.Run("With no term", func(t *testing.T) {
policies, total, err := th.App.SearchAccessControlPolicies(rctx, model.AccessControlPolicySearch{})
require.Nil(t, err)
require.NotNil(t, policies)
require.Equal(t, int64(1), total)
require.Equal(t, parentPolicy.ID, policies[0].ID)
})
t.Run("With term", func(t *testing.T) {
policies, total, err := th.App.SearchAccessControlPolicies(rctx, model.AccessControlPolicySearch{
Term: "parent",
})
require.Nil(t, err)
require.NotNil(t, policies)
require.Equal(t, int64(1), total)
require.Equal(t, parentPolicy.ID, policies[0].ID)
})
t.Run("With term and no results", func(t *testing.T) {
policies, total, err := th.App.SearchAccessControlPolicies(rctx, model.AccessControlPolicySearch{
Term: "something else",
})
require.Nil(t, err)
require.Empty(t, policies)
require.Equal(t, int64(0), total)
})
})
}
func TestAssignAccessControlPolicyToChannels(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
rctx := request.TestContext(t)
parentID := model.NewId()
parentPolicy := &model.AccessControlPolicy{
Type: model.AccessControlPolicyTypeParent,
ID: parentID,
Name: "parentPolicy",
Revision: 1,
Version: model.AccessControlPolicyVersionV0_1,
Rules: []model.AccessControlPolicyRule{
{
Actions: []string{"*"},
Expression: "user.attributes.program == \"non-existent-program\"",
},
},
}
var err error
parentPolicy, err = th.App.Srv().Store().AccessControlPolicy().Save(rctx, parentPolicy)
require.NoError(t, err)
require.NotNil(t, parentPolicy)
t.Cleanup(func() {
dErr := th.App.Srv().Store().AccessControlPolicy().Delete(rctx, parentPolicy.ID)
require.NoError(t, dErr)
})
t.Run("Feature not enabled", func(t *testing.T) {
th.App.Srv().ch.AccessControl = nil
policies, err := th.App.AssignAccessControlPolicyToChannels(rctx, parentID, []string{})
require.NotNil(t, err)
assert.Nil(t, policies)
assert.Equal(t, "app.pap.assign_access_control_policy_to_channels.app_error", err.Id)
})
t.Run("Error saving policy", func(t *testing.T) {
mockAccessControl := &mocks.AccessControlServiceInterface{}
th.App.Srv().ch.AccessControl = mockAccessControl
mockAccessControl.On("GetPolicy", rctx, parentID).Return(parentPolicy, nil)
mockAccessControl.On("SavePolicy", rctx, mock.Anything).Return(nil, model.NewAppError("SavePolicy", "error", nil, "save error", http.StatusInternalServerError))
ch := th.CreatePrivateChannel(rctx, th.BasicTeam)
t.Cleanup(func() {
appErr := th.App.PermanentDeleteChannel(rctx, ch)
require.Nil(t, appErr)
})
policies, err := th.App.AssignAccessControlPolicyToChannels(rctx, parentID, []string{ch.Id})
require.NotNil(t, err)
require.Empty(t, policies)
})
t.Run("Parent policy not found", func(t *testing.T) {
mockAccessControl := &mocks.AccessControlServiceInterface{}
th.App.Srv().ch.AccessControl = mockAccessControl
mockAccessControl.On("GetPolicy", rctx, parentID).Return(nil, model.NewAppError("GetPolicy", "error", nil, "not found", http.StatusNotFound))
policies, err := th.App.AssignAccessControlPolicyToChannels(rctx, parentID, []string{})
require.NotNil(t, err)
assert.Nil(t, policies)
})
t.Run("Policy is not of type parent", func(t *testing.T) {
mockAccessControl := &mocks.AccessControlServiceInterface{}
th.App.Srv().ch.AccessControl = mockAccessControl
mockAccessControl.On("GetPolicy", rctx, parentID).Return(&model.AccessControlPolicy{Type: model.AccessControlPolicyTypeChannel}, nil)
policies, err := th.App.AssignAccessControlPolicyToChannels(rctx, parentID, []string{})
require.NotNil(t, err)
assert.Nil(t, policies)
assert.Equal(t, "app.pap.assign_access_control_policy_to_channels.app_error", err.Id)
})
t.Run("Channel is not private", func(t *testing.T) {
mockAccessControl := &mocks.AccessControlServiceInterface{}
th.App.Srv().ch.AccessControl = mockAccessControl
mockAccessControl.On("GetPolicy", rctx, parentID).Return(&model.AccessControlPolicy{Type: model.AccessControlPolicyTypeParent}, nil)
// Create a public channel
publicChannel := th.CreateChannel(rctx, th.BasicTeam)
t.Cleanup(func() {
appErr := th.App.PermanentDeleteChannel(rctx, publicChannel)
require.Nil(t, appErr)
})
policies, err := th.App.AssignAccessControlPolicyToChannels(rctx, parentID, []string{publicChannel.Id})
require.NotNil(t, err)
assert.Nil(t, policies)
assert.Contains(t, err.Error(), "Channel is not of type private")
})
t.Run("Channel is shared", func(t *testing.T) {
mockAccessControl := &mocks.AccessControlServiceInterface{}
th.App.Srv().ch.AccessControl = mockAccessControl
mockAccessControl.On("GetPolicy", rctx, parentID).Return(&model.AccessControlPolicy{Type: model.AccessControlPolicyTypeParent}, nil)
privateChannel := th.CreatePrivateChannel(rctx, th.BasicTeam)
t.Cleanup(func() {
appErr := th.App.PermanentDeleteChannel(rctx, privateChannel)
require.Nil(t, appErr)
})
privateChannel.Shared = model.NewPointer(true)
_, err := th.App.Srv().Store().Channel().Update(rctx, privateChannel)
require.NoError(t, err)
policies, appErr := th.App.AssignAccessControlPolicyToChannels(rctx, parentID, []string{privateChannel.Id})
require.NotNil(t, appErr)
assert.Nil(t, policies)
assert.Contains(t, appErr.Error(), "Channel is shared")
})
t.Run("Successful assignment", func(t *testing.T) {
ch1 := th.CreatePrivateChannel(rctx, th.BasicTeam)
t.Cleanup(func() {
appErr := th.App.PermanentDeleteChannel(rctx, ch1)
require.Nil(t, appErr)
})
ch2 := th.CreatePrivateChannel(rctx, th.BasicTeam)
t.Cleanup(func() {
appErr := th.App.PermanentDeleteChannel(rctx, ch2)
require.Nil(t, appErr)
})
childP1, appErr := parentPolicy.Inherit(ch1.Id, model.AccessControlPolicyTypeChannel)
require.Nil(t, appErr)
childP2, appErr := parentPolicy.Inherit(ch2.Id, model.AccessControlPolicyTypeChannel)
require.Nil(t, appErr)
mockAccessControl := &mocks.AccessControlServiceInterface{}
th.App.Srv().ch.AccessControl = mockAccessControl
mockAccessControl.On("GetPolicy", rctx, parentID).Return(parentPolicy, nil)
mockAccessControl.On("SavePolicy", rctx, mock.MatchedBy(func(p *model.AccessControlPolicy) bool { return p.ID == ch1.Id })).Return(childP1, nil)
mockAccessControl.On("SavePolicy", rctx, mock.MatchedBy(func(p *model.AccessControlPolicy) bool { return p.ID == ch2.Id })).Return(childP2, nil)
policies, err := th.App.AssignAccessControlPolicyToChannels(rctx, parentID, []string{ch1.Id, ch2.Id})
require.Nil(t, err)
require.NotNil(t, policies)
require.Len(t, policies, 2)
assert.ElementsMatch(t, []string{ch1.Id, ch2.Id}, []string{policies[0].ID, policies[1].ID})
mockAccessControl.AssertCalled(t, "SavePolicy", rctx, mock.AnythingOfType("*model.AccessControlPolicy"))
})
}
func TestUnAssignPoliciesFromChannels(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
rctx := request.TestContext(t)
parentPolicy := &model.AccessControlPolicy{
ID: model.NewId(),
Type: model.AccessControlPolicyTypeParent,
Name: "parent-for-unassign-tests",
Revision: 1,
Version: model.AccessControlPolicyVersionV0_1,
Rules: []model.AccessControlPolicyRule{
{Actions: []string{"*"}, Expression: "true"},
},
}
var err error
parentPolicy, err = th.App.Srv().Store().AccessControlPolicy().Save(rctx, parentPolicy)
require.NoError(t, err)
require.NotNil(t, parentPolicy)
t.Cleanup(func() {
sErr := th.App.Srv().Store().AccessControlPolicy().Delete(rctx, parentPolicy.ID)
require.NoError(t, sErr)
})
ch1 := th.CreatePrivateChannel(rctx, th.BasicTeam)
t.Cleanup(func() {
sErr := th.App.PermanentDeleteChannel(rctx, ch1)
require.Nil(t, sErr)
})
ch2 := th.CreatePrivateChannel(rctx, th.BasicTeam)
t.Cleanup(func() {
sErr := th.App.PermanentDeleteChannel(rctx, ch2)
require.Nil(t, sErr)
})
childPolicy1, appErrInherit1 := parentPolicy.Inherit(ch1.Id, model.AccessControlPolicyTypeChannel)
require.Nil(t, appErrInherit1)
childPolicy1, err = th.App.Srv().Store().AccessControlPolicy().Save(rctx, childPolicy1)
require.NoError(t, err)
require.NotNil(t, childPolicy1)
t.Cleanup(func() {
sErr := th.App.Srv().Store().AccessControlPolicy().Delete(rctx, childPolicy1.ID)
require.NoError(t, sErr)
})
childPolicy2, appErrInherit2 := parentPolicy.Inherit(ch2.Id, model.AccessControlPolicyTypeChannel)
require.Nil(t, appErrInherit2)
childPolicy2, err = th.App.Srv().Store().AccessControlPolicy().Save(rctx, childPolicy2)
require.NoError(t, err)
require.NotNil(t, childPolicy2)
t.Cleanup(func() {
sErr := th.App.Srv().Store().AccessControlPolicy().Delete(rctx, childPolicy2.ID)
require.NoError(t, sErr)
})
t.Run("Feature not enabled", func(t *testing.T) {
th.App.Srv().ch.AccessControl = nil
appErr := th.App.UnAssignPoliciesFromChannels(rctx, parentPolicy.ID, []string{ch1.Id, ch2.Id})
require.NotNil(t, appErr)
assert.Equal(t, "app.pap.unassign_access_control_policy_from_channels.app_error", appErr.Id)
})
t.Run("Error deleting policy from AccessControlService", func(t *testing.T) {
mockAccessControl := &mocks.AccessControlServiceInterface{}
th.App.Srv().ch.AccessControl = mockAccessControl
expectedErr := model.NewAppError("DeletePolicy", "mock.delete.error", nil, "failed to delete from acs", http.StatusInternalServerError)
mockAccessControl.On("DeletePolicy", rctx, ch1.Id).Return(expectedErr).Once()
mockAccessControl.On("DeletePolicy", rctx, ch2.Id).Return(nil).Maybe()
appErr := th.App.UnAssignPoliciesFromChannels(rctx, parentPolicy.ID, []string{ch1.Id, ch2.Id})
require.NotNil(t, appErr)
assert.Equal(t, expectedErr.Id, appErr.Id)
assert.Equal(t, expectedErr.Message, appErr.Message)
mockAccessControl.AssertCalled(t, "DeletePolicy", rctx, ch1.Id)
mockAccessControl.AssertNotCalled(t, "DeletePolicy", rctx, ch2.Id)
p1, storeErr := th.App.Srv().Store().AccessControlPolicy().Get(rctx, ch1.Id)
assert.NoError(t, storeErr)
assert.NotNil(t, p1)
p2, storeErr := th.App.Srv().Store().AccessControlPolicy().Get(rctx, ch2.Id)
assert.NoError(t, storeErr)
assert.NotNil(t, p2)
})
t.Run("Channel not actually a child policy", func(t *testing.T) {
ch3 := th.CreatePrivateChannel(rctx, th.BasicTeam) // Not a child of parentPolicy
t.Cleanup(func() { _ = th.App.PermanentDeleteChannel(rctx, ch3) })
mockAccessControl := &mocks.AccessControlServiceInterface{}
th.App.Srv().ch.AccessControl = mockAccessControl
mockAccessControl.On("DeletePolicy", rctx, ch1.Id).Return(nil).Once()
mockAccessControl.On("DeletePolicy", rctx, ch2.Id).Return(nil).Once()
appErr := th.App.UnAssignPoliciesFromChannels(rctx, parentPolicy.ID, []string{ch1.Id, ch2.Id, ch3.Id})
require.Nil(t, appErr)
})
t.Run("Successful unassignment", func(t *testing.T) {
mockAccessControl := &mocks.AccessControlServiceInterface{}
th.App.Srv().ch.AccessControl = mockAccessControl
mockAccessControl.On("DeletePolicy", rctx, ch1.Id).Return(nil).Once()
mockAccessControl.On("DeletePolicy", rctx, ch2.Id).Return(nil).Once()
appErr := th.App.UnAssignPoliciesFromChannels(rctx, parentPolicy.ID, []string{ch1.Id, ch2.Id})
require.Nil(t, appErr)
})
}

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

@@ -635,6 +635,14 @@ func (a *App) GetGroupChannel(c request.CTX, userIDs []string) (*model.Channel,
// UpdateChannel updates a given channel by its Id. It also publishes the CHANNEL_UPDATED event.
func (a *App) UpdateChannel(c request.CTX, channel *model.Channel) (*model.Channel, *model.AppError) {
ok, appErr := a.ChannelAccessControlled(c, channel.Id)
if appErr != nil {
return nil, appErr
}
if ok && channel.Type != model.ChannelTypePrivate {
return nil, model.NewAppError("UpdateChannel", "api.channel.update_channel.not_allowed.app_error", nil, "", http.StatusForbidden)
}
_, err := a.Srv().Store().Channel().Update(c, channel)
if err != nil {
var appErr *model.AppError
@@ -1576,6 +1584,40 @@ func (a *App) addUserToChannel(c request.CTX, user *model.User, channel *model.C
newMember.SchemeAdmin = userShouldBeAdmin
}
if channel.Type == model.ChannelTypePrivate {
if ok, appErr := a.ChannelAccessControlled(c, channel.Id); ok {
if acs := a.Srv().Channels().AccessControl; acs != nil {
groupID, err := a.CpaGroupID()
if err != nil {
return nil, model.NewAppError("AddUserToChannel", "api.channel.add_user.to.channel.failed.app_error", nil,
fmt.Sprintf("failed to get group: %v, user_id: %s, channel_id: %s", err, user.Id, channel.Id), http.StatusInternalServerError)
}
s, err := a.Srv().Store().Attributes().GetSubject(c, user.Id, groupID)
if err != nil {
return nil, model.NewAppError("AddUserToChannel", "api.channel.add_user.to.channel.failed.app_error", nil,
fmt.Sprintf("failed to get subject: %v, user_id: %s, channel_id: %s", err, user.Id, channel.Id), http.StatusNotFound)
}
decision, evalErr := acs.AccessEvaluation(c, model.AccessRequest{
Subject: *s,
Resource: model.Resource{
Type: model.AccessControlPolicyTypeChannel,
ID: channel.Id,
},
Action: "join_channel",
})
if evalErr != nil {
return nil, evalErr
} else if !decision.Decision {
return nil, model.NewAppError("AddUserToChannel", "api.channel.add_user.to.channel.rejected", nil, "", http.StatusForbidden)
}
}
} else if appErr != nil {
c.Logger().Error("Error checking access control policy for channel", mlog.Err(appErr))
}
}
newMember, nErr = a.Srv().Store().Channel().SaveMember(c, newMember)
if nErr != nil {
return nil, model.NewAppError("AddUserToChannel", "api.channel.add_user.to.channel.failed.app_error", nil,
@@ -1989,13 +2031,15 @@ func (a *App) GetAllChannels(c request.CTX, page, perPage int, opts model.Channe
opts.ExcludeChannelNames = a.DefaultChannelNames(c)
}
storeOpts := store.ChannelSearchOpts{
NotAssociatedToGroup: opts.NotAssociatedToGroup,
IncludeDeleted: opts.IncludeDeleted,
ExcludeChannelNames: opts.ExcludeChannelNames,
GroupConstrained: opts.GroupConstrained,
ExcludeGroupConstrained: opts.ExcludeGroupConstrained,
ExcludePolicyConstrained: opts.ExcludePolicyConstrained,
IncludePolicyID: opts.IncludePolicyID,
NotAssociatedToGroup: opts.NotAssociatedToGroup,
IncludeDeleted: opts.IncludeDeleted,
ExcludeChannelNames: opts.ExcludeChannelNames,
GroupConstrained: opts.GroupConstrained,
ExcludeGroupConstrained: opts.ExcludeGroupConstrained,
ExcludePolicyConstrained: opts.ExcludePolicyConstrained,
IncludePolicyID: opts.IncludePolicyID,
AccessControlPolicyEnforced: opts.AccessControlPolicyEnforced,
ExcludeAccessControlPolicyEnforced: opts.ExcludeAccessControlPolicyEnforced,
}
channels, err := a.Srv().Store().Channel().GetAllChannels(page*perPage, perPage, storeOpts)
if err != nil {
@@ -2962,22 +3006,25 @@ func (a *App) SearchAllChannels(c request.CTX, term string, opts model.ChannelSe
opts.ExcludeChannelNames = a.DefaultChannelNames(c)
}
storeOpts := store.ChannelSearchOpts{
ExcludeChannelNames: opts.ExcludeChannelNames,
NotAssociatedToGroup: opts.NotAssociatedToGroup,
IncludeDeleted: opts.IncludeDeleted,
Deleted: opts.Deleted,
TeamIds: opts.TeamIds,
GroupConstrained: opts.GroupConstrained,
ExcludeGroupConstrained: opts.ExcludeGroupConstrained,
PolicyID: opts.PolicyID,
IncludePolicyID: opts.IncludePolicyID,
IncludeSearchByID: opts.IncludeSearchById,
ExcludeRemote: opts.ExcludeRemote,
ExcludePolicyConstrained: opts.ExcludePolicyConstrained,
Public: opts.Public,
Private: opts.Private,
Page: opts.Page,
PerPage: opts.PerPage,
ExcludeChannelNames: opts.ExcludeChannelNames,
NotAssociatedToGroup: opts.NotAssociatedToGroup,
IncludeDeleted: opts.IncludeDeleted,
Deleted: opts.Deleted,
TeamIds: opts.TeamIds,
GroupConstrained: opts.GroupConstrained,
ExcludeGroupConstrained: opts.ExcludeGroupConstrained,
PolicyID: opts.PolicyID,
IncludePolicyID: opts.IncludePolicyID,
IncludeSearchByID: opts.IncludeSearchById,
ExcludeRemote: opts.ExcludeRemote,
ExcludePolicyConstrained: opts.ExcludePolicyConstrained,
Public: opts.Public,
Private: opts.Private,
Page: opts.Page,
PerPage: opts.PerPage,
AccessControlPolicyEnforced: opts.AccessControlPolicyEnforced,
ExcludeAccessControlPolicyEnforced: opts.ExcludeAccessControlPolicyEnforced,
ParentAccessControlPolicyId: opts.ParentAccessControlPolicyId,
}
term = strings.TrimSpace(term)
@@ -3815,3 +3862,19 @@ func (s *Server) getDirectChannel(c request.CTX, userID, otherUserID string) (*m
return channel, nil
}
func (a *App) ChannelAccessControlled(c request.CTX, channelID string) (bool, *model.AppError) {
if l := a.License(); !model.MinimumEnterpriseAdvancedLicense(l) || !*a.Config().AccessControlSettings.EnableAttributeBasedAccessControl {
return false, nil
}
_, err := a.Srv().Store().AccessControlPolicy().Get(c, channelID)
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)
} else if errors.As(err, &nfErr) {
return false, nil
}
return true, nil
}

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

@@ -64,6 +64,7 @@ type Channels struct {
Saml einterfaces.SamlInterface
Notification einterfaces.NotificationInterface
Ldap einterfaces.LdapInterface
AccessControl einterfaces.AccessControlServiceInterface
// These are used to prevent concurrent upload requests
// for a given upload session which could cause inconsistencies
@@ -132,6 +133,23 @@ func NewChannels(s *Server) (*Channels, error) {
}
})
}
if accessControlServiceInterface != nil {
app := New(ServerConnector(ch))
ch.AccessControl = accessControlServiceInterface(app)
appErr := ch.AccessControl.Init(request.EmptyContext(s.Log()))
if appErr != nil {
s.Log().Error("An error occurred while initializing Access Control", mlog.Err(appErr))
}
app.AddLicenseListener(func(newCfg, old *model.License) {
if ch.AccessControl != nil {
if appErr := ch.AccessControl.Init(request.EmptyContext(s.Log())); appErr != nil {
s.Log().Error("An error occurred while initializing Access Control", mlog.Err(appErr))
}
}
})
}
var imgErr error
decoderConcurrency := int(*ch.cfgSvc.Config().FileSettings.MaxImageDecoderConcurrency)

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

@@ -98,6 +98,18 @@ func RegisterIPFilteringInterface(f func(*App) einterfaces.IPFilteringInterface)
ipFilteringInterface = f
}
var accessControlServiceInterface func(*App) einterfaces.AccessControlServiceInterface
func RegisterAccessControlServiceInterface(f func(*App) einterfaces.AccessControlServiceInterface) {
accessControlServiceInterface = f
}
var jobsAccessControlSyncJobInterface func(*Server) ejobs.AccessControlSyncJobInterface
func RegisterJobsAccessControlSyncJobInterface(f func(*Server) ejobs.AccessControlSyncJobInterface) {
jobsAccessControlSyncJobInterface = f
}
func (s *Server) initEnterprise() {
if cloudInterface != nil {
s.Cloud = cloudInterface(s)

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

@@ -108,6 +108,8 @@ func (a *App) SessionHasPermissionToCreateJob(session model.Session, job *model.
model.JobTypeCloud,
model.JobTypeExtractContent:
return a.SessionHasPermissionTo(session, model.PermissionManageJobs), model.PermissionManageJobs
case model.JobTypeAccessControlSync:
return a.SessionHasPermissionTo(session, model.PermissionManageSystem), model.PermissionManageSystem
}
return false, nil
@@ -142,6 +144,8 @@ func (a *App) SessionHasPermissionToManageJob(session model.Session, job *model.
model.JobTypeCloud,
model.JobTypeExtractContent:
permission = model.PermissionManageJobs
case model.JobTypeAccessControlSync:
permission = model.PermissionManageSystem
}
if permission == nil {
@@ -178,6 +182,8 @@ func (a *App) SessionHasPermissionToReadJob(session model.Session, jobType strin
model.JobTypeMobileSessionMetadata,
model.JobTypeExtractContent:
return a.SessionHasPermissionTo(session, model.PermissionReadJobs), model.PermissionReadJobs
case model.JobTypeAccessControlSync:
return a.SessionHasPermissionTo(session, model.PermissionManageSystem), model.PermissionManageSystem
}
return false, nil

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

@@ -38,8 +38,8 @@ func RegisterMetricsInterface(f func(*PlatformService, string, string) einterfac
metricsInterfaceFn = f
}
var pdpInterface func(*PlatformService) einterfaces.PolicyDecisionPointInterface
var accessControlServiceInterface func(*PlatformService) einterfaces.AccessControlServiceInterface
func RegisterPdpInterface(f func(*PlatformService) einterfaces.PolicyDecisionPointInterface) {
pdpInterface = f
func RegisterAccessControlServiceInterface(f func(*PlatformService) einterfaces.AccessControlServiceInterface) {
accessControlServiceInterface = f
}

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

@@ -477,8 +477,8 @@ func (ps *PlatformService) initEnterprise() {
ps.licenseManager = licenseInterface(ps)
}
if pdpInterface != nil {
ps.pdpService = pdpInterface(ps)
if accessControlServiceInterface != nil {
ps.pdpService = accessControlServiceInterface(ps)
}
}

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

@@ -1499,6 +1499,11 @@ func (s *Server) initJobs() {
s.Jobs.RegisterJobType(model.JobTypeLdapSync, builder.MakeWorker(), builder.MakeScheduler())
}
if jobsAccessControlSyncJobInterface != nil {
builder := jobsAccessControlSyncJobInterface(s)
s.Jobs.RegisterJobType(model.JobTypeAccessControlSync, builder.MakeWorker(), builder.MakeScheduler())
}
s.Jobs.RegisterJobType(
model.JobTypeBlevePostIndexing,
indexer.MakeWorker(s.Jobs, s.platform.SearchEngine.BleveEngine.(*bleveengine.BleveEngine)),

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

@@ -2090,6 +2090,26 @@ func (a *App) SearchUsersInChannel(channelID string, term string, options *model
func (a *App) SearchUsersNotInChannel(teamID string, channelID string, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
term = strings.TrimSpace(term)
ctx := request.EmptyContext(a.Log())
if ok, err := a.ChannelAccessControlled(ctx, channelID); err != nil {
return nil, err
} else if ok {
acs := a.Srv().Channels().AccessControl
if acs != nil {
users, _, appErr := acs.QueryUsersForResource(ctx, channelID, "*", model.SubjectSearchOptions{
Term: term,
TeamID: teamID,
Limit: options.Limit,
})
if appErr != nil {
return nil, appErr
}
return users, nil
}
}
users, err := a.Srv().Store().User().SearchNotInChannel(teamID, channelID, term, options)
if err != nil {
return nil, model.NewAppError("SearchUsersNotInChannel", "app.user.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)