From c99bc1119f14ca79139436cf1c681e02566e7d35 Mon Sep 17 00:00:00 2001 From: Christine Dodrill Date: Fri, 30 Oct 2020 10:26:56 -0400 Subject: [PATCH] split handler families into files --- migrations/2020-10-28-190823_handlers/up.sql | 5 +- src/api/handler.rs | 61 +++++++++++ src/{api.rs => api/mod.rs} | 87 +++------------ src/api/token.rs | 55 ++++++++++ src/api/user.rs | 19 ++++ src/gitea.rs | 91 ++++++++++++++++ src/jwt.rs | 4 +- src/main.rs | 106 ++----------------- src/models.rs | 23 +++- src/schema.rs | 5 +- 10 files changed, 278 insertions(+), 178 deletions(-) create mode 100644 src/api/handler.rs rename src/{api.rs => api/mod.rs} (58%) create mode 100644 src/api/token.rs create mode 100644 src/api/user.rs diff --git a/migrations/2020-10-28-190823_handlers/up.sql b/migrations/2020-10-28-190823_handlers/up.sql index 5271903..1680068 100644 --- a/migrations/2020-10-28-190823_handlers/up.sql +++ b/migrations/2020-10-28-190823_handlers/up.sql @@ -2,10 +2,11 @@ CREATE TABLE IF NOT EXISTS handlers ( id UUID DEFAULT uuid_generate_v4() NOT NULL , user_id UUID NOT NULL , human_name VARCHAR NOT NULL - , current_version VARCHAR NOT NULL - , async_impl BOOLEAN DEFAULT false + , current_version VARCHAR + , async_impl BOOLEAN NOT NULL DEFAULT false , created_at TIMESTAMP NOT NULL DEFAULT NOW() , updated_at TIMESTAMP NOT NULL DEFAULT NOW() + , deleted_at TIMESTAMP , PRIMARY KEY (id) , CONSTRAINT fk_user_id FOREIGN KEY (user_id) diff --git a/src/api/handler.rs b/src/api/handler.rs new file mode 100644 index 0000000..8cc6c98 --- /dev/null +++ b/src/api/handler.rs @@ -0,0 +1,61 @@ +use crate::{models, schema, MainDatabase}; +use super::{Error, Result}; +use chrono::prelude::*; +use diesel::prelude::*; +use rocket_contrib::{json::Json, uuid::Uuid}; + +#[instrument(skip(conn))] +#[get("/handler")] +pub fn list(user: models::User, conn: MainDatabase) -> Result>> { + use schema::handlers::dsl::*; + + Ok(Json( + handlers + .filter(user_id.eq(user.id)) + .load::(&*conn) + .map_err(Error::Database)?, + )) +} + +#[instrument(skip(conn))] +#[get("/handler/")] +pub fn get( + user: models::User, + uuid: Uuid, + conn: MainDatabase, +) -> Result> { + use schema::handlers::dsl::*; + let uuid = uuid.into_inner(); + let handler = handlers + .find(uuid) + .get_result::(&*conn) + .map_err(Error::Database)?; + + if handler.user_id != user.id { + Err(Error::LackPermissions) + } else { + Ok(Json(handler)) + } +} + +#[instrument(skip(conn))] +#[delete("/handler/")] +pub fn delete(user: models::User, uuid: Uuid, conn: MainDatabase) -> Result { + use schema::handlers::dsl::*; + let uuid = uuid.into_inner(); + + let hdl: models::Handler = handlers + .find(uuid.clone()) + .get_result(&*conn) + .map_err(Error::Database)?; + + if hdl.user_id != user.id && !user.is_admin { + return Err(Error::LackPermissions); + } + + diesel::update(handlers.find(uuid)) + .set(deleted_at.eq(Utc::now().naive_utc())) + .get_result::(&*conn)?; + + Ok(()) +} diff --git a/src/api.rs b/src/api/mod.rs similarity index 58% rename from src/api.rs rename to src/api/mod.rs index 64a707e..2324e9c 100644 --- a/src/api.rs +++ b/src/api/mod.rs @@ -1,79 +1,16 @@ -use crate::{jwt, models, schema, MainDatabase}; -use chrono::prelude::*; +use crate::{jwt, models, MainDatabase}; use color_eyre::eyre::Report; -use diesel::prelude::*; -use rocket::http::{ContentType, Status}; -use rocket::request::{self, FromRequest, Request}; -use rocket::response::Responder; -use rocket::Outcome; -use rocket::Response; -use rocket_contrib::{json::Json, uuid::Uuid}; +use rocket::{ + http::{ContentType, Status}, + request::{self, FromRequest, Request}, + response::Responder, + Outcome, Response, +}; use std::io::Cursor; -#[tracing::instrument] -#[get("/user/")] -pub fn get_user(user: models::User, uuid: Uuid) -> Result> { - if uuid != user.id { - return Err(Error::LackPermissions); - } - - Ok(Json(user)) -} - -#[instrument] -#[get("/whoami")] -pub fn whoami(user: models::User) -> Json { - Json(user) -} - -#[instrument(skip(conn))] -#[get("/token")] -pub fn get_tokens(user: models::User, conn: MainDatabase) -> Result>> { - use schema::tokens::dsl::*; - - Ok(Json( - tokens - .filter(user_id.eq(user.id)) - .load::(&*conn) - .map_err(Error::Database)?, - )) -} - -#[instrument(skip(conn))] -#[delete("/token/")] -pub fn delete_token(user: models::User, conn: MainDatabase, uuid: Uuid) -> Result { - use schema::tokens::dsl::*; - let uuid = uuid.into_inner(); - - let tok: models::Token = tokens - .find(uuid.clone()) - .get_result(&*conn) - .map_err(Error::Database)?; - - if tok.user_id != user.id && !user.is_admin { - return Err(Error::LackPermissions); - } - - diesel::update(tokens.find(uuid)) - .set(deleted_at.eq(Utc::now().naive_utc())) - .get_result::(&*conn)?; - - Ok(()) -} - -#[instrument(skip(conn))] -#[post("/token")] -pub fn create_token(user: models::User, conn: MainDatabase) -> Result { - use schema::tokens; - - let tok: models::Token = diesel::insert_into(tokens::table) - .values(&models::NewToken { - user_id: user.id.clone(), - }) - .get_result(&*conn).map_err(Error::Database)?; - - Ok(jwt::make(user.id, tok.id)?) -} +pub mod handler; +pub mod token; +pub mod user; #[derive(thiserror::Error, Debug)] pub enum Error { @@ -143,7 +80,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for models::User { match jwt::verify(tok, conn) { Err(why) => { - tracing::error!("JWT verification error: {}", why); + error!("JWT verification error: {}", why); Outcome::Failure((Status::Unauthorized, ())) } Ok(user) => Outcome::Success(user), @@ -155,7 +92,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for models::User { let tok = keys[0].to_string(); match jwt::verify(tok, conn) { Err(why) => { - tracing::error!("JWT verification error: {}", why); + error!("JWT verification error: {}", why); Outcome::Failure((Status::Unauthorized, ())) } Ok(user) => Outcome::Success(user), diff --git a/src/api/token.rs b/src/api/token.rs new file mode 100644 index 0000000..38b6694 --- /dev/null +++ b/src/api/token.rs @@ -0,0 +1,55 @@ +use crate::{jwt, models, schema, MainDatabase}; +use super::{Error, Result}; +use chrono::prelude::*; +use diesel::prelude::*; +use rocket_contrib::{json::Json, uuid::Uuid}; + +#[instrument(skip(conn))] +#[get("/token")] +pub fn list(user: models::User, conn: MainDatabase) -> Result>> { + use schema::tokens::dsl::*; + + Ok(Json( + tokens + .filter(user_id.eq(user.id)) + .load::(&*conn) + .map_err(Error::Database)?, + )) +} + +#[instrument(skip(conn))] +#[delete("/token/")] +pub fn delete(user: models::User, conn: MainDatabase, uuid: Uuid) -> Result { + use schema::tokens::dsl::*; + let uuid = uuid.into_inner(); + + let tok: models::Token = tokens + .find(uuid.clone()) + .get_result(&*conn) + .map_err(Error::Database)?; + + if tok.user_id != user.id && !user.is_admin { + return Err(Error::LackPermissions); + } + + diesel::update(tokens.find(uuid)) + .set(deleted_at.eq(Utc::now().naive_utc())) + .get_result::(&*conn)?; + + Ok(()) +} + +#[instrument(skip(conn))] +#[post("/token")] +pub fn create(user: models::User, conn: MainDatabase) -> Result { + use schema::tokens; + + let tok: models::Token = diesel::insert_into(tokens::table) + .values(&models::NewToken { + user_id: user.id.clone(), + }) + .get_result(&*conn) + .map_err(Error::Database)?; + + Ok(jwt::make(user.id, tok.id)?) +} diff --git a/src/api/user.rs b/src/api/user.rs new file mode 100644 index 0000000..d40ed5e --- /dev/null +++ b/src/api/user.rs @@ -0,0 +1,19 @@ +use crate::models; +use super::{Error, Result}; +use rocket_contrib::{json::Json, uuid::Uuid}; + +#[instrument] +#[get("/user/")] +pub fn get(user: models::User, uuid: Uuid) -> Result> { + if uuid != user.id { + return Err(Error::LackPermissions); + } + + Ok(Json(user)) +} + +#[instrument] +#[get("/whoami")] +pub fn whoami(user: models::User) -> Json { + Json(user) +} diff --git a/src/gitea.rs b/src/gitea.rs index 0b2ca59..fcdd4ea 100644 --- a/src/gitea.rs +++ b/src/gitea.rs @@ -1,4 +1,11 @@ +use crate::{MainDatabase, Gitea, models, jwt, schema, api}; use serde::{Deserialize, Serialize}; +use diesel::prelude::*; +use rocket::{ + http::{Cookie, Cookies, SameSite}, + response::Redirect, +}; +use rocket_oauth2::{OAuth2, TokenResponse}; /// A user. /// https://try.gitea.io/api/swagger#model-User @@ -25,3 +32,87 @@ pub fn user(token: String) -> std::io::Result { let user: User = resp.into_json_deserialize()?; Ok(user) } + +#[instrument(skip(oauth2, cookies))] +#[get("/login/gitea")] +pub fn login(oauth2: OAuth2, mut cookies: Cookies<'_>) -> Redirect { + oauth2.get_redirect(&mut cookies, &[""]).unwrap() +} + +#[instrument(skip(conn, token, cookies))] +#[get("/auth/gitea")] +pub fn callback( + conn: MainDatabase, + token: TokenResponse, + mut cookies: Cookies<'_>, +) -> api::Result { + let tok = token.access_token().to_string(); + let refresh = token.refresh_token().unwrap().to_string(); + + let gitea_user = + user(tok.clone()).map_err(|why| api::Error::ExternalDependencyFailed(why.into()))?; + + use schema::{ + gitea_tokens, tokens, + users::{ + dsl::{email, users}, + table as users_table, + }, + }; + let u: Vec = users + .filter(email.eq(gitea_user.email.clone())) + .limit(1) + .load::(&*conn) + .map_err(api::Error::Database)?; + + let user = if u.len() == 0 { + let u = models::NewUser { + salutation: gitea_user.full_name, + email: gitea_user.email, + is_admin: gitea_user.is_admin, + is_locked: false, + tier: 0, + }; + + let u: models::User = diesel::insert_into(users_table) + .values(&u) + .get_result(&*conn) + .map_err(api::Error::Database)?; + + let tok = models::NewGiteaToken { + user_id: u.id.clone(), + access_token: tok, + refresh_token: refresh, + }; + + let _: models::GiteaToken = diesel::insert_into(gitea_tokens::table) + .values(&tok) + .get_result(&*conn) + .map_err(api::Error::Database)?; + + info!("new account created for {:?}", u); + + u + } else { + info!("{} {:?} logged in", u[0].id, u[0].salutation); + u[0].clone() + }; + + let tok: models::Token = diesel::insert_into(tokens::table) + .values(&models::NewToken { + user_id: user.id.clone(), + }) + .get_result(&*conn) + .map_err(api::Error::Database)?; + info!("created new token for {} with id {}", user.id, tok.id); + + let tok = jwt::make(user.id, tok.id).map_err(api::Error::InternalServerError)?; + + cookies.add_private( + Cookie::build("token", tok.clone()) + .same_site(SameSite::Lax) + .finish(), + ); + + Ok(tok) +} diff --git a/src/jwt.rs b/src/jwt.rs index b30f251..cba7583 100644 --- a/src/jwt.rs +++ b/src/jwt.rs @@ -15,7 +15,7 @@ lazy_static! { .to_string(); } -#[tracing::instrument] +#[instrument] pub fn make(user_id: uuid::Uuid, token_id: uuid::Uuid) -> Result { let key: Hmac = Hmac::new_varkey(&*SECRET.as_bytes()).unwrap(); let mut claims = BTreeMap::new(); @@ -27,7 +27,7 @@ pub fn make(user_id: uuid::Uuid, token_id: uuid::Uuid) -> Result { Ok(token_str) } -#[tracing::instrument(skip(token, conn))] +#[instrument(skip(token, conn))] pub fn verify(token: String, conn: MainDatabase) -> Result { use schema::{tokens::dsl::tokens, users::dsl::users}; let key: Hmac = Hmac::new_varkey(&*SECRET.as_bytes()).unwrap(); diff --git a/src/main.rs b/src/main.rs index e279381..8eb3608 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,13 +11,8 @@ extern crate tracing; use color_eyre::eyre::Result; use diesel::pg::PgConnection; -use diesel::prelude::*; -use rocket::{ - http::{Cookie, Cookies, SameSite}, - response::Redirect, -}; use rocket_contrib::helmet::SpaceHelmet; -use rocket_oauth2::{OAuth2, TokenResponse}; +use rocket_oauth2::{OAuth2}; pub mod api; pub mod gitea; @@ -30,90 +25,6 @@ pub struct MainDatabase(PgConnection); pub struct Gitea; -#[instrument(skip(oauth2, cookies))] -#[get("/login/gitea")] -fn gitea_login(oauth2: OAuth2, mut cookies: Cookies<'_>) -> Redirect { - oauth2.get_redirect(&mut cookies, &[""]).unwrap() -} - -#[instrument(skip(conn, token, cookies))] -#[get("/auth/gitea")] -fn gitea_callback( - conn: MainDatabase, - token: TokenResponse, - mut cookies: Cookies<'_>, -) -> api::Result { - let tok = token.access_token().to_string(); - let refresh = token.refresh_token().unwrap().to_string(); - - let gitea_user = - gitea::user(tok.clone()).map_err(|why| api::Error::ExternalDependencyFailed(why.into()))?; - - use schema::{ - gitea_tokens, tokens, - users::{ - dsl::{email, users}, - table as users_table, - }, - }; - let u: Vec = users - .filter(email.eq(gitea_user.email.clone())) - .limit(1) - .load::(&*conn) - .map_err(api::Error::Database)?; - - let user = if u.len() == 0 { - let u = models::NewUser { - salutation: gitea_user.full_name, - email: gitea_user.email, - is_admin: gitea_user.is_admin, - is_locked: false, - tier: 0, - }; - - let u: models::User = diesel::insert_into(users_table) - .values(&u) - .get_result(&*conn) - .map_err(api::Error::Database)?; - - let tok = models::NewGiteaToken { - user_id: u.id.clone(), - access_token: tok, - refresh_token: refresh, - }; - - let _: models::GiteaToken = diesel::insert_into(gitea_tokens::table) - .values(&tok) - .get_result(&*conn) - .map_err(api::Error::Database)?; - - info!("new account created for {:?}", u); - - u - } else { - info!("{} {:?} logged in", u[0].id, u[0].salutation); - u[0].clone() - }; - - let tok: models::Token = diesel::insert_into(tokens::table) - .values(&models::NewToken { - user_id: user.id.clone(), - }) - .get_result(&*conn) - .map_err(api::Error::Database)?; - info!("created new token for {} with id {}", user.id, tok.id); - - let tok = jwt::make(user.id, tok.id).map_err(api::Error::InternalServerError)?; - - cookies.add_private( - Cookie::build("token", tok.clone()) - .same_site(SameSite::Lax) - .finish(), - ); - - Ok(tok) -} - fn main() -> Result<()> { color_eyre::install()?; tracing_subscriber::fmt::init(); @@ -129,14 +40,17 @@ fn main() -> Result<()> { .mount( "/api", routes![ - api::whoami, - api::get_user, - api::get_tokens, - api::delete_token, - api::create_token, + api::handler::list, + api::handler::get, + api::handler::delete, + api::user::whoami, + api::user::get, + api::token::list, + api::token::delete, + api::token::create, ], ) - .mount("/", routes![gitea_login, gitea_callback]) + .mount("/", routes![gitea::login, gitea::callback]) .launch(); Ok(()) diff --git a/src/models.rs b/src/models.rs index f34effe..da28910 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,4 +1,4 @@ -use crate::schema::{gitea_tokens, tokens, users}; +use crate::schema::{gitea_tokens, tokens, users, handlers}; use chrono::NaiveDateTime; use serde::Serialize; use uuid::Uuid; @@ -57,3 +57,24 @@ pub struct Token { pub updated_at: NaiveDateTime, pub deleted_at: Option, } + +#[derive(Insertable)] +#[table_name = "handlers"] +pub struct NewHandler { + pub user_id: Uuid, + pub human_name: String, + pub current_version: Option, + pub async_impl: bool, +} + +#[derive(Queryable, Debug, Clone, Serialize)] +pub struct Handler { + pub id: Uuid, + pub user_id: Uuid, + pub human_name: String, + pub current_version: Option, + pub async_impl: bool, + pub created_at: NaiveDateTime, + pub updated_at: NaiveDateTime, + pub deleted_at: Option, +} diff --git a/src/schema.rs b/src/schema.rs index b67a783..61b1056 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -14,10 +14,11 @@ table! { id -> Uuid, user_id -> Uuid, human_name -> Varchar, - current_version -> Varchar, - async_impl -> Nullable, + current_version -> Nullable, + async_impl -> Bool, created_at -> Timestamp, updated_at -> Timestamp, + deleted_at -> Nullable, } }