fix(api): read OAUTH_TTL_DAYS from dify_config, keeping the clamp-and-warn for bad values

This commit is contained in:
GareArc
2026-10-12 17:34:42 -07:00
parent f219ecc0ec
commit 10d15e2e9c
6 changed files with 73 additions and 41 deletions
+3
View File
@@ -48,6 +48,9 @@ ACCESS_TOKEN_EXPIRE_MINUTES=60
# Refresh token expiration time in days
REFRESH_TOKEN_EXPIRE_DAYS=30
# Lifetime in days of difyctl (OAuth device flow) tokens, 1-365; out-of-range values are clamped
# OAUTH_TTL_DAYS=14
# redis configuration
REDIS_HOST=localhost
REDIS_PORT=6379
+37 -1
View File
@@ -1,6 +1,7 @@
import logging
from datetime import datetime, timedelta
from enum import StrEnum
from typing import Literal
from typing import Final, Literal
from pydantic import (
AliasChoices,
@@ -18,6 +19,12 @@ from pydantic_settings import BaseSettings
from .hosted_service import HostedServiceConfig
logger = logging.getLogger(__name__)
DEFAULT_OAUTH_TTL_DAYS: Final = 14
MIN_OAUTH_TTL_DAYS: Final = 1
MAX_OAUTH_TTL_DAYS: Final = 365
class SecurityConfig(BaseSettings):
"""
@@ -1090,6 +1097,35 @@ class AuthConfig(BaseSettings):
default=True,
)
OAUTH_TTL_DAYS: int = Field(
description="Lifetime in days of the OAuth bearer tokens the device flow issues (difyctl login). "
f"Values outside {MIN_OAUTH_TTL_DAYS}-{MAX_OAUTH_TTL_DAYS} are clamped; a non-integer falls back "
"to the default.",
ge=MIN_OAUTH_TTL_DAYS,
le=MAX_OAUTH_TTL_DAYS,
default=DEFAULT_OAUTH_TTL_DAYS,
)
@field_validator("OAUTH_TTL_DAYS", mode="before")
@classmethod
def _clamp_oauth_ttl_days(cls, v: object) -> int:
# Clamp and warn instead of failing startup, so deployments with a bad value keep booting.
try:
value = int(str(v))
except ValueError:
logger.warning("OAUTH_TTL_DAYS=%r is not an int; falling back to %d", v, DEFAULT_OAUTH_TTL_DAYS)
return DEFAULT_OAUTH_TTL_DAYS
clamped = min(max(value, MIN_OAUTH_TTL_DAYS), MAX_OAUTH_TTL_DAYS)
if clamped != value:
logger.warning(
"OAUTH_TTL_DAYS=%d outside %d-%d; clamping to %d",
value,
MIN_OAUTH_TTL_DAYS,
MAX_OAUTH_TTL_DAYS,
clamped,
)
return clamped
OPENAPI_RATE_LIMIT_PER_TOKEN: NonNegativeInt = Field(
description="Per-token rate limit on /openapi/v1/* (requests per minute). "
"Bucket keyed on sha256(token), shared across api replicas via Redis. "
+2 -26
View File
@@ -1,7 +1,6 @@
"""Outer adapters for the OAuth device external-SSO use cases."""
import logging
import os
import secrets
from collections.abc import Mapping
from datetime import UTC, datetime, timedelta
@@ -46,10 +45,6 @@ from services.oauth_device_contracts import (
logger = logging.getLogger(__name__)
_EMAIL_FIELD = Field(min_length=3, max_length=320, pattern=r"^[^@\s]+@[^@\s]+$")
_DEFAULT_OAUTH_TTL_DAYS = 14
_MIN_OAUTH_TTL_DAYS = 1
_MAX_OAUTH_TTL_DAYS = 365
_TTL_ENV_VAR = "OAUTH_TTL_DAYS"
_OAUTH_TOKEN_BODY_BYTES = 32
_RESERVE_APPROVAL_NONCE_LUA = """
local current = redis.call('GET', KEYS[1])
@@ -92,28 +87,9 @@ class EnvironmentOAuthDeviceTokenTTLPolicy(OAuthDeviceTokenTTLPolicy):
@override
def ttl_days(self, workspace_id: str | None) -> int:
# Reserved for a future tenant-specific policy, which should take
# precedence over the deployment-wide environment value.
# precedence over the deployment-wide setting.
_ = workspace_id
raw = os.environ.get(_TTL_ENV_VAR)
if raw is None:
return _DEFAULT_OAUTH_TTL_DAYS
try:
value = int(raw)
except ValueError:
logger.warning(
"%s=%r is not an int; falling back to %d",
_TTL_ENV_VAR,
raw,
_DEFAULT_OAUTH_TTL_DAYS,
)
return _DEFAULT_OAUTH_TTL_DAYS
if value < _MIN_OAUTH_TTL_DAYS:
logger.warning("%s=%d below min %d; clamping", _TTL_ENV_VAR, value, _MIN_OAUTH_TTL_DAYS)
return _MIN_OAUTH_TTL_DAYS
if value > _MAX_OAUTH_TTL_DAYS:
logger.warning("%s=%d above max %d; clamping", _TTL_ENV_VAR, value, _MAX_OAUTH_TTL_DAYS)
return _MAX_OAUTH_TTL_DAYS
return value
return dify_config.OAUTH_TTL_DAYS
class OAuthDeviceTokenIssuanceGateway(OAuthDeviceTokenIssuer):
@@ -0,0 +1,24 @@
import pytest
from configs.feature import AuthConfig
from tests.unit_tests.configs._isolated_settings import InitSettingsOnly
class _IsolatedAuthConfig(InitSettingsOnly, AuthConfig):
pass
def test_oauth_ttl_days_defaults_to_fourteen() -> None:
config = _IsolatedAuthConfig()
assert config.OAUTH_TTL_DAYS == 14
@pytest.mark.parametrize(
("configured", "expected"),
[("30", 30), ("1", 1), ("365", 365), ("0", 1), ("-5", 1), ("500", 365), ("invalid", 14), ("", 14)],
)
def test_oauth_ttl_days_clamps_or_falls_back_instead_of_failing(configured: str, expected: int) -> None:
config = _IsolatedAuthConfig(OAUTH_TTL_DAYS=configured)
assert expected == config.OAUTH_TTL_DAYS
@@ -4,6 +4,7 @@ from typing import override
import pytest
from configs import dify_config
from extensions.ext_redis import RedisClientWrapper
from libs import jws
from libs.rate_limit import LIMIT_APPROVE_EXT_PER_EMAIL
@@ -172,18 +173,7 @@ def test_approval_nonce_release_only_removes_matching_reservation(config_overrid
assert gateway.reserve_approval_nonce("nonce-1", "reservation-2") is True
@pytest.mark.parametrize(
("configured", "expected"),
[(None, 14), ("invalid", 14), ("0", 1), ("500", 365), ("30", 30)],
)
def test_token_ttl_policy_owns_environment_parsing(
monkeypatch: pytest.MonkeyPatch,
configured: str | None,
expected: int,
) -> None:
if configured is None:
monkeypatch.delenv("OAUTH_TTL_DAYS", raising=False)
else:
monkeypatch.setenv("OAUTH_TTL_DAYS", configured)
def test_token_ttl_policy_reads_the_configured_days(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(dify_config, "OAUTH_TTL_DAYS", 30)
assert EnvironmentOAuthDeviceTokenTTLPolicy().ttl_days("workspace-1") == expected
assert EnvironmentOAuthDeviceTokenTTLPolicy().ttl_days("workspace-1") == 30
@@ -19,6 +19,9 @@ KNOWLEDGE_FS_JWT_SECRET=
KNOWLEDGE_FS_SSE_READ_TIMEOUT_SECONDS=300
KNOWLEDGE_FS_TIMEOUT_SECONDS=10
# Lifetime in days of difyctl (OAuth device flow) tokens, 1-365; out-of-range values are clamped
# OAUTH_TTL_DAYS=14
# Cloudflare Turnstile server-side verification for Dify Cloud sign-in
TURNSTILE_SECRET_KEY=
TURNSTILE_ALLOWED_HOSTNAMES=