147 lines
5.6 KiB
Bash
147 lines
5.6 KiB
Bash
#!/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 <route-prefix> <dataverse-host> <client-id> <client-secret>
|
|
|
|
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-prefix>-route
|
|
route uri /<route-prefix>/*
|
|
token cache key dataverse_token_<route-prefix with '-' replaced by '_'>
|
|
upstream https://<dataverse-host>
|
|
token scope https://<dataverse-host>/.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" <<EOF
|
|
{
|
|
"uri": "${ROUTE_URI_JSON}",
|
|
"name": "${ROUTE_ID_JSON}",
|
|
"plugins": {
|
|
"key-auth": {},
|
|
"proxy-rewrite": {
|
|
"regex_uri": [
|
|
"${REWRITE_REGEX_JSON}",
|
|
"\$1"
|
|
]
|
|
},
|
|
"serverless-pre-function": {
|
|
"phase": "access",
|
|
"functions": [
|
|
"${LUA_FUNCTION_JSON}"
|
|
]
|
|
},
|
|
"limit-count": {
|
|
"count": 100,
|
|
"time_window": 60,
|
|
"rejected_code": 429
|
|
}
|
|
},
|
|
"upstream": {
|
|
"type": "roundrobin",
|
|
"nodes": {
|
|
"${UPSTREAM_NODE_JSON}": 1
|
|
},
|
|
"pass_host": "node",
|
|
"scheme": "https"
|
|
}
|
|
}
|
|
EOF
|
|
|
|
echo "Creating/updating APISIX route:"
|
|
echo " route id: ${ROUTE_ID}"
|
|
echo " public uri: ${ROUTE_URI}"
|
|
echo " upstream: https://${DATAVERSE_HOST}"
|
|
echo " rewrite: /${ROUTE_PREFIX}/... -> /..."
|
|
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}"
|