From e1cd64613591cf5a990442a69ebf188258bd0cb5 Mon Sep 17 00:00:00 2001 From: George Goldberg Date: Tue, 6 Feb 2018 15:34:08 +0000 Subject: XYZ-37: Advanced Permissions Phase 1 Backend. (#8159) * XYZ-13: Update Permission and Role structs to new design. * XYZ-10: Role store. * XYZ-9/XYZ-44: Roles API endpoints and WebSocket message. * XYZ-8: Switch server permissions checks to store backed roles. * XYZ-58: Proper validation of roles where required. * XYZ-11/XYZ-55: Migration to store backed roles from policy config. * XYZ-37: Update unit tests to work with database roles. * XYZ-56: Remove the "guest" role. * Changes to SetDefaultRolesFromConfig. * Short-circuit the store if nothing has changed. * Address first round of review comments. * Address second round of review comments. --- store/storetest/mocks/LayeredStoreDatabaseLayer.go | 108 +++++++++ store/storetest/mocks/LayeredStoreSupplier.go | 92 ++++++++ store/storetest/mocks/RoleStore.go | 78 +++++++ store/storetest/mocks/SqlStore.go | 16 ++ store/storetest/mocks/Store.go | 16 ++ store/storetest/role_store.go | 244 +++++++++++++++++++++ store/storetest/store.go | 3 + 7 files changed, 557 insertions(+) create mode 100644 store/storetest/mocks/RoleStore.go create mode 100644 store/storetest/role_store.go (limited to 'store/storetest') diff --git a/store/storetest/mocks/LayeredStoreDatabaseLayer.go b/store/storetest/mocks/LayeredStoreDatabaseLayer.go index 9c66c4aac..d0162a01e 100644 --- a/store/storetest/mocks/LayeredStoreDatabaseLayer.go +++ b/store/storetest/mocks/LayeredStoreDatabaseLayer.go @@ -416,6 +416,114 @@ func (_m *LayeredStoreDatabaseLayer) ReactionSave(ctx context.Context, reaction return r0 } +// Role provides a mock function with given fields: +func (_m *LayeredStoreDatabaseLayer) Role() store.RoleStore { + ret := _m.Called() + + var r0 store.RoleStore + if rf, ok := ret.Get(0).(func() store.RoleStore); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(store.RoleStore) + } + } + + return r0 +} + +// RoleGet provides a mock function with given fields: ctx, roleId, hints +func (_m *LayeredStoreDatabaseLayer) RoleGet(ctx context.Context, roleId string, hints ...store.LayeredStoreHint) *store.LayeredStoreSupplierResult { + _va := make([]interface{}, len(hints)) + for _i := range hints { + _va[_i] = hints[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, roleId) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *store.LayeredStoreSupplierResult + if rf, ok := ret.Get(0).(func(context.Context, string, ...store.LayeredStoreHint) *store.LayeredStoreSupplierResult); ok { + r0 = rf(ctx, roleId, hints...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*store.LayeredStoreSupplierResult) + } + } + + return r0 +} + +// RoleGetByName provides a mock function with given fields: ctx, name, hints +func (_m *LayeredStoreDatabaseLayer) RoleGetByName(ctx context.Context, name string, hints ...store.LayeredStoreHint) *store.LayeredStoreSupplierResult { + _va := make([]interface{}, len(hints)) + for _i := range hints { + _va[_i] = hints[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, name) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *store.LayeredStoreSupplierResult + if rf, ok := ret.Get(0).(func(context.Context, string, ...store.LayeredStoreHint) *store.LayeredStoreSupplierResult); ok { + r0 = rf(ctx, name, hints...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*store.LayeredStoreSupplierResult) + } + } + + return r0 +} + +// RoleGetByNames provides a mock function with given fields: ctx, names, hints +func (_m *LayeredStoreDatabaseLayer) RoleGetByNames(ctx context.Context, names []string, hints ...store.LayeredStoreHint) *store.LayeredStoreSupplierResult { + _va := make([]interface{}, len(hints)) + for _i := range hints { + _va[_i] = hints[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, names) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *store.LayeredStoreSupplierResult + if rf, ok := ret.Get(0).(func(context.Context, []string, ...store.LayeredStoreHint) *store.LayeredStoreSupplierResult); ok { + r0 = rf(ctx, names, hints...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*store.LayeredStoreSupplierResult) + } + } + + return r0 +} + +// RoleSave provides a mock function with given fields: ctx, role, hints +func (_m *LayeredStoreDatabaseLayer) RoleSave(ctx context.Context, role *model.Role, hints ...store.LayeredStoreHint) *store.LayeredStoreSupplierResult { + _va := make([]interface{}, len(hints)) + for _i := range hints { + _va[_i] = hints[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, role) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *store.LayeredStoreSupplierResult + if rf, ok := ret.Get(0).(func(context.Context, *model.Role, ...store.LayeredStoreHint) *store.LayeredStoreSupplierResult); ok { + r0 = rf(ctx, role, hints...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*store.LayeredStoreSupplierResult) + } + } + + return r0 +} + // Session provides a mock function with given fields: func (_m *LayeredStoreDatabaseLayer) Session() store.SessionStore { ret := _m.Called() diff --git a/store/storetest/mocks/LayeredStoreSupplier.go b/store/storetest/mocks/LayeredStoreSupplier.go index f4187dae9..59fd31cb8 100644 --- a/store/storetest/mocks/LayeredStoreSupplier.go +++ b/store/storetest/mocks/LayeredStoreSupplier.go @@ -145,6 +145,98 @@ func (_m *LayeredStoreSupplier) ReactionSave(ctx context.Context, reaction *mode return r0 } +// RoleGet provides a mock function with given fields: ctx, roleId, hints +func (_m *LayeredStoreSupplier) RoleGet(ctx context.Context, roleId string, hints ...store.LayeredStoreHint) *store.LayeredStoreSupplierResult { + _va := make([]interface{}, len(hints)) + for _i := range hints { + _va[_i] = hints[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, roleId) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *store.LayeredStoreSupplierResult + if rf, ok := ret.Get(0).(func(context.Context, string, ...store.LayeredStoreHint) *store.LayeredStoreSupplierResult); ok { + r0 = rf(ctx, roleId, hints...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*store.LayeredStoreSupplierResult) + } + } + + return r0 +} + +// RoleGetByName provides a mock function with given fields: ctx, name, hints +func (_m *LayeredStoreSupplier) RoleGetByName(ctx context.Context, name string, hints ...store.LayeredStoreHint) *store.LayeredStoreSupplierResult { + _va := make([]interface{}, len(hints)) + for _i := range hints { + _va[_i] = hints[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, name) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *store.LayeredStoreSupplierResult + if rf, ok := ret.Get(0).(func(context.Context, string, ...store.LayeredStoreHint) *store.LayeredStoreSupplierResult); ok { + r0 = rf(ctx, name, hints...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*store.LayeredStoreSupplierResult) + } + } + + return r0 +} + +// RoleGetByNames provides a mock function with given fields: ctx, names, hints +func (_m *LayeredStoreSupplier) RoleGetByNames(ctx context.Context, names []string, hints ...store.LayeredStoreHint) *store.LayeredStoreSupplierResult { + _va := make([]interface{}, len(hints)) + for _i := range hints { + _va[_i] = hints[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, names) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *store.LayeredStoreSupplierResult + if rf, ok := ret.Get(0).(func(context.Context, []string, ...store.LayeredStoreHint) *store.LayeredStoreSupplierResult); ok { + r0 = rf(ctx, names, hints...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*store.LayeredStoreSupplierResult) + } + } + + return r0 +} + +// RoleSave provides a mock function with given fields: ctx, role, hints +func (_m *LayeredStoreSupplier) RoleSave(ctx context.Context, role *model.Role, hints ...store.LayeredStoreHint) *store.LayeredStoreSupplierResult { + _va := make([]interface{}, len(hints)) + for _i := range hints { + _va[_i] = hints[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, role) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *store.LayeredStoreSupplierResult + if rf, ok := ret.Get(0).(func(context.Context, *model.Role, ...store.LayeredStoreHint) *store.LayeredStoreSupplierResult); ok { + r0 = rf(ctx, role, hints...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*store.LayeredStoreSupplierResult) + } + } + + return r0 +} + // SetChainNext provides a mock function with given fields: _a0 func (_m *LayeredStoreSupplier) SetChainNext(_a0 store.LayeredStoreSupplier) { _m.Called(_a0) diff --git a/store/storetest/mocks/RoleStore.go b/store/storetest/mocks/RoleStore.go new file mode 100644 index 000000000..8150460ae --- /dev/null +++ b/store/storetest/mocks/RoleStore.go @@ -0,0 +1,78 @@ +// Code generated by mockery v1.0.0 + +// Regenerate this file using `make store-mocks`. + +package mocks + +import mock "github.com/stretchr/testify/mock" +import model "github.com/mattermost/mattermost-server/model" +import store "github.com/mattermost/mattermost-server/store" + +// RoleStore is an autogenerated mock type for the RoleStore type +type RoleStore struct { + mock.Mock +} + +// Get provides a mock function with given fields: roleId +func (_m *RoleStore) Get(roleId string) store.StoreChannel { + ret := _m.Called(roleId) + + var r0 store.StoreChannel + if rf, ok := ret.Get(0).(func(string) store.StoreChannel); ok { + r0 = rf(roleId) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(store.StoreChannel) + } + } + + return r0 +} + +// GetByName provides a mock function with given fields: name +func (_m *RoleStore) GetByName(name string) store.StoreChannel { + ret := _m.Called(name) + + var r0 store.StoreChannel + if rf, ok := ret.Get(0).(func(string) store.StoreChannel); ok { + r0 = rf(name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(store.StoreChannel) + } + } + + return r0 +} + +// GetByNames provides a mock function with given fields: names +func (_m *RoleStore) GetByNames(names []string) store.StoreChannel { + ret := _m.Called(names) + + var r0 store.StoreChannel + if rf, ok := ret.Get(0).(func([]string) store.StoreChannel); ok { + r0 = rf(names) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(store.StoreChannel) + } + } + + return r0 +} + +// Save provides a mock function with given fields: role +func (_m *RoleStore) Save(role *model.Role) store.StoreChannel { + ret := _m.Called(role) + + var r0 store.StoreChannel + if rf, ok := ret.Get(0).(func(*model.Role) store.StoreChannel); ok { + r0 = rf(role) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(store.StoreChannel) + } + } + + return r0 +} diff --git a/store/storetest/mocks/SqlStore.go b/store/storetest/mocks/SqlStore.go index b9b962101..43709fc0e 100644 --- a/store/storetest/mocks/SqlStore.go +++ b/store/storetest/mocks/SqlStore.go @@ -538,6 +538,22 @@ func (_m *SqlStore) RenameColumnIfExists(tableName string, oldColumnName string, return r0 } +// Role provides a mock function with given fields: +func (_m *SqlStore) Role() store.RoleStore { + ret := _m.Called() + + var r0 store.RoleStore + if rf, ok := ret.Get(0).(func() store.RoleStore); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(store.RoleStore) + } + } + + return r0 +} + // Session provides a mock function with given fields: func (_m *SqlStore) Session() store.SessionStore { ret := _m.Called() diff --git a/store/storetest/mocks/Store.go b/store/storetest/mocks/Store.go index 40b50a554..cb7e511f6 100644 --- a/store/storetest/mocks/Store.go +++ b/store/storetest/mocks/Store.go @@ -283,6 +283,22 @@ func (_m *Store) Reaction() store.ReactionStore { return r0 } +// Role provides a mock function with given fields: +func (_m *Store) Role() store.RoleStore { + ret := _m.Called() + + var r0 store.RoleStore + if rf, ok := ret.Get(0).(func() store.RoleStore); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(store.RoleStore) + } + } + + return r0 +} + // Session provides a mock function with given fields: func (_m *Store) Session() store.SessionStore { ret := _m.Called() diff --git a/store/storetest/role_store.go b/store/storetest/role_store.go new file mode 100644 index 000000000..499e36e1e --- /dev/null +++ b/store/storetest/role_store.go @@ -0,0 +1,244 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package storetest + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/mattermost/mattermost-server/model" + "github.com/mattermost/mattermost-server/store" +) + +func TestRoleStore(t *testing.T, ss store.Store) { + t.Run("Save", func(t *testing.T) { testRoleStoreSave(t, ss) }) + t.Run("Get", func(t *testing.T) { testRoleStoreGet(t, ss) }) + t.Run("GetByName", func(t *testing.T) { testRoleStoreGetByName(t, ss) }) + t.Run("GetNames", func(t *testing.T) { testRoleStoreGetByNames(t, ss) }) +} + +func testRoleStoreSave(t *testing.T, ss store.Store) { + // Save a new role. + r1 := &model.Role{ + Name: model.NewId(), + DisplayName: model.NewId(), + Description: model.NewId(), + Permissions: []string{ + "invite_user", + "create_public_channel", + "add_user_to_team", + }, + SchemeManaged: false, + } + + res1 := <-ss.Role().Save(r1) + assert.Nil(t, res1.Err) + d1 := res1.Data.(*model.Role) + assert.Len(t, d1.Id, 26) + assert.Equal(t, r1.Name, d1.Name) + assert.Equal(t, r1.DisplayName, d1.DisplayName) + assert.Equal(t, r1.Description, d1.Description) + assert.Equal(t, r1.Permissions, d1.Permissions) + assert.Equal(t, r1.SchemeManaged, d1.SchemeManaged) + + // Change the role permissions and update. + d1.Permissions = []string{ + "invite_user", + "add_user_to_team", + "delete_public_channel", + } + + res2 := <-ss.Role().Save(d1) + assert.Nil(t, res2.Err) + d2 := res2.Data.(*model.Role) + assert.Len(t, d2.Id, 26) + assert.Equal(t, r1.Name, d2.Name) + assert.Equal(t, r1.DisplayName, d2.DisplayName) + assert.Equal(t, r1.Description, d2.Description) + assert.Equal(t, d1.Permissions, d2.Permissions) + assert.Equal(t, r1.SchemeManaged, d2.SchemeManaged) + + // Try saving one with an invalid ID set. + r3 := &model.Role{ + Id: model.NewId(), + Name: model.NewId(), + DisplayName: model.NewId(), + Description: model.NewId(), + Permissions: []string{ + "invite_user", + "create_public_channel", + "add_user_to_team", + }, + SchemeManaged: false, + } + + res3 := <-ss.Role().Save(r3) + assert.NotNil(t, res3.Err) + + // Try saving one with a duplicate "name" field. + r4 := &model.Role{ + Name: r1.Name, + DisplayName: model.NewId(), + Description: model.NewId(), + Permissions: []string{ + "invite_user", + "create_public_channel", + "add_user_to_team", + }, + SchemeManaged: false, + } + + res4 := <-ss.Role().Save(r4) + assert.NotNil(t, res4.Err) +} + +func testRoleStoreGet(t *testing.T, ss store.Store) { + // Save a role to test with. + r1 := &model.Role{ + Name: model.NewId(), + DisplayName: model.NewId(), + Description: model.NewId(), + Permissions: []string{ + "invite_user", + "create_public_channel", + "add_user_to_team", + }, + SchemeManaged: false, + } + + res1 := <-ss.Role().Save(r1) + assert.Nil(t, res1.Err) + d1 := res1.Data.(*model.Role) + assert.Len(t, d1.Id, 26) + + // Get a valid role + res2 := <-ss.Role().Get(d1.Id) + assert.Nil(t, res2.Err) + d2 := res1.Data.(*model.Role) + assert.Equal(t, d1.Id, d2.Id) + assert.Equal(t, r1.Name, d2.Name) + assert.Equal(t, r1.DisplayName, d2.DisplayName) + assert.Equal(t, r1.Description, d2.Description) + assert.Equal(t, r1.Permissions, d2.Permissions) + assert.Equal(t, r1.SchemeManaged, d2.SchemeManaged) + + // Get an invalid role + res3 := <-ss.Role().Get(model.NewId()) + assert.NotNil(t, res3.Err) +} + +func testRoleStoreGetByName(t *testing.T, ss store.Store) { + // Save a role to test with. + r1 := &model.Role{ + Name: model.NewId(), + DisplayName: model.NewId(), + Description: model.NewId(), + Permissions: []string{ + "invite_user", + "create_public_channel", + "add_user_to_team", + }, + SchemeManaged: false, + } + + res1 := <-ss.Role().Save(r1) + assert.Nil(t, res1.Err) + d1 := res1.Data.(*model.Role) + assert.Len(t, d1.Id, 26) + + // Get a valid role + res2 := <-ss.Role().GetByName(d1.Name) + assert.Nil(t, res2.Err) + d2 := res1.Data.(*model.Role) + assert.Equal(t, d1.Id, d2.Id) + assert.Equal(t, r1.Name, d2.Name) + assert.Equal(t, r1.DisplayName, d2.DisplayName) + assert.Equal(t, r1.Description, d2.Description) + assert.Equal(t, r1.Permissions, d2.Permissions) + assert.Equal(t, r1.SchemeManaged, d2.SchemeManaged) + + // Get an invalid role + res3 := <-ss.Role().GetByName(model.NewId()) + assert.NotNil(t, res3.Err) +} + +func testRoleStoreGetByNames(t *testing.T, ss store.Store) { + // Save some roles to test with. + r1 := &model.Role{ + Name: model.NewId(), + DisplayName: model.NewId(), + Description: model.NewId(), + Permissions: []string{ + "invite_user", + "create_public_channel", + "add_user_to_team", + }, + SchemeManaged: false, + } + r2 := &model.Role{ + Name: model.NewId(), + DisplayName: model.NewId(), + Description: model.NewId(), + Permissions: []string{ + "read_channel", + "create_public_channel", + "add_user_to_team", + }, + SchemeManaged: false, + } + r3 := &model.Role{ + Name: model.NewId(), + DisplayName: model.NewId(), + Description: model.NewId(), + Permissions: []string{ + "invite_user", + "delete_private_channel", + "add_user_to_team", + }, + SchemeManaged: false, + } + + res1 := <-ss.Role().Save(r1) + assert.Nil(t, res1.Err) + d1 := res1.Data.(*model.Role) + assert.Len(t, d1.Id, 26) + + res2 := <-ss.Role().Save(r2) + assert.Nil(t, res2.Err) + d2 := res2.Data.(*model.Role) + assert.Len(t, d2.Id, 26) + + res3 := <-ss.Role().Save(r3) + assert.Nil(t, res3.Err) + d3 := res3.Data.(*model.Role) + assert.Len(t, d3.Id, 26) + + // Get two valid roles. + n4 := []string{r1.Name, r2.Name} + res4 := <-ss.Role().GetByNames(n4) + assert.Nil(t, res4.Err) + roles4 := res4.Data.([]*model.Role) + assert.Len(t, roles4, 2) + assert.Contains(t, roles4, d1) + assert.Contains(t, roles4, d2) + assert.NotContains(t, roles4, d3) + + // Get two invalid roles. + n5 := []string{model.NewId(), model.NewId()} + res5 := <-ss.Role().GetByNames(n5) + assert.Nil(t, res5.Err) + roles5 := res5.Data.([]*model.Role) + assert.Len(t, roles5, 0) + + // Get one valid one and one invalid one. + n6 := []string{r1.Name, model.NewId()} + res6 := <-ss.Role().GetByNames(n6) + assert.Nil(t, res6.Err) + roles6 := res6.Data.([]*model.Role) + assert.Len(t, roles6, 1) + assert.Contains(t, roles6, d1) + assert.NotContains(t, roles6, d2) + assert.NotContains(t, roles6, d3) +} diff --git a/store/storetest/store.go b/store/storetest/store.go index 367c5f441..44f426075 100644 --- a/store/storetest/store.go +++ b/store/storetest/store.go @@ -43,6 +43,7 @@ type Store struct { UserAccessTokenStore mocks.UserAccessTokenStore PluginStore mocks.PluginStore ChannelMemberHistoryStore mocks.ChannelMemberHistoryStore + RoleStore mocks.RoleStore } func (s *Store) Team() store.TeamStore { return &s.TeamStore } @@ -68,6 +69,7 @@ func (s *Store) Reaction() store.ReactionStore { return &s.React func (s *Store) Job() store.JobStore { return &s.JobStore } func (s *Store) UserAccessToken() store.UserAccessTokenStore { return &s.UserAccessTokenStore } func (s *Store) Plugin() store.PluginStore { return &s.PluginStore } +func (s *Store) Role() store.RoleStore { return &s.RoleStore } func (s *Store) ChannelMemberHistory() store.ChannelMemberHistoryStore { return &s.ChannelMemberHistoryStore } @@ -104,5 +106,6 @@ func (s *Store) AssertExpectations(t mock.TestingT) bool { &s.UserAccessTokenStore, &s.ChannelMemberHistoryStore, &s.PluginStore, + &s.RoleStore, ) } -- cgit v1.2.3-1-g7c22