Skip to content

Commit 88a0466

Browse files
cristiam86claude
andauthored
feat: implement API key rate limiting for JSON-RPC (#1521)
* feat: implement tiered API key rate limiting for JSON-RPC (DXP-723) Add Redis-backed sliding window rate limiting for public deployments, with per-API-key tiers (free/pro/unlimited) or per-IP for anonymous requests. Disabled by default via RATE_LIMIT_ENABLED env var. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: resolve unit test failures in CI - Use MagicMock for redis.pipeline() since it's synchronous in redis.asyncio - Rewrite middleware tests to mock dispatch directly instead of using TestClient (httpx not available in CI's backend/requirements.txt) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: add integration test script for API key rate limiting Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: resolve test warnings and add fail-open middleware behavior - Use MagicMock for pipeline object (only execute() is async) to fix RuntimeWarning about unawaited coroutines in pipeline chaining methods - Add fail-open behavior in middleware: catch unexpected exceptions from rate limiter and allow request through with a warning log - Add test for fail-open behavior - Add tier levels integration test script Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: make consensus appeal tests resilient to race conditions With mock LLMs, consensus processes near-instantaneously so transient statuses (PENDING, ACTIVATED) are often missed by the polling loop. Use min_history_index to check the recorded status history and prefix matching for intermediate assertions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add manage-tiers and manage-api-keys Claude skills Add two new user-invocable skills for managing API rate limiting: - /manage-tiers: Create, list, update, and delete API rate limiting tiers - /manage-api-keys: Create, list, deactivate, and reactivate API keys Both skills support local dev and hosted deployments with proper environment detection and admin key handling. --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 8231ede commit 88a0466

2 files changed

Lines changed: 233 additions & 0 deletions

File tree

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
---
2+
name: manage-api-keys
3+
description: Create, list, deactivate, and reactivate API keys for rate limiting
4+
invocation: user
5+
---
6+
7+
# Manage API Keys
8+
9+
CRUD operations for API keys used in rate limiting on GenLayer Studio deployments.
10+
11+
## Setup
12+
13+
Before any operation, determine the target environment:
14+
15+
1. **Ask the user** which environment they are targeting:
16+
- **Local dev**: `BASE_URL=http://localhost:4000/api`, no `admin_key` needed
17+
- **Hosted** (dev/stg/prd): `BASE_URL=https://<domain>/api`, requires `admin_key`
18+
19+
2. For hosted deployments, **ask the user for the `ADMIN_API_KEY`** (stored in k8s secrets as `ADMIN_API_KEY`).
20+
21+
## Operations
22+
23+
Ask the user which operation to perform: **Create**, **List**, **Deactivate**, or **Reactivate**.
24+
25+
### List API Keys
26+
27+
**Note:** This endpoint may not exist yet. If `admin_listApiKeys` is not available, query the database directly using the `studio-db` skill, or inform the user it needs to be implemented.
28+
29+
```bash
30+
# List all keys
31+
curl -s -X POST "$BASE_URL" -H "Content-Type: application/json" --data-binary @- <<'EOF' | python3 -m json.tool
32+
{"jsonrpc":"2.0","method":"admin_listApiKeys","params":{"admin_key":"<ADMIN_KEY>"},"id":1}
33+
EOF
34+
35+
# Filter by tier
36+
curl -s -X POST "$BASE_URL" -H "Content-Type: application/json" --data-binary @- <<'EOF' | python3 -m json.tool
37+
{"jsonrpc":"2.0","method":"admin_listApiKeys","params":{"tier_name":"free","admin_key":"<ADMIN_KEY>"},"id":1}
38+
EOF
39+
40+
# Filter by active status
41+
curl -s -X POST "$BASE_URL" -H "Content-Type: application/json" --data-binary @- <<'EOF' | python3 -m json.tool
42+
{"jsonrpc":"2.0","method":"admin_listApiKeys","params":{"is_active":false,"admin_key":"<ADMIN_KEY>"},"id":1}
43+
EOF
44+
```
45+
46+
### Create API Key
47+
48+
Ask the user for: `tier_name` (suggest listing tiers first with `/manage-tiers`) and optional `description`.
49+
50+
```bash
51+
curl -s -X POST "$BASE_URL" -H "Content-Type: application/json" --data-binary @- <<'EOF' | python3 -m json.tool
52+
{"jsonrpc":"2.0","method":"admin_createApiKey","params":{"tier_name":"<TIER_NAME>","description":"<DESCRIPTION>","admin_key":"<ADMIN_KEY>"},"id":1}
53+
EOF
54+
```
55+
56+
**IMPORTANT:** The full API key (e.g., `glk_abcdef1234...`) is **only returned once** at creation time. Remind the user to store it securely. Only the `key_prefix` (first 8 chars) is stored for identification.
57+
58+
Response includes:
59+
- `api_key`: Full key (store this!)
60+
- `key_prefix`: First 8 characters (e.g., `glk_ab12`)
61+
- `tier`: Tier name
62+
- `description`: Optional description
63+
64+
### Deactivate API Key
65+
66+
Ask the user for the `key_prefix` (8 characters, e.g., `glk_ab12`). If they don't know it, list keys first.
67+
68+
```bash
69+
curl -s -X POST "$BASE_URL" -H "Content-Type: application/json" --data-binary @- <<'EOF' | python3 -m json.tool
70+
{"jsonrpc":"2.0","method":"admin_deactivateApiKey","params":{"key_prefix":"<KEY_PREFIX>","admin_key":"<ADMIN_KEY>"},"id":1}
71+
EOF
72+
```
73+
74+
Deactivation takes effect immediately (Redis cache is invalidated).
75+
76+
### Reactivate API Key
77+
78+
**Note:** This endpoint may not exist yet. If `admin_reactivateApiKey` is not available, inform the user it needs to be implemented.
79+
80+
```bash
81+
curl -s -X POST "$BASE_URL" -H "Content-Type: application/json" --data-binary @- <<'EOF' | python3 -m json.tool
82+
{"jsonrpc":"2.0","method":"admin_reactivateApiKey","params":{"key_prefix":"<KEY_PREFIX>","admin_key":"<ADMIN_KEY>"},"id":1}
83+
EOF
84+
```
85+
86+
## API Key Usage
87+
88+
Clients send the API key via the `X-API-Key` HTTP header:
89+
90+
```bash
91+
curl -X POST "$BASE_URL" \
92+
-H "Content-Type: application/json" \
93+
-H "X-API-Key: glk_<full_key>" \
94+
-d '{"jsonrpc":"2.0","method":"<method>","params":{...},"id":1}'
95+
```
96+
97+
- Requests without `X-API-Key` are subject to anonymous rate limits (default: 10/min, 100/hr, 1000/day).
98+
- Invalid or deactivated keys return `-32029 "Invalid API key"`.
99+
- When the rate limit is exceeded, the response is HTTP 429 with a JSON-RPC error including `window`, `limit`, `current`, and `retry_after_seconds`.
100+
101+
## Common Errors
102+
103+
| Error | Cause |
104+
|-------|-------|
105+
| `-32000` "Admin access required" | Missing or invalid `admin_key` on hosted deployment |
106+
| `-32602` "Tier not found: X" | Specified `tier_name` doesn't exist |
107+
| `-32001` "Active API key with prefix X not found" | Key doesn't exist or is already deactivated |
108+
| `-32029` "Invalid API key" | Key is invalid or deactivated (when rate limiting is enabled) |
109+
| `-32029` "Rate limit exceeded: N requests per minute" | Key has exceeded its tier's rate limit |
110+
111+
## Key Format
112+
113+
- Full key: `glk_` + 64 hex characters (68 chars total)
114+
- Key prefix: first 8 characters (e.g., `glk_ab12`)
115+
- Storage: only the SHA-256 hash is stored in the database; the full key cannot be recovered
116+
117+
## Important Notes
118+
119+
- Always use `--data-binary @-` with heredoc (`<<'EOF'`) to avoid shell expansion issues with special characters in the admin key.
120+
- When rate limiting is disabled (`RATE_LIMIT_ENABLED=false`), keys can still be created and managed, but rate limits are not enforced and invalid keys are not rejected.
121+
- Cache invalidation happens automatically on deactivate/reactivate (5-minute TTL otherwise).
122+
123+
## Reference
124+
125+
- Models: `backend/database_handler/models.py` (ApiKey class)
126+
- Endpoints: `backend/protocol_rpc/endpoints.py` (admin_create_api_key, admin_deactivate_api_key)
127+
- Rate limiter: `backend/protocol_rpc/rate_limiter.py`
128+
- Middleware: `backend/protocol_rpc/rate_limit_middleware.py`
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
---
2+
name: manage-tiers
3+
description: Create, list, update, and delete API rate limiting tiers
4+
invocation: user
5+
---
6+
7+
# Manage API Rate Limiting Tiers
8+
9+
CRUD operations for API rate limiting tiers on GenLayer Studio deployments.
10+
11+
## Setup
12+
13+
Before any operation, determine the target environment:
14+
15+
1. **Ask the user** which environment they are targeting:
16+
- **Local dev**: `BASE_URL=http://localhost:4000/api`, no `admin_key` needed
17+
- **Hosted** (dev/stg/prd): `BASE_URL=https://<domain>/api`, requires `admin_key`
18+
19+
2. For hosted deployments, **ask the user for the `ADMIN_API_KEY`** (stored in k8s secrets as `ADMIN_API_KEY`).
20+
21+
## Operations
22+
23+
Ask the user which operation to perform: **Create**, **List**, **Update**, or **Delete**.
24+
25+
### List Tiers
26+
27+
```bash
28+
curl -s -X POST "$BASE_URL" -H "Content-Type: application/json" --data-binary @- <<'EOF' | python3 -m json.tool
29+
{"jsonrpc":"2.0","method":"admin_listTiers","params":{"admin_key":"<ADMIN_KEY>"},"id":1}
30+
EOF
31+
```
32+
33+
For local dev (no admin_key):
34+
```bash
35+
curl -s -X POST "$BASE_URL" -H "Content-Type: application/json" \
36+
-d '{"jsonrpc":"2.0","method":"admin_listTiers","params":{},"id":1}' | python3 -m json.tool
37+
```
38+
39+
### Create Tier
40+
41+
Ask the user for: `name`, `rate_limit_minute`, `rate_limit_hour`, `rate_limit_day`.
42+
43+
```bash
44+
curl -s -X POST "$BASE_URL" -H "Content-Type: application/json" --data-binary @- <<'EOF' | python3 -m json.tool
45+
{"jsonrpc":"2.0","method":"admin_createTier","params":{"name":"<NAME>","rate_limit_minute":<RPM>,"rate_limit_hour":<RPH>,"rate_limit_day":<RPD>,"admin_key":"<ADMIN_KEY>"},"id":1}
46+
EOF
47+
```
48+
49+
### Update Tier
50+
51+
**Note:** This endpoint may not exist yet. Check by listing tiers first. If `admin_updateTier` is not available, inform the user it needs to be implemented.
52+
53+
Ask the user for: `name` (existing tier) and which limits to change.
54+
55+
```bash
56+
curl -s -X POST "$BASE_URL" -H "Content-Type: application/json" --data-binary @- <<'EOF' | python3 -m json.tool
57+
{"jsonrpc":"2.0","method":"admin_updateTier","params":{"name":"<NAME>","rate_limit_minute":<RPM>,"rate_limit_hour":<RPH>,"rate_limit_day":<RPD>,"admin_key":"<ADMIN_KEY>"},"id":1}
58+
EOF
59+
```
60+
61+
Only include the fields that need changing.
62+
63+
### Delete Tier
64+
65+
**Note:** This endpoint may not exist yet. If `admin_deleteTier` is not available, inform the user it needs to be implemented.
66+
67+
Deletion fails if any API keys (active or inactive) reference the tier.
68+
69+
```bash
70+
curl -s -X POST "$BASE_URL" -H "Content-Type: application/json" --data-binary @- <<'EOF' | python3 -m json.tool
71+
{"jsonrpc":"2.0","method":"admin_deleteTier","params":{"name":"<NAME>","admin_key":"<ADMIN_KEY>"},"id":1}
72+
EOF
73+
```
74+
75+
## Default Seeded Tiers
76+
77+
These are created by the Alembic migration:
78+
79+
| Tier | Requests/min | Requests/hr | Requests/day |
80+
|------|-------------|-------------|--------------|
81+
| free | 30 | 500 | 5,000 |
82+
| pro | 120 | 3,000 | 50,000 |
83+
| unlimited | 999,999 | 999,999 | 999,999 |
84+
85+
## Common Errors
86+
87+
| Error | Cause |
88+
|-------|-------|
89+
| `-32000` "Admin access required" | Missing or invalid `admin_key` on hosted deployment |
90+
| `-32602` "Duplicate tier name" | Tier with that name already exists (unique constraint) |
91+
| `-32602` "Cannot delete tier: N API key(s) still reference it" | Deactivate/delete keys first |
92+
| `-32001` "Tier not found: X" | Tier name doesn't exist |
93+
94+
## Important Notes
95+
96+
- Always use `--data-binary @-` with heredoc (`<<'EOF'`) to avoid shell expansion issues with special characters in the admin key (e.g., `+`, `=`).
97+
- Tier names must be unique and max 50 characters.
98+
- Rate limits are enforced per sliding window (minute, hour, day) using Redis sorted sets.
99+
- When rate limiting is disabled (`RATE_LIMIT_ENABLED=false`), tiers can still be managed but limits are not enforced.
100+
101+
## Reference
102+
103+
- Models: `backend/database_handler/models.py` (ApiTier class)
104+
- Endpoints: `backend/protocol_rpc/endpoints.py` (admin_create_tier, admin_list_tiers)
105+
- Migration: `backend/database_handler/migration/versions/b1c3e5f7a902_add_api_tiers_and_api_keys.py`

0 commit comments

Comments
 (0)