From 61ab457e371aea68df36940a058cb1f7b983c7f0 Mon Sep 17 00:00:00 2001 From: boxbeam Date: Mon, 5 Feb 2024 13:41:56 -0500 Subject: [PATCH] feat(webserver): Use hash IDs rather than numeric row IDs in external API (#1382) * feat(webserver): Use hash IDs rather than numeric row IDs in external API * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) * Fix unit test, apply suggested changes * [autofix.ci] apply automated fixes * Revert lib.rs --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- Cargo.lock | 9 ++++++++ ee/tabby-db/Cargo.toml | 3 +++ ee/tabby-db/src/lib.rs | 22 ++++++++++++++++++++ ee/tabby-webserver/src/schema/dao.rs | 13 ++++++------ ee/tabby-webserver/src/schema/mod.rs | 9 ++++---- ee/tabby-webserver/src/service/auth.rs | 6 +++--- ee/tabby-webserver/src/service/repository.rs | 6 +++--- 7 files changed, 52 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7ad187e9d0bd..193962adf357 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1454,6 +1454,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "hash-ids" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9289cbc8064a1c2f505c92d4d17f7fe5050d487728c19cee7936bd204069bc9d" + [[package]] name = "hashbrown" version = "0.12.3" @@ -4347,8 +4353,11 @@ dependencies = [ "anyhow", "assert_matches", "chrono", + "hash-ids", + "lazy_static", "sqlx", "tabby-common", + "thiserror", "tokio", "uuid 1.6.1", ] diff --git a/ee/tabby-db/Cargo.toml b/ee/tabby-db/Cargo.toml index e3a8a36c74df..1202c5c507d2 100644 --- a/ee/tabby-db/Cargo.toml +++ b/ee/tabby-db/Cargo.toml @@ -12,8 +12,11 @@ prod-db = [] [dependencies] anyhow.workspace = true chrono = { workspace = true, features = ["serde"] } +hash-ids = "0.2.1" +lazy_static.workspace = true sqlx = { version = "0.7.3", features = ["sqlite", "chrono", "runtime-tokio"] } tabby-common = { path = "../../crates/tabby-common" } +thiserror.workspace = true tokio = { workspace = true, features = ["fs"] } uuid.workspace = true diff --git a/ee/tabby-db/src/lib.rs b/ee/tabby-db/src/lib.rs index cbaf91d6aedb..2fc8509396d1 100644 --- a/ee/tabby-db/src/lib.rs +++ b/ee/tabby-db/src/lib.rs @@ -1,6 +1,7 @@ pub use email_setting::EmailSettingDAO; pub use github_oauth_credential::GithubOAuthCredentialDAO; pub use google_oauth_credential::GoogleOAuthCredentialDAO; +use hash_ids::HashIds; pub use invitations::InvitationDAO; pub use job_runs::JobRunDAO; pub use repositories::RepositoryDAO; @@ -18,14 +19,35 @@ mod repositories; mod users; use anyhow::Result; +use lazy_static::lazy_static; use sqlx::sqlite::SqliteConnectOptions; +lazy_static! { + static ref HASHER: HashIds = HashIds::builder().with_salt("tabby-id-serializer").finish(); +} + +#[derive(thiserror::Error, Debug)] +#[error("Invalid ID")] +pub struct InvalidIDError; + #[derive(Clone)] pub struct DbConn { pool: Pool, } impl DbConn { + pub fn to_id(rowid: i32) -> String { + HASHER.encode(&[rowid as u64]) + } + + pub fn to_rowid(id: &str) -> Result { + HASHER + .decode(id) + .first() + .map(|i| *i as i32) + .ok_or(InvalidIDError) + } + #[cfg(any(test, feature = "testutils"))] pub async fn new_in_memory() -> Result { use std::str::FromStr; diff --git a/ee/tabby-webserver/src/schema/dao.rs b/ee/tabby-webserver/src/schema/dao.rs index 35afd6500908..9e5e78286f25 100644 --- a/ee/tabby-webserver/src/schema/dao.rs +++ b/ee/tabby-webserver/src/schema/dao.rs @@ -1,6 +1,7 @@ +use juniper::ID; use tabby_db::{ - EmailSettingDAO, GithubOAuthCredentialDAO, GoogleOAuthCredentialDAO, InvitationDAO, JobRunDAO, - RepositoryDAO, UserDAO, + DbConn, EmailSettingDAO, GithubOAuthCredentialDAO, GoogleOAuthCredentialDAO, InvitationDAO, + JobRunDAO, RepositoryDAO, UserDAO, }; use super::{email::EmailSetting, repository::Repository}; @@ -13,7 +14,7 @@ use crate::schema::{ impl From for auth::Invitation { fn from(val: InvitationDAO) -> Self { Self { - id: juniper::ID::new(val.id.to_string()), + id: ID::new(DbConn::to_id(val.id)), email: val.email, code: val.code, created_at: val.created_at, @@ -24,7 +25,7 @@ impl From for auth::Invitation { impl From for job::JobRun { fn from(run: JobRunDAO) -> Self { Self { - id: juniper::ID::new(run.id.to_string()), + id: ID::new(DbConn::to_id(run.id)), job_name: run.job_name, start_time: run.start_time, finish_time: run.finish_time, @@ -38,7 +39,7 @@ impl From for job::JobRun { impl From for auth::User { fn from(val: UserDAO) -> Self { auth::User { - id: juniper::ID::new(val.id.to_string()), + id: ID::new(DbConn::to_id(val.id)), email: val.email, is_admin: val.is_admin, auth_token: val.auth_token, @@ -77,7 +78,7 @@ impl From for OAuthCredential { impl From for Repository { fn from(value: RepositoryDAO) -> Self { Repository { - id: juniper::ID::new(value.id.to_string()), + id: ID::new(DbConn::to_id(value.id)), name: value.name, git_url: value.git_url, } diff --git a/ee/tabby-webserver/src/schema/mod.rs b/ee/tabby-webserver/src/schema/mod.rs index d8ee6f1e4c98..6766b597a7cd 100644 --- a/ee/tabby-webserver/src/schema/mod.rs +++ b/ee/tabby-webserver/src/schema/mod.rs @@ -5,7 +5,7 @@ pub mod job; pub mod repository; pub mod worker; -use std::{num::ParseIntError, sync::Arc}; +use std::sync::Arc; use auth::{ validate_jwt, AuthenticationService, Invitation, RefreshTokenError, RefreshTokenResponse, @@ -21,6 +21,7 @@ use juniper_axum::{ FromAuth, }; use tabby_common::api::{code::CodeSearch, event::RawEventLogger}; +use tabby_db::{DbConn, InvalidIDError}; use tracing::{error, warn}; use validator::ValidationErrors; use worker::{Worker, WorkerService}; @@ -67,7 +68,7 @@ pub enum CoreError { Other(#[from] anyhow::Error), #[error("Malformed ID input")] - InvalidIDError(#[from] ParseIntError), + InvalidID(#[from] InvalidIDError), } impl IntoFieldError for CoreError { @@ -304,7 +305,7 @@ impl Mutation { async fn update_user_active(ctx: &Context, id: ID, active: bool) -> Result { ctx.locator .auth() - .update_user_active(id.parse()?, active) + .update_user_active(DbConn::to_rowid(&id)?, active) .await?; Ok(true) } @@ -358,7 +359,7 @@ impl Mutation { "Failed to send invitation email, please check your SMTP settings are correct: {e}" ); } - Ok(ID::new(invitation.id.to_string())) + Ok(ID::new(DbConn::to_id(invitation.id))) } async fn create_repository( diff --git a/ee/tabby-webserver/src/service/auth.rs b/ee/tabby-webserver/src/service/auth.rs index be612913adce..b8934f9d76b7 100644 --- a/ee/tabby-webserver/src/service/auth.rs +++ b/ee/tabby-webserver/src/service/auth.rs @@ -298,8 +298,8 @@ impl AuthenticationService for DbConn { } async fn delete_invitation(&self, id: ID) -> Result { - let id = id.parse::()?; - Ok(ID::new(self.delete_invitation(id).await?.to_string())) + let id = DbConn::to_rowid(&id)?; + Ok(ID::new(DbConn::to_id(self.delete_invitation(id).await?))) } async fn reset_user_auth_token(&self, email: &str) -> Result<()> { @@ -569,7 +569,7 @@ mod tests { // Used invitation should have been deleted, following delete attempt should fail. assert!(conn - .delete_invitation(invitation.id.parse::().unwrap()) + .delete_invitation(DbConn::to_rowid(&invitation.id).unwrap()) .await .is_err()); } diff --git a/ee/tabby-webserver/src/service/repository.rs b/ee/tabby-webserver/src/service/repository.rs index fdec7b87ff0c..e01028c8d392 100644 --- a/ee/tabby-webserver/src/service/repository.rs +++ b/ee/tabby-webserver/src/service/repository.rs @@ -35,15 +35,15 @@ impl RepositoryService for DbConn { Ok(self .create_repository(input.name, input.git_url) .await - .map(|i| ID::new(i.to_string()))?) + .map(|id| ID::new(DbConn::to_id(id)))?) } async fn delete_repository(&self, id: ID) -> Result { - self.delete_repository(id.parse()?).await + self.delete_repository(DbConn::to_rowid(&id)?).await } async fn update_repository(&self, id: ID, name: String, git_url: String) -> Result { - self.update_repository(id.parse()?, name, git_url) + self.update_repository(DbConn::to_rowid(&id)?, name, git_url) .await .map(|_| true) }