Saltar para o conteudo
Avanet

Autenticar a API Sophos Email e encaminhar para o tenant correto

Cada chamada à API Sophos Email Management começa com três passos separados: um service principal obtém um token OAuth2 de curta duração, whoami determina o tipo e o ID do autor da chamada e só depois o tenant de destino é associado ao respetivo host regional. Um token válido não seleciona, por si só, o tenant ou a região.

O resultado seguro é um conjunto inseparável: SOPHOS_ACCESS_TOKEN, SOPHOS_TENANT_ID e SOPHOS_API_HOST. O host global serve apenas para descobrir identidades e tenants. As operações Email usam ${SOPHOS_API_HOST}/email/v1 com Authorization e X-Tenant-ID.

Criar um service principal para acesso API Preview

Para um tenant direto, inicie sessão no Sophos Fusion Admin (anteriormente Sophos Central Admin) como Super Admin e abra Global Settings > API Credentials. No Sophos Fusion Partner, use Settings & Policies > API Credentials. Enquanto as API estiverem em Preview, o acesso é de nível SuperAdmin. Restrinja esta credencial à automatização controlada e não pressuponha que existe uma função de service principal mais restrita selecionável.

  1. Crie credenciais separadas por aplicação e ambiente.
  2. Identifique proprietário, finalidade e tenant no nome e descrição.
  3. Registe o estado Preview e o acesso SuperAdmin como risco e limite a utilização ao tenant aprovado.
  4. Guarde imediatamente Client ID e Client Secret num cofre empresarial.
  5. Monitorize externamente expiração, rotação e contacto de emergência.

Pedir o access token sem deixar vestígios

Este exemplo Bash requer curl e jq. Lê o segredo sem o mostrar, codifica os campos e envia-os pela entrada padrão. Assim, o segredo não aparece no histórico da shell nem nos argumentos de processo expandidos:

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, o caminho do token e Content-Type: application/x-www-form-urlencoded são fixos. client_id e client_secret vêm da credencial. Uma resposta válida contém access_token, token_type: "bearer" e expires_in. Quando expirar, peça outro token pelo mesmo fluxo e nunca registe a resposta completa.

Identificar o autor com Who-am-I

whoami é uma chamada global de discovery; o bearer header volta a ser passado pela entrada padrão do curl. O token é inserido nesse fluxo de configuração, que o exemplo não apresenta:

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 é o UUID do autor; idType distingue tenant, partner e organization; apiHosts.global é o host global; apiHosts.dataRegion é o host regional de um tenant direto.

Com idType: "tenant", id também é o ID de tenant necessário. Valide ambos os valores:

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")

A ausência de dataRegion, um idType inesperado ou um host inválido exige interrupção. Não deduza hosts a partir de exemplos, códigos de região ou tabelas estáticas.

Determinar o tenant de destino como partner

Com idType: "partner", o id de whoami é um ID de partner e nunca pode ser X-Tenant-ID. Enumere os tenants através do endpoint global de partner com X-Partner-ID; cada objeto fornece id, dataRegion e apiHost próprios.

A primeira página usa pageTotal=true; depois são lidas as páginas numeradas até pages.total, com um limite local:

[[ "$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

Selecione o destino por um UUID aprovado, nunca apenas por name. dataRegion é um identificador; o pedido utiliza o apiHost completo correspondente:

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" também é um contexto separado, não um ID de tenant. Este guia implementa apenas tenant e partner; não trate uma organização como partner.

Validar routing e headers com uma leitura

Trate token, ID de tenant e host regional como um único conjunto. Esta verificação neutra quanto à operação valida apenas formato e associação, sem chamar endpoints de runbooks posteriores:

[[ -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"

A descoberta só passa se whoami devolver o tipo e ID esperados, o UUID vier do mapeamento aprovado e o host pertencer ao tenant. Para um teste de leitura seguro executável e o respetivo esquema, use o runbook de automatização de caixas de correio; a introdução à API classifica todas as famílias.

Cada pedido JSON Email requer o host regional de apiHosts.dataRegion ou apiHost, mais /email/v1; Authorization: Bearer <access-token>; X-Tenant-ID: <tenant-uuid>; Accept: application/json; e Content-Type: application/json para JSON.

Distinguir 401, 403, 404 e 429

  • 401 Unauthorized: credencial ausente, inválida ou bloqueada, ou JWT expirado. Verifique credencial e versão do segredo e autentique exatamente mais uma vez; não mude tenant ou região.
  • 403 Forbidden: a autenticação funcionou. Como o acesso Preview já é de nível SuperAdmin, verifique a associação do tenant, a permissão da operação e a disponibilidade atual da API; mudar a credencial não corrige o routing.
  • 404 Not Found: verifique host regional, /email/v1 e ID do objeto; não experimente outras regiões.
  • 429 Too Many Requests: respeite os headers de retry ou rate limit e limite as tentativas.

Os limites gerais documentados são: máximo recomendado de 10 chamadas por segundo, 100 por minuto imposto com picos até 300, 1.000 por hora recomendado e 200.000 por dia imposto. Consoante o limite, a contagem abrange credencial API, conta e IP de origem; não os altere para contornar limites. Podem aplicar-se limites adicionais específicos da operação.

Para 429 e 5xx temporários, use Full Jitter: random_between(0, min(cap, base * (2 ** attempt))). A Sophos dá base = 1000 ms e cap = 30000 ms como exemplos. Limite também tentativas e duração total; depois pare e alerte. Não repita automaticamente 401, 403 ou 404.

Nunca repita cegamente escrita, eliminação, libertação ou clawback: o servidor pode ter aceite a operação antes do timeout. Primeiro verifique o estado específico e a idempotência.

Remover segredos e dados de sessão

Os corpos das respostas podem conter dados do tenant e dados pessoais. Registe apenas hora, método, caminho ocultado, estado HTTP e request ID ou trackingId, nunca Client Secret, tokens de acesso ou refresh nem respostas completas. Limpe as variáveis no final:

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

Para um segredo perdido ou suspeito, crie novas credenciais, valide o routing e execute depois o teste controlado do runbook aplicável, migre a aplicação e elimine as antigas. A eliminação revoga chamadas futuras, mas não desfaz uma operação Email já executada.