#!/usr/bin/env python3 """ OAuth Provider Configuration Script for Gatehouse This script allows administrators to configure OAuth providers at the application level using the new ApplicationProviderConfig architecture. Usage: # Create a new provider configuration python scripts/configure_oauth_provider.py create google \\ --client-id "YOUR_CLIENT_ID" \\ --client-secret "YOUR_CLIENT_SECRET" \\ --redirect-url "http://localhost:5173/auth/callback" # List all configured providers python scripts/configure_oauth_provider.py list # Show details of a specific provider python scripts/configure_oauth_provider.py show google # Update a provider configuration python scripts/configure_oauth_provider.py update google --enabled false # Delete a provider configuration python scripts/configure_oauth_provider.py delete google # Use environment variables GOOGLE_CLIENT_ID=xxx GOOGLE_CLIENT_SECRET=yyy \\ python scripts/configure_oauth_provider.py create google """ import os import sys import argparse from typing import Optional, Dict, Any # Add the parent directory to the path for imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) # Load environment variables from .env file before any other imports # This ensures database and other configurations are available from dotenv import load_dotenv script_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) env_file = os.path.join(script_dir, '.env') if os.path.exists(env_file): load_dotenv(env_file) # Import after path setup from gatehouse_app import create_app from gatehouse_app.services.external_auth_service import ExternalAuthService, ExternalAuthError # Provider endpoint configurations PROVIDER_DEFAULTS = { "google": { "auth_url": "https://accounts.google.com/o/oauth2/v2/auth", "token_url": "https://oauth2.googleapis.com/token", "userinfo_url": "https://openidconnect.googleapis.com/v1/userinfo", "jwks_url": "https://www.googleapis.com/oauth2/v3/certs", "scopes": ["openid", "profile", "email"], }, "github": { "auth_url": "https://github.com/login/oauth/authorize", "token_url": "https://github.com/login/oauth/access_token", "userinfo_url": "https://api.github.com/user", "scopes": ["read:user", "user:email"], }, "microsoft": { "auth_url": "https://login.microsoftonline.com/common/oauth2/v2.0/authorize", "token_url": "https://login.microsoftonline.com/common/oauth2/v2.0/token", "userinfo_url": "https://graph.microsoft.com/oidc/userinfo", "jwks_url": "https://login.microsoftonline.com/common/discovery/v2.0/keys", "scopes": ["openid", "profile", "email"], }, } class Colors: """ANSI color codes for terminal output.""" HEADER = '\033[95m' OKBLUE = '\033[94m' OKCYAN = '\033[96m' OKGREEN = '\033[92m' WARNING = '\033[93m' FAIL = '\033[91m' ENDC = '\033[0m' BOLD = '\033[1m' UNDERLINE = '\033[4m' def print_success(message: str): """Print success message in green.""" print(f"{Colors.OKGREEN}✓ {message}{Colors.ENDC}") def print_error(message: str): """Print error message in red.""" print(f"{Colors.FAIL}✗ {message}{Colors.ENDC}", file=sys.stderr) def print_warning(message: str): """Print warning message in yellow.""" print(f"{Colors.WARNING}⚠ {message}{Colors.ENDC}") def print_info(message: str): """Print info message in blue.""" print(f"{Colors.OKBLUE}ℹ {message}{Colors.ENDC}") def print_header(message: str): """Print header message.""" print(f"\n{Colors.BOLD}{Colors.HEADER}{message}{Colors.ENDC}") def get_env_credentials(provider_type: str) -> Dict[str, Optional[str]]: """ Get OAuth credentials from environment variables. Supports the following patterns: - {PROVIDER}_CLIENT_ID - {PROVIDER}_CLIENT_SECRET - {PROVIDER}_REDIRECT_URL Args: provider_type: Provider type (google, github, microsoft) Returns: Dictionary with client_id, client_secret, and redirect_url if found """ provider_upper = provider_type.upper() return { "client_id": os.environ.get(f"{provider_upper}_CLIENT_ID"), "client_secret": os.environ.get(f"{provider_upper}_CLIENT_SECRET"), "redirect_url": os.environ.get(f"{provider_upper}_REDIRECT_URL"), } def create_provider(args): """Create a new OAuth provider configuration.""" provider_type = args.provider.lower() print_header(f"Creating {provider_type.title()} OAuth Provider Configuration") # Get credentials from args or environment env_creds = get_env_credentials(provider_type) client_id = args.client_id or env_creds.get("client_id") client_secret = args.client_secret or env_creds.get("client_secret") redirect_url = args.redirect_url or env_creds.get("redirect_url") # Validation if not client_id: print_error(f"Client ID is required. Provide via --client-id or {provider_type.upper()}_CLIENT_ID environment variable.") return 1 if not client_secret: print_error(f"Client secret is required. Provide via --client-secret or {provider_type.upper()}_CLIENT_SECRET environment variable.") return 1 # Get provider defaults if provider_type not in PROVIDER_DEFAULTS: print_error(f"Unknown provider: {provider_type}. Supported providers: {', '.join(PROVIDER_DEFAULTS.keys())}") return 1 defaults = PROVIDER_DEFAULTS[provider_type] # Build configuration config_data = { "client_id": client_id, "client_secret": client_secret, "default_redirect_url": redirect_url, "is_enabled": not args.disabled, **defaults, } # Add custom settings if provided if args.settings: settings = {} for setting in args.settings: try: key, value = setting.split("=", 1) settings[key] = value except ValueError: print_warning(f"Skipping invalid setting format: {setting}") config_data["settings"] = settings try: # Create the provider configuration config = ExternalAuthService.create_app_provider_config( provider_type=provider_type, **config_data ) print_success(f"{provider_type.title()} provider created successfully!") print_info(f"Provider ID: {config.id}") print_info(f"Client ID: {config.client_id}") if redirect_url: print_info(f"Default Redirect URL: {redirect_url}") print_info(f"Enabled: {config.is_enabled}") return 0 except ExternalAuthError as e: print_error(f"Failed to create provider: {e.message}") if e.error_type == "PROVIDER_EXISTS": print_info("Use 'update' command to modify existing provider configuration.") return 1 except Exception as e: print_error(f"Unexpected error: {str(e)}") return 1 def update_provider(args): """Update an existing OAuth provider configuration.""" provider_type = args.provider.lower() print_header(f"Updating {provider_type.title()} OAuth Provider Configuration") # Build updates dictionary updates = {} if args.client_id: updates["client_id"] = args.client_id if args.client_secret: updates["client_secret"] = args.client_secret if args.redirect_url: updates["default_redirect_url"] = args.redirect_url if args.enabled is not None: updates["is_enabled"] = args.enabled if args.settings: settings = {} for setting in args.settings: try: key, value = setting.split("=", 1) settings[key] = value except ValueError: print_warning(f"Skipping invalid setting format: {setting}") updates["settings"] = settings if not updates: print_warning("No updates specified. Use --help to see available options.") return 1 try: config = ExternalAuthService.update_app_provider_config( provider_type=provider_type, **updates ) print_success(f"{provider_type.title()} provider updated successfully!") print_info(f"Provider ID: {config.id}") print_info(f"Client ID: {config.client_id}") if config.default_redirect_url: print_info(f"Default Redirect URL: {config.default_redirect_url}") print_info(f"Enabled: {config.is_enabled}") return 0 except ExternalAuthError as e: print_error(f"Failed to update provider: {e.message}") if e.error_type == "PROVIDER_NOT_FOUND": print_info("Use 'create' command to add a new provider configuration.") return 1 except Exception as e: print_error(f"Unexpected error: {str(e)}") return 1 def list_providers(args): """List all configured OAuth providers.""" print_header("Configured OAuth Providers") try: configs = ExternalAuthService.list_app_provider_configs() if not configs: print_info("No OAuth providers configured yet.") print_info("Use 'create' command to add a provider.") return 0 print() for config in configs: status = f"{Colors.OKGREEN}enabled{Colors.ENDC}" if config.get("is_enabled") else f"{Colors.WARNING}disabled{Colors.ENDC}" print(f" {Colors.BOLD}{config['provider_type']}{Colors.ENDC} - {status}") print(f" Client ID: {config['client_id']}") if config.get('default_redirect_url'): print(f" Redirect URL: {config['default_redirect_url']}") print(f" Created: {config.get('created_at', 'N/A')}") # Show endpoint info if available additional_config = config.get('additional_config', {}) if additional_config: if additional_config.get('auth_url'): print(f" Auth URL: {additional_config['auth_url']}") if additional_config.get('scopes'): scopes = ', '.join(additional_config['scopes']) print(f" Scopes: {scopes}") print() return 0 except Exception as e: print_error(f"Failed to list providers: {str(e)}") return 1 def show_provider(args): """Show details of a specific OAuth provider.""" provider_type = args.provider.lower() print_header(f"{provider_type.title()} OAuth Provider Details") try: config = ExternalAuthService.get_app_provider_config(provider_type) config_dict = config.to_dict() print() print(f"{Colors.BOLD}Basic Information:{Colors.ENDC}") print(f" Provider Type: {config_dict['provider_type']}") print(f" Provider ID: {config_dict['id']}") print(f" Client ID: {config_dict['client_id']}") status = f"{Colors.OKGREEN}enabled{Colors.ENDC}" if config_dict['is_enabled'] else f"{Colors.WARNING}disabled{Colors.ENDC}" print(f" Status: {status}") if config_dict.get('default_redirect_url'): print(f" Default Redirect URL: {config_dict['default_redirect_url']}") print() print(f"{Colors.BOLD}Timestamps:{Colors.ENDC}") print(f" Created: {config_dict.get('created_at', 'N/A')}") print(f" Updated: {config_dict.get('updated_at', 'N/A')}") # Show additional configuration additional_config = config_dict.get('additional_config', {}) if additional_config: print() print(f"{Colors.BOLD}OAuth Configuration:{Colors.ENDC}") if additional_config.get('auth_url'): print(f" Authorization URL: {additional_config['auth_url']}") if additional_config.get('token_url'): print(f" Token URL: {additional_config['token_url']}") if additional_config.get('userinfo_url'): print(f" User Info URL: {additional_config['userinfo_url']}") if additional_config.get('jwks_url'): print(f" JWKS URL: {additional_config['jwks_url']}") if additional_config.get('scopes'): scopes = ', '.join(additional_config['scopes']) print(f" Scopes: {scopes}") # Show any custom settings custom_settings = {k: v for k, v in additional_config.items() if k not in ['auth_url', 'token_url', 'userinfo_url', 'jwks_url', 'scopes']} if custom_settings: print() print(f"{Colors.BOLD}Custom Settings:{Colors.ENDC}") for key, value in custom_settings.items(): print(f" {key}: {value}") print() return 0 except ExternalAuthError as e: print_error(f"Failed to get provider: {e.message}") return 1 except Exception as e: print_error(f"Unexpected error: {str(e)}") return 1 def delete_provider(args): """Delete an OAuth provider configuration.""" provider_type = args.provider.lower() print_header(f"Deleting {provider_type.title()} OAuth Provider Configuration") # Confirm deletion unless --yes flag is provided if not args.yes: print_warning("This will permanently delete the provider configuration.") response = input(f"Are you sure you want to delete {provider_type}? (yes/no): ") if response.lower() not in ['yes', 'y']: print_info("Deletion cancelled.") return 0 try: ExternalAuthService.delete_app_provider_config(provider_type) print_success(f"{provider_type.title()} provider deleted successfully!") return 0 except ExternalAuthError as e: print_error(f"Failed to delete provider: {e.message}") return 1 except Exception as e: print_error(f"Unexpected error: {str(e)}") return 1 def main(): """Main entry point for the script.""" parser = argparse.ArgumentParser( description="Configure OAuth providers for Gatehouse authentication", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: # Create Google OAuth configuration %(prog)s create google --client-id "CLIENT_ID" --client-secret "SECRET" # Create with environment variables GOOGLE_CLIENT_ID=xxx GOOGLE_CLIENT_SECRET=yyy %(prog)s create google # List all providers %(prog)s list # Show provider details %(prog)s show google # Update provider %(prog)s update google --enabled true # Delete provider %(prog)s delete google --yes Supported Providers: - google - github - microsoft """ ) subparsers = parser.add_subparsers(dest="command", help="Command to execute") subparsers.required = True # Create command create_parser = subparsers.add_parser("create", help="Create a new OAuth provider configuration") create_parser.add_argument("provider", help="Provider type (google, github, microsoft)") create_parser.add_argument("--client-id", help="OAuth client ID") create_parser.add_argument("--client-secret", help="OAuth client secret") create_parser.add_argument("--redirect-url", help="Default redirect URL for OAuth callbacks") create_parser.add_argument("--disabled", action="store_true", help="Create provider in disabled state") create_parser.add_argument("--settings", action="append", help="Custom settings (key=value format)") create_parser.set_defaults(func=create_provider) # Update command update_parser = subparsers.add_parser("update", help="Update an existing OAuth provider configuration") update_parser.add_argument("provider", help="Provider type to update") update_parser.add_argument("--client-id", help="New OAuth client ID") update_parser.add_argument("--client-secret", help="New OAuth client secret") update_parser.add_argument("--redirect-url", help="New default redirect URL") update_parser.add_argument("--enabled", type=lambda x: x.lower() in ['true', '1', 'yes'], help="Enable or disable the provider (true/false)") update_parser.add_argument("--settings", action="append", help="Custom settings to update (key=value format)") update_parser.set_defaults(func=update_provider) # List command list_parser = subparsers.add_parser("list", help="List all configured OAuth providers") list_parser.set_defaults(func=list_providers) # Show command show_parser = subparsers.add_parser("show", help="Show details of a specific OAuth provider") show_parser.add_argument("provider", help="Provider type to show") show_parser.set_defaults(func=show_provider) # Delete command delete_parser = subparsers.add_parser("delete", help="Delete an OAuth provider configuration") delete_parser.add_argument("provider", help="Provider type to delete") delete_parser.add_argument("--yes", "-y", action="store_true", help="Skip confirmation prompt") delete_parser.set_defaults(func=delete_provider) args = parser.parse_args() # Create Flask app context app = create_app() with app.app_context(): return args.func(args) if __name__ == "__main__": sys.exit(main())