Aug 21 - MT22 logo seam fix

This commit is contained in:
2026-08-21 09:19:42 -04:00
parent 54a4a44bae
commit 6371b13c13
7 changed files with 352 additions and 21 deletions
+25 -17
View File
@@ -29,6 +29,7 @@ from app.models.tenant_settings import TenantSettings
from app.utils.decorators import admin_required
from app.utils.audit import log_action, ACTION_UPDATE, ACTION_CREATE, ACTION_DELETE
from app.utils.time_utils import now_eastern
from app.utils import storage
logger = logging.getLogger(__name__)
@@ -43,7 +44,19 @@ _DOMAIN_RE = re.compile(
# ── helpers ───────────────────────────────────────────────────────────────────
def _save_logo(file_obj):
"""Save uploaded logo to static/uploads/logos/; return relative URL or None."""
"""Save an uploaded logo via the storage seam; return its key or None.
MT-22: this previously wrote straight to ``UPLOAD_FOLDER/logos/`` with
``file_obj.save()`` — the last direct-to-disk writer in the app. On an
R2-backed tenant the logo never reached the bucket, and on the local
backend every tenant's logo landed in one shared directory.
Validation stays here by design: ``storage.py`` only moves bytes, and
callers own extension / magic-byte checks (see its module docstring).
The returned key keeps the exact ``uploads/logos/<file>`` shape already
stored in ``TenantSettings.logo_url``, so no migration and no data rewrite.
"""
if not file_obj or not file_obj.filename:
return None
allowed = {'png', 'jpg', 'jpeg', 'gif', 'svg', 'webp'}
@@ -63,27 +76,22 @@ def _save_logo(file_obj):
}
if not any(header.startswith(m) for m in magic):
return None
import secrets
logos_dir = os.path.join(current_app.config['UPLOAD_FOLDER'], 'logos')
os.makedirs(logos_dir, exist_ok=True)
filename = f'{secrets.token_hex(12)}.{ext}'
file_obj.save(os.path.join(logos_dir, filename))
return f'uploads/logos/{filename}'
file_obj.seek(0)
return storage.save(file_obj, 'logos')
def _delete_logo(logo_url):
"""Remove a logo file from disk. Silently ignores missing files.
Safety guard: only deletes files inside the uploads/logos/ subfolder."""
"""Remove a stored logo. Silently ignores missing objects.
Safety guard: only deletes keys inside the uploads/logos/ subfolder."""
if not logo_url:
return # nothing stored — never issue a delete for 'uploads/logos/'
try:
# Use only the basename to avoid any path-traversal via the stored URL.
# All logos are written into logos_dir by _save_logo(), so joining the
# basename back to that directory is always the correct path.
logos_dir = os.path.join(current_app.config['UPLOAD_FOLDER'], 'logos')
abs_logos = os.path.abspath(logos_dir)
abs_path = os.path.join(abs_logos, os.path.basename(logo_url))
if os.path.isfile(abs_path):
os.remove(abs_path)
logger.info('SETTINGS | logo_deleted | path=%s', abs_path)
# All logos are written to 'uploads/logos/' by _save_logo(), so
# rebuilding the key from the basename is always the correct target.
key = f'uploads/logos/{os.path.basename(str(logo_url or ""))}'
storage.delete(key)
logger.info('SETTINGS | logo_deleted | key=%s', key)
except Exception as exc:
logger.warning('SETTINGS | logo_delete_failed | url=%s err=%s', logo_url, exc)