stuff
This commit is contained in:
266
services/connection_manager.py
Normal file
266
services/connection_manager.py
Normal file
@@ -0,0 +1,266 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user