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:
parent
7c169fc9b5
commit
09b93caa9b
3 changed files with 25 additions and 139 deletions
|
@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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(
|
|
||||||
¶ms.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,
|
||||||
|
|
|
@ -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",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue