520 lines
16 KiB
Rust
520 lines
16 KiB
Rust
|
|
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);
|
||
|
|
}
|