\0/ got integration test working
This commit is contained in:
parent
5437600b1c
commit
1dc40ec251
File diff suppressed because it is too large
Load Diff
|
@ -14,3 +14,8 @@ webauthn-rs = "0.4.8"
|
|||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
handlebars = "4.3.6"
|
||||
deadpool-postgres = "0.11.0"
|
||||
tokio-postgres = "0.7.10"
|
||||
anyhow = "1.0.75"
|
||||
refinery = { version = "0.8.11", features = ["tokio-postgres"] }
|
||||
uuid = { version = "1.4.1", features = ["v4"] }
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use webauthn_rs::prelude::{RegisterPublicKeyCredential, Uuid};
|
||||
use actix_web::{error, HttpResponse, http::header::{ContentType, self}, web, cookie::{Cookie, SameSite}, HttpRequest};
|
||||
|
||||
use crate::{models::{AppState, User}, templates};
|
||||
use crate::{models::{AppState, User, PendingRegistration}, templates};
|
||||
|
||||
pub async fn landing_page (app_state: web::Data<AppState<'_>>) -> HttpResponse {
|
||||
let content: String = templates::landing_page(app_state.hbs.clone()).unwrap();
|
||||
|
@ -10,16 +10,21 @@ pub async fn landing_page (app_state: web::Data<AppState<'_>>) -> HttpResponse {
|
|||
.body(content)
|
||||
}
|
||||
|
||||
/// First phase of the webauthn key enrolling.
|
||||
///
|
||||
/// For now, we don't save anything to the DB. We just generate the
|
||||
/// challenge, the UUID and store everything in the session hashmap
|
||||
/// server-side, in the response and cookie on the client-side.
|
||||
pub async fn start_webauthn_registration(app_state: web::Data<AppState<'_>>, user: web::Json<User>) -> HttpResponse {
|
||||
let (creation_challenge_response, passkey_registration) = app_state.webauthn.start_passkey_registration(user.uuid, &user.user_name, &user.display_name, None).unwrap();
|
||||
let uuid_str = user.uuid.to_string();
|
||||
let uuid = Uuid::new_v4();
|
||||
let (creation_challenge_response, passkey_registration) = app_state.webauthn.start_passkey_registration(uuid, &user.user_name, &user.display_name, None).unwrap();
|
||||
let uuid_str = uuid.to_string();
|
||||
{
|
||||
let mut session = app_state.session.user_registrations.write().await;
|
||||
session.insert(user.clone(), passkey_registration);
|
||||
}
|
||||
{
|
||||
let mut uuid_db = app_state.db.user_uuid_object.write().await;
|
||||
uuid_db.insert(user.uuid, user.into_inner());
|
||||
let mut user_registrations = app_state.session.user_registrations.write().await;
|
||||
user_registrations.insert(uuid, PendingRegistration{
|
||||
user: user.into_inner(),
|
||||
registration: passkey_registration
|
||||
});
|
||||
}
|
||||
let res = serde_json::to_string(&creation_challenge_response).unwrap();
|
||||
let cookie = Cookie::build("uuid", &uuid_str)
|
||||
|
@ -32,24 +37,37 @@ pub async fn start_webauthn_registration(app_state: web::Data<AppState<'_>>, use
|
|||
.body(res)
|
||||
}
|
||||
|
||||
/// Final phase of the webauthn key enrolling.
|
||||
///
|
||||
/// We verify the key enrolling challenge, then store the new key in the DB.
|
||||
pub async fn finish_webauthn_registration(req: HttpRequest, app_state: web::Data<AppState<'_>>, register: web::Json<RegisterPublicKeyCredential>) -> impl actix_web::Responder {
|
||||
let cook = req.cookie("uuid");
|
||||
let uuid = Uuid::parse_str(cook.unwrap().value()).unwrap();
|
||||
let registration_result = {
|
||||
let users = app_state.db.user_uuid_object.read().await;
|
||||
let user = users.get(&uuid).unwrap();
|
||||
let session = app_state.session.user_registrations.read().await;
|
||||
let passkey_registration = session.get(user).unwrap();
|
||||
app_state.webauthn.finish_passkey_registration(®ister, passkey_registration)
|
||||
let pending_registration: Option<PendingRegistration> = {
|
||||
let user_registrations = app_state.session.user_registrations.read().await;
|
||||
user_registrations.get(&uuid).cloned()
|
||||
};
|
||||
let mut user_keys = app_state.db.user_keys.write().await;
|
||||
match registration_result {
|
||||
Ok(passkey) => {
|
||||
user_keys.insert(uuid, passkey);
|
||||
HttpResponse::Ok()
|
||||
.body("ok")
|
||||
|
||||
// 1. Check registration.
|
||||
// 2. Save user.
|
||||
// 3. Save key.
|
||||
match pending_registration {
|
||||
Some(PendingRegistration { user, registration }) => {
|
||||
let registration_result = app_state.webauthn.finish_passkey_registration(®ister, ®istration);
|
||||
match registration_result {
|
||||
Ok(passkey) => {
|
||||
let _ = app_state.save_user(&user).await;
|
||||
let _ = app_state.save_user_key(&uuid, &passkey).await;
|
||||
HttpResponse::Ok()
|
||||
.body("ok")
|
||||
},
|
||||
Err(_) =>
|
||||
HttpResponse::from_error(error::ErrorUnauthorized("Webauthn challenge failed"))
|
||||
}
|
||||
},
|
||||
Err(_) =>
|
||||
HttpResponse::from_error(error::ErrorUnauthorized("Webauthn challenge failed"))
|
||||
None => {
|
||||
return HttpResponse::from_error(error::ErrorInternalServerError("Session expired"))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
pub mod app;
|
||||
pub mod handlers;
|
||||
pub mod models;
|
||||
pub mod templates;
|
26
src/main.rs
26
src/main.rs
|
@ -1,6 +1,11 @@
|
|||
use std::net::SocketAddr;
|
||||
use actix_web::{App, web, HttpServer};
|
||||
use anyhow::{Result, Context};
|
||||
use clap::Parser;
|
||||
use models::Configuration;
|
||||
use serde::Deserialize;
|
||||
use serde_json;
|
||||
use std::fs;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
mod app;
|
||||
mod handlers;
|
||||
|
@ -11,19 +16,30 @@ mod templates;
|
|||
#[command(author, version, about, long_about = None)]
|
||||
struct CLIArgs {
|
||||
#[arg(short, long)]
|
||||
bind: String
|
||||
bind: String,
|
||||
#[arg(short, long)]
|
||||
config: String
|
||||
}
|
||||
|
||||
fn read_config(config_path: &str) -> Result<Configuration> {
|
||||
let content = fs::read_to_string(config_path)
|
||||
.with_context(|| format!("Cannot read the configuration file at {}.", config_path))?;
|
||||
let res: Configuration = serde_json::from_str(&content)
|
||||
.context("Cannot parse JSON configuration.")?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
let args = CLIArgs::parse();
|
||||
let addr: SocketAddr = args.bind.parse().unwrap_or_else(|_| panic!("Cannot bind to {}. Please provide a host and port like [::1]:8000", &args.bind));
|
||||
|
||||
println!("Server listening to {}", &args.bind);
|
||||
|
||||
HttpServer::new(
|
||||
|| {
|
||||
let state = models::AppState::new();
|
||||
move || {
|
||||
let config = read_config(&args.config)
|
||||
.unwrap_or_else(|e| panic!("Cannot read config file: {}", e.to_string()));
|
||||
let state = models::AppState::new(config);
|
||||
App::new().app_data(web::Data::new(state))
|
||||
.route("/", web::get().to(handlers::landing_page))
|
||||
.route("/account/register-init", web::post().to(handlers::start_webauthn_registration))
|
||||
|
|
|
@ -1,30 +1,47 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
use std::ops::DerefMut;
|
||||
use std::sync::Arc;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use anyhow::Result;
|
||||
use deadpool_postgres::{Manager, ManagerConfig, Pool, RecyclingMethod};
|
||||
use handlebars::Handlebars;
|
||||
use tokio::sync::RwLock;
|
||||
use tokio_postgres::NoTls;
|
||||
use webauthn_rs::prelude::{Uuid, PasskeyRegistration, Passkey};
|
||||
use webauthn_rs::{Webauthn, WebauthnBuilder};
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct Configuration {
|
||||
pub url: String,
|
||||
pub db_host: Option<String>,
|
||||
pub db_port: Option<u16>,
|
||||
pub db_name: String
|
||||
}
|
||||
|
||||
pub type DbField<T> = Arc<RwLock<T>>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Db {
|
||||
|
||||
pub user_keys: DbField<HashMap<Uuid, Passkey>>,
|
||||
pub user_uuid_object: DbField<HashMap<Uuid, User>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TempSession {
|
||||
pub user_registrations: Arc<RwLock<HashMap<User,PasskeyRegistration>>>
|
||||
pub user_registrations: Arc<RwLock<HashMap<Uuid,PendingRegistration>>>
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PendingRegistration {
|
||||
pub user: User,
|
||||
pub registration: PasskeyRegistration
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct User {
|
||||
pub uuid: Uuid,
|
||||
pub user_name: String,
|
||||
pub display_name: String,
|
||||
}
|
||||
|
@ -32,33 +49,79 @@ pub struct User {
|
|||
#[derive(Clone)]
|
||||
pub struct AppState<'a>{
|
||||
pub webauthn: Arc<Webauthn>,
|
||||
pub db: Db,
|
||||
pub db: Pool,
|
||||
pub hbs: Arc<Handlebars<'a>>,
|
||||
pub session: TempSession
|
||||
}
|
||||
|
||||
mod embedded {
|
||||
use refinery::embed_migrations;
|
||||
embed_migrations!("src/sql/01-init.sql");
|
||||
}
|
||||
|
||||
impl AppState<'_> {
|
||||
pub fn new() -> Self {
|
||||
pub fn new(conf: Configuration) -> Self {
|
||||
let rp = "localhost";
|
||||
let rp_origin = Url::parse("http://localhost:8000").expect("Invalid URL");
|
||||
let rp_origin = Url::parse(&conf.url).expect("Invalid URL");
|
||||
let builder = WebauthnBuilder::new(rp, &rp_origin).expect("Invalid configuration");
|
||||
let builder = builder.rp_name("LocalHost");
|
||||
let webauthn = Arc::new(builder.build().expect("Invalid configuration"));
|
||||
let db: Db = Db {
|
||||
user_keys: Arc::new(RwLock::new(HashMap::new())),
|
||||
user_uuid_object: Arc::new(RwLock::new(HashMap::new()))
|
||||
|
||||
};
|
||||
let hbs = Arc::new(crate::templates::new().unwrap());
|
||||
let session: TempSession = TempSession {
|
||||
user_registrations: Arc::new(RwLock::new(HashMap::new()))
|
||||
};
|
||||
let mut pg_config = tokio_postgres::Config::new();
|
||||
pg_config.host_path(conf.db_host.unwrap().clone());
|
||||
pg_config.dbname(&conf.db_name);
|
||||
pg_config.port(conf.db_port.unwrap());
|
||||
let mgr_config = ManagerConfig {
|
||||
recycling_method: RecyclingMethod::Fast
|
||||
};
|
||||
println!("{:?}", pg_config);
|
||||
let mgr = Manager::from_config(pg_config, NoTls, mgr_config);
|
||||
let pool = Pool::builder(mgr).max_size(16).build().unwrap();
|
||||
|
||||
AppState {
|
||||
webauthn,
|
||||
db,
|
||||
db: pool,
|
||||
hbs,
|
||||
session
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run_migrations(&mut self) -> Result<()> {
|
||||
let mut conn = self.db.get().await?;
|
||||
let client = conn.deref_mut().deref_mut();
|
||||
embedded::migrations::runner().run_async(client).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
pub async fn save_user(&self, user: &User) -> Result<()> {
|
||||
let mut conn = self.db.get().await?;
|
||||
let client = conn.deref_mut();
|
||||
let stmt = client.prepare_cached("INSERT INTO Users (user_name, display_name) VALUES ($1, $2, $3)").await?;
|
||||
client.query(&stmt, &[&user.user_name, &user.display_name]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn save_user_key(&self, uuid: &Uuid, passkey: &Passkey) -> Result<()> {
|
||||
let passkey_json: String = serde_json::to_string(&passkey)?;
|
||||
let mut conn = self.db.get().await?;
|
||||
let client = conn.deref_mut();
|
||||
let stmt = client.prepare_cached("INSERT INTO Keys (key_dump, user_id) VALUES ($1,$2)").await?;
|
||||
client.query(&stmt, &[&passkey_json, &uuid.to_string()]).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_user_keys(&self, user_uuid: &Uuid) -> Result<Vec<Result<Passkey, serde_json::Error>>> {
|
||||
let mut conn = self.db.get().await?;
|
||||
let client = conn.deref_mut();
|
||||
let stmt = client.prepare_cached("SELECT key_dump FROM Keys WHERE user_id = $1").await?;
|
||||
let res = client.query(&stmt, &[&user_uuid.to_string()]).await?;
|
||||
let res2 = res.iter().map(|row| {
|
||||
serde_json::from_str(row.get(0))
|
||||
}).collect();
|
||||
Ok(res2)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
CREATE TABLE Users (
|
||||
id uuid DEFAULT uuid_generate_v4 (),
|
||||
user_name text NOT NULL,
|
||||
display_name text NOT NULL,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE Keys (
|
||||
id integer PRIMARY KEY,
|
||||
key_dump jsonb NOT NULL,
|
||||
user_id uuid NOT NULL,
|
||||
CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES Users(id)
|
||||
);
|
||||
|
||||
-- We'll mostly querying the Keys using the associated uid.
|
||||
CREATE INDEX idx_keys_uid ON Keys USING HASH (user_id);
|
|
@ -0,0 +1,71 @@
|
|||
use std::{process::{Command, Child}, env::temp_dir, path::PathBuf, fs::remove_dir_all, fs::read_dir, time::Duration};
|
||||
|
||||
use nix_cache_bucket_gc::models::{self, Configuration, AppState};
|
||||
use tokio::time::sleep;
|
||||
use uuid::Uuid;
|
||||
use anyhow::Result;
|
||||
|
||||
|
||||
struct TestDB {
|
||||
path: PathBuf,
|
||||
pid: Child,
|
||||
db_name: String,
|
||||
port: u16
|
||||
}
|
||||
|
||||
async fn setup_db() -> Result<TestDB> {
|
||||
let mut dbdir = temp_dir();
|
||||
let dir_uuid = Uuid::new_v4();
|
||||
let db_name = "nom-nom-integration-test".to_string();
|
||||
let port: u16 = 12345;
|
||||
dbdir.push(dir_uuid.to_string());
|
||||
let dbdir_str = dbdir.to_str().unwrap();
|
||||
Command::new("initdb")
|
||||
.arg(&dbdir)
|
||||
.spawn()?
|
||||
.wait()?;
|
||||
let db_proc_handle = Command::new("postgres")
|
||||
.args(["-D", dbdir_str, "-c", &format!("unix_socket_directories={}", dbdir_str), "-c", "listen_addresses=", "-c", &format!("port={}", &port.to_string())])
|
||||
.spawn()?;
|
||||
let test = sleep(Duration::from_secs(1)).await;
|
||||
let cmd = Command::new("createdb")
|
||||
.args([ "-h", dbdir_str, "-p", &port.to_string(), &db_name])
|
||||
.spawn()?
|
||||
.wait()?;
|
||||
println!("{:?}", cmd);
|
||||
Ok(TestDB{ path: dbdir, pid: db_proc_handle, db_name, port })
|
||||
}
|
||||
|
||||
fn teardown_db(mut db: TestDB) -> Result <()> {
|
||||
println!("Stopping postgres");
|
||||
db.pid.kill()?;
|
||||
remove_dir_all(db.path)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_migratios() {
|
||||
let mdb = setup_db().await;
|
||||
let db = mdb.expect("setup db");
|
||||
let mut dbpath = db.path.to_str().unwrap().to_string();
|
||||
dbpath.push('/');
|
||||
let conf = Configuration {
|
||||
url: "http://localhost:9000".to_string(),
|
||||
db_port: Some(db.port),
|
||||
db_host: Some(dbpath),
|
||||
db_name: db.db_name.clone()
|
||||
};
|
||||
|
||||
println!("{:?}", conf);
|
||||
println!("Listing dir:");
|
||||
let postgres_content = read_dir(&db.path).unwrap();
|
||||
for file in postgres_content {
|
||||
println!("{}", file.unwrap().path().display());
|
||||
}
|
||||
let mut state = AppState::new(conf);
|
||||
let res = state.run_migrations().await;
|
||||
|
||||
teardown_db(db).expect("Failed to teardown DB.");
|
||||
|
||||
res.expect("migrations should not fail");
|
||||
}
|
Loading…
Reference in New Issue