diff --git a/backend/Cargo.lock b/backend/Cargo.lock index ac1ff76..df5d986 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -1608,6 +1608,7 @@ dependencies = [ "egg-mode", "elefren", "futures-io", + "hex", "jsonfeed", "kankyo", "log 0.4.11", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 7f745dd..d9105df 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -30,6 +30,7 @@ uuid = { version = "0.7", features = ["serde", "v4"] } rocket_prometheus = "0.7.0" prometheus = { version = "0.10", default-features = false, features = ["process"] } futures-io = "0.3" +hex = "0.4" jsonfeed = { git = "https://github.com/Xe/site" } diff --git a/backend/src/api.rs b/backend/src/api.rs index dff7634..d0e5cf3 100644 --- a/backend/src/api.rs +++ b/backend/src/api.rs @@ -1,4 +1,4 @@ -use crate::{models, schema, web, MainDatabase}; +use crate::{models, paseto, schema, web, MainDatabase}; use chrono::prelude::*; use diesel::prelude::*; use rocket::{ @@ -16,7 +16,7 @@ use std::io::Read; #[get("/members")] #[instrument(skip(conn), err)] -pub fn get_members(conn: MainDatabase) -> Result>> { +pub fn get_members(tok: paseto::Token, conn: MainDatabase) -> Result>> { use schema::members; let results = members::table .load::(&*conn) @@ -40,6 +40,7 @@ pub fn get_switches( conn: MainDatabase, count: Option, page: Option, + tok: paseto::Token, ) -> Result>> { use schema::{members, switches}; @@ -73,7 +74,7 @@ pub fn get_switches( #[get("/switches/current")] #[instrument(skip(conn), err)] -pub fn get_current_front(conn: MainDatabase) -> Result> { +pub fn get_current_front(conn: MainDatabase, tok: paseto::Token) -> Result> { use schema::{members, switches}; let mut front: Vec<(models::Switch, models::Member)> = switches::table @@ -102,6 +103,7 @@ pub fn make_switch( who: StringBody, sc: State, pk: State, + tok: paseto::Token, ) -> Result { use schema::{members, switches}; let who = who.unwrap(); @@ -159,7 +161,11 @@ pub fn make_switch( #[get("/switches/")] #[instrument(skip(conn), err)] -pub fn get_switch(conn: MainDatabase, switch_id: String) -> Result> { +pub fn get_switch( + tok: paseto::Token, + conn: MainDatabase, + switch_id: String, +) -> Result> { use schema::{members, switches::dsl::switches}; let (switch, member): (models::Switch, models::Member) = switches diff --git a/backend/src/lib.rs b/backend/src/lib.rs index c2a06a1..a71dedb 100644 --- a/backend/src/lib.rs +++ b/backend/src/lib.rs @@ -20,6 +20,7 @@ pub const APPLICATION_NAME: &str = concat!( pub mod api; pub mod models; +pub mod paseto; pub mod schema; pub mod web; diff --git a/backend/src/main.rs b/backend/src/main.rs index dd3db2e..66af15d 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -9,7 +9,7 @@ use color_eyre::eyre::Result; use rocket_contrib::helmet::SpaceHelmet; use rocket_prometheus::PrometheusMetrics; -use ::mi::{api, web, MainDatabase, APPLICATION_NAME}; +use ::mi::{api, paseto, web, MainDatabase, APPLICATION_NAME}; #[get("/.within/botinfo")] fn botinfo() -> &'static str { @@ -42,13 +42,14 @@ fn main() -> Result<()> { .attach(prometheus.clone()) .attach(MainDatabase::fairing()) .attach(SpaceHelmet::default()) + .attach(paseto::ed25519_keypair()) .attach(web::pluralkit::Client::fairing()) .attach(web::switchcounter::Client::fairing()) .mount("/metrics", prometheus) + .mount("/", routes![botinfo]) .mount( - "/", + "/api", routes![ - botinfo, api::get_members, api::get_switches, api::get_switch, diff --git a/backend/src/paseto.rs b/backend/src/paseto.rs new file mode 100644 index 0000000..ac611ee --- /dev/null +++ b/backend/src/paseto.rs @@ -0,0 +1,73 @@ +use paseto::tokens::{validate_public_token, PasetoPublicKey}; +use paseto::PasetoBuilder; +use ring::signature::Ed25519KeyPair; +use rocket::{ + fairing::AdHoc, + http::Status, + request::{self, FromRequest, Request}, + Outcome, State, +}; +use rusty_ulid::generate_ulid_string; +use serde::{Deserialize, Serialize}; + +pub fn ed25519_keypair() -> AdHoc { + AdHoc::on_attach("ed25519 keypair for paseto", |rocket| { + let cfg = rocket.config(); + let table = cfg.get_table("paseto").unwrap(); + let private = table["private"].as_str().unwrap().to_string(); + let private = hex::decode(&private).unwrap(); + let public = table["public"].as_str().unwrap().to_string(); + let public = hex::decode(&public).unwrap(); + let kp = Ed25519KeyPair::from_seed_and_public_key(&private, &public).unwrap(); + + let token = PasetoBuilder::new() + .set_ed25519_key(kp) + .set_issued_at(None) + .set_issuer("manual API call".into()) + .set_audience("wizards".into()) + .set_jti(generate_ulid_string()) + .set_subject("Within".into()) + .build() + .unwrap(); + debug!("token: {}", token); + + Ok(rocket + .manage(Ed25519KeyPair::from_seed_and_public_key(&private, &public).unwrap()) + .manage(PasetoPublicKey::ED25519KeyPair( + Ed25519KeyPair::from_seed_and_public_key(&private, &public).unwrap(), + ))) + }) +} + +#[derive(Debug, Deserialize)] +pub struct Token { + pub jti: String, + pub sub: String, + pub aud: String, +} + +impl<'a, 'r> FromRequest<'a, 'r> for Token { + type Error = (); + + fn from_request(request: &'a Request<'r>) -> request::Outcome { + let keys: Vec<_> = request.headers().get("authorization").collect(); + match keys.len() { + 1 => { + let tok = keys[0]; + let paseto_key = request.guard::>().unwrap(); + + match validate_public_token(tok, None, &paseto_key) { + Ok(val) => { + let tok: Token = serde_json::from_value(val).unwrap(); + Outcome::Success(tok) + } + Err(why) => { + error!("paseto error: {}", why); + Outcome::Failure((Status::Unauthorized, ())) + } + } + } + _ => Outcome::Failure((Status::Unauthorized, ())), + } + } +}