[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 удалений

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

@@ -855,7 +855,7 @@ test-migration:
# db_migrations differ due to a typo in the 92. migration name
# for now we exclude plugins such as playbooks and focalboard
# we also exlude systems table temporarily due to adding some keys while running the initial migration
bin/dbcmp --source "${MYSQL_DSN}" --target "${POSTGRES_DSN}" --exclude="db_migrations","ir_","focalboard","systems"
bin/dbcmp --source "${MYSQL_DSN}" --target "${POSTGRES_DSN}" --exclude="db_migrations","ir_","focalboard","systems","attributeview"
test-local-filestore: # Run tests for local filestore
$(GO) test ./platform/shared/filestore -run '^TestLocalFileBackend' -v

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

@@ -0,0 +1,515 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"strconv"
"strings"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/v8/channels/audit"
)
func (api *API) InitAccessControlPolicy() {
if !api.srv.Config().FeatureFlags.AttributeBasedAccessControl {
return
}
api.BaseRoutes.AccessControlPolicies.Handle("", api.APISessionRequired(createAccessControlPolicy)).Methods(http.MethodPut)
api.BaseRoutes.AccessControlPolicies.Handle("/search", api.APISessionRequired(searchAccessControlPolicies)).Methods(http.MethodPost)
api.BaseRoutes.AccessControlPolicies.Handle("/cel/check", api.APISessionRequired(checkExpression)).Methods(http.MethodPost)
api.BaseRoutes.AccessControlPolicies.Handle("/cel/test", api.APISessionRequired(testExpression)).Methods(http.MethodPost)
api.BaseRoutes.AccessControlPolicies.Handle("/cel/autocomplete/fields", api.APISessionRequired(getFieldsAutocomplete)).Methods(http.MethodGet)
api.BaseRoutes.AccessControlPolicies.Handle("/cel/visual_ast", api.APISessionRequired(convertToVisualAST)).Methods(http.MethodPost)
api.BaseRoutes.AccessControlPolicy.Handle("", api.APISessionRequired(getAccessControlPolicy)).Methods(http.MethodGet)
api.BaseRoutes.AccessControlPolicy.Handle("", api.APISessionRequired(deleteAccessControlPolicy)).Methods(http.MethodDelete)
api.BaseRoutes.AccessControlPolicy.Handle("/activate", api.APISessionRequired(updateActiveStatus)).Methods(http.MethodGet)
api.BaseRoutes.AccessControlPolicy.Handle("/assign", api.APISessionRequired(assignAccessPolicy)).Methods(http.MethodPost)
api.BaseRoutes.AccessControlPolicy.Handle("/unassign", api.APISessionRequired(unassignAccessPolicy)).Methods(http.MethodDelete)
api.BaseRoutes.AccessControlPolicy.Handle("/resources/channels", api.APISessionRequired(getChannelsForAccessControlPolicy)).Methods(http.MethodGet)
api.BaseRoutes.AccessControlPolicy.Handle("/resources/channels/search", api.APISessionRequired(searchChannelsForAccessControlPolicy)).Methods(http.MethodPost)
}
func createAccessControlPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
var policy model.AccessControlPolicy
if jsonErr := json.NewDecoder(r.Body).Decode(&policy); jsonErr != nil {
c.SetInvalidParamWithErr("policy", jsonErr)
return
}
auditRec := c.MakeAuditRecord("createAccessControlPolicy", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "requested", &policy)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
np, appErr := c.App.CreateOrUpdateAccessControlPolicy(c.AppContext, &policy)
if appErr != nil {
c.Err = appErr
return
}
auditRec.Success()
auditRec.AddEventObjectType("access_control_policy")
auditRec.AddEventResultState(np)
js, err := json.Marshal(np)
if err != nil {
c.Err = model.NewAppError("createAccessControlPolicy", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
if _, err := w.Write(js); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getAccessControlPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
c.RequirePolicyId()
if c.Err != nil {
return
}
policyID := c.Params.PolicyId
policy, appErr := c.App.GetAccessControlPolicy(c.AppContext, policyID)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(policy)
if err != nil {
c.Err = model.NewAppError("getAccessControlPolicy", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
if _, err := w.Write(js); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func deleteAccessControlPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
c.RequirePolicyId()
if c.Err != nil {
return
}
policyID := c.Params.PolicyId
auditRec := c.MakeAuditRecord("deleteAccessControlPolicy", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "id", policyID)
appErr := c.App.DeleteAccessControlPolicy(c.AppContext, policyID)
if appErr != nil {
c.Err = appErr
return
}
auditRec.Success()
}
func checkExpression(c *Context, w http.ResponseWriter, r *http.Request) {
// request type reserved for future expansion
// for now, we only support the expression check
checkExpressionRequest := struct {
Expression string `json:"expression"`
}{}
if jsonErr := json.NewDecoder(r.Body).Decode(&checkExpressionRequest); jsonErr != nil {
c.SetInvalidParamWithErr("user", jsonErr)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
errs, appErr := c.App.CheckExpression(c.AppContext, checkExpressionRequest.Expression)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(errs)
if err != nil {
c.Err = model.NewAppError("checkExpression", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
if _, err := w.Write(js); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func testExpression(c *Context, w http.ResponseWriter, r *http.Request) {
var checkExpressionRequest model.QueryExpressionParams
if jsonErr := json.NewDecoder(r.Body).Decode(&checkExpressionRequest); jsonErr != nil {
c.SetInvalidParamWithErr("user", jsonErr)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
users, count, appErr := c.App.TestExpression(c.AppContext, checkExpressionRequest.Expression, model.SubjectSearchOptions{
Term: checkExpressionRequest.Term,
Limit: checkExpressionRequest.Limit,
Cursor: model.SubjectCursor{
TargetID: checkExpressionRequest.After,
},
})
if appErr != nil {
c.Err = appErr
return
}
resp := model.AccessControlPolicyTestResponse{
Users: users,
Total: count,
}
js, err := json.Marshal(resp)
if err != nil {
c.Err = model.NewAppError("checkExpression", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
if _, err := w.Write(js); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func searchAccessControlPolicies(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
var props *model.AccessControlPolicySearch
err := json.NewDecoder(r.Body).Decode(&props)
if err != nil || props == nil {
c.SetInvalidParamWithErr("access_control_policy_search", err)
return
}
policies, total, appErr := c.App.SearchAccessControlPolicies(c.AppContext, *props)
if appErr != nil {
c.Err = appErr
return
}
result := model.AccessControlPoliciesWithCount{
Policies: policies,
Total: total,
}
js, err := json.Marshal(result)
if err != nil {
c.Err = model.NewAppError("searchAccessControlPolicies", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
if _, err := w.Write(js); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func updateActiveStatus(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
c.RequirePolicyId()
if c.Err != nil {
return
}
policyID := c.Params.PolicyId
auditRec := c.MakeAuditRecord("updateActiveStatus", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "id", policyID)
active := r.URL.Query().Get("active")
if active != "true" && active != "false" {
c.SetInvalidParam("active")
return
}
activeBool, err := strconv.ParseBool(active)
if err != nil {
c.SetInvalidParamWithErr("active", err)
return
}
audit.AddEventParameter(auditRec, "active", activeBool)
appErr := c.App.UpdateAccessControlPolicyActive(c.AppContext, policyID, activeBool)
if appErr != nil {
c.Err = appErr
return
}
auditRec.Success()
}
func assignAccessPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
c.RequirePolicyId()
if c.Err != nil {
return
}
policyID := c.Params.PolicyId
var assignments struct {
ChannelIds []string `json:"channel_ids"`
}
err := json.NewDecoder(r.Body).Decode(&assignments)
if err != nil {
c.SetInvalidParamWithErr("assignments", err)
return
}
auditRec := c.MakeAuditRecord("assignAccessPolicy", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "id", policyID)
audit.AddEventParameter(auditRec, "channel_ids", assignments.ChannelIds)
if len(assignments.ChannelIds) != 0 {
_, appErr := c.App.AssignAccessControlPolicyToChannels(c.AppContext, policyID, assignments.ChannelIds)
if appErr != nil {
c.Err = appErr
return
}
}
auditRec.Success()
}
func unassignAccessPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
c.RequirePolicyId()
if c.Err != nil {
return
}
policyID := c.Params.PolicyId
var assignments struct {
ChannelIds []string `json:"channel_ids"`
}
auditRec := c.MakeAuditRecord("unassignAccessPolicy", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "id", policyID)
audit.AddEventParameter(auditRec, "channel_ids", assignments.ChannelIds)
err := json.NewDecoder(r.Body).Decode(&assignments)
if err != nil {
c.SetInvalidParamWithErr("assignments", err)
return
}
if len(assignments.ChannelIds) != 0 {
appErr := c.App.UnAssignPoliciesFromChannels(c.AppContext, policyID, assignments.ChannelIds)
if appErr != nil {
c.Err = appErr
return
}
}
auditRec.Success()
}
func getChannelsForAccessControlPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
c.RequirePolicyId()
if c.Err != nil {
return
}
policyID := c.Params.PolicyId
afterID := r.URL.Query().Get("after")
if afterID != "" && !model.IsValidId(afterID) {
c.SetInvalidParam("after")
return
}
limitStr := r.URL.Query().Get("limit")
limit, err := strconv.Atoi(limitStr)
if err != nil {
c.Err = model.NewAppError("getChannelsForAccessControlPolicy", "api.access_control_policy.get_channels.limit.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
channels, total, appErr := c.App.GetChannelsForPolicy(c.AppContext, policyID, model.AccessControlPolicyCursor{
ID: afterID,
}, limit)
if appErr != nil {
c.Err = appErr
return
}
data := model.ChannelsWithCount{Channels: channels, TotalCount: total}
js, err := json.Marshal(data)
if err != nil {
c.Err = model.NewAppError("getChannelsForAccessControlPolicy", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
if _, err := w.Write(js); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func searchChannelsForAccessControlPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
c.RequirePolicyId()
if c.Err != nil {
return
}
var props *model.ChannelSearch
err := json.NewDecoder(r.Body).Decode(&props)
if err != nil || props == nil {
c.SetInvalidParamWithErr("channel_search", err)
return
}
policyID := c.Params.PolicyId
c.RequirePolicyId()
opts := model.ChannelSearchOpts{
Deleted: props.Deleted,
IncludeDeleted: props.IncludeDeleted,
Private: true,
ExcludeGroupConstrained: true,
TeamIds: props.TeamIds,
ParentAccessControlPolicyId: policyID,
}
channels, total, appErr := c.App.SearchAllChannels(c.AppContext, props.Term, opts)
if appErr != nil {
c.Err = appErr
return
}
data := model.ChannelsWithCount{Channels: channels, TotalCount: total}
channelsJSON, jsonErr := json.Marshal(data)
if jsonErr != nil {
c.Err = model.NewAppError("searchChannelsInPolicy", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
return
}
if _, err := w.Write(channelsJSON); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getFieldsAutocomplete(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
after := r.URL.Query().Get("after")
if after != "" && !model.IsValidId(after) {
c.SetInvalidParam("after")
return
} else if after == "" {
after = strings.Repeat("0", 26)
}
limitStr := r.URL.Query().Get("limit")
limit, err := strconv.Atoi(limitStr)
if err != nil {
c.Err = model.NewAppError("getFieldsAutocomplete", "api.access_control_policy.get_fields.limit.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
if limit <= 0 || limit > 100 {
c.Err = model.NewAppError("getFieldsAutocomplete", "api.access_control_policy.get_fields.limit.app_error", nil, "", http.StatusBadRequest)
return
}
ac, appErr := c.App.GetAccessControlFieldsAutocomplete(c.AppContext, after, limit)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(ac)
if err != nil {
c.Err = model.NewAppError("getExpressionAutocomplete", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
if _, err := w.Write(js); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func convertToVisualAST(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
var cel struct {
Expression string `json:"expression"`
}
if jsonErr := json.NewDecoder(r.Body).Decode(&cel); jsonErr != nil {
c.SetInvalidParamWithErr("user", jsonErr)
return
}
visualAST, appErr := c.App.ExpressionToVisualAST(c.AppContext, cel.Expression)
if appErr != nil {
c.Err = appErr
return
}
b, err := json.Marshal(visualAST)
if err != nil {
c.Err = model.NewAppError("convertToVisualAST", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
if _, err := w.Write(b); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}

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

@@ -0,0 +1,27 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import "net/http"
func (api *API) InitAccessControlPolicyLocal() {
if !api.srv.Config().FeatureFlags.AttributeBasedAccessControl {
return
}
api.BaseRoutes.AccessControlPolicies.Handle("", api.APILocal(createAccessControlPolicy)).Methods(http.MethodPut)
api.BaseRoutes.AccessControlPolicies.Handle("/search", api.APILocal(searchAccessControlPolicies)).Methods(http.MethodPost)
api.BaseRoutes.AccessControlPolicies.Handle("/cel/check", api.APILocal(checkExpression)).Methods(http.MethodPost)
api.BaseRoutes.AccessControlPolicies.Handle("/cel/test", api.APILocal(testExpression)).Methods(http.MethodPost)
api.BaseRoutes.AccessControlPolicies.Handle("/cel/autocomplete/fields", api.APILocal(getFieldsAutocomplete)).Methods(http.MethodGet)
api.BaseRoutes.AccessControlPolicies.Handle("/cel/visual_ast", api.APILocal(convertToVisualAST)).Methods(http.MethodPost)
api.BaseRoutes.AccessControlPolicy.Handle("", api.APILocal(getAccessControlPolicy)).Methods(http.MethodGet)
api.BaseRoutes.AccessControlPolicy.Handle("", api.APILocal(deleteAccessControlPolicy)).Methods(http.MethodDelete)
api.BaseRoutes.AccessControlPolicy.Handle("/activate", api.APILocal(updateActiveStatus)).Methods(http.MethodGet)
api.BaseRoutes.AccessControlPolicy.Handle("/assign", api.APILocal(assignAccessPolicy)).Methods(http.MethodPost)
api.BaseRoutes.AccessControlPolicy.Handle("/unassign", api.APILocal(unassignAccessPolicy)).Methods(http.MethodDelete)
api.BaseRoutes.AccessControlPolicy.Handle("/resources/channels", api.APILocal(getChannelsForAccessControlPolicy)).Methods(http.MethodGet)
api.BaseRoutes.AccessControlPolicy.Handle("/resources/channels/search", api.APILocal(searchChannelsForAccessControlPolicy)).Methods(http.MethodPost)
}

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

@@ -0,0 +1,613 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"context"
"os"
"testing"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/plugin/plugintest/mock"
"github.com/mattermost/mattermost/server/v8/einterfaces/mocks"
"github.com/stretchr/testify/require"
)
func TestCreateAccessControlPolicy(t *testing.T) {
os.Setenv("MM_FEATUREFLAGS_ATTRIBUTEBASEDACCESSCONTROL", "true")
th := Setup(t)
t.Cleanup(func() {
th.TearDown()
os.Unsetenv("MM_FEATUREFLAGS_ATTRIBUTEBASEDACCESSCONTROL")
})
samplePolicy := &model.AccessControlPolicy{
Type: model.AccessControlPolicyTypeChannel,
Version: model.AccessControlPolicyVersionV0_1,
Revision: 1,
Rules: []model.AccessControlPolicyRule{
{
Expression: "user.attributes.team == 'engineering'",
Actions: []string{"*"},
},
},
}
t.Run("CreateAccessControlPolicy without license", func(t *testing.T) {
_, resp, err := th.SystemAdminClient.CreateAccessControlPolicy(context.Background(), samplePolicy)
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
})
t.Run("CreateAccessControlPolicy with regular user", func(t *testing.T) {
ok := th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
require.True(t, ok, "SetLicense should return true")
// Create and set up the mock
mockAccessControlService := &mocks.AccessControlServiceInterface{}
th.App.Srv().Channels().AccessControl = mockAccessControlService
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.AccessControlSettings.EnableAttributeBasedAccessControl = model.NewPointer(true)
})
_, resp, err := th.Client.CreateAccessControlPolicy(context.Background(), samplePolicy)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
// Set up a test license with Data Retention enabled
ok := th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
require.True(t, ok, "SetLicense should return true")
// Create and set up the mock
mockAccessControlService := &mocks.AccessControlServiceInterface{}
th.App.Srv().Channels().AccessControl = mockAccessControlService
// Set up mock expectations
mockAccessControlService.On("SavePolicy", mock.AnythingOfType("*request.Context"), mock.AnythingOfType("*model.AccessControlPolicy")).Return(samplePolicy, nil).Times(1)
// Set the mock on the app
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.AccessControlSettings.EnableAttributeBasedAccessControl = model.NewPointer(true)
})
_, resp, err := client.CreateAccessControlPolicy(context.Background(), samplePolicy)
require.NoError(t, err)
CheckOKStatus(t, resp)
}, "CreateAccessControlPolicy with system admin")
}
func TestGetAccessControlPolicy(t *testing.T) {
os.Setenv("MM_FEATUREFLAGS_ATTRIBUTEBASEDACCESSCONTROL", "true")
th := Setup(t)
t.Cleanup(func() {
th.TearDown()
os.Unsetenv("MM_FEATUREFLAGS_ATTRIBUTEBASEDACCESSCONTROL")
})
samplePolicy := &model.AccessControlPolicy{
ID: model.NewId(),
Type: model.AccessControlPolicyTypeChannel,
Version: model.AccessControlPolicyVersionV0_1,
Revision: 1,
Rules: []model.AccessControlPolicyRule{
{
Expression: "user.attributes.team == 'engineering'",
Actions: []string{"*"},
},
},
}
t.Run("GetAccessControlPolicy without license", func(t *testing.T) {
_, resp, err := th.SystemAdminClient.GetAccessControlPolicy(context.Background(), samplePolicy.ID)
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
})
t.Run("GetAccessControlPolicy with regular user", func(t *testing.T) {
ok := th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
require.True(t, ok, "SetLicense should return true")
// Create and set up the mock
mockAccessControlService := &mocks.AccessControlServiceInterface{}
th.App.Srv().Channels().AccessControl = mockAccessControlService
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.AccessControlSettings.EnableAttributeBasedAccessControl = model.NewPointer(true)
})
_, resp, err := th.Client.GetAccessControlPolicy(context.Background(), samplePolicy.ID)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
ok := th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
require.True(t, ok, "SetLicense should return true")
// Create and set up the mock
mockAccessControlService := &mocks.AccessControlServiceInterface{}
th.App.Srv().Channels().AccessControl = mockAccessControlService
mockAccessControlService.On("GetPolicy", mock.AnythingOfType("*request.Context"), samplePolicy.ID).Return(samplePolicy, nil).Times(1)
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.AccessControlSettings.EnableAttributeBasedAccessControl = model.NewPointer(true)
})
_, resp, err := client.GetAccessControlPolicy(context.Background(), samplePolicy.ID)
require.NoError(t, err)
CheckOKStatus(t, resp)
}, "GetAccessControlPolicy with system admin")
}
func TestDeleteAccessControlPolicy(t *testing.T) {
os.Setenv("MM_FEATUREFLAGS_ATTRIBUTEBASEDACCESSCONTROL", "true")
th := Setup(t)
t.Cleanup(func() {
th.TearDown()
os.Unsetenv("MM_FEATUREFLAGS_ATTRIBUTEBASEDACCESSCONTROL")
})
samplePolicyID := model.NewId()
t.Run("DeleteAccessControlPolicy without license", func(t *testing.T) {
resp, err := th.SystemAdminClient.DeleteAccessControlPolicy(context.Background(), samplePolicyID)
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
})
t.Run("DeleteAccessControlPolicy with regular user", func(t *testing.T) {
ok := th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
require.True(t, ok, "SetLicense should return true")
mockAccessControlService := &mocks.AccessControlServiceInterface{}
th.App.Srv().Channels().AccessControl = mockAccessControlService
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.AccessControlSettings.EnableAttributeBasedAccessControl = model.NewPointer(true)
})
resp, err := th.Client.DeleteAccessControlPolicy(context.Background(), samplePolicyID)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
ok := th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
require.True(t, ok, "SetLicense should return true")
mockAccessControlService := &mocks.AccessControlServiceInterface{}
th.App.Srv().Channels().AccessControl = mockAccessControlService
mockAccessControlService.On("DeletePolicy", mock.AnythingOfType("*request.Context"), samplePolicyID).Return(nil).Times(1)
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.AccessControlSettings.EnableAttributeBasedAccessControl = model.NewPointer(true)
})
resp, err := client.DeleteAccessControlPolicy(context.Background(), samplePolicyID)
require.NoError(t, err)
CheckOKStatus(t, resp)
})
}
func TestCheckExpression(t *testing.T) {
os.Setenv("MM_FEATUREFLAGS_ATTRIBUTEBASEDACCESSCONTROL", "true")
th := Setup(t)
t.Cleanup(func() {
th.TearDown()
os.Unsetenv("MM_FEATUREFLAGS_ATTRIBUTEBASEDACCESSCONTROL")
})
t.Run("CheckExpression without license", func(t *testing.T) {
_, resp, err := th.SystemAdminClient.CheckExpression(context.Background(), "true")
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
})
t.Run("CheckExpression with regular user", func(t *testing.T) {
ok := th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
require.True(t, ok, "SetLicense should return true")
mockAccessControlService := &mocks.AccessControlServiceInterface{}
th.App.Srv().Channels().AccessControl = mockAccessControlService
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.AccessControlSettings.EnableAttributeBasedAccessControl = model.NewPointer(true)
})
_, resp, err := th.Client.CheckExpression(context.Background(), "true")
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
ok := th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
require.True(t, ok, "SetLicense should return true")
mockAccessControlService := &mocks.AccessControlServiceInterface{}
th.App.Srv().Channels().AccessControl = mockAccessControlService
mockAccessControlService.On("CheckExpression", mock.AnythingOfType("*request.Context"), "true").Return([]model.CELExpressionError{}, nil).Times(1)
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.AccessControlSettings.EnableAttributeBasedAccessControl = model.NewPointer(true)
})
errors, resp, err := client.CheckExpression(context.Background(), "true")
require.NoError(t, err)
CheckOKStatus(t, resp)
require.Empty(t, errors, "expected no errors")
}, "CheckExpression with system admin")
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
ok := th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
require.True(t, ok, "SetLicense should return true")
mockAccessControlService := &mocks.AccessControlServiceInterface{}
th.App.Srv().Channels().AccessControl = mockAccessControlService
mockAccessControlService.On("CheckExpression", mock.AnythingOfType("*request.Context"), "true").Return([]model.CELExpressionError{
{
Line: 1,
Column: 1,
Message: "Syntax error",
},
}, nil).Times(1)
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.AccessControlSettings.EnableAttributeBasedAccessControl = model.NewPointer(true)
})
errors, resp, err := client.CheckExpression(context.Background(), "true")
require.NoError(t, err)
CheckOKStatus(t, resp)
require.NotEmpty(t, errors, "expected errors")
}, "CheckExpression with system admin errors returned")
}
func TestTestExpression(t *testing.T) {
os.Setenv("MM_FEATUREFLAGS_ATTRIBUTEBASEDACCESSCONTROL", "true")
th := Setup(t)
t.Cleanup(func() {
th.TearDown()
os.Unsetenv("MM_FEATUREFLAGS_ATTRIBUTEBASEDACCESSCONTROL")
})
t.Run("TestExpression without license", func(t *testing.T) {
_, resp, err := th.SystemAdminClient.TestExpression(context.Background(), model.QueryExpressionParams{})
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
})
t.Run("TestExpression with regular user", func(t *testing.T) {
ok := th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
require.True(t, ok, "SetLicense should return true")
mockAccessControlService := &mocks.AccessControlServiceInterface{}
th.App.Srv().Channels().AccessControl = mockAccessControlService
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.AccessControlSettings.EnableAttributeBasedAccessControl = model.NewPointer(true)
})
_, resp, err := th.Client.TestExpression(context.Background(), model.QueryExpressionParams{})
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
ok := th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
require.True(t, ok, "SetLicense should return true")
mockAccessControlService := &mocks.AccessControlServiceInterface{}
th.App.Srv().Channels().AccessControl = mockAccessControlService
mockAccessControlService.On("QueryUsersForExpression", mock.AnythingOfType("*request.Context"), "true", model.SubjectSearchOptions{}).Return([]*model.User{}, int64(0), nil).Times(1)
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.AccessControlSettings.EnableAttributeBasedAccessControl = model.NewPointer(true)
})
usersResp, resp, err := client.TestExpression(context.Background(), model.QueryExpressionParams{
Expression: "true",
})
require.NoError(t, err)
CheckOKStatus(t, resp)
require.Empty(t, usersResp.Users, "expected no users")
require.Equal(t, int64(0), usersResp.Total, "expected count 0 users")
}, "TestExpression with system admin")
}
func TestSearchAccessControlPolicies(t *testing.T) {
os.Setenv("MM_FEATUREFLAGS_ATTRIBUTEBASEDACCESSCONTROL", "true")
th := Setup(t)
t.Cleanup(func() {
th.TearDown()
os.Unsetenv("MM_FEATUREFLAGS_ATTRIBUTEBASEDACCESSCONTROL")
})
t.Run("SearchAccessControlPolicies without license", func(t *testing.T) {
_, resp, err := th.SystemAdminClient.SearchAccessControlPolicies(context.Background(), model.AccessControlPolicySearch{})
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
})
t.Run("SearchAccessControlPolicies with regular user", func(t *testing.T) {
ok := th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
require.True(t, ok, "SetLicense should return true")
mockAccessControlService := &mocks.AccessControlServiceInterface{}
th.App.Srv().Channels().AccessControl = mockAccessControlService
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.AccessControlSettings.EnableAttributeBasedAccessControl = model.NewPointer(true)
})
_, resp, err := th.Client.SearchAccessControlPolicies(context.Background(), model.AccessControlPolicySearch{})
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
ok := th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
require.True(t, ok, "SetLicense should return true")
mockAccessControlService := &mocks.AccessControlServiceInterface{}
th.App.Srv().Channels().AccessControl = mockAccessControlService
mockAccessControlService.On("SearchPolicies", mock.AnythingOfType("*request.Context"), model.AccessControlPolicySearch{
Term: "engineering",
}).Return([]*model.AccessControlPolicy{}, int64(0), nil).Times(1)
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.AccessControlSettings.EnableAttributeBasedAccessControl = model.NewPointer(true)
})
policiesResp, resp, err := client.SearchAccessControlPolicies(context.Background(), model.AccessControlPolicySearch{
Term: "engineering",
})
require.NoError(t, err)
CheckOKStatus(t, resp)
require.Empty(t, policiesResp.Policies, "expected no policies")
require.Equal(t, int64(0), policiesResp.Total, "expected count 0 policies")
}, "SearchAccessControlPolicies with system admin")
}
func TestAssignAccessPolicy(t *testing.T) {
os.Setenv("MM_FEATUREFLAGS_ATTRIBUTEBASEDACCESSCONTROL", "true")
th := Setup(t)
t.Cleanup(func() {
th.TearDown()
os.Unsetenv("MM_FEATUREFLAGS_ATTRIBUTEBASEDACCESSCONTROL")
})
samplePolicy := &model.AccessControlPolicy{
ID: model.NewId(),
Type: model.AccessControlPolicyTypeParent,
Version: model.AccessControlPolicyVersionV0_1,
Revision: 1,
Rules: []model.AccessControlPolicyRule{
{
Expression: "user.attributes.team == 'engineering'",
Actions: []string{"*"},
},
},
}
t.Run("AssignAccessPolicy without license", func(t *testing.T) {
resp, err := th.SystemAdminClient.AssignAccessControlPolicies(context.Background(), model.NewId(), []string{model.NewId()})
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
})
t.Run("AssignAccessPolicy with regular user", func(t *testing.T) {
ok := th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
require.True(t, ok, "SetLicense should return true")
mockAccessControlService := &mocks.AccessControlServiceInterface{}
th.App.Srv().Channels().AccessControl = mockAccessControlService
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.AccessControlSettings.EnableAttributeBasedAccessControl = model.NewPointer(true)
})
resp, err := th.Client.AssignAccessControlPolicies(context.Background(), model.NewId(), []string{model.NewId()})
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
resourceID := model.NewId()
ok := th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
require.True(t, ok, "SetLicense should return true")
child, appErr := samplePolicy.Inherit(resourceID, model.AccessControlPolicyTypeChannel)
require.Nil(t, appErr)
mockAccessControlService := &mocks.AccessControlServiceInterface{}
th.App.Srv().Channels().AccessControl = mockAccessControlService
mockAccessControlService.On("GetPolicy", mock.AnythingOfType("*request.Context"), samplePolicy.ID).Return(samplePolicy, nil).Times(1)
mockAccessControlService.On("SavePolicy", mock.AnythingOfType("*request.Context"), mock.AnythingOfType("*model.AccessControlPolicy")).Return(child, nil).Times(1)
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.AccessControlSettings.EnableAttributeBasedAccessControl = model.NewPointer(true)
})
resp, err := client.AssignAccessControlPolicies(context.Background(), samplePolicy.ID, []string{resourceID})
require.NoError(t, err)
CheckOKStatus(t, resp)
}, "AssignAccessPolicy with system admin")
}
func TestUnassignAccessPolicy(t *testing.T) {
os.Setenv("MM_FEATUREFLAGS_ATTRIBUTEBASEDACCESSCONTROL", "true")
th := Setup(t)
t.Cleanup(func() {
th.TearDown()
os.Unsetenv("MM_FEATUREFLAGS_ATTRIBUTEBASEDACCESSCONTROL")
})
samplePolicy := &model.AccessControlPolicy{
ID: model.NewId(),
Type: model.AccessControlPolicyTypeParent,
Version: model.AccessControlPolicyVersionV0_1,
Revision: 1,
Rules: []model.AccessControlPolicyRule{
{
Expression: "user.attributes.team == 'engineering'",
Actions: []string{"*"},
},
},
}
t.Run("UnassignAccessPolicy without license", func(t *testing.T) {
resp, err := th.SystemAdminClient.UnassignAccessControlPolicies(context.Background(), samplePolicy.ID, []string{model.NewId()})
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
})
t.Run("UnassignAccessPolicy with regular user", func(t *testing.T) {
ok := th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
require.True(t, ok, "SetLicense should return true")
mockAccessControlService := &mocks.AccessControlServiceInterface{}
th.App.Srv().Channels().AccessControl = mockAccessControlService
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.AccessControlSettings.EnableAttributeBasedAccessControl = model.NewPointer(true)
})
resp, err := th.Client.UnassignAccessControlPolicies(context.Background(), samplePolicy.ID, []string{model.NewId()})
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
resourceID := model.NewId()
ok := th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
require.True(t, ok, "SetLicense should return true")
child, appErr := samplePolicy.Inherit(resourceID, model.AccessControlPolicyTypeChannel)
require.Nil(t, appErr)
mockAccessControlService := &mocks.AccessControlServiceInterface{}
th.App.Srv().Channels().AccessControl = mockAccessControlService
mockAccessControlService.On("GetPolicy", mock.AnythingOfType("*request.Context"), samplePolicy.ID).Return(samplePolicy, nil).Times(1)
mockAccessControlService.On("SearchPolicies", mock.AnythingOfType("*request.Context"), model.AccessControlPolicySearch{
Type: model.AccessControlPolicyTypeChannel,
ParentID: samplePolicy.ID,
}).Return([]*model.AccessControlPolicy{child}, nil).Times(1)
mockAccessControlService.On("DeletePolicy", mock.AnythingOfType("*request.Context"), child.ID).Return(nil).Times(1)
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.AccessControlSettings.EnableAttributeBasedAccessControl = model.NewPointer(true)
})
resp, err := client.UnassignAccessControlPolicies(context.Background(), samplePolicy.ID, []string{child.ID})
require.NoError(t, err)
CheckOKStatus(t, resp)
}, "UnassignAccessPolicy with system admin")
}
func TestGetChannelsForAccessControlPolicy(t *testing.T) {
os.Setenv("MM_FEATUREFLAGS_ATTRIBUTEBASEDACCESSCONTROL", "true")
th := Setup(t)
t.Cleanup(func() {
th.TearDown()
os.Unsetenv("MM_FEATUREFLAGS_ATTRIBUTEBASEDACCESSCONTROL")
})
samplePolicy := &model.AccessControlPolicy{
ID: model.NewId(),
Type: model.AccessControlPolicyTypeParent,
Version: model.AccessControlPolicyVersionV0_1,
Revision: 1,
Rules: []model.AccessControlPolicyRule{
{
Expression: "user.attributes.team == 'engineering'",
Actions: []string{"*"},
},
},
}
t.Run("GetChannelsForAccessControlPolicy without license", func(t *testing.T) {
_, resp, err := th.SystemAdminClient.GetChannelsForAccessControlPolicy(context.Background(), samplePolicy.ID, "", 1000)
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
})
t.Run("GetChannelsForAccessControlPolicy with regular user", func(t *testing.T) {
ok := th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
require.True(t, ok, "SetLicense should return true")
mockAccessControlService := &mocks.AccessControlServiceInterface{}
th.App.Srv().Channels().AccessControl = mockAccessControlService
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.AccessControlSettings.EnableAttributeBasedAccessControl = model.NewPointer(true)
})
_, resp, err := th.Client.GetChannelsForAccessControlPolicy(context.Background(), samplePolicy.ID, "", 1000)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
ok := th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
require.True(t, ok, "SetLicense should return true")
mockAccessControlService := &mocks.AccessControlServiceInterface{}
th.App.Srv().Channels().AccessControl = mockAccessControlService
mockAccessControlService.On("GetPolicy", mock.AnythingOfType("*request.Context"), samplePolicy.ID).Return(samplePolicy, nil).Times(1)
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.AccessControlSettings.EnableAttributeBasedAccessControl = model.NewPointer(true)
})
channelsResp, resp, err := client.GetChannelsForAccessControlPolicy(context.Background(), samplePolicy.ID, "", 1000)
require.NoError(t, err)
CheckOKStatus(t, resp)
require.Empty(t, channelsResp.Channels, "expected no channels")
require.Equal(t, int64(0), channelsResp.TotalCount, "expected count 0 channels")
}, "GetChannelsForAccessControlPolicy with system admin")
}
func TestSearchChannelsForAccessControlPolicy(t *testing.T) {
os.Setenv("MM_FEATUREFLAGS_ATTRIBUTEBASEDACCESSCONTROL", "true")
th := Setup(t)
t.Cleanup(func() {
th.TearDown()
os.Unsetenv("MM_FEATUREFLAGS_ATTRIBUTEBASEDACCESSCONTROL")
})
samplePolicy := &model.AccessControlPolicy{
ID: model.NewId(),
Type: model.AccessControlPolicyTypeParent,
Version: model.AccessControlPolicyVersionV0_1,
Revision: 1,
Rules: []model.AccessControlPolicyRule{
{
Expression: "user.attributes.team == 'engineering'",
Actions: []string{"*"},
},
},
}
t.Run("SearchChannelsForAccessControlPolicy with regular user", func(t *testing.T) {
ok := th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
require.True(t, ok, "SetLicense should return true")
mockAccessControlService := &mocks.AccessControlServiceInterface{}
th.App.Srv().Channels().AccessControl = mockAccessControlService
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.AccessControlSettings.EnableAttributeBasedAccessControl = model.NewPointer(true)
})
_, resp, err := th.Client.SearchChannelsForAccessControlPolicy(context.Background(), samplePolicy.ID, model.ChannelSearch{})
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
}

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

@@ -158,6 +158,9 @@ type Routes struct {
CustomProfileAttributesValues *mux.Router // 'api/v4/custom_profile_attributes/values'
AuditLogs *mux.Router // 'api/v4/audit_logs'
AccessControlPolicies *mux.Router // 'api/v4/access_control_policies'
AccessControlPolicy *mux.Router // 'api/v4/access_control_policies/{policy_id:[A-Za-z0-9]+}'
}
type API struct {
@@ -302,6 +305,9 @@ func Init(srv *app.Server) (*API, error) {
api.BaseRoutes.AuditLogs = api.BaseRoutes.APIRoot.PathPrefix("/audit_logs").Subrouter()
api.BaseRoutes.AccessControlPolicies = api.BaseRoutes.APIRoot.PathPrefix("/access_control_policies").Subrouter()
api.BaseRoutes.AccessControlPolicy = api.BaseRoutes.APIRoot.PathPrefix("/access_control_policies/{policy_id:[A-Za-z0-9]+}").Subrouter()
api.InitUser()
api.InitBot()
api.InitTeam()
@@ -354,6 +360,7 @@ func Init(srv *app.Server) (*API, error) {
api.InitScheduledPost()
api.InitCustomProfileAttributes()
api.InitAuditLogging()
api.InitAccessControlPolicy()
// If we allow testing then listen for manual testing URL hits
if *srv.Config().ServiceSettings.EnableTesting {
@@ -441,6 +448,9 @@ func InitLocal(srv *app.Server) *API {
api.BaseRoutes.CustomProfileAttributesField = api.BaseRoutes.CustomProfileAttributesFields.PathPrefix("/{field_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.CustomProfileAttributesValues = api.BaseRoutes.CustomProfileAttributes.PathPrefix("/values").Subrouter()
api.BaseRoutes.AccessControlPolicies = api.BaseRoutes.APIRoot.PathPrefix("/access_control_policies").Subrouter()
api.BaseRoutes.AccessControlPolicy = api.BaseRoutes.APIRoot.PathPrefix("/access_control_policies/{policy_id:[A-Za-z0-9]+}").Subrouter()
api.InitUserLocal()
api.InitTeamLocal()
api.InitChannelLocal()
@@ -462,6 +472,7 @@ func InitLocal(srv *app.Server) *API {
api.InitJobLocal()
api.InitSamlLocal()
api.InitCustomProfileAttributesLocal()
api.InitAccessControlPolicyLocal()
srv.LocalRouter.Handle("/api/v4/{anything:.*}", http.HandlerFunc(api.Handle404))

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

@@ -64,6 +64,7 @@ func (api *API) InitChannel() {
api.BaseRoutes.Channel.Handle("/member_counts_by_group", api.APISessionRequired(channelMemberCountsByGroup)).Methods(http.MethodGet)
api.BaseRoutes.Channel.Handle("/common_teams", api.APISessionRequired(getGroupMessageMembersCommonTeams)).Methods(http.MethodGet)
api.BaseRoutes.Channel.Handle("/convert_to_channel", api.APISessionRequired(convertGroupMessageToChannel)).Methods(http.MethodPost)
api.BaseRoutes.Channel.Handle("/access_control/attributes", api.APISessionRequired(getChannelAccessControlAttributes)).Methods(http.MethodGet)
api.BaseRoutes.ChannelForUser.Handle("/unread", api.APISessionRequired(getChannelUnread)).Methods(http.MethodGet)
@@ -823,11 +824,18 @@ func getAllChannels(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
if c.Params.ExcludeAccessControlPolicyEnforced && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
opts := model.ChannelSearchOpts{
NotAssociatedToGroup: c.Params.NotAssociatedToGroup,
ExcludeDefaultChannels: c.Params.ExcludeDefaultChannels,
IncludeDeleted: c.Params.IncludeDeleted,
ExcludePolicyConstrained: c.Params.ExcludePolicyConstrained,
NotAssociatedToGroup: c.Params.NotAssociatedToGroup,
ExcludeDefaultChannels: c.Params.ExcludeDefaultChannels,
IncludeDeleted: c.Params.IncludeDeleted,
ExcludePolicyConstrained: c.Params.ExcludePolicyConstrained,
AccessControlPolicyEnforced: c.Params.AccessControlPolicyEnforced,
ExcludeAccessControlPolicyEnforced: c.Params.ExcludeAccessControlPolicyEnforced,
}
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadComplianceDataRetentionPolicy) {
opts.IncludePolicyID = true
@@ -1309,20 +1317,23 @@ func searchAllChannels(c *Context, w http.ResponseWriter, r *http.Request) {
includeDeleted, _ := strconv.ParseBool(r.URL.Query().Get("include_deleted"))
includeDeleted = includeDeleted || props.IncludeDeleted
opts := model.ChannelSearchOpts{
NotAssociatedToGroup: props.NotAssociatedToGroup,
ExcludeDefaultChannels: props.ExcludeDefaultChannels,
TeamIds: props.TeamIds,
GroupConstrained: props.GroupConstrained,
ExcludeGroupConstrained: props.ExcludeGroupConstrained,
ExcludePolicyConstrained: props.ExcludePolicyConstrained,
IncludeSearchById: props.IncludeSearchById,
ExcludeRemote: props.ExcludeRemote,
Public: props.Public,
Private: props.Private,
IncludeDeleted: includeDeleted,
Deleted: props.Deleted,
Page: props.Page,
PerPage: props.PerPage,
NotAssociatedToGroup: props.NotAssociatedToGroup,
ExcludeDefaultChannels: props.ExcludeDefaultChannels,
TeamIds: props.TeamIds,
GroupConstrained: props.GroupConstrained,
ExcludeGroupConstrained: props.ExcludeGroupConstrained,
ExcludePolicyConstrained: props.ExcludePolicyConstrained,
IncludeSearchById: props.IncludeSearchById,
ExcludeRemote: props.ExcludeRemote,
Public: props.Public,
Private: props.Private,
IncludeDeleted: includeDeleted,
Deleted: props.Deleted,
Page: props.Page,
PerPage: props.PerPage,
AccessControlPolicyEnforced: props.AccessControlPolicyEnforced,
ExcludeAccessControlPolicyEnforced: props.ExcludeAccessControlPolicyEnforced,
ParentAccessControlPolicyId: props.ParentAccessControlPolicyId,
}
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadComplianceDataRetentionPolicy) {
opts.IncludePolicyID = true
@@ -2478,3 +2489,25 @@ func canEditChannelBanner(c *Context, originalChannel *model.Channel) {
c.Err = model.NewAppError("patchChannel", "api.channel.update_channel.banner_info.channel_type.not_allowed", nil, "", http.StatusBadRequest)
}
}
func getChannelAccessControlAttributes(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
attributes, err := c.App.GetAccessControlPolicyAttributes(c.AppContext, c.Params.ChannelId, "*")
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(attributes); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}

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)

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

@@ -267,6 +267,8 @@ channels/db/migrations/mysql/000134_create_access_control_policies.down.sql
channels/db/migrations/mysql/000134_create_access_control_policies.up.sql
channels/db/migrations/mysql/000135_sidebarchannels_categoryid.down.sql
channels/db/migrations/mysql/000135_sidebarchannels_categoryid.up.sql
channels/db/migrations/mysql/000136_create_attribute_view.down.sql
channels/db/migrations/mysql/000136_create_attribute_view.up.sql
channels/db/migrations/postgres/000001_create_teams.down.sql
channels/db/migrations/postgres/000001_create_teams.up.sql
channels/db/migrations/postgres/000002_create_team_members.down.sql
@@ -535,3 +537,5 @@ channels/db/migrations/postgres/000134_create_access_control_policies.down.sql
channels/db/migrations/postgres/000134_create_access_control_policies.up.sql
channels/db/migrations/postgres/000135_sidebarchannels_categoryid.down.sql
channels/db/migrations/postgres/000135_sidebarchannels_categoryid.up.sql
channels/db/migrations/postgres/000136_create_attribute_view.down.sql
channels/db/migrations/postgres/000136_create_attribute_view.up.sql

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

@@ -0,0 +1 @@
DROP VIEW IF EXISTS AttributeView;

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

@@ -0,0 +1,11 @@
CREATE OR REPLACE VIEW AttributeView AS
SELECT
pv.GroupID,
pv.TargetID,
pv.TargetType,
JSON_OBJECTAGG(pf.Name, pv.Value)
AS Attributes
FROM PropertyValues pv
LEFT JOIN PropertyFields pf ON pf.ID = pv.FieldID
GROUP BY GroupID, TargetID, TargetType;

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

@@ -0,0 +1 @@
DROP MATERIALIZED VIEW IF EXISTS AttributeView;

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

@@ -0,0 +1,37 @@
CREATE OR REPLACE PROCEDURE create_attribute_view()
LANGUAGE plpgsql
AS $$
BEGIN
EXECUTE '
CREATE MATERIALIZED VIEW IF NOT EXISTS AttributeView AS
SELECT
pv.GroupID,
pv.TargetID,
pv.TargetType,
jsonb_object_agg(
pf.Name,
CASE
WHEN pf.Type = ''select'' THEN (
SELECT to_jsonb(options.name)
FROM jsonb_to_recordset(pf.Attrs->''options'') AS options(id text, name text)
WHERE options.id = pv.Value #>> ''{}''
LIMIT 1
)
WHEN pf.Type = ''multiselect'' THEN (
SELECT jsonb_agg(option_names.name)
FROM jsonb_array_elements_text(pv.Value) AS option_id
JOIN jsonb_to_recordset(pf.Attrs->''options'') AS option_names(id text, name text)
ON option_id = option_names.id
)
ELSE pv.Value
END
) AS Attributes FROM PropertyValues pv
LEFT JOIN PropertyFields pf ON pf.ID = pv.FieldID
WHERE pv.DeleteAt = 0 OR pv.DeleteAt IS NULL
GROUP BY pv.GroupID, pv.TargetID, pv.TargetType
';
END;
$$;
call create_attribute_view();
DROP PROCEDURE create_attribute_view();

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

@@ -24,6 +24,7 @@ const mySQLDeadlockCode = uint16(1213)
type RetryLayer struct {
store.Store
AccessControlPolicyStore store.AccessControlPolicyStore
AttributesStore store.AttributesStore
AuditStore store.AuditStore
BotStore store.BotStore
ChannelStore store.ChannelStore
@@ -79,6 +80,10 @@ func (s *RetryLayer) AccessControlPolicy() store.AccessControlPolicyStore {
return s.AccessControlPolicyStore
}
func (s *RetryLayer) Attributes() store.AttributesStore {
return s.AttributesStore
}
func (s *RetryLayer) Audit() store.AuditStore {
return s.AuditStore
}
@@ -280,6 +285,11 @@ type RetryLayerAccessControlPolicyStore struct {
Root *RetryLayer
}
type RetryLayerAttributesStore struct {
store.AttributesStore
Root *RetryLayer
}
type RetryLayerAuditStore struct {
store.AuditStore
Root *RetryLayer
@@ -583,27 +593,6 @@ func (s *RetryLayerAccessControlPolicyStore) Get(c request.CTX, id string) (*mod
}
func (s *RetryLayerAccessControlPolicyStore) GetAll(rctxc request.CTX, opts store.GetPolicyOptions) ([]*model.AccessControlPolicy, error) {
tries := 0
for {
result, err := s.AccessControlPolicyStore.GetAll(rctxc, opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerAccessControlPolicyStore) Save(c request.CTX, policy *model.AccessControlPolicy) (*model.AccessControlPolicy, error) {
tries := 0
@@ -625,6 +614,27 @@ func (s *RetryLayerAccessControlPolicyStore) Save(c request.CTX, policy *model.A
}
func (s *RetryLayerAccessControlPolicyStore) SearchPolicies(rctx request.CTX, opts model.AccessControlPolicySearch) ([]*model.AccessControlPolicy, int64, error) {
tries := 0
for {
result, resultVar1, err := s.AccessControlPolicyStore.SearchPolicies(rctx, opts)
if err == nil {
return result, resultVar1, nil
}
if !isRepeatableError(err) {
return result, resultVar1, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, resultVar1, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerAccessControlPolicyStore) SetActiveStatus(c request.CTX, id string, active bool) (*model.AccessControlPolicy, error) {
tries := 0
@@ -646,6 +656,90 @@ func (s *RetryLayerAccessControlPolicyStore) SetActiveStatus(c request.CTX, id s
}
func (s *RetryLayerAttributesStore) GetChannelMembersToRemove(rctx request.CTX, channelID string, opts model.SubjectSearchOptions) ([]*model.ChannelMember, error) {
tries := 0
for {
result, err := s.AttributesStore.GetChannelMembersToRemove(rctx, channelID, opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerAttributesStore) GetSubject(rctx request.CTX, ID string, groupID string) (*model.Subject, error) {
tries := 0
for {
result, err := s.AttributesStore.GetSubject(rctx, ID, groupID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerAttributesStore) RefreshAttributes() error {
tries := 0
for {
err := s.AttributesStore.RefreshAttributes()
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerAttributesStore) SearchUsers(rctx request.CTX, opts model.SubjectSearchOptions) ([]*model.User, int64, error) {
tries := 0
for {
result, resultVar1, err := s.AttributesStore.SearchUsers(rctx, opts)
if err == nil {
return result, resultVar1, nil
}
if !isRepeatableError(err) {
return result, resultVar1, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, resultVar1, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerAuditStore) Get(userID string, offset int, limit int) (model.Audits, error) {
tries := 0
@@ -16513,6 +16607,7 @@ func New(childStore store.Store) *RetryLayer {
}
newStore.AccessControlPolicyStore = &RetryLayerAccessControlPolicyStore{AccessControlPolicyStore: childStore.AccessControlPolicy(), Root: &newStore}
newStore.AttributesStore = &RetryLayerAttributesStore{AttributesStore: childStore.Attributes(), Root: &newStore}
newStore.AuditStore = &RetryLayerAuditStore{AuditStore: childStore.Audit(), Root: &newStore}
newStore.BotStore = &RetryLayerBotStore{BotStore: childStore.Bot(), Root: &newStore}
newStore.ChannelStore = &RetryLayerChannelStore{ChannelStore: childStore.Channel(), Root: &newStore}

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

@@ -67,6 +67,7 @@ func genStore() *mocks.Store {
mock.On("PropertyGroup").Return(&mocks.PropertyGroupStore{})
mock.On("PropertyValue").Return(&mocks.PropertyValueStore{})
mock.On("AccessControlPolicy").Return(&mocks.AccessControlPolicyStore{})
mock.On("Attributes").Return(&mocks.AttributesStore{})
return mock
}

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

@@ -4,6 +4,7 @@
package sqlstore
import (
"bytes"
"database/sql"
"encoding/json"
"fmt"
@@ -17,6 +18,8 @@ import (
sq "github.com/mattermost/squirrel"
)
const MaxPerPage = 1000
// Usually rules are how we define the policy, hence the versioning. For v0.1, we also
// have the imports field which is used to link with the parent policy.
type accessControlPolicyV0_1 struct {
@@ -152,7 +155,7 @@ func newSqlAccessControlPolicyStore(sqlStore *SqlStore, metrics einterfaces.Metr
return s
}
func preSaveAccessControlPolicy(policy, existingPolicy *model.AccessControlPolicy) {
func preSaveAccessControlPolicy(policy *storeAccessControlPolicy, existingPolicy *model.AccessControlPolicy) {
// since policies are immutable, we need to create a new revision
// also if it's going to be saved, eventually it will be the new one
// we overwrite createAt to make sure it gets the correct timestamp before saving
@@ -181,38 +184,6 @@ func (s *SqlAccessControlPolicyStore) Save(rctx request.CTX, policy *model.Acces
return nil, errors.Wrapf(err, "failed to fetch policy with id=%s", policy.ID)
}
if existingPolicy != nil {
// move existing policy to history
tmp, err2 := fromModel(existingPolicy)
if err2 != nil {
return nil, errors.Wrapf(err2, "failed to parse policy with id=%s", policy.ID)
}
data := tmp.Data
props := tmp.Props
if s.IsBinaryParamEnabled() {
data = AppendBinaryFlag(data)
props = AppendBinaryFlag(props)
}
query := s.getQueryBuilder().
Insert("AccessControlPolicyHistory").
Columns(accessControlPolicyHistorySliceColumns()...).
Values(tmp.ID, tmp.Name, tmp.Type, tmp.CreateAt, tmp.Revision, tmp.Version, data, props)
_, err = tx.ExecBuilder(query)
if err != nil {
return nil, errors.Wrapf(err, "failed to save policy with id=%s to history", policy.ID)
}
err = s.deleteT(rctx, tx, existingPolicy.ID)
if err != nil {
return nil, errors.Wrapf(err, "failed to delete policy with id=%s", policy.ID)
}
}
preSaveAccessControlPolicy(policy, existingPolicy)
storePolicy, err := fromModel(policy)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse policy with Id=%s", policy.ID)
@@ -225,6 +196,57 @@ func (s *SqlAccessControlPolicyStore) Save(rctx request.CTX, policy *model.Acces
props = AppendBinaryFlag(props)
}
if existingPolicy != nil {
if existingPolicy.Type != policy.Type {
return nil, errors.New("cannot change type of existing policy")
}
// move existing policy to history
tmp, err2 := fromModel(existingPolicy)
if err2 != nil {
return nil, errors.Wrapf(err2, "failed to parse policy with id=%s", policy.ID)
}
// Check if the policy has actually changed
// We compare data, name, and version fields, and ensure type hasn't changed
if bytes.Equal(storePolicy.Data, tmp.Data) &&
storePolicy.Name == tmp.Name &&
storePolicy.Version == tmp.Version {
return existingPolicy, nil
}
existingData := tmp.Data
existingProps := tmp.Props
if s.IsBinaryParamEnabled() {
existingData = AppendBinaryFlag(existingData)
existingProps = AppendBinaryFlag(existingProps)
}
query := s.getQueryBuilder().
Insert("AccessControlPolicyHistory").
Columns(accessControlPolicyHistorySliceColumns()...).
Values(tmp.ID, tmp.Name, tmp.Type, tmp.CreateAt, tmp.Revision, tmp.Version, existingData, existingProps)
_, err = tx.ExecBuilder(query)
if err != nil {
return nil, errors.Wrapf(err, "failed to save policy with id=%s to history", policy.ID)
}
err = s.deleteT(rctx, tx, existingPolicy.ID)
if err != nil {
return nil, errors.Wrapf(err, "failed to delete policy with id=%s", policy.ID)
}
} else {
// if there is no existing policy, also check the history table
// to make sure we are not overwriting an existing policy
existingPolicy, err = s.getHistoryT(rctx, tx, policy.ID)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return nil, errors.Wrapf(err, "failed to fetch policy with id=%s", policy.ID)
}
}
preSaveAccessControlPolicy(storePolicy, existingPolicy)
query := s.getQueryBuilder().
Insert("AccessControlPolicies").
Columns(accessControlPolicySliceColumns()...).
@@ -329,11 +351,29 @@ func (s *SqlAccessControlPolicyStore) SetActiveStatus(rctx request.CTX, id strin
if err != nil {
return nil, errors.Wrapf(err, "failed to build query for policy with id=%s", id)
}
_, err = tx.Query(query, args...)
_, err = tx.Exec(query, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to update policy with id=%s", id)
}
if existingPolicy.Type == model.AccessControlPolicyTypeParent {
// if the policy is a parent, we need to update the child policies
var expr sq.Sqlizer
if s.DriverName() == model.DatabaseDriverPostgres {
expr = sq.Expr("Data->'imports' @> ?::jsonb", fmt.Sprintf("%q", id))
} else {
expr = sq.Expr("JSON_CONTAINS(JSON_EXTRACT(Data, '$.imports'), ?)", fmt.Sprintf("%q", id))
}
query, args, err = s.getQueryBuilder().Update("AccessControlPolicies").Set("Active", active).Where(expr).ToSql()
if err != nil {
return nil, errors.Wrapf(err, "failed to build query for policy with id=%s", id)
}
_, err = tx.Exec(query, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to update child policies with id=%s", id)
}
}
if err = tx.Commit(); err != nil {
return nil, errors.Wrap(err, "commit_transaction")
}
@@ -345,7 +385,7 @@ func (s *SqlAccessControlPolicyStore) Get(_ request.CTX, id string) (*model.Acce
p := storeAccessControlPolicy{}
query := s.selectQueryBuilder.Where(sq.Eq{"ID": id})
err := s.GetReplica().GetBuilder(&p, query)
err := s.GetMaster().GetBuilder(&p, query)
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("AccessControlPolicy", id)
@@ -388,7 +428,35 @@ func (s *SqlAccessControlPolicyStore) getT(_ request.CTX, tx *sqlxTxWrapper, id
return policy, nil
}
func (s *SqlAccessControlPolicyStore) GetAll(_ request.CTX, opts store.GetPolicyOptions) ([]*model.AccessControlPolicy, error) {
func (s *SqlAccessControlPolicyStore) getHistoryT(_ request.CTX, tx *sqlxTxWrapper, id string) (*model.AccessControlPolicy, error) {
query := s.getQueryBuilder().
Select(accessControlPolicyHistorySliceColumns()...).
From("AccessControlPolicyHistory").
Where(
sq.Eq{"ID": id},
).OrderBy("Revision DESC").
Limit(1)
sql, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrapf(err, "failed to build query for policy with id=%s", id)
}
var storePolicy storeAccessControlPolicy
err = tx.Get(&storePolicy, sql, args...)
if err != nil {
return nil, err
}
policy, err := storePolicy.toModel()
if err != nil {
return nil, errors.Wrapf(err, "failed to parse policy with id=%s", id)
}
return policy, nil
}
func (s *SqlAccessControlPolicyStore) GetAll(_ request.CTX, opts model.GetAccessControlPolicyOptions) ([]*model.AccessControlPolicy, model.AccessControlPolicyCursor, error) {
p := []storeAccessControlPolicy{}
query := s.selectQueryBuilder
@@ -404,18 +472,156 @@ func (s *SqlAccessControlPolicyStore) GetAll(_ request.CTX, opts store.GetPolicy
query = query.Where(sq.Eq{"Type": opts.Type})
}
cursor := opts.Cursor
if !cursor.IsEmpty() {
query = query.Where(sq.Or{
sq.Gt{"Id": cursor.ID},
})
}
limit := uint64(opts.Limit)
if limit < 1 {
limit = 10
} else if limit > MaxPerPage {
limit = MaxPerPage
}
query = query.Limit(limit)
err := s.GetReplica().SelectBuilder(&p, query)
if err != nil {
return nil, errors.Wrapf(err, "failed to find policies with opts={\"parentID\"=%q, \"resourceType\"=%q", opts.ParentID, opts.Type)
return nil, cursor, errors.Wrapf(err, "failed to find policies with opts={\"parentID\"=%q, \"resourceType\"=%q", opts.ParentID, opts.Type)
}
policies := make([]*model.AccessControlPolicy, len(p))
for i := range p {
policies[i], err = p[i].toModel()
if err != nil {
return nil, errors.Wrapf(err, "failed to parse policy with id=%s", p[i].ID)
return nil, cursor, errors.Wrapf(err, "failed to parse policy with id=%s", p[i].ID)
}
}
return policies, nil
if len(policies) != 0 {
cursor.ID = policies[len(policies)-1].ID
}
return policies, cursor, nil
}
func (s *SqlAccessControlPolicyStore) SearchPolicies(rctx request.CTX, opts model.AccessControlPolicySearch) ([]*model.AccessControlPolicy, int64, error) {
type wrapper struct {
storeAccessControlPolicy
ChildIDs json.RawMessage
}
p := []wrapper{}
var query sq.SelectBuilder
if opts.IncludeChildren && opts.ParentID == "" {
columns := accessControlPolicySliceColumns("p")
if s.DriverName() == model.DatabaseDriverPostgres {
childIDs := `COALESCE((SELECT JSON_AGG(c.ID)
FROM AccessControlPolicies c
WHERE c.Type != 'parent'
AND c.Data->'imports' @> JSONB_BUILD_ARRAY(p.ID)), '[]'::json) AS ChildIDs`
columns = append(columns, childIDs)
} else {
childIDs := `COALESCE((SELECT JSON_ARRAYAGG(c.ID)
FROM AccessControlPolicies c
WHERE c.Type != 'parent'
AND JSON_SEARCH(c.Data->'$.imports', 'one', p.ID) IS NOT NULL), JSON_ARRAY()) AS ChildIDs`
columns = append(columns, childIDs)
}
query = s.getQueryBuilder().Select(columns...).From("AccessControlPolicies p")
} else {
query = s.selectQueryBuilder
}
count := s.getQueryBuilder().Select("COUNT(*)").From("AccessControlPolicies")
if opts.Term != "" {
condition := sq.Like{"Name": fmt.Sprintf("%%%s%%", opts.Term)}
query = query.Where(condition)
count = count.Where(condition)
}
if opts.Type != "" {
condition := sq.Eq{"Type": opts.Type}
query = query.Where(condition)
count = count.Where(condition)
}
if opts.ParentID != "" {
if s.DriverName() == model.DatabaseDriverPostgres {
condition := sq.Expr("Data->'imports' @> ?", fmt.Sprintf("%q", opts.ParentID))
query = query.Where(condition)
count = count.Where(condition)
} else {
condition := sq.Expr("JSON_CONTAINS(JSON_EXTRACT(Data, '$.imports'), ?)", fmt.Sprintf("%q", opts.ParentID))
query = query.Where(condition)
count = count.Where(condition)
}
}
if opts.Active {
query = query.Where(sq.Eq{"Active": true})
count = count.Where(sq.Eq{"Active": true})
}
cursor := opts.Cursor
if !cursor.IsEmpty() {
query = query.Where(sq.Gt{"Id": cursor.ID})
}
limit := uint64(opts.Limit)
if limit < 1 {
limit = 10
} else if limit > MaxPerPage {
limit = MaxPerPage
}
query = query.Limit(limit)
err := s.GetReplica().SelectBuilder(&p, query)
if err != nil {
return nil, 0, errors.Wrapf(err, "failed to find policies with opts={\"name\"=%q, \"resourceType\"=%q", opts.Term, opts.Type)
}
policies := make([]*model.AccessControlPolicy, len(p))
for i := range p {
m, err2 := p[i].toModel()
if err2 != nil {
return nil, 0, errors.Wrapf(err2, "failed to parse policy with id=%s", p[i].ID)
}
// Props field is not guaranteed to be persisted correctly, and it shouldn't be.
// This is a field that we want to include metadata, some values may be stored but
// not all of them. For example for the childs, we don't want to update it whenever a
// child policy changes.
if opts.IncludeChildren && opts.ParentID == "" {
if m.Props == nil {
m.Props = make(map[string]any)
}
// Unmarshal the JSON array into a slice of strings
var childIDs []string
if err = json.Unmarshal(p[i].ChildIDs, &childIDs); err != nil {
return nil, 0, errors.Wrapf(err, "failed to unmarshal child IDs for policy with id=%s", p[i].ID)
}
m.Props["child_ids"] = childIDs
}
policies[i] = m
}
var total int64
err = s.GetReplica().GetBuilder(&total, count)
if err != nil {
return nil, 0, errors.Wrapf(err, "failed to count policies with opts={\"name\"=%q, \"resourceType\"=%q", opts.Term, opts.Type)
}
if len(policies) != 0 {
cursor.ID = policies[len(policies)-1].ID
}
return policies, total, nil
}

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

@@ -0,0 +1,253 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"encoding/json"
"fmt"
"strings"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/store"
"github.com/mattermost/mattermost/server/v8/einterfaces"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
)
type SqlAttributesStore struct {
*SqlStore
metrics einterfaces.MetricsInterface
selectQueryBuilder sq.SelectBuilder
}
func attributesSliceColumns(prefix ...string) []string {
var p string
if len(prefix) == 1 {
p = prefix[0] + "."
} else if len(prefix) > 1 {
panic("cannot accept multiple prefixes")
}
return []string{
p + "TargetID as ID",
p + "TargetType as Type",
p + "Attributes",
}
}
func newSqlAttributesStore(sqlStore *SqlStore, metrics einterfaces.MetricsInterface) store.AttributesStore {
s := &SqlAttributesStore{
SqlStore: sqlStore,
metrics: metrics,
}
s.selectQueryBuilder = s.getQueryBuilder().Select(attributesSliceColumns()...).From("AttributeView")
return s
}
func (s *SqlAttributesStore) RefreshAttributes() error {
if s.DriverName() == model.DatabaseDriverPostgres {
if _, err := s.GetMaster().Exec("REFRESH MATERIALIZED VIEW AttributeView"); err != nil {
return errors.Wrap(err, "error refreshing materialized view AttributeView")
}
}
return nil
}
func (s *SqlAttributesStore) GetSubject(rctx request.CTX, ID, groupID string) (*model.Subject, error) {
query := s.selectQueryBuilder.Where(sq.And{sq.Eq{"TargetID": ID}, sq.Eq{"GroupID": groupID}})
q, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "failed to build query for subject")
}
row := s.GetReplica().QueryRowxContext(rctx.Context(), q, args...)
if err := row.Err(); err != nil {
return nil, errors.Wrap(err, "failed to get subject")
}
var subject model.Subject
var properties []byte
if err := row.Scan(&subject.ID, &subject.Type, &properties); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Attributes", ID)
}
return nil, errors.Wrap(err, "failed to scan subject row")
}
if err := json.Unmarshal(properties, &subject.Attributes); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal attributes")
}
return &subject, nil
}
func (s *SqlAttributesStore) SearchUsers(rctx request.CTX, opts model.SubjectSearchOptions) ([]*model.User, int64, error) {
query := s.getQueryBuilder().
Select(getUsersColumns()...).From("Users").LeftJoin("AttributeView ON Users.Id = AttributeView.TargetID").
OrderBy("Users.Id ASC")
count := s.getQueryBuilder().Select("COUNT(*)").From("Users").LeftJoin("AttributeView ON Users.Id = AttributeView.TargetID")
if opts.Query != "" {
query = query.Where(sq.Expr(opts.Query, opts.Args...))
count = count.Where(sq.Expr(opts.Query, opts.Args...))
}
argCount := len(opts.Args)
if opts.Limit > 0 {
query = query.Limit(uint64(opts.Limit))
} else if opts.Limit > MaxPerPage {
query = query.Limit(uint64(MaxPerPage))
}
if !opts.AllowInactive {
query = query.Where("Users.DeleteAt = 0")
count = count.Where("Users.DeleteAt = 0")
}
if opts.TeamID != "" {
argCount++
if s.DriverName() == model.DatabaseDriverMysql {
query = query.Where("Users.Id IN (SELECT UserId FROM TeamMembers WHERE TeamId = ? AND DeleteAt = 0)", opts.TeamID)
count = count.Where("Users.Id IN (SELECT UserId FROM TeamMembers WHERE TeamId = ? AND DeleteAt = 0)", opts.TeamID)
} else {
query = query.Where(sq.Expr(fmt.Sprintf("Users.Id IN (SELECT UserId FROM TeamMembers WHERE TeamId = $%d AND DeleteAt = 0)", argCount), opts.TeamID))
count = count.Where(sq.Expr(fmt.Sprintf("Users.Id IN (SELECT UserId FROM TeamMembers WHERE TeamId = $%d AND DeleteAt = 0)", argCount), opts.TeamID))
}
}
if opts.ExcludeChannelMembers != "" {
argCount++
if s.DriverName() == model.DatabaseDriverMysql {
query = query.Where(sq.Expr("NOT EXISTS (SELECT 1 FROM ChannelMembers WHERE ChannelMembers.UserId = Users.Id AND ChannelMembers.ChannelId = ?)", opts.ExcludeChannelMembers))
} else {
query = query.Where(sq.Expr(fmt.Sprintf("NOT EXISTS (SELECT 1 FROM ChannelMembers WHERE ChannelMembers.UserId = Users.Id AND ChannelMembers.ChannelId = $%d)", argCount), opts.ExcludeChannelMembers))
}
}
if opts.Cursor.TargetID != "" {
argCount++
if s.DriverName() == model.DatabaseDriverMysql {
query = query.Where(sq.Expr("TargetID > ?", opts.Cursor.TargetID))
} else {
query = query.Where(sq.Expr(fmt.Sprintf("TargetID > $%d", argCount), opts.Cursor.TargetID))
}
}
searchFields := make([]string, 0, len(UserSearchTypeNames))
for _, field := range UserSearchTypeNames {
searchFields = append(searchFields, strings.Join([]string{"Users", field}, "."))
}
if term := opts.Term; strings.TrimSpace(term) != "" {
_, query = generateSearchQueryForExpression(query, strings.Fields(term), searchFields, s.DriverName() == model.DatabaseDriverPostgres, argCount)
_, count = generateSearchQueryForExpression(count, strings.Fields(term), searchFields, s.DriverName() == model.DatabaseDriverPostgres, argCount)
}
q, args, err := query.ToSql()
if err != nil {
return nil, 0, errors.Wrap(err, "failed to build query for subjects")
}
users := []*model.User{}
if err = s.GetReplica().Select(&users, q, args...); err != nil {
return nil, 0, errors.Wrapf(err, "failed to find Users with term=%s and searchType=%v", opts.Term, searchFields)
}
for _, u := range users {
u.Sanitize(map[string]bool{})
}
var total int64
if !opts.IgnoreCount {
err = s.GetReplica().GetBuilder(&total, count)
if err != nil {
return nil, 0, errors.Wrapf(err, "failed to count Users with term=%s and searchType=%v", opts.Term, searchFields)
}
}
return users, total, nil
}
func (s *SqlAttributesStore) GetChannelMembersToRemove(rctx request.CTX, channelID string, opts model.SubjectSearchOptions) ([]*model.ChannelMember, error) {
query := s.getQueryBuilder().
Select(channelMemberSliceColumns()...).From("ChannelMembers").LeftJoin("AttributeView ON ChannelMembers.UserId = AttributeView.TargetID").
OrderBy("ChannelMembers.UserId ASC")
if opts.Query != "" {
query = query.Where(sq.Expr(fmt.Sprintf("(NOT (%s) OR AttributeView.TargetID IS NULL)", opts.Query), opts.Args...))
}
argCount := len(opts.Args)
if s.DriverName() == model.DatabaseDriverMysql {
query = query.Where(sq.Eq{"ChannelMembers.ChannelId": channelID})
} else {
argCount++
query = query.Where(sq.Expr(fmt.Sprintf("ChannelMembers.ChannelId = $%d", argCount), channelID))
}
if opts.Limit > 0 {
query = query.Limit(uint64(opts.Limit))
} else if opts.Limit > MaxPerPage {
query = query.Limit(uint64(MaxPerPage))
}
if opts.Cursor.TargetID != "" {
argCount++
if s.DriverName() == model.DatabaseDriverMysql {
query = query.Where(sq.Expr("ChannelMembers.UserId > ?", opts.Cursor.TargetID))
} else {
query = query.Where(sq.Expr(fmt.Sprintf("ChannelMembers.UserId > $%d", argCount), opts.Cursor.TargetID))
}
}
q, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "failed to build query for subjects")
}
members := []*model.ChannelMember{}
if err := s.GetReplica().Select(&members, q, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find channel members with for channel id=%s", channelID)
}
return members, nil
}
func generateSearchQueryForExpression(query sq.SelectBuilder, terms []string, fields []string, isPostgreSQL bool, prevArgs int) (int, sq.SelectBuilder) {
for _, term := range terms {
searchFields := []string{}
termArgs := []any{}
for _, field := range fields {
if isPostgreSQL {
prevArgs++
searchFields = append(searchFields, fmt.Sprintf("lower(%s) LIKE lower($%d) escape '*' ", field, prevArgs))
} else {
searchFields = append(searchFields, fmt.Sprintf("%s LIKE ? escape '*' ", field))
}
termArgs = append(termArgs, fmt.Sprintf("%%%s%%", strings.TrimLeft(term, "@")))
}
if isPostgreSQL {
prevArgs++
searchFields = append(searchFields, fmt.Sprintf("lower(%s) LIKE lower($%d) escape '*' ", "Id", prevArgs))
} else {
searchFields = append(searchFields, "Id = ?")
}
termArgs = append(termArgs, strings.TrimLeft(term, "@"))
query = query.Where(fmt.Sprintf("(%s)", strings.Join(searchFields, " OR ")), termArgs...)
}
return prevArgs, query
}

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

@@ -0,0 +1,14 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"testing"
"github.com/mattermost/mattermost/server/v8/channels/store/storetest"
)
func TestAttributesStore(t *testing.T) {
StoreTestWithSqlStore(t, storetest.TestAttributesStore)
}

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

@@ -108,7 +108,7 @@ func channelMemberSliceColumns() []string {
// channelSliceColumns returns fields of the channel as a string slice.
// Optionally, you can add a prefix (accepts only 1 value) to the fields.
func channelSliceColumns(prefix ...string) []string {
func channelSliceColumns(isSelect bool, prefix ...string) []string {
var p string
if len(prefix) == 1 {
p = prefix[0] + "."
@@ -116,7 +116,7 @@ func channelSliceColumns(prefix ...string) []string {
panic("cannot accept multiple prefixes")
}
return []string{
columns := []string{
p + "Id",
p + "CreateAt",
p + "UpdateAt",
@@ -138,6 +138,16 @@ func channelSliceColumns(prefix ...string) []string {
p + "LastRootPostAt",
p + "BannerInfo",
}
if isSelect {
if p == "" {
p = "Channels."
}
columns = append(columns, fmt.Sprintf("EXISTS (SELECT 1 FROM AccessControlPolicies acp WHERE acp.ID = %sId) AS PolicyEnforced", p))
}
return columns
}
func channelToSlice(channel *model.Channel) []any {
@@ -493,7 +503,7 @@ func newSqlChannelStore(sqlStore *SqlStore, metrics einterfaces.MetricsInterface
metrics: metrics,
}
s.tableSelectQuery = s.getQueryBuilder().Select(channelSliceColumns()...).From("Channels")
s.tableSelectQuery = s.getQueryBuilder().Select(channelSliceColumns(true)...).From("Channels")
s.sidebarCategorySelectQuery = s.getQueryBuilder().
Select("SidebarCategories.Id", "SidebarCategories.UserId", "SidebarCategories.TeamId", "SidebarCategories.SortOrder", "SidebarCategories.Sorting", "SidebarCategories.Type", "SidebarCategories.DisplayName", "SidebarCategories.Muted", "SidebarCategories.Collapsed").
@@ -731,7 +741,7 @@ func (s SqlChannelStore) saveChannelT(transaction *sqlxTxWrapper, channel *model
insert := s.getQueryBuilder().
Insert("Channels").
Columns(channelSliceColumns()...).
Columns(channelSliceColumns(false)...).
Values(channelToSlice(channel)...)
if s.DriverName() == model.DatabaseDriverMysql {
insert = insert.SuffixExpr(sq.Expr("ON DUPLICATE KEY UPDATE Id=Id"))
@@ -908,7 +918,7 @@ func (s SqlChannelStore) Get(id string, allowFromCache bool) (*model.Channel, er
//nolint:unparam
func (s SqlChannelStore) GetMany(ids []string, allowFromCache bool) (model.ChannelList, error) {
query := s.getQueryBuilder().
Select(channelSliceColumns()...).
Select(channelSliceColumns(true)...).
From("Channels").
Where(sq.Eq{"Id": ids})
sql, args, err := query.ToSql()
@@ -1070,7 +1080,7 @@ func (s SqlChannelStore) PermanentDeleteMembersByChannel(rctx request.CTX, chann
func (s SqlChannelStore) GetChannels(teamId string, userId string, opts *model.ChannelSearchOpts) (model.ChannelList, error) {
query := s.getQueryBuilder().
Select(channelSliceColumns("ch")...).
Select(channelSliceColumns(true, "ch")...).
From("Channels ch, ChannelMembers cm").
Where(
sq.And{
@@ -1125,7 +1135,7 @@ func (s SqlChannelStore) GetChannels(teamId string, userId string, opts *model.C
func (s SqlChannelStore) GetChannelsByUser(userId string, includeDeleted bool, lastDeleteAt, pageSize int, fromChannelID string) (model.ChannelList, error) {
query := s.getQueryBuilder().
Select(channelSliceColumns("Channels")...).
Select(channelSliceColumns(true, "Channels")...).
From("Channels, ChannelMembers").
Where(
sq.And{
@@ -1233,7 +1243,7 @@ func (s SqlChannelStore) getAllChannelsQuery(opts store.ChannelSearchOpts, forCo
Select("count(c.Id)")
} else {
selectQuery = s.getQueryBuilder().
Select(channelSliceColumns("c")...).
Select(channelSliceColumns(true, "c")...).
Columns(
"Teams.DisplayName AS TeamDisplayName",
"Teams.Name AS TeamName",
@@ -1280,6 +1290,11 @@ func (s SqlChannelStore) getAllChannelsQuery(opts store.ChannelSearchOpts, forCo
if opts.ExcludePolicyConstrained {
query = query.Where("RetentionPoliciesChannels.ChannelId IS NULL")
}
if opts.ExcludeAccessControlPolicyEnforced {
query = query.Where("c.Id NOT IN (SELECT ID From AccessControlPolicies WHERE Type = ?)", model.AccessControlPolicyTypeChannel)
} else if opts.AccessControlPolicyEnforced {
query = query.InnerJoin("AccessControlPolicies acp ON c.Id = acp.ID")
}
return query
}
@@ -1296,7 +1311,7 @@ func (s SqlChannelStore) GetMoreChannels(teamId string, userId string, offset in
})
query := s.getQueryBuilder().
Select(channelSliceColumns("Channels")...).
Select(channelSliceColumns(true, "Channels")...).
From("Channels").
Join("PublicChannels c ON (c.Id = Channels.Id)").
Where(sq.Eq{
@@ -1321,7 +1336,7 @@ func (s SqlChannelStore) GetPrivateChannelsForTeam(teamId string, offset int, li
channels := model.ChannelList{}
builder := s.getQueryBuilder().
Select(channelSliceColumns()...).
Select(channelSliceColumns(true)...).
From("Channels").
Where(sq.Eq{"Type": model.ChannelTypePrivate, "TeamId": teamId, "DeleteAt": 0}).
OrderBy("DisplayName").
@@ -1342,7 +1357,7 @@ func (s SqlChannelStore) GetPrivateChannelsForTeam(teamId string, offset int, li
func (s SqlChannelStore) GetPublicChannelsForTeam(teamId string, offset int, limit int) (model.ChannelList, error) {
query := s.getQueryBuilder().
Select(channelSliceColumns("Channels")...).
Select(channelSliceColumns(true, "Channels")...).
From("Channels").
Join("PublicChannels pc ON (pc.Id = Channels.Id)").
Where(sq.Eq{
@@ -1386,7 +1401,7 @@ func (s SqlChannelStore) GetPublicChannelsByIdsForTeam(teamId string, channelIds
var data model.ChannelList
builder := s.getQueryBuilder().
Select(channelSliceColumns("Channels")...).
Select(channelSliceColumns(true, "Channels")...).
From("Channels").
Join("PublicChannels pc ON (pc.Id = Channels.Id)").
Where(sq.And{
@@ -1481,7 +1496,7 @@ func (s SqlChannelStore) getByNames(teamId string, names []string, allowFromCach
}
builder := s.getQueryBuilder().
Select(channelSliceColumns()...).
Select(channelSliceColumns(true)...).
From("Channels").
Where(cond)
@@ -1516,7 +1531,7 @@ func (s SqlChannelStore) GetByName(teamId string, name string, allowFromCache bo
func (s SqlChannelStore) getByName(teamId string, name string, includeDeleted bool, allowFromCache bool) (*model.Channel, error) {
query := s.getQueryBuilder().
Select(channelSliceColumns()...).
Select(channelSliceColumns(true)...).
From("Channels").
Where(sq.Eq{"Name": name}).
Where(sq.Or{
@@ -1567,7 +1582,7 @@ func (s SqlChannelStore) GetDeleted(teamId string, offset int, limit int, userId
channels := model.ChannelList{}
builder := s.getQueryBuilder().
Select(channelSliceColumns()...).
Select(channelSliceColumns(true)...).
From("Channels").
Where(sq.Or{
sq.Eq{"TeamId": teamId},
@@ -2883,7 +2898,7 @@ func (s SqlChannelStore) GetAll(teamId string) ([]*model.Channel, error) {
func (s SqlChannelStore) GetChannelsByIds(channelIds []string, includeDeleted bool) ([]*model.Channel, error) {
query := s.getQueryBuilder().
Select(channelSliceColumns()...).
Select(channelSliceColumns(true)...).
From("Channels").
Where(sq.Eq{"Id": channelIds}).
OrderBy("Name")
@@ -2907,7 +2922,7 @@ func (s SqlChannelStore) GetChannelsByIds(channelIds []string, includeDeleted bo
func (s SqlChannelStore) GetChannelsWithTeamDataByIds(channelIDs []string, includeDeleted bool) ([]*model.ChannelWithTeamData, error) {
query := s.getQueryBuilder().
Select(channelSliceColumns("c")...).
Select(channelSliceColumns(true, "c")...).
Columns(
"COALESCE(t.DisplayName, '') As TeamDisplayName",
"COALESCE(t.Name, '') AS TeamName",
@@ -2937,7 +2952,7 @@ func (s SqlChannelStore) GetChannelsWithTeamDataByIds(channelIDs []string, inclu
func (s SqlChannelStore) GetForPost(postId string) (*model.Channel, error) {
query := s.getQueryBuilder().
Select(channelSliceColumns("Channels")...).
Select(channelSliceColumns(true, "Channels")...).
From("Channels").
Join("Posts ON Channels.Id = Posts.ChannelId").
Where(sq.Eq{
@@ -3110,7 +3125,7 @@ func (s SqlChannelStore) GetTeamMembersForChannel(channelID string) ([]string, e
func (s SqlChannelStore) Autocomplete(rctx request.CTX, userID, term string, includeDeleted, isGuest bool) (model.ChannelListWithTeamData, error) {
query := s.getQueryBuilder().
Select(channelSliceColumns("c")...).
Select(channelSliceColumns(true, "c")...).
Columns(
"t.DisplayName AS TeamDisplayName",
"t.Name AS TeamName",
@@ -3167,7 +3182,7 @@ func (s SqlChannelStore) Autocomplete(rctx request.CTX, userID, term string, inc
}
func (s SqlChannelStore) AutocompleteInTeam(rctx request.CTX, teamID, userID, term string, includeDeleted, isGuest bool) (model.ChannelList, error) {
query := s.getQueryBuilder().Select(channelSliceColumns()...).
query := s.getQueryBuilder().Select(channelSliceColumns(true, "c")...).
From("Channels c").
Where(sq.Eq{"c.TeamId": teamID}).
OrderBy("c.DisplayName").
@@ -3203,7 +3218,7 @@ func (s SqlChannelStore) AutocompleteInTeam(rctx request.CTX, teamID, userID, te
func (s SqlChannelStore) AutocompleteInTeamForSearch(teamID string, userID string, term string, includeDeleted bool) (model.ChannelList, error) {
// shared query
query := s.getSubQueryBuilder().Select(channelSliceColumns("C")...).
query := s.getSubQueryBuilder().Select(channelSliceColumns(true, "C")...).
From("Channels AS C").
Join("ChannelMembers AS CM ON CM.ChannelId = C.Id").
Limit(50).
@@ -3294,7 +3309,7 @@ func (s SqlChannelStore) AutocompleteInTeamForSearch(teamID string, userID strin
func (s SqlChannelStore) autocompleteInTeamForSearchDirectMessages(userID string, term string) ([]*model.Channel, error) {
// create the main query
query := s.getQueryBuilder().
Select(channelSliceColumns("C")...).
Select(channelSliceColumns(true, "C")...).
Columns("OtherUsers.Username AS DisplayName").
From("Channels AS C").
Join("ChannelMembers AS CM ON CM.ChannelId = C.Id").
@@ -3339,7 +3354,7 @@ func (s SqlChannelStore) autocompleteInTeamForSearchDirectMessages(userID string
}
func (s SqlChannelStore) SearchInTeam(teamId string, term string, includeDeleted bool) (model.ChannelList, error) {
query := s.getQueryBuilder().Select(channelSliceColumns("Channels")...).
query := s.getQueryBuilder().Select(channelSliceColumns(true, "Channels")...).
From("Channels").
Join("PublicChannels c ON (c.Id = Channels.Id)").
Where(sq.Eq{"c.TeamId": teamId}).
@@ -3361,7 +3376,7 @@ func (s SqlChannelStore) SearchInTeam(teamId string, term string, includeDeleted
}
func (s SqlChannelStore) SearchArchivedInTeam(teamId string, term string, userId string) (model.ChannelList, error) {
queryBase := s.getQueryBuilder().Select(channelSliceColumns("Channels")...).
queryBase := s.getQueryBuilder().Select(channelSliceColumns(true, "Channels")...).
From("Channels").
Join("Channels c ON (c.Id = Channels.Id)").
Where(sq.And{
@@ -3405,7 +3420,7 @@ func (s SqlChannelStore) SearchArchivedInTeam(teamId string, term string, userId
}
func (s SqlChannelStore) SearchForUserInTeam(userId string, teamId string, term string, includeDeleted bool) (model.ChannelList, error) {
query := s.getQueryBuilder().Select(channelSliceColumns("Channels")...).
query := s.getQueryBuilder().Select(channelSliceColumns(true, "Channels")...).
From("Channels").
Join("PublicChannels c ON (c.Id = Channels.Id)").
Join("ChannelMembers cm ON (c.Id = cm.ChannelId)").
@@ -3441,7 +3456,7 @@ func (s SqlChannelStore) channelSearchQuery(opts *store.ChannelSearchOpts) sq.Se
selectQuery = s.getQueryBuilder().Select("count(*)")
} else {
selectQuery = s.getQueryBuilder().
Select(channelSliceColumns("c")...)
Select(channelSliceColumns(true, "c")...)
if opts.IncludeTeamInfo {
selectQuery = selectQuery.Columns(
"t.DisplayName AS TeamDisplayName",
@@ -3557,6 +3572,18 @@ func (s SqlChannelStore) channelSearchQuery(opts *store.ChannelSearchOpts) sq.Se
})
}
if opts.ExcludeAccessControlPolicyEnforced {
query = query.Where("c.Id NOT IN (SELECT ID From AccessControlPolicies WHERE Type = ?)", model.AccessControlPolicyTypeChannel)
} else if opts.ParentAccessControlPolicyId != "" {
if s.DriverName() == model.DatabaseDriverPostgres {
query = query.Where(sq.Expr("c.Id IN (SELECT ID From AccessControlPolicies WHERE Type = ? AND Data->'imports' @> ?)", model.AccessControlPolicyTypeChannel, fmt.Sprintf("%q", opts.ParentAccessControlPolicyId)))
} else {
query = query.Where(sq.Expr("c.Id IN (SELECT ID From AccessControlPolicies WHERE Type = ? AND JSON_CONTAINS(JSON_EXTRACT(Data, '$.imports'), ?))", model.AccessControlPolicyTypeChannel, fmt.Sprintf("%q", opts.ParentAccessControlPolicyId)))
}
} else if opts.AccessControlPolicyEnforced {
query = query.InnerJoin("AccessControlPolicies acp ON acp.ID = c.Id")
}
return query
}
@@ -3601,7 +3628,7 @@ func (s SqlChannelStore) SearchMore(userId string, teamId string, term string) (
"c.DeleteAt": 0,
})
query := s.getQueryBuilder().Select(channelSliceColumns("Channels")...).
query := s.getQueryBuilder().Select(channelSliceColumns(true, "Channels")...).
From("Channels").
Join("PublicChannels c ON (c.Id=Channels.Id)").
Where(sq.And{
@@ -3808,7 +3835,7 @@ func (s SqlChannelStore) searchGroupChannelsQuery(userId, term string, isPostgre
Having(having).
Limit(model.ChannelSearchDefaultLimit)
return s.getQueryBuilder().Select(channelSliceColumns()...).
return s.getQueryBuilder().Select(channelSliceColumns(true)...).
From("Channels").
Where(sq.Expr("Id IN (?)", subq))
}
@@ -3820,7 +3847,7 @@ func (s SqlChannelStore) searchGroupChannelsQuery(userId, term string, isPostgre
having = append(having, sq.Expr(baseLikeTerm, "%"+term+"%"))
}
cc := s.getSubQueryBuilder().Select(channelSliceColumns("c")...).
cc := s.getSubQueryBuilder().Select(channelSliceColumns(true, "c")...).
From("Channels c").
Join("ChannelMembers cm ON c.Id=cm.ChannelId").
Join("Users u on u.Id = cm.UserId").
@@ -4154,7 +4181,7 @@ func (s SqlChannelStore) ClearAllCustomRoleAssignments() (err error) {
func (s SqlChannelStore) GetAllChannelsForExportAfter(limit int, afterId string) ([]*model.ChannelForExport, error) {
query := s.getQueryBuilder().
Select(channelSliceColumns("Channels")...).
Select(channelSliceColumns(true, "Channels")...).
Columns(
"Teams.Name as TeamName",
"Schemes.Name as SchemeName",
@@ -4222,7 +4249,7 @@ func (s SqlChannelStore) GetChannelMembersForExport(userId string, teamId string
func (s SqlChannelStore) GetAllDirectChannelsForExportAfter(limit int, afterId string, includeArchivedChannels bool) ([]*model.DirectChannelForExport, error) {
directChannelsForExport := []*model.DirectChannelForExport{}
query := s.getQueryBuilder().
Select(channelSliceColumns("Channels")...).
Select(channelSliceColumns(true, "Channels")...).
From("Channels").
Where(sq.And{
sq.Gt{"Channels.Id": afterId},

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

@@ -119,6 +119,7 @@ type SqlStoreStores struct {
propertyField store.PropertyFieldStore
propertyValue store.PropertyValueStore
accessControlPolicy store.AccessControlPolicyStore
Attributes store.AttributesStore
}
type SqlStore struct {
@@ -265,6 +266,7 @@ func New(settings model.SqlSettings, logger mlog.LoggerIFace, metrics einterface
store.stores.propertyField = newPropertyFieldStore(store)
store.stores.propertyValue = newPropertyValueStore(store)
store.stores.accessControlPolicy = newSqlAccessControlPolicyStore(store, metrics)
store.stores.Attributes = newSqlAttributesStore(store, metrics)
store.stores.preference.(*SqlPreferenceStore).deleteUnusedFeatures()
@@ -1085,6 +1087,10 @@ func (ss *SqlStore) AccessControlPolicy() store.AccessControlPolicyStore {
return ss.stores.accessControlPolicy
}
func (ss *SqlStore) Attributes() store.AttributesStore {
return ss.stores.Attributes
}
func (ss *SqlStore) DropAllTables() {
if ss.DriverName() == model.DatabaseDriverPostgres {
ss.masterX.Exec(`DO

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

@@ -96,6 +96,7 @@ type Store interface {
PropertyField() PropertyFieldStore
PropertyValue() PropertyValueStore
AccessControlPolicy() AccessControlPolicyStore
Attributes() AttributesStore
}
type RetentionPolicyStore interface {
@@ -1116,7 +1117,14 @@ type AccessControlPolicyStore interface {
Delete(c request.CTX, id string) error
SetActiveStatus(c request.CTX, id string, active bool) (*model.AccessControlPolicy, error)
Get(c request.CTX, id string) (*model.AccessControlPolicy, error)
GetAll(rctxc request.CTX, opts GetPolicyOptions) ([]*model.AccessControlPolicy, error)
SearchPolicies(rctx request.CTX, opts model.AccessControlPolicySearch) ([]*model.AccessControlPolicy, int64, error)
}
type AttributesStore interface {
RefreshAttributes() error
GetSubject(rctx request.CTX, ID, groupID string) (*model.Subject, error)
SearchUsers(rctx request.CTX, opts model.SubjectSearchOptions) ([]*model.User, int64, error)
GetChannelMembersToRemove(rctx request.CTX, channelID string, opts model.SubjectSearchOptions) ([]*model.ChannelMember, error)
}
// ChannelSearchOpts contains options for searching channels.
@@ -1129,27 +1137,30 @@ type AccessControlPolicyStore interface {
// Page page requested, if results are paginated.
// PerPage number of results per page, if paginated.
type ChannelSearchOpts struct {
Term string
NotAssociatedToGroup string
IncludeDeleted bool
Deleted bool
ExcludeChannelNames []string
TeamIds []string
GroupConstrained bool
ExcludeGroupConstrained bool
PolicyID string
ExcludePolicyConstrained bool
IncludePolicyID bool
IncludeTeamInfo bool
IncludeSearchByID bool
ExcludeRemote bool
CountOnly bool
Public bool
Private bool
Page *int
PerPage *int
LastDeleteAt int
LastUpdateAt int
Term string
NotAssociatedToGroup string
IncludeDeleted bool
Deleted bool
ExcludeChannelNames []string
TeamIds []string
GroupConstrained bool
ExcludeGroupConstrained bool
PolicyID string
ExcludePolicyConstrained bool
IncludePolicyID bool
IncludeTeamInfo bool
IncludeSearchByID bool
ExcludeRemote bool
CountOnly bool
Public bool
Private bool
Page *int
PerPage *int
LastDeleteAt int
LastUpdateAt int
AccessControlPolicyEnforced bool
ExcludeAccessControlPolicyEnforced bool
ParentAccessControlPolicyId string
}
func (c *ChannelSearchOpts) IsPaginated() bool {
@@ -1211,11 +1222,3 @@ type ThreadMembershipImportData struct {
// UnreadMentions is the number of unread mentions to set the UnreadMentions field to.
UnreadMentions int64
}
// GetPolicyOptions contains options for filtering policy records.
type GetPolicyOptions struct {
// ParentID will filter policy records where they inherit parent with PolicyID.
ParentID string
// Type will filter policy records where they are associated with the Type.
Type string
}

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

@@ -290,24 +290,51 @@ func testAccessControlPolicyStoreGetAll(t *testing.T, rctx request.CTX, ss store
require.NoError(t, err)
})
id3 := "zzz" + model.NewId()[3:] // ensure the order of the ID
parentPolicy2 := &model.AccessControlPolicy{
ID: id3,
Name: "Name",
Type: model.AccessControlPolicyTypeParent,
Active: true,
Revision: 1,
Version: model.AccessControlPolicyVersionV0_1,
Imports: []string{},
Rules: []model.AccessControlPolicyRule{
{
Actions: []string{"action"},
Expression: "user.properties.program == \"engineering\"",
},
},
}
t.Cleanup(func() {
err = ss.AccessControlPolicy().Delete(rctx, id)
require.NoError(t, err)
})
_, err = ss.AccessControlPolicy().Save(rctx, parentPolicy2)
require.NoError(t, err)
require.NotNil(t, parentPolicy)
resourcePolicy, err = ss.AccessControlPolicy().Save(rctx, resourcePolicy)
require.NoError(t, err)
require.NotNil(t, resourcePolicy)
t.Run("GetAll", func(t *testing.T) {
policies, err := ss.AccessControlPolicy().GetAll(rctx, store.GetPolicyOptions{})
policies, _, err := ss.AccessControlPolicy().SearchPolicies(rctx, model.AccessControlPolicySearch{})
require.NoError(t, err)
require.NotNil(t, policies)
require.Len(t, policies, 2)
require.Len(t, policies, 3)
})
t.Run("GetAll by type", func(t *testing.T) {
policies, err := ss.AccessControlPolicy().GetAll(rctx, store.GetPolicyOptions{Type: model.AccessControlPolicyTypeParent})
policies, _, err := ss.AccessControlPolicy().SearchPolicies(rctx, model.AccessControlPolicySearch{Type: model.AccessControlPolicyTypeParent, IncludeChildren: true})
require.NoError(t, err)
require.NotNil(t, policies)
require.Len(t, policies, 1)
require.Len(t, policies, 2)
require.Equal(t, parentPolicy.ID, policies[0].ID)
require.Equal(t, map[string]any{"child_ids": []string{resourcePolicy.ID}}, policies[0].Props)
require.Equal(t, map[string]any{"child_ids": []string{}}, policies[1].Props)
policies, err = ss.AccessControlPolicy().GetAll(rctx, store.GetPolicyOptions{Type: model.AccessControlPolicyTypeChannel})
policies, _, err = ss.AccessControlPolicy().SearchPolicies(rctx, model.AccessControlPolicySearch{Type: model.AccessControlPolicyTypeChannel})
require.NoError(t, err)
require.NotNil(t, policies)
require.Len(t, policies, 1)
@@ -315,13 +342,13 @@ func testAccessControlPolicyStoreGetAll(t *testing.T, rctx request.CTX, ss store
})
t.Run("GetAll by parent", func(t *testing.T) {
policies, err := ss.AccessControlPolicy().GetAll(rctx, store.GetPolicyOptions{ParentID: parentPolicy.ID})
policies, _, err := ss.AccessControlPolicy().SearchPolicies(rctx, model.AccessControlPolicySearch{ParentID: parentPolicy.ID})
require.NoError(t, err)
require.NotNil(t, policies)
require.Len(t, policies, 1)
require.Equal(t, resourcePolicy.ID, policies[0].ID)
policies, err = ss.AccessControlPolicy().GetAll(rctx, store.GetPolicyOptions{ParentID: model.NewId()})
policies, _, err = ss.AccessControlPolicy().SearchPolicies(rctx, model.AccessControlPolicySearch{ParentID: model.NewId()})
require.NoError(t, err)
require.NotNil(t, policies)
require.Len(t, policies, 0)

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

@@ -0,0 +1,282 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"encoding/json"
"strings"
"testing"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/store"
"github.com/stretchr/testify/require"
)
const (
testPropertyGroupName = "test_property_group"
testPropertyA = "test_property_a"
testPropertyB = "test_property_b"
testPropertyValueA1 = "value_a1"
testPropertyValueA2 = "value_a2"
testPropertyValueB1 = "value_b1"
)
var (
testTeamID = model.NewId()
)
func TestAttributesStore(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore) {
t.Run("RefreshAndGet", func(t *testing.T) { testAttributesStoreRefresh(t, rctx, ss) })
t.Run("SearchUsers", func(t *testing.T) { testAttributesStoreSearchUsers(t, rctx, ss, s) })
}
func createTestUsers(t *testing.T, rctx request.CTX, ss store.Store) ([]*model.User, string, func()) {
maxUsersPerTeam := 50
u1 := model.User{
Email: MakeEmail(),
Username: model.NewUsername(),
}
_, err := ss.User().Save(rctx, &u1)
require.NoError(t, err, "couldn't save user")
_, nErr := ss.Team().SaveMember(rctx, &model.TeamMember{TeamId: testTeamID, UserId: u1.Id}, maxUsersPerTeam)
require.NoError(t, nErr)
u2 := model.User{
Email: MakeEmail(),
Username: model.NewUsername(),
}
_, err = ss.User().Save(rctx, &u2)
require.NoError(t, err, "couldn't save user")
_, nErr = ss.Team().SaveMember(rctx, &model.TeamMember{TeamId: testTeamID, UserId: u2.Id}, maxUsersPerTeam)
require.NoError(t, nErr)
// user3 does not have any attributes
u3 := model.User{
Email: MakeEmail(),
Username: model.NewUsername(),
}
_, err = ss.User().Save(rctx, &u3)
require.NoError(t, err, "couldn't save user")
// user3 does not have any attributes
u4 := model.User{
Email: MakeEmail(),
Username: model.NewUsername(),
}
_, err = ss.User().Save(rctx, &u4)
require.NoError(t, err, "couldn't save user")
group, err := ss.PropertyGroup().Register(testPropertyGroupName)
require.NoError(t, err)
require.NotZero(t, group.ID)
require.Equal(t, testPropertyGroupName, group.Name)
groupID := group.ID
fieldA, err := ss.PropertyField().Create(&model.PropertyField{
GroupID: groupID,
Name: testPropertyA,
Type: model.PropertyFieldTypeText,
})
require.NoError(t, err)
fieldB, err := ss.PropertyField().Create(&model.PropertyField{
GroupID: groupID,
Name: testPropertyB,
Type: model.PropertyFieldTypeText,
})
require.NoError(t, err)
vala1, err := json.Marshal(testPropertyValueA1)
require.NoError(t, err)
vala2, err := json.Marshal(testPropertyValueA2)
require.NoError(t, err)
valab1, err := json.Marshal(testPropertyValueB1)
require.NoError(t, err)
pva1, err := ss.PropertyValue().Create(&model.PropertyValue{
TargetID: u1.Id,
TargetType: "user",
GroupID: groupID,
FieldID: fieldA.ID,
Value: vala1,
})
require.NoError(t, err)
pvb1, err := ss.PropertyValue().Create(&model.PropertyValue{
TargetID: u1.Id,
TargetType: "user",
GroupID: groupID,
FieldID: fieldB.ID,
Value: valab1,
})
require.NoError(t, err)
pva2, err := ss.PropertyValue().Create(&model.PropertyValue{
TargetID: u2.Id,
TargetType: "user",
GroupID: groupID,
FieldID: fieldA.ID,
Value: vala2,
})
require.NoError(t, err)
pva3, err := ss.PropertyValue().Create(&model.PropertyValue{
TargetID: u3.Id,
TargetType: "user",
GroupID: groupID,
FieldID: fieldA.ID,
Value: vala1,
})
require.NoError(t, err)
return []*model.User{&u1, &u2, &u3}, groupID, func() {
for _, pv := range []*model.PropertyValue{pva1, pvb1, pva2, pva3} {
dErr := ss.PropertyValue().Delete(groupID, pv.ID)
require.NoError(t, dErr, "couldn't delete property value")
}
for _, field := range []*model.PropertyField{fieldA, fieldB} {
dErr := ss.PropertyField().Delete(groupID, field.ID)
require.NoError(t, dErr, "couldn't delete property field")
}
for _, u := range []*model.User{&u1, &u2, &u3, &u4} {
dErr := ss.User().PermanentDelete(rctx, u.Id)
require.NoError(t, dErr, "couldn't delete user")
}
}
}
func testAttributesStoreRefresh(t *testing.T, rctx request.CTX, ss store.Store) {
users, groupID, cleanup := createTestUsers(t, rctx, ss)
t.Cleanup(cleanup)
t.Run("Refresh attributes", func(t *testing.T) {
err := ss.Attributes().RefreshAttributes()
require.NoError(t, err, "couldn't refresh attributes")
// Check if the attributes are set correctly
for _, user := range users {
subject, err := ss.Attributes().GetSubject(rctx, user.Id, groupID)
require.NoError(t, err, "couldn't get subject")
require.Equal(t, user.Id, subject.ID)
require.Equal(t, "user", subject.Type)
}
})
t.Run("Get non-existing subject", func(t *testing.T) {
subject, err := ss.Attributes().GetSubject(rctx, "non-existing-id", groupID)
require.Error(t, err, "expected error when getting non-existing subject")
require.IsType(t, &store.ErrNotFound{}, err, "expected not found error")
require.Nil(t, subject, "expected nil subject for non-existing ID")
})
}
func testAttributesStoreSearchUsers(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore) {
users, _, cleanup := createTestUsers(t, rctx, ss)
t.Cleanup(cleanup)
require.Len(t, users, 3, "expected 3 users")
err := ss.Attributes().RefreshAttributes()
require.NoError(t, err, "couldn't refresh attributes")
t.Run("Search users without query", func(t *testing.T) {
subjects, count, err := ss.Attributes().SearchUsers(rctx, model.SubjectSearchOptions{})
require.NoError(t, err, "couldn't search users")
require.Len(t, subjects, 4, "expected 4 users")
require.Equal(t, int64(4), count, "expected count 4 users")
})
t.Run("Search users without query, limit by team", func(t *testing.T) {
subjects, count, err := ss.Attributes().SearchUsers(rctx, model.SubjectSearchOptions{
TeamID: testTeamID,
})
require.NoError(t, err, "couldn't search users")
require.Len(t, subjects, 2, "expected 2 users")
require.Equal(t, int64(2), count, "expected count 2 users")
})
t.Run("Search users with a random value query", func(t *testing.T) {
subjects, count, err := ss.Attributes().SearchUsers(rctx, model.SubjectSearchOptions{
Query: "Attributes ->> '$." + testPropertyA + "' = ?",
Args: []any{"random_value"},
})
require.NoError(t, err, "couldn't search users")
require.Empty(t, subjects, "expected no users with the query")
require.Equal(t, int64(0), count, "expected count 0 users")
})
t.Run("Search users with a valid value query", func(t *testing.T) {
var query string
if s.DriverName() == model.DatabaseDriverMysql {
query = "Attributes ->> '$." + testPropertyB + "' = ?"
} else {
query = "Attributes ->> '" + testPropertyB + "' = $1::text"
}
subjects, count, err := ss.Attributes().SearchUsers(rctx, model.SubjectSearchOptions{
Query: query,
Args: []any{testPropertyValueB1},
})
require.NoError(t, err, "couldn't search users")
require.Len(t, subjects, 1, "expected 1 user with the query")
require.Equal(t, subjects[0].Id, users[0].Id, "expected user ID to match")
require.Equal(t, int64(1), count, "expected count 1 user")
})
t.Run("Search users with a valid value query and limit", func(t *testing.T) {
var query string
if s.DriverName() == model.DatabaseDriverMysql {
query = "Attributes ->> '$." + testPropertyA + "' = ?"
} else {
query = "Attributes ->> '" + testPropertyA + "' = $1::text"
}
subjects, count, err := ss.Attributes().SearchUsers(rctx, model.SubjectSearchOptions{
Query: query,
Args: []any{testPropertyValueA1},
Limit: 1,
})
require.NoError(t, err, "couldn't search users")
require.Len(t, subjects, 1, "expected 1 user with the query")
if users[0].Id < users[2].Id {
require.Equal(t, subjects[0].Id, users[0].Id, "expected user ID to match")
} else {
require.Equal(t, subjects[0].Id, users[2].Id, "expected user ID to match")
}
require.Equal(t, int64(2), count, "expected count 1 user")
})
t.Run("Search users with pagination", func(t *testing.T) {
var query string
if s.DriverName() == model.DatabaseDriverMysql {
query = "Attributes ->> '$." + testPropertyA + "' = ?"
} else {
query = "Attributes ->> '" + testPropertyA + "' = $1::text"
}
cursor := strings.Repeat("0", 26)
for i := 0; i < 5; i++ {
subjects, count, err := ss.Attributes().SearchUsers(rctx, model.SubjectSearchOptions{
Query: query,
Args: []any{testPropertyValueA1},
Limit: 1,
Cursor: model.SubjectCursor{
TargetID: cursor,
},
})
if len(subjects) == 0 {
break
}
cursor = subjects[0].Id
require.NoError(t, err, "couldn't search users")
require.Len(t, subjects, 1, "expected 1 user with the query")
require.Equal(t, int64(2), count, "expected count 2 user with the query")
}
})
}

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

@@ -8,8 +8,6 @@ import (
model "github.com/mattermost/mattermost/server/public/model"
request "github.com/mattermost/mattermost/server/public/shared/request"
mock "github.com/stretchr/testify/mock"
store "github.com/mattermost/mattermost/server/v8/channels/store"
)
// AccessControlPolicyStore is an autogenerated mock type for the AccessControlPolicyStore type
@@ -65,36 +63,6 @@ func (_m *AccessControlPolicyStore) Get(c request.CTX, id string) (*model.Access
return r0, r1
}
// GetAll provides a mock function with given fields: rctxc, opts
func (_m *AccessControlPolicyStore) GetAll(rctxc request.CTX, opts store.GetPolicyOptions) ([]*model.AccessControlPolicy, error) {
ret := _m.Called(rctxc, opts)
if len(ret) == 0 {
panic("no return value specified for GetAll")
}
var r0 []*model.AccessControlPolicy
var r1 error
if rf, ok := ret.Get(0).(func(request.CTX, store.GetPolicyOptions) ([]*model.AccessControlPolicy, error)); ok {
return rf(rctxc, opts)
}
if rf, ok := ret.Get(0).(func(request.CTX, store.GetPolicyOptions) []*model.AccessControlPolicy); ok {
r0 = rf(rctxc, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.AccessControlPolicy)
}
}
if rf, ok := ret.Get(1).(func(request.CTX, store.GetPolicyOptions) error); ok {
r1 = rf(rctxc, opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Save provides a mock function with given fields: c, policy
func (_m *AccessControlPolicyStore) Save(c request.CTX, policy *model.AccessControlPolicy) (*model.AccessControlPolicy, error) {
ret := _m.Called(c, policy)
@@ -125,6 +93,43 @@ func (_m *AccessControlPolicyStore) Save(c request.CTX, policy *model.AccessCont
return r0, r1
}
// SearchPolicies provides a mock function with given fields: rctx, opts
func (_m *AccessControlPolicyStore) SearchPolicies(rctx request.CTX, opts model.AccessControlPolicySearch) ([]*model.AccessControlPolicy, int64, error) {
ret := _m.Called(rctx, opts)
if len(ret) == 0 {
panic("no return value specified for SearchPolicies")
}
var r0 []*model.AccessControlPolicy
var r1 int64
var r2 error
if rf, ok := ret.Get(0).(func(request.CTX, model.AccessControlPolicySearch) ([]*model.AccessControlPolicy, int64, error)); ok {
return rf(rctx, opts)
}
if rf, ok := ret.Get(0).(func(request.CTX, model.AccessControlPolicySearch) []*model.AccessControlPolicy); ok {
r0 = rf(rctx, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.AccessControlPolicy)
}
}
if rf, ok := ret.Get(1).(func(request.CTX, model.AccessControlPolicySearch) int64); ok {
r1 = rf(rctx, opts)
} else {
r1 = ret.Get(1).(int64)
}
if rf, ok := ret.Get(2).(func(request.CTX, model.AccessControlPolicySearch) error); ok {
r2 = rf(rctx, opts)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// SetActiveStatus provides a mock function with given fields: c, id, active
func (_m *AccessControlPolicyStore) SetActiveStatus(c request.CTX, id string, active bool) (*model.AccessControlPolicy, error) {
ret := _m.Called(c, id, active)

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

@@ -0,0 +1,145 @@
// Code generated by mockery v2.42.2. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost/server/public/model"
request "github.com/mattermost/mattermost/server/public/shared/request"
mock "github.com/stretchr/testify/mock"
)
// AttributesStore is an autogenerated mock type for the AttributesStore type
type AttributesStore struct {
mock.Mock
}
// GetChannelMembersToRemove provides a mock function with given fields: rctx, channelID, opts
func (_m *AttributesStore) GetChannelMembersToRemove(rctx request.CTX, channelID string, opts model.SubjectSearchOptions) ([]*model.ChannelMember, error) {
ret := _m.Called(rctx, channelID, opts)
if len(ret) == 0 {
panic("no return value specified for GetChannelMembersToRemove")
}
var r0 []*model.ChannelMember
var r1 error
if rf, ok := ret.Get(0).(func(request.CTX, string, model.SubjectSearchOptions) ([]*model.ChannelMember, error)); ok {
return rf(rctx, channelID, opts)
}
if rf, ok := ret.Get(0).(func(request.CTX, string, model.SubjectSearchOptions) []*model.ChannelMember); ok {
r0 = rf(rctx, channelID, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.ChannelMember)
}
}
if rf, ok := ret.Get(1).(func(request.CTX, string, model.SubjectSearchOptions) error); ok {
r1 = rf(rctx, channelID, opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetSubject provides a mock function with given fields: rctx, ID, groupID
func (_m *AttributesStore) GetSubject(rctx request.CTX, ID string, groupID string) (*model.Subject, error) {
ret := _m.Called(rctx, ID, groupID)
if len(ret) == 0 {
panic("no return value specified for GetSubject")
}
var r0 *model.Subject
var r1 error
if rf, ok := ret.Get(0).(func(request.CTX, string, string) (*model.Subject, error)); ok {
return rf(rctx, ID, groupID)
}
if rf, ok := ret.Get(0).(func(request.CTX, string, string) *model.Subject); ok {
r0 = rf(rctx, ID, groupID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Subject)
}
}
if rf, ok := ret.Get(1).(func(request.CTX, string, string) error); ok {
r1 = rf(rctx, ID, groupID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// RefreshAttributes provides a mock function with given fields:
func (_m *AttributesStore) RefreshAttributes() error {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for RefreshAttributes")
}
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// SearchUsers provides a mock function with given fields: rctx, opts
func (_m *AttributesStore) SearchUsers(rctx request.CTX, opts model.SubjectSearchOptions) ([]*model.User, int64, error) {
ret := _m.Called(rctx, opts)
if len(ret) == 0 {
panic("no return value specified for SearchUsers")
}
var r0 []*model.User
var r1 int64
var r2 error
if rf, ok := ret.Get(0).(func(request.CTX, model.SubjectSearchOptions) ([]*model.User, int64, error)); ok {
return rf(rctx, opts)
}
if rf, ok := ret.Get(0).(func(request.CTX, model.SubjectSearchOptions) []*model.User); ok {
r0 = rf(rctx, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
if rf, ok := ret.Get(1).(func(request.CTX, model.SubjectSearchOptions) int64); ok {
r1 = rf(rctx, opts)
} else {
r1 = ret.Get(1).(int64)
}
if rf, ok := ret.Get(2).(func(request.CTX, model.SubjectSearchOptions) error); ok {
r2 = rf(rctx, opts)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// NewAttributesStore creates a new instance of AttributesStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewAttributesStore(t interface {
mock.TestingT
Cleanup(func())
}) *AttributesStore {
mock := &AttributesStore{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

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

@@ -44,6 +44,26 @@ func (_m *Store) AccessControlPolicy() store.AccessControlPolicyStore {
return r0
}
// Attributes provides a mock function with given fields:
func (_m *Store) Attributes() store.AttributesStore {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Attributes")
}
var r0 store.AttributesStore
if rf, ok := ret.Get(0).(func() store.AttributesStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.AttributesStore)
}
}
return r0
}
// Audit provides a mock function with given fields:
func (_m *Store) Audit() store.AuditStore {
ret := _m.Called()

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

@@ -70,6 +70,7 @@ type Store struct {
PropertyFieldStore mocks.PropertyFieldStore
PropertyValueStore mocks.PropertyValueStore
AccessControlPolicyStore mocks.AccessControlPolicyStore
AttributesStore mocks.AttributesStore
}
func (s *Store) SetContext(context context.Context) { s.context = context }
@@ -158,6 +159,9 @@ func (s *Store) ReplicaLagTime() error { return nil }
func (s *Store) AccessControlPolicy() store.AccessControlPolicyStore {
return &s.AccessControlPolicyStore
}
func (s *Store) Attributes() store.AttributesStore {
return &s.AttributesStore
}
func (s *Store) AssertExpectations(t mock.TestingT) bool {
return mock.AssertExpectationsForObjects(t,
@@ -202,5 +206,6 @@ func (s *Store) AssertExpectations(t mock.TestingT) bool {
&s.ChannelBookmarkStore,
&s.ScheduledPostStore,
&s.AccessControlPolicyStore,
&s.AttributesStore,
)
}

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

@@ -20,6 +20,7 @@ type TimerLayer struct {
store.Store
Metrics einterfaces.MetricsInterface
AccessControlPolicyStore store.AccessControlPolicyStore
AttributesStore store.AttributesStore
AuditStore store.AuditStore
BotStore store.BotStore
ChannelStore store.ChannelStore
@@ -75,6 +76,10 @@ func (s *TimerLayer) AccessControlPolicy() store.AccessControlPolicyStore {
return s.AccessControlPolicyStore
}
func (s *TimerLayer) Attributes() store.AttributesStore {
return s.AttributesStore
}
func (s *TimerLayer) Audit() store.AuditStore {
return s.AuditStore
}
@@ -276,6 +281,11 @@ type TimerLayerAccessControlPolicyStore struct {
Root *TimerLayer
}
type TimerLayerAttributesStore struct {
store.AttributesStore
Root *TimerLayer
}
type TimerLayerAuditStore struct {
store.AuditStore
Root *TimerLayer
@@ -553,22 +563,6 @@ func (s *TimerLayerAccessControlPolicyStore) Get(c request.CTX, id string) (*mod
return result, err
}
func (s *TimerLayerAccessControlPolicyStore) GetAll(rctxc request.CTX, opts store.GetPolicyOptions) ([]*model.AccessControlPolicy, error) {
start := time.Now()
result, err := s.AccessControlPolicyStore.GetAll(rctxc, opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("AccessControlPolicyStore.GetAll", success, elapsed)
}
return result, err
}
func (s *TimerLayerAccessControlPolicyStore) Save(c request.CTX, policy *model.AccessControlPolicy) (*model.AccessControlPolicy, error) {
start := time.Now()
@@ -585,6 +579,22 @@ func (s *TimerLayerAccessControlPolicyStore) Save(c request.CTX, policy *model.A
return result, err
}
func (s *TimerLayerAccessControlPolicyStore) SearchPolicies(rctx request.CTX, opts model.AccessControlPolicySearch) ([]*model.AccessControlPolicy, int64, error) {
start := time.Now()
result, resultVar1, err := s.AccessControlPolicyStore.SearchPolicies(rctx, opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("AccessControlPolicyStore.SearchPolicies", success, elapsed)
}
return result, resultVar1, err
}
func (s *TimerLayerAccessControlPolicyStore) SetActiveStatus(c request.CTX, id string, active bool) (*model.AccessControlPolicy, error) {
start := time.Now()
@@ -601,6 +611,70 @@ func (s *TimerLayerAccessControlPolicyStore) SetActiveStatus(c request.CTX, id s
return result, err
}
func (s *TimerLayerAttributesStore) GetChannelMembersToRemove(rctx request.CTX, channelID string, opts model.SubjectSearchOptions) ([]*model.ChannelMember, error) {
start := time.Now()
result, err := s.AttributesStore.GetChannelMembersToRemove(rctx, channelID, opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("AttributesStore.GetChannelMembersToRemove", success, elapsed)
}
return result, err
}
func (s *TimerLayerAttributesStore) GetSubject(rctx request.CTX, ID string, groupID string) (*model.Subject, error) {
start := time.Now()
result, err := s.AttributesStore.GetSubject(rctx, ID, groupID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("AttributesStore.GetSubject", success, elapsed)
}
return result, err
}
func (s *TimerLayerAttributesStore) RefreshAttributes() error {
start := time.Now()
err := s.AttributesStore.RefreshAttributes()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("AttributesStore.RefreshAttributes", success, elapsed)
}
return err
}
func (s *TimerLayerAttributesStore) SearchUsers(rctx request.CTX, opts model.SubjectSearchOptions) ([]*model.User, int64, error) {
start := time.Now()
result, resultVar1, err := s.AttributesStore.SearchUsers(rctx, opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("AttributesStore.SearchUsers", success, elapsed)
}
return result, resultVar1, err
}
func (s *TimerLayerAuditStore) Get(userID string, offset int, limit int) (model.Audits, error) {
start := time.Now()
@@ -13021,6 +13095,7 @@ func New(childStore store.Store, metrics einterfaces.MetricsInterface) *TimerLay
}
newStore.AccessControlPolicyStore = &TimerLayerAccessControlPolicyStore{AccessControlPolicyStore: childStore.AccessControlPolicy(), Root: &newStore}
newStore.AttributesStore = &TimerLayerAttributesStore{AttributesStore: childStore.Attributes(), Root: &newStore}
newStore.AuditStore = &TimerLayerAuditStore{AuditStore: childStore.Audit(), Root: &newStore}
newStore.BotStore = &TimerLayerBotStore{BotStore: childStore.Bot(), Root: &newStore}
newStore.ChannelStore = &TimerLayerChannelStore{ChannelStore: childStore.Channel(), Root: &newStore}

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

@@ -25,86 +25,88 @@ const (
)
type Params struct {
UserId string
TeamId string
InviteId string
TokenId string
ThreadId string
Timestamp int64
TimeRange string
ChannelId string
PostId string
PolicyId string
FileId string
Filename string
UploadId string
PluginId string
CommandId string
HookId string
ReportId string
EmojiId string
AppId string
Email string
Username string
TeamName string
ChannelName string
PreferenceName string
EmojiName string
Category string
Service string
JobId string
JobType string
ActionId string
RoleId string
RoleName string
SchemeId string
Scope string
GroupId string
Page int
PerPage int
LogsPerPage int
Permanent bool
RemoteId string
SyncableId string
SyncableType model.GroupSyncableType
BotUserId string
Q string
IsLinked *bool
IsConfigured *bool
NotAssociatedToTeam string
NotAssociatedToChannel string
Paginate *bool
IncludeMemberCount bool
IncludeMemberIDs bool
NotAssociatedToGroup string
ExcludeDefaultChannels bool
LimitAfter int
LimitBefore int
GroupIDs string
IncludeTotalCount bool
IncludeDeleted bool
FilterAllowReference bool
FilterArchived bool
FilterParentTeamPermitted bool
CategoryId string
ExportName string
ExcludePolicyConstrained bool
GroupSource model.GroupSource
FilterHasMember string
IncludeChannelMemberCount string
OutgoingOAuthConnectionID string
ExcludeOffline bool
InChannel string
NotInChannel string
Topic string
CreatorId string
OnlyConfirmed bool
OnlyPlugins bool
IncludeUnconfirmed bool
ExcludeConfirmed bool
ExcludePlugins bool
ExcludeHome bool
ExcludeRemote bool
UserId string
TeamId string
InviteId string
TokenId string
ThreadId string
Timestamp int64
TimeRange string
ChannelId string
PostId string
PolicyId string
FileId string
Filename string
UploadId string
PluginId string
CommandId string
HookId string
ReportId string
EmojiId string
AppId string
Email string
Username string
TeamName string
ChannelName string
PreferenceName string
EmojiName string
Category string
Service string
JobId string
JobType string
ActionId string
RoleId string
RoleName string
SchemeId string
Scope string
GroupId string
Page int
PerPage int
LogsPerPage int
Permanent bool
RemoteId string
SyncableId string
SyncableType model.GroupSyncableType
BotUserId string
Q string
IsLinked *bool
IsConfigured *bool
NotAssociatedToTeam string
NotAssociatedToChannel string
Paginate *bool
IncludeMemberCount bool
IncludeMemberIDs bool
NotAssociatedToGroup string
ExcludeDefaultChannels bool
LimitAfter int
LimitBefore int
GroupIDs string
IncludeTotalCount bool
IncludeDeleted bool
FilterAllowReference bool
FilterArchived bool
FilterParentTeamPermitted bool
CategoryId string
ExportName string
ExcludePolicyConstrained bool
GroupSource model.GroupSource
FilterHasMember string
IncludeChannelMemberCount string
OutgoingOAuthConnectionID string
ExcludeOffline bool
InChannel string
NotInChannel string
Topic string
CreatorId string
OnlyConfirmed bool
OnlyPlugins bool
IncludeUnconfirmed bool
ExcludeConfirmed bool
ExcludePlugins bool
ExcludeHome bool
ExcludeRemote bool
AccessControlPolicyEnforced bool
ExcludeAccessControlPolicyEnforced bool
//Bookmarks
ChannelBookmarkId string
@@ -277,6 +279,8 @@ func ParamsFromRequest(r *http.Request) *Params {
params.IncludeDeleted, _ = strconv.ParseBool(query.Get("include_deleted"))
params.ExportName = props["export_name"]
params.ExcludePolicyConstrained, _ = strconv.ParseBool(query.Get("exclude_policy_constrained"))
params.AccessControlPolicyEnforced, _ = strconv.ParseBool(query.Get("access_control_policy_enforced"))
params.ExcludeAccessControlPolicyEnforced, _ = strconv.ParseBool(query.Get("exclude_access_control_policy_enforced"))
if val := query.Get("group_source"); val != "" {
switch val {

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

@@ -0,0 +1,12 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package einterfaces
// AccessControlServiceInterface is the interface that provides access control
// services. It combines the PolicyAdministrationPointInterface and
// PolicyDecisionPointInterface interfaces to provide a complete access control solution.
type AccessControlServiceInterface interface {
PolicyAdministrationPointInterface
PolicyDecisionPointInterface
}

13
server/einterfaces/jobs/access_control.go Обычный файл
Просмотреть файл

@@ -0,0 +1,13 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package jobs
import (
"github.com/mattermost/mattermost/server/public/model"
)
type AccessControlSyncJobInterface interface {
MakeWorker() model.Worker
MakeScheduler() Scheduler
}

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

@@ -135,7 +135,7 @@ type MetricsInterface interface {
ObserveDesktopCpuUsage(platform, version, process string, usage float64)
ObserveDesktopMemoryUsage(platform, version, process string, usage float64)
ObserveAccessControlEngineInitDuration(value float64)
ObserveAccessControlSearchQueryDuration(value float64)
ObserveAccessControlExpressionCompileDuration(value float64)
ObserveAccessControlEvaluateDuration(value float64)
IncrementAccessControlCacheInvalidation()

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

@@ -0,0 +1,402 @@
// Code generated by mockery v2.42.2. DO NOT EDIT.
// Regenerate this file using `make einterfaces-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost/server/public/model"
request "github.com/mattermost/mattermost/server/public/shared/request"
mock "github.com/stretchr/testify/mock"
)
// AccessControlServiceInterface is an autogenerated mock type for the AccessControlServiceInterface type
type AccessControlServiceInterface struct {
mock.Mock
}
// AccessEvaluation provides a mock function with given fields: rctx, accessRequest
func (_m *AccessControlServiceInterface) AccessEvaluation(rctx request.CTX, accessRequest model.AccessRequest) (model.AccessDecision, *model.AppError) {
ret := _m.Called(rctx, accessRequest)
if len(ret) == 0 {
panic("no return value specified for AccessEvaluation")
}
var r0 model.AccessDecision
var r1 *model.AppError
if rf, ok := ret.Get(0).(func(request.CTX, model.AccessRequest) (model.AccessDecision, *model.AppError)); ok {
return rf(rctx, accessRequest)
}
if rf, ok := ret.Get(0).(func(request.CTX, model.AccessRequest) model.AccessDecision); ok {
r0 = rf(rctx, accessRequest)
} else {
r0 = ret.Get(0).(model.AccessDecision)
}
if rf, ok := ret.Get(1).(func(request.CTX, model.AccessRequest) *model.AppError); ok {
r1 = rf(rctx, accessRequest)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// CheckExpression provides a mock function with given fields: rctx, expression
func (_m *AccessControlServiceInterface) CheckExpression(rctx request.CTX, expression string) ([]model.CELExpressionError, *model.AppError) {
ret := _m.Called(rctx, expression)
if len(ret) == 0 {
panic("no return value specified for CheckExpression")
}
var r0 []model.CELExpressionError
var r1 *model.AppError
if rf, ok := ret.Get(0).(func(request.CTX, string) ([]model.CELExpressionError, *model.AppError)); ok {
return rf(rctx, expression)
}
if rf, ok := ret.Get(0).(func(request.CTX, string) []model.CELExpressionError); ok {
r0 = rf(rctx, expression)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]model.CELExpressionError)
}
}
if rf, ok := ret.Get(1).(func(request.CTX, string) *model.AppError); ok {
r1 = rf(rctx, expression)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// DeletePolicy provides a mock function with given fields: rctx, id
func (_m *AccessControlServiceInterface) DeletePolicy(rctx request.CTX, id string) *model.AppError {
ret := _m.Called(rctx, id)
if len(ret) == 0 {
panic("no return value specified for DeletePolicy")
}
var r0 *model.AppError
if rf, ok := ret.Get(0).(func(request.CTX, string) *model.AppError); ok {
r0 = rf(rctx, id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AppError)
}
}
return r0
}
// ExpressionToVisualAST provides a mock function with given fields: rctx, expression
func (_m *AccessControlServiceInterface) ExpressionToVisualAST(rctx request.CTX, expression string) (*model.VisualExpression, *model.AppError) {
ret := _m.Called(rctx, expression)
if len(ret) == 0 {
panic("no return value specified for ExpressionToVisualAST")
}
var r0 *model.VisualExpression
var r1 *model.AppError
if rf, ok := ret.Get(0).(func(request.CTX, string) (*model.VisualExpression, *model.AppError)); ok {
return rf(rctx, expression)
}
if rf, ok := ret.Get(0).(func(request.CTX, string) *model.VisualExpression); ok {
r0 = rf(rctx, expression)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.VisualExpression)
}
}
if rf, ok := ret.Get(1).(func(request.CTX, string) *model.AppError); ok {
r1 = rf(rctx, expression)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// GetChannelMembersToRemove provides a mock function with given fields: rctx, channelID
func (_m *AccessControlServiceInterface) GetChannelMembersToRemove(rctx request.CTX, channelID string) ([]*model.ChannelMember, *model.AppError) {
ret := _m.Called(rctx, channelID)
if len(ret) == 0 {
panic("no return value specified for GetChannelMembersToRemove")
}
var r0 []*model.ChannelMember
var r1 *model.AppError
if rf, ok := ret.Get(0).(func(request.CTX, string) ([]*model.ChannelMember, *model.AppError)); ok {
return rf(rctx, channelID)
}
if rf, ok := ret.Get(0).(func(request.CTX, string) []*model.ChannelMember); ok {
r0 = rf(rctx, channelID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.ChannelMember)
}
}
if rf, ok := ret.Get(1).(func(request.CTX, string) *model.AppError); ok {
r1 = rf(rctx, channelID)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// GetPolicy provides a mock function with given fields: rctx, id
func (_m *AccessControlServiceInterface) GetPolicy(rctx request.CTX, id string) (*model.AccessControlPolicy, *model.AppError) {
ret := _m.Called(rctx, id)
if len(ret) == 0 {
panic("no return value specified for GetPolicy")
}
var r0 *model.AccessControlPolicy
var r1 *model.AppError
if rf, ok := ret.Get(0).(func(request.CTX, string) (*model.AccessControlPolicy, *model.AppError)); ok {
return rf(rctx, id)
}
if rf, ok := ret.Get(0).(func(request.CTX, string) *model.AccessControlPolicy); ok {
r0 = rf(rctx, id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AccessControlPolicy)
}
}
if rf, ok := ret.Get(1).(func(request.CTX, string) *model.AppError); ok {
r1 = rf(rctx, id)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// GetPolicyRuleAttributes provides a mock function with given fields: rctx, policyID, action
func (_m *AccessControlServiceInterface) GetPolicyRuleAttributes(rctx request.CTX, policyID string, action string) (map[string][]string, *model.AppError) {
ret := _m.Called(rctx, policyID, action)
if len(ret) == 0 {
panic("no return value specified for GetPolicyRuleAttributes")
}
var r0 map[string][]string
var r1 *model.AppError
if rf, ok := ret.Get(0).(func(request.CTX, string, string) (map[string][]string, *model.AppError)); ok {
return rf(rctx, policyID, action)
}
if rf, ok := ret.Get(0).(func(request.CTX, string, string) map[string][]string); ok {
r0 = rf(rctx, policyID, action)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string][]string)
}
}
if rf, ok := ret.Get(1).(func(request.CTX, string, string) *model.AppError); ok {
r1 = rf(rctx, policyID, action)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// Init provides a mock function with given fields: rctx
func (_m *AccessControlServiceInterface) Init(rctx request.CTX) *model.AppError {
ret := _m.Called(rctx)
if len(ret) == 0 {
panic("no return value specified for Init")
}
var r0 *model.AppError
if rf, ok := ret.Get(0).(func(request.CTX) *model.AppError); ok {
r0 = rf(rctx)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AppError)
}
}
return r0
}
// NormalizePolicy provides a mock function with given fields: rctx, policy
func (_m *AccessControlServiceInterface) NormalizePolicy(rctx request.CTX, policy *model.AccessControlPolicy) (*model.AccessControlPolicy, *model.AppError) {
ret := _m.Called(rctx, policy)
if len(ret) == 0 {
panic("no return value specified for NormalizePolicy")
}
var r0 *model.AccessControlPolicy
var r1 *model.AppError
if rf, ok := ret.Get(0).(func(request.CTX, *model.AccessControlPolicy) (*model.AccessControlPolicy, *model.AppError)); ok {
return rf(rctx, policy)
}
if rf, ok := ret.Get(0).(func(request.CTX, *model.AccessControlPolicy) *model.AccessControlPolicy); ok {
r0 = rf(rctx, policy)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AccessControlPolicy)
}
}
if rf, ok := ret.Get(1).(func(request.CTX, *model.AccessControlPolicy) *model.AppError); ok {
r1 = rf(rctx, policy)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// QueryUsersForExpression provides a mock function with given fields: rctx, expression, opts
func (_m *AccessControlServiceInterface) QueryUsersForExpression(rctx request.CTX, expression string, opts model.SubjectSearchOptions) ([]*model.User, int64, *model.AppError) {
ret := _m.Called(rctx, expression, opts)
if len(ret) == 0 {
panic("no return value specified for QueryUsersForExpression")
}
var r0 []*model.User
var r1 int64
var r2 *model.AppError
if rf, ok := ret.Get(0).(func(request.CTX, string, model.SubjectSearchOptions) ([]*model.User, int64, *model.AppError)); ok {
return rf(rctx, expression, opts)
}
if rf, ok := ret.Get(0).(func(request.CTX, string, model.SubjectSearchOptions) []*model.User); ok {
r0 = rf(rctx, expression, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
if rf, ok := ret.Get(1).(func(request.CTX, string, model.SubjectSearchOptions) int64); ok {
r1 = rf(rctx, expression, opts)
} else {
r1 = ret.Get(1).(int64)
}
if rf, ok := ret.Get(2).(func(request.CTX, string, model.SubjectSearchOptions) *model.AppError); ok {
r2 = rf(rctx, expression, opts)
} else {
if ret.Get(2) != nil {
r2 = ret.Get(2).(*model.AppError)
}
}
return r0, r1, r2
}
// QueryUsersForResource provides a mock function with given fields: rctx, resourceID, action, opts
func (_m *AccessControlServiceInterface) QueryUsersForResource(rctx request.CTX, resourceID string, action string, opts model.SubjectSearchOptions) ([]*model.User, int64, *model.AppError) {
ret := _m.Called(rctx, resourceID, action, opts)
if len(ret) == 0 {
panic("no return value specified for QueryUsersForResource")
}
var r0 []*model.User
var r1 int64
var r2 *model.AppError
if rf, ok := ret.Get(0).(func(request.CTX, string, string, model.SubjectSearchOptions) ([]*model.User, int64, *model.AppError)); ok {
return rf(rctx, resourceID, action, opts)
}
if rf, ok := ret.Get(0).(func(request.CTX, string, string, model.SubjectSearchOptions) []*model.User); ok {
r0 = rf(rctx, resourceID, action, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
if rf, ok := ret.Get(1).(func(request.CTX, string, string, model.SubjectSearchOptions) int64); ok {
r1 = rf(rctx, resourceID, action, opts)
} else {
r1 = ret.Get(1).(int64)
}
if rf, ok := ret.Get(2).(func(request.CTX, string, string, model.SubjectSearchOptions) *model.AppError); ok {
r2 = rf(rctx, resourceID, action, opts)
} else {
if ret.Get(2) != nil {
r2 = ret.Get(2).(*model.AppError)
}
}
return r0, r1, r2
}
// SavePolicy provides a mock function with given fields: rctx, policy
func (_m *AccessControlServiceInterface) SavePolicy(rctx request.CTX, policy *model.AccessControlPolicy) (*model.AccessControlPolicy, *model.AppError) {
ret := _m.Called(rctx, policy)
if len(ret) == 0 {
panic("no return value specified for SavePolicy")
}
var r0 *model.AccessControlPolicy
var r1 *model.AppError
if rf, ok := ret.Get(0).(func(request.CTX, *model.AccessControlPolicy) (*model.AccessControlPolicy, *model.AppError)); ok {
return rf(rctx, policy)
}
if rf, ok := ret.Get(0).(func(request.CTX, *model.AccessControlPolicy) *model.AccessControlPolicy); ok {
r0 = rf(rctx, policy)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AccessControlPolicy)
}
}
if rf, ok := ret.Get(1).(func(request.CTX, *model.AccessControlPolicy) *model.AppError); ok {
r1 = rf(rctx, policy)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// NewAccessControlServiceInterface creates a new instance of AccessControlServiceInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewAccessControlServiceInterface(t interface {
mock.TestingT
Cleanup(func())
}) *AccessControlServiceInterface {
mock := &AccessControlServiceInterface{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

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

@@ -0,0 +1,71 @@
// Code generated by mockery v2.42.2. DO NOT EDIT.
// Regenerate this file using `make einterfaces-mocks`.
package mocks
import (
jobs "github.com/mattermost/mattermost/server/v8/einterfaces/jobs"
mock "github.com/stretchr/testify/mock"
model "github.com/mattermost/mattermost/server/public/model"
)
// AccessControlSyncJobInterface is an autogenerated mock type for the AccessControlSyncJobInterface type
type AccessControlSyncJobInterface struct {
mock.Mock
}
// MakeScheduler provides a mock function with given fields:
func (_m *AccessControlSyncJobInterface) MakeScheduler() jobs.Scheduler {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for MakeScheduler")
}
var r0 jobs.Scheduler
if rf, ok := ret.Get(0).(func() jobs.Scheduler); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(jobs.Scheduler)
}
}
return r0
}
// MakeWorker provides a mock function with given fields:
func (_m *AccessControlSyncJobInterface) MakeWorker() model.Worker {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for MakeWorker")
}
var r0 model.Worker
if rf, ok := ret.Get(0).(func() model.Worker); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.Worker)
}
}
return r0
}
// NewAccessControlSyncJobInterface creates a new instance of AccessControlSyncJobInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewAccessControlSyncJobInterface(t interface {
mock.TestingT
Cleanup(func())
}) *AccessControlSyncJobInterface {
mock := &AccessControlSyncJobInterface{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

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

@@ -308,11 +308,6 @@ func (_m *MetricsInterface) ObserveAPIEndpointDuration(endpoint string, method s
_m.Called(endpoint, method, statusCode, originClient, pageLoadContext, elapsed)
}
// ObserveAccessControlEngineInitDuration provides a mock function with given fields: value
func (_m *MetricsInterface) ObserveAccessControlEngineInitDuration(value float64) {
_m.Called(value)
}
// ObserveAccessControlEvaluateDuration provides a mock function with given fields: value
func (_m *MetricsInterface) ObserveAccessControlEvaluateDuration(value float64) {
_m.Called(value)
@@ -323,6 +318,11 @@ func (_m *MetricsInterface) ObserveAccessControlExpressionCompileDuration(value
_m.Called(value)
}
// ObserveAccessControlSearchQueryDuration provides a mock function with given fields: value
func (_m *MetricsInterface) ObserveAccessControlSearchQueryDuration(value float64) {
_m.Called(value)
}
// ObserveClientChannelSwitchDuration provides a mock function with given fields: platform, agent, fresh, userID, elapsed
func (_m *MetricsInterface) ObserveClientChannelSwitchDuration(platform string, agent string, fresh string, userID string, elapsed float64) {
_m.Called(platform, agent, fresh, userID, elapsed)

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

@@ -0,0 +1,372 @@
// Code generated by mockery v2.42.2. DO NOT EDIT.
// Regenerate this file using `make einterfaces-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost/server/public/model"
request "github.com/mattermost/mattermost/server/public/shared/request"
mock "github.com/stretchr/testify/mock"
)
// PolicyAdministrationPointInterface is an autogenerated mock type for the PolicyAdministrationPointInterface type
type PolicyAdministrationPointInterface struct {
mock.Mock
}
// CheckExpression provides a mock function with given fields: rctx, expression
func (_m *PolicyAdministrationPointInterface) CheckExpression(rctx request.CTX, expression string) ([]model.CELExpressionError, *model.AppError) {
ret := _m.Called(rctx, expression)
if len(ret) == 0 {
panic("no return value specified for CheckExpression")
}
var r0 []model.CELExpressionError
var r1 *model.AppError
if rf, ok := ret.Get(0).(func(request.CTX, string) ([]model.CELExpressionError, *model.AppError)); ok {
return rf(rctx, expression)
}
if rf, ok := ret.Get(0).(func(request.CTX, string) []model.CELExpressionError); ok {
r0 = rf(rctx, expression)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]model.CELExpressionError)
}
}
if rf, ok := ret.Get(1).(func(request.CTX, string) *model.AppError); ok {
r1 = rf(rctx, expression)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// DeletePolicy provides a mock function with given fields: rctx, id
func (_m *PolicyAdministrationPointInterface) DeletePolicy(rctx request.CTX, id string) *model.AppError {
ret := _m.Called(rctx, id)
if len(ret) == 0 {
panic("no return value specified for DeletePolicy")
}
var r0 *model.AppError
if rf, ok := ret.Get(0).(func(request.CTX, string) *model.AppError); ok {
r0 = rf(rctx, id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AppError)
}
}
return r0
}
// ExpressionToVisualAST provides a mock function with given fields: rctx, expression
func (_m *PolicyAdministrationPointInterface) ExpressionToVisualAST(rctx request.CTX, expression string) (*model.VisualExpression, *model.AppError) {
ret := _m.Called(rctx, expression)
if len(ret) == 0 {
panic("no return value specified for ExpressionToVisualAST")
}
var r0 *model.VisualExpression
var r1 *model.AppError
if rf, ok := ret.Get(0).(func(request.CTX, string) (*model.VisualExpression, *model.AppError)); ok {
return rf(rctx, expression)
}
if rf, ok := ret.Get(0).(func(request.CTX, string) *model.VisualExpression); ok {
r0 = rf(rctx, expression)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.VisualExpression)
}
}
if rf, ok := ret.Get(1).(func(request.CTX, string) *model.AppError); ok {
r1 = rf(rctx, expression)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// GetChannelMembersToRemove provides a mock function with given fields: rctx, channelID
func (_m *PolicyAdministrationPointInterface) GetChannelMembersToRemove(rctx request.CTX, channelID string) ([]*model.ChannelMember, *model.AppError) {
ret := _m.Called(rctx, channelID)
if len(ret) == 0 {
panic("no return value specified for GetChannelMembersToRemove")
}
var r0 []*model.ChannelMember
var r1 *model.AppError
if rf, ok := ret.Get(0).(func(request.CTX, string) ([]*model.ChannelMember, *model.AppError)); ok {
return rf(rctx, channelID)
}
if rf, ok := ret.Get(0).(func(request.CTX, string) []*model.ChannelMember); ok {
r0 = rf(rctx, channelID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.ChannelMember)
}
}
if rf, ok := ret.Get(1).(func(request.CTX, string) *model.AppError); ok {
r1 = rf(rctx, channelID)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// GetPolicy provides a mock function with given fields: rctx, id
func (_m *PolicyAdministrationPointInterface) GetPolicy(rctx request.CTX, id string) (*model.AccessControlPolicy, *model.AppError) {
ret := _m.Called(rctx, id)
if len(ret) == 0 {
panic("no return value specified for GetPolicy")
}
var r0 *model.AccessControlPolicy
var r1 *model.AppError
if rf, ok := ret.Get(0).(func(request.CTX, string) (*model.AccessControlPolicy, *model.AppError)); ok {
return rf(rctx, id)
}
if rf, ok := ret.Get(0).(func(request.CTX, string) *model.AccessControlPolicy); ok {
r0 = rf(rctx, id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AccessControlPolicy)
}
}
if rf, ok := ret.Get(1).(func(request.CTX, string) *model.AppError); ok {
r1 = rf(rctx, id)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// GetPolicyRuleAttributes provides a mock function with given fields: rctx, policyID, action
func (_m *PolicyAdministrationPointInterface) GetPolicyRuleAttributes(rctx request.CTX, policyID string, action string) (map[string][]string, *model.AppError) {
ret := _m.Called(rctx, policyID, action)
if len(ret) == 0 {
panic("no return value specified for GetPolicyRuleAttributes")
}
var r0 map[string][]string
var r1 *model.AppError
if rf, ok := ret.Get(0).(func(request.CTX, string, string) (map[string][]string, *model.AppError)); ok {
return rf(rctx, policyID, action)
}
if rf, ok := ret.Get(0).(func(request.CTX, string, string) map[string][]string); ok {
r0 = rf(rctx, policyID, action)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string][]string)
}
}
if rf, ok := ret.Get(1).(func(request.CTX, string, string) *model.AppError); ok {
r1 = rf(rctx, policyID, action)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// Init provides a mock function with given fields: rctx
func (_m *PolicyAdministrationPointInterface) Init(rctx request.CTX) *model.AppError {
ret := _m.Called(rctx)
if len(ret) == 0 {
panic("no return value specified for Init")
}
var r0 *model.AppError
if rf, ok := ret.Get(0).(func(request.CTX) *model.AppError); ok {
r0 = rf(rctx)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AppError)
}
}
return r0
}
// NormalizePolicy provides a mock function with given fields: rctx, policy
func (_m *PolicyAdministrationPointInterface) NormalizePolicy(rctx request.CTX, policy *model.AccessControlPolicy) (*model.AccessControlPolicy, *model.AppError) {
ret := _m.Called(rctx, policy)
if len(ret) == 0 {
panic("no return value specified for NormalizePolicy")
}
var r0 *model.AccessControlPolicy
var r1 *model.AppError
if rf, ok := ret.Get(0).(func(request.CTX, *model.AccessControlPolicy) (*model.AccessControlPolicy, *model.AppError)); ok {
return rf(rctx, policy)
}
if rf, ok := ret.Get(0).(func(request.CTX, *model.AccessControlPolicy) *model.AccessControlPolicy); ok {
r0 = rf(rctx, policy)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AccessControlPolicy)
}
}
if rf, ok := ret.Get(1).(func(request.CTX, *model.AccessControlPolicy) *model.AppError); ok {
r1 = rf(rctx, policy)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// QueryUsersForExpression provides a mock function with given fields: rctx, expression, opts
func (_m *PolicyAdministrationPointInterface) QueryUsersForExpression(rctx request.CTX, expression string, opts model.SubjectSearchOptions) ([]*model.User, int64, *model.AppError) {
ret := _m.Called(rctx, expression, opts)
if len(ret) == 0 {
panic("no return value specified for QueryUsersForExpression")
}
var r0 []*model.User
var r1 int64
var r2 *model.AppError
if rf, ok := ret.Get(0).(func(request.CTX, string, model.SubjectSearchOptions) ([]*model.User, int64, *model.AppError)); ok {
return rf(rctx, expression, opts)
}
if rf, ok := ret.Get(0).(func(request.CTX, string, model.SubjectSearchOptions) []*model.User); ok {
r0 = rf(rctx, expression, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
if rf, ok := ret.Get(1).(func(request.CTX, string, model.SubjectSearchOptions) int64); ok {
r1 = rf(rctx, expression, opts)
} else {
r1 = ret.Get(1).(int64)
}
if rf, ok := ret.Get(2).(func(request.CTX, string, model.SubjectSearchOptions) *model.AppError); ok {
r2 = rf(rctx, expression, opts)
} else {
if ret.Get(2) != nil {
r2 = ret.Get(2).(*model.AppError)
}
}
return r0, r1, r2
}
// QueryUsersForResource provides a mock function with given fields: rctx, resourceID, action, opts
func (_m *PolicyAdministrationPointInterface) QueryUsersForResource(rctx request.CTX, resourceID string, action string, opts model.SubjectSearchOptions) ([]*model.User, int64, *model.AppError) {
ret := _m.Called(rctx, resourceID, action, opts)
if len(ret) == 0 {
panic("no return value specified for QueryUsersForResource")
}
var r0 []*model.User
var r1 int64
var r2 *model.AppError
if rf, ok := ret.Get(0).(func(request.CTX, string, string, model.SubjectSearchOptions) ([]*model.User, int64, *model.AppError)); ok {
return rf(rctx, resourceID, action, opts)
}
if rf, ok := ret.Get(0).(func(request.CTX, string, string, model.SubjectSearchOptions) []*model.User); ok {
r0 = rf(rctx, resourceID, action, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
if rf, ok := ret.Get(1).(func(request.CTX, string, string, model.SubjectSearchOptions) int64); ok {
r1 = rf(rctx, resourceID, action, opts)
} else {
r1 = ret.Get(1).(int64)
}
if rf, ok := ret.Get(2).(func(request.CTX, string, string, model.SubjectSearchOptions) *model.AppError); ok {
r2 = rf(rctx, resourceID, action, opts)
} else {
if ret.Get(2) != nil {
r2 = ret.Get(2).(*model.AppError)
}
}
return r0, r1, r2
}
// SavePolicy provides a mock function with given fields: rctx, policy
func (_m *PolicyAdministrationPointInterface) SavePolicy(rctx request.CTX, policy *model.AccessControlPolicy) (*model.AccessControlPolicy, *model.AppError) {
ret := _m.Called(rctx, policy)
if len(ret) == 0 {
panic("no return value specified for SavePolicy")
}
var r0 *model.AccessControlPolicy
var r1 *model.AppError
if rf, ok := ret.Get(0).(func(request.CTX, *model.AccessControlPolicy) (*model.AccessControlPolicy, *model.AppError)); ok {
return rf(rctx, policy)
}
if rf, ok := ret.Get(0).(func(request.CTX, *model.AccessControlPolicy) *model.AccessControlPolicy); ok {
r0 = rf(rctx, policy)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AccessControlPolicy)
}
}
if rf, ok := ret.Get(1).(func(request.CTX, *model.AccessControlPolicy) *model.AppError); ok {
r1 = rf(rctx, policy)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// NewPolicyAdministrationPointInterface creates a new instance of PolicyAdministrationPointInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewPolicyAdministrationPointInterface(t interface {
mock.TestingT
Cleanup(func())
}) *PolicyAdministrationPointInterface {
mock := &PolicyAdministrationPointInterface{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

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

@@ -16,24 +16,22 @@ type PolicyDecisionPointInterface struct {
}
// AccessEvaluation provides a mock function with given fields: rctx, accessRequest
func (_m *PolicyDecisionPointInterface) AccessEvaluation(rctx request.CTX, accessRequest model.AccessRequest) (*model.AccessDecision, *model.AppError) {
func (_m *PolicyDecisionPointInterface) AccessEvaluation(rctx request.CTX, accessRequest model.AccessRequest) (model.AccessDecision, *model.AppError) {
ret := _m.Called(rctx, accessRequest)
if len(ret) == 0 {
panic("no return value specified for AccessEvaluation")
}
var r0 *model.AccessDecision
var r0 model.AccessDecision
var r1 *model.AppError
if rf, ok := ret.Get(0).(func(request.CTX, model.AccessRequest) (*model.AccessDecision, *model.AppError)); ok {
if rf, ok := ret.Get(0).(func(request.CTX, model.AccessRequest) (model.AccessDecision, *model.AppError)); ok {
return rf(rctx, accessRequest)
}
if rf, ok := ret.Get(0).(func(request.CTX, model.AccessRequest) *model.AccessDecision); ok {
if rf, ok := ret.Get(0).(func(request.CTX, model.AccessRequest) model.AccessDecision); ok {
r0 = rf(rctx, accessRequest)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AccessDecision)
}
r0 = ret.Get(0).(model.AccessDecision)
}
if rf, ok := ret.Get(1).(func(request.CTX, model.AccessRequest) *model.AppError); ok {

42
server/einterfaces/pap.go Обычный файл
Просмотреть файл

@@ -0,0 +1,42 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package einterfaces
import (
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/request"
)
// PolicyAdministrationPointInterface is the service that manages access control policies.
// It is responsible for creating, updating, and deleting policies.
// Also, it provides methods to check the validity of expressions and to retrieve policies.
type PolicyAdministrationPointInterface interface {
// Init initializes the policy administration point and intiates the CEL engine.
// It is an idempotent operation, meaning that it can be called multiple times.
Init(rctx request.CTX) *model.AppError
// GetPolicyRuleAttributes retrieves the attributes of the given policy.
// It returns a map of attribute names to their values for given action.
GetPolicyRuleAttributes(rctx request.CTX, policyID string, action string) (map[string][]string, *model.AppError)
// CheckExpression checks the validity of the given expression using the CEL engine.
// It returns a list of CELExpressionError if the expression is invalid.
// If the expression is valid, it returns an empty list.
CheckExpression(rctx request.CTX, expression string) ([]model.CELExpressionError, *model.AppError)
// ExpressionToVisualAST converts the given expression to a visual AST.
ExpressionToVisualAST(rctx request.CTX, expression string) (*model.VisualExpression, *model.AppError)
// NormalizePolicy normalizes the given policy by restoring ids back to names.
NormalizePolicy(rctx request.CTX, policy *model.AccessControlPolicy) (*model.AccessControlPolicy, *model.AppError)
// QueryUsersForExpression evaluates the given expression using the CEL engine.
// It returns a list of users that match the expression.
QueryUsersForExpression(rctx request.CTX, expression string, opts model.SubjectSearchOptions) ([]*model.User, int64, *model.AppError)
// QueryUsersForResource evaluates finds the users match to the resource.
QueryUsersForResource(rctx request.CTX, resourceID, action string, opts model.SubjectSearchOptions) ([]*model.User, int64, *model.AppError)
// GetChannelMembersToRemove retrieves the channel members that need to be removed from the given channel.
GetChannelMembersToRemove(rctx request.CTX, channelID string) ([]*model.ChannelMember, *model.AppError)
// SavePolicy saves the given access control policy.
SavePolicy(rctx request.CTX, policy *model.AccessControlPolicy) (*model.AccessControlPolicy, *model.AppError)
// GetPolicy retrieves the access control policy with the given ID.
GetPolicy(rctx request.CTX, id string) (*model.AccessControlPolicy, *model.AppError)
// DeletePolicy deletes the access control policy with the given ID.
DeletePolicy(rctx request.CTX, id string) *model.AppError
}

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

@@ -12,5 +12,5 @@ import (
// using the OpenID Auth API spec. It determines whether a subject can perform
// an action on a resource based on the resource policy.
type PolicyDecisionPointInterface interface {
AccessEvaluation(rctx request.CTX, accessRequest model.AccessRequest) (*model.AccessDecision, *model.AppError)
AccessEvaluation(rctx request.CTX, accessRequest model.AccessRequest) (model.AccessDecision, *model.AppError)
}

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

@@ -34,4 +34,6 @@ import (
_ "github.com/mattermost/enterprise/ip_filtering"
// Needed to ensure the init() method in the EE gets run
_ "github.com/mattermost/enterprise/outgoing_oauth_connections"
// Needed to ensure the init() method in the EE gets run
_ "github.com/mattermost/enterprise/access_control"
)

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

@@ -236,9 +236,9 @@ type MetricsInterfaceImpl struct {
DesktopClientCPUUsage *prometheus.HistogramVec
DesktopClientMemoryUsage *prometheus.HistogramVec
AccessControlEngineInitDuration prometheus.Histogram
AccessControlExpressionCompileDuration prometheus.Histogram
AccessControlEvaluateDuration prometheus.Histogram
AccessControlSearchQueryDuration prometheus.Histogram
AccessControlCacheInvalidation prometheus.Counter
}
@@ -1541,34 +1541,31 @@ func New(ps *platform.PlatformService, driver, dataSource string) *MetricsInterf
)
m.Registry.MustRegister(m.DesktopClientMemoryUsage)
m.AccessControlEngineInitDuration = prometheus.NewHistogram(
prometheus.HistogramOpts{
Namespace: MetricsNamespace,
Subsystem: MetricsSubsystemAccessControl,
Name: "engine_init_duration_seconds",
Help: "Duration of the time taken to initialize the access control engine (seconds)",
ConstLabels: additionalLabels,
})
m.Registry.MustRegister(m.AccessControlEngineInitDuration)
m.AccessControlSearchQueryDuration = prometheus.NewHistogram(
withLabels(prometheus.HistogramOpts{
Namespace: MetricsNamespace,
Subsystem: MetricsSubsystemAccessControl,
Name: "search_query_duration_seconds",
Help: "Duration of the time taken to query users against an expression (seconds)",
}))
m.Registry.MustRegister(m.AccessControlSearchQueryDuration)
m.AccessControlEvaluateDuration = prometheus.NewHistogram(
prometheus.HistogramOpts{
Namespace: MetricsNamespace,
Subsystem: MetricsSubsystemAccessControl,
Name: "evaluate_duration_seconds",
Help: "Duration of the time taken to evaluate the access control engine (seconds)",
ConstLabels: additionalLabels,
})
withLabels(prometheus.HistogramOpts{
Namespace: MetricsNamespace,
Subsystem: MetricsSubsystemAccessControl,
Name: "evaluate_duration_seconds",
Help: "Duration of the time taken to evaluate the access control engine (seconds)",
}))
m.Registry.MustRegister(m.AccessControlEvaluateDuration)
m.AccessControlExpressionCompileDuration = prometheus.NewHistogram(
prometheus.HistogramOpts{
Namespace: MetricsNamespace,
Subsystem: MetricsSubsystemAccessControl,
Name: "expression_compile_duration_seconds",
Help: "Duration of the time taken to compile the access control engine expression (seconds)",
ConstLabels: additionalLabels,
})
withLabels(prometheus.HistogramOpts{
Namespace: MetricsNamespace,
Subsystem: MetricsSubsystemAccessControl,
Name: "expression_compile_duration_seconds",
Help: "Duration of the time taken to compile the access control engine expression (seconds)",
}))
m.Registry.MustRegister(m.AccessControlExpressionCompileDuration)
m.AccessControlCacheInvalidation = prometheus.NewCounter(
@@ -2177,8 +2174,8 @@ func (mi *MetricsInterfaceImpl) ObserveMobileClientSessionMetadata(version, plat
mi.MobileClientSessionMetadataGauge.With(prometheus.Labels{"version": version, "platform": platform, "notifications_disabled": notificationDisabled}).Set(value)
}
func (mi *MetricsInterfaceImpl) ObserveAccessControlEngineInitDuration(value float64) {
mi.AccessControlEngineInitDuration.Observe(value)
func (mi *MetricsInterfaceImpl) ObserveAccessControlSearchQueryDuration(value float64) {
mi.AccessControlSearchQueryDuration.Observe(value)
}
func (mi *MetricsInterfaceImpl) ObserveAccessControlExpressionCompileDuration(value float64) {

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

@@ -4,6 +4,8 @@ go 1.23.0
toolchain go1.23.7
//replace github.com/mattermost/mattermost/server/public => /Users/ibrahim/go/src/github.com/mattermost/mattermost-server/server/public
require (
code.sajari.com/docconv/v2 v2.0.0-pre.4
github.com/Masterminds/semver/v3 v3.3.1
@@ -224,9 +226,9 @@ require (
golang.org/x/sys v0.32.0 // indirect
golang.org/x/text v0.24.0 // indirect
golang.org/x/tools v0.29.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250124145028-65684f501c47 // indirect
google.golang.org/grpc v1.70.0 // indirect
google.golang.org/protobuf v1.36.4 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect
google.golang.org/grpc v1.71.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect

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

@@ -670,10 +670,10 @@ go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4=
go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU=
go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
@@ -860,14 +860,14 @@ google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoA
google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250124145028-65684f501c47 h1:91mG8dNTpkC0uChJUQ9zCiRqx3GEEFOWaRZ0mI6Oj2I=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250124145028-65684f501c47/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 h1:iK2jbkWL86DXjEx0qiHcRE9dE4/Ahua5k6V8OWFb//c=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -875,8 +875,8 @@ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miE
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=

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

@@ -47,6 +47,14 @@
"id": "September",
"translation": "September"
},
{
"id": "api.access_control_policy.get_channels.limit.app_error",
"translation": "Get channels limit is not valid."
},
{
"id": "api.access_control_policy.get_fields.limit.app_error",
"translation": "Get fields limit is not valid."
},
{
"id": "api.acknowledgement.delete.archived_channel.app_error",
"translation": "You cannot remove an acknowledgment in an archived channel."
@@ -239,6 +247,10 @@
"id": "api.channel.add_user.to.channel.failed.deleted.app_error",
"translation": "Failed to add user to channel because they have been removed from the team."
},
{
"id": "api.channel.add_user.to.channel.rejected",
"translation": "User does not have required attributes to join the channel."
},
{
"id": "api.channel.add_user_to_channel.type.app_error",
"translation": "Can not add user to this channel type."
@@ -531,6 +543,10 @@
"id": "api.channel.update_channel.deleted.app_error",
"translation": "The channel has been archived or deleted."
},
{
"id": "api.channel.update_channel.not_allowed.app_error",
"translation": "Policy enforced channels cannot be updated."
},
{
"id": "api.channel.update_channel.tried.app_error",
"translation": "Tried to perform an invalid update of the default channel {{.Channel}}."
@@ -4646,6 +4662,10 @@
"id": "app.channel.delete.app_error",
"translation": "Unable to delete the channel."
},
{
"id": "app.channel.get.app_error",
"translation": "Could not get channel."
},
{
"id": "app.channel.get.existing.app_error",
"translation": "Unable to find the existing channel {{.channel_id}}."
@@ -6206,6 +6226,94 @@
"id": "app.oauth.update_app.updating.app_error",
"translation": "We encountered an error updating the app."
},
{
"id": "app.pap.assign_access_control_policy_to_channels.app_error",
"translation": "Unable to assign access control policy to channels."
},
{
"id": "app.pap.check_expression.app_error",
"translation": "Could not check expression."
},
{
"id": "app.pap.create_access_control_policy.app_error",
"translation": "Could not create access control policy."
},
{
"id": "app.pap.delete_access_control_policy.app_error",
"translation": "Could not delete access control policy."
},
{
"id": "app.pap.delete_policy.app_error",
"translation": "Unable to delete access control policy."
},
{
"id": "app.pap.expression_to_visual_ast.app_error",
"translation": "Could not genereate visual AST from expression."
},
{
"id": "app.pap.get_access_control_auto_complete.app_error",
"translation": "Could not get access control auto complete."
},
{
"id": "app.pap.get_all_access_control_policies.app_error",
"translation": "Could not get access control policies."
},
{
"id": "app.pap.get_channel_access_control_attributes.app_error",
"translation": "Could not get attributes for channel."
},
{
"id": "app.pap.get_channel_members_to_remove.app_error",
"translation": "Could not get channel members to remove."
},
{
"id": "app.pap.get_policy.app_error",
"translation": "Unable to retrieve the access control policy."
},
{
"id": "app.pap.get_policy_attributes.app_error",
"translation": "Could not get attributes for policy."
},
{
"id": "app.pap.init.app_error",
"translation": "Unable to initialize access control service."
},
{
"id": "app.pap.is_ready.app_error",
"translation": "Access control service is not ready."
},
{
"id": "app.pap.missing_attribute.app_error",
"translation": "An attribute is missing from the expression."
},
{
"id": "app.pap.normalize_policy.app_error",
"translation": "Could not normalize policy expression."
},
{
"id": "app.pap.query_expression.app_error",
"translation": "Could not query for expression."
},
{
"id": "app.pap.save_policy.app_error",
"translation": "Unable to save access control policy."
},
{
"id": "app.pap.search_access_control_policies.app_error",
"translation": "Could not search access control policies."
},
{
"id": "app.pap.unassign_access_control_policy_from_channels.app_error",
"translation": "Could not unassign access control policy from channels."
},
{
"id": "app.pap.update_access_control_policy_active.app_error",
"translation": "Could not change active status of access control policy."
},
{
"id": "app.pdp.access_evaluation.app_error",
"translation": "Failed evaluate access control policy."
},
{
"id": "app.plugin.cluster.save_config.app_error",
"translation": "The plugin configuration in your config.json file must be updated manually when using ReadOnlyConfig with clustering enabled."
@@ -7772,6 +7880,10 @@
"id": "common.parse_error_int64",
"translation": "Failed to parse the value:{{.Value}} to int64"
},
{
"id": "ent.access_control.sync_job.app_error",
"translation": "Failed to run access control sync job."
},
{
"id": "ent.account_migration.get_all_failed",
"translation": "Unable to get users."
@@ -8552,6 +8664,10 @@
"id": "model.access.is_valid.user_id.app_error",
"translation": "Invalid user id."
},
{
"id": "model.access_policy.inherit.version.app_error",
"translation": "Could not inherit access control policy."
},
{
"id": "model.access_policy.is_valid.id.app_error",
"translation": "Invalid policy id."

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

@@ -78,6 +78,7 @@ const (
TrackConfigExport = "config_export"
TrackConfigWrangler = "config_wrangler"
TrackConfigConnectedWorkspaces = "config_connected_workspaces"
TrackConfigAccessControl = "config_access_control"
TrackFeatureFlags = "config_feature_flags"
TrackPermissionsGeneral = "permissions_general"
TrackPermissionsSystemScheme = "permissions_system_scheme"
@@ -973,6 +974,11 @@ func (ts *TelemetryService) trackConfig() {
"max_posts_per_sync": *cfg.ConnectedWorkspacesSettings.MaxPostsPerSync,
}
configs[TrackConfigAccessControl] = map[string]any{
"enable_attribute_based_access_control": *cfg.AccessControlSettings.EnableAttributeBasedAccessControl,
"enable_channel_scope_access_control": *cfg.AccessControlSettings.EnableChannelScopeAccessControl,
}
// Convert feature flags to map[string]any for sending
flags := cfg.FeatureFlags.ToMap()
interfaceFlags := make(map[string]any)

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

@@ -4,8 +4,10 @@
package model
import (
"fmt"
"slices"
"github.com/pkg/errors"
"golang.org/x/mod/semver"
)
@@ -18,13 +20,41 @@ const (
AccessControlPolicyVersionV0_1 = "v0.1"
)
// ParentPolicy is a augmented version of AccessPolicy to be used in
// system console and API responses.
type ParentPolicy struct {
ID string `json:"id"`
Name string `json:"name"`
Attributes map[string]string `json:"attributes"`
Children []*AccessControlPolicy `json:"children"`
// AccessControlAttribute represents a user attribute with its name and possible values
type AccessControlAttribute struct {
Attribute PropertyField `json:"attribute"`
Values []string `json:"values"`
}
type AccessControlPolicyTestResponse struct {
Users []*User `json:"users"`
Total int64 `json:"total"`
}
type GetAccessControlPolicyOptions struct {
Type string `json:"type"`
ParentID string `json:"parent_id"`
Cursor AccessControlPolicyCursor `json:"cursor"`
Limit int `json:"limit"`
}
type AccessControlPolicySearch struct {
Term string `json:"term"`
Type string `json:"type"`
ParentID string `json:"parent_id"`
Cursor AccessControlPolicyCursor `json:"cursor"`
Limit int `json:"limit"`
IncludeChildren bool `json:"include_children"`
Active bool `json:"active"`
}
type AccessControlPolicyCursor struct {
ID string `json:"id"`
}
type AccessControlPoliciesWithCount struct {
Policies []*AccessControlPolicy `json:"policies"`
Total int64 `json:"total"`
}
type AccessControlPolicy struct {
@@ -48,6 +78,16 @@ type AccessControlPolicyRule struct {
Expression string `json:"expression"`
}
type CELExpressionError struct {
Line int `json:"line"`
Column int `json:"column"`
Message string `json:"message"`
}
type AccessControlQueryResult struct {
MatchedSubjectIDs []string `json:"matched_subject_ids"`
}
func (p *AccessControlPolicy) IsValid() *AppError {
switch p.Version {
case AccessControlPolicyVersionV0_1:
@@ -103,3 +143,63 @@ func (p *AccessControlPolicy) accessPolicyVersionV0_1() *AppError {
return nil
}
func (p *AccessControlPolicy) Inherit(resourceID, resourceType string) (*AccessControlPolicy, *AppError) {
rules := make([]AccessControlPolicyRule, len(p.Rules))
switch p.Version {
case AccessControlPolicyVersionV0_1:
for i, rule := range p.Rules {
actions := make([]string, len(rule.Actions))
copy(actions, rule.Actions)
rules[i] = AccessControlPolicyRule{
Actions: actions,
Expression: fmt.Sprintf("policies.id_%s", p.ID),
}
}
default:
return nil, NewAppError("AccessControlPolicy.Inherit", "model.access_policy.inherit.version.app_error", nil, "", 400)
}
child := &AccessControlPolicy{
ID: resourceID,
Type: resourceType,
Active: p.Active,
CreateAt: GetMillis(),
Version: p.Version,
Imports: []string{p.ID},
Rules: rules,
Props: map[string]any{},
}
if appErr := child.IsValid(); appErr != nil {
return nil, appErr
}
return child, nil
}
func (c *AccessControlPolicyCursor) IsEmpty() bool {
return c.ID == ""
}
func (c *AccessControlPolicyCursor) IsValid() error {
if c.IsEmpty() {
return nil
}
if !IsValidId(c.ID) {
return errors.New("cursor id is invalid")
}
return nil
}
func (p *AccessControlPolicy) Auditable() map[string]any {
return map[string]any{
"id": p.ID,
"type": p.Type,
"revision": p.Revision,
}
}

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

@@ -11,10 +11,30 @@ type Subject struct {
ID string `json:"id"`
// Type specifies the type of the Subject, eg. user, bot, etc.
Type string `json:"type"`
// Properties are the key-value pairs assicuated with the subject.
// Attributes are the key-value pairs assicuated with the subject.
// An attribute may be single-valued or multi-valued and can be a primitive type
// (string, boolean, number) or a complex type like a JSON object or array.
Properties map[string]any `json:"properties"`
Attributes map[string]any `json:"attributes"`
}
type SubjectSearchOptions struct {
Term string `json:"term"`
TeamID string `json:"team_id"`
// Query and Args should be generated within the Access Control Service
// and passed here wrt database driver
Query string `json:"query"`
Args []any `json:"args"`
Limit int `json:"limit"`
Cursor SubjectCursor `json:"cursor"`
AllowInactive bool `json:"allow_inactive"`
IgnoreCount bool `json:"ignore_count"`
// ExcludeChannelMembers is used to exclude members from the search results
// specifically used when syncing channel members
ExcludeChannelMembers string `json:"exclude_members"`
}
type SubjectCursor struct {
TargetID string `json:"target_id"`
}
// Resource is the target of an access request.
@@ -41,3 +61,10 @@ type AccessDecision struct {
Decision bool `json:"decision"`
Context map[string]any `json:"context,omitempty"`
}
type QueryExpressionParams struct {
Expression string `json:"expression"`
Term string `json:"term"`
Limit int `json:"limit"`
After string `json:"after"`
}

30
server/public/model/cel.go Обычный файл
Просмотреть файл

@@ -0,0 +1,30 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
// ValueType indicates whether a value is a literal or another attribute.
type ValueType int
const (
LiteralValue ValueType = iota
AttrValue
)
// Condition represents a single logical condition (e.g., user.attributes.Team == "Engineering").
type Condition struct {
// Left-hand side attribute selector (e.g., "user.attributes.Team").
Attribute string `json:"attribute"`
// The comparison operator.
Operator string `json:"operator"`
// Right-hand side value(s). Can be a single value or a slice for 'in'.
Value any `json:"value"`
// Type of the Value (LiteralValue or AttributeValue). Needed for comparisons like user.attr1 == user.attr2.
ValueType ValueType `json:"value_type"`
}
// VisualExpression represents a series of conditions combined with logical AND.
type VisualExpression struct {
// Conditions is a list of individual conditions that will be ANDed together.
Conditions []Condition `json:"conditions"`
}

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

@@ -99,6 +99,7 @@ type Channel struct {
PolicyID *string `json:"policy_id"`
LastRootPostAt int64 `json:"last_root_post_at"`
BannerInfo *ChannelBannerInfo `json:"banner_info"`
PolicyEnforced bool `json:"policy_enforced"`
}
func (o *Channel) Auditable() map[string]any {
@@ -119,6 +120,7 @@ func (o *Channel) Auditable() map[string]any {
"total_msg_count_root": o.TotalMsgCountRoot,
"type": o.Type,
"update_at": o.UpdateAt,
"policy_enforced": o.PolicyEnforced,
}
}
@@ -209,26 +211,30 @@ type ChannelModeratedRolesPatch struct {
// Paginate whether to paginate the results.
// Page page requested, if results are paginated.
// PerPage number of results per page, if paginated.
// ExcludeAccessPolicyEnforced will exclude channels that are enforced by an access policy.
type ChannelSearchOpts struct {
NotAssociatedToGroup string
ExcludeDefaultChannels bool
IncludeDeleted bool // If true, deleted channels will be included in the results.
Deleted bool
ExcludeChannelNames []string
TeamIds []string
GroupConstrained bool
ExcludeGroupConstrained bool
PolicyID string
ExcludePolicyConstrained bool
IncludePolicyID bool
IncludeSearchById bool
ExcludeRemote bool
Public bool
Private bool
Page *int
PerPage *int
LastDeleteAt int // When combined with IncludeDeleted, only channels deleted after this time will be returned.
LastUpdateAt int
NotAssociatedToGroup string
ExcludeDefaultChannels bool
IncludeDeleted bool // If true, deleted channels will be included in the results.
Deleted bool
ExcludeChannelNames []string
TeamIds []string
GroupConstrained bool
ExcludeGroupConstrained bool
PolicyID string
ExcludePolicyConstrained bool
IncludePolicyID bool
IncludeSearchById bool
ExcludeRemote bool
Public bool
Private bool
Page *int
PerPage *int
LastDeleteAt int // When combined with IncludeDeleted, only channels deleted after this time will be returned.
LastUpdateAt int
AccessControlPolicyEnforced bool
ExcludeAccessControlPolicyEnforced bool
ParentAccessControlPolicyId string
}
type ChannelMemberCountByGroup struct {

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

@@ -6,19 +6,22 @@ package model
const ChannelSearchDefaultLimit = 50
type ChannelSearch struct {
Term string `json:"term"`
ExcludeDefaultChannels bool `json:"exclude_default_channels"`
NotAssociatedToGroup string `json:"not_associated_to_group"`
TeamIds []string `json:"team_ids"`
GroupConstrained bool `json:"group_constrained"`
ExcludeGroupConstrained bool `json:"exclude_group_constrained"`
ExcludePolicyConstrained bool `json:"exclude_policy_constrained"`
Public bool `json:"public"`
Private bool `json:"private"`
IncludeDeleted bool `json:"include_deleted"`
IncludeSearchById bool `json:"include_search_by_id"`
ExcludeRemote bool `json:"exclude_remote"`
Deleted bool `json:"deleted"`
Page *int `json:"page,omitempty"`
PerPage *int `json:"per_page,omitempty"`
Term string `json:"term"`
ExcludeDefaultChannels bool `json:"exclude_default_channels"`
NotAssociatedToGroup string `json:"not_associated_to_group"`
TeamIds []string `json:"team_ids"`
GroupConstrained bool `json:"group_constrained"`
ExcludeGroupConstrained bool `json:"exclude_group_constrained"`
ExcludePolicyConstrained bool `json:"exclude_policy_constrained"`
Public bool `json:"public"`
Private bool `json:"private"`
IncludeDeleted bool `json:"include_deleted"`
IncludeSearchById bool `json:"include_search_by_id"`
ExcludeRemote bool `json:"exclude_remote"`
Deleted bool `json:"deleted"`
Page *int `json:"page,omitempty"`
PerPage *int `json:"per_page,omitempty"`
AccessControlPolicyEnforced bool `json:"access_control_policy_enforced"`
ExcludeAccessControlPolicyEnforced bool `json:"exclude_access_control_policy_enforced"`
ParentAccessControlPolicyId string `json:"parent_access_control_policy_id"`
}

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

@@ -622,6 +622,18 @@ func (c *Client4) customProfileAttributeValuesRoute() string {
return fmt.Sprintf("%s/values", c.customProfileAttributesRoute())
}
func (c *Client4) accessControlPoliciesRoute() string {
return "/access_control_policies"
}
func (c *Client4) celRoute() string {
return "/access_control_policies/cel"
}
func (c *Client4) accessControlPolicyRoute(policyID string) string {
return fmt.Sprintf(c.accessControlPoliciesRoute()+"/%v", policyID)
}
func (c *Client4) GetServerLimits(ctx context.Context) (*ServerLimits, *Response, error) {
r, err := c.DoAPIGet(ctx, c.limitsRoute()+"/users", "")
if err != nil {
@@ -9564,3 +9576,191 @@ func (c *Client4) PatchCPAValues(ctx context.Context, values map[string]json.Raw
return patchedValues, BuildResponse(r), nil
}
// Access Control Policies Section
// CreateAccessControlPolicy creates a new access control policy.
func (c *Client4) CreateAccessControlPolicy(ctx context.Context, policy *AccessControlPolicy) (*AccessControlPolicy, *Response, error) {
b, err := json.Marshal(policy)
if err != nil {
return nil, nil, NewAppError("CreateAccessControlPolicy", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPutBytes(ctx, c.accessControlPoliciesRoute(), b)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var p AccessControlPolicy
if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
return nil, nil, NewAppError("CreateAccessControlPolicy", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &p, BuildResponse(r), nil
}
func (c *Client4) GetAccessControlPolicy(ctx context.Context, id string) (*AccessControlPolicy, *Response, error) {
r, err := c.DoAPIGet(ctx, c.accessControlPolicyRoute(id), "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var policy AccessControlPolicy
if err := json.NewDecoder(r.Body).Decode(&policy); err != nil {
return nil, nil, NewAppError("GetAccessControlPolicy", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &policy, BuildResponse(r), nil
}
func (c *Client4) DeleteAccessControlPolicy(ctx context.Context, id string) (*Response, error) {
r, err := c.DoAPIDelete(ctx, c.accessControlPolicyRoute(id))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
func (c *Client4) CheckExpression(ctx context.Context, expression string) ([]CELExpressionError, *Response, error) {
checkExpressionRequest := struct {
Expression string `json:"expression"`
}{
Expression: expression,
}
b, err := json.Marshal(checkExpressionRequest)
if err != nil {
return nil, nil, NewAppError("CheckExpression", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(ctx, c.celRoute()+"/check", b)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var errors []CELExpressionError
if err := json.NewDecoder(r.Body).Decode(&errors); err != nil {
return nil, nil, NewAppError("CheckExpression", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return errors, BuildResponse(r), nil
}
func (c *Client4) TestExpression(ctx context.Context, params QueryExpressionParams) (*AccessControlPolicyTestResponse, *Response, error) {
b, err := json.Marshal(params)
if err != nil {
return nil, nil, NewAppError("TestExpression", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(ctx, c.celRoute()+"/test", b)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var testResponse AccessControlPolicyTestResponse
if err := json.NewDecoder(r.Body).Decode(&testResponse); err != nil {
return nil, nil, NewAppError("TestExpression", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &testResponse, BuildResponse(r), nil
}
func (c *Client4) SearchAccessControlPolicies(ctx context.Context, options AccessControlPolicySearch) (*AccessControlPoliciesWithCount, *Response, error) {
b, err := json.Marshal(options)
if err != nil {
return nil, nil, NewAppError("SearchAccessControlPolicies", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(ctx, c.accessControlPoliciesRoute()+"/search", b)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var policies AccessControlPoliciesWithCount
if err := json.NewDecoder(r.Body).Decode(&policies); err != nil {
return nil, nil, NewAppError("SearchAccessControlPolicies", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &policies, BuildResponse(r), nil
}
func (c *Client4) AssignAccessControlPolicies(ctx context.Context, policyID string, resourceIDs []string) (*Response, error) {
var assignments struct {
ChannelIds []string `json:"channel_ids"`
}
assignments.ChannelIds = resourceIDs
b, err := json.Marshal(assignments)
if err != nil {
return nil, NewAppError("AssignAccessControlPolicies", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(ctx, c.accessControlPolicyRoute(policyID)+"/assign", b)
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
func (c *Client4) UnassignAccessControlPolicies(ctx context.Context, policyID string, resourceIDs []string) (*Response, error) {
var unassignments struct {
ChannelIds []string `json:"channel_ids"`
}
unassignments.ChannelIds = resourceIDs
b, err := json.Marshal(unassignments)
if err != nil {
return nil, NewAppError("UnassignAccessControlPolicies", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIDeleteBytes(ctx, c.accessControlPolicyRoute(policyID)+"/unassign", b)
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
func (c *Client4) GetChannelsForAccessControlPolicy(ctx context.Context, policyID string, after string, limit int) (*ChannelsWithCount, *Response, error) {
r, err := c.DoAPIGet(ctx, c.accessControlPolicyRoute(policyID)+"/resources/channels?after="+after+"&limit="+strconv.Itoa(limit), "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var channels ChannelsWithCount
if err := json.NewDecoder(r.Body).Decode(&channels); err != nil {
return nil, nil, NewAppError("GetChannelsForAccessControlPolicy", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &channels, BuildResponse(r), nil
}
func (c *Client4) SearchChannelsForAccessControlPolicy(ctx context.Context, policyID string, options ChannelSearch) (*ChannelsWithCount, *Response, error) {
b, err := json.Marshal(options)
if err != nil {
return nil, nil, NewAppError("SearchChannelsForAccessControlPolicy", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(ctx, c.accessControlPolicyRoute(policyID)+"/resources/channels/search", b)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var channels ChannelsWithCount
if err := json.NewDecoder(r.Body).Decode(&channels); err != nil {
return nil, nil, NewAppError("SearchChannelsForAccessControlPolicy", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &channels, BuildResponse(r), nil
}

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

@@ -57,6 +57,8 @@ type FeatureFlags struct {
ExperimentalAuditSettingsSystemConsoleUI bool
CustomProfileAttributes bool
AttributeBasedAccessControl bool
}
func (f *FeatureFlags) SetDefaults() {
@@ -81,6 +83,7 @@ func (f *FeatureFlags) SetDefaults() {
f.NotificationMonitoring = true
f.ExperimentalAuditSettingsSystemConsoleUI = false
f.CustomProfileAttributes = false
f.AttributeBasedAccessControl = false
}
// ToMap returns the feature flags as a map[string]string

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

@@ -44,6 +44,7 @@ const (
JobTypeExportUsersToCSV = "export_users_to_csv"
JobTypeDeleteDmsPreferencesMigration = "delete_dms_preferences_migration"
JobTypeMobileSessionMetadata = "mobile_session_metadata"
JobTypeAccessControlSync = "access_control_sync"
JobStatusPending = "pending"
JobStatusInProgress = "in_progress"