# streaming_providers/base/auth/credential_manager.py from typing import Optional from .credentials import BaseCredentials, UserPasswordCredentials, ClientCredentials import time from typing import Dict, Any, List # Import centralized logger and VFS from ..utils.logger import logger class CredentialManager: """ Manages loading and saving credentials from/to persistent storage Now supports country-specific credentials """ def __init__(self, config_dir: Optional[str] = None): # Initialize VFS with config directory support from ..utils.vfs import VFS self.vfs = VFS(config_dir=config_dir) # Credentials file is always in the root of the VFS base path self.credentials_file = 'credentials.json' # Ensure base directory exists self.vfs.mkdirs('') logger.debug(f"CredentialManager initialized with VFS base: {self.vfs.base_path}") @staticmethod def _get_credential_path(provider: str, country: Optional[str] = None) -> tuple: """ Determine the path to credential data based on country Args: provider: Provider name country: Optional country code (e.g., 'de', 'at', 'ch') Returns: Tuple of (keys_path, is_nested) where keys_path is list of keys to navigate """ if country: return [provider, country], True else: return [provider], False def load_credentials(self, provider: str, country: Optional[str] = None) -> Optional[BaseCredentials]: """ Load credentials for a specific provider and optional country Args: provider: Provider name (e.g., 'rtlplus', 'joyn') country: Optional country code (e.g., 'de', 'at', 'ch') Returns: BaseCredentials instance or None if not found """ country_str = f" (country: {country})" if country else "" try: logger.debug(f"CredentialManager: Loading credentials for '{provider}{country_str}'") # Check if credentials file exists if not self.vfs.exists(self.credentials_file): logger.debug(f"CredentialManager: Credentials file does not exist: {self.credentials_file}") return None logger.debug(f"CredentialManager: Reading credentials file: {self.credentials_file}") data = self.vfs.read_json(self.credentials_file) if not data: logger.debug(f"CredentialManager: Credentials file is empty or invalid JSON") return None logger.debug(f"CredentialManager: Available providers in file: {list(data.keys())}") # Navigate to the correct credential data keys_path, is_nested = self._get_credential_path(provider, country) provider_data = data for key in keys_path: if not isinstance(provider_data, dict) or key not in provider_data: logger.debug(f"CredentialManager: No data found at path: {' -> '.join(keys_path)}") return None provider_data = provider_data[key] if not provider_data: logger.debug(f"CredentialManager: No data found for provider '{provider}{country_str}'") return None logger.debug(f"CredentialManager: Found data for '{provider}{country_str}': {list(provider_data.keys())}") credential_type = provider_data.get('type') logger.debug(f"CredentialManager: Credential type for '{provider}{country_str}': {credential_type}") if credential_type == 'user_password': username = provider_data.get('username', '') password_present = 'password' in provider_data client_id = provider_data.get('client_id') logger.debug(f"CredentialManager: Creating UserPasswordCredentials for '{provider}{country_str}'") logger.debug( f"CredentialManager: Username: '{username}', Password present: {password_present}, Client ID: {client_id}") creds = UserPasswordCredentials( username=username, password=self._decode_password(provider_data.get('password', '')), client_id=client_id, grant_type=provider_data.get('grant_type', 'password') ) logger.debug( f"CredentialManager: Successfully created UserPasswordCredentials for '{provider}{country_str}'") return creds elif credential_type == 'client_credentials': client_id = provider_data.get('client_id', '') client_secret_present = 'client_secret' in provider_data logger.debug(f"CredentialManager: Creating ClientCredentials for '{provider}{country_str}'") logger.debug( f"CredentialManager: Client ID: '{client_id}', Client secret present: {client_secret_present}") creds = ClientCredentials( client_id=client_id, client_secret=self._decode_password(provider_data.get('client_secret', '')), grant_type=provider_data.get('grant_type', 'client_credentials') ) logger.debug(f"CredentialManager: Successfully created ClientCredentials for '{provider}{country_str}'") return creds else: logger.error( f"CredentialManager: Unknown credential type for '{provider}{country_str}': {credential_type}") return None except Exception as e: country_str = f" (country: {country})" if country else "" logger.error(f"CredentialManager: Error loading credentials for '{provider}{country_str}': {e}") return None def save_credentials(self, provider: str, credentials: BaseCredentials, country: Optional[str] = None) -> bool: """ Save credentials for a specific provider and optional country Args: provider: Provider name credentials: Credentials to save country: Optional country code Returns: True if successful, False otherwise """ country_str = f" (country: {country})" if country else "" try: logger.debug(f"Saving {credentials.credential_type} credentials for {provider}{country_str}") # Load existing data data = {} if self.vfs.exists(self.credentials_file): data = self.vfs.read_json(self.credentials_file) or {} # Prepare provider data based on credential type if isinstance(credentials, UserPasswordCredentials): provider_data = { 'type': 'user_password', 'username': credentials.username, 'password': self._encode_password(credentials.password), 'grant_type': credentials.grant_type } if credentials.client_id: provider_data['client_id'] = credentials.client_id elif isinstance(credentials, ClientCredentials): provider_data = { 'type': 'client_credentials', 'client_id': credentials.client_id, 'client_secret': self._encode_password(credentials.client_secret), 'grant_type': credentials.grant_type } else: logger.error(f"Unsupported credential type: {type(credentials)}") return False # Navigate and create nested structure if needed keys_path, is_nested = self._get_credential_path(provider, country) # Build nested structure current = data for i, key in enumerate(keys_path[:-1]): if key not in current: current[key] = {} elif not isinstance(current[key], dict): logger.warning(f"Overwriting non-dict value at {key}") current[key] = {} current = current[key] # Set the final value current[keys_path[-1]] = provider_data # Save to file using VFS success = self.vfs.write_json(self.credentials_file, data) if success: logger.info(f"Successfully saved credentials for {provider}{country_str}") # Verify by reading back verify_data = self.vfs.read_json(self.credentials_file) if verify_data: verify_current = verify_data found = True for key in keys_path: if not isinstance(verify_current, dict) or key not in verify_current: found = False break verify_current = verify_current[key] if found: logger.debug(f"Verification successful: {provider}{country_str} data found in saved file") else: logger.error(f"Verification failed: {provider}{country_str} data NOT found in saved file") return False return True else: logger.error(f"Failed to save credentials for {provider}{country_str}") return False except Exception as e: country_str = f" (country: {country})" if country else "" logger.error(f"Error saving credentials for {provider}{country_str}: {e}") return False def delete_credentials(self, provider: str, country: Optional[str] = None) -> bool: """ Delete credentials for a specific provider and optional country Args: provider: Provider name country: Optional country code (if None, deletes entire provider or all countries) Returns: True if successful, False otherwise """ country_str = f" (country: {country})" if country else "" try: if not self.vfs.exists(self.credentials_file): logger.debug(f"No credentials file exists, nothing to delete for {provider}{country_str}") return True data = self.vfs.read_json(self.credentials_file) if not data: logger.debug(f"Credentials file is empty, nothing to delete for {provider}{country_str}") return True if country: # Delete specific country data if provider in data and isinstance(data[provider], dict) and country in data[provider]: del data[provider][country] logger.info(f"Deleted stored credentials for {provider}{country_str}") # If provider dict is now empty, remove it entirely if not data[provider]: del data[provider] logger.debug(f"Provider {provider} had no more countries, removed entirely") return self.vfs.write_json(self.credentials_file, data) else: logger.debug(f"No stored credentials found to delete for {provider}{country_str}") else: # Delete entire provider data (all countries) if provider in data: del data[provider] logger.info(f"Deleted all stored credentials for {provider}") return self.vfs.write_json(self.credentials_file, data) else: logger.debug(f"No stored credentials found to delete for {provider}") return True except Exception as e: country_str = f" (country: {country})" if country else "" logger.error(f"Error deleting credentials for {provider}{country_str}: {e}") return False def list_providers(self) -> List[str]: """ List all providers with stored credentials Returns: List of provider names """ try: if not self.vfs.exists(self.credentials_file): logger.debug("No credentials file exists, returning empty provider list") return [] data = self.vfs.read_json(self.credentials_file) if not data: logger.debug("Credentials file is empty, returning empty provider list") return [] providers = list(data.keys()) logger.debug(f"Found stored credentials for providers: {providers}") return providers except Exception as e: logger.error(f"Error listing providers: {e}") return [] def get_all_countries(self, provider: str) -> List[str]: """ Get all countries that have credentials for a provider Args: provider: Provider name Returns: List of country codes """ try: if not self.vfs.exists(self.credentials_file): return [] data = self.vfs.read_json(self.credentials_file) if not data or provider not in data: return [] provider_data = data[provider] # Check if this is a nested (country-aware) structure if isinstance(provider_data, dict): # Check if any keys look like country codes (2-3 char strings) # and their values are dicts (credential data) countries = [] for key, value in provider_data.items(): if isinstance(value, dict) and len(key) <= 3 and 'type' in value: countries.append(key) return countries return [] except Exception as e: logger.error(f"Error getting countries for {provider}: {e}") return [] def has_credentials(self, provider: str, country: Optional[str] = None) -> bool: """ Check if credentials exist for a provider and optional country Args: provider: Provider name country: Optional country code Returns: True if credentials exist, False otherwise """ credentials = self.load_credentials(provider, country) return credentials is not None and credentials.validate() @staticmethod def _encode_password(password: str) -> str: """No encoding - store as plaintext (TEMPORARY INSECURE SOLUTION).""" logger.warning("Storing password in plaintext - this is insecure and should be replaced with proper encryption") return password @staticmethod def _decode_password(encoded_password: str) -> str: """No decoding needed - passwords are stored in plaintext.""" return encoded_password def export_config(self, provider: Optional[str] = None, country: Optional[str] = None) -> Dict[str, Any]: """ Export credentials to a portable format Args: provider: Optional provider name to export (if None, exports all) country: Optional country code (only used if provider is specified) Returns: Dictionary with credentials data """ export_data = { 'version': '1.1', # Bumped for country support 'exported_at': time.time(), 'credentials': {} } if provider: # Export specific provider if country: # Export specific country credentials = self.load_credentials(provider, country) if credentials: export_data['credentials'][provider] = { country: self._credential_to_dict(credentials) } else: # Export all countries for provider or non-country data countries = self.get_all_countries(provider) if countries: # Provider has country-specific credentials provider_data = {} for ctry in countries: credentials = self.load_credentials(provider, ctry) if credentials: provider_data[ctry] = self._credential_to_dict(credentials) if provider_data: export_data['credentials'][provider] = provider_data else: # Non-country provider credentials = self.load_credentials(provider) if credentials: export_data['credentials'][provider] = self._credential_to_dict(credentials) else: # Export all providers for prov in self.list_providers(): countries = self.get_all_countries(prov) if countries: # Provider has country-specific credentials provider_data = {} for ctry in countries: credentials = self.load_credentials(prov, ctry) if credentials: provider_data[ctry] = self._credential_to_dict(credentials) if provider_data: export_data['credentials'][prov] = provider_data else: # Non-country provider credentials = self.load_credentials(prov) if credentials: export_data['credentials'][prov] = self._credential_to_dict(credentials) return export_data @staticmethod def _credential_to_dict(credentials: BaseCredentials) -> Dict[str, Any]: """Convert credentials object to dictionary for export""" if isinstance(credentials, UserPasswordCredentials): return { 'type': 'user_password', 'username': credentials.username, 'password': credentials.password, 'client_id': credentials.client_id } elif isinstance(credentials, ClientCredentials): return { 'type': 'client_credentials', 'client_id': credentials.client_id, 'client_secret': credentials.client_secret } else: return {} def import_config(self, config_data: Dict[str, Any]) -> Dict[str, bool]: """ Import credentials from exported configuration Args: config_data: Configuration data from export_config() Returns: Dictionary mapping provider names (or provider_country) to import success status """ results = {} # Validate config data if not isinstance(config_data, dict) or 'credentials' not in config_data: logger.error("Invalid credential config data format") return results credentials_data = config_data.get('credentials', {}) for provider, provider_data in credentials_data.items(): try: # Check if this is country-aware structure if isinstance(provider_data, dict) and not provider_data.get('type'): # Nested structure with countries for country, cred_data in provider_data.items(): if not isinstance(cred_data, dict): continue credentials = self._dict_to_credential(cred_data) if credentials: success = self.save_credentials(provider, credentials, country) results[f"{provider}_{country}"] = success if success: logger.info(f"Successfully imported credentials for {provider} ({country})") else: logger.error(f"Failed to import credentials for {provider} ({country})") else: # Flat structure (no country) credentials = self._dict_to_credential(provider_data) if credentials: success = self.save_credentials(provider, credentials) results[provider] = success if success: logger.info(f"Successfully imported credentials for {provider}") else: logger.error(f"Failed to import credentials for {provider}") except Exception as e: logger.error(f"Error importing credentials for {provider}: {e}") results[provider] = False return results @staticmethod def _dict_to_credential(cred_data: Dict[str, Any]) -> Optional[BaseCredentials]: """Convert dictionary to credentials object""" try: if cred_data.get('type') == 'user_password': return UserPasswordCredentials( username=cred_data.get('username', ''), password=cred_data.get('password', ''), client_id=cred_data.get('client_id') ) elif cred_data.get('type') == 'client_credentials': return ClientCredentials( client_id=cred_data.get('client_id', ''), client_secret=cred_data.get('client_secret', '') ) else: logger.error(f"Unknown credential type: {cred_data.get('type')}") return None except Exception as e: logger.error(f"Error creating credential from dict: {e}") return None def debug_credentials_file(self) -> None: """Debug method to log the current state of the credentials file""" try: logger.info(f"=== CREDENTIALS FILE DEBUG INFO ===") credentials_file_path = self.vfs.join_path(self.credentials_file) logger.info(f"Credentials file path: {credentials_file_path}") logger.info(f"Credentials file exists: {self.vfs.exists(self.credentials_file)}") if self.vfs.exists(self.credentials_file): file_size = self.vfs.get_size(self.credentials_file) logger.info(f"Credentials file size: {file_size} bytes") if file_size and file_size > 0: data = self.vfs.read_json(self.credentials_file) if data: logger.info(f"Credentials file contains {len(data)} providers: {list(data.keys())}") for provider, provider_data in data.items(): if isinstance(provider_data, dict): # Check if nested (has countries) has_countries = any( isinstance(v, dict) and len(k) <= 3 and 'type' in v for k, v in provider_data.items() ) if has_countries: logger.info(f" {provider} (country-aware):") for country, cred_data in provider_data.items(): if isinstance(cred_data, dict) and 'type' in cred_data: cred_type = cred_data.get('type') username = cred_data.get('username', 'N/A') logger.info(f" {country}: type={cred_type}, username={username}") else: # Flat structure (no country) cred_type = provider_data.get('type', 'unknown') username = provider_data.get('username', 'N/A') logger.info(f" {provider} (no country): type={cred_type}, username={username}") else: logger.error("Credentials file contains invalid JSON or is empty") else: logger.info("Credentials file is empty") else: logger.info("Credentials file does not exist yet") logger.info(f"=== END CREDENTIALS FILE DEBUG ===") except Exception as e: logger.error(f"Error during credentials file debug: {e}")