From 25249d00e5e0d3790782ce4d7e2e9dd38d3f1939 Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Fri, 17 Sep 2021 00:12:39 +0100 Subject: [PATCH] Add some extra info to ssh keys such as name & last used --- Cargo.lock | 1 + chartered-frontend/package-lock.json | 31 +++++++++++++++++++++++++++++++ chartered-frontend/package.json | 1 + chartered-git/Cargo.toml | 1 + chartered-db/src/schema.rs | 3 +++ chartered-db/src/users.rs | 88 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------ chartered-git/src/main.rs | 9 ++++++++- migrations/2021-08-31-214501_create_crates_table/up.sql | 3 +++ chartered-frontend/src/pages/ssh-keys/ListSshKeys.tsx | 12 ++++++++++-- chartered-web/src/endpoints/web_api/login.rs | 2 +- chartered-web/src/endpoints/web_api/ssh_key.rs | 16 ++++++++++++++++ 11 files changed, 124 insertions(+), 43 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1e43bf8..bbf6099 100644 --- a/Cargo.lock +++ a/Cargo.lock @@ -239,6 +239,7 @@ "futures", "hex", "itoa", + "log", "serde", "serde_json", "sha-1", diff --git a/chartered-frontend/package-lock.json b/chartered-frontend/package-lock.json index 968988d..0c70238 100644 --- a/chartered-frontend/package-lock.json +++ a/chartered-frontend/package-lock.json @@ -14,6 +14,7 @@ "react-bootstrap": "^2.0.0-beta.6", "react-bootstrap-icons": "^1.5.0", "react-dom": "^17.0.2", + "react-human-time": "^1.2.0", "react-markdown": "^7.0.1", "react-router-dom": "^5.3.0", "react-syntax-highlighter": "^15.4.4", @@ -5416,6 +5417,11 @@ "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", "dev": true }, + "node_modules/human-time": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/human-time/-/human-time-0.0.1.tgz", + "integrity": "sha1-KA0DNjeRmTBrLhUY49X2OBy4UH0=" + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -8880,6 +8886,16 @@ }, "peerDependencies": { "react": "17.0.2" + } + }, + "node_modules/react-human-time": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/react-human-time/-/react-human-time-1.2.0.tgz", + "integrity": "sha512-4pKOGL4eswR9O4K+TD9uhjglCH6Tv96nN4y9Mpgx317gY013ld3f6dxml8hZoj2iedcfHgFYrh26uo5l99vpIw==", + "dependencies": { + "@types/react": ">=16.0.0", + "human-time": "0.0.1", + "react": ">=16.0.0" } }, "node_modules/react-is": { @@ -15821,6 +15837,11 @@ "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", "dev": true + }, + "human-time": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/human-time/-/human-time-0.0.1.tgz", + "integrity": "sha1-KA0DNjeRmTBrLhUY49X2OBy4UH0=" }, "iconv-lite": { "version": "0.4.24", @@ -18452,6 +18473,16 @@ "loose-envify": "^1.1.0", "object-assign": "^4.1.1", "scheduler": "^0.20.2" + } + }, + "react-human-time": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/react-human-time/-/react-human-time-1.2.0.tgz", + "integrity": "sha512-4pKOGL4eswR9O4K+TD9uhjglCH6Tv96nN4y9Mpgx317gY013ld3f6dxml8hZoj2iedcfHgFYrh26uo5l99vpIw==", + "requires": { + "@types/react": ">=16.0.0", + "human-time": "0.0.1", + "react": ">=16.0.0" } }, "react-is": { diff --git a/chartered-frontend/package.json b/chartered-frontend/package.json index b938b02..7e28737 100644 --- a/chartered-frontend/package.json +++ a/chartered-frontend/package.json @@ -25,6 +25,7 @@ "react-bootstrap": "^2.0.0-beta.6", "react-bootstrap-icons": "^1.5.0", "react-dom": "^17.0.2", + "react-human-time": "^1.2.0", "react-markdown": "^7.0.1", "react-router-dom": "^5.3.0", "react-syntax-highlighter": "^15.4.4", diff --git a/chartered-git/Cargo.toml b/chartered-git/Cargo.toml index ce2e466..041b4bd 100644 --- a/chartered-git/Cargo.toml +++ a/chartered-git/Cargo.toml @@ -21,6 +21,7 @@ futures = "0.3" hex = "0.4" itoa = "0.4" +log = "0.4" serde = { version = "1", features = ["derive"] } serde_json = "1" sha-1 = "0.9" diff --git a/chartered-db/src/schema.rs b/chartered-db/src/schema.rs index efe34f3..df2edd6 100644 --- a/chartered-db/src/schema.rs +++ a/chartered-db/src/schema.rs @@ -48,8 +48,11 @@ table! { user_ssh_keys (id) { id -> Integer, + name -> Text, user_id -> Integer, ssh_key -> Binary, + created_at -> Timestamp, + last_used_at -> Nullable, } } diff --git a/chartered-db/src/users.rs b/chartered-db/src/users.rs index 2cbbaac..4bffdaa 100644 --- a/chartered-db/src/users.rs +++ a/chartered-db/src/users.rs @@ -1,10 +1,10 @@ use super::{ schema::{user_crate_permissions, user_sessions, user_ssh_keys, users}, ConnectionPool, Result, }; use diesel::{insert_into, prelude::*, Associations, Identifiable, Queryable}; use rand::{thread_rng, Rng}; -use std::{collections::HashMap, sync::Arc}; +use std::sync::Arc; use thrussh_keys::PublicKeyBase64; #[derive(Identifiable, Queryable, Associations, PartialEq, Eq, Hash, Debug)] @@ -89,14 +89,16 @@ }; let parsed_key = thrussh_keys::parse_public_key_base64(key)?; + let parsed_name = split.next().unwrap_or("(none)").to_string(); tokio::task::spawn_blocking(move || { - use crate::schema::user_ssh_keys::dsl::{ssh_key, user_id}; + use crate::schema::user_ssh_keys::dsl::{name, ssh_key, user_id}; let conn = conn.get()?; insert_into(crate::schema::user_ssh_keys::dsl::user_ssh_keys) .values(( + name.eq(parsed_name), ssh_key.eq(parsed_key.public_key_bytes()), user_id.eq(self.id), )) @@ -129,51 +131,20 @@ .await? } - /// Get all the SSH keys for the user, returned as `id:fingerprint`. - pub async fn list_ssh_keys( - self: Arc, - conn: ConnectionPool, - ) -> Result> { + /// Get all the SSH keys for the user. + pub async fn list_ssh_keys(self: Arc, conn: ConnectionPool) -> Result> { tokio::task::spawn_blocking(move || { use crate::schema::user_ssh_keys::dsl::user_id; let conn = conn.get()?; - let selected: Vec<(i32, Vec)> = crate::schema::user_ssh_keys::table + let selected = crate::schema::user_ssh_keys::table .filter(user_id.eq(self.id)) .inner_join(users::table) - .select((user_ssh_keys::id, user_ssh_keys::ssh_key)) + .select(user_ssh_keys::all_columns) .load(&conn)?; - Ok(selected - .into_iter() - .map(|(id, key)| { - ( - id, - thrussh_keys::key::parse_public_key(&key) - .map_err(|e| e.into()) - .and_then(|v| { - let raw_hex = hex::encode( - base64::decode(&v.fingerprint()) - .map_err(|_| thrussh_keys::Error::CouldNotReadKey)?, - ); - let mut hex = - String::with_capacity(raw_hex.len() + (raw_hex.len() / 2 - 1)); - - for (i, c) in raw_hex.chars().enumerate() { - if i != 0 && i % 2 == 0 { - hex.push(':'); - } - - hex.push(c); - } - - Ok::<_, crate::Error>(hex) - }) - .unwrap_or_else(|e| format!("INVALID: {}", e)), - ) - }) - .collect()) + Ok(selected) }) .await? } @@ -319,8 +290,11 @@ #[belongs_to(User)] pub struct UserSshKey { pub id: i32, + pub name: String, pub user_id: i32, pub ssh_key: Vec, + pub created_at: chrono::NaiveDateTime, + pub last_used_at: Option, } impl UserSshKey { @@ -359,6 +333,44 @@ Ok(res) } else { UserSession::generate(conn, self.user_id, Some(self.id), None, None, ip).await + } + } + + /// Updates the last used time of this SSH key for reporting purposes in the + /// dashboard. + pub async fn update_last_used(self: Arc, conn: ConnectionPool) -> Result<()> { + use crate::schema::user_ssh_keys::dsl::{id, last_used_at, user_ssh_keys}; + + tokio::task::spawn_blocking(move || { + let conn = conn.get()?; + + diesel::update(user_ssh_keys.filter(id.eq(self.id))) + .set(last_used_at.eq(diesel::dsl::now)) + .execute(&conn) + .map(|_| ()) + .map_err(Into::into) + }) + .await? + } + + /// Retrieves the key fingerprint, encoded in hex and separated in two character chunks + /// with colons. + pub fn fingerprint(&self) -> Result { + let key = thrussh_keys::key::parse_public_key(&self.ssh_key)?; + + let raw_hex = hex::encode( + base64::decode(&key.fingerprint()).map_err(|_| thrussh_keys::Error::CouldNotReadKey)?, + ); + let mut hex = String::with_capacity(raw_hex.len() + (raw_hex.len() / 2 - 1)); + + for (i, c) in raw_hex.chars().enumerate() { + if i != 0 && i % 2 == 0 { + hex.push(':'); + } + + hex.push(c); } + + Ok(hex) } } diff --git a/chartered-git/src/main.rs b/chartered-git/src/main.rs index 7d57715..f4ff6bf 100644 --- a/chartered-git/src/main.rs +++ a/chartered-git/src/main.rs @@ -19,6 +19,7 @@ }; use thrussh_keys::{key, PublicKeyBase64}; use tokio_util::codec::{Decoder, Encoder as TokioEncoder}; +use log::warn; #[tokio::main] #[allow(clippy::semicolon_if_nothing_returned)] // broken clippy lint @@ -183,9 +184,15 @@ Some(user) => user, None => return self.finished_auth(server::Auth::Reject).await, }; + let ssh_key = Arc::new(ssh_key); + if let Err(e) = ssh_key.clone().update_last_used(self.db.clone()).await { + warn!("Failed to update last used key: {:?}", e); + } + self.user = Some(login_user); - self.user_ssh_key = Some(Arc::new(ssh_key)); + self.user_ssh_key = Some(ssh_key); + self.finished_auth(server::Auth::Accept).await }) } diff --git a/migrations/2021-08-31-214501_create_crates_table/up.sql b/migrations/2021-08-31-214501_create_crates_table/up.sql index d1d3b05..787c099 100644 --- a/migrations/2021-08-31-214501_create_crates_table/up.sql +++ a/migrations/2021-08-31-214501_create_crates_table/up.sql @@ -29,8 +29,11 @@ CREATE TABLE user_ssh_keys ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + name VARCHAR(255) NOT NULL, user_id INTEGER NOT NULL, ssh_key BLOB NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + last_used_at DATETIME, FOREIGN KEY (user_id) REFERENCES users (id) ); diff --git a/chartered-frontend/src/pages/ssh-keys/ListSshKeys.tsx b/chartered-frontend/src/pages/ssh-keys/ListSshKeys.tsx index e2c9359..e8a5a15 100644 --- a/chartered-frontend/src/pages/ssh-keys/ListSshKeys.tsx +++ a/chartered-frontend/src/pages/ssh-keys/ListSshKeys.tsx @@ -8,6 +8,7 @@ import { Plus, Trash } from "react-bootstrap-icons"; import { Button, Modal } from "react-bootstrap"; +import HumanTime from "react-human-time"; export default function ListSshKeys() { const auth = useAuth(); @@ -50,6 +51,9 @@ } }; + const dateMonthAgo = new Date(); + dateMonthAgo.setMonth(dateMonthAgo.getMonth() - 1); + return (