324 lines
12 KiB
Python
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()
|