diff options
84 files changed, 7850 insertions, 182 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..952c21df3 100644 --- a/api4/apitestlib.go +++ b/api4/apitestlib.go @@ -767,7 +767,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 +783,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/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/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..508602d0e 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,53 @@ 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 { + 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 + } + + 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 = &scheme.Id + + _, 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..b47a8b650 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,71 @@ 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 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..e5a496c6b 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" @@ -93,6 +94,8 @@ type App struct { clientConfig map[string]string clientConfigHash string diagnosticId string + + phase2PermissionsMigrationComplete bool } var appCount = 0 @@ -321,6 +324,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) { @@ -415,6 +424,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 +592,14 @@ 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 +} diff --git a/app/apptestlib.go b/app/apptestlib.go index 7fc78c9c9..ec4992a75 100644 --- a/app/apptestlib.go +++ b/app/apptestlib.go @@ -316,6 +316,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) 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..70b8cc689 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 @@ -23,3 +46,138 @@ func (a *App) ResetPermissionsSystem() *model.AppError { 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/permissions.go b/cmd/mattermost/commands/permissions.go index 52cae0ecb..e1bb34963 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) } @@ -62,3 +86,45 @@ func resetPermissionsCmdF(command *cobra.Command, args []string) error { 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/i18n/en.json b/i18n/en.json index 1f4d55476..d631e7e1d 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" }, @@ -2511,6 +2575,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" }, @@ -6707,6 +6779,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" }, @@ -6747,6 +6827,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..1b9110425 --- /dev/null +++ b/migrations/migrationstestlib.go @@ -0,0 +1,424 @@ +// 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.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/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..93399d7d9 100644 --- a/store/sqlstore/upgrade.go +++ b/store/sqlstore/upgrade.go @@ -425,8 +425,20 @@ func UpgradeDatabaseToVersion410(sqlStore SqlStore) { } func UpgradeDatabaseToVersion50(sqlStore SqlStore) { - // TODO: Uncomment following condition when version 3.10.0 is released + // 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 d3fd733b7..c85da7067 100644 --- a/utils/config.go +++ b/utils/config.go @@ -553,6 +553,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 { |