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
00c44fd0f2
commit
3cfbf7ef59
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 \
|
curl \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
libsodium23 \
|
libsodium23 \
|
||||||
libsecp256k1-1 \
|
|
||||||
libnuma1 \
|
libnuma1 \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
|
|
||||||
74
main.py
74
main.py
|
|
@ -8,10 +8,19 @@ Known policy IDs:
|
||||||
- TRP: 9c4bd4a90cdb73d9ff681215ecf7dea9fb183d916d30487d17098e05
|
- TRP: 9c4bd4a90cdb73d9ff681215ecf7dea9fb183d916d30487d17098e05
|
||||||
- MAP: 24bd9e7b9ae3a61df79eca72fd8355d0f7767e4c55a04a0d919c019c
|
- MAP: 24bd9e7b9ae3a61df79eca72fd8355d0f7767e4c55a04a0d919c019c
|
||||||
|
|
||||||
TRP Gating Tiers:
|
Access Tiers (strictly enforced):
|
||||||
- 0 TRP → anonymous rate limits (20 req/min)
|
- Anonymous (0 TRP, 20 req/min):
|
||||||
- 50+ TRP → standard tier (100 req/min)
|
db-sync read-only ONLY. Balance, transactions, tokens, blocks, assets, pools, sync status.
|
||||||
- 500+ TRP → elevated tier (1000 req/min)
|
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
|
import os
|
||||||
|
|
@ -520,6 +529,42 @@ async def require_master_key(auth: dict = Depends(get_auth_context)):
|
||||||
return auth
|
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 ============
|
# ============ Middleware ============
|
||||||
|
|
||||||
@app.middleware("http")
|
@app.middleware("http")
|
||||||
|
|
@ -635,10 +680,13 @@ async def get_sync_status(auth: dict = Depends(get_auth_context)):
|
||||||
# ============ Node Integration Endpoints ============
|
# ============ Node Integration Endpoints ============
|
||||||
|
|
||||||
@app.get("/v1/address/{address}/utxos")
|
@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.
|
Query UTxOs for an address directly from the node.
|
||||||
Faster than db-sync for current unspent outputs.
|
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}"
|
cache_key = f"utxos_{address}"
|
||||||
cached = await get_cached(cache_key)
|
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")
|
@app.post("/v1/tx/submit")
|
||||||
async def submit_transaction(
|
async def submit_transaction(
|
||||||
request: TxSubmitRequest,
|
request: TxSubmitRequest,
|
||||||
auth: dict = Depends(get_auth_context)
|
auth: dict = Depends(require_elevated_tier)
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Submit a signed transaction to the network.
|
Submit a signed transaction to the network.
|
||||||
Accepts hex or base64 encoded CBOR transaction.
|
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")
|
allowed, retry_after = await check_rate_limit(auth["identifier"], auth["tier"], "tx_submit")
|
||||||
if not allowed:
|
if not allowed:
|
||||||
if auth["tier"] == "anonymous":
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=403,
|
|
||||||
detail={"error": "forbidden", "message": "Transaction submission requires an API key"}
|
|
||||||
)
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=429,
|
status_code=429,
|
||||||
detail={"error": "rate_limit_exceeded", "retry_after": retry_after}
|
detail={"error": "rate_limit_exceeded", "retry_after": retry_after}
|
||||||
|
|
@ -808,10 +854,12 @@ async def submit_transaction(
|
||||||
|
|
||||||
|
|
||||||
@app.get("/v1/protocol-params")
|
@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.
|
Get current epoch protocol parameters from the node.
|
||||||
Cached for 5 minutes.
|
Cached for 5 minutes.
|
||||||
|
|
||||||
|
Requires: standard tier (50+ TRP) or higher.
|
||||||
"""
|
"""
|
||||||
global protocol_params_cache
|
global protocol_params_cache
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue