mirror of
https://github.com/mattermost/mattermost.git
synced 2026-04-13 13:08:56 -04:00
Some checks are pending
API / build (push) Waiting to run
Server CI / Compute Go Version (push) Waiting to run
Server CI / Check mocks (push) Blocked by required conditions
Server CI / Check go mod tidy (push) Blocked by required conditions
Server CI / check-style (push) Blocked by required conditions
Server CI / Check serialization methods for hot structs (push) Blocked by required conditions
Server CI / Vet API (push) Blocked by required conditions
Server CI / Check migration files (push) Blocked by required conditions
Server CI / Generate email templates (push) Blocked by required conditions
Server CI / Check store layers (push) Blocked by required conditions
Server CI / Check mmctl docs (push) Blocked by required conditions
Server CI / Postgres with binary parameters (push) Blocked by required conditions
Server CI / Postgres (push) Blocked by required conditions
Server CI / Postgres (FIPS) (push) Blocked by required conditions
Server CI / Generate Test Coverage (push) Blocked by required conditions
Server CI / Run mmctl tests (push) Blocked by required conditions
Server CI / Run mmctl tests (FIPS) (push) Blocked by required conditions
Server CI / Build mattermost server app (push) Blocked by required conditions
Web App CI / check-lint (push) Waiting to run
Web App CI / check-i18n (push) Blocked by required conditions
Web App CI / check-external-links (push) Blocked by required conditions
Web App CI / check-types (push) Blocked by required conditions
Web App CI / test (platform) (push) Blocked by required conditions
Web App CI / test (mattermost-redux) (push) Blocked by required conditions
Web App CI / test (channels shard 1/4) (push) Blocked by required conditions
Web App CI / test (channels shard 2/4) (push) Blocked by required conditions
Web App CI / test (channels shard 3/4) (push) Blocked by required conditions
Web App CI / test (channels shard 4/4) (push) Blocked by required conditions
Web App CI / upload-coverage (push) Blocked by required conditions
Web App CI / build (push) Blocked by required conditions
* Refactor property system with app layer routing and access control separation Establish the app layer as the primary entry point for property operations with intelligent routing based on group type. This architecture separates access-controlled operations (CPA groups) from standard operations, improving performance and code clarity. Architecture Changes: - App layer now routes operations based on group type: - CPA groups -> PropertyAccessService (enforces access control) - Non-CPA groups -> PropertyService (direct, no access control) - PropertyAccessService simplified to handle only CPA operations - Eliminated redundant group type checks throughout the codebase * Move access control routing into PropertyService This change makes the PropertyService the main entrypoint for property related operations, and adds a routing mechanism to decide if extra behaviors or checks should run for each operation, in this case, the property access service logic. To add specific payloads that pluggable checks and operations may need, we use the request context. When the request comes from the API, the endpoints are in charge of adding the caller ID to the payload, and in the case of the plugin API, on receiving a request, the server automatically tags the context with the plugin ID so the property service can react accordingly. Finally, the new design enforces all these checks migrating the actual property logic to internal, non-exposed methods, so any caller from the App layer needs to go through the service checks that decide if pluggable logic is needed, avoiding any possibility of a bypass. * Fix i18n * Fix bad error string * Added nil guards to property methods * Add check for multiple group IDs on value operations * Add nil guard to the plugin checker * Fix build error * Update value tests * Fix linter * Adds early return when content flaggin a thread with no replies * Fix mocks * Clean the state of plugin property tests before each run * Do not wrap appErr on API response and fix i18n * Fix create property field test * Remove the need to cache cpaGroupID as part of the property service * Split the property.go file into multiple * Not found group doesn't bypass access control check * Unexport SetPluginCheckerForTests * Rename plugin context getter to be more PSA specific --------- Co-authored-by: Miguel de la Cruz <miguel@ctrlz.es>
511 lines
21 KiB
Go
511 lines
21 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package app
|
|
|
|
import (
|
|
"net/http"
|
|
"slices"
|
|
|
|
"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) GetChannelsForPolicy(rctx request.CTX, policyID string, cursor model.AccessControlPolicyCursor, limit int) ([]*model.ChannelWithTeamData, int64, *model.AppError) {
|
|
policy, appErr := a.GetAccessControlPolicy(rctx, policyID)
|
|
if appErr != nil {
|
|
return nil, 0, appErr
|
|
}
|
|
|
|
switch policy.Type {
|
|
case model.AccessControlPolicyTypeParent:
|
|
policies, total, err := a.Srv().Store().AccessControlPolicy().SearchPolicies(rctx, model.AccessControlPolicySearch{
|
|
Type: model.AccessControlPolicyTypeChannel,
|
|
ParentID: policyID,
|
|
Cursor: cursor,
|
|
Limit: limit,
|
|
})
|
|
if err != nil {
|
|
return nil, 0, model.NewAppError("GetChannelsForPolicy", "app.pap.get_all_access_control_policies.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
channelIDs := make([]string, 0, len(policies))
|
|
|
|
// channel IDs are the same as policy IDs
|
|
for _, p := range policies {
|
|
channelIDs = append(channelIDs, p.ID)
|
|
}
|
|
|
|
chs, err := a.Srv().Store().Channel().GetChannelsWithTeamDataByIds(channelIDs, true)
|
|
if err != nil {
|
|
return nil, 0, model.NewAppError("GetChannelsForPolicy", "app.pap.get_all_access_control_policies.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
return chs, total, nil
|
|
case model.AccessControlPolicyTypeChannel:
|
|
chs, err := a.Srv().Store().Channel().GetChannelsWithTeamDataByIds([]string{policyID}, true)
|
|
if err != nil {
|
|
return nil, 0, model.NewAppError("GetChannelsForPolicy", "app.pap.get_all_access_control_policies.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
total := int64(len(chs))
|
|
return chs, total, nil
|
|
default:
|
|
return nil, 0, model.NewAppError("GetChannelsForPolicy", "app.pap.get_all_access_control_policies.app_error", nil, "Invalid policy type", http.StatusBadRequest)
|
|
}
|
|
}
|
|
|
|
func (a *App) GetAccessControlPolicy(rctx request.CTX, id string) (*model.AccessControlPolicy, *model.AppError) {
|
|
acs := a.Srv().ch.AccessControl
|
|
if acs == nil {
|
|
return nil, model.NewAppError("GetPolicy", "app.pap.get_policy.app_error", nil, "Policy Administration Point is not initialized", http.StatusNotImplemented)
|
|
}
|
|
|
|
policy, appErr := acs.GetPolicy(rctx, id)
|
|
if appErr != nil {
|
|
return nil, appErr
|
|
}
|
|
|
|
return policy, nil
|
|
}
|
|
|
|
func (a *App) CreateOrUpdateAccessControlPolicy(rctx request.CTX, policy *model.AccessControlPolicy) (*model.AccessControlPolicy, *model.AppError) {
|
|
acs := a.Srv().ch.AccessControl
|
|
if acs == nil {
|
|
return nil, model.NewAppError("CreateAccessControlPolicy", "app.pap.create_access_control_policy.app_error", nil, "Policy Administration Point is not initialized", http.StatusNotImplemented)
|
|
}
|
|
|
|
if policy.ID == "" {
|
|
policy.ID = model.NewId()
|
|
}
|
|
|
|
var appErr *model.AppError
|
|
policy, appErr = acs.SavePolicy(rctx, policy)
|
|
if appErr != nil {
|
|
return nil, appErr
|
|
}
|
|
|
|
return policy, nil
|
|
}
|
|
|
|
func (a *App) DeleteAccessControlPolicy(rctx request.CTX, id string) *model.AppError {
|
|
acs := a.Srv().ch.AccessControl
|
|
if acs == nil {
|
|
return model.NewAppError("DeleteAccessControlPolicy", "app.pap.delete_access_control_policy.app_error", nil, "Policy Administration Point is not initialized", http.StatusNotImplemented)
|
|
}
|
|
|
|
appErr := acs.DeletePolicy(rctx, id)
|
|
if appErr != nil {
|
|
return appErr
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) CheckExpression(rctx request.CTX, expression string) ([]model.CELExpressionError, *model.AppError) {
|
|
acs := a.Srv().ch.AccessControl
|
|
if acs == nil {
|
|
return nil, model.NewAppError("CheckExpression", "app.pap.check_expression.app_error", nil, "Policy Administration Point is not initialized", http.StatusNotImplemented)
|
|
}
|
|
|
|
errs, appErr := acs.CheckExpression(rctx, expression)
|
|
if appErr != nil {
|
|
return nil, model.NewAppError("CheckExpression", "app.pap.check_expression.app_error", nil, appErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
return errs, nil
|
|
}
|
|
|
|
func (a *App) TestExpression(rctx request.CTX, expression string, opts model.SubjectSearchOptions) ([]*model.User, int64, *model.AppError) {
|
|
acs := a.Srv().ch.AccessControl
|
|
if acs == nil {
|
|
return nil, 0, model.NewAppError("TestExpression", "app.pap.check_expression.app_error", nil, "Policy Administration Point is not initialized", http.StatusNotImplemented)
|
|
}
|
|
|
|
res, count, err := acs.QueryUsersForExpression(rctx, expression, opts)
|
|
if err != nil {
|
|
return nil, 0, model.NewAppError("TestExpression", "app.pap.check_expression.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
return res, count, nil
|
|
}
|
|
|
|
func (a *App) AssignAccessControlPolicyToChannels(rctx request.CTX, parentID string, channelIDs []string) ([]*model.AccessControlPolicy, *model.AppError) {
|
|
acs := a.Srv().ch.AccessControl
|
|
if acs == nil {
|
|
return nil, model.NewAppError("AssignAccessControlPolicyToChannels", "app.pap.assign_access_control_policy_to_channels.app_error", nil, "Policy Administration Point is not initialized", http.StatusNotImplemented)
|
|
}
|
|
|
|
policy, appErr := a.GetAccessControlPolicy(rctx, parentID)
|
|
if appErr != nil {
|
|
return nil, appErr
|
|
}
|
|
|
|
if policy.Type != model.AccessControlPolicyTypeParent {
|
|
return nil, model.NewAppError("AssignAccessControlPolicyToChannels", "app.pap.assign_access_control_policy_to_channels.app_error", nil, "Policy is not of type parent", http.StatusBadRequest)
|
|
}
|
|
|
|
channels, err := a.GetChannels(rctx, channelIDs)
|
|
if err != nil {
|
|
return nil, appErr
|
|
}
|
|
|
|
policies := make([]*model.AccessControlPolicy, 0, len(channelIDs))
|
|
for _, channel := range channels {
|
|
if channel.Type != model.ChannelTypePrivate || channel.IsGroupConstrained() {
|
|
return nil, model.NewAppError("AssignAccessControlPolicyToChannels", "app.pap.assign_access_control_policy_to_channels.app_error", nil, "Channel is not of type private", http.StatusBadRequest)
|
|
}
|
|
|
|
if channel.IsShared() {
|
|
return nil, model.NewAppError("AssignAccessControlPolicyToChannels", "app.pap.assign_access_control_policy_to_channels.app_error", nil, "Channel is shared", http.StatusBadRequest)
|
|
}
|
|
|
|
child, err := acs.GetPolicy(rctx, channel.Id)
|
|
if err != nil && err.StatusCode != http.StatusNotFound {
|
|
return nil, model.NewAppError("AssignAccessControlPolicyToChannels", "app.pap.assign_access_control_policy_to_channels.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
if child == nil {
|
|
child = &model.AccessControlPolicy{
|
|
ID: channel.Id,
|
|
Type: model.AccessControlPolicyTypeChannel,
|
|
Active: policy.Active,
|
|
CreateAt: model.GetMillis(),
|
|
Props: map[string]any{},
|
|
}
|
|
}
|
|
child.Version = model.AccessControlPolicyVersionV0_2
|
|
|
|
appErr := child.Inherit(policy)
|
|
if appErr != nil {
|
|
return nil, appErr
|
|
}
|
|
|
|
child, appErr = acs.SavePolicy(rctx, child)
|
|
if appErr != nil {
|
|
return nil, appErr
|
|
}
|
|
policies = append(policies, child)
|
|
}
|
|
|
|
return policies, nil
|
|
}
|
|
|
|
func (a *App) UnassignPoliciesFromChannels(rctx request.CTX, policyID string, channelIDs []string) *model.AppError {
|
|
acs := a.Srv().ch.AccessControl
|
|
if acs == nil {
|
|
return model.NewAppError("UnassignPoliciesFromChannels", "app.pap.unassign_access_control_policy_from_channels.app_error", nil, "Policy Administration Point is not initialized", http.StatusNotImplemented)
|
|
}
|
|
|
|
cps, _, err := a.Srv().Store().AccessControlPolicy().SearchPolicies(rctx, model.AccessControlPolicySearch{
|
|
Type: model.AccessControlPolicyTypeChannel,
|
|
ParentID: policyID,
|
|
Limit: 1000,
|
|
})
|
|
if err != nil {
|
|
return model.NewAppError("UnassignPoliciesFromChannels", "app.pap.unassign_access_control_policy_from_channels.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
childPolicies := make(map[string]bool)
|
|
for _, p := range cps {
|
|
childPolicies[p.ID] = true
|
|
}
|
|
|
|
for _, channelID := range channelIDs {
|
|
if _, ok := childPolicies[channelID]; !ok {
|
|
mlog.Warn("Policy is not assigned to the parent policy", mlog.String("channel_id", channelID), mlog.String("parent_policy_id", policyID))
|
|
continue
|
|
}
|
|
|
|
child, appErr := acs.GetPolicy(rctx, channelID)
|
|
if appErr != nil {
|
|
return model.NewAppError("UnassignPoliciesFromChannels", "app.pap.unassign_access_control_policy_from_channels.app_error", nil, appErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
child.Imports = slices.DeleteFunc(child.Imports, func(importID string) bool {
|
|
return importID == policyID
|
|
})
|
|
if len(child.Imports) == 0 && len(child.Rules) == 0 {
|
|
// If the policy has no imports and no rules, we can delete it
|
|
if err := acs.DeletePolicy(rctx, child.ID); err != nil {
|
|
return model.NewAppError("UnassignPoliciesFromChannels", "app.pap.unassign_access_control_policy_from_channels.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
// invalidate the channel cache
|
|
a.Srv().Store().Channel().InvalidateChannel(channelID)
|
|
continue
|
|
}
|
|
_, appErr = acs.SavePolicy(rctx, child)
|
|
if appErr != nil {
|
|
return model.NewAppError("UnassignPoliciesFromChannels", "app.pap.unassign_access_control_policy_from_channels.app_error", nil, appErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) SearchAccessControlPolicies(rctx request.CTX, opts model.AccessControlPolicySearch) ([]*model.AccessControlPolicy, int64, *model.AppError) {
|
|
acs := a.Srv().ch.AccessControl
|
|
if acs == nil {
|
|
return nil, 0, model.NewAppError("SearchAccessControlPolicies", "app.pap.search_access_control_policies.app_error", nil, "Policy Administration Point is not initialized", http.StatusNotImplemented)
|
|
}
|
|
|
|
policies, total, err := a.Srv().Store().AccessControlPolicy().SearchPolicies(rctx, opts)
|
|
if err != nil {
|
|
return nil, 0, model.NewAppError("SearchAccessControlPolicies", "app.pap.search_access_control_policies.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
for i, policy := range policies {
|
|
if policy.Type != model.AccessControlPolicyTypeParent {
|
|
continue
|
|
}
|
|
|
|
normlizedPolicy, appErr := acs.NormalizePolicy(rctx, policy)
|
|
if appErr != nil {
|
|
mlog.Error("Failed to normalize policy", mlog.String("policy_id", policy.ID), mlog.Err(appErr))
|
|
continue
|
|
}
|
|
policies[i] = normlizedPolicy
|
|
}
|
|
|
|
return policies, total, nil
|
|
}
|
|
|
|
func (a *App) GetAccessControlPolicyAttributes(rctx request.CTX, channelID string, action string) (map[string][]string, *model.AppError) {
|
|
acs := a.Srv().ch.AccessControl
|
|
if acs == nil {
|
|
return nil, model.NewAppError("GetChannelAccessControlAttributes", "app.pap.get_channel_access_control_attributes.app_error", nil, "Policy Administration Point is not initialized", http.StatusNotImplemented)
|
|
}
|
|
|
|
attributes, appErr := acs.GetPolicyRuleAttributes(rctx, channelID, action)
|
|
if appErr != nil {
|
|
return nil, appErr
|
|
}
|
|
|
|
return attributes, nil
|
|
}
|
|
|
|
func (a *App) GetAccessControlFieldsAutocomplete(rctx request.CTX, after string, limit int, callerID string) ([]*model.PropertyField, *model.AppError) {
|
|
cpaGroupID, appErr := a.CpaGroupID()
|
|
if appErr != nil {
|
|
return nil, model.NewAppError("GetAccessControlAutoComplete", "app.pap.get_access_control_auto_complete.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
|
|
}
|
|
|
|
// Use property app layer to enforce access control
|
|
rctxWithCaller := RequestContextWithCallerID(rctx, callerID)
|
|
fields, appErr := a.SearchPropertyFields(rctxWithCaller, cpaGroupID, model.PropertyFieldSearchOpts{
|
|
Cursor: model.PropertyFieldSearchCursor{
|
|
PropertyFieldID: after,
|
|
CreateAt: 1,
|
|
},
|
|
PerPage: limit,
|
|
})
|
|
if appErr != nil {
|
|
return nil, model.NewAppError("GetAccessControlAutoComplete", "app.pap.get_access_control_auto_complete.app_error", nil, appErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
return fields, nil
|
|
}
|
|
|
|
func (a *App) UpdateAccessControlPoliciesActive(rctx request.CTX, updates []model.AccessControlPolicyActiveUpdate) ([]*model.AccessControlPolicy, *model.AppError) {
|
|
acs := a.Srv().ch.AccessControl
|
|
if acs == nil {
|
|
return nil, model.NewAppError("ExpressionToVisualAST", "app.pap.update_access_control_policies_active.app_error", nil, "Policy Administration Point is not initialized", http.StatusNotImplemented)
|
|
}
|
|
|
|
policies, err := a.Srv().Store().AccessControlPolicy().SetActiveStatusMultiple(rctx, updates)
|
|
if err != nil {
|
|
return nil, model.NewAppError("UpdateAccessControlPoliciesActive", "app.pap.update_access_control_policies_active.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
return policies, nil
|
|
}
|
|
|
|
func (a *App) ExpressionToVisualAST(rctx request.CTX, expression string) (*model.VisualExpression, *model.AppError) {
|
|
acs := a.Srv().ch.AccessControl
|
|
if acs == nil {
|
|
return nil, model.NewAppError("ExpressionToVisualAST", "app.pap.expression_to_visual_ast.app_error", nil, "Policy Administration Point is not initialized", http.StatusNotImplemented)
|
|
}
|
|
|
|
visualAST, appErr := acs.ExpressionToVisualAST(rctx, expression)
|
|
if appErr != nil {
|
|
return nil, appErr
|
|
}
|
|
|
|
return visualAST, nil
|
|
}
|
|
|
|
// ValidateChannelAccessControlPermission validates if a user has permission to manage access control for a specific channel
|
|
func (a *App) ValidateChannelAccessControlPermission(rctx request.CTX, userID, channelID string) *model.AppError {
|
|
// Verify the channel exists
|
|
channel, appErr := a.GetChannel(rctx, channelID)
|
|
if appErr != nil {
|
|
return appErr
|
|
}
|
|
|
|
// Check if user has channel admin permission for the specific channel
|
|
if ok, _ := a.HasPermissionToChannel(rctx, userID, channelID, model.PermissionManageChannelAccessRules); !ok {
|
|
return model.NewAppError("ValidateChannelAccessControlPermission", "app.pap.access_control.insufficient_channel_permissions", nil, "user_id="+userID+" channel_id="+channelID, http.StatusForbidden)
|
|
}
|
|
|
|
// Verify the channel is a private channel
|
|
if channel.Type != model.ChannelTypePrivate {
|
|
return model.NewAppError("ValidateChannelAccessControlPermission", "app.pap.access_control.channel_not_private", nil, "channel_id="+channelID, http.StatusBadRequest)
|
|
}
|
|
|
|
if channel.IsGroupConstrained() {
|
|
return model.NewAppError("ValidateChannelAccessControlPermission", "app.pap.access_control.channel_group_constrained", nil, "channel_id="+channelID, http.StatusBadRequest)
|
|
}
|
|
|
|
if channel.IsShared() {
|
|
return model.NewAppError("ValidateChannelAccessControlPermission", "app.pap.access_control.channel_shared", nil, "channel_id="+channelID, http.StatusBadRequest)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ValidateAccessControlPolicyPermission validates if a user has permission to manage a specific existing access control policy
|
|
func (a *App) ValidateAccessControlPolicyPermission(rctx request.CTX, userID, policyID string) *model.AppError {
|
|
return a.ValidateAccessControlPolicyPermissionWithOptions(rctx, userID, policyID, ValidateAccessControlPolicyPermissionOptions{})
|
|
}
|
|
|
|
type ValidateAccessControlPolicyPermissionOptions struct {
|
|
isReadOnly bool
|
|
channelID string
|
|
}
|
|
|
|
func (a *App) ValidateAccessControlPolicyPermissionWithOptions(rctx request.CTX, userID, policyID string, opts ValidateAccessControlPolicyPermissionOptions) *model.AppError {
|
|
// System admins can manage any policy
|
|
if a.HasPermissionTo(userID, model.PermissionManageSystem) {
|
|
return nil
|
|
}
|
|
|
|
// Get the policy to determine its type
|
|
policy, appErr := a.GetAccessControlPolicy(rctx, policyID)
|
|
if appErr != nil {
|
|
return appErr
|
|
}
|
|
|
|
// For read-only operations, allow access to system policies if they're applied to the specific channel
|
|
if opts.isReadOnly && policy.Type != model.AccessControlPolicyTypeChannel && opts.channelID != "" {
|
|
// Check if user has access to the channel
|
|
if ok, _ := a.HasPermissionToChannel(rctx, userID, opts.channelID, model.PermissionReadChannel); !ok {
|
|
return model.NewAppError("ValidateAccessControlPolicyPermissionWithOptions", "app.pap.access_control.insufficient_permissions", nil, "user_id="+userID+" channel_id="+opts.channelID, http.StatusForbidden)
|
|
}
|
|
|
|
// Check if this system policy is applied to the specific channel
|
|
if a.isSystemPolicyAppliedToChannel(rctx, policyID, opts.channelID) {
|
|
return nil // Allow read-only access
|
|
}
|
|
return model.NewAppError("ValidateAccessControlPolicyPermissionWithOptions", "app.pap.access_control.insufficient_permissions", nil, "user_id="+userID+" policy_type="+policy.Type+" channel_id="+opts.channelID, http.StatusForbidden)
|
|
}
|
|
|
|
// Non-system admins can only manage channel-type policies (for non-read-only operations)
|
|
if policy.Type != model.AccessControlPolicyTypeChannel {
|
|
return model.NewAppError("ValidateAccessControlPolicyPermissionWithOptions", "app.pap.access_control.insufficient_permissions", nil, "user_id="+userID+" policy_type="+policy.Type, http.StatusForbidden)
|
|
}
|
|
|
|
// For channel-type policies, validate channel-specific permission (policy ID equals channel ID)
|
|
return a.ValidateChannelAccessControlPermission(rctx, userID, policyID)
|
|
}
|
|
|
|
// ValidateAccessControlPolicyPermissionWithMode validates access control policy permissions with read-only mode option
|
|
func (a *App) ValidateAccessControlPolicyPermissionWithMode(rctx request.CTX, userID, policyID string, isReadOnly bool) *model.AppError {
|
|
return a.ValidateAccessControlPolicyPermissionWithOptions(rctx, userID, policyID, ValidateAccessControlPolicyPermissionOptions{
|
|
isReadOnly: isReadOnly,
|
|
})
|
|
}
|
|
|
|
// ValidateAccessControlPolicyPermissionWithChannelContext validates access control policy permissions with channel context
|
|
func (a *App) ValidateAccessControlPolicyPermissionWithChannelContext(rctx request.CTX, userID, policyID string, isReadOnly bool, channelID string) *model.AppError {
|
|
return a.ValidateAccessControlPolicyPermissionWithOptions(rctx, userID, policyID, ValidateAccessControlPolicyPermissionOptions{
|
|
isReadOnly: isReadOnly,
|
|
channelID: channelID,
|
|
})
|
|
}
|
|
|
|
// isSystemPolicyAppliedToChannel checks if a system policy is applied to a specific channel
|
|
func (a *App) isSystemPolicyAppliedToChannel(rctx request.CTX, policyID, channelID string) bool {
|
|
// Get the channel's policy (channel ID = policy ID for channel policies)
|
|
channelPolicy, err := a.GetAccessControlPolicy(rctx, channelID)
|
|
if err != nil {
|
|
return false // Channel doesn't have a policy
|
|
}
|
|
|
|
// Check if the channel policy imports this system policy
|
|
if channelPolicy.Imports != nil {
|
|
return slices.Contains(channelPolicy.Imports, policyID)
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// ValidateChannelAccessControlPolicyCreation validates if a user can create a channel-specific access control policy
|
|
func (a *App) ValidateChannelAccessControlPolicyCreation(rctx request.CTX, userID string, policy *model.AccessControlPolicy) *model.AppError {
|
|
// System admins can create any type of policy
|
|
if a.HasPermissionTo(userID, model.PermissionManageSystem) {
|
|
return nil
|
|
}
|
|
|
|
// Non-system admins can only create channel-type policies
|
|
if policy.Type != model.AccessControlPolicyTypeChannel {
|
|
return model.NewAppError("ValidateChannelAccessControlPolicyCreation", "app.access_control.insufficient_permissions", nil, "user_id="+userID+" policy_type="+policy.Type, http.StatusForbidden)
|
|
}
|
|
|
|
// For channel-type policies, validate channel-specific permission (policy ID equals channel ID)
|
|
return a.ValidateChannelAccessControlPermission(rctx, userID, policy.ID)
|
|
}
|
|
|
|
// TestExpressionWithChannelContext tests expressions for channel admins with attribute validation
|
|
// Channel admins can only see users that match expressions they themselves would match
|
|
func (a *App) TestExpressionWithChannelContext(rctx request.CTX, expression string, opts model.SubjectSearchOptions) ([]*model.User, int64, *model.AppError) {
|
|
// Get the current user (channel admin)
|
|
session := rctx.Session()
|
|
if session == nil {
|
|
return nil, 0, model.NewAppError("TestExpressionWithChannelContext", "api.context.session_expired.app_error", nil, "", http.StatusUnauthorized)
|
|
}
|
|
|
|
currentUserID := session.UserId
|
|
|
|
// SECURITY: First check if the channel admin themselves matches this expression
|
|
// If they don't match, they shouldn't be able to see users who do
|
|
adminMatches, appErr := a.ValidateExpressionAgainstRequester(rctx, expression, currentUserID)
|
|
if appErr != nil {
|
|
return nil, 0, appErr
|
|
}
|
|
|
|
if !adminMatches {
|
|
// Channel admin doesn't match the expression, so return empty results
|
|
return []*model.User{}, 0, nil
|
|
}
|
|
|
|
// If the channel admin matches the expression, run it against all users
|
|
acs := a.Srv().ch.AccessControl
|
|
if acs == nil {
|
|
return nil, 0, model.NewAppError("TestExpressionWithChannelContext", "app.pap.check_expression.app_error", nil, "Policy Administration Point is not initialized", http.StatusNotImplemented)
|
|
}
|
|
|
|
return a.TestExpression(rctx, expression, opts)
|
|
}
|
|
|
|
// ValidateExpressionAgainstRequester validates an expression directly against a specific user
|
|
func (a *App) ValidateExpressionAgainstRequester(rctx request.CTX, expression string, requesterID string) (bool, *model.AppError) {
|
|
// Self-exclusion validation should work with any attribute
|
|
// Channel admins should be able to validate any expression they're testing
|
|
|
|
// Use access control service to evaluate expression
|
|
acs := a.Srv().ch.AccessControl
|
|
if acs == nil {
|
|
return false, model.NewAppError("ValidateExpressionAgainstRequester", "app.pap.check_expression.app_error", nil, "Policy Administration Point is not initialized", http.StatusNotImplemented)
|
|
}
|
|
|
|
// Search only for the specific requester user ID
|
|
users, _, appErr := acs.QueryUsersForExpression(rctx, expression, model.SubjectSearchOptions{
|
|
SubjectID: requesterID, // Only check this specific user
|
|
Limit: 1, // Maximum 1 result expected
|
|
})
|
|
if appErr != nil {
|
|
return false, appErr
|
|
}
|
|
if len(users) == 1 && users[0].Id == requesterID {
|
|
return true, nil
|
|
}
|
|
return false, nil
|
|
}
|