#!/usr/bin/env nu # [command] # name = "auth login" # group = "authentication" # tags = ["authentication", "jwt", "interactive", "login"] # version = "3.0.0" # requires = ["nushell:0.109.0"] # Authentication Plugin Wrapper with HTTP Fallback # Provides graceful degradation to HTTP API when nu_plugin_auth is unavailable use ../config/accessor.nu * use ../commands/traits.nu * # Check if auth plugin is available def is-plugin-available [] { (which auth | length) > 0 } # Check if auth plugin is enabled in config def is-plugin-enabled [] { config-get "plugins.auth_enabled" true } # Get control center base URL def get-control-center-url [] { config-get "platform.control_center.url" "http://localhost:3000" } # Store token in OS keyring (requires plugin) def store-token-keyring [ token: string ] { if (is-plugin-available) { auth store-token $token } else { print "⚠️ Keyring storage unavailable (plugin not loaded)" } } # Retrieve token from OS keyring (requires plugin) def get-token-keyring [] { if (is-plugin-available) { auth get-token } else { "" } } # Helper to safely execute a closure and return null on error def try-plugin [callback: closure] { do -i $callback } # Login with username and password export def plugin-login [ username: string password: string --mfa-code: string = "" # Optional MFA code ] { let enabled = is-plugin-enabled let available = is-plugin-available if $enabled and $available { let plugin_result = (try-plugin { # Note: Plugin login command may not support MFA code directly # If MFA is required, it should be handled separately via mfa-verify let result = (auth login $username $password) store-token-keyring $result.access_token # If MFA code provided, verify it after login if not ($mfa_code | is-empty) { let mfa_result = (try-plugin { auth mfa-verify $mfa_code }) if $mfa_result == null { print "⚠️ MFA verification failed, but login succeeded" } } $result }) if $plugin_result != null { return $plugin_result } print "⚠️ Plugin login failed, falling back to HTTP" } # HTTP fallback print "⚠️ Using HTTP fallback (plugin not available)" let url = $"(get-control-center-url)/api/auth/login" let body = if ($mfa_code | is-empty) { {username: $username, password: $password} } else { {username: $username, password: $password, mfa_code: $mfa_code} } let result = (do -i { http post $url $body }) if $result != null { return $result } error make { msg: "Login failed" label: { text: "HTTP request failed" span: (metadata $username).span } } } # Logout and revoke tokens export def plugin-logout [] { let enabled = is-plugin-enabled let available = is-plugin-available let token = get-token-keyring if $enabled and $available { let plugin_result = (try-plugin { auth logout }) if $plugin_result != null { return $plugin_result } print "⚠️ Plugin logout failed, falling back to HTTP" } # HTTP fallback print "⚠️ Using HTTP fallback (plugin not available)" let url = $"(get-control-center-url)/api/auth/logout" let result = (do -i { if ($token | is-empty) { http post $url } else { http post $url --headers {Authorization: $"Bearer ($token)"} } }) if $result != null { return {success: true, message: "Logged out successfully"} } {success: false, message: "Logout failed"} } # Verify current authentication token export def plugin-verify [] { let enabled = is-plugin-enabled let available = is-plugin-available if $enabled and $available { let plugin_result = (try-plugin { auth verify }) if $plugin_result != null { return $plugin_result } print "⚠️ Plugin verify failed, falling back to HTTP" } # HTTP fallback print "⚠️ Using HTTP fallback (plugin not available)" let token = get-token-keyring if ($token | is-empty) { return {valid: false, message: "No token found"} } let url = $"(get-control-center-url)/api/auth/verify" let result = (do -i { http get $url --headers {Authorization: $"Bearer ($token)"} }) if $result != null { return $result } {valid: false, message: "Token verification failed"} } # List active sessions export def plugin-sessions [] { let enabled = is-plugin-enabled let available = is-plugin-available if $enabled and $available { let plugin_result = (try-plugin { auth sessions }) if $plugin_result != null { return $plugin_result } print "⚠️ Plugin sessions failed, falling back to HTTP" } # HTTP fallback print "⚠️ Using HTTP fallback (plugin not available)" let token = get-token-keyring if ($token | is-empty) { return [] } let url = $"(get-control-center-url)/api/auth/sessions" let response = (do -i { http get $url --headers {Authorization: $"Bearer ($token)"} }) if $response != null { return ($response | get sessions? | default []) } [] } # Enroll MFA device (TOTP) export def plugin-mfa-enroll [ --type: string = "totp" # totp or webauthn ] { let enabled = is-plugin-enabled let available = is-plugin-available if $enabled and $available { let plugin_result = (try-plugin { auth mfa-enroll --type $type }) if $plugin_result != null { return $plugin_result } print "⚠️ Plugin MFA enroll failed, falling back to HTTP" } # HTTP fallback print "⚠️ Using HTTP fallback (plugin not available)" let token = get-token-keyring if ($token | is-empty) { error make { msg: "Authentication required" label: {text: "No valid token found"} } } let url = $"(get-control-center-url)/api/mfa/enroll" let result = (do -i { http post $url {type: $type} --headers {Authorization: $"Bearer ($token)"} }) if $result != null { return $result } error make { msg: "MFA enrollment failed" label: {text: "HTTP request failed"} } } # Verify MFA code export def plugin-mfa-verify [ code: string --type: string = "totp" # totp or webauthn ] { let enabled = is-plugin-enabled let available = is-plugin-available if $enabled and $available { let plugin_result = (try-plugin { auth mfa-verify $code --type $type }) if $plugin_result != null { return $plugin_result } print "⚠️ Plugin MFA verify failed, falling back to HTTP" } # HTTP fallback print "⚠️ Using HTTP fallback (plugin not available)" let token = get-token-keyring if ($token | is-empty) { error make { msg: "Authentication required" label: {text: "No valid token found"} } } let url = $"(get-control-center-url)/api/mfa/verify" let result = (do -i { http post $url {code: $code, type: $type} --headers {Authorization: $"Bearer ($token)"} }) if $result != null { return $result } error make { msg: "MFA verification failed" label: { text: "HTTP request failed" span: (metadata $code).span } } } # Get current authentication status export def plugin-auth-status [] { let plugin_available = is-plugin-available let plugin_enabled = is-plugin-enabled let token = get-token-keyring let has_token = not ($token | is-empty) { plugin_available: $plugin_available plugin_enabled: $plugin_enabled has_token: $has_token mode: (if ($plugin_enabled and $plugin_available) { "plugin" } else { "http" }) } } # ============================================================================ # Metadata-Driven Authentication Helpers # ============================================================================ # Get auth requirements from metadata for a specific command def get-metadata-auth-requirements [ command_name: string # Command to check (e.g., "server create", "cluster delete") ] { let metadata = (get-command-metadata $command_name) if ($metadata | type) == "record" { let requirements = ($metadata | get requirements? | default {}) { requires_auth: ($requirements | get requires_auth? | default false) auth_type: ($requirements | get auth_type? | default "none") requires_confirmation: ($requirements | get requires_confirmation? | default false) min_permission: ($requirements | get min_permission? | default "read") side_effect_type: ($requirements | get side_effect_type? | default "none") } } else { { requires_auth: false auth_type: "none" requires_confirmation: false min_permission: "read" side_effect_type: "none" } } } # Determine if MFA is required based on metadata auth_type def requires-mfa-from-metadata [ command_name: string # Command to check ] { let auth_reqs = (get-metadata-auth-requirements $command_name) $auth_reqs.auth_type == "mfa" or $auth_reqs.auth_type == "cedar" } # Determine if operation is destructive based on metadata def is-destructive-from-metadata [ command_name: string # Command to check ] { let auth_reqs = (get-metadata-auth-requirements $command_name) $auth_reqs.side_effect_type == "delete" } # Check if metadata indicates this is a production operation def is-production-from-metadata [ command_name: string # Command to check ] { let metadata = (get-command-metadata $command_name) if ($metadata | type) == "record" { let tags = ($metadata | get tags? | default []) ($tags | any { |tag| $tag == "production" or $tag == "deploy" }) } else { false } } # Validate minimum permission level required by metadata def validate-permission-level [ command_name: string # Command to check user_level: string # User's permission level (read, write, admin, superadmin) ] { let auth_reqs = (get-metadata-auth-requirements $command_name) let required_level = $auth_reqs.min_permission # Permission level hierarchy (lower index = lower permission) let level_map = { read: 0 write: 1 admin: 2 superadmin: 3 } # Get required permission level index let req_level = ( if $required_level == "read" { 0 } else if $required_level == "write" { 1 } else if $required_level == "admin" { 2 } else if $required_level == "superadmin" { 3 } else { -1 } ) # Get user permission level index let usr_level = ( if $user_level == "read" { 0 } else if $user_level == "write" { 1 } else if $user_level == "admin" { 2 } else if $user_level == "superadmin" { 3 } else { -1 } ) # User must have equal or higher permission level if $req_level < 0 or $usr_level < 0 { return false } $usr_level >= $req_level } # Determine auth enforcement based on metadata export def should-enforce-auth-from-metadata [ command_name: string # Command to check ] { let auth_reqs = (get-metadata-auth-requirements $command_name) # If metadata explicitly requires auth, enforce it if $auth_reqs.requires_auth { return true } # If side effects, enforce auth if $auth_reqs.side_effect_type != "none" { return true } # Otherwise check configuration (should-require-auth) } # ============================================================================ # Security Policy Enforcement Functions # ============================================================================ # Check if authentication is required based on configuration export def should-require-auth [] { let config_required = (config-get "security.require_auth" false) let env_bypass = ($env.PROVISIONING_SKIP_AUTH? | default "false") == "true" let allow_bypass = (config-get "security.bypass.allow_skip_auth" false) $config_required and not ($env_bypass and $allow_bypass) } # Check if MFA is required for production operations export def should-require-mfa-prod [] { let environment = (config-get "environment" "dev") let require_mfa = (config-get "security.require_mfa_for_production" true) ($environment == "prod") and $require_mfa } # Check if MFA is required for destructive operations export def should-require-mfa-destructive [] { (config-get "security.require_mfa_for_destructive" true) } # Check if user is authenticated export def is-authenticated [] { let result = (plugin-verify) ($result | get valid? | default false) } # Check if MFA is verified export def is-mfa-verified [] { let result = (plugin-verify) ($result | get mfa_verified? | default false) } # Get current authenticated user export def get-authenticated-user [] { let result = (plugin-verify) ($result | get username? | default "") } # Require authentication with clear error messages export def require-auth [ operation: string # Operation name for error messages --allow-skip # Allow skip-auth flag bypass ] { # Check if authentication is required if not (should-require-auth) { return true } # Check if skip is allowed if $allow_skip and (($env.PROVISIONING_SKIP_AUTH? | default "false") == "true") { print $"⚠️ Authentication bypassed with PROVISIONING_SKIP_AUTH flag" print $" (ansi yellow_bold)WARNING: This should only be used in development/testing!(ansi reset)" return true } # Verify authentication let auth_status = (plugin-verify) if not ($auth_status | get valid? | default false) { print $"(ansi red_bold)❌ Authentication Required(ansi reset)" print "" print $"Operation: (ansi cyan_bold)($operation)(ansi reset)" print $"You must be logged in to perform this operation." print "" print $"(ansi green_bold)To login:(ansi reset)" print $" provisioning auth login " print "" print $"(ansi yellow_bold)Note:(ansi reset) Your credentials will be securely stored in the system keyring." if ($auth_status | get message? | default null | is-not-empty) { print "" print $"(ansi red)Error:(ansi reset) ($auth_status.message)" } exit 1 } let username = ($auth_status | get username? | default "unknown") print $"(ansi green)✓(ansi reset) Authenticated as: (ansi cyan_bold)($username)(ansi reset)" true } # Require MFA verification with clear error messages export def require-mfa [ operation: string # Operation name for error messages reason: string # Reason MFA is required ] { let auth_status = (plugin-verify) if not ($auth_status | get mfa_verified? | default false) { print $"(ansi red_bold)❌ MFA Verification Required(ansi reset)" print "" print $"Operation: (ansi cyan_bold)($operation)(ansi reset)" print $"Reason: (ansi yellow)($reason)(ansi reset)" print "" print $"(ansi green_bold)To verify MFA:(ansi reset)" print $" 1. Get code from your authenticator app" print $" 2. Run: provisioning auth mfa verify --code <6-digit-code>" print "" print $"(ansi yellow_bold)Don't have MFA set up?(ansi reset)" print $" Run: provisioning auth mfa enroll totp" exit 1 } print $"(ansi green)✓(ansi reset) MFA verified" true } # Check authentication and MFA for production operations (enhanced with metadata) export def check-auth-for-production [ operation: string # Operation name --allow-skip # Allow skip-auth flag bypass ] { # First check if this command is actually production-related via metadata if (is-production-from-metadata $operation) { # Require authentication first require-auth $operation --allow-skip=$allow_skip # Check if MFA is required based on metadata or config let requires_mfa_metadata = (requires-mfa-from-metadata $operation) if $requires_mfa_metadata or (should-require-mfa-prod) { require-mfa $operation "production environment operation" } return true } # Fallback to configuration-based check if not in metadata if (should-require-mfa-prod) { require-auth $operation --allow-skip=$allow_skip require-mfa $operation "production environment operation" } true } # Check authentication and MFA for destructive operations (enhanced with metadata) export def check-auth-for-destructive [ operation: string # Operation name --allow-skip # Allow skip-auth flag bypass ] { # Check if this is a destructive operation via metadata if (is-destructive-from-metadata $operation) { # Always require authentication for destructive ops require-auth $operation --allow-skip=$allow_skip # Check if MFA is required based on metadata or config let requires_mfa_metadata = (requires-mfa-from-metadata $operation) if $requires_mfa_metadata or (should-require-mfa-destructive) { require-mfa $operation "destructive operation (delete/destroy)" } return true } # Fallback to configuration-based check if (should-require-mfa-destructive) { require-auth $operation --allow-skip=$allow_skip require-mfa $operation "destructive operation (delete/destroy)" } true } # Helper: Check if operation is in check mode (should skip auth) export def is-check-mode [flags: record] { (($flags | get check? | default false) or ($flags | get check_mode? | default false) or ($flags | get c? | default false)) } # Helper: Determine if operation is destructive export def is-destructive-operation [operation_type: string] { $operation_type in ["delete" "destroy" "remove"] } # Main authentication check for any operation (enhanced with metadata) export def check-operation-auth [ operation_name: string # Name of operation operation_type: string # Type: create, delete, modify, read flags?: record # Command flags ] { # Skip in check mode if ($flags | is-not-empty) and (is-check-mode $flags) { print $"(ansi dim)Skipping authentication check (check mode)(ansi reset)" return true } # Check metadata-driven auth enforcement first if (should-enforce-auth-from-metadata $operation_name) { let auth_reqs = (get-metadata-auth-requirements $operation_name) # Require authentication let allow_skip = (config-get "security.bypass.allow_skip_auth" false) require-auth $operation_name --allow-skip=$allow_skip # Check MFA based on auth_type from metadata if $auth_reqs.auth_type == "mfa" { require-mfa $operation_name $"MFA required for ($operation_name)" } else if $auth_reqs.auth_type == "cedar" { # Cedar policy evaluation would go here require-mfa $operation_name "Cedar policy verification required" } # Validate permission level if set let user_level = (config-get "security.user_permission_level" "read") if not (validate-permission-level $operation_name $user_level) { print $"(ansi red_bold)❌ Insufficient Permissions(ansi reset)" print $"Operation: (ansi cyan)($operation_name)(ansi reset)" print $"Required: (ansi yellow)($auth_reqs.min_permission)(ansi reset)" print $"Your level: (ansi yellow)($user_level)(ansi reset)" exit 1 } return true } # Skip if auth not required by configuration if not (should-require-auth) { return true } # Fallback to configuration-based checks let allow_skip = (config-get "security.bypass.allow_skip_auth" false) require-auth $operation_name --allow-skip=$allow_skip # Get environment let environment = (config-get "environment" "dev") # Check MFA requirements based on environment and operation type if $environment == "prod" and (should-require-mfa-prod) { require-mfa $operation_name "production environment" } else if (is-destructive-operation $operation_type) and (should-require-mfa-destructive) { require-mfa $operation_name "destructive operation" } true } # Get authentication metadata for audit logging export def get-auth-metadata [] { let auth_status = (plugin-verify) { authenticated: ($auth_status | get valid? | default false) mfa_verified: ($auth_status | get mfa_verified? | default false) username: ($auth_status | get username? | default "anonymous") timestamp: (date now | format date "%Y-%m-%d %H:%M:%S") } } # Log authenticated operation for audit trail export def log-authenticated-operation [ operation: string # Operation performed details: record # Operation details ] { let auth_metadata = (get-auth-metadata) let log_entry = { timestamp: $auth_metadata.timestamp user: $auth_metadata.username operation: $operation details: $details mfa_verified: $auth_metadata.mfa_verified } # Log to file if configured let log_path = (config-get "security.audit_log_path" "") if ($log_path | is-not-empty) { let log_dir = ($log_path | path dirname) if ($log_dir | path exists) { $log_entry | to json | save --append $log_path } } } # Print current authentication status (user-friendly) export def print-auth-status [] { let auth_status = (plugin-verify) let is_valid = ($auth_status | get valid? | default false) print $"(ansi blue_bold)Authentication Status(ansi reset)" print $"━━━━━━━━━━━━━━━━━━━━━━━━" if $is_valid { let username = ($auth_status | get username? | default "unknown") let mfa_verified = ($auth_status | get mfa_verified? | default false) print $"Status: (ansi green_bold)✓ Authenticated(ansi reset)" print $"User: (ansi cyan)($username)(ansi reset)" if $mfa_verified { print $"MFA: (ansi green_bold)✓ Verified(ansi reset)" } else { print $"MFA: (ansi yellow)Not verified(ansi reset)" } } else { print $"Status: (ansi red)✗ Not authenticated(ansi reset)" print "" print $"Run: (ansi green)provisioning auth login (ansi reset)" } print "" print $"(ansi dim)Authentication required:(ansi reset) (should-require-auth)" print $"(ansi dim)MFA for production:(ansi reset) (should-require-mfa-prod)" print $"(ansi dim)MFA for destructive:(ansi reset) (should-require-mfa-destructive)" } # ============================================================================ # TYPEDIALOG HELPER FUNCTIONS # ============================================================================ # Run TypeDialog form via bash wrapper for authentication # This pattern avoids TTY/input issues in Nushell's execution stack def run-typedialog-auth-form [ wrapper_script: string --backend: string = "tui" ] { # Check if the wrapper script exists if not ($wrapper_script | path exists) { return { success: false error: "TypeDialog wrapper not available" use_fallback: true } } # Set backend environment variable $env.TYPEDIALOG_BACKEND = $backend # Run bash wrapper (handles TTY input properly) let result = (do { bash $wrapper_script } | complete) if $result.exit_code != 0 { return { success: false error: $result.stderr use_fallback: true } } # Read the generated JSON file let json_output = ($wrapper_script | path dirname | path join "generated" | path join ($wrapper_script | path basename | str replace ".sh" "-result.json")) if not ($json_output | path exists) { return { success: false error: "Output file not found" use_fallback: true } } # Parse JSON output let result = do { open $json_output | from json } | complete if $result.exit_code == 0 { let values = $result.stdout { success: true values: $values use_fallback: false } } else { return { success: false error: "Failed to parse TypeDialog output" use_fallback: true } } } # ============================================================================ # INTERACTIVE FORM HANDLERS (TypeDialog Integration) # ============================================================================ # Interactive login with form export def login-interactive [ --backend: string = "tui" ] : nothing -> record { print "🔐 Interactive Authentication" print "" # Run the login form via bash wrapper let wrapper_script = "provisioning/core/shlib/auth-login-tty.sh" let form_result = (run-typedialog-auth-form $wrapper_script --backend $backend) # Fallback to basic prompts if TypeDialog not available if not $form_result.success or $form_result.use_fallback { print "ℹ️ TypeDialog not available. Using basic prompts..." print "" print "Username: " let username = (input) print "Password: " let password = (input --suppress-output) print "Do you have MFA enabled? (y/n): " let has_mfa_input = (input) let has_mfa = ($has_mfa_input == "y" or $has_mfa_input == "Y") let mfa_code = if $has_mfa { print "MFA Code (6 digits): " input } else { "" } if ($username | is-empty) or ($password | is-empty) { return { success: false error: "Username and password are required" } } let login_result = (plugin-login $username $password --mfa-code $mfa_code) return { success: true result: $login_result username: $username mfa_enabled: $has_mfa } } let form_values = $form_result.values # Check if user cancelled or didn't confirm if not ($form_values.auth?.confirm_login? | default false) { return { success: false error: "Login cancelled by user" } } # Perform login with provided credentials let username = ($form_values.auth?.username? | default "") let password = ($form_values.auth?.password? | default "") let has_mfa = ($form_values.auth?.has_mfa? | default false) let mfa_code = if $has_mfa { $form_values.auth?.mfa_code? | default "" } else { "" } if ($username | is-empty) or ($password | is-empty) { return { success: false error: "Username and password are required" } } # Call the plugin login function let login_result = (plugin-login $username $password --mfa-code $mfa_code) { success: true result: $login_result username: $username mfa_enabled: $has_mfa } } # Interactive MFA enrollment with form export def mfa-enroll-interactive [ --backend: string = "tui" ] : nothing -> record { print "🔐 Multi-Factor Authentication Setup" print "" # Check if user is already authenticated let auth_status = (plugin-verify) let is_authenticated = ($auth_status.valid // false) if not $is_authenticated { return { success: false error: "Must be authenticated to enroll in MFA. Please login first." } } # Run the MFA enrollment form via bash wrapper let wrapper_script = "provisioning/core/shlib/mfa-enroll-tty.sh" let form_result = (run-typedialog-auth-form $wrapper_script --backend $backend) # Fallback to basic prompts if TypeDialog not available if not $form_result.success or $form_result.use_fallback { print "ℹ️ TypeDialog not available. Using basic prompts..." print "" print "MFA Type (totp/webauthn/sms): " let mfa_type = (input) let device_name = if ($mfa_type == "totp" or $mfa_type == "webauthn") { print "Device name: " input } else if $mfa_type == "sms" { "" } else { "" } let phone_number = if $mfa_type == "sms" { print "Phone number (international format, e.g., +1234567890): " input } else { "" } let verification_code = if ($mfa_type == "totp" or $mfa_type == "sms") { print "Verification code (6 digits): " input } else { "" } print "Generate backup codes? (y/n): " let generate_backup_input = (input) let generate_backup = ($generate_backup_input == "y" or $generate_backup_input == "Y") let backup_count = if $generate_backup { print "Number of backup codes (5-20): " let count_str = (input) $count_str | into int | default 10 } else { 0 } return { success: true mfa_type: $mfa_type device_name: $device_name phone_number: $phone_number verification_code: $verification_code generate_backup_codes: $generate_backup backup_codes_count: $backup_count } } let form_values = $form_result.values # Check if user confirmed if not ($form_values.mfa?.confirm_enroll? | default false) { return { success: false error: "MFA enrollment cancelled by user" } } # Extract MFA type and parameters from form values let mfa_type = ($form_values.mfa?.type? | default "totp") let device_name = if $mfa_type == "totp" { $form_values.mfa?.totp?.device_name? | default "Authenticator App" } else if $mfa_type == "webauthn" { $form_values.mfa?.webauthn?.device_name? | default "Security Key" } else if $mfa_type == "sms" { "" } else { "" } let phone_number = if $mfa_type == "sms" { $form_values.mfa?.sms?.phone_number? | default "" } else { "" } let verification_code = if $mfa_type == "totp" { $form_values.mfa?.totp?.verification_code? | default "" } else if $mfa_type == "sms" { $form_values.mfa?.sms?.verification_code? | default "" } else { "" } let generate_backup = ($form_values.mfa?.generate_backup_codes? | default true) let backup_count = ($form_values.mfa?.backup_codes_count? | default 10) # Call the plugin MFA enrollment function let enroll_result = (plugin-mfa-enroll --type $mfa_type) { success: true result: $enroll_result mfa_type: $mfa_type device_name: $device_name phone_number: $phone_number verification_code: $verification_code generate_backup_codes: $generate_backup backup_codes_count: $backup_count } }