handle survey sessions with actix sessions

This commit is contained in:
Aravinth Manivannan 2021-10-14 18:53:06 +05:30
parent 9ea9732fcd
commit 95dba20074
Signed by: realaravinth
GPG key ID: AD9F0F08E855ED88
17 changed files with 176 additions and 82 deletions

3
.gitignore vendored
View file

@ -13,3 +13,6 @@ coverage
dist dist
assets assets
scripts/creds.py scripts/creds.py
__pycache__/
*.py[cod]
*$py.class

17
Cargo.lock generated
View file

@ -153,6 +153,22 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
] ]
[[package]]
name = "actix-session"
version = "0.5.0-beta.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b853e383318e1074c1dc988871c33cd186b89bfadfd543758b2f7ffebb88f47"
dependencies = [
"actix-service",
"actix-web",
"derive_more",
"futures-util",
"log",
"serde 1.0.130",
"serde_json",
"time 0.2.27",
]
[[package]] [[package]]
name = "actix-tls" name = "actix-tls"
version = "3.0.0-beta.5" version = "3.0.0-beta.5"
@ -2415,6 +2431,7 @@ dependencies = [
"actix-identity", "actix-identity",
"actix-rt", "actix-rt",
"actix-service", "actix-service",
"actix-session",
"actix-web", "actix-web",
"actix-web-codegen 0.5.0-beta.4 (git+https://github.com/realaravinth/actix-web)", "actix-web-codegen 0.5.0-beta.4 (git+https://github.com/realaravinth/actix-web)",
"argon2-creds", "argon2-creds",

View file

@ -24,6 +24,7 @@ path = "./src/tests-migrate.rs"
[dependencies] [dependencies]
actix-web = "4.0.0-beta.9" actix-web = "4.0.0-beta.9"
actix-identity = "0.4.0-beta.2" actix-identity = "0.4.0-beta.2"
actix-session = "0.5.0-beta.2"
actix-http = "3.0.0-beta.8" actix-http = "3.0.0-beta.8"
actix-rt = "2" actix-rt = "2"
actix-cors = "0.6.0-beta.2" actix-cors = "0.6.0-beta.2"

View file

@ -2,11 +2,13 @@ debug = true
allow_registration = true allow_registration = true
source_code = "https://github.com/mcaptcha/survey" source_code = "https://github.com/mcaptcha/survey"
password = "password" password = "password"
default_campaign = "b6b261fa-3ef9-4d7f-8852-339b8f81bb01"
[server] [server]
# Please set a unique value, your kaizen instance's security depends on this being # Please set a unique value, your kaizen instance's security depends on this being
# unique # unique
cookie_secret = "8ce364dab188452ffa76c3e1869be5d40dcb9db4826b7b78a3e6ce1a8ca19d32" cookie_secret = "8ce364dab188452ffa76c3e1869be5d40dcb9db4826b7b78a3e6ce1a8ca19d32"
cookie_secret2 = "408f276a8dec44992b14bdf1be9289a2c67547807e16c5ebcf5e904fcc916208"
# The port at which you want authentication to listen to # The port at which you want authentication to listen to
# takes a number, choose from 1000-10000 if you dont know what you are doing # takes a number, choose from 1000-10000 if you dont know what you are doing
port = 7000 port = 7000

View file

@ -1,16 +1,16 @@
#!/bin/python #!/bin/python
# Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net> # Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the # published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version. # License, or (at your option) any later version.
# #
# This program is distributed in the hope that it will be useful, # This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details. # GNU Affero General Public License for more details.
# #
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
import requests import requests
@ -18,32 +18,33 @@ import json
from creds import COOKIE from creds import COOKIE
def add_campaign(): def add_campaign():
"""Add campaign""" """Add campaign"""
url = "localhost:7000/admin/api/v1/campaign/add" url = "http://localhost:7000/admin/api/v1/campaign/add"
payload = json.dumps({ payload = json.dumps(
"name": "test_1", {
"difficulties": [ "name": "test_1",
50000, "difficulties": [
100000, 50000,
150000, 100000,
200000, 150000,
250000, 200000,
300000, 250000,
350000, 300000,
400000, 350000,
450000 400000,
] 450000,
}) ],
headers = { }
'Content-Type': 'application/json', )
'Cookie': COOKIE headers = {"Content-Type": "application/json", "Cookie": COOKIE}
}
response = requests.request("POST", url, headers=headers, data=payload) response = requests.request("POST", url, headers=headers, data=payload)
data = response.json() data = response.json()
print('campaign ID: %s' % (data["campaign_id"])) print("campaign ID: %s" % (data["campaign_id"]))
if __name__ == "__main__": if __name__ == "__main__":
add_campaign() add_campaign()

View file

@ -409,7 +409,6 @@ mod tests {
let cookies = get_cookie!(signin_resp); let cookies = get_cookie!(signin_resp);
let survey = get_survey_user(data.clone()).await; let survey = get_survey_user(data.clone()).await;
let survey_cookie = get_cookie!(survey); let survey_cookie = get_cookie!(survey);
// let app = get_app!(data).await;
let campaign = create_new_campaign(NAME, data.clone(), cookies.clone()).await; let campaign = create_new_campaign(NAME, data.clone(), cookies.clone()).await;
let campaign_config = let campaign_config =

View file

@ -31,7 +31,11 @@ pub fn services(cfg: &mut ServiceConfig) {
} }
pub fn get_admin_check_login() -> crate::CheckLogin<auth::routes::Auth> { pub fn get_admin_check_login() -> crate::CheckLogin<auth::routes::Auth> {
crate::CheckLogin::new(crate::V1_API_ROUTES.admin.auth) use crate::middleware::auth::*;
CheckLogin::new(
crate::V1_API_ROUTES.admin.auth,
AuthenticatedSession::ActixIdentity,
)
} }
pub mod routes { pub mod routes {

View file

@ -17,7 +17,7 @@
use std::borrow::Cow; use std::borrow::Cow;
use std::str::FromStr; use std::str::FromStr;
use actix_identity::Identity; use actix_session::Session;
use actix_web::{http, 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};
@ -28,6 +28,8 @@ use super::{get_uuid, RedirectQuery};
use crate::errors::*; use crate::errors::*;
use crate::AppData; use crate::AppData;
pub const SURVEY_USER_ID: &str = "survey_user_id";
pub mod routes { pub mod routes {
use crate::middleware::auth::GetLoginRoute; use crate::middleware::auth::GetLoginRoute;
@ -124,11 +126,11 @@ pub mod runners {
#[my_codegen::get(path = "crate::V1_API_ROUTES.benches.register")] #[my_codegen::get(path = "crate::V1_API_ROUTES.benches.register")]
async fn register( async fn register(
data: AppData, data: AppData,
id: Identity, session: Session,
path: web::Query<RedirectQuery>, path: web::Query<RedirectQuery>,
) -> ServiceResult<HttpResponse> { ) -> ServiceResult<HttpResponse> {
let uuid = runners::register_runner(&data).await?; let uuid = runners::register_runner(&data).await?;
id.remember(uuid.to_string()); session.insert(SURVEY_USER_ID, uuid.to_string()).unwrap();
let path = path.into_inner(); let path = path.into_inner();
if let Some(redirect_to) = path.redirect_to { if let Some(redirect_to) = path.redirect_to {
Ok(HttpResponse::Found() Ok(HttpResponse::Found()
@ -159,8 +161,12 @@ pub struct SubmissionProof {
pub proof: String, pub proof: String,
} }
fn get_check_login() -> crate::CheckLogin<routes::Benches> { pub fn get_check_login() -> crate::CheckLogin<routes::Benches> {
crate::CheckLogin::new(crate::V1_API_ROUTES.benches) use crate::middleware::auth::*;
CheckLogin::new(
crate::V1_API_ROUTES.benches,
AuthenticatedSession::ActixSession,
)
} }
#[my_codegen::post( #[my_codegen::post(
@ -169,11 +175,11 @@ fn get_check_login() -> crate::CheckLogin<routes::Benches> {
)] )]
async fn submit( async fn submit(
data: AppData, data: AppData,
id: Identity, session: Session,
payload: web::Json<Submission>, payload: web::Json<Submission>,
path: web::Path<String>, path: web::Path<String>,
) -> ServiceResult<impl Responder> { ) -> ServiceResult<impl Responder> {
let username = id.identity().unwrap(); let username = session.get::<String>(SURVEY_USER_ID).unwrap().unwrap();
let path = path.into_inner(); let path = path.into_inner();
let campaign_id = Uuid::parse_str(&path).map_err(|_| ServiceError::NotAnId)?; let campaign_id = Uuid::parse_str(&path).map_err(|_| ServiceError::NotAnId)?;

View file

@ -110,7 +110,7 @@ async fn main() -> std::io::Result<()> {
.header("Permissions-Policy", "interest-cohort=()"), .header("Permissions-Policy", "interest-cohort=()"),
) )
.wrap(get_identity_service()) .wrap(get_identity_service())
.wrap(get_survey_identity_service()) .wrap(get_survey_session())
.wrap(actix_middleware::NormalizePath::new( .wrap(actix_middleware::NormalizePath::new(
actix_middleware::TrailingSlash::Trim, actix_middleware::TrailingSlash::Trim,
)) ))
@ -132,16 +132,15 @@ pub fn get_json_err() -> JsonConfig {
} }
#[cfg(not(tarpaulin_include))] #[cfg(not(tarpaulin_include))]
pub fn get_survey_identity_service() -> IdentityService<CookieIdentityPolicy> { pub fn get_survey_session() -> actix_session::CookieSession {
let cookie_secret = &SETTINGS.server.cookie_secret; let cookie_secret = &SETTINGS.server.cookie_secret2;
IdentityService::new( actix_session::CookieSession::private(cookie_secret.as_bytes())
CookieIdentityPolicy::new(cookie_secret.as_bytes()) .domain(&SETTINGS.server.domain)
.name("survey-id") .name("survey-id")
.path("/survey") .path("/survey")
.max_age_secs(30 * 60) .max_age(30 * 60)
.domain(&SETTINGS.server.domain) .domain(&SETTINGS.server.domain)
.secure(false), .secure(false)
)
} }
#[cfg(not(tarpaulin_include))] #[cfg(not(tarpaulin_include))]

View file

@ -18,12 +18,20 @@
use std::rc::Rc; use std::rc::Rc;
use crate::api::v1::bench::SURVEY_USER_ID;
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};
use actix_session::Session;
use actix_web::dev::{ServiceRequest, ServiceResponse}; use actix_web::dev::{ServiceRequest, ServiceResponse};
use actix_web::{http, Error, FromRequest, HttpResponse}; use actix_web::{http, Error, FromRequest, HttpResponse};
#[derive(Clone)]
pub enum AuthenticatedSession {
ActixIdentity,
ActixSession,
}
use futures::future::{ok, Either, Ready}; use futures::future::{ok, Either, Ready};
pub trait GetLoginRoute { pub trait GetLoginRoute {
@ -32,12 +40,16 @@ pub trait GetLoginRoute {
pub struct CheckLogin<T: GetLoginRoute> { pub struct CheckLogin<T: GetLoginRoute> {
login: Rc<T>, login: Rc<T>,
session_type: AuthenticatedSession,
} }
impl<T: GetLoginRoute> CheckLogin<T> { impl<T: GetLoginRoute> CheckLogin<T> {
pub fn new(login: T) -> Self { pub fn new(login: T, session_type: AuthenticatedSession) -> Self {
let login = Rc::new(login); let login = Rc::new(login);
Self { login } Self {
login,
session_type,
}
} }
} }
@ -57,12 +69,14 @@ where
ok(CheckLoginMiddleware { ok(CheckLoginMiddleware {
service, service,
login: self.login.clone(), login: self.login.clone(),
session_type: self.session_type.clone(),
}) })
} }
} }
pub struct CheckLoginMiddleware<S, GT> { pub struct CheckLoginMiddleware<S, GT> {
service: S, service: S,
login: Rc<GT>, login: Rc<GT>,
session_type: AuthenticatedSession,
} }
impl<S, GT> Service<ServiceRequest> for CheckLoginMiddleware<S, GT> impl<S, GT> Service<ServiceRequest> for CheckLoginMiddleware<S, GT>
@ -79,13 +93,30 @@ where
fn call(&self, req: ServiceRequest) -> Self::Future { fn call(&self, req: ServiceRequest) -> Self::Future {
let (r, mut pl) = req.into_parts(); let (r, mut pl) = req.into_parts();
let mut is_authenticated = || match self.session_type {
AuthenticatedSession::ActixSession => {
if let Ok(Ok(Some(_))) = Session::from_request(&r, &mut pl)
.into_inner()
.map(|x| x.get::<String>(SURVEY_USER_ID))
{
true
} else {
false
}
}
// TODO investigate when the bellow statement will AuthenticatedSession::ActixIdentity => {
// return error if let Ok(Some(_)) = Identity::from_request(&r, &mut pl)
if let Ok(Some(_)) = Identity::from_request(&r, &mut pl) .into_inner()
.into_inner() .map(|x| x.identity())
.map(|x| x.identity()) {
{ true
} else {
false
}
}
};
if is_authenticated() {
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 {

View file

@ -31,7 +31,8 @@ pub fn services(cfg: &mut ServiceConfig) {
} }
pub fn get_page_check_login() -> crate::CheckLogin<auth::routes::Auth> { pub fn get_page_check_login() -> crate::CheckLogin<auth::routes::Auth> {
crate::CheckLogin::new(crate::PAGES.auth) use crate::middleware::auth::*;
CheckLogin::new(crate::PAGES.auth, AuthenticatedSession::ActixIdentity)
} }
#[cfg(not(tarpaulin_include))] #[cfg(not(tarpaulin_include))]
@ -97,7 +98,7 @@ mod tests {
let headers = authenticated_resp.headers(); let headers = authenticated_resp.headers();
assert_eq!( assert_eq!(
headers.get(header::LOCATION).unwrap(), headers.get(header::LOCATION).unwrap(),
PAGES.panel.campaigns.home &*super::panel::DEFAULT_CAMPAIGN_ABOUT
); );
} else { } else {
assert_eq!(authenticated_resp.status(), StatusCode::OK); assert_eq!(authenticated_resp.status(), StatusCode::OK);

View file

@ -36,7 +36,7 @@ lazy_static! {
#[get( #[get(
path = "PAGES.panel.campaigns.bench", path = "PAGES.panel.campaigns.bench",
wrap = "crate::pages::get_page_check_login()" wrap = "crate::api::v1::bench::get_check_login()"
)] )]
pub async fn bench(path: web::Path<String>) -> PageResult<impl Responder> { pub async fn bench(path: web::Path<String>) -> PageResult<impl Responder> {
let path = path.into_inner(); let path = path.into_inner();

View file

@ -107,12 +107,37 @@ pub async fn home(data: AppData, id: Identity) -> impl Responder {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use actix_web::cookie::Cookie;
use actix_web::http::StatusCode; use actix_web::http::StatusCode;
use actix_web::test; use actix_web::test;
use crate::tests::*; use crate::tests::*;
use crate::*; use crate::*;
async fn protect_urls_test(urls: &[String], data: Arc<Data>, cookie: Cookie<'_>) {
let app = get_app!(data).await;
for url in urls.iter() {
let resp =
test::call_service(&app, test::TestRequest::get().uri(url).to_request())
.await;
if resp.status() != StatusCode::FOUND {
println!("Probably error url: {}", url);
}
assert_eq!(resp.status(), StatusCode::FOUND);
let authenticated_resp = test::call_service(
&app,
test::TestRequest::get()
.uri(url)
.cookie(cookie.clone())
.to_request(),
)
.await;
assert_eq!(authenticated_resp.status(), StatusCode::OK);
}
}
#[actix_rt::test] #[actix_rt::test]
async fn survey_pages_work() { async fn survey_pages_work() {
const NAME: &str = "surveyuserpages"; const NAME: &str = "surveyuserpages";
@ -127,13 +152,15 @@ mod tests {
let (_, _, signin_resp) = register_and_signin(NAME, EMAIL, PASSWORD).await; let (_, _, signin_resp) = register_and_signin(NAME, EMAIL, PASSWORD).await;
let cookies = get_cookie!(signin_resp); let cookies = get_cookie!(signin_resp);
let survey = get_survey_user(data.clone()).await;
let survey_cookie = get_cookie!(survey);
let campaign = let campaign =
create_new_campaign(CAMPAIGN_NAME, data.clone(), cookies.clone()).await; create_new_campaign(CAMPAIGN_NAME, data.clone(), cookies.clone()).await;
let app = get_app!(data).await; let app = get_app!(data).await;
let protected_urls = let survey_protected_urls =
vec![PAGES.panel.campaigns.get_bench_route(&campaign.campaign_id)]; vec![PAGES.panel.campaigns.get_bench_route(&campaign.campaign_id)];
let public_urls = let public_urls =
@ -149,25 +176,6 @@ mod tests {
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
} }
for url in protected_urls.iter() { protect_urls_test(&survey_protected_urls, data, survey_cookie).await;
let resp =
test::call_service(&app, test::TestRequest::get().uri(url).to_request())
.await;
if resp.status() != StatusCode::FOUND {
println!("Probably error url: {}", url);
}
assert_eq!(resp.status(), StatusCode::FOUND);
let authenticated_resp = test::call_service(
&app,
test::TestRequest::get()
.uri(url)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(authenticated_resp.status(), StatusCode::OK);
}
} }
} }

View file

@ -14,10 +14,9 @@
* You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. * You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
use actix_web::{http, HttpResponse, Responder}; use actix_web::{http, HttpResponse, Responder};
use lazy_static::lazy_static;
use my_codegen::get; use my_codegen::get;
use super::get_page_check_login;
use crate::PAGES; use crate::PAGES;
mod campaigns; mod campaigns;
@ -50,9 +49,18 @@ pub fn services(cfg: &mut actix_web::web::ServiceConfig) {
campaigns::services(cfg); campaigns::services(cfg);
} }
#[get(path = "PAGES.panel.home", wrap = "get_page_check_login()")] lazy_static! {
pub static ref DEFAULT_CAMPAIGN_ABOUT: String = PAGES
.panel
.campaigns
.get_about_route(&*crate::SETTINGS.default_campaign);
}
#[get(path = "PAGES.panel.home")]
pub async fn home() -> impl Responder { pub async fn home() -> impl Responder {
let loc: &str = &*DEFAULT_CAMPAIGN_ABOUT;
HttpResponse::Found() HttpResponse::Found()
.insert_header((http::header::LOCATION, PAGES.panel.campaigns.home)) //.insert_header((http::header::LOCATION, PAGES.panel.campaigns.home))
.insert_header((http::header::LOCATION, loc))
.finish() .finish()
} }

View file

@ -21,12 +21,14 @@ use config::{Config, ConfigError, Environment, File};
use log::{debug, warn}; use log::{debug, warn};
use serde::Deserialize; use serde::Deserialize;
use url::Url; use url::Url;
use uuid::Uuid;
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct Server { pub struct Server {
pub port: u32, pub port: u32,
pub domain: String, pub domain: String,
pub cookie_secret: String, pub cookie_secret: String,
pub cookie_secret2: String,
pub ip: String, pub ip: String,
pub proxy_has_tls: bool, pub proxy_has_tls: bool,
} }
@ -80,6 +82,7 @@ pub struct Settings {
pub server: Server, pub server: Server,
pub source_code: String, pub source_code: String,
pub password: String, pub password: String,
pub default_campaign: String,
} }
#[cfg(not(tarpaulin_include))] #[cfg(not(tarpaulin_include))]
@ -106,9 +109,10 @@ impl Settings {
log::warn!("configuration file not found"); log::warn!("configuration file not found");
} }
s.merge(Environment::with_prefix("MCAPTCHA").separator("_"))?; s.merge(Environment::with_prefix("MCAPTCHA").separator("__"))?;
check_url(&s); check_url(&s);
check_uuid(&s);
match env::var("PORT") { match env::var("PORT") {
Ok(val) => { Ok(val) => {
@ -144,6 +148,17 @@ fn check_url(s: &Config) {
Url::parse(&url).expect("Please enter a URL for source_code in settings"); Url::parse(&url).expect("Please enter a URL for source_code in settings");
} }
#[cfg(not(tarpaulin_include))]
fn check_uuid(s: &Config) {
use std::str::FromStr;
let id = s
.get::<String>("default_campaign")
.expect("Couldn't access default_campaign");
Uuid::from_str(&id).expect("Please enter a UUID for default_campaign in settings");
}
#[cfg(not(tarpaulin_include))] #[cfg(not(tarpaulin_include))]
fn set_from_database_url(s: &mut Config, database_conf: &DatabaseBuilder) { fn set_from_database_url(s: &mut Config, database_conf: &DatabaseBuilder) {
s.set("database.username", database_conf.username.clone()) s.set("database.username", database_conf.username.clone())

View file

@ -88,7 +88,7 @@ macro_rules! get_app {
actix_web::App::new() actix_web::App::new()
.app_data(crate::get_json_err()) .app_data(crate::get_json_err())
.wrap(crate::get_identity_service()) .wrap(crate::get_identity_service())
.wrap(get_survey_identity_service()) .wrap(crate::get_survey_session())
.wrap(actix_web::middleware::NormalizePath::new( .wrap(actix_web::middleware::NormalizePath::new(
actix_web::middleware::TrailingSlash::Trim, actix_web::middleware::TrailingSlash::Trim,
)) ))

View file

@ -78,7 +78,6 @@
<a <a
class="link__btn" class="link__btn"
href="<.= crate::PAGES.panel.campaigns.get_bench_route(uuid) .>" href="<.= crate::PAGES.panel.campaigns.get_bench_route(uuid) .>"
target="_blank"
>Get started</a >Get started</a
> >
</main> </main>