Files
script.service.ultimate/lib/streaming_providers/base/auth/credential_manager.py
T
2025-10-29 20:23:50 +01:00

583 lines
24 KiB
Python

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