From b13a228b0451098ea32933a36fe64566e366583d Mon Sep 17 00:00:00 2001 From: George Goldberg Date: Wed, 18 Apr 2018 10:18:07 +0100 Subject: [PATCH] MM-10121: CLI command to reset permissions system to default state. (#8637) * MM-10121: CLI command to reset permissions system to default state. * Review comment. --- app/permissions.go | 25 +++++++ cmd/commands/permissions.go | 66 +++++++++++++++++++ i18n/en.json | 8 +++ store/layered_store.go | 6 ++ store/layered_store_supplier.go | 1 + store/local_cache_supplier_roles.go | 7 ++ store/redis_supplier_roles.go | 14 ++++ store/sqlstore/role_supplier.go | 10 +++ store/sqlstore/system_store.go | 11 ++++ store/store.go | 2 + .../mocks/LayeredStoreDatabaseLayer.go | 23 +++++++ store/storetest/mocks/LayeredStoreSupplier.go | 23 +++++++ store/storetest/mocks/PostStore.go | 31 ++++----- store/storetest/mocks/RoleStore.go | 16 +++++ store/storetest/mocks/SystemStore.go | 16 +++++ store/storetest/role_store.go | 40 +++++++++++ store/storetest/system_store.go | 36 ++++++++++ 17 files changed, 320 insertions(+), 15 deletions(-) create mode 100644 app/permissions.go create mode 100644 cmd/commands/permissions.go diff --git a/app/permissions.go b/app/permissions.go new file mode 100644 index 0000000000..be975e03da --- /dev/null +++ b/app/permissions.go @@ -0,0 +1,25 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "github.com/mattermost/mattermost-server/model" +) + +func (a *App) ResetPermissionsSystem() *model.AppError { + // Purge all roles from the database. + if result := <-a.Srv.Store.Role().PermanentDeleteAll(); result.Err != nil { + return result.Err + } + + // Remove the "System" table entry that marks the advanced permissions migration as done. + if result := <-a.Srv.Store.System().PermanentDeleteByName(ADVANCED_PERMISSIONS_MIGRATION_KEY); result.Err != nil { + return result.Err + } + + // Now that the permissions system has been reset, re-run the migration to reinitialise it. + a.DoAdvancedPermissionsMigration() + + return nil +} diff --git a/cmd/commands/permissions.go b/cmd/commands/permissions.go new file mode 100644 index 0000000000..33c255a319 --- /dev/null +++ b/cmd/commands/permissions.go @@ -0,0 +1,66 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package commands + +import ( + "errors" + "fmt" + + "github.com/spf13/cobra" + + "github.com/mattermost/mattermost-server/cmd" +) + +var PermissionsCmd = &cobra.Command{ + Use: "permissions", + Short: "Management of the Permissions system", +} + +var ResetPermissionsCmd = &cobra.Command{ + Use: "reset", + Short: "Reset the permissions system to its default state", + Long: "Reset the permissions system to its default state", + Example: " permissions reset", + RunE: resetPermissionsCmdF, +} + +func init() { + ResetPermissionsCmd.Flags().Bool("confirm", false, "Confirm you really want to reset the permissions system and a database backup has been performed.") + + PermissionsCmd.AddCommand( + ResetPermissionsCmd, + ) + cmd.RootCmd.AddCommand(PermissionsCmd) +} + +func resetPermissionsCmdF(command *cobra.Command, args []string) error { + a, err := cmd.InitDBCommandContextCobra(command) + if err != nil { + return err + } + + confirmFlag, _ := command.Flags().GetBool("confirm") + if !confirmFlag { + var confirm string + cmd.CommandPrettyPrintln("Have you performed a database backup? (YES/NO): ") + fmt.Scanln(&confirm) + + if confirm != "YES" { + return errors.New("ABORTED: You did not answer YES exactly, in all capitals.") + } + cmd.CommandPrettyPrintln("Are you sure you want to reset the permissions system? All data related to the permissions system will be permanently deleted and all users will revert to having the default permissions. (YES/NO): ") + fmt.Scanln(&confirm) + if confirm != "YES" { + return errors.New("ABORTED: You did not answer YES exactly, in all capitals.") + } + } + + if err := a.ResetPermissionsSystem(); err != nil { + return errors.New(err.Error()) + } + + cmd.CommandPrettyPrintln("Permissions system successfully reset") + + return nil +} diff --git a/i18n/en.json b/i18n/en.json index e66b88a828..d71ad8009d 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -6562,6 +6562,14 @@ "id": "store.sql_post.search.disabled", "translation": "Searching has been disabled on this server. Please contact your System Administrator." }, + { + "id": "store.sql_system.permanent_delete_by_name.app_error", + "translation": "We could not permanently delete the system table entry" + }, + { + "id": "store.sql_role.permanent_delete_all.app_error", + "translation": "We could not permanently delete all the roles" + }, { "id": "store.sql_post.search.warn", "translation": "Query error searching posts: %v" diff --git a/store/layered_store.go b/store/layered_store.go index cac0f61d35..5ef9072609 100644 --- a/store/layered_store.go +++ b/store/layered_store.go @@ -252,3 +252,9 @@ func (s *LayeredRoleStore) GetByNames(names []string) StoreChannel { return supplier.RoleGetByNames(s.TmpContext, names) }) } + +func (s *LayeredRoleStore) PermanentDeleteAll() StoreChannel { + return s.RunQuery(func(supplier LayeredStoreSupplier) *LayeredStoreSupplierResult { + return supplier.RolePermanentDeleteAll(s.TmpContext) + }) +} diff --git a/store/layered_store_supplier.go b/store/layered_store_supplier.go index 80fe3cb246..9a7604b20a 100644 --- a/store/layered_store_supplier.go +++ b/store/layered_store_supplier.go @@ -35,4 +35,5 @@ type LayeredStoreSupplier interface { RoleGet(ctx context.Context, roleId string, hints ...LayeredStoreHint) *LayeredStoreSupplierResult RoleGetByName(ctx context.Context, name string, hints ...LayeredStoreHint) *LayeredStoreSupplierResult RoleGetByNames(ctx context.Context, names []string, hints ...LayeredStoreHint) *LayeredStoreSupplierResult + RolePermanentDeleteAll(ctx context.Context, hints ...LayeredStoreHint) *LayeredStoreSupplierResult } diff --git a/store/local_cache_supplier_roles.go b/store/local_cache_supplier_roles.go index 8cbde0a23f..7c82f60eb1 100644 --- a/store/local_cache_supplier_roles.go +++ b/store/local_cache_supplier_roles.go @@ -68,3 +68,10 @@ func (s *LocalCacheSupplier) RoleGetByNames(ctx context.Context, roleNames []str return result } + +func (s *LocalCacheSupplier) RolePermanentDeleteAll(ctx context.Context, hints ...LayeredStoreHint) *LayeredStoreSupplierResult { + defer s.roleCache.Purge() + defer s.doClearCacheCluster(s.roleCache) + + return s.Next().RolePermanentDeleteAll(ctx, hints...) +} diff --git a/store/redis_supplier_roles.go b/store/redis_supplier_roles.go index 170420f1fc..232a8c0401 100644 --- a/store/redis_supplier_roles.go +++ b/store/redis_supplier_roles.go @@ -84,6 +84,20 @@ func (s *RedisSupplier) RoleGetByNames(ctx context.Context, roleNames []string, return result } +func (s *RedisSupplier) RolePermanentDeleteAll(ctx context.Context, hints ...LayeredStoreHint) *LayeredStoreSupplierResult { + defer func() { + if keys, err := s.client.Keys("roles:*").Result(); err != nil { + l4g.Error("Redis encountered an error on read: " + err.Error()) + } else { + if err := s.client.Del(keys...).Err(); err != nil { + l4g.Error("Redis encountered an error on delete: " + err.Error()) + } + } + }() + + return s.Next().RolePermanentDeleteAll(ctx, hints...) +} + func buildRedisKeyForRoleName(roleName string) string { return fmt.Sprintf("roles:%s", roleName) } diff --git a/store/sqlstore/role_supplier.go b/store/sqlstore/role_supplier.go index 828f4484e2..ddbdaca52b 100644 --- a/store/sqlstore/role_supplier.go +++ b/store/sqlstore/role_supplier.go @@ -174,3 +174,13 @@ func (s *SqlSupplier) RoleGetByNames(ctx context.Context, names []string, hints return result } + +func (s *SqlSupplier) RolePermanentDeleteAll(ctx context.Context, hints ...store.LayeredStoreHint) *store.LayeredStoreSupplierResult { + result := store.NewSupplierResult() + + if _, err := s.GetMaster().Exec("DELETE FROM Roles"); err != nil { + result.Err = model.NewAppError("SqlRoleStore.PermanentDeleteAll", "store.sql_role.permanent_delete_all.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + return result +} diff --git a/store/sqlstore/system_store.go b/store/sqlstore/system_store.go index 496ff2ced5..4065bb955f 100644 --- a/store/sqlstore/system_store.go +++ b/store/sqlstore/system_store.go @@ -85,3 +85,14 @@ func (s SqlSystemStore) GetByName(name string) store.StoreChannel { result.Data = &system }) } + +func (s SqlSystemStore) PermanentDeleteByName(name string) store.StoreChannel { + return store.Do(func(result *store.StoreResult) { + var system model.System + if _, err := s.GetReplica().Exec("DELETE FROM Systems WHERE Name = :Name", map[string]interface{}{"Name": name}); err != nil { + result.Err = model.NewAppError("SqlSystemStore.PermanentDeleteByName", "store.sql_system.permanent_delete_by_name.app_error", nil, "", http.StatusInternalServerError) + } + + result.Data = &system + }) +} diff --git a/store/store.go b/store/store.go index 79a14baba8..e64089068e 100644 --- a/store/store.go +++ b/store/store.go @@ -322,6 +322,7 @@ type SystemStore interface { Update(system *model.System) StoreChannel Get() StoreChannel GetByName(name string) StoreChannel + PermanentDeleteByName(name string) StoreChannel } type WebhookStore interface { @@ -476,4 +477,5 @@ type RoleStore interface { Get(roleId string) StoreChannel GetByName(name string) StoreChannel GetByNames(names []string) StoreChannel + PermanentDeleteAll() StoreChannel } diff --git a/store/storetest/mocks/LayeredStoreDatabaseLayer.go b/store/storetest/mocks/LayeredStoreDatabaseLayer.go index d0162a01e3..6fa31bb1b3 100644 --- a/store/storetest/mocks/LayeredStoreDatabaseLayer.go +++ b/store/storetest/mocks/LayeredStoreDatabaseLayer.go @@ -501,6 +501,29 @@ func (_m *LayeredStoreDatabaseLayer) RoleGetByNames(ctx context.Context, names [ return r0 } +// RolePermanentDeleteAll provides a mock function with given fields: ctx, hints +func (_m *LayeredStoreDatabaseLayer) RolePermanentDeleteAll(ctx context.Context, 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) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *store.LayeredStoreSupplierResult + if rf, ok := ret.Get(0).(func(context.Context, ...store.LayeredStoreHint) *store.LayeredStoreSupplierResult); ok { + r0 = rf(ctx, 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)) diff --git a/store/storetest/mocks/LayeredStoreSupplier.go b/store/storetest/mocks/LayeredStoreSupplier.go index 59fd31cb82..d4242708b6 100644 --- a/store/storetest/mocks/LayeredStoreSupplier.go +++ b/store/storetest/mocks/LayeredStoreSupplier.go @@ -214,6 +214,29 @@ func (_m *LayeredStoreSupplier) RoleGetByNames(ctx context.Context, names []stri return r0 } +// RolePermanentDeleteAll provides a mock function with given fields: ctx, hints +func (_m *LayeredStoreSupplier) RolePermanentDeleteAll(ctx context.Context, 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) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *store.LayeredStoreSupplierResult + if rf, ok := ret.Get(0).(func(context.Context, ...store.LayeredStoreHint) *store.LayeredStoreSupplierResult); ok { + r0 = rf(ctx, 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)) diff --git a/store/storetest/mocks/PostStore.go b/store/storetest/mocks/PostStore.go index bdd0d1d16b..bdfbb33217 100644 --- a/store/storetest/mocks/PostStore.go +++ b/store/storetest/mocks/PostStore.go @@ -162,6 +162,22 @@ func (_m *PostStore) GetFlaggedPostsForTeam(userId string, teamId string, offset return r0 } +// GetMaxPostSize provides a mock function with given fields: +func (_m *PostStore) GetMaxPostSize() store.StoreChannel { + ret := _m.Called() + + var r0 store.StoreChannel + if rf, ok := ret.Get(0).(func() store.StoreChannel); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(store.StoreChannel) + } + } + + return r0 +} + // GetOldest provides a mock function with given fields: func (_m *PostStore) GetOldest() store.StoreChannel { ret := _m.Called() @@ -422,18 +438,3 @@ func (_m *PostStore) Update(newPost *model.Post, oldPost *model.Post) store.Stor return r0 } - -func (_m *PostStore) GetMaxPostSize() store.StoreChannel { - ret := _m.Called() - - var r0 store.StoreChannel - if rf, ok := ret.Get(0).(func() store.StoreChannel); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(store.StoreChannel) - } - } - - return r0 -} diff --git a/store/storetest/mocks/RoleStore.go b/store/storetest/mocks/RoleStore.go index 8150460aec..3c01ee3413 100644 --- a/store/storetest/mocks/RoleStore.go +++ b/store/storetest/mocks/RoleStore.go @@ -61,6 +61,22 @@ func (_m *RoleStore) GetByNames(names []string) store.StoreChannel { return r0 } +// PermanentDeleteAll provides a mock function with given fields: +func (_m *RoleStore) PermanentDeleteAll() store.StoreChannel { + ret := _m.Called() + + var r0 store.StoreChannel + if rf, ok := ret.Get(0).(func() store.StoreChannel); ok { + r0 = rf() + } 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) diff --git a/store/storetest/mocks/SystemStore.go b/store/storetest/mocks/SystemStore.go index 2c5774fcfb..b31e4646dc 100644 --- a/store/storetest/mocks/SystemStore.go +++ b/store/storetest/mocks/SystemStore.go @@ -45,6 +45,22 @@ func (_m *SystemStore) GetByName(name string) store.StoreChannel { return r0 } +// PermanentDeleteByName provides a mock function with given fields: name +func (_m *SystemStore) PermanentDeleteByName(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 +} + // Save provides a mock function with given fields: system func (_m *SystemStore) Save(system *model.System) store.StoreChannel { ret := _m.Called(system) diff --git a/store/storetest/role_store.go b/store/storetest/role_store.go index 499e36e1e9..e51c326229 100644 --- a/store/storetest/role_store.go +++ b/store/storetest/role_store.go @@ -17,6 +17,7 @@ func TestRoleStore(t *testing.T, ss store.Store) { 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) }) + t.Run("PermanentDeleteAll", func(t *testing.T) { testRoleStorePermanentDeleteAll(t, ss) }) } func testRoleStoreSave(t *testing.T, ss store.Store) { @@ -242,3 +243,42 @@ func testRoleStoreGetByNames(t *testing.T, ss store.Store) { assert.NotContains(t, roles6, d2) assert.NotContains(t, roles6, d3) } + +func testRoleStorePermanentDeleteAll(t *testing.T, ss store.Store) { + 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, + } + + store.Must(ss.Role().Save(r1)) + store.Must(ss.Role().Save(r2)) + + res1 := <-ss.Role().GetByNames([]string{r1.Name, r2.Name}) + assert.Nil(t, res1.Err) + assert.Len(t, res1.Data.([]*model.Role), 2) + + res2 := <-ss.Role().PermanentDeleteAll() + assert.Nil(t, res2.Err) + + res3 := <-ss.Role().GetByNames([]string{r1.Name, r2.Name}) + assert.Nil(t, res3.Err) + assert.Len(t, res3.Data.([]*model.Role), 0) +} diff --git a/store/storetest/system_store.go b/store/storetest/system_store.go index 32c39ee413..a06b72a837 100644 --- a/store/storetest/system_store.go +++ b/store/storetest/system_store.go @@ -6,6 +6,8 @@ package storetest import ( "testing" + "github.com/stretchr/testify/assert" + "github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/store" ) @@ -13,6 +15,7 @@ import ( func TestSystemStore(t *testing.T, ss store.Store) { t.Run("", func(t *testing.T) { testSystemStore(t, ss) }) t.Run("SaveOrUpdate", func(t *testing.T) { testSystemStoreSaveOrUpdate(t, ss) }) + t.Run("PermanentDeleteByName", func(t *testing.T) { testSystemStorePermanentDeleteByName(t, ss) }) } func testSystemStore(t *testing.T, ss store.Store) { @@ -56,3 +59,36 @@ func testSystemStoreSaveOrUpdate(t *testing.T, ss store.Store) { t.Fatal(r.Err) } } + +func testSystemStorePermanentDeleteByName(t *testing.T, ss store.Store) { + s1 := &model.System{Name: model.NewId(), Value: "value"} + s2 := &model.System{Name: model.NewId(), Value: "value"} + + store.Must(ss.System().Save(s1)) + store.Must(ss.System().Save(s2)) + + res1 := <-ss.System().GetByName(s1.Name) + assert.Nil(t, res1.Err) + + res2 := <-ss.System().GetByName(s2.Name) + assert.Nil(t, res2.Err) + + res3 := <-ss.System().PermanentDeleteByName(s1.Name) + assert.Nil(t, res3.Err) + + res4 := <-ss.System().GetByName(s1.Name) + assert.NotNil(t, res4.Err) + + res5 := <-ss.System().GetByName(s2.Name) + assert.Nil(t, res5.Err) + + res6 := <-ss.System().PermanentDeleteByName(s2.Name) + assert.Nil(t, res6.Err) + + res7 := <-ss.System().GetByName(s1.Name) + assert.NotNil(t, res7.Err) + + res8 := <-ss.System().GetByName(s2.Name) + assert.NotNil(t, res8.Err) + +}