use nu_plugin::{ serve_plugin, EngineInterface, EvaluatedCall, MsgPackSerializer, Plugin, PluginCommand, SimplePluginCommand, }; use nu_protocol::{record, Category, Example, LabeledError, Signature, SyntaxShape, Type, Value}; mod helpers; #[cfg(test)] mod tests; /// Nushell plugin for provisioning authentication (JWT, MFA) pub struct AuthPlugin; impl Plugin for AuthPlugin { /// Returns the plugin version from Cargo.toml fn version(&self) -> String { env!("CARGO_PKG_VERSION").into() } /// Returns the list of commands provided by this plugin fn commands(&self) -> Vec>> { vec![ Box::new(Login), Box::new(Logout), Box::new(Verify), Box::new(Sessions), Box::new(MfaEnroll), Box::new(MfaVerify), ] } } /// Login command - Authenticate with the provisioning platform pub struct Login; impl SimplePluginCommand for Login { type Plugin = AuthPlugin; fn name(&self) -> &str { "auth login" } fn signature(&self) -> Signature { Signature::build(PluginCommand::name(self)) .input_output_type(Type::Nothing, Type::Record(vec![].into())) .required("username", SyntaxShape::String, "Username for login") .optional( "password", SyntaxShape::String, "Password (will prompt if omitted)", ) .named( "url", SyntaxShape::String, "Control center URL (default: http://localhost:8081)", None, ) .switch("save", "Save credentials to secure keyring", None) .category(Category::Custom("provisioning".into())) } fn description(&self) -> &str { "Login to provisioning platform with JWT authentication" } fn examples(&self) -> Vec> { vec![ Example { example: "auth login admin", description: "Login as admin (password prompt)", result: None, }, Example { example: "auth login admin mypassword", description: "Login with password in command", result: None, }, Example { example: "auth login admin --url http://control.example.com:8081", description: "Login to custom control center URL", result: None, }, Example { example: "auth login admin --save", description: "Login and save credentials to keyring", result: None, }, ] } fn run( &self, _plugin: &AuthPlugin, _engine: &EngineInterface, call: &EvaluatedCall, _input: &Value, ) -> Result { let username: String = call.req(0)?; let password_arg: Option = call.opt(1)?; let url = call .get_flag::("url")? .unwrap_or("http://localhost:8081".to_string()); let save_token = call.has_flag("save")?; // Get password (from arg or prompt) let password = if let Some(pwd) = password_arg { pwd } else { helpers::prompt_password("Password: ") .map_err(|e| LabeledError::new(format!("Password input failed: {}", e)))? }; // Send login request let token_response = helpers::send_login_request(&url, &username, &password) .map_err(|e| LabeledError::new(format!("Login failed: {}", e)))?; // Store tokens in keyring if requested if save_token { helpers::store_tokens_in_keyring( &username, &token_response.access_token, &token_response.refresh_token, ) .map_err(|e| LabeledError::new(format!("Failed to save tokens: {}", e)))?; } // Return success response Ok(Value::record( record! { "success" => Value::bool(true, call.head), "user" => Value::record(record! { "id" => Value::string(&token_response.user.id, call.head), "username" => Value::string(&token_response.user.username, call.head), "email" => Value::string(&token_response.user.email, call.head), "roles" => Value::list( token_response.user.roles.iter() .map(|r| Value::string(r, call.head)) .collect(), call.head ), }, call.head), "expires_in" => Value::int(token_response.expires_in, call.head), "token_saved" => Value::bool(save_token, call.head), }, call.head, )) } } /// Logout command - Remove authentication session pub struct Logout; impl SimplePluginCommand for Logout { type Plugin = AuthPlugin; fn name(&self) -> &str { "auth logout" } fn signature(&self) -> Signature { Signature::build(PluginCommand::name(self)) .input_output_type(Type::Nothing, Type::Record(vec![].into())) .named( "user", SyntaxShape::String, "Username (defaults to current user)", Some('u'), ) .named("url", SyntaxShape::String, "Control Center URL", None) .switch("all", "Logout from all active sessions", Some('a')) .category(Category::Custom("provisioning".into())) } fn description(&self) -> &str { "Logout from provisioning platform" } fn examples(&self) -> Vec> { vec![ Example { example: "auth logout", description: "Logout from current session", result: None, }, Example { example: "auth logout --all", description: "Logout from all active sessions", result: None, }, ] } fn run( &self, _plugin: &AuthPlugin, _engine: &EngineInterface, call: &EvaluatedCall, _input: &Value, ) -> Result { let username_arg: Option = call.get_flag("user")?; let _all_sessions = call.has_flag("all")?; let url = call .get_flag::("url")? .unwrap_or("http://localhost:8081".to_string()); // Get username (from flag or current user) let username = if let Some(user) = username_arg { user } else { std::env::var("USER").unwrap_or("default".to_string()) }; // Get access token let access_token = helpers::get_access_token(&username) .map_err(|e| LabeledError::new(format!("No active session: {}", e)))?; // Send logout request helpers::send_logout_request(&url, &access_token) .map_err(|e| LabeledError::new(format!("Logout failed: {}", e)))?; // Remove tokens from keyring helpers::remove_tokens_from_keyring(&username) .map_err(|e| LabeledError::new(format!("Failed to remove tokens: {}", e)))?; Ok(Value::record( record! { "success" => Value::bool(true, call.head), "message" => Value::string("Logged out successfully", call.head), "user" => Value::string(&username, call.head), }, call.head, )) } } /// Verify token command - Check authentication status pub struct Verify; impl SimplePluginCommand for Verify { type Plugin = AuthPlugin; fn name(&self) -> &str { "auth verify" } fn signature(&self) -> Signature { Signature::build(PluginCommand::name(self)) .input_output_type(Type::Nothing, Type::Record(vec![].into())) .named( "token", SyntaxShape::String, "Token to verify (uses stored token if omitted)", None, ) .category(Category::Custom("provisioning".into())) } fn description(&self) -> &str { "Verify current authentication token" } fn examples(&self) -> Vec> { vec![ Example { example: "auth verify", description: "Verify stored authentication token", result: None, }, Example { example: "auth verify --token eyJhbGc...", description: "Verify specific token", result: None, }, ] } fn run( &self, _plugin: &AuthPlugin, _engine: &EngineInterface, call: &EvaluatedCall, _input: &Value, ) -> Result { let _token: Option = call.get_flag("token")?; // Placeholder - will be implemented by Agente 4 Ok(Value::record( record! { "valid" => Value::bool(true, call.head), "message" => Value::string("Verify placeholder - to be implemented", call.head), }, call.head, )) } } /// Sessions command - List active authentication sessions pub struct Sessions; impl SimplePluginCommand for Sessions { type Plugin = AuthPlugin; fn name(&self) -> &str { "auth sessions" } fn signature(&self) -> Signature { Signature::build(PluginCommand::name(self)) .input_output_type( Type::Nothing, Type::List(Box::new(Type::Record(vec![].into()))), ) .switch("active", "Show only active sessions", None) .category(Category::Custom("provisioning".into())) } fn description(&self) -> &str { "List active authentication sessions" } fn examples(&self) -> Vec> { vec![ Example { example: "auth sessions", description: "List all sessions", result: None, }, Example { example: "auth sessions --active", description: "List only active sessions", result: None, }, ] } fn run( &self, _plugin: &AuthPlugin, _engine: &EngineInterface, call: &EvaluatedCall, _input: &Value, ) -> Result { let _active: bool = call.has_flag("active")?; // Placeholder - will be implemented by Agente 5 Ok(Value::list(vec![], call.head)) } } /// MFA Enrollment command pub struct MfaEnroll; impl SimplePluginCommand for MfaEnroll { type Plugin = AuthPlugin; fn name(&self) -> &str { "auth mfa enroll" } fn signature(&self) -> Signature { Signature::build(PluginCommand::name(self)) .input_output_type(Type::Nothing, Type::Record(vec![].into())) .required("type", SyntaxShape::String, "MFA type: totp or webauthn") .named("user", SyntaxShape::String, "Username", Some('u')) .named("url", SyntaxShape::String, "Control Center URL", None) .category(Category::Custom("provisioning".into())) } fn description(&self) -> &str { "Enroll in MFA (TOTP or WebAuthn)" } fn examples(&self) -> Vec> { vec![ Example { example: "auth mfa enroll totp", description: "Enroll TOTP (Google Authenticator, Authy)", result: None, }, Example { example: "auth mfa enroll webauthn", description: "Enroll WebAuthn (YubiKey, Touch ID)", result: None, }, Example { example: "auth mfa enroll totp --user alice", description: "Enroll TOTP for specific user", result: None, }, ] } fn run( &self, _plugin: &AuthPlugin, _engine: &EngineInterface, call: &EvaluatedCall, _input: &Value, ) -> Result { let mfa_type: String = call.req(0)?; let username = call .get_flag::("user")? .unwrap_or_else(|| std::env::var("USER").unwrap_or("default".to_string())); let url = call .get_flag::("url")? .unwrap_or("http://localhost:3000".to_string()); // Get access token let access_token = helpers::get_access_token(&username) .map_err(|e| LabeledError::new(format!("Not logged in: {}", e)))?; // Send enrollment request let response = helpers::send_mfa_enroll_request(&url, &access_token, &mfa_type) .map_err(|e| LabeledError::new(format!("MFA enrollment failed: {}", e)))?; // Display QR code if TOTP if mfa_type == "totp" { helpers::display_qr_code(&response.qr_code_uri) .map_err(|e| LabeledError::new(format!("QR display failed: {}", e)))?; } Ok(Value::record( record! { "success" => Value::bool(true, call.head), "mfa_type" => Value::string(&mfa_type, call.head), "secret" => Value::string(&response.secret, call.head), "backup_codes" => Value::list( response.backup_codes.iter() .map(|c| Value::string(c, call.head)) .collect(), call.head ), }, call.head, )) } } /// MFA Verify command pub struct MfaVerify; impl SimplePluginCommand for MfaVerify { type Plugin = AuthPlugin; fn name(&self) -> &str { "auth mfa verify" } fn signature(&self) -> Signature { Signature::build(PluginCommand::name(self)) .input_output_type(Type::Nothing, Type::Record(vec![].into())) .named("code", SyntaxShape::String, "6-digit TOTP code", Some('c')) .named("user", SyntaxShape::String, "Username", Some('u')) .named("url", SyntaxShape::String, "Control Center URL", None) .category(Category::Custom("provisioning".into())) } fn description(&self) -> &str { "Verify MFA code" } fn examples(&self) -> Vec> { vec![ Example { example: "auth mfa verify --code 123456", description: "Verify TOTP code", result: None, }, Example { example: "auth mfa verify --code 123456 --user alice", description: "Verify TOTP code for specific user", result: None, }, ] } fn run( &self, _plugin: &AuthPlugin, _engine: &EngineInterface, call: &EvaluatedCall, _input: &Value, ) -> Result { let code = call .get_flag::("code")? .ok_or_else(|| LabeledError::new("--code required"))?; let username = call .get_flag::("user")? .unwrap_or_else(|| std::env::var("USER").unwrap_or("default".to_string())); let url = call .get_flag::("url")? .unwrap_or("http://localhost:3000".to_string()); // Get access token let access_token = helpers::get_access_token(&username) .map_err(|e| LabeledError::new(format!("Not logged in: {}", e)))?; // Verify code let valid = helpers::send_mfa_verify_request(&url, &access_token, &code) .map_err(|e| LabeledError::new(format!("MFA verification failed: {}", e)))?; Ok(Value::record( record! { "valid" => Value::bool(valid, call.head), "message" => Value::string( if valid { "MFA verified" } else { "Invalid code" }, call.head ), }, call.head, )) } } /// Entry point for the plugin binary fn main() { serve_plugin(&AuthPlugin, MsgPackSerializer); }