Files
I-SecretUpdate/main.py
T
2025-12-22 09:57:49 +01:00

324 lines
12 KiB
Python

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