authentication middleware handles redirects

This commit is contained in:
Aravinth Manivannan 2021-10-12 21:56:16 +05:30
parent f10c620040
commit 4158147f84
Signed by: realaravinth
GPG key ID: AD9F0F08E855ED88
10 changed files with 294 additions and 50 deletions

7
Cargo.lock generated
View file

@ -2679,6 +2679,7 @@ dependencies = [
"sqlx", "sqlx",
"tokio", "tokio",
"url", "url",
"urlencoding",
"uuid", "uuid",
"validator", "validator",
] ]
@ -2963,6 +2964,12 @@ dependencies = [
"percent-encoding", "percent-encoding",
] ]
[[package]]
name = "urlencoding"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68b90931029ab9b034b300b797048cf23723400aa757e8a2bfb9d748102f9821"
[[package]] [[package]]
name = "utf-8" name = "utf-8"
version = "0.7.6" version = "0.7.6"

View file

@ -54,6 +54,7 @@ log = "0.4"
lazy_static = "1.4" lazy_static = "1.4"
url = "2.2" url = "2.2"
urlencoding = "2.1.0"
rand = "0.8" rand = "0.8"
uuid = { version="0.8.2", features = ["v4"]} uuid = { version="0.8.2", features = ["v4"]}

View file

@ -25,12 +25,29 @@ use crate::errors::*;
use crate::AppData; use crate::AppData;
pub mod routes { pub mod routes {
use crate::middleware::auth::GetLoginRoute;
use url::Url;
pub struct Auth { pub struct Auth {
pub logout: &'static str, pub logout: &'static str,
pub login: &'static str, pub login: &'static str,
pub register: &'static str, pub register: &'static str,
} }
impl GetLoginRoute for Auth {
fn get_login_route(&self, src: Option<&str>) -> String {
if let Some(redirect_to) = src {
let mut url = Url::parse("http://x/").unwrap();
url.set_path(self.login);
url.query_pairs_mut()
.append_pair("redirect_to", redirect_to);
let path = format!("{}/?{}", url.path(), url.query().unwrap());
path
} else {
self.login.to_string()
}
}
}
impl Auth { impl Auth {
pub const fn new() -> Auth { pub const fn new() -> Auth {
let login = "/api/v1/admin/signin"; let login = "/api/v1/admin/signin";

View file

@ -51,7 +51,7 @@ pub mod runners {
let mut uuid; let mut uuid;
let now = OffsetDateTime::now_utc(); let now = OffsetDateTime::now_utc();
payload.difficulties.sort(); payload.difficulties.sort_unstable();
loop { loop {
uuid = get_uuid(); uuid = get_uuid();
@ -121,11 +121,45 @@ async fn add(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::api::v1::bench::{Bench, Submission}; use crate::api::v1::bench::Submission;
use crate::data::Data; use crate::data::Data;
use crate::middleware::auth::GetLoginRoute;
use crate::tests::*; use crate::tests::*;
use crate::*; use crate::*;
use actix_web::{http::header, test};
#[actix_rt::test]
async fn test_bench_register_works() {
let data = Data::new().await;
let app = get_app!(data).await;
let signin_resp = test::call_service(
&app,
test::TestRequest::get()
.uri(V1_API_ROUTES.benches.register)
.to_request(),
)
.await;
assert_eq!(signin_resp.status(), StatusCode::OK);
let redirect_to = Some("foo");
let signin_resp = test::call_service(
&app,
test::TestRequest::get()
.uri(&V1_API_ROUTES.benches.get_login_route(redirect_to))
.to_request(),
)
.await;
assert_eq!(signin_resp.status(), StatusCode::FOUND);
let headers = signin_resp.headers();
assert_eq!(
headers.get(header::LOCATION).unwrap(),
redirect_to.as_ref().unwrap()
)
}
#[actix_rt::test] #[actix_rt::test]
async fn test_add_campaign() { async fn test_add_campaign() {
const NAME: &str = "testadminuser"; const NAME: &str = "testadminuser";
@ -136,29 +170,6 @@ mod tests {
const DEVICE_SOFTWARE_RECOGNISED: &str = "Foobar.v2"; const DEVICE_SOFTWARE_RECOGNISED: &str = "Foobar.v2";
const THREADS: i32 = 4; const THREADS: i32 = 4;
let benches = vec![
Bench {
difficulty: 1,
duration: 1.00,
},
Bench {
difficulty: 2,
duration: 2.00,
},
Bench {
difficulty: 3,
duration: 3.00,
},
Bench {
difficulty: 4,
duration: 4.00,
},
Bench {
difficulty: 5,
duration: 5.00,
},
];
{ {
let data = Data::new().await; let data = Data::new().await;
delete_user(NAME, &data).await; delete_user(NAME, &data).await;
@ -181,7 +192,7 @@ mod tests {
device_user_provided: DEVICE_USER_PROVIDED.into(), device_user_provided: DEVICE_USER_PROVIDED.into(),
device_software_recognised: DEVICE_SOFTWARE_RECOGNISED.into(), device_software_recognised: DEVICE_SOFTWARE_RECOGNISED.into(),
threads: THREADS, threads: THREADS,
benches: benches.clone(), benches: BENCHES.clone(),
}; };
let _proof = let _proof =

View file

@ -30,8 +30,8 @@ pub fn services(cfg: &mut ServiceConfig) {
campaigns::services(cfg); campaigns::services(cfg);
} }
pub fn get_admin_check_login() -> crate::CheckLogin { pub fn get_admin_check_login() -> crate::CheckLogin<auth::routes::Auth> {
crate::CheckLogin::new(crate::V1_API_ROUTES.admin.auth.register) crate::CheckLogin::new(crate::V1_API_ROUTES.admin.auth)
} }
pub mod routes { pub mod routes {

View file

@ -18,7 +18,7 @@ use std::borrow::Cow;
use std::str::FromStr; use std::str::FromStr;
use actix_identity::Identity; use actix_identity::Identity;
use actix_web::{web, HttpResponse, Responder}; use actix_web::{http, web, HttpResponse, Responder};
use futures::future::try_join_all; use futures::future::try_join_all;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::types::time::OffsetDateTime; use sqlx::types::time::OffsetDateTime;
@ -29,6 +29,9 @@ use crate::errors::*;
use crate::AppData; use crate::AppData;
pub mod routes { pub mod routes {
use crate::middleware::auth::GetLoginRoute;
pub struct Benches { pub struct Benches {
pub submit: &'static str, pub submit: &'static str,
pub register: &'static str, pub register: &'static str,
@ -36,6 +39,25 @@ pub mod routes {
pub scope: &'static str, pub scope: &'static str,
} }
impl GetLoginRoute for Benches {
fn get_login_route(&self, src: Option<&str>) -> String {
if let Some(redirect_to) = src {
// uri::Builder::new().path_and_query(
format!(
"{}?redirect_to={}",
self.register,
urlencoding::encode(redirect_to)
)
// let mut url: Uri = self.register.parse().unwrap();
// url.qu
// url.query_pairs_mut()
// .append_pair("redirect_to", redirect_to);
} else {
self.register.to_string()
}
}
}
impl Benches { impl Benches {
pub const fn new() -> Benches { pub const fn new() -> Benches {
let submit = "/api/v1/benches/{campaign_id}/submit"; let submit = "/api/v1/benches/{campaign_id}/submit";
@ -50,10 +72,10 @@ pub mod routes {
} }
} }
pub fn submit_route(&self, campaign_id: &str) -> String { pub fn submit_route(&self, campaign_id: &str) -> String {
self.submit.replace("{campaign_id}", &campaign_id) self.submit.replace("{campaign_id}", campaign_id)
} }
pub fn fetch_routes(&self, campaign_id: &str) -> String { pub fn fetch_routes(&self, campaign_id: &str) -> String {
self.fetch.replace("{campaign_id}", &campaign_id) self.fetch.replace("{campaign_id}", campaign_id)
} }
} }
} }
@ -99,11 +121,27 @@ pub mod runners {
} }
} }
#[derive(Deserialize)]
pub struct Query {
pub redirect_to: Option<String>,
}
#[my_codegen::get(path = "crate::V1_API_ROUTES.benches.register")] #[my_codegen::get(path = "crate::V1_API_ROUTES.benches.register")]
async fn register(data: AppData, id: Identity) -> ServiceResult<impl Responder> { async fn register(
data: AppData,
id: Identity,
path: web::Query<Query>,
) -> ServiceResult<HttpResponse> {
let uuid = runners::register_runner(&data).await?; let uuid = runners::register_runner(&data).await?;
id.remember(uuid.to_string()); id.remember(uuid.to_string());
Ok(HttpResponse::Ok()) let path = path.into_inner();
if let Some(redirect_to) = path.redirect_to {
Ok(HttpResponse::Found()
.insert_header((http::header::LOCATION, redirect_to))
.finish())
} else {
Ok(HttpResponse::Ok().into())
}
} }
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
@ -126,8 +164,8 @@ pub struct SubmissionProof {
pub proof: String, pub proof: String,
} }
fn get_check_login() -> crate::CheckLogin { fn get_check_login() -> crate::CheckLogin<routes::Benches> {
crate::CheckLogin::new(crate::V1_API_ROUTES.benches.register) crate::CheckLogin::new(crate::V1_API_ROUTES.benches)
} }
#[my_codegen::post( #[my_codegen::post(

View file

@ -16,6 +16,8 @@
*/ */
#![allow(clippy::type_complexity)] #![allow(clippy::type_complexity)]
use std::rc::Rc;
use actix_http::body::AnyBody; use actix_http::body::AnyBody;
use actix_identity::Identity; use actix_identity::Identity;
use actix_service::{Service, Transform}; use actix_service::{Service, Transform};
@ -24,43 +26,50 @@ use actix_web::{http, Error, FromRequest, HttpResponse};
use futures::future::{ok, Either, Ready}; use futures::future::{ok, Either, Ready};
pub struct CheckLogin { pub trait GetLoginRoute {
login: &'static str, fn get_login_route(&self, src: Option<&str>) -> String;
} }
impl CheckLogin { pub struct CheckLogin<T: GetLoginRoute> {
pub fn new(login: &'static str) -> Self { login: Rc<T>,
}
impl<T: GetLoginRoute> CheckLogin<T> {
pub fn new(login: T) -> Self {
let login = Rc::new(login);
Self { login } Self { login }
} }
} }
impl<S> Transform<S, ServiceRequest> for CheckLogin impl<S, GT> Transform<S, ServiceRequest> for CheckLogin<GT>
where where
S: Service<ServiceRequest, Response = ServiceResponse<AnyBody>, Error = Error>, S: Service<ServiceRequest, Response = ServiceResponse<AnyBody>, Error = Error>,
S::Future: 'static, S::Future: 'static,
GT: GetLoginRoute,
{ {
type Response = ServiceResponse<AnyBody>; type Response = ServiceResponse<AnyBody>;
type Error = Error; type Error = Error;
type Transform = CheckLoginMiddleware<S>; type Transform = CheckLoginMiddleware<S, GT>;
type InitError = (); type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>; type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future { fn new_transform(&self, service: S) -> Self::Future {
ok(CheckLoginMiddleware { ok(CheckLoginMiddleware {
service, service,
login: self.login, login: self.login.clone(),
}) })
} }
} }
pub struct CheckLoginMiddleware<S> { pub struct CheckLoginMiddleware<S, GT> {
service: S, service: S,
login: &'static str, login: Rc<GT>,
} }
impl<S> Service<ServiceRequest> for CheckLoginMiddleware<S> impl<S, GT> Service<ServiceRequest> for CheckLoginMiddleware<S, GT>
where where
S: Service<ServiceRequest, Response = ServiceResponse<AnyBody>, Error = Error>, S: Service<ServiceRequest, Response = ServiceResponse<AnyBody>, Error = Error>,
S::Future: 'static, S::Future: 'static,
GT: GetLoginRoute,
{ {
type Response = ServiceResponse<AnyBody>; type Response = ServiceResponse<AnyBody>;
type Error = Error; type Error = Error;
@ -80,12 +89,134 @@ where
let req = ServiceRequest::from_parts(r, pl); let req = ServiceRequest::from_parts(r, pl);
Either::Left(self.service.call(req)) Either::Left(self.service.call(req))
} else { } else {
let req = ServiceRequest::from_parts(r, pl); //.ok().unwrap(); let path = r.uri().path_and_query().map(|path| path.as_str());
let path = self.login.get_login_route(path);
let req = ServiceRequest::from_parts(r, pl);
Either::Right(ok(req.into_response( Either::Right(ok(req.into_response(
HttpResponse::Found() HttpResponse::Found()
.insert_header((http::header::LOCATION, self.login)) .insert_header((http::header::LOCATION, path))
.finish(), .finish(),
))) )))
} }
} }
} }
#[cfg(test)]
mod tests {
use url::Url;
use crate::api::v1::bench::Submission;
use crate::data::Data;
use crate::middleware::auth::GetLoginRoute;
use crate::tests::*;
use crate::*;
use actix_web::{http::header, test};
#[actix_rt::test]
async fn auth_middleware_works() {
fn make_uri(path: &str, queries: &Option<Vec<(&str, &str)>>) -> String {
let mut url = Url::parse("http://x/").unwrap();
let final_path;
url.set_path(path);
if let Some(queries) = queries {
{
let mut query_pairs = url.query_pairs_mut();
queries.iter().for_each(|(k, v)| {
query_pairs.append_pair(k, v);
});
}
final_path = format!("{}?{}", url.path(), url.query().unwrap());
} else {
final_path = url.path().to_string();
}
final_path
}
const NAME: &str = "testmiddlewareuser";
const EMAIL: &str = "testuserupda@testmiddlewareuser.com";
const PASSWORD: &str = "longpassword2";
const DEVICE_USER_PROVIDED: &str = "foo";
const DEVICE_SOFTWARE_RECOGNISED: &str = "Foobar.v2";
const THREADS: i32 = 4;
let queries = Some(vec![
("foo", "bar"),
("src", "/x/y/z"),
("with_q", "/a/b/c/?goo=x"),
]);
{
let data = Data::new().await;
delete_user(NAME, &data).await;
}
let (data, _creds, signin_resp) =
register_and_signin(NAME, EMAIL, PASSWORD).await;
let cookies = get_cookie!(signin_resp);
let survey = get_survey_user(data.clone()).await;
let survey_cookie = get_cookie!(survey);
let campaign = create_new_campaign(NAME, data.clone(), cookies.clone()).await;
let bench_submit_route =
V1_API_ROUTES.benches.submit_route(&campaign.campaign_id);
let bench_routes = vec![
(&bench_submit_route, queries.clone()),
(&bench_submit_route, None),
];
let app = get_app!(data).await;
// let campaign_routes = vec![
// (Some(V1_API_ROUTES.camp.submit), queries.clone()),
// (None, None),
// (Some(V1_API_ROUTES.benches.submit), None),
// ];
let bench_submit_payload = Submission {
device_user_provided: DEVICE_USER_PROVIDED.into(),
device_software_recognised: DEVICE_SOFTWARE_RECOGNISED.into(),
threads: THREADS,
benches: BENCHES.clone(),
};
for (from, query) in bench_routes.iter() {
let route = make_uri(from, query);
let signin_resp = test::call_service(
&app,
post_request!(&bench_submit_payload, &route).to_request(),
)
.await;
assert_eq!(signin_resp.status(), StatusCode::FOUND);
let redirect_to = V1_API_ROUTES.benches.get_login_route(Some(&route));
let headers = signin_resp.headers();
assert_eq!(headers.get(header::LOCATION).unwrap(), &redirect_to);
let add_feedback_resp = test::call_service(
&app,
post_request!(&bench_submit_payload, &route)
.cookie(survey_cookie.clone())
.to_request(),
)
.await;
assert_eq!(add_feedback_resp.status(), StatusCode::OK);
}
}
// let signin_resp = test::call_service(
// &app,
// test::TestRequest::get()
// .uri(V1_API_ROUTES.benches.get_login_route(redirect_to).as_ref().unwrap())
// .to_request(),
// )
// .await;
// assert_eq!(signin_resp.status(), StatusCode::FOUND);
// let headers = signin_resp.headers();
// assert_eq!(
// headers.get(header::LOCATION).unwrap(),
// redirect_to.as_ref().unwrap()
// )
//
}

View file

@ -126,8 +126,8 @@ mod tests {
for file in [ for file in [
assets::LOGO.path, assets::LOGO.path,
assets::HEADSETS.path, assets::HEADSETS.path,
&*crate::JS, *crate::JS,
&*crate::GLUE, *crate::GLUE,
] ]
.iter() .iter()
{ {

View file

@ -20,6 +20,7 @@ use std::sync::Arc;
use actix_web::cookie::Cookie; use actix_web::cookie::Cookie;
use actix_web::test; use actix_web::test;
use actix_web::{dev::ServiceResponse, error::ResponseError, http::StatusCode}; use actix_web::{dev::ServiceResponse, error::ResponseError, http::StatusCode};
use lazy_static::lazy_static;
use serde::Serialize; use serde::Serialize;
use uuid::Uuid; use uuid::Uuid;
@ -28,7 +29,7 @@ use crate::api::v1::admin::{
auth::runners::{Login, Register}, auth::runners::{Login, Register},
campaigns::{AddCapmaign, AddCapmaignResp}, campaigns::{AddCapmaign, AddCapmaignResp},
}; };
use crate::api::v1::bench::{BenchConfig, Submission, SubmissionProof}; use crate::api::v1::bench::{Bench, BenchConfig, Submission, SubmissionProof};
use crate::data::Data; use crate::data::Data;
use crate::errors::*; use crate::errors::*;
use crate::V1_API_ROUTES; use crate::V1_API_ROUTES;
@ -396,3 +397,28 @@ pub async fn submit_bench(
// assert_eq!(get_feedback_resp.status(), StatusCode::OK); // assert_eq!(get_feedback_resp.status(), StatusCode::OK);
// test::read_body_json(get_feedback_resp).await // test::read_body_json(get_feedback_resp).await
//} //}
lazy_static! {
pub static ref BENCHES: Vec<Bench> = vec![
Bench {
difficulty: 1,
duration: 1.00,
},
Bench {
difficulty: 2,
duration: 2.00,
},
Bench {
difficulty: 3,
duration: 3.00,
},
Bench {
difficulty: 4,
duration: 4.00,
},
Bench {
difficulty: 5,
duration: 5.00,
},
];
}

13
templates/vendor.ts Normal file
View file

@ -0,0 +1,13 @@
/*
* mCaptcha is a PoW based DoS protection software.
* This is the frontend web component of the mCaptcha system
* Copyright © 2021 Aravinth Manivnanan <realaravinth@batsense.net>.
*
* Use of this source code is governed by Apache 2.0 or MIT license.
* You shoud have received a copy of MIT and Apache 2.0 along with
* this program. If not, see <https://spdx.org/licenses/MIT.html> for
* MIT or <http://www.apache.org/licenses/LICENSE-2.0> for Apache.
*/
import * as glue from 'mcaptcha-glue'
glue.init()