mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-11 03:12:55 +00:00
refactor(settings): validate cross-field rules in the model
The "AUTH_TYPE=oidc requires OIDC_ISSUER, OIDC_CLIENT_ID and OIDC_FRONTEND_URL" check lived in app.py, so it only ran when the Flask app was imported; a worker or script with the same misconfiguration started fine. It is now a model validator on the auth group and runs wherever Settings is loaded, with the same message. DEPLOYMENT_TYPE, which app.py read straight from the environment to decide whether a missing JWT_SECRET_KEY is fatal, is a documented setting on the server group now, so it shows up in the reference like every other variable the app reads.
This commit is contained in:
1 parent
95d0799494
commit
a8dab8864d
5 files changed
+43
-12
No files matched your search
@@ -1111,6 +1111,12 @@ Public callback URL for MCP OAuth; unset derives it from CONNECTOR_REDIRECT_BASE
|
||||
|
||||
Serving the UI, public URLs, and process-level knobs of the API server.
|
||||
|
||||
### `DEPLOYMENT_TYPE`
|
||||
|
||||
Type `str`, default unset.
|
||||
|
||||
Deployment class, e.g. cloud or production. A production class refuses to run without a configured JWT_SECRET_KEY instead of generating a local one on disk.
|
||||
|
||||
### `SERVE_UI`
|
||||
|
||||
Type `bool`, default `true`.
|
||||
|
||||
+1
-10
@@ -1,5 +1,4 @@
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import uuid
|
||||
|
||||
@@ -170,16 +169,8 @@ def enforce_document_upload_request_size_limit():
|
||||
# only local development may use the atomic filesystem fallback.
|
||||
settings.JWT_SECRET_KEY = resolve_jwt_secret_key(
|
||||
settings.JWT_SECRET_KEY,
|
||||
os.getenv("DEPLOYMENT_TYPE"),
|
||||
settings.DEPLOYMENT_TYPE,
|
||||
)
|
||||
if settings.AUTH_TYPE == "oidc":
|
||||
_missing_oidc = [
|
||||
name
|
||||
for name in ("OIDC_ISSUER", "OIDC_CLIENT_ID", "OIDC_FRONTEND_URL")
|
||||
if not getattr(settings, name)
|
||||
]
|
||||
if _missing_oidc:
|
||||
raise RuntimeError(f"AUTH_TYPE=oidc requires settings: {', '.join(_missing_oidc)}")
|
||||
SIMPLE_JWT_TOKEN = None
|
||||
if settings.AUTH_TYPE == "simple_jwt":
|
||||
payload = {"sub": "local"}
|
||||
|
||||
@@ -4,11 +4,15 @@ from __future__ import annotations
|
||||
|
||||
from typing import Literal, Optional
|
||||
|
||||
from pydantic import Field, field_validator
|
||||
from pydantic import Field, field_validator, model_validator
|
||||
|
||||
from docsgpt.core.settings._shared import SettingsGroup, normalize_choice, normalize_secret
|
||||
|
||||
|
||||
#: Settings an OIDC deployment cannot run without; checked when AUTH_TYPE=oidc.
|
||||
OIDC_REQUIRED = ("OIDC_ISSUER", "OIDC_CLIENT_ID", "OIDC_FRONTEND_URL")
|
||||
|
||||
|
||||
class AuthSettings(SettingsGroup):
|
||||
"""How users authenticate: none, a shared token, per-session JWTs, or OIDC SSO."""
|
||||
|
||||
@@ -91,3 +95,11 @@ class AuthSettings(SettingsGroup):
|
||||
def _normalize_auth_type(cls, v):
|
||||
# ``AUTH_TYPE=None`` and ``AUTH_TYPE=`` in .env both mean "no authentication".
|
||||
return normalize_choice(normalize_secret(v))
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _require_oidc_settings(self):
|
||||
if self.AUTH_TYPE == "oidc":
|
||||
missing = [name for name in OIDC_REQUIRED if not getattr(self, name)]
|
||||
if missing:
|
||||
raise ValueError(f"AUTH_TYPE=oidc requires settings: {', '.join(missing)}")
|
||||
return self
|
||||
@@ -12,6 +12,13 @@ from docsgpt.core.settings._shared import SettingsGroup
|
||||
class ServerSettings(SettingsGroup):
|
||||
"""Serving the UI, public URLs, and process-level knobs of the API server."""
|
||||
|
||||
DEPLOYMENT_TYPE: Optional[str] = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"Deployment class, e.g. cloud or production. A production class refuses to run without a "
|
||||
"configured JWT_SECRET_KEY instead of generating a local one on disk."
|
||||
),
|
||||
)
|
||||
SERVE_UI: bool = Field(
|
||||
default=True, description="Serve the web UI shipped in the package (docsgpt/static) from the API process."
|
||||
)
|
||||
|
||||
@@ -110,6 +110,21 @@ class TestReference:
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestCrossFieldRules:
|
||||
OIDC = {"OIDC_ISSUER": "https://idp.example/", "OIDC_CLIENT_ID": "docsgpt", "OIDC_FRONTEND_URL": "http://app"}
|
||||
|
||||
def test_oidc_requires_issuer_client_and_frontend(self):
|
||||
with pytest.raises(ValidationError, match="AUTH_TYPE=oidc requires settings: OIDC_CLIENT_ID, OIDC_FRONTEND_URL"):
|
||||
Settings.model_validate({"AUTH_TYPE": "oidc", "OIDC_ISSUER": self.OIDC["OIDC_ISSUER"]})
|
||||
|
||||
def test_oidc_with_required_settings_loads(self):
|
||||
assert Settings.model_validate({"AUTH_TYPE": "OIDC", **self.OIDC}).AUTH_TYPE == "oidc"
|
||||
|
||||
def test_oidc_settings_are_not_required_for_other_modes(self):
|
||||
assert Settings.model_validate({"AUTH_TYPE": "session_jwt"}).OIDC_ISSUER is None
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestClosedChoices:
|
||||
"""Enum-like settings are Literal types: a typo fails at startup instead of falling through."""
|
||||
@@ -121,7 +136,7 @@ class TestClosedChoices:
|
||||
@pytest.mark.parametrize(
|
||||
("name", "raw", "expected"),
|
||||
[
|
||||
("AUTH_TYPE", " OIDC ", "oidc"),
|
||||
("AUTH_TYPE", " Session_JWT ", "session_jwt"),
|
||||
("VECTOR_STORE", "PGVector", "pgvector"),
|
||||
("STORAGE_TYPE", "S3", "s3"),
|
||||
("URL_STRATEGY", "Backend", "backend"),
|
||||
|
||||
Reference in new issue
Block a user