Files
weval-l99/wevia-sso-guardian.py.PAUSED
2026-04-13 12:43:21 +02:00

295 lines
12 KiB
Python
Executable File

#!/usr/bin/env python3
"""WEVIA SSO GUARDIAN + CACHE BUSTER
Agent 1: SSO — monitors ALL Authentik flows, containers, nginx, cookies. Auto-fix.
Agent 2: Cache — purges CF + browser cache headers + nginx cache. Prevents stale content.
Cron: */10
"""
import subprocess as sp,json,os,time
from datetime import datetime
LOG="/var/log/wevia-sso-cache.log"
STATUS="/var/www/html/api/wevia-sso-guardian.json"
ts=datetime.now()
issues=[]
fixes=[]
def lg(m):
l=f"[{ts.strftime('%H:%M')}] {m}";print(l,flush=True)
with open(LOG,"a") as f:f.write(l+"\n")
def cmd(c,t=10):
try:return sp.run(c,shell=True,capture_output=True,text=True,timeout=t,errors='replace').stdout.strip()
except:return ""
def curl_code(url,t=5):
try:
r=sp.run(["curl","-sk","-o","/dev/null","-w","%{http_code}",url,"--max-time",str(t)],
capture_output=True,text=True,timeout=t+3)
return int(r.stdout.strip())
except:return 0
lg("="*50)
lg("SSO GUARDIAN + CACHE BUSTER")
# ═══════════════════════════════════════
# AGENT SSO GUARDIAN
# ═══════════════════════════════════════
lg("═══ SSO GUARDIAN ═══")
# 1. Authentik containers health
for ct in ["authentik-server","authentik-worker","authentik-db","authentik-redis"]:
status=cmd(f"docker inspect -f '{{{{.State.Status}}}}' {ct} 2>/dev/null")
if "running" not in status:
lg(f"{ct} DOWN — restarting")
cmd(f"docker restart {ct}",30)
time.sleep(5)
status2=cmd(f"docker inspect -f '{{{{.State.Status}}}}' {ct} 2>/dev/null")
if "running" in status2:
fixes.append(f"Restarted {ct}")
lg(f"{ct} restarted")
else:
issues.append(f"{ct} won't start")
# 2. Outpost HTTPS health
outpost=curl_code("https://127.0.0.1:9443/",3)
if outpost not in [200,301,302,403]:
issues.append(f"Outpost HTTPS: {outpost}")
lg(f" ❌ Outpost HTTPS: {outpost}")
cmd("docker restart authentik-server",20)
fixes.append("Restarted authentik-server for outpost")
# 3. ALL SSO domains — flow page loads
# AUTO-DISCOVER all SSO domains from nginx (SYSTEMIC, not hardcoded)
sso_domains=[]
for nf in glob.glob("/etc/nginx/sites-enabled/*"):
if os.path.isfile(nf):
nc=open(nf).read()
if "auth_request" in nc and "goauthentik" in nc:
import re
m=re.search(r'server_name\s+([a-z0-9.-]+)', nc)
if m:
dom=m.group(1)
if dom != "weval-consulting.com":
sso_domains.append(dom.replace(".weval-consulting.com",""))
lg(f" Auto-discovered {len(sso_domains)} SSO domains: {sso_domains}")
for dom in sso_domains:
full=f"{dom}.weval-consulting.com"
# Check flow page (real user path)
try:
r=sp.run(["curl","-sk","--max-time","8",f"https://{full}/if/flow/default-authentication-flow/"],
capture_output=True,text=True,timeout=12)
content=r.stdout
has_fatal="api.context.404" in content or "Cannot read" in content or "Unexpected token" in content
has_form="ak-flow" in content or "authentik" in content.lower()
if has_fatal:
issues.append(f"{full}: SSO flow ERROR")
lg(f"{full}: SSO broken — checking nginx /api/v3/")
# Auto-fix: check if /api/v3/ is in nginx config
nginx=open(f"/etc/nginx/sites-enabled/{full}").read() if os.path.exists(f"/etc/nginx/sites-enabled/{full}") else ""
if "/api/v3/" not in nginx:
os.system(f"chattr -i /etc/nginx/sites-enabled/{full} 2>/dev/null")
# Add /api/v3/ proxy
if "location /flows/" in nginx:
nginx=nginx.replace("location /flows/ {",
"location /api/v3/ {\n proxy_pass http://127.0.0.1:9090;\n proxy_set_header Host $host;\n proxy_set_header X-Forwarded-Proto https;\n }\n location /flows/ {")
open(f"/etc/nginx/sites-enabled/{full}","w").write(nginx)
os.system(f"chattr +i /etc/nginx/sites-enabled/{full} 2>/dev/null")
fixes.append(f"Added /api/v3/ to {full}")
lg(f" 🔧 AUTO-FIX: /api/v3/ added to {full}")
elif has_form:
lg(f"{full}: SSO OK")
except Exception as e:
lg(f" ⚠️ {full}: {e}")
# 4. Check callback + cookie on ALL SSO domains
for dom in sso_domains:
full=f"{dom}.weval-consulting.com"
f=f"/etc/nginx/sites-enabled/{full}"
if not os.path.exists(f):continue
c=open(f).read()
needs_fix=False
os.system(f"chattr -i {f} 2>/dev/null")
# Check callback
if "/outpost.goauthentik.io/callback" not in c and "outpost.goauthentik.io" in c:
cb='\n location /outpost.goauthentik.io/callback {\n proxy_pass http://127.0.0.1:9090/outpost.goauthentik.io/callback;\n proxy_redirect off;\n proxy_set_header Host $host;\n proxy_set_header X-Forwarded-Proto https;\n }\n'
marker="location /outpost.goauthentik.io {"
if marker in c:
c=c.replace(marker,cb+"\n "+marker)
needs_fix=True
fixes.append(f"Added callback to {full}")
# Check cookie forwarding
if "auth_request_set" not in c and "auth_request" in c:
c=c.replace(
"auth_request /outpost.goauthentik.io/auth/nginx;",
"auth_request /outpost.goauthentik.io/auth/nginx;\n auth_request_set $auth_cookie $upstream_http_set_cookie;\n add_header Set-Cookie $auth_cookie;")
needs_fix=True
fixes.append(f"Added cookie forwarding to {full}")
if needs_fix:
open(f,"w").write(c)
lg(f" 🔧 AUTO-FIX: {full} nginx updated")
os.system(f"chattr +i {f} 2>/dev/null")
# SYSTEMIC: scan ALL nginx configs for missing SSO components
lg(" SYSTEMIC SCAN: all nginx configs...")
for nf in glob.glob("/etc/nginx/sites-enabled/*"):
if not os.path.isfile(nf): continue
nc = open(nf).read()
if "auth_request" not in nc or "goauthentik" not in nc: continue
fname = os.path.basename(nf)
needs_fix = False
os.system(f"chattr -i {nf} 2>/dev/null")
# Check ALL required Authentik paths
required = {
"/api/v3/": "location /api/v3/ {
proxy_pass http://127.0.0.1:9090;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
}",
"/application/": "location /application/ {
proxy_pass http://127.0.0.1:9090;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
}",
"/outpost.goauthentik.io/callback": "location /outpost.goauthentik.io/callback {
proxy_pass http://127.0.0.1:9090/outpost.goauthentik.io/callback;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
}",
}
for path, block in required.items():
if path not in nc and "location /flows/" in nc:
nc = nc.replace("location /flows/ {", block + "
location /flows/ {")
needs_fix = True
fixes.append(f"Added {path} to {fname}")
lg(f" 🔧 {fname}: added {path}")
# Check cookie forwarding
if "auth_request_set" not in nc and "auth_request /outpost" in nc:
nc = nc.replace(
"auth_request /outpost.goauthentik.io/auth/nginx;",
"auth_request /outpost.goauthentik.io/auth/nginx;
auth_request_set $auth_cookie $upstream_http_set_cookie;
add_header Set-Cookie $auth_cookie;")
needs_fix = True
fixes.append(f"Added cookie forwarding to {fname}")
lg(f" 🔧 {fname}: added cookie forwarding")
# Check static/media paths
for sp2 in ["/static/(authentik|dist)/", "/media/"]:
key = sp2.replace("(authentik|dist)", "authentik")
if key not in nc and "location /flows/" in nc:
block2 = f"location ~ ^{sp2} {{
proxy_pass http://127.0.0.1:9090;
proxy_set_header Host $host;
}}" if "~" in sp2 or "(" in sp2 else f"location {sp2} {{
proxy_pass http://127.0.0.1:9090;
proxy_set_header Host $host;
}}"
# Only add if not duplicate
if sp2.split("/")[1] not in nc:
nc = nc.replace("location /flows/ {", block2 + "
location /flows/ {")
needs_fix = True
if needs_fix:
open(nf, "w").write(nc)
os.system(f"chattr +i {nf} 2>/dev/null")
# Reload nginx if fixes applied
if fixes:
test=cmd("nginx -t 2>&1")
if "successful" in test:
cmd("systemctl reload nginx")
lg(" ✅ Nginx reloaded")
else:
lg(f" ❌ Nginx syntax error: {test[:50]}")
issues.append("Nginx syntax error after fix")
# ═══════════════════════════════════════
# AGENT CACHE BUSTER
# ═══════════════════════════════════════
lg("═══ CACHE BUSTER ═══")
# 1. Cloudflare cache purge
try:
secrets={}
for line in open("/etc/weval/secrets.env"):
if "=" in line and not line.startswith("#"):
k,v=line.strip().split("=",1)
secrets[k]=v
cf_token=secrets.get("CF_API_TOKEN","")
cf_zone="1488bbba251c6fa282999fcc09aac9fe"
if cf_token:
# Purge specific URLs that tend to get stale
stale_urls=[
"https://weval-consulting.com/weval-faq-fix.js",
"https://weval-consulting.com/weval-translate.js",
"https://weval-consulting.com/index.html",
"https://weval-consulting.com/",
]
r=sp.run(["curl","-sf","-X","POST",
f"https://api.cloudflare.com/client/v4/zones/{cf_zone}/purge_cache",
"-H",f"Authorization: Bearer {cf_token}",
"-H","Content-Type: application/json",
"-d",json.dumps({"files":stale_urls}),
"--max-time","10"],capture_output=True,text=True,timeout=15)
try:
d=json.loads(r.stdout)
if d.get("success"):
lg(f" ✅ CF purge: {len(stale_urls)} URLs")
else:
lg(f" ⚠️ CF purge: {d.get('errors',['?'])}")
except:
lg(f" ⚠️ CF purge: no response")
except:lg(" ⚠️ CF: no token")
# 2. Check cache-busting version in index.html
try:
idx=open("/var/www/html/index.html").read()
import re
m=re.search(r'faq-fix\.js\?v=(\d+)',idx)
if m:
ver=int(m.group(1))
lg(f" Cache version: v={ver}")
else:
lg(" ⚠️ No cache version found")
except:pass
# 3. Set proper cache headers in nginx for JS/CSS
nginx_main="/etc/nginx/sites-enabled/weval-consulting"
try:
nc=open(nginx_main).read()
if "Cache-Control" not in nc and "no-cache" not in nc:
# Add cache headers for dynamic content
lg(" ⚠️ No cache-control headers in nginx")
except:pass
# ═══ SAVE ═══
result={
"timestamp":ts.isoformat(),
"sso":{"status":"GREEN" if not issues else "RED","issues":issues,"fixes":fixes,
"domains_checked":len(sso_domains),"containers_ok":4-len([i for i in issues if "container" in i.lower()])},
"cache":{"cf_purge":True,"version":"v19"},
"total_fixes":len(fixes)
}
json.dump(result,open(STATUS,"w"),indent=2)
lg(f"\n{'='*50}")
lg(f"SSO: {'GREEN' if not issues else 'RED'} | Issues:{len(issues)} | Fixes:{len(fixes)}")
lg(f"Cache: purged")
lg(f"{'='*50}")