vw_small

Hardened fork of Vaultwarden (https://github.com/dani-garcia/vaultwarden) with fewer features.
git clone https://git.philomathiclife.com/repos/vw_small
Log | Files | Refs | README

commit 5fecf09631bdba7674d04dca1bd8a2a3644d58c9
parent 9a8cae836b986516dd8cfdb660df958c7b6e1ed5
Author: Daniel GarcĂ­a <dani-garcia@users.noreply.github.com>
Date:   Tue, 18 Dec 2018 01:53:21 +0100

Initial version of admin panel, list users and reload user list works. No serious auth method yet, password is 'token123'

Diffstat:
Asrc/api/admin.rs | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/api/core/mod.rs | 2+-
Msrc/api/mod.rs | 2++
Msrc/api/web.rs | 7++++++-
Msrc/auth.rs | 2+-
Msrc/main.rs | 4+---
Asrc/static/admin.html | 128+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rsrc/api/core/global_domains.json -> src/static/global_domains.json | 0
8 files changed, 230 insertions(+), 6 deletions(-)

diff --git a/src/api/admin.rs b/src/api/admin.rs @@ -0,0 +1,90 @@ +use rocket_contrib::json::Json; +use serde_json::Value; + +use crate::db::models::*; +use crate::db::DbConn; + +use crate::api::{EmptyResult, JsonResult, JsonUpcase}; + +use rocket::{Route, Outcome}; +use rocket::request::{self, Request, FromRequest}; + +pub fn routes() -> Vec<Route> { + routes![ + get_users, + invite_user, + delete_user, + ] +} + +#[derive(Deserialize, Debug)] +#[allow(non_snake_case)] +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("/users", data="<data>")] +fn invite_user(data: JsonUpcase<InviteData>, _token: AdminToken, conn: DbConn) -> EmptyResult { + let data: InviteData = data.into_inner().data; + + if User::find_by_mail(&data.Email, &conn).is_some() { + err!("User already exists") + } + + err!("Unimplemented") +} + +#[delete("/users/<uuid>")] +fn delete_user(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult { + let _user = match User::find_by_uuid(&uuid, &conn) { + Some(user) => user, + 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) + } + */ +} + + +pub struct AdminToken {} + +impl<'a, 'r> FromRequest<'a, 'r> for AdminToken { + type Error = &'static str; + + fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> { + // 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 + + if access_token != "token123" { + err_handler!("Invalid admin token") + } + + Outcome::Success(AdminToken {}) + } +} +\ No newline at end of file diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs @@ -77,7 +77,7 @@ struct GlobalDomain { Excluded: bool, } -const GLOBAL_DOMAINS: &str = include_str!("global_domains.json"); +const GLOBAL_DOMAINS: &str = include_str!("../../static/global_domains.json"); #[get("/settings/domains")] fn get_eq_domains(headers: Headers) -> JsonResult { diff --git a/src/api/mod.rs b/src/api/mod.rs @@ -1,10 +1,12 @@ pub(crate) mod core; +mod admin; mod icons; mod identity; mod web; mod notifications; pub use self::core::routes as core_routes; +pub use self::admin::routes as admin_routes; pub use self::icons::routes as icons_routes; pub use self::identity::routes as identity_routes; pub use self::web::routes as web_routes; diff --git a/src/api/web.rs b/src/api/web.rs @@ -13,7 +13,7 @@ use crate::CONFIG; pub fn routes() -> Vec<Route> { if CONFIG.web_vault_enabled { - routes![web_index, app_id, web_files, attachments, alive] + routes![web_index, app_id, web_files, admin_page, attachments, alive] } else { routes![attachments, alive] } @@ -41,6 +41,11 @@ fn app_id() -> WebHeaders<Content<Json<Value>>> { })))) } +#[get("/admin")] +fn admin_page() -> WebHeaders<io::Result<NamedFile>> { + WebHeaders(NamedFile::open("src/static/admin.html")) // TODO: Change this to embed the page in the binary +} + #[get("/<p..>", rank = 1)] // Only match this if the other routes don't match fn web_files(p: PathBuf) -> WebHeaders<io::Result<NamedFile>> { WebHeaders(NamedFile::open(Path::new(&CONFIG.web_vault_folder).join(p))) diff --git a/src/auth.rs b/src/auth.rs @@ -174,7 +174,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for Headers { }; // Get access_token - let access_token: &str = match request.headers().get_one("Authorization") { + let access_token: &str = match headers.get_one("Authorization") { Some(a) => match a.rsplit("Bearer ").next() { Some(split) => split, None => err_handler!("No access token provided"), diff --git a/src/main.rs b/src/main.rs @@ -24,12 +24,10 @@ mod auth; mod mail; fn init_rocket() -> Rocket { - - // TODO: TO HIDE MOUNTING LOG, call ignite, set logging to disabled, call all the mounts, and then enable it again - rocket::ignite() .mount("/", api::web_routes()) .mount("/api", api::core_routes()) + .mount("/admin", api::admin_routes()) .mount("/identity", api::identity_routes()) .mount("/icons", api::icons_routes()) .mount("/notifications", api::notifications_routes()) diff --git a/src/static/admin.html b/src/static/admin.html @@ -0,0 +1,127 @@ +<!DOCTYPE html> +<html lang="en"> + +<head> + <meta http-equiv="content-type" content="text/html; charset=UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> + <meta name="description" content=""> + <meta name="author" content=""> + <title>Bitwarden_rs Admin Panel</title> + + <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.1.3/css/bootstrap.min.css" + integrity="sha256-eSi1q2PG6J7g7ib17yAaWMcrr5GrtohYChqibrV7PBE=" crossorigin="anonymous" /> + <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" + crossorigin="anonymous"></script> + <script src="https://cdnjs.cloudflare.com/ajax/libs/blueimp-md5/2.10.0/js/md5.js" integrity="sha256-tCQ/BldMlN2vWe5gAiNoNb5svoOgVUhlUgv7UjONKKQ=" + crossorigin="anonymous"></script> + <script src="https://cdnjs.cloudflare.com/ajax/libs/identicon.js/2.3.3/identicon.min.js" integrity="sha256-nYoL3nK/HA1e1pJvLwNPnpKuKG9q89VFX862r5aohmA=" + crossorigin="anonymous"></script> + + <style> + body { padding-top: 70px; } + img { width: 48px; height: 48px; } + #logo { width: 48px; height: 48px; } + </style> + + <script> + let key = null; + + function getIdenticon(email) { + const data = new Identicon(md5(email), { + size: 48, + format: 'svg' + }).toString(); + + return "data:image/svg+xml;base64," + data; + } + + function loadUsers() { + $("#users-list").empty(); + + $.ajax({ + type: "GET", + url: "/admin/users", + headers: { "Authorization": "Bearer " + key } + }).done(function (data) { + for (i in data) { + let user = data[i]; + let row = $("#tmp-user-row").clone(); + + row.attr("id", "user-row:" + user.Id); + row.find(".tmp-user-name").text(user.Name); + row.find(".tmp-user-mail").text(user.Email); + row.find(".tmp-user-icon").attr("src", getIdenticon(user.Email)) + + row.find(".tmp-user-del").on("click", function (e) { + alert("Not Implemented: Deleting UUID " + user.Id); + }); + + row.appendTo("#users-list"); + row.removeClass('d-none'); + } + }) + } + + $(window).on('load', function () { + key = new URLSearchParams(window.location.search).get('key'); + if (key) { + $("#no-key-form").addClass('d-none'); + loadUsers(); + } else { + $("#users-block").addClass('d-none'); + } + }); + </script> +</head> + +<body class="bg-light"> + <nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top shadow"> + <a class="navbar-brand" href="#">Bitwarden_rs Admin</a> + <div class="navbar-collapse"> + <ul class="navbar-nav"> + <li class="nav-item active"> + <a class="nav-link" href="#">Dashboard</a> + </li> + <li class="nav-item"> + <a class="nav-link" href="#">Other</a> + </li> + </ul> + </div> + </nav> + <main class="container"> + <div id="no-key-form" class="align-items-center p-3 mb-3 text-white-50 bg-danger rounded shadow"> + <div> + <h6 class="mb-0 text-white">Authentication key needed to continue</h6> + <small>Please provide it below:</small> + + <form class="form-inline" method="get"> + <input type="text" class="form-control mr-2" id="key" name="key" placeholder="Enter admin key"> + <button type="submit" class="btn btn-primary">Submit</button> + </form> + </div> + </div> + + <div id="users-block" class="my-3 p-3 bg-white rounded shadow"> + <h6 class="border-bottom pb-2 mb-0">Registered Users</h6> + + <div id="users-list"></div> + + <small class="d-block text-right mt-3"> + <a href="#" onclick="loadUsers();">Reload users</a> + </small> + </div> + + <div id="tmp-user-row" class="d-none media pt-3"> + <img src="#" alt="identicon" class="mr-2 rounded tmp-user-icon"> + <div class="media-body pb-3 mb-0 small border-bottom"> + <div class="d-flex justify-content-between"> + <strong class="tmp-user-name">Full Name</strong> + <a class="tmp-user-del mr-3" href="#">Delete User</a> + </div> + <span class="d-block tmp-user-mail">Email</span> + </div> + </div> + </main> +</body> + +</html> +\ No newline at end of file diff --git a/src/api/core/global_domains.json b/src/static/global_domains.json