mirror of
https://github.com/hanzoai/team.git
synced 2026-09-30 01:57:35 +00:00
* MM-69063 - Add team ABAC model and constants foundation
* Add team ABAC store EXISTS, channel Type retrofit, policy count split, and index migration
* Add team ABAC app layer: access gate, hydrators, assign/unassign, cleanup, and GetTeamMembersToRemove store
* Enforce team membership ABAC on join and hide policy governed teams from non-qualifying users in the directory
* Add team_ids to access policy assign/unassign, expose per-team policy GET, and support abac_match_only for not_in_team user listing
* Add team ABAC client methods, websocket handler, per-team System Console policy UI, and hide policy-governed teams from non-qualifying users
* Make team ABAC mode-aware: advisory on public teams, strict on private, and surface governed private teams to qualifying users in directory listings
* Flag-gate team ABAC mutation/read APIs and fix policy-save error handling, member-removal limit, team-id validation, export, and audit cleanup
* coderabbit feedback; Broadcast team policy enforcement updates on policy create/update and activation, not only on delete
* Update team access control policy schema to allow nullable policies and enhance test cases with channel counts
* Enhance access control policy tests to include team policy search alongside channel policy search
* Add team membership access control feature flag to docker-compose generation
* Implement team access control policy checks and refactor related components
* MM-69100 - add team membership ABAC sync worker with mode-aware removal and auto-add
* Add team ABAC removal/auto-add notifications, cascade audit, and team custom-rules save backend
* Add Team Settings team membership tab with custom-rules editor, auto-add toggle, save confirmation, and mode-flip sync trigger
* Add team access control policy panels, job details team list, and e2e coverage for membership tab and discoverability cards
* ix save double-submit with loading state, team privacy via updateTeamPrivacy, mode-flip count accuracy, sync trigger logic, i18n keys, and e2e test cleanup
* Add team membership policy notices to invite and add-to-team flows, team access control system messages, and a team policy attributes endpoint
* Add team membership recommended tag for qualifying users and team requirements notice in the members modal
* temp
* adjust styling, add new e2e, fix team admin job permissions issues
* e2e clean items created
* fix directory and team integration e2e tests
* Surface team sync results in the Sync Job Details modal via a Teams tab linked from the chained channel job
* code clean up, adjust styling
* Add access control attributes endpoint and enhance E2E tests for team membership
* Enhance team access control by updating permission checks and syncing jobs
* Add API-level enforcement gate tests for ABAC team membership
* Fix formatting in team channel settings component by adding missing semicolon
* Fix formatting in Client4 class by adjusting type annotations for clarity
* revert unwanted changes in package-lock
* Refactor job permission test and simplify error handling in team details component
* Update test assertions and enhance auto-add functionality in team membership tab
* Update onboarding tests to use 'Public Team' card for team access settings
* Refactor AccessSettings and TeamPolicyEditor components to replace allowOpenInviteCheckbox with public and private team buttons, and update access control job dispatching logic.
* Remove redundant state updates for channel assignments in access control policy reducer
* Implement email suppression for team membership notifications and add related tests
* Authorize ABAC team self-join by attribute match instead of the join_private_teams role
* comments clean up
* Gate team privacy type normalization behind active ABAC, keep legacy allow_open_invite-only path otherwise
* adjust job details styles
* Add team membership policy disconnect confirmation, fix linked-policy affected-member count, and polish job details modal
* Combine custom rules with system policy expressions for accurate confirm counts
* Implement child resource count stamping and deletion gating for access control policies
* Enhance team sync job triggering logic for membership changes without auto-add
* Refactor team privacy handling to align with open-directory model; update comments for clarity on allow_open_invite logic.
* Deduplicate team ABAC sync jobs per policy to prevent concurrent runs emitting duplicate DMs and audit records on HA clusters
* fix linter warnings
* Add confirmation for policy removal in TeamDetails tests
* Update delete policy message to include Teams in the warning
* Add linked teams warning to policy deletion and update translations
* Add Auto-add feature for policy selection and team management
- Enhanced PolicySelectionModal to include an Auto-add checkbox for each policy.
- Updated PolicyList to manage Auto-add state and reflect changes in the UI.
- Modified TeamAccessControl to handle Auto-add functionality and persist changes.
- Added tests to ensure Auto-add behavior works as expected in various scenarios.
- Updated translations for Auto-add related strings.
* Add team membership sync footer and enhance job fetching with policy ID
* Refactor team membership policy handling and sync footer implementation
* Add team sync channel cascade test for private channel membership
---------
(cherry picked from commit a582f03570)
Co-authored-by: Pablo Vélez <[email protected]>
493 lines
17 KiB
Go
493 lines
17 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package app
|
|
|
|
import (
|
|
"net/http"
|
|
"sort"
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/mattermost/mattermost/server/public/shared/i18n"
|
|
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
|
"github.com/mattermost/mattermost/server/public/shared/request"
|
|
)
|
|
|
|
const (
|
|
teamPoliciesDefaultPerPage = 10
|
|
teamPoliciesMaxFetch = 1000
|
|
)
|
|
|
|
// SearchTeamAccessPolicies returns team-scoped parent policies that the requester
|
|
// satisfies. Fetches all matching policies first, applies self-inclusion filtering,
|
|
// then paginates the result to keep totals accurate.
|
|
func (a *App) SearchTeamAccessPolicies(rctx request.CTX, teamID, requesterID string, opts model.AccessControlPolicySearch) ([]*model.AccessControlPolicy, int64, *model.AppError) {
|
|
requestedCursor := opts.Cursor
|
|
requestedLimit := opts.Limit
|
|
if requestedLimit <= 0 {
|
|
requestedLimit = teamPoliciesDefaultPerPage
|
|
}
|
|
|
|
// Fetch policies matching via channel-inference (TeamID filter)
|
|
channelOpts := opts
|
|
channelOpts.TeamID = teamID
|
|
channelOpts.Type = model.AccessControlPolicyTypeParent
|
|
channelOpts.IncludeChildren = true
|
|
channelOpts.Cursor = model.AccessControlPolicyCursor{}
|
|
channelOpts.Limit = teamPoliciesMaxFetch
|
|
|
|
policies, _, appErr := a.SearchAccessControlPolicies(rctx, channelOpts)
|
|
if appErr != nil {
|
|
return nil, 0, appErr
|
|
}
|
|
|
|
// Also fetch policies matching via explicit scope (for channelless policies)
|
|
scopeOpts := opts
|
|
scopeOpts.Type = model.AccessControlPolicyTypeParent
|
|
scopeOpts.Scope = model.AccessControlPolicyScopeTeam
|
|
scopeOpts.ScopeID = teamID
|
|
scopeOpts.IncludeChildren = true
|
|
scopeOpts.Cursor = model.AccessControlPolicyCursor{}
|
|
scopeOpts.Limit = teamPoliciesMaxFetch
|
|
|
|
scopePolicies, _, appErr := a.SearchAccessControlPolicies(rctx, scopeOpts)
|
|
if appErr != nil {
|
|
return nil, 0, appErr
|
|
}
|
|
|
|
// Merge, deduplicating by ID, then sort by ID for stable cursor-based pagination.
|
|
seen := make(map[string]bool, len(policies))
|
|
for _, p := range policies {
|
|
seen[p.ID] = true
|
|
}
|
|
for _, p := range scopePolicies {
|
|
if !seen[p.ID] {
|
|
policies = append(policies, p)
|
|
seen[p.ID] = true
|
|
}
|
|
}
|
|
sort.Slice(policies, func(i, j int) bool {
|
|
return policies[i].ID < policies[j].ID
|
|
})
|
|
|
|
// Single batched Channel lookup for all policies' child_ids so we don't
|
|
// issue GetChannelsByIds once per policy (N+1).
|
|
unionSet := make(map[string]struct{})
|
|
for _, policy := range policies {
|
|
ids, ok := childIDsFromPolicyProps(policy)
|
|
if !ok {
|
|
continue
|
|
}
|
|
for _, id := range ids {
|
|
unionSet[id] = struct{}{}
|
|
}
|
|
}
|
|
union := make([]string, 0, len(unionSet))
|
|
for id := range unionSet {
|
|
union = append(union, id)
|
|
}
|
|
sort.Strings(union)
|
|
|
|
var idToType map[string]model.ChannelType
|
|
batchLookupFailed := false
|
|
if len(union) > 0 {
|
|
channels, err := a.Srv().Store().Channel().GetChannelsByIds(union, true)
|
|
if err != nil {
|
|
rctx.Logger().Warn("Failed to look up child channels for self-inclusion gating batch; keeping the filter on",
|
|
mlog.Err(err),
|
|
mlog.Int("policy_count", len(policies)),
|
|
)
|
|
batchLookupFailed = true
|
|
} else {
|
|
if len(channels) != len(union) {
|
|
rctx.Logger().Warn("Partial batch child-channel lookup for self-inclusion gating",
|
|
mlog.Int("requested", len(union)),
|
|
mlog.Int("returned", len(channels)),
|
|
)
|
|
}
|
|
idToType = make(map[string]model.ChannelType, len(channels))
|
|
for _, ch := range channels {
|
|
idToType[ch.Id] = ch.Type
|
|
}
|
|
}
|
|
} else {
|
|
idToType = map[string]model.ChannelType{}
|
|
}
|
|
|
|
// Filter by self-inclusion. Skip the filter for parent policies whose
|
|
// children are all public channels — public-channel ABAC is advisory and
|
|
// can never lock the requesting admin out, so a non-matching expression
|
|
// is not a reason to hide the policy from them.
|
|
filtered := make([]*model.AccessControlPolicy, 0, len(policies))
|
|
for _, policy := range policies {
|
|
if len(policy.Rules) > 0 && policyAppliesToPrivateChannel(rctx, policy, idToType, batchLookupFailed) {
|
|
expression := policy.Rules[0].Expression
|
|
matches, matchErr := a.ValidateExpressionAgainstRequester(rctx, expression, requesterID)
|
|
if matchErr != nil {
|
|
rctx.Logger().Warn("Failed to validate self-inclusion for policy",
|
|
mlog.String("policy_id", policy.ID), mlog.Err(matchErr))
|
|
continue
|
|
}
|
|
if !matches {
|
|
continue
|
|
}
|
|
}
|
|
filtered = append(filtered, policy)
|
|
}
|
|
|
|
total := int64(len(filtered))
|
|
|
|
// Paginate the filtered results. If the cursor ID was removed by
|
|
// self-inclusion filtering, startIdx stays 0 (resets to first page).
|
|
startIdx := 0
|
|
if !requestedCursor.IsEmpty() {
|
|
for i, p := range filtered {
|
|
if p.ID == requestedCursor.ID {
|
|
startIdx = i + 1
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
endIdx := min(startIdx+requestedLimit, len(filtered))
|
|
|
|
return filtered[startIdx:endIdx], total, nil
|
|
}
|
|
|
|
// ValidateTeamAdminPolicyOwnership checks whether a policy is scoped to a given team.
|
|
// Returns (true, nil) if the policy belongs to the team, (false, nil) if it does not,
|
|
// or (false, err) if an internal error occurred during the check.
|
|
//
|
|
// Checks two mechanisms:
|
|
// 1. Explicit scope: policy has scope=team and scope_id=teamID in Data JSONB
|
|
// 2. Channel inference: all assigned channels belong to this team (pre-scope policies)
|
|
func (a *App) ValidateTeamAdminPolicyOwnership(rctx request.CTX, teamID, policyID string) (bool, *model.AppError) {
|
|
// Check explicit scope first
|
|
scopeResults, _, err := a.Srv().Store().AccessControlPolicy().SearchPolicies(rctx, model.AccessControlPolicySearch{
|
|
IDs: []string{policyID},
|
|
Type: model.AccessControlPolicyTypeParent,
|
|
Scope: model.AccessControlPolicyScopeTeam,
|
|
ScopeID: teamID,
|
|
Limit: 1,
|
|
})
|
|
if err != nil {
|
|
return false, model.NewAppError("ValidateTeamAdminPolicyOwnership",
|
|
"app.team.access_policies.ownership_check.app_error",
|
|
nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
if len(scopeResults) > 0 {
|
|
return true, nil
|
|
}
|
|
|
|
// Fall back to channel-inference for pre-scope policies
|
|
channelResults, _, err := a.Srv().Store().AccessControlPolicy().SearchPolicies(rctx, model.AccessControlPolicySearch{
|
|
IDs: []string{policyID},
|
|
Type: model.AccessControlPolicyTypeParent,
|
|
TeamID: teamID,
|
|
Limit: 1,
|
|
})
|
|
if err != nil {
|
|
return false, model.NewAppError("ValidateTeamAdminPolicyOwnership",
|
|
"app.team.access_policies.ownership_check.app_error",
|
|
nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return len(channelResults) > 0, nil
|
|
}
|
|
|
|
// ReconcilePolicyTeamScope evaluates a parent policy's assigned channels and
|
|
// sets or clears the team scope accordingly:
|
|
// - All channels belong to a single team → scope = "team", scope_id = that team
|
|
// - Channels span multiple teams → scope cleared
|
|
// - No channels remaining → scope unchanged (preserves the team set at creation)
|
|
//
|
|
// This runs after every assign/unassign regardless of the caller's role, so that
|
|
// system admins adding cross-team channels correctly clears the team scope.
|
|
func (a *App) ReconcilePolicyTeamScope(rctx request.CTX, policyID string) *model.AppError {
|
|
// Find all child channel policies for this parent.
|
|
// Limit of 1000 is ok, since In practice a single policy will not have 1000+ channel
|
|
// private children.
|
|
children, _, err := a.Srv().Store().AccessControlPolicy().SearchPolicies(rctx, model.AccessControlPolicySearch{
|
|
ParentID: policyID,
|
|
Type: model.AccessControlPolicyTypeChannel,
|
|
Limit: 1000,
|
|
})
|
|
if err != nil {
|
|
return model.NewAppError("ReconcilePolicyTeamScope",
|
|
"app.team.access_policies.reconcile_scope.app_error",
|
|
nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
// No channels — keep existing scope unchanged
|
|
if len(children) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Collect channel IDs (child policy ID = channel ID)
|
|
channelIDs := make([]string, len(children))
|
|
for i, child := range children {
|
|
channelIDs[i] = child.ID
|
|
}
|
|
|
|
// Resolve channels to determine their teams.
|
|
// If some channels were deleted after being assigned, GetChannels returns
|
|
// fewer results — skip reconciliation to avoid stamping a stale scope.
|
|
channels, appErr := a.GetChannels(rctx, channelIDs)
|
|
if appErr != nil {
|
|
return appErr
|
|
}
|
|
if len(channels) != len(channelIDs) {
|
|
return nil
|
|
}
|
|
|
|
// Check if all channels belong to a single team
|
|
teamIDs := make(map[string]struct{})
|
|
for _, ch := range channels {
|
|
teamIDs[ch.TeamId] = struct{}{}
|
|
}
|
|
|
|
// Fetch the parent policy directly from the store (not the enterprise layer)
|
|
// to avoid unnecessary CEL expression normalization — we only need scope fields.
|
|
policy, storeErr := a.Srv().Store().AccessControlPolicy().Get(rctx, policyID)
|
|
if storeErr != nil {
|
|
return model.NewAppError("ReconcilePolicyTeamScope",
|
|
"app.team.access_policies.reconcile_scope.app_error",
|
|
nil, "", http.StatusInternalServerError).Wrap(storeErr)
|
|
}
|
|
|
|
var newScope, newScopeID string
|
|
if len(teamIDs) == 1 {
|
|
// All channels belong to one team — set scope
|
|
for tid := range teamIDs {
|
|
newScope = model.AccessControlPolicyScopeTeam
|
|
newScopeID = tid
|
|
}
|
|
}
|
|
// len(teamIDs) > 1: multiple teams — clear scope (newScope/newScopeID stay "")
|
|
|
|
// Skip save if nothing changed
|
|
if policy.Scope == newScope && policy.ScopeID == newScopeID {
|
|
return nil
|
|
}
|
|
|
|
policy.Scope = newScope
|
|
policy.ScopeID = newScopeID
|
|
|
|
// Save directly via the store — scope is metadata that doesn't require
|
|
// enterprise-layer CEL normalization. This avoids re-processing expressions
|
|
// and works consistently regardless of the enterprise layer state.
|
|
if _, err := a.Srv().Store().AccessControlPolicy().Save(rctx, policy); err != nil {
|
|
return model.NewAppError("ReconcilePolicyTeamScope",
|
|
"app.team.access_policies.reconcile_scope.app_error",
|
|
nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ValidateTeamScopePolicyChannelAssignment validates that all channels are eligible
|
|
// for team policy assignment, is the gate that run before channels are attached to a 'team' policy:
|
|
// - At least one channel provided
|
|
// - All channels exist
|
|
// - All channels belong to the given team
|
|
// - All channels are public or private (DM/GM excluded)
|
|
// - No group-constrained channels
|
|
// - No shared channels
|
|
// - No team default channels (e.g. town-square)
|
|
func (a *App) ValidateTeamScopePolicyChannelAssignment(rctx request.CTX, teamID string, channelIDs []string) *model.AppError {
|
|
if len(channelIDs) == 0 {
|
|
return model.NewAppError("ValidateTeamScopePolicyChannelAssignment",
|
|
"app.team.access_policies.channels_required.app_error",
|
|
nil, "at least one channel is required", http.StatusBadRequest)
|
|
}
|
|
|
|
channels, appErr := a.GetChannels(rctx, channelIDs)
|
|
if appErr != nil {
|
|
return appErr
|
|
}
|
|
|
|
if len(channels) != len(channelIDs) {
|
|
return model.NewAppError("ValidateTeamScopePolicyChannelAssignment",
|
|
"app.team.access_policies.channel_not_found.app_error",
|
|
nil, "one or more channels not found", http.StatusBadRequest)
|
|
}
|
|
|
|
for _, channel := range channels {
|
|
if channel.TeamId != teamID {
|
|
return model.NewAppError("ValidateTeamScopePolicyChannelAssignment",
|
|
"app.team.access_policies.channel_wrong_team.app_error",
|
|
map[string]any{"ChannelId": channel.Id},
|
|
"channel does not belong to this team", http.StatusBadRequest)
|
|
}
|
|
|
|
if appErr := a.ValidateChannelEligibilityForAccessControl(rctx, channel); appErr != nil {
|
|
return appErr
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ValidateTeamAdminSelfInclusion ensures the requesting admin satisfies the given
|
|
// CEL expression. Returns an error if the admin would be excluded by the expression.
|
|
func (a *App) ValidateTeamAdminSelfInclusion(rctx request.CTX, userID, expression string) *model.AppError {
|
|
if expression == "" {
|
|
return nil
|
|
}
|
|
|
|
matches, appErr := a.ValidateExpressionAgainstRequester(rctx, expression, userID)
|
|
if appErr != nil {
|
|
return model.NewAppError("ValidateTeamAdminSelfInclusion",
|
|
"app.team.access_policies.validation_error.app_error",
|
|
nil, "", http.StatusInternalServerError).Wrap(appErr)
|
|
}
|
|
|
|
if !matches {
|
|
return model.NewAppError("ValidateTeamAdminSelfInclusion",
|
|
"app.team.access_policies.self_exclusion.app_error",
|
|
nil, "policy rules would exclude the requesting admin", http.StatusBadRequest)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// SendTeamAccessControlRemovalNotification DMs the user, from the system bot,
|
|
// that the team's membership policy removed them. The message carries only the
|
|
// team name — never any policy/attribute detail.
|
|
// The systemBot may be pre-resolved by the caller (the sync worker resolves it
|
|
// once per team rather than once per user, since GetSystemBot is not free); pass
|
|
// nil to have it resolved here.
|
|
func (a *App) SendTeamAccessControlRemovalNotification(rctx request.CTX, systemBot *model.Bot, userID string, team *model.Team) *model.AppError {
|
|
locale := ""
|
|
if user, err := a.GetUser(userID); err == nil {
|
|
locale = user.Locale
|
|
}
|
|
T := i18n.GetUserTranslations(locale)
|
|
message := T("api.team.access_control.removed.system_message", map[string]any{"TeamName": team.DisplayName})
|
|
return a.sendTeamAccessControlMembershipDM(rctx, systemBot, userID, team, model.PostTypeAccessControlTeamRemoval, message)
|
|
}
|
|
|
|
// SendTeamAccessControlAdditionNotification audits the auto-add and DMs the
|
|
// user, from the system bot, that they were added to the team because they meet
|
|
// its membership policy. Like the removal DM, it leaks no policy detail. The
|
|
// audit record is always written; only the DM depends on the system bot.
|
|
func (a *App) SendTeamAccessControlAdditionNotification(rctx request.CTX, systemBot *model.Bot, userID string, team *model.Team) *model.AppError {
|
|
rec := a.MakeAuditRecord(rctx, model.AuditEventTeamMembershipAdded, model.AuditStatusSuccess)
|
|
model.AddEventParameterToAuditRec(rec, "user_id", userID)
|
|
model.AddEventParameterToAuditRec(rec, "team_id", team.Id)
|
|
a.LogAuditRec(rctx, rec, nil)
|
|
|
|
locale := ""
|
|
if user, err := a.GetUser(userID); err == nil {
|
|
locale = user.Locale
|
|
}
|
|
T := i18n.GetUserTranslations(locale)
|
|
message := T("api.team.access_control.added.system_message", map[string]any{"TeamName": team.DisplayName})
|
|
return a.sendTeamAccessControlMembershipDM(rctx, systemBot, userID, team, model.PostTypeAccessControlTeamAddition, message)
|
|
}
|
|
|
|
func (a *App) sendTeamAccessControlMembershipDM(rctx request.CTX, systemBot *model.Bot, userID string, team *model.Team, postType, message string) *model.AppError {
|
|
if systemBot == nil {
|
|
var appErr *model.AppError
|
|
if systemBot, appErr = a.GetSystemBot(rctx); appErr != nil {
|
|
return appErr
|
|
}
|
|
}
|
|
|
|
channel, appErr := a.GetOrCreateDirectChannel(rctx, userID, systemBot.UserId)
|
|
if appErr != nil {
|
|
return appErr
|
|
}
|
|
|
|
post := &model.Post{
|
|
ChannelId: channel.Id,
|
|
Message: message,
|
|
Type: postType,
|
|
UserId: systemBot.UserId,
|
|
Props: model.StringInterface{"team_name": team.DisplayName},
|
|
}
|
|
|
|
if _, _, appErr := a.CreatePost(rctx, post, channel, model.CreatePostFlags{SetOnline: true}); appErr != nil {
|
|
return appErr
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// childIDsFromPolicyProps reads Props["child_ids"], which is normally []string
|
|
// (set by the policy store) but may be []any after JSON round-trip.
|
|
// If ok is false, the shape is unsupported and callers should treat it as unknown.
|
|
func childIDsFromPolicyProps(policy *model.AccessControlPolicy) (ids []string, ok bool) {
|
|
if policy == nil || policy.Props == nil {
|
|
return nil, false
|
|
}
|
|
switch raw := policy.Props["child_ids"].(type) {
|
|
case []string:
|
|
return raw, true
|
|
case []any:
|
|
out := make([]string, 0, len(raw))
|
|
for _, item := range raw {
|
|
if id, itemOk := item.(string); itemOk {
|
|
out = append(out, id)
|
|
}
|
|
}
|
|
return out, true
|
|
default:
|
|
return nil, false
|
|
}
|
|
}
|
|
|
|
// policyAppliesToPrivateChannel reports whether a parent policy has at least
|
|
// one private child channel — used to gate the self-inclusion filter, which
|
|
// only matters when a non-matching admin could actually be locked out. Public
|
|
// channels under ABAC are advisory (no member removal, anyone can join), so
|
|
// a non-matching admin is never at risk there.
|
|
//
|
|
// idToType maps channel ID → channel type from a batched GetChannelsByIds;
|
|
// batchLookupFailed means the batch store call failed and the conservative
|
|
// answer is to apply the filter.
|
|
//
|
|
// Returns `true` (i.e. apply the filter) on metadata or lookup errors, since
|
|
// the safer fallback is to keep the existing behavior rather than silently
|
|
// expose a policy a private channel might depend on.
|
|
func policyAppliesToPrivateChannel(rctx request.CTX, policy *model.AccessControlPolicy, idToType map[string]model.ChannelType, batchLookupFailed bool) bool {
|
|
if policy == nil || policy.Props == nil {
|
|
return true
|
|
}
|
|
|
|
childIDs, propsOk := childIDsFromPolicyProps(policy)
|
|
if !propsOk {
|
|
return true
|
|
}
|
|
|
|
if len(childIDs) == 0 {
|
|
// Channel-less policy (newly created, not yet assigned). Until a
|
|
// channel is attached there's nothing to lock anyone out of, so the
|
|
// filter is meaningless here.
|
|
return false
|
|
}
|
|
|
|
if batchLookupFailed {
|
|
return true
|
|
}
|
|
|
|
found := 0
|
|
for _, id := range childIDs {
|
|
if _, ok := idToType[id]; ok {
|
|
found++
|
|
}
|
|
}
|
|
if found != len(childIDs) {
|
|
rctx.Logger().Warn("Partial child-channel lookup for self-inclusion gating; keeping the filter on",
|
|
mlog.String("policy_id", policy.ID),
|
|
mlog.Int("requested", len(childIDs)),
|
|
mlog.Int("returned", found),
|
|
)
|
|
return true
|
|
}
|
|
for _, id := range childIDs {
|
|
if idToType[id] == model.ChannelTypePrivate {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|