Files
ctms/docs/setup-config-curl-smoke.sh
T
Cheng Zhou 74feca4467 feat: harden auth and study workflows
- replace plaintext login and unlock requests with RSA-OAEP/AES-GCM encrypted payloads

- add login challenge replay protection, production RSA key validation, and auth tests

- wire compose to environment-driven dev/prod settings without committing local secrets

- update setup-config smoke scripts and Postman docs for encrypted login

- add visit schedule migrations/tests and update study/subject setup workflows
2026-05-08 22:16:43 +08:00

153 lines
4.8 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
BASE_URL="${BASE_URL:-http://localhost:8888}"
EMAIL="${EMAIL:-admin@example.com}"
PASSWORD="${PASSWORD:-admin123}"
STUDY_ID="${STUDY_ID:-aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa}"
echo "[1/6] login: $EMAIL"
TOKEN=$(BASE_URL="$BASE_URL" EMAIL="$EMAIL" PASSWORD="$PASSWORD" python3 - <<'PY'
import base64
import json
import os
import urllib.request
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
base_url = os.environ["BASE_URL"]
email = os.environ["EMAIL"]
password = os.environ["PASSWORD"]
with urllib.request.urlopen(f"{base_url}/api/v1/auth/login-key", timeout=20) as resp:
login_key = json.loads(resp.read().decode())
public_key = serialization.load_pem_public_key(login_key["public_key"].encode("utf-8"))
plaintext = json.dumps(
{"email": email, "password": password, "challenge": login_key["challenge"]},
separators=(",", ":"),
).encode("utf-8")
aes_key = AESGCM.generate_key(bit_length=256)
iv = os.urandom(12)
encrypted_data = AESGCM(aes_key).encrypt(iv, plaintext, None)
encrypted_key = public_key.encrypt(
aes_key,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None,
),
)
payload = json.dumps(
{
"key_id": login_key["key_id"],
"challenge": login_key["challenge"],
"ciphertext": base64.b64encode(
json.dumps(
{
"encrypted_key": base64.b64encode(encrypted_key).decode("ascii"),
"iv": base64.b64encode(iv).decode("ascii"),
"data": base64.b64encode(encrypted_data).decode("ascii"),
},
separators=(",", ":"),
).encode("utf-8")
).decode("ascii"),
}
).encode()
req = urllib.request.Request(
f"{base_url}/api/v1/auth/login",
method="POST",
headers={"Content-Type": "application/json"},
data=payload,
)
with urllib.request.urlopen(req, timeout=20) as resp:
print(json.loads(resp.read().decode()).get("access_token", ""))
PY
)
if [[ -z "$TOKEN" ]]; then
echo "login failed: access_token empty"
exit 1
fi
AUTH=(-H "Authorization: Bearer $TOKEN")
echo "[2/6] get setup-config"
GET_RESP=$(curl -sS "$BASE_URL/api/v1/studies/$STUDY_ID/setup-config" "${AUTH[@]}")
VERSION=$(echo "$GET_RESP" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("version",""))')
if [[ -z "$VERSION" ]]; then
echo "get setup-config failed"
echo "$GET_RESP"
exit 1
fi
echo "version=$VERSION"
echo "[3/6] save draft"
PUT_PAYLOAD=$(cat <<JSON
{
"expected_version": $VERSION,
"data": {
"projectMilestones": [],
"enrollmentPlan": {
"totalTarget": 123,
"startDate": "2026-02-01",
"endDate": "2026-12-31",
"monthlyGoalNote": "curl smoke",
"stageBreakdown": "phase-1"
},
"siteMilestones": [],
"siteEnrollmentPlans": [],
"monitoringStrategies": [],
"centerConfirm": []
}
}
JSON
)
PUT_RESP=$(curl -sS -X PUT "$BASE_URL/api/v1/studies/$STUDY_ID/setup-config" \
"${AUTH[@]}" -H 'Content-Type: application/json' -d "$PUT_PAYLOAD")
VERSION=$(echo "$PUT_RESP" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("version",""))')
if [[ -z "$VERSION" ]]; then
echo "save draft failed"
echo "$PUT_RESP"
exit 1
fi
echo "saved version=$VERSION"
echo "[4/6] publish"
PUB_RESP=$(curl -sS -X POST "$BASE_URL/api/v1/studies/$STUDY_ID/setup-config/publish" \
"${AUTH[@]}" -H 'Content-Type: application/json' -d "{\"expected_version\":$VERSION}")
PUB_STATUS=$(echo "$PUB_RESP" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("publish_status",""))')
VERSION=$(echo "$PUB_RESP" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("version",""))')
if [[ "$PUB_STATUS" != "PUBLISHED" ]]; then
echo "publish failed"
echo "$PUB_RESP"
exit 1
fi
echo "publish_status=$PUB_STATUS version=$VERSION"
echo "[5/6] export excel"
EXCEL_FILE="/tmp/setup-config-export.xlsx"
curl -sS "$BASE_URL/api/v1/studies/$STUDY_ID/setup-config/export-excel" "${AUTH[@]}" -o "$EXCEL_FILE"
if [[ ! -s "$EXCEL_FILE" ]]; then
echo "export excel failed"
exit 1
fi
echo "export saved: $EXCEL_FILE"
echo "[6/6] import excel (re-import export file)"
IMP_RESP=$(curl -sS -X POST "$BASE_URL/api/v1/studies/$STUDY_ID/setup-config/import-excel" \
"${AUTH[@]}" \
-F "expected_version=$VERSION" \
-F "file=@$EXCEL_FILE;type=application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
IMP_VER=$(echo "$IMP_RESP" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("version",""))')
if [[ -z "$IMP_VER" ]]; then
echo "import failed"
echo "$IMP_RESP"
exit 1
fi
echo "import version=$IMP_VER"
echo "setup-config curl smoke passed"