From 4d99aa22ba067b4a0441e73e35d587e8d64223eb Mon Sep 17 00:00:00 2001 From: Martin Kraft Date: Mon, 23 Mar 2020 13:44:20 -0400 Subject: [PATCH] MM-22212 & MM-22208: Read from the higher-scoped scheme if the permission is non-moderated. (#13813) * MM-22212: Read non-moderated permissions from higher-scoped scheme. * MM-2212: Corrects test count in comment. * MM-22212: Adds godoc comment. * MM-2212: Switches to the channel roles check in a few more places. * MM-22212: Refactors and fixes. * MM-22212: Reverts change, no longer required. * MM-22212: Removes translation. * MM-22212: Un-comments merged new permission. * MM-22212: Un-comments merged new permission. * MM-22212: Performance tweak. * MM-22212: Fixes some fmting. * MM-22212: Add unit test for newly-added store methods. * MM-22212: Renames app method. * MM-22212: Re-uses existing function to find string in slice. * MM-22212: Keeps 'higher-scoped' terminology for consistency. * MM-22212: Refactors based on PR feedback. * MM-22212: Fix for some bad merging. * MM-22212: Renamed some things. * MM-22212: Use an 'else' instead of a 'continue' for readability. * MM-22212: Caches (*SqlRoleStore).ChannelRolesUnderTeamRole. * MM-22212: Adds mock to new cache store. * MM-22212: Adds missing open tracing app layer methods. * MM-22212: Adds migration to add moderated permissions to channel_admin if present on channel_user. * MM-22212: Migrates team schemes. Removes unused AppError. * MM-22212: Fix for for if. * MM-22212: Fixes iterator. * MM-22212: Updates open tracing generated methods. * MM-22212: Fix mocks. * MM-22212: Change migration key name. * MM-22212: Switched to data structure from other branch. * MM-22212: Fixes tests after adding 'use_channel_mentions' to the channel_admin role. * MM-22212: Adds tracking of channel moderation. * Revert "MM-22212: Adds tracking of channel moderation." This reverts commit 23689efa22c112e4ba37f6a212535dd7ebfb63db. * MM-22212: Switch some functions to methods and vice versa. * MM-22212: Fix for refactor bug not notifiying websocket about changed role. * MM-22212: Adds test for public/private 'manage_members' handling. * MM-22122 Fix manage channel members edge case for public and private channels (#14049) * MM-22212: Adds moderated permission to team_admin. * MM-22212: Updates migration. * MM-22212: Revert unnecessary update to default roles. * Add channel scheme updated event when channel scheme is deleted or created (#14057) * MM-22212: Adds newline. * MM-22212: Migration fix. * MM-22212: Fix for migration. * MM-22212: Test fix. Co-authored-by: Farhan Munshi <3207297+fm2munsh@users.noreply.github.com> --- api4/channel_test.go | 28 ++++ app/app_iface.go | 4 +- app/app_test.go | 6 + app/authorization_test.go | 10 +- app/channel.go | 25 +-- app/notification_test.go | 2 + app/opentracing_layer.go | 11 +- app/permissions.go | 2 +- app/permissions_migrations.go | 185 +++++++++++++++++----- app/post_test.go | 4 + app/role.go | 97 +++++++++++- app/role_test.go | 217 ++++++++++++++++++++++++++ app/scheme.go | 4 +- cmd/mattermost/commands/sampledata.go | 14 +- model/cluster_message.go | 1 + model/migration.go | 2 +- model/role.go | 83 +++++++++- model/role_test.go | 7 +- model/utils.go | 8 + model/websocket_message.go | 1 + store/localcachelayer/layer.go | 9 +- store/localcachelayer/main_test.go | 6 + store/localcachelayer/role_layer.go | 30 ++++ store/opentracing_layer.go | 58 ++++++- store/sqlstore/channel_store.go | 1 + store/sqlstore/role_store.go | 152 +++++++++++++++++- store/sqlstore/scheme_store.go | 26 +++ store/sqlstore/supplier.go | 4 +- store/sqlstore/supplier_reactions.go | 3 - store/sqlstore/team_store.go | 1 + store/sqlstore/upgrade.go | 7 +- store/store.go | 13 +- store/storetest/group_store.go | 23 +-- store/storetest/mocks/RoleStore.go | 85 +++++++++- store/storetest/role_store.go | 157 +++++++++++++++++++ store/timer_layer.go | 52 +++++- testlib/store.go | 6 +- tests/channel-role-has-permission.csv | 17 ++ utils/utils.go | 14 ++ 39 files changed, 1252 insertions(+), 123 deletions(-) create mode 100644 app/role_test.go create mode 100644 tests/channel-role-has-permission.csv diff --git a/api4/channel_test.go b/api4/channel_test.go index 960cbfaa60f..d7e4e0cd7b6 100644 --- a/api4/channel_test.go +++ b/api4/channel_test.go @@ -3110,6 +3110,34 @@ func TestGetChannelModerations(t *testing.T) { } } }) + + t.Run("Retuns the correct value for manage_members depending on whether the channel is public or private", func(t *testing.T) { + scheme := th.SetupTeamScheme() + team.SchemeId = &scheme.Id + _, err := th.App.UpdateTeamScheme(team) + require.Nil(t, err) + + th.RemovePermissionFromRole(model.PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS.Id, scheme.DefaultChannelUserRole) + defer th.AddPermissionToRole(model.PERMISSION_CREATE_POST.Id, scheme.DefaultChannelUserRole) + + // public channel does not have the permission + moderations, res := th.SystemAdminClient.GetChannelModerations(channel.Id, "") + require.Nil(t, res.Error) + for _, moderation := range moderations { + if moderation.Name == "manage_members" { + require.Equal(t, moderation.Roles.Members.Value, false) + } + } + + // private channel does have the permission + moderations, res = th.SystemAdminClient.GetChannelModerations(th.BasicPrivateChannel.Id, "") + require.Nil(t, res.Error) + for _, moderation := range moderations { + if moderation.Name == "manage_members" { + require.Equal(t, moderation.Roles.Members.Value, true) + } + } + }) } func TestPatchChannelModerations(t *testing.T) { diff --git a/app/app_iface.go b/app/app_iface.go index c69b1f78048..bd24703381b 100644 --- a/app/app_iface.go +++ b/app/app_iface.go @@ -102,7 +102,7 @@ type AppIface interface { // Notifies cluster peers through config change. DisablePlugin(id string) *model.AppError // DoPermissionsMigrations execute all the permissions migrations need by the current version. - DoPermissionsMigrations() *model.AppError + DoPermissionsMigrations() error // EnablePlugin will set the config for an installed plugin to enabled, triggering asynchronous // activation if inactive anywhere in the cluster. // Notifies cluster peers through config change. @@ -782,7 +782,7 @@ type AppIface interface { SaveLicense(licenseBytes []byte) (*model.License, *model.AppError) SaveReactionForPost(reaction *model.Reaction) (*model.Reaction, *model.AppError) SaveUserTermsOfService(userId, termsOfServiceId string, accepted bool) *model.AppError - SchemesIterator(batchSize int) func() []*model.Scheme + SchemesIterator(scope string, batchSize int) func() []*model.Scheme SearchArchivedChannels(teamId string, term string, userId string) (*model.ChannelList, *model.AppError) SearchChannels(teamId string, term string) (*model.ChannelList, *model.AppError) SearchChannelsForUser(userId, teamId, term string) (*model.ChannelList, *model.AppError) diff --git a/app/app_test.go b/app/app_test.go index a7943542d65..557d9850c62 100644 --- a/app/app_test.go +++ b/app/app_test.go @@ -596,6 +596,12 @@ func TestDoEmojisPermissionsMigration(t *testing.T) { model.PERMISSION_DELETE_OTHERS_POSTS.Id, model.PERMISSION_CREATE_EMOJIS.Id, model.PERMISSION_DELETE_EMOJIS.Id, + model.PERMISSION_ADD_REACTION.Id, + model.PERMISSION_CREATE_POST.Id, + model.PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS.Id, + model.PERMISSION_MANAGE_PRIVATE_CHANNEL_MEMBERS.Id, + model.PERMISSION_REMOVE_REACTION.Id, + model.PERMISSION_USE_CHANNEL_MENTIONS.Id, } sort.Strings(expected2) sort.Strings(role2.Permissions) diff --git a/app/authorization_test.go b/app/authorization_test.go index 5eea5db76c9..5955b9795f1 100644 --- a/app/authorization_test.go +++ b/app/authorization_test.go @@ -6,7 +6,7 @@ package app import ( "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/mattermost/mattermost-server/v5/model" ) @@ -31,7 +31,13 @@ func TestCheckIfRolesGrantPermission(t *testing.T) { } for _, testcase := range cases { - assert.Equal(t, th.App.RolesGrantPermission(testcase.roles, testcase.permissionId), testcase.shouldGrant) + require.Equal(t, th.App.RolesGrantPermission(testcase.roles, testcase.permissionId), testcase.shouldGrant) } } + +func TestChannelRolesGrantPermission(t *testing.T) { + testPermissionInheritance(t, func(t *testing.T, th *TestHelper, testData permissionInheritanceTestData) { + require.Equal(t, testData.shouldHavePermission, th.App.RolesGrantPermission([]string{testData.channelRole.Name}, testData.permission.Id), "row: %+v\n", testData.truthTableRow) + }) +} diff --git a/app/channel.go b/app/channel.go index 6ad454a7561..e1e66f5a007 100644 --- a/app/channel.go +++ b/app/channel.go @@ -720,7 +720,7 @@ func (a *App) GetChannelModerationsForChannel(channel *model.Channel) ([]*model. return nil, err } - return buildChannelModerations(memberRole, guestRole, higherScopedMemberRole, higherScopedGuestRole), nil + return buildChannelModerations(channel.Type, memberRole, guestRole, higherScopedMemberRole, higherScopedGuestRole), nil } // PatchChannelModerationsForChannel Updates a channels scheme roles based on a given ChannelModerationPatch, if the permissions match the higher scoped role the scheme is deleted. @@ -736,8 +736,8 @@ func (a *App) PatchChannelModerationsForChannel(channel *model.Channel, channelM return nil, err } - higherScopedMemberPermissions := higherScopedMemberRole.GetChannelModeratedPermissions() - higherScopedGuestPermissions := higherScopedGuestRole.GetChannelModeratedPermissions() + higherScopedMemberPermissions := higherScopedMemberRole.GetChannelModeratedPermissions(channel.Type) + higherScopedGuestPermissions := higherScopedGuestRole.GetChannelModeratedPermissions(channel.Type) for _, moderationPatch := range channelModerationsPatch { if moderationPatch.Roles.Members != nil && *moderationPatch.Roles.Members && !higherScopedMemberPermissions[*moderationPatch.Name] { @@ -753,6 +753,9 @@ func (a *App) PatchChannelModerationsForChannel(channel *model.Channel, channelM if _, err = a.CreateChannelScheme(channel); err != nil { return nil, err } + + message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_CHANNEL_SCHEME_UPDATED, "", channel.Id, "", nil) + a.Publish(message) mlog.Info("Permission scheme created.", mlog.String("channel_id", channel.Id), mlog.String("channel_name", channel.Name)) } @@ -796,6 +799,10 @@ func (a *App) PatchChannelModerationsForChannel(channel *model.Channel, channelM if _, err = a.DeleteChannelScheme(channel); err != nil { return nil, err } + + message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_CHANNEL_SCHEME_UPDATED, "", channel.Id, "", nil) + a.Publish(message) + memberRole = higherScopedMemberRole guestRole = higherScopedGuestRole mlog.Info("Permission scheme deleted.", mlog.String("channel_id", channel.Id), mlog.String("channel_name", channel.Name)) @@ -810,14 +817,14 @@ func (a *App) PatchChannelModerationsForChannel(channel *model.Channel, channelM } } - return buildChannelModerations(memberRole, guestRole, higherScopedMemberRole, higherScopedGuestRole), nil + return buildChannelModerations(channel.Type, memberRole, guestRole, higherScopedMemberRole, higherScopedGuestRole), nil } -func buildChannelModerations(memberRole *model.Role, guestRole *model.Role, higherScopedMemberRole *model.Role, higherScopedGuestRole *model.Role) []*model.ChannelModeration { - memberPermissions := memberRole.GetChannelModeratedPermissions() - guestPermissions := guestRole.GetChannelModeratedPermissions() - higherScopedMemberPermissions := higherScopedMemberRole.GetChannelModeratedPermissions() - higherScopedGuestPermissions := higherScopedGuestRole.GetChannelModeratedPermissions() +func buildChannelModerations(channelType string, memberRole *model.Role, guestRole *model.Role, higherScopedMemberRole *model.Role, higherScopedGuestRole *model.Role) []*model.ChannelModeration { + memberPermissions := memberRole.GetChannelModeratedPermissions(channelType) + guestPermissions := guestRole.GetChannelModeratedPermissions(channelType) + higherScopedMemberPermissions := higherScopedMemberRole.GetChannelModeratedPermissions(channelType) + higherScopedGuestPermissions := higherScopedGuestRole.GetChannelModeratedPermissions(channelType) var channelModerations []*model.ChannelModeration for _, permissionKey := range model.CHANNEL_MODERATED_PERMISSIONS { diff --git a/app/notification_test.go b/app/notification_test.go index 2e07a913bd5..0e609bbc983 100644 --- a/app/notification_test.go +++ b/app/notification_test.go @@ -905,7 +905,9 @@ func TestAllowChannelMentions(t *testing.T) { t.Run("should return false for a post where the post user does not have USE_CHANNEL_MENTIONS permission", func(t *testing.T) { defer th.AddPermissionToRole(model.PERMISSION_USE_CHANNEL_MENTIONS.Id, model.CHANNEL_USER_ROLE_ID) + defer th.AddPermissionToRole(model.PERMISSION_USE_CHANNEL_MENTIONS.Id, model.CHANNEL_ADMIN_ROLE_ID) th.RemovePermissionFromRole(model.PERMISSION_USE_CHANNEL_MENTIONS.Id, model.CHANNEL_USER_ROLE_ID) + th.RemovePermissionFromRole(model.PERMISSION_USE_CHANNEL_MENTIONS.Id, model.CHANNEL_ADMIN_ROLE_ID) allowChannelMentions := th.App.allowChannelMentions(post, 5) assert.False(t, allowChannelMentions) }) diff --git a/app/opentracing_layer.go b/app/opentracing_layer.go index aa5c911af70..dbe33e8ac1e 100644 --- a/app/opentracing_layer.go +++ b/app/opentracing_layer.go @@ -3046,7 +3046,7 @@ func (a *OpenTracingAppLayer) DoLogin(w http.ResponseWriter, r *http.Request, us return resultVar0 } -func (a *OpenTracingAppLayer) DoPermissionsMigrations() *model.AppError { +func (a *OpenTracingAppLayer) DoPermissionsMigrations() error { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DoPermissionsMigrations") @@ -3060,11 +3060,6 @@ func (a *OpenTracingAppLayer) DoPermissionsMigrations() *model.AppError { defer span.Finish() resultVar0 := a.app.DoPermissionsMigrations() - if resultVar0 != nil { - span.LogFields(spanlog.Error(resultVar0)) - ext.Error.Set(span, true) - } - return resultVar0 } @@ -11547,7 +11542,7 @@ func (a *OpenTracingAppLayer) SaveUserTermsOfService(userId string, termsOfServi return resultVar0 } -func (a *OpenTracingAppLayer) SchemesIterator(batchSize int) func() []*model.Scheme { +func (a *OpenTracingAppLayer) SchemesIterator(scope string, batchSize int) func() []*model.Scheme { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SchemesIterator") @@ -11559,7 +11554,7 @@ func (a *OpenTracingAppLayer) SchemesIterator(batchSize int) func() []*model.Sch }() defer span.Finish() - resultVar0 := a.app.SchemesIterator(batchSize) + resultVar0 := a.app.SchemesIterator(scope, batchSize) return resultVar0 } diff --git a/app/permissions.go b/app/permissions.go index cdee338da07..7049ade4d8e 100644 --- a/app/permissions.go +++ b/app/permissions.go @@ -65,7 +65,7 @@ func (a *App) ResetPermissionsSystem() *model.AppError { func (a *App) ExportPermissions(w io.Writer) error { - next := a.SchemesIterator(permissionsExportBatchSize) + next := a.SchemesIterator("", permissionsExportBatchSize) var schemeBatch []*model.Scheme for schemeBatch = next(); len(schemeBatch) > 0; schemeBatch = next() { diff --git a/app/permissions_migrations.go b/app/permissions_migrations.go index dcf10ec97f0..0fcc39147e8 100644 --- a/app/permissions_migrations.go +++ b/app/permissions_migrations.go @@ -49,6 +49,10 @@ const ( PERMISSION_USE_CHANNEL_MENTIONS = "use_channel_mentions" PERMISSION_CREATE_POST = "create_post" PERMISSION_CREATE_POST_PUBLIC = "create_post_public" + PERMISSION_ADD_REACTION = "add_reaction" + PERMISSION_REMOVE_REACTION = "remove_reaction" + PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS = "manage_public_channel_members" + PERMISSION_MANAGE_PRIVATE_CHANNEL_MEMBERS = "manage_private_channel_members" ) func isRole(role string) func(string, map[string]map[string]bool) bool { @@ -152,7 +156,7 @@ func (a *App) doPermissionsMigration(key string, migrationMap permissionsMap) *m return nil } -func getEmojisPermissionsSplitMigration() permissionsMap { +func (a *App) getEmojisPermissionsSplitMigration() (permissionsMap, error) { return permissionsMap{ permissionTransformation{ On: permissionExists(PERMISSION_MANAGE_EMOJIS), @@ -164,10 +168,10 @@ func getEmojisPermissionsSplitMigration() permissionsMap { Add: []string{PERMISSION_DELETE_OTHERS_EMOJIS}, Remove: []string{PERMISSION_MANAGE_OTHERS_EMOJIS}, }, - } + }, nil } -func getWebhooksPermissionsSplitMigration() permissionsMap { +func (a *App) getWebhooksPermissionsSplitMigration() (permissionsMap, error) { return permissionsMap{ permissionTransformation{ On: permissionExists(PERMISSION_MANAGE_WEBHOOKS), @@ -179,10 +183,10 @@ func getWebhooksPermissionsSplitMigration() permissionsMap { Add: []string{PERMISSION_MANAGE_OTHERS_INCOMING_WEBHOOKS, PERMISSION_MANAGE_OTHERS_OUTGOING_WEBHOOKS}, Remove: []string{PERMISSION_MANAGE_OTHERS_WEBHOOKS}, }, - } + }, nil } -func getListJoinPublicPrivateTeamsPermissionsMigration() permissionsMap { +func (a *App) getListJoinPublicPrivateTeamsPermissionsMigration() (permissionsMap, error) { return permissionsMap{ permissionTransformation{ On: isRole(model.SYSTEM_ADMIN_ROLE_ID), @@ -194,29 +198,29 @@ func getListJoinPublicPrivateTeamsPermissionsMigration() permissionsMap { Add: []string{PERMISSION_LIST_PUBLIC_TEAMS, PERMISSION_JOIN_PUBLIC_TEAMS}, Remove: []string{}, }, - } + }, nil } -func removePermanentDeleteUserMigration() permissionsMap { +func (a *App) removePermanentDeleteUserMigration() (permissionsMap, error) { return permissionsMap{ permissionTransformation{ On: permissionExists(PERMISSION_PERMANENT_DELETE_USER), Remove: []string{PERMISSION_PERMANENT_DELETE_USER}, }, - } + }, nil } -func getAddBotPermissionsMigration() permissionsMap { +func (a *App) getAddBotPermissionsMigration() (permissionsMap, error) { return permissionsMap{ permissionTransformation{ On: isRole(model.SYSTEM_ADMIN_ROLE_ID), Add: []string{PERMISSION_CREATE_BOT, PERMISSION_READ_BOTS, PERMISSION_READ_OTHERS_BOTS, PERMISSION_MANAGE_BOTS, PERMISSION_MANAGE_OTHERS_BOTS}, Remove: []string{}, }, - } + }, nil } -func applyChannelManageDeleteToChannelUser() permissionsMap { +func (a *App) applyChannelManageDeleteToChannelUser() (permissionsMap, error) { return permissionsMap{ permissionTransformation{ On: permissionAnd(isRole(model.CHANNEL_USER_ROLE_ID), onOtherRole(model.TEAM_USER_ROLE_ID, permissionExists(PERMISSION_MANAGE_PRIVATE_CHANNEL_PROPERTIES))), @@ -234,10 +238,10 @@ func applyChannelManageDeleteToChannelUser() permissionsMap { On: permissionAnd(isRole(model.CHANNEL_USER_ROLE_ID), onOtherRole(model.TEAM_USER_ROLE_ID, permissionExists(PERMISSION_DELETE_PUBLIC_CHANNEL))), Add: []string{PERMISSION_DELETE_PUBLIC_CHANNEL}, }, - } + }, nil } -func removeChannelManageDeleteFromTeamUser() permissionsMap { +func (a *App) removeChannelManageDeleteFromTeamUser() (permissionsMap, error) { return permissionsMap{ permissionTransformation{ On: permissionAnd(isRole(model.TEAM_USER_ROLE_ID), permissionExists(PERMISSION_MANAGE_PRIVATE_CHANNEL_PROPERTIES)), @@ -255,10 +259,10 @@ func removeChannelManageDeleteFromTeamUser() permissionsMap { On: permissionAnd(isRole(model.TEAM_USER_ROLE_ID), permissionExists(PERMISSION_DELETE_PUBLIC_CHANNEL)), Remove: []string{PERMISSION_DELETE_PUBLIC_CHANNEL}, }, - } + }, nil } -func getViewMembersPermissionMigration() permissionsMap { +func (a *App) getViewMembersPermissionMigration() (permissionsMap, error) { return permissionsMap{ permissionTransformation{ On: isRole(model.SYSTEM_USER_ROLE_ID), @@ -268,47 +272,154 @@ func getViewMembersPermissionMigration() permissionsMap { On: isRole(model.SYSTEM_ADMIN_ROLE_ID), Add: []string{PERMISSION_VIEW_MEMBERS}, }, - } + }, nil } -func getAddManageGuestsPermissionsMigration() permissionsMap { +func (a *App) getAddManageGuestsPermissionsMigration() (permissionsMap, error) { return permissionsMap{ permissionTransformation{ On: isRole(model.SYSTEM_ADMIN_ROLE_ID), Add: []string{PERMISSION_PROMOTE_GUEST, PERMISSION_DEMOTE_TO_GUEST, PERMISSION_INVITE_GUEST}, }, - } + }, nil } -func getAddUseMentionChannelsPermissionMigration() permissionsMap { - return permissionsMap{ - permissionTransformation{ - On: permissionOr(permissionExists(PERMISSION_CREATE_POST), permissionExists(PERMISSION_CREATE_POST_PUBLIC)), - Add: []string{PERMISSION_USE_CHANNEL_MENTIONS}, - }, +func (a *App) channelModerationPermissionsMigration() (permissionsMap, error) { + transformations := permissionsMap{} + + var allTeamSchemes []*model.Scheme + next := a.SchemesIterator(model.SCHEME_SCOPE_TEAM, 100) + var schemeBatch []*model.Scheme + for schemeBatch = next(); len(schemeBatch) > 0; schemeBatch = next() { + allTeamSchemes = append(allTeamSchemes, schemeBatch...) } + + moderatedPermissionsMinusCreatePost := []string{ + PERMISSION_ADD_REACTION, + PERMISSION_REMOVE_REACTION, + PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS, + PERMISSION_MANAGE_PRIVATE_CHANNEL_MEMBERS, + PERMISSION_USE_CHANNEL_MENTIONS, + } + + teamAndChannelAdminConditionalTransformations := func(teamAdminID, channelAdminID, channelUserID, channelGuestID string) []permissionTransformation { + transformations := []permissionTransformation{} + + for _, perm := range moderatedPermissionsMinusCreatePost { + // add each moderated permission to the channel admin if channel user or guest has the permission + trans := permissionTransformation{ + On: permissionAnd( + isRole(channelAdminID), + permissionOr( + onOtherRole(channelUserID, permissionExists(perm)), + onOtherRole(channelGuestID, permissionExists(perm)), + ), + ), + Add: []string{perm}, + } + transformations = append(transformations, trans) + + // add each moderated permission to the team admin if channel admin, user, or guest has the permission + trans = permissionTransformation{ + On: permissionAnd( + isRole(teamAdminID), + permissionOr( + onOtherRole(channelAdminID, permissionExists(perm)), + onOtherRole(channelUserID, permissionExists(perm)), + onOtherRole(channelGuestID, permissionExists(perm)), + ), + ), + Add: []string{perm}, + } + transformations = append(transformations, trans) + } + + return transformations + } + + for _, ts := range allTeamSchemes { + // ensure all team scheme channel admins have create_post because it's not exposed via the UI + trans := permissionTransformation{ + On: isRole(ts.DefaultChannelAdminRole), + Add: []string{PERMISSION_CREATE_POST}, + } + transformations = append(transformations, trans) + + // ensure all team scheme team admins have create_post because it's not exposed via the UI + trans = permissionTransformation{ + On: isRole(ts.DefaultTeamAdminRole), + Add: []string{PERMISSION_CREATE_POST}, + } + transformations = append(transformations, trans) + + // conditionally add all other moderated permissions to team and channel admins + transformations = append(transformations, teamAndChannelAdminConditionalTransformations( + ts.DefaultTeamAdminRole, + ts.DefaultChannelAdminRole, + ts.DefaultChannelUserRole, + ts.DefaultChannelGuestRole, + )...) + } + + // ensure team admins have create_post + transformations = append(transformations, permissionTransformation{ + On: isRole(model.TEAM_ADMIN_ROLE_ID), + Add: []string{PERMISSION_CREATE_POST}, + }) + + // ensure channel admins have create_post + transformations = append(transformations, permissionTransformation{ + On: isRole(model.CHANNEL_ADMIN_ROLE_ID), + Add: []string{PERMISSION_CREATE_POST}, + }) + + // conditionally add all other moderated permissions to team and channel admins + transformations = append(transformations, teamAndChannelAdminConditionalTransformations( + model.TEAM_ADMIN_ROLE_ID, + model.CHANNEL_ADMIN_ROLE_ID, + model.CHANNEL_USER_ROLE_ID, + model.CHANNEL_GUEST_ROLE_ID, + )...) + + // ensure system admin has all of the moderated permissions + transformations = append(transformations, permissionTransformation{ + On: isRole(model.SYSTEM_ADMIN_ROLE_ID), + Add: append(moderatedPermissionsMinusCreatePost, PERMISSION_CREATE_POST), + }) + + // add the new use_channel_mentions permission to everyone who has create_post + transformations = append(transformations, permissionTransformation{ + On: permissionOr(permissionExists(PERMISSION_CREATE_POST), permissionExists(PERMISSION_CREATE_POST_PUBLIC)), + Add: []string{PERMISSION_USE_CHANNEL_MENTIONS}, + }) + + return transformations, nil } // DoPermissionsMigrations execute all the permissions migrations need by the current version. -func (a *App) DoPermissionsMigrations() *model.AppError { +func (a *App) DoPermissionsMigrations() error { PermissionsMigrations := []struct { Key string - Migration func() permissionsMap + Migration func() (permissionsMap, error) }{ - {Key: model.MIGRATION_KEY_EMOJI_PERMISSIONS_SPLIT, Migration: getEmojisPermissionsSplitMigration}, - {Key: model.MIGRATION_KEY_WEBHOOK_PERMISSIONS_SPLIT, Migration: getWebhooksPermissionsSplitMigration}, - {Key: model.MIGRATION_KEY_LIST_JOIN_PUBLIC_PRIVATE_TEAMS, Migration: getListJoinPublicPrivateTeamsPermissionsMigration}, - {Key: model.MIGRATION_KEY_REMOVE_PERMANENT_DELETE_USER, Migration: removePermanentDeleteUserMigration}, - {Key: model.MIGRATION_KEY_ADD_BOT_PERMISSIONS, Migration: getAddBotPermissionsMigration}, - {Key: model.MIGRATION_KEY_APPLY_CHANNEL_MANAGE_DELETE_TO_CHANNEL_USER, Migration: applyChannelManageDeleteToChannelUser}, - {Key: model.MIGRATION_KEY_REMOVE_CHANNEL_MANAGE_DELETE_FROM_TEAM_USER, Migration: removeChannelManageDeleteFromTeamUser}, - {Key: model.MIGRATION_KEY_VIEW_MEMBERS_NEW_PERMISSION, Migration: getViewMembersPermissionMigration}, - {Key: model.MIGRATION_KEY_ADD_MANAGE_GUESTS_PERMISSIONS, Migration: getAddManageGuestsPermissionsMigration}, - {Key: model.MIGRATION_KEY_ADD_USE_CHANNEL_MENTIONS_PERMISSION, Migration: getAddUseMentionChannelsPermissionMigration}, + {Key: model.MIGRATION_KEY_EMOJI_PERMISSIONS_SPLIT, Migration: a.getEmojisPermissionsSplitMigration}, + {Key: model.MIGRATION_KEY_WEBHOOK_PERMISSIONS_SPLIT, Migration: a.getWebhooksPermissionsSplitMigration}, + {Key: model.MIGRATION_KEY_LIST_JOIN_PUBLIC_PRIVATE_TEAMS, Migration: a.getListJoinPublicPrivateTeamsPermissionsMigration}, + {Key: model.MIGRATION_KEY_REMOVE_PERMANENT_DELETE_USER, Migration: a.removePermanentDeleteUserMigration}, + {Key: model.MIGRATION_KEY_ADD_BOT_PERMISSIONS, Migration: a.getAddBotPermissionsMigration}, + {Key: model.MIGRATION_KEY_APPLY_CHANNEL_MANAGE_DELETE_TO_CHANNEL_USER, Migration: a.applyChannelManageDeleteToChannelUser}, + {Key: model.MIGRATION_KEY_REMOVE_CHANNEL_MANAGE_DELETE_FROM_TEAM_USER, Migration: a.removeChannelManageDeleteFromTeamUser}, + {Key: model.MIGRATION_KEY_VIEW_MEMBERS_NEW_PERMISSION, Migration: a.getViewMembersPermissionMigration}, + {Key: model.MIGRATION_KEY_ADD_MANAGE_GUESTS_PERMISSIONS, Migration: a.getAddManageGuestsPermissionsMigration}, + {Key: model.MIGRATION_KEY_CHANNEL_MODERATIONS_PERMISSIONS, Migration: a.channelModerationPermissionsMigration}, } for _, migration := range PermissionsMigrations { - if err := a.doPermissionsMigration(migration.Key, migration.Migration()); err != nil { + migMap, err := migration.Migration() + if err != nil { + return err + } + if err := a.doPermissionsMigration(migration.Key, migMap); err != nil { return err } } diff --git a/app/post_test.go b/app/post_test.go index ac42960422d..17606796212 100644 --- a/app/post_test.go +++ b/app/post_test.go @@ -708,6 +708,7 @@ func TestCreatePost(t *testing.T) { t.Run("Sets prop when post has mentions and user does not have USE_CHANNEL_MENTIONS", func(t *testing.T) { th.RemovePermissionFromRole(model.PERMISSION_USE_CHANNEL_MENTIONS.Id, model.CHANNEL_USER_ROLE_ID) + th.RemovePermissionFromRole(model.PERMISSION_USE_CHANNEL_MENTIONS.Id, model.CHANNEL_ADMIN_ROLE_ID) postWithNoMention := &model.Post{ ChannelId: th.BasicChannel.Id, @@ -728,6 +729,7 @@ func TestCreatePost(t *testing.T) { assert.Equal(t, rpost.GetProp(model.POST_PROPS_MENTION_HIGHLIGHT_DISABLED), true) th.AddPermissionToRole(model.PERMISSION_USE_CHANNEL_MENTIONS.Id, model.CHANNEL_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_USE_CHANNEL_MENTIONS.Id, model.CHANNEL_ADMIN_ROLE_ID) }) }) } @@ -798,6 +800,7 @@ func TestPatchPost(t *testing.T) { t.Run("Sets prop when user does not have USE_CHANNEL_MENTIONS", func(t *testing.T) { th.RemovePermissionFromRole(model.PERMISSION_USE_CHANNEL_MENTIONS.Id, model.CHANNEL_USER_ROLE_ID) + th.RemovePermissionFromRole(model.PERMISSION_USE_CHANNEL_MENTIONS.Id, model.CHANNEL_ADMIN_ROLE_ID) patchWithNoMention := &model.PostPatch{Message: model.NewString("This patch still does not have a mention")} rpost, err = th.App.PatchPost(rpost.Id, patchWithNoMention) @@ -811,6 +814,7 @@ func TestPatchPost(t *testing.T) { assert.Equal(t, rpost.GetProp(model.POST_PROPS_MENTION_HIGHLIGHT_DISABLED), true) th.AddPermissionToRole(model.PERMISSION_USE_CHANNEL_MENTIONS.Id, model.CHANNEL_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_USE_CHANNEL_MENTIONS.Id, model.CHANNEL_ADMIN_ROLE_ID) }) }) } diff --git a/app/role.go b/app/role.go index d05773d9855..9cad4d098a7 100644 --- a/app/role.go +++ b/app/role.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/mattermost/mattermost-server/v5/model" + "github.com/mattermost/mattermost-server/v5/utils" ) func (a *App) GetRole(id string) (*model.Role, *model.AppError) { @@ -20,11 +21,62 @@ func (a *App) GetAllRoles() ([]*model.Role, *model.AppError) { } func (a *App) GetRoleByName(name string) (*model.Role, *model.AppError) { - return a.Srv().Store.Role().GetByName(name) + role, err := a.Srv().Store.Role().GetByName(name) + if err != nil { + return nil, err + } + + err = a.mergeChannelHigherScopedPermissions([]*model.Role{role}) + if err != nil { + return nil, err + } + + return role, nil } func (a *App) GetRolesByNames(names []string) ([]*model.Role, *model.AppError) { - return a.Srv().Store.Role().GetByNames(names) + roles, err := a.Srv().Store.Role().GetByNames(names) + if err != nil { + return nil, err + } + + err = a.mergeChannelHigherScopedPermissions(roles) + if err != nil { + return nil, err + } + + return roles, nil +} + +// mergeChannelHigherScopedPermissions updates the permissions based on the role type, whether the permission is +// moderated, and the value of the permission on the higher-scoped scheme. +func (a *App) mergeChannelHigherScopedPermissions(roles []*model.Role) *model.AppError { + var higherScopeNamesToQuery []string + + for _, role := range roles { + if role.SchemeManaged { + higherScopeNamesToQuery = append(higherScopeNamesToQuery, role.Name) + } + } + + if len(higherScopeNamesToQuery) == 0 { + return nil + } + + higherScopedPermissionsMap, err := a.Srv().Store.Role().ChannelHigherScopedPermissions(higherScopeNamesToQuery) + if err != nil { + return err + } + + for _, role := range roles { + if role.SchemeManaged { + if higherScopedPermissions, ok := higherScopedPermissionsMap[role.Name]; ok { + role.MergeChannelHigherScopedPermissions(higherScopedPermissions) + } + } + } + + return nil } func (a *App) PatchRole(role *model.Role, patch *model.RolePatch) (*model.Role, *model.AppError) { @@ -59,10 +111,47 @@ func (a *App) UpdateRole(role *model.Role) (*model.Role, *model.AppError) { if err != nil { return nil, err } - a.sendUpdatedRoleEvent(savedRole) + + builtInChannelRoles := []string{ + model.CHANNEL_GUEST_ROLE_ID, + model.CHANNEL_USER_ROLE_ID, + model.CHANNEL_ADMIN_ROLE_ID, + } + + builtInRolesMinusChannelRoles := utils.RemoveStringsFromSlice(model.BuiltInSchemeManagedRoleIDs, builtInChannelRoles...) + + if utils.StringInSlice(savedRole.Name, builtInRolesMinusChannelRoles) { + return savedRole, nil + } + + var roleRetrievalFunc func() ([]*model.Role, *model.AppError) + + if utils.StringInSlice(savedRole.Name, builtInChannelRoles) { + roleRetrievalFunc = func() ([]*model.Role, *model.AppError) { + return a.Srv().Store.Role().AllChannelSchemeRoles() + } + } else { + roleRetrievalFunc = func() ([]*model.Role, *model.AppError) { + return a.Srv().Store.Role().ChannelRolesUnderTeamRole(savedRole.Name) + } + } + + impactedRoles, err := roleRetrievalFunc() + if err != nil { + return nil, err + } + impactedRoles = append(impactedRoles, role) + + err = a.mergeChannelHigherScopedPermissions(impactedRoles) + if err != nil { + return nil, err + } + + for _, ir := range impactedRoles { + a.sendUpdatedRoleEvent(ir) + } return savedRole, nil - } func (a *App) CheckRolesExist(roleNames []string) *model.AppError { diff --git a/app/role_test.go b/app/role_test.go new file mode 100644 index 00000000000..d9fdaae7c00 --- /dev/null +++ b/app/role_test.go @@ -0,0 +1,217 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package app + +import ( + "encoding/csv" + "io/ioutil" + "os" + "strconv" + "strings" + "testing" + + "github.com/mattermost/mattermost-server/v5/model" + "github.com/mattermost/mattermost-server/v5/utils" + "github.com/stretchr/testify/require" +) + +type permissionInheritanceTestData struct { + channelRole *model.Role + permission *model.Permission + shouldHavePermission bool + channel *model.Channel + higherScopedRole *model.Role + truthTableRow []string +} + +func TestGetRolesByNames(t *testing.T) { + testPermissionInheritance(t, func(t *testing.T, th *TestHelper, testData permissionInheritanceTestData) { + actualRoles, err := th.App.GetRolesByNames([]string{testData.channelRole.Name}) + require.Nil(t, err) + require.Len(t, actualRoles, 1) + + actualRole := actualRoles[0] + require.NotNil(t, actualRole) + require.Equal(t, testData.channelRole.Name, actualRole.Name) + + require.Equal(t, testData.shouldHavePermission, utils.StringInSlice(testData.permission.Id, actualRole.Permissions)) + }) +} + +func TestGetRoleByName(t *testing.T) { + testPermissionInheritance(t, func(t *testing.T, th *TestHelper, testData permissionInheritanceTestData) { + actualRole, err := th.App.GetRoleByName(testData.channelRole.Name) + require.Nil(t, err) + require.NotNil(t, actualRole) + require.Equal(t, testData.channelRole.Name, actualRole.Name) + require.Equal(t, testData.shouldHavePermission, utils.StringInSlice(testData.permission.Id, actualRole.Permissions), "row: %+v", testData.truthTableRow) + }) +} + +// testPermissionInheritance tests 48 combinations of scheme, permission, role data. +func testPermissionInheritance(t *testing.T, testCallback func(t *testing.T, th *TestHelper, testData permissionInheritanceTestData)) { + th := Setup(t).InitBasic() + defer th.TearDown() + + th.App.SetLicense(model.NewTestLicense("")) + th.App.SetPhase2PermissionsMigrationStatus(true) + + permissionsDefault := []string{ + model.PERMISSION_MANAGE_CHANNEL_ROLES.Id, + model.PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS.Id, + } + + // Defer resetting the system scheme permissions + systemSchemeRoles, err := th.App.GetRolesByNames([]string{ + model.CHANNEL_GUEST_ROLE_ID, + model.CHANNEL_USER_ROLE_ID, + model.CHANNEL_ADMIN_ROLE_ID, + }) + require.Nil(t, err) + require.Len(t, systemSchemeRoles, 3) + + // defer resetting the system role permissions + for _, systemRole := range systemSchemeRoles { + defer th.App.PatchRole(systemRole, &model.RolePatch{ + Permissions: &systemRole.Permissions, + }) + } + + // Make a channel scheme, clear its permissions + channelScheme, err := th.App.CreateScheme(&model.Scheme{ + Name: model.NewId(), + DisplayName: model.NewId(), + Scope: model.SCHEME_SCOPE_CHANNEL, + }) + require.Nil(t, err) + defer th.App.DeleteScheme(channelScheme.Id) + + team := th.CreateTeam() + defer th.App.PermanentDeleteTeamId(team.Id) + + // Make a channel + channel := th.CreateChannel(team) + defer th.App.PermanentDeleteChannel(channel) + + // Set the channel scheme + channel.SchemeId = &channelScheme.Id + channel, err = th.App.UpdateChannelScheme(channel) + require.Nil(t, err) + + // Get the truth table from CSV + file, e := os.Open("tests/channel-role-has-permission.csv") + require.Nil(t, e) + defer file.Close() + + b, e := ioutil.ReadAll(file) + require.Nil(t, e) + + r := csv.NewReader(strings.NewReader(string(b))) + records, e := r.ReadAll() + require.Nil(t, e) + + test := func(higherScopedGuest, higherScopedUser, higherScopedAdmin string) { + for _, roleNameUnderTest := range []string{higherScopedGuest, higherScopedUser, higherScopedAdmin} { + for i, row := range records { + // skip csv header + if i == 0 { + continue + } + + higherSchemeHasPermission, e := strconv.ParseBool(row[0]) + require.Nil(t, e) + + permissionIsModerated, e := strconv.ParseBool(row[1]) + require.Nil(t, e) + + channelSchemeHasPermission, e := strconv.ParseBool(row[2]) + require.Nil(t, e) + + channelRoleIsChannelAdmin, e := strconv.ParseBool(row[3]) + require.Nil(t, e) + + shouldHavePermission, e := strconv.ParseBool(row[4]) + require.Nil(t, e) + + // skip some invalid combinations because of the outer loop iterating all 3 channel roles + if (channelRoleIsChannelAdmin && roleNameUnderTest != higherScopedAdmin) || (!channelRoleIsChannelAdmin && roleNameUnderTest == higherScopedAdmin) { + continue + } + + // select the permission to test (moderated or non-moderated) + var permission *model.Permission + if permissionIsModerated { + permission = model.PERMISSION_CREATE_POST // moderated + } else { + permission = model.PERMISSION_READ_CHANNEL // non-moderated + } + + // add or remove the permission from the higher-scoped scheme + higherScopedRole, testErr := th.App.GetRoleByName(roleNameUnderTest) + require.Nil(t, testErr) + + var higherScopedPermissions []string + if higherSchemeHasPermission { + higherScopedPermissions = []string{permission.Id} + } else { + higherScopedPermissions = permissionsDefault + } + higherScopedRole, testErr = th.App.PatchRole(higherScopedRole, &model.RolePatch{Permissions: &higherScopedPermissions}) + require.Nil(t, testErr) + + // get channel role + var channelRoleName string + switch roleNameUnderTest { + case higherScopedGuest: + channelRoleName = channelScheme.DefaultChannelGuestRole + case higherScopedUser: + channelRoleName = channelScheme.DefaultChannelUserRole + case higherScopedAdmin: + channelRoleName = channelScheme.DefaultChannelAdminRole + } + channelRole, testErr := th.App.GetRoleByName(channelRoleName) + require.Nil(t, testErr) + + // add or remove the permission from the channel scheme + var channelSchemePermissions []string + if channelSchemeHasPermission { + channelSchemePermissions = []string{permission.Id} + } else { + channelSchemePermissions = permissionsDefault + } + channelRole, testErr = th.App.PatchRole(channelRole, &model.RolePatch{Permissions: &channelSchemePermissions}) + require.Nil(t, testErr) + + testCallback(t, th, permissionInheritanceTestData{ + channelRole: channelRole, + permission: permission, + shouldHavePermission: shouldHavePermission, + channel: channel, + higherScopedRole: higherScopedRole, + truthTableRow: row, + }) + } + } + } + + // test 24 combinations where the higher-scoped scheme is the SYSTEM scheme + test(model.CHANNEL_GUEST_ROLE_ID, model.CHANNEL_USER_ROLE_ID, model.CHANNEL_ADMIN_ROLE_ID) + + // create a team scheme + teamScheme, err := th.App.CreateScheme(&model.Scheme{ + Name: model.NewId(), + DisplayName: model.NewId(), + Scope: model.SCHEME_SCOPE_TEAM, + }) + require.Nil(t, err) + defer th.App.DeleteScheme(teamScheme.Id) + + // assign the scheme to the team + team.SchemeId = &teamScheme.Id + team, err = th.App.UpdateTeamScheme(team) + require.Nil(t, err) + + // test 24 combinations where the higher-scoped scheme is a TEAM scheme + test(teamScheme.DefaultChannelGuestRole, teamScheme.DefaultChannelUserRole, teamScheme.DefaultChannelAdminRole) +} diff --git a/app/scheme.go b/app/scheme.go index d4f29c48b12..2cccee16380 100644 --- a/app/scheme.go +++ b/app/scheme.go @@ -139,10 +139,10 @@ func (a *App) IsPhase2MigrationCompleted() *model.AppError { return nil } -func (a *App) SchemesIterator(batchSize int) func() []*model.Scheme { +func (a *App) SchemesIterator(scope string, batchSize int) func() []*model.Scheme { offset := 0 return func() []*model.Scheme { - schemes, err := a.Srv().Store.Scheme().GetAllPage("", offset, batchSize) + schemes, err := a.Srv().Store.Scheme().GetAllPage(scope, offset, batchSize) if err != nil { return []*model.Scheme{} } diff --git a/cmd/mattermost/commands/sampledata.go b/cmd/mattermost/commands/sampledata.go index 725eccc2e54..d11bca2dc49 100644 --- a/cmd/mattermost/commands/sampledata.go +++ b/cmd/mattermost/commands/sampledata.go @@ -18,6 +18,7 @@ import ( "github.com/icrowley/fake" "github.com/mattermost/mattermost-server/v5/app" "github.com/mattermost/mattermost-server/v5/model" + "github.com/mattermost/mattermost-server/v5/utils" "github.com/spf13/cobra" ) @@ -52,15 +53,6 @@ func init() { RootCmd.AddCommand(SampleDataCmd) } -func sliceIncludes(vs []string, t string) bool { - for _, v := range vs { - if v == t { - return true - } - } - return false -} - func randomPastTime(seconds int) int64 { now := time.Now() today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.FixedZone("UTC", 0)) @@ -343,7 +335,7 @@ func sampleDataCmdF(command *cobra.Command, args []string) error { totalUsers := 3 + rand.Intn(3) for len(users) < totalUsers { user := allUsers[rand.Intn(len(allUsers))] - if !sliceIncludes(users, user) { + if !utils.StringInSlice(user, users) { users = append(users, user) } } @@ -356,7 +348,7 @@ func sampleDataCmdF(command *cobra.Command, args []string) error { totalUsers := 3 + rand.Intn(3) for len(users) < totalUsers { user := allUsers[rand.Intn(len(allUsers))] - if !sliceIncludes(users, user) { + if !utils.StringInSlice(user, users) { users = append(users, user) } } diff --git a/model/cluster_message.go b/model/cluster_message.go index 0105d9481e4..86113d78091 100644 --- a/model/cluster_message.go +++ b/model/cluster_message.go @@ -24,6 +24,7 @@ const ( CLUSTER_EVENT_INVALIDATE_CACHE_FOR_USER_TEAMS = "inv_user_teams" CLUSTER_EVENT_CLEAR_SESSION_CACHE_FOR_USER = "clear_session_user" CLUSTER_EVENT_INVALIDATE_CACHE_FOR_ROLES = "inv_roles" + CLUSTER_EVENT_INVALIDATE_CACHE_FOR_ROLE_PERMISSIONS = "inv_role_permissions" CLUSTER_EVENT_INVALIDATE_CACHE_FOR_PROFILE_BY_IDS = "inv_profile_ids" CLUSTER_EVENT_INVALIDATE_CACHE_FOR_PROFILE_IN_CHANNEL = "inv_profile_in_channel" CLUSTER_EVENT_INVALIDATE_CACHE_FOR_SCHEMES = "inv_schemes" diff --git a/model/migration.go b/model/migration.go index 7b040082af5..cca8550ee34 100644 --- a/model/migration.go +++ b/model/migration.go @@ -15,5 +15,5 @@ const ( MIGRATION_KEY_REMOVE_CHANNEL_MANAGE_DELETE_FROM_TEAM_USER = "remove_channel_manage_delete_from_team_user" MIGRATION_KEY_VIEW_MEMBERS_NEW_PERMISSION = "view_members_new_permission" MIGRATION_KEY_ADD_MANAGE_GUESTS_PERMISSIONS = "add_manage_guests_permissions" - MIGRATION_KEY_ADD_USE_CHANNEL_MENTIONS_PERMISSION = "add_use_channel_mentions_permission" + MIGRATION_KEY_CHANNEL_MODERATIONS_PERMISSIONS = "channel_moderations_permissions" ) diff --git a/model/role.go b/model/role.go index ee3fa479276..08dce3c863d 100644 --- a/model/role.go +++ b/model/role.go @@ -9,6 +9,29 @@ import ( "strings" ) +var BuiltInSchemeManagedRoleIDs []string + +func init() { + BuiltInSchemeManagedRoleIDs = []string{ + SYSTEM_GUEST_ROLE_ID, + SYSTEM_USER_ROLE_ID, + SYSTEM_ADMIN_ROLE_ID, + SYSTEM_POST_ALL_ROLE_ID, + SYSTEM_POST_ALL_PUBLIC_ROLE_ID, + SYSTEM_USER_ACCESS_TOKEN_ROLE_ID, + + TEAM_GUEST_ROLE_ID, + TEAM_USER_ROLE_ID, + TEAM_ADMIN_ROLE_ID, + TEAM_POST_ALL_ROLE_ID, + TEAM_POST_ALL_PUBLIC_ROLE_ID, + + CHANNEL_GUEST_ROLE_ID, + CHANNEL_USER_ROLE_ID, + CHANNEL_ADMIN_ROLE_ID, + } +} + type RoleType string type RoleScope string @@ -60,6 +83,11 @@ type RolePatch struct { Permissions *[]string `json:"permissions"` } +type RolePermissions struct { + RoleID string + Permissions []string +} + func (r *Role) ToJson() string { b, _ := json.Marshal(r) return string(b) @@ -99,6 +127,45 @@ func (r *Role) Patch(patch *RolePatch) { } } +// MergeChannelHigherScopedPermissions is meant to be invoked on a channel scheme's role and merges the higher-scoped +// channel role's permissions. +func (r *Role) MergeChannelHigherScopedPermissions(higherScopedPermissions *RolePermissions) { + mergedPermissions := []string{} + + higherScopedPermissionsMap := AsStringBoolMap(higherScopedPermissions.Permissions) + rolePermissionsMap := AsStringBoolMap(r.Permissions) + + for _, cp := range ALL_PERMISSIONS { + if cp.Scope != PERMISSION_SCOPE_CHANNEL { + continue + } + + _, presentOnHigherScope := higherScopedPermissionsMap[cp.Id] + + // For the channel admin role always look to the higher scope to determine if the role has ther permission. + // The channel admin is a special case because they're not part of the UI to be "channel moderated", only + // channel members and channel guests are. + if higherScopedPermissions.RoleID == CHANNEL_ADMIN_ROLE_ID && presentOnHigherScope { + mergedPermissions = append(mergedPermissions, cp.Id) + continue + } + + _, permissionIsModerated := CHANNEL_MODERATED_PERMISSIONS_MAP[cp.Id] + if permissionIsModerated { + _, presentOnRole := rolePermissionsMap[cp.Id] + if presentOnRole && presentOnHigherScope { + mergedPermissions = append(mergedPermissions, cp.Id) + } + } else { + if presentOnHigherScope { + mergedPermissions = append(mergedPermissions, cp.Id) + } + } + } + + r.Permissions = mergedPermissions +} + // Returns an array of permissions that are in either role.Permissions // or patch.Permissions, but not both. func PermissionsChangedByPatch(role *Role, patch *RolePatch) []string { @@ -172,7 +239,7 @@ func ChannelModeratedPermissionsChangedByPatch(role *Role, patch *RolePatch) []s } // GetChannelModeratedPermissions returns a map of channel moderated permissions that the role has access to -func (r *Role) GetChannelModeratedPermissions() map[string]bool { +func (r *Role) GetChannelModeratedPermissions(channelType string) map[string]bool { moderatedPermissions := make(map[string]bool) for _, permission := range r.Permissions { if _, found := CHANNEL_MODERATED_PERMISSIONS_MAP[permission]; !found { @@ -180,8 +247,20 @@ func (r *Role) GetChannelModeratedPermissions() map[string]bool { } for moderated, moderatedPermissionValue := range CHANNEL_MODERATED_PERMISSIONS_MAP { + // the moderated permission has already been found to be true so skip this iteration + if moderatedPermissions[moderatedPermissionValue] { + continue + } + if moderated == permission { - moderatedPermissions[moderatedPermissionValue] = true + // Special case where the channel moderated permission for `manage_members` is different depending on whether the channel is private or public + if moderated == PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS.Id || moderated == PERMISSION_MANAGE_PRIVATE_CHANNEL_MEMBERS.Id { + canManagePublic := channelType == CHANNEL_OPEN && moderated == PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS.Id + canManagePrivate := channelType == CHANNEL_PRIVATE && moderated == PERMISSION_MANAGE_PRIVATE_CHANNEL_MEMBERS.Id + moderatedPermissions[moderatedPermissionValue] = canManagePublic || canManagePrivate + } else { + moderatedPermissions[moderatedPermissionValue] = true + } } } } diff --git a/model/role_test.go b/model/role_test.go index 8b28cfe9c65..a3bd9efae42 100644 --- a/model/role_test.go +++ b/model/role_test.go @@ -231,16 +231,19 @@ func TestGetChannelModeratedPermissions(t *testing.T) { tests := []struct { Name string Permissions []string + ChannelType string Expected map[string]bool }{ { "Filters non moderated permissions", []string{PERMISSION_CREATE_BOT.Id}, + CHANNEL_OPEN, map[string]bool{}, }, { "Returns a map of moderated permissions", []string{PERMISSION_CREATE_POST.Id, PERMISSION_ADD_REACTION.Id, PERMISSION_REMOVE_REACTION.Id, PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS.Id, PERMISSION_MANAGE_PRIVATE_CHANNEL_MEMBERS.Id, PERMISSION_USE_CHANNEL_MENTIONS.Id}, + CHANNEL_OPEN, map[string]bool{ CHANNEL_MODERATED_PERMISSIONS[0]: true, CHANNEL_MODERATED_PERMISSIONS[1]: true, @@ -251,6 +254,7 @@ func TestGetChannelModeratedPermissions(t *testing.T) { { "Returns a map of moderated permissions when non moderated present", []string{PERMISSION_CREATE_POST.Id, PERMISSION_CREATE_DIRECT_CHANNEL.Id}, + CHANNEL_OPEN, map[string]bool{ CHANNEL_MODERATED_PERMISSIONS[0]: true, }, @@ -258,13 +262,14 @@ func TestGetChannelModeratedPermissions(t *testing.T) { { "Returns a nothing when no permissions present", []string{}, + CHANNEL_OPEN, map[string]bool{}, }, } for _, tc := range tests { t.Run(tc.Name, func(t *testing.T) { role := &Role{Permissions: tc.Permissions} - moderatedPermissions := role.GetChannelModeratedPermissions() + moderatedPermissions := role.GetChannelModeratedPermissions(tc.ChannelType) for permission := range moderatedPermissions { assert.Equal(t, moderatedPermissions[permission], tc.Expected[permission]) } diff --git a/model/utils.go b/model/utils.go index 2f6d83fc2a4..053af33e6f7 100644 --- a/model/utils.go +++ b/model/utils.go @@ -639,3 +639,11 @@ func GetPreferredTimezone(timezone StringMap) string { func IsSamlFile(saml *SamlSettings, filename string) bool { return filename == *saml.PublicCertificateFile || filename == *saml.PrivateKeyFile || filename == *saml.IdpCertificateFile } + +func AsStringBoolMap(list []string) map[string]bool { + listMap := map[string]bool{} + for _, p := range list { + listMap[p] = true + } + return listMap +} diff --git a/model/websocket_message.go b/model/websocket_message.go index 4d69330197e..48d6e4b1305 100644 --- a/model/websocket_message.go +++ b/model/websocket_message.go @@ -22,6 +22,7 @@ const ( WEBSOCKET_EVENT_CHANNEL_RESTORED = "channel_restored" WEBSOCKET_EVENT_CHANNEL_UPDATED = "channel_updated" WEBSOCKET_EVENT_CHANNEL_MEMBER_UPDATED = "channel_member_updated" + WEBSOCKET_EVENT_CHANNEL_SCHEME_UPDATED = "channel_scheme_updated" WEBSOCKET_EVENT_DIRECT_ADDED = "direct_added" WEBSOCKET_EVENT_GROUP_ADDED = "group_added" WEBSOCKET_EVENT_NEW_USER = "new_user" diff --git a/store/localcachelayer/layer.go b/store/localcachelayer/layer.go index f6f2f54209c..0027e6c26c4 100644 --- a/store/localcachelayer/layer.go +++ b/store/localcachelayer/layer.go @@ -71,8 +71,9 @@ type LocalCacheStore struct { fileInfo LocalCacheFileInfoStore fileInfoCache cache.Cache - role LocalCacheRoleStore - roleCache cache.Cache + role LocalCacheRoleStore + roleCache cache.Cache + rolePermissionsCache cache.Cache scheme LocalCacheSchemeStore schemeCache cache.Cache @@ -118,6 +119,7 @@ func NewLocalCacheLayer(baseStore store.Store, metrics einterfaces.MetricsInterf // Roles localCacheStore.roleCache = cacheProvider.NewCacheWithParams(ROLE_CACHE_SIZE, "Role", ROLE_CACHE_SEC, model.CLUSTER_EVENT_INVALIDATE_CACHE_FOR_ROLES) + localCacheStore.rolePermissionsCache = cacheProvider.NewCacheWithParams(ROLE_CACHE_SIZE, "RolePermission", ROLE_CACHE_SEC, model.CLUSTER_EVENT_INVALIDATE_CACHE_FOR_ROLE_PERMISSIONS) localCacheStore.role = LocalCacheRoleStore{RoleStore: baseStore.Role(), rootStore: &localCacheStore} // Schemes @@ -165,6 +167,7 @@ func NewLocalCacheLayer(baseStore store.Store, metrics einterfaces.MetricsInterf if cluster != nil { cluster.RegisterClusterMessageHandler(model.CLUSTER_EVENT_INVALIDATE_CACHE_FOR_REACTIONS, localCacheStore.reaction.handleClusterInvalidateReaction) cluster.RegisterClusterMessageHandler(model.CLUSTER_EVENT_INVALIDATE_CACHE_FOR_ROLES, localCacheStore.role.handleClusterInvalidateRole) + cluster.RegisterClusterMessageHandler(model.CLUSTER_EVENT_INVALIDATE_CACHE_FOR_ROLE_PERMISSIONS, localCacheStore.role.handleClusterInvalidateRolePermissions) cluster.RegisterClusterMessageHandler(model.CLUSTER_EVENT_INVALIDATE_CACHE_FOR_SCHEMES, localCacheStore.scheme.handleClusterInvalidateScheme) cluster.RegisterClusterMessageHandler(model.CLUSTER_EVENT_INVALIDATE_CACHE_FOR_FILE_INFOS, localCacheStore.fileInfo.handleClusterInvalidateFileInfo) cluster.RegisterClusterMessageHandler(model.CLUSTER_EVENT_INVALIDATE_CACHE_FOR_LAST_POST_TIME, localCacheStore.post.handleClusterInvalidateLastPostTime) @@ -294,4 +297,6 @@ func (s *LocalCacheStore) Invalidate() { s.doClearCacheCluster(s.userProfileByIdsCache) s.doClearCacheCluster(s.profilesInChannelCache) s.doClearCacheCluster(s.teamAllTeamIdsForUserCache) + s.doClearCacheCluster(s.roleCache) + s.doClearCacheCluster(s.rolePermissionsCache) } diff --git a/store/localcachelayer/main_test.go b/store/localcachelayer/main_test.go index 93543ed3954..39c6655106d 100644 --- a/store/localcachelayer/main_test.go +++ b/store/localcachelayer/main_test.go @@ -32,6 +32,12 @@ func getMockCacheProvider() *mocks.CacheProvider { mock.AnythingOfType("int64"), mock.AnythingOfType("string")).Return(lru.New(128)) + mockCacheProvider.On("NewCacheWithParams", + mock.AnythingOfType("int"), + "RolePermission", + mock.AnythingOfType("int64"), + mock.AnythingOfType("string")).Return(lru.New(128)) + mockCacheProvider.On("NewCacheWithParams", mock.AnythingOfType("int"), "Scheme", diff --git a/store/localcachelayer/role_layer.go b/store/localcachelayer/role_layer.go index e2556c91c6e..2c6ac715fa9 100644 --- a/store/localcachelayer/role_layer.go +++ b/store/localcachelayer/role_layer.go @@ -4,6 +4,9 @@ package localcachelayer import ( + "sort" + "strings" + "github.com/mattermost/mattermost-server/v5/model" "github.com/mattermost/mattermost-server/v5/store" ) @@ -21,9 +24,18 @@ func (s *LocalCacheRoleStore) handleClusterInvalidateRole(msg *model.ClusterMess } } +func (s *LocalCacheRoleStore) handleClusterInvalidateRolePermissions(msg *model.ClusterMessage) { + if msg.Data == CLEAR_CACHE_MESSAGE_DATA { + s.rootStore.rolePermissionsCache.Purge() + } else { + s.rootStore.rolePermissionsCache.Remove(msg.Data) + } +} + func (s LocalCacheRoleStore) Save(role *model.Role) (*model.Role, *model.AppError) { if len(role.Name) != 0 { defer s.rootStore.doInvalidateCacheCluster(s.rootStore.roleCache, role.Name) + defer s.rootStore.doClearCacheCluster(s.rootStore.rolePermissionsCache) // TODO: This in other places? } return s.RoleStore.Save(role) } @@ -67,6 +79,7 @@ func (s LocalCacheRoleStore) Delete(roleId string) (*model.Role, *model.AppError if err == nil { s.rootStore.doInvalidateCacheCluster(s.rootStore.roleCache, role.Name) + defer s.rootStore.doClearCacheCluster(s.rootStore.rolePermissionsCache) } return role, err } @@ -74,6 +87,23 @@ func (s LocalCacheRoleStore) Delete(roleId string) (*model.Role, *model.AppError func (s LocalCacheRoleStore) PermanentDeleteAll() *model.AppError { defer s.rootStore.roleCache.Purge() defer s.rootStore.doClearCacheCluster(s.rootStore.roleCache) + defer s.rootStore.doClearCacheCluster(s.rootStore.rolePermissionsCache) return s.RoleStore.PermanentDeleteAll() } + +func (s LocalCacheRoleStore) ChannelHigherScopedPermissions(roleNames []string) (map[string]*model.RolePermissions, *model.AppError) { + sort.Strings(roleNames) + cacheKey := strings.Join(roleNames, "/") + if rolePermissionsMap := s.rootStore.doStandardReadCache(s.rootStore.rolePermissionsCache, cacheKey); rolePermissionsMap != nil { + return rolePermissionsMap.(map[string]*model.RolePermissions), nil + } + + rolePermissionsMap, err := s.RoleStore.ChannelHigherScopedPermissions(roleNames) + if err != nil { + return nil, err + } + + s.rootStore.doStandardAddToCache(s.rootStore.rolePermissionsCache, cacheKey, rolePermissionsMap) + return rolePermissionsMap, nil +} diff --git a/store/opentracing_layer.go b/store/opentracing_layer.go index 8976af7112a..8c2d904b0ea 100644 --- a/store/opentracing_layer.go +++ b/store/opentracing_layer.go @@ -5179,7 +5179,61 @@ func (s *OpenTracingLayerReactionStore) Save(reaction *model.Reaction) (*model.R return resultVar0, resultVar1 } -func (s *OpenTracingLayerRoleStore) Delete(roldId string) (*model.Role, *model.AppError) { +func (s *OpenTracingLayerRoleStore) AllChannelSchemeRoles() ([]*model.Role, *model.AppError) { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RoleStore.AllChannelSchemeRoles") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + resultVar0, resultVar1 := s.RoleStore.AllChannelSchemeRoles() + if resultVar1 != nil { + span.LogFields(spanlog.Error(resultVar1)) + ext.Error.Set(span, true) + } + + return resultVar0, resultVar1 +} + +func (s *OpenTracingLayerRoleStore) ChannelHigherScopedPermissions(roleNames []string) (map[string]*model.RolePermissions, *model.AppError) { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RoleStore.ChannelHigherScopedPermissions") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + resultVar0, resultVar1 := s.RoleStore.ChannelHigherScopedPermissions(roleNames) + if resultVar1 != nil { + span.LogFields(spanlog.Error(resultVar1)) + ext.Error.Set(span, true) + } + + return resultVar0, resultVar1 +} + +func (s *OpenTracingLayerRoleStore) ChannelRolesUnderTeamRole(roleName string) ([]*model.Role, *model.AppError) { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RoleStore.ChannelRolesUnderTeamRole") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + resultVar0, resultVar1 := s.RoleStore.ChannelRolesUnderTeamRole(roleName) + if resultVar1 != nil { + span.LogFields(spanlog.Error(resultVar1)) + ext.Error.Set(span, true) + } + + return resultVar0, resultVar1 +} + +func (s *OpenTracingLayerRoleStore) Delete(roleId string) (*model.Role, *model.AppError) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RoleStore.Delete") s.Root.Store.SetContext(newCtx) @@ -5188,7 +5242,7 @@ func (s *OpenTracingLayerRoleStore) Delete(roldId string) (*model.Role, *model.A }() defer span.Finish() - resultVar0, resultVar1 := s.RoleStore.Delete(roldId) + resultVar0, resultVar1 := s.RoleStore.Delete(roleId) if resultVar1 != nil { span.LogFields(spanlog.Error(resultVar1)) ext.Error.Set(span, true) diff --git a/store/sqlstore/channel_store.go b/store/sqlstore/channel_store.go index 717dccf8e79..2d13ec34ef9 100644 --- a/store/sqlstore/channel_store.go +++ b/store/sqlstore/channel_store.go @@ -353,6 +353,7 @@ func (s SqlChannelStore) createIndexesIfNotExists() { s.CreateIndexIfNotExists("idx_publicchannels_displayname_lower", "PublicChannels", "lower(DisplayName)") } s.CreateFullTextIndexIfNotExists("idx_publicchannels_search_txt", "PublicChannels", "Name, DisplayName, Purpose") + s.CreateIndexIfNotExists("idx_channels_scheme_id", "Channels", "SchemeId") } // MigratePublicChannels initializes the PublicChannels table with data created before this version diff --git a/store/sqlstore/role_store.go b/store/sqlstore/role_store.go index 62f48183164..9999a061e6d 100644 --- a/store/sqlstore/role_store.go +++ b/store/sqlstore/role_store.go @@ -9,6 +9,7 @@ import ( "net/http" "strings" + sq "github.com/Masterminds/squirrel" "github.com/mattermost/gorp" "github.com/mattermost/mattermost-server/v5/model" "github.com/mattermost/mattermost-server/v5/store" @@ -31,6 +32,15 @@ type Role struct { BuiltIn bool } +type channelRolesPermissions struct { + GuestRoleName string + UserRoleName string + AdminRoleName string + HigherScopedGuestPermissions string + HigherScopedUserPermissions string + HigherScopedAdminPermissions string +} + func NewRoleFromModel(role *model.Role) *Role { permissionsMap := make(map[string]bool) permissions := "" @@ -85,9 +95,6 @@ func newSqlRoleStore(sqlStore SqlStore) store.RoleStore { return s } -func (s SqlRoleStore) createIndexesIfNotExists() { -} - func (s *SqlRoleStore) Save(role *model.Role) (*model.Role, *model.AppError) { // Check the role is valid before proceeding. if !role.IsValidWithoutId() { @@ -240,3 +247,142 @@ func (s *SqlRoleStore) PermanentDeleteAll() *model.AppError { return nil } + +func (s *SqlRoleStore) channelHigherScopedPermissionsQuery(roleNames []string) string { + sqlTmpl := ` + SELECT + RoleSchemes.DefaultChannelGuestRole AS GuestRoleName, + RoleSchemes.DefaultChannelUserRole AS UserRoleName, + RoleSchemes.DefaultChannelAdminRole AS AdminRoleName, + GuestRoles.Permissions AS HigherScopedGuestPermissions, + UserRoles.Permissions AS HigherScopedUserPermissions, + AdminRoles.Permissions AS HigherScopedAdminPermissions + FROM + Schemes AS RoleSchemes + JOIN Channels ON Channels.SchemeId = RoleSchemes.Id + JOIN Teams ON Teams.Id = Channels.TeamId + JOIN Schemes ON Schemes.Id = Teams.SchemeId + JOIN Roles AS GuestRoles ON GuestRoles.Name = Schemes.DefaultChannelGuestRole + JOIN Roles AS UserRoles ON UserRoles.Name = Schemes.DefaultChannelUserRole + JOIN Roles AS AdminRoles ON AdminRoles.Name = Schemes.DefaultChannelAdminRole + WHERE + RoleSchemes.DefaultChannelGuestRole IN ('%[1]s') + OR RoleSchemes.DefaultChannelUserRole IN ('%[1]s') + OR RoleSchemes.DefaultChannelAdminRole IN ('%[1]s') + UNION + SELECT + Schemes.DefaultChannelGuestRole AS GuestRoleName, + Schemes.DefaultChannelUserRole AS UserRoleName, + Schemes.DefaultChannelAdminRole AS AdminRoleName, + GuestRoles.Permissions AS HigherScopedGuestPermissions, + UserRoles.Permissions AS HigherScopedUserPermissions, + AdminRoles.Permissions AS HigherScopedAdminPermissions + FROM + Schemes + JOIN Channels ON Channels.SchemeId = Schemes.Id + JOIN Teams ON Teams.Id = Channels.TeamId + JOIN Roles AS GuestRoles ON GuestRoles.Name = '%[2]s' + JOIN Roles AS UserRoles ON UserRoles.Name = '%[3]s' + JOIN Roles AS AdminRoles ON AdminRoles.Name = '%[4]s' + WHERE + (Schemes.DefaultChannelGuestRole IN ('%[1]s') + OR Schemes.DefaultChannelUserRole IN ('%[1]s') + OR Schemes.DefaultChannelAdminRole IN ('%[1]s')) + AND (Teams.SchemeId = '' + OR Teams.SchemeId IS NULL) + ` + + // The below three channel role names are referenced by their name value because there is no system scheme + // record that ships with Mattermost, otherwise the system scheme would be referenced by name and the channel + // roles would be referenced by their column names. + return fmt.Sprintf( + sqlTmpl, + strings.Join(roleNames, "', '"), + model.CHANNEL_GUEST_ROLE_ID, + model.CHANNEL_USER_ROLE_ID, + model.CHANNEL_ADMIN_ROLE_ID, + ) +} + +func (s *SqlRoleStore) ChannelHigherScopedPermissions(roleNames []string) (map[string]*model.RolePermissions, *model.AppError) { + sql := s.channelHigherScopedPermissionsQuery(roleNames) + + var rolesPermissions []*channelRolesPermissions + if _, err := s.GetReplica().Select(&rolesPermissions, sql); err != nil { + return nil, model.NewAppError("SqlRoleStore.HigherScopedPermissions", "store.sql_role.get_by_names.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + roleNameHigherScopedPermissions := map[string]*model.RolePermissions{} + + for _, rp := range rolesPermissions { + roleNameHigherScopedPermissions[rp.GuestRoleName] = &model.RolePermissions{RoleID: model.CHANNEL_GUEST_ROLE_ID, Permissions: strings.Split(rp.HigherScopedGuestPermissions, " ")} + roleNameHigherScopedPermissions[rp.UserRoleName] = &model.RolePermissions{RoleID: model.CHANNEL_USER_ROLE_ID, Permissions: strings.Split(rp.HigherScopedUserPermissions, " ")} + roleNameHigherScopedPermissions[rp.AdminRoleName] = &model.RolePermissions{RoleID: model.CHANNEL_ADMIN_ROLE_ID, Permissions: strings.Split(rp.HigherScopedAdminPermissions, " ")} + } + + return roleNameHigherScopedPermissions, nil +} + +func (s *SqlRoleStore) AllChannelSchemeRoles() ([]*model.Role, *model.AppError) { + query := s.getQueryBuilder(). + Select("Roles.*"). + From("Schemes"). + Join("Roles ON Schemes.DefaultChannelGuestRole = Roles.Name OR Schemes.DefaultChannelUserRole = Roles.Name OR Schemes.DefaultChannelAdminRole = Roles.Name"). + Where(sq.Eq{"Schemes.Scope": model.SCHEME_SCOPE_CHANNEL}). + Where(sq.Eq{"Roles.DeleteAt": 0}). + Where(sq.Eq{"Schemes.DeleteAt": 0}) + + queryString, args, err := query.ToSql() + if err != nil { + return nil, model.NewAppError("SqlRoleStore.AllChannelSchemeManagedRoles", "store.sql.build_query.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + var dbRoles []*Role + if _, err = s.GetReplica().Select(&dbRoles, queryString, args...); err != nil { + return nil, model.NewAppError("SqlRoleStore.AllChannelSchemeManagedRoles", "store.sql_role.get.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + var roles []*model.Role + for _, dbRole := range dbRoles { + roles = append(roles, dbRole.ToModel()) + } + + return roles, nil +} + +// ChannelRolesUnderTeamRole finds all of the channel-scheme roles under the team of the given team-scheme role. +func (s *SqlRoleStore) ChannelRolesUnderTeamRole(roleName string) ([]*model.Role, *model.AppError) { + query := s.getQueryBuilder(). + Select("ChannelSchemeRoles.*"). + From("Roles AS HigherScopedRoles"). + Join("Schemes AS HigherScopedSchemes ON (HigherScopedRoles.Name = HigherScopedSchemes.DefaultChannelGuestRole OR HigherScopedRoles.Name = HigherScopedSchemes.DefaultChannelUserRole OR HigherScopedRoles.Name = HigherScopedSchemes.DefaultChannelAdminRole)"). + Join("Teams ON Teams.SchemeId = HigherScopedSchemes.Id"). + Join("Channels ON Channels.TeamId = Teams.Id"). + Join("Schemes AS ChannelSchemes ON Channels.SchemeId = ChannelSchemes.Id"). + Join("Roles AS ChannelSchemeRoles ON (ChannelSchemeRoles.Name = ChannelSchemes.DefaultChannelGuestRole OR ChannelSchemeRoles.Name = ChannelSchemes.DefaultChannelUserRole OR ChannelSchemeRoles.Name = ChannelSchemes.DefaultChannelAdminRole)"). + Where(sq.Eq{"HigherScopedSchemes.Scope": model.SCHEME_SCOPE_TEAM}). + Where(sq.Eq{"HigherScopedRoles.Name": roleName}). + Where(sq.Eq{"HigherScopedRoles.DeleteAt": 0}). + Where(sq.Eq{"HigherScopedSchemes.DeleteAt": 0}). + Where(sq.Eq{"Teams.DeleteAt": 0}). + Where(sq.Eq{"Channels.DeleteAt": 0}). + Where(sq.Eq{"ChannelSchemes.DeleteAt": 0}). + Where(sq.Eq{"ChannelSchemeRoles.DeleteAt": 0}) + + queryString, args, err := query.ToSql() + if err != nil { + return nil, model.NewAppError("SqlRoleStore.ChannelRolesUnderTeamRole", "store.sql.build_query.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + var dbRoles []*Role + if _, err = s.GetReplica().Select(&dbRoles, queryString, args...); err != nil { + return nil, model.NewAppError("SqlRoleStore.ChannelRolesUnderTeamRole", "store.sql_role.get.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + var roles []*model.Role + for _, dbRole := range dbRoles { + roles = append(roles, dbRole.ToModel()) + } + + return roles, nil +} diff --git a/store/sqlstore/scheme_store.go b/store/sqlstore/scheme_store.go index d8db29e6699..cd16f9923bc 100644 --- a/store/sqlstore/scheme_store.go +++ b/store/sqlstore/scheme_store.go @@ -41,6 +41,9 @@ func newSqlSchemeStore(sqlStore SqlStore) store.SchemeStore { } func (s SqlSchemeStore) createIndexesIfNotExists() { + s.CreateIndexIfNotExists("idx_schemes_channel_guest_role", "Schemes", "DefaultChannelGuestRole") + s.CreateIndexIfNotExists("idx_schemes_channel_user_role", "Schemes", "DefaultChannelUserRole") + s.CreateIndexIfNotExists("idx_schemes_channel_admin_role", "Schemes", "DefaultChannelAdminRole") } func (s *SqlSchemeStore) Save(scheme *model.Scheme) (*model.Scheme, *model.AppError) { @@ -152,6 +155,7 @@ func (s *SqlSchemeStore) createScheme(scheme *model.Scheme, transaction *gorp.Tr } scheme.DefaultTeamGuestRole = savedRole.Name } + if scheme.Scope == model.SCHEME_SCOPE_TEAM || scheme.Scope == model.SCHEME_SCOPE_CHANNEL { // Channel Admin Role channelAdminRole := &model.Role{ @@ -161,6 +165,10 @@ func (s *SqlSchemeStore) createScheme(scheme *model.Scheme, transaction *gorp.Tr SchemeManaged: true, } + if scheme.Scope == model.SCHEME_SCOPE_CHANNEL { + channelAdminRole.Permissions = []string{} + } + savedRole, err := s.SqlStore.Role().(*SqlRoleStore).createRole(channelAdminRole, transaction) if err != nil { return nil, err @@ -175,6 +183,10 @@ func (s *SqlSchemeStore) createScheme(scheme *model.Scheme, transaction *gorp.Tr SchemeManaged: true, } + if scheme.Scope == model.SCHEME_SCOPE_CHANNEL { + channelUserRole.Permissions = filterModerated(channelUserRole.Permissions) + } + savedRole, err = s.SqlStore.Role().(*SqlRoleStore).createRole(channelUserRole, transaction) if err != nil { return nil, err @@ -189,6 +201,10 @@ func (s *SqlSchemeStore) createScheme(scheme *model.Scheme, transaction *gorp.Tr SchemeManaged: true, } + if scheme.Scope == model.SCHEME_SCOPE_CHANNEL { + channelGuestRole.Permissions = filterModerated(channelGuestRole.Permissions) + } + savedRole, err = s.SqlStore.Role().(*SqlRoleStore).createRole(channelGuestRole, transaction) if err != nil { return nil, err @@ -215,6 +231,16 @@ func (s *SqlSchemeStore) createScheme(scheme *model.Scheme, transaction *gorp.Tr return scheme, nil } +func filterModerated(permissions []string) []string { + filteredPermissions := []string{} + for _, perm := range permissions { + if _, ok := model.CHANNEL_MODERATED_PERMISSIONS_MAP[perm]; ok { + filteredPermissions = append(filteredPermissions, perm) + } + } + return filteredPermissions +} + func (s *SqlSchemeStore) Get(schemeId string) (*model.Scheme, *model.AppError) { var scheme model.Scheme if err := s.GetReplica().SelectOne(&scheme, "SELECT * from Schemes WHERE Id = :Id", map[string]interface{}{"Id": schemeId}); err != nil { diff --git a/store/sqlstore/supplier.go b/store/sqlstore/supplier.go index f4122d9be0a..3c4cb1cfd35 100644 --- a/store/sqlstore/supplier.go +++ b/store/sqlstore/supplier.go @@ -205,10 +205,8 @@ func NewSqlSupplier(settings model.SqlSettings, metrics einterfaces.MetricsInter supplier.stores.TermsOfService.(SqlTermsOfServiceStore).createIndexesIfNotExists() supplier.stores.UserTermsOfService.(SqlUserTermsOfServiceStore).createIndexesIfNotExists() supplier.stores.linkMetadata.(*SqlLinkMetadataStore).createIndexesIfNotExists() - supplier.stores.reaction.(*SqlReactionStore).createIndexesIfNotExists() - supplier.stores.role.(*SqlRoleStore).createIndexesIfNotExists() - supplier.stores.scheme.(*SqlSchemeStore).createIndexesIfNotExists() supplier.stores.group.(*SqlGroupStore).createIndexesIfNotExists() + supplier.stores.scheme.(*SqlSchemeStore).createIndexesIfNotExists() supplier.stores.preference.(*SqlPreferenceStore).deleteUnusedFeatures() return supplier diff --git a/store/sqlstore/supplier_reactions.go b/store/sqlstore/supplier_reactions.go index b3bd89b6b02..457e36750c4 100644 --- a/store/sqlstore/supplier_reactions.go +++ b/store/sqlstore/supplier_reactions.go @@ -29,9 +29,6 @@ func newSqlReactionStore(sqlStore SqlStore) store.ReactionStore { return s } -func (s SqlReactionStore) createIndexesIfNotExists() { -} - func (s *SqlReactionStore) Save(reaction *model.Reaction) (*model.Reaction, *model.AppError) { reaction.PreSave() if err := reaction.IsValid(); err != nil { diff --git a/store/sqlstore/team_store.go b/store/sqlstore/team_store.go index 60de970a331..0e8a1c36b20 100644 --- a/store/sqlstore/team_store.go +++ b/store/sqlstore/team_store.go @@ -183,6 +183,7 @@ func (s SqlTeamStore) createIndexesIfNotExists() { s.CreateIndexIfNotExists("idx_teams_update_at", "Teams", "UpdateAt") s.CreateIndexIfNotExists("idx_teams_create_at", "Teams", "CreateAt") s.CreateIndexIfNotExists("idx_teams_delete_at", "Teams", "DeleteAt") + s.CreateIndexIfNotExists("idx_teams_scheme_id", "Teams", "SchemeId") s.CreateIndexIfNotExists("idx_teammembers_team_id", "TeamMembers", "TeamId") s.CreateIndexIfNotExists("idx_teammembers_user_id", "TeamMembers", "UserId") diff --git a/store/sqlstore/upgrade.go b/store/sqlstore/upgrade.go index 51d1749f53b..308e9709f1b 100644 --- a/store/sqlstore/upgrade.go +++ b/store/sqlstore/upgrade.go @@ -772,8 +772,13 @@ func upgradeDatabaseToVersion521(sqlStore SqlStore) { } func upgradeDatabaseToVersion522(sqlStore SqlStore) { - // TODO: Uncomment following condition when version 5.22.0 is released // if shouldPerformUpgrade(sqlStore, VERSION_5_21_0, VERSION_5_22_0) { + sqlStore.CreateIndexIfNotExists("idx_teams_scheme_id", "Teams", "SchemeId") + sqlStore.CreateIndexIfNotExists("idx_channels_scheme_id", "Channels", "SchemeId") + sqlStore.CreateIndexIfNotExists("idx_channels_scheme_id", "Channels", "SchemeId") + sqlStore.CreateIndexIfNotExists("idx_schemes_channel_guest_role", "Schemes", "DefaultChannelGuestRole") + sqlStore.CreateIndexIfNotExists("idx_schemes_channel_user_role", "Schemes", "DefaultChannelUserRole") + sqlStore.CreateIndexIfNotExists("idx_schemes_channel_admin_role", "Schemes", "DefaultChannelAdminRole") // sqlStore.CreateColumnIfNotExistsNoDefault("Bots", "LastIconUpdate", "bigint", "bigint") // sqlStore.AlterPrimaryKey("Reactions", []string{"PostId", "UserId", "EmojiName"}) diff --git a/store/store.go b/store/store.go index 72aabc00088..851f9dc66f3 100644 --- a/store/store.go +++ b/store/store.go @@ -569,8 +569,19 @@ type RoleStore interface { GetAll() ([]*model.Role, *model.AppError) GetByName(name string) (*model.Role, *model.AppError) GetByNames(names []string) ([]*model.Role, *model.AppError) - Delete(roldId string) (*model.Role, *model.AppError) + Delete(roleId string) (*model.Role, *model.AppError) PermanentDeleteAll() *model.AppError + + // HigherScopedPermissions retrieves the higher-scoped permissions of a list of role names. The higher-scope + // (either team scheme or system scheme) is determined based on whether the team has a scheme or not. + ChannelHigherScopedPermissions(roleNames []string) (map[string]*model.RolePermissions, *model.AppError) + + // AllChannelSchemeRoles returns all of the roles associated to channel schemes. + AllChannelSchemeRoles() ([]*model.Role, *model.AppError) + + // ChannelRolesUnderTeamRole returns all of the non-deleted roles that are affected by updates to the + // given role. + ChannelRolesUnderTeamRole(roleName string) ([]*model.Role, *model.AppError) } type SchemeStore interface { diff --git a/store/storetest/group_store.go b/store/storetest/group_store.go index 432483f880c..b923df16aaf 100644 --- a/store/storetest/group_store.go +++ b/store/storetest/group_store.go @@ -16,6 +16,7 @@ import ( "github.com/mattermost/mattermost-server/v5/model" "github.com/mattermost/mattermost-server/v5/store" + "github.com/mattermost/mattermost-server/v5/utils" ) func TestGroupStore(t *testing.T, ss store.Store) { @@ -3620,15 +3621,6 @@ func groupTestpUpdateMembersRoleTeam(t *testing.T, ss store.Store) { }, } - includes := func(list []string, item string) bool { - for _, it := range list { - if it == item { - return true - } - } - return false - } - for _, tt := range tests { t.Run(tt.testName, func(t *testing.T) { err = ss.Team().UpdateMembersRole(team.Id, tt.inUserIDs) @@ -3639,7 +3631,7 @@ func groupTestpUpdateMembersRoleTeam(t *testing.T, ss store.Store) { require.GreaterOrEqual(t, len(members), 4) // sanity check for team membership for _, member := range members { - if includes(tt.inUserIDs, member.UserId) { + if utils.StringInSlice(member.UserId, tt.inUserIDs) { require.True(t, member.SchemeAdmin) } else { require.False(t, member.SchemeAdmin) @@ -3738,15 +3730,6 @@ func groupTestpUpdateMembersRoleChannel(t *testing.T, ss store.Store) { }, } - includes := func(list []string, item string) bool { - for _, it := range list { - if it == item { - return true - } - } - return false - } - for _, tt := range tests { t.Run(tt.testName, func(t *testing.T) { err = ss.Channel().UpdateMembersRole(channel.Id, tt.inUserIDs) @@ -3758,7 +3741,7 @@ func groupTestpUpdateMembersRoleChannel(t *testing.T, ss store.Store) { require.GreaterOrEqual(t, len(*members), 4) // sanity check for channel membership for _, member := range *members { - if includes(tt.inUserIDs, member.UserId) { + if utils.StringInSlice(member.UserId, tt.inUserIDs) { require.True(t, member.SchemeAdmin) } else { require.False(t, member.SchemeAdmin) diff --git a/store/storetest/mocks/RoleStore.go b/store/storetest/mocks/RoleStore.go index 46e3f07ac3f..a8809e33e32 100644 --- a/store/storetest/mocks/RoleStore.go +++ b/store/storetest/mocks/RoleStore.go @@ -14,13 +14,88 @@ type RoleStore struct { mock.Mock } -// Delete provides a mock function with given fields: roldId -func (_m *RoleStore) Delete(roldId string) (*model.Role, *model.AppError) { - ret := _m.Called(roldId) +// AllChannelSchemeRoles provides a mock function with given fields: +func (_m *RoleStore) AllChannelSchemeRoles() ([]*model.Role, *model.AppError) { + ret := _m.Called() + + var r0 []*model.Role + if rf, ok := ret.Get(0).(func() []*model.Role); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*model.Role) + } + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func() *model.AppError); ok { + r1 = rf() + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} + +// ChannelHigherScopedPermissions provides a mock function with given fields: roleNames +func (_m *RoleStore) ChannelHigherScopedPermissions(roleNames []string) (map[string]*model.RolePermissions, *model.AppError) { + ret := _m.Called(roleNames) + + var r0 map[string]*model.RolePermissions + if rf, ok := ret.Get(0).(func([]string) map[string]*model.RolePermissions); ok { + r0 = rf(roleNames) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]*model.RolePermissions) + } + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func([]string) *model.AppError); ok { + r1 = rf(roleNames) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} + +// ChannelRolesUnderTeamRole provides a mock function with given fields: roleName +func (_m *RoleStore) ChannelRolesUnderTeamRole(roleName string) ([]*model.Role, *model.AppError) { + ret := _m.Called(roleName) + + var r0 []*model.Role + if rf, ok := ret.Get(0).(func(string) []*model.Role); ok { + r0 = rf(roleName) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*model.Role) + } + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func(string) *model.AppError); ok { + r1 = rf(roleName) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} + +// Delete provides a mock function with given fields: roleId +func (_m *RoleStore) Delete(roleId string) (*model.Role, *model.AppError) { + ret := _m.Called(roleId) var r0 *model.Role if rf, ok := ret.Get(0).(func(string) *model.Role); ok { - r0 = rf(roldId) + r0 = rf(roleId) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*model.Role) @@ -29,7 +104,7 @@ func (_m *RoleStore) Delete(roldId string) (*model.Role, *model.AppError) { var r1 *model.AppError if rf, ok := ret.Get(1).(func(string) *model.AppError); ok { - r1 = rf(roldId) + r1 = rf(roleId) } else { if ret.Get(1) != nil { r1 = ret.Get(1).(*model.AppError) diff --git a/store/storetest/role_store.go b/store/storetest/role_store.go index ee4c04e6704..df6bbc202bd 100644 --- a/store/storetest/role_store.go +++ b/store/storetest/role_store.go @@ -21,6 +21,7 @@ func TestRoleStore(t *testing.T, ss store.Store) { 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) }) + t.Run("LowerScopedChannelSchemeRoles_AllChannelSchemeRoles", func(t *testing.T) { testRoleStoreLowerScopedChannelSchemeRoles(t, ss) }) } func testRoleStoreSave(t *testing.T, ss store.Store) { @@ -356,3 +357,159 @@ func testRoleStorePermanentDeleteAll(t *testing.T, ss store.Store) { assert.Nil(t, err) assert.Empty(t, roles) } + +func testRoleStoreLowerScopedChannelSchemeRoles(t *testing.T, ss store.Store) { + createDefaultRoles(t, ss) + + teamScheme1 := &model.Scheme{ + DisplayName: model.NewId(), + Name: model.NewId(), + Description: model.NewId(), + Scope: model.SCHEME_SCOPE_TEAM, + } + teamScheme1, err := ss.Scheme().Save(teamScheme1) + require.Nil(t, err) + defer ss.Scheme().Delete(teamScheme1.Id) + + teamScheme2 := &model.Scheme{ + DisplayName: model.NewId(), + Name: model.NewId(), + Description: model.NewId(), + Scope: model.SCHEME_SCOPE_TEAM, + } + teamScheme2, err = ss.Scheme().Save(teamScheme2) + require.Nil(t, err) + defer ss.Scheme().Delete(teamScheme2.Id) + + channelScheme1 := &model.Scheme{ + DisplayName: model.NewId(), + Name: model.NewId(), + Description: model.NewId(), + Scope: model.SCHEME_SCOPE_CHANNEL, + } + channelScheme1, err = ss.Scheme().Save(channelScheme1) + require.Nil(t, err) + defer ss.Scheme().Delete(channelScheme1.Id) + + channelScheme2 := &model.Scheme{ + DisplayName: model.NewId(), + Name: model.NewId(), + Description: model.NewId(), + Scope: model.SCHEME_SCOPE_CHANNEL, + } + channelScheme2, err = ss.Scheme().Save(channelScheme2) + require.Nil(t, err) + defer ss.Scheme().Delete(channelScheme1.Id) + + team1 := &model.Team{ + DisplayName: "Name", + Name: "zz" + model.NewId(), + Email: MakeEmail(), + Type: model.TEAM_OPEN, + SchemeId: &teamScheme1.Id, + } + team1, err = ss.Team().Save(team1) + require.Nil(t, err) + defer ss.Team().PermanentDelete(team1.Id) + + team2 := &model.Team{ + DisplayName: "Name", + Name: "zz" + model.NewId(), + Email: MakeEmail(), + Type: model.TEAM_OPEN, + SchemeId: &teamScheme2.Id, + } + team2, err = ss.Team().Save(team2) + require.Nil(t, err) + defer ss.Team().PermanentDelete(team2.Id) + + channel1 := &model.Channel{ + TeamId: team1.Id, + DisplayName: "Display " + model.NewId(), + Name: "zz" + model.NewId() + "b", + Type: model.CHANNEL_OPEN, + SchemeId: &channelScheme1.Id, + } + channel1, err = ss.Channel().Save(channel1, -1) + require.Nil(t, err) + defer ss.Channel().Delete(channel1.Id, 0) + + channel2 := &model.Channel{ + TeamId: team2.Id, + DisplayName: "Display " + model.NewId(), + Name: "zz" + model.NewId() + "b", + Type: model.CHANNEL_OPEN, + SchemeId: &channelScheme2.Id, + } + channel2, err = ss.Channel().Save(channel2, -1) + require.Nil(t, err) + defer ss.Channel().Delete(channel2.Id, 0) + + t.Run("ChannelRolesUnderTeamRole", func(t *testing.T) { + t.Run("guest role for the right team's channels are returned", func(t *testing.T) { + actualRoles, err := ss.Role().ChannelRolesUnderTeamRole(teamScheme1.DefaultChannelGuestRole) + require.Nil(t, err) + + var actualRoleNames []string + for _, role := range actualRoles { + actualRoleNames = append(actualRoleNames, role.Name) + } + + require.Contains(t, actualRoleNames, channelScheme1.DefaultChannelGuestRole) + require.NotContains(t, actualRoleNames, channelScheme2.DefaultChannelGuestRole) + }) + + t.Run("user role for the right team's channels are returned", func(t *testing.T) { + actualRoles, err := ss.Role().ChannelRolesUnderTeamRole(teamScheme1.DefaultChannelUserRole) + require.Nil(t, err) + + var actualRoleNames []string + for _, role := range actualRoles { + actualRoleNames = append(actualRoleNames, role.Name) + } + + require.Contains(t, actualRoleNames, channelScheme1.DefaultChannelUserRole) + require.NotContains(t, actualRoleNames, channelScheme2.DefaultChannelUserRole) + }) + + t.Run("admin role for the right team's channels are returned", func(t *testing.T) { + actualRoles, err := ss.Role().ChannelRolesUnderTeamRole(teamScheme1.DefaultChannelAdminRole) + require.Nil(t, err) + + var actualRoleNames []string + for _, role := range actualRoles { + actualRoleNames = append(actualRoleNames, role.Name) + } + + require.Contains(t, actualRoleNames, channelScheme1.DefaultChannelAdminRole) + require.NotContains(t, actualRoleNames, channelScheme2.DefaultChannelAdminRole) + }) + }) + + t.Run("AllChannelSchemeRoles", func(t *testing.T) { + t.Run("guest role for the right team's channels are returned", func(t *testing.T) { + actualRoles, err := ss.Role().AllChannelSchemeRoles() + require.Nil(t, err) + + var actualRoleNames []string + for _, role := range actualRoles { + actualRoleNames = append(actualRoleNames, role.Name) + } + + allRoleNames := []string{ + channelScheme1.DefaultChannelGuestRole, + channelScheme2.DefaultChannelGuestRole, + + channelScheme1.DefaultChannelUserRole, + channelScheme2.DefaultChannelUserRole, + + channelScheme1.DefaultChannelAdminRole, + channelScheme2.DefaultChannelAdminRole, + } + + for _, roleName := range allRoleNames { + require.Contains(t, actualRoleNames, roleName) + } + }) + }) +} diff --git a/store/timer_layer.go b/store/timer_layer.go index 817cc81421f..ac0abc09289 100644 --- a/store/timer_layer.go +++ b/store/timer_layer.go @@ -4716,10 +4716,58 @@ func (s *TimerLayerReactionStore) Save(reaction *model.Reaction) (*model.Reactio return resultVar0, resultVar1 } -func (s *TimerLayerRoleStore) Delete(roldId string) (*model.Role, *model.AppError) { +func (s *TimerLayerRoleStore) AllChannelSchemeRoles() ([]*model.Role, *model.AppError) { start := timemodule.Now() - resultVar0, resultVar1 := s.RoleStore.Delete(roldId) + resultVar0, resultVar1 := s.RoleStore.AllChannelSchemeRoles() + + elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second) + if s.Root.Metrics != nil { + success := "false" + if resultVar1 == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("RoleStore.AllChannelSchemeRoles", success, elapsed) + } + return resultVar0, resultVar1 +} + +func (s *TimerLayerRoleStore) ChannelHigherScopedPermissions(roleNames []string) (map[string]*model.RolePermissions, *model.AppError) { + start := timemodule.Now() + + resultVar0, resultVar1 := s.RoleStore.ChannelHigherScopedPermissions(roleNames) + + elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second) + if s.Root.Metrics != nil { + success := "false" + if resultVar1 == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("RoleStore.ChannelHigherScopedPermissions", success, elapsed) + } + return resultVar0, resultVar1 +} + +func (s *TimerLayerRoleStore) ChannelRolesUnderTeamRole(roleName string) ([]*model.Role, *model.AppError) { + start := timemodule.Now() + + resultVar0, resultVar1 := s.RoleStore.ChannelRolesUnderTeamRole(roleName) + + elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second) + if s.Root.Metrics != nil { + success := "false" + if resultVar1 == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("RoleStore.ChannelRolesUnderTeamRole", success, elapsed) + } + return resultVar0, resultVar1 +} + +func (s *TimerLayerRoleStore) Delete(roleId string) (*model.Role, *model.AppError) { + start := timemodule.Now() + + resultVar0, resultVar1 := s.RoleStore.Delete(roleId) elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second) if s.Root.Metrics != nil { diff --git a/testlib/store.go b/testlib/store.go index 34f20ac69f2..cfe60e5f79f 100644 --- a/testlib/store.go +++ b/testlib/store.go @@ -39,7 +39,7 @@ func GetMockStoreForSetupFunctions() *mocks.Store { systemStore.On("GetByName", model.MIGRATION_KEY_REMOVE_CHANNEL_MANAGE_DELETE_FROM_TEAM_USER).Return(&model.System{Name: model.MIGRATION_KEY_REMOVE_CHANNEL_MANAGE_DELETE_FROM_TEAM_USER, Value: "true"}, nil) systemStore.On("GetByName", model.MIGRATION_KEY_VIEW_MEMBERS_NEW_PERMISSION).Return(&model.System{Name: model.MIGRATION_KEY_VIEW_MEMBERS_NEW_PERMISSION, Value: "true"}, nil) systemStore.On("GetByName", model.MIGRATION_KEY_ADD_MANAGE_GUESTS_PERMISSIONS).Return(&model.System{Name: model.MIGRATION_KEY_ADD_MANAGE_GUESTS_PERMISSIONS, Value: "true"}, nil) - systemStore.On("GetByName", model.MIGRATION_KEY_ADD_USE_CHANNEL_MENTIONS_PERMISSION).Return(&model.System{Name: model.MIGRATION_KEY_ADD_USE_CHANNEL_MENTIONS_PERMISSION, Value: "true"}, nil) + systemStore.On("GetByName", model.MIGRATION_KEY_CHANNEL_MODERATIONS_PERMISSIONS).Return(&model.System{Name: model.MIGRATION_KEY_CHANNEL_MODERATIONS_PERMISSIONS, Value: "true"}, nil) systemStore.On("Get").Return(make(model.StringMap), nil) systemStore.On("Save", mock.AnythingOfType("*model.System")).Return(nil) @@ -57,6 +57,9 @@ func GetMockStoreForSetupFunctions() *mocks.Store { channelStore := mocks.ChannelStore{} channelStore.On("ClearCaches").Return(nil) + schemeStore := mocks.SchemeStore{} + schemeStore.On("GetAllPage", model.SCHEME_SCOPE_TEAM, mock.Anything, 100).Return([]*model.Scheme{}, nil) + teamStore := mocks.TeamStore{} mockStore.On("System").Return(&systemStore) @@ -65,6 +68,7 @@ func GetMockStoreForSetupFunctions() *mocks.Store { mockStore.On("Status").Return(&statusStore) mockStore.On("Channel").Return(&channelStore) mockStore.On("Team").Return(&teamStore) + mockStore.On("Scheme").Return(&schemeStore) mockStore.On("Close").Return(nil) mockStore.On("DropAllTables").Return(nil) mockStore.On("MarkSystemRanUnitTests").Return(nil) diff --git a/tests/channel-role-has-permission.csv b/tests/channel-role-has-permission.csv new file mode 100644 index 00000000000..98aa22aea2f --- /dev/null +++ b/tests/channel-role-has-permission.csv @@ -0,0 +1,17 @@ +"let p be ""higher-scoped scheme has the permission""","let q be ""permission is moderated""","let r be ""channel scheme has the permission""","let s be ""channel role is channel_admin""","""channel role has the permission"" = p ∧ (s ∨ (q → r))" +TRUE,TRUE,TRUE,TRUE,TRUE +TRUE,TRUE,TRUE,FALSE,TRUE +TRUE,TRUE,FALSE,TRUE,TRUE +TRUE,TRUE,FALSE,FALSE,FALSE +TRUE,FALSE,TRUE,TRUE,TRUE +TRUE,FALSE,TRUE,FALSE,TRUE +TRUE,FALSE,FALSE,TRUE,TRUE +TRUE,FALSE,FALSE,FALSE,TRUE +FALSE,TRUE,TRUE,TRUE,FALSE +FALSE,TRUE,TRUE,FALSE,FALSE +FALSE,TRUE,FALSE,TRUE,FALSE +FALSE,TRUE,FALSE,FALSE,FALSE +FALSE,FALSE,TRUE,TRUE,FALSE +FALSE,FALSE,TRUE,FALSE,FALSE +FALSE,FALSE,FALSE,TRUE,FALSE +FALSE,FALSE,FALSE,FALSE,FALSE \ No newline at end of file diff --git a/utils/utils.go b/utils/utils.go index f9d347cafc0..66e82a8f070 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -19,6 +19,7 @@ func StringInSlice(a string, slice []string) bool { return false } +// RemoveStringFromSlice removes the first occurrence of a from slice. func RemoveStringFromSlice(a string, slice []string) []string { for i, str := range slice { if str == a { @@ -28,6 +29,19 @@ func RemoveStringFromSlice(a string, slice []string) []string { return slice } +// RemoveStringsFromSlice removes all occurrences of strings from slice. +func RemoveStringsFromSlice(slice []string, strings ...string) []string { + newSlice := []string{} + + for _, item := range slice { + if !StringInSlice(item, strings) { + newSlice = append(newSlice, item) + } + } + + return newSlice +} + func StringArrayIntersection(arr1, arr2 []string) []string { arrMap := map[string]bool{} result := []string{}