Browse Source

implement secure cooking

main
xenua 12 months 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 @@ @@ -1,12 +1,13 @@
use std::{fmt::Display, sync::Arc, time::Duration};
use cookie::{Cookie, Key};
use cookie::{Cookie, CookieBuilder, Key, SameSite};
use dashmap::DashMap;
use sea_orm::{error::DbErr, DatabaseConnection, EntityTrait, QuerySelect};
use serde::{Deserialize, Serialize};
use tokio::time::Instant;
use entity::{link, user};
use tracing::warn;
use uuid::Uuid;
use crate::utils;
@ -61,16 +62,6 @@ impl AppState { @@ -61,16 +62,6 @@ impl AppState {
});
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)]
@ -137,9 +128,32 @@ impl UserSession { @@ -137,9 +128,32 @@ impl UserSession {
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()
}
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 {

12
src/main.rs

@ -1,10 +1,7 @@ @@ -1,10 +1,7 @@
use std::{env, sync::Arc};
use axum::{
extract::Path,
http::StatusCode,
middleware::{from_fn, from_fn_with_state},
response::{IntoResponse, Redirect},
routing::{get, post},
Router,
};
@ -13,7 +10,7 @@ use miette::IntoDiagnostic; @@ -13,7 +10,7 @@ use miette::IntoDiagnostic;
use sea_orm::Database;
use time::macros::format_description;
use tower::ServiceBuilder;
use tower_http::trace::TraceLayer;
use tower_http::{services::ServeDir, trace::TraceLayer};
use tracing::info;
use tracing_subscriber::{fmt::time::UtcTime, EnvFilter, FmtSubscriber};
@ -38,6 +35,7 @@ async fn main() -> miette::Result<()> { @@ -38,6 +35,7 @@ async fn main() -> miette::Result<()> {
Err(_) => "sqlx=warn",
};
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")
.unwrap_or("abcdefghijkmnpqrstuwxyz123467890".to_string())
.chars()
@ -62,7 +60,9 @@ async fn main() -> miette::Result<()> { @@ -62,7 +60,9 @@ async fn main() -> miette::Result<()> {
let db = Database::connect(db_conn.clone()).await.into_diagnostic()?;
utils::migrate_and_seed_db(&db).await.into_diagnostic()?;
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");
}
@ -79,7 +79,9 @@ async fn main() -> miette::Result<()> { @@ -79,7 +79,9 @@ async fn main() -> miette::Result<()> {
let app = Router::new()
.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
.nest_service("/gay/static", ServeDir::new(static_file_path))
.nest(
"/gay", // everything is under this namespace to avoid blocking possible link uris as much as possible
Router::new()

37
src/utils.rs

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

9
src/views/auth.rs

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

28
src/views/link.rs

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

Loading…
Cancel
Save