893 lines
27 KiB
Plaintext
Raw Permalink Normal View History

#!/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 <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
]: 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 <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)"
}
# ============================================================================
# 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)
}
}