Rework authentication for local Cloud/Collab development (#35450)

This PR reworks authentication for developing Zed against a local
version of Cloud and/or Collab.

You will still connect the same way—using the `zed-local` script—but
will need to be running an instance of Cloud locally.

Release Notes:

- N/A
This commit is contained in:
Marshall Bowers 2025-07-31 20:55:17 -04:00 committed by GitHub
parent 7c169fc9b5
commit 09b93caa9b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 25 additions and 139 deletions

View file

@ -7,14 +7,13 @@ pub mod telemetry;
pub mod user; pub mod user;
pub mod zed_urls; pub mod zed_urls;
use anyhow::{Context as _, Result, anyhow, bail}; use anyhow::{Context as _, Result, anyhow};
use async_recursion::async_recursion; use async_recursion::async_recursion;
use async_tungstenite::tungstenite::{ use async_tungstenite::tungstenite::{
client::IntoClientRequest, client::IntoClientRequest,
error::Error as WebsocketError, error::Error as WebsocketError,
http::{HeaderValue, Request, StatusCode}, http::{HeaderValue, Request, StatusCode},
}; };
use chrono::{DateTime, Utc};
use clock::SystemClock; use clock::SystemClock;
use cloud_api_client::CloudApiClient; use cloud_api_client::CloudApiClient;
use credentials_provider::CredentialsProvider; use credentials_provider::CredentialsProvider;
@ -23,7 +22,7 @@ use futures::{
channel::oneshot, future::BoxFuture, channel::oneshot, future::BoxFuture,
}; };
use gpui::{App, AsyncApp, Entity, Global, Task, WeakEntity, actions}; use gpui::{App, AsyncApp, Entity, Global, Task, WeakEntity, actions};
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl, http}; use http_client::{HttpClient, HttpClientWithUrl, http};
use parking_lot::RwLock; use parking_lot::RwLock;
use postage::watch; use postage::watch;
use proxy::connect_proxy_stream; use proxy::connect_proxy_stream;
@ -1379,96 +1378,31 @@ impl Client {
self: &Arc<Self>, self: &Arc<Self>,
http: Arc<HttpClientWithUrl>, http: Arc<HttpClientWithUrl>,
login: String, login: String,
mut api_token: String, api_token: String,
) -> Result<Credentials> { ) -> Result<Credentials> {
#[derive(Deserialize)] #[derive(Serialize)]
struct AuthenticatedUserResponse { struct ImpersonateUserBody {
user: User, github_login: String,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
struct User { struct ImpersonateUserResponse {
id: u64, user_id: u64,
access_token: String,
} }
let github_user = { let url = self
#[derive(Deserialize)] .http
struct GithubUser { .build_zed_cloud_url("/internal/users/impersonate", &[])?;
id: i32, let request = Request::post(url.as_str())
login: String, .header("Content-Type", "application/json")
created_at: DateTime<Utc>, .header("Authorization", format!("Bearer {api_token}"))
} .body(
serde_json::to_string(&ImpersonateUserBody {
let request = { github_login: login,
let mut request_builder = })?
Request::get(&format!("https://api.github.com/users/{login}")); .into(),
if let Ok(github_token) = std::env::var("GITHUB_TOKEN") { )?;
request_builder =
request_builder.header("Authorization", format!("Bearer {}", github_token));
}
request_builder.body(AsyncBody::empty())?
};
let mut response = http
.send(request)
.await
.context("error fetching GitHub user")?;
let mut body = Vec::new();
response
.body_mut()
.read_to_end(&mut body)
.await
.context("error reading GitHub user")?;
if !response.status().is_success() {
let text = String::from_utf8_lossy(body.as_slice());
bail!(
"status error {}, response: {text:?}",
response.status().as_u16()
);
}
serde_json::from_slice::<GithubUser>(body.as_slice()).map_err(|err| {
log::error!("Error deserializing: {:?}", err);
log::error!(
"GitHub API response text: {:?}",
String::from_utf8_lossy(body.as_slice())
);
anyhow!("error deserializing GitHub user")
})?
};
let query_params = [
("github_login", &github_user.login),
("github_user_id", &github_user.id.to_string()),
(
"github_user_created_at",
&github_user.created_at.to_rfc3339(),
),
];
// Use the collab server's admin API to retrieve the ID
// of the impersonated user.
let mut url = self.rpc_url(http.clone(), None).await?;
url.set_path("/user");
url.set_query(Some(
&query_params
.iter()
.map(|(key, value)| {
format!(
"{}={}",
key,
url::form_urlencoded::byte_serialize(value.as_bytes()).collect::<String>()
)
})
.collect::<Vec<String>>()
.join("&"),
));
let request: http_client::Request<AsyncBody> = Request::get(url.as_str())
.header("Authorization", format!("token {api_token}"))
.body("".into())?;
let mut response = http.send(request).await?; let mut response = http.send(request).await?;
let mut body = String::new(); let mut body = String::new();
@ -1479,13 +1413,11 @@ impl Client {
response.status().as_u16(), response.status().as_u16(),
body, body,
); );
let response: AuthenticatedUserResponse = serde_json::from_str(&body)?; let response: ImpersonateUserResponse = serde_json::from_str(&body)?;
// Use the admin API token to authenticate as the impersonated user.
api_token.insert_str(0, "ADMIN_TOKEN:");
Ok(Credentials { Ok(Credentials {
user_id: response.user.id, user_id: response.user_id,
access_token: api_token, access_token: response.access_token,
}) })
} }

View file

@ -100,7 +100,6 @@ impl std::fmt::Display for SystemIdHeader {
pub fn routes(rpc_server: Arc<rpc::Server>) -> Router<(), Body> { pub fn routes(rpc_server: Arc<rpc::Server>) -> Router<(), Body> {
Router::new() Router::new()
.route("/user", get(legacy_update_or_create_authenticated_user))
.route("/users/look_up", get(look_up_user)) .route("/users/look_up", get(look_up_user))
.route("/users/:id/access_tokens", post(create_access_token)) .route("/users/:id/access_tokens", post(create_access_token))
.route("/users/:id/refresh_llm_tokens", post(refresh_llm_tokens)) .route("/users/:id/refresh_llm_tokens", post(refresh_llm_tokens))
@ -145,51 +144,6 @@ pub async fn validate_api_token<B>(req: Request<B>, next: Next<B>) -> impl IntoR
Ok::<_, Error>(next.run(req).await) Ok::<_, Error>(next.run(req).await)
} }
#[derive(Debug, Deserialize)]
struct AuthenticatedUserParams {
github_user_id: i32,
github_login: String,
github_email: Option<String>,
github_name: Option<String>,
github_user_created_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Serialize)]
struct AuthenticatedUserResponse {
user: User,
metrics_id: String,
feature_flags: Vec<String>,
}
/// This is a legacy endpoint that is no longer used in production.
///
/// It currently only exists to be used when developing Collab locally.
async fn legacy_update_or_create_authenticated_user(
Query(params): Query<AuthenticatedUserParams>,
Extension(app): Extension<Arc<AppState>>,
) -> Result<Json<AuthenticatedUserResponse>> {
let initial_channel_id = app.config.auto_join_channel_id;
let user = app
.db
.update_or_create_user_by_github_account(
&params.github_login,
params.github_user_id,
params.github_email.as_deref(),
params.github_name.as_deref(),
params.github_user_created_at,
initial_channel_id,
)
.await?;
let metrics_id = app.db.get_user_metrics_id(user.id).await?;
let feature_flags = app.db.get_user_flags(user.id).await?;
Ok(Json(AuthenticatedUserResponse {
user,
metrics_id,
feature_flags,
}))
}
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct LookUpUserParams { struct LookUpUserParams {
identifier: String, identifier: String,

View file

@ -213,7 +213,7 @@ setTimeout(() => {
platform === "win32" platform === "win32"
? "http://127.0.0.1:8080/rpc" ? "http://127.0.0.1:8080/rpc"
: "http://localhost:8080/rpc", : "http://localhost:8080/rpc",
ZED_ADMIN_API_TOKEN: "secret", ZED_ADMIN_API_TOKEN: "internal-api-key-secret",
ZED_WINDOW_SIZE: size, ZED_WINDOW_SIZE: size,
ZED_CLIENT_CHECKSUM_SEED: "development-checksum-seed", ZED_CLIENT_CHECKSUM_SEED: "development-checksum-seed",
RUST_LOG: process.env.RUST_LOG || "info", RUST_LOG: process.env.RUST_LOG || "info",