From d5fbec496f31757ce075e7c405797ba87aa27cee Mon Sep 17 00:00:00 2001 From: Kayos Date: Sat, 21 Mar 2026 09:15:40 -0700 Subject: [PATCH] fix: Enforce strict tier-based access control for node endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- Dockerfile | 1 - main.py | 74 ++++++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 61 insertions(+), 14 deletions(-) diff --git a/Dockerfile b/Dockerfile index 31e204a..6dcf377 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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/* diff --git a/main.py b/main.py index 90753d8..dd58005 100644 --- a/main.py +++ b/main.py @@ -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