// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. package api4 import ( "encoding/json" "net/http" "slices" "strconv" "strings" "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" "github.com/mattermost/mattermost/server/v8/channels/app" ) // shouldRedactExpressions reports whether raw CEL expressions should be masked for this caller. // Masking is attribute-based, not permission-based: system admins who do not hold all values // in a policy must also receive redacted raw expressions. func shouldRedactExpressions(c *Context) bool { return c.App.Config().FeatureFlags.AttributeValueMasking } // preserveSystemManagedFields pins parent imports and team-scope metadata to the stored values: // attaching/detaching parents belongs to the assign/unassign endpoints, so a channel/team admin // editing rules here can't change them. No stored policy (first-time create) means they start empty. func preserveSystemManagedFields(c *Context, policy *model.AccessControlPolicy) *model.AppError { stored, appErr := c.App.GetAccessControlPolicy(c.AppContext, policy.ID) if appErr != nil { if appErr.StatusCode == http.StatusNotFound { policy.Imports = nil policy.Scope = "" policy.ScopeID = "" return nil } return appErr } // Clone so a later mutation of policy.Imports can't reach back into the stored object. policy.Imports = slices.Clone(stored.Imports) policy.Scope = stored.Scope policy.ScopeID = stored.ScopeID return nil } func (api *API) InitAccessControlPolicy() { api.BaseRoutes.AccessControlPolicies.Handle("", api.APISessionRequired(createAccessControlPolicy)).Methods(http.MethodPut) api.BaseRoutes.AccessControlPolicies.Handle("/search", api.APISessionRequired(searchAccessControlPolicies)).Methods(http.MethodPost) api.BaseRoutes.AccessControlPolicies.Handle("/activate", api.APISessionRequired(setActiveStatus)).Methods(http.MethodPut) api.BaseRoutes.AccessControlPolicies.Handle("/cel/check", api.APISessionRequired(checkExpression)).Methods(http.MethodPost) api.BaseRoutes.AccessControlPolicies.Handle("/cel/test", api.APISessionRequired(testExpression)).Methods(http.MethodPost) api.BaseRoutes.AccessControlPolicies.Handle("/cel/simulate_users", api.APISessionRequired(simulatePolicyForUsers)).Methods(http.MethodPost) api.BaseRoutes.AccessControlPolicies.Handle("/cel/validate_requester", api.APISessionRequired(validateExpressionAgainstRequester)).Methods(http.MethodPost) api.BaseRoutes.AccessControlPolicies.Handle("/cel/autocomplete/fields", api.APISessionRequired(getFieldsAutocomplete)).Methods(http.MethodGet) api.BaseRoutes.AccessControlPolicies.Handle("/cel/visual_ast", api.APISessionRequired(convertToVisualAST)).Methods(http.MethodPost) api.BaseRoutes.AccessControlPolicy.Handle("", api.APISessionRequired(getAccessControlPolicy)).Methods(http.MethodGet) api.BaseRoutes.AccessControlPolicy.Handle("", api.APISessionRequired(deleteAccessControlPolicy)).Methods(http.MethodDelete) api.BaseRoutes.AccessControlPolicy.Handle("/activate", api.APISessionRequired(updateActiveStatus)).Methods(http.MethodGet) api.BaseRoutes.AccessControlPolicy.Handle("/assign", api.APISessionRequired(assignAccessPolicy)).Methods(http.MethodPost) api.BaseRoutes.AccessControlPolicy.Handle("/unassign", api.APISessionRequired(unassignAccessPolicy)).Methods(http.MethodDelete) api.BaseRoutes.AccessControlPolicy.Handle("/resources/channels", api.APISessionRequired(getChannelsForAccessControlPolicy)).Methods(http.MethodGet) api.BaseRoutes.AccessControlPolicy.Handle("/resources/channels/search", api.APISessionRequired(searchChannelsForAccessControlPolicy)).Methods(http.MethodPost) api.BaseRoutes.AccessControlDecisions.Handle("/actions/search", api.APISessionRequired(searchAccessControlDecisionActions)).Methods(http.MethodPost) } // searchAccessControlDecisionActions returns non-authoritative, render-time ABAC // decisions for the current session user on a single resource. If Subject is // provided in the request it must match the authenticated session user ID — any // other value is rejected with 403. Results are for rendering only; protected // endpoints always re-evaluate the PDP live. func searchAccessControlDecisionActions(c *Context, w http.ResponseWriter, r *http.Request) { var req model.ActionSearchRequest if jsonErr := json.NewDecoder(r.Body).Decode(&req); jsonErr != nil { c.SetInvalidParamWithErr("action_search", jsonErr) return } if appErr := req.IsValid(); appErr != nil { c.Err = appErr return } // Fail closed on resource types we can't authorize: only channel decisions are exposed // today, so anything else is rejected rather than silently skipping the access check. switch req.Resource.Type { case model.AccessControlPolicyTypeChannel: if hasPermission, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), req.Resource.ID, model.PermissionReadChannel); !hasPermission { c.SetPermissionError(model.PermissionReadChannel) return } default: c.Err = model.NewAppError("searchAccessControlDecisionActions", "api.access_control.decision.unsupported_resource_type.app_error", map[string]any{"Type": req.Resource.Type}, "", http.StatusBadRequest) return } resp, appErr := c.App.SearchAllowedActionsForCurrentUser(c.AppContext, req) if appErr != nil { c.Err = appErr return } js, err := json.Marshal(resp) if err != nil { c.Err = model.NewAppError("searchAccessControlDecisionActions", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err) return } if _, err := w.Write(js); err != nil { c.Logger.Warn("Error while writing response", mlog.Err(err)) } } func createAccessControlPolicy(c *Context, w http.ResponseWriter, r *http.Request) { var policy model.AccessControlPolicy if jsonErr := json.NewDecoder(r.Body).Decode(&policy); jsonErr != nil { c.SetInvalidParamWithErr("policy", jsonErr) return } if policy.Type == model.AccessControlPolicyTypePermission && !c.App.Config().FeatureFlags.PermissionPolicies { c.Err = model.NewAppError("createAccessControlPolicy", "api.access_control_policy.permission_policies.feature_disabled", nil, "", http.StatusNotImplemented) return } // Channel-scope policies are always available, but a channel policy // that carries a permission-rule action (upload_file_attachment, // download_file_attachment) is gated behind the channel-level // sub-flag — that's the toggle that exposes the Channel Settings → // Permissions Policy tab on the frontend. Membership-only channel // policies stay unaffected. Helper enforces the PermissionPolicies // umbrella too, so a request slipping in with the sub-flag on but // the umbrella off is also rejected here. if policy.Type == model.AccessControlPolicyTypeChannel && policy.HasPermissionRuleAction() && !c.App.Config().FeatureFlags.IsChannelPermissionPoliciesEnabled() { c.Err = model.NewAppError("createAccessControlPolicy", "api.access_control_policy.channel_permission_policies.feature_disabled", nil, "", http.StatusNotImplemented) return } auditRec := c.MakeAuditRecord(model.AuditEventCreateAccessControlPolicy, model.AuditStatusFail) defer c.LogAuditRec(auditRec) model.AddEventParameterAuditableToAuditRec(auditRec, "requested", &policy) // Sysadmin is allowed in every case; each case layers its own non-sysadmin fallback. hasManageSystem := c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) switch policy.Type { case model.AccessControlPolicyTypeParent: teamID := r.URL.Query().Get("team_id") if !hasManageSystem { if teamID == "" || !model.IsValidId(teamID) { c.SetPermissionError(model.PermissionManageSystem) return } if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamID, model.PermissionManageTeamAccessRules) { c.SetPermissionError(model.PermissionManageTeamAccessRules) return } // For updates, verify the team admin owns the policy if policy.ID != "" { owned, appErr := c.App.ValidateTeamAdminPolicyOwnership(c.AppContext, teamID, policy.ID) if appErr != nil { c.Err = appErr return } if !owned { c.SetPermissionError(model.PermissionManageTeamAccessRules) return } } } // Scope stamping: for team admins never trust the body's scope fields — always // override from the authenticated query param so a crafted request cannot assign // the policy to a different team. For system admins, inject only when the body // did not supply scope (preserves the existing sysadmin-sets-scope-explicitly path). if !hasManageSystem { policy.Scope = model.AccessControlPolicyScopeTeam policy.ScopeID = teamID } else if teamID != "" && model.IsValidId(teamID) && policy.Scope == "" { policy.Scope = model.AccessControlPolicyScopeTeam policy.ScopeID = teamID } case model.AccessControlPolicyTypePermission: if !hasManageSystem { c.SetPermissionError(model.PermissionManageSystem) return } case model.AccessControlPolicyTypeChannel: if !hasManageSystem { // For non-system admins, check channel-specific permission if !model.IsValidId(policy.ID) { c.SetInvalidParam("policy.id") return } hasChannelPermission, _ := c.App.HasPermissionToChannel(c.AppContext, c.AppContext.Session().UserId, policy.ID, model.PermissionManageChannelAccessRules) if !hasChannelPermission { c.SetPermissionError(model.PermissionManageChannelAccessRules) return } // Now do the full validation (channel exists, is private, etc.) if appErr := c.App.ValidateChannelAccessControlPolicyCreation(c.AppContext, c.AppContext.Session().UserId, &policy); appErr != nil { c.Err = appErr return } if appErr := preserveSystemManagedFields(c, &policy); appErr != nil { c.Err = appErr return } } case model.AccessControlPolicyTypeTeam: // Team-type policies are keyed by the team ID, so policy.ID is the team. if !hasManageSystem { if !model.IsValidId(policy.ID) { c.SetInvalidParam("policy.id") return } if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), policy.ID, model.PermissionManageTeamAccessRules) { c.SetPermissionError(model.PermissionManageTeamAccessRules) return } // Hard-block a team admin from saving rules that would lock themselves // out of their own team. for _, rule := range policy.Rules { if appErr := c.App.ValidateTeamAdminSelfInclusion(c.AppContext, c.AppContext.Session().UserId, rule.Expression); appErr != nil { c.Err = appErr return } } if appErr := preserveSystemManagedFields(c, &policy); appErr != nil { c.Err = appErr return } } default: c.SetInvalidParam("type") return } np, appErr := c.App.CreateOrUpdateAccessControlPolicy(c.AppContext, &policy) if appErr != nil { c.Err = appErr return } auditRec.Success() auditRec.AddEventObjectType("access_control_policy") auditRec.AddEventResultState(np) if shouldRedactExpressions(c) { c.App.MaskPolicyExpressions(c.AppContext, np, c.AppContext.Session().UserId) } js, err := json.Marshal(np) if err != nil { c.Err = model.NewAppError("createAccessControlPolicy", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err) return } if _, err := w.Write(js); err != nil { c.Logger.Warn("Error while writing response", mlog.Err(err)) } } func getAccessControlPolicy(c *Context, w http.ResponseWriter, r *http.Request) { c.RequirePolicyId() if c.Err != nil { return } policyID := c.Params.PolicyId // Extract optional channelId from query parameters for context channelID := r.URL.Query().Get("channelId") hasSystemPermission := c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) if !hasSystemPermission { // Try delegated admin permissions: channel admin first, then team admin channelPermErr := c.App.ValidateAccessControlPolicyPermissionWithChannelContext(c.AppContext, c.AppContext.Session().UserId, policyID, true, channelID) if channelPermErr != nil { // Try team-admin permission teamID := r.URL.Query().Get("team_id") if teamID != "" && model.IsValidId(teamID) && c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamID, model.PermissionManageTeamAccessRules) { owned, appErr := c.App.ValidateTeamAdminPolicyOwnership(c.AppContext, teamID, policyID) if appErr != nil { c.Err = appErr return } if !owned { c.SetPermissionError(model.PermissionManageTeamAccessRules) return } } else { c.SetPermissionError(model.PermissionManageSystem) return } } } policy, appErr := c.App.GetAccessControlPolicy(c.AppContext, policyID) if appErr != nil { c.Err = appErr return } // Stamp assigned-resource counts (channel_count, team_count, child_ids) so the // policy editor can disable deletion while any channel or team is still linked, // mirroring the policy list. GetAccessControlPolicy does not compute these. c.App.PopulateAccessControlPolicyChildCounts(c.AppContext, policy) if shouldRedactExpressions(c) { c.App.MaskPolicyExpressions(c.AppContext, policy, c.AppContext.Session().UserId) } js, err := json.Marshal(policy) if err != nil { c.Err = model.NewAppError("getAccessControlPolicy", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err) return } if _, err := w.Write(js); err != nil { c.Logger.Warn("Error while writing response", mlog.Err(err)) } } func deleteAccessControlPolicy(c *Context, w http.ResponseWriter, r *http.Request) { c.RequirePolicyId() if c.Err != nil { return } policyID := c.Params.PolicyId auditRec := c.MakeAuditRecord(model.AuditEventDeleteAccessControlPolicy, model.AuditStatusFail) defer c.LogAuditRec(auditRec) model.AddEventParameterToAuditRec(auditRec, "id", policyID) hasSystemPermission := c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) if !hasSystemPermission { // Try delegated admin permissions: channel admin first, then team admin channelPermErr := c.App.ValidateAccessControlPolicyPermission(c.AppContext, c.AppContext.Session().UserId, policyID) if channelPermErr != nil { teamID := r.URL.Query().Get("team_id") if teamID == "" || !model.IsValidId(teamID) { c.SetPermissionError(model.PermissionManageSystem) return } if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamID, model.PermissionManageTeamAccessRules) { c.SetPermissionError(model.PermissionManageTeamAccessRules) return } // Team-type policies use the team's own ID as the policy ID. // A team admin can delete their team's direct policy without the // parent-ownership check (which only covers parent-type policies). if policyID != teamID { owned, appErr := c.App.ValidateTeamAdminPolicyOwnership(c.AppContext, teamID, policyID) if appErr != nil { c.Err = appErr return } if !owned { c.SetPermissionError(model.PermissionManageTeamAccessRules) return } } } } appErr := c.App.DeleteAccessControlPolicy(c.AppContext, policyID) if appErr != nil { c.Err = appErr return } auditRec.Success() // Middleware forces a JSON content-type, so an empty 200 body fails the // client's parse and surfaces a spurious error on a successful delete. ReturnStatusOK(w) } func checkExpression(c *Context, w http.ResponseWriter, r *http.Request) { checkExpressionRequest := struct { Expression string `json:"expression"` ChannelId string `json:"channelId,omitempty"` TeamId string `json:"teamId,omitempty"` }{} if jsonErr := json.NewDecoder(r.Body).Decode(&checkExpressionRequest); jsonErr != nil { c.SetInvalidParamWithErr("user", jsonErr) return } channelId := checkExpressionRequest.ChannelId if channelId != "" && !model.IsValidId(channelId) { c.SetInvalidParam("channelId") return } hasSystemPermission := c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) if !hasSystemPermission { if !teamAdminCELContextOK(c, channelId, checkExpressionRequest.TeamId) { if channelId == "" { c.SetPermissionError(model.PermissionManageSystem) return } hasChannelPermission, _ := c.App.HasPermissionToChannel(c.AppContext, c.AppContext.Session().UserId, channelId, model.PermissionManageChannelAccessRules) if !hasChannelPermission { c.SetPermissionError(model.PermissionManageChannelAccessRules) return } } } errs, appErr := c.App.CheckExpression(c.AppContext, checkExpressionRequest.Expression) if appErr != nil { c.Err = appErr return } js, err := json.Marshal(errs) if err != nil { c.Err = model.NewAppError("checkExpression", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err) return } if _, err := w.Write(js); err != nil { c.Logger.Warn("Error while writing response", mlog.Err(err)) } } func testExpression(c *Context, w http.ResponseWriter, r *http.Request) { var checkExpressionRequest model.QueryExpressionParams if jsonErr := json.NewDecoder(r.Body).Decode(&checkExpressionRequest); jsonErr != nil { c.SetInvalidParamWithErr("user", jsonErr) return } channelId := checkExpressionRequest.ChannelId if channelId != "" && !model.IsValidId(channelId) { c.SetInvalidParam("channelId") return } teamID := checkExpressionRequest.TeamId if teamID != "" && !model.IsValidId(teamID) { c.SetInvalidParam("teamId") return } hasSystemPermission := c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) hasTeamPermission := !hasSystemPermission && teamAdminCELContextOK(c, channelId, teamID) if !hasSystemPermission && !hasTeamPermission { if channelId == "" { c.SetPermissionError(model.PermissionManageSystem) return } hasChannelPermission, _ := c.App.HasPermissionToChannel(c.AppContext, c.AppContext.Session().UserId, channelId, model.PermissionManageChannelAccessRules) if !hasChannelPermission { c.SetPermissionError(model.PermissionManageChannelAccessRules) return } } var users []*model.User var count int64 var appErr *model.AppError searchOpts := model.SubjectSearchOptions{ Term: checkExpressionRequest.Term, Limit: checkExpressionRequest.Limit, Cursor: model.SubjectCursor{ TargetID: checkExpressionRequest.After, }, // Carry the channel so a resource.attributes.* expression resolves // against that channel's values; ignored for resource-free expressions. ResourceID: channelId, } // Scope results to a team's current members only for callers authorized on // that team. A channel admin clears the permission gate via the channel // branch, so binding the team scope to teamID alone would let them point the // search at any team — gate it on the verified team permission instead. if hasSystemPermission || hasTeamPermission { searchOpts.TeamID = teamID } // Only system admins see ALL matching users (unrestricted). // Delegated admins (team and channel) see only users matching expressions with attributes they possess. if hasSystemPermission { users, count, appErr = c.App.TestExpression(c.AppContext, checkExpressionRequest.Expression, searchOpts) } else if hasTeamPermission { users, count, appErr = c.App.TestExpressionWithTeamContext(c.AppContext, checkExpressionRequest.Expression, searchOpts) } else { users, count, appErr = c.App.TestExpressionWithChannelContext(c.AppContext, checkExpressionRequest.Expression, searchOpts) } if appErr != nil { c.Err = appErr return } for _, user := range users { c.App.SanitizeProfile(user, hasSystemPermission) } resp := model.AccessControlPolicyTestResponse{ Users: users, Total: count, } js, err := json.Marshal(resp) if err != nil { c.Err = model.NewAppError("checkExpression", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err) return } if _, err := w.Write(js); err != nil { c.Logger.Warn("Error while writing response", mlog.Err(err)) } } // validateTeamIdsExist returns a 400 if any entry is not a well-formed, existing // team id. GetTeams silently drops unknown ids (partial list) or returns 404 (all // invalid), so the handler must confirm existence itself to reject bad input here. func validateTeamIdsExist(c *Context, teamIDs []string) *model.AppError { if len(teamIDs) == 0 { return nil } for _, teamID := range teamIDs { if !model.IsValidId(teamID) { return model.NewAppError("validateTeamIdsExist", "api.context.invalid_body_param.app_error", map[string]any{"Name": "team_ids"}, "", http.StatusBadRequest) } } // One batch lookup instead of a GetTeam per id. GetTeams returns 404 only when // every id is unknown (treated as a bad request here) and otherwise drops // unknown ids from a partial list, so confirm each requested id is present. teams, appErr := c.App.GetTeams(teamIDs) if appErr != nil { if appErr.StatusCode == http.StatusNotFound { return model.NewAppError("validateTeamIdsExist", "api.context.invalid_body_param.app_error", map[string]any{"Name": "team_ids"}, "", http.StatusBadRequest) } return appErr } found := make(map[string]bool, len(teams)) for _, team := range teams { found[team.Id] = true } for _, teamID := range teamIDs { if !found[teamID] { return model.NewAppError("validateTeamIdsExist", "api.context.invalid_body_param.app_error", map[string]any{"Name": "team_ids"}, "", http.StatusBadRequest) } } return nil } // teamAdminCELContextOK reports whether the session may use the delegated // team-admin shortcut for CEL tooling: valid team_id, ManageTeamAccessRules on // that team, and when a channel_id is supplied it must resolve to a channel in // that same team. Prevents pairing a team the admin manages with an unrelated // channel solely to satisfy the channel branch of auth. func teamAdminCELContextOK(c *Context, channelID, teamID string) bool { if teamID == "" || !model.IsValidId(teamID) { return false } if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamID, model.PermissionManageTeamAccessRules) { return false } if channelID == "" { return true } if !model.IsValidId(channelID) { return false } channel, appErr := c.App.GetChannel(c.AppContext, channelID) if appErr != nil { return false } return channel.TeamId == teamID } // authorizeSimulatePolicy checks the caller's permission to simulate a // policy and returns whether they have system-level access — used by // the caller to scope SanitizeProfile. // // Authorization order: // - system admin: always. // - team admin: only when teamID is set AND any provided channelID // resolves to a channel in that team. Without this guard a team // admin could simulate a policy for any channel by pairing their // team_id with a foreign channel_id; the cross-team check forces // the auth to fall through to HasPermissionToChannel for any // channel outside the admin's team. // - channel admin: when channelID is set, via HasPermissionToChannel // (which already covers the channel's actual team admins). // // On failure the function sets the appropriate permission error on `c` // and returns ok=false. Callers MUST early-return when ok=false. func authorizeSimulatePolicy(c *Context, channelID, teamID string) (hasSystemPermission bool, ok bool) { hasSystemPermission = c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) if hasSystemPermission { return true, true } if teamAdminCELContextOK(c, channelID, teamID) { return false, true } if channelID == "" { c.SetPermissionError(model.PermissionManageSystem) return false, false } hasChannelPermission, _ := c.App.HasPermissionToChannel(c.AppContext, c.AppContext.Session().UserId, channelID, model.PermissionManageChannelAccessRules) if !hasChannelPermission { c.SetPermissionError(model.PermissionManageChannelAccessRules) return false, false } return false, true } // simulatePolicyForUsers runs the dual-lane PDP simulation against a draft // policy (not persisted) plus any higher-scoped persisted permission // policies, for an explicit set of user IDs (with optional per-user session // attribute overrides). The response carries per-user, per-action // ALLOW/DENY decisions plus blame attribution for any deny — used by the // "Simulate access" picker UX in the System Console and Channel Settings. // // Permission gates: // - System admins: full access. // - Team admins (with PermissionManageTeamAccessRules on the team): when a // team_id is present in the body and any provided channel_id resolves // to a channel in that team. // - Channel admins (with PermissionManageChannelAccessRules on the // channel): when a channel_id is present in the body. // // Non-system admins may only simulate users who belong to the request's // channel (when channel_id is set) or team (team-scoped simulation). // The endpoint requires the PolicySimulation feature flag (which // itself depends on the PermissionPolicies umbrella) and an // Enterprise Advanced license. Returns 501 when ABAC is unavailable. func simulatePolicyForUsers(c *Context, w http.ResponseWriter, r *http.Request) { if !c.App.Config().FeatureFlags.IsPolicySimulationEnabled() { c.Err = model.NewAppError("simulatePolicyForUsers", "api.access_control_policy.policy_simulation.feature_disabled", nil, "", http.StatusNotImplemented) return } var params model.PolicySimulationByUsersParams if jsonErr := json.NewDecoder(r.Body).Decode(¶ms); jsonErr != nil { c.SetInvalidParamWithErr("simulation", jsonErr) return } if params.Policy == nil { c.SetInvalidParam("policy") return } if len(params.Users) == 0 { c.SetInvalidParam("users") return } if params.ChannelID != "" && !model.IsValidId(params.ChannelID) { c.SetInvalidParam("channel_id") return } if params.TeamID != "" && !model.IsValidId(params.TeamID) { c.SetInvalidParam("team_id") return } switch params.EvaluationScope { case "", model.PolicyEvaluationScopeThisRule, model.PolicyEvaluationScopeAll: default: c.SetInvalidParam("evaluation_scope") return } // Normalize the empty string up front to the default if params.EvaluationScope == "" { params.EvaluationScope = model.PolicyEvaluationScopeThisRule } hasSystemPermission, ok := authorizeSimulatePolicy(c, params.ChannelID, params.TeamID) if !ok { return } // Cross-team consistency check: when both IDs are provided, the // channel must actually belong to the named team. authorizeSimulatePolicy // covers this for the team-admin shortcut, but a system admin's auth // short-circuit happens earlier so we re-check here for everyone. // Mismatched IDs would otherwise let downstream user-scope validation // run against the wrong team. We canonicalise params.TeamID from the // channel rather than rejecting outright — the channel ID is the // authoritative scope for a channel-policy simulation. if params.ChannelID != "" && params.TeamID != "" { channel, appErr := c.App.GetChannel(c.AppContext, params.ChannelID) if appErr != nil { c.Err = appErr return } if channel.TeamId != params.TeamID { c.SetInvalidParam("team_id") return } params.TeamID = channel.TeamId } if !hasSystemPermission { if appErr := c.App.ValidatePolicySimulationUsersInScope(c.AppContext, params.TeamID, params.ChannelID, params.Users); appErr != nil { c.Err = appErr return } } resp, appErr := c.App.SimulateAccessControlPolicyForUsers(c.AppContext, params) if appErr != nil { c.Err = appErr return } for i := range resp.Results { c.App.SanitizeProfile(resp.Results[i].User, hasSystemPermission) } // Redact protected CPA attribute values for non-system-admin // callers. Targets the user's actual attribute values shown in // the Decision Details panel and per-leaf ActualValue strings. c.App.RedactSimulationAttributesForCaller(c.AppContext, resp, hasSystemPermission) // Sanitize evaluation traces for non-system-admin callers. // Traces are sysadmin-only; channel/team admins get flat // expressions and the frontend falls back when trees are absent. c.App.SanitizeSimulationEvaluationTracesForCaller(resp, hasSystemPermission) js, err := json.Marshal(resp) if err != nil { c.Err = model.NewAppError("simulatePolicyForUsers", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err) return } if _, err := w.Write(js); err != nil { c.Logger.Warn("Error while writing response", mlog.Err(err)) } } func validateExpressionAgainstRequester(c *Context, w http.ResponseWriter, r *http.Request) { var request struct { Expression string `json:"expression"` ChannelId string `json:"channelId,omitempty"` TeamId string `json:"teamId,omitempty"` } if jsonErr := json.NewDecoder(r.Body).Decode(&request); jsonErr != nil { c.SetInvalidParamWithErr("request", jsonErr) return } channelId := request.ChannelId if channelId != "" && !model.IsValidId(channelId) { c.SetInvalidParam("channelId") return } teamID := request.TeamId if teamID != "" && !model.IsValidId(teamID) { c.SetInvalidParam("teamId") return } hasSystemPermission := c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) if !hasSystemPermission { if !teamAdminCELContextOK(c, channelId, teamID) { if channelId == "" { c.SetPermissionError(model.PermissionManageSystem) return } hasChannelPermission, _ := c.App.HasPermissionToChannel(c.AppContext, c.AppContext.Session().UserId, channelId, model.PermissionManageChannelAccessRules) if !hasChannelPermission { c.SetPermissionError(model.PermissionManageChannelAccessRules) return } } } // Direct validation against requester matches, appErr := c.App.ValidateExpressionAgainstRequester(c.AppContext, request.Expression, c.AppContext.Session().UserId) if appErr != nil { c.Err = appErr return } response := struct { RequesterMatches bool `json:"requester_matches"` }{ RequesterMatches: matches, } w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(response); err != nil { c.Logger.Warn("Error while writing response", mlog.Err(err)) } } func searchAccessControlPolicies(c *Context, w http.ResponseWriter, r *http.Request) { var props *model.AccessControlPolicySearch err := json.NewDecoder(r.Body).Decode(&props) if err != nil || props == nil { c.SetInvalidParamWithErr("access_control_policy_search", err) return } permissionPoliciesEnabled := c.App.Config().FeatureFlags.PermissionPolicies var policies []*model.AccessControlPolicy var total int64 var appErr *model.AppError if props.TeamID != "" { // Team-scoped search: requires manage_team_access_rules (system admins have this implicitly) if !model.IsValidId(props.TeamID) { c.SetInvalidParam("team_id") return } if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), props.TeamID, model.PermissionManageTeamAccessRules) { c.SetPermissionError(model.PermissionManageTeamAccessRules) return } requesterID := c.AppContext.Session().UserId policies, total, appErr = c.App.SearchTeamAccessPolicies(c.AppContext, props.TeamID, requesterID, *props) } else { if props.Type == model.AccessControlPolicyTypePermission && !permissionPoliciesEnabled { c.Err = model.NewAppError("searchAccessControlPolicies", "api.access_control_policy.permission_policies.feature_disabled", nil, "", http.StatusNotImplemented) return } // System-wide search: requires system permission if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) { c.SetPermissionError(model.PermissionManageSystem) return } policies, total, appErr = c.App.SearchAccessControlPolicies(c.AppContext, *props) } if appErr != nil { c.Err = appErr return } if !permissionPoliciesEnabled && props.Type == "" { filtered := make([]*model.AccessControlPolicy, 0, len(policies)) for _, p := range policies { if p.Type != model.AccessControlPolicyTypePermission { filtered = append(filtered, p) } } total -= int64(len(policies) - len(filtered)) policies = filtered } if shouldRedactExpressions(c) { for _, p := range policies { c.App.MaskPolicyExpressions(c.AppContext, p, c.AppContext.Session().UserId) } } result := model.AccessControlPoliciesWithCount{ Policies: policies, Total: total, } js, err := json.Marshal(result) if err != nil { c.Err = model.NewAppError("searchAccessControlPolicies", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err) return } if _, err := w.Write(js); err != nil { c.Logger.Warn("Error while writing response", mlog.Err(err)) } } // updateActiveStatus updates the active status of a single access control policy. // // Deprecated: This endpoint is deprecated and will be removed in a future release. // Use PUT /api/v4/access_control/policies/activate instead, which supports batch updates. func updateActiveStatus(c *Context, w http.ResponseWriter, r *http.Request) { c.RequirePolicyId() if c.Err != nil { return } // CSRF barrier: only allow header-based auth (reject cookie sessions) token, tokenLocation := app.ParseAuthTokenFromRequest(r) if token == "" || tokenLocation == app.TokenLocationCookie { c.Err = model.NewAppError("updateActiveStatus", "api.context.session_cookie_not_allowed.app_error", nil, "This endpoint requires header-based authentication", http.StatusUnauthorized) return } policyID := c.Params.PolicyId // Check if user has system admin permission OR channel-specific permission for this policy hasManageSystemPermission := c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) if !hasManageSystemPermission { // For non-system admins, validate policy access permission if appErr := c.App.ValidateAccessControlPolicyPermission(c.AppContext, c.AppContext.Session().UserId, policyID); appErr != nil { c.SetPermissionError(model.PermissionManageSystem) return } } auditRec := c.MakeAuditRecord(model.AuditEventUpdateActiveStatus, model.AuditStatusFail) defer c.LogAuditRec(auditRec) model.AddEventParameterToAuditRec(auditRec, "id", policyID) active := r.URL.Query().Get("active") if active != "true" && active != "false" { c.SetInvalidParam("active") return } activeBool, err := strconv.ParseBool(active) if err != nil { c.SetInvalidParamWithErr("active", err) return } model.AddEventParameterToAuditRec(auditRec, "active", activeBool) // Wrap single update in slice to use the batch update method updates := []model.AccessControlPolicyActiveUpdate{ {ID: policyID, Active: activeBool}, } _, appErr := c.App.UpdateAccessControlPoliciesActive(c.AppContext, updates) if appErr != nil { c.Err = appErr return } auditRec.Success() // Return success response response := map[string]any{ "status": "OK", } // Set deprecation header to inform clients w.Header().Set("Deprecation", "true") w.Header().Set("Link", "; rel=\"successor-version\"") w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(response); err != nil { c.Logger.Warn("Error while writing response", mlog.Err(err)) } } func setActiveStatus(c *Context, w http.ResponseWriter, r *http.Request) { var list model.AccessControlPolicyActiveUpdateRequest if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { c.SetInvalidParamWithErr("request", jsonErr) return } auditRec := c.MakeAuditRecord(model.AuditEventSetActiveStatus, model.AuditStatusFail) defer c.LogAuditRec(auditRec) model.AddEventParameterAuditableToAuditRec(auditRec, "requested", &list) hasSystemPermission := c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) if !hasSystemPermission { for _, entry := range list.Entries { // Try delegated admin permissions: channel admin first, then team admin channelPermErr := c.App.ValidateAccessControlPolicyPermission(c.AppContext, c.AppContext.Session().UserId, entry.ID) if channelPermErr != nil { // Try team-admin permission via team_id in request if list.TeamID != "" && model.IsValidId(list.TeamID) && c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), list.TeamID, model.PermissionManageTeamAccessRules) { // Team-type policies use the team's own ID as the policy ID. // Skip the parent-ownership check for a team admin toggling their // own team policy's active flag. if entry.ID != list.TeamID { owned, appErr := c.App.ValidateTeamAdminPolicyOwnership(c.AppContext, list.TeamID, entry.ID) if appErr != nil { c.Err = appErr return } if !owned { c.SetPermissionError(model.PermissionManageTeamAccessRules) return } } } else { c.SetPermissionError(model.PermissionManageChannelAccessRules) return } } } } policies, appErr := c.App.UpdateAccessControlPoliciesActive(c.AppContext, list.Entries) if appErr != nil { c.Err = appErr return } auditRec.Success() if shouldRedactExpressions(c) { for _, p := range policies { c.App.MaskPolicyExpressions(c.AppContext, p, c.AppContext.Session().UserId) } } w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(policies); err != nil { c.Logger.Warn("Error while writing response", mlog.Err(err)) } } func assignAccessPolicy(c *Context, w http.ResponseWriter, r *http.Request) { c.RequirePolicyId() if c.Err != nil { return } policyID := c.Params.PolicyId var assignments struct { ChannelIds []string `json:"channel_ids"` TeamID string `json:"team_id"` TeamIds []string `json:"team_ids"` } err := json.NewDecoder(r.Body).Decode(&assignments) if err != nil { c.SetInvalidParamWithErr("assignments", err) return } // Team-resource assignment ships behind its own dark-launch flag. Reject it // outright when team membership ABAC is not fully enabled so inert team child // policy rows can't be created and later go live when the flag is flipped on. if len(assignments.TeamIds) != 0 && !c.App.TeamMembershipAccessControlEnabled() { c.Err = model.NewAppError("assignAccessPolicy", "api.access_control_policy.team_membership.feature_disabled", nil, "", http.StatusNotImplemented) return } hasSystemPermission := c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) // Assigning a parent policy to a team resource is a system-level operation — // distinct from the team-admin channel_ids+team_id path below. if len(assignments.TeamIds) != 0 && !hasSystemPermission { c.SetPermissionError(model.PermissionManageSystem) return } if !hasSystemPermission { if assignments.TeamID == "" || !model.IsValidId(assignments.TeamID) { c.SetPermissionError(model.PermissionManageSystem) return } if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), assignments.TeamID, model.PermissionManageTeamAccessRules) { c.SetPermissionError(model.PermissionManageTeamAccessRules) return } // Policy must be team-scoped to this team. Channel validation below ensures all // channels belong to the requesting team regardless. owned, ownershipErr := c.App.ValidateTeamAdminPolicyOwnership(c.AppContext, assignments.TeamID, policyID) if ownershipErr != nil { c.Err = ownershipErr return } if !owned { c.SetPermissionError(model.PermissionManageTeamAccessRules) return } if appErr := c.App.ValidateTeamScopePolicyChannelAssignment(c.AppContext, assignments.TeamID, assignments.ChannelIds); appErr != nil { c.Err = appErr return } } auditRec := c.MakeAuditRecord(model.AuditEventAssignAccessPolicy, model.AuditStatusFail) defer c.LogAuditRec(auditRec) model.AddEventParameterToAuditRec(auditRec, "id", policyID) model.AddEventParameterToAuditRec(auditRec, "team_id", assignments.TeamID) model.AddEventParameterToAuditRec(auditRec, "channel_ids", assignments.ChannelIds) model.AddEventParameterToAuditRec(auditRec, "team_ids", assignments.TeamIds) // The app layer's GetTeams silently drops unknown IDs from a partial list and // returns 404 for an all-invalid one, so neither surfaces a usable 400. Validate // existence here so any unknown team_ids entry is rejected as a bad request. if appErr := validateTeamIdsExist(c, assignments.TeamIds); appErr != nil { c.Err = appErr return } if len(assignments.ChannelIds) != 0 { _, appErr := c.App.AssignAccessControlPolicyToChannels(c.AppContext, policyID, assignments.ChannelIds) if appErr != nil { c.Err = appErr return } } if len(assignments.TeamIds) != 0 { _, appErr := c.App.AssignAccessControlPolicyToTeams(c.AppContext, policyID, assignments.TeamIds) if appErr != nil { c.Err = appErr return } } // Reconcile team scope: if all channels now belong to one team, set scope; // if channels span multiple teams (e.g. system admin added cross-team), clear scope. if appErr := c.App.ReconcilePolicyTeamScope(c.AppContext, policyID); appErr != nil { c.Logger.Warn("Failed to reconcile policy team scope after assign", mlog.String("policy_id", policyID), mlog.Err(appErr)) } auditRec.Success() ReturnStatusOK(w) } func unassignAccessPolicy(c *Context, w http.ResponseWriter, r *http.Request) { c.RequirePolicyId() if c.Err != nil { return } policyID := c.Params.PolicyId var assignments struct { ChannelIds []string `json:"channel_ids"` TeamID string `json:"team_id"` TeamIds []string `json:"team_ids"` } err := json.NewDecoder(r.Body).Decode(&assignments) if err != nil { c.SetInvalidParamWithErr("assignments", err) return } // Mirror the assign path: team-resource unassignment is gated by the same // dark-launch flag so the feature can be rolled back cleanly. if len(assignments.TeamIds) != 0 && !c.App.TeamMembershipAccessControlEnabled() { c.Err = model.NewAppError("unassignAccessPolicy", "api.access_control_policy.team_membership.feature_disabled", nil, "", http.StatusNotImplemented) return } hasSystemPermission := c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) if len(assignments.TeamIds) != 0 && !hasSystemPermission { c.SetPermissionError(model.PermissionManageSystem) return } if !hasSystemPermission { if assignments.TeamID == "" || !model.IsValidId(assignments.TeamID) { c.SetPermissionError(model.PermissionManageSystem) return } if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), assignments.TeamID, model.PermissionManageTeamAccessRules) { c.SetPermissionError(model.PermissionManageTeamAccessRules) return } owned, appErr := c.App.ValidateTeamAdminPolicyOwnership(c.AppContext, assignments.TeamID, policyID) if appErr != nil { c.Err = appErr return } if !owned { c.SetPermissionError(model.PermissionManageTeamAccessRules) return } if appErr := c.App.ValidateTeamScopePolicyChannelAssignment(c.AppContext, assignments.TeamID, assignments.ChannelIds); appErr != nil { c.Err = appErr return } } auditRec := c.MakeAuditRecord(model.AuditEventUnassignAccessPolicy, model.AuditStatusFail) defer c.LogAuditRec(auditRec) model.AddEventParameterToAuditRec(auditRec, "id", policyID) model.AddEventParameterToAuditRec(auditRec, "team_id", assignments.TeamID) model.AddEventParameterToAuditRec(auditRec, "channel_ids", assignments.ChannelIds) model.AddEventParameterToAuditRec(auditRec, "team_ids", assignments.TeamIds) if appErr := validateTeamIdsExist(c, assignments.TeamIds); appErr != nil { c.Err = appErr return } // Pre-flight: ensure scope is set before removing channels. This handles // pre-scope policies (created before the scope field existed) by capturing // the team from current channels before they are removed. Without this, // unassigning all channels would leave the policy with no scope and no // channels, making it unmanageable by team admins. if appErr := c.App.ReconcilePolicyTeamScope(c.AppContext, policyID); appErr != nil { c.Logger.Warn("Failed to reconcile policy team scope before unassign", mlog.String("policy_id", policyID), mlog.Err(appErr)) } if len(assignments.ChannelIds) != 0 { appErr := c.App.UnassignPoliciesFromChannels(c.AppContext, policyID, assignments.ChannelIds) if appErr != nil { c.Err = appErr return } } if len(assignments.TeamIds) != 0 { if appErr := c.App.UnassignPoliciesFromTeams(c.AppContext, policyID, assignments.TeamIds); appErr != nil { c.Err = appErr return } } // Post-unassign reconcile: update scope based on remaining channels. // If channels remain in one team → scope set. Multiple teams → scope cleared. // No channels remain → no-op (scope preserved from pre-flight or creation). if appErr := c.App.ReconcilePolicyTeamScope(c.AppContext, policyID); appErr != nil { c.Logger.Warn("Failed to reconcile policy team scope after unassign", mlog.String("policy_id", policyID), mlog.Err(appErr)) } auditRec.Success() ReturnStatusOK(w) } func getChannelsForAccessControlPolicy(c *Context, w http.ResponseWriter, r *http.Request) { c.RequirePolicyId() if c.Err != nil { return } policyID := c.Params.PolicyId hasSystemPermission := c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) if !hasSystemPermission { teamID := r.URL.Query().Get("team_id") if teamID != "" && model.IsValidId(teamID) && c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamID, model.PermissionManageTeamAccessRules) { owned, appErr := c.App.ValidateTeamAdminPolicyOwnership(c.AppContext, teamID, policyID) if appErr != nil { c.Err = appErr return } if !owned { c.SetPermissionError(model.PermissionManageTeamAccessRules) return } } else { c.SetPermissionError(model.PermissionManageSystem) return } } afterID := r.URL.Query().Get("after") if afterID != "" && !model.IsValidId(afterID) { c.SetInvalidParam("after") return } limitStr := r.URL.Query().Get("limit") limit, err := strconv.Atoi(limitStr) if err != nil { c.Err = model.NewAppError("getChannelsForAccessControlPolicy", "api.access_control_policy.get_channels.limit.app_error", nil, "", http.StatusBadRequest).Wrap(err) return } channels, total, appErr := c.App.GetChannelsForPolicy(c.AppContext, policyID, model.AccessControlPolicyCursor{ ID: afterID, }, limit) if appErr != nil { c.Err = appErr return } data := model.ChannelsWithCount{Channels: channels, TotalCount: total} js, err := json.Marshal(data) if err != nil { c.Err = model.NewAppError("getChannelsForAccessControlPolicy", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err) return } if _, err := w.Write(js); err != nil { c.Logger.Warn("Error while writing response", mlog.Err(err)) } } func searchChannelsForAccessControlPolicy(c *Context, w http.ResponseWriter, r *http.Request) { c.RequirePolicyId() if c.Err != nil { return } var authorizedTeamID string hasSystemPermission := c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) if !hasSystemPermission { authorizedTeamID = r.URL.Query().Get("team_id") if authorizedTeamID != "" && model.IsValidId(authorizedTeamID) && c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), authorizedTeamID, model.PermissionManageTeamAccessRules) { owned, appErr := c.App.ValidateTeamAdminPolicyOwnership(c.AppContext, authorizedTeamID, c.Params.PolicyId) if appErr != nil { c.Err = appErr return } if !owned { c.SetPermissionError(model.PermissionManageTeamAccessRules) return } } else { c.SetPermissionError(model.PermissionManageSystem) return } } var props *model.ChannelSearch err := json.NewDecoder(r.Body).Decode(&props) if err != nil || props == nil { c.SetInvalidParamWithErr("channel_search", err) return } policyID := c.Params.PolicyId // For non-system-admins, force the search to only the authorized team teamIds := props.TeamIds if !hasSystemPermission && authorizedTeamID != "" { teamIds = []string{authorizedTeamID} } opts := model.ChannelSearchOpts{ Deleted: props.Deleted, IncludeDeleted: props.IncludeDeleted, ExcludeGroupConstrained: true, TeamIds: teamIds, ParentAccessControlPolicyId: policyID, } channels, total, appErr := c.App.SearchAllChannels(c.AppContext, props.Term, opts) if appErr != nil { c.Err = appErr return } data := model.ChannelsWithCount{Channels: channels, TotalCount: total} channelsJSON, jsonErr := json.Marshal(data) if jsonErr != nil { c.Err = model.NewAppError("searchChannelsInPolicy", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr) return } if _, err := w.Write(channelsJSON); err != nil { c.Logger.Warn("Error while writing response", mlog.Err(err)) } } func getFieldsAutocomplete(c *Context, w http.ResponseWriter, r *http.Request) { channelId := r.URL.Query().Get("channelId") if channelId != "" && !model.IsValidId(channelId) { c.SetInvalidParam("channelId") return } teamID := r.URL.Query().Get("team_id") // Pulls channel-object-type CPA fields — the ones a rule references as // resource.attributes.* — for a caller with no single channel to scope by, // such as an editor for a policy that many channels import. Only meaningful // without a channelId (a channel scope already includes them), and in that // case the permission check below requires either ManageSystem or // team-admin access-rule permission on team_id (via teamAdminCELContextOK). includeResourceFields := r.URL.Query().Get("include_resource_fields") == "true" hasSystemPermission := c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) if !hasSystemPermission { if !teamAdminCELContextOK(c, channelId, teamID) { if channelId == "" { c.SetPermissionError(model.PermissionManageSystem) return } hasChannelPermission, _ := c.App.HasPermissionToChannel(c.AppContext, c.AppContext.Session().UserId, channelId, model.PermissionManageChannelAccessRules) if !hasChannelPermission { c.SetPermissionError(model.PermissionManageChannelAccessRules) return } } } after := r.URL.Query().Get("after") if after != "" && !model.IsValidId(after) { c.SetInvalidParam("after") return } else if after == "" { after = strings.Repeat("0", 26) } limitStr := r.URL.Query().Get("limit") limit, err := strconv.Atoi(limitStr) if err != nil { c.Err = model.NewAppError("getFieldsAutocomplete", "api.access_control_policy.get_fields.limit.app_error", nil, "", http.StatusBadRequest).Wrap(err) return } if limit <= 0 || limit > 100 { c.Err = model.NewAppError("getFieldsAutocomplete", "api.access_control_policy.get_fields.limit.app_error", nil, "", http.StatusBadRequest) return } var ac []*model.PropertyField var appErr *model.AppError ac, appErr = c.App.GetAccessControlFieldsAutocomplete(c.AppContext, channelId, includeResourceFields, after, limit, c.AppContext.Session().UserId) if appErr != nil { c.Err = appErr return } js, err := json.Marshal(ac) if err != nil { c.Err = model.NewAppError("getExpressionAutocomplete", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err) return } if _, err := w.Write(js); err != nil { c.Logger.Warn("Error while writing response", mlog.Err(err)) } } func convertToVisualAST(c *Context, w http.ResponseWriter, r *http.Request) { var cel struct { Expression string `json:"expression"` ChannelId string `json:"channelId,omitempty"` TeamId string `json:"teamId,omitempty"` } if jsonErr := json.NewDecoder(r.Body).Decode(&cel); jsonErr != nil { c.SetInvalidParamWithErr("user", jsonErr) return } channelId := cel.ChannelId if channelId != "" && !model.IsValidId(channelId) { c.SetInvalidParam("channelId") return } hasSystemPermission := c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) if !hasSystemPermission { if !teamAdminCELContextOK(c, channelId, cel.TeamId) { if channelId == "" { c.SetPermissionError(model.PermissionManageSystem) return } hasChannelPermission, _ := c.App.HasPermissionToChannel(c.AppContext, c.AppContext.Session().UserId, channelId, model.PermissionManageChannelAccessRules) if !hasChannelPermission { c.SetPermissionError(model.PermissionManageChannelAccessRules) return } } } var visualAST *model.VisualExpression var appErr *model.AppError // Masking is attribute-based, not permission-based: all admins receive a // filtered AST based on what they themselves hold, regardless of role. if shouldRedactExpressions(c) { visualAST, appErr = c.App.GetMaskedVisualAST(c.AppContext, cel.Expression, c.AppContext.Session().UserId) } else { visualAST, appErr = c.App.ExpressionToVisualAST(c.AppContext, cel.Expression) } if appErr != nil { c.Err = appErr return } b, err := json.Marshal(visualAST) if err != nil { c.Err = model.NewAppError("convertToVisualAST", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err) return } if _, err := w.Write(b); err != nil { c.Logger.Warn("Error while writing response", mlog.Err(err)) } }