collab: Remove `POST /users/:id/access_tokens` endpoint (#49297)

Marshall Bowers created

This PR removes the `POST /users/:id/access_tokens` endpoint from
Collab, as it is no longer used.

Closes CLO-290.

Release Notes:

- N/A

Change summary

crates/collab/src/api.rs                        | 63 ------------------
crates/collab/src/auth.rs                       | 45 -------------
crates/collab/src/db/queries/access_tokens.rs   |  4 
crates/collab/tests/integration/collab_tests.rs | 28 +++++++
4 files changed, 33 insertions(+), 107 deletions(-)

Detailed changes

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<rpc::Server>) -> 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<ErasedJson> {
     Ok(ErasedJson::pretty(rpc_server.snapshot().await))
 }
-
-#[derive(Deserialize)]
-struct CreateAccessTokenQueryParams {
-    public_key: String,
-    impersonate: Option<String>,
-}
-
-#[derive(Serialize)]
-struct CreateAccessTokenResponse {
-    user_id: UserId,
-    encrypted_access_token: String,
-}
-
-async fn create_access_token(
-    Path(user_id): Path<UserId>,
-    Query(params): Query<CreateAccessTokenQueryParams>,
-    Extension(app): Extension<Arc<AppState>>,
-) -> Result<Json<CreateAccessTokenResponse>> {
-    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,
-    }))
-}

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<B>(mut req: Request<B>, next: Next<B>) -> 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<UserId>,
-) -> Result<String> {
-    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<String> {
-    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<UserId>,

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<AccessTokenId> {
+        use sea_orm::sea_query::Query;
+
         self.transaction(|tx| async {
             let tx = tx;
 

crates/collab/tests/integration/collab_tests.rs 🔗

@@ -53,8 +53,7 @@ fn channel_id(room: &Entity<Room>, cx: &mut TestAppContext) -> Option<ChannelId>
 
 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<UserId>,
+    ) -> Result<String> {
+        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());