mirror of
https://github.com/zed-industries/zed.git
synced 2024-09-18 18:08:07 +03:00
Allow impersonating users via the api token, bypassing oauth
This commit is contained in:
parent
5e57a33df7
commit
c410935c9c
@ -13,11 +13,13 @@ use async_tungstenite::tungstenite::{
|
|||||||
http::{Request, StatusCode},
|
http::{Request, StatusCode},
|
||||||
};
|
};
|
||||||
use db::Db;
|
use db::Db;
|
||||||
use futures::{future::LocalBoxFuture, FutureExt, SinkExt, StreamExt, TryStreamExt};
|
use futures::{future::LocalBoxFuture, AsyncReadExt, FutureExt, SinkExt, StreamExt, TryStreamExt};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, serde_json::Value, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle,
|
actions,
|
||||||
AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
|
serde_json::{self, Value},
|
||||||
MutableAppContext, Task, View, ViewContext, ViewHandle,
|
AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, AppContext,
|
||||||
|
AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task, View, ViewContext,
|
||||||
|
ViewHandle,
|
||||||
};
|
};
|
||||||
use http::HttpClient;
|
use http::HttpClient;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
@ -25,6 +27,7 @@ use parking_lot::RwLock;
|
|||||||
use postage::watch;
|
use postage::watch;
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, RequestMessage};
|
use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, RequestMessage};
|
||||||
|
use serde::Deserialize;
|
||||||
use std::{
|
use std::{
|
||||||
any::TypeId,
|
any::TypeId,
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
@ -50,6 +53,9 @@ lazy_static! {
|
|||||||
pub static ref IMPERSONATE_LOGIN: Option<String> = std::env::var("ZED_IMPERSONATE")
|
pub static ref IMPERSONATE_LOGIN: Option<String> = std::env::var("ZED_IMPERSONATE")
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|s| if s.is_empty() { None } else { Some(s) });
|
.and_then(|s| if s.is_empty() { None } else { Some(s) });
|
||||||
|
pub static ref ADMIN_API_TOKEN: Option<String> = std::env::var("ZED_ADMIN_API_TOKEN")
|
||||||
|
.ok()
|
||||||
|
.and_then(|s| if s.is_empty() { None } else { Some(s) });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const ZED_SECRET_CLIENT_TOKEN: &str = "618033988749894";
|
pub const ZED_SECRET_CLIENT_TOKEN: &str = "618033988749894";
|
||||||
@ -919,6 +925,32 @@ impl Client {
|
|||||||
self.establish_websocket_connection(credentials, cx)
|
self.establish_websocket_connection(credentials, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_rpc_url(http: Arc<dyn HttpClient>) -> Result<Url> {
|
||||||
|
let rpc_response = http
|
||||||
|
.get(
|
||||||
|
&(format!("{}/rpc", *ZED_SERVER_URL)),
|
||||||
|
Default::default(),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
if !rpc_response.status().is_redirection() {
|
||||||
|
Err(anyhow!(
|
||||||
|
"unexpected /rpc response status {}",
|
||||||
|
rpc_response.status()
|
||||||
|
))?
|
||||||
|
}
|
||||||
|
|
||||||
|
let rpc_url = rpc_response
|
||||||
|
.headers()
|
||||||
|
.get("Location")
|
||||||
|
.ok_or_else(|| anyhow!("missing location header in /rpc response"))?
|
||||||
|
.to_str()
|
||||||
|
.map_err(EstablishConnectionError::other)?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
Url::parse(&rpc_url).context("invalid rpc url")
|
||||||
|
}
|
||||||
|
|
||||||
fn establish_websocket_connection(
|
fn establish_websocket_connection(
|
||||||
self: &Arc<Self>,
|
self: &Arc<Self>,
|
||||||
credentials: &Credentials,
|
credentials: &Credentials,
|
||||||
@ -933,28 +965,7 @@ impl Client {
|
|||||||
|
|
||||||
let http = self.http.clone();
|
let http = self.http.clone();
|
||||||
cx.background().spawn(async move {
|
cx.background().spawn(async move {
|
||||||
let mut rpc_url = format!("{}/rpc", *ZED_SERVER_URL);
|
let mut rpc_url = Self::get_rpc_url(http).await?;
|
||||||
let rpc_response = http.get(&rpc_url, Default::default(), false).await?;
|
|
||||||
if rpc_response.status().is_redirection() {
|
|
||||||
rpc_url = rpc_response
|
|
||||||
.headers()
|
|
||||||
.get("Location")
|
|
||||||
.ok_or_else(|| anyhow!("missing location header in /rpc response"))?
|
|
||||||
.to_str()
|
|
||||||
.map_err(EstablishConnectionError::other)?
|
|
||||||
.to_string();
|
|
||||||
}
|
|
||||||
// Until we switch the zed.dev domain to point to the new Next.js app, there
|
|
||||||
// will be no redirect required, and the app will connect directly to
|
|
||||||
// wss://zed.dev/rpc.
|
|
||||||
else if rpc_response.status() != StatusCode::UPGRADE_REQUIRED {
|
|
||||||
Err(anyhow!(
|
|
||||||
"unexpected /rpc response status {}",
|
|
||||||
rpc_response.status()
|
|
||||||
))?
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut rpc_url = Url::parse(&rpc_url).context("invalid rpc url")?;
|
|
||||||
let rpc_host = rpc_url
|
let rpc_host = rpc_url
|
||||||
.host_str()
|
.host_str()
|
||||||
.zip(rpc_url.port_or_known_default())
|
.zip(rpc_url.port_or_known_default())
|
||||||
@ -997,6 +1008,7 @@ impl Client {
|
|||||||
let platform = cx.platform();
|
let platform = cx.platform();
|
||||||
let executor = cx.background();
|
let executor = cx.background();
|
||||||
let telemetry = self.telemetry.clone();
|
let telemetry = self.telemetry.clone();
|
||||||
|
let http = self.http.clone();
|
||||||
executor.clone().spawn(async move {
|
executor.clone().spawn(async move {
|
||||||
// Generate a pair of asymmetric encryption keys. The public key will be used by the
|
// Generate a pair of asymmetric encryption keys. The public key will be used by the
|
||||||
// zed server to encrypt the user's access token, so that it can'be intercepted by
|
// zed server to encrypt the user's access token, so that it can'be intercepted by
|
||||||
@ -1006,6 +1018,10 @@ impl Client {
|
|||||||
let public_key_string =
|
let public_key_string =
|
||||||
String::try_from(public_key).expect("failed to serialize public key for auth");
|
String::try_from(public_key).expect("failed to serialize public key for auth");
|
||||||
|
|
||||||
|
if let Some((login, token)) = IMPERSONATE_LOGIN.as_ref().zip(ADMIN_API_TOKEN.as_ref()) {
|
||||||
|
return Self::authenticate_as_admin(http, login.clone(), token.clone()).await;
|
||||||
|
}
|
||||||
|
|
||||||
// Start an HTTP server to receive the redirect from Zed's sign-in page.
|
// Start an HTTP server to receive the redirect from Zed's sign-in page.
|
||||||
let server = tiny_http::Server::http("127.0.0.1:0").expect("failed to find open port");
|
let server = tiny_http::Server::http("127.0.0.1:0").expect("failed to find open port");
|
||||||
let port = server.server_addr().port();
|
let port = server.server_addr().port();
|
||||||
@ -1084,6 +1100,49 @@ impl Client {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn authenticate_as_admin(
|
||||||
|
http: Arc<dyn HttpClient>,
|
||||||
|
login: String,
|
||||||
|
mut api_token: String,
|
||||||
|
) -> Result<Credentials> {
|
||||||
|
let mut url = Self::get_rpc_url(http.clone()).await?;
|
||||||
|
url.set_path("/user");
|
||||||
|
url.set_query(Some(&format!("github_login={login}")));
|
||||||
|
let request = Request::get(url.as_str())
|
||||||
|
.header("Authorization", format!("token {api_token}"))
|
||||||
|
.body("".into())?;
|
||||||
|
|
||||||
|
let mut response = http.send(request).await?;
|
||||||
|
let mut body = String::new();
|
||||||
|
response.body_mut().read_to_string(&mut body).await?;
|
||||||
|
|
||||||
|
if !response.status().is_success() {
|
||||||
|
Err(anyhow!(
|
||||||
|
"admin user request failed {} - {}",
|
||||||
|
response.status().as_u16(),
|
||||||
|
body,
|
||||||
|
))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct AuthenticatedUserResponse {
|
||||||
|
user: User,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct User {
|
||||||
|
id: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
let response: AuthenticatedUserResponse = serde_json::from_str(&body)?;
|
||||||
|
|
||||||
|
api_token.insert_str(0, "ADMIN_TOKEN:");
|
||||||
|
Ok(Credentials {
|
||||||
|
user_id: response.user.id,
|
||||||
|
access_token: api_token,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn disconnect(self: &Arc<Self>, cx: &AsyncAppContext) -> Result<()> {
|
pub fn disconnect(self: &Arc<Self>, cx: &AsyncAppContext) -> Result<()> {
|
||||||
let conn_id = self.connection_id()?;
|
let conn_id = self.connection_id()?;
|
||||||
self.peer.disconnect(conn_id);
|
self.peer.disconnect(conn_id);
|
||||||
|
@ -3,7 +3,5 @@ HTTP_PORT = 8080
|
|||||||
API_TOKEN = "secret"
|
API_TOKEN = "secret"
|
||||||
INVITE_LINK_PREFIX = "http://localhost:3000/invites/"
|
INVITE_LINK_PREFIX = "http://localhost:3000/invites/"
|
||||||
|
|
||||||
# HONEYCOMB_API_KEY=
|
|
||||||
# HONEYCOMB_DATASET=
|
|
||||||
# RUST_LOG=info
|
# RUST_LOG=info
|
||||||
# LOG_JSON=true
|
# LOG_JSON=true
|
||||||
|
@ -88,7 +88,7 @@ pub async fn validate_api_token<B>(req: Request<B>, next: Next<B>) -> impl IntoR
|
|||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct AuthenticatedUserParams {
|
struct AuthenticatedUserParams {
|
||||||
github_user_id: i32,
|
github_user_id: Option<i32>,
|
||||||
github_login: String,
|
github_login: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,7 +104,7 @@ async fn get_authenticated_user(
|
|||||||
) -> Result<Json<AuthenticatedUserResponse>> {
|
) -> Result<Json<AuthenticatedUserResponse>> {
|
||||||
let user = app
|
let user = app
|
||||||
.db
|
.db
|
||||||
.get_user_by_github_account(¶ms.github_login, Some(params.github_user_id))
|
.get_user_by_github_account(¶ms.github_login, params.github_user_id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| Error::Http(StatusCode::NOT_FOUND, "user not found".into()))?;
|
.ok_or_else(|| Error::Http(StatusCode::NOT_FOUND, "user not found".into()))?;
|
||||||
let metrics_id = app.db.get_user_metrics_id(user.id).await?;
|
let metrics_id = app.db.get_user_metrics_id(user.id).await?;
|
||||||
|
@ -41,12 +41,18 @@ pub async fn validate_header<B>(mut req: Request<B>, next: Next<B>) -> impl Into
|
|||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let state = req.extensions().get::<Arc<AppState>>().unwrap();
|
|
||||||
let mut credentials_valid = false;
|
let mut credentials_valid = false;
|
||||||
for password_hash in state.db.get_access_token_hashes(user_id).await? {
|
let state = req.extensions().get::<Arc<AppState>>().unwrap();
|
||||||
if verify_access_token(access_token, &password_hash)? {
|
if let Some(admin_token) = access_token.strip_prefix("ADMIN_TOKEN:") {
|
||||||
|
if state.config.api_token == admin_token {
|
||||||
credentials_valid = true;
|
credentials_valid = true;
|
||||||
break;
|
}
|
||||||
|
} else {
|
||||||
|
for password_hash in state.db.get_access_token_hashes(user_id).await? {
|
||||||
|
if verify_access_token(access_token, &password_hash)? {
|
||||||
|
credentials_valid = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1 +1 @@
|
|||||||
ZED_SERVER_URL=http://localhost:3000 cargo run $@
|
ZED_ADMIN_API_TOKEN=secret ZED_SERVER_URL=http://localhost:3000 cargo run $@
|
||||||
|
Loading…
Reference in New Issue
Block a user