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:
parent
163de03322
commit
d5fbec496f
2 changed files with 61 additions and 14 deletions
|
|
@ -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
74
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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue