Implement admin JWT cookie, separate JWT issuers for each type of token and migrate admin page to handlebars template

This commit is contained in:
Daniel García
2019-01-19 21:36:34 +01:00
parent 97aa407fe4
commit 834c847746
12 changed files with 366 additions and 319 deletions

View File

@ -1,18 +1,93 @@
use rocket_contrib::json::Json;
use serde_json::Value;
use crate::api::{JsonResult, JsonUpcase};
use crate::CONFIG;
use crate::db::models::*;
use crate::db::DbConn;
use crate::mail;
use rocket::request::{self, FromRequest, Request};
use rocket::http::{Cookie, Cookies};
use rocket::request::{self, FlashMessage, Form, FromRequest, Request};
use rocket::response::{content::Html, Flash, Redirect};
use rocket::{Outcome, Route};
use crate::api::{JsonResult, JsonUpcase};
use crate::auth::{decode_admin, encode_jwt, generate_admin_claims, ClientIp};
use crate::db::{models::*, DbConn};
use crate::error::Error;
use crate::mail;
use crate::CONFIG;
pub fn routes() -> Vec<Route> {
routes![get_users, invite_user, delete_user]
if CONFIG.admin_token.is_none() {
return Vec::new();
}
routes![admin_login, post_admin_login, admin_page, invite_user, delete_user]
}
#[derive(FromForm)]
struct LoginForm {
token: String,
}
const COOKIE_NAME: &'static str = "BWRS_ADMIN";
const ADMIN_PATH: &'static str = "/admin";
#[get("/", rank = 2)]
fn admin_login(flash: Option<FlashMessage>) -> Result<Html<String>, Error> {
// If there is an error, show it
let msg = flash
.map(|msg| format!("{}: {}", msg.name(), msg.msg()))
.unwrap_or_default();
let error = json!({ "error": msg });
// Return the page
let text = CONFIG.templates.render("admin/admin_login", &error)?;
Ok(Html(text))
}
#[post("/", data = "<data>")]
fn post_admin_login(data: Form<LoginForm>, mut cookies: Cookies, ip: ClientIp) -> Result<Redirect, Flash<Redirect>> {
let data = data.into_inner();
if !_validate_token(&data.token) {
error!("Invalid admin token. IP: {}", ip.ip);
Err(Flash::error(
Redirect::to(ADMIN_PATH),
"Invalid admin token, please try again.",
))
} else {
// If the token received is valid, generate JWT and save it as a cookie
let claims = generate_admin_claims();
let jwt = encode_jwt(&claims);
let cookie = Cookie::build(COOKIE_NAME, jwt)
.path(ADMIN_PATH)
.http_only(true)
.finish();
cookies.add(cookie);
Ok(Redirect::to(ADMIN_PATH))
}
}
fn _validate_token(token: &str) -> bool {
match CONFIG.admin_token.as_ref() {
None => false,
Some(t) => t == token,
}
}
#[derive(Serialize)]
struct AdminTemplateData {
users: Vec<Value>,
}
#[get("/", rank = 1)]
fn admin_page(_token: AdminToken, conn: DbConn) -> Result<Html<String>, Error> {
let users = User::get_all(&conn);
let users_json: Vec<Value> = users.iter().map(|u| u.to_json(&conn)).collect();
let data = AdminTemplateData { users: users_json };
let text = CONFIG.templates.render("admin/admin_page", &data)?;
Ok(Html(text))
}
#[derive(Deserialize, Debug)]
@ -21,14 +96,6 @@ struct InviteData {
Email: String,
}
#[get("/users")]
fn get_users(_token: AdminToken, conn: DbConn) -> JsonResult {
let users = User::get_all(&conn);
let users_json: Vec<Value> = users.iter().map(|u| u.to_json(&conn)).collect();
Ok(Json(Value::Array(users_json)))
}
#[post("/invite", data = "<data>")]
fn invite_user(data: JsonUpcase<InviteData>, _token: AdminToken, conn: DbConn) -> JsonResult {
let data: InviteData = data.into_inner().data;
@ -71,35 +138,23 @@ impl<'a, 'r> FromRequest<'a, 'r> for AdminToken {
type Error = &'static str;
fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
let config_token = match CONFIG.admin_token.as_ref() {
Some(token) => token,
None => err_handler!("Admin panel is disabled"),
let mut cookies = request.cookies();
let access_token = match cookies.get(COOKIE_NAME) {
Some(cookie) => cookie.value(),
None => return Outcome::Forward(()), // If there is no cookie, redirect to login
};
// Get access_token
let access_token: &str = match request.headers().get_one("Authorization") {
Some(a) => match a.rsplit("Bearer ").next() {
Some(split) => split,
None => err_handler!("No access token provided"),
},
None => err_handler!("No access token provided"),
};
// TODO: What authentication to use?
// Option 1: Make it a config option
// Option 2: Generate random token, and
// Option 2a: Send it to admin email, like upstream
// Option 2b: Print in console or save to data dir, so admin can check
use crate::auth::ClientIp;
let ip = match request.guard::<ClientIp>() {
Outcome::Success(ip) => ip,
Outcome::Success(ip) => ip.ip,
_ => err_handler!("Error getting Client IP"),
};
if access_token != config_token {
err_handler!("Invalid admin token", format!("IP: {}.", ip.ip))
if decode_admin(access_token).is_err() {
// Remove admin cookie
cookies.remove(Cookie::named(COOKIE_NAME));
error!("Invalid or expired admin JWT. IP: {}.", ip);
return Outcome::Forward(());
}
Outcome::Success(AdminToken {})

View File

@ -4,7 +4,7 @@ use crate::db::models::*;
use crate::db::DbConn;
use crate::api::{EmptyResult, JsonResult, JsonUpcase, Notify, NumberOrString, PasswordData, UpdateType};
use crate::auth::{decode_invite_jwt, Headers, InviteJWTClaims};
use crate::auth::{decode_invite, Headers};
use crate::mail;
use crate::CONFIG;
@ -66,7 +66,7 @@ fn register(data: JsonUpcase<RegisterData>, conn: DbConn) -> EmptyResult {
}
if let Some(token) = data.Token {
let claims: InviteJWTClaims = decode_invite_jwt(&token)?;
let claims = decode_invite(&token)?;
if claims.email == data.Email {
user
} else {

View File

@ -7,7 +7,7 @@ use crate::db::DbConn;
use crate::CONFIG;
use crate::api::{EmptyResult, JsonResult, JsonUpcase, Notify, NumberOrString, PasswordData, UpdateType};
use crate::auth::{decode_invite_jwt, AdminHeaders, Headers, InviteJWTClaims, OwnerHeaders};
use crate::auth::{decode_invite, AdminHeaders, Headers, OwnerHeaders};
use crate::mail;
@ -582,7 +582,7 @@ fn accept_invite(_org_id: String, _org_user_id: String, data: JsonUpcase<AcceptD
// The web-vault passes org_id and org_user_id in the URL, but we are just reading them from the JWT instead
let data: AcceptData = data.into_inner().data;
let token = &data.Token;
let claims: InviteJWTClaims = decode_invite_jwt(&token)?;
let claims = decode_invite(&token)?;
match User::find_by_mail(&claims.email, &conn) {
Some(_) => {

View File

@ -135,7 +135,7 @@ impl Handler for WSHandler {
// Validate the user
use crate::auth;
let claims = match auth::decode_jwt(access_token) {
let claims = match auth::decode_login(access_token) {
Ok(claims) => claims,
Err(_) => return Err(ws::Error::new(ws::ErrorKind::Internal, "Invalid access token provided")),
};

View File

@ -2,18 +2,18 @@ use std::io;
use std::path::{Path, PathBuf};
use rocket::http::ContentType;
use rocket::request::Request;
use rocket::response::content::Content;
use rocket::response::{self, NamedFile, Responder};
use rocket::response::NamedFile;
use rocket::Route;
use rocket_contrib::json::Json;
use serde_json::Value;
use crate::CONFIG;
use crate::util::Cached;
pub fn routes() -> Vec<Route> {
if CONFIG.web_vault_enabled {
routes![web_index, app_id, web_files, admin_page, attachments, alive]
routes![web_index, app_id, web_files, attachments, alive]
} else {
routes![attachments, alive]
}
@ -43,52 +43,11 @@ fn app_id() -> Cached<Content<Json<Value>>> {
))
}
const ADMIN_PAGE: &'static str = include_str!("../static/admin.html");
use rocket::response::content::Html;
#[get("/admin")]
fn admin_page() -> Cached<Html<&'static str>> {
Cached::short(Html(ADMIN_PAGE))
}
/* // Use this during Admin page development
#[get("/admin")]
fn admin_page() -> Cached<io::Result<NamedFile>> {
Cached::short(NamedFile::open("src/static/admin.html"))
}
*/
#[get("/<p..>", rank = 1)] // Only match this if the other routes don't match
#[get("/<p..>", rank = 10)] // Only match this if the other routes don't match
fn web_files(p: PathBuf) -> Cached<io::Result<NamedFile>> {
Cached::long(NamedFile::open(Path::new(&CONFIG.web_vault_folder).join(p)))
}
struct Cached<R>(R, &'static str);
impl<R> Cached<R> {
fn long(r: R) -> Cached<R> {
// 7 days
Cached(r, "public, max-age=604800")
}
fn short(r: R) -> Cached<R> {
// 10 minutes
Cached(r, "public, max-age=600")
}
}
impl<'r, R: Responder<'r>> Responder<'r> for Cached<R> {
fn respond_to(self, req: &Request) -> response::Result<'r> {
match self.0.respond_to(req) {
Ok(mut res) => {
res.set_raw_header("Cache-Control", self.1);
Ok(res)
}
e @ Err(_) => e,
}
}
}
#[get("/attachments/<uuid>/<file..>")]
fn attachments(uuid: String, file: PathBuf) -> io::Result<NamedFile> {
NamedFile::open(Path::new(&CONFIG.attachments_folder).join(uuid).join(file))