mirror of
https://github.com/forcedotcom/afv-library.git
synced 2026-08-08 00:10:29 +08:00
431 lines
21 KiB
Bash
Executable File
431 lines
21 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
# Define Agent Access for the Pipeline Management Sales Agent (W-23242378)
|
|
#
|
|
# PROBLEM this solves:
|
|
# When the Pipeline Management Sales Agent (a BotDefinition matched by
|
|
# AgentTemplate — SalesMgmt__NGASalesAgent / SalesMgmt__SalesAgent) is created,
|
|
# it is NOT added to the "Agent Access" section of any permission set that
|
|
# human users hold. Users assigned the managed PSG `SalesManagementUserPsg`
|
|
# therefore cannot launch or chat with the agent, even though the agent is
|
|
# active and the PSG is assigned.
|
|
#
|
|
# WHY it can't be done directly on the PSG:
|
|
# `SalesManagementUserPsg` is a MANAGED permission set group — its Agent Access
|
|
# cannot be edited. Agent Access (SetupEntityAccess with SetupEntityType=
|
|
# 'BotDefinition') can only live on a CUSTOM permission set (License=None).
|
|
#
|
|
# THE FIX (mirrors the manual Setup UI flow):
|
|
# 1. Create a CUSTOM permission set `Sales_Agent_Access` (License=None).
|
|
# 2. Add the agent to its Agent Access: insert SetupEntityAccess
|
|
# (ParentId=<permset Id>, SetupEntityId=<BotDefinition Id>). Standard Data API.
|
|
# SetupEntityType is derived — do NOT set it on insert. Requires API v64.0+.
|
|
# 3. Add the custom permset to `SalesManagementUserPsg` via
|
|
# PermissionSetGroupComponent (Tooling API object — needs --use-tooling-api).
|
|
# 4. Confirm the PSG recalculates (the component insert triggers async recalc;
|
|
# poll Status until 'Updated'). => Agent becomes available to all users
|
|
# holding SalesManagementUserPsg.
|
|
#
|
|
# The BotDefinition is detected by AgentTemplate (via shared/agent-detection.sh),
|
|
# NOT by a hardcoded DeveloperName, so it matches whatever local name the agent
|
|
# was published with (SalesAgent, Sale_Agent, etc.) — the same logic the
|
|
# scheduled flow uses.
|
|
#
|
|
# This script is idempotent (query-then-skip; SetupEntityAccess has no Update)
|
|
# and safe to re-run. It must run AFTER the agent (BotDefinition) exists.
|
|
#
|
|
# Usage: ./define-agent-access.sh <org-alias>
|
|
|
|
set -euo pipefail
|
|
|
|
# sf CLI can emit ANSI color codes inside --json stdout, breaking jq parsing.
|
|
# 2>/dev/null only strips stderr; these env vars are the reliable fix (see PM notes).
|
|
export NO_COLOR=1
|
|
export FORCE_COLOR=0
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
|
# Shared PM bot detection (AgentTemplate-based). Provides PM_AGENT_TEMPLATES_SOQL
|
|
# and pm_bot_developer_name().
|
|
source "$SCRIPT_DIR/shared/agent-detection.sh"
|
|
# Shared PSG recalculation status reader (psg_status).
|
|
source "$SCRIPT_DIR/shared/psg.sh"
|
|
|
|
ORG_ALIAS="${1:-}"
|
|
|
|
# --- Helpers (mirror setup-all.sh conventions) ---
|
|
log_pass() { echo " [PASS] $1"; }
|
|
log_warn() { echo " [WARN] $1"; }
|
|
log_fail() { echo " [FAIL] $1"; }
|
|
log_info() { echo " [....] $1"; }
|
|
log_try() { echo " [TRY ] $1"; }
|
|
|
|
if [[ -z "$ORG_ALIAS" ]]; then
|
|
echo "Error: Missing org alias"
|
|
echo "Usage: $0 <org-alias>"
|
|
exit 1
|
|
fi
|
|
|
|
# Validate org alias (prevent shell injection via metacharacters), matching
|
|
# the pattern used by setup-all.sh and verify-all.sh.
|
|
if [[ ! "$ORG_ALIAS" =~ ^[a-zA-Z0-9][a-zA-Z0-9._-]*$ ]]; then
|
|
echo "ERROR: Invalid org alias. Only alphanumeric, dots, hyphens, and underscores allowed (must start with alphanumeric)."
|
|
exit 1
|
|
fi
|
|
|
|
# Preflight: this script parses every CLI response with jq and calls the sf CLI,
|
|
# and SetupEntityAccess for BotDefinition needs the sf CLI's default API (v64.0+,
|
|
# shipped by sf CLI v2.x). Fail fast with an actionable message instead of a
|
|
# cryptic mid-run error.
|
|
if ! command -v jq >/dev/null 2>&1; then
|
|
echo "ERROR: 'jq' is required but not installed. Install it (e.g. 'brew install jq') and re-run."
|
|
exit 1
|
|
fi
|
|
if ! command -v sf >/dev/null 2>&1; then
|
|
echo "ERROR: Salesforce CLI ('sf') is required but not installed. Install the v2.x 'sf' CLI and re-run."
|
|
exit 1
|
|
fi
|
|
SF_MAJOR=$(sf --version 2>/dev/null | grep -oE '@salesforce/cli/[0-9]+' | grep -oE '[0-9]+$' || echo "")
|
|
if [[ -n "$SF_MAJOR" && "$SF_MAJOR" -lt 2 ]]; then
|
|
echo "ERROR: Salesforce CLI v2.x or newer is required (found major version ${SF_MAJOR})."
|
|
echo " Agent Access (SetupEntityAccess for BotDefinition) needs API v64.0+, which ships with sf CLI v2.x."
|
|
exit 1
|
|
fi
|
|
|
|
# Configurable identifiers
|
|
ACCESS_PS_NAME="Sales_Agent_Access"
|
|
ACCESS_PS_LABEL="Sales Agent Access"
|
|
USER_PSG_NAME="SalesManagementUserPsg"
|
|
AGENT_PSG_NAME="SalesManagementAgentUserPsg"
|
|
|
|
echo "=== Define Agent Access for Pipeline Management Sales Agent ==="
|
|
echo ""
|
|
|
|
# ------------------------------------------------------------
|
|
# Step 0: The agent must exist first (BotDefinition Id is runtime-only).
|
|
# Detect it by AgentTemplate, matching the scheduled flow's own lookup — not by
|
|
# a hardcoded DeveloperName. Uses a TWO-STEP query: first find the BotDefinition
|
|
# by AgentTemplate (no subquery), then separately confirm it has an Active
|
|
# BotVersion. The single-query `Id IN (SELECT ... FROM BotVersion WHERE Status =
|
|
# 'Active')` form was removed in de77d520 because that correlated subquery hangs
|
|
# 60+ seconds on some orgs.
|
|
# ------------------------------------------------------------
|
|
BOT_DEF=$(sf data query \
|
|
--query "SELECT Id, DeveloperName FROM BotDefinition WHERE AgentTemplate IN (${PM_AGENT_TEMPLATES_SOQL}) ORDER BY LastModifiedDate DESC LIMIT 1" \
|
|
--target-org "$ORG_ALIAS" \
|
|
--json 2>/dev/null | jq -r '.result.records[0] // empty' 2>/dev/null || echo "")
|
|
|
|
BOT_DEF_ID=$(echo "$BOT_DEF" | jq -r '.Id // empty' 2>/dev/null || echo "")
|
|
BOT_DEF_NAME=$(echo "$BOT_DEF" | jq -r '.DeveloperName // empty' 2>/dev/null || echo "")
|
|
|
|
# BOT_DEF_ID is interpolated into later SOQL; reject anything that is not a
|
|
# valid Salesforce record Id (15 or 18 char alphanumeric).
|
|
if [[ -n "$BOT_DEF_ID" && ! "$BOT_DEF_ID" =~ ^[a-zA-Z0-9]{15,18}$ ]]; then
|
|
log_fail "Invalid BotDefinition ID format: $BOT_DEF_ID"
|
|
exit 1
|
|
fi
|
|
|
|
if [[ -z "$BOT_DEF_ID" ]]; then
|
|
log_warn "No Pipeline Management BotDefinition found — cannot define Agent Access yet."
|
|
echo " Agent Access can only be defined after the agent exists and has an Active version."
|
|
echo " Create the agent first (./create-agent.sh $ORG_ALIAS), then re-run this script."
|
|
exit 0
|
|
fi
|
|
|
|
# Separately confirm the bot has an Active BotVersion (flat query on BotVersion,
|
|
# no correlated subquery). Agent Access on an inactive bot has no runtime effect.
|
|
ACTIVE_VERSION_COUNT=$(sf data query \
|
|
--query "SELECT COUNT() FROM BotVersion WHERE BotDefinitionId = '${BOT_DEF_ID}' AND Status = 'Active'" \
|
|
--target-org "$ORG_ALIAS" \
|
|
--json 2>/dev/null | jq -r '.result.totalSize // 0' 2>/dev/null | head -1 | tr -cd '0-9' || echo "0")
|
|
ACTIVE_VERSION_COUNT="${ACTIVE_VERSION_COUNT:-0}"
|
|
|
|
if [[ "$ACTIVE_VERSION_COUNT" -lt 1 ]]; then
|
|
log_warn "Agent BotDefinition '${BOT_DEF_NAME:-<unknown>}' has no Active BotVersion — cannot define Agent Access yet."
|
|
echo " Activate the agent version first, then re-run this script."
|
|
exit 0
|
|
fi
|
|
log_pass "Found active agent BotDefinition: ${BOT_DEF_NAME:-<unknown>} ($BOT_DEF_ID)"
|
|
|
|
# ------------------------------------------------------------
|
|
# Step 1: Create the custom permission set (idempotent)
|
|
# PermissionSet is createable via the STANDARD Data API (NOT Tooling).
|
|
# Omitting License leaves LicenseId null (= "None"), which is required so the
|
|
# permset can be added as a component of the managed PSG.
|
|
# ------------------------------------------------------------
|
|
PS_ID=$(sf data query \
|
|
--query "SELECT Id FROM PermissionSet WHERE Name = '${ACCESS_PS_NAME}'" \
|
|
--target-org "$ORG_ALIAS" \
|
|
--json 2>/dev/null | jq -r '.result.records[0].Id // empty' 2>/dev/null || echo "")
|
|
|
|
if [[ -n "$PS_ID" ]]; then
|
|
log_pass "Custom permission set '${ACCESS_PS_NAME}' already exists ($PS_ID)"
|
|
else
|
|
log_try "Creating custom permission set '${ACCESS_PS_NAME}' (License=None)..."
|
|
CREATE_PS=$(sf data create record \
|
|
--sobject PermissionSet \
|
|
--values "Name='${ACCESS_PS_NAME}' Label='${ACCESS_PS_LABEL}'" \
|
|
--target-org "$ORG_ALIAS" \
|
|
--json 2>/dev/null || echo '{"status":1}')
|
|
|
|
PS_ID=$(echo "$CREATE_PS" | jq -r '.result.id // empty' 2>/dev/null || echo "")
|
|
if [[ -z "$PS_ID" ]]; then
|
|
# Re-query in case of a race / partial output. A concurrent create or a
|
|
# not-yet-visible write can make the row lag briefly, so retry with a short
|
|
# backoff rather than giving up after a single lookup.
|
|
for _ in $(seq 1 5); do
|
|
PS_ID=$(sf data query \
|
|
--query "SELECT Id FROM PermissionSet WHERE Name = '${ACCESS_PS_NAME}'" \
|
|
--target-org "$ORG_ALIAS" \
|
|
--json 2>/dev/null | jq -r '.result.records[0].Id // empty' 2>/dev/null || echo "")
|
|
[[ -n "$PS_ID" ]] && break
|
|
sleep 2
|
|
done
|
|
fi
|
|
|
|
if [[ -z "$PS_ID" ]]; then
|
|
log_fail "Could not create permission set '${ACCESS_PS_NAME}'"
|
|
echo "$CREATE_PS" | jq -r '.message // .result.message // empty' 2>/dev/null || true
|
|
exit 1
|
|
fi
|
|
log_pass "Created custom permission set '${ACCESS_PS_NAME}' ($PS_ID)"
|
|
fi
|
|
|
|
# ------------------------------------------------------------
|
|
# Step 2: Add the agent to Agent Access on that permission set
|
|
# SetupEntityAccess: Create/Delete/Query only (NO Update). SetupEntityType is
|
|
# derived from SetupEntityId — do NOT set it on insert. BotDefinition is a valid
|
|
# SetupEntityType only at API v64.0+ (sf CLI default is well above this).
|
|
# ------------------------------------------------------------
|
|
EXISTING_ACCESS=$(sf data query \
|
|
--query "SELECT Id FROM SetupEntityAccess WHERE ParentId = '${PS_ID}' AND SetupEntityId = '${BOT_DEF_ID}'" \
|
|
--target-org "$ORG_ALIAS" \
|
|
--json 2>/dev/null | jq -r '.result.records[0].Id // empty' 2>/dev/null || echo "")
|
|
|
|
if [[ -n "$EXISTING_ACCESS" ]]; then
|
|
log_pass "Agent Access already granted to '${ACCESS_PS_NAME}' ($EXISTING_ACCESS)"
|
|
else
|
|
log_try "Granting Agent Access (SetupEntityAccess) to '${ACCESS_PS_NAME}'..."
|
|
CREATE_ACCESS=$(sf data create record \
|
|
--sobject SetupEntityAccess \
|
|
--values "ParentId='${PS_ID}' SetupEntityId='${BOT_DEF_ID}'" \
|
|
--target-org "$ORG_ALIAS" \
|
|
--json 2>/dev/null || echo '{"status":1}')
|
|
|
|
# The query-then-create above is non-atomic: a concurrent run can insert the
|
|
# same grant between our SELECT and INSERT, so tolerate DUPLICATE_VALUE — the
|
|
# grant exists either way, which is the outcome we want (idempotent).
|
|
if echo "$CREATE_ACCESS" | jq -e '.result.id // empty' >/dev/null 2>&1; then
|
|
log_pass "Agent Access granted (agent added to '${ACCESS_PS_NAME}')"
|
|
elif echo "$CREATE_ACCESS" | jq -r '.message // .result.message // ""' 2>/dev/null | grep -qi "DUPLICATE_VALUE"; then
|
|
log_pass "Agent Access already granted (duplicate insert ignored)"
|
|
else
|
|
ACCESS_ERR=$(echo "$CREATE_ACCESS" | jq -r '.message // .result.message // "unknown"' 2>/dev/null || echo "unknown")
|
|
log_fail "Could not grant Agent Access: $ACCESS_ERR"
|
|
echo " SetupEntityAccess requires a CUSTOM permission set and API v64.0+."
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
# ------------------------------------------------------------
|
|
# Step 3: Add the custom permset as a component of the managed PSG
|
|
# PermissionSetGroupComponent is a TOOLING API object — must use --use-tooling-api.
|
|
# ------------------------------------------------------------
|
|
PSG_ID=$(sf data query \
|
|
--query "SELECT Id FROM PermissionSetGroup WHERE DeveloperName = '${USER_PSG_NAME}'" \
|
|
--target-org "$ORG_ALIAS" \
|
|
--use-tooling-api \
|
|
--json 2>/dev/null | jq -r '.result.records[0].Id // empty' 2>/dev/null || echo "")
|
|
|
|
COMPONENT_LINKED=0
|
|
if [[ -z "$PSG_ID" ]]; then
|
|
log_warn "PSG '${USER_PSG_NAME}' not found — skipping component link + recalc."
|
|
echo " The custom permission set '${ACCESS_PS_NAME}' still carries Agent Access;"
|
|
echo " it can be assigned directly to users as a fallback (see below)."
|
|
else
|
|
EXISTING_COMPONENT=$(sf data query \
|
|
--query "SELECT Id FROM PermissionSetGroupComponent WHERE PermissionSetGroupId = '${PSG_ID}' AND PermissionSetId = '${PS_ID}'" \
|
|
--target-org "$ORG_ALIAS" \
|
|
--use-tooling-api \
|
|
--json 2>/dev/null | jq -r '.result.records[0].Id // empty' 2>/dev/null || echo "")
|
|
|
|
if [[ -n "$EXISTING_COMPONENT" ]]; then
|
|
log_pass "'${ACCESS_PS_NAME}' is already a component of '${USER_PSG_NAME}'"
|
|
COMPONENT_LINKED=1
|
|
else
|
|
log_try "Adding '${ACCESS_PS_NAME}' as a component of '${USER_PSG_NAME}'..."
|
|
CREATE_COMPONENT=$(sf data create record \
|
|
--sobject PermissionSetGroupComponent \
|
|
--values "PermissionSetGroupId='${PSG_ID}' PermissionSetId='${PS_ID}'" \
|
|
--target-org "$ORG_ALIAS" \
|
|
--use-tooling-api \
|
|
--json 2>/dev/null || echo '{"status":1}')
|
|
|
|
if echo "$CREATE_COMPONENT" | jq -e '.result.id // empty' >/dev/null 2>&1; then
|
|
log_pass "Linked '${ACCESS_PS_NAME}' into '${USER_PSG_NAME}'"
|
|
COMPONENT_LINKED=1
|
|
elif echo "$CREATE_COMPONENT" | jq -r '.message // .result.message // ""' 2>/dev/null | grep -qi "DUPLICATE_VALUE"; then
|
|
log_pass "'${ACCESS_PS_NAME}' already a component of '${USER_PSG_NAME}' (duplicate insert ignored)"
|
|
COMPONENT_LINKED=1
|
|
else
|
|
COMPONENT_ERR=$(echo "$CREATE_COMPONENT" | jq -r '.message // .result.message // "unknown"' 2>/dev/null || echo "unknown")
|
|
log_warn "Could not link custom permset into managed PSG: $COMPONENT_ERR"
|
|
echo " Falling back to direct assignment of '${ACCESS_PS_NAME}' to PSG users."
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# ------------------------------------------------------------
|
|
# Step 3b: Add the custom permset as a component of the agent-user PSG too.
|
|
# The autonomous agent user runs the suggestion flow AS the agent, so it also
|
|
# needs Agent Access — wire the same custom permset into SalesManagementAgentUserPsg.
|
|
# Mirrors Step 3 (Tooling API object; idempotent query-then-skip). No direct-
|
|
# assignment fallback: the agent user is a single system user, so the component
|
|
# link is the right (and only) path.
|
|
# ------------------------------------------------------------
|
|
AGENT_COMPONENT_LINKED=0
|
|
AGENT_PSG_ID=$(sf data query \
|
|
--query "SELECT Id FROM PermissionSetGroup WHERE DeveloperName = '${AGENT_PSG_NAME}'" \
|
|
--target-org "$ORG_ALIAS" \
|
|
--use-tooling-api \
|
|
--json 2>/dev/null | jq -r '.result.records[0].Id // empty' 2>/dev/null || echo "")
|
|
|
|
if [[ -z "$AGENT_PSG_ID" ]]; then
|
|
log_warn "PSG '${AGENT_PSG_NAME}' not found — skipping agent component link + recalc."
|
|
else
|
|
EXISTING_AGENT_COMPONENT=$(sf data query \
|
|
--query "SELECT Id FROM PermissionSetGroupComponent WHERE PermissionSetGroupId = '${AGENT_PSG_ID}' AND PermissionSetId = '${PS_ID}'" \
|
|
--target-org "$ORG_ALIAS" \
|
|
--use-tooling-api \
|
|
--json 2>/dev/null | jq -r '.result.records[0].Id // empty' 2>/dev/null || echo "")
|
|
|
|
if [[ -n "$EXISTING_AGENT_COMPONENT" ]]; then
|
|
log_pass "'${ACCESS_PS_NAME}' is already a component of '${AGENT_PSG_NAME}'"
|
|
AGENT_COMPONENT_LINKED=1
|
|
else
|
|
log_try "Adding '${ACCESS_PS_NAME}' as a component of '${AGENT_PSG_NAME}'..."
|
|
CREATE_AGENT_COMPONENT=$(sf data create record \
|
|
--sobject PermissionSetGroupComponent \
|
|
--values "PermissionSetGroupId='${AGENT_PSG_ID}' PermissionSetId='${PS_ID}'" \
|
|
--target-org "$ORG_ALIAS" \
|
|
--use-tooling-api \
|
|
--json 2>/dev/null || echo '{"status":1}')
|
|
|
|
if echo "$CREATE_AGENT_COMPONENT" | jq -e '.result.id // empty' >/dev/null 2>&1; then
|
|
log_pass "Linked '${ACCESS_PS_NAME}' into '${AGENT_PSG_NAME}'"
|
|
AGENT_COMPONENT_LINKED=1
|
|
elif echo "$CREATE_AGENT_COMPONENT" | jq -r '.message // .result.message // ""' 2>/dev/null | grep -qi "DUPLICATE_VALUE"; then
|
|
log_pass "'${ACCESS_PS_NAME}' already a component of '${AGENT_PSG_NAME}' (duplicate insert ignored)"
|
|
AGENT_COMPONENT_LINKED=1
|
|
else
|
|
AGENT_COMPONENT_ERR=$(echo "$CREATE_AGENT_COMPONENT" | jq -r '.message // .result.message // "unknown"' 2>/dev/null || echo "unknown")
|
|
log_warn "Could not link custom permset into agent PSG: $AGENT_COMPONENT_ERR"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# ------------------------------------------------------------
|
|
# Step 4: Confirm the PSG recalculates so the added component takes effect.
|
|
# The PermissionSetGroupComponent insert in Step 3 is what actually TRIGGERS the
|
|
# async recalculation — PermissionSetGroup.Status is a system-computed field
|
|
# (Updated/Updating/Outdated/Failed), NOT client-writable. There is no CLI recalc
|
|
# verb, so we simply POLL Status until it returns to 'Updated'. A Failed status
|
|
# means Agent Access is NOT live and must be surfaced as a hard error.
|
|
# ------------------------------------------------------------
|
|
# The PermissionSetGroupComponent insert in Step 3 IS the recalc trigger — the
|
|
# async recalculation is already under way and the org converges on its own; we
|
|
# deliberately do NOT block waiting for it (recalc completion only matters when a
|
|
# user later launches the agent). We read Status once, for reporting.
|
|
if [[ "$COMPONENT_LINKED" -eq 1 && -n "$PSG_ID" ]]; then
|
|
log_try "Recalculation triggered for '${USER_PSG_NAME}' (component linked)."
|
|
PSG_STATUS=$(psg_status "$ORG_ALIAS" "$USER_PSG_NAME")
|
|
case "$PSG_STATUS" in
|
|
Updated)
|
|
log_pass "PSG '${USER_PSG_NAME}' Status: Updated" ;;
|
|
Failed)
|
|
log_fail "PSG '${USER_PSG_NAME}' recalculation Status: Failed — Agent Access not active"
|
|
exit 1 ;;
|
|
*)
|
|
log_info "PSG '${USER_PSG_NAME}' Status: ${PSG_STATUS} — recalculation is async and will complete on its own."
|
|
echo " Agent Access takes effect once Status reaches 'Updated' (verify-all.sh reports it)." ;;
|
|
esac
|
|
fi
|
|
|
|
# ------------------------------------------------------------
|
|
# Step 4b: Report the agent-user PSG recalc, mirroring Step 4 (read Status once,
|
|
# never sleep-poll — the Step 3b component insert already triggered the async
|
|
# recalculation).
|
|
# ------------------------------------------------------------
|
|
if [[ "$AGENT_COMPONENT_LINKED" -eq 1 && -n "$AGENT_PSG_ID" ]]; then
|
|
log_try "Recalculation triggered for '${AGENT_PSG_NAME}' (component linked)."
|
|
AGENT_PSG_STATUS=$(psg_status "$ORG_ALIAS" "$AGENT_PSG_NAME")
|
|
case "$AGENT_PSG_STATUS" in
|
|
Updated)
|
|
log_pass "PSG '${AGENT_PSG_NAME}' Status: Updated" ;;
|
|
Failed)
|
|
log_fail "PSG '${AGENT_PSG_NAME}' recalculation Status: Failed — Agent Access not active"
|
|
exit 1 ;;
|
|
*)
|
|
log_info "PSG '${AGENT_PSG_NAME}' Status: ${AGENT_PSG_STATUS} — recalculation is async and will complete on its own."
|
|
echo " Agent Access takes effect once Status reaches 'Updated' (verify-all.sh reports it)." ;;
|
|
esac
|
|
fi
|
|
|
|
# ------------------------------------------------------------
|
|
# Fallback: if the component link failed, assign the custom permset directly to
|
|
# the users who hold the managed PSG so they still get Agent Access.
|
|
# ------------------------------------------------------------
|
|
if [[ "$COMPONENT_LINKED" -eq 0 && -n "$PSG_ID" ]]; then
|
|
log_info "Fallback: assigning '${ACCESS_PS_NAME}' directly to users of '${USER_PSG_NAME}'..."
|
|
# LIMIT 200 caps the fan-out: each user below costs one sequential API call, so
|
|
# an unbounded query on a large org could issue hundreds of calls. The linked
|
|
# component (Step 3) is the scalable path; this direct-assignment fallback is a
|
|
# best-effort stopgap. If the PSG has >200 users, prefer fixing the component
|
|
# link so recalculation reaches everyone at once.
|
|
ASSIGNEE_IDS=$(sf data query \
|
|
--query "SELECT AssigneeId FROM PermissionSetAssignment WHERE PermissionSetGroup.DeveloperName = '${USER_PSG_NAME}' LIMIT 200" \
|
|
--target-org "$ORG_ALIAS" \
|
|
--json 2>/dev/null | jq -r '.result.records[].AssigneeId' 2>/dev/null || echo "")
|
|
|
|
if [[ -z "$ASSIGNEE_IDS" ]]; then
|
|
log_warn "No users currently hold '${USER_PSG_NAME}' — nothing to assign."
|
|
else
|
|
# NOTE: do not name this loop var UID — UID is a readonly special var in bash.
|
|
while IFS= read -r ASSIGNEE_ID; do
|
|
[[ -z "$ASSIGNEE_ID" ]] && continue
|
|
CREATE_RESULT=$(sf data create record \
|
|
--sobject PermissionSetAssignment \
|
|
--values "AssigneeId='${ASSIGNEE_ID}' PermissionSetId='${PS_ID}'" \
|
|
--target-org "$ORG_ALIAS" \
|
|
--json 2>/dev/null || echo '{"status":1}')
|
|
|
|
# Distinguish "already assigned" (benign) from real failures like a
|
|
# deactivated user or license conflict — don't paper over the latter.
|
|
RESULT_MSG=$(echo "$CREATE_RESULT" | jq -r '.message // .result.message // ""' 2>/dev/null || echo "")
|
|
if echo "$CREATE_RESULT" | jq -e '.result.id // empty' >/dev/null 2>&1; then
|
|
log_pass "Assigned '${ACCESS_PS_NAME}' to user $ASSIGNEE_ID"
|
|
elif echo "$RESULT_MSG" | grep -qi "DUPLICATE_VALUE"; then
|
|
log_info "Already assigned: $ASSIGNEE_ID"
|
|
elif echo "$RESULT_MSG" | grep -qi "FIELD_INTEGRITY_EXCEPTION"; then
|
|
log_warn "License conflict for $ASSIGNEE_ID (FIELD_INTEGRITY_EXCEPTION) — user may need a license upgrade"
|
|
elif echo "$RESULT_MSG" | grep -qi "INVALID_CROSS_REFERENCE_KEY"; then
|
|
log_warn "Assignment for $ASSIGNEE_ID rejected (INVALID_CROSS_REFERENCE_KEY) — the PSG has not finished recalculating (Status not yet 'Updated'). Re-run after recalculation completes."
|
|
else
|
|
ASSIGN_ERR="${RESULT_MSG:-unknown}"
|
|
log_warn "Failed to assign to $ASSIGNEE_ID: $ASSIGN_ERR"
|
|
fi
|
|
done <<< "$ASSIGNEE_IDS"
|
|
fi
|
|
fi
|
|
|
|
echo ""
|
|
echo "=== Agent Access definition complete ==="
|
|
echo ""
|
|
echo " Custom permission set : ${ACCESS_PS_NAME} ($PS_ID)"
|
|
echo " Agent : ${BOT_DEF_NAME:-<unknown>} ($BOT_DEF_ID)"
|
|
echo " Target PSG : ${USER_PSG_NAME}"
|
|
echo " Agent PSG : ${AGENT_PSG_NAME}"
|
|
echo ""
|
|
echo " Users holding '${USER_PSG_NAME}' can launch the Sales Agent once the PSG"
|
|
echo " recalculation completes (Status 'Updated'). The recalc runs asynchronously."
|