Files
VPNTray/services/connection_manager.py
2025-09-06 16:54:45 +02:00

267 lines
9.5 KiB
Python

"""High-level connection manager that integrates VPN and Passbolt."""
import logging
from typing import Optional, Dict, Any
from pathlib import Path
from dataclasses import dataclass
from .vpn_manager import VPNManager, VPNStatus, VPNConnectionError
from .passbolt_client import PassboltClient, PassboltError
logger = logging.getLogger(__name__)
@dataclass
class ConnectionConfig:
"""Configuration for a VPN connection."""
name: str
vpn_config_path: str
nmcli_connection_name: Optional[str] = None
auto_import: bool = True # Auto-import .ovpn if not in NetworkManager
# Credentials can be:
# - Passbolt UUID string (for future implementation)
# - Dict with 'username' and 'password' keys
# - None if no credentials needed
vpn_credentials: Optional[dict | str] = None
class ConnectionManager:
"""Manages VPN connections with Passbolt credential integration."""
def __init__(self, use_passbolt: bool = True):
"""Initialize the connection manager.
Args:
use_passbolt: Whether to use Passbolt for credentials
"""
self.vpn_manager = VPNManager()
self.passbolt_client = None
if use_passbolt:
try:
self.passbolt_client = PassboltClient()
logger.info("Passbolt client initialized successfully")
except PassboltError as e:
logger.warning(f"Passbolt not available: {e}")
logger.info("Falling back to manual credential entry")
def connect_location(self, config: ConnectionConfig,
username: Optional[str] = None,
password: Optional[str] = None) -> None:
"""Connect to a VPN location.
Args:
config: Connection configuration
username: Override username (if not using Passbolt)
password: Override password (if not using Passbolt)
"""
# Ensure connection exists in NetworkManager
connection_name = self._ensure_connection(config)
# Get credentials - check overrides first, then config
if not username or not password:
creds_username, creds_password = self._get_credentials_from_config(config)
username = username or creds_username
password = password or creds_password
if not username or not password:
logger.info(f"No credentials provided for {connection_name}")
# nmcli will prompt for credentials
# Connect
try:
logger.info(f"Connecting to {connection_name}")
self.vpn_manager.connect(connection_name, username, password)
logger.info(f"Successfully connected to {connection_name}")
except VPNConnectionError as e:
logger.error(f"Failed to connect to {connection_name}: {e}")
raise
def disconnect_location(self, config: ConnectionConfig) -> None:
"""Disconnect from a VPN location.
Args:
config: Connection configuration
"""
connection_name = config.nmcli_connection_name or config.name
if not self.vpn_manager.connection_exists(connection_name):
logger.warning(f"Connection {connection_name} does not exist")
return
try:
logger.info(f"Disconnecting from {connection_name}")
self.vpn_manager.disconnect(connection_name)
logger.info(f"Successfully disconnected from {connection_name}")
except VPNConnectionError as e:
logger.error(f"Failed to disconnect from {connection_name}: {e}")
raise
def get_connection_status(self, config: ConnectionConfig) -> VPNStatus:
"""Get the status of a VPN connection.
Args:
config: Connection configuration
Returns:
Current VPN status
"""
connection_name = config.nmcli_connection_name or config.name
if not self.vpn_manager.connection_exists(connection_name):
return VPNStatus.DISCONNECTED
return self.vpn_manager.get_status(connection_name)
def _ensure_connection(self, config: ConnectionConfig) -> str:
"""Ensure VPN connection exists in NetworkManager.
Args:
config: Connection configuration
Returns:
Name of the NetworkManager connection
"""
connection_name = config.nmcli_connection_name or config.name
# Check if connection already exists
if self.vpn_manager.connection_exists(connection_name):
logger.debug(f"Connection {connection_name} already exists")
return connection_name
# Import if auto_import is enabled and config file exists
if config.auto_import and config.vpn_config_path:
vpn_file = Path(config.vpn_config_path)
if vpn_file.exists():
logger.info(f"Importing VPN configuration from {vpn_file}")
imported_name = self.vpn_manager.import_ovpn(
str(vpn_file),
connection_name
)
logger.info(f"Imported connection as {imported_name}")
return imported_name
else:
raise VPNConnectionError(
f"VPN config file not found: {config.vpn_config_path}"
)
raise VPNConnectionError(
f"Connection {connection_name} does not exist and auto-import is disabled"
)
def _get_credentials_from_config(self, config: ConnectionConfig) -> tuple[Optional[str], Optional[str]]:
"""Get credentials from the configuration.
Args:
config: Connection configuration
Returns:
Tuple of (username, password) or (None, None)
"""
if not config.vpn_credentials:
return None, None
# If it's a dict with username/password
if isinstance(config.vpn_credentials, dict):
username = config.vpn_credentials.get('username')
password = config.vpn_credentials.get('password')
return username, password
# If it's a string (Passbolt UUID for future use)
if isinstance(config.vpn_credentials, str):
# For now, try to use Passbolt if available
if self.passbolt_client:
try:
return self._get_passbolt_credentials(config.vpn_credentials)
except (PassboltError, ValueError) as e:
logger.warning(f"Failed to get Passbolt credentials: {e}")
else:
logger.warning(f"Passbolt UUID provided but Passbolt client not available")
return None, None
def _get_passbolt_credentials(self, resource_id: str) -> tuple[str, str]:
"""Get credentials from Passbolt.
Args:
resource_id: Passbolt resource UUID
Returns:
Tuple of (username, password)
"""
if not self.passbolt_client:
raise ValueError("Passbolt client not initialized")
try:
credential = self.passbolt_client.get_credential(resource_id)
if not credential.username or not credential.password:
raise ValueError(
f"Incomplete credentials for resource {resource_id}")
return credential.username, credential.password
except PassboltError as e:
logger.error(f"Failed to get Passbolt credentials: {e}")
raise
def validate_passbolt_resource(self, resource_id: str) -> bool:
"""Validate that a Passbolt resource exists and has required fields.
Args:
resource_id: Passbolt resource UUID
Returns:
True if resource is valid for VPN use
"""
if not self.passbolt_client:
return False
try:
credential = self.passbolt_client.get_credential(resource_id)
return bool(credential.username and credential.password)
except PassboltError:
return False
def import_all_configs(self, configs: list[ConnectionConfig]) -> Dict[str, bool]:
"""Import multiple VPN configurations.
Args:
configs: List of connection configurations
Returns:
Dictionary mapping connection names to success status
"""
results = {}
for config in configs:
try:
connection_name = self._ensure_connection(config)
results[connection_name] = True
logger.info(f"Successfully imported {connection_name}")
except VPNConnectionError as e:
results[config.name] = False
logger.error(f"Failed to import {config.name}: {e}")
return results
def cleanup_connection(self, config: ConnectionConfig,
remove_from_nm: bool = False) -> None:
"""Clean up a VPN connection.
Args:
config: Connection configuration
remove_from_nm: Whether to remove from NetworkManager
"""
connection_name = config.nmcli_connection_name or config.name
# Disconnect if connected
if self.get_connection_status(config) == VPNStatus.CONNECTED:
self.disconnect_location(config)
# Remove from NetworkManager if requested
if remove_from_nm and self.vpn_manager.connection_exists(connection_name):
logger.info(f"Removing {connection_name} from NetworkManager")
self.vpn_manager.delete_connection(connection_name)