security(v3): harden multi-tenant isolation and enhance failure feedback

This commit is contained in:
Scott Idem
2026-02-13 18:45:20 -05:00
parent 61e17f1efa
commit 2266f149f7
15 changed files with 389 additions and 317 deletions

View File

@@ -38,6 +38,7 @@ def get_account_context_optional(
resolved_token_payload = None
auth_method = 'guest'
api_key_authorized = False
auth_error = None
# 1. Mandatory Machine Auth (API Key)
# Prefer header, fallback to query param
@@ -56,16 +57,22 @@ def get_account_context_optional(
if (not enable_from or enable_from <= now) and (not enable_to or now <= enable_to):
api_key_authorized = True
else:
log.error(f"Security: API Key {key_to_check} expired/not yet valid.")
auth_error = "API Key expired or not yet valid."
log.error(f"Security: {auth_error} Key: {key_to_check}")
else:
log.error(f"Security: API Key {key_to_check} is disabled.")
auth_error = "API Key is disabled."
log.error(f"Security: {auth_error} Key: {key_to_check}")
else:
log.error(f"Security: API Key {key_to_check} not found.")
auth_error = "API Key not found or invalid."
log.error(f"Security: {auth_error} Key: {key_to_check}")
else:
auth_error = "Mandatory API Key missing."
# 2. Context Resolution (Only if API Key is valid)
if api_key_authorized:
# Default to machine auth if no account context is provided
auth_method = 'api_key'
auth_error = "Account context required for this operation."
# A. Resolve via Account ID Header
if x_account_id:
@@ -73,6 +80,9 @@ def get_account_context_optional(
if looked_up_id := redis_lookup_id_random(table_name='account', record_id_random=x_account_id):
resolved_account_id = looked_up_id
auth_method = 'account_header'
auth_error = None
else:
auth_error = f"Account ID '{x_account_id}' not found."
# B. Resolve via JWT / Token Query Param
elif x_no_account_id_token:
@@ -88,23 +98,34 @@ def get_account_context_optional(
if looked_up_id := redis_lookup_id_random(table_name='account', record_id_random=resolved_account_id_random):
resolved_account_id = looked_up_id
auth_method = 'jwt_token'
auth_error = None
else:
auth_error = f"Account ID '{resolved_account_id_random}' from token not found."
else:
# JWT is valid but has no account_id (e.g. platform-wide guest)
# We keep auth_method as 'jwt_token' but account_id as None.
auth_method = 'jwt_token'
auth_error = "Valid token provided, but no account context found in payload."
else:
log.warning("Security: Failed to decode JWT token.")
auth_error = "Failed to decode JWT token."
log.warning(f"Security: {auth_error}")
# Legacy Fallback (just a raw random ID string)
if auth_method in ['guest', 'api_key']:
resolved_account_id_random = x_no_account_id_token
if auth_method in ['guest', 'api_key', 'jwt_token'] and auth_error:
if looked_up_id := redis_lookup_id_random(table_name='account', record_id_random=x_no_account_id_token):
resolved_account_id = looked_up_id
resolved_account_id_random = x_no_account_id_token
auth_method = 'token_query'
auth_error = None
# C. Resolve via Administrative Bypass
elif x_no_account_id and x_no_account_id.lower() not in ['false', '0', 'null', 'undefined', 'none', 'no_account_id_here']:
resolved_account_id = 1
resolved_account_id_random = '--- NO ACCOUNT ---'
auth_method = 'bypass'
auth_error = None
log.info(f"V3 Auth: method={auth_method}, authorized={api_key_authorized}, account={resolved_account_id_random}")
log.info(f"V3 Auth: method={auth_method}, authorized={api_key_authorized}, account={resolved_account_id_random}, error={auth_error}")
is_admin = (auth_method == 'bypass')
is_manager = (auth_method == 'bypass')
@@ -122,7 +143,8 @@ def get_account_context_optional(
administrator=is_admin,
manager=is_manager,
super=is_super,
token_payload=resolved_token_payload
token_payload=resolved_token_payload,
auth_error=auth_error
)
def get_account_context(
@@ -134,8 +156,9 @@ def get_account_context(
) -> AccountContext:
"""Strict version of account context resolution."""
ctx = get_account_context_optional(x_account_id, x_no_account_id, x_no_account_id_token, x_aether_api_key, x_aether_api_key_query)
if ctx.auth_method == 'guest':
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='Account context required.')
if ctx.auth_method == 'guest' or (ctx.account_id is None and not ctx.super):
reason = ctx.auth_error or "Account context required."
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=reason)
return ctx