aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorYigit Sever2021-04-11 21:39:18 +0300
committerYigit Sever2021-04-12 00:03:23 +0300
commitd3889bd5945b2ffc63d20942b7730b5a1d0e3a42 (patch)
treea8b2dfcd416f308e7fae4baea95d6107a9871c43
parent11b498dc44a7d2ed8f1acc62d64be7f114adc336 (diff)
downloadgradecoin-d3889bd5945b2ffc63d20942b7730b5a1d0e3a42.tar.gz
gradecoin-d3889bd5945b2ffc63d20942b7730b5a1d0e3a42.tar.bz2
gradecoin-d3889bd5945b2ffc63d20942b7730b5a1d0e3a42.zip
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?
-rw-r--r--Cargo.lock1
-rw-r--r--Cargo.toml5
-rw-r--r--src/custom_filters.rs12
-rw-r--r--src/handlers.rs83
-rw-r--r--src/main.rs2
-rw-r--r--src/routes.rs80
-rw-r--r--src/schema.rs95
7 files changed, 237 insertions, 41 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 45ae0a0..d4d5926 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -281,6 +281,7 @@ name = "gradecoin"
281version = "0.1.0" 281version = "0.1.0"
282dependencies = [ 282dependencies = [
283 "chrono", 283 "chrono",
284 "lazy_static",
284 "log", 285 "log",
285 "parking_lot", 286 "parking_lot",
286 "pretty_env_logger", 287 "pretty_env_logger",
diff --git a/Cargo.toml b/Cargo.toml
index a203a6f..2ae2b25 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -14,7 +14,8 @@ chrono = { version = "0.4.10", features = ["serde"] }
14log = "0.4.8" 14log = "0.4.8"
15pretty_env_logger = "0.3.1" 15pretty_env_logger = "0.3.1"
16parking_lot = "0.10.0" 16parking_lot = "0.10.0"
17serde_json = "1.0.59"
18lazy_static = "1.4.0"
17 19
18[dev-dependencies] 20[dev-dependencies]
19serde_json = "1.0.44" 21serde_test = "1.0.117"
20serde_test = "1.0.104"
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 @@
3use std::convert::Infallible; 3use std::convert::Infallible;
4use warp::{Filter, Rejection}; 4use warp::{Filter, Rejection};
5 5
6use crate::schema::{Block, Db, Transaction}; 6use crate::schema::{Block, Db, Transaction, AuthRequest};
7 7
8// Database context for routes 8// Database context for routes
9pub fn with_db(db: Db) -> impl Filter<Extract = (Db,), Error = Infallible> + Clone { 9pub fn with_db(db: Db) -> impl Filter<Extract = (Db,), Error = Infallible> + Clone {
10 warp::any().map(move || db.clone()) 10 warp::any().map(move || db.clone())
11} 11}
12 12
13// Accept only json encoded User body and reject big payloads
14// TODO: find a good limit for this, (=e2482057; 8 char String + rsa pem) <11-04-21, yigit> //
15pub fn auth_request_json_body() -> impl Filter<Extract = (AuthRequest,), Error = Rejection> + Clone {
16 warp::body::content_length_limit(1024 * 32).and(warp::body::json())
17}
18
13// Accept only json encoded Transaction body and reject big payloads 19// Accept only json encoded Transaction body and reject big payloads
20// TODO: find a good limit for this <11-04-21, yigit> //
14pub fn transaction_json_body() -> impl Filter<Extract = (Transaction,), Error = Rejection> + Clone { 21pub fn transaction_json_body() -> impl Filter<Extract = (Transaction,), Error = Rejection> + Clone {
15 warp::body::content_length_limit(1024 * 32).and(warp::body::json()) 22 warp::body::content_length_limit(1024 * 32).and(warp::body::json())
16} 23}
17 24
18// Accept only json encoded Transaction body and reject big payloads 25// Accept only json encoded Block body and reject big payloads
26// TODO: find a good limit for this <11-04-21, yigit> //
19pub fn block_json_body() -> impl Filter<Extract = (Block,), Error = Rejection> + Clone { 27pub fn block_json_body() -> impl Filter<Extract = (Block,), Error = Rejection> + Clone {
20 warp::body::content_length_limit(1024 * 32).and(warp::body::json()) 28 warp::body::content_length_limit(1024 * 32).and(warp::body::json())
21} 29}
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 @@
1/// API handlers, the ends of each filter chain 1/// API handlers, the ends of each filter chain
2use log::debug; // this is more useful than debug! learn how to use this 2use log::debug;
3use parking_lot::RwLockUpgradableReadGuard; 3use parking_lot::RwLockUpgradableReadGuard;
4use serde_json;
4use std::convert::Infallible; 5use std::convert::Infallible;
5use warp::{http::StatusCode, reply}; 6use warp::{http::Response, http::StatusCode, reply};
6 7
7use crate::schema::{Block, Db, Transaction}; 8use std::fs;
9
10use crate::schema::{AuthRequest, Block, Db, MetuId, Transaction, User};
11
12/// POST /register
13/// Enables a student to introduce themselves to the system
14/// Can fail
15pub async fn authenticate_user(
16 request: AuthRequest,
17 db: Db,
18) -> Result<impl warp::Reply, warp::Rejection> {
19 let given_id = request.student_id.clone();
20
21 if let Some(priv_student_id) = MetuId::new(request.student_id) {
22 let userlist = db.users.upgradable_read();
23
24 if userlist.contains_key(&given_id) {
25
26 let res = Response::builder()
27 .status(StatusCode::BAD_REQUEST)
28 .body("This user is already authenticated");
29
30 Ok(res)
31 } else {
32 let new_user = User {
33 user_id: priv_student_id,
34 public_key: request.public_key,
35 balance: 0,
36 };
37
38 let user_json = serde_json::to_string(&new_user).unwrap();
39
40 fs::write(format!("users/{}.guy", new_user.user_id), user_json).unwrap();
41
42 let mut userlist = RwLockUpgradableReadGuard::upgrade(userlist);
43 userlist.insert(given_id, new_user);
44 // TODO: signature of the public key, please <11-04-21, yigit> //
45
46 let res = Response::builder()
47 .status(StatusCode::CREATED)
48 .body("Ready to use Gradecoin");
49
50 Ok(res)
51 }
52 } else {
53 let res = Response::builder()
54 .status(StatusCode::BAD_REQUEST)
55 .body("This user cannot have a gradecoin account");
56
57 Ok(res)
58 }
59}
8 60
9/// GET /transaction 61/// GET /transaction
10/// Returns JSON array of transactions 62/// Returns JSON array of transactions
@@ -28,16 +80,11 @@ pub async fn list_transactions(db: Db) -> Result<impl warp::Reply, Infallible> {
28/// Cannot fail 80/// Cannot fail
29/// Mostly around for debug purposes 81/// Mostly around for debug purposes
30pub async fn list_blocks(db: Db) -> Result<impl warp::Reply, Infallible> { 82pub async fn list_blocks(db: Db) -> Result<impl warp::Reply, Infallible> {
31 debug!("list all blocks"); 83 debug!("list all block");
32
33 let mut result = Vec::new();
34 let blocks = db.blockchain.read();
35 84
36 for block in blocks.iter() { 85 let block = db.blockchain.read();
37 result.push(block);
38 }
39 86
40 Ok(reply::with_status(reply::json(&result), StatusCode::OK)) 87 Ok(reply::with_status(reply::json(&*block), StatusCode::OK))
41} 88}
42 89
43/// POST /transaction 90/// POST /transaction
@@ -70,7 +117,7 @@ pub async fn propose_block(new_block: Block, db: Db) -> Result<impl warp::Reply,
70 let pending_transactions = db.pending_transactions.upgradable_read(); 117 let pending_transactions = db.pending_transactions.upgradable_read();
71 let blockchain = db.blockchain.upgradable_read(); 118 let blockchain = db.blockchain.upgradable_read();
72 119
73 // TODO: check 1, new_block.transaction_list from pending_transactions pool? <07-04-21, yigit> // 120 // check 1, new_block.transaction_list from pending_transactions pool? <07-04-21, yigit> //
74 for transaction_hash in new_block.transaction_list.iter() { 121 for transaction_hash in new_block.transaction_list.iter() {
75 if !pending_transactions.contains_key(transaction_hash) { 122 if !pending_transactions.contains_key(transaction_hash) {
76 return Ok(StatusCode::BAD_REQUEST); 123 return Ok(StatusCode::BAD_REQUEST);
@@ -81,7 +128,17 @@ pub async fn propose_block(new_block: Block, db: Db) -> Result<impl warp::Reply,
81 // assume it is for now 128 // assume it is for now
82 129
83 let mut blockchain = RwLockUpgradableReadGuard::upgrade(blockchain); 130 let mut blockchain = RwLockUpgradableReadGuard::upgrade(blockchain);
84 blockchain.push(new_block); 131
132 let block_json = serde_json::to_string(&new_block).unwrap();
133
134 // let mut file = File::create(format!("{}.block", new_block.timestamp.timestamp())).unwrap();
135 fs::write(
136 format!("blocks/{}.block", new_block.timestamp.timestamp()),
137 block_json,
138 )
139 .unwrap();
140
141 *blockchain = new_block;
85 142
86 let mut pending_transactions = RwLockUpgradableReadGuard::upgrade(pending_transactions); 143 let mut pending_transactions = RwLockUpgradableReadGuard::upgrade(pending_transactions);
87 pending_transactions.clear(); 144 pending_transactions.clear();
diff --git a/src/main.rs b/src/main.rs
index 7ef2597..373223c 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,8 +1,8 @@
1use std::env; 1use std::env;
2use warp::Filter; 2use warp::Filter;
3 3
4mod custom_filters;
5mod handlers; 4mod handlers;
5mod custom_filters;
6mod routes; 6mod routes;
7mod schema; 7mod schema;
8// mod validators; 8// mod validators;
diff --git a/src/routes.rs b/src/routes.rs
index 95138e6..9f0adc5 100644
--- a/src/routes.rs
+++ b/src/routes.rs
@@ -7,11 +7,21 @@ use crate::schema::Db;
7/// Root, all routes combined 7/// Root, all routes combined
8pub fn consensus_routes(db: Db) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone { 8pub fn consensus_routes(db: Db) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
9 transaction_list(db.clone()) 9 transaction_list(db.clone())
10 .or(register_user(db.clone()))
10 .or(transaction_propose(db.clone())) 11 .or(transaction_propose(db.clone()))
11 .or(block_propose(db.clone())) 12 .or(block_propose(db.clone()))
12 .or(block_list(db.clone())) 13 .or(block_list(db.clone()))
13} 14}
14 15
16/// POST /register warp route
17pub fn register_user(db: Db) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
18 warp::path!("register")
19 .and(warp::post())
20 .and(custom_filters::auth_request_json_body())
21 .and(custom_filters::with_db(db))
22 .and_then(handlers::authenticate_user)
23}
24
15/// GET /transaction warp route 25/// GET /transaction warp route
16pub fn transaction_list(db: Db) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone { 26pub fn transaction_list(db: Db) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
17 warp::path!("transaction") 27 warp::path!("transaction")
@@ -50,13 +60,13 @@ pub fn block_propose(db: Db) -> impl Filter<Extract = impl Reply, Error = Reject
50mod tests { 60mod tests {
51 use super::*; 61 use super::*;
52 62
53 use chrono::prelude::*; 63 // use chrono::prelude::*;
54 use parking_lot::RwLock; 64 // use parking_lot::RwLock;
55 use std::sync::Arc; 65 // use std::sync::Arc;
56 use warp::http::StatusCode; 66 use warp::http::StatusCode;
57 67
58 use crate::schema; 68 use crate::schema;
59 use crate::schema::{Block, Transaction}; 69 use crate::schema::{AuthRequest, Block, Transaction};
60 70
61 /// Create a mock database to be used in tests 71 /// Create a mock database to be used in tests
62 fn mocked_db() -> Db { 72 fn mocked_db() -> Db {
@@ -72,7 +82,7 @@ mod tests {
72 }, 82 },
73 ); 83 );
74 84
75 db.blockchain.write().push(Block { 85 *db.blockchain.write() = Block {
76 transaction_list: vec![ 86 transaction_list: vec![
77 "old_transaction_hash_1".to_owned(), 87 "old_transaction_hash_1".to_owned(),
78 "old_transaction_hash_2".to_owned(), 88 "old_transaction_hash_2".to_owned(),
@@ -81,11 +91,27 @@ mod tests {
81 nonce: "not_a_thing_yet".to_owned(), 91 nonce: "not_a_thing_yet".to_owned(),
82 timestamp: chrono::NaiveDate::from_ymd(2021, 04, 08).and_hms(12, 30, 30), 92 timestamp: chrono::NaiveDate::from_ymd(2021, 04, 08).and_hms(12, 30, 30),
83 hash: "not_a_thing_yet".to_owned(), 93 hash: "not_a_thing_yet".to_owned(),
84 }); 94 };
85 95
86 db 96 db
87 } 97 }
88 98
99 /// Create a mock user that is allowed to be in gradecoin to be used in tests
100 fn priviliged_mocked_user() -> AuthRequest {
101 AuthRequest {
102 student_id: String::from("e254275"),
103 public_key: "NOT IMPLEMENTED".to_owned(),
104 }
105 }
106
107 /// Create a mock user that is NOT allowed to be in gradecoin to be used in tests
108 fn unpriviliged_mocked_user() -> AuthRequest {
109 AuthRequest {
110 student_id: String::from("foobarbaz"),
111 public_key: "NOT IMPLEMENTED".to_owned(),
112 }
113 }
114
89 /// Create a mock transaction to be used in tests 115 /// Create a mock transaction to be used in tests
90 fn mocked_transaction() -> Transaction { 116 fn mocked_transaction() -> Transaction {
91 Transaction { 117 Transaction {
@@ -134,7 +160,7 @@ mod tests {
134 160
135 assert_eq!(res.status(), StatusCode::OK); 161 assert_eq!(res.status(), StatusCode::OK);
136 162
137 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"}]"#; 163 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"}"#;
138 assert_eq!(res.body(), expected_json_body); 164 assert_eq!(res.body(), expected_json_body);
139 } 165 }
140 166
@@ -175,6 +201,46 @@ mod tests {
175 assert_eq!(db.pending_transactions.read().len(), 2); 201 assert_eq!(db.pending_transactions.read().len(), 2);
176 } 202 }
177 203
204 /// TEST a POST request to /transaction, an endpoint that exists
205 /// https://tools.ietf.org/html/rfc7231#section-6.3.2
206 /// Should accept the json request, create a new user and
207 /// add it to the user hashmap in the db
208 #[tokio::test]
209 async fn post_register_priviliged_user() {
210 let db = mocked_db();
211 let filter = consensus_routes(db.clone());
212
213 let res = warp::test::request()
214 .method("POST")
215 .json(&priviliged_mocked_user())
216 .path("/register")
217 .reply(&filter)
218 .await;
219
220 println!("{:?}", res.body());
221 assert_eq!(res.status(), StatusCode::CREATED);
222 assert_eq!(db.users.read().len(), 1);
223 }
224 /// TEST a POST request to /transaction, an endpoint that exists
225 /// https://tools.ietf.org/html/rfc7231#section-6.3.2
226 /// Should NOT accept the json request
227 #[tokio::test]
228 async fn post_register_unpriviliged_user() {
229 let db = mocked_db();
230 let filter = consensus_routes(db.clone());
231
232 let res = warp::test::request()
233 .method("POST")
234 .json(&unpriviliged_mocked_user())
235 .path("/register")
236 .reply(&filter)
237 .await;
238
239 println!("{:?}", res.body());
240 assert_eq!(res.status(), StatusCode::BAD_REQUEST);
241 assert_eq!(db.users.read().len(), 0);
242 }
243
178 /// Test a POST request to /transaction, a resource that exists with a longer than expected 244 /// Test a POST request to /transaction, a resource that exists with a longer than expected
179 /// payload 245 /// payload
180 /// https://tools.ietf.org/html/rfc7231#section-6.5.11 246 /// 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 @@
1use chrono::NaiveDateTime; 1use chrono::{NaiveDate, NaiveDateTime};
2use lazy_static::lazy_static;
2use parking_lot::RwLock; 3use parking_lot::RwLock;
3use serde::{Deserialize, Serialize}; 4use serde::{Deserialize, Serialize};
4use std::collections::HashMap; 5use std::collections::{HashMap, HashSet};
6use std::fmt;
7use std::fs;
5use std::sync::Arc; 8use std::sync::Arc;
6 9
7// use crate::validators; 10// use crate::validators;
8 11
9// In memory data structure 12/// We need persistence for blocks and users, not so much for transactions
10// Two approaches here 13/// There are around 30 students, a full fledged database would be an overkill (for next year?)
11// 1. Db is a type pub type Db = Arc<RwLock<Vec<Ledger>>>; Ledger is a struct, we wrap the ledger 14/// Pending transactions are held in memory, these are cleared with every new block
12// with arc + mutex in ledger() to access transactions we need to unwrap blocks as well, vice 15/// Only the last block is held in memory, every block is written to a file
13// versa 16/// Users are held in memory and they're also backed up to text files
14//
15// 2. Db is a struct attributes are wrapped we can offload ::new() to it's member method blocks and
16// transactions are accessible separately, which is the biggest pro
17//
18// 3. use an actual database (for blockchain and users this makes the most sense tbh but pending
19// transactions are perfectly fine in memory)
20 17
21/// Creates a new database 18/// Creates a new database connection
22pub fn create_database() -> Db { 19pub fn create_database() -> Db {
20 fs::create_dir_all("blocks").unwrap();
21 fs::create_dir_all("users").unwrap();
23 Db::new() 22 Db::new()
24} 23}
25 24
26#[derive(Debug, Clone)] 25#[derive(Debug, Clone)]
27pub struct Db { 26pub struct Db {
28 // heh. also https://doc.rust-lang.org/std/collections/struct.LinkedList.html says Vec is generally faster 27 // heh. also https://doc.rust-lang.org/std/collections/struct.LinkedList.html says Vec is generally faster
29 pub blockchain: Arc<RwLock<Vec<Block>>>, 28 pub blockchain: Arc<RwLock<Block>>,
30 // every proposer can have _one_ pending transaction, a way to enforce this, String is proposer identifier 29 // every proposer can have _one_ pending transaction, a way to enforce this, String is proposer identifier
31 pub pending_transactions: Arc<RwLock<HashMap<String, Transaction>>>, 30 pub pending_transactions: Arc<RwLock<HashMap<String, Transaction>>>,
31 pub users: Arc<RwLock<HashMap<String, User>>>,
32} 32}
33 33
34impl Db { 34impl Db {
35 fn new() -> Self { 35 fn new() -> Self {
36 Db { 36 Db {
37 blockchain: Arc::new(RwLock::new(Vec::new())), 37 blockchain: Arc::new(RwLock::new(Block::new())),
38 pending_transactions: Arc::new(RwLock::new(HashMap::new())), 38 pending_transactions: Arc::new(RwLock::new(HashMap::new())),
39 users: Arc::new(RwLock::new(HashMap::new())),
39 } 40 }
40 } 41 }
41} 42}
@@ -43,6 +44,7 @@ impl Db {
43/// A transaction between `source` and `target` that moves `amount` 44/// A transaction between `source` and `target` that moves `amount`
44#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] 45#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
45pub struct Transaction { 46pub struct Transaction {
47 // TODO: new field by <11-04-21, yigit> //
46 pub source: String, 48 pub source: String,
47 pub target: String, 49 pub target: String,
48 pub amount: i32, 50 pub amount: i32,
@@ -65,5 +67,66 @@ pub struct Block {
65 pub hash: String, // future proof'd baby 67 pub hash: String, // future proof'd baby
66} 68}
67 69
70impl Block {
71 /// Genesis block
72 pub fn new() -> Block {
73 Block {
74 transaction_list: vec![],
75 nonce: String::from(""),
76 timestamp: NaiveDate::from_ymd(2021, 04, 11).and_hms(20, 45, 00),
77 hash: String::from(""),
78 }
79 }
80}
81
82/// Or simply a Student
83#[derive(Serialize, Deserialize, Debug)]
84pub struct User {
85 pub user_id: MetuId,
86 pub public_key: String,
87 pub balance: i32,
88}
89
90/// The values will be hard coded so MetuId::new() can accept/reject values based on that
91#[derive(Serialize, Deserialize, Debug)]
92pub struct MetuId {
93 id: String,
94}
95
96#[derive(Serialize, Deserialize, Debug)]
97pub struct AuthRequest {
98 pub student_id: String,
99 pub public_key: String,
100}
101
102lazy_static! {
103 static ref OUR_STUDENTS: HashSet<&'static str> = {
104 [
105 "e254275", "e223687", "e211024", "e209888", "e223725", "e209362", "e209898", "e230995",
106 "e223743", "e223747", "e223749", "e223751", "e188126", "e209913", "e203608", "e233013",
107 "e216982", "e217185", "e223780", "e194931", "e223783", "e254550", "e217203", "e217477",
108 "e223786", "e231060", "e223795",
109 ]
110 .iter()
111 .cloned()
112 .collect()
113 };
114}
115
116impl fmt::Display for MetuId {
117 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118 write!(f, "{}", self.id)
119 }
120}
121
122impl MetuId {
123 pub fn new(id: String) -> Option<Self> {
124 if OUR_STUDENTS.contains(&*id) {
125 Some(MetuId { id: id })
126 } else {
127 None
128 }
129 }
130}
68 131
69// TODO: write schema tests using the original repo <09-04-21, yigit> // 132// TODO: write schema tests using the original repo <09-04-21, yigit> //