build: Add type checking
This commit is contained in:
@@ -34,7 +34,7 @@ class ProgressiveConsentTokenVerifier:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
token_storage: RefreshTokenStorage,
|
||||
token_storage: RefreshTokenStorage | None,
|
||||
token_broker: Optional[TokenBrokerService] = None,
|
||||
oidc_discovery_url: Optional[str] = None,
|
||||
nextcloud_host: Optional[str] = None,
|
||||
@@ -80,7 +80,7 @@ class ProgressiveConsentTokenVerifier:
|
||||
# Create token broker if not provided
|
||||
if token_broker:
|
||||
self.token_broker = token_broker
|
||||
elif self.encryption_key:
|
||||
elif self.encryption_key and token_storage and self.nextcloud_host:
|
||||
self.token_broker = TokenBrokerService(
|
||||
storage=token_storage,
|
||||
oidc_discovery_url=self.oidc_discovery_url,
|
||||
@@ -89,7 +89,12 @@ class ProgressiveConsentTokenVerifier:
|
||||
)
|
||||
else:
|
||||
self.token_broker = None
|
||||
logger.warning("Token broker not available - encryption key missing")
|
||||
if not self.encryption_key:
|
||||
logger.warning("Token broker not available - encryption key missing")
|
||||
elif not token_storage:
|
||||
logger.warning("Token broker not available - token storage missing")
|
||||
elif not self.nextcloud_host:
|
||||
logger.warning("Token broker not available - nextcloud host missing")
|
||||
|
||||
async def verify_token(self, token: str) -> Optional[AccessToken]:
|
||||
"""
|
||||
|
||||
@@ -25,7 +25,7 @@ import logging
|
||||
import os
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from typing import Any, Optional
|
||||
|
||||
import aiosqlite
|
||||
from cryptography.fernet import Fernet
|
||||
@@ -283,7 +283,7 @@ class RefreshTokenStorage:
|
||||
)
|
||||
|
||||
async def store_user_profile(
|
||||
self, user_id: str, profile_data: dict[str, any]
|
||||
self, user_id: str, profile_data: dict[str, Any]
|
||||
) -> None:
|
||||
"""
|
||||
Store user profile data (cached from IdP userinfo endpoint).
|
||||
@@ -314,7 +314,7 @@ class RefreshTokenStorage:
|
||||
|
||||
logger.debug(f"Cached user profile for {user_id}")
|
||||
|
||||
async def get_user_profile(self, user_id: str) -> Optional[dict[str, any]]:
|
||||
async def get_user_profile(self, user_id: str) -> Optional[dict[str, Any]]:
|
||||
"""
|
||||
Retrieve cached user profile data.
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import logging
|
||||
import os
|
||||
from functools import wraps
|
||||
from typing import Callable
|
||||
from typing import Any, Callable
|
||||
|
||||
from mcp.server.auth.middleware.auth_context import get_access_token
|
||||
from mcp.server.auth.provider import AccessToken
|
||||
@@ -88,15 +88,18 @@ def require_scopes(*required_scopes: str):
|
||||
ScopeAuthorizationError: If required scopes are not present in the access token
|
||||
"""
|
||||
|
||||
def decorator(func: Callable):
|
||||
def decorator(func: Callable) -> Callable:
|
||||
# Store scope requirements as function metadata for dynamic filtering
|
||||
func._required_scopes = list(required_scopes) # type: ignore
|
||||
func._required_scopes = list(required_scopes) # type: ignore[attr-defined]
|
||||
|
||||
# Get function name for logging (works for any callable)
|
||||
func_name = getattr(func, "__name__", repr(func))
|
||||
|
||||
# Find which parameter receives the Context (FastMCP injects it by name)
|
||||
context_param_name = find_context_parameter(func)
|
||||
|
||||
@wraps(func)
|
||||
async def wrapper(*args, **kwargs):
|
||||
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
||||
# Extract context from kwargs (where FastMCP injected it)
|
||||
ctx: Context | None = (
|
||||
kwargs.get(context_param_name) if context_param_name else None
|
||||
@@ -106,7 +109,7 @@ def require_scopes(*required_scopes: str):
|
||||
# No context parameter found - likely BasicAuth mode
|
||||
# In BasicAuth mode, all operations are allowed
|
||||
logger.debug(
|
||||
f"No context parameter for {func.__name__} - allowing (BasicAuth mode)"
|
||||
f"No context parameter for {func_name} - allowing (BasicAuth mode)"
|
||||
)
|
||||
return await func(*args, **kwargs)
|
||||
|
||||
@@ -119,7 +122,7 @@ def require_scopes(*required_scopes: str):
|
||||
# Not in OAuth mode (BasicAuth or no auth)
|
||||
# In BasicAuth mode, all operations are allowed
|
||||
logger.debug(
|
||||
f"No access token present for {func.__name__} - allowing (BasicAuth mode)"
|
||||
f"No access token present for {func_name} - allowing (BasicAuth mode)"
|
||||
)
|
||||
return await func(*args, **kwargs)
|
||||
|
||||
@@ -172,7 +175,7 @@ def require_scopes(*required_scopes: str):
|
||||
|
||||
if not has_nextcloud_scopes:
|
||||
error_msg = (
|
||||
f"Access denied to {func.__name__}: "
|
||||
f"Access denied to {func_name}: "
|
||||
f"Nextcloud resource access not provisioned. "
|
||||
f"Please run the 'provision_nextcloud_access' tool first."
|
||||
)
|
||||
@@ -183,7 +186,7 @@ def require_scopes(*required_scopes: str):
|
||||
missing_scopes = required_scopes_set - token_scopes
|
||||
if missing_scopes:
|
||||
error_msg = (
|
||||
f"Access denied to {func.__name__}: "
|
||||
f"Access denied to {func_name}: "
|
||||
f"Missing required scopes: {', '.join(sorted(missing_scopes))}. "
|
||||
f"Token has scopes: {', '.join(sorted(token_scopes)) if token_scopes else 'none'}"
|
||||
)
|
||||
@@ -192,7 +195,7 @@ def require_scopes(*required_scopes: str):
|
||||
|
||||
# All required scopes present - allow execution
|
||||
logger.debug(
|
||||
f"Scope authorization passed for {func.__name__}: {required_scopes}"
|
||||
f"Scope authorization passed for {func_name}: {required_scopes}"
|
||||
)
|
||||
return await func(*args, **kwargs)
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ class TokenCache:
|
||||
logger.debug(f"Using cached token for user {user_id}")
|
||||
return token
|
||||
|
||||
async def set(self, user_id: str, token: str, expires_in: int = None):
|
||||
async def set(self, user_id: str, token: str, expires_in: int | None = None):
|
||||
"""Store token in cache."""
|
||||
async with self._lock:
|
||||
# Use provided expiry or default TTL
|
||||
|
||||
@@ -114,7 +114,8 @@ class TokenExchangeService:
|
||||
if not self.oidc_discovery_url:
|
||||
# Fallback to Nextcloud OIDC if no discovery URL
|
||||
self.oidc_discovery_url = urljoin(
|
||||
self.nextcloud_host, "/.well-known/openid-configuration"
|
||||
self.nextcloud_host, # type: ignore[arg-type]
|
||||
"/.well-known/openid-configuration",
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -363,6 +364,7 @@ class TokenExchangeService:
|
||||
True if provisioned, False otherwise
|
||||
"""
|
||||
await self._ensure_storage()
|
||||
assert self.storage is not None # _ensure_storage() ensures this
|
||||
token_data = await self.storage.get_refresh_token(user_id)
|
||||
return token_data is not None
|
||||
|
||||
@@ -376,6 +378,7 @@ class TokenExchangeService:
|
||||
Refresh token if found, None otherwise
|
||||
"""
|
||||
await self._ensure_storage()
|
||||
assert self.storage is not None # _ensure_storage() ensures this
|
||||
token_data = await self.storage.get_refresh_token(user_id)
|
||||
if token_data:
|
||||
return token_data.get("refresh_token")
|
||||
|
||||
@@ -165,6 +165,7 @@ class NextcloudTokenVerifier(TokenVerifier):
|
||||
"""
|
||||
try:
|
||||
# Get signing key from JWKS
|
||||
assert self._jwks_client is not None # Caller should check before calling
|
||||
signing_key = self._jwks_client.get_signing_key_from_jwt(token)
|
||||
|
||||
# Verify and decode JWT
|
||||
@@ -257,7 +258,7 @@ class NextcloudTokenVerifier(TokenVerifier):
|
||||
try:
|
||||
# Introspection requires client authentication
|
||||
response = await self._client.post(
|
||||
self.introspection_uri,
|
||||
self.introspection_uri, # type: ignore
|
||||
data={"token": token},
|
||||
auth=(self.client_id, self.client_secret),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user