diff --git a/crates/collab/src/api.rs b/crates/collab/src/api.rs index e70bbe8fe875d479a7a65618d0a247b7804599fa..4debbc81f8b3c9f0b76299dccf34f5a7c18e2544 100644 --- a/crates/collab/src/api.rs +++ b/crates/collab/src/api.rs @@ -1,20 +1,17 @@ pub mod events; pub mod extensions; -use crate::{AppState, Error, Result, auth, db::UserId, rpc}; -use anyhow::Context as _; +use crate::{AppState, Error, Result, rpc}; use axum::{ - Extension, Json, Router, + Extension, Router, body::Body, - extract::{Path, Query}, headers::Header, http::{self, HeaderName, Request, StatusCode}, middleware::{self, Next}, response::IntoResponse, - routing::{get, post}, + routing::get, }; use axum_extra::response::ErasedJson; -use serde::{Deserialize, Serialize}; use std::sync::{Arc, OnceLock}; use tower::ServiceBuilder; @@ -88,7 +85,6 @@ impl std::fmt::Display for SystemIdHeader { pub fn routes(rpc_server: Arc) -> Router<(), Body> { Router::new() - .route("/users/:id/access_tokens", post(create_access_token)) .route("/rpc_server_snapshot", get(get_rpc_server_snapshot)) .layer( ServiceBuilder::new() @@ -133,56 +129,3 @@ async fn get_rpc_server_snapshot( ) -> Result { Ok(ErasedJson::pretty(rpc_server.snapshot().await)) } - -#[derive(Deserialize)] -struct CreateAccessTokenQueryParams { - public_key: String, - impersonate: Option, -} - -#[derive(Serialize)] -struct CreateAccessTokenResponse { - user_id: UserId, - encrypted_access_token: String, -} - -async fn create_access_token( - Path(user_id): Path, - Query(params): Query, - Extension(app): Extension>, -) -> Result> { - let user = app - .db - .get_user_by_id(user_id) - .await? - .context("user not found")?; - - let mut impersonated_user_id = None; - if let Some(impersonate) = params.impersonate { - if user.admin { - if let Some(impersonated_user) = app.db.get_user_by_github_login(&impersonate).await? { - impersonated_user_id = Some(impersonated_user.id); - } else { - return Err(Error::http( - StatusCode::UNPROCESSABLE_ENTITY, - format!("user {impersonate} does not exist"), - )); - } - } else { - return Err(Error::http( - StatusCode::UNAUTHORIZED, - "you do not have permission to impersonate other users".to_string(), - )); - } - } - - let access_token = - auth::create_access_token(app.db.as_ref(), user_id, impersonated_user_id).await?; - let encrypted_access_token = - auth::encrypt_access_token(&access_token, params.public_key.clone())?; - - Ok(Json(CreateAccessTokenResponse { - user_id: impersonated_user_id.unwrap_or(user_id), - encrypted_access_token, - })) -} diff --git a/crates/collab/src/auth.rs b/crates/collab/src/auth.rs index 3134b6dff694ba6fcfbcfcbb6f4d73391d14b91c..b844bdcd58b3aad706e30611d8f37aad61d29b13 100644 --- a/crates/collab/src/auth.rs +++ b/crates/collab/src/auth.rs @@ -1,6 +1,6 @@ use crate::{ AppState, Error, Result, - db::{self, AccessTokenId, Database, UserId}, + db::{AccessTokenId, Database, UserId}, rpc::Principal, }; use anyhow::Context as _; @@ -108,8 +108,6 @@ pub async fn validate_header(mut req: Request, next: Next) -> impl Into )) } -pub const MAX_ACCESS_TOKENS_TO_STORE: usize = 8; - #[derive(Serialize, Deserialize)] pub struct AccessTokenJson { pub version: usize, @@ -117,31 +115,6 @@ pub struct AccessTokenJson { pub token: String, } -/// Creates a new access token to identify the given user. before returning it, you should -/// encrypt it with the user's public key. -pub async fn create_access_token( - db: &db::Database, - user_id: UserId, - impersonated_user_id: Option, -) -> Result { - const VERSION: usize = 1; - let access_token = rpc::auth::random_token(); - let access_token_hash = hash_access_token(&access_token); - let id = db - .create_access_token( - user_id, - impersonated_user_id, - &access_token_hash, - MAX_ACCESS_TOKENS_TO_STORE, - ) - .await?; - Ok(serde_json::to_string(&AccessTokenJson { - version: VERSION, - id, - token: access_token, - })?) -} - /// Hashing prevents anyone with access to the database being able to login. /// As the token is randomly generated, we don't need to worry about scrypt-style /// protection. @@ -150,22 +123,6 @@ pub fn hash_access_token(token: &str) -> String { format!("$sha256${}", BASE64_URL_SAFE.encode(digest)) } -/// Encrypts the given access token with the given public key to avoid leaking it on the way -/// to the client. -pub fn encrypt_access_token(access_token: &str, public_key: String) -> Result { - use rpc::auth::EncryptionFormat; - - /// The encryption format to use for the access token. - const ENCRYPTION_FORMAT: EncryptionFormat = EncryptionFormat::V1; - - let native_app_public_key = - rpc::auth::PublicKey::try_from(public_key).context("failed to parse app public key")?; - let encrypted_access_token = native_app_public_key - .encrypt_string(access_token, ENCRYPTION_FORMAT) - .context("failed to encrypt access token with public key")?; - Ok(encrypted_access_token) -} - pub struct VerifyAccessTokenResult { pub is_valid: bool, pub impersonator_id: Option, diff --git a/crates/collab/src/db/queries/access_tokens.rs b/crates/collab/src/db/queries/access_tokens.rs index 6ddecc64d0769eac572e6afb703bfb393416cfc9..c7921639d206fe0ff086df790234fa1be7f45658 100644 --- a/crates/collab/src/db/queries/access_tokens.rs +++ b/crates/collab/src/db/queries/access_tokens.rs @@ -1,9 +1,9 @@ use super::*; use anyhow::Context as _; -use sea_orm::sea_query::Query; impl Database { /// Creates a new access token for the given user. + #[cfg(any(test, feature = "test-support"))] pub async fn create_access_token( &self, user_id: UserId, @@ -11,6 +11,8 @@ impl Database { access_token_hash: &str, max_access_token_count: usize, ) -> Result { + use sea_orm::sea_query::Query; + self.transaction(|tx| async { let tx = tx; diff --git a/crates/collab/tests/integration/collab_tests.rs b/crates/collab/tests/integration/collab_tests.rs index 239fe4f0135672750e76c40aa51e427a9573ce40..c5d3e05c48dff81ea6589c2aa0c65f809c4da4f9 100644 --- a/crates/collab/tests/integration/collab_tests.rs +++ b/crates/collab/tests/integration/collab_tests.rs @@ -53,8 +53,7 @@ fn channel_id(room: &Entity, cx: &mut TestAppContext) -> Option mod auth_token_tests { use collab::auth::{ - AccessTokenJson, MAX_ACCESS_TOKENS_TO_STORE, VerifyAccessTokenResult, create_access_token, - verify_access_token, + AccessTokenJson, VerifyAccessTokenResult, hash_access_token, verify_access_token, }; use rand::prelude::*; use scrypt::Scrypt; @@ -64,6 +63,31 @@ mod auth_token_tests { use collab::db::{Database, NewUserParams, UserId, access_token}; use collab::*; + const MAX_ACCESS_TOKENS_TO_STORE: usize = 8; + + async fn create_access_token( + db: &db::Database, + user_id: UserId, + impersonated_user_id: Option, + ) -> Result { + const VERSION: usize = 1; + let access_token = ::rpc::auth::random_token(); + let access_token_hash = hash_access_token(&access_token); + let id = db + .create_access_token( + user_id, + impersonated_user_id, + &access_token_hash, + MAX_ACCESS_TOKENS_TO_STORE, + ) + .await?; + Ok(serde_json::to_string(&AccessTokenJson { + version: VERSION, + id, + token: access_token, + })?) + } + #[gpui::test] async fn test_verify_access_token(cx: &mut gpui::TestAppContext) { let test_db = crate::db_tests::TestDb::sqlite(cx.executor());