Add anyhow, Improve Auth token code

Moved test helper code to its own module.
This commit is contained in:
Cameron Cordes
2021-10-07 20:09:05 -04:00
parent e4dac64776
commit 2c50b4ae2f
7 changed files with 138 additions and 88 deletions

7
Cargo.lock generated
View File

@@ -372,6 +372,12 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "anyhow"
version = "1.0.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1"
[[package]] [[package]]
name = "async-trait" name = "async-trait"
version = "0.1.50" version = "0.1.50"
@@ -1156,6 +1162,7 @@ dependencies = [
"actix-rt", "actix-rt",
"actix-web", "actix-web",
"actix-web-prom", "actix-web-prom",
"anyhow",
"bcrypt", "bcrypt",
"chrono", "chrono",
"diesel", "diesel",

View File

@@ -35,3 +35,4 @@ env_logger="0.8"
actix-web-prom = "0.5.1" actix-web-prom = "0.5.1"
prometheus = "0.11" prometheus = "0.11"
lazy_static = "1.1" lazy_static = "1.1"
anyhow = "1.0"

View File

@@ -32,6 +32,7 @@ pub async fn login(
user_dao: web::Data<Box<dyn UserDao>>, user_dao: web::Data<Box<dyn UserDao>>,
) -> HttpResponse { ) -> HttpResponse {
debug!("Logging in: {}", creds.username); debug!("Logging in: {}", creds.username);
if let Some(user) = user_dao.get_user(&creds.username, &creds.password) { if let Some(user) = user_dao.get_user(&creds.username, &creds.password) {
let claims = Claims { let claims = Claims {
sub: user.id.to_string(), sub: user.id.to_string(),
@@ -43,6 +44,7 @@ pub async fn login(
&EncodingKey::from_secret(secret_key().as_bytes()), &EncodingKey::from_secret(secret_key().as_bytes()),
) )
.unwrap(); .unwrap();
HttpResponse::Ok().json(Token { token: &token }) HttpResponse::Ok().json(Token { token: &token })
} else { } else {
error!( error!(
@@ -56,7 +58,7 @@ pub async fn login(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::database::testhelpers::{BodyReader, TestUserDao}; use crate::testhelpers::{BodyReader, TestUserDao};
#[actix_rt::test] #[actix_rt::test]
async fn test_login_reports_200_when_user_exists() { async fn test_login_reports_200_when_user_exists() {

View File

@@ -35,7 +35,7 @@ impl FromStr for Claims {
let token = *(s.split("Bearer ").collect::<Vec<_>>().last().unwrap_or(&"")); let token = *(s.split("Bearer ").collect::<Vec<_>>().last().unwrap_or(&""));
match decode::<Claims>( match decode::<Claims>(
&token, token,
&DecodingKey::from_secret(secret_key().as_bytes()), &DecodingKey::from_secret(secret_key().as_bytes()),
&Validation::new(Algorithm::HS256), &Validation::new(Algorithm::HS256),
) { ) {
@@ -54,18 +54,27 @@ impl FromRequest for Claims {
type Config = (); type Config = ();
fn from_request(req: &HttpRequest, _payload: &mut dev::Payload) -> Self::Future { fn from_request(req: &HttpRequest, _payload: &mut dev::Payload) -> Self::Future {
let claims = match req.headers().get(header::AUTHORIZATION) { req.headers()
Some(header) => Claims::from_str(header.to_str().unwrap_or("")), .get(header::AUTHORIZATION)
None => Err(jsonwebtoken::errors::Error::from( .map_or_else(
jsonwebtoken::errors::ErrorKind::InvalidToken, || Err(anyhow!("No authorization header")),
)), |header| {
}; header
.to_str()
if let Ok(claims) = claims { .context("Unable to read Authorization header to string")
ok(claims) },
} else { )
.and_then(|header| {
Claims::from_str(header)
.with_context(|| format!("Unable to decode token from: {}", header))
})
.map_or_else(
|e| {
error!("{}", e);
err(ErrorUnauthorized("Bad token")) err(ErrorUnauthorized("Bad token"))
} },
ok,
)
} }
} }
@@ -156,4 +165,17 @@ mod tests {
} }
} }
} }
#[test]
fn test_junk_token_is_invalid() {
let err = Claims::from_str("uni-֍ՓՓՓՓՓՓՓՓՓՓՓՓՓՓՓ");
match err.unwrap_err().into_kind() {
ErrorKind::InvalidToken => assert!(true),
kind => {
println!("Unexpected error: {:?}", kind);
assert!(false)
}
}
}
} }

View File

@@ -8,7 +8,7 @@ use std::{
use crate::database::models::{Favorite, InsertFavorite, InsertUser, User}; use crate::database::models::{Favorite, InsertFavorite, InsertUser, User};
mod models; pub mod models;
mod schema; mod schema;
pub trait UserDao { pub trait UserDao {
@@ -141,7 +141,7 @@ impl FavoriteDao for SqliteFavoriteDao {
diesel::insert_into(favorites) diesel::insert_into(favorites)
.values(InsertFavorite { .values(InsertFavorite {
userid: &user_id, userid: &user_id,
path: &favorite_path, path: favorite_path,
}) })
.execute(connection) .execute(connection)
.map_err(|_| DbError::new(DbErrorKind::InsertError)) .map_err(|_| DbError::new(DbErrorKind::InsertError))
@@ -168,74 +168,3 @@ impl FavoriteDao for SqliteFavoriteDao {
.map_err(|_| DbError::new(DbErrorKind::QueryError)) .map_err(|_| DbError::new(DbErrorKind::QueryError))
} }
} }
#[cfg(test)]
pub mod testhelpers {
use actix_web::dev::{Body, ResponseBody};
use super::{models::User, UserDao};
use std::cell::RefCell;
use std::option::Option;
pub struct TestUserDao {
pub user_map: RefCell<Vec<User>>,
}
impl TestUserDao {
pub fn new() -> Self {
Self {
user_map: RefCell::new(Vec::new()),
}
}
}
impl UserDao for TestUserDao {
fn create_user(&self, username: &str, password: &str) -> Option<User> {
let u = User {
id: (self.user_map.borrow().len() + 1) as i32,
username: username.to_string(),
password: password.to_string(),
};
self.user_map.borrow_mut().push(u.clone());
Some(u)
}
fn get_user(&self, user: &str, pass: &str) -> Option<User> {
match self
.user_map
.borrow()
.iter()
.find(|&u| u.username == user && u.password == pass)
{
Some(u) => {
let copy = (*u).clone();
Some(copy)
}
None => None,
}
}
fn user_exists(&self, user: &str) -> bool {
self.user_map
.borrow()
.iter()
.find(|&u| u.username == user)
.is_some()
}
}
pub trait BodyReader {
fn read_to_str(&self) -> &str;
}
impl BodyReader for ResponseBody<Body> {
fn read_to_str(&self) -> &str {
match self {
ResponseBody::Body(Body::Bytes(ref b)) => std::str::from_utf8(b).unwrap(),
_ => panic!("Unknown response body"),
}
}
}
}

View File

@@ -42,6 +42,9 @@ mod database;
mod files; mod files;
mod video; mod video;
#[cfg(test)]
mod testhelpers;
lazy_static! { lazy_static! {
static ref IMAGE_GAUGE: IntGauge = IntGauge::new( static ref IMAGE_GAUGE: IntGauge = IntGauge::new(
"imageserver_image_total", "imageserver_image_total",
@@ -190,7 +193,7 @@ async fn generate_video(
let filename = name.to_str().expect("Filename should convert to string"); let filename = name.to_str().expect("Filename should convert to string");
let playlist = format!("tmp/{}.m3u8", filename); let playlist = format!("tmp/{}.m3u8", filename);
if let Some(path) = is_valid_path(&body.path) { if let Some(path) = is_valid_path(&body.path) {
if let Ok(child) = create_playlist(&path.to_str().unwrap(), &playlist).await { if let Ok(child) = create_playlist(path.to_str().unwrap(), &playlist).await {
data.stream_manager data.stream_manager
.do_send(ProcessMessage(playlist.clone(), child)); .do_send(ProcessMessage(playlist.clone(), child));
} }

86
src/testhelpers.rs Normal file
View File

@@ -0,0 +1,86 @@
use actix_web::dev::{Body, ResponseBody};
use serde::Deserialize;
use crate::database::{models::User, UserDao};
use std::cell::RefCell;
use std::option::Option;
pub struct TestUserDao {
pub user_map: RefCell<Vec<User>>,
}
impl TestUserDao {
pub fn new() -> Self {
Self {
user_map: RefCell::new(Vec::new()),
}
}
}
impl UserDao for TestUserDao {
fn create_user(&self, username: &str, password: &str) -> Option<User> {
let u = User {
id: (self.user_map.borrow().len() + 1) as i32,
username: username.to_string(),
password: password.to_string(),
};
self.user_map.borrow_mut().push(u.clone());
Some(u)
}
fn get_user(&self, user: &str, pass: &str) -> Option<User> {
match self
.user_map
.borrow()
.iter()
.find(|&u| u.username == user && u.password == pass)
{
Some(u) => {
let copy = (*u).clone();
Some(copy)
}
None => None,
}
}
fn user_exists(&self, user: &str) -> bool {
self.user_map
.borrow()
.iter()
.find(|&u| u.username == user)
.is_some()
}
}
pub trait BodyReader {
fn read_to_str(&self) -> &str;
}
impl BodyReader for ResponseBody<Body> {
fn read_to_str(&self) -> &str {
match self {
ResponseBody::Body(Body::Bytes(ref b)) => std::str::from_utf8(b).unwrap(),
_ => panic!("Unknown response body"),
}
}
}
pub trait TypedBodyReader<'a, T>
where
T: Deserialize<'a>,
{
fn read_body(&'a self) -> T;
}
impl<'a, T: Deserialize<'a>> TypedBodyReader<'a, T> for ResponseBody<Body> {
fn read_body(&'a self) -> T {
match self {
ResponseBody::Body(Body::Bytes(ref b)) => {
serde_json::from_str(std::str::from_utf8(b).unwrap()).unwrap()
}
_ => panic!("Unknown response body"),
}
}
}