Merge branch 'main' into future-web-vault

This commit is contained in:
Mathijs van Veluw
2021-07-15 21:43:23 +02:00
committed by GitHub
18 changed files with 256 additions and 99 deletions

View File

@@ -580,24 +580,45 @@ struct PasswordHintData {
#[post("/accounts/password-hint", data = "<data>")]
fn password_hint(data: JsonUpcase<PasswordHintData>, conn: DbConn) -> EmptyResult {
let data: PasswordHintData = data.into_inner().data;
let hint = match User::find_by_mail(&data.Email, &conn) {
Some(user) => user.password_hint,
None => return Ok(()),
};
if CONFIG.mail_enabled() {
mail::send_password_hint(&data.Email, hint)?;
} else if CONFIG.show_password_hint() {
if let Some(hint) = hint {
err!(format!("Your password hint is: {}", &hint));
} else {
err!("Sorry, you have no password hint...");
}
if !CONFIG.mail_enabled() && !CONFIG.show_password_hint() {
err!("This server is not configured to provide password hints.");
}
Ok(())
const NO_HINT: &str = "Sorry, you have no password hint...";
let data: PasswordHintData = data.into_inner().data;
let email = &data.Email;
match User::find_by_mail(email, &conn) {
None => {
// To prevent user enumeration, act as if the user exists.
if CONFIG.mail_enabled() {
// There is still a timing side channel here in that the code
// paths that send mail take noticeably longer than ones that
// don't. Add a randomized sleep to mitigate this somewhat.
use rand::{thread_rng, Rng};
let mut rng = thread_rng();
let base = 1000;
let delta: i32 = 100;
let sleep_ms = (base + rng.gen_range(-delta..=delta)) as u64;
std::thread::sleep(std::time::Duration::from_millis(sleep_ms));
Ok(())
} else {
err!(NO_HINT);
}
}
Some(user) => {
let hint: Option<String> = user.password_hint;
if CONFIG.mail_enabled() {
mail::send_password_hint(email, hint)?;
Ok(())
} else if let Some(hint) = hint {
err!(format!("Your password hint is: {}", hint));
} else {
err!(NO_HINT);
}
}
}
}
#[derive(Deserialize)]

View File

@@ -871,7 +871,7 @@ fn save_attachment(
Some(limit_kb) => {
let left = (limit_kb * 1024) - Attachment::size_by_user(user_uuid, conn) + size_adjust;
if left <= 0 {
err_discard!("Attachment size limit reached! Delete some files to open space", data)
err_discard!("Attachment storage limit reached! Delete some attachments to free up space", data)
}
Some(left as u64)
}
@@ -883,7 +883,7 @@ fn save_attachment(
Some(limit_kb) => {
let left = (limit_kb * 1024) - Attachment::size_by_org(org_uuid, conn) + size_adjust;
if left <= 0 {
err_discard!("Attachment size limit reached! Delete some files to open space", data)
err_discard!("Attachment storage limit reached! Delete some attachments to free up space", data)
}
Some(left as u64)
}
@@ -937,7 +937,7 @@ fn save_attachment(
return;
}
SaveResult::Partial(_, reason) => {
error = Some(format!("Attachment size limit exceeded with this file: {:?}", reason));
error = Some(format!("Attachment storage limit exceeded with this file: {:?}", reason));
return;
}
SaveResult::Error(e) => {

View File

@@ -687,6 +687,19 @@ fn accept_invite(_org_id: String, _org_user_id: String, data: JsonUpcase<AcceptD
err!("User already accepted the invitation")
}
let user_twofactor_disabled = TwoFactor::find_by_user(&user_org.user_uuid, &conn).is_empty();
let policy = OrgPolicyType::TwoFactorAuthentication as i32;
let org_twofactor_policy_enabled =
match OrgPolicy::find_by_org_and_type(&user_org.org_uuid, policy, &conn) {
Some(p) => p.enabled,
None => false,
};
if org_twofactor_policy_enabled && user_twofactor_disabled {
err!("You cannot join this organization until you enable two-step login on your user account.")
}
user_org.status = UserOrgStatus::Accepted as i32;
user_org.save(&conn)?;
}
@@ -1039,6 +1052,24 @@ fn put_policy(
None => err!("Invalid policy type"),
};
if pol_type_enum == OrgPolicyType::TwoFactorAuthentication && data.enabled {
let org_list = UserOrganization::find_by_org(&org_id, &conn);
for user_org in org_list.into_iter() {
let user_twofactor_disabled = TwoFactor::find_by_user(&user_org.user_uuid, &conn).is_empty();
if user_twofactor_disabled && user_org.atype < UserOrgType::Admin {
if CONFIG.mail_enabled() {
let org = Organization::find_by_uuid(&user_org.org_uuid, &conn).unwrap();
let user = User::find_by_uuid(&user_org.user_uuid, &conn).unwrap();
mail::send_2fa_removed_from_org(&user.email, &org.name)?;
}
user_org.delete(&conn)?;
}
}
}
let mut policy = match OrgPolicy::find_by_org_and_type(&org_id, pol_type, &conn) {
Some(p) => p,
None => OrgPolicy::new(org_id, pol_type_enum, "{}".to_string()),

View File

@@ -10,6 +10,7 @@ use crate::{
api::{ApiResult, EmptyResult, JsonResult, JsonUpcase, Notify, UpdateType},
auth::{Headers, Host},
db::{models::*, DbConn, DbPool},
util::SafeString,
CONFIG,
};
@@ -173,7 +174,7 @@ fn post_send_file(data: Data, content_type: &ContentType, headers: Headers, conn
Some(limit_kb) => {
let left = (limit_kb * 1024) - Attachment::size_by_user(&headers.user.uuid, &conn);
if left <= 0 {
err!("Attachment size limit reached! Delete some files to open space")
err!("Attachment storage limit reached! Delete some attachments to free up space")
}
std::cmp::Ord::max(left as u64, SIZE_525_MB)
}
@@ -205,7 +206,7 @@ fn post_send_file(data: Data, content_type: &ContentType, headers: Headers, conn
}
SaveResult::Partial(_, reason) => {
std::fs::remove_file(&file_path).ok();
err!(format!("Attachment size limit exceeded with this file: {:?}", reason));
err!(format!("Attachment storage limit exceeded with this file: {:?}", reason));
}
SaveResult::Error(e) => {
std::fs::remove_file(&file_path).ok();
@@ -335,7 +336,7 @@ fn post_access_file(
}
#[get("/sends/<send_id>/<file_id>?<t>")]
fn download_send(send_id: String, file_id: String, t: String) -> Option<NamedFile> {
fn download_send(send_id: SafeString, file_id: SafeString, t: String) -> Option<NamedFile> {
if let Ok(claims) = crate::auth::decode_send(&t) {
if claims.sub == format!("{}/{}", send_id, file_id) {
return NamedFile::open(Path::new(&CONFIG.sends_folder()).join(send_id).join(file_id)).ok();

View File

@@ -7,10 +7,8 @@ use crate::{
api::{JsonResult, JsonUpcase, NumberOrString, PasswordData},
auth::Headers,
crypto,
db::{
models::{TwoFactor, User},
DbConn,
},
db::{models::*, DbConn},
mail, CONFIG,
};
pub mod authenticator;
@@ -130,6 +128,23 @@ fn disable_twofactor(data: JsonUpcase<DisableTwoFactorData>, headers: Headers, c
twofactor.delete(&conn)?;
}
let twofactor_disabled = TwoFactor::find_by_user(&user.uuid, &conn).is_empty();
if twofactor_disabled {
let policy_type = OrgPolicyType::TwoFactorAuthentication;
let org_list = UserOrganization::find_by_user_and_policy(&user.uuid, policy_type, &conn);
for user_org in org_list.into_iter() {
if user_org.atype < UserOrgType::Admin {
if CONFIG.mail_enabled() {
let org = Organization::find_by_uuid(&user_org.org_uuid, &conn).unwrap();
mail::send_2fa_removed_from_org(&user.email, &org.name)?;
}
user_org.delete(&conn)?;
}
}
}
Ok(Json(json!({
"Enabled": false,
"Type": type_,

View File

@@ -4,7 +4,7 @@ use rocket::{http::ContentType, response::content::Content, response::NamedFile,
use rocket_contrib::json::Json;
use serde_json::Value;
use crate::{error::Error, util::Cached, CONFIG};
use crate::{CONFIG, error::Error, util::{Cached, SafeString}};
pub fn routes() -> Vec<Route> {
// If addding more routes here, consider also adding them to
@@ -56,7 +56,7 @@ fn web_files(p: PathBuf) -> Cached<Option<NamedFile>> {
}
#[get("/attachments/<uuid>/<file_id>")]
fn attachments(uuid: String, file_id: String) -> Option<NamedFile> {
fn attachments(uuid: SafeString, file_id: SafeString) -> Option<NamedFile> {
NamedFile::open(Path::new(&CONFIG.attachments_folder()).join(uuid).join(file_id)).ok()
}

View File

@@ -356,9 +356,9 @@ make_config! {
/// HIBP Api Key |> HaveIBeenPwned API Key, request it here: https://haveibeenpwned.com/API/Key
hibp_api_key: Pass, true, option;
/// Per-user attachment limit (KB) |> Limit in kilobytes for a users attachments, once the limit is exceeded it won't be possible to upload more
/// Per-user attachment storage limit (KB) |> Max kilobytes of attachment storage allowed per user. When this limit is reached, the user will not be allowed to upload further attachments.
user_attachment_limit: i64, true, option;
/// Per-organization attachment limit (KB) |> Limit in kilobytes for an organization attachments, once the limit is exceeded it won't be possible to upload more
/// Per-organization attachment storage limit (KB) |> Max kilobytes of attachment storage allowed per org. When this limit is reached, org members will not be allowed to upload further attachments for ciphers owned by that org.
org_attachment_limit: i64, true, option;
/// Trash auto-delete days |> Number of days to wait before auto-deleting a trashed item.
@@ -388,9 +388,10 @@ make_config! {
/// Password iterations |> Number of server-side passwords hashing iterations.
/// The changes only apply when a user changes their password. Not recommended to lower the value
password_iterations: i32, true, def, 100_000;
/// Show password hints |> Controls if the password hint should be shown directly in the web page.
/// Otherwise, if email is disabled, there is no way to see the password hint
show_password_hint: bool, true, def, true;
/// Show password hint |> Controls whether a password hint should be shown directly in the web page
/// if SMTP service is not configured. Not recommended for publicly-accessible instances as this
/// provides unauthenticated access to potentially sensitive data.
show_password_hint: bool, true, def, false;
/// Admin page token |> The token used to authenticate in this very same page. Changing it here won't deauthorize the current session
admin_token: Pass, true, option;
@@ -857,6 +858,7 @@ where
reg!("email/new_device_logged_in", ".html");
reg!("email/pw_hint_none", ".html");
reg!("email/pw_hint_some", ".html");
reg!("email/send_2fa_removed_from_org", ".html");
reg!("email/send_org_invite", ".html");
reg!("email/twofactor_email", ".html");
reg!("email/verify_email", ".html");

View File

@@ -22,7 +22,7 @@ db_object! {
}
}
#[derive(Copy, Clone, num_derive::FromPrimitive)]
#[derive(Copy, Clone, PartialEq, num_derive::FromPrimitive)]
pub enum OrgPolicyType {
TwoFactorAuthentication = 0,
MasterPassword = 1,

View File

@@ -2,7 +2,7 @@ use num_traits::FromPrimitive;
use serde_json::Value;
use std::cmp::Ordering;
use super::{CollectionUser, OrgPolicy, User};
use super::{CollectionUser, OrgPolicy, OrgPolicyType, User};
db_object! {
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
@@ -544,6 +544,25 @@ impl UserOrganization {
}}
}
pub fn find_by_user_and_policy(user_uuid: &str, policy_type: OrgPolicyType, conn: &DbConn) -> Vec<Self> {
db_run! { conn: {
users_organizations::table
.inner_join(
org_policies::table.on(
org_policies::org_uuid.eq(users_organizations::org_uuid)
.and(users_organizations::user_uuid.eq(user_uuid))
.and(org_policies::atype.eq(policy_type as i32))
.and(org_policies::enabled.eq(true)))
)
.filter(
users_organizations::status.eq(UserOrgStatus::Confirmed as i32)
)
.select(users_organizations::all_columns)
.load::<UserOrganizationDb>(conn)
.unwrap_or_default().from_db()
}}
}
pub fn find_by_cipher_and_org(cipher_uuid: &str, org_uuid: &str, conn: &DbConn) -> Vec<Self> {
db_run! { conn: {
users_organizations::table

View File

@@ -166,7 +166,7 @@ fn _serialize(e: &impl serde::Serialize, _msg: &str) -> String {
fn _api_error(_: &impl std::any::Any, msg: &str) -> String {
let json = json!({
"Message": "",
"Message": msg,
"error": "",
"error_description": "",
"ValidationErrors": {"": [ msg ]},

View File

@@ -180,6 +180,18 @@ pub fn send_welcome_must_verify(address: &str, uuid: &str) -> EmptyResult {
send_email(address, &subject, body_html, body_text)
}
pub fn send_2fa_removed_from_org(address: &str, org_name: &str) -> EmptyResult {
let (subject, body_html, body_text) = get_text(
"email/send_2fa_removed_from_org",
json!({
"url": CONFIG.domain(),
"org_name": org_name,
}),
)?;
send_email(address, &subject, body_html, body_text)
}
pub fn send_invite(
address: &str,
uuid: &str,

View File

@@ -1,23 +1,22 @@
</td>
</tr>
</table>
</td>
</tr>
</td>
</tr>
</table>
<table class="footer" cellpadding="0" cellspacing="0" width="100%" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; width: 100%;">
</td>
</tr>
</table>
<table class="footer" cellpadding="0" cellspacing="0" width="100%" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; width: 100%;">
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
<td class="aligncenter social-icons" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 15px 0 0 0;" valign="top">
<table cellpadding="0" cellspacing="0" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto;">
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
<td class="aligncenter social-icons" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 15px 0 0 0;" valign="top">
<table cellpadding="0" cellspacing="0" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto;">
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
<td style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 0 10px;" valign="top"><a href="https://github.com/dani-garcia/vaultwarden" target="_blank" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; text-decoration: underline;"><img src="{{url}}/bwrs_static/mail-github.png" alt="GitHub" width="30" height="30" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;" /></a></td>
</tr>
</table>
</td>
<td style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 0 10px;" valign="top"><a href="https://github.com/dani-garcia/vaultwarden" target="_blank" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; text-decoration: underline;"><img src="{{url}}/bwrs_static/mail-github.png" alt="GitHub" width="30" height="30" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;" /></a></td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>

View File

@@ -0,0 +1,9 @@
Removed from {{{org_name}}}
<!---------------->
You have been removed from organization *{{org_name}}* because your account does not have Two-step Login enabled.
You can enable Two-step Login in your account settings.
===
Github: https://github.com/dani-garcia/vaultwarden

View File

@@ -0,0 +1,16 @@
Removed from {{{org_name}}}
<!---------------->
{{> email/email_header }}
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
You have been removed from organization <b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{org_name}}</b> because your account does not have Two-step Login enabled.
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
You can enable Two-step Login in your account settings.
</td>
</tr>
</table>
{{> email/email_footer }}

View File

@@ -5,7 +5,8 @@ use std::io::Cursor;
use rocket::{
fairing::{Fairing, Info, Kind},
http::{ContentType, Header, HeaderMap, Method, Status},
http::{ContentType, Header, HeaderMap, Method, RawStr, Status},
request::FromParam,
response::{self, Responder},
Data, Request, Response, Rocket,
};
@@ -29,7 +30,10 @@ impl Fairing for AppHeaders {
res.set_raw_header("X-Content-Type-Options", "nosniff");
res.set_raw_header("X-XSS-Protection", "1; mode=block");
let csp = format!(
"frame-ancestors 'self' chrome-extension://nngceckbapebfimnlniiiahkandclblb moz-extension://* {};",
// Chrome Web Store: https://chrome.google.com/webstore/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb
// Edge Add-ons: https://microsoftedge.microsoft.com/addons/detail/bitwarden-free-password/jbkfoedolllekgbhcbcoahefnbanhhlh?hl=en-US
// Firefox Browser Add-ons: https://addons.mozilla.org/en-US/firefox/addon/bitwarden-password-manager/
"frame-ancestors 'self' chrome-extension://nngceckbapebfimnlniiiahkandclblb chrome-extension://jbkfoedolllekgbhcbcoahefnbanhhlh moz-extension://* {};",
CONFIG.allowed_iframe_ancestors()
);
res.set_raw_header("Content-Security-Policy", csp);
@@ -125,6 +129,36 @@ impl<'r, R: Responder<'r>> Responder<'r> for Cached<R> {
}
}
pub struct SafeString(String);
impl std::fmt::Display for SafeString {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl AsRef<Path> for SafeString {
#[inline]
fn as_ref(&self) -> &Path {
Path::new(&self.0)
}
}
impl<'r> FromParam<'r> for SafeString {
type Error = ();
#[inline(always)]
fn from_param(param: &'r RawStr) -> Result<Self, Self::Error> {
let s = param.percent_decode().map(|cow| cow.into_owned()).map_err(|_| ())?;
if s.chars().all(|c| matches!(c, 'a'..='z' | 'A'..='Z' |'0'..='9' | '-')) {
Ok(SafeString(s))
} else {
Err(())
}
}
}
// Log all the routes from the main paths list, and the attachments endpoint
// Effectively ignores, any static file route, and the alive endpoint
const LOGGED_ROUTES: [&str; 6] =