Merge branch 'main' into site-v2
This commit is contained in:
+43
-44
@@ -18,7 +18,7 @@ use scrypt::{
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{borrow::Cow, convert::TryFrom, sync::Arc};
|
||||
use surf::{StatusCode, Url};
|
||||
use tide::{log, Server};
|
||||
use tide::{log, Error, Server};
|
||||
use zrpc::auth as zed_auth;
|
||||
|
||||
static CURRENT_GITHUB_USER: &'static str = "current_github_user";
|
||||
@@ -33,51 +33,48 @@ pub struct User {
|
||||
pub is_admin: bool,
|
||||
}
|
||||
|
||||
pub struct VerifyToken;
|
||||
pub async fn process_auth_header(request: &Request) -> tide::Result<UserId> {
|
||||
let mut auth_header = request
|
||||
.header("Authorization")
|
||||
.ok_or_else(|| {
|
||||
Error::new(
|
||||
StatusCode::BadRequest,
|
||||
anyhow!("missing authorization header"),
|
||||
)
|
||||
})?
|
||||
.last()
|
||||
.as_str()
|
||||
.split_whitespace();
|
||||
let user_id = UserId(auth_header.next().unwrap_or("").parse().map_err(|_| {
|
||||
Error::new(
|
||||
StatusCode::BadRequest,
|
||||
anyhow!("missing user id in authorization header"),
|
||||
)
|
||||
})?);
|
||||
let access_token = auth_header.next().ok_or_else(|| {
|
||||
Error::new(
|
||||
StatusCode::BadRequest,
|
||||
anyhow!("missing access token in authorization header"),
|
||||
)
|
||||
})?;
|
||||
|
||||
#[async_trait]
|
||||
impl tide::Middleware<Arc<AppState>> for VerifyToken {
|
||||
async fn handle(
|
||||
&self,
|
||||
mut request: Request,
|
||||
next: tide::Next<'_, Arc<AppState>>,
|
||||
) -> tide::Result {
|
||||
let mut auth_header = request
|
||||
.header("Authorization")
|
||||
.ok_or_else(|| anyhow!("no authorization header"))?
|
||||
.last()
|
||||
.as_str()
|
||||
.split_whitespace();
|
||||
|
||||
let user_id = UserId(
|
||||
auth_header
|
||||
.next()
|
||||
.ok_or_else(|| anyhow!("missing user id in authorization header"))?
|
||||
.parse()?,
|
||||
);
|
||||
let access_token = auth_header
|
||||
.next()
|
||||
.ok_or_else(|| anyhow!("missing access token in authorization header"))?;
|
||||
|
||||
let state = request.state().clone();
|
||||
|
||||
let mut credentials_valid = false;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
if credentials_valid {
|
||||
request.set_ext(user_id);
|
||||
Ok(next.run(request).await)
|
||||
} else {
|
||||
let mut response = tide::Response::new(StatusCode::Unauthorized);
|
||||
response.set_body("invalid credentials");
|
||||
Ok(response)
|
||||
let state = request.state().clone();
|
||||
let mut credentials_valid = false;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
if !credentials_valid {
|
||||
Err(Error::new(
|
||||
StatusCode::Unauthorized,
|
||||
anyhow!("invalid credentials"),
|
||||
))?;
|
||||
}
|
||||
|
||||
Ok(user_id)
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -263,11 +260,13 @@ async fn post_sign_out(mut request: Request) -> tide::Result {
|
||||
Ok(tide::Redirect::new("/").into())
|
||||
}
|
||||
|
||||
const MAX_ACCESS_TOKENS_TO_STORE: usize = 8;
|
||||
|
||||
pub async fn create_access_token(db: &db::Db, user_id: UserId) -> tide::Result<String> {
|
||||
let access_token = zed_auth::random_token();
|
||||
let access_token_hash =
|
||||
hash_access_token(&access_token).context("failed to hash access token")?;
|
||||
db.create_access_token_hash(user_id, access_token_hash)
|
||||
db.create_access_token_hash(user_id, &access_token_hash, MAX_ACCESS_TOKENS_TO_STORE)
|
||||
.await?;
|
||||
Ok(access_token)
|
||||
}
|
||||
|
||||
+66
-11
@@ -175,25 +175,48 @@ impl Db {
|
||||
pub async fn create_access_token_hash(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
access_token_hash: String,
|
||||
access_token_hash: &str,
|
||||
max_access_token_count: usize,
|
||||
) -> Result<()> {
|
||||
test_support!(self, {
|
||||
let query = "
|
||||
INSERT INTO access_tokens (user_id, hash)
|
||||
VALUES ($1, $2)
|
||||
";
|
||||
sqlx::query(query)
|
||||
let insert_query = "
|
||||
INSERT INTO access_tokens (user_id, hash)
|
||||
VALUES ($1, $2);
|
||||
";
|
||||
let cleanup_query = "
|
||||
DELETE FROM access_tokens
|
||||
WHERE id IN (
|
||||
SELECT id from access_tokens
|
||||
WHERE user_id = $1
|
||||
ORDER BY id DESC
|
||||
OFFSET $3
|
||||
)
|
||||
";
|
||||
|
||||
let mut tx = self.pool.begin().await?;
|
||||
sqlx::query(insert_query)
|
||||
.bind(user_id.0)
|
||||
.bind(access_token_hash)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map(drop)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
sqlx::query(cleanup_query)
|
||||
.bind(user_id.0)
|
||||
.bind(access_token_hash)
|
||||
.bind(max_access_token_count as u32)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
tx.commit().await
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_access_token_hashes(&self, user_id: UserId) -> Result<Vec<String>> {
|
||||
test_support!(self, {
|
||||
let query = "SELECT hash FROM access_tokens WHERE user_id = $1";
|
||||
let query = "
|
||||
SELECT hash
|
||||
FROM access_tokens
|
||||
WHERE user_id = $1
|
||||
ORDER BY id DESC
|
||||
";
|
||||
sqlx::query_scalar(query)
|
||||
.bind(user_id.0)
|
||||
.fetch_all(&self.pool)
|
||||
@@ -652,4 +675,36 @@ pub mod tests {
|
||||
assert_eq!(msg1_id, msg3_id);
|
||||
assert_eq!(msg2_id, msg4_id);
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_create_access_tokens() {
|
||||
let test_db = TestDb::new();
|
||||
let db = test_db.db();
|
||||
let user = db.create_user("the-user", false).await.unwrap();
|
||||
|
||||
db.create_access_token_hash(user, "h1", 3).await.unwrap();
|
||||
db.create_access_token_hash(user, "h2", 3).await.unwrap();
|
||||
assert_eq!(
|
||||
db.get_access_token_hashes(user).await.unwrap(),
|
||||
&["h2".to_string(), "h1".to_string()]
|
||||
);
|
||||
|
||||
db.create_access_token_hash(user, "h3", 3).await.unwrap();
|
||||
assert_eq!(
|
||||
db.get_access_token_hashes(user).await.unwrap(),
|
||||
&["h3".to_string(), "h2".to_string(), "h1".to_string(),]
|
||||
);
|
||||
|
||||
db.create_access_token_hash(user, "h4", 3).await.unwrap();
|
||||
assert_eq!(
|
||||
db.get_access_token_hashes(user).await.unwrap(),
|
||||
&["h4".to_string(), "h3".to_string(), "h2".to_string(),]
|
||||
);
|
||||
|
||||
db.create_access_token_hash(user, "h5", 3).await.unwrap();
|
||||
assert_eq!(
|
||||
db.get_access_token_hashes(user).await.unwrap(),
|
||||
&["h5".to_string(), "h4".to_string(), "h3".to_string()]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+17
-6
@@ -1,7 +1,7 @@
|
||||
mod store;
|
||||
|
||||
use super::{
|
||||
auth,
|
||||
auth::process_auth_header,
|
||||
db::{ChannelId, MessageId, UserId},
|
||||
AppState,
|
||||
};
|
||||
@@ -885,8 +885,7 @@ where
|
||||
|
||||
pub fn add_routes(app: &mut tide::Server<Arc<AppState>>, rpc: &Arc<Peer>) {
|
||||
let server = Server::new(app.state().clone(), rpc.clone(), None);
|
||||
app.at("/rpc").with(auth::VerifyToken).get(move |request: Request<Arc<AppState>>| {
|
||||
let user_id = request.ext::<UserId>().copied();
|
||||
app.at("/rpc").get(move |request: Request<Arc<AppState>>| {
|
||||
let server = server.clone();
|
||||
async move {
|
||||
const WEBSOCKET_GUID: &str = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
||||
@@ -894,8 +893,11 @@ pub fn add_routes(app: &mut tide::Server<Arc<AppState>>, rpc: &Arc<Peer>) {
|
||||
let connection_upgrade = header_contains_ignore_case(&request, CONNECTION, "upgrade");
|
||||
let upgrade_to_websocket = header_contains_ignore_case(&request, UPGRADE, "websocket");
|
||||
let upgrade_requested = connection_upgrade && upgrade_to_websocket;
|
||||
let client_protocol_version: Option<u32> = request
|
||||
.header("X-Zed-Protocol-Version")
|
||||
.and_then(|v| v.as_str().parse().ok());
|
||||
|
||||
if !upgrade_requested {
|
||||
if !upgrade_requested || client_protocol_version != Some(zrpc::PROTOCOL_VERSION) {
|
||||
return Ok(Response::new(StatusCode::UpgradeRequired));
|
||||
}
|
||||
|
||||
@@ -904,6 +906,8 @@ pub fn add_routes(app: &mut tide::Server<Arc<AppState>>, rpc: &Arc<Peer>) {
|
||||
None => return Err(anyhow!("expected sec-websocket-key"))?,
|
||||
};
|
||||
|
||||
let user_id = process_auth_header(&request).await?;
|
||||
|
||||
let mut response = Response::new(StatusCode::SwitchingProtocols);
|
||||
response.insert_header(UPGRADE, "websocket");
|
||||
response.insert_header(CONNECTION, "Upgrade");
|
||||
@@ -914,10 +918,17 @@ pub fn add_routes(app: &mut tide::Server<Arc<AppState>>, rpc: &Arc<Peer>) {
|
||||
let http_res: &mut tide::http::Response = response.as_mut();
|
||||
let upgrade_receiver = http_res.recv_upgrade().await;
|
||||
let addr = request.remote().unwrap_or("unknown").to_string();
|
||||
let user_id = user_id.ok_or_else(|| anyhow!("user_id is not present on request. ensure auth::VerifyToken middleware is present"))?;
|
||||
task::spawn(async move {
|
||||
if let Some(stream) = upgrade_receiver.await {
|
||||
server.handle_connection(Connection::new(WebSocketStream::from_raw_socket(stream, Role::Server, None).await), addr, user_id).await;
|
||||
server
|
||||
.handle_connection(
|
||||
Connection::new(
|
||||
WebSocketStream::from_raw_socket(stream, Role::Server, None).await,
|
||||
),
|
||||
addr,
|
||||
user_id,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user