Files
mattermost/app/authorization.go
mkraft 7fa6b321ae Custom groups (#18839)
* WIP

* adding initial creategroup endpoint

* fetching by group source

* fixing startup error

* updating create endpoint to take an array of user_ids, this will allow us to create the group with one request

* adding delete group endpoint and appropriate test

* adding source param for getGroups

* adding add members and delete members endpoints

* locking down crud endpoints to only be allowed for custom groups

* user search stuff

* allowing remoteid be null by changing field to pointer

* code cleanup and store level tests

* adding new tests and removing unused endpoint

* resolving conflicts

* Adds authz check for group.

* Adds authz checks to groups APIs.

* Updated create group authz tests.

* Updates delete group tests.

* Tests create group.

* Adds some tests and validations.

* adding new parameter so I can get users not in a group

* Fixed all lint warnings.

* Fix type.

* fixing search users not in group

* Fixes some lint errors.

* Moves entry in JSON array.

* Fixed SQL query.

* Fixes permission migration test.

* Fixes migration test.

* Fixes some group store tests.

* Fix test.

* Fix test.

* Revert lint change.

* Migrated CreateWithUserIds to sqlx.

* Adds tests for GetMember; migrates implementation to sqlx.

* Tests GetNonMemberUsersPage and hanles wrong group id.

* Fixes test.

* Switches GetMaster to GetMasterX.

* Switches GetReplica to GetReplicaX.

* Fixes logic.

* Fixes shadow declaration.

* Adds include_member_count to get group API endpoint.

* Adds filter_has_member param to getGroups.

* Fixes.

* Removes array of group sources.

* fixing error

* Testing reverting CreateWithUserIds back to gorp.

* Added websocket event for CreateGroupWithUserIds.

* Changed a few response status codes. Switched to correct permission.

* Added member count to ws payload for group when updating or creating.

* Adds feature flag checks for custom groups.

* Added middleware function to require license. Added config to disable custom groups.

* Change for function signature change of executePossiblyEmptyQuery.

* Lint fixes.

* Adds telemetry none comment.

* Adds translations.

* Migrated to sqlx.

* Temp. removal of translation.

* Fixed typo.

* Added an intermediary model to query with a field that is now ignored by sqlx on read queries.

* Re-used existing store struct.

* Inludes member count.

* Fix for merge error.'

* Require license for group endpoints.

* Updates translations.

* Fix shadow declaration.

* Renames permissions. Switches to new method to retrieve remoteid.

* Added WS events for upsert and delete member(s).

* Added new store error type ErrUniqueConstraint.

* Added EnableCustonGroups to the client config.

* Sanitized some user records.

* Added parameter to include_total_count for listing groups.

* Added translations.

* adding deleteAt field to getByUsers query

* Revert sanitize.

* Added uniqueness constraint error to UpdateGroup.

* Removed the FutureFeatures flag so that the feature is not enabled on old Enterprise licenses.

* Renamed function.

* Updates authz check for user search related to groups.

* Removed debug statement.

* Removed unused app method.

* Added telemetry for enable_custom_groups.

* Returns early from nil license.

* Updates test.

* Returned early to avoid nesting in (*SqlGroupStore).checkUserExist. Switched to reading from replica in (*SqlGroupStore).GetMember. Handled JSON marshal error in (*Client4).UpsertGroupMembers

* Switched to SanitizeProfile.

* Switched to model.NewInt.

* Switched from status NotImplemented to Forbidden for missing license.

* Removed deactivated users from 'exists' set.

* Revert gotool update.

* Ignored lint error that I think is invalid.

* Added the approprate access tag for disabling custom groups.

* Revert change to response status.

* Fixed refactor mistake.

* Limited the group member WS events to individual users.

* Removed WS event of deleted groups.

* Updated license check for searchUsers endpoint.

* Switched from license feature to license sku.

* Update app/group.go

Co-authored-by: Claudio Costa <cstcld91@gmail.com>

* Update app/group.go

Co-authored-by: Claudio Costa <cstcld91@gmail.com>

* Remove linter ignore comment.

* Added function to create sku-specific license.

* Fixed typo. Removed comment.

* Fixed for wrong type.

* Added missing param to client. Removed unnecessary props setting. Added test for retrieving groups by source.

* Updated some tests now that we're validating group membership not created for deactivated user.

* Fix for groups endpoint returning all group types by default.

* Changes constant names. Adds migration for all users to manage custom group members.

* Removes requirement for manage_system permission to filter user search by group.

* Added migration mock.

* Removes default permissions from custom_group_user role.

* Fixes migration.

* Fixes emoji migration test.

* fixing issue with member counts

* fixing search issue for deleted members

Co-authored-by: Benjamin Cooke <benjamincooke@Benjamins-MacBook-Pro.local>
Co-authored-by: Benjamin Cooke <benjamincooke@Benjamins-MBP.ht.home>
Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Co-authored-by: Benjamin Cooke <benjamincooke@Benjamins-MacBook-Pro.fritz.box>
Co-authored-by: Claudio Costa <cstcld91@gmail.com>
2022-02-17 12:34:39 -05:00

306 lines
9.1 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"context"
"database/sql"
"errors"
"net/http"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/shared/mlog"
)
func (a *App) MakePermissionError(s *model.Session, permissions []*model.Permission) *model.AppError {
permissionsStr := "permission="
for _, permission := range permissions {
permissionsStr += permission.Id
permissionsStr += ","
}
return model.NewAppError("Permissions", "api.context.permissions.app_error", nil, "userId="+s.UserId+", "+permissionsStr, http.StatusForbidden)
}
func (a *App) SessionHasPermissionTo(session model.Session, permission *model.Permission) bool {
if session.IsUnrestricted() {
return true
}
return a.RolesGrantPermission(session.GetUserRoles(), permission.Id)
}
func (a *App) SessionHasPermissionToAny(session model.Session, permissions []*model.Permission) bool {
for _, perm := range permissions {
if a.SessionHasPermissionTo(session, perm) {
return true
}
}
return false
}
func (a *App) SessionHasPermissionToTeam(session model.Session, teamID string, permission *model.Permission) bool {
if teamID == "" {
return false
}
if session.IsUnrestricted() {
return true
}
teamMember := session.GetTeamByTeamId(teamID)
if teamMember != nil {
if a.RolesGrantPermission(teamMember.GetRoles(), permission.Id) {
return true
}
}
return a.RolesGrantPermission(session.GetUserRoles(), permission.Id)
}
func (a *App) SessionHasPermissionToChannel(session model.Session, channelID string, permission *model.Permission) bool {
if channelID == "" {
return false
}
ids, err := a.Srv().Store.Channel().GetAllChannelMembersForUser(session.UserId, true, true)
var channelRoles []string
if err == nil {
if roles, ok := ids[channelID]; ok {
channelRoles = strings.Fields(roles)
if a.RolesGrantPermission(channelRoles, permission.Id) {
return true
}
}
}
channel, appErr := a.GetChannel(channelID)
if appErr != nil && appErr.StatusCode == http.StatusNotFound {
return false
}
if session.IsUnrestricted() {
return true
}
if appErr == nil && channel.TeamId != "" {
return a.SessionHasPermissionToTeam(session, channel.TeamId, permission)
}
return a.SessionHasPermissionTo(session, permission)
}
func (a *App) SessionHasPermissionToGroup(session model.Session, groupID string, permission *model.Permission) bool {
groupMember, err := a.Srv().Store.Group().GetMember(groupID, session.UserId)
// don't reject immediately on ErrNoRows error because there's further authz logic below for non-groupmembers
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return false
}
// each member of a group is implicitly considered to have the 'custom_group_user' role in that group, so if the user is a member of the
// group and custom_group_user on their system has the requested permission then return true
if groupMember != nil && a.RolesGrantPermission([]string{model.CustomGroupUserRoleId}, permission.Id) {
return true
}
// Not implemented: group-override schemes.
// ...otherwise check their system roles to see if they have the requested permission system-wide
return a.SessionHasPermissionTo(session, permission)
}
func (a *App) SessionHasPermissionToChannelByPost(session model.Session, postID string, permission *model.Permission) bool {
if channelMember, err := a.Srv().Store.Channel().GetMemberForPost(postID, session.UserId); err == nil {
if a.RolesGrantPermission(channelMember.GetRoles(), permission.Id) {
return true
}
}
if channel, err := a.Srv().Store.Channel().GetForPost(postID); err == nil {
if channel.TeamId != "" {
return a.SessionHasPermissionToTeam(session, channel.TeamId, permission)
}
}
return a.SessionHasPermissionTo(session, permission)
}
func (a *App) SessionHasPermissionToCategory(session model.Session, userID, teamID, categoryId string) bool {
if a.SessionHasPermissionTo(session, model.PermissionEditOtherUsers) {
return true
}
category, err := a.GetSidebarCategory(categoryId)
return err == nil && category != nil && category.UserId == session.UserId && category.UserId == userID && category.TeamId == teamID
}
func (a *App) SessionHasPermissionToUser(session model.Session, userID string) bool {
if userID == "" {
return false
}
if session.IsUnrestricted() {
return true
}
if session.UserId == userID {
return true
}
if a.SessionHasPermissionTo(session, model.PermissionEditOtherUsers) {
return true
}
return false
}
func (a *App) SessionHasPermissionToUserOrBot(session model.Session, userID string) bool {
if session.IsUnrestricted() {
return true
}
if a.SessionHasPermissionToUser(session, userID) {
return true
}
if err := a.SessionHasPermissionToManageBot(session, userID); err == nil {
return true
}
return false
}
func (a *App) HasPermissionTo(askingUserId string, permission *model.Permission) bool {
user, err := a.GetUser(askingUserId)
if err != nil {
return false
}
roles := user.GetRoles()
return a.RolesGrantPermission(roles, permission.Id)
}
func (a *App) HasPermissionToTeam(askingUserId string, teamID string, permission *model.Permission) bool {
if teamID == "" || askingUserId == "" {
return false
}
teamMember, _ := a.GetTeamMember(teamID, askingUserId)
if teamMember != nil && teamMember.DeleteAt == 0 {
if a.RolesGrantPermission(teamMember.GetRoles(), permission.Id) {
return true
}
}
return a.HasPermissionTo(askingUserId, permission)
}
func (a *App) HasPermissionToChannel(askingUserId string, channelID string, permission *model.Permission) bool {
if channelID == "" || askingUserId == "" {
return false
}
channelMember, err := a.GetChannelMember(context.Background(), channelID, askingUserId)
if err == nil {
roles := channelMember.GetRoles()
if a.RolesGrantPermission(roles, permission.Id) {
return true
}
}
var channel *model.Channel
channel, err = a.GetChannel(channelID)
if err == nil {
return a.HasPermissionToTeam(askingUserId, channel.TeamId, permission)
}
return a.HasPermissionTo(askingUserId, permission)
}
func (a *App) HasPermissionToChannelByPost(askingUserId string, postID string, permission *model.Permission) bool {
if channelMember, err := a.Srv().Store.Channel().GetMemberForPost(postID, askingUserId); err == nil {
if a.RolesGrantPermission(channelMember.GetRoles(), permission.Id) {
return true
}
}
if channel, err := a.Srv().Store.Channel().GetForPost(postID); err == nil {
return a.HasPermissionToTeam(askingUserId, channel.TeamId, permission)
}
return a.HasPermissionTo(askingUserId, permission)
}
func (a *App) HasPermissionToUser(askingUserId string, userID string) bool {
if askingUserId == userID {
return true
}
if a.HasPermissionTo(askingUserId, model.PermissionEditOtherUsers) {
return true
}
return false
}
func (a *App) RolesGrantPermission(roleNames []string, permissionId string) bool {
roles, err := a.GetRolesByNames(roleNames)
if err != nil {
// This should only happen if something is very broken. We can't realistically
// recover the situation, so deny permission and log an error.
mlog.Error("Failed to get roles from database with role names: "+strings.Join(roleNames, ",")+" ", mlog.Err(err))
return false
}
for _, role := range roles {
if role.DeleteAt != 0 {
continue
}
permissions := role.Permissions
for _, permission := range permissions {
if permission == permissionId {
return true
}
}
}
return false
}
// SessionHasPermissionToManageBot returns nil if the session has access to manage the given bot.
// This function deviates from other authorization checks in returning an error instead of just
// a boolean, allowing the permission failure to be exposed with more granularity.
func (a *App) SessionHasPermissionToManageBot(session model.Session, botUserId string) *model.AppError {
existingBot, err := a.GetBot(botUserId, true)
if err != nil {
return err
}
if session.IsUnrestricted() {
return nil
}
if existingBot.OwnerId == session.UserId {
if !a.SessionHasPermissionTo(session, model.PermissionManageBots) {
if !a.SessionHasPermissionTo(session, model.PermissionReadBots) {
// If the user doesn't have permission to read bots, pretend as if
// the bot doesn't exist at all.
return model.MakeBotNotFoundError(botUserId)
}
return a.MakePermissionError(&session, []*model.Permission{model.PermissionManageBots})
}
} else {
if !a.SessionHasPermissionTo(session, model.PermissionManageOthersBots) {
if !a.SessionHasPermissionTo(session, model.PermissionReadOthersBots) {
// If the user doesn't have permission to read others' bots,
// pretend as if the bot doesn't exist at all.
return model.MakeBotNotFoundError(botUserId)
}
return a.MakePermissionError(&session, []*model.Permission{model.PermissionManageOthersBots})
}
}
return nil
}
func (a *App) HasPermissionToReadChannel(userID string, channel *model.Channel) bool {
return a.HasPermissionToChannel(userID, channel.Id, model.PermissionReadChannel) || (channel.Type == model.ChannelTypeOpen && a.HasPermissionToTeam(userID, channel.TeamId, model.PermissionReadPublicChannel))
}