summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--api4/api.go5
-rw-r--r--api4/apitestlib.go25
-rw-r--r--api4/channel.go51
-rw-r--r--api4/channel_test.go78
-rw-r--r--api4/emoji.go66
-rw-r--r--api4/emoji_test.go124
-rw-r--r--api4/post.go41
-rw-r--r--api4/role.go1
-rw-r--r--api4/scheme.go211
-rw-r--r--api4/scheme_test.go737
-rw-r--r--api4/team.go53
-rw-r--r--api4/team_test.go76
-rw-r--r--app/app.go106
-rw-r--r--app/app_test.go135
-rw-r--r--app/apptestlib.go47
-rw-r--r--app/authorization.go17
-rw-r--r--app/channel.go97
-rw-r--r--app/channel_test.go22
-rw-r--r--app/permissions.go159
-rw-r--r--app/permissions_test.go253
-rw-r--r--app/scheme.go168
-rw-r--r--app/team.go96
-rw-r--r--app/team_test.go18
-rw-r--r--cmd/mattermost/commands/init.go1
-rw-r--r--cmd/mattermost/commands/permissions.go70
-rw-r--r--cmd/mattermost/commands/permissions_test.go40
-rw-r--r--cmd/mattermost/commands/roles.go46
-rw-r--r--cmd/mattermost/commands/roles_test.go15
-rw-r--r--cmd/mattermost/commands/server.go1
-rw-r--r--einterfaces/emoji.go12
-rw-r--r--i18n/en.json172
-rw-r--r--imports/placeholder.go4
-rw-r--r--jobs/interfaces/migrations_interface.go11
-rw-r--r--jobs/jobs.go7
-rw-r--r--jobs/jobs_watcher.go7
-rw-r--r--jobs/schedulers.go4
-rw-r--r--jobs/server.go2
-rw-r--r--jobs/workers.go13
-rw-r--r--migrations/advanced_permissions_phase_2.go106
-rw-r--r--migrations/migrations.go63
-rw-r--r--migrations/migrations_test.go140
-rw-r--r--migrations/migrationstestlib.go425
-rw-r--r--migrations/scheduler.go110
-rw-r--r--migrations/worker.go166
-rw-r--r--model/channel.go29
-rw-r--r--model/channel_member.go19
-rw-r--r--model/client4.go110
-rw-r--r--model/cluster_message.go1
-rw-r--r--model/job.go2
-rw-r--r--model/license.go6
-rw-r--r--model/license_test.go10
-rw-r--r--model/migration.go8
-rw-r--r--model/permission.go16
-rw-r--r--model/role.go22
-rw-r--r--model/scheme.go192
-rw-r--r--model/team.go29
-rw-r--r--model/team_member.go11
-rw-r--r--store/layered_store.go46
-rw-r--r--store/layered_store_supplier.go8
-rw-r--r--store/local_cache_supplier.go5
-rw-r--r--store/local_cache_supplier_roles.go11
-rw-r--r--store/local_cache_supplier_schemes.go54
-rw-r--r--store/redis_supplier_roles.go15
-rw-r--r--store/redis_supplier_schemes.go35
-rw-r--r--store/sqlstore/channel_store.go437
-rw-r--r--store/sqlstore/channel_store_test.go926
-rw-r--r--store/sqlstore/role_supplier.go80
-rw-r--r--store/sqlstore/scheme_store_test.go14
-rw-r--r--store/sqlstore/scheme_supplier.go298
-rw-r--r--store/sqlstore/store.go2
-rw-r--r--store/sqlstore/supplier.go40
-rw-r--r--store/sqlstore/team_store.go267
-rw-r--r--store/sqlstore/team_store_test.go367
-rw-r--r--store/sqlstore/upgrade.go18
-rw-r--r--store/store.go16
-rw-r--r--store/storetest/channel_store.go187
-rw-r--r--store/storetest/mocks/ChannelStore.go48
-rw-r--r--store/storetest/mocks/LayeredStoreDatabaseLayer.go154
-rw-r--r--store/storetest/mocks/LayeredStoreSupplier.go138
-rw-r--r--store/storetest/mocks/RoleStore.go16
-rw-r--r--store/storetest/mocks/SchemeStore.go94
-rw-r--r--store/storetest/mocks/SqlStore.go30
-rw-r--r--store/storetest/mocks/Store.go16
-rw-r--r--store/storetest/mocks/TeamStore.go48
-rw-r--r--store/storetest/role_store.go45
-rw-r--r--store/storetest/scheme_store.go425
-rw-r--r--store/storetest/store.go3
-rw-r--r--store/storetest/team_store.go184
-rw-r--r--utils/config.go2
-rw-r--r--utils/license.go1
-rw-r--r--web/context.go11
-rw-r--r--web/params.go8
-rw-r--r--web/web_test.go1
93 files changed, 8286 insertions, 220 deletions
diff --git a/api4/api.go b/api4/api.go
index 918154c0d..02b884db7 100644
--- a/api4/api.go
+++ b/api4/api.go
@@ -97,7 +97,8 @@ type Routes struct {
Reactions *mux.Router // 'api/v4/reactions'
- Roles *mux.Router // 'api/v4/roles'
+ Roles *mux.Router // 'api/v4/roles'
+ Schemes *mux.Router // 'api/v4/schemes'
Emojis *mux.Router // 'api/v4/emoji'
Emoji *mux.Router // 'api/v4/emoji/{emoji_id:[A-Za-z0-9]+}'
@@ -198,6 +199,7 @@ func Init(a *app.App, root *mux.Router) *API {
api.BaseRoutes.OpenGraph = api.BaseRoutes.ApiRoot.PathPrefix("/opengraph").Subrouter()
api.BaseRoutes.Roles = api.BaseRoutes.ApiRoot.PathPrefix("/roles").Subrouter()
+ api.BaseRoutes.Schemes = api.BaseRoutes.ApiRoot.PathPrefix("/schemes").Subrouter()
api.BaseRoutes.Image = api.BaseRoutes.ApiRoot.PathPrefix("/image").Subrouter()
@@ -227,6 +229,7 @@ func Init(a *app.App, root *mux.Router) *API {
api.InitOpenGraph()
api.InitPlugin()
api.InitRole()
+ api.InitScheme()
api.InitImage()
root.Handle("/api/v4/{anything:.*}", http.HandlerFunc(api.Handle404))
diff --git a/api4/apitestlib.go b/api4/apitestlib.go
index 86150e05a..22084a1d6 100644
--- a/api4/apitestlib.go
+++ b/api4/apitestlib.go
@@ -125,6 +125,7 @@ func setupTestHelper(enterprise bool) *TestHelper {
wsapi.Init(th.App, th.App.Srv.WebSocketRouter)
th.App.Srv.Store.MarkSystemRanUnitTests()
th.App.DoAdvancedPermissionsMigration()
+ th.App.DoEmojisPermissionsMigration()
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.EnableOpenServer = true })
@@ -767,7 +768,7 @@ func (me *TestHelper) MakeUserChannelAdmin(user *model.User, channel *model.Chan
if cmr := <-me.App.Srv.Store.Channel().GetMember(channel.Id, user.Id); cmr.Err == nil {
cm := cmr.Data.(*model.ChannelMember)
- cm.Roles = "channel_admin channel_user"
+ cm.SchemeAdmin = true
if sr := <-me.App.Srv.Store.Channel().UpdateMember(cm); sr.Err != nil {
utils.EnableDebugLogForTest()
panic(sr.Err)
@@ -783,28 +784,42 @@ func (me *TestHelper) MakeUserChannelAdmin(user *model.User, channel *model.Chan
func (me *TestHelper) UpdateUserToTeamAdmin(user *model.User, team *model.Team) {
utils.DisableDebugLogForTest()
- tm := &model.TeamMember{TeamId: team.Id, UserId: user.Id, Roles: model.TEAM_USER_ROLE_ID + " " + model.TEAM_ADMIN_ROLE_ID}
- if tmr := <-me.App.Srv.Store.Team().UpdateMember(tm); tmr.Err != nil {
+ if tmr := <-me.App.Srv.Store.Team().GetMember(team.Id, user.Id); tmr.Err == nil {
+ tm := tmr.Data.(*model.TeamMember)
+ tm.SchemeAdmin = true
+ if sr := <-me.App.Srv.Store.Team().UpdateMember(tm); sr.Err != nil {
+ utils.EnableDebugLogForTest()
+ panic(sr.Err)
+ }
+ } else {
utils.EnableDebugLogForTest()
mlog.Error(tmr.Err.Error())
time.Sleep(time.Second)
panic(tmr.Err)
}
+
utils.EnableDebugLogForTest()
}
func (me *TestHelper) UpdateUserToNonTeamAdmin(user *model.User, team *model.Team) {
utils.DisableDebugLogForTest()
- tm := &model.TeamMember{TeamId: team.Id, UserId: user.Id, Roles: model.TEAM_USER_ROLE_ID}
- if tmr := <-me.App.Srv.Store.Team().UpdateMember(tm); tmr.Err != nil {
+ if tmr := <-me.App.Srv.Store.Team().GetMember(team.Id, user.Id); tmr.Err == nil {
+ tm := tmr.Data.(*model.TeamMember)
+ tm.SchemeAdmin = false
+ if sr := <-me.App.Srv.Store.Team().UpdateMember(tm); sr.Err != nil {
+ utils.EnableDebugLogForTest()
+ panic(sr.Err)
+ }
+ } else {
utils.EnableDebugLogForTest()
mlog.Error(tmr.Err.Error())
time.Sleep(time.Second)
panic(tmr.Err)
}
+
utils.EnableDebugLogForTest()
}
diff --git a/api4/channel.go b/api4/channel.go
index 1026a41ad..e5101ada8 100644
--- a/api4/channel.go
+++ b/api4/channel.go
@@ -15,6 +15,7 @@ func (api *API) InitChannel() {
api.BaseRoutes.Channels.Handle("/direct", api.ApiSessionRequired(createDirectChannel)).Methods("POST")
api.BaseRoutes.Channels.Handle("/group", api.ApiSessionRequired(createGroupChannel)).Methods("POST")
api.BaseRoutes.Channels.Handle("/members/{user_id:[A-Za-z0-9]+}/view", api.ApiSessionRequired(viewChannel)).Methods("POST")
+ api.BaseRoutes.Channels.Handle("/{channel_id:[A-Za-z0-9]+}/scheme", api.ApiSessionRequired(updateChannelScheme)).Methods("PUT")
api.BaseRoutes.ChannelsForTeam.Handle("", api.ApiSessionRequired(getPublicChannelsForTeam)).Methods("GET")
api.BaseRoutes.ChannelsForTeam.Handle("/deleted", api.ApiSessionRequired(getDeletedChannelsForTeam)).Methods("GET")
@@ -946,3 +947,53 @@ func removeChannelMember(c *Context, w http.ResponseWriter, r *http.Request) {
ReturnStatusOK(w)
}
+
+func updateChannelScheme(c *Context, w http.ResponseWriter, r *http.Request) {
+ c.RequireChannelId()
+ if c.Err != nil {
+ return
+ }
+
+ schemeID := model.SchemeIDFromJson(r.Body)
+ if schemeID == nil || len(*schemeID) != 26 {
+ c.SetInvalidParam("scheme_id")
+ return
+ }
+
+ if c.App.License() == nil {
+ c.Err = model.NewAppError("Api4.UpdateChannelScheme", "api.channel.update_channel_scheme.license.error", nil, "", http.StatusNotImplemented)
+ return
+ }
+
+ if !c.App.SessionHasPermissionToChannel(c.Session, c.Params.ChannelId, model.PERMISSION_MANAGE_SYSTEM) {
+ c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM)
+ return
+ }
+
+ scheme, err := c.App.GetScheme(*schemeID)
+ if err != nil {
+ c.Err = err
+ return
+ }
+
+ if scheme.Scope != model.SCHEME_SCOPE_CHANNEL {
+ c.Err = model.NewAppError("Api4.UpdateChannelScheme", "api.channel.update_channel_scheme.scheme_scope.error", nil, "", http.StatusBadRequest)
+ return
+ }
+
+ channel, err := c.App.GetChannel(c.Params.ChannelId)
+ if err != nil {
+ c.Err = err
+ return
+ }
+
+ channel.SchemeId = &scheme.Id
+
+ _, err = c.App.UpdateChannelScheme(channel)
+ if err != nil {
+ c.Err = err
+ return
+ }
+
+ ReturnStatusOK(w)
+}
diff --git a/api4/channel_test.go b/api4/channel_test.go
index 2a1e78753..7b677f77f 100644
--- a/api4/channel_test.go
+++ b/api4/channel_test.go
@@ -1463,7 +1463,7 @@ func TestUpdateChannelRoles(t *testing.T) {
defer th.TearDown()
Client := th.Client
- const CHANNEL_ADMIN = "channel_admin channel_user"
+ const CHANNEL_ADMIN = "channel_user channel_admin"
const CHANNEL_MEMBER = "channel_user"
// User 1 creates a channel, making them channel admin by default.
@@ -1914,3 +1914,79 @@ func TestAutocompleteChannels(t *testing.T) {
}
}
}
+
+func TestUpdateChannelScheme(t *testing.T) {
+ th := Setup().InitBasic().InitSystemAdmin()
+ defer th.TearDown()
+
+ th.App.SetLicense(model.NewTestLicense(""))
+
+ th.App.SetPhase2PermissionsMigrationStatus(true)
+
+ team := &model.Team{
+ DisplayName: "Name",
+ Description: "Some description",
+ CompanyName: "Some company name",
+ AllowOpenInvite: false,
+ InviteId: "inviteid0",
+ Name: "z-z-" + model.NewId() + "a",
+ Email: "success+" + model.NewId() + "@simulator.amazonses.com",
+ Type: model.TEAM_OPEN,
+ }
+ team, _ = th.SystemAdminClient.CreateTeam(team)
+
+ channel := &model.Channel{
+ DisplayName: "Name",
+ Name: "z-z-" + model.NewId() + "a",
+ Type: model.CHANNEL_OPEN,
+ TeamId: team.Id,
+ }
+ channel, _ = th.SystemAdminClient.CreateChannel(channel)
+
+ channelScheme := &model.Scheme{
+ DisplayName: "DisplayName",
+ Name: model.NewId(),
+ Description: "Some description",
+ Scope: model.SCHEME_SCOPE_CHANNEL,
+ }
+ channelScheme, _ = th.SystemAdminClient.CreateScheme(channelScheme)
+ teamScheme := &model.Scheme{
+ DisplayName: "DisplayName",
+ Name: model.NewId(),
+ Description: "Some description",
+ Scope: model.SCHEME_SCOPE_TEAM,
+ }
+ teamScheme, _ = th.SystemAdminClient.CreateScheme(teamScheme)
+
+ // Test the setup/base case.
+ _, resp := th.SystemAdminClient.UpdateChannelScheme(channel.Id, channelScheme.Id)
+ CheckNoError(t, resp)
+
+ // Test various invalid channel and scheme id combinations.
+ _, resp = th.SystemAdminClient.UpdateChannelScheme(channel.Id, "x")
+ CheckBadRequestStatus(t, resp)
+ _, resp = th.SystemAdminClient.UpdateChannelScheme("x", channelScheme.Id)
+ CheckBadRequestStatus(t, resp)
+ _, resp = th.SystemAdminClient.UpdateChannelScheme("x", "x")
+ CheckBadRequestStatus(t, resp)
+
+ // Test that permissions are required.
+ _, resp = th.Client.UpdateChannelScheme(channel.Id, channelScheme.Id)
+ CheckForbiddenStatus(t, resp)
+
+ // Test that a license is requried.
+ th.App.SetLicense(nil)
+ _, resp = th.SystemAdminClient.UpdateChannelScheme(channel.Id, channelScheme.Id)
+ CheckNotImplementedStatus(t, resp)
+ th.App.SetLicense(model.NewTestLicense(""))
+
+ // Test an invalid scheme scope.
+ _, resp = th.SystemAdminClient.UpdateChannelScheme(channel.Id, teamScheme.Id)
+ fmt.Printf("resp: %+v\n", resp)
+ CheckBadRequestStatus(t, resp)
+
+ // Test that an unauthenticated user gets rejected.
+ th.SystemAdminClient.Logout()
+ _, resp = th.SystemAdminClient.UpdateChannelScheme(channel.Id, channelScheme.Id)
+ CheckUnauthorizedStatus(t, resp)
+}
diff --git a/api4/emoji.go b/api4/emoji.go
index cfb5dd6ab..42f66a22a 100644
--- a/api4/emoji.go
+++ b/api4/emoji.go
@@ -33,12 +33,6 @@ func createEmoji(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if emojiInterface := c.App.Emoji; emojiInterface != nil &&
- !emojiInterface.CanUserCreateEmoji(c.Session.Roles, c.Session.TeamMembers) {
- c.Err = model.NewAppError("getEmoji", "api.emoji.disabled.app_error", nil, "user_id="+c.Session.UserId, http.StatusUnauthorized)
- return
- }
-
if len(*c.App.Config().FileSettings.DriverName) == 0 {
c.Err = model.NewAppError("createEmoji", "api.emoji.storage.app_error", nil, "", http.StatusNotImplemented)
return
@@ -54,6 +48,28 @@ func createEmoji(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
+ // Allow any user with MANAGE_EMOJIS permission at Team level to manage emojis at system level
+ memberships, err := c.App.GetTeamMembersForUser(c.Session.UserId)
+
+ if err != nil {
+ c.Err = err
+ return
+ }
+
+ if !c.App.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_EMOJIS) {
+ hasPermission := false
+ for _, membership := range memberships {
+ if c.App.SessionHasPermissionToTeam(c.Session, membership.TeamId, model.PERMISSION_MANAGE_EMOJIS) {
+ hasPermission = true
+ break
+ }
+ }
+ if !hasPermission {
+ c.SetPermissionError(model.PERMISSION_MANAGE_EMOJIS)
+ return
+ }
+ }
+
m := r.MultipartForm
props := m.Value
@@ -110,11 +126,45 @@ func deleteEmoji(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if c.Session.UserId != emoji.CreatorId && !c.App.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) {
- c.Err = model.NewAppError("deleteImage", "api.emoji.delete.permissions.app_error", nil, "user_id="+c.Session.UserId, http.StatusUnauthorized)
+ // Allow any user with MANAGE_EMOJIS permission at Team level to manage emojis at system level
+ memberships, err := c.App.GetTeamMembersForUser(c.Session.UserId)
+
+ if err != nil {
+ c.Err = err
return
}
+ if !c.App.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_EMOJIS) {
+ hasPermission := false
+ for _, membership := range memberships {
+ if c.App.SessionHasPermissionToTeam(c.Session, membership.TeamId, model.PERMISSION_MANAGE_EMOJIS) {
+ hasPermission = true
+ break
+ }
+ }
+ if !hasPermission {
+ c.SetPermissionError(model.PERMISSION_MANAGE_EMOJIS)
+ return
+ }
+ }
+
+ if c.Session.UserId != emoji.CreatorId {
+ if !c.App.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_OTHERS_EMOJIS) {
+ hasPermission := false
+ for _, membership := range memberships {
+ if c.App.SessionHasPermissionToTeam(c.Session, membership.TeamId, model.PERMISSION_MANAGE_OTHERS_EMOJIS) {
+ hasPermission = true
+ break
+ }
+ }
+
+ if !hasPermission {
+ c.SetPermissionError(model.PERMISSION_MANAGE_OTHERS_EMOJIS)
+ return
+ }
+ }
+ }
+
err = c.App.DeleteEmoji(emoji)
if err != nil {
c.Err = err
diff --git a/api4/emoji_test.go b/api4/emoji_test.go
index 39da4aaef..cb6398312 100644
--- a/api4/emoji_test.go
+++ b/api4/emoji_test.go
@@ -26,6 +26,11 @@ func TestCreateEmoji(t *testing.T) {
}()
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCustomEmoji = false })
+ defaultRolePermissions := th.SaveDefaultRolePermissions()
+ defer func() {
+ th.RestoreDefaultRolePermissions(defaultRolePermissions)
+ }()
+
emoji := &model.Emoji{
CreatorId: th.BasicUser.Id,
Name: model.NewId(),
@@ -141,6 +146,28 @@ func TestCreateEmoji(t *testing.T) {
_, resp = Client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 10), "image.gif")
CheckForbiddenStatus(t, resp)
+
+ // try to create an emoji without permissions
+ th.RemovePermissionFromRole(model.PERMISSION_MANAGE_EMOJIS.Id, model.SYSTEM_USER_ROLE_ID)
+
+ emoji = &model.Emoji{
+ CreatorId: th.BasicUser.Id,
+ Name: model.NewId(),
+ }
+
+ _, resp = Client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 10), "image.gif")
+ CheckForbiddenStatus(t, resp)
+
+ // create an emoji with permissions in one team
+ th.AddPermissionToRole(model.PERMISSION_MANAGE_EMOJIS.Id, model.TEAM_USER_ROLE_ID)
+
+ emoji = &model.Emoji{
+ CreatorId: th.BasicUser.Id,
+ Name: model.NewId(),
+ }
+
+ _, resp = Client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 10), "image.gif")
+ CheckNoError(t, resp)
}
func TestGetEmojiList(t *testing.T) {
@@ -186,7 +213,7 @@ func TestGetEmojiList(t *testing.T) {
}
}
if !found {
- t.Fatalf("failed to get emoji with id %v", emoji.Id)
+ t.Fatalf("failed to get emoji with id %v, %v", emoji.Id, len(listEmoji))
}
}
@@ -231,6 +258,11 @@ func TestDeleteEmoji(t *testing.T) {
}()
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCustomEmoji = true })
+ defaultRolePermissions := th.SaveDefaultRolePermissions()
+ defer func() {
+ th.RestoreDefaultRolePermissions(defaultRolePermissions)
+ }()
+
emoji := &model.Emoji{
CreatorId: th.BasicUser.Id,
Name: model.NewId(),
@@ -277,14 +309,100 @@ func TestDeleteEmoji(t *testing.T) {
_, resp = Client.DeleteEmoji("")
CheckNotFoundStatus(t, resp)
- //Try to delete other user's custom emoji
+ //Try to delete my custom emoji without permissions
+ newEmoji, resp = Client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 10), "image.gif")
+ CheckNoError(t, resp)
+
+ th.RemovePermissionFromRole(model.PERMISSION_MANAGE_EMOJIS.Id, model.SYSTEM_USER_ROLE_ID)
+ _, resp = Client.DeleteEmoji(newEmoji.Id)
+ CheckForbiddenStatus(t, resp)
+ th.AddPermissionToRole(model.PERMISSION_MANAGE_EMOJIS.Id, model.SYSTEM_USER_ROLE_ID)
+
+ //Try to delete other user's custom emoji without MANAGE_EMOJIS permissions
+ emoji = &model.Emoji{
+ CreatorId: th.BasicUser.Id,
+ Name: model.NewId(),
+ }
+
newEmoji, resp = Client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 10), "image.gif")
CheckNoError(t, resp)
+ th.RemovePermissionFromRole(model.PERMISSION_MANAGE_EMOJIS.Id, model.SYSTEM_USER_ROLE_ID)
+ th.AddPermissionToRole(model.PERMISSION_MANAGE_OTHERS_EMOJIS.Id, model.SYSTEM_USER_ROLE_ID)
Client.Logout()
th.LoginBasic2()
ok, resp = Client.DeleteEmoji(newEmoji.Id)
- CheckUnauthorizedStatus(t, resp)
+ CheckForbiddenStatus(t, resp)
+ th.RemovePermissionFromRole(model.PERMISSION_MANAGE_OTHERS_EMOJIS.Id, model.SYSTEM_USER_ROLE_ID)
+ th.AddPermissionToRole(model.PERMISSION_MANAGE_EMOJIS.Id, model.SYSTEM_USER_ROLE_ID)
+ Client.Logout()
+ th.LoginBasic()
+
+ //Try to delete other user's custom emoji without MANAGE_OTHERS_EMOJIS permissions
+ emoji = &model.Emoji{
+ CreatorId: th.BasicUser.Id,
+ Name: model.NewId(),
+ }
+
+ newEmoji, resp = Client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 10), "image.gif")
+ CheckNoError(t, resp)
+
+ Client.Logout()
+ th.LoginBasic2()
+ ok, resp = Client.DeleteEmoji(newEmoji.Id)
+ CheckForbiddenStatus(t, resp)
+ Client.Logout()
+ th.LoginBasic()
+
+ //Try to delete other user's custom emoji with permissions
+ emoji = &model.Emoji{
+ CreatorId: th.BasicUser.Id,
+ Name: model.NewId(),
+ }
+
+ newEmoji, resp = Client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 10), "image.gif")
+ CheckNoError(t, resp)
+
+ th.AddPermissionToRole(model.PERMISSION_MANAGE_EMOJIS.Id, model.SYSTEM_USER_ROLE_ID)
+ th.AddPermissionToRole(model.PERMISSION_MANAGE_OTHERS_EMOJIS.Id, model.SYSTEM_USER_ROLE_ID)
+ Client.Logout()
+ th.LoginBasic2()
+ ok, resp = Client.DeleteEmoji(newEmoji.Id)
+ CheckNoError(t, resp)
+
+ Client.Logout()
+ th.LoginBasic()
+
+ //Try to delete my custom emoji with permissions at team level
+ newEmoji, resp = Client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 10), "image.gif")
+ CheckNoError(t, resp)
+
+ th.RemovePermissionFromRole(model.PERMISSION_MANAGE_EMOJIS.Id, model.SYSTEM_USER_ROLE_ID)
+ th.AddPermissionToRole(model.PERMISSION_MANAGE_EMOJIS.Id, model.TEAM_USER_ROLE_ID)
+ _, resp = Client.DeleteEmoji(newEmoji.Id)
+ CheckNoError(t, resp)
+ th.AddPermissionToRole(model.PERMISSION_MANAGE_EMOJIS.Id, model.SYSTEM_USER_ROLE_ID)
+ th.RemovePermissionFromRole(model.PERMISSION_MANAGE_EMOJIS.Id, model.TEAM_USER_ROLE_ID)
+
+ //Try to delete other user's custom emoji with permissions at team level
+ emoji = &model.Emoji{
+ CreatorId: th.BasicUser.Id,
+ Name: model.NewId(),
+ }
+
+ newEmoji, resp = Client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 10), "image.gif")
+ CheckNoError(t, resp)
+
+ th.RemovePermissionFromRole(model.PERMISSION_MANAGE_EMOJIS.Id, model.SYSTEM_USER_ROLE_ID)
+ th.RemovePermissionFromRole(model.PERMISSION_MANAGE_OTHERS_EMOJIS.Id, model.SYSTEM_USER_ROLE_ID)
+
+ th.AddPermissionToRole(model.PERMISSION_MANAGE_EMOJIS.Id, model.TEAM_USER_ROLE_ID)
+ th.AddPermissionToRole(model.PERMISSION_MANAGE_OTHERS_EMOJIS.Id, model.TEAM_USER_ROLE_ID)
+
+ Client.Logout()
+ th.LoginBasic2()
+ ok, resp = Client.DeleteEmoji(newEmoji.Id)
+ CheckNoError(t, resp)
}
func TestGetEmoji(t *testing.T) {
diff --git a/api4/post.go b/api4/post.go
index 189edfc20..b4392a74e 100644
--- a/api4/post.go
+++ b/api4/post.go
@@ -246,11 +246,24 @@ func deletePost(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if !c.App.SessionHasPermissionToPost(c.Session, c.Params.PostId, model.PERMISSION_DELETE_OTHERS_POSTS) {
- c.SetPermissionError(model.PERMISSION_DELETE_OTHERS_POSTS)
+ post, err := c.App.GetSinglePost(c.Params.PostId)
+ if err != nil {
+ c.SetPermissionError(model.PERMISSION_DELETE_POST)
return
}
+ if c.Session.UserId == post.UserId {
+ if !c.App.SessionHasPermissionToChannel(c.Session, post.ChannelId, model.PERMISSION_DELETE_POST) {
+ c.SetPermissionError(model.PERMISSION_DELETE_POST)
+ return
+ }
+ } else {
+ if !c.App.SessionHasPermissionToChannel(c.Session, post.ChannelId, model.PERMISSION_DELETE_OTHERS_POSTS) {
+ c.SetPermissionError(model.PERMISSION_DELETE_OTHERS_POSTS)
+ return
+ }
+ }
+
if _, err := c.App.DeletePost(c.Params.PostId); err != nil {
c.Err = err
return
@@ -364,11 +377,19 @@ func updatePost(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if !c.App.SessionHasPermissionToPost(c.Session, c.Params.PostId, model.PERMISSION_EDIT_OTHERS_POSTS) {
- c.SetPermissionError(model.PERMISSION_EDIT_OTHERS_POSTS)
+ originalPost, err := c.App.GetSinglePost(c.Params.PostId)
+ if err != nil {
+ c.SetPermissionError(model.PERMISSION_EDIT_POST)
return
}
+ if c.Session.UserId != originalPost.UserId {
+ if !c.App.SessionHasPermissionToChannelByPost(c.Session, c.Params.PostId, model.PERMISSION_EDIT_OTHERS_POSTS) {
+ c.SetPermissionError(model.PERMISSION_EDIT_OTHERS_POSTS)
+ return
+ }
+ }
+
post.Id = c.Params.PostId
rpost, err := c.App.UpdatePost(c.App.PostWithProxyRemovedFromImageURLs(post), false)
@@ -398,11 +419,19 @@ func patchPost(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if !c.App.SessionHasPermissionToPost(c.Session, c.Params.PostId, model.PERMISSION_EDIT_OTHERS_POSTS) {
- c.SetPermissionError(model.PERMISSION_EDIT_OTHERS_POSTS)
+ originalPost, err := c.App.GetSinglePost(c.Params.PostId)
+ if err != nil {
+ c.SetPermissionError(model.PERMISSION_EDIT_POST)
return
}
+ if c.Session.UserId != originalPost.UserId {
+ if !c.App.SessionHasPermissionToChannelByPost(c.Session, c.Params.PostId, model.PERMISSION_EDIT_OTHERS_POSTS) {
+ c.SetPermissionError(model.PERMISSION_EDIT_OTHERS_POSTS)
+ return
+ }
+ }
+
patchedPost, err := c.App.PatchPost(c.Params.PostId, c.App.PostPatchWithProxyRemovedFromImageURLs(post))
if err != nil {
c.Err = err
diff --git a/api4/role.go b/api4/role.go
index c4203137b..2c0465891 100644
--- a/api4/role.go
+++ b/api4/role.go
@@ -100,6 +100,7 @@ func patchRole(c *Context, w http.ResponseWriter, r *http.Request) {
model.PERMISSION_MANAGE_SLASH_COMMANDS.Id,
model.PERMISSION_MANAGE_OAUTH.Id,
model.PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH.Id,
+ model.PERMISSION_MANAGE_EMOJIS.Id,
}
changedPermissions := model.PermissionsChangedByPatch(oldRole, patch)
diff --git a/api4/scheme.go b/api4/scheme.go
new file mode 100644
index 000000000..5070d1c4a
--- /dev/null
+++ b/api4/scheme.go
@@ -0,0 +1,211 @@
+// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package api4
+
+import (
+ "net/http"
+
+ "github.com/mattermost/mattermost-server/model"
+)
+
+func (api *API) InitScheme() {
+ api.BaseRoutes.Schemes.Handle("", api.ApiSessionRequired(getSchemes)).Methods("GET")
+ api.BaseRoutes.Schemes.Handle("", api.ApiSessionRequired(createScheme)).Methods("POST")
+ api.BaseRoutes.Schemes.Handle("/{scheme_id:[A-Za-z0-9]+}", api.ApiSessionRequired(deleteScheme)).Methods("DELETE")
+ api.BaseRoutes.Schemes.Handle("/{scheme_id:[A-Za-z0-9]+}", api.ApiSessionRequiredTrustRequester(getScheme)).Methods("GET")
+ api.BaseRoutes.Schemes.Handle("/{scheme_id:[A-Za-z0-9]+}/patch", api.ApiSessionRequired(patchScheme)).Methods("PUT")
+ api.BaseRoutes.Schemes.Handle("/{scheme_id:[A-Za-z0-9]+}/teams", api.ApiSessionRequiredTrustRequester(getTeamsForScheme)).Methods("GET")
+ api.BaseRoutes.Schemes.Handle("/{scheme_id:[A-Za-z0-9]+}/channels", api.ApiSessionRequiredTrustRequester(getChannelsForScheme)).Methods("GET")
+}
+
+func createScheme(c *Context, w http.ResponseWriter, r *http.Request) {
+ scheme := model.SchemeFromJson(r.Body)
+ if scheme == nil {
+ c.SetInvalidParam("scheme")
+ return
+ }
+
+ if c.App.License() == nil || !*c.App.License().Features.CustomPermissionsSchemes {
+ c.Err = model.NewAppError("Api4.CreateScheme", "api.scheme.create_scheme.license.error", nil, "", http.StatusNotImplemented)
+ return
+ }
+
+ if !c.App.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) {
+ c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM)
+ return
+ }
+
+ var err *model.AppError
+ if scheme, err = c.App.CreateScheme(scheme); err != nil {
+ c.Err = err
+ return
+ } else {
+ w.WriteHeader(http.StatusCreated)
+ w.Write([]byte(scheme.ToJson()))
+ }
+}
+
+func getScheme(c *Context, w http.ResponseWriter, r *http.Request) {
+ c.RequireSchemeId()
+ if c.Err != nil {
+ return
+ }
+
+ if !c.App.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) {
+ c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM)
+ return
+ }
+
+ if scheme, err := c.App.GetScheme(c.Params.SchemeId); err != nil {
+ c.Err = err
+ return
+ } else {
+ w.Write([]byte(scheme.ToJson()))
+ }
+}
+
+func getSchemes(c *Context, w http.ResponseWriter, r *http.Request) {
+ if c.Err != nil {
+ return
+ }
+
+ if !c.App.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) {
+ c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM)
+ return
+ }
+
+ scope := c.Params.Scope
+ if scope != "" && scope != model.SCHEME_SCOPE_TEAM && scope != model.SCHEME_SCOPE_CHANNEL {
+ c.SetInvalidParam("scope")
+ return
+ }
+
+ if schemes, err := c.App.GetSchemesPage(c.Params.Scope, c.Params.Page, c.Params.PerPage); err != nil {
+ c.Err = err
+ return
+ } else {
+ w.Write([]byte(model.SchemesToJson(schemes)))
+ }
+}
+
+func getTeamsForScheme(c *Context, w http.ResponseWriter, r *http.Request) {
+ c.RequireSchemeId()
+ if c.Err != nil {
+ return
+ }
+
+ if !c.App.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) {
+ c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM)
+ return
+ }
+
+ scheme, err := c.App.GetScheme(c.Params.SchemeId)
+ if err != nil {
+ c.Err = err
+ return
+ }
+
+ if scheme.Scope != model.SCHEME_SCOPE_TEAM {
+ c.Err = model.NewAppError("Api4.GetTeamsForScheme", "api.scheme.get_teams_for_scheme.scope.error", nil, "", http.StatusBadRequest)
+ return
+ }
+
+ if teams, err := c.App.GetTeamsForSchemePage(scheme, c.Params.Page, c.Params.PerPage); err != nil {
+ c.Err = err
+ return
+ } else {
+ w.Write([]byte(model.TeamListToJson(teams)))
+ }
+}
+
+func getChannelsForScheme(c *Context, w http.ResponseWriter, r *http.Request) {
+ c.RequireSchemeId()
+ if c.Err != nil {
+ return
+ }
+
+ if !c.App.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) {
+ c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM)
+ return
+ }
+
+ scheme, err := c.App.GetScheme(c.Params.SchemeId)
+ if err != nil {
+ c.Err = err
+ return
+ }
+
+ if scheme.Scope != model.SCHEME_SCOPE_CHANNEL {
+ c.Err = model.NewAppError("Api4.GetChannelsForScheme", "api.scheme.get_channels_for_scheme.scope.error", nil, "", http.StatusBadRequest)
+ return
+ }
+
+ if channels, err := c.App.GetChannelsForSchemePage(scheme, c.Params.Page, c.Params.PerPage); err != nil {
+ c.Err = err
+ return
+ } else {
+ w.Write([]byte(channels.ToJson()))
+ }
+}
+
+func patchScheme(c *Context, w http.ResponseWriter, r *http.Request) {
+ c.RequireSchemeId()
+ if c.Err != nil {
+ return
+ }
+
+ patch := model.SchemePatchFromJson(r.Body)
+ if patch == nil {
+ c.SetInvalidParam("scheme")
+ return
+ }
+
+ if c.App.License() == nil || !*c.App.License().Features.CustomPermissionsSchemes {
+ c.Err = model.NewAppError("Api4.PatchScheme", "api.scheme.patch_scheme.license.error", nil, "", http.StatusNotImplemented)
+ return
+ }
+
+ scheme, err := c.App.GetScheme(c.Params.SchemeId)
+ if err != nil {
+ c.Err = err
+ return
+ }
+
+ if !c.App.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) {
+ c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM)
+ return
+ }
+
+ if scheme, err = c.App.PatchScheme(scheme, patch); err != nil {
+ c.Err = err
+ return
+ } else {
+ c.LogAudit("")
+ w.Write([]byte(scheme.ToJson()))
+ }
+}
+
+func deleteScheme(c *Context, w http.ResponseWriter, r *http.Request) {
+ c.RequireSchemeId()
+ if c.Err != nil {
+ return
+ }
+
+ if c.App.License() == nil || !*c.App.License().Features.CustomPermissionsSchemes {
+ c.Err = model.NewAppError("Api4.DeleteScheme", "api.scheme.delete_scheme.license.error", nil, "", http.StatusNotImplemented)
+ return
+ }
+
+ if !c.App.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) {
+ c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM)
+ return
+ }
+
+ if _, err := c.App.DeleteScheme(c.Params.SchemeId); err != nil {
+ c.Err = err
+ return
+ }
+
+ ReturnStatusOK(w)
+}
diff --git a/api4/scheme_test.go b/api4/scheme_test.go
new file mode 100644
index 000000000..67cfda4fc
--- /dev/null
+++ b/api4/scheme_test.go
@@ -0,0 +1,737 @@
+// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package api4
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+ "github.com/mattermost/mattermost-server/model"
+)
+
+func TestCreateScheme(t *testing.T) {
+ th := Setup().InitBasic().InitSystemAdmin()
+ defer th.TearDown()
+
+ th.App.SetLicense(model.NewTestLicense("custom_permissions_schemes"))
+
+ th.App.SetPhase2PermissionsMigrationStatus(true)
+
+ // Basic test of creating a team scheme.
+ scheme1 := &model.Scheme{
+ DisplayName: model.NewId(),
+ Name: model.NewId(),
+ Description: model.NewId(),
+ Scope: model.SCHEME_SCOPE_TEAM,
+ }
+
+ s1, r1 := th.SystemAdminClient.CreateScheme(scheme1)
+ CheckNoError(t, r1)
+
+ assert.Equal(t, s1.DisplayName, scheme1.DisplayName)
+ assert.Equal(t, s1.Name, scheme1.Name)
+ assert.Equal(t, s1.Description, scheme1.Description)
+ assert.NotZero(t, s1.CreateAt)
+ assert.Equal(t, s1.CreateAt, s1.UpdateAt)
+ assert.Zero(t, s1.DeleteAt)
+ assert.Equal(t, s1.Scope, scheme1.Scope)
+ assert.NotZero(t, len(s1.DefaultTeamAdminRole))
+ assert.NotZero(t, len(s1.DefaultTeamUserRole))
+ assert.NotZero(t, len(s1.DefaultChannelAdminRole))
+ assert.NotZero(t, len(s1.DefaultChannelUserRole))
+
+ // Check the default roles have been created.
+ _, roleRes1 := th.SystemAdminClient.GetRole(s1.DefaultTeamAdminRole)
+ CheckNoError(t, roleRes1)
+ _, roleRes2 := th.SystemAdminClient.GetRole(s1.DefaultTeamUserRole)
+ CheckNoError(t, roleRes2)
+ _, roleRes3 := th.SystemAdminClient.GetRole(s1.DefaultChannelAdminRole)
+ CheckNoError(t, roleRes3)
+ _, roleRes4 := th.SystemAdminClient.GetRole(s1.DefaultChannelUserRole)
+ CheckNoError(t, roleRes4)
+
+ // Basic Test of a Channel scheme.
+ scheme2 := &model.Scheme{
+ DisplayName: model.NewId(),
+ Name: model.NewId(),
+ Description: model.NewId(),
+ Scope: model.SCHEME_SCOPE_CHANNEL,
+ }
+
+ s2, r2 := th.SystemAdminClient.CreateScheme(scheme2)
+ CheckNoError(t, r2)
+
+ assert.Equal(t, s2.DisplayName, scheme2.DisplayName)
+ assert.Equal(t, s2.Name, scheme2.Name)
+ assert.Equal(t, s2.Description, scheme2.Description)
+ assert.NotZero(t, s2.CreateAt)
+ assert.Equal(t, s2.CreateAt, s2.UpdateAt)
+ assert.Zero(t, s2.DeleteAt)
+ assert.Equal(t, s2.Scope, scheme2.Scope)
+ assert.Zero(t, len(s2.DefaultTeamAdminRole))
+ assert.Zero(t, len(s2.DefaultTeamUserRole))
+ assert.NotZero(t, len(s2.DefaultChannelAdminRole))
+ assert.NotZero(t, len(s2.DefaultChannelUserRole))
+
+ // Check the default roles have been created.
+ _, roleRes5 := th.SystemAdminClient.GetRole(s2.DefaultChannelAdminRole)
+ CheckNoError(t, roleRes5)
+ _, roleRes6 := th.SystemAdminClient.GetRole(s2.DefaultChannelUserRole)
+ CheckNoError(t, roleRes6)
+
+ // Try and create a scheme with an invalid scope.
+ scheme3 := &model.Scheme{
+ DisplayName: model.NewId(),
+ Name: model.NewId(),
+ Description: model.NewId(),
+ Scope: model.NewId(),
+ }
+
+ _, r3 := th.SystemAdminClient.CreateScheme(scheme3)
+ CheckBadRequestStatus(t, r3)
+
+ // Try and create a scheme with an invalid display name.
+ scheme4 := &model.Scheme{
+ DisplayName: strings.Repeat(model.NewId(), 100),
+ Name: "Name",
+ Description: model.NewId(),
+ Scope: model.NewId(),
+ }
+ _, r4 := th.SystemAdminClient.CreateScheme(scheme4)
+ CheckBadRequestStatus(t, r4)
+
+ // Try and create a scheme with an invalid name.
+ scheme8 := &model.Scheme{
+ DisplayName: "DisplayName",
+ Name: strings.Repeat(model.NewId(), 100),
+ Description: model.NewId(),
+ Scope: model.NewId(),
+ }
+ _, r8 := th.SystemAdminClient.CreateScheme(scheme8)
+ CheckBadRequestStatus(t, r8)
+
+ // Try and create a scheme without the appropriate permissions.
+ scheme5 := &model.Scheme{
+ DisplayName: model.NewId(),
+ Name: model.NewId(),
+ Description: model.NewId(),
+ Scope: model.SCHEME_SCOPE_TEAM,
+ }
+ _, r5 := th.Client.CreateScheme(scheme5)
+ CheckForbiddenStatus(t, r5)
+
+ // Try and create a scheme without a license.
+ th.App.SetLicense(nil)
+ scheme6 := &model.Scheme{
+ DisplayName: model.NewId(),
+ Name: model.NewId(),
+ Description: model.NewId(),
+ Scope: model.SCHEME_SCOPE_TEAM,
+ }
+ _, r6 := th.SystemAdminClient.CreateScheme(scheme6)
+ CheckNotImplementedStatus(t, r6)
+
+ th.App.SetPhase2PermissionsMigrationStatus(false)
+
+ th.LoginSystemAdmin()
+ th.App.SetLicense(model.NewTestLicense("custom_permissions_schemes"))
+
+ scheme7 := &model.Scheme{
+ DisplayName: model.NewId(),
+ Name: model.NewId(),
+ Description: model.NewId(),
+ Scope: model.SCHEME_SCOPE_TEAM,
+ }
+ _, r7 := th.SystemAdminClient.CreateScheme(scheme7)
+ CheckNotImplementedStatus(t, r7)
+}
+
+func TestGetScheme(t *testing.T) {
+ th := Setup().InitBasic().InitSystemAdmin()
+ defer th.TearDown()
+
+ th.App.SetLicense(model.NewTestLicense("custom_permissions_schemes"))
+
+ // Basic test of creating a team scheme.
+ scheme1 := &model.Scheme{
+ DisplayName: model.NewId(),
+ Name: model.NewId(),
+ Description: model.NewId(),
+ Scope: model.SCHEME_SCOPE_TEAM,
+ }
+
+ th.App.SetPhase2PermissionsMigrationStatus(true)
+
+ s1, r1 := th.SystemAdminClient.CreateScheme(scheme1)
+ CheckNoError(t, r1)
+
+ assert.Equal(t, s1.DisplayName, scheme1.DisplayName)
+ assert.Equal(t, s1.Name, scheme1.Name)
+ assert.Equal(t, s1.Description, scheme1.Description)
+ assert.NotZero(t, s1.CreateAt)
+ assert.Equal(t, s1.CreateAt, s1.UpdateAt)
+ assert.Zero(t, s1.DeleteAt)
+ assert.Equal(t, s1.Scope, scheme1.Scope)
+ assert.NotZero(t, len(s1.DefaultTeamAdminRole))
+ assert.NotZero(t, len(s1.DefaultTeamUserRole))
+ assert.NotZero(t, len(s1.DefaultChannelAdminRole))
+ assert.NotZero(t, len(s1.DefaultChannelUserRole))
+
+ s2, r2 := th.SystemAdminClient.GetScheme(s1.Id)
+ CheckNoError(t, r2)
+
+ assert.Equal(t, s1, s2)
+
+ _, r3 := th.SystemAdminClient.GetScheme(model.NewId())
+ CheckNotFoundStatus(t, r3)
+
+ _, r4 := th.SystemAdminClient.GetScheme("12345")
+ CheckBadRequestStatus(t, r4)
+
+ th.SystemAdminClient.Logout()
+ _, r5 := th.SystemAdminClient.GetScheme(s1.Id)
+ CheckUnauthorizedStatus(t, r5)
+
+ th.SystemAdminClient.Login(th.SystemAdminUser.Username, th.SystemAdminUser.Password)
+ th.App.SetLicense(nil)
+ _, r6 := th.SystemAdminClient.GetScheme(s1.Id)
+ CheckNoError(t, r6)
+
+ _, r7 := th.Client.GetScheme(s1.Id)
+ CheckForbiddenStatus(t, r7)
+
+ th.App.SetPhase2PermissionsMigrationStatus(false)
+
+ _, r8 := th.SystemAdminClient.GetScheme(s1.Id)
+ CheckNotImplementedStatus(t, r8)
+}
+
+func TestGetSchemes(t *testing.T) {
+ th := Setup().InitBasic().InitSystemAdmin()
+ defer th.TearDown()
+
+ th.App.SetLicense(model.NewTestLicense("custom_permissions_schemes"))
+
+ scheme1 := &model.Scheme{
+ DisplayName: model.NewId(),
+ Name: model.NewId(),
+ Description: model.NewId(),
+ Scope: model.SCHEME_SCOPE_TEAM,
+ }
+
+ scheme2 := &model.Scheme{
+ DisplayName: model.NewId(),
+ Name: model.NewId(),
+ Description: model.NewId(),
+ Scope: model.SCHEME_SCOPE_CHANNEL,
+ }
+
+ th.App.SetPhase2PermissionsMigrationStatus(true)
+
+ _, r1 := th.SystemAdminClient.CreateScheme(scheme1)
+ CheckNoError(t, r1)
+ _, r2 := th.SystemAdminClient.CreateScheme(scheme2)
+ CheckNoError(t, r2)
+
+ l3, r3 := th.SystemAdminClient.GetSchemes("", 0, 100)
+ CheckNoError(t, r3)
+
+ assert.NotZero(t, len(l3))
+
+ l4, r4 := th.SystemAdminClient.GetSchemes("team", 0, 100)
+ CheckNoError(t, r4)
+
+ for _, s := range l4 {
+ assert.Equal(t, "team", s.Scope)
+ }
+
+ l5, r5 := th.SystemAdminClient.GetSchemes("channel", 0, 100)
+ CheckNoError(t, r5)
+
+ for _, s := range l5 {
+ assert.Equal(t, "channel", s.Scope)
+ }
+
+ _, r6 := th.SystemAdminClient.GetSchemes("asdf", 0, 100)
+ CheckBadRequestStatus(t, r6)
+
+ th.Client.Logout()
+ _, r7 := th.Client.GetSchemes("", 0, 100)
+ CheckUnauthorizedStatus(t, r7)
+
+ th.Client.Login(th.BasicUser.Username, th.BasicUser.Password)
+ _, r8 := th.Client.GetSchemes("", 0, 100)
+ CheckForbiddenStatus(t, r8)
+
+ th.App.SetPhase2PermissionsMigrationStatus(false)
+
+ _, r9 := th.SystemAdminClient.GetSchemes("", 0, 100)
+ CheckNotImplementedStatus(t, r9)
+}
+
+func TestGetTeamsForScheme(t *testing.T) {
+ th := Setup().InitBasic().InitSystemAdmin()
+ defer th.TearDown()
+
+ th.App.SetLicense(model.NewTestLicense("custom_permissions_schemes"))
+
+ th.App.SetPhase2PermissionsMigrationStatus(true)
+
+ scheme1 := &model.Scheme{
+ DisplayName: model.NewId(),
+ Name: model.NewId(),
+ Description: model.NewId(),
+ Scope: model.SCHEME_SCOPE_TEAM,
+ }
+ scheme1, r1 := th.SystemAdminClient.CreateScheme(scheme1)
+ CheckNoError(t, r1)
+
+ team1 := &model.Team{
+ Name: GenerateTestUsername(),
+ DisplayName: "A Test Team",
+ Type: model.TEAM_OPEN,
+ }
+
+ result1 := <-th.App.Srv.Store.Team().Save(team1)
+ assert.Nil(t, result1.Err)
+ team1 = result1.Data.(*model.Team)
+
+ l2, r2 := th.SystemAdminClient.GetTeamsForScheme(scheme1.Id, 0, 100)
+ CheckNoError(t, r2)
+ assert.Zero(t, len(l2))
+
+ team1.SchemeId = &scheme1.Id
+ result2 := <-th.App.Srv.Store.Team().Update(team1)
+ assert.Nil(t, result2.Err)
+ team1 = result2.Data.(*model.Team)
+
+ l3, r3 := th.SystemAdminClient.GetTeamsForScheme(scheme1.Id, 0, 100)
+ CheckNoError(t, r3)
+ assert.Len(t, l3, 1)
+ assert.Equal(t, team1.Id, l3[0].Id)
+
+ team2 := &model.Team{
+ Name: GenerateTestUsername(),
+ DisplayName: "B Test Team",
+ Type: model.TEAM_OPEN,
+ SchemeId: &scheme1.Id,
+ }
+ result3 := <-th.App.Srv.Store.Team().Save(team2)
+ assert.Nil(t, result3.Err)
+ team2 = result3.Data.(*model.Team)
+
+ l4, r4 := th.SystemAdminClient.GetTeamsForScheme(scheme1.Id, 0, 100)
+ CheckNoError(t, r4)
+ assert.Len(t, l4, 2)
+ assert.Equal(t, team1.Id, l4[0].Id)
+ assert.Equal(t, team2.Id, l4[1].Id)
+
+ l5, r5 := th.SystemAdminClient.GetTeamsForScheme(scheme1.Id, 1, 1)
+ CheckNoError(t, r5)
+ assert.Len(t, l5, 1)
+ assert.Equal(t, team2.Id, l5[0].Id)
+
+ // Check various error cases.
+ _, ri1 := th.SystemAdminClient.GetTeamsForScheme(model.NewId(), 0, 100)
+ CheckNotFoundStatus(t, ri1)
+
+ _, ri2 := th.SystemAdminClient.GetTeamsForScheme("", 0, 100)
+ CheckBadRequestStatus(t, ri2)
+
+ th.Client.Logout()
+ _, ri3 := th.Client.GetTeamsForScheme(model.NewId(), 0, 100)
+ CheckUnauthorizedStatus(t, ri3)
+
+ th.Client.Login(th.BasicUser.Username, th.BasicUser.Password)
+ _, ri4 := th.Client.GetTeamsForScheme(model.NewId(), 0, 100)
+ CheckForbiddenStatus(t, ri4)
+
+ scheme2 := &model.Scheme{
+ DisplayName: model.NewId(),
+ Name: model.NewId(),
+ Description: model.NewId(),
+ Scope: model.SCHEME_SCOPE_CHANNEL,
+ }
+ scheme2, rs2 := th.SystemAdminClient.CreateScheme(scheme2)
+ CheckNoError(t, rs2)
+
+ _, ri5 := th.SystemAdminClient.GetTeamsForScheme(scheme2.Id, 0, 100)
+ CheckBadRequestStatus(t, ri5)
+
+ th.App.SetPhase2PermissionsMigrationStatus(false)
+
+ _, ri6 := th.SystemAdminClient.GetTeamsForScheme(scheme1.Id, 0, 100)
+ CheckNotImplementedStatus(t, ri6)
+}
+
+func TestGetChannelsForScheme(t *testing.T) {
+ th := Setup().InitBasic().InitSystemAdmin()
+ defer th.TearDown()
+
+ th.App.SetLicense(model.NewTestLicense("custom_permissions_schemes"))
+
+ th.App.SetPhase2PermissionsMigrationStatus(true)
+
+ scheme1 := &model.Scheme{
+ DisplayName: model.NewId(),
+ Name: model.NewId(),
+ Description: model.NewId(),
+ Scope: model.SCHEME_SCOPE_CHANNEL,
+ }
+ scheme1, r1 := th.SystemAdminClient.CreateScheme(scheme1)
+ CheckNoError(t, r1)
+
+ channel1 := &model.Channel{
+ TeamId: model.NewId(),
+ DisplayName: "A Name",
+ Name: model.NewId(),
+ Type: model.CHANNEL_OPEN,
+ }
+
+ result1 := <-th.App.Srv.Store.Channel().Save(channel1, 1000000)
+ assert.Nil(t, result1.Err)
+ channel1 = result1.Data.(*model.Channel)
+
+ l2, r2 := th.SystemAdminClient.GetChannelsForScheme(scheme1.Id, 0, 100)
+ CheckNoError(t, r2)
+ assert.Zero(t, len(l2))
+
+ channel1.SchemeId = &scheme1.Id
+ result2 := <-th.App.Srv.Store.Channel().Update(channel1)
+ assert.Nil(t, result2.Err)
+ channel1 = result2.Data.(*model.Channel)
+
+ l3, r3 := th.SystemAdminClient.GetChannelsForScheme(scheme1.Id, 0, 100)
+ CheckNoError(t, r3)
+ assert.Len(t, l3, 1)
+ assert.Equal(t, channel1.Id, l3[0].Id)
+
+ channel2 := &model.Channel{
+ TeamId: model.NewId(),
+ DisplayName: "B Name",
+ Name: model.NewId(),
+ Type: model.CHANNEL_OPEN,
+ SchemeId: &scheme1.Id,
+ }
+ result3 := <-th.App.Srv.Store.Channel().Save(channel2, 1000000)
+ assert.Nil(t, result3.Err)
+ channel2 = result3.Data.(*model.Channel)
+
+ l4, r4 := th.SystemAdminClient.GetChannelsForScheme(scheme1.Id, 0, 100)
+ CheckNoError(t, r4)
+ assert.Len(t, l4, 2)
+ assert.Equal(t, channel1.Id, l4[0].Id)
+ assert.Equal(t, channel2.Id, l4[1].Id)
+
+ l5, r5 := th.SystemAdminClient.GetChannelsForScheme(scheme1.Id, 1, 1)
+ CheckNoError(t, r5)
+ assert.Len(t, l5, 1)
+ assert.Equal(t, channel2.Id, l5[0].Id)
+
+ // Check various error cases.
+ _, ri1 := th.SystemAdminClient.GetChannelsForScheme(model.NewId(), 0, 100)
+ CheckNotFoundStatus(t, ri1)
+
+ _, ri2 := th.SystemAdminClient.GetChannelsForScheme("", 0, 100)
+ CheckBadRequestStatus(t, ri2)
+
+ th.Client.Logout()
+ _, ri3 := th.Client.GetChannelsForScheme(model.NewId(), 0, 100)
+ CheckUnauthorizedStatus(t, ri3)
+
+ th.Client.Login(th.BasicUser.Username, th.BasicUser.Password)
+ _, ri4 := th.Client.GetChannelsForScheme(model.NewId(), 0, 100)
+ CheckForbiddenStatus(t, ri4)
+
+ scheme2 := &model.Scheme{
+ DisplayName: model.NewId(),
+ Name: model.NewId(),
+ Description: model.NewId(),
+ Scope: model.SCHEME_SCOPE_TEAM,
+ }
+ scheme2, rs2 := th.SystemAdminClient.CreateScheme(scheme2)
+ CheckNoError(t, rs2)
+
+ _, ri5 := th.SystemAdminClient.GetChannelsForScheme(scheme2.Id, 0, 100)
+ CheckBadRequestStatus(t, ri5)
+
+ th.App.SetPhase2PermissionsMigrationStatus(false)
+
+ _, ri6 := th.SystemAdminClient.GetChannelsForScheme(scheme1.Id, 0, 100)
+ CheckNotImplementedStatus(t, ri6)
+}
+
+func TestPatchScheme(t *testing.T) {
+ th := Setup().InitBasic().InitSystemAdmin()
+ defer th.TearDown()
+
+ th.App.SetLicense(model.NewTestLicense("custom_permissions_schemes"))
+
+ th.App.SetPhase2PermissionsMigrationStatus(true)
+
+ // Basic test of creating a team scheme.
+ scheme1 := &model.Scheme{
+ DisplayName: model.NewId(),
+ Name: model.NewId(),
+ Description: model.NewId(),
+ Scope: model.SCHEME_SCOPE_TEAM,
+ }
+
+ s1, r1 := th.SystemAdminClient.CreateScheme(scheme1)
+ CheckNoError(t, r1)
+
+ assert.Equal(t, s1.DisplayName, scheme1.DisplayName)
+ assert.Equal(t, s1.Name, scheme1.Name)
+ assert.Equal(t, s1.Description, scheme1.Description)
+ assert.NotZero(t, s1.CreateAt)
+ assert.Equal(t, s1.CreateAt, s1.UpdateAt)
+ assert.Zero(t, s1.DeleteAt)
+ assert.Equal(t, s1.Scope, scheme1.Scope)
+ assert.NotZero(t, len(s1.DefaultTeamAdminRole))
+ assert.NotZero(t, len(s1.DefaultTeamUserRole))
+ assert.NotZero(t, len(s1.DefaultChannelAdminRole))
+ assert.NotZero(t, len(s1.DefaultChannelUserRole))
+
+ s2, r2 := th.SystemAdminClient.GetScheme(s1.Id)
+ CheckNoError(t, r2)
+
+ assert.Equal(t, s1, s2)
+
+ // Test with a valid patch.
+ schemePatch := &model.SchemePatch{
+ DisplayName: new(string),
+ Name: new(string),
+ Description: new(string),
+ }
+ *schemePatch.DisplayName = model.NewId()
+ *schemePatch.Name = model.NewId()
+ *schemePatch.Description = model.NewId()
+
+ s3, r3 := th.SystemAdminClient.PatchScheme(s2.Id, schemePatch)
+ CheckNoError(t, r3)
+ assert.Equal(t, s3.Id, s2.Id)
+ assert.Equal(t, s3.DisplayName, *schemePatch.DisplayName)
+ assert.Equal(t, s3.Name, *schemePatch.Name)
+ assert.Equal(t, s3.Description, *schemePatch.Description)
+
+ s4, r4 := th.SystemAdminClient.GetScheme(s3.Id)
+ CheckNoError(t, r4)
+ assert.Equal(t, s3, s4)
+
+ // Test with a partial patch.
+ *schemePatch.Name = model.NewId()
+ *schemePatch.DisplayName = model.NewId()
+ schemePatch.Description = nil
+
+ s5, r5 := th.SystemAdminClient.PatchScheme(s4.Id, schemePatch)
+ CheckNoError(t, r5)
+ assert.Equal(t, s5.Id, s4.Id)
+ assert.Equal(t, s5.DisplayName, *schemePatch.DisplayName)
+ assert.Equal(t, s5.Name, *schemePatch.Name)
+ assert.Equal(t, s5.Description, s4.Description)
+
+ s6, r6 := th.SystemAdminClient.GetScheme(s5.Id)
+ CheckNoError(t, r6)
+ assert.Equal(t, s5, s6)
+
+ // Test with invalid patch.
+ *schemePatch.Name = strings.Repeat(model.NewId(), 20)
+ _, r7 := th.SystemAdminClient.PatchScheme(s6.Id, schemePatch)
+ CheckBadRequestStatus(t, r7)
+
+ // Test with unknown ID.
+ *schemePatch.Name = model.NewId()
+ _, r8 := th.SystemAdminClient.PatchScheme(model.NewId(), schemePatch)
+ CheckNotFoundStatus(t, r8)
+
+ // Test with invalid ID.
+ _, r9 := th.SystemAdminClient.PatchScheme("12345", schemePatch)
+ CheckBadRequestStatus(t, r9)
+
+ // Test without required permissions.
+ _, r10 := th.Client.PatchScheme(s6.Id, schemePatch)
+ CheckForbiddenStatus(t, r10)
+
+ // Test without license.
+ th.App.SetLicense(nil)
+ _, r11 := th.SystemAdminClient.PatchScheme(s6.Id, schemePatch)
+ CheckNotImplementedStatus(t, r11)
+
+ th.App.SetPhase2PermissionsMigrationStatus(false)
+
+ th.LoginSystemAdmin()
+ th.App.SetLicense(model.NewTestLicense("custom_permissions_schemes"))
+
+ _, r12 := th.SystemAdminClient.PatchScheme(s6.Id, schemePatch)
+ CheckNotImplementedStatus(t, r12)
+}
+
+func TestDeleteScheme(t *testing.T) {
+ th := Setup().InitBasic().InitSystemAdmin()
+ defer th.TearDown()
+
+ t.Run("ValidTeamScheme", func(t *testing.T) {
+ th.App.SetLicense(model.NewTestLicense("custom_permissions_schemes"))
+
+ th.App.SetPhase2PermissionsMigrationStatus(true)
+
+ // Create a team scheme.
+ scheme1 := &model.Scheme{
+ DisplayName: model.NewId(),
+ Name: model.NewId(),
+ Description: model.NewId(),
+ Scope: model.SCHEME_SCOPE_TEAM,
+ }
+
+ s1, r1 := th.SystemAdminClient.CreateScheme(scheme1)
+ CheckNoError(t, r1)
+
+ // Retrieve the roles and check they are not deleted.
+ role1, roleRes1 := th.SystemAdminClient.GetRole(s1.DefaultTeamAdminRole)
+ CheckNoError(t, roleRes1)
+ role2, roleRes2 := th.SystemAdminClient.GetRole(s1.DefaultTeamUserRole)
+ CheckNoError(t, roleRes2)
+ role3, roleRes3 := th.SystemAdminClient.GetRole(s1.DefaultChannelAdminRole)
+ CheckNoError(t, roleRes3)
+ role4, roleRes4 := th.SystemAdminClient.GetRole(s1.DefaultChannelUserRole)
+ CheckNoError(t, roleRes4)
+
+ assert.Zero(t, role1.DeleteAt)
+ assert.Zero(t, role2.DeleteAt)
+ assert.Zero(t, role3.DeleteAt)
+ assert.Zero(t, role4.DeleteAt)
+
+ // Make sure this scheme is in use by a team.
+ res := <-th.App.Srv.Store.Team().Save(&model.Team{
+ Name: model.NewId(),
+ DisplayName: model.NewId(),
+ Email: model.NewId() + "@nowhere.com",
+ Type: model.TEAM_OPEN,
+ SchemeId: &s1.Id,
+ })
+ assert.Nil(t, res.Err)
+ team := res.Data.(*model.Team)
+
+ // Delete the Scheme.
+ _, r3 := th.SystemAdminClient.DeleteScheme(s1.Id)
+ CheckNoError(t, r3)
+
+ // Check the roles were deleted.
+ role1, roleRes1 = th.SystemAdminClient.GetRole(s1.DefaultTeamAdminRole)
+ CheckNoError(t, roleRes1)
+ role2, roleRes2 = th.SystemAdminClient.GetRole(s1.DefaultTeamUserRole)
+ CheckNoError(t, roleRes2)
+ role3, roleRes3 = th.SystemAdminClient.GetRole(s1.DefaultChannelAdminRole)
+ CheckNoError(t, roleRes3)
+ role4, roleRes4 = th.SystemAdminClient.GetRole(s1.DefaultChannelUserRole)
+ CheckNoError(t, roleRes4)
+
+ assert.NotZero(t, role1.DeleteAt)
+ assert.NotZero(t, role2.DeleteAt)
+ assert.NotZero(t, role3.DeleteAt)
+ assert.NotZero(t, role4.DeleteAt)
+
+ // Check the team now uses the default scheme
+ c2, resp := th.SystemAdminClient.GetTeam(team.Id, "")
+ CheckNoError(t, resp)
+ assert.Equal(t, "", *c2.SchemeId)
+ })
+
+ t.Run("ValidChannelScheme", func(t *testing.T) {
+ th.App.SetLicense(model.NewTestLicense("custom_permissions_schemes"))
+
+ th.App.SetPhase2PermissionsMigrationStatus(true)
+
+ // Create a channel scheme.
+ scheme1 := &model.Scheme{
+ DisplayName: model.NewId(),
+ Name: model.NewId(),
+ Description: model.NewId(),
+ Scope: model.SCHEME_SCOPE_CHANNEL,
+ }
+
+ s1, r1 := th.SystemAdminClient.CreateScheme(scheme1)
+ CheckNoError(t, r1)
+
+ // Retrieve the roles and check they are not deleted.
+ role3, roleRes3 := th.SystemAdminClient.GetRole(s1.DefaultChannelAdminRole)
+ CheckNoError(t, roleRes3)
+ role4, roleRes4 := th.SystemAdminClient.GetRole(s1.DefaultChannelUserRole)
+ CheckNoError(t, roleRes4)
+
+ assert.Zero(t, role3.DeleteAt)
+ assert.Zero(t, role4.DeleteAt)
+
+ // Make sure this scheme is in use by a team.
+ res := <-th.App.Srv.Store.Channel().Save(&model.Channel{
+ TeamId: model.NewId(),
+ DisplayName: model.NewId(),
+ Name: model.NewId(),
+ Type: model.CHANNEL_OPEN,
+ SchemeId: &s1.Id,
+ }, -1)
+ assert.Nil(t, res.Err)
+ channel := res.Data.(*model.Channel)
+
+ // Delete the Scheme.
+ _, r3 := th.SystemAdminClient.DeleteScheme(s1.Id)
+ CheckNoError(t, r3)
+
+ // Check the roles were deleted.
+ role3, roleRes3 = th.SystemAdminClient.GetRole(s1.DefaultChannelAdminRole)
+ CheckNoError(t, roleRes3)
+ role4, roleRes4 = th.SystemAdminClient.GetRole(s1.DefaultChannelUserRole)
+ CheckNoError(t, roleRes4)
+
+ assert.NotZero(t, role3.DeleteAt)
+ assert.NotZero(t, role4.DeleteAt)
+
+ // Check the channel now uses the default scheme
+ c2, resp := th.SystemAdminClient.GetChannelByName(channel.Name, channel.TeamId, "")
+ CheckNoError(t, resp)
+ assert.Equal(t, "", *c2.SchemeId)
+ })
+
+ t.Run("FailureCases", func(t *testing.T) {
+ th.App.SetLicense(model.NewTestLicense("custom_permissions_schemes"))
+
+ th.App.SetPhase2PermissionsMigrationStatus(true)
+
+ scheme1 := &model.Scheme{
+ DisplayName: model.NewId(),
+ Name: model.NewId(),
+ Description: model.NewId(),
+ Scope: model.SCHEME_SCOPE_CHANNEL,
+ }
+
+ s1, r1 := th.SystemAdminClient.CreateScheme(scheme1)
+ CheckNoError(t, r1)
+
+ // Test with unknown ID.
+ _, r2 := th.SystemAdminClient.DeleteScheme(model.NewId())
+ CheckNotFoundStatus(t, r2)
+
+ // Test with invalid ID.
+ _, r3 := th.SystemAdminClient.DeleteScheme("12345")
+ CheckBadRequestStatus(t, r3)
+
+ // Test without required permissions.
+ _, r4 := th.Client.DeleteScheme(s1.Id)
+ CheckForbiddenStatus(t, r4)
+
+ // Test without license.
+ th.App.SetLicense(nil)
+ _, r5 := th.SystemAdminClient.DeleteScheme(s1.Id)
+ CheckNotImplementedStatus(t, r5)
+
+ th.App.SetPhase2PermissionsMigrationStatus(false)
+
+ th.App.SetLicense(model.NewTestLicense("custom_permissions_schemes"))
+
+ _, r6 := th.SystemAdminClient.DeleteScheme(s1.Id)
+ CheckNotImplementedStatus(t, r6)
+ })
+}
diff --git a/api4/team.go b/api4/team.go
index 44771ae60..74b385122 100644
--- a/api4/team.go
+++ b/api4/team.go
@@ -20,6 +20,7 @@ const (
func (api *API) InitTeam() {
api.BaseRoutes.Teams.Handle("", api.ApiSessionRequired(createTeam)).Methods("POST")
api.BaseRoutes.Teams.Handle("", api.ApiSessionRequired(getAllTeams)).Methods("GET")
+ api.BaseRoutes.Teams.Handle("/{team_id:[A-Za-z0-9]+}/scheme", api.ApiSessionRequired(updateTeamScheme)).Methods("PUT")
api.BaseRoutes.Teams.Handle("/search", api.ApiSessionRequired(searchTeams)).Methods("POST")
api.BaseRoutes.TeamsForUser.Handle("", api.ApiSessionRequired(getTeamsForUser)).Methods("GET")
api.BaseRoutes.TeamsForUser.Handle("/unread", api.ApiSessionRequired(getTeamsUnreadForUser)).Methods("GET")
@@ -833,3 +834,55 @@ func removeTeamIcon(c *Context, w http.ResponseWriter, r *http.Request) {
c.LogAudit("")
ReturnStatusOK(w)
}
+
+func updateTeamScheme(c *Context, w http.ResponseWriter, r *http.Request) {
+ c.RequireTeamId()
+ if c.Err != nil {
+ return
+ }
+
+ schemeID := model.SchemeIDFromJson(r.Body)
+ if schemeID == nil || (len(*schemeID) != 26 && *schemeID != "") {
+ c.SetInvalidParam("scheme_id")
+ return
+ }
+
+ if c.App.License() == nil {
+ c.Err = model.NewAppError("Api4.UpdateTeamScheme", "api.team.update_team_scheme.license.error", nil, "", http.StatusNotImplemented)
+ return
+ }
+
+ if !c.App.SessionHasPermissionToTeam(c.Session, c.Params.TeamId, model.PERMISSION_MANAGE_SYSTEM) {
+ c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM)
+ return
+ }
+
+ if *schemeID != "" {
+ scheme, err := c.App.GetScheme(*schemeID)
+ if err != nil {
+ c.Err = err
+ return
+ }
+
+ if scheme.Scope != model.SCHEME_SCOPE_TEAM {
+ c.Err = model.NewAppError("Api4.UpdateTeamScheme", "api.team.update_team_scheme.scheme_scope.error", nil, "", http.StatusBadRequest)
+ return
+ }
+ }
+
+ team, err := c.App.GetTeam(c.Params.TeamId)
+ if err != nil {
+ c.Err = err
+ return
+ }
+
+ team.SchemeId = schemeID
+
+ _, err = c.App.UpdateTeamScheme(team)
+ if err != nil {
+ c.Err = err
+ return
+ }
+
+ ReturnStatusOK(w)
+}
diff --git a/api4/team_test.go b/api4/team_test.go
index bf67d8fde..079ba37ec 100644
--- a/api4/team_test.go
+++ b/api4/team_test.go
@@ -1675,7 +1675,7 @@ func TestUpdateTeamMemberRoles(t *testing.T) {
// user 1 (team admin) tries to demote system admin (not member of a team)
_, resp = Client.UpdateTeamMemberRoles(th.BasicTeam.Id, th.SystemAdminUser.Id, TEAM_MEMBER)
- CheckBadRequestStatus(t, resp)
+ CheckNotFoundStatus(t, resp)
// user 1 (team admin) demotes system admin (member of a team)
th.LinkUserToTeam(th.SystemAdminUser, th.BasicTeam)
@@ -1701,7 +1701,7 @@ func TestUpdateTeamMemberRoles(t *testing.T) {
// user 1 (team admin) tries to promote a random user
_, resp = Client.UpdateTeamMemberRoles(th.BasicTeam.Id, model.NewId(), TEAM_ADMIN)
- CheckBadRequestStatus(t, resp)
+ CheckNotFoundStatus(t, resp)
// user 1 (team admin) tries to promote invalid team permission
_, resp = Client.UpdateTeamMemberRoles(th.BasicTeam.Id, th.BasicUser.Id, "junk")
@@ -2055,3 +2055,75 @@ func TestRemoveTeamIcon(t *testing.T) {
_, resp = Client.RemoveTeamIcon(team.Id)
CheckForbiddenStatus(t, resp)
}
+
+func TestUpdateTeamScheme(t *testing.T) {
+ th := Setup().InitBasic().InitSystemAdmin()
+ defer th.TearDown()
+
+ th.App.SetLicense(model.NewTestLicense(""))
+
+ th.App.SetPhase2PermissionsMigrationStatus(true)
+
+ team := &model.Team{
+ DisplayName: "Name",
+ Description: "Some description",
+ CompanyName: "Some company name",
+ AllowOpenInvite: false,
+ InviteId: "inviteid0",
+ Name: "z-z-" + model.NewId() + "a",
+ Email: "success+" + model.NewId() + "@simulator.amazonses.com",
+ Type: model.TEAM_OPEN,
+ }
+ team, _ = th.SystemAdminClient.CreateTeam(team)
+
+ teamScheme := &model.Scheme{
+ DisplayName: "DisplayName",
+ Name: model.NewId(),
+ Description: "Some description",
+ Scope: model.SCHEME_SCOPE_TEAM,
+ }
+ teamScheme, _ = th.SystemAdminClient.CreateScheme(teamScheme)
+ channelScheme := &model.Scheme{
+ DisplayName: "DisplayName",
+ Name: model.NewId(),
+ Description: "Some description",
+ Scope: model.SCHEME_SCOPE_CHANNEL,
+ }
+ channelScheme, _ = th.SystemAdminClient.CreateScheme(channelScheme)
+
+ // Test the setup/base case.
+ _, resp := th.SystemAdminClient.UpdateTeamScheme(team.Id, teamScheme.Id)
+ CheckNoError(t, resp)
+
+ // Test the return to default scheme
+ _, resp = th.SystemAdminClient.UpdateTeamScheme(team.Id, "")
+ CheckNoError(t, resp)
+
+ // Test various invalid team and scheme id combinations.
+ _, resp = th.SystemAdminClient.UpdateTeamScheme(team.Id, "x")
+ CheckBadRequestStatus(t, resp)
+ _, resp = th.SystemAdminClient.UpdateTeamScheme("x", teamScheme.Id)
+ CheckBadRequestStatus(t, resp)
+ _, resp = th.SystemAdminClient.UpdateTeamScheme("x", "x")
+ CheckBadRequestStatus(t, resp)
+
+ // Test that permissions are required.
+ _, resp = th.Client.UpdateTeamScheme(team.Id, teamScheme.Id)
+ CheckForbiddenStatus(t, resp)
+
+ // Test that a license is requried.
+ th.App.SetLicense(nil)
+ _, resp = th.SystemAdminClient.UpdateTeamScheme(team.Id, teamScheme.Id)
+ CheckNotImplementedStatus(t, resp)
+ th.App.SetLicense(model.NewTestLicense(""))
+
+ // Test an invalid scheme scope.
+ _, resp = th.SystemAdminClient.UpdateTeamScheme(team.Id, channelScheme.Id)
+ fmt.Printf("resp: %+v\n", resp)
+ CheckBadRequestStatus(t, resp)
+
+ // Test that an unauthenticated user gets rejected.
+ th.SystemAdminClient.Logout()
+ _, resp = th.SystemAdminClient.UpdateTeamScheme(team.Id, teamScheme.Id)
+ CheckUnauthorizedStatus(t, resp)
+}
diff --git a/app/app.go b/app/app.go
index 6de75855c..bda56ca1a 100644
--- a/app/app.go
+++ b/app/app.go
@@ -20,6 +20,7 @@ import (
"github.com/mattermost/mattermost-server/einterfaces"
ejobs "github.com/mattermost/mattermost-server/einterfaces/jobs"
"github.com/mattermost/mattermost-server/jobs"
+ tjobs "github.com/mattermost/mattermost-server/jobs/interfaces"
"github.com/mattermost/mattermost-server/mlog"
"github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/plugin/pluginenv"
@@ -29,6 +30,7 @@ import (
)
const ADVANCED_PERMISSIONS_MIGRATION_KEY = "AdvancedPermissionsMigrationComplete"
+const EMOJIS_PERMISSIONS_MIGRATION_KEY = "EmojisPermissionsMigrationComplete"
type App struct {
goroutineCount int32
@@ -56,7 +58,6 @@ type App struct {
Compliance einterfaces.ComplianceInterface
DataRetention einterfaces.DataRetentionInterface
Elasticsearch einterfaces.ElasticsearchInterface
- Emoji einterfaces.EmojiInterface
Ldap einterfaces.LdapInterface
MessageExport einterfaces.MessageExportInterface
Metrics einterfaces.MetricsInterface
@@ -93,6 +94,8 @@ type App struct {
clientConfig map[string]string
clientConfigHash string
diagnosticId string
+
+ phase2PermissionsMigrationComplete bool
}
var appCount = 0
@@ -285,12 +288,6 @@ func RegisterElasticsearchInterface(f func(*App) einterfaces.ElasticsearchInterf
elasticsearchInterface = f
}
-var emojiInterface func(*App) einterfaces.EmojiInterface
-
-func RegisterEmojiInterface(f func(*App) einterfaces.EmojiInterface) {
- emojiInterface = f
-}
-
var jobsDataRetentionJobInterface func(*App) ejobs.DataRetentionJobInterface
func RegisterJobsDataRetentionJobInterface(f func(*App) ejobs.DataRetentionJobInterface) {
@@ -321,6 +318,12 @@ func RegisterJobsLdapSyncInterface(f func(*App) ejobs.LdapSyncInterface) {
jobsLdapSyncInterface = f
}
+var jobsMigrationsInterface func(*App) tjobs.MigrationsJobInterface
+
+func RegisterJobsMigrationsJobInterface(f func(*App) tjobs.MigrationsJobInterface) {
+ jobsMigrationsInterface = f
+}
+
var ldapInterface func(*App) einterfaces.LdapInterface
func RegisterLdapInterface(f func(*App) einterfaces.LdapInterface) {
@@ -367,9 +370,6 @@ func (a *App) initEnterprise() {
if elasticsearchInterface != nil {
a.Elasticsearch = elasticsearchInterface(a)
}
- if emojiInterface != nil {
- a.Emoji = emojiInterface(a)
- }
if ldapInterface != nil {
a.Ldap = ldapInterface(a)
a.AddConfigListener(func(_, cfg *model.Config) {
@@ -415,6 +415,9 @@ func (a *App) initJobs() {
if jobsLdapSyncInterface != nil {
a.Jobs.LdapSync = jobsLdapSyncInterface(a)
}
+ if jobsMigrationsInterface != nil {
+ a.Jobs.Migrations = jobsMigrationsInterface(a)
+ }
}
func (a *App) DiagnosticId() string {
@@ -580,3 +583,86 @@ func (a *App) DoAdvancedPermissionsMigration() {
mlog.Critical(fmt.Sprint(result.Err))
}
}
+
+func (a *App) SetPhase2PermissionsMigrationStatus(isComplete bool) error {
+ if !isComplete {
+ res := <-a.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2)
+ if res.Err != nil {
+ return res.Err
+ }
+ }
+ a.phase2PermissionsMigrationComplete = isComplete
+ return nil
+}
+
+func (a *App) DoEmojisPermissionsMigration() {
+ // If the migration is already marked as completed, don't do it again.
+ if result := <-a.Srv.Store.System().GetByName(EMOJIS_PERMISSIONS_MIGRATION_KEY); result.Err == nil {
+ return
+ }
+
+ var role *model.Role = nil
+ var systemAdminRole *model.Role = nil
+ var err *model.AppError = nil
+
+ mlog.Info("Migrating emojis config to database.")
+ switch *a.Config().ServiceSettings.RestrictCustomEmojiCreation {
+ case model.RESTRICT_EMOJI_CREATION_ALL:
+ role, err = a.GetRoleByName(model.SYSTEM_USER_ROLE_ID)
+ if err != nil {
+ mlog.Critical("Failed to migrate emojis creation permissions from mattermost config.")
+ mlog.Critical(err.Error())
+ return
+ }
+ break
+ case model.RESTRICT_EMOJI_CREATION_ADMIN:
+ role, err = a.GetRoleByName(model.TEAM_ADMIN_ROLE_ID)
+ if err != nil {
+ mlog.Critical("Failed to migrate emojis creation permissions from mattermost config.")
+ mlog.Critical(err.Error())
+ return
+ }
+ break
+ case model.RESTRICT_EMOJI_CREATION_SYSTEM_ADMIN:
+ role = nil
+ break
+ default:
+ mlog.Critical("Failed to migrate emojis creation permissions from mattermost config.")
+ mlog.Critical("Invalid restrict emoji creation setting")
+ return
+ }
+
+ if role != nil {
+ role.Permissions = append(role.Permissions, model.PERMISSION_MANAGE_EMOJIS.Id)
+ if result := <-a.Srv.Store.Role().Save(role); result.Err != nil {
+ mlog.Critical("Failed to migrate emojis creation permissions from mattermost config.")
+ mlog.Critical(result.Err.Error())
+ return
+ }
+ }
+
+ systemAdminRole, err = a.GetRoleByName(model.SYSTEM_ADMIN_ROLE_ID)
+ if err != nil {
+ mlog.Critical("Failed to migrate emojis creation permissions from mattermost config.")
+ mlog.Critical(err.Error())
+ return
+ }
+
+ systemAdminRole.Permissions = append(systemAdminRole.Permissions, model.PERMISSION_MANAGE_EMOJIS.Id)
+ systemAdminRole.Permissions = append(systemAdminRole.Permissions, model.PERMISSION_MANAGE_OTHERS_EMOJIS.Id)
+ if result := <-a.Srv.Store.Role().Save(systemAdminRole); result.Err != nil {
+ mlog.Critical("Failed to migrate emojis creation permissions from mattermost config.")
+ mlog.Critical(result.Err.Error())
+ return
+ }
+
+ system := model.System{
+ Name: EMOJIS_PERMISSIONS_MIGRATION_KEY,
+ Value: "true",
+ }
+
+ if result := <-a.Srv.Store.System().Save(&system); result.Err != nil {
+ mlog.Critical("Failed to mark emojis permissions migration as completed.")
+ mlog.Critical(fmt.Sprint(result.Err))
+ }
+}
diff --git a/app/app_test.go b/app/app_test.go
index cb6917073..dd6f0b593 100644
--- a/app/app_test.go
+++ b/app/app_test.go
@@ -455,3 +455,138 @@ func TestDoAdvancedPermissionsMigration(t *testing.T) {
*config.ServiceSettings.PostEditTimeLimit = 300
th.App.SaveConfig(config, false)
}
+
+func TestDoEmojisPermissionsMigration(t *testing.T) {
+ th := Setup()
+ defer th.TearDown()
+
+ if testStoreSqlSupplier == nil {
+ t.Skip("This test requires a TestStore to be run.")
+ }
+
+ // Add a license and change the policy config.
+ restrictCustomEmojiCreation := *th.App.Config().ServiceSettings.RestrictCustomEmojiCreation
+
+ defer func() {
+ th.App.UpdateConfig(func(cfg *model.Config) {
+ *cfg.ServiceSettings.RestrictCustomEmojiCreation = restrictCustomEmojiCreation
+ })
+ }()
+
+ th.App.UpdateConfig(func(cfg *model.Config) {
+ *cfg.ServiceSettings.RestrictCustomEmojiCreation = model.RESTRICT_EMOJI_CREATION_SYSTEM_ADMIN
+ })
+
+ th.ResetEmojisMigration()
+ th.App.DoEmojisPermissionsMigration()
+
+ expectedSystemAdmin := []string{
+ model.PERMISSION_ASSIGN_SYSTEM_ADMIN_ROLE.Id,
+ model.PERMISSION_MANAGE_SYSTEM.Id,
+ model.PERMISSION_MANAGE_ROLES.Id,
+ model.PERMISSION_MANAGE_PUBLIC_CHANNEL_PROPERTIES.Id,
+ model.PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS.Id,
+ model.PERMISSION_MANAGE_PRIVATE_CHANNEL_MEMBERS.Id,
+ model.PERMISSION_DELETE_PUBLIC_CHANNEL.Id,
+ model.PERMISSION_CREATE_PUBLIC_CHANNEL.Id,
+ model.PERMISSION_MANAGE_PRIVATE_CHANNEL_PROPERTIES.Id,
+ model.PERMISSION_DELETE_PRIVATE_CHANNEL.Id,
+ model.PERMISSION_CREATE_PRIVATE_CHANNEL.Id,
+ model.PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH.Id,
+ model.PERMISSION_MANAGE_OTHERS_WEBHOOKS.Id,
+ model.PERMISSION_EDIT_OTHER_USERS.Id,
+ model.PERMISSION_MANAGE_OAUTH.Id,
+ model.PERMISSION_INVITE_USER.Id,
+ model.PERMISSION_DELETE_POST.Id,
+ model.PERMISSION_DELETE_OTHERS_POSTS.Id,
+ model.PERMISSION_CREATE_TEAM.Id,
+ model.PERMISSION_ADD_USER_TO_TEAM.Id,
+ model.PERMISSION_LIST_USERS_WITHOUT_TEAM.Id,
+ model.PERMISSION_MANAGE_JOBS.Id,
+ model.PERMISSION_CREATE_POST_PUBLIC.Id,
+ model.PERMISSION_CREATE_POST_EPHEMERAL.Id,
+ model.PERMISSION_CREATE_USER_ACCESS_TOKEN.Id,
+ model.PERMISSION_READ_USER_ACCESS_TOKEN.Id,
+ model.PERMISSION_REVOKE_USER_ACCESS_TOKEN.Id,
+ model.PERMISSION_REMOVE_OTHERS_REACTIONS.Id,
+ model.PERMISSION_LIST_TEAM_CHANNELS.Id,
+ model.PERMISSION_JOIN_PUBLIC_CHANNELS.Id,
+ model.PERMISSION_READ_PUBLIC_CHANNEL.Id,
+ model.PERMISSION_VIEW_TEAM.Id,
+ model.PERMISSION_READ_CHANNEL.Id,
+ model.PERMISSION_ADD_REACTION.Id,
+ model.PERMISSION_REMOVE_REACTION.Id,
+ model.PERMISSION_UPLOAD_FILE.Id,
+ model.PERMISSION_GET_PUBLIC_LINK.Id,
+ model.PERMISSION_CREATE_POST.Id,
+ model.PERMISSION_USE_SLASH_COMMANDS.Id,
+ model.PERMISSION_EDIT_OTHERS_POSTS.Id,
+ model.PERMISSION_REMOVE_USER_FROM_TEAM.Id,
+ model.PERMISSION_MANAGE_TEAM.Id,
+ model.PERMISSION_IMPORT_TEAM.Id,
+ model.PERMISSION_MANAGE_TEAM_ROLES.Id,
+ model.PERMISSION_MANAGE_CHANNEL_ROLES.Id,
+ model.PERMISSION_MANAGE_SLASH_COMMANDS.Id,
+ model.PERMISSION_MANAGE_OTHERS_SLASH_COMMANDS.Id,
+ model.PERMISSION_MANAGE_WEBHOOKS.Id,
+ model.PERMISSION_EDIT_POST.Id,
+ model.PERMISSION_MANAGE_EMOJIS.Id,
+ model.PERMISSION_MANAGE_OTHERS_EMOJIS.Id,
+ }
+
+ role1, err1 := th.App.GetRoleByName(model.SYSTEM_ADMIN_ROLE_ID)
+ assert.Nil(t, err1)
+ assert.Equal(t, expectedSystemAdmin, role1.Permissions, fmt.Sprintf("'%v' did not have expected permissions", model.SYSTEM_ADMIN_ROLE_ID))
+
+ th.App.UpdateConfig(func(cfg *model.Config) {
+ *cfg.ServiceSettings.RestrictCustomEmojiCreation = model.RESTRICT_EMOJI_CREATION_ADMIN
+ })
+
+ th.ResetEmojisMigration()
+ th.App.DoEmojisPermissionsMigration()
+
+ role2, err2 := th.App.GetRoleByName(model.TEAM_ADMIN_ROLE_ID)
+ assert.Nil(t, err2)
+ expected2 := []string{
+ model.PERMISSION_EDIT_OTHERS_POSTS.Id,
+ model.PERMISSION_REMOVE_USER_FROM_TEAM.Id,
+ model.PERMISSION_MANAGE_TEAM.Id,
+ model.PERMISSION_IMPORT_TEAM.Id,
+ model.PERMISSION_MANAGE_TEAM_ROLES.Id,
+ model.PERMISSION_MANAGE_CHANNEL_ROLES.Id,
+ model.PERMISSION_MANAGE_OTHERS_WEBHOOKS.Id,
+ model.PERMISSION_MANAGE_SLASH_COMMANDS.Id,
+ model.PERMISSION_MANAGE_OTHERS_SLASH_COMMANDS.Id,
+ model.PERMISSION_MANAGE_WEBHOOKS.Id,
+ model.PERMISSION_DELETE_POST.Id,
+ model.PERMISSION_DELETE_OTHERS_POSTS.Id,
+ model.PERMISSION_MANAGE_EMOJIS.Id,
+ }
+ assert.Equal(t, expected2, role2.Permissions, fmt.Sprintf("'%v' did not have expected permissions", model.TEAM_ADMIN_ROLE_ID))
+
+ systemAdmin1, systemAdminErr1 := th.App.GetRoleByName(model.SYSTEM_ADMIN_ROLE_ID)
+ assert.Nil(t, systemAdminErr1)
+ assert.Equal(t, expectedSystemAdmin, systemAdmin1.Permissions, fmt.Sprintf("'%v' did not have expected permissions", model.SYSTEM_ADMIN_ROLE_ID))
+
+ th.App.UpdateConfig(func(cfg *model.Config) {
+ *cfg.ServiceSettings.RestrictCustomEmojiCreation = model.RESTRICT_EMOJI_CREATION_ALL
+ })
+
+ th.ResetEmojisMigration()
+ th.App.DoEmojisPermissionsMigration()
+
+ role3, err3 := th.App.GetRoleByName(model.SYSTEM_USER_ROLE_ID)
+ assert.Nil(t, err3)
+ expected3 := []string{
+ model.PERMISSION_CREATE_DIRECT_CHANNEL.Id,
+ model.PERMISSION_CREATE_GROUP_CHANNEL.Id,
+ model.PERMISSION_PERMANENT_DELETE_USER.Id,
+ model.PERMISSION_CREATE_TEAM.Id,
+ model.PERMISSION_MANAGE_EMOJIS.Id,
+ }
+ assert.Equal(t, expected3, role3.Permissions, fmt.Sprintf("'%v' did not have expected permissions", model.SYSTEM_USER_ROLE_ID))
+
+ systemAdmin2, systemAdminErr2 := th.App.GetRoleByName(model.SYSTEM_ADMIN_ROLE_ID)
+ assert.Nil(t, systemAdminErr2)
+ assert.Equal(t, expectedSystemAdmin, systemAdmin2.Permissions, fmt.Sprintf("'%v' did not have expected permissions", model.SYSTEM_ADMIN_ROLE_ID))
+}
diff --git a/app/apptestlib.go b/app/apptestlib.go
index 7fc78c9c9..d4a79bdcc 100644
--- a/app/apptestlib.go
+++ b/app/apptestlib.go
@@ -110,6 +110,7 @@ func setupTestHelper(enterprise bool) *TestHelper {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ListenAddress = prevListenAddress })
th.App.DoAdvancedPermissionsMigration()
+ th.App.DoEmojisPermissionsMigration()
th.App.Srv.Store.MarkSystemRanUnitTests()
@@ -316,6 +317,40 @@ func (me *TestHelper) AddUserToChannel(user *model.User, channel *model.Channel)
return member
}
+func (me *TestHelper) CreateScheme() (*model.Scheme, []*model.Role) {
+ utils.DisableDebugLogForTest()
+
+ scheme, err := me.App.CreateScheme(&model.Scheme{
+ DisplayName: "Test Scheme Display Name",
+ Name: model.NewId(),
+ Description: "Test scheme description",
+ Scope: model.SCHEME_SCOPE_TEAM,
+ })
+ if err != nil {
+ panic(err)
+ }
+
+ roleIDs := []string{
+ scheme.DefaultTeamAdminRole,
+ scheme.DefaultTeamUserRole,
+ scheme.DefaultChannelAdminRole,
+ scheme.DefaultChannelUserRole,
+ }
+
+ var roles []*model.Role
+ for _, roleID := range roleIDs {
+ role, err := me.App.GetRole(roleID)
+ if err != nil {
+ panic(err)
+ }
+ roles = append(roles, role)
+ }
+
+ utils.EnableDebugLogForTest()
+
+ return scheme, roles
+}
+
func (me *TestHelper) TearDown() {
me.App.Shutdown()
os.Remove(me.tempConfigPath)
@@ -399,6 +434,18 @@ func (me *TestHelper) ResetRoleMigration() {
}
}
+func (me *TestHelper) ResetEmojisMigration() {
+ if _, err := testStoreSqlSupplier.GetMaster().Exec("UPDATE Roles SET Permissions=REPLACE(Permissions, ', manage_emojis', '') WHERE builtin=True"); err != nil {
+ panic(err)
+ }
+
+ testClusterInterface.sendClearRoleCacheMessage()
+
+ if _, err := testStoreSqlSupplier.GetMaster().Exec("DELETE from Systems where Name = :Name", map[string]interface{}{"Name": EMOJIS_PERMISSIONS_MIGRATION_KEY}); err != nil {
+ panic(err)
+ }
+}
+
type FakeClusterInterface struct {
clusterMessageHandler einterfaces.ClusterMessageHandler
}
diff --git a/app/authorization.go b/app/authorization.go
index f281b3e65..3de50e27b 100644
--- a/app/authorization.go
+++ b/app/authorization.go
@@ -94,19 +94,6 @@ func (a *App) SessionHasPermissionToUser(session model.Session, userId string) b
return false
}
-func (a *App) SessionHasPermissionToPost(session model.Session, postId string, permission *model.Permission) bool {
- post, err := a.GetSinglePost(postId)
- if err != nil {
- return false
- }
-
- if post.UserId == session.UserId {
- return true
- }
-
- return a.SessionHasPermissionToChannel(session, post.ChannelId, permission)
-}
-
func (a *App) HasPermissionTo(askingUserId string, permission *model.Permission) bool {
user, err := a.GetUser(askingUserId)
if err != nil {
@@ -200,6 +187,10 @@ func (a *App) RolesGrantPermission(roleNames []string, permissionId string) bool
}
for _, role := range roles {
+ if role.DeleteAt != 0 {
+ continue
+ }
+
permissions := role.Permissions
for _, permission := range permissions {
if permission == permissionId {
diff --git a/app/channel.go b/app/channel.go
index b5afdea2d..55a5008d4 100644
--- a/app/channel.go
+++ b/app/channel.go
@@ -32,7 +32,7 @@ func (a *App) CreateDefaultChannels(teamId string) ([]*model.Channel, *model.App
return channels, nil
}
-func (a *App) JoinDefaultChannels(teamId string, user *model.User, channelRole string, userRequestorId string) *model.AppError {
+func (a *App) JoinDefaultChannels(teamId string, user *model.User, shouldBeAdmin bool, userRequestorId string) *model.AppError {
var err *model.AppError = nil
var requestor *model.User
@@ -52,7 +52,8 @@ func (a *App) JoinDefaultChannels(teamId string, user *model.User, channelRole s
cm := &model.ChannelMember{
ChannelId: townSquare.Id,
UserId: user.Id,
- Roles: channelRole,
+ SchemeUser: true,
+ SchemeAdmin: shouldBeAdmin,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
@@ -85,7 +86,8 @@ func (a *App) JoinDefaultChannels(teamId string, user *model.User, channelRole s
cm := &model.ChannelMember{
ChannelId: offTopic.Id,
UserId: user.Id,
- Roles: channelRole,
+ SchemeUser: true,
+ SchemeAdmin: shouldBeAdmin,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
@@ -166,7 +168,8 @@ func (a *App) CreateChannel(channel *model.Channel, addMember bool) (*model.Chan
cm := &model.ChannelMember{
ChannelId: sc.Id,
UserId: channel.CreatorId,
- Roles: model.CHANNEL_USER_ROLE_ID + " " + model.CHANNEL_ADMIN_ROLE_ID,
+ SchemeUser: true,
+ SchemeAdmin: true,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
@@ -322,7 +325,7 @@ func (a *App) createGroupChannel(userIds []string, creatorId string) (*model.Cha
UserId: user.Id,
ChannelId: group.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
- Roles: model.CHANNEL_USER_ROLE_ID,
+ SchemeUser: true,
}
if result := <-a.Srv.Store.Channel().SaveMember(cm); result.Err != nil {
@@ -351,6 +354,23 @@ func (a *App) UpdateChannel(channel *model.Channel) (*model.Channel, *model.AppE
}
}
+func (a *App) UpdateChannelScheme(channel *model.Channel) (*model.Channel, *model.AppError) {
+ var oldChannel *model.Channel
+ var err *model.AppError
+ if oldChannel, err = a.GetChannel(channel.Id); err != nil {
+ return nil, err
+ }
+
+ oldChannel.SchemeId = channel.SchemeId
+
+ newChannel, err := a.UpdateChannel(oldChannel)
+ if err != nil {
+ return nil, err
+ }
+
+ return newChannel, nil
+}
+
func (a *App) UpdateChannelPrivacy(oldChannel *model.Channel, user *model.User) (*model.Channel, *model.AppError) {
if channel, err := a.UpdateChannel(oldChannel); err != nil {
return channel, err
@@ -438,6 +458,39 @@ func (a *App) PatchChannel(channel *model.Channel, patch *model.ChannelPatch, us
return channel, err
}
+func (a *App) GetSchemeRolesForChannel(channelId string) (string, string, *model.AppError) {
+ var channel *model.Channel
+ var err *model.AppError
+
+ if channel, err = a.GetChannel(channelId); err != nil {
+ return "", "", err
+ }
+
+ if channel.SchemeId != nil && len(*channel.SchemeId) != 0 {
+ if scheme, err := a.GetScheme(*channel.SchemeId); err != nil {
+ return "", "", err
+ } else {
+ return scheme.DefaultChannelUserRole, scheme.DefaultChannelAdminRole, nil
+ }
+ }
+
+ var team *model.Team
+
+ if team, err = a.GetTeam(channel.TeamId); err != nil {
+ return "", "", err
+ }
+
+ if team.SchemeId != nil && len(*team.SchemeId) != 0 {
+ if scheme, err := a.GetScheme(*team.SchemeId); err != nil {
+ return "", "", err
+ } else {
+ return scheme.DefaultChannelUserRole, scheme.DefaultChannelAdminRole, nil
+ }
+ }
+
+ return model.CHANNEL_USER_ROLE_ID, model.CHANNEL_ADMIN_ROLE_ID, nil
+}
+
func (a *App) UpdateChannelMemberRoles(channelId string, userId string, newRoles string) (*model.ChannelMember, *model.AppError) {
var member *model.ChannelMember
var err *model.AppError
@@ -445,14 +498,42 @@ func (a *App) UpdateChannelMemberRoles(channelId string, userId string, newRoles
return nil, err
}
- if err := a.CheckRolesExist(strings.Fields(newRoles)); err != nil {
+ schemeUserRole, schemeAdminRole, err := a.GetSchemeRolesForChannel(channelId)
+ if err != nil {
return nil, err
}
- member.Roles = newRoles
+ var newExplicitRoles []string
+ member.SchemeUser = false
+ member.SchemeAdmin = false
+
+ for _, roleName := range strings.Fields(newRoles) {
+ if role, err := a.GetRoleByName(roleName); err != nil {
+ err.StatusCode = http.StatusBadRequest
+ return nil, err
+ } else if !role.SchemeManaged {
+ // The role is not scheme-managed, so it's OK to apply it to the explicit roles field.
+ newExplicitRoles = append(newExplicitRoles, roleName)
+ } else {
+ // The role is scheme-managed, so need to check if it is part of the scheme for this channel or not.
+ switch roleName {
+ case schemeAdminRole:
+ member.SchemeAdmin = true
+ case schemeUserRole:
+ member.SchemeUser = true
+ default:
+ // If not part of the scheme for this channel, then it is not allowed to apply it as an explicit role.
+ return nil, model.NewAppError("UpdateChannelMemberRoles", "api.channel.update_channel_member_roles.scheme_role.app_error", nil, "role_name="+roleName, http.StatusBadRequest)
+ }
+ }
+ }
+
+ member.ExplicitRoles = strings.Join(newExplicitRoles, " ")
if result := <-a.Srv.Store.Channel().UpdateMember(member); result.Err != nil {
return nil, result.Err
+ } else {
+ member = result.Data.(*model.ChannelMember)
}
a.InvalidateCacheForUser(userId)
@@ -597,7 +678,7 @@ func (a *App) addUserToChannel(user *model.User, channel *model.Channel, teamMem
ChannelId: channel.Id,
UserId: user.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
- Roles: model.CHANNEL_USER_ROLE_ID,
+ SchemeUser: true,
}
if result := <-a.Srv.Store.Channel().SaveMember(newMember); result.Err != nil {
mlog.Error(fmt.Sprintf("Failed to add member user_id=%v channel_id=%v err=%v", user.Id, channel.Id, result.Err), mlog.String("user_id", user.Id))
diff --git a/app/channel_test.go b/app/channel_test.go
index a4e0806a6..336d9b25b 100644
--- a/app/channel_test.go
+++ b/app/channel_test.go
@@ -120,7 +120,7 @@ func TestJoinDefaultChannelsCreatesChannelMemberHistoryRecordTownSquare(t *testi
// create a new user that joins the default channels
user := th.CreateUser()
- th.App.JoinDefaultChannels(th.BasicTeam.Id, user, model.CHANNEL_USER_ROLE_ID, "")
+ th.App.JoinDefaultChannels(th.BasicTeam.Id, user, false, "")
// there should be a ChannelMemberHistory record for the user
histories := store.Must(th.App.Srv.Store.ChannelMemberHistory().GetUsersInChannelDuring(model.GetMillis()-100, model.GetMillis()+100, townSquareChannelId)).([]*model.ChannelMemberHistoryResult)
@@ -146,7 +146,7 @@ func TestJoinDefaultChannelsCreatesChannelMemberHistoryRecordOffTopic(t *testing
// create a new user that joins the default channels
user := th.CreateUser()
- th.App.JoinDefaultChannels(th.BasicTeam.Id, user, model.CHANNEL_USER_ROLE_ID, "")
+ th.App.JoinDefaultChannels(th.BasicTeam.Id, user, false, "")
// there should be a ChannelMemberHistory record for the user
histories := store.Must(th.App.Srv.Store.ChannelMemberHistory().GetUsersInChannelDuring(model.GetMillis()-100, model.GetMillis()+100, offTopicChannelId)).([]*model.ChannelMemberHistoryResult)
@@ -381,3 +381,21 @@ func TestAddChannelMemberNoUserRequestor(t *testing.T) {
assert.Equal(t, user.Username, post.Props["username"])
}
}
+
+func TestAppUpdateChannelScheme(t *testing.T) {
+ th := Setup().InitBasic()
+ defer th.TearDown()
+
+ channel := th.BasicChannel
+ mockID := model.NewString("x")
+ channel.SchemeId = mockID
+
+ updatedChannel, err := th.App.UpdateChannelScheme(channel)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if updatedChannel.SchemeId != mockID {
+ t.Fatal("Wrong Channel SchemeId")
+ }
+}
diff --git a/app/permissions.go b/app/permissions.go
index be975e03d..46090070e 100644
--- a/app/permissions.go
+++ b/app/permissions.go
@@ -4,10 +4,33 @@
package app
import (
+ "bufio"
+ "encoding/json"
+ "fmt"
+ "io"
+
"github.com/mattermost/mattermost-server/model"
+ "github.com/pkg/errors"
)
+const permissionsExportBatchSize = 100
+
func (a *App) ResetPermissionsSystem() *model.AppError {
+ // Reset all Teams to not have a scheme.
+ if result := <-a.Srv.Store.Team().ResetAllTeamSchemes(); result.Err != nil {
+ return result.Err
+ }
+
+ // Reset all Channels to not have a scheme.
+ if result := <-a.Srv.Store.Channel().ResetAllChannelSchemes(); result.Err != nil {
+ return result.Err
+ }
+
+ // Purge all schemes from the database.
+ if result := <-a.Srv.Store.Scheme().PermanentDeleteAll(); result.Err != nil {
+ return result.Err
+ }
+
// Purge all roles from the database.
if result := <-a.Srv.Store.Role().PermanentDeleteAll(); result.Err != nil {
return result.Err
@@ -20,6 +43,142 @@ func (a *App) ResetPermissionsSystem() *model.AppError {
// Now that the permissions system has been reset, re-run the migration to reinitialise it.
a.DoAdvancedPermissionsMigration()
+ a.DoEmojisPermissionsMigration()
+
+ return nil
+}
+
+func (a *App) ExportPermissions(w io.Writer) error {
+
+ next := a.SchemesIterator(permissionsExportBatchSize)
+ var schemeBatch []*model.Scheme
+
+ for schemeBatch = next(); len(schemeBatch) > 0; schemeBatch = next() {
+
+ for _, scheme := range schemeBatch {
+
+ roleIDs := []string{
+ scheme.DefaultTeamAdminRole,
+ scheme.DefaultTeamUserRole,
+ scheme.DefaultChannelAdminRole,
+ scheme.DefaultChannelUserRole,
+ }
+
+ roles := []*model.Role{}
+ for _, roleID := range roleIDs {
+ if len(roleID) == 0 {
+ continue
+ }
+ role, err := a.GetRole(roleID)
+ if err != nil {
+ return err
+ }
+ roles = append(roles, role)
+ }
+
+ schemeExport, err := json.Marshal(&model.SchemeConveyor{
+ Name: scheme.Name,
+ DisplayName: scheme.DisplayName,
+ Description: scheme.Description,
+ Scope: scheme.Scope,
+ TeamAdmin: scheme.DefaultTeamAdminRole,
+ TeamUser: scheme.DefaultTeamUserRole,
+ ChannelAdmin: scheme.DefaultChannelAdminRole,
+ ChannelUser: scheme.DefaultChannelUserRole,
+ Roles: roles,
+ })
+ if err != nil {
+ return err
+ }
+
+ schemeExport = append(schemeExport, []byte("\n")...)
+
+ _, err = w.Write(schemeExport)
+ if err != nil {
+ return err
+ }
+ }
+
+ }
+
+ return nil
+}
+
+func (a *App) ImportPermissions(jsonl io.Reader) error {
+ createdSchemeIDs := []string{}
+
+ scanner := bufio.NewScanner(jsonl)
+
+ for scanner.Scan() {
+ var schemeConveyor *model.SchemeConveyor
+ err := json.Unmarshal(scanner.Bytes(), &schemeConveyor)
+ if err != nil {
+ return err
+ }
+
+ // Create the new Scheme. The new Roles are created automatically.
+ var appErr *model.AppError
+ schemeCreated, appErr := a.CreateScheme(schemeConveyor.Scheme())
+ if appErr != nil {
+ return errors.New(appErr.Message)
+ }
+ createdSchemeIDs = append(createdSchemeIDs, schemeCreated.Id)
+
+ schemeIn := schemeConveyor.Scheme()
+ roleIDTuples := [][]string{
+ {schemeCreated.DefaultTeamAdminRole, schemeIn.DefaultTeamAdminRole},
+ {schemeCreated.DefaultTeamUserRole, schemeIn.DefaultTeamUserRole},
+ {schemeCreated.DefaultChannelAdminRole, schemeIn.DefaultChannelAdminRole},
+ {schemeCreated.DefaultChannelUserRole, schemeIn.DefaultChannelUserRole},
+ }
+ for _, roleIDTuple := range roleIDTuples {
+ if len(roleIDTuple[0]) == 0 || len(roleIDTuple[1]) == 0 {
+ continue
+ }
+
+ err = updateRole(a, schemeConveyor, roleIDTuple[0], roleIDTuple[1])
+ if err != nil {
+ // Delete the new Schemes. The new Roles are deleted automatically.
+ for _, schemeID := range createdSchemeIDs {
+ a.DeleteScheme(schemeID)
+ }
+ return err
+ }
+ }
+ }
+
+ if err := scanner.Err(); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func updateRole(a *App, sc *model.SchemeConveyor, roleCreatedID, defaultRoleID string) error {
+ var err *model.AppError
+
+ roleCreated, err := a.GetRole(roleCreatedID)
+ if err != nil {
+ return errors.New(err.Message)
+ }
+
+ var roleIn *model.Role
+ for _, role := range sc.Roles {
+ if role.Id == defaultRoleID {
+ roleIn = role
+ break
+ }
+ }
+
+ roleCreated.Name = roleIn.Name
+ roleCreated.DisplayName = roleIn.DisplayName
+ roleCreated.Description = roleIn.Description
+ roleCreated.Permissions = roleIn.Permissions
+
+ _, err = a.UpdateRole(roleCreated)
+ if err != nil {
+ return errors.New(fmt.Sprintf("%v: %v\n", err.Message, err.DetailedError))
+ }
return nil
}
diff --git a/app/permissions_test.go b/app/permissions_test.go
new file mode 100644
index 000000000..575e21429
--- /dev/null
+++ b/app/permissions_test.go
@@ -0,0 +1,253 @@
+package app
+
+import (
+ "encoding/json"
+ "fmt"
+ "strings"
+ "testing"
+
+ "github.com/mattermost/mattermost-server/model"
+)
+
+type testWriter struct {
+ write func(p []byte) (int, error)
+}
+
+func (tw testWriter) Write(p []byte) (int, error) {
+ return tw.write(p)
+}
+
+func TestExportPermissions(t *testing.T) {
+ th := Setup().InitBasic()
+ defer th.TearDown()
+
+ var scheme *model.Scheme
+ var roles []*model.Role
+ withMigrationMarkedComplete(th, func() {
+ scheme, roles = th.CreateScheme()
+ })
+
+ results := [][]byte{}
+
+ tw := testWriter{
+ write: func(p []byte) (int, error) {
+ results = append(results, p)
+ return len(p), nil
+ },
+ }
+
+ err := th.App.ExportPermissions(tw)
+ if err != nil {
+ t.Error(err)
+ }
+
+ if len(results) == 0 {
+ t.Error("Expected export to have returned something.")
+ }
+
+ firstResult := results[0]
+
+ var row map[string]interface{}
+ err = json.Unmarshal(firstResult, &row)
+ if err != nil {
+ t.Error(err)
+ }
+
+ getRoleByID := func(id string) string {
+ for _, role := range roles {
+ if role.Id == id {
+ return role.Id
+ }
+ }
+ return ""
+ }
+
+ expectations := map[string]func(str string) string{
+ scheme.DisplayName: func(str string) string { return row["display_name"].(string) },
+ scheme.Name: func(str string) string { return row["name"].(string) },
+ scheme.Description: func(str string) string { return row["description"].(string) },
+ scheme.Scope: func(str string) string { return row["scope"].(string) },
+ scheme.DefaultTeamAdminRole: func(str string) string { return getRoleByID(str) },
+ scheme.DefaultTeamUserRole: func(str string) string { return getRoleByID(str) },
+ scheme.DefaultChannelAdminRole: func(str string) string { return getRoleByID(str) },
+ scheme.DefaultChannelUserRole: func(str string) string { return getRoleByID(str) },
+ }
+
+ for key, valF := range expectations {
+ expected := key
+ actual := valF(key)
+ if actual != expected {
+ t.Errorf("Expected %v but got %v.", expected, actual)
+ }
+ }
+
+}
+
+func TestImportPermissions(t *testing.T) {
+ th := Setup().InitBasic()
+ defer th.TearDown()
+
+ name := model.NewId()
+ displayName := model.NewId()
+ description := "my test description"
+ scope := model.SCHEME_SCOPE_CHANNEL
+ roleName1 := model.NewId()
+ roleName2 := model.NewId()
+
+ var results []*model.Scheme
+ var beforeCount int
+ withMigrationMarkedComplete(th, func() {
+
+ var appErr *model.AppError
+ results, appErr = th.App.GetSchemes(scope, 0, 100)
+ if appErr != nil {
+ panic(appErr)
+ }
+ beforeCount = len(results)
+
+ json := fmt.Sprintf(`{"display_name":"%v","name":"%v","description":"%v","scope":"%v","default_team_admin_role":"","default_team_user_role":"","default_channel_admin_role":"yzfx3g9xjjfw8cqo6bpn33xr7o","default_channel_user_role":"a7s3cp4n33dfxbsrmyh9djao3a","roles":[{"id":"yzfx3g9xjjfw8cqo6bpn33xr7o","name":"%v","display_name":"Channel Admin Role for Scheme my_scheme_1526475590","description":"","create_at":1526475589687,"update_at":1526475589687,"delete_at":0,"permissions":["manage_channel_roles"],"scheme_managed":true,"built_in":false},{"id":"a7s3cp4n33dfxbsrmyh9djao3a","name":"%v","display_name":"Channel User Role for Scheme my_scheme_1526475590","description":"","create_at":1526475589688,"update_at":1526475589688,"delete_at":0,"permissions":["read_channel","add_reaction","remove_reaction","manage_public_channel_members","upload_file","get_public_link","create_post","use_slash_commands","manage_private_channel_members","delete_post","edit_post"],"scheme_managed":true,"built_in":false}]}`, displayName, name, description, scope, roleName1, roleName2)
+ r := strings.NewReader(json)
+
+ err := th.App.ImportPermissions(r)
+ if err != nil {
+ t.Error(err)
+ }
+ results, appErr = th.App.GetSchemes(scope, 0, 100)
+ if appErr != nil {
+ panic(appErr)
+ }
+
+ })
+
+ actual := len(results)
+ expected := beforeCount + 1
+ if actual != expected {
+ t.Errorf("Expected %v roles but got %v.", expected, actual)
+ }
+
+ newScheme := results[0]
+
+ channelAdminRole, appErr := th.App.GetRole(newScheme.DefaultChannelAdminRole)
+ if appErr != nil {
+ t.Error(appErr)
+ }
+
+ channelUserRole, appErr := th.App.GetRole(newScheme.DefaultChannelUserRole)
+ if appErr != nil {
+ t.Error(appErr)
+ }
+
+ expectations := map[string]string{
+ newScheme.DisplayName: displayName,
+ newScheme.Name: name,
+ newScheme.Description: description,
+ newScheme.Scope: scope,
+ newScheme.DefaultTeamAdminRole: "",
+ newScheme.DefaultTeamUserRole: "",
+ channelAdminRole.Name: roleName1,
+ channelUserRole.Name: roleName2,
+ }
+
+ for actual, expected := range expectations {
+ if actual != expected {
+ t.Errorf("Expected %v but got %v.", expected, actual)
+ }
+ }
+
+}
+
+func TestImportPermissions_idempotentScheme(t *testing.T) {
+ th := Setup().InitBasic()
+ defer th.TearDown()
+
+ name := model.NewId()
+ displayName := model.NewId()
+ description := "my test description"
+ scope := model.SCHEME_SCOPE_CHANNEL
+ roleName1 := model.NewId()
+ roleName2 := model.NewId()
+
+ json := fmt.Sprintf(`{"display_name":"%v","name":"%v","description":"%v","scope":"%v","default_team_admin_role":"","default_team_user_role":"","default_channel_admin_role":"yzfx3g9xjjfw8cqo6bpn33xr7o","default_channel_user_role":"a7s3cp4n33dfxbsrmyh9djao3a","roles":[{"id":"yzfx3g9xjjfw8cqo6bpn33xr7o","name":"%v","display_name":"Channel Admin Role for Scheme my_scheme_1526475590","description":"","create_at":1526475589687,"update_at":1526475589687,"delete_at":0,"permissions":["manage_channel_roles"],"scheme_managed":true,"built_in":false},{"id":"a7s3cp4n33dfxbsrmyh9djao3a","name":"%v","display_name":"Channel User Role for Scheme my_scheme_1526475590","description":"","create_at":1526475589688,"update_at":1526475589688,"delete_at":0,"permissions":["read_channel","add_reaction","remove_reaction","manage_public_channel_members","upload_file","get_public_link","create_post","use_slash_commands","manage_private_channel_members","delete_post","edit_post"],"scheme_managed":true,"built_in":false}]}`, displayName, name, description, scope, roleName1, roleName2)
+ jsonl := strings.Repeat(json+"\n", 4)
+ r := strings.NewReader(jsonl)
+
+ var results []*model.Scheme
+ var expected int
+ withMigrationMarkedComplete(th, func() {
+ var appErr *model.AppError
+ results, appErr = th.App.GetSchemes(model.SCHEME_SCOPE_CHANNEL, 0, 100)
+ if appErr != nil {
+ panic(appErr)
+ }
+ expected = len(results) + 1
+
+ err := th.App.ImportPermissions(r)
+ if err == nil {
+ t.Error(err)
+ }
+
+ results, appErr = th.App.GetSchemes(model.SCHEME_SCOPE_CHANNEL, 0, 100)
+ if appErr != nil {
+ panic(appErr)
+ }
+ })
+ actual := len(results)
+
+ if expected != actual {
+ t.Errorf("Expected count to be %v but got %v", expected, actual)
+ }
+
+}
+
+func TestImportPermissions_schemeDeletedOnRoleFailure(t *testing.T) {
+ th := Setup().InitBasic()
+ defer th.TearDown()
+
+ name := model.NewId()
+ displayName := model.NewId()
+ description := "my test description"
+ scope := model.SCHEME_SCOPE_CHANNEL
+ roleName1 := model.NewId()
+ roleName2 := "some invalid role name"
+
+ jsonl := fmt.Sprintf(`{"display_name":"%v","name":"%v","description":"%v","scope":"%v","default_team_admin_role":"","default_team_user_role":"","default_channel_admin_role":"yzfx3g9xjjfw8cqo6bpn33xr7o","default_channel_user_role":"a7s3cp4n33dfxbsrmyh9djao3a","roles":[{"id":"yzfx3g9xjjfw8cqo6bpn33xr7o","name":"%v","display_name":"Channel Admin Role for Scheme my_scheme_1526475590","description":"","create_at":1526475589687,"update_at":1526475589687,"delete_at":0,"permissions":["manage_channel_roles"],"scheme_managed":true,"built_in":false},{"id":"a7s3cp4n33dfxbsrmyh9djao3a","name":"%v","display_name":"Channel User Role for Scheme my_scheme_1526475590","description":"","create_at":1526475589688,"update_at":1526475589688,"delete_at":0,"permissions":["read_channel","add_reaction","remove_reaction","manage_public_channel_members","upload_file","get_public_link","create_post","use_slash_commands","manage_private_channel_members","delete_post","edit_post"],"scheme_managed":true,"built_in":false}]}`, displayName, name, description, scope, roleName1, roleName2)
+ r := strings.NewReader(jsonl)
+
+ var results []*model.Scheme
+ var expected int
+ withMigrationMarkedComplete(th, func() {
+ var appErr *model.AppError
+ results, appErr = th.App.GetSchemes(model.SCHEME_SCOPE_CHANNEL, 0, 100)
+ if appErr != nil {
+ panic(appErr)
+ }
+ expected = len(results)
+
+ err := th.App.ImportPermissions(r)
+ if err == nil {
+ t.Error(err)
+ }
+
+ results, appErr = th.App.GetSchemes(model.SCHEME_SCOPE_CHANNEL, 0, 100)
+ if appErr != nil {
+ panic(appErr)
+ }
+ })
+ actual := len(results)
+
+ if expected != actual {
+ t.Errorf("Expected count to be %v but got %v", expected, actual)
+ }
+
+}
+
+func withMigrationMarkedComplete(th *TestHelper, f func()) {
+ // Mark the migration as done.
+ <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2)
+ <-th.App.Srv.Store.System().Save(&model.System{Name: model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2, Value: "true"})
+ // Un-mark the migration at the end of the test.
+ defer func() {
+ <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2)
+ }()
+ f()
+}
diff --git a/app/scheme.go b/app/scheme.go
new file mode 100644
index 000000000..f070e36f8
--- /dev/null
+++ b/app/scheme.go
@@ -0,0 +1,168 @@
+// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "net/http"
+
+ "github.com/mattermost/mattermost-server/model"
+ "github.com/mattermost/mattermost-server/store"
+)
+
+func (a *App) GetScheme(id string) (*model.Scheme, *model.AppError) {
+ if err := a.IsPhase2MigrationCompleted(); err != nil {
+ return nil, err
+ }
+
+ if result := <-a.Srv.Store.Scheme().Get(id); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.Scheme), nil
+ }
+}
+
+func (a *App) GetSchemesPage(scope string, page int, perPage int) ([]*model.Scheme, *model.AppError) {
+ if err := a.IsPhase2MigrationCompleted(); err != nil {
+ return nil, err
+ }
+
+ return a.GetSchemes(scope, page*perPage, perPage)
+}
+
+func (a *App) GetSchemes(scope string, offset int, limit int) ([]*model.Scheme, *model.AppError) {
+ if err := a.IsPhase2MigrationCompleted(); err != nil {
+ return nil, err
+ }
+
+ if result := <-a.Srv.Store.Scheme().GetAllPage(scope, offset, limit); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.([]*model.Scheme), nil
+ }
+}
+
+func (a *App) CreateScheme(scheme *model.Scheme) (*model.Scheme, *model.AppError) {
+ if err := a.IsPhase2MigrationCompleted(); err != nil {
+ return nil, err
+ }
+
+ // Clear any user-provided values for trusted properties.
+ scheme.DefaultTeamAdminRole = ""
+ scheme.DefaultTeamUserRole = ""
+ scheme.DefaultChannelAdminRole = ""
+ scheme.DefaultChannelUserRole = ""
+ scheme.CreateAt = 0
+ scheme.UpdateAt = 0
+ scheme.DeleteAt = 0
+
+ if result := <-a.Srv.Store.Scheme().Save(scheme); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return scheme, nil
+ }
+}
+
+func (a *App) PatchScheme(scheme *model.Scheme, patch *model.SchemePatch) (*model.Scheme, *model.AppError) {
+ if err := a.IsPhase2MigrationCompleted(); err != nil {
+ return nil, err
+ }
+
+ scheme.Patch(patch)
+ scheme, err := a.UpdateScheme(scheme)
+ if err != nil {
+ return nil, err
+ }
+
+ return scheme, err
+}
+
+func (a *App) UpdateScheme(scheme *model.Scheme) (*model.Scheme, *model.AppError) {
+ if err := a.IsPhase2MigrationCompleted(); err != nil {
+ return nil, err
+ }
+
+ if result := <-a.Srv.Store.Scheme().Save(scheme); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return scheme, nil
+ }
+}
+
+func (a *App) DeleteScheme(schemeId string) (*model.Scheme, *model.AppError) {
+ if err := a.IsPhase2MigrationCompleted(); err != nil {
+ return nil, err
+ }
+
+ if result := <-a.Srv.Store.Scheme().Delete(schemeId); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.Scheme), nil
+ }
+}
+
+func (a *App) GetTeamsForSchemePage(scheme *model.Scheme, page int, perPage int) ([]*model.Team, *model.AppError) {
+ if err := a.IsPhase2MigrationCompleted(); err != nil {
+ return nil, err
+ }
+
+ return a.GetTeamsForScheme(scheme, page*perPage, perPage)
+}
+
+func (a *App) GetTeamsForScheme(scheme *model.Scheme, offset int, limit int) ([]*model.Team, *model.AppError) {
+ if err := a.IsPhase2MigrationCompleted(); err != nil {
+ return nil, err
+ }
+
+ if result := <-a.Srv.Store.Team().GetTeamsByScheme(scheme.Id, offset, limit); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.([]*model.Team), nil
+ }
+}
+
+func (a *App) GetChannelsForSchemePage(scheme *model.Scheme, page int, perPage int) (model.ChannelList, *model.AppError) {
+ if err := a.IsPhase2MigrationCompleted(); err != nil {
+ return nil, err
+ }
+
+ return a.GetChannelsForScheme(scheme, page*perPage, perPage)
+}
+
+func (a *App) GetChannelsForScheme(scheme *model.Scheme, offset int, limit int) (model.ChannelList, *model.AppError) {
+ if err := a.IsPhase2MigrationCompleted(); err != nil {
+ return nil, err
+ }
+
+ if result := <-a.Srv.Store.Channel().GetChannelsByScheme(scheme.Id, offset, limit); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(model.ChannelList), nil
+ }
+}
+
+func (a *App) IsPhase2MigrationCompleted() *model.AppError {
+ if a.phase2PermissionsMigrationComplete {
+ return nil
+ }
+
+ if result := <-a.Srv.Store.System().GetByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2); result.Err != nil {
+ return model.NewAppError("App.IsPhase2MigrationCompleted", "app.schemes.is_phase_2_migration_completed.not_completed.app_error", nil, result.Err.Error(), http.StatusNotImplemented)
+ }
+
+ a.phase2PermissionsMigrationComplete = true
+
+ return nil
+}
+
+func (a *App) SchemesIterator(batchSize int) func() []*model.Scheme {
+ offset := 0
+ return func() []*model.Scheme {
+ var result store.StoreResult
+ if result = <-a.Srv.Store.Scheme().GetAllPage("", offset, batchSize); result.Err != nil {
+ return []*model.Scheme{}
+ }
+ offset += batchSize
+ return result.Data.([]*model.Scheme)
+ }
+}
diff --git a/app/team.go b/app/team.go
index aca99dd1e..2833e2eed 100644
--- a/app/team.go
+++ b/app/team.go
@@ -114,6 +114,24 @@ func (a *App) UpdateTeam(team *model.Team) (*model.Team, *model.AppError) {
return oldTeam, nil
}
+func (a *App) UpdateTeamScheme(team *model.Team) (*model.Team, *model.AppError) {
+ var oldTeam *model.Team
+ var err *model.AppError
+ if oldTeam, err = a.GetTeam(team.Id); err != nil {
+ return nil, err
+ }
+
+ oldTeam.SchemeId = team.SchemeId
+
+ if result := <-a.Srv.Store.Team().Update(oldTeam); result.Err != nil {
+ return nil, result.Err
+ }
+
+ a.sendTeamEvent(oldTeam, model.WEBSOCKET_EVENT_UPDATE_TEAM)
+
+ return oldTeam, nil
+}
+
func (a *App) PatchTeam(teamId string, patch *model.TeamPatch) (*model.Team, *model.AppError) {
team, err := a.GetTeam(teamId)
if err != nil {
@@ -142,17 +160,31 @@ func (a *App) sendTeamEvent(team *model.Team, event string) {
a.Publish(message)
}
+func (a *App) GetSchemeRolesForTeam(teamId string) (string, string, *model.AppError) {
+ var team *model.Team
+ var err *model.AppError
+
+ if team, err = a.GetTeam(teamId); err != nil {
+ return "", "", err
+ }
+
+ if team.SchemeId != nil && len(*team.SchemeId) != 0 {
+ if scheme, err := a.GetScheme(*team.SchemeId); err != nil {
+ return "", "", err
+ } else {
+ return scheme.DefaultTeamUserRole, scheme.DefaultTeamAdminRole, nil
+ }
+ }
+
+ return model.TEAM_USER_ROLE_ID, model.TEAM_ADMIN_ROLE_ID, nil
+}
+
func (a *App) UpdateTeamMemberRoles(teamId string, userId string, newRoles string) (*model.TeamMember, *model.AppError) {
var member *model.TeamMember
- if result := <-a.Srv.Store.Team().GetTeamsForUser(userId); result.Err != nil {
+ if result := <-a.Srv.Store.Team().GetMember(teamId, userId); result.Err != nil {
return nil, result.Err
} else {
- members := result.Data.([]*model.TeamMember)
- for _, m := range members {
- if m.TeamId == teamId {
- member = m
- }
- }
+ member = result.Data.(*model.TeamMember)
}
if member == nil {
@@ -160,14 +192,42 @@ func (a *App) UpdateTeamMemberRoles(teamId string, userId string, newRoles strin
return nil, err
}
- if err := a.CheckRolesExist(strings.Fields(newRoles)); err != nil {
+ schemeUserRole, schemeAdminRole, err := a.GetSchemeRolesForTeam(teamId)
+ if err != nil {
return nil, err
}
- member.Roles = newRoles
+ var newExplicitRoles []string
+ member.SchemeUser = false
+ member.SchemeAdmin = false
+
+ for _, roleName := range strings.Fields(newRoles) {
+ if role, err := a.GetRoleByName(roleName); err != nil {
+ err.StatusCode = http.StatusBadRequest
+ return nil, err
+ } else if !role.SchemeManaged {
+ // The role is not scheme-managed, so it's OK to apply it to the explicit roles field.
+ newExplicitRoles = append(newExplicitRoles, roleName)
+ } else {
+ // The role is scheme-managed, so need to check if it is part of the scheme for this channel or not.
+ switch roleName {
+ case schemeAdminRole:
+ member.SchemeAdmin = true
+ case schemeUserRole:
+ member.SchemeUser = true
+ default:
+ // If not part of the scheme for this channel, then it is not allowed to apply it as an explicit role.
+ return nil, model.NewAppError("UpdateTeamMemberRoles", "api.channel.update_team_member_roles.scheme_role.app_error", nil, "role_name="+roleName, http.StatusBadRequest)
+ }
+ }
+ }
+
+ member.ExplicitRoles = strings.Join(newExplicitRoles, " ")
if result := <-a.Srv.Store.Team().UpdateMember(member); result.Err != nil {
return nil, result.Err
+ } else {
+ member = result.Data.(*model.TeamMember)
}
a.ClearSessionCacheForUser(userId)
@@ -293,13 +353,13 @@ func (a *App) AddUserToTeamByInviteId(inviteId string, userId string) (*model.Te
// 3. a pointer to an AppError if something went wrong.
func (a *App) joinUserToTeam(team *model.Team, user *model.User) (*model.TeamMember, bool, *model.AppError) {
tm := &model.TeamMember{
- TeamId: team.Id,
- UserId: user.Id,
- Roles: model.TEAM_USER_ROLE_ID,
+ TeamId: team.Id,
+ UserId: user.Id,
+ SchemeUser: true,
}
if team.Email == user.Email {
- tm.Roles = model.TEAM_USER_ROLE_ID + " " + model.TEAM_ADMIN_ROLE_ID
+ tm.SchemeAdmin = true
}
if etmr := <-a.Srv.Store.Team().GetMember(team.Id, user.Id); etmr.Err == nil {
@@ -343,15 +403,11 @@ func (a *App) JoinUserToTeam(team *model.Team, user *model.User, userRequestorId
return uua.Err
}
- channelRole := model.CHANNEL_USER_ROLE_ID
-
- if team.Email == user.Email {
- channelRole = model.CHANNEL_USER_ROLE_ID + " " + model.CHANNEL_ADMIN_ROLE_ID
- }
+ shouldBeAdmin := team.Email == user.Email
// Soft error if there is an issue joining the default channels
- if err := a.JoinDefaultChannels(team.Id, user, channelRole, userRequestorId); err != nil {
- mlog.Error(fmt.Sprintf("Encountered an issue joining default channels user_id=%s, team_id=%s, err=%v", user.Id, team.Id, err), mlog.String("user_id", user.Id))
+ if err := a.JoinDefaultChannels(team.Id, user, shouldBeAdmin, userRequestorId); err != nil {
+ mlog.Error(fmt.Sprintf("Encountered an issue joining default channels err=%v", err), mlog.String("user_id", user.Id), mlog.String("team_id", team.Id))
}
a.ClearSessionCacheForUser(user.Id)
diff --git a/app/team_test.go b/app/team_test.go
index 7ebfb8166..6a47da58b 100644
--- a/app/team_test.go
+++ b/app/team_test.go
@@ -559,3 +559,21 @@ func TestJoinUserToTeam(t *testing.T) {
}
})
}
+
+func TestAppUpdateTeamScheme(t *testing.T) {
+ th := Setup().InitBasic()
+ defer th.TearDown()
+
+ team := th.BasicTeam
+ mockID := model.NewString("x")
+ team.SchemeId = mockID
+
+ updatedTeam, err := th.App.UpdateTeamScheme(th.BasicTeam)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if updatedTeam.SchemeId != mockID {
+ t.Fatal("Wrong Team SchemeId")
+ }
+}
diff --git a/cmd/mattermost/commands/init.go b/cmd/mattermost/commands/init.go
index aea2b1230..ea7e8ec84 100644
--- a/cmd/mattermost/commands/init.go
+++ b/cmd/mattermost/commands/init.go
@@ -23,6 +23,7 @@ func InitDBCommandContextCobra(command *cobra.Command) (*app.App, error) {
}
a.DoAdvancedPermissionsMigration()
+ a.DoEmojisPermissionsMigration()
return a, nil
}
diff --git a/cmd/mattermost/commands/permissions.go b/cmd/mattermost/commands/permissions.go
index 52cae0ecb..9d9962ce5 100644
--- a/cmd/mattermost/commands/permissions.go
+++ b/cmd/mattermost/commands/permissions.go
@@ -6,8 +6,11 @@ package commands
import (
"errors"
"fmt"
+ "os"
"github.com/spf13/cobra"
+
+ "github.com/mattermost/mattermost-server/utils"
)
var PermissionsCmd = &cobra.Command{
@@ -23,11 +26,32 @@ var ResetPermissionsCmd = &cobra.Command{
RunE: resetPermissionsCmdF,
}
+var ExportPermissionsCmd = &cobra.Command{
+ Use: "export",
+ Short: "Export permissions data",
+ Long: "Export Roles and Schemes to JSONL for use by Mattermost permissions import.",
+ Example: " permissions export > export.jsonl",
+ RunE: exportPermissionsCmdF,
+ PreRun: func(cmd *cobra.Command, args []string) {
+ os.Setenv("MM_LOGSETTINGS_CONSOLELEVEL", "error")
+ },
+}
+
+var ImportPermissionsCmd = &cobra.Command{
+ Use: "import [file]",
+ Short: "Import permissions data",
+ Long: "Import Roles and Schemes JSONL data as created by the Mattermost permissions export.",
+ Example: " permissions import export.jsonl",
+ RunE: importPermissionsCmdF,
+}
+
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,
+ ExportPermissionsCmd,
+ ImportPermissionsCmd,
)
RootCmd.AddCommand(PermissionsCmd)
}
@@ -58,7 +82,51 @@ func resetPermissionsCmdF(command *cobra.Command, args []string) error {
return errors.New(err.Error())
}
- CommandPrettyPrintln("Permissions system successfully reset")
+ CommandPrettyPrintln("Permissions system successfully reset.")
+ CommandPrettyPrintln("Changes will take effect gradually as the server caches expire.")
+ CommandPrettyPrintln("For the changes to take effect immediately, go to the Mattermost System Console > General > Configuration and click \"Purge All Caches\".")
+
+ return nil
+}
+
+func exportPermissionsCmdF(command *cobra.Command, args []string) error {
+ a, err := InitDBCommandContextCobra(command)
+ if err != nil {
+ return err
+ }
+ defer a.Shutdown()
+
+ if license := a.License(); license == nil {
+ return errors.New(utils.T("cli.license.critical"))
+ }
+
+ if err = a.ExportPermissions(os.Stdout); err != nil {
+ return errors.New(err.Error())
+ }
+
+ return nil
+}
+
+func importPermissionsCmdF(command *cobra.Command, args []string) error {
+ a, err := InitDBCommandContextCobra(command)
+ if err != nil {
+ return err
+ }
+ defer a.Shutdown()
+
+ if license := a.License(); license == nil {
+ return errors.New(utils.T("cli.license.critical"))
+ }
+
+ file, err := os.Open(args[0])
+ if err != nil {
+ return err
+ }
+ defer file.Close()
+
+ if err := a.ImportPermissions(file); err != nil {
+ return err
+ }
return nil
}
diff --git a/cmd/mattermost/commands/permissions_test.go b/cmd/mattermost/commands/permissions_test.go
new file mode 100644
index 000000000..eeaa17109
--- /dev/null
+++ b/cmd/mattermost/commands/permissions_test.go
@@ -0,0 +1,40 @@
+// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package commands
+
+import (
+ "os"
+ "os/exec"
+ "strings"
+ "testing"
+
+ "github.com/mattermost/mattermost-server/api4"
+ "github.com/mattermost/mattermost-server/utils"
+)
+
+func TestPermissionsExport_rejectsUnlicensed(t *testing.T) {
+ permissionsLicenseRequiredTest(t, "export")
+}
+
+func TestPermissionsImport_rejectsUnlicensed(t *testing.T) {
+ permissionsLicenseRequiredTest(t, "import")
+}
+
+func permissionsLicenseRequiredTest(t *testing.T, subcommand string) {
+ th := api4.Setup().InitBasic()
+ defer th.TearDown()
+
+ path, err := os.Executable()
+ if err != nil {
+ t.Fail()
+ }
+ args := []string{"-test.run", "ExecCommand", "--", "--disableconfigwatch", "permissions", subcommand}
+ output, err := exec.Command(path, args...).CombinedOutput()
+
+ actual := string(output)
+ expected := utils.T("cli.license.critical")
+ if !strings.Contains(actual, expected) {
+ t.Errorf("Expected '%v' but got '%v'.", expected, actual)
+ }
+}
diff --git a/cmd/mattermost/commands/roles.go b/cmd/mattermost/commands/roles.go
index b8fdcdba2..f1c9d34c8 100644
--- a/cmd/mattermost/commands/roles.go
+++ b/cmd/mattermost/commands/roles.go
@@ -5,8 +5,11 @@ package commands
import (
"errors"
+ "strings"
"github.com/spf13/cobra"
+
+ "github.com/mattermost/mattermost-server/model"
)
var RolesCmd = &cobra.Command{
@@ -55,7 +58,27 @@ func makeSystemAdminCmdF(command *cobra.Command, args []string) error {
return errors.New("Unable to find user '" + args[i] + "'")
}
- if _, err := a.UpdateUserRoles(user.Id, "system_admin system_user", true); err != nil {
+ systemAdmin := false
+ systemUser := false
+
+ roles := strings.Fields(user.Roles)
+ for _, role := range roles {
+ switch role {
+ case model.SYSTEM_ADMIN_ROLE_ID:
+ systemAdmin = true
+ case model.SYSTEM_USER_ROLE_ID:
+ systemUser = true
+ }
+ }
+
+ if !systemUser {
+ roles = append(roles, model.SYSTEM_USER_ROLE_ID)
+ }
+ if !systemAdmin {
+ roles = append(roles, model.SYSTEM_ADMIN_ROLE_ID)
+ }
+
+ if _, err := a.UpdateUserRoles(user.Id, strings.Join(roles, " "), true); err != nil {
return err
}
}
@@ -80,7 +103,26 @@ func makeMemberCmdF(command *cobra.Command, args []string) error {
return errors.New("Unable to find user '" + args[i] + "'")
}
- if _, err := a.UpdateUserRoles(user.Id, "system_user", true); err != nil {
+ systemUser := false
+ var newRoles []string
+
+ roles := strings.Fields(user.Roles)
+ for _, role := range roles {
+ switch role {
+ case model.SYSTEM_ADMIN_ROLE_ID:
+ default:
+ if role == model.SYSTEM_USER_ROLE_ID {
+ systemUser = true
+ }
+ newRoles = append(newRoles, role)
+ }
+ }
+
+ if !systemUser {
+ newRoles = append(roles, model.SYSTEM_USER_ROLE_ID)
+ }
+
+ if _, err := a.UpdateUserRoles(user.Id, strings.Join(newRoles, " "), true); err != nil {
return err
}
}
diff --git a/cmd/mattermost/commands/roles_test.go b/cmd/mattermost/commands/roles_test.go
index 0144bf0df..4f11ce7ed 100644
--- a/cmd/mattermost/commands/roles_test.go
+++ b/cmd/mattermost/commands/roles_test.go
@@ -20,8 +20,19 @@ func TestAssignRole(t *testing.T) {
t.Fatal()
} else {
user := result.Data.(*model.User)
- if user.Roles != "system_admin system_user" {
- t.Fatal()
+ if user.Roles != "system_user system_admin" {
+ t.Fatal("Got wrong roles:", user.Roles)
+ }
+ }
+
+ CheckCommand(t, "roles", "member", th.BasicUser.Email)
+
+ if result := <-th.App.Srv.Store.User().GetByEmail(th.BasicUser.Email); result.Err != nil {
+ t.Fatal()
+ } else {
+ user := result.Data.(*model.User)
+ if user.Roles != "system_user" {
+ t.Fatal("Got wrong roles:", user.Roles, user.Id)
}
}
}
diff --git a/cmd/mattermost/commands/server.go b/cmd/mattermost/commands/server.go
index 20ebfade6..10b44ad83 100644
--- a/cmd/mattermost/commands/server.go
+++ b/cmd/mattermost/commands/server.go
@@ -96,6 +96,7 @@ func runServer(configFileLocation string, disableConfigWatch bool, usedPlatform
}
a.DoAdvancedPermissionsMigration()
+ a.DoEmojisPermissionsMigration()
a.InitPlugins(*a.Config().PluginSettings.Directory, *a.Config().PluginSettings.ClientDirectory, nil)
a.AddConfigListener(func(prevCfg, cfg *model.Config) {
diff --git a/einterfaces/emoji.go b/einterfaces/emoji.go
deleted file mode 100644
index b8d61e748..000000000
--- a/einterfaces/emoji.go
+++ /dev/null
@@ -1,12 +0,0 @@
-// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-package einterfaces
-
-import (
- "github.com/mattermost/mattermost-server/model"
-)
-
-type EmojiInterface interface {
- CanUserCreateEmoji(string, []*model.TeamMember) bool
-}
diff --git a/i18n/en.json b/i18n/en.json
index c0b80b86a..ac3cb4110 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -48,6 +48,54 @@
"translation": "September"
},
{
+ "id": "migrations.worker.run_advanced_permissions_phase_2_migration.invalid_progress",
+ "translation": "Migration failed due to invalid progress data."
+ },
+ {
+ "id": "migrations.worker.run_migration.unknown_key",
+ "translation": "Cannot run migration job due to unknown migration key."
+ },
+ {
+ "id": "store.sql_channel.migrate_channel_members.open_transaction.app_error",
+ "translation": "Failed to open the database transaction"
+ },
+ {
+ "id": "store.sql_channel.migrate_channel_members.select.app_error",
+ "translation": "Failed to select the batch of channel members"
+ },
+ {
+ "id": "store.sql_channel.migrate_channel_members.rollback_transaction.app_error",
+ "translation": "Failed to roll back the database transaction"
+ },
+ {
+ "id": "store.sql_channel.migrate_channel_members.update.app_error",
+ "translation": "Failed to update the channel member"
+ },
+ {
+ "id": "store.sql_channel.migrate_channel_members.commit_transaction.app_error",
+ "translation": "Failed to commit the database transaction"
+ },
+ {
+ "id": "store.sql_team.migrate_team_members.open_transaction.app_error",
+ "translation": "Failed to open the database transaction"
+ },
+ {
+ "id": "store.sql_team.migrate_team_members.select.app_error",
+ "translation": " Failed to select the batch of team members"
+ },
+ {
+ "id": "store.sql_team.migrate_team_members.rollback_transaction.app_error",
+ "translation": "Failed to roll back the database transaction"
+ },
+ {
+ "id": "store.sql_team.migrate_team_members.update.app_error",
+ "translation": "Failed to update the team member"
+ },
+ {
+ "id": "store.sql_team.migrate_team_members.commit_transaction.app_error",
+ "translation": "Failed to commit the database transaction"
+ },
+ {
"id": "api.admin.add_certificate.no_file.app_error",
"translation": "No file under 'certificate' in request."
},
@@ -196,6 +244,10 @@
"translation": "Private Channel management and creation is restricted to System Administrators."
},
{
+ "id": "app.schemes.is_phase_2_migration_completed.not_completed.app_error",
+ "translation": "This API endpoint is not accessible as required migrations have not yet completed."
+ },
+ {
"id": "api.channel.can_manage_channel.private_restricted_team_admin.app_error",
"translation": "Private Channel management and creation is restricted to Team and System Administrators."
},
@@ -224,6 +276,18 @@
"translation": "The channel requested to convert is already a private channel."
},
{
+ "id": "store.sql_channel.reset_all_channel_schemes.app_error",
+ "translation": "We could not reset the channel schemes"
+ },
+ {
+ "id": "store.sql_scheme.permanent_delete_all.app_error",
+ "translation": "We could not permanently delete the schemes"
+ },
+ {
+ "id": "store.sql_team.reset_all_team_schemes.app_error",
+ "translation": "We could not reset the team schemes"
+ },
+ {
"id": "api.channel.create_channel.direct_channel.app_error",
"translation": "Must use createDirectChannel API service for direct message channel creation"
},
@@ -1273,10 +1337,6 @@
"translation": "Unable to delete reactions when deleting emoji with emoji name %v"
},
{
- "id": "api.emoji.delete.permissions.app_error",
- "translation": "Invalid permissions to delete emoji."
- },
- {
"id": "api.emoji.disabled.app_error",
"translation": "Custom emoji have been disabled by the system admin."
},
@@ -2503,6 +2563,14 @@
"translation": "You do not have the appropriate permissions"
},
{
+ "id": "api.team.update_team_scheme.license.error",
+ "translation": "License does not support updating a team's scheme"
+ },
+ {
+ "id": "api.team.update_team_scheme.scheme_scope.error",
+ "translation": "Unable to set the scheme to the team because the supplied scheme is not a team scheme."
+ },
+ {
"id": "api.templates.channel_name.group",
"translation": "Group Message"
},
@@ -6703,6 +6771,14 @@
"translation": "Unable to delete reaction"
},
{
+ "id": "api.scheme.get_teams_for_scheme.scope.error",
+ "translation": "Unable to get the teams for scheme because the supplied scheme is not a team scheme."
+ },
+ {
+ "id": "api.scheme.get_channels_for_scheme.scope.error",
+ "translation": "Unable to get the channels for scheme because the supplied scheme is not a channel scheme."
+ },
+ {
"id": "store.sql_reaction.delete_all_with_emoj_name.delete_reactions.app_error",
"translation": "Unable to delete reactions with the given emoji name"
},
@@ -6743,6 +6819,94 @@
"translation": "Unable to get role"
},
{
+ "id": "api.channel.update_channel_member_roles.scheme_role.app_error",
+ "translation": "The provided role is managed by a Scheme and therefore cannot be applied directly to a Channel Member"
+ },
+ {
+ "id": "api.channel.update_team_member_roles.scheme_role.app_error",
+ "translation": "The provided role is managed by a Scheme and therefore cannot be applied directly to a Team Member"
+ },
+ {
+ "id": "api.channel.update_channel_scheme.license.error",
+ "translation": "License does not support updating a channel's scheme"
+ },
+ {
+ "id": "api.channel.update_channel_scheme.scheme_scope.error",
+ "translation": "Unable to set the scheme to the channel because the supplied scheme is not a channel scheme."
+ },
+ {
+ "id": "store.sql_channel.get_by_scheme.app_error",
+ "translation": "Unable to get the channels for the provided scheme"
+ },
+ {
+ "id": "store.sql_team.get_by_scheme.app_error",
+ "translation": "Unable to get the channels for the provided scheme"
+ },
+ {
+ "id": "store.sql_role.save.open_transaction.app_error",
+ "translation": "Failed to open the transaction to save the role"
+ },
+ {
+ "id": "store.sql_role.save_role.commit_transaction.app_error",
+ "translation": "Failed to commit the transaction to save the role"
+ },
+ {
+ "id": "store.sql_role.save.invalid_role.app_error",
+ "translation": "The provided role is invalid"
+ },
+ {
+ "id": "store.sql_role.delete.update.app_error",
+ "translation": "Unable to delete the role"
+ },
+ {
+ "id": "store.sql_scheme.save.open_transaction.app_error",
+ "translation": "Failed to open the transaction to save the scheme"
+ },
+ {
+ "id": "store.sql_scheme.save_scheme.commit_transaction.app_error",
+ "translation": "Failed to commit the transaction to save the scheme"
+ },
+ {
+ "id": "store.sql_scheme.save.invalid_scheme.app_error",
+ "translation": "The provided scheme is invalid"
+ },
+ {
+ "id": "store.sql_scheme.save.update.app_error",
+ "translation": "Unable to update the scheme"
+ },
+ {
+ "id": "store.sql_scheme.save.retrieve_default_scheme_roles.app_error",
+ "translation": "Unable to retrieve the default scheme roles"
+ },
+ {
+ "id": "store.sql_scheme.save.insert.app_error",
+ "translation": "Unable to create the scheme"
+ },
+ {
+ "id": "store.sql_scheme.get.app_error",
+ "translation": "Unable to get the scheme"
+ },
+ {
+ "id": "store.sql_scheme.reset_teams.app_error",
+ "translation": "Unable to reset all teams using this scheme to the default scheme"
+ },
+ {
+ "id": "store.sql_scheme.delete.scheme_in_use.app_error",
+ "translation": "Unable to delete the scheme as it in use by 1 or more teams or channels"
+ },
+ {
+ "id": "store.sql_scheme.reset_channels.app_error",
+ "translation": "Unable to reset all channels using this scheme to the default scheme"
+ },
+ {
+ "id": "store.sql_scheme.delete.role_update.app_error",
+ "translation": "Unable to delete the roles belonging to this scheme"
+ },
+ {
+ "id": "store.sql_scheme.delete.update.app_error",
+ "translation": "Unable to delete the scheme"
+ },
+ {
"id": "store.sql_role.get_by_names.app_error",
"translation": "Unable to get roles"
},
diff --git a/imports/placeholder.go b/imports/placeholder.go
index 98e5decd5..b7a5d449c 100644
--- a/imports/placeholder.go
+++ b/imports/placeholder.go
@@ -4,3 +4,7 @@
package imports
// This is a placeholder so this package can be imported in Team Edition when it will be otherwise empty
+
+import (
+ _ "github.com/mattermost/mattermost-server/migrations"
+)
diff --git a/jobs/interfaces/migrations_interface.go b/jobs/interfaces/migrations_interface.go
new file mode 100644
index 000000000..48dc9f579
--- /dev/null
+++ b/jobs/interfaces/migrations_interface.go
@@ -0,0 +1,11 @@
+// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package interfaces
+
+import "github.com/mattermost/mattermost-server/model"
+
+type MigrationsJobInterface interface {
+ MakeWorker() model.Worker
+ MakeScheduler() model.Scheduler
+}
diff --git a/jobs/jobs.go b/jobs/jobs.go
index 850491403..ddbc4489b 100644
--- a/jobs/jobs.go
+++ b/jobs/jobs.go
@@ -106,6 +106,13 @@ func (srv *JobServer) SetJobCanceled(job *model.Job) *model.AppError {
return result.Err
}
+func (srv *JobServer) UpdateInProgressJobData(job *model.Job) *model.AppError {
+ job.Status = model.JOB_STATUS_IN_PROGRESS
+ job.LastActivityAt = model.GetMillis()
+ result := <-srv.Store.Job().UpdateOptimistically(job, model.JOB_STATUS_IN_PROGRESS)
+ return result.Err
+}
+
func (srv *JobServer) RequestCancellation(jobId string) *model.AppError {
if result := <-srv.Store.Job().UpdateStatusOptimistically(jobId, model.JOB_STATUS_PENDING, model.JOB_STATUS_CANCELED); result.Err != nil {
return result.Err
diff --git a/jobs/jobs_watcher.go b/jobs/jobs_watcher.go
index 07979442d..01d0a8d0f 100644
--- a/jobs/jobs_watcher.go
+++ b/jobs/jobs_watcher.go
@@ -107,6 +107,13 @@ func (watcher *Watcher) PollAndNotify() {
default:
}
}
+ } else if job.Type == model.JOB_TYPE_MIGRATIONS {
+ if watcher.workers.Migrations != nil {
+ select {
+ case watcher.workers.Migrations.JobChannel() <- *job:
+ default:
+ }
+ }
}
}
}
diff --git a/jobs/schedulers.go b/jobs/schedulers.go
index 2823036df..96aa2b635 100644
--- a/jobs/schedulers.go
+++ b/jobs/schedulers.go
@@ -50,6 +50,10 @@ func (srv *JobServer) InitSchedulers() *Schedulers {
schedulers.schedulers = append(schedulers.schedulers, ldapSyncInterface.MakeScheduler())
}
+ if migrationsInterface := srv.Migrations; migrationsInterface != nil {
+ schedulers.schedulers = append(schedulers.schedulers, migrationsInterface.MakeScheduler())
+ }
+
schedulers.nextRunTimes = make([]*time.Time, len(schedulers.schedulers))
return schedulers
}
diff --git a/jobs/server.go b/jobs/server.go
index 01cf821dc..10ea9a46f 100644
--- a/jobs/server.go
+++ b/jobs/server.go
@@ -5,6 +5,7 @@ package jobs
import (
ejobs "github.com/mattermost/mattermost-server/einterfaces/jobs"
+ tjobs "github.com/mattermost/mattermost-server/jobs/interfaces"
"github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/store"
)
@@ -34,6 +35,7 @@ type JobServer struct {
ElasticsearchAggregator ejobs.ElasticsearchAggregatorInterface
ElasticsearchIndexer ejobs.ElasticsearchIndexerInterface
LdapSync ejobs.LdapSyncInterface
+ Migrations tjobs.MigrationsJobInterface
}
func NewJobServer(configService ConfigService, store store.Store) *JobServer {
diff --git a/jobs/workers.go b/jobs/workers.go
index 57a255013..67ab43241 100644
--- a/jobs/workers.go
+++ b/jobs/workers.go
@@ -20,6 +20,7 @@ type Workers struct {
ElasticsearchIndexing model.Worker
ElasticsearchAggregation model.Worker
LdapSync model.Worker
+ Migrations model.Worker
listenerId string
}
@@ -50,6 +51,10 @@ func (srv *JobServer) InitWorkers() *Workers {
workers.LdapSync = ldapSyncInterface.MakeWorker()
}
+ if migrationsInterface := srv.Migrations; migrationsInterface != nil {
+ workers.Migrations = migrationsInterface.MakeWorker()
+ }
+
return workers
}
@@ -77,6 +82,10 @@ func (workers *Workers) Start() *Workers {
go workers.LdapSync.Run()
}
+ if workers.Migrations != nil {
+ go workers.Migrations.Run()
+ }
+
go workers.Watcher.Start()
})
@@ -152,6 +161,10 @@ func (workers *Workers) Stop() *Workers {
workers.LdapSync.Stop()
}
+ if workers.Migrations != nil {
+ workers.Migrations.Stop()
+ }
+
mlog.Info("Stopped workers")
return workers
diff --git a/migrations/advanced_permissions_phase_2.go b/migrations/advanced_permissions_phase_2.go
new file mode 100644
index 000000000..55b1876c4
--- /dev/null
+++ b/migrations/advanced_permissions_phase_2.go
@@ -0,0 +1,106 @@
+// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package migrations
+
+import (
+ "encoding/json"
+ "io"
+ "net/http"
+ "strings"
+
+ "github.com/mattermost/mattermost-server/model"
+)
+
+type AdvancedPermissionsPhase2Progress struct {
+ CurrentTable string `json:"current_table"`
+ LastTeamId string `json:"last_team_id"`
+ LastChannelId string `json:"last_channel_id"`
+ LastUserId string `json:"last_user"`
+}
+
+func (p *AdvancedPermissionsPhase2Progress) ToJson() string {
+ b, _ := json.Marshal(p)
+ return string(b)
+}
+
+func AdvancedPermissionsPhase2ProgressFromJson(data io.Reader) *AdvancedPermissionsPhase2Progress {
+ var o *AdvancedPermissionsPhase2Progress
+ json.NewDecoder(data).Decode(&o)
+ return o
+}
+
+func (p *AdvancedPermissionsPhase2Progress) IsValid() bool {
+ if len(p.LastChannelId) != 26 {
+ return false
+ }
+
+ if len(p.LastTeamId) != 26 {
+ return false
+ }
+
+ if len(p.LastUserId) != 26 {
+ return false
+ }
+
+ switch p.CurrentTable {
+ case "TeamMembers":
+ case "ChannelMembers":
+ default:
+ return false
+ }
+
+ return true
+}
+
+func (worker *Worker) runAdvancedPermissionsPhase2Migration(lastDone string) (bool, string, *model.AppError) {
+ var progress *AdvancedPermissionsPhase2Progress
+ if len(lastDone) == 0 {
+ // Haven't started the migration yet.
+ progress = new(AdvancedPermissionsPhase2Progress)
+ progress.CurrentTable = "TeamMembers"
+ progress.LastChannelId = strings.Repeat("0", 26)
+ progress.LastTeamId = strings.Repeat("0", 26)
+ progress.LastUserId = strings.Repeat("0", 26)
+ } else {
+ progress = AdvancedPermissionsPhase2ProgressFromJson(strings.NewReader(lastDone))
+ if !progress.IsValid() {
+ return false, "", model.NewAppError("MigrationsWorker.runAdvancedPermissionsPhase2Migration", "migrations.worker.run_advanced_permissions_phase_2_migration.invalid_progress", map[string]interface{}{"progress": progress.ToJson()}, "", http.StatusInternalServerError)
+ }
+ }
+
+ if progress.CurrentTable == "TeamMembers" {
+ // Run a TeamMembers migration batch.
+ if result := <-worker.app.Srv.Store.Team().MigrateTeamMembers(progress.LastTeamId, progress.LastUserId); result.Err != nil {
+ return false, progress.ToJson(), result.Err
+ } else {
+ if result.Data == nil {
+ // We haven't progressed. That means that we've reached the end of this stage of the migration, and should now advance to the next stage.
+ progress.LastUserId = strings.Repeat("0", 26)
+ progress.CurrentTable = "ChannelMembers"
+ return false, progress.ToJson(), nil
+ }
+
+ data := result.Data.(map[string]string)
+ progress.LastTeamId = data["TeamId"]
+ progress.LastUserId = data["UserId"]
+ }
+ } else if progress.CurrentTable == "ChannelMembers" {
+ // Run a ChannelMembers migration batch.
+ if result := <-worker.app.Srv.Store.Channel().MigrateChannelMembers(progress.LastChannelId, progress.LastUserId); result.Err != nil {
+ return false, progress.ToJson(), result.Err
+ } else {
+ if result.Data == nil {
+ // We haven't progressed. That means we've reached the end of this final stage of the migration.
+
+ return true, progress.ToJson(), nil
+ }
+
+ data := result.Data.(map[string]string)
+ progress.LastChannelId = data["ChannelId"]
+ progress.LastUserId = data["UserId"]
+ }
+ }
+
+ return false, progress.ToJson(), nil
+}
diff --git a/migrations/migrations.go b/migrations/migrations.go
new file mode 100644
index 000000000..940992839
--- /dev/null
+++ b/migrations/migrations.go
@@ -0,0 +1,63 @@
+// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package migrations
+
+import (
+ "github.com/mattermost/mattermost-server/app"
+ tjobs "github.com/mattermost/mattermost-server/jobs/interfaces"
+ "github.com/mattermost/mattermost-server/model"
+ "github.com/mattermost/mattermost-server/store"
+)
+
+const (
+ MIGRATION_STATE_UNSCHEDULED = "unscheduled"
+ MIGRATION_STATE_IN_PROGRESS = "in_progress"
+ MIGRATION_STATE_COMPLETED = "completed"
+
+ JOB_DATA_KEY_MIGRATION = "migration_key"
+ JOB_DATA_KEY_MIGRATION_LAST_DONE = "last_done"
+)
+
+type MigrationsJobInterfaceImpl struct {
+ App *app.App
+}
+
+func init() {
+ app.RegisterJobsMigrationsJobInterface(func(a *app.App) tjobs.MigrationsJobInterface {
+ return &MigrationsJobInterfaceImpl{a}
+ })
+}
+
+func MakeMigrationsList() []string {
+ return []string{
+ model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2,
+ }
+}
+
+func GetMigrationState(migration string, store store.Store) (string, *model.Job, *model.AppError) {
+ if result := <-store.System().GetByName(migration); result.Err == nil {
+ return MIGRATION_STATE_COMPLETED, nil, nil
+ }
+
+ if result := <-store.Job().GetAllByType(model.JOB_TYPE_MIGRATIONS); result.Err != nil {
+ return "", nil, result.Err
+ } else {
+ for _, job := range result.Data.([]*model.Job) {
+ if key, ok := job.Data[JOB_DATA_KEY_MIGRATION]; ok {
+ if key != migration {
+ continue
+ }
+
+ switch job.Status {
+ case model.JOB_STATUS_IN_PROGRESS, model.JOB_STATUS_PENDING:
+ return MIGRATION_STATE_IN_PROGRESS, job, nil
+ default:
+ return MIGRATION_STATE_UNSCHEDULED, job, nil
+ }
+ }
+ }
+ }
+
+ return MIGRATION_STATE_UNSCHEDULED, nil, nil
+}
diff --git a/migrations/migrations_test.go b/migrations/migrations_test.go
new file mode 100644
index 000000000..308319430
--- /dev/null
+++ b/migrations/migrations_test.go
@@ -0,0 +1,140 @@
+// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package migrations
+
+import (
+ "flag"
+ "os"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+ "github.com/mattermost/mattermost-server/mlog"
+ "github.com/mattermost/mattermost-server/model"
+ "github.com/mattermost/mattermost-server/store/storetest"
+ "github.com/mattermost/mattermost-server/utils"
+)
+
+func TestMain(m *testing.M) {
+ flag.Parse()
+
+ // Setup a global logger to catch tests logging outside of app context
+ // The global logger will be stomped by apps initalizing but that's fine for testing. Ideally this won't happen.
+ mlog.InitGlobalLogger(mlog.NewLogger(&mlog.LoggerConfiguration{
+ EnableConsole: true,
+ ConsoleJson: true,
+ ConsoleLevel: "error",
+ EnableFile: false,
+ }))
+
+ utils.TranslationsPreInit()
+
+ // In the case where a dev just wants to run a single test, it's faster to just use the default
+ // store.
+ if filter := flag.Lookup("test.run").Value.String(); filter != "" && filter != "." {
+ mlog.Info("-test.run used, not creating temporary containers")
+ os.Exit(m.Run())
+ }
+
+ status := 0
+
+ container, settings, err := storetest.NewMySQLContainer()
+ if err != nil {
+ panic(err)
+ }
+
+ UseTestStore(container, settings)
+
+ defer func() {
+ StopTestStore()
+ os.Exit(status)
+ }()
+
+ status = m.Run()
+}
+
+func TestGetMigrationState(t *testing.T) {
+ th := Setup()
+ defer th.TearDown()
+
+ migrationKey := model.NewId()
+
+ th.DeleteAllJobsByTypeAndMigrationKey(model.JOB_TYPE_MIGRATIONS, migrationKey)
+
+ // Test with no job yet.
+ state, job, err := GetMigrationState(migrationKey, th.App.Srv.Store)
+ assert.Nil(t, err)
+ assert.Nil(t, job)
+ assert.Equal(t, "unscheduled", state)
+
+ // Test with the system table showing the migration as done.
+ system := model.System{
+ Name: migrationKey,
+ Value: "true",
+ }
+ res1 := <-th.App.Srv.Store.System().Save(&system)
+ assert.Nil(t, res1.Err)
+
+ state, job, err = GetMigrationState(migrationKey, th.App.Srv.Store)
+ assert.Nil(t, err)
+ assert.Nil(t, job)
+ assert.Equal(t, "completed", state)
+
+ res2 := <-th.App.Srv.Store.System().PermanentDeleteByName(migrationKey)
+ assert.Nil(t, res2.Err)
+
+ // Test with a job scheduled in "pending" state.
+ j1 := &model.Job{
+ Id: model.NewId(),
+ CreateAt: model.GetMillis(),
+ Data: map[string]string{
+ JOB_DATA_KEY_MIGRATION: migrationKey,
+ },
+ Status: model.JOB_STATUS_PENDING,
+ Type: model.JOB_TYPE_MIGRATIONS,
+ }
+
+ j1 = (<-th.App.Srv.Store.Job().Save(j1)).Data.(*model.Job)
+
+ state, job, err = GetMigrationState(migrationKey, th.App.Srv.Store)
+ assert.Nil(t, err)
+ assert.Equal(t, j1.Id, job.Id)
+ assert.Equal(t, "in_progress", state)
+
+ // Test with a job scheduled in "in progress" state.
+ j2 := &model.Job{
+ Id: model.NewId(),
+ CreateAt: j1.CreateAt + 1,
+ Data: map[string]string{
+ JOB_DATA_KEY_MIGRATION: migrationKey,
+ },
+ Status: model.JOB_STATUS_IN_PROGRESS,
+ Type: model.JOB_TYPE_MIGRATIONS,
+ }
+
+ j2 = (<-th.App.Srv.Store.Job().Save(j2)).Data.(*model.Job)
+
+ state, job, err = GetMigrationState(migrationKey, th.App.Srv.Store)
+ assert.Nil(t, err)
+ assert.Equal(t, j2.Id, job.Id)
+ assert.Equal(t, "in_progress", state)
+
+ // Test with a job scheduled in "error" state.
+ j3 := &model.Job{
+ Id: model.NewId(),
+ CreateAt: j2.CreateAt + 1,
+ Data: map[string]string{
+ JOB_DATA_KEY_MIGRATION: migrationKey,
+ },
+ Status: model.JOB_STATUS_ERROR,
+ Type: model.JOB_TYPE_MIGRATIONS,
+ }
+
+ j3 = (<-th.App.Srv.Store.Job().Save(j3)).Data.(*model.Job)
+
+ state, job, err = GetMigrationState(migrationKey, th.App.Srv.Store)
+ assert.Nil(t, err)
+ assert.Equal(t, j3.Id, job.Id)
+ assert.Equal(t, "unscheduled", state)
+}
diff --git a/migrations/migrationstestlib.go b/migrations/migrationstestlib.go
new file mode 100644
index 000000000..b501291b0
--- /dev/null
+++ b/migrations/migrationstestlib.go
@@ -0,0 +1,425 @@
+// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package migrations
+
+import (
+ "encoding/json"
+ "io"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "time"
+
+ "github.com/mattermost/mattermost-server/app"
+ "github.com/mattermost/mattermost-server/einterfaces"
+ "github.com/mattermost/mattermost-server/mlog"
+ "github.com/mattermost/mattermost-server/model"
+ "github.com/mattermost/mattermost-server/plugin"
+ "github.com/mattermost/mattermost-server/plugin/pluginenv"
+ "github.com/mattermost/mattermost-server/store"
+ "github.com/mattermost/mattermost-server/store/sqlstore"
+ "github.com/mattermost/mattermost-server/store/storetest"
+ "github.com/mattermost/mattermost-server/utils"
+)
+
+type TestHelper struct {
+ App *app.App
+ BasicTeam *model.Team
+ BasicUser *model.User
+ BasicUser2 *model.User
+ BasicChannel *model.Channel
+ BasicPost *model.Post
+
+ SystemAdminUser *model.User
+
+ tempConfigPath string
+ tempWorkspace string
+ pluginHooks map[string]plugin.Hooks
+}
+
+type persistentTestStore struct {
+ store.Store
+}
+
+func (*persistentTestStore) Close() {}
+
+var testStoreContainer *storetest.RunningContainer
+var testStore *persistentTestStore
+var testStoreSqlSupplier *sqlstore.SqlSupplier
+var testClusterInterface *FakeClusterInterface
+
+// UseTestStore sets the container and corresponding settings to use for tests. Once the tests are
+// complete (e.g. at the end of your TestMain implementation), you should call StopTestStore.
+func UseTestStore(container *storetest.RunningContainer, settings *model.SqlSettings) {
+ testClusterInterface = &FakeClusterInterface{}
+ testStoreContainer = container
+ testStoreSqlSupplier = sqlstore.NewSqlSupplier(*settings, nil)
+ testStore = &persistentTestStore{store.NewLayeredStore(testStoreSqlSupplier, nil, testClusterInterface)}
+}
+
+func StopTestStore() {
+ if testStoreContainer != nil {
+ testStoreContainer.Stop()
+ testStoreContainer = nil
+ }
+}
+
+func setupTestHelper(enterprise bool) *TestHelper {
+ permConfig, err := os.Open(utils.FindConfigFile("config.json"))
+ if err != nil {
+ panic(err)
+ }
+ defer permConfig.Close()
+ tempConfig, err := ioutil.TempFile("", "")
+ if err != nil {
+ panic(err)
+ }
+ _, err = io.Copy(tempConfig, permConfig)
+ tempConfig.Close()
+ if err != nil {
+ panic(err)
+ }
+
+ options := []app.Option{app.ConfigFile(tempConfig.Name()), app.DisableConfigWatch}
+ if testStore != nil {
+ options = append(options, app.StoreOverride(testStore))
+ }
+
+ a, err := app.New(options...)
+ if err != nil {
+ panic(err)
+ }
+
+ th := &TestHelper{
+ App: a,
+ pluginHooks: make(map[string]plugin.Hooks),
+ tempConfigPath: tempConfig.Name(),
+ }
+
+ th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.MaxUsersPerTeam = 50 })
+ th.App.UpdateConfig(func(cfg *model.Config) { *cfg.RateLimitSettings.Enable = false })
+ prevListenAddress := *th.App.Config().ServiceSettings.ListenAddress
+ if testStore != nil {
+ th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ListenAddress = ":0" })
+ }
+ serverErr := th.App.StartServer()
+ if serverErr != nil {
+ panic(serverErr)
+ }
+
+ th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ListenAddress = prevListenAddress })
+
+ th.App.DoAdvancedPermissionsMigration()
+ th.App.DoEmojisPermissionsMigration()
+
+ th.App.Srv.Store.MarkSystemRanUnitTests()
+
+ th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.EnableOpenServer = true })
+
+ if enterprise {
+ th.App.SetLicense(model.NewTestLicense())
+ } else {
+ th.App.SetLicense(nil)
+ }
+
+ return th
+}
+
+func SetupEnterprise() *TestHelper {
+ return setupTestHelper(true)
+}
+
+func Setup() *TestHelper {
+ return setupTestHelper(false)
+}
+
+func (me *TestHelper) InitBasic() *TestHelper {
+ me.BasicTeam = me.CreateTeam()
+ me.BasicUser = me.CreateUser()
+ me.LinkUserToTeam(me.BasicUser, me.BasicTeam)
+ me.BasicUser2 = me.CreateUser()
+ me.LinkUserToTeam(me.BasicUser2, me.BasicTeam)
+ me.BasicChannel = me.CreateChannel(me.BasicTeam)
+ me.BasicPost = me.CreatePost(me.BasicChannel)
+
+ return me
+}
+
+func (me *TestHelper) InitSystemAdmin() *TestHelper {
+ me.SystemAdminUser = me.CreateUser()
+ me.App.UpdateUserRoles(me.SystemAdminUser.Id, model.SYSTEM_USER_ROLE_ID+" "+model.SYSTEM_ADMIN_ROLE_ID, false)
+ me.SystemAdminUser, _ = me.App.GetUser(me.SystemAdminUser.Id)
+
+ return me
+}
+
+func (me *TestHelper) MakeEmail() string {
+ return "success_" + model.NewId() + "@simulator.amazonses.com"
+}
+
+func (me *TestHelper) CreateTeam() *model.Team {
+ id := model.NewId()
+ team := &model.Team{
+ DisplayName: "dn_" + id,
+ Name: "name" + id,
+ Email: "success+" + id + "@simulator.amazonses.com",
+ Type: model.TEAM_OPEN,
+ }
+
+ utils.DisableDebugLogForTest()
+ var err *model.AppError
+ if team, err = me.App.CreateTeam(team); err != nil {
+ mlog.Error(err.Error())
+
+ time.Sleep(time.Second)
+ panic(err)
+ }
+ utils.EnableDebugLogForTest()
+ return team
+}
+
+func (me *TestHelper) CreateUser() *model.User {
+ id := model.NewId()
+
+ user := &model.User{
+ Email: "success+" + id + "@simulator.amazonses.com",
+ Username: "un_" + id,
+ Nickname: "nn_" + id,
+ Password: "Password1",
+ EmailVerified: true,
+ }
+
+ utils.DisableDebugLogForTest()
+ var err *model.AppError
+ if user, err = me.App.CreateUser(user); err != nil {
+ mlog.Error(err.Error())
+
+ time.Sleep(time.Second)
+ panic(err)
+ }
+ utils.EnableDebugLogForTest()
+ return user
+}
+
+func (me *TestHelper) CreateChannel(team *model.Team) *model.Channel {
+ return me.createChannel(team, model.CHANNEL_OPEN)
+}
+
+func (me *TestHelper) createChannel(team *model.Team, channelType string) *model.Channel {
+ id := model.NewId()
+
+ channel := &model.Channel{
+ DisplayName: "dn_" + id,
+ Name: "name_" + id,
+ Type: channelType,
+ TeamId: team.Id,
+ CreatorId: me.BasicUser.Id,
+ }
+
+ utils.DisableDebugLogForTest()
+ var err *model.AppError
+ if channel, err = me.App.CreateChannel(channel, true); err != nil {
+ mlog.Error(err.Error())
+
+ time.Sleep(time.Second)
+ panic(err)
+ }
+ utils.EnableDebugLogForTest()
+ return channel
+}
+
+func (me *TestHelper) CreateDmChannel(user *model.User) *model.Channel {
+ utils.DisableDebugLogForTest()
+ var err *model.AppError
+ var channel *model.Channel
+ if channel, err = me.App.CreateDirectChannel(me.BasicUser.Id, user.Id); err != nil {
+ mlog.Error(err.Error())
+
+ time.Sleep(time.Second)
+ panic(err)
+ }
+ utils.EnableDebugLogForTest()
+ return channel
+}
+
+func (me *TestHelper) CreatePost(channel *model.Channel) *model.Post {
+ id := model.NewId()
+
+ post := &model.Post{
+ UserId: me.BasicUser.Id,
+ ChannelId: channel.Id,
+ Message: "message_" + id,
+ CreateAt: model.GetMillis() - 10000,
+ }
+
+ utils.DisableDebugLogForTest()
+ var err *model.AppError
+ if post, err = me.App.CreatePost(post, channel, false); err != nil {
+ mlog.Error(err.Error())
+
+ time.Sleep(time.Second)
+ panic(err)
+ }
+ utils.EnableDebugLogForTest()
+ return post
+}
+
+func (me *TestHelper) LinkUserToTeam(user *model.User, team *model.Team) {
+ utils.DisableDebugLogForTest()
+
+ err := me.App.JoinUserToTeam(team, user, "")
+ if err != nil {
+ mlog.Error(err.Error())
+
+ time.Sleep(time.Second)
+ panic(err)
+ }
+
+ utils.EnableDebugLogForTest()
+}
+
+func (me *TestHelper) AddUserToChannel(user *model.User, channel *model.Channel) *model.ChannelMember {
+ utils.DisableDebugLogForTest()
+
+ member, err := me.App.AddUserToChannel(user, channel)
+ if err != nil {
+ mlog.Error(err.Error())
+
+ time.Sleep(time.Second)
+ panic(err)
+ }
+
+ utils.EnableDebugLogForTest()
+
+ return member
+}
+
+func (me *TestHelper) TearDown() {
+ me.App.Shutdown()
+ os.Remove(me.tempConfigPath)
+ if err := recover(); err != nil {
+ StopTestStore()
+ panic(err)
+ }
+ if me.tempWorkspace != "" {
+ os.RemoveAll(me.tempWorkspace)
+ }
+}
+
+type mockPluginSupervisor struct {
+ hooks plugin.Hooks
+}
+
+func (s *mockPluginSupervisor) Start(api plugin.API) error {
+ return s.hooks.OnActivate(api)
+}
+
+func (s *mockPluginSupervisor) Stop() error {
+ return nil
+}
+
+func (s *mockPluginSupervisor) Hooks() plugin.Hooks {
+ return s.hooks
+}
+
+func (s *mockPluginSupervisor) Wait() error { return nil }
+
+func (me *TestHelper) InstallPlugin(manifest *model.Manifest, hooks plugin.Hooks) {
+ if me.tempWorkspace == "" {
+ dir, err := ioutil.TempDir("", "apptest")
+ if err != nil {
+ panic(err)
+ }
+ me.tempWorkspace = dir
+ }
+
+ pluginDir := filepath.Join(me.tempWorkspace, "plugins")
+ webappDir := filepath.Join(me.tempWorkspace, "webapp")
+ me.App.InitPlugins(pluginDir, webappDir, func(bundle *model.BundleInfo) (plugin.Supervisor, error) {
+ if hooks, ok := me.pluginHooks[bundle.Manifest.Id]; ok {
+ return &mockPluginSupervisor{hooks}, nil
+ }
+ return pluginenv.DefaultSupervisorProvider(bundle)
+ })
+
+ me.pluginHooks[manifest.Id] = hooks
+
+ manifestCopy := *manifest
+ if manifestCopy.Backend == nil {
+ manifestCopy.Backend = &model.ManifestBackend{}
+ }
+ manifestBytes, err := json.Marshal(&manifestCopy)
+ if err != nil {
+ panic(err)
+ }
+
+ if err := os.MkdirAll(filepath.Join(pluginDir, manifest.Id), 0700); err != nil {
+ panic(err)
+ }
+
+ if err := ioutil.WriteFile(filepath.Join(pluginDir, manifest.Id, "plugin.json"), manifestBytes, 0600); err != nil {
+ panic(err)
+ }
+}
+
+func (me *TestHelper) ResetRoleMigration() {
+ if _, err := testStoreSqlSupplier.GetMaster().Exec("DELETE from Roles"); err != nil {
+ panic(err)
+ }
+
+ testClusterInterface.sendClearRoleCacheMessage()
+
+ if _, err := testStoreSqlSupplier.GetMaster().Exec("DELETE from Systems where Name = :Name", map[string]interface{}{"Name": app.ADVANCED_PERMISSIONS_MIGRATION_KEY}); err != nil {
+ panic(err)
+ }
+}
+
+func (me *TestHelper) DeleteAllJobsByTypeAndMigrationKey(jobType string, migrationKey string) {
+ if res := <-me.App.Srv.Store.Job().GetAllByType(model.JOB_TYPE_MIGRATIONS); res.Err != nil {
+ panic(res.Err)
+ } else {
+ jobs := res.Data.([]*model.Job)
+
+ for _, job := range jobs {
+ if key, ok := job.Data[JOB_DATA_KEY_MIGRATION]; ok && key == migrationKey {
+ if res := <-me.App.Srv.Store.Job().Delete(job.Id); res.Err != nil {
+ panic(res.Err)
+ }
+ }
+ }
+ }
+}
+
+type FakeClusterInterface struct {
+ clusterMessageHandler einterfaces.ClusterMessageHandler
+}
+
+func (me *FakeClusterInterface) StartInterNodeCommunication() {}
+func (me *FakeClusterInterface) StopInterNodeCommunication() {}
+func (me *FakeClusterInterface) RegisterClusterMessageHandler(event string, crm einterfaces.ClusterMessageHandler) {
+ me.clusterMessageHandler = crm
+}
+func (me *FakeClusterInterface) GetClusterId() string { return "" }
+func (me *FakeClusterInterface) IsLeader() bool { return false }
+func (me *FakeClusterInterface) GetMyClusterInfo() *model.ClusterInfo { return nil }
+func (me *FakeClusterInterface) GetClusterInfos() []*model.ClusterInfo { return nil }
+func (me *FakeClusterInterface) SendClusterMessage(cluster *model.ClusterMessage) {}
+func (me *FakeClusterInterface) NotifyMsg(buf []byte) {}
+func (me *FakeClusterInterface) GetClusterStats() ([]*model.ClusterStats, *model.AppError) {
+ return nil, nil
+}
+func (me *FakeClusterInterface) GetLogs(page, perPage int) ([]string, *model.AppError) {
+ return []string{}, nil
+}
+func (me *FakeClusterInterface) ConfigChanged(previousConfig *model.Config, newConfig *model.Config, sendToOtherServer bool) *model.AppError {
+ return nil
+}
+func (me *FakeClusterInterface) sendClearRoleCacheMessage() {
+ me.clusterMessageHandler(&model.ClusterMessage{
+ Event: model.CLUSTER_EVENT_INVALIDATE_CACHE_FOR_ROLES,
+ })
+}
+func (me *FakeClusterInterface) GetPluginStatuses() (model.PluginStatuses, *model.AppError) {
+ return nil, nil
+}
diff --git a/migrations/scheduler.go b/migrations/scheduler.go
new file mode 100644
index 000000000..8a7ac30d0
--- /dev/null
+++ b/migrations/scheduler.go
@@ -0,0 +1,110 @@
+// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package migrations
+
+import (
+ "time"
+
+ "github.com/mattermost/mattermost-server/app"
+ "github.com/mattermost/mattermost-server/mlog"
+ "github.com/mattermost/mattermost-server/model"
+ "github.com/mattermost/mattermost-server/store"
+)
+
+const (
+ MIGRATION_JOB_WEDGED_TIMEOUT_MILLISECONDS = 3600000 // 1 hour
+)
+
+type Scheduler struct {
+ App *app.App
+ allMigrationsCompleted bool
+}
+
+func (m *MigrationsJobInterfaceImpl) MakeScheduler() model.Scheduler {
+ return &Scheduler{m.App, false}
+}
+
+func (scheduler *Scheduler) Name() string {
+ return "MigrationsScheduler"
+}
+
+func (scheduler *Scheduler) JobType() string {
+ return model.JOB_TYPE_MIGRATIONS
+}
+
+func (scheduler *Scheduler) Enabled(cfg *model.Config) bool {
+ return true
+}
+
+func (scheduler *Scheduler) NextScheduleTime(cfg *model.Config, now time.Time, pendingJobs bool, lastSuccessfulJob *model.Job) *time.Time {
+ if scheduler.allMigrationsCompleted {
+ return nil
+ }
+
+ nextTime := time.Now().Add(60 * time.Second)
+ return &nextTime
+}
+
+func (scheduler *Scheduler) ScheduleJob(cfg *model.Config, pendingJobs bool, lastSuccessfulJob *model.Job) (*model.Job, *model.AppError) {
+ mlog.Debug("Scheduling Job", mlog.String("scheduler", scheduler.Name()))
+
+ // Work through the list of migrations in order. Schedule the first one that isn't done (assuming it isn't in progress already).
+ for _, key := range MakeMigrationsList() {
+ state, job, err := GetMigrationState(key, scheduler.App.Srv.Store)
+ if err != nil {
+ mlog.Error("Failed to determine status of migration: ", mlog.String("scheduler", scheduler.Name()), mlog.String("migration_key", key), mlog.String("error", err.Error()))
+ return nil, nil
+ }
+
+ if state == MIGRATION_STATE_IN_PROGRESS {
+ // Check the migration job isn't wedged.
+ if job != nil && job.LastActivityAt < model.GetMillis()-MIGRATION_JOB_WEDGED_TIMEOUT_MILLISECONDS {
+ mlog.Warn("Job appears to be wedged. Rescheduling another instance.", mlog.String("scheduler", scheduler.Name()), mlog.String("wedged_job_id", job.Id), mlog.String("migration_key", key))
+ if err := scheduler.App.Jobs.SetJobError(job, nil); err != nil {
+ mlog.Error("Worker: Failed to set job error", mlog.String("scheduler", scheduler.Name()), mlog.String("job_id", job.Id), mlog.String("error", err.Error()))
+ }
+ return scheduler.createJob(key, job, scheduler.App.Srv.Store)
+ }
+
+ return nil, nil
+ }
+
+ if state == MIGRATION_STATE_COMPLETED {
+ // This migration is done. Continue to check the next.
+ continue
+ }
+
+ if state == MIGRATION_STATE_UNSCHEDULED {
+ mlog.Debug("Scheduling a new job for migration.", mlog.String("scheduler", scheduler.Name()), mlog.String("migration_key", key))
+ return scheduler.createJob(key, job, scheduler.App.Srv.Store)
+ }
+
+ mlog.Error("Unknown migration state. Not doing anything.", mlog.String("migration_state", state))
+ return nil, nil
+ }
+
+ // If we reached here, then there aren't any migrations left to run.
+ scheduler.allMigrationsCompleted = true
+ mlog.Debug("All migrations are complete.", mlog.String("scheduler", scheduler.Name()))
+
+ return nil, nil
+}
+
+func (scheduler *Scheduler) createJob(migrationKey string, lastJob *model.Job, store store.Store) (*model.Job, *model.AppError) {
+ var lastDone string
+ if lastJob != nil {
+ lastDone = lastJob.Data[JOB_DATA_KEY_MIGRATION_LAST_DONE]
+ }
+
+ data := map[string]string{
+ JOB_DATA_KEY_MIGRATION: migrationKey,
+ JOB_DATA_KEY_MIGRATION_LAST_DONE: lastDone,
+ }
+
+ if job, err := scheduler.App.Jobs.CreateJob(model.JOB_TYPE_MIGRATIONS, data); err != nil {
+ return nil, err
+ } else {
+ return job, nil
+ }
+}
diff --git a/migrations/worker.go b/migrations/worker.go
new file mode 100644
index 000000000..7a64dd609
--- /dev/null
+++ b/migrations/worker.go
@@ -0,0 +1,166 @@
+// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package migrations
+
+import (
+ "context"
+ "net/http"
+ "time"
+
+ "github.com/mattermost/mattermost-server/app"
+ "github.com/mattermost/mattermost-server/jobs"
+ "github.com/mattermost/mattermost-server/mlog"
+ "github.com/mattermost/mattermost-server/model"
+)
+
+const (
+ TIME_BETWEEN_BATCHES = 100
+)
+
+type Worker struct {
+ name string
+ stop chan bool
+ stopped chan bool
+ jobs chan model.Job
+ jobServer *jobs.JobServer
+ app *app.App
+}
+
+func (m *MigrationsJobInterfaceImpl) MakeWorker() model.Worker {
+ worker := Worker{
+ name: "Migrations",
+ stop: make(chan bool, 1),
+ stopped: make(chan bool, 1),
+ jobs: make(chan model.Job),
+ jobServer: m.App.Jobs,
+ app: m.App,
+ }
+
+ return &worker
+}
+
+func (worker *Worker) Run() {
+ mlog.Debug("Worker started", mlog.String("worker", worker.name))
+
+ defer func() {
+ mlog.Debug("Worker finished", mlog.String("worker", worker.name))
+ worker.stopped <- true
+ }()
+
+ for {
+ select {
+ case <-worker.stop:
+ mlog.Debug("Worker received stop signal", mlog.String("worker", worker.name))
+ return
+ case job := <-worker.jobs:
+ mlog.Debug("Worker received a new candidate job.", mlog.String("worker", worker.name))
+ worker.DoJob(&job)
+ }
+ }
+}
+
+func (worker *Worker) Stop() {
+ mlog.Debug("Worker stopping", mlog.String("worker", worker.name))
+ worker.stop <- true
+ <-worker.stopped
+}
+
+func (worker *Worker) JobChannel() chan<- model.Job {
+ return worker.jobs
+}
+
+func (worker *Worker) DoJob(job *model.Job) {
+ if claimed, err := worker.jobServer.ClaimJob(job); err != nil {
+ mlog.Info("Worker experienced an error while trying to claim job",
+ mlog.String("worker", worker.name),
+ mlog.String("job_id", job.Id),
+ mlog.String("error", err.Error()))
+ return
+ } else if !claimed {
+ return
+ }
+
+ cancelCtx, cancelCancelWatcher := context.WithCancel(context.Background())
+ cancelWatcherChan := make(chan interface{}, 1)
+ go worker.app.Jobs.CancellationWatcher(cancelCtx, job.Id, cancelWatcherChan)
+
+ defer cancelCancelWatcher()
+
+ for {
+ select {
+ case <-cancelWatcherChan:
+ mlog.Debug("Worker: Job has been canceled via CancellationWatcher", mlog.String("worker", worker.name), mlog.String("job_id", job.Id))
+ worker.setJobCanceled(job)
+ return
+
+ case <-worker.stop:
+ mlog.Debug("Worker: Job has been canceled via Worker Stop", mlog.String("worker", worker.name), mlog.String("job_id", job.Id))
+ worker.setJobCanceled(job)
+ return
+
+ case <-time.After(TIME_BETWEEN_BATCHES * time.Millisecond):
+ done, progress, err := worker.runMigration(job.Data[JOB_DATA_KEY_MIGRATION], job.Data[JOB_DATA_KEY_MIGRATION_LAST_DONE])
+ if err != nil {
+ mlog.Error("Worker: Failed to run migration", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.String("error", err.Error()))
+ worker.setJobError(job, err)
+ return
+ } else if done {
+ mlog.Info("Worker: Job is complete", mlog.String("worker", worker.name), mlog.String("job_id", job.Id))
+ worker.setJobSuccess(job)
+ return
+ } else {
+ job.Data[JOB_DATA_KEY_MIGRATION_LAST_DONE] = progress
+ if err := worker.app.Jobs.UpdateInProgressJobData(job); err != nil {
+ mlog.Error("Worker: Failed to update migration status data for job", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.String("error", err.Error()))
+ worker.setJobError(job, err)
+ return
+ }
+ }
+ }
+ }
+}
+
+func (worker *Worker) setJobSuccess(job *model.Job) {
+ if err := worker.app.Jobs.SetJobSuccess(job); err != nil {
+ mlog.Error("Worker: Failed to set success for job", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.String("error", err.Error()))
+ worker.setJobError(job, err)
+ }
+}
+
+func (worker *Worker) setJobError(job *model.Job, appError *model.AppError) {
+ if err := worker.app.Jobs.SetJobError(job, appError); err != nil {
+ mlog.Error("Worker: Failed to set job error", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.String("error", err.Error()))
+ }
+}
+
+func (worker *Worker) setJobCanceled(job *model.Job) {
+ if err := worker.app.Jobs.SetJobCanceled(job); err != nil {
+ mlog.Error("Worker: Failed to mark job as canceled", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.String("error", err.Error()))
+ }
+}
+
+// Return parameters:
+// - whether the migration is completed on this run (true) or still incomplete (false).
+// - the updated lastDone string for the migration.
+// - any error which may have occurred while running the migration.
+func (worker *Worker) runMigration(key string, lastDone string) (bool, string, *model.AppError) {
+ var done bool
+ var progress string
+ var err *model.AppError
+
+ switch key {
+ case model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2:
+ done, progress, err = worker.runAdvancedPermissionsPhase2Migration(lastDone)
+ default:
+ return false, "", model.NewAppError("MigrationsWorker.runMigration", "migrations.worker.run_migration.unknown_key", map[string]interface{}{"key": key}, "", http.StatusInternalServerError)
+ }
+
+ if done {
+ if result := <-worker.app.Srv.Store.System().Save(&model.System{Name: key, Value: "true"}); result.Err != nil {
+ return false, "", result.Err
+ }
+ }
+
+ return done, progress, err
+}
diff --git a/model/channel.go b/model/channel.go
index 950e910dd..5617240e6 100644
--- a/model/channel.go
+++ b/model/channel.go
@@ -32,20 +32,21 @@ const (
)
type Channel struct {
- Id string `json:"id"`
- CreateAt int64 `json:"create_at"`
- UpdateAt int64 `json:"update_at"`
- DeleteAt int64 `json:"delete_at"`
- TeamId string `json:"team_id"`
- Type string `json:"type"`
- DisplayName string `json:"display_name"`
- Name string `json:"name"`
- Header string `json:"header"`
- Purpose string `json:"purpose"`
- LastPostAt int64 `json:"last_post_at"`
- TotalMsgCount int64 `json:"total_msg_count"`
- ExtraUpdateAt int64 `json:"extra_update_at"`
- CreatorId string `json:"creator_id"`
+ Id string `json:"id"`
+ CreateAt int64 `json:"create_at"`
+ UpdateAt int64 `json:"update_at"`
+ DeleteAt int64 `json:"delete_at"`
+ TeamId string `json:"team_id"`
+ Type string `json:"type"`
+ DisplayName string `json:"display_name"`
+ Name string `json:"name"`
+ Header string `json:"header"`
+ Purpose string `json:"purpose"`
+ LastPostAt int64 `json:"last_post_at"`
+ TotalMsgCount int64 `json:"total_msg_count"`
+ ExtraUpdateAt int64 `json:"extra_update_at"`
+ CreatorId string `json:"creator_id"`
+ SchemeId *string `json:"scheme_id"`
}
type ChannelPatch struct {
diff --git a/model/channel_member.go b/model/channel_member.go
index e9895aea0..709ad3ccd 100644
--- a/model/channel_member.go
+++ b/model/channel_member.go
@@ -28,14 +28,17 @@ type ChannelUnread struct {
}
type ChannelMember struct {
- ChannelId string `json:"channel_id"`
- UserId string `json:"user_id"`
- Roles string `json:"roles"`
- LastViewedAt int64 `json:"last_viewed_at"`
- MsgCount int64 `json:"msg_count"`
- MentionCount int64 `json:"mention_count"`
- NotifyProps StringMap `json:"notify_props"`
- LastUpdateAt int64 `json:"last_update_at"`
+ ChannelId string `json:"channel_id"`
+ UserId string `json:"user_id"`
+ Roles string `json:"roles"`
+ LastViewedAt int64 `json:"last_viewed_at"`
+ MsgCount int64 `json:"msg_count"`
+ MentionCount int64 `json:"mention_count"`
+ NotifyProps StringMap `json:"notify_props"`
+ LastUpdateAt int64 `json:"last_update_at"`
+ SchemeUser bool `json:"scheme_user"`
+ SchemeAdmin bool `json:"scheme_admin"`
+ ExplicitRoles string `json:"explicit_roles"`
}
type ChannelMembers []ChannelMember
diff --git a/model/client4.go b/model/client4.go
index 97dd30790..c2b6ba948 100644
--- a/model/client4.go
+++ b/model/client4.go
@@ -368,6 +368,14 @@ func (c *Client4) GetRolesRoute() string {
return fmt.Sprintf("/roles")
}
+func (c *Client4) GetSchemesRoute() string {
+ return fmt.Sprintf("/schemes")
+}
+
+func (c *Client4) GetSchemeRoute(id string) string {
+ return c.GetSchemesRoute() + fmt.Sprintf("/%v", id)
+}
+
func (c *Client4) GetAnalyticsRoute() string {
return fmt.Sprintf("/analytics")
}
@@ -376,6 +384,14 @@ func (c *Client4) GetTimezonesRoute() string {
return fmt.Sprintf(c.GetSystemRoute() + "/timezones")
}
+func (c *Client4) GetChannelSchemeRoute(channelId string) string {
+ return fmt.Sprintf(c.GetChannelsRoute()+"/%v/scheme", channelId)
+}
+
+func (c *Client4) GetTeamSchemeRoute(teamId string) string {
+ return fmt.Sprintf(c.GetTeamsRoute()+"/%v/scheme", teamId)
+}
+
func (c *Client4) DoApiGet(url string, etag string) (*http.Response, *AppError) {
return c.DoApiRequest(http.MethodGet, c.ApiUrl+url, "", etag)
}
@@ -3484,6 +3500,78 @@ func (c *Client4) PatchRole(roleId string, patch *RolePatch) (*Role, *Response)
}
}
+// Schemes Section
+
+// CreateScheme creates a new Scheme.
+func (c *Client4) CreateScheme(scheme *Scheme) (*Scheme, *Response) {
+ if r, err := c.DoApiPost(c.GetSchemesRoute(), scheme.ToJson()); err != nil {
+ return nil, BuildErrorResponse(r, err)
+ } else {
+ defer closeBody(r)
+ return SchemeFromJson(r.Body), BuildResponse(r)
+ }
+}
+
+// GetScheme gets a single scheme by ID.
+func (c *Client4) GetScheme(id string) (*Scheme, *Response) {
+ if r, err := c.DoApiGet(c.GetSchemeRoute(id), ""); err != nil {
+ return nil, BuildErrorResponse(r, err)
+ } else {
+ defer closeBody(r)
+ return SchemeFromJson(r.Body), BuildResponse(r)
+ }
+}
+
+// Get all schemes, sorted with the most recently created first, optionally filtered by scope.
+func (c *Client4) GetSchemes(scope string, page int, perPage int) ([]*Scheme, *Response) {
+ if r, err := c.DoApiGet(c.GetSchemesRoute()+fmt.Sprintf("?scope=%v&page=%v&per_page=%v", scope, page, perPage), ""); err != nil {
+ return nil, BuildErrorResponse(r, err)
+ } else {
+ defer closeBody(r)
+ return SchemesFromJson(r.Body), BuildResponse(r)
+ }
+}
+
+// DeleteScheme deletes a single scheme by ID.
+func (c *Client4) DeleteScheme(id string) (bool, *Response) {
+ if r, err := c.DoApiDelete(c.GetSchemeRoute(id)); err != nil {
+ return false, BuildErrorResponse(r, err)
+ } else {
+ defer closeBody(r)
+ return CheckStatusOK(r), BuildResponse(r)
+ }
+}
+
+// PatchScheme partially updates a scheme in the system. Any missing fields are not updated.
+func (c *Client4) PatchScheme(id string, patch *SchemePatch) (*Scheme, *Response) {
+ if r, err := c.DoApiPut(c.GetSchemeRoute(id)+"/patch", patch.ToJson()); err != nil {
+ return nil, BuildErrorResponse(r, err)
+ } else {
+ defer closeBody(r)
+ return SchemeFromJson(r.Body), BuildResponse(r)
+ }
+}
+
+// Get the teams using this scheme, sorted alphabetically by display name.
+func (c *Client4) GetTeamsForScheme(schemeId string, page int, perPage int) ([]*Team, *Response) {
+ if r, err := c.DoApiGet(c.GetSchemeRoute(schemeId)+fmt.Sprintf("/teams?page=%v&per_page=%v", page, perPage), ""); err != nil {
+ return nil, BuildErrorResponse(r, err)
+ } else {
+ defer closeBody(r)
+ return TeamListFromJson(r.Body), BuildResponse(r)
+ }
+}
+
+// Get the channels using this scheme, sorted alphabetically by display name.
+func (c *Client4) GetChannelsForScheme(schemeId string, page int, perPage int) (ChannelList, *Response) {
+ if r, err := c.DoApiGet(c.GetSchemeRoute(schemeId)+fmt.Sprintf("/channels?page=%v&per_page=%v", page, perPage), ""); err != nil {
+ return nil, BuildErrorResponse(r, err)
+ } else {
+ defer closeBody(r)
+ return *ChannelListFromJson(r.Body), BuildResponse(r)
+ }
+}
+
// Plugin Section
// UploadPlugin takes an io.Reader stream pointing to the contents of a .tar.gz plugin.
@@ -3589,3 +3677,25 @@ func (c *Client4) DeactivatePlugin(id string) (bool, *Response) {
return CheckStatusOK(r), BuildResponse(r)
}
}
+
+// UpdateChannelScheme will update a channel's scheme.
+func (c *Client4) UpdateChannelScheme(channelId, schemeId string) (bool, *Response) {
+ sip := &SchemeIDPatch{SchemeID: &schemeId}
+ if r, err := c.DoApiPut(c.GetChannelSchemeRoute(channelId), sip.ToJson()); err != nil {
+ return false, BuildErrorResponse(r, err)
+ } else {
+ defer closeBody(r)
+ return CheckStatusOK(r), BuildResponse(r)
+ }
+}
+
+// UpdateTeamScheme will update a team's scheme.
+func (c *Client4) UpdateTeamScheme(teamId, schemeId string) (bool, *Response) {
+ sip := &SchemeIDPatch{SchemeID: &schemeId}
+ if r, err := c.DoApiPut(c.GetTeamSchemeRoute(teamId), sip.ToJson()); err != nil {
+ return false, BuildErrorResponse(r, err)
+ } else {
+ defer closeBody(r)
+ return CheckStatusOK(r), BuildResponse(r)
+ }
+}
diff --git a/model/cluster_message.go b/model/cluster_message.go
index cf9e3f9f2..d02da3ee1 100644
--- a/model/cluster_message.go
+++ b/model/cluster_message.go
@@ -22,6 +22,7 @@ const (
CLUSTER_EVENT_INVALIDATE_CACHE_FOR_USER = "inv_user"
CLUSTER_EVENT_CLEAR_SESSION_CACHE_FOR_USER = "clear_session_user"
CLUSTER_EVENT_INVALIDATE_CACHE_FOR_ROLES = "inv_roles"
+ CLUSTER_EVENT_INVALIDATE_CACHE_FOR_SCHEMES = "inv_schemes"
CLUSTER_SEND_BEST_EFFORT = "best_effort"
CLUSTER_SEND_RELIABLE = "reliable"
diff --git a/model/job.go b/model/job.go
index e10ed1f5d..c16614958 100644
--- a/model/job.go
+++ b/model/job.go
@@ -16,6 +16,7 @@ const (
JOB_TYPE_ELASTICSEARCH_POST_INDEXING = "elasticsearch_post_indexing"
JOB_TYPE_ELASTICSEARCH_POST_AGGREGATION = "elasticsearch_post_aggregation"
JOB_TYPE_LDAP_SYNC = "ldap_sync"
+ JOB_TYPE_MIGRATIONS = "migrations"
JOB_STATUS_PENDING = "pending"
JOB_STATUS_IN_PROGRESS = "in_progress"
@@ -52,6 +53,7 @@ func (j *Job) IsValid() *AppError {
case JOB_TYPE_ELASTICSEARCH_POST_AGGREGATION:
case JOB_TYPE_LDAP_SYNC:
case JOB_TYPE_MESSAGE_EXPORT:
+ case JOB_TYPE_MIGRATIONS:
default:
return NewAppError("Job.IsValid", "model.job.is_valid.type.app_error", nil, "id="+j.Id, http.StatusBadRequest)
}
diff --git a/model/license.go b/model/license.go
index b69c13c54..b6a6f2ac8 100644
--- a/model/license.go
+++ b/model/license.go
@@ -55,6 +55,7 @@ type Features struct {
EmailNotificationContents *bool `json:"email_notification_contents"`
DataRetention *bool `json:"data_retention"`
MessageExport *bool `json:"message_export"`
+ CustomPermissionsSchemes *bool `json:"custom_permissions_schemes"`
// after we enabled more features for webrtc we'll need to control them with this
FutureFeatures *bool `json:"future_features"`
@@ -76,6 +77,7 @@ func (f *Features) ToMap() map[string]interface{} {
"email_notification_contents": *f.EmailNotificationContents,
"data_retention": *f.DataRetention,
"message_export": *f.MessageExport,
+ "custom_permissions_schemes": *f.CustomPermissionsSchemes,
"future": *f.FutureFeatures,
}
}
@@ -152,6 +154,10 @@ func (f *Features) SetDefaults() {
if f.MessageExport == nil {
f.MessageExport = NewBool(*f.FutureFeatures)
}
+
+ if f.CustomPermissionsSchemes == nil {
+ f.CustomPermissionsSchemes = NewBool(*f.FutureFeatures)
+ }
}
func (l *License) IsExpired() bool {
diff --git a/model/license_test.go b/model/license_test.go
index 4b40c4101..a9379d78e 100644
--- a/model/license_test.go
+++ b/model/license_test.go
@@ -27,6 +27,8 @@ func TestLicenseFeaturesToMap(t *testing.T) {
CheckTrue(t, m["elastic_search"].(bool))
CheckTrue(t, m["email_notification_contents"].(bool))
CheckTrue(t, m["data_retention"].(bool))
+ CheckTrue(t, m["message_export"].(bool))
+ CheckTrue(t, m["custom_permissions_schemes"].(bool))
CheckTrue(t, m["future"].(bool))
}
@@ -48,6 +50,8 @@ func TestLicenseFeaturesSetDefaults(t *testing.T) {
CheckTrue(t, *f.Elasticsearch)
CheckTrue(t, *f.EmailNotificationContents)
CheckTrue(t, *f.DataRetention)
+ CheckTrue(t, *f.MessageExport)
+ CheckTrue(t, *f.CustomPermissionsSchemes)
CheckTrue(t, *f.FutureFeatures)
f = Features{}
@@ -67,6 +71,8 @@ func TestLicenseFeaturesSetDefaults(t *testing.T) {
*f.SAML = true
*f.Elasticsearch = true
*f.DataRetention = true
+ *f.MessageExport = true
+ *f.CustomPermissionsSchemes = true
*f.EmailNotificationContents = true
f.SetDefaults()
@@ -85,6 +91,8 @@ func TestLicenseFeaturesSetDefaults(t *testing.T) {
CheckTrue(t, *f.Elasticsearch)
CheckTrue(t, *f.EmailNotificationContents)
CheckTrue(t, *f.DataRetention)
+ CheckTrue(t, *f.MessageExport)
+ CheckTrue(t, *f.CustomPermissionsSchemes)
CheckFalse(t, *f.FutureFeatures)
}
@@ -166,6 +174,8 @@ func TestLicenseToFromJson(t *testing.T) {
CheckBool(t, *f1.SAML, *f.SAML)
CheckBool(t, *f1.Elasticsearch, *f.Elasticsearch)
CheckBool(t, *f1.DataRetention, *f.DataRetention)
+ CheckBool(t, *f1.MessageExport, *f.MessageExport)
+ CheckBool(t, *f1.CustomPermissionsSchemes, *f.CustomPermissionsSchemes)
CheckBool(t, *f1.FutureFeatures, *f.FutureFeatures)
invalid := `{"asdf`
diff --git a/model/migration.go b/model/migration.go
new file mode 100644
index 000000000..ead7acce2
--- /dev/null
+++ b/model/migration.go
@@ -0,0 +1,8 @@
+// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+const (
+ MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2 = "migration_advanced_permissions_phase_2"
+)
diff --git a/model/permission.go b/model/permission.go
index 792c7d42e..737321cc7 100644
--- a/model/permission.go
+++ b/model/permission.go
@@ -50,6 +50,8 @@ var PERMISSION_MANAGE_WEBHOOKS *Permission
var PERMISSION_MANAGE_OTHERS_WEBHOOKS *Permission
var PERMISSION_MANAGE_OAUTH *Permission
var PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH *Permission
+var PERMISSION_MANAGE_EMOJIS *Permission
+var PERMISSION_MANAGE_OTHERS_EMOJIS *Permission
var PERMISSION_CREATE_POST *Permission
var PERMISSION_CREATE_POST_PUBLIC *Permission
var PERMISSION_CREATE_POST_EPHEMERAL *Permission
@@ -286,6 +288,18 @@ func initializePermissions() {
"authentication.permissions.manage_system_wide_oauth.description",
PERMISSION_SCOPE_SYSTEM,
}
+ PERMISSION_MANAGE_EMOJIS = &Permission{
+ "manage_emojis",
+ "authentication.permissions.manage_emojis.name",
+ "authentication.permissions.manage_emojis.description",
+ PERMISSION_SCOPE_TEAM,
+ }
+ PERMISSION_MANAGE_OTHERS_EMOJIS = &Permission{
+ "manage_others_emojis",
+ "authentication.permissions.manage_others_emojis.name",
+ "authentication.permissions.manage_others_emojis.description",
+ PERMISSION_SCOPE_TEAM,
+ }
PERMISSION_CREATE_POST = &Permission{
"create_post",
"authentication.permissions.create_post.name",
@@ -424,6 +438,8 @@ func initializePermissions() {
PERMISSION_MANAGE_OTHERS_WEBHOOKS,
PERMISSION_MANAGE_OAUTH,
PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH,
+ PERMISSION_MANAGE_EMOJIS,
+ PERMISSION_MANAGE_OTHERS_EMOJIS,
PERMISSION_CREATE_POST,
PERMISSION_CREATE_POST_PUBLIC,
PERMISSION_CREATE_POST_EPHEMERAL,
diff --git a/model/role.go b/model/role.go
index f10b52537..80ae1ae34 100644
--- a/model/role.go
+++ b/model/role.go
@@ -39,6 +39,7 @@ type Role struct {
DeleteAt int64 `json:"delete_at"`
Permissions []string `json:"permissions"`
SchemeManaged bool `json:"scheme_managed"`
+ BuiltIn bool `json:"built_in"`
}
type RolePatch struct {
@@ -187,6 +188,7 @@ func MakeDefaultRoles() map[string]*Role {
PERMISSION_USE_SLASH_COMMANDS.Id,
},
SchemeManaged: true,
+ BuiltIn: true,
}
roles[CHANNEL_ADMIN_ROLE_ID] = &Role{
@@ -197,6 +199,7 @@ func MakeDefaultRoles() map[string]*Role {
PERMISSION_MANAGE_CHANNEL_ROLES.Id,
},
SchemeManaged: true,
+ BuiltIn: true,
}
roles[TEAM_USER_ROLE_ID] = &Role{
@@ -210,6 +213,7 @@ func MakeDefaultRoles() map[string]*Role {
PERMISSION_VIEW_TEAM.Id,
},
SchemeManaged: true,
+ BuiltIn: true,
}
roles[TEAM_POST_ALL_ROLE_ID] = &Role{
@@ -219,7 +223,8 @@ func MakeDefaultRoles() map[string]*Role {
Permissions: []string{
PERMISSION_CREATE_POST.Id,
},
- SchemeManaged: true,
+ SchemeManaged: false,
+ BuiltIn: true,
}
roles[TEAM_POST_ALL_PUBLIC_ROLE_ID] = &Role{
@@ -229,7 +234,8 @@ func MakeDefaultRoles() map[string]*Role {
Permissions: []string{
PERMISSION_CREATE_POST_PUBLIC.Id,
},
- SchemeManaged: true,
+ SchemeManaged: false,
+ BuiltIn: true,
}
roles[TEAM_ADMIN_ROLE_ID] = &Role{
@@ -249,6 +255,7 @@ func MakeDefaultRoles() map[string]*Role {
PERMISSION_MANAGE_WEBHOOKS.Id,
},
SchemeManaged: true,
+ BuiltIn: true,
}
roles[SYSTEM_USER_ROLE_ID] = &Role{
@@ -261,6 +268,7 @@ func MakeDefaultRoles() map[string]*Role {
PERMISSION_PERMANENT_DELETE_USER.Id,
},
SchemeManaged: true,
+ BuiltIn: true,
}
roles[SYSTEM_POST_ALL_ROLE_ID] = &Role{
@@ -270,7 +278,8 @@ func MakeDefaultRoles() map[string]*Role {
Permissions: []string{
PERMISSION_CREATE_POST.Id,
},
- SchemeManaged: true,
+ SchemeManaged: false,
+ BuiltIn: true,
}
roles[SYSTEM_POST_ALL_PUBLIC_ROLE_ID] = &Role{
@@ -280,7 +289,8 @@ func MakeDefaultRoles() map[string]*Role {
Permissions: []string{
PERMISSION_CREATE_POST_PUBLIC.Id,
},
- SchemeManaged: true,
+ SchemeManaged: false,
+ BuiltIn: true,
}
roles[SYSTEM_USER_ACCESS_TOKEN_ROLE_ID] = &Role{
@@ -292,7 +302,8 @@ func MakeDefaultRoles() map[string]*Role {
PERMISSION_READ_USER_ACCESS_TOKEN.Id,
PERMISSION_REVOKE_USER_ACCESS_TOKEN.Id,
},
- SchemeManaged: true,
+ SchemeManaged: false,
+ BuiltIn: true,
}
roles[SYSTEM_ADMIN_ROLE_ID] = &Role{
@@ -345,6 +356,7 @@ func MakeDefaultRoles() map[string]*Role {
roles[CHANNEL_ADMIN_ROLE_ID].Permissions...,
),
SchemeManaged: true,
+ BuiltIn: true,
}
return roles
diff --git a/model/scheme.go b/model/scheme.go
new file mode 100644
index 000000000..959b80c24
--- /dev/null
+++ b/model/scheme.go
@@ -0,0 +1,192 @@
+// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "regexp"
+)
+
+const (
+ SCHEME_DISPLAY_NAME_MAX_LENGTH = 128
+ SCHEME_NAME_MAX_LENGTH = 64
+ SCHEME_DESCRIPTION_MAX_LENGTH = 1024
+ SCHEME_SCOPE_TEAM = "team"
+ SCHEME_SCOPE_CHANNEL = "channel"
+)
+
+type Scheme struct {
+ Id string `json:"id"`
+ Name string `json:"name"`
+ DisplayName string `json:"display_name"`
+ Description string `json:"description"`
+ CreateAt int64 `json:"create_at"`
+ UpdateAt int64 `json:"update_at"`
+ DeleteAt int64 `json:"delete_at"`
+ Scope string `json:"scope"`
+ DefaultTeamAdminRole string `json:"default_team_admin_role"`
+ DefaultTeamUserRole string `json:"default_team_user_role"`
+ DefaultChannelAdminRole string `json:"default_channel_admin_role"`
+ DefaultChannelUserRole string `json:"default_channel_user_role"`
+}
+
+type SchemePatch struct {
+ Name *string `json:"name"`
+ DisplayName *string `json:"display_name"`
+ Description *string `json:"description"`
+}
+
+type SchemeIDPatch struct {
+ SchemeID *string `json:"scheme_id"`
+}
+
+// SchemeConveyor is used for importing and exporting a Scheme and its associated Roles.
+type SchemeConveyor struct {
+ Name string `json:"name"`
+ DisplayName string `json:"display_name"`
+ Description string `json:"description"`
+ Scope string `json:"scope"`
+ TeamAdmin string `json:"default_team_admin_role"`
+ TeamUser string `json:"default_team_user_role"`
+ ChannelAdmin string `json:"default_channel_admin_role"`
+ ChannelUser string `json:"default_channel_user_role"`
+ Roles []*Role `json:"roles"`
+}
+
+func (sc *SchemeConveyor) Scheme() *Scheme {
+ return &Scheme{
+ DisplayName: sc.DisplayName,
+ Name: sc.Name,
+ Description: sc.Description,
+ Scope: sc.Scope,
+ DefaultTeamAdminRole: sc.TeamAdmin,
+ DefaultTeamUserRole: sc.TeamUser,
+ DefaultChannelAdminRole: sc.ChannelAdmin,
+ DefaultChannelUserRole: sc.ChannelUser,
+ }
+}
+
+func (scheme *Scheme) ToJson() string {
+ b, _ := json.Marshal(scheme)
+ return string(b)
+}
+
+func SchemeFromJson(data io.Reader) *Scheme {
+ var scheme *Scheme
+ json.NewDecoder(data).Decode(&scheme)
+ return scheme
+}
+
+func SchemesToJson(schemes []*Scheme) string {
+ b, _ := json.Marshal(schemes)
+ return string(b)
+}
+
+func SchemesFromJson(data io.Reader) []*Scheme {
+ var schemes []*Scheme
+ if err := json.NewDecoder(data).Decode(&schemes); err == nil {
+ return schemes
+ } else {
+ return nil
+ }
+}
+
+func (scheme *Scheme) IsValid() bool {
+ if len(scheme.Id) != 26 {
+ return false
+ }
+
+ return scheme.IsValidForCreate()
+}
+
+func (scheme *Scheme) IsValidForCreate() bool {
+ if len(scheme.DisplayName) == 0 || len(scheme.DisplayName) > SCHEME_DISPLAY_NAME_MAX_LENGTH {
+ return false
+ }
+
+ if !IsValidSchemeName(scheme.Name) {
+ return false
+ }
+
+ if len(scheme.Description) > SCHEME_DESCRIPTION_MAX_LENGTH {
+ return false
+ }
+
+ switch scheme.Scope {
+ case SCHEME_SCOPE_TEAM, SCHEME_SCOPE_CHANNEL:
+ default:
+ return false
+ }
+
+ if !IsValidRoleName(scheme.DefaultChannelAdminRole) {
+ return false
+ }
+
+ if !IsValidRoleName(scheme.DefaultChannelUserRole) {
+ return false
+ }
+
+ if scheme.Scope == SCHEME_SCOPE_TEAM {
+ if !IsValidRoleName(scheme.DefaultTeamAdminRole) {
+ return false
+ }
+
+ if !IsValidRoleName(scheme.DefaultTeamUserRole) {
+ return false
+ }
+ }
+
+ if scheme.Scope == SCHEME_SCOPE_CHANNEL {
+ if len(scheme.DefaultTeamAdminRole) != 0 {
+ return false
+ }
+
+ if len(scheme.DefaultTeamUserRole) != 0 {
+ return false
+ }
+ }
+
+ return true
+}
+
+func (scheme *Scheme) Patch(patch *SchemePatch) {
+ if patch.DisplayName != nil {
+ scheme.DisplayName = *patch.DisplayName
+ }
+ if patch.Name != nil {
+ scheme.Name = *patch.Name
+ }
+ if patch.Description != nil {
+ scheme.Description = *patch.Description
+ }
+}
+
+func (patch *SchemePatch) ToJson() string {
+ b, _ := json.Marshal(patch)
+ return string(b)
+}
+
+func SchemePatchFromJson(data io.Reader) *SchemePatch {
+ var patch *SchemePatch
+ json.NewDecoder(data).Decode(&patch)
+ return patch
+}
+
+func SchemeIDFromJson(data io.Reader) *string {
+ var p *SchemeIDPatch
+ json.NewDecoder(data).Decode(&p)
+ return p.SchemeID
+}
+
+func (p *SchemeIDPatch) ToJson() string {
+ b, _ := json.Marshal(p)
+ return string(b)
+}
+
+func IsValidSchemeName(name string) bool {
+ re := regexp.MustCompile(fmt.Sprintf("^[a-z0-9_]{0,%d}$", SCHEME_NAME_MAX_LENGTH))
+ return re.MatchString(name)
+}
diff --git a/model/team.go b/model/team.go
index 7968c9d48..edf9d3a41 100644
--- a/model/team.go
+++ b/model/team.go
@@ -26,20 +26,21 @@ const (
)
type Team struct {
- Id string `json:"id"`
- CreateAt int64 `json:"create_at"`
- UpdateAt int64 `json:"update_at"`
- DeleteAt int64 `json:"delete_at"`
- DisplayName string `json:"display_name"`
- Name string `json:"name"`
- Description string `json:"description"`
- Email string `json:"email"`
- Type string `json:"type"`
- CompanyName string `json:"company_name"`
- AllowedDomains string `json:"allowed_domains"`
- InviteId string `json:"invite_id"`
- AllowOpenInvite bool `json:"allow_open_invite"`
- LastTeamIconUpdate int64 `json:"last_team_icon_update,omitempty"`
+ Id string `json:"id"`
+ CreateAt int64 `json:"create_at"`
+ UpdateAt int64 `json:"update_at"`
+ DeleteAt int64 `json:"delete_at"`
+ DisplayName string `json:"display_name"`
+ Name string `json:"name"`
+ Description string `json:"description"`
+ Email string `json:"email"`
+ Type string `json:"type"`
+ CompanyName string `json:"company_name"`
+ AllowedDomains string `json:"allowed_domains"`
+ InviteId string `json:"invite_id"`
+ AllowOpenInvite bool `json:"allow_open_invite"`
+ LastTeamIconUpdate int64 `json:"last_team_icon_update,omitempty"`
+ SchemeId *string `json:"scheme_id"`
}
type TeamPatch struct {
diff --git a/model/team_member.go b/model/team_member.go
index 2fcd1e151..0bda96121 100644
--- a/model/team_member.go
+++ b/model/team_member.go
@@ -11,10 +11,13 @@ import (
)
type TeamMember struct {
- TeamId string `json:"team_id"`
- UserId string `json:"user_id"`
- Roles string `json:"roles"`
- DeleteAt int64 `json:"delete_at"`
+ TeamId string `json:"team_id"`
+ UserId string `json:"user_id"`
+ Roles string `json:"roles"`
+ DeleteAt int64 `json:"delete_at"`
+ SchemeUser bool `json:"scheme_user"`
+ SchemeAdmin bool `json:"scheme_admin"`
+ ExplicitRoles string `json:"explicit_roles"`
}
type TeamUnread struct {
diff --git a/store/layered_store.go b/store/layered_store.go
index a0a31fb39..69513febf 100644
--- a/store/layered_store.go
+++ b/store/layered_store.go
@@ -24,6 +24,7 @@ type LayeredStore struct {
TmpContext context.Context
ReactionStore ReactionStore
RoleStore RoleStore
+ SchemeStore SchemeStore
DatabaseLayer LayeredStoreDatabaseLayer
LocalCacheLayer *LocalCacheSupplier
RedisLayer *RedisSupplier
@@ -39,6 +40,7 @@ func NewLayeredStore(db LayeredStoreDatabaseLayer, metrics einterfaces.MetricsIn
store.ReactionStore = &LayeredReactionStore{store}
store.RoleStore = &LayeredRoleStore{store}
+ store.SchemeStore = &LayeredSchemeStore{store}
// Setup the chain
if ENABLE_EXPERIMENTAL_REDIS {
@@ -167,6 +169,10 @@ func (s *LayeredStore) Role() RoleStore {
return s.RoleStore
}
+func (s *LayeredStore) Scheme() SchemeStore {
+ return s.SchemeStore
+}
+
func (s *LayeredStore) MarkSystemRanUnitTests() {
s.DatabaseLayer.MarkSystemRanUnitTests()
}
@@ -253,8 +259,48 @@ func (s *LayeredRoleStore) GetByNames(names []string) StoreChannel {
})
}
+func (s *LayeredRoleStore) Delete(roldId string) StoreChannel {
+ return s.RunQuery(func(supplier LayeredStoreSupplier) *LayeredStoreSupplierResult {
+ return supplier.RoleDelete(s.TmpContext, roldId)
+ })
+}
+
func (s *LayeredRoleStore) PermanentDeleteAll() StoreChannel {
return s.RunQuery(func(supplier LayeredStoreSupplier) *LayeredStoreSupplierResult {
return supplier.RolePermanentDeleteAll(s.TmpContext)
})
}
+
+type LayeredSchemeStore struct {
+ *LayeredStore
+}
+
+func (s *LayeredSchemeStore) Save(scheme *model.Scheme) StoreChannel {
+ return s.RunQuery(func(supplier LayeredStoreSupplier) *LayeredStoreSupplierResult {
+ return supplier.SchemeSave(s.TmpContext, scheme)
+ })
+}
+
+func (s *LayeredSchemeStore) Get(schemeId string) StoreChannel {
+ return s.RunQuery(func(supplier LayeredStoreSupplier) *LayeredStoreSupplierResult {
+ return supplier.SchemeGet(s.TmpContext, schemeId)
+ })
+}
+
+func (s *LayeredSchemeStore) Delete(schemeId string) StoreChannel {
+ return s.RunQuery(func(supplier LayeredStoreSupplier) *LayeredStoreSupplierResult {
+ return supplier.SchemeDelete(s.TmpContext, schemeId)
+ })
+}
+
+func (s *LayeredSchemeStore) GetAllPage(scope string, offset int, limit int) StoreChannel {
+ return s.RunQuery(func(supplier LayeredStoreSupplier) *LayeredStoreSupplierResult {
+ return supplier.SchemeGetAllPage(s.TmpContext, scope, offset, limit)
+ })
+}
+
+func (s *LayeredSchemeStore) PermanentDeleteAll() StoreChannel {
+ return s.RunQuery(func(supplier LayeredStoreSupplier) *LayeredStoreSupplierResult {
+ return supplier.SchemePermanentDeleteAll(s.TmpContext)
+ })
+}
diff --git a/store/layered_store_supplier.go b/store/layered_store_supplier.go
index 9a7604b20..6bf4a0310 100644
--- a/store/layered_store_supplier.go
+++ b/store/layered_store_supplier.go
@@ -35,5 +35,13 @@ 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
+ RoleDelete(ctx context.Context, roldId string, hints ...LayeredStoreHint) *LayeredStoreSupplierResult
RolePermanentDeleteAll(ctx context.Context, hints ...LayeredStoreHint) *LayeredStoreSupplierResult
+
+ // Schemes
+ SchemeSave(ctx context.Context, scheme *model.Scheme, hints ...LayeredStoreHint) *LayeredStoreSupplierResult
+ SchemeGet(ctx context.Context, schemeId string, hints ...LayeredStoreHint) *LayeredStoreSupplierResult
+ SchemeDelete(ctx context.Context, schemeId string, hints ...LayeredStoreHint) *LayeredStoreSupplierResult
+ SchemeGetAllPage(ctx context.Context, scope string, offset int, limit int, hints ...LayeredStoreHint) *LayeredStoreSupplierResult
+ SchemePermanentDeleteAll(ctx context.Context, hints ...LayeredStoreHint) *LayeredStoreSupplierResult
}
diff --git a/store/local_cache_supplier.go b/store/local_cache_supplier.go
index 2343f10a7..417ffc892 100644
--- a/store/local_cache_supplier.go
+++ b/store/local_cache_supplier.go
@@ -18,6 +18,9 @@ const (
ROLE_CACHE_SIZE = 20000
ROLE_CACHE_SEC = 30 * 60
+ SCHEME_CACHE_SIZE = 20000
+ SCHEME_CACHE_SEC = 30 * 60
+
CLEAR_CACHE_MESSAGE_DATA = ""
)
@@ -25,6 +28,7 @@ type LocalCacheSupplier struct {
next LayeredStoreSupplier
reactionCache *utils.Cache
roleCache *utils.Cache
+ schemeCache *utils.Cache
metrics einterfaces.MetricsInterface
cluster einterfaces.ClusterInterface
}
@@ -33,6 +37,7 @@ func NewLocalCacheSupplier(metrics einterfaces.MetricsInterface, cluster einterf
supplier := &LocalCacheSupplier{
reactionCache: utils.NewLruWithParams(REACTION_CACHE_SIZE, "Reaction", REACTION_CACHE_SEC, model.CLUSTER_EVENT_INVALIDATE_CACHE_FOR_REACTIONS),
roleCache: utils.NewLruWithParams(ROLE_CACHE_SIZE, "Role", ROLE_CACHE_SEC, model.CLUSTER_EVENT_INVALIDATE_CACHE_FOR_ROLES),
+ schemeCache: utils.NewLruWithParams(SCHEME_CACHE_SIZE, "Scheme", SCHEME_CACHE_SEC, model.CLUSTER_EVENT_INVALIDATE_CACHE_FOR_SCHEMES),
metrics: metrics,
cluster: cluster,
}
diff --git a/store/local_cache_supplier_roles.go b/store/local_cache_supplier_roles.go
index 7c82f60eb..41f88a216 100644
--- a/store/local_cache_supplier_roles.go
+++ b/store/local_cache_supplier_roles.go
@@ -69,6 +69,17 @@ func (s *LocalCacheSupplier) RoleGetByNames(ctx context.Context, roleNames []str
return result
}
+func (s *LocalCacheSupplier) RoleDelete(ctx context.Context, roleId string, hints ...LayeredStoreHint) *LayeredStoreSupplierResult {
+ result := s.Next().RoleDelete(ctx, roleId, hints...)
+
+ if result.Err == nil {
+ role := result.Data.(*model.Role)
+ s.doInvalidateCacheCluster(s.roleCache, role.Name)
+ }
+
+ return result
+}
+
func (s *LocalCacheSupplier) RolePermanentDeleteAll(ctx context.Context, hints ...LayeredStoreHint) *LayeredStoreSupplierResult {
defer s.roleCache.Purge()
defer s.doClearCacheCluster(s.roleCache)
diff --git a/store/local_cache_supplier_schemes.go b/store/local_cache_supplier_schemes.go
new file mode 100644
index 000000000..b6cde0fc4
--- /dev/null
+++ b/store/local_cache_supplier_schemes.go
@@ -0,0 +1,54 @@
+// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package store
+
+import (
+ "context"
+
+ "github.com/mattermost/mattermost-server/model"
+)
+
+func (s *LocalCacheSupplier) handleClusterInvalidateScheme(msg *model.ClusterMessage) {
+ if msg.Data == CLEAR_CACHE_MESSAGE_DATA {
+ s.schemeCache.Purge()
+ } else {
+ s.schemeCache.Remove(msg.Data)
+ }
+}
+
+func (s *LocalCacheSupplier) SchemeSave(ctx context.Context, scheme *model.Scheme, hints ...LayeredStoreHint) *LayeredStoreSupplierResult {
+ if len(scheme.Id) != 0 {
+ defer s.doInvalidateCacheCluster(s.schemeCache, scheme.Id)
+ }
+ return s.Next().SchemeSave(ctx, scheme, hints...)
+}
+
+func (s *LocalCacheSupplier) SchemeGet(ctx context.Context, schemeId string, hints ...LayeredStoreHint) *LayeredStoreSupplierResult {
+ if result := s.doStandardReadCache(ctx, s.schemeCache, schemeId, hints...); result != nil {
+ return result
+ }
+
+ result := s.Next().SchemeGet(ctx, schemeId, hints...)
+
+ s.doStandardAddToCache(ctx, s.schemeCache, schemeId, result, hints...)
+
+ return result
+}
+
+func (s *LocalCacheSupplier) SchemeDelete(ctx context.Context, schemeId string, hints ...LayeredStoreHint) *LayeredStoreSupplierResult {
+ defer s.doInvalidateCacheCluster(s.schemeCache, schemeId)
+ defer s.doClearCacheCluster(s.roleCache)
+
+ return s.Next().SchemeDelete(ctx, schemeId, hints...)
+}
+
+func (s *LocalCacheSupplier) SchemeGetAllPage(ctx context.Context, scope string, offset int, limit int, hints ...LayeredStoreHint) *LayeredStoreSupplierResult {
+ return s.Next().SchemeGetAllPage(ctx, scope, offset, limit, hints...)
+}
+
+func (s *LocalCacheSupplier) SchemePermanentDeleteAll(ctx context.Context, hints ...LayeredStoreHint) *LayeredStoreSupplierResult {
+ defer s.doClearCacheCluster(s.schemeCache)
+
+ return s.Next().SchemePermanentDeleteAll(ctx, hints...)
+}
diff --git a/store/redis_supplier_roles.go b/store/redis_supplier_roles.go
index c53614113..49f8ede48 100644
--- a/store/redis_supplier_roles.go
+++ b/store/redis_supplier_roles.go
@@ -83,6 +83,21 @@ func (s *RedisSupplier) RoleGetByNames(ctx context.Context, roleNames []string,
return result
}
+func (s *RedisSupplier) RoleDelete(ctx context.Context, roleId string, hints ...LayeredStoreHint) *LayeredStoreSupplierResult {
+ result := s.Next().RoleGet(ctx, roleId, hints...)
+
+ if result.Err == nil {
+ role := result.Data.(*model.Role)
+ key := buildRedisKeyForRoleName(role.Name)
+
+ if err := s.client.Del(key).Err(); err != nil {
+ mlog.Error("Redis failed to remove key " + key + " Error: " + err.Error())
+ }
+ }
+
+ return result
+}
+
func (s *RedisSupplier) RolePermanentDeleteAll(ctx context.Context, hints ...LayeredStoreHint) *LayeredStoreSupplierResult {
defer func() {
if keys, err := s.client.Keys("roles:*").Result(); err != nil {
diff --git a/store/redis_supplier_schemes.go b/store/redis_supplier_schemes.go
new file mode 100644
index 000000000..1af9dafde
--- /dev/null
+++ b/store/redis_supplier_schemes.go
@@ -0,0 +1,35 @@
+// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package store
+
+import (
+ "context"
+
+ "github.com/mattermost/mattermost-server/model"
+)
+
+func (s *RedisSupplier) SchemeSave(ctx context.Context, scheme *model.Scheme, hints ...LayeredStoreHint) *LayeredStoreSupplierResult {
+ // TODO: Redis caching.
+ return s.Next().SchemeSave(ctx, scheme, hints...)
+}
+
+func (s *RedisSupplier) SchemeGet(ctx context.Context, schemeId string, hints ...LayeredStoreHint) *LayeredStoreSupplierResult {
+ // TODO: Redis caching.
+ return s.Next().SchemeGet(ctx, schemeId, hints...)
+}
+
+func (s *RedisSupplier) SchemeDelete(ctx context.Context, schemeId string, hints ...LayeredStoreHint) *LayeredStoreSupplierResult {
+ // TODO: Redis caching.
+ return s.Next().SchemeDelete(ctx, schemeId, hints...)
+}
+
+func (s *RedisSupplier) SchemeGetAllPage(ctx context.Context, scope string, offset int, limit int, hints ...LayeredStoreHint) *LayeredStoreSupplierResult {
+ // TODO: Redis caching.
+ return s.Next().SchemeGetAllPage(ctx, scope, offset, limit, hints...)
+}
+
+func (s *RedisSupplier) SchemePermanentDeleteAll(ctx context.Context, hints ...LayeredStoreHint) *LayeredStoreSupplierResult {
+ // TODO: Redis caching.
+ return s.Next().SchemePermanentDeleteAll(ctx, hints...)
+}
diff --git a/store/sqlstore/channel_store.go b/store/sqlstore/channel_store.go
index eebc3ad69..5f336d904 100644
--- a/store/sqlstore/channel_store.go
+++ b/store/sqlstore/channel_store.go
@@ -12,6 +12,7 @@ import (
"strings"
"github.com/mattermost/gorp"
+
"github.com/mattermost/mattermost-server/einterfaces"
"github.com/mattermost/mattermost-server/mlog"
"github.com/mattermost/mattermost-server/model"
@@ -37,6 +38,200 @@ type SqlChannelStore struct {
metrics einterfaces.MetricsInterface
}
+type channelMember struct {
+ ChannelId string
+ UserId string
+ Roles string
+ LastViewedAt int64
+ MsgCount int64
+ MentionCount int64
+ NotifyProps model.StringMap
+ LastUpdateAt int64
+ SchemeUser sql.NullBool
+ SchemeAdmin sql.NullBool
+}
+
+func NewChannelMemberFromModel(cm *model.ChannelMember) *channelMember {
+ return &channelMember{
+ ChannelId: cm.ChannelId,
+ UserId: cm.UserId,
+ Roles: cm.ExplicitRoles,
+ LastViewedAt: cm.LastViewedAt,
+ MsgCount: cm.MsgCount,
+ MentionCount: cm.MentionCount,
+ NotifyProps: cm.NotifyProps,
+ LastUpdateAt: cm.LastUpdateAt,
+ SchemeUser: sql.NullBool{Valid: true, Bool: cm.SchemeUser},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: cm.SchemeAdmin},
+ }
+}
+
+type channelMemberWithSchemeRoles struct {
+ ChannelId string
+ UserId string
+ Roles string
+ LastViewedAt int64
+ MsgCount int64
+ MentionCount int64
+ NotifyProps model.StringMap
+ LastUpdateAt int64
+ SchemeUser sql.NullBool
+ SchemeAdmin sql.NullBool
+ TeamSchemeDefaultUserRole sql.NullString
+ TeamSchemeDefaultAdminRole sql.NullString
+ ChannelSchemeDefaultUserRole sql.NullString
+ ChannelSchemeDefaultAdminRole sql.NullString
+}
+
+type channelMemberWithSchemeRolesList []channelMemberWithSchemeRoles
+
+func (db channelMemberWithSchemeRoles) ToModel() *model.ChannelMember {
+ var roles []string
+ var explicitRoles []string
+
+ // Identify any system-wide scheme derived roles that are in "Roles" field due to not yet being migrated,
+ // and exclude them from ExplicitRoles field.
+ schemeUser := db.SchemeUser.Valid && db.SchemeUser.Bool
+ schemeAdmin := db.SchemeAdmin.Valid && db.SchemeAdmin.Bool
+ for _, role := range strings.Fields(db.Roles) {
+ isImplicit := false
+ if role == model.CHANNEL_USER_ROLE_ID {
+ // We have an implicit role via the system scheme. Override the "schemeUser" field to true.
+ schemeUser = true
+ isImplicit = true
+ } else if role == model.CHANNEL_ADMIN_ROLE_ID {
+ // We have an implicit role via the system scheme.
+ schemeAdmin = true
+ isImplicit = true
+ }
+
+ if !isImplicit {
+ explicitRoles = append(explicitRoles, role)
+ }
+ roles = append(roles, role)
+ }
+
+ // Add any scheme derived roles that are not in the Roles field due to being Implicit from the Scheme, and add
+ // them to the Roles field for backwards compatibility reasons.
+ var schemeImpliedRoles []string
+ if db.SchemeUser.Valid && db.SchemeUser.Bool {
+ if db.ChannelSchemeDefaultUserRole.Valid && db.ChannelSchemeDefaultUserRole.String != "" {
+ schemeImpliedRoles = append(schemeImpliedRoles, db.ChannelSchemeDefaultUserRole.String)
+ } else if db.TeamSchemeDefaultUserRole.Valid && db.TeamSchemeDefaultUserRole.String != "" {
+ schemeImpliedRoles = append(schemeImpliedRoles, db.TeamSchemeDefaultUserRole.String)
+ } else {
+ schemeImpliedRoles = append(schemeImpliedRoles, model.CHANNEL_USER_ROLE_ID)
+ }
+ }
+ if db.SchemeAdmin.Valid && db.SchemeAdmin.Bool {
+ if db.ChannelSchemeDefaultAdminRole.Valid && db.ChannelSchemeDefaultAdminRole.String != "" {
+ schemeImpliedRoles = append(schemeImpliedRoles, db.ChannelSchemeDefaultAdminRole.String)
+ } else if db.TeamSchemeDefaultAdminRole.Valid && db.TeamSchemeDefaultAdminRole.String != "" {
+ schemeImpliedRoles = append(schemeImpliedRoles, db.TeamSchemeDefaultAdminRole.String)
+ } else {
+ schemeImpliedRoles = append(schemeImpliedRoles, model.CHANNEL_ADMIN_ROLE_ID)
+ }
+ }
+ for _, impliedRole := range schemeImpliedRoles {
+ alreadyThere := false
+ for _, role := range roles {
+ if role == impliedRole {
+ alreadyThere = true
+ }
+ }
+ if !alreadyThere {
+ roles = append(roles, impliedRole)
+ }
+ }
+
+ return &model.ChannelMember{
+ ChannelId: db.ChannelId,
+ UserId: db.UserId,
+ Roles: strings.Join(roles, " "),
+ LastViewedAt: db.LastViewedAt,
+ MsgCount: db.MsgCount,
+ MentionCount: db.MentionCount,
+ NotifyProps: db.NotifyProps,
+ LastUpdateAt: db.LastUpdateAt,
+ SchemeAdmin: schemeAdmin,
+ SchemeUser: schemeUser,
+ ExplicitRoles: strings.Join(explicitRoles, " "),
+ }
+}
+
+func (db channelMemberWithSchemeRolesList) ToModel() *model.ChannelMembers {
+ cms := model.ChannelMembers{}
+
+ for _, cm := range db {
+ cms = append(cms, *cm.ToModel())
+ }
+
+ return &cms
+}
+
+type allChannelMember struct {
+ ChannelId string
+ Roles string
+ SchemeUser sql.NullBool
+ SchemeAdmin sql.NullBool
+ TeamSchemeDefaultUserRole sql.NullString
+ TeamSchemeDefaultAdminRole sql.NullString
+ ChannelSchemeDefaultUserRole sql.NullString
+ ChannelSchemeDefaultAdminRole sql.NullString
+}
+
+type allChannelMembers []allChannelMember
+
+func (db allChannelMember) Process() (string, string) {
+ roles := strings.Fields(db.Roles)
+
+ // Add any scheme derived roles that are not in the Roles field due to being Implicit from the Scheme, and add
+ // them to the Roles field for backwards compatibility reasons.
+ var schemeImpliedRoles []string
+ if db.SchemeUser.Valid && db.SchemeUser.Bool {
+ if db.ChannelSchemeDefaultUserRole.Valid && db.ChannelSchemeDefaultUserRole.String != "" {
+ schemeImpliedRoles = append(schemeImpliedRoles, db.ChannelSchemeDefaultUserRole.String)
+ } else if db.TeamSchemeDefaultUserRole.Valid && db.TeamSchemeDefaultUserRole.String != "" {
+ schemeImpliedRoles = append(schemeImpliedRoles, db.TeamSchemeDefaultUserRole.String)
+ } else {
+ schemeImpliedRoles = append(schemeImpliedRoles, model.CHANNEL_USER_ROLE_ID)
+ }
+ }
+ if db.SchemeAdmin.Valid && db.SchemeAdmin.Bool {
+ if db.ChannelSchemeDefaultAdminRole.Valid && db.ChannelSchemeDefaultAdminRole.String != "" {
+ schemeImpliedRoles = append(schemeImpliedRoles, db.ChannelSchemeDefaultAdminRole.String)
+ } else if db.TeamSchemeDefaultAdminRole.Valid && db.TeamSchemeDefaultAdminRole.String != "" {
+ schemeImpliedRoles = append(schemeImpliedRoles, db.TeamSchemeDefaultAdminRole.String)
+ } else {
+ schemeImpliedRoles = append(schemeImpliedRoles, model.CHANNEL_ADMIN_ROLE_ID)
+ }
+ }
+ for _, impliedRole := range schemeImpliedRoles {
+ alreadyThere := false
+ for _, role := range roles {
+ if role == impliedRole {
+ alreadyThere = true
+ }
+ }
+ if !alreadyThere {
+ roles = append(roles, impliedRole)
+ }
+ }
+
+ return db.ChannelId, strings.Join(roles, " ")
+}
+
+func (db allChannelMembers) ToMapStringString() map[string]string {
+ result := make(map[string]string)
+
+ for _, item := range db {
+ key, value := item.Process()
+ result[key] = value
+ }
+
+ return result
+}
+
var channelMemberCountsCache = utils.NewLru(CHANNEL_MEMBERS_COUNTS_CACHE_SIZE)
var allChannelMembersForUserCache = utils.NewLru(ALL_CHANNEL_MEMBERS_FOR_USER_CACHE_SIZE)
var allChannelMembersNotifyPropsForChannelCache = utils.NewLru(ALL_CHANNEL_MEMBERS_NOTIFY_PROPS_FOR_CHANNEL_CACHE_SIZE)
@@ -76,8 +271,9 @@ func NewSqlChannelStore(sqlStore SqlStore, metrics einterfaces.MetricsInterface)
table.ColMap("Header").SetMaxSize(1024)
table.ColMap("Purpose").SetMaxSize(250)
table.ColMap("CreatorId").SetMaxSize(26)
+ table.ColMap("SchemeId").SetMaxSize(26)
- tablem := db.AddTableWithName(model.ChannelMember{}, "ChannelMembers").SetKeys(false, "ChannelId", "UserId")
+ tablem := db.AddTableWithName(channelMember{}, "ChannelMembers").SetKeys(false, "ChannelId", "UserId")
tablem.ColMap("ChannelId").SetMaxSize(26)
tablem.ColMap("UserId").SetMaxSize(26)
tablem.ColMap("Roles").SetMaxSize(64)
@@ -138,12 +334,12 @@ func (s SqlChannelStore) CreateDirectChannel(userId string, otherUserId string)
cm1 := &model.ChannelMember{
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
- Roles: model.CHANNEL_USER_ROLE_ID,
+ SchemeUser: true,
}
cm2 := &model.ChannelMember{
UserId: otherUserId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
- Roles: model.CHANNEL_USER_ROLE_ID,
+ SchemeUser: true,
}
return s.SaveDirectChannel(channel, cm1, cm2)
@@ -713,6 +909,25 @@ func (s SqlChannelStore) GetDeleted(teamId string, offset int, limit int) store.
})
}
+var CHANNEL_MEMBERS_WITH_SCHEME_SELECT_QUERY = `
+ SELECT
+ ChannelMembers.*,
+ TeamScheme.DefaultChannelUserRole TeamSchemeDefaultUserRole,
+ TeamScheme.DefaultChannelAdminRole TeamSchemeDefaultAdminRole,
+ ChannelScheme.DefaultChannelUserRole ChannelSchemeDefaultUserRole,
+ ChannelScheme.DefaultChannelAdminRole ChannelSchemeDefaultAdminRole
+ FROM
+ ChannelMembers
+ INNER JOIN
+ Channels ON ChannelMembers.ChannelId = Channels.Id
+ LEFT JOIN
+ Schemes ChannelScheme ON Channels.SchemeId = ChannelScheme.Id
+ LEFT JOIN
+ Teams ON Channels.TeamId = Teams.Id
+ LEFT JOIN
+ Schemes TeamScheme ON Teams.SchemeId = TeamScheme.Id
+`
+
func (s SqlChannelStore) SaveMember(member *model.ChannelMember) store.StoreChannel {
return store.Do(func(result *store.StoreResult) {
// Grab the channel we are saving this member to
@@ -747,14 +962,25 @@ func (s SqlChannelStore) saveMemberT(transaction *gorp.Transaction, member *mode
return result
}
- if err := transaction.Insert(member); err != nil {
+ dbMember := NewChannelMemberFromModel(member)
+
+ if err := transaction.Insert(dbMember); err != nil {
if IsUniqueConstraintError(err, []string{"ChannelId", "channelmembers_pkey"}) {
result.Err = model.NewAppError("SqlChannelStore.SaveMember", "store.sql_channel.save_member.exists.app_error", nil, "channel_id="+member.ChannelId+", user_id="+member.UserId+", "+err.Error(), http.StatusBadRequest)
} else {
result.Err = model.NewAppError("SqlChannelStore.SaveMember", "store.sql_channel.save_member.save.app_error", nil, "channel_id="+member.ChannelId+", user_id="+member.UserId+", "+err.Error(), http.StatusInternalServerError)
}
} else {
- result.Data = member
+ var retrievedMember channelMemberWithSchemeRoles
+ if err := transaction.SelectOne(&retrievedMember, CHANNEL_MEMBERS_WITH_SCHEME_SELECT_QUERY+"WHERE ChannelMembers.ChannelId = :ChannelId AND ChannelMembers.UserId = :UserId", map[string]interface{}{"ChannelId": dbMember.ChannelId, "UserId": dbMember.UserId}); err != nil {
+ if err == sql.ErrNoRows {
+ result.Err = model.NewAppError("SqlChannelStore.GetMember", store.MISSING_CHANNEL_MEMBER_ERROR, nil, "channel_id="+dbMember.ChannelId+"user_id="+dbMember.UserId+","+err.Error(), http.StatusNotFound)
+ } else {
+ result.Err = model.NewAppError("SqlChannelStore.GetMember", "store.sql_channel.get_member.app_error", nil, "channel_id="+dbMember.ChannelId+"user_id="+dbMember.UserId+","+err.Error(), http.StatusInternalServerError)
+ }
+ } else {
+ result.Data = retrievedMember.ToModel()
+ }
}
return result
@@ -768,38 +994,48 @@ func (s SqlChannelStore) UpdateMember(member *model.ChannelMember) store.StoreCh
return
}
- if _, err := s.GetMaster().Update(member); err != nil {
+ if _, err := s.GetMaster().Update(NewChannelMemberFromModel(member)); err != nil {
result.Err = model.NewAppError("SqlChannelStore.UpdateMember", "store.sql_channel.update_member.app_error", nil, "channel_id="+member.ChannelId+", "+"user_id="+member.UserId+", "+err.Error(), http.StatusInternalServerError)
} else {
- result.Data = member
+ var dbMember channelMemberWithSchemeRoles
+
+ if err := s.GetReplica().SelectOne(&dbMember, CHANNEL_MEMBERS_WITH_SCHEME_SELECT_QUERY+"WHERE ChannelMembers.ChannelId = :ChannelId AND ChannelMembers.UserId = :UserId", map[string]interface{}{"ChannelId": member.ChannelId, "UserId": member.UserId}); err != nil {
+ if err == sql.ErrNoRows {
+ result.Err = model.NewAppError("SqlChannelStore.GetMember", store.MISSING_CHANNEL_MEMBER_ERROR, nil, "channel_id="+member.ChannelId+"user_id="+member.UserId+","+err.Error(), http.StatusNotFound)
+ } else {
+ result.Err = model.NewAppError("SqlChannelStore.GetMember", "store.sql_channel.get_member.app_error", nil, "channel_id="+member.ChannelId+"user_id="+member.UserId+","+err.Error(), http.StatusInternalServerError)
+ }
+ } else {
+ result.Data = dbMember.ToModel()
+ }
}
})
}
func (s SqlChannelStore) GetMembers(channelId string, offset, limit int) store.StoreChannel {
return store.Do(func(result *store.StoreResult) {
- var members model.ChannelMembers
- _, err := s.GetReplica().Select(&members, "SELECT * FROM ChannelMembers WHERE ChannelId = :ChannelId LIMIT :Limit OFFSET :Offset", map[string]interface{}{"ChannelId": channelId, "Limit": limit, "Offset": offset})
+ var dbMembers channelMemberWithSchemeRolesList
+ _, err := s.GetReplica().Select(&dbMembers, CHANNEL_MEMBERS_WITH_SCHEME_SELECT_QUERY+"WHERE ChannelId = :ChannelId LIMIT :Limit OFFSET :Offset", map[string]interface{}{"ChannelId": channelId, "Limit": limit, "Offset": offset})
if err != nil {
result.Err = model.NewAppError("SqlChannelStore.GetMembers", "store.sql_channel.get_members.app_error", nil, "channel_id="+channelId+err.Error(), http.StatusInternalServerError)
} else {
- result.Data = &members
+ result.Data = dbMembers.ToModel()
}
})
}
func (s SqlChannelStore) GetMember(channelId string, userId string) store.StoreChannel {
return store.Do(func(result *store.StoreResult) {
- var member model.ChannelMember
+ var dbMember channelMemberWithSchemeRoles
- if err := s.GetReplica().SelectOne(&member, "SELECT * FROM ChannelMembers WHERE ChannelId = :ChannelId AND UserId = :UserId", map[string]interface{}{"ChannelId": channelId, "UserId": userId}); err != nil {
+ if err := s.GetReplica().SelectOne(&dbMember, CHANNEL_MEMBERS_WITH_SCHEME_SELECT_QUERY+"WHERE ChannelMembers.ChannelId = :ChannelId AND ChannelMembers.UserId = :UserId", map[string]interface{}{"ChannelId": channelId, "UserId": userId}); err != nil {
if err == sql.ErrNoRows {
result.Err = model.NewAppError("SqlChannelStore.GetMember", store.MISSING_CHANNEL_MEMBER_ERROR, nil, "channel_id="+channelId+"user_id="+userId+","+err.Error(), http.StatusNotFound)
} else {
result.Err = model.NewAppError("SqlChannelStore.GetMember", "store.sql_channel.get_member.app_error", nil, "channel_id="+channelId+"user_id="+userId+","+err.Error(), http.StatusInternalServerError)
}
} else {
- result.Data = &member
+ result.Data = dbMember.ToModel()
}
})
}
@@ -843,30 +1079,37 @@ func (s SqlChannelStore) IsUserInChannelUseCache(userId string, channelId string
func (s SqlChannelStore) GetMemberForPost(postId string, userId string) store.StoreChannel {
return store.Do(func(result *store.StoreResult) {
- member := &model.ChannelMember{}
- if err := s.GetReplica().SelectOne(
- member,
- `SELECT
- ChannelMembers.*
- FROM
- ChannelMembers,
- Posts
+ var dbMember channelMemberWithSchemeRoles
+ if err := s.GetReplica().SelectOne(&dbMember,
+ `
+ SELECT
+ ChannelMembers.*,
+ TeamScheme.DefaultChannelUserRole TeamSchemeDefaultUserRole,
+ TeamScheme.DefaultChannelAdminRole TeamSchemeDefaultAdminRole,
+ ChannelScheme.DefaultChannelUserRole ChannelSchemeDefaultUserRole,
+ ChannelScheme.DefaultChannelAdminRole ChannelSchemeDefaultAdminRole
+ FROM
+ ChannelMembers
+ INNER JOIN
+ Posts ON ChannelMembers.ChannelId = Posts.ChannelId
+ INNER JOIN
+ Channels ON ChannelMembers.ChannelId = Channels.Id
+ LEFT JOIN
+ Schemes ChannelScheme ON Channels.SchemeId = ChannelScheme.Id
+ LEFT JOIN
+ Teams ON Channels.TeamId = Teams.Id
+ LEFT JOIN
+ Schemes TeamScheme ON Teams.SchemeId = TeamScheme.Id
WHERE
- ChannelMembers.ChannelId = Posts.ChannelId
- AND ChannelMembers.UserId = :UserId
+ ChannelMembers.UserId = :UserId
AND Posts.Id = :PostId`, map[string]interface{}{"UserId": userId, "PostId": postId}); err != nil {
result.Err = model.NewAppError("SqlChannelStore.GetMemberForPost", "store.sql_channel.get_member_for_post.app_error", nil, "postId="+postId+", err="+err.Error(), http.StatusInternalServerError)
} else {
- result.Data = member
+ result.Data = dbMember.ToModel()
}
})
}
-type allChannelMember struct {
- ChannelId string
- Roles string
-}
-
func (s SqlChannelStore) GetAllChannelMembersForUser(userId string, allowFromCache bool) store.StoreChannel {
return store.Do(func(result *store.StoreResult) {
if allowFromCache {
@@ -887,17 +1130,32 @@ func (s SqlChannelStore) GetAllChannelMembersForUser(userId string, allowFromCac
}
}
- var data []allChannelMember
- _, err := s.GetReplica().Select(&data, "SELECT ChannelId, Roles FROM Channels, ChannelMembers WHERE Channels.Id = ChannelMembers.ChannelId AND ChannelMembers.UserId = :UserId AND Channels.DeleteAt = 0", map[string]interface{}{"UserId": userId})
+ var data allChannelMembers
+ _, err := s.GetReplica().Select(&data, `
+ SELECT
+ ChannelMembers.ChannelId, ChannelMembers.Roles, ChannelMembers.SchemeUser, ChannelMembers.SchemeAdmin,
+ TeamScheme.DefaultChannelUserRole TeamSchemeDefaultUserRole,
+ TeamScheme.DefaultChannelAdminRole TeamSchemeDefaultAdminRole,
+ ChannelScheme.DefaultChannelUserRole ChannelSchemeDefaultUserRole,
+ ChannelScheme.DefaultChannelAdminRole ChannelSchemeDefaultAdminRole
+ FROM
+ ChannelMembers
+ INNER JOIN
+ Channels ON ChannelMembers.ChannelId = Channels.Id
+ LEFT JOIN
+ Schemes ChannelScheme ON Channels.SchemeId = ChannelScheme.Id
+ LEFT JOIN
+ Teams ON Channels.TeamId = Teams.Id
+ LEFT JOIN
+ Schemes TeamScheme ON Teams.SchemeId = TeamScheme.Id
+ WHERE
+ Channels.DeleteAt = 0
+ AND ChannelMembers.UserId = :UserId`, map[string]interface{}{"UserId": userId})
if err != nil {
result.Err = model.NewAppError("SqlChannelStore.GetAllChannelMembersForUser", "store.sql_channel.get_channels.get.app_error", nil, "userId="+userId+", err="+err.Error(), http.StatusInternalServerError)
} else {
-
- ids := make(map[string]string)
- for i := range data {
- ids[data[i].ChannelId] = data[i].Roles
- }
+ ids := data.ToMapStringString()
result.Data = ids
@@ -1214,21 +1472,13 @@ func (s SqlChannelStore) AnalyticsDeletedTypeCount(teamId string, channelType st
func (s SqlChannelStore) GetMembersForUser(teamId string, userId string) store.StoreChannel {
return store.Do(func(result *store.StoreResult) {
- members := &model.ChannelMembers{}
- _, err := s.GetReplica().Select(members, `
- SELECT cm.*
- FROM ChannelMembers cm
- INNER JOIN Channels c
- ON c.Id = cm.ChannelId
- AND (c.TeamId = :TeamId OR c.TeamId = '')
- AND c.DeleteAt = 0
- WHERE cm.UserId = :UserId
- `, map[string]interface{}{"TeamId": teamId, "UserId": userId})
+ var dbMembers channelMemberWithSchemeRolesList
+ _, err := s.GetReplica().Select(&dbMembers, CHANNEL_MEMBERS_WITH_SCHEME_SELECT_QUERY+"WHERE ChannelMembers.UserId = :UserId", map[string]interface{}{"TeamId": teamId, "UserId": userId})
if err != nil {
result.Err = model.NewAppError("SqlChannelStore.GetMembersForUser", "store.sql_channel.get_members.app_error", nil, "teamId="+teamId+", userId="+userId+", err="+err.Error(), http.StatusInternalServerError)
} else {
- result.Data = members
+ result.Data = dbMembers.ToModel()
}
})
}
@@ -1420,7 +1670,7 @@ func (s SqlChannelStore) performSearch(searchQuery string, term string, paramete
func (s SqlChannelStore) GetMembersByIds(channelId string, userIds []string) store.StoreChannel {
return store.Do(func(result *store.StoreResult) {
- var members model.ChannelMembers
+ var dbMembers channelMemberWithSchemeRolesList
props := make(map[string]interface{})
idQuery := ""
@@ -1435,11 +1685,98 @@ func (s SqlChannelStore) GetMembersByIds(channelId string, userIds []string) sto
props["ChannelId"] = channelId
- if _, err := s.GetReplica().Select(&members, "SELECT * FROM ChannelMembers WHERE ChannelId = :ChannelId AND UserId IN ("+idQuery+")", props); err != nil {
+ if _, err := s.GetReplica().Select(&dbMembers, CHANNEL_MEMBERS_WITH_SCHEME_SELECT_QUERY+"WHERE ChannelMembers.ChannelId = :ChannelId AND ChannelMembers.UserId IN ("+idQuery+")", props); err != nil {
result.Err = model.NewAppError("SqlChannelStore.GetMembersByIds", "store.sql_channel.get_members_by_ids.app_error", nil, "channelId="+channelId+" "+err.Error(), http.StatusInternalServerError)
} else {
- result.Data = &members
+ result.Data = dbMembers.ToModel()
+ }
+ })
+}
+
+func (s SqlChannelStore) GetChannelsByScheme(schemeId string, offset int, limit int) store.StoreChannel {
+ return store.Do(func(result *store.StoreResult) {
+ var channels model.ChannelList
+ _, err := s.GetReplica().Select(&channels, "SELECT * FROM Channels WHERE SchemeId = :SchemeId ORDER BY DisplayName LIMIT :Limit OFFSET :Offset", map[string]interface{}{"SchemeId": schemeId, "Offset": offset, "Limit": limit})
+ if err != nil {
+ result.Err = model.NewAppError("SqlChannelStore.GetChannelsByScheme", "store.sql_channel.get_by_scheme.app_error", nil, "schemeId="+schemeId+" "+err.Error(), http.StatusInternalServerError)
+ } else {
+ result.Data = channels
+ }
+ })
+}
+
+// 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
+ })
+}
+
+func (s SqlChannelStore) ResetAllChannelSchemes() store.StoreChannel {
+ return store.Do(func(result *store.StoreResult) {
+ if _, err := s.GetMaster().Exec("UPDATE Channels SET SchemeId=''"); err != nil {
+ result.Err = model.NewAppError("SqlChannelStore.ResetAllChannelSchemes", "store.sql_channel.reset_all_channel_schemes.app_error", nil, err.Error(), http.StatusInternalServerError)
}
})
}
diff --git a/store/sqlstore/channel_store_test.go b/store/sqlstore/channel_store_test.go
index 8e5ad5f0f..0e8b4191a 100644
--- a/store/sqlstore/channel_store_test.go
+++ b/store/sqlstore/channel_store_test.go
@@ -4,11 +4,937 @@
package sqlstore
import (
+ "database/sql"
"testing"
+ "github.com/stretchr/testify/assert"
+
+ "github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/store/storetest"
)
func TestChannelStore(t *testing.T) {
StoreTest(t, storetest.TestChannelStore)
}
+
+func TestChannelStoreInternalDataTypes(t *testing.T) {
+ t.Run("NewChannelMemberFromModel", func(t *testing.T) { testNewChannelMemberFromModel(t) })
+ t.Run("ChannelMemberWithSchemeRolesToModel", func(t *testing.T) { testChannelMemberWithSchemeRolesToModel(t) })
+ t.Run("AllChannelMemberProcess", func(t *testing.T) { testAllChannelMemberProcess(t) })
+}
+
+func testNewChannelMemberFromModel(t *testing.T) {
+ m := model.ChannelMember{
+ ChannelId: model.NewId(),
+ UserId: model.NewId(),
+ Roles: "channel_user channel_admin custom_role",
+ LastViewedAt: 12345,
+ MsgCount: 2,
+ MentionCount: 1,
+ NotifyProps: model.StringMap{"key": "value"},
+ LastUpdateAt: 54321,
+ SchemeUser: true,
+ SchemeAdmin: true,
+ ExplicitRoles: "custom_role",
+ }
+
+ db := NewChannelMemberFromModel(&m)
+
+ assert.Equal(t, m.ChannelId, db.ChannelId)
+ assert.Equal(t, m.UserId, db.UserId)
+ assert.Equal(t, m.LastViewedAt, db.LastViewedAt)
+ assert.Equal(t, m.MsgCount, db.MsgCount)
+ assert.Equal(t, m.MentionCount, db.MentionCount)
+ assert.Equal(t, m.NotifyProps, db.NotifyProps)
+ assert.Equal(t, m.LastUpdateAt, db.LastUpdateAt)
+ assert.Equal(t, true, db.SchemeUser.Valid)
+ assert.Equal(t, true, db.SchemeAdmin.Valid)
+ assert.Equal(t, m.SchemeUser, db.SchemeUser.Bool)
+ assert.Equal(t, m.SchemeAdmin, db.SchemeAdmin.Bool)
+ assert.Equal(t, m.ExplicitRoles, db.Roles)
+}
+
+func testChannelMemberWithSchemeRolesToModel(t *testing.T) {
+ t.Run("BasicProperties", func(t *testing.T) {
+ // Test all the non-roles properties here.
+ db := channelMemberWithSchemeRoles{
+ ChannelId: model.NewId(),
+ UserId: model.NewId(),
+ Roles: "custom_role",
+ LastViewedAt: 12345,
+ MsgCount: 2,
+ MentionCount: 1,
+ NotifyProps: model.StringMap{"key": "value"},
+ LastUpdateAt: 54321,
+ SchemeUser: sql.NullBool{Valid: true, Bool: true},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: true},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: false},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultUserRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ }
+
+ m := db.ToModel()
+
+ assert.Equal(t, db.ChannelId, m.ChannelId)
+ assert.Equal(t, db.UserId, m.UserId)
+ assert.Equal(t, "custom_role channel_user channel_admin", m.Roles)
+ assert.Equal(t, db.LastViewedAt, m.LastViewedAt)
+ assert.Equal(t, db.MsgCount, m.MsgCount)
+ assert.Equal(t, db.MentionCount, m.MentionCount)
+ assert.Equal(t, db.NotifyProps, m.NotifyProps)
+ assert.Equal(t, db.LastUpdateAt, m.LastUpdateAt)
+ assert.Equal(t, db.SchemeUser.Bool, m.SchemeUser)
+ assert.Equal(t, db.SchemeAdmin.Bool, m.SchemeAdmin)
+ assert.Equal(t, db.Roles, m.ExplicitRoles)
+ })
+
+ // Example data *before* the Phase 2 migration has taken place.
+ t.Run("Unmigrated_NoScheme_User", func(t *testing.T) {
+ db := channelMemberWithSchemeRoles{
+ Roles: "channel_user",
+ SchemeUser: sql.NullBool{Valid: false, Bool: false},
+ SchemeAdmin: sql.NullBool{Valid: false, Bool: false},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: false},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultUserRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ }
+
+ cm := db.ToModel()
+
+ assert.Equal(t, "channel_user", cm.Roles)
+ assert.Equal(t, true, cm.SchemeUser)
+ assert.Equal(t, false, cm.SchemeAdmin)
+ assert.Equal(t, "", cm.ExplicitRoles)
+ })
+
+ t.Run("Unmigrated_NoScheme_Admin", func(t *testing.T) {
+ db := channelMemberWithSchemeRoles{
+ Roles: "channel_admin channel_user",
+ SchemeUser: sql.NullBool{Valid: false, Bool: false},
+ SchemeAdmin: sql.NullBool{Valid: false, Bool: false},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: false},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultUserRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ }
+
+ cm := db.ToModel()
+
+ assert.Equal(t, "channel_admin channel_user", cm.Roles)
+ assert.Equal(t, true, cm.SchemeUser)
+ assert.Equal(t, true, cm.SchemeAdmin)
+ assert.Equal(t, "", cm.ExplicitRoles)
+ })
+
+ t.Run("Unmigrated_NoScheme_CustomRole", func(t *testing.T) {
+ db := channelMemberWithSchemeRoles{
+ Roles: "custom_role",
+ SchemeUser: sql.NullBool{Valid: false, Bool: false},
+ SchemeAdmin: sql.NullBool{Valid: false, Bool: false},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: false},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultUserRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ }
+
+ cm := db.ToModel()
+
+ assert.Equal(t, "custom_role", cm.Roles)
+ assert.Equal(t, false, cm.SchemeUser)
+ assert.Equal(t, false, cm.SchemeAdmin)
+ assert.Equal(t, "custom_role", cm.ExplicitRoles)
+ })
+
+ t.Run("Unmigrated_NoScheme_UserAndCustomRole", func(t *testing.T) {
+ db := channelMemberWithSchemeRoles{
+ Roles: "channel_user custom_role",
+ SchemeUser: sql.NullBool{Valid: false, Bool: false},
+ SchemeAdmin: sql.NullBool{Valid: false, Bool: false},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: false},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultUserRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ }
+
+ cm := db.ToModel()
+
+ assert.Equal(t, "channel_user custom_role", cm.Roles)
+ assert.Equal(t, true, cm.SchemeUser)
+ assert.Equal(t, false, cm.SchemeAdmin)
+ assert.Equal(t, "custom_role", cm.ExplicitRoles)
+ })
+
+ t.Run("Unmigrated_NoScheme_AdminAndCustomRole", func(t *testing.T) {
+ db := channelMemberWithSchemeRoles{
+ Roles: "channel_user channel_admin custom_role",
+ SchemeUser: sql.NullBool{Valid: false, Bool: false},
+ SchemeAdmin: sql.NullBool{Valid: false, Bool: false},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: false},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultUserRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ }
+
+ cm := db.ToModel()
+
+ assert.Equal(t, "channel_user channel_admin custom_role", cm.Roles)
+ assert.Equal(t, true, cm.SchemeUser)
+ assert.Equal(t, true, cm.SchemeAdmin)
+ assert.Equal(t, "custom_role", cm.ExplicitRoles)
+ })
+
+ t.Run("Unmigrated_NoScheme_NoRoles", func(t *testing.T) {
+ db := channelMemberWithSchemeRoles{
+ Roles: "",
+ SchemeUser: sql.NullBool{Valid: false, Bool: false},
+ SchemeAdmin: sql.NullBool{Valid: false, Bool: false},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: false},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultUserRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ }
+
+ cm := db.ToModel()
+
+ assert.Equal(t, "", cm.Roles)
+ assert.Equal(t, false, cm.SchemeUser)
+ assert.Equal(t, false, cm.SchemeAdmin)
+ assert.Equal(t, "", cm.ExplicitRoles)
+ })
+
+ // Example data *after* the Phase 2 migration has taken place.
+ t.Run("Migrated_NoScheme_User", func(t *testing.T) {
+ db := channelMemberWithSchemeRoles{
+ Roles: "",
+ SchemeUser: sql.NullBool{Valid: true, Bool: true},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: false},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: false},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultUserRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ }
+
+ cm := db.ToModel()
+
+ assert.Equal(t, "channel_user", cm.Roles)
+ assert.Equal(t, true, cm.SchemeUser)
+ assert.Equal(t, false, cm.SchemeAdmin)
+ assert.Equal(t, "", cm.ExplicitRoles)
+ })
+
+ t.Run("Migrated_NoScheme_Admin", func(t *testing.T) {
+ db := channelMemberWithSchemeRoles{
+ Roles: "",
+ SchemeUser: sql.NullBool{Valid: true, Bool: true},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: true},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: false},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultUserRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ }
+
+ cm := db.ToModel()
+
+ assert.Equal(t, "channel_user channel_admin", cm.Roles)
+ assert.Equal(t, true, cm.SchemeUser)
+ assert.Equal(t, true, cm.SchemeAdmin)
+ assert.Equal(t, "", cm.ExplicitRoles)
+ })
+
+ t.Run("Migrated_NoScheme_CustomRole", func(t *testing.T) {
+ db := channelMemberWithSchemeRoles{
+ Roles: "custom_role",
+ SchemeUser: sql.NullBool{Valid: true, Bool: false},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: false},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: false},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultUserRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ }
+
+ cm := db.ToModel()
+
+ assert.Equal(t, "custom_role", cm.Roles)
+ assert.Equal(t, false, cm.SchemeUser)
+ assert.Equal(t, false, cm.SchemeAdmin)
+ assert.Equal(t, "custom_role", cm.ExplicitRoles)
+ })
+
+ t.Run("Migrated_NoScheme_UserAndCustomRole", func(t *testing.T) {
+ db := channelMemberWithSchemeRoles{
+ Roles: "custom_role",
+ SchemeUser: sql.NullBool{Valid: true, Bool: true},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: false},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: false},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultUserRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ }
+
+ cm := db.ToModel()
+
+ assert.Equal(t, "custom_role channel_user", cm.Roles)
+ assert.Equal(t, true, cm.SchemeUser)
+ assert.Equal(t, false, cm.SchemeAdmin)
+ assert.Equal(t, "custom_role", cm.ExplicitRoles)
+ })
+
+ t.Run("Migrated_NoScheme_AdminAndCustomRole", func(t *testing.T) {
+ db := channelMemberWithSchemeRoles{
+ Roles: "custom_role",
+ SchemeUser: sql.NullBool{Valid: true, Bool: true},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: true},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: false},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultUserRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ }
+
+ cm := db.ToModel()
+
+ assert.Equal(t, "custom_role channel_user channel_admin", cm.Roles)
+ assert.Equal(t, true, cm.SchemeUser)
+ assert.Equal(t, true, cm.SchemeAdmin)
+ assert.Equal(t, "custom_role", cm.ExplicitRoles)
+ })
+
+ t.Run("Migrated_NoScheme_NoRoles", func(t *testing.T) {
+ db := channelMemberWithSchemeRoles{
+ Roles: "",
+ SchemeUser: sql.NullBool{Valid: true, Bool: false},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: false},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: false},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultUserRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ }
+
+ cm := db.ToModel()
+
+ assert.Equal(t, "", cm.Roles)
+ assert.Equal(t, false, cm.SchemeUser)
+ assert.Equal(t, false, cm.SchemeAdmin)
+ assert.Equal(t, "", cm.ExplicitRoles)
+ })
+
+ // Example data with a channel scheme.
+ t.Run("Migrated_ChannelScheme_User", func(t *testing.T) {
+ db := channelMemberWithSchemeRoles{
+ Roles: "",
+ SchemeUser: sql.NullBool{Valid: true, Bool: true},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: false},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: false},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultUserRole: sql.NullString{Valid: true, String: "cscheme_user"},
+ ChannelSchemeDefaultAdminRole: sql.NullString{Valid: true, String: "cscheme_admin"},
+ }
+
+ cm := db.ToModel()
+
+ assert.Equal(t, "cscheme_user", cm.Roles)
+ assert.Equal(t, true, cm.SchemeUser)
+ assert.Equal(t, false, cm.SchemeAdmin)
+ assert.Equal(t, "", cm.ExplicitRoles)
+ })
+
+ t.Run("Migrated_ChannelScheme_Admin", func(t *testing.T) {
+ db := channelMemberWithSchemeRoles{
+ Roles: "",
+ SchemeUser: sql.NullBool{Valid: true, Bool: true},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: true},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: false},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultUserRole: sql.NullString{Valid: true, String: "cscheme_user"},
+ ChannelSchemeDefaultAdminRole: sql.NullString{Valid: true, String: "cscheme_admin"},
+ }
+
+ cm := db.ToModel()
+
+ assert.Equal(t, "cscheme_user cscheme_admin", cm.Roles)
+ assert.Equal(t, true, cm.SchemeUser)
+ assert.Equal(t, true, cm.SchemeAdmin)
+ assert.Equal(t, "", cm.ExplicitRoles)
+ })
+
+ t.Run("Migrated_ChannelScheme_CustomRole", func(t *testing.T) {
+ db := channelMemberWithSchemeRoles{
+ Roles: "custom_role",
+ SchemeUser: sql.NullBool{Valid: true, Bool: false},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: false},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: false},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultUserRole: sql.NullString{Valid: true, String: "cscheme_user"},
+ ChannelSchemeDefaultAdminRole: sql.NullString{Valid: true, String: "cscheme_admin"},
+ }
+
+ cm := db.ToModel()
+
+ assert.Equal(t, "custom_role", cm.Roles)
+ assert.Equal(t, false, cm.SchemeUser)
+ assert.Equal(t, false, cm.SchemeAdmin)
+ assert.Equal(t, "custom_role", cm.ExplicitRoles)
+ })
+
+ t.Run("Migrated_ChannelScheme_UserAndCustomRole", func(t *testing.T) {
+ db := channelMemberWithSchemeRoles{
+ Roles: "custom_role",
+ SchemeUser: sql.NullBool{Valid: true, Bool: true},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: false},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: false},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultUserRole: sql.NullString{Valid: true, String: "cscheme_user"},
+ ChannelSchemeDefaultAdminRole: sql.NullString{Valid: true, String: "cscheme_admin"},
+ }
+
+ cm := db.ToModel()
+
+ assert.Equal(t, "custom_role cscheme_user", cm.Roles)
+ assert.Equal(t, true, cm.SchemeUser)
+ assert.Equal(t, false, cm.SchemeAdmin)
+ assert.Equal(t, "custom_role", cm.ExplicitRoles)
+ })
+
+ t.Run("Migrated_ChannelScheme_AdminAndCustomRole", func(t *testing.T) {
+ db := channelMemberWithSchemeRoles{
+ Roles: "custom_role",
+ SchemeUser: sql.NullBool{Valid: true, Bool: true},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: true},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: false},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultUserRole: sql.NullString{Valid: true, String: "cscheme_user"},
+ ChannelSchemeDefaultAdminRole: sql.NullString{Valid: true, String: "cscheme_admin"},
+ }
+
+ cm := db.ToModel()
+
+ assert.Equal(t, "custom_role cscheme_user cscheme_admin", cm.Roles)
+ assert.Equal(t, true, cm.SchemeUser)
+ assert.Equal(t, true, cm.SchemeAdmin)
+ assert.Equal(t, "custom_role", cm.ExplicitRoles)
+ })
+
+ t.Run("Migrated_ChannelScheme_NoRoles", func(t *testing.T) {
+ db := channelMemberWithSchemeRoles{
+ Roles: "",
+ SchemeUser: sql.NullBool{Valid: true, Bool: false},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: false},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: false},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultUserRole: sql.NullString{Valid: true, String: "cscheme_user"},
+ ChannelSchemeDefaultAdminRole: sql.NullString{Valid: true, String: "cscheme_admin"},
+ }
+
+ cm := db.ToModel()
+
+ assert.Equal(t, "", cm.Roles)
+ assert.Equal(t, false, cm.SchemeUser)
+ assert.Equal(t, false, cm.SchemeAdmin)
+ assert.Equal(t, "", cm.ExplicitRoles)
+ })
+
+ // Example data with a team scheme.
+ t.Run("Migrated_TeamScheme_User", func(t *testing.T) {
+ db := channelMemberWithSchemeRoles{
+ Roles: "",
+ SchemeUser: sql.NullBool{Valid: true, Bool: true},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: false},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: true, String: "tscheme_channeluser"},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: true, String: "tscheme_channeladmin"},
+ ChannelSchemeDefaultUserRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ }
+
+ cm := db.ToModel()
+
+ assert.Equal(t, "tscheme_channeluser", cm.Roles)
+ assert.Equal(t, true, cm.SchemeUser)
+ assert.Equal(t, false, cm.SchemeAdmin)
+ assert.Equal(t, "", cm.ExplicitRoles)
+ })
+
+ t.Run("Migrated_TeamScheme_Admin", func(t *testing.T) {
+ db := channelMemberWithSchemeRoles{
+ Roles: "",
+ SchemeUser: sql.NullBool{Valid: true, Bool: true},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: true},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: true, String: "tscheme_channeluser"},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: true, String: "tscheme_channeladmin"},
+ ChannelSchemeDefaultUserRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ }
+
+ cm := db.ToModel()
+
+ assert.Equal(t, "tscheme_channeluser tscheme_channeladmin", cm.Roles)
+ assert.Equal(t, true, cm.SchemeUser)
+ assert.Equal(t, true, cm.SchemeAdmin)
+ assert.Equal(t, "", cm.ExplicitRoles)
+ })
+
+ t.Run("Migrated_TeamScheme_CustomRole", func(t *testing.T) {
+ db := channelMemberWithSchemeRoles{
+ Roles: "custom_role",
+ SchemeUser: sql.NullBool{Valid: true, Bool: false},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: false},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: true, String: "tscheme_channeluser"},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: true, String: "tscheme_channeladmin"},
+ ChannelSchemeDefaultUserRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ }
+
+ cm := db.ToModel()
+
+ assert.Equal(t, "custom_role", cm.Roles)
+ assert.Equal(t, false, cm.SchemeUser)
+ assert.Equal(t, false, cm.SchemeAdmin)
+ assert.Equal(t, "custom_role", cm.ExplicitRoles)
+ })
+
+ t.Run("Migrated_TeamScheme_UserAndCustomRole", func(t *testing.T) {
+ db := channelMemberWithSchemeRoles{
+ Roles: "custom_role",
+ SchemeUser: sql.NullBool{Valid: true, Bool: true},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: false},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: true, String: "tscheme_channeluser"},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: true, String: "tscheme_channeladmin"},
+ ChannelSchemeDefaultUserRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ }
+
+ cm := db.ToModel()
+
+ assert.Equal(t, "custom_role tscheme_channeluser", cm.Roles)
+ assert.Equal(t, true, cm.SchemeUser)
+ assert.Equal(t, false, cm.SchemeAdmin)
+ assert.Equal(t, "custom_role", cm.ExplicitRoles)
+ })
+
+ t.Run("Migrated_TeamScheme_AdminAndCustomRole", func(t *testing.T) {
+ db := channelMemberWithSchemeRoles{
+ Roles: "custom_role",
+ SchemeUser: sql.NullBool{Valid: true, Bool: true},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: true},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: true, String: "tscheme_channeluser"},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: true, String: "tscheme_channeladmin"},
+ ChannelSchemeDefaultUserRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ }
+
+ cm := db.ToModel()
+
+ assert.Equal(t, "custom_role tscheme_channeluser tscheme_channeladmin", cm.Roles)
+ assert.Equal(t, true, cm.SchemeUser)
+ assert.Equal(t, true, cm.SchemeAdmin)
+ assert.Equal(t, "custom_role", cm.ExplicitRoles)
+ })
+
+ t.Run("Migrated_TeamScheme_NoRoles", func(t *testing.T) {
+ db := channelMemberWithSchemeRoles{
+ Roles: "",
+ SchemeUser: sql.NullBool{Valid: true, Bool: false},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: false},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: true, String: "tscheme_channeluser"},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: true, String: "tscheme_channeladmin"},
+ ChannelSchemeDefaultUserRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ }
+
+ cm := db.ToModel()
+
+ assert.Equal(t, "", cm.Roles)
+ assert.Equal(t, false, cm.SchemeUser)
+ assert.Equal(t, false, cm.SchemeAdmin)
+ assert.Equal(t, "", cm.ExplicitRoles)
+ })
+
+ // Example data with a team and channel scheme.
+ t.Run("Migrated_TeamAndChannelScheme_User", func(t *testing.T) {
+ db := channelMemberWithSchemeRoles{
+ Roles: "",
+ SchemeUser: sql.NullBool{Valid: true, Bool: true},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: false},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: true, String: "tscheme_channeluser"},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: true, String: "tscheme_channeladmin"},
+ ChannelSchemeDefaultUserRole: sql.NullString{Valid: true, String: "cscheme_user"},
+ ChannelSchemeDefaultAdminRole: sql.NullString{Valid: true, String: "cscheme_admin"},
+ }
+
+ cm := db.ToModel()
+
+ assert.Equal(t, "cscheme_user", cm.Roles)
+ assert.Equal(t, true, cm.SchemeUser)
+ assert.Equal(t, false, cm.SchemeAdmin)
+ assert.Equal(t, "", cm.ExplicitRoles)
+ })
+
+ t.Run("Migrated_TeamAndChannelScheme_Admin", func(t *testing.T) {
+ db := channelMemberWithSchemeRoles{
+ Roles: "",
+ SchemeUser: sql.NullBool{Valid: true, Bool: true},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: true},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: true, String: "tscheme_channeluser"},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: true, String: "tscheme_channeladmin"},
+ ChannelSchemeDefaultUserRole: sql.NullString{Valid: true, String: "cscheme_user"},
+ ChannelSchemeDefaultAdminRole: sql.NullString{Valid: true, String: "cscheme_admin"},
+ }
+
+ cm := db.ToModel()
+
+ assert.Equal(t, "cscheme_user cscheme_admin", cm.Roles)
+ assert.Equal(t, true, cm.SchemeUser)
+ assert.Equal(t, true, cm.SchemeAdmin)
+ assert.Equal(t, "", cm.ExplicitRoles)
+ })
+
+ t.Run("Migrated_TeamAndChannelScheme_CustomRole", func(t *testing.T) {
+ db := channelMemberWithSchemeRoles{
+ Roles: "custom_role",
+ SchemeUser: sql.NullBool{Valid: true, Bool: false},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: false},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: true, String: "tscheme_channeluser"},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: true, String: "tscheme_channeladmin"},
+ ChannelSchemeDefaultUserRole: sql.NullString{Valid: true, String: "cscheme_user"},
+ ChannelSchemeDefaultAdminRole: sql.NullString{Valid: true, String: "cscheme_admin"},
+ }
+
+ cm := db.ToModel()
+
+ assert.Equal(t, "custom_role", cm.Roles)
+ assert.Equal(t, false, cm.SchemeUser)
+ assert.Equal(t, false, cm.SchemeAdmin)
+ assert.Equal(t, "custom_role", cm.ExplicitRoles)
+ })
+
+ t.Run("Migrated_TeamAndChannelScheme_UserAndCustomRole", func(t *testing.T) {
+ db := channelMemberWithSchemeRoles{
+ Roles: "custom_role",
+ SchemeUser: sql.NullBool{Valid: true, Bool: true},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: false},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: true, String: "tscheme_channeluser"},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: true, String: "tscheme_channeladmin"},
+ ChannelSchemeDefaultUserRole: sql.NullString{Valid: true, String: "cscheme_user"},
+ ChannelSchemeDefaultAdminRole: sql.NullString{Valid: true, String: "cscheme_admin"},
+ }
+
+ cm := db.ToModel()
+
+ assert.Equal(t, "custom_role cscheme_user", cm.Roles)
+ assert.Equal(t, true, cm.SchemeUser)
+ assert.Equal(t, false, cm.SchemeAdmin)
+ assert.Equal(t, "custom_role", cm.ExplicitRoles)
+ })
+
+ t.Run("Migrated_TeamAndChannelScheme_AdminAndCustomRole", func(t *testing.T) {
+ db := channelMemberWithSchemeRoles{
+ Roles: "custom_role",
+ SchemeUser: sql.NullBool{Valid: true, Bool: true},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: true},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: true, String: "tscheme_channeluser"},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: true, String: "tscheme_channeladmin"},
+ ChannelSchemeDefaultUserRole: sql.NullString{Valid: true, String: "cscheme_user"},
+ ChannelSchemeDefaultAdminRole: sql.NullString{Valid: true, String: "cscheme_admin"},
+ }
+
+ cm := db.ToModel()
+
+ assert.Equal(t, "custom_role cscheme_user cscheme_admin", cm.Roles)
+ assert.Equal(t, true, cm.SchemeUser)
+ assert.Equal(t, true, cm.SchemeAdmin)
+ assert.Equal(t, "custom_role", cm.ExplicitRoles)
+ })
+
+ t.Run("Migrated_TeamAndChannelScheme_NoRoles", func(t *testing.T) {
+ db := channelMemberWithSchemeRoles{
+ Roles: "",
+ SchemeUser: sql.NullBool{Valid: true, Bool: false},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: false},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: true, String: "tscheme_channeluser"},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: true, String: "tscheme_channeladmin"},
+ ChannelSchemeDefaultUserRole: sql.NullString{Valid: true, String: "cscheme_user"},
+ ChannelSchemeDefaultAdminRole: sql.NullString{Valid: true, String: "cscheme_admin"},
+ }
+
+ cm := db.ToModel()
+
+ assert.Equal(t, "", cm.Roles)
+ assert.Equal(t, false, cm.SchemeUser)
+ assert.Equal(t, false, cm.SchemeAdmin)
+ assert.Equal(t, "", cm.ExplicitRoles)
+ })
+}
+
+func testAllChannelMemberProcess(t *testing.T) {
+ t.Run("Unmigrated_User", func(t *testing.T) {
+ db := allChannelMember{
+ Roles: "channel_user",
+ SchemeUser: sql.NullBool{Valid: false, Bool: false},
+ SchemeAdmin: sql.NullBool{Valid: false, Bool: false},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: false},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultUserRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ }
+
+ _, roles := db.Process()
+
+ assert.Equal(t, "channel_user", roles)
+ })
+
+ t.Run("Unmigrated_Admin", func(t *testing.T) {
+ db := allChannelMember{
+ Roles: "channel_user channel_admin",
+ SchemeUser: sql.NullBool{Valid: false, Bool: false},
+ SchemeAdmin: sql.NullBool{Valid: false, Bool: false},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: false},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultUserRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ }
+
+ _, roles := db.Process()
+
+ assert.Equal(t, "channel_user channel_admin", roles)
+ })
+
+ t.Run("Unmigrated_Neither", func(t *testing.T) {
+ db := allChannelMember{
+ Roles: "",
+ SchemeUser: sql.NullBool{Valid: false, Bool: false},
+ SchemeAdmin: sql.NullBool{Valid: false, Bool: false},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: false},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultUserRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ }
+
+ _, roles := db.Process()
+
+ assert.Equal(t, "", roles)
+ })
+
+ t.Run("Unmigrated_Custom", func(t *testing.T) {
+ db := allChannelMember{
+ Roles: "custom",
+ SchemeUser: sql.NullBool{Valid: false, Bool: false},
+ SchemeAdmin: sql.NullBool{Valid: false, Bool: false},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: false},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultUserRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ }
+
+ _, roles := db.Process()
+
+ assert.Equal(t, "custom", roles)
+ })
+
+ t.Run("MigratedNoScheme_User", func(t *testing.T) {
+ db := allChannelMember{
+ Roles: "",
+ SchemeUser: sql.NullBool{Valid: true, Bool: true},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: false},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: false},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultUserRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ }
+
+ _, roles := db.Process()
+
+ assert.Equal(t, "channel_user", roles)
+ })
+
+ t.Run("MigratedNoScheme_Admin", func(t *testing.T) {
+ db := allChannelMember{
+ Roles: "",
+ SchemeUser: sql.NullBool{Valid: true, Bool: true},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: true},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: false},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultUserRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ }
+
+ _, roles := db.Process()
+
+ assert.Equal(t, "channel_user channel_admin", roles)
+ })
+
+ t.Run("MigratedNoScheme_Neither", func(t *testing.T) {
+ db := allChannelMember{
+ Roles: "",
+ SchemeUser: sql.NullBool{Valid: true, Bool: false},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: false},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: false},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultUserRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ }
+
+ _, roles := db.Process()
+
+ assert.Equal(t, "", roles)
+ })
+
+ t.Run("MigratedChannelScheme_User", func(t *testing.T) {
+ db := allChannelMember{
+ Roles: "",
+ SchemeUser: sql.NullBool{Valid: true, Bool: true},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: false},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: false},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultUserRole: sql.NullString{Valid: true, String: "cscheme_user"},
+ ChannelSchemeDefaultAdminRole: sql.NullString{Valid: true, String: "cscheme_admin"},
+ }
+
+ _, roles := db.Process()
+
+ assert.Equal(t, "cscheme_user", roles)
+ })
+
+ t.Run("MigratedChannelScheme_Admin", func(t *testing.T) {
+ db := allChannelMember{
+ Roles: "",
+ SchemeUser: sql.NullBool{Valid: true, Bool: true},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: true},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: false},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultUserRole: sql.NullString{Valid: true, String: "cscheme_user"},
+ ChannelSchemeDefaultAdminRole: sql.NullString{Valid: true, String: "cscheme_admin"},
+ }
+
+ _, roles := db.Process()
+
+ assert.Equal(t, "cscheme_user cscheme_admin", roles)
+ })
+
+ t.Run("MigratedChannelScheme_Neither", func(t *testing.T) {
+ db := allChannelMember{
+ Roles: "",
+ SchemeUser: sql.NullBool{Valid: true, Bool: false},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: false},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: false},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultUserRole: sql.NullString{Valid: true, String: "cscheme_user"},
+ ChannelSchemeDefaultAdminRole: sql.NullString{Valid: true, String: "cscheme_admin"},
+ }
+
+ _, roles := db.Process()
+
+ assert.Equal(t, "", roles)
+ })
+
+ t.Run("MigratedTeamScheme_User", func(t *testing.T) {
+ db := allChannelMember{
+ Roles: "",
+ SchemeUser: sql.NullBool{Valid: true, Bool: true},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: false},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: true, String: "tscheme_channeluser"},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: true, String: "tscheme_channeladmin"},
+ ChannelSchemeDefaultUserRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ }
+
+ _, roles := db.Process()
+
+ assert.Equal(t, "tscheme_channeluser", roles)
+ })
+
+ t.Run("MigratedTeamScheme_Admin", func(t *testing.T) {
+ db := allChannelMember{
+ Roles: "",
+ SchemeUser: sql.NullBool{Valid: true, Bool: true},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: true},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: true, String: "tscheme_channeluser"},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: true, String: "tscheme_channeladmin"},
+ ChannelSchemeDefaultUserRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ }
+
+ _, roles := db.Process()
+
+ assert.Equal(t, "tscheme_channeluser tscheme_channeladmin", roles)
+ })
+
+ t.Run("MigratedTeamScheme_Neither", func(t *testing.T) {
+ db := allChannelMember{
+ Roles: "",
+ SchemeUser: sql.NullBool{Valid: true, Bool: false},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: false},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: true, String: "tscheme_channeluser"},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: true, String: "tscheme_channeladmin"},
+ ChannelSchemeDefaultUserRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ }
+
+ _, roles := db.Process()
+
+ assert.Equal(t, "", roles)
+ })
+
+ t.Run("MigratedTeamAndChannelScheme_User", func(t *testing.T) {
+ db := allChannelMember{
+ Roles: "",
+ SchemeUser: sql.NullBool{Valid: true, Bool: true},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: false},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: true, String: "tscheme_channeluser"},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: true, String: "tscheme_channeladmin"},
+ ChannelSchemeDefaultUserRole: sql.NullString{Valid: true, String: "cscheme_user"},
+ ChannelSchemeDefaultAdminRole: sql.NullString{Valid: true, String: "cscheme_admin"},
+ }
+
+ _, roles := db.Process()
+
+ assert.Equal(t, "cscheme_user", roles)
+ })
+
+ t.Run("MigratedTeamAndChannelScheme_Admin", func(t *testing.T) {
+ db := allChannelMember{
+ Roles: "",
+ SchemeUser: sql.NullBool{Valid: true, Bool: true},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: true},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: true, String: "tscheme_channeluser"},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: true, String: "tscheme_channeladmin"},
+ ChannelSchemeDefaultUserRole: sql.NullString{Valid: true, String: "cscheme_user"},
+ ChannelSchemeDefaultAdminRole: sql.NullString{Valid: true, String: "cscheme_admin"},
+ }
+
+ _, roles := db.Process()
+
+ assert.Equal(t, "cscheme_user cscheme_admin", roles)
+ })
+
+ t.Run("MigratedTeamAndChannelScheme_Neither", func(t *testing.T) {
+ db := allChannelMember{
+ Roles: "",
+ SchemeUser: sql.NullBool{Valid: true, Bool: false},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: false},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: true, String: "tscheme_channeluser"},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: true, String: "tscheme_channeladmin"},
+ ChannelSchemeDefaultUserRole: sql.NullString{Valid: true, String: "cscheme_user"},
+ ChannelSchemeDefaultAdminRole: sql.NullString{Valid: true, String: "cscheme_admin"},
+ }
+
+ _, roles := db.Process()
+
+ assert.Equal(t, "", roles)
+ })
+
+ t.Run("DeduplicationCheck", func(t *testing.T) {
+ db := allChannelMember{
+ Roles: "channel_user",
+ SchemeUser: sql.NullBool{Valid: true, Bool: true},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: false},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: false},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultUserRole: sql.NullString{Valid: false},
+ ChannelSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ }
+
+ _, roles := db.Process()
+
+ assert.Equal(t, "channel_user", roles)
+ })
+}
diff --git a/store/sqlstore/role_supplier.go b/store/sqlstore/role_supplier.go
index ddbdaca52..19ef602eb 100644
--- a/store/sqlstore/role_supplier.go
+++ b/store/sqlstore/role_supplier.go
@@ -10,6 +10,8 @@ import (
"net/http"
"strings"
+ "github.com/mattermost/gorp"
+
"github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/store"
)
@@ -24,6 +26,7 @@ type Role struct {
DeleteAt int64
Permissions string
SchemeManaged bool
+ BuiltIn bool
}
func NewRoleFromModel(role *model.Role) *Role {
@@ -47,6 +50,7 @@ func NewRoleFromModel(role *model.Role) *Role {
DeleteAt: role.DeleteAt,
Permissions: permissions,
SchemeManaged: role.SchemeManaged,
+ BuiltIn: role.BuiltIn,
}
}
@@ -61,6 +65,7 @@ func (role Role) ToModel() *model.Role {
DeleteAt: role.DeleteAt,
Permissions: strings.Fields(role.Permissions),
SchemeManaged: role.SchemeManaged,
+ BuiltIn: role.BuiltIn,
}
}
@@ -84,21 +89,52 @@ func (s *SqlSupplier) RoleSave(ctx context.Context, role *model.Role, hints ...s
return result
}
- dbRole := NewRoleFromModel(role)
- if len(dbRole.Id) == 0 {
- dbRole.Id = model.NewId()
- dbRole.CreateAt = model.GetMillis()
- dbRole.UpdateAt = dbRole.CreateAt
- if err := s.GetMaster().Insert(dbRole); err != nil {
- result.Err = model.NewAppError("SqlRoleStore.Save", "store.sql_role.save.insert.app_error", nil, err.Error(), http.StatusInternalServerError)
+ if len(role.Id) == 0 {
+ if transaction, err := s.GetMaster().Begin(); err != nil {
+ result.Err = model.NewAppError("SqlRoleStore.RoleSave", "store.sql_role.save.open_transaction.app_error", nil, err.Error(), http.StatusInternalServerError)
+ return result
+ } else {
+ result = s.createRole(ctx, role, transaction, hints...)
+
+ if result.Err != nil {
+ transaction.Rollback()
+ } else if err := transaction.Commit(); err != nil {
+ result.Err = model.NewAppError("SqlRoleStore.RoleSave", "store.sql_role.save_role.commit_transaction.app_error", nil, err.Error(), http.StatusInternalServerError)
+ }
}
} else {
+ dbRole := NewRoleFromModel(role)
+
dbRole.UpdateAt = model.GetMillis()
if rowsChanged, err := s.GetMaster().Update(dbRole); err != nil {
result.Err = model.NewAppError("SqlRoleStore.Save", "store.sql_role.save.update.app_error", nil, err.Error(), http.StatusInternalServerError)
} else if rowsChanged != 1 {
result.Err = model.NewAppError("SqlRoleStore.Save", "store.sql_role.save.update.app_error", nil, "no record to update", http.StatusInternalServerError)
}
+
+ result.Data = dbRole.ToModel()
+ }
+
+ return result
+}
+
+func (s *SqlSupplier) createRole(ctx context.Context, role *model.Role, transaction *gorp.Transaction, hints ...store.LayeredStoreHint) *store.LayeredStoreSupplierResult {
+ result := store.NewSupplierResult()
+
+ // Check the role is valid before proceeding.
+ if !role.IsValidWithoutId() {
+ result.Err = model.NewAppError("SqlRoleStore.Save", "store.sql_role.save.invalid_role.app_error", nil, "", http.StatusBadRequest)
+ return result
+ }
+
+ dbRole := NewRoleFromModel(role)
+
+ dbRole.Id = model.NewId()
+ dbRole.CreateAt = model.GetMillis()
+ dbRole.UpdateAt = dbRole.CreateAt
+
+ if err := transaction.Insert(dbRole); err != nil {
+ result.Err = model.NewAppError("SqlRoleStore.Save", "store.sql_role.save.insert.app_error", nil, err.Error(), http.StatusInternalServerError)
}
result.Data = dbRole.ToModel()
@@ -175,6 +211,36 @@ func (s *SqlSupplier) RoleGetByNames(ctx context.Context, names []string, hints
return result
}
+func (s *SqlSupplier) RoleDelete(ctx context.Context, roleId string, hints ...store.LayeredStoreHint) *store.LayeredStoreSupplierResult {
+ result := store.NewSupplierResult()
+
+ // Get the role.
+ var role *Role
+ if err := s.GetReplica().SelectOne(&role, "SELECT * from Roles WHERE Id = :Id", map[string]interface{}{"Id": roleId}); err != nil {
+ if err == sql.ErrNoRows {
+ result.Err = model.NewAppError("SqlRoleStore.Delete", "store.sql_role.get.app_error", nil, "Id="+roleId+", "+err.Error(), http.StatusNotFound)
+ } else {
+ result.Err = model.NewAppError("SqlRoleStore.Delete", "store.sql_role.get.app_error", nil, err.Error(), http.StatusInternalServerError)
+ }
+
+ return result
+ }
+
+ time := model.GetMillis()
+ role.DeleteAt = time
+ role.UpdateAt = time
+
+ if rowsChanged, err := s.GetMaster().Update(role); err != nil {
+ result.Err = model.NewAppError("SqlRoleStore.Delete", "store.sql_role.delete.update.app_error", nil, err.Error(), http.StatusInternalServerError)
+ } else if rowsChanged != 1 {
+ result.Err = model.NewAppError("SqlRoleStore.Delete", "store.sql_role.delete.update.app_error", nil, "no record to update", http.StatusInternalServerError)
+ } else {
+ result.Data = role.ToModel()
+ }
+
+ return result
+}
+
func (s *SqlSupplier) RolePermanentDeleteAll(ctx context.Context, hints ...store.LayeredStoreHint) *store.LayeredStoreSupplierResult {
result := store.NewSupplierResult()
diff --git a/store/sqlstore/scheme_store_test.go b/store/sqlstore/scheme_store_test.go
new file mode 100644
index 000000000..b07495715
--- /dev/null
+++ b/store/sqlstore/scheme_store_test.go
@@ -0,0 +1,14 @@
+// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package sqlstore
+
+import (
+ "testing"
+
+ "github.com/mattermost/mattermost-server/store/storetest"
+)
+
+func TestSchemeStore(t *testing.T) {
+ StoreTest(t, storetest.TestSchemeStore)
+}
diff --git a/store/sqlstore/scheme_supplier.go b/store/sqlstore/scheme_supplier.go
new file mode 100644
index 000000000..f272040a6
--- /dev/null
+++ b/store/sqlstore/scheme_supplier.go
@@ -0,0 +1,298 @@
+// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package sqlstore
+
+import (
+ "context"
+ "database/sql"
+ "fmt"
+ "net/http"
+ "strings"
+
+ "github.com/mattermost/gorp"
+
+ "github.com/mattermost/mattermost-server/model"
+ "github.com/mattermost/mattermost-server/store"
+)
+
+func initSqlSupplierSchemes(sqlStore SqlStore) {
+ for _, db := range sqlStore.GetAllConns() {
+ table := db.AddTableWithName(model.Scheme{}, "Schemes").SetKeys(false, "Id")
+ table.ColMap("Id").SetMaxSize(26)
+ table.ColMap("Name").SetMaxSize(model.SCHEME_NAME_MAX_LENGTH).SetUnique(true)
+ table.ColMap("DisplayName").SetMaxSize(model.SCHEME_DISPLAY_NAME_MAX_LENGTH)
+ table.ColMap("Description").SetMaxSize(model.SCHEME_DESCRIPTION_MAX_LENGTH)
+ table.ColMap("Scope").SetMaxSize(32)
+ table.ColMap("DefaultTeamAdminRole").SetMaxSize(64)
+ table.ColMap("DefaultTeamUserRole").SetMaxSize(64)
+ table.ColMap("DefaultChannelAdminRole").SetMaxSize(64)
+ table.ColMap("DefaultChannelUserRole").SetMaxSize(64)
+ }
+}
+
+func (s *SqlSupplier) SchemeSave(ctx context.Context, scheme *model.Scheme, hints ...store.LayeredStoreHint) *store.LayeredStoreSupplierResult {
+ result := store.NewSupplierResult()
+
+ if len(scheme.Id) == 0 {
+ if transaction, err := s.GetMaster().Begin(); err != nil {
+ result.Err = model.NewAppError("SqlSchemeStore.SaveScheme", "store.sql_scheme.save.open_transaction.app_error", nil, err.Error(), http.StatusInternalServerError)
+ } else {
+ result = s.createScheme(ctx, scheme, transaction, hints...)
+
+ if result.Err != nil {
+ transaction.Rollback()
+ } else if err := transaction.Commit(); err != nil {
+ result.Err = model.NewAppError("SqlSchemeStore.SchemeSave", "store.sql_scheme.save_scheme.commit_transaction.app_error", nil, err.Error(), http.StatusInternalServerError)
+ }
+ }
+ } else {
+ if !scheme.IsValid() {
+ result.Err = model.NewAppError("SqlSchemeStore.Save", "store.sql_scheme.save.invalid_scheme.app_error", nil, "", http.StatusBadRequest)
+ return result
+ }
+
+ scheme.UpdateAt = model.GetMillis()
+
+ if rowsChanged, err := s.GetMaster().Update(scheme); err != nil {
+ result.Err = model.NewAppError("SqlSchemeStore.Save", "store.sql_scheme.save.update.app_error", nil, err.Error(), http.StatusInternalServerError)
+ } else if rowsChanged != 1 {
+ result.Err = model.NewAppError("SqlSchemeStore.Save", "store.sql_scheme.save.update.app_error", nil, "no record to update", http.StatusInternalServerError)
+ }
+
+ result.Data = scheme
+ }
+
+ return result
+}
+
+func (s *SqlSupplier) createScheme(ctx context.Context, scheme *model.Scheme, transaction *gorp.Transaction, hints ...store.LayeredStoreHint) *store.LayeredStoreSupplierResult {
+ result := store.NewSupplierResult()
+
+ // Fetch the default system scheme roles to populate default permissions.
+ defaultRoleNames := []string{model.TEAM_ADMIN_ROLE_ID, model.TEAM_USER_ROLE_ID, model.CHANNEL_ADMIN_ROLE_ID, model.CHANNEL_USER_ROLE_ID}
+ defaultRoles := make(map[string]*model.Role)
+ if rolesResult := s.RoleGetByNames(ctx, defaultRoleNames); rolesResult.Err != nil {
+ result.Err = rolesResult.Err
+ return result
+ } else {
+ for _, role := range rolesResult.Data.([]*model.Role) {
+ switch role.Name {
+ case model.TEAM_ADMIN_ROLE_ID:
+ defaultRoles[model.TEAM_ADMIN_ROLE_ID] = role
+ case model.TEAM_USER_ROLE_ID:
+ defaultRoles[model.TEAM_USER_ROLE_ID] = role
+ case model.CHANNEL_ADMIN_ROLE_ID:
+ defaultRoles[model.CHANNEL_ADMIN_ROLE_ID] = role
+ case model.CHANNEL_USER_ROLE_ID:
+ defaultRoles[model.CHANNEL_USER_ROLE_ID] = role
+ }
+ }
+
+ if len(defaultRoles) != 4 {
+ result.Err = model.NewAppError("SqlSchemeStore.SaveScheme", "store.sql_scheme.save.retrieve_default_scheme_roles.app_error", nil, "", http.StatusInternalServerError)
+ return result
+ }
+ }
+
+ // Create the appropriate default roles for the scheme.
+ if scheme.Scope == model.SCHEME_SCOPE_TEAM {
+ // Team Admin Role
+ teamAdminRole := &model.Role{
+ Name: model.NewId(),
+ DisplayName: fmt.Sprintf("Team Admin Role for Scheme %s", scheme.Name),
+ Permissions: defaultRoles[model.TEAM_ADMIN_ROLE_ID].Permissions,
+ SchemeManaged: true,
+ }
+
+ if saveRoleResult := s.createRole(ctx, teamAdminRole, transaction); saveRoleResult.Err != nil {
+ result.Err = saveRoleResult.Err
+ return result
+ } else {
+ scheme.DefaultTeamAdminRole = saveRoleResult.Data.(*model.Role).Id
+ }
+
+ // Team User Role
+ teamUserRole := &model.Role{
+ Name: model.NewId(),
+ DisplayName: fmt.Sprintf("Team User Role for Scheme %s", scheme.Name),
+ Permissions: defaultRoles[model.TEAM_USER_ROLE_ID].Permissions,
+ SchemeManaged: true,
+ }
+
+ if saveRoleResult := s.createRole(ctx, teamUserRole, transaction); saveRoleResult.Err != nil {
+ result.Err = saveRoleResult.Err
+ return result
+ } else {
+ scheme.DefaultTeamUserRole = saveRoleResult.Data.(*model.Role).Id
+ }
+ }
+ if scheme.Scope == model.SCHEME_SCOPE_TEAM || scheme.Scope == model.SCHEME_SCOPE_CHANNEL {
+ // Channel Admin Role
+ channelAdminRole := &model.Role{
+ Name: model.NewId(),
+ DisplayName: fmt.Sprintf("Channel Admin Role for Scheme %s", scheme.Name),
+ Permissions: defaultRoles[model.CHANNEL_ADMIN_ROLE_ID].Permissions,
+ SchemeManaged: true,
+ }
+
+ if saveRoleResult := s.createRole(ctx, channelAdminRole, transaction); saveRoleResult.Err != nil {
+ result.Err = saveRoleResult.Err
+ return result
+ } else {
+ scheme.DefaultChannelAdminRole = saveRoleResult.Data.(*model.Role).Id
+ }
+
+ // Channel User Role
+ channelUserRole := &model.Role{
+ Name: model.NewId(),
+ DisplayName: fmt.Sprintf("Channel User Role for Scheme %s", scheme.Name),
+ Permissions: defaultRoles[model.CHANNEL_USER_ROLE_ID].Permissions,
+ SchemeManaged: true,
+ }
+
+ if saveRoleResult := s.createRole(ctx, channelUserRole, transaction); saveRoleResult.Err != nil {
+ result.Err = saveRoleResult.Err
+ return result
+ } else {
+ scheme.DefaultChannelUserRole = saveRoleResult.Data.(*model.Role).Id
+ }
+ }
+
+ scheme.Id = model.NewId()
+ if len(scheme.Name) == 0 {
+ scheme.Name = model.NewId()
+ }
+ scheme.CreateAt = model.GetMillis()
+ scheme.UpdateAt = scheme.CreateAt
+
+ // Validate the scheme
+ if !scheme.IsValidForCreate() {
+ result.Err = model.NewAppError("SqlSchemeStore.Save", "store.sql_scheme.save.invalid_scheme.app_error", nil, "", http.StatusBadRequest)
+ return result
+ }
+
+ if err := transaction.Insert(scheme); err != nil {
+ result.Err = model.NewAppError("SqlSchemeStore.Save", "store.sql_scheme.save.insert.app_error", nil, err.Error(), http.StatusInternalServerError)
+ }
+
+ result.Data = scheme
+
+ return result
+}
+
+func (s *SqlSupplier) SchemeGet(ctx context.Context, schemeId string, hints ...store.LayeredStoreHint) *store.LayeredStoreSupplierResult {
+ result := store.NewSupplierResult()
+
+ var scheme model.Scheme
+
+ if err := s.GetReplica().SelectOne(&scheme, "SELECT * from Schemes WHERE Id = :Id", map[string]interface{}{"Id": schemeId}); err != nil {
+ if err == sql.ErrNoRows {
+ result.Err = model.NewAppError("SqlSchemeStore.Get", "store.sql_scheme.get.app_error", nil, "Id="+schemeId+", "+err.Error(), http.StatusNotFound)
+ } else {
+ result.Err = model.NewAppError("SqlSchemeStore.Get", "store.sql_scheme.get.app_error", nil, err.Error(), http.StatusInternalServerError)
+ }
+ }
+
+ result.Data = &scheme
+
+ return result
+}
+
+func (s *SqlSupplier) SchemeDelete(ctx context.Context, schemeId string, hints ...store.LayeredStoreHint) *store.LayeredStoreSupplierResult {
+ result := store.NewSupplierResult()
+
+ // Get the scheme
+ var scheme model.Scheme
+ if err := s.GetReplica().SelectOne(&scheme, "SELECT * from Schemes WHERE Id = :Id", map[string]interface{}{"Id": schemeId}); err != nil {
+ if err == sql.ErrNoRows {
+ result.Err = model.NewAppError("SqlSchemeStore.Delete", "store.sql_scheme.get.app_error", nil, "Id="+schemeId+", "+err.Error(), http.StatusNotFound)
+ } else {
+ result.Err = model.NewAppError("SqlSchemeStore.Delete", "store.sql_scheme.get.app_error", nil, "Id="+schemeId+", "+err.Error(), http.StatusInternalServerError)
+ }
+
+ return result
+ }
+
+ // Update any teams or channels using this scheme to the default scheme.
+ if scheme.Scope == model.SCHEME_SCOPE_TEAM {
+ if _, err := s.GetReplica().Exec("UPDATE Teams SET SchemeId = '' WHERE SchemeId = :SchemeId", map[string]interface{}{"SchemeId": schemeId}); err != nil {
+ result.Err = model.NewAppError("SqlSchemeStore.Delete", "store.sql_scheme.reset_teams.app_error", nil, "Id="+schemeId+", "+err.Error(), http.StatusInternalServerError)
+ return result
+ }
+ } else if scheme.Scope == model.SCHEME_SCOPE_CHANNEL {
+ if _, err := s.GetReplica().Exec("UPDATE Channels SET SchemeId = '' WHERE SchemeId = :SchemeId", map[string]interface{}{"SchemeId": schemeId}); err != nil {
+ result.Err = model.NewAppError("SqlSchemeStore.Delete", "store.sql_scheme.reset_channels.app_error", nil, "Id="+schemeId+", "+err.Error(), http.StatusInternalServerError)
+ return result
+ }
+
+ // Blow away the channel caches.
+ s.Channel().ClearCaches()
+ }
+
+ // Delete the roles belonging to the scheme.
+ roleIds := []string{scheme.DefaultChannelUserRole, scheme.DefaultChannelAdminRole}
+ if scheme.Scope == model.SCHEME_SCOPE_TEAM {
+ roleIds = append(roleIds, scheme.DefaultTeamUserRole, scheme.DefaultTeamAdminRole)
+ }
+
+ var inQueryList []string
+ queryArgs := make(map[string]interface{})
+ for i, roleId := range roleIds {
+ inQueryList = append(inQueryList, fmt.Sprintf(":RoleId%v", i))
+ queryArgs[fmt.Sprintf("RoleId%v", i)] = roleId
+ }
+ inQuery := strings.Join(inQueryList, ", ")
+
+ time := model.GetMillis()
+ queryArgs["UpdateAt"] = time
+ queryArgs["DeleteAt"] = time
+
+ if _, err := s.GetMaster().Exec("UPDATE Roles SET UpdateAt = :UpdateAt, DeleteAt = :DeleteAt WHERE Id IN ("+inQuery+")", queryArgs); err != nil {
+ result.Err = model.NewAppError("SqlSchemeStore.Delete", "store.sql_scheme.delete.role_update.app_error", nil, "Id="+schemeId+", "+err.Error(), http.StatusInternalServerError)
+ return result
+ }
+
+ // Delete the scheme itself.
+ scheme.UpdateAt = time
+ scheme.DeleteAt = time
+
+ if rowsChanged, err := s.GetMaster().Update(&scheme); err != nil {
+ result.Err = model.NewAppError("SqlSchemeStore.Delete", "store.sql_scheme.delete.update.app_error", nil, "Id="+schemeId+", "+err.Error(), http.StatusInternalServerError)
+ } else if rowsChanged != 1 {
+ result.Err = model.NewAppError("SqlSchemeStore.Delete", "store.sql_scheme.delete.update.app_error", nil, "no record to update", http.StatusInternalServerError)
+ } else {
+ result.Data = &scheme
+ }
+
+ return result
+}
+
+func (s *SqlSupplier) SchemeGetAllPage(ctx context.Context, scope string, offset int, limit int, hints ...store.LayeredStoreHint) *store.LayeredStoreSupplierResult {
+ result := store.NewSupplierResult()
+
+ var schemes []*model.Scheme
+
+ scopeClause := ""
+ if len(scope) > 0 {
+ scopeClause = " AND Scope=:Scope "
+ }
+
+ if _, err := s.GetReplica().Select(&schemes, "SELECT * from Schemes WHERE DeleteAt = 0 "+scopeClause+" ORDER BY CreateAt DESC LIMIT :Limit OFFSET :Offset", map[string]interface{}{"Limit": limit, "Offset": offset, "Scope": scope}); err != nil {
+ result.Err = model.NewAppError("SqlSchemeStore.Get", "store.sql_scheme.get.app_error", nil, err.Error(), http.StatusInternalServerError)
+ }
+
+ result.Data = schemes
+
+ return result
+}
+
+func (s *SqlSupplier) SchemePermanentDeleteAll(ctx context.Context, hints ...store.LayeredStoreHint) *store.LayeredStoreSupplierResult {
+ result := store.NewSupplierResult()
+
+ if _, err := s.GetMaster().Exec("DELETE from Schemes"); err != nil {
+ result.Err = model.NewAppError("SqlSchemeStore.PermanentDeleteAll", "store.sql_scheme.permanent_delete_all.app_error", nil, err.Error(), http.StatusInternalServerError)
+ }
+
+ return result
+}
diff --git a/store/sqlstore/store.go b/store/sqlstore/store.go
index 1c623f0b1..fc7b3be18 100644
--- a/store/sqlstore/store.go
+++ b/store/sqlstore/store.go
@@ -52,6 +52,7 @@ type SqlStore interface {
DoesTableExist(tablename string) bool
DoesColumnExist(tableName string, columName string) bool
CreateColumnIfNotExists(tableName string, columnName string, mySqlColType string, postgresColType string, defaultValue string) bool
+ CreateColumnIfNotExistsNoDefault(tableName string, columnName string, mySqlColType string, postgresColType string) bool
RemoveColumnIfExists(tableName string, columnName string) bool
RemoveTableIfExists(tableName string) bool
RenameColumnIfExists(tableName string, oldColumnName string, newColumnName string, colType string) bool
@@ -88,4 +89,5 @@ type SqlStore interface {
Plugin() store.PluginStore
UserAccessToken() store.UserAccessTokenStore
Role() store.RoleStore
+ Scheme() store.SchemeStore
}
diff --git a/store/sqlstore/supplier.go b/store/sqlstore/supplier.go
index 0b02d9d47..02a3cef7f 100644
--- a/store/sqlstore/supplier.go
+++ b/store/sqlstore/supplier.go
@@ -91,6 +91,7 @@ type SqlSupplierOldStores struct {
plugin store.PluginStore
channelMemberHistory store.ChannelMemberHistoryStore
role store.RoleStore
+ scheme store.SchemeStore
}
type SqlSupplier struct {
@@ -141,6 +142,7 @@ func NewSqlSupplier(settings model.SqlSettings, metrics einterfaces.MetricsInter
initSqlSupplierReactions(supplier)
initSqlSupplierRoles(supplier)
+ initSqlSupplierSchemes(supplier)
err := supplier.GetMaster().CreateTablesIfNotExists()
if err != nil {
@@ -493,6 +495,40 @@ func (ss *SqlSupplier) CreateColumnIfNotExists(tableName string, columnName stri
}
}
+func (ss *SqlSupplier) CreateColumnIfNotExistsNoDefault(tableName string, columnName string, mySqlColType string, postgresColType string) bool {
+
+ if ss.DoesColumnExist(tableName, columnName) {
+ return false
+ }
+
+ if ss.DriverName() == model.DATABASE_DRIVER_POSTGRES {
+ _, err := ss.GetMaster().ExecNoTimeout("ALTER TABLE " + tableName + " ADD " + columnName + " " + postgresColType)
+ if err != nil {
+ mlog.Critical(fmt.Sprintf("Failed to create column %v", err))
+ time.Sleep(time.Second)
+ os.Exit(EXIT_CREATE_COLUMN_POSTGRES)
+ }
+
+ return true
+
+ } else if ss.DriverName() == model.DATABASE_DRIVER_MYSQL {
+ _, err := ss.GetMaster().ExecNoTimeout("ALTER TABLE " + tableName + " ADD " + columnName + " " + mySqlColType)
+ if err != nil {
+ mlog.Critical(fmt.Sprintf("Failed to create column %v", err))
+ time.Sleep(time.Second)
+ os.Exit(EXIT_CREATE_COLUMN_MYSQL)
+ }
+
+ return true
+
+ } else {
+ mlog.Critical("Failed to create column because of missing driver")
+ time.Sleep(time.Second)
+ os.Exit(EXIT_CREATE_COLUMN_MISSING)
+ return false
+ }
+}
+
func (ss *SqlSupplier) RemoveColumnIfExists(tableName string, columnName string) bool {
if !ss.DoesColumnExist(tableName, columnName) {
@@ -865,6 +901,10 @@ func (ss *SqlSupplier) Role() store.RoleStore {
return ss.oldStores.role
}
+func (ss *SqlSupplier) Scheme() store.SchemeStore {
+ return ss.oldStores.scheme
+}
+
func (ss *SqlSupplier) DropAllTables() {
ss.master.TruncateTables()
}
diff --git a/store/sqlstore/team_store.go b/store/sqlstore/team_store.go
index ad27675ce..f8d76bba1 100644
--- a/store/sqlstore/team_store.go
+++ b/store/sqlstore/team_store.go
@@ -7,7 +7,9 @@ import (
"database/sql"
"net/http"
"strconv"
+ "strings"
+ "github.com/mattermost/gorp"
"github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/store"
)
@@ -20,6 +22,116 @@ type SqlTeamStore struct {
SqlStore
}
+type teamMember struct {
+ TeamId string
+ UserId string
+ Roles string
+ DeleteAt int64
+ SchemeUser sql.NullBool
+ SchemeAdmin sql.NullBool
+}
+
+func NewTeamMemberFromModel(tm *model.TeamMember) *teamMember {
+ return &teamMember{
+ TeamId: tm.TeamId,
+ UserId: tm.UserId,
+ Roles: tm.ExplicitRoles,
+ DeleteAt: tm.DeleteAt,
+ SchemeUser: sql.NullBool{Valid: true, Bool: tm.SchemeUser},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: tm.SchemeAdmin},
+ }
+}
+
+type teamMemberWithSchemeRoles struct {
+ TeamId string
+ UserId string
+ Roles string
+ DeleteAt int64
+ SchemeUser sql.NullBool
+ SchemeAdmin sql.NullBool
+ TeamSchemeDefaultUserRole sql.NullString
+ TeamSchemeDefaultAdminRole sql.NullString
+}
+
+type teamMemberWithSchemeRolesList []teamMemberWithSchemeRoles
+
+func (db teamMemberWithSchemeRoles) ToModel() *model.TeamMember {
+ var roles []string
+ var explicitRoles []string
+
+ // Identify any scheme derived roles that are in "Roles" field due to not yet being migrated, and exclude
+ // them from ExplicitRoles field.
+ schemeUser := db.SchemeUser.Valid && db.SchemeUser.Bool
+ schemeAdmin := db.SchemeAdmin.Valid && db.SchemeAdmin.Bool
+ for _, role := range strings.Fields(db.Roles) {
+ isImplicit := false
+ if role == model.TEAM_USER_ROLE_ID {
+ // We have an implicit role via the system scheme. Override the "schemeUser" field to true.
+ schemeUser = true
+ isImplicit = true
+ } else if role == model.TEAM_ADMIN_ROLE_ID {
+ // We have an implicit role via the system scheme.
+ schemeAdmin = true
+ isImplicit = true
+ }
+
+ if !isImplicit {
+ explicitRoles = append(explicitRoles, role)
+ }
+ roles = append(roles, role)
+ }
+
+ // Add any scheme derived roles that are not in the Roles field due to being Implicit from the Scheme, and add
+ // them to the Roles field for backwards compatibility reasons.
+ var schemeImpliedRoles []string
+ if db.SchemeUser.Valid && db.SchemeUser.Bool {
+ if db.TeamSchemeDefaultUserRole.Valid && db.TeamSchemeDefaultUserRole.String != "" {
+ schemeImpliedRoles = append(schemeImpliedRoles, db.TeamSchemeDefaultUserRole.String)
+ } else {
+ schemeImpliedRoles = append(schemeImpliedRoles, model.TEAM_USER_ROLE_ID)
+ }
+ }
+ if db.SchemeAdmin.Valid && db.SchemeAdmin.Bool {
+ if db.TeamSchemeDefaultAdminRole.Valid && db.TeamSchemeDefaultAdminRole.String != "" {
+ schemeImpliedRoles = append(schemeImpliedRoles, db.TeamSchemeDefaultAdminRole.String)
+ } else {
+ schemeImpliedRoles = append(schemeImpliedRoles, model.TEAM_ADMIN_ROLE_ID)
+ }
+ }
+ for _, impliedRole := range schemeImpliedRoles {
+ alreadyThere := false
+ for _, role := range roles {
+ if role == impliedRole {
+ alreadyThere = true
+ }
+ }
+ if !alreadyThere {
+ roles = append(roles, impliedRole)
+ }
+ }
+
+ tm := &model.TeamMember{
+ TeamId: db.TeamId,
+ UserId: db.UserId,
+ Roles: strings.Join(roles, " "),
+ DeleteAt: db.DeleteAt,
+ SchemeUser: schemeUser,
+ SchemeAdmin: schemeAdmin,
+ ExplicitRoles: strings.Join(explicitRoles, " "),
+ }
+ return tm
+}
+
+func (db teamMemberWithSchemeRolesList) ToModel() []*model.TeamMember {
+ tms := make([]*model.TeamMember, 0)
+
+ for _, tm := range db {
+ tms = append(tms, tm.ToModel())
+ }
+
+ return tms
+}
+
func NewSqlTeamStore(sqlStore SqlStore) store.TeamStore {
s := &SqlTeamStore{sqlStore}
@@ -34,7 +146,7 @@ func NewSqlTeamStore(sqlStore SqlStore) store.TeamStore {
table.ColMap("AllowedDomains").SetMaxSize(500)
table.ColMap("InviteId").SetMaxSize(32)
- tablem := db.AddTableWithName(model.TeamMember{}, "TeamMembers").SetKeys(false, "TeamId", "UserId")
+ tablem := db.AddTableWithName(teamMember{}, "TeamMembers").SetKeys(false, "TeamId", "UserId")
tablem.ColMap("TeamId").SetMaxSize(26)
tablem.ColMap("UserId").SetMaxSize(26)
tablem.ColMap("Roles").SetMaxSize(64)
@@ -325,12 +437,27 @@ func (s SqlTeamStore) AnalyticsTeamCount() store.StoreChannel {
})
}
+var TEAM_MEMBERS_WITH_SCHEME_SELECT_QUERY = `
+ SELECT
+ TeamMembers.*,
+ TeamScheme.DefaultChannelUserRole TeamSchemeDefaultUserRole,
+ TeamScheme.DefaultChannelAdminRole TeamSchemeDefaultAdminRole
+ FROM
+ TeamMembers
+ LEFT JOIN
+ Teams ON TeamMembers.TeamId = Teams.Id
+ LEFT JOIN
+ Schemes TeamScheme ON Teams.SchemeId = TeamScheme.Id
+`
+
func (s SqlTeamStore) SaveMember(member *model.TeamMember, maxUsersPerTeam int) store.StoreChannel {
return store.Do(func(result *store.StoreResult) {
if result.Err = member.IsValid(); result.Err != nil {
return
}
+ dbMember := NewTeamMemberFromModel(member)
+
if maxUsersPerTeam >= 0 {
if count, err := s.GetMaster().SelectInt(
`SELECT
@@ -353,14 +480,23 @@ func (s SqlTeamStore) SaveMember(member *model.TeamMember, maxUsersPerTeam int)
}
}
- if err := s.GetMaster().Insert(member); err != nil {
+ if err := s.GetMaster().Insert(dbMember); err != nil {
if IsUniqueConstraintError(err, []string{"TeamId", "teammembers_pkey", "PRIMARY"}) {
result.Err = model.NewAppError("SqlTeamStore.SaveMember", TEAM_MEMBER_EXISTS_ERROR, nil, "team_id="+member.TeamId+", user_id="+member.UserId+", "+err.Error(), http.StatusBadRequest)
} else {
result.Err = model.NewAppError("SqlTeamStore.SaveMember", "store.sql_team.save_member.save.app_error", nil, "team_id="+member.TeamId+", user_id="+member.UserId+", "+err.Error(), http.StatusInternalServerError)
}
} else {
- result.Data = member
+ var retrievedMember teamMemberWithSchemeRoles
+ if err := s.GetMaster().SelectOne(&retrievedMember, TEAM_MEMBERS_WITH_SCHEME_SELECT_QUERY+"WHERE TeamMembers.TeamId = :TeamId AND TeamMembers.UserId = :UserId", map[string]interface{}{"TeamId": dbMember.TeamId, "UserId": dbMember.UserId}); err != nil {
+ if err == sql.ErrNoRows {
+ result.Err = model.NewAppError("SqlTeamStore.SaveMember", "store.sql_team.get_member.missing.app_error", nil, "team_id="+dbMember.TeamId+"user_id="+dbMember.UserId+","+err.Error(), http.StatusNotFound)
+ } else {
+ result.Err = model.NewAppError("SqlTeamStore.SaveMember", "store.sql_team.get_member.app_error", nil, "team_id="+dbMember.TeamId+"user_id="+dbMember.UserId+","+err.Error(), http.StatusInternalServerError)
+ }
+ } else {
+ result.Data = retrievedMember.ToModel()
+ }
}
})
}
@@ -373,18 +509,27 @@ func (s SqlTeamStore) UpdateMember(member *model.TeamMember) store.StoreChannel
return
}
- if _, err := s.GetMaster().Update(member); err != nil {
+ if _, err := s.GetMaster().Update(NewTeamMemberFromModel(member)); err != nil {
result.Err = model.NewAppError("SqlTeamStore.UpdateMember", "store.sql_team.save_member.save.app_error", nil, err.Error(), http.StatusInternalServerError)
} else {
- result.Data = member
+ var retrievedMember teamMemberWithSchemeRoles
+ if err := s.GetMaster().SelectOne(&retrievedMember, TEAM_MEMBERS_WITH_SCHEME_SELECT_QUERY+"WHERE TeamMembers.TeamId = :TeamId AND TeamMembers.UserId = :UserId", map[string]interface{}{"TeamId": member.TeamId, "UserId": member.UserId}); err != nil {
+ if err == sql.ErrNoRows {
+ result.Err = model.NewAppError("SqlTeamStore.UpdateMember", "store.sql_team.get_member.missing.app_error", nil, "team_id="+member.TeamId+"user_id="+member.UserId+","+err.Error(), http.StatusNotFound)
+ } else {
+ result.Err = model.NewAppError("SqlTeamStore.UpdateMember", "store.sql_team.get_member.app_error", nil, "team_id="+member.TeamId+"user_id="+member.UserId+","+err.Error(), http.StatusInternalServerError)
+ }
+ } else {
+ result.Data = retrievedMember.ToModel()
+ }
}
})
}
func (s SqlTeamStore) GetMember(teamId string, userId string) store.StoreChannel {
return store.Do(func(result *store.StoreResult) {
- var member model.TeamMember
- err := s.GetReplica().SelectOne(&member, "SELECT * FROM TeamMembers WHERE TeamId = :TeamId AND UserId = :UserId", map[string]interface{}{"TeamId": teamId, "UserId": userId})
+ var dbMember teamMemberWithSchemeRoles
+ err := s.GetReplica().SelectOne(&dbMember, TEAM_MEMBERS_WITH_SCHEME_SELECT_QUERY+"WHERE TeamMembers.TeamId = :TeamId AND TeamMembers.UserId = :UserId", map[string]interface{}{"TeamId": teamId, "UserId": userId})
if err != nil {
if err == sql.ErrNoRows {
result.Err = model.NewAppError("SqlTeamStore.GetMember", "store.sql_team.get_member.missing.app_error", nil, "teamId="+teamId+" userId="+userId+" "+err.Error(), http.StatusNotFound)
@@ -392,19 +537,19 @@ func (s SqlTeamStore) GetMember(teamId string, userId string) store.StoreChannel
result.Err = model.NewAppError("SqlTeamStore.GetMember", "store.sql_team.get_member.app_error", nil, "teamId="+teamId+" userId="+userId+" "+err.Error(), http.StatusInternalServerError)
}
} else {
- result.Data = &member
+ result.Data = dbMember.ToModel()
}
})
}
func (s SqlTeamStore) GetMembers(teamId string, offset int, limit int) store.StoreChannel {
return store.Do(func(result *store.StoreResult) {
- var members []*model.TeamMember
- _, err := s.GetReplica().Select(&members, "SELECT * FROM TeamMembers WHERE TeamId = :TeamId AND DeleteAt = 0 LIMIT :Limit OFFSET :Offset", map[string]interface{}{"TeamId": teamId, "Offset": offset, "Limit": limit})
+ var dbMembers teamMemberWithSchemeRolesList
+ _, err := s.GetReplica().Select(&dbMembers, TEAM_MEMBERS_WITH_SCHEME_SELECT_QUERY+"WHERE TeamMembers.TeamId = :TeamId AND TeamMembers.DeleteAt = 0 LIMIT :Limit OFFSET :Offset", map[string]interface{}{"TeamId": teamId, "Limit": limit, "Offset": offset})
if err != nil {
result.Err = model.NewAppError("SqlTeamStore.GetMembers", "store.sql_team.get_members.app_error", nil, "teamId="+teamId+" "+err.Error(), http.StatusInternalServerError)
} else {
- result.Data = members
+ result.Data = dbMembers.ToModel()
}
})
}
@@ -452,7 +597,7 @@ func (s SqlTeamStore) GetActiveMemberCount(teamId string) store.StoreChannel {
func (s SqlTeamStore) GetMembersByIds(teamId string, userIds []string) store.StoreChannel {
return store.Do(func(result *store.StoreResult) {
- var members []*model.TeamMember
+ var dbMembers teamMemberWithSchemeRolesList
props := make(map[string]interface{})
idQuery := ""
@@ -467,22 +612,22 @@ func (s SqlTeamStore) GetMembersByIds(teamId string, userIds []string) store.Sto
props["TeamId"] = teamId
- if _, err := s.GetReplica().Select(&members, "SELECT * FROM TeamMembers WHERE TeamId = :TeamId AND UserId IN ("+idQuery+") AND DeleteAt = 0", props); err != nil {
+ if _, err := s.GetReplica().Select(&dbMembers, TEAM_MEMBERS_WITH_SCHEME_SELECT_QUERY+"WHERE TeamMembers.TeamId = :TeamId AND TeamMembers.UserId IN ("+idQuery+") AND TeamMembers.DeleteAt = 0", props); err != nil {
result.Err = model.NewAppError("SqlTeamStore.GetMembersByIds", "store.sql_team.get_members_by_ids.app_error", nil, "teamId="+teamId+" "+err.Error(), http.StatusInternalServerError)
} else {
- result.Data = members
+ result.Data = dbMembers.ToModel()
}
})
}
func (s SqlTeamStore) GetTeamsForUser(userId string) store.StoreChannel {
return store.Do(func(result *store.StoreResult) {
- var members []*model.TeamMember
- _, err := s.GetReplica().Select(&members, "SELECT * FROM TeamMembers WHERE UserId = :UserId", map[string]interface{}{"UserId": userId})
+ var dbMembers teamMemberWithSchemeRolesList
+ _, err := s.GetReplica().Select(&dbMembers, TEAM_MEMBERS_WITH_SCHEME_SELECT_QUERY+"WHERE TeamMembers.UserId = :UserId", map[string]interface{}{"UserId": userId})
if err != nil {
result.Err = model.NewAppError("SqlTeamStore.GetMembers", "store.sql_team.get_members.app_error", nil, "userId="+userId+" "+err.Error(), http.StatusInternalServerError)
} else {
- result.Data = members
+ result.Data = dbMembers.ToModel()
}
})
}
@@ -569,3 +714,91 @@ func (us SqlTeamStore) UpdateLastTeamIconUpdate(teamId string, curTime int64) st
}
})
}
+
+func (s SqlTeamStore) GetTeamsByScheme(schemeId string, offset int, limit int) store.StoreChannel {
+ return store.Do(func(result *store.StoreResult) {
+ var teams []*model.Team
+ _, err := s.GetReplica().Select(&teams, "SELECT * FROM Teams WHERE SchemeId = :SchemeId ORDER BY DisplayName LIMIT :Limit OFFSET :Offset", map[string]interface{}{"SchemeId": schemeId, "Offset": offset, "Limit": limit})
+ if err != nil {
+ result.Err = model.NewAppError("SqlTeamStore.GetTeamsByScheme", "store.sql_team.get_by_scheme.app_error", nil, "schemeId="+schemeId+" "+err.Error(), http.StatusInternalServerError)
+ } else {
+ result.Data = teams
+ }
+ })
+}
+
+// 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
+ })
+}
+
+func (s SqlTeamStore) ResetAllTeamSchemes() store.StoreChannel {
+ return store.Do(func(result *store.StoreResult) {
+ if _, err := s.GetMaster().Exec("UPDATE Teams SET SchemeId=''"); err != nil {
+ result.Err = model.NewAppError("SqlTeamStore.ResetAllTeamSchemes", "store.sql_team.reset_all_team_schemes.app_error", nil, err.Error(), http.StatusInternalServerError)
+ }
+ })
+}
diff --git a/store/sqlstore/team_store_test.go b/store/sqlstore/team_store_test.go
index 6618285c4..4aaefd1a6 100644
--- a/store/sqlstore/team_store_test.go
+++ b/store/sqlstore/team_store_test.go
@@ -4,11 +4,378 @@
package sqlstore
import (
+ "database/sql"
"testing"
+ "github.com/stretchr/testify/assert"
+
+ "github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/store/storetest"
)
func TestTeamStore(t *testing.T) {
StoreTest(t, storetest.TestTeamStore)
}
+
+func TestTeamStoreInternalDataTypes(t *testing.T) {
+ t.Run("NewTeamMemberFromModel", func(t *testing.T) { testNewTeamMemberFromModel(t) })
+ t.Run("TeamMemberWithSchemeRolesToModel", func(t *testing.T) { testTeamMemberWithSchemeRolesToModel(t) })
+}
+
+func testNewTeamMemberFromModel(t *testing.T) {
+ m := model.TeamMember{
+ TeamId: model.NewId(),
+ UserId: model.NewId(),
+ Roles: "team_user team_admin custom_role",
+ DeleteAt: 12345,
+ SchemeUser: true,
+ SchemeAdmin: true,
+ ExplicitRoles: "custom_role",
+ }
+
+ db := NewTeamMemberFromModel(&m)
+
+ assert.Equal(t, m.TeamId, db.TeamId)
+ assert.Equal(t, m.UserId, db.UserId)
+ assert.Equal(t, m.DeleteAt, db.DeleteAt)
+ assert.Equal(t, true, db.SchemeUser.Valid)
+ assert.Equal(t, true, db.SchemeAdmin.Valid)
+ assert.Equal(t, m.SchemeUser, db.SchemeUser.Bool)
+ assert.Equal(t, m.SchemeAdmin, db.SchemeAdmin.Bool)
+ assert.Equal(t, m.ExplicitRoles, db.Roles)
+}
+
+func testTeamMemberWithSchemeRolesToModel(t *testing.T) {
+ // Test all the non-role-related properties here.
+ t.Run("BasicProperties", func(t *testing.T) {
+ db := teamMemberWithSchemeRoles{
+ TeamId: model.NewId(),
+ UserId: model.NewId(),
+ Roles: "custom_role",
+ DeleteAt: 12345,
+ SchemeUser: sql.NullBool{Valid: true, Bool: true},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: true},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: false},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ }
+
+ m := db.ToModel()
+
+ assert.Equal(t, db.TeamId, m.TeamId)
+ assert.Equal(t, db.UserId, m.UserId)
+ assert.Equal(t, "custom_role team_user team_admin", m.Roles)
+ assert.Equal(t, db.DeleteAt, m.DeleteAt)
+ assert.Equal(t, db.SchemeUser.Bool, m.SchemeUser)
+ assert.Equal(t, db.SchemeAdmin.Bool, m.SchemeAdmin)
+ assert.Equal(t, db.Roles, m.ExplicitRoles)
+ })
+
+ // Example data *before* the Phase 2 migration has taken place.
+ t.Run("Unmigrated_NoScheme_User", func(t *testing.T) {
+ db := teamMemberWithSchemeRoles{
+ Roles: "team_user",
+ SchemeUser: sql.NullBool{Valid: false, Bool: false},
+ SchemeAdmin: sql.NullBool{Valid: false, Bool: false},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: false},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ }
+
+ m := db.ToModel()
+
+ assert.Equal(t, "team_user", m.Roles)
+ assert.Equal(t, true, m.SchemeUser)
+ assert.Equal(t, false, m.SchemeAdmin)
+ assert.Equal(t, "", m.ExplicitRoles)
+ })
+
+ t.Run("Unmigrated_NoScheme_Admin", func(t *testing.T) {
+ db := teamMemberWithSchemeRoles{
+ Roles: "team_user team_admin",
+ SchemeUser: sql.NullBool{Valid: false, Bool: false},
+ SchemeAdmin: sql.NullBool{Valid: false, Bool: false},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: false},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ }
+
+ m := db.ToModel()
+
+ assert.Equal(t, "team_user team_admin", m.Roles)
+ assert.Equal(t, true, m.SchemeUser)
+ assert.Equal(t, true, m.SchemeAdmin)
+ assert.Equal(t, "", m.ExplicitRoles)
+ })
+
+ t.Run("Unmigrated_NoScheme_CustomRole", func(t *testing.T) {
+ db := teamMemberWithSchemeRoles{
+ Roles: "custom_role",
+ SchemeUser: sql.NullBool{Valid: false, Bool: false},
+ SchemeAdmin: sql.NullBool{Valid: false, Bool: false},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: false},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ }
+
+ m := db.ToModel()
+
+ assert.Equal(t, "custom_role", m.Roles)
+ assert.Equal(t, false, m.SchemeUser)
+ assert.Equal(t, false, m.SchemeAdmin)
+ assert.Equal(t, "custom_role", m.ExplicitRoles)
+ })
+
+ t.Run("Unmigrated_NoScheme_UserAndCustomRole", func(t *testing.T) {
+ db := teamMemberWithSchemeRoles{
+ Roles: "team_user custom_role",
+ SchemeUser: sql.NullBool{Valid: false, Bool: false},
+ SchemeAdmin: sql.NullBool{Valid: false, Bool: false},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: false},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ }
+
+ m := db.ToModel()
+
+ assert.Equal(t, "team_user custom_role", m.Roles)
+ assert.Equal(t, true, m.SchemeUser)
+ assert.Equal(t, false, m.SchemeAdmin)
+ assert.Equal(t, "custom_role", m.ExplicitRoles)
+ })
+
+ t.Run("Unmigrated_NoScheme_AdminAndCustomRole", func(t *testing.T) {
+ db := teamMemberWithSchemeRoles{
+ Roles: "team_user team_admin custom_role",
+ SchemeUser: sql.NullBool{Valid: false, Bool: false},
+ SchemeAdmin: sql.NullBool{Valid: false, Bool: false},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: false},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ }
+
+ m := db.ToModel()
+
+ assert.Equal(t, "team_user team_admin custom_role", m.Roles)
+ assert.Equal(t, true, m.SchemeUser)
+ assert.Equal(t, true, m.SchemeAdmin)
+ assert.Equal(t, "custom_role", m.ExplicitRoles)
+ })
+
+ t.Run("Unmigrated_NoScheme_NoRoles", func(t *testing.T) {
+ db := teamMemberWithSchemeRoles{
+ Roles: "",
+ SchemeUser: sql.NullBool{Valid: false, Bool: false},
+ SchemeAdmin: sql.NullBool{Valid: false, Bool: false},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: false},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ }
+
+ m := db.ToModel()
+
+ assert.Equal(t, "", m.Roles)
+ assert.Equal(t, false, m.SchemeUser)
+ assert.Equal(t, false, m.SchemeAdmin)
+ assert.Equal(t, "", m.ExplicitRoles)
+ })
+
+ // Example data *after* the Phase 2 migration has taken place.
+ t.Run("Migrated_NoScheme_User", func(t *testing.T) {
+ db := teamMemberWithSchemeRoles{
+ Roles: "",
+ SchemeUser: sql.NullBool{Valid: true, Bool: true},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: false},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: false},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ }
+
+ m := db.ToModel()
+
+ assert.Equal(t, "team_user", m.Roles)
+ assert.Equal(t, true, m.SchemeUser)
+ assert.Equal(t, false, m.SchemeAdmin)
+ assert.Equal(t, "", m.ExplicitRoles)
+ })
+
+ t.Run("Migrated_NoScheme_Admin", func(t *testing.T) {
+ db := teamMemberWithSchemeRoles{
+ Roles: "",
+ SchemeUser: sql.NullBool{Valid: true, Bool: true},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: true},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: false},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ }
+
+ m := db.ToModel()
+
+ assert.Equal(t, "team_user team_admin", m.Roles)
+ assert.Equal(t, true, m.SchemeUser)
+ assert.Equal(t, true, m.SchemeAdmin)
+ assert.Equal(t, "", m.ExplicitRoles)
+ })
+
+ t.Run("Migrated_NoScheme_CustomRole", func(t *testing.T) {
+ db := teamMemberWithSchemeRoles{
+ Roles: "custom_role",
+ SchemeUser: sql.NullBool{Valid: true, Bool: false},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: false},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: false},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ }
+
+ m := db.ToModel()
+
+ assert.Equal(t, "custom_role", m.Roles)
+ assert.Equal(t, false, m.SchemeUser)
+ assert.Equal(t, false, m.SchemeAdmin)
+ assert.Equal(t, "custom_role", m.ExplicitRoles)
+ })
+
+ t.Run("Migrated_NoScheme_UserAndCustomRole", func(t *testing.T) {
+ db := teamMemberWithSchemeRoles{
+ Roles: "custom_role",
+ SchemeUser: sql.NullBool{Valid: true, Bool: true},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: false},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: false},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ }
+
+ m := db.ToModel()
+
+ assert.Equal(t, "custom_role team_user", m.Roles)
+ assert.Equal(t, true, m.SchemeUser)
+ assert.Equal(t, false, m.SchemeAdmin)
+ assert.Equal(t, "custom_role", m.ExplicitRoles)
+ })
+
+ t.Run("Migrated_NoScheme_AdminAndCustomRole", func(t *testing.T) {
+ db := teamMemberWithSchemeRoles{
+ Roles: "custom_role",
+ SchemeUser: sql.NullBool{Valid: true, Bool: true},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: true},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: false},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ }
+
+ m := db.ToModel()
+
+ assert.Equal(t, "custom_role team_user team_admin", m.Roles)
+ assert.Equal(t, true, m.SchemeUser)
+ assert.Equal(t, true, m.SchemeAdmin)
+ assert.Equal(t, "custom_role", m.ExplicitRoles)
+ })
+
+ t.Run("Migrated_NoScheme_NoRoles", func(t *testing.T) {
+ db := teamMemberWithSchemeRoles{
+ Roles: "",
+ SchemeUser: sql.NullBool{Valid: true, Bool: false},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: false},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: false},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: false},
+ }
+
+ m := db.ToModel()
+
+ assert.Equal(t, "", m.Roles)
+ assert.Equal(t, false, m.SchemeUser)
+ assert.Equal(t, false, m.SchemeAdmin)
+ assert.Equal(t, "", m.ExplicitRoles)
+ })
+
+ // Example data with a team scheme.
+ t.Run("Migrated_TeamScheme_User", func(t *testing.T) {
+ db := teamMemberWithSchemeRoles{
+ Roles: "",
+ SchemeUser: sql.NullBool{Valid: true, Bool: true},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: false},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: true, String: "tscheme_user"},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: true, String: "tscheme_admin"},
+ }
+
+ m := db.ToModel()
+
+ assert.Equal(t, "tscheme_user", m.Roles)
+ assert.Equal(t, true, m.SchemeUser)
+ assert.Equal(t, false, m.SchemeAdmin)
+ assert.Equal(t, "", m.ExplicitRoles)
+ })
+
+ t.Run("Migrated_TeamScheme_Admin", func(t *testing.T) {
+ db := teamMemberWithSchemeRoles{
+ Roles: "",
+ SchemeUser: sql.NullBool{Valid: true, Bool: true},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: true},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: true, String: "tscheme_user"},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: true, String: "tscheme_admin"},
+ }
+
+ m := db.ToModel()
+
+ assert.Equal(t, "tscheme_user tscheme_admin", m.Roles)
+ assert.Equal(t, true, m.SchemeUser)
+ assert.Equal(t, true, m.SchemeAdmin)
+ assert.Equal(t, "", m.ExplicitRoles)
+ })
+
+ t.Run("Migrated_TeamScheme_CustomRole", func(t *testing.T) {
+ db := teamMemberWithSchemeRoles{
+ Roles: "custom_role",
+ SchemeUser: sql.NullBool{Valid: true, Bool: false},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: false},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: true, String: "tscheme_user"},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: true, String: "tscheme_admin"},
+ }
+
+ m := db.ToModel()
+
+ assert.Equal(t, "custom_role", m.Roles)
+ assert.Equal(t, false, m.SchemeUser)
+ assert.Equal(t, false, m.SchemeAdmin)
+ assert.Equal(t, "custom_role", m.ExplicitRoles)
+ })
+
+ t.Run("Migrated_TeamScheme_UserAndCustomRole", func(t *testing.T) {
+ db := teamMemberWithSchemeRoles{
+ Roles: "custom_role",
+ SchemeUser: sql.NullBool{Valid: true, Bool: true},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: false},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: true, String: "tscheme_user"},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: true, String: "tscheme_admin"},
+ }
+
+ m := db.ToModel()
+
+ assert.Equal(t, "custom_role tscheme_user", m.Roles)
+ assert.Equal(t, true, m.SchemeUser)
+ assert.Equal(t, false, m.SchemeAdmin)
+ assert.Equal(t, "custom_role", m.ExplicitRoles)
+ })
+
+ t.Run("Migrated_TeamScheme_AdminAndCustomRole", func(t *testing.T) {
+ db := teamMemberWithSchemeRoles{
+ Roles: "custom_role",
+ SchemeUser: sql.NullBool{Valid: true, Bool: true},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: true},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: true, String: "tscheme_user"},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: true, String: "tscheme_admin"},
+ }
+
+ m := db.ToModel()
+
+ assert.Equal(t, "custom_role tscheme_user tscheme_admin", m.Roles)
+ assert.Equal(t, true, m.SchemeUser)
+ assert.Equal(t, true, m.SchemeAdmin)
+ assert.Equal(t, "custom_role", m.ExplicitRoles)
+ })
+
+ t.Run("Migrated_TeamScheme_NoRoles", func(t *testing.T) {
+ db := teamMemberWithSchemeRoles{
+ Roles: "",
+ SchemeUser: sql.NullBool{Valid: true, Bool: false},
+ SchemeAdmin: sql.NullBool{Valid: true, Bool: false},
+ TeamSchemeDefaultUserRole: sql.NullString{Valid: true, String: "tscheme_user"},
+ TeamSchemeDefaultAdminRole: sql.NullString{Valid: true, String: "tscheme_admin"},
+ }
+
+ m := db.ToModel()
+
+ assert.Equal(t, "", m.Roles)
+ assert.Equal(t, false, m.SchemeUser)
+ assert.Equal(t, false, m.SchemeAdmin)
+ assert.Equal(t, "", m.ExplicitRoles)
+ })
+}
diff --git a/store/sqlstore/upgrade.go b/store/sqlstore/upgrade.go
index 4099ac11a..98a89f36d 100644
--- a/store/sqlstore/upgrade.go
+++ b/store/sqlstore/upgrade.go
@@ -425,8 +425,24 @@ func UpgradeDatabaseToVersion410(sqlStore SqlStore) {
}
func UpgradeDatabaseToVersion50(sqlStore SqlStore) {
- // TODO: Uncomment following condition when version 3.10.0 is released
+ // This version of Mattermost includes an App-Layer migration which migrates from hard-coded emojis configured
+ // in `config.json` to a `Permission` in the database. The migration code can be seen
+ // in the file `app/app.go` in the function `DoEmojisPermissionsMigration()`.
+
+ // TODO: Uncomment following condition when version 5.0.0 is released
//if shouldPerformUpgrade(sqlStore, VERSION_4_10_0, VERSION_5_0_0) {
+
+ sqlStore.CreateColumnIfNotExistsNoDefault("Teams", "SchemeId", "varchar(26)", "varchar(26)")
+ sqlStore.CreateColumnIfNotExistsNoDefault("Channels", "SchemeId", "varchar(26)", "varchar(26)")
+
+ sqlStore.CreateColumnIfNotExistsNoDefault("TeamMembers", "SchemeUser", "boolean", "boolean")
+ sqlStore.CreateColumnIfNotExistsNoDefault("TeamMembers", "SchemeAdmin", "boolean", "boolean")
+ sqlStore.CreateColumnIfNotExistsNoDefault("ChannelMembers", "SchemeUser", "boolean", "boolean")
+ sqlStore.CreateColumnIfNotExistsNoDefault("ChannelMembers", "SchemeAdmin", "boolean", "boolean")
+
+ sqlStore.CreateColumnIfNotExists("Roles", "BuiltIn", "boolean", "boolean", "0")
+ sqlStore.GetMaster().Exec("UPDATE Roles SET BuiltIn=true")
+ sqlStore.GetMaster().Exec("UPDATE Roles SET SchemeManaged=false WHERE Name NOT IN ('system_user', 'system_admin', 'team_user', 'team_admin', 'channel_user', 'channel_admin')")
sqlStore.CreateColumnIfNotExists("IncomingWebhooks", "ChannelLocked", "boolean", "boolean", "0")
// saveSchemaVersion(sqlStore, VERSION_5_0_0)
diff --git a/store/store.go b/store/store.go
index 7fcebf0b1..bfc0ab845 100644
--- a/store/store.go
+++ b/store/store.go
@@ -60,6 +60,7 @@ type Store interface {
FileInfo() FileInfoStore
Reaction() ReactionStore
Role() RoleStore
+ Scheme() SchemeStore
Job() JobStore
UserAccessToken() UserAccessTokenStore
ChannelMemberHistory() ChannelMemberHistoryStore
@@ -103,6 +104,9 @@ type TeamStore interface {
RemoveAllMembersByTeam(teamId string) StoreChannel
RemoveAllMembersByUser(userId string) StoreChannel
UpdateLastTeamIconUpdate(teamId string, curTime int64) StoreChannel
+ GetTeamsByScheme(schemeId string, offset int, limit int) StoreChannel
+ MigrateTeamMembers(fromTeamId string, fromUserId string) StoreChannel
+ ResetAllTeamSchemes() StoreChannel
}
type ChannelStore interface {
@@ -160,6 +164,9 @@ type ChannelStore interface {
AnalyticsDeletedTypeCount(teamId string, channelType string) StoreChannel
GetChannelUnread(channelId, userId string) StoreChannel
ClearCaches()
+ GetChannelsByScheme(schemeId string, offset int, limit int) StoreChannel
+ MigrateChannelMembers(fromChannelId string, fromUserId string) StoreChannel
+ ResetAllChannelSchemes() StoreChannel
}
type ChannelMemberHistoryStore interface {
@@ -475,5 +482,14 @@ type RoleStore interface {
Get(roleId string) StoreChannel
GetByName(name string) StoreChannel
GetByNames(names []string) StoreChannel
+ Delete(roldId string) StoreChannel
+ PermanentDeleteAll() StoreChannel
+}
+
+type SchemeStore interface {
+ Save(scheme *model.Scheme) StoreChannel
+ Get(schemeId string) StoreChannel
+ GetAllPage(scope string, offset int, limit int) StoreChannel
+ Delete(schemeId string) StoreChannel
PermanentDeleteAll() StoreChannel
}
diff --git a/store/storetest/channel_store.go b/store/storetest/channel_store.go
index 9abb62856..eea79d42f 100644
--- a/store/storetest/channel_store.go
+++ b/store/storetest/channel_store.go
@@ -5,6 +5,7 @@ package storetest
import (
"sort"
+ "strings"
"testing"
"time"
@@ -16,6 +17,8 @@ import (
)
func TestChannelStore(t *testing.T, ss store.Store) {
+ createDefaultRoles(t, ss)
+
t.Run("Save", func(t *testing.T) { testChannelStoreSave(t, ss) })
t.Run("SaveDirectChannel", func(t *testing.T) { testChannelStoreSaveDirectChannel(t, ss) })
t.Run("CreateDirectChannel", func(t *testing.T) { testChannelStoreCreateDirectChannel(t, ss) })
@@ -49,6 +52,10 @@ func TestChannelStore(t *testing.T, ss store.Store) {
t.Run("AnalyticsDeletedTypeCount", func(t *testing.T) { testChannelStoreAnalyticsDeletedTypeCount(t, ss) })
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) })
+ t.Run("ResetAllChannelSchemes", func(t *testing.T) { testResetAllChannelSchemes(t, ss) })
+
}
func testChannelStoreSave(t *testing.T, ss store.Store) {
@@ -2171,3 +2178,183 @@ func testChannelStoreMaxChannelsPerTeam(t *testing.T, ss store.Store) {
result = <-ss.Channel().Save(channel, 1)
assert.Nil(t, result.Err)
}
+
+func testChannelStoreGetChannelsByScheme(t *testing.T, ss store.Store) {
+ // Create some schemes.
+ s1 := &model.Scheme{
+ DisplayName: model.NewId(),
+ Name: model.NewId(),
+ Description: model.NewId(),
+ Scope: model.SCHEME_SCOPE_CHANNEL,
+ }
+
+ s2 := &model.Scheme{
+ DisplayName: model.NewId(),
+ Name: model.NewId(),
+ Description: model.NewId(),
+ Scope: model.SCHEME_SCOPE_CHANNEL,
+ }
+
+ s1 = (<-ss.Scheme().Save(s1)).Data.(*model.Scheme)
+ s2 = (<-ss.Scheme().Save(s2)).Data.(*model.Scheme)
+
+ // Create and save some teams.
+ c1 := &model.Channel{
+ TeamId: model.NewId(),
+ DisplayName: "Name",
+ Name: model.NewId(),
+ Type: model.CHANNEL_OPEN,
+ SchemeId: &s1.Id,
+ }
+
+ c2 := &model.Channel{
+ TeamId: model.NewId(),
+ DisplayName: "Name",
+ Name: model.NewId(),
+ Type: model.CHANNEL_OPEN,
+ SchemeId: &s1.Id,
+ }
+
+ c3 := &model.Channel{
+ TeamId: model.NewId(),
+ DisplayName: "Name",
+ Name: model.NewId(),
+ Type: model.CHANNEL_OPEN,
+ }
+
+ c1 = (<-ss.Channel().Save(c1, 100)).Data.(*model.Channel)
+ c2 = (<-ss.Channel().Save(c2, 100)).Data.(*model.Channel)
+ c3 = (<-ss.Channel().Save(c3, 100)).Data.(*model.Channel)
+
+ // Get the channels by a valid Scheme ID.
+ res1 := <-ss.Channel().GetChannelsByScheme(s1.Id, 0, 100)
+ assert.Nil(t, res1.Err)
+ d1 := res1.Data.(model.ChannelList)
+ assert.Len(t, d1, 2)
+
+ // Get the channels by a valid Scheme ID where there aren't any matching Channel.
+ res2 := <-ss.Channel().GetChannelsByScheme(s2.Id, 0, 100)
+ assert.Nil(t, res2.Err)
+ d2 := res2.Data.(model.ChannelList)
+ assert.Len(t, d2, 0)
+
+ // Get the channels by an invalid Scheme ID.
+ res3 := <-ss.Channel().GetChannelsByScheme(model.NewId(), 0, 100)
+ assert.Nil(t, res3.Err)
+ 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)
+}
+
+func testResetAllChannelSchemes(t *testing.T, ss store.Store) {
+ s1 := &model.Scheme{
+ Name: model.NewId(),
+ DisplayName: model.NewId(),
+ Description: model.NewId(),
+ Scope: model.SCHEME_SCOPE_CHANNEL,
+ }
+ s1 = (<-ss.Scheme().Save(s1)).Data.(*model.Scheme)
+
+ c1 := &model.Channel{
+ TeamId: model.NewId(),
+ DisplayName: "Name",
+ Name: model.NewId(),
+ Type: model.CHANNEL_OPEN,
+ SchemeId: &s1.Id,
+ }
+
+ c2 := &model.Channel{
+ TeamId: model.NewId(),
+ DisplayName: "Name",
+ Name: model.NewId(),
+ Type: model.CHANNEL_OPEN,
+ SchemeId: &s1.Id,
+ }
+
+ c1 = (<-ss.Channel().Save(c1, 100)).Data.(*model.Channel)
+ c2 = (<-ss.Channel().Save(c2, 100)).Data.(*model.Channel)
+
+ assert.Equal(t, s1.Id, *c1.SchemeId)
+ assert.Equal(t, s1.Id, *c2.SchemeId)
+
+ res := <-ss.Channel().ResetAllChannelSchemes()
+ assert.Nil(t, res.Err)
+
+ c1 = (<-ss.Channel().Get(c1.Id, true)).Data.(*model.Channel)
+ c2 = (<-ss.Channel().Get(c2.Id, true)).Data.(*model.Channel)
+
+ assert.Equal(t, "", *c1.SchemeId)
+ assert.Equal(t, "", *c2.SchemeId)
+}
diff --git a/store/storetest/mocks/ChannelStore.go b/store/storetest/mocks/ChannelStore.go
index ec3fa1253..10ac908e4 100644
--- a/store/storetest/mocks/ChannelStore.go
+++ b/store/storetest/mocks/ChannelStore.go
@@ -258,6 +258,22 @@ func (_m *ChannelStore) GetChannels(teamId string, userId string) store.StoreCha
return r0
}
+// GetChannelsByScheme provides a mock function with given fields: schemeId, offset, limit
+func (_m *ChannelStore) GetChannelsByScheme(schemeId string, offset int, limit int) store.StoreChannel {
+ ret := _m.Called(schemeId, offset, limit)
+
+ var r0 store.StoreChannel
+ if rf, ok := ret.Get(0).(func(string, int, int) store.StoreChannel); ok {
+ r0 = rf(schemeId, offset, limit)
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).(store.StoreChannel)
+ }
+ }
+
+ return r0
+}
+
// GetDeleted provides a mock function with given fields: team_id, offset, limit
func (_m *ChannelStore) GetDeleted(team_id string, offset int, limit int) store.StoreChannel {
ret := _m.Called(team_id, offset, limit)
@@ -567,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)
@@ -647,6 +679,22 @@ func (_m *ChannelStore) RemoveMember(channelId string, userId string) store.Stor
return r0
}
+// ResetAllChannelSchemes provides a mock function with given fields:
+func (_m *ChannelStore) ResetAllChannelSchemes() 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
+}
+
// Restore provides a mock function with given fields: channelId, time
func (_m *ChannelStore) Restore(channelId string, time int64) store.StoreChannel {
ret := _m.Called(channelId, time)
diff --git a/store/storetest/mocks/LayeredStoreDatabaseLayer.go b/store/storetest/mocks/LayeredStoreDatabaseLayer.go
index cf1d776ef..c5b821b05 100644
--- a/store/storetest/mocks/LayeredStoreDatabaseLayer.go
+++ b/store/storetest/mocks/LayeredStoreDatabaseLayer.go
@@ -432,6 +432,29 @@ func (_m *LayeredStoreDatabaseLayer) Role() store.RoleStore {
return r0
}
+// RoleDelete provides a mock function with given fields: ctx, roldId, hints
+func (_m *LayeredStoreDatabaseLayer) RoleDelete(ctx context.Context, roldId 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, roldId)
+ _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, roldId, hints...)
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).(*store.LayeredStoreSupplierResult)
+ }
+ }
+
+ 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))
@@ -547,6 +570,137 @@ func (_m *LayeredStoreDatabaseLayer) RoleSave(ctx context.Context, role *model.R
return r0
}
+// Scheme provides a mock function with given fields:
+func (_m *LayeredStoreDatabaseLayer) Scheme() store.SchemeStore {
+ ret := _m.Called()
+
+ var r0 store.SchemeStore
+ if rf, ok := ret.Get(0).(func() store.SchemeStore); ok {
+ r0 = rf()
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).(store.SchemeStore)
+ }
+ }
+
+ return r0
+}
+
+// SchemeDelete provides a mock function with given fields: ctx, schemeId, hints
+func (_m *LayeredStoreDatabaseLayer) SchemeDelete(ctx context.Context, schemeId 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, schemeId)
+ _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, schemeId, hints...)
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).(*store.LayeredStoreSupplierResult)
+ }
+ }
+
+ return r0
+}
+
+// SchemeGet provides a mock function with given fields: ctx, schemeId, hints
+func (_m *LayeredStoreDatabaseLayer) SchemeGet(ctx context.Context, schemeId 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, schemeId)
+ _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, schemeId, hints...)
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).(*store.LayeredStoreSupplierResult)
+ }
+ }
+
+ return r0
+}
+
+// SchemeGetAllPage provides a mock function with given fields: ctx, scope, offset, limit, hints
+func (_m *LayeredStoreDatabaseLayer) SchemeGetAllPage(ctx context.Context, scope string, offset int, limit int, 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, scope, offset, limit)
+ _ca = append(_ca, _va...)
+ ret := _m.Called(_ca...)
+
+ var r0 *store.LayeredStoreSupplierResult
+ if rf, ok := ret.Get(0).(func(context.Context, string, int, int, ...store.LayeredStoreHint) *store.LayeredStoreSupplierResult); ok {
+ r0 = rf(ctx, scope, offset, limit, hints...)
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).(*store.LayeredStoreSupplierResult)
+ }
+ }
+
+ return r0
+}
+
+// SchemePermanentDeleteAll provides a mock function with given fields: ctx, hints
+func (_m *LayeredStoreDatabaseLayer) SchemePermanentDeleteAll(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
+}
+
+// SchemeSave provides a mock function with given fields: ctx, scheme, hints
+func (_m *LayeredStoreDatabaseLayer) SchemeSave(ctx context.Context, scheme *model.Scheme, 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, scheme)
+ _ca = append(_ca, _va...)
+ ret := _m.Called(_ca...)
+
+ var r0 *store.LayeredStoreSupplierResult
+ if rf, ok := ret.Get(0).(func(context.Context, *model.Scheme, ...store.LayeredStoreHint) *store.LayeredStoreSupplierResult); ok {
+ r0 = rf(ctx, scheme, 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 443112615..37a01df14 100644
--- a/store/storetest/mocks/LayeredStoreSupplier.go
+++ b/store/storetest/mocks/LayeredStoreSupplier.go
@@ -145,6 +145,29 @@ func (_m *LayeredStoreSupplier) ReactionSave(ctx context.Context, reaction *mode
return r0
}
+// RoleDelete provides a mock function with given fields: ctx, roldId, hints
+func (_m *LayeredStoreSupplier) RoleDelete(ctx context.Context, roldId 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, roldId)
+ _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, roldId, hints...)
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).(*store.LayeredStoreSupplierResult)
+ }
+ }
+
+ 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))
@@ -260,6 +283,121 @@ func (_m *LayeredStoreSupplier) RoleSave(ctx context.Context, role *model.Role,
return r0
}
+// SchemeDelete provides a mock function with given fields: ctx, schemeId, hints
+func (_m *LayeredStoreSupplier) SchemeDelete(ctx context.Context, schemeId 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, schemeId)
+ _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, schemeId, hints...)
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).(*store.LayeredStoreSupplierResult)
+ }
+ }
+
+ return r0
+}
+
+// SchemeGet provides a mock function with given fields: ctx, schemeId, hints
+func (_m *LayeredStoreSupplier) SchemeGet(ctx context.Context, schemeId 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, schemeId)
+ _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, schemeId, hints...)
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).(*store.LayeredStoreSupplierResult)
+ }
+ }
+
+ return r0
+}
+
+// SchemeGetAllPage provides a mock function with given fields: ctx, scope, offset, limit, hints
+func (_m *LayeredStoreSupplier) SchemeGetAllPage(ctx context.Context, scope string, offset int, limit int, 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, scope, offset, limit)
+ _ca = append(_ca, _va...)
+ ret := _m.Called(_ca...)
+
+ var r0 *store.LayeredStoreSupplierResult
+ if rf, ok := ret.Get(0).(func(context.Context, string, int, int, ...store.LayeredStoreHint) *store.LayeredStoreSupplierResult); ok {
+ r0 = rf(ctx, scope, offset, limit, hints...)
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).(*store.LayeredStoreSupplierResult)
+ }
+ }
+
+ return r0
+}
+
+// SchemePermanentDeleteAll provides a mock function with given fields: ctx, hints
+func (_m *LayeredStoreSupplier) SchemePermanentDeleteAll(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
+}
+
+// SchemeSave provides a mock function with given fields: ctx, scheme, hints
+func (_m *LayeredStoreSupplier) SchemeSave(ctx context.Context, scheme *model.Scheme, 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, scheme)
+ _ca = append(_ca, _va...)
+ ret := _m.Called(_ca...)
+
+ var r0 *store.LayeredStoreSupplierResult
+ if rf, ok := ret.Get(0).(func(context.Context, *model.Scheme, ...store.LayeredStoreHint) *store.LayeredStoreSupplierResult); ok {
+ r0 = rf(ctx, scheme, 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
index cb69b2f2e..95e1914e0 100644
--- a/store/storetest/mocks/RoleStore.go
+++ b/store/storetest/mocks/RoleStore.go
@@ -13,6 +13,22 @@ type RoleStore struct {
mock.Mock
}
+// Delete provides a mock function with given fields: roldId
+func (_m *RoleStore) Delete(roldId string) store.StoreChannel {
+ ret := _m.Called(roldId)
+
+ var r0 store.StoreChannel
+ if rf, ok := ret.Get(0).(func(string) store.StoreChannel); ok {
+ r0 = rf(roldId)
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).(store.StoreChannel)
+ }
+ }
+
+ return r0
+}
+
// Get provides a mock function with given fields: roleId
func (_m *RoleStore) Get(roleId string) store.StoreChannel {
ret := _m.Called(roleId)
diff --git a/store/storetest/mocks/SchemeStore.go b/store/storetest/mocks/SchemeStore.go
new file mode 100644
index 000000000..ffb10f931
--- /dev/null
+++ b/store/storetest/mocks/SchemeStore.go
@@ -0,0 +1,94 @@
+// 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"
+
+// SchemeStore is an autogenerated mock type for the SchemeStore type
+type SchemeStore struct {
+ mock.Mock
+}
+
+// Delete provides a mock function with given fields: schemeId
+func (_m *SchemeStore) Delete(schemeId string) store.StoreChannel {
+ ret := _m.Called(schemeId)
+
+ var r0 store.StoreChannel
+ if rf, ok := ret.Get(0).(func(string) store.StoreChannel); ok {
+ r0 = rf(schemeId)
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).(store.StoreChannel)
+ }
+ }
+
+ return r0
+}
+
+// Get provides a mock function with given fields: schemeId
+func (_m *SchemeStore) Get(schemeId string) store.StoreChannel {
+ ret := _m.Called(schemeId)
+
+ var r0 store.StoreChannel
+ if rf, ok := ret.Get(0).(func(string) store.StoreChannel); ok {
+ r0 = rf(schemeId)
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).(store.StoreChannel)
+ }
+ }
+
+ return r0
+}
+
+// GetAllPage provides a mock function with given fields: scope, offset, limit
+func (_m *SchemeStore) GetAllPage(scope string, offset int, limit int) store.StoreChannel {
+ ret := _m.Called(scope, offset, limit)
+
+ var r0 store.StoreChannel
+ if rf, ok := ret.Get(0).(func(string, int, int) store.StoreChannel); ok {
+ r0 = rf(scope, offset, limit)
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).(store.StoreChannel)
+ }
+ }
+
+ return r0
+}
+
+// PermanentDeleteAll provides a mock function with given fields:
+func (_m *SchemeStore) 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: scheme
+func (_m *SchemeStore) Save(scheme *model.Scheme) store.StoreChannel {
+ ret := _m.Called(scheme)
+
+ var r0 store.StoreChannel
+ if rf, ok := ret.Get(0).(func(*model.Scheme) store.StoreChannel); ok {
+ r0 = rf(scheme)
+ } 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 20cfd1721..6d2c7ec15 100644
--- a/store/storetest/mocks/SqlStore.go
+++ b/store/storetest/mocks/SqlStore.go
@@ -143,6 +143,20 @@ func (_m *SqlStore) CreateColumnIfNotExists(tableName string, columnName string,
return r0
}
+// CreateColumnIfNotExistsNoDefault provides a mock function with given fields: tableName, columnName, mySqlColType, postgresColType
+func (_m *SqlStore) CreateColumnIfNotExistsNoDefault(tableName string, columnName string, mySqlColType string, postgresColType string) bool {
+ ret := _m.Called(tableName, columnName, mySqlColType, postgresColType)
+
+ var r0 bool
+ if rf, ok := ret.Get(0).(func(string, string, string, string) bool); ok {
+ r0 = rf(tableName, columnName, mySqlColType, postgresColType)
+ } else {
+ r0 = ret.Get(0).(bool)
+ }
+
+ return r0
+}
+
// CreateCompositeIndexIfNotExists provides a mock function with given fields: indexName, tableName, columnNames
func (_m *SqlStore) CreateCompositeIndexIfNotExists(indexName string, tableName string, columnNames []string) bool {
ret := _m.Called(indexName, tableName, columnNames)
@@ -554,6 +568,22 @@ func (_m *SqlStore) Role() store.RoleStore {
return r0
}
+// Scheme provides a mock function with given fields:
+func (_m *SqlStore) Scheme() store.SchemeStore {
+ ret := _m.Called()
+
+ var r0 store.SchemeStore
+ if rf, ok := ret.Get(0).(func() store.SchemeStore); ok {
+ r0 = rf()
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).(store.SchemeStore)
+ }
+ }
+
+ 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 a0438b66e..5af15d125 100644
--- a/store/storetest/mocks/Store.go
+++ b/store/storetest/mocks/Store.go
@@ -299,6 +299,22 @@ func (_m *Store) Role() store.RoleStore {
return r0
}
+// Scheme provides a mock function with given fields:
+func (_m *Store) Scheme() store.SchemeStore {
+ ret := _m.Called()
+
+ var r0 store.SchemeStore
+ if rf, ok := ret.Get(0).(func() store.SchemeStore); ok {
+ r0 = rf()
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).(store.SchemeStore)
+ }
+ }
+
+ return r0
+}
+
// Session provides a mock function with given fields:
func (_m *Store) Session() store.SessionStore {
ret := _m.Called()
diff --git a/store/storetest/mocks/TeamStore.go b/store/storetest/mocks/TeamStore.go
index 993c9b86f..ef5529a1f 100644
--- a/store/storetest/mocks/TeamStore.go
+++ b/store/storetest/mocks/TeamStore.go
@@ -237,6 +237,22 @@ func (_m *TeamStore) GetMembersByIds(teamId string, userIds []string) store.Stor
return r0
}
+// GetTeamsByScheme provides a mock function with given fields: schemeId, offset, limit
+func (_m *TeamStore) GetTeamsByScheme(schemeId string, offset int, limit int) store.StoreChannel {
+ ret := _m.Called(schemeId, offset, limit)
+
+ var r0 store.StoreChannel
+ if rf, ok := ret.Get(0).(func(string, int, int) store.StoreChannel); ok {
+ r0 = rf(schemeId, offset, limit)
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).(store.StoreChannel)
+ }
+ }
+
+ return r0
+}
+
// GetTeamsByUserId provides a mock function with given fields: userId
func (_m *TeamStore) GetTeamsByUserId(userId string) store.StoreChannel {
ret := _m.Called(userId)
@@ -285,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)
@@ -349,6 +381,22 @@ func (_m *TeamStore) RemoveMember(teamId string, userId string) store.StoreChann
return r0
}
+// ResetAllTeamSchemes provides a mock function with given fields:
+func (_m *TeamStore) ResetAllTeamSchemes() 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: team
func (_m *TeamStore) Save(team *model.Team) store.StoreChannel {
ret := _m.Called(team)
diff --git a/store/storetest/role_store.go b/store/storetest/role_store.go
index e51c32622..1618b6c6d 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("Delete", func(t *testing.T) { testRoleStoreDelete(t, ss) })
t.Run("PermanentDeleteAll", func(t *testing.T) { testRoleStorePermanentDeleteAll(t, ss) })
}
@@ -244,6 +245,49 @@ func testRoleStoreGetByNames(t *testing.T, ss store.Store) {
assert.NotContains(t, roles6, d3)
}
+func testRoleStoreDelete(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)
+
+ // Check the role is there.
+ res2 := <-ss.Role().Get(d1.Id)
+ assert.Nil(t, res2.Err)
+
+ // Delete the role.
+ res3 := <-ss.Role().Delete(d1.Id)
+ assert.Nil(t, res3.Err)
+
+ // Check the role is deleted there.
+ res4 := <-ss.Role().Get(d1.Id)
+ assert.Nil(t, res4.Err)
+ d2 := res4.Data.(*model.Role)
+ assert.NotZero(t, d2.DeleteAt)
+
+ res5 := <-ss.Role().GetByName(d1.Name)
+ assert.Nil(t, res5.Err)
+ d3 := res5.Data.(*model.Role)
+ assert.NotZero(t, d3.DeleteAt)
+
+ // Try and delete a role that does not exist.
+ res6 := <-ss.Role().Delete(model.NewId())
+ assert.NotNil(t, res6.Err)
+}
+
func testRoleStorePermanentDeleteAll(t *testing.T, ss store.Store) {
r1 := &model.Role{
Name: model.NewId(),
@@ -256,6 +300,7 @@ func testRoleStorePermanentDeleteAll(t *testing.T, ss store.Store) {
},
SchemeManaged: false,
}
+
r2 := &model.Role{
Name: model.NewId(),
DisplayName: model.NewId(),
diff --git a/store/storetest/scheme_store.go b/store/storetest/scheme_store.go
new file mode 100644
index 000000000..39920c109
--- /dev/null
+++ b/store/storetest/scheme_store.go
@@ -0,0 +1,425 @@
+// 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 TestSchemeStore(t *testing.T, ss store.Store) {
+ createDefaultRoles(t, ss)
+
+ t.Run("Save", func(t *testing.T) { testSchemeStoreSave(t, ss) })
+ t.Run("Get", func(t *testing.T) { testSchemeStoreGet(t, ss) })
+ t.Run("GetAllPage", func(t *testing.T) { testSchemeStoreGetAllPage(t, ss) })
+ t.Run("Delete", func(t *testing.T) { testSchemeStoreDelete(t, ss) })
+ t.Run("PermanentDeleteAll", func(t *testing.T) { testSchemeStorePermanentDeleteAll(t, ss) })
+}
+
+func createDefaultRoles(t *testing.T, ss store.Store) {
+ <-ss.Role().Save(&model.Role{
+ Name: model.TEAM_ADMIN_ROLE_ID,
+ DisplayName: model.TEAM_ADMIN_ROLE_ID,
+ Permissions: []string{
+ model.PERMISSION_EDIT_OTHERS_POSTS.Id,
+ model.PERMISSION_DELETE_OTHERS_POSTS.Id,
+ },
+ })
+
+ <-ss.Role().Save(&model.Role{
+ Name: model.TEAM_USER_ROLE_ID,
+ DisplayName: model.TEAM_USER_ROLE_ID,
+ Permissions: []string{
+ model.PERMISSION_VIEW_TEAM.Id,
+ model.PERMISSION_ADD_USER_TO_TEAM.Id,
+ },
+ })
+
+ <-ss.Role().Save(&model.Role{
+ Name: model.CHANNEL_ADMIN_ROLE_ID,
+ DisplayName: model.CHANNEL_ADMIN_ROLE_ID,
+ Permissions: []string{
+ model.PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS.Id,
+ model.PERMISSION_MANAGE_PRIVATE_CHANNEL_MEMBERS.Id,
+ },
+ })
+
+ <-ss.Role().Save(&model.Role{
+ Name: model.CHANNEL_USER_ROLE_ID,
+ DisplayName: model.CHANNEL_USER_ROLE_ID,
+ Permissions: []string{
+ model.PERMISSION_READ_CHANNEL.Id,
+ model.PERMISSION_CREATE_POST.Id,
+ },
+ })
+}
+
+func testSchemeStoreSave(t *testing.T, ss store.Store) {
+ // Save a new scheme.
+ s1 := &model.Scheme{
+ DisplayName: model.NewId(),
+ Name: model.NewId(),
+ Description: model.NewId(),
+ Scope: model.SCHEME_SCOPE_TEAM,
+ }
+
+ // Check all fields saved correctly.
+ res1 := <-ss.Scheme().Save(s1)
+ assert.Nil(t, res1.Err)
+ d1 := res1.Data.(*model.Scheme)
+ assert.Len(t, d1.Id, 26)
+ assert.Equal(t, s1.DisplayName, d1.DisplayName)
+ assert.Equal(t, s1.Name, d1.Name)
+ assert.Equal(t, s1.Description, d1.Description)
+ assert.NotZero(t, d1.CreateAt)
+ assert.NotZero(t, d1.UpdateAt)
+ assert.Zero(t, d1.DeleteAt)
+ assert.Equal(t, s1.Scope, d1.Scope)
+ assert.Len(t, d1.DefaultTeamAdminRole, 26)
+ assert.Len(t, d1.DefaultTeamUserRole, 26)
+ assert.Len(t, d1.DefaultChannelAdminRole, 26)
+ assert.Len(t, d1.DefaultChannelUserRole, 26)
+
+ // Check the default roles were created correctly.
+ roleRes1 := <-ss.Role().Get(d1.DefaultTeamAdminRole)
+ assert.Nil(t, roleRes1.Err)
+ role1 := roleRes1.Data.(*model.Role)
+ assert.Equal(t, role1.Permissions, []string{"edit_others_posts", "delete_others_posts"})
+ assert.True(t, role1.SchemeManaged)
+
+ roleRes2 := <-ss.Role().Get(d1.DefaultTeamUserRole)
+ assert.Nil(t, roleRes2.Err)
+ role2 := roleRes2.Data.(*model.Role)
+ assert.Equal(t, role2.Permissions, []string{"view_team", "add_user_to_team"})
+ assert.True(t, role2.SchemeManaged)
+
+ roleRes3 := <-ss.Role().Get(d1.DefaultChannelAdminRole)
+ assert.Nil(t, roleRes3.Err)
+ role3 := roleRes3.Data.(*model.Role)
+ assert.Equal(t, role3.Permissions, []string{"manage_public_channel_members", "manage_private_channel_members"})
+ assert.True(t, role3.SchemeManaged)
+
+ roleRes4 := <-ss.Role().Get(d1.DefaultChannelUserRole)
+ assert.Nil(t, roleRes4.Err)
+ role4 := roleRes4.Data.(*model.Role)
+ assert.Equal(t, role4.Permissions, []string{"read_channel", "create_post"})
+ assert.True(t, role4.SchemeManaged)
+
+ // Change the scheme description and update.
+ d1.Description = model.NewId()
+
+ res2 := <-ss.Scheme().Save(d1)
+ assert.Nil(t, res2.Err)
+ d2 := res2.Data.(*model.Scheme)
+ assert.Equal(t, d1.Id, d2.Id)
+ assert.Equal(t, s1.DisplayName, d2.DisplayName)
+ assert.Equal(t, s1.Name, d2.Name)
+ assert.Equal(t, d1.Description, d2.Description)
+ assert.NotZero(t, d2.CreateAt)
+ assert.NotZero(t, d2.UpdateAt)
+ assert.Zero(t, d2.DeleteAt)
+ assert.Equal(t, s1.Scope, d2.Scope)
+ assert.Equal(t, d1.DefaultTeamAdminRole, d2.DefaultTeamAdminRole)
+ assert.Equal(t, d1.DefaultTeamUserRole, d2.DefaultTeamUserRole)
+ assert.Equal(t, d1.DefaultChannelAdminRole, d2.DefaultChannelAdminRole)
+ assert.Equal(t, d1.DefaultChannelUserRole, d2.DefaultChannelUserRole)
+
+ // Try saving one with an invalid ID set.
+ s3 := &model.Scheme{
+ Id: model.NewId(),
+ DisplayName: model.NewId(),
+ Name: model.NewId(),
+ Description: model.NewId(),
+ Scope: model.SCHEME_SCOPE_TEAM,
+ }
+
+ res3 := <-ss.Scheme().Save(s3)
+ assert.NotNil(t, res3.Err)
+}
+
+func testSchemeStoreGet(t *testing.T, ss store.Store) {
+ // Save a scheme to test with.
+ s1 := &model.Scheme{
+ DisplayName: model.NewId(),
+ Name: model.NewId(),
+ Description: model.NewId(),
+ Scope: model.SCHEME_SCOPE_TEAM,
+ }
+
+ res1 := <-ss.Scheme().Save(s1)
+ assert.Nil(t, res1.Err)
+ d1 := res1.Data.(*model.Scheme)
+ assert.Len(t, d1.Id, 26)
+
+ // Get a valid scheme
+ res2 := <-ss.Scheme().Get(d1.Id)
+ assert.Nil(t, res2.Err)
+ d2 := res1.Data.(*model.Scheme)
+ assert.Equal(t, d1.Id, d2.Id)
+ assert.Equal(t, s1.DisplayName, d2.DisplayName)
+ assert.Equal(t, s1.Name, d2.Name)
+ assert.Equal(t, d1.Description, d2.Description)
+ assert.NotZero(t, d2.CreateAt)
+ assert.NotZero(t, d2.UpdateAt)
+ assert.Zero(t, d2.DeleteAt)
+ assert.Equal(t, s1.Scope, d2.Scope)
+ assert.Equal(t, d1.DefaultTeamAdminRole, d2.DefaultTeamAdminRole)
+ assert.Equal(t, d1.DefaultTeamUserRole, d2.DefaultTeamUserRole)
+ assert.Equal(t, d1.DefaultChannelAdminRole, d2.DefaultChannelAdminRole)
+ assert.Equal(t, d1.DefaultChannelUserRole, d2.DefaultChannelUserRole)
+
+ // Get an invalid scheme
+ res3 := <-ss.Scheme().Get(model.NewId())
+ assert.NotNil(t, res3.Err)
+}
+
+func testSchemeStoreGetAllPage(t *testing.T, ss store.Store) {
+ // Save a scheme to test with.
+ schemes := []*model.Scheme{
+ {
+ DisplayName: model.NewId(),
+ Name: model.NewId(),
+ Description: model.NewId(),
+ Scope: model.SCHEME_SCOPE_TEAM,
+ },
+ {
+ DisplayName: model.NewId(),
+ Name: model.NewId(),
+ Description: model.NewId(),
+ Scope: model.SCHEME_SCOPE_CHANNEL,
+ },
+ {
+ DisplayName: model.NewId(),
+ Name: model.NewId(),
+ Description: model.NewId(),
+ Scope: model.SCHEME_SCOPE_TEAM,
+ },
+ {
+ DisplayName: model.NewId(),
+ Name: model.NewId(),
+ Description: model.NewId(),
+ Scope: model.SCHEME_SCOPE_CHANNEL,
+ },
+ }
+
+ for _, scheme := range schemes {
+ store.Must(ss.Scheme().Save(scheme))
+ }
+
+ r1 := <-ss.Scheme().GetAllPage("", 0, 2)
+ assert.Nil(t, r1.Err)
+ s1 := r1.Data.([]*model.Scheme)
+ assert.Len(t, s1, 2)
+
+ r2 := <-ss.Scheme().GetAllPage("", 2, 2)
+ assert.Nil(t, r2.Err)
+ s2 := r2.Data.([]*model.Scheme)
+ assert.Len(t, s2, 2)
+ assert.NotEqual(t, s1[0].DisplayName, s2[0].DisplayName)
+ assert.NotEqual(t, s1[0].DisplayName, s2[1].DisplayName)
+ assert.NotEqual(t, s1[1].DisplayName, s2[0].DisplayName)
+ assert.NotEqual(t, s1[1].DisplayName, s2[1].DisplayName)
+ assert.NotEqual(t, s1[0].Name, s2[0].Name)
+ assert.NotEqual(t, s1[0].Name, s2[1].Name)
+ assert.NotEqual(t, s1[1].Name, s2[0].Name)
+ assert.NotEqual(t, s1[1].Name, s2[1].Name)
+
+ r3 := <-ss.Scheme().GetAllPage("team", 0, 1000)
+ assert.Nil(t, r3.Err)
+ s3 := r3.Data.([]*model.Scheme)
+ assert.NotZero(t, len(s3))
+ for _, s := range s3 {
+ assert.Equal(t, "team", s.Scope)
+ }
+
+ r4 := <-ss.Scheme().GetAllPage("channel", 0, 1000)
+ assert.Nil(t, r4.Err)
+ s4 := r4.Data.([]*model.Scheme)
+ assert.NotZero(t, len(s4))
+ for _, s := range s4 {
+ assert.Equal(t, "channel", s.Scope)
+ }
+}
+
+func testSchemeStoreDelete(t *testing.T, ss store.Store) {
+ // Save a new scheme.
+ s1 := &model.Scheme{
+ DisplayName: model.NewId(),
+ Name: model.NewId(),
+ Description: model.NewId(),
+ Scope: model.SCHEME_SCOPE_TEAM,
+ }
+
+ // Check all fields saved correctly.
+ res1 := <-ss.Scheme().Save(s1)
+ assert.Nil(t, res1.Err)
+ d1 := res1.Data.(*model.Scheme)
+ assert.Len(t, d1.Id, 26)
+ assert.Equal(t, s1.DisplayName, d1.DisplayName)
+ assert.Equal(t, s1.Name, d1.Name)
+ assert.Equal(t, s1.Description, d1.Description)
+ assert.NotZero(t, d1.CreateAt)
+ assert.NotZero(t, d1.UpdateAt)
+ assert.Zero(t, d1.DeleteAt)
+ assert.Equal(t, s1.Scope, d1.Scope)
+ assert.Len(t, d1.DefaultTeamAdminRole, 26)
+ assert.Len(t, d1.DefaultTeamUserRole, 26)
+ assert.Len(t, d1.DefaultChannelAdminRole, 26)
+ assert.Len(t, d1.DefaultChannelUserRole, 26)
+
+ // Check the default roles were created correctly.
+ roleRes1 := <-ss.Role().Get(d1.DefaultTeamAdminRole)
+ assert.Nil(t, roleRes1.Err)
+ role1 := roleRes1.Data.(*model.Role)
+ assert.Equal(t, role1.Permissions, []string{"edit_others_posts", "delete_others_posts"})
+ assert.True(t, role1.SchemeManaged)
+
+ roleRes2 := <-ss.Role().Get(d1.DefaultTeamUserRole)
+ assert.Nil(t, roleRes2.Err)
+ role2 := roleRes2.Data.(*model.Role)
+ assert.Equal(t, role2.Permissions, []string{"view_team", "add_user_to_team"})
+ assert.True(t, role2.SchemeManaged)
+
+ roleRes3 := <-ss.Role().Get(d1.DefaultChannelAdminRole)
+ assert.Nil(t, roleRes3.Err)
+ role3 := roleRes3.Data.(*model.Role)
+ assert.Equal(t, role3.Permissions, []string{"manage_public_channel_members", "manage_private_channel_members"})
+ assert.True(t, role3.SchemeManaged)
+
+ roleRes4 := <-ss.Role().Get(d1.DefaultChannelUserRole)
+ assert.Nil(t, roleRes4.Err)
+ role4 := roleRes4.Data.(*model.Role)
+ assert.Equal(t, role4.Permissions, []string{"read_channel", "create_post"})
+ assert.True(t, role4.SchemeManaged)
+
+ // Delete the scheme.
+ res2 := <-ss.Scheme().Delete(d1.Id)
+ if !assert.Nil(t, res2.Err) {
+ t.Fatal(res2.Err)
+ }
+ d2 := res2.Data.(*model.Scheme)
+ assert.NotZero(t, d2.DeleteAt)
+
+ // Check that the roles are deleted too.
+ roleRes5 := <-ss.Role().Get(d1.DefaultTeamAdminRole)
+ assert.Nil(t, roleRes5.Err)
+ role5 := roleRes5.Data.(*model.Role)
+ assert.NotZero(t, role5.DeleteAt)
+
+ roleRes6 := <-ss.Role().Get(d1.DefaultTeamUserRole)
+ assert.Nil(t, roleRes6.Err)
+ role6 := roleRes6.Data.(*model.Role)
+ assert.NotZero(t, role6.DeleteAt)
+
+ roleRes7 := <-ss.Role().Get(d1.DefaultChannelAdminRole)
+ assert.Nil(t, roleRes7.Err)
+ role7 := roleRes7.Data.(*model.Role)
+ assert.NotZero(t, role7.DeleteAt)
+
+ roleRes8 := <-ss.Role().Get(d1.DefaultChannelUserRole)
+ assert.Nil(t, roleRes8.Err)
+ role8 := roleRes8.Data.(*model.Role)
+ assert.NotZero(t, role8.DeleteAt)
+
+ // Try deleting a scheme that does not exist.
+ res3 := <-ss.Scheme().Delete(model.NewId())
+ assert.NotNil(t, res3.Err)
+
+ // Try deleting a team scheme that's in use.
+ s4 := &model.Scheme{
+ DisplayName: model.NewId(),
+ Name: model.NewId(),
+ Description: model.NewId(),
+ Scope: model.SCHEME_SCOPE_TEAM,
+ }
+ res4 := <-ss.Scheme().Save(s4)
+ assert.Nil(t, res4.Err)
+ d4 := res4.Data.(*model.Scheme)
+
+ t4 := &model.Team{
+ Name: model.NewId(),
+ DisplayName: model.NewId(),
+ Email: model.NewId() + "@nowhere.com",
+ Type: model.TEAM_OPEN,
+ SchemeId: &d4.Id,
+ }
+ tres4 := <-ss.Team().Save(t4)
+ assert.Nil(t, tres4.Err)
+ t4 = tres4.Data.(*model.Team)
+
+ sres4 := <-ss.Scheme().Delete(d4.Id)
+ assert.Nil(t, sres4.Err)
+
+ tres5 := <-ss.Team().Get(t4.Id)
+ assert.Nil(t, tres5.Err)
+ t5 := tres5.Data.(*model.Team)
+ assert.Equal(t, "", *t5.SchemeId)
+
+ // Try deleting a channel scheme that's in use.
+ s5 := &model.Scheme{
+ DisplayName: model.NewId(),
+ Name: model.NewId(),
+ Description: model.NewId(),
+ Scope: model.SCHEME_SCOPE_CHANNEL,
+ }
+ res5 := <-ss.Scheme().Save(s5)
+ assert.Nil(t, res5.Err)
+ d5 := res5.Data.(*model.Scheme)
+
+ c5 := &model.Channel{
+ TeamId: model.NewId(),
+ DisplayName: model.NewId(),
+ Name: model.NewId(),
+ Type: model.CHANNEL_OPEN,
+ SchemeId: &d5.Id,
+ }
+ cres5 := <-ss.Channel().Save(c5, -1)
+ assert.Nil(t, cres5.Err)
+ c5 = cres5.Data.(*model.Channel)
+
+ sres5 := <-ss.Scheme().Delete(d5.Id)
+ assert.Nil(t, sres5.Err)
+
+ cres6 := <-ss.Channel().Get(c5.Id, true)
+ assert.Nil(t, cres6.Err)
+ c6 := cres6.Data.(*model.Channel)
+ assert.Equal(t, "", *c6.SchemeId)
+}
+
+func testSchemeStorePermanentDeleteAll(t *testing.T, ss store.Store) {
+ s1 := &model.Scheme{
+ Name: model.NewId(),
+ DisplayName: model.NewId(),
+ Description: model.NewId(),
+ Scope: model.SCHEME_SCOPE_TEAM,
+ }
+
+ s2 := &model.Scheme{
+ Name: model.NewId(),
+ DisplayName: model.NewId(),
+ Description: model.NewId(),
+ Scope: model.SCHEME_SCOPE_CHANNEL,
+ }
+
+ s1 = (<-ss.Scheme().Save(s1)).Data.(*model.Scheme)
+ s2 = (<-ss.Scheme().Save(s2)).Data.(*model.Scheme)
+
+ res := <-ss.Scheme().PermanentDeleteAll()
+ assert.Nil(t, res.Err)
+
+ res1 := <-ss.Scheme().Get(s1.Id)
+ assert.NotNil(t, res1.Err)
+
+ res2 := <-ss.Scheme().Get(s2.Id)
+ assert.NotNil(t, res2.Err)
+
+ res3 := <-ss.Scheme().GetAllPage("", 0, 100000)
+ assert.Nil(t, res3.Err)
+ assert.Len(t, res3.Data.([]*model.Scheme), 0)
+}
diff --git a/store/storetest/store.go b/store/storetest/store.go
index 44f426075..677a63101 100644
--- a/store/storetest/store.go
+++ b/store/storetest/store.go
@@ -44,6 +44,7 @@ type Store struct {
PluginStore mocks.PluginStore
ChannelMemberHistoryStore mocks.ChannelMemberHistoryStore
RoleStore mocks.RoleStore
+ SchemeStore mocks.SchemeStore
}
func (s *Store) Team() store.TeamStore { return &s.TeamStore }
@@ -70,6 +71,7 @@ func (s *Store) Job() store.JobStore { return &s.JobSt
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) Scheme() store.SchemeStore { return &s.SchemeStore }
func (s *Store) ChannelMemberHistory() store.ChannelMemberHistoryStore {
return &s.ChannelMemberHistoryStore
}
@@ -107,5 +109,6 @@ func (s *Store) AssertExpectations(t mock.TestingT) bool {
&s.ChannelMemberHistoryStore,
&s.PluginStore,
&s.RoleStore,
+ &s.SchemeStore,
)
}
diff --git a/store/storetest/team_store.go b/store/storetest/team_store.go
index cab06f87f..b209b48c4 100644
--- a/store/storetest/team_store.go
+++ b/store/storetest/team_store.go
@@ -4,14 +4,19 @@
package storetest
import (
+ "strings"
"testing"
"time"
+ "github.com/stretchr/testify/assert"
+
"github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/store"
)
func TestTeamStore(t *testing.T, ss store.Store) {
+ createDefaultRoles(t, ss)
+
t.Run("Save", func(t *testing.T) { testTeamStoreSave(t, ss) })
t.Run("Update", func(t *testing.T) { testTeamStoreUpdate(t, ss) })
t.Run("UpdateDisplayName", func(t *testing.T) { testTeamStoreUpdateDisplayName(t, ss) })
@@ -34,6 +39,9 @@ func TestTeamStore(t *testing.T, ss store.Store) {
t.Run("GetChannelUnreadsForAllTeams", func(t *testing.T) { testGetChannelUnreadsForAllTeams(t, ss) })
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) })
+ t.Run("ResetAllTeamSchemes", func(t *testing.T) { testResetAllTeamSchemes(t, ss) })
}
func testTeamStoreSave(t *testing.T, ss store.Store) {
@@ -1029,3 +1037,179 @@ func testUpdateLastTeamIconUpdate(t *testing.T, ss store.Store) {
t.Fatal("LastTeamIconUpdate not updated")
}
}
+
+func testGetTeamsByScheme(t *testing.T, ss store.Store) {
+ // Create some schemes.
+ s1 := &model.Scheme{
+ DisplayName: model.NewId(),
+ Name: model.NewId(),
+ Description: model.NewId(),
+ Scope: model.SCHEME_SCOPE_TEAM,
+ }
+
+ s2 := &model.Scheme{
+ DisplayName: model.NewId(),
+ Name: model.NewId(),
+ Description: model.NewId(),
+ Scope: model.SCHEME_SCOPE_TEAM,
+ }
+
+ s1 = (<-ss.Scheme().Save(s1)).Data.(*model.Scheme)
+ s2 = (<-ss.Scheme().Save(s2)).Data.(*model.Scheme)
+
+ // Create and save some teams.
+ t1 := &model.Team{
+ Name: model.NewId(),
+ DisplayName: model.NewId(),
+ Email: model.NewId() + "@nowhere.com",
+ Type: model.TEAM_OPEN,
+ SchemeId: &s1.Id,
+ }
+
+ t2 := &model.Team{
+ Name: model.NewId(),
+ DisplayName: model.NewId(),
+ Email: model.NewId() + "@nowhere.com",
+ Type: model.TEAM_OPEN,
+ SchemeId: &s1.Id,
+ }
+
+ t3 := &model.Team{
+ Name: model.NewId(),
+ DisplayName: model.NewId(),
+ Email: model.NewId() + "@nowhere.com",
+ Type: model.TEAM_OPEN,
+ }
+
+ t1 = (<-ss.Team().Save(t1)).Data.(*model.Team)
+ t2 = (<-ss.Team().Save(t2)).Data.(*model.Team)
+ t3 = (<-ss.Team().Save(t3)).Data.(*model.Team)
+
+ // Get the teams by a valid Scheme ID.
+ res1 := <-ss.Team().GetTeamsByScheme(s1.Id, 0, 100)
+ assert.Nil(t, res1.Err)
+ d1 := res1.Data.([]*model.Team)
+ assert.Len(t, d1, 2)
+
+ // Get the teams by a valid Scheme ID where there aren't any matching Teams.
+ res2 := <-ss.Team().GetTeamsByScheme(s2.Id, 0, 100)
+ assert.Nil(t, res2.Err)
+ d2 := res2.Data.([]*model.Team)
+ assert.Len(t, d2, 0)
+
+ // Get the teams by an invalid Scheme ID.
+ res3 := <-ss.Team().GetTeamsByScheme(model.NewId(), 0, 100)
+ assert.Nil(t, res3.Err)
+ 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)
+}
+
+func testResetAllTeamSchemes(t *testing.T, ss store.Store) {
+ s1 := &model.Scheme{
+ Name: model.NewId(),
+ DisplayName: model.NewId(),
+ Description: model.NewId(),
+ Scope: model.SCHEME_SCOPE_TEAM,
+ }
+ s1 = (<-ss.Scheme().Save(s1)).Data.(*model.Scheme)
+
+ t1 := &model.Team{
+ Name: model.NewId(),
+ DisplayName: model.NewId(),
+ Email: model.NewId() + "@nowhere.com",
+ Type: model.TEAM_OPEN,
+ SchemeId: &s1.Id,
+ }
+
+ t2 := &model.Team{
+ Name: model.NewId(),
+ DisplayName: model.NewId(),
+ Email: model.NewId() + "@nowhere.com",
+ Type: model.TEAM_OPEN,
+ SchemeId: &s1.Id,
+ }
+
+ t1 = (<-ss.Team().Save(t1)).Data.(*model.Team)
+ t2 = (<-ss.Team().Save(t2)).Data.(*model.Team)
+
+ assert.Equal(t, s1.Id, *t1.SchemeId)
+ assert.Equal(t, s1.Id, *t2.SchemeId)
+
+ res := <-ss.Team().ResetAllTeamSchemes()
+ assert.Nil(t, res.Err)
+
+ t1 = (<-ss.Team().Get(t1.Id)).Data.(*model.Team)
+ t2 = (<-ss.Team().Get(t2.Id)).Data.(*model.Team)
+
+ assert.Equal(t, "", *t1.SchemeId)
+ assert.Equal(t, "", *t2.SchemeId)
+}
diff --git a/utils/config.go b/utils/config.go
index 2e6f4182f..8feb7d882 100644
--- a/utils/config.go
+++ b/utils/config.go
@@ -554,6 +554,8 @@ func GenerateClientConfig(c *model.Config, diagnosticId string, license *model.L
hasImageProxy := c.ServiceSettings.ImageProxyType != nil && *c.ServiceSettings.ImageProxyType != "" && c.ServiceSettings.ImageProxyURL != nil && *c.ServiceSettings.ImageProxyURL != ""
props["HasImageProxy"] = strconv.FormatBool(hasImageProxy)
+ props["RunJobs"] = strconv.FormatBool(*c.JobSettings.RunJobs)
+
// Set default values for all options that require a license.
props["ExperimentalHideTownSquareinLHS"] = "false"
props["ExperimentalTownSquareIsReadOnly"] = "false"
diff --git a/utils/license.go b/utils/license.go
index 832d41557..8bb214734 100644
--- a/utils/license.go
+++ b/utils/license.go
@@ -151,6 +151,7 @@ func GetClientLicense(l *model.License) map[string]string {
props["PhoneNumber"] = l.Customer.PhoneNumber
props["EmailNotificationContents"] = strconv.FormatBool(*l.Features.EmailNotificationContents)
props["MessageExport"] = strconv.FormatBool(*l.Features.MessageExport)
+ props["CustomPermissionsSchemes"] = strconv.FormatBool(*l.Features.CustomPermissionsSchemes)
}
return props
diff --git a/web/context.go b/web/context.go
index 711d241ae..8f79421e5 100644
--- a/web/context.go
+++ b/web/context.go
@@ -490,6 +490,17 @@ func (c *Context) RequireRoleId() *Context {
return c
}
+func (c *Context) RequireSchemeId() *Context {
+ if c.Err != nil {
+ return c
+ }
+
+ if len(c.Params.SchemeId) != 26 {
+ c.SetInvalidUrlParam("scheme_id")
+ }
+ return c
+}
+
func (c *Context) RequireRoleName() *Context {
if c.Err != nil {
return c
diff --git a/web/params.go b/web/params.go
index e97b206ab..2c30ba4c0 100644
--- a/web/params.go
+++ b/web/params.go
@@ -47,6 +47,8 @@ type Params struct {
ActionId string
RoleId string
RoleName string
+ SchemeId string
+ Scope string
Page int
PerPage int
LogsPerPage int
@@ -167,6 +169,12 @@ func ParamsFromRequest(r *http.Request) *Params {
params.RoleName = val
}
+ if val, ok := props["scheme_id"]; ok {
+ params.SchemeId = val
+ }
+
+ params.Scope = query.Get("scope")
+
if val, err := strconv.Atoi(query.Get("page")); err != nil || val < 0 {
params.Page = PAGE_DEFAULT
} else {
diff --git a/web/web_test.go b/web/web_test.go
index 9b6230013..b53ed9618 100644
--- a/web/web_test.go
+++ b/web/web_test.go
@@ -61,6 +61,7 @@ func Setup() *TestHelper {
ApiClient = model.NewAPIv4Client(URL)
a.DoAdvancedPermissionsMigration()
+ a.DoEmojisPermissionsMigration()
a.Srv.Store.MarkSystemRanUnitTests()