From dd0b0a3d6793e73110dd7383b449d9300f143448 Mon Sep 17 00:00:00 2001 From: Martin Kraft Date: Tue, 17 Mar 2020 11:09:37 -0400 Subject: [PATCH] MM-22153: Adds tracking of channel moderation. (#14095) * MM-22153: Adds tracking of channel moderation. * MM-22153: Fixes test setup. * MM-22153: Split role constants into scope and type. --- app/diagnostics.go | 61 ++++++++ model/role.go | 11 ++ .../{role_supplier.go => role_store.go} | 0 .../{scheme_supplier.go => scheme_store.go} | 27 ++++ store/store.go | 2 + store/storetest/mocks/SchemeStore.go | 46 ++++++ store/storetest/scheme_store.go | 144 ++++++++++++++++++ 7 files changed, 291 insertions(+) rename store/sqlstore/{role_supplier.go => role_store.go} (100%) rename store/sqlstore/{scheme_supplier.go => scheme_store.go} (91%) diff --git a/app/diagnostics.go b/app/diagnostics.go index 2dd56b18f6..39140e3994 100644 --- a/app/diagnostics.go +++ b/app/diagnostics.go @@ -54,6 +54,7 @@ const ( TRACK_PERMISSIONS_TEAM_SCHEMES = "permissions_team_schemes" TRACK_ELASTICSEARCH = "elasticsearch" TRACK_GROUPS = "groups" + TRACK_CHANNEL_MODERATION = "channel_moderation" TRACK_ACTIVITY = "activity" TRACK_LICENSE = "license" @@ -76,6 +77,7 @@ func (a *App) sendDailyDiagnostics(override bool) { a.trackPermissions() a.trackElasticsearch() a.trackGroups() + a.trackChannelModeration() } } @@ -969,3 +971,62 @@ func (a *App) trackGroups() { "distinct_group_member_count": distinctGroupMemberCount, }) } + +func (a *App) trackChannelModeration() { + channelSchemeCount, err := a.Srv().Store.Scheme().CountByScope(model.SCHEME_SCOPE_CHANNEL) + if err != nil { + mlog.Error(err.Error()) + } + + createPostUser, err := a.Srv().Store.Scheme().CountWithoutPermission(model.SCHEME_SCOPE_CHANNEL, model.PERMISSION_CREATE_POST.Id, model.RoleScopeChannel, model.RoleTypeUser) + if err != nil { + mlog.Error(err.Error()) + } + + createPostGuest, err := a.Srv().Store.Scheme().CountWithoutPermission(model.SCHEME_SCOPE_CHANNEL, model.PERMISSION_CREATE_POST.Id, model.RoleScopeChannel, model.RoleTypeGuest) + if err != nil { + mlog.Error(err.Error()) + } + + // only need to track one of 'add_reaction' or 'remove_reaction` because they're both toggled together by the channel moderation feature + postReactionsUser, err := a.Srv().Store.Scheme().CountWithoutPermission(model.SCHEME_SCOPE_CHANNEL, model.PERMISSION_ADD_REACTION.Id, model.RoleScopeChannel, model.RoleTypeUser) + if err != nil { + mlog.Error(err.Error()) + } + + postReactionsGuest, err := a.Srv().Store.Scheme().CountWithoutPermission(model.SCHEME_SCOPE_CHANNEL, model.PERMISSION_ADD_REACTION.Id, model.RoleScopeChannel, model.RoleTypeGuest) + if err != nil { + mlog.Error(err.Error()) + } + + // only need to track one of 'manage_public_channel_members' or 'manage_private_channel_members` because they're both toggled together by the channel moderation feature + manageMembersUser, err := a.Srv().Store.Scheme().CountWithoutPermission(model.SCHEME_SCOPE_CHANNEL, model.PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS.Id, model.RoleScopeChannel, model.RoleTypeUser) + if err != nil { + mlog.Error(err.Error()) + } + + useChannelMentionsUser, err := a.Srv().Store.Scheme().CountWithoutPermission(model.SCHEME_SCOPE_CHANNEL, model.PERMISSION_USE_CHANNEL_MENTIONS.Id, model.RoleScopeChannel, model.RoleTypeUser) + if err != nil { + mlog.Error(err.Error()) + } + + useChannelMentionsGuest, err := a.Srv().Store.Scheme().CountWithoutPermission(model.SCHEME_SCOPE_CHANNEL, model.PERMISSION_USE_CHANNEL_MENTIONS.Id, model.RoleScopeChannel, model.RoleTypeGuest) + if err != nil { + mlog.Error(err.Error()) + } + + a.SendDiagnostic(TRACK_CHANNEL_MODERATION, map[string]interface{}{ + "channel_scheme_count": channelSchemeCount, + + "create_post_user_disabled_count": createPostUser, + "create_post_guest_disabled_count": createPostGuest, + + "post_reactions_user_disabled_count": postReactionsUser, + "post_reactions_guest_disabled_count": postReactionsGuest, + + "manage_members_user_disabled_count": manageMembersUser, // the UI does not allow this to be removed for guests + + "use_channel_mentions_user_disabled_count": useChannelMentionsUser, + "use_channel_mentions_guest_disabled_count": useChannelMentionsGuest, + }) +} diff --git a/model/role.go b/model/role.go index 4eda3809ff..ee3fa47927 100644 --- a/model/role.go +++ b/model/role.go @@ -9,6 +9,9 @@ import ( "strings" ) +type RoleType string +type RoleScope string + const ( SYSTEM_GUEST_ROLE_ID = "system_guest" SYSTEM_USER_ROLE_ID = "system_user" @@ -30,6 +33,14 @@ const ( ROLE_NAME_MAX_LENGTH = 64 ROLE_DISPLAY_NAME_MAX_LENGTH = 128 ROLE_DESCRIPTION_MAX_LENGTH = 1024 + + RoleScopeSystem RoleScope = "System" + RoleScopeTeam RoleScope = "Team" + RoleScopeChannel RoleScope = "Channel" + + RoleTypeGuest RoleType = "Guest" + RoleTypeUser RoleType = "User" + RoleTypeAdmin RoleType = "Admin" ) type Role struct { diff --git a/store/sqlstore/role_supplier.go b/store/sqlstore/role_store.go similarity index 100% rename from store/sqlstore/role_supplier.go rename to store/sqlstore/role_store.go diff --git a/store/sqlstore/scheme_supplier.go b/store/sqlstore/scheme_store.go similarity index 91% rename from store/sqlstore/scheme_supplier.go rename to store/sqlstore/scheme_store.go index 04ef4c799c..d8db29e669 100644 --- a/store/sqlstore/scheme_supplier.go +++ b/store/sqlstore/scheme_store.go @@ -322,3 +322,30 @@ func (s *SqlSchemeStore) PermanentDeleteAll() *model.AppError { return nil } + +func (s *SqlSchemeStore) CountByScope(scope string) (int64, *model.AppError) { + count, err := s.GetReplica().SelectInt("SELECT count(*) FROM Schemes WHERE Scope = :Scope AND DeleteAt = 0", map[string]interface{}{"Scope": scope}) + if err != nil { + return int64(0), model.NewAppError("SqlSchemeStore.CountByScope", "store.select_error", nil, err.Error(), http.StatusInternalServerError) + } + return count, nil +} + +func (s *SqlSchemeStore) CountWithoutPermission(schemeScope, permissionID string, roleScope model.RoleScope, roleType model.RoleType) (int64, *model.AppError) { + joinCol := fmt.Sprintf("Default%s%sRole", roleScope, roleType) + query := fmt.Sprintf(` + SELECT + count(*) + FROM Schemes + JOIN Roles ON Roles.Name = Schemes.%s + WHERE + Schemes.DeleteAt = 0 AND + Schemes.Scope = '%s' AND + Roles.Permissions NOT LIKE '%%%s%%' + `, joinCol, schemeScope, permissionID) + count, err := s.GetReplica().SelectInt(query) + if err != nil { + return int64(0), model.NewAppError("SqlSchemeStore.CountWithoutPermission", "store.select_error", nil, err.Error(), http.StatusInternalServerError) + } + return count, nil +} diff --git a/store/store.go b/store/store.go index 26e61a1d18..72aabc0008 100644 --- a/store/store.go +++ b/store/store.go @@ -580,6 +580,8 @@ type SchemeStore interface { GetAllPage(scope string, offset int, limit int) ([]*model.Scheme, *model.AppError) Delete(schemeId string) (*model.Scheme, *model.AppError) PermanentDeleteAll() *model.AppError + CountByScope(scope string) (int64, *model.AppError) + CountWithoutPermission(scope, permissionID string, roleScope model.RoleScope, roleType model.RoleType) (int64, *model.AppError) } type TermsOfServiceStore interface { diff --git a/store/storetest/mocks/SchemeStore.go b/store/storetest/mocks/SchemeStore.go index 27e7b5cc28..65d652fb8e 100644 --- a/store/storetest/mocks/SchemeStore.go +++ b/store/storetest/mocks/SchemeStore.go @@ -14,6 +14,52 @@ type SchemeStore struct { mock.Mock } +// CountByScope provides a mock function with given fields: scope +func (_m *SchemeStore) CountByScope(scope string) (int64, *model.AppError) { + ret := _m.Called(scope) + + var r0 int64 + if rf, ok := ret.Get(0).(func(string) int64); ok { + r0 = rf(scope) + } else { + r0 = ret.Get(0).(int64) + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func(string) *model.AppError); ok { + r1 = rf(scope) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} + +// CountWithoutPermission provides a mock function with given fields: scope, permissionID, roleScope, roleType +func (_m *SchemeStore) CountWithoutPermission(scope string, permissionID string, roleScope model.RoleScope, roleType model.RoleType) (int64, *model.AppError) { + ret := _m.Called(scope, permissionID, roleScope, roleType) + + var r0 int64 + if rf, ok := ret.Get(0).(func(string, string, model.RoleScope, model.RoleType) int64); ok { + r0 = rf(scope, permissionID, roleScope, roleType) + } else { + r0 = ret.Get(0).(int64) + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func(string, string, model.RoleScope, model.RoleType) *model.AppError); ok { + r1 = rf(scope, permissionID, roleScope, roleType) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} + // Delete provides a mock function with given fields: schemeId func (_m *SchemeStore) Delete(schemeId string) (*model.Scheme, *model.AppError) { ret := _m.Called(schemeId) diff --git a/store/storetest/scheme_store.go b/store/storetest/scheme_store.go index c81a4fc21c..a3780c0d21 100644 --- a/store/storetest/scheme_store.go +++ b/store/storetest/scheme_store.go @@ -22,6 +22,8 @@ func TestSchemeStore(t *testing.T, ss store.Store) { t.Run("Delete", func(t *testing.T) { testSchemeStoreDelete(t, ss) }) t.Run("PermanentDeleteAll", func(t *testing.T) { testSchemeStorePermanentDeleteAll(t, ss) }) t.Run("GetByName", func(t *testing.T) { testSchemeStoreGetByName(t, ss) }) + t.Run("CountByScope", func(t *testing.T) { testSchemeStoreCountByScope(t, ss) }) + t.Run("CountWithoutPermission", func(t *testing.T) { testCountWithoutPermission(t, ss) }) } func createDefaultRoles(t *testing.T, ss store.Store) { @@ -486,3 +488,145 @@ func testSchemeStorePermanentDeleteAll(t *testing.T, ss store.Store) { assert.Nil(t, err) assert.Empty(t, schemes) } + +func testSchemeStoreCountByScope(t *testing.T, ss store.Store) { + testCounts := func(expectedTeamCount, expectedChannelCount int) { + actualCount, err := ss.Scheme().CountByScope(model.SCHEME_SCOPE_TEAM) + require.Nil(t, err) + require.Equal(t, int64(expectedTeamCount), actualCount) + + actualCount, err = ss.Scheme().CountByScope(model.SCHEME_SCOPE_CHANNEL) + require.Nil(t, err) + require.Equal(t, int64(expectedChannelCount), actualCount) + } + + createScheme := func(scope string) { + _, err := ss.Scheme().Save(&model.Scheme{ + Name: model.NewId(), + DisplayName: model.NewId(), + Description: model.NewId(), + Scope: scope, + }) + require.Nil(t, err) + } + + err := ss.Scheme().PermanentDeleteAll() + require.Nil(t, err) + + createScheme(model.SCHEME_SCOPE_CHANNEL) + createScheme(model.SCHEME_SCOPE_TEAM) + testCounts(1, 1) + createScheme(model.SCHEME_SCOPE_TEAM) + testCounts(2, 1) + createScheme(model.SCHEME_SCOPE_CHANNEL) + testCounts(2, 2) +} + +func testCountWithoutPermission(t *testing.T, ss store.Store) { + perm := model.PERMISSION_CREATE_POST.Id + + createScheme := func(scope string) *model.Scheme { + scheme, err := ss.Scheme().Save(&model.Scheme{ + Name: model.NewId(), + DisplayName: model.NewId(), + Description: model.NewId(), + Scope: scope, + }) + require.Nil(t, err) + return scheme + } + + getRoles := func(scheme *model.Scheme) (channelUser, channelGuest *model.Role) { + var err *model.AppError + channelUser, err = ss.Role().GetByName(scheme.DefaultChannelUserRole) + require.Nil(t, err) + require.NotNil(t, channelUser) + channelGuest, err = ss.Role().GetByName(scheme.DefaultChannelGuestRole) + require.Nil(t, err) + require.NotNil(t, channelGuest) + return + } + + teamScheme1 := createScheme(model.SCHEME_SCOPE_TEAM) + defer ss.Scheme().Delete(teamScheme1.Id) + teamScheme2 := createScheme(model.SCHEME_SCOPE_TEAM) + defer ss.Scheme().Delete(teamScheme2.Id) + channelScheme1 := createScheme(model.SCHEME_SCOPE_CHANNEL) + defer ss.Scheme().Delete(channelScheme1.Id) + channelScheme2 := createScheme(model.SCHEME_SCOPE_CHANNEL) + defer ss.Scheme().Delete(channelScheme2.Id) + + ts1User, ts1Guest := getRoles(teamScheme1) + ts2User, ts2Guest := getRoles(teamScheme2) + cs1User, cs1Guest := getRoles(channelScheme1) + cs2User, cs2Guest := getRoles(channelScheme2) + + allRoles := []*model.Role{ + ts1User, + ts1Guest, + ts2User, + ts2Guest, + cs1User, + cs1Guest, + cs2User, + cs2Guest, + } + + teamUserCount, err := ss.Scheme().CountWithoutPermission(model.SCHEME_SCOPE_TEAM, perm, model.RoleScopeChannel, model.RoleTypeUser) + require.Nil(t, err) + require.Equal(t, int64(0), teamUserCount) + + teamGuestCount, err := ss.Scheme().CountWithoutPermission(model.SCHEME_SCOPE_TEAM, perm, model.RoleScopeChannel, model.RoleTypeGuest) + require.Nil(t, err) + require.Equal(t, int64(0), teamGuestCount) + + var tests = []struct { + removePermissionFromRole *model.Role + expectTeamSchemeChannelUserCount int + expectTeamSchemeChannelGuestCount int + expectChannelSchemeChannelUserCount int + expectChannelSchemeChannelGuestCount int + }{ + {ts1User, 1, 0, 0, 0}, + {ts1Guest, 1, 1, 0, 0}, + {ts2User, 2, 1, 0, 0}, + {ts2Guest, 2, 2, 0, 0}, + {cs1User, 2, 2, 1, 0}, + {cs1Guest, 2, 2, 1, 1}, + {cs2User, 2, 2, 2, 1}, + {cs2Guest, 2, 2, 2, 2}, + } + + removePermission := func(targetRole *model.Role) { + roleMatched := false + for _, role := range allRoles { + if targetRole == role { + roleMatched = true + role.Permissions = []string{} + _, err = ss.Role().Save(role) + require.Nil(t, err) + } + } + require.True(t, roleMatched) + } + + for _, test := range tests { + removePermission(test.removePermissionFromRole) + + count, err := ss.Scheme().CountWithoutPermission(model.SCHEME_SCOPE_TEAM, perm, model.RoleScopeChannel, model.RoleTypeUser) + require.Nil(t, err) + require.Equal(t, int64(test.expectTeamSchemeChannelUserCount), count) + + count, err = ss.Scheme().CountWithoutPermission(model.SCHEME_SCOPE_TEAM, perm, model.RoleScopeChannel, model.RoleTypeGuest) + require.Nil(t, err) + require.Equal(t, int64(test.expectTeamSchemeChannelGuestCount), count) + + count, err = ss.Scheme().CountWithoutPermission(model.SCHEME_SCOPE_CHANNEL, perm, model.RoleScopeChannel, model.RoleTypeUser) + require.Nil(t, err) + require.Equal(t, int64(test.expectChannelSchemeChannelUserCount), count) + + count, err = ss.Scheme().CountWithoutPermission(model.SCHEME_SCOPE_CHANNEL, perm, model.RoleScopeChannel, model.RoleTypeGuest) + require.Nil(t, err) + require.Equal(t, int64(test.expectChannelSchemeChannelGuestCount), count) + } +}