Mini-app Flask (admin/) accessible à https://portail.alpinux.org/admin/ : - Authentification AlpID OIDC, accès restreint au groupe « admins » Keycloak - Bouton « Lancer mkdocs build » avec confirmation - Exécution de deploy-wiki.sh en arrière-plan (thread), log capturé en direct - Polling JS toutes les 2s pendant le build (status + log + historique) - Affichage du journal en terminal sombre avec suivi automatique - Historique des 20 derniers builds (date, déclencheur, résultat, durée) - ProxyFix pour X-Forwarded-Proto / X-Script-Name (Apache reverse proxy) Infrastructure : - scripts/portail.alpinux.org.admin.conf : bloc ProxyPass pour ISPConfig - scripts/alpinux-admin.service : systemd Gunicorn port 5002 - admin/.env.example, admin/requirements.txt Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
120 lines
3.9 KiB
Python
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 et X-Script-Name envoyés par Apache
|
|
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_prefix=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)
|