From 9c4dd5df51bc720ee082c0e2b84e082a5a3f34bc Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Mon, 11 May 2026 13:03:37 -0400 Subject: [PATCH] fix: admin-only suppression enforcement, links.html broken date parsing Security: add require_admin decorator; apply to POST/DELETE /api/suppressions and /suppressions page. Previously any user in allowed_groups could create or delete suppressions even though the nav restricts the UI to admins. Bug: links.html "Updated:" timestamp and stale-warning both produced Invalid Date because the raw "YYYY-MM-DD HH:MM:SS UTC" string was appended with 'Z' instead of being normalised through _toIso(). Fix both call sites to use _toIso(), and remove the now-redundant local _toIso redefinition. Style: use `with open(sentinel, 'w'): pass` consistently (was open().close() at avatar JPEG validation path). Co-Authored-By: Claude Sonnet 4.6 --- app.py | 17 ++++++++++++++++- templates/links.html | 5 ++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/app.py b/app.py index 0a900e0..339cd9a 100644 --- a/app.py +++ b/app.py @@ -155,6 +155,17 @@ def require_auth(f): return wrapper +def require_admin(f): + """Decorator: require require_auth AND membership in the 'admin' group.""" + @wraps(f) + def wrapper(*args, **kwargs): + user = _get_user() + if 'admin' not in user.get('groups', []): + return jsonify({'error': 'Admin access required'}), 403 + return f(*args, **kwargs) + return wrapper + + # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- @@ -228,6 +239,7 @@ def inspector(): @app.route('/suppressions') @require_auth +@require_admin def suppressions_page(): user = _get_user() active = db.get_active_suppressions() @@ -323,6 +335,7 @@ def api_get_suppressions(): @app.route('/api/suppressions', methods=['POST']) @require_auth +@require_admin def api_create_suppression(): user = _get_user() data = request.get_json(silent=True) or {} @@ -371,6 +384,7 @@ def api_create_suppression(): @app.route('/api/suppressions/', methods=['DELETE']) @require_auth +@require_admin def api_delete_suppression(sup_id: int): user = _get_user() db.deactivate_suppression(sup_id) @@ -612,7 +626,8 @@ def api_avatar(): avatar_data = avatar_data.encode('latin-1') if avatar_data[:3] != b'\xFF\xD8\xFF': logger.warning(f'Non-JPEG avatar data for {username}') - open(sentinel, 'w').close() + with open(sentinel, 'w'): + pass return '', 404 with open(cache_file, 'wb') as f: diff --git a/templates/links.html b/templates/links.html index fb628fb..7d4df9f 100644 --- a/templates/links.html +++ b/templates/links.html @@ -36,7 +36,6 @@ {% block scripts %}