#!/usr/bin/env nu # [command] # name = "auth login" # group = "authentication" # tags = ["authentication", "jwt", "interactive", "login"] # version = "2.1.0" # requires = ["forminquire.nu:1.0.0", "nushell:0.109.0"] # note = "Migrated to FormInquire interactive forms for login and MFA enrollment" # Authentication Plugin Wrapper with HTTP Fallback # Provides graceful degradation to HTTP API when nu_plugin_auth is unavailable use ../config/accessor.nu * use ../../../forminquire/nulib/forminquire.nu * use ../commands/traits.nu * # Check if auth plugin is available def is-plugin-available []: nothing -> bool { (which auth | length) > 0 } # Check if auth plugin is enabled in config def is-plugin-enabled []: nothing -> bool { config-get "plugins.auth_enabled" true } # Get control center base URL def get-control-center-url []: nothing -> string { config-get "platform.control_center.url" "http://localhost:3000" } # Store token in OS keyring (requires plugin) def store-token-keyring [ token: string ]: nothing -> nothing { 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 []: nothing -> string { if (is-plugin-available) { auth get-token } else { "" } } # Helper to safely execute a closure and return null on error def try-plugin [callback: closure]: nothing -> any { 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 []: nothing -> record { 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") ]: nothing -> record { 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 ]: nothing -> bool { 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 ]: nothing -> bool { 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 ]: nothing -> bool { 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) ]: nothing -> bool { 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 ]: nothing -> bool { 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 []: nothing -> bool { 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 []: nothing -> bool { 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 []: nothing -> bool { (config-get "security.require_mfa_for_destructive" true) } # Check if user is authenticated export def is-authenticated []: nothing -> bool { let result = (plugin-verify) ($result | get valid? | default false) } # Check if MFA is verified export def is-mfa-verified []: nothing -> bool { let result = (plugin-verify) ($result | get mfa_verified? | default false) } # Get current authenticated user export def get-authenticated-user []: nothing -> string { 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 ]: nothing -> bool { # 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 ]: nothing -> bool { 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 ]: nothing -> bool { # 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 ]: nothing -> bool { # 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]: nothing -> bool { (($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]: nothing -> bool { $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 ]: nothing -> bool { # 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 []: nothing -> record { 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 ]: nothing -> nothing { 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 []: nothing -> nothing { 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)" } # ============================================================================ # INTERACTIVE FORM HANDLERS (FormInquire Integration) # ============================================================================ # Interactive login with form export def login-interactive [] : nothing -> record { print "🔐 Interactive Authentication" print "" # Run the login form let form_result = (run-forminquire-form "provisioning/core/shlib/forms/authentication/auth_login.toml") if not $form_result.success { return { success: false error: $form_result.error } } let form_values = $form_result.values # Check if user cancelled or didn't confirm if not ($form_values.confirm_login // false) { return { success: false error: "Login cancelled by user" } } # Perform login with provided credentials let username = ($form_values.username // "") let password = ($form_values.password // "") let mfa_code = (if ($form_values.has_mfa // false) { $form_values.mfa_code // "" } 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: ($form_values.has_mfa // false) } } # Interactive MFA enrollment with form export def mfa-enroll-interactive [] : 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 let form_result = (run-forminquire-form "provisioning/core/shlib/forms/authentication/mfa_enroll.toml") if not $form_result.success { return { success: false error: $form_result.error } } let form_values = $form_result.values # Check if user confirmed if not ($form_values.confirm_enroll // false) { return { success: false error: "MFA enrollment cancelled by user" } } # Determine MFA type and parameters let mfa_type = if ($form_values.mfa_type | str contains "TOTP") { "totp" } else { "webauthn" } # Call the plugin MFA enrollment function let enroll_result = (plugin-mfa-enroll --type $mfa_type) { success: true result: $enroll_result mfa_type: $mfa_type backup_codes_saved: ($form_values.totp_backups // false) } }