mirror of
https://github.com/hanzoai/team.git
synced 2026-09-30 01:57:35 +00:00
* MM-68952: Resolve public channel mentions for non-members under Compliance Channel mention name resolution reused HasPermissionToReadChannel, a content-read check that returns false for non-members of a public channel when Compliance Monitoring is enabled (MM-45272) or when the channel is on another team (MM-66791). As a result, the channel_mentions post prop was stripped per-viewer (since #34235), and the webapp fell back to rendering the raw (anonymized) channel slug instead of a clickable link. Introduce HasPermissionToResolveChannelMention, which exposes only a public channel display name and link (not content) and is therefore independent of ComplianceSettings, while still requiring team membership for public channels (blocks cross-team disclosure) and channel membership for private/DM/GM channels. Switch the three mention call sites (FillInPostProps, sanitizeChannelMentionsForUser, channelMentionsBroadcastHook) to the new helper. HasPermissionToReadChannel and all content-read paths are unchanged. * MM-68952: Add author-side and E2E coverage for channel mention resolution Add a Go test (TestFillInPostPropsChannelMentionResolution) that exercises the author-side persistence of the channel_mentions prop in FillInPostProps. It locks in the new behavior: an author who is a team member but not a member of a referenced public channel now persists the mention prop even when Compliance Monitoring is enabled, while public channels on other teams and private channels the author is not in are still dropped. Add a Playwright spec (channel_mention_resolution.spec.ts) with a license-free cross-team case (a public channel mention stays unresolved for a viewer not on the channel's team) and a license-gated case (with Compliance enabled, a team member who is not in the channel sees the resolved mention link). * MM-68952: Tighten channel mention test assertions and fix lint Use strings.Builder when assembling the test message to avoid the golangci-lint stringsbuilder (modernize) warning about string += string in a loop. Tighten the cross-team public and private non-member cases to assert.Nil on the resolved mentions so the contract requires no persisted channel_mentions map rather than merely an empty one. --------- Co-authored-by: Hanzo Dev <[email protected]>
791 lines
28 KiB
Go
791 lines
28 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package app
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
"net/http"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
|
"github.com/mattermost/mattermost/server/public/shared/request"
|
|
)
|
|
|
|
func (a *App) SessionHasPermissionTo(session model.Session, permission *model.Permission) bool {
|
|
if session.IsUnrestricted() {
|
|
return true
|
|
}
|
|
return a.RolesGrantPermission(session.GetUserRoles(), permission.Id)
|
|
}
|
|
|
|
// SessionHasPermissionToAndNotRestrictedAdmin is a variant of [App.SessionHasPermissionTo] that
|
|
// denies access to restricted system admins. Note that a local session is always unrestricted.
|
|
func (a *App) SessionHasPermissionToAndNotRestrictedAdmin(session model.Session, permission *model.Permission) bool {
|
|
if session.IsUnrestricted() {
|
|
return true
|
|
}
|
|
|
|
if *a.Config().ExperimentalSettings.RestrictSystemAdmin {
|
|
return false
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
// SessionHasPermissionToTeams returns true only if user has access to all teams.
|
|
func (a *App) SessionHasPermissionToTeams(rctx request.CTX, session model.Session, teamIDs []string, permission *model.Permission) bool {
|
|
if len(teamIDs) == 0 {
|
|
return true
|
|
}
|
|
|
|
if slices.Contains(teamIDs, "") {
|
|
return false
|
|
}
|
|
|
|
// Check session permission, if it allows access, no need to check teams.
|
|
if a.SessionHasPermissionTo(session, permission) {
|
|
return true
|
|
}
|
|
for _, teamID := range teamIDs {
|
|
tm := session.GetTeamByTeamId(teamID)
|
|
if tm != nil {
|
|
// If a team member has permission, then no need to check further.
|
|
if a.RolesGrantPermission(tm.GetRoles(), permission.Id) {
|
|
continue
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// SessionHasPermissionToChannel checks if the session has permission to the given channel.
|
|
//
|
|
// Returns:
|
|
//
|
|
// (hasPermission, isMember)
|
|
//
|
|
// hasPermission: true if the user has the specified permission for the channel, otherwise false.
|
|
// isMember: used for auditing access without membership. True if the user is a member of the channel, otherwise false.
|
|
func (a *App) SessionHasPermissionToChannel(rctx request.CTX, session model.Session, channelID string, permission *model.Permission) (hasPermission bool, isMember bool) {
|
|
if channelID == "" {
|
|
return false, false
|
|
}
|
|
|
|
channel, appErr := a.GetChannel(rctx, channelID)
|
|
if appErr != nil && appErr.StatusCode == http.StatusNotFound {
|
|
return false, false
|
|
} else if appErr != nil {
|
|
rctx.Logger().Warn("Failed to get channel", mlog.String("channel_id", channelID), mlog.Err(appErr))
|
|
return false, false
|
|
}
|
|
|
|
if session.IsUnrestricted() {
|
|
return true, false
|
|
}
|
|
|
|
isMember = false
|
|
ids, err := a.Srv().Store().Channel().GetAllChannelMembersForUser(rctx, session.UserId, true, true)
|
|
var channelRoles []string
|
|
if err == nil {
|
|
if roles, ok := ids[channelID]; ok {
|
|
isMember = true
|
|
channelRoles = strings.Fields(roles)
|
|
if a.RolesGrantPermission(channelRoles, permission.Id) {
|
|
return true, isMember
|
|
}
|
|
}
|
|
}
|
|
|
|
if a.RolesGrantPermission(session.GetUserRoles(), model.PermissionManageSystem.Id) {
|
|
return true, isMember
|
|
}
|
|
|
|
if channel.TeamId != "" {
|
|
return a.SessionHasPermissionToTeam(session, channel.TeamId, permission), isMember
|
|
}
|
|
|
|
return a.SessionHasPermissionTo(session, permission), isMember
|
|
}
|
|
|
|
// SessionHasPermissionToChannels returns true only if user has access to all channels.
|
|
func (a *App) SessionHasPermissionToChannels(rctx request.CTX, session model.Session, channelIDs []string, permission *model.Permission) bool {
|
|
if len(channelIDs) == 0 {
|
|
return true
|
|
}
|
|
|
|
if session.IsUnrestricted() || a.RolesGrantPermission(session.GetUserRoles(), model.PermissionManageSystem.Id) {
|
|
return true
|
|
}
|
|
|
|
// make sure all channels exist, otherwise return false.
|
|
for _, channelID := range channelIDs {
|
|
if channelID == "" {
|
|
return false
|
|
}
|
|
|
|
_, appErr := a.GetChannel(rctx, channelID)
|
|
if appErr != nil {
|
|
return false
|
|
}
|
|
}
|
|
|
|
// if System Roles (i.e. Admin, TeamAdmin) allow permissions
|
|
// if so, no reason to check team
|
|
if a.SessionHasPermissionTo(session, permission) {
|
|
return true
|
|
}
|
|
|
|
ids, err := a.Srv().Store().Channel().GetAllChannelMembersForUser(rctx, session.UserId, true, true)
|
|
var channelRoles []string
|
|
for _, channelID := range channelIDs {
|
|
if err == nil {
|
|
// If a channel member has permission, then no need to check further.
|
|
if roles, ok := ids[channelID]; ok {
|
|
channelRoles = strings.Fields(roles)
|
|
if a.RolesGrantPermission(channelRoles, permission.Id) {
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
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 postID == "" {
|
|
return false
|
|
}
|
|
|
|
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) SessionHasPermissionToReadPost(rctx request.CTX, session model.Session, postID string) (hasPErmission bool, isMember bool) {
|
|
if postID == "" {
|
|
return false, false
|
|
}
|
|
|
|
channel, err := a.Srv().Store().Channel().GetForPost(postID)
|
|
if err != nil {
|
|
// Original implementation (SessionHasPermissionToChannelByPost) still checks for
|
|
// general permissions even if the channel is not found, and some tests rely on this behavior.
|
|
return a.SessionHasPermissionTo(session, model.PermissionReadChannelContent), false
|
|
}
|
|
|
|
return a.SessionHasPermissionToReadChannel(rctx, session, channel)
|
|
}
|
|
|
|
func (a *App) SessionHasPermissionToCategory(rctx request.CTX, session model.Session, userID, teamID, categoryId string) bool {
|
|
if a.SessionHasPermissionTo(session, model.PermissionEditOtherUsers) {
|
|
return true
|
|
}
|
|
category, err := a.GetSidebarCategory(rctx, 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() || a.SessionHasPermissionTo(session, model.PermissionManageSystem) {
|
|
return true
|
|
}
|
|
|
|
if session.UserId == userID {
|
|
return true
|
|
}
|
|
|
|
if !a.SessionHasPermissionTo(session, model.PermissionEditOtherUsers) {
|
|
return false
|
|
}
|
|
|
|
user, err := a.GetUser(userID)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
if user.IsSystemAdmin() {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (a *App) SessionHasPermissionToUserOrBot(rctx request.CTX, session model.Session, userID string) bool {
|
|
if session.IsUnrestricted() {
|
|
return true
|
|
}
|
|
|
|
err := a.SessionHasPermissionToManageBot(rctx, session, userID)
|
|
if err == nil {
|
|
return true
|
|
}
|
|
if err.Id == "store.sql_bot.get.missing.app_error" && err.Where == "SqlBotStore.Get" {
|
|
if a.SessionHasPermissionToUser(session, userID) {
|
|
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(rctx request.CTX, askingUserId string, teamID string, permission *model.Permission) bool {
|
|
if teamID == "" || askingUserId == "" {
|
|
return false
|
|
}
|
|
teamMember, _ := a.GetTeamMember(rctx, teamID, askingUserId)
|
|
if teamMember != nil && teamMember.DeleteAt == 0 {
|
|
if a.RolesGrantPermission(teamMember.GetRoles(), permission.Id) {
|
|
return true
|
|
}
|
|
}
|
|
return a.HasPermissionTo(askingUserId, permission)
|
|
}
|
|
|
|
// HasPermissionToChannel determines if the specified user has the given permission on the provided channel.
|
|
//
|
|
// Returns:
|
|
//
|
|
// (hasPermission, isMember)
|
|
//
|
|
// hasPermission: true if the user has the specified permission for the channel, otherwise false.
|
|
// isMember: used for auditing access without membership. True if the user is a member of the channel, otherwise false.
|
|
func (a *App) HasPermissionToChannel(rctx request.CTX, askingUserId string, channelID string, permission *model.Permission) (hasPermission bool, isMember bool) {
|
|
if channelID == "" || askingUserId == "" {
|
|
return false, false
|
|
}
|
|
|
|
isMember = false
|
|
|
|
// We call GetAllChannelMembersForUser instead of just getting
|
|
// a single member from the DB, because it's cache backed
|
|
// and this is a very frequent call.
|
|
ids, err := a.Srv().Store().Channel().GetAllChannelMembersForUser(rctx, askingUserId, true, true)
|
|
var channelRoles []string
|
|
if err == nil {
|
|
if roles, ok := ids[channelID]; ok {
|
|
isMember = true
|
|
channelRoles = strings.Fields(roles)
|
|
if a.RolesGrantPermission(channelRoles, permission.Id) {
|
|
return true, isMember
|
|
}
|
|
}
|
|
}
|
|
|
|
channel, appErr := a.GetChannel(rctx, channelID)
|
|
if appErr == nil && channel.TeamId != "" {
|
|
return a.HasPermissionToTeam(rctx, askingUserId, channel.TeamId, permission), isMember
|
|
}
|
|
|
|
return a.HasPermissionTo(askingUserId, permission), isMember
|
|
}
|
|
|
|
func (a *App) HasPermissionToChannelByPost(rctx request.CTX, 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(rctx, 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
|
|
if slices.Contains(permissions, 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(rctx request.CTX, session model.Session, botUserId string) *model.AppError {
|
|
existingBot, err := a.GetBot(rctx, 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("permissions", botUserId)
|
|
}
|
|
return model.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("permissions", botUserId)
|
|
}
|
|
return model.MakePermissionError(&session, []*model.Permission{model.PermissionManageOthersBots})
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// SessionHasPermissionToReadChannel checks whether the given session has permission
|
|
// to read the specified channel.
|
|
//
|
|
// Returns:
|
|
//
|
|
// (hasPermission, isMember)
|
|
//
|
|
// hasPermission: true if the user has permission to read the channel, false otherwise
|
|
// isMember: used for auditing access without membership. True if the user is a member of the channel, false otherwise
|
|
func (a *App) SessionHasPermissionToReadChannel(rctx request.CTX, session model.Session, channel *model.Channel) (hasPermission bool, isMember bool) {
|
|
if session.IsUnrestricted() {
|
|
return true, false
|
|
}
|
|
|
|
return a.HasPermissionToReadChannel(rctx, session.UserId, channel)
|
|
}
|
|
|
|
// HasPermissionToReadChannel determines if the specified user has permission to read the given channel.
|
|
//
|
|
// Returns:
|
|
//
|
|
// (hasPermission, isMember)
|
|
//
|
|
// hasPermission: true if the user has permission to read the channel, false otherwise
|
|
// isMember: used for auditing access without membership. True if the user is a member of the channel, false otherwise
|
|
func (a *App) HasPermissionToReadChannel(rctx request.CTX, userID string, channel *model.Channel) (hasPermission bool, isMember bool) {
|
|
if ok, member := a.HasPermissionToChannel(rctx, userID, channel.Id, model.PermissionReadChannelContent); ok {
|
|
return true, member
|
|
}
|
|
|
|
if (channel.Type == model.ChannelTypeOpen || channel.Type == model.ChannelTypeOpenBoard) && !*a.Config().ComplianceSettings.Enable {
|
|
return a.HasPermissionToTeam(rctx, userID, channel.TeamId, model.PermissionReadPublicChannel), false
|
|
}
|
|
|
|
return false, false
|
|
}
|
|
|
|
// HasPermissionToResolveChannelMention determines whether the specified user may have a
|
|
// ~channel mention resolved to its display name and link.
|
|
//
|
|
// Unlike HasPermissionToReadChannel, this exposes only the public channel NAME and link —
|
|
// never post/file content — so it is intentionally INDEPENDENT of ComplianceSettings.
|
|
// A public channel name is already discoverable within its team via browse/search/autocomplete,
|
|
// and following the link still requires joining the channel (which creates the compliance trail).
|
|
//
|
|
// Private/DM/GM channels resolve only for members (via the content-read check). Public channels
|
|
// on a team the user does not belong to stay unresolved, preventing cross-team disclosure.
|
|
func (a *App) HasPermissionToResolveChannelMention(rctx request.CTX, userID string, channel *model.Channel) bool {
|
|
if ok, _ := a.HasPermissionToChannel(rctx, userID, channel.Id, model.PermissionReadChannelContent); ok {
|
|
return true
|
|
}
|
|
|
|
if channel.Type == model.ChannelTypeOpen || channel.Type == model.ChannelTypeOpenBoard {
|
|
return a.HasPermissionToTeam(rctx, userID, channel.TeamId, model.PermissionReadPublicChannel)
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (a *App) HasPermissionToChannelMemberCount(rctx request.CTX, userID string, channel *model.Channel) bool {
|
|
if ok, _ := a.HasPermissionToChannel(rctx, userID, channel.Id, model.PermissionReadChannelContent); ok {
|
|
return true
|
|
}
|
|
|
|
if channel.Type == model.ChannelTypeOpen || channel.Type == model.ChannelTypeOpenBoard {
|
|
return a.HasPermissionToTeam(rctx, userID, channel.TeamId, model.PermissionListTeamChannels)
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// SessionHasPermissionToEditPropertyField checks if the session has permission to edit the field definition.
|
|
// Returns false if the field is nil, protected, or if PermissionField is nil (legacy fields).
|
|
func (a *App) SessionHasPermissionToEditPropertyField(rctx request.CTX, session model.Session, field *model.PropertyField) bool {
|
|
if field == nil {
|
|
return false
|
|
}
|
|
if field.Protected {
|
|
return false
|
|
}
|
|
if field.PermissionField == nil {
|
|
return false
|
|
}
|
|
if session.IsUnrestricted() {
|
|
return true
|
|
}
|
|
return a.hasPropertyFieldPermissionLevel(rctx, session.UserId, field, *field.PermissionField)
|
|
}
|
|
|
|
// SessionHasPermissionToSetPropertyFieldValues checks if the session has
|
|
// permission to set the given value on the field. The valueTargetID is the
|
|
// specific object the value is attached to (the channel/post/user/team ID
|
|
// for that ObjectType); admin and member levels are evaluated against it.
|
|
// Returns false if the field is nil or if PermissionValues is nil (legacy fields).
|
|
func (a *App) SessionHasPermissionToSetPropertyFieldValues(rctx request.CTX, session model.Session, field *model.PropertyField, valueTargetID string) bool {
|
|
if field == nil {
|
|
return false
|
|
}
|
|
if field.PermissionValues == nil {
|
|
return false
|
|
}
|
|
if session.IsUnrestricted() {
|
|
return true
|
|
}
|
|
return a.hasPropertyFieldValuePermissionLevel(rctx, session.UserId, field, valueTargetID, *field.PermissionValues)
|
|
}
|
|
|
|
// SessionHasPermissionToManagePropertyFieldOptions checks if the session has permission to manage field options.
|
|
// Returns false if the field is nil or if PermissionOptions is nil (legacy fields).
|
|
func (a *App) SessionHasPermissionToManagePropertyFieldOptions(rctx request.CTX, session model.Session, field *model.PropertyField) bool {
|
|
if field == nil {
|
|
return false
|
|
}
|
|
if field.PermissionOptions == nil {
|
|
return false
|
|
}
|
|
if session.IsUnrestricted() {
|
|
return true
|
|
}
|
|
return a.hasPropertyFieldPermissionLevel(rctx, session.UserId, field, *field.PermissionOptions)
|
|
}
|
|
|
|
// HasPermissionToEditPropertyField checks if the user has permission to edit the field definition.
|
|
// Returns false if the field is nil, protected, userID is empty, or if PermissionField is nil (legacy fields).
|
|
func (a *App) HasPermissionToEditPropertyField(rctx request.CTX, userID string, field *model.PropertyField) bool {
|
|
if field == nil || userID == "" {
|
|
return false
|
|
}
|
|
if field.Protected {
|
|
return false
|
|
}
|
|
if field.PermissionField == nil {
|
|
return false
|
|
}
|
|
return a.hasPropertyFieldPermissionLevel(rctx, userID, field, *field.PermissionField)
|
|
}
|
|
|
|
// HasPermissionToSetPropertyFieldValues checks if the user has permission to
|
|
// set the given value on the field. The valueTargetID is the specific object
|
|
// the value is attached to (the channel/post/user/team ID for that
|
|
// ObjectType); admin and member levels are evaluated against it.
|
|
// Returns false if the field is nil, userID is empty, or if PermissionValues is nil (legacy fields).
|
|
func (a *App) HasPermissionToSetPropertyFieldValues(rctx request.CTX, userID string, field *model.PropertyField, valueTargetID string) bool {
|
|
if field == nil || userID == "" {
|
|
return false
|
|
}
|
|
if field.PermissionValues == nil {
|
|
return false
|
|
}
|
|
return a.hasPropertyFieldValuePermissionLevel(rctx, userID, field, valueTargetID, *field.PermissionValues)
|
|
}
|
|
|
|
// HasPermissionToManagePropertyFieldOptions checks if the user has permission to manage field options.
|
|
// Returns false if the field is nil, userID is empty, or if PermissionOptions is nil (legacy fields).
|
|
func (a *App) HasPermissionToManagePropertyFieldOptions(rctx request.CTX, userID string, field *model.PropertyField) bool {
|
|
if field == nil || userID == "" {
|
|
return false
|
|
}
|
|
if field.PermissionOptions == nil {
|
|
return false
|
|
}
|
|
return a.hasPropertyFieldPermissionLevel(rctx, userID, field, *field.PermissionOptions)
|
|
}
|
|
|
|
// hasPropertyFieldPermissionLevel checks if the user has the specified permission level for the field.
|
|
// "admin" resolves against the field's target: manage_system on system targets,
|
|
// manage_team on team targets, manage_channel_roles on channel targets — i.e.
|
|
// the permission that the corresponding built-in admin role grants. Note this
|
|
// is a stricter check than hasTargetAccess (which uses manage_*_channel_properties
|
|
// for channel writes): hasTargetAccess is the outer "may write anything here"
|
|
// gate, and PermissionLevelAdmin is the inner "is a channel admin" tier above it.
|
|
func (a *App) hasPropertyFieldPermissionLevel(rctx request.CTX, userID string, field *model.PropertyField, level model.PermissionLevel) bool {
|
|
switch level {
|
|
case model.PermissionLevelNone:
|
|
return false
|
|
case model.PermissionLevelSysadmin:
|
|
return a.HasPermissionTo(userID, model.PermissionManageSystem)
|
|
case model.PermissionLevelMember:
|
|
return a.hasPropertyFieldScopeAccess(rctx, userID, field)
|
|
case model.PermissionLevelAdmin:
|
|
switch field.TargetType {
|
|
case string(model.PropertyFieldTargetLevelSystem):
|
|
return a.HasPermissionTo(userID, model.PermissionManageSystem)
|
|
case string(model.PropertyFieldTargetLevelTeam):
|
|
return a.HasPermissionToTeam(rctx, userID, field.TargetID, model.PermissionManageTeam)
|
|
case string(model.PropertyFieldTargetLevelChannel):
|
|
hasPermission, _ := a.HasPermissionToChannel(rctx, userID, field.TargetID, model.PermissionManageChannelRoles)
|
|
return hasPermission
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// hasPropertyFieldValuePermissionLevel evaluates a permission level against
|
|
// the value's specific target rather than the field's target. The "admin"
|
|
// and "member" levels dispatch on field.ObjectType against valueTargetID —
|
|
// so a value on a channel-object field is gated by the user's role on that
|
|
// channel, regardless of how the field itself is scoped. "sysadmin" and
|
|
// "none" behave identically to the field-level dispatch.
|
|
func (a *App) hasPropertyFieldValuePermissionLevel(rctx request.CTX, userID string, field *model.PropertyField, valueTargetID string, level model.PermissionLevel) bool {
|
|
switch level {
|
|
case model.PermissionLevelSysadmin:
|
|
return a.HasPermissionTo(userID, model.PermissionManageSystem)
|
|
case model.PermissionLevelAdmin:
|
|
return a.hasPropertyFieldValueAdmin(rctx, userID, field, valueTargetID)
|
|
case model.PermissionLevelMember:
|
|
return a.hasPropertyFieldValueScopeAccess(rctx, userID, field, valueTargetID)
|
|
case model.PermissionLevelNone:
|
|
return false
|
|
}
|
|
return false
|
|
}
|
|
|
|
// hasPropertyFieldValueAdmin reports whether the user administers the
|
|
// value's target. For channel/post-object fields, this is channel admin
|
|
// (manage_channel_roles) on the value's channel (or the post's channel).
|
|
// For user/system/template fields the value's target has no admin concept,
|
|
// so the check defers to the field's TargetType (sysadmin / team admin /
|
|
// channel admin) via the field-level dispatch.
|
|
func (a *App) hasPropertyFieldValueAdmin(rctx request.CTX, userID string, field *model.PropertyField, valueTargetID string) bool {
|
|
switch field.ObjectType {
|
|
case model.PropertyFieldObjectTypeChannel:
|
|
ok, _ := a.HasPermissionToChannel(rctx, userID, valueTargetID, model.PermissionManageChannelRoles)
|
|
return ok
|
|
case model.PropertyFieldObjectTypePost:
|
|
post, err := a.Srv().Store().Post().GetSingle(rctx, valueTargetID, false)
|
|
if err != nil {
|
|
rctx.Logger().Warn("Failed to look up post for property value admin check",
|
|
mlog.String("post_id", valueTargetID),
|
|
mlog.String("user_id", userID),
|
|
mlog.String("field_id", field.ID),
|
|
mlog.Err(err),
|
|
)
|
|
return false
|
|
}
|
|
ok, _ := a.HasPermissionToChannel(rctx, userID, post.ChannelId, model.PermissionManageChannelRoles)
|
|
return ok
|
|
case model.PropertyFieldObjectTypeUser,
|
|
model.PropertyFieldObjectTypeSystem,
|
|
model.PropertyFieldObjectTypeTemplate:
|
|
return a.hasPropertyFieldPermissionLevel(rctx, userID, field, model.PermissionLevelAdmin)
|
|
}
|
|
return false
|
|
}
|
|
|
|
// hasPropertyFieldValueScopeAccess reports whether the user can write the
|
|
// value's target as a regular member. For channel-object fields this is
|
|
// membership in the value's channel. For post-object fields this is
|
|
// membership in the post's channel — any channel member can set values on
|
|
// any post in that channel. Both are checked via HasPermissionToChannel so
|
|
// sysadmins and team admins cascade through. User/system/template fields
|
|
// have no per-object membership and defer to the field's TargetType-based scope.
|
|
func (a *App) hasPropertyFieldValueScopeAccess(rctx request.CTX, userID string, field *model.PropertyField, valueTargetID string) bool {
|
|
switch field.ObjectType {
|
|
case model.PropertyFieldObjectTypeChannel:
|
|
ok, _ := a.HasPermissionToChannel(rctx, userID, valueTargetID, model.PermissionReadChannel)
|
|
return ok
|
|
case model.PropertyFieldObjectTypePost:
|
|
post, err := a.Srv().Store().Post().GetSingle(rctx, valueTargetID, false)
|
|
if err != nil {
|
|
rctx.Logger().Warn("Failed to look up post for property value scope check",
|
|
mlog.String("post_id", valueTargetID),
|
|
mlog.String("user_id", userID),
|
|
mlog.String("field_id", field.ID),
|
|
mlog.Err(err),
|
|
)
|
|
return false
|
|
}
|
|
ok, _ := a.HasPermissionToChannel(rctx, userID, post.ChannelId, model.PermissionReadChannel)
|
|
return ok
|
|
case model.PropertyFieldObjectTypeUser,
|
|
model.PropertyFieldObjectTypeSystem,
|
|
model.PropertyFieldObjectTypeTemplate:
|
|
return a.hasPropertyFieldScopeAccess(rctx, userID, field)
|
|
}
|
|
return false
|
|
}
|
|
|
|
// hasPropertyFieldScopeAccess checks if the user has access to the property
|
|
// field's scope. System-level properties are open to any authenticated user.
|
|
// Team- and channel-level properties go through HasPermissionToTeam /
|
|
// HasPermissionToChannel so sysadmins (and team-admins for channel scopes)
|
|
// cascade through — matching the value-level scope check.
|
|
func (a *App) hasPropertyFieldScopeAccess(rctx request.CTX, userID string, field *model.PropertyField) bool {
|
|
switch field.TargetType {
|
|
case string(model.PropertyFieldTargetLevelSystem):
|
|
return true
|
|
case string(model.PropertyFieldTargetLevelTeam):
|
|
return a.HasPermissionToTeam(rctx, userID, field.TargetID, model.PermissionViewTeam)
|
|
case string(model.PropertyFieldTargetLevelChannel):
|
|
ok, _ := a.HasPermissionToChannel(rctx, userID, field.TargetID, model.PermissionReadChannel)
|
|
return ok
|
|
}
|
|
return false
|
|
}
|
|
|
|
// HasPermissionToFileAction evaluates whether the user is allowed to perform the
|
|
// given file action (e.g. upload_file_attachment, download_file_attachment) on
|
|
// a channel, based on ABAC permission policies.
|
|
// Returns true if allowed (or if ABAC is not active), false if denied.
|
|
func (a *App) HasPermissionToFileAction(rctx request.CTX, userID string, roles string, channelID string, action string) bool {
|
|
acs := a.Srv().Channels().AccessControl
|
|
if acs == nil {
|
|
return true
|
|
}
|
|
|
|
if !*a.Config().AccessControlSettings.EnableAttributeBasedAccessControl {
|
|
return true
|
|
}
|
|
|
|
if !a.Config().FeatureFlags.PermissionPolicies {
|
|
return true
|
|
}
|
|
|
|
var subject *model.Subject
|
|
var appErr *model.AppError
|
|
if rctx.Session().UserId == userID {
|
|
subject, appErr = a.BuildAccessControlSubjectForSession(rctx, channelID)
|
|
} else {
|
|
subject, appErr = a.BuildAccessControlSubject(rctx, userID, roles, channelID)
|
|
}
|
|
if appErr != nil {
|
|
rctx.Logger().Info("Failed to build ABAC subject for file action evaluation",
|
|
mlog.String("user_id", userID),
|
|
mlog.String("action", action),
|
|
mlog.Err(appErr),
|
|
)
|
|
return false
|
|
}
|
|
|
|
decision, evalErr := acs.AccessEvaluation(rctx, model.AccessRequest{
|
|
Subject: *subject,
|
|
Resource: model.Resource{
|
|
Type: model.AccessControlPolicyTypeChannel,
|
|
ID: channelID,
|
|
},
|
|
Action: action,
|
|
})
|
|
if evalErr != nil {
|
|
rctx.Logger().Debug("ABAC file action evaluation failed, denying by default",
|
|
mlog.String("user_id", userID),
|
|
mlog.String("action", action),
|
|
mlog.String("channel_id", channelID),
|
|
mlog.Err(evalErr),
|
|
)
|
|
return false
|
|
}
|
|
|
|
return decision.Decision
|
|
}
|