-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
**Description:** Implementation of address based global rate limiting **option**. Rate limiting is an optional security feature that controls API request frequency on a remote address basis. It's enabled by setting the `STAC_FASTAPI_RATE_LIMIT` environment variable, e.g., `500/minute`. This limits each client to 500 requests per minute, helping prevent abuse and maintain API stability. Implementation examples are available in the [examples/rate_limit](examples/rate_limit) directory. **PR Checklist:** - [x] Code is formatted and linted (run `pre-commit run --all-files`) - [x] Tests pass (run `make test`) - [x] Documentation has been updated to reflect changes, if applicable - [x] Changes are added to the changelog
- Loading branch information
Showing
11 changed files
with
254 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
version: '3.9' | ||
|
||
services: | ||
app-elasticsearch: | ||
container_name: stac-fastapi-es | ||
image: stac-utils/stac-fastapi-es | ||
restart: always | ||
build: | ||
context: . | ||
dockerfile: dockerfiles/Dockerfile.dev.es | ||
environment: | ||
- STAC_FASTAPI_TITLE=stac-fastapi-elasticsearch | ||
- STAC_FASTAPI_DESCRIPTION=A STAC FastAPI with an Elasticsearch backend | ||
- STAC_FASTAPI_VERSION=2.1 | ||
- APP_HOST=0.0.0.0 | ||
- APP_PORT=8080 | ||
- RELOAD=true | ||
- ENVIRONMENT=local | ||
- WEB_CONCURRENCY=10 | ||
- ES_HOST=elasticsearch | ||
- ES_PORT=9200 | ||
- ES_USE_SSL=false | ||
- ES_VERIFY_CERTS=false | ||
- BACKEND=elasticsearch | ||
- STAC_FASTAPI_RATE_LIMIT=500/minute | ||
ports: | ||
- "8080:8080" | ||
volumes: | ||
- ./stac_fastapi:/app/stac_fastapi | ||
- ./scripts:/app/scripts | ||
- ./esdata:/usr/share/elasticsearch/data | ||
depends_on: | ||
- elasticsearch | ||
command: | ||
bash -c "./scripts/wait-for-it-es.sh es-container:9200 && python -m stac_fastapi.elasticsearch.app" | ||
|
||
app-opensearch: | ||
container_name: stac-fastapi-os | ||
image: stac-utils/stac-fastapi-os | ||
restart: always | ||
build: | ||
context: . | ||
dockerfile: dockerfiles/Dockerfile.dev.os | ||
environment: | ||
- STAC_FASTAPI_TITLE=stac-fastapi-opensearch | ||
- STAC_FASTAPI_DESCRIPTION=A STAC FastAPI with an Opensearch backend | ||
- STAC_FASTAPI_VERSION=3.0.0a2 | ||
- APP_HOST=0.0.0.0 | ||
- APP_PORT=8082 | ||
- RELOAD=true | ||
- ENVIRONMENT=local | ||
- WEB_CONCURRENCY=10 | ||
- ES_HOST=opensearch | ||
- ES_PORT=9202 | ||
- ES_USE_SSL=false | ||
- ES_VERIFY_CERTS=false | ||
- BACKEND=opensearch | ||
- STAC_FASTAPI_RATE_LIMIT=200/minute | ||
ports: | ||
- "8082:8082" | ||
volumes: | ||
- ./stac_fastapi:/app/stac_fastapi | ||
- ./scripts:/app/scripts | ||
- ./osdata:/usr/share/opensearch/data | ||
depends_on: | ||
- opensearch | ||
command: | ||
bash -c "./scripts/wait-for-it-es.sh os-container:9202 && python -m stac_fastapi.opensearch.app" | ||
|
||
elasticsearch: | ||
container_name: es-container | ||
image: docker.elastic.co/elasticsearch/elasticsearch:${ELASTICSEARCH_VERSION:-8.11.0} | ||
hostname: elasticsearch | ||
environment: | ||
ES_JAVA_OPTS: -Xms512m -Xmx1g | ||
volumes: | ||
- ./elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml | ||
- ./elasticsearch/snapshots:/usr/share/elasticsearch/snapshots | ||
ports: | ||
- "9200:9200" | ||
|
||
opensearch: | ||
container_name: os-container | ||
image: opensearchproject/opensearch:${OPENSEARCH_VERSION:-2.11.1} | ||
hostname: opensearch | ||
environment: | ||
- discovery.type=single-node | ||
- plugins.security.disabled=true | ||
- OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m | ||
volumes: | ||
- ./opensearch/config/opensearch.yml:/usr/share/opensearch/config/opensearch.yml | ||
- ./opensearch/snapshots:/usr/share/opensearch/snapshots | ||
ports: | ||
- "9202:9202" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,6 +19,7 @@ | |
"pygeofilter==0.2.1", | ||
"typing_extensions==4.8.0", | ||
"jsonschema", | ||
"slowapi==0.1.9", | ||
] | ||
|
||
setup( | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
"""Rate limiting middleware.""" | ||
|
||
import logging | ||
import os | ||
from typing import Optional | ||
|
||
from fastapi import FastAPI, Request | ||
from slowapi import Limiter, _rate_limit_exceeded_handler | ||
from slowapi.errors import RateLimitExceeded | ||
from slowapi.middleware import SlowAPIMiddleware | ||
from slowapi.util import get_remote_address | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
def get_limiter(key_func=get_remote_address): | ||
"""Create and return a Limiter instance for rate limiting.""" | ||
return Limiter(key_func=key_func) | ||
|
||
|
||
def setup_rate_limit( | ||
app: FastAPI, rate_limit: Optional[str] = None, key_func=get_remote_address | ||
): | ||
"""Set up rate limiting middleware.""" | ||
RATE_LIMIT = rate_limit or os.getenv("STAC_FASTAPI_RATE_LIMIT") | ||
|
||
if not RATE_LIMIT: | ||
logger.info("Rate limiting is disabled") | ||
return | ||
|
||
logger.info(f"Setting up rate limit with RATE_LIMIT={RATE_LIMIT}") | ||
|
||
limiter = get_limiter(key_func) | ||
app.state.limiter = limiter | ||
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) | ||
app.add_middleware(SlowAPIMiddleware) | ||
|
||
@app.middleware("http") | ||
@limiter.limit(RATE_LIMIT) | ||
async def rate_limit_middleware(request: Request, call_next): | ||
response = await call_next(request) | ||
return response | ||
|
||
logger.info("Rate limit setup complete") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import logging | ||
|
||
import pytest | ||
from httpx import AsyncClient | ||
from slowapi.errors import RateLimitExceeded | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_rate_limit(app_client_rate_limit: AsyncClient, ctx): | ||
expected_status_codes = [200, 200, 429, 429, 429] | ||
|
||
for i, expected_status_code in enumerate(expected_status_codes): | ||
try: | ||
response = await app_client_rate_limit.get("/collections") | ||
status_code = response.status_code | ||
except RateLimitExceeded: | ||
status_code = 429 | ||
|
||
logger.info(f"Request {i+1}: Status code {status_code}") | ||
assert ( | ||
status_code == expected_status_code | ||
), f"Expected status code {expected_status_code}, but got {status_code}" | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_rate_limit_no_limit(app_client: AsyncClient, ctx): | ||
expected_status_codes = [200, 200, 200, 200, 200] | ||
|
||
for i, expected_status_code in enumerate(expected_status_codes): | ||
response = await app_client.get("/collections") | ||
status_code = response.status_code | ||
|
||
logger.info(f"Request {i+1}: Status code {status_code}") | ||
assert ( | ||
status_code == expected_status_code | ||
), f"Expected status code {expected_status_code}, but got {status_code}" |