Add authentication #1
|
@ -20,6 +20,7 @@ mod auth;
|
||||||
mod schema;
|
mod schema;
|
||||||
|
|
||||||
use models::errors::ErrorResponse;
|
use models::errors::ErrorResponse;
|
||||||
|
use models::users::UserInfo;
|
||||||
use auth::routes::*;
|
use auth::routes::*;
|
||||||
|
|
||||||
|
|
||||||
|
@ -27,7 +28,12 @@ use auth::routes::*;
|
||||||
pub struct DbConn(diesel::SqliteConnection);
|
pub struct DbConn(diesel::SqliteConnection);
|
||||||
|
|
||||||
#[get("/zones/<zone>/records")]
|
#[get("/zones/<zone>/records")]
|
||||||
fn get_zone_records(client: State<SyncClient<TcpClientConnection>>, zone: String) -> Result<Json<Vec<models::dns::Record>>, ErrorResponse<()>> {
|
fn get_zone_records(
|
||||||
|
client: State<SyncClient<TcpClientConnection>>,
|
||||||
|
_user_info: UserInfo,
|
||||||
|
zone: String
|
||||||
|
) -> Result<Json<Vec<models::dns::Record>>, ErrorResponse<()>> {
|
||||||
|
|
||||||
// TODO: Implement FromParam for Name
|
// TODO: Implement FromParam for Name
|
||||||
let name = Name::from_utf8(&zone).unwrap();
|
let name = Name::from_utf8(&zone).unwrap();
|
||||||
|
|
||||||
|
|
|
@ -54,8 +54,12 @@ impl<'r, T: Serialize> Responder<'r> for ErrorResponse<T> {
|
||||||
impl From<UserError> for ErrorResponse<()> {
|
impl From<UserError> for ErrorResponse<()> {
|
||||||
fn from(e: UserError) -> Self {
|
fn from(e: UserError) -> Self {
|
||||||
match e {
|
match e {
|
||||||
UserError::NotFound => ErrorResponse::new(Status::Unauthorized, "Incorrect password or username.".into()),
|
UserError::NotFound => ErrorResponse::new(Status::Unauthorized, "Provided credentials or token do not match any existing user".into()),
|
||||||
UserError::UserExists => ErrorResponse::new(Status::Conflict, "User already exists.".into()),
|
UserError::UserExists => ErrorResponse::new(Status::Conflict, "User already exists".into()),
|
||||||
|
UserError::BadToken => ErrorResponse::new(Status::BadRequest, "Malformed token".into()),
|
||||||
|
UserError::ExpiredToken => ErrorResponse::new(Status::Unauthorized, "The provided token has expired".into()),
|
||||||
|
UserError::MalformedHeader => ErrorResponse::new(Status::BadRequest, "Malformed authorization header".into()),
|
||||||
|
UserError::PermissionDenied => ErrorResponse::new(Status::Forbidden, "Bearer is not authorized to access the resource".into()),
|
||||||
UserError::DbError(e) => make_500(e),
|
UserError::DbError(e) => make_500(e),
|
||||||
UserError::PasswordError(e) => make_500(e)
|
UserError::PasswordError(e) => make_500(e)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,17 +2,29 @@ use uuid::Uuid;
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use diesel::result::Error as DieselError;
|
use diesel::result::Error as DieselError;
|
||||||
use diesel_derive_enum::DbEnum;
|
use diesel_derive_enum::DbEnum;
|
||||||
use rocket::request::{FromRequest, Request, Outcome};
|
use rocket::request::{FromRequest, Request, Outcome, State};
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
|
use rocket::http::Status;
|
||||||
use chrono::serde::ts_seconds;
|
use chrono::serde::ts_seconds;
|
||||||
use chrono::prelude::{DateTime, Utc};
|
use chrono::prelude::{DateTime, Utc};
|
||||||
use chrono::Duration;
|
use chrono::Duration;
|
||||||
// TODO: Maybe just use argon2 crate directly
|
// TODO: Maybe just use argon2 crate directly
|
||||||
use djangohashers::{make_password_with_algorithm, check_password, HasherError, Algorithm};
|
use djangohashers::{make_password_with_algorithm, check_password, HasherError, Algorithm};
|
||||||
use jsonwebtoken::{encode, Header, EncodingKey, errors::Result as JwtResult};
|
use jsonwebtoken::{
|
||||||
|
encode, decode,
|
||||||
|
Header, Validation,
|
||||||
|
Algorithm as JwtAlgorithm, EncodingKey, DecodingKey,
|
||||||
|
errors::Result as JwtResult,
|
||||||
|
errors::ErrorKind as JwtErrorKind
|
||||||
|
};
|
||||||
|
|
||||||
use crate::schema::*;
|
use crate::schema::*;
|
||||||
use crate::DbConn;
|
use crate::DbConn;
|
||||||
|
use crate::config::Config;
|
||||||
|
|
||||||
|
|
||||||
|
const BEARER: &'static str = "Bearer ";
|
||||||
|
const AUTH_HEADER: &'static str = "Authentication";
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, DbEnum, Deserialize)]
|
#[derive(Debug, DbEnum, Deserialize)]
|
||||||
|
@ -82,11 +94,44 @@ pub struct UserInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, 'r> FromRequest<'a, 'r> for UserInfo {
|
impl<'a, 'r> FromRequest<'a, 'r> for UserInfo {
|
||||||
type Error = ();
|
type Error = UserError;
|
||||||
|
|
||||||
fn from_request(request: &'a Request<'r>) -> Outcome<UserInfo, ()> {
|
fn from_request(request: &'a Request<'r>) -> Outcome<UserInfo, UserError> {
|
||||||
// LocalUser::get_user_by_uuid()
|
let auth_header = match request.headers().get_one(AUTH_HEADER) {
|
||||||
Outcome::Forward(())
|
None => return Outcome::Forward(()),
|
||||||
|
Some(auth_header) => auth_header,
|
||||||
|
};
|
||||||
|
|
||||||
|
let token = if auth_header.starts_with(BEARER) {
|
||||||
|
auth_header.trim_start_matches(BEARER)
|
||||||
|
} else {
|
||||||
|
return Outcome::Failure((Status::BadRequest, UserError::MalformedHeader))
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: Better error handling
|
||||||
|
let config = request.guard::<State<Config>>().unwrap();
|
||||||
|
let conn = request.guard::<DbConn>().unwrap();
|
||||||
|
|
||||||
|
let token_data = AuthClaims::decode(
|
||||||
|
token, &config.web_app.secret
|
||||||
|
).map_err(|e| match e.into_kind() {
|
||||||
|
JwtErrorKind::ExpiredSignature => (Status::Unauthorized, UserError::ExpiredToken),
|
||||||
|
_ => (Status::BadRequest, UserError::BadToken),
|
||||||
|
});
|
||||||
|
|
||||||
|
let token_data = match token_data {
|
||||||
|
Err(e) => return Outcome::Failure(e),
|
||||||
|
Ok(data) => data
|
||||||
|
};
|
||||||
|
|
||||||
|
let user_id = token_data.sub;
|
||||||
|
let user_info = match LocalUser::get_user_by_uuid(&conn, user_id) {
|
||||||
|
Err(UserError::NotFound) => return Outcome::Failure((Status::NotFound, UserError::NotFound)),
|
||||||
|
Err(e) => return Outcome::Failure((Status::InternalServerError, e)),
|
||||||
|
Ok(d) => d,
|
||||||
|
};
|
||||||
|
|
||||||
|
return Outcome::Success(user_info)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,6 +139,10 @@ impl<'a, 'r> FromRequest<'a, 'r> for UserInfo {
|
||||||
pub enum UserError {
|
pub enum UserError {
|
||||||
NotFound,
|
NotFound,
|
||||||
UserExists,
|
UserExists,
|
||||||
|
BadToken,
|
||||||
|
ExpiredToken,
|
||||||
|
MalformedHeader,
|
||||||
|
PermissionDenied,
|
||||||
DbError(DieselError),
|
DbError(DieselError),
|
||||||
PasswordError(HasherError),
|
PasswordError(HasherError),
|
||||||
}
|
}
|
||||||
|
@ -180,8 +229,19 @@ impl LocalUser {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_user_by_uuid(user_id: Uuid) -> Result<UserInfo, ()> {
|
pub fn get_user_by_uuid(conn: &DbConn, request_user_id: String) -> Result<UserInfo, UserError> {
|
||||||
unimplemented!()
|
use crate::schema::localuser::dsl::*;
|
||||||
|
use crate::schema::user::dsl::*;
|
||||||
|
|
||||||
|
let (client_user, client_localuser): (User, LocalUser) = user.inner_join(localuser)
|
||||||
|
.filter(id.eq(request_user_id))
|
||||||
|
.get_result(&**conn)?;
|
||||||
|
|
||||||
|
Ok(UserInfo {
|
||||||
|
id: client_user.id,
|
||||||
|
role: client_user.role,
|
||||||
|
username: client_localuser.username,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -200,7 +260,15 @@ impl AuthClaims {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn decode(token: &str, secret: &str) -> JwtResult<AuthClaims> {
|
||||||
|
decode::<AuthClaims>(
|
||||||
|
token,
|
||||||
|
&DecodingKey::from_secret(secret.as_ref()),
|
||||||
|
&Validation::new(JwtAlgorithm::HS256)
|
||||||
|
).and_then(|data| Ok(data.claims))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn encode(self, secret: &str) -> JwtResult<String> {
|
pub fn encode(self, secret: &str) -> JwtResult<String> {
|
||||||
encode(&Header::default(), &self, &EncodingKey::from_secret(secret.as_bytes()))
|
encode(&Header::default(), &self, &EncodingKey::from_secret(secret.as_ref()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue