mirror of
https://github.com/langgenius/dify.git
synced 2026-10-16 15:35:32 +08:00
fix(api): read OAUTH_TTL_DAYS from dify_config, keeping the clamp-and-warn for bad values
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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. "
|
||||
|
||||
@@ -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=
|
||||
|
||||
Reference in New Issue
Block a user