Include client key in a header, not the URL

Since this value is used both for identification and authentication, it
shouldn't be in the URL where it might be logged or otherwise
discovered.
This commit is contained in:
Dustin J. Mitchell 2020-12-28 23:08:42 +00:00
parent 92d629522b
commit 31378cb8d4
5 changed files with 68 additions and 41 deletions

View file

@ -1,8 +1,8 @@
use crate::api::{
failure_to_ise, ServerState, HISTORY_SEGMENT_CONTENT_TYPE, PARENT_VERSION_ID_HEADER,
VERSION_ID_HEADER,
client_key_header, failure_to_ise, ServerState, HISTORY_SEGMENT_CONTENT_TYPE,
PARENT_VERSION_ID_HEADER, VERSION_ID_HEADER,
};
use crate::server::{add_version, AddVersionResult, ClientKey, VersionId, NO_VERSION_ID};
use crate::server::{add_version, AddVersionResult, VersionId, NO_VERSION_ID};
use actix_web::{error, post, web, HttpMessage, HttpRequest, HttpResponse, Result};
use futures::StreamExt;
@ -19,11 +19,11 @@ const MAX_SIZE: usize = 100 * 1024 * 1024;
/// parent version ID in the `X-Parent-Version-Id` header.
///
/// Returns other 4xx or 5xx responses on other errors.
#[post("/client/{client_key}/add-version/{parent_version_id}")]
#[post("/client/add-version/{parent_version_id}")]
pub(crate) async fn service(
req: HttpRequest,
server_state: web::Data<ServerState>,
web::Path((client_key, parent_version_id)): web::Path<(ClientKey, VersionId)>,
web::Path((parent_version_id,)): web::Path<(VersionId,)>,
mut payload: web::Payload,
) -> Result<HttpResponse> {
// check content-type
@ -31,6 +31,8 @@ pub(crate) async fn service(
return Err(error::ErrorBadRequest("Bad content-type"));
}
let client_key = client_key_header(&req)?;
// read the body in its entirety
let mut body = web::BytesMut::new();
while let Some(chunk) = payload.next().await {
@ -97,13 +99,14 @@ mod test {
let server_state = ServerState::new(server_box);
let mut app = test::init_service(App::new().service(app_scope(server_state))).await;
let uri = format!("/client/{}/add-version/{}", client_key, parent_version_id);
let uri = format!("/client/add-version/{}", parent_version_id);
let req = test::TestRequest::post()
.uri(&uri)
.header(
"Content-Type",
"application/vnd.taskchampion.history-segment",
)
.header("X-Client-Key", client_key.to_string())
.set_payload(b"abcd".to_vec())
.to_request();
let resp = test::call_service(&mut app, req).await;
@ -133,13 +136,14 @@ mod test {
let server_state = ServerState::new(server_box);
let mut app = test::init_service(App::new().service(app_scope(server_state))).await;
let uri = format!("/client/{}/add-version/{}", client_key, parent_version_id);
let uri = format!("/client/add-version/{}", parent_version_id);
let req = test::TestRequest::post()
.uri(&uri)
.header(
"Content-Type",
"application/vnd.taskchampion.history-segment",
)
.header("X-Client-Key", client_key.to_string())
.set_payload(b"abcd".to_vec())
.to_request();
let resp = test::call_service(&mut app, req).await;
@ -159,10 +163,11 @@ mod test {
let server_state = ServerState::new(server_box);
let mut app = test::init_service(App::new().service(app_scope(server_state))).await;
let uri = format!("/client/{}/add-version/{}", client_key, parent_version_id);
let uri = format!("/client/add-version/{}", parent_version_id);
let req = test::TestRequest::post()
.uri(&uri)
.header("Content-Type", "not/correct")
.header("X-Client-Key", client_key.to_string())
.set_payload(b"abcd".to_vec())
.to_request();
let resp = test::call_service(&mut app, req).await;
@ -177,13 +182,14 @@ mod test {
let server_state = ServerState::new(server_box);
let mut app = test::init_service(App::new().service(app_scope(server_state))).await;
let uri = format!("/client/{}/add-version/{}", client_key, parent_version_id);
let uri = format!("/client/add-version/{}", parent_version_id);
let req = test::TestRequest::post()
.uri(&uri)
.header(
"Content-Type",
"application/vnd.taskchampion.history-segment",
)
.header("X-Client-Key", client_key.to_string())
.to_request();
let resp = test::call_service(&mut app, req).await;
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);

View file

@ -1,9 +1,9 @@
use crate::api::{
failure_to_ise, ServerState, HISTORY_SEGMENT_CONTENT_TYPE, PARENT_VERSION_ID_HEADER,
VERSION_ID_HEADER,
client_key_header, failure_to_ise, ServerState, HISTORY_SEGMENT_CONTENT_TYPE,
PARENT_VERSION_ID_HEADER, VERSION_ID_HEADER,
};
use crate::server::{get_child_version, ClientKey, VersionId};
use actix_web::{error, get, web, HttpResponse, Result};
use crate::server::{get_child_version, VersionId};
use actix_web::{error, get, web, HttpRequest, HttpResponse, Result};
/// Get a child version.
///
@ -13,13 +13,16 @@ use actix_web::{error, get, web, HttpResponse, Result};
///
/// If no such child exists, returns a 404 with no content.
/// Returns other 4xx or 5xx responses on other errors.
#[get("/client/{client_key}/get-child-version/{parent_version_id}")]
#[get("/client/get-child-version/{parent_version_id}")]
pub(crate) async fn service(
req: HttpRequest,
server_state: web::Data<ServerState>,
web::Path((client_key, parent_version_id)): web::Path<(ClientKey, VersionId)>,
web::Path((parent_version_id,)): web::Path<(VersionId,)>,
) -> Result<HttpResponse> {
let mut txn = server_state.txn().map_err(failure_to_ise)?;
let client_key = client_key_header(&req)?;
txn.get_client(client_key)
.map_err(failure_to_ise)?
.ok_or_else(|| error::ErrorNotFound("no such client"))?;
@ -65,11 +68,11 @@ mod test {
let server_state = ServerState::new(server_box);
let mut app = test::init_service(App::new().service(app_scope(server_state))).await;
let uri = format!(
"/client/{}/get-child-version/{}",
client_key, parent_version_id
);
let req = test::TestRequest::get().uri(&uri).to_request();
let uri = format!("/client/get-child-version/{}", parent_version_id);
let req = test::TestRequest::get()
.uri(&uri)
.header("X-Client-Key", client_key.to_string())
.to_request();
let mut resp = test::call_service(&mut app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
@ -98,11 +101,11 @@ mod test {
let server_state = ServerState::new(server_box);
let mut app = test::init_service(App::new().service(app_scope(server_state))).await;
let uri = format!(
"/client/{}/get-child-version/{}",
client_key, parent_version_id
);
let req = test::TestRequest::get().uri(&uri).to_request();
let uri = format!("/client/get-child-version/{}", parent_version_id);
let req = test::TestRequest::get()
.uri(&uri)
.header("X-Client-Key", client_key.to_string())
.to_request();
let resp = test::call_service(&mut app, req).await;
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
assert_eq!(resp.headers().get("X-Version-Id"), None);
@ -123,11 +126,11 @@ mod test {
let server_state = ServerState::new(server_box);
let mut app = test::init_service(App::new().service(app_scope(server_state))).await;
let uri = format!(
"/client/{}/get-child-version/{}",
client_key, parent_version_id
);
let req = test::TestRequest::get().uri(&uri).to_request();
let uri = format!("/client/get-child-version/{}", parent_version_id);
let req = test::TestRequest::get()
.uri(&uri)
.header("X-Client-Key", client_key.to_string())
.to_request();
let resp = test::call_service(&mut app, req).await;
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
assert_eq!(resp.headers().get("X-Version-Id"), None);

View file

@ -1,5 +1,6 @@
use crate::server::ClientKey;
use crate::storage::Storage;
use actix_web::{error, http::StatusCode, web, Scope};
use actix_web::{error, http::StatusCode, web, HttpRequest, Result, Scope};
use std::sync::Arc;
mod add_version;
@ -9,10 +10,13 @@ mod get_child_version;
pub(crate) const HISTORY_SEGMENT_CONTENT_TYPE: &str =
"application/vnd.taskchampion.history-segment";
/// The header names for version ID
/// The header name for version ID
pub(crate) const VERSION_ID_HEADER: &str = "X-Version-Id";
/// The header names for parent version ID
/// The header name for client key
pub(crate) const CLIENT_KEY_HEADER: &str = "X-Client-Key";
/// The header name for parent version ID
pub(crate) const PARENT_VERSION_ID_HEADER: &str = "X-Parent-Version-Id";
/// The type containing a reference to the Storage object in the Actix state.
@ -28,3 +32,17 @@ pub(crate) fn api_scope() -> Scope {
fn failure_to_ise(err: failure::Error) -> impl actix_web::ResponseError {
error::InternalError::new(err, StatusCode::INTERNAL_SERVER_ERROR)
}
/// Get the client key
fn client_key_header(req: &HttpRequest) -> Result<ClientKey> {
fn badrequest() -> error::Error {
error::ErrorBadRequest("bad x-client-id")
}
if let Some(client_key_hdr) = req.headers().get(CLIENT_KEY_HEADER) {
let client_key = client_key_hdr.to_str().map_err(|_| badrequest())?;
let client_key = ClientKey::parse_str(client_key).map_err(|_| badrequest())?;
Ok(client_key)
} else {
Err(badrequest())
}
}