"""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)