[MM-63428] add access control policy store (#30597)
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
3eb854c58d
Коммит
10b1f4c5ac
329
server/channels/store/storetest/access_control_policy_store.go
Обычный файл
329
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)
|
||||
})
|
||||
}
|
||||
170
server/channels/store/storetest/mocks/AccessControlPolicyStore.go
Обычный файл
170
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
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user