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

@@ -1,7 +1,7 @@
import datetime, pytz
from typing import Dict, List, Optional, Set, Union
from pydantic import BaseModel, EmailStr, Field, Json, PrivateAttr, ValidationError, validator
from pydantic import BaseModel, EmailStr, Field, Json, PrivateAttr, ValidationError, validator, root_validator
from app.db_sql import redis_lookup_id_random
from app.lib_general import log, logging
@@ -14,15 +14,39 @@ class Archive_Base(BaseModel):
log.setLevel(logging.WARNING) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL
log.debug(locals())
id_random: Optional[str] = Field(
**base_fields['archive_id_random'],
alias = 'archive_id_random',
)
id: Optional[int] = Field(
alias = 'archive_id'
)
account_id_random: Optional[str]
account_id: Optional[int]
# --- Standardized Vision IDs (Strings for API, Integers for DB) ---
id: Optional[Union[int, str]] = Field(None, **base_fields['archive_id_random'])
archive_id: Optional[Union[int, str]] = Field(None, **base_fields['archive_id_random'])
account_id: Optional[Union[int, str]] = Field(None, **base_fields['account_id_random'])
# --- Standardized Legacy / Internal IDs (Excluded) ---
id_random: Optional[str] = Field(None, alias='archive_id_random', exclude=True)
account_id_random: Optional[str] = Field(None, exclude=True)
@root_validator(pre=True)
def map_v3_ids(cls, values):
"""
Vision Transformer:
Map DB keys to clean API keys and strip internal integers during READ operations.
During CREATE (POST) operations, we ensure resolved integers are preserved.
"""
# 1. Map Random Strings to Clean Names
rid = values.get('id_random') or values.get('archive_id_random')
if rid and isinstance(rid, str):
values['id'] = rid
values['archive_id'] = rid
if a_rid := values.get('account_id_random'): values['account_id'] = a_rid
# 2. Prevent leakage of integers during API responses (Vision Standard)
for k in ['id', 'archive_id', 'account_id']:
val = values.get(k)
if val is not None and not isinstance(val, str):
if values.get(f'{k}_random') or (k=='id' and values.get('id_random')):
del values[k]
return values
archive_type_id: Optional[int]
archive_type: Optional[str]
@@ -70,22 +94,8 @@ class Archive_Base(BaseModel):
_processed_at: datetime.datetime = PrivateAttr(default_factory=datetime.datetime.now)
@validator('id', always=True)
def archive_id_lookup(cls, v, values, **kwargs):
if isinstance(v, int) and v > 0: return v
elif id_random := values.get('id_random'):
return redis_lookup_id_random(record_id_random=id_random, table_name='archive')
return None
@validator('account_id', always=True)
def account_id_lookup(cls, v, values, **kwargs):
if isinstance(v, int) and v > 0: return v
elif id_random := values.get('account_id_random'):
return redis_lookup_id_random(record_id_random=id_random, table_name='account')
return None
class Config:
underscore_attrs_are_private = True
allow_population_by_field_name = True
allow_population_by_field_name = False
fields = base_fields
# ### END ### API Archive Models ### Archive_Base() ###