From cd55c44c8fd8f61cdb7cbfb57a588be82c7aa0ab Mon Sep 17 00:00:00 2001 From: George Goldberg Date: Fri, 20 Apr 2018 19:49:13 +0100 Subject: MM-8796: Full implementation of "Schemes" in Store/Model/App layers. (#8357) * Add Scheme model and stub store. * Port ChannelStore to be Scheme aware. * Make almost all the API/APP layer work with ChannelSchemes. Only thing still hacky is UpdateChannelMemberRoles(). * Add basic SchemeStore implementation. * Migrate UpdateChannelMemberRoles properly and fix tests. * Update store tests and mocks so they work. * Include creating default roles in Scheme create store function. * Implement role deletion and start scheme deletion. * Only use non-deleted roles for authorization. * Add GetByScheme method to Team store. * Add GetChannelsByScheme. * Update store mocks. * Implement scheme deletion in the store. * Rename is valid function. * Add offset and limit to queries to fetch teams and channels by scheme. * Fix queries. * Implement scheme awareness in Team store and add a migration. * Tidy up ChannelStore mapping functions and add exhaustive unit tests. * Add all missing i18n. * Proper tests for TeamStore internal functions and fix them. * Make additional TeamMember fields nullable. * Make new ChannelMember fields nullable. * Create new nullable columns without defaults. * Make new fields in large tables nullalble. * Fix empty list of TeamMembers. * Deduplicate SQL queries. * Fix spelling. * Fix review comment. * More review fixes. * More review fixes. --- api/apitestlib.go | 32 +- api4/apitestlib.go | 30 +- api4/channel_test.go | 2 +- api4/team_test.go | 4 +- app/authorization.go | 4 + app/channel.go | 80 +- app/channel_test.go | 4 +- app/scheme.go | 14 + app/team.go | 76 +- i18n/en.json | 80 ++ model/channel.go | 29 +- model/channel_member.go | 19 +- model/cluster_message.go | 1 + model/role.go | 22 +- model/scheme.go | 95 +++ model/team.go | 29 +- model/team_member.go | 11 +- store/layered_store.go | 34 + store/layered_store_supplier.go | 6 + store/local_cache_supplier.go | 5 + store/local_cache_supplier_roles.go | 11 + store/local_cache_supplier_schemes.go | 44 + store/redis_supplier_roles.go | 15 + store/redis_supplier_schemes.go | 25 + store/sqlstore/channel_store.go | 363 ++++++-- store/sqlstore/channel_store_test.go | 926 +++++++++++++++++++++ store/sqlstore/role_supplier.go | 80 +- store/sqlstore/scheme_store_test.go | 14 + store/sqlstore/scheme_supplier.go | 272 ++++++ store/sqlstore/store.go | 2 + store/sqlstore/supplier.go | 40 + store/sqlstore/team_store.go | 190 ++++- store/sqlstore/team_store_test.go | 367 ++++++++ store/sqlstore/upgrade.go | 13 + store/store.go | 10 + store/storetest/channel_store.go | 68 ++ store/storetest/mocks/ChannelStore.go | 16 + store/storetest/mocks/LayeredStoreDatabaseLayer.go | 108 +++ store/storetest/mocks/LayeredStoreSupplier.go | 92 ++ store/storetest/mocks/RoleStore.go | 16 + store/storetest/mocks/SchemeStore.go | 62 ++ store/storetest/mocks/SqlStore.go | 16 + store/storetest/mocks/Store.go | 16 + store/storetest/mocks/TeamStore.go | 16 + store/storetest/role_store.go | 45 + store/storetest/scheme_store.go | 303 +++++++ store/storetest/store.go | 3 + store/storetest/team_store.go | 69 ++ 48 files changed, 3604 insertions(+), 175 deletions(-) create mode 100644 app/scheme.go create mode 100644 model/scheme.go create mode 100644 store/local_cache_supplier_schemes.go create mode 100644 store/redis_supplier_schemes.go create mode 100644 store/sqlstore/scheme_store_test.go create mode 100644 store/sqlstore/scheme_supplier.go create mode 100644 store/storetest/mocks/SchemeStore.go create mode 100644 store/storetest/scheme_store.go diff --git a/api/apitestlib.go b/api/apitestlib.go index 699b0eb90..f1199ea91 100644 --- a/api/apitestlib.go +++ b/api/apitestlib.go @@ -245,28 +245,36 @@ func (me *TestHelper) LinkUserToTeam(user *model.User, team *model.Team) { 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() - l4g.Error(tmr.Err.Error()) - l4g.Close() - 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() - l4g.Error(tmr.Err.Error()) - l4g.Close() - time.Sleep(time.Second) panic(tmr.Err) } + utils.EnableDebugLogForTest() } @@ -275,7 +283,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) @@ -293,7 +301,7 @@ func (me *TestHelper) MakeUserChannelUser(user *model.User, channel *model.Chann if cmr := <-me.App.Srv.Store.Channel().GetMember(channel.Id, user.Id); cmr.Err == nil { cm := cmr.Data.(*model.ChannelMember) - cm.Roles = "channel_user" + cm.SchemeAdmin = false if sr := <-me.App.Srv.Store.Channel().UpdateMember(cm); sr.Err != nil { utils.EnableDebugLogForTest() panic(sr.Err) diff --git a/api4/apitestlib.go b/api4/apitestlib.go index 4620c5f4e..b56934c0a 100644 --- a/api4/apitestlib.go +++ b/api4/apitestlib.go @@ -765,7 +765,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) @@ -781,28 +781,36 @@ 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() - l4g.Error(tmr.Err.Error()) - l4g.Close() - 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() - l4g.Error(tmr.Err.Error()) - l4g.Close() - time.Sleep(time.Second) panic(tmr.Err) } + utils.EnableDebugLogForTest() } diff --git a/api4/channel_test.go b/api4/channel_test.go index 4c27e040a..0603afe74 100644 --- a/api4/channel_test.go +++ b/api4/channel_test.go @@ -1388,7 +1388,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. diff --git a/api4/team_test.go b/api4/team_test.go index cdf201771..3cd9d7d93 100644 --- a/api4/team_test.go +++ b/api4/team_test.go @@ -1672,7 +1672,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) @@ -1698,7 +1698,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") diff --git a/app/authorization.go b/app/authorization.go index 2187472f7..6bce9e6e7 100644 --- a/app/authorization.go +++ b/app/authorization.go @@ -199,6 +199,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 76eb4d337..c63023fb3 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 { @@ -432,6 +435,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 @@ -439,14 +475,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) @@ -591,7 +655,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 { l4g.Error("Failed to add member user_id=%v channel_id=%v err=%v", user.Id, channel.Id, result.Err) diff --git a/app/channel_test.go b/app/channel_test.go index a4e0806a6..de8a6a6a0 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) diff --git a/app/scheme.go b/app/scheme.go new file mode 100644 index 000000000..26ec6cd2a --- /dev/null +++ b/app/scheme.go @@ -0,0 +1,14 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import "github.com/mattermost/mattermost-server/model" + +func (a *App) GetScheme(id string) (*model.Scheme, *model.AppError) { + if result := <-a.Srv.Store.Scheme().Get(id); result.Err != nil { + return nil, result.Err + } else { + return result.Data.(*model.Scheme), nil + } +} diff --git a/app/team.go b/app/team.go index 47e28f2ed..3b30816be 100644 --- a/app/team.go +++ b/app/team.go @@ -141,17 +141,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 { @@ -159,14 +173,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) @@ -292,13 +334,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 { @@ -342,14 +384,10 @@ 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 { + if err := a.JoinDefaultChannels(team.Id, user, shouldBeAdmin, userRequestorId); err != nil { l4g.Error(utils.T("api.user.create_user.joining.error"), user.Id, team.Id, err) } diff --git a/i18n/en.json b/i18n/en.json index ea6314107..61b3e1bf0 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -6690,6 +6690,86 @@ "id": "store.sql_role.get_by_name.app_error", "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": "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.team_count.app_error", + "translation": "Unable to count the number of teams using this 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.channel_count.app_error", + "translation": "Unable to count the number of channels using this 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/model/channel.go b/model/channel.go index df68202d6..29ec67ed6 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/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/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..9ad153c73 --- /dev/null +++ b/model/scheme.go @@ -0,0 +1,95 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +const ( + 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"` + 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"` +} + +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 (scheme *Scheme) IsValid() bool { + if len(scheme.Id) != 26 { + return false + } + + return scheme.IsValidForCreate() +} + +func (scheme *Scheme) IsValidForCreate() bool { + if len(scheme.Name) == 0 || len(scheme.Name) > SCHEME_NAME_MAX_LENGTH { + 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 +} 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 5ef907260..d713226a9 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,36 @@ 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) + }) +} diff --git a/store/layered_store_supplier.go b/store/layered_store_supplier.go index 9a7604b20..04fa26fd3 100644 --- a/store/layered_store_supplier.go +++ b/store/layered_store_supplier.go @@ -35,5 +35,11 @@ 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 } 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..2a8f73a71 --- /dev/null +++ b/store/local_cache_supplier_schemes.go @@ -0,0 +1,44 @@ +// 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...) +} diff --git a/store/redis_supplier_roles.go b/store/redis_supplier_roles.go index 232a8c040..c4f269268 100644 --- a/store/redis_supplier_roles.go +++ b/store/redis_supplier_roles.go @@ -84,6 +84,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 { + l4g.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..4c05e9329 --- /dev/null +++ b/store/redis_supplier_schemes.go @@ -0,0 +1,25 @@ +// 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...) +} diff --git a/store/sqlstore/channel_store.go b/store/sqlstore/channel_store.go index 21785c461..0ddbc7221 100644 --- a/store/sqlstore/channel_store.go +++ b/store/sqlstore/channel_store.go @@ -13,6 +13,7 @@ import ( l4g "github.com/alecthomas/log4go" "github.com/mattermost/gorp" + "github.com/mattermost/mattermost-server/einterfaces" "github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/store" @@ -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) @@ -732,6 +928,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 @@ -750,7 +965,7 @@ func (s SqlChannelStore) SaveMember(member *model.ChannelMember) store.StoreChan if err := transaction.Commit(); err != nil { result.Err = model.NewAppError("SqlChannelStore.SaveMember", "store.sql_channel.save_member.commit_transaction.app_error", nil, err.Error(), http.StatusInternalServerError) } - // If successfull record members have changed in channel + // If successful record members have changed in channel if mu := <-s.extraUpdated(channel); mu.Err != nil { result.Err = mu.Err } @@ -770,14 +985,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 @@ -791,38 +1017,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() } }) } @@ -866,30 +1102,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 { @@ -910,17 +1153,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 @@ -1249,21 +1507,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() } }) } @@ -1455,7 +1705,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 := "" @@ -1470,11 +1720,22 @@ 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.Channel + _, 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 } }) } 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..278d1a3c4 --- /dev/null +++ b/store/sqlstore/scheme_supplier.go @@ -0,0 +1,272 @@ +// 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(64) + table.ColMap("Description").SetMaxSize(1024) + 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() + 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 + } + + // Check that the scheme isn't being used on any Teams or Channels. + if scheme.Scope == model.SCHEME_SCOPE_TEAM { + if c, err := s.GetReplica().SelectInt("SELECT COUNT(*) FROM Teams WHERE SchemeId = :SchemeId", map[string]interface{}{"SchemeId": schemeId}); err != nil { + result.Err = model.NewAppError("SqlSchemeStore.Delete", "store.sql_scheme.team_count.app_error", nil, "Id="+schemeId+", "+err.Error(), http.StatusInternalServerError) + return result + } else { + if c > 0 { + result.Err = model.NewAppError("SqlSchemeStore.Delete", "store.sql_scheme.delete.scheme_in_use.app_error", nil, "Id="+schemeId, http.StatusInternalServerError) + return result + } + } + } else if scheme.Scope == model.SCHEME_SCOPE_CHANNEL { + if c, err := s.GetReplica().SelectInt("SELECT COUNT(*) FROM Channels WHERE SchemeId = :SchemeId", map[string]interface{}{"SchemeId": schemeId}); err != nil { + result.Err = model.NewAppError("SqlSchemeStore.Delete", "store.sql_scheme.channel_count.app_error", nil, "Id="+schemeId+", "+err.Error(), http.StatusInternalServerError) + return result + } else { + if c > 0 { + result.Err = model.NewAppError("SqlSchemeStore.Delete", "store.sql_scheme.delete.scheme_in_use.app_error", nil, "Id="+schemeId, http.StatusInternalServerError) + return result + } + } + } + + // 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 +} 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 99b35a664..db24ba980 100644 --- a/store/sqlstore/supplier.go +++ b/store/sqlstore/supplier.go @@ -89,6 +89,7 @@ type SqlSupplierOldStores struct { plugin store.PluginStore channelMemberHistory store.ChannelMemberHistoryStore role store.RoleStore + scheme store.SchemeStore } type SqlSupplier struct { @@ -139,6 +140,7 @@ func NewSqlSupplier(settings model.SqlSettings, metrics einterfaces.MetricsInter initSqlSupplierReactions(supplier) initSqlSupplierRoles(supplier) + initSqlSupplierSchemes(supplier) err := supplier.GetMaster().CreateTablesIfNotExists() if err != nil { @@ -462,6 +464,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 { + l4g.Critical(utils.T("store.sql.create_column.critical"), 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 { + l4g.Critical(utils.T("store.sql.create_column.critical"), err) + time.Sleep(time.Second) + os.Exit(EXIT_CREATE_COLUMN_MYSQL) + } + + return true + + } else { + l4g.Critical(utils.T("store.sql.create_column_missing_driver.critical")) + 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) { @@ -834,6 +870,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 6528b8e4c..5c39dc839 100644 --- a/store/sqlstore/team_store.go +++ b/store/sqlstore/team_store.go @@ -7,6 +7,7 @@ import ( "database/sql" "net/http" "strconv" + "strings" "github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/store" @@ -20,6 +21,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 +145,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) @@ -326,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 @@ -354,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() + } } }) } @@ -374,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) @@ -393,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() } }) } @@ -453,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 := "" @@ -468,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() } }) } @@ -570,3 +714,15 @@ 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 + } + }) +} 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 059d1a866..1d288eae0 100644 --- a/store/sqlstore/upgrade.go +++ b/store/sqlstore/upgrade.go @@ -420,5 +420,18 @@ func UpgradeDatabaseToVersion410(sqlStore SqlStore) { sqlStore.RemoveIndexIfExists("ClientId_2", "OAuthAccessData") // saveSchemaVersion(sqlStore, VERSION_4_10_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')") + + // saveSchemaVersion(sqlStore, VERSION_4_9_0) //} } diff --git a/store/store.go b/store/store.go index e64089068..98c394a21 100644 --- a/store/store.go +++ b/store/store.go @@ -62,6 +62,7 @@ type Store interface { FileInfo() FileInfoStore Reaction() ReactionStore Role() RoleStore + Scheme() SchemeStore Job() JobStore UserAccessToken() UserAccessTokenStore ChannelMemberHistory() ChannelMemberHistoryStore @@ -105,6 +106,7 @@ 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 } type ChannelStore interface { @@ -162,6 +164,7 @@ type ChannelStore interface { AnalyticsDeletedTypeCount(teamId string, channelType string) StoreChannel GetChannelUnread(channelId, userId string) StoreChannel ClearCaches() + GetChannelsByScheme(schemeId string, offset int, limit int) StoreChannel } type ChannelMemberHistoryStore interface { @@ -477,5 +480,12 @@ 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 + Delete(schemeId string) StoreChannel +} diff --git a/store/storetest/channel_store.go b/store/storetest/channel_store.go index 481631783..7427c816c 100644 --- a/store/storetest/channel_store.go +++ b/store/storetest/channel_store.go @@ -16,6 +16,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 +51,8 @@ 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) }) + } func testChannelStoreSave(t *testing.T, ss store.Store) { @@ -2186,3 +2190,67 @@ 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{ + Name: model.NewId(), + Description: model.NewId(), + Scope: model.SCHEME_SCOPE_CHANNEL, + } + + s2 := &model.Scheme{ + 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.Channel) + 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.Channel) + 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.Channel) + assert.Len(t, d3, 0) +} diff --git a/store/storetest/mocks/ChannelStore.go b/store/storetest/mocks/ChannelStore.go index 6eab47073..912dbf29c 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) diff --git a/store/storetest/mocks/LayeredStoreDatabaseLayer.go b/store/storetest/mocks/LayeredStoreDatabaseLayer.go index 6fa31bb1b..6f6776b47 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,91 @@ 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 +} + +// 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 d4242708b..8e1920d17 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,75 @@ 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 +} + +// 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 3c01ee341..c1b14d5dc 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..00eeb0573 --- /dev/null +++ b/store/storetest/mocks/SchemeStore.go @@ -0,0 +1,62 @@ +// 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 +} + +// 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 43709fc0e..baf112e87 100644 --- a/store/storetest/mocks/SqlStore.go +++ b/store/storetest/mocks/SqlStore.go @@ -554,6 +554,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 cb7e511f6..dd1967cd5 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 d38fb5f27..42303ff26 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) 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..45d136d3e --- /dev/null +++ b/store/storetest/scheme_store.go @@ -0,0 +1,303 @@ +// 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("Delete", func(t *testing.T) { testSchemeStoreDelete(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{ + 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.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.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(), + 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{ + 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.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 testSchemeStoreDelete(t *testing.T, ss store.Store) { + // Save a new scheme. + s1 := &model.Scheme{ + 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.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{ + 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.NotNil(t, sres4.Err) + + // Try deleting a channel scheme that's in use. + s5 := &model.Scheme{ + 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.NotNil(t, sres5.Err) +} 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..ff79650d5 100644 --- a/store/storetest/team_store.go +++ b/store/storetest/team_store.go @@ -7,11 +7,15 @@ import ( "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 +38,7 @@ 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) }) } func testTeamStoreSave(t *testing.T, ss store.Store) { @@ -1029,3 +1034,67 @@ 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{ + Name: model.NewId(), + Description: model.NewId(), + Scope: model.SCHEME_SCOPE_TEAM, + } + + s2 := &model.Scheme{ + 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) +} -- cgit v1.2.3-1-g7c22 From aa42ed8abae72ade1cb193027b06e6f19b579ddf Mon Sep 17 00:00:00 2001 From: Christopher Speller Date: Fri, 27 Apr 2018 14:26:58 -0700 Subject: Fixing structured logging conflicts. --- store/redis_supplier_roles.go | 2 +- store/sqlstore/supplier.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/store/redis_supplier_roles.go b/store/redis_supplier_roles.go index ec4142273..49f8ede48 100644 --- a/store/redis_supplier_roles.go +++ b/store/redis_supplier_roles.go @@ -91,7 +91,7 @@ func (s *RedisSupplier) RoleDelete(ctx context.Context, roleId string, hints ... key := buildRedisKeyForRoleName(role.Name) if err := s.client.Del(key).Err(); err != nil { - l4g.Error("Redis failed to remove key " + key + " Error: " + err.Error()) + mlog.Error("Redis failed to remove key " + key + " Error: " + err.Error()) } } diff --git a/store/sqlstore/supplier.go b/store/sqlstore/supplier.go index d36a55097..02a3cef7f 100644 --- a/store/sqlstore/supplier.go +++ b/store/sqlstore/supplier.go @@ -504,7 +504,7 @@ func (ss *SqlSupplier) CreateColumnIfNotExistsNoDefault(tableName string, column if ss.DriverName() == model.DATABASE_DRIVER_POSTGRES { _, err := ss.GetMaster().ExecNoTimeout("ALTER TABLE " + tableName + " ADD " + columnName + " " + postgresColType) if err != nil { - l4g.Critical(utils.T("store.sql.create_column.critical"), err) + mlog.Critical(fmt.Sprintf("Failed to create column %v", err)) time.Sleep(time.Second) os.Exit(EXIT_CREATE_COLUMN_POSTGRES) } @@ -514,7 +514,7 @@ func (ss *SqlSupplier) CreateColumnIfNotExistsNoDefault(tableName string, column } else if ss.DriverName() == model.DATABASE_DRIVER_MYSQL { _, err := ss.GetMaster().ExecNoTimeout("ALTER TABLE " + tableName + " ADD " + columnName + " " + mySqlColType) if err != nil { - l4g.Critical(utils.T("store.sql.create_column.critical"), err) + mlog.Critical(fmt.Sprintf("Failed to create column %v", err)) time.Sleep(time.Second) os.Exit(EXIT_CREATE_COLUMN_MYSQL) } @@ -522,7 +522,7 @@ func (ss *SqlSupplier) CreateColumnIfNotExistsNoDefault(tableName string, column return true } else { - l4g.Critical(utils.T("store.sql.create_column_missing_driver.critical")) + mlog.Critical("Failed to create column because of missing driver") time.Sleep(time.Second) os.Exit(EXIT_CREATE_COLUMN_MISSING) return false -- cgit v1.2.3-1-g7c22 From f4dcb4edf2aafca85c9af631131a77888da24bc7 Mon Sep 17 00:00:00 2001 From: Martin Kraft Date: Wed, 2 May 2018 07:31:14 -0400 Subject: MM-10182 & MM-10183: Adds channel scheme and team scheme API endpoint. (#8680) * MM-10183: Adds channel scheme API endpoint. MM-10182: Adds team scheme API endpoint. MM-10182_3: Switch from scheme_id in path to body. * MM-10182/MM-10183: Changes path from 'schemes' to 'scheme'. * MM-10182: Fix merge error. --- api4/channel.go | 51 +++++++++++++++++++++++++++++++++++++ api4/channel_test.go | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++++ api4/team.go | 51 +++++++++++++++++++++++++++++++++++++ api4/team_test.go | 64 ++++++++++++++++++++++++++++++++++++++++++++++ app/channel.go | 17 +++++++++++++ app/channel_test.go | 18 +++++++++++++ app/team.go | 18 +++++++++++++ app/team_test.go | 18 +++++++++++++ i18n/en.json | 16 ++++++++++++ model/client4.go | 30 ++++++++++++++++++++++ model/scheme.go | 15 +++++++++++ 11 files changed, 370 insertions(+) diff --git a/api4/channel.go b/api4/channel.go index 83fa8eb18..a19a1b094 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") @@ -948,3 +949,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 c3d8c8039..7618b22d9 100644 --- a/api4/channel_test.go +++ b/api4/channel_test.go @@ -1879,3 +1879,75 @@ func TestAutocompleteChannels(t *testing.T) { } } } + +func TestUpdateChannelScheme(t *testing.T) { + th := Setup().InitBasic().InitSystemAdmin() + defer th.TearDown() + + th.App.SetLicense(model.NewTestLicense("")) + + 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{ + Name: "Name", + Description: "Some description", + Scope: model.SCHEME_SCOPE_CHANNEL, + } + channelScheme, _ = th.SystemAdminClient.CreateScheme(channelScheme) + teamScheme := &model.Scheme{ + Name: "Name", + 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/team.go b/api4/team.go index 023289579..1c2e9514e 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 6540457b0..6df56f754 100644 --- a/api4/team_test.go +++ b/api4/team_test.go @@ -2052,3 +2052,67 @@ 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("")) + + 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{ + Name: "Name", + Description: "Some description", + Scope: model.SCHEME_SCOPE_TEAM, + } + teamScheme, _ = th.SystemAdminClient.CreateScheme(teamScheme) + channelScheme := &model.Scheme{ + Name: "Name", + 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/channel.go b/app/channel.go index 516e8d094..4b606ac27 100644 --- a/app/channel.go +++ b/app/channel.go @@ -354,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 diff --git a/app/channel_test.go b/app/channel_test.go index de8a6a6a0..336d9b25b 100644 --- a/app/channel_test.go +++ b/app/channel_test.go @@ -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/team.go b/app/team.go index 4fc410934..d8ebbab2a 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 { 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/i18n/en.json b/i18n/en.json index bbbeb3302..c0c8ea184 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -2506,6 +2506,14 @@ "id": "api.team.update_team.permissions.app_error", "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" @@ -6694,6 +6702,14 @@ "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" diff --git a/model/client4.go b/model/client4.go index cf34c9fd7..f17bb089a 100644 --- a/model/client4.go +++ b/model/client4.go @@ -326,6 +326,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) } @@ -3505,3 +3513,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/scheme.go b/model/scheme.go index 9ad153c73..c3ae7f15d 100644 --- a/model/scheme.go +++ b/model/scheme.go @@ -29,6 +29,10 @@ type Scheme struct { DefaultChannelUserRole string `json:"default_channel_user_role"` } +type SchemeIDPatch struct { + SchemeID *string `json:"scheme_id"` +} + func (scheme *Scheme) ToJson() string { b, _ := json.Marshal(scheme) return string(b) @@ -93,3 +97,14 @@ func (scheme *Scheme) IsValidForCreate() bool { return true } + +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) +} -- cgit v1.2.3-1-g7c22 From 60cf74352f13874a7d07c609c03b1c763af19cea Mon Sep 17 00:00:00 2001 From: George Goldberg Date: Thu, 3 May 2018 14:00:26 +0100 Subject: MM-10140: API Implementation for Schemes related Endpoints (#8615) * Implement basic scheme CRUD endpoints. * Get All Schemes (Paged) Endpoint and store plumbing. * Add get teams/channels for schemes. * Fix unit tests. * Review fixes. * More review fixes. --- api4/api.go | 5 +- api4/context.go | 11 + api4/params.go | 8 + api4/scheme.go | 211 +++++++ api4/scheme_test.go | 664 +++++++++++++++++++++ app/scheme.go | 101 ++++ i18n/en.json | 8 + model/client4.go | 80 +++ model/scheme.go | 39 ++ store/layered_store.go | 6 + store/layered_store_supplier.go | 1 + store/local_cache_supplier_schemes.go | 4 + store/redis_supplier_schemes.go | 5 + store/sqlstore/channel_store.go | 2 +- store/sqlstore/scheme_supplier.go | 19 + store/store.go | 1 + store/storetest/channel_store.go | 6 +- store/storetest/mocks/LayeredStoreDatabaseLayer.go | 23 + store/storetest/mocks/LayeredStoreSupplier.go | 23 + store/storetest/mocks/SchemeStore.go | 16 + store/storetest/mocks/SqlStore.go | 14 + store/storetest/scheme_store.go | 61 ++ 22 files changed, 1303 insertions(+), 5 deletions(-) create mode 100644 api4/scheme.go create mode 100644 api4/scheme_test.go diff --git a/api4/api.go b/api4/api.go index d36c3e3ee..f2821b42e 100644 --- a/api4/api.go +++ b/api4/api.go @@ -99,7 +99,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]+}' @@ -200,6 +201,7 @@ func Init(a *app.App, root *mux.Router, full bool) *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() @@ -229,6 +231,7 @@ func Init(a *app.App, root *mux.Router, full bool) *API { api.InitOpenGraph() api.InitPlugin() api.InitRole() + api.InitScheme() api.InitImage() root.Handle("/api/v4/{anything:.*}", http.HandlerFunc(Handle404)) diff --git a/api4/context.go b/api4/context.go index c965e1d80..6afb964ce 100644 --- a/api4/context.go +++ b/api4/context.go @@ -650,6 +650,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/api4/params.go b/api4/params.go index e8e3f25e7..35f21e0ec 100644 --- a/api4/params.go +++ b/api4/params.go @@ -47,6 +47,8 @@ type ApiParams struct { ActionId string RoleId string RoleName string + SchemeId string + Scope string Page int PerPage int LogsPerPage int @@ -167,6 +169,12 @@ func ApiParamsFromRequest(r *http.Request) *ApiParams { params.RoleName = val } + if val, ok := props["scheme_id"]; ok { + params.SchemeId = val + } + + params.Scope = query.Get("scope") + if val, err := strconv.Atoi(query.Get("page")); err != nil || val < 0 { params.Page = PAGE_DEFAULT } else { diff --git a/api4/scheme.go b/api4/scheme.go new file mode 100644 index 000000000..bdfe69870 --- /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.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.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.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..a0ea1e9b0 --- /dev/null +++ b/api4/scheme_test.go @@ -0,0 +1,664 @@ +// 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("")) + + // Basic test of creating a team scheme. + scheme1 := &model.Scheme{ + Name: model.NewId(), + Description: model.NewId(), + Scope: model.SCHEME_SCOPE_TEAM, + } + + s1, r1 := th.SystemAdminClient.CreateScheme(scheme1) + CheckNoError(t, r1) + + 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{ + Name: model.NewId(), + Description: model.NewId(), + Scope: model.SCHEME_SCOPE_CHANNEL, + } + + s2, r2 := th.SystemAdminClient.CreateScheme(scheme2) + CheckNoError(t, r2) + + 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{ + 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 name. + scheme4 := &model.Scheme{ + Name: strings.Repeat(model.NewId(), 100), + Description: model.NewId(), + Scope: model.NewId(), + } + _, r4 := th.SystemAdminClient.CreateScheme(scheme4) + CheckBadRequestStatus(t, r4) + + // Try and create a scheme without the appropriate permissions. + scheme5 := &model.Scheme{ + 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{ + Name: model.NewId(), + Description: model.NewId(), + Scope: model.SCHEME_SCOPE_TEAM, + } + _, r6 := th.SystemAdminClient.CreateScheme(scheme6) + CheckNotImplementedStatus(t, r6) +} + +func TestGetScheme(t *testing.T) { + th := Setup().InitBasic().InitSystemAdmin() + defer th.TearDown() + + th.App.SetLicense(model.NewTestLicense("")) + + // Basic test of creating a team scheme. + scheme1 := &model.Scheme{ + Name: model.NewId(), + Description: model.NewId(), + Scope: model.SCHEME_SCOPE_TEAM, + } + + s1, r1 := th.SystemAdminClient.CreateScheme(scheme1) + CheckNoError(t, r1) + + 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) +} + +func TestGetSchemes(t *testing.T) { + th := Setup().InitBasic().InitSystemAdmin() + defer th.TearDown() + + th.App.SetLicense(model.NewTestLicense("")) + + scheme1 := &model.Scheme{ + Name: model.NewId(), + Description: model.NewId(), + Scope: model.SCHEME_SCOPE_TEAM, + } + + scheme2 := &model.Scheme{ + Name: model.NewId(), + Description: model.NewId(), + Scope: model.SCHEME_SCOPE_CHANNEL, + } + + _, 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) +} + +func TestGetTeamsForScheme(t *testing.T) { + th := Setup().InitBasic().InitSystemAdmin() + defer th.TearDown() + + th.App.SetLicense(model.NewTestLicense("")) + + scheme1 := &model.Scheme{ + 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{ + 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) +} + +func TestGetChannelsForScheme(t *testing.T) { + th := Setup().InitBasic().InitSystemAdmin() + defer th.TearDown() + + th.App.SetLicense(model.NewTestLicense("")) + + scheme1 := &model.Scheme{ + 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{ + 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) +} + +func TestPatchScheme(t *testing.T) { + th := Setup().InitBasic().InitSystemAdmin() + defer th.TearDown() + + th.App.SetLicense(model.NewTestLicense("")) + + // Basic test of creating a team scheme. + scheme1 := &model.Scheme{ + Name: model.NewId(), + Description: model.NewId(), + Scope: model.SCHEME_SCOPE_TEAM, + } + + s1, r1 := th.SystemAdminClient.CreateScheme(scheme1) + CheckNoError(t, r1) + + 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{ + Name: new(string), + Description: new(string), + } + *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.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.Description = nil + + s5, r5 := th.SystemAdminClient.PatchScheme(s4.Id, schemePatch) + CheckNoError(t, r5) + assert.Equal(t, s5.Id, s4.Id) + 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) +} + +func TestDeleteScheme(t *testing.T) { + th := Setup().InitBasic().InitSystemAdmin() + defer th.TearDown() + + t.Run("ValidTeamScheme", func(t *testing.T) { + th.App.SetLicense(model.NewTestLicense("")) + + // Create a team scheme. + scheme1 := &model.Scheme{ + 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) + + // Try and fail to delete the scheme. + _, r2 := th.SystemAdminClient.DeleteScheme(s1.Id) + CheckInternalErrorStatus(t, r2) + + 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) + + // Change the team using it to a different scheme. + emptyString := "" + team.SchemeId = &emptyString + res = <-th.App.Srv.Store.Team().Update(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) + }) + + t.Run("ValidChannelScheme", func(t *testing.T) { + th.App.SetLicense(model.NewTestLicense("")) + + // Create a channel scheme. + scheme1 := &model.Scheme{ + 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) + + // Try and fail to delete the scheme. + _, r2 := th.SystemAdminClient.DeleteScheme(s1.Id) + CheckInternalErrorStatus(t, r2) + + 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) + + // Change the team using it to a different scheme. + emptyString := "" + channel.SchemeId = &emptyString + res = <-th.App.Srv.Store.Channel().Update(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) + }) + + t.Run("FailureCases", func(t *testing.T) { + th.App.SetLicense(model.NewTestLicense("")) + + scheme1 := &model.Scheme{ + 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) + }) +} diff --git a/app/scheme.go b/app/scheme.go index 26ec6cd2a..b43914eb8 100644 --- a/app/scheme.go +++ b/app/scheme.go @@ -12,3 +12,104 @@ func (a *App) GetScheme(id string) (*model.Scheme, *model.AppError) { return result.Data.(*model.Scheme), nil } } + +func (a *App) GetSchemesPage(scope string, page int, perPage int) ([]*model.Scheme, *model.AppError) { + return a.GetSchemes(scope, page*perPage, perPage) +} + +func (a *App) GetSchemes(scope string, offset int, limit int) ([]*model.Scheme, *model.AppError) { + 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) { + return a.GetTeamsForScheme(scheme, page*perPage, perPage) +} + +func (a *App) GetTeamsForScheme(scheme *model.Scheme, offset int, limit int) ([]*model.Team, *model.AppError) { + 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) { + return a.GetChannelsForScheme(scheme, page*perPage, perPage) +} + +func (a *App) GetChannelsForScheme(scheme *model.Scheme, offset int, limit int) (model.ChannelList, *model.AppError) { + 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 { + // TODO: Actually check the Phase 2 migration has completed before permitting these actions. + + return nil +} diff --git a/i18n/en.json b/i18n/en.json index 0ff5e5378..c993b0411 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -6654,6 +6654,14 @@ "id": "store.sql_reaction.delete.save.app_error", "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" diff --git a/model/client4.go b/model/client4.go index f17bb089a..d4410a5c3 100644 --- a/model/client4.go +++ b/model/client4.go @@ -318,6 +318,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") } @@ -3420,6 +3428,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. diff --git a/model/scheme.go b/model/scheme.go index c3ae7f15d..f949d9122 100644 --- a/model/scheme.go +++ b/model/scheme.go @@ -29,6 +29,11 @@ type Scheme struct { DefaultChannelUserRole string `json:"default_channel_user_role"` } +type SchemePatch struct { + Name *string `json:"name"` + Description *string `json:"description"` +} + type SchemeIDPatch struct { SchemeID *string `json:"scheme_id"` } @@ -44,6 +49,20 @@ func SchemeFromJson(data io.Reader) *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 @@ -98,6 +117,26 @@ func (scheme *Scheme) IsValidForCreate() bool { return true } +func (scheme *Scheme) Patch(patch *SchemePatch) { + 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) diff --git a/store/layered_store.go b/store/layered_store.go index 603976fc0..cbabe9d22 100644 --- a/store/layered_store.go +++ b/store/layered_store.go @@ -292,3 +292,9 @@ func (s *LayeredSchemeStore) Delete(schemeId string) StoreChannel { 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) + }) +} diff --git a/store/layered_store_supplier.go b/store/layered_store_supplier.go index 04fa26fd3..4f57004bb 100644 --- a/store/layered_store_supplier.go +++ b/store/layered_store_supplier.go @@ -42,4 +42,5 @@ type LayeredStoreSupplier interface { 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 } diff --git a/store/local_cache_supplier_schemes.go b/store/local_cache_supplier_schemes.go index 2a8f73a71..809c60510 100644 --- a/store/local_cache_supplier_schemes.go +++ b/store/local_cache_supplier_schemes.go @@ -42,3 +42,7 @@ func (s *LocalCacheSupplier) SchemeDelete(ctx context.Context, schemeId string, 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...) +} diff --git a/store/redis_supplier_schemes.go b/store/redis_supplier_schemes.go index 4c05e9329..3bd747044 100644 --- a/store/redis_supplier_schemes.go +++ b/store/redis_supplier_schemes.go @@ -23,3 +23,8 @@ func (s *RedisSupplier) SchemeDelete(ctx context.Context, schemeId string, hints // 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...) +} diff --git a/store/sqlstore/channel_store.go b/store/sqlstore/channel_store.go index b12c553a4..beef1be80 100644 --- a/store/sqlstore/channel_store.go +++ b/store/sqlstore/channel_store.go @@ -1730,7 +1730,7 @@ func (s SqlChannelStore) GetMembersByIds(channelId string, userIds []string) sto func (s SqlChannelStore) GetChannelsByScheme(schemeId string, offset int, limit int) store.StoreChannel { return store.Do(func(result *store.StoreResult) { - var channels []*model.Channel + 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) diff --git a/store/sqlstore/scheme_supplier.go b/store/sqlstore/scheme_supplier.go index 278d1a3c4..448e5a92f 100644 --- a/store/sqlstore/scheme_supplier.go +++ b/store/sqlstore/scheme_supplier.go @@ -270,3 +270,22 @@ func (s *SqlSupplier) SchemeDelete(ctx context.Context, schemeId string, hints . 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 +} diff --git a/store/store.go b/store/store.go index 1085198bd..dd149fbe9 100644 --- a/store/store.go +++ b/store/store.go @@ -485,5 +485,6 @@ type RoleStore interface { type SchemeStore interface { Save(scheme *model.Scheme) StoreChannel Get(schemeId string) StoreChannel + GetAllPage(scope string, offset int, limit int) StoreChannel Delete(schemeId string) StoreChannel } diff --git a/store/storetest/channel_store.go b/store/storetest/channel_store.go index 7427c816c..d90a0ae1e 100644 --- a/store/storetest/channel_store.go +++ b/store/storetest/channel_store.go @@ -2239,18 +2239,18 @@ func testChannelStoreGetChannelsByScheme(t *testing.T, ss store.Store) { // 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.Channel) + 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.Channel) + 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.Channel) + d3 := res3.Data.(model.ChannelList) assert.Len(t, d3, 0) } diff --git a/store/storetest/mocks/LayeredStoreDatabaseLayer.go b/store/storetest/mocks/LayeredStoreDatabaseLayer.go index 6f6776b47..a505b6434 100644 --- a/store/storetest/mocks/LayeredStoreDatabaseLayer.go +++ b/store/storetest/mocks/LayeredStoreDatabaseLayer.go @@ -632,6 +632,29 @@ func (_m *LayeredStoreDatabaseLayer) SchemeGet(ctx context.Context, schemeId str 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 +} + // 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)) diff --git a/store/storetest/mocks/LayeredStoreSupplier.go b/store/storetest/mocks/LayeredStoreSupplier.go index 8e1920d17..18dbe3af1 100644 --- a/store/storetest/mocks/LayeredStoreSupplier.go +++ b/store/storetest/mocks/LayeredStoreSupplier.go @@ -329,6 +329,29 @@ func (_m *LayeredStoreSupplier) SchemeGet(ctx context.Context, schemeId string, 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 +} + // 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)) diff --git a/store/storetest/mocks/SchemeStore.go b/store/storetest/mocks/SchemeStore.go index 00eeb0573..2868521b3 100644 --- a/store/storetest/mocks/SchemeStore.go +++ b/store/storetest/mocks/SchemeStore.go @@ -45,6 +45,22 @@ func (_m *SchemeStore) Get(schemeId string) 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 +} + // Save provides a mock function with given fields: scheme func (_m *SchemeStore) Save(scheme *model.Scheme) store.StoreChannel { ret := _m.Called(scheme) diff --git a/store/storetest/mocks/SqlStore.go b/store/storetest/mocks/SqlStore.go index baf112e87..021baa7d3 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) diff --git a/store/storetest/scheme_store.go b/store/storetest/scheme_store.go index 45d136d3e..c0cbe5deb 100644 --- a/store/storetest/scheme_store.go +++ b/store/storetest/scheme_store.go @@ -17,6 +17,7 @@ func TestSchemeStore(t *testing.T, ss store.Store) { 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) }) } @@ -171,6 +172,66 @@ func testSchemeStoreGet(t *testing.T, ss store.Store) { assert.NotNil(t, res3.Err) } +func testSchemeStoreGetAllPage(t *testing.T, ss store.Store) { + // Save a scheme to test with. + schemes := []*model.Scheme{ + { + Name: model.NewId(), + Description: model.NewId(), + Scope: model.SCHEME_SCOPE_TEAM, + }, + { + Name: model.NewId(), + Description: model.NewId(), + Scope: model.SCHEME_SCOPE_CHANNEL, + }, + { + Name: model.NewId(), + Description: model.NewId(), + Scope: model.SCHEME_SCOPE_TEAM, + }, + { + 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].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{ -- cgit v1.2.3-1-g7c22 From 51bd710ecdca6628461c9fa2679737073e4d5059 Mon Sep 17 00:00:00 2001 From: George Goldberg Date: Mon, 14 May 2018 15:59:04 +0100 Subject: MM-9728: Online migration for advanced permissions phase 2 (#8744) * MM-9728: Online migration for advanced permissions phase 2 * Add unit tests for new store functions. * Move migration specific code to own file. * Add migration state function test. * Style fixes. * Add i18n strings. * Fix mocks. * Add TestMain to migrations package tests. * Fix typo. * Fix review comments. * Fix up the "Check if migration is done" check to actually work. --- api4/channel_test.go | 13 + api4/scheme_test.go | 121 ++++++++- api4/team_test.go | 11 + app/app.go | 10 + app/scheme.go | 8 +- i18n/en.json | 48 ++++ imports/placeholder.go | 4 + jobs/interfaces/migrations_interface.go | 11 + jobs/jobs.go | 7 + jobs/jobs_watcher.go | 7 + jobs/schedulers.go | 4 + jobs/server.go | 2 + jobs/workers.go | 13 + migrations/advanced_permissions_phase_2.go | 106 ++++++++ migrations/migrations.go | 63 +++++ migrations/migrations_test.go | 140 ++++++++++ migrations/migrationstestlib.go | 419 +++++++++++++++++++++++++++++ migrations/scheduler.go | 110 ++++++++ migrations/worker.go | 166 ++++++++++++ model/job.go | 2 + model/migration.go | 8 + store/sqlstore/channel_store.go | 68 +++++ store/sqlstore/team_store.go | 69 +++++ store/store.go | 2 + store/storetest/channel_store.go | 75 ++++++ store/storetest/mocks/ChannelStore.go | 16 ++ store/storetest/mocks/TeamStore.go | 16 ++ store/storetest/team_store.go | 71 +++++ 28 files changed, 1586 insertions(+), 4 deletions(-) create mode 100644 jobs/interfaces/migrations_interface.go create mode 100644 migrations/advanced_permissions_phase_2.go create mode 100644 migrations/migrations.go create mode 100644 migrations/migrations_test.go create mode 100644 migrations/migrationstestlib.go create mode 100644 migrations/scheduler.go create mode 100644 migrations/worker.go create mode 100644 model/migration.go diff --git a/api4/channel_test.go b/api4/channel_test.go index 11d313291..551a1a484 100644 --- a/api4/channel_test.go +++ b/api4/channel_test.go @@ -12,6 +12,8 @@ import ( "strings" "testing" + "github.com/stretchr/testify/assert" + "github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/utils" ) @@ -1890,6 +1892,17 @@ func TestUpdateChannelScheme(t *testing.T) { th.App.SetLicense(model.NewTestLicense("")) + // Mark the migration as done. + <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) + res := <-th.App.Srv.Store.System().Save(&model.System{Name: model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2, Value: "true"}) + assert.Nil(t, res.Err) + + // Un-mark the migration at the end of the test. + defer func() { + res := <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) + assert.Nil(t, res.Err) + }() + team := &model.Team{ DisplayName: "Name", Description: "Some description", diff --git a/api4/scheme_test.go b/api4/scheme_test.go index a0ea1e9b0..92cfa4d30 100644 --- a/api4/scheme_test.go +++ b/api4/scheme_test.go @@ -18,6 +18,11 @@ func TestCreateScheme(t *testing.T) { th.App.SetLicense(model.NewTestLicense("")) + // Mark the migration as done. + <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) + res := <-th.App.Srv.Store.System().Save(&model.System{Name: model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2, Value: "true"}) + assert.Nil(t, res.Err) + // Basic test of creating a team scheme. scheme1 := &model.Scheme{ Name: model.NewId(), @@ -113,6 +118,21 @@ func TestCreateScheme(t *testing.T) { } _, r6 := th.SystemAdminClient.CreateScheme(scheme6) CheckNotImplementedStatus(t, r6) + + // Mark the migration as not done. + res = <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) + assert.Nil(t, res.Err) + + th.LoginSystemAdmin() + th.App.SetLicense(model.NewTestLicense("")) + + scheme7 := &model.Scheme{ + Name: model.NewId(), + Description: model.NewId(), + Scope: model.SCHEME_SCOPE_TEAM, + } + _, r7 := th.SystemAdminClient.CreateScheme(scheme7) + CheckInternalErrorStatus(t, r7) } func TestGetScheme(t *testing.T) { @@ -128,9 +148,17 @@ func TestGetScheme(t *testing.T) { Scope: model.SCHEME_SCOPE_TEAM, } + // Mark the migration as done while we create the scheme. + <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) + res := <-th.App.Srv.Store.System().Save(&model.System{Name: model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2, Value: "true"}) + assert.Nil(t, res.Err) + s1, r1 := th.SystemAdminClient.CreateScheme(scheme1) CheckNoError(t, r1) + res = <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) + assert.Nil(t, res.Err) + assert.Equal(t, s1.Name, scheme1.Name) assert.Equal(t, s1.Description, scheme1.Description) assert.NotZero(t, s1.CreateAt) @@ -184,11 +212,19 @@ func TestGetSchemes(t *testing.T) { Scope: model.SCHEME_SCOPE_CHANNEL, } + // Mark the migration as done while we create the scheme. + <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) + res := <-th.App.Srv.Store.System().Save(&model.System{Name: model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2, Value: "true"}) + assert.Nil(t, res.Err) + _, r1 := th.SystemAdminClient.CreateScheme(scheme1) CheckNoError(t, r1) _, r2 := th.SystemAdminClient.CreateScheme(scheme2) CheckNoError(t, r2) + res = <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) + assert.Nil(t, res.Err) + l3, r3 := th.SystemAdminClient.GetSchemes("", 0, 100) CheckNoError(t, r3) @@ -226,6 +262,11 @@ func TestGetTeamsForScheme(t *testing.T) { th.App.SetLicense(model.NewTestLicense("")) + // Mark the migration as done while we create the scheme. + <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) + res := <-th.App.Srv.Store.System().Save(&model.System{Name: model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2, Value: "true"}) + assert.Nil(t, res.Err) + scheme1 := &model.Scheme{ Name: model.NewId(), Description: model.NewId(), @@ -234,6 +275,9 @@ func TestGetTeamsForScheme(t *testing.T) { scheme1, r1 := th.SystemAdminClient.CreateScheme(scheme1) CheckNoError(t, r1) + res = <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) + assert.Nil(t, res.Err) + team1 := &model.Team{ Name: GenerateTestUsername(), DisplayName: "A Test Team", @@ -294,6 +338,10 @@ func TestGetTeamsForScheme(t *testing.T) { _, ri4 := th.Client.GetTeamsForScheme(model.NewId(), 0, 100) CheckForbiddenStatus(t, ri4) + // Mark the migration as done again while we create a scheme. + res = <-th.App.Srv.Store.System().Save(&model.System{Name: model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2, Value: "true"}) + assert.Nil(t, res.Err) + scheme2 := &model.Scheme{ Name: model.NewId(), Description: model.NewId(), @@ -302,6 +350,9 @@ func TestGetTeamsForScheme(t *testing.T) { scheme2, rs2 := th.SystemAdminClient.CreateScheme(scheme2) CheckNoError(t, rs2) + res = <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) + assert.Nil(t, res.Err) + _, ri5 := th.SystemAdminClient.GetTeamsForScheme(scheme2.Id, 0, 100) CheckBadRequestStatus(t, ri5) } @@ -312,6 +363,11 @@ func TestGetChannelsForScheme(t *testing.T) { th.App.SetLicense(model.NewTestLicense("")) + // Mark the migration as done while we create the scheme. + <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) + res := <-th.App.Srv.Store.System().Save(&model.System{Name: model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2, Value: "true"}) + assert.Nil(t, res.Err) + scheme1 := &model.Scheme{ Name: model.NewId(), Description: model.NewId(), @@ -320,6 +376,9 @@ func TestGetChannelsForScheme(t *testing.T) { scheme1, r1 := th.SystemAdminClient.CreateScheme(scheme1) CheckNoError(t, r1) + res = <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) + assert.Nil(t, res.Err) + channel1 := &model.Channel{ TeamId: model.NewId(), DisplayName: "A Name", @@ -382,6 +441,10 @@ func TestGetChannelsForScheme(t *testing.T) { _, ri4 := th.Client.GetChannelsForScheme(model.NewId(), 0, 100) CheckForbiddenStatus(t, ri4) + // Mark the migration as done again while we create a scheme. + res = <-th.App.Srv.Store.System().Save(&model.System{Name: model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2, Value: "true"}) + assert.Nil(t, res.Err) + scheme2 := &model.Scheme{ Name: model.NewId(), Description: model.NewId(), @@ -390,6 +453,9 @@ func TestGetChannelsForScheme(t *testing.T) { scheme2, rs2 := th.SystemAdminClient.CreateScheme(scheme2) CheckNoError(t, rs2) + res = <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) + assert.Nil(t, res.Err) + _, ri5 := th.SystemAdminClient.GetChannelsForScheme(scheme2.Id, 0, 100) CheckBadRequestStatus(t, ri5) } @@ -400,6 +466,11 @@ func TestPatchScheme(t *testing.T) { th.App.SetLicense(model.NewTestLicense("")) + // Mark the migration as done. + <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) + res := <-th.App.Srv.Store.System().Save(&model.System{Name: model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2, Value: "true"}) + assert.Nil(t, res.Err) + // Basic test of creating a team scheme. scheme1 := &model.Scheme{ Name: model.NewId(), @@ -480,6 +551,16 @@ func TestPatchScheme(t *testing.T) { th.App.SetLicense(nil) _, r11 := th.SystemAdminClient.PatchScheme(s6.Id, schemePatch) CheckNotImplementedStatus(t, r11) + + // Mark the migration as not done. + res = <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) + assert.Nil(t, res.Err) + + th.LoginSystemAdmin() + th.App.SetLicense(model.NewTestLicense("")) + + _, r12 := th.SystemAdminClient.PatchScheme(s6.Id, schemePatch) + CheckInternalErrorStatus(t, r12) } func TestDeleteScheme(t *testing.T) { @@ -489,6 +570,17 @@ func TestDeleteScheme(t *testing.T) { t.Run("ValidTeamScheme", func(t *testing.T) { th.App.SetLicense(model.NewTestLicense("")) + // Mark the migration as done. + <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) + res := <-th.App.Srv.Store.System().Save(&model.System{Name: model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2, Value: "true"}) + assert.Nil(t, res.Err) + + // Un-mark the migration at the end of the test. + defer func() { + res := <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) + assert.Nil(t, res.Err) + }() + // Create a team scheme. scheme1 := &model.Scheme{ Name: model.NewId(), @@ -515,7 +607,7 @@ func TestDeleteScheme(t *testing.T) { assert.Zero(t, role4.DeleteAt) // Make sure this scheme is in use by a team. - res := <-th.App.Srv.Store.Team().Save(&model.Team{ + res = <-th.App.Srv.Store.Team().Save(&model.Team{ Name: model.NewId(), DisplayName: model.NewId(), Email: model.NewId() + "@nowhere.com", @@ -571,6 +663,17 @@ func TestDeleteScheme(t *testing.T) { t.Run("ValidChannelScheme", func(t *testing.T) { th.App.SetLicense(model.NewTestLicense("")) + // Mark the migration as done. + <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) + res := <-th.App.Srv.Store.System().Save(&model.System{Name: model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2, Value: "true"}) + assert.Nil(t, res.Err) + + // Un-mark the migration at the end of the test. + defer func() { + res := <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) + assert.Nil(t, res.Err) + }() + // Create a channel scheme. scheme1 := &model.Scheme{ Name: model.NewId(), @@ -591,7 +694,7 @@ func TestDeleteScheme(t *testing.T) { assert.Zero(t, role4.DeleteAt) // Make sure this scheme is in use by a team. - res := <-th.App.Srv.Store.Channel().Save(&model.Channel{ + res = <-th.App.Srv.Store.Channel().Save(&model.Channel{ TeamId: model.NewId(), DisplayName: model.NewId(), Name: model.NewId(), @@ -635,6 +738,11 @@ func TestDeleteScheme(t *testing.T) { t.Run("FailureCases", func(t *testing.T) { th.App.SetLicense(model.NewTestLicense("")) + // Mark the migration as done. + <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) + res := <-th.App.Srv.Store.System().Save(&model.System{Name: model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2, Value: "true"}) + assert.Nil(t, res.Err) + scheme1 := &model.Scheme{ Name: model.NewId(), Description: model.NewId(), @@ -660,5 +768,14 @@ func TestDeleteScheme(t *testing.T) { th.App.SetLicense(nil) _, r5 := th.SystemAdminClient.DeleteScheme(s1.Id) CheckNotImplementedStatus(t, r5) + + // Test with migration not being done. + res = <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) + assert.Nil(t, res.Err) + + th.App.SetLicense(model.NewTestLicense("")) + + _, r6 := th.SystemAdminClient.DeleteScheme(s1.Id) + CheckInternalErrorStatus(t, r6) }) } diff --git a/api4/team_test.go b/api4/team_test.go index 6df56f754..45d8e8f08 100644 --- a/api4/team_test.go +++ b/api4/team_test.go @@ -2059,6 +2059,17 @@ func TestUpdateTeamScheme(t *testing.T) { th.App.SetLicense(model.NewTestLicense("")) + // Mark the migration as done. + <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) + res := <-th.App.Srv.Store.System().Save(&model.System{Name: model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2, Value: "true"}) + assert.Nil(t, res.Err) + + // Un-mark the migration at the end of the test. + defer func() { + res := <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) + assert.Nil(t, res.Err) + }() + team := &model.Team{ DisplayName: "Name", Description: "Some description", diff --git a/app/app.go b/app/app.go index 2cdf333c1..d4a663e32 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" @@ -319,6 +320,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) { @@ -413,6 +420,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 { diff --git a/app/scheme.go b/app/scheme.go index b43914eb8..a8eb9ef46 100644 --- a/app/scheme.go +++ b/app/scheme.go @@ -3,7 +3,9 @@ package app -import "github.com/mattermost/mattermost-server/model" +import ( + "github.com/mattermost/mattermost-server/model" +) func (a *App) GetScheme(id string) (*model.Scheme, *model.AppError) { if result := <-a.Srv.Store.Scheme().Get(id); result.Err != nil { @@ -109,7 +111,9 @@ func (a *App) GetChannelsForScheme(scheme *model.Scheme, offset int, limit int) } func (a *App) IsPhase2MigrationCompleted() *model.AppError { - // TODO: Actually check the Phase 2 migration has completed before permitting these actions. + if result := <-a.Srv.Store.System().GetByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2); result.Err != nil { + return result.Err + } return nil } diff --git a/i18n/en.json b/i18n/en.json index 58a950921..0b954d814 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -47,6 +47,54 @@ "id": "September", "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." 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..b52f7af79 --- /dev/null +++ b/migrations/migrationstestlib.go @@ -0,0 +1,419 @@ +// 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 (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, + }) +} 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/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/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/store/sqlstore/channel_store.go b/store/sqlstore/channel_store.go index beef1be80..dceebc92e 100644 --- a/store/sqlstore/channel_store.go +++ b/store/sqlstore/channel_store.go @@ -1739,3 +1739,71 @@ func (s SqlChannelStore) GetChannelsByScheme(schemeId string, offset int, limit } }) } + +// This function does the Advanced Permissions Phase 2 migration for ChannelMember objects. It performs the migration +// in batches as a single transaction per batch to ensure consistency but to also minimise execution time to avoid +// causing unnecessary table locks. **THIS FUNCTION SHOULD NOT BE USED FOR ANY OTHER PURPOSE.** Executing this function +// *after* the new Schemes functionality has been used on an installation will have unintended consequences. +func (s SqlChannelStore) MigrateChannelMembers(fromChannelId string, fromUserId string) store.StoreChannel { + return store.Do(func(result *store.StoreResult) { + var transaction *gorp.Transaction + var err error + + if transaction, err = s.GetMaster().Begin(); err != nil { + result.Err = model.NewAppError("SqlChannelStore.MigrateChannelMembers", "store.sql_channel.migrate_channel_members.open_transaction.app_error", nil, err.Error(), http.StatusInternalServerError) + return + } + + var channelMembers []channelMember + if _, err := transaction.Select(&channelMembers, "SELECT * from ChannelMembers WHERE (ChannelId, UserId) > (:FromChannelId, :FromUserId) ORDER BY ChannelId, UserId LIMIT 100", map[string]interface{}{"FromChannelId": fromChannelId, "FromUserId": fromUserId}); err != nil { + result.Err = model.NewAppError("SqlChannelStore.MigrateChannelMembers", "store.sql_channel.migrate_channel_members.select.app_error", nil, err.Error(), http.StatusInternalServerError) + return + } + + if len(channelMembers) == 0 { + // No more channel members in query result means that the migration has finished. + return + } + + for _, member := range channelMembers { + roles := strings.Fields(member.Roles) + var newRoles []string + member.SchemeAdmin = sql.NullBool{Bool: false, Valid: true} + member.SchemeUser = sql.NullBool{Bool: false, Valid: true} + for _, role := range roles { + if role == model.CHANNEL_ADMIN_ROLE_ID { + member.SchemeAdmin = sql.NullBool{Bool: true, Valid: true} + } else if role == model.CHANNEL_USER_ROLE_ID { + member.SchemeUser = sql.NullBool{Bool: true, Valid: true} + } else { + newRoles = append(newRoles, role) + } + } + member.Roles = strings.Join(newRoles, " ") + + if _, err := transaction.Update(&member); err != nil { + if err2 := transaction.Rollback(); err2 != nil { + result.Err = model.NewAppError("SqlChannelStore.MigrateChannelMembers", "store.sql_channel.migrate_channel_members.rollback_transaction.app_error", nil, err2.Error(), http.StatusInternalServerError) + return + } + result.Err = model.NewAppError("SqlChannelStore.MigrateChannelMembers", "store.sql_channel.migrate_channel_members.update.app_error", nil, err.Error(), http.StatusInternalServerError) + return + } + + } + + if err := transaction.Commit(); err != nil { + if err2 := transaction.Rollback(); err2 != nil { + result.Err = model.NewAppError("SqlChannelStore.MigrateChannelMembers", "store.sql_channel.migrate_channel_members.rollback_transaction.app_error", nil, err2.Error(), http.StatusInternalServerError) + return + } + result.Err = model.NewAppError("SqlChannelStore.MigrateChannelMembers", "store.sql_channel.migrate_channel_members.commit_transaction.app_error", nil, err.Error(), http.StatusInternalServerError) + return + } + + data := make(map[string]string) + data["ChannelId"] = channelMembers[len(channelMembers)-1].ChannelId + data["UserId"] = channelMembers[len(channelMembers)-1].UserId + result.Data = data + }) +} diff --git a/store/sqlstore/team_store.go b/store/sqlstore/team_store.go index 9e72cc82e..ea5f7fd1f 100644 --- a/store/sqlstore/team_store.go +++ b/store/sqlstore/team_store.go @@ -9,6 +9,7 @@ import ( "strconv" "strings" + "github.com/mattermost/gorp" "github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/store" ) @@ -725,3 +726,71 @@ func (s SqlTeamStore) GetTeamsByScheme(schemeId string, offset int, limit int) s } }) } + +// This function does the Advanced Permissions Phase 2 migration for TeamMember objects. It performs the migration +// in batches as a single transaction per batch to ensure consistency but to also minimise execution time to avoid +// causing unnecessary table locks. **THIS FUNCTION SHOULD NOT BE USED FOR ANY OTHER PURPOSE.** Executing this function +// *after* the new Schemes functionality has been used on an installation will have unintended consequences. +func (s SqlTeamStore) MigrateTeamMembers(fromTeamId string, fromUserId string) store.StoreChannel { + return store.Do(func(result *store.StoreResult) { + var transaction *gorp.Transaction + var err error + + if transaction, err = s.GetMaster().Begin(); err != nil { + result.Err = model.NewAppError("SqlTeamStore.MigrateTeamMembers", "store.sql_team.migrate_team_members.open_transaction.app_error", nil, err.Error(), http.StatusInternalServerError) + return + } + + var teamMembers []teamMember + if _, err := transaction.Select(&teamMembers, "SELECT * from TeamMembers WHERE (TeamId, UserId) > (:FromTeamId, :FromUserId) ORDER BY TeamId, UserId LIMIT 100", map[string]interface{}{"FromTeamId": fromTeamId, "FromUserId": fromUserId}); err != nil { + result.Err = model.NewAppError("SqlTeamStore.MigrateTeamMembers", "store.sql_team.migrate_team_members.select.app_error", nil, err.Error(), http.StatusInternalServerError) + return + } + + if len(teamMembers) == 0 { + // No more team members in query result means that the migration has finished. + return + } + + for _, member := range teamMembers { + roles := strings.Fields(member.Roles) + var newRoles []string + member.SchemeAdmin = sql.NullBool{Bool: false, Valid: true} + member.SchemeUser = sql.NullBool{Bool: false, Valid: true} + for _, role := range roles { + if role == model.TEAM_ADMIN_ROLE_ID { + member.SchemeAdmin = sql.NullBool{Bool: true, Valid: true} + } else if role == model.TEAM_USER_ROLE_ID { + member.SchemeUser = sql.NullBool{Bool: true, Valid: true} + } else { + newRoles = append(newRoles, role) + } + } + member.Roles = strings.Join(newRoles, " ") + + if _, err := transaction.Update(&member); err != nil { + if err2 := transaction.Rollback(); err2 != nil { + result.Err = model.NewAppError("SqlTeamStore.MigrateTeamMembers", "store.sql_team.migrate_team_members.rollback_transaction.app_error", nil, err2.Error(), http.StatusInternalServerError) + return + } + result.Err = model.NewAppError("SqlTeamStore.MigrateTeamMembers", "store.sql_team.migrate_team_members.update.app_error", nil, err.Error(), http.StatusInternalServerError) + return + } + + } + + if err := transaction.Commit(); err != nil { + if err2 := transaction.Rollback(); err2 != nil { + result.Err = model.NewAppError("SqlTeamStore.MigrateTeamMembers", "store.sql_team.migrate_team_members.rollback_transaction.app_error", nil, err2.Error(), http.StatusInternalServerError) + return + } + result.Err = model.NewAppError("SqlTeamStore.MigrateTeamMembers", "store.sql_team.migrate_team_members.commit_transaction.app_error", nil, err.Error(), http.StatusInternalServerError) + return + } + + data := make(map[string]string) + data["TeamId"] = teamMembers[len(teamMembers)-1].TeamId + data["UserId"] = teamMembers[len(teamMembers)-1].UserId + result.Data = data + }) +} diff --git a/store/store.go b/store/store.go index 2e85c0a68..bf2ac42f5 100644 --- a/store/store.go +++ b/store/store.go @@ -105,6 +105,7 @@ type TeamStore interface { RemoveAllMembersByUser(userId string) StoreChannel UpdateLastTeamIconUpdate(teamId string, curTime int64) StoreChannel GetTeamsByScheme(schemeId string, offset int, limit int) StoreChannel + MigrateTeamMembers(fromTeamId string, fromUserId string) StoreChannel } type ChannelStore interface { @@ -163,6 +164,7 @@ type ChannelStore interface { GetChannelUnread(channelId, userId string) StoreChannel ClearCaches() GetChannelsByScheme(schemeId string, offset int, limit int) StoreChannel + MigrateChannelMembers(fromChannelId string, fromUserId string) StoreChannel } type ChannelMemberHistoryStore interface { diff --git a/store/storetest/channel_store.go b/store/storetest/channel_store.go index d90a0ae1e..d044f3907 100644 --- a/store/storetest/channel_store.go +++ b/store/storetest/channel_store.go @@ -5,6 +5,7 @@ package storetest import ( "sort" + "strings" "testing" "time" @@ -52,6 +53,7 @@ func TestChannelStore(t *testing.T, ss store.Store) { t.Run("GetPinnedPosts", func(t *testing.T) { testChannelStoreGetPinnedPosts(t, ss) }) t.Run("MaxChannelsPerTeam", func(t *testing.T) { testChannelStoreMaxChannelsPerTeam(t, ss) }) t.Run("GetChannelsByScheme", func(t *testing.T) { testChannelStoreGetChannelsByScheme(t, ss) }) + t.Run("MigrateChannelMembers", func(t *testing.T) { testChannelStoreMigrateChannelMembers(t, ss) }) } @@ -2254,3 +2256,76 @@ func testChannelStoreGetChannelsByScheme(t *testing.T, ss store.Store) { d3 := res3.Data.(model.ChannelList) assert.Len(t, d3, 0) } + +func testChannelStoreMigrateChannelMembers(t *testing.T, ss store.Store) { + s1 := model.NewId() + c1 := &model.Channel{ + TeamId: model.NewId(), + DisplayName: "Name", + Name: model.NewId(), + Type: model.CHANNEL_OPEN, + SchemeId: &s1, + } + c1 = (<-ss.Channel().Save(c1, 100)).Data.(*model.Channel) + + cm1 := &model.ChannelMember{ + ChannelId: c1.Id, + UserId: model.NewId(), + ExplicitRoles: "channel_admin channel_user", + NotifyProps: model.GetDefaultChannelNotifyProps(), + } + cm2 := &model.ChannelMember{ + ChannelId: c1.Id, + UserId: model.NewId(), + ExplicitRoles: "channel_user", + NotifyProps: model.GetDefaultChannelNotifyProps(), + } + cm3 := &model.ChannelMember{ + ChannelId: c1.Id, + UserId: model.NewId(), + ExplicitRoles: "something_else", + NotifyProps: model.GetDefaultChannelNotifyProps(), + } + + cm1 = (<-ss.Channel().SaveMember(cm1)).Data.(*model.ChannelMember) + cm2 = (<-ss.Channel().SaveMember(cm2)).Data.(*model.ChannelMember) + cm3 = (<-ss.Channel().SaveMember(cm3)).Data.(*model.ChannelMember) + + lastDoneChannelId := strings.Repeat("0", 26) + lastDoneUserId := strings.Repeat("0", 26) + + for { + res := <-ss.Channel().MigrateChannelMembers(lastDoneChannelId, lastDoneUserId) + if assert.Nil(t, res.Err) { + if res.Data == nil { + break + } + data := res.Data.(map[string]string) + lastDoneChannelId = data["ChannelId"] + lastDoneUserId = data["UserId"] + } + } + + ss.Channel().ClearCaches() + + res1 := <-ss.Channel().GetMember(cm1.ChannelId, cm1.UserId) + assert.Nil(t, res1.Err) + cm1b := res1.Data.(*model.ChannelMember) + assert.Equal(t, "", cm1b.ExplicitRoles) + assert.True(t, cm1b.SchemeUser) + assert.True(t, cm1b.SchemeAdmin) + + res2 := <-ss.Channel().GetMember(cm2.ChannelId, cm2.UserId) + assert.Nil(t, res2.Err) + cm2b := res2.Data.(*model.ChannelMember) + assert.Equal(t, "", cm2b.ExplicitRoles) + assert.True(t, cm2b.SchemeUser) + assert.False(t, cm2b.SchemeAdmin) + + res3 := <-ss.Channel().GetMember(cm3.ChannelId, cm3.UserId) + assert.Nil(t, res3.Err) + cm3b := res3.Data.(*model.ChannelMember) + assert.Equal(t, "something_else", cm3b.ExplicitRoles) + assert.False(t, cm3b.SchemeUser) + assert.False(t, cm3b.SchemeAdmin) +} diff --git a/store/storetest/mocks/ChannelStore.go b/store/storetest/mocks/ChannelStore.go index ecc8b8768..8858e3d3b 100644 --- a/store/storetest/mocks/ChannelStore.go +++ b/store/storetest/mocks/ChannelStore.go @@ -583,6 +583,22 @@ func (_m *ChannelStore) IsUserInChannelUseCache(userId string, channelId string) return r0 } +// MigrateChannelMembers provides a mock function with given fields: fromChannelId, fromUserId +func (_m *ChannelStore) MigrateChannelMembers(fromChannelId string, fromUserId string) store.StoreChannel { + ret := _m.Called(fromChannelId, fromUserId) + + var r0 store.StoreChannel + if rf, ok := ret.Get(0).(func(string, string) store.StoreChannel); ok { + r0 = rf(fromChannelId, fromUserId) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(store.StoreChannel) + } + } + + return r0 +} + // PermanentDelete provides a mock function with given fields: channelId func (_m *ChannelStore) PermanentDelete(channelId string) store.StoreChannel { ret := _m.Called(channelId) diff --git a/store/storetest/mocks/TeamStore.go b/store/storetest/mocks/TeamStore.go index 51a968784..93cb84caf 100644 --- a/store/storetest/mocks/TeamStore.go +++ b/store/storetest/mocks/TeamStore.go @@ -301,6 +301,22 @@ func (_m *TeamStore) GetTotalMemberCount(teamId string) store.StoreChannel { return r0 } +// MigrateTeamMembers provides a mock function with given fields: fromTeamId, fromUserId +func (_m *TeamStore) MigrateTeamMembers(fromTeamId string, fromUserId string) store.StoreChannel { + ret := _m.Called(fromTeamId, fromUserId) + + var r0 store.StoreChannel + if rf, ok := ret.Get(0).(func(string, string) store.StoreChannel); ok { + r0 = rf(fromTeamId, fromUserId) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(store.StoreChannel) + } + } + + return r0 +} + // PermanentDelete provides a mock function with given fields: teamId func (_m *TeamStore) PermanentDelete(teamId string) store.StoreChannel { ret := _m.Called(teamId) diff --git a/store/storetest/team_store.go b/store/storetest/team_store.go index ff79650d5..726c17a99 100644 --- a/store/storetest/team_store.go +++ b/store/storetest/team_store.go @@ -4,6 +4,7 @@ package storetest import ( + "strings" "testing" "time" @@ -39,6 +40,7 @@ func TestTeamStore(t *testing.T, ss store.Store) { t.Run("GetChannelUnreadsForTeam", func(t *testing.T) { testGetChannelUnreadsForTeam(t, ss) }) t.Run("UpdateLastTeamIconUpdate", func(t *testing.T) { testUpdateLastTeamIconUpdate(t, ss) }) t.Run("GetTeamsByScheme", func(t *testing.T) { testGetTeamsByScheme(t, ss) }) + t.Run("MigrateTeamMembers", func(t *testing.T) { testTeamStoreMigrateTeamMembers(t, ss) }) } func testTeamStoreSave(t *testing.T, ss store.Store) { @@ -1098,3 +1100,72 @@ func testGetTeamsByScheme(t *testing.T, ss store.Store) { d3 := res3.Data.([]*model.Team) assert.Len(t, d3, 0) } + +func testTeamStoreMigrateTeamMembers(t *testing.T, ss store.Store) { + s1 := model.NewId() + t1 := &model.Team{ + DisplayName: "Name", + Name: "z-z-z" + model.NewId() + "b", + Email: model.NewId() + "@nowhere.com", + Type: model.TEAM_OPEN, + InviteId: model.NewId(), + SchemeId: &s1, + } + t1 = store.Must(ss.Team().Save(t1)).(*model.Team) + + tm1 := &model.TeamMember{ + TeamId: t1.Id, + UserId: model.NewId(), + ExplicitRoles: "team_admin team_user", + } + tm2 := &model.TeamMember{ + TeamId: t1.Id, + UserId: model.NewId(), + ExplicitRoles: "team_user", + } + tm3 := &model.TeamMember{ + TeamId: t1.Id, + UserId: model.NewId(), + ExplicitRoles: "something_else", + } + + tm1 = (<-ss.Team().SaveMember(tm1, -1)).Data.(*model.TeamMember) + tm2 = (<-ss.Team().SaveMember(tm2, -1)).Data.(*model.TeamMember) + tm3 = (<-ss.Team().SaveMember(tm3, -1)).Data.(*model.TeamMember) + + lastDoneTeamId := strings.Repeat("0", 26) + lastDoneUserId := strings.Repeat("0", 26) + + for { + res := <-ss.Team().MigrateTeamMembers(lastDoneTeamId, lastDoneUserId) + if assert.Nil(t, res.Err) { + if res.Data == nil { + break + } + data := res.Data.(map[string]string) + lastDoneTeamId = data["TeamId"] + lastDoneUserId = data["UserId"] + } + } + + res1 := <-ss.Team().GetMember(tm1.TeamId, tm1.UserId) + assert.Nil(t, res1.Err) + tm1b := res1.Data.(*model.TeamMember) + assert.Equal(t, "", tm1b.ExplicitRoles) + assert.True(t, tm1b.SchemeUser) + assert.True(t, tm1b.SchemeAdmin) + + res2 := <-ss.Team().GetMember(tm2.TeamId, tm2.UserId) + assert.Nil(t, res2.Err) + tm2b := res2.Data.(*model.TeamMember) + assert.Equal(t, "", tm2b.ExplicitRoles) + assert.True(t, tm2b.SchemeUser) + assert.False(t, tm2b.SchemeAdmin) + + res3 := <-ss.Team().GetMember(tm3.TeamId, tm3.UserId) + assert.Nil(t, res3.Err) + tm3b := res3.Data.(*model.TeamMember) + assert.Equal(t, "something_else", tm3b.ExplicitRoles) + assert.False(t, tm3b.SchemeUser) + assert.False(t, tm3b.SchemeAdmin) +} -- cgit v1.2.3-1-g7c22 From 16bbbc2abca7c2e5dc2e6876da0dba2bae9eed04 Mon Sep 17 00:00:00 2001 From: George Goldberg Date: Wed, 16 May 2018 12:02:43 +0100 Subject: Fix build failure from bad merge. --- store/sqlstore/channel_store.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/store/sqlstore/channel_store.go b/store/sqlstore/channel_store.go index 4baddddb2..4948d0995 100644 --- a/store/sqlstore/channel_store.go +++ b/store/sqlstore/channel_store.go @@ -946,10 +946,6 @@ func (s SqlChannelStore) SaveMember(member *model.ChannelMember) store.StoreChan if err := transaction.Commit(); err != nil { result.Err = model.NewAppError("SqlChannelStore.SaveMember", "store.sql_channel.save_member.commit_transaction.app_error", nil, err.Error(), http.StatusInternalServerError) } - // If successful record members have changed in channel - if mu := <-s.extraUpdated(channel); mu.Err != nil { - result.Err = mu.Err - } } } } -- cgit v1.2.3-1-g7c22 From b9b76b275ac4670dc400357795cd1a45e425eba1 Mon Sep 17 00:00:00 2001 From: George Goldberg Date: Thu, 17 May 2018 10:18:49 +0100 Subject: MM-10234: Make CLI roles command advanced-permissions aware. (#8771) * MM-10234: Make CLI roles command advanced-permissions aware. * Fix for loop scope. * Fix style. --- cmd/commands/roles.go | 48 +++++++++++++++++++++++++++++++++++++++++++--- cmd/commands/roles_test.go | 15 +++++++++++++-- 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/cmd/commands/roles.go b/cmd/commands/roles.go index 6d832d82a..72192c925 100644 --- a/cmd/commands/roles.go +++ b/cmd/commands/roles.go @@ -5,9 +5,12 @@ package commands import ( "errors" + "strings" - "github.com/mattermost/mattermost-server/cmd" "github.com/spf13/cobra" + + "github.com/mattermost/mattermost-server/cmd" + "github.com/mattermost/mattermost-server/model" ) var RolesCmd = &cobra.Command{ @@ -56,7 +59,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 } } @@ -81,7 +104,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/commands/roles_test.go b/cmd/commands/roles_test.go index 1e0a46a4e..7179a9157 100644 --- a/cmd/commands/roles_test.go +++ b/cmd/commands/roles_test.go @@ -21,8 +21,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) + } + } + + cmd.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) } } } -- cgit v1.2.3-1-g7c22 From c2ab85e0a36af24ee804c1d140cfe216022a4e45 Mon Sep 17 00:00:00 2001 From: George Goldberg Date: Thu, 17 May 2018 12:48:18 +0100 Subject: MM-10591: Well known error for all scheme endpoints pre-migration. (#8812) --- api4/scheme_test.go | 60 ++++++++++++++++++++++++++--------------------------- app/scheme.go | 31 ++++++++++++++++++++++++++- i18n/en.json | 4 ++++ 3 files changed, 63 insertions(+), 32 deletions(-) diff --git a/api4/scheme_test.go b/api4/scheme_test.go index 92cfa4d30..2762ef92a 100644 --- a/api4/scheme_test.go +++ b/api4/scheme_test.go @@ -132,7 +132,7 @@ func TestCreateScheme(t *testing.T) { Scope: model.SCHEME_SCOPE_TEAM, } _, r7 := th.SystemAdminClient.CreateScheme(scheme7) - CheckInternalErrorStatus(t, r7) + CheckNotImplementedStatus(t, r7) } func TestGetScheme(t *testing.T) { @@ -148,7 +148,6 @@ func TestGetScheme(t *testing.T) { Scope: model.SCHEME_SCOPE_TEAM, } - // Mark the migration as done while we create the scheme. <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) res := <-th.App.Srv.Store.System().Save(&model.System{Name: model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2, Value: "true"}) assert.Nil(t, res.Err) @@ -156,9 +155,6 @@ func TestGetScheme(t *testing.T) { s1, r1 := th.SystemAdminClient.CreateScheme(scheme1) CheckNoError(t, r1) - res = <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) - assert.Nil(t, res.Err) - assert.Equal(t, s1.Name, scheme1.Name) assert.Equal(t, s1.Description, scheme1.Description) assert.NotZero(t, s1.CreateAt) @@ -192,6 +188,13 @@ func TestGetScheme(t *testing.T) { _, r7 := th.Client.GetScheme(s1.Id) CheckForbiddenStatus(t, r7) + + // Mark the migration as not done. + res = <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) + assert.Nil(t, res.Err) + + _, r8 := th.SystemAdminClient.GetScheme(s1.Id) + CheckNotImplementedStatus(t, r8) } func TestGetSchemes(t *testing.T) { @@ -212,7 +215,6 @@ func TestGetSchemes(t *testing.T) { Scope: model.SCHEME_SCOPE_CHANNEL, } - // Mark the migration as done while we create the scheme. <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) res := <-th.App.Srv.Store.System().Save(&model.System{Name: model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2, Value: "true"}) assert.Nil(t, res.Err) @@ -222,9 +224,6 @@ func TestGetSchemes(t *testing.T) { _, r2 := th.SystemAdminClient.CreateScheme(scheme2) CheckNoError(t, r2) - res = <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) - assert.Nil(t, res.Err) - l3, r3 := th.SystemAdminClient.GetSchemes("", 0, 100) CheckNoError(t, r3) @@ -254,6 +253,13 @@ func TestGetSchemes(t *testing.T) { th.Client.Login(th.BasicUser.Username, th.BasicUser.Password) _, r8 := th.Client.GetSchemes("", 0, 100) CheckForbiddenStatus(t, r8) + + // Mark the migration as not done. + res = <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) + assert.Nil(t, res.Err) + + _, r9 := th.SystemAdminClient.GetSchemes("", 0, 100) + CheckNotImplementedStatus(t, r9) } func TestGetTeamsForScheme(t *testing.T) { @@ -262,7 +268,6 @@ func TestGetTeamsForScheme(t *testing.T) { th.App.SetLicense(model.NewTestLicense("")) - // Mark the migration as done while we create the scheme. <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) res := <-th.App.Srv.Store.System().Save(&model.System{Name: model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2, Value: "true"}) assert.Nil(t, res.Err) @@ -275,9 +280,6 @@ func TestGetTeamsForScheme(t *testing.T) { scheme1, r1 := th.SystemAdminClient.CreateScheme(scheme1) CheckNoError(t, r1) - res = <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) - assert.Nil(t, res.Err) - team1 := &model.Team{ Name: GenerateTestUsername(), DisplayName: "A Test Team", @@ -338,10 +340,6 @@ func TestGetTeamsForScheme(t *testing.T) { _, ri4 := th.Client.GetTeamsForScheme(model.NewId(), 0, 100) CheckForbiddenStatus(t, ri4) - // Mark the migration as done again while we create a scheme. - res = <-th.App.Srv.Store.System().Save(&model.System{Name: model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2, Value: "true"}) - assert.Nil(t, res.Err) - scheme2 := &model.Scheme{ Name: model.NewId(), Description: model.NewId(), @@ -350,11 +348,15 @@ func TestGetTeamsForScheme(t *testing.T) { scheme2, rs2 := th.SystemAdminClient.CreateScheme(scheme2) CheckNoError(t, rs2) + _, ri5 := th.SystemAdminClient.GetTeamsForScheme(scheme2.Id, 0, 100) + CheckBadRequestStatus(t, ri5) + + // Mark the migration as not done. res = <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) assert.Nil(t, res.Err) - _, ri5 := th.SystemAdminClient.GetTeamsForScheme(scheme2.Id, 0, 100) - CheckBadRequestStatus(t, ri5) + _, ri6 := th.SystemAdminClient.GetTeamsForScheme(scheme1.Id, 0, 100) + CheckNotImplementedStatus(t, ri6) } func TestGetChannelsForScheme(t *testing.T) { @@ -363,7 +365,6 @@ func TestGetChannelsForScheme(t *testing.T) { th.App.SetLicense(model.NewTestLicense("")) - // Mark the migration as done while we create the scheme. <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) res := <-th.App.Srv.Store.System().Save(&model.System{Name: model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2, Value: "true"}) assert.Nil(t, res.Err) @@ -376,9 +377,6 @@ func TestGetChannelsForScheme(t *testing.T) { scheme1, r1 := th.SystemAdminClient.CreateScheme(scheme1) CheckNoError(t, r1) - res = <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) - assert.Nil(t, res.Err) - channel1 := &model.Channel{ TeamId: model.NewId(), DisplayName: "A Name", @@ -441,10 +439,6 @@ func TestGetChannelsForScheme(t *testing.T) { _, ri4 := th.Client.GetChannelsForScheme(model.NewId(), 0, 100) CheckForbiddenStatus(t, ri4) - // Mark the migration as done again while we create a scheme. - res = <-th.App.Srv.Store.System().Save(&model.System{Name: model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2, Value: "true"}) - assert.Nil(t, res.Err) - scheme2 := &model.Scheme{ Name: model.NewId(), Description: model.NewId(), @@ -453,11 +447,15 @@ func TestGetChannelsForScheme(t *testing.T) { scheme2, rs2 := th.SystemAdminClient.CreateScheme(scheme2) CheckNoError(t, rs2) + _, ri5 := th.SystemAdminClient.GetChannelsForScheme(scheme2.Id, 0, 100) + CheckBadRequestStatus(t, ri5) + + // Mark the migration as not done. res = <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) assert.Nil(t, res.Err) - _, ri5 := th.SystemAdminClient.GetChannelsForScheme(scheme2.Id, 0, 100) - CheckBadRequestStatus(t, ri5) + _, ri6 := th.SystemAdminClient.GetChannelsForScheme(scheme1.Id, 0, 100) + CheckNotImplementedStatus(t, ri6) } func TestPatchScheme(t *testing.T) { @@ -560,7 +558,7 @@ func TestPatchScheme(t *testing.T) { th.App.SetLicense(model.NewTestLicense("")) _, r12 := th.SystemAdminClient.PatchScheme(s6.Id, schemePatch) - CheckInternalErrorStatus(t, r12) + CheckNotImplementedStatus(t, r12) } func TestDeleteScheme(t *testing.T) { @@ -776,6 +774,6 @@ func TestDeleteScheme(t *testing.T) { th.App.SetLicense(model.NewTestLicense("")) _, r6 := th.SystemAdminClient.DeleteScheme(s1.Id) - CheckInternalErrorStatus(t, r6) + CheckNotImplementedStatus(t, r6) }) } diff --git a/app/scheme.go b/app/scheme.go index a8eb9ef46..f1dc256b2 100644 --- a/app/scheme.go +++ b/app/scheme.go @@ -5,9 +5,14 @@ package app import ( "github.com/mattermost/mattermost-server/model" + "net/http" ) 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 { @@ -16,10 +21,18 @@ func (a *App) GetScheme(id string) (*model.Scheme, *model.AppError) { } 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 { @@ -87,10 +100,18 @@ func (a *App) DeleteScheme(schemeId string) (*model.Scheme, *model.AppError) { } 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 { @@ -99,10 +120,18 @@ func (a *App) GetTeamsForScheme(scheme *model.Scheme, offset int, limit int) ([] } 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 { @@ -112,7 +141,7 @@ func (a *App) GetChannelsForScheme(scheme *model.Scheme, offset int, limit int) func (a *App) IsPhase2MigrationCompleted() *model.AppError { if result := <-a.Srv.Store.System().GetByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2); result.Err != nil { - return result.Err + return model.NewAppError("App.IsPhase2MigrationCompleted", "app.schemes.is_phase_2_migration_completed.not_completed.app_error", nil, result.Err.Error(), http.StatusNotImplemented) } return nil diff --git a/i18n/en.json b/i18n/en.json index b9b744608..68f6bad59 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -243,6 +243,10 @@ "id": "api.channel.can_manage_channel.private_restricted_system_admin.app_error", "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." -- cgit v1.2.3-1-g7c22 From 319d61123a0418ea9caa9510b8ad1e9a302c7b93 Mon Sep 17 00:00:00 2001 From: George Goldberg Date: Thu, 17 May 2018 12:48:31 +0100 Subject: MM-10615: Reset teams/channels to default scheme on delete scheme. (#8811) --- api4/scheme_test.go | 50 ++++++++------------------------------- i18n/en.json | 8 +++---- store/sqlstore/scheme_supplier.go | 23 +++++++----------- store/storetest/scheme_store.go | 14 +++++++++-- 4 files changed, 34 insertions(+), 61 deletions(-) diff --git a/api4/scheme_test.go b/api4/scheme_test.go index 2762ef92a..16c87cfac 100644 --- a/api4/scheme_test.go +++ b/api4/scheme_test.go @@ -615,29 +615,6 @@ func TestDeleteScheme(t *testing.T) { assert.Nil(t, res.Err) team := res.Data.(*model.Team) - // Try and fail to delete the scheme. - _, r2 := th.SystemAdminClient.DeleteScheme(s1.Id) - CheckInternalErrorStatus(t, r2) - - 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) - - // Change the team using it to a different scheme. - emptyString := "" - team.SchemeId = &emptyString - res = <-th.App.Srv.Store.Team().Update(team) - // Delete the Scheme. _, r3 := th.SystemAdminClient.DeleteScheme(s1.Id) CheckNoError(t, r3) @@ -656,6 +633,11 @@ func TestDeleteScheme(t *testing.T) { 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) { @@ -702,23 +684,6 @@ func TestDeleteScheme(t *testing.T) { assert.Nil(t, res.Err) channel := res.Data.(*model.Channel) - // Try and fail to delete the scheme. - _, r2 := th.SystemAdminClient.DeleteScheme(s1.Id) - CheckInternalErrorStatus(t, r2) - - 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) - - // Change the team using it to a different scheme. - emptyString := "" - channel.SchemeId = &emptyString - res = <-th.App.Srv.Store.Channel().Update(channel) - // Delete the Scheme. _, r3 := th.SystemAdminClient.DeleteScheme(s1.Id) CheckNoError(t, r3) @@ -731,6 +696,11 @@ func TestDeleteScheme(t *testing.T) { 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) { diff --git a/i18n/en.json b/i18n/en.json index 68f6bad59..5dc05eba7 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -6851,16 +6851,16 @@ "translation": "Unable to get the scheme" }, { - "id": "store.sql_scheme.team_count.app_error", - "translation": "Unable to count the number of teams using this 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.channel_count.app_error", - "translation": "Unable to count the number of channels using this scheme" + "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", diff --git a/store/sqlstore/scheme_supplier.go b/store/sqlstore/scheme_supplier.go index 448e5a92f..233d2f660 100644 --- a/store/sqlstore/scheme_supplier.go +++ b/store/sqlstore/scheme_supplier.go @@ -210,27 +210,20 @@ func (s *SqlSupplier) SchemeDelete(ctx context.Context, schemeId string, hints . return result } - // Check that the scheme isn't being used on any Teams or Channels. + // Update any teams or channels using this scheme to the default scheme. if scheme.Scope == model.SCHEME_SCOPE_TEAM { - if c, err := s.GetReplica().SelectInt("SELECT COUNT(*) FROM Teams WHERE SchemeId = :SchemeId", map[string]interface{}{"SchemeId": schemeId}); err != nil { - result.Err = model.NewAppError("SqlSchemeStore.Delete", "store.sql_scheme.team_count.app_error", nil, "Id="+schemeId+", "+err.Error(), http.StatusInternalServerError) + 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 c > 0 { - result.Err = model.NewAppError("SqlSchemeStore.Delete", "store.sql_scheme.delete.scheme_in_use.app_error", nil, "Id="+schemeId, http.StatusInternalServerError) - return result - } } } else if scheme.Scope == model.SCHEME_SCOPE_CHANNEL { - if c, err := s.GetReplica().SelectInt("SELECT COUNT(*) FROM Channels WHERE SchemeId = :SchemeId", map[string]interface{}{"SchemeId": schemeId}); err != nil { - result.Err = model.NewAppError("SqlSchemeStore.Delete", "store.sql_scheme.channel_count.app_error", nil, "Id="+schemeId+", "+err.Error(), http.StatusInternalServerError) + 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 - } else { - if c > 0 { - result.Err = model.NewAppError("SqlSchemeStore.Delete", "store.sql_scheme.delete.scheme_in_use.app_error", nil, "Id="+schemeId, http.StatusInternalServerError) - return result - } } + + // Blow away the channel caches. + s.Channel().ClearCaches() } // Delete the roles belonging to the scheme. diff --git a/store/storetest/scheme_store.go b/store/storetest/scheme_store.go index c0cbe5deb..49bc92bb6 100644 --- a/store/storetest/scheme_store.go +++ b/store/storetest/scheme_store.go @@ -336,7 +336,12 @@ func testSchemeStoreDelete(t *testing.T, ss store.Store) { t4 = tres4.Data.(*model.Team) sres4 := <-ss.Scheme().Delete(d4.Id) - assert.NotNil(t, sres4.Err) + 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{ @@ -360,5 +365,10 @@ func testSchemeStoreDelete(t *testing.T, ss store.Store) { c5 = cres5.Data.(*model.Channel) sres5 := <-ss.Scheme().Delete(d5.Id) - assert.NotNil(t, sres5.Err) + 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) } -- cgit v1.2.3-1-g7c22 From d61f13d9b47efbbfb220b1ea33623372c4f6d1ae Mon Sep 17 00:00:00 2001 From: Martin Kraft Date: Thu, 17 May 2018 08:20:36 -0400 Subject: Merge error fix. --- api/apitestlib.go | 522 ------------------------------------------------------ 1 file changed, 522 deletions(-) delete mode 100644 api/apitestlib.go diff --git a/api/apitestlib.go b/api/apitestlib.go deleted file mode 100644 index b1e1f0ec8..000000000 --- a/api/apitestlib.go +++ /dev/null @@ -1,522 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package api - -import ( - "fmt" - "io" - "io/ioutil" - "net" - "os" - "strings" - "time" - - "github.com/mattermost/mattermost-server/api4" - "github.com/mattermost/mattermost-server/app" - "github.com/mattermost/mattermost-server/mlog" - "github.com/mattermost/mattermost-server/model" - "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" - "github.com/mattermost/mattermost-server/wsapi" -) - -type TestHelper struct { - App *app.App - tempConfigPath string - - BasicClient *model.Client - BasicTeam *model.Team - BasicUser *model.User - BasicUser2 *model.User - BasicChannel *model.Channel - BasicPost *model.Post - PinnedPost *model.Post - - SystemAdminClient *model.Client - SystemAdminTeam *model.Team - SystemAdminUser *model.User - SystemAdminChannel *model.Channel -} - -type persistentTestStore struct { - store.Store -} - -func (*persistentTestStore) Close() {} - -var testStoreContainer *storetest.RunningContainer -var testStore *persistentTestStore - -// 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) { - testStoreContainer = container - testStore = &persistentTestStore{store.NewLayeredStore(sqlstore.NewSqlSupplier(*settings, nil), nil, nil)} -} - -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, - tempConfigPath: tempConfig.Name(), - } - - th.App.UpdateConfig(func(cfg *model.Config) { - *cfg.TeamSettings.MaxUsersPerTeam = 50 - *cfg.RateLimitSettings.Enable = false - cfg.EmailSettings.SendEmailNotifications = true - *cfg.ServiceSettings.EnableAPIv3 = true - }) - 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 }) - api4.Init(th.App, th.App.Srv.Router, false) - Init(th.App, th.App.Srv.Router) - wsapi.Init(th.App, th.App.Srv.WebSocketRouter) - th.App.Srv.Store.MarkSystemRanUnitTests() - th.App.DoAdvancedPermissionsMigration() - - 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.waitForConnectivity() - - me.BasicClient = me.CreateClient() - me.BasicUser = me.CreateUser(me.BasicClient) - me.App.UpdateUserRoles(me.BasicUser.Id, model.SYSTEM_USER_ROLE_ID, false) - me.LoginBasic() - me.BasicTeam = me.CreateTeam(me.BasicClient) - me.LinkUserToTeam(me.BasicUser, me.BasicTeam) - me.UpdateUserToNonTeamAdmin(me.BasicUser, me.BasicTeam) - me.BasicUser2 = me.CreateUser(me.BasicClient) - me.LinkUserToTeam(me.BasicUser2, me.BasicTeam) - me.BasicClient.SetTeamId(me.BasicTeam.Id) - me.BasicChannel = me.CreateChannel(me.BasicClient, me.BasicTeam) - me.BasicPost = me.CreatePost(me.BasicClient, me.BasicChannel) - - pinnedPostChannel := me.CreateChannel(me.BasicClient, me.BasicTeam) - me.PinnedPost = me.CreatePinnedPost(me.BasicClient, pinnedPostChannel) - - return me -} - -func (me *TestHelper) InitSystemAdmin() *TestHelper { - me.waitForConnectivity() - - me.SystemAdminClient = me.CreateClient() - me.SystemAdminUser = me.CreateUser(me.SystemAdminClient) - me.SystemAdminUser.Password = "Password1" - me.LoginSystemAdmin() - me.SystemAdminTeam = me.CreateTeam(me.SystemAdminClient) - me.LinkUserToTeam(me.SystemAdminUser, me.SystemAdminTeam) - me.SystemAdminClient.SetTeamId(me.SystemAdminTeam.Id) - me.App.UpdateUserRoles(me.SystemAdminUser.Id, model.SYSTEM_USER_ROLE_ID+" "+model.SYSTEM_ADMIN_ROLE_ID, false) - me.SystemAdminChannel = me.CreateChannel(me.SystemAdminClient, me.SystemAdminTeam) - - return me -} - -func (me *TestHelper) waitForConnectivity() { - for i := 0; i < 1000; i++ { - conn, err := net.Dial("tcp", fmt.Sprintf("localhost:%v", me.App.Srv.ListenAddr.Port)) - if err == nil { - conn.Close() - return - } - time.Sleep(time.Millisecond * 20) - } - panic("unable to connect") -} - -func (me *TestHelper) CreateClient() *model.Client { - return model.NewClient(fmt.Sprintf("http://localhost:%v", me.App.Srv.ListenAddr.Port)) -} - -func (me *TestHelper) CreateWebSocketClient() (*model.WebSocketClient, *model.AppError) { - return model.NewWebSocketClient(fmt.Sprintf("ws://localhost:%v", me.App.Srv.ListenAddr.Port), me.BasicClient.AuthToken) -} - -func (me *TestHelper) CreateTeam(client *model.Client) *model.Team { - id := model.NewId() - team := &model.Team{ - DisplayName: "dn_" + id, - Name: GenerateTestTeamName(), - Email: me.GenerateTestEmail(), - Type: model.TEAM_OPEN, - } - - utils.DisableDebugLogForTest() - r := client.Must(client.CreateTeam(team)).Data.(*model.Team) - utils.EnableDebugLogForTest() - return r -} - -func (me *TestHelper) CreateUser(client *model.Client) *model.User { - id := model.NewId() - - user := &model.User{ - Email: me.GenerateTestEmail(), - Username: "un_" + id, - Nickname: "nn_" + id, - Password: "Password1", - } - - utils.DisableDebugLogForTest() - ruser := client.Must(client.CreateUser(user, "")).Data.(*model.User) - ruser.Password = "Password1" - store.Must(me.App.Srv.Store.User().VerifyEmail(ruser.Id)) - utils.EnableDebugLogForTest() - return ruser -} - -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) UpdateUserToTeamAdmin(user *model.User, team *model.Team) { - utils.DisableDebugLogForTest() - - 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() - - 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() -} - -func (me *TestHelper) MakeUserChannelAdmin(user *model.User, channel *model.Channel) { - utils.DisableDebugLogForTest() - - if cmr := <-me.App.Srv.Store.Channel().GetMember(channel.Id, user.Id); cmr.Err == nil { - cm := cmr.Data.(*model.ChannelMember) - cm.SchemeAdmin = true - if sr := <-me.App.Srv.Store.Channel().UpdateMember(cm); sr.Err != nil { - utils.EnableDebugLogForTest() - panic(sr.Err) - } - } else { - utils.EnableDebugLogForTest() - panic(cmr.Err) - } - - utils.EnableDebugLogForTest() -} - -func (me *TestHelper) MakeUserChannelUser(user *model.User, channel *model.Channel) { - utils.DisableDebugLogForTest() - - if cmr := <-me.App.Srv.Store.Channel().GetMember(channel.Id, user.Id); cmr.Err == nil { - cm := cmr.Data.(*model.ChannelMember) - cm.SchemeAdmin = false - if sr := <-me.App.Srv.Store.Channel().UpdateMember(cm); sr.Err != nil { - utils.EnableDebugLogForTest() - panic(sr.Err) - } - } else { - utils.EnableDebugLogForTest() - panic(cmr.Err) - } - - utils.EnableDebugLogForTest() -} - -func (me *TestHelper) CreateChannel(client *model.Client, team *model.Team) *model.Channel { - return me.createChannel(client, team, model.CHANNEL_OPEN) -} - -func (me *TestHelper) CreatePrivateChannel(client *model.Client, team *model.Team) *model.Channel { - return me.createChannel(client, team, model.CHANNEL_PRIVATE) -} - -func (me *TestHelper) createChannel(client *model.Client, team *model.Team, channelType string) *model.Channel { - id := model.NewId() - - channel := &model.Channel{ - DisplayName: "dn_" + id, - Name: "name_" + id, - Type: channelType, - TeamId: team.Id, - } - - utils.DisableDebugLogForTest() - r := client.Must(client.CreateChannel(channel)).Data.(*model.Channel) - utils.EnableDebugLogForTest() - return r -} - -func (me *TestHelper) CreatePost(client *model.Client, channel *model.Channel) *model.Post { - id := model.NewId() - - post := &model.Post{ - ChannelId: channel.Id, - Message: "message_" + id, - } - - utils.DisableDebugLogForTest() - r := client.Must(client.CreatePost(post)).Data.(*model.Post) - utils.EnableDebugLogForTest() - return r -} - -func (me *TestHelper) CreatePinnedPost(client *model.Client, channel *model.Channel) *model.Post { - id := model.NewId() - - post := &model.Post{ - ChannelId: channel.Id, - Message: "message_" + id, - IsPinned: true, - } - - utils.DisableDebugLogForTest() - r := client.Must(client.CreatePost(post)).Data.(*model.Post) - utils.EnableDebugLogForTest() - return r -} - -func (me *TestHelper) LoginBasic() { - utils.DisableDebugLogForTest() - me.BasicClient.Must(me.BasicClient.Login(me.BasicUser.Email, me.BasicUser.Password)) - utils.EnableDebugLogForTest() -} - -func (me *TestHelper) LoginBasic2() { - utils.DisableDebugLogForTest() - me.BasicClient.Must(me.BasicClient.Login(me.BasicUser2.Email, me.BasicUser2.Password)) - utils.EnableDebugLogForTest() -} - -func (me *TestHelper) LoginSystemAdmin() { - utils.DisableDebugLogForTest() - me.SystemAdminClient.Must(me.SystemAdminClient.Login(me.SystemAdminUser.Email, me.SystemAdminUser.Password)) - utils.EnableDebugLogForTest() -} - -func (me *TestHelper) GenerateTestEmail() string { - if me.App.Config().EmailSettings.SMTPServer != "dockerhost" && os.Getenv("CI_INBUCKET_PORT") == "" { - return strings.ToLower("success+" + model.NewId() + "@simulator.amazonses.com") - } - return strings.ToLower(model.NewId() + "@dockerhost") -} - -func GenerateTestTeamName() string { - return "faketeam" + model.NewRandomString(6) -} - -func (me *TestHelper) TearDown() { - me.App.Shutdown() - os.Remove(me.tempConfigPath) - if err := recover(); err != nil { - StopTestStore() - panic(err) - } -} - -func (me *TestHelper) SaveDefaultRolePermissions() map[string][]string { - utils.DisableDebugLogForTest() - - results := make(map[string][]string) - - for _, roleName := range []string{ - "system_user", - "system_admin", - "team_user", - "team_admin", - "channel_user", - "channel_admin", - } { - role, err1 := me.App.GetRoleByName(roleName) - if err1 != nil { - utils.EnableDebugLogForTest() - panic(err1) - } - - results[roleName] = role.Permissions - } - - utils.EnableDebugLogForTest() - return results -} - -func (me *TestHelper) RestoreDefaultRolePermissions(data map[string][]string) { - utils.DisableDebugLogForTest() - - for roleName, permissions := range data { - role, err1 := me.App.GetRoleByName(roleName) - if err1 != nil { - utils.EnableDebugLogForTest() - panic(err1) - } - - if strings.Join(role.Permissions, " ") == strings.Join(permissions, " ") { - continue - } - - role.Permissions = permissions - - _, err2 := me.App.UpdateRole(role) - if err2 != nil { - utils.EnableDebugLogForTest() - panic(err2) - } - } - - utils.EnableDebugLogForTest() -} - -func (me *TestHelper) RemovePermissionFromRole(permission string, roleName string) { - utils.DisableDebugLogForTest() - - role, err1 := me.App.GetRoleByName(roleName) - if err1 != nil { - utils.EnableDebugLogForTest() - panic(err1) - } - - var newPermissions []string - for _, p := range role.Permissions { - if p != permission { - newPermissions = append(newPermissions, p) - } - } - - if strings.Join(role.Permissions, " ") == strings.Join(newPermissions, " ") { - utils.EnableDebugLogForTest() - return - } - - role.Permissions = newPermissions - - _, err2 := me.App.UpdateRole(role) - if err2 != nil { - utils.EnableDebugLogForTest() - panic(err2) - } - - utils.EnableDebugLogForTest() -} - -func (me *TestHelper) AddPermissionToRole(permission string, roleName string) { - utils.DisableDebugLogForTest() - - role, err1 := me.App.GetRoleByName(roleName) - if err1 != nil { - utils.EnableDebugLogForTest() - panic(err1) - } - - for _, existingPermission := range role.Permissions { - if existingPermission == permission { - utils.EnableDebugLogForTest() - return - } - } - - role.Permissions = append(role.Permissions, permission) - - _, err2 := me.App.UpdateRole(role) - if err2 != nil { - utils.EnableDebugLogForTest() - panic(err2) - } - - utils.EnableDebugLogForTest() -} -- cgit v1.2.3-1-g7c22 From a09dc68e1d99394f5d636284e0580dd17b2773b3 Mon Sep 17 00:00:00 2001 From: George Goldberg Date: Thu, 17 May 2018 16:28:14 +0100 Subject: MM-10235: Make permissions reset CLI shcemes-aware. (#8773) * MM-10235: Make permissions reset CLI shcemes-aware. * Add i18n strings. --- app/permissions.go | 15 ++++++++ i18n/en.json | 12 +++++++ store/layered_store.go | 6 ++++ store/layered_store_supplier.go | 1 + store/local_cache_supplier_schemes.go | 6 ++++ store/redis_supplier_schemes.go | 5 +++ store/sqlstore/channel_store.go | 8 +++++ store/sqlstore/scheme_supplier.go | 10 ++++++ store/sqlstore/team_store.go | 8 +++++ store/store.go | 3 ++ store/storetest/channel_store.go | 41 ++++++++++++++++++++++ store/storetest/mocks/ChannelStore.go | 16 +++++++++ store/storetest/mocks/LayeredStoreDatabaseLayer.go | 23 ++++++++++++ store/storetest/mocks/LayeredStoreSupplier.go | 23 ++++++++++++ store/storetest/mocks/SchemeStore.go | 16 +++++++++ store/storetest/mocks/TeamStore.go | 16 +++++++++ store/storetest/scheme_store.go | 31 ++++++++++++++++ store/storetest/team_store.go | 41 ++++++++++++++++++++++ 18 files changed, 281 insertions(+) diff --git a/app/permissions.go b/app/permissions.go index be975e03d..75aa2ecf9 100644 --- a/app/permissions.go +++ b/app/permissions.go @@ -8,6 +8,21 @@ import ( ) 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 diff --git a/i18n/en.json b/i18n/en.json index 5dc05eba7..8d2c90db0 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -275,6 +275,18 @@ "id": "api.channel.convert_channel_to_private.private_channel_error", "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" diff --git a/store/layered_store.go b/store/layered_store.go index cbabe9d22..69513febf 100644 --- a/store/layered_store.go +++ b/store/layered_store.go @@ -298,3 +298,9 @@ func (s *LayeredSchemeStore) GetAllPage(scope string, offset int, limit int) Sto 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 4f57004bb..6bf4a0310 100644 --- a/store/layered_store_supplier.go +++ b/store/layered_store_supplier.go @@ -43,4 +43,5 @@ type LayeredStoreSupplier interface { 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_schemes.go b/store/local_cache_supplier_schemes.go index 809c60510..b6cde0fc4 100644 --- a/store/local_cache_supplier_schemes.go +++ b/store/local_cache_supplier_schemes.go @@ -46,3 +46,9 @@ func (s *LocalCacheSupplier) SchemeDelete(ctx context.Context, schemeId string, 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_schemes.go b/store/redis_supplier_schemes.go index 3bd747044..1af9dafde 100644 --- a/store/redis_supplier_schemes.go +++ b/store/redis_supplier_schemes.go @@ -28,3 +28,8 @@ func (s *RedisSupplier) SchemeGetAllPage(ctx context.Context, scope string, offs // 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 4948d0995..5f336d904 100644 --- a/store/sqlstore/channel_store.go +++ b/store/sqlstore/channel_store.go @@ -1772,3 +1772,11 @@ func (s SqlChannelStore) MigrateChannelMembers(fromChannelId string, fromUserId 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/scheme_supplier.go b/store/sqlstore/scheme_supplier.go index 233d2f660..e15bb3629 100644 --- a/store/sqlstore/scheme_supplier.go +++ b/store/sqlstore/scheme_supplier.go @@ -282,3 +282,13 @@ func (s *SqlSupplier) SchemeGetAllPage(ctx context.Context, scope string, offset 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/team_store.go b/store/sqlstore/team_store.go index ea5f7fd1f..f8d76bba1 100644 --- a/store/sqlstore/team_store.go +++ b/store/sqlstore/team_store.go @@ -794,3 +794,11 @@ func (s SqlTeamStore) MigrateTeamMembers(fromTeamId string, fromUserId string) s 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/store.go b/store/store.go index bf2ac42f5..bfc0ab845 100644 --- a/store/store.go +++ b/store/store.go @@ -106,6 +106,7 @@ type TeamStore interface { 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 { @@ -165,6 +166,7 @@ type ChannelStore interface { ClearCaches() GetChannelsByScheme(schemeId string, offset int, limit int) StoreChannel MigrateChannelMembers(fromChannelId string, fromUserId string) StoreChannel + ResetAllChannelSchemes() StoreChannel } type ChannelMemberHistoryStore interface { @@ -489,4 +491,5 @@ type SchemeStore interface { 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 6e0d30a2d..21db7eb91 100644 --- a/store/storetest/channel_store.go +++ b/store/storetest/channel_store.go @@ -54,6 +54,7 @@ func TestChannelStore(t *testing.T, ss store.Store) { 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) }) } @@ -2296,3 +2297,43 @@ func testChannelStoreMigrateChannelMembers(t *testing.T, ss store.Store) { assert.False(t, cm3b.SchemeUser) assert.False(t, cm3b.SchemeAdmin) } + +func testResetAllChannelSchemes(t *testing.T, ss store.Store) { + s1 := &model.Scheme{ + Name: 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 8858e3d3b..10ac908e4 100644 --- a/store/storetest/mocks/ChannelStore.go +++ b/store/storetest/mocks/ChannelStore.go @@ -679,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 e6b8bafb1..c5b821b05 100644 --- a/store/storetest/mocks/LayeredStoreDatabaseLayer.go +++ b/store/storetest/mocks/LayeredStoreDatabaseLayer.go @@ -655,6 +655,29 @@ func (_m *LayeredStoreDatabaseLayer) SchemeGetAllPage(ctx context.Context, scope 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)) diff --git a/store/storetest/mocks/LayeredStoreSupplier.go b/store/storetest/mocks/LayeredStoreSupplier.go index ef6fd99e2..37a01df14 100644 --- a/store/storetest/mocks/LayeredStoreSupplier.go +++ b/store/storetest/mocks/LayeredStoreSupplier.go @@ -352,6 +352,29 @@ func (_m *LayeredStoreSupplier) SchemeGetAllPage(ctx context.Context, scope stri 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)) diff --git a/store/storetest/mocks/SchemeStore.go b/store/storetest/mocks/SchemeStore.go index 2868521b3..ffb10f931 100644 --- a/store/storetest/mocks/SchemeStore.go +++ b/store/storetest/mocks/SchemeStore.go @@ -61,6 +61,22 @@ func (_m *SchemeStore) GetAllPage(scope string, offset int, limit int) store.Sto 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) diff --git a/store/storetest/mocks/TeamStore.go b/store/storetest/mocks/TeamStore.go index 93cb84caf..ef5529a1f 100644 --- a/store/storetest/mocks/TeamStore.go +++ b/store/storetest/mocks/TeamStore.go @@ -381,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/scheme_store.go b/store/storetest/scheme_store.go index 49bc92bb6..636000953 100644 --- a/store/storetest/scheme_store.go +++ b/store/storetest/scheme_store.go @@ -19,6 +19,7 @@ func TestSchemeStore(t *testing.T, ss store.Store) { 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) { @@ -372,3 +373,33 @@ func testSchemeStoreDelete(t *testing.T, ss store.Store) { c6 := cres6.Data.(*model.Channel) assert.Equal(t, "", *c6.SchemeId) } + +func testSchemeStorePermanentDeleteAll(t *testing.T, ss store.Store) { + s1 := &model.Scheme{ + Name: model.NewId(), + Description: model.NewId(), + Scope: model.SCHEME_SCOPE_TEAM, + } + + s2 := &model.Scheme{ + 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) + + 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/team_store.go b/store/storetest/team_store.go index 726c17a99..996f8fd8f 100644 --- a/store/storetest/team_store.go +++ b/store/storetest/team_store.go @@ -41,6 +41,7 @@ func TestTeamStore(t *testing.T, ss store.Store) { 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) { @@ -1169,3 +1170,43 @@ func testTeamStoreMigrateTeamMembers(t *testing.T, ss store.Store) { assert.False(t, tm3b.SchemeUser) assert.False(t, tm3b.SchemeAdmin) } + +func testResetAllTeamSchemes(t *testing.T, ss store.Store) { + s1 := &model.Scheme{ + Name: 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) +} -- cgit v1.2.3-1-g7c22 From 463065c8ba4b4aece7fd9b7764ba917df3e73292 Mon Sep 17 00:00:00 2001 From: George Goldberg Date: Thu, 17 May 2018 16:29:31 +0100 Subject: MM-10606: License feature flag for custom schemes. (#8804) * MM-10606: Add new field to license for custom schemes. * Add feature flag to license check for Schemes. --- api4/scheme.go | 6 +++--- api4/scheme_test.go | 24 ++++++++++++------------ model/license.go | 6 ++++++ model/license_test.go | 10 ++++++++++ utils/license.go | 1 + 5 files changed, 32 insertions(+), 15 deletions(-) diff --git a/api4/scheme.go b/api4/scheme.go index bdfe69870..5070d1c4a 100644 --- a/api4/scheme.go +++ b/api4/scheme.go @@ -26,7 +26,7 @@ func createScheme(c *Context, w http.ResponseWriter, r *http.Request) { return } - if c.App.License() == nil { + 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 } @@ -161,7 +161,7 @@ func patchScheme(c *Context, w http.ResponseWriter, r *http.Request) { return } - if c.App.License() == nil { + 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 } @@ -192,7 +192,7 @@ func deleteScheme(c *Context, w http.ResponseWriter, r *http.Request) { return } - if c.App.License() == nil { + 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 } diff --git a/api4/scheme_test.go b/api4/scheme_test.go index 16c87cfac..9e5ed1aca 100644 --- a/api4/scheme_test.go +++ b/api4/scheme_test.go @@ -16,7 +16,7 @@ func TestCreateScheme(t *testing.T) { th := Setup().InitBasic().InitSystemAdmin() defer th.TearDown() - th.App.SetLicense(model.NewTestLicense("")) + th.App.SetLicense(model.NewTestLicense("custom_permissions_schemes")) // Mark the migration as done. <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) @@ -124,7 +124,7 @@ func TestCreateScheme(t *testing.T) { assert.Nil(t, res.Err) th.LoginSystemAdmin() - th.App.SetLicense(model.NewTestLicense("")) + th.App.SetLicense(model.NewTestLicense("custom_permissions_schemes")) scheme7 := &model.Scheme{ Name: model.NewId(), @@ -139,7 +139,7 @@ func TestGetScheme(t *testing.T) { th := Setup().InitBasic().InitSystemAdmin() defer th.TearDown() - th.App.SetLicense(model.NewTestLicense("")) + th.App.SetLicense(model.NewTestLicense("custom_permissions_schemes")) // Basic test of creating a team scheme. scheme1 := &model.Scheme{ @@ -201,7 +201,7 @@ func TestGetSchemes(t *testing.T) { th := Setup().InitBasic().InitSystemAdmin() defer th.TearDown() - th.App.SetLicense(model.NewTestLicense("")) + th.App.SetLicense(model.NewTestLicense("custom_permissions_schemes")) scheme1 := &model.Scheme{ Name: model.NewId(), @@ -266,7 +266,7 @@ func TestGetTeamsForScheme(t *testing.T) { th := Setup().InitBasic().InitSystemAdmin() defer th.TearDown() - th.App.SetLicense(model.NewTestLicense("")) + th.App.SetLicense(model.NewTestLicense("custom_permissions_schemes")) <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) res := <-th.App.Srv.Store.System().Save(&model.System{Name: model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2, Value: "true"}) @@ -363,7 +363,7 @@ func TestGetChannelsForScheme(t *testing.T) { th := Setup().InitBasic().InitSystemAdmin() defer th.TearDown() - th.App.SetLicense(model.NewTestLicense("")) + th.App.SetLicense(model.NewTestLicense("custom_permissions_schemes")) <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) res := <-th.App.Srv.Store.System().Save(&model.System{Name: model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2, Value: "true"}) @@ -462,7 +462,7 @@ func TestPatchScheme(t *testing.T) { th := Setup().InitBasic().InitSystemAdmin() defer th.TearDown() - th.App.SetLicense(model.NewTestLicense("")) + th.App.SetLicense(model.NewTestLicense("custom_permissions_schemes")) // Mark the migration as done. <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) @@ -555,7 +555,7 @@ func TestPatchScheme(t *testing.T) { assert.Nil(t, res.Err) th.LoginSystemAdmin() - th.App.SetLicense(model.NewTestLicense("")) + th.App.SetLicense(model.NewTestLicense("custom_permissions_schemes")) _, r12 := th.SystemAdminClient.PatchScheme(s6.Id, schemePatch) CheckNotImplementedStatus(t, r12) @@ -566,7 +566,7 @@ func TestDeleteScheme(t *testing.T) { defer th.TearDown() t.Run("ValidTeamScheme", func(t *testing.T) { - th.App.SetLicense(model.NewTestLicense("")) + th.App.SetLicense(model.NewTestLicense("custom_permissions_schemes")) // Mark the migration as done. <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) @@ -641,7 +641,7 @@ func TestDeleteScheme(t *testing.T) { }) t.Run("ValidChannelScheme", func(t *testing.T) { - th.App.SetLicense(model.NewTestLicense("")) + th.App.SetLicense(model.NewTestLicense("custom_permissions_schemes")) // Mark the migration as done. <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) @@ -704,7 +704,7 @@ func TestDeleteScheme(t *testing.T) { }) t.Run("FailureCases", func(t *testing.T) { - th.App.SetLicense(model.NewTestLicense("")) + th.App.SetLicense(model.NewTestLicense("custom_permissions_schemes")) // Mark the migration as done. <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) @@ -741,7 +741,7 @@ func TestDeleteScheme(t *testing.T) { res = <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) assert.Nil(t, res.Err) - th.App.SetLicense(model.NewTestLicense("")) + th.App.SetLicense(model.NewTestLicense("custom_permissions_schemes")) _, r6 := th.SystemAdminClient.DeleteScheme(s1.Id) CheckNotImplementedStatus(t, r6) diff --git a/model/license.go b/model/license.go index 942a18d55..dea326287 100644 --- a/model/license.go +++ b/model/license.go @@ -56,6 +56,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"` @@ -78,6 +79,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, } } @@ -158,6 +160,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 f953d47b3..93f2ff61a 100644 --- a/model/license_test.go +++ b/model/license_test.go @@ -28,6 +28,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)) } @@ -50,6 +52,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{} @@ -70,6 +74,8 @@ func TestLicenseFeaturesSetDefaults(t *testing.T) { *f.PasswordRequirements = true *f.Elasticsearch = true *f.DataRetention = true + *f.MessageExport = true + *f.CustomPermissionsSchemes = true *f.EmailNotificationContents = true f.SetDefaults() @@ -89,6 +95,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) } @@ -171,6 +179,8 @@ func TestLicenseToFromJson(t *testing.T) { CheckBool(t, *f1.PasswordRequirements, *f.PasswordRequirements) 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/utils/license.go b/utils/license.go index aa89026ea..1d76cf994 100644 --- a/utils/license.go +++ b/utils/license.go @@ -152,6 +152,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 -- cgit v1.2.3-1-g7c22 From e0390632b3c941670671d968b8828bcefbf71581 Mon Sep 17 00:00:00 2001 From: Martin Kraft Date: Thu, 17 May 2018 11:37:00 -0400 Subject: MM-10264: Adds CLI command to import and export permissions. (#8787) * MM-10264: Adds CLI command to import and export permissions. * MM-10264: Changes Scheme Name to DisplayName and adds Name slug field. * MM-10264: Changes display name max size. * MM-10264: Another merge fix. * MM-10264: Changes for more Schemes methods checking for migration. * MM-10264: More updates for Schemes migration checking. --- api4/channel_test.go | 6 +- api4/scheme_test.go | 41 +++++- api4/team_test.go | 6 +- app/apptestlib.go | 34 +++++ app/permissions.go | 143 +++++++++++++++++++++ app/permissions_test.go | 253 ++++++++++++++++++++++++++++++++++++++ app/scheme.go | 16 ++- cmd/commands/permissions.go | 65 ++++++++++ cmd/commands/permissions_test.go | 40 ++++++ model/scheme.go | 53 +++++++- store/sqlstore/scheme_supplier.go | 5 +- store/storetest/channel_store.go | 2 + store/storetest/scheme_store.go | 18 +++ store/storetest/team_store.go | 2 + 14 files changed, 670 insertions(+), 14 deletions(-) create mode 100644 app/permissions_test.go create mode 100644 cmd/commands/permissions_test.go diff --git a/api4/channel_test.go b/api4/channel_test.go index 551a1a484..f871e66ea 100644 --- a/api4/channel_test.go +++ b/api4/channel_test.go @@ -1924,13 +1924,15 @@ func TestUpdateChannelScheme(t *testing.T) { channel, _ = th.SystemAdminClient.CreateChannel(channel) channelScheme := &model.Scheme{ - Name: "Name", + DisplayName: "DisplayName", + Name: model.NewId(), Description: "Some description", Scope: model.SCHEME_SCOPE_CHANNEL, } channelScheme, _ = th.SystemAdminClient.CreateScheme(channelScheme) teamScheme := &model.Scheme{ - Name: "Name", + DisplayName: "DisplayName", + Name: model.NewId(), Description: "Some description", Scope: model.SCHEME_SCOPE_TEAM, } diff --git a/api4/scheme_test.go b/api4/scheme_test.go index 9e5ed1aca..461b03421 100644 --- a/api4/scheme_test.go +++ b/api4/scheme_test.go @@ -25,6 +25,7 @@ func TestCreateScheme(t *testing.T) { // Basic test of creating a team scheme. scheme1 := &model.Scheme{ + DisplayName: model.NewId(), Name: model.NewId(), Description: model.NewId(), Scope: model.SCHEME_SCOPE_TEAM, @@ -33,6 +34,7 @@ func TestCreateScheme(t *testing.T) { 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) @@ -56,6 +58,7 @@ func TestCreateScheme(t *testing.T) { // Basic Test of a Channel scheme. scheme2 := &model.Scheme{ + DisplayName: model.NewId(), Name: model.NewId(), Description: model.NewId(), Scope: model.SCHEME_SCOPE_CHANNEL, @@ -64,6 +67,7 @@ func TestCreateScheme(t *testing.T) { 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) @@ -83,6 +87,7 @@ func TestCreateScheme(t *testing.T) { // Try and create a scheme with an invalid scope. scheme3 := &model.Scheme{ + DisplayName: model.NewId(), Name: model.NewId(), Description: model.NewId(), Scope: model.NewId(), @@ -91,17 +96,29 @@ func TestCreateScheme(t *testing.T) { _, r3 := th.SystemAdminClient.CreateScheme(scheme3) CheckBadRequestStatus(t, r3) - // Try and create a scheme with an invalid name. + // Try and create a scheme with an invalid display name. scheme4 := &model.Scheme{ - Name: strings.Repeat(model.NewId(), 100), + 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, @@ -112,6 +129,7 @@ func TestCreateScheme(t *testing.T) { // 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, @@ -127,6 +145,7 @@ func TestCreateScheme(t *testing.T) { 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, @@ -143,6 +162,7 @@ func TestGetScheme(t *testing.T) { // Basic test of creating a team scheme. scheme1 := &model.Scheme{ + DisplayName: model.NewId(), Name: model.NewId(), Description: model.NewId(), Scope: model.SCHEME_SCOPE_TEAM, @@ -155,6 +175,7 @@ func TestGetScheme(t *testing.T) { 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) @@ -204,12 +225,14 @@ func TestGetSchemes(t *testing.T) { 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, @@ -273,6 +296,7 @@ func TestGetTeamsForScheme(t *testing.T) { assert.Nil(t, res.Err) scheme1 := &model.Scheme{ + DisplayName: model.NewId(), Name: model.NewId(), Description: model.NewId(), Scope: model.SCHEME_SCOPE_TEAM, @@ -341,6 +365,7 @@ func TestGetTeamsForScheme(t *testing.T) { CheckForbiddenStatus(t, ri4) scheme2 := &model.Scheme{ + DisplayName: model.NewId(), Name: model.NewId(), Description: model.NewId(), Scope: model.SCHEME_SCOPE_CHANNEL, @@ -370,6 +395,7 @@ func TestGetChannelsForScheme(t *testing.T) { assert.Nil(t, res.Err) scheme1 := &model.Scheme{ + DisplayName: model.NewId(), Name: model.NewId(), Description: model.NewId(), Scope: model.SCHEME_SCOPE_CHANNEL, @@ -440,6 +466,7 @@ func TestGetChannelsForScheme(t *testing.T) { CheckForbiddenStatus(t, ri4) scheme2 := &model.Scheme{ + DisplayName: model.NewId(), Name: model.NewId(), Description: model.NewId(), Scope: model.SCHEME_SCOPE_TEAM, @@ -471,6 +498,7 @@ func TestPatchScheme(t *testing.T) { // Basic test of creating a team scheme. scheme1 := &model.Scheme{ + DisplayName: model.NewId(), Name: model.NewId(), Description: model.NewId(), Scope: model.SCHEME_SCOPE_TEAM, @@ -479,6 +507,7 @@ func TestPatchScheme(t *testing.T) { 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) @@ -497,15 +526,18 @@ func TestPatchScheme(t *testing.T) { // 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) @@ -515,11 +547,13 @@ func TestPatchScheme(t *testing.T) { // 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) @@ -581,6 +615,7 @@ func TestDeleteScheme(t *testing.T) { // Create a team scheme. scheme1 := &model.Scheme{ + DisplayName: model.NewId(), Name: model.NewId(), Description: model.NewId(), Scope: model.SCHEME_SCOPE_TEAM, @@ -656,6 +691,7 @@ func TestDeleteScheme(t *testing.T) { // Create a channel scheme. scheme1 := &model.Scheme{ + DisplayName: model.NewId(), Name: model.NewId(), Description: model.NewId(), Scope: model.SCHEME_SCOPE_CHANNEL, @@ -712,6 +748,7 @@ func TestDeleteScheme(t *testing.T) { assert.Nil(t, res.Err) scheme1 := &model.Scheme{ + DisplayName: model.NewId(), Name: model.NewId(), Description: model.NewId(), Scope: model.SCHEME_SCOPE_CHANNEL, diff --git a/api4/team_test.go b/api4/team_test.go index 45d8e8f08..a3f4af0cf 100644 --- a/api4/team_test.go +++ b/api4/team_test.go @@ -2083,13 +2083,15 @@ func TestUpdateTeamScheme(t *testing.T) { team, _ = th.SystemAdminClient.CreateTeam(team) teamScheme := &model.Scheme{ - Name: "Name", + DisplayName: "DisplayName", + Name: model.NewId(), Description: "Some description", Scope: model.SCHEME_SCOPE_TEAM, } teamScheme, _ = th.SystemAdminClient.CreateScheme(teamScheme) channelScheme := &model.Scheme{ - Name: "Name", + DisplayName: "DisplayName", + Name: model.NewId(), Description: "Some description", Scope: model.SCHEME_SCOPE_CHANNEL, } diff --git a/app/apptestlib.go b/app/apptestlib.go index b245ddabf..ffd1da055 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/permissions.go b/app/permissions.go index 75aa2ecf9..70b8cc689 100644 --- a/app/permissions.go +++ b/app/permissions.go @@ -4,9 +4,17 @@ 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 { @@ -38,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 index f1dc256b2..c44690954 100644 --- a/app/scheme.go +++ b/app/scheme.go @@ -4,8 +4,10 @@ package app import ( - "github.com/mattermost/mattermost-server/model" "net/http" + + "github.com/mattermost/mattermost-server/model" + "github.com/mattermost/mattermost-server/store" ) func (a *App) GetScheme(id string) (*model.Scheme, *model.AppError) { @@ -146,3 +148,15 @@ func (a *App) IsPhase2MigrationCompleted() *model.AppError { 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/cmd/commands/permissions.go b/cmd/commands/permissions.go index 33c255a31..e8f862547 100644 --- a/cmd/commands/permissions.go +++ b/cmd/commands/permissions.go @@ -6,10 +6,12 @@ package commands import ( "errors" "fmt" + "os" "github.com/spf13/cobra" "github.com/mattermost/mattermost-server/cmd" + "github.com/mattermost/mattermost-server/utils" ) var PermissionsCmd = &cobra.Command{ @@ -25,11 +27,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, ) cmd.RootCmd.AddCommand(PermissionsCmd) } @@ -64,3 +87,45 @@ func resetPermissionsCmdF(command *cobra.Command, args []string) error { return nil } + +func exportPermissionsCmdF(command *cobra.Command, args []string) error { + a, err := cmd.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 := cmd.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/commands/permissions_test.go b/cmd/commands/permissions_test.go new file mode 100644 index 000000000..eeaa17109 --- /dev/null +++ b/cmd/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/model/scheme.go b/model/scheme.go index f949d9122..959b80c24 100644 --- a/model/scheme.go +++ b/model/scheme.go @@ -5,19 +5,23 @@ package model import ( "encoding/json" + "fmt" "io" + "regexp" ) const ( - SCHEME_NAME_MAX_LENGTH = 64 - SCHEME_DESCRIPTION_MAX_LENGTH = 1024 - SCHEME_SCOPE_TEAM = "team" - SCHEME_SCOPE_CHANNEL = "channel" + 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"` @@ -31,6 +35,7 @@ type Scheme struct { type SchemePatch struct { Name *string `json:"name"` + DisplayName *string `json:"display_name"` Description *string `json:"description"` } @@ -38,6 +43,32 @@ 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) @@ -72,7 +103,11 @@ func (scheme *Scheme) IsValid() bool { } func (scheme *Scheme) IsValidForCreate() bool { - if len(scheme.Name) == 0 || len(scheme.Name) > SCHEME_NAME_MAX_LENGTH { + if len(scheme.DisplayName) == 0 || len(scheme.DisplayName) > SCHEME_DISPLAY_NAME_MAX_LENGTH { + return false + } + + if !IsValidSchemeName(scheme.Name) { return false } @@ -118,6 +153,9 @@ func (scheme *Scheme) IsValidForCreate() bool { } func (scheme *Scheme) Patch(patch *SchemePatch) { + if patch.DisplayName != nil { + scheme.DisplayName = *patch.DisplayName + } if patch.Name != nil { scheme.Name = *patch.Name } @@ -147,3 +185,8 @@ 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/store/sqlstore/scheme_supplier.go b/store/sqlstore/scheme_supplier.go index e15bb3629..776ca9130 100644 --- a/store/sqlstore/scheme_supplier.go +++ b/store/sqlstore/scheme_supplier.go @@ -20,8 +20,9 @@ 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(64) - table.ColMap("Description").SetMaxSize(1024) + 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) diff --git a/store/storetest/channel_store.go b/store/storetest/channel_store.go index 21db7eb91..d70046d19 100644 --- a/store/storetest/channel_store.go +++ b/store/storetest/channel_store.go @@ -2164,12 +2164,14 @@ func testChannelStoreMaxChannelsPerTeam(t *testing.T, ss store.Store) { 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, diff --git a/store/storetest/scheme_store.go b/store/storetest/scheme_store.go index 636000953..30a63d4c1 100644 --- a/store/storetest/scheme_store.go +++ b/store/storetest/scheme_store.go @@ -63,6 +63,7 @@ func createDefaultRoles(t *testing.T, ss store.Store) { 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, @@ -73,6 +74,7 @@ func testSchemeStoreSave(t *testing.T, ss store.Store) { 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) @@ -116,6 +118,7 @@ func testSchemeStoreSave(t *testing.T, ss store.Store) { 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) @@ -130,6 +133,7 @@ func testSchemeStoreSave(t *testing.T, ss store.Store) { // 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, @@ -142,6 +146,7 @@ func testSchemeStoreSave(t *testing.T, ss store.Store) { 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, @@ -157,6 +162,7 @@ func testSchemeStoreGet(t *testing.T, ss store.Store) { 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) @@ -177,21 +183,25 @@ 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, @@ -211,6 +221,10 @@ func testSchemeStoreGetAllPage(t *testing.T, ss store.Store) { 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) @@ -236,6 +250,7 @@ func testSchemeStoreGetAllPage(t *testing.T, ss store.Store) { 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, @@ -246,6 +261,7 @@ func testSchemeStoreDelete(t *testing.T, ss store.Store) { 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) @@ -317,6 +333,7 @@ func testSchemeStoreDelete(t *testing.T, ss store.Store) { // 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, @@ -346,6 +363,7 @@ func testSchemeStoreDelete(t *testing.T, ss store.Store) { // 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, diff --git a/store/storetest/team_store.go b/store/storetest/team_store.go index 996f8fd8f..72c6f89f1 100644 --- a/store/storetest/team_store.go +++ b/store/storetest/team_store.go @@ -1041,12 +1041,14 @@ func testUpdateLastTeamIconUpdate(t *testing.T, ss store.Store) { 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, -- cgit v1.2.3-1-g7c22 From eddb38bbef67cd003fdcad6f766fae1fd1d28b8a Mon Sep 17 00:00:00 2001 From: George Goldberg Date: Fri, 18 May 2018 11:54:46 +0100 Subject: Fix tests. --- store/storetest/channel_store.go | 1 + store/storetest/scheme_store.go | 2 ++ store/storetest/team_store.go | 1 + 3 files changed, 4 insertions(+) diff --git a/store/storetest/channel_store.go b/store/storetest/channel_store.go index d70046d19..7e0a2d552 100644 --- a/store/storetest/channel_store.go +++ b/store/storetest/channel_store.go @@ -2303,6 +2303,7 @@ func testChannelStoreMigrateChannelMembers(t *testing.T, ss store.Store) { 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, } diff --git a/store/storetest/scheme_store.go b/store/storetest/scheme_store.go index 30a63d4c1..39920c109 100644 --- a/store/storetest/scheme_store.go +++ b/store/storetest/scheme_store.go @@ -395,12 +395,14 @@ func testSchemeStoreDelete(t *testing.T, ss store.Store) { 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, } diff --git a/store/storetest/team_store.go b/store/storetest/team_store.go index 72c6f89f1..b209b48c4 100644 --- a/store/storetest/team_store.go +++ b/store/storetest/team_store.go @@ -1176,6 +1176,7 @@ func testTeamStoreMigrateTeamMembers(t *testing.T, ss store.Store) { 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, } -- cgit v1.2.3-1-g7c22 From 8a0702e0c31014c5f2f8bad8c9e32e9343252469 Mon Sep 17 00:00:00 2001 From: George Goldberg Date: Fri, 18 May 2018 12:47:27 +0100 Subject: Reinstate upgrade code that mysteriously vanished during some merge. --- store/sqlstore/upgrade.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/store/sqlstore/upgrade.go b/store/sqlstore/upgrade.go index 45515178d..371639312 100644 --- a/store/sqlstore/upgrade.go +++ b/store/sqlstore/upgrade.go @@ -425,8 +425,21 @@ 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')") + // saveSchemaVersion(sqlStore, VERSION_5_0_0) //} } -- cgit v1.2.3-1-g7c22 From c6ba5c32d62f8c1fd0e0669a3df0c844ec794590 Mon Sep 17 00:00:00 2001 From: Martin Kraft Date: Fri, 18 May 2018 08:41:44 -0400 Subject: Merge fix. --- cmd/commands/permissions_test.go | 40 ----------------------------- cmd/mattermost/commands/permissions_test.go | 40 +++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 40 deletions(-) delete mode 100644 cmd/commands/permissions_test.go create mode 100644 cmd/mattermost/commands/permissions_test.go diff --git a/cmd/commands/permissions_test.go b/cmd/commands/permissions_test.go deleted file mode 100644 index eeaa17109..000000000 --- a/cmd/commands/permissions_test.go +++ /dev/null @@ -1,40 +0,0 @@ -// 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/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) + } +} -- cgit v1.2.3-1-g7c22 From eb78d273f39202046fa71555a5a19b0ec8a95cb3 Mon Sep 17 00:00:00 2001 From: Martin Kraft Date: Mon, 21 May 2018 06:10:26 -0400 Subject: Refactors migrations check. (#8814) --- api4/channel_test.go | 13 +------- api4/scheme_test.go | 85 +++++++++++----------------------------------------- api4/team_test.go | 11 +------ app/app.go | 13 ++++++++ app/scheme.go | 6 ++++ 5 files changed, 39 insertions(+), 89 deletions(-) diff --git a/api4/channel_test.go b/api4/channel_test.go index f871e66ea..8fd68fc08 100644 --- a/api4/channel_test.go +++ b/api4/channel_test.go @@ -12,8 +12,6 @@ import ( "strings" "testing" - "github.com/stretchr/testify/assert" - "github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/utils" ) @@ -1892,16 +1890,7 @@ func TestUpdateChannelScheme(t *testing.T) { th.App.SetLicense(model.NewTestLicense("")) - // Mark the migration as done. - <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) - res := <-th.App.Srv.Store.System().Save(&model.System{Name: model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2, Value: "true"}) - assert.Nil(t, res.Err) - - // Un-mark the migration at the end of the test. - defer func() { - res := <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) - assert.Nil(t, res.Err) - }() + th.App.SetPhase2PermissionsMigrationStatus(true) team := &model.Team{ DisplayName: "Name", diff --git a/api4/scheme_test.go b/api4/scheme_test.go index 461b03421..67cfda4fc 100644 --- a/api4/scheme_test.go +++ b/api4/scheme_test.go @@ -18,10 +18,7 @@ func TestCreateScheme(t *testing.T) { th.App.SetLicense(model.NewTestLicense("custom_permissions_schemes")) - // Mark the migration as done. - <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) - res := <-th.App.Srv.Store.System().Save(&model.System{Name: model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2, Value: "true"}) - assert.Nil(t, res.Err) + th.App.SetPhase2PermissionsMigrationStatus(true) // Basic test of creating a team scheme. scheme1 := &model.Scheme{ @@ -137,9 +134,7 @@ func TestCreateScheme(t *testing.T) { _, r6 := th.SystemAdminClient.CreateScheme(scheme6) CheckNotImplementedStatus(t, r6) - // Mark the migration as not done. - res = <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) - assert.Nil(t, res.Err) + th.App.SetPhase2PermissionsMigrationStatus(false) th.LoginSystemAdmin() th.App.SetLicense(model.NewTestLicense("custom_permissions_schemes")) @@ -168,9 +163,7 @@ func TestGetScheme(t *testing.T) { Scope: model.SCHEME_SCOPE_TEAM, } - <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) - res := <-th.App.Srv.Store.System().Save(&model.System{Name: model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2, Value: "true"}) - assert.Nil(t, res.Err) + th.App.SetPhase2PermissionsMigrationStatus(true) s1, r1 := th.SystemAdminClient.CreateScheme(scheme1) CheckNoError(t, r1) @@ -210,9 +203,7 @@ func TestGetScheme(t *testing.T) { _, r7 := th.Client.GetScheme(s1.Id) CheckForbiddenStatus(t, r7) - // Mark the migration as not done. - res = <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) - assert.Nil(t, res.Err) + th.App.SetPhase2PermissionsMigrationStatus(false) _, r8 := th.SystemAdminClient.GetScheme(s1.Id) CheckNotImplementedStatus(t, r8) @@ -238,9 +229,7 @@ func TestGetSchemes(t *testing.T) { Scope: model.SCHEME_SCOPE_CHANNEL, } - <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) - res := <-th.App.Srv.Store.System().Save(&model.System{Name: model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2, Value: "true"}) - assert.Nil(t, res.Err) + th.App.SetPhase2PermissionsMigrationStatus(true) _, r1 := th.SystemAdminClient.CreateScheme(scheme1) CheckNoError(t, r1) @@ -277,9 +266,7 @@ func TestGetSchemes(t *testing.T) { _, r8 := th.Client.GetSchemes("", 0, 100) CheckForbiddenStatus(t, r8) - // Mark the migration as not done. - res = <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) - assert.Nil(t, res.Err) + th.App.SetPhase2PermissionsMigrationStatus(false) _, r9 := th.SystemAdminClient.GetSchemes("", 0, 100) CheckNotImplementedStatus(t, r9) @@ -291,9 +278,7 @@ func TestGetTeamsForScheme(t *testing.T) { th.App.SetLicense(model.NewTestLicense("custom_permissions_schemes")) - <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) - res := <-th.App.Srv.Store.System().Save(&model.System{Name: model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2, Value: "true"}) - assert.Nil(t, res.Err) + th.App.SetPhase2PermissionsMigrationStatus(true) scheme1 := &model.Scheme{ DisplayName: model.NewId(), @@ -376,9 +361,7 @@ func TestGetTeamsForScheme(t *testing.T) { _, ri5 := th.SystemAdminClient.GetTeamsForScheme(scheme2.Id, 0, 100) CheckBadRequestStatus(t, ri5) - // Mark the migration as not done. - res = <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) - assert.Nil(t, res.Err) + th.App.SetPhase2PermissionsMigrationStatus(false) _, ri6 := th.SystemAdminClient.GetTeamsForScheme(scheme1.Id, 0, 100) CheckNotImplementedStatus(t, ri6) @@ -390,9 +373,7 @@ func TestGetChannelsForScheme(t *testing.T) { th.App.SetLicense(model.NewTestLicense("custom_permissions_schemes")) - <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) - res := <-th.App.Srv.Store.System().Save(&model.System{Name: model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2, Value: "true"}) - assert.Nil(t, res.Err) + th.App.SetPhase2PermissionsMigrationStatus(true) scheme1 := &model.Scheme{ DisplayName: model.NewId(), @@ -477,9 +458,7 @@ func TestGetChannelsForScheme(t *testing.T) { _, ri5 := th.SystemAdminClient.GetChannelsForScheme(scheme2.Id, 0, 100) CheckBadRequestStatus(t, ri5) - // Mark the migration as not done. - res = <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) - assert.Nil(t, res.Err) + th.App.SetPhase2PermissionsMigrationStatus(false) _, ri6 := th.SystemAdminClient.GetChannelsForScheme(scheme1.Id, 0, 100) CheckNotImplementedStatus(t, ri6) @@ -491,10 +470,7 @@ func TestPatchScheme(t *testing.T) { th.App.SetLicense(model.NewTestLicense("custom_permissions_schemes")) - // Mark the migration as done. - <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) - res := <-th.App.Srv.Store.System().Save(&model.System{Name: model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2, Value: "true"}) - assert.Nil(t, res.Err) + th.App.SetPhase2PermissionsMigrationStatus(true) // Basic test of creating a team scheme. scheme1 := &model.Scheme{ @@ -584,9 +560,7 @@ func TestPatchScheme(t *testing.T) { _, r11 := th.SystemAdminClient.PatchScheme(s6.Id, schemePatch) CheckNotImplementedStatus(t, r11) - // Mark the migration as not done. - res = <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) - assert.Nil(t, res.Err) + th.App.SetPhase2PermissionsMigrationStatus(false) th.LoginSystemAdmin() th.App.SetLicense(model.NewTestLicense("custom_permissions_schemes")) @@ -602,16 +576,7 @@ func TestDeleteScheme(t *testing.T) { t.Run("ValidTeamScheme", func(t *testing.T) { th.App.SetLicense(model.NewTestLicense("custom_permissions_schemes")) - // Mark the migration as done. - <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) - res := <-th.App.Srv.Store.System().Save(&model.System{Name: model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2, Value: "true"}) - assert.Nil(t, res.Err) - - // Un-mark the migration at the end of the test. - defer func() { - res := <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) - assert.Nil(t, res.Err) - }() + th.App.SetPhase2PermissionsMigrationStatus(true) // Create a team scheme. scheme1 := &model.Scheme{ @@ -640,7 +605,7 @@ func TestDeleteScheme(t *testing.T) { assert.Zero(t, role4.DeleteAt) // Make sure this scheme is in use by a team. - res = <-th.App.Srv.Store.Team().Save(&model.Team{ + res := <-th.App.Srv.Store.Team().Save(&model.Team{ Name: model.NewId(), DisplayName: model.NewId(), Email: model.NewId() + "@nowhere.com", @@ -678,16 +643,7 @@ func TestDeleteScheme(t *testing.T) { t.Run("ValidChannelScheme", func(t *testing.T) { th.App.SetLicense(model.NewTestLicense("custom_permissions_schemes")) - // Mark the migration as done. - <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) - res := <-th.App.Srv.Store.System().Save(&model.System{Name: model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2, Value: "true"}) - assert.Nil(t, res.Err) - - // Un-mark the migration at the end of the test. - defer func() { - res := <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) - assert.Nil(t, res.Err) - }() + th.App.SetPhase2PermissionsMigrationStatus(true) // Create a channel scheme. scheme1 := &model.Scheme{ @@ -710,7 +666,7 @@ func TestDeleteScheme(t *testing.T) { assert.Zero(t, role4.DeleteAt) // Make sure this scheme is in use by a team. - res = <-th.App.Srv.Store.Channel().Save(&model.Channel{ + res := <-th.App.Srv.Store.Channel().Save(&model.Channel{ TeamId: model.NewId(), DisplayName: model.NewId(), Name: model.NewId(), @@ -742,10 +698,7 @@ func TestDeleteScheme(t *testing.T) { t.Run("FailureCases", func(t *testing.T) { th.App.SetLicense(model.NewTestLicense("custom_permissions_schemes")) - // Mark the migration as done. - <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) - res := <-th.App.Srv.Store.System().Save(&model.System{Name: model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2, Value: "true"}) - assert.Nil(t, res.Err) + th.App.SetPhase2PermissionsMigrationStatus(true) scheme1 := &model.Scheme{ DisplayName: model.NewId(), @@ -774,9 +727,7 @@ func TestDeleteScheme(t *testing.T) { _, r5 := th.SystemAdminClient.DeleteScheme(s1.Id) CheckNotImplementedStatus(t, r5) - // Test with migration not being done. - res = <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) - assert.Nil(t, res.Err) + th.App.SetPhase2PermissionsMigrationStatus(false) th.App.SetLicense(model.NewTestLicense("custom_permissions_schemes")) diff --git a/api4/team_test.go b/api4/team_test.go index a3f4af0cf..f08aa6ba9 100644 --- a/api4/team_test.go +++ b/api4/team_test.go @@ -2059,16 +2059,7 @@ func TestUpdateTeamScheme(t *testing.T) { th.App.SetLicense(model.NewTestLicense("")) - // Mark the migration as done. - <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) - res := <-th.App.Srv.Store.System().Save(&model.System{Name: model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2, Value: "true"}) - assert.Nil(t, res.Err) - - // Un-mark the migration at the end of the test. - defer func() { - res := <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2) - assert.Nil(t, res.Err) - }() + th.App.SetPhase2PermissionsMigrationStatus(true) team := &model.Team{ DisplayName: "Name", diff --git a/app/app.go b/app/app.go index d4a663e32..16470d850 100644 --- a/app/app.go +++ b/app/app.go @@ -92,6 +92,8 @@ type App struct { clientConfig map[string]string clientConfigHash string diagnosticId string + + phase2PermissionsMigrationComplete bool } var appCount = 0 @@ -588,3 +590,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/scheme.go b/app/scheme.go index c44690954..f070e36f8 100644 --- a/app/scheme.go +++ b/app/scheme.go @@ -142,10 +142,16 @@ func (a *App) GetChannelsForScheme(scheme *model.Scheme, offset int, limit int) } 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 } -- cgit v1.2.3-1-g7c22 From e46b94fa66c029b9a493fdc6ff7bcb98e9651568 Mon Sep 17 00:00:00 2001 From: Martin Kraft Date: Tue, 22 May 2018 16:22:20 -0400 Subject: fmt fix. --- store/sqlstore/upgrade.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/store/sqlstore/upgrade.go b/store/sqlstore/upgrade.go index f6eb383f5..93399d7d9 100644 --- a/store/sqlstore/upgrade.go +++ b/store/sqlstore/upgrade.go @@ -427,7 +427,7 @@ func UpgradeDatabaseToVersion410(sqlStore SqlStore) { func UpgradeDatabaseToVersion50(sqlStore SqlStore) { // 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)") -- cgit v1.2.3-1-g7c22 From ac0d656f8289fd48b776207192983e0486cbc6dd Mon Sep 17 00:00:00 2001 From: Martin Kraft Date: Thu, 24 May 2018 09:13:54 -0400 Subject: Merge fix. --- model/channel.go | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) 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 { -- cgit v1.2.3-1-g7c22 From 5f654079c05f7158aeeaecac8d663d5937d84f78 Mon Sep 17 00:00:00 2001 From: Martin Kraft Date: Thu, 24 May 2018 09:29:35 -0400 Subject: Merge fixes. --- migrations/migrationstestlib.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/migrations/migrationstestlib.go b/migrations/migrationstestlib.go index b52f7af79..1b9110425 100644 --- a/migrations/migrationstestlib.go +++ b/migrations/migrationstestlib.go @@ -322,6 +322,8 @@ 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") @@ -417,3 +419,6 @@ func (me *FakeClusterInterface) sendClearRoleCacheMessage() { Event: model.CLUSTER_EVENT_INVALIDATE_CACHE_FOR_ROLES, }) } +func (me *FakeClusterInterface) GetPluginStatuses() (model.PluginStatuses, *model.AppError) { + return nil, nil +} -- cgit v1.2.3-1-g7c22 From a51b2367a34d5cbefe720232b35ccf0d1aa63c98 Mon Sep 17 00:00:00 2001 From: Martin Kraft Date: Thu, 24 May 2018 10:22:54 -0400 Subject: MM-10402: Adds RunJobs to client config API. (#8846) --- utils/config.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/utils/config.go b/utils/config.go index 18e25c999..9a606389d 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["ExperimentalTownSquareIsReadOnly"] = "false" props["ExperimentalEnableAuthenticationTransfer"] = "true" -- cgit v1.2.3-1-g7c22 From f1cd33c8220d6702d9358a0c1eed459807781210 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Fri, 25 May 2018 17:24:29 +0200 Subject: Add autogenerate scheme.name for new schemes without explicit name (#8848) --- store/sqlstore/scheme_supplier.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/store/sqlstore/scheme_supplier.go b/store/sqlstore/scheme_supplier.go index 776ca9130..f272040a6 100644 --- a/store/sqlstore/scheme_supplier.go +++ b/store/sqlstore/scheme_supplier.go @@ -160,6 +160,9 @@ func (s *SqlSupplier) createScheme(ctx context.Context, scheme *model.Scheme, tr } scheme.Id = model.NewId() + if len(scheme.Name) == 0 { + scheme.Name = model.NewId() + } scheme.CreateAt = model.GetMillis() scheme.UpdateAt = scheme.CreateAt -- cgit v1.2.3-1-g7c22 From 7225abddeefb569f1f2da739211d7797b63814a2 Mon Sep 17 00:00:00 2001 From: George Goldberg Date: Mon, 28 May 2018 14:46:52 +0100 Subject: MM-8814: Remove implicit permission grants from post ownership. (#8391) --- api4/post.go | 41 +++++++++++++++++++++++++++++++++++------ app/authorization.go | 13 ------------- 2 files changed, 35 insertions(+), 19 deletions(-) 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/app/authorization.go b/app/authorization.go index 57a38c199..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 { -- cgit v1.2.3-1-g7c22 From bf4cefc3496686850757b2d44219ea2425871dda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 29 May 2018 10:32:07 +0200 Subject: Allow to update the teams scheme to default scheme (#8855) --- api4/team.go | 22 ++++++++++++---------- api4/team_test.go | 4 ++++ 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/api4/team.go b/api4/team.go index 508602d0e..74b385122 100644 --- a/api4/team.go +++ b/api4/team.go @@ -842,7 +842,7 @@ func updateTeamScheme(c *Context, w http.ResponseWriter, r *http.Request) { } schemeID := model.SchemeIDFromJson(r.Body) - if schemeID == nil || len(*schemeID) != 26 { + if schemeID == nil || (len(*schemeID) != 26 && *schemeID != "") { c.SetInvalidParam("scheme_id") return } @@ -857,15 +857,17 @@ func updateTeamScheme(c *Context, w http.ResponseWriter, r *http.Request) { return } - scheme, err := c.App.GetScheme(*schemeID) - if err != nil { - c.Err = err - return - } + if *schemeID != "" { + scheme, err := c.App.GetScheme(*schemeID) + if err != nil { + c.Err = err + return + } - if scheme.Scope != model.SCHEME_SCOPE_TEAM { - c.Err = model.NewAppError("Api4.UpdateTeamScheme", "api.team.update_team_scheme.scheme_scope.error", nil, "", http.StatusBadRequest) - return + 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) @@ -874,7 +876,7 @@ func updateTeamScheme(c *Context, w http.ResponseWriter, r *http.Request) { return } - team.SchemeId = &scheme.Id + team.SchemeId = schemeID _, err = c.App.UpdateTeamScheme(team) if err != nil { diff --git a/api4/team_test.go b/api4/team_test.go index b47a8b650..079ba37ec 100644 --- a/api4/team_test.go +++ b/api4/team_test.go @@ -2095,6 +2095,10 @@ func TestUpdateTeamScheme(t *testing.T) { _, resp := th.SystemAdminClient.UpdateTeamScheme(team.Id, teamScheme.Id) CheckNoError(t, resp) + // Test the return to default scheme + _, resp = th.SystemAdminClient.UpdateTeamScheme(team.Id, "") + CheckNoError(t, resp) + // Test various invalid team and scheme id combinations. _, resp = th.SystemAdminClient.UpdateTeamScheme(team.Id, "x") CheckBadRequestStatus(t, resp) -- cgit v1.2.3-1-g7c22 From e88fe4bb1dea4918284ee3c6e5aee5a8497ff2b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 29 May 2018 16:58:12 +0200 Subject: MM-8853: Adding MANAGE_EMOJIS and MANAGE_OTHERS_EMOJIS permissions (#8860) * MM-8853: Adding MANAGE_EMOJIS and MANAGE_OTHERS_EMOJIS permissions * MM-8853: Removing unnecesary emoji enterprise feature * Create emojis migration * Adding MANAGE_EMOJIS and MANAGE_OTHERS_EMOJIS always to system admins * Simplifing permissions checks * Revert "Simplifing permissions checks" This reverts commit e2cafc1905fc9e20125dd9a1552d2d0c7340ae59. --- api4/apitestlib.go | 1 + api4/emoji.go | 66 ++++++++++++++++--- api4/emoji_test.go | 124 +++++++++++++++++++++++++++++++++- api4/role.go | 1 + app/app.go | 83 ++++++++++++++++++++--- app/app_test.go | 135 ++++++++++++++++++++++++++++++++++++++ app/apptestlib.go | 13 ++++ app/permissions.go | 1 + cmd/mattermost/commands/init.go | 1 + cmd/mattermost/commands/server.go | 1 + einterfaces/emoji.go | 12 ---- i18n/en.json | 4 -- migrations/migrationstestlib.go | 1 + model/permission.go | 16 +++++ store/sqlstore/upgrade.go | 4 ++ web/web_test.go | 1 + 16 files changed, 427 insertions(+), 37 deletions(-) delete mode 100644 einterfaces/emoji.go diff --git a/api4/apitestlib.go b/api4/apitestlib.go index 952c21df3..22084a1d6 100644 --- a/api4/apitestlib.go +++ b/api4/apitestlib.go @@ -125,6 +125,7 @@ func setupTestHelper(enterprise bool) *TestHelper { wsapi.Init(th.App, th.App.Srv.WebSocketRouter) th.App.Srv.Store.MarkSystemRanUnitTests() th.App.DoAdvancedPermissionsMigration() + th.App.DoEmojisPermissionsMigration() th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.EnableOpenServer = true }) diff --git a/api4/emoji.go b/api4/emoji.go index cfb5dd6ab..42f66a22a 100644 --- a/api4/emoji.go +++ b/api4/emoji.go @@ -33,12 +33,6 @@ func createEmoji(c *Context, w http.ResponseWriter, r *http.Request) { return } - if emojiInterface := c.App.Emoji; emojiInterface != nil && - !emojiInterface.CanUserCreateEmoji(c.Session.Roles, c.Session.TeamMembers) { - c.Err = model.NewAppError("getEmoji", "api.emoji.disabled.app_error", nil, "user_id="+c.Session.UserId, http.StatusUnauthorized) - return - } - if len(*c.App.Config().FileSettings.DriverName) == 0 { c.Err = model.NewAppError("createEmoji", "api.emoji.storage.app_error", nil, "", http.StatusNotImplemented) return @@ -54,6 +48,28 @@ func createEmoji(c *Context, w http.ResponseWriter, r *http.Request) { return } + // Allow any user with MANAGE_EMOJIS permission at Team level to manage emojis at system level + memberships, err := c.App.GetTeamMembersForUser(c.Session.UserId) + + if err != nil { + c.Err = err + return + } + + if !c.App.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_EMOJIS) { + hasPermission := false + for _, membership := range memberships { + if c.App.SessionHasPermissionToTeam(c.Session, membership.TeamId, model.PERMISSION_MANAGE_EMOJIS) { + hasPermission = true + break + } + } + if !hasPermission { + c.SetPermissionError(model.PERMISSION_MANAGE_EMOJIS) + return + } + } + m := r.MultipartForm props := m.Value @@ -110,11 +126,45 @@ func deleteEmoji(c *Context, w http.ResponseWriter, r *http.Request) { return } - if c.Session.UserId != emoji.CreatorId && !c.App.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) { - c.Err = model.NewAppError("deleteImage", "api.emoji.delete.permissions.app_error", nil, "user_id="+c.Session.UserId, http.StatusUnauthorized) + // Allow any user with MANAGE_EMOJIS permission at Team level to manage emojis at system level + memberships, err := c.App.GetTeamMembersForUser(c.Session.UserId) + + if err != nil { + c.Err = err return } + if !c.App.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_EMOJIS) { + hasPermission := false + for _, membership := range memberships { + if c.App.SessionHasPermissionToTeam(c.Session, membership.TeamId, model.PERMISSION_MANAGE_EMOJIS) { + hasPermission = true + break + } + } + if !hasPermission { + c.SetPermissionError(model.PERMISSION_MANAGE_EMOJIS) + return + } + } + + if c.Session.UserId != emoji.CreatorId { + if !c.App.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_OTHERS_EMOJIS) { + hasPermission := false + for _, membership := range memberships { + if c.App.SessionHasPermissionToTeam(c.Session, membership.TeamId, model.PERMISSION_MANAGE_OTHERS_EMOJIS) { + hasPermission = true + break + } + } + + if !hasPermission { + c.SetPermissionError(model.PERMISSION_MANAGE_OTHERS_EMOJIS) + return + } + } + } + err = c.App.DeleteEmoji(emoji) if err != nil { c.Err = err diff --git a/api4/emoji_test.go b/api4/emoji_test.go index 39da4aaef..cb6398312 100644 --- a/api4/emoji_test.go +++ b/api4/emoji_test.go @@ -26,6 +26,11 @@ func TestCreateEmoji(t *testing.T) { }() th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCustomEmoji = false }) + defaultRolePermissions := th.SaveDefaultRolePermissions() + defer func() { + th.RestoreDefaultRolePermissions(defaultRolePermissions) + }() + emoji := &model.Emoji{ CreatorId: th.BasicUser.Id, Name: model.NewId(), @@ -141,6 +146,28 @@ func TestCreateEmoji(t *testing.T) { _, resp = Client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 10), "image.gif") CheckForbiddenStatus(t, resp) + + // try to create an emoji without permissions + th.RemovePermissionFromRole(model.PERMISSION_MANAGE_EMOJIS.Id, model.SYSTEM_USER_ROLE_ID) + + emoji = &model.Emoji{ + CreatorId: th.BasicUser.Id, + Name: model.NewId(), + } + + _, resp = Client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 10), "image.gif") + CheckForbiddenStatus(t, resp) + + // create an emoji with permissions in one team + th.AddPermissionToRole(model.PERMISSION_MANAGE_EMOJIS.Id, model.TEAM_USER_ROLE_ID) + + emoji = &model.Emoji{ + CreatorId: th.BasicUser.Id, + Name: model.NewId(), + } + + _, resp = Client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 10), "image.gif") + CheckNoError(t, resp) } func TestGetEmojiList(t *testing.T) { @@ -186,7 +213,7 @@ func TestGetEmojiList(t *testing.T) { } } if !found { - t.Fatalf("failed to get emoji with id %v", emoji.Id) + t.Fatalf("failed to get emoji with id %v, %v", emoji.Id, len(listEmoji)) } } @@ -231,6 +258,11 @@ func TestDeleteEmoji(t *testing.T) { }() th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCustomEmoji = true }) + defaultRolePermissions := th.SaveDefaultRolePermissions() + defer func() { + th.RestoreDefaultRolePermissions(defaultRolePermissions) + }() + emoji := &model.Emoji{ CreatorId: th.BasicUser.Id, Name: model.NewId(), @@ -277,14 +309,100 @@ func TestDeleteEmoji(t *testing.T) { _, resp = Client.DeleteEmoji("") CheckNotFoundStatus(t, resp) - //Try to delete other user's custom emoji + //Try to delete my custom emoji without permissions + newEmoji, resp = Client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 10), "image.gif") + CheckNoError(t, resp) + + th.RemovePermissionFromRole(model.PERMISSION_MANAGE_EMOJIS.Id, model.SYSTEM_USER_ROLE_ID) + _, resp = Client.DeleteEmoji(newEmoji.Id) + CheckForbiddenStatus(t, resp) + th.AddPermissionToRole(model.PERMISSION_MANAGE_EMOJIS.Id, model.SYSTEM_USER_ROLE_ID) + + //Try to delete other user's custom emoji without MANAGE_EMOJIS permissions + emoji = &model.Emoji{ + CreatorId: th.BasicUser.Id, + Name: model.NewId(), + } + newEmoji, resp = Client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 10), "image.gif") CheckNoError(t, resp) + th.RemovePermissionFromRole(model.PERMISSION_MANAGE_EMOJIS.Id, model.SYSTEM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_MANAGE_OTHERS_EMOJIS.Id, model.SYSTEM_USER_ROLE_ID) Client.Logout() th.LoginBasic2() ok, resp = Client.DeleteEmoji(newEmoji.Id) - CheckUnauthorizedStatus(t, resp) + CheckForbiddenStatus(t, resp) + th.RemovePermissionFromRole(model.PERMISSION_MANAGE_OTHERS_EMOJIS.Id, model.SYSTEM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_MANAGE_EMOJIS.Id, model.SYSTEM_USER_ROLE_ID) + Client.Logout() + th.LoginBasic() + + //Try to delete other user's custom emoji without MANAGE_OTHERS_EMOJIS permissions + emoji = &model.Emoji{ + CreatorId: th.BasicUser.Id, + Name: model.NewId(), + } + + newEmoji, resp = Client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 10), "image.gif") + CheckNoError(t, resp) + + Client.Logout() + th.LoginBasic2() + ok, resp = Client.DeleteEmoji(newEmoji.Id) + CheckForbiddenStatus(t, resp) + Client.Logout() + th.LoginBasic() + + //Try to delete other user's custom emoji with permissions + emoji = &model.Emoji{ + CreatorId: th.BasicUser.Id, + Name: model.NewId(), + } + + newEmoji, resp = Client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 10), "image.gif") + CheckNoError(t, resp) + + th.AddPermissionToRole(model.PERMISSION_MANAGE_EMOJIS.Id, model.SYSTEM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_MANAGE_OTHERS_EMOJIS.Id, model.SYSTEM_USER_ROLE_ID) + Client.Logout() + th.LoginBasic2() + ok, resp = Client.DeleteEmoji(newEmoji.Id) + CheckNoError(t, resp) + + Client.Logout() + th.LoginBasic() + + //Try to delete my custom emoji with permissions at team level + newEmoji, resp = Client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 10), "image.gif") + CheckNoError(t, resp) + + th.RemovePermissionFromRole(model.PERMISSION_MANAGE_EMOJIS.Id, model.SYSTEM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_MANAGE_EMOJIS.Id, model.TEAM_USER_ROLE_ID) + _, resp = Client.DeleteEmoji(newEmoji.Id) + CheckNoError(t, resp) + th.AddPermissionToRole(model.PERMISSION_MANAGE_EMOJIS.Id, model.SYSTEM_USER_ROLE_ID) + th.RemovePermissionFromRole(model.PERMISSION_MANAGE_EMOJIS.Id, model.TEAM_USER_ROLE_ID) + + //Try to delete other user's custom emoji with permissions at team level + emoji = &model.Emoji{ + CreatorId: th.BasicUser.Id, + Name: model.NewId(), + } + + newEmoji, resp = Client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 10), "image.gif") + CheckNoError(t, resp) + + th.RemovePermissionFromRole(model.PERMISSION_MANAGE_EMOJIS.Id, model.SYSTEM_USER_ROLE_ID) + th.RemovePermissionFromRole(model.PERMISSION_MANAGE_OTHERS_EMOJIS.Id, model.SYSTEM_USER_ROLE_ID) + + th.AddPermissionToRole(model.PERMISSION_MANAGE_EMOJIS.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_MANAGE_OTHERS_EMOJIS.Id, model.TEAM_USER_ROLE_ID) + + Client.Logout() + th.LoginBasic2() + ok, resp = Client.DeleteEmoji(newEmoji.Id) + CheckNoError(t, resp) } func TestGetEmoji(t *testing.T) { diff --git a/api4/role.go b/api4/role.go index c4203137b..2c0465891 100644 --- a/api4/role.go +++ b/api4/role.go @@ -100,6 +100,7 @@ func patchRole(c *Context, w http.ResponseWriter, r *http.Request) { model.PERMISSION_MANAGE_SLASH_COMMANDS.Id, model.PERMISSION_MANAGE_OAUTH.Id, model.PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH.Id, + model.PERMISSION_MANAGE_EMOJIS.Id, } changedPermissions := model.PermissionsChangedByPatch(oldRole, patch) diff --git a/app/app.go b/app/app.go index e5a496c6b..bda56ca1a 100644 --- a/app/app.go +++ b/app/app.go @@ -30,6 +30,7 @@ import ( ) const ADVANCED_PERMISSIONS_MIGRATION_KEY = "AdvancedPermissionsMigrationComplete" +const EMOJIS_PERMISSIONS_MIGRATION_KEY = "EmojisPermissionsMigrationComplete" type App struct { goroutineCount int32 @@ -57,7 +58,6 @@ type App struct { Compliance einterfaces.ComplianceInterface DataRetention einterfaces.DataRetentionInterface Elasticsearch einterfaces.ElasticsearchInterface - Emoji einterfaces.EmojiInterface Ldap einterfaces.LdapInterface MessageExport einterfaces.MessageExportInterface Metrics einterfaces.MetricsInterface @@ -288,12 +288,6 @@ func RegisterElasticsearchInterface(f func(*App) einterfaces.ElasticsearchInterf elasticsearchInterface = f } -var emojiInterface func(*App) einterfaces.EmojiInterface - -func RegisterEmojiInterface(f func(*App) einterfaces.EmojiInterface) { - emojiInterface = f -} - var jobsDataRetentionJobInterface func(*App) ejobs.DataRetentionJobInterface func RegisterJobsDataRetentionJobInterface(f func(*App) ejobs.DataRetentionJobInterface) { @@ -376,9 +370,6 @@ func (a *App) initEnterprise() { if elasticsearchInterface != nil { a.Elasticsearch = elasticsearchInterface(a) } - if emojiInterface != nil { - a.Emoji = emojiInterface(a) - } if ldapInterface != nil { a.Ldap = ldapInterface(a) a.AddConfigListener(func(_, cfg *model.Config) { @@ -603,3 +594,75 @@ func (a *App) SetPhase2PermissionsMigrationStatus(isComplete bool) error { a.phase2PermissionsMigrationComplete = isComplete return nil } + +func (a *App) DoEmojisPermissionsMigration() { + // If the migration is already marked as completed, don't do it again. + if result := <-a.Srv.Store.System().GetByName(EMOJIS_PERMISSIONS_MIGRATION_KEY); result.Err == nil { + return + } + + var role *model.Role = nil + var systemAdminRole *model.Role = nil + var err *model.AppError = nil + + mlog.Info("Migrating emojis config to database.") + switch *a.Config().ServiceSettings.RestrictCustomEmojiCreation { + case model.RESTRICT_EMOJI_CREATION_ALL: + role, err = a.GetRoleByName(model.SYSTEM_USER_ROLE_ID) + if err != nil { + mlog.Critical("Failed to migrate emojis creation permissions from mattermost config.") + mlog.Critical(err.Error()) + return + } + break + case model.RESTRICT_EMOJI_CREATION_ADMIN: + role, err = a.GetRoleByName(model.TEAM_ADMIN_ROLE_ID) + if err != nil { + mlog.Critical("Failed to migrate emojis creation permissions from mattermost config.") + mlog.Critical(err.Error()) + return + } + break + case model.RESTRICT_EMOJI_CREATION_SYSTEM_ADMIN: + role = nil + break + default: + mlog.Critical("Failed to migrate emojis creation permissions from mattermost config.") + mlog.Critical("Invalid restrict emoji creation setting") + return + } + + if role != nil { + role.Permissions = append(role.Permissions, model.PERMISSION_MANAGE_EMOJIS.Id) + if result := <-a.Srv.Store.Role().Save(role); result.Err != nil { + mlog.Critical("Failed to migrate emojis creation permissions from mattermost config.") + mlog.Critical(result.Err.Error()) + return + } + } + + systemAdminRole, err = a.GetRoleByName(model.SYSTEM_ADMIN_ROLE_ID) + if err != nil { + mlog.Critical("Failed to migrate emojis creation permissions from mattermost config.") + mlog.Critical(err.Error()) + return + } + + systemAdminRole.Permissions = append(systemAdminRole.Permissions, model.PERMISSION_MANAGE_EMOJIS.Id) + systemAdminRole.Permissions = append(systemAdminRole.Permissions, model.PERMISSION_MANAGE_OTHERS_EMOJIS.Id) + if result := <-a.Srv.Store.Role().Save(systemAdminRole); result.Err != nil { + mlog.Critical("Failed to migrate emojis creation permissions from mattermost config.") + mlog.Critical(result.Err.Error()) + return + } + + system := model.System{ + Name: EMOJIS_PERMISSIONS_MIGRATION_KEY, + Value: "true", + } + + if result := <-a.Srv.Store.System().Save(&system); result.Err != nil { + mlog.Critical("Failed to mark emojis permissions migration as completed.") + mlog.Critical(fmt.Sprint(result.Err)) + } +} diff --git a/app/app_test.go b/app/app_test.go index cb6917073..dd6f0b593 100644 --- a/app/app_test.go +++ b/app/app_test.go @@ -455,3 +455,138 @@ func TestDoAdvancedPermissionsMigration(t *testing.T) { *config.ServiceSettings.PostEditTimeLimit = 300 th.App.SaveConfig(config, false) } + +func TestDoEmojisPermissionsMigration(t *testing.T) { + th := Setup() + defer th.TearDown() + + if testStoreSqlSupplier == nil { + t.Skip("This test requires a TestStore to be run.") + } + + // Add a license and change the policy config. + restrictCustomEmojiCreation := *th.App.Config().ServiceSettings.RestrictCustomEmojiCreation + + defer func() { + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.ServiceSettings.RestrictCustomEmojiCreation = restrictCustomEmojiCreation + }) + }() + + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.ServiceSettings.RestrictCustomEmojiCreation = model.RESTRICT_EMOJI_CREATION_SYSTEM_ADMIN + }) + + th.ResetEmojisMigration() + th.App.DoEmojisPermissionsMigration() + + expectedSystemAdmin := []string{ + model.PERMISSION_ASSIGN_SYSTEM_ADMIN_ROLE.Id, + model.PERMISSION_MANAGE_SYSTEM.Id, + model.PERMISSION_MANAGE_ROLES.Id, + model.PERMISSION_MANAGE_PUBLIC_CHANNEL_PROPERTIES.Id, + model.PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS.Id, + model.PERMISSION_MANAGE_PRIVATE_CHANNEL_MEMBERS.Id, + model.PERMISSION_DELETE_PUBLIC_CHANNEL.Id, + model.PERMISSION_CREATE_PUBLIC_CHANNEL.Id, + model.PERMISSION_MANAGE_PRIVATE_CHANNEL_PROPERTIES.Id, + model.PERMISSION_DELETE_PRIVATE_CHANNEL.Id, + model.PERMISSION_CREATE_PRIVATE_CHANNEL.Id, + model.PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH.Id, + model.PERMISSION_MANAGE_OTHERS_WEBHOOKS.Id, + model.PERMISSION_EDIT_OTHER_USERS.Id, + model.PERMISSION_MANAGE_OAUTH.Id, + model.PERMISSION_INVITE_USER.Id, + model.PERMISSION_DELETE_POST.Id, + model.PERMISSION_DELETE_OTHERS_POSTS.Id, + model.PERMISSION_CREATE_TEAM.Id, + model.PERMISSION_ADD_USER_TO_TEAM.Id, + model.PERMISSION_LIST_USERS_WITHOUT_TEAM.Id, + model.PERMISSION_MANAGE_JOBS.Id, + model.PERMISSION_CREATE_POST_PUBLIC.Id, + model.PERMISSION_CREATE_POST_EPHEMERAL.Id, + model.PERMISSION_CREATE_USER_ACCESS_TOKEN.Id, + model.PERMISSION_READ_USER_ACCESS_TOKEN.Id, + model.PERMISSION_REVOKE_USER_ACCESS_TOKEN.Id, + model.PERMISSION_REMOVE_OTHERS_REACTIONS.Id, + model.PERMISSION_LIST_TEAM_CHANNELS.Id, + model.PERMISSION_JOIN_PUBLIC_CHANNELS.Id, + model.PERMISSION_READ_PUBLIC_CHANNEL.Id, + model.PERMISSION_VIEW_TEAM.Id, + model.PERMISSION_READ_CHANNEL.Id, + model.PERMISSION_ADD_REACTION.Id, + model.PERMISSION_REMOVE_REACTION.Id, + model.PERMISSION_UPLOAD_FILE.Id, + model.PERMISSION_GET_PUBLIC_LINK.Id, + model.PERMISSION_CREATE_POST.Id, + model.PERMISSION_USE_SLASH_COMMANDS.Id, + model.PERMISSION_EDIT_OTHERS_POSTS.Id, + model.PERMISSION_REMOVE_USER_FROM_TEAM.Id, + model.PERMISSION_MANAGE_TEAM.Id, + model.PERMISSION_IMPORT_TEAM.Id, + model.PERMISSION_MANAGE_TEAM_ROLES.Id, + model.PERMISSION_MANAGE_CHANNEL_ROLES.Id, + model.PERMISSION_MANAGE_SLASH_COMMANDS.Id, + model.PERMISSION_MANAGE_OTHERS_SLASH_COMMANDS.Id, + model.PERMISSION_MANAGE_WEBHOOKS.Id, + model.PERMISSION_EDIT_POST.Id, + model.PERMISSION_MANAGE_EMOJIS.Id, + model.PERMISSION_MANAGE_OTHERS_EMOJIS.Id, + } + + role1, err1 := th.App.GetRoleByName(model.SYSTEM_ADMIN_ROLE_ID) + assert.Nil(t, err1) + assert.Equal(t, expectedSystemAdmin, role1.Permissions, fmt.Sprintf("'%v' did not have expected permissions", model.SYSTEM_ADMIN_ROLE_ID)) + + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.ServiceSettings.RestrictCustomEmojiCreation = model.RESTRICT_EMOJI_CREATION_ADMIN + }) + + th.ResetEmojisMigration() + th.App.DoEmojisPermissionsMigration() + + role2, err2 := th.App.GetRoleByName(model.TEAM_ADMIN_ROLE_ID) + assert.Nil(t, err2) + expected2 := []string{ + model.PERMISSION_EDIT_OTHERS_POSTS.Id, + model.PERMISSION_REMOVE_USER_FROM_TEAM.Id, + model.PERMISSION_MANAGE_TEAM.Id, + model.PERMISSION_IMPORT_TEAM.Id, + model.PERMISSION_MANAGE_TEAM_ROLES.Id, + model.PERMISSION_MANAGE_CHANNEL_ROLES.Id, + model.PERMISSION_MANAGE_OTHERS_WEBHOOKS.Id, + model.PERMISSION_MANAGE_SLASH_COMMANDS.Id, + model.PERMISSION_MANAGE_OTHERS_SLASH_COMMANDS.Id, + model.PERMISSION_MANAGE_WEBHOOKS.Id, + model.PERMISSION_DELETE_POST.Id, + model.PERMISSION_DELETE_OTHERS_POSTS.Id, + model.PERMISSION_MANAGE_EMOJIS.Id, + } + assert.Equal(t, expected2, role2.Permissions, fmt.Sprintf("'%v' did not have expected permissions", model.TEAM_ADMIN_ROLE_ID)) + + systemAdmin1, systemAdminErr1 := th.App.GetRoleByName(model.SYSTEM_ADMIN_ROLE_ID) + assert.Nil(t, systemAdminErr1) + assert.Equal(t, expectedSystemAdmin, systemAdmin1.Permissions, fmt.Sprintf("'%v' did not have expected permissions", model.SYSTEM_ADMIN_ROLE_ID)) + + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.ServiceSettings.RestrictCustomEmojiCreation = model.RESTRICT_EMOJI_CREATION_ALL + }) + + th.ResetEmojisMigration() + th.App.DoEmojisPermissionsMigration() + + role3, err3 := th.App.GetRoleByName(model.SYSTEM_USER_ROLE_ID) + assert.Nil(t, err3) + expected3 := []string{ + model.PERMISSION_CREATE_DIRECT_CHANNEL.Id, + model.PERMISSION_CREATE_GROUP_CHANNEL.Id, + model.PERMISSION_PERMANENT_DELETE_USER.Id, + model.PERMISSION_CREATE_TEAM.Id, + model.PERMISSION_MANAGE_EMOJIS.Id, + } + assert.Equal(t, expected3, role3.Permissions, fmt.Sprintf("'%v' did not have expected permissions", model.SYSTEM_USER_ROLE_ID)) + + systemAdmin2, systemAdminErr2 := th.App.GetRoleByName(model.SYSTEM_ADMIN_ROLE_ID) + assert.Nil(t, systemAdminErr2) + assert.Equal(t, expectedSystemAdmin, systemAdmin2.Permissions, fmt.Sprintf("'%v' did not have expected permissions", model.SYSTEM_ADMIN_ROLE_ID)) +} diff --git a/app/apptestlib.go b/app/apptestlib.go index ec4992a75..d4a79bdcc 100644 --- a/app/apptestlib.go +++ b/app/apptestlib.go @@ -110,6 +110,7 @@ func setupTestHelper(enterprise bool) *TestHelper { th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ListenAddress = prevListenAddress }) th.App.DoAdvancedPermissionsMigration() + th.App.DoEmojisPermissionsMigration() th.App.Srv.Store.MarkSystemRanUnitTests() @@ -433,6 +434,18 @@ func (me *TestHelper) ResetRoleMigration() { } } +func (me *TestHelper) ResetEmojisMigration() { + if _, err := testStoreSqlSupplier.GetMaster().Exec("UPDATE Roles SET Permissions=REPLACE(Permissions, ', manage_emojis', '') WHERE builtin=True"); err != nil { + panic(err) + } + + testClusterInterface.sendClearRoleCacheMessage() + + if _, err := testStoreSqlSupplier.GetMaster().Exec("DELETE from Systems where Name = :Name", map[string]interface{}{"Name": EMOJIS_PERMISSIONS_MIGRATION_KEY}); err != nil { + panic(err) + } +} + type FakeClusterInterface struct { clusterMessageHandler einterfaces.ClusterMessageHandler } diff --git a/app/permissions.go b/app/permissions.go index 70b8cc689..46090070e 100644 --- a/app/permissions.go +++ b/app/permissions.go @@ -43,6 +43,7 @@ func (a *App) ResetPermissionsSystem() *model.AppError { // Now that the permissions system has been reset, re-run the migration to reinitialise it. a.DoAdvancedPermissionsMigration() + a.DoEmojisPermissionsMigration() return nil } diff --git a/cmd/mattermost/commands/init.go b/cmd/mattermost/commands/init.go index aea2b1230..ea7e8ec84 100644 --- a/cmd/mattermost/commands/init.go +++ b/cmd/mattermost/commands/init.go @@ -23,6 +23,7 @@ func InitDBCommandContextCobra(command *cobra.Command) (*app.App, error) { } a.DoAdvancedPermissionsMigration() + a.DoEmojisPermissionsMigration() return a, nil } diff --git a/cmd/mattermost/commands/server.go b/cmd/mattermost/commands/server.go index 299005b6a..67e2f69c5 100644 --- a/cmd/mattermost/commands/server.go +++ b/cmd/mattermost/commands/server.go @@ -92,6 +92,7 @@ func runServer(configFileLocation string, disableConfigWatch bool, usedPlatform } a.DoAdvancedPermissionsMigration() + a.DoEmojisPermissionsMigration() a.InitPlugins(*a.Config().PluginSettings.Directory, *a.Config().PluginSettings.ClientDirectory, nil) a.AddConfigListener(func(prevCfg, cfg *model.Config) { diff --git a/einterfaces/emoji.go b/einterfaces/emoji.go deleted file mode 100644 index b8d61e748..000000000 --- a/einterfaces/emoji.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package einterfaces - -import ( - "github.com/mattermost/mattermost-server/model" -) - -type EmojiInterface interface { - CanUserCreateEmoji(string, []*model.TeamMember) bool -} diff --git a/i18n/en.json b/i18n/en.json index d631e7e1d..0fc9a733f 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1336,10 +1336,6 @@ "id": "api.emoji.delete.delete_reactions.app_error", "translation": "Unable to delete reactions when deleting emoji with emoji name %v" }, - { - "id": "api.emoji.delete.permissions.app_error", - "translation": "Invalid permissions to delete emoji." - }, { "id": "api.emoji.disabled.app_error", "translation": "Custom emoji have been disabled by the system admin." diff --git a/migrations/migrationstestlib.go b/migrations/migrationstestlib.go index 1b9110425..b501291b0 100644 --- a/migrations/migrationstestlib.go +++ b/migrations/migrationstestlib.go @@ -111,6 +111,7 @@ func setupTestHelper(enterprise bool) *TestHelper { th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ListenAddress = prevListenAddress }) th.App.DoAdvancedPermissionsMigration() + th.App.DoEmojisPermissionsMigration() th.App.Srv.Store.MarkSystemRanUnitTests() diff --git a/model/permission.go b/model/permission.go index 792c7d42e..737321cc7 100644 --- a/model/permission.go +++ b/model/permission.go @@ -50,6 +50,8 @@ var PERMISSION_MANAGE_WEBHOOKS *Permission var PERMISSION_MANAGE_OTHERS_WEBHOOKS *Permission var PERMISSION_MANAGE_OAUTH *Permission var PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH *Permission +var PERMISSION_MANAGE_EMOJIS *Permission +var PERMISSION_MANAGE_OTHERS_EMOJIS *Permission var PERMISSION_CREATE_POST *Permission var PERMISSION_CREATE_POST_PUBLIC *Permission var PERMISSION_CREATE_POST_EPHEMERAL *Permission @@ -286,6 +288,18 @@ func initializePermissions() { "authentication.permissions.manage_system_wide_oauth.description", PERMISSION_SCOPE_SYSTEM, } + PERMISSION_MANAGE_EMOJIS = &Permission{ + "manage_emojis", + "authentication.permissions.manage_emojis.name", + "authentication.permissions.manage_emojis.description", + PERMISSION_SCOPE_TEAM, + } + PERMISSION_MANAGE_OTHERS_EMOJIS = &Permission{ + "manage_others_emojis", + "authentication.permissions.manage_others_emojis.name", + "authentication.permissions.manage_others_emojis.description", + PERMISSION_SCOPE_TEAM, + } PERMISSION_CREATE_POST = &Permission{ "create_post", "authentication.permissions.create_post.name", @@ -424,6 +438,8 @@ func initializePermissions() { PERMISSION_MANAGE_OTHERS_WEBHOOKS, PERMISSION_MANAGE_OAUTH, PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH, + PERMISSION_MANAGE_EMOJIS, + PERMISSION_MANAGE_OTHERS_EMOJIS, PERMISSION_CREATE_POST, PERMISSION_CREATE_POST_PUBLIC, PERMISSION_CREATE_POST_EPHEMERAL, diff --git a/store/sqlstore/upgrade.go b/store/sqlstore/upgrade.go index 93399d7d9..98a89f36d 100644 --- a/store/sqlstore/upgrade.go +++ b/store/sqlstore/upgrade.go @@ -425,6 +425,10 @@ func UpgradeDatabaseToVersion410(sqlStore SqlStore) { } func UpgradeDatabaseToVersion50(sqlStore SqlStore) { + // This version of Mattermost includes an App-Layer migration which migrates from hard-coded emojis configured + // in `config.json` to a `Permission` in the database. The migration code can be seen + // in the file `app/app.go` in the function `DoEmojisPermissionsMigration()`. + // TODO: Uncomment following condition when version 5.0.0 is released //if shouldPerformUpgrade(sqlStore, VERSION_4_10_0, VERSION_5_0_0) { diff --git a/web/web_test.go b/web/web_test.go index 9b6230013..b53ed9618 100644 --- a/web/web_test.go +++ b/web/web_test.go @@ -61,6 +61,7 @@ func Setup() *TestHelper { ApiClient = model.NewAPIv4Client(URL) a.DoAdvancedPermissionsMigration() + a.DoEmojisPermissionsMigration() a.Srv.Store.MarkSystemRanUnitTests() -- cgit v1.2.3-1-g7c22 From 994ccf475f96bcad668269fe25b0d22e975bc222 Mon Sep 17 00:00:00 2001 From: George Goldberg Date: Wed, 30 May 2018 11:21:36 +0100 Subject: Add note about clearing caches when permissions reset CLI done. (#8823) * Add note about clearing caches when permissions reset CLI done. * Adjust text. --- cmd/mattermost/commands/permissions.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cmd/mattermost/commands/permissions.go b/cmd/mattermost/commands/permissions.go index e1bb34963..9d9962ce5 100644 --- a/cmd/mattermost/commands/permissions.go +++ b/cmd/mattermost/commands/permissions.go @@ -82,7 +82,9 @@ func resetPermissionsCmdF(command *cobra.Command, args []string) error { return errors.New(err.Error()) } - CommandPrettyPrintln("Permissions system successfully reset") + CommandPrettyPrintln("Permissions system successfully reset.") + CommandPrettyPrintln("Changes will take effect gradually as the server caches expire.") + CommandPrettyPrintln("For the changes to take effect immediately, go to the Mattermost System Console > General > Configuration and click \"Purge All Caches\".") return nil } -- cgit v1.2.3-1-g7c22