#!/bin/sh # The GraphQL transport, exercised as a request rather than as a bean inventory. # # auth-smoke proves the realm issues a usable token and that the application answers its public # health path. It never sends an authenticated request, and never touches /graphql at all — so the # JWT decoder, the security filter chain and the GraphQL execution path were each covered by their # own tests and by nothing that put them in one line together. # # Three requests, in this order, because each is only meaningful given the one before: # # 1. an unauthenticated query, which must be refused — otherwise steps 2 and 3 prove nothing about # authentication, they just prove the endpoint answers; # 2. a malformed token, which must be refused without a server error — a 500 here means the # decoder threw where it should have rejected; # 3. the real token, which must return the schema's liveness field — the request path, the policy # instrumentation and the resolver, in one call. set -eu SECRET_FILE="/run/secrets/keycloak-graphql-smoke-client-secret" GRAPHQL_PATH="${GRAPHQL_PATH:-/graphql}" QUERY='{"query":"{ _health }"}' fail() { echo "graphql-smoke: $1" >&2; exit 1; } post() { # $1 = Authorization header value, or empty for none. Prints the status code; body to /tmp/gql.json. if [ -n "$1" ]; then curl -s -o /tmp/gql.json -w '%{http_code}' -X POST "${APP_BASE_URL}${GRAPHQL_PATH}" \ -H "Authorization: $1" -H 'Content-Type: application/json' -d "${QUERY}" else curl -s -o /tmp/gql.json -w '%{http_code}' -X POST "${APP_BASE_URL}${GRAPHQL_PATH}" \ -H 'Content-Type: application/json' -d "${QUERY}" fi } # 1. no credential at all ANON_STATUS="$(post '')" case "${ANON_STATUS}" in 401|403) : ;; 200) fail "an unauthenticated GraphQL query was answered (${ANON_STATUS}); /graphql is not guarded" ;; *) fail "an unauthenticated GraphQL query answered ${ANON_STATUS}: $(cat /tmp/gql.json)" ;; esac # 2. a credential that is not a token BAD_STATUS="$(post 'Bearer not-a-real-token')" [ "${BAD_STATUS}" != "500" ] \ || fail "a malformed token produced a server error rather than a refusal" case "${BAD_STATUS}" in 401|403) : ;; *) fail "a malformed token answered ${BAD_STATUS}, which is neither a refusal nor a server error" ;; esac # 3. the real thing [ -r "${SECRET_FILE}" ] || fail "the client secret was not mounted" TOKEN_RESPONSE="$(curl -sf -X POST "${KEYCLOAK_ISSUER}/protocol/openid-connect/token" \ -d grant_type=client_credentials \ -d "client_id=${KEYCLOAK_CLIENT_ID}" \ --data-urlencode "client_secret=$(cat "${SECRET_FILE}")")" \ || fail "client-credentials token request failed" ACCESS_TOKEN="$(echo "${TOKEN_RESPONSE}" | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p')" [ -n "${ACCESS_TOKEN}" ] || fail "the token response carried no access_token" OK_STATUS="$(post "Bearer ${ACCESS_TOKEN}")" [ "${OK_STATUS}" = "200" ] \ || fail "an authenticated GraphQL query answered ${OK_STATUS}: $(cat /tmp/gql.json)" # The body, not just the status. A 200 carrying a GraphQL `errors` array is how a refused or failed # execution looks over HTTP, so a status-only check would pass on an unresolved field. grep -q '"_health"' /tmp/gql.json \ || fail "the response carried no _health field: $(cat /tmp/gql.json)" if grep -q '"errors"' /tmp/gql.json; then fail "the query returned GraphQL errors: $(cat /tmp/gql.json)" fi echo "graphql-smoke: /graphql refused anonymous and malformed credentials and answered the authenticated query"