alpinux.site.2026/admin/app.py
Cédrix 80574a83f3 fix: admin.alpinux.org — sous-domaine dédié conforme à la convention ISPConfig
Remplace portail.alpinux.org.admin.conf (snippet incorrectement formaté)
par scripts/admin.alpinux.org.vhost.conf : VirtualHost complet HTTP+HTTPS,
reverse proxy Gunicorn port 5002, même structure que les autres vhosts.

admin/app.py : supprime x_prefix=1 du ProxyFix (plus de sous-chemin /admin/)
admin/.env.example : client Keycloak renommé admin-alpinux
scripts/alpinux-admin.service : description mise à jour

redirect_uri Keycloak attendu : https://admin.alpinux.org/auth/callback

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 16:01:47 +02:00

120 lines
3.9 KiB
Python

import os
from flask import Flask, redirect, url_for, session, request, jsonify, render_template, abort
from authlib.integrations.flask_client import OAuth
from werkzeug.middleware.proxy_fix import ProxyFix
import builds
app = Flask(__name__)
app.secret_key = os.environ["SECRET_KEY"]
# Gère X-Forwarded-Proto envoyé par Apache
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1)
# ── OIDC AlpID ────────────────────────────────────────────────────
oauth = OAuth(app)
oauth.register(
name="alpid",
server_metadata_url=os.environ["ALPID_DISCOVERY_URL"],
client_id=os.environ["ALPID_CLIENT_ID"],
client_secret=os.environ["ALPID_CLIENT_SECRET"],
client_kwargs={"scope": "openid profile email groups"},
)
ADMIN_GROUPS = set(os.environ.get("ADMIN_GROUPS", "admins").split(","))
def _user():
return session.get("user")
def _require_admin():
user = _user()
if not user:
session["next_url"] = request.url
return redirect(url_for("login"))
if not user.get("is_admin"):
abort(403)
return None
# ── Auth ──────────────────────────────────────────────────────────
@app.route("/auth/login")
def login():
return oauth.alpid.authorize_redirect(url_for("callback", _external=True))
@app.route("/auth/callback")
def callback():
token = oauth.alpid.authorize_access_token()
info = token.get("userinfo") or oauth.alpid.userinfo(token=token)
# Keycloak expose les groupes dans le claim "groups" (à activer dans le mapper)
groups = set(info.get("groups", []))
is_admin = bool(groups & ADMIN_GROUPS)
session["user"] = {
"sub": info["sub"],
"name": info.get("name") or info.get("preferred_username", "Admin"),
"email": info.get("email", ""),
"is_admin": is_admin,
}
return redirect(session.pop("next_url", url_for("index")))
@app.route("/auth/logout")
def logout():
session.clear()
return redirect(url_for("index"))
# ── Pages ─────────────────────────────────────────────────────────
@app.route("/")
def index():
redir = _require_admin()
if redir:
return redir
state = builds.get_state()
log = builds.get_log()
return render_template("index.html", user=_user(), state=state, log=log)
# ── API JSON ──────────────────────────────────────────────────────
@app.route("/api/build", methods=["POST"])
def api_build():
redir = _require_admin()
if redir:
return jsonify({"error": "non authentifié"}), 401
user = _user()
ok = builds.start_build(triggered_by=user["name"])
if ok:
return jsonify({"started": True})
return jsonify({"started": False, "reason": "Un build est déjà en cours"}), 409
@app.route("/api/status")
def api_status():
redir = _require_admin()
if redir:
return jsonify({"error": "non authentifié"}), 401
state = builds.get_state()
return jsonify({
"running": state.get("running", False),
"last_success": state.get("last_success"),
"last_duration": state.get("last_duration_s"),
"started_at": state.get("started_at"),
"triggered_by": state.get("triggered_by"),
"history": state.get("history", [])[:5],
})
@app.route("/api/log")
def api_log():
redir = _require_admin()
if redir:
return jsonify({"error": "non authentifié"}), 401
return jsonify({"log": builds.get_log()})
if __name__ == "__main__":
app.run(debug=False, port=5002)