feat: implement V3 Uniform Lookup System with hierarchical overrides and site-based whitelisting

This commit is contained in:
Scott Idem
2026-02-20 14:48:50 -05:00
parent 2b2a2bc00f
commit 6bfbff309a
8 changed files with 390 additions and 51 deletions

96
app/routers/lookup_v3.py Normal file
View File

@@ -0,0 +1,96 @@
from fastapi import APIRouter, Depends, HTTPException, Path, Query, Response, status
from typing import List, Optional
import json
import logging
from app.lib_general_v3 import AccountContext, get_account_context
from app.methods.lookup_methods import get_lookup_list_v3, resolve_lookup_v3
from app.methods.site_methods import load_site_obj
from app.models.response_models import Resp_Body_Base, mk_resp
from app.object_definitions.lookups import lu_obj_li
log = logging.getLogger(__name__)
router = APIRouter()
@router.get("/{lu_type}/list", response_model=Resp_Body_Base)
async def get_v3_lookup_list(
lu_type: str = Path(..., min_length=2, max_length=50),
for_type: Optional[str] = Query(None, min_length=2, max_length=50),
for_id: Optional[int] = Query(None),
site_id: Optional[str] = Query(None, min_length=8, max_length=22),
include_disabled: bool = Query(False),
account_ctx: AccountContext = Depends(get_account_context),
response: Response = Response
):
"""
Returns a hierarchical, ranked, and deduplicated list of lookup records.
Supports Object/Account overrides, negative shadowing, and Site Whitelist policies.
"""
v3_key = f"lu_v3_{lu_type}"
if v3_key not in lu_obj_li:
return mk_resp(data=False, status_code=400, response=response, status_message=f"Lookup type '{lu_type}' not supported in V3.")
# Phase 2: Whitelist Policy Injection
whitelist = None
if site_id:
if site_obj := load_site_obj(site_id=site_id, model_as_dict=True):
# Check if this site belongs to the current account context
# NOTE: site_obj.get('account_id') returns the RANDOM string ID in V3 models
if site_obj.get('account_id') == account_ctx.account_id_random:
cfg = site_obj.get('cfg_json')
if isinstance(cfg, str):
try: cfg = json.loads(cfg)
except: cfg = {}
if isinstance(cfg, dict):
policy = cfg.get('lookup_policy', {})
whitelist = policy.get(lu_type)
else:
return mk_resp(data=False, status_code=403, response=response, status_message="Site does not belong to the authorized account.")
results = get_lookup_list_v3(
lu_type=lu_type,
account_ctx=account_ctx,
for_type=for_type,
for_id=for_id,
include_disabled=include_disabled,
whitelist=whitelist
)
if not results and not include_disabled:
return mk_resp(data=[], status_code=200, response=response, status_message="No active records found.")
return mk_resp(data=results)
@router.get("/{lu_type}/resolve", response_model=Resp_Body_Base)
async def resolve_v3_lookup(
lu_type: str = Path(..., min_length=2, max_length=50),
q: str = Query(..., min_length=1, description="The code, group, or identity to resolve."),
site_id: Optional[str] = Query(None, min_length=8, max_length=22),
account_ctx: AccountContext = Depends(get_account_context),
response: Response = Response
):
"""
Resolves an identity string to the highest-priority hierarchical match.
"""
v3_key = f"lu_v3_{lu_type}"
if v3_key not in lu_obj_li:
return mk_resp(data=False, status_code=400, response=response, status_message=f"Lookup type '{lu_type}' not supported in V3.")
# TODO: Add whitelist support for resolve if needed.
# For now, resolve uses the full ranked list.
identity_fields = lu_obj_li[v3_key].get("searchable_fields", ["group"])
result = resolve_lookup_v3(
lu_type=lu_type,
query=q,
account_ctx=account_ctx,
identity_fields=identity_fields
)
if not result:
return mk_resp(data=None, status_code=404, response=response, status_message=f"Could not resolve '{q}' for lookup '{lu_type}'.")
return mk_resp(data=result)

View File

@@ -7,7 +7,7 @@ from app.routers import (
event_device, event_exhibit, event_exhibit_tracking, event_file, event_importing,
event_location, event_person,
event_presentation, event_presenter, event_session,
flask_cfg, hosted_file, api_v3_actions_hosted_file, api_v3_actions_event_file, api_v3_actions_e_zoom, lookup,
flask_cfg, hosted_file, api_v3_actions_hosted_file, api_v3_actions_event_file, api_v3_actions_e_zoom, lookup, lookup_v3,
organization, page, person,
person_user, qr, site, site_domain, user,
util_email, websockets, websockets_redis, websockets_v3, e_confex, e_cvent, e_impexium, e_stripe
@@ -51,6 +51,7 @@ def setup_routers(app: FastAPI):
app.include_router(api_v3_actions_event_file.router, prefix='/v3/action/event_file', tags=['Event File (V3 Actions)'])
app.include_router(api_v3_actions_e_zoom.router, prefix='/v3/action/e_zoom', tags=['Zoom Events (V3 Actions)'])
app.include_router(lookup.router, prefix='/lu', tags=['Lookup'])
app.include_router(lookup_v3.router, prefix='/v3/lookup', tags=['Lookup V3'])
# app.include_router(organization.router, prefix='/organization', tags=['Organization'], dependencies=[Depends(DeprecationParams)])
# app.include_router(page.router, prefix='/page', tags=['Page'], dependencies=[Depends(DeprecationParams)])