fix: Enforce strict tier-based access control for node endpoints

Access control hierarchy:
- Anonymous (free): db-sync read-only ONLY, no node access
- Standard (≥50 TRP): db-sync + node read (UTxOs, protocol-params)
- Elevated (≥500 TRP): everything + tx submit
- Master: unrestricted

Node endpoints now return HTTP 403 for insufficient tier:
- GET /v1/address/{addr}/utxos → requires standard+
- GET /v1/protocol-params → requires standard+
- POST /v1/tx/submit → requires elevated+ (403 for standard/anonymous)

Added require_standard_tier and require_elevated_tier dependencies.
This commit is contained in:
Kayos 2026-03-21 09:15:40 -07:00
parent 163de03322
commit d5fbec496f
2 changed files with 61 additions and 14 deletions

View file

@ -5,7 +5,6 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
ca-certificates \
libsodium23 \
libsecp256k1-1 \
libnuma1 \
&& rm -rf /var/lib/apt/lists/*

74
main.py
View file

@ -8,10 +8,19 @@ Known policy IDs:
- TRP: 9c4bd4a90cdb73d9ff681215ecf7dea9fb183d916d30487d17098e05
- MAP: 24bd9e7b9ae3a61df79eca72fd8355d0f7767e4c55a04a0d919c019c
TRP Gating Tiers:
- 0 TRP anonymous rate limits (20 req/min)
- 50+ TRP standard tier (100 req/min)
- 500+ TRP elevated tier (1000 req/min)
Access Tiers (strictly enforced):
- Anonymous (0 TRP, 20 req/min):
db-sync read-only ONLY. Balance, transactions, tokens, blocks, assets, pools, sync status.
NO node access whatsoever.
- Standard (50 TRP, 100 req/min):
db-sync read + node read-only. UTxO queries, protocol params.
NO transaction submission.
- Elevated (500 TRP, 1000 req/min):
Everything above + POST /v1/tx/submit (transaction broadcasting).
- Master key: Unrestricted access.
Node endpoints (/v1/address/{addr}/utxos, /v1/protocol-params, /v1/tx/submit)
return HTTP 403 for insufficient tier.
"""
import os
@ -520,6 +529,42 @@ async def require_master_key(auth: dict = Depends(get_auth_context)):
return auth
async def require_standard_tier(auth: dict = Depends(get_auth_context)):
"""
Require standard tier or higher for node read endpoints.
Anonymous users get db-sync only - no direct node access.
"""
if auth["tier"] == "anonymous":
raise HTTPException(
status_code=403,
detail={
"error": "tier_required",
"message": "Node access requires standard tier or higher (50+ TRP)",
"required_tier": "standard",
"current_tier": auth["tier"]
}
)
return auth
async def require_elevated_tier(auth: dict = Depends(get_auth_context)):
"""
Require elevated tier for transaction submission.
Standard tier gets read-only node access, elevated gets write.
"""
if auth["tier"] not in ("elevated", "master"):
raise HTTPException(
status_code=403,
detail={
"error": "tier_required",
"message": "Transaction submission requires elevated tier (500+ TRP)",
"required_tier": "elevated",
"current_tier": auth["tier"]
}
)
return auth
# ============ Middleware ============
@app.middleware("http")
@ -635,10 +680,13 @@ async def get_sync_status(auth: dict = Depends(get_auth_context)):
# ============ Node Integration Endpoints ============
@app.get("/v1/address/{address}/utxos")
async def get_address_utxos(address: str, auth: dict = Depends(get_auth_context)):
async def get_address_utxos(address: str, auth: dict = Depends(require_standard_tier)):
"""
Query UTxOs for an address directly from the node.
Faster than db-sync for current unspent outputs.
Requires: standard tier (50+ TRP) or higher.
Anonymous users should use /v1/address/{address}/balance (db-sync) instead.
"""
cache_key = f"utxos_{address}"
cached = await get_cached(cache_key)
@ -715,20 +763,18 @@ async def get_address_utxos(address: str, auth: dict = Depends(get_auth_context)
@app.post("/v1/tx/submit")
async def submit_transaction(
request: TxSubmitRequest,
auth: dict = Depends(get_auth_context)
auth: dict = Depends(require_elevated_tier)
):
"""
Submit a signed transaction to the network.
Accepts hex or base64 encoded CBOR transaction.
Requires: elevated tier (500+ TRP) or master key.
Standard tier gets read-only node access.
"""
# Check tx submission rate limit
# Check tx submission rate limit (elevated tier still has limits)
allowed, retry_after = await check_rate_limit(auth["identifier"], auth["tier"], "tx_submit")
if not allowed:
if auth["tier"] == "anonymous":
raise HTTPException(
status_code=403,
detail={"error": "forbidden", "message": "Transaction submission requires an API key"}
)
raise HTTPException(
status_code=429,
detail={"error": "rate_limit_exceeded", "retry_after": retry_after}
@ -808,10 +854,12 @@ async def submit_transaction(
@app.get("/v1/protocol-params")
async def get_protocol_params(auth: dict = Depends(get_auth_context)):
async def get_protocol_params(auth: dict = Depends(require_standard_tier)):
"""
Get current epoch protocol parameters from the node.
Cached for 5 minutes.
Requires: standard tier (50+ TRP) or higher.
"""
global protocol_params_cache