Layed groundwork for microservice split

This commit is contained in:
Jef Roosens 2021-10-25 21:28:22 +02:00
parent e2003442e2
commit 0b048eb8b0
Signed by: Jef Roosens
GPG key ID: 955C0660072F691F
44 changed files with 461 additions and 259 deletions

49
common/Cargo.toml Normal file
View file

@ -0,0 +1,49 @@
[package]
name = "rb"
version = "0.1.0"
authors = ["Jef Roosens <roosensjef@gmail.com>"]
edition = "2018"
[lib]
name = "rb"
path = "src/lib.rs"
# [features]
# web = []
# docs = []
# static = ["web", "docs"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
# Backend web framework
rocket = { version = "0.5.0-rc.1", features = [ "json", "uuid" ] }
# Used to provide Rocket routes with database connections
rocket_sync_db_pools = { version = "0.1.0-rc.1", default_features = false, features = [ "diesel_postgres_pool" ] }
# Used to (de)serialize JSON
serde = { version = "1.0.127", features = [ "derive" ] }
# ORM
diesel = { version = "1.4.7", features = ["postgres", "uuidv07", "chrono"] }
diesel_migrations = "1.4.0"
# To properly compile libpq statically
openssl = "0.10.36"
# For password hashing & verification
rust-argon2 = "0.8.3"
rand = "0.8.4"
uuid = { version = "0.8.2", features = ["serde"] }
# Authentification
jwt = "0.14.0"
hmac = "*"
sha2 = "*"
# Timestamps for JWT tokens
chrono = { version = "*", features = [ "serde" ] }
# Encoding of refresh tokens
base64 = "0.13.0"
# Reading in configuration files
figment = { version = "*", features = [ "yaml" ] }
mimalloc = { version = "0.1.26", default_features = false }
# [profile.release]
# lto = "fat"
# panic = "abort"
# codegen-units = 1

118
common/src/auth/jwt.rs Normal file
View file

@ -0,0 +1,118 @@
use chrono::Utc;
use diesel::PgConnection;
use hmac::{Hmac, NewMac};
use jwt::SignWithKey;
use rand::{thread_rng, Rng};
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use crate::{
db,
errors::{RbError, RbResult},
RbJwtConf,
};
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct JWTResponse
{
token: String,
refresh_token: String,
}
#[derive(Serialize, Deserialize)]
pub struct Claims
{
pub id: uuid::Uuid,
pub username: String,
pub admin: bool,
pub exp: i64,
}
pub fn generate_jwt_token(
conn: &PgConnection,
jwt: &RbJwtConf,
user: &db::User,
) -> RbResult<JWTResponse>
{
let key: Hmac<Sha256> = Hmac::new_from_slice(jwt.key.as_bytes())
.map_err(|_| RbError::Custom("Couldn't create Hmac key."))?;
let current_time = Utc::now();
// Create the claims
let claims = Claims {
id: user.id,
username: user.username.clone(),
admin: user.admin,
exp: current_time.timestamp() + jwt.refresh_token_expire,
};
// Sign the claims into a new token
let token = claims
.sign_with_key(&key)
.map_err(|_| RbError::Custom("Couldn't sign JWT."))?;
// Generate a random refresh token
let mut refresh_token = vec![0u8; jwt.refresh_token_size];
thread_rng().fill(&mut refresh_token[..]);
let refresh_expire =
(current_time + chrono::Duration::seconds(jwt.refresh_token_expire)).naive_utc();
// Store refresh token in database
db::tokens::create(
conn,
&db::NewRefreshToken {
token: refresh_token.to_vec(),
user_id: user.id,
expires_at: refresh_expire,
},
)?;
Ok(JWTResponse {
token,
refresh_token: base64::encode(refresh_token),
})
}
pub fn refresh_token(
conn: &PgConnection,
jwt: &RbJwtConf,
refresh_token: &str,
) -> RbResult<JWTResponse>
{
let token_bytes =
base64::decode(refresh_token).map_err(|_| RbError::AuthInvalidRefreshToken)?;
// First, we request the token from the database to see if it's really a valid token
let (token_entry, user) =
db::tokens::find_with_user(conn, &token_bytes).ok_or(RbError::AuthInvalidRefreshToken)?;
// If we see that the token has already been used before, we block the user.
if token_entry.last_used_at.is_some() {
// If we fail to block the user, the end user must know
if let Err(err) = db::users::block(conn, token_entry.user_id) {
return Err(err);
}
return Err(RbError::AuthDuplicateRefreshToken);
}
// Then we check if the user is blocked
if user.blocked {
return Err(RbError::AuthBlockedUser);
}
// Now we check if the token has already expired
let cur_time = Utc::now().naive_utc();
if token_entry.expires_at < cur_time {
return Err(RbError::AuthTokenExpired);
}
// We update the last_used_at value for the refresh token
db::tokens::update_last_used_at(conn, &token_entry.token, cur_time)?;
generate_jwt_token(conn, jwt, &user)
}

68
common/src/auth/mod.rs Normal file
View file

@ -0,0 +1,68 @@
use rocket::{serde::json::Json, State};
use serde::Deserialize;
use self::{
jwt::{generate_jwt_token, JWTResponse},
pass::verify_user,
};
use crate::{errors::RbResult, guards::User, RbConfig, RbDbConn};
pub mod jwt;
pub mod pass;
#[derive(Deserialize)]
pub struct Credentials
{
username: String,
password: String,
}
#[post("/login")]
pub async fn already_logged_in(_user: User) -> String
{
String::from("You're already logged in!")
}
#[post("/login", data = "<credentials>", rank = 2)]
pub async fn login(
conn: RbDbConn,
conf: &State<RbConfig>,
credentials: Json<Credentials>,
) -> RbResult<Json<JWTResponse>>
{
let credentials = credentials.into_inner();
let jwt = conf.jwt.clone();
// Get the user, if credentials are valid
let user = conn
.run(move |c| verify_user(c, &credentials.username, &credentials.password))
.await?;
Ok(Json(
conn.run(move |c| generate_jwt_token(c, &jwt, &user))
.await?,
))
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RefreshTokenRequest
{
pub refresh_token: String,
}
#[post("/refresh", data = "<refresh_token_request>")]
pub async fn refresh_token(
conn: RbDbConn,
conf: &State<RbConfig>,
refresh_token_request: Json<RefreshTokenRequest>,
) -> RbResult<Json<JWTResponse>>
{
let refresh_token = refresh_token_request.into_inner().refresh_token;
let jwt = conf.jwt.clone();
Ok(Json(
conn.run(move |c| crate::auth::jwt::refresh_token(c, &jwt, &refresh_token))
.await?,
))
}

36
common/src/auth/pass.rs Normal file
View file

@ -0,0 +1,36 @@
use argon2::verify_encoded;
use diesel::PgConnection;
use rand::{thread_rng, Rng};
use crate::{
db,
errors::{RbError, RbResult},
};
pub fn verify_user(conn: &PgConnection, username: &str, password: &str) -> RbResult<db::User>
{
// TODO handle non-"NotFound" Diesel errors accordingely
let user = db::users::find_by_username(conn, username).map_err(|_| RbError::AuthUnknownUser)?;
// Check if a user is blocked
if user.blocked {
return Err(RbError::AuthBlockedUser);
}
match verify_encoded(user.password.as_str(), password.as_bytes()) {
Ok(true) => Ok(user),
_ => Err(RbError::AuthInvalidPassword),
}
}
pub fn hash_password(password: &str) -> RbResult<String>
{
// Generate a random salt
let mut salt = [0u8; 64];
thread_rng().fill(&mut salt[..]);
// Encode the actual password
let config = argon2::Config::default();
argon2::hash_encoded(password.as_bytes(), &salt, &config)
.map_err(|_| RbError::Custom("Couldn't hash password."))
}

12
common/src/db/mod.rs Normal file
View file

@ -0,0 +1,12 @@
//! The db module contains all Diesel-related logic. This is to prevent the various Diesel imports
//! from poluting other modules' namespaces.
pub mod posts;
pub mod sections;
pub mod tokens;
pub mod users;
pub use posts::{NewPost, PatchPost, Post};
pub use sections::{NewSection, Section};
pub use tokens::{NewRefreshToken, RefreshToken};
pub use users::{NewUser, User};

85
common/src/db/posts.rs Normal file
View file

@ -0,0 +1,85 @@
use chrono::NaiveDate;
use diesel::{insert_into, prelude::*, Insertable, PgConnection, Queryable};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::{
errors::{RbError, RbOption, RbResult},
schema::{posts, posts::dsl::*},
};
#[derive(Queryable, Serialize)]
pub struct Post
{
pub id: Uuid,
pub section_id: Uuid,
pub title: Option<String>,
pub publish_date: NaiveDate,
pub content: String,
}
#[derive(Deserialize, Insertable)]
#[table_name = "posts"]
#[serde(rename_all = "camelCase")]
pub struct NewPost
{
pub section_id: Uuid,
pub title: Option<String>,
pub publish_date: NaiveDate,
pub content: String,
}
#[derive(Deserialize, AsChangeset)]
#[table_name = "posts"]
pub struct PatchPost
{
pub section_id: Option<Uuid>,
pub title: Option<String>,
pub publish_date: Option<NaiveDate>,
pub content: Option<String>,
}
pub fn get(conn: &PgConnection, offset_: u32, limit_: u32) -> RbResult<Vec<Post>>
{
Ok(posts
.offset(offset_.into())
.limit(limit_.into())
.load(conn)
.map_err(|_| RbError::DbError("Couldn't query posts."))?)
}
pub fn find(conn: &PgConnection, id_: &Uuid) -> RbOption<Post>
{
match posts.find(id_).first(conn) {
Ok(val) => Ok(Some(val)),
Err(diesel::NotFound) => Ok(None),
_ => Err(RbError::DbError("Couldn't find post.")),
}
}
pub fn create(conn: &PgConnection, new_post: &NewPost) -> RbResult<Post>
{
Ok(insert_into(posts)
.values(new_post)
.get_result(conn)
.map_err(|_| RbError::DbError("Couldn't insert post."))?)
// TODO check for conflict?
}
pub fn update(conn: &PgConnection, post_id: &Uuid, patch_post: &PatchPost) -> RbResult<Post>
{
Ok(diesel::update(posts.filter(id.eq(post_id)))
.set(patch_post)
.get_result(conn)
.map_err(|_| RbError::DbError("Couldn't update post."))?)
}
pub fn delete(conn: &PgConnection, post_id: &Uuid) -> RbResult<()>
{
diesel::delete(posts.filter(id.eq(post_id)))
.execute(conn)
.map_err(|_| RbError::DbError("Couldn't delete post."))?;
Ok(())
}

79
common/src/db/sections.rs Normal file
View file

@ -0,0 +1,79 @@
use diesel::{insert_into, prelude::*, Insertable, PgConnection, Queryable};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::{
errors::{RbError, RbResult},
schema::{sections, sections::dsl::*},
};
#[derive(Queryable, Serialize)]
pub struct Section
{
pub id: Uuid,
pub title: String,
pub shortname: String,
pub description: Option<String>,
pub is_default: bool,
pub has_titles: bool,
}
#[derive(Deserialize, Insertable)]
#[table_name = "sections"]
#[serde(rename_all = "camelCase")]
pub struct NewSection
{
title: String,
pub shortname: String,
description: Option<String>,
is_default: Option<bool>,
has_titles: Option<bool>,
}
#[derive(Deserialize, AsChangeset)]
#[table_name = "sections"]
#[serde(rename_all = "camelCase")]
pub struct PatchSection
{
title: Option<String>,
shortname: Option<String>,
description: Option<String>,
is_default: Option<bool>,
has_titles: Option<bool>,
}
pub fn get(conn: &PgConnection, offset_: u32, limit_: u32) -> RbResult<Vec<Section>>
{
Ok(sections
.offset(offset_.into())
.limit(limit_.into())
.load(conn)
.map_err(|_| RbError::DbError("Couldn't query sections."))?)
}
pub fn create(conn: &PgConnection, new_post: &NewSection) -> RbResult<Section>
{
Ok(insert_into(sections)
.values(new_post)
.get_result(conn)
.map_err(|_| RbError::DbError("Couldn't insert section."))?)
// TODO check for conflict?
}
pub fn update(conn: &PgConnection, post_id: &Uuid, patch_post: &PatchSection) -> RbResult<Section>
{
Ok(diesel::update(sections.filter(id.eq(post_id)))
.set(patch_post)
.get_result(conn)
.map_err(|_| RbError::DbError("Couldn't update section."))?)
}
pub fn delete(conn: &PgConnection, post_id: &Uuid) -> RbResult<()>
{
diesel::delete(sections.filter(id.eq(post_id)))
.execute(conn)
.map_err(|_| RbError::DbError("Couldn't delete section."))?;
Ok(())
}

122
common/src/db/tokens.rs Normal file
View file

@ -0,0 +1,122 @@
//! Handles refresh token-related database operations.
use diesel::{insert_into, prelude::*, Insertable, PgConnection, Queryable};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::{
errors::{RbError, RbResult},
schema::{refresh_tokens, refresh_tokens::dsl::*},
};
/// A refresh token as stored in the database
#[derive(Queryable, Serialize)]
pub struct RefreshToken
{
pub token: Vec<u8>,
pub user_id: Uuid,
pub expires_at: chrono::NaiveDateTime,
pub last_used_at: Option<chrono::NaiveDateTime>,
}
/// A new refresh token to be added into the database
#[derive(Deserialize, Insertable)]
#[table_name = "refresh_tokens"]
pub struct NewRefreshToken
{
pub token: Vec<u8>,
pub user_id: Uuid,
pub expires_at: chrono::NaiveDateTime,
}
#[derive(Deserialize, AsChangeset)]
#[table_name = "refresh_tokens"]
pub struct PatchRefreshToken
{
pub expires_at: Option<chrono::NaiveDateTime>,
pub last_used_at: Option<chrono::NaiveDateTime>,
}
pub fn get(conn: &PgConnection, offset_: u32, limit_: u32) -> RbResult<Vec<RefreshToken>>
{
Ok(refresh_tokens
.offset(offset_.into())
.limit(limit_.into())
.load(conn)
.map_err(|_| RbError::DbError("Couldn't query tokens."))?)
}
pub fn create(conn: &PgConnection, new_token: &NewRefreshToken) -> RbResult<RefreshToken>
{
Ok(insert_into(refresh_tokens)
.values(new_token)
.get_result(conn)
.map_err(|_| RbError::DbError("Couldn't insert refresh token."))?)
// TODO check for conflict?
}
pub fn update(
conn: &PgConnection,
token_: &[u8],
patch_token: &PatchRefreshToken,
) -> RbResult<RefreshToken>
{
Ok(diesel::update(refresh_tokens.filter(token.eq(token_)))
.set(patch_token)
.get_result(conn)
.map_err(|_| RbError::DbError("Couldn't update token."))?)
}
pub fn delete(conn: &PgConnection, token_: &[u8]) -> RbResult<()>
{
diesel::delete(refresh_tokens.filter(token.eq(token_)))
.execute(conn)
.map_err(|_| RbError::DbError("Couldn't delete token."))?;
Ok(())
}
/// Returns the token & user data associated with the given refresh token value.
///
/// # Arguments
///
/// * `conn` - database connection to use
/// * `token_val` - token value to search for
pub fn find_with_user(
conn: &PgConnection,
token_: &[u8],
) -> Option<(RefreshToken, super::users::User)>
{
// TODO actually check for errors here
refresh_tokens
.inner_join(crate::schema::users::dsl::users)
.filter(token.eq(token_))
.first::<(RefreshToken, super::users::User)>(conn)
.map_err(|_| RbError::DbError("Couldn't get refresh token & user."))
.ok()
}
/// Updates a token's `last_used_at` column value.
///
/// # Arguments
///
/// * `conn` - database connection to use
/// * `token_` - value of the refresh token to update
/// * `last_used_at_` - date value to update column with
///
/// **NOTE**: argument names use trailing underscores as to not conflict with Diesel's imported dsl
/// names.
pub fn update_last_used_at(
conn: &PgConnection,
token_: &[u8],
last_used_at_: chrono::NaiveDateTime,
) -> RbResult<()>
{
diesel::update(refresh_tokens.filter(token.eq(token_)))
.set(last_used_at.eq(last_used_at_))
.execute(conn)
.map_err(|_| RbError::DbError("Couldn't update last_used_at."))?;
Ok(())
}

131
common/src/db/users.rs Normal file
View file

@ -0,0 +1,131 @@
use diesel::{prelude::*, AsChangeset, Insertable, Queryable};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::{
errors::{RbError, RbResult},
schema::{users, users::dsl::*},
};
#[derive(Queryable, Serialize)]
pub struct User
{
pub id: Uuid,
pub username: String,
#[serde(skip_serializing)]
pub password: String,
pub blocked: bool,
pub admin: bool,
}
#[derive(Insertable, Deserialize)]
#[table_name = "users"]
pub struct NewUser
{
pub username: String,
pub password: String,
pub admin: bool,
}
#[derive(Deserialize, AsChangeset)]
#[table_name = "users"]
#[serde(rename_all = "camelCase")]
pub struct PatchSection
{
username: Option<String>,
admin: Option<bool>,
}
pub fn get(conn: &PgConnection, offset_: u32, limit_: u32) -> RbResult<Vec<User>>
{
Ok(users
.offset(offset_.into())
.limit(limit_.into())
.load(conn)
.map_err(|_| RbError::DbError("Couldn't query users."))?)
}
pub fn find(conn: &PgConnection, user_id: Uuid) -> Option<User>
{
users.find(user_id).first::<User>(conn).ok()
}
pub fn find_by_username(conn: &PgConnection, username_: &str) -> RbResult<User>
{
Ok(users
.filter(username.eq(username_))
.first::<User>(conn)
.map_err(|_| RbError::DbError("Couldn't find users by username."))?)
}
/// Insert a new user into the database
///
/// # Arguments
///
/// * `conn` - database connection to use
/// * `new_user` - user to insert
pub fn create(conn: &PgConnection, new_user: &NewUser) -> RbResult<()>
{
let count = diesel::insert_into(users)
.values(new_user)
.execute(conn)
.map_err(|_| RbError::DbError("Couldn't create user."))?;
if count == 0 {
return Err(RbError::UMDuplicateUser);
}
Ok(())
}
/// Either create a new user or update an existing one on conflict.
///
/// # Arguments
///
/// * `conn` - database connection to use
/// * `new_user` - user to insert/update
// pub fn create_or_update(conn: &PgConnection, new_user: &NewUser) -> RbResult<()>
// {
// diesel::insert_into(users)
// .values(new_user)
// .on_conflict(username)
// .do_update()
// .set(new_user)
// .execute(conn)
// .map_err(|_| RbError::DbError("Couldn't create or update user."))?;
// Ok(())
// }
/// Delete the user with the given ID.
///
/// # Arguments
///
/// `conn` - database connection to use
/// `user_id` - ID of user to delete
pub fn delete(conn: &PgConnection, user_id: Uuid) -> RbResult<()>
{
diesel::delete(users.filter(id.eq(user_id)))
.execute(conn)
.map_err(|_| RbError::DbError("Couldn't delete user."))?;
Ok(())
}
/// Block a user given an ID.
/// In practice, this means updating the user's entry so that the `blocked` column is set to
/// `true`.
///
/// # Arguments
///
/// `conn` - database connection to use
/// `user_id` - ID of user to block
pub fn block(conn: &PgConnection, user_id: Uuid) -> RbResult<()>
{
diesel::update(users.filter(id.eq(user_id)))
.set(blocked.eq(true))
.execute(conn)
.map_err(|_| RbError::DbError("Couldn't block user."))?;
Ok(())
}

94
common/src/errors.rs Normal file
View file

@ -0,0 +1,94 @@
use rocket::{
http::Status,
request::Request,
response::{self, Responder},
serde::json::json,
};
#[derive(Debug)]
pub enum RbError
{
AuthUnknownUser,
AuthBlockedUser,
AuthInvalidPassword,
AuthUnauthorized,
AuthTokenExpired,
AuthRefreshTokenExpired,
AuthInvalidRefreshToken,
AuthDuplicateRefreshToken,
AuthMissingHeader,
// UM = User Management
UMDuplicateUser,
UMUnknownUser,
DbError(&'static str),
Custom(&'static str),
}
impl RbError
{
pub fn status(&self) -> Status
{
// Every entry gets its own line for easy editing later when needed
match self {
RbError::AuthUnknownUser => Status::NotFound,
RbError::AuthBlockedUser => Status::Forbidden,
RbError::AuthInvalidPassword => Status::Unauthorized,
RbError::AuthUnauthorized => Status::Unauthorized,
RbError::AuthTokenExpired => Status::Unauthorized,
RbError::AuthRefreshTokenExpired => Status::Unauthorized,
RbError::AuthInvalidRefreshToken => Status::Unauthorized,
RbError::AuthDuplicateRefreshToken => Status::Unauthorized,
RbError::AuthMissingHeader => Status::BadRequest,
RbError::UMDuplicateUser => Status::Conflict,
RbError::Custom(_) => Status::InternalServerError,
_ => Status::InternalServerError,
}
}
pub fn message(&self) -> &'static str
{
match self {
RbError::AuthUnknownUser => "This user doesn't exist.",
RbError::AuthBlockedUser => "This user is blocked.",
RbError::AuthInvalidPassword => "Invalid credentials.",
RbError::AuthUnauthorized => "You are not authorized to access this resource.",
RbError::AuthTokenExpired => "This token is not valid anymore.",
RbError::AuthRefreshTokenExpired => "This refresh token is not valid anymore.",
RbError::AuthInvalidRefreshToken => "This refresh token is not valid.",
RbError::AuthDuplicateRefreshToken => {
"This refresh token has already been used. The user has been blocked."
},
RbError::AuthMissingHeader => "Missing Authorization header.",
RbError::UMDuplicateUser => "This user already exists.",
RbError::Custom(message) => message,
_ => "",
}
}
}
impl<'r> Responder<'r, 'static> for RbError
{
fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static>
{
let status = self.status();
let content = json!({
"status": status.code,
"message": self.message(),
});
// TODO add status to response
content.respond_to(req)
}
}
/// Type alias for results that can return an RbError
pub type RbResult<T> = std::result::Result<T, RbError>;
/// Type alias for optional results that can fail & return an RbError
pub type RbOption<T> = RbResult<Option<T>>;

115
common/src/guards.rs Normal file
View file

@ -0,0 +1,115 @@
use hmac::{Hmac, NewMac};
use jwt::VerifyWithKey;
use rocket::{
http::Status,
outcome::try_outcome,
request::{FromRequest, Outcome, Request},
State,
};
use sha2::Sha256;
use crate::{auth::jwt::Claims, errors::RbError, RbConfig};
/// Extracts an "Authorization: Bearer" string from the headers.
pub struct Bearer<'a>(&'a str);
#[rocket::async_trait]
impl<'r> FromRequest<'r> for Bearer<'r>
{
type Error = crate::errors::RbError;
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error>
{
// If the header isn't present, just forward to the next route
let header = match req.headers().get_one("Authorization") {
None => return Outcome::Forward(()),
Some(val) => val,
};
if header.starts_with("Bearer ") {
match header.get(7..) {
Some(s) => Outcome::Success(Self(s)),
None => Outcome::Failure((Status::Unauthorized, Self::Error::AuthUnauthorized)),
}
} else {
Outcome::Forward(())
}
}
}
/// Verifies the provided JWT is valid.
pub struct Jwt(Claims);
#[rocket::async_trait]
impl<'r> FromRequest<'r> for Jwt
{
type Error = RbError;
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error>
{
let bearer = try_outcome!(req.guard::<Bearer>().await).0;
let config = try_outcome!(req.guard::<&State<RbConfig>>().await.map_failure(|_| (
Status::InternalServerError,
RbError::Custom("Couldn't get config guard.")
)));
let key: Hmac<Sha256> = match Hmac::new_from_slice(&config.jwt.key.as_bytes()) {
Ok(key) => key,
Err(_) => {
return Outcome::Failure((
Status::InternalServerError,
Self::Error::Custom("Failed to do Hmac thing."),
))
},
};
// Verify token using key
match bearer.verify_with_key(&key) {
Ok(claims) => Outcome::Success(Self(claims)),
Err(_) => {
return Outcome::Failure((Status::Unauthorized, Self::Error::AuthUnauthorized))
},
}
}
}
/// Verifies the JWT has not expired.
pub struct User(Claims);
#[rocket::async_trait]
impl<'r> FromRequest<'r> for User
{
type Error = crate::errors::RbError;
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error>
{
let claims = try_outcome!(req.guard::<Jwt>().await).0;
// Verify key hasn't yet expired
if chrono::Utc::now().timestamp() > claims.exp {
Outcome::Failure((Status::Forbidden, Self::Error::AuthTokenExpired))
} else {
Outcome::Success(Self(claims))
}
}
}
/// Verifies the JWT belongs to an admin.
pub struct Admin(Claims);
#[rocket::async_trait]
impl<'r> FromRequest<'r> for Admin
{
type Error = crate::errors::RbError;
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error>
{
let user = try_outcome!(req.guard::<User>().await).0;
if user.admin {
Outcome::Success(Self(user))
} else {
Outcome::Failure((Status::Unauthorized, RbError::AuthUnauthorized))
}
}
}

8
common/src/lib.rs Normal file
View file

@ -0,0 +1,8 @@
#[macro_use]
extern crate diesel;
pub mod auth;
pub mod db;
pub mod errors;
pub mod guards;
pub(crate) mod schema;

44
common/src/schema.rs Normal file
View file

@ -0,0 +1,44 @@
table! {
posts (id) {
id -> Uuid,
section_id -> Uuid,
title -> Nullable<Varchar>,
publish_date -> Date,
content -> Text,
}
}
table! {
refresh_tokens (token) {
token -> Bytea,
user_id -> Uuid,
expires_at -> Timestamp,
last_used_at -> Nullable<Timestamp>,
}
}
table! {
sections (id) {
id -> Uuid,
title -> Varchar,
shortname -> Varchar,
description -> Nullable<Text>,
is_default -> Bool,
has_titles -> Bool,
}
}
table! {
users (id) {
id -> Uuid,
username -> Varchar,
password -> Text,
blocked -> Bool,
admin -> Bool,
}
}
joinable!(posts -> sections (section_id));
joinable!(refresh_tokens -> users (user_id));
allow_tables_to_appear_in_same_query!(posts, refresh_tokens, sections, users,);

71
common/tests/admin.py Normal file
View file

@ -0,0 +1,71 @@
import requests
class RbClient:
def __init__(self, username = "admin", password = "password", base_url = "http://localhost:8000/api"):
self.username = username
self.password = password
self.base_url = base_url
self.jwt = None
self.refresh_token = None
def _login(self):
r = requests.post(f"{self.base_url}/auth/login", json={
"username": self.username,
"password": self.password,
})
if r.status_code != 200:
print(r.text)
raise Exception("Couldn't login")
res = r.json()
self.jwt = res["token"]
self.refresh_token = res["refreshToken"]
def _refresh(self):
r = requests.post(f"{self.base_url}/auth/refresh", json={"refreshToken": self.refresh_token})
if r.status_code != 200:
raise Exception("Couldn't refresh")
res = r.json()
self.jwt = res["token"]
self.refresh_token = res["refreshToken"]
def _request(self, type_, url, retry=2, *args, **kwargs):
if self.jwt:
headers = kwargs.get("headers", {})
headers["Authorization"] = f"Bearer {self.jwt}"
kwargs["headers"] = headers
print(kwargs["headers"])
r = requests.request(type_, url, *args, **kwargs)
if r.status_code != 200 and retry > 0:
if self.refresh_token:
self._refresh()
else:
self._login()
r = self._request(type_, url, *args, **kwargs, retry=retry - 1)
return r
def get(self, url, *args, **kwargs):
return self._request("GET", f"{self.base_url}{url}", *args, **kwargs)
def post(self, url, *args, **kwargs):
return self._request("POST", f"{self.base_url}{url}", *args, **kwargs)
if __name__ == "__main__":
client = RbClient()
# print(client.get("/admin/users").json())
client.post("/sections", json={
"title": "this is a title"
})