mirror of
https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin.git
synced 2026-09-02 20:28:50 +02:00
✅ test(e2e/redis): exercise read-from-replica path
The redis scenario only set redisCacheHost, so it validated the writer but never the round-robin reader path this feature adds. Split the mock into two roles: the primary (--redis-addr) now answers every GET with a miss, while the replica (--redis-read-addr) serves the hardcoded verdicts. The scenario points redisCacheReadHosts at the replica (twice, to drive round-robin), so the banned-IP-blocked assertion only passes if the plugin actually reads decisions from the replica rather than the primary. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,7 @@ LAPI_PORT="${LAPI_PORT:-8090}"
|
|||||||
BACKEND_PORT="${BACKEND_PORT:-8091}"
|
BACKEND_PORT="${BACKEND_PORT:-8091}"
|
||||||
APPSEC_PORT="${APPSEC_PORT:-8092}"
|
APPSEC_PORT="${APPSEC_PORT:-8092}"
|
||||||
REDIS_PORT="${REDIS_PORT:-8093}"
|
REDIS_PORT="${REDIS_PORT:-8093}"
|
||||||
|
REDIS_READ_PORT="${REDIS_READ_PORT:-8094}"
|
||||||
LAPI_KEY="${LAPI_KEY:-e2e-mock-key}"
|
LAPI_KEY="${LAPI_KEY:-e2e-mock-key}"
|
||||||
|
|
||||||
MOCK_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
MOCK_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
@@ -189,6 +190,7 @@ start_stack() {
|
|||||||
-e "s|@@APPSEC_HOST@@|127.0.0.1:${APPSEC_PORT}|g" \
|
-e "s|@@APPSEC_HOST@@|127.0.0.1:${APPSEC_PORT}|g" \
|
||||||
-e "s|@@BACKEND_URL@@|http://127.0.0.1:${BACKEND_PORT}|g" \
|
-e "s|@@BACKEND_URL@@|http://127.0.0.1:${BACKEND_PORT}|g" \
|
||||||
-e "s|@@REDIS_HOST@@|127.0.0.1:${REDIS_PORT}|g" \
|
-e "s|@@REDIS_HOST@@|127.0.0.1:${REDIS_PORT}|g" \
|
||||||
|
-e "s|@@REDIS_READ_HOST@@|127.0.0.1:${REDIS_READ_PORT}|g" \
|
||||||
-e "s|@@SCENARIO_DIR@@|${scenario_dir}|g" \
|
-e "s|@@SCENARIO_DIR@@|${scenario_dir}|g" \
|
||||||
"$scenario_dir/dynamic.yml" > "$WORKDIR/dynamic.yml"
|
"$scenario_dir/dynamic.yml" > "$WORKDIR/dynamic.yml"
|
||||||
|
|
||||||
@@ -206,6 +208,7 @@ start_stack() {
|
|||||||
--backend-addr "127.0.0.1:${BACKEND_PORT}" \
|
--backend-addr "127.0.0.1:${BACKEND_PORT}" \
|
||||||
--appsec-addr "127.0.0.1:${APPSEC_PORT}" \
|
--appsec-addr "127.0.0.1:${APPSEC_PORT}" \
|
||||||
--redis-addr "127.0.0.1:${REDIS_PORT}" \
|
--redis-addr "127.0.0.1:${REDIS_PORT}" \
|
||||||
|
--redis-read-addr "127.0.0.1:${REDIS_READ_PORT}" \
|
||||||
"${mock_tls_args[@]}" >"$WORKDIR/mock.log" 2>&1 &
|
"${mock_tls_args[@]}" >"$WORKDIR/mock.log" 2>&1 &
|
||||||
MOCK_PID=$!
|
MOCK_PID=$!
|
||||||
|
|
||||||
|
|||||||
@@ -51,16 +51,19 @@ func list(m map[string]Decision) []Decision {
|
|||||||
|
|
||||||
// --- Redis mock (inline-command wire format, as spoken by simpleredis) ---
|
// --- Redis mock (inline-command wire format, as spoken by simpleredis) ---
|
||||||
|
|
||||||
// serveRedis is a hardcoded stand-in. Every line is scanned for known IPs:
|
// serveRedis is a hardcoded stand-in. When verdicts is true it plays a replica
|
||||||
// 1.2.3.4 → "f", 1.2.3.5 → "t", GET for anything else → miss ($-1).
|
// that holds decisions: every line is scanned for known IPs, 1.2.3.4 → "f"
|
||||||
|
// (clean), 1.2.3.5 → "t" (banned); any other GET is a miss ($-1). When verdicts
|
||||||
|
// is false it plays the primary and answers every GET with a miss, so a
|
||||||
|
// scenario can prove reads are served from the replica and not the primary.
|
||||||
// SET, DEL, AUTH, SELECT get +OK (they don't read the response anyway).
|
// SET, DEL, AUTH, SELECT get +OK (they don't read the response anyway).
|
||||||
func serveRedis(addr string) {
|
func serveRedis(addr string, verdicts bool) {
|
||||||
ln, err := net.Listen("tcp", addr)
|
ln, err := net.Listen("tcp", addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
defer ln.Close()
|
defer ln.Close()
|
||||||
log.Printf("mocklapi: Redis mock listening on %s", addr)
|
log.Printf("mocklapi: Redis mock listening on %s (verdicts:%v)", addr, verdicts)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
conn, err := ln.Accept()
|
conn, err := ln.Accept()
|
||||||
@@ -77,9 +80,9 @@ func serveRedis(addr string) {
|
|||||||
}
|
}
|
||||||
s := string(line)
|
s := string(line)
|
||||||
switch {
|
switch {
|
||||||
case strings.Contains(s, "1.2.3.4"):
|
case verdicts && strings.Contains(s, "1.2.3.4"):
|
||||||
conn.Write([]byte("$1\r\nf\r\n"))
|
conn.Write([]byte("$1\r\nf\r\n"))
|
||||||
case strings.Contains(s, "1.2.3.5"):
|
case verdicts && strings.Contains(s, "1.2.3.5"):
|
||||||
conn.Write([]byte("$1\r\nt\r\n"))
|
conn.Write([]byte("$1\r\nt\r\n"))
|
||||||
case strings.HasPrefix(strings.ToUpper(s), "GET "):
|
case strings.HasPrefix(strings.ToUpper(s), "GET "):
|
||||||
conn.Write([]byte("$-1\r\n"))
|
conn.Write([]byte("$-1\r\n"))
|
||||||
@@ -98,9 +101,12 @@ func main() {
|
|||||||
backendAddr := flag.String("backend-addr", "127.0.0.1:8091", "address for the stub upstream service")
|
backendAddr := flag.String("backend-addr", "127.0.0.1:8091", "address for the stub upstream service")
|
||||||
// AppSec WAF stand-in (the real engine listens on :7422). Not a CRS engine.
|
// AppSec WAF stand-in (the real engine listens on :7422). Not a CRS engine.
|
||||||
appsecAddr := flag.String("appsec-addr", "127.0.0.1:8092", "address for the AppSec mock")
|
appsecAddr := flag.String("appsec-addr", "127.0.0.1:8092", "address for the AppSec mock")
|
||||||
// Redis stand-in: the mock always serves a hardcoded GET on a plain TCP
|
// Redis stand-ins on plain TCP ports, enough to exercise the plugin's redis
|
||||||
// port, enough to exercise the plugin's redis cache path.
|
// cache path. The primary answers every GET with a miss; the replica serves
|
||||||
redisAddr := flag.String("redis-addr", "127.0.0.1:8093", "address for the Redis mock")
|
// the hardcoded verdicts, so a scenario pointing redisCacheReadHosts at the
|
||||||
|
// replica proves reads are offloaded to replicas.
|
||||||
|
redisAddr := flag.String("redis-addr", "127.0.0.1:8093", "address for the Redis primary mock (writes; GET always misses)")
|
||||||
|
redisReadAddr := flag.String("redis-read-addr", "127.0.0.1:8094", "address for the Redis replica mock (serves cached verdicts)")
|
||||||
// Optional TLS for the LAPI: when both are set the LAPI is served over HTTPS
|
// Optional TLS for the LAPI: when both are set the LAPI is served over HTTPS
|
||||||
// (cert signed by the scenario's throwaway CA) so the suite can exercise the
|
// (cert signed by the scenario's throwaway CA) so the suite can exercise the
|
||||||
// bouncer's system-trust-store path. Backend and AppSec stay plaintext.
|
// bouncer's system-trust-store path. Backend and AppSec stay plaintext.
|
||||||
@@ -144,7 +150,8 @@ func main() {
|
|||||||
})))
|
})))
|
||||||
}()
|
}()
|
||||||
|
|
||||||
go serveRedis(*redisAddr)
|
go serveRedis(*redisAddr, false)
|
||||||
|
go serveRedis(*redisReadAddr, true)
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
@@ -204,9 +211,9 @@ func main() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if *lapiTLSCert != "" && *lapiTLSKey != "" {
|
if *lapiTLSCert != "" && *lapiTLSKey != "" {
|
||||||
log.Printf("mocklapi: LAPI on %s (TLS), backend on %s, appsec on %s, redis on %s", *lapiAddr, *backendAddr, *appsecAddr, *redisAddr)
|
log.Printf("mocklapi: LAPI on %s (TLS), backend on %s, appsec on %s, redis on %s (read %s)", *lapiAddr, *backendAddr, *appsecAddr, *redisAddr, *redisReadAddr)
|
||||||
log.Fatal(http.ListenAndServeTLS(*lapiAddr, *lapiTLSCert, *lapiTLSKey, mux))
|
log.Fatal(http.ListenAndServeTLS(*lapiAddr, *lapiTLSCert, *lapiTLSKey, mux))
|
||||||
}
|
}
|
||||||
log.Printf("mocklapi: LAPI on %s, backend on %s, appsec on %s, redis on %s", *lapiAddr, *backendAddr, *appsecAddr, *redisAddr)
|
log.Printf("mocklapi: LAPI on %s, backend on %s, appsec on %s, redis on %s (read %s)", *lapiAddr, *backendAddr, *appsecAddr, *redisAddr, *redisReadAddr)
|
||||||
log.Fatal(http.ListenAndServe(*lapiAddr, mux))
|
log.Fatal(http.ListenAndServe(*lapiAddr, mux))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,5 +23,8 @@ http:
|
|||||||
crowdsecLapiKey: "@@APIKEY@@"
|
crowdsecLapiKey: "@@APIKEY@@"
|
||||||
redisCacheEnabled: "true"
|
redisCacheEnabled: "true"
|
||||||
redisCacheHost: "@@REDIS_HOST@@"
|
redisCacheHost: "@@REDIS_HOST@@"
|
||||||
|
redisCacheReadHosts:
|
||||||
|
- "@@REDIS_READ_HOST@@"
|
||||||
|
- "@@REDIS_READ_HOST@@"
|
||||||
forwardedHeadersTrustedIps:
|
forwardedHeadersTrustedIps:
|
||||||
- "127.0.0.1/32"
|
- "127.0.0.1/32"
|
||||||
|
|||||||
@@ -7,10 +7,13 @@ source "$HERE/../../lib/common.sh"
|
|||||||
|
|
||||||
SCENARIO=redis
|
SCENARIO=redis
|
||||||
|
|
||||||
# Redis cache check: the mock returns "f" (not banned) for 1.2.3.4 and "t"
|
# Redis cache check: reads are offloaded to a replica (redisCacheReadHosts)
|
||||||
# (banned) for 1.2.3.5. All other IPs return a miss, which falls through to the
|
# while writes go to the primary (redisCacheHost). The replica mock returns "f"
|
||||||
# LAPI (no decision → allowed). This proves the plugin reads cached decisions
|
# (not banned) for 1.2.3.4 and "t" (banned) for 1.2.3.5; the primary mock always
|
||||||
# from Redis correctly.
|
# misses. All other IPs miss on the replica and fall through to the LAPI (no
|
||||||
|
# decision → allowed). Because the verdicts live only on the replica, the banned
|
||||||
|
# IP being blocked proves the plugin reads decisions from the replica, not the
|
||||||
|
# primary.
|
||||||
body() {
|
body() {
|
||||||
echo "[$SCENARIO] cached clean IP must pass"
|
echo "[$SCENARIO] cached clean IP must pass"
|
||||||
assert_status "http://127.0.0.1:${WEB_PORT}/foo" 200 -H "X-Forwarded-For: 1.2.3.4"
|
assert_status "http://127.0.0.1:${WEB_PORT}/foo" 200 -H "X-Forwarded-For: 1.2.3.4"
|
||||||
|
|||||||
Reference in New Issue
Block a user