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 2db533ea55dd75ddb5fd5058f0caa5b5acc4b1fe
parent 14218be42821ed1a6120f24171c9bd827f575896
Author: Zack Newman <zack@philomathiclife.com>
Date:   Fri, 17 Nov 2023 00:08:48 -0700

another round of cleanup. change config file format

Diffstat:
D.env | 29-----------------------------
MCargo.toml | 28+++++++++-------------------
Aconfig.toml | 11+++++++++++
Dresources/404.svg | 93-------------------------------------------------------------------------------
Dresources/vaultwarden-icon-white.svg | 78------------------------------------------------------------------------------
Dresources/vaultwarden-icon.svg | 74--------------------------------------------------------------------------
Dresources/vaultwarden-logo-white.svg | 88-------------------------------------------------------------------------------
Dresources/vaultwarden-logo.svg | 88-------------------------------------------------------------------------------
Msrc/api/core/accounts.rs | 231++++++++++++++-----------------------------------------------------------------
Msrc/api/core/ciphers.rs | 15+++++++++------
Msrc/api/core/emergency_access.rs | 721++++++++-----------------------------------------------------------------------
Msrc/api/core/mod.rs | 2+-
Msrc/api/core/organizations.rs | 405+++++++++----------------------------------------------------------------------
Msrc/api/core/public.rs | 34+++-------------------------------
Msrc/api/core/sends.rs | 625++++++++-----------------------------------------------------------------------
Msrc/api/core/two_factor/authenticator.rs | 9+--------
Msrc/api/core/two_factor/mod.rs | 7-------
Msrc/api/core/two_factor/protected_actions.rs | 75+++++++++++++++++++++------------------------------------------------------
Msrc/api/core/two_factor/webauthn.rs | 19++++++-------------
Msrc/api/icons.rs | 4++--
Msrc/api/identity.rs | 92++++++++-----------------------------------------------------------------------
Msrc/api/notifications.rs | 27+--------------------------
Msrc/api/web.rs | 36+++++++++++++-----------------------
Msrc/auth.rs | 385++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Msrc/config.rs | 1054++++++++++++-------------------------------------------------------------------
Msrc/crypto.rs | 16----------------
Msrc/db/mod.rs | 34+++++++++++++---------------------
Msrc/db/models/attachment.rs | 37++++++++++++++++++++++++++++---------
Msrc/db/models/cipher.rs | 14+-------------
Msrc/db/models/device.rs | 45++++++++++++++++++++++++++++++++++-----------
Msrc/db/models/emergency_access.rs | 57++++++++++++++++++++++++++++++++++-----------------------
Msrc/db/models/event.rs | 21++-------------------
Msrc/db/models/organization.rs | 12++----------
Msrc/db/models/send.rs | 16+++++++++++++---
Msrc/db/models/two_factor_incomplete.rs | 52+++++++++-------------------------------------------
Msrc/db/models/user.rs | 27+++++++++++++++++----------
Msrc/error.rs | 12------------
Dsrc/mail.rs | 596-------------------------------------------------------------------------------
Msrc/main.rs | 164++++++++++++++++++++++---------------------------------------------------------
Dsrc/ratelimit.rs | 31-------------------------------
Dsrc/static/templates/404.hbs | 38--------------------------------------
Dsrc/static/templates/admin/base.hbs | 98-------------------------------------------------------------------------------
Dsrc/static/templates/admin/diagnostics.hbs | 212-------------------------------------------------------------------------------
Dsrc/static/templates/admin/login.hbs | 24------------------------
Dsrc/static/templates/admin/organizations.hbs | 65-----------------------------------------------------------------
Dsrc/static/templates/admin/settings.hbs | 156-------------------------------------------------------------------------------
Dsrc/static/templates/admin/users.hbs | 146-------------------------------------------------------------------------------
Dsrc/static/templates/email/admin_reset_password.hbs | 4----
Dsrc/static/templates/email/admin_reset_password.html.hbs | 11-----------
Dsrc/static/templates/email/change_email.hbs | 6------
Dsrc/static/templates/email/change_email.html.hbs | 17-----------------
Dsrc/static/templates/email/delete_account.hbs | 9---------
Dsrc/static/templates/email/delete_account.html.hbs | 25-------------------------
Dsrc/static/templates/email/email_footer.hbs | 24------------------------
Dsrc/static/templates/email/email_footer_text.hbs | 3---
Dsrc/static/templates/email/email_header.hbs | 94-------------------------------------------------------------------------------
Dsrc/static/templates/email/emergency_access_invite_accepted.hbs | 8--------
Dsrc/static/templates/email/emergency_access_invite_accepted.html.hbs | 21---------------------
Dsrc/static/templates/email/emergency_access_invite_confirmed.hbs | 6------
Dsrc/static/templates/email/emergency_access_invite_confirmed.html.hbs | 16----------------
Dsrc/static/templates/email/emergency_access_recovery_approved.hbs | 4----
Dsrc/static/templates/email/emergency_access_recovery_approved.html.hbs | 11-----------
Dsrc/static/templates/email/emergency_access_recovery_initiated.hbs | 6------
Dsrc/static/templates/email/emergency_access_recovery_initiated.html.hbs | 16----------------
Dsrc/static/templates/email/emergency_access_recovery_rejected.hbs | 4----
Dsrc/static/templates/email/emergency_access_recovery_rejected.html.hbs | 11-----------
Dsrc/static/templates/email/emergency_access_recovery_reminder.hbs | 6------
Dsrc/static/templates/email/emergency_access_recovery_reminder.html.hbs | 16----------------
Dsrc/static/templates/email/emergency_access_recovery_timed_out.hbs | 4----
Dsrc/static/templates/email/emergency_access_recovery_timed_out.html.hbs | 11-----------
Dsrc/static/templates/email/incomplete_2fa_login.hbs | 10----------
Dsrc/static/templates/email/incomplete_2fa_login.html.hbs | 31-------------------------------
Dsrc/static/templates/email/invite_accepted.hbs | 6------
Dsrc/static/templates/email/invite_accepted.html.hbs | 22----------------------
Dsrc/static/templates/email/invite_confirmed.hbs | 6------
Dsrc/static/templates/email/invite_confirmed.html.hbs | 18------------------
Dsrc/static/templates/email/new_device_logged_in.hbs | 11-----------
Dsrc/static/templates/email/new_device_logged_in.html.hbs | 32--------------------------------
Dsrc/static/templates/email/protected_action.hbs | 6------
Dsrc/static/templates/email/protected_action.html.hbs | 16----------------
Dsrc/static/templates/email/pw_hint_none.hbs | 9---------
Dsrc/static/templates/email/pw_hint_none.html.hbs | 22----------------------
Dsrc/static/templates/email/pw_hint_some.hbs | 12------------
Dsrc/static/templates/email/pw_hint_some.html.hbs | 28----------------------------
Dsrc/static/templates/email/send_2fa_removed_from_org.hbs | 7-------
Dsrc/static/templates/email/send_2fa_removed_from_org.html.hbs | 16----------------
Dsrc/static/templates/email/send_emergency_access_invite.hbs | 8--------
Dsrc/static/templates/email/send_emergency_access_invite.html.hbs | 25-------------------------
Dsrc/static/templates/email/send_org_invite.hbs | 11-----------
Dsrc/static/templates/email/send_org_invite.html.hbs | 25-------------------------
Dsrc/static/templates/email/send_single_org_removed_from_org.hbs | 4----
Dsrc/static/templates/email/send_single_org_removed_from_org.html.hbs | 11-----------
Dsrc/static/templates/email/smtp_test.hbs | 7-------
Dsrc/static/templates/email/smtp_test.html.hbs | 17-----------------
Dsrc/static/templates/email/twofactor_email.hbs | 6------
Dsrc/static/templates/email/twofactor_email.html.hbs | 16----------------
Dsrc/static/templates/email/verify_email.hbs | 9---------
Dsrc/static/templates/email/verify_email.html.hbs | 25-------------------------
Dsrc/static/templates/email/welcome.hbs | 7-------
Dsrc/static/templates/email/welcome.html.hbs | 17-----------------
Dsrc/static/templates/email/welcome_must_verify.hbs | 9---------
Dsrc/static/templates/email/welcome_must_verify.html.hbs | 25-------------------------
Msrc/util.rs | 90++++++-------------------------------------------------------------------------
103 files changed, 866 insertions(+), 6134 deletions(-)

diff --git a/.env b/.env @@ -1,29 +0,0 @@ -_ENABLE_DUO=false -_ENABLE_EMAIL_2FA=false -_ENABLE_SMTP=false -_ENABLE_YUBICO=false -AUTHENTICATOR_DISABLE_TIME_DRIFT=true -DATABASE_MAX_CONNS=4 -DB_CONNECTION_RETRIES=8 -DISABLE_2FA_REMEMBER=true -DISABLE_ICON_DOWNLOAD=true -DOMAIN=https://pmd.philomathiclife.com:8443 -EMAIL_CHANGE_ALLOWED=false -EMERGENCY_ACCESS_ALLOWED=false -EXTENDED_LOGGING=false -INVITATION_ORG_NAME=pmd -INVITATIONS_ALLOWED=false -JOB_POLL_INTERVAL_MS=0 -LOG_LEVEL=OFF -ORG_ATTACHMENT_LIMIT=0 -ORG_CREATION_USERS=none -PASSWORD_HINTS_ALLOWED=false -ROCKET_ADDRESS=fdb5:d87:ae42:1::1 -ROCKET_CLI_COLORS=false -ROCKET_ENV=production -ROCKET_PORT=8443 -ROCKET_TLS={ciphers=["TLS_CHACHA20_POLY1305_SHA256","TLS_AES_256_GCM_SHA384","TLS_AES_128_GCM_SHA256","TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256"],certs="/etc/ssl/pmd.philomathiclife.com.fullchain",key="/etc/ssl/pmd.philomathiclife.com.fullchain.key",prefer_server_cipher_order=true} -ROCKET_WORKERS=4 -SENDS_ALLOWED=false -SIGNUPS_ALLOWED=false -USER_ATTACHMENT_LIMIT=0 diff --git a/Cargo.toml b/Cargo.toml @@ -1,12 +1,14 @@ [package] -authors = ["Daniel GarcĂ­a <dani-garcia@users.noreply.github.com>"] +authors = ["Zack Newman <zack@philomathiclife.com>"] +categories = ["api-bindings", "web-programming::http-server"] +description = "Fork of Vaultwarden with fewer features and pledge(2) and unveil(2) support." +documentation = "https://github.com/dani-garcia/vaultwarden/wiki" edition = "2021" +keywords = ["password", "vaultwarden"] license = "AGPL-3.0-only" -name = "vaultwarden" +name = "vw_small" publish = false -readme = "README.md" -repository = "https://github.com/dani-garcia/vaultwarden" -resolver = "2" +repository = "https://git.philomathiclife.com/repos/vw_small/" version = "1.30.0" [features] @@ -18,22 +20,15 @@ priv_sep = { version = "0.8.1", default-features = false, features = ["openbsd"] [dependencies] bytes = { version = "1.5.0", default-features = false } chrono = { version = "0.4.31", default-features = false, features = ["serde"] } -chrono-tz = { version = "0.8.4", default-features = false } dashmap = { version = "5.5.3", default-features = false } data-encoding = { version = "2.4.0", default-features = false } diesel = { version = "2.1.4", default-features = false, features = ["32-column-tables", "chrono", "r2d2", "sqlite"] } diesel_migrations = { version = "2.1.0", default-features = false } -dotenvy = { version = "0.15.7", default-features = false } -email_address = { version = "0.2.4", default-features = false } futures = { version = "0.3.29", default-features = false } -governor = { version = "0.6.0", default-features = false, features = ["dashmap", "std"] } -handlebars = { version = "4.5.0", default-features = false, features = ["dir_source"] } jsonwebtoken = { version = "9.1.0", default-features = false, features = ["use_pem"] } libsqlite3-sys = { version = "0.27.0", default-features = false, features = ["bundled"] } -lettre = { version = "0.11.1", default-features = false, features = ["builder", "sendmail-transport", "smtp-transport", "tokio1-native-tls"] } num-derive = { version = "0.4.1", default-features = false } num-traits = { version = "0.2.17", default-features = false } -once_cell = {version = "1.18.0", default-features = false } openssl = { version = "0.10.59", default-features = false } paste = { version = "1.0.14", default-features = false } percent-encoding = { version = "2.3.0", default-features = false } @@ -42,23 +37,18 @@ regex = { version = "1.10.2", default-features = false, features = ["std"] } ring = { version = "0.17.5", default-features = false } rmpv = { version = "1.0.1", default-features = false } rocket = { version = "0.5.0-rc.4", default-features = false, features = ["json", "tls"] } -rocket_ws = { version ="0.1.0-rc.4", default-features = false, features = ["tokio-tungstenite"] } +rocket_ws = { version = "0.1.0-rc.4", default-features = false, features = ["tokio-tungstenite"] } semver = { version = "1.0.20", default-features = false } serde = { version = "1.0.192", default-features = false } serde_json = { version = "1.0.108", default-features = false } time = { version = "0.3.30", default-features = false } tokio = { version = "1.34.0", default-features = false } tokio-tungstenite = { version = "0.20.1", default-features = false } +toml= { version = "0.8.8", default-features = false, features = ["parse"] } totp-lite = { version = "2.0.1", default-features = false } -tracing = { version = "0.1.40", default-features = false } url = { version = "2.4.1", default-features = false } uuid = { version = "1.5.0", default-features = false, features = ["v4"] } webauthn-rs = { version = "0.3.2", default-features = false, features = ["core"] } -which = { version = "5.0.0", default-features = false } - -# A little bit of a speedup -[profile.dev] -split-debuginfo = "unpacked" [profile.release] lto = true diff --git a/config.toml b/config.toml @@ -0,0 +1,11 @@ +database_max_conns=4 +db_connection_retries=8 +domain="pmd.philomathiclife.com" +ip="fdb5:d87:ae42:1::1" +port=8443 +workers=4 +[tls] +cert="/etc/ssl/pmd.philomathiclife.com.fullchain" +ciphers=["TLS_CHACHA20_POLY1305_SHA256","TLS_AES_256_GCM_SHA384","TLS_AES_128_GCM_SHA256","TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256"] +key="/etc/ssl/pmd.philomathiclife.com.fullchain.key" +prefer_server_cipher_order=true diff --git a/resources/404.svg b/resources/404.svg @@ -1,93 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<!-- Created with Inkscape (http://www.inkscape.org/) --> - -<svg - width="500" - height="222" - viewBox="0 0 500 222" - version="1.1" - id="svg5" - xml:space="preserve" - inkscape:version="1.2.1 (9c6d41e410, 2022-07-14, custom)" - sodipodi:docname="404.svg" - inkscape:export-filename="404.png" - inkscape:export-xdpi="96" - inkscape:export-ydpi="96" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns:xlink="http://www.w3.org/1999/xlink" - xmlns="http://www.w3.org/2000/svg" - xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview - id="namedview7" - pagecolor="#ffffff" - bordercolor="#666666" - borderopacity="1.0" - inkscape:showpageshadow="2" - inkscape:pageopacity="0.0" - inkscape:pagecheckerboard="0" - inkscape:deskcolor="#d1d1d1" - inkscape:document-units="px" - showgrid="false" - inkscape:zoom="1.3791767" - inkscape:cx="284.59007" - inkscape:cy="214.25826" - inkscape:window-width="1916" - inkscape:window-height="1038" - inkscape:window-x="0" - inkscape:window-y="18" - inkscape:window-maximized="1" - inkscape:current-layer="layer1" - showguides="false" /><defs - id="defs2"><mask - id="holes"><rect - x="-60" - y="-60" - width="120" - height="120" - fill="#ffffff" - id="rect3296" /><circle - id="hole" - cy="-40" - r="3" - cx="0" /><use - transform="rotate(72)" - xlink:href="#hole" - id="use3299" /><use - transform="rotate(144)" - xlink:href="#hole" - id="use3301" /><use - transform="rotate(-144)" - xlink:href="#hole" - id="use3303" /><use - transform="rotate(-72)" - xlink:href="#hole" - id="use3305" /></mask></defs><g - inkscape:label="Ebene 1" - inkscape:groupmode="layer" - id="layer1"><rect - style="fill:none;fill-opacity:0.5;stroke:none;stroke-width:0.74;stroke-opacity:1" - id="rect681" - width="666" - height="222" - x="0" - y="0" /><text - xml:space="preserve" - style="font-size:128px;line-height:1.25;font-family:'Open Sans';-inkscape-font-specification:'Open Sans';text-align:center;text-anchor:middle;fill:#000000;fill-opacity:0.7;stroke-width:1" - x="249.9375" - y="134.8125" - id="text3425"><tspan - id="tspan3423" - x="249.9375" - y="134.8125" - style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:128px;font-family:'Open Sans';-inkscape-font-specification:'Open Sans';text-align:center;text-anchor:middle;fill:#000000;fill-opacity:0.7;stroke-width:1" - sodipodi:role="line">404</tspan></text><text - xml:space="preserve" - style="font-size:26.6667px;line-height:1.25;font-family:'Open Sans';-inkscape-font-specification:'Open Sans';text-align:center;text-anchor:middle" - x="249.04297" - y="194.68582" - id="text4067"><tspan - sodipodi:role="line" - id="tspan4065" - x="249.04295" - y="194.68582" - style="font-size:26.6667px;text-align:center;text-anchor:middle;fill:#000000;fill-opacity:0.7">Return to the web vault?</tspan></text></g></svg> diff --git a/resources/vaultwarden-icon-white.svg b/resources/vaultwarden-icon-white.svg @@ -1,78 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg version="1.1" viewBox="0 0 256 256" id="svg3917" sodipodi:docname="vaultwarden-icon-white.svg" inkscape:version="1.2.1 (9c6d41e410, 2022-07-14, custom)" width="256" height="256" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"> - <defs id="defs3921" /> - <sodipodi:namedview id="namedview3919" pagecolor="#000000" bordercolor="#666666" borderopacity="1.0" inkscape:showpageshadow="2" inkscape:pageopacity="0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#202020" showgrid="false" inkscape:zoom="3.3359375" inkscape:cx="128" inkscape:cy="128" inkscape:window-width="1874" inkscape:window-height="1056" inkscape:window-x="46" inkscape:window-y="24" inkscape:window-maximized="1" inkscape:current-layer="svg3917" /> - <title id="title3817">Vaultwarden Icon - White</title> - <g id="logo" transform="matrix(2.4381018,0,0,2.4381018,128,128)"> - <g id="gear" mask="url(#holes)" stroke="#fff"> - <path d="m-31.1718-33.813208 26.496029 74.188883h9.3515399l26.49603-74.188883h-9.767164l-16.728866 47.588948q-1.662496 4.571864-2.805462 8.624198-1.142966 3.948427-1.870308 7.585137-.72734199-3.63671-1.8703079-7.689043-1.142966-4.052334-2.805462-8.728104l-16.624959-47.381136z" fill="#fff" stroke-width="4.51171" id="path3819" /> - <circle transform="scale(-1,1)" r="43" fill="none" stroke-width="9" id="circle3821" /> - <g id="cogs" transform="scale(-1,1)"> - <polygon id="cog" points="51 0 46 -3 46 3" fill="#fff" stroke="#fff" stroke-linejoin="round" stroke-width="3" /> - <g fill="#fff" stroke="#fff" id="g3886"> - <use transform="rotate(11.25)" xlink:href="#cog" id="use3824" /> - <use transform="rotate(22.5)" xlink:href="#cog" id="use3826" /> - <use transform="rotate(33.75)" xlink:href="#cog" id="use3828" /> - <use transform="rotate(45)" xlink:href="#cog" id="use3830" /> - <use transform="rotate(56.25)" xlink:href="#cog" id="use3832" /> - <use transform="rotate(67.5)" xlink:href="#cog" id="use3834" /> - <use transform="rotate(78.75)" xlink:href="#cog" id="use3836" /> - <use transform="rotate(90)" xlink:href="#cog" id="use3838" /> - <use transform="rotate(101.25)" xlink:href="#cog" id="use3840" /> - <use transform="rotate(112.5)" xlink:href="#cog" id="use3842" /> - <use transform="rotate(123.75)" xlink:href="#cog" id="use3844" /> - <use transform="rotate(135)" xlink:href="#cog" id="use3846" /> - <use transform="rotate(146.25)" xlink:href="#cog" id="use3848" /> - <use transform="rotate(157.5)" xlink:href="#cog" id="use3850" /> - <use transform="rotate(168.75)" xlink:href="#cog" id="use3852" /> - <use transform="scale(-1)" xlink:href="#cog" id="use3854" /> - <use transform="rotate(191.25)" xlink:href="#cog" id="use3856" /> - <use transform="rotate(202.5)" xlink:href="#cog" id="use3858" /> - <use transform="rotate(213.75)" xlink:href="#cog" id="use3860" /> - <use transform="rotate(225)" xlink:href="#cog" id="use3862" /> - <use transform="rotate(236.25)" xlink:href="#cog" id="use3864" /> - <use transform="rotate(247.5)" xlink:href="#cog" id="use3866" /> - <use transform="rotate(258.75)" xlink:href="#cog" id="use3868" /> - <use transform="rotate(-90)" xlink:href="#cog" id="use3870" /> - <use transform="rotate(-78.75)" xlink:href="#cog" id="use3872" /> - <use transform="rotate(-67.5)" xlink:href="#cog" id="use3874" /> - <use transform="rotate(-56.25)" xlink:href="#cog" id="use3876" /> - <use transform="rotate(-45)" xlink:href="#cog" id="use3878" /> - <use transform="rotate(-33.75)" xlink:href="#cog" id="use3880" /> - <use transform="rotate(-22.5)" xlink:href="#cog" id="use3882" /> - <use transform="rotate(-11.25)" xlink:href="#cog" id="use3884" /> - </g> - </g> - <g id="mounts" transform="scale(-1,1)"> - <polygon id="mount" points="0 -35 7 -42 -7 -42" fill="#fff" stroke="#fff" stroke-linejoin="round" stroke-width="6" /> - <g fill="#fff" stroke="#fff" id="g3898"> - <use transform="rotate(72)" xlink:href="#mount" id="use3890" /> - <use transform="rotate(144)" xlink:href="#mount" id="use3892" /> - <use transform="rotate(216)" xlink:href="#mount" id="use3894" /> - <use transform="rotate(-72)" xlink:href="#mount" id="use3896" /> - </g> - </g> - </g> - <mask id="holes"> - <rect x="-60" y="-60" width="120" height="120" fill="#fff" id="rect3902" /> - <circle id="hole" cy="-40" r="3" /> - <use transform="rotate(72)" xlink:href="#hole" id="use3905" /> - <use transform="rotate(144)" xlink:href="#hole" id="use3907" /> - <use transform="rotate(216)" xlink:href="#hole" id="use3909" /> - <use transform="rotate(-72)" xlink:href="#hole" id="use3911" /> - </mask> - </g> - <metadata id="metadata3915"> - <rdf:RDF> - <cc:Work rdf:about=""> - <dc:title>Vaultwarden Icon - White</dc:title> - <dc:creator> - <cc:Agent> - <dc:title>Mathijs van Veluw</dc:title> - </cc:Agent> - </dc:creator> - <dc:relation>Rust Logo</dc:relation> - </cc:Work> - </rdf:RDF> - </metadata> -</svg> diff --git a/resources/vaultwarden-icon.svg b/resources/vaultwarden-icon.svg @@ -1,74 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg version="1.1" viewBox="0 0 256 256" id="svg383" sodipodi:docname="vaultwarden-icon.svg" inkscape:version="1.2.1 (9c6d41e410, 2022-07-14, custom)" width="256" height="256" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"> - <defs id="defs387" /> - <sodipodi:namedview id="namedview385" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" inkscape:showpageshadow="2" inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" showgrid="false" inkscape:zoom="3.3359375" inkscape:cx="128" inkscape:cy="128" inkscape:window-width="1874" inkscape:window-height="1056" inkscape:window-x="46" inkscape:window-y="24" inkscape:window-maximized="1" inkscape:current-layer="svg383" /> - <title id="title287">Vaultwarden Icon</title> - <g id="logo" transform="matrix(2.4381018,0,0,2.4381018,128,128)"> - <g id="gear" mask="url(#holes)"> - <path d="m-31.1718-33.813208 26.496029 74.188883h9.3515399l26.49603-74.188883h-9.767164l-16.728866 47.588948q-1.662496 4.571864-2.805462 8.624198-1.142966 3.948427-1.870308 7.585137-.72734199-3.63671-1.8703079-7.689043-1.142966-4.052334-2.805462-8.728104l-16.624959-47.381136z" stroke="#000" stroke-width="4.51171" id="path289" /> - <circle transform="scale(-1,1)" r="43" fill="none" stroke="#000" stroke-width="9" id="circle291" /> - <g id="cogs" transform="scale(-1,1)"> - <polygon id="cog" points="51 0 46 -3 46 3" stroke="#000" stroke-linejoin="round" stroke-width="3" /> - <use transform="rotate(11.25)" xlink:href="#cog" id="use294" /> - <use transform="rotate(22.5)" xlink:href="#cog" id="use296" /> - <use transform="rotate(33.75)" xlink:href="#cog" id="use298" /> - <use transform="rotate(45)" xlink:href="#cog" id="use300" /> - <use transform="rotate(56.25)" xlink:href="#cog" id="use302" /> - <use transform="rotate(67.5)" xlink:href="#cog" id="use304" /> - <use transform="rotate(78.75)" xlink:href="#cog" id="use306" /> - <use transform="rotate(90)" xlink:href="#cog" id="use308" /> - <use transform="rotate(101.25)" xlink:href="#cog" id="use310" /> - <use transform="rotate(112.5)" xlink:href="#cog" id="use312" /> - <use transform="rotate(123.75)" xlink:href="#cog" id="use314" /> - <use transform="rotate(135)" xlink:href="#cog" id="use316" /> - <use transform="rotate(146.25)" xlink:href="#cog" id="use318" /> - <use transform="rotate(157.5)" xlink:href="#cog" id="use320" /> - <use transform="rotate(168.75)" xlink:href="#cog" id="use322" /> - <use transform="scale(-1)" xlink:href="#cog" id="use324" /> - <use transform="rotate(191.25)" xlink:href="#cog" id="use326" /> - <use transform="rotate(202.5)" xlink:href="#cog" id="use328" /> - <use transform="rotate(213.75)" xlink:href="#cog" id="use330" /> - <use transform="rotate(225)" xlink:href="#cog" id="use332" /> - <use transform="rotate(236.25)" xlink:href="#cog" id="use334" /> - <use transform="rotate(247.5)" xlink:href="#cog" id="use336" /> - <use transform="rotate(258.75)" xlink:href="#cog" id="use338" /> - <use transform="rotate(-90)" xlink:href="#cog" id="use340" /> - <use transform="rotate(-78.75)" xlink:href="#cog" id="use342" /> - <use transform="rotate(-67.5)" xlink:href="#cog" id="use344" /> - <use transform="rotate(-56.25)" xlink:href="#cog" id="use346" /> - <use transform="rotate(-45)" xlink:href="#cog" id="use348" /> - <use transform="rotate(-33.75)" xlink:href="#cog" id="use350" /> - <use transform="rotate(-22.5)" xlink:href="#cog" id="use352" /> - <use transform="rotate(-11.25)" xlink:href="#cog" id="use354" /> - </g> - <g id="mounts" transform="scale(-1,1)"> - <polygon id="mount" points="0 -35 7 -42 -7 -42" stroke="#000" stroke-linejoin="round" stroke-width="6" /> - <use transform="rotate(72)" xlink:href="#mount" id="use358" /> - <use transform="rotate(144)" xlink:href="#mount" id="use360" /> - <use transform="rotate(216)" xlink:href="#mount" id="use362" /> - <use transform="rotate(-72)" xlink:href="#mount" id="use364" /> - </g> - </g> - <mask id="holes"> - <rect x="-60" y="-60" width="120" height="120" fill="#fff" id="rect368" /> - <circle id="hole" cy="-40" r="3" /> - <use transform="rotate(72)" xlink:href="#hole" id="use371" /> - <use transform="rotate(144)" xlink:href="#hole" id="use373" /> - <use transform="rotate(216)" xlink:href="#hole" id="use375" /> - <use transform="rotate(-72)" xlink:href="#hole" id="use377" /> - </mask> - </g> - <metadata id="metadata381"> - <rdf:RDF> - <cc:Work rdf:about=""> - <dc:title>Vaultwarden Icon</dc:title> - <dc:creator> - <cc:Agent> - <dc:title>Mathijs van Veluw</dc:title> - </cc:Agent> - </dc:creator> - <dc:relation>Rust Logo</dc:relation> - </cc:Work> - </rdf:RDF> - </metadata> -</svg> diff --git a/resources/vaultwarden-logo-white.svg b/resources/vaultwarden-logo-white.svg @@ -1,88 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg width="1365.8256" height="280.48944" version="1.1" viewBox="0 0 1365.8255 280.48944" id="svg3412" sodipodi:docname="vaultwarden-logo.svg" inkscape:version="1.2.1 (9c6d41e410, 2022-07-14, custom)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"> - <sodipodi:namedview id="namedview3414" pagecolor="#000000" bordercolor="#666666" borderopacity="1.0" inkscape:showpageshadow="2" inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#202020" showgrid="false" inkscape:zoom="0.95107314" inkscape:cx="683.4385" inkscape:cy="139.84203" inkscape:window-width="1874" inkscape:window-height="1056" inkscape:window-x="46" inkscape:window-y="24" inkscape:window-maximized="1" inkscape:current-layer="svg3412" /> - <title id="title3292">Vaultwarden Logo - White</title> - <defs id="defs3308"> - <mask id="holes"> - <rect x="-60" y="-60" width="120" height="120" fill="#fff" id="rect3296" /> - <circle id="hole" cy="-40" r="3" /> - <use transform="rotate(72)" xlink:href="#hole" id="use3299" /> - <use transform="rotate(144)" xlink:href="#hole" id="use3301" /> - <use transform="rotate(216)" xlink:href="#hole" id="use3303" /> - <use transform="rotate(-72)" xlink:href="#hole" id="use3305" /> - </mask> - </defs> - <text transform="translate(-10.708266,-9.2965379)" x="286.59244" y="223.43649" fill="#e6e6e6" font-family="'Open Sans'" font-size="200px" style="line-height:1.25" xml:space="preserve" id="text3314"><tspan x="286.59244" y="223.43649" font-family="'Open Sans'" font-size="200px" id="tspan3312"><tspan font-family="'Open Sans'" font-size="200px" font-weight="bold" id="tspan3310">ault</tspan>warden</tspan></text> - <g transform="translate(-10.708266,-9.2965379)" id="g3410"> - <g id="logo" transform="matrix(2.6712834,0,0,2.6712834,150.95027,149.53854)"> - <g id="gear" mask="url(#holes)"> - <path d="m-31.1718-33.813208 26.496029 74.188883h9.3515399l26.49603-74.188883h-9.767164l-16.728866 47.588948q-1.662496 4.571864-2.805462 8.624198-1.142966 3.948427-1.870308 7.585137-.72734199-3.63671-1.8703079-7.689043-1.142966-4.052334-2.805462-8.728104l-16.624959-47.381136z" fill="#e6e6e6" stroke="#e6e6e6" stroke-width="4.51171" id="path3316" /> - <circle transform="scale(-1,1)" r="43" fill="none" stroke="#e6e6e6" stroke-width="9" id="circle3318" /> - <g id="cogs" transform="scale(-1,1)"> - <polygon id="cog" points="46 -3 46 3 51 0" fill="#e6e6e6" stroke="#e6e6e6" stroke-linejoin="round" stroke-width="3" /> - <use transform="rotate(11.25)" xlink:href="#cog" id="use3321" /> - <use transform="rotate(22.5)" xlink:href="#cog" id="use3323" /> - <use transform="rotate(33.75)" xlink:href="#cog" id="use3325" /> - <use transform="rotate(45)" xlink:href="#cog" id="use3327" /> - <use transform="rotate(56.25)" xlink:href="#cog" id="use3329" /> - <use transform="rotate(67.5)" xlink:href="#cog" id="use3331" /> - <use transform="rotate(78.75)" xlink:href="#cog" id="use3333" /> - <use transform="rotate(90)" xlink:href="#cog" id="use3335" /> - <use transform="rotate(101.25)" xlink:href="#cog" id="use3337" /> - <use transform="rotate(112.5)" xlink:href="#cog" id="use3339" /> - <use transform="rotate(123.75)" xlink:href="#cog" id="use3341" /> - <use transform="rotate(135)" xlink:href="#cog" id="use3343" /> - <use transform="rotate(146.25)" xlink:href="#cog" id="use3345" /> - <use transform="rotate(157.5)" xlink:href="#cog" id="use3347" /> - <use transform="rotate(168.75)" xlink:href="#cog" id="use3349" /> - <use transform="scale(-1)" xlink:href="#cog" id="use3351" /> - <use transform="rotate(191.25)" xlink:href="#cog" id="use3353" /> - <use transform="rotate(202.5)" xlink:href="#cog" id="use3355" /> - <use transform="rotate(213.75)" xlink:href="#cog" id="use3357" /> - <use transform="rotate(225)" xlink:href="#cog" id="use3359" /> - <use transform="rotate(236.25)" xlink:href="#cog" id="use3361" /> - <use transform="rotate(247.5)" xlink:href="#cog" id="use3363" /> - <use transform="rotate(258.75)" xlink:href="#cog" id="use3365" /> - <use transform="rotate(-90)" xlink:href="#cog" id="use3367" /> - <use transform="rotate(-78.75)" xlink:href="#cog" id="use3369" /> - <use transform="rotate(-67.5)" xlink:href="#cog" id="use3371" /> - <use transform="rotate(-56.25)" xlink:href="#cog" id="use3373" /> - <use transform="rotate(-45)" xlink:href="#cog" id="use3375" /> - <use transform="rotate(-33.75)" xlink:href="#cog" id="use3377" /> - <use transform="rotate(-22.5)" xlink:href="#cog" id="use3379" /> - <use transform="rotate(-11.25)" xlink:href="#cog" id="use3381" /> - </g> - <g id="mounts" transform="scale(-1,1)"> - <polygon id="mount" points="7 -42 -7 -42 0 -35" stroke="#e6e6e6" stroke-linejoin="round" stroke-width="6" /> - <use transform="rotate(72)" xlink:href="#mount" id="use3385" /> - <use transform="rotate(144)" xlink:href="#mount" id="use3387" /> - <use transform="rotate(216)" xlink:href="#mount" id="use3389" /> - <use transform="rotate(-72)" xlink:href="#mount" id="use3391" /> - </g> - </g> - <mask id="mask3407"> - <rect x="-60" y="-60" width="120" height="120" fill="#e6e6e6" id="rect3395" /> - <circle cy="-40" r="3" id="circle3397" /> - <use transform="rotate(72)" xlink:href="#hole" id="use3399" /> - <use transform="rotate(144)" xlink:href="#hole" id="use3401" /> - <use transform="rotate(216)" xlink:href="#hole" id="use3403" /> - <use transform="rotate(-72)" xlink:href="#hole" id="use3405" /> - </mask> - </g> - </g> - <metadata id="metadata3294"> - <rdf:RDF> - <cc:Work rdf:about=""> - <dc:format>image/svg+xml</dc:format> - <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> - <dc:title>Vaultwarden Logo - White</dc:title> - <dc:creator> - <cc:Agent> - <dc:title>Mathijs van Veluw</dc:title> - </cc:Agent> - </dc:creator> - <dc:relation>Rust Logo</dc:relation> - </cc:Work> - </rdf:RDF> - </metadata> -</svg> diff --git a/resources/vaultwarden-logo.svg b/resources/vaultwarden-logo.svg @@ -1,88 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg width="1365.8256" height="280.48944" version="1.1" viewBox="0 0 1365.8255 280.48944" id="svg3412" sodipodi:docname="vaultwarden-logo.svg" inkscape:version="1.2.1 (9c6d41e410, 2022-07-14, custom)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"> - <sodipodi:namedview id="namedview3414" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" inkscape:showpageshadow="2" inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" showgrid="false" inkscape:zoom="0.95107314" inkscape:cx="683.4385" inkscape:cy="139.84203" inkscape:window-width="1874" inkscape:window-height="1056" inkscape:window-x="46" inkscape:window-y="24" inkscape:window-maximized="1" inkscape:current-layer="svg3412" /> - <title id="title3292">Vaultwarden Logo</title> - <defs id="defs3308"> - <mask id="holes"> - <rect x="-60" y="-60" width="120" height="120" fill="#fff" id="rect3296" /> - <circle id="hole" cy="-40" r="3" /> - <use transform="rotate(72)" xlink:href="#hole" id="use3299" /> - <use transform="rotate(144)" xlink:href="#hole" id="use3301" /> - <use transform="rotate(216)" xlink:href="#hole" id="use3303" /> - <use transform="rotate(-72)" xlink:href="#hole" id="use3305" /> - </mask> - </defs> - <text transform="translate(-10.708266,-9.2965379)" x="286.59244" y="223.43649" fill="#000000" font-family="'Open Sans'" font-size="200px" style="line-height:1.25" xml:space="preserve" id="text3314"><tspan x="286.59244" y="223.43649" font-family="'Open Sans'" font-size="200px" id="tspan3312"><tspan font-family="'Open Sans'" font-size="200px" font-weight="bold" id="tspan3310">ault</tspan>warden</tspan></text> - <g transform="translate(-10.708266,-9.2965379)" id="g3410"> - <g id="logo" transform="matrix(2.6712834,0,0,2.6712834,150.95027,149.53854)"> - <g id="gear" mask="url(#holes)"> - <path d="m-31.1718-33.813208 26.496029 74.188883h9.3515399l26.49603-74.188883h-9.767164l-16.728866 47.588948q-1.662496 4.571864-2.805462 8.624198-1.142966 3.948427-1.870308 7.585137-.72734199-3.63671-1.8703079-7.689043-1.142966-4.052334-2.805462-8.728104l-16.624959-47.381136z" stroke="#000" stroke-width="4.51171" id="path3316" /> - <circle transform="scale(-1,1)" r="43" fill="none" stroke="#000" stroke-width="9" id="circle3318" /> - <g id="cogs" transform="scale(-1,1)"> - <polygon id="cog" points="46 -3 46 3 51 0" stroke="#000" stroke-linejoin="round" stroke-width="3" /> - <use transform="rotate(11.25)" xlink:href="#cog" id="use3321" /> - <use transform="rotate(22.5)" xlink:href="#cog" id="use3323" /> - <use transform="rotate(33.75)" xlink:href="#cog" id="use3325" /> - <use transform="rotate(45)" xlink:href="#cog" id="use3327" /> - <use transform="rotate(56.25)" xlink:href="#cog" id="use3329" /> - <use transform="rotate(67.5)" xlink:href="#cog" id="use3331" /> - <use transform="rotate(78.75)" xlink:href="#cog" id="use3333" /> - <use transform="rotate(90)" xlink:href="#cog" id="use3335" /> - <use transform="rotate(101.25)" xlink:href="#cog" id="use3337" /> - <use transform="rotate(112.5)" xlink:href="#cog" id="use3339" /> - <use transform="rotate(123.75)" xlink:href="#cog" id="use3341" /> - <use transform="rotate(135)" xlink:href="#cog" id="use3343" /> - <use transform="rotate(146.25)" xlink:href="#cog" id="use3345" /> - <use transform="rotate(157.5)" xlink:href="#cog" id="use3347" /> - <use transform="rotate(168.75)" xlink:href="#cog" id="use3349" /> - <use transform="scale(-1)" xlink:href="#cog" id="use3351" /> - <use transform="rotate(191.25)" xlink:href="#cog" id="use3353" /> - <use transform="rotate(202.5)" xlink:href="#cog" id="use3355" /> - <use transform="rotate(213.75)" xlink:href="#cog" id="use3357" /> - <use transform="rotate(225)" xlink:href="#cog" id="use3359" /> - <use transform="rotate(236.25)" xlink:href="#cog" id="use3361" /> - <use transform="rotate(247.5)" xlink:href="#cog" id="use3363" /> - <use transform="rotate(258.75)" xlink:href="#cog" id="use3365" /> - <use transform="rotate(-90)" xlink:href="#cog" id="use3367" /> - <use transform="rotate(-78.75)" xlink:href="#cog" id="use3369" /> - <use transform="rotate(-67.5)" xlink:href="#cog" id="use3371" /> - <use transform="rotate(-56.25)" xlink:href="#cog" id="use3373" /> - <use transform="rotate(-45)" xlink:href="#cog" id="use3375" /> - <use transform="rotate(-33.75)" xlink:href="#cog" id="use3377" /> - <use transform="rotate(-22.5)" xlink:href="#cog" id="use3379" /> - <use transform="rotate(-11.25)" xlink:href="#cog" id="use3381" /> - </g> - <g id="mounts" transform="scale(-1,1)"> - <polygon id="mount" points="7 -42 -7 -42 0 -35" stroke="#000" stroke-linejoin="round" stroke-width="6" /> - <use transform="rotate(72)" xlink:href="#mount" id="use3385" /> - <use transform="rotate(144)" xlink:href="#mount" id="use3387" /> - <use transform="rotate(216)" xlink:href="#mount" id="use3389" /> - <use transform="rotate(-72)" xlink:href="#mount" id="use3391" /> - </g> - </g> - <mask id="mask3407"> - <rect x="-60" y="-60" width="120" height="120" fill="#fff" id="rect3395" /> - <circle cy="-40" r="3" id="circle3397" /> - <use transform="rotate(72)" xlink:href="#hole" id="use3399" /> - <use transform="rotate(144)" xlink:href="#hole" id="use3401" /> - <use transform="rotate(216)" xlink:href="#hole" id="use3403" /> - <use transform="rotate(-72)" xlink:href="#hole" id="use3405" /> - </mask> - </g> - </g> - <metadata id="metadata3294"> - <rdf:RDF> - <cc:Work rdf:about=""> - <dc:format>image/svg+xml</dc:format> - <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> - <dc:title>Vaultwarden Logo</dc:title> - <dc:creator> - <cc:Agent> - <dc:title>Mathijs van Veluw</dc:title> - </cc:Agent> - </dc:creator> - <dc:relation>Rust Logo</dc:relation> - </cc:Work> - </rdf:RDF> - </metadata> -</svg> diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs @@ -8,9 +8,8 @@ use crate::{ PasswordOrOtpData, UpdateType, }, auth::{decode_delete, decode_invite, decode_verify_email, ClientHeaders, Headers}, - crypto, + config, crypto, db::{models::*, DbConn}, - mail, CONFIG, }; use rocket::{ @@ -96,7 +95,7 @@ fn clean_password_hint(password_hint: &Option<String>) -> Option<String> { } fn enforce_password_hint_setting(password_hint: &Option<String>) -> EmptyResult { - if password_hint.is_some() && !CONFIG.password_hints_allowed() { + if password_hint.is_some() { err!("Password hints have been disabled by the administrator. Remove the hint and try again."); } Ok(()) @@ -123,9 +122,6 @@ pub async fn _register(data: JsonUpcase<RegisterData>, mut conn: DbConn) -> Json // can retry without losing their invitation below. let password_hint = clean_password_hint(&data.MasterPasswordHint); enforce_password_hint_setting(&password_hint)?; - - let mut verified_by_invite = false; - let mut user = match User::find_by_mail(&email, &mut conn).await { Some(mut user) => { if !user.password_hash.is_empty() { @@ -135,8 +131,6 @@ pub async fn _register(data: JsonUpcase<RegisterData>, mut conn: DbConn) -> Json if let Some(token) = data.Token { let claims = decode_invite(&token)?; if claims.email == email { - // Verify the email address when signing up via a valid invite token - verified_by_invite = true; user.verified_at = Some(Utc::now().naive_utc()); user } else { @@ -151,12 +145,6 @@ pub async fn _register(data: JsonUpcase<RegisterData>, mut conn: DbConn) -> Json user_org.save(&mut conn).await?; } user - } else if CONFIG.is_signup_allowed(&email) - || EmergencyAccess::find_invited_by_grantee_email(&email, &mut conn) - .await - .is_some() - { - user } else { err!("Registration not allowed or user already exists") } @@ -165,7 +153,7 @@ pub async fn _register(data: JsonUpcase<RegisterData>, mut conn: DbConn) -> Json // Order is important here; the invitation check must come first // because the vaultwarden admin can invite anyone, regardless // of other signup restrictions. - if Invitation::take(&email, &mut conn).await || CONFIG.is_signup_allowed(&email) { + if Invitation::take(&email, &mut conn).await { User::new(email.clone()) } else { err!("Registration not allowed or user already exists") @@ -199,16 +187,6 @@ pub async fn _register(data: JsonUpcase<RegisterData>, mut conn: DbConn) -> Json user.private_key = Some(keys.EncryptedPrivateKey); user.public_key = Some(keys.PublicKey); } - - if CONFIG.mail_enabled() { - if CONFIG.signups_verify() && !verified_by_invite { - mail::send_welcome_must_verify(&user.email, &user.uuid).await?; - user.last_verifying_at = Some(user.created_at); - } else { - mail::send_welcome(&user.email).await?; - } - } - user.save(&mut conn).await?; Ok(Json(json!({ "Object": "register", @@ -552,132 +530,46 @@ async fn post_sstamp( } #[derive(Deserialize)] -#[allow(non_snake_case)] +#[allow(dead_code, non_snake_case)] struct EmailTokenData { MasterPasswordHash: String, NewEmail: String, } +#[allow(unused_variables)] #[post("/accounts/email-token", data = "<data>")] -async fn post_email_token( +fn post_email_token( data: JsonUpcase<EmailTokenData>, - headers: Headers, - mut conn: DbConn, + _headers: Headers, + _conn: DbConn, ) -> EmptyResult { - if !CONFIG.email_change_allowed() { - err!("Email change is not allowed."); - } - - let data: EmailTokenData = data.into_inner().data; - let mut user = headers.user; - - if !user.check_valid_password(&data.MasterPasswordHash) { - err!("Invalid password") - } - - if User::find_by_mail(&data.NewEmail, &mut conn) - .await - .is_some() - { - err!("Email already in use"); - } - - if !CONFIG.is_email_domain_allowed(&data.NewEmail) { - err!("Email domain not allowed"); - } - - let token = crypto::generate_email_token(6); - - if CONFIG.mail_enabled() { - mail::send_change_email(&data.NewEmail, &token).await? - } - - user.email_new = Some(data.NewEmail); - user.email_new_token = Some(token); - user.save(&mut conn).await + err!("Email change is not allowed."); } #[derive(Deserialize)] -#[allow(non_snake_case)] +#[allow(dead_code, non_snake_case)] struct ChangeEmailData { MasterPasswordHash: String, NewEmail: String, - Key: String, NewMasterPasswordHash: String, Token: NumberOrString, } +#[allow(unused_variables)] #[post("/accounts/email", data = "<data>")] -async fn post_email( +fn post_email( data: JsonUpcase<ChangeEmailData>, - headers: Headers, - mut conn: DbConn, - nt: Notify<'_>, + _headers: Headers, + _conn: DbConn, + _nt: Notify<'_>, ) -> EmptyResult { - if !CONFIG.email_change_allowed() { - err!("Email change is not allowed."); - } - - let data: ChangeEmailData = data.into_inner().data; - let mut user = headers.user; - - if !user.check_valid_password(&data.MasterPasswordHash) { - err!("Invalid password") - } - - if User::find_by_mail(&data.NewEmail, &mut conn) - .await - .is_some() - { - err!("Email already in use"); - } - - match user.email_new { - Some(ref val) => { - if val != &data.NewEmail { - err!("Email change mismatch"); - } - } - None => err!("No email change pending"), - } - - if CONFIG.mail_enabled() { - // Only check the token if we sent out an email... - match user.email_new_token { - Some(ref val) => { - if *val != data.Token.into_string() { - err!("Token mismatch"); - } - } - None => err!("No email change pending"), - } - user.verified_at = Some(Utc::now().naive_utc()); - } else { - user.verified_at = None; - } - - user.email = data.NewEmail; - user.email_new = None; - user.email_new_token = None; - - user.set_password(&data.NewMasterPasswordHash, Some(data.Key), true, None); - - let save_result = user.save(&mut conn).await; - - nt.send_logout(&user, None).await; - - save_result + err!("Email change is not allowed."); } #[post("/accounts/verify-email")] -async fn post_verify_email(headers: Headers) -> EmptyResult { - let user = headers.user; - - if !CONFIG.mail_enabled() { - err!("Cannot verify email address"); - } - mail::send_verify_email(&user.email, &user.uuid).await +fn post_verify_email(_headers: Headers) -> EmptyResult { + err!("Cannot verify email address") } #[derive(Deserialize)] @@ -713,28 +605,15 @@ async fn post_verify_email_token( } #[derive(Deserialize)] -#[allow(non_snake_case)] +#[allow(dead_code, non_snake_case)] struct DeleteRecoverData { Email: String, } +#[allow(unused_variables)] #[post("/accounts/delete-recover", data = "<data>")] -async fn post_delete_recover(data: JsonUpcase<DeleteRecoverData>, mut conn: DbConn) -> EmptyResult { - let data: DeleteRecoverData = data.into_inner().data; - - if CONFIG.mail_enabled() { - if let Some(user) = User::find_by_mail(&data.Email, &mut conn).await { - mail::send_delete_account(&user.email, &user.uuid).await - } else { - Ok(()) - } - } else { - // We don't support sending emails, but we shouldn't allow anybody - // to delete accounts without at least logging in... And if the user - // cannot remember their password then they will need to contact - // the administrator to delete it... - err!("Please contact the administrator to delete your account"); - } +fn post_delete_recover(data: JsonUpcase<DeleteRecoverData>, _conn: DbConn) -> EmptyResult { + err!("Please contact the administrator to delete your account"); } #[derive(Deserialize)] @@ -794,51 +673,15 @@ fn revision_date(headers: Headers) -> JsonResult { } #[derive(Deserialize)] -#[allow(non_snake_case)] +#[allow(dead_code, non_snake_case)] struct PasswordHintData { Email: String, } +#[allow(unused_variables)] #[post("/accounts/password-hint", data = "<data>")] -async fn password_hint(data: JsonUpcase<PasswordHintData>, mut conn: DbConn) -> EmptyResult { - if !CONFIG.mail_enabled() && !CONFIG.show_password_hint() { - err!("This server is not configured to provide password hints."); - } - - 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, &mut conn).await { - 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::{rngs::SmallRng, Rng, SeedableRng}; - let mut rng = SmallRng::from_entropy(); - let delta: i32 = 100; - let sleep_ms = (1_000 + rng.gen_range(-delta..=delta)) as u64; - tokio::time::sleep(tokio::time::Duration::from_millis(sleep_ms)).await; - Ok(()) - } else { - err!(NO_HINT); - } - } - Some(user) => { - let hint: Option<String> = user.password_hint; - if CONFIG.mail_enabled() { - mail::send_password_hint(email, hint).await?; - Ok(()) - } else if let Some(hint) = hint { - err!(format!("Your password hint is: {hint}")); - } else { - err!(NO_HINT); - } - } - } +fn password_hint(data: JsonUpcase<PasswordHintData>, _conn: DbConn) -> EmptyResult { + err!("This server is not configured to provide password hints.") } #[derive(Deserialize)] @@ -975,12 +818,20 @@ impl<'r> FromRequest<'r> for KnownDevice { let email_bytes = match data_encoding::BASE64URL_NOPAD.decode(email_b64.as_bytes()) { Ok(bytes) => bytes, Err(_) => { - return Outcome::Error((Status::BadRequest, "X-Request-Email value failed to decode as base64url")); + return Outcome::Error(( + Status::BadRequest, + "X-Request-Email value failed to decode as base64url", + )); } }; match String::from_utf8(email_bytes) { Ok(email) => email, - Err(_) => return Outcome::Error((Status::BadRequest, "X-Request-Email value failed to decode as UTF-8")), + Err(_) => { + return Outcome::Error(( + Status::BadRequest, + "X-Request-Email value failed to decode as UTF-8", + )) + } } } else { return Outcome::Error((Status::BadRequest, "X-Request-Email value is required")); @@ -1086,7 +937,7 @@ async fn post_auth_request( "creationDate": auth_request.creation_date.and_utc(), "responseDate": null, "requestApproved": false, - "origin": CONFIG.domain_origin(), + "origin": config::get_config().domain_origin(), "object": "auth-request" }))) } @@ -1115,7 +966,7 @@ async fn get_auth_request(uuid: &str, mut conn: DbConn) -> JsonResult { "creationDate": auth_request.creation_date.and_utc(), "responseDate": response_date_utc, "requestApproved": auth_request.approved, - "origin": CONFIG.domain_origin(), + "origin": config::get_config().domain_origin(), "object":"auth-request" } ))) @@ -1179,7 +1030,7 @@ async fn put_auth_request( "creationDate": auth_request.creation_date.and_utc(), "responseDate": response_date_utc, "requestApproved": auth_request.approved, - "origin": CONFIG.domain_origin(), + "origin": config::get_config().domain_origin(), "object":"auth-request" } ))) @@ -1213,7 +1064,7 @@ async fn get_auth_request_response(uuid: &str, code: &str, mut conn: DbConn) -> "creationDate": auth_request.creation_date.and_utc(), "responseDate": response_date_utc, "requestApproved": auth_request.approved, - "origin": CONFIG.domain_origin(), + "origin": config::get_config().domain_origin(), "object":"auth-request" } ))) @@ -1240,7 +1091,7 @@ async fn get_auth_requests(headers: Headers, mut conn: DbConn) -> JsonResult { "creationDate": request.creation_date.and_utc(), "responseDate": response_date_utc, "requestApproved": request.approved, - "origin": CONFIG.domain_origin(), + "origin": config::get_config().domain_origin(), "object":"auth-request" }) }).collect::<Vec<Value>>(), diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs @@ -2,9 +2,9 @@ use super::folders::FolderData; use crate::{ api::{self, EmptyResult, JsonResult, JsonUpcase, Notify, PasswordOrOtpData, UpdateType}, auth::Headers, + config::{self, Config}, crypto, db::{models::*, DbConn}, - CONFIG, }; use chrono::{NaiveDateTime, Utc}; use rocket::fs::TempFile; @@ -1158,6 +1158,7 @@ struct UploadData<'f> { /// /// When used with the v2 API, post_attachment_v2() has already created the /// database record, which is passed in as `attachment`. +#[allow(clippy::cast_lossless)] async fn save_attachment( mut attachment: Option<Attachment>, cipher_uuid: &str, @@ -1186,10 +1187,11 @@ async fn save_attachment( }; let size_limit = if let Some(ref user_uuid) = cipher.user_uuid { - match CONFIG.user_attachment_limit() { + match config::get_config().user_attachment_limit { Some(0) => err!("Attachments are disabled"), Some(limit_kb) => { - let left = (limit_kb * 1024) - Attachment::size_by_user(user_uuid, &mut conn).await + let left = (limit_kb as i64 * 1024) + - Attachment::size_by_user(user_uuid, &mut conn).await + size_adjust; if left <= 0 { err!("Attachment storage limit reached! Delete some attachments to free up space") @@ -1199,10 +1201,11 @@ async fn save_attachment( None => None, } } else if let Some(ref org_uuid) = cipher.organization_uuid { - match CONFIG.org_attachment_limit() { + match config::get_config().org_attachment_limit { Some(0) => err!("Attachments are disabled"), Some(limit_kb) => { - let left = (limit_kb * 1024) - Attachment::size_by_org(org_uuid, &mut conn).await + let left = (limit_kb as i64 * 1024) + - Attachment::size_by_org(org_uuid, &mut conn).await + size_adjust; if left <= 0 { err!("Attachment storage limit reached! Delete some attachments to free up space") @@ -1228,7 +1231,7 @@ async fn save_attachment( None => crypto::generate_attachment_id(), // Legacy API }; - let folder_path = tokio::fs::canonicalize(&CONFIG.attachments_folder()) + let folder_path = tokio::fs::canonicalize(Config::ATTACHMENTS_FOLDER) .await? .join(cipher_uuid); let file_path = folder_path.join(&file_id); diff --git a/src/api/core/emergency_access.rs b/src/api/core/emergency_access.rs @@ -1,22 +1,17 @@ -use chrono::Utc; -use rocket::{serde::json::Json, Route}; -use serde_json::Value; - use crate::{ - api::{ - core::{CipherSyncData, CipherSyncType}, - EmptyResult, JsonResult, JsonUpcase, NumberOrString, - }, - auth::{decode_emergency_access_invite, Headers}, + api::{EmptyResult, JsonResult, JsonUpcase, NumberOrString}, + auth::Headers, db::{models::*, DbConn}, - mail, CONFIG, }; +use rocket::{serde::json::Json, Route}; +use serde_json::Value; pub fn routes() -> Vec<Route> { routes![ get_contacts, get_grantees, get_emergency_access, + post_emergency_access, put_emergency_access, delete_emergency_access, post_delete_emergency_access, @@ -33,737 +28,166 @@ pub fn routes() -> Vec<Route> { policies_emergency_access, ] } - -// region get - #[get("/emergency-access/trusted")] -async fn get_contacts(headers: Headers, mut conn: DbConn) -> JsonResult { - check_emergency_access_allowed()?; - - let emergency_access_list = - EmergencyAccess::find_all_by_grantor_uuid(&headers.user.uuid, &mut conn).await; - let mut emergency_access_list_json = Vec::with_capacity(emergency_access_list.len()); - for ea in emergency_access_list { - emergency_access_list_json.push(ea.to_json_grantee_details(&mut conn).await); - } - - Ok(Json(json!({ - "Data": emergency_access_list_json, - "Object": "list", - "ContinuationToken": null - }))) +fn get_contacts(_headers: Headers, _conn: DbConn) -> JsonResult { + err!("Emergency access is not allowed.") } #[get("/emergency-access/granted")] -async fn get_grantees(headers: Headers, mut conn: DbConn) -> JsonResult { - check_emergency_access_allowed()?; - - let emergency_access_list = - EmergencyAccess::find_all_by_grantee_uuid(&headers.user.uuid, &mut conn).await; - let mut emergency_access_list_json = Vec::with_capacity(emergency_access_list.len()); - for ea in emergency_access_list { - emergency_access_list_json.push(ea.to_json_grantor_details(&mut conn).await); - } - - Ok(Json(json!({ - "Data": emergency_access_list_json, - "Object": "list", - "ContinuationToken": null - }))) +fn get_grantees(_headers: Headers, _conn: DbConn) -> JsonResult { + err!("Emergency access is not allowed.") } - +#[allow(unused_variables)] #[get("/emergency-access/<emer_id>")] -async fn get_emergency_access(emer_id: &str, mut conn: DbConn) -> JsonResult { - check_emergency_access_allowed()?; - - match EmergencyAccess::find_by_uuid(emer_id, &mut conn).await { - Some(emergency_access) => Ok(Json( - emergency_access.to_json_grantee_details(&mut conn).await, - )), - None => err!("Emergency access not valid."), - } +fn get_emergency_access(emer_id: &str, _conn: DbConn) -> JsonResult { + err!("Emergency access is not allowed.") } -// endregion - -// region put/post - #[derive(Deserialize)] -#[allow(non_snake_case)] +#[allow(dead_code, non_snake_case)] struct EmergencyAccessUpdateData { Type: NumberOrString, WaitTimeDays: i32, KeyEncrypted: Option<String>, } - +#[allow(unused_variables)] #[put("/emergency-access/<emer_id>", data = "<data>")] -async fn put_emergency_access( +fn put_emergency_access( emer_id: &str, data: JsonUpcase<EmergencyAccessUpdateData>, - conn: DbConn, + _conn: DbConn, ) -> JsonResult { - post_emergency_access(emer_id, data, conn).await + err!("Emergency access is not allowed.") } +#[allow(unused_variables)] #[post("/emergency-access/<emer_id>", data = "<data>")] -async fn post_emergency_access( +fn post_emergency_access( emer_id: &str, data: JsonUpcase<EmergencyAccessUpdateData>, - mut conn: DbConn, + _conn: DbConn, ) -> JsonResult { - check_emergency_access_allowed()?; - - let data: EmergencyAccessUpdateData = data.into_inner().data; - - let mut emergency_access = match EmergencyAccess::find_by_uuid(emer_id, &mut conn).await { - Some(emergency_access) => emergency_access, - None => err!("Emergency access not valid."), - }; - - let new_type = match EmergencyAccessType::from_str(&data.Type.into_string()) { - Some(new_type) => new_type as i32, - None => err!("Invalid emergency access type."), - }; - - emergency_access.atype = new_type; - emergency_access.wait_time_days = data.WaitTimeDays; - if data.KeyEncrypted.is_some() { - emergency_access.key_encrypted = data.KeyEncrypted; - } - - emergency_access.save(&mut conn).await?; - Ok(Json(emergency_access.to_json())) + err!("Emergency access is not allowed.") } -// endregion - -// region delete - +#[allow(unused_variables)] #[delete("/emergency-access/<emer_id>")] -async fn delete_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> EmptyResult { - check_emergency_access_allowed()?; - - let grantor_user = headers.user; - - let emergency_access = match EmergencyAccess::find_by_uuid(emer_id, &mut conn).await { - Some(emer) => { - if emer.grantor_uuid != grantor_user.uuid - && emer.grantee_uuid != Some(grantor_user.uuid) - { - err!("Emergency access not valid.") - } - emer - } - None => err!("Emergency access not valid."), - }; - emergency_access.delete(&mut conn).await?; - Ok(()) +fn delete_emergency_access(emer_id: &str, _headers: Headers, _conn: DbConn) -> EmptyResult { + err!("Emergency access is not allowed.") } +#[allow(unused_variables)] #[post("/emergency-access/<emer_id>/delete")] -async fn post_delete_emergency_access( - emer_id: &str, - headers: Headers, - conn: DbConn, -) -> EmptyResult { - delete_emergency_access(emer_id, headers, conn).await +fn post_delete_emergency_access(emer_id: &str, _headers: Headers, _conn: DbConn) -> EmptyResult { + err!("Emergency access is not allowed.") } -// endregion - -// region invite - #[derive(Deserialize)] -#[allow(non_snake_case)] +#[allow(dead_code, non_snake_case)] struct EmergencyAccessInviteData { Email: String, Type: NumberOrString, WaitTimeDays: i32, } +#[allow(unused_variables)] #[post("/emergency-access/invite", data = "<data>")] -async fn send_invite( +fn send_invite( data: JsonUpcase<EmergencyAccessInviteData>, - headers: Headers, - mut conn: DbConn, + _headers: Headers, + _conn: DbConn, ) -> EmptyResult { - check_emergency_access_allowed()?; - - let data: EmergencyAccessInviteData = data.into_inner().data; - let email = data.Email.to_lowercase(); - let wait_time_days = data.WaitTimeDays; - - let emergency_access_status = EmergencyAccessStatus::Invited as i32; - - let new_type = match EmergencyAccessType::from_str(&data.Type.into_string()) { - Some(new_type) => new_type as i32, - None => err!("Invalid emergency access type."), - }; - - let grantor_user = headers.user; - - // avoid setting yourself as emergency contact - if email == grantor_user.email { - err!("You can not set yourself as an emergency contact.") - } - - let grantee_user = match User::find_by_mail(&email, &mut conn).await { - None => { - if !CONFIG.invitations_allowed() { - err!(format!("Grantee user does not exist: {}", &email)) - } - - if !CONFIG.is_email_domain_allowed(&email) { - err!("Email domain not eligible for invitations") - } - - if !CONFIG.mail_enabled() { - let invitation = Invitation::new(&email); - invitation.save(&mut conn).await?; - } - - let mut user = User::new(email.clone()); - user.save(&mut conn).await?; - user - } - Some(user) => user, - }; - - if EmergencyAccess::find_by_grantor_uuid_and_grantee_uuid_or_email( - &grantor_user.uuid, - &grantee_user.uuid, - &grantee_user.email, - &mut conn, - ) - .await - .is_some() - { - err!(format!( - "Grantee user already invited: {}", - &grantee_user.email - )) - } - - let mut new_emergency_access = EmergencyAccess::new( - grantor_user.uuid, - grantee_user.email, - emergency_access_status, - new_type, - wait_time_days, - ); - new_emergency_access.save(&mut conn).await?; - - if CONFIG.mail_enabled() { - mail::send_emergency_access_invite( - &new_emergency_access - .email - .expect("Grantee email does not exists"), - &grantee_user.uuid, - &new_emergency_access.uuid, - &grantor_user.name, - &grantor_user.email, - ) - .await?; - } else { - // Automatically mark user as accepted if no email invites - match User::find_by_mail(&email, &mut conn).await { - Some(user) => match accept_invite_process( - &user.uuid, - &mut new_emergency_access, - &email, - &mut conn, - ) - .await - { - Ok(v) => v, - Err(e) => err!(e.to_string()), - }, - None => err!("Grantee user not found."), - } - } - - Ok(()) + err!("Emergency access is not allowed.") } +#[allow(unused_variables)] #[post("/emergency-access/<emer_id>/reinvite")] -async fn resend_invite(emer_id: &str, headers: Headers, mut conn: DbConn) -> EmptyResult { - check_emergency_access_allowed()?; - - let mut emergency_access = match EmergencyAccess::find_by_uuid(emer_id, &mut conn).await { - Some(emer) => emer, - None => err!("Emergency access not valid."), - }; - - if emergency_access.grantor_uuid != headers.user.uuid { - err!("Emergency access not valid."); - } - - if emergency_access.status != EmergencyAccessStatus::Invited as i32 { - err!("The grantee user is already accepted or confirmed to the organization"); - } - - let email = match emergency_access.email.clone() { - Some(email) => email, - None => err!("Email not valid."), - }; - - let grantee_user = match User::find_by_mail(&email, &mut conn).await { - Some(user) => user, - None => err!("Grantee user not found."), - }; - - let grantor_user = headers.user; - - if CONFIG.mail_enabled() { - mail::send_emergency_access_invite( - &email, - &grantor_user.uuid, - &emergency_access.uuid, - &grantor_user.name, - &grantor_user.email, - ) - .await?; - } else { - if Invitation::find_by_mail(&email, &mut conn).await.is_none() { - let invitation = Invitation::new(&email); - invitation.save(&mut conn).await?; - } - - // Automatically mark user as accepted if no email invites - match accept_invite_process(&grantee_user.uuid, &mut emergency_access, &email, &mut conn) - .await - { - Ok(v) => v, - Err(e) => err!(e.to_string()), - } - } - - Ok(()) +fn resend_invite(emer_id: &str, _headers: Headers, _conn: DbConn) -> EmptyResult { + err!("Emergency access is not allowed.") } #[derive(Deserialize)] -#[allow(non_snake_case)] +#[allow(dead_code, non_snake_case)] struct AcceptData { Token: String, } +#[allow(unused_variables)] #[post("/emergency-access/<emer_id>/accept", data = "<data>")] -async fn accept_invite( +fn accept_invite( emer_id: &str, data: JsonUpcase<AcceptData>, - headers: Headers, - mut conn: DbConn, -) -> EmptyResult { - check_emergency_access_allowed()?; - - let data: AcceptData = data.into_inner().data; - let token = &data.Token; - let claims = decode_emergency_access_invite(token)?; - - // This can happen if the user who received the invite used a different email to signup. - // Since we do not know if this is intended, we error out here and do nothing with the invite. - if claims.email != headers.user.email { - err!("Claim email does not match current users email") - } - - let grantee_user = match User::find_by_mail(&claims.email, &mut conn).await { - Some(user) => { - Invitation::take(&claims.email, &mut conn).await; - user - } - None => err!("Invited user not found"), - }; - - let mut emergency_access = match EmergencyAccess::find_by_uuid(emer_id, &mut conn).await { - Some(emer) => emer, - None => err!("Emergency access not valid."), - }; - - // get grantor user to send Accepted email - let grantor_user = match User::find_by_uuid(&emergency_access.grantor_uuid, &mut conn).await { - Some(user) => user, - None => err!("Grantor user not found."), - }; - - if emer_id == claims.emer_id - && grantor_user.name == claims.grantor_name - && grantor_user.email == claims.grantor_email - { - match accept_invite_process( - &grantee_user.uuid, - &mut emergency_access, - &grantee_user.email, - &mut conn, - ) - .await - { - Ok(v) => v, - Err(e) => err!(e.to_string()), - } - - if CONFIG.mail_enabled() { - mail::send_emergency_access_invite_accepted(&grantor_user.email, &grantee_user.email) - .await?; - } - - Ok(()) - } else { - err!("Emergency access invitation error.") - } -} - -async fn accept_invite_process( - grantee_uuid: &str, - emergency_access: &mut EmergencyAccess, - grantee_email: &str, - conn: &mut DbConn, + _headers: Headers, + _conn: DbConn, ) -> EmptyResult { - if emergency_access.email.is_none() || emergency_access.email.as_ref().unwrap() != grantee_email - { - err!("User email does not match invite."); - } - - if emergency_access.status == EmergencyAccessStatus::Accepted as i32 { - err!("Emergency contact already accepted."); - } - - emergency_access.status = EmergencyAccessStatus::Accepted as i32; - emergency_access.grantee_uuid = Some(String::from(grantee_uuid)); - emergency_access.email = None; - emergency_access.save(conn).await + err!("Emergency access is not allowed.") } #[derive(Deserialize)] -#[allow(non_snake_case)] +#[allow(dead_code, non_snake_case)] struct ConfirmData { Key: String, } +#[allow(unused_variables)] #[post("/emergency-access/<emer_id>/confirm", data = "<data>")] -async fn confirm_emergency_access( +fn confirm_emergency_access( emer_id: &str, data: JsonUpcase<ConfirmData>, - headers: Headers, - mut conn: DbConn, + _headers: Headers, + _conn: DbConn, ) -> JsonResult { - check_emergency_access_allowed()?; - - let confirming_user = headers.user; - let data: ConfirmData = data.into_inner().data; - let key = data.Key; - - let mut emergency_access = match EmergencyAccess::find_by_uuid(emer_id, &mut conn).await { - Some(emer) => emer, - None => err!("Emergency access not valid."), - }; - - if emergency_access.status != EmergencyAccessStatus::Accepted as i32 - || emergency_access.grantor_uuid != confirming_user.uuid - { - err!("Emergency access not valid.") - } - - let grantor_user = match User::find_by_uuid(&confirming_user.uuid, &mut conn).await { - Some(user) => user, - None => err!("Grantor user not found."), - }; - - if let Some(grantee_uuid) = emergency_access.grantee_uuid.as_ref() { - let grantee_user = match User::find_by_uuid(grantee_uuid, &mut conn).await { - Some(user) => user, - None => err!("Grantee user not found."), - }; - - emergency_access.status = EmergencyAccessStatus::Confirmed as i32; - emergency_access.key_encrypted = Some(key); - emergency_access.email = None; - - emergency_access.save(&mut conn).await?; - - if CONFIG.mail_enabled() { - mail::send_emergency_access_invite_confirmed(&grantee_user.email, &grantor_user.name) - .await?; - } - Ok(Json(emergency_access.to_json())) - } else { - err!("Grantee user not found.") - } + err!("Emergency access is not allowed.") } -// endregion - -// region access emergency access - +#[allow(unused_variables)] #[post("/emergency-access/<emer_id>/initiate")] -async fn initiate_emergency_access( - emer_id: &str, - headers: Headers, - mut conn: DbConn, -) -> JsonResult { - check_emergency_access_allowed()?; - - let initiating_user = headers.user; - let mut emergency_access = match EmergencyAccess::find_by_uuid(emer_id, &mut conn).await { - Some(emer) => emer, - None => err!("Emergency access not valid."), - }; - - if emergency_access.status != EmergencyAccessStatus::Confirmed as i32 - || emergency_access.grantee_uuid != Some(initiating_user.uuid) - { - err!("Emergency access not valid.") - } - - let grantor_user = match User::find_by_uuid(&emergency_access.grantor_uuid, &mut conn).await { - Some(user) => user, - None => err!("Grantor user not found."), - }; - - let now = Utc::now().naive_utc(); - emergency_access.status = EmergencyAccessStatus::RecoveryInitiated as i32; - emergency_access.updated_at = now; - emergency_access.recovery_initiated_at = Some(now); - emergency_access.last_notification_at = Some(now); - emergency_access.save(&mut conn).await?; - - if CONFIG.mail_enabled() { - mail::send_emergency_access_recovery_initiated( - &grantor_user.email, - &initiating_user.name, - emergency_access.get_type_as_str(), - &emergency_access.wait_time_days, - ) - .await?; - } - Ok(Json(emergency_access.to_json())) +fn initiate_emergency_access(emer_id: &str, _headers: Headers, _conn: DbConn) -> JsonResult { + err!("Emergency access is not allowed.") } +#[allow(unused_variables)] #[post("/emergency-access/<emer_id>/approve")] -async fn approve_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult { - check_emergency_access_allowed()?; - - let mut emergency_access = match EmergencyAccess::find_by_uuid(emer_id, &mut conn).await { - Some(emer) => emer, - None => err!("Emergency access not valid."), - }; - - if emergency_access.status != EmergencyAccessStatus::RecoveryInitiated as i32 - || emergency_access.grantor_uuid != headers.user.uuid - { - err!("Emergency access not valid.") - } - - let grantor_user = match User::find_by_uuid(&headers.user.uuid, &mut conn).await { - Some(user) => user, - None => err!("Grantor user not found."), - }; - - if let Some(grantee_uuid) = emergency_access.grantee_uuid.as_ref() { - let grantee_user = match User::find_by_uuid(grantee_uuid, &mut conn).await { - Some(user) => user, - None => err!("Grantee user not found."), - }; - - emergency_access.status = EmergencyAccessStatus::RecoveryApproved as i32; - emergency_access.save(&mut conn).await?; - - if CONFIG.mail_enabled() { - mail::send_emergency_access_recovery_approved(&grantee_user.email, &grantor_user.name) - .await?; - } - Ok(Json(emergency_access.to_json())) - } else { - err!("Grantee user not found.") - } +fn approve_emergency_access(emer_id: &str, _headers: Headers, _conn: DbConn) -> JsonResult { + err!("Emergency access is not allowed.") } +#[allow(unused_variables)] #[post("/emergency-access/<emer_id>/reject")] -async fn reject_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult { - check_emergency_access_allowed()?; - - let mut emergency_access = match EmergencyAccess::find_by_uuid(emer_id, &mut conn).await { - Some(emer) => emer, - None => err!("Emergency access not valid."), - }; - - if (emergency_access.status != EmergencyAccessStatus::RecoveryInitiated as i32 - && emergency_access.status != EmergencyAccessStatus::RecoveryApproved as i32) - || emergency_access.grantor_uuid != headers.user.uuid - { - err!("Emergency access not valid.") - } - - let grantor_user = match User::find_by_uuid(&headers.user.uuid, &mut conn).await { - Some(user) => user, - None => err!("Grantor user not found."), - }; - - if let Some(grantee_uuid) = emergency_access.grantee_uuid.as_ref() { - let grantee_user = match User::find_by_uuid(grantee_uuid, &mut conn).await { - Some(user) => user, - None => err!("Grantee user not found."), - }; - - emergency_access.status = EmergencyAccessStatus::Confirmed as i32; - emergency_access.save(&mut conn).await?; - - if CONFIG.mail_enabled() { - mail::send_emergency_access_recovery_rejected(&grantee_user.email, &grantor_user.name) - .await?; - } - Ok(Json(emergency_access.to_json())) - } else { - err!("Grantee user not found.") - } +fn reject_emergency_access(emer_id: &str, _headers: Headers, _conn: DbConn) -> JsonResult { + err!("Emergency access is not allowed.") } -// endregion - -// region action - +#[allow(unused_variables)] #[post("/emergency-access/<emer_id>/view")] -async fn view_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult { - check_emergency_access_allowed()?; - - let emergency_access = match EmergencyAccess::find_by_uuid(emer_id, &mut conn).await { - Some(emer) => emer, - None => err!("Emergency access not valid."), - }; - - if !is_valid_request( - &emergency_access, - &headers.user.uuid, - EmergencyAccessType::View, - ) { - err!("Emergency access not valid.") - } - - let ciphers = Cipher::find_owned_by_user(&emergency_access.grantor_uuid, &mut conn).await; - let cipher_sync_data = CipherSyncData::new( - &emergency_access.grantor_uuid, - CipherSyncType::User, - &mut conn, - ) - .await; - - let mut ciphers_json = Vec::with_capacity(ciphers.len()); - for c in ciphers { - ciphers_json.push( - c.to_json( - &headers.host, - &emergency_access.grantor_uuid, - Some(&cipher_sync_data), - CipherSyncType::User, - &mut conn, - ) - .await, - ); - } - - Ok(Json(json!({ - "Ciphers": ciphers_json, - "KeyEncrypted": &emergency_access.key_encrypted, - "Object": "emergencyAccessView", - }))) +fn view_emergency_access(emer_id: &str, _headers: Headers, _conn: DbConn) -> JsonResult { + err!("Emergency access is not allowed.") } +#[allow(unused_variables)] #[post("/emergency-access/<emer_id>/takeover")] -async fn takeover_emergency_access( - emer_id: &str, - headers: Headers, - mut conn: DbConn, -) -> JsonResult { - check_emergency_access_allowed()?; - - let requesting_user = headers.user; - let emergency_access = match EmergencyAccess::find_by_uuid(emer_id, &mut conn).await { - Some(emer) => emer, - None => err!("Emergency access not valid."), - }; - - if !is_valid_request( - &emergency_access, - &requesting_user.uuid, - EmergencyAccessType::Takeover, - ) { - err!("Emergency access not valid.") - } - - let grantor_user = match User::find_by_uuid(&emergency_access.grantor_uuid, &mut conn).await { - Some(user) => user, - None => err!("Grantor user not found."), - }; - - let result = json!({ - "Kdf": grantor_user.client_kdf_type, - "KdfIterations": grantor_user.client_kdf_iter, - "KdfMemory": grantor_user.client_kdf_memory, - "KdfParallelism": grantor_user.client_kdf_parallelism, - "KeyEncrypted": &emergency_access.key_encrypted, - "Object": "emergencyAccessTakeover", - }); - - Ok(Json(result)) +fn takeover_emergency_access(emer_id: &str, _headers: Headers, _conn: DbConn) -> JsonResult { + err!("Emergency access is not allowed.") } #[derive(Deserialize)] -#[allow(non_snake_case)] +#[allow(dead_code, non_snake_case)] struct EmergencyAccessPasswordData { NewMasterPasswordHash: String, Key: String, } +#[allow(unused_variables)] #[post("/emergency-access/<emer_id>/password", data = "<data>")] -async fn password_emergency_access( +fn password_emergency_access( emer_id: &str, data: JsonUpcase<EmergencyAccessPasswordData>, - headers: Headers, - mut conn: DbConn, + _headers: Headers, + _conn: DbConn, ) -> EmptyResult { - check_emergency_access_allowed()?; - - let data: EmergencyAccessPasswordData = data.into_inner().data; - let new_master_password_hash = &data.NewMasterPasswordHash; - //let key = &data.Key; - - let requesting_user = headers.user; - let emergency_access = match EmergencyAccess::find_by_uuid(emer_id, &mut conn).await { - Some(emer) => emer, - None => err!("Emergency access not valid."), - }; - - if !is_valid_request( - &emergency_access, - &requesting_user.uuid, - EmergencyAccessType::Takeover, - ) { - err!("Emergency access not valid.") - } - - let mut grantor_user = match User::find_by_uuid(&emergency_access.grantor_uuid, &mut conn).await - { - Some(user) => user, - None => err!("Grantor user not found."), - }; - - // change grantor_user password - grantor_user.set_password(new_master_password_hash, Some(data.Key), true, None); - grantor_user.save(&mut conn).await?; - - // Disable TwoFactor providers since they will otherwise block logins - TwoFactor::delete_all_by_user(&grantor_user.uuid, &mut conn).await?; - - // Remove grantor from all organisations unless Owner - for user_org in UserOrganization::find_any_state_by_user(&grantor_user.uuid, &mut conn).await { - if user_org.atype != UserOrgType::Owner as i32 { - user_org.delete(&mut conn).await?; - } - } - Ok(()) + err!("Emergency access is not allowed.") } -// endregion - #[get("/emergency-access/<emer_id>/policies")] async fn policies_emergency_access( emer_id: &str, @@ -809,10 +233,3 @@ fn is_valid_request( && emergency_access.status == EmergencyAccessStatus::RecoveryApproved as i32 && emergency_access.atype == requested_access_type as i32 } - -fn check_emergency_access_allowed() -> EmptyResult { - if !CONFIG.emergency_access_allowed() { - err!("Emergency access is not allowed.") - } - Ok(()) -} diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs @@ -150,7 +150,7 @@ fn version() -> Json<&'static str> { #[get("/config")] fn config() -> Json<Value> { - let domain = crate::CONFIG.domain(); + let domain = &crate::config::get_config().domain; Json(json!({ // Note: The clients use this version to handle backwards compatibility concerns // This means they expect a version that closely matches the Bitwarden server version diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs @@ -9,9 +9,7 @@ use crate::{ }, db::{models::*, DbConn}, error::Error, - mail, util::convert_json_key_lcase_first, - CONFIG, }; use num_traits::FromPrimitive; use rocket::serde::json::Json; @@ -102,7 +100,7 @@ pub fn routes() -> Vec<Route> { } #[derive(Deserialize)] -#[allow(non_snake_case)] +#[allow(dead_code, non_snake_case)] struct OrgData { BillingEmail: String, CollectionName: String, @@ -150,50 +148,10 @@ struct OrgBulkIds { Ids: Vec<String>, } +#[allow(unused_variables)] #[post("/organizations", data = "<data>")] -async fn create_organization( - headers: Headers, - data: JsonUpcase<OrgData>, - mut conn: DbConn, -) -> JsonResult { - if !CONFIG.is_org_creation_allowed(&headers.user.email) { - err!("User not allowed to create organizations") - } - if OrgPolicy::is_applicable_to_user( - &headers.user.uuid, - OrgPolicyType::SingleOrg, - None, - &mut conn, - ) - .await - { - err!( - "You may not create an organization. You belong to an organization which has a policy that prohibits you from being a member of any other organization." - ) - } - - let data: OrgData = data.into_inner().data; - let (private_key, public_key) = if data.Keys.is_some() { - let keys: OrgKeyData = data.Keys.unwrap(); - (Some(keys.EncryptedPrivateKey), Some(keys.PublicKey)) - } else { - (None, None) - }; - - let org = Organization::new(data.Name, data.BillingEmail, private_key, public_key); - let mut user_org = UserOrganization::new(headers.user.uuid, org.uuid.clone()); - let collection = Collection::new(org.uuid.clone(), data.CollectionName, None); - - user_org.akey = data.Key; - user_org.access_all = true; - user_org.atype = UserOrgType::Owner as i32; - user_org.status = UserOrgStatus::Confirmed as i32; - - org.save(&mut conn).await?; - user_org.save(&mut conn).await?; - collection.save(&mut conn).await?; - - Ok(Json(org.to_json())) +fn create_organization(_headers: Headers, data: JsonUpcase<OrgData>, _conn: DbConn) -> JsonResult { + err!("User not allowed to create organizations") } #[delete("/organizations/<org_id>", data = "<data>")] @@ -204,9 +162,7 @@ async fn delete_organization( mut conn: DbConn, ) -> EmptyResult { let data: PasswordOrOtpData = data.into_inner().data; - data.validate(&headers.user, true, &mut conn).await?; - match Organization::find_by_uuid(org_id, &mut conn).await { None => err!("Organization not found"), Some(org) => org.delete(&mut conn).await, @@ -270,7 +226,6 @@ async fn post_organization( mut conn: DbConn, ) -> JsonResult { let data: OrganizationUpdateData = data.into_inner().data; - let mut org = match Organization::find_by_uuid(org_id, &mut conn).await { Some(organization) => organization, None => err!("Can't find organization details"), @@ -895,22 +850,7 @@ async fn send_invite( let mut user_org_status = UserOrgStatus::Invited as i32; let user = match User::find_by_mail(&email, &mut conn).await { None => { - if !CONFIG.invitations_allowed() { - err!(format!("User does not exist: {email}")) - } - - if !CONFIG.is_email_domain_allowed(&email) { - err!("Email domain not eligible for invitations") - } - - if !CONFIG.mail_enabled() { - let invitation = Invitation::new(&email); - invitation.save(&mut conn).await?; - } - - let mut user = User::new(email.clone()); - user.save(&mut conn).await?; - user + err!(format!("User does not exist: {email}")) } Some(user) => { if UserOrganization::find_by_user_and_org(&user.uuid, org_id, &mut conn) @@ -920,7 +860,7 @@ async fn send_invite( err!(format!("User already in organization: {email}")) } else { // automatically accept existing users if mail is disabled - if !CONFIG.mail_enabled() && !user.password_hash.is_empty() { + if !user.password_hash.is_empty() { user_org_status = UserOrgStatus::Accepted as i32; } user @@ -952,60 +892,34 @@ async fn send_invite( } } } - new_user.save(&mut conn).await?; - for group in data.Groups.iter() { let mut group_entry = GroupUser::new(String::from(group), user.uuid.clone()); group_entry.save(&mut conn).await?; } - if CONFIG.mail_enabled() { - let org_name = match Organization::find_by_uuid(org_id, &mut conn).await { - Some(org) => org.name, - None => err!("Error looking up organization"), - }; - - mail::send_invite( - &email, - &user.uuid, - Some(String::from(org_id)), - Some(new_user.uuid), - &org_name, - Some(headers.user.email.clone()), - ) - .await?; - } } Ok(()) } - +#[allow(unused_variables)] #[post("/organizations/<org_id>/users/reinvite", data = "<data>")] -async fn bulk_reinvite_user( +fn bulk_reinvite_user( org_id: &str, data: JsonUpcase<OrgBulkIds>, - headers: AdminHeaders, - mut conn: DbConn, + _headers: AdminHeaders, + _conn: DbConn, ) -> Json<Value> { let data: OrgBulkIds = data.into_inner().data; - let mut bulk_response = Vec::new(); for org_user_id in data.Ids { - let err_msg = - match _reinvite_user(org_id, &org_user_id, &headers.user.email, &mut conn).await { - Ok(_) => String::new(), - Err(e) => format!("{e:?}"), - }; - bulk_response.push(json!( { "Object": "OrganizationBulkConfirmResponseModel", "Id": org_user_id, - "Error": err_msg + "Error": format!("{:?}", crate::error::Error::new("Invitations are not allowed.", "Invitations are not allowed.")) } )) } - Json(json!({ "Data": bulk_response, "Object": "list", @@ -1013,65 +927,15 @@ async fn bulk_reinvite_user( })) } +#[allow(unused_variables)] #[post("/organizations/<org_id>/users/<user_org>/reinvite")] -async fn reinvite_user( +fn reinvite_user( org_id: &str, user_org: &str, - headers: AdminHeaders, - mut conn: DbConn, + _headers: AdminHeaders, + _conn: DbConn, ) -> EmptyResult { - _reinvite_user(org_id, user_org, &headers.user.email, &mut conn).await -} - -async fn _reinvite_user( - org_id: &str, - user_org: &str, - invited_by_email: &str, - conn: &mut DbConn, -) -> EmptyResult { - if !CONFIG.invitations_allowed() { - err!("Invitations are not allowed.") - } - - if !CONFIG.mail_enabled() { - err!("SMTP is not configured.") - } - - let user_org = match UserOrganization::find_by_uuid(user_org, conn).await { - Some(user_org) => user_org, - None => err!("The user hasn't been invited to the organization."), - }; - - if user_org.status != UserOrgStatus::Invited as i32 { - err!("The user is already accepted or confirmed to the organization") - } - - let user = match User::find_by_uuid(&user_org.user_uuid, conn).await { - Some(user) => user, - None => err!("User not found."), - }; - - let org_name = match Organization::find_by_uuid(org_id, conn).await { - Some(org) => org.name, - None => err!("Error looking up organization."), - }; - - if CONFIG.mail_enabled() { - mail::send_invite( - &user.email, - &user.uuid, - Some(org_id.to_string()), - Some(user_org.uuid), - &org_name, - Some(invited_by_email.to_string()), - ) - .await?; - } else { - let invitation = Invitation::new(&user.email); - invitation.save(conn).await?; - } - - Ok(()) + err!("Invitations are not allowed.") } #[derive(Deserialize)] @@ -1140,24 +1004,6 @@ async fn accept_invite( } None => err!("Invited user not found"), } - - if CONFIG.mail_enabled() { - let mut org_name = CONFIG.invitation_org_name(); - if let Some(org_id) = &claims.org_id { - org_name = match Organization::find_by_uuid(org_id, &mut conn).await { - Some(org) => org.name, - None => err!("Organization not found."), - }; - }; - if let Some(invited_by_email) = &claims.invited_by_email { - // User was invited to an organization, so they must be confirmed manually after acceptance - mail::send_invite_accepted(&claims.email, invited_by_email, &org_name).await?; - } else { - // User was invited from /admin, so they are automatically confirmed - mail::send_invite_confirmed(&claims.email, &org_name).await?; - } - } - Ok(()) } @@ -1260,24 +1106,10 @@ async fn _confirm_invite( user_to_confirm.status = UserOrgStatus::Confirmed as i32; user_to_confirm.akey = key.to_string(); - if CONFIG.mail_enabled() { - let org_name = match Organization::find_by_uuid(org_id, conn).await { - Some(org) => org.name, - None => err!("Error looking up organization."), - }; - let address = match User::find_by_uuid(&user_to_confirm.user_uuid, conn).await { - Some(user) => user.email, - None => err!("Error looking up user."), - }; - mail::send_invite_confirmed(&address, &org_name).await?; - } - let save_result = user_to_confirm.save(conn).await; - if let Some(user) = User::find_by_uuid(&user_to_confirm.user_uuid, conn).await { nt.send_user_update(UpdateType::SyncOrgKeys, &user).await; } - save_result } @@ -1748,16 +1580,6 @@ async fn put_policy( && member.atype < UserOrgType::Admin && member.status != UserOrgStatus::Invited as i32 { - if CONFIG.mail_enabled() { - let org = Organization::find_by_uuid(&member.org_uuid, &mut conn) - .await - .unwrap(); - let user = User::find_by_uuid(&member.user_uuid, &mut conn) - .await - .unwrap(); - - mail::send_2fa_removed_from_org(&user.email, &org.name).await?; - } member.delete(&mut conn).await?; } } @@ -1782,16 +1604,6 @@ async fn put_policy( .await > 1 { - if CONFIG.mail_enabled() { - let org = Organization::find_by_uuid(&member.org_uuid, &mut conn) - .await - .unwrap(); - let user = User::find_by_uuid(&member.user_uuid, &mut conn) - .await - .unwrap(); - - mail::send_single_org_removed_from_org(&user.email, &org.name).await?; - } member.delete(&mut conn).await?; } } @@ -1927,11 +1739,7 @@ async fn import( .is_none() { if let Some(user) = User::find_by_mail(&user_data.Email, &mut conn).await { - let user_org_status = if CONFIG.mail_enabled() { - UserOrgStatus::Invited as i32 - } else { - UserOrgStatus::Accepted as i32 // Automatically mark user as accepted if no email invites - }; + let user_org_status = UserOrgStatus::Accepted as i32; let mut new_org_user = UserOrganization::new(user.uuid.clone(), String::from(org_id)); @@ -1939,26 +1747,9 @@ async fn import( new_org_user.atype = UserOrgType::User as i32; new_org_user.status = user_org_status; new_org_user.save(&mut conn).await?; - if CONFIG.mail_enabled() { - let org_name = match Organization::find_by_uuid(org_id, &mut conn).await { - Some(org) => org.name, - None => err!("Error looking up organization"), - }; - - mail::send_invite( - &user_data.Email, - &user.uuid, - Some(String::from(org_id)), - Some(new_org_user.uuid), - &org_name, - Some(headers.user.email.clone()), - ) - .await?; - } } } } - // If this flag is enabled, any user that isn't provided in the Users list will be removed (by default they will be kept unless they have Deleted == true) if data.OverwriteExisting { for user_org in @@ -2406,14 +2197,14 @@ fn delete_group_user( } #[derive(Deserialize)] -#[allow(non_snake_case)] +#[allow(dead_code, non_snake_case)] struct OrganizationUserResetPasswordEnrollmentRequest { ResetPasswordKey: Option<String>, MasterPasswordHash: Option<String>, } #[derive(Deserialize)] -#[allow(non_snake_case)] +#[allow(dead_code, non_snake_case)] struct OrganizationUserResetPasswordRequest { NewMasterPasswordHash: String, Key: String, @@ -2433,6 +2224,7 @@ async fn get_organization_keys(org_id: &str, mut conn: DbConn) -> JsonResult { }))) } +#[allow(unused_variables)] #[put( "/organizations/<org_id>/users/<org_user_id>/reset-password", data = "<data>" @@ -2440,10 +2232,10 @@ async fn get_organization_keys(org_id: &str, mut conn: DbConn) -> JsonResult { async fn put_reset_password( org_id: &str, org_user_id: &str, - headers: AdminHeaders, + _headers: AdminHeaders, data: JsonUpcase<OrganizationUserResetPasswordRequest>, mut conn: DbConn, - nt: Notify<'_>, + _nt: Notify<'_>, ) -> EmptyResult { let org = match Organization::find_by_uuid(org_id, &mut conn).await { Some(org) => org, @@ -2455,120 +2247,39 @@ async fn put_reset_password( Some(user) => user, None => err!("User to reset isn't member of required organization"), }; - - let user = match User::find_by_uuid(&org_user.user_uuid, &mut conn).await { - Some(user) => user, + match User::find_by_uuid(&org_user.user_uuid, &mut conn).await { + Some(_) => err!("Password reset is not supported on an email-disabled instance."), None => err!("User not found"), - }; - - check_reset_password_applicable_and_permissions(org_id, org_user_id, &headers, &mut conn) - .await?; - - if org_user.reset_password_key.is_none() { - err!("Password reset not or not correctly enrolled"); } - if org_user.status != (UserOrgStatus::Confirmed as i32) { - err!("Organization user must be confirmed for password reset functionality"); - } - - // Sending email before resetting password to ensure working email configuration and the resulting - // user notification. Also this might add some protection against security flaws and misuse - if let Err(e) = mail::send_admin_reset_password(&user.email, &user.name, &org.name).await { - err!(format!("Error sending user reset password email: {e:#?}")); - } - - let reset_request = data.into_inner().data; - - let mut user = user; - user.set_password( - reset_request.NewMasterPasswordHash.as_str(), - Some(reset_request.Key), - true, - None, - ); - user.save(&mut conn).await?; - nt.send_logout(&user, None).await; - Ok(()) } #[get("/organizations/<org_id>/users/<org_user_id>/reset-password-details")] async fn get_reset_password_details( org_id: &str, org_user_id: &str, - headers: AdminHeaders, + _headers: AdminHeaders, mut conn: DbConn, ) -> JsonResult { - let org = match Organization::find_by_uuid(org_id, &mut conn).await { - Some(org) => org, + match Organization::find_by_uuid(org_id, &mut conn).await { + Some(_) => { + let org_user = match UserOrganization::find_by_uuid_and_org( + org_user_id, + org_id, + &mut conn, + ) + .await + { + Some(user) => user, + None => err!("User to reset isn't member of required organization"), + }; + match User::find_by_uuid(&org_user.user_uuid, &mut conn).await { + Some(_) => err!("Password reset is not supported on an email-disabled instance."), + None => err!("User not found"), + } + } None => err!("Required organization not found"), - }; - - let org_user = - match UserOrganization::find_by_uuid_and_org(org_user_id, org_id, &mut conn).await { - Some(user) => user, - None => err!("User to reset isn't member of required organization"), - }; - - let user = match User::find_by_uuid(&org_user.user_uuid, &mut conn).await { - Some(user) => user, - None => err!("User not found"), - }; - - check_reset_password_applicable_and_permissions(org_id, org_user_id, &headers, &mut conn) - .await?; - - // https://github.com/bitwarden/server/blob/3b50ccb9f804efaacdc46bed5b60e5b28eddefcf/src/Api/Models/Response/Organizations/OrganizationUserResponseModel.cs#L111 - Ok(Json(json!({ - "Object": "organizationUserResetPasswordDetails", - "Kdf":user.client_kdf_type, - "KdfIterations":user.client_kdf_iter, - "KdfMemory":user.client_kdf_memory, - "KdfParallelism":user.client_kdf_parallelism, - "ResetPasswordKey":org_user.reset_password_key, - "EncryptedPrivateKey":org.private_key, - - }))) -} - -async fn check_reset_password_applicable_and_permissions( - org_id: &str, - org_user_id: &str, - headers: &AdminHeaders, - conn: &mut DbConn, -) -> EmptyResult { - check_reset_password_applicable(org_id, conn).await?; - - let target_user = match UserOrganization::find_by_uuid_and_org(org_user_id, org_id, conn).await - { - Some(user) => user, - None => err!("Reset target user not found"), - }; - - // Resetting user must be higher/equal to user to reset - match headers.org_user_type { - UserOrgType::Owner => Ok(()), - UserOrgType::Admin if target_user.atype <= UserOrgType::Admin => Ok(()), - _ => err!("No permission to reset this user's password"), } } - -async fn check_reset_password_applicable(org_id: &str, conn: &mut DbConn) -> EmptyResult { - if !CONFIG.mail_enabled() { - err!("Password reset is not supported on an email-disabled instance."); - } - - let policy = - match OrgPolicy::find_by_org_and_type(org_id, OrgPolicyType::ResetPassword, conn).await { - Some(p) => p, - None => err!("Policy not found"), - }; - - if !policy.enabled { - err!("Reset password policy not enabled"); - } - - Ok(()) -} #[allow(unused_variables)] #[put( "/organizations/<org_id>/users/<org_user_id>/reset-password-enrollment", @@ -2581,36 +2292,10 @@ async fn put_reset_password_enrollment( data: JsonUpcase<OrganizationUserResetPasswordEnrollmentRequest>, mut conn: DbConn, ) -> EmptyResult { - let mut org_user = - match UserOrganization::find_by_user_and_org(&headers.user.uuid, org_id, &mut conn).await { - Some(u) => u, - None => err!("User to enroll isn't member of required organization"), - }; - - check_reset_password_applicable(org_id, &mut conn).await?; - - let reset_request = data.into_inner().data; - - if reset_request.ResetPasswordKey.is_none() - && OrgPolicy::org_is_reset_password_auto_enroll(org_id, &mut conn).await - { - err!("Reset password can't be withdrawed due to an enterprise policy"); - } - - if reset_request.ResetPasswordKey.is_some() { - match reset_request.MasterPasswordHash { - Some(password) => { - if !headers.user.check_valid_password(&password) { - err!("Invalid or wrong password") - } - } - None => err!("No password provided"), - }; + match UserOrganization::find_by_user_and_org(&headers.user.uuid, org_id, &mut conn).await { + Some(_) => err!("Password reset is not supported on an email-disabled instance."), + None => err!("User to enroll isn't member of required organization"), } - - org_user.reset_password_key = reset_request.ResetPasswordKey; - org_user.save(&mut conn).await?; - Ok(()) } // This is a new function active since the v2022.9.x clients. diff --git a/src/api/core/public.rs b/src/api/core/public.rs @@ -10,7 +10,6 @@ use crate::{ api::{EmptyResult, JsonUpcase}, auth, db::{models::*, DbConn}, - mail, CONFIG, }; pub fn routes() -> Vec<Route> { @@ -98,45 +97,18 @@ async fn ldap_import( // User does not exist yet let mut new_user = User::new(user_data.Email.clone()); new_user.save(&mut conn).await?; - - if !CONFIG.mail_enabled() { - let invitation = Invitation::new(&new_user.email); - invitation.save(&mut conn).await?; - } + let invitation = Invitation::new(&new_user.email); + invitation.save(&mut conn).await?; new_user } }; - let user_org_status = if CONFIG.mail_enabled() || user.password_hash.is_empty() { - UserOrgStatus::Invited as i32 - } else { - UserOrgStatus::Accepted as i32 // Automatically mark user as accepted if no email invites - }; - + let user_org_status = UserOrgStatus::Accepted as i32; let mut new_org_user = UserOrganization::new(user.uuid.clone(), org_id.clone()); new_org_user.set_external_id(Some(user_data.ExternalId.clone())); new_org_user.access_all = false; new_org_user.atype = UserOrgType::User as i32; new_org_user.status = user_org_status; - new_org_user.save(&mut conn).await?; - - if CONFIG.mail_enabled() { - let (org_name, org_email) = - match Organization::find_by_uuid(&org_id, &mut conn).await { - Some(org) => (org.name, org.billing_email), - None => err!("Error looking up organization"), - }; - - mail::send_invite( - &user_data.Email, - &user.uuid, - Some(org_id.clone()), - Some(new_org_user.uuid), - &org_name, - Some(org_email), - ) - .await?; - } } } warn!("Group support is disabled, groups will not be imported!"); diff --git a/src/api/core/sends.rs b/src/api/core/sends.rs @@ -1,24 +1,14 @@ -use std::path::Path; - -use chrono::{DateTime, Duration, Utc}; -use rocket::form::Form; -use rocket::fs::NamedFile; -use rocket::fs::TempFile; -use rocket::serde::json::Json; -use serde_json::Value; - use crate::{ - api::{ApiResult, EmptyResult, JsonResult, JsonUpcase, Notify, NumberOrString, UpdateType}, + api::{EmptyResult, JsonResult, JsonUpcase, Notify, NumberOrString}, auth::{ClientIp, Headers, Host}, - db::{models::*, DbConn}, + db::DbConn, util::SafeString, - CONFIG, }; - -const SEND_INACCESSIBLE_MSG: &str = "Send does not exist or is no longer available"; - -// The max file size allowed by Bitwarden clients and add an extra 5% to avoid issues -const SIZE_525_MB: u64 = 550_502_400; +use chrono::{DateTime, Utc}; +use rocket::form::Form; +use rocket::fs::{NamedFile, TempFile}; +use rocket::serde::json::Json; +use serde_json::Value; pub fn routes() -> Vec<rocket::Route> { routes![ @@ -38,7 +28,7 @@ pub fn routes() -> Vec<rocket::Route> { } #[derive(Deserialize)] -#[allow(non_snake_case)] +#[allow(dead_code, non_snake_case)] struct SendData { Type: i32, Key: String, @@ -48,8 +38,6 @@ struct SendData { DeletionDate: DateTime<Utc>, Disabled: bool, HideEmail: Option<bool>, - - // Data field Name: String, Notes: Option<String>, Text: Option<Value>, @@ -57,622 +45,137 @@ struct SendData { FileLength: Option<NumberOrString>, } -/// Enforces the `Disable Send` policy. A non-owner/admin user belonging to -/// an org with this policy enabled isn't allowed to create new Sends or -/// modify existing ones, but is allowed to delete them. -/// -/// Ref: https://bitwarden.com/help/article/policies/#disable-send -/// -/// There is also a Vaultwarden-specific `sends_allowed` config setting that -/// controls this policy globally. -async fn enforce_disable_send_policy(headers: &Headers, conn: &mut DbConn) -> EmptyResult { - let user_uuid = &headers.user.uuid; - if !CONFIG.sends_allowed() - || OrgPolicy::is_applicable_to_user(user_uuid, OrgPolicyType::DisableSend, None, conn).await - { - err!("Due to an Enterprise Policy, you are only able to delete an existing Send.") - } - Ok(()) -} - -/// Enforces the `DisableHideEmail` option of the `Send Options` policy. -/// A non-owner/admin user belonging to an org with this option enabled isn't -/// allowed to hide their email address from the recipient of a Bitwarden Send, -/// but is allowed to remove this option from an existing Send. -/// -/// Ref: https://bitwarden.com/help/article/policies/#send-options -async fn enforce_disable_hide_email_policy( - data: &SendData, - headers: &Headers, - conn: &mut DbConn, -) -> EmptyResult { - let user_uuid = &headers.user.uuid; - let hide_email = data.HideEmail.unwrap_or(false); - if hide_email && OrgPolicy::is_hide_email_disabled(user_uuid, conn).await { - err!( - "Due to an Enterprise Policy, you are not allowed to hide your email address \ - from recipients when creating or editing a Send." - ) - } - Ok(()) -} - -fn create_send(data: SendData, user_uuid: String) -> ApiResult<Send> { - let data_val = if data.Type == SendType::Text as i32 { - data.Text - } else if data.Type == SendType::File as i32 { - data.File - } else { - err!("Invalid Send type") - }; - - let data_str = if let Some(mut d) = data_val { - d.as_object_mut().and_then(|o| o.remove("Response")); - serde_json::to_string(&d)? - } else { - err!("Send data not provided"); - }; - - if data.DeletionDate > Utc::now() + Duration::days(31) { - err!( - "You cannot have a Send with a deletion date that far into the future. Adjust the Deletion Date to a value less than 31 days from now and try again." - ); - } - - let mut send = Send::new( - data.Type, - data.Name, - data_str, - data.Key, - data.DeletionDate.naive_utc(), - ); - send.user_uuid = Some(user_uuid); - send.notes = data.Notes; - send.max_access_count = match data.MaxAccessCount { - Some(m) => Some(m.into_i32()?), - _ => None, - }; - send.expiration_date = data.ExpirationDate.map(|d| d.naive_utc()); - send.disabled = data.Disabled; - send.hide_email = data.HideEmail; - send.atype = data.Type; - - send.set_password(data.Password.as_deref()); - - Ok(send) -} - +#[allow(unused_variables)] #[get("/sends")] -async fn get_sends(headers: Headers, mut conn: DbConn) -> Json<Value> { - let sends = Send::find_by_user(&headers.user.uuid, &mut conn); - let sends_json: Vec<Value> = sends.await.iter().map(|s| s.to_json()).collect(); - +fn get_sends(_headers: Headers, _conn: DbConn) -> Json<Value> { Json(json!({ - "Data": sends_json, + "Data": Vec::<Value>::new(), "Object": "list", "ContinuationToken": null })) } +#[allow(unused_variables)] #[get("/sends/<uuid>")] -async fn get_send(uuid: &str, headers: Headers, mut conn: DbConn) -> JsonResult { - let send = match Send::find_by_uuid(uuid, &mut conn).await { - Some(send) => send, - None => err!("Send not found"), - }; - - if send.user_uuid.as_ref() != Some(&headers.user.uuid) { - err!("Send is not owned by user") - } - - Ok(Json(send.to_json())) +fn get_send(uuid: &str, _headers: Headers, _conn: DbConn) -> JsonResult { + err!("Sends are permanently disabled.") } +#[allow(unused_variables)] #[post("/sends", data = "<data>")] -async fn post_send( +fn post_send( data: JsonUpcase<SendData>, - headers: Headers, - mut conn: DbConn, - nt: Notify<'_>, + _headers: Headers, + _conn: DbConn, + _nt: Notify<'_>, ) -> JsonResult { - enforce_disable_send_policy(&headers, &mut conn).await?; - - let data: SendData = data.into_inner().data; - enforce_disable_hide_email_policy(&data, &headers, &mut conn).await?; - - if data.Type == SendType::File as i32 { - err!("File sends should use /api/sends/file") - } - - let mut send = create_send(data, headers.user.uuid)?; - send.save(&mut conn).await?; - nt.send_send_update( - UpdateType::SyncSendCreate, - &send, - &send.update_users_revision(&mut conn).await, - &headers.device.uuid, - &mut conn, - ) - .await; - - Ok(Json(send.to_json())) + err!("Sends are permanently disabled.") } +#[allow(dead_code)] #[derive(FromForm)] struct UploadData<'f> { model: Json<crate::util::UpCase<SendData>>, data: TempFile<'f>, } +#[allow(dead_code)] #[derive(FromForm)] struct UploadDataV2<'f> { data: TempFile<'f>, } -// @deprecated Mar 25 2021: This method has been deprecated in favor of direct uploads (v2). -// This method still exists to support older clients, probably need to remove it sometime. -// Upstream: https://github.com/bitwarden/server/blob/d0c793c95181dfb1b447eb450f85ba0bfd7ef643/src/Api/Controllers/SendsController.cs#L164-L167 +#[allow(unused_variables)] #[post("/sends/file", format = "multipart/form-data", data = "<data>")] -async fn post_send_file( +fn post_send_file( data: Form<UploadData<'_>>, - headers: Headers, - mut conn: DbConn, - nt: Notify<'_>, + _headers: Headers, + _conn: DbConn, + _nt: Notify<'_>, ) -> JsonResult { - enforce_disable_send_policy(&headers, &mut conn).await?; - - let UploadData { model, mut data } = data.into_inner(); - let model = model.into_inner().data; - - enforce_disable_hide_email_policy(&model, &headers, &mut conn).await?; - - let size_limit = match CONFIG.user_attachment_limit() { - Some(0) => err!("File uploads are disabled"), - Some(limit_kb) => { - let left = - (limit_kb * 1024) - Attachment::size_by_user(&headers.user.uuid, &mut conn).await; - if left <= 0 { - err!("Attachment storage limit reached! Delete some attachments to free up space") - } - std::cmp::Ord::max(left as u64, SIZE_525_MB) - } - None => SIZE_525_MB, - }; - - let mut send = create_send(model, headers.user.uuid)?; - if send.atype != SendType::File as i32 { - err!("Send content is not a file"); - } - - let size = data.len(); - if size > size_limit { - err!("Attachment storage limit exceeded with this file"); - } - - let file_id = crate::crypto::generate_send_id(); - let folder_path = tokio::fs::canonicalize(&CONFIG.sends_folder()) - .await? - .join(&send.uuid); - let file_path = folder_path.join(&file_id); - tokio::fs::create_dir_all(&folder_path).await?; - - if let Err(_err) = data.persist_to(&file_path).await { - data.move_copy_to(file_path).await? - } - - let mut data_value: Value = serde_json::from_str(&send.data)?; - if let Some(o) = data_value.as_object_mut() { - o.insert(String::from("Id"), Value::String(file_id)); - o.insert(String::from("Size"), Value::Number(size.into())); - o.insert( - String::from("SizeName"), - Value::String(crate::util::get_display_size(size as i32)), - ); - } - send.data = serde_json::to_string(&data_value)?; - - // Save the changes in the database - send.save(&mut conn).await?; - nt.send_send_update( - UpdateType::SyncSendCreate, - &send, - &send.update_users_revision(&mut conn).await, - &headers.device.uuid, - &mut conn, - ) - .await; - - Ok(Json(send.to_json())) + err!("Sends are permanently disabled.") } -// Upstream: https://github.com/bitwarden/server/blob/d0c793c95181dfb1b447eb450f85ba0bfd7ef643/src/Api/Controllers/SendsController.cs#L190 +#[allow(unused_variables)] #[post("/sends/file/v2", data = "<data>")] -async fn post_send_file_v2( - data: JsonUpcase<SendData>, - headers: Headers, - mut conn: DbConn, -) -> JsonResult { - enforce_disable_send_policy(&headers, &mut conn).await?; - - let data = data.into_inner().data; - - if data.Type != SendType::File as i32 { - err!("Send content is not a file"); - } - - enforce_disable_hide_email_policy(&data, &headers, &mut conn).await?; - - let file_length = match &data.FileLength { - Some(m) => Some(m.into_i32()?), - _ => None, - }; - - let size_limit = match CONFIG.user_attachment_limit() { - Some(0) => err!("File uploads are disabled"), - Some(limit_kb) => { - let left = - (limit_kb * 1024) - Attachment::size_by_user(&headers.user.uuid, &mut conn).await; - if left <= 0 { - err!("Attachment storage limit reached! Delete some attachments to free up space") - } - std::cmp::Ord::max(left as u64, SIZE_525_MB) - } - None => SIZE_525_MB, - }; - - if file_length.is_some() && file_length.unwrap() as u64 > size_limit { - err!("Attachment storage limit exceeded with this file"); - } - - let mut send = create_send(data, headers.user.uuid)?; - - let file_id = crate::crypto::generate_send_id(); - - let mut data_value: Value = serde_json::from_str(&send.data)?; - if let Some(o) = data_value.as_object_mut() { - o.insert(String::from("Id"), Value::String(file_id.clone())); - o.insert( - String::from("Size"), - Value::Number(file_length.unwrap().into()), - ); - o.insert( - String::from("SizeName"), - Value::String(crate::util::get_display_size(file_length.unwrap())), - ); - } - send.data = serde_json::to_string(&data_value)?; - send.save(&mut conn).await?; - - Ok(Json(json!({ - "fileUploadType": 0, // 0 == Direct | 1 == Azure - "object": "send-fileUpload", - "url": format!("/sends/{}/file/{}", send.uuid, file_id), - "sendResponse": send.to_json() - }))) +fn post_send_file_v2(data: JsonUpcase<SendData>, _headers: Headers, _conn: DbConn) -> JsonResult { + err!("Sends are permanently disabled.") } -// https://github.com/bitwarden/server/blob/d0c793c95181dfb1b447eb450f85ba0bfd7ef643/src/Api/Controllers/SendsController.cs#L243 +#[allow(unused_variables)] #[post( "/sends/<send_uuid>/file/<file_id>", format = "multipart/form-data", data = "<data>" )] -async fn post_send_file_v2_data( +fn post_send_file_v2_data( send_uuid: &str, file_id: &str, data: Form<UploadDataV2<'_>>, - headers: Headers, - mut conn: DbConn, - nt: Notify<'_>, + _headers: Headers, + _conn: DbConn, + _nt: Notify<'_>, ) -> EmptyResult { - enforce_disable_send_policy(&headers, &mut conn).await?; - - let mut data = data.into_inner(); - - let Some(send) = Send::find_by_uuid(send_uuid, &mut conn).await else { - err!("Send not found. Unable to save the file.") - }; - - let Some(send_user_id) = &send.user_uuid else { - err!("Sends are only supported for users at the moment") - }; - if send_user_id != &headers.user.uuid { - err!("Send doesn't belong to user"); - } - - let folder_path = tokio::fs::canonicalize(&CONFIG.sends_folder()) - .await? - .join(send_uuid); - let file_path = folder_path.join(file_id); - tokio::fs::create_dir_all(&folder_path).await?; - - if let Err(_err) = data.data.persist_to(&file_path).await { - data.data.move_copy_to(file_path).await? - } - - nt.send_send_update( - UpdateType::SyncSendCreate, - &send, - &send.update_users_revision(&mut conn).await, - &headers.device.uuid, - &mut conn, - ) - .await; - - Ok(()) + err!("Sends are permanently disabled.") } #[derive(Deserialize)] -#[allow(non_snake_case)] +#[allow(dead_code, non_snake_case)] pub struct SendAccessData { pub Password: Option<String>, } +#[allow(unused_variables)] #[post("/sends/access/<access_id>", data = "<data>")] -async fn post_access( +fn post_access( access_id: &str, data: JsonUpcase<SendAccessData>, - mut conn: DbConn, - ip: ClientIp, - nt: Notify<'_>, + _conn: DbConn, + _ip: ClientIp, + _nt: Notify<'_>, ) -> JsonResult { - let mut send = match Send::find_by_access_id(access_id, &mut conn).await { - Some(s) => s, - None => err_code!(SEND_INACCESSIBLE_MSG, 404), - }; - - if let Some(max_access_count) = send.max_access_count { - if send.access_count >= max_access_count { - err_code!(SEND_INACCESSIBLE_MSG, 404); - } - } - - if let Some(expiration) = send.expiration_date { - if Utc::now().naive_utc() >= expiration { - err_code!(SEND_INACCESSIBLE_MSG, 404) - } - } - - if Utc::now().naive_utc() >= send.deletion_date { - err_code!(SEND_INACCESSIBLE_MSG, 404) - } - - if send.disabled { - err_code!(SEND_INACCESSIBLE_MSG, 404) - } - - if send.password_hash.is_some() { - match data.into_inner().data.Password { - Some(ref p) if send.check_password(p) => { /* Nothing to do here */ } - Some(_) => err!("Invalid password", format!("IP: {}.", ip.ip)), - None => err_code!("Password not provided", format!("IP: {}.", ip.ip), 401), - } - } - - // Files are incremented during the download - if send.atype == SendType::Text as i32 { - send.access_count += 1; - } - - send.save(&mut conn).await?; - - nt.send_send_update( - UpdateType::SyncSendUpdate, - &send, - &send.update_users_revision(&mut conn).await, - &String::from("00000000-0000-0000-0000-000000000000"), - &mut conn, - ) - .await; - - Ok(Json(send.to_json_access(&mut conn).await)) + err!("Sends are permanently disabled.") } +#[allow(unused_variables)] #[post("/sends/<send_id>/access/file/<file_id>", data = "<data>")] -async fn post_access_file( +fn post_access_file( send_id: &str, file_id: &str, data: JsonUpcase<SendAccessData>, - host: Host, - mut conn: DbConn, - nt: Notify<'_>, + _host: Host, + _conn: DbConn, + _nt: Notify<'_>, ) -> JsonResult { - let mut send = match Send::find_by_uuid(send_id, &mut conn).await { - Some(s) => s, - None => err_code!(SEND_INACCESSIBLE_MSG, 404), - }; - - if let Some(max_access_count) = send.max_access_count { - if send.access_count >= max_access_count { - err_code!(SEND_INACCESSIBLE_MSG, 404) - } - } - - if let Some(expiration) = send.expiration_date { - if Utc::now().naive_utc() >= expiration { - err_code!(SEND_INACCESSIBLE_MSG, 404) - } - } - - if Utc::now().naive_utc() >= send.deletion_date { - err_code!(SEND_INACCESSIBLE_MSG, 404) - } - - if send.disabled { - err_code!(SEND_INACCESSIBLE_MSG, 404) - } - - if send.password_hash.is_some() { - match data.into_inner().data.Password { - Some(ref p) if send.check_password(p) => { /* Nothing to do here */ } - Some(_) => err!("Invalid password."), - None => err_code!("Password not provided", 401), - } - } - - send.access_count += 1; - - send.save(&mut conn).await?; - - nt.send_send_update( - UpdateType::SyncSendUpdate, - &send, - &send.update_users_revision(&mut conn).await, - &String::from("00000000-0000-0000-0000-000000000000"), - &mut conn, - ) - .await; - - let token_claims = crate::auth::generate_send_claims(send_id, file_id); - let token = crate::auth::encode_jwt(&token_claims); - Ok(Json(json!({ - "Object": "send-fileDownload", - "Id": file_id, - "Url": format!("{}/api/sends/{}/{}?t={}", &host.host, send_id, file_id, token) - }))) + err!("Sends are permanently disabled.") } +#[allow(unused_variables)] #[get("/sends/<send_id>/<file_id>?<t>")] -async fn download_send(send_id: SafeString, file_id: SafeString, t: &str) -> 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), - ) - .await - .ok(); - } - } +fn download_send(send_id: SafeString, file_id: SafeString, t: &str) -> Option<NamedFile> { None } +#[allow(unused_variables)] #[put("/sends/<id>", data = "<data>")] -async fn put_send( +fn put_send( id: &str, data: JsonUpcase<SendData>, - headers: Headers, - mut conn: DbConn, - nt: Notify<'_>, + _headers: Headers, + _conn: DbConn, + _nt: Notify<'_>, ) -> JsonResult { - enforce_disable_send_policy(&headers, &mut conn).await?; - - let data: SendData = data.into_inner().data; - enforce_disable_hide_email_policy(&data, &headers, &mut conn).await?; - - let mut send = match Send::find_by_uuid(id, &mut conn).await { - Some(s) => s, - None => err!("Send not found"), - }; - - if send.user_uuid.as_ref() != Some(&headers.user.uuid) { - err!("Send is not owned by user") - } - - if send.atype != data.Type { - err!("Sends can't change type") - } - - // When updating a file Send, we receive nulls in the File field, as it's immutable, - // so we only need to update the data field in the Text case - if data.Type == SendType::Text as i32 { - let data_str = if let Some(mut d) = data.Text { - d.as_object_mut().and_then(|d| d.remove("Response")); - serde_json::to_string(&d)? - } else { - err!("Send data not provided"); - }; - send.data = data_str; - } - - if data.DeletionDate > Utc::now() + Duration::days(31) { - err!( - "You cannot have a Send with a deletion date that far into the future. Adjust the Deletion Date to a value less than 31 days from now and try again." - ); - } - send.name = data.Name; - send.akey = data.Key; - send.deletion_date = data.DeletionDate.naive_utc(); - send.notes = data.Notes; - send.max_access_count = match data.MaxAccessCount { - Some(m) => Some(m.into_i32()?), - _ => None, - }; - send.expiration_date = data.ExpirationDate.map(|d| d.naive_utc()); - send.hide_email = data.HideEmail; - send.disabled = data.Disabled; - - // Only change the value if it's present - if let Some(password) = data.Password { - send.set_password(Some(&password)); - } - - send.save(&mut conn).await?; - nt.send_send_update( - UpdateType::SyncSendUpdate, - &send, - &send.update_users_revision(&mut conn).await, - &headers.device.uuid, - &mut conn, - ) - .await; - - Ok(Json(send.to_json())) + err!("Sends are permanently disabled.") } +#[allow(unused_variables)] #[delete("/sends/<id>")] -async fn delete_send(id: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { - let send = match Send::find_by_uuid(id, &mut conn).await { - Some(s) => s, - None => err!("Send not found"), - }; - - if send.user_uuid.as_ref() != Some(&headers.user.uuid) { - err!("Send is not owned by user") - } - - send.delete(&mut conn).await?; - nt.send_send_update( - UpdateType::SyncSendDelete, - &send, - &send.update_users_revision(&mut conn).await, - &headers.device.uuid, - &mut conn, - ) - .await; - - Ok(()) +fn delete_send(id: &str, _headers: Headers, _conn: DbConn, _nt: Notify<'_>) -> EmptyResult { + err!("Sends are permanently disabled.") } +#[allow(unused_variables)] #[put("/sends/<id>/remove-password")] -async fn put_remove_password( - id: &str, - headers: Headers, - mut conn: DbConn, - nt: Notify<'_>, -) -> JsonResult { - enforce_disable_send_policy(&headers, &mut conn).await?; - - let mut send = match Send::find_by_uuid(id, &mut conn).await { - Some(s) => s, - None => err!("Send not found"), - }; - - if send.user_uuid.as_ref() != Some(&headers.user.uuid) { - err!("Send is not owned by user") - } - - send.set_password(None); - send.save(&mut conn).await?; - nt.send_send_update( - UpdateType::SyncSendUpdate, - &send, - &send.update_users_revision(&mut conn).await, - &headers.device.uuid, - &mut conn, - ) - .await; - - Ok(Json(send.to_json())) +fn put_remove_password(id: &str, _headers: Headers, _conn: DbConn, _nt: Notify<'_>) -> JsonResult { + err!("Sends are permanently disabled.") } diff --git a/src/api/core/two_factor/authenticator.rs b/src/api/core/two_factor/authenticator.rs @@ -12,8 +12,6 @@ use crate::{ }, }; -pub use crate::config::CONFIG; - pub fn routes() -> Vec<Route> { routes![ generate_authenticator, @@ -149,12 +147,7 @@ pub async fn validate_totp_code( secret.to_string(), ), }; - - // The amount of steps back and forward in time - // Also check if we need to disable time drifted TOTP codes. - // If that is the case, we set the steps to 0 so only the current TOTP is valid. - let steps = i64::from(!CONFIG.authenticator_disable_time_drift()); - + let steps = 0; // Get the current system time in UNIX Epoch (UTC) let current_time = chrono::Utc::now(); let current_timestamp = current_time.timestamp(); diff --git a/src/api/core/two_factor/mod.rs b/src/api/core/two_factor/mod.rs @@ -2,7 +2,6 @@ use crate::{ api::{JsonResult, JsonUpcase, NumberOrString, PasswordOrOtpData}, auth::{ClientHeaders, Headers}, db::{models::*, DbConn}, - mail, CONFIG, }; pub mod authenticator; pub mod protected_actions; @@ -143,12 +142,6 @@ async fn disable_twofactor( .into_iter() { if user_org.atype < UserOrgType::Admin { - if CONFIG.mail_enabled() { - let org = Organization::find_by_uuid(&user_org.org_uuid, &mut conn) - .await - .unwrap(); - mail::send_2fa_removed_from_org(&user.email, &org.name).await?; - } user_org.delete(&mut conn).await?; } } diff --git a/src/api/core/two_factor/protected_actions.rs b/src/api/core/two_factor/protected_actions.rs @@ -10,7 +10,6 @@ use crate::{ DbConn, }, error::{Error, MapResult}, - mail, CONFIG, }; pub fn routes() -> Vec<Route> { @@ -29,18 +28,6 @@ pub struct ProtectedActionData { } impl ProtectedActionData { - pub fn new(token: String) -> Self { - Self { - token, - token_sent: Utc::now().naive_utc().timestamp(), - attempts: 0, - } - } - - pub fn to_json(&self) -> String { - serde_json::to_string(&self).unwrap() - } - pub fn from_json(string: &str) -> Result<Self, Error> { let res: Result<Self, crate::serde_json::Error> = serde_json::from_str(string); match res { @@ -55,30 +42,8 @@ impl ProtectedActionData { } #[post("/accounts/request-otp")] -async fn request_otp(headers: Headers, mut conn: DbConn) -> EmptyResult { - if !CONFIG.mail_enabled() { - err!("Email is disabled for this server. Either enable email or login using your master password instead of login via device."); - } - - let user = headers.user; - - // Only one Protected Action per user is allowed to take place, delete the previous one - if let Some(pa) = - TwoFactor::find_by_user_and_type(&user.uuid, TwoFactorType::ProtectedActions as i32, &mut conn).await - { - pa.delete(&mut conn).await?; - } - - let generated_token = crypto::generate_email_token(CONFIG.email_token_size()); - let pa_data = ProtectedActionData::new(generated_token); - - // Uses EmailVerificationChallenge as type to show that it's not verified yet. - let twofactor = TwoFactor::new(user.uuid, TwoFactorType::ProtectedActions, pa_data.to_json()); - twofactor.save(&mut conn).await?; - - mail::send_protected_action_token(&user.email, &pa_data.token).await?; - - Ok(()) +fn request_otp(_headers: Headers, _conn: DbConn) -> EmptyResult { + err!("Email is disabled for this server. Either enable email or login using your master password instead of login via device.") } #[derive(Deserialize, Serialize, Debug)] @@ -87,18 +52,14 @@ struct ProtectedActionVerify { OTP: String, } +#[allow(unused_variables)] #[post("/accounts/verify-otp", data = "<data>")] -async fn verify_otp(data: JsonUpcase<ProtectedActionVerify>, headers: Headers, mut conn: DbConn) -> EmptyResult { - if !CONFIG.mail_enabled() { - err!("Email is disabled for this server. Either enable email or login using your master password instead of login via device."); - } - - let user = headers.user; - let data: ProtectedActionVerify = data.into_inner().data; - - // Delete the token after one validation attempt - // This endpoint only gets called for the vault export, and doesn't need a second attempt - validate_protected_action_otp(&data.OTP, &user.uuid, true, &mut conn).await +fn verify_otp( + data: JsonUpcase<ProtectedActionVerify>, + _headers: Headers, + _conn: DbConn, +) -> EmptyResult { + err!("Email is disabled for this server. Either enable email or login using your master password instead of login via device."); } pub async fn validate_protected_action_otp( @@ -107,9 +68,15 @@ pub async fn validate_protected_action_otp( delete_if_valid: bool, conn: &mut DbConn, ) -> EmptyResult { - let pa = TwoFactor::find_by_user_and_type(user_uuid, TwoFactorType::ProtectedActions as i32, conn) - .await - .map_res("Protected action token not found, try sending the code again or restart the process")?; + let pa = TwoFactor::find_by_user_and_type( + user_uuid, + TwoFactorType::ProtectedActions as i32, + conn, + ) + .await + .map_res( + "Protected action token not found, try sending the code again or restart the process", + )?; let mut pa_data = ProtectedActionData::from_json(&pa.data)?; pa_data.add_attempt(); @@ -121,9 +88,9 @@ pub async fn validate_protected_action_otp( } // Check if the token has expired (Using the email 2fa expiration time) - let date = - NaiveDateTime::from_timestamp_opt(pa_data.token_sent, 0).expect("Protected Action token timestamp invalid."); - let max_time = CONFIG.email_expiration_time() as i64; + let date = NaiveDateTime::from_timestamp_opt(pa_data.token_sent, 0) + .expect("Protected Action token timestamp invalid."); + let max_time = 600; if date + Duration::seconds(max_time) < Utc::now().naive_utc() { pa.delete(conn).await?; err!("Token has expired") diff --git a/src/api/core/two_factor/webauthn.rs b/src/api/core/two_factor/webauthn.rs @@ -9,12 +9,12 @@ use webauthn_rs::{ use crate::{ api::{EmptyResult, JsonResult, JsonUpcase, NumberOrString, PasswordOrOtpData}, auth::Headers, + config, db::{ models::{EventType, TwoFactor, TwoFactorType}, DbConn, }, error::Error, - CONFIG, }; pub fn routes() -> Vec<Route> { @@ -57,15 +57,11 @@ struct WebauthnConfig { impl WebauthnConfig { fn load() -> Webauthn<Self> { - let domain = CONFIG.domain(); - let domain_origin = CONFIG.domain_origin(); + let domain = &config::get_config().domain; + let domain_origin = config::get_config().domain_origin(); Webauthn::new(Self { - rpid: Url::parse(&domain) - .map(|u| u.domain().map(str::to_owned)) - .ok() - .flatten() - .unwrap_or_default(), - url: domain, + rpid: domain.domain().expect("a valid domain").to_owned(), + url: domain.to_string(), origin: Url::parse(&domain_origin).unwrap(), }) } @@ -117,9 +113,6 @@ async fn get_webauthn( headers: Headers, mut conn: DbConn, ) -> JsonResult { - if !CONFIG.domain_set() { - err!("`DOMAIN` environment variable is not set. Webauthn disabled") - } let data: PasswordOrOtpData = data.into_inner().data; let user = headers.user; @@ -396,7 +389,7 @@ pub async fn generate_webauthn_login(user_uuid: &str, conn: &mut DbConn) -> Json // Generate a challenge based on the credentials let ext = RequestAuthenticationExtensions::builder() - .appid(format!("{}/app-id.json", &CONFIG.domain())) + .appid(format!("{}/app-id.json", config::get_config().domain)) .build(); let (response, state) = WebauthnConfig::load().generate_challenge_authenticate_options(creds, Some(ext))?; diff --git a/src/api/icons.rs b/src/api/icons.rs @@ -1,4 +1,4 @@ -use crate::{util::Cached, CONFIG}; +use crate::util::Cached; use rocket::{http::ContentType, Route}; pub fn routes() -> Vec<Route> { routes![icon_internal] @@ -11,7 +11,7 @@ fn icon_internal(domain: &str) -> Cached<(ContentType, Vec<u8>)> { ContentType::new("image", "png"), include_bytes!("../static/images/fallback-icon.png").to_vec(), ), - CONFIG.icon_cache_negttl(), + 2_592_000, true, ) } diff --git a/src/api/identity.rs b/src/api/identity.rs @@ -1,4 +1,3 @@ -use chrono::Utc; use num_traits::FromPrimitive; use rocket::serde::json::Json; use rocket::{ @@ -13,9 +12,10 @@ use crate::{ ApiResult, EmptyResult, JsonResult, JsonUpcase, }, auth::{generate_organization_api_key_login_claims, ClientHeaders, ClientIp}, + config, db::{models::*, DbConn}, error::MapResult, - mail, util, CONFIG, + util, }; pub fn routes() -> Vec<Route> { @@ -29,9 +29,7 @@ async fn login( mut conn: DbConn, ) -> JsonResult { let data: ConnectData = data.into_inner(); - let mut user_uuid: Option<String> = None; - let login_result = match data.grant_type.as_ref() { "refresh_token" => { _check_is_some(&data.refresh_token, "refresh_token cannot be blank")?; @@ -115,10 +113,6 @@ async fn _password_login( err!("Scope not supported") } let scope_vec = vec!["api".into(), "offline_access".into()]; - - // Ratelimit the login - crate::ratelimit::check_limit_login(&ip.ip)?; - // Get the user let username = data.username.as_ref().unwrap().trim(); let mut user = match User::find_by_mail(username, conn).await { @@ -167,8 +161,8 @@ async fn _password_login( } // Change the KDF Iterations - if user.password_iterations != CONFIG.password_iterations() { - user.password_iterations = CONFIG.password_iterations(); + if user.password_iterations != config::get_config().password_iterations { + user.password_iterations = config::get_config().password_iterations; user.set_password(password, None, false, None); if let Err(e) = user.save(conn).await { @@ -186,46 +180,8 @@ async fn _password_login( } ) } - - let now = Utc::now().naive_utc(); - - if user.verified_at.is_none() && CONFIG.mail_enabled() && CONFIG.signups_verify() { - if user.last_verifying_at.is_none() - || now - .signed_duration_since(user.last_verifying_at.unwrap()) - .num_seconds() - > CONFIG.signups_verify_resend_time() as i64 - { - let resend_limit = CONFIG.signups_verify_resend_limit() as i32; - if resend_limit == 0 || user.login_verify_count < resend_limit { - // We want to send another email verification if we require signups to verify - // their email address, and we haven't sent them a reminder in a while... - user.last_verifying_at = Some(now); - user.login_verify_count += 1; - - if let Err(e) = user.save(conn).await { - panic!("Error updating user: {:#?}", e); - } - - if let Err(e) = mail::send_verify_email(&user.email, &user.uuid).await { - panic!("Error auto-sending email verification email: {:#?}", e); - } - } - } - - // We still want the login to fail until they actually verified the email address - err!( - "Please verify your email before trying again.", - format!("IP: {}. Username: {}.", ip.ip, username), - ErrorEvent { - event: EventType::UserFailedLogIn - } - ) - } - let (mut device, _) = get_device(&data, conn, &user).await; let twofactor_token = twofactor_auth(&user.uuid, &data, &mut device, ip, conn).await?; - // Common let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, conn).await; let (access_token, expires_in) = device.refresh_tokens(&user, orgs, scope_vec); device.save(conn).await?; @@ -266,9 +222,6 @@ async fn _api_key_login( conn: &mut DbConn, ip: &ClientIp, ) -> JsonResult { - // Ratelimit the login - crate::ratelimit::check_limit_login(&ip.ip)?; - // Validate scope match data.scope.as_ref().unwrap().as_ref() { "api" => _user_api_key_login(data, user_uuid, conn, ip).await, @@ -320,25 +273,7 @@ async fn _user_api_key_login( ) } - let (mut device, new_device) = get_device(&data, conn, &user).await; - - if CONFIG.mail_enabled() && new_device { - let now = Utc::now().naive_utc(); - if mail::send_new_device_logged_in(&user.email, &ip.ip.to_string(), &now, &device.name) - .await - .is_err() - && CONFIG.require_device_email() - { - err!( - "Could not send login notification email. Please contact your administrator.", - ErrorEvent { - event: EventType::UserFailedLogIn - } - ) - } - } - - // Common + let (mut device, _) = get_device(&data, conn, &user).await; let scope_vec = vec!["api".into()]; let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, conn).await; let (access_token, expires_in) = device.refresh_tokens(&user, orgs, scope_vec); @@ -443,7 +378,6 @@ async fn twofactor_auth( if twofactors.is_empty() { return Ok(None); } - TwoFactorIncomplete::mark_incomplete(user_uuid, &device.uuid, &device.name, ip, conn).await?; let twofactor_ids: Vec<_> = twofactors.iter().map(|tf| tf.atype).collect(); let selected_id = data.two_factor_provider.unwrap_or(twofactor_ids[0]); // If we aren't given a two factor provider, assume the first one let twofactor_code = match data.two_factor_token { @@ -458,7 +392,6 @@ async fn twofactor_auth( .find(|tf| tf.atype == selected_id && tf.enabled); use crate::api::core::two_factor as _tf; let selected_data = _selected_data(selected_twofactor); - let remember = data.two_factor_remember.unwrap_or(0); match TwoFactorType::from_i32(selected_id) { Some(TwoFactorType::Authenticator) => { _tf::authenticator::validate_totp_code_str( @@ -480,15 +413,8 @@ async fn twofactor_auth( } ), } - - TwoFactorIncomplete::mark_complete(user_uuid, &device.uuid, conn).await?; - - if !CONFIG.disable_2fa_remember() && remember == 1 { - Ok(Some(device.refresh_twofactor_remember())) - } else { - device.delete_twofactor_remember(); - Ok(None) - } + device.delete_twofactor_remember(); + Ok(None) } fn _selected_data(tf: Option<TwoFactor>) -> ApiResult<String> { @@ -514,8 +440,7 @@ async fn _json_err_twofactor( match TwoFactorType::from_i32(*provider) { Some(TwoFactorType::Authenticator) => { /* Nothing to do for TOTP */ } - - Some(TwoFactorType::Webauthn) if CONFIG.domain_set() => { + Some(TwoFactorType::Webauthn) => { let request = two_factor::webauthn::generate_webauthn_login(user_uuid, conn).await?; result["TwoFactorProviders2"][provider.to_string()] = request.0; @@ -588,6 +513,7 @@ struct ConnectData { two_factor_token: Option<String>, #[field(name = uncased("two_factor_remember"))] #[field(name = uncased("twofactorremember"))] + #[allow(dead_code)] two_factor_remember: Option<i32>, #[field(name = uncased("authrequest"))] auth_request: Option<String>, diff --git a/src/api/notifications.rs b/src/api/notifications.rs @@ -1,7 +1,7 @@ use crate::{ auth::{ClientIp, WsAccessTokenHeader}, db::{ - models::{Cipher, Folder, Send as DbSend, User}, + models::{Cipher, Folder, User}, DbConn, }, Error, @@ -427,31 +427,6 @@ impl WebSocketUsers { } } - pub async fn send_send_update( - &self, - ut: UpdateType, - send: &DbSend, - user_uuids: &[String], - _: &String, - _: &mut DbConn, - ) { - let user_uuid = convert_option(send.user_uuid.clone()); - - let data = create_update( - vec![ - ("Id".into(), send.uuid.clone().into()), - ("UserId".into(), user_uuid), - ("RevisionDate".into(), serialize_date(send.revision_date)), - ], - ut, - None, - ); - - for uuid in user_uuids { - self.send_update(uuid, &data).await; - } - } - pub async fn send_auth_request( &self, user_uuid: &str, diff --git a/src/api/web.rs b/src/api/web.rs @@ -1,31 +1,26 @@ -use std::path::{Path, PathBuf}; - -use rocket::{ - fs::NamedFile, http::ContentType, response::content::RawHtml as Html, serde::json::Json, - Catcher, Route, -}; -use serde_json::Value; - use crate::{ - api::{core::now, ApiResult, EmptyResult}, + api::{core::now, EmptyResult}, auth::decode_file_download, + config::{self, Config}, error::Error, util::{Cached, SafeString}, - CONFIG, }; +use rocket::{fs::NamedFile, http::ContentType, serde::json::Json, Catcher, Route}; +use serde_json::Value; +use std::path::{Path, PathBuf}; pub fn routes() -> Vec<Route> { // If adding more routes here, consider also adding them to // crate::utils::LOGGED_ROUTES to make sure they appear in the log let mut routes = routes![attachments, alive, alive_head, static_files]; - if CONFIG.web_vault_enabled() { + if config::get_config().web_vault_enabled { routes.append(&mut routes![web_index, web_index_head, app_id, web_files]); } routes } pub fn catchers() -> Vec<Catcher> { - if CONFIG.web_vault_enabled() { + if config::get_config().web_vault_enabled { catchers![not_found] } else { catchers![] @@ -33,19 +28,14 @@ pub fn catchers() -> Vec<Catcher> { } #[catch(404)] -fn not_found() -> ApiResult<Html<String>> { - // Return the page - let json = json!({ - "urlpath": CONFIG.domain_path() - }); - let text = CONFIG.render_template("404", &json)?; - Ok(Html(text)) +fn not_found() -> &'static str { + "Admin panel or web vault is permanently disabled." } #[get("/")] async fn web_index() -> Cached<Option<NamedFile>> { Cached::short( - NamedFile::open(Path::new(&CONFIG.web_vault_folder()).join("index.html")) + NamedFile::open(Path::new(Config::WEB_VAULT_FOLDER).join("index.html")) .await .ok(), false, @@ -85,7 +75,7 @@ fn app_id() -> Cached<(ContentType, Json<Value>)> { // This leaves it unclear as to whether the path must be empty, // or whether it can be non-empty and will be ignored. To be on // the safe side, use a proper web origin (with empty path). - &CONFIG.domain_origin(), + config::get_config().domain_origin(), "ios:bundle-id:com.8bit.bitwarden", "android:apk-key-hash:dUGFzUzf3lmHSLBDBIv+WaFyZMI" ] }] @@ -98,7 +88,7 @@ fn app_id() -> Cached<(ContentType, Json<Value>)> { #[get("/<p..>", rank = 10)] // Only match this if the other routes don't match async fn web_files(p: PathBuf) -> Cached<Option<NamedFile>> { Cached::long( - NamedFile::open(Path::new(&CONFIG.web_vault_folder()).join(p)) + NamedFile::open(Path::new(Config::WEB_VAULT_FOLDER).join(p)) .await .ok(), true, @@ -115,7 +105,7 @@ async fn attachments(uuid: SafeString, file_id: SafeString, token: String) -> Op } NamedFile::open( - Path::new(&CONFIG.attachments_folder()) + Path::new(Config::ATTACHMENTS_FOLDER) .join(uuid) .join(file_id), ) diff --git a/src/auth.rs b/src/auth.rs @@ -1,54 +1,194 @@ -// JWT Handling -// +use crate::{ + config::{self, Config}, + error::Error, +}; use chrono::{Duration, Utc}; -use num_traits::FromPrimitive; -use once_cell::sync::Lazy; - use jsonwebtoken::{self, errors::ErrorKind, Algorithm, DecodingKey, EncodingKey, Header}; +use num_traits::FromPrimitive; use serde::de::DeserializeOwned; use serde::ser::Serialize; - -use crate::{error::Error, CONFIG}; +use std::sync::OnceLock; const JWT_ALGORITHM: Algorithm = Algorithm::RS256; - -pub static DEFAULT_VALIDITY: Lazy<Duration> = Lazy::new(|| Duration::hours(2)); -static JWT_HEADER: Lazy<Header> = Lazy::new(|| Header::new(JWT_ALGORITHM)); - -pub static JWT_LOGIN_ISSUER: Lazy<String> = - Lazy::new(|| format!("{}|login", CONFIG.domain_origin())); -static JWT_INVITE_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|invite", CONFIG.domain_origin())); -static JWT_EMERGENCY_ACCESS_INVITE_ISSUER: Lazy<String> = - Lazy::new(|| format!("{}|emergencyaccessinvite", CONFIG.domain_origin())); -static JWT_DELETE_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|delete", CONFIG.domain_origin())); -static JWT_VERIFYEMAIL_ISSUER: Lazy<String> = - Lazy::new(|| format!("{}|verifyemail", CONFIG.domain_origin())); -static JWT_SEND_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|send", CONFIG.domain_origin())); -static JWT_ORG_API_KEY_ISSUER: Lazy<String> = - Lazy::new(|| format!("{}|api.organization", CONFIG.domain_origin())); -static JWT_FILE_DOWNLOAD_ISSUER: Lazy<String> = - Lazy::new(|| format!("{}|file_download", CONFIG.domain_origin())); - -static PRIVATE_RSA_KEY: Lazy<EncodingKey> = Lazy::new(|| { - let key = std::fs::read(CONFIG.private_rsa_key()) +static DEFAULT_VALIDITY: OnceLock<Duration> = OnceLock::new(); +#[inline] +fn init_default_validity() { + DEFAULT_VALIDITY + .set(Duration::hours(2)) + .expect("DEFAULT_VALIDITY must only be initialized once") +} +#[inline] +pub fn get_default_validity() -> &'static Duration { + DEFAULT_VALIDITY + .get() + .expect("DEFAULT_VALIDITY must be initialized in main") +} +static JWT_HEADER: OnceLock<Header> = OnceLock::new(); +#[inline] +fn init_jwt_header() { + JWT_HEADER + .set(Header::new(JWT_ALGORITHM)) + .expect("JWT_HEADER must only be initialized once") +} +#[inline] +fn get_jwt_header() -> &'static Header { + JWT_HEADER + .get() + .expect("JWT_HEADER must be initialized in main") +} +pub static JWT_LOGIN_ISSUER: OnceLock<String> = OnceLock::new(); +#[inline] +fn init_jwt_login_issuer() { + JWT_LOGIN_ISSUER + .set(format!("{}|login", config::get_config().domain_origin())) + .expect("JWT_LOGIN_ISSUER must only be initialized once") +} +#[inline] +pub fn get_jwt_login_issuer() -> &'static str { + JWT_LOGIN_ISSUER + .get() + .expect("JWT_LOGIN_ISSUER must be initialized in main") + .as_str() +} +static JWT_INVITE_ISSUER: OnceLock<String> = OnceLock::new(); +#[inline] +fn init_jwt_invite_issuer() { + JWT_INVITE_ISSUER + .set(format!("{}|invite", config::get_config().domain_origin())) + .expect("JWT_INVITE_ISSUER must only be initialized once") +} +#[inline] +fn get_jwt_invite_issuer() -> &'static str { + JWT_INVITE_ISSUER + .get() + .expect("JWT_INVITE_ISSUER must be initialized in main") + .as_str() +} +static JWT_DELETE_ISSUER: OnceLock<String> = OnceLock::new(); +#[inline] +fn init_jwt_delete_issuer() { + JWT_DELETE_ISSUER + .set(format!("{}|delete", config::get_config().domain_origin())) + .expect("JWT_DELETE_ISSUER must only be initialized once") +} +#[inline] +fn get_jwt_delete_issuer() -> &'static str { + JWT_DELETE_ISSUER + .get() + .expect("JWT_DELETE_ISSUER must be initialized in main") + .as_str() +} +static JWT_VERIFYEMAIL_ISSUER: OnceLock<String> = OnceLock::new(); +#[inline] +fn init_jwt_verifyemail_issuer() { + JWT_VERIFYEMAIL_ISSUER + .set(format!( + "{}|verifyemail", + config::get_config().domain_origin() + )) + .expect("JWT_VERIFYEMAIL_ISSUER must only be initialized once") +} +#[inline] +fn get_jwt_verifyemail_issuer() -> &'static str { + JWT_VERIFYEMAIL_ISSUER + .get() + .expect("JWT_VERIFYEMAIL_ISSUER must be initialized in main") + .as_str() +} +static JWT_ORG_API_KEY_ISSUER: OnceLock<String> = OnceLock::new(); +#[inline] +fn init_jwt_org_api_key_issuer() { + JWT_ORG_API_KEY_ISSUER + .set(format!( + "{}|api.organization", + config::get_config().domain_origin() + )) + .expect("JWT_ORG_API_KEY_ISSUER must only be initialized once") +} +#[inline] +fn get_jwt_org_api_key_issuer() -> &'static str { + JWT_ORG_API_KEY_ISSUER + .get() + .expect("JWT_ORG_API_KEY_ISSUER must be initialized in main") + .as_str() +} +static JWT_FILE_DOWNLOAD_ISSUER: OnceLock<String> = OnceLock::new(); +#[inline] +fn init_jwt_file_download_issuer() { + JWT_FILE_DOWNLOAD_ISSUER + .set(format!( + "{}|file_download", + config::get_config().domain_origin() + )) + .expect("JWT_FILE_DOWNLOAD_ISSUER must only be initialized once") +} +#[inline] +fn get_jwt_file_download_issuer() -> &'static str { + JWT_FILE_DOWNLOAD_ISSUER + .get() + .expect("JWT_FILE_DOWNLOAD_ISSUER must be initialized in main") + .as_str() +} +static PRIVATE_RSA_KEY: OnceLock<EncodingKey> = OnceLock::new(); +#[inline] +fn init_private_rsa_key() { + let key = std::fs::read(Config::PRIVATE_RSA_KEY) .unwrap_or_else(|e| panic!("Error loading private RSA Key. \n{e}")); - EncodingKey::from_rsa_pem(&key) - .unwrap_or_else(|e| panic!("Error decoding private RSA Key.\n{e}")) -}); -static PUBLIC_RSA_KEY: Lazy<DecodingKey> = Lazy::new(|| { - let key = std::fs::read(CONFIG.public_rsa_key()) + assert!( + PRIVATE_RSA_KEY + .set( + EncodingKey::from_rsa_pem(&key) + .unwrap_or_else(|e| panic!("Error decoding private RSA Key.\n{e}")) + ) + .is_ok(), + "PRIVATE_RSA_KEY must only be initialized once" + ) +} +#[inline] +fn get_private_rsa_key() -> &'static EncodingKey { + PRIVATE_RSA_KEY + .get() + .expect("PRIVATE_RSA_KEY must be initialized in main") +} +static PUBLIC_RSA_KEY: OnceLock<DecodingKey> = OnceLock::new(); +#[inline] +fn init_public_rsa_key() { + let key = std::fs::read(Config::PUBLIC_RSA_KEY) .unwrap_or_else(|e| panic!("Error loading public RSA Key. \n{e}")); - DecodingKey::from_rsa_pem(&key) - .unwrap_or_else(|e| panic!("Error decoding public RSA Key.\n{e}")) -}); - -pub fn load_keys() { - Lazy::force(&PRIVATE_RSA_KEY); - Lazy::force(&PUBLIC_RSA_KEY); + assert!( + PUBLIC_RSA_KEY + .set( + DecodingKey::from_rsa_pem(&key) + .unwrap_or_else(|e| panic!("Error decoding public RSA Key.\n{e}")) + ) + .is_ok(), + "PUBLIC_RSA_KEY must only be initialized once" + ) +} +#[inline] +fn get_public_rsa_key() -> &'static DecodingKey { + PUBLIC_RSA_KEY + .get() + .expect("PUBLIC_RSA_KEY must be initialized in main") +} +#[inline] +pub fn init_values() { + init_default_validity(); + init_jwt_header(); + init_jwt_login_issuer(); + init_jwt_invite_issuer(); + init_jwt_delete_issuer(); + init_jwt_verifyemail_issuer(); + init_jwt_org_api_key_issuer(); + init_jwt_file_download_issuer(); +} +#[inline] +pub fn init_rsa_keys() { + init_private_rsa_key(); + init_public_rsa_key(); } - pub fn encode_jwt<T: Serialize>(claims: &T) -> String { - match jsonwebtoken::encode(&JWT_HEADER, claims, &PRIVATE_RSA_KEY) { + match jsonwebtoken::encode(get_jwt_header(), claims, get_private_rsa_key()) { Ok(token) => token, Err(e) => panic!("Error encoding jwt {e}"), } @@ -62,7 +202,7 @@ fn decode_jwt<T: DeserializeOwned>(token: &str, issuer: String) -> Result<T, Err validation.set_issuer(&[issuer]); let token = token.replace(char::is_whitespace, ""); - match jsonwebtoken::decode(&token, &PUBLIC_RSA_KEY, &validation) { + match jsonwebtoken::decode(&token, get_public_rsa_key(), &validation) { Ok(d) => Ok(d.claims), Err(err) => match *err.kind() { ErrorKind::InvalidToken => err!("Token is invalid"), @@ -74,37 +214,27 @@ fn decode_jwt<T: DeserializeOwned>(token: &str, issuer: String) -> Result<T, Err } pub fn decode_login(token: &str) -> Result<LoginJwtClaims, Error> { - decode_jwt(token, JWT_LOGIN_ISSUER.to_string()) + decode_jwt(token, get_jwt_login_issuer().to_string()) } pub fn decode_invite(token: &str) -> Result<InviteJwtClaims, Error> { - decode_jwt(token, JWT_INVITE_ISSUER.to_string()) -} - -pub fn decode_emergency_access_invite( - token: &str, -) -> Result<EmergencyAccessInviteJwtClaims, Error> { - decode_jwt(token, JWT_EMERGENCY_ACCESS_INVITE_ISSUER.to_string()) + decode_jwt(token, get_jwt_invite_issuer().to_string()) } pub fn decode_delete(token: &str) -> Result<BasicJwtClaims, Error> { - decode_jwt(token, JWT_DELETE_ISSUER.to_string()) + decode_jwt(token, get_jwt_delete_issuer().to_string()) } pub fn decode_verify_email(token: &str) -> Result<BasicJwtClaims, Error> { - decode_jwt(token, JWT_VERIFYEMAIL_ISSUER.to_string()) -} - -pub fn decode_send(token: &str) -> Result<BasicJwtClaims, Error> { - decode_jwt(token, JWT_SEND_ISSUER.to_string()) + decode_jwt(token, get_jwt_verifyemail_issuer().to_string()) } pub fn decode_api_org(token: &str) -> Result<OrgApiKeyLoginJwtClaims, Error> { - decode_jwt(token, JWT_ORG_API_KEY_ISSUER.to_string()) + decode_jwt(token, get_jwt_org_api_key_issuer().to_string()) } pub fn decode_file_download(token: &str) -> Result<FileDownloadClaims, Error> { - decode_jwt(token, JWT_FILE_DOWNLOAD_ISSUER.to_string()) + decode_jwt(token, get_jwt_file_download_issuer().to_string()) } #[derive(Debug, Serialize, Deserialize)] @@ -155,27 +285,6 @@ pub struct InviteJwtClaims { pub invited_by_email: Option<String>, } -pub fn generate_invite_claims( - uuid: String, - email: String, - org_id: Option<String>, - user_org_id: Option<String>, - invited_by_email: Option<String>, -) -> InviteJwtClaims { - let time_now = Utc::now().naive_utc(); - let expire_hours = i64::from(CONFIG.invitation_expiration_hours()); - InviteJwtClaims { - nbf: time_now.timestamp(), - exp: (time_now + Duration::hours(expire_hours)).timestamp(), - iss: JWT_INVITE_ISSUER.to_string(), - sub: uuid, - email, - org_id, - user_org_id, - invited_by_email, - } -} - #[derive(Debug, Serialize, Deserialize)] pub struct EmergencyAccessInviteJwtClaims { // Not before @@ -193,27 +302,6 @@ pub struct EmergencyAccessInviteJwtClaims { pub grantor_email: String, } -pub fn generate_emergency_access_invite_claims( - uuid: String, - email: String, - emer_id: String, - grantor_name: String, - grantor_email: String, -) -> EmergencyAccessInviteJwtClaims { - let time_now = Utc::now().naive_utc(); - let expire_hours = i64::from(CONFIG.invitation_expiration_hours()); - EmergencyAccessInviteJwtClaims { - nbf: time_now.timestamp(), - exp: (time_now + Duration::hours(expire_hours)).timestamp(), - iss: JWT_EMERGENCY_ACCESS_INVITE_ISSUER.to_string(), - sub: uuid, - email, - emer_id, - grantor_name, - grantor_email, - } -} - #[derive(Debug, Serialize, Deserialize)] pub struct OrgApiKeyLoginJwtClaims { // Not before @@ -238,7 +326,7 @@ pub fn generate_organization_api_key_login_claims( OrgApiKeyLoginJwtClaims { nbf: time_now.timestamp(), exp: (time_now + Duration::hours(1)).timestamp(), - iss: JWT_ORG_API_KEY_ISSUER.to_string(), + iss: get_jwt_org_api_key_issuer().to_string(), sub: uuid, client_id: format!("organization.{org_id}"), client_sub: org_id, @@ -265,7 +353,7 @@ pub fn generate_file_download_claims(uuid: String, file_id: String) -> FileDownl FileDownloadClaims { nbf: time_now.timestamp(), exp: (time_now + Duration::minutes(5)).timestamp(), - iss: JWT_FILE_DOWNLOAD_ISSUER.to_string(), + iss: get_jwt_file_download_issuer().to_string(), sub: uuid, file_id, } @@ -283,41 +371,6 @@ pub struct BasicJwtClaims { pub sub: String, } -pub fn generate_delete_claims(uuid: String) -> BasicJwtClaims { - let time_now = Utc::now().naive_utc(); - let expire_hours = i64::from(CONFIG.invitation_expiration_hours()); - BasicJwtClaims { - nbf: time_now.timestamp(), - exp: (time_now + Duration::hours(expire_hours)).timestamp(), - iss: JWT_DELETE_ISSUER.to_string(), - sub: uuid, - } -} - -pub fn generate_verify_email_claims(uuid: String) -> BasicJwtClaims { - let time_now = Utc::now().naive_utc(); - let expire_hours = i64::from(CONFIG.invitation_expiration_hours()); - BasicJwtClaims { - nbf: time_now.timestamp(), - exp: (time_now + Duration::hours(expire_hours)).timestamp(), - iss: JWT_VERIFYEMAIL_ISSUER.to_string(), - sub: uuid, - } -} - -pub fn generate_send_claims(send_id: &str, file_id: &str) -> BasicJwtClaims { - let time_now = Utc::now().naive_utc(); - BasicJwtClaims { - nbf: time_now.timestamp(), - exp: (time_now + Duration::minutes(2)).timestamp(), - iss: JWT_SEND_ISSUER.to_string(), - sub: format!("{send_id}/{file_id}"), - } -} - -// -// Bearer token authentication -// use rocket::{ outcome::try_outcome, request::{FromRequest, Outcome, Request}, @@ -338,38 +391,10 @@ pub struct Host { impl<'r> FromRequest<'r> for Host { type Error = &'static str; - async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> { - let headers = request.headers(); - - // Get host - let host = if CONFIG.domain_set() { - CONFIG.domain() - } else if let Some(referer) = headers.get_one("Referer") { - referer.to_string() - } else { - // Try to guess from the headers - use std::env; - - let protocol = if let Some(proto) = headers.get_one("X-Forwarded-Proto") { - proto - } else if env::var("ROCKET_TLS").is_ok() { - "https" - } else { - "http" - }; - - let host = if let Some(host) = headers.get_one("X-Forwarded-Host") { - host - } else if let Some(host) = headers.get_one("Host") { - host - } else { - "" - }; - - format!("{protocol}://{host}") - }; - - Outcome::Success(Host { host }) + async fn from_request(_: &'r Request<'_>) -> Outcome<Self, Self::Error> { + Outcome::Success(Host { + host: config::get_config().domain.to_string(), + }) } } @@ -820,19 +845,15 @@ impl<'r> FromRequest<'r> for ClientIp { type Error = (); async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> { - let ip = if CONFIG._ip_header_enabled() { - req.headers().get_one(&CONFIG.ip_header()).and_then(|ip| { - match ip.find(',') { - Some(idx) => &ip[..idx], - None => ip, - } - .parse() - .map_err(|_| warn!("'{}' header is malformed: {}", CONFIG.ip_header(), ip)) - .ok() - }) - } else { - None - }; + let ip = req.headers().get_one("X-Real-IP").and_then(|ip| { + match ip.find(',') { + Some(idx) => &ip[..idx], + None => ip, + } + .parse() + .map_err(|_| warn!("'X-Real-IP' header is malformed: {ip}")) + .ok() + }); let ip = ip .or_else(|| req.remote().map(|r| r.ip())) diff --git a/src/config.rs b/src/config.rs @@ -1,921 +1,175 @@ -use once_cell::sync::Lazy; -use std::env::consts::EXE_SUFFIX; -use std::sync::RwLock; -use url::Url; - -use crate::{ - db::DbConnType, - error::Error, - util::{get_env, get_env_bool}, -}; - -pub static CONFIG: Lazy<Config> = Lazy::new(|| Config::load().expect("unable to load '.env'")); -pub type Pass = String; - -macro_rules! make_config { - ($( - $(#[doc = $groupdoc:literal])? - $group:ident $(: $group_enabled:ident)? { - $( - $(#[doc = $doc:literal])+ - $name:ident : $ty:ident, $editable:literal, $none_action:ident $(, $default:expr)?; - )+}, - )+) => { - pub struct Config { inner: RwLock<Inner> } - - struct Inner { - rocket_shutdown_handle: Option<rocket::Shutdown>, - ws_shutdown_handle: Option<tokio::sync::oneshot::Sender<()>>, - templates: Handlebars<'static>, - config: ConfigItems, - _env: ConfigBuilder, - } - - #[derive(Clone, Default, Deserialize, Serialize)] - pub struct ConfigBuilder { - $($( - #[serde(skip_serializing_if = "Option::is_none")] - $name: Option<$ty>, - )+)+ - } - - impl ConfigBuilder { - #[allow(clippy::field_reassign_with_default)] - fn from_env() -> Self { - if dotenvy::from_path(".env").is_err() { - panic!("'.env' does not exist") - } - let mut builder = ConfigBuilder::default(); - $($( - builder.$name = make_config! { @getenv paste::paste!(stringify!([<$name:upper>])), $ty }; - )+)+ - - builder - } - fn build(&self) -> ConfigItems { - let mut config = ConfigItems::default(); - let _domain_set = self.domain.is_some(); - $($( - config.$name = make_config!{ @build self.$name.clone(), &config, $none_action, $($default)? }; - )+)+ - config.domain_set = _domain_set; - - config.domain = config.domain.trim_end_matches('/').to_string(); - - config.signups_domains_whitelist = config.signups_domains_whitelist.trim().to_lowercase(); - config.org_creation_users = config.org_creation_users.trim().to_lowercase(); - - config - } - } - #[derive(Clone, Default)] - struct ConfigItems { $($( $name: make_config!{@type $ty, $none_action}, )+)+ } - #[allow(unused)] - impl Config { - $($( - $(#[doc = $doc])+ - pub fn $name(&self) -> make_config!{@type $ty, $none_action} { - self.inner.read().unwrap().config.$name.clone() - } - )+)+ - } - }; - // Support string print - ( @supportstr $name:ident, $value:expr, Pass, option ) => { serde_json::to_value($value.as_ref().map(|_| String::from("***"))).unwrap() }; // Optional pass, we map to an Option<String> with "***" - ( @supportstr $name:ident, $value:expr, Pass, $none_action:ident ) => { "***".into() }; // Required pass, we return "***" - ( @supportstr $name:ident, $value:expr, String, option ) => { // Optional other value, we return as is or convert to string to apply the privacy config - if PRIVACY_CONFIG.contains(&stringify!($name)) { - serde_json::to_value($value.as_ref().map(|x| _privacy_mask(x) )).unwrap() - } else { - serde_json::to_value($value).unwrap() - } - }; - ( @supportstr $name:ident, $value:expr, String, $none_action:ident ) => { // Required other value, we return as is or convert to string to apply the privacy config - if PRIVACY_CONFIG.contains(&stringify!($name)) { - _privacy_mask(&$value).into() - } else { - ($value).into() - } - }; - ( @supportstr $name:ident, $value:expr, $ty:ty, option ) => { serde_json::to_value($value).unwrap() }; // Optional other value, we return as is or convert to string to apply the privacy config - ( @supportstr $name:ident, $value:expr, $ty:ty, $none_action:ident ) => { ($value).into() }; // Required other value, we return as is or convert to string to apply the privacy config - - // Group or empty string - ( @show ) => { "" }; - ( @show $lit:literal ) => { $lit }; - - // Wrap the optionals in an Option type - ( @type $ty:ty, option) => { Option<$ty> }; - ( @type $ty:ty, $id:ident) => { $ty }; - - // Generate the values depending on none_action - ( @build $value:expr, $config:expr, option, ) => { $value }; - ( @build $value:expr, $config:expr, def, $default:expr ) => { $value.unwrap_or($default) }; - ( @build $value:expr, $config:expr, auto, $default_fn:expr ) => {{ - match $value { - Some(v) => v, - None => { - let f: &dyn Fn(&ConfigItems) -> _ = &$default_fn; - f($config) - } - } - }}; - ( @build $value:expr, $config:expr, gen, $default_fn:expr ) => {{ - let f: &dyn Fn(&ConfigItems) -> _ = &$default_fn; - f($config) - }}; - - ( @getenv $name:expr, bool ) => { get_env_bool($name) }; - ( @getenv $name:expr, $ty:ident ) => { get_env($name) }; - +use core::fmt::{self, Display, Formatter}; +use core::num::NonZeroU8; +use rocket::config::{CipherSuite, LogLevel, TlsConfig}; +use rocket::data::{Limits, ToByteUnit}; +use std::env; +use std::error; +use std::fs; +use std::io::Error; +use std::net::IpAddr; +use std::sync::OnceLock; +use toml::{self, de}; +use url::{ParseError, Url}; +static CONFIG: OnceLock<Config> = OnceLock::new(); +#[inline] +pub fn init_config() { + CONFIG + .set(Config::load().expect("valid TOML config file at 'config.toml'")) + .expect("CONFIG must only be initialized once") } - -//STRUCTURE: -// /// Short description (without this they won't appear on the list) -// group { -// /// Friendly Name |> Description (Optional) -// name: type, is_editable, action, <default_value (Optional)> -// } -// -// Where action applied when the value wasn't provided and can be: -// def: Use a default value -// auto: Value is auto generated based on other values -// option: Value is optional -// gen: Value is always autogenerated and it's original value ignored -make_config! { - folders { - /// Data folder |> Main data folder - data_folder: String, false, def, "data".to_string(); - /// Database URL - database_url: String, false, auto, |c| format!("{}/{}", c.data_folder, "db.sqlite3"); - /// Icon cache folder - icon_cache_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "icon_cache"); - /// Attachments folder - attachments_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "attachments"); - /// Sends folder - sends_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "sends"); - /// Temp folder |> Used for storing temporary file uploads - tmp_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "tmp"); - /// Templates folder - templates_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "templates"); - /// Session JWT key - rsa_key_filename: String, false, auto, |c| format!("{}/{}", c.data_folder, "rsa_key"); - /// Web vault folder - web_vault_folder: String, false, def, "web-vault/".to_string(); - }, - ws { - /// Enable websocket notifications - websocket_enabled: bool, false, def, false; - /// Websocket address - websocket_address: String, false, def, "0.0.0.0".to_string(); - /// Websocket port - websocket_port: u16, false, def, 3012; - }, - push { - /// Enable push notifications - push_enabled: bool, false, def, false; - /// Push relay base uri - push_relay_uri: String, false, def, "https://push.bitwarden.com".to_string(); - /// Installation id |> The installation id from https://bitwarden.com/host - push_installation_id: Pass, false, def, String::new(); - /// Installation key |> The installation key from https://bitwarden.com/host - push_installation_key: Pass, false, def, String::new(); - }, - jobs { - /// Job scheduler poll interval |> How often the job scheduler thread checks for jobs to run. - /// Set to 0 to globally disable scheduled jobs. - job_poll_interval_ms: u64, false, def, 30_000; - /// Send purge schedule |> Cron schedule of the job that checks for Sends past their deletion date. - /// Defaults to hourly. Set blank to disable this job. - send_purge_schedule: String, false, def, "0 5 * * * *".to_string(); - /// Trash purge schedule |> Cron schedule of the job that checks for trashed items to delete permanently. - /// Defaults to daily. Set blank to disable this job. - trash_purge_schedule: String, false, def, "0 5 0 * * *".to_string(); - /// Incomplete 2FA login schedule |> Cron schedule of the job that checks for incomplete 2FA logins. - /// Defaults to once every minute. Set blank to disable this job. - incomplete_2fa_schedule: String, false, def, "30 * * * * *".to_string(); - /// Emergency notification reminder schedule |> Cron schedule of the job that sends expiration reminders to emergency access grantors. - /// Defaults to hourly. (3 minutes after the hour) Set blank to disable this job. - emergency_notification_reminder_schedule: String, false, def, "0 3 * * * *".to_string(); - /// Emergency request timeout schedule |> Cron schedule of the job that grants emergency access requests that have met the required wait time. - /// Defaults to hourly. (7 minutes after the hour) Set blank to disable this job. - emergency_request_timeout_schedule: String, false, def, "0 7 * * * *".to_string(); - /// Event cleanup schedule |> Cron schedule of the job that cleans old events from the event table. - /// Defaults to daily. Set blank to disable this job. - event_cleanup_schedule: String, false, def, "0 10 0 * * *".to_string(); - /// Auth Request cleanup schedule |> Cron schedule of the job that cleans old auth requests from the auth request. - /// Defaults to every minute. Set blank to disable this job. - auth_request_purge_schedule: String, false, def, "30 * * * * *".to_string(); - - }, - - /// General settings - settings { - /// Domain URL |> This needs to be set to the URL used to access the server, including 'http[s]://' - /// and port, if it's different than the default. Some server functions don't work correctly without this value - domain: String, true, def, "http://localhost".to_string(); - /// Domain Set |> Indicates if the domain is set by the admin. Otherwise the default will be used. - domain_set: bool, false, def, false; - /// Domain origin |> Domain URL origin (in https://example.com:8443/path, https://example.com:8443 is the origin) - domain_origin: String, false, auto, |c| extract_url_origin(&c.domain); - /// Domain path |> Domain URL path (in https://example.com:8443/path, /path is the path) - domain_path: String, false, auto, |c| extract_url_path(&c.domain); - /// Enable web vault - web_vault_enabled: bool, false, def, true; - - /// Allow Sends |> Controls whether users are allowed to create Bitwarden Sends. - /// This setting applies globally to all users. To control this on a per-org basis instead, use the "Disable Send" org policy. - sends_allowed: bool, true, def, true; - - /// HIBP Api Key |> HaveIBeenPwned API Key, request it here: https://haveibeenpwned.com/API/Key - hibp_api_key: Pass, true, option; - - /// 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 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. - /// If unset, trashed items are not auto-deleted. This setting applies globally, so make - /// sure to inform all users of any changes to this setting. - trash_auto_delete_days: i64, true, option; - - /// Incomplete 2FA time limit |> Number of minutes to wait before a 2FA-enabled login is - /// considered incomplete, resulting in an email notification. An incomplete 2FA login is one - /// where the correct master password was provided but the required 2FA step was not completed, - /// which potentially indicates a master password compromise. Set to 0 to disable this check. - /// This setting applies globally to all users. - incomplete_2fa_time_limit: i64, true, def, 3; - - /// Disable icon downloads |> Set to true to disable icon downloading in the internal icon service. - /// This still serves existing icons from $ICON_CACHE_FOLDER, without generating any external - /// network requests. $ICON_CACHE_TTL must also be set to 0; otherwise, the existing icons - /// will be deleted eventually, but won't be downloaded again. - disable_icon_download: bool, true, def, false; - /// Allow new signups |> Controls whether new users can register. Users can be invited by the vaultwarden admin even if this is disabled - signups_allowed: bool, true, def, true; - /// Require email verification on signups. This will prevent logins from succeeding until the address has been verified - signups_verify: bool, true, def, false; - /// If signups require email verification, automatically re-send verification email if it hasn't been sent for a while (in seconds) - signups_verify_resend_time: u64, true, def, 3_600; - /// If signups require email verification, limit how many emails are automatically sent when login is attempted (0 means no limit) - signups_verify_resend_limit: u32, true, def, 6; - /// Email domain whitelist |> Allow signups only from this list of comma-separated domains, even when signups are otherwise disabled - signups_domains_whitelist: String, true, def, String::new(); - /// Enable event logging |> Enables event logging for organizations. - org_events_enabled: bool, false, def, false; - /// Org creation users |> Allow org creation only by this list of comma-separated user emails. - /// Blank or 'all' means all users can create orgs; 'none' means no users can create orgs. - org_creation_users: String, true, def, String::new(); - /// Allow invitations |> Controls whether users can be invited by organization admins, even when signups are otherwise disabled - invitations_allowed: bool, true, def, true; - /// Invitation token expiration time (in hours) |> The number of hours after which an organization invite token, emergency access invite token, - /// email verification token and deletion request token will expire (must be at least 1) - invitation_expiration_hours: u32, false, def, 120; - /// Allow emergency access |> Controls whether users can enable emergency access to their accounts. This setting applies globally to all users. - emergency_access_allowed: bool, true, def, true; - /// Allow email change |> Controls whether users can change their email. This setting applies globally to all users. - email_change_allowed: bool, true, def, true; - /// Password iterations |> Number of server-side passwords hashing iterations for the password hash. - /// The default for new users. If changed, it will be updated during login for existing users. - password_iterations: i32, true, def, 600_000; - /// Allow password hints |> Controls whether users can set password hints. This setting applies globally to all users. - password_hints_allowed: 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 token/Argon2 PHC |> The plain text token or Argon2 PHC string used to authenticate in this very same page. Changing it here will not deauthorize the current session! - admin_token: Pass, true, option; - - /// Invitation organization name |> Name shown in the invitation emails that don't come from a specific organization - invitation_org_name: String, true, def, "Vaultwarden".to_string(); - - /// Events days retain |> Number of days to retain events stored in the database. If unset, events are kept indefinitely. - events_days_retain: i64, false, option; - }, - - /// Advanced settings - advanced { - /// Client IP header |> If not present, the remote IP is used. - /// Set to the string "none" (without quotes), to disable any headers and just use the remote IP - ip_header: String, true, def, "X-Real-IP".to_string(); - /// Internal IP header property, used to avoid recomputing each time - _ip_header_enabled: bool, false, gen, |c| &c.ip_header.trim().to_lowercase() != "none"; - /// Icon service |> The predefined icon services are: internal, bitwarden, duckduckgo, google. - /// To specify a custom icon service, set a URL template with exactly one instance of `{}`, - /// which is replaced with the domain. For example: `https://icon.example.com/domain/{}`. - /// `internal` refers to Vaultwarden's built-in icon fetching implementation. If an external - /// service is set, an icon request to Vaultwarden will return an HTTP redirect to the - /// corresponding icon at the external service. - icon_service: String, false, def, "internal".to_string(); - /// _icon_service_url - _icon_service_url: String, false, gen, |c| generate_icon_service_url(&c.icon_service); - /// _icon_service_csp - _icon_service_csp: String, false, gen, |c| generate_icon_service_csp(&c.icon_service, &c._icon_service_url); - /// Icon redirect code |> The HTTP status code to use for redirects to an external icon service. - /// The supported codes are 301 (legacy permanent), 302 (legacy temporary), 307 (temporary), and 308 (permanent). - /// Temporary redirects are useful while testing different icon services, but once a service - /// has been decided on, consider using permanent redirects for cacheability. The legacy codes - /// are currently better supported by the Bitwarden clients. - icon_redirect_code: u32, true, def, 302; - /// Positive icon cache expiry |> Number of seconds to consider that an already cached icon is fresh. After this period, the icon will be refreshed - icon_cache_ttl: u64, true, def, 2_592_000; - /// Negative icon cache expiry |> Number of seconds before trying to download an icon that failed again. - icon_cache_negttl: u64, true, def, 259_200; - /// Icon download timeout |> Number of seconds when to stop attempting to download an icon. - icon_download_timeout: u64, true, def, 10; - /// Icon blacklist Regex |> Any domains or IPs that match this regex won't be fetched by the icon service. - /// Useful to hide other servers in the local network. Check the WIKI for more details - icon_blacklist_regex: String, true, option; - /// Icon blacklist non global IPs |> Any IP which is not defined as a global IP will be blacklisted. - /// Useful to secure your internal environment: See https://en.wikipedia.org/wiki/Reserved_IP_addresses for a list of IPs which it will block - icon_blacklist_non_global_ips: bool, true, def, true; - - /// Disable Two-Factor remember |> Enabling this would force the users to use a second factor to login every time. - /// Note that the checkbox would still be present, but ignored. - disable_2fa_remember: bool, true, def, false; - - /// Disable authenticator time drifted codes to be valid |> Enabling this only allows the current TOTP code to be valid - /// TOTP codes of the previous and next 30 seconds will be invalid. - authenticator_disable_time_drift: bool, true, def, false; - - /// Require new device emails |> When a user logs in an email is required to be sent. - /// If sending the email fails the login attempt will fail. - require_device_email: bool, true, def, false; - - /// Reload templates (Dev) |> When this is set to true, the templates get reloaded with every request. - /// ONLY use this during development, as it can slow down the server - reload_templates: bool, true, def, false; - /// Enable extended logging - extended_logging: bool, false, def, true; - /// Log timestamp format - log_timestamp_format: String, true, def, "%Y-%m-%d %H:%M:%S.%3f".to_string(); - /// Enable the log to output to Syslog - use_syslog: bool, false, def, false; - /// Log file path - log_file: String, false, option; - /// Log level - log_level: String, false, def, "Info".to_string(); - - /// Enable DB WAL |> Turning this off might lead to worse performance, but might help if using vaultwarden on some exotic filesystems, - /// that do not support WAL. Please make sure you read project wiki on the topic before changing this setting. - enable_db_wal: bool, false, def, true; - - /// Max database connection retries |> Number of times to retry the database connection during startup, with 1 second between each retry, set to 0 to retry indefinitely - db_connection_retries: u32, false, def, 15; - - /// Timeout when acquiring database connection - database_timeout: u64, false, def, 30; - - /// Database connection pool size - database_max_conns: u32, false, def, 10; - - /// Database connection init |> SQL statements to run when creating a new database connection, mainly useful for connection-scoped pragmas. If empty, a database-specific default is used. - database_conn_init: String, false, def, String::new(); - - /// Bypass admin page security (Know the risks!) |> Disables the Admin Token for the admin page so you may use your own auth in-front - disable_admin_token: bool, false, def, false; - - /// Allowed iframe ancestors (Know the risks!) |> Allows other domains to embed the web vault into an iframe, useful for embedding into secure intranets - allowed_iframe_ancestors: String, true, def, String::new(); - - /// Seconds between login requests |> Number of seconds, on average, between login and 2FA requests from the same IP address before rate limiting kicks in - login_ratelimit_seconds: u64, false, def, 60; - /// Max burst size for login requests |> Allow a burst of requests of up to this size, while maintaining the average indicated by `login_ratelimit_seconds`. Note that this applies to both the login and the 2FA, so it's recommended to allow a burst size of at least 2 - login_ratelimit_max_burst: u32, false, def, 10; - - /// Seconds between admin login requests |> Number of seconds, on average, between admin requests from the same IP address before rate limiting kicks in - admin_ratelimit_seconds: u64, false, def, 300; - /// Max burst size for admin login requests |> Allow a burst of requests of up to this size, while maintaining the average indicated by `admin_ratelimit_seconds` - admin_ratelimit_max_burst: u32, false, def, 3; - - /// Admin session lifetime |> Set the lifetime of admin sessions to this value (in minutes). - admin_session_lifetime: i64, true, def, 20; - - /// Enable groups (BETA!) (Know the risks!) |> Enables groups support for organizations (Currently contains known issues!). - org_groups_enabled: bool, false, def, false; - }, - - /// Yubikey settings - yubico: _enable_yubico { - /// Enabled - _enable_yubico: bool, true, def, true; - /// Client ID - yubico_client_id: String, true, option; - /// Secret Key - yubico_secret_key: Pass, true, option; - /// Server - yubico_server: String, true, option; - }, - - /// Global Duo settings (Note that users can override them) - duo: _enable_duo { - /// Enabled - _enable_duo: bool, true, def, true; - /// Integration Key - duo_ikey: String, true, option; - /// Secret Key - duo_skey: Pass, true, option; - /// Host - duo_host: String, true, option; - /// Application Key (generated automatically) - _duo_akey: Pass, false, option; - }, - - /// SMTP Email Settings - smtp: _enable_smtp { - /// Enabled - _enable_smtp: bool, true, def, true; - /// Use Sendmail |> Whether to send mail via the `sendmail` command - use_sendmail: bool, true, def, false; - /// Sendmail Command |> Which sendmail command to use. The one found in the $PATH is used if not specified. - sendmail_command: String, true, option; - /// Host - smtp_host: String, true, option; - /// DEPRECATED smtp_ssl |> DEPRECATED - Please use SMTP_SECURITY - smtp_ssl: bool, false, option; - /// DEPRECATED smtp_explicit_tls |> DEPRECATED - Please use SMTP_SECURITY - smtp_explicit_tls: bool, false, option; - /// Secure SMTP |> ("starttls", "force_tls", "off") Enable a secure connection. Default is "starttls" (Explicit - ports 587 or 25), "force_tls" (Implicit - port 465) or "off", no encryption - smtp_security: String, true, auto, |c| smtp_convert_deprecated_ssl_options(c.smtp_ssl, c.smtp_explicit_tls); // TODO: After deprecation make it `def, "starttls".to_string()` - /// Port - smtp_port: u16, true, auto, |c| if c.smtp_security == *"force_tls" {465} else if c.smtp_security == *"starttls" {587} else {25}; - /// From Address - smtp_from: String, true, def, String::new(); - /// From Name - smtp_from_name: String, true, def, "Vaultwarden".to_string(); - /// Username - smtp_username: String, true, option; - /// Password - smtp_password: Pass, true, option; - /// SMTP Auth mechanism |> Defaults for SSL is "Plain" and "Login" and nothing for Non-SSL connections. Possible values: ["Plain", "Login", "Xoauth2"]. Multiple options need to be separated by a comma ','. - smtp_auth_mechanism: String, true, option; - /// SMTP connection timeout |> Number of seconds when to stop trying to connect to the SMTP server - smtp_timeout: u64, true, def, 15; - /// Server name sent during HELO |> By default this value should be is on the machine's hostname, but might need to be changed in case it trips some anti-spam filters - helo_name: String, true, option; - /// Embed images as email attachments. - smtp_embed_images: bool, true, def, true; - /// _smtp_img_src - _smtp_img_src: String, false, gen, |c| generate_smtp_img_src(c.smtp_embed_images, &c.domain); - /// Enable SMTP debugging (Know the risks!) |> DANGEROUS: Enabling this will output very detailed SMTP messages. This could contain sensitive information like passwords and usernames! Only enable this during troubleshooting! - smtp_debug: bool, false, def, false; - /// Accept Invalid Certs (Know the risks!) |> DANGEROUS: Allow invalid certificates. This option introduces significant vulnerabilities to man-in-the-middle attacks! - smtp_accept_invalid_certs: bool, true, def, false; - /// Accept Invalid Hostnames (Know the risks!) |> DANGEROUS: Allow invalid hostnames. This option introduces significant vulnerabilities to man-in-the-middle attacks! - smtp_accept_invalid_hostnames: bool, true, def, false; - }, - - /// Email 2FA Settings - email_2fa: _enable_email_2fa { - /// Enabled |> Disabling will prevent users from setting up new email 2FA and using existing email 2FA configured - _enable_email_2fa: bool, true, auto, |c| c._enable_smtp && (c.smtp_host.is_some() || c.use_sendmail); - /// Email token size |> Number of digits in an email 2FA token (min: 6, max: 255). Note that the Bitwarden clients are hardcoded to mention 6 digit codes regardless of this setting. - email_token_size: u8, true, def, 6; - /// Token expiration time |> Maximum time in seconds a token is valid. The time the user has to open email client and copy token. - email_expiration_time: u64, true, def, 600; - /// Maximum attempts |> Maximum attempts before an email token is reset and a new email will need to be sent - email_attempts_limit: u64, true, def, 3; - }, +#[inline] +pub fn get_config() -> &'static Config { + CONFIG.get().expect("CONFIG must be initialized in main") } - -fn validate_config(cfg: &ConfigItems) -> Result<(), Error> { - // Validate connection URL is valid and DB feature is enabled - let url = &cfg.database_url; - if DbConnType::from_url(url)? == DbConnType::sqlite && url.contains('/') { - let path = std::path::Path::new(&url); - if let Some(parent) = path.parent() { - if !parent.is_dir() { - err!(format!( - "SQLite database directory `{}` does not exist or is not a directory", - parent.display() - )); - } - } - } - - if cfg.password_iterations < 100_000 { - err!("PASSWORD_ITERATIONS should be at least 100000 or higher. The default is 600000!"); - } - - let limit = 256; - if cfg.database_max_conns < 1 || cfg.database_max_conns > limit { - err!(format!( - "`DATABASE_MAX_CONNS` contains an invalid value. Ensure it is between 1 and {limit}.", - )); - } - let dom = cfg.domain.to_lowercase(); - if !dom.starts_with("http://") && !dom.starts_with("https://") { - err!( - "DOMAIN variable needs to contain the protocol (http, https). Use 'http[s]://bw.example.com' instead of 'bw.example.com'" - ); - } - - let whitelist = &cfg.signups_domains_whitelist; - if !whitelist.is_empty() && whitelist.split(',').any(|d| d.trim().is_empty()) { - err!("`SIGNUPS_DOMAINS_WHITELIST` contains empty tokens"); - } - - let org_creation_users = cfg.org_creation_users.trim().to_lowercase(); - if !(org_creation_users.is_empty() - || org_creation_users == "all" - || org_creation_users == "none") - && org_creation_users.split(',').any(|u| !u.contains('@')) - { - err!("`ORG_CREATION_USERS` contains invalid email addresses"); - } - if cfg._enable_smtp { - match cfg.smtp_security.as_str() { - "off" | "starttls" | "force_tls" => (), - _ => err!( - "`SMTP_SECURITY` is invalid. It needs to be one of the following options: starttls, force_tls or off" +#[derive(Debug)] +pub enum ConfigErr { + Io(Error), + De(de::Error), + Url(ParseError), + BadDomain, +} +impl Display for ConfigErr { + #[inline] + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match *self { + Self::Io(ref err) => err.fmt(f), + Self::De(ref err) => err.fmt(f), + Self::Url(ref err) => err.fmt(f), + Self::BadDomain => f.write_str( + "https://<domain>:<port> was unable to be parsed into a URL with a domain", ), } - - if cfg.use_sendmail { - let command = cfg - .sendmail_command - .clone() - .unwrap_or_else(|| format!("sendmail{EXE_SUFFIX}")); - - let mut path = std::path::PathBuf::from(&command); - - if !path.is_absolute() { - match which::which(&command) { - Ok(result) => path = result, - Err(_) => err!(format!("sendmail command {command:?} not found in $PATH")), - } - } - - match path.metadata() { - Err(err) if err.kind() == std::io::ErrorKind::NotFound => { - err!(format!("sendmail command not found at `{path:?}`")) - } - Err(err) => { - err!(format!( - "failed to access sendmail command at `{path:?}`: {err}" - )) - } - Ok(metadata) => { - if metadata.is_dir() { - err!(format!("sendmail command at `{path:?}` isn't a directory")); - } - - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - if !metadata.permissions().mode() & 0o111 != 0 { - err!(format!("sendmail command at `{path:?}` isn't executable")); - } - } - } - } - } else { - if cfg.smtp_host.is_some() == cfg.smtp_from.is_empty() { - err!("Both `SMTP_HOST` and `SMTP_FROM` need to be set for email support without `USE_SENDMAIL`") - } - - if cfg.smtp_username.is_some() != cfg.smtp_password.is_some() { - err!("Both `SMTP_USERNAME` and `SMTP_PASSWORD` need to be set to enable email authentication without `USE_SENDMAIL`") - } - } - - if (cfg.smtp_host.is_some() || cfg.use_sendmail) && !cfg.smtp_from.contains('@') { - err!("SMTP_FROM does not contain a mandatory @ sign") - } - } - // Check if the icon service is valid - let icon_service = cfg.icon_service.as_str(); - match icon_service { - "internal" => (), - _ => err!(format!( - "Icon service URL `{icon_service}` must start with \"http\"" - )), } - if cfg.invitation_expiration_hours < 1 { - err!("`INVITATION_EXPIRATION_HOURS` has a minimum duration of 1 hour") - } - Ok(()) } - -/// Extracts an RFC 6454 web origin from a URL. -fn extract_url_origin(url: &str) -> String { - match Url::parse(url) { - Ok(u) => u.origin().ascii_serialization(), - Err(e) => { - println!("Error validating domain: {e}"); - String::new() - } +impl error::Error for ConfigErr {} +impl From<Error> for ConfigErr { + #[inline] + fn from(value: Error) -> Self { + Self::Io(value) } } - -/// Extracts the path from a URL. -/// All trailing '/' chars are trimmed, even if the path is a lone '/'. -fn extract_url_path(url: &str) -> String { - match Url::parse(url) { - Ok(u) => u.path().trim_end_matches('/').to_string(), - Err(_) => { - // We already print it in the method above, no need to do it again - String::new() - } +impl From<de::Error> for ConfigErr { + #[inline] + fn from(value: de::Error) -> Self { + Self::De(value) } } - -fn generate_smtp_img_src(embed_images: bool, domain: &str) -> String { - if embed_images { - "cid:".to_string() - } else { - format!("{domain}/vw_static/") +impl From<ParseError> for ConfigErr { + #[inline] + fn from(value: ParseError) -> Self { + Self::Url(value) } } - -/// Generate the correct URL for the icon service. -/// This will be used within icons.rs to call the external icon service. -fn generate_icon_service_url(icon_service: &str) -> String { - match icon_service { - "internal" => String::new(), - "bitwarden" => "https://icons.bitwarden.net/{}/icon.png".to_string(), - "duckduckgo" => "https://icons.duckduckgo.com/ip3/{}.ico".to_string(), - "google" => "https://www.google.com/s2/favicons?domain={}&sz=32".to_string(), - _ => icon_service.to_string(), - } +#[derive(Debug, serde::Deserialize)] +struct Tls { + ciphers: Option<Vec<CipherSuite>>, + cert: String, + key: String, + prefer_server_cipher_order: Option<bool>, } - -/// Generate the CSP string needed to allow redirected icon fetching -fn generate_icon_service_csp(icon_service: &str, icon_service_url: &str) -> String { - // We split on the first '{', since that is the variable delimiter for an icon service URL. - // Everything up until the first '{' should be fixed and can be used as an CSP string. - let csp_string = match icon_service_url.split_once('{') { - Some((c, _)) => c.to_string(), - None => String::new(), - }; - - // Because Google does a second redirect to there gstatic.com domain, we need to add an extra csp string. - match icon_service { - "google" => csp_string + " https://*.gstatic.com/favicon", - _ => csp_string, - } +#[derive(Debug, serde::Deserialize)] +struct ConfigFile { + database_max_conns: Option<NonZeroU8>, + database_timeout: Option<u16>, + db_connection_retries: Option<NonZeroU8>, + domain: String, + ip: IpAddr, + org_attachment_limit: Option<u32>, + password_iterations: Option<i32>, + port: u16, + tls: Tls, + user_attachment_limit: Option<u32>, + web_vault_enabled: Option<bool>, + workers: Option<NonZeroU8>, } - -/// Convert the old SMTP_SSL and SMTP_EXPLICIT_TLS options -fn smtp_convert_deprecated_ssl_options( - smtp_ssl: Option<bool>, - smtp_explicit_tls: Option<bool>, -) -> String { - if smtp_explicit_tls.is_some() || smtp_ssl.is_some() { - println!("[DEPRECATED]: `SMTP_SSL` or `SMTP_EXPLICIT_TLS` is set. Please use `SMTP_SECURITY` instead."); - } - if smtp_explicit_tls.is_some() && smtp_explicit_tls.unwrap() { - return "force_tls".to_string(); - } else if smtp_ssl.is_some() && !smtp_ssl.unwrap() { - return "off".to_string(); - } - // Return the default `starttls` in all other cases - "starttls".to_string() +#[derive(Debug)] +pub struct Config { + pub database_max_conns: NonZeroU8, + pub database_timeout: u16, + pub db_connection_retries: NonZeroU8, + pub domain: Url, + pub org_attachment_limit: Option<u32>, + pub password_iterations: i32, + pub rocket: rocket::Config, + pub user_attachment_limit: Option<u32>, + pub web_vault_enabled: bool, } - impl Config { - pub fn load() -> Result<Self, Error> { - let _env = ConfigBuilder::from_env(); - let config = _env.build(); - validate_config(&config)?; - Ok(Config { - inner: RwLock::new(Inner { - rocket_shutdown_handle: None, - ws_shutdown_handle: None, - templates: load_templates(&config.templates_folder), - config, - _env, - }), - }) - } - /// Tests whether an email's domain is allowed. A domain is allowed if it - /// is in signups_domains_whitelist, or if no whitelist is set (so there - /// are no domain restrictions in effect). - pub fn is_email_domain_allowed(&self, email: &str) -> bool { - let e: Vec<&str> = email.rsplitn(2, '@').collect(); - if e.len() != 2 || e[0].is_empty() || e[1].is_empty() { - warn!("Failed to parse email address '{}'", email); - return false; - } - let email_domain = e[0].to_lowercase(); - let whitelist = self.signups_domains_whitelist(); - - whitelist.is_empty() || whitelist.split(',').any(|d| d.trim() == email_domain) - } - - /// Tests whether signup is allowed for an email address, taking into - /// account the signups_allowed and signups_domains_whitelist settings. - pub fn is_signup_allowed(&self, email: &str) -> bool { - if !self.signups_domains_whitelist().is_empty() { - // The whitelist setting overrides the signups_allowed setting. - self.is_email_domain_allowed(email) - } else { - self.signups_allowed() - } - } - - /// Tests whether the specified user is allowed to create an organization. - pub fn is_org_creation_allowed(&self, email: &str) -> bool { - let users = self.org_creation_users(); - if users.is_empty() || users == "all" { - true - } else if users == "none" { - false - } else { - let email = email.to_lowercase(); - users.split(',').any(|u| u.trim() == email) - } - } - - pub fn private_rsa_key(&self) -> String { - format!("{}.pem", CONFIG.rsa_key_filename()) - } - pub fn public_rsa_key(&self) -> String { - format!("{}.pub.pem", CONFIG.rsa_key_filename()) - } - pub fn mail_enabled(&self) -> bool { - let inner = &self.inner.read().unwrap().config; - inner._enable_smtp && (inner.smtp_host.is_some() || inner.use_sendmail) - } - /// Tests whether the admin token is set to a non-empty value. - pub fn is_admin_token_set(&self) -> bool { - let token = self.admin_token(); - token.is_some() && !token.unwrap().trim().is_empty() - } - - pub fn render_template<T: serde::ser::Serialize>( - &self, - name: &str, - data: &T, - ) -> Result<String, crate::error::Error> { - let hb = &CONFIG.inner.read().unwrap().templates; - hb.render(name, data).map_err(Into::into) - } - - pub fn set_rocket_shutdown_handle(&self, handle: rocket::Shutdown) { - self.inner.write().unwrap().rocket_shutdown_handle = Some(handle); - } - - pub fn set_ws_shutdown_handle(&self, handle: tokio::sync::oneshot::Sender<()>) { - self.inner.write().unwrap().ws_shutdown_handle = Some(handle); - } - - pub fn shutdown(&self) { - if let Ok(mut c) = self.inner.write() { - if let Some(handle) = c.ws_shutdown_handle.take() { - handle.send(()).ok(); - } - - if let Some(handle) = c.rocket_shutdown_handle.take() { - handle.notify(); + #[inline] + pub fn load() -> Result<Self, ConfigErr> { + let config_file = + toml::from_str::<ConfigFile>(fs::read_to_string("config.toml")?.as_str())?; + let mut tls = TlsConfig::from_paths(config_file.tls.cert, config_file.tls.key); + tls = match config_file.tls.ciphers { + Some(ciphers) => { + if ciphers.is_empty() { + tls + } else { + tls.with_ciphers(ciphers) + } } - } - } -} - -use handlebars::{ - Context, Handlebars, Helper, HelperResult, Output, RenderContext, RenderError, Renderable, -}; - -fn load_templates<P>(path: P) -> Handlebars<'static> -where - P: AsRef<std::path::Path>, -{ - let mut hb = Handlebars::new(); - // Error on missing params - hb.set_strict_mode(true); - // Register helpers - hb.register_helper("case", Box::new(case_helper)); - hb.register_helper("jsesc", Box::new(js_escape_helper)); - hb.register_helper("to_json", Box::new(to_json)); - - macro_rules! reg { - ($name:expr) => {{ - let template = include_str!(concat!("static/templates/", $name, ".hbs")); - hb.register_template_string($name, template).unwrap(); - }}; - ($name:expr, $ext:expr) => {{ - reg!($name); - reg!(concat!($name, $ext)); - }}; - } - - // First register default templates here - reg!("email/email_header"); - reg!("email/email_footer"); - reg!("email/email_footer_text"); - - reg!("email/admin_reset_password", ".html"); - reg!("email/change_email", ".html"); - reg!("email/delete_account", ".html"); - reg!("email/emergency_access_invite_accepted", ".html"); - reg!("email/emergency_access_invite_confirmed", ".html"); - reg!("email/emergency_access_recovery_approved", ".html"); - reg!("email/emergency_access_recovery_initiated", ".html"); - reg!("email/emergency_access_recovery_rejected", ".html"); - reg!("email/emergency_access_recovery_reminder", ".html"); - reg!("email/emergency_access_recovery_timed_out", ".html"); - reg!("email/incomplete_2fa_login", ".html"); - reg!("email/invite_accepted", ".html"); - reg!("email/invite_confirmed", ".html"); - reg!("email/new_device_logged_in", ".html"); - reg!("email/protected_action", ".html"); - reg!("email/pw_hint_none", ".html"); - reg!("email/pw_hint_some", ".html"); - reg!("email/send_2fa_removed_from_org", ".html"); - reg!("email/send_emergency_access_invite", ".html"); - reg!("email/send_org_invite", ".html"); - reg!("email/send_single_org_removed_from_org", ".html"); - reg!("email/smtp_test", ".html"); - reg!("email/twofactor_email", ".html"); - reg!("email/verify_email", ".html"); - reg!("email/welcome_must_verify", ".html"); - reg!("email/welcome", ".html"); - - reg!("admin/base"); - reg!("admin/login"); - reg!("admin/settings"); - reg!("admin/users"); - reg!("admin/organizations"); - reg!("admin/diagnostics"); - - reg!("404"); - - // And then load user templates to overwrite the defaults - // Use .hbs extension for the files - // Templates get registered with their relative name - hb.register_templates_directory(".hbs", path).unwrap(); - - hb -} - -fn case_helper<'reg, 'rc>( - h: &Helper<'reg, 'rc>, - r: &'reg Handlebars<'_>, - ctx: &'rc Context, - rc: &mut RenderContext<'reg, 'rc>, - out: &mut dyn Output, -) -> HelperResult { - let param = h - .param(0) - .ok_or_else(|| RenderError::new("Param not found for helper \"case\""))?; - let value = param.value().clone(); - - if h.params().iter().skip(1).any(|x| x.value() == &value) { - h.template() - .map(|t| t.render(r, ctx, rc, out)) - .unwrap_or_else(|| Ok(())) - } else { - Ok(()) + None => tls, + }; + tls = match config_file.tls.prefer_server_cipher_order { + Some(prefer) => tls.with_preferred_server_cipher_order(prefer), + None => tls, + }; + let mut tmp_folder = env::current_dir()?; + tmp_folder.push(Self::TMP_FOLDER); + let mut rocket = rocket::Config { + address: config_file.ip, + cli_colors: false, + limits: Limits::new() + .limit("json", 20.megabytes()) + .limit("data-form", 525.megabytes()) + .limit("file", 525.megabytes()), + log_level: LogLevel::Off, + port: config_file.port, + temp_dir: tmp_folder.into(), + tls: Some(tls), + ..Default::default() + }; + if let Some(count) = config_file.workers { + rocket.workers = count.get() as usize; + } + let domain = + Url::parse(format!("https://{}:{}", config_file.domain, config_file.port).as_str())?; + if domain.domain().is_none() { + return Err(ConfigErr::BadDomain); + } + Ok(Self { + database_max_conns: config_file + .database_max_conns + .unwrap_or(NonZeroU8::new(10).unwrap()), + database_timeout: config_file.database_timeout.unwrap_or(30), + db_connection_retries: config_file + .db_connection_retries + .unwrap_or(NonZeroU8::new(15).unwrap()), + domain, + org_attachment_limit: config_file.org_attachment_limit, + password_iterations: config_file.password_iterations.unwrap_or(600_000), + rocket, + user_attachment_limit: config_file.user_attachment_limit, + web_vault_enabled: config_file.web_vault_enabled.unwrap_or(true), + }) } } - -fn js_escape_helper<'reg, 'rc>( - h: &Helper<'reg, 'rc>, - _r: &'reg Handlebars<'_>, - _ctx: &'rc Context, - _rc: &mut RenderContext<'reg, 'rc>, - out: &mut dyn Output, -) -> HelperResult { - let param = h - .param(0) - .ok_or_else(|| RenderError::new("Param not found for helper \"jsesc\""))?; - - let no_quote = h.param(1).is_some(); - - let value = param - .value() - .as_str() - .ok_or_else(|| RenderError::new("Param for helper \"jsesc\" is not a String"))?; - - let mut escaped_value = value - .replace('\\', "") - .replace('\'', "\\x22") - .replace('\"', "\\x27"); - if !no_quote { - escaped_value = format!("&quot;{escaped_value}&quot;"); +impl Config { + pub const ATTACHMENTS_FOLDER: &str = "data/attachments"; + pub const DATA_FOLDER: &str = "data"; + pub const DATABASE_URL: &str = "data/db.sqlite3"; + pub const ICON_CACHE_FOLDER: &str = "data/icon_cache"; + pub const PRIVATE_RSA_KEY: &str = "data/rsa_key.pem"; + pub const PUBLIC_RSA_KEY: &str = "data/rsa_key.pub.pem"; + pub const SENDS_FOLDER: &str = "data/sends"; + pub const TMP_FOLDER: &str = "data/tmp"; + pub const WEB_VAULT_FOLDER: &str = "web-vault/"; + #[inline] + pub fn domain_origin(&self) -> String { + self.domain.origin().ascii_serialization() + } + #[inline] + pub fn domain_path(&self) -> &str { + self.domain.path().trim_end_matches('/') } - - out.write(&escaped_value)?; - Ok(()) -} - -fn to_json<'reg, 'rc>( - h: &Helper<'reg, 'rc>, - _r: &'reg Handlebars<'_>, - _ctx: &'rc Context, - _rc: &mut RenderContext<'reg, 'rc>, - out: &mut dyn Output, -) -> HelperResult { - let param = h - .param(0) - .ok_or_else(|| RenderError::new("Expected 1 parameter for \"to_json\""))? - .value(); - let json = serde_json::to_string(param) - .map_err(|e| RenderError::new(format!("Can't serialize parameter to JSON: {e}")))?; - out.write(&json)?; - Ok(()) } diff --git a/src/crypto.rs b/src/crypto.rs @@ -57,12 +57,6 @@ pub fn get_random_string(alphabet: &[u8], num_chars: usize) -> String { .collect() } -/// Generates a random numeric string. -pub fn get_random_string_numeric(num_chars: usize) -> String { - const ALPHABET: &[u8] = b"0123456789"; - get_random_string(ALPHABET, num_chars) -} - /// Generates a random alphanumeric string. pub fn get_random_string_alphanum(num_chars: usize) -> String { const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ\ @@ -75,21 +69,11 @@ pub fn generate_id<const N: usize>() -> String { encode_random_bytes::<N>(HEXLOWER) } -pub fn generate_send_id() -> String { - // Send IDs are globally scoped, so make them longer to avoid collisions. - generate_id::<32>() // 256 bits -} - pub fn generate_attachment_id() -> String { // Attachment IDs are scoped to a cipher, so they can be smaller. generate_id::<10>() // 80 bits } -/// Generates a numeric token for email-based verifications. -pub fn generate_email_token(token_size: u8) -> String { - get_random_string_numeric(token_size as usize) -} - /// Generates a personal API key. /// Upstream uses 30 chars, which is ~178 bits of entropy. pub fn generate_api_key() -> String { diff --git a/src/db/mod.rs b/src/db/mod.rs @@ -17,8 +17,8 @@ use tokio::{ }; use crate::{ + config::{self, Config}, error::{Error, MapResult}, - CONFIG, }; #[path = "schemas/sqlite/schema.rs"] @@ -114,7 +114,7 @@ macro_rules! generate_connections { impl DbPool { // For the given database URL, guess its type, run migrations, create pool, and return it pub fn from_config() -> Result<Self, Error> { - let url = CONFIG.database_url(); + let url = Config::DATABASE_URL; let conn_type = DbConnType::from_url(&url)?; match conn_type { $( @@ -122,10 +122,10 @@ macro_rules! generate_connections { #[cfg($name)] { paste::paste!{ [< $name _migrations >]::run_migrations()?; } - let manager = ConnectionManager::new(&url); + let manager = ConnectionManager::new(url); let pool = Pool::builder() - .max_size(CONFIG.database_max_conns()) - .connection_timeout(Duration::from_secs(CONFIG.database_timeout())) + .max_size(config::get_config().database_max_conns.get() as u32) + .connection_timeout(Duration::from_secs(config::get_config().database_timeout as u64)) .connection_customizer(Box::new(DbConnOptions{ init_stmts: conn_type.get_init_stmts() })) @@ -133,7 +133,7 @@ macro_rules! generate_connections { .map_res("Failed to create pool")?; Ok(DbPool { pool: Some(DbPoolInner::$name(pool)), - semaphore: Arc::new(Semaphore::new(CONFIG.database_max_conns() as usize)), + semaphore: Arc::new(Semaphore::new(config::get_config().database_max_conns.get() as usize)), }) } #[cfg(not($name))] @@ -143,7 +143,7 @@ macro_rules! generate_connections { } // Get a connection from the pool pub async fn get(&self) -> Result<DbConn, Error> { - let duration = Duration::from_secs(CONFIG.database_timeout()); + let duration = Duration::from_secs(config::get_config().database_timeout as u64); let permit = match timeout(duration, Arc::clone(&self.semaphore).acquire_owned()).await { Ok(p) => p.expect("Semaphore should be open"), Err(_) => { @@ -179,12 +179,7 @@ impl DbConnType { } pub fn get_init_stmts(&self) -> String { - let init_stmts = CONFIG.database_conn_init(); - if !init_stmts.is_empty() { - init_stmts - } else { - self.default_init_stmts() - } + self.default_init_stmts() } pub fn default_init_stmts(&self) -> String { @@ -337,11 +332,11 @@ mod sqlite_migrations { pub fn run_migrations() -> Result<(), super::Error> { use diesel::{Connection, RunQueryDsl}; - let url = crate::CONFIG.database_url(); + let url = crate::Config::DATABASE_URL; // Establish a connection to the sqlite database (this will create a new one, if it does // not exist, and exit if there is an error). - let mut connection = diesel::sqlite::SqliteConnection::establish(&url)?; + let mut connection = diesel::sqlite::SqliteConnection::establish(url)?; // Run the migrations after successfully establishing a connection // Disable Foreign Key Checks during migration @@ -350,12 +345,9 @@ mod sqlite_migrations { .execute(&mut connection) .expect("Failed to disable Foreign Key Checks during migrations"); - // Turn on WAL in SQLite - if crate::CONFIG.enable_db_wal() { - diesel::sql_query("PRAGMA journal_mode=wal") - .execute(&mut connection) - .expect("Failed to turn on WAL"); - } + diesel::sql_query("PRAGMA journal_mode=wal") + .execute(&mut connection) + .expect("Failed to turn on WAL"); connection .run_pending_migrations(MIGRATIONS) diff --git a/src/db/models/attachment.rs b/src/db/models/attachment.rs @@ -1,8 +1,6 @@ -use std::io::ErrorKind; - +use crate::config::Config; use serde_json::Value; - -use crate::CONFIG; +use std::io::ErrorKind; db_object! { #[derive(Identifiable, Queryable, Insertable, AsChangeset)] @@ -20,7 +18,13 @@ db_object! { /// Local methods impl Attachment { - pub const fn new(id: String, cipher_uuid: String, file_name: String, file_size: i32, akey: Option<String>) -> Self { + pub const fn new( + id: String, + cipher_uuid: String, + file_name: String, + file_size: i32, + akey: Option<String>, + ) -> Self { Self { id, cipher_uuid, @@ -31,12 +35,23 @@ impl Attachment { } pub fn get_file_path(&self) -> String { - format!("{}/{}/{}", CONFIG.attachments_folder(), self.cipher_uuid, self.id) + format!( + "{}/{}/{}", + Config::ATTACHMENTS_FOLDER, + self.cipher_uuid, + self.id + ) } pub fn get_url(&self, host: &str) -> String { - let token = encode_jwt(&generate_file_download_claims(self.cipher_uuid.clone(), self.id.clone())); - format!("{}/attachments/{}/{}?token={}", host, self.cipher_uuid, self.id, token) + let token = encode_jwt(&generate_file_download_claims( + self.cipher_uuid.clone(), + self.id.clone(), + )); + format!( + "{}/attachments/{}/{}?token={}", + host, self.cipher_uuid, self.id, token + ) } pub fn to_json(&self, host: &str) -> Value { @@ -192,7 +207,11 @@ impl Attachment { // This will return all attachments linked to the user or org // There is no filtering done here if the user actually has access! // It is used to speed up the sync process, and the matching is done in a different part. - pub async fn find_all_by_user_and_orgs(user_uuid: &str, org_uuids: &Vec<String>, conn: &mut DbConn) -> Vec<Self> { + pub async fn find_all_by_user_and_orgs( + user_uuid: &str, + org_uuids: &Vec<String>, + conn: &mut DbConn, + ) -> Vec<Self> { db_run! { conn: { attachments::table .left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid))) diff --git a/src/db/models/cipher.rs b/src/db/models/cipher.rs @@ -1,5 +1,4 @@ -use crate::CONFIG; -use chrono::{Duration, NaiveDateTime, Utc}; +use chrono::{NaiveDateTime, Utc}; use serde_json::Value; use super::{ @@ -363,17 +362,6 @@ impl Cipher { Ok(()) } - /// Purge all ciphers that are old enough to be auto-deleted. - pub async fn purge_trash(conn: &mut DbConn) { - if let Some(auto_delete_days) = CONFIG.trash_auto_delete_days() { - let now = Utc::now().naive_utc(); - let dt = now - Duration::days(auto_delete_days); - for cipher in Self::find_deleted_before(&dt, conn).await { - cipher.delete(conn).await.ok(); - } - } - } - pub async fn move_to_folder( &self, folder_uuid: Option<String>, diff --git a/src/db/models/device.rs b/src/db/models/device.rs @@ -1,6 +1,6 @@ use chrono::{NaiveDateTime, Utc}; -use crate::{crypto, CONFIG}; +use crate::crypto; use core::fmt; db_object! { @@ -75,23 +75,39 @@ impl Device { let time_now = Utc::now().naive_utc(); self.updated_at = time_now; - let orgowner: Vec<_> = orgs.iter().filter(|o| o.atype == 0).map(|o| o.org_uuid.clone()).collect(); - let orgadmin: Vec<_> = orgs.iter().filter(|o| o.atype == 1).map(|o| o.org_uuid.clone()).collect(); - let orguser: Vec<_> = orgs.iter().filter(|o| o.atype == 2).map(|o| o.org_uuid.clone()).collect(); - let orgmanager: Vec<_> = orgs.iter().filter(|o| o.atype == 3).map(|o| o.org_uuid.clone()).collect(); + let orgowner: Vec<_> = orgs + .iter() + .filter(|o| o.atype == 0) + .map(|o| o.org_uuid.clone()) + .collect(); + let orgadmin: Vec<_> = orgs + .iter() + .filter(|o| o.atype == 1) + .map(|o| o.org_uuid.clone()) + .collect(); + let orguser: Vec<_> = orgs + .iter() + .filter(|o| o.atype == 2) + .map(|o| o.org_uuid.clone()) + .collect(); + let orgmanager: Vec<_> = orgs + .iter() + .filter(|o| o.atype == 3) + .map(|o| o.org_uuid.clone()) + .collect(); // Create the JWT claims struct, to send to the client - use crate::auth::{encode_jwt, LoginJwtClaims, DEFAULT_VALIDITY, JWT_LOGIN_ISSUER}; + use crate::auth::{self, encode_jwt, LoginJwtClaims}; let claims = LoginJwtClaims { nbf: time_now.timestamp(), - exp: (time_now + *DEFAULT_VALIDITY).timestamp(), - iss: JWT_LOGIN_ISSUER.to_string(), + exp: (time_now + *auth::get_default_validity()).timestamp(), + iss: auth::get_jwt_login_issuer().to_string(), sub: user.uuid.clone(), premium: true, name: user.name.clone(), email: user.email.clone(), - email_verified: !CONFIG.mail_enabled() || user.verified_at.is_some(), + email_verified: true, orgowner, orgadmin, @@ -104,7 +120,10 @@ impl Device { amr: vec!["Application".into()], }; - (encode_jwt(&claims), DEFAULT_VALIDITY.num_seconds()) + ( + encode_jwt(&claims), + auth::get_default_validity().num_seconds(), + ) } } @@ -143,7 +162,11 @@ impl Device { }} } - pub async fn find_by_uuid_and_user(uuid: &str, user_uuid: &str, conn: &mut DbConn) -> Option<Self> { + pub async fn find_by_uuid_and_user( + uuid: &str, + user_uuid: &str, + conn: &mut DbConn, + ) -> Option<Self> { db_run! { conn: { devices::table .filter(devices::uuid.eq(uuid)) diff --git a/src/db/models/emergency_access.rs b/src/db/models/emergency_access.rs @@ -29,7 +29,13 @@ db_object! { /// Local methods impl EmergencyAccess { - pub fn new(grantor_uuid: String, email: String, status: i32, atype: i32, wait_time_days: i32) -> Self { + pub fn new( + grantor_uuid: String, + email: String, + status: i32, + atype: i32, + wait_time_days: i32, + ) -> Self { let now = Utc::now().naive_utc(); Self { @@ -67,7 +73,9 @@ impl EmergencyAccess { } pub async fn to_json_grantor_details(&self, conn: &mut DbConn) -> Value { - let grantor_user = User::find_by_uuid(&self.grantor_uuid, conn).await.expect("Grantor user not found."); + let grantor_user = User::find_by_uuid(&self.grantor_uuid, conn) + .await + .expect("Grantor user not found."); json!({ "Id": self.uuid, @@ -83,9 +91,17 @@ impl EmergencyAccess { pub async fn to_json_grantee_details(&self, conn: &mut DbConn) -> Value { let grantee_user = if let Some(grantee_uuid) = self.grantee_uuid.as_deref() { - Some(User::find_by_uuid(grantee_uuid, conn).await.expect("Grantee user not found.")) + Some( + User::find_by_uuid(grantee_uuid, conn) + .await + .expect("Grantee user not found."), + ) } else if let Some(email) = self.email.as_deref() { - Some(User::find_by_mail(email, conn).await.expect("Grantee user not found.")) + Some( + User::find_by_mail(email, conn) + .await + .expect("Grantee user not found."), + ) } else { None }; @@ -109,26 +125,12 @@ pub enum EmergencyAccessType { Takeover = 1, } -impl EmergencyAccessType { - pub fn from_str(s: &str) -> Option<Self> { - match s { - "0" | "View" => Some(EmergencyAccessType::View), - "1" | "Takeover" => Some(EmergencyAccessType::Takeover), - _ => None, - } - } -} - pub enum EmergencyAccessStatus { Invited = 0, - Accepted = 1, - Confirmed = 2, RecoveryInitiated = 3, RecoveryApproved = 4, } -// region Database methods - impl EmergencyAccess { pub async fn save(&mut self, conn: &mut DbConn) -> EmptyResult { User::update_uuid_revision(&self.grantor_uuid, conn).await; @@ -172,7 +174,11 @@ impl EmergencyAccess { conn: &mut DbConn, ) -> EmptyResult { // Update the grantee so that it will refresh it's status. - User::update_uuid_revision(self.grantee_uuid.as_ref().expect("Error getting grantee"), conn).await; + User::update_uuid_revision( + self.grantee_uuid.as_ref().expect("Error getting grantee"), + conn, + ) + .await; self.status = status; self.updated_at = date.to_owned(); @@ -257,7 +263,11 @@ impl EmergencyAccess { }} } - pub async fn find_by_uuid_and_grantor_uuid(uuid: &str, grantor_uuid: &str, conn: &mut DbConn) -> Option<Self> { + pub async fn find_by_uuid_and_grantor_uuid( + uuid: &str, + grantor_uuid: &str, + conn: &mut DbConn, + ) -> Option<Self> { db_run! { conn: { emergency_access::table .filter(emergency_access::uuid.eq(uuid)) @@ -275,7 +285,10 @@ impl EmergencyAccess { }} } - pub async fn find_invited_by_grantee_email(grantee_email: &str, conn: &mut DbConn) -> Option<Self> { + pub async fn find_invited_by_grantee_email( + grantee_email: &str, + conn: &mut DbConn, + ) -> Option<Self> { db_run! { conn: { emergency_access::table .filter(emergency_access::email.eq(grantee_email)) @@ -293,5 +306,3 @@ impl EmergencyAccess { }} } } - -// endregion diff --git a/src/db/models/event.rs b/src/db/models/event.rs @@ -1,12 +1,8 @@ use crate::db::DbConn; +use crate::{api::EmptyResult, error::MapResult}; +use chrono::{NaiveDateTime, Utc}; use serde_json::Value; -use crate::{api::EmptyResult, error::MapResult, CONFIG}; - -use chrono::{Duration, NaiveDateTime, Utc}; - -// https://bitwarden.com/help/event-logs/ - db_object! { // Upstream: https://github.com/bitwarden/server/blob/8a22c0479e987e756ce7412c48a732f9002f0a2d/src/Core/Services/Implementations/EventService.cs // Upstream: https://github.com/bitwarden/server/blob/8a22c0479e987e756ce7412c48a732f9002f0a2d/src/Api/Models/Public/Response/EventResponseModel.cs @@ -313,17 +309,4 @@ impl Event { .from_db() }} } - - pub async fn clean_events(conn: &mut DbConn) -> EmptyResult { - if let Some(days_to_retain) = CONFIG.events_days_retain() { - let dt = Utc::now().naive_utc() - Duration::days(days_to_retain); - db_run! { conn: { - diesel::delete(event::table.filter(event::event_date.lt(dt))) - .execute(conn) - .map_res("Error cleaning old events") - }} - } else { - Ok(()) - } - } } diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs @@ -4,7 +4,6 @@ use serde_json::Value; use std::cmp::Ordering; use super::{CollectionUser, Group, GroupUser, OrgPolicy, OrgPolicyType, TwoFactor, User}; -use crate::CONFIG; db_object! { #[derive(Identifiable, Queryable, Insertable, AsChangeset)] @@ -183,7 +182,7 @@ impl Organization { "SelfHost": true, "UseApi": true, "HasPublicAndPrivateKeys": self.private_key.is_some() && self.public_key.is_some(), - "UseResetPassword": CONFIG.mail_enabled(), + "UseResetPassword": false, "BusinessName": null, "BusinessAddress1": null, @@ -279,13 +278,6 @@ use crate::error::MapResult; /// Database methods impl Organization { pub async fn save(&self, conn: &mut DbConn) -> EmptyResult { - if !email_address::EmailAddress::is_valid(self.billing_email.trim()) { - err!(format!( - "BillingEmail {} is not a valid email address", - self.billing_email.trim() - )) - } - for user_org in UserOrganization::find_by_org(&self.uuid, conn).await.iter() { User::update_uuid_revision(&user_org.user_uuid, conn).await; } @@ -379,7 +371,7 @@ impl UserOrganization { "SelfHost": true, "HasPublicAndPrivateKeys": org.private_key.is_some() && org.public_key.is_some(), "ResetPasswordEnrolled": self.reset_password_key.is_some(), - "UseResetPassword": CONFIG.mail_enabled(), + "UseResetPassword": false, "SsoBound": false, // Not supported "UseSso": false, // Not supported "ProviderId": null, diff --git a/src/db/models/send.rs b/src/db/models/send.rs @@ -45,7 +45,13 @@ pub enum SendType { } impl Send { - pub fn new(atype: i32, name: String, data: String, akey: String, deletion_date: NaiveDateTime) -> Self { + pub fn new( + atype: i32, + name: String, + data: String, + akey: String, + deletion_date: NaiveDateTime, + ) -> Self { let now = Utc::now().naive_utc(); Self { @@ -82,7 +88,8 @@ impl Send { if let Some(password) = password { self.password_iter = Some(PASSWORD_ITER); let salt = crate::crypto::get_random_bytes::<64>().to_vec(); - let hash = crate::crypto::hash_password(password.as_bytes(), &salt, PASSWORD_ITER as u32); + let hash = + crate::crypto::hash_password(password.as_bytes(), &salt, PASSWORD_ITER as u32); self.password_salt = Some(salt); self.password_hash = Some(hash); } else { @@ -213,7 +220,10 @@ impl Send { self.update_users_revision(conn).await; if self.atype == SendType::File as i32 { - std::fs::remove_dir_all(std::path::Path::new(&crate::CONFIG.sends_folder()).join(&self.uuid)).ok(); + std::fs::remove_dir_all( + std::path::Path::new(&crate::config::Config::SENDS_FOLDER).join(&self.uuid), + ) + .ok(); } db_run! { conn: { diff --git a/src/db/models/two_factor_incomplete.rs b/src/db/models/two_factor_incomplete.rs @@ -1,6 +1,5 @@ -use chrono::{NaiveDateTime, Utc}; - -use crate::{api::EmptyResult, auth::ClientIp, db::DbConn, error::MapResult, CONFIG}; +use crate::{api::EmptyResult, db::DbConn, error::MapResult}; +use chrono::NaiveDateTime; db_object! { #[derive(Identifiable, Queryable, Insertable, AsChangeset)] @@ -19,48 +18,11 @@ db_object! { } impl TwoFactorIncomplete { - pub async fn mark_incomplete( + pub async fn find_by_user_and_device( user_uuid: &str, device_uuid: &str, - device_name: &str, - ip: &ClientIp, conn: &mut DbConn, - ) -> EmptyResult { - if CONFIG.incomplete_2fa_time_limit() <= 0 || !CONFIG.mail_enabled() { - return Ok(()); - } - - // Don't update the data for an existing user/device pair, since that - // would allow an attacker to arbitrarily delay notifications by - // sending repeated 2FA attempts to reset the timer. - let existing = Self::find_by_user_and_device(user_uuid, device_uuid, conn).await; - if existing.is_some() { - return Ok(()); - } - - db_run! { conn: { - diesel::insert_into(twofactor_incomplete::table) - .values(( - twofactor_incomplete::user_uuid.eq(user_uuid), - twofactor_incomplete::device_uuid.eq(device_uuid), - twofactor_incomplete::device_name.eq(device_name), - twofactor_incomplete::login_time.eq(Utc::now().naive_utc()), - twofactor_incomplete::ip_address.eq(ip.ip.to_string()), - )) - .execute(conn) - .map_res("Error adding twofactor_incomplete record") - }} - } - - pub async fn mark_complete(user_uuid: &str, device_uuid: &str, conn: &mut DbConn) -> EmptyResult { - if CONFIG.incomplete_2fa_time_limit() <= 0 || !CONFIG.mail_enabled() { - return Ok(()); - } - - Self::delete_by_user_and_device(user_uuid, device_uuid, conn).await - } - - pub async fn find_by_user_and_device(user_uuid: &str, device_uuid: &str, conn: &mut DbConn) -> Option<Self> { + ) -> Option<Self> { db_run! { conn: { twofactor_incomplete::table .filter(twofactor_incomplete::user_uuid.eq(user_uuid)) @@ -85,7 +47,11 @@ impl TwoFactorIncomplete { Self::delete_by_user_and_device(&self.user_uuid, &self.device_uuid, conn).await } - pub async fn delete_by_user_and_device(user_uuid: &str, device_uuid: &str, conn: &mut DbConn) -> EmptyResult { + pub async fn delete_by_user_and_device( + user_uuid: &str, + device_uuid: &str, + conn: &mut DbConn, + ) -> EmptyResult { db_run! { conn: { diesel::delete(twofactor_incomplete::table .filter(twofactor_incomplete::user_uuid.eq(user_uuid)) diff --git a/src/db/models/user.rs b/src/db/models/user.rs @@ -1,8 +1,8 @@ use chrono::{Duration, NaiveDateTime, Utc}; use serde_json::Value; +use crate::config; use crate::crypto; -use crate::CONFIG; db_object! { #[derive(Identifiable, Queryable, Insertable, AsChangeset)] @@ -105,7 +105,7 @@ impl User { password_hash: Vec::new(), salt: crypto::get_random_bytes::<64>().to_vec(), - password_iterations: CONFIG.password_iterations(), + password_iterations: config::get_config().password_iterations, security_stamp: crate::util::get_uuid(), stamp_exception: None, @@ -172,7 +172,11 @@ impl User { reset_security_stamp: bool, allow_next_route: Option<Vec<String>>, ) { - self.password_hash = crypto::hash_password(password.as_bytes(), &self.salt, self.password_iterations as u32); + self.password_hash = crypto::hash_password( + password.as_bytes(), + &self.salt, + self.password_iterations as u32, + ); if let Some(route) = allow_next_route { self.set_stamp_exception(route); @@ -214,8 +218,8 @@ impl User { } use super::{ - Cipher, Device, EmergencyAccess, Favorite, Folder, Send, TwoFactor, TwoFactorIncomplete, UserOrgType, - UserOrganization, + Cipher, Device, EmergencyAccess, Favorite, Folder, Send, TwoFactor, TwoFactorIncomplete, + UserOrgType, UserOrganization, }; use crate::db::DbConn; @@ -244,7 +248,7 @@ impl User { "Id": self.uuid, "Name": self.name, "Email": self.email, - "EmailVerified": !CONFIG.mail_enabled() || self.verified_at.is_some(), + "EmailVerified": true, "Premium": true, "MasterPasswordHint": self.password_hint, "Culture": "en-US", @@ -302,7 +306,12 @@ impl User { pub async fn delete(self, conn: &mut DbConn) -> EmptyResult { for user_org in UserOrganization::find_confirmed_by_user(&self.uuid, conn).await { if user_org.atype == UserOrgType::Owner - && UserOrganization::count_confirmed_by_org_and_type(&user_org.org_uuid, UserOrgType::Owner, conn).await + && UserOrganization::count_confirmed_by_org_and_type( + &user_org.org_uuid, + UserOrgType::Owner, + conn, + ) + .await <= 1 { err!("Can't delete last owner") @@ -397,9 +406,7 @@ impl User { impl Invitation { pub fn new(email: &str) -> Self { let email = email.to_lowercase(); - Self { - email, - } + Self { email } } pub async fn save(&self, conn: &mut DbConn) -> EmptyResult { diff --git a/src/error.rs b/src/error.rs @@ -41,11 +41,7 @@ use core::convert::Infallible; use diesel::r2d2::PoolError as R2d2Err; use diesel::result::Error as DieselErr; use diesel::ConnectionError as DieselConErr; -use handlebars::RenderError as HbErr; use jsonwebtoken::errors::Error as JwtErr; -use lettre::address::AddressError as AddrErr; -use lettre::error::Error as LettreErr; -use lettre::transport::smtp::Error as SmtpErr; use openssl::error::ErrorStack as SSLErr; #[cfg(all(feature = "priv_sep", target_os = "openbsd"))] use priv_sep::UnveilErr; @@ -77,15 +73,11 @@ make_error! { R2d2(R2d2Err): _has_source, _api_error, Serde(SerdeErr): _has_source, _api_error, JWt(JwtErr): _has_source, _api_error, - Handlebars(HbErr): _has_source, _api_error, Io(IoErr): _has_source, _api_error, Time(TimeErr): _has_source, _api_error, Regex(RegexErr): _has_source, _api_error, - Lettre(LettreErr): _has_source, _api_error, - Address(AddrErr): _has_source, _api_error, - Smtp(SmtpErr): _has_source, _api_error, OpenSSL(SSLErr): _has_source, _api_error, Rocket(RocketErr): _has_source, _api_error, @@ -110,15 +102,11 @@ make_error! { R2d2(R2d2Err): _has_source, _api_error, Serde(SerdeErr): _has_source, _api_error, JWt(JwtErr): _has_source, _api_error, - Handlebars(HbErr): _has_source, _api_error, Io(IoErr): _has_source, _api_error, Time(TimeErr): _has_source, _api_error, Regex(RegexErr): _has_source, _api_error, - Lettre(LettreErr): _has_source, _api_error, - Address(AddrErr): _has_source, _api_error, - Smtp(SmtpErr): _has_source, _api_error, OpenSSL(SSLErr): _has_source, _api_error, Rocket(RocketErr): _has_source, _api_error, Unveil(UnveilErr): _has_source, _api_error, diff --git a/src/mail.rs b/src/mail.rs @@ -1,596 +0,0 @@ -use std::str::FromStr; - -use chrono::NaiveDateTime; -use percent_encoding::{percent_encode, NON_ALPHANUMERIC}; - -use lettre::{ - message::{Attachment, Body, Mailbox, Message, MultiPart, SinglePart}, - transport::smtp::authentication::{Credentials, Mechanism as SmtpAuthMechanism}, - transport::smtp::client::{Tls, TlsParameters}, - transport::smtp::extension::ClientId, - Address, AsyncSendmailTransport, AsyncSmtpTransport, AsyncTransport, Tokio1Executor, -}; - -use crate::{ - api::EmptyResult, - auth::{ - encode_jwt, generate_delete_claims, generate_emergency_access_invite_claims, - generate_invite_claims, generate_verify_email_claims, - }, - error::Error, - CONFIG, -}; - -fn sendmail_transport() -> AsyncSendmailTransport<Tokio1Executor> { - if let Some(command) = CONFIG.sendmail_command() { - AsyncSendmailTransport::new_with_command(command) - } else { - AsyncSendmailTransport::new() - } -} - -fn smtp_transport() -> AsyncSmtpTransport<Tokio1Executor> { - use std::time::Duration; - let host = CONFIG.smtp_host().unwrap(); - - let smtp_client = AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous(host.as_str()) - .port(CONFIG.smtp_port()) - .timeout(Some(Duration::from_secs(CONFIG.smtp_timeout()))); - - // Determine security - let smtp_client = if CONFIG.smtp_security() != *"off" { - let mut tls_parameters = TlsParameters::builder(host); - if CONFIG.smtp_accept_invalid_hostnames() { - tls_parameters = tls_parameters.dangerous_accept_invalid_hostnames(true); - } - if CONFIG.smtp_accept_invalid_certs() { - tls_parameters = tls_parameters.dangerous_accept_invalid_certs(true); - } - let tls_parameters = tls_parameters.build().unwrap(); - - if CONFIG.smtp_security() == *"force_tls" { - smtp_client.tls(Tls::Wrapper(tls_parameters)) - } else { - smtp_client.tls(Tls::Required(tls_parameters)) - } - } else { - smtp_client - }; - - let smtp_client = match (CONFIG.smtp_username(), CONFIG.smtp_password()) { - (Some(user), Some(pass)) => smtp_client.credentials(Credentials::new(user, pass)), - _ => smtp_client, - }; - - let smtp_client = match CONFIG.helo_name() { - Some(helo_name) => smtp_client.hello_name(ClientId::Domain(helo_name)), - None => smtp_client, - }; - - let smtp_client = match CONFIG.smtp_auth_mechanism() { - Some(mechanism) => { - let allowed_mechanisms = [ - SmtpAuthMechanism::Plain, - SmtpAuthMechanism::Login, - SmtpAuthMechanism::Xoauth2, - ]; - let mut selected_mechanisms = vec![]; - for wanted_mechanism in mechanism.split(',') { - for m in &allowed_mechanisms { - if m.to_string().to_lowercase() - == wanted_mechanism - .trim_matches(|c| c == '"' || c == '\'' || c == ' ') - .to_lowercase() - { - selected_mechanisms.push(*m); - } - } - } - - if !selected_mechanisms.is_empty() { - smtp_client.authentication(selected_mechanisms) - } else { - // Only show a warning, and return without setting an actual authentication mechanism - warn!( - "No valid SMTP Auth mechanism found for '{}', using default values", - mechanism - ); - smtp_client - } - } - _ => smtp_client, - }; - - smtp_client.build() -} - -fn get_text( - template_name: &'static str, - data: serde_json::Value, -) -> Result<(String, String, String), Error> { - let (subject_html, body_html) = get_template(&format!("{template_name}.html"), &data)?; - let (_subject_text, body_text) = get_template(template_name, &data)?; - Ok((subject_html, body_html, body_text)) -} - -fn get_template(template_name: &str, data: &serde_json::Value) -> Result<(String, String), Error> { - let text = CONFIG.render_template(template_name, data)?; - let mut text_split = text.split("<!---------------->"); - - let subject = match text_split.next() { - Some(s) => s.trim().to_string(), - None => err!("Template doesn't contain subject"), - }; - - let body = match text_split.next() { - Some(s) => s.trim().to_string(), - None => err!("Template doesn't contain body"), - }; - - Ok((subject, body)) -} - -pub async fn send_password_hint(address: &str, hint: Option<String>) -> EmptyResult { - let template_name = if hint.is_some() { - "email/pw_hint_some" - } else { - "email/pw_hint_none" - }; - - let (subject, body_html, body_text) = get_text( - template_name, - json!({ - "url": CONFIG.domain(), - "img_src": CONFIG._smtp_img_src(), - "hint": hint, - }), - )?; - - send_email(address, &subject, body_html, body_text).await -} - -pub async fn send_delete_account(address: &str, uuid: &str) -> EmptyResult { - let claims = generate_delete_claims(uuid.to_string()); - let delete_token = encode_jwt(&claims); - - let (subject, body_html, body_text) = get_text( - "email/delete_account", - json!({ - "url": CONFIG.domain(), - "img_src": CONFIG._smtp_img_src(), - "user_id": uuid, - "email": percent_encode(address.as_bytes(), NON_ALPHANUMERIC).to_string(), - "token": delete_token, - }), - )?; - - send_email(address, &subject, body_html, body_text).await -} - -pub async fn send_verify_email(address: &str, uuid: &str) -> EmptyResult { - let claims = generate_verify_email_claims(uuid.to_string()); - let verify_email_token = encode_jwt(&claims); - - let (subject, body_html, body_text) = get_text( - "email/verify_email", - json!({ - "url": CONFIG.domain(), - "img_src": CONFIG._smtp_img_src(), - "user_id": uuid, - "email": percent_encode(address.as_bytes(), NON_ALPHANUMERIC).to_string(), - "token": verify_email_token, - }), - )?; - - send_email(address, &subject, body_html, body_text).await -} - -pub async fn send_welcome(address: &str) -> EmptyResult { - let (subject, body_html, body_text) = get_text( - "email/welcome", - json!({ - "url": CONFIG.domain(), - "img_src": CONFIG._smtp_img_src(), - }), - )?; - - send_email(address, &subject, body_html, body_text).await -} - -pub async fn send_welcome_must_verify(address: &str, uuid: &str) -> EmptyResult { - let claims = generate_verify_email_claims(uuid.to_string()); - let verify_email_token = encode_jwt(&claims); - - let (subject, body_html, body_text) = get_text( - "email/welcome_must_verify", - json!({ - "url": CONFIG.domain(), - "img_src": CONFIG._smtp_img_src(), - "user_id": uuid, - "token": verify_email_token, - }), - )?; - - send_email(address, &subject, body_html, body_text).await -} - -pub async 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(), - "img_src": CONFIG._smtp_img_src(), - "org_name": org_name, - }), - )?; - - send_email(address, &subject, body_html, body_text).await -} - -pub async fn send_single_org_removed_from_org(address: &str, org_name: &str) -> EmptyResult { - let (subject, body_html, body_text) = get_text( - "email/send_single_org_removed_from_org", - json!({ - "url": CONFIG.domain(), - "img_src": CONFIG._smtp_img_src(), - "org_name": org_name, - }), - )?; - - send_email(address, &subject, body_html, body_text).await -} - -pub async fn send_invite( - address: &str, - uuid: &str, - org_id: Option<String>, - org_user_id: Option<String>, - org_name: &str, - invited_by_email: Option<String>, -) -> EmptyResult { - let claims = generate_invite_claims( - uuid.to_string(), - String::from(address), - org_id.clone(), - org_user_id.clone(), - invited_by_email, - ); - let invite_token = encode_jwt(&claims); - - let (subject, body_html, body_text) = get_text( - "email/send_org_invite", - json!({ - "url": CONFIG.domain(), - "img_src": CONFIG._smtp_img_src(), - "org_id": org_id.as_deref().unwrap_or("_"), - "org_user_id": org_user_id.as_deref().unwrap_or("_"), - "email": percent_encode(address.as_bytes(), NON_ALPHANUMERIC).to_string(), - "org_name_encoded": percent_encode(org_name.as_bytes(), NON_ALPHANUMERIC).to_string(), - "org_name": org_name, - "token": invite_token, - }), - )?; - - send_email(address, &subject, body_html, body_text).await -} - -pub async fn send_emergency_access_invite( - address: &str, - uuid: &str, - emer_id: &str, - grantor_name: &str, - grantor_email: &str, -) -> EmptyResult { - let claims = generate_emergency_access_invite_claims( - String::from(uuid), - String::from(address), - String::from(emer_id), - String::from(grantor_name), - String::from(grantor_email), - ); - - let invite_token = encode_jwt(&claims); - - let (subject, body_html, body_text) = get_text( - "email/send_emergency_access_invite", - json!({ - "url": CONFIG.domain(), - "img_src": CONFIG._smtp_img_src(), - "emer_id": emer_id, - "email": percent_encode(address.as_bytes(), NON_ALPHANUMERIC).to_string(), - "grantor_name": grantor_name, - "token": invite_token, - }), - )?; - - send_email(address, &subject, body_html, body_text).await -} - -pub async fn send_emergency_access_invite_accepted( - address: &str, - grantee_email: &str, -) -> EmptyResult { - let (subject, body_html, body_text) = get_text( - "email/emergency_access_invite_accepted", - json!({ - "url": CONFIG.domain(), - "img_src": CONFIG._smtp_img_src(), - "grantee_email": grantee_email, - }), - )?; - - send_email(address, &subject, body_html, body_text).await -} - -pub async fn send_emergency_access_invite_confirmed( - address: &str, - grantor_name: &str, -) -> EmptyResult { - let (subject, body_html, body_text) = get_text( - "email/emergency_access_invite_confirmed", - json!({ - "url": CONFIG.domain(), - "img_src": CONFIG._smtp_img_src(), - "grantor_name": grantor_name, - }), - )?; - - send_email(address, &subject, body_html, body_text).await -} - -pub async fn send_emergency_access_recovery_approved( - address: &str, - grantor_name: &str, -) -> EmptyResult { - let (subject, body_html, body_text) = get_text( - "email/emergency_access_recovery_approved", - json!({ - "url": CONFIG.domain(), - "img_src": CONFIG._smtp_img_src(), - "grantor_name": grantor_name, - }), - )?; - - send_email(address, &subject, body_html, body_text).await -} - -pub async fn send_emergency_access_recovery_initiated( - address: &str, - grantee_name: &str, - atype: &str, - wait_time_days: &i32, -) -> EmptyResult { - let (subject, body_html, body_text) = get_text( - "email/emergency_access_recovery_initiated", - json!({ - "url": CONFIG.domain(), - "img_src": CONFIG._smtp_img_src(), - "grantee_name": grantee_name, - "atype": atype, - "wait_time_days": wait_time_days, - }), - )?; - - send_email(address, &subject, body_html, body_text).await -} - -pub async fn send_emergency_access_recovery_rejected( - address: &str, - grantor_name: &str, -) -> EmptyResult { - let (subject, body_html, body_text) = get_text( - "email/emergency_access_recovery_rejected", - json!({ - "url": CONFIG.domain(), - "img_src": CONFIG._smtp_img_src(), - "grantor_name": grantor_name, - }), - )?; - - send_email(address, &subject, body_html, body_text).await -} - -pub async fn send_invite_accepted( - new_user_email: &str, - address: &str, - org_name: &str, -) -> EmptyResult { - let (subject, body_html, body_text) = get_text( - "email/invite_accepted", - json!({ - "url": CONFIG.domain(), - "img_src": CONFIG._smtp_img_src(), - "email": new_user_email, - "org_name": org_name, - }), - )?; - - send_email(address, &subject, body_html, body_text).await -} - -pub async fn send_invite_confirmed(address: &str, org_name: &str) -> EmptyResult { - let (subject, body_html, body_text) = get_text( - "email/invite_confirmed", - json!({ - "url": CONFIG.domain(), - "img_src": CONFIG._smtp_img_src(), - "org_name": org_name, - }), - )?; - - send_email(address, &subject, body_html, body_text).await -} - -pub async fn send_new_device_logged_in( - address: &str, - ip: &str, - dt: &NaiveDateTime, - device: &str, -) -> EmptyResult { - use crate::util::upcase_first; - let device = upcase_first(device); - - let fmt = "%A, %B %_d, %Y at %r %Z"; - let (subject, body_html, body_text) = get_text( - "email/new_device_logged_in", - json!({ - "url": CONFIG.domain(), - "img_src": CONFIG._smtp_img_src(), - "ip": ip, - "device": device, - "datetime": crate::util::format_naive_datetime_local(dt, fmt), - }), - )?; - - send_email(address, &subject, body_html, body_text).await -} -pub async fn send_change_email(address: &str, token: &str) -> EmptyResult { - let (subject, body_html, body_text) = get_text( - "email/change_email", - json!({ - "url": CONFIG.domain(), - "img_src": CONFIG._smtp_img_src(), - "token": token, - }), - )?; - - send_email(address, &subject, body_html, body_text).await -} - -pub async fn send_admin_reset_password( - address: &str, - user_name: &str, - org_name: &str, -) -> EmptyResult { - let (subject, body_html, body_text) = get_text( - "email/admin_reset_password", - json!({ - "url": CONFIG.domain(), - "img_src": CONFIG._smtp_img_src(), - "user_name": user_name, - "org_name": org_name, - }), - )?; - send_email(address, &subject, body_html, body_text).await -} - -pub async fn send_protected_action_token(address: &str, token: &str) -> EmptyResult { - let (subject, body_html, body_text) = get_text( - "email/protected_action", - json!({ - "url": CONFIG.domain(), - "img_src": CONFIG._smtp_img_src(), - "token": token, - }), - )?; - - send_email(address, &subject, body_html, body_text).await -} - -async fn send_with_selected_transport(email: Message) -> EmptyResult { - if CONFIG.use_sendmail() { - match sendmail_transport().send(email).await { - Ok(_) => Ok(()), - // Match some common errors and make them more user friendly - Err(e) => { - if e.is_client() { - debug!("Sendmail client error: {:#?}", e); - err!(format!("Sendmail client error: {e}")); - } else if e.is_response() { - debug!("Sendmail response error: {:#?}", e); - err!(format!("Sendmail response error: {e}")); - } else { - debug!("Sendmail error: {:#?}", e); - err!(format!("Sendmail error: {e}")); - } - } - } - } else { - match smtp_transport().send(email).await { - Ok(_) => Ok(()), - // Match some common errors and make them more user friendly - Err(e) => { - if e.is_client() { - debug!("SMTP client error: {:#?}", e); - err!(format!("SMTP client error: {e}")); - } else if e.is_transient() { - debug!("SMTP 4xx error: {:#?}", e); - err!(format!("SMTP 4xx error: {e}")); - } else if e.is_permanent() { - debug!("SMTP 5xx error: {:#?}", e); - let mut msg = e.to_string(); - // Add a special check for 535 to add a more descriptive message - if msg.contains("(535)") { - msg = format!("{msg} - Authentication credentials invalid"); - } - err!(format!("SMTP 5xx error: {msg}")); - } else if e.is_timeout() { - debug!("SMTP timeout error: {:#?}", e); - err!(format!("SMTP timeout error: {e}")); - } else if e.is_tls() { - debug!("SMTP encryption error: {:#?}", e); - err!(format!("SMTP encryption error: {e}")); - } else { - debug!("SMTP error: {:#?}", e); - err!(format!("SMTP error: {e}")); - } - } - } - } -} - -async fn send_email( - address: &str, - subject: &str, - body_html: String, - body_text: String, -) -> EmptyResult { - let smtp_from = &CONFIG.smtp_from(); - - let body = if CONFIG.smtp_embed_images() { - let logo_gray_body = Body::new( - crate::api::static_files("logo-gray.png") - .unwrap() - .1 - .to_vec(), - ); - let mail_github_body = Body::new( - crate::api::static_files("mail-github.png") - .unwrap() - .1 - .to_vec(), - ); - MultiPart::alternative() - .singlepart(SinglePart::plain(body_text)) - .multipart( - MultiPart::related() - .singlepart(SinglePart::html(body_html)) - .singlepart( - Attachment::new_inline(String::from("logo-gray.png")) - .body(logo_gray_body, "image/png".parse().unwrap()), - ) - .singlepart( - Attachment::new_inline(String::from("mail-github.png")) - .body(mail_github_body, "image/png".parse().unwrap()), - ), - ) - } else { - MultiPart::alternative_plain_html(body_text, body_html) - }; - - let email = Message::builder() - .message_id(Some(format!( - "<{}@{}>", - crate::util::get_uuid(), - smtp_from.split('@').collect::<Vec<&str>>()[1] - ))) - .to(Mailbox::new(None, Address::from_str(address)?)) - .from(Mailbox::new( - Some(CONFIG.smtp_from_name()), - Address::from_str(smtp_from)?, - )) - .subject(subject) - .multipart(body)?; - - send_with_selected_transport(email).await -} diff --git a/src/main.rs b/src/main.rs @@ -47,11 +47,7 @@ extern crate diesel; #[macro_use] extern crate diesel_migrations; -use std::{ - fs::{canonicalize, create_dir_all}, - path::Path, - process::exit, -}; +use std::{fs::create_dir_all, path::Path, process::exit}; #[macro_use] mod error; mod api; @@ -60,32 +56,28 @@ mod config; mod crypto; #[macro_use] mod db; -mod mail; mod priv_sep; -mod ratelimit; mod util; -pub use config::CONFIG; +use config::Config; pub use error::{Error, MapResult}; -use rocket::data::{Limits, ToByteUnit}; use std::env; -use std::path::PathBuf; use std::sync::Arc; use tokio::runtime::Builder; fn main() -> Result<(), Error> { let mut promises = priv_sep::pledge_init()?; - launch_info(); - let cur_dir = env::current_dir()?; + let mut cur_dir = env::current_dir()?; priv_sep::unveil_read(cur_dir.as_path())?; static_init(); - validate_config(cur_dir)?; + cur_dir.push(Config::DATA_FOLDER); + unveil_create_read_write(cur_dir)?; check_data_folder(); check_rsa_keys().expect("error creating keys"); check_web_vault(); - create_dir(&CONFIG.icon_cache_folder(), "icon cache"); - create_dir(&CONFIG.tmp_folder(), "tmp folder"); - create_dir(&CONFIG.sends_folder(), "sends folder"); - create_dir(&CONFIG.attachments_folder(), "attachments folder"); + create_dir(Config::ICON_CACHE_FOLDER, "icon cache"); + create_dir(Config::TMP_FOLDER, "tmp folder"); + create_dir(Config::SENDS_FOLDER, "sends folder"); + create_dir(Config::ATTACHMENTS_FOLDER, "attachments folder"); Builder::new_multi_thread() .enable_all() .build() @@ -93,87 +85,32 @@ fn main() -> Result<(), Error> { |e| Err(Error::from(e)), |runtime| { runtime.block_on(async { - let config = rocket::Config::from(rocket::Config::figment()); - config.tls.as_ref().map_or(Ok(()), |tls| { - tls.certs().left().map_or(Ok(()), |certs| { - priv_sep::unveil_read(certs).and_then(|()| { - tls.key().left().map_or(Ok(()), priv_sep::unveil_read) + config::get_config() + .rocket + .tls + .as_ref() + .map_or(Ok(()), |tls| { + tls.certs().left().map_or(Ok(()), |certs| { + priv_sep::unveil_read(certs).and_then(|()| { + tls.key().left().map_or(Ok(()), priv_sep::unveil_read) + }) }) - }) - })?; + })?; priv_sep::pledge_away_unveil(&mut promises)?; - launch_rocket(create_db_pool().await, config).await + launch_rocket(create_db_pool().await).await }) }, ) } #[inline] fn static_init() { + config::init_config(); + auth::init_values(); api::init_ws_users(); api::init_ws_anonymous_subscriptions(); - ratelimit::init_limiter(); -} -#[inline] -fn validate_config(mut path: PathBuf) -> Result<(), Error> { - if CONFIG.job_poll_interval_ms() > 0 { - err!("'JOB_POLL_INTERVAL_MS=0' must be set in the config file") - } else if CONFIG.extended_logging() { - err!("'EXTENDED_LOGGING=false' must be set in the config file") - } else if CONFIG._enable_yubico() { - err!("'_ENABLE_YUBICO=false' must be set in the config file") - } else if CONFIG._enable_duo() { - err!("'_ENABLE_DUO=false' must be set in the config file") - } else if CONFIG._enable_email_2fa() { - err!("'_ENABLE_EMAIL_2FA=false' must be set in the config file") - } else if CONFIG.is_admin_token_set() { - err!("'ADMIN_TOKEN' must not exist in the config file or be empty") - } else if !CONFIG.disable_icon_download() { - err!("'DISABLE_ICON_DOWNLOAD=true' must be set in the config file") - } else if CONFIG.org_events_enabled() { - err!("'ORG_EVENTS_ENABLED=false' must be set in the config file") - } else if CONFIG.org_groups_enabled() { - err!("'ORG_GROUPS_ENABLED=false' must be set in the config file") - } else if !CONFIG.log_level().eq_ignore_ascii_case("OFF") { - err!("'LOG_LEVEL=OFF' must be set in the config file") - } else if CONFIG.use_syslog() { - err!("'USE_SYSLOG=false' must be set in the config file") - } else if CONFIG.log_file().is_some() { - err!("'LOG_FILE' must not exist in the config file") - } else if !CONFIG.disable_2fa_remember() { - err!("'DISABLE_2FA_REMEMBER=true' must be set in the config file") - } else { - let data_folder = PathBuf::from(CONFIG.data_folder()); - if data_folder.is_absolute() { - unveil_create_read_write(data_folder).map_err(Error::from) - } else { - path.push(data_folder); - unveil_create_read_write(path).map_err(Error::from) - } - } } pub const VERSION: &str = env!("CARGO_PKG_VERSION"); -fn launch_info() { - println!( - "\ - /--------------------------------------------------------------------\\\n\ - | Starting Vaultwarden |" - ); - println!("|{:^68}|", format!("Version {VERSION}")); - println!( - "\ - |--------------------------------------------------------------------|\n\ - | This is an *unofficial* Bitwarden implementation, DO NOT use the |\n\ - | official channels to report bugs/features, regardless of client. |\n\ - | Send usage/configuration questions or feature requests to: |\n\ - | https://github.com/dani-garcia/vaultwarden/discussions or |\n\ - | https://vaultwarden.discourse.group/ |\n\ - | Report suspected bugs/issues in the software itself at: |\n\ - | https://github.com/dani-garcia/vaultwarden/issues/new |\n\ - \\--------------------------------------------------------------------/\n" - ); -} - fn create_dir(path: &str, description: &str) { // Try to create the specified dir, if it doesn't already exist. let err_msg = format!("Error creating {description} directory '{path}'"); @@ -181,7 +118,7 @@ fn create_dir(path: &str, description: &str) { } fn check_data_folder() { - let data_folder = &CONFIG.data_folder(); + let data_folder = Config::DATA_FOLDER; let path = Path::new(data_folder); if !path.exists() { exit(1); @@ -192,41 +129,39 @@ fn check_data_folder() { } fn check_rsa_keys() -> Result<(), crate::error::Error> { // If the RSA keys don't exist, try to create them - let priv_path = CONFIG.private_rsa_key(); - let pub_path = CONFIG.public_rsa_key(); - - if !util::file_exists(&priv_path) { + let priv_path = Config::PRIVATE_RSA_KEY; + let pub_path = Config::PUBLIC_RSA_KEY; + if !util::file_exists(priv_path) { let rsa_key = openssl::rsa::Rsa::generate(2048)?; - let priv_key = rsa_key.private_key_to_pem()?; - crate::util::write_file(&priv_path, &priv_key)?; + crate::util::write_file(priv_path, priv_key.as_slice())?; } - - if !util::file_exists(&pub_path) { - let rsa_key = openssl::rsa::Rsa::private_key_from_pem(&std::fs::read(&priv_path)?)?; - + if !util::file_exists(pub_path) { + let rsa_key = openssl::rsa::Rsa::private_key_from_pem(&std::fs::read(priv_path)?)?; let pub_key = rsa_key.public_key_to_pem()?; - crate::util::write_file(&pub_path, &pub_key)?; + crate::util::write_file(pub_path, pub_key.as_slice())?; } - - auth::load_keys(); + auth::init_rsa_keys(); Ok(()) } fn check_web_vault() { - if !CONFIG.web_vault_enabled() { + if !config::get_config().web_vault_enabled { return; } - - let index_path = Path::new(&CONFIG.web_vault_folder()).join("index.html"); - + let index_path = Path::new(Config::WEB_VAULT_FOLDER).join("index.html"); if !index_path.exists() { exit(1); } } - +#[allow(clippy::cast_lossless)] async fn create_db_pool() -> db::DbPool { - match util::retry_db(db::DbPool::from_config, CONFIG.db_connection_retries()).await { + match util::retry_db( + db::DbPool::from_config, + config::get_config().db_connection_retries.get() as u32, + ) + .await + { Ok(p) => p, Err(_) => { exit(1); @@ -234,18 +169,9 @@ async fn create_db_pool() -> db::DbPool { } } -async fn launch_rocket(pool: db::DbPool, mut config: rocket::Config) -> Result<(), Error> { - let basepath = &CONFIG.domain_path(); - config.temp_dir = canonicalize(CONFIG.tmp_folder()).unwrap().into(); - config.cli_colors = false; // Make sure Rocket does not color any values for logging. - config.limits = Limits::new() - .limit("json", 20.megabytes()) // 20MB should be enough for very large imports, something like 5000+ vault entries - .limit("data-form", 525.megabytes()) // This needs to match the maximum allowed file size for Send - .limit("file", 525.megabytes()); // This needs to match the maximum allowed file size for attachments - - // If adding more paths here, consider also adding them to - // crate::utils::LOGGED_ROUTES to make sure they appear in the log - let instance = rocket::custom(config) +async fn launch_rocket(pool: db::DbPool) -> Result<(), Error> { + let basepath = config::get_config().domain_path(); + let instance = rocket::custom(&config::get_config().rocket) .mount([basepath, "/"].concat(), api::web_routes()) .mount([basepath, "/api"].concat(), api::core_routes()) .mount([basepath, "/admin"].concat(), api::admin_routes()) @@ -266,12 +192,12 @@ async fn launch_rocket(pool: db::DbPool, mut config: rocket::Config) -> Result<( .attach(util::Cors()) .ignite() .await?; - CONFIG.set_rocket_shutdown_handle(instance.shutdown()); + let shutdown = instance.shutdown(); tokio::spawn(async move { tokio::signal::ctrl_c() .await .expect("Error setting Ctrl-C handler"); - CONFIG.shutdown(); + shutdown.notify(); }); let _ = instance.launch().await?; Ok(()) diff --git a/src/ratelimit.rs b/src/ratelimit.rs @@ -1,31 +0,0 @@ -use crate::{Error, CONFIG}; -use governor::{clock::DefaultClock, state::keyed::DashMapStateStore, Quota, RateLimiter}; -use std::{net::IpAddr, num::NonZeroU32, sync::OnceLock, time::Duration}; -type Limiter<T = IpAddr> = RateLimiter<T, DashMapStateStore<T>, DefaultClock>; -static LIMITER_LOGIN: OnceLock<Limiter> = OnceLock::new(); -pub fn init_limiter() { - LIMITER_LOGIN - .set({ - let seconds = Duration::from_secs(CONFIG.login_ratelimit_seconds()); - let burst = NonZeroU32::new(CONFIG.login_ratelimit_max_burst()) - .expect("Non-zero login ratelimit burst"); - RateLimiter::keyed( - Quota::with_period(seconds) - .expect("Non-zero login ratelimit seconds") - .allow_burst(burst), - ) - }) - .expect("") -} -pub fn check_limit_login(ip: &IpAddr) -> Result<(), Error> { - match LIMITER_LOGIN - .get() - .expect("LIMITER_LOGIN should be initialized in main") - .check_key(ip) - { - Ok(_) => Ok(()), - Err(_e) => { - err_code!("Too many login requests", 429); - } - } -} diff --git a/src/static/templates/404.hbs b/src/static/templates/404.hbs @@ -1,38 +0,0 @@ -<!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="robots" content="noindex,nofollow" /> - <link rel="icon" type="image/png" href="{{urlpath}}/vw_static/vaultwarden-favicon.png"> - <title>Page not found!</title> - <link rel="stylesheet" href="{{urlpath}}/vw_static/bootstrap.css" /> - <link rel="stylesheet" href="{{urlpath}}/vw_static/404.css" /> -</head> - -<body class="bg-light"> - - <nav class="navbar navbar-expand-md navbar-dark bg-dark mb-4 shadow fixed-top"> - <div class="container"> - <a class="navbar-brand" href="{{urlpath}}/"><img class="vaultwarden-icon" src="{{urlpath}}/vw_static/vaultwarden-icon.png" alt="V">aultwarden</a> - <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarCollapse" - aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation"> - <span class="navbar-toggler-icon"></span> - </button> - <div class="collapse navbar-collapse" id="navbarCollapse"> - <ul class="navbar-nav me-auto"> - </div> - </div> - </nav> - - <main class="container inner content text-center"> - <h2>Page not found!</h2> - <p class="lead">Sorry, but the page you were looking for could not be found.</p> - <p class="display-6"> - <a href="{{urlpath}}/"><img class="vw-404" src="{{urlpath}}/vw_static/404.png" alt="Return to the web vault?"></a></p> - <p>You can <a href="{{urlpath}}/">return to the web-vault</a>, or <a href="https://github.com/dani-garcia/vaultwarden">contact us</a>.</p> - </main> - - <div class="container footer text-muted content">Vaultwarden (unofficial Bitwarden&reg; server)</div> -</body> -</html> diff --git a/src/static/templates/admin/base.hbs b/src/static/templates/admin/base.hbs @@ -1,98 +0,0 @@ -<!DOCTYPE html> -<html lang="en" data-bs-theme="auto"> -<head> - <meta charset="UTF-8"> - <meta name="viewport" content="width=device-width, initial-scale=1" /> - <meta name="robots" content="noindex,nofollow" /> - <link rel="icon" type="image/png" href="{{urlpath}}/vw_static/vaultwarden-favicon.png"> - <title>Vaultwarden Admin Panel</title> - <link rel="stylesheet" href="{{urlpath}}/vw_static/bootstrap.css" /> - <link rel="stylesheet" href="{{urlpath}}/vw_static/admin.css" /> - <script src="{{urlpath}}/vw_static/admin.js"></script> -</head> -<body> - <nav class="navbar navbar-expand-md navbar-dark bg-dark mb-4 shadow fixed-top"> - <div class="container-xl"> - <a class="navbar-brand" href="{{urlpath}}/admin"><img class="vaultwarden-icon" src="{{urlpath}}/vw_static/vaultwarden-icon.png" alt="V">aultwarden Admin</a> - <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarCollapse" - aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation"> - <span class="navbar-toggler-icon"></span> - </button> - <div class="collapse navbar-collapse" id="navbarCollapse"> - <ul class="navbar-nav me-auto"> - {{#if logged_in}} - <li class="nav-item"> - <a class="nav-link" href="{{urlpath}}/admin">Settings</a> - </li> - <li class="nav-item"> - <a class="nav-link" href="{{urlpath}}/admin/users/overview">Users</a> - </li> - <li class="nav-item"> - <a class="nav-link" href="{{urlpath}}/admin/organizations/overview">Organizations</a> - </li> - <li class="nav-item"> - <a class="nav-link" href="{{urlpath}}/admin/diagnostics">Diagnostics</a> - </li> - {{/if}} - <li class="nav-item"> - <a class="nav-link" href="{{urlpath}}/" target="_blank" rel="noreferrer">Vault</a> - </li> - </ul> - - <ul class="navbar-nav"> - <li class="nav-item dropdown"> - <button - class="btn btn-link nav-link py-0 px-0 px-md-2 dropdown-toggle d-flex align-items-center" - id="bd-theme" type="button" aria-expanded="false" data-bs-toggle="dropdown" - data-bs-display="static" aria-label="Toggle theme (auto)"> - <span class="my-1 fs-4 theme-icon-active"> - <use>&#9775;</use> - </span> - <span class="d-md-none ms-2" id="bd-theme-text">Toggle theme</span> - </button> - <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="bd-theme-text"> - <li> - <button type="button" class="dropdown-item d-flex align-items-center" - data-bs-theme-value="light" aria-pressed="false"> - <span class="me-2 fs-4 theme-icon"> - <use>&#9728;</use> - </span> - Light - </button> - </li> - <li> - <button type="button" class="dropdown-item d-flex align-items-center" - data-bs-theme-value="dark" aria-pressed="false"> - <span class="me-2 fs-4 theme-icon"> - <use>&starf;</use> - </span> - Dark - </button> - </li> - <li> - <button type="button" class="dropdown-item d-flex align-items-center active" - data-bs-theme-value="auto" aria-pressed="true"> - <span class="me-2 fs-4 theme-icon"> - <use>&#9775;</use> - </span> - Auto - </button> - </li> - </ul> - </li> - </ul> - - {{#if logged_in}} - <a class="btn btn-sm btn-secondary" href="{{urlpath}}/admin/logout">Log Out</a> - {{/if}} - - </div> - </div> - </nav> - - {{> (lookup this "page_content") }} - - <!-- This script needs to be at the bottom, else it will fail! --> - <script src="{{urlpath}}/vw_static/bootstrap.bundle.js"></script> -</body> -</html> diff --git a/src/static/templates/admin/diagnostics.hbs b/src/static/templates/admin/diagnostics.hbs @@ -1,212 +0,0 @@ -<main class="container-xl"> - <div id="diagnostics-block" class="my-3 p-3 rounded shadow"> - <h6 class="border-bottom pb-2 mb-2">Diagnostics</h6> - - <h3>Versions</h3> - <div class="row"> - <div class="col-md"> - <dl class="row"> - <dt class="col-sm-5">Server Installed - <span class="badge bg-success d-none" id="server-success" title="Latest version is installed.">Ok</span> - <span class="badge bg-warning text-dark d-none" id="server-warning" title="There seems to be an update available.">Update</span> - <span class="badge bg-info text-dark d-none" id="server-branch" title="This is a branched version.">Branched</span> - </dt> - <dd class="col-sm-7"> - <span id="server-installed">{{page_data.current_release}}</span> - </dd> - <dt class="col-sm-5">Server Latest - <span class="badge bg-secondary d-none" id="server-failed" title="Unable to determine latest version.">Unknown</span> - </dt> - <dd class="col-sm-7"> - <span id="server-latest">{{page_data.latest_release}}<span id="server-latest-commit" class="d-none">-{{page_data.latest_commit}}</span></span> - </dd> - {{#if page_data.web_vault_enabled}} - <dt class="col-sm-5">Web Installed - <span class="badge bg-success d-none" id="web-success" title="Latest version is installed.">Ok</span> - <span class="badge bg-warning d-none" id="web-warning" title="There seems to be an update available.">Update</span> - </dt> - <dd class="col-sm-7"> - <span id="web-installed">{{page_data.web_vault_version}}</span> - </dd> - {{#unless page_data.running_within_docker}} - <dt class="col-sm-5">Web Latest - <span class="badge bg-secondary d-none" id="web-failed" title="Unable to determine latest version.">Unknown</span> - </dt> - <dd class="col-sm-7"> - <span id="web-latest">{{page_data.latest_web_build}}</span> - </dd> - {{/unless}} - {{/if}} - {{#unless page_data.web_vault_enabled}} - <dt class="col-sm-5">Web Installed</dt> - <dd class="col-sm-7"> - <span id="web-installed">Web Vault is disabled</span> - </dd> - {{/unless}} - <dt class="col-sm-5">Database</dt> - <dd class="col-sm-7"> - <span><b>{{page_data.db_type}}:</b> {{page_data.db_version}}</span> - </dd> - </dl> - </div> - </div> - - <h3>Checks</h3> - <div class="row"> - <div class="col-md"> - <dl class="row"> - <dt class="col-sm-5">OS/Arch</dt> - <dd class="col-sm-7"> - <span class="d-block"><b>{{ page_data.host_os }} / {{ page_data.host_arch }}</b></span> - </dd> - <dt class="col-sm-5">Running within Docker</dt> - <dd class="col-sm-7"> - {{#if page_data.running_within_docker}} - <span class="d-block"><b>Yes (Base: {{ page_data.docker_base_image }})</b></span> - {{/if}} - {{#unless page_data.running_within_docker}} - <span class="d-block"><b>No</b></span> - {{/unless}} - </dd> - <dt class="col-sm-5">Environment settings overridden</dt> - <dd class="col-sm-7"> - {{#if page_data.overrides}} - <span class="d-block" title="The following settings are overridden: {{page_data.overrides}}"><b>Yes</b></span> - {{/if}} - {{#unless page_data.overrides}} - <span class="d-block"><b>No</b></span> - {{/unless}} - </dd> - <dt class="col-sm-5">Uses a reverse proxy</dt> - <dd class="col-sm-7"> - {{#if page_data.ip_header_exists}} - <span class="d-block" title="IP Header found."><b>Yes</b></span> - {{/if}} - {{#unless page_data.ip_header_exists}} - <span class="d-block" title="No IP Header found."><b>No</b></span> - {{/unless}} - </dd> - {{!-- Only show this if the IP Header Exists --}} - {{#if page_data.ip_header_exists}} - <dt class="col-sm-5">IP header - {{#if page_data.ip_header_match}} - <span class="badge bg-success" title="IP_HEADER config seems to be valid.">Match</span> - {{/if}} - {{#unless page_data.ip_header_match}} - <span class="badge bg-danger" title="IP_HEADER config seems to be invalid. IP's in the log could be invalid. Please fix.">No Match</span> - {{/unless}} - </dt> - <dd class="col-sm-7"> - {{#if page_data.ip_header_match}} - <span class="d-block"><b>Config/Server:</b> {{ page_data.ip_header_name }}</span> - {{/if}} - {{#unless page_data.ip_header_match}} - <span class="d-block"><b>Config:</b> {{ page_data.ip_header_config }}</span> - <span class="d-block"><b>Server:</b> {{ page_data.ip_header_name }}</span> - {{/unless}} - </dd> - {{/if}} - {{!-- End if IP Header Exists --}} - <dt class="col-sm-5">Internet access - {{#if page_data.has_http_access}} - <span class="badge bg-success" title="We have internet access!">Ok</span> - {{/if}} - {{#unless page_data.has_http_access}} - <span class="badge bg-danger" title="There seems to be no internet access. Please fix.">Error</span> - {{/unless}} - </dt> - <dd class="col-sm-7"> - {{#if page_data.has_http_access}} - <span class="d-block"><b>Yes</b></span> - {{/if}} - {{#unless page_data.has_http_access}} - <span class="d-block"><b>No</b></span> - {{/unless}} - </dd> - <dt class="col-sm-5">Internet access via a proxy</dt> - <dd class="col-sm-7"> - {{#if page_data.uses_proxy}} - <span class="d-block" title="Internet access goes via a proxy (HTTPS_PROXY or HTTP_PROXY is configured)."><b>Yes</b></span> - {{/if}} - {{#unless page_data.uses_proxy}} - <span class="d-block" title="We have direct internet access, no outgoing proxy configured."><b>No</b></span> - {{/unless}} - </dd> - <dt class="col-sm-5">DNS (github.com) - <span class="badge bg-success d-none" id="dns-success" title="DNS Resolving works!">Ok</span> - <span class="badge bg-danger d-none" id="dns-warning" title="DNS Resolving failed. Please fix.">Error</span> - </dt> - <dd class="col-sm-7"> - <span id="dns-resolved">{{page_data.dns_resolved}}</span> - </dd> - <dt class="col-sm-5">Date & Time (Local)</dt> - <dd class="col-sm-7"> - <span><b>Server:</b> {{page_data.server_time_local}}</span> - </dd> - <dt class="col-sm-5">Date & Time (UTC) - <span class="badge bg-success d-none" id="time-success" title="Server and browser times are within 15 seconds of each other.">Server/Browser Ok</span> - <span class="badge bg-danger d-none" id="time-warning" title="Server and browser times are more than 15 seconds apart.">Server/Browser Error</span> - <span class="badge bg-success d-none" id="ntp-server-success" title="Server and NTP times are within 15 seconds of each other.">Server NTP Ok</span> - <span class="badge bg-danger d-none" id="ntp-server-warning" title="Server and NTP times are more than 15 seconds apart.">Server NTP Error</span> - <span class="badge bg-success d-none" id="ntp-browser-success" title="Browser and NTP times are within 15 seconds of each other.">Browser NTP Ok</span> - <span class="badge bg-danger d-none" id="ntp-browser-warning" title="Browser and NTP times are more than 15 seconds apart.">Browser NTP Error</span> - </dt> - <dd class="col-sm-7"> - <span id="ntp-time" class="d-block"><b>NTP:</b> <span id="ntp-server-string">{{page_data.ntp_time}}</span></span> - <span id="time-server" class="d-block"><b>Server:</b> <span id="time-server-string">{{page_data.server_time}}</span></span> - <span id="time-browser" class="d-block"><b>Browser:</b> <span id="time-browser-string"></span></span> - </dd> - - <dt class="col-sm-5">Domain configuration - <span class="badge bg-success d-none" id="domain-success" title="The domain variable matches the browser location and seems to be configured correctly.">Match</span> - <span class="badge bg-danger d-none" id="domain-warning" title="The domain variable does not match the browser location.&#013;&#010;The domain variable does not seem to be configured correctly.&#013;&#010;Some features may not work as expected!">No Match</span> - <span class="badge bg-success d-none" id="https-success" title="Configured to use HTTPS">HTTPS</span> - <span class="badge bg-danger d-none" id="https-warning" title="Not configured to use HTTPS.&#013;&#010;Some features may not work as expected!">No HTTPS</span> - </dt> - <dd class="col-sm-7"> - <span id="domain-server" class="d-block"><b>Server:</b> <span id="domain-server-string">{{page_data.admin_url}}</span></span> - <span id="domain-browser" class="d-block"><b>Browser:</b> <span id="domain-browser-string"></span></span> - </dd> - </dl> - </div> - </div> - - <h3>Support</h3> - <div class="row"> - <div class="col-md"> - <dl class="row"> - <dd class="col-sm-12"> - If you need support please check the following links first before you create a new issue: - <a href="https://vaultwarden.discourse.group/" target="_blank" rel="noreferrer noopener">Vaultwarden Forum</a> - | <a href="https://github.com/dani-garcia/vaultwarden/discussions" target="_blank" rel="noreferrer noopener">Github Discussions</a> - </dd> - </dl> - <dl class="row"> - <dd class="col-sm-12"> - You can use the button below to pre-generate a string which you can copy/paste on either the Forum or when Creating a new issue at Github.<br> - We try to hide the most sensitive values from the generated support string by default, but please verify if there is nothing in there which you want to hide!<br> - </dd> - </dl> - <dl class="row"> - <dt class="col-sm-3"> - <button type="button" id="gen-support" class="btn btn-primary">Generate Support String</button> - <br><br> - <button type="button" id="copy-support" class="btn btn-info mb-3 d-none">Copy To Clipboard</button> - <div class="toast-container position-absolute float-start vw-copy-toast"> - <div id="toastClipboardCopy" class="toast fade hide" role="status" aria-live="polite" aria-atomic="true" data-bs-autohide="true" data-bs-delay="1500"> - <div class="toast-body"> - Copied to clipboard! - </div> - </div> - </div> - </dt> - <dd class="col-sm-9"> - <pre id="support-string" class="pre-scrollable d-none w-100 border p-2"></pre> - </dd> - </dl> - </div> - </div> - </div> -</main> -<script src="{{urlpath}}/vw_static/admin_diagnostics.js"></script> -<script type="application/json" id="diagnostics_json">{{to_json page_data}}</script> diff --git a/src/static/templates/admin/login.hbs b/src/static/templates/admin/login.hbs @@ -1,24 +0,0 @@ -<main class="container-xl"> - {{#if error}} - <div class="align-items-center p-3 mb-3 text-opacity-50 text-dark bg-warning rounded shadow"> - <div> - <h6 class="mb-0 text-dark">{{error}}</h6> - </div> - </div> - {{/if}} - - <div class="align-items-center p-3 mb-3 text-opacity-75 text-light bg-danger rounded shadow"> - <div> - <h6 class="mb-0 text-light">Authentication key needed to continue</h6> - <small>Please provide it below:</small> - - <form class="form-inline" method="post" action="{{urlpath}}/admin"> - <input type="password" autocomplete="password" class="form-control w-50 mr-2" name="token" placeholder="Enter admin token" autofocus="autofocus"> - {{#if redirect}} - <input type="hidden" id="redirect" name="redirect" value="/{{redirect}}"> - {{/if}} - <button type="submit" class="btn btn-primary mt-2">Enter</button> - </form> - </div> - </div> -</main> diff --git a/src/static/templates/admin/organizations.hbs b/src/static/templates/admin/organizations.hbs @@ -1,65 +0,0 @@ -<main class="container-xl"> - <div id="organizations-block" class="my-3 p-3 rounded shadow"> - <h6 class="border-bottom pb-2 mb-3">Organizations</h6> - <div class="table-responsive-xl small"> - <table id="orgs-table" class="table table-sm table-striped table-hover"> - <thead> - <tr> - <th class="vw-org-details">Organization</th> - <th class="vw-users">Users</th> - <th class="vw-entries">Entries</th> - <th class="vw-attachments">Attachments</th> - <th class="vw-misc">Misc</th> - <th class="vw-actions">Actions</th> - </tr> - </thead> - <tbody> - {{#each page_data}} - <tr> - <td> - <svg width="48" height="48" class="float-start me-2 rounded" data-jdenticon-value="{{Id}}"> - <div class="float-start"> - <strong>{{Name}}</strong> - <span class="me-2">({{BillingEmail}})</span> - <span class="d-block"> - <span class="badge bg-success font-monospace">{{Id}}</span> - </span> - </div> - </td> - <td> - <span class="d-block">{{user_count}}</span> - </td> - <td> - <span class="d-block">{{cipher_count}}</span> - </td> - <td> - <span class="d-block"><strong>Amount:</strong> {{attachment_count}}</span> - {{#if attachment_count}} - <span class="d-block"><strong>Size:</strong> {{attachment_size}}</span> - {{/if}} - </td> - <td> - <span class="d-block"><strong>Collections:</strong> {{collection_count}}</span> - <span class="d-block"><strong>Groups:</strong> {{group_count}}</span> - <span class="d-block"><strong>Events:</strong> {{event_count}}</span> - </td> - <td class="text-end px-0 small"> - <button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-delete-organization data-vw-org-uuid="{{jsesc Id no_quote}}" data-vw-org-name="{{jsesc Name no_quote}}" data-vw-billing-email="{{jsesc BillingEmail no_quote}}">Delete Organization</button><br> - </td> - </tr> - {{/each}} - </tbody> - </table> - </div> - - <div class="mt-3 clearfix"> - <button type="button" class="btn btn-sm btn-primary float-end" id="reload">Reload organizations</button> - </div> - </div> -</main> - -<link rel="stylesheet" href="{{urlpath}}/vw_static/datatables.css" /> -<script src="{{urlpath}}/vw_static/jquery-3.7.0.slim.js"></script> -<script src="{{urlpath}}/vw_static/datatables.js"></script> -<script src="{{urlpath}}/vw_static/admin_organizations.js"></script> -<script src="{{urlpath}}/vw_static/jdenticon.js"></script> diff --git a/src/static/templates/admin/settings.hbs b/src/static/templates/admin/settings.hbs @@ -1,156 +0,0 @@ -<main class="container-xl"> - <div id="admin_token_warning" class="alert alert-warning alert-dismissible fade show d-none"> - <button type="button" class="btn-close" data-bs-target="admin_token_warning" data-bs-dismiss="alert" aria-label="Close"></button> - You are using a plain text `ADMIN_TOKEN` which is insecure.<br> - Please generate a secure Argon2 PHC string by using `vaultwarden hash` or `argon2`.<br> - See: <a href="https://github.com/dani-garcia/vaultwarden/wiki/Enabling-admin-page#secure-the-admin_token" target="_blank" rel="noopener noreferrer">Enabling admin page - Secure the `ADMIN_TOKEN`</a> - </div> - <div id="config-block" class="align-items-center p-3 mb-3 bg-secondary rounded shadow"> - <div> - <h6 class="text-white mb-3">Configuration</h6> - <div class="small text-white mb-3"> - <span class="font-weight-bolder">NOTE:</span> The settings here override the environment variables. Once saved, it's recommended to stop setting them to avoid confusion.<br> - This does not apply to the read-only section, which can only be set via environment variables.<br> - Settings which are overridden are shown with <span class="is-overridden-true alert-row px-1">a yellow colored background</span>. - </div> - - <form class="form needs-validation" id="config-form" novalidate> - {{#each page_data.config}} - {{#if groupdoc}} - <div class="card mb-3"> - <button id="b_{{group}}" type="button" class="card-header text-start btn btn-link text-decoration-none" aria-expanded="false" aria-controls="g_{{group}}" data-bs-toggle="collapse" data-bs-target="#g_{{group}}">{{groupdoc}}</button> - <div id="g_{{group}}" class="card-body collapse"> - {{#each elements}} - {{#if editable}} - <div class="row my-2 align-items-center is-overridden-{{overridden}} alert-row" title="[{{name}}] {{doc.description}}"> - {{#case type "text" "number" "password"}} - <label for="input_{{name}}" class="col-sm-3 col-form-label">{{doc.name}}</label> - <div class="col-sm-8"> - <div class="input-group"> - <input class="form-control conf-{{type}}" id="input_{{name}}" type="{{type}}" - name="{{name}}" value="{{value}}" {{#if default}} placeholder="Default: {{default}}"{{/if}}> - {{#case type "password"}} - <button class="btn btn-outline-secondary input-group-text" type="button" data-vw-pw-toggle="input_{{name}}">Show/hide</button> - {{/case}} - </div> - </div> - {{/case}} - {{#case type "checkbox"}} - <div class="col-sm-3 col-form-label">{{doc.name}}</div> - <div class="col-sm-8"> - <div class="form-check"> - <input class="form-check-input conf-{{type}}" type="checkbox" id="input_{{name}}" - name="{{name}}" {{#if value}} checked {{/if}}> - - <label class="form-check-label" for="input_{{name}}"> Default: {{default}} </label> - </div> - </div> - {{/case}} - </div> - {{/if}} - {{/each}} - {{#case group "smtp"}} - <div class="row my-2 align-items-center pt-3 border-top" title="Send a test email to given email address"> - <label for="smtp-test-email" class="col-sm-3 col-form-label">Test SMTP</label> - <div class="col-sm-8 input-group"> - <input class="form-control" id="smtp-test-email" type="email" placeholder="Enter test email" required spellcheck="false"> - <button type="button" class="btn btn-outline-primary input-group-text" id="smtpTest">Send test email</button> - <div class="invalid-tooltip">Please provide a valid email address</div> - </div> - </div> - {{/case}} - </div> - </div> - {{/if}} - {{/each}} - - <div class="card mb-3"> - <button id="b_readonly" type="button" class="card-header text-start btn btn-link text-decoration-none" aria-expanded="false" aria-controls="g_readonly" - data-bs-toggle="collapse" data-bs-target="#g_readonly">Read-Only Config</button> - <div id="g_readonly" class="card-body collapse"> - <div class="small mb-3"> - NOTE: These options can't be modified in the editor because they would require the server - to be restarted. To modify them, you need to set the correct environment variables when - launching the server. You can check the variable names in the tooltips of each option. - </div> - - {{#each page_data.config}} - {{#each elements}} - {{#unless editable}} - <div class="row my-2 align-items-center alert-row" title="[{{name}}] {{doc.description}}"> - {{#case type "text" "number" "password"}} - <label for="input_{{name}}" class="col-sm-3 col-form-label">{{doc.name}}</label> - <div class="col-sm-8"> - <div class="input-group"> - {{!-- - Also set the database_url input as password here. - If we would set it to password in config.rs it will not be character masked for the support string. - And sometimes this is more useful for providing support than just 3 asterisk. - --}} - {{#if (eq name "database_url")}} - <input readonly class="form-control" id="input_{{name}}" type="password" value="{{value}}" {{#if default}} placeholder="Default: {{default}}" {{/if}}> - <button class="btn btn-outline-secondary" type="button" data-vw-pw-toggle="input_{{name}}">Show/hide</button> - {{else}} - <input readonly class="form-control" id="input_{{name}}" type="{{type}}" value="{{value}}" {{#if default}} placeholder="Default: {{default}}" {{/if}} spellcheck="false"> - {{#case type "password"}} - <button class="btn btn-outline-secondary" type="button" data-vw-pw-toggle="input_{{name}}">Show/hide</button> - {{/case}} - {{/if}} - </div> - </div> - {{/case}} - {{#case type "checkbox"}} - <div class="col-sm-3 col-form-label">{{doc.name}}</div> - <div class="col-sm-8"> - <div class="form-check align-middle"> - <input disabled class="form-check-input" type="checkbox" id="input_{{name}}" - {{#if value}} checked {{/if}}> - - <label class="form-check-label" for="input_{{name}}"> Default: {{default}} </label> - </div> - </div> - {{/case}} - </div> - {{/unless}} - {{/each}} - {{/each}} - - </div> - </div> - - {{#if page_data.can_backup}} - <div class="card mb-3"> - <button id="b_database" type="button" class="card-header text-start btn btn-link text-decoration-none" aria-expanded="false" aria-controls="g_database" - data-bs-toggle="collapse" data-bs-target="#g_database">Backup Database</button> - <div id="g_database" class="card-body collapse"> - <div class="small mb-3"> - WARNING: This function only creates a backup copy of the SQLite database. - This does not include any configuration or file attachment data that may - also be needed to fully restore a vaultwarden instance. For details on - how to perform complete backups, refer to the wiki page on - <a href="https://github.com/dani-garcia/vaultwarden/wiki/Backing-up-your-vault" target="_blank" rel="noopener noreferrer">backups</a>. - </div> - <button type="button" class="btn btn-primary" id="backupDatabase">Backup Database</button> - </div> - </div> - {{/if}} - - <button type="submit" class="btn btn-primary">Save</button> - <button type="button" class="btn btn-danger float-end" id="deleteConf">Reset defaults</button> - </form> - </div> - </div> -</main> -<style> - #config-block ::placeholder { - /* Most modern browsers support this now. */ - color: orangered; - } - - .is-overridden-true { - --bs-alert-color: #664d03; - --bs-alert-bg: #fff3cd; - --bs-alert-border-color: #ffecb5; - } -</style> -<script src="{{urlpath}}/vw_static/admin_settings.js"></script> diff --git a/src/static/templates/admin/users.hbs b/src/static/templates/admin/users.hbs @@ -1,146 +0,0 @@ -<main class="container-xl"> - <div id="users-block" class="my-3 p-3 rounded shadow"> - <h6 class="border-bottom pb-2 mb-3">Registered Users</h6> - <div class="table-responsive-xl small"> - <table id="users-table" class="table table-sm table-striped table-hover"> - <thead> - <tr> - <th class="vw-account-details">User</th> - <th class="vw-created-at">Created at</th> - <th class="vw-last-active">Last Active</th> - <th class="vw-entries">Entries</th> - <th class="vw-attachments">Attachments</th> - <th class="vw-organizations">Organizations</th> - <th class="vw-actions">Actions</th> - </tr> - </thead> - <tbody> - {{#each page_data}} - <tr> - <td> - <svg width="48" height="48" class="float-start me-2 rounded" data-jdenticon-value="{{Email}}"> - <div class="float-start"> - <strong>{{Name}}</strong> - <span class="d-block">{{Email}}</span> - <span class="d-block"> - {{#unless user_enabled}} - <span class="badge bg-danger me-2" title="User is disabled">Disabled</span> - {{/unless}} - {{#if TwoFactorEnabled}} - <span class="badge bg-success me-2" title="2FA is enabled">2FA</span> - {{/if}} - {{#case _Status 1}} - <span class="badge bg-warning text-dark me-2" title="User is invited">Invited</span> - {{/case}} - {{#if EmailVerified}} - <span class="badge bg-success me-2" title="Email has been verified">Verified</span> - {{/if}} - </span> - </div> - </td> - <td> - <span class="d-block">{{created_at}}</span> - </td> - <td> - <span class="d-block">{{last_active}}</span> - </td> - <td> - <span class="d-block">{{cipher_count}}</span> - </td> - <td> - <span class="d-block"><strong>Amount:</strong> {{attachment_count}}</span> - {{#if attachment_count}} - <span class="d-block"><strong>Size:</strong> {{attachment_size}}</span> - {{/if}} - </td> - <td> - <div class="overflow-auto vw-org-cell" data-vw-user-email="{{jsesc Email no_quote}}" data-vw-user-uuid="{{jsesc Id no_quote}}"> - {{#each Organizations}} - <button class="badge" data-bs-toggle="modal" data-bs-target="#userOrgTypeDialog" data-vw-org-type="{{Type}}" data-vw-org-uuid="{{jsesc Id no_quote}}" data-vw-org-name="{{jsesc Name no_quote}}">{{Name}}</button> - {{/each}} - </div> - </td> - <td class="text-end px-0 small"> - <span data-vw-user-uuid="{{jsesc Id no_quote}}" data-vw-user-email="{{jsesc Email no_quote}}"> - {{#if TwoFactorEnabled}} - <button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-remove2fa>Remove all 2FA</button><br> - {{/if}} - <button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-deauth-user>Deauthorize sessions</button><br> - <button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-delete-user>Delete User</button><br> - {{#if user_enabled}} - <button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-disable-user>Disable User</button><br> - {{else}} - <button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-enable-user>Enable User</button><br> - {{/if}} - {{#case _Status 1}} - <button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-resend-user-invite>Resend invite</button><br> - {{/case}} - </span> - </td> - </tr> - {{/each}} - </tbody> - </table> - </div> - - <div class="mt-3 clearfix"> - <button type="button" class="btn btn-sm btn-danger" id="updateRevisions" - title="Force all clients to fetch new data next time they connect. Useful after restoring a backup to remove any stale data."> - Force clients to resync - </button> - - <button type="button" class="btn btn-sm btn-primary float-end" id="reload">Reload users</button> - </div> - </div> - - <div id="inviteUserFormBlock" class="align-items-center p-3 mb-3 text-white-50 bg-secondary rounded shadow"> - <div> - <h6 class="mb-0 text-white">Invite User</h6> - <small>Email:</small> - - <form class="form-inline input-group w-50" id="inviteUserForm"> - <input type="email" class="form-control me-2" id="inviteEmail" placeholder="Enter email" required spellcheck="false"> - <button type="submit" class="btn btn-primary">Invite</button> - </form> - </div> - </div> - - <div id="userOrgTypeDialog" class="modal fade" tabindex="-1" role="dialog" aria-hidden="true"> - <div class="modal-dialog modal-dialog-centered modal-sm"> - <div class="modal-content"> - <div class="modal-header"> - <h6 class="modal-title" id="userOrgTypeDialogTitle"></h6> - <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> - </div> - <form class="form" id="userOrgTypeForm"> - <input type="hidden" name="user_uuid" id="userOrgTypeUserUuid" value=""> - <input type="hidden" name="org_uuid" id="userOrgTypeOrgUuid" value=""> - <div class="modal-body"> - <div class="radio"> - <label><input type="radio" value="2" class="form-radio-input" name="user_type" id="userOrgTypeUser">&nbsp;User</label> - </div> - <div class="radio"> - <label><input type="radio" value="3" class="form-radio-input" name="user_type" id="userOrgTypeManager">&nbsp;Manager</label> - </div> - <div class="radio"> - <label><input type="radio" value="1" class="form-radio-input" name="user_type" id="userOrgTypeAdmin">&nbsp;Admin</label> - </div> - <div class="radio"> - <label><input type="radio" value="0" class="form-radio-input" name="user_type" id="userOrgTypeOwner">&nbsp;Owner</label> - </div> - </div> - <div class="modal-footer"> - <button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Cancel</button> - <button type="submit" class="btn btn-sm btn-primary">Change Role</button> - </div> - </form> - </div> - </div> - </div> -</main> - -<link rel="stylesheet" href="{{urlpath}}/vw_static/datatables.css" /> -<script src="{{urlpath}}/vw_static/jquery-3.7.0.slim.js"></script> -<script src="{{urlpath}}/vw_static/datatables.js"></script> -<script src="{{urlpath}}/vw_static/admin_users.js"></script> -<script src="{{urlpath}}/vw_static/jdenticon.js"></script> diff --git a/src/static/templates/email/admin_reset_password.hbs b/src/static/templates/email/admin_reset_password.hbs @@ -1,4 +0,0 @@ -Master Password Has Been Changed -<!----------------> -The master password for {{user_name}} has been changed by an administrator in your {{org_name}} organization. If you did not initiate this request, please reach out to your administrator immediately. -{{> email/email_footer_text }} diff --git a/src/static/templates/email/admin_reset_password.html.hbs b/src/static/templates/email/admin_reset_password.html.hbs @@ -1,11 +0,0 @@ -Master Password Has Been Changed -<!----------------> -{{> 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;" valign="top"> - The master password for <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;">{{user_name}}</b> has been changed by an administrator in your <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> organization. If you did not initiate this request, please reach out to your administrator immediately. - </td> - </tr> -</table> -{{> email/email_footer }} diff --git a/src/static/templates/email/change_email.hbs b/src/static/templates/email/change_email.hbs @@ -1,6 +0,0 @@ -Your Email Change -<!----------------> -To finalize changing your email address enter the following code in web vault: {{token}} - -If you did not try to change an email address, you can safely ignore this email. -{{> email/email_footer_text }} diff --git a/src/static/templates/email/change_email.html.hbs b/src/static/templates/email/change_email.html.hbs @@ -1,16 +0,0 @@ -Your Email Change -<!----------------> -{{> 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"> - To finalize changing your email address enter the following code in web vault: <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;">{{token}}</b> - </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"> - If you did not try to change an email address, you can safely ignore this email. - </td> - </tr> -</table> -{{> email/email_footer }} -\ No newline at end of file diff --git a/src/static/templates/email/delete_account.hbs b/src/static/templates/email/delete_account.hbs @@ -1,8 +0,0 @@ -Delete Your Account -<!----------------> -Click the link below to delete your account. - -Delete Your Account: {{url}}/#/verify-recover-delete?userId={{user_id}}&token={{token}}&email={{email}} - -If you did not request this email to delete your account, you can safely ignore this email. -{{> email/email_footer_text }} -\ No newline at end of file diff --git a/src/static/templates/email/delete_account.html.hbs b/src/static/templates/email/delete_account.html.hbs @@ -1,24 +0,0 @@ -Delete Your Account -<!----------------> -{{> 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"> - Click the link below to delete your account. - </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" 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"> - <a href="{{url}}/#/verify-recover-delete?userId={{user_id}}&token={{token}}&email={{email}}" - clicktracking=off target="_blank" style="color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #3c8dbc; border-color: #3c8dbc; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> - Delete Your Account - </a> - </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"> - If you did not request this email to delete your account, you can safely ignore this email. - </td> - </tr> -</table> -{{> email/email_footer }} -\ No newline at end of file diff --git a/src/static/templates/email/email_footer.hbs b/src/static/templates/email/email_footer.hbs @@ -1,24 +0,0 @@ - </td> - </tr> - </table> - </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 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="{{img_src}}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> - </body> -</html> diff --git a/src/static/templates/email/email_footer_text.hbs b/src/static/templates/email/email_footer_text.hbs @@ -1,3 +0,0 @@ - -=== -Github: https://github.com/dani-garcia/vaultwarden diff --git a/src/static/templates/email/email_header.hbs b/src/static/templates/email/email_header.hbs @@ -1,94 +0,0 @@ -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" xmlns="http://www.w3.org/1999/xhtml" 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;"> - <head> - <meta name="viewport" content="width=device-width" /> - <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> - <title>Vaultwarden</title> - </head> - <body style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; height: 100%; line-height: 25px; width: 100% !important;" bgcolor="#f6f6f6"> - <style type="text/css"> - body { - 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; - } - body * { - 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; - } - img { - max-width: 100%; - border: none; - } - body { - -webkit-font-smoothing: antialiased; - -webkit-text-size-adjust: none; - width: 100% !important; - height: 100%; - line-height: 25px; - } - body { - background-color: #f6f6f6; - } - @media only screen and (max-width: 600px) { - body { - padding: 0 !important; - } - .container { - padding: 0 !important; - width: 100% !important; - } - .container-table { - padding: 0 !important; - width: 100% !important; - } - .content { - padding: 0 0 10px 0 !important; - } - .content-wrap { - padding: 10px !important; - } - .invoice { - width: 100% !important; - } - .main { - border-right: none !important; - border-left: none !important; - border-radius: 0 !important; - } - .logo { - padding-top: 10px !important; - } - .footer { - margin-top: 10px !important; - } - .indented { - padding-left: 10px; - } - } - </style> - <table class="body-wrap" 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; width: 100%;" bgcolor="#f6f6f6"> - <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 valign="middle" class="aligncenter middle logo" 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; padding: 20px 0 10px;" align="center"> - <img src="{{img_src}}logo-gray.png" alt="Vaultwarden" width="190" height="39" 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%;" /> - </td> - </tr> - <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="container" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;" valign="top"> - <table cellpadding="0" cellspacing="0" class="container-table" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: max-content;"> - <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="content" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; display: block; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 0; line-height: 0; margin: 0 auto; max-width: 600px; padding-bottom: 20px;" valign="top"> - <table class="main" width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; margin: 0; -webkit-text-size-adjust: none; border: 1px solid #e9e9e9; border-radius: 3px;" bgcolor="white"> - <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-wrap" 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: 20px; -webkit-text-size-adjust: none;" valign="top"> diff --git a/src/static/templates/email/emergency_access_invite_accepted.hbs b/src/static/templates/email/emergency_access_invite_accepted.hbs @@ -1,8 +0,0 @@ -Emergency access contact {{{grantee_email}}} accepted -<!----------------> -This email is to notify you that {{grantee_email}} has accepted your invitation to become an emergency access contact. - -To confirm this user, log into the web vault ({{url}}), go to settings and confirm the user. - -If you do not wish to confirm this user, you can also remove them on the same page. -{{> email/email_footer_text }} diff --git a/src/static/templates/email/emergency_access_invite_accepted.html.hbs b/src/static/templates/email/emergency_access_invite_accepted.html.hbs @@ -1,21 +0,0 @@ -Emergency access contact {{{grantee_email}}} accepted -<!----------------> -{{> 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;" valign="top"> - This email is to notify you that {{grantee_email}} has accepted your invitation to become an emergency access contact. - </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" 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;" valign="top"> - To confirm this user, log into the <a href="{{url}}/">web vault</a>, go to settings and confirm the user. - </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;" valign="top"> - If you do not wish to confirm this user, you can also remove them on the same page. - </td> - </tr> -</table> -{{> email/email_footer }} diff --git a/src/static/templates/email/emergency_access_invite_confirmed.hbs b/src/static/templates/email/emergency_access_invite_confirmed.hbs @@ -1,6 +0,0 @@ -Emergency access contact for {{{grantor_name}}} confirmed -<!----------------> -This email is to notify you that you have been confirmed as an emergency access contact for *{{grantor_name}}*. - -You can now initiate emergency access requests from the web vault ({{url}}). -{{> email/email_footer_text }} diff --git a/src/static/templates/email/emergency_access_invite_confirmed.html.hbs b/src/static/templates/email/emergency_access_invite_confirmed.html.hbs @@ -1,16 +0,0 @@ -Emergency access contact for {{{grantor_name}}} confirmed -<!----------------> -{{> 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;" valign="top"> - This email is to notify you that you have been confirmed as an emergency access contact for <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;">{{grantor_name}}</b>. - </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;" valign="top"> - You can now initiate emergency access requests from the <a href="{{url}}/">web vault</a>. - </td> - </tr> - </table> -{{> email/email_footer }} diff --git a/src/static/templates/email/emergency_access_recovery_approved.hbs b/src/static/templates/email/emergency_access_recovery_approved.hbs @@ -1,4 +0,0 @@ -Emergency access request for {{{grantor_name}}} approved -<!----------------> -{{grantor_name}} has approved your emergency access request. You may now login on the web vault ({{url}}) and access their account. -{{> email/email_footer_text }} diff --git a/src/static/templates/email/emergency_access_recovery_approved.html.hbs b/src/static/templates/email/emergency_access_recovery_approved.html.hbs @@ -1,11 +0,0 @@ -Emergency access request for {{{grantor_name}}} approved -<!----------------> -{{> 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;" valign="top"> - <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;">{{grantor_name}}</b> has approved your emergency access request. You may now login on the <a href="{{url}}/">web vault</a> and access their account. - </td> - </tr> - </table> -{{> email/email_footer }} diff --git a/src/static/templates/email/emergency_access_recovery_initiated.hbs b/src/static/templates/email/emergency_access_recovery_initiated.hbs @@ -1,6 +0,0 @@ -Emergency access request by {{{grantee_name}}} initiated -<!----------------> -{{grantee_name}} has initiated an emergency access request to {{atype}} your account. You may login on the web vault ({{url}}) and manually approve or reject this request. - -If you do nothing, the request will automatically be approved after {{wait_time_days}} day(s). -{{> email/email_footer_text }} diff --git a/src/static/templates/email/emergency_access_recovery_initiated.html.hbs b/src/static/templates/email/emergency_access_recovery_initiated.html.hbs @@ -1,16 +0,0 @@ -Emergency access request by {{{grantee_name}}} initiated -<!----------------> -{{> 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;" valign="top"> - <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;">{{grantee_name}}</b> has initiated an emergency access request to <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;">{{atype}}</b> your account. You may login on the <a href="{{url}}/">web vault</a> and manually approve or reject this request. - </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;" valign="top"> - If you do nothing, the request will automatically be approved after <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;">{{wait_time_days}}</b> day(s). - </td> - </tr> - </table> -{{> email/email_footer }} diff --git a/src/static/templates/email/emergency_access_recovery_rejected.hbs b/src/static/templates/email/emergency_access_recovery_rejected.hbs @@ -1,4 +0,0 @@ -Emergency access request to {{{grantor_name}}} rejected -<!----------------> -{{grantor_name}} has rejected your emergency access request. -{{> email/email_footer_text }} diff --git a/src/static/templates/email/emergency_access_recovery_rejected.html.hbs b/src/static/templates/email/emergency_access_recovery_rejected.html.hbs @@ -1,11 +0,0 @@ -Emergency access request to {{{grantor_name}}} rejected -<!----------------> -{{> 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;" valign="top"> - <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;">{{grantor_name}}</b> has rejected your emergency access request. - </td> - </tr> - </table> -{{> email/email_footer }} diff --git a/src/static/templates/email/emergency_access_recovery_reminder.hbs b/src/static/templates/email/emergency_access_recovery_reminder.hbs @@ -1,6 +0,0 @@ -Emergency access request by {{{grantee_name}}} is pending -<!----------------> -{{grantee_name}} has a pending emergency access request to {{atype}} your account. You may login on the web vault ({{url}}) and manually approve or reject this request. - -If you do nothing, the request will automatically be approved after {{days_left}} day(s). -{{> email/email_footer_text }} diff --git a/src/static/templates/email/emergency_access_recovery_reminder.html.hbs b/src/static/templates/email/emergency_access_recovery_reminder.html.hbs @@ -1,16 +0,0 @@ -Emergency access request by {{{grantee_name}}} is pending -<!----------------> -{{> 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;" valign="top"> - <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;">{{grantee_name}}</b> has a pending emergency access request to <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;">{{atype}}</b> your account. You may login on the <a href="{{url}}/">web vault</a> and manually approve or reject this request. - </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;" valign="top"> - If you do nothing, the request will automatically be approved after <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;">{{days_left}}</b> day(s). - </td> - </tr> - </table> -{{> email/email_footer }} diff --git a/src/static/templates/email/emergency_access_recovery_timed_out.hbs b/src/static/templates/email/emergency_access_recovery_timed_out.hbs @@ -1,4 +0,0 @@ -Emergency access request by {{{grantee_name}}} granted -<!----------------> -{{grantee_name}} has been granted emergency access to {{atype}} your account. You may login on the web vault ({{url}}) and manually revoke this request. -{{> email/email_footer_text }} diff --git a/src/static/templates/email/emergency_access_recovery_timed_out.html.hbs b/src/static/templates/email/emergency_access_recovery_timed_out.html.hbs @@ -1,11 +0,0 @@ -Emergency access request by {{{grantee_name}}} granted -<!----------------> -{{> 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;" valign="top"> - <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;">{{grantee_name}}</b> has been granted emergency access to <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;">{{atype}}</b> your account. You may login on the <a href="{{url}}/">web vault</a> and manually revoke this request. - </td> - </tr> - </table> -{{> email/email_footer }} diff --git a/src/static/templates/email/incomplete_2fa_login.hbs b/src/static/templates/email/incomplete_2fa_login.hbs @@ -1,10 +0,0 @@ -Incomplete Two-Step Login From {{{device}}} -<!----------------> -Someone attempted to log into your account with the correct master password, but did not provide the correct token or action required to complete the two-step login process within {{time_limit}} minutes of the initial login attempt. - -* Date: {{datetime}} -* IP Address: {{ip}} -* Device Type: {{device}} - -If this was not you or someone you authorized, then you should change your master password as soon as possible, as it is likely to be compromised. -{{> email/email_footer_text }} diff --git a/src/static/templates/email/incomplete_2fa_login.html.hbs b/src/static/templates/email/incomplete_2fa_login.html.hbs @@ -1,31 +0,0 @@ -Incomplete Two-Step Login From {{{device}}} -<!----------------> -{{> 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;" valign="top"> - Someone attempted to log into your account with the correct master password, but did not provide the correct token or action required to complete the two-step login process within {{time_limit}} minutes of the initial login attempt. - </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" 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;" valign="top"> - <b>Date</b>: {{datetime}} - </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" 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;" valign="top"> - <b>IP Address:</b> {{ip}} - </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" 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;" valign="top"> - <b>Device Type:</b> {{device}} - </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;" valign="top"> - If this was not you or someone you authorized, then you should change your master password as soon as possible, as it is likely to be compromised. - </td> - </tr> -</table> -{{> email/email_footer }} diff --git a/src/static/templates/email/invite_accepted.hbs b/src/static/templates/email/invite_accepted.hbs @@ -1,5 +0,0 @@ -Invitation to {{{org_name}}} accepted -<!----------------> -This email is to notify you that {{email}} has accepted your invitation to join {{org_name}}. -Please log in via {{url}} to the vaultwarden server and confirm them from the organization management page. -{{> email/email_footer_text }} -\ No newline at end of file diff --git a/src/static/templates/email/invite_accepted.html.hbs b/src/static/templates/email/invite_accepted.html.hbs @@ -1,21 +0,0 @@ -Invitation to {{{org_name}}} accepted -<!----------------> -{{> 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;" valign="top"> - This email is to notify you that {{email}} has accepted your invitation to join <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>. - </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" 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;" valign="top"> - Please <a href="{{url}}/">log in</a> to the vaultwarden server and confirm them from the organization management page. - </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;" valign="top"> - If you do not wish to confirm this user, you can also remove them from the organization on the same page. - </td> - </tr> -</table> -{{> email/email_footer }} -\ No newline at end of file diff --git a/src/static/templates/email/invite_confirmed.hbs b/src/static/templates/email/invite_confirmed.hbs @@ -1,5 +0,0 @@ -Invitation to {{{org_name}}} confirmed -<!----------------> -This email is to notify you that you have been confirmed as a user of {{org_name}}. -Any collections and logins being shared with you by this organization will now appear in your Vaultwarden vault at {{url}}. -{{> email/email_footer_text }} -\ No newline at end of file diff --git a/src/static/templates/email/invite_confirmed.html.hbs b/src/static/templates/email/invite_confirmed.html.hbs @@ -1,17 +0,0 @@ -Invitation to {{{org_name}}} confirmed -<!----------------> -{{> 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;" valign="top"> - This email is to notify you that you have been confirmed as a user of <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>. - </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;" valign="top"> - Any collections and logins being shared with you by this organization will now appear in your Vaultwarden vault. <br> - <a href="{{url}}/">Log in</a> - </td> - </tr> -</table> -{{> email/email_footer }} -\ No newline at end of file diff --git a/src/static/templates/email/new_device_logged_in.hbs b/src/static/templates/email/new_device_logged_in.hbs @@ -1,10 +0,0 @@ -New Device Logged In From {{{device}}} -<!----------------> -Your account was just logged into from a new device. - -* Date: {{datetime}} -* IP Address: {{ip}} -* Device Type: {{device}} - -You can deauthorize all devices that have access to your account from the web vault ( {{url}} ) under Settings > My Account > Deauthorize Sessions. -{{> email/email_footer_text }} -\ No newline at end of file diff --git a/src/static/templates/email/new_device_logged_in.html.hbs b/src/static/templates/email/new_device_logged_in.html.hbs @@ -1,31 +0,0 @@ -New Device Logged In From {{{device}}} -<!----------------> -{{> 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;" valign="top"> - Your account was just logged into from a new device. - </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" 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;" valign="top"> - <b>Date</b>: {{datetime}} - </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" 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;" valign="top"> - <b>IP Address:</b> {{ip}} - </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" 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;" valign="top"> - <b>Device Type:</b> {{device}} - </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;" valign="top"> - You can deauthorize all devices that have access to your account from the <a href="{{url}}/">web vault</a> under Settings > My Account > Deauthorize Sessions. - </td> - </tr> -</table> -{{> email/email_footer }} -\ No newline at end of file diff --git a/src/static/templates/email/protected_action.hbs b/src/static/templates/email/protected_action.hbs @@ -1,6 +0,0 @@ -Your Vaultwarden Verification Code -<!----------------> -Your email verification code is: {{token}} - -Use this code to complete the protected action in Vaultwarden. -{{> email/email_footer_text }} diff --git a/src/static/templates/email/protected_action.html.hbs b/src/static/templates/email/protected_action.html.hbs @@ -1,16 +0,0 @@ -Your Vaultwarden Verification Code -<!----------------> -{{> 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;" valign="top"> - Your email verification code is: <b>{{token}}</b> - </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" 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;" valign="top"> - Use this code to complete the protected action in Vaultwarden. - </td> - </tr> -</table> -{{> email/email_footer }} diff --git a/src/static/templates/email/pw_hint_none.hbs b/src/static/templates/email/pw_hint_none.hbs @@ -1,8 +0,0 @@ -Your master password hint -<!----------------> -You (or someone) recently requested your master password hint. Unfortunately, your account does not have a master password hint. - -If you cannot remember your master password, there is no way to recover your data. The only option to gain access to your account again is to delete the account ( {{url}}/#/recover-delete ) so that you can register again and start over. All data associated with your account will be deleted. - -If you did not request your master password hint you can safely ignore this email. -{{> email/email_footer_text }} -\ No newline at end of file diff --git a/src/static/templates/email/pw_hint_none.html.hbs b/src/static/templates/email/pw_hint_none.html.hbs @@ -1,21 +0,0 @@ -Sorry, you have no password hint... -<!----------------> -{{> 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;" valign="top"> - You (or someone) recently requested your master password hint. Unfortunately, your account does not have a master password hint. <br 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> - </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;" valign="top"> - If you cannot remember your master password, there is no way to recover your data. The only option to gain access to your account again is to <a href="{{url}}/#/recover-delete">delete the account</a> so that you can register again and start over. All data associated with your account will be deleted. - </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;" valign="top"> - If you did not request your master password hint you can safely ignore this email. - </td> - </tr> -</table> -{{> email/email_footer }} -\ No newline at end of file diff --git a/src/static/templates/email/pw_hint_some.hbs b/src/static/templates/email/pw_hint_some.hbs @@ -1,11 +0,0 @@ -Your master password hint -<!----------------> -You (or someone) recently requested your master password hint. - -Your hint is: *{{hint}}* -Log in to the web vault: {{url}} - -If you cannot remember your master password, there is no way to recover your data. The only option to gain access to your account again is to delete the account ( {{url}}/#/recover-delete ) so that you can register again and start over. All data associated with your account will be deleted. - -If you did not request your master password hint you can safely ignore this email. -{{> email/email_footer_text }} -\ No newline at end of file diff --git a/src/static/templates/email/pw_hint_some.html.hbs b/src/static/templates/email/pw_hint_some.html.hbs @@ -1,27 +0,0 @@ -Your master password hint -<!----------------> -{{> 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;" valign="top"> - You (or someone) recently requested your master password hint. - </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" 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;" valign="top"> - Your hint is: "{{hint}}"<br 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;" /> - Log in: <a href="{{url}}/">Web Vault</a> - </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;" valign="top"> - If you cannot remember your master password, there is no way to recover your data. The only option to gain access to your account again is to <a href="{{url}}/#/recover-delete">delete the account</a> so that you can register again and start over. All data associated with your account will be deleted. - </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;" valign="top"> - If you did not request your master password hint you can safely ignore this email. - </td> - </tr> -</table> -{{> email/email_footer }} -\ No newline at end of file diff --git a/src/static/templates/email/send_2fa_removed_from_org.hbs b/src/static/templates/email/send_2fa_removed_from_org.hbs @@ -1,7 +0,0 @@ -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. -{{> email/email_footer_text }} diff --git a/src/static/templates/email/send_2fa_removed_from_org.html.hbs b/src/static/templates/email/send_2fa_removed_from_org.html.hbs @@ -1,16 +0,0 @@ -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 }} diff --git a/src/static/templates/email/send_emergency_access_invite.hbs b/src/static/templates/email/send_emergency_access_invite.hbs @@ -1,8 +0,0 @@ -Emergency access for {{{grantor_name}}} -<!----------------> -You have been invited to become an emergency contact for {{grantor_name}}. To accept this invite, click the following link: - -Click here to join: {{url}}/#/accept-emergency/?id={{emer_id}}&name={{grantor_name}}&email={{email}}&token={{token}} - -If you do not wish to become an emergency contact for {{grantor_name}}, you can safely ignore this email. -{{> email/email_footer_text }} diff --git a/src/static/templates/email/send_emergency_access_invite.html.hbs b/src/static/templates/email/send_emergency_access_invite.html.hbs @@ -1,24 +0,0 @@ -Emergency access for {{{grantor_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 invited to become an emergency contact for <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;">{{grantor_name}}</b>. - </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" 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"> - <a href="{{url}}/#/accept-emergency/?id={{emer_id}}&name={{grantor_name}}&email={{email}}&token={{token}}" - clicktracking=off target="_blank" style="color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #3c8dbc; border-color: #3c8dbc; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> - Become emergency contact - </a> - </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"> - If you do not wish to become an emergency contact for {{grantor_name}}, you can safely ignore this email. - </td> - </tr> - </table> -{{> email/email_footer }} -\ No newline at end of file diff --git a/src/static/templates/email/send_org_invite.hbs b/src/static/templates/email/send_org_invite.hbs @@ -1,10 +0,0 @@ -Join {{{org_name}}} -<!----------------> -You have been invited to join the *{{org_name}}* organization. - - -Click here to join: {{url}}/#/accept-organization/?organizationId={{org_id}}&organizationUserId={{org_user_id}}&email={{email}}&organizationName={{org_name_encoded}}&token={{token}} - - -If you do not wish to join this organization, you can safely ignore this email. -{{> email/email_footer_text }} -\ No newline at end of file diff --git a/src/static/templates/email/send_org_invite.html.hbs b/src/static/templates/email/send_org_invite.html.hbs @@ -1,24 +0,0 @@ -Join {{{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 invited to join the <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> organization. - </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" 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"> - <a href="{{url}}/#/accept-organization/?organizationId={{org_id}}&organizationUserId={{org_user_id}}&email={{email}}&organizationName={{org_name_encoded}}&token={{token}}" - clicktracking=off target="_blank" style="color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #3c8dbc; border-color: #3c8dbc; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> - Join Organization Now - </a> - </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"> - If you do not wish to join this organization, you can safely ignore this email. - </td> - </tr> -</table> -{{> email/email_footer }} -\ No newline at end of file diff --git a/src/static/templates/email/send_single_org_removed_from_org.hbs b/src/static/templates/email/send_single_org_removed_from_org.hbs @@ -1,4 +0,0 @@ -You have been removed from {{{org_name}}} -<!----------------> -Your user account has been removed from the *{{org_name}}* organization because you are a part of another organization. The {{org_name}} organization has enabled a policy that prevents users from being a part of multiple organizations. Before you can re-join this organization you need to leave all other organizations or join with a different account. -{{> email/email_footer_text }} diff --git a/src/static/templates/email/send_single_org_removed_from_org.html.hbs b/src/static/templates/email/send_single_org_removed_from_org.html.hbs @@ -1,11 +0,0 @@ -You have been 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"> - Your user account has been removed from the <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> organization because you are a part of another organization. The {{org_name}} organization has enabled a policy that prevents users from being a part of multiple organizations. Before you can re-join this organization you need to leave all other organizations or join with a different account. - </td> - </tr> -</table> -{{> email/email_footer }} diff --git a/src/static/templates/email/smtp_test.hbs b/src/static/templates/email/smtp_test.hbs @@ -1,6 +0,0 @@ -Vaultwarden SMTP Test -<!----------------> -This is a test email to verify the SMTP configuration for {{url}}. - -When you can read this email it is probably configured correctly. -{{> email/email_footer_text }} -\ No newline at end of file diff --git a/src/static/templates/email/smtp_test.html.hbs b/src/static/templates/email/smtp_test.html.hbs @@ -1,16 +0,0 @@ -Vaultwarden SMTP Test -<!----------------> -{{> 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"> - This is a test email to verify the SMTP configuration for <a href="{{url}}">{{url}}</a>. - </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"> - When you can read this email it is probably configured correctly. - </td> - </tr> -</table> -{{> email/email_footer }} -\ No newline at end of file diff --git a/src/static/templates/email/twofactor_email.hbs b/src/static/templates/email/twofactor_email.hbs @@ -1,6 +0,0 @@ -Vaultwarden Login Verification Code -<!----------------> -Your two-step verification code is: {{token}} - -Use this code to complete logging in with Vaultwarden. -{{> email/email_footer_text }} diff --git a/src/static/templates/email/twofactor_email.html.hbs b/src/static/templates/email/twofactor_email.html.hbs @@ -1,16 +0,0 @@ -Vaultwarden Login Verification Code -<!----------------> -{{> 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;" valign="top"> - Your two-step verification code is: <b>{{token}}</b> - </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" 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;" valign="top"> - Use this code to complete logging in with Vaultwarden. - </td> - </tr> -</table> -{{> email/email_footer }} diff --git a/src/static/templates/email/verify_email.hbs b/src/static/templates/email/verify_email.hbs @@ -1,8 +0,0 @@ -Verify Your Email -<!----------------> -Verify this email address for your account by clicking the link below. - -Verify Email Address Now: {{url}}/#/verify-email/?userId={{user_id}}&token={{token}} - -If you did not request to verify your account, you can safely ignore this email. -{{> email/email_footer_text }} -\ No newline at end of file diff --git a/src/static/templates/email/verify_email.html.hbs b/src/static/templates/email/verify_email.html.hbs @@ -1,24 +0,0 @@ -Verify Your Email -<!----------------> -{{> 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"> - Verify this email address for your account by clicking the link below. - </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" 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"> - <a href="{{url}}/#/verify-email/?userId={{user_id}}&token={{token}}" - clicktracking=off target="_blank" style="color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #3c8dbc; border-color: #3c8dbc; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> - Verify Email Address Now - </a> - </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"> - If you did not request to verify your account, you can safely ignore this email. - </td> - </tr> -</table> -{{> email/email_footer }} -\ No newline at end of file diff --git a/src/static/templates/email/welcome.hbs b/src/static/templates/email/welcome.hbs @@ -1,6 +0,0 @@ -Welcome -<!----------------> -Thank you for creating an account at {{url}}. You may now log in with your new account. - -If you did not request to create an account, you can safely ignore this email. -{{> email/email_footer_text }} -\ No newline at end of file diff --git a/src/static/templates/email/welcome.html.hbs b/src/static/templates/email/welcome.html.hbs @@ -1,16 +0,0 @@ -Welcome -<!----------------> -{{> 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"> - Thank you for creating an account at <a href="{{url}}/">{{url}}</a>. You may now log in with your new account. - </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"> - If you did not request to create an account, you can safely ignore this email. - </td> - </tr> -</table> -{{> email/email_footer }} -\ No newline at end of file diff --git a/src/static/templates/email/welcome_must_verify.hbs b/src/static/templates/email/welcome_must_verify.hbs @@ -1,8 +0,0 @@ -Welcome -<!----------------> -Thank you for creating an account at {{url}}. Before you can login with your new account, you must verify this email address by clicking the link below. - -Verify Email Address Now: {{url}}/#/verify-email/?userId={{user_id}}&token={{token}} - -If you did not request to create an account, you can safely ignore this email. -{{> email/email_footer_text }} -\ No newline at end of file diff --git a/src/static/templates/email/welcome_must_verify.html.hbs b/src/static/templates/email/welcome_must_verify.html.hbs @@ -1,24 +0,0 @@ -Welcome -<!----------------> -{{> 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"> - Thank you for creating an account at <a href="{{url}}/">{{url}}</a>. Before you can login with your new account, you must verify this email address by clicking the link below. - </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" 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"> - <a href="{{url}}/#/verify-email/?userId={{user_id}}&token={{token}}" - clicktracking=off target="_blank" style="color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #3c8dbc; border-color: #3c8dbc; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> - Verify Email Address Now - </a> - </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"> - If you did not request to create an account, you can safely ignore this email. - </td> - </tr> -</table> -{{> email/email_footer }} -\ No newline at end of file diff --git a/src/util.rs b/src/util.rs @@ -1,8 +1,4 @@ -// -// Web Headers and caching -// -use std::{io::Cursor, ops::Deref}; - +use crate::config; use rocket::{ fairing::{Fairing, Info, Kind}, http::{ContentType, Header, HeaderMap, Method, Status}, @@ -10,14 +6,12 @@ use rocket::{ response::{self, Responder}, Request, Response, }; - +use std::{io::Cursor, ops::Deref}; use tokio::{ runtime::Handle, time::{sleep, Duration}, }; -use crate::CONFIG; - pub struct AppHeaders(); #[rocket::async_trait] @@ -103,8 +97,8 @@ impl Fairing for AppHeaders { https://api.forwardemail.net \ ;\ ", - icon_service_csp = CONFIG._icon_service_csp(), - allowed_iframe_ancestors = CONFIG.allowed_iframe_ancestors() + icon_service_csp = "", + allowed_iframe_ancestors = "" ); res.set_raw_header("Content-Security-Policy", csp); res.set_raw_header("X-Frame-Options", "SAMEORIGIN"); @@ -134,7 +128,7 @@ impl Cors { // If a match exists, return it. Otherwise, return None. fn get_allowed_origin(headers: &HeaderMap<'_>) -> Option<String> { let origin = Cors::get_header(headers, "Origin"); - let domain_origin = CONFIG.domain_origin(); + let domain_origin = config::get_config().domain_origin(); let safari_extension_origin = "file://"; if origin == domain_origin || origin == safari_extension_origin { Some(origin) @@ -363,52 +357,7 @@ where None } } - -// -// Env methods -// - -use std::env; - -pub fn get_env_str_value(key: &str) -> Option<String> { - let key_file = format!("{key}_FILE"); - let value_from_env = env::var(key); - let value_file = env::var(&key_file); - - match (value_from_env, value_file) { - (Ok(_), Ok(_)) => panic!("You should not define both {key} and {key_file}!"), - (Ok(v_env), Err(_)) => Some(v_env), - (Err(_), Ok(v_file)) => match fs::read_to_string(v_file) { - Ok(content) => Some(content.trim().to_string()), - Err(e) => panic!("Failed to load {key}: {e:?}"), - }, - _ => None, - } -} - -pub fn get_env<V>(key: &str) -> Option<V> -where - V: FromStr, -{ - try_parse_string(get_env_str_value(key)) -} - -pub fn get_env_bool(key: &str) -> Option<bool> { - const TRUE_VALUES: &[&str] = &["true", "t", "yes", "y", "1"]; - const FALSE_VALUES: &[&str] = &["false", "f", "no", "n", "0"]; - - match get_env_str_value(key) { - Some(val) if TRUE_VALUES.contains(&val.to_lowercase().as_ref()) => Some(true), - Some(val) if FALSE_VALUES.contains(&val.to_lowercase().as_ref()) => Some(false), - _ => None, - } -} - -// -// Date util methods -// - -use chrono::{DateTime, Local, NaiveDateTime, TimeZone}; +use chrono::{DateTime, Local, NaiveDateTime}; // Format used by Bitwarden API const DATETIME_FORMAT: &str = "%Y-%m-%dT%H:%M:%S%.6fZ"; @@ -419,33 +368,6 @@ pub fn format_date(dt: &NaiveDateTime) -> String { dt.format(DATETIME_FORMAT).to_string() } -/// Formats a `DateTime<Local>` using the specified format string. -/// -/// For a `DateTime<Local>`, the `%Z` specifier normally formats as the -/// time zone's UTC offset (e.g., `+00:00`). In this function, if the -/// `TZ` environment variable is set, then `%Z` instead formats as the -/// abbreviation for that time zone (e.g., `UTC`). -pub fn format_datetime_local(dt: &DateTime<Local>, fmt: &str) -> String { - // Try parsing the `TZ` environment variable to enable formatting `%Z` as - // a time zone abbreviation. - if let Ok(tz) = env::var("TZ") { - if let Ok(tz) = tz.parse::<chrono_tz::Tz>() { - return dt.with_timezone(&tz).format(fmt).to_string(); - } - } - - // Otherwise, fall back to formatting `%Z` as a UTC offset. - dt.format(fmt).to_string() -} - -/// Formats a UTC-offset `NaiveDateTime` as a datetime in the local time zone. -/// -/// This function basically converts the `NaiveDateTime` to a `DateTime<Local>`, -/// and then calls [format_datetime_local](crate::util::format_datetime_local). -pub fn format_naive_datetime_local(dt: &NaiveDateTime, fmt: &str) -> String { - format_datetime_local(&Local.from_utc_datetime(dt), fmt) -} - /// Formats a `DateTime<Local>` as required for HTTP /// /// https://httpwg.org/specs/rfc7231.html#http.date