mirror of
https://github.com/CCC-MF/bwhc-kafka-rest-proxy.git
synced 2025-04-19 19:16:51 +00:00
feat: require token as http basic auth
This commit is contained in:
parent
92c2286174
commit
2b77a6b064
@ -36,6 +36,12 @@ features = [ "cmake-build", "libz-static" ]
|
|||||||
version = "1.7"
|
version = "1.7"
|
||||||
features = [ "v4" ]
|
features = [ "v4" ]
|
||||||
|
|
||||||
|
[dependencies.base64]
|
||||||
|
version = "0.22"
|
||||||
|
|
||||||
|
[dependencies.bcrypt]
|
||||||
|
version = "0.15"
|
||||||
|
|
||||||
[dependencies.bwhc-dto]
|
[dependencies.bwhc-dto]
|
||||||
git = "https://github.com/ccc-mf/bwhc-dto-rs"
|
git = "https://github.com/ccc-mf/bwhc-dto-rs"
|
||||||
branch = "master"
|
branch = "master"
|
17
README.md
17
README.md
@ -17,6 +17,7 @@ Die Anwendung lässt sich mit Umgebungsvariablen konfigurieren.
|
|||||||
|
|
||||||
* `APP_KAFKA_SERVERS`: Zu verwendende Kafka-Bootstrap-Server als kommagetrennte Liste
|
* `APP_KAFKA_SERVERS`: Zu verwendende Kafka-Bootstrap-Server als kommagetrennte Liste
|
||||||
* `APP_KAFKA_TOPIC`: Zu verwendendes Topic zum Warten auf neue Anfragen. Standardwert: `etl-processor_input`
|
* `APP_KAFKA_TOPIC`: Zu verwendendes Topic zum Warten auf neue Anfragen. Standardwert: `etl-processor_input`
|
||||||
|
* `APP_SECURITY_TOKEN`: Verpflichtende Angabe es Tokens als *bcrypt*-Hash
|
||||||
|
|
||||||
## HTTP-Requests
|
## HTTP-Requests
|
||||||
|
|
||||||
@ -30,5 +31,19 @@ Die folgenden Endpunkte sind verfügbar:
|
|||||||
Zum Löschen von Patienteninformationen wird intern ein MTB-File mit Consent-Status `REJECTED` erzeugt und weiter
|
Zum Löschen von Patienteninformationen wird intern ein MTB-File mit Consent-Status `REJECTED` erzeugt und weiter
|
||||||
geleitet. Hier ist kein Request-Body erforderlich.
|
geleitet. Hier ist kein Request-Body erforderlich.
|
||||||
|
|
||||||
Bei Erfolg enthält die Antwort enthält im HTTP-Header `X-Request-Id` die Anfrage-ID, die auch im ETL-Prozessor verwendet
|
Bei Erfolg enthält die Antwort enthält im HTTP-Header `x-request-id` die Anfrage-ID, die auch im ETL-Prozessor verwendet
|
||||||
wird.
|
wird.
|
||||||
|
|
||||||
|
### Authentifizierung
|
||||||
|
|
||||||
|
Requests müssen einen HTTP-Header `authorization` für HTTP-Basic enthalten. Hier ist es erforderlich, dass der
|
||||||
|
Benutzername `token` gewählt wird.
|
||||||
|
|
||||||
|
Es ist hierzu erforderlich, die erforderliche Umgebungsvariable `APP_SECURITY_TOKEN` zu setzen. Dies kann z.B. mit
|
||||||
|
*htpasswd* erzeugt werden:
|
||||||
|
|
||||||
|
```
|
||||||
|
htpasswd -Bn token
|
||||||
|
```
|
||||||
|
|
||||||
|
Der hintere Teil (hinter `token:`) entspricht dem *bcrypt*-Hash des Tokens.
|
67
src/auth.rs
Normal file
67
src/auth.rs
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
use base64::Engine;
|
||||||
|
use base64::prelude::BASE64_STANDARD;
|
||||||
|
|
||||||
|
pub fn check_basic_auth(auth_header: &str, expected_token: &str) -> bool {
|
||||||
|
let split = auth_header.split(' ').collect::<Vec<_>>();
|
||||||
|
if split.len() == 2 && split.first().map(|first| first.to_lowercase()) == Some("basic".into()) {
|
||||||
|
if let Ok(auth) = BASE64_STANDARD.decode(split.last().unwrap_or(&"")) {
|
||||||
|
if let Ok(auth) = String::from_utf8(auth) {
|
||||||
|
let split = auth.split(':').collect::<Vec<_>>();
|
||||||
|
if split.len() == 2 && split.first() == Some(&"token") {
|
||||||
|
match split.last() {
|
||||||
|
None => {}
|
||||||
|
Some(&token) => {
|
||||||
|
if let Ok(true) = bcrypt::verify(token, expected_token) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::auth::check_basic_auth;
|
||||||
|
|
||||||
|
// plain text value 'very-secret'
|
||||||
|
const EXPECTED_TOKEN: &str = "$2y$05$LIIFF4Rbi3iRVA4UIqxzPeTJ0NOn/cV2hDnSKFftAMzbEZRa42xSG";
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_reject_non_basic_header_content() {
|
||||||
|
assert_eq!(check_basic_auth("token 123456789", EXPECTED_TOKEN), false)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_reject_invalid_basic_auth() {
|
||||||
|
assert_eq!(check_basic_auth("Basic 123456789", EXPECTED_TOKEN), false)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_reject_basic_auth_without_token_username() {
|
||||||
|
assert_eq!(
|
||||||
|
check_basic_auth("Basic dXNlcjoxMjM0NTY3ODk=", EXPECTED_TOKEN),
|
||||||
|
false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_reject_basic_auth_without_wrong_token() {
|
||||||
|
assert_eq!(
|
||||||
|
check_basic_auth("Basic dG9rZW46MTIzNDU2Nzg5", EXPECTED_TOKEN),
|
||||||
|
false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_accept_basic_auth_without_correct_token() {
|
||||||
|
assert!(check_basic_auth(
|
||||||
|
"Basic dG9rZW46dmVyeS1zZWNyZXQ=",
|
||||||
|
EXPECTED_TOKEN
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
42
src/main.rs
42
src/main.rs
@ -4,7 +4,9 @@ use std::time::Duration;
|
|||||||
use axum::{Extension, Json, Router};
|
use axum::{Extension, Json, Router};
|
||||||
use axum::body::Body;
|
use axum::body::Body;
|
||||||
use axum::extract::Path;
|
use axum::extract::Path;
|
||||||
use axum::http::StatusCode;
|
use axum::http::{Request, StatusCode};
|
||||||
|
use axum::http::header::AUTHORIZATION;
|
||||||
|
use axum::middleware::{from_fn, Next};
|
||||||
use axum::response::Response;
|
use axum::response::Response;
|
||||||
use axum::routing::{delete, post};
|
use axum::routing::{delete, post};
|
||||||
use bwhc_dto::MtbFile;
|
use bwhc_dto::MtbFile;
|
||||||
@ -14,10 +16,7 @@ use rdkafka::producer::{FutureProducer, FutureRecord};
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[derive(Clone)]
|
mod auth;
|
||||||
struct KafkaConfig {
|
|
||||||
dst_topic: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
struct RecordKey {
|
struct RecordKey {
|
||||||
@ -29,8 +28,9 @@ struct RecordKey {
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
|
let _ = bcrypt_hashed_token();
|
||||||
|
|
||||||
let boostrap_servers = env::var("KAFKA_BOOTSTRAP_SERVERS").unwrap_or("kafka:9094".into());
|
let boostrap_servers = env::var("KAFKA_BOOTSTRAP_SERVERS").unwrap_or("kafka:9094".into());
|
||||||
let dst_topic = env::var("APP_KAFKA_TOPIC").unwrap_or("etl-processor_input".into());
|
|
||||||
|
|
||||||
let producer: FutureProducer = ClientConfig::new()
|
let producer: FutureProducer = ClientConfig::new()
|
||||||
.set("bootstrap.servers", boostrap_servers.as_str())
|
.set("bootstrap.servers", boostrap_servers.as_str())
|
||||||
@ -42,20 +42,31 @@ async fn main() {
|
|||||||
.route("/mtbfile", post(handle_post))
|
.route("/mtbfile", post(handle_post))
|
||||||
.route("/mtbfile/:patient_id", delete(handle_delete))
|
.route("/mtbfile/:patient_id", delete(handle_delete))
|
||||||
.layer(Extension(producer))
|
.layer(Extension(producer))
|
||||||
.layer(Extension(KafkaConfig { dst_topic }));
|
.layer(from_fn(check_basic_auth));
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:8000").await.unwrap();
|
let listener = tokio::net::TcpListener::bind("0.0.0.0:8000").await.unwrap();
|
||||||
axum::serve(listener, app).await.unwrap();
|
axum::serve(listener, app).await.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn check_basic_auth(request: Request<Body>, next: Next) -> Response {
|
||||||
|
if let Some(Ok(auth_header)) = request.headers().get(AUTHORIZATION).map(|x| x.to_str()) {
|
||||||
|
if auth::check_basic_auth(auth_header, &bcrypt_hashed_token()) {
|
||||||
|
return next.run(request).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Response::builder()
|
||||||
|
.status(StatusCode::UNAUTHORIZED)
|
||||||
|
.body(Body::empty())
|
||||||
|
.expect("response built")
|
||||||
|
}
|
||||||
|
|
||||||
async fn handle_delete(
|
async fn handle_delete(
|
||||||
Path(patient_id): Path<String>,
|
Path(patient_id): Path<String>,
|
||||||
Extension(producer): Extension<FutureProducer>,
|
Extension(producer): Extension<FutureProducer>,
|
||||||
Extension(kafka_config): Extension<KafkaConfig>,
|
|
||||||
) -> Response {
|
) -> Response {
|
||||||
let delete_mtb_file = MtbFile::new_with_consent_rejected(&patient_id);
|
let delete_mtb_file = MtbFile::new_with_consent_rejected(&patient_id);
|
||||||
|
|
||||||
match send_mtb_file(producer, &kafka_config.dst_topic, delete_mtb_file).await {
|
match send_mtb_file(producer, &dst_topic(), delete_mtb_file).await {
|
||||||
Ok(request_id) => success_response(&request_id),
|
Ok(request_id) => success_response(&request_id),
|
||||||
_ => error_response(),
|
_ => error_response(),
|
||||||
}
|
}
|
||||||
@ -63,10 +74,9 @@ async fn handle_delete(
|
|||||||
|
|
||||||
async fn handle_post(
|
async fn handle_post(
|
||||||
Extension(producer): Extension<FutureProducer>,
|
Extension(producer): Extension<FutureProducer>,
|
||||||
Extension(kafka_config): Extension<KafkaConfig>,
|
|
||||||
Json(mtb_file): Json<MtbFile>,
|
Json(mtb_file): Json<MtbFile>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
match send_mtb_file(producer, &kafka_config.dst_topic, mtb_file).await {
|
match send_mtb_file(producer, &dst_topic(), mtb_file).await {
|
||||||
Ok(request_id) => success_response(&request_id),
|
Ok(request_id) => success_response(&request_id),
|
||||||
_ => error_response(),
|
_ => error_response(),
|
||||||
}
|
}
|
||||||
@ -128,3 +138,13 @@ fn error_response() -> Response {
|
|||||||
.body(Body::empty())
|
.body(Body::empty())
|
||||||
.expect("response built")
|
.expect("response built")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn dst_topic() -> String {
|
||||||
|
env::var("APP_KAFKA_TOPIC").unwrap_or("etl-processor_input".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bcrypt_hashed_token() -> String {
|
||||||
|
env::var("APP_SECURITY_TOKEN").unwrap_or_else(|_| {
|
||||||
|
panic!("Missing configuration 'APP_SECURITY_TOKEN'. Provide bcrypt hashed token value.")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user