De Sophos Email API verifiëren en naar de juiste tenant routeren
Elke aanroep van de Sophos Email Management API begint met drie afzonderlijke stappen: een service principal haalt een kortlevend OAuth2-token op, whoami bepaalt type en ID van de aanroeper en pas daarna wordt de doeltenant gekoppeld aan zijn regionale API-host. Een geldig token kiest op zichzelf geen tenant of dataregio.
Het veilige eindresultaat is één ondeelbare set: SOPHOS_ACCESS_TOKEN, SOPHOS_TENANT_ID en SOPHOS_API_HOST. Gebruik de globale host uitsluitend voor identiteits- en tenantdetectie. Email-bewerkingen gaan naar ${SOPHOS_API_HOST}/email/v1 met Authorization en X-Tenant-ID.
Een service principal voor API Preview-toegang maken
Meld je voor een directe tenant als Super Admin aan bij Sophos Fusion Admin (voorheen Sophos Central Admin) en open Global Settings > API Credentials. Gebruik in Sophos Fusion Partner Settings & Policies > API Credentials. Zolang de API’s Preview zijn, is de API-toegang van SuperAdmin-niveau. Beperk deze credential tot het gecontroleerde automatiseringsdoel en neem niet aan dat een beperktere selecteerbare service-principalrol beschikbaar is.
- Maak aparte credentials per toepassing en omgeving.
- Leg eigenaar, doel en doeltenant vast in naam en beschrijving.
- Leg Preview-status en SuperAdmin-toegang als risico vast en beperk gebruik tot de goedgekeurde tenant.
- Sla Client ID en Client Secret direct op in een zakelijke secret store.
- Bewaak verloopdatum, rotatie en noodcontact buiten Sophos Fusion.
Een access token zonder secretsporen aanvragen
Dit Bash-voorbeeld vereist curl en jq. Het leest het secret verborgen, URL-codeert de formulierwaarden en verstuurt ze via standaardinvoer. Daardoor verschijnt het secret niet in de shellgeschiedenis of als uitgevouwen procesargument:
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, het tokenpad en Content-Type: application/x-www-form-urlencoded liggen vast. client_id en client_secret komen uit de credential. Een geldig antwoord bevat access_token, token_type: "bearer" en expires_in. Vraag na afloop via dezelfde flow een nieuw token aan en log nooit het volledige antwoord.
De aanroeper bepalen met Who-am-I
whoami is een globale discovery-aanroep; ook hier loopt de bearer header via de standaardinvoerconfiguratie van curl. Het token wordt in die configuratiestroom ingevoegd; het voorbeeld drukt die stroom niet af:
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 is de UUID van de aanroeper; idType onderscheidt tenant, partner en organization; apiHosts.global is de globale host; apiHosts.dataRegion is voor een directe tenant de regionale host.
Bij idType: "tenant" is id ook de vereiste tenant-ID. Valideer beide routingwaarden:
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")
Een ontbrekende dataRegion, onverwacht idType of ongeldige host is een stopvoorwaarde. Leid een host nooit af uit een voorbeeld, regiocode of statische tabel.
Als partner de doeltenant bepalen
Bij idType: "partner" is de whoami-id een partner-ID en nooit een X-Tenant-ID. Vraag beheerde tenants op via het globale partnerendpoint met X-Partner-ID; elk object levert eigen id, dataRegion en volledige apiHost.
De eerste pagina gebruikt pageTotal=true; daarna worden genummerde pagina’s tot pages.total gelezen, met een lokale bovengrens:
[[ "$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
Selecteer de tenant via een goedgekeurde UUID, nooit alleen via name. dataRegion is een aanduiding; het verzoek gebruikt de bijbehorende volledige 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
Ook idType: "organization" is een aparte context en geen tenant-ID. Deze gids implementeert bewust alleen tenant en partner; behandel een organisatie niet stilzwijgend als partner.
Routing en headers valideren met een leestest
Behandel token, tenant-ID en regionale host als één set. Deze bewerkingsneutrale controle valideert alleen formaat en koppeling en roept geen endpoint uit een later runbook aan:
[[ -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 slaagt alleen als whoami het verwachte type en ID geeft, de UUID uit de goedgekeurde koppeling komt en de host bij die tenant hoort. Gebruik voor een uitvoerbare veilige leestest en het schema het runbook voor mailboxautomatisering; het API-overzicht deelt alle families in.
Elk tenantgebonden Email-JSON-verzoek vereist de regionale host uit apiHosts.dataRegion of apiHost, plus /email/v1; Authorization: Bearer <access-token>; X-Tenant-ID: <tenant-uuid>; Accept: application/json; en voor JSON Content-Type: application/json.
401, 403, 404 en 429 onderscheiden
401 Unauthorized: credential ontbreekt, is ongeldig of geblokkeerd, of JWT is verlopen. Controleer credential en secretversie en verifieer precies eenmaal opnieuw; wijzig tenant of regio niet.403 Forbidden: authenticatie lukte. Omdat Preview-toegang al SuperAdmin-niveau heeft, controleer je tenanttoewijzing, bewerkingsrecht en actuele API-beschikbaarheid; een andere credential herstelt routing niet.404 Not Found: controleer regionale host,/email/v1en object-ID; probeer geen andere regio’s.429 Too Many Requests: respecteer aanwezige retry- of rate-limitheaders en begrens herhalingen.
De gedocumenteerde algemene limieten zijn: aanbevolen maximaal 10 aanroepen per seconde, afgedwongen 100 per minuut met bursts tot 300, aanbevolen 1.000 per uur en afgedwongen 200.000 per dag. Afhankelijk van de limiet telt Sophos per API-credential, account en bron-IP; wissel die nooit om een limiet te omzeilen. Daarnaast kunnen bewerkingsspecifieke limieten gelden.
Gebruik voor 429 en tijdelijke 5xx Full Jitter: random_between(0, min(cap, base * (2 ** attempt))). Sophos noemt base = 1000 ms en cap = 30000 ms als voorbeelden. Begrens ook pogingen en totale duur; stop daarna en alarmeer. Herhaal 401, 403 en 404 niet automatisch.
Herhaal schrijf-, verwijder-, release- of clawbackaanroepen nooit blind: de server kan ze vóór een timeout hebben geaccepteerd. Controleer eerst productspecifieke status en idempotentie.
Secrets en sessiegegevens verwijderen
Responsebodies kunnen tenantgegevens en persoonsgegevens bevatten. Log alleen tijdstip, methode, geredigeerd pad, HTTP-status en eventuele request ID of trackingId, nooit Client Secret, access- of refreshtokens of volledige antwoorden. Wis daarna de variabelen:
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
Maak bij een verloren of verdacht secret een nieuwe credential, valideer de routing en voer daarna de gecontroleerde test van het toepasselijke runbook uit, zet de toepassing om en verwijder de oude. Verwijderen blokkeert toekomstige aanroepen maar maakt een uitgevoerde Email-bewerking niet ongedaan.