From 2e78e5b1f65b7bde7e5f628ca97e510920403986 Mon Sep 17 00:00:00 2001 From: Jeidnx Date: Tue, 29 Oct 2024 15:09:03 +0100 Subject: [PATCH] feat: generate authorized_keys --- src/db/host.rs | 39 +++++++- src/db/mod.rs | 3 + src/routes/hosts.rs | 92 +++++++++++++++---- src/sshclient.rs | 35 ++++--- static/style.css | 15 +++ templates/hosts/authorized_keyfile_dialog.htm | 8 ++ 6 files changed, 162 insertions(+), 30 deletions(-) create mode 100644 templates/hosts/authorized_keyfile_dialog.htm diff --git a/src/db/host.rs b/src/db/host.rs index 3ea5df1..1f53f25 100644 --- a/src/db/host.rs +++ b/src/db/host.rs @@ -3,6 +3,7 @@ use crate::schema::user; use crate::schema::user_in_host; use crate::schema::user_key; use crate::sshclient::ConnectionDetails; +use crate::sshclient::SshClient; use crate::sshclient::SshClientError; use crate::{ models::{Host, NewHost, PublicUserKey}, @@ -14,6 +15,7 @@ use diesel::prelude::*; use super::query; use super::query_drop; use super::AllowedUserOnHost; +use super::AuthorizedKeysList; use super::UserAndOptions; impl Host { @@ -89,11 +91,11 @@ impl Host { query(host::table.load::(conn)) } - /// Gets all allowed users allowed on this host + /// Gets all allowed users allowed on this host, sorted by user_on_host pub fn get_authorized_keys( &self, conn: &mut DbConnection, - ) -> Result, String> { + ) -> Result { query( user::table .inner_join(user_key::table) @@ -105,6 +107,7 @@ impl Host { user_in_host::options, )) .filter(user_in_host::host_id.eq(self.id)) + .order(user_in_host::user.desc()) .load::<(PublicUserKey, String, String, Option)>(conn), ) .map(|allowed_list| { @@ -114,4 +117,36 @@ impl Host { .collect() }) } + + /// Generate authorized key file for a user on a host. Includes ssm key, if applicable + pub fn get_authorized_keys_file_for( + &self, + ssh_client: &SshClient, + conn: &mut DbConnection, + user_on_host: &str, + ) -> Result { + let res: Vec<(PublicUserKey, Option)> = query( + user::table + .inner_join(user_key::table) + .inner_join(user_in_host::table) + .select((PublicUserKey::as_select(), user_in_host::options)) + .filter(user_in_host::host_id.eq(self.id)) + .filter(user_in_host::user.eq(user_on_host)) + .load::<(PublicUserKey, Option)>(conn), + )?; + + let estimated_size = (res.len() + 2) * 150; + + Ok(res.into_iter().fold( + String::with_capacity(estimated_size), + |buf, (key, options)| { + buf + options.unwrap_or(String::new()).as_str() + key.to_openssh().as_str() + "\n" + }, + ) + (if self.username.eq(&user_on_host) { + ssh_client.get_own_key_openssh() + "\n" + } else { + String::new() + }) + .as_str()) + } } diff --git a/src/db/mod.rs b/src/db/mod.rs index 28cac80..0b248b1 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -58,6 +58,9 @@ pub type UsernameAndKey = (String, PublicUserKey); /// A list of allowed hosts for a user: Hostname, User on host, Options pub type Authorization = (String, String, Option); +/// List of authorized_keys files +pub type AuthorizedKeysList = Vec; + /// Prints database Errors and returns a generic String pub fn query(query_result: Result) -> Result { query_result.map_err(|e| { diff --git a/src/routes/hosts.rs b/src/routes/hosts.rs index 8a89929..ee2d3a8 100644 --- a/src/routes/hosts.rs +++ b/src/routes/hosts.rs @@ -22,23 +22,9 @@ pub fn hosts_config(cfg: &mut web::ServiceConfig) { .service(render_hosts) .service(show_host) .service(add_host) - .service(remove_key_from_host) - .service(authorize_user); -} - -#[derive(Deserialize)] -struct RemoveKeyFromHostForm { - key_base64: String, -} - -#[post("/{name}/remove_key")] -async fn remove_key_from_host( - conn: Data, - host_name: Path, - key: web::Form, -) -> actix_web::Result { - //TODO: remove key from db - Ok(FormResponseBuilder::error(String::from("Not implemented"))) + .service(authorize_user) + .service(gen_authorized_keys) + .service(set_authorized_keys); } #[derive(Template)] @@ -304,3 +290,75 @@ async fn authorize_user( Err(e) => FormResponseBuilder::error(e), }) } + +#[derive(Deserialize)] +struct GenAuthorizedKeysForm { + host_name: String, + user_on_host: String, +} + +#[derive(Template)] +#[template(path = "hosts/authorized_keyfile_dialog.htm")] +struct AuthorizedKeyfileDialog { + user_on_host: String, + authorized_keys: String, +} + +#[post("/gen_authorized_keys")] +async fn gen_authorized_keys( + conn: Data, + ssh_client: Data, + form: web::Form, +) -> actix_web::Result { + let host_name = form.host_name.clone(); + let user_on_host = form.user_on_host.clone(); + let res = web::block(move || { + let mut connection = conn.get().unwrap(); + + let host = Host::get_host_name(&mut connection, form.host_name.clone()).ok()?; + + host.and_then(|host| { + host.get_authorized_keys_file_for(&ssh_client, &mut connection, &form.user_on_host) + .ok() + }) + }) + .await?; + Ok(match res { + Some(authorized_keys) => FormResponseBuilder::dialog(Modal { + title: format!("Authorized keyfile for '{user_on_host}' on '{host_name}':"), + request_target: format!("/hosts/{host_name}/set_authorized_keys"), + template: AuthorizedKeyfileDialog { + user_on_host, + authorized_keys, + } + .to_string(), + }), + None => FormResponseBuilder::error(String::from("Couldn't find host")), + }) +} + +#[derive(Deserialize)] +struct SetAuthorizedKeysForm { + user_on_host: String, + authorized_keys: String, +} + +#[post("/{name}/set_authorized_keys")] +async fn set_authorized_keys( + form: web::Form, + host: Path, + ssh_client: Data, +) -> actix_web::Result { + let res = ssh_client + .set_authorized_keys( + host.to_string(), + form.user_on_host.clone(), + form.authorized_keys.clone(), + ) + .await; + + Ok(match res { + Ok(()) => FormResponseBuilder::success(String::from("Applied authorized_keys")), + Err(error) => FormResponseBuilder::error(error.to_string()), + }) +} diff --git a/src/sshclient.rs b/src/sshclient.rs index 248c688..e9e2d88 100644 --- a/src/sshclient.rs +++ b/src/sshclient.rs @@ -208,6 +208,12 @@ impl SshClient { } } + pub fn get_own_key_openssh(&self) -> String { + let b64 = self.key.public_key_base64(); + let algo = self.key.name(); + format!("{algo} {b64} ssm") + } + /// Tries to connect to a host and returns hostkeys to validate pub async fn get_hostkey( &self, @@ -310,6 +316,12 @@ impl SshClient { .map_err(SshClientError::DatabaseError)? .ok_or(SshClientError::NoSuchHost) } + async fn get_host_from_name(&self, host_name: String) -> Result { + // TODO: this is blocking the thread + Host::get_host_name(&mut self.conn.get().unwrap(), host_name) + .map_err(SshClientError::DatabaseError)? + .ok_or(SshClientError::NoSuchHost) + } fn connect( self, @@ -426,19 +438,20 @@ impl SshClient { .collect()) } - async fn set_authorized_keys_for( + pub async fn set_authorized_keys( &self, - handle: &russh::client::Handle, - user: String, + host_name: String, + user_on_host: String, authorized_keys: String, - ) -> Result { - let res = self - .execute_bash( - handle, - BashCommand::SetAuthorizedKeyfile(user, authorized_keys), - ) - .await??; - Ok(res) + ) -> Result<(), SshClientError> { + let host = self.get_host_from_name(host_name).await?; + let handle = self.clone().connect(host).await?; + self.execute_bash( + &handle, + BashCommand::SetAuthorizedKeyfile(user_on_host, authorized_keys), + ) + .await??; + Ok(()) } pub async fn get_users_on_host(&self, host: Host) -> Result, SshClientError> { diff --git a/static/style.css b/static/style.css index 3460ae3..72aa299 100644 --- a/static/style.css +++ b/static/style.css @@ -77,6 +77,21 @@ form>label { code { background-color: var(--bg-color-alt); + /* white-space: pre-line; */ + word-break: break-all; +} + +code>span { + display: block; + width: 100%; +} + +code>span:nth-child(odd) { + background-color: var(--bg-color-alt); +} + +code>span:nth-child(even) { + background-color: var(--bg-color); } table { diff --git a/templates/hosts/authorized_keyfile_dialog.htm b/templates/hosts/authorized_keyfile_dialog.htm new file mode 100644 index 0000000..e1d9dc8 --- /dev/null +++ b/templates/hosts/authorized_keyfile_dialog.htm @@ -0,0 +1,8 @@ + + + + {% for line in authorized_keys.lines() %} + {{ line }} + {% endfor %} + + \ No newline at end of file