From d3889bd5945b2ffc63d20942b7730b5a1d0e3a42 Mon Sep 17 00:00:00 2001 From: Yigit Sever Date: Sun, 11 Apr 2021 21:39:18 +0300 Subject: Implement User handling and authentication New struct: User, corresponds to a student Blocks and users are persistent (written to a text file) PostgreSQL would've been overkill, we have 30 students AuthRequest is the representation for incoming register requests and User is the inner representation Students who are enrolled to the class are hardcoded, only they can register new accounts There are two new tests, one checks if a priviliged (=enrolled) user can create an account and the other checks if a unpriviliged one cannot There are quick verbose error messages that I'm not married to, might move on to something better honestly There's nothing stopping a malicious user to pre-register everyone with mock public keys and effectively lock everyone out, what's a good secret we can use? --- src/custom_filters.rs | 12 +++++-- src/handlers.rs | 83 +++++++++++++++++++++++++++++++++++++------- src/main.rs | 2 +- src/routes.rs | 80 +++++++++++++++++++++++++++++++++++++++---- src/schema.rs | 95 ++++++++++++++++++++++++++++++++++++++++++--------- 5 files changed, 233 insertions(+), 39 deletions(-) (limited to 'src') diff --git a/src/custom_filters.rs b/src/custom_filters.rs index 8c36d02..0806c6d 100644 --- a/src/custom_filters.rs +++ b/src/custom_filters.rs @@ -3,19 +3,27 @@ use std::convert::Infallible; use warp::{Filter, Rejection}; -use crate::schema::{Block, Db, Transaction}; +use crate::schema::{Block, Db, Transaction, AuthRequest}; // Database context for routes pub fn with_db(db: Db) -> impl Filter + Clone { warp::any().map(move || db.clone()) } +// Accept only json encoded User body and reject big payloads +// TODO: find a good limit for this, (=e2482057; 8 char String + rsa pem) <11-04-21, yigit> // +pub fn auth_request_json_body() -> impl Filter + Clone { + warp::body::content_length_limit(1024 * 32).and(warp::body::json()) +} + // Accept only json encoded Transaction body and reject big payloads +// TODO: find a good limit for this <11-04-21, yigit> // pub fn transaction_json_body() -> impl Filter + Clone { warp::body::content_length_limit(1024 * 32).and(warp::body::json()) } -// Accept only json encoded Transaction body and reject big payloads +// Accept only json encoded Block body and reject big payloads +// TODO: find a good limit for this <11-04-21, yigit> // pub fn block_json_body() -> impl Filter + Clone { warp::body::content_length_limit(1024 * 32).and(warp::body::json()) } diff --git a/src/handlers.rs b/src/handlers.rs index 856970d..bfd57bc 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -1,10 +1,62 @@ /// API handlers, the ends of each filter chain -use log::debug; // this is more useful than debug! learn how to use this +use log::debug; use parking_lot::RwLockUpgradableReadGuard; +use serde_json; use std::convert::Infallible; -use warp::{http::StatusCode, reply}; +use warp::{http::Response, http::StatusCode, reply}; -use crate::schema::{Block, Db, Transaction}; +use std::fs; + +use crate::schema::{AuthRequest, Block, Db, MetuId, Transaction, User}; + +/// POST /register +/// Enables a student to introduce themselves to the system +/// Can fail +pub async fn authenticate_user( + request: AuthRequest, + db: Db, +) -> Result { + let given_id = request.student_id.clone(); + + if let Some(priv_student_id) = MetuId::new(request.student_id) { + let userlist = db.users.upgradable_read(); + + if userlist.contains_key(&given_id) { + + let res = Response::builder() + .status(StatusCode::BAD_REQUEST) + .body("This user is already authenticated"); + + Ok(res) + } else { + let new_user = User { + user_id: priv_student_id, + public_key: request.public_key, + balance: 0, + }; + + let user_json = serde_json::to_string(&new_user).unwrap(); + + fs::write(format!("users/{}.guy", new_user.user_id), user_json).unwrap(); + + let mut userlist = RwLockUpgradableReadGuard::upgrade(userlist); + userlist.insert(given_id, new_user); + // TODO: signature of the public key, please <11-04-21, yigit> // + + let res = Response::builder() + .status(StatusCode::CREATED) + .body("Ready to use Gradecoin"); + + Ok(res) + } + } else { + let res = Response::builder() + .status(StatusCode::BAD_REQUEST) + .body("This user cannot have a gradecoin account"); + + Ok(res) + } +} /// GET /transaction /// Returns JSON array of transactions @@ -28,16 +80,11 @@ pub async fn list_transactions(db: Db) -> Result { /// Cannot fail /// Mostly around for debug purposes pub async fn list_blocks(db: Db) -> Result { - debug!("list all blocks"); - - let mut result = Vec::new(); - let blocks = db.blockchain.read(); + debug!("list all block"); - for block in blocks.iter() { - result.push(block); - } + let block = db.blockchain.read(); - Ok(reply::with_status(reply::json(&result), StatusCode::OK)) + Ok(reply::with_status(reply::json(&*block), StatusCode::OK)) } /// POST /transaction @@ -70,7 +117,7 @@ pub async fn propose_block(new_block: Block, db: Db) -> Result // + // check 1, new_block.transaction_list from pending_transactions pool? <07-04-21, yigit> // for transaction_hash in new_block.transaction_list.iter() { if !pending_transactions.contains_key(transaction_hash) { return Ok(StatusCode::BAD_REQUEST); @@ -81,7 +128,17 @@ pub async fn propose_block(new_block: Block, db: Db) -> Result impl Filter + Clone { transaction_list(db.clone()) + .or(register_user(db.clone())) .or(transaction_propose(db.clone())) .or(block_propose(db.clone())) .or(block_list(db.clone())) } +/// POST /register warp route +pub fn register_user(db: Db) -> impl Filter + Clone { + warp::path!("register") + .and(warp::post()) + .and(custom_filters::auth_request_json_body()) + .and(custom_filters::with_db(db)) + .and_then(handlers::authenticate_user) +} + /// GET /transaction warp route pub fn transaction_list(db: Db) -> impl Filter + Clone { warp::path!("transaction") @@ -50,13 +60,13 @@ pub fn block_propose(db: Db) -> impl Filter Db { @@ -72,7 +82,7 @@ mod tests { }, ); - db.blockchain.write().push(Block { + *db.blockchain.write() = Block { transaction_list: vec![ "old_transaction_hash_1".to_owned(), "old_transaction_hash_2".to_owned(), @@ -81,11 +91,27 @@ mod tests { nonce: "not_a_thing_yet".to_owned(), timestamp: chrono::NaiveDate::from_ymd(2021, 04, 08).and_hms(12, 30, 30), hash: "not_a_thing_yet".to_owned(), - }); + }; db } + /// Create a mock user that is allowed to be in gradecoin to be used in tests + fn priviliged_mocked_user() -> AuthRequest { + AuthRequest { + student_id: String::from("e254275"), + public_key: "NOT IMPLEMENTED".to_owned(), + } + } + + /// Create a mock user that is NOT allowed to be in gradecoin to be used in tests + fn unpriviliged_mocked_user() -> AuthRequest { + AuthRequest { + student_id: String::from("foobarbaz"), + public_key: "NOT IMPLEMENTED".to_owned(), + } + } + /// Create a mock transaction to be used in tests fn mocked_transaction() -> Transaction { Transaction { @@ -134,7 +160,7 @@ mod tests { assert_eq!(res.status(), StatusCode::OK); - let expected_json_body = r#"[{"transaction_list":["old_transaction_hash_1","old_transaction_hash_2","old_transaction_hash_3"],"nonce":"not_a_thing_yet","timestamp":"2021-04-08T12:30:30","hash":"not_a_thing_yet"}]"#; + let expected_json_body = r#"{"transaction_list":["old_transaction_hash_1","old_transaction_hash_2","old_transaction_hash_3"],"nonce":"not_a_thing_yet","timestamp":"2021-04-08T12:30:30","hash":"not_a_thing_yet"}"#; assert_eq!(res.body(), expected_json_body); } @@ -175,6 +201,46 @@ mod tests { assert_eq!(db.pending_transactions.read().len(), 2); } + /// TEST a POST request to /transaction, an endpoint that exists + /// https://tools.ietf.org/html/rfc7231#section-6.3.2 + /// Should accept the json request, create a new user and + /// add it to the user hashmap in the db + #[tokio::test] + async fn post_register_priviliged_user() { + let db = mocked_db(); + let filter = consensus_routes(db.clone()); + + let res = warp::test::request() + .method("POST") + .json(&priviliged_mocked_user()) + .path("/register") + .reply(&filter) + .await; + + println!("{:?}", res.body()); + assert_eq!(res.status(), StatusCode::CREATED); + assert_eq!(db.users.read().len(), 1); + } + /// TEST a POST request to /transaction, an endpoint that exists + /// https://tools.ietf.org/html/rfc7231#section-6.3.2 + /// Should NOT accept the json request + #[tokio::test] + async fn post_register_unpriviliged_user() { + let db = mocked_db(); + let filter = consensus_routes(db.clone()); + + let res = warp::test::request() + .method("POST") + .json(&unpriviliged_mocked_user()) + .path("/register") + .reply(&filter) + .await; + + println!("{:?}", res.body()); + assert_eq!(res.status(), StatusCode::BAD_REQUEST); + assert_eq!(db.users.read().len(), 0); + } + /// Test a POST request to /transaction, a resource that exists with a longer than expected /// payload /// https://tools.ietf.org/html/rfc7231#section-6.5.11 diff --git a/src/schema.rs b/src/schema.rs index 556e625..909b5cd 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -1,41 +1,42 @@ -use chrono::NaiveDateTime; +use chrono::{NaiveDate, NaiveDateTime}; +use lazy_static::lazy_static; use parking_lot::RwLock; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; +use std::fmt; +use std::fs; use std::sync::Arc; // use crate::validators; -// In memory data structure -// Two approaches here -// 1. Db is a type pub type Db = Arc>>; Ledger is a struct, we wrap the ledger -// with arc + mutex in ledger() to access transactions we need to unwrap blocks as well, vice -// versa -// -// 2. Db is a struct attributes are wrapped we can offload ::new() to it's member method blocks and -// transactions are accessible separately, which is the biggest pro -// -// 3. use an actual database (for blockchain and users this makes the most sense tbh but pending -// transactions are perfectly fine in memory) +/// We need persistence for blocks and users, not so much for transactions +/// There are around 30 students, a full fledged database would be an overkill (for next year?) +/// Pending transactions are held in memory, these are cleared with every new block +/// Only the last block is held in memory, every block is written to a file +/// Users are held in memory and they're also backed up to text files -/// Creates a new database +/// Creates a new database connection pub fn create_database() -> Db { + fs::create_dir_all("blocks").unwrap(); + fs::create_dir_all("users").unwrap(); Db::new() } #[derive(Debug, Clone)] pub struct Db { // heh. also https://doc.rust-lang.org/std/collections/struct.LinkedList.html says Vec is generally faster - pub blockchain: Arc>>, + pub blockchain: Arc>, // every proposer can have _one_ pending transaction, a way to enforce this, String is proposer identifier pub pending_transactions: Arc>>, + pub users: Arc>>, } impl Db { fn new() -> Self { Db { - blockchain: Arc::new(RwLock::new(Vec::new())), + blockchain: Arc::new(RwLock::new(Block::new())), pending_transactions: Arc::new(RwLock::new(HashMap::new())), + users: Arc::new(RwLock::new(HashMap::new())), } } } @@ -43,6 +44,7 @@ impl Db { /// A transaction between `source` and `target` that moves `amount` #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct Transaction { + // TODO: new field by <11-04-21, yigit> // pub source: String, pub target: String, pub amount: i32, @@ -65,5 +67,66 @@ pub struct Block { pub hash: String, // future proof'd baby } +impl Block { + /// Genesis block + pub fn new() -> Block { + Block { + transaction_list: vec![], + nonce: String::from(""), + timestamp: NaiveDate::from_ymd(2021, 04, 11).and_hms(20, 45, 00), + hash: String::from(""), + } + } +} + +/// Or simply a Student +#[derive(Serialize, Deserialize, Debug)] +pub struct User { + pub user_id: MetuId, + pub public_key: String, + pub balance: i32, +} + +/// The values will be hard coded so MetuId::new() can accept/reject values based on that +#[derive(Serialize, Deserialize, Debug)] +pub struct MetuId { + id: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct AuthRequest { + pub student_id: String, + pub public_key: String, +} + +lazy_static! { + static ref OUR_STUDENTS: HashSet<&'static str> = { + [ + "e254275", "e223687", "e211024", "e209888", "e223725", "e209362", "e209898", "e230995", + "e223743", "e223747", "e223749", "e223751", "e188126", "e209913", "e203608", "e233013", + "e216982", "e217185", "e223780", "e194931", "e223783", "e254550", "e217203", "e217477", + "e223786", "e231060", "e223795", + ] + .iter() + .cloned() + .collect() + }; +} + +impl fmt::Display for MetuId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.id) + } +} + +impl MetuId { + pub fn new(id: String) -> Option { + if OUR_STUDENTS.contains(&*id) { + Some(MetuId { id: id }) + } else { + None + } + } +} // TODO: write schema tests using the original repo <09-04-21, yigit> // -- cgit v1.2.3-70-g09d2