diff --git a/apisix-stack/apisix_conf/route_create.sh b/apisix-stack/apisix_conf/route_create.sh new file mode 100644 index 0000000..35d0f6e --- /dev/null +++ b/apisix-stack/apisix_conf/route_create.sh @@ -0,0 +1,146 @@ +#!/usr/bin/env bash +set -euo pipefail + +ADMIN_API_URL="${ADMIN_API_URL:-http://localhost:9180}" +ADMIN_API_KEY="${ADMIN_API_KEY:-edd1c9f034335f136f87ad84b625c8f1}" +TENANT_ID="${TENANT_ID:-2081a403-c773-427a-86a4-543f0983d921}" + +usage() { + cat <<'EOF' +Usage: + ./route_create.sh + +Params: + route-prefix APISIX public route prefix, for example: ssm-vn-dev + dataverse-host Dataverse host without scheme, for example: tngredev.crm5.dynamics.com + client-id Microsoft Entra application client id + client-secret Microsoft Entra application client secret + +Derived values: + route id -route + route uri //* + token cache key dataverse_token_ + upstream https:// + token scope https:///.default + +Example: + ./route_create.sh ssm-vn-dev tngredev.crm5.dynamics.com 2f680fb4-35b8-4be2-99ee-a3d183b607c9 'YOUR_CLIENT_SECRET' + +Optional env: + ADMIN_API_URL Default: http://localhost:9180 + ADMIN_API_KEY Default: edd1c9f034335f136f87ad84b625c8f1 + TENANT_ID Default: 2081a403-c773-427a-86a4-543f0983d921 +EOF +} + +if [ "$#" -ne 4 ]; then + echo "ERROR: Missing required params. Expected 4 params, got $#." >&2 + echo >&2 + usage >&2 + exit 1 +fi + +ROUTE_PREFIX="$1" +DATAVERSE_HOST="$2" +CLIENT_ID="$3" +CLIENT_SECRET="$4" + +if ! [[ "$ROUTE_PREFIX" =~ ^[A-Za-z0-9][A-Za-z0-9_-]*$ ]]; then + echo "ERROR: route-prefix is invalid: $ROUTE_PREFIX" >&2 + echo "Use only letters, numbers, hyphen, and underscore. Example: ssm-vn-dev" >&2 + exit 1 +fi + +if [[ "$DATAVERSE_HOST" == http://* || "$DATAVERSE_HOST" == https://* || "$DATAVERSE_HOST" == */* ]]; then + echo "ERROR: dataverse-host must be host only, without scheme/path." >&2 + echo "Example: tngredev.crm5.dynamics.com" >&2 + exit 1 +fi + +json_escape() { + sed \ + -e 's/\\/\\\\/g' \ + -e 's/"/\\"/g' \ + -e ':a;N;$!ba;s/\n/\\n/g' +} + +lua_escape() { + sed \ + -e 's/\\/\\\\/g' \ + -e 's/"/\\"/g' \ + -e ':a;N;$!ba;s/\n/\\n/g' +} + +ROUTE_ID="${ROUTE_PREFIX}-route" +TOKEN_SUFFIX="${ROUTE_PREFIX//-/_}" +TOKEN_CACHE_KEY="dataverse_token_${TOKEN_SUFFIX}" +ROUTE_URI="/${ROUTE_PREFIX}/*" +REWRITE_REGEX="^/${ROUTE_PREFIX}(/.*)$" +UPSTREAM_NODE="${DATAVERSE_HOST}:443" +TOKEN_SCOPE="https://${DATAVERSE_HOST}/.default" + +TOKEN_CACHE_KEY_LUA="$(printf '%s' "$TOKEN_CACHE_KEY" | lua_escape)" +TENANT_ID_LUA="$(printf '%s' "$TENANT_ID" | lua_escape)" +CLIENT_ID_LUA="$(printf '%s' "$CLIENT_ID" | lua_escape)" +CLIENT_SECRET_LUA="$(printf '%s' "$CLIENT_SECRET" | lua_escape)" +TOKEN_SCOPE_LUA="$(printf '%s' "$TOKEN_SCOPE" | lua_escape)" + +LUA_FUNCTION="return function(conf, ctx) local core = require(\"apisix.core\"); local http = require(\"resty.http\"); local cache = ngx.shared.oauth_cache; if not cache then core.log.error(\"oauth_cache not defined in config.yaml\"); return end; local token = cache:get(\"${TOKEN_CACHE_KEY_LUA}\"); if not token then local httpc = http.new(); httpc:set_timeout(5000); local tenant_id = \"${TENANT_ID_LUA}\"; local client_id = \"${CLIENT_ID_LUA}\"; local client_secret = \"${CLIENT_SECRET_LUA}\"; local scope = \"${TOKEN_SCOPE_LUA}\"; local body = \"grant_type=client_credentials&client_id=\" .. ngx.escape_uri(client_id) .. \"&client_secret=\" .. ngx.escape_uri(client_secret) .. \"&scope=\" .. ngx.escape_uri(scope); local res, err = httpc:request_uri(\"https://login.microsoftonline.com/\" .. tenant_id .. \"/oauth2/v2.0/token\", { method = \"POST\", body = body, headers = { [\"Content-Type\"] = \"application/x-www-form-urlencoded\" }, ssl_verify = false }); if not res or res.status ~= 200 then core.log.error(\"Failed to fetch Azure AD token: \", err or (res and res.body)); return core.response.exit(500, { error = \"Auth failed to upstream provider\" }) end; local data, decode_err = core.json.decode(res.body); if not data or not data.access_token then core.log.error(\"Failed to decode token response\"); return core.response.exit(500, { error = \"Invalid token response\" }) end; token = data.access_token; local expires_in = data.expires_in or 3600; cache:set(\"${TOKEN_CACHE_KEY_LUA}\", token, expires_in - 60) end; ngx.req.set_header(\"Authorization\", \"Bearer \" .. token) end" + +ROUTE_ID_JSON="$(printf '%s' "$ROUTE_ID" | json_escape)" +ROUTE_URI_JSON="$(printf '%s' "$ROUTE_URI" | json_escape)" +REWRITE_REGEX_JSON="$(printf '%s' "$REWRITE_REGEX" | json_escape)" +UPSTREAM_NODE_JSON="$(printf '%s' "$UPSTREAM_NODE" | json_escape)" +LUA_FUNCTION_JSON="$(printf '%s' "$LUA_FUNCTION" | json_escape)" + +PAYLOAD_FILE="$(mktemp)" +trap 'rm -f "$PAYLOAD_FILE"' EXIT + +cat > "$PAYLOAD_FILE" < /..." +echo " token cache key: ${TOKEN_CACHE_KEY}" +echo + +curl -i "${ADMIN_API_URL}/apisix/admin/routes/${ROUTE_ID}" \ + -H "X-API-KEY: ${ADMIN_API_KEY}" \ + -H 'Content-Type: application/json' \ + -X PUT \ + --data-binary "@${PAYLOAD_FILE}"