Browse Source

implement secure cooking

main
xenua 1 year ago
parent
commit
98ae013906
Signed by: xenua
GPG Key ID: 8F93B68BD37255B8
  1. 38
      src/data.rs
  2. 12
      src/main.rs
  3. 37
      src/utils.rs
  4. 9
      src/views/auth.rs
  5. 28
      src/views/link.rs

38
src/data.rs

@ -1,12 +1,13 @@
use std::{fmt::Display, sync::Arc, time::Duration}; use std::{fmt::Display, sync::Arc, time::Duration};
use cookie::{Cookie, Key}; use cookie::{Cookie, CookieBuilder, Key, SameSite};
use dashmap::DashMap; use dashmap::DashMap;
use sea_orm::{error::DbErr, DatabaseConnection, EntityTrait, QuerySelect}; use sea_orm::{error::DbErr, DatabaseConnection, EntityTrait, QuerySelect};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::time::Instant; use tokio::time::Instant;
use entity::{link, user}; use entity::{link, user};
use tracing::warn;
use uuid::Uuid; use uuid::Uuid;
use crate::utils; use crate::utils;
@ -61,16 +62,6 @@ impl AppState {
}); });
Ok(()) Ok(())
} }
pub async fn is_user_admin(&self, id: i32) -> Result<bool, DbErr> {
Ok(user::Entity::find_by_id(id)
.select_only()
.column(user::Column::IsAdmin)
.one(&self.db)
.await?
.map(|u| u.is_admin)
.unwrap_or(false))
}
} }
#[derive(Clone)] #[derive(Clone)]
@ -137,9 +128,32 @@ impl UserSession {
self.created.elapsed() > self.expiry self.created.elapsed() > self.expiry
} }
pub fn get_decrypted_cookie(&self) -> String { pub fn get_cookie_value(&self) -> String {
self.decrypted_session_cookie.to_string() self.decrypted_session_cookie.to_string()
} }
pub fn into_cookie(self, opts: AppOptions) -> Cookie<'static> {
if opts.use_secure_cookie() {
if opts.domain.is_none() {
warn!("configured to use secure cookie, but no domain is set! falling back to insecure cookie");
return self.into_insecure_cookie().finish();
}
let domain = opts.domain.expect("math broke");
self.into_insecure_cookie()
.secure(true)
.domain(domain)
.same_site(SameSite::Strict)
.finish()
} else {
self.into_insecure_cookie().finish()
}
}
fn into_insecure_cookie(self) -> CookieBuilder<'static> {
Cookie::build("_session", self.get_cookie_value())
.http_only(true)
.max_age(cookie::time::Duration::days(7))
}
} }
impl Display for UserSession { impl Display for UserSession {

12
src/main.rs

@ -1,10 +1,7 @@
use std::{env, sync::Arc}; use std::{env, sync::Arc};
use axum::{ use axum::{
extract::Path,
http::StatusCode,
middleware::{from_fn, from_fn_with_state}, middleware::{from_fn, from_fn_with_state},
response::{IntoResponse, Redirect},
routing::{get, post}, routing::{get, post},
Router, Router,
}; };
@ -13,7 +10,7 @@ use miette::IntoDiagnostic;
use sea_orm::Database; use sea_orm::Database;
use time::macros::format_description; use time::macros::format_description;
use tower::ServiceBuilder; use tower::ServiceBuilder;
use tower_http::trace::TraceLayer; use tower_http::{services::ServeDir, trace::TraceLayer};
use tracing::info; use tracing::info;
use tracing_subscriber::{fmt::time::UtcTime, EnvFilter, FmtSubscriber}; use tracing_subscriber::{fmt::time::UtcTime, EnvFilter, FmtSubscriber};
@ -38,6 +35,7 @@ async fn main() -> miette::Result<()> {
Err(_) => "sqlx=warn", Err(_) => "sqlx=warn",
}; };
let domain = env::var("BASE_DOMAIN").ok(); let domain = env::var("BASE_DOMAIN").ok();
let static_file_path = env::var("STATIC_FILE_PATH").unwrap_or("static".to_string());
let charset: Vec<char> = env::var("V_ID_CHARSET") let charset: Vec<char> = env::var("V_ID_CHARSET")
.unwrap_or("abcdefghijkmnpqrstuwxyz123467890".to_string()) .unwrap_or("abcdefghijkmnpqrstuwxyz123467890".to_string())
.chars() .chars()
@ -62,7 +60,9 @@ async fn main() -> miette::Result<()> {
let db = Database::connect(db_conn.clone()).await.into_diagnostic()?; let db = Database::connect(db_conn.clone()).await.into_diagnostic()?;
utils::migrate_and_seed_db(&db).await.into_diagnostic()?; utils::migrate_and_seed_db(&db).await.into_diagnostic()?;
if test_data { if test_data {
utils::insert_dev_test_data(&db, charset.clone(), id_len).await.into_diagnostic()?; utils::insert_dev_test_data(&db, charset.clone(), id_len)
.await
.into_diagnostic()?;
info!("V_DEV_TEST_DATA is enabled. credentials: dev:a-password"); info!("V_DEV_TEST_DATA is enabled. credentials: dev:a-password");
} }
@ -79,7 +79,9 @@ async fn main() -> miette::Result<()> {
let app = Router::new() let app = Router::new()
.route("/", get(|| async { "landing page with login form" })) .route("/", get(|| async { "landing page with login form" }))
.route("/404", get(|| async { "404 not found " }))
.route("/gay/login", post(views::auth::login)) // this one's excempt from the login requirement .route("/gay/login", post(views::auth::login)) // this one's excempt from the login requirement
.nest_service("/gay/static", ServeDir::new(static_file_path))
.nest( .nest(
"/gay", // everything is under this namespace to avoid blocking possible link uris as much as possible "/gay", // everything is under this namespace to avoid blocking possible link uris as much as possible
Router::new() Router::new()

37
src/utils.rs

@ -12,8 +12,8 @@ use entity::prelude::*;
use entity::user; use entity::user;
use migration::Migrator; use migration::Migrator;
use migration::MigratorTrait; use migration::MigratorTrait;
use sea_orm::{DatabaseConnection, DbErr};
use sea_orm::entity::prelude::*; use sea_orm::entity::prelude::*;
use sea_orm::{DatabaseConnection, DbErr};
use serde_json::json; use serde_json::json;
use tera::{Context, Tera}; use tera::{Context, Tera};
@ -39,32 +39,49 @@ pub async fn migrate_and_seed_db(db: &DatabaseConnection) -> Result<(), DbErr> {
} }
} }
pub async fn insert_dev_test_data(db: &DatabaseConnection, charset: Vec<char>, len: usize) -> Result<(), DbErr> { pub async fn insert_dev_test_data(
if User::find().filter(user::Column::Name.eq("dev")).one(db).await?.is_none() { db: &DatabaseConnection,
charset: Vec<char>,
len: usize,
) -> Result<(), DbErr> {
if User::find()
.filter(user::Column::Name.eq("dev"))
.one(db)
.await?
.is_none()
{
let dev_password = hash_password("a-password".as_bytes()); let dev_password = hash_password("a-password".as_bytes());
let dev_uid = User::insert(user::ActiveModel::from_json(json!({ let dev_uid = User::insert(user::ActiveModel::from_json(json!({
"name": "dev", "name": "dev",
"is_admin": false, "is_admin": false,
"password": dev_password, "password": dev_password,
}))?).exec(db).await?.last_insert_id; }))?)
.exec(db)
.await?
.last_insert_id;
info!("created dev user"); info!("created dev user");
let dev_link1 = Link::insert(link::ActiveModel::from_json(json!({ let dev_link1 = Link::insert(link::ActiveModel::from_json(json!({
"source": "example", "source": "example",
"target": "http://example.com", "target": "http://example.com",
"user_id": dev_uid, "user_id": dev_uid,
}))?).exec(db).await?.last_insert_id; }))?)
.exec(db)
.await?
.last_insert_id;
let dev_link2 = Link::insert(link::ActiveModel::from_json(json!({ let dev_link2 = Link::insert(link::ActiveModel::from_json(json!({
"source": gen_id(len, charset), "source": gen_id(len, charset),
"target": "https://random.org", "target": "https://random.org",
"user_id": dev_uid, "user_id": dev_uid,
}))?).exec(db).await?.last_insert_id; }))?)
.exec(db)
.await?
.last_insert_id;
info!("created two links for dev user"); info!("created two links for dev user");
} }
Ok(()) Ok(())
} }

9
src/views/auth.rs

@ -4,7 +4,6 @@ use axum::{
response::{IntoResponse, Redirect, Response}, response::{IntoResponse, Redirect, Response},
Extension, Form, Extension, Form,
}; };
use cookie::Cookie;
use entity::user; use entity::user;
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
use std::sync::Arc; use std::sync::Arc;
@ -23,17 +22,17 @@ pub async fn login(
.map_err(|_| Redirect::to("/?redirect_reason=login_failed"))? .map_err(|_| Redirect::to("/?redirect_reason=login_failed"))?
{ {
let session = UserSession::new(u.id, u.is_admin); let session = UserSession::new(u.id, u.is_admin);
let cookie_dec = Cookie::new("_session", session.get_decrypted_cookie()); let cookie_dec = session.clone().into_cookie(state.opts.clone());
let cookie_enc = state.encrypt_cookie(cookie_dec); let cookie_enc = state.encrypt_cookie(cookie_dec);
state state
.sessions .sessions
.insert(session.get_decrypted_cookie(), session) .insert(session.get_cookie_value(), session)
.map(|old_session| { .map(|old_session| {
warn!( warn!(
"sound the alarms! same session cookie for user id={} generated twice: '{}'", "sound the alarms! same session cookie for user id={} generated twice: '{}'",
old_session.get_decrypted_cookie(), old_session.get_cookie_value(),
session.get_decrypted_cookie() session.get_cookie_value()
) )
}); });

28
src/views/link.rs

@ -18,12 +18,12 @@ use std::{collections::HashMap, sync::Arc};
/// crud routes for /// crud routes for
pub fn get_routes() -> Router<Arc<AppState>> { pub fn get_routes() -> Router<Arc<AppState>> {
Router::new() Router::new().route("/", get(list).post(create)).route(
.route("/", get(list).post(create)) "/:name",
.route( get(|| async { "filled out form with link data" })
"/:name", .patch(update)
get(|| async { "filled out form with link data" }).patch(update).delete(delete), .delete(delete),
) )
// admins can change/delete all, users their own // admins can change/delete all, users their own
} }
@ -31,15 +31,19 @@ pub async fn detail(
State(state): State<Arc<AppState>>, State(state): State<Arc<AppState>>,
Extension(user): Extension<CurrentUser>, Extension(user): Extension<CurrentUser>,
Path(source): Path<String>, Path(source): Path<String>,
) -> String { ) -> String {
let link = match link::Entity::find().filter(link::Column::Source.eq(source)).one(&state.db).await { let link = match link::Entity::find()
.filter(link::Column::Source.eq(source))
.one(&state.db)
.await
{
Err(_) => return utils::render_status_code(StatusCode::INTERNAL_SERVER_ERROR).await, Err(_) => return utils::render_status_code(StatusCode::INTERNAL_SERVER_ERROR).await,
Ok(None) => return utils::render_status_code(StatusCode::NOT_FOUND).await, Ok(None) => return utils::render_status_code(StatusCode::NOT_FOUND).await,
Ok(Some(l)) => l, Ok(Some(l)) => l,
}; };
if (!user.is_admin) && link.user_id != user.id { if (!user.is_admin) && link.user_id != user.id {
return utils::render_status_code(StatusCode::UNAUTHORIZED).await return utils::render_status_code(StatusCode::UNAUTHORIZED).await;
} }
let mut context = Context::new(); let mut context = Context::new();
@ -187,12 +191,12 @@ pub async fn delete(
Err(utils::render_status_code(StatusCode::UNAUTHORIZED).await)? Err(utils::render_status_code(StatusCode::UNAUTHORIZED).await)?
} }
l l
}, }
}; };
match link.delete(&state.db).await { match link.delete(&state.db).await {
Err(_) => Err(utils::render_status_code(StatusCode::INTERNAL_SERVER_ERROR).await)?, Err(_) => Err(utils::render_status_code(StatusCode::INTERNAL_SERVER_ERROR).await)?,
Ok(_) => () Ok(_) => (),
}; };
Ok(Redirect::to("/gay/link")) Ok(Redirect::to("/gay/link"))

Loading…
Cancel
Save