mirror of
https://github.com/GothenburgBitFactory/taskchampion-sync-server.git
synced 2026-04-06 01:30:42 +00:00
Support a client-id allowlist (#62)
This will support setting up publicly-accessible personal servers, without also allowing anyone to create a new client.
This commit is contained in:
committed by
GitHub
parent
5ad3b8e8bf
commit
50d028f45e
@ -1,4 +1,4 @@
|
||||
use crate::api::{client_id_header, server_error_to_actix, ServerState, SNAPSHOT_CONTENT_TYPE};
|
||||
use crate::api::{server_error_to_actix, ServerState, SNAPSHOT_CONTENT_TYPE};
|
||||
use actix_web::{error, post, web, HttpMessage, HttpRequest, HttpResponse, Result};
|
||||
use futures::StreamExt;
|
||||
use std::sync::Arc;
|
||||
@ -29,7 +29,7 @@ pub(crate) async fn service(
|
||||
return Err(error::ErrorBadRequest("Bad content-type"));
|
||||
}
|
||||
|
||||
let client_id = client_id_header(&req)?;
|
||||
let client_id = server_state.client_id_header(&req)?;
|
||||
|
||||
// read the body in its entirety
|
||||
let mut body = web::BytesMut::new();
|
||||
@ -75,7 +75,7 @@ mod test {
|
||||
txn.add_version(client_id, version_id, NIL_VERSION_ID, vec![])?;
|
||||
}
|
||||
|
||||
let server = WebServer::new(Default::default(), storage);
|
||||
let server = WebServer::new(Default::default(), None, storage);
|
||||
let app = App::new().configure(|sc| server.config(sc));
|
||||
let app = test::init_service(app).await;
|
||||
|
||||
@ -117,7 +117,7 @@ mod test {
|
||||
txn.new_client(client_id, NIL_VERSION_ID).unwrap();
|
||||
}
|
||||
|
||||
let server = WebServer::new(Default::default(), storage);
|
||||
let server = WebServer::new(Default::default(), None, storage);
|
||||
let app = App::new().configure(|sc| server.config(sc));
|
||||
let app = test::init_service(app).await;
|
||||
|
||||
@ -147,7 +147,7 @@ mod test {
|
||||
let client_id = Uuid::new_v4();
|
||||
let version_id = Uuid::new_v4();
|
||||
let storage = InMemoryStorage::new();
|
||||
let server = WebServer::new(Default::default(), storage);
|
||||
let server = WebServer::new(Default::default(), None, storage);
|
||||
let app = App::new().configure(|sc| server.config(sc));
|
||||
let app = test::init_service(app).await;
|
||||
|
||||
@ -167,7 +167,7 @@ mod test {
|
||||
let client_id = Uuid::new_v4();
|
||||
let version_id = Uuid::new_v4();
|
||||
let storage = InMemoryStorage::new();
|
||||
let server = WebServer::new(Default::default(), storage);
|
||||
let server = WebServer::new(Default::default(), None, storage);
|
||||
let app = App::new().configure(|sc| server.config(sc));
|
||||
let app = test::init_service(app).await;
|
||||
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
use crate::api::{
|
||||
client_id_header, failure_to_ise, server_error_to_actix, ServerState,
|
||||
HISTORY_SEGMENT_CONTENT_TYPE, PARENT_VERSION_ID_HEADER, SNAPSHOT_REQUEST_HEADER,
|
||||
VERSION_ID_HEADER,
|
||||
failure_to_ise, server_error_to_actix, ServerState, HISTORY_SEGMENT_CONTENT_TYPE,
|
||||
PARENT_VERSION_ID_HEADER, SNAPSHOT_REQUEST_HEADER, VERSION_ID_HEADER,
|
||||
};
|
||||
use actix_web::{error, post, web, HttpMessage, HttpRequest, HttpResponse, Result};
|
||||
use futures::StreamExt;
|
||||
@ -40,7 +39,7 @@ pub(crate) async fn service(
|
||||
return Err(error::ErrorBadRequest("Bad content-type"));
|
||||
}
|
||||
|
||||
let client_id = client_id_header(&req)?;
|
||||
let client_id = server_state.client_id_header(&req)?;
|
||||
|
||||
// read the body in its entirety
|
||||
let mut body = web::BytesMut::new();
|
||||
@ -116,7 +115,7 @@ mod test {
|
||||
txn.new_client(client_id, Uuid::nil()).unwrap();
|
||||
}
|
||||
|
||||
let server = WebServer::new(Default::default(), storage);
|
||||
let server = WebServer::new(Default::default(), None, storage);
|
||||
let app = App::new().configure(|sc| server.config(sc));
|
||||
let app = test::init_service(app).await;
|
||||
|
||||
@ -150,7 +149,7 @@ mod test {
|
||||
let client_id = Uuid::new_v4();
|
||||
let version_id = Uuid::new_v4();
|
||||
let parent_version_id = Uuid::new_v4();
|
||||
let server = WebServer::new(Default::default(), InMemoryStorage::new());
|
||||
let server = WebServer::new(Default::default(), None, InMemoryStorage::new());
|
||||
let app = App::new().configure(|sc| server.config(sc));
|
||||
let app = test::init_service(app).await;
|
||||
|
||||
@ -201,7 +200,7 @@ mod test {
|
||||
txn.new_client(client_id, version_id).unwrap();
|
||||
}
|
||||
|
||||
let server = WebServer::new(Default::default(), storage);
|
||||
let server = WebServer::new(Default::default(), None, storage);
|
||||
let app = App::new().configure(|sc| server.config(sc));
|
||||
let app = test::init_service(app).await;
|
||||
|
||||
@ -229,7 +228,7 @@ mod test {
|
||||
let client_id = Uuid::new_v4();
|
||||
let parent_version_id = Uuid::new_v4();
|
||||
let storage = InMemoryStorage::new();
|
||||
let server = WebServer::new(Default::default(), storage);
|
||||
let server = WebServer::new(Default::default(), None, storage);
|
||||
let app = App::new().configure(|sc| server.config(sc));
|
||||
let app = test::init_service(app).await;
|
||||
|
||||
@ -249,7 +248,7 @@ mod test {
|
||||
let client_id = Uuid::new_v4();
|
||||
let parent_version_id = Uuid::new_v4();
|
||||
let storage = InMemoryStorage::new();
|
||||
let server = WebServer::new(Default::default(), storage);
|
||||
let server = WebServer::new(Default::default(), None, storage);
|
||||
let app = App::new().configure(|sc| server.config(sc));
|
||||
let app = test::init_service(app).await;
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
use crate::api::{
|
||||
client_id_header, server_error_to_actix, ServerState, HISTORY_SEGMENT_CONTENT_TYPE,
|
||||
PARENT_VERSION_ID_HEADER, VERSION_ID_HEADER,
|
||||
server_error_to_actix, ServerState, HISTORY_SEGMENT_CONTENT_TYPE, PARENT_VERSION_ID_HEADER,
|
||||
VERSION_ID_HEADER,
|
||||
};
|
||||
use actix_web::{error, get, web, HttpRequest, HttpResponse, Result};
|
||||
use std::sync::Arc;
|
||||
@ -21,7 +21,7 @@ pub(crate) async fn service(
|
||||
path: web::Path<VersionId>,
|
||||
) -> Result<HttpResponse> {
|
||||
let parent_version_id = path.into_inner();
|
||||
let client_id = client_id_header(&req)?;
|
||||
let client_id = server_state.client_id_header(&req)?;
|
||||
|
||||
return match server_state
|
||||
.server
|
||||
@ -70,7 +70,7 @@ mod test {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let server = WebServer::new(Default::default(), storage);
|
||||
let server = WebServer::new(Default::default(), None, storage);
|
||||
let app = App::new().configure(|sc| server.config(sc));
|
||||
let app = test::init_service(app).await;
|
||||
|
||||
@ -104,7 +104,7 @@ mod test {
|
||||
let client_id = Uuid::new_v4();
|
||||
let parent_version_id = Uuid::new_v4();
|
||||
let storage = InMemoryStorage::new();
|
||||
let server = WebServer::new(Default::default(), storage);
|
||||
let server = WebServer::new(Default::default(), None, storage);
|
||||
let app = App::new().configure(|sc| server.config(sc));
|
||||
let app = test::init_service(app).await;
|
||||
|
||||
@ -132,7 +132,7 @@ mod test {
|
||||
txn.add_version(client_id, test_version_id, NIL_VERSION_ID, b"vers".to_vec())
|
||||
.unwrap();
|
||||
}
|
||||
let server = WebServer::new(Default::default(), storage);
|
||||
let server = WebServer::new(Default::default(), None, storage);
|
||||
let app = App::new().configure(|sc| server.config(sc));
|
||||
let app = test::init_service(app).await;
|
||||
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
use crate::api::{
|
||||
client_id_header, server_error_to_actix, ServerState, SNAPSHOT_CONTENT_TYPE, VERSION_ID_HEADER,
|
||||
};
|
||||
use crate::api::{server_error_to_actix, ServerState, SNAPSHOT_CONTENT_TYPE, VERSION_ID_HEADER};
|
||||
use actix_web::{error, get, web, HttpRequest, HttpResponse, Result};
|
||||
use std::sync::Arc;
|
||||
|
||||
@ -17,7 +15,7 @@ pub(crate) async fn service(
|
||||
req: HttpRequest,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<HttpResponse> {
|
||||
let client_id = client_id_header(&req)?;
|
||||
let client_id = server_state.client_id_header(&req)?;
|
||||
|
||||
if let Some((version_id, data)) = server_state
|
||||
.server
|
||||
@ -54,7 +52,7 @@ mod test {
|
||||
txn.new_client(client_id, Uuid::new_v4()).unwrap();
|
||||
}
|
||||
|
||||
let server = WebServer::new(Default::default(), storage);
|
||||
let server = WebServer::new(Default::default(), None, storage);
|
||||
let app = App::new().configure(|sc| server.config(sc));
|
||||
let app = test::init_service(app).await;
|
||||
|
||||
@ -90,7 +88,7 @@ mod test {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let server = WebServer::new(Default::default(), storage);
|
||||
let server = WebServer::new(Default::default(), None, storage);
|
||||
let app = App::new().configure(|sc| server.config(sc));
|
||||
let app = test::init_service(app).await;
|
||||
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use actix_web::{error, web, HttpRequest, Result, Scope};
|
||||
use taskchampion_sync_server_core::{ClientId, Server, ServerError};
|
||||
use uuid::Uuid;
|
||||
|
||||
mod add_snapshot;
|
||||
mod add_version;
|
||||
@ -28,6 +31,28 @@ pub(crate) const SNAPSHOT_REQUEST_HEADER: &str = "X-Snapshot-Request";
|
||||
/// The type containing a reference to the persistent state for the server
|
||||
pub(crate) struct ServerState {
|
||||
pub(crate) server: Server,
|
||||
pub(crate) client_id_allowlist: Option<HashSet<Uuid>>,
|
||||
}
|
||||
|
||||
impl ServerState {
|
||||
/// Get the client id
|
||||
fn client_id_header(&self, req: &HttpRequest) -> Result<ClientId> {
|
||||
fn badrequest() -> error::Error {
|
||||
error::ErrorBadRequest("bad x-client-id")
|
||||
}
|
||||
if let Some(client_id_hdr) = req.headers().get(CLIENT_ID_HEADER) {
|
||||
let client_id = client_id_hdr.to_str().map_err(|_| badrequest())?;
|
||||
let client_id = ClientId::parse_str(client_id).map_err(|_| badrequest())?;
|
||||
if let Some(allow_list) = &self.client_id_allowlist {
|
||||
if !allow_list.contains(&client_id) {
|
||||
return Err(error::ErrorForbidden("unknown x-client-id"));
|
||||
}
|
||||
}
|
||||
Ok(client_id)
|
||||
} else {
|
||||
Err(badrequest())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn api_scope() -> Scope {
|
||||
@ -51,16 +76,46 @@ fn server_error_to_actix(err: ServerError) -> actix_web::Error {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the client id
|
||||
fn client_id_header(req: &HttpRequest) -> Result<ClientId> {
|
||||
fn badrequest() -> error::Error {
|
||||
error::ErrorBadRequest("bad x-client-id")
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use taskchampion_sync_server_core::InMemoryStorage;
|
||||
|
||||
#[test]
|
||||
fn client_id_header_allow_all() {
|
||||
let client_id = Uuid::new_v4();
|
||||
let state = ServerState {
|
||||
server: Server::new(Default::default(), InMemoryStorage::new()),
|
||||
client_id_allowlist: None,
|
||||
};
|
||||
let req = actix_web::test::TestRequest::default()
|
||||
.insert_header((CLIENT_ID_HEADER, client_id.to_string()))
|
||||
.to_http_request();
|
||||
assert_eq!(state.client_id_header(&req).unwrap(), client_id);
|
||||
}
|
||||
if let Some(client_id_hdr) = req.headers().get(CLIENT_ID_HEADER) {
|
||||
let client_id = client_id_hdr.to_str().map_err(|_| badrequest())?;
|
||||
let client_id = ClientId::parse_str(client_id).map_err(|_| badrequest())?;
|
||||
Ok(client_id)
|
||||
} else {
|
||||
Err(badrequest())
|
||||
|
||||
#[test]
|
||||
fn client_id_header_allow_list() {
|
||||
let client_id_ok = Uuid::new_v4();
|
||||
let client_id_disallowed = Uuid::new_v4();
|
||||
let state = ServerState {
|
||||
server: Server::new(Default::default(), InMemoryStorage::new()),
|
||||
client_id_allowlist: Some([client_id_ok].into()),
|
||||
};
|
||||
let req = actix_web::test::TestRequest::default()
|
||||
.insert_header((CLIENT_ID_HEADER, client_id_ok.to_string()))
|
||||
.to_http_request();
|
||||
assert_eq!(state.client_id_header(&req).unwrap(), client_id_ok);
|
||||
let req = actix_web::test::TestRequest::default()
|
||||
.insert_header((CLIENT_ID_HEADER, client_id_disallowed.to_string()))
|
||||
.to_http_request();
|
||||
assert_eq!(
|
||||
state
|
||||
.client_id_header(&req)
|
||||
.unwrap_err()
|
||||
.as_response_error()
|
||||
.status_code(),
|
||||
403
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user