Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Added RequestContextStore, RequestContext and SignedPrivateCookieJar #633

Draft
wants to merge 43 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
e59c252
Added RequestContextStore, RequestContext and SignedPrivateCookieJar
yinho999 Jul 1, 2024
73b0738
Added Cookie Map as inner driver of request context.
yinho999 Jul 7, 2024
c683b1c
Minor refactor
yinho999 Jul 7, 2024
cdd62d2
Merge remote-tracking branch 'origin/master' into requestcontext
yinho999 Jul 15, 2024
77d0f18
Added drive actions and init request context layer
yinho999 Jul 17, 2024
39e5ab7
added driver testing
yinho999 Jul 18, 2024
f0b9207
Request id will no longer initialized in tracing layer but request id…
yinho999 Jul 19, 2024
815b562
Request id will no longer initialized in tracing layer but request id…
yinho999 Jul 19, 2024
3b07485
Added request context middleware, refactored request context and tes…
yinho999 Jul 21, 2024
22122b1
Merge remote-tracking branch 'origin/master' into requestcontext
yinho999 Aug 10, 2024
47261bc
Fixed test
yinho999 Aug 11, 2024
b4ead0c
Merge remote-tracking branch 'origin/master' into requestcontext
yinho999 Aug 11, 2024
b5d10b1
refactor: replace `RequestId` with `LocoRequestId` across request con…
yinho999 Aug 11, 2024
9b05a67
refactor: replace `RequestId` with `LocoRequestId` across request con…
yinho999 Aug 11, 2024
c326037
feat: add request context management and tests
yinho999 Aug 11, 2024
55d7ac8
Cargo clippy
yinho999 Aug 11, 2024
e9e0355
example cargo clippy
yinho999 Aug 11, 2024
aa08161
refactor: simplify `RequestContext` usage by implementing `Deref` and…
yinho999 Aug 13, 2024
8ef8e39
feat: enhance `RequestContext` with session management methods
yinho999 Aug 14, 2024
96dfdad
Merge remote-tracking branch 'origin/master' into requestcontext
yinho999 Aug 16, 2024
41b3253
feat: enhance session management with custom session store and config…
yinho999 Aug 17, 2024
16368e2
Merge branch 'master' into requestcontext
yinho999 Aug 20, 2024
b8684a1
Merge remote-tracking branch 'origin/master' into requestcontext
yinho999 Sep 6, 2024
c7778bc
impl: enhance request context layer with tracing and session support
yinho999 Sep 7, 2024
adbf72e
refactor: optimize imports and improve session management in app_rout…
yinho999 Sep 9, 2024
95312b4
Merge branch 'master' into requestcontext
yinho999 Sep 25, 2024
715a765
refactor: update import paths in mysession controller and tests
yinho999 Sep 25, 2024
f71d820
cargo fmt
yinho999 Sep 25, 2024
8f53ee0
feat: enhance session management with configurable cookie attributes
yinho999 Sep 30, 2024
3799d35
Merge branch 'refs/heads/master' into requestcontext
yinho999 Oct 12, 2024
b1470a0
refactor: update cookie handling to use session config and remove har…
yinho999 Oct 13, 2024
c31b842
Refactored for new middleware design and fixed test in demo app and f…
yinho999 Oct 14, 2024
3fefbec
Merge branch 'refs/heads/master' into requestcontext
yinho999 Oct 14, 2024
9aa4cbc
Merge branch 'master' into requestcontext
yinho999 Oct 15, 2024
0104015
Fixed option error
yinho999 Oct 23, 2024
fad6290
Merge branch 'master' into requestcontext
yinho999 Oct 23, 2024
9709afa
Merge remote-tracking branch 'origin/master' into requestcontext
yinho999 Nov 1, 2024
538a4e7
Merge remote-tracking branch 'origin/requestcontext' into requestcontext
yinho999 Nov 1, 2024
edd9fca
Reorder imports in scheduler tests module for better organization
yinho999 Nov 1, 2024
8575966
Merge branch 'master' into requestcontext
yinho999 Dec 5, 2024
fad4bfb
Added cookie size validator
yinho999 Dec 5, 2024
1e4ff90
Added request context testing and also fixed existed tests
yinho999 Dec 8, 2024
660cdfc
Merge branch 'master' into requestcontext
yinho999 Dec 11, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 26 additions & 23 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,10 @@ colored = "2"


sea-orm = { version = "1.0.0-rc.4", features = [
"sqlx-postgres", # `DATABASE_DRIVER` feature
"sqlx-sqlite",
"runtime-tokio-rustls",
"macros",
"sqlx-postgres", # `DATABASE_DRIVER` feature
"sqlx-sqlite",
"runtime-tokio-rustls",
"macros",
], optional = true }

tokio = { version = "1.33.0", default-features = false }
Expand All @@ -67,17 +67,17 @@ async-trait = { workspace = true }
bb8 = "0.8.1"

axum = { workspace = true }
axum-extra = { version = "0.9", features = ["cookie"] }
axum-extra = { version = "0.9", features = ["cookie", "cookie-private", "cookie-signed"] }
regex = "1"
lazy_static = "1.4.0"
fs-err = "2.11.0"
# mailer
tera = "1.19.1"
lettre = { version = "0.11.4", default-features = false, features = [
"builder",
"hostname",
"smtp-transport",
"tokio1-rustls-tls",
"builder",
"hostname",
"smtp-transport",
"tokio1-rustls-tls",
] }
include_dir = "0.7.3"
thiserror = "1"
Expand Down Expand Up @@ -124,31 +124,34 @@ object_store = { version = "0.9.0", default-features = false }
# cache
moka = { version = "0.12.7", features = ["sync"], optional = true }

# sessions
tower-sessions = "0.12"

[workspace.dependencies]
async-trait = { version = "0.1.74" }
axum = { version = "0.7.1", features = ["macros"] }
tower = "0.4"
tower-http = { version = "0.5.0", features = [
"trace",
"catch-panic",
"timeout",
"add-extension",
"cors",
"fs",
"set-header",
"compression-full",
"trace",
"catch-panic",
"timeout",
"add-extension",
"cors",
"fs",
"set-header",
"compression-full",
] }

[dependencies.sea-orm-migration]
optional = true
version = "1.0.0-rc.4"
features = [
# Enable at least one `ASYNC_RUNTIME` and `DATABASE_DRIVER` feature if you want to run migration via CLI.
# View the list of supported features at https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime.
# e.g.
"runtime-tokio-rustls", # `ASYNC_RUNTIME` feature
"sqlx-postgres", # `DATABASE_DRIVER` feature
"sqlx-sqlite",
# Enable at least one `ASYNC_RUNTIME` and `DATABASE_DRIVER` feature if you want to run migration via CLI.
# View the list of supported features at https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime.
# e.g.
"runtime-tokio-rustls", # `ASYNC_RUNTIME` feature
"sqlx-postgres", # `DATABASE_DRIVER` feature
"sqlx-sqlite",
]

[package.metadata.docs.rs]
Expand Down
2 changes: 2 additions & 0 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ pub struct AppContext {
pub storage: Arc<Storage>,
// Cache instance for the application
pub cache: Arc<cache::Cache>,
// An optional request context for the application
pub request_context: Arc<crate::request_context::RequestContextStore>,
}

/// A trait that defines hooks for customizing and extending the behavior of a
Expand Down
28 changes: 28 additions & 0 deletions src/boot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
//! This module contains functions and structures for bootstrapping and running
//! your application.
use axum::Router;
use axum_extra::extract::cookie::Key;
#[cfg(feature = "with-db")]
use sea_orm_migration::MigratorTrait;
use tracing::{info, trace, warn};
Expand Down Expand Up @@ -205,6 +206,7 @@ pub async fn create_context<H: Hooks>(environment: &Environment) -> Result<AppCo
queue: connect_redis(&config).await,
storage: Storage::single(storage::drivers::null::new()).into(),
cache: cache::Cache::new(cache::drivers::null::new()).into(),
request_context: create_request_context_store(&config.request_context)?.into(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what happens to request context when there is no request involved? for example tasks, workers ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, that is a good point

config,
mailer,
};
Expand Down Expand Up @@ -340,6 +342,32 @@ fn create_mailer(config: &config::Mailer) -> Result<Option<EmailSender>> {
}
Ok(None)
}
/// Creates a [`RequestContextStore`] based on the provided configuration settings ([`config::RequestContext`]).
fn create_request_context_store(
config: &config::RequestContext,
) -> Result<crate::request_context::RequestContextStore> {
match config {
config::RequestContext::Cookie {
private_key,
signed_key,
} => {
let (private_key, signed_key) = (
Key::try_from(&private_key[..]).map_err(|e| {
tracing::error!(error = ?e, "could not convert private key from configuration");
Error::Message("could not convert private key from configuration".to_string())
})?,
Key::try_from(&signed_key[..]).map_err(|e| {
tracing::error!(error = ?e, "could not convert signed key from configuration");
Error::Message("could not convert signed key from configuration".to_string())
})?,
);
Ok(crate::request_context::RequestContextStore::new(
private_key,
signed_key,
))
}
}
}

#[allow(clippy::missing_panics_doc)]
/// Establishes a connection to a Redis server based on the provided
Expand Down
23 changes: 23 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ pub struct Config {
pub server: Server,
#[cfg(feature = "with-db")]
pub database: Database,
pub request_context: RequestContext,
pub queue: Option<Redis>,
pub auth: Option<Auth>,
#[serde(default)]
Expand Down Expand Up @@ -177,6 +178,28 @@ pub struct Database {
pub dangerously_recreate: bool,
}

/// Request context configuration
///
/// Example (development):
/// ```yaml
/// # config/development.yaml
/// request_context:
/// type: Cookie
/// value:
/// private_key: <your private key>
/// signed_key: <your signed key>
/// ```
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(tag = "type", content = "value")]
pub enum RequestContext {
/// Cookie session configuration
Cookie {
/// Private key for Private Cookie Jar in Cookie Sessions, must be more than 64 bytes.
private_key: Vec<u8>,
/// Signed key for Signed Cookie Jar in Cookie Sessions, must be more than 64 bytes.
signed_key: Vec<u8>,
},
}
/// Redis Configuration
///
/// Example (development):
Expand Down
3 changes: 3 additions & 0 deletions src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ pub enum Error {
#[error(transparent)]
Storage(#[from] crate::storage::StorageError),

#[error(transparent)]
RequestContext(#[from] crate::request_context::RequestContextError),

#[error(transparent)]
Any(#[from] Box<dyn std::error::Error + Send + Sync>),

Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ pub mod validation;
pub mod worker;
#[cfg(feature = "channels")]
pub use socketioxide;
pub mod request_context;
#[cfg(feature = "testing")]
pub mod tests_cfg;
pub use validator;
Expand Down
158 changes: 158 additions & 0 deletions src/request_context/driver/cookie.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
use crate::request_context::driver::PRIVATE_COOKIE_NAME;
use axum::http::HeaderMap;
use axum_extra::extract::cookie::Cookie;
use axum_extra::extract::cookie::{Key, PrivateCookieJar, SignedCookieJar};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

#[derive(Debug, Clone)]
pub struct SignedPrivateCookieJar {
private_jar: PrivateCookieJar,
signed_jar: SignedCookieJar,
}

impl SignedPrivateCookieJar {
#[must_use]
pub fn new(private_key: &Key, signed_key: &Key) -> Self {
Self {
private_jar: PrivateCookieJar::new(private_key.clone()),
signed_jar: SignedCookieJar::new(signed_key.clone()),
}
}

#[must_use]
pub fn from_headers(

Check failure on line 24 in src/request_context/driver/cookie.rs

View workflow job for this annotation

GitHub Actions / Run Clippy

docs for function returning `Result` missing `# Errors` section

Check failure on line 24 in src/request_context/driver/cookie.rs

View workflow job for this annotation

GitHub Actions / Run Clippy

this function has an empty `#[must_use]` attribute, but returns a type already marked as `#[must_use]`
private_key: &Key,
signed_key: &Key,
headers: &HeaderMap,
) -> Result<Self, SignedPrivateCookieJarError> {
// Create a new instance of the SignedPrivateCookieJar
let signed_jar = SignedCookieJar::from_headers(headers, signed_key.clone());
// Create a new instance of the PrivateCookieJar
let private_jar = PrivateCookieJar::from_headers(headers, private_key.clone());
let private_map = private_jar.get(PRIVATE_COOKIE_NAME);
let signed_map = signed_jar.get(PRIVATE_COOKIE_NAME);
match (private_map, signed_map) {
(Some(_), None) => Err(SignedPrivateCookieJarError::FromHeaders(
"Private cookie is present but signed cookie is missing".to_string(),
)),
(None, Some(_)) => Err(SignedPrivateCookieJarError::FromHeaders(
"Signed cookie is present but private cookie is missing".to_string(),
)),
(None, None) => Ok(Self::new(private_key, signed_key)),
(Some(private_cookie), Some(signed_cookie)) => {
if private_cookie.value() == signed_cookie.value() {
Ok(Self {
private_jar,
signed_jar,
})
} else {
Err(SignedPrivateCookieJarError::FromHeaders(
"Private cookie and signed cookie do not match".to_string(),
))
}
}
}
}

#[must_use]
pub fn add(

Check failure on line 59 in src/request_context/driver/cookie.rs

View workflow job for this annotation

GitHub Actions / Run Clippy

docs for function returning `Result` missing `# Errors` section

Check failure on line 59 in src/request_context/driver/cookie.rs

View workflow job for this annotation

GitHub Actions / Run Clippy

this function has an empty `#[must_use]` attribute, but returns a type already marked as `#[must_use]`
&mut self,
name: &str,
value: impl Serialize + Send,
) -> Result<(), SignedPrivateCookieJarError> {
// Firstly, get the Hashmap from the private_jar
let mut map: HashMap<String, serde_json::Value> =
if let Some(cookie) = self.private_jar.get(PRIVATE_COOKIE_NAME) {
let cookie_value = cookie.value().to_owned();
serde_json::from_str(&cookie_value)?
} else {
HashMap::new()
};
// Insert the value into the Hashmap
map.insert(name.to_owned(), serde_json::to_value(value)?);
// Serialize the updated map back to a string
let updated_cookie_value = serde_json::to_string(&map)?;
// Create a new cookie with the updated value
let new_cookie = Cookie::new(PRIVATE_COOKIE_NAME, updated_cookie_value);
// Add the new cookie to the jar
self.private_jar = self.private_jar.clone().add(new_cookie.clone());
// Then, sign the encrypted data
if let Some(encrypted_cookie) = self.private_jar.get(PRIVATE_COOKIE_NAME) {
self.signed_jar = self.signed_jar.clone().add(encrypted_cookie.clone());
}

Ok(())
}

#[must_use]
pub fn get<T: for<'de> Deserialize<'de>>(

Check failure on line 89 in src/request_context/driver/cookie.rs

View workflow job for this annotation

GitHub Actions / Run Clippy

docs for function returning `Result` missing `# Errors` section

Check failure on line 89 in src/request_context/driver/cookie.rs

View workflow job for this annotation

GitHub Actions / Run Clippy

this function has an empty `#[must_use]` attribute, but returns a type already marked as `#[must_use]`
&self,
name: &str,
) -> Result<Option<T>, SignedPrivateCookieJarError> {
// Firstly, get the Hashmap from the private_jar
if let Some(cookie) = self.private_jar.get(PRIVATE_COOKIE_NAME) {
let cookie_value = cookie.value().to_owned();
let map: HashMap<String, serde_json::Value> = serde_json::from_str(&cookie_value)?;
// Deserialize the value from the Hashmap
return Ok(map
.get(name)
.and_then(|value| serde_json::from_value(value.clone()).ok()));
}
Ok(None)
}

#[must_use]
pub fn remove(&mut self, name: &str) -> Result<(), SignedPrivateCookieJarError> {

Check failure on line 106 in src/request_context/driver/cookie.rs

View workflow job for this annotation

GitHub Actions / Run Clippy

docs for function which may panic missing `# Panics` section

Check failure on line 106 in src/request_context/driver/cookie.rs

View workflow job for this annotation

GitHub Actions / Run Clippy

docs for function returning `Result` missing `# Errors` section

Check failure on line 106 in src/request_context/driver/cookie.rs

View workflow job for this annotation

GitHub Actions / Run Clippy

this function has an empty `#[must_use]` attribute, but returns a type already marked as `#[must_use]`
// Firstly, get the Hashmap from the private_jar
let mut map: HashMap<String, serde_json::Value> =
if let Some(cookie) = self.private_jar.get(PRIVATE_COOKIE_NAME) {
let cookie_value = cookie.value().to_owned();
serde_json::from_str(&cookie_value)?
} else {
HashMap::new()
};

// Remove the value from the Hashmap
map.remove(name);
// Serialize the updated map back to a string
let updated_cookie_value = serde_json::to_string(&map).unwrap();
// Create a new cookie with the updated value
let new_cookie = Cookie::new(PRIVATE_COOKIE_NAME, updated_cookie_value);
// Add the new cookie to the jar
self.private_jar = self.private_jar.clone().add(new_cookie.clone());
// Then, sign the encrypted data
if let Some(encrypted_cookie) = self.private_jar.get(PRIVATE_COOKIE_NAME) {
self.signed_jar = self.signed_jar.clone().add(encrypted_cookie.clone());
}
Ok(())
}
}

#[derive(thiserror::Error, Debug)]
pub enum SignedPrivateCookieJarError {
#[error("Unable to extract data from cookie")]
ExtractData(#[from] serde_json::Error),
#[error("From headers error")]
FromHeaders(String),
}

#[cfg(test)]
mod test {
use super::*;
use axum_extra::extract::cookie::Cookie;
use axum_extra::extract::cookie::Key;
use std::collections::HashMap;

#[test]
fn test_signed_private_cookie_jar() -> Result<(), SignedPrivateCookieJarError> {
let (private_key, signed_key) = (Key::generate(), Key::generate());
let mut jar = SignedPrivateCookieJar::new(&private_key, &signed_key);
let (name, value) = ("foo", "bar".to_string());
jar.add(name, value.clone())?;
assert_eq!(jar.get::<String>(name)?, Some(value.clone()));
jar.remove(name)?;
assert_eq!(jar.get::<String>(name)?, None);
Ok(())
}
}
13 changes: 13 additions & 0 deletions src/request_context/driver/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
pub mod cookie;

use crate::request_context::driver::cookie::SignedPrivateCookieJar;
use tower_sessions::Session;

pub const PRIVATE_COOKIE_NAME: &str = "__loco_app_session";
#[derive(Debug, Clone)]
pub enum Driver {
TowerSession(Session),
SignedPrivateCookieJar(Box<SignedPrivateCookieJar>),
}

impl Driver {}
Loading
Loading