From 10b1f4c5ac9a320ce4f27f9ddaa2651fb2ff7a9f Mon Sep 17 00:00:00 2001 From: Ibrahim Serdar Acikgoz Date: Wed, 2 Apr 2025 13:39:28 +0200 Subject: [PATCH] [MM-63428] add access control policy store (#30597) --- server/channels/db/migrations/migrations.list | 4 + ...34_create_access_control_policies.down.sql | 2 + ...0134_create_access_control_policies.up.sql | 23 + ...34_create_access_control_policies.down.sql | 2 + ...0134_create_access_control_policies.up.sql | 23 + .../channels/store/layer_generators/main.go | 4 +- .../channels/store/retrylayer/retrylayer.go | 116 +++++ .../store/retrylayer/retrylayer_test.go | 1 + .../sqlstore/access_control_policy_store.go | 421 ++++++++++++++++++ .../access_control_policy_store_test.go | 14 + server/channels/store/sqlstore/store.go | 6 + server/channels/store/store.go | 17 + .../storetest/access_control_policy_store.go | 329 ++++++++++++++ .../mocks/AccessControlPolicyStore.go | 170 +++++++ .../channels/store/storetest/mocks/Store.go | 20 + server/channels/store/storetest/store.go | 5 + .../channels/store/timerlayer/timerlayer.go | 91 ++++ server/go.mod | 10 +- server/go.sum | 20 +- 19 files changed, 1261 insertions(+), 17 deletions(-) create mode 100644 server/channels/db/migrations/mysql/000134_create_access_control_policies.down.sql create mode 100644 server/channels/db/migrations/mysql/000134_create_access_control_policies.up.sql create mode 100644 server/channels/db/migrations/postgres/000134_create_access_control_policies.down.sql create mode 100644 server/channels/db/migrations/postgres/000134_create_access_control_policies.up.sql create mode 100644 server/channels/store/sqlstore/access_control_policy_store.go create mode 100644 server/channels/store/sqlstore/access_control_policy_store_test.go create mode 100644 server/channels/store/storetest/access_control_policy_store.go create mode 100644 server/channels/store/storetest/mocks/AccessControlPolicyStore.go diff --git a/server/channels/db/migrations/migrations.list b/server/channels/db/migrations/migrations.list index 7e342bcded..60a6bc31b4 100644 --- a/server/channels/db/migrations/migrations.list +++ b/server/channels/db/migrations/migrations.list @@ -263,6 +263,8 @@ channels/db/migrations/mysql/000132_create_index_pagination_on_property_fields.d channels/db/migrations/mysql/000132_create_index_pagination_on_property_fields.up.sql channels/db/migrations/mysql/000133_add_channel_banner_fields.down.sql channels/db/migrations/mysql/000133_add_channel_banner_fields.up.sql +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/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 @@ -527,3 +529,5 @@ channels/db/migrations/postgres/000132_create_index_pagination_on_property_field channels/db/migrations/postgres/000132_create_index_pagination_on_property_fields.up.sql channels/db/migrations/postgres/000133_add_channel_banner_fields.down.sql channels/db/migrations/postgres/000133_add_channel_banner_fields.up.sql +channels/db/migrations/postgres/000134_create_access_control_policies.down.sql +channels/db/migrations/postgres/000134_create_access_control_policies.up.sql diff --git a/server/channels/db/migrations/mysql/000134_create_access_control_policies.down.sql b/server/channels/db/migrations/mysql/000134_create_access_control_policies.down.sql new file mode 100644 index 0000000000..58f450d680 --- /dev/null +++ b/server/channels/db/migrations/mysql/000134_create_access_control_policies.down.sql @@ -0,0 +1,2 @@ +DROP TABLE IF EXISTS AccessControlPolicyHistory; +DROP TABLE IF EXISTS AccessControlPolicies; diff --git a/server/channels/db/migrations/mysql/000134_create_access_control_policies.up.sql b/server/channels/db/migrations/mysql/000134_create_access_control_policies.up.sql new file mode 100644 index 0000000000..87d7e32809 --- /dev/null +++ b/server/channels/db/migrations/mysql/000134_create_access_control_policies.up.sql @@ -0,0 +1,23 @@ +CREATE TABLE IF NOT EXISTS AccessControlPolicies ( + ID varchar(26) PRIMARY KEY, + Name varchar(128) NOT NULL, + Type varchar(128) NOT NULL, + Active bool NOT NULL, + CreateAt bigint(20) NOT NULL, + Revision int NOT NULL, + Version varchar(8) NOT NULL, + Data json, + Props json +); + +CREATE TABLE IF NOT EXISTS AccessControlPolicyHistory ( + ID varchar(26) NOT NULL, + Name varchar(128) NOT NULL, + Type varchar(128) NOT NULL, + CreateAt bigint(20) NOT NULL, + Revision int NOT NULL, + Version varchar(8) NOT NULL, + Data json, + Props json, + PRIMARY KEY (ID, Revision) +); diff --git a/server/channels/db/migrations/postgres/000134_create_access_control_policies.down.sql b/server/channels/db/migrations/postgres/000134_create_access_control_policies.down.sql new file mode 100644 index 0000000000..58f450d680 --- /dev/null +++ b/server/channels/db/migrations/postgres/000134_create_access_control_policies.down.sql @@ -0,0 +1,2 @@ +DROP TABLE IF EXISTS AccessControlPolicyHistory; +DROP TABLE IF EXISTS AccessControlPolicies; diff --git a/server/channels/db/migrations/postgres/000134_create_access_control_policies.up.sql b/server/channels/db/migrations/postgres/000134_create_access_control_policies.up.sql new file mode 100644 index 0000000000..efa435e029 --- /dev/null +++ b/server/channels/db/migrations/postgres/000134_create_access_control_policies.up.sql @@ -0,0 +1,23 @@ +CREATE TABLE IF NOT EXISTS AccessControlPolicies ( + ID varchar(26) PRIMARY KEY, + Name varchar(128) NOT NULL, + Type varchar(128) NOT NULL, + Active bool NOT NULL, + CreateAt bigint NOT NULL, + Revision int NOT NULL, + Version varchar(8) NOT NULL, + Data jsonb, + Props jsonb +); + +CREATE TABLE IF NOT EXISTS AccessControlPolicyHistory ( + ID varchar(26) NOT NULL, + Name varchar(128) NOT NULL, + Type varchar(128) NOT NULL, + CreateAt bigint NOT NULL, + Revision int NOT NULL, + Version varchar(8) NOT NULL, + Data jsonb, + Props jsonb, + PRIMARY KEY (ID, Revision) +); diff --git a/server/channels/store/layer_generators/main.go b/server/channels/store/layer_generators/main.go index f854efb6f4..93963b1547 100644 --- a/server/channels/store/layer_generators/main.go +++ b/server/channels/store/layer_generators/main.go @@ -250,7 +250,7 @@ func generateLayer(name, templateFile string) ([]byte, error) { paramsWithType := []string{} for _, param := range params { switch param.Type { - case "ChannelSearchOpts", "UserGetByIdsOpts", "ThreadMembershipOpts": + case "ChannelSearchOpts", "UserGetByIdsOpts", "ThreadMembershipOpts", "GetPolicyOptions": paramsWithType = append(paramsWithType, fmt.Sprintf("%s store.%s", param.Name, param.Type)) case "*UserGetByIdsOpts", "*SidebarCategorySearchOpts": paramsWithType = append(paramsWithType, fmt.Sprintf("%s *store.%s", param.Name, strings.TrimPrefix(param.Type, "*"))) @@ -264,7 +264,7 @@ func generateLayer(name, templateFile string) ([]byte, error) { paramsWithType := []string{} for _, param := range params { switch param.Type { - case "ChannelSearchOpts", "UserGetByIdsOpts", "ThreadMembershipOpts": + case "ChannelSearchOpts", "UserGetByIdsOpts", "ThreadMembershipOpts", "GetPolicyOptions": paramsWithType = append(paramsWithType, fmt.Sprintf("%s store.%s", param.Name, param.Type)) case "*UserGetByIdsOpts", "*SidebarCategorySearchOpts": paramsWithType = append(paramsWithType, fmt.Sprintf("%s *store.%s", param.Name, strings.TrimPrefix(param.Type, "*"))) diff --git a/server/channels/store/retrylayer/retrylayer.go b/server/channels/store/retrylayer/retrylayer.go index c56b12c3e1..a93fbd8ac1 100644 --- a/server/channels/store/retrylayer/retrylayer.go +++ b/server/channels/store/retrylayer/retrylayer.go @@ -23,6 +23,7 @@ const mySQLDeadlockCode = uint16(1213) type RetryLayer struct { store.Store + AccessControlPolicyStore store.AccessControlPolicyStore AuditStore store.AuditStore BotStore store.BotStore ChannelStore store.ChannelStore @@ -74,6 +75,10 @@ type RetryLayer struct { WebhookStore store.WebhookStore } +func (s *RetryLayer) AccessControlPolicy() store.AccessControlPolicyStore { + return s.AccessControlPolicyStore +} + func (s *RetryLayer) Audit() store.AuditStore { return s.AuditStore } @@ -270,6 +275,11 @@ func (s *RetryLayer) Webhook() store.WebhookStore { return s.WebhookStore } +type RetryLayerAccessControlPolicyStore struct { + store.AccessControlPolicyStore + Root *RetryLayer +} + type RetryLayerAuditStore struct { store.AuditStore Root *RetryLayer @@ -531,6 +541,111 @@ func isRepeatableError(err error) bool { return false } +func (s *RetryLayerAccessControlPolicyStore) Delete(c request.CTX, id string) error { + + tries := 0 + for { + err := s.AccessControlPolicyStore.Delete(c, id) + 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 *RetryLayerAccessControlPolicyStore) Get(c request.CTX, id string) (*model.AccessControlPolicy, error) { + + tries := 0 + for { + result, err := s.AccessControlPolicyStore.Get(c, id) + 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) 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 + for { + result, err := s.AccessControlPolicyStore.Save(c, policy) + 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) SetActiveStatus(c request.CTX, id string, active bool) (*model.AccessControlPolicy, error) { + + tries := 0 + for { + result, err := s.AccessControlPolicyStore.SetActiveStatus(c, id, active) + 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 *RetryLayerAuditStore) Get(userID string, offset int, limit int) (model.Audits, error) { tries := 0 @@ -16334,6 +16449,7 @@ func New(childStore store.Store) *RetryLayer { Store: childStore, } + newStore.AccessControlPolicyStore = &RetryLayerAccessControlPolicyStore{AccessControlPolicyStore: childStore.AccessControlPolicy(), 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} diff --git a/server/channels/store/retrylayer/retrylayer_test.go b/server/channels/store/retrylayer/retrylayer_test.go index 7e4a319485..f89777623c 100644 --- a/server/channels/store/retrylayer/retrylayer_test.go +++ b/server/channels/store/retrylayer/retrylayer_test.go @@ -66,6 +66,7 @@ func genStore() *mocks.Store { mock.On("PropertyField").Return(&mocks.PropertyFieldStore{}) mock.On("PropertyGroup").Return(&mocks.PropertyGroupStore{}) mock.On("PropertyValue").Return(&mocks.PropertyValueStore{}) + mock.On("AccessControlPolicy").Return(&mocks.AccessControlPolicyStore{}) return mock } diff --git a/server/channels/store/sqlstore/access_control_policy_store.go b/server/channels/store/sqlstore/access_control_policy_store.go new file mode 100644 index 0000000000..c07a82052f --- /dev/null +++ b/server/channels/store/sqlstore/access_control_policy_store.go @@ -0,0 +1,421 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package sqlstore + +import ( + "database/sql" + "encoding/json" + "fmt" + + "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" + "github.com/pkg/errors" + + sq "github.com/mattermost/squirrel" +) + +// 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 { + Imports []string `json:"imports"` + Rules []model.AccessControlPolicyRule `json:"rules"` +} + +// These are the fields that meant to be unchanged with the policy versions. +type storeAccessControlPolicy struct { + ID string + Name string + Type string + Active bool + CreateAt int64 + Revision int + Version string + Data []byte + Props []byte +} + +// This needs to be updated with the new version of the policy. +// with the new name as this only supports v0.1 +func (s *storeAccessControlPolicy) toModel() (*model.AccessControlPolicy, error) { + policy := &model.AccessControlPolicy{ + ID: s.ID, + Name: s.Name, + Type: s.Type, + Active: s.Active, + CreateAt: s.CreateAt, + Revision: s.Revision, + Version: s.Version, + } + + var p accessControlPolicyV0_1 + if err := json.Unmarshal(s.Data, &p); err != nil { + return nil, err + } + + policy.Imports = p.Imports + policy.Rules = p.Rules + + if err := json.Unmarshal(s.Props, &policy.Props); err != nil { + return nil, err + } + + return policy, nil +} + +func fromModel(policy *model.AccessControlPolicy) (*storeAccessControlPolicy, error) { + data, err := json.Marshal(&accessControlPolicyV0_1{ + Imports: policy.Imports, + Rules: policy.Rules, + }) + if err != nil { + return nil, err + } + + props, err := json.Marshal(policy.Props) + if err != nil { + return nil, err + } + + return &storeAccessControlPolicy{ + ID: policy.ID, + Name: policy.Name, + Type: policy.Type, + Active: policy.Active, + CreateAt: policy.CreateAt, + Revision: policy.Revision, + Version: policy.Version, + Data: data, + Props: props, + }, nil +} + +func accessControlPolicySliceColumns(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 + "ID", + p + "Name", + p + "Type", + p + "Active", + p + "CreateAt", + p + "Revision", + p + "Version", + p + "Data", + p + "Props", + } +} + +func accessControlPolicyHistorySliceColumns(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 + "ID", + p + "Name", + p + "Type", + p + "CreateAt", + p + "Revision", + p + "Version", + p + "Data", + p + "Props", + } +} + +type SqlAccessControlPolicyStore struct { + *SqlStore + metrics einterfaces.MetricsInterface + + selectQueryBuilder sq.SelectBuilder +} + +// newSqlAccessControlPolicyStore creates an instance of AccessControlPolicyStorea. +func newSqlAccessControlPolicyStore(sqlStore *SqlStore, metrics einterfaces.MetricsInterface) store.AccessControlPolicyStore { + s := &SqlAccessControlPolicyStore{ + SqlStore: sqlStore, + metrics: metrics, + } + + s.selectQueryBuilder = s.getQueryBuilder().Select(accessControlPolicySliceColumns()...).From("AccessControlPolicies") + + return s +} + +func preSaveAccessControlPolicy(policy, 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 + // if there is no existing policy, we set the revision to 1 + policy.CreateAt = model.GetMillis() + if existingPolicy != nil { + policy.Revision = existingPolicy.Revision + 1 + } else { + policy.Revision = 1 + } +} + +func (s *SqlAccessControlPolicyStore) Save(rctx request.CTX, policy *model.AccessControlPolicy) (*model.AccessControlPolicy, error) { + if err := policy.IsValid(); err != nil { + return nil, err + } + + tx, err := s.GetMaster().Beginx() + if err != nil { + return nil, errors.Wrap(err, "failed to start transaction") + } + defer finalizeTransactionX(tx, &err) + + existingPolicy, err := s.getT(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) + } + + 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) + } + + data := storePolicy.Data + props := storePolicy.Props + if s.IsBinaryParamEnabled() { + data = AppendBinaryFlag(data) + props = AppendBinaryFlag(props) + } + + query := s.getQueryBuilder(). + Insert("AccessControlPolicies"). + Columns(accessControlPolicySliceColumns()...). + Values(storePolicy.ID, storePolicy.Name, storePolicy.Type, storePolicy.Active, storePolicy.CreateAt, storePolicy.Revision, storePolicy.Version, data, props) + + _, err = tx.ExecBuilder(query) + if err != nil { + return nil, errors.Wrapf(err, "failed to save policy with id=%s", policy.ID) + } + + cp, err := storePolicy.toModel() + if err != nil { + return nil, errors.Wrapf(err, "failed to parse policy with id=%s", policy.ID) + } + + if err = tx.Commit(); err != nil { + return nil, errors.Wrap(err, "commit_transaction") + } + + return cp, nil +} + +func (s *SqlAccessControlPolicyStore) Delete(rctx request.CTX, id string) error { + tx, err := s.GetMaster().Beginx() + if err != nil { + return errors.Wrap(err, "failed to start transaction") + } + defer finalizeTransactionX(tx, &err) + + existingPolicy, err := s.getT(rctx, tx, id) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return errors.Wrapf(err, "failed to fetch policy with id=%s", id) + } + + if existingPolicy != nil { + tmp, err2 := fromModel(existingPolicy) + if err2 != nil { + return errors.Wrapf(err2, "failed to parse policy with id=%s", 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 errors.Wrapf(err, "failed to save policy with id=%s to history", id) + } + + err = s.deleteT(rctx, tx, existingPolicy.ID) + if err != nil { + return errors.Wrapf(err, "failed to delete policy with id=%s", id) + } + } + + if err = tx.Commit(); err != nil { + return errors.Wrap(err, "commit_transaction") + } + + return nil +} + +func (s *SqlAccessControlPolicyStore) deleteT(_ request.CTX, tx *sqlxTxWrapper, id string) error { + query := s.getQueryBuilder().Delete("AccessControlPolicies").Where(sq.Eq{"ID": id}) + _, err := tx.ExecBuilder(query) + if err != nil { + return errors.Wrapf(err, "failed to delete policy with id=%s", id) + } + + return nil +} + +func (s *SqlAccessControlPolicyStore) SetActiveStatus(rctx request.CTX, id string, active bool) (*model.AccessControlPolicy, error) { + tx, err := s.GetMaster().Beginx() + if err != nil { + return nil, errors.Wrap(err, "failed to start transaction") + } + defer finalizeTransactionX(tx, &err) + + existingPolicy, err := s.getT(rctx, tx, id) + if err != nil { + return nil, errors.Wrapf(err, "failed to fetch policy with id=%s", id) + } else if errors.Is(err, sql.ErrNoRows) { + return nil, store.NewErrNotFound("AccessControlPolicy", id) + } + + // also make sure if the policy is valid before updating active status + // just in case + existingPolicy.Active = active + if appErr := existingPolicy.IsValid(); err != nil { + return nil, appErr + } + + query, args, err := s.getQueryBuilder().Update("AccessControlPolicies").Set("Active", active).Where(sq.Eq{"ID": id}).ToSql() + if err != nil { + return nil, errors.Wrapf(err, "failed to build query for policy with id=%s", id) + } + _, err = tx.Query(query, args...) + if err != nil { + return nil, errors.Wrapf(err, "failed to update policy with id=%s", id) + } + + if err = tx.Commit(); err != nil { + return nil, errors.Wrap(err, "commit_transaction") + } + + return existingPolicy, nil +} + +func (s *SqlAccessControlPolicyStore) Get(_ request.CTX, id string) (*model.AccessControlPolicy, error) { + p := storeAccessControlPolicy{} + query := s.selectQueryBuilder.Where(sq.Eq{"ID": id}) + + err := s.GetReplica().GetBuilder(&p, query) + if err != nil { + if err == sql.ErrNoRows { + return nil, store.NewErrNotFound("AccessControlPolicy", id) + } + return nil, errors.Wrapf(err, "failed to find policy with id=%s", id) + } + + policy, err := p.toModel() + if err != nil { + return nil, errors.Wrapf(err, "failed to parse policy with id=%s", id) + } + + return policy, nil +} + +func (s *SqlAccessControlPolicyStore) getT(_ request.CTX, tx *sqlxTxWrapper, id string) (*model.AccessControlPolicy, error) { + query := s.getQueryBuilder(). + Select(accessControlPolicySliceColumns()...). + From("AccessControlPolicies"). + Where( + sq.Eq{"ID": id}, + ) + + 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 store.GetPolicyOptions) ([]*model.AccessControlPolicy, error) { + p := []storeAccessControlPolicy{} + query := s.selectQueryBuilder + + if opts.ParentID != "" { + if s.DriverName() == model.DatabaseDriverPostgres { + query = query.Where(sq.Expr("Data->'imports' @> ?", fmt.Sprintf("%q", opts.ParentID))) + } else { + query = query.Where(sq.Expr("JSON_CONTAINS(JSON_EXTRACT(Data, '$.imports'), ?)", fmt.Sprintf("%q", opts.ParentID))) + } + } + + if opts.Type != "" { + query = query.Where(sq.Eq{"Type": opts.Type}) + } + + 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) + } + + 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 policies, nil +} diff --git a/server/channels/store/sqlstore/access_control_policy_store_test.go b/server/channels/store/sqlstore/access_control_policy_store_test.go new file mode 100644 index 0000000000..1889859e0c --- /dev/null +++ b/server/channels/store/sqlstore/access_control_policy_store_test.go @@ -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 TestAccessControlPolicyStore(t *testing.T) { + StoreTestWithSqlStore(t, storetest.TestAccessControlPolicyStore) +} diff --git a/server/channels/store/sqlstore/store.go b/server/channels/store/sqlstore/store.go index fc2e547f9d..ac176a111c 100644 --- a/server/channels/store/sqlstore/store.go +++ b/server/channels/store/sqlstore/store.go @@ -118,6 +118,7 @@ type SqlStoreStores struct { propertyGroup store.PropertyGroupStore propertyField store.PropertyFieldStore propertyValue store.PropertyValueStore + accessControlPolicy store.AccessControlPolicyStore } type SqlStore struct { @@ -263,6 +264,7 @@ func New(settings model.SqlSettings, logger mlog.LoggerIFace, metrics einterface store.stores.propertyGroup = newPropertyGroupStore(store) store.stores.propertyField = newPropertyFieldStore(store) store.stores.propertyValue = newPropertyValueStore(store) + store.stores.accessControlPolicy = newSqlAccessControlPolicyStore(store, metrics) store.stores.preference.(*SqlPreferenceStore).deleteUnusedFeatures() @@ -1079,6 +1081,10 @@ func (ss *SqlStore) PropertyValue() store.PropertyValueStore { return ss.stores.propertyValue } +func (ss *SqlStore) AccessControlPolicy() store.AccessControlPolicyStore { + return ss.stores.accessControlPolicy +} + func (ss *SqlStore) DropAllTables() { if ss.DriverName() == model.DatabaseDriverPostgres { ss.masterX.Exec(`DO diff --git a/server/channels/store/store.go b/server/channels/store/store.go index 8eb230c4b5..0a329febb2 100644 --- a/server/channels/store/store.go +++ b/server/channels/store/store.go @@ -95,6 +95,7 @@ type Store interface { PropertyGroup() PropertyGroupStore PropertyField() PropertyFieldStore PropertyValue() PropertyValueStore + AccessControlPolicy() AccessControlPolicyStore } type RetentionPolicyStore interface { @@ -1107,6 +1108,14 @@ type PropertyValueStore interface { DeleteForField(id string) error } +type AccessControlPolicyStore interface { + Save(c request.CTX, policy *model.AccessControlPolicy) (*model.AccessControlPolicy, error) + 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) +} + // ChannelSearchOpts contains options for searching channels. // // NotAssociatedToGroup will exclude channels that have associated, active GroupChannels records. @@ -1199,3 +1208,11 @@ 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 +} diff --git a/server/channels/store/storetest/access_control_policy_store.go b/server/channels/store/storetest/access_control_policy_store.go new file mode 100644 index 0000000000..3116c71d37 --- /dev/null +++ b/server/channels/store/storetest/access_control_policy_store.go @@ -0,0 +1,329 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package storetest + +import ( + "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" +) + +func TestAccessControlPolicyStore(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore) { + t.Run("Save", func(t *testing.T) { testAccessControlPolicyStoreSaveAndGet(t, rctx, ss) }) + t.Run("Delete", func(t *testing.T) { testAccessControlPolicyStoreDelete(t, rctx, ss) }) + t.Run("SetActive", func(t *testing.T) { testAccessControlPolicyStoreSetActive(t, rctx, ss) }) + t.Run("GetAll", func(t *testing.T) { testAccessControlPolicyStoreGetAll(t, rctx, ss) }) +} + +func testAccessControlPolicyStoreSaveAndGet(t *testing.T, rctx request.CTX, ss store.Store) { + t.Run("Save parent policy", func(t *testing.T) { + policy := &model.AccessControlPolicy{ + ID: model.NewId(), + 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\"", + }, + }, + } + + policy, err := ss.AccessControlPolicy().Save(rctx, policy) + require.NoError(t, err) + require.NotNil(t, policy) + + t.Cleanup(func() { + err := ss.AccessControlPolicy().Delete(rctx, policy.ID) + require.NoError(t, err) + }) + }) + + t.Run("Save resource policy", func(t *testing.T) { + parent1 := model.NewId() + + policy := &model.AccessControlPolicy{ + ID: model.NewId(), + Name: "Name", + Type: model.AccessControlPolicyTypeChannel, + Active: true, + Revision: 1, + Version: model.AccessControlPolicyVersionV0_1, + Imports: []string{parent1}, + Rules: []model.AccessControlPolicyRule{ + { + Actions: []string{"action"}, + Expression: "policies." + parent1 + " == true", + }, + }, + } + + policy, err := ss.AccessControlPolicy().Save(rctx, policy) + require.NoError(t, err) + require.NotNil(t, policy) + + t.Cleanup(func() { + err := ss.AccessControlPolicy().Delete(rctx, policy.ID) + require.NoError(t, err) + }) + }) + + t.Run("update resource policy", func(t *testing.T) { + policyID := model.NewId() + + policy := &model.AccessControlPolicy{ + ID: policyID, + Name: "Name", + Type: model.AccessControlPolicyTypeChannel, + Active: true, + Revision: 1, + Version: model.AccessControlPolicyVersionV0_1, + Imports: []string{}, + Rules: []model.AccessControlPolicyRule{ + { + Actions: []string{"action"}, + Expression: "user.properties.program == \"engineering\"", + }, + }, + } + + policy, err := ss.AccessControlPolicy().Save(rctx, policy) + require.NoError(t, err) + require.NotNil(t, policy) + + policy.Rules = []model.AccessControlPolicyRule{ + { + Actions: []string{"action"}, + Expression: "user.properties.program == \"engineering\" || user.properties.department == \"engineering\"", + }, + } + + policy, err = ss.AccessControlPolicy().Save(rctx, policy) + require.NoError(t, err) + require.NotNil(t, policy) + + policy, err = ss.AccessControlPolicy().Get(rctx, policyID) + require.NoError(t, err) + require.NotNil(t, policy) + require.Equal(t, 2, policy.Revision) + + t.Cleanup(func() { + err := ss.AccessControlPolicy().Delete(rctx, policy.ID) + require.NoError(t, err) + }) + }) + + t.Run("Get non-existent policy", func(t *testing.T) { + id := model.NewId() + policy, err := ss.AccessControlPolicy().Get(rctx, id) + require.EqualError(t, err, store.NewErrNotFound("AccessControlPolicy", id).Error()) + require.Nil(t, policy) + }) +} + +func testAccessControlPolicyStoreDelete(t *testing.T, rctx request.CTX, ss store.Store) { + t.Run("Delete parent policy", func(t *testing.T) { + policy := &model.AccessControlPolicy{ + ID: model.NewId(), + 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\"", + }, + }, + } + + policy, err := ss.AccessControlPolicy().Save(rctx, policy) + require.NoError(t, err) + require.NotNil(t, policy) + + err = ss.AccessControlPolicy().Delete(rctx, policy.ID) + require.NoError(t, err) + + id := policy.ID + policy, err = ss.AccessControlPolicy().Get(rctx, policy.ID) + require.EqualError(t, err, store.NewErrNotFound("AccessControlPolicy", id).Error()) + require.Nil(t, policy) + }) + + t.Run("Delete resource policy", func(t *testing.T) { + parent1 := model.NewId() + + policy := &model.AccessControlPolicy{ + ID: model.NewId(), + Name: "Name", + Type: model.AccessControlPolicyTypeChannel, + Active: true, + Revision: 1, + Version: model.AccessControlPolicyVersionV0_1, + Imports: []string{parent1}, + Rules: []model.AccessControlPolicyRule{ + { + Actions: []string{"action"}, + Expression: "policies." + parent1 + " == true", + }, + }, + } + + policy, err := ss.AccessControlPolicy().Save(rctx, policy) + require.NoError(t, err) + require.NotNil(t, policy) + + err = ss.AccessControlPolicy().Delete(rctx, policy.ID) + require.NoError(t, err) + + id := policy.ID + policy, err = ss.AccessControlPolicy().Get(rctx, policy.ID) + require.EqualError(t, err, store.NewErrNotFound("AccessControlPolicy", id).Error()) + require.Nil(t, policy) + }) + + t.Run("Delete non-existent policy", func(t *testing.T) { + err := ss.AccessControlPolicy().Delete(rctx, model.NewId()) + require.NoError(t, err) + }) +} + +func testAccessControlPolicyStoreSetActive(t *testing.T, rctx request.CTX, ss store.Store) { + t.Run("Save policy", func(t *testing.T) { + id := model.NewId() + policy := &model.AccessControlPolicy{ + ID: id, + Name: "Name", + Type: model.AccessControlPolicyTypeChannel, + Active: false, + Revision: 1, + Version: model.AccessControlPolicyVersionV0_1, + Imports: []string{}, + Rules: []model.AccessControlPolicyRule{ + { + Actions: []string{"action"}, + Expression: "user.properties.program == \"engineering\"", + }, + }, + } + + policy, err := ss.AccessControlPolicy().Save(rctx, policy) + require.NoError(t, err) + require.NotNil(t, policy) + + t.Cleanup(func() { + err = ss.AccessControlPolicy().Delete(rctx, id) + require.NoError(t, err) + }) + + policy, err = ss.AccessControlPolicy().Get(rctx, policy.ID) + require.NoError(t, err) + require.NotNil(t, policy) + require.False(t, policy.Active) + + policy, err = ss.AccessControlPolicy().SetActiveStatus(rctx, policy.ID, true) + require.NoError(t, err) + require.NotNil(t, policy) + require.True(t, policy.Active) + + policy, err = ss.AccessControlPolicy().Get(rctx, policy.ID) + require.NoError(t, err) + require.NotNil(t, policy) + require.True(t, policy.Active) + }) +} + +func testAccessControlPolicyStoreGetAll(t *testing.T, rctx request.CTX, ss store.Store) { + id := model.NewId() + parentPolicy := &model.AccessControlPolicy{ + ID: id, + 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) + }) + + parentPolicy, err := ss.AccessControlPolicy().Save(rctx, parentPolicy) + require.NoError(t, err) + require.NotNil(t, parentPolicy) + + id2 := model.NewId() + resourcePolicy := &model.AccessControlPolicy{ + ID: id2, + Name: "Name", + Type: model.AccessControlPolicyTypeChannel, + Active: true, + Revision: 1, + Version: model.AccessControlPolicyVersionV0_1, + Imports: []string{parentPolicy.ID}, + Rules: []model.AccessControlPolicyRule{ + { + Actions: []string{"action"}, + Expression: "policies." + parentPolicy.ID + " == true", + }, + }, + } + t.Cleanup(func() { + err = ss.AccessControlPolicy().Delete(rctx, id2) + require.NoError(t, err) + }) + + 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{}) + require.NoError(t, err) + require.NotNil(t, policies) + require.Len(t, policies, 2) + }) + + t.Run("GetAll by type", func(t *testing.T) { + policies, err := ss.AccessControlPolicy().GetAll(rctx, store.GetPolicyOptions{Type: model.AccessControlPolicyTypeParent}) + require.NoError(t, err) + require.NotNil(t, policies) + require.Len(t, policies, 1) + require.Equal(t, parentPolicy.ID, policies[0].ID) + + policies, err = ss.AccessControlPolicy().GetAll(rctx, store.GetPolicyOptions{Type: model.AccessControlPolicyTypeChannel}) + require.NoError(t, err) + require.NotNil(t, policies) + require.Len(t, policies, 1) + require.Equal(t, resourcePolicy.ID, policies[0].ID) + }) + + t.Run("GetAll by parent", func(t *testing.T) { + policies, err := ss.AccessControlPolicy().GetAll(rctx, store.GetPolicyOptions{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()}) + require.NoError(t, err) + require.NotNil(t, policies) + require.Len(t, policies, 0) + }) +} diff --git a/server/channels/store/storetest/mocks/AccessControlPolicyStore.go b/server/channels/store/storetest/mocks/AccessControlPolicyStore.go new file mode 100644 index 0000000000..9413180636 --- /dev/null +++ b/server/channels/store/storetest/mocks/AccessControlPolicyStore.go @@ -0,0 +1,170 @@ +// 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" + + store "github.com/mattermost/mattermost/server/v8/channels/store" +) + +// AccessControlPolicyStore is an autogenerated mock type for the AccessControlPolicyStore type +type AccessControlPolicyStore struct { + mock.Mock +} + +// Delete provides a mock function with given fields: c, id +func (_m *AccessControlPolicyStore) Delete(c request.CTX, id string) error { + ret := _m.Called(c, id) + + if len(ret) == 0 { + panic("no return value specified for Delete") + } + + var r0 error + if rf, ok := ret.Get(0).(func(request.CTX, string) error); ok { + r0 = rf(c, id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Get provides a mock function with given fields: c, id +func (_m *AccessControlPolicyStore) Get(c request.CTX, id string) (*model.AccessControlPolicy, error) { + ret := _m.Called(c, id) + + if len(ret) == 0 { + panic("no return value specified for Get") + } + + var r0 *model.AccessControlPolicy + var r1 error + if rf, ok := ret.Get(0).(func(request.CTX, string) (*model.AccessControlPolicy, error)); ok { + return rf(c, id) + } + if rf, ok := ret.Get(0).(func(request.CTX, string) *model.AccessControlPolicy); ok { + r0 = rf(c, id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.AccessControlPolicy) + } + } + + if rf, ok := ret.Get(1).(func(request.CTX, string) error); ok { + r1 = rf(c, id) + } else { + r1 = ret.Error(1) + } + + 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) + + if len(ret) == 0 { + panic("no return value specified for Save") + } + + var r0 *model.AccessControlPolicy + var r1 error + if rf, ok := ret.Get(0).(func(request.CTX, *model.AccessControlPolicy) (*model.AccessControlPolicy, error)); ok { + return rf(c, policy) + } + if rf, ok := ret.Get(0).(func(request.CTX, *model.AccessControlPolicy) *model.AccessControlPolicy); ok { + r0 = rf(c, policy) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.AccessControlPolicy) + } + } + + if rf, ok := ret.Get(1).(func(request.CTX, *model.AccessControlPolicy) error); ok { + r1 = rf(c, policy) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// 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) + + if len(ret) == 0 { + panic("no return value specified for SetActiveStatus") + } + + var r0 *model.AccessControlPolicy + var r1 error + if rf, ok := ret.Get(0).(func(request.CTX, string, bool) (*model.AccessControlPolicy, error)); ok { + return rf(c, id, active) + } + if rf, ok := ret.Get(0).(func(request.CTX, string, bool) *model.AccessControlPolicy); ok { + r0 = rf(c, id, active) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.AccessControlPolicy) + } + } + + if rf, ok := ret.Get(1).(func(request.CTX, string, bool) error); ok { + r1 = rf(c, id, active) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewAccessControlPolicyStore creates a new instance of AccessControlPolicyStore. 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 NewAccessControlPolicyStore(t interface { + mock.TestingT + Cleanup(func()) +}) *AccessControlPolicyStore { + mock := &AccessControlPolicyStore{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/server/channels/store/storetest/mocks/Store.go b/server/channels/store/storetest/mocks/Store.go index 370d7e362f..0b27378b29 100644 --- a/server/channels/store/storetest/mocks/Store.go +++ b/server/channels/store/storetest/mocks/Store.go @@ -24,6 +24,26 @@ type Store struct { mock.Mock } +// AccessControlPolicy provides a mock function with given fields: +func (_m *Store) AccessControlPolicy() store.AccessControlPolicyStore { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for AccessControlPolicy") + } + + var r0 store.AccessControlPolicyStore + if rf, ok := ret.Get(0).(func() store.AccessControlPolicyStore); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(store.AccessControlPolicyStore) + } + } + + return r0 +} + // Audit provides a mock function with given fields: func (_m *Store) Audit() store.AuditStore { ret := _m.Called() diff --git a/server/channels/store/storetest/store.go b/server/channels/store/storetest/store.go index ca744d34ac..7e2f9757bb 100644 --- a/server/channels/store/storetest/store.go +++ b/server/channels/store/storetest/store.go @@ -69,6 +69,7 @@ type Store struct { PropertyGroupStore mocks.PropertyGroupStore PropertyFieldStore mocks.PropertyFieldStore PropertyValueStore mocks.PropertyValueStore + AccessControlPolicyStore mocks.AccessControlPolicyStore } func (s *Store) SetContext(context context.Context) { s.context = context } @@ -154,6 +155,9 @@ func (s *Store) CheckIntegrity() <-chan model.IntegrityCheckResult { } func (s *Store) ReplicaLagAbs() error { return nil } func (s *Store) ReplicaLagTime() error { return nil } +func (s *Store) AccessControlPolicy() store.AccessControlPolicyStore { + return &s.AccessControlPolicyStore +} func (s *Store) AssertExpectations(t mock.TestingT) bool { return mock.AssertExpectationsForObjects(t, @@ -197,5 +201,6 @@ func (s *Store) AssertExpectations(t mock.TestingT) bool { &s.DesktopTokensStore, &s.ChannelBookmarkStore, &s.ScheduledPostStore, + &s.AccessControlPolicyStore, ) } diff --git a/server/channels/store/timerlayer/timerlayer.go b/server/channels/store/timerlayer/timerlayer.go index 66fe13b2c7..50cc2e05ec 100644 --- a/server/channels/store/timerlayer/timerlayer.go +++ b/server/channels/store/timerlayer/timerlayer.go @@ -19,6 +19,7 @@ import ( type TimerLayer struct { store.Store Metrics einterfaces.MetricsInterface + AccessControlPolicyStore store.AccessControlPolicyStore AuditStore store.AuditStore BotStore store.BotStore ChannelStore store.ChannelStore @@ -70,6 +71,10 @@ type TimerLayer struct { WebhookStore store.WebhookStore } +func (s *TimerLayer) AccessControlPolicy() store.AccessControlPolicyStore { + return s.AccessControlPolicyStore +} + func (s *TimerLayer) Audit() store.AuditStore { return s.AuditStore } @@ -266,6 +271,11 @@ func (s *TimerLayer) Webhook() store.WebhookStore { return s.WebhookStore } +type TimerLayerAccessControlPolicyStore struct { + store.AccessControlPolicyStore + Root *TimerLayer +} + type TimerLayerAuditStore struct { store.AuditStore Root *TimerLayer @@ -511,6 +521,86 @@ type TimerLayerWebhookStore struct { Root *TimerLayer } +func (s *TimerLayerAccessControlPolicyStore) Delete(c request.CTX, id string) error { + start := time.Now() + + err := s.AccessControlPolicyStore.Delete(c, id) + + 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.Delete", success, elapsed) + } + return err +} + +func (s *TimerLayerAccessControlPolicyStore) Get(c request.CTX, id string) (*model.AccessControlPolicy, error) { + start := time.Now() + + result, err := s.AccessControlPolicyStore.Get(c, id) + + 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.Get", success, elapsed) + } + 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() + + result, err := s.AccessControlPolicyStore.Save(c, policy) + + 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.Save", success, elapsed) + } + return result, err +} + +func (s *TimerLayerAccessControlPolicyStore) SetActiveStatus(c request.CTX, id string, active bool) (*model.AccessControlPolicy, error) { + start := time.Now() + + result, err := s.AccessControlPolicyStore.SetActiveStatus(c, id, active) + + 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.SetActiveStatus", success, elapsed) + } + return result, err +} + func (s *TimerLayerAuditStore) Get(userID string, offset int, limit int) (model.Audits, error) { start := time.Now() @@ -12882,6 +12972,7 @@ func New(childStore store.Store, metrics einterfaces.MetricsInterface) *TimerLay Metrics: metrics, } + newStore.AccessControlPolicyStore = &TimerLayerAccessControlPolicyStore{AccessControlPolicyStore: childStore.AccessControlPolicy(), 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} diff --git a/server/go.mod b/server/go.mod index a522f088ac..aae68e00a1 100644 --- a/server/go.mod +++ b/server/go.mod @@ -44,7 +44,7 @@ require ( github.com/mattermost/gosaml2 v0.8.0 github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956 github.com/mattermost/logr/v2 v2.0.21 - github.com/mattermost/mattermost/server/public v0.1.9 + github.com/mattermost/mattermost/server/public v0.1.11 github.com/mattermost/morph v1.1.0 github.com/mattermost/rsc v0.0.0-20160330161541-bbaefb05eaa0 github.com/mattermost/squirrel v0.4.0 @@ -72,11 +72,11 @@ require ( github.com/wiggin77/merror v1.0.5 github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c github.com/yuin/goldmark v1.7.8 - golang.org/x/crypto v0.32.0 + golang.org/x/crypto v0.35.0 golang.org/x/image v0.23.0 - golang.org/x/net v0.34.0 + golang.org/x/net v0.36.0 golang.org/x/sync v0.12.0 - golang.org/x/term v0.28.0 + golang.org/x/term v0.29.0 gopkg.in/mail.v2 v2.3.1 gopkg.in/yaml.v3 v3.0.1 ) @@ -221,7 +221,7 @@ require ( go.uber.org/multierr v1.11.0 // indirect golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect golang.org/x/mod v0.22.0 // indirect - golang.org/x/sys v0.29.0 // indirect + golang.org/x/sys v0.30.0 // indirect golang.org/x/text v0.23.0 // indirect golang.org/x/tools v0.29.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250124145028-65684f501c47 // indirect diff --git a/server/go.sum b/server/go.sum index 933697bc2f..951325cbb8 100644 --- a/server/go.sum +++ b/server/go.sum @@ -378,8 +378,8 @@ github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956 h1:Y1Tu/swM31pVwwb github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956/go.mod h1:SRl30Lb7/QoYyohYeVBuqYvvmXSZJxZgiV3Zf6VbxjI= github.com/mattermost/logr/v2 v2.0.21 h1:CMHsP+nrbRlEC4g7BwOk1GAnMtHkniFhlSQPXy52be4= github.com/mattermost/logr/v2 v2.0.21/go.mod h1:kZkB/zqKL9e+RY5gB3vGpsyenC+TpuiOenjMkvJJbzc= -github.com/mattermost/mattermost/server/public v0.1.9 h1:l/OKPRVuFeqL0yqRVC/JpveG5sLNKcT9llxqMkO9e+s= -github.com/mattermost/mattermost/server/public v0.1.9/go.mod h1:SkTKbMul91Rq0v2dIxe8mqzUOY+3KwlwwLmAlxDfGCk= +github.com/mattermost/mattermost/server/public v0.1.11 h1:qxn36BE1rk5lTiMrHCVjdEdeUcbkOy/fxFvlRVyK0rI= +github.com/mattermost/mattermost/server/public v0.1.11/go.mod h1:h/rage94cF+ZWqDdVRaROXFUEPVyO3Wbq8tfKRPRL0s= github.com/mattermost/morph v1.1.0 h1:Q9vrJbeM3s2jfweGheq12EFIzdNp9a/6IovcbvOQ6Cw= github.com/mattermost/morph v1.1.0/go.mod h1:gD+EaqX2UMyyuzmF4PFh4r33XneQ8Nzi+0E8nXjMa3A= github.com/mattermost/rsc v0.0.0-20160330161541-bbaefb05eaa0 h1:G9tL6JXRBMzjuD1kkBtcnd42kUiT6QDwxfFYu7adM6o= @@ -696,8 +696,8 @@ golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= -golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= +golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA= golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= @@ -740,8 +740,8 @@ golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= -golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA= +golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -800,8 +800,8 @@ golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -812,8 +812,8 @@ golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= -golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= -golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= +golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= +golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=