#!/usr/bin/env bash # Replays the six mutations of review axis 2 (entry, sessions, CSRF) against a COPY of platform/. # It edits the copy, runs the packages that must redden, and reverts. Never point it at the repo. # # Usage: mutations-axis2.sh /path/to/copy/platform # env: TM_PLATFORM_TEST_DSN TM_PLATFORM_TEST_ENGINE_BIN TM_PLATFORM_TEST_BOOK_TEMPLATE set -u P=${1:?path to the platform COPY} case "$P" in *projects/textmachine*) echo "refusing to mutate the live repository"; exit 2;; esac cd "$P" || exit 1 SNAP=$(mktemp -d); cp internal/auth/session.go internal/auth/cookie.go internal/httpapi/server.go \ internal/login/dev.go internal/pgstore/sessions.go "$SNAP/" revert() { cp "$SNAP/session.go" internal/auth/; cp "$SNAP/cookie.go" internal/auth/; cp "$SNAP/server.go" internal/httpapi/; cp "$SNAP/dev.go" internal/login/; cp "$SNAP/sessions.go" internal/pgstore/; } trap revert EXIT sub() { python3 - "$1" "$2" "$3" <<'PY' import sys p,old,new=sys.argv[1],sys.argv[2],sys.argv[3] s=open(p).read(); assert old in s, "anchor not found in "+p open(p,'w').write(s.replace(old,new)) PY } run() { echo " -> $*"; go test "$@" -count=1 2>&1 | tail -4; } echo "== M1 (control) Digest returns the plaintext token ==" sub internal/auth/session.go 'func Digest(token string) []byte { sum := sha256.Sum256([]byte(token)) return sum[:] }' 'func Digest(token string) []byte { _ = sha256.Sum256([]byte(token)) return []byte(token) }' run ./internal/auth/; run ./internal/pgstore/ -run 'TestStoredCredentialIsAHashNotTheToken' revert echo "== M2 sign-out stops expiring the session cookie ==" sub internal/auth/cookie.go 'func (c Cookies) ClearSession(w http.ResponseWriter) { c.set(w, c.SessionName(), "", -time.Second) }' \ 'func (c Cookies) ClearSession(w http.ResponseWriter) { c.set(w, c.SessionName(), "", time.Hour) }' run ./internal/auth/ ./internal/login/ ./internal/httpapi/ revert echo "== M2b no cookie is ever expired (session AND login state) ==" sub internal/auth/cookie.go ' maxAge := int(ttl.Seconds()) if ttl < 0 { maxAge = -1 }' ' maxAge := int(ttl.Seconds()) if ttl < 0 { maxAge = 3600 }' run ./internal/auth/ ./internal/login/ ./internal/httpapi/ revert echo "== M3 the /auth subtree loses the CSRF layer ==" sub internal/httpapi/server.go 'mux.Handle("/auth/", LimitBody(DefaultMaxBody)(csrf(d.Login.Routes(d.Auth.Require))))' \ 'mux.Handle("/auth/", LimitBody(DefaultMaxBody)(d.Login.Routes(d.Auth.Require)))' run ./internal/httpapi/ revert echo "== M4 the development sign-in stops rotating the presented session ==" sub internal/login/dev.go ' if old, _, ok := auth.Present(r, d.cookies.SessionName()); ok { if err := d.store.RevokeSession(r.Context(), auth.Digest(old), d.now()); err != nil { d.log.ErrorContext(r.Context(), "cannot revoke the pre-login session", "err", err) } } ' '' run ./internal/login/ revert echo "== M5 the absolute ceiling written to the row is 100x the configured max age ==" sub internal/pgstore/sessions.go 'now.Add(idleTTL), now.Add(maxAge)); err != nil {' \ 'now.Add(idleTTL), now.Add(100*maxAge)); err != nil {' run ./internal/pgstore/ -run 'Session|Credential|Touch|Revoke' run ./internal/config/ run ./internal/auth/ ./internal/login/ ./internal/httpapi/ revert echo "== done; the copy is back to its original state =="