mirror of
https://github.com/LukeMathWalker/zero-to-production.git
synced 2024-12-18 14:06:37 +00:00
Extract basic credentials.
This commit is contained in:
parent
03edd1e3b6
commit
1d0d1cc382
4 changed files with 82 additions and 26 deletions
37
Cargo.lock
generated
37
Cargo.lock
generated
|
@ -701,9 +701,9 @@ checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures"
|
name = "futures"
|
||||||
version = "0.3.15"
|
version = "0.3.16"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0e7e43a803dae2fa37c1f6a8fe121e1f7bf9548b4dfc0522a42f34145dadfc27"
|
checksum = "1adc00f486adfc9ce99f77d717836f0c5aa84965eb0b4f051f4e83f7cab53f8b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
@ -716,9 +716,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-channel"
|
name = "futures-channel"
|
||||||
version = "0.3.15"
|
version = "0.3.16"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e682a68b29a882df0545c143dc3646daefe80ba479bcdede94d5a703de2871e2"
|
checksum = "74ed2411805f6e4e3d9bc904c95d5d423b89b3b25dc0250aa74729de20629ff9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
|
@ -726,15 +726,15 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-core"
|
name = "futures-core"
|
||||||
version = "0.3.15"
|
version = "0.3.16"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0402f765d8a89a26043b889b26ce3c4679d268fa6bb22cd7c6aad98340e179d1"
|
checksum = "af51b1b4a7fdff033703db39de8802c673eb91855f2e0d47dcf3bf2c0ef01f99"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-executor"
|
name = "futures-executor"
|
||||||
version = "0.3.15"
|
version = "0.3.16"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "badaa6a909fac9e7236d0620a2f57f7664640c56575b71a7552fbd68deafab79"
|
checksum = "4d0d535a57b87e1ae31437b892713aee90cd2d7b0ee48727cd11fc72ef54761c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-task",
|
"futures-task",
|
||||||
|
@ -743,9 +743,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-io"
|
name = "futures-io"
|
||||||
version = "0.3.15"
|
version = "0.3.16"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "acc499defb3b348f8d8f3f66415835a9131856ff7714bf10dadfc4ec4bdb29a1"
|
checksum = "0b0e06c393068f3a6ef246c75cdca793d6a46347e75286933e5e75fd2fd11582"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-lite"
|
name = "futures-lite"
|
||||||
|
@ -764,9 +764,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-macro"
|
name = "futures-macro"
|
||||||
version = "0.3.15"
|
version = "0.3.16"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a4c40298486cdf52cc00cd6d6987892ba502c7656a16a4192a9992b1ccedd121"
|
checksum = "c54913bae956fb8df7f4dc6fc90362aa72e69148e3f39041fbe8742d21e0ac57"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
"proc-macro-hack",
|
"proc-macro-hack",
|
||||||
|
@ -777,15 +777,15 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-sink"
|
name = "futures-sink"
|
||||||
version = "0.3.15"
|
version = "0.3.16"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a57bead0ceff0d6dde8f465ecd96c9338121bb7717d3e7b108059531870c4282"
|
checksum = "c0f30aaa67363d119812743aa5f33c201a7a66329f97d1a887022971feea4b53"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-task"
|
name = "futures-task"
|
||||||
version = "0.3.15"
|
version = "0.3.16"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8a16bef9fc1a4dddb5bee51c989e3fbba26569cbb0e31f5b303c184e3dd33dae"
|
checksum = "bbe54a98670017f3be909561f6ad13e810d9a51f3f061b902062ca3da80799f2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-timer"
|
name = "futures-timer"
|
||||||
|
@ -795,9 +795,9 @@ checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-util"
|
name = "futures-util"
|
||||||
version = "0.3.15"
|
version = "0.3.16"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "feb5c238d27e2bf94ffdfd27b2c29e3df4a68c4193bb6427384259e2bf191967"
|
checksum = "67eb846bfd58e44a8481a00049e82c43e0ccb5d61f8dc071057cb19249dd4d78"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
|
@ -2676,6 +2676,7 @@ dependencies = [
|
||||||
"actix-rt",
|
"actix-rt",
|
||||||
"actix-web",
|
"actix-web",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"base64",
|
||||||
"chrono",
|
"chrono",
|
||||||
"claim",
|
"claim",
|
||||||
"config",
|
"config",
|
||||||
|
|
|
@ -35,6 +35,7 @@ rand = { version = "0.8", features=["std_rng"] }
|
||||||
sha2 = { version = "0.9" }
|
sha2 = { version = "0.9" }
|
||||||
tracing-actix-web = "0.4.0-beta.8"
|
tracing-actix-web = "0.4.0-beta.8"
|
||||||
anyhow = "1.0.40"
|
anyhow = "1.0.40"
|
||||||
|
base64 = "0.13.0"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
once_cell = "1.7.2"
|
once_cell = "1.7.2"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::domain::SubscriberEmail;
|
use crate::domain::SubscriberEmail;
|
||||||
use crate::email_client::EmailClient;
|
use crate::email_client::EmailClient;
|
||||||
use crate::routes::error_chain_fmt;
|
use crate::routes::error_chain_fmt;
|
||||||
use actix_web::http::StatusCode;
|
use actix_web::http::{HeaderMap, HeaderValue, StatusCode};
|
||||||
use actix_web::{web, HttpResponse, ResponseError};
|
use actix_web::{web, HttpResponse, ResponseError};
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
|
@ -20,6 +20,8 @@ pub struct Content {
|
||||||
|
|
||||||
#[derive(thiserror::Error)]
|
#[derive(thiserror::Error)]
|
||||||
pub enum PublishError {
|
pub enum PublishError {
|
||||||
|
#[error("Authentication failed.")]
|
||||||
|
AuthError(#[source] anyhow::Error),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
UnexpectedError(#[from] anyhow::Error),
|
UnexpectedError(#[from] anyhow::Error),
|
||||||
}
|
}
|
||||||
|
@ -31,18 +33,63 @@ impl std::fmt::Debug for PublishError {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ResponseError for PublishError {
|
impl ResponseError for PublishError {
|
||||||
fn status_code(&self) -> StatusCode {
|
fn error_response(&self) -> HttpResponse {
|
||||||
match self {
|
match self {
|
||||||
PublishError::UnexpectedError(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
PublishError::UnexpectedError(_) => {
|
||||||
|
HttpResponse::new(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
}
|
||||||
|
PublishError::AuthError(_) => {
|
||||||
|
let mut response = HttpResponse::new(StatusCode::UNAUTHORIZED);
|
||||||
|
let header_value = HeaderValue::from_str(r#"Basic realm="publish""#).unwrap();
|
||||||
|
response
|
||||||
|
.headers_mut()
|
||||||
|
.insert(actix_web::http::header::WWW_AUTHENTICATE, header_value);
|
||||||
|
response
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Credentials {
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn basic_authentication(headers: &HeaderMap) -> Result<Credentials, anyhow::Error> {
|
||||||
|
// The header value, if present, must be a valid UTF8 string
|
||||||
|
let header_value = headers
|
||||||
|
.get("Authorization")
|
||||||
|
.context("The 'Authorization' header was missing")?
|
||||||
|
.to_str()
|
||||||
|
.context("The 'Authorization' header was not a valid UTF8 string.")?;
|
||||||
|
let base64encoded_credentials = header_value
|
||||||
|
.strip_prefix("Basic ")
|
||||||
|
.context("The authorization scheme was not 'Basic'.")?;
|
||||||
|
let decoded_credentials = base64::decode_config(base64encoded_credentials, base64::STANDARD)
|
||||||
|
.context("Failed to base64-decode 'Basic' credentials.")?;
|
||||||
|
let decoded_credentials = String::from_utf8(decoded_credentials)
|
||||||
|
.context("The decoded credential string is valid UTF8.")?;
|
||||||
|
|
||||||
|
let mut credentials = decoded_credentials.splitn(2, ':');
|
||||||
|
let username = credentials
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("A username must be provided in 'Basic' auth."))?
|
||||||
|
.to_string();
|
||||||
|
let password = credentials
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("A password must be provided in 'Basic' auth."))?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
Ok(Credentials { username, password })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn publish_newsletter(
|
pub async fn publish_newsletter(
|
||||||
body: web::Json<BodyData>,
|
body: web::Json<BodyData>,
|
||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
email_client: web::Data<EmailClient>,
|
email_client: web::Data<EmailClient>,
|
||||||
|
request: web::HttpRequest,
|
||||||
) -> Result<HttpResponse, PublishError> {
|
) -> Result<HttpResponse, PublishError> {
|
||||||
|
let credentials = basic_authentication(request.headers()).map_err(PublishError::AuthError)?;
|
||||||
let subscribers = get_confirmed_subscribers(&pool).await?;
|
let subscribers = get_confirmed_subscribers(&pool).await?;
|
||||||
for subscriber in subscribers {
|
for subscriber in subscribers {
|
||||||
match subscriber {
|
match subscriber {
|
||||||
|
|
|
@ -134,14 +134,21 @@ async fn requests_missing_authorization_are_rejected() {
|
||||||
|
|
||||||
let response = reqwest::Client::new()
|
let response = reqwest::Client::new()
|
||||||
.post(&format!("{}/newsletters", &app.address))
|
.post(&format!("{}/newsletters", &app.address))
|
||||||
// The body should not matter - authentication must be performed
|
.json(&serde_json::json!({
|
||||||
// BEFORE any further processing takes place.
|
"title": "Newsletter title",
|
||||||
.json(&serde_json::json!({}))
|
"content": {
|
||||||
|
"text": "Newsletter body as plain text",
|
||||||
|
"html": "<p>Newsletter body as HTML</p>",
|
||||||
|
}
|
||||||
|
}))
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.expect("Failed to execute request.");
|
.expect("Failed to execute request.");
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assert_eq!(401, response.status().as_u16());
|
assert_eq!(401, response.status().as_u16());
|
||||||
assert_eq!(r#"Basic realm="publish""#, response.headers()["WWW-Authenticate"]);
|
assert_eq!(
|
||||||
|
r#"Basic realm="publish""#,
|
||||||
|
response.headers()["WWW-Authenticate"]
|
||||||
|
);
|
||||||
}
|
}
|
Loading…
Reference in a new issue