Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 75 additions & 4 deletions apisix_conf/setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ if [ "$ENV" = "dev" ]; then
"rejected_code": 429
},
"limit-count": {
"count": 10,
"count": 20,
"time_window": 60,
"key": "consumer_name",
"rejected_code": 429
Expand Down Expand Up @@ -132,13 +132,13 @@ if [ "$ENV" = "dev" ]; then
"plugins": {
"key-auth": {},
"limit-req": {
"rate": 10,
"burst": 20,
"rate": 20,
"burst": 40,
"key": "consumer_name",
"rejected_code": 429
},
"limit-count": {
"count": 10,
"count": 200,
"time_window": 60,
"key": "consumer_name",
"rejected_code": 429
Expand All @@ -158,5 +158,76 @@ if [ "$ENV" = "dev" ]; then
"http://localhost:$port/apisix/admin/routes" 2>&1)

check_response "$response" "$port"

response=$(curl -i -X PUT \
-H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' \
-d '{
"id": "baz",
"uri": "/baz",
"vars": [
"OR",
[
"http_apikey",
"~~",
".+"
],
[
"arg_apikey",
"~~",
".+"
]
],
"plugins": {
"proxy-rewrite": {
"regex_uri": ["^/baz(.*)", "/$1"]
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"web1:80": 1
},
"scheme": "http"
}
}' \
"http://localhost:$port/apisix/admin/routes" 2>&1)

check_response "$response" "$port"

response=$(curl -i -X PUT \
-H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' \
-d '{
"id": "qux",
"uri": "/qux",
"vars": [
"OR",
[
"http_apikey",
"~~",
".+"
],
[
"arg_apikey",
"~~",
".+"
]
],
"plugins": {
"key-auth": {},
"proxy-rewrite": {
"regex_uri": ["^/qux(.*)", "/$1"]
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"web1:80": 1
},
"scheme": "http"
}
}' \
"http://localhost:$port/apisix/admin/routes" 2>&1)

check_response "$response" "$port"
done
fi
15 changes: 15 additions & 0 deletions backend/app/models/apisix.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,21 @@ def set_group_id(cls, value: list[str]) -> str:
return USER_GROUP


class APISixConsumerGroup(BaseModel):
"""
Representing an APISIX consumer group.

Attributes:
instance_name (str): The name of the APISIX instance.
id (str): The group ID.
plugins (dict[str, dict[str, Any]]): The plugins associated with the group.
"""

instance_name: str
id: str
plugins: dict[str, dict[str, Any]] = {}


class APISixRoutes(BaseModel):
"""
Represents a list of key auth routes in APISix.
Expand Down
11 changes: 10 additions & 1 deletion backend/app/models/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,18 @@ class MessageResponse(BaseModel):
message: str = Field(..., description="The response message")


class RouteWithLimits(BaseModel):
"""A route URL with rate limit information"""

url: str = Field(..., description="The route URL")
limits: str = Field(..., description="Effective rate limits for the current user")


class GetRoutes(BaseModel):
"""
Response model for the GET /routes endpoint
"""

routes: list[str] = Field(..., description="The routes that require key authentication")
routes: list[RouteWithLimits] = Field(
..., description="The routes that require key authentication"
)
59 changes: 42 additions & 17 deletions backend/app/routers/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
from app.dependencies.jwt_token import validate_token, AccessToken
from app.dependencies.http_client import get_http_client
from app.services import apisix
from app.models.response import GetRoutes
from app.models.apisix import APISixRoutes
from app.exceptions import APISIXError
from app.models.response import GetRoutes, RouteWithLimits
from app.models.apisix import APISixConsumer
from app.models.request import User

router = APIRouter()

Expand All @@ -23,18 +23,18 @@
# Either naming this route differently or creating routes for routes and apikey
@router.get("/routes", response_model=GetRoutes)
async def get_routes(
_token: AccessToken = Depends(validate_token),
token: AccessToken = Depends(validate_token),
client: AsyncClient = Depends(get_http_client),
) -> GetRoutes:
"""
Retrieve all the APISIX routes that requires key authentication.

Args:
- _token (AccessToken): The access token used for authentication.
- token (AccessToken): The access token used for authentication.
- client (AsyncClient): The HTTP client used for making requests.

Returns:
- GetRoutes: An object containing the retrieved routes.
- GetRoutes: An object containing the retrieved routes with rate limits.

Raises:
- HTTPException: If there is an error retrieving the routes.
Expand All @@ -45,24 +45,49 @@ async def get_routes(

logger.debug("retrieving all the routes that requires key authentication")

routes_responses: list[APISixRoutes | APISIXError] = await asyncio.gather(
*apisix.create_tasks(apisix.get_routes, client), return_exceptions=True
# Get APISIX consumer for the user from all instances
user = User(id=token.sub, groups=token.groups)
apisix_consumers = await asyncio.gather(
*apisix.create_tasks(apisix.get_apisix_consumer, client, user.id),
return_exceptions=True,
)
valid_consumers = [
consumer if isinstance(consumer, APISixConsumer) else None for consumer in apisix_consumers
]

routes_responses = await asyncio.gather(
*[
apisix.get_routes_with_limits(client, instance, consumer)
for instance, consumer in zip(config.apisix.instances, valid_consumers)
],
return_exceptions=True,
)

# Routes are same across all instances so error only if all instances failed
if all(isinstance(response, APISIXError) for response in routes_responses):
if all(isinstance(response, Exception) for response in routes_responses):
raise HTTPException(
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
detail=str(routes_responses[0]),
)

routes = [
route
for response in routes_responses
if isinstance(response, APISixRoutes)
for route in response.routes
all_routes = []
for response in routes_responses:
if isinstance(response, list):
all_routes.extend(response)

unique_routes_dict = {}
for route in all_routes:
url = route["url"]
if url not in unique_routes_dict:
unique_routes_dict[url] = route

routes_with_limits = [
RouteWithLimits(
url=route["url"],
limits=route["limits"],
)
for route in unique_routes_dict.values()
]
unique_routes = list(set(routes))
logger.debug("found %s unique routes: %s", len(unique_routes), unique_routes)

return GetRoutes(routes=unique_routes)
logger.debug("found %s unique routes: %s", len(routes_with_limits), routes_with_limits)
return GetRoutes(routes=routes_with_limits)
Loading