Create API keys per SSH key & implement expiring API keys for web
Diff
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(-)
@@ -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",
@@ -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"
@@ -21,6 +21,8 @@
id -> Integer,
user_id -> Integer,
api_key -> Text,
user_ssh_key_id -> Nullable<Integer>,
expires_at -> Nullable<Timestamp>,
}
}
@@ -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));
@@ -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<Option<User>> {
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<u8>,
) -> Result<Option<User>> {
) -> Result<Option<(UserSshKey, User)>> {
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<i32>,
pub expires_at: Option<chrono::NaiveDateTime>,
}
impl UserApiKey {
pub async fn generate(
conn: ConnectionPool,
given_user_id: i32,
given_user_ssh_key_id: Option<i32>,
given_expires_at: Option<chrono::NaiveDateTime>,
) -> Result<UserApiKey> {
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<u8>,
}
impl UserSshKey {
pub async fn get_or_insert_api_key(
self: Arc<Self>,
conn: ConnectionPool,
) -> Result<UserApiKey> {
use crate::schema::user_api_keys::dsl::{expires_at, user_id};
let res: Option<UserApiKey> = 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
}
}
}
@@ -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<chartered_db::users::User>,
user_ssh_key: Option<Arc<chartered_db::users::UserSshKey>>,
}
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"}"#,
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,
@@ -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 (