Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
httpd: Remove session serialization, improve tests
Sebastian Martinez committed 3 years ago
commit d39ca8e4042e78f555d27833469a3e24dfcbffb5
parent ffdf7c171d4e0205fc7da39d79f2b3eb1297a140
3 files changed +48 -44
modified 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"] }
modified 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<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
-
        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,
}
modified 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<Context>) -> impl IntoResponse
        .take(32)
        .collect::<String>();
    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<AuthChallenge>,
) -> 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);
    }
}