Compare commits

..

12 Commits

Author SHA1 Message Date
Mathijs van Veluw
cdfdc6ff4f Fix Org Import duplicate collections (#5200)
This fixes an issue with collections be duplicated same as was an issue with folders.
Also made some optimizations by using HashSet where possible and device the Vec/Hash capacity.
And instead of passing objects only use the UUID which was the only value we needed.

Also found an issue with importing a personal export via the Org import where folders are used.
Since Org's do not use folder we needed to clear those out, same as Bitwarden does.

Fixes #5193

Signed-off-by: BlackDex <black.dex@gmail.com>
2024-11-17 21:33:23 +01:00
Daniel García
2393c3f3c0 Support SSH keys on desktop 2024.12 (#5187)
* Support SSH keys on desktop 2024.12

* Document flags in .env.template

* Validate key rotation contents
2024-11-15 18:38:16 +01:00
Daniel García
0d16b38a68 Some more authrequest changes (#5188) 2024-11-15 11:25:51 +01:00
Stefan Melmuk
ff33534c07 don't infer manage permission for groups (#5190)
the web-vault v2024.6.2 currently cannot deal with manage permission so
instead of relying on the org user type this should just default to false
2024-11-13 19:19:19 +01:00
Stefan Melmuk
adb21d5c1a fix password hint check (#5189)
* fix password hint check

don't show password hints if you have disabled the hints with
PASSWORD_HINTS_ALLOWED=false or if you have not configured mail and
opted into showing password hints

* update descriptions for pw hints options
2024-11-12 21:22:25 +01:00
Mathijs van Veluw
e927b8aa5e Remove auth-request deletion (#5184)
2FA is needed to login even when using login-with-device.
If the user didn't saved the 2FA token they still need to provide this.
We deleted the auth-request after validation the request, but before 2FA was triggered.

Removing the deletion of this record from that point as it will get cleaned-up automatically anyways.

Signed-off-by: BlackDex <black.dex@gmail.com>
2024-11-12 15:48:39 +01:00
Mathijs van Veluw
ba48ca68fc fix hibp username encoding and pw hint check (#5180)
* fix hibp username encoding

Signed-off-by: BlackDex <black.dex@gmail.com>

* Fix password-hint check

Signed-off-by: BlackDex <black.dex@gmail.com>

---------

Signed-off-by: BlackDex <black.dex@gmail.com>
2024-11-12 11:09:28 +01:00
Mathijs van Veluw
294b429436 Add dynamic CSS support (#4940)
* Add dynamic CSS support

Together with https://github.com/dani-garcia/bw_web_builds/pull/180 this PR will add support for dynamic CSS changes.

For example, we could hide the register link if signups are not allowed.
In the future show or hide the SSO button depending on if it is enabled or not.

There also is a special `user.vaultwarden.scss` file so that users can add custom CSS without the need to modify the default (static) changes.
This will prevent future changes from not being applied and still have the custom user changes to be added.

Also added a special redirect when someone goes directly to `/index.html` as that might cause issues with loading other scripts and files.

Signed-off-by: BlackDex <black.dex@gmail.com>

* Add versions and fallback to built-in

- Add both Vaultwarden and web-vault versions to the css_options.
- Fallback to the inner templates if rendering or compiling the scss fails.
  This ensures the basics are always working even if someone breaks the templates.

Signed-off-by: BlackDex <black.dex@gmail.com>

* Fix fallback code to actually work

The fallback now works by using an alternative `reg!` macro.
This adds an extra template register which prefixes the template with `fallback_`.

Signed-off-by: BlackDex <black.dex@gmail.com>

* Updated the wiki link in the user template

---------

Signed-off-by: BlackDex <black.dex@gmail.com>
2024-11-11 20:14:04 +01:00
Daniel García
37c14c3c69 More authrequest fixes (#5176) 2024-11-11 20:13:02 +01:00
Mathijs van Veluw
d0581da638 Fix if logic error (#5171)
Fixing a logical error in an if statement where we used `&&` which should have been `||`.

Signed-off-by: BlackDex <black.dex@gmail.com>
2024-11-11 11:50:33 +01:00
Daniel García
38aad4f7be Limit HIBP to authed users 2024-11-10 23:59:06 +01:00
BlackDex
20d9e885bf Update crates and fix several issues
Signed-off-by: BlackDex <black.dex@gmail.com>
2024-11-10 23:56:19 +01:00
30 changed files with 799 additions and 2184 deletions

View File

@@ -280,12 +280,13 @@
## The default for new users. If changed, it will be updated during login for existing users. ## The default for new users. If changed, it will be updated during login for existing users.
# PASSWORD_ITERATIONS=600000 # PASSWORD_ITERATIONS=600000
## Controls whether users can set password hints. This setting applies globally to all users. ## Controls whether users can set or show password hints. This setting applies globally to all users.
# PASSWORD_HINTS_ALLOWED=true # PASSWORD_HINTS_ALLOWED=true
## Controls whether a password hint should be shown directly in the web page if ## 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 ## SMTP service is not configured and password hints are allowed.
## as this provides unauthenticated access to potentially sensitive data. ## Not recommended for publicly-accessible instances because this provides
## unauthenticated access to potentially sensitive data.
# SHOW_PASSWORD_HINT=false # SHOW_PASSWORD_HINT=false
######################### #########################
@@ -349,6 +350,8 @@
## - "browser-fileless-import": Directly import credentials from other providers without a file. ## - "browser-fileless-import": Directly import credentials from other providers without a file.
## - "extension-refresh": Temporarily enable the new extension design until general availability (should be used with the beta Chrome extension) ## - "extension-refresh": Temporarily enable the new extension design until general availability (should be used with the beta Chrome extension)
## - "fido2-vault-credentials": Enable the use of FIDO2 security keys as second factor. ## - "fido2-vault-credentials": Enable the use of FIDO2 security keys as second factor.
## - "ssh-key-vault-item": Enable the creation and use of SSH key vault items. (Needs clients >=2024.12.0)
## - "ssh-agent": Enable SSH agent support on Desktop. (Needs desktop >=2024.12.0)
# EXPERIMENTAL_CLIENT_FEATURE_FLAGS=fido2-vault-credentials # EXPERIMENTAL_CLIENT_FEATURE_FLAGS=fido2-vault-credentials
## Require new device emails. When a user logs in an email is required to be sent. ## Require new device emails. When a user logs in an email is required to be sent.

230
Cargo.lock generated
View File

@@ -55,9 +55,9 @@ dependencies = [
[[package]] [[package]]
name = "allocator-api2" name = "allocator-api2"
version = "0.2.18" version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" checksum = "611cc2ae7d2e242c457e4be7f97036b8ad9ca152b499f53faf99b1ed8fc2553f"
[[package]] [[package]]
name = "android-tzdata" name = "android-tzdata"
@@ -153,9 +153,9 @@ dependencies = [
[[package]] [[package]]
name = "async-io" name = "async-io"
version = "2.3.4" version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "444b0228950ee6501b3568d3c93bf1176a1fdbc3b758dcd9475046d30f4dc7e8" checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059"
dependencies = [ dependencies = [
"async-lock", "async-lock",
"cfg-if", "cfg-if",
@@ -352,9 +352,9 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]] [[package]]
name = "bigdecimal" name = "bigdecimal"
version = "0.4.5" version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d712318a27c7150326677b321a5fa91b55f6d9034ffd67f20319e147d40cee" checksum = "8f850665a0385e070b64c38d2354e6c104c8479c59868d1e48a0c13ee2c7a1c1"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"libm", "libm",
@@ -459,9 +459,9 @@ checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da"
[[package]] [[package]]
name = "cached" name = "cached"
version = "0.53.1" version = "0.54.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4d73155ae6b28cf5de4cfc29aeb02b8a1c6dab883cb015d15cd514e42766846" checksum = "9718806c4a2fe9e8a56fd736f97b340dd10ed1be8ed733ed50449f351dc33cae"
dependencies = [ dependencies = [
"ahash", "ahash",
"async-trait", "async-trait",
@@ -495,9 +495,9 @@ checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.1.31" version = "1.1.37"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f" checksum = "40545c26d092346d8a8dab71ee48e7685a7a9cba76e634790c215b41a4a7b4cf"
dependencies = [ dependencies = [
"shlex", "shlex",
] ]
@@ -552,6 +552,12 @@ dependencies = [
"stacker", "stacker",
] ]
[[package]]
name = "codemap"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e769b5c8c8283982a987c6e948e540254f1058d5a74b8794914d4ef5fc2a24"
[[package]] [[package]]
name = "concurrent-queue" name = "concurrent-queue"
version = "2.5.0" version = "2.5.0"
@@ -574,12 +580,13 @@ dependencies = [
[[package]] [[package]]
name = "cookie_store" name = "cookie_store"
version = "0.21.0" version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4934e6b7e8419148b6ef56950d277af8561060b56afd59e2aadf98b59fce6baa" checksum = "2eac901828f88a5241ee0600950ab981148a18f2f756900ffba1b125ca6a3ef9"
dependencies = [ dependencies = [
"cookie", "cookie",
"idna 0.5.0", "document-features",
"idna 1.0.3",
"log", "log",
"publicsuffix", "publicsuffix",
"serde", "serde",
@@ -842,6 +849,15 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "document-features"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb6969eaabd2421f8a2775cfd2471a2b634372b4a25d41e3bd647b79912850a0"
dependencies = [
"litrs",
]
[[package]] [[package]]
name = "dotenvy" name = "dotenvy"
version = "0.15.7" version = "0.15.7"
@@ -962,9 +978,9 @@ dependencies = [
[[package]] [[package]]
name = "fastrand" name = "fastrand"
version = "2.1.1" version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4"
[[package]] [[package]]
name = "fern" name = "fern"
@@ -1082,9 +1098,9 @@ checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]] [[package]]
name = "futures-lite" name = "futures-lite"
version = "2.3.0" version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1"
dependencies = [ dependencies = [
"fastrand", "fastrand",
"futures-core", "futures-core",
@@ -1221,6 +1237,19 @@ dependencies = [
"spinning_top", "spinning_top",
] ]
[[package]]
name = "grass_compiler"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d9e3df7f0222ce5184154973d247c591d9aadc28ce7a73c6cd31100c9facff6"
dependencies = [
"codemap",
"indexmap",
"lasso",
"once_cell",
"phf",
]
[[package]] [[package]]
name = "h2" name = "h2"
version = "0.3.26" version = "0.3.26"
@@ -1267,11 +1296,12 @@ checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403"
[[package]] [[package]]
name = "handlebars" name = "handlebars"
version = "6.1.0" version = "6.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce25b617d1375ef96eeb920ae717e3da34a02fc979fe632c75128350f9e1f74a" checksum = "fd4ccde012831f9a071a637b0d4e31df31c0f6c525784b35ae76a9ac6bc1e315"
dependencies = [ dependencies = [
"log", "log",
"num-order",
"pest", "pest",
"pest_derive", "pest_derive",
"serde", "serde",
@@ -1292,9 +1322,9 @@ dependencies = [
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.15.0" version = "0.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3"
[[package]] [[package]]
name = "heck" name = "heck"
@@ -1401,9 +1431,9 @@ dependencies = [
[[package]] [[package]]
name = "html5gum" name = "html5gum"
version = "0.5.7" version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c4e556171a058ba117bbe88b059fb37b6289023e007d2903ea6dca3a3cbff14" checksum = "91b361633dcc40096d01de35ed535b6089be91880be47b6fd8f560497af7f716"
dependencies = [ dependencies = [
"jetscii", "jetscii",
] ]
@@ -1530,7 +1560,7 @@ dependencies = [
"http 1.1.0", "http 1.1.0",
"hyper 1.5.0", "hyper 1.5.0",
"hyper-util", "hyper-util",
"rustls 0.23.15", "rustls 0.23.16",
"rustls-pki-types", "rustls-pki-types",
"tokio", "tokio",
"tokio-rustls 0.26.0", "tokio-rustls 0.26.0",
@@ -1568,9 +1598,9 @@ dependencies = [
[[package]] [[package]]
name = "hyper-util" name = "hyper-util"
version = "0.1.9" version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b" checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures-channel", "futures-channel",
@@ -1754,24 +1784,23 @@ dependencies = [
[[package]] [[package]]
name = "idna" name = "idna"
version = "0.5.0" version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
dependencies = [ dependencies = [
"unicode-bidi", "idna_adapter",
"unicode-normalization", "smallvec",
"utf8_iter",
] ]
[[package]] [[package]]
name = "idna" name = "idna_adapter"
version = "1.0.2" version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd69211b9b519e98303c015e21a007e293db403b6c85b9b124e133d25e242cdd" checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71"
dependencies = [ dependencies = [
"icu_normalizer", "icu_normalizer",
"icu_properties", "icu_properties",
"smallvec",
"utf8_iter",
] ]
[[package]] [[package]]
@@ -1781,7 +1810,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown 0.15.0", "hashbrown 0.15.1",
"serde", "serde",
] ]
@@ -1876,6 +1905,15 @@ dependencies = [
"log", "log",
] ]
[[package]]
name = "lasso"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e14eda50a3494b3bf7b9ce51c52434a761e383d7238ce1dd5dcec2fbc13e9fb"
dependencies = [
"hashbrown 0.14.5",
]
[[package]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.5.0" version = "1.5.0"
@@ -1899,7 +1937,7 @@ dependencies = [
"futures-util", "futures-util",
"hostname 0.4.0", "hostname 0.4.0",
"httpdate", "httpdate",
"idna 1.0.2", "idna 1.0.3",
"mime", "mime",
"native-tls", "native-tls",
"nom", "nom",
@@ -1915,15 +1953,15 @@ dependencies = [
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.161" version = "0.2.162"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398"
[[package]] [[package]]
name = "libm" name = "libm"
version = "0.2.8" version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
[[package]] [[package]]
name = "libmimalloc-sys" name = "libmimalloc-sys"
@@ -1964,6 +2002,12 @@ version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704"
[[package]]
name = "litrs"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5"
[[package]] [[package]]
name = "lock_api" name = "lock_api"
version = "0.4.12" version = "0.4.12"
@@ -2205,6 +2249,21 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[package]]
name = "num-modular"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17bb261bf36fa7d83f4c294f834e91256769097b3cb505d44831e0a179ac647f"
[[package]]
name = "num-order"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "537b596b97c40fcf8056d153049eb22f481c17ebce72a513ec9286e4986d1bb6"
dependencies = [
"num-modular",
]
[[package]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.19" version = "0.2.19"
@@ -2453,6 +2512,7 @@ version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
dependencies = [ dependencies = [
"phf_macros",
"phf_shared", "phf_shared",
] ]
@@ -2476,6 +2536,19 @@ dependencies = [
"rand", "rand",
] ]
[[package]]
name = "phf_macros"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b"
dependencies = [
"phf_generator",
"phf_shared",
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "phf_shared" name = "phf_shared"
version = "0.11.2" version = "0.11.2"
@@ -2493,9 +2566,9 @@ checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.14" version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff"
[[package]] [[package]]
name = "pin-utils" name = "pin-utils"
@@ -2522,9 +2595,9 @@ checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
[[package]] [[package]]
name = "polling" name = "polling"
version = "3.7.3" version = "3.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511" checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"concurrent-queue", "concurrent-queue",
@@ -2729,9 +2802,9 @@ dependencies = [
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.11.0" version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@@ -2824,9 +2897,9 @@ dependencies = [
[[package]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.12.8" version = "0.12.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f713147fbe92361e52392c73b8c9e48c04c6625bce969ef54dc901e58e042a7b" checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f"
dependencies = [ dependencies = [
"async-compression", "async-compression",
"base64 0.22.1", "base64 0.22.1",
@@ -3041,9 +3114,9 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "0.38.37" version = "0.38.40"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" checksum = "99e4ea3e1cdc4b559b8e5650f9c8e5998e3e5c1343b4eaf034565f32318d63c0"
dependencies = [ dependencies = [
"bitflags 2.6.0", "bitflags 2.6.0",
"errno", "errno",
@@ -3066,9 +3139,9 @@ dependencies = [
[[package]] [[package]]
name = "rustls" name = "rustls"
version = "0.23.15" version = "0.23.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fbb44d7acc4e873d613422379f69f237a1b141928c02f6bc6ccfddddc2d7993" checksum = "eee87ff5d9b36712a58574e12e9f0ea80f915a5b0ac518d322b24a465617925e"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"rustls-pki-types", "rustls-pki-types",
@@ -3198,9 +3271,9 @@ dependencies = [
[[package]] [[package]]
name = "security-framework-sys" name = "security-framework-sys"
version = "2.12.0" version = "2.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6" checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2"
dependencies = [ dependencies = [
"core-foundation-sys", "core-foundation-sys",
"libc", "libc",
@@ -3214,9 +3287,9 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.213" version = "1.0.214"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ea7893ff5e2466df8d720bb615088341b295f849602c6956047f8f80f0e9bc1" checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
@@ -3233,9 +3306,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.213" version = "1.0.214"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e85ad2009c50b58e87caa8cd6dac16bdf511bbfb7af6c33df902396aa480fa5" checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -3440,9 +3513,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.85" version = "2.0.87"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -3532,9 +3605,9 @@ dependencies = [
[[package]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.13.0" version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"fastrand", "fastrand",
@@ -3545,18 +3618,18 @@ dependencies = [
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.65" version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl",
] ]
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "1.0.65" version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -3642,9 +3715,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.41.0" version = "1.41.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "145f3413504347a2be84393cc8a7d2fb4d863b375909ea59f2158261aa258bbb" checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33"
dependencies = [ dependencies = [
"backtrace", "backtrace",
"bytes", "bytes",
@@ -3695,7 +3768,7 @@ version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
dependencies = [ dependencies = [
"rustls 0.23.15", "rustls 0.23.16",
"rustls-pki-types", "rustls-pki-types",
"tokio", "tokio",
] ]
@@ -3953,12 +4026,12 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]] [[package]]
name = "url" name = "url"
version = "2.5.2" version = "2.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada"
dependencies = [ dependencies = [
"form_urlencoded", "form_urlencoded",
"idna 0.5.0", "idna 1.0.3",
"percent-encoding", "percent-encoding",
"serde", "serde",
] ]
@@ -4025,6 +4098,7 @@ dependencies = [
"fern", "fern",
"futures", "futures",
"governor", "governor",
"grass_compiler",
"handlebars", "handlebars",
"hickory-resolver", "hickory-resolver",
"html5gum", "html5gum",
@@ -4043,7 +4117,7 @@ dependencies = [
"pico-args", "pico-args",
"rand", "rand",
"regex", "regex",
"reqwest 0.12.8", "reqwest 0.12.9",
"ring", "ring",
"rmpv", "rmpv",
"rocket", "rocket",
@@ -4170,9 +4244,9 @@ checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d"
[[package]] [[package]]
name = "wasm-streams" name = "wasm-streams"
version = "0.4.1" version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e072d4e72f700fb3443d8fe94a39315df013eef1104903cdb0a2abd322bbecd" checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
dependencies = [ dependencies = [
"futures-util", "futures-util",
"js-sys", "js-sys",
@@ -4222,9 +4296,9 @@ dependencies = [
[[package]] [[package]]
name = "which" name = "which"
version = "6.0.3" version = "7.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ee928febd44d98f2f459a4a79bd4d928591333a494a10a868418ac1b39cf1f" checksum = "c9cad3279ade7346b96e38731a641d7343dd6a53d55083dd54eadfa5a1b38c6b"
dependencies = [ dependencies = [
"either", "either",
"home", "home",

View File

@@ -53,7 +53,7 @@ once_cell = "1.20.2"
# Numerical libraries # Numerical libraries
num-traits = "0.2.19" num-traits = "0.2.19"
num-derive = "0.4.2" num-derive = "0.4.2"
bigdecimal = "0.4.5" bigdecimal = "0.4.6"
# Web framework # Web framework
rocket = { version = "0.5.1", features = ["tls", "json"], default-features = false } rocket = { version = "0.5.1", features = ["tls", "json"], default-features = false }
@@ -67,10 +67,10 @@ dashmap = "6.1.0"
# Async futures # Async futures
futures = "0.3.31" futures = "0.3.31"
tokio = { version = "1.41.0", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] } tokio = { version = "1.41.1", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] }
# A generic serialization/deserialization framework # A generic serialization/deserialization framework
serde = { version = "1.0.213", features = ["derive"] } serde = { version = "1.0.214", features = ["derive"] }
serde_json = "1.0.132" serde_json = "1.0.132"
# A safe, extensible ORM and Query builder # A safe, extensible ORM and Query builder
@@ -112,7 +112,7 @@ yubico = { version = "0.11.0", features = ["online-tokio"], default-features = f
webauthn-rs = "0.3.2" webauthn-rs = "0.3.2"
# Handling of URL's for WebAuthn and favicons # Handling of URL's for WebAuthn and favicons
url = "2.5.2" url = "2.5.3"
# Email libraries # Email libraries
lettre = { version = "0.11.10", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "tokio1-native-tls", "hostname", "tracing", "tokio1"], default-features = false } lettre = { version = "0.11.10", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "tokio1-native-tls", "hostname", "tracing", "tokio1"], default-features = false }
@@ -120,24 +120,24 @@ percent-encoding = "2.3.1" # URL encoding library used for URL's in the emails
email_address = "0.2.9" email_address = "0.2.9"
# HTML Template library # HTML Template library
handlebars = { version = "6.1.0", features = ["dir_source"] } handlebars = { version = "6.2.0", features = ["dir_source"] }
# HTTP client (Used for favicons, version check, DUO and HIBP API) # HTTP client (Used for favicons, version check, DUO and HIBP API)
reqwest = { version = "0.12.8", features = ["native-tls-alpn", "stream", "json", "gzip", "brotli", "socks", "cookies"] } reqwest = { version = "0.12.9", features = ["native-tls-alpn", "stream", "json", "gzip", "brotli", "socks", "cookies"] }
hickory-resolver = "0.24.1" hickory-resolver = "0.24.1"
# Favicon extraction libraries # Favicon extraction libraries
html5gum = "0.5.7" html5gum = "0.6.1"
regex = { version = "1.11.0", features = ["std", "perf", "unicode-perl"], default-features = false } regex = { version = "1.11.1", features = ["std", "perf", "unicode-perl"], default-features = false }
data-url = "0.3.1" data-url = "0.3.1"
bytes = "1.8.0" bytes = "1.8.0"
# Cache function results (Used for version check and favicon fetching) # Cache function results (Used for version check and favicon fetching)
cached = { version = "0.53.1", features = ["async"] } cached = { version = "0.54.0", features = ["async"] }
# Used for custom short lived cookie jar during favicon extraction # Used for custom short lived cookie jar during favicon extraction
cookie = "0.18.1" cookie = "0.18.1"
cookie_store = "0.21.0" cookie_store = "0.21.1"
# Used by U2F, JWT and PostgreSQL # Used by U2F, JWT and PostgreSQL
openssl = "0.10.68" openssl = "0.10.68"
@@ -155,7 +155,7 @@ semver = "1.0.23"
# Allow overriding the default memory allocator # Allow overriding the default memory allocator
# Mainly used for the musl builds, since the default musl malloc is very slow # Mainly used for the musl builds, since the default musl malloc is very slow
mimalloc = { version = "0.1.43", features = ["secure"], default-features = false, optional = true } mimalloc = { version = "0.1.43", features = ["secure"], default-features = false, optional = true }
which = "6.0.3" which = "7.0.0"
# Argon2 library with support for the PHC format # Argon2 library with support for the PHC format
argon2 = "0.5.3" argon2 = "0.5.3"
@@ -163,6 +163,9 @@ argon2 = "0.5.3"
# Reading a password from the cli for generating the Argon2id ADMIN_TOKEN # Reading a password from the cli for generating the Argon2id ADMIN_TOKEN
rpassword = "7.3.1" rpassword = "7.3.1"
# Loading a dynamic CSS Stylesheet
grass_compiler = { version = "0.13.4", default-features = false }
# Strip debuginfo from the release builds # Strip debuginfo from the release builds
# The symbols are the provide better panic traces # The symbols are the provide better panic traces
# Also enable fat LTO and use 1 codegen unit for optimizations # Also enable fat LTO and use 1 codegen unit for optimizations

View File

@@ -1,2 +0,0 @@
[workspace.metadata.dylint]
libraries = [{ path = "dylints/*" }]

View File

@@ -1,7 +0,0 @@
# How to run Lints
```sh
cargo install cargo-dylint dylint-link
RUSTFLAGS="-Aunreachable_patterns" cargo dylint --all -- --features sqlite
```

View File

@@ -1,2 +0,0 @@
[target.'cfg(all())']
linker = "dylint-link"

View File

@@ -1 +0,0 @@
/target

File diff suppressed because it is too large Load Diff

View File

@@ -1,20 +0,0 @@
[package]
name = "non_authenticated_routes"
version = "0.1.0"
authors = ["authors go here"]
description = "description goes here"
edition = "2021"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
clippy_utils = { git = "https://github.com/rust-lang/rust-clippy", rev = "4f0e46b74dbc8441daf084b6f141a7fe414672a2" }
dylint_linting = "3.2.1"
[dev-dependencies]
dylint_testing = "3.2.1"
[package.metadata.rust-analyzer]
rustc_private = true

View File

@@ -1,3 +0,0 @@
[toolchain]
channel = "nightly-2024-11-09"
components = ["llvm-tools-preview", "rustc-dev"]

View File

@@ -1,167 +0,0 @@
#![feature(rustc_private)]
#![feature(let_chains)]
extern crate rustc_arena;
extern crate rustc_ast;
extern crate rustc_ast_pretty;
extern crate rustc_attr;
extern crate rustc_data_structures;
extern crate rustc_errors;
extern crate rustc_hir;
extern crate rustc_hir_pretty;
extern crate rustc_index;
extern crate rustc_infer;
extern crate rustc_lexer;
extern crate rustc_middle;
extern crate rustc_mir_dataflow;
extern crate rustc_parse;
extern crate rustc_span;
extern crate rustc_target;
extern crate rustc_trait_selection;
use clippy_utils::diagnostics::span_lint;
use rustc_hir::{def_id::DefId, Item, ItemKind, QPath, TyKind};
use rustc_lint::{LateContext, LateLintPass};
use rustc_span::{symbol::Ident, Span, Symbol};
dylint_linting::impl_late_lint! {
/// ### What it does
///
/// ### Why is this bad?
///
/// ### Known problems
/// Remove if none.
///
/// ### Example
/// ```rust
/// // example code where a warning is issued
/// ```
/// Use instead:
/// ```rust
/// // example code that does not raise a warning
/// ```
pub NON_AUTHENTICATED_ROUTES,
Warn,
"description goes here",
NonAuthenticatedRoutes::default()
}
#[derive(Default)]
pub struct NonAuthenticatedRoutes {
last_function_item: Option<(Ident, Span, bool)>,
}
// Collect all the attribute macros that are applied to the given span
fn attr_def_ids(mut span: rustc_span::Span) -> Vec<(DefId, Symbol, Option<DefId>)> {
use rustc_span::hygiene::{walk_chain, ExpnKind, MacroKind};
use rustc_span::{ExpnData, SyntaxContext};
let mut def_ids = Vec::new();
while span.ctxt() != SyntaxContext::root() {
if let ExpnData {
kind: ExpnKind::Macro(MacroKind::Attr, macro_symbol),
macro_def_id: Some(def_id),
parent_module,
..
} = span.ctxt().outer_expn_data()
{
def_ids.push((def_id, macro_symbol, parent_module));
}
span = walk_chain(span, SyntaxContext::root());
}
def_ids
}
const ROCKET_MACRO_EXCEPTIONS: [(&str, &str); 1] = [("rocket::catch", "catch")];
const VALID_AUTH_HEADERS: [&str; 6] = [
"auth::Headers",
"auth::OrgHeaders",
"auth::AdminHeaders",
"auth::ManagerHeaders",
"auth::ManagerHeadersLoose",
"auth::OwnerHeaders",
];
impl<'tcx> LateLintPass<'tcx> for NonAuthenticatedRoutes {
fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item) {
if let ItemKind::Fn(sig, ..) = item.kind {
let mut has_auth_headers = false;
for input in sig.decl.inputs {
let TyKind::Path(QPath::Resolved(_, path)) = input.kind else {
continue;
};
for seg in path.segments {
if let Some(def_id) = seg.res.opt_def_id() {
let def = cx.tcx.def_path_str(def_id);
if VALID_AUTH_HEADERS.contains(&def.as_str()) {
has_auth_headers = true;
}
}
}
}
self.last_function_item = Some((item.ident, sig.span, has_auth_headers));
return;
}
let ItemKind::Struct(_data, _generics) = item.kind else {
return;
};
let def_ids = attr_def_ids(item.span);
let mut is_rocket_route = false;
for (def_id, sym, parent) in &def_ids {
let def_id = cx.tcx.def_path_str(*def_id);
let sym = sym.as_str();
let parent = parent.map(|parent| cx.tcx.def_path_str(parent));
if ROCKET_MACRO_EXCEPTIONS.contains(&(&def_id, sym)) {
is_rocket_route = false;
break;
}
if def_id.starts_with("rocket::") || parent.as_deref() == Some("rocket_codegen") {
is_rocket_route = true;
break;
}
}
if !is_rocket_route {
return;
}
let Some((func_ident, func_span, has_auth_headers)) = self.last_function_item.take() else {
span_lint(cx, NON_AUTHENTICATED_ROUTES, item.span, "No function found before the expanded route");
return;
};
if func_ident != item.ident {
span_lint(
cx,
NON_AUTHENTICATED_ROUTES,
item.span,
"The function before the expanded route does not match the route",
);
return;
}
if !has_auth_headers {
span_lint(
cx,
NON_AUTHENTICATED_ROUTES,
func_span,
"This Rocket route does not have any authentication headers",
);
}
}
}
#[test]
fn ui() {
dylint_testing::ui_test(env!("CARGO_PKG_NAME"), "ui");
}

View File

@@ -1 +0,0 @@
fn main() {}

View File

@@ -1,5 +1,7 @@
use std::collections::HashSet;
use crate::db::DbPool; use crate::db::DbPool;
use chrono::{SecondsFormat, Utc}; use chrono::Utc;
use rocket::serde::json::Json; use rocket::serde::json::Json;
use serde_json::Value; use serde_json::Value;
@@ -13,7 +15,7 @@ use crate::{
crypto, crypto,
db::{models::*, DbConn}, db::{models::*, DbConn},
mail, mail,
util::NumberOrString, util::{format_date, NumberOrString},
CONFIG, CONFIG,
}; };
@@ -477,6 +479,60 @@ struct KeyData {
private_key: String, private_key: String,
} }
fn validate_keydata(
data: &KeyData,
existing_ciphers: &[Cipher],
existing_folders: &[Folder],
existing_emergency_access: &[EmergencyAccess],
existing_user_orgs: &[UserOrganization],
existing_sends: &[Send],
) -> EmptyResult {
// Check that we're correctly rotating all the user's ciphers
let existing_cipher_ids = existing_ciphers.iter().map(|c| c.uuid.as_str()).collect::<HashSet<_>>();
let provided_cipher_ids = data
.ciphers
.iter()
.filter(|c| c.organization_id.is_none())
.filter_map(|c| c.id.as_deref())
.collect::<HashSet<_>>();
if !provided_cipher_ids.is_superset(&existing_cipher_ids) {
err!("All existing ciphers must be included in the rotation")
}
// Check that we're correctly rotating all the user's folders
let existing_folder_ids = existing_folders.iter().map(|f| f.uuid.as_str()).collect::<HashSet<_>>();
let provided_folder_ids = data.folders.iter().filter_map(|f| f.id.as_deref()).collect::<HashSet<_>>();
if !provided_folder_ids.is_superset(&existing_folder_ids) {
err!("All existing folders must be included in the rotation")
}
// Check that we're correctly rotating all the user's emergency access keys
let existing_emergency_access_ids =
existing_emergency_access.iter().map(|ea| ea.uuid.as_str()).collect::<HashSet<_>>();
let provided_emergency_access_ids =
data.emergency_access_keys.iter().map(|ea| ea.id.as_str()).collect::<HashSet<_>>();
if !provided_emergency_access_ids.is_superset(&existing_emergency_access_ids) {
err!("All existing emergency access keys must be included in the rotation")
}
// Check that we're correctly rotating all the user's reset password keys
let existing_reset_password_ids = existing_user_orgs.iter().map(|uo| uo.org_uuid.as_str()).collect::<HashSet<_>>();
let provided_reset_password_ids =
data.reset_password_keys.iter().map(|rp| rp.organization_id.as_str()).collect::<HashSet<_>>();
if !provided_reset_password_ids.is_superset(&existing_reset_password_ids) {
err!("All existing reset password keys must be included in the rotation")
}
// Check that we're correctly rotating all the user's sends
let existing_send_ids = existing_sends.iter().map(|s| s.uuid.as_str()).collect::<HashSet<_>>();
let provided_send_ids = data.sends.iter().filter_map(|s| s.id.as_deref()).collect::<HashSet<_>>();
if !provided_send_ids.is_superset(&existing_send_ids) {
err!("All existing sends must be included in the rotation")
}
Ok(())
}
#[post("/accounts/key", data = "<data>")] #[post("/accounts/key", data = "<data>")]
async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
// TODO: See if we can wrap everything within a SQL Transaction. If something fails it should revert everything. // TODO: See if we can wrap everything within a SQL Transaction. If something fails it should revert everything.
@@ -494,20 +550,35 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn,
let user_uuid = &headers.user.uuid; let user_uuid = &headers.user.uuid;
// TODO: Ideally we'd do everything after this point in a single transaction.
let mut existing_ciphers = Cipher::find_owned_by_user(user_uuid, &mut conn).await;
let mut existing_folders = Folder::find_by_user(user_uuid, &mut conn).await;
let mut existing_emergency_access = EmergencyAccess::find_all_by_grantor_uuid(user_uuid, &mut conn).await;
let mut existing_user_orgs = UserOrganization::find_by_user(user_uuid, &mut conn).await;
// We only rotate the reset password key if it is set.
existing_user_orgs.retain(|uo| uo.reset_password_key.is_some());
let mut existing_sends = Send::find_by_user(user_uuid, &mut conn).await;
validate_keydata(
&data,
&existing_ciphers,
&existing_folders,
&existing_emergency_access,
&existing_user_orgs,
&existing_sends,
)?;
// Update folder data // Update folder data
for folder_data in data.folders { for folder_data in data.folders {
// Skip `null` folder id entries. // Skip `null` folder id entries.
// See: https://github.com/bitwarden/clients/issues/8453 // See: https://github.com/bitwarden/clients/issues/8453
if let Some(folder_id) = folder_data.id { if let Some(folder_id) = folder_data.id {
let mut saved_folder = match Folder::find_by_uuid(&folder_id, &mut conn).await { let saved_folder = match existing_folders.iter_mut().find(|f| f.uuid == folder_id) {
Some(folder) => folder, Some(folder) => folder,
None => err!("Folder doesn't exist"), None => err!("Folder doesn't exist"),
}; };
if &saved_folder.user_uuid != user_uuid {
err!("The folder is not owned by the user")
}
saved_folder.name = folder_data.name; saved_folder.name = folder_data.name;
saved_folder.save(&mut conn).await? saved_folder.save(&mut conn).await?
} }
@@ -515,9 +586,8 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn,
// Update emergency access data // Update emergency access data
for emergency_access_data in data.emergency_access_keys { for emergency_access_data in data.emergency_access_keys {
let mut saved_emergency_access = let saved_emergency_access =
match EmergencyAccess::find_by_uuid_and_grantor_uuid(&emergency_access_data.id, user_uuid, &mut conn).await match existing_emergency_access.iter_mut().find(|ea| ea.uuid == emergency_access_data.id) {
{
Some(emergency_access) => emergency_access, Some(emergency_access) => emergency_access,
None => err!("Emergency access doesn't exist or is not owned by the user"), None => err!("Emergency access doesn't exist or is not owned by the user"),
}; };
@@ -528,13 +598,11 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn,
// Update reset password data // Update reset password data
for reset_password_data in data.reset_password_keys { for reset_password_data in data.reset_password_keys {
let mut user_org = let user_org = match existing_user_orgs.iter_mut().find(|uo| uo.org_uuid == reset_password_data.organization_id)
match UserOrganization::find_by_user_and_org(user_uuid, &reset_password_data.organization_id, &mut conn) {
.await Some(reset_password) => reset_password,
{ None => err!("Reset password doesn't exist"),
Some(reset_password) => reset_password, };
None => err!("Reset password doesn't exist"),
};
user_org.reset_password_key = Some(reset_password_data.reset_password_key); user_org.reset_password_key = Some(reset_password_data.reset_password_key);
user_org.save(&mut conn).await? user_org.save(&mut conn).await?
@@ -542,12 +610,12 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn,
// Update send data // Update send data
for send_data in data.sends { for send_data in data.sends {
let mut send = match Send::find_by_uuid(send_data.id.as_ref().unwrap(), &mut conn).await { let send = match existing_sends.iter_mut().find(|s| &s.uuid == send_data.id.as_ref().unwrap()) {
Some(send) => send, Some(send) => send,
None => err!("Send doesn't exist"), None => err!("Send doesn't exist"),
}; };
update_send_from_data(&mut send, send_data, &headers, &mut conn, &nt, UpdateType::None).await?; update_send_from_data(send, send_data, &headers, &mut conn, &nt, UpdateType::None).await?;
} }
// Update cipher data // Update cipher data
@@ -555,20 +623,15 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn,
for cipher_data in data.ciphers { for cipher_data in data.ciphers {
if cipher_data.organization_id.is_none() { if cipher_data.organization_id.is_none() {
let mut saved_cipher = match Cipher::find_by_uuid(cipher_data.id.as_ref().unwrap(), &mut conn).await { let saved_cipher = match existing_ciphers.iter_mut().find(|c| &c.uuid == cipher_data.id.as_ref().unwrap()) {
Some(cipher) => cipher, Some(cipher) => cipher,
None => err!("Cipher doesn't exist"), None => err!("Cipher doesn't exist"),
}; };
if saved_cipher.user_uuid.as_ref().unwrap() != user_uuid {
err!("The cipher is not owned by the user")
}
// Prevent triggering cipher updates via WebSockets by settings UpdateType::None // Prevent triggering cipher updates via WebSockets by settings UpdateType::None
// The user sessions are invalidated because all the ciphers were re-encrypted and thus triggering an update could cause issues. // The user sessions are invalidated because all the ciphers were re-encrypted and thus triggering an update could cause issues.
// We force the users to logout after the user has been saved to try and prevent these issues. // We force the users to logout after the user has been saved to try and prevent these issues.
update_cipher_from_data(&mut saved_cipher, cipher_data, &headers, None, &mut conn, &nt, UpdateType::None) update_cipher_from_data(saved_cipher, cipher_data, &headers, None, &mut conn, &nt, UpdateType::None).await?
.await?
} }
} }
@@ -842,7 +905,7 @@ struct PasswordHintData {
#[post("/accounts/password-hint", data = "<data>")] #[post("/accounts/password-hint", data = "<data>")]
async fn password_hint(data: Json<PasswordHintData>, mut conn: DbConn) -> EmptyResult { async fn password_hint(data: Json<PasswordHintData>, mut conn: DbConn) -> EmptyResult {
if !CONFIG.mail_enabled() && !CONFIG.show_password_hint() { if !CONFIG.password_hints_allowed() || (!CONFIG.mail_enabled() && !CONFIG.show_password_hint()) {
err!("This server is not configured to provide password hints."); err!("This server is not configured to provide password hints.");
} }
@@ -901,14 +964,12 @@ pub async fn _prelogin(data: Json<PreloginData>, mut conn: DbConn) -> Json<Value
None => (User::CLIENT_KDF_TYPE_DEFAULT, User::CLIENT_KDF_ITER_DEFAULT, None, None), None => (User::CLIENT_KDF_TYPE_DEFAULT, User::CLIENT_KDF_ITER_DEFAULT, None, None),
}; };
let result = json!({ Json(json!({
"kdf": kdf_type, "kdf": kdf_type,
"kdfIterations": kdf_iter, "kdfIterations": kdf_iter,
"kdfMemory": kdf_mem, "kdfMemory": kdf_mem,
"kdfParallelism": kdf_para, "kdfParallelism": kdf_para,
}); }))
Json(result)
} }
// https://github.com/bitwarden/server/blob/master/src/Api/Models/Request/Accounts/SecretVerificationRequestModel.cs // https://github.com/bitwarden/server/blob/master/src/Api/Models/Request/Accounts/SecretVerificationRequestModel.cs
@@ -1084,14 +1145,15 @@ struct AuthRequestRequest {
device_identifier: String, device_identifier: String,
email: String, email: String,
public_key: String, public_key: String,
#[serde(alias = "type")] // Not used for now
_type: i32, // #[serde(alias = "type")]
// _type: i32,
} }
#[post("/auth-requests", data = "<data>")] #[post("/auth-requests", data = "<data>")]
async fn post_auth_request( async fn post_auth_request(
data: Json<AuthRequestRequest>, data: Json<AuthRequestRequest>,
headers: ClientHeaders, client_headers: ClientHeaders,
mut conn: DbConn, mut conn: DbConn,
nt: Notify<'_>, nt: Notify<'_>,
) -> JsonResult { ) -> JsonResult {
@@ -1099,16 +1161,20 @@ async fn post_auth_request(
let user = match User::find_by_mail(&data.email, &mut conn).await { let user = match User::find_by_mail(&data.email, &mut conn).await {
Some(user) => user, Some(user) => user,
None => { None => err!("AuthRequest doesn't exist", "User not found"),
err!("AuthRequest doesn't exist")
}
}; };
// Validate device uuid and type
match Device::find_by_uuid_and_user(&data.device_identifier, &user.uuid, &mut conn).await {
Some(device) if device.atype == client_headers.device_type => {}
_ => err!("AuthRequest doesn't exist", "Device verification failed"),
}
let mut auth_request = AuthRequest::new( let mut auth_request = AuthRequest::new(
user.uuid.clone(), user.uuid.clone(),
data.device_identifier.clone(), data.device_identifier.clone(),
headers.device_type, client_headers.device_type,
headers.ip.ip.to_string(), client_headers.ip.ip.to_string(),
data.access_code, data.access_code,
data.public_key, data.public_key,
); );
@@ -1123,7 +1189,7 @@ async fn post_auth_request(
"requestIpAddress": auth_request.request_ip, "requestIpAddress": auth_request.request_ip,
"key": null, "key": null,
"masterPasswordHash": null, "masterPasswordHash": null,
"creationDate": auth_request.creation_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true), "creationDate": format_date(&auth_request.creation_date),
"responseDate": null, "responseDate": null,
"requestApproved": false, "requestApproved": false,
"origin": CONFIG.domain_origin(), "origin": CONFIG.domain_origin(),
@@ -1132,33 +1198,31 @@ async fn post_auth_request(
} }
#[get("/auth-requests/<uuid>")] #[get("/auth-requests/<uuid>")]
async fn get_auth_request(uuid: &str, mut conn: DbConn) -> JsonResult { async fn get_auth_request(uuid: &str, headers: Headers, mut conn: DbConn) -> JsonResult {
let auth_request = match AuthRequest::find_by_uuid(uuid, &mut conn).await { let auth_request = match AuthRequest::find_by_uuid(uuid, &mut conn).await {
Some(auth_request) => auth_request, Some(auth_request) => auth_request,
None => { None => err!("AuthRequest doesn't exist", "Record not found"),
err!("AuthRequest doesn't exist")
}
}; };
let response_date_utc = auth_request if headers.user.uuid != auth_request.user_uuid {
.response_date err!("AuthRequest doesn't exist", "User uuid's do not match")
.map(|response_date| response_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true)); }
Ok(Json(json!( let response_date_utc = auth_request.response_date.map(|response_date| format_date(&response_date));
{
"id": uuid, Ok(Json(json!({
"publicKey": auth_request.public_key, "id": uuid,
"requestDeviceType": DeviceType::from_i32(auth_request.device_type).to_string(), "publicKey": auth_request.public_key,
"requestIpAddress": auth_request.request_ip, "requestDeviceType": DeviceType::from_i32(auth_request.device_type).to_string(),
"key": auth_request.enc_key, "requestIpAddress": auth_request.request_ip,
"masterPasswordHash": auth_request.master_password_hash, "key": auth_request.enc_key,
"creationDate": auth_request.creation_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true), "masterPasswordHash": auth_request.master_password_hash,
"responseDate": response_date_utc, "creationDate": format_date(&auth_request.creation_date),
"requestApproved": auth_request.approved, "responseDate": response_date_utc,
"origin": CONFIG.domain_origin(), "requestApproved": auth_request.approved,
"object":"auth-request" "origin": CONFIG.domain_origin(),
} "object":"auth-request"
))) })))
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@@ -1174,6 +1238,7 @@ struct AuthResponseRequest {
async fn put_auth_request( async fn put_auth_request(
uuid: &str, uuid: &str,
data: Json<AuthResponseRequest>, data: Json<AuthResponseRequest>,
headers: Headers,
mut conn: DbConn, mut conn: DbConn,
ant: AnonymousNotify<'_>, ant: AnonymousNotify<'_>,
nt: Notify<'_>, nt: Notify<'_>,
@@ -1181,75 +1246,84 @@ async fn put_auth_request(
let data = data.into_inner(); let data = data.into_inner();
let mut auth_request: AuthRequest = match AuthRequest::find_by_uuid(uuid, &mut conn).await { let mut auth_request: AuthRequest = match AuthRequest::find_by_uuid(uuid, &mut conn).await {
Some(auth_request) => auth_request, Some(auth_request) => auth_request,
None => { None => err!("AuthRequest doesn't exist", "Record not found"),
err!("AuthRequest doesn't exist")
}
}; };
auth_request.approved = Some(data.request_approved); if headers.user.uuid != auth_request.user_uuid {
auth_request.enc_key = Some(data.key); err!("AuthRequest doesn't exist", "User uuid's do not match")
auth_request.master_password_hash = data.master_password_hash;
auth_request.response_device_id = Some(data.device_identifier.clone());
auth_request.save(&mut conn).await?;
if auth_request.approved.unwrap_or(false) {
ant.send_auth_response(&auth_request.user_uuid, &auth_request.uuid).await;
nt.send_auth_response(&auth_request.user_uuid, &auth_request.uuid, data.device_identifier, &mut conn).await;
} }
let response_date_utc = auth_request if auth_request.approved.is_some() {
.response_date err!("An authentication request with the same device already exists")
.map(|response_date| response_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true)); }
Ok(Json(json!( let response_date = Utc::now().naive_utc();
{ let response_date_utc = format_date(&response_date);
"id": uuid,
"publicKey": auth_request.public_key, if data.request_approved {
"requestDeviceType": DeviceType::from_i32(auth_request.device_type).to_string(), auth_request.approved = Some(data.request_approved);
"requestIpAddress": auth_request.request_ip, auth_request.enc_key = Some(data.key);
"key": auth_request.enc_key, auth_request.master_password_hash = data.master_password_hash;
"masterPasswordHash": auth_request.master_password_hash, auth_request.response_device_id = Some(data.device_identifier.clone());
"creationDate": auth_request.creation_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true), auth_request.response_date = Some(response_date);
"responseDate": response_date_utc, auth_request.save(&mut conn).await?;
"requestApproved": auth_request.approved,
"origin": CONFIG.domain_origin(), ant.send_auth_response(&auth_request.user_uuid, &auth_request.uuid).await;
"object":"auth-request" nt.send_auth_response(&auth_request.user_uuid, &auth_request.uuid, data.device_identifier, &mut conn).await;
} } else {
))) // If denied, there's no reason to keep the request
auth_request.delete(&mut conn).await?;
}
Ok(Json(json!({
"id": uuid,
"publicKey": auth_request.public_key,
"requestDeviceType": DeviceType::from_i32(auth_request.device_type).to_string(),
"requestIpAddress": auth_request.request_ip,
"key": auth_request.enc_key,
"masterPasswordHash": auth_request.master_password_hash,
"creationDate": format_date(&auth_request.creation_date),
"responseDate": response_date_utc,
"requestApproved": auth_request.approved,
"origin": CONFIG.domain_origin(),
"object":"auth-request"
})))
} }
#[get("/auth-requests/<uuid>/response?<code>")] #[get("/auth-requests/<uuid>/response?<code>")]
async fn get_auth_request_response(uuid: &str, code: &str, mut conn: DbConn) -> JsonResult { async fn get_auth_request_response(
uuid: &str,
code: &str,
client_headers: ClientHeaders,
mut conn: DbConn,
) -> JsonResult {
let auth_request = match AuthRequest::find_by_uuid(uuid, &mut conn).await { let auth_request = match AuthRequest::find_by_uuid(uuid, &mut conn).await {
Some(auth_request) => auth_request, Some(auth_request) => auth_request,
None => { None => err!("AuthRequest doesn't exist", "User not found"),
err!("AuthRequest doesn't exist")
}
}; };
if !auth_request.check_access_code(code) { if auth_request.device_type != client_headers.device_type
err!("Access code invalid doesn't exist") || auth_request.request_ip != client_headers.ip.ip.to_string()
|| !auth_request.check_access_code(code)
{
err!("AuthRequest doesn't exist", "Invalid device, IP or code")
} }
let response_date_utc = auth_request let response_date_utc = auth_request.response_date.map(|response_date| format_date(&response_date));
.response_date
.map(|response_date| response_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true));
Ok(Json(json!( Ok(Json(json!({
{ "id": uuid,
"id": uuid, "publicKey": auth_request.public_key,
"publicKey": auth_request.public_key, "requestDeviceType": DeviceType::from_i32(auth_request.device_type).to_string(),
"requestDeviceType": DeviceType::from_i32(auth_request.device_type).to_string(), "requestIpAddress": auth_request.request_ip,
"requestIpAddress": auth_request.request_ip, "key": auth_request.enc_key,
"key": auth_request.enc_key, "masterPasswordHash": auth_request.master_password_hash,
"masterPasswordHash": auth_request.master_password_hash, "creationDate": format_date(&auth_request.creation_date),
"creationDate": auth_request.creation_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true), "responseDate": response_date_utc,
"responseDate": response_date_utc, "requestApproved": auth_request.approved,
"requestApproved": auth_request.approved, "origin": CONFIG.domain_origin(),
"origin": CONFIG.domain_origin(), "object":"auth-request"
"object":"auth-request" })))
}
)))
} }
#[get("/auth-requests")] #[get("/auth-requests")]
@@ -1261,7 +1335,7 @@ async fn get_auth_requests(headers: Headers, mut conn: DbConn) -> JsonResult {
.iter() .iter()
.filter(|request| request.approved.is_none()) .filter(|request| request.approved.is_none())
.map(|request| { .map(|request| {
let response_date_utc = request.response_date.map(|response_date| response_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true)); let response_date_utc = request.response_date.map(|response_date| format_date(&response_date));
json!({ json!({
"id": request.uuid, "id": request.uuid,
@@ -1270,7 +1344,7 @@ async fn get_auth_requests(headers: Headers, mut conn: DbConn) -> JsonResult {
"requestIpAddress": request.request_ip, "requestIpAddress": request.request_ip,
"key": request.enc_key, "key": request.enc_key,
"masterPasswordHash": request.master_password_hash, "masterPasswordHash": request.master_password_hash,
"creationDate": request.creation_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true), "creationDate": format_date(&request.creation_date),
"responseDate": response_date_utc, "responseDate": response_date_utc,
"requestApproved": request.approved, "requestApproved": request.approved,
"origin": CONFIG.domain_origin(), "origin": CONFIG.domain_origin(),

View File

@@ -10,6 +10,7 @@ use rocket::{
}; };
use serde_json::Value; use serde_json::Value;
use crate::auth::ClientVersion;
use crate::util::NumberOrString; use crate::util::NumberOrString;
use crate::{ use crate::{
api::{self, core::log_event, EmptyResult, JsonResult, Notify, PasswordOrOtpData, UpdateType}, api::{self, core::log_event, EmptyResult, JsonResult, Notify, PasswordOrOtpData, UpdateType},
@@ -104,11 +105,27 @@ struct SyncData {
} }
#[get("/sync?<data..>")] #[get("/sync?<data..>")]
async fn sync(data: SyncData, headers: Headers, mut conn: DbConn) -> Json<Value> { async fn sync(
data: SyncData,
headers: Headers,
client_version: Option<ClientVersion>,
mut conn: DbConn,
) -> Json<Value> {
let user_json = headers.user.to_json(&mut conn).await; let user_json = headers.user.to_json(&mut conn).await;
// Get all ciphers which are visible by the user // Get all ciphers which are visible by the user
let ciphers = Cipher::find_by_user_visible(&headers.user.uuid, &mut conn).await; let mut ciphers = Cipher::find_by_user_visible(&headers.user.uuid, &mut conn).await;
// Filter out SSH keys if the client version is less than 2024.12.0
let show_ssh_keys = if let Some(client_version) = client_version {
let ver_match = semver::VersionReq::parse(">=2024.12.0").unwrap();
ver_match.matches(&client_version.0)
} else {
false
};
if !show_ssh_keys {
ciphers.retain(|c| c.atype != 5);
}
let cipher_sync_data = CipherSyncData::new(&headers.user.uuid, CipherSyncType::User, &mut conn).await; let cipher_sync_data = CipherSyncData::new(&headers.user.uuid, CipherSyncType::User, &mut conn).await;
@@ -205,7 +222,7 @@ pub struct CipherData {
// Id is optional as it is included only in bulk share // Id is optional as it is included only in bulk share
pub id: Option<String>, pub id: Option<String>,
// Folder id is not included in import // Folder id is not included in import
folder_id: Option<String>, pub folder_id: Option<String>,
// TODO: Some of these might appear all the time, no need for Option // TODO: Some of these might appear all the time, no need for Option
#[serde(alias = "organizationID")] #[serde(alias = "organizationID")]
pub organization_id: Option<String>, pub organization_id: Option<String>,
@@ -216,7 +233,8 @@ pub struct CipherData {
Login = 1, Login = 1,
SecureNote = 2, SecureNote = 2,
Card = 3, Card = 3,
Identity = 4 Identity = 4,
SshKey = 5
*/ */
pub r#type: i32, pub r#type: i32,
pub name: String, pub name: String,
@@ -228,6 +246,7 @@ pub struct CipherData {
secure_note: Option<Value>, secure_note: Option<Value>,
card: Option<Value>, card: Option<Value>,
identity: Option<Value>, identity: Option<Value>,
ssh_key: Option<Value>,
favorite: Option<bool>, favorite: Option<bool>,
reprompt: Option<i32>, reprompt: Option<i32>,
@@ -469,6 +488,7 @@ pub async fn update_cipher_from_data(
2 => data.secure_note, 2 => data.secure_note,
3 => data.card, 3 => data.card,
4 => data.identity, 4 => data.identity,
5 => data.ssh_key,
_ => err!("Invalid type"), _ => err!("Invalid type"),
}; };
@@ -565,11 +585,11 @@ async fn post_ciphers_import(
Cipher::validate_cipher_data(&data.ciphers)?; Cipher::validate_cipher_data(&data.ciphers)?;
// Read and create the folders // Read and create the folders
let existing_folders: Vec<String> = let existing_folders: HashSet<Option<String>> =
Folder::find_by_user(&headers.user.uuid, &mut conn).await.into_iter().map(|f| f.uuid).collect(); Folder::find_by_user(&headers.user.uuid, &mut conn).await.into_iter().map(|f| Some(f.uuid)).collect();
let mut folders: Vec<String> = Vec::with_capacity(data.folders.len()); let mut folders: Vec<String> = Vec::with_capacity(data.folders.len());
for folder in data.folders.into_iter() { for folder in data.folders.into_iter() {
let folder_uuid = if folder.id.is_some() && existing_folders.contains(folder.id.as_ref().unwrap()) { let folder_uuid = if existing_folders.contains(&folder.id) {
folder.id.unwrap() folder.id.unwrap()
} else { } else {
let mut new_folder = Folder::new(headers.user.uuid.clone(), folder.name); let mut new_folder = Folder::new(headers.user.uuid.clone(), folder.name);
@@ -581,8 +601,8 @@ async fn post_ciphers_import(
} }
// Read the relations between folders and ciphers // Read the relations between folders and ciphers
// Ciphers can only be in one folder at the same time
let mut relations_map = HashMap::with_capacity(data.folder_relationships.len()); let mut relations_map = HashMap::with_capacity(data.folder_relationships.len());
for relation in data.folder_relationships { for relation in data.folder_relationships {
relations_map.insert(relation.key, relation.value); relations_map.insert(relation.key, relation.value);
} }

View File

@@ -135,12 +135,13 @@ async fn put_eq_domains(data: Json<EquivDomainData>, headers: Headers, conn: DbC
} }
#[get("/hibp/breach?<username>")] #[get("/hibp/breach?<username>")]
async fn hibp_breach(username: &str) -> JsonResult { async fn hibp_breach(username: &str, _headers: Headers) -> JsonResult {
let url = format!( let username: String = url::form_urlencoded::byte_serialize(username.as_bytes()).collect();
"https://haveibeenpwned.com/api/v3/breachedaccount/{username}?truncateResponse=false&includeUnverified=false"
);
if let Some(api_key) = crate::CONFIG.hibp_api_key() { if let Some(api_key) = crate::CONFIG.hibp_api_key() {
let url = format!(
"https://haveibeenpwned.com/api/v3/breachedaccount/{username}?truncateResponse=false&includeUnverified=false"
);
let res = make_http_request(Method::GET, &url)?.header("hibp-api-key", api_key).send().await?; let res = make_http_request(Method::GET, &url)?.header("hibp-api-key", api_key).send().await?;
// If we get a 404, return a 404, it means no breached accounts // If we get a 404, return a 404, it means no breached accounts

View File

@@ -9,9 +9,8 @@ use crate::{
core::{log_event, two_factor, CipherSyncData, CipherSyncType}, core::{log_event, two_factor, CipherSyncData, CipherSyncType},
EmptyResult, JsonResult, Notify, PasswordOrOtpData, UpdateType, EmptyResult, JsonResult, Notify, PasswordOrOtpData, UpdateType,
}, },
auth::{decode_invite, AdminHeaders, Headers, ManagerHeaders, ManagerHeadersLoose, OwnerHeaders}, auth::{decode_invite, AdminHeaders, ClientVersion, Headers, ManagerHeaders, ManagerHeadersLoose, OwnerHeaders},
db::{models::*, DbConn}, db::{models::*, DbConn},
error::Error,
mail, mail,
util::{convert_json_key_lcase_first, NumberOrString}, util::{convert_json_key_lcase_first, NumberOrString},
CONFIG, CONFIG,
@@ -127,6 +126,7 @@ struct NewCollectionData {
name: String, name: String,
groups: Vec<NewCollectionObjectData>, groups: Vec<NewCollectionObjectData>,
users: Vec<NewCollectionObjectData>, users: Vec<NewCollectionObjectData>,
id: Option<String>,
external_id: Option<String>, external_id: Option<String>,
} }
@@ -1598,40 +1598,43 @@ async fn post_org_import(
// TODO: See if we can optimize the whole cipher adding/importing and prevent duplicate code and checks. // TODO: See if we can optimize the whole cipher adding/importing and prevent duplicate code and checks.
Cipher::validate_cipher_data(&data.ciphers)?; Cipher::validate_cipher_data(&data.ciphers)?;
let mut collections = Vec::new(); let existing_collections: HashSet<Option<String>> =
Collection::find_by_organization(&org_id, &mut conn).await.into_iter().map(|c| (Some(c.uuid))).collect();
let mut collections: Vec<String> = Vec::with_capacity(data.collections.len());
for coll in data.collections { for coll in data.collections {
let collection = Collection::new(org_id.clone(), coll.name, coll.external_id); let collection_uuid = if existing_collections.contains(&coll.id) {
if collection.save(&mut conn).await.is_err() { coll.id.unwrap()
collections.push(Err(Error::new("Failed to create Collection", "Failed to create Collection")));
} else { } else {
collections.push(Ok(collection)); let new_collection = Collection::new(org_id.clone(), coll.name, coll.external_id);
} new_collection.save(&mut conn).await?;
new_collection.uuid
};
collections.push(collection_uuid);
} }
// Read the relations between collections and ciphers // Read the relations between collections and ciphers
let mut relations = Vec::new(); // Ciphers can be in multiple collections at the same time
let mut relations = Vec::with_capacity(data.collection_relationships.len());
for relation in data.collection_relationships { for relation in data.collection_relationships {
relations.push((relation.key, relation.value)); relations.push((relation.key, relation.value));
} }
let headers: Headers = headers.into(); let headers: Headers = headers.into();
let mut ciphers = Vec::new(); let mut ciphers: Vec<String> = Vec::with_capacity(data.ciphers.len());
for cipher_data in data.ciphers { for mut cipher_data in data.ciphers {
// Always clear folder_id's via an organization import
cipher_data.folder_id = None;
let mut cipher = Cipher::new(cipher_data.r#type, cipher_data.name.clone()); let mut cipher = Cipher::new(cipher_data.r#type, cipher_data.name.clone());
update_cipher_from_data(&mut cipher, cipher_data, &headers, None, &mut conn, &nt, UpdateType::None).await.ok(); update_cipher_from_data(&mut cipher, cipher_data, &headers, None, &mut conn, &nt, UpdateType::None).await.ok();
ciphers.push(cipher); ciphers.push(cipher.uuid);
} }
// Assign the collections // Assign the collections
for (cipher_index, coll_index) in relations { for (cipher_index, coll_index) in relations {
let cipher_id = &ciphers[cipher_index].uuid; let cipher_id = &ciphers[cipher_index];
let coll = &collections[coll_index]; let coll_id = &collections[coll_index];
let coll_id = match coll {
Ok(coll) => coll.uuid.as_str(),
Err(_) => err!("Failed to assign to collection"),
};
CollectionCipher::save(cipher_id, coll_id, &mut conn).await?; CollectionCipher::save(cipher_id, coll_id, &mut conn).await?;
} }
@@ -2305,14 +2308,14 @@ async fn _restore_organization_user(
} }
#[get("/organizations/<org_id>/groups")] #[get("/organizations/<org_id>/groups")]
async fn get_groups(org_id: &str, headers: ManagerHeadersLoose, mut conn: DbConn) -> JsonResult { async fn get_groups(org_id: &str, _headers: ManagerHeadersLoose, mut conn: DbConn) -> JsonResult {
let groups: Vec<Value> = if CONFIG.org_groups_enabled() { let groups: Vec<Value> = if CONFIG.org_groups_enabled() {
// Group::find_by_organization(&org_id, &mut conn).await.iter().map(Group::to_json).collect::<Value>() // Group::find_by_organization(&org_id, &mut conn).await.iter().map(Group::to_json).collect::<Value>()
let groups = Group::find_by_organization(org_id, &mut conn).await; let groups = Group::find_by_organization(org_id, &mut conn).await;
let mut groups_json = Vec::with_capacity(groups.len()); let mut groups_json = Vec::with_capacity(groups.len());
for g in groups { for g in groups {
groups_json.push(g.to_json_details(&headers.org_user.atype, &mut conn).await) groups_json.push(g.to_json_details(&mut conn).await)
} }
groups_json groups_json
} else { } else {
@@ -2500,7 +2503,7 @@ async fn add_update_group(
} }
#[get("/organizations/<_org_id>/groups/<group_id>/details")] #[get("/organizations/<_org_id>/groups/<group_id>/details")]
async fn get_group_details(_org_id: &str, group_id: &str, headers: AdminHeaders, mut conn: DbConn) -> JsonResult { async fn get_group_details(_org_id: &str, group_id: &str, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult {
if !CONFIG.org_groups_enabled() { if !CONFIG.org_groups_enabled() {
err!("Group support is disabled"); err!("Group support is disabled");
} }
@@ -2510,7 +2513,7 @@ async fn get_group_details(_org_id: &str, group_id: &str, headers: AdminHeaders,
_ => err!("Group could not be found!"), _ => err!("Group could not be found!"),
}; };
Ok(Json(group.to_json_details(&(headers.org_user_type as i32), &mut conn).await)) Ok(Json(group.to_json_details(&mut conn).await))
} }
#[post("/organizations/<org_id>/groups/<group_id>/delete")] #[post("/organizations/<org_id>/groups/<group_id>/delete")]
@@ -2999,18 +3002,20 @@ async fn put_reset_password_enrollment(
// We need to convert all keys so they have the first character to be a lowercase. // We need to convert all keys so they have the first character to be a lowercase.
// Else the export will be just an empty JSON file. // Else the export will be just an empty JSON file.
#[get("/organizations/<org_id>/export")] #[get("/organizations/<org_id>/export")]
async fn get_org_export(org_id: &str, headers: AdminHeaders, mut conn: DbConn) -> Json<Value> { async fn get_org_export(
use semver::{Version, VersionReq}; org_id: &str,
headers: AdminHeaders,
client_version: Option<ClientVersion>,
mut conn: DbConn,
) -> Json<Value> {
// Since version v2023.1.0 the format of the export is different. // Since version v2023.1.0 the format of the export is different.
// Also, this endpoint was created since v2022.9.0. // Also, this endpoint was created since v2022.9.0.
// Therefore, we will check for any version smaller then v2023.1.0 and return a different response. // Therefore, we will check for any version smaller then v2023.1.0 and return a different response.
// If we can't determine the version, we will use the latest default v2023.1.0 and higher. // If we can't determine the version, we will use the latest default v2023.1.0 and higher.
// https://github.com/bitwarden/server/blob/9ca93381ce416454734418c3a9f99ab49747f1b6/src/Api/Controllers/OrganizationExportController.cs#L44 // https://github.com/bitwarden/server/blob/9ca93381ce416454734418c3a9f99ab49747f1b6/src/Api/Controllers/OrganizationExportController.cs#L44
let use_list_response_model = if let Some(client_version) = headers.client_version { let use_list_response_model = if let Some(client_version) = client_version {
let ver_match = VersionReq::parse("<2023.1.0").unwrap(); let ver_match = semver::VersionReq::parse("<2023.1.0").unwrap();
let client_version = Version::parse(&client_version).unwrap(); ver_match.matches(&client_version.0)
ver_match.matches(&client_version)
} else { } else {
false false
}; };

View File

@@ -211,10 +211,7 @@ impl DuoClient {
nonce, nonce,
}; };
let token = match self.encode_duo_jwt(jwt_payload) { let token = self.encode_duo_jwt(jwt_payload)?;
Ok(token) => token,
Err(e) => return Err(e),
};
let authz_endpoint = format!("https://{}/oauth/v1/authorize", self.api_host); let authz_endpoint = format!("https://{}/oauth/v1/authorize", self.api_host);
let mut auth_url = match Url::parse(authz_endpoint.as_str()) { let mut auth_url = match Url::parse(authz_endpoint.as_str()) {

View File

@@ -165,20 +165,22 @@ async fn _password_login(
// Set the user_uuid here to be passed back used for event logging. // Set the user_uuid here to be passed back used for event logging.
*user_uuid = Some(user.uuid.clone()); *user_uuid = Some(user.uuid.clone());
// Check password // Check if the user is disabled
let password = data.password.as_ref().unwrap(); if !user.enabled {
if let Some(auth_request_uuid) = data.auth_request.clone() { err!(
if let Some(auth_request) = AuthRequest::find_by_uuid(auth_request_uuid.as_str(), conn).await { "This user has been disabled",
if !auth_request.check_access_code(password) { format!("IP: {}. Username: {}.", ip.ip, username),
err!( ErrorEvent {
"Username or access code is incorrect. Try again", event: EventType::UserFailedLogIn
format!("IP: {}. Username: {}.", ip.ip, username),
ErrorEvent {
event: EventType::UserFailedLogIn,
}
)
} }
} else { )
}
let password = data.password.as_ref().unwrap();
// If we get an auth request, we don't check the user's password, but the access code of the auth request
if let Some(ref auth_request_uuid) = data.auth_request {
let Some(auth_request) = AuthRequest::find_by_uuid(auth_request_uuid.as_str(), conn).await else {
err!( err!(
"Auth request not found. Try again.", "Auth request not found. Try again.",
format!("IP: {}. Username: {}.", ip.ip, username), format!("IP: {}. Username: {}.", ip.ip, username),
@@ -186,6 +188,24 @@ async fn _password_login(
event: EventType::UserFailedLogIn, event: EventType::UserFailedLogIn,
} }
) )
};
let expiration_time = auth_request.creation_date + chrono::Duration::minutes(5);
let request_expired = Utc::now().naive_utc() >= expiration_time;
if auth_request.user_uuid != user.uuid
|| !auth_request.approved.unwrap_or(false)
|| request_expired
|| ip.ip.to_string() != auth_request.request_ip
|| !auth_request.check_access_code(password)
{
err!(
"Username or access code is incorrect. Try again",
format!("IP: {}. Username: {}.", ip.ip, username),
ErrorEvent {
event: EventType::UserFailedLogIn,
}
)
} }
} else if !user.check_valid_password(password) { } else if !user.check_valid_password(password) {
err!( err!(
@@ -197,8 +217,8 @@ async fn _password_login(
) )
} }
// Change the KDF Iterations // Change the KDF Iterations (only when not logging in with an auth request)
if user.password_iterations != CONFIG.password_iterations() { if data.auth_request.is_none() && user.password_iterations != CONFIG.password_iterations() {
user.password_iterations = CONFIG.password_iterations(); user.password_iterations = CONFIG.password_iterations();
user.set_password(password, None, false, None); user.set_password(password, None, false, None);
@@ -207,17 +227,6 @@ async fn _password_login(
} }
} }
// Check if the user is disabled
if !user.enabled {
err!(
"This user has been disabled",
format!("IP: {}. Username: {}.", ip.ip, username),
ErrorEvent {
event: EventType::UserFailedLogIn
}
)
}
let now = Utc::now().naive_utc(); let now = Utc::now().naive_utc();
if user.verified_at.is_none() && CONFIG.mail_enabled() && CONFIG.signups_verify() { if user.verified_at.is_none() && CONFIG.mail_enabled() && CONFIG.signups_verify() {

View File

@@ -1,13 +1,20 @@
use once_cell::sync::Lazy;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use rocket::{fs::NamedFile, http::ContentType, response::content::RawHtml as Html, serde::json::Json, Catcher, Route}; use rocket::{
fs::NamedFile,
http::ContentType,
response::{content::RawCss as Css, content::RawHtml as Html, Redirect},
serde::json::Json,
Catcher, Route,
};
use serde_json::Value; use serde_json::Value;
use crate::{ use crate::{
api::{core::now, ApiResult, EmptyResult}, api::{core::now, ApiResult, EmptyResult},
auth::decode_file_download, auth::decode_file_download,
error::Error, error::Error,
util::{Cached, SafeString}, util::{get_web_vault_version, Cached, SafeString},
CONFIG, CONFIG,
}; };
@@ -16,7 +23,7 @@ pub fn routes() -> Vec<Route> {
// crate::utils::LOGGED_ROUTES to make sure they appear in the log // crate::utils::LOGGED_ROUTES to make sure they appear in the log
let mut routes = routes![attachments, alive, alive_head, static_files]; let mut routes = routes![attachments, alive, alive_head, static_files];
if CONFIG.web_vault_enabled() { if CONFIG.web_vault_enabled() {
routes.append(&mut routes![web_index, web_index_head, app_id, web_files]); routes.append(&mut routes![web_index, web_index_direct, web_index_head, app_id, web_files, vaultwarden_css]);
} }
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
@@ -45,11 +52,101 @@ fn not_found() -> ApiResult<Html<String>> {
Ok(Html(text)) Ok(Html(text))
} }
#[get("/css/vaultwarden.css")]
fn vaultwarden_css() -> Cached<Css<String>> {
// Configure the web-vault version as an integer so it can be used as a comparison smaller or greater then.
// The default is based upon the version since this feature is added.
static WEB_VAULT_VERSION: Lazy<u32> = Lazy::new(|| {
let re = regex::Regex::new(r"(\d{4})\.(\d{1,2})\.(\d{1,2})").unwrap();
let vault_version = get_web_vault_version();
let (major, minor, patch) = match re.captures(&vault_version) {
Some(c) if c.len() == 4 => (
c.get(1).unwrap().as_str().parse().unwrap(),
c.get(2).unwrap().as_str().parse().unwrap(),
c.get(3).unwrap().as_str().parse().unwrap(),
),
_ => (2024, 6, 2),
};
format!("{major}{minor:02}{patch:02}").parse::<u32>().unwrap()
});
// Configure the Vaultwarden version as an integer so it can be used as a comparison smaller or greater then.
// The default is based upon the version since this feature is added.
static VW_VERSION: Lazy<u32> = Lazy::new(|| {
let re = regex::Regex::new(r"(\d{1})\.(\d{1,2})\.(\d{1,2})").unwrap();
let vw_version = crate::VERSION.unwrap_or("1.32.1");
let (major, minor, patch) = match re.captures(vw_version) {
Some(c) if c.len() == 4 => (
c.get(1).unwrap().as_str().parse().unwrap(),
c.get(2).unwrap().as_str().parse().unwrap(),
c.get(3).unwrap().as_str().parse().unwrap(),
),
_ => (1, 32, 1),
};
format!("{major}{minor:02}{patch:02}").parse::<u32>().unwrap()
});
let css_options = json!({
"web_vault_version": *WEB_VAULT_VERSION,
"vw_version": *VW_VERSION,
"signup_disabled": !CONFIG.signups_allowed() && CONFIG.signups_domains_whitelist().is_empty(),
"mail_enabled": CONFIG.mail_enabled(),
"yubico_enabled": CONFIG._enable_yubico() && (CONFIG.yubico_client_id().is_some() == CONFIG.yubico_secret_key().is_some()),
"emergency_access_allowed": CONFIG.emergency_access_allowed(),
"sends_allowed": CONFIG.sends_allowed(),
"load_user_scss": true,
});
let scss = match CONFIG.render_template("scss/vaultwarden.scss", &css_options) {
Ok(t) => t,
Err(e) => {
// Something went wrong loading the template. Use the fallback
warn!("Loading scss/vaultwarden.scss.hbs or scss/user.vaultwarden.scss.hbs failed. {e}");
CONFIG
.render_fallback_template("scss/vaultwarden.scss", &css_options)
.expect("Fallback scss/vaultwarden.scss.hbs to render")
}
};
let css = match grass_compiler::from_string(
scss,
&grass_compiler::Options::default().style(grass_compiler::OutputStyle::Compressed),
) {
Ok(css) => css,
Err(e) => {
// Something went wrong compiling the scss. Use the fallback
warn!("Compiling the Vaultwarden SCSS styles failed. {e}");
let mut css_options = css_options;
css_options["load_user_scss"] = json!(false);
let scss = CONFIG
.render_fallback_template("scss/vaultwarden.scss", &css_options)
.expect("Fallback scss/vaultwarden.scss.hbs to render");
grass_compiler::from_string(
scss,
&grass_compiler::Options::default().style(grass_compiler::OutputStyle::Compressed),
)
.expect("SCSS to compile")
}
};
// Cache for one day should be enough and not too much
Cached::ttl(Css(css), 86_400, false)
}
#[get("/")] #[get("/")]
async fn web_index() -> Cached<Option<NamedFile>> { async fn web_index() -> Cached<Option<NamedFile>> {
Cached::short(NamedFile::open(Path::new(&CONFIG.web_vault_folder()).join("index.html")).await.ok(), false) Cached::short(NamedFile::open(Path::new(&CONFIG.web_vault_folder()).join("index.html")).await.ok(), false)
} }
// Make sure that `/index.html` redirect to actual domain path.
// If not, this might cause issues with the web-vault
#[get("/index.html")]
fn web_index_direct() -> Redirect {
Redirect::to(format!("{}/", CONFIG.domain_path()))
}
#[head("/")] #[head("/")]
fn web_index_head() -> EmptyResult { fn web_index_head() -> EmptyResult {
// Add an explicit HEAD route to prevent uptime monitoring services from // Add an explicit HEAD route to prevent uptime monitoring services from

View File

@@ -615,7 +615,6 @@ pub struct AdminHeaders {
pub device: Device, pub device: Device,
pub user: User, pub user: User,
pub org_user_type: UserOrgType, pub org_user_type: UserOrgType,
pub client_version: Option<String>,
pub ip: ClientIp, pub ip: ClientIp,
} }
@@ -625,14 +624,12 @@ impl<'r> FromRequest<'r> for AdminHeaders {
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> { async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
let headers = try_outcome!(OrgHeaders::from_request(request).await); let headers = try_outcome!(OrgHeaders::from_request(request).await);
let client_version = request.headers().get_one("Bitwarden-Client-Version").map(String::from);
if headers.org_user_type >= UserOrgType::Admin { if headers.org_user_type >= UserOrgType::Admin {
Outcome::Success(Self { Outcome::Success(Self {
host: headers.host, host: headers.host,
device: headers.device, device: headers.device,
user: headers.user, user: headers.user,
org_user_type: headers.org_user_type, org_user_type: headers.org_user_type,
client_version,
ip: headers.ip, ip: headers.ip,
}) })
} else { } else {
@@ -900,3 +897,24 @@ impl<'r> FromRequest<'r> for WsAccessTokenHeader {
}) })
} }
} }
pub struct ClientVersion(pub semver::Version);
#[rocket::async_trait]
impl<'r> FromRequest<'r> for ClientVersion {
type Error = &'static str;
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
let headers = request.headers();
let Some(version) = headers.get_one("Bitwarden-Client-Version") else {
err_handler!("No Bitwarden-Client-Version header provided")
};
let Ok(version) = semver::Version::parse(version) else {
err_handler!("Invalid Bitwarden-Client-Version header provided")
};
Outcome::Success(ClientVersion(version))
}
}

View File

@@ -497,11 +497,11 @@ make_config! {
/// Password iterations |> Number of server-side passwords hashing iterations for the password hash. /// 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. /// The default for new users. If changed, it will be updated during login for existing users.
password_iterations: i32, true, def, 600_000; password_iterations: i32, true, def, 600_000;
/// Allow password hints |> Controls whether users can set password hints. This setting applies globally to all users. /// Allow password hints |> Controls whether users can set or show password hints. This setting applies globally to all users.
password_hints_allowed: bool, true, def, true; password_hints_allowed: bool, true, def, true;
/// Show password hint |> Controls whether a password hint should be shown directly in the web page /// Show password hint (Know the risks!) |> 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 /// if SMTP service is not configured and password hints are allowed. Not recommended for publicly-accessible instances
/// provides unauthenticated access to potentially sensitive data. /// because this provides unauthenticated access to potentially sensitive data.
show_password_hint: bool, true, def, false; 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/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!
@@ -811,8 +811,15 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
} }
// TODO: deal with deprecated flags so they can be removed from this list, cf. #4263 // TODO: deal with deprecated flags so they can be removed from this list, cf. #4263
const KNOWN_FLAGS: &[&str] = const KNOWN_FLAGS: &[&str] = &[
&["autofill-overlay", "autofill-v2", "browser-fileless-import", "extension-refresh", "fido2-vault-credentials"]; "autofill-overlay",
"autofill-v2",
"browser-fileless-import",
"extension-refresh",
"fido2-vault-credentials",
"ssh-key-vault-item",
"ssh-agent",
];
let configured_flags = parse_experimental_client_feature_flags(&cfg.experimental_client_feature_flags); let configured_flags = parse_experimental_client_feature_flags(&cfg.experimental_client_feature_flags);
let invalid_flags: Vec<_> = configured_flags.keys().filter(|flag| !KNOWN_FLAGS.contains(&flag.as_str())).collect(); let invalid_flags: Vec<_> = configured_flags.keys().filter(|flag| !KNOWN_FLAGS.contains(&flag.as_str())).collect();
if !invalid_flags.is_empty() { if !invalid_flags.is_empty() {
@@ -1269,11 +1276,16 @@ impl Config {
let hb = load_templates(CONFIG.templates_folder()); let hb = load_templates(CONFIG.templates_folder());
hb.render(name, data).map_err(Into::into) hb.render(name, data).map_err(Into::into)
} else { } else {
let hb = &CONFIG.inner.read().unwrap().templates; let hb = &self.inner.read().unwrap().templates;
hb.render(name, data).map_err(Into::into) hb.render(name, data).map_err(Into::into)
} }
} }
pub fn render_fallback_template<T: serde::ser::Serialize>(&self, name: &str, data: &T) -> Result<String, Error> {
let hb = &self.inner.read().unwrap().templates;
hb.render(&format!("fallback_{name}"), data).map_err(Into::into)
}
pub fn set_rocket_shutdown_handle(&self, handle: rocket::Shutdown) { pub fn set_rocket_shutdown_handle(&self, handle: rocket::Shutdown) {
self.inner.write().unwrap().rocket_shutdown_handle = Some(handle); self.inner.write().unwrap().rocket_shutdown_handle = Some(handle);
} }
@@ -1312,6 +1324,11 @@ where
reg!($name); reg!($name);
reg!(concat!($name, $ext)); reg!(concat!($name, $ext));
}}; }};
(@withfallback $name:expr) => {{
let template = include_str!(concat!("static/templates/", $name, ".hbs"));
hb.register_template_string($name, template).unwrap();
hb.register_template_string(concat!("fallback_", $name), template).unwrap();
}};
} }
// First register default templates here // First register default templates here
@@ -1355,6 +1372,9 @@ where
reg!("404"); reg!("404");
reg!(@withfallback "scss/vaultwarden.scss");
reg!("scss/user.vaultwarden.scss");
// And then load user templates to overwrite the defaults // And then load user templates to overwrite the defaults
// Use .hbs extension for the files // Use .hbs extension for the files
// Templates get registered with their relative name // Templates get registered with their relative name

View File

@@ -1,6 +1,6 @@
use crate::util::LowerCase; use crate::util::LowerCase;
use crate::CONFIG; use crate::CONFIG;
use chrono::{DateTime, NaiveDateTime, TimeDelta, Utc}; use chrono::{NaiveDateTime, TimeDelta, Utc};
use serde_json::Value; use serde_json::Value;
use super::{ use super::{
@@ -30,7 +30,8 @@ db_object! {
Login = 1, Login = 1,
SecureNote = 2, SecureNote = 2,
Card = 3, Card = 3,
Identity = 4 Identity = 4,
SshKey = 5
*/ */
pub atype: i32, pub atype: i32,
pub name: String, pub name: String,
@@ -216,11 +217,13 @@ impl Cipher {
Some(p) if p.is_string() => Some(d.data), Some(p) if p.is_string() => Some(d.data),
_ => None, _ => None,
}) })
.map(|d| match d.get("lastUsedDate").and_then(|l| l.as_str()) { .map(|mut d| match d.get("lastUsedDate").and_then(|l| l.as_str()) {
Some(l) if DateTime::parse_from_rfc3339(l).is_ok() => d, Some(l) => {
d["lastUsedDate"] = json!(crate::util::validate_and_format_date(l));
d
}
_ => { _ => {
let mut d = d; d["lastUsedDate"] = json!("1970-01-01T00:00:00.000000Z");
d["lastUsedDate"] = json!("1970-01-01T00:00:00.000Z");
d d
} }
}) })
@@ -317,6 +320,7 @@ impl Cipher {
"secureNote": null, "secureNote": null,
"card": null, "card": null,
"identity": null, "identity": null,
"sshKey": null,
}); });
// These values are only needed for user/default syncs // These values are only needed for user/default syncs
@@ -345,6 +349,7 @@ impl Cipher {
2 => "secureNote", 2 => "secureNote",
3 => "card", 3 => "card",
4 => "identity", 4 => "identity",
5 => "sshKey",
_ => panic!("Wrong type"), _ => panic!("Wrong type"),
}; };

View File

@@ -1,4 +1,4 @@
use super::{User, UserOrgType, UserOrganization}; use super::{User, UserOrganization};
use crate::api::EmptyResult; use crate::api::EmptyResult;
use crate::db::DbConn; use crate::db::DbConn;
use crate::error::MapResult; use crate::error::MapResult;
@@ -73,7 +73,7 @@ impl Group {
}) })
} }
pub async fn to_json_details(&self, user_org_type: &i32, conn: &mut DbConn) -> Value { pub async fn to_json_details(&self, conn: &mut DbConn) -> Value {
let collections_groups: Vec<Value> = CollectionGroup::find_by_group(&self.uuid, conn) let collections_groups: Vec<Value> = CollectionGroup::find_by_group(&self.uuid, conn)
.await .await
.iter() .iter()
@@ -82,7 +82,7 @@ impl Group {
"id": entry.collections_uuid, "id": entry.collections_uuid,
"readOnly": entry.read_only, "readOnly": entry.read_only,
"hidePasswords": entry.hide_passwords, "hidePasswords": entry.hide_passwords,
"manage": *user_org_type >= UserOrgType::Admin || (*user_org_type == UserOrgType::Manager && !entry.read_only && !entry.hide_passwords) "manage": false
}) })
}) })
.collect(); .collect();

View File

@@ -96,7 +96,31 @@ fn smtp_transport() -> AsyncSmtpTransport<Tokio1Executor> {
smtp_client.build() smtp_client.build()
} }
// This will sanitize the string values by stripping all the html tags to prevent XSS and HTML Injections
fn sanitize_data(data: &mut serde_json::Value) {
use regex::Regex;
use std::sync::LazyLock;
static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"<[^>]+>").unwrap());
match data {
serde_json::Value::String(s) => *s = RE.replace_all(s, "").to_string(),
serde_json::Value::Object(obj) => {
for d in obj.values_mut() {
sanitize_data(d);
}
}
serde_json::Value::Array(arr) => {
for d in arr.iter_mut() {
sanitize_data(d);
}
}
_ => {}
}
}
fn get_text(template_name: &'static str, data: serde_json::Value) -> Result<(String, String, String), Error> { fn get_text(template_name: &'static str, data: serde_json::Value) -> Result<(String, String, String), Error> {
let mut data = data;
sanitize_data(&mut data);
let (subject_html, body_html) = get_template(&format!("{template_name}.html"), &data)?; let (subject_html, body_html) = get_template(&format!("{template_name}.html"), &data)?;
let (_subject_text, body_text) = get_template(template_name, &data)?; let (_subject_text, body_text) = get_template(template_name, &data)?;
Ok((subject_html, body_html, body_text)) Ok((subject_html, body_html, body_text))
@@ -116,6 +140,10 @@ fn get_template(template_name: &str, data: &serde_json::Value) -> Result<(String
None => err!("Template doesn't contain body"), None => err!("Template doesn't contain body"),
}; };
if text_split.next().is_some() {
err!("Template contains more than one body");
}
Ok((subject, body)) Ok((subject, body))
} }
@@ -259,16 +287,15 @@ pub async fn send_invite(
} }
let query_string = match query.query() { let query_string = match query.query() {
None => err!(format!("Failed to build invite URL query parameters")), None => err!("Failed to build invite URL query parameters"),
Some(query) => query, Some(query) => query,
}; };
// `url.Url` would place the anchor `#` after the query parameters
let url = format!("{}/#/accept-organization/?{}", CONFIG.domain(), query_string);
let (subject, body_html, body_text) = get_text( let (subject, body_html, body_text) = get_text(
"email/send_org_invite", "email/send_org_invite",
json!({ json!({
"url": url, // `url.Url` would place the anchor `#` after the query parameters
"url": format!("{}/#/accept-organization/?{}", CONFIG.domain(), query_string),
"img_src": CONFIG._smtp_img_src(), "img_src": CONFIG._smtp_img_src(),
"org_name": org_name, "org_name": org_name,
}), }),
@@ -292,17 +319,29 @@ pub async fn send_emergency_access_invite(
String::from(grantor_email), String::from(grantor_email),
); );
let invite_token = encode_jwt(&claims); // Build the query here to ensure proper escaping
let mut query = url::Url::parse("https://query.builder").unwrap();
{
let mut query_params = query.query_pairs_mut();
query_params
.append_pair("id", emer_id)
.append_pair("name", grantor_name)
.append_pair("email", address)
.append_pair("token", &encode_jwt(&claims));
}
let query_string = match query.query() {
None => err!("Failed to build emergency invite URL query parameters"),
Some(query) => query,
};
let (subject, body_html, body_text) = get_text( let (subject, body_html, body_text) = get_text(
"email/send_emergency_access_invite", "email/send_emergency_access_invite",
json!({ json!({
"url": CONFIG.domain(), // `url.Url` would place the anchor `#` after the query parameters
"url": format!("{}/#/accept-emergency/?{query_string}", CONFIG.domain()),
"img_src": CONFIG._smtp_img_src(), "img_src": CONFIG._smtp_img_src(),
"emer_id": emer_id,
"email": percent_encode(address.as_bytes(), NON_ALPHANUMERIC).to_string(),
"grantor_name": grantor_name, "grantor_name": grantor_name,
"token": invite_token,
}), }),
)?; )?;

View File

@@ -2,7 +2,7 @@ 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: 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}} Click here to join: {{{url}}}
If you do not wish to become an emergency contact for {{grantor_name}}, you can safely ignore this email. If you do not wish to become an emergency contact for {{grantor_name}}, you can safely ignore this email.
{{> email/email_footer_text }} {{> email/email_footer_text }}

View File

@@ -9,7 +9,7 @@ Emergency access for {{{grantor_name}}}
</tr> </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;"> <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"> <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}}" <a href="{{{url}}}"
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;"> 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 Become emergency contact
</a> </a>
@@ -21,4 +21,4 @@ Emergency access for {{{grantor_name}}}
</td> </td>
</tr> </tr>
</table> </table>
{{> email/email_footer }} {{> email/email_footer }}

View File

@@ -0,0 +1 @@
/* See the wiki for examples and details: https://github.com/dani-garcia/vaultwarden/wiki/Customize-Vaultwarden-CSS */

View File

@@ -0,0 +1,105 @@
/**** START Static Vaultwarden changes ****/
/* This combines all selectors extending it into one */
%vw-hide {
display: none !important;
}
/* This allows searching for the combined style in the browsers dev-tools (look into the head tag) */
.vw-hide,
head {
@extend %vw-hide;
}
/* Hide the Subscription Page tab */
bit-nav-item[route="settings/subscription"] {
@extend %vw-hide;
}
/* Hide any link pointing to Free Bitwarden Families */
a[href$="/settings/sponsored-families"] {
@extend %vw-hide;
}
/* Hide the `Enterprise Single Sign-On` button on the login page */
a[routerlink="/sso"] {
@extend %vw-hide;
}
/* Hide Two-Factor menu in Organization settings */
bit-nav-item[route="settings/two-factor"],
a[href$="/settings/two-factor"] {
@extend %vw-hide;
}
/* Hide Business Owned checkbox */
app-org-info > form:nth-child(1) > div:nth-child(3) {
@extend %vw-hide;
}
/* Hide the `This account is owned by a business` checkbox and label */
#ownedBusiness,
label[for^="ownedBusiness"] {
@extend %vw-hide;
}
/* Hide the radio button and label for the `Custom` org user type */
#userTypeCustom,
label[for^="userTypeCustom"] {
@extend %vw-hide;
}
/* Hide Business Name */
app-org-account form div bit-form-field.tw-block:nth-child(3) {
@extend %vw-hide;
}
/* Hide organization plans */
app-organization-plans > form > bit-section:nth-child(2) {
@extend %vw-hide;
}
/* Hide Device Verification form at the Two Step Login screen */
app-security > app-two-factor-setup > form {
@extend %vw-hide;
}
/**** END Static Vaultwarden Changes ****/
/**** START Dynamic Vaultwarden Changes ****/
{{#if signup_disabled}}
/* Hide the register link on the login screen */
app-frontend-layout > app-login > form > div > div > div > p {
@extend %vw-hide;
}
{{/if}}
/* Hide `Email` 2FA if mail is not enabled */
{{#unless mail_enabled}}
app-two-factor-setup ul.list-group.list-group-2fa li.list-group-item:nth-child(5) {
@extend %vw-hide;
}
{{/unless}}
/* Hide `YubiKey OTP security key` 2FA if it is not enabled */
{{#unless yubico_enabled}}
app-two-factor-setup ul.list-group.list-group-2fa li.list-group-item:nth-child(2) {
@extend %vw-hide;
}
{{/unless}}
/* Hide Emergency Access if not allowed */
{{#unless emergency_access_allowed}}
bit-nav-item[route="settings/emergency-access"] {
@extend %vw-hide;
}
{{/unless}}
/* Hide Sends if not allowed */
{{#unless sends_allowed}}
bit-nav-item[route="sends"] {
@extend %vw-hide;
}
{{/unless}}
/**** End Dynamic Vaultwarden Changes ****/
/**** Include a special user stylesheet for custom changes ****/
{{#if load_user_scss}}
{{> scss/user.vaultwarden.scss }}
{{/if}}

View File

@@ -438,13 +438,19 @@ pub fn get_env_bool(key: &str) -> Option<bool> {
use chrono::{DateTime, Local, NaiveDateTime, TimeZone}; use chrono::{DateTime, Local, NaiveDateTime, TimeZone};
// Format used by Bitwarden API
const DATETIME_FORMAT: &str = "%Y-%m-%dT%H:%M:%S%.6fZ";
/// Formats a UTC-offset `NaiveDateTime` in the format used by Bitwarden API /// Formats a UTC-offset `NaiveDateTime` in the format used by Bitwarden API
/// responses with "date" fields (`CreationDate`, `RevisionDate`, etc.). /// responses with "date" fields (`CreationDate`, `RevisionDate`, etc.).
pub fn format_date(dt: &NaiveDateTime) -> String { pub fn format_date(dt: &NaiveDateTime) -> String {
dt.format(DATETIME_FORMAT).to_string() dt.and_utc().to_rfc3339_opts(chrono::SecondsFormat::Micros, true)
}
/// Validates and formats a RFC3339 timestamp
/// If parsing fails it will return the start of the unix datetime
pub fn validate_and_format_date(dt: &str) -> String {
match DateTime::parse_from_rfc3339(dt) {
Ok(dt) => dt.to_rfc3339_opts(chrono::SecondsFormat::Micros, true),
_ => String::from("1970-01-01T00:00:00.000000Z"),
}
} }
/// Formats a `DateTime<Local>` using the specified format string. /// Formats a `DateTime<Local>` using the specified format string.
@@ -486,7 +492,7 @@ pub fn format_datetime_http(dt: &DateTime<Local>) -> String {
} }
pub fn parse_date(date: &str) -> NaiveDateTime { pub fn parse_date(date: &str) -> NaiveDateTime {
NaiveDateTime::parse_from_str(date, DATETIME_FORMAT).unwrap() DateTime::parse_from_rfc3339(date).unwrap().naive_utc()
} }
// //