Autentisera Sophos Email API och dirigera till rätt tenant
Varje anrop till Sophos Email Management API börjar med tre separata steg: en service principal hämtar en kortlivad OAuth2-token, whoami fastställer anroparens typ och ID och först därefter kopplas måltenant till sin regionala API-värd. En giltig token väljer inte tenant eller dataregion på egen hand.
Det säkra slutläget är en odelbar uppsättning: SOPHOS_ACCESS_TOKEN, SOPHOS_TENANT_ID och SOPHOS_API_HOST. Den globala värden används bara för identitets- och tenantidentifiering. Email-operationer går till ${SOPHOS_API_HOST}/email/v1 med Authorization och X-Tenant-ID.
Skapa en service principal för API Preview-åtkomst
För en direkt tenant loggar du in i Sophos Fusion Admin (tidigare Sophos Central Admin) som Super Admin och öppnar Global Settings > API Credentials. I Sophos Fusion Partner används Settings & Policies > API Credentials. Så länge API:erna är i Preview sker åtkomsten på SuperAdmin-nivå. Begränsa denna credential till den kontrollerade automatiseringen och förutsätt inte att en snävare valbar service-principal-roll finns.
- Skapa separata credentials per program och miljö.
- Ange ägare, syfte och måltenant i namn och beskrivning.
- Dokumentera Preview-status och SuperAdmin-åtkomst som en risk och begränsa användningen till godkänd tenant.
- Lägg omedelbart Client ID och Client Secret i organisationens hemlighetsvalv.
- Övervaka utgångsdatum, rotation och nödkontakt utanför Sophos Fusion.
Begär access token utan att lämna hemligheten
Bash-exemplet kräver curl och jq. Det läser hemligheten dolt, URL-kodar formulärvärden och skickar dem via standard input. Därmed syns hemligheten varken i shellhistoriken eller som ett expanderat processargument:
set -e -o pipefail
cleanup() {
if [[ -n ${TENANTS_FILE:-} ]]; then
rm -f "$TENANTS_FILE" "${TENANTS_FILE}.new"
fi
unset SOPHOS_CLIENT_SECRET TOKEN_RESPONSE PAGE_RESPONSE WHOAMI
}
trap cleanup EXIT
read -r -p "Client ID: " SOPHOS_CLIENT_ID
read -r -s -p "Client Secret: " SOPHOS_CLIENT_SECRET
printf '\n'
TOKEN_RESPONSE=$(
printf 'grant_type=client_credentials&client_id=%s&client_secret=%s&scope=token' \
"$(jq -rn --arg v "$SOPHOS_CLIENT_ID" '$v|@uri')" \
"$(jq -rn --arg v "$SOPHOS_CLIENT_SECRET" '$v|@uri')" |
curl --fail-with-body --silent --show-error \
--request POST \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-binary @- \
https://id.sophos.com/api/v2/oauth2/token
) || { cleanup; exit 1; }
unset SOPHOS_CLIENT_SECRET
SOPHOS_ACCESS_TOKEN=$(jq -er '
select(.token_type == "bearer") |
select((.expires_in | type) == "number" and .expires_in > 0) |
.access_token | select(type == "string" and length > 0)
' <<<"$TOKEN_RESPONSE") || { cleanup; exit 1; }
unset TOKEN_RESPONSE
grant_type=client_credentials, scope=token, tokensökvägen och Content-Type: application/x-www-form-urlencoded är fasta. client_id och client_secret kommer från credential. Ett giltigt svar innehåller access_token, token_type: "bearer" och expires_in. Hämta en ny token med samma flöde när den går ut och logga aldrig hela svaret.
Identifiera anroparen med Who-am-I
whoami är ett globalt discovery-anrop; bearer header skickas åter via curl-konfigurationens standard input. Token infogas i konfigurationsströmmen, som exemplet inte skriver ut:
WHOAMI=$(
printf 'header = "Authorization: Bearer %s"\n' "$SOPHOS_ACCESS_TOKEN" |
curl --fail-with-body --silent --show-error \
--config - https://api.central.sophos.com/whoami/v1
)
SOPHOS_ID=$(jq -er '.id | select(type == "string" and length > 0)' <<<"$WHOAMI")
SOPHOS_ID_TYPE=$(jq -er '
.idType | select(. == "tenant" or . == "partner" or . == "organization")
' <<<"$WHOAMI")
id är anroparens UUID; idType skiljer tenant, partner och organization; apiHosts.global är den globala värden; apiHosts.dataRegion är den regionala värden för en direkt tenant.
Vid idType: "tenant" är id även det tenant-ID som krävs. Validera båda routingvärdena:
UUID_RE='^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$'
[[ "$SOPHOS_ID_TYPE" == "tenant" ]] || {
printf 'Direct-tenant flow requires idType tenant\n' >&2
exit 1
}
SOPHOS_TENANT_ID=$(jq -er --arg re "$UUID_RE" '
.id | select(type == "string" and test($re))
' <<<"$WHOAMI")
SOPHOS_API_HOST=$(jq -er '
.apiHosts.dataRegion |
select(type == "string" and test("^https://api-[a-z0-9-]+\\.central\\.sophos\\.com$"))
' <<<"$WHOAMI")
Saknad dataRegion, oväntad idType eller ogiltig värd är ett stoppvillkor. Härled aldrig värden från exempel, regionkod eller statisk tabell.
Slå upp måltenant som partner
Vid idType: "partner" är whoami.id ett partner-ID och får aldrig användas som X-Tenant-ID. Lista hanterade tenants via den globala partnerendpointen med X-Partner-ID; varje objekt ger eget id, dataRegion och fullständigt apiHost.
Första sidan begär pageTotal=true; därefter läses numrerade sidor till pages.total, med en lokal övre gräns:
[[ "$SOPHOS_ID_TYPE" == "partner" ]] || {
printf 'Partner flow requires idType partner\n' >&2
exit 1
}
TENANTS_FILE=$(mktemp) || exit 1
trap cleanup EXIT
printf '[]\n' >"$TENANTS_FILE"
page=1
max_pages=1000
expected_pages=
while (( page <= max_pages )); do
if (( page == 1 )); then
PAGE_QUERY=(--data-urlencode 'page=1' --data-urlencode 'pageSize=100' --data-urlencode 'pageTotal=true')
else
PAGE_QUERY=(--data-urlencode "page=$page" --data-urlencode 'pageSize=100')
fi
PAGE_RESPONSE=$(
printf 'header = "Authorization: Bearer %s"\n' "$SOPHOS_ACCESS_TOKEN" |
curl --fail-with-body --silent --show-error --get --config - \
--header "X-Partner-ID: $SOPHOS_ID" \
"${PAGE_QUERY[@]}" \
https://api.central.sophos.com/partner/v1/tenants
)
if (( page == 1 )); then
expected_pages=$(jq -er '
select((.items | type) == "array") |
.pages.total | select(type == "number" and floor == . and . >= 1 and . <= 1000)
' <<<"$PAGE_RESPONSE")
else
jq -e '(.items | type) == "array"' <<<"$PAGE_RESPONSE" >/dev/null || exit 1
fi
jq -e --argjson page "$PAGE_RESPONSE" '. + $page.items' \
"$TENANTS_FILE" >"${TENANTS_FILE}.new" &&
mv "${TENANTS_FILE}.new" "$TENANTS_FILE" || exit 1
(( page >= expected_pages )) && break
((page++))
done
(( page == expected_pages )) || exit 1
unset PAGE_RESPONSE PAGE_QUERY page expected_pages max_pages
Välj målet med ett godkänt UUID, aldrig endast name. dataRegion är en beteckning; anropet använder motsvarande fullständiga apiHost:
read -r -p 'Approved target tenant UUID: ' TARGET_TENANT_ID
TARGET_TENANT=$(jq -cer --arg id "$TARGET_TENANT_ID" --arg re "$UUID_RE" '
[.[] |
select((.id | type) == "string" and (.id | test($re))) |
select((.id | ascii_downcase) == ($id | ascii_downcase)) |
select((.apiHost | type) == "string" and
(.apiHost | test("^https://api-[a-z0-9-]+\\.central\\.sophos\\.com$")))] |
select(length == 1) | .[0]
' "$TENANTS_FILE")
SOPHOS_TENANT_ID=$(jq -r '.id' <<<"$TARGET_TENANT")
SOPHOS_API_HOST=$(jq -r '.apiHost' <<<"$TARGET_TENANT")
rm -f "$TENANTS_FILE"
unset TARGET_TENANT TARGET_TENANT_ID TENANTS_FILE
idType: "organization" är också en separat kontext, inte ett tenant-ID. Guiden implementerar avsiktligt bara tenant och partner; behandla inte en organisation som partner.
Validera routing och headers med ett lästest
Behandla token, tenant-ID och regional värd som en uppsättning. Denna operationsneutrala kontroll validerar endast format och koppling och anropar ingen endpoint från ett senare runbook:
[[ -n "$SOPHOS_ACCESS_TOKEN" ]] || exit 1
[[ "$SOPHOS_TENANT_ID" =~ $UUID_RE ]] || exit 1
[[ "$SOPHOS_API_HOST" =~ ^https://api-[a-z0-9-]+\.central\.sophos\.com$ ]] || exit 1
EMAIL_API_HOST="${SOPHOS_API_HOST%/}/email/v1"
Discovery lyckas bara om whoami ger väntad typ och ID, UUID kommer från godkänd mappning och värden hör till rätt tenant. Använd runbooken för mailbox-automatisering för ett körbart säkert lästest och dess schema; API-översikten delar in alla familjer.
Varje tenantbundet Email-JSON-anrop kräver den regionala värden från apiHosts.dataRegion eller apiHost, plus /email/v1; Authorization: Bearer <access-token>; X-Tenant-ID: <tenant-uuid>; Accept: application/json; och för JSON Content-Type: application/json.
Skilj mellan 401, 403, 404 och 429
401 Unauthorized: credential saknas, är ogiltig eller blockerad, eller JWT har gått ut. Kontrollera credential och secretversion och autentisera exakt en gång till; byt inte tenant eller region.403 Forbidden: autentiseringen lyckades. Eftersom Preview-åtkomst redan är på SuperAdmin-nivå kontrollerar du tenantkoppling, operationsbehörighet och aktuell API-tillgänglighet; byte av credential reparerar inte routing.404 Not Found: kontrollera regional värd,/email/v1och objekt-ID; prova inte andra regioner.429 Too Many Requests: följ tillgängliga retry- eller rate-limitheaders och begränsa försöken.
De dokumenterade allmänna gränserna är: rekommenderat högst 10 anrop per sekund, verkställt 100 per minut med burst upp till 300, rekommenderat 1 000 per timme och verkställt 200 000 per dag. Beroende på gränsen räknas API-credential, konto och käll-IP; byt aldrig dessa för att kringgå en gräns. Operationsspecifika gränser kan gälla dessutom.
För 429 och tillfälliga 5xx används Full Jitter: random_between(0, min(cap, base * (2 ** attempt))). Sophos anger base = 1000 ms och cap = 30000 ms som exempel. Begränsa även antal försök och total tid; stoppa sedan och larma. Upprepa inte 401, 403 eller 404 automatiskt.
Upprepa aldrig skrivning, borttagning, release eller clawback blint: servern kan ha godkänt åtgärden före timeout. Kontrollera först produktspecifik status och idempotens.
Ta bort hemligheter och sessionsdata
Svarskroppar kan innehålla tenantdata och personuppgifter. Logga bara tid, metod, maskerad sökväg, HTTP-status och request ID eller trackingId, aldrig Client Secret, access- eller refresh-token eller fullständiga svar.
cleanup
unset SOPHOS_ACCESS_TOKEN SOPHOS_CLIENT_ID SOPHOS_ID SOPHOS_ID_TYPE
unset SOPHOS_TENANT_ID SOPHOS_API_HOST EMAIL_API_HOST WHOAMI
trap - EXIT
Vid förlorad eller misstänkt hemlighet skapar du en ny credential, validerar routing och kör därefter det kontrollerade testet i rätt runbook, växlar programmet och tar bort den gamla. Borttagning återkallar framtida API-anrop men ångrar inte en redan utförd Email-operation.