diff --git a/app/models/tenant_settings.py b/app/models/tenant_settings.py index 2ffe627..a20c135 100644 --- a/app/models/tenant_settings.py +++ b/app/models/tenant_settings.py @@ -7,6 +7,8 @@ One row per tenant DB. Created on first save; reads fall back to defaults when no row exists so tenant-zero needs zero data migration. """ +import sqlalchemy.exc + from app import db from app.utils.time_utils import now_eastern @@ -44,9 +46,9 @@ class TenantSettings(db.Model): """ try: row = cls.query.first() - except Exception: - # ProgrammingError: Table 'tenant_settings' doesn't exist - # (phase33 migration not yet applied to this tenant DB) + except (sqlalchemy.exc.ProgrammingError, sqlalchemy.exc.OperationalError): + # Table 'tenant_settings' doesn't exist — phase33 migration not yet applied. + # Any other DB error (connection failure, permission error) is re-raised. return None if row is not None: return row diff --git a/app/routes/auth.py b/app/routes/auth.py index f9b2015..ae7dd64 100644 --- a/app/routes/auth.py +++ b/app/routes/auth.py @@ -524,6 +524,7 @@ def reset_password(token): # ── MT-4: Superadmin impersonation ──────────────────────────────────────────── @bp.route('/impersonate') +@login_required def impersonate_entry(): """ Validate a superadmin impersonation token and bind the session to a tenant. @@ -531,7 +532,8 @@ def impersonate_entry(): GET /auth/impersonate?token= Sets session['impersonating_tenant_id'] which the tenancy middleware reads to short-circuit normal Host resolution for the duration of the session. - This route is CSRF-exempt by nature — the HMAC token already provides auth. + Requires an authenticated user so the HMAC token alone cannot grant access + to an anonymous session. """ from flask import session as flask_session token = request.args.get('token', '') @@ -550,6 +552,11 @@ def impersonate_entry(): tenant_id = payload.get('tid') superadmin_id = payload.get('said') + if tenant_id is None: + logger.warning('AUTH | impersonate_invalid | reason=missing tid in payload') + flash('Invalid impersonation token (missing tenant).', 'danger') + return redirect(url_for('auth.login')) + flask_session['impersonating_tenant_id'] = tenant_id flask_session['impersonating_superadmin_id'] = superadmin_id diff --git a/app/routes/facilities.py b/app/routes/facilities.py index cab2bd6..9f1caad 100644 --- a/app/routes/facilities.py +++ b/app/routes/facilities.py @@ -9,7 +9,6 @@ from app.utils.decorators import supervisor_required, admin_required from app.utils.audit import log_action, ACTION_CREATE, ACTION_UPDATE, ACTION_DELETE from app.utils.scope import get_customer_scope, get_inspector_scope from app.tenancy.gates import quota_soft_check -from app.tenancy.gates import feature_required bp = Blueprint('facilities', __name__, url_prefix='/facilities') @@ -17,8 +16,6 @@ logger = logging.getLogger(__name__) @bp.route('/') @login_required -@supervisor_required -@feature_required('scheduled_reports') def list_facilities(): if current_user.role == 'customer': cids = get_customer_scope(current_user) or [] diff --git a/app/routes/tenant_settings.py b/app/routes/tenant_settings.py index 0b245ec..bc38634 100644 --- a/app/routes/tenant_settings.py +++ b/app/routes/tenant_settings.py @@ -119,6 +119,10 @@ def branding(): branding_allowed = tenant.allow_branding settings = TenantSettings.get_or_default() + if settings is None: + flash('Branding settings are not available yet — the phase33 migration has not been applied to this tenant DB. ' + 'Run: python -m control.tenant_migrate upgrade --tenant ', 'warning') + return redirect(url_for('dashboard.index')) if request.method == 'POST': if not branding_allowed: