Add SSH cert flows and user service installer
This commit is contained in:
parent
26f81ff1ef
commit
f302342b1c
21 changed files with 2158 additions and 14 deletions
|
|
@ -2,6 +2,7 @@ use anyhow::{Context, Result, bail};
|
|||
use clap::{Args, Parser, Subcommand};
|
||||
use geth_config::GethPaths;
|
||||
use geth_control::{ControlRequest, ControlResponse};
|
||||
use geth_node::service::{ServiceInstallOptions, ServiceManager, ServiceReport};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
|
|
@ -76,6 +77,44 @@ pub enum Command {
|
|||
#[derive(Debug, Subcommand)]
|
||||
pub enum DaemonCommand {
|
||||
Run,
|
||||
Service {
|
||||
#[command(subcommand)]
|
||||
command: ServiceCommand,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum ServiceCommand {
|
||||
Install {
|
||||
#[arg(long, default_value = "auto")]
|
||||
manager: String,
|
||||
#[arg(long)]
|
||||
bin: Option<PathBuf>,
|
||||
#[arg(long)]
|
||||
start: bool,
|
||||
},
|
||||
Uninstall {
|
||||
#[arg(long, default_value = "auto")]
|
||||
manager: String,
|
||||
},
|
||||
Start {
|
||||
#[arg(long, default_value = "auto")]
|
||||
manager: String,
|
||||
},
|
||||
Stop {
|
||||
#[arg(long, default_value = "auto")]
|
||||
manager: String,
|
||||
},
|
||||
Status {
|
||||
#[arg(long, default_value = "auto")]
|
||||
manager: String,
|
||||
},
|
||||
Print {
|
||||
#[arg(long, default_value = "auto")]
|
||||
manager: String,
|
||||
#[arg(long)]
|
||||
bin: Option<PathBuf>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
|
|
@ -171,7 +210,68 @@ pub enum DocumentCommand {
|
|||
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum SshCommand {
|
||||
Proxy { node: String },
|
||||
Proxy {
|
||||
node: String,
|
||||
},
|
||||
Cert {
|
||||
#[command(subcommand)]
|
||||
command: SshCertCommand,
|
||||
},
|
||||
Revocation {
|
||||
#[command(subcommand)]
|
||||
command: SshRevocationCommand,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum SshCertCommand {
|
||||
Request {
|
||||
#[arg(long)]
|
||||
public_key: PathBuf,
|
||||
#[arg(long, default_value = "user")]
|
||||
kind: String,
|
||||
#[arg(long = "principal", required = true)]
|
||||
principals: Vec<String>,
|
||||
#[arg(long)]
|
||||
valid_for: Option<String>,
|
||||
#[arg(long)]
|
||||
renewal_of: Option<String>,
|
||||
#[arg(long)]
|
||||
reason: Option<String>,
|
||||
},
|
||||
Requests,
|
||||
Approve {
|
||||
request_id: String,
|
||||
#[arg(long)]
|
||||
ca_key: PathBuf,
|
||||
#[arg(long)]
|
||||
valid_for: Option<String>,
|
||||
#[arg(long)]
|
||||
serial: Option<u64>,
|
||||
#[arg(long)]
|
||||
out: Option<PathBuf>,
|
||||
},
|
||||
Import {
|
||||
request_id: String,
|
||||
#[arg(long)]
|
||||
cert: PathBuf,
|
||||
},
|
||||
List,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum SshRevocationCommand {
|
||||
Add {
|
||||
kind: String,
|
||||
target: String,
|
||||
#[arg(long)]
|
||||
reason: Option<String>,
|
||||
},
|
||||
List,
|
||||
Export {
|
||||
#[arg(long)]
|
||||
out: PathBuf,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
|
|
@ -194,6 +294,13 @@ pub async fn run() -> Result<()> {
|
|||
.await
|
||||
.context("run geth daemon")?;
|
||||
}
|
||||
Command::Daemon {
|
||||
command: DaemonCommand::Service { command },
|
||||
} => {
|
||||
let report =
|
||||
run_service_command(&paths, command).context("manage geth user service")?;
|
||||
print_service_report(report, cli.json || cli.jsonl)?;
|
||||
}
|
||||
command => {
|
||||
let request = request_for_command(command)?;
|
||||
let response = geth_node::send_control(&paths, request)
|
||||
|
|
@ -277,14 +384,113 @@ fn request_for_command(command: Command) -> Result<ControlRequest> {
|
|||
module: "document".to_owned(),
|
||||
command: format!("{command:?}"),
|
||||
},
|
||||
Command::Ssh { command } => ControlRequest::ModuleStub {
|
||||
module: "ssh-proxy".to_owned(),
|
||||
command: format!("{command:?}"),
|
||||
Command::Ssh { command } => match command {
|
||||
SshCommand::Proxy { node } => ControlRequest::ModuleStub {
|
||||
module: "ssh-proxy".to_owned(),
|
||||
command: format!("proxy {node}"),
|
||||
},
|
||||
SshCommand::Cert { command } => match command {
|
||||
SshCertCommand::Request {
|
||||
public_key,
|
||||
kind,
|
||||
principals,
|
||||
valid_for,
|
||||
renewal_of,
|
||||
reason,
|
||||
} => ControlRequest::SshCertRequest {
|
||||
public_key_path: public_key,
|
||||
cert_kind: kind,
|
||||
principals,
|
||||
requested_validity: valid_for,
|
||||
renewal_of,
|
||||
reason,
|
||||
},
|
||||
SshCertCommand::Requests => ControlRequest::SshCertRequests,
|
||||
SshCertCommand::Approve {
|
||||
request_id,
|
||||
ca_key,
|
||||
valid_for,
|
||||
serial,
|
||||
out,
|
||||
} => ControlRequest::SshCertApprove {
|
||||
request_id,
|
||||
ca_key_path: ca_key,
|
||||
valid_for,
|
||||
serial,
|
||||
out,
|
||||
},
|
||||
SshCertCommand::Import { request_id, cert } => ControlRequest::SshCertImport {
|
||||
request_id,
|
||||
cert_path: cert,
|
||||
},
|
||||
SshCertCommand::List => ControlRequest::SshCertList,
|
||||
},
|
||||
SshCommand::Revocation { command } => match command {
|
||||
SshRevocationCommand::Add {
|
||||
kind,
|
||||
target,
|
||||
reason,
|
||||
} => ControlRequest::SshRevocationAdd {
|
||||
kind,
|
||||
target,
|
||||
reason,
|
||||
},
|
||||
SshRevocationCommand::List => ControlRequest::SshRevocationList,
|
||||
SshRevocationCommand::Export { out } => ControlRequest::SshRevocationExport { out },
|
||||
},
|
||||
},
|
||||
Command::Init | Command::Daemon { .. } => bail!("command is handled directly"),
|
||||
})
|
||||
}
|
||||
|
||||
fn run_service_command(paths: &GethPaths, command: ServiceCommand) -> Result<ServiceReport> {
|
||||
Ok(match command {
|
||||
ServiceCommand::Install {
|
||||
manager,
|
||||
bin,
|
||||
start,
|
||||
} => {
|
||||
geth_node::init_node(paths).context("initialize geth home before service install")?;
|
||||
let manager = manager.parse::<ServiceManager>()?;
|
||||
let executable = service_executable(bin)?;
|
||||
geth_node::service::install_user_service(
|
||||
paths,
|
||||
ServiceInstallOptions {
|
||||
manager,
|
||||
executable,
|
||||
start,
|
||||
},
|
||||
)?
|
||||
}
|
||||
ServiceCommand::Uninstall { manager } => {
|
||||
geth_node::service::uninstall_user_service(manager.parse::<ServiceManager>()?)?
|
||||
}
|
||||
ServiceCommand::Start { manager } => {
|
||||
geth_node::service::start_user_service(manager.parse::<ServiceManager>()?)?
|
||||
}
|
||||
ServiceCommand::Stop { manager } => {
|
||||
geth_node::service::stop_user_service(manager.parse::<ServiceManager>()?)?
|
||||
}
|
||||
ServiceCommand::Status { manager } => {
|
||||
geth_node::service::status_user_service(manager.parse::<ServiceManager>()?)?
|
||||
}
|
||||
ServiceCommand::Print { manager, bin } => {
|
||||
let executable = service_executable(bin)?;
|
||||
geth_node::service::print_user_service(
|
||||
paths,
|
||||
manager.parse::<ServiceManager>()?,
|
||||
&executable,
|
||||
)?
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn service_executable(bin: Option<PathBuf>) -> Result<PathBuf> {
|
||||
bin.map(Ok)
|
||||
.unwrap_or_else(std::env::current_exe)
|
||||
.context("resolve current geth executable")
|
||||
}
|
||||
|
||||
fn print_response(response: ControlResponse, json: bool) -> Result<()> {
|
||||
if json {
|
||||
println!("{}", serde_json::to_string_pretty(&response)?);
|
||||
|
|
@ -356,6 +562,103 @@ fn print_response(response: ControlResponse, json: bool) -> Result<()> {
|
|||
println!("reason: {}", explain.reason);
|
||||
println!("evaluated_ops: {}", explain.evaluated_ops);
|
||||
}
|
||||
ControlResponse::SshCertRequested { request } => {
|
||||
println!("ssh cert request: {}", request.id);
|
||||
println!("status: {}", request.status);
|
||||
println!("kind: {}", request.cert_kind);
|
||||
println!("principals: {}", request.principals.join(","));
|
||||
println!("public_key_fingerprint: {}", request.public_key_fingerprint);
|
||||
}
|
||||
ControlResponse::SshCertRequests { requests } => {
|
||||
if requests.is_empty() {
|
||||
println!("no ssh certificate requests");
|
||||
} else {
|
||||
for request in requests {
|
||||
println!(
|
||||
"{}\t{}\t{}\t{}\t{}",
|
||||
request.id,
|
||||
request.status,
|
||||
request.cert_kind,
|
||||
request.principals.join(","),
|
||||
request.public_key_fingerprint
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
ControlResponse::SshCertApproved { approval } => {
|
||||
println!("approved ssh cert request: {}", approval.request_id);
|
||||
println!("valid_for: {}", approval.valid_for);
|
||||
if let Some(serial) = approval.serial {
|
||||
println!("serial: {serial}");
|
||||
}
|
||||
if let Some(output_path) = approval.output_path {
|
||||
println!("expected_certificate: {output_path}");
|
||||
}
|
||||
println!("signing_command:");
|
||||
println!("{}", shell_quote_command(&approval.signing_command));
|
||||
println!("note: {}", approval.note);
|
||||
}
|
||||
ControlResponse::SshCertImported { certificate } => {
|
||||
println!("imported ssh certificate: {}", certificate.id);
|
||||
println!("request: {}", certificate.request_id);
|
||||
println!("fingerprint: {}", certificate.certificate_fingerprint);
|
||||
}
|
||||
ControlResponse::SshCertList {
|
||||
requests,
|
||||
certificates,
|
||||
} => {
|
||||
println!("requests:");
|
||||
if requests.is_empty() {
|
||||
println!(" none");
|
||||
} else {
|
||||
for request in requests {
|
||||
println!(
|
||||
" {}\t{}\t{}\t{}",
|
||||
request.id,
|
||||
request.status,
|
||||
request.cert_kind,
|
||||
request.principals.join(",")
|
||||
);
|
||||
}
|
||||
}
|
||||
println!("certificates:");
|
||||
if certificates.is_empty() {
|
||||
println!(" none");
|
||||
} else {
|
||||
for certificate in certificates {
|
||||
println!(
|
||||
" {}\t{}\t{}",
|
||||
certificate.id, certificate.request_id, certificate.certificate_fingerprint
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
ControlResponse::SshRevocationAdded { revocation } => {
|
||||
println!("added ssh revocation: {}", revocation.id);
|
||||
println!("kind: {}", revocation.kind);
|
||||
println!("target: {}", revocation.target);
|
||||
if let Some(reason) = revocation.reason {
|
||||
println!("reason: {reason}");
|
||||
}
|
||||
}
|
||||
ControlResponse::SshRevocationList { revocations } => {
|
||||
if revocations.is_empty() {
|
||||
println!("no ssh revocations");
|
||||
} else {
|
||||
for revocation in revocations {
|
||||
println!(
|
||||
"{}\t{}\t{}\t{}",
|
||||
revocation.id,
|
||||
revocation.kind,
|
||||
revocation.target,
|
||||
revocation.reason.unwrap_or_default()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
ControlResponse::SshRevocationExported { out, count } => {
|
||||
println!("exported {count} ssh revocations to {}", out.display());
|
||||
}
|
||||
ControlResponse::NotImplemented { module, command } => {
|
||||
println!("{module} {command}: not implemented yet");
|
||||
}
|
||||
|
|
@ -363,3 +666,57 @@ fn print_response(response: ControlResponse, json: bool) -> Result<()> {
|
|||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_service_report(report: ServiceReport, json: bool) -> Result<()> {
|
||||
if json {
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::json!({
|
||||
"manager": report.manager.to_string(),
|
||||
"action": format!("{:?}", report.action),
|
||||
"service_name": report.service_name,
|
||||
"definition_path": report.definition_path,
|
||||
"definition": report.definition,
|
||||
"commands": report.commands,
|
||||
"note": report.note,
|
||||
})
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("service: {}", report.service_name);
|
||||
println!("manager: {}", report.manager);
|
||||
println!("action: {:?}", report.action);
|
||||
if let Some(path) = report.definition_path {
|
||||
println!("definition: {}", path.display());
|
||||
}
|
||||
if !report.commands.is_empty() {
|
||||
println!("commands:");
|
||||
for command in report.commands {
|
||||
println!(" {}", shell_quote_command(&command));
|
||||
}
|
||||
}
|
||||
if let Some(definition) = report.definition {
|
||||
println!("definition_body:");
|
||||
print!("{definition}");
|
||||
}
|
||||
println!("note: {}", report.note);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn shell_quote_command(command: &[String]) -> String {
|
||||
command
|
||||
.iter()
|
||||
.map(|arg| {
|
||||
if arg
|
||||
.bytes()
|
||||
.all(|byte| byte.is_ascii_alphanumeric() || b"-_./:=+@,".contains(&byte))
|
||||
{
|
||||
arg.clone()
|
||||
} else {
|
||||
format!("'{}'", arg.replace('\'', "'\\''"))
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue