Unified CSS, JavaScript utilities, HTML template, and framework skeleton files for Tinker Tickets (PHP), PULSE (Node.js), and GANDALF (Flask). Includes aesthetic_diff.md documenting every divergence between the three apps with prioritised recommendations for convergence. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
93 lines
2.9 KiB
Python
93 lines
2.9 KiB
Python
"""
|
|
LOTUSGUILD TERMINAL DESIGN SYSTEM — Flask Auth Helpers
|
|
Provides Authelia SSO integration via remote headers.
|
|
|
|
Usage:
|
|
from auth import require_auth, get_user
|
|
|
|
@app.route('/my-page')
|
|
@require_auth
|
|
def my_page():
|
|
user = get_user()
|
|
return render_template('page.html', user=user)
|
|
"""
|
|
from functools import wraps
|
|
from flask import request, g
|
|
|
|
|
|
def get_user() -> dict:
|
|
"""
|
|
Parse Authelia SSO headers into a normalised user dict.
|
|
Authelia injects these headers after validating the session:
|
|
Remote-User → username / login name
|
|
Remote-Name → display name
|
|
Remote-Email → email address
|
|
Remote-Groups → comma-separated group list
|
|
"""
|
|
groups_raw = request.headers.get('Remote-Groups', '')
|
|
groups = [g.strip() for g in groups_raw.split(',') if g.strip()]
|
|
return {
|
|
'username': request.headers.get('Remote-User', ''),
|
|
'name': request.headers.get('Remote-Name', ''),
|
|
'email': request.headers.get('Remote-Email', ''),
|
|
'groups': groups,
|
|
'is_admin': 'admin' in groups,
|
|
}
|
|
|
|
|
|
def require_auth(f):
|
|
"""
|
|
Decorator that enforces authentication + group-based authorisation.
|
|
Reads allowed_groups from app config (key: 'auth.allowed_groups').
|
|
Falls back to ['admin'] if not configured.
|
|
|
|
Returns 401 if no user header present (request bypassed Authelia).
|
|
Returns 403 if user is not in an allowed group.
|
|
"""
|
|
@wraps(f)
|
|
def wrapper(*args, **kwargs):
|
|
from flask import current_app
|
|
user = get_user()
|
|
|
|
if not user['username']:
|
|
return (
|
|
'<h1 style="font-family:monospace;color:#ff4444">401 — Not Authenticated</h1>'
|
|
'<p style="font-family:monospace">Access this service through Authelia SSO.</p>',
|
|
401,
|
|
)
|
|
|
|
cfg = current_app.config.get('APP_CONFIG', {})
|
|
allowed = cfg.get('auth', {}).get('allowed_groups', ['admin'])
|
|
|
|
if not any(grp in allowed for grp in user['groups']):
|
|
return (
|
|
f'<h1 style="font-family:monospace;color:#ff4444">403 — Access Denied</h1>'
|
|
f'<p style="font-family:monospace">'
|
|
f'Account <strong>{user["username"]}</strong> is not in an allowed group '
|
|
f'({", ".join(allowed)}).</p>',
|
|
403,
|
|
)
|
|
|
|
# Store user on Flask's request context for convenience
|
|
g.user = user
|
|
return f(*args, **kwargs)
|
|
|
|
return wrapper
|
|
|
|
|
|
def require_admin(f):
|
|
"""
|
|
Stricter decorator — requires the 'admin' group regardless of config.
|
|
"""
|
|
@wraps(f)
|
|
@require_auth
|
|
def wrapper(*args, **kwargs):
|
|
user = get_user()
|
|
if not user['is_admin']:
|
|
return (
|
|
'<h1 style="font-family:monospace;color:#ff4444">403 — Admin Required</h1>',
|
|
403,
|
|
)
|
|
return f(*args, **kwargs)
|
|
return wrapper
|