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