From 7a14d33d8d84b199fb5819f9ca610d8329a97cbe Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Fri, 10 Sep 2021 01:58:31 +0100 Subject: [PATCH] Create API keys per SSH key & implement expiring API keys for web --- Cargo.lock | 3 +++ chartered-db/Cargo.toml | 4 +++- chartered-db/src/schema.rs | 3 +++ chartered-db/src/users.rs | 93 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- chartered-git/src/main.rs | 19 ++++++++++++++++++- migrations/2021-08-31-214501_create_crates_table/up.sql | 3 +++ 6 files changed, 117 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e59057b..418337d 100644 --- a/Cargo.lock +++ a/Cargo.lock @@ -188,10 +188,12 @@ dependencies = [ "bitflags", "chartered-fs", + "chrono", "diesel", "displaydoc", "dotenv", "itertools", + "rand", "thiserror", "tokio", ] @@ -357,6 +359,7 @@ checksum = "bba51ca66f57261fd17cadf8b73e4775cc307d0521d855de3f5de91a8f074e0e" dependencies = [ "byteorder", + "chrono", "diesel_derives", "libsqlite3-sys", "r2d2", diff --git a/chartered-db/Cargo.toml b/chartered-db/Cargo.toml index d458b17..e5dbda2 100644 --- a/chartered-db/Cargo.toml +++ a/chartered-db/Cargo.toml @@ -9,9 +9,11 @@ chartered-fs = { path = "../chartered-fs" } bitflags = "1" -diesel = { version = "1", features = ["sqlite", "r2d2"] } +chrono = "0.4" +diesel = { version = "1", features = ["sqlite", "r2d2", "chrono"] } displaydoc = "0.2" itertools = "0.10" +rand = "0.8" thiserror = "1" tokio = "1" dotenv = "0.15" diff --git a/chartered-db/src/schema.rs b/chartered-db/src/schema.rs index 060e617..92e9f46 100644 --- a/chartered-db/src/schema.rs +++ a/chartered-db/src/schema.rs @@ -21,6 +21,8 @@ id -> Integer, user_id -> Integer, api_key -> Text, + user_ssh_key_id -> Nullable, + expires_at -> Nullable, } } @@ -49,6 +51,7 @@ } joinable!(crate_versions -> crates (crate_id)); +joinable!(user_api_keys -> user_ssh_keys (user_ssh_key_id)); joinable!(user_api_keys -> users (user_id)); joinable!(user_crate_permissions -> crates (crate_id)); joinable!(user_crate_permissions -> users (user_id)); diff --git a/chartered-db/src/users.rs b/chartered-db/src/users.rs index cb72a04..4473eab 100644 --- a/chartered-db/src/users.rs +++ a/chartered-db/src/users.rs @@ -1,8 +1,9 @@ use super::{ schema::{user_api_keys, user_crate_permissions, user_ssh_keys, users}, ConnectionPool, Result, }; -use diesel::{prelude::*, Associations, Identifiable, Queryable}; +use diesel::{insert_into, prelude::*, Associations, Identifiable, Queryable}; +use rand::{thread_rng, Rng}; use std::sync::Arc; #[derive(Identifiable, Queryable, Associations, PartialEq, Eq, Hash, Debug)] @@ -16,12 +17,17 @@ conn: ConnectionPool, given_api_key: String, ) -> Result> { - use crate::schema::user_api_keys::dsl::api_key; + use crate::schema::user_api_keys::dsl::{api_key, expires_at}; tokio::task::spawn_blocking(move || { let conn = conn.get()?; Ok(crate::schema::user_api_keys::table + .filter( + expires_at + .is_null() + .or(expires_at.gt(chrono::Utc::now().naive_utc())), + ) .filter(api_key.eq(given_api_key)) .inner_join(users::table) .select((users::dsl::id, users::dsl::username)) @@ -34,7 +40,7 @@ pub async fn find_by_ssh_key( conn: ConnectionPool, given_ssh_key: Vec, - ) -> Result> { + ) -> Result> { use crate::schema::user_ssh_keys::dsl::ssh_key; eprintln!("ssh key: {:x?}", given_ssh_key); @@ -45,7 +51,7 @@ Ok(crate::schema::user_ssh_keys::table .filter(ssh_key.eq(given_ssh_key)) .inner_join(users::table) - .select((users::dsl::id, users::dsl::username)) + .select((user_ssh_keys::all_columns, users::all_columns)) .get_result(&conn) .optional()?) }) @@ -83,10 +89,50 @@ #[derive(Identifiable, Queryable, Associations, PartialEq, Eq, Hash, Debug)] #[belongs_to(User)] +#[belongs_to(UserSshKey)] pub struct UserApiKey { pub id: i32, pub user_id: i32, pub api_key: String, + pub user_ssh_key_id: Option, + pub expires_at: Option, +} + +impl UserApiKey { + pub async fn generate( + conn: ConnectionPool, + given_user_id: i32, + given_user_ssh_key_id: Option, + given_expires_at: Option, + ) -> Result { + use crate::schema::user_api_keys::dsl::{ + api_key, expires_at, user_api_keys, user_id, user_ssh_key_id, + }; + + tokio::task::spawn_blocking(move || { + let conn = conn.get()?; + + let generated_api_key: String = thread_rng() + .sample_iter(&rand::distributions::Alphanumeric) + .take(48) + .map(char::from) + .collect(); + + insert_into(user_api_keys) + .values(( + user_id.eq(given_user_id), + api_key.eq(&generated_api_key), + user_ssh_key_id.eq(given_user_ssh_key_id), + expires_at.eq(given_expires_at), + )) + .execute(&conn)?; + + Ok(crate::schema::user_api_keys::table + .filter(api_key.eq(generated_api_key)) + .get_result(&conn)?) + }) + .await? + } } bitflags::bitflags! { @@ -149,4 +195,43 @@ pub id: i32, pub user_id: i32, pub ssh_key: Vec, +} + +impl UserSshKey { + /// Every SSH key should have a corresponding API key so when the config is pulled from git we + /// can return a key in there. The API key might have, however, been compromised and removed + /// using the Web UI/database/etc - this function will regenerate the key on next pull so + /// there's no disruption in service. + pub async fn get_or_insert_api_key( + self: Arc, + conn: ConnectionPool, + ) -> Result { + use crate::schema::user_api_keys::dsl::{expires_at, user_id}; + + let res: Option = tokio::task::spawn_blocking({ + let conn = conn.clone(); + let this = self.clone(); + move || { + let conn = conn.get()?; + + UserApiKey::belonging_to(&*this) + .filter( + expires_at + .is_null() + .or(expires_at.gt(chrono::Utc::now().naive_utc())), + ) + .filter(user_id.eq(this.user_id)) + .get_result(&conn) + .optional() + .map_err(crate::Error::Query) + } + }) + .await??; + + if let Some(res) = res { + Ok(res) + } else { + UserApiKey::generate(conn, self.user_id, Some(self.id), None).await + } + } } diff --git a/chartered-git/src/main.rs b/chartered-git/src/main.rs index 1f3419b..ddd8126 100644 --- a/chartered-git/src/main.rs +++ a/chartered-git/src/main.rs @@ -55,6 +55,7 @@ output_bytes: BytesMut::default(), db: self.db.clone(), user: None, + user_ssh_key: None, } } } @@ -65,6 +66,7 @@ output_bytes: BytesMut, db: chartered_db::ConnectionPool, user: Option, + user_ssh_key: Option>, } impl Handler { @@ -165,7 +167,7 @@ let public_key = key.public_key_bytes(); Box::pin(async move { - let login_user = + let (ssh_key, login_user) = match chartered_db::users::User::find_by_ssh_key(self.db.clone(), public_key) .await? { @@ -174,6 +176,7 @@ }; self.user = Some(login_user); + self.user_ssh_key = Some(Arc::new(ssh_key)); self.finished_auth(server::Auth::Accept).await }) } @@ -235,9 +238,19 @@ let mut pack_file_entries = Vec::new(); let mut root_tree = Vec::new(); - let config_file = PackFileEntry::Blob( - br#"{"dl":"http://127.0.0.1:8888/a/abc/api/v1/crates","api":"http://127.0.0.1:8888/a/abc"}"#, + // TODO: key should be cached + let config = format!( + r#"{{"dl":"http://127.0.0.1:8888/a/{key}/api/v1/crates","api":"http://127.0.0.1:8888/a/{key}"}}"#, + key = self + .user_ssh_key + .as_ref() + .unwrap() + .clone() + .get_or_insert_api_key(self.db.clone()) + .await? + .api_key, ); + let config_file = PackFileEntry::Blob(config.as_bytes()); root_tree.push(TreeItem { kind: TreeItemKind::File, 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 6b56fe8..1c3640a 100644 --- a/migrations/2021-08-31-214501_create_crates_table/up.sql +++ a/migrations/2021-08-31-214501_create_crates_table/up.sql @@ -30,7 +30,10 @@ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, api_key VARCHAR(255) NOT NULL UNIQUE, + user_ssh_key_id INTEGER, + expires_at DATETIME, FOREIGN KEY (user_id) REFERENCES users (id) + FOREIGN KEY (user_ssh_key_id) REFERENCES user_ssh_keys (id) ); CREATE TABLE user_crate_permissions ( -- rgit 0.1.3