76 lines
3.4 KiB
Bash
Executable File
76 lines
3.4 KiB
Bash
Executable File
#!/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"
|