summaryrefslogtreecommitdiffstats
path: root/store
diff options
context:
space:
mode:
authorGeorge Goldberg <george@gberg.me>2018-05-14 15:59:04 +0100
committerGitHub <noreply@github.com>2018-05-14 15:59:04 +0100
commit51bd710ecdca6628461c9fa2679737073e4d5059 (patch)
treeb2a4837ced3ed515ee505728917a6630b0553f76 /store
parent91557bbd978500388a11b99401783164e143a966 (diff)
downloadchat-51bd710ecdca6628461c9fa2679737073e4d5059.tar.gz
chat-51bd710ecdca6628461c9fa2679737073e4d5059.tar.bz2
chat-51bd710ecdca6628461c9fa2679737073e4d5059.zip
MM-9728: Online migration for advanced permissions phase 2 (#8744)
* MM-9728: Online migration for advanced permissions phase 2 * Add unit tests for new store functions. * Move migration specific code to own file. * Add migration state function test. * Style fixes. * Add i18n strings. * Fix mocks. * Add TestMain to migrations package tests. * Fix typo. * Fix review comments. * Fix up the "Check if migration is done" check to actually work.
Diffstat (limited to 'store')
-rw-r--r--store/sqlstore/channel_store.go68
-rw-r--r--store/sqlstore/team_store.go69
-rw-r--r--store/store.go2
-rw-r--r--store/storetest/channel_store.go75
-rw-r--r--store/storetest/mocks/ChannelStore.go16
-rw-r--r--store/storetest/mocks/TeamStore.go16
-rw-r--r--store/storetest/team_store.go71
7 files changed, 317 insertions, 0 deletions
diff --git a/store/sqlstore/channel_store.go b/store/sqlstore/channel_store.go
index beef1be80..dceebc92e 100644
--- a/store/sqlstore/channel_store.go
+++ b/store/sqlstore/channel_store.go
@@ -1739,3 +1739,71 @@ func (s SqlChannelStore) GetChannelsByScheme(schemeId string, offset int, limit
}
})
}
+
+// This function does the Advanced Permissions Phase 2 migration for ChannelMember objects. It performs the migration
+// in batches as a single transaction per batch to ensure consistency but to also minimise execution time to avoid
+// causing unnecessary table locks. **THIS FUNCTION SHOULD NOT BE USED FOR ANY OTHER PURPOSE.** Executing this function
+// *after* the new Schemes functionality has been used on an installation will have unintended consequences.
+func (s SqlChannelStore) MigrateChannelMembers(fromChannelId string, fromUserId string) store.StoreChannel {
+ return store.Do(func(result *store.StoreResult) {
+ var transaction *gorp.Transaction
+ var err error
+
+ if transaction, err = s.GetMaster().Begin(); err != nil {
+ result.Err = model.NewAppError("SqlChannelStore.MigrateChannelMembers", "store.sql_channel.migrate_channel_members.open_transaction.app_error", nil, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ var channelMembers []channelMember
+ if _, err := transaction.Select(&channelMembers, "SELECT * from ChannelMembers WHERE (ChannelId, UserId) > (:FromChannelId, :FromUserId) ORDER BY ChannelId, UserId LIMIT 100", map[string]interface{}{"FromChannelId": fromChannelId, "FromUserId": fromUserId}); err != nil {
+ result.Err = model.NewAppError("SqlChannelStore.MigrateChannelMembers", "store.sql_channel.migrate_channel_members.select.app_error", nil, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ if len(channelMembers) == 0 {
+ // No more channel members in query result means that the migration has finished.
+ return
+ }
+
+ for _, member := range channelMembers {
+ roles := strings.Fields(member.Roles)
+ var newRoles []string
+ member.SchemeAdmin = sql.NullBool{Bool: false, Valid: true}
+ member.SchemeUser = sql.NullBool{Bool: false, Valid: true}
+ for _, role := range roles {
+ if role == model.CHANNEL_ADMIN_ROLE_ID {
+ member.SchemeAdmin = sql.NullBool{Bool: true, Valid: true}
+ } else if role == model.CHANNEL_USER_ROLE_ID {
+ member.SchemeUser = sql.NullBool{Bool: true, Valid: true}
+ } else {
+ newRoles = append(newRoles, role)
+ }
+ }
+ member.Roles = strings.Join(newRoles, " ")
+
+ if _, err := transaction.Update(&member); err != nil {
+ if err2 := transaction.Rollback(); err2 != nil {
+ result.Err = model.NewAppError("SqlChannelStore.MigrateChannelMembers", "store.sql_channel.migrate_channel_members.rollback_transaction.app_error", nil, err2.Error(), http.StatusInternalServerError)
+ return
+ }
+ result.Err = model.NewAppError("SqlChannelStore.MigrateChannelMembers", "store.sql_channel.migrate_channel_members.update.app_error", nil, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ }
+
+ if err := transaction.Commit(); err != nil {
+ if err2 := transaction.Rollback(); err2 != nil {
+ result.Err = model.NewAppError("SqlChannelStore.MigrateChannelMembers", "store.sql_channel.migrate_channel_members.rollback_transaction.app_error", nil, err2.Error(), http.StatusInternalServerError)
+ return
+ }
+ result.Err = model.NewAppError("SqlChannelStore.MigrateChannelMembers", "store.sql_channel.migrate_channel_members.commit_transaction.app_error", nil, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ data := make(map[string]string)
+ data["ChannelId"] = channelMembers[len(channelMembers)-1].ChannelId
+ data["UserId"] = channelMembers[len(channelMembers)-1].UserId
+ result.Data = data
+ })
+}
diff --git a/store/sqlstore/team_store.go b/store/sqlstore/team_store.go
index 9e72cc82e..ea5f7fd1f 100644
--- a/store/sqlstore/team_store.go
+++ b/store/sqlstore/team_store.go
@@ -9,6 +9,7 @@ import (
"strconv"
"strings"
+ "github.com/mattermost/gorp"
"github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/store"
)
@@ -725,3 +726,71 @@ func (s SqlTeamStore) GetTeamsByScheme(schemeId string, offset int, limit int) s
}
})
}
+
+// This function does the Advanced Permissions Phase 2 migration for TeamMember objects. It performs the migration
+// in batches as a single transaction per batch to ensure consistency but to also minimise execution time to avoid
+// causing unnecessary table locks. **THIS FUNCTION SHOULD NOT BE USED FOR ANY OTHER PURPOSE.** Executing this function
+// *after* the new Schemes functionality has been used on an installation will have unintended consequences.
+func (s SqlTeamStore) MigrateTeamMembers(fromTeamId string, fromUserId string) store.StoreChannel {
+ return store.Do(func(result *store.StoreResult) {
+ var transaction *gorp.Transaction
+ var err error
+
+ if transaction, err = s.GetMaster().Begin(); err != nil {
+ result.Err = model.NewAppError("SqlTeamStore.MigrateTeamMembers", "store.sql_team.migrate_team_members.open_transaction.app_error", nil, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ var teamMembers []teamMember
+ if _, err := transaction.Select(&teamMembers, "SELECT * from TeamMembers WHERE (TeamId, UserId) > (:FromTeamId, :FromUserId) ORDER BY TeamId, UserId LIMIT 100", map[string]interface{}{"FromTeamId": fromTeamId, "FromUserId": fromUserId}); err != nil {
+ result.Err = model.NewAppError("SqlTeamStore.MigrateTeamMembers", "store.sql_team.migrate_team_members.select.app_error", nil, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ if len(teamMembers) == 0 {
+ // No more team members in query result means that the migration has finished.
+ return
+ }
+
+ for _, member := range teamMembers {
+ roles := strings.Fields(member.Roles)
+ var newRoles []string
+ member.SchemeAdmin = sql.NullBool{Bool: false, Valid: true}
+ member.SchemeUser = sql.NullBool{Bool: false, Valid: true}
+ for _, role := range roles {
+ if role == model.TEAM_ADMIN_ROLE_ID {
+ member.SchemeAdmin = sql.NullBool{Bool: true, Valid: true}
+ } else if role == model.TEAM_USER_ROLE_ID {
+ member.SchemeUser = sql.NullBool{Bool: true, Valid: true}
+ } else {
+ newRoles = append(newRoles, role)
+ }
+ }
+ member.Roles = strings.Join(newRoles, " ")
+
+ if _, err := transaction.Update(&member); err != nil {
+ if err2 := transaction.Rollback(); err2 != nil {
+ result.Err = model.NewAppError("SqlTeamStore.MigrateTeamMembers", "store.sql_team.migrate_team_members.rollback_transaction.app_error", nil, err2.Error(), http.StatusInternalServerError)
+ return
+ }
+ result.Err = model.NewAppError("SqlTeamStore.MigrateTeamMembers", "store.sql_team.migrate_team_members.update.app_error", nil, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ }
+
+ if err := transaction.Commit(); err != nil {
+ if err2 := transaction.Rollback(); err2 != nil {
+ result.Err = model.NewAppError("SqlTeamStore.MigrateTeamMembers", "store.sql_team.migrate_team_members.rollback_transaction.app_error", nil, err2.Error(), http.StatusInternalServerError)
+ return
+ }
+ result.Err = model.NewAppError("SqlTeamStore.MigrateTeamMembers", "store.sql_team.migrate_team_members.commit_transaction.app_error", nil, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ data := make(map[string]string)
+ data["TeamId"] = teamMembers[len(teamMembers)-1].TeamId
+ data["UserId"] = teamMembers[len(teamMembers)-1].UserId
+ result.Data = data
+ })
+}
diff --git a/store/store.go b/store/store.go
index 2e85c0a68..bf2ac42f5 100644
--- a/store/store.go
+++ b/store/store.go
@@ -105,6 +105,7 @@ type TeamStore interface {
RemoveAllMembersByUser(userId string) StoreChannel
UpdateLastTeamIconUpdate(teamId string, curTime int64) StoreChannel
GetTeamsByScheme(schemeId string, offset int, limit int) StoreChannel
+ MigrateTeamMembers(fromTeamId string, fromUserId string) StoreChannel
}
type ChannelStore interface {
@@ -163,6 +164,7 @@ type ChannelStore interface {
GetChannelUnread(channelId, userId string) StoreChannel
ClearCaches()
GetChannelsByScheme(schemeId string, offset int, limit int) StoreChannel
+ MigrateChannelMembers(fromChannelId string, fromUserId string) StoreChannel
}
type ChannelMemberHistoryStore interface {
diff --git a/store/storetest/channel_store.go b/store/storetest/channel_store.go
index d90a0ae1e..d044f3907 100644
--- a/store/storetest/channel_store.go
+++ b/store/storetest/channel_store.go
@@ -5,6 +5,7 @@ package storetest
import (
"sort"
+ "strings"
"testing"
"time"
@@ -52,6 +53,7 @@ func TestChannelStore(t *testing.T, ss store.Store) {
t.Run("GetPinnedPosts", func(t *testing.T) { testChannelStoreGetPinnedPosts(t, ss) })
t.Run("MaxChannelsPerTeam", func(t *testing.T) { testChannelStoreMaxChannelsPerTeam(t, ss) })
t.Run("GetChannelsByScheme", func(t *testing.T) { testChannelStoreGetChannelsByScheme(t, ss) })
+ t.Run("MigrateChannelMembers", func(t *testing.T) { testChannelStoreMigrateChannelMembers(t, ss) })
}
@@ -2254,3 +2256,76 @@ func testChannelStoreGetChannelsByScheme(t *testing.T, ss store.Store) {
d3 := res3.Data.(model.ChannelList)
assert.Len(t, d3, 0)
}
+
+func testChannelStoreMigrateChannelMembers(t *testing.T, ss store.Store) {
+ s1 := model.NewId()
+ c1 := &model.Channel{
+ TeamId: model.NewId(),
+ DisplayName: "Name",
+ Name: model.NewId(),
+ Type: model.CHANNEL_OPEN,
+ SchemeId: &s1,
+ }
+ c1 = (<-ss.Channel().Save(c1, 100)).Data.(*model.Channel)
+
+ cm1 := &model.ChannelMember{
+ ChannelId: c1.Id,
+ UserId: model.NewId(),
+ ExplicitRoles: "channel_admin channel_user",
+ NotifyProps: model.GetDefaultChannelNotifyProps(),
+ }
+ cm2 := &model.ChannelMember{
+ ChannelId: c1.Id,
+ UserId: model.NewId(),
+ ExplicitRoles: "channel_user",
+ NotifyProps: model.GetDefaultChannelNotifyProps(),
+ }
+ cm3 := &model.ChannelMember{
+ ChannelId: c1.Id,
+ UserId: model.NewId(),
+ ExplicitRoles: "something_else",
+ NotifyProps: model.GetDefaultChannelNotifyProps(),
+ }
+
+ cm1 = (<-ss.Channel().SaveMember(cm1)).Data.(*model.ChannelMember)
+ cm2 = (<-ss.Channel().SaveMember(cm2)).Data.(*model.ChannelMember)
+ cm3 = (<-ss.Channel().SaveMember(cm3)).Data.(*model.ChannelMember)
+
+ lastDoneChannelId := strings.Repeat("0", 26)
+ lastDoneUserId := strings.Repeat("0", 26)
+
+ for {
+ res := <-ss.Channel().MigrateChannelMembers(lastDoneChannelId, lastDoneUserId)
+ if assert.Nil(t, res.Err) {
+ if res.Data == nil {
+ break
+ }
+ data := res.Data.(map[string]string)
+ lastDoneChannelId = data["ChannelId"]
+ lastDoneUserId = data["UserId"]
+ }
+ }
+
+ ss.Channel().ClearCaches()
+
+ res1 := <-ss.Channel().GetMember(cm1.ChannelId, cm1.UserId)
+ assert.Nil(t, res1.Err)
+ cm1b := res1.Data.(*model.ChannelMember)
+ assert.Equal(t, "", cm1b.ExplicitRoles)
+ assert.True(t, cm1b.SchemeUser)
+ assert.True(t, cm1b.SchemeAdmin)
+
+ res2 := <-ss.Channel().GetMember(cm2.ChannelId, cm2.UserId)
+ assert.Nil(t, res2.Err)
+ cm2b := res2.Data.(*model.ChannelMember)
+ assert.Equal(t, "", cm2b.ExplicitRoles)
+ assert.True(t, cm2b.SchemeUser)
+ assert.False(t, cm2b.SchemeAdmin)
+
+ res3 := <-ss.Channel().GetMember(cm3.ChannelId, cm3.UserId)
+ assert.Nil(t, res3.Err)
+ cm3b := res3.Data.(*model.ChannelMember)
+ assert.Equal(t, "something_else", cm3b.ExplicitRoles)
+ assert.False(t, cm3b.SchemeUser)
+ assert.False(t, cm3b.SchemeAdmin)
+}
diff --git a/store/storetest/mocks/ChannelStore.go b/store/storetest/mocks/ChannelStore.go
index ecc8b8768..8858e3d3b 100644
--- a/store/storetest/mocks/ChannelStore.go
+++ b/store/storetest/mocks/ChannelStore.go
@@ -583,6 +583,22 @@ func (_m *ChannelStore) IsUserInChannelUseCache(userId string, channelId string)
return r0
}
+// MigrateChannelMembers provides a mock function with given fields: fromChannelId, fromUserId
+func (_m *ChannelStore) MigrateChannelMembers(fromChannelId string, fromUserId string) store.StoreChannel {
+ ret := _m.Called(fromChannelId, fromUserId)
+
+ var r0 store.StoreChannel
+ if rf, ok := ret.Get(0).(func(string, string) store.StoreChannel); ok {
+ r0 = rf(fromChannelId, fromUserId)
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).(store.StoreChannel)
+ }
+ }
+
+ return r0
+}
+
// PermanentDelete provides a mock function with given fields: channelId
func (_m *ChannelStore) PermanentDelete(channelId string) store.StoreChannel {
ret := _m.Called(channelId)
diff --git a/store/storetest/mocks/TeamStore.go b/store/storetest/mocks/TeamStore.go
index 51a968784..93cb84caf 100644
--- a/store/storetest/mocks/TeamStore.go
+++ b/store/storetest/mocks/TeamStore.go
@@ -301,6 +301,22 @@ func (_m *TeamStore) GetTotalMemberCount(teamId string) store.StoreChannel {
return r0
}
+// MigrateTeamMembers provides a mock function with given fields: fromTeamId, fromUserId
+func (_m *TeamStore) MigrateTeamMembers(fromTeamId string, fromUserId string) store.StoreChannel {
+ ret := _m.Called(fromTeamId, fromUserId)
+
+ var r0 store.StoreChannel
+ if rf, ok := ret.Get(0).(func(string, string) store.StoreChannel); ok {
+ r0 = rf(fromTeamId, fromUserId)
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).(store.StoreChannel)
+ }
+ }
+
+ return r0
+}
+
// PermanentDelete provides a mock function with given fields: teamId
func (_m *TeamStore) PermanentDelete(teamId string) store.StoreChannel {
ret := _m.Called(teamId)
diff --git a/store/storetest/team_store.go b/store/storetest/team_store.go
index ff79650d5..726c17a99 100644
--- a/store/storetest/team_store.go
+++ b/store/storetest/team_store.go
@@ -4,6 +4,7 @@
package storetest
import (
+ "strings"
"testing"
"time"
@@ -39,6 +40,7 @@ func TestTeamStore(t *testing.T, ss store.Store) {
t.Run("GetChannelUnreadsForTeam", func(t *testing.T) { testGetChannelUnreadsForTeam(t, ss) })
t.Run("UpdateLastTeamIconUpdate", func(t *testing.T) { testUpdateLastTeamIconUpdate(t, ss) })
t.Run("GetTeamsByScheme", func(t *testing.T) { testGetTeamsByScheme(t, ss) })
+ t.Run("MigrateTeamMembers", func(t *testing.T) { testTeamStoreMigrateTeamMembers(t, ss) })
}
func testTeamStoreSave(t *testing.T, ss store.Store) {
@@ -1098,3 +1100,72 @@ func testGetTeamsByScheme(t *testing.T, ss store.Store) {
d3 := res3.Data.([]*model.Team)
assert.Len(t, d3, 0)
}
+
+func testTeamStoreMigrateTeamMembers(t *testing.T, ss store.Store) {
+ s1 := model.NewId()
+ t1 := &model.Team{
+ DisplayName: "Name",
+ Name: "z-z-z" + model.NewId() + "b",
+ Email: model.NewId() + "@nowhere.com",
+ Type: model.TEAM_OPEN,
+ InviteId: model.NewId(),
+ SchemeId: &s1,
+ }
+ t1 = store.Must(ss.Team().Save(t1)).(*model.Team)
+
+ tm1 := &model.TeamMember{
+ TeamId: t1.Id,
+ UserId: model.NewId(),
+ ExplicitRoles: "team_admin team_user",
+ }
+ tm2 := &model.TeamMember{
+ TeamId: t1.Id,
+ UserId: model.NewId(),
+ ExplicitRoles: "team_user",
+ }
+ tm3 := &model.TeamMember{
+ TeamId: t1.Id,
+ UserId: model.NewId(),
+ ExplicitRoles: "something_else",
+ }
+
+ tm1 = (<-ss.Team().SaveMember(tm1, -1)).Data.(*model.TeamMember)
+ tm2 = (<-ss.Team().SaveMember(tm2, -1)).Data.(*model.TeamMember)
+ tm3 = (<-ss.Team().SaveMember(tm3, -1)).Data.(*model.TeamMember)
+
+ lastDoneTeamId := strings.Repeat("0", 26)
+ lastDoneUserId := strings.Repeat("0", 26)
+
+ for {
+ res := <-ss.Team().MigrateTeamMembers(lastDoneTeamId, lastDoneUserId)
+ if assert.Nil(t, res.Err) {
+ if res.Data == nil {
+ break
+ }
+ data := res.Data.(map[string]string)
+ lastDoneTeamId = data["TeamId"]
+ lastDoneUserId = data["UserId"]
+ }
+ }
+
+ res1 := <-ss.Team().GetMember(tm1.TeamId, tm1.UserId)
+ assert.Nil(t, res1.Err)
+ tm1b := res1.Data.(*model.TeamMember)
+ assert.Equal(t, "", tm1b.ExplicitRoles)
+ assert.True(t, tm1b.SchemeUser)
+ assert.True(t, tm1b.SchemeAdmin)
+
+ res2 := <-ss.Team().GetMember(tm2.TeamId, tm2.UserId)
+ assert.Nil(t, res2.Err)
+ tm2b := res2.Data.(*model.TeamMember)
+ assert.Equal(t, "", tm2b.ExplicitRoles)
+ assert.True(t, tm2b.SchemeUser)
+ assert.False(t, tm2b.SchemeAdmin)
+
+ res3 := <-ss.Team().GetMember(tm3.TeamId, tm3.UserId)
+ assert.Nil(t, res3.Err)
+ tm3b := res3.Data.(*model.TeamMember)
+ assert.Equal(t, "something_else", tm3b.ExplicitRoles)
+ assert.False(t, tm3b.SchemeUser)
+ assert.False(t, tm3b.SchemeAdmin)
+}