Compare commits

..

1 Commits

Author SHA1 Message Date
Daniel García
f312e00dfa Test dylint 2024-11-09 17:31:22 +01:00
30 changed files with 2182 additions and 797 deletions

View File

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

View File

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

2
dylint.toml Normal file
View File

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

7
dylints/README.md Normal file
View File

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

View File

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

View File

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

1659
dylints/non_authenticated_routes/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,20 @@
[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

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

View File

@@ -0,0 +1,167 @@
#![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

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

View File

@@ -1,7 +1,5 @@
use std::collections::HashSet;
use crate::db::DbPool;
use chrono::Utc;
use chrono::{SecondsFormat, Utc};
use rocket::serde::json::Json;
use serde_json::Value;
@@ -15,7 +13,7 @@ use crate::{
crypto,
db::{models::*, DbConn},
mail,
util::{format_date, NumberOrString},
util::NumberOrString,
CONFIG,
};
@@ -479,60 +477,6 @@ struct KeyData {
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>")]
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.
@@ -550,35 +494,20 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn,
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
for folder_data in data.folders {
// Skip `null` folder id entries.
// See: https://github.com/bitwarden/clients/issues/8453
if let Some(folder_id) = folder_data.id {
let saved_folder = match existing_folders.iter_mut().find(|f| f.uuid == folder_id) {
let mut saved_folder = match Folder::find_by_uuid(&folder_id, &mut conn).await {
Some(folder) => folder,
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.save(&mut conn).await?
}
@@ -586,8 +515,9 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn,
// Update emergency access data
for emergency_access_data in data.emergency_access_keys {
let saved_emergency_access =
match existing_emergency_access.iter_mut().find(|ea| ea.uuid == emergency_access_data.id) {
let mut saved_emergency_access =
match EmergencyAccess::find_by_uuid_and_grantor_uuid(&emergency_access_data.id, user_uuid, &mut conn).await
{
Some(emergency_access) => emergency_access,
None => err!("Emergency access doesn't exist or is not owned by the user"),
};
@@ -598,11 +528,13 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn,
// Update reset password data
for reset_password_data in data.reset_password_keys {
let user_org = match existing_user_orgs.iter_mut().find(|uo| uo.org_uuid == reset_password_data.organization_id)
{
Some(reset_password) => reset_password,
None => err!("Reset password doesn't exist"),
};
let mut user_org =
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"),
};
user_org.reset_password_key = Some(reset_password_data.reset_password_key);
user_org.save(&mut conn).await?
@@ -610,12 +542,12 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn,
// Update send data
for send_data in data.sends {
let send = match existing_sends.iter_mut().find(|s| &s.uuid == send_data.id.as_ref().unwrap()) {
let mut send = match Send::find_by_uuid(send_data.id.as_ref().unwrap(), &mut conn).await {
Some(send) => send,
None => err!("Send doesn't exist"),
};
update_send_from_data(send, send_data, &headers, &mut conn, &nt, UpdateType::None).await?;
update_send_from_data(&mut send, send_data, &headers, &mut conn, &nt, UpdateType::None).await?;
}
// Update cipher data
@@ -623,15 +555,20 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn,
for cipher_data in data.ciphers {
if cipher_data.organization_id.is_none() {
let saved_cipher = match existing_ciphers.iter_mut().find(|c| &c.uuid == cipher_data.id.as_ref().unwrap()) {
let mut saved_cipher = match Cipher::find_by_uuid(cipher_data.id.as_ref().unwrap(), &mut conn).await {
Some(cipher) => cipher,
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
// 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.
update_cipher_from_data(saved_cipher, cipher_data, &headers, None, &mut conn, &nt, UpdateType::None).await?
update_cipher_from_data(&mut saved_cipher, cipher_data, &headers, None, &mut conn, &nt, UpdateType::None)
.await?
}
}
@@ -905,7 +842,7 @@ struct PasswordHintData {
#[post("/accounts/password-hint", data = "<data>")]
async fn password_hint(data: Json<PasswordHintData>, mut conn: DbConn) -> EmptyResult {
if !CONFIG.password_hints_allowed() || (!CONFIG.mail_enabled() && !CONFIG.show_password_hint()) {
if !CONFIG.mail_enabled() && !CONFIG.show_password_hint() {
err!("This server is not configured to provide password hints.");
}
@@ -964,12 +901,14 @@ 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),
};
Json(json!({
let result = json!({
"kdf": kdf_type,
"kdfIterations": kdf_iter,
"kdfMemory": kdf_mem,
"kdfParallelism": kdf_para,
}))
});
Json(result)
}
// https://github.com/bitwarden/server/blob/master/src/Api/Models/Request/Accounts/SecretVerificationRequestModel.cs
@@ -1145,15 +1084,14 @@ struct AuthRequestRequest {
device_identifier: String,
email: String,
public_key: String,
// Not used for now
// #[serde(alias = "type")]
// _type: i32,
#[serde(alias = "type")]
_type: i32,
}
#[post("/auth-requests", data = "<data>")]
async fn post_auth_request(
data: Json<AuthRequestRequest>,
client_headers: ClientHeaders,
headers: ClientHeaders,
mut conn: DbConn,
nt: Notify<'_>,
) -> JsonResult {
@@ -1161,20 +1099,16 @@ async fn post_auth_request(
let user = match User::find_by_mail(&data.email, &mut conn).await {
Some(user) => user,
None => err!("AuthRequest doesn't exist", "User not found"),
None => {
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(
user.uuid.clone(),
data.device_identifier.clone(),
client_headers.device_type,
client_headers.ip.ip.to_string(),
headers.device_type,
headers.ip.ip.to_string(),
data.access_code,
data.public_key,
);
@@ -1189,7 +1123,7 @@ async fn post_auth_request(
"requestIpAddress": auth_request.request_ip,
"key": null,
"masterPasswordHash": null,
"creationDate": format_date(&auth_request.creation_date),
"creationDate": auth_request.creation_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true),
"responseDate": null,
"requestApproved": false,
"origin": CONFIG.domain_origin(),
@@ -1198,31 +1132,33 @@ async fn post_auth_request(
}
#[get("/auth-requests/<uuid>")]
async fn get_auth_request(uuid: &str, headers: Headers, mut conn: DbConn) -> JsonResult {
async fn get_auth_request(uuid: &str, mut conn: DbConn) -> JsonResult {
let auth_request = match AuthRequest::find_by_uuid(uuid, &mut conn).await {
Some(auth_request) => auth_request,
None => err!("AuthRequest doesn't exist", "Record not found"),
None => {
err!("AuthRequest doesn't exist")
}
};
if headers.user.uuid != auth_request.user_uuid {
err!("AuthRequest doesn't exist", "User uuid's do not match")
}
let response_date_utc = auth_request
.response_date
.map(|response_date| response_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true));
let response_date_utc = auth_request.response_date.map(|response_date| format_date(&response_date));
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"
})))
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": auth_request.creation_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true),
"responseDate": response_date_utc,
"requestApproved": auth_request.approved,
"origin": CONFIG.domain_origin(),
"object":"auth-request"
}
)))
}
#[derive(Debug, Deserialize)]
@@ -1238,7 +1174,6 @@ struct AuthResponseRequest {
async fn put_auth_request(
uuid: &str,
data: Json<AuthResponseRequest>,
headers: Headers,
mut conn: DbConn,
ant: AnonymousNotify<'_>,
nt: Notify<'_>,
@@ -1246,84 +1181,75 @@ async fn put_auth_request(
let data = data.into_inner();
let mut auth_request: AuthRequest = match AuthRequest::find_by_uuid(uuid, &mut conn).await {
Some(auth_request) => auth_request,
None => err!("AuthRequest doesn't exist", "Record not found"),
None => {
err!("AuthRequest doesn't exist")
}
};
if headers.user.uuid != auth_request.user_uuid {
err!("AuthRequest doesn't exist", "User uuid's do not match")
}
if auth_request.approved.is_some() {
err!("An authentication request with the same device already exists")
}
let response_date = Utc::now().naive_utc();
let response_date_utc = format_date(&response_date);
if data.request_approved {
auth_request.approved = Some(data.request_approved);
auth_request.enc_key = Some(data.key);
auth_request.master_password_hash = data.master_password_hash;
auth_request.response_device_id = Some(data.device_identifier.clone());
auth_request.response_date = Some(response_date);
auth_request.save(&mut conn).await?;
auth_request.approved = Some(data.request_approved);
auth_request.enc_key = Some(data.key);
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;
} 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"
})))
let response_date_utc = auth_request
.response_date
.map(|response_date| response_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true));
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": auth_request.creation_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true),
"responseDate": response_date_utc,
"requestApproved": auth_request.approved,
"origin": CONFIG.domain_origin(),
"object":"auth-request"
}
)))
}
#[get("/auth-requests/<uuid>/response?<code>")]
async fn get_auth_request_response(
uuid: &str,
code: &str,
client_headers: ClientHeaders,
mut conn: DbConn,
) -> JsonResult {
async fn get_auth_request_response(uuid: &str, code: &str, mut conn: DbConn) -> JsonResult {
let auth_request = match AuthRequest::find_by_uuid(uuid, &mut conn).await {
Some(auth_request) => auth_request,
None => err!("AuthRequest doesn't exist", "User not found"),
None => {
err!("AuthRequest doesn't exist")
}
};
if auth_request.device_type != client_headers.device_type
|| 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")
if !auth_request.check_access_code(code) {
err!("Access code invalid doesn't exist")
}
let response_date_utc = auth_request.response_date.map(|response_date| format_date(&response_date));
let response_date_utc = auth_request
.response_date
.map(|response_date| response_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true));
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"
})))
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": auth_request.creation_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true),
"responseDate": response_date_utc,
"requestApproved": auth_request.approved,
"origin": CONFIG.domain_origin(),
"object":"auth-request"
}
)))
}
#[get("/auth-requests")]
@@ -1335,7 +1261,7 @@ async fn get_auth_requests(headers: Headers, mut conn: DbConn) -> JsonResult {
.iter()
.filter(|request| request.approved.is_none())
.map(|request| {
let response_date_utc = request.response_date.map(|response_date| format_date(&response_date));
let response_date_utc = request.response_date.map(|response_date| response_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true));
json!({
"id": request.uuid,
@@ -1344,7 +1270,7 @@ async fn get_auth_requests(headers: Headers, mut conn: DbConn) -> JsonResult {
"requestIpAddress": request.request_ip,
"key": request.enc_key,
"masterPasswordHash": request.master_password_hash,
"creationDate": format_date(&request.creation_date),
"creationDate": request.creation_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true),
"responseDate": response_date_utc,
"requestApproved": request.approved,
"origin": CONFIG.domain_origin(),

View File

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

View File

@@ -135,13 +135,12 @@ async fn put_eq_domains(data: Json<EquivDomainData>, headers: Headers, conn: DbC
}
#[get("/hibp/breach?<username>")]
async fn hibp_breach(username: &str, _headers: Headers) -> JsonResult {
let username: String = url::form_urlencoded::byte_serialize(username.as_bytes()).collect();
if let Some(api_key) = crate::CONFIG.hibp_api_key() {
let url = format!(
"https://haveibeenpwned.com/api/v3/breachedaccount/{username}?truncateResponse=false&includeUnverified=false"
);
async fn hibp_breach(username: &str) -> JsonResult {
let url = format!(
"https://haveibeenpwned.com/api/v3/breachedaccount/{username}?truncateResponse=false&includeUnverified=false"
);
if let Some(api_key) = crate::CONFIG.hibp_api_key() {
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

View File

@@ -9,8 +9,9 @@ use crate::{
core::{log_event, two_factor, CipherSyncData, CipherSyncType},
EmptyResult, JsonResult, Notify, PasswordOrOtpData, UpdateType,
},
auth::{decode_invite, AdminHeaders, ClientVersion, Headers, ManagerHeaders, ManagerHeadersLoose, OwnerHeaders},
auth::{decode_invite, AdminHeaders, Headers, ManagerHeaders, ManagerHeadersLoose, OwnerHeaders},
db::{models::*, DbConn},
error::Error,
mail,
util::{convert_json_key_lcase_first, NumberOrString},
CONFIG,
@@ -126,7 +127,6 @@ struct NewCollectionData {
name: String,
groups: Vec<NewCollectionObjectData>,
users: Vec<NewCollectionObjectData>,
id: Option<String>,
external_id: Option<String>,
}
@@ -1598,43 +1598,40 @@ async fn post_org_import(
// TODO: See if we can optimize the whole cipher adding/importing and prevent duplicate code and checks.
Cipher::validate_cipher_data(&data.ciphers)?;
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());
let mut collections = Vec::new();
for coll in data.collections {
let collection_uuid = if existing_collections.contains(&coll.id) {
coll.id.unwrap()
let collection = Collection::new(org_id.clone(), coll.name, coll.external_id);
if collection.save(&mut conn).await.is_err() {
collections.push(Err(Error::new("Failed to create Collection", "Failed to create Collection")));
} else {
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);
collections.push(Ok(collection));
}
}
// Read the relations between collections and ciphers
// Ciphers can be in multiple collections at the same time
let mut relations = Vec::with_capacity(data.collection_relationships.len());
let mut relations = Vec::new();
for relation in data.collection_relationships {
relations.push((relation.key, relation.value));
}
let headers: Headers = headers.into();
let mut ciphers: Vec<String> = Vec::with_capacity(data.ciphers.len());
for mut cipher_data in data.ciphers {
// Always clear folder_id's via an organization import
cipher_data.folder_id = None;
let mut ciphers = Vec::new();
for cipher_data in data.ciphers {
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();
ciphers.push(cipher.uuid);
ciphers.push(cipher);
}
// Assign the collections
for (cipher_index, coll_index) in relations {
let cipher_id = &ciphers[cipher_index];
let coll_id = &collections[coll_index];
let cipher_id = &ciphers[cipher_index].uuid;
let coll = &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?;
}
@@ -2308,14 +2305,14 @@ async fn _restore_organization_user(
}
#[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() {
// 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 mut groups_json = Vec::with_capacity(groups.len());
for g in groups {
groups_json.push(g.to_json_details(&mut conn).await)
groups_json.push(g.to_json_details(&headers.org_user.atype, &mut conn).await)
}
groups_json
} else {
@@ -2503,7 +2500,7 @@ async fn add_update_group(
}
#[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() {
err!("Group support is disabled");
}
@@ -2513,7 +2510,7 @@ async fn get_group_details(_org_id: &str, group_id: &str, _headers: AdminHeaders
_ => err!("Group could not be found!"),
};
Ok(Json(group.to_json_details(&mut conn).await))
Ok(Json(group.to_json_details(&(headers.org_user_type as i32), &mut conn).await))
}
#[post("/organizations/<org_id>/groups/<group_id>/delete")]
@@ -3002,20 +2999,18 @@ async fn put_reset_password_enrollment(
// 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.
#[get("/organizations/<org_id>/export")]
async fn get_org_export(
org_id: &str,
headers: AdminHeaders,
client_version: Option<ClientVersion>,
mut conn: DbConn,
) -> Json<Value> {
async fn get_org_export(org_id: &str, headers: AdminHeaders, mut conn: DbConn) -> Json<Value> {
use semver::{Version, VersionReq};
// Since version v2023.1.0 the format of the export is different.
// 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.
// 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
let use_list_response_model = if let Some(client_version) = client_version {
let ver_match = semver::VersionReq::parse("<2023.1.0").unwrap();
ver_match.matches(&client_version.0)
let use_list_response_model = if let Some(client_version) = headers.client_version {
let ver_match = VersionReq::parse("<2023.1.0").unwrap();
let client_version = Version::parse(&client_version).unwrap();
ver_match.matches(&client_version)
} else {
false
};

View File

@@ -211,7 +211,10 @@ impl DuoClient {
nonce,
};
let token = self.encode_duo_jwt(jwt_payload)?;
let token = match 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 mut auth_url = match Url::parse(authz_endpoint.as_str()) {

View File

@@ -165,22 +165,20 @@ async fn _password_login(
// Set the user_uuid here to be passed back used for event logging.
*user_uuid = Some(user.uuid.clone());
// 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
}
)
}
// Check password
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 {
if let Some(auth_request_uuid) = data.auth_request.clone() {
if let Some(auth_request) = AuthRequest::find_by_uuid(auth_request_uuid.as_str(), conn).await {
if !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 {
err!(
"Auth request not found. Try again.",
format!("IP: {}. Username: {}.", ip.ip, username),
@@ -188,24 +186,6 @@ async fn _password_login(
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) {
err!(
@@ -217,8 +197,8 @@ async fn _password_login(
)
}
// Change the KDF Iterations (only when not logging in with an auth request)
if data.auth_request.is_none() && user.password_iterations != CONFIG.password_iterations() {
// Change the KDF Iterations
if user.password_iterations != CONFIG.password_iterations() {
user.password_iterations = CONFIG.password_iterations();
user.set_password(password, None, false, None);
@@ -227,6 +207,17 @@ 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();
if user.verified_at.is_none() && CONFIG.mail_enabled() && CONFIG.signups_verify() {

View File

@@ -1,20 +1,13 @@
use once_cell::sync::Lazy;
use std::path::{Path, PathBuf};
use rocket::{
fs::NamedFile,
http::ContentType,
response::{content::RawCss as Css, content::RawHtml as Html, Redirect},
serde::json::Json,
Catcher, Route,
};
use rocket::{fs::NamedFile, http::ContentType, response::content::RawHtml as Html, serde::json::Json, Catcher, Route};
use serde_json::Value;
use crate::{
api::{core::now, ApiResult, EmptyResult},
auth::decode_file_download,
error::Error,
util::{get_web_vault_version, Cached, SafeString},
util::{Cached, SafeString},
CONFIG,
};
@@ -23,7 +16,7 @@ pub fn routes() -> Vec<Route> {
// crate::utils::LOGGED_ROUTES to make sure they appear in the log
let mut routes = routes![attachments, alive, alive_head, static_files];
if CONFIG.web_vault_enabled() {
routes.append(&mut routes![web_index, web_index_direct, web_index_head, app_id, web_files, vaultwarden_css]);
routes.append(&mut routes![web_index, web_index_head, app_id, web_files]);
}
#[cfg(debug_assertions)]
@@ -52,101 +45,11 @@ fn not_found() -> ApiResult<Html<String>> {
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("/")]
async fn web_index() -> Cached<Option<NamedFile>> {
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("/")]
fn web_index_head() -> EmptyResult {
// Add an explicit HEAD route to prevent uptime monitoring services from

View File

@@ -615,6 +615,7 @@ pub struct AdminHeaders {
pub device: Device,
pub user: User,
pub org_user_type: UserOrgType,
pub client_version: Option<String>,
pub ip: ClientIp,
}
@@ -624,12 +625,14 @@ impl<'r> FromRequest<'r> for AdminHeaders {
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
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 {
Outcome::Success(Self {
host: headers.host,
device: headers.device,
user: headers.user,
org_user_type: headers.org_user_type,
client_version,
ip: headers.ip,
})
} else {
@@ -897,24 +900,3 @@ 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.
/// The default for new users. If changed, it will be updated during login for existing users.
password_iterations: i32, true, def, 600_000;
/// Allow password hints |> Controls whether users can set or show password hints. This setting applies globally to all users.
/// Allow password hints |> Controls whether users can set password hints. This setting applies globally to all users.
password_hints_allowed: bool, true, def, true;
/// Show password hint (Know the risks!) |> Controls whether a password hint should be shown directly in the web page
/// if SMTP service is not configured and password hints are allowed. Not recommended for publicly-accessible instances
/// because this provides unauthenticated access to potentially sensitive data.
/// Show password hint |> Controls whether a password hint should be shown directly in the web page
/// if SMTP service is not configured. Not recommended for publicly-accessible instances as this
/// provides unauthenticated access to potentially sensitive data.
show_password_hint: bool, true, def, false;
/// Admin token/Argon2 PHC |> The plain text token or Argon2 PHC string used to authenticate in this very same page. Changing it here will not deauthorize the current session!
@@ -811,15 +811,8 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
}
// TODO: deal with deprecated flags so they can be removed from this list, cf. #4263
const KNOWN_FLAGS: &[&str] = &[
"autofill-overlay",
"autofill-v2",
"browser-fileless-import",
"extension-refresh",
"fido2-vault-credentials",
"ssh-key-vault-item",
"ssh-agent",
];
const KNOWN_FLAGS: &[&str] =
&["autofill-overlay", "autofill-v2", "browser-fileless-import", "extension-refresh", "fido2-vault-credentials"];
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();
if !invalid_flags.is_empty() {
@@ -1276,16 +1269,11 @@ impl Config {
let hb = load_templates(CONFIG.templates_folder());
hb.render(name, data).map_err(Into::into)
} else {
let hb = &self.inner.read().unwrap().templates;
let hb = &CONFIG.inner.read().unwrap().templates;
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) {
self.inner.write().unwrap().rocket_shutdown_handle = Some(handle);
}
@@ -1324,11 +1312,6 @@ where
reg!($name);
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
@@ -1372,9 +1355,6 @@ where
reg!("404");
reg!(@withfallback "scss/vaultwarden.scss");
reg!("scss/user.vaultwarden.scss");
// And then load user templates to overwrite the defaults
// Use .hbs extension for the files
// Templates get registered with their relative name

View File

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

View File

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

View File

@@ -96,31 +96,7 @@ fn smtp_transport() -> AsyncSmtpTransport<Tokio1Executor> {
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> {
let mut data = data;
sanitize_data(&mut data);
let (subject_html, body_html) = get_template(&format!("{template_name}.html"), &data)?;
let (_subject_text, body_text) = get_template(template_name, &data)?;
Ok((subject_html, body_html, body_text))
@@ -140,10 +116,6 @@ fn get_template(template_name: &str, data: &serde_json::Value) -> Result<(String
None => err!("Template doesn't contain body"),
};
if text_split.next().is_some() {
err!("Template contains more than one body");
}
Ok((subject, body))
}
@@ -287,15 +259,16 @@ pub async fn send_invite(
}
let query_string = match query.query() {
None => err!("Failed to build invite URL query parameters"),
None => err!(format!("Failed to build invite URL query parameters")),
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(
"email/send_org_invite",
json!({
// `url.Url` would place the anchor `#` after the query parameters
"url": format!("{}/#/accept-organization/?{}", CONFIG.domain(), query_string),
"url": url,
"img_src": CONFIG._smtp_img_src(),
"org_name": org_name,
}),
@@ -319,29 +292,17 @@ pub async fn send_emergency_access_invite(
String::from(grantor_email),
);
// 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 invite_token = encode_jwt(&claims);
let (subject, body_html, body_text) = get_text(
"email/send_emergency_access_invite",
json!({
// `url.Url` would place the anchor `#` after the query parameters
"url": format!("{}/#/accept-emergency/?{query_string}", CONFIG.domain()),
"url": CONFIG.domain(),
"img_src": CONFIG._smtp_img_src(),
"emer_id": emer_id,
"email": percent_encode(address.as_bytes(), NON_ALPHANUMERIC).to_string(),
"grantor_name": grantor_name,
"token": invite_token,
}),
)?;

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:
Click here to join: {{{url}}}
Click here to join: {{url}}/#/accept-emergency/?id={{emer_id}}&name={{grantor_name}}&email={{email}}&token={{token}}
If you do not wish to become an emergency contact for {{grantor_name}}, you can safely ignore this email.
{{> email/email_footer_text }}

View File

@@ -9,7 +9,7 @@ Emergency access for {{{grantor_name}}}
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
<a href="{{{url}}}"
<a href="{{url}}/#/accept-emergency/?id={{emer_id}}&name={{grantor_name}}&email={{email}}&token={{token}}"
clicktracking=off target="_blank" style="color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #3c8dbc; border-color: #3c8dbc; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
Become emergency contact
</a>
@@ -21,4 +21,4 @@ Emergency access for {{{grantor_name}}}
</td>
</tr>
</table>
{{> email/email_footer }}
{{> email/email_footer }}

View File

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

View File

@@ -1,105 +0,0 @@
/**** 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,19 +438,13 @@ pub fn get_env_bool(key: &str) -> Option<bool> {
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
/// responses with "date" fields (`CreationDate`, `RevisionDate`, etc.).
pub fn format_date(dt: &NaiveDateTime) -> 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"),
}
dt.format(DATETIME_FORMAT).to_string()
}
/// Formats a `DateTime<Local>` using the specified format string.
@@ -492,7 +486,7 @@ pub fn format_datetime_http(dt: &DateTime<Local>) -> String {
}
pub fn parse_date(date: &str) -> NaiveDateTime {
DateTime::parse_from_rfc3339(date).unwrap().naive_utc()
NaiveDateTime::parse_from_str(date, DATETIME_FORMAT).unwrap()
}
//