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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user