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
assets
scripts/creds.py
__pycache__/
*.py[cod]
*$py.class

17
Cargo.lock generated
View file

@ -153,6 +153,22 @@ dependencies = [
"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]]
name = "actix-tls"
version = "3.0.0-beta.5"
@ -2415,6 +2431,7 @@ dependencies = [
"actix-identity",
"actix-rt",
"actix-service",
"actix-session",
"actix-web",
"actix-web-codegen 0.5.0-beta.4 (git+https://github.com/realaravinth/actix-web)",
"argon2-creds",

View file

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

View file

@ -2,11 +2,13 @@ debug = true
allow_registration = true
source_code = "https://github.com/mcaptcha/survey"
password = "password"
default_campaign = "b6b261fa-3ef9-4d7f-8852-339b8f81bb01"
[server]
# Please set a unique value, your kaizen instance's security depends on this being
# unique
cookie_secret = "8ce364dab188452ffa76c3e1869be5d40dcb9db4826b7b78a3e6ce1a8ca19d32"
cookie_secret2 = "408f276a8dec44992b14bdf1be9289a2c67547807e16c5ebcf5e904fcc916208"
# 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
port = 7000

View file

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

View file

@ -409,7 +409,6 @@ mod tests {
let cookies = get_cookie!(signin_resp);
let survey = get_survey_user(data.clone()).await;
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_config =

View file

@ -31,7 +31,11 @@ pub fn services(cfg: &mut ServiceConfig) {
}
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 {

View file

@ -17,7 +17,7 @@
use std::borrow::Cow;
use std::str::FromStr;
use actix_identity::Identity;
use actix_session::Session;
use actix_web::{http, web, HttpResponse, Responder};
use futures::future::try_join_all;
use serde::{Deserialize, Serialize};
@ -28,6 +28,8 @@ use super::{get_uuid, RedirectQuery};
use crate::errors::*;
use crate::AppData;
pub const SURVEY_USER_ID: &str = "survey_user_id";
pub mod routes {
use crate::middleware::auth::GetLoginRoute;
@ -124,11 +126,11 @@ pub mod runners {
#[my_codegen::get(path = "crate::V1_API_ROUTES.benches.register")]
async fn register(
data: AppData,
id: Identity,
session: Session,
path: web::Query<RedirectQuery>,
) -> ServiceResult<HttpResponse> {
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();
if let Some(redirect_to) = path.redirect_to {
Ok(HttpResponse::Found()
@ -159,8 +161,12 @@ pub struct SubmissionProof {
pub proof: String,
}
fn get_check_login() -> crate::CheckLogin<routes::Benches> {
crate::CheckLogin::new(crate::V1_API_ROUTES.benches)
pub fn get_check_login() -> crate::CheckLogin<routes::Benches> {
use crate::middleware::auth::*;
CheckLogin::new(
crate::V1_API_ROUTES.benches,
AuthenticatedSession::ActixSession,
)
}
#[my_codegen::post(
@ -169,11 +175,11 @@ fn get_check_login() -> crate::CheckLogin<routes::Benches> {
)]
async fn submit(
data: AppData,
id: Identity,
session: Session,
payload: web::Json<Submission>,
path: web::Path<String>,
) -> ServiceResult<impl Responder> {
let username = id.identity().unwrap();
let username = session.get::<String>(SURVEY_USER_ID).unwrap().unwrap();
let path = path.into_inner();
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=()"),
)
.wrap(get_identity_service())
.wrap(get_survey_identity_service())
.wrap(get_survey_session())
.wrap(actix_middleware::NormalizePath::new(
actix_middleware::TrailingSlash::Trim,
))
@ -132,16 +132,15 @@ pub fn get_json_err() -> JsonConfig {
}
#[cfg(not(tarpaulin_include))]
pub fn get_survey_identity_service() -> IdentityService<CookieIdentityPolicy> {
let cookie_secret = &SETTINGS.server.cookie_secret;
IdentityService::new(
CookieIdentityPolicy::new(cookie_secret.as_bytes())
.name("survey-id")
.path("/survey")
.max_age_secs(30 * 60)
.domain(&SETTINGS.server.domain)
.secure(false),
)
pub fn get_survey_session() -> actix_session::CookieSession {
let cookie_secret = &SETTINGS.server.cookie_secret2;
actix_session::CookieSession::private(cookie_secret.as_bytes())
.domain(&SETTINGS.server.domain)
.name("survey-id")
.path("/survey")
.max_age(30 * 60)
.domain(&SETTINGS.server.domain)
.secure(false)
}
#[cfg(not(tarpaulin_include))]

View file

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

View file

@ -31,7 +31,8 @@ pub fn services(cfg: &mut ServiceConfig) {
}
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))]
@ -97,7 +98,7 @@ mod tests {
let headers = authenticated_resp.headers();
assert_eq!(
headers.get(header::LOCATION).unwrap(),
PAGES.panel.campaigns.home
&*super::panel::DEFAULT_CAMPAIGN_ABOUT
);
} else {
assert_eq!(authenticated_resp.status(), StatusCode::OK);

View file

@ -36,7 +36,7 @@ lazy_static! {
#[get(
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> {
let path = path.into_inner();

View file

@ -107,12 +107,37 @@ pub async fn home(data: AppData, id: Identity) -> impl Responder {
#[cfg(test)]
mod tests {
use actix_web::cookie::Cookie;
use actix_web::http::StatusCode;
use actix_web::test;
use crate::tests::*;
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]
async fn survey_pages_work() {
const NAME: &str = "surveyuserpages";
@ -127,13 +152,15 @@ mod tests {
let (_, _, 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(CAMPAIGN_NAME, data.clone(), cookies.clone()).await;
let app = get_app!(data).await;
let protected_urls =
let survey_protected_urls =
vec![PAGES.panel.campaigns.get_bench_route(&campaign.campaign_id)];
let public_urls =
@ -149,25 +176,6 @@ mod tests {
assert_eq!(resp.status(), StatusCode::OK);
}
for url in protected_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(cookies.clone())
.to_request(),
)
.await;
assert_eq!(authenticated_resp.status(), StatusCode::OK);
}
protect_urls_test(&survey_protected_urls, data, survey_cookie).await;
}
}

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/>.
*/
use actix_web::{http, HttpResponse, Responder};
use lazy_static::lazy_static;
use my_codegen::get;
use super::get_page_check_login;
use crate::PAGES;
mod campaigns;
@ -50,9 +49,18 @@ pub fn services(cfg: &mut actix_web::web::ServiceConfig) {
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 {
let loc: &str = &*DEFAULT_CAMPAIGN_ABOUT;
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()
}

View file

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

View file

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

View file

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