reqkey 0.2.0__tar.gz → 0.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {reqkey-0.2.0 → reqkey-0.2.1}/PKG-INFO +24 -8
- {reqkey-0.2.0 → reqkey-0.2.1}/README.md +23 -7
- {reqkey-0.2.0 → reqkey-0.2.1}/pyproject.toml +1 -1
- {reqkey-0.2.0 → reqkey-0.2.1}/src/reqkey/__init__.py +1 -1
- {reqkey-0.2.0 → reqkey-0.2.1}/src/reqkey/asgi.py +14 -1
- {reqkey-0.2.0 → reqkey-0.2.1}/src/reqkey/client.py +13 -1
- {reqkey-0.2.0 → reqkey-0.2.1}/src/reqkey/django.py +20 -2
- {reqkey-0.2.0 → reqkey-0.2.1}/src/reqkey/wsgi.py +12 -1
- {reqkey-0.2.0 → reqkey-0.2.1}/tests/test_client.py +4 -0
- {reqkey-0.2.0 → reqkey-0.2.1}/tests/test_django.py +9 -2
- {reqkey-0.2.0 → reqkey-0.2.1}/tests/test_fastapi.py +8 -2
- {reqkey-0.2.0 → reqkey-0.2.1}/tests/test_wsgi.py +6 -2
- {reqkey-0.2.0 → reqkey-0.2.1}/.github/workflows/ci.yml +0 -0
- {reqkey-0.2.0 → reqkey-0.2.1}/.github/workflows/release.yml +0 -0
- {reqkey-0.2.0 → reqkey-0.2.1}/.gitignore +0 -0
- {reqkey-0.2.0 → reqkey-0.2.1}/LICENSE +0 -0
- {reqkey-0.2.0 → reqkey-0.2.1}/examples/django_settings.py +0 -0
- {reqkey-0.2.0 → reqkey-0.2.1}/examples/fastapi_app.py +0 -0
- {reqkey-0.2.0 → reqkey-0.2.1}/examples/flask_app.py +0 -0
- {reqkey-0.2.0 → reqkey-0.2.1}/src/reqkey/_middleware.py +0 -0
- {reqkey-0.2.0 → reqkey-0.2.1}/src/reqkey/exceptions.py +0 -0
- {reqkey-0.2.0 → reqkey-0.2.1}/src/reqkey/fastapi.py +0 -0
- {reqkey-0.2.0 → reqkey-0.2.1}/src/reqkey/flask.py +0 -0
- {reqkey-0.2.0 → reqkey-0.2.1}/src/reqkey/models.py +0 -0
- {reqkey-0.2.0 → reqkey-0.2.1}/src/reqkey/py.typed +0 -0
- {reqkey-0.2.0 → reqkey-0.2.1}/tests/test_middleware.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: reqkey
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: The official Python SDK for ReqKey API key validation and usage analytics
|
|
5
5
|
Project-URL: Homepage, https://reqkey.com
|
|
6
6
|
Project-URL: Documentation, https://reqkey.com/docs
|
|
@@ -551,6 +551,8 @@ Metadata sent by default:
|
|
|
551
551
|
- response status
|
|
552
552
|
- endpoint processing latency
|
|
553
553
|
- user agent
|
|
554
|
+
- the consumer API key in the dedicated `apiKey` field, so ReqKey can resolve
|
|
555
|
+
the internal consumer name
|
|
554
556
|
- `consumerName` when a resolver supplies one
|
|
555
557
|
|
|
556
558
|
Additional data is opt-in:
|
|
@@ -574,9 +576,11 @@ app.add_middleware(
|
|
|
574
576
|
```
|
|
575
577
|
|
|
576
578
|
Authorization, cookies, `Set-Cookie`, common API-key headers, and the configured
|
|
577
|
-
consumer-key header are always removed from captured headers.
|
|
578
|
-
|
|
579
|
-
|
|
579
|
+
consumer-key header are always removed from captured headers and paths. The
|
|
580
|
+
middleware sends the extracted consumer key only through `/ingest`'s dedicated
|
|
581
|
+
`apiKey` field for consumer resolution. Response-body capture is limited to
|
|
582
|
+
textual content and a hard maximum of 1,000 characters. The limit is enforced
|
|
583
|
+
again by the HTTP client before `/ingest` is called.
|
|
580
584
|
|
|
581
585
|
### Consumer names
|
|
582
586
|
|
|
@@ -600,8 +604,18 @@ app.add_middleware(
|
|
|
600
604
|
)
|
|
601
605
|
```
|
|
602
606
|
|
|
603
|
-
The resolved value is
|
|
604
|
-
consumer API key and does not change authorization.
|
|
607
|
+
The resolved value is optional analytics metadata. It is not used as the
|
|
608
|
+
consumer API key and does not change authorization. ReqKey resolves the
|
|
609
|
+
consumer display name in this order:
|
|
610
|
+
|
|
611
|
+
1. A non-empty `consumerName` supplied by the resolver.
|
|
612
|
+
2. The middleware's extracted consumer key, sent as `apiKey` for an internal
|
|
613
|
+
consumer lookup.
|
|
614
|
+
3. A `consumerId` supplied to the direct client.
|
|
615
|
+
|
|
616
|
+
No resolver is required for normal ReqKey-managed consumers. The middleware
|
|
617
|
+
still sends the validation `requestId` for log correlation, but `/ingest` does
|
|
618
|
+
not use it to resolve consumer identity.
|
|
605
619
|
|
|
606
620
|
For Django, set `CONSUMER_NAME_RESOLVER` in `REQKEY`. For WSGI, the resolver
|
|
607
621
|
receives the WSGI environment, so a header can be read as
|
|
@@ -767,7 +781,7 @@ with ReqKey(project_key=os.environ["REQKEY_PROJECT_KEY"]) as reqkey:
|
|
|
767
781
|
decision = reqkey.verify("consumer_key_...", api_id="api_payments")
|
|
768
782
|
```
|
|
769
783
|
|
|
770
|
-
Direct ingestion
|
|
784
|
+
Direct ingestion accepts the same consumer identity inputs as `/ingest`:
|
|
771
785
|
|
|
772
786
|
```python
|
|
773
787
|
reqkey.ingest(
|
|
@@ -776,7 +790,9 @@ reqkey.ingest(
|
|
|
776
790
|
method="POST",
|
|
777
791
|
path="/payments",
|
|
778
792
|
status_code=201,
|
|
779
|
-
|
|
793
|
+
api_key="consumer_key_...", # resolves the internal consumer name
|
|
794
|
+
# consumer_name="rapidapi-user", # optional explicit override
|
|
795
|
+
# consumer_id="consumer_...", # fallback when no API key is available
|
|
780
796
|
)
|
|
781
797
|
```
|
|
782
798
|
|
|
@@ -478,6 +478,8 @@ Metadata sent by default:
|
|
|
478
478
|
- response status
|
|
479
479
|
- endpoint processing latency
|
|
480
480
|
- user agent
|
|
481
|
+
- the consumer API key in the dedicated `apiKey` field, so ReqKey can resolve
|
|
482
|
+
the internal consumer name
|
|
481
483
|
- `consumerName` when a resolver supplies one
|
|
482
484
|
|
|
483
485
|
Additional data is opt-in:
|
|
@@ -501,9 +503,11 @@ app.add_middleware(
|
|
|
501
503
|
```
|
|
502
504
|
|
|
503
505
|
Authorization, cookies, `Set-Cookie`, common API-key headers, and the configured
|
|
504
|
-
consumer-key header are always removed from captured headers.
|
|
505
|
-
|
|
506
|
-
|
|
506
|
+
consumer-key header are always removed from captured headers and paths. The
|
|
507
|
+
middleware sends the extracted consumer key only through `/ingest`'s dedicated
|
|
508
|
+
`apiKey` field for consumer resolution. Response-body capture is limited to
|
|
509
|
+
textual content and a hard maximum of 1,000 characters. The limit is enforced
|
|
510
|
+
again by the HTTP client before `/ingest` is called.
|
|
507
511
|
|
|
508
512
|
### Consumer names
|
|
509
513
|
|
|
@@ -527,8 +531,18 @@ app.add_middleware(
|
|
|
527
531
|
)
|
|
528
532
|
```
|
|
529
533
|
|
|
530
|
-
The resolved value is
|
|
531
|
-
consumer API key and does not change authorization.
|
|
534
|
+
The resolved value is optional analytics metadata. It is not used as the
|
|
535
|
+
consumer API key and does not change authorization. ReqKey resolves the
|
|
536
|
+
consumer display name in this order:
|
|
537
|
+
|
|
538
|
+
1. A non-empty `consumerName` supplied by the resolver.
|
|
539
|
+
2. The middleware's extracted consumer key, sent as `apiKey` for an internal
|
|
540
|
+
consumer lookup.
|
|
541
|
+
3. A `consumerId` supplied to the direct client.
|
|
542
|
+
|
|
543
|
+
No resolver is required for normal ReqKey-managed consumers. The middleware
|
|
544
|
+
still sends the validation `requestId` for log correlation, but `/ingest` does
|
|
545
|
+
not use it to resolve consumer identity.
|
|
532
546
|
|
|
533
547
|
For Django, set `CONSUMER_NAME_RESOLVER` in `REQKEY`. For WSGI, the resolver
|
|
534
548
|
receives the WSGI environment, so a header can be read as
|
|
@@ -694,7 +708,7 @@ with ReqKey(project_key=os.environ["REQKEY_PROJECT_KEY"]) as reqkey:
|
|
|
694
708
|
decision = reqkey.verify("consumer_key_...", api_id="api_payments")
|
|
695
709
|
```
|
|
696
710
|
|
|
697
|
-
Direct ingestion
|
|
711
|
+
Direct ingestion accepts the same consumer identity inputs as `/ingest`:
|
|
698
712
|
|
|
699
713
|
```python
|
|
700
714
|
reqkey.ingest(
|
|
@@ -703,7 +717,9 @@ reqkey.ingest(
|
|
|
703
717
|
method="POST",
|
|
704
718
|
path="/payments",
|
|
705
719
|
status_code=201,
|
|
706
|
-
|
|
720
|
+
api_key="consumer_key_...", # resolves the internal consumer name
|
|
721
|
+
# consumer_name="rapidapi-user", # optional explicit override
|
|
722
|
+
# consumer_id="consumer_...", # fallback when no API key is available
|
|
707
723
|
)
|
|
708
724
|
```
|
|
709
725
|
|
|
@@ -135,6 +135,8 @@ class AsyncReqKeyLike(Protocol):
|
|
|
135
135
|
user_agent: str | None = None,
|
|
136
136
|
user_id: str | None = None,
|
|
137
137
|
consumer_name: str | None = None,
|
|
138
|
+
api_key: str | None = None,
|
|
139
|
+
consumer_id: str | None = None,
|
|
138
140
|
query_params: Mapping[str, Any] | None = None,
|
|
139
141
|
request_headers: Mapping[str, str] | None = None,
|
|
140
142
|
response_headers: Mapping[str, str] | None = None,
|
|
@@ -276,13 +278,14 @@ class ReqKeyMiddleware:
|
|
|
276
278
|
request_started = time.perf_counter()
|
|
277
279
|
decision: VerificationResult | None = None
|
|
278
280
|
validation_time_ms: float | None = None
|
|
281
|
+
consumer_key = await self._consumer_key(request)
|
|
279
282
|
|
|
280
283
|
if self.mode in {"validate", "both"}:
|
|
281
|
-
consumer_key = await self._consumer_key(request)
|
|
282
284
|
if consumer_key is None:
|
|
283
285
|
await self._ingest_denied_request(
|
|
284
286
|
request,
|
|
285
287
|
decision=None,
|
|
288
|
+
consumer_key=None,
|
|
286
289
|
status_code=401,
|
|
287
290
|
error="missing_api_key",
|
|
288
291
|
headers={},
|
|
@@ -336,6 +339,7 @@ class ReqKeyMiddleware:
|
|
|
336
339
|
await self._ingest_denied_request(
|
|
337
340
|
request,
|
|
338
341
|
decision=decision,
|
|
342
|
+
consumer_key=consumer_key,
|
|
339
343
|
status_code=status_code,
|
|
340
344
|
error=error,
|
|
341
345
|
headers=headers,
|
|
@@ -372,6 +376,7 @@ class ReqKeyMiddleware:
|
|
|
372
376
|
receive,
|
|
373
377
|
send,
|
|
374
378
|
decision=decision,
|
|
379
|
+
consumer_key=consumer_key,
|
|
375
380
|
validation_time_ms=validation_time_ms,
|
|
376
381
|
)
|
|
377
382
|
|
|
@@ -398,6 +403,7 @@ class ReqKeyMiddleware:
|
|
|
398
403
|
send: Send,
|
|
399
404
|
*,
|
|
400
405
|
decision: VerificationResult | None,
|
|
406
|
+
consumer_key: str | None,
|
|
401
407
|
validation_time_ms: float | None,
|
|
402
408
|
) -> None:
|
|
403
409
|
response_status = 500
|
|
@@ -431,6 +437,7 @@ class ReqKeyMiddleware:
|
|
|
431
437
|
await self._ingest_safely(
|
|
432
438
|
request=request,
|
|
433
439
|
decision=decision,
|
|
440
|
+
consumer_key=consumer_key,
|
|
434
441
|
response_status=response_status,
|
|
435
442
|
latency_ms=round((time.perf_counter() - started) * 1000),
|
|
436
443
|
response_headers=response_headers,
|
|
@@ -446,6 +453,7 @@ class ReqKeyMiddleware:
|
|
|
446
453
|
await self._ingest_safely(
|
|
447
454
|
request=request,
|
|
448
455
|
decision=decision,
|
|
456
|
+
consumer_key=consumer_key,
|
|
449
457
|
response_status=500,
|
|
450
458
|
latency_ms=round((time.perf_counter() - started) * 1000),
|
|
451
459
|
response_headers=response_headers,
|
|
@@ -457,6 +465,7 @@ class ReqKeyMiddleware:
|
|
|
457
465
|
await self._ingest_safely(
|
|
458
466
|
request=request,
|
|
459
467
|
decision=decision,
|
|
468
|
+
consumer_key=consumer_key,
|
|
460
469
|
response_status=response_status,
|
|
461
470
|
latency_ms=round((time.perf_counter() - started) * 1000),
|
|
462
471
|
response_headers=response_headers,
|
|
@@ -537,6 +546,7 @@ class ReqKeyMiddleware:
|
|
|
537
546
|
*,
|
|
538
547
|
request: Request,
|
|
539
548
|
decision: VerificationResult | None,
|
|
549
|
+
consumer_key: str | None,
|
|
540
550
|
response_status: int,
|
|
541
551
|
latency_ms: int,
|
|
542
552
|
response_headers: Headers,
|
|
@@ -572,6 +582,7 @@ class ReqKeyMiddleware:
|
|
|
572
582
|
client_ip=client_ip,
|
|
573
583
|
user_agent=user_agent,
|
|
574
584
|
consumer_name=consumer_name,
|
|
585
|
+
api_key=consumer_key,
|
|
575
586
|
query_params=query_params,
|
|
576
587
|
request_headers=request_headers,
|
|
577
588
|
response_headers=captured_response_headers,
|
|
@@ -592,6 +603,7 @@ class ReqKeyMiddleware:
|
|
|
592
603
|
request: Request,
|
|
593
604
|
*,
|
|
594
605
|
decision: VerificationResult | None,
|
|
606
|
+
consumer_key: str | None,
|
|
595
607
|
status_code: int,
|
|
596
608
|
error: str,
|
|
597
609
|
headers: Mapping[str, str],
|
|
@@ -617,6 +629,7 @@ class ReqKeyMiddleware:
|
|
|
617
629
|
await self._ingest_safely(
|
|
618
630
|
request=request,
|
|
619
631
|
decision=decision,
|
|
632
|
+
consumer_key=consumer_key,
|
|
620
633
|
response_status=status_code,
|
|
621
634
|
latency_ms=round((time.perf_counter() - started) * 1000),
|
|
622
635
|
response_headers=response_headers,
|
|
@@ -20,7 +20,7 @@ from .models import VerificationReason, VerificationResult
|
|
|
20
20
|
|
|
21
21
|
DEFAULT_BASE_URL = "https://api.reqkey.com"
|
|
22
22
|
DEFAULT_TIMEOUT_SECONDS = 2.0
|
|
23
|
-
USER_AGENT = "reqkey-python/0.2.
|
|
23
|
+
USER_AGENT = "reqkey-python/0.2.1"
|
|
24
24
|
MAX_BODY_CHARACTERS = 1000
|
|
25
25
|
_DECISION_STATUS_CODES = {200, 402, 403, 429}
|
|
26
26
|
|
|
@@ -168,6 +168,8 @@ def _ingest_payload(
|
|
|
168
168
|
user_agent: str | None = None,
|
|
169
169
|
user_id: str | None = None,
|
|
170
170
|
consumer_name: str | None = None,
|
|
171
|
+
api_key: str | None = None,
|
|
172
|
+
consumer_id: str | None = None,
|
|
171
173
|
query_params: Mapping[str, Any] | None = None,
|
|
172
174
|
request_headers: Mapping[str, str] | None = None,
|
|
173
175
|
response_headers: Mapping[str, str] | None = None,
|
|
@@ -196,6 +198,8 @@ def _ingest_payload(
|
|
|
196
198
|
"userAgent": user_agent,
|
|
197
199
|
"userId": user_id,
|
|
198
200
|
"consumerName": consumer_name,
|
|
201
|
+
"apiKey": api_key,
|
|
202
|
+
"consumerId": consumer_id,
|
|
199
203
|
"queryParams": dict(query_params) if query_params is not None else None,
|
|
200
204
|
"requestHeaders": dict(request_headers) if request_headers is not None else None,
|
|
201
205
|
"responseHeaders": dict(response_headers) if response_headers is not None else None,
|
|
@@ -297,6 +301,8 @@ class ReqKey:
|
|
|
297
301
|
user_agent: str | None = None,
|
|
298
302
|
user_id: str | None = None,
|
|
299
303
|
consumer_name: str | None = None,
|
|
304
|
+
api_key: str | None = None,
|
|
305
|
+
consumer_id: str | None = None,
|
|
300
306
|
query_params: Mapping[str, Any] | None = None,
|
|
301
307
|
request_headers: Mapping[str, str] | None = None,
|
|
302
308
|
response_headers: Mapping[str, str] | None = None,
|
|
@@ -318,6 +324,8 @@ class ReqKey:
|
|
|
318
324
|
user_agent=user_agent,
|
|
319
325
|
user_id=user_id,
|
|
320
326
|
consumer_name=consumer_name,
|
|
327
|
+
api_key=api_key,
|
|
328
|
+
consumer_id=consumer_id,
|
|
321
329
|
query_params=query_params,
|
|
322
330
|
request_headers=request_headers,
|
|
323
331
|
response_headers=response_headers,
|
|
@@ -418,6 +426,8 @@ class AsyncReqKey:
|
|
|
418
426
|
user_agent: str | None = None,
|
|
419
427
|
user_id: str | None = None,
|
|
420
428
|
consumer_name: str | None = None,
|
|
429
|
+
api_key: str | None = None,
|
|
430
|
+
consumer_id: str | None = None,
|
|
421
431
|
query_params: Mapping[str, Any] | None = None,
|
|
422
432
|
request_headers: Mapping[str, str] | None = None,
|
|
423
433
|
response_headers: Mapping[str, str] | None = None,
|
|
@@ -439,6 +449,8 @@ class AsyncReqKey:
|
|
|
439
449
|
user_agent=user_agent,
|
|
440
450
|
user_id=user_id,
|
|
441
451
|
consumer_name=consumer_name,
|
|
452
|
+
api_key=api_key,
|
|
453
|
+
consumer_id=consumer_id,
|
|
442
454
|
query_params=query_params,
|
|
443
455
|
request_headers=request_headers,
|
|
444
456
|
response_headers=response_headers,
|
|
@@ -67,6 +67,8 @@ class SyncReqKeyLike(Protocol):
|
|
|
67
67
|
user_agent: str | None = None,
|
|
68
68
|
user_id: str | None = None,
|
|
69
69
|
consumer_name: str | None = None,
|
|
70
|
+
api_key: str | None = None,
|
|
71
|
+
consumer_id: str | None = None,
|
|
70
72
|
query_params: Mapping[str, Any] | None = None,
|
|
71
73
|
request_headers: Mapping[str, str] | None = None,
|
|
72
74
|
response_headers: Mapping[str, str] | None = None,
|
|
@@ -100,6 +102,8 @@ class AsyncReqKeyLike(Protocol):
|
|
|
100
102
|
user_agent: str | None = None,
|
|
101
103
|
user_id: str | None = None,
|
|
102
104
|
consumer_name: str | None = None,
|
|
105
|
+
api_key: str | None = None,
|
|
106
|
+
consumer_id: str | None = None,
|
|
103
107
|
query_params: Mapping[str, Any] | None = None,
|
|
104
108
|
request_headers: Mapping[str, str] | None = None,
|
|
105
109
|
response_headers: Mapping[str, str] | None = None,
|
|
@@ -245,9 +249,9 @@ class ReqKeyMiddleware:
|
|
|
245
249
|
decision: VerificationResult | None = None
|
|
246
250
|
validation_time_ms: float | None = None
|
|
247
251
|
client = cast(SyncReqKeyLike, self.client)
|
|
252
|
+
consumer_key = self._consumer_key_sync(request)
|
|
248
253
|
|
|
249
254
|
if self.mode in {"validate", "both"}:
|
|
250
|
-
consumer_key = self._consumer_key_sync(request)
|
|
251
255
|
if consumer_key is None:
|
|
252
256
|
missing_response = self._respond(401, "missing_api_key")
|
|
253
257
|
if self.mode == "both" and self.ingest_denied_requests:
|
|
@@ -255,6 +259,7 @@ class ReqKeyMiddleware:
|
|
|
255
259
|
client,
|
|
256
260
|
request=request,
|
|
257
261
|
decision=None,
|
|
262
|
+
consumer_key=None,
|
|
258
263
|
response=missing_response,
|
|
259
264
|
status_code=401,
|
|
260
265
|
latency_ms=round(
|
|
@@ -294,6 +299,7 @@ class ReqKeyMiddleware:
|
|
|
294
299
|
client,
|
|
295
300
|
request=request,
|
|
296
301
|
decision=decision,
|
|
302
|
+
consumer_key=consumer_key,
|
|
297
303
|
response=denied,
|
|
298
304
|
status_code=denied.status_code,
|
|
299
305
|
latency_ms=round(
|
|
@@ -312,6 +318,7 @@ class ReqKeyMiddleware:
|
|
|
312
318
|
client,
|
|
313
319
|
request=request,
|
|
314
320
|
decision=decision,
|
|
321
|
+
consumer_key=consumer_key,
|
|
315
322
|
response=None,
|
|
316
323
|
status_code=500,
|
|
317
324
|
latency_ms=round((time.perf_counter() - started) * 1000),
|
|
@@ -323,6 +330,7 @@ class ReqKeyMiddleware:
|
|
|
323
330
|
client,
|
|
324
331
|
request=request,
|
|
325
332
|
decision=decision,
|
|
333
|
+
consumer_key=consumer_key,
|
|
326
334
|
response=response,
|
|
327
335
|
status_code=response.status_code,
|
|
328
336
|
latency_ms=round((time.perf_counter() - started) * 1000),
|
|
@@ -338,9 +346,9 @@ class ReqKeyMiddleware:
|
|
|
338
346
|
decision: VerificationResult | None = None
|
|
339
347
|
validation_time_ms: float | None = None
|
|
340
348
|
client = cast(AsyncReqKeyLike, self.client)
|
|
349
|
+
consumer_key = await self._consumer_key_async(request)
|
|
341
350
|
|
|
342
351
|
if self.mode in {"validate", "both"}:
|
|
343
|
-
consumer_key = await self._consumer_key_async(request)
|
|
344
352
|
if consumer_key is None:
|
|
345
353
|
missing_response = self._respond(401, "missing_api_key")
|
|
346
354
|
if self.mode == "both" and self.ingest_denied_requests:
|
|
@@ -348,6 +356,7 @@ class ReqKeyMiddleware:
|
|
|
348
356
|
client,
|
|
349
357
|
request=request,
|
|
350
358
|
decision=None,
|
|
359
|
+
consumer_key=None,
|
|
351
360
|
response=missing_response,
|
|
352
361
|
status_code=401,
|
|
353
362
|
latency_ms=round(
|
|
@@ -387,6 +396,7 @@ class ReqKeyMiddleware:
|
|
|
387
396
|
client,
|
|
388
397
|
request=request,
|
|
389
398
|
decision=decision,
|
|
399
|
+
consumer_key=consumer_key,
|
|
390
400
|
response=denied,
|
|
391
401
|
status_code=denied.status_code,
|
|
392
402
|
latency_ms=round(
|
|
@@ -405,6 +415,7 @@ class ReqKeyMiddleware:
|
|
|
405
415
|
client,
|
|
406
416
|
request=request,
|
|
407
417
|
decision=decision,
|
|
418
|
+
consumer_key=consumer_key,
|
|
408
419
|
response=None,
|
|
409
420
|
status_code=500,
|
|
410
421
|
latency_ms=round((time.perf_counter() - started) * 1000),
|
|
@@ -416,6 +427,7 @@ class ReqKeyMiddleware:
|
|
|
416
427
|
client,
|
|
417
428
|
request=request,
|
|
418
429
|
decision=decision,
|
|
430
|
+
consumer_key=consumer_key,
|
|
419
431
|
response=response,
|
|
420
432
|
status_code=response.status_code,
|
|
421
433
|
latency_ms=round((time.perf_counter() - started) * 1000),
|
|
@@ -508,6 +520,7 @@ class ReqKeyMiddleware:
|
|
|
508
520
|
*,
|
|
509
521
|
request: HttpRequest,
|
|
510
522
|
decision: VerificationResult | None,
|
|
523
|
+
consumer_key: str | None,
|
|
511
524
|
response: HttpResponseBase | None,
|
|
512
525
|
status_code: int,
|
|
513
526
|
latency_ms: int,
|
|
@@ -524,6 +537,7 @@ class ReqKeyMiddleware:
|
|
|
524
537
|
self._resource_path_sync(request),
|
|
525
538
|
self._client_ip_sync(request),
|
|
526
539
|
self._consumer_name_sync(request),
|
|
540
|
+
consumer_key,
|
|
527
541
|
),
|
|
528
542
|
)
|
|
529
543
|
except ReqKeyError as exc:
|
|
@@ -542,6 +556,7 @@ class ReqKeyMiddleware:
|
|
|
542
556
|
*,
|
|
543
557
|
request: HttpRequest,
|
|
544
558
|
decision: VerificationResult | None,
|
|
559
|
+
consumer_key: str | None,
|
|
545
560
|
response: HttpResponseBase | None,
|
|
546
561
|
status_code: int,
|
|
547
562
|
latency_ms: int,
|
|
@@ -558,6 +573,7 @@ class ReqKeyMiddleware:
|
|
|
558
573
|
await self._resource_path_async(request),
|
|
559
574
|
await self._client_ip_async(request),
|
|
560
575
|
await self._consumer_name_async(request),
|
|
576
|
+
consumer_key,
|
|
561
577
|
),
|
|
562
578
|
)
|
|
563
579
|
except ReqKeyError as exc:
|
|
@@ -579,6 +595,7 @@ class ReqKeyMiddleware:
|
|
|
579
595
|
resource_path: str,
|
|
580
596
|
client_ip: str | None,
|
|
581
597
|
consumer_name: str | None,
|
|
598
|
+
consumer_key: str | None,
|
|
582
599
|
) -> dict[str, Any]:
|
|
583
600
|
query_params, query = self._captured_query(request)
|
|
584
601
|
request_headers = dict(request.headers.items())
|
|
@@ -596,6 +613,7 @@ class ReqKeyMiddleware:
|
|
|
596
613
|
),
|
|
597
614
|
"query_params": query_params,
|
|
598
615
|
"consumer_name": consumer_name,
|
|
616
|
+
"api_key": consumer_key,
|
|
599
617
|
"request_headers": (
|
|
600
618
|
filtered_headers(request_headers, self.excluded_headers)
|
|
601
619
|
if self.capture_request_headers
|
|
@@ -84,6 +84,8 @@ class ReqKeyLike(Protocol):
|
|
|
84
84
|
user_agent: str | None = None,
|
|
85
85
|
user_id: str | None = None,
|
|
86
86
|
consumer_name: str | None = None,
|
|
87
|
+
api_key: str | None = None,
|
|
88
|
+
consumer_id: str | None = None,
|
|
87
89
|
query_params: Mapping[str, Any] | None = None,
|
|
88
90
|
request_headers: Mapping[str, str] | None = None,
|
|
89
91
|
response_headers: Mapping[str, str] | None = None,
|
|
@@ -255,13 +257,14 @@ class ReqKeyMiddleware:
|
|
|
255
257
|
request_started = time.perf_counter()
|
|
256
258
|
decision: VerificationResult | None = None
|
|
257
259
|
validation_time_ms: float | None = None
|
|
260
|
+
consumer_key = self._consumer_key(environ)
|
|
258
261
|
|
|
259
262
|
if self.mode in {"validate", "both"}:
|
|
260
|
-
consumer_key = self._consumer_key(environ)
|
|
261
263
|
if consumer_key is None:
|
|
262
264
|
self._ingest_denied_safely(
|
|
263
265
|
environ,
|
|
264
266
|
decision=None,
|
|
267
|
+
consumer_key=None,
|
|
265
268
|
status_code=401,
|
|
266
269
|
error="missing_api_key",
|
|
267
270
|
headers={},
|
|
@@ -301,6 +304,7 @@ class ReqKeyMiddleware:
|
|
|
301
304
|
self._ingest_denied_safely(
|
|
302
305
|
environ,
|
|
303
306
|
decision=decision,
|
|
307
|
+
consumer_key=consumer_key,
|
|
304
308
|
status_code=status_code,
|
|
305
309
|
error=error,
|
|
306
310
|
headers=headers,
|
|
@@ -321,6 +325,7 @@ class ReqKeyMiddleware:
|
|
|
321
325
|
environ,
|
|
322
326
|
wrapped_start,
|
|
323
327
|
decision=decision,
|
|
328
|
+
consumer_key=consumer_key,
|
|
324
329
|
)
|
|
325
330
|
|
|
326
331
|
def _run_with_ingest(
|
|
@@ -329,6 +334,7 @@ class ReqKeyMiddleware:
|
|
|
329
334
|
start_response: StartResponse,
|
|
330
335
|
*,
|
|
331
336
|
decision: VerificationResult | None,
|
|
337
|
+
consumer_key: str | None,
|
|
332
338
|
) -> Iterator[bytes]:
|
|
333
339
|
response = _ResponseState()
|
|
334
340
|
started = time.perf_counter()
|
|
@@ -372,6 +378,7 @@ class ReqKeyMiddleware:
|
|
|
372
378
|
self._ingest_safely(
|
|
373
379
|
environ=environ,
|
|
374
380
|
decision=decision,
|
|
381
|
+
consumer_key=consumer_key,
|
|
375
382
|
response_status=500 if failed else response.status_code,
|
|
376
383
|
latency_ms=round((time.perf_counter() - started) * 1000),
|
|
377
384
|
response_headers=response.headers,
|
|
@@ -431,6 +438,7 @@ class ReqKeyMiddleware:
|
|
|
431
438
|
*,
|
|
432
439
|
environ: WSGIEnvironment,
|
|
433
440
|
decision: VerificationResult | None,
|
|
441
|
+
consumer_key: str | None,
|
|
434
442
|
response_status: int,
|
|
435
443
|
latency_ms: int,
|
|
436
444
|
response_headers: Mapping[str, str],
|
|
@@ -456,6 +464,7 @@ class ReqKeyMiddleware:
|
|
|
456
464
|
request_headers.get("user-agent") if self.capture_user_agent else None
|
|
457
465
|
),
|
|
458
466
|
consumer_name=self._consumer_name(environ),
|
|
467
|
+
api_key=consumer_key,
|
|
459
468
|
query_params=query_params,
|
|
460
469
|
request_headers=(
|
|
461
470
|
filtered_headers(request_headers, self.excluded_headers)
|
|
@@ -484,6 +493,7 @@ class ReqKeyMiddleware:
|
|
|
484
493
|
environ: WSGIEnvironment,
|
|
485
494
|
*,
|
|
486
495
|
decision: VerificationResult | None,
|
|
496
|
+
consumer_key: str | None,
|
|
487
497
|
status_code: int,
|
|
488
498
|
error: str,
|
|
489
499
|
headers: Mapping[str, str],
|
|
@@ -498,6 +508,7 @@ class ReqKeyMiddleware:
|
|
|
498
508
|
self._ingest_safely(
|
|
499
509
|
environ=environ,
|
|
500
510
|
decision=decision,
|
|
511
|
+
consumer_key=consumer_key,
|
|
501
512
|
response_status=status_code,
|
|
502
513
|
latency_ms=round((time.perf_counter() - started) * 1000),
|
|
503
514
|
response_headers=response_headers,
|
|
@@ -128,6 +128,8 @@ async def test_async_ingest_uses_request_id_and_truncates_bodies() -> None:
|
|
|
128
128
|
method="POST",
|
|
129
129
|
endpoint="/payments",
|
|
130
130
|
consumer_name="rapidapi-user",
|
|
131
|
+
api_key="prod_consumer",
|
|
132
|
+
consumer_id="consumer_123",
|
|
131
133
|
response_body="x" * 1200,
|
|
132
134
|
)
|
|
133
135
|
await http_client.aclose()
|
|
@@ -135,6 +137,8 @@ async def test_async_ingest_uses_request_id_and_truncates_bodies() -> None:
|
|
|
135
137
|
assert captured["requestId"] == "request_123"
|
|
136
138
|
assert captured["method"] == "POST"
|
|
137
139
|
assert captured["consumerName"] == "rapidapi-user"
|
|
140
|
+
assert captured["apiKey"] == "prod_consumer"
|
|
141
|
+
assert captured["consumerId"] == "consumer_123"
|
|
138
142
|
assert len(str(captured["responseBody"])) == 1000
|
|
139
143
|
|
|
140
144
|
|
|
@@ -170,6 +170,7 @@ def test_django_sync_middleware_validates_attaches_state_and_ingests() -> None:
|
|
|
170
170
|
}
|
|
171
171
|
event = fake.ingest_calls[0]
|
|
172
172
|
assert event["request_id"] == "request_123"
|
|
173
|
+
assert event["api_key"] == "prod_consumer"
|
|
173
174
|
assert event["path"] == "/protected?page=2"
|
|
174
175
|
assert event["query_params"] == {"page": "2"}
|
|
175
176
|
assert event["request_headers"]["X-Safe"] == "visible"
|
|
@@ -196,6 +197,7 @@ def test_django_missing_key_short_circuits_before_view() -> None:
|
|
|
196
197
|
assert called is False
|
|
197
198
|
assert fake.verify_calls == []
|
|
198
199
|
assert fake.ingest_calls[0]["request_id"] is None
|
|
200
|
+
assert fake.ingest_calls[0]["api_key"] is None
|
|
199
201
|
assert fake.ingest_calls[0]["status_code"] == 401
|
|
200
202
|
assert fake.ingest_calls[0]["path"] == "/protected"
|
|
201
203
|
|
|
@@ -217,6 +219,7 @@ def test_django_rate_limit_preserves_retry_after_and_skips_view() -> None:
|
|
|
217
219
|
assert json.loads(response.content)["error"] == "rate_limited"
|
|
218
220
|
assert fake.ingest_calls[0]["request_id"] == "request_limited"
|
|
219
221
|
assert fake.ingest_calls[0]["status_code"] == 429
|
|
222
|
+
assert fake.ingest_calls[0]["api_key"] == "prod_consumer"
|
|
220
223
|
|
|
221
224
|
|
|
222
225
|
def test_django_resolves_consumer_name_and_client_ip() -> None:
|
|
@@ -243,6 +246,7 @@ def test_django_resolves_consumer_name_and_client_ip() -> None:
|
|
|
243
246
|
|
|
244
247
|
assert response.status_code == 200
|
|
245
248
|
assert fake.ingest_calls[0]["consumer_name"] == "rapidapi-user"
|
|
249
|
+
assert fake.ingest_calls[0]["api_key"] == "prod_consumer"
|
|
246
250
|
assert fake.ingest_calls[0]["client_ip"] == "203.0.113.30"
|
|
247
251
|
|
|
248
252
|
|
|
@@ -306,7 +310,7 @@ def test_django_reports_validation_and_ingestion_errors() -> None:
|
|
|
306
310
|
assert ingest_events[0].request_id == "request_123"
|
|
307
311
|
|
|
308
312
|
|
|
309
|
-
def
|
|
313
|
+
def test_django_query_key_is_redacted_from_path_but_ingested_as_identity() -> None:
|
|
310
314
|
fake = FakeSyncReqKey(allowed_result())
|
|
311
315
|
|
|
312
316
|
with override_settings(
|
|
@@ -327,7 +331,7 @@ def test_django_query_key_is_redacted_from_analytics() -> None:
|
|
|
327
331
|
event = fake.ingest_calls[0]
|
|
328
332
|
assert event["query_params"] == {"page": "2"}
|
|
329
333
|
assert event["path"] == "/protected?page=2"
|
|
330
|
-
assert "
|
|
334
|
+
assert event["api_key"] == "prod_secret"
|
|
331
335
|
|
|
332
336
|
|
|
333
337
|
def test_django_fail_open_exposes_error_and_continues() -> None:
|
|
@@ -371,6 +375,7 @@ async def test_django_async_middleware_uses_async_client_without_adaptation() ->
|
|
|
371
375
|
assert response.status_code == 200
|
|
372
376
|
assert fake.verify_calls[0]["key"] == "prod_consumer"
|
|
373
377
|
assert fake.ingest_calls[0]["request_id"] == "request_123"
|
|
378
|
+
assert fake.ingest_calls[0]["api_key"] == "prod_consumer"
|
|
374
379
|
|
|
375
380
|
|
|
376
381
|
@pytest.mark.asyncio
|
|
@@ -410,6 +415,7 @@ async def test_django_async_denial_uses_async_resolvers_and_ingests() -> None:
|
|
|
410
415
|
assert fake.ingest_calls[0]["request_id"] == "request_limited"
|
|
411
416
|
assert fake.ingest_calls[0]["status_code"] == 429
|
|
412
417
|
assert fake.ingest_calls[0]["consumer_name"] == "rapidapi-user"
|
|
418
|
+
assert fake.ingest_calls[0]["api_key"] == "prod_consumer"
|
|
413
419
|
assert fake.ingest_calls[0]["client_ip"] == "203.0.113.40"
|
|
414
420
|
|
|
415
421
|
|
|
@@ -449,3 +455,4 @@ def test_django_ingest_only_mode_does_not_require_consumer_key() -> None:
|
|
|
449
455
|
assert response.status_code == 200
|
|
450
456
|
assert fake.verify_calls == []
|
|
451
457
|
assert fake.ingest_calls[0]["api_id"] == "payments"
|
|
458
|
+
assert fake.ingest_calls[0]["api_key"] is None
|
|
@@ -129,6 +129,7 @@ def test_both_mode_gates_then_records_correlated_metadata() -> None:
|
|
|
129
129
|
]
|
|
130
130
|
assert fake.ingest_calls[0]["request_id"] == "request_123"
|
|
131
131
|
assert fake.ingest_calls[0]["api_id"] == "payments"
|
|
132
|
+
assert fake.ingest_calls[0]["api_key"] == "prod_consumer"
|
|
132
133
|
assert fake.ingest_calls[0]["path"] == "/protected"
|
|
133
134
|
assert fake.ingest_calls[0]["query_params"] is None
|
|
134
135
|
|
|
@@ -152,6 +153,7 @@ def test_ingest_mode_does_not_require_or_validate_a_consumer_key() -> None:
|
|
|
152
153
|
assert fake.verify_calls == []
|
|
153
154
|
assert fake.ingest_calls[0]["request_id"] is None
|
|
154
155
|
assert fake.ingest_calls[0]["api_id"] == "payments"
|
|
156
|
+
assert fake.ingest_calls[0]["api_key"] is None
|
|
155
157
|
|
|
156
158
|
|
|
157
159
|
def test_missing_key_is_denied_before_handler() -> None:
|
|
@@ -163,6 +165,7 @@ def test_missing_key_is_denied_before_handler() -> None:
|
|
|
163
165
|
assert response.json()["error"] == "missing_api_key"
|
|
164
166
|
assert fake.verify_calls == []
|
|
165
167
|
assert fake.ingest_calls[0]["request_id"] is None
|
|
168
|
+
assert fake.ingest_calls[0]["api_key"] is None
|
|
166
169
|
assert fake.ingest_calls[0]["status_code"] == 401
|
|
167
170
|
assert fake.ingest_calls[0]["path"] == "/protected"
|
|
168
171
|
|
|
@@ -198,6 +201,7 @@ def test_rate_limit_preserves_retry_after() -> None:
|
|
|
198
201
|
assert response.status_code == 429
|
|
199
202
|
assert response.headers["Retry-After"] == "4"
|
|
200
203
|
assert fake.ingest_calls[0]["status_code"] == 429
|
|
204
|
+
assert fake.ingest_calls[0]["api_key"] == "prod_consumer"
|
|
201
205
|
|
|
202
206
|
|
|
203
207
|
def test_denied_ingestion_can_be_disabled() -> None:
|
|
@@ -268,7 +272,7 @@ def test_query_parameter_key_and_custom_header_name() -> None:
|
|
|
268
272
|
assert fake.verify_calls[0]["key"] == "prod_consumer"
|
|
269
273
|
|
|
270
274
|
|
|
271
|
-
def
|
|
275
|
+
def test_query_parameter_consumer_key_is_only_ingested_as_identity() -> None:
|
|
272
276
|
fake = FakeReqKey(allowed_result())
|
|
273
277
|
with TestClient(
|
|
274
278
|
make_app(
|
|
@@ -283,7 +287,7 @@ def test_query_parameter_consumer_key_is_never_ingested() -> None:
|
|
|
283
287
|
assert response.status_code == 200
|
|
284
288
|
assert fake.ingest_calls[0]["query_params"] == {"page": "2"}
|
|
285
289
|
assert fake.ingest_calls[0]["path"] == "/protected?page=2"
|
|
286
|
-
assert
|
|
290
|
+
assert fake.ingest_calls[0]["api_key"] == "prod_secret"
|
|
287
291
|
|
|
288
292
|
|
|
289
293
|
def test_capture_options_redact_consumer_key_and_capture_response() -> None:
|
|
@@ -308,6 +312,7 @@ def test_capture_options_redact_consumer_key_and_capture_response() -> None:
|
|
|
308
312
|
assert event["path"] == "/protected?page=2"
|
|
309
313
|
assert event["request_headers"]["x-safe"] == "visible"
|
|
310
314
|
assert "x-api-key" not in event["request_headers"]
|
|
315
|
+
assert event["api_key"] == "prod_consumer"
|
|
311
316
|
assert event["response_headers"]["content-type"] == "application/json"
|
|
312
317
|
assert json.loads(event["response_body"]) == {
|
|
313
318
|
"ok": True,
|
|
@@ -336,6 +341,7 @@ def test_consumer_name_and_client_ip_resolvers_are_recorded() -> None:
|
|
|
336
341
|
|
|
337
342
|
assert response.status_code == 200
|
|
338
343
|
assert fake.ingest_calls[0]["consumer_name"] == "rapidapi-user"
|
|
344
|
+
assert fake.ingest_calls[0]["api_key"] == "prod_consumer"
|
|
339
345
|
assert fake.ingest_calls[0]["client_ip"] == "203.0.113.10"
|
|
340
346
|
|
|
341
347
|
|
|
@@ -183,6 +183,7 @@ def test_wsgi_both_mode_validates_and_records_correlated_metadata() -> None:
|
|
|
183
183
|
]
|
|
184
184
|
event = fake.ingest_calls[0]
|
|
185
185
|
assert event["request_id"] == "request_123"
|
|
186
|
+
assert event["api_key"] == "prod_consumer"
|
|
186
187
|
assert event["path"] == "/protected?page=2"
|
|
187
188
|
assert event["query_params"] == {"page": "2"}
|
|
188
189
|
assert event["client_ip"] == "127.0.0.1"
|
|
@@ -201,6 +202,7 @@ def test_wsgi_missing_key_is_denied_before_application() -> None:
|
|
|
201
202
|
assert json.loads(body)["error"] == "missing_api_key"
|
|
202
203
|
assert fake.verify_calls == []
|
|
203
204
|
assert fake.ingest_calls[0]["request_id"] is None
|
|
205
|
+
assert fake.ingest_calls[0]["api_key"] is None
|
|
204
206
|
assert fake.ingest_calls[0]["status_code"] == 401
|
|
205
207
|
assert fake.ingest_calls[0]["path"] == "/protected"
|
|
206
208
|
|
|
@@ -217,6 +219,7 @@ def test_wsgi_rate_limit_preserves_retry_after_and_skips_application() -> None:
|
|
|
217
219
|
assert json.loads(body)["error"] == "rate_limited"
|
|
218
220
|
assert fake.ingest_calls[0]["request_id"] == "request_limited"
|
|
219
221
|
assert fake.ingest_calls[0]["status_code"] == 429
|
|
222
|
+
assert fake.ingest_calls[0]["api_key"] == "prod_consumer"
|
|
220
223
|
|
|
221
224
|
|
|
222
225
|
def test_wsgi_denied_ingestion_can_be_disabled() -> None:
|
|
@@ -259,6 +262,7 @@ def test_wsgi_resolves_consumer_name_and_client_ip() -> None:
|
|
|
259
262
|
|
|
260
263
|
assert status == 200
|
|
261
264
|
assert fake.ingest_calls[0]["consumer_name"] == "rapidapi-user"
|
|
265
|
+
assert fake.ingest_calls[0]["api_key"] == "prod_consumer"
|
|
262
266
|
assert fake.ingest_calls[0]["client_ip"] == "203.0.113.10"
|
|
263
267
|
|
|
264
268
|
|
|
@@ -322,7 +326,7 @@ def test_wsgi_reports_validation_and_ingestion_errors() -> None:
|
|
|
322
326
|
assert ingest_events[0].request_id == "request_123"
|
|
323
327
|
|
|
324
328
|
|
|
325
|
-
def
|
|
329
|
+
def test_wsgi_query_key_is_redacted_from_path_but_ingested_as_identity() -> None:
|
|
326
330
|
fake = FakeReqKey(allowed_result())
|
|
327
331
|
middleware = ReqKeyMiddleware(
|
|
328
332
|
app,
|
|
@@ -343,7 +347,7 @@ def test_wsgi_query_key_is_validated_but_never_ingested() -> None:
|
|
|
343
347
|
event = fake.ingest_calls[0]
|
|
344
348
|
assert event["query_params"] == {"page": "2"}
|
|
345
349
|
assert event["path"] == "/protected?page=2"
|
|
346
|
-
assert "
|
|
350
|
+
assert event["api_key"] == "prod_secret"
|
|
347
351
|
|
|
348
352
|
|
|
349
353
|
def test_wsgi_failure_mode_open_runs_application_and_records_error() -> None:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|