Enforce tenant isolation on verified audit firm domains
This commit is contained in:
@@ -33,6 +33,54 @@ SAFE_POST_LOGIN_REDIRECTS = {
|
||||
"/employee/dashboard",
|
||||
}
|
||||
|
||||
# Verified active audit-firm domains are tenant authentication boundaries.
|
||||
# Marketplace and consultant domain behaviour remains unchanged.
|
||||
TENANT_BOUND_DOMAIN_TYPES = {"audit_firm_domain", "audit_firm_subdomain"}
|
||||
|
||||
|
||||
def _bound_domain_tenant_id(request: Request) -> int | None:
|
||||
"""Return the trusted tenant id bound to the current audit-firm domain.
|
||||
|
||||
DomainResolverMiddleware only marks exact, active and verified mappings as
|
||||
resolved. The additional checks here make the authentication boundary
|
||||
explicit and safe if the middleware evolves later.
|
||||
"""
|
||||
if not bool(getattr(request.state, "domain_resolved", False)):
|
||||
return None
|
||||
if not bool(getattr(request.state, "domain_is_verified", False)):
|
||||
return None
|
||||
if (getattr(request.state, "domain_status", None) or "").strip().lower() != "active":
|
||||
return None
|
||||
if (getattr(request.state, "domain_type", None) or "").strip() not in TENANT_BOUND_DOMAIN_TYPES:
|
||||
return None
|
||||
tenant_id = getattr(request.state, "domain_tenant_id", None)
|
||||
try:
|
||||
return int(tenant_id) if tenant_id not in (None, "", 0, "0") else None
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def _clear_login_session(request: Request) -> None:
|
||||
"""Remove authentication/context state without disturbing CSRF/session middleware."""
|
||||
for key in (
|
||||
SESSION_USER_ID_KEY,
|
||||
SESSION_LOGIN_AT_KEY,
|
||||
"user_email",
|
||||
"tenant_id",
|
||||
"branch_id",
|
||||
"tenant_code",
|
||||
"branch_code",
|
||||
"active_tenant_id",
|
||||
"active_branch_id",
|
||||
"active_tenant_code",
|
||||
"active_branch_code",
|
||||
"active_financial_year",
|
||||
"must_change_password",
|
||||
"post_login_redirect",
|
||||
"otp_verified",
|
||||
):
|
||||
request.session.pop(key, None)
|
||||
|
||||
|
||||
def _consume_safe_post_login_redirect(request: Request) -> str | None:
|
||||
value = request.session.pop(PENDING_POST_LOGIN_REDIRECT_KEY, None)
|
||||
@@ -447,6 +495,20 @@ def login_submit(
|
||||
permissions = _user_permissions(db, user_id)
|
||||
bs = _get_branch_security_policy(db, user)
|
||||
|
||||
# A verified active audit-firm domain is a hard tenant boundary.
|
||||
# System Admin retains the existing platform-support capability, but all
|
||||
# tenant users must belong to the tenant mapped to this hostname.
|
||||
bound_tenant_id = _bound_domain_tenant_id(request)
|
||||
is_system_admin = "System Admin" in set(roles or [])
|
||||
user_tenant_id = int(tenant_id) if tenant_id not in (None, "", 0, "0") else None
|
||||
if bound_tenant_id is not None and not is_system_admin and user_tenant_id != bound_tenant_id:
|
||||
_clear_login_session(request)
|
||||
return _render_login(
|
||||
request,
|
||||
flash="This account does not belong to the firm associated with this domain. Please use your firm's login URL.",
|
||||
status_code=403,
|
||||
)
|
||||
|
||||
request.session[SESSION_USER_ID_KEY] = user_id
|
||||
request.session[SESSION_LOGIN_AT_KEY] = now.isoformat()
|
||||
request.session["user_email"] = user_email
|
||||
|
||||
Reference in New Issue
Block a user