520 lines
16 KiB
Rust
Raw Normal View History

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<Box<dyn PluginCommand<Plugin = Self>>> {
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<Example<'_>> {
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<Value, LabeledError> {
let username: String = call.req(0)?;
let password_arg: Option<String> = call.opt(1)?;
let url = call
.get_flag::<String>("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<Example<'_>> {
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<Value, LabeledError> {
let username_arg: Option<String> = call.get_flag("user")?;
let _all_sessions = call.has_flag("all")?;
let url = call
.get_flag::<String>("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<Example<'_>> {
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<Value, LabeledError> {
let _token: Option<String> = 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<Example<'_>> {
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<Value, LabeledError> {
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<Example<'_>> {
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<Value, LabeledError> {
let mfa_type: String = call.req(0)?;
let username = call
.get_flag::<String>("user")?
.unwrap_or_else(|| std::env::var("USER").unwrap_or("default".to_string()));
let url = call
.get_flag::<String>("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<Example<'_>> {
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<Value, LabeledError> {
let code = call
.get_flag::<String>("code")?
.ok_or_else(|| LabeledError::new("--code required"))?;
let username = call
.get_flag::<String>("user")?
.unwrap_or_else(|| std::env::var("USER").unwrap_or("default".to_string()));
let url = call
.get_flag::<String>("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);
}