🏡 index : ~doyle/chartered.git

author Jordan Doyle <jordan@doyle.la> 2021-09-17 0:12:39.0 +01:00:00
committer Jordan Doyle <jordan@doyle.la> 2021-09-17 0:12:39.0 +01:00:00
commit
25249d00e5e0d3790782ce4d7e2e9dd38d3f1939 [patch]
tree
bc06b2715cd0e7ed99903be69258b431ffd9c9da
parent
3f39f5ca1460fed33b69fb2478dd7820a98a429a
download
25249d00e5e0d3790782ce4d7e2e9dd38d3f1939.tar.gz

Add some extra info to ssh keys such as name & last used



Diff

 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<Timestamp>,
    }
}

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<Self>,
        conn: ConnectionPool,
    ) -> Result<HashMap<i32, String>> {
    /// Get all the SSH keys for the user.

    pub async fn list_ssh_keys(self: Arc<Self>, conn: ConnectionPool) -> Result<Vec<UserSshKey>> {
        tokio::task::spawn_blocking(move || {
            use crate::schema::user_ssh_keys::dsl::user_id;

            let conn = conn.get()?;

            let selected: Vec<(i32, Vec<u8>)> = 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<u8>,
    pub created_at: chrono::NaiveDateTime,
    pub last_used_at: Option<chrono::NaiveDateTime>,
}

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<Self>, 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<String> {
        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 (
        <div className="text-white">
            <Nav />
@@ -71,10 +75,14 @@
                                {sshKeys.keys.map(key => (
                                    <tr key={key.id}>
                                        <td className="align-middle">
                                            <h6 className="m-0 lh-sm">{key.name}</h6>
                                            <pre className="m-0">{key.fingerprint}</pre>
                                            <div className="lh-sm" style={{ fontSize: '.75rem' }}>
                                                <div>Added on 10 May 2016</div>
                                                <div className="text-success">Last used within the last week</div>
                                                <span className="text-muted">Added <HumanTime time={key.created_at} /></span>
                                                <span className="mx-2"></span>
                                                <span className={`text-${key.last_used_at ? (new Date(key.last_used_at) > dateMonthAgo ? 'success' : 'danger') : 'muted'}`}>
                                                    Last used {key.last_used_at ? <HumanTime time={key.last_used_at} /> : <>never</>}
                                                </span>
                                            </div>
                                        </td>

diff --git a/chartered-web/src/endpoints/web_api/login.rs b/chartered-web/src/endpoints/web_api/login.rs
index 03bce1e..a617bb8 100644
--- a/chartered-web/src/endpoints/web_api/login.rs
+++ a/chartered-web/src/endpoints/web_api/login.rs
@@ -31,7 +31,7 @@
    extract::Extension(db): extract::Extension<ConnectionPool>,
    extract::Json(req): extract::Json<Request>,
    user_agent: Option<extract::TypedHeader<headers::UserAgent>>,
    extract::ConnectInfo(addr) : extract::ConnectInfo<std::net::SocketAddr>,
    extract::ConnectInfo(addr): extract::ConnectInfo<std::net::SocketAddr>,
) -> Result<Json<Response>, Error> {
    // TODO: passwords
    let user = User::find_by_username(db.clone(), req.username)
diff --git a/chartered-web/src/endpoints/web_api/ssh_key.rs b/chartered-web/src/endpoints/web_api/ssh_key.rs
index f378be7..c5cba09 100644
--- a/chartered-web/src/endpoints/web_api/ssh_key.rs
+++ a/chartered-web/src/endpoints/web_api/ssh_key.rs
@@ -1,6 +1,8 @@
use chartered_db::{users::User, ConnectionPool};

use axum::{extract, Json};
use chrono::NaiveDateTime;
use log::warn;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use thiserror::Error;
@@ -13,7 +15,10 @@
#[derive(Serialize)]
pub struct GetResponseKey {
    id: i32, // TODO: this should be a UUID so we don't leak incremental IDs
    name: String,
    fingerprint: String,
    created_at: NaiveDateTime,
    last_used_at: Option<NaiveDateTime>,
}

pub async fn handle_get(
@@ -24,7 +29,16 @@
        .list_ssh_keys(db)
        .await?
        .into_iter()
        .map(|(id, fingerprint)| GetResponseKey { id, fingerprint })
        .map(|key| GetResponseKey {
            id: key.id,
            fingerprint: key.fingerprint().unwrap_or_else(|e| {
                warn!("Failed to parse key with id {}: {}", key.id, e);
                "INVALID".to_string()
            }),
            name: key.name,
            created_at: key.created_at,
            last_used_at: key.last_used_at,
        })
        .collect();

    Ok(Json(GetResponse { keys }))