From 1b5134dfe29d96befff035d76b8996973abc7c90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa?= Date: Tue, 18 Dec 2018 18:52:58 +0100 Subject: [PATCH] Fixed delete user when 2FA is enabled, implemented delete user for admin panel, and the front-end part for invite user. Secured admin panel behind a configurable token. --- .env | 14 +++- src/api/admin.rs | 49 ++++++------ src/db/models/two_factor.rs | 10 ++- src/db/models/user.rs | 3 +- src/main.rs | 4 +- src/static/admin.html | 145 ++++++++++++++++++++++++++---------- 6 files changed, 154 insertions(+), 71 deletions(-) diff --git a/.env b/.env index 6e1ab169..51bb9e0f 100644 --- a/.env +++ b/.env @@ -34,15 +34,23 @@ ## It's recommended to also set 'ROCKET_CLI_COLORS=off' # LOG_FILE=/path/to/log -## Controls if new users can register -# SIGNUPS_ALLOWED=true - ## Use a local favicon extractor ## Set to false to use bitwarden's official icon servers ## Set to true to use the local version, which is not as smart, ## but it doesn't send the cipher domains to bitwarden's servers # LOCAL_ICON_EXTRACTOR=false +## Controls if new users can register +# SIGNUPS_ALLOWED=true + +## Token for the admin interface, preferably use a long random string +## One option is to use 'openssl rand -base64 48' +## If not set, the admin panel is disabled +# ADMIN_TOKEN=Vy2VyYTTsKPv8W5aEOWUbB/Bt3DEKePbHmI4m9VcemUMS2rEviDowNAFqYi1xjmp + +## Invitations org admins to invite users, even when signups are disabled +# INVITATIONS_ALLOWED=true + ## Controls the PBBKDF password iterations to apply on the server ## The change only applies when the password is changed # PASSWORD_ITERATIONS=100000 diff --git a/src/api/admin.rs b/src/api/admin.rs index ab5b5610..54cf0eb0 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -1,20 +1,17 @@ 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::api::{EmptyResult, JsonResult, JsonUpcase}; - -use rocket::{Route, Outcome}; -use rocket::request::{self, Request, FromRequest}; +use rocket::request::{self, FromRequest, Request}; +use rocket::{Outcome, Route}; pub fn routes() -> Vec { - routes![ - get_users, - invite_user, - delete_user, - ] + routes![get_users, invite_user, delete_user] } #[derive(Deserialize, Debug)] @@ -25,14 +22,14 @@ struct InviteData { #[get("/users")] fn get_users(_token: AdminToken, conn: DbConn) -> JsonResult { - let users = User::get_all(&conn); + let users = User::get_all(&conn); let users_json: Vec = users.iter().map(|u| u.to_json(&conn)).collect(); - + Ok(Json(Value::Array(users_json))) } -#[post("/users", data="")] -fn invite_user(data: JsonUpcase, _token: AdminToken, conn: DbConn) -> EmptyResult { +#[post("/invite", data = "")] +fn invite_user(data: JsonUpcase, _token: AdminToken, conn: DbConn) -> JsonResult { let data: InviteData = data.into_inner().data; if User::find_by_mail(&data.Email, &conn).is_some() { @@ -42,30 +39,30 @@ fn invite_user(data: JsonUpcase, _token: AdminToken, conn: DbConn) - err!("Unimplemented") } -#[delete("/users/")] -fn delete_user(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult { - let _user = match User::find_by_uuid(&uuid, &conn) { +#[post("/users//delete")] +fn delete_user(uuid: String, _token: AdminToken, conn: DbConn) -> JsonResult { + let user = match User::find_by_uuid(&uuid, &conn) { Some(user) => user, - None => err!("User doesn't exist") + None => err!("User doesn't exist"), }; - // TODO: Enable this once we have a more secure auth method - err!("Unimplemented") - /* match user.delete(&conn) { - Ok(_) => Ok(()), - Err(e) => err!("Error deleting user", e) + Ok(_) => Ok(Json(json!({}))), + Err(e) => err!("Error deleting user", e), } - */ } - pub struct AdminToken {} impl<'a, 'r> FromRequest<'a, 'r> for AdminToken { type Error = &'static str; fn from_request(request: &'a Request<'r>) -> request::Outcome { + let config_token = match CONFIG.admin_token.as_ref() { + Some(token) => token, + None => err_handler!("Admin panel is disabled"), + }; + // Get access_token let access_token: &str = match request.headers().get_one("Authorization") { Some(a) => match a.rsplit("Bearer ").next() { @@ -81,10 +78,10 @@ impl<'a, 'r> FromRequest<'a, 'r> for AdminToken { // Option 2a: Send it to admin email, like upstream // Option 2b: Print in console or save to data dir, so admin can check - if access_token != "token123" { + if access_token != config_token { err_handler!("Invalid admin token") } Outcome::Success(AdminToken {}) } -} \ No newline at end of file +} diff --git a/src/db/models/two_factor.rs b/src/db/models/two_factor.rs index 1e2c9a57..c3945aab 100644 --- a/src/db/models/two_factor.rs +++ b/src/db/models/two_factor.rs @@ -107,4 +107,12 @@ impl TwoFactor { .filter(twofactor::type_.eq(type_)) .first::(&**conn).ok() } -} \ No newline at end of file + + pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> QueryResult { + diesel::delete( + twofactor::table.filter( + twofactor::user_uuid.eq(user_uuid) + ) + ).execute(&**conn) + } +} diff --git a/src/db/models/user.rs b/src/db/models/user.rs index 8f509997..a581c76e 100644 --- a/src/db/models/user.rs +++ b/src/db/models/user.rs @@ -113,7 +113,7 @@ use diesel; use diesel::prelude::*; use crate::db::DbConn; use crate::db::schema::{users, invitations}; -use super::{Cipher, Folder, Device, UserOrganization, UserOrgType}; +use super::{Cipher, Folder, Device, UserOrganization, UserOrgType, TwoFactor}; /// Database methods impl User { @@ -168,6 +168,7 @@ impl User { Cipher::delete_all_by_user(&self.uuid, &*conn)?; Folder::delete_all_by_user(&self.uuid, &*conn)?; Device::delete_all_by_user(&self.uuid, &*conn)?; + TwoFactor::delete_all_by_user(&self.uuid, &*conn)?; Invitation::take(&self.email, &*conn); // Delete invitation if any diesel::delete(users::table.filter( diff --git a/src/main.rs b/src/main.rs index c8a95c33..83c2b398 100644 --- a/src/main.rs +++ b/src/main.rs @@ -272,6 +272,7 @@ pub struct Config { local_icon_extractor: bool, signups_allowed: bool, invitations_allowed: bool, + admin_token: Option, server_admin_email: Option, password_iterations: i32, show_password_hint: bool, @@ -325,7 +326,8 @@ impl Config { local_icon_extractor: get_env_or("LOCAL_ICON_EXTRACTOR", false), signups_allowed: get_env_or("SIGNUPS_ALLOWED", true), - server_admin_email: get_env("SERVER_ADMIN_EMAIL"), + admin_token: get_env("ADMIN_TOKEN"), + server_admin_email:None, // TODO: Delete this invitations_allowed: get_env_or("INVITATIONS_ALLOWED", true), password_iterations: get_env_or("PASSWORD_ITERATIONS", 100_000), show_password_hint: get_env_or("SHOW_PASSWORD_HINT", true), diff --git a/src/static/admin.html b/src/static/admin.html index a464b526..44d5069b 100644 --- a/src/static/admin.html +++ b/src/static/admin.html @@ -20,13 +20,12 @@ @@ -89,36 +144,48 @@
-
+
Authentication key needed to continue
Please provide it below: -
- - + + +
-
+
Registered Users
- Reload users + Reload users
-
- identicon +
+
+
Invite User
+ Email: + +
+ + +
+
+
+ +
+
- Full Name - Delete User + Full Name + Delete User
- Email + Email