From d9f878bc45f693a38559bc70faa6062187aa9059 Mon Sep 17 00:00:00 2001 From: Sebastian Martinez Date: Thu, 2 Feb 2023 16:37:13 -0300 Subject: [PATCH] httpd: Remove session serialization, improve tests Signed-off-by: Sebastian Martinez --- radicle-httpd/Cargo.toml | 2 +- radicle-httpd/src/api/auth.rs | 24 ++++------ radicle-httpd/src/api/v1/sessions.rs | 66 ++++++++++++++++------------ 3 files changed, 48 insertions(+), 44 deletions(-) diff --git a/radicle-httpd/Cargo.toml b/radicle-httpd/Cargo.toml index 8bc87c1d..a2b53608 100644 --- a/radicle-httpd/Cargo.toml +++ b/radicle-httpd/Cargo.toml @@ -26,7 +26,7 @@ lexopt = { version = "0.2.1" } serde = { version = "1", features = ["derive"] } serde_json = { version = "1", features = ["preserve_order"] } thiserror = { version = "1" } -time = { version = "0.3.17" } +time = { version = "0.3.17", features = ["parsing", "serde"] } tokio = { version = "1.21", default-features = false, features = ["macros", "rt-multi-thread"] } tower-http = { version = "0.3.4", default-features = false, features = ["trace", "cors", "set-header"] } tracing = { version = "0.1.37", default-features = false, features = ["std", "log"] } diff --git a/radicle-httpd/src/api/auth.rs b/radicle-httpd/src/api/auth.rs index 317ef983..14c52049 100644 --- a/radicle-httpd/src/api/auth.rs +++ b/radicle-httpd/src/api/auth.rs @@ -1,27 +1,21 @@ use radicle::crypto::PublicKey; -use serde::{Serialize, Serializer}; -use time::OffsetDateTime; +use serde::{Deserialize, Serialize}; +use time::{serde::timestamp, OffsetDateTime}; -#[derive(Clone, PartialEq, PartialOrd)] -pub struct DateTime(pub OffsetDateTime); - -impl Serialize for DateTime { - fn serialize(&self, serializer: S) -> Result { - serializer.serialize_str(&format!("{}", self.0)) - } -} - -#[derive(Clone, Serialize, PartialEq)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] #[serde(rename_all = "lowercase")] pub enum AuthState { Authorized, Unauthorized, } -#[derive(Clone)] +#[derive(Clone, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct Session { pub status: AuthState, pub public_key: PublicKey, - pub issued_at: DateTime, - pub expires_at: DateTime, + #[serde(with = "timestamp")] + pub issued_at: OffsetDateTime, + #[serde(with = "timestamp")] + pub expires_at: OffsetDateTime, } diff --git a/radicle-httpd/src/api/v1/sessions.rs b/radicle-httpd/src/api/v1/sessions.rs index 7b02a424..d0661024 100644 --- a/radicle-httpd/src/api/v1/sessions.rs +++ b/radicle-httpd/src/api/v1/sessions.rs @@ -4,12 +4,13 @@ use axum::extract::State; use axum::response::IntoResponse; use axum::routing::{post, put}; use axum::{Json, Router}; +use hyper::StatusCode; use radicle::crypto::{PublicKey, Signature}; use serde::{Deserialize, Serialize}; use serde_json::json; use time::{Duration, OffsetDateTime}; -use crate::api::auth::{AuthState, DateTime, Session}; +use crate::api::auth::{AuthState, Session}; use crate::api::axum_extra::Path; use crate::api::error::Error; use crate::api::Context; @@ -41,24 +42,26 @@ async fn session_create_handler(State(ctx): State) -> impl IntoResponse .take(32) .collect::(); let signer = ctx.profile.signer().map_err(Error::from)?; - let expiration_time = OffsetDateTime::now_utc() - .checked_add(UNAUTHORIZED_SESSIONS_EXPIRATION) - .unwrap(); let session = Session { status: AuthState::Unauthorized, public_key: *signer.public_key(), - issued_at: DateTime(OffsetDateTime::now_utc()), - expires_at: DateTime(expiration_time), + issued_at: OffsetDateTime::now_utc(), + expires_at: OffsetDateTime::now_utc() + .checked_add(UNAUTHORIZED_SESSIONS_EXPIRATION) + .unwrap(), }; let mut sessions = ctx.sessions.write().await; sessions.insert(session_id.clone(), session.clone()); - Ok::<_, Error>(Json(json!({ - "sessionId": session_id, - "publicKey": session.public_key, - "issuedAt": session.issued_at, - "expiresAt": session.expires_at - }))) + Ok::<_, Error>(( + StatusCode::CREATED, + Json(json!({ + "sessionId": session_id, + "publicKey": session.public_key, + "issuedAt": session.issued_at, + "expiresAt": session.expires_at + })), + )) } /// Get a session. @@ -73,8 +76,8 @@ async fn session_handler( Ok::<_, Error>(Json(json!({ "status": session.status, "publicKey": session.public_key, - "issuedAt": session.issued_at, - "expiresAt": session.expires_at + "issuedAt": session.issued_at.unix_timestamp(), + "expiresAt": session.expires_at.unix_timestamp() }))) } @@ -86,12 +89,12 @@ async fn session_signin_handler( Json(request): Json, ) -> impl IntoResponse { let mut sessions = ctx.sessions.write().await; - let session = sessions.get(&session_id).ok_or(Error::NotFound)?; + let session = sessions.get_mut(&session_id).ok_or(Error::NotFound)?; if session.status == AuthState::Unauthorized { if session.public_key != request.pk { return Err(Error::Auth("Invalid public key")); } - if session.expires_at <= DateTime(OffsetDateTime::now_utc()) { + if session.expires_at <= OffsetDateTime::now_utc() { return Err(Error::Auth("Session expired")); } let payload = format!("{}:{}", session_id, request.pk); @@ -99,16 +102,10 @@ async fn session_signin_handler( .pk .verify(payload.as_bytes(), &request.sig) .map_err(Error::from)?; - let expiration_time = OffsetDateTime::now_utc() + session.status = AuthState::Authorized; + session.expires_at = OffsetDateTime::now_utc() .checked_add(AUTHORIZED_SESSIONS_EXPIRATION) .unwrap(); - let session = Session { - status: AuthState::Authorized, - public_key: request.pk, - issued_at: session.issued_at.to_owned(), - expires_at: DateTime(expiration_time), - }; - sessions.insert(session_id.clone(), session); return Ok::<_, Error>(Json(json!({ "success": true }))); } @@ -122,7 +119,10 @@ mod routes { use axum::http::StatusCode; use radicle_cli::commands::rad_web::{self, SessionInfo}; - use crate::api::test::{self, get, post, put}; + use crate::api::{ + auth::{AuthState, Session}, + test::{self, get, post, put}, + }; #[tokio::test] async fn test_session() { @@ -130,12 +130,19 @@ mod routes { let ctx = test::seed(tmp.path()); let app = super::router(ctx.to_owned()); - // Create session + // Create session. let response = post(&app, "/sessions", None).await; - assert_eq!(response.status(), StatusCode::OK); + assert_eq!(response.status(), StatusCode::CREATED); let json = response.json().await; let session_info: SessionInfo = serde_json::from_value(json).unwrap(); + // Check that an unauthorized session has been created. + let response = get(&app, format!("/sessions/{}", session_info.session_id)).await; + assert_eq!(response.status(), StatusCode::OK); + let json = response.json().await; + let body: Session = serde_json::from_value(json).unwrap(); + assert_eq!(body.status, AuthState::Unauthorized); + // Create request body let signer = ctx.profile.signer().unwrap(); let signature = rad_web::sign(signer, &session_info).unwrap(); @@ -151,10 +158,13 @@ mod routes { Some(Body::from(body)), ) .await; - assert_eq!(response.status(), StatusCode::OK); + // Check that session has been authorized. let response = get(&app, format!("/sessions/{}", session_info.session_id)).await; assert_eq!(response.status(), StatusCode::OK); + let json = response.json().await; + let body: Session = serde_json::from_value(json).unwrap(); + assert_eq!(body.status, AuthState::Authorized); } }