Skip to content

Commit 0eeacdc

Browse files
authored
refactor: enhance API token validation with session locking and last used timestamp update (#12426)
Signed-off-by: -LAN- <[email protected]>
1 parent 41f39bf commit 0eeacdc

File tree

5 files changed

+35
-23
lines changed

5 files changed

+35
-23
lines changed

api/controllers/service_api/wraps.py

+22-15
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from collections.abc import Callable
2-
from datetime import UTC, datetime
2+
from datetime import UTC, datetime, timedelta
33
from enum import Enum
44
from functools import wraps
55
from typing import Optional
@@ -8,6 +8,8 @@
88
from flask_login import user_logged_in # type: ignore
99
from flask_restful import Resource # type: ignore
1010
from pydantic import BaseModel
11+
from sqlalchemy import select, update
12+
from sqlalchemy.orm import Session
1113
from werkzeug.exceptions import Forbidden, Unauthorized
1214

1315
from extensions.ext_database import db
@@ -174,7 +176,7 @@ def decorated(*args, **kwargs):
174176
return decorator
175177

176178

177-
def validate_and_get_api_token(scope=None):
179+
def validate_and_get_api_token(scope: str | None = None):
178180
"""
179181
Validate and get API token.
180182
"""
@@ -188,20 +190,25 @@ def validate_and_get_api_token(scope=None):
188190
if auth_scheme != "bearer":
189191
raise Unauthorized("Authorization scheme must be 'Bearer'")
190192

191-
api_token = (
192-
db.session.query(ApiToken)
193-
.filter(
194-
ApiToken.token == auth_token,
195-
ApiToken.type == scope,
193+
current_time = datetime.now(UTC).replace(tzinfo=None)
194+
cutoff_time = current_time - timedelta(minutes=1)
195+
with Session(db.engine, expire_on_commit=False) as session:
196+
update_stmt = (
197+
update(ApiToken)
198+
.where(ApiToken.token == auth_token, ApiToken.last_used_at < cutoff_time, ApiToken.type == scope)
199+
.values(last_used_at=current_time)
200+
.returning(ApiToken)
196201
)
197-
.first()
198-
)
199-
200-
if not api_token:
201-
raise Unauthorized("Access token is invalid")
202-
203-
api_token.last_used_at = datetime.now(UTC).replace(tzinfo=None)
204-
db.session.commit()
202+
result = session.execute(update_stmt)
203+
api_token = result.scalar_one_or_none()
204+
205+
if not api_token:
206+
stmt = select(ApiToken).where(ApiToken.token == auth_token, ApiToken.type == scope)
207+
api_token = session.scalar(stmt)
208+
if not api_token:
209+
raise Unauthorized("Access token is invalid")
210+
else:
211+
session.commit()
205212

206213
return api_token
207214

api/docker/entrypoint.sh

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ else
3333
--bind "${DIFY_BIND_ADDRESS:-0.0.0.0}:${DIFY_PORT:-5001}" \
3434
--workers ${SERVER_WORKER_AMOUNT:-1} \
3535
--worker-class ${SERVER_WORKER_CLASS:-gevent} \
36+
--worker-connections ${SERVER_WORKER_CONNECTIONS:-10} \
3637
--timeout ${GUNICORN_TIMEOUT:-200} \
3738
app:app
3839
fi

api/services/billing_service.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import os
2-
from typing import Optional
2+
from typing import Literal, Optional
33

44
import httpx
55
from tenacity import retry, retry_if_exception_type, stop_before_delay, wait_fixed
@@ -17,7 +17,6 @@ def get_info(cls, tenant_id: str):
1717
params = {"tenant_id": tenant_id}
1818

1919
billing_info = cls._send_request("GET", "/subscription/info", params=params)
20-
2120
return billing_info
2221

2322
@classmethod
@@ -47,12 +46,13 @@ def get_invoices(cls, prefilled_email: str = "", tenant_id: str = ""):
4746
retry=retry_if_exception_type(httpx.RequestError),
4847
reraise=True,
4948
)
50-
def _send_request(cls, method, endpoint, json=None, params=None):
49+
def _send_request(cls, method: Literal["GET", "POST", "DELETE"], endpoint: str, json=None, params=None):
5150
headers = {"Content-Type": "application/json", "Billing-Api-Secret-Key": cls.secret_key}
5251

5352
url = f"{cls.base_url}{endpoint}"
5453
response = httpx.request(method, url, json=json, params=params, headers=headers)
55-
54+
if method == "GET" and response.status_code != httpx.codes.OK:
55+
raise ValueError("Unable to retrieve billing information. Please try again later or contact support.")
5656
return response.json()
5757

5858
@staticmethod

docker/.env.example

+5-2
Original file line numberDiff line numberDiff line change
@@ -126,10 +126,13 @@ DIFY_PORT=5001
126126
# The number of API server workers, i.e., the number of workers.
127127
# Formula: number of cpu cores x 2 + 1 for sync, 1 for Gevent
128128
# Reference: https://docs.gunicorn.org/en/stable/design.html#how-many-workers
129-
SERVER_WORKER_AMOUNT=
129+
SERVER_WORKER_AMOUNT=1
130130

131131
# Defaults to gevent. If using windows, it can be switched to sync or solo.
132-
SERVER_WORKER_CLASS=
132+
SERVER_WORKER_CLASS=gevent
133+
134+
# Default number of worker connections, the default is 10.
135+
SERVER_WORKER_CONNECTIONS=10
133136

134137
# Similar to SERVER_WORKER_CLASS.
135138
# If using windows, it can be switched to sync or solo.

docker/docker-compose.yaml

+3-2
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,9 @@ x-shared-env: &shared-api-worker-env
3232
APP_MAX_EXECUTION_TIME: ${APP_MAX_EXECUTION_TIME:-1200}
3333
DIFY_BIND_ADDRESS: ${DIFY_BIND_ADDRESS:-0.0.0.0}
3434
DIFY_PORT: ${DIFY_PORT:-5001}
35-
SERVER_WORKER_AMOUNT: ${SERVER_WORKER_AMOUNT:-}
36-
SERVER_WORKER_CLASS: ${SERVER_WORKER_CLASS:-}
35+
SERVER_WORKER_AMOUNT: ${SERVER_WORKER_AMOUNT:-1}
36+
SERVER_WORKER_CLASS: ${SERVER_WORKER_CLASS:-gevent}
37+
SERVER_WORKER_CONNECTIONS: ${SERVER_WORKER_CONNECTIONS:-10}
3738
CELERY_WORKER_CLASS: ${CELERY_WORKER_CLASS:-}
3839
GUNICORN_TIMEOUT: ${GUNICORN_TIMEOUT:-360}
3940
CELERY_WORKER_AMOUNT: ${CELERY_WORKER_AMOUNT:-}

0 commit comments

Comments
 (0)