🏡 index : ~doyle/chartered.git

author Jordan Doyle <jordan@doyle.la> 2021-09-10 1:58:31.0 +01:00:00
committer Jordan Doyle <jordan@doyle.la> 2021-09-10 1:58:31.0 +01:00:00
commit
7a14d33d8d84b199fb5819f9ca610d8329a97cbe [patch]
tree
8a1b96c4ec5af854f61bf20d5d4f61e51272f8c9
parent
c8a7ec1461bdbb82e262aadb8c3f2de745e8de3a
download
7a14d33d8d84b199fb5819f9ca610d8329a97cbe.tar.gz

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(-)

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<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));
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<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 {
    /// 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<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
        }
    }
}
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<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"}"#,
            // 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 (