Add anyhow, Improve Auth token code
Moved test helper code to its own module.
This commit is contained in:
7
Cargo.lock
generated
7
Cargo.lock
generated
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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
86
src/testhelpers.rs
Normal 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"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user