From 79c786bc0cbd737185407c7cde827fa4baec6047 Mon Sep 17 00:00:00 2001 From: Farhan Munshi <3207297+fm2munsh@users.noreply.github.com> Date: Thu, 5 Mar 2020 10:04:34 -0500 Subject: [PATCH] [MM-22206] Add PATCH channel moderations (PUT /moderations/patch) (#13845) * MM-22205 Add get channel moderations endpoint * MM-22206 Add patch channel moderations endpoint Add api tests for patch channel moderations * MM-22205 Ensure ordered permissions returned and create struct ChannelModeratedRoles * MM-22206 Use structs instead of map * MM-22206 Add test cases for GetChannelModeratedPermissions * MM-22206 Add tests for ChannelModeratedPermissionsChangedByPatch * MM-22206 Use NewBool instead of defining booleans * MM-22206 Tie Channel Mentions to Create Posts when building Channel Moderations * Revert "MM-22206 Tie Channel Mentions to Create Posts when building Channel Moderations" This reverts commit a0bfc95f1732955c5ef5fc3e4b05ea8ab954acea. * MM-22206 Review changes Modify GetSchemeRolesForChannel to return named variables Change calls to SessionHasPermissionToChannel to SessionHasPermissionTo Add a CreateChannelScheme method Add a DeleteChannelScheme method Move GetChannelModeratedPermissions to Role model * Fix lint * Add ChannelModeration methods to App interface * MM-22206 Rename method to GetTeamSchemeChannelRoles * MM-22206 Check CHANNEL_MODERATED_PERMISSIONS_MAP for existing permission before iterating through it * Modify wording to higherScoped to match #13813 * MM-22206 Delete channel scheme between tests * MM-22206 Fix tests * Actually patch role * MM-22206 Shadow declaration of err --- api4/api.go | 2 + api4/apitestlib.go | 22 +++ api4/channel.go | 81 +++++++++- api4/channel_test.go | 224 ++++++++++++++++++++++++++++ app/app_iface.go | 2 + app/channel.go | 206 ++++++++++++++++++++++--- app/channel_test.go | 347 +++++++++++++++++++++++++++++++++++++++++++ i18n/en.json | 8 + model/channel.go | 37 +++++ model/client4.go | 19 +++ model/permission.go | 19 +++ model/role.go | 109 ++++++++++++++ model/role_test.go | 273 ++++++++++++++++++++++++++++++++++ 13 files changed, 1331 insertions(+), 18 deletions(-) create mode 100644 model/role_test.go diff --git a/api4/api.go b/api4/api.go index 88cc69941f..20748c5ce6 100644 --- a/api4/api.go +++ b/api4/api.go @@ -45,6 +45,7 @@ type Routes struct { ChannelMembers *mux.Router // 'api/v4/channels/{channel_id:[A-Za-z0-9]+}/members' ChannelMember *mux.Router // 'api/v4/channels/{channel_id:[A-Za-z0-9]+}/members/{user_id:[A-Za-z0-9]+}' ChannelMembersForUser *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/teams/{team_id:[A-Za-z0-9]+}/channels/members' + ChannelModerations *mux.Router // 'api/v4/channels/{channel_id:[A-Za-z0-9]+}/moderations' Posts *mux.Router // 'api/v4/posts' Post *mux.Router // 'api/v4/posts/{post_id:[A-Za-z0-9]+}' @@ -156,6 +157,7 @@ func Init(configservice configservice.ConfigService, globalOptionsFunc app.AppOp api.BaseRoutes.ChannelMembers = api.BaseRoutes.Channel.PathPrefix("/members").Subrouter() api.BaseRoutes.ChannelMember = api.BaseRoutes.ChannelMembers.PathPrefix("/{user_id:[A-Za-z0-9]+}").Subrouter() api.BaseRoutes.ChannelMembersForUser = api.BaseRoutes.User.PathPrefix("/teams/{team_id:[A-Za-z0-9]+}/channels/members").Subrouter() + api.BaseRoutes.ChannelModerations = api.BaseRoutes.Channel.PathPrefix("/moderations").Subrouter() api.BaseRoutes.Posts = api.BaseRoutes.ApiRoot.PathPrefix("/posts").Subrouter() api.BaseRoutes.Post = api.BaseRoutes.Posts.PathPrefix("/{post_id:[A-Za-z0-9]+}").Subrouter() diff --git a/api4/apitestlib.go b/api4/apitestlib.go index 3b768aaec4..81d7b8d577 100644 --- a/api4/apitestlib.go +++ b/api4/apitestlib.go @@ -1009,3 +1009,25 @@ func (me *TestHelper) AddPermissionToRole(permission string, roleName string) { utils.EnableDebugLogForTest() } + +func (me *TestHelper) SetupTeamScheme() *model.Scheme { + return me.SetupScheme(model.SCHEME_SCOPE_TEAM) +} + +func (me *TestHelper) SetupChannelScheme() *model.Scheme { + return me.SetupScheme(model.SCHEME_SCOPE_CHANNEL) +} + +func (me *TestHelper) SetupScheme(scope string) *model.Scheme { + scheme := model.Scheme{ + Name: model.NewId(), + DisplayName: model.NewId(), + Scope: scope, + } + + if scheme, err := me.App.CreateScheme(&scheme); err == nil { + return scheme + } else { + panic(err) + } +} diff --git a/api4/channel.go b/api4/channel.go index f23b7791ba..f261b00bfb 100644 --- a/api4/channel.go +++ b/api4/channel.go @@ -43,6 +43,7 @@ func (api *API) InitChannel() { api.BaseRoutes.Channel.Handle("/pinned", api.ApiSessionRequired(getPinnedPosts)).Methods("GET") api.BaseRoutes.Channel.Handle("/timezones", api.ApiSessionRequired(getChannelMembersTimezones)).Methods("GET") api.BaseRoutes.Channel.Handle("/members_minus_group_members", api.ApiSessionRequired(channelMembersMinusGroupMembers)).Methods("GET") + api.BaseRoutes.ChannelForUser.Handle("/unread", api.ApiSessionRequired(getChannelUnread)).Methods("GET") api.BaseRoutes.ChannelByName.Handle("", api.ApiSessionRequired(getChannelByName)).Methods("GET") @@ -57,6 +58,9 @@ func (api *API) InitChannel() { api.BaseRoutes.ChannelMember.Handle("/roles", api.ApiSessionRequired(updateChannelMemberRoles)).Methods("PUT") api.BaseRoutes.ChannelMember.Handle("/schemeRoles", api.ApiSessionRequired(updateChannelMemberSchemeRoles)).Methods("PUT") api.BaseRoutes.ChannelMember.Handle("/notify_props", api.ApiSessionRequired(updateChannelMemberNotifyProps)).Methods("PUT") + + api.BaseRoutes.ChannelModerations.Handle("", api.ApiSessionRequired(getChannelModerations)).Methods("GET") + api.BaseRoutes.ChannelModerations.Handle("/patch", api.ApiSessionRequired(patchChannelModerations)).Methods("PUT") } func createChannel(c *Context, w http.ResponseWriter, r *http.Request) { @@ -1453,7 +1457,7 @@ func updateChannelScheme(c *Context, w http.ResponseWriter, r *http.Request) { return } - if !c.App.SessionHasPermissionToChannel(*c.App.Session(), c.Params.ChannelId, model.PERMISSION_MANAGE_SYSTEM) { + if !c.App.SessionHasPermissionTo(*c.App.Session(), model.PERMISSION_MANAGE_SYSTEM) { c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM) return } @@ -1535,3 +1539,78 @@ func channelMembersMinusGroupMembers(c *Context, w http.ResponseWriter, r *http. w.Write(b) } + +func getChannelModerations(c *Context, w http.ResponseWriter, r *http.Request) { + if c.App.License() == nil { + c.Err = model.NewAppError("Api4.GetChannelModerations", "api.channel.get_channel_moderations.license.error", nil, "", http.StatusNotImplemented) + return + } + + c.RequireChannelId() + if c.Err != nil { + return + } + + if !c.App.SessionHasPermissionTo(*c.App.Session(), model.PERMISSION_MANAGE_SYSTEM) { + c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM) + return + } + + channel, err := c.App.GetChannel(c.Params.ChannelId) + if err != nil { + c.Err = err + return + } + + channelModerations, err := c.App.GetChannelModerationsForChannel(channel) + if err != nil { + c.Err = err + return + } + + b, marshalErr := json.Marshal(channelModerations) + if marshalErr != nil { + c.Err = model.NewAppError("Api4.getChannelModerations", "api.marshal_error", nil, marshalErr.Error(), http.StatusInternalServerError) + return + } + + w.Write(b) +} + +func patchChannelModerations(c *Context, w http.ResponseWriter, r *http.Request) { + if c.App.License() == nil { + c.Err = model.NewAppError("Api4.patchChannelModerations", "api.channel.patch_channel_moderations.license.error", nil, "", http.StatusNotImplemented) + return + } + + c.RequireChannelId() + if c.Err != nil { + return + } + + if !c.App.SessionHasPermissionTo(*c.App.Session(), model.PERMISSION_MANAGE_SYSTEM) { + c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM) + return + } + + channel, err := c.App.GetChannel(c.Params.ChannelId) + if err != nil { + c.Err = err + return + } + + channelModerationsPatch := model.ChannelModerationsPatchFromJson(r.Body) + channelModerations, err := c.App.PatchChannelModerationsForChannel(channel, channelModerationsPatch) + if err != nil { + c.Err = err + return + } + + b, marshalErr := json.Marshal(channelModerations) + if marshalErr != nil { + c.Err = model.NewAppError("Api4.patchChannelModerations", "api.marshal_error", nil, marshalErr.Error(), http.StatusInternalServerError) + return + } + + w.Write(b) +} diff --git a/api4/channel_test.go b/api4/channel_test.go index a06d1366e2..960cbfaa60 100644 --- a/api4/channel_test.go +++ b/api4/channel_test.go @@ -3001,3 +3001,227 @@ func TestChannelMembersMinusGroupMembers(t *testing.T) { }) } } + +func TestGetChannelModerations(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + channel := th.BasicChannel + team := th.BasicTeam + + th.App.SetPhase2PermissionsMigrationStatus(true) + + t.Run("Errors without a license", func(t *testing.T) { + _, res := th.SystemAdminClient.GetChannelModerations(channel.Id, "") + require.Equal(t, "api.channel.get_channel_moderations.license.error", res.Error.Id) + }) + + th.App.SetLicense(model.NewTestLicense()) + + t.Run("Errors as a non sysadmin", func(t *testing.T) { + _, res := th.Client.GetChannelModerations(channel.Id, "") + require.Equal(t, "api.context.permissions.app_error", res.Error.Id) + }) + + th.App.SetLicense(model.NewTestLicense()) + + t.Run("Returns default moderations with default roles", func(t *testing.T) { + moderations, res := th.SystemAdminClient.GetChannelModerations(channel.Id, "") + require.Nil(t, res.Error) + require.Equal(t, len(moderations), 4) + for _, moderation := range moderations { + if moderation.Name == "manage_members" { + require.Empty(t, moderation.Roles.Guests) + } else { + require.Equal(t, moderation.Roles.Guests.Value, true) + require.Equal(t, moderation.Roles.Guests.Enabled, true) + } + + require.Equal(t, moderation.Roles.Members.Value, true) + require.Equal(t, moderation.Roles.Members.Enabled, true) + } + }) + + t.Run("Returns value false and enabled false for permissions that are not present in higher scoped scheme when no channel scheme present", func(t *testing.T) { + scheme := th.SetupTeamScheme() + team.SchemeId = &scheme.Id + _, err := th.App.UpdateTeamScheme(team) + require.Nil(t, err) + + th.RemovePermissionFromRole(model.PERMISSION_CREATE_POST.Id, scheme.DefaultChannelGuestRole) + defer th.AddPermissionToRole(model.PERMISSION_CREATE_POST.Id, scheme.DefaultChannelGuestRole) + + moderations, res := th.SystemAdminClient.GetChannelModerations(channel.Id, "") + require.Nil(t, res.Error) + for _, moderation := range moderations { + if moderation.Name == model.PERMISSION_CREATE_POST.Id { + require.Equal(t, moderation.Roles.Members.Value, true) + require.Equal(t, moderation.Roles.Members.Enabled, true) + require.Equal(t, moderation.Roles.Guests.Value, false) + require.Equal(t, moderation.Roles.Guests.Enabled, false) + } + } + }) + + t.Run("Returns value false and enabled true for permissions that are not present in channel scheme but present in team scheme", func(t *testing.T) { + scheme := th.SetupChannelScheme() + channel.SchemeId = &scheme.Id + _, err := th.App.UpdateChannelScheme(channel) + require.Nil(t, err) + + th.RemovePermissionFromRole(model.PERMISSION_CREATE_POST.Id, scheme.DefaultChannelGuestRole) + defer th.AddPermissionToRole(model.PERMISSION_CREATE_POST.Id, scheme.DefaultChannelGuestRole) + + moderations, res := th.SystemAdminClient.GetChannelModerations(channel.Id, "") + require.Nil(t, res.Error) + for _, moderation := range moderations { + if moderation.Name == model.PERMISSION_CREATE_POST.Id { + require.Equal(t, moderation.Roles.Members.Value, true) + require.Equal(t, moderation.Roles.Members.Enabled, true) + require.Equal(t, moderation.Roles.Guests.Value, false) + require.Equal(t, moderation.Roles.Guests.Enabled, true) + } + } + }) + + t.Run("Returns value false and enabled false for permissions that are not present in channel & team scheme", func(t *testing.T) { + teamScheme := th.SetupTeamScheme() + team.SchemeId = &teamScheme.Id + th.App.UpdateTeamScheme(team) + + scheme := th.SetupChannelScheme() + channel.SchemeId = &scheme.Id + th.App.UpdateChannelScheme(channel) + + th.RemovePermissionFromRole(model.PERMISSION_CREATE_POST.Id, scheme.DefaultChannelGuestRole) + th.RemovePermissionFromRole(model.PERMISSION_CREATE_POST.Id, teamScheme.DefaultChannelGuestRole) + + defer th.AddPermissionToRole(model.PERMISSION_CREATE_POST.Id, scheme.DefaultChannelGuestRole) + defer th.AddPermissionToRole(model.PERMISSION_CREATE_POST.Id, teamScheme.DefaultChannelGuestRole) + + moderations, res := th.SystemAdminClient.GetChannelModerations(channel.Id, "") + require.Nil(t, res.Error) + for _, moderation := range moderations { + if moderation.Name == model.PERMISSION_CREATE_POST.Id { + require.Equal(t, moderation.Roles.Members.Value, true) + require.Equal(t, moderation.Roles.Members.Enabled, true) + require.Equal(t, moderation.Roles.Guests.Value, false) + require.Equal(t, moderation.Roles.Guests.Enabled, false) + } + } + }) +} + +func TestPatchChannelModerations(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + channel := th.BasicChannel + + emptyPatch := []*model.ChannelModerationPatch{} + + createPosts := model.CHANNEL_MODERATED_PERMISSIONS[0] + + th.App.SetPhase2PermissionsMigrationStatus(true) + + t.Run("Errors without a license", func(t *testing.T) { + _, res := th.SystemAdminClient.PatchChannelModerations(channel.Id, emptyPatch) + require.Equal(t, "api.channel.patch_channel_moderations.license.error", res.Error.Id) + }) + + th.App.SetLicense(model.NewTestLicense()) + + t.Run("Errors as a non sysadmin", func(t *testing.T) { + _, res := th.Client.PatchChannelModerations(channel.Id, emptyPatch) + require.Equal(t, "api.context.permissions.app_error", res.Error.Id) + }) + + th.App.SetLicense(model.NewTestLicense()) + + t.Run("Returns default moderations with empty patch", func(t *testing.T) { + moderations, res := th.SystemAdminClient.PatchChannelModerations(channel.Id, emptyPatch) + require.Nil(t, res.Error) + require.Equal(t, len(moderations), 4) + for _, moderation := range moderations { + if moderation.Name == "manage_members" { + require.Empty(t, moderation.Roles.Guests) + } else { + require.Equal(t, moderation.Roles.Guests.Value, true) + require.Equal(t, moderation.Roles.Guests.Enabled, true) + } + + require.Equal(t, moderation.Roles.Members.Value, true) + require.Equal(t, moderation.Roles.Members.Enabled, true) + } + + require.Nil(t, channel.SchemeId) + }) + + t.Run("Creates a scheme and returns the updated channel moderations when patching an existing permission", func(t *testing.T) { + patch := []*model.ChannelModerationPatch{ + { + Name: &createPosts, + Roles: &model.ChannelModeratedRolesPatch{Members: model.NewBool(false)}, + }, + } + + moderations, res := th.SystemAdminClient.PatchChannelModerations(channel.Id, patch) + require.Nil(t, res.Error) + require.Equal(t, len(moderations), 4) + for _, moderation := range moderations { + if moderation.Name == "manage_members" { + require.Empty(t, moderation.Roles.Guests) + } else { + require.Equal(t, moderation.Roles.Guests.Value, true) + require.Equal(t, moderation.Roles.Guests.Enabled, true) + } + + if moderation.Name == createPosts { + require.Equal(t, moderation.Roles.Members.Value, false) + require.Equal(t, moderation.Roles.Members.Enabled, true) + } else { + require.Equal(t, moderation.Roles.Members.Value, true) + require.Equal(t, moderation.Roles.Members.Enabled, true) + } + } + channel, _ = th.App.GetChannel(channel.Id) + require.NotNil(t, channel.SchemeId) + }) + + t.Run("Removes the existing scheme when moderated permissions are set back to higher scoped values", func(t *testing.T) { + channel, _ = th.App.GetChannel(channel.Id) + schemeId := channel.SchemeId + + scheme, _ := th.App.GetScheme(*schemeId) + require.Equal(t, scheme.DeleteAt, int64(0)) + + patch := []*model.ChannelModerationPatch{ + { + Name: &createPosts, + Roles: &model.ChannelModeratedRolesPatch{Members: model.NewBool(true)}, + }, + } + + moderations, res := th.SystemAdminClient.PatchChannelModerations(channel.Id, patch) + require.Nil(t, res.Error) + require.Equal(t, len(moderations), 4) + for _, moderation := range moderations { + if moderation.Name == "manage_members" { + require.Empty(t, moderation.Roles.Guests) + } else { + require.Equal(t, moderation.Roles.Guests.Value, true) + require.Equal(t, moderation.Roles.Guests.Enabled, true) + } + + require.Equal(t, moderation.Roles.Members.Value, true) + require.Equal(t, moderation.Roles.Members.Enabled, true) + } + + channel, _ = th.App.GetChannel(channel.Id) + require.Nil(t, channel.SchemeId) + + scheme, _ = th.App.GetScheme(*schemeId) + require.NotEqual(t, scheme.DeleteAt, int64(0)) + }) + +} diff --git a/app/app_iface.go b/app/app_iface.go index 9604cf3f84..5b8566712a 100644 --- a/app/app_iface.go +++ b/app/app_iface.go @@ -461,6 +461,7 @@ type AppIface interface { GetChannelMembersForUserWithPagination(teamId, userId string, page, perPage int) ([]*model.ChannelMember, *model.AppError) GetChannelMembersPage(channelId string, page, perPage int) (*model.ChannelMembers, *model.AppError) GetChannelMembersTimezones(channelId string) ([]string, *model.AppError) + GetChannelModerationsForChannel(channel *model.Channel) ([]*model.ChannelModeration, *model.AppError) GetChannelPinnedPostCount(channelId string) (int64, *model.AppError) GetChannelUnread(channelId, userId string) (*model.ChannelUnread, *model.AppError) GetChannelsByNames(channelNames []string, teamId string) ([]*model.Channel, *model.AppError) @@ -715,6 +716,7 @@ type AppIface interface { OpenInteractiveDialog(request model.OpenDialogRequest) *model.AppError OriginChecker() func(*http.Request) bool PatchChannel(channel *model.Channel, patch *model.ChannelPatch, userId string) (*model.Channel, *model.AppError) + PatchChannelModerationsForChannel(channel *model.Channel, channelModerationsPatch []*model.ChannelModerationPatch) ([]*model.ChannelModeration, *model.AppError) PatchPost(postId string, patch *model.PostPatch) (*model.Post, *model.AppError) PatchRole(role *model.Role, patch *model.RolePatch) (*model.Role, *model.AppError) PatchScheme(scheme *model.Scheme, patch *model.SchemePatch) (*model.Scheme, *model.AppError) diff --git a/app/channel.go b/app/channel.go index aec6a3b2a1..2ee89a6466 100644 --- a/app/channel.go +++ b/app/channel.go @@ -547,6 +547,34 @@ func (a *App) UpdateChannel(channel *model.Channel) (*model.Channel, *model.AppE return channel, nil } +// CreateChannelScheme creates a new Scheme of scope channel and assigns it to the channel. +func (a *App) CreateChannelScheme(channel *model.Channel) (*model.Scheme, *model.AppError) { + scheme, err := a.CreateScheme(&model.Scheme{ + Name: model.NewId(), + DisplayName: model.NewId(), + Scope: model.SCHEME_SCOPE_CHANNEL, + }) + if err != nil { + return nil, err + } + + channel.SchemeId = &scheme.Id + if _, err := a.UpdateChannelScheme(channel); err != nil { + return nil, err + } + return scheme, nil +} + +// DeleteChannelScheme deletes a channels scheme and sets its SchemeId to nil. +func (a *App) DeleteChannelScheme(channel *model.Channel) (*model.Channel, *model.AppError) { + if _, err := a.DeleteScheme(*channel.SchemeId); err != nil { + return nil, err + } + channel.SchemeId = nil + return a.UpdateChannelScheme(channel) +} + +// UpdateChannelScheme saves the new SchemeId of the channel passed. func (a *App) UpdateChannelScheme(channel *model.Channel) (*model.Channel, *model.AppError) { var oldChannel *model.Channel var err *model.AppError @@ -555,13 +583,7 @@ func (a *App) UpdateChannelScheme(channel *model.Channel) (*model.Channel, *mode } oldChannel.SchemeId = channel.SchemeId - - newChannel, err := a.UpdateChannel(oldChannel) - if err != nil { - return nil, err - } - - return newChannel, nil + return a.UpdateChannel(oldChannel) } func (a *App) UpdateChannelPrivacy(oldChannel *model.Channel, user *model.User) (*model.Channel, *model.AppError) { @@ -685,35 +707,185 @@ func (a *App) PatchChannel(channel *model.Channel, patch *model.ChannelPatch, us return channel, nil } -func (a *App) GetSchemeRolesForChannel(channelId string) (string, string, string, *model.AppError) { +// GetSchemeRolesForChannel Checks if a channel or its team has an override scheme for channel roles and returns the scheme roles or default channel roles. +func (a *App) GetSchemeRolesForChannel(channelId string) (guestRoleName, userRoleName, adminRoleName string, err *model.AppError) { channel, err := a.GetChannel(channelId) if err != nil { - return "", "", "", err + return } if channel.SchemeId != nil && len(*channel.SchemeId) != 0 { var scheme *model.Scheme scheme, err = a.GetScheme(*channel.SchemeId) if err != nil { - return "", "", "", err + return } - return scheme.DefaultChannelGuestRole, scheme.DefaultChannelUserRole, scheme.DefaultChannelAdminRole, nil + + guestRoleName = scheme.DefaultChannelGuestRole + userRoleName = scheme.DefaultChannelUserRole + adminRoleName = scheme.DefaultChannelAdminRole + + return } - team, err := a.GetTeam(channel.TeamId) + return a.GetTeamSchemeChannelRoles(channel.TeamId) +} + +// GetTeamSchemeChannelRoles Checks if a team has an override scheme and returns the scheme channel role names or default channel role names. +func (a *App) GetTeamSchemeChannelRoles(teamId string) (guestRoleName, userRoleName, adminRoleName string, err *model.AppError) { + team, err := a.GetTeam(teamId) if err != nil { - return "", "", "", err + return } if team.SchemeId != nil && len(*team.SchemeId) != 0 { - scheme, err := a.GetScheme(*team.SchemeId) + var scheme *model.Scheme + scheme, err = a.GetScheme(*team.SchemeId) if err != nil { - return "", "", "", err + return } - return scheme.DefaultChannelGuestRole, scheme.DefaultChannelUserRole, scheme.DefaultChannelAdminRole, nil + + guestRoleName = scheme.DefaultChannelGuestRole + userRoleName = scheme.DefaultChannelUserRole + adminRoleName = scheme.DefaultChannelAdminRole + } else { + guestRoleName = model.CHANNEL_GUEST_ROLE_ID + userRoleName = model.CHANNEL_USER_ROLE_ID + adminRoleName = model.CHANNEL_ADMIN_ROLE_ID } - return model.CHANNEL_GUEST_ROLE_ID, model.CHANNEL_USER_ROLE_ID, model.CHANNEL_ADMIN_ROLE_ID, nil + return +} + +// PatchChannelModerationsForChannel Gets a channels ChannelModerations from either the higherScoped roles or from the channel scheme roles. +func (a *App) GetChannelModerationsForChannel(channel *model.Channel) ([]*model.ChannelModeration, *model.AppError) { + guestRoleName, memberRoleName, _, _ := a.GetSchemeRolesForChannel(channel.Id) + memberRole, err := a.GetRoleByName(memberRoleName) + if err != nil { + return nil, err + } + + guestRole, err := a.GetRoleByName(guestRoleName) + if err != nil { + return nil, err + } + + higherScopedGuestRoleName, higherScopedMemberRoleName, _, _ := a.GetTeamSchemeChannelRoles(channel.TeamId) + higherScopedMemberRole, err := a.GetRoleByName(higherScopedMemberRoleName) + if err != nil { + return nil, err + } + + higherScopedGuestRole, err := a.GetRoleByName(higherScopedGuestRoleName) + if err != nil { + return nil, err + } + + return buildChannelModerations(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. +func (a *App) PatchChannelModerationsForChannel(channel *model.Channel, channelModerationsPatch []*model.ChannelModerationPatch) ([]*model.ChannelModeration, *model.AppError) { + higherScopedGuestRoleName, higherScopedMemberRoleName, _, _ := a.GetTeamSchemeChannelRoles(channel.TeamId) + higherScopedMemberRole, err := a.GetRoleByName(higherScopedMemberRoleName) + if err != nil { + return nil, err + } + + higherScopedGuestRole, err := a.GetRoleByName(higherScopedGuestRoleName) + if err != nil { + return nil, err + } + + higherScopedMemberPermissions := higherScopedMemberRole.GetChannelModeratedPermissions() + higherScopedGuestPermissions := higherScopedGuestRole.GetChannelModeratedPermissions() + + for _, moderationPatch := range channelModerationsPatch { + if moderationPatch.Roles.Members != nil && *moderationPatch.Roles.Members && !higherScopedMemberPermissions[*moderationPatch.Name] { + return nil, &model.AppError{Message: "Cannot add a permission that is restricted by the team or system permission scheme"} + } + if moderationPatch.Roles.Guests != nil && *moderationPatch.Roles.Guests && !higherScopedGuestPermissions[*moderationPatch.Name] { + return nil, &model.AppError{Message: "Cannot add a permission that is restricted by the team or system permission scheme"} + } + } + + // Channel has no scheme so create one + if channel.SchemeId == nil || len(*channel.SchemeId) == 0 { + if _, err = a.CreateChannelScheme(channel); err != nil { + return nil, err + } + } + + guestRoleName, memberRoleName, _, _ := a.GetSchemeRolesForChannel(channel.Id) + memberRole, err := a.GetRoleByName(memberRoleName) + if err != nil { + return nil, err + } + + guestRole, err := a.GetRoleByName(guestRoleName) + if err != nil { + return nil, err + } + + memberRolePatch := memberRole.RolePatchFromChannelModerationsPatch(channelModerationsPatch, "members") + guestRolePatch := guestRole.RolePatchFromChannelModerationsPatch(channelModerationsPatch, "guests") + + memberRolePermissionsUnmodified := len(model.ChannelModeratedPermissionsChangedByPatch(higherScopedMemberRole, memberRolePatch)) == 0 + guestRolePermissionsUnmodified := len(model.ChannelModeratedPermissionsChangedByPatch(higherScopedGuestRole, guestRolePatch)) == 0 + if memberRolePermissionsUnmodified && guestRolePermissionsUnmodified { + // The channel scheme matches the permissions of its higherScoped scheme so delete the scheme + if _, err = a.DeleteChannelScheme(channel); err != nil { + return nil, err + } + memberRole = higherScopedMemberRole + guestRole = higherScopedGuestRole + } else { + memberRole, err = a.PatchRole(memberRole, memberRolePatch) + if err != nil { + return nil, err + } + guestRole, err = a.PatchRole(guestRole, guestRolePatch) + if err != nil { + return nil, err + } + } + + return buildChannelModerations(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() + + var channelModerations []*model.ChannelModeration + for _, permissionKey := range model.CHANNEL_MODERATED_PERMISSIONS { + roles := &model.ChannelModeratedRoles{} + + roles.Members = &model.ChannelModeratedRole{ + Value: memberPermissions[permissionKey], + Enabled: higherScopedMemberPermissions[permissionKey], + } + + if permissionKey == "manage_members" { + roles.Guests = nil + } else { + roles.Guests = &model.ChannelModeratedRole{ + Value: guestPermissions[permissionKey], + Enabled: higherScopedGuestPermissions[permissionKey], + } + } + + moderation := &model.ChannelModeration{ + Name: permissionKey, + Roles: roles, + } + + channelModerations = append(channelModerations, moderation) + } + + return channelModerations } func (a *App) UpdateChannelMemberRoles(channelId string, userId string, newRoles string) (*model.ChannelMember, *model.AppError) { diff --git a/app/channel_test.go b/app/channel_test.go index e5d98fe177..0b6c1dfc1a 100644 --- a/app/channel_test.go +++ b/app/channel_test.go @@ -1301,3 +1301,350 @@ func TestRemoveUserFromChannel(t *testing.T) { err = th.App.RemoveUserFromChannel(botUser.Id, th.SystemAdminUser.Id, privateChannel) require.Nil(t, err) } + +func TestPatchChannelModerationsForChannel(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + th.App.SetPhase2PermissionsMigrationStatus(true) + channel := th.BasicChannel + + createPosts := model.CHANNEL_MODERATED_PERMISSIONS[0] + createReactions := model.CHANNEL_MODERATED_PERMISSIONS[1] + manageMembers := model.CHANNEL_MODERATED_PERMISSIONS[2] + channelMentions := model.CHANNEL_MODERATED_PERMISSIONS[3] + + nonChannelModeratedPermission := model.PERMISSION_CREATE_BOT.Id + + testCases := []struct { + Name string + ChannelModerationsPatch []*model.ChannelModerationPatch + PermissionsModeratedByPatch map[string]*model.ChannelModeratedRoles + RevertChannelModerationsPatch []*model.ChannelModerationPatch + HigherScopedMemberPermissions []string + HigherScopedGuestPermissions []string + ShouldError bool + ShouldHaveNoChannelScheme bool + }{ + { + Name: "Removing create posts from members role", + ChannelModerationsPatch: []*model.ChannelModerationPatch{ + { + Name: &createPosts, + Roles: &model.ChannelModeratedRolesPatch{Members: model.NewBool(false)}, + }, + }, + PermissionsModeratedByPatch: map[string]*model.ChannelModeratedRoles{ + createPosts: { + Members: &model.ChannelModeratedRole{Value: false, Enabled: true}, + }, + }, + RevertChannelModerationsPatch: []*model.ChannelModerationPatch{ + { + Name: &createPosts, + Roles: &model.ChannelModeratedRolesPatch{Members: model.NewBool(true)}, + }, + }, + }, + { + Name: "Removing create reactions from members role", + ChannelModerationsPatch: []*model.ChannelModerationPatch{ + { + Name: &createReactions, + Roles: &model.ChannelModeratedRolesPatch{Members: model.NewBool(false)}, + }, + }, + PermissionsModeratedByPatch: map[string]*model.ChannelModeratedRoles{ + createReactions: { + Members: &model.ChannelModeratedRole{Value: false, Enabled: true}, + }, + }, + RevertChannelModerationsPatch: []*model.ChannelModerationPatch{ + { + Name: &createReactions, + Roles: &model.ChannelModeratedRolesPatch{Members: model.NewBool(true)}, + }, + }, + }, + { + Name: "Removing channel mentions from members role", + ChannelModerationsPatch: []*model.ChannelModerationPatch{ + { + Name: &channelMentions, + Roles: &model.ChannelModeratedRolesPatch{Members: model.NewBool(false)}, + }, + }, + PermissionsModeratedByPatch: map[string]*model.ChannelModeratedRoles{ + channelMentions: { + Members: &model.ChannelModeratedRole{Value: false, Enabled: true}, + }, + }, + RevertChannelModerationsPatch: []*model.ChannelModerationPatch{ + { + Name: &channelMentions, + Roles: &model.ChannelModeratedRolesPatch{Members: model.NewBool(true)}, + }, + }, + }, + { + Name: "Removing manage members from members role", + ChannelModerationsPatch: []*model.ChannelModerationPatch{ + { + Name: &manageMembers, + Roles: &model.ChannelModeratedRolesPatch{Members: model.NewBool(false)}, + }, + }, + PermissionsModeratedByPatch: map[string]*model.ChannelModeratedRoles{ + manageMembers: { + Members: &model.ChannelModeratedRole{Value: false, Enabled: true}, + }, + }, + RevertChannelModerationsPatch: []*model.ChannelModerationPatch{ + { + Name: &manageMembers, + Roles: &model.ChannelModeratedRolesPatch{Members: model.NewBool(true)}, + }, + }, + }, + { + Name: "Removing create posts from guests role", + ChannelModerationsPatch: []*model.ChannelModerationPatch{ + { + Name: &createPosts, + Roles: &model.ChannelModeratedRolesPatch{Guests: model.NewBool(false)}, + }, + }, + PermissionsModeratedByPatch: map[string]*model.ChannelModeratedRoles{ + createPosts: { + Guests: &model.ChannelModeratedRole{Value: false, Enabled: true}, + }, + }, + RevertChannelModerationsPatch: []*model.ChannelModerationPatch{ + { + Name: &createPosts, + Roles: &model.ChannelModeratedRolesPatch{Guests: model.NewBool(true)}, + }, + }, + }, + { + Name: "Removing create reactions from guests role", + ChannelModerationsPatch: []*model.ChannelModerationPatch{ + { + Name: &createReactions, + Roles: &model.ChannelModeratedRolesPatch{Guests: model.NewBool(false)}, + }, + }, + PermissionsModeratedByPatch: map[string]*model.ChannelModeratedRoles{ + createReactions: { + Guests: &model.ChannelModeratedRole{Value: false, Enabled: true}, + }, + }, + RevertChannelModerationsPatch: []*model.ChannelModerationPatch{ + { + Name: &createReactions, + Roles: &model.ChannelModeratedRolesPatch{Guests: model.NewBool(true)}, + }, + }, + }, + { + Name: "Removing channel mentions from guests role", + ChannelModerationsPatch: []*model.ChannelModerationPatch{ + { + Name: &channelMentions, + Roles: &model.ChannelModeratedRolesPatch{Guests: model.NewBool(false)}, + }, + }, + PermissionsModeratedByPatch: map[string]*model.ChannelModeratedRoles{ + channelMentions: { + Guests: &model.ChannelModeratedRole{Value: false, Enabled: true}, + }, + }, + RevertChannelModerationsPatch: []*model.ChannelModerationPatch{ + { + Name: &channelMentions, + Roles: &model.ChannelModeratedRolesPatch{Guests: model.NewBool(true)}, + }, + }, + }, + { + Name: "Removing manage members from guests role should error", + ChannelModerationsPatch: []*model.ChannelModerationPatch{ + { + Name: &manageMembers, + Roles: &model.ChannelModeratedRolesPatch{Guests: model.NewBool(false)}, + }, + }, + PermissionsModeratedByPatch: map[string]*model.ChannelModeratedRoles{}, + ShouldError: true, + }, + { + Name: "Removing a permission that is not channel moderated should error", + ChannelModerationsPatch: []*model.ChannelModerationPatch{ + { + Name: &nonChannelModeratedPermission, + Roles: &model.ChannelModeratedRolesPatch{ + Members: model.NewBool(false), + Guests: model.NewBool(false), + }, + }, + }, + PermissionsModeratedByPatch: map[string]*model.ChannelModeratedRoles{}, + ShouldError: true, + }, + { + Name: "Error when adding a permission that is disabled in the parent member role", + ChannelModerationsPatch: []*model.ChannelModerationPatch{ + { + Name: &createPosts, + Roles: &model.ChannelModeratedRolesPatch{ + Members: model.NewBool(true), + Guests: model.NewBool(false), + }, + }, + }, + PermissionsModeratedByPatch: map[string]*model.ChannelModeratedRoles{}, + HigherScopedMemberPermissions: []string{}, + ShouldError: true, + }, + { + Name: "Error when adding a permission that is disabled in the parent guest role", + ChannelModerationsPatch: []*model.ChannelModerationPatch{ + { + Name: &createPosts, + Roles: &model.ChannelModeratedRolesPatch{ + Members: model.NewBool(false), + Guests: model.NewBool(true), + }, + }, + }, + PermissionsModeratedByPatch: map[string]*model.ChannelModeratedRoles{}, + HigherScopedGuestPermissions: []string{}, + ShouldError: true, + }, + { + Name: "Removing a permission from the member role that is disabled in the parent guest role", + ChannelModerationsPatch: []*model.ChannelModerationPatch{ + { + Name: &createPosts, + Roles: &model.ChannelModeratedRolesPatch{ + Members: model.NewBool(false), + }, + }, + }, + PermissionsModeratedByPatch: map[string]*model.ChannelModeratedRoles{ + createPosts: { + Members: &model.ChannelModeratedRole{Value: false, Enabled: true}, + Guests: &model.ChannelModeratedRole{Value: false, Enabled: false}, + }, + createReactions: { + Guests: &model.ChannelModeratedRole{Value: false, Enabled: false}, + }, + channelMentions: { + Guests: &model.ChannelModeratedRole{Value: false, Enabled: false}, + }, + }, + HigherScopedGuestPermissions: []string{}, + ShouldError: false, + }, + { + Name: "Channel should have no scheme when all moderated permissions are equivalent to higher scoped role", + ChannelModerationsPatch: []*model.ChannelModerationPatch{ + { + Name: &createPosts, + Roles: &model.ChannelModeratedRolesPatch{ + Members: model.NewBool(true), + Guests: model.NewBool(true), + }, + }, + { + Name: &createReactions, + Roles: &model.ChannelModeratedRolesPatch{ + Members: model.NewBool(true), + Guests: model.NewBool(true), + }, + }, + { + Name: &channelMentions, + Roles: &model.ChannelModeratedRolesPatch{ + Members: model.NewBool(true), + Guests: model.NewBool(true), + }, + }, + { + Name: &manageMembers, + Roles: &model.ChannelModeratedRolesPatch{ + Members: model.NewBool(true), + }, + }, + }, + PermissionsModeratedByPatch: map[string]*model.ChannelModeratedRoles{}, + ShouldHaveNoChannelScheme: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + higherScopedPermissionsOverriden := tc.HigherScopedMemberPermissions != nil || tc.HigherScopedGuestPermissions != nil + // If the test case restricts higher scoped permissions. + if higherScopedPermissionsOverriden { + higherScopedGuestRoleName, higherScopedMemberRoleName, _, _ := th.App.GetTeamSchemeChannelRoles(channel.TeamId) + if tc.HigherScopedMemberPermissions != nil { + higherScopedMemberRole, err := th.App.GetRoleByName(higherScopedMemberRoleName) + require.Nil(t, err) + originalPermissions := higherScopedMemberRole.Permissions + + th.App.PatchRole(higherScopedMemberRole, &model.RolePatch{Permissions: &tc.HigherScopedMemberPermissions}) + defer th.App.PatchRole(higherScopedMemberRole, &model.RolePatch{Permissions: &originalPermissions}) + } + + if tc.HigherScopedGuestPermissions != nil { + higherScopedGuestRole, err := th.App.GetRoleByName(higherScopedGuestRoleName) + require.Nil(t, err) + originalPermissions := higherScopedGuestRole.Permissions + + th.App.PatchRole(higherScopedGuestRole, &model.RolePatch{Permissions: &tc.HigherScopedGuestPermissions}) + defer th.App.PatchRole(higherScopedGuestRole, &model.RolePatch{Permissions: &originalPermissions}) + } + } + + moderations, err := th.App.PatchChannelModerationsForChannel(channel, tc.ChannelModerationsPatch) + if tc.ShouldError { + require.Error(t, err) + return + } + require.Nil(t, err) + + updatedChannel, _ := th.App.GetChannel(channel.Id) + if tc.ShouldHaveNoChannelScheme { + require.Nil(t, updatedChannel.SchemeId) + } else { + require.NotNil(t, updatedChannel.SchemeId) + } + + for _, moderation := range moderations { + // If the permission is not found in the expected modified permissions table then require it to be true + if permission, found := tc.PermissionsModeratedByPatch[moderation.Name]; found && permission.Members != nil { + require.Equal(t, moderation.Roles.Members.Value, permission.Members.Value) + require.Equal(t, moderation.Roles.Members.Enabled, permission.Members.Enabled) + } else { + require.Equal(t, moderation.Roles.Members.Value, true) + require.Equal(t, moderation.Roles.Members.Enabled, true) + } + + if permission, found := tc.PermissionsModeratedByPatch[moderation.Name]; found && permission.Guests != nil { + require.Equal(t, moderation.Roles.Guests.Value, permission.Guests.Value) + require.Equal(t, moderation.Roles.Guests.Enabled, permission.Guests.Enabled) + } else if moderation.Name == manageMembers { + require.Empty(t, moderation.Roles.Guests) + } else { + require.Equal(t, moderation.Roles.Guests.Value, true) + require.Equal(t, moderation.Roles.Guests.Enabled, true) + } + } + + if tc.RevertChannelModerationsPatch != nil { + th.App.PatchChannelModerationsForChannel(channel, tc.RevertChannelModerationsPatch) + } + }) + } +} diff --git a/i18n/en.json b/i18n/en.json index 91944b2404..63d6bfe240 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -299,6 +299,10 @@ "id": "api.channel.delete_channel.type.invalid", "translation": "Unable to delete direct or group message channels" }, + { + "id": "api.channel.get_channel_moderations.license.error", + "translation": "Your license does not support channel moderation" + }, { "id": "api.channel.guest_join_channel.post_and_forget", "translation": "%v joined the channel as guest." @@ -327,6 +331,10 @@ "id": "api.channel.leave.left", "translation": "%v left the channel." }, + { + "id": "api.channel.patch_channel_moderations.license.error", + "translation": "Your license does not support channel moderation" + }, { "id": "api.channel.patch_update_channel.forbidden.app_error", "translation": "Failed to update the channel." diff --git a/model/channel.go b/model/channel.go index 62b0761185..76421789b1 100644 --- a/model/channel.go +++ b/model/channel.go @@ -84,6 +84,31 @@ type DirectChannelForExport struct { Members *[]string } +type ChannelModeration struct { + Name string `json:"name"` + Roles *ChannelModeratedRoles `json:"roles"` +} + +type ChannelModeratedRoles struct { + Guests *ChannelModeratedRole `json:"guests"` + Members *ChannelModeratedRole `json:"members"` +} + +type ChannelModeratedRole struct { + Value bool `json:"value"` + Enabled bool `json:"enabled"` +} + +type ChannelModerationPatch struct { + Name *string `json:"name"` + Roles *ChannelModeratedRolesPatch `json:"roles"` +} + +type ChannelModeratedRolesPatch struct { + Guests *bool `json:"guests"` + Members *bool `json:"members"` +} + // ChannelSearchOpts contains options for searching channels. // // NotAssociatedToGroup will exclude channels that have associated, active GroupChannels records. @@ -144,6 +169,18 @@ func ChannelPatchFromJson(data io.Reader) *ChannelPatch { return o } +func ChannelModerationsFromJson(data io.Reader) []*ChannelModeration { + var o []*ChannelModeration + json.NewDecoder(data).Decode(&o) + return o +} + +func ChannelModerationsPatchFromJson(data io.Reader) []*ChannelModerationPatch { + var o []*ChannelModerationPatch + json.NewDecoder(data).Decode(&o) + return o +} + func (o *Channel) Etag() string { return Etag(o.Id, o.UpdateAt) } diff --git a/model/client4.go b/model/client4.go index 236eed9268..47a78b0d08 100644 --- a/model/client4.go +++ b/model/client4.go @@ -4912,3 +4912,22 @@ func (c *Client4) PatchConfig(config *Config) (*Config, *Response) { defer closeBody(r) return ConfigFromJson(r.Body), BuildResponse(r) } + +func (c *Client4) GetChannelModerations(channelID string, etag string) ([]*ChannelModeration, *Response) { + r, err := c.DoApiGet(c.GetChannelRoute(channelID)+"/moderations", etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ChannelModerationsFromJson(r.Body), BuildResponse(r) +} + +func (c *Client4) PatchChannelModerations(channelID string, patch []*ChannelModerationPatch) ([]*ChannelModeration, *Response) { + payload, _ := json.Marshal(patch) + r, err := c.DoApiPut(c.GetChannelRoute(channelID)+"/moderations/patch", string(payload)) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ChannelModerationsFromJson(r.Body), BuildResponse(r) +} diff --git a/model/permission.go b/model/permission.go index 4871e9399a..e5bf106325 100644 --- a/model/permission.go +++ b/model/permission.go @@ -99,6 +99,9 @@ var PERMISSION_MANAGE_SYSTEM *Permission var ALL_PERMISSIONS []*Permission +var CHANNEL_MODERATED_PERMISSIONS []string +var CHANNEL_MODERATED_PERMISSIONS_MAP map[string]string + func initializePermissions() { PERMISSION_INVITE_USER = &Permission{ "invite_user", @@ -641,6 +644,22 @@ func initializePermissions() { PERMISSION_DEMOTE_TO_GUEST, PERMISSION_USE_CHANNEL_MENTIONS, } + + CHANNEL_MODERATED_PERMISSIONS = []string{ + PERMISSION_CREATE_POST.Id, + "create_reactions", + "manage_members", + PERMISSION_USE_CHANNEL_MENTIONS.Id, + } + + CHANNEL_MODERATED_PERMISSIONS_MAP = map[string]string{ + PERMISSION_CREATE_POST.Id: CHANNEL_MODERATED_PERMISSIONS[0], + PERMISSION_ADD_REACTION.Id: CHANNEL_MODERATED_PERMISSIONS[1], + PERMISSION_REMOVE_REACTION.Id: CHANNEL_MODERATED_PERMISSIONS[1], + PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS.Id: CHANNEL_MODERATED_PERMISSIONS[2], + PERMISSION_MANAGE_PRIVATE_CHANNEL_MEMBERS.Id: CHANNEL_MODERATED_PERMISSIONS[2], + PERMISSION_USE_CHANNEL_MENTIONS.Id: CHANNEL_MODERATED_PERMISSIONS[3], + } } func init() { diff --git a/model/role.go b/model/role.go index 6423ef52e5..4eda3809ff 100644 --- a/model/role.go +++ b/model/role.go @@ -123,6 +123,115 @@ func PermissionsChangedByPatch(role *Role, patch *RolePatch) []string { return result } +func ChannelModeratedPermissionsChangedByPatch(role *Role, patch *RolePatch) []string { + var result []string + + if patch.Permissions == nil { + return result + } + + roleMap := make(map[string]bool) + patchMap := make(map[string]bool) + + for _, permission := range role.Permissions { + if channelModeratedPermissionName, found := CHANNEL_MODERATED_PERMISSIONS_MAP[permission]; found { + roleMap[channelModeratedPermissionName] = true + } + } + + for _, permission := range *patch.Permissions { + if channelModeratedPermissionName, found := CHANNEL_MODERATED_PERMISSIONS_MAP[permission]; found { + patchMap[channelModeratedPermissionName] = true + } + } + + for permissionKey := range roleMap { + if !patchMap[permissionKey] { + result = append(result, permissionKey) + } + } + + for permissionKey := range patchMap { + if !roleMap[permissionKey] { + result = append(result, permissionKey) + } + } + + return result +} + +// GetChannelModeratedPermissions returns a map of channel moderated permissions that the role has access to +func (r *Role) GetChannelModeratedPermissions() map[string]bool { + moderatedPermissions := make(map[string]bool) + for _, permission := range r.Permissions { + if _, found := CHANNEL_MODERATED_PERMISSIONS_MAP[permission]; !found { + continue + } + + for moderated, moderatedPermissionValue := range CHANNEL_MODERATED_PERMISSIONS_MAP { + if moderated == permission { + moderatedPermissions[moderatedPermissionValue] = true + } + } + } + + return moderatedPermissions +} + +// RolePatchFromChannelModerationsPatch Creates and returns a RolePatch based on a slice of ChannelModerationPatchs, roleName is expected to be either "members" or "guests". +func (r *Role) RolePatchFromChannelModerationsPatch(channelModerationsPatch []*ChannelModerationPatch, roleName string) *RolePatch { + permissionsToAddToPatch := make(map[string]bool) + + // Iterate through the list of existing permissions on the role and append permissions that we want to keep. + for _, permission := range r.Permissions { + // Permission is not moderated so dont add it to the patch and skip the channelModerationsPatch + if _, isModerated := CHANNEL_MODERATED_PERMISSIONS_MAP[permission]; !isModerated { + continue + } + + permissionEnabled := true + // Check if permission has a matching moderated permission name inside the channel moderation patch + for _, channelModerationPatch := range channelModerationsPatch { + if *channelModerationPatch.Name == CHANNEL_MODERATED_PERMISSIONS_MAP[permission] { + // Permission key exists in patch with a value of false so skip over it + if roleName == "members" { + if channelModerationPatch.Roles.Members != nil && !*channelModerationPatch.Roles.Members { + permissionEnabled = false + } + } else if roleName == "guests" { + if channelModerationPatch.Roles.Guests != nil && !*channelModerationPatch.Roles.Guests { + permissionEnabled = false + } + } + } + } + + if permissionEnabled { + permissionsToAddToPatch[permission] = true + } + } + + // Iterate through the patch and add any permissions that dont already exist on the role + for _, channelModerationPatch := range channelModerationsPatch { + for permission, moderatedPermissionName := range CHANNEL_MODERATED_PERMISSIONS_MAP { + if roleName == "members" && channelModerationPatch.Roles.Members != nil && *channelModerationPatch.Roles.Members && *channelModerationPatch.Name == moderatedPermissionName { + permissionsToAddToPatch[permission] = true + } + + if roleName == "guests" && channelModerationPatch.Roles.Guests != nil && *channelModerationPatch.Roles.Guests && *channelModerationPatch.Name == moderatedPermissionName { + permissionsToAddToPatch[permission] = true + } + } + } + + patchPermissions := make([]string, 0, len(permissionsToAddToPatch)) + for permission := range permissionsToAddToPatch { + patchPermissions = append(patchPermissions, permission) + } + + return &RolePatch{Permissions: &patchPermissions} +} + func (r *Role) IsValid() bool { if len(r.Id) != 26 { return false diff --git a/model/role_test.go b/model/role_test.go new file mode 100644 index 0000000000..8b28cfe9c6 --- /dev/null +++ b/model/role_test.go @@ -0,0 +1,273 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestChannelModeratedPermissionsChangedByPatch(t *testing.T) { + testCases := []struct { + Name string + Permissions []string + PatchPermissions []string + Expected []string + }{ + { + "Empty patch returns empty slice", + []string{}, + []string{}, + []string{}, + }, + { + "Adds permissions to empty initial permissions list", + []string{}, + []string{PERMISSION_CREATE_POST.Id, PERMISSION_ADD_REACTION.Id}, + []string{CHANNEL_MODERATED_PERMISSIONS[0], CHANNEL_MODERATED_PERMISSIONS[1]}, + }, + { + "Ignores non moderated permissions in initial permissions list", + []string{PERMISSION_ASSIGN_BOT.Id}, + []string{PERMISSION_CREATE_POST.Id, PERMISSION_REMOVE_REACTION.Id}, + []string{CHANNEL_MODERATED_PERMISSIONS[0], CHANNEL_MODERATED_PERMISSIONS[1]}, + }, + { + "Adds removed moderated permissions from initial permissions list", + []string{PERMISSION_CREATE_POST.Id}, + []string{}, + []string{PERMISSION_CREATE_POST.Id}, + }, + { + "No changes returns empty slice", + []string{PERMISSION_CREATE_POST.Id, PERMISSION_ASSIGN_BOT.Id}, + []string{PERMISSION_CREATE_POST.Id}, + []string{}, + }, + } + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + baseRole := &Role{Permissions: tc.Permissions} + rolePatch := &RolePatch{Permissions: &tc.PatchPermissions} + result := ChannelModeratedPermissionsChangedByPatch(baseRole, rolePatch) + assert.ElementsMatch(t, tc.Expected, result) + }) + } +} + +func TestRolePatchFromChannelModerationsPatch(t *testing.T) { + createPosts := CHANNEL_MODERATED_PERMISSIONS[0] + createReactions := CHANNEL_MODERATED_PERMISSIONS[1] + manageMembers := CHANNEL_MODERATED_PERMISSIONS[2] + channelMentions := CHANNEL_MODERATED_PERMISSIONS[3] + + basePermissions := []string{ + PERMISSION_ADD_REACTION.Id, + PERMISSION_REMOVE_REACTION.Id, + PERMISSION_CREATE_POST.Id, + PERMISSION_USE_CHANNEL_MENTIONS.Id, + PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS.Id, + PERMISSION_UPLOAD_FILE.Id, + PERMISSION_GET_PUBLIC_LINK.Id, + PERMISSION_USE_SLASH_COMMANDS.Id, + } + + baseModeratedPermissions := []string{ + PERMISSION_ADD_REACTION.Id, + PERMISSION_REMOVE_REACTION.Id, + PERMISSION_CREATE_POST.Id, + PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS.Id, + PERMISSION_USE_CHANNEL_MENTIONS.Id, + } + + testCases := []struct { + Name string + Permissions []string + ChannelModerationsPatch []*ChannelModerationPatch + RoleName string + ExpectedPatchPermissions []string + }{ + { + "Patch to member role adding a permission that already exists", + basePermissions, + []*ChannelModerationPatch{ + { + Name: &createReactions, + Roles: &ChannelModeratedRolesPatch{Members: NewBool(true)}, + }, + }, + "members", + baseModeratedPermissions, + }, + { + "Patch to member role with moderation patch for guest role", + basePermissions, + []*ChannelModerationPatch{ + { + Name: &createReactions, + Roles: &ChannelModeratedRolesPatch{Guests: NewBool(true)}, + }, + }, + "members", + baseModeratedPermissions, + }, + { + "Patch to guest role with moderation patch for member role", + basePermissions, + []*ChannelModerationPatch{ + { + Name: &createReactions, + Roles: &ChannelModeratedRolesPatch{Members: NewBool(true)}, + }, + }, + "guests", + baseModeratedPermissions, + }, + { + "Patch to member role removing multiple channel moderated permissions", + basePermissions, + []*ChannelModerationPatch{ + { + Name: &createReactions, + Roles: &ChannelModeratedRolesPatch{Members: NewBool(false)}, + }, + { + Name: &manageMembers, + Roles: &ChannelModeratedRolesPatch{Members: NewBool(false)}, + }, + { + Name: &channelMentions, + Roles: &ChannelModeratedRolesPatch{Members: NewBool(false)}, + }, + }, + "members", + []string{PERMISSION_CREATE_POST.Id}, + }, + { + "Patch to guest role removing multiple channel moderated permissions", + basePermissions, + []*ChannelModerationPatch{ + { + Name: &createReactions, + Roles: &ChannelModeratedRolesPatch{Guests: NewBool(false)}, + }, + { + Name: &manageMembers, + Roles: &ChannelModeratedRolesPatch{Guests: NewBool(false)}, + }, + { + Name: &channelMentions, + Roles: &ChannelModeratedRolesPatch{Guests: NewBool(false)}, + }, + }, + "guests", + []string{PERMISSION_CREATE_POST.Id}, + }, + { + "Patch enabling and removing multiple channel moderated permissions ", + []string{PERMISSION_ADD_REACTION.Id, PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS.Id}, + []*ChannelModerationPatch{ + { + Name: &createReactions, + Roles: &ChannelModeratedRolesPatch{Members: NewBool(false)}, + }, + { + Name: &manageMembers, + Roles: &ChannelModeratedRolesPatch{Members: NewBool(false)}, + }, + { + Name: &channelMentions, + Roles: &ChannelModeratedRolesPatch{Members: NewBool(true)}, + }, + { + Name: &createPosts, + Roles: &ChannelModeratedRolesPatch{Members: NewBool(true)}, + }, + }, + "members", + []string{PERMISSION_CREATE_POST.Id, PERMISSION_USE_CHANNEL_MENTIONS.Id}, + }, + { + "Patch enabling a partially enabled permission", + []string{PERMISSION_ADD_REACTION.Id}, + []*ChannelModerationPatch{ + { + Name: &createReactions, + Roles: &ChannelModeratedRolesPatch{Members: NewBool(true)}, + }, + }, + "members", + []string{PERMISSION_ADD_REACTION.Id, PERMISSION_REMOVE_REACTION.Id}, + }, + { + "Patch disabling a partially disabled permission", + []string{PERMISSION_ADD_REACTION.Id}, + []*ChannelModerationPatch{ + { + Name: &createReactions, + Roles: &ChannelModeratedRolesPatch{Members: NewBool(false)}, + }, + { + Name: &createPosts, + Roles: &ChannelModeratedRolesPatch{Members: NewBool(true)}, + }, + }, + "members", + []string{PERMISSION_CREATE_POST.Id}, + }, + } + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + baseRole := &Role{Permissions: tc.Permissions} + rolePatch := baseRole.RolePatchFromChannelModerationsPatch(tc.ChannelModerationsPatch, tc.RoleName) + assert.ElementsMatch(t, tc.ExpectedPatchPermissions, *rolePatch.Permissions) + }) + } +} + +func TestGetChannelModeratedPermissions(t *testing.T) { + tests := []struct { + Name string + Permissions []string + Expected map[string]bool + }{ + { + "Filters non moderated permissions", + []string{PERMISSION_CREATE_BOT.Id}, + 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}, + map[string]bool{ + CHANNEL_MODERATED_PERMISSIONS[0]: true, + CHANNEL_MODERATED_PERMISSIONS[1]: true, + CHANNEL_MODERATED_PERMISSIONS[2]: true, + CHANNEL_MODERATED_PERMISSIONS[3]: true, + }, + }, + { + "Returns a map of moderated permissions when non moderated present", + []string{PERMISSION_CREATE_POST.Id, PERMISSION_CREATE_DIRECT_CHANNEL.Id}, + map[string]bool{ + CHANNEL_MODERATED_PERMISSIONS[0]: true, + }, + }, + { + "Returns a nothing when no permissions present", + []string{}, + map[string]bool{}, + }, + } + for _, tc := range tests { + t.Run(tc.Name, func(t *testing.T) { + role := &Role{Permissions: tc.Permissions} + moderatedPermissions := role.GetChannelModeratedPermissions() + for permission := range moderatedPermissions { + assert.Equal(t, moderatedPermissions[permission], tc.Expected[permission]) + } + }) + } +}