support global ssh keys
This commit is contained in:
parent
9ca4e87eb7
commit
e684b81660
10 changed files with 230 additions and 38 deletions
3
Cargo.lock
generated
3
Cargo.lock
generated
|
@ -1812,10 +1812,13 @@ dependencies = [
|
||||||
"figment",
|
"figment",
|
||||||
"futures",
|
"futures",
|
||||||
"hickory-proto",
|
"hickory-proto",
|
||||||
|
"lazy_static",
|
||||||
"log",
|
"log",
|
||||||
|
"regex",
|
||||||
"serde",
|
"serde",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"tarpc",
|
"tarpc",
|
||||||
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
|
@ -7,7 +7,7 @@ edition = "2021"
|
||||||
nzr-api = { path = "../nzr-api" }
|
nzr-api = { path = "../nzr-api" }
|
||||||
clap = { version = "4.0.26", features = ["derive"] }
|
clap = { version = "4.0.26", features = ["derive"] }
|
||||||
home = "0.5.4"
|
home = "0.5.4"
|
||||||
tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] }
|
tokio = { version = "1.0", features = ["fs", "macros", "rt-multi-thread"] }
|
||||||
tokio-serde = { version = "0.9", features = ["bincode"] }
|
tokio-serde = { version = "0.9", features = ["bincode"] }
|
||||||
tabled = "0.15"
|
tabled = "0.15"
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
|
|
@ -123,6 +123,16 @@ enum NetCmd {
|
||||||
Dump { name: Option<String> },
|
Dump { name: Option<String> },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Subcommand)]
|
||||||
|
enum KeyCmd {
|
||||||
|
/// Add a new SSH key
|
||||||
|
Add { path: PathBuf },
|
||||||
|
/// List SSH keys
|
||||||
|
List,
|
||||||
|
/// Delete an SSH key
|
||||||
|
Delete { id: i32 },
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Subcommand)]
|
#[derive(Debug, Subcommand)]
|
||||||
enum Commands {
|
enum Commands {
|
||||||
/// Commands for managing instances
|
/// Commands for managing instances
|
||||||
|
@ -135,6 +145,11 @@ enum Commands {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: NetCmd,
|
command: NetCmd,
|
||||||
},
|
},
|
||||||
|
/// Commands for managing SSH public keys
|
||||||
|
SshKey {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: KeyCmd,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
|
@ -215,39 +230,6 @@ async fn handle_command() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
InstanceCmd::New(args) => {
|
InstanceCmd::New(args) => {
|
||||||
/*
|
|
||||||
let ssh_keys: Vec<String> = {
|
|
||||||
let key_file = args.sshkey_file.map_or_else(
|
|
||||||
|| {
|
|
||||||
home::home_dir().map_or_else(
|
|
||||||
|| {
|
|
||||||
Err(CommandError::from(
|
|
||||||
"SSH keyfile not defined, and couldn't find home directory",
|
|
||||||
))
|
|
||||||
},
|
|
||||||
|hd| Ok(hd.join(".ssh/authorized_keys")),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
Ok,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
if !key_file.exists() {
|
|
||||||
Err("SSH keyfile doesn't exist".into())
|
|
||||||
} else {
|
|
||||||
match std::fs::read_to_string(&key_file) {
|
|
||||||
Ok(data) => {
|
|
||||||
let keys: Vec<String> =
|
|
||||||
data.split('\n').map(|s| s.trim().to_owned()).collect();
|
|
||||||
Ok(keys)
|
|
||||||
}
|
|
||||||
Err(err) => Err(CommandError::new(
|
|
||||||
format!("Couldn't read {} for SSH keys", &key_file.display()),
|
|
||||||
err,
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}?;
|
|
||||||
*/
|
|
||||||
let ci_userdata = {
|
let ci_userdata = {
|
||||||
if let Some(path) = &args.ci_userdata {
|
if let Some(path) = &args.ci_userdata {
|
||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
|
@ -323,7 +305,9 @@ async fn handle_command() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
InstanceCmd::Delete { name } => {
|
InstanceCmd::Delete { name } => {
|
||||||
(client.delete_instance(nzr_api::default_ctx(), name).await?)?;
|
client
|
||||||
|
.delete_instance(nzr_api::default_ctx(), name)
|
||||||
|
.await??;
|
||||||
}
|
}
|
||||||
InstanceCmd::List => {
|
InstanceCmd::List => {
|
||||||
let instances = client.get_instances(nzr_api::default_ctx(), true).await?;
|
let instances = client.get_instances(nzr_api::default_ctx(), true).await?;
|
||||||
|
@ -426,6 +410,30 @@ async fn handle_command() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
println!("{}", table.with(tabled::settings::Style::psql()));
|
println!("{}", table.with(tabled::settings::Style::psql()));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
Commands::SshKey { command } => match command {
|
||||||
|
KeyCmd::Add { path } => {
|
||||||
|
if !path.exists() {
|
||||||
|
return Err("Provided path doesn't exist".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let keyfile = tokio::fs::read_to_string(&path).await?;
|
||||||
|
let res = client
|
||||||
|
.add_ssh_pubkey(nzr_api::default_ctx(), keyfile)
|
||||||
|
.await??;
|
||||||
|
println!("Key #{} added.", res.id.unwrap_or(-1));
|
||||||
|
}
|
||||||
|
KeyCmd::List => {
|
||||||
|
let keys = client.get_ssh_pubkeys(nzr_api::default_ctx()).await??;
|
||||||
|
let tabular = keys.iter().map(table::SshKey::from);
|
||||||
|
let mut table = tabled::Table::new(tabular);
|
||||||
|
println!("{}", table.with(tabled::settings::Style::psql()));
|
||||||
|
}
|
||||||
|
KeyCmd::Delete { id } => {
|
||||||
|
client
|
||||||
|
.delete_ssh_pubkey(nzr_api::default_ctx(), id)
|
||||||
|
.await??;
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,3 +40,23 @@ impl From<&model::Subnet> for Subnet {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Tabled)]
|
||||||
|
pub struct SshKey {
|
||||||
|
#[tabled(rename = "ID")]
|
||||||
|
id: i32,
|
||||||
|
#[tabled(rename = "Comment")]
|
||||||
|
comment: String,
|
||||||
|
#[tabled(rename = "Key data")]
|
||||||
|
key_data: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&model::SshPubkey> for SshKey {
|
||||||
|
fn from(value: &model::SshPubkey) -> Self {
|
||||||
|
Self {
|
||||||
|
id: value.id.unwrap_or(-1),
|
||||||
|
comment: value.comment.clone().unwrap_or_default(),
|
||||||
|
key_data: format!("{} {}", value.algorithm, value.key_data),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -19,6 +19,9 @@ log = "0.4.17"
|
||||||
sqlx = "0.8"
|
sqlx = "0.8"
|
||||||
diesel = { version = "2.2", optional = true }
|
diesel = { version = "2.2", optional = true }
|
||||||
futures = { version = "0.3", optional = true }
|
futures = { version = "0.3", optional = true }
|
||||||
|
thiserror = "1"
|
||||||
|
regex = "1"
|
||||||
|
lazy_static = "1"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
uuid = { version = "1.2.2", features = ["serde", "v4"] }
|
uuid = { version = "1.2.2", features = ["serde", "v4"] }
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use std::net::Ipv4Addr;
|
use std::net::Ipv4Addr;
|
||||||
|
|
||||||
use model::{CreateStatus, Instance, Subnet};
|
use model::{CreateStatus, Instance, SshPubkey, Subnet};
|
||||||
|
|
||||||
pub mod args;
|
pub mod args;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
|
@ -51,6 +51,12 @@ pub trait Nazrin {
|
||||||
async fn delete_subnet(interface: String) -> Result<(), String>;
|
async fn delete_subnet(interface: String) -> Result<(), String>;
|
||||||
/// Gets the cloud-init user-data for the given instance.
|
/// Gets the cloud-init user-data for the given instance.
|
||||||
async fn get_instance_userdata(id: i32) -> Result<Vec<u8>, String>;
|
async fn get_instance_userdata(id: i32) -> Result<Vec<u8>, String>;
|
||||||
|
/// Gets all SSH keys stored in the database.
|
||||||
|
async fn get_ssh_pubkeys() -> Result<Vec<SshPubkey>, String>;
|
||||||
|
/// Adds a new SSH public key to the database.
|
||||||
|
async fn add_ssh_pubkey(pub_key: String) -> Result<SshPubkey, String>;
|
||||||
|
/// Deletes an SSH public key from the database.
|
||||||
|
async fn delete_ssh_pubkey(id: i32) -> Result<(), String>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new NazrinClient.
|
/// Create a new NazrinClient.
|
||||||
|
|
|
@ -2,7 +2,7 @@ pub mod client;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test;
|
mod test;
|
||||||
|
|
||||||
use std::{collections::HashMap, sync::Arc};
|
use std::{collections::HashMap, str::FromStr, sync::Arc};
|
||||||
|
|
||||||
use tarpc::server::{BaseChannel, Channel as _};
|
use tarpc::server::{BaseChannel, Channel as _};
|
||||||
|
|
||||||
|
@ -36,6 +36,7 @@ struct MockDb {
|
||||||
subnet_lease: HashMap<i32, u32>,
|
subnet_lease: HashMap<i32, u32>,
|
||||||
ci_userdatas: HashMap<String, Vec<u8>>,
|
ci_userdatas: HashMap<String, Vec<u8>>,
|
||||||
create_tasks: HashMap<uuid::Uuid, (model::Instance, bool)>,
|
create_tasks: HashMap<uuid::Uuid, (model::Instance, bool)>,
|
||||||
|
ssh_keys: Vec<Option<model::SshPubkey>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mock Nazrin RPC server for testing, where the full server isn't required.
|
/// Mock Nazrin RPC server for testing, where the full server isn't required.
|
||||||
|
@ -252,6 +253,41 @@ impl Nazrin for MockServer {
|
||||||
async fn garbage_collect(self, _: tarpc::context::Context) -> Result<(), String> {
|
async fn garbage_collect(self, _: tarpc::context::Context) -> Result<(), String> {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_ssh_pubkeys(
|
||||||
|
self,
|
||||||
|
_: tarpc::context::Context,
|
||||||
|
) -> Result<Vec<model::SshPubkey>, String> {
|
||||||
|
let db = self.db.read().await;
|
||||||
|
|
||||||
|
Ok(db
|
||||||
|
.ssh_keys
|
||||||
|
.iter()
|
||||||
|
.filter_map(|key| key.as_ref().cloned())
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add_ssh_pubkey(
|
||||||
|
self,
|
||||||
|
_: tarpc::context::Context,
|
||||||
|
pub_key: String,
|
||||||
|
) -> Result<model::SshPubkey, String> {
|
||||||
|
let mut key_model = model::SshPubkey::from_str(&pub_key).map_err(|e| e.to_string())?;
|
||||||
|
let mut db = self.db.write().await;
|
||||||
|
key_model.id = Some(db.ssh_keys.len() as i32);
|
||||||
|
db.ssh_keys.push(Some(key_model.clone()));
|
||||||
|
Ok(key_model)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_ssh_pubkey(self, _: tarpc::context::Context, id: i32) -> Result<(), String> {
|
||||||
|
let mut db = self.db.write().await;
|
||||||
|
if let Some(key) = db.ssh_keys.get_mut(id as usize) {
|
||||||
|
key.take();
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err("No such key".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generates a MockServer task and connected client.
|
/// Generates a MockServer task and connected client.
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
use hickory_proto::rr::Name;
|
use hickory_proto::rr::Name;
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use regex::Regex;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{fmt, net::Ipv4Addr};
|
use std::{fmt, net::Ipv4Addr};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::net::{cidr::CidrV4, mac::MacAddr};
|
use crate::net::{cidr::CidrV4, mac::MacAddr};
|
||||||
|
|
||||||
|
@ -127,3 +130,58 @@ impl SubnetData {
|
||||||
self.network.host_bits(&self.end_host)
|
self.network.host_bits(&self.end_host)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct SshPubkey {
|
||||||
|
pub id: Option<i32>,
|
||||||
|
pub algorithm: String,
|
||||||
|
pub key_data: String,
|
||||||
|
pub comment: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for SshPubkey {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
if let Some(comment) = &self.comment {
|
||||||
|
write!(f, "{} {} {}", &self.algorithm, &self.key_data, comment)
|
||||||
|
} else {
|
||||||
|
write!(f, "{} {}", &self.algorithm, &self.key_data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum SshPubkeyParseError {
|
||||||
|
#[error("Key file is not of the expected format")]
|
||||||
|
MissingField,
|
||||||
|
#[error("Key data must be base64-encoded")]
|
||||||
|
InvalidKeyData,
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref BASE64_RE: Regex = Regex::new(r"^[A-Za-z0-9+/=]+$").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::str::FromStr for SshPubkey {
|
||||||
|
type Err = SshPubkeyParseError;
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let mut pieces = s.split(' ');
|
||||||
|
let Some(algorithm) = pieces.next() else {
|
||||||
|
return Err(SshPubkeyParseError::MissingField);
|
||||||
|
};
|
||||||
|
let Some(key_data) = pieces.next() else {
|
||||||
|
return Err(SshPubkeyParseError::MissingField);
|
||||||
|
};
|
||||||
|
// Validate key data
|
||||||
|
if !BASE64_RE.is_match(key_data) {
|
||||||
|
return Err(SshPubkeyParseError::InvalidKeyData);
|
||||||
|
}
|
||||||
|
let comment = pieces.next().map(|s| s.to_owned());
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
id: None,
|
||||||
|
algorithm: algorithm.to_owned(),
|
||||||
|
key_data: key_data.to_owned(),
|
||||||
|
comment,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -488,6 +488,19 @@ impl SshPubkey {
|
||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get(ctx: &Context, id: i32) -> Result<Option<Self>, ModelError> {
|
||||||
|
Ok(ctx
|
||||||
|
.spawn_db(move |mut db| {
|
||||||
|
Self::table()
|
||||||
|
.find(id)
|
||||||
|
.select(Self::as_select())
|
||||||
|
.load::<Self>(&mut db)
|
||||||
|
})
|
||||||
|
.await??
|
||||||
|
.into_iter()
|
||||||
|
.next())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn insert(
|
pub async fn insert(
|
||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
algorithm: impl AsRef<str>,
|
algorithm: impl AsRef<str>,
|
||||||
|
@ -513,6 +526,15 @@ impl SshPubkey {
|
||||||
Ok(ent)
|
Ok(ent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn api_model(&self) -> nzr_api::model::SshPubkey {
|
||||||
|
nzr_api::model::SshPubkey {
|
||||||
|
id: Some(self.id),
|
||||||
|
algorithm: self.algorithm.clone(),
|
||||||
|
key_data: self.algorithm.clone(),
|
||||||
|
comment: self.comment.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn delete(self, ctx: &Context) -> Result<(), ModelError> {
|
pub async fn delete(self, ctx: &Context) -> Result<(), ModelError> {
|
||||||
ctx.spawn_db(move |mut db| diesel::delete(&self).execute(&mut db))
|
ctx.spawn_db(move |mut db| diesel::delete(&self).execute(&mut db))
|
||||||
.await??;
|
.await??;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use futures::{future, StreamExt};
|
use futures::{future, StreamExt};
|
||||||
use nzr_api::{args, model, InstanceQuery, Nazrin};
|
use nzr_api::{args, model, InstanceQuery, Nazrin};
|
||||||
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tarpc::server::{BaseChannel, Channel};
|
use tarpc::server::{BaseChannel, Channel};
|
||||||
use tarpc::tokio_serde::formats::Bincode;
|
use tarpc::tokio_serde::formats::Bincode;
|
||||||
|
@ -11,7 +12,7 @@ use uuid::Uuid;
|
||||||
|
|
||||||
use crate::cmd;
|
use crate::cmd;
|
||||||
use crate::ctx::Context;
|
use crate::ctx::Context;
|
||||||
use crate::model::{Instance, Subnet};
|
use crate::model::{Instance, SshPubkey, Subnet};
|
||||||
use log::*;
|
use log::*;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
@ -252,6 +253,41 @@ impl Nazrin for NzrServer {
|
||||||
|
|
||||||
Ok(db_model.ci_userdata.unwrap_or_default())
|
Ok(db_model.ci_userdata.unwrap_or_default())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_ssh_pubkeys(
|
||||||
|
self,
|
||||||
|
_: tarpc::context::Context,
|
||||||
|
) -> Result<Vec<model::SshPubkey>, String> {
|
||||||
|
SshPubkey::all(&self.ctx).await.map_or_else(
|
||||||
|
|e| Err(e.to_string()),
|
||||||
|
|k| Ok(k.iter().map(|k| k.api_model()).collect()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add_ssh_pubkey(
|
||||||
|
self,
|
||||||
|
_: tarpc::context::Context,
|
||||||
|
pub_key: String,
|
||||||
|
) -> Result<model::SshPubkey, String> {
|
||||||
|
let pubkey = model::SshPubkey::from_str(&pub_key).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
SshPubkey::insert(&self.ctx, pubkey.algorithm, pubkey.key_data, pubkey.comment)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
.map(|k| k.api_model())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_ssh_pubkey(self, _: tarpc::context::Context, id: i32) -> Result<(), String> {
|
||||||
|
let Some(key) = SshPubkey::get(&self.ctx, id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
else {
|
||||||
|
return Err("SSH key with ID doesn't exist".into());
|
||||||
|
};
|
||||||
|
|
||||||
|
key.delete(&self.ctx).await.map_err(|e| e.to_string())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|
Loading…
Reference in a new issue