mattermost/server/public/model/access_request.go

575 lines
28 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
// AccessControlSubjectScope* enumerates the supported scopes for ScopedRole.
const (
AccessControlSubjectScopeSystem = "system"
AccessControlSubjectScopeChannel = "channel"
)
// ScopedRole pairs a role identifier with the scope it applies to. A subject
// may carry multiple ScopedRoles (e.g. one for the system, one for a channel)
// so the PDP can select the appropriate role when matching against a v0.4
// channel resource policy rule whose Role field is a channel-scoped role.
type ScopedRole struct {
// Scope is one of AccessControlSubjectScope* constants.
Scope string `json:"scope"`
// Role is the role identifier within that scope (e.g. "system_user",
// "channel_admin").
Role string `json:"role"`
}
// Subject represents the user or a virtual entity for which the Authorization
// API is called.
type Subject struct {
// ID is the unique identifier of the Subject.
// it can be a user ID, bot ID, etc and it is scoped to the Type.
ID string `json:"id"`
// Type specifies the type of the Subject, eg. user, bot, etc.
Type string `json:"type"`
// Role is the system role of the subject (e.g. "system_user", "system_guest", "system_admin").
// This is separate from custom profile attributes since it's a first-class system concept.
//
// Deprecated: prefer ScopedRoles which can express both system and
// channel-scoped roles. Role is still populated for backward
// compatibility and acts as the system-scope fallback inside
// RoleForScope: a system-scope lookup returns Role whenever
// ScopedRoles has no entry whose Scope is system — including
// when the slice is empty AND when it contains only
// channel-scoped entries. Populating ScopedRoles with non-system
// entries does NOT suppress this fallback.
Role string `json:"role"`
// ScopedRoles carries roles paired with the scope they apply to (system
// or channel). The PDP uses this slice to match a rule's scoped Role
// (e.g. v0.4 channel resource policy rules) against the subject.
ScopedRoles []ScopedRole `json:"scoped_roles,omitempty"`
// Attributes are the key-value pairs assicuated with the subject.
// An attribute may be single-valued or multi-valued and can be a primitive type
// (string, boolean, number) or a complex type like a JSON object or array.
Attributes map[string]any `json:"attributes"`
// Session carries environmental / per-session attributes that policy
// authors reference as `user.session.<key>` (e.g. user.session.network_status,
// user.session.client_type, user.session.device_managed, user.session.ip_range,
// user.session.platform, user.session.device_id).
//
// Session lives under the Subject — not as a sibling top-level CEL
// variable — because every value here is keyed to the requesting
// principal: the network the user is currently on, the client they're
// using, whether their device is MDM-managed, etc. Modeling it as part
// of the Subject keeps the Subject the single source of truth for
// "everything we know about the requester at decision time" and
// matches OpenID AuthZen's subject.properties / subject.session shape.
//
// The simulator populates this map from the picker's session-attribute
// overrides and the requesting admin's active-session snapshot. The
// live PDP populates it from rctx.Session() once the production wiring
// for environmental telemetry lands; until then SavePolicy rejects
// rules that reference user.session.* (see access_control.administration
// in the enterprise repo) so authors cannot ship a control whose
// production behaviour silently diverges from the simulator preview.
Session map[string]any `json:"session,omitempty"`
}
// RoleForScope returns the role assigned to this subject within the given
// scope. It first walks ScopedRoles for a matching Scope; for the system
// scope it falls back to the legacy Role field whenever no system-scoped
// entry exists in ScopedRoles (including when the slice is empty or
// contains only channel-scoped entries).
func (s *Subject) RoleForScope(scope string) string {
for _, sr := range s.ScopedRoles {
if sr.Scope == scope {
return sr.Role
}
}
if scope == AccessControlSubjectScopeSystem {
return s.Role
}
return ""
}
// RolesForScope returns every role assigned to this subject within the
// given scope, preserving the order they appear in ScopedRoles. Unlike
// RoleForScope it does NOT fall back to the legacy Role field — callers
// that need legacy single-role fallback should keep using RoleForScope.
//
// The current PDP only ever populates one entry per scope, so this
// helper returns at most a single-element slice today. It exists to
// give multi-role-per-scope consumers (a future capability — Mattermost
// users can carry multiple system roles like "system_user system_admin")
// a stable accessor that won't change shape when the underlying
// invariant is relaxed.
//
// Returns nil when no entry matches the scope.
func (s *Subject) RolesForScope(scope string) []string {
var roles []string
for _, sr := range s.ScopedRoles {
if sr.Scope == scope {
roles = append(roles, sr.Role)
}
}
return roles
}
// SetScopedRole upserts a single role for the given scope, preserving
// the per-scope uniqueness invariant the PDP currently relies on. If an
// entry for the scope already exists, its role is replaced (keeping its
// position in ScopedRoles); any later duplicates with the same scope
// are removed. If no entry exists, a new one is appended.
//
// Passing an empty role removes every entry for the scope. This mirrors
// the convention used by the channel-scope hot path in
// attachChannelScopedRole, where an empty channel role lookup means "no
// channel role applies — drop any stale entry from the cached subject."
//
// Passing an empty scope is a no-op (defensive — the PDP never
// constructs scope="" entries).
//
// SetScopedRole always allocates a fresh ScopedRoles backing array, so
// it is safe to call on a Subject whose ScopedRoles slice is aliased
// with another Subject (e.g. the per-user cached Subject reused across
// many channels in attachChannelScopedRole).
func (s *Subject) SetScopedRole(scope, role string) {
if scope == "" {
return
}
updated := false
out := make([]ScopedRole, 0, len(s.ScopedRoles)+1)
for _, sr := range s.ScopedRoles {
if sr.Scope != scope {
out = append(out, sr)
continue
}
if role == "" || updated {
continue
}
out = append(out, ScopedRole{Scope: scope, Role: role})
updated = true
}
if !updated && role != "" {
out = append(out, ScopedRole{Scope: scope, Role: role})
}
s.ScopedRoles = out
}
type SubjectSearchOptions struct {
Term string `json:"term"`
TeamID string `json:"team_id"`
// Query and Args should be generated within the Access Control Service
// and passed here wrt database driver
Query string `json:"query"`
Args []any `json:"args"`
Limit int `json:"limit"`
Cursor SubjectCursor `json:"cursor"`
AllowInactive bool `json:"allow_inactive"`
IgnoreCount bool `json:"ignore_count"`
// ExcludeChannelMembers is used to exclude members from the search results
// specifically used when syncing channel members
ExcludeChannelMembers string `json:"exclude_members"`
// SubjectID is used to filter search results to a specific user ID
// This is particularly useful for validation queries where we only need to check
// if a specific user matches an expression, rather than fetching all matching users
SubjectID string `json:"subject_id"`
}
type SubjectCursor struct {
TargetID string `json:"target_id"`
}
// Resource is the target of an access request.
type Resource struct {
// ID is the unique identifier of the Resource.
// It can be a channel ID, post ID, etc and it is scoped to the Type.
ID string `json:"id"`
// Type specifies the type of the Resource, eg. channel, post, etc.
Type string `json:"type"`
}
// AccessRequest represents the input to the Policy Decision Point (PDP).
// It contains the Subject, Resource, Action and optional Context attributes.
type AccessRequest struct {
Subject Subject `json:"subject"`
Resource Resource `json:"resource"`
Action string `json:"action"`
Context map[string]any `json:"context,omitempty"`
}
// The PDP evaluates the request and returns an AccessDecision.
// The Decision field is a boolean indicating whether the request is allowed or not.
type AccessDecision struct {
Decision bool `json:"decision"`
Context map[string]any `json:"context,omitempty"`
}
type QueryExpressionParams struct {
Expression string `json:"expression"`
Term string `json:"term"`
Limit int `json:"limit"`
After string `json:"after"`
ChannelId string `json:"channelId,omitempty"`
TeamId string `json:"teamId,omitempty"`
}
// PolicySimulationBlameSource enumerates where a deny originated when running
// the test (simulate) workflow against a draft policy.
const (
// PolicySimulationBlameSourceThisRule means the deny came from the rule
// that the author is currently editing.
PolicySimulationBlameSourceThisRule = "this_rule"
// PolicySimulationBlameSourceSiblingRule means the deny came from another
// rule inside the same draft policy (same channel, different role/action
// or different rule on the same role/action that resolves to deny).
PolicySimulationBlameSourceSiblingRule = "sibling_rule"
// PolicySimulationBlameSourceChannelPolicy means the deny came from a
// resource-policy rule that is not the one being edited but contributes
// to the same effective decision (e.g. an inherited parent policy).
PolicySimulationBlameSourceChannelPolicy = "channel_policy"
// PolicySimulationBlameSourceSystemPermission means the deny came from a
// truly higher-scoped, persisted permission policy. Distinct from
// PolicySimulationBlameSourcePeerPolicy (same-scope) — the simulator
// emits both as system_permission, but the public-server reclassifies
// peer-scope blame entries before the response leaves the server. The
// expression of an upper-scoped policy is intentionally not exposed
// to the simulate UI to preserve scope privacy.
PolicySimulationBlameSourceSystemPermission = "system_permission"
// PolicySimulationBlameSourcePeerPolicy means the deny came from another
// persisted policy at the SAME scope as the draft (same Type and same
// ParentID). It's carved out of system_permission by the public-server
// post-processing so the picker can show the peer's name + the failing
// rule's CEL expression instead of an opaque "upper-scoped policy"
// chip — at the editing scope, peers are visible to the author.
PolicySimulationBlameSourcePeerPolicy = "peer_policy"
// PolicySimulationBlameSourceNoApplicablePolicy is a synthetic blame
// source emitted by the simulator when the draft policy does not apply
// to a candidate user (e.g. a system_user user is added to test a
// system_admin policy). The decision is recorded as ALLOW (vacuously,
// because the policy is silent on this user) and the picker renders a
// "Policy doesn't apply" pill from this entry. Never produced by
// production evaluation — simulation-only.
PolicySimulationBlameSourceNoApplicablePolicy = "no_applicable_policy"
// PolicySimulationBlameSourceSiblingSaved is attached to an ALLOW
// decision when the rule the author is editing alone would have DENIED
// the subject, but a sibling rule (same role + action, OR-combined at
// compile time) flipped the bucket back to ALLOW. Useful so the
// picker can surface "this rule alone wouldn't have allowed them — a
// sibling did". Simulation-only.
PolicySimulationBlameSourceSiblingSaved = "sibling_saved"
// PolicySimulationBlameSourceNoApplicableRule is the synthetic blame
// source the "this rule only" post-process emits when the rule the
// author is editing is silent on the subject — either a sibling
// rule's OR-bucket saved an otherwise-denied user, or the deny
// originated entirely outside the editing rule (upper-scoped policy,
// peer policy, etc.). The decision is normalized to a vacuous ALLOW
// like no_applicable_policy and the picker renders a neutral
// "This rule doesn't apply" pill from this entry instead of the
// misleading "Allowed · another rule" / plain "Allowed" chips that
// the sibling_saved / orphaned-deny branches used to surface.
// Simulation-only and only emitted under the "this_rule"
// EvaluationScope (the "All policies" view keeps the original
// sibling_saved chip because at that scope the other rule IS
// relevant context for the verdict).
PolicySimulationBlameSourceNoApplicableRule = "no_applicable_rule"
)
// PolicySimulationBlameOutcome enumerates the per-blame verdict the
// simulator records for a contributing policy. Most blame entries
// carry the deny that produced the overall decision (PolicySimulationBlameOutcomeDeny);
// the simulator additionally emits informational entries with PolicySimulationBlameOutcomeAllow
// so the picker can show "your draft policy allowed this user" in
// multi-policy contexts where a peer policy is the denier.
const (
PolicySimulationBlameOutcomeDeny = "deny"
PolicySimulationBlameOutcomeAllow = "allow"
)
// PolicySimulationBlame attributes a deny decision back to the rule or policy
// that caused it. Some entries are informational (Outcome="allow") rather
// than deniers — those exist so the picker can surface the editing draft's
// evaluation alongside any peer policies' deny attribution; consumers that
// only care about deny attribution should filter to Outcome=="" or
// Outcome==PolicySimulationBlameOutcomeDeny (empty Outcome is treated as
// deny for backward compatibility with simulator builds that pre-date the
// field).
type PolicySimulationBlame struct {
// Source is one of the PolicySimulationBlameSource* constants.
Source string `json:"source"`
// Outcome is one of the PolicySimulationBlameOutcome* constants.
// Defaults to "deny" semantically when empty (backward compat with
// older simulators) — every blame entry shipped before this field
// existed was a denier. The picker uses Outcome to differentiate
// the editing draft's "I allowed" informational entry from the
// peer policies that actually caused the deny so each can render
// with the right indicator.
Outcome string `json:"outcome,omitempty"`
// PolicyID is the ID of the contributing policy (for system permission
// or channel policy sources). Empty when the deny originated from the
// draft itself (no persisted ID exists yet).
PolicyID string `json:"policy_id,omitempty"`
// PolicyName is the human-readable name of the contributing policy.
PolicyName string `json:"policy_name,omitempty"`
// RuleName is the name of the contributing rule (v0.4 permission rules
// always carry a unique name within their policy).
RuleName string `json:"rule_name,omitempty"`
// Role is the scoped role (system_* or channel_*) of the contributing
// rule or policy. Useful for explaining hierarchy fallbacks.
Role string `json:"role,omitempty"`
// Expression is the CEL text of the contributing rule. Only populated
// for blame entries at the draft's own scope (this_rule, sibling_rule,
// sibling_saved, peer_policy). Truly upper-scoped sources
// (system_permission, channel_policy) deliberately omit this field so
// the simulate UI can't leak the expression of a policy outside the
// editing scope.
Expression string `json:"expression,omitempty"`
// EvaluationTree is the per-node evaluation breakdown of the
// contributing rule, mirroring the boolean shape of the CEL
// expression's AST. Same scope-privacy rule as Expression: only
// populated for draft-side / peer-policy blame; truly upper-scoped
// sources omit it. The simulate UI renders it as a structured
// AND/OR/NOT tree showing exactly which sub-expression(s) produced
// the deny.
EvaluationTree *PolicySimulationEvaluationNode `json:"evaluation_tree,omitempty"`
// MergedRules lists every authored rule that was OR-folded into
// `Expression` for this contribution (see engine.JoinExpressions).
// Populated only when the contributing scope has more than one
// rule sharing the same (role, action) — single-rule
// contributions leave this empty so the simulate UI can keep the
// simpler "Rule: <name>" header. Order mirrors the policy's rule
// order, which is also the order JoinExpressions used when
// constructing the merged expression — so a UI can number rules
// consistently with the merged tree's branches.
//
// Same scope-privacy rule as Expression: populated only for
// same-scope blame (this_rule / sibling_rule / sibling_saved /
// peer_policy). Truly upper-scoped sources never carry this so
// the picker can't enumerate the rules of an out-of-scope policy.
MergedRules []PolicySimulationMergedRule `json:"merged_rules,omitempty"`
}
// PolicySimulationMergedRule is one entry in a blame's MergedRules:
// the name + expression + standalone evaluation tree of a single rule
// that was OR-folded into the blame's merged expression. A standalone
// tree (computed against the same activation as the merged tree) lets
// the UI render a per-rule breakdown numbered 1..N alongside the
// merged tree, so authors can map specific branches back to the rule
// they came from. The standalone tree carries the same scope-privacy
// rule as the surrounding blame's Expression; truly upper-scoped
// blame never carries MergedRules at all.
type PolicySimulationMergedRule struct {
// Name of the contributing rule (matches AccessControlPolicy.Rules[i].Name).
Name string `json:"name"`
// Expression is the rule's CEL text, before JoinExpressions wraps
// it in parens for the OR-fold. Useful when the UI wants to show
// the contributing rule on its own without reparsing.
Expression string `json:"expression,omitempty"`
// EvaluationTree is the standalone per-node evaluation breakdown
// of just this rule's expression (not the merged whole). The
// outcome on the root reflects whether THIS rule alone matched
// for the subject, which is what the picker needs to render
// "rule 1: TRUE / rule 2: FALSE" per-rule chips above each tree.
EvaluationTree *PolicySimulationEvaluationNode `json:"evaluation_tree,omitempty"`
}
// Kind values for PolicySimulationEvaluationNode.Kind. Compound kinds
// carry children; leaf kinds carry attribute / actual / expected
// metadata. PolicySimulationEvaluationKindOther is the catch-all for
// shapes the simulator doesn't decompose (bare attribute reference,
// ternary, unknown call).
const (
PolicySimulationEvaluationKindAnd = "and"
PolicySimulationEvaluationKindOr = "or"
PolicySimulationEvaluationKindNot = "not"
PolicySimulationEvaluationKindCompare = "compare"
PolicySimulationEvaluationKindFunction = "function"
PolicySimulationEvaluationKindOther = "other"
)
// Outcome values for PolicySimulationEvaluationNode.Outcome. Mirrors
// the three-way truth result of CEL evaluation — a clean true/false,
// or an error condition (missing attribute, type mismatch).
const (
PolicySimulationEvaluationOutcomeTrue = "true"
PolicySimulationEvaluationOutcomeFalse = "false"
PolicySimulationEvaluationOutcomeError = "error"
)
// PolicySimulationEvaluationNode is a single node in the evaluation
// tree returned by the simulate-by-users endpoint when the simulator
// is asked to explain a deny. The tree mirrors the boolean shape of
// the failing rule's CEL expression — short-circuit branches are
// walked regardless of their parent's outcome so the consumer can
// render the state of every clause, not just the first one that
// decided the verdict.
type PolicySimulationEvaluationNode struct {
// Kind classifies the node (compound vs leaf vs other). One of the
// PolicySimulationEvaluationKind* constants above.
Kind string `json:"kind"`
// Expression is the textual form of THIS subtree, suitable for the
// UI to render a snippet without rebuilding text from the AST.
Expression string `json:"expression"`
// Outcome is the per-node verdict. One of the
// PolicySimulationEvaluationOutcome* constants.
Outcome string `json:"outcome"`
// Error is a human-readable description of an evaluation-time
// failure. Populated only when Outcome == "error".
Error string `json:"error,omitempty"`
// Operator names the leaf operation: "==", "!=", "<", ">", ">=",
// "<=", "in", "startsWith", "endsWith", "contains". Empty for
// compound and other nodes.
Operator string `json:"operator,omitempty"`
// Attribute is the user-attribute path the leaf references when
// it could be unambiguously identified
// (e.g. user.attributes.region). Empty when the leaf does not
// reference an attribute or when both sides are non-attribute
// expressions.
Attribute string `json:"attribute,omitempty"`
// ActualValue is a display-formatted rendering of the user's
// value for Attribute. Empty when the attribute is missing — a
// missing attribute is also reflected in Outcome="error".
ActualValue string `json:"actual_value,omitempty"`
// ExpectedValue is a display-formatted rendering of the literal
// (or list of literals) the leaf compared against. Empty when the
// other side is itself an attribute reference.
ExpectedValue string `json:"expected_value,omitempty"`
// Children are the operands of a compound node, walked in
// expression order. Empty for leaf and other nodes.
Children []PolicySimulationEvaluationNode `json:"children,omitempty"`
}
// PolicySimulationActionDecision is the per-action verdict for a single user.
type PolicySimulationActionDecision struct {
Decision bool `json:"decision"`
Blame []PolicySimulationBlame `json:"blame,omitempty"`
}
// PolicySimulationSession is the per-session breakdown entry for the
// simulate-by-users response. Populated when the caller requests per-session
// evaluation (typically a system admin: their active sessions are individually
// evaluated so the picker can show why two sessions of the same user come
// back with different verdicts). Channel admins receive at most a single
// synthetic session populated with default values that they can override
// through the per-row session-attribute editor.
type PolicySimulationSession struct {
// ID is the persistent session identifier. Empty for synthetic sessions.
ID string `json:"id,omitempty"`
// Device is a human-readable device/client label (e.g. "MacBook Pro").
Device string `json:"device,omitempty"`
// Network classifies the connection (e.g. "WiFi", "VPN", "Mobile").
Network string `json:"network,omitempty"`
// LastActiveAt is the last-active timestamp in milliseconds since epoch.
LastActiveAt int64 `json:"last_active_at,omitempty"`
// Decisions maps action name → verdict for THIS session specifically,
// using the session's own session.* attributes (the user's profile
// attributes are constant across sessions).
Decisions map[string]PolicySimulationActionDecision `json:"decisions,omitempty"`
// Attributes is the session-attribute snapshot the simulator used when
// evaluating this session (network_status, device_managed, ip_range,
// etc.). Surfaced to the picker's "Decision details" view so the
// author can read the deny like an evaluation trace. Optional — omitted
// when the simulator hasn't populated it.
Attributes map[string]string `json:"attributes,omitempty"`
}
// PolicySimulationUserResult is one row in the simulation response.
type PolicySimulationUserResult struct {
User *User `json:"user"`
// Decisions maps action name → verdict. Always populated when the
// simulation request had non-empty Actions; nil when ExpressionOnly is
// true (fallback mode). When Sessions is populated, this represents the
// "headline" decision (e.g. from the most-recently-active session) so
// the picker can render a single chip without consulting Sessions.
Decisions map[string]PolicySimulationActionDecision `json:"decisions,omitempty"`
// Sessions is the optional per-session breakdown. Empty/nil falls back
// to the user-level Decisions only.
Sessions []PolicySimulationSession `json:"sessions,omitempty"`
// Attributes is the user profile attribute snapshot the simulator used
// when evaluating this user (department, region, clearance, etc.).
// Surfaced to the picker's "Decision details" view so the author can
// read the deny as an evaluation trace. Optional — omitted when the
// simulator hasn't populated it.
Attributes map[string]string `json:"attributes,omitempty"`
}
// PolicySimulationResponse is the body returned by cel/simulate_users.
type PolicySimulationResponse struct {
Results []PolicySimulationUserResult `json:"results"`
Total int64 `json:"total"`
}
// PolicySimulationUserOverride captures the per-user inputs the picker UI
// sends to /access_control_policies/cel/simulate_users. The simulator
// resolves each user's profile attributes from CPA storage and then layers
// session context on top: first the active-session snapshot (when
// UseActiveSession is set), then the explicit SessionOverrides map.
type PolicySimulationUserOverride struct {
// UserID identifies the user to simulate against.
UserID string `json:"user_id"`
// UseActiveSession injects the requesting admin's session.* attributes
// (network_status, client_type, device_managed, ip_range, platform,
// device_id) into this user's evaluation context. When the live PDP
// does not yet populate session.* on the request context this is a
// no-op; the API surface is forward-compatible.
UseActiveSession bool `json:"use_active_session,omitempty"`
// SessionOverrides replaces individual session.* attributes for this
// user only. Applied on top of the active-session snapshot when both
// are set, so a future "configure" panel can shadow specific values
// without discarding the rest of the active session.
//
// Mirrors the shape of Subject.Session (map[string]any) so the picker
// can carry mixed-typed session attributes (e.g. boolean
// device_managed alongside string network_status) without coercing
// everything through string. Nested maps / slices flow through to the
// CEL evaluator unchanged.
SessionOverrides map[string]any `json:"session_overrides,omitempty"`
}
// PolicyEvaluationScope* constants enumerate the supported evaluation
// scopes for /cel/simulate_users.
const (
// PolicyEvaluationScopeThisRule evaluates ONLY the rule the author is
// editing — sibling rules in the same policy, system permission
// policies, imported parent policies, and any other peer policies are
// excluded. This is the authoring-time "what does this rule alone do?"
// view: useful for iterating on a single rule's expression without
// other rules shadowing or compensating for it. Default when the
// request omits EvaluationScope.
PolicyEvaluationScopeThisRule = "this_rule"
// PolicyEvaluationScopeAll co-evaluates every contributing program —
// the entire draft policy (all rules), persisted system permission
// policies, parent policies — exactly as the live PDP would at
// request time. This is the "what verdict will the user actually
// experience?" view.
PolicyEvaluationScopeAll = "all"
)
// PolicySimulationByUsersParams is the request body for
// /access_control_policies/cel/simulate_users.
//
// The picker-based "Simulate access" UX hand-selects users to dry-run a
// draft policy against. Each user is run through the same dual-lane PDP
// path the live request would take and the response carries per-user,
// per-action ALLOW/DENY decisions plus blame attribution.
type PolicySimulationByUsersParams struct {
// Policy is the draft policy as it currently sits in the editor. Not
// persisted; compiled in-memory only.
Policy *AccessControlPolicy `json:"policy"`
// Actions is the set of permission actions to simulate. Required —
// a picker UX only makes sense once an action is in scope.
Actions []string `json:"actions"`
// RuleName identifies which rule in Policy.Rules the author is
// editing (used for blame attribution). Optional. When set, denies
// originating from this rule are tagged source=this_rule; other
// denies in the same draft are tagged source=sibling_rule.
RuleName string `json:"rule_name,omitempty"`
// ChannelID and TeamID provide context for delegated admin auth and
// channel-scope evaluation.
ChannelID string `json:"channel_id,omitempty"`
TeamID string `json:"team_id,omitempty"`
// Users is the explicit set of users to evaluate, with per-user
// session-attribute overrides.
Users []PolicySimulationUserOverride `json:"users"`
// EvaluationScope selects whether the simulator considers only the
// rule under simulation (this_rule) or co-evaluates every contributing
// program (all). Empty defaults to this_rule on the server.
EvaluationScope string `json:"evaluation_scope,omitempty"`
}