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

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

@@ -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