Fix impersonation in local development (#16755)

Marshall Bowers created

This PR fixes impersonation in local development by fetching the user
from the GitHub API so we can get their `github_user_id`.

The `github_user_id` is now required after #16704.

Since this is just a development flow, we're fetching the user on the
client as opposed to making changes on the server.

This request uses the `GITHUB_TOKEN` environment variable for
authentication, if it exists, or will make an unauthenticated GitHub API
request.

Release Notes:

- N/A

Change summary

crates/client/src/client.rs | 57 +++++++++++++++++++++++++++++++++++++-
1 file changed, 55 insertions(+), 2 deletions(-)

Detailed changes

crates/client/src/client.rs 🔗

@@ -5,7 +5,7 @@ mod socks;
 pub mod telemetry;
 pub mod user;
 
-use anyhow::{anyhow, Context as _, Result};
+use anyhow::{anyhow, bail, Context as _, Result};
 use async_recursion::async_recursion;
 use async_tungstenite::tungstenite::{
     client::IntoClientRequest,
@@ -1395,11 +1395,64 @@ impl Client {
             id: u64,
         }
 
+        let github_user = {
+            #[derive(Deserialize)]
+            struct GithubUser {
+                id: i32,
+                login: String,
+            }
+
+            let request = {
+                let mut request_builder =
+                    Request::get(&format!("https://api.github.com/users/{login}"));
+                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()
+                );
+            }
+
+            let user = 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")
+            })?;
+
+            user
+        };
+
         // 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(&format!("github_login={login}")));
+        url.set_query(Some(&format!(
+            "github_login={}&github_user_id={}",
+            github_user.login, github_user.id
+        )));
         let request: http_client::Request<AsyncBody> = Request::get(url.as_str())
             .header("Authorization", format!("token {api_token}"))
             .body("".into())?;