feat(hosted-file): implement hash-based download action and flexible auth

- Adds GET /v3/action/hosted_file/hash/{sha256}/download for direct content-addressable storage access.
- Updates V3 authentication dependencies to support 'api_key' in the query parameter (alias 'api_key').
- Implements auth_method: 'api_key' for machine-to-machine requests that provide a valid key but no user/account context.
- Updates GUIDE__V3_FRONTEND_API.md with the new endpoint and auth options.
This commit is contained in:
Scott Idem
2026-02-03 13:44:07 -05:00
parent faa6de866d
commit 07609bae9a
3 changed files with 71 additions and 9 deletions

View File

@@ -21,9 +21,11 @@ def get_account_context_optional(
x_no_account_id: Optional[str] = Header(None, min_length=3, max_length=100),
x_no_account_id_token: Optional[str] = Query(None, alias='jwt', min_length=11),
x_aether_api_key: Optional[str] = Header(None, min_length=11, max_length=22),
x_aether_api_key_query: Optional[str] = Query(None, alias='api_key', min_length=11, max_length=22),
) -> AccountContext:
"""
Resolves the account context and enforces API Key validation.
Supports API Key in Header or Query param 'api_key'.
Uses DEFERRED imports to prevent circular dependency at startup.
"""
from app.db_sql import redis_lookup_id_random, sql_select
@@ -38,10 +40,12 @@ def get_account_context_optional(
api_key_authorized = False
# 1. Mandatory Machine Auth (API Key)
# This identifies the script/app, regardless of the user/account context.
if x_aether_api_key:
# Prefer header, fallback to query param
key_to_check = x_aether_api_key or x_aether_api_key_query
if key_to_check:
sql = "SELECT * FROM api_key WHERE (public_key = :key OR secret_key = :key) LIMIT 1"
if api_key_results := sql_select(sql=sql, data={'key': x_aether_api_key}):
if api_key_results := sql_select(sql=sql, data={'key': key_to_check}):
# sql_select returns a list when raw SQL is used
api_key_rec = api_key_results[0] if isinstance(api_key_results, list) else api_key_results
@@ -52,14 +56,17 @@ 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.warning(f"Security: API Key {x_aether_api_key} expired/not yet valid.")
log.error(f"Security: API Key {key_to_check} expired/not yet valid.")
else:
log.warning(f"Security: API Key {x_aether_api_key} is disabled.")
log.error(f"Security: API Key {key_to_check} is disabled.")
else:
log.warning(f"Security: API Key {x_aether_api_key} not found.")
log.error(f"Security: API Key {key_to_check} not found.")
# 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'
# A. Resolve via Account ID Header
if x_account_id:
resolved_account_id_random = x_account_id
@@ -85,7 +92,7 @@ def get_account_context_optional(
log.warning("Security: Failed to decode JWT token.")
# Legacy Fallback (just a raw random ID string)
if auth_method == 'guest':
if auth_method in ['guest', 'api_key']:
resolved_account_id_random = x_no_account_id_token
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
@@ -96,6 +103,8 @@ def get_account_context_optional(
resolved_account_id = 1
resolved_account_id_random = '--- NO ACCOUNT ---'
auth_method = 'bypass'
log.info(f"V3 Auth: method={auth_method}, authorized={api_key_authorized}, account={resolved_account_id_random}")
is_admin = (auth_method == 'bypass')
is_manager = (auth_method == 'bypass')
@@ -121,9 +130,10 @@ def get_account_context(
x_no_account_id: Optional[str] = Header(None, min_length=3, max_length=100),
x_no_account_id_token: Optional[str] = Query(None, alias='jwt', min_length=11),
x_aether_api_key: Optional[str] = Header(None, min_length=11, max_length=22),
x_aether_api_key_query: Optional[str] = Query(None, alias='api_key', min_length=11, max_length=22),
) -> 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)
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.')
return ctx