- Documented Fluent-based i18n system with locale detection - Bumped version from 1.0.10 to 1.0.11
1067 lines
31 KiB
Plaintext
1067 lines
31 KiB
Plaintext
#!/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 <username>"
|
||
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 <username>(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
|
||
}
|
||
}
|