mirror of
https://github.com/jmagar/unraid-mcp.git
synced 2026-03-23 12:39:24 -07:00
feat(auth): add Google OAuth settings with is_google_auth_configured()
Add GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, UNRAID_MCP_BASE_URL, and UNRAID_MCP_JWT_SIGNING_KEY env vars to settings.py, along with the is_google_auth_configured() predicate and three new keys in get_config_summary(). TDD: 4 tests written red-first, all passing green.
This commit is contained in:
29
.env.example
29
.env.example
@@ -34,4 +34,31 @@ UNRAID_MAX_RECONNECT_ATTEMPTS=10
|
|||||||
|
|
||||||
# Optional: Custom log file path for subscription auto-start diagnostics
|
# Optional: Custom log file path for subscription auto-start diagnostics
|
||||||
# Defaults to standard log if not specified
|
# Defaults to standard log if not specified
|
||||||
# UNRAID_AUTOSTART_LOG_PATH=/custom/path/to/autostart.log
|
# UNRAID_AUTOSTART_LOG_PATH=/custom/path/to/autostart.log
|
||||||
|
|
||||||
|
# Google OAuth Protection (Optional)
|
||||||
|
# -----------------------------------
|
||||||
|
# Protects the MCP HTTP server — clients must authenticate with Google before calling tools.
|
||||||
|
# Requires streamable-http or sse transport (not stdio).
|
||||||
|
#
|
||||||
|
# Setup:
|
||||||
|
# 1. Google Cloud Console → APIs & Services → Credentials
|
||||||
|
# 2. Create OAuth 2.0 Client ID (Web application)
|
||||||
|
# 3. Authorized redirect URIs: <UNRAID_MCP_BASE_URL>/auth/callback
|
||||||
|
# 4. Copy Client ID and Client Secret below
|
||||||
|
#
|
||||||
|
# UNRAID_MCP_BASE_URL: Public URL clients use to reach THIS server (for redirect URIs).
|
||||||
|
# Examples:
|
||||||
|
# http://10.1.0.2:6970 (LAN)
|
||||||
|
# http://100.x.x.x:6970 (Tailscale)
|
||||||
|
# https://mcp.yourdomain.com (reverse proxy)
|
||||||
|
#
|
||||||
|
# UNRAID_MCP_JWT_SIGNING_KEY: Stable secret for signing FastMCP JWT tokens.
|
||||||
|
# Generate once: python3 -c "import secrets; print(secrets.token_hex(32))"
|
||||||
|
# NEVER change after first use — all client sessions will be invalidated.
|
||||||
|
#
|
||||||
|
# Leave GOOGLE_CLIENT_ID empty to disable OAuth (server runs unprotected).
|
||||||
|
# GOOGLE_CLIENT_ID=
|
||||||
|
# GOOGLE_CLIENT_SECRET=
|
||||||
|
# UNRAID_MCP_BASE_URL=http://10.1.0.2:6970
|
||||||
|
# UNRAID_MCP_JWT_SIGNING_KEY=<generate with command above>
|
||||||
84
tests/test_auth_settings.py
Normal file
84
tests/test_auth_settings.py
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
"""Tests for Google OAuth settings loading."""
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
|
||||||
|
|
||||||
|
def _reload_settings(monkeypatch, overrides: dict) -> object:
|
||||||
|
"""Reload settings module with given env vars set."""
|
||||||
|
for k, v in overrides.items():
|
||||||
|
monkeypatch.setenv(k, v)
|
||||||
|
import unraid_mcp.config.settings as mod
|
||||||
|
|
||||||
|
importlib.reload(mod)
|
||||||
|
return mod
|
||||||
|
|
||||||
|
|
||||||
|
def test_google_auth_defaults_to_empty(monkeypatch):
|
||||||
|
"""Google auth vars default to empty string when not set."""
|
||||||
|
monkeypatch.delenv("GOOGLE_CLIENT_ID", raising=False)
|
||||||
|
monkeypatch.delenv("GOOGLE_CLIENT_SECRET", raising=False)
|
||||||
|
monkeypatch.delenv("UNRAID_MCP_BASE_URL", raising=False)
|
||||||
|
monkeypatch.delenv("UNRAID_MCP_JWT_SIGNING_KEY", raising=False)
|
||||||
|
mod = _reload_settings(monkeypatch, {})
|
||||||
|
assert mod.GOOGLE_CLIENT_ID == ""
|
||||||
|
assert mod.GOOGLE_CLIENT_SECRET == ""
|
||||||
|
assert mod.UNRAID_MCP_BASE_URL == ""
|
||||||
|
assert mod.UNRAID_MCP_JWT_SIGNING_KEY == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_google_auth_reads_env_vars(monkeypatch):
|
||||||
|
"""Google auth vars are read from environment."""
|
||||||
|
mod = _reload_settings(
|
||||||
|
monkeypatch,
|
||||||
|
{
|
||||||
|
"GOOGLE_CLIENT_ID": "test-client-id.apps.googleusercontent.com",
|
||||||
|
"GOOGLE_CLIENT_SECRET": "GOCSPX-test-secret",
|
||||||
|
"UNRAID_MCP_BASE_URL": "http://10.1.0.2:6970",
|
||||||
|
"UNRAID_MCP_JWT_SIGNING_KEY": "a" * 32,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert mod.GOOGLE_CLIENT_ID == "test-client-id.apps.googleusercontent.com"
|
||||||
|
assert mod.GOOGLE_CLIENT_SECRET == "GOCSPX-test-secret"
|
||||||
|
assert mod.UNRAID_MCP_BASE_URL == "http://10.1.0.2:6970"
|
||||||
|
assert mod.UNRAID_MCP_JWT_SIGNING_KEY == "a" * 32
|
||||||
|
|
||||||
|
|
||||||
|
def test_google_auth_enabled_requires_both_vars(monkeypatch):
|
||||||
|
"""is_google_auth_configured() requires both client_id and client_secret."""
|
||||||
|
# Only client_id — not configured
|
||||||
|
mod = _reload_settings(
|
||||||
|
monkeypatch,
|
||||||
|
{
|
||||||
|
"GOOGLE_CLIENT_ID": "test-id",
|
||||||
|
"GOOGLE_CLIENT_SECRET": "",
|
||||||
|
"UNRAID_MCP_BASE_URL": "http://10.1.0.2:6970",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
monkeypatch.delenv("GOOGLE_CLIENT_SECRET", raising=False)
|
||||||
|
importlib.reload(mod)
|
||||||
|
assert not mod.is_google_auth_configured()
|
||||||
|
|
||||||
|
# Both set — configured
|
||||||
|
mod2 = _reload_settings(
|
||||||
|
monkeypatch,
|
||||||
|
{
|
||||||
|
"GOOGLE_CLIENT_ID": "test-id",
|
||||||
|
"GOOGLE_CLIENT_SECRET": "test-secret",
|
||||||
|
"UNRAID_MCP_BASE_URL": "http://10.1.0.2:6970",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert mod2.is_google_auth_configured()
|
||||||
|
|
||||||
|
|
||||||
|
def test_google_auth_requires_base_url(monkeypatch):
|
||||||
|
"""is_google_auth_configured() is False when base_url is missing."""
|
||||||
|
mod = _reload_settings(
|
||||||
|
monkeypatch,
|
||||||
|
{
|
||||||
|
"GOOGLE_CLIENT_ID": "test-id",
|
||||||
|
"GOOGLE_CLIENT_SECRET": "test-secret",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
monkeypatch.delenv("UNRAID_MCP_BASE_URL", raising=False)
|
||||||
|
importlib.reload(mod)
|
||||||
|
assert not mod.is_google_auth_configured()
|
||||||
@@ -76,6 +76,28 @@ elif raw_verify_ssl in ["true", "1", "yes"]:
|
|||||||
else: # Path to CA bundle
|
else: # Path to CA bundle
|
||||||
UNRAID_VERIFY_SSL = raw_verify_ssl
|
UNRAID_VERIFY_SSL = raw_verify_ssl
|
||||||
|
|
||||||
|
# Google OAuth Configuration (Optional)
|
||||||
|
# -------------------------------------
|
||||||
|
# When set, the MCP HTTP server requires Google login before tool calls.
|
||||||
|
# UNRAID_MCP_BASE_URL must match the public URL clients use to reach this server.
|
||||||
|
# Google Cloud Console → Credentials → Authorized redirect URIs:
|
||||||
|
# Add: <UNRAID_MCP_BASE_URL>/auth/callback
|
||||||
|
GOOGLE_CLIENT_ID = os.getenv("GOOGLE_CLIENT_ID", "")
|
||||||
|
GOOGLE_CLIENT_SECRET = os.getenv("GOOGLE_CLIENT_SECRET", "")
|
||||||
|
UNRAID_MCP_BASE_URL = os.getenv("UNRAID_MCP_BASE_URL", "")
|
||||||
|
|
||||||
|
# JWT signing key for FastMCP OAuth tokens.
|
||||||
|
# MUST be set to a stable secret so tokens survive server restarts.
|
||||||
|
# Generate once: python3 -c "import secrets; print(secrets.token_hex(32))"
|
||||||
|
# Never change this value — all existing tokens will be invalidated.
|
||||||
|
UNRAID_MCP_JWT_SIGNING_KEY = os.getenv("UNRAID_MCP_JWT_SIGNING_KEY", "")
|
||||||
|
|
||||||
|
|
||||||
|
def is_google_auth_configured() -> bool:
|
||||||
|
"""Return True when all required Google OAuth vars are present."""
|
||||||
|
return bool(GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET and UNRAID_MCP_BASE_URL)
|
||||||
|
|
||||||
|
|
||||||
# Logging Configuration
|
# Logging Configuration
|
||||||
LOG_LEVEL_STR = os.getenv("UNRAID_MCP_LOG_LEVEL", "INFO").upper()
|
LOG_LEVEL_STR = os.getenv("UNRAID_MCP_LOG_LEVEL", "INFO").upper()
|
||||||
LOG_FILE_NAME = os.getenv("UNRAID_MCP_LOG_FILE", "unraid-mcp.log")
|
LOG_FILE_NAME = os.getenv("UNRAID_MCP_LOG_FILE", "unraid-mcp.log")
|
||||||
@@ -155,6 +177,9 @@ def get_config_summary() -> dict[str, Any]:
|
|||||||
"log_file": str(LOG_FILE_PATH),
|
"log_file": str(LOG_FILE_PATH),
|
||||||
"config_valid": is_valid,
|
"config_valid": is_valid,
|
||||||
"missing_config": missing if not is_valid else None,
|
"missing_config": missing if not is_valid else None,
|
||||||
|
"google_auth_enabled": is_google_auth_configured(),
|
||||||
|
"google_auth_base_url": UNRAID_MCP_BASE_URL if is_google_auth_configured() else None,
|
||||||
|
"jwt_signing_key_configured": bool(UNRAID_MCP_JWT_SIGNING_KEY),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user