From 51d71ce13330ad3058a0a64601cfb2a7cd65e3d2 Mon Sep 17 00:00:00 2001 From: Denys Konovalov Date: Thu, 29 Jul 2021 20:03:10 +0200 Subject: [PATCH] - extended API Endpoints - restructured API paths - added JWT authorization - added login function (without authentication) --- Cargo.toml | 5 +- Rocket.toml | 7 + src/main.rs | 124 +++++++++++++++++- static/index.html | 5 + tutor | 1 - xml/Cargo.toml | 8 -- xml/src/main.rs | 39 ------ xml/target/.rustc_info.json | 1 - xml/target/CACHEDIR.TAG | 3 - xml/target/debug/.cargo-lock | 0 .../xml-af563bb74dd611b8/invoked.timestamp | 1 - .../xml-af563bb74dd611b8/output-bin-xml | 2 - 12 files changed, 134 insertions(+), 62 deletions(-) create mode 100644 static/index.html delete mode 100644 tutor delete mode 100644 xml/Cargo.toml delete mode 100644 xml/src/main.rs delete mode 100644 xml/target/.rustc_info.json delete mode 100644 xml/target/CACHEDIR.TAG delete mode 100644 xml/target/debug/.cargo-lock delete mode 100644 xml/target/debug/.fingerprint/xml-af563bb74dd611b8/invoked.timestamp delete mode 100644 xml/target/debug/.fingerprint/xml-af563bb74dd611b8/output-bin-xml diff --git a/Cargo.toml b/Cargo.toml index c011c6b..2bcefca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,10 @@ serde = "1.0" diesel = { version = "1.4", features = ["postgres", "serde_json"] } reqwest = "0.11" quickxml_to_serde = "0.4" -serde_json = "1.0.64" +serde_json = "1.0" +jsonwebtoken = "7.2" +time = "0.2" + [dependencies.serde_derive] version = "1.0" diff --git a/Rocket.toml b/Rocket.toml index 5f4bad3..0a3d71a 100644 --- a/Rocket.toml +++ b/Rocket.toml @@ -3,3 +3,10 @@ port = 3000 [debug.databases] timetable = { url = "postgres://meincantor:meincantor_password@localhost/meincantor_db" } + +[release] +address = "192.168.0.12" +port = 3000 + +[release.databases] +timetable = { url = "postgres://meincantor:meincantor_password@localhost/meincantor_db" } diff --git a/src/main.rs b/src/main.rs index 0398c7f..63e2717 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ mod schema; extern crate serde; + use rocket::serde::json::{Json, json}; use crate::schema::timetable; use diesel::{Queryable, Insertable}; @@ -15,6 +16,13 @@ use reqwest; use quickxml_to_serde::{xml_string_to_json, Config}; extern crate serde_json; mod config; +use jsonwebtoken::{encode, decode, Header, Validation, EncodingKey, DecodingKey}; +use std::time::{SystemTime, Duration, UNIX_EPOCH}; +use time::OffsetDateTime; +use rocket::http::Status; +use rocket::request::{self, Outcome, Request, FromRequest}; +use rocket::fs::{FileServer, relative}; + #[database("timetable")] struct DbConn(PgConnection); @@ -34,6 +42,102 @@ struct TimetableData { courses: Vec } +#[derive(Debug, Serialize, Deserialize)] +struct Credentials { + user: String, + password: String, + devid: String +} + +#[derive(Debug, Serialize, Deserialize)] +enum Roles { + Student, + Teacher, + Admin +} + +#[derive(Debug, Serialize, Deserialize)] +struct Claims { + iss: String, + user: String, + roles: Vec, + // permissions: Vec, + blacklist: Vec, + whitelist: Vec, + jid: String, + exp: u64 +} + +#[derive(Debug, Serialize, Deserialize)] +struct Token { + token: String +} + +struct ApiKey<'r>(&'r str); + +#[derive(Debug)] +enum ApiKeyError { + Missing, + Invalid, +} + +#[rocket::async_trait] +impl<'r> FromRequest<'r> for ApiKey<'r> { + type Error = ApiKeyError; + + async fn from_request(req: &'r Request<'_>) -> Outcome { + /// Returns true if `key` is a valid API key string. + fn is_valid(key: &str) -> bool { + let token = decode::(&key, &DecodingKey::from_secret(config::JWT_SECRET.as_ref()), &Validation::default()); + token.is_ok() + } + + fn has_permissions(key: &str, uri: &str) -> bool { + let student_permissions = vec![String::from("/classes"), String::from("/timetable")]; + let teacher_permissions = vec![String::from("/classes"), String::from("/timetable"), String::from("/t_timetable")]; + let token = decode::(&key, &DecodingKey::from_secret(config::JWT_SECRET.as_ref()), &Validation::default()); + println!("{:?}", token); + let mut token = token.unwrap(); + let mut permissions = Vec::new(); + for role in token.claims.roles.iter() { + match role { + Roles::Admin => permissions.push(String::from("all")), + Roles::Student => permissions.extend(student_permissions.iter().cloned()), + Roles::Teacher => permissions.extend(teacher_permissions.iter().cloned()), + _ => permissions.push(String::from("none")) + } + } + permissions.contains(&String::from(uri)) | permissions.contains(&String::from("all")) | &token.claims.whitelist.contains(&String::from(uri)) && !token.claims.blacklist.contains(&String::from(uri)) + } + + match req.headers().get_one("x-api-key") { + None => Outcome::Failure((Status::Unauthorized, ApiKeyError::Missing)), + Some(key) if is_valid(key) && has_permissions(key, req.uri().path().as_str()) => Outcome::Success(ApiKey(key)), + Some(_) => Outcome::Failure((Status::BadRequest, ApiKeyError::Invalid)), + } + } +} + +#[post("/", data="")] +fn login(credentials: Json) -> Json { + let credentials = credentials.into_inner(); + let system_time = OffsetDateTime::now_utc(); + let datetime = system_time.format("%d/%m/%Y %T"); + let my_claims = Claims { + iss: String::from("Georg-Cantor-Gymnasium Halle(Saale)"), + user: credentials.user, + roles: vec![Roles::Student, Roles::Admin], + // permissions: vec![""] + blacklist: vec![String::from("/classes")], + whitelist: vec![String::from("/hello/sensitive")], + jid: String::from(credentials.devid + "@" + &datetime), + exp: SystemTime::now().duration_since(UNIX_EPOCH).expect("Time went backwards").as_secs() + Duration::from_secs(31536000).as_secs() + }; + println!("{:?}", SystemTime::now()); + let token = encode(&Header::default(), &my_claims, &EncodingKey::from_secret(config::JWT_SECRET.as_ref())); + Json(Token { token: token.unwrap() }) +} + async fn get_timetable_xml() -> serde_json::value::Value { let client = reqwest::Client::new(); let resp = client @@ -57,7 +161,7 @@ async fn get_timetable_xml_data() -> Vec { } #[get("/")] -async fn get_timetable(_conn: DbConn) -> Json> { +async fn get_timetable(_conn: DbConn, _key: ApiKey<'_>) -> Json> { let xml = get_timetable_xml().await; let classes = get_timetable_xml_data().await; let mut timetable: Vec = Vec::new(); @@ -106,7 +210,7 @@ async fn get_timetable(_conn: DbConn) -> Json> { } #[get("/")] -async fn get_class_timetable(_conn: DbConn, class: String) -> Json { +async fn get_class_timetable(_conn: DbConn, class: String, _key: ApiKey<'_>) -> Json { let classes = get_timetable_xml_data().await; let courses: Vec = Vec::new(); let mut response = TimetableData { @@ -140,8 +244,8 @@ async fn get_class_timetable(_conn: DbConn, class: String) -> Json Json> { +#[get("/")] +async fn get_classes(_key: ApiKey<'_>) -> Json> { let classes = get_timetable_xml_data().await; let mut class_list: Vec = Vec::new(); for i in classes.iter() { @@ -163,10 +267,18 @@ fn hello_name(name: String) -> String { format!("Hello, {}!", name) } +#[get("/sensitive")] +fn sensitive(key: ApiKey<'_>) -> &'static str { + "Sensitive data." +} + #[launch] fn rocket() -> _ { rocket::build() .attach(DbConn::fairing()) - .mount("/hello", routes![hello, hello_name]) - .mount("/timetable", routes![get_timetable, get_class_timetable, get_classes]) + .mount("/", FileServer::from(relative!("static"))) + .mount("/login", routes![login]) + .mount("/hello", routes![hello, hello_name, sensitive]) + .mount("/api/timetable", routes![get_timetable, get_class_timetable]) + .mount("/api/classes", routes![get_classes]) } diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..6343fd9 --- /dev/null +++ b/static/index.html @@ -0,0 +1,5 @@ + + +

H1

+ + diff --git a/tutor b/tutor deleted file mode 100644 index 8b13789..0000000 --- a/tutor +++ /dev/null @@ -1 +0,0 @@ - diff --git a/xml/Cargo.toml b/xml/Cargo.toml deleted file mode 100644 index 9cdf821..0000000 --- a/xml/Cargo.toml +++ /dev/null @@ -1,8 +0,0 @@ -[package] -name = "xml" -version = "0.1.0" -edition = "2018" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] diff --git a/xml/src/main.rs b/xml/src/main.rs deleted file mode 100644 index 496519d..0000000 --- a/xml/src/main.rs +++ /dev/null @@ -1,39 +0,0 @@ - - -use quick_xml::Reader; -use quick_xml::events::Event; - -let xml = r#" - Test - - Test 2 - - "#; - -let mut reader = Reader::from_str(xml); -reader.trim_text(true); - -let mut count = 0; -let mut txt = Vec::new(); -let mut buf = Vec::new(); - -// The `Reader` does not implement `Iterator` because it outputs borrowed data (`Cow`s) -loop { - match reader.read_event(&mut buf) { - Ok(Event::Start(ref e)) => { - match e.name() { - b"tag1" => println!("attributes values: {:?}", - e.attributes().map(|a| a.unwrap().value).collect::>()), - b"tag2" => count += 1, - _ => (), - } - }, - Ok(Event::Text(e)) => txt.push(e.unescape_and_decode(&reader).unwrap()), - Ok(Event::Eof) => break, // exits the loop when reaching end of file - Err(e) => panic!("Error at position {}: {:?}", reader.buffer_position(), e), - _ => (), // There are several other `Event`s we do not consider here - } - - // if we don't keep a borrow elsewhere, we can clear the buffer to keep memory usage low - buf.clear(); -} diff --git a/xml/target/.rustc_info.json b/xml/target/.rustc_info.json deleted file mode 100644 index 8e48b92..0000000 --- a/xml/target/.rustc_info.json +++ /dev/null @@ -1 +0,0 @@ -{"rustc_fingerprint":6590233451751340509,"outputs":{"17598535894874457435":{"success":true,"status":"","code":0,"stdout":"rustc 1.53.0 (53cb7b09b 2021-06-17)\nbinary: rustc\ncommit-hash: 53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b\ncommit-date: 2021-06-17\nhost: x86_64-unknown-linux-gnu\nrelease: 1.53.0\nLLVM version: 12.0.1\n","stderr":""},"931469667778813386":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.so\nlib___.so\nlib___.a\nlib___.so\n/home/denyskon/.rustup/toolchains/stable-x86_64-unknown-linux-gnu\ndebug_assertions\nproc_macro\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"gnu\"\ntarget_family=\"unix\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_os=\"linux\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"unknown\"\nunix\n","stderr":""},"2797684049618456168":{"success":false,"status":"exit status: 1","code":1,"stdout":"","stderr":"error: `-Csplit-debuginfo` is unstable on this platform\n\n"}},"successes":{}} \ No newline at end of file diff --git a/xml/target/CACHEDIR.TAG b/xml/target/CACHEDIR.TAG deleted file mode 100644 index 20d7c31..0000000 --- a/xml/target/CACHEDIR.TAG +++ /dev/null @@ -1,3 +0,0 @@ -Signature: 8a477f597d28d172789f06886806bc55 -# This file is a cache directory tag created by cargo. -# For information about cache directory tags see https://bford.info/cachedir/ diff --git a/xml/target/debug/.cargo-lock b/xml/target/debug/.cargo-lock deleted file mode 100644 index e69de29..0000000 diff --git a/xml/target/debug/.fingerprint/xml-af563bb74dd611b8/invoked.timestamp b/xml/target/debug/.fingerprint/xml-af563bb74dd611b8/invoked.timestamp deleted file mode 100644 index e00328d..0000000 --- a/xml/target/debug/.fingerprint/xml-af563bb74dd611b8/invoked.timestamp +++ /dev/null @@ -1 +0,0 @@ -This file has an mtime of when this was started. \ No newline at end of file diff --git a/xml/target/debug/.fingerprint/xml-af563bb74dd611b8/output-bin-xml b/xml/target/debug/.fingerprint/xml-af563bb74dd611b8/output-bin-xml deleted file mode 100644 index 6ee4ad6..0000000 --- a/xml/target/debug/.fingerprint/xml-af563bb74dd611b8/output-bin-xml +++ /dev/null @@ -1,2 +0,0 @@ -{"message":"expected item, found keyword `let`","code":null,"level":"error","spans":[{"file_name":"src/main.rs","byte_start":56,"byte_end":59,"line_start":6,"line_end":6,"column_start":1,"column_end":4,"is_primary":true,"text":[{"text":"let xml = r#\"","highlight_start":1,"highlight_end":4}],"label":"expected item","suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[],"rendered":"\u001b[0m\u001b[1m\u001b[38;5;9merror\u001b[0m\u001b[0m\u001b[1m: expected item, found keyword `let`\u001b[0m\n\u001b[0m \u001b[0m\u001b[0m\u001b[1m\u001b[38;5;12m--> \u001b[0m\u001b[0msrc/main.rs:6:1\u001b[0m\n\u001b[0m \u001b[0m\u001b[0m\u001b[1m\u001b[38;5;12m|\u001b[0m\n\u001b[0m\u001b[1m\u001b[38;5;12m6\u001b[0m\u001b[0m \u001b[0m\u001b[0m\u001b[1m\u001b[38;5;12m| \u001b[0m\u001b[0mlet xml = r#\"\u001b[0m\n\u001b[0m \u001b[0m\u001b[0m\u001b[1m\u001b[38;5;12m| \u001b[0m\u001b[0m\u001b[1m\u001b[38;5;9m^^^\u001b[0m\u001b[0m \u001b[0m\u001b[0m\u001b[1m\u001b[38;5;9mexpected item\u001b[0m\n\n"} -{"message":"aborting due to previous error","code":null,"level":"error","spans":[],"children":[],"rendered":"\u001b[0m\u001b[1m\u001b[38;5;9merror\u001b[0m\u001b[0m\u001b[1m: aborting due to previous error\u001b[0m\n\n"}