gateway/modules/serviceCenter/core/serviceSecurity/mainServiceSecurity.py
ValueOn AG 0a0973d41b hotfix msft/google login tokens end to end separated from connection
feat(billing): Nutzerhinweise bei leerem Budget + Mandats-Mail (402/SSE)
Gateway
- InsufficientBalanceException: billingModel, userAction (TOP_UP_SELF /
  CONTACT_MANDATE_ADMIN), DE/EN-Texte, toClientDict(), fromBalanceCheck()
- HTTP 402 + JSON detail für globale API-Fehlerbehandlung
- AI/Chatbot: vor Raise ggf. E-Mail an BillingSettings.notifyEmails
  (PREPAY_MANDATE, Throttle 1h/Mandat) via billingExhaustedNotify
- Agent-Loop & Workspace-Route: SSE-ERROR mit strukturiertem Billing-Payload
- datamodelBilling: notifyEmails-Doku für Pool-Alerts
frontend_nyla
- useWorkspace: SSE onError für INSUFFICIENT_BALANCE mit messageDe/En
  und Hinweis auf Billing-Pfad bei TOP_UP_SELF
2026-03-21 01:34:40 +01:00

91 lines
3.8 KiB
Python

# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
"""
Security service for token management operations.
Core service - not requested by features directly.
"""
import logging
from typing import Optional, Callable, Any
from modules.datamodels.datamodelSecurity import Token, TokenPurpose
from modules.auth import TokenManager
logger = logging.getLogger(__name__)
class SecurityService:
"""Security service providing token management operations."""
def __init__(self, context: Any, get_service: Callable[[str], Any]):
"""Initialize with service center context and resolver."""
self._context = context
self._get_service = get_service
self._tokenManager = TokenManager()
from modules.interfaces.interfaceDbApp import getInterface as getAppInterface
self._interfaceDbApp = getAppInterface(
context.user,
mandateId=context.mandate_id,
)
def getFreshToken(self, connectionId: str, secondsBeforeExpiry: int = 30 * 60) -> Optional[Token]:
"""Get a fresh token for a connection, refreshing when expiring soon."""
try:
token = self._interfaceDbApp.getConnectionToken(connectionId)
if not token:
return None
_tp = (
token.tokenPurpose.value
if isinstance(token.tokenPurpose, TokenPurpose)
else token.tokenPurpose
)
if _tp != TokenPurpose.DATA_CONNECTION.value:
logger.warning(
f"getFreshToken: connection {connectionId} tokenPurpose is {_tp}, expected dataConnection"
)
return None
return self._tokenManager.ensureFreshToken(
token,
secondsBeforeExpiry=secondsBeforeExpiry,
saveCallback=lambda t: self._interfaceDbApp.saveConnectionToken(t)
)
except Exception as e:
logger.error(f"getFreshToken: Error fetching or refreshing token for connection {connectionId}: {e}")
return None
def refreshToken(self, oldToken: Token) -> Optional[Token]:
"""Refresh an expired token using the appropriate OAuth service."""
try:
return self._tokenManager.refreshToken(oldToken)
except Exception as e:
logger.error(f"refreshToken: Error refreshing token: {e}")
return None
def ensureFreshToken(self, token: Token, *, secondsBeforeExpiry: int = 30 * 60,
saveCallback: Optional[Callable[[Token], None]] = None) -> Optional[Token]:
"""Ensure a token is fresh; refresh if expiring within threshold."""
try:
return self._tokenManager.ensureFreshToken(
token,
secondsBeforeExpiry=secondsBeforeExpiry,
saveCallback=saveCallback
)
except Exception as e:
logger.error(f"ensureFreshToken: Error ensuring fresh token: {e}")
return None
def refreshMicrosoftToken(self, refreshToken: str, userId: str, oldToken: Token) -> Optional[Token]:
"""Refresh Microsoft OAuth token using refresh token."""
try:
return self._tokenManager.refreshMicrosoftToken(refreshToken, userId, oldToken)
except Exception as e:
logger.error(f"refreshMicrosoftToken: Error refreshing Microsoft token: {e}")
return None
def refreshGoogleToken(self, refreshToken: str, userId: str, oldToken: Token) -> Optional[Token]:
"""Refresh Google OAuth token using refresh token."""
try:
return self._tokenManager.refreshGoogleToken(refreshToken, userId, oldToken)
except Exception as e:
logger.error(f"refreshGoogleToken: Error refreshing Google token: {e}")
return None