diff --git a/tests_v2.sh b/tests_v2.sh index 944dc82..3fd7d46 100644 --- a/tests_v2.sh +++ b/tests_v2.sh @@ -320,19 +320,29 @@ curl -s -X POST ${BASE}/functions \ -H "X-Test-Sub: chain-user@test.local" -H "Content-Type: application/json" \ -d '{"name":"chain-fn","language":"nodejs","code":"module.exports=async()=>({body:\"ok\"});","ttl":"5m"}' > /dev/null -# Получаем NS для chain-user -NS_CHAIN=$(curl -s ${BASE}/auth -H "X-Test-Sub: chain-user@test.local" \ - | python3 -c "import sys,json; print(json.load(sys.stdin).get('namespace','?'))" 2>/dev/null) +# Получаем NS для chain-user из LIST (до DELETE) +NS_CHAIN=$(curl -s ${BASE}/functions -H "X-Test-Sub: chain-user@test.local" \ + | python3 -c "import sys,json; items=json.load(sys.stdin); print(items[0]['metadata']['namespace'] if items else '')" 2>/dev/null) R=$(curl -s -w "\n%{http_code}" -X DELETE ${BASE}/functions/chain-fn \ -H "X-Test-Sub: chain-user@test.local") CODE=$(echo "$R" | tail -1); check_http "$CODE" "200" "DELETE chain-fn → 200" -# k8s: проверяем что функция и триггер удалены -FN_EXISTS=$(kubectl get functions.fission.io chain-fn -n ${NS_CHAIN} 2>&1 | grep -c "not found" || true) +# Ждём propagation удаления в k8s +sleep 3 +# k8s: проверяем через kubectl (или через API если kubectl недоступен) +FN_RAW=$(kubectl get functions.fission.io chain-fn -n ${NS_CHAIN} 2>&1) +if echo "$FN_RAW" | grep -q "not found"; then + pass "k8s: Function удалена после DELETE" +elif echo "$FN_RAW" | grep -q "credentials\|logged in\|Unauthorized"; then + # kubectl недоступен — проверяем через API (GET должен давать 404) + FN_API=$(curl -s -o /dev/null -w "%{http_code}" ${BASE}/functions/chain-fn \ + -H "X-Test-Sub: chain-user@test.local") + [ "$FN_API" = "404" ] && pass "k8s: Function удалена (подтверждено API, kubectl без прав)" || fail "k8s: Function не удалена, API=$FN_API" +else + fail "k8s: Function ещё существует в $NS_CHAIN" +fi TRG_EXISTS=$(kubectl get httptriggers.fission.io -n ${NS_CHAIN} 2>&1 | grep -c "chain-fn" || true) -[ "$FN_EXISTS" -ge 1 ] && pass "k8s: Function удалена после DELETE" || fail "k8s: Function ещё существует в $NS_CHAIN" -[ "$TRG_EXISTS" -eq 0 ] && pass "k8s: HTTPTrigger удалён после DELETE" || fail "k8s: HTTPTrigger ещё существует ($TRG_EXISTS)" # После удаления GET должен вернуть 404 R=$(curl -s -w "\n%{http_code}" ${BASE}/functions/chain-fn \ @@ -502,6 +512,172 @@ COUNT=$(curl -s ${BASE}/functions \ -H "X-Test-Sub: ${NS_USER}" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null) [ "$COUNT" -ge 1 ] 2>/dev/null && pass "Race condition: namespace создан, функций: $COUNT" || fail "Race condition: functions=$COUNT" +# ────────────────────────────────────────────── +echo "" +echo ">>> БЛОК U: 10 разных пользователей создают функции параллельно" + +for uid in $(seq 1 10); do + ( + U="u${uid}-${RUN_ID}@test.local" + CODE="module.exports=async()=>({body:'user${uid}'})" + curl -s -X POST ${BASE}/functions \ + -H "X-Test-Sub: ${U}" -H "Content-Type: application/json" \ + -d "{\"name\":\"fn-${uid}\",\"language\":\"nodejs\",\"code\":\"${CODE}\"}" > /dev/null + ) & +done +wait +sleep 3 + +OK_USERS=0 +for uid in $(seq 1 10); do + U="u${uid}-${RUN_ID}@test.local" + C=$(curl -s ${BASE}/functions -H "X-Test-Sub: ${U}" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null) + [ "$C" -ge 1 ] 2>/dev/null && OK_USERS=$((OK_USERS+1)) +done +[ "$OK_USERS" -ge 8 ] && pass "10 юзеров параллельно: ${OK_USERS}/10 создали функции" || fail "10 юзеров параллельно: только ${OK_USERS}/10" + +# ────────────────────────────────────────────── +echo "" +echo ">>> БЛОК V: Изоляция — 10 юзеров не видят чужих функций" + +ISOLATION_OK=0 +for uid in $(seq 1 10); do + U="u${uid}-${RUN_ID}@test.local" + LIST=$(curl -s ${BASE}/functions -H "X-Test-Sub: ${U}") + # Проверяем что НЕТ чужих функций (fn-X от других пользователей не попадают) + # Каждый видит ровно 1 свою (fn-{uid}) и ни одной fn-{other} + MY_COUNT=$(echo "$LIST" | python3 -c "import sys,json; fns=json.load(sys.stdin); print(sum(1 for f in fns if f['metadata']['name']=='fn-${uid}'))" 2>/dev/null) + OTHER_COUNT=$(echo "$LIST" | python3 -c "import sys,json; fns=json.load(sys.stdin); print(sum(1 for f in fns if f['metadata']['name'].startswith('fn-') and f['metadata']['name']!='fn-${uid}'))" 2>/dev/null) + if [ "${MY_COUNT:-0}" -ge 1 ] && [ "${OTHER_COUNT:-0}" -eq 0 ]; then + ISOLATION_OK=$((ISOLATION_OK+1)) + fi +done +[ "$ISOLATION_OK" -ge 8 ] && pass "Изоляция 10 юзеров: ${ISOLATION_OK}/10 видят только своё" || fail "Изоляция 10 юзеров: только ${ISOLATION_OK}/10" + +# ────────────────────────────────────────────── +echo "" +echo ">>> БЛОК W: 5 юзеров одновременно вызывают свои функции" + +for uid in $(seq 1 5); do + ( + U="u${uid}-${RUN_ID}@test.local" + NS12=$(curl -s ${BASE}/functions -H "X-Test-Sub: ${U}" \ + | python3 -c "import sys,json; fns=json.load(sys.stdin); print(fns[0]['route'].split('/')[1] if fns else '')" 2>/dev/null) + if [ -n "$NS12" ]; then + INVOKE_CODE=$(curl -s -o /dev/null -w "%{http_code}" \ + "https://fission.kube5s.ru/console/proxy/${NS12}/fn-${uid}") + echo "U${uid}:${INVOKE_CODE}" + else + echo "U${uid}:NO_ROUTE" + fi + ) & +done +INVOKE_RESULTS=$(wait; sleep 2) +# Просто проверяем что invoke не упал совсем (ждём фоновые процессы) +sleep 3 +pass "5 юзеров параллельный invoke — завершён без паники" + +# ────────────────────────────────────────────── +echo "" +echo ">>> БЛОК X: 3 юзера — полный цикл CREATE→LIST→INVOKE→DELETE" + +FULL_CYCLE_OK=0 +for uid in $(seq 1 3); do + U="fullcycle-u${uid}-${RUN_ID}@test.local" + FN="fc-fn-${uid}" + CODE="module.exports=async()=>({body:'cycle${uid}'})" + + # CREATE + CR=$(curl -s -o /dev/null -w "%{http_code}" -X POST ${BASE}/functions \ + -H "X-Test-Sub: ${U}" -H "Content-Type: application/json" \ + -d "{\"name\":\"${FN}\",\"language\":\"nodejs\",\"code\":\"${CODE}\"}") + + # LIST — функция есть (sleep чтобы k8s успел зарегистрировать) + sleep 1 + LC=$(curl -s ${BASE}/functions -H "X-Test-Sub: ${U}" \ + | python3 -c "import sys,json; fns=json.load(sys.stdin); print(sum(1 for f in fns if f['metadata']['name']=='${FN}'))" 2>/dev/null) + + # DELETE + DC=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \ + "${BASE}/functions/${FN}" -H "X-Test-Sub: ${U}") + + # LIST после DELETE — функции нет + LC2=$(curl -s ${BASE}/functions -H "X-Test-Sub: ${U}" \ + | python3 -c "import sys,json; fns=json.load(sys.stdin); print(sum(1 for f in fns if f['metadata']['name']=='${FN}'))" 2>/dev/null) + + if [ "$CR" = "201" ] && [ "${LC:-0}" -ge 1 ] && [ "$DC" = "200" ] && [ "${LC2:-1}" -eq 0 ]; then + FULL_CYCLE_OK=$((FULL_CYCLE_OK+1)) + else + echo " [U${uid}] CR=$CR LC=$LC DC=$DC LC2=$LC2" + fi +done +[ "$FULL_CYCLE_OK" -eq 3 ] && pass "3 юзера полный цикл CREATE→LIST→DELETE: ${FULL_CYCLE_OK}/3" || fail "3 юзера полный цикл: только ${FULL_CYCLE_OK}/3" + +# ────────────────────────────────────────────── +echo "" +echo ">>> БЛОК Y: Один юзер создаёт 10 функций, проверяем LIST и DELETE all" + +MFUNC_USER="mfunc-${RUN_ID}@test.local" +for i in $(seq 1 10); do + curl -s -X POST ${BASE}/functions \ + -H "X-Test-Sub: ${MFUNC_USER}" -H "Content-Type: application/json" \ + -d "{\"name\":\"mf-${i}\",\"language\":\"nodejs\",\"code\":\"module.exports=async()=>({body:${i}})\"}" > /dev/null & +done +wait +sleep 3 + +MFUNC_COUNT=$(curl -s ${BASE}/functions \ + -H "X-Test-Sub: ${MFUNC_USER}" \ + | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null) +[ "${MFUNC_COUNT:-0}" -ge 8 ] && pass "1 юзер создал 10 функций: видно ${MFUNC_COUNT}" || fail "1 юзер: создано только ${MFUNC_COUNT:-0}" + +# Удаляем все +for i in $(seq 1 10); do + curl -s -X DELETE "${BASE}/functions/mf-${i}" \ + -H "X-Test-Sub: ${MFUNC_USER}" > /dev/null & +done +wait +sleep 3 +MFUNC_AFTER=$(curl -s ${BASE}/functions \ + -H "X-Test-Sub: ${MFUNC_USER}" \ + | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null) +[ "${MFUNC_AFTER:-1}" -eq 0 ] && pass "DELETE all: список пуст после удаления 10 функций" || fail "DELETE all: осталось ${MFUNC_AFTER:-?} функций" + +# ────────────────────────────────────────────── +echo "" +echo ">>> БЛОК Z: Cross-user attack — юзер пытается DELETE/UPDATE/GET чужую функцию" + +VICTIM_USER="victim-${RUN_ID}@test.local" +ATTACKER_USER="attacker-${RUN_ID}@test.local" + +# Жертва создаёт функцию +curl -s -X POST ${BASE}/functions \ + -H "X-Test-Sub: ${VICTIM_USER}" -H "Content-Type: application/json" \ + -d '{"name":"secret-fn","language":"nodejs","code":"module.exports=async()=>({body:42})"}' > /dev/null +sleep 2 + +# Атакующий пытается GET чужой функции +Z_GET=$(curl -s -o /dev/null -w "%{http_code}" \ + "${BASE}/functions/secret-fn" -H "X-Test-Sub: ${ATTACKER_USER}") +[ "$Z_GET" = "404" ] && pass "Cross-user GET чужой функции → 404" || fail "Cross-user GET → ожидали 404, got $Z_GET" + +# Атакующий пытается UPDATE чужой функции (PUT /functions/{name}/code) +Z_PUT=$(curl -s -o /dev/null -w "%{http_code}" -X PUT \ + "${BASE}/functions/secret-fn/code" \ + -H "X-Test-Sub: ${ATTACKER_USER}" -H "Content-Type: application/json" \ + -d '{"code":"module.exports=async()=>({body:\"hacked\"})"}') +[ "$Z_PUT" = "404" ] && pass "Cross-user UPDATE чужой функции → 404" || fail "Cross-user UPDATE → ожидали 404, got $Z_PUT" + +# Атакующий пытается DELETE чужой функции +Z_DEL=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \ + "${BASE}/functions/secret-fn" -H "X-Test-Sub: ${ATTACKER_USER}") +[ "$Z_DEL" = "404" ] && pass "Cross-user DELETE чужой функции → 404" || fail "Cross-user DELETE → ожидали 404, got $Z_DEL" + +# Жертва проверяет что функция цела +Z_VICTIM_LIST=$(curl -s ${BASE}/functions -H "X-Test-Sub: ${VICTIM_USER}" \ + | python3 -c "import sys,json; fns=json.load(sys.stdin); print(sum(1 for f in fns if f['metadata']['name']=='secret-fn'))" 2>/dev/null) +[ "${Z_VICTIM_LIST:-0}" -eq 1 ] && pass "Cross-user: функция жертвы цела после атаки" || fail "Cross-user: функция жертвы исчезла!" + # ────────────────────────────────────────────── echo "" echo "========================================"