Skip to content

Commit

Permalink
feat: generate authorized_keys
Browse files Browse the repository at this point in the history
  • Loading branch information
Jeidnx committed Oct 29, 2024
1 parent 86597d3 commit 2e78e5b
Show file tree
Hide file tree
Showing 6 changed files with 162 additions and 30 deletions.
39 changes: 37 additions & 2 deletions src/db/host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand All @@ -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 {
Expand Down Expand Up @@ -89,11 +91,11 @@ impl Host {
query(host::table.load::<Self>(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<Vec<AllowedUserOnHost>, String> {
) -> Result<AuthorizedKeysList, String> {
query(
user::table
.inner_join(user_key::table)
Expand All @@ -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<String>)>(conn),
)
.map(|allowed_list| {
Expand All @@ -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<String, String> {
let res: Vec<(PublicUserKey, Option<String>)> = 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<String>)>(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())
}
}
3 changes: 3 additions & 0 deletions src/db/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>);

/// List of authorized_keys files
pub type AuthorizedKeysList = Vec<AllowedUserOnHost>;

/// Prints database Errors and returns a generic String
pub fn query<T>(query_result: Result<T, Error>) -> Result<T, String> {
query_result.map_err(|e| {
Expand Down
92 changes: 75 additions & 17 deletions src/routes/hosts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ConnectionPool>,
host_name: Path<String>,
key: web::Form<RemoveKeyFromHostForm>,
) -> actix_web::Result<impl Responder> {
//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)]
Expand Down Expand Up @@ -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<ConnectionPool>,
ssh_client: Data<SshClient>,
form: web::Form<GenAuthorizedKeysForm>,
) -> actix_web::Result<impl Responder> {
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<SetAuthorizedKeysForm>,
host: Path<String>,
ssh_client: Data<SshClient>,
) -> actix_web::Result<impl Responder> {
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()),
})
}
35 changes: 24 additions & 11 deletions src/sshclient.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<Host, SshClientError> {
// 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,
Expand Down Expand Up @@ -426,19 +438,20 @@ impl SshClient {
.collect())
}

async fn set_authorized_keys_for(
pub async fn set_authorized_keys(
&self,
handle: &russh::client::Handle<SshHandler>,
user: String,
host_name: String,
user_on_host: String,
authorized_keys: String,
) -> Result<String, SshClientError> {
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<Vec<String>, SshClientError> {
Expand Down
15 changes: 15 additions & 0 deletions static/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
8 changes: 8 additions & 0 deletions templates/hosts/authorized_keyfile_dialog.htm
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<input type="hidden" name="user_on_host" value="{{ user_on_host }}" />
<input type="hidden" name="authorized_keys" value="{{ authorized_keys }}" />
<code>
{% for line in authorized_keys.lines() %}
<span>{{ line }}</span>
{% endfor %}
</code>
<button>Apply</button>

0 comments on commit 2e78e5b

Please sign in to comment.