Sep 16 - Optimize code, part 1

This commit is contained in:
2026-09-16 10:52:59 -04:00
parent 4212726611
commit 7626287344
22 changed files with 587 additions and 96 deletions
+73
View File
@@ -0,0 +1,73 @@
"""
utils/excel_safety.py
=====================
Protection against spreadsheet formula injection in exports.
Text that reaches an export can come from the public check-in page (address,
device, selected location), from imported time-clock files, or from the legacy
database. In an .xlsx file a cell whose value starts with "=" is stored as a
formula; in a CSV a value starting with = + - @ tab or CR is evaluated when the
file is opened in Excel. These helpers keep such text inert while the formulas
the exports generate on purpose (map HYPERLINKs, SUM / SUMIF totals) keep working.
"""
import re
# Formulas the exports generate themselves. Any other cell value starting with
# "=" is written as plain text. HYPERLINKs are accepted only with a Google Maps
# target and correctly escaped string arguments ("" for a quote), so user text
# inside them can never close the string and append another function.
_ALLOWED_FORMULA_PATTERNS = (
re.compile(
r'^=HYPERLINK\("(?:https://www\.google\.com/maps/place/|http://maps\.google\.com/maps\?q=)'
r'(?:[^"]|"")*","(?:[^"]|"")*"\)$'
),
re.compile(r'^=SUM\(\$?[A-Z]{1,3}\$?\d+:\$?[A-Z]{1,3}\$?\d+\)$'),
re.compile(
r'^=SUMIF\(\$?[A-Z]{1,3}\$?\d+:\$?[A-Z]{1,3}\$?\d+,'
r'\$?[A-Z]{1,3}\$?\d+,'
r'\$?[A-Z]{1,3}\$?\d+:\$?[A-Z]{1,3}\$?\d+\)$'
),
)
_CSV_FORMULA_PREFIXES = ('=', '+', '-', '@', '\t', '\r')
def excel_escape_string(text):
"""Escape text for use inside a double-quoted string argument of an Excel formula."""
value = '' if text is None else str(text)
return value.replace('"', '""').replace('\r', ' ').replace('\n', ' ')
def excel_hyperlink(url, display_text):
"""=HYPERLINK("url","display text") with both arguments safely quoted."""
return f'=HYPERLINK("{excel_escape_string(url)}","{excel_escape_string(display_text)}")'
def is_allowed_formula(value):
"""True for the formula shapes the exports generate on purpose."""
return any(pattern.match(value) for pattern in _ALLOWED_FORMULA_PATTERNS)
def neutralize_unexpected_formulas(workbook):
"""
Store every formula the app did not generate itself as plain text.
Call right before saving an export workbook. Returns the number of cells
changed. The cell keeps its visible text; it is just never evaluated.
"""
changed = 0
for worksheet in workbook.worksheets:
for row in worksheet.iter_rows():
for cell in row:
value = cell.value
if isinstance(value, str) and value.startswith('=') and not is_allowed_formula(value):
cell.data_type = 's' # written as a string, not as <f>
changed += 1
return changed
def csv_safe(value):
"""A CSV value that Excel will not evaluate as a formula (leading apostrophe)."""
if isinstance(value, str) and value.startswith(_CSV_FORMULA_PREFIXES):
return "'" + value
return value
+57 -1
View File
@@ -16,7 +16,7 @@ from datetime import datetime, date, time, timedelta
from functools import wraps
import qrcode
from flask import session, redirect, flash, request, url_for
from flask import session, redirect, flash, request, url_for, jsonify
from user_agents import parse
from extensions import logger_handler
@@ -323,6 +323,62 @@ def staff_or_admin_required(f):
return decorated_function
# Role sets for areas the sidebar only offers to some roles. Enforced on the
# server so a user cannot reach them by typing the URL (base_authenticated.html
# is the reference for who sees what).
PAYROLL_AREA_ROLES = ('admin', 'payroll', 'accounting') # Time Attendance, Legacy, Employees, Statistics
QR_MANAGEMENT_ROLES = ('admin', 'staff', 'payroll', 'accounting') # create / edit / bulk-import QR codes
def _wants_json_response():
"""API and fetch() callers get JSON errors instead of a redirect to an HTML page."""
return (request.path.startswith('/api/')
or request.headers.get('X-Requested-With') == 'XMLHttpRequest'
or request.accept_mimetypes.best == 'application/json')
def _role_denied_response(allowed_roles):
"""None when the logged-in user's role is allowed; otherwise the response to return."""
if 'user_id' not in session:
if _wants_json_response():
return jsonify({'success': False, 'error': 'Authentication required'}), 401
flash('Please log in to access this page.', 'error')
return redirect(url_for('auth.login'))
role = session.get('role')
if role in allowed_roles:
return None
logger_handler.logger.warning(
f"Access denied: user {session.get('username')} (role {role}) -> {request.endpoint}"
)
if _wants_json_response():
return jsonify({'success': False, 'error': 'Access denied'}), 403
flash('You do not have permission to access that page.', 'error')
return redirect(url_for('dashboard.dashboard'))
def roles_required(*allowed_roles):
"""Decorator: the user must be logged in with one of allowed_roles."""
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
denied = _role_denied_response(allowed_roles)
if denied is not None:
return denied
return f(*args, **kwargs)
return decorated_function
return decorator
def restrict_blueprint_to_roles(blueprint, allowed_roles):
"""Apply the role check to every route of a blueprint (one line per module)."""
@blueprint.before_request
def _enforce_blueprint_roles():
return _role_denied_response(allowed_roles)
return blueprint
def is_admin_user(user_id):
"""Helper function to safely check if user is admin"""
from extensions import db