Skip to content

Commit 832bf71

Browse files
committed
Lots of cleanup of secrets in code
Signed-off-by: Jonathan Springer <[email protected]>
1 parent 01ad4db commit 832bf71

File tree

9 files changed

+74
-56
lines changed

9 files changed

+74
-56
lines changed

mcpgateway/config.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@
4949

5050
# Standard
5151
from functools import lru_cache
52-
from importlib.abc import Traversable
5352
from importlib.resources import files
5453
import json # TODO: consider typjson for type safety loading from configuration data.
5554
import logging
@@ -155,8 +154,9 @@ class Settings(BaseSettings):
155154
database_url: str = "sqlite:///./mcp.db"
156155

157156
# Absolute paths resolved at import-time (still override-able via env vars)
158-
templates_dir: Traversable = files("mcpgateway") / "templates"
159-
static_dir: Traversable = files("mcpgateway") / "static"
157+
templates_dir: Path = Field(default_factory=lambda: Path(str(files("mcpgateway") / "templates")))
158+
static_dir: Path = Field(default_factory=lambda: Path(str(files("mcpgateway") / "static")))
159+
160160
app_root_path: str = ""
161161

162162
# Protocol

mcpgateway/main.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,8 @@
104104
from mcpgateway.services.completion_service import CompletionService
105105
from mcpgateway.services.export_service import ExportError, ExportService
106106
from mcpgateway.services.gateway_service import GatewayConnectionError, GatewayError, GatewayNameConflictError, GatewayNotFoundError, GatewayService, GatewayUrlConflictError
107-
from mcpgateway.services.import_service import ConflictStrategy, ImportConflictError
107+
from mcpgateway.services.import_service import ConflictStrategy, ImportConflictError, ImportService, ImportValidationError
108108
from mcpgateway.services.import_service import ImportError as ImportServiceError
109-
from mcpgateway.services.import_service import ImportService, ImportValidationError
110109
from mcpgateway.services.logging_service import LoggingService
111110
from mcpgateway.services.prompt_service import PromptError, PromptNameConflictError, PromptNotFoundError, PromptService
112111
from mcpgateway.services.resource_service import ResourceError, ResourceNotFoundError, ResourceService, ResourceURIConflictError
@@ -1082,9 +1081,10 @@ def require_api_key(api_key: str) -> None:
10821081
10831082
Examples:
10841083
>>> from mcpgateway.config import settings
1084+
>>> from pydantic import SecretStr
10851085
>>> settings.auth_required = True
10861086
>>> settings.basic_auth_user = "admin"
1087-
>>> settings.basic_auth_password = "secret"
1087+
>>> settings.basic_auth_password = SecretStr("secret")
10881088
>>>
10891089
>>> # Valid API key
10901090
>>> require_api_key("admin:secret") # Should not raise
@@ -2694,8 +2694,10 @@ async def read_resource(resource_id: str, request: Request, db: Session = Depend
26942694
# Ensure a plain JSON-serializable structure
26952695
try:
26962696
# First-Party
2697-
from mcpgateway.models import ResourceContent # pylint: disable=import-outside-toplevel
2698-
from mcpgateway.models import TextContent # pylint: disable=import-outside-toplevel
2697+
from mcpgateway.models import (
2698+
ResourceContent, # pylint: disable=import-outside-toplevel
2699+
TextContent, # pylint: disable=import-outside-toplevel
2700+
)
26992701

27002702
# If already a ResourceContent, serialize directly
27012703
if isinstance(content, ResourceContent):

mcpgateway/scripts/validate_env.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ def get_security_warnings(settings: Settings) -> list[str]:
5353
warnings.append(f"PORT: Out of allowed range (1-65535). Got: {settings.port}")
5454

5555
# --- PLATFORM_ADMIN_PASSWORD ---
56-
pw = settings.platform_admin_password
56+
pw = settings.platform_admin_password.get_secret_value() if isinstance(settings.platform_admin_password, SecretStr) else settings.platform_admin_password
57+
5758
if not pw or pw.lower() in ("changeme", "admin", "password"):
5859
warnings.append("Default admin password detected! Please change PLATFORM_ADMIN_PASSWORD immediately.")
5960
min_length = settings.password_min_length
@@ -64,7 +65,7 @@ def get_security_warnings(settings: Settings) -> list[str]:
6465
warnings.append("Admin password has low complexity. Should contain at least 3 of: uppercase, lowercase, digits, special characters")
6566

6667
# --- BASIC_AUTH_PASSWORD ---
67-
basic_pw = settings.basic_auth_password
68+
basic_pw = settings.basic_auth_password.get_secret_value() if isinstance(settings.basic_auth_password, SecretStr) else settings.basic_auth_password
6869
if not basic_pw or basic_pw.lower() in ("changeme", "password"):
6970
warnings.append("Default BASIC_AUTH_PASSWORD detected! Please change it immediately.")
7071
min_length = settings.password_min_length

mcpgateway/utils/verify_credentials.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@
1010
headers and cookies.
1111
Examples:
1212
>>> from mcpgateway.utils import verify_credentials as vc
13+
>>> from pydantic import SecretStr
1314
>>> class DummySettings:
1415
... jwt_secret_key = 'secret'
1516
... jwt_algorithm = 'HS256'
1617
... jwt_audience = 'mcpgateway-api'
1718
... jwt_issuer = 'mcpgateway'
1819
... jwt_audience_verification = True
1920
... basic_auth_user = 'user'
20-
... basic_auth_password = 'pass'
21+
... basic_auth_password = SecretStr('pass')
2122
... auth_required = True
2223
... require_token_expiration = False
2324
... docs_allow_basic_auth = False
@@ -153,14 +154,15 @@ async def verify_credentials(token: str) -> dict:
153154
154155
Examples:
155156
>>> from mcpgateway.utils import verify_credentials as vc
157+
>>> from pydantic import SecretStr
156158
>>> class DummySettings:
157159
... jwt_secret_key = 'secret'
158160
... jwt_algorithm = 'HS256'
159161
... jwt_audience = 'mcpgateway-api'
160162
... jwt_issuer = 'mcpgateway'
161163
... jwt_audience_verification = True
162164
... basic_auth_user = 'user'
163-
... basic_auth_password = 'pass'
165+
... basic_auth_password = SecretStr('pass')
164166
... auth_required = True
165167
... require_token_expiration = False
166168
... docs_allow_basic_auth = False
@@ -202,14 +204,15 @@ async def require_auth(request: Request, credentials: Optional[HTTPAuthorization
202204
203205
Examples:
204206
>>> from mcpgateway.utils import verify_credentials as vc
207+
>>> from pydantic import SecretStr
205208
>>> class DummySettings:
206209
... jwt_secret_key = 'secret'
207210
... jwt_algorithm = 'HS256'
208211
... jwt_audience = 'mcpgateway-api'
209212
... jwt_issuer = 'mcpgateway'
210213
... jwt_audience_verification = True
211214
... basic_auth_user = 'user'
212-
... basic_auth_password = 'pass'
215+
... basic_auth_password = SecretStr('pass')
213216
... auth_required = True
214217
... mcp_client_auth_enabled = True
215218
... trust_proxy_auth = False
@@ -306,14 +309,15 @@ async def verify_basic_credentials(credentials: HTTPBasicCredentials) -> str:
306309
307310
Examples:
308311
>>> from mcpgateway.utils import verify_credentials as vc
312+
>>> from pydantic import SecretStr
309313
>>> class DummySettings:
310314
... jwt_secret_key = 'secret'
311315
... jwt_algorithm = 'HS256'
312316
... jwt_audience = 'mcpgateway-api'
313317
... jwt_issuer = 'mcpgateway'
314318
... jwt_audience_verification = True
315319
... basic_auth_user = 'user'
316-
... basic_auth_password = 'pass'
320+
... basic_auth_password = SecretStr('pass')
317321
... auth_required = True
318322
... docs_allow_basic_auth = False
319323
>>> vc.settings = DummySettings()
@@ -330,7 +334,7 @@ async def verify_basic_credentials(credentials: HTTPBasicCredentials) -> str:
330334
error
331335
"""
332336
is_valid_user = credentials.username == settings.basic_auth_user
333-
is_valid_pass = credentials.password == settings.basic_auth_password
337+
is_valid_pass = credentials.password == settings.basic_auth_password.get_secret_value()
334338

335339
if not (is_valid_user and is_valid_pass):
336340
raise HTTPException(
@@ -359,14 +363,15 @@ async def require_basic_auth(credentials: HTTPBasicCredentials = Depends(basic_s
359363
360364
Examples:
361365
>>> from mcpgateway.utils import verify_credentials as vc
366+
>>> from pydantic import SecretStr
362367
>>> class DummySettings:
363368
... jwt_secret_key = 'secret'
364369
... jwt_algorithm = 'HS256'
365370
... jwt_audience = 'mcpgateway-api'
366371
... jwt_issuer = 'mcpgateway'
367372
... jwt_audience_verification = True
368373
... basic_auth_user = 'user'
369-
... basic_auth_password = 'pass'
374+
... basic_auth_password = SecretStr('pass')
370375
... auth_required = True
371376
... docs_allow_basic_auth = False
372377
>>> vc.settings = DummySettings()
@@ -420,14 +425,15 @@ async def require_docs_basic_auth(auth_header: str) -> str:
420425
421426
Examples:
422427
>>> from mcpgateway.utils import verify_credentials as vc
428+
>>> from pydantic import SecretStr
423429
>>> class DummySettings:
424430
... jwt_secret_key = 'secret'
425431
... jwt_algorithm = 'HS256'
426432
... jwt_audience = 'mcpgateway-api'
427433
... jwt_issuer = 'mcpgateway'
428434
... jwt_audience_verification = True
429435
... basic_auth_user = 'user'
430-
... basic_auth_password = 'pass'
436+
... basic_auth_password = SecretStr('pass')
431437
... auth_required = True
432438
... require_token_expiration = False
433439
... docs_allow_basic_auth = True
@@ -633,14 +639,15 @@ async def require_auth_override(
633639
634640
Examples:
635641
>>> from mcpgateway.utils import verify_credentials as vc
642+
>>> from pydantic import SecretStr
636643
>>> class DummySettings:
637644
... jwt_secret_key = 'secret'
638645
... jwt_algorithm = 'HS256'
639646
... jwt_audience = 'mcpgateway-api'
640647
... jwt_issuer = 'mcpgateway'
641648
... jwt_audience_verification = True
642649
... basic_auth_user = 'user'
643-
... basic_auth_password = 'pass'
650+
... basic_auth_password = SecretStr('pass')
644651
... auth_required = True
645652
... mcp_client_auth_enabled = True
646653
... trust_proxy_auth = False

tests/unit/mcpgateway/test_bootstrap_db.py

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from unittest.mock import AsyncMock, MagicMock, Mock, patch
1313

1414
# Third-Party
15+
from pydantic import SecretStr
1516
import pytest
1617

1718
# First-Party
@@ -30,7 +31,7 @@ def mock_settings():
3031
settings = Mock()
3132
settings.email_auth_enabled = True
3233
settings.platform_admin_email = "[email protected]"
33-
settings.platform_admin_password = "secure_password"
34+
settings.platform_admin_password = SecretStr("secure_password")
3435
settings.platform_admin_full_name = "Platform Admin"
3536
settings.auto_create_personal_teams = True
3637
settings.database_url = "sqlite:///:memory:"
@@ -124,20 +125,25 @@ async def test_bootstrap_admin_user_success(self, mock_settings, mock_db_session
124125
mock_email_auth_service.get_user_by_email.return_value = None
125126
mock_email_auth_service.create_user.return_value = mock_admin_user
126127

127-
with patch("mcpgateway.bootstrap_db.settings", mock_settings):
128-
with patch("mcpgateway.bootstrap_db.SessionLocal", return_value=mock_db_session):
129-
with patch("mcpgateway.services.email_auth_service.EmailAuthService", return_value=mock_email_auth_service):
130-
with patch("mcpgateway.db.utc_now") as mock_utc_now:
131-
mock_utc_now.return_value = "2024-01-01T00:00:00Z"
132-
with patch("mcpgateway.bootstrap_db.logger") as mock_logger:
133-
await bootstrap_admin_user()
134-
135-
mock_email_auth_service.create_user.assert_called_once_with(
136-
email=mock_settings.platform_admin_email, password=mock_settings.platform_admin_password, full_name=mock_settings.platform_admin_full_name, is_admin=True
137-
)
138-
assert mock_admin_user.email_verified_at == "2024-01-01T00:00:00Z"
139-
assert mock_db_session.commit.call_count == 2
140-
mock_logger.info.assert_any_call(f"Platform admin user created successfully: {mock_settings.platform_admin_email}")
128+
with (
129+
patch("mcpgateway.bootstrap_db.settings", mock_settings),
130+
patch("mcpgateway.bootstrap_db.SessionLocal", return_value=mock_db_session),
131+
patch("mcpgateway.services.email_auth_service.EmailAuthService", return_value=mock_email_auth_service),
132+
patch("mcpgateway.db.utc_now") as mock_utc_now,
133+
patch("mcpgateway.bootstrap_db.logger") as mock_logger,
134+
):
135+
mock_utc_now.return_value = "2024-01-01T00:00:00Z"
136+
await bootstrap_admin_user()
137+
138+
mock_email_auth_service.create_user.assert_called_once_with(
139+
email=mock_settings.platform_admin_email,
140+
password=mock_settings.platform_admin_password.get_secret_value(),
141+
full_name=mock_settings.platform_admin_full_name,
142+
is_admin=True,
143+
)
144+
assert mock_admin_user.email_verified_at == "2024-01-01T00:00:00Z"
145+
assert mock_db_session.commit.call_count == 2
146+
mock_logger.info.assert_any_call(f"Platform admin user created successfully: {mock_settings.platform_admin_email}")
141147

142148
@pytest.mark.asyncio
143149
async def test_bootstrap_admin_user_with_personal_team(self, mock_settings, mock_db_session, mock_email_auth_service, mock_admin_user):

tests/unit/mcpgateway/test_config.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,11 @@
1111
# Standard
1212
import os
1313
from pathlib import Path
14-
from typing import Any, Dict, List
1514
from unittest.mock import MagicMock, patch
1615

17-
# Third-Party
18-
from fastapi import HTTPException
16+
from pydantic import SecretStr
1917

18+
# Third-Party
2019
# Third-party
2120
import pytest
2221

@@ -53,7 +52,7 @@ def test_parse_federation_peers_json_and_csv():
5352
# --------------------------------------------------------------------------- #
5453
# database / CORS helpers #
5554
# --------------------------------------------------------------------------- #
56-
def test_database_settings_sqlite_and_non_sqlite(tmp_path: Path):
55+
def test_database_settings_sqlite_and_non_sqlite(tmp_path: Path) -> None:
5756
"""connect_args differs for sqlite vs everything else."""
5857
# sqlite -> check_same_thread flag present
5958
db_file = tmp_path / "foo" / "bar.db"
@@ -66,7 +65,7 @@ def test_database_settings_sqlite_and_non_sqlite(tmp_path: Path):
6665
assert s_pg.database_settings["connect_args"] == {}
6766

6867

69-
def test_validate_database_creates_missing_parent(tmp_path: Path):
68+
def test_validate_database_creates_missing_parent(tmp_path: Path) -> None:
7069
db_file = tmp_path / "newdir" / "db.sqlite"
7170
url = f"sqlite:///{db_file}"
7271
s = Settings(database_url=url, _env_file=None)
@@ -139,7 +138,7 @@ def test_settings_default_values():
139138
assert settings.port == 4444
140139
assert settings.database_url == "sqlite:///./mcp.db"
141140
assert settings.basic_auth_user == "admin"
142-
assert settings.basic_auth_password == "changeme"
141+
assert settings.basic_auth_password == SecretStr("changeme")
143142
assert settings.auth_required is True
144143
assert settings.jwt_secret_key.get_secret_value() == "x" * 32
145144
assert settings.auth_encryption_secret.get_secret_value() == "dummy-secret"

tests/unit/mcpgateway/test_coverage_push.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# Third-Party
1414
from fastapi import HTTPException
1515
from fastapi.testclient import TestClient
16+
from pydantic import SecretStr
1617
import pytest
1718

1819
# First-Party
@@ -36,14 +37,14 @@ def test_require_api_key_scenarios():
3637
with patch("mcpgateway.main.settings") as mock_settings:
3738
mock_settings.auth_required = True
3839
mock_settings.basic_auth_user = "admin"
39-
mock_settings.basic_auth_password = "secret"
40+
mock_settings.basic_auth_password = SecretStr("secret")
4041
require_api_key("admin:secret") # Should not raise
4142

4243
# Test with auth enabled and incorrect key
4344
with patch("mcpgateway.main.settings") as mock_settings:
4445
mock_settings.auth_required = True
4546
mock_settings.basic_auth_user = "admin"
46-
mock_settings.basic_auth_password = "secret"
47+
mock_settings.basic_auth_password = SecretStr("secret")
4748

4849
with pytest.raises(HTTPException):
4950
require_api_key("wrong:key")

tests/unit/mcpgateway/test_validate_env.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
11
# -*- coding: utf-8 -*-
22
# File: tests/unit/mcpgateway/test_validate_env.py
3-
from pathlib import Path
4-
import pytest
53
import logging
64
import os
5+
from pathlib import Path
76
from unittest.mock import patch
87

9-
# Suppress mcpgateway.config logs during tests
10-
logging.getLogger("mcpgateway.config").setLevel(logging.ERROR)
8+
import pytest
119

1210
# Import the validate_env script directly
1311
from mcpgateway.scripts import validate_env as ve
1412

13+
# Suppress mcpgateway.config logs during tests
14+
logging.getLogger("mcpgateway.config").setLevel(logging.ERROR)
15+
1516

1617
@pytest.fixture
17-
def valid_env(tmp_path: Path):
18+
def valid_env(tmp_path: Path) -> Path:
1819
envfile = tmp_path / ".env"
1920
envfile.write_text(
2021
"APP_DOMAIN=http://localhost:8000\n"
@@ -30,14 +31,14 @@ def valid_env(tmp_path: Path):
3031

3132

3233
@pytest.fixture
33-
def invalid_env(tmp_path: Path):
34+
def invalid_env(tmp_path: Path) -> Path:
3435
envfile = tmp_path / ".env"
3536
# Invalid URL + wrong log level + invalid port
3637
envfile.write_text("APP_DOMAIN=not-a-url\nPORT=-1\nLOG_LEVEL=wronglevel\n")
3738
return envfile
3839

3940

40-
def test_validate_env_success_direct(valid_env: Path):
41+
def test_validate_env_success_direct(valid_env: Path) -> None:
4142
"""
4243
Test a valid .env. Warnings will be printed but do NOT fail the test.
4344
"""
@@ -57,7 +58,7 @@ def test_validate_env_success_direct(valid_env: Path):
5758
assert code == 0
5859

5960

60-
def test_validate_env_failure_direct(invalid_env: Path):
61+
def test_validate_env_failure_direct(invalid_env: Path) -> None:
6162
"""
6263
Test an invalid .env. Should fail due to ValidationError.
6364
"""

0 commit comments

Comments
 (0)