""" Azure Key Vault Secret Manager Main application entry point. """ import asyncio import customtkinter as ctk from tkinter import messagebox import sys # Import modules import config from auth.graph_authenticator import GraphAuthenticator from auth.azure_authenticator import AzureAuthenticator from services.app_registration_service import AppRegistrationService from services.secret_service import SecretService from services.keyvault_service import KeyVaultService from ui.main_window import MainWindow from utils.logger import setup_logger # Set appearance ctk.set_appearance_mode("system") ctk.set_default_color_theme("blue") class Application: """Main application class.""" def __init__(self): """Initialize the application.""" self.logger = setup_logger() # Initialize async worker FIRST (before any async operations) from utils.async_worker import AsyncWorker self.async_worker = AsyncWorker() self.async_worker.start() # Authentication self.graph_auth = GraphAuthenticator() self.azure_auth = AzureAuthenticator() # Services (will be initialized after authentication) self.app_service = None self.secret_service = None self.vault_service = None # Data self.apps = [] self.vaults = [] self.subscriptions = [] self.selected_app = None self.selected_subscription = None # Create main window self.window = MainWindow( on_connect=self.handle_connect, on_subscription_selected=self.handle_subscription_selected, on_app_selected=self.handle_app_selected, on_generate_secret=self.handle_generate_secret, on_generate_another=self.handle_generate_another ) def handle_connect(self): """Handle authentication button click.""" self.window.set_connecting() # Submit to async worker instead of creating new loop def on_complete(future): """Handle authentication completion in main thread.""" try: future.result() # Raises if authentication failed except Exception as e: self.logger.error(f"Authentication failed: {str(e)}") self.window.after(0, lambda: messagebox.showerror( "Authentication Error", f"Failed to authenticate:\n\n{str(e)}\n\nPlease try again." )) self.window.after(0, self.window.enable_connect_button) future = self.async_worker.submit(self._authenticate()) future.add_done_callback(on_complete) async def _authenticate(self): """Perform authentication with tenant discovery.""" try: self.logger.info("Starting authentication with tenant discovery...") # PHASE 1: Discover tenant ID using "organizations" endpoint (single auth prompt) self.logger.info("Discovering tenant ID...") discovered_tenant_id, orgs_credential, subscriptions = self.azure_auth.discover_tenant_id() self.logger.info(f"Discovered tenant: {discovered_tenant_id}") # PHASE 2: Reuse the "organizations" credential for Graph # Skip validation to avoid triggering Graph API auth here - it will auth when first used self.logger.info("Initializing Microsoft Graph with shared credential...") await self.graph_auth.authenticate(credential=orgs_credential, skip_validation=True) self.logger.info("Initializing Azure with shared credential...") self.azure_auth.authenticate(credential=orgs_credential, tenant_id=discovered_tenant_id, subscriptions=subscriptions) # Initialize Graph services graph_client = self.graph_auth.get_client() self.app_service = AppRegistrationService(graph_client) self.secret_service = SecretService(graph_client) # Update UI self.window.set_authenticated(True) self.logger.info("Authentication successful") # Load subscriptions (already loaded during discover_tenant_id) await self._load_subscriptions() except Exception as e: self.logger.error(f"Authentication failed: {str(e)}") messagebox.showerror( "Authentication Error", f"Failed to authenticate:\n\n{str(e)}\n\nPlease try again." ) self.window.enable_connect_button() async def _load_subscriptions(self): """Load Azure subscriptions.""" try: self.window.set_loading_subscriptions(True) self.logger.info("Loading subscriptions...") # Get subscriptions from authenticator self.subscriptions = self.azure_auth.get_subscriptions() self.window.set_subscriptions(self.subscriptions) self.window.set_loading_subscriptions(False) self.logger.info(f"Loaded {len(self.subscriptions)} subscription(s)") except Exception as e: self.logger.error(f"Failed to load subscriptions: {str(e)}") messagebox.showerror( "Load Error", f"Failed to load subscriptions:\n\n{str(e)}" ) def handle_subscription_selected(self, subscription): """Handle subscription selection.""" self.selected_subscription = subscription self.logger.info(f"Selected subscription: {subscription['name']} ({subscription['id']})") # Set the subscription in the azure authenticator self.azure_auth.set_subscription(subscription['id']) # Initialize KeyVault service with the selected subscription self.vault_service = KeyVaultService( self.azure_auth.get_credential(), self.azure_auth.get_subscription_id() ) # Enable UI elements self.window.set_subscription_selected() # Submit data loading to async worker (no threading.Thread needed) def on_complete(future): """Handle data loading completion.""" try: future.result() except Exception as e: self.logger.error(f"Failed to load data: {str(e)}") self.window.after(0, lambda: messagebox.showerror( "Load Error", f"Failed to load data:\n\n{str(e)}" )) future = self.async_worker.submit(self._load_data()) future.add_done_callback(on_complete) async def _load_data(self): """Load app registrations and Key Vaults.""" try: # Load apps self.window.set_loading_apps(True) self.logger.info("Loading app registrations...") self.apps = await self.app_service.list_applications() self.window.set_apps(self.apps) self.window.set_loading_apps(False) self.logger.info(f"Loaded {len(self.apps)} app registrations") # Load Key Vaults self.window.set_loading_vaults(True) self.logger.info("Loading Key Vaults...") self.vaults = self.vault_service.list_keyvaults() self.window.set_vaults(self.vaults) self.window.set_loading_vaults(False) self.logger.info(f"Loaded {len(self.vaults)} Key Vaults") except Exception as e: self.logger.error(f"Failed to load data: {str(e)}") messagebox.showerror( "Load Error", f"Failed to load data:\n\n{str(e)}" ) def handle_app_selected(self, app): """Handle app selection.""" self.selected_app = app self.logger.info(f"Selected app: {app['display_name']}") def handle_generate_secret(self, description: str, vault: dict, remove_old: bool): """Handle secret generation.""" # Submit to async worker instead of creating new loop def on_complete(future): """Handle generation completion.""" try: future.result() except Exception as e: self.logger.error(f"Failed to generate secret: {str(e)}") self.window.after(0, lambda: self.window.set_generating(False)) self.window.after(0, lambda: messagebox.showerror( "Generation Error", f"Failed to generate secret:\n\n{str(e)}" )) future = self.async_worker.submit(self._generate_secret(description, vault, remove_old)) future.add_done_callback(on_complete) async def _generate_secret(self, description: str, vault: dict, remove_old: bool): """Generate secret asynchronously.""" try: # Validate inputs app = self.window.get_selected_app() if not app: messagebox.showwarning("Validation Error", "Please select an app registration.") return if not description: messagebox.showwarning("Validation Error", "Please enter a secret description.") return if not vault: messagebox.showwarning("Validation Error", "Please select a Key Vault.") return self.window.set_generating(True) self.logger.info(f"Generating secret for app: {app['display_name']}") # Create secret secret_result = await self.secret_service.create_secret( app_object_id=app['id'], description=description ) self.logger.info(f"Secret created successfully. Key ID: {secret_result['key_id']}") # Remove old secrets if requested removed_count = 0 if remove_old: self.logger.info("Removing old secrets...") removed_count = await self.secret_service.remove_old_secrets( app_object_id=app['id'], keep_key_id=secret_result['key_id'] ) self.logger.info(f"Removed {removed_count} old secret(s)") # Store in Key Vault self.logger.info(f"Storing secret in Key Vault: {vault['name']}") sanitized_name = self.vault_service.store_secret( vault_name=vault['name'], secret_name=app['display_name'], secret_value=secret_result['secret_text'], description=description, secret_id=secret_result['key_id'], expires=secret_result['end_datetime'] ) self.logger.info(f"Secret stored successfully: {sanitized_name}") # Show result in the main window (no popup needed - result frame shows all info) self.window.show_result( secret_name=sanitized_name, vault_name=vault['name'], secret_value=secret_result['secret_text'], removed_count=removed_count ) # Reset generating state self.window.set_generating(False) except Exception as e: self.logger.error(f"Failed to generate secret: {str(e)}") self.window.set_generating(False) messagebox.showerror( "Generation Error", f"Failed to generate secret:\n\n{str(e)}" ) def handle_generate_another(self): """Handle generate another secret.""" self.window.reset_form() self.logger.info("Form reset for another secret generation") def cleanup(self): """Cleanup resources on application exit.""" self.logger.info("Shutting down application...") if hasattr(self, 'async_worker'): self.async_worker.stop() def _on_closing(self): """Handle window close event.""" self.cleanup() self.window.destroy() def run(self): """Run the application.""" self.logger.info("Starting Azure Key Vault Secret Manager") # Register cleanup on window close self.window.protocol("WM_DELETE_WINDOW", self._on_closing) self.window.mainloop() if __name__ == "__main__": app = Application() app.run()