Skip to content

Commit 5d92659

Browse files
committed
Reject unknown fields
We've had an issue where a YAML field was indented wrong and lightspeed stack accepted it. I think it's a good idea to reject unknown fields, to prevent typos from becoming silent misconfigurations.
1 parent 72c0428 commit 5d92659

2 files changed

Lines changed: 37 additions & 18 deletions

File tree

src/models/config.py

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from jsonpath_ng.exceptions import JSONPathError
99
from pydantic import (
1010
BaseModel,
11+
ConfigDict,
1112
Field,
1213
model_validator,
1314
FilePath,
@@ -21,7 +22,13 @@
2122
from utils import checks
2223

2324

24-
class TLSConfiguration(BaseModel):
25+
class ConfigurationBase(BaseModel):
26+
"""Base class for all configuration models that rejects unknown fields."""
27+
28+
model_config = ConfigDict(extra="forbid")
29+
30+
31+
class TLSConfiguration(ConfigurationBase):
2532
"""TLS configuration."""
2633

2734
tls_certificate_path: Optional[FilePath] = None
@@ -34,7 +41,7 @@ def check_tls_configuration(self) -> Self:
3441
return self
3542

3643

37-
class CORSConfiguration(BaseModel):
44+
class CORSConfiguration(ConfigurationBase):
3845
"""CORS configuration."""
3946

4047
allow_origins: list[str] = [
@@ -58,13 +65,13 @@ def check_cors_configuration(self) -> Self:
5865
return self
5966

6067

61-
class SQLiteDatabaseConfiguration(BaseModel):
68+
class SQLiteDatabaseConfiguration(ConfigurationBase):
6269
"""SQLite database configuration."""
6370

6471
db_path: str
6572

6673

67-
class PostgreSQLDatabaseConfiguration(BaseModel):
74+
class PostgreSQLDatabaseConfiguration(ConfigurationBase):
6875
"""PostgreSQL database configuration."""
6976

7077
host: str = "localhost"
@@ -85,7 +92,7 @@ def check_postgres_configuration(self) -> Self:
8592
return self
8693

8794

88-
class DatabaseConfiguration(BaseModel):
95+
class DatabaseConfiguration(ConfigurationBase):
8996
"""Database configuration."""
9097

9198
sqlite: Optional[SQLiteDatabaseConfiguration] = None
@@ -126,7 +133,7 @@ def config(self) -> SQLiteDatabaseConfiguration | PostgreSQLDatabaseConfiguratio
126133
raise ValueError("No database configuration found")
127134

128135

129-
class ServiceConfiguration(BaseModel):
136+
class ServiceConfiguration(ConfigurationBase):
130137
"""Service configuration."""
131138

132139
host: str = "localhost"
@@ -146,15 +153,15 @@ def check_service_configuration(self) -> Self:
146153
return self
147154

148155

149-
class ModelContextProtocolServer(BaseModel):
156+
class ModelContextProtocolServer(ConfigurationBase):
150157
"""model context protocol server configuration."""
151158

152159
name: str
153160
provider_id: str = "model-context-protocol"
154161
url: str
155162

156163

157-
class LlamaStackConfiguration(BaseModel):
164+
class LlamaStackConfiguration(ConfigurationBase):
158165
"""Llama stack configuration."""
159166

160167
url: Optional[str] = None
@@ -200,7 +207,7 @@ def check_llama_stack_model(self) -> Self:
200207
return self
201208

202209

203-
class UserDataCollection(BaseModel):
210+
class UserDataCollection(ConfigurationBase):
204211
"""User data collection configuration."""
205212

206213
feedback_enabled: bool = False
@@ -228,7 +235,7 @@ class JsonPathOperator(str, Enum):
228235
IN = "in"
229236

230237

231-
class JwtRoleRule(BaseModel):
238+
class JwtRoleRule(ConfigurationBase):
232239
"""Rule for extracting roles from JWT claims."""
233240

234241
jsonpath: str # JSONPath expression to evaluate against the JWT payload
@@ -306,22 +313,22 @@ class Action(str, Enum):
306313
INFO = "info"
307314

308315

309-
class AccessRule(BaseModel):
316+
class AccessRule(ConfigurationBase):
310317
"""Rule defining what actions a role can perform."""
311318

312319
role: str # Role name
313320
actions: list[Action] # Allowed actions for this role
314321

315322

316-
class AuthorizationConfiguration(BaseModel):
323+
class AuthorizationConfiguration(ConfigurationBase):
317324
"""Authorization configuration."""
318325

319326
access_rules: list[AccessRule] = Field(
320327
default_factory=list
321328
) # Rules for role-based access control
322329

323330

324-
class JwtConfiguration(BaseModel):
331+
class JwtConfiguration(ConfigurationBase):
325332
"""JWT configuration."""
326333

327334
user_id_claim: str = constants.DEFAULT_JWT_UID_CLAIM
@@ -331,14 +338,14 @@ class JwtConfiguration(BaseModel):
331338
) # Rules for extracting roles from JWT claims
332339

333340

334-
class JwkConfiguration(BaseModel):
341+
class JwkConfiguration(ConfigurationBase):
335342
"""JWK configuration."""
336343

337344
url: AnyHttpUrl
338345
jwt_configuration: JwtConfiguration = JwtConfiguration()
339346

340347

341-
class AuthenticationConfiguration(BaseModel):
348+
class AuthenticationConfiguration(ConfigurationBase):
342349
"""Authentication configuration."""
343350

344351
module: str = constants.DEFAULT_AUTHENTICATION_MODULE
@@ -377,7 +384,7 @@ def jwk_configuration(self) -> JwkConfiguration:
377384
return self.jwk_config
378385

379386

380-
class Customization(BaseModel):
387+
class Customization(ConfigurationBase):
381388
"""Service customization."""
382389

383390
disable_query_system_prompt: bool = False
@@ -395,7 +402,7 @@ def check_customization_model(self) -> Self:
395402
return self
396403

397404

398-
class InferenceConfiguration(BaseModel):
405+
class InferenceConfiguration(ConfigurationBase):
399406
"""Inference configuration."""
400407

401408
default_model: Optional[str] = None
@@ -415,7 +422,7 @@ def check_default_model_and_provider(self) -> Self:
415422
return self
416423

417424

418-
class Configuration(BaseModel):
425+
class Configuration(ConfigurationBase):
419426
"""Global service configuration."""
420427

421428
name: str
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
"""Test configuration validation for unknown fields."""
2+
3+
import pytest
4+
from pydantic import ValidationError
5+
6+
from models.config import ServiceConfiguration
7+
8+
9+
def test_configuration_rejects_unknown_fields():
10+
"""Test that configuration models reject unknown fields."""
11+
with pytest.raises(ValidationError, match="Extra inputs are not permitted"):
12+
ServiceConfiguration(host="localhost", port=8080, unknown_field="should_fail")

0 commit comments

Comments
 (0)