Aug 20 - Session tenant binding
This commit is contained in:
@@ -8,6 +8,7 @@ from app.utils import mfa
|
||||
import logging
|
||||
from app.utils.audit import log_action, ACTION_CREATE, ACTION_UPDATE, ACTION_DELETE, ACTION_LOGIN, ACTION_LOGOUT
|
||||
from app.tenancy.gates import quota_soft_check
|
||||
from app.tenancy.session_binding import bind_session_tenant, SESSION_TENANT_KEY
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -40,6 +41,7 @@ def login():
|
||||
# second-factor step. Password is verified; identity is NOT yet
|
||||
# established until the code is confirmed at /auth/mfa.
|
||||
if user.mfa_enabled and user.mfa_secret:
|
||||
bind_session_tenant() # MT-21: tag before identity enters the session
|
||||
session['mfa_pending_user_id'] = user.id
|
||||
session['mfa_pending_remember'] = bool(form.remember_me.data)
|
||||
session['mfa_pending_next'] = safe_redirect_url(request.args.get('next'))
|
||||
@@ -47,6 +49,7 @@ def login():
|
||||
return redirect(url_for('auth.mfa_challenge'))
|
||||
|
||||
login_user(user, remember=form.remember_me.data)
|
||||
bind_session_tenant() # MT-21
|
||||
# Use validated next URL — never redirect blindly to request.args['next']
|
||||
next_page = safe_redirect_url(request.args.get('next'))
|
||||
log_action(ACTION_LOGIN, 'User', user.id, user.username)
|
||||
@@ -106,6 +109,7 @@ def mfa_challenge():
|
||||
next_page = session.pop('mfa_pending_next', None)
|
||||
session.pop('mfa_pending_user_id', None)
|
||||
login_user(user, remember=remember)
|
||||
bind_session_tenant() # MT-21
|
||||
log_action(ACTION_LOGIN, 'User', user.id, user.username, f'2fa via {via}')
|
||||
if via == 'recovery':
|
||||
remaining_n = len(user.mfa_recovery_codes or [])
|
||||
@@ -948,6 +952,23 @@ def impersonate_entry():
|
||||
flask_session['impersonating_tenant_id'] = tenant_id
|
||||
flask_session['impersonating_superadmin_id'] = superadmin_id
|
||||
|
||||
# ── MT-21: re-tag the session for the impersonated tenant ────────────────
|
||||
# Identity in the session is tenant-tagged (User.get_id), and from the next
|
||||
# request onward the middleware binds the impersonated tenant's database.
|
||||
# Without re-tagging, load_user() would correctly reject the tag issued by
|
||||
# the host tenant and the superadmin would land on a login page.
|
||||
#
|
||||
# This preserves the pre-existing impersonation semantics EXACTLY: the
|
||||
# numeric user id carries over, so the superadmin is loaded as the
|
||||
# same-numbered user in the target tenant's database. That behaviour is
|
||||
# arbitrary and worth revisiting (see MULTI_TENANT_PLAN.md open items) —
|
||||
# but changing it is a separate decision, not a security fix.
|
||||
flask_session[SESSION_TENANT_KEY] = tenant_id
|
||||
raw_uid = flask_session.get('_user_id')
|
||||
if raw_uid is not None:
|
||||
numeric_uid = str(raw_uid).rpartition(':')[2]
|
||||
flask_session['_user_id'] = f'{tenant_id}:{numeric_uid}'
|
||||
|
||||
logger.info('AUTH | impersonate_start | sa=%s tenant=%s', superadmin_id, tenant_id)
|
||||
|
||||
import os
|
||||
@@ -971,6 +992,11 @@ def impersonate_end():
|
||||
import os
|
||||
flask_session.pop('impersonating_tenant_id', None)
|
||||
flask_session.pop('impersonating_superadmin_id', None)
|
||||
# MT-21: the session identity is still tagged for the impersonated tenant.
|
||||
# Drop it rather than carrying it back to the superadmin's own host, where
|
||||
# the middleware would clear it on tenant mismatch anyway.
|
||||
logout_user()
|
||||
flask_session.clear()
|
||||
panel_url = f"https://admin.{os.environ.get('TENANT_BASE_DOMAIN', 'jqc.app')}"
|
||||
logger.info('AUTH | impersonate_end | redirecting to panel')
|
||||
return redirect(panel_url)
|
||||
Reference in New Issue
Block a user