mirror of
https://github.com/dani-garcia/vaultwarden.wiki.git
synced 2026-10-25 20:16:49 +02:00
Compare commits
5
Commits
f4696877b9
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44c4853d37 | ||
|
|
e5a90968f2 | ||
|
|
7ce705be56 | ||
|
|
c925c04de0 | ||
|
|
b71cb7a832 |
No files matched your search
+3
-3
@@ -78,7 +78,7 @@
|
||||
# DB_CONNECTION_RETRIES=15
|
||||
|
||||
## Database timeout
|
||||
## Timeout when acquiring database connection
|
||||
## Timeout in seconds when acquiring database connection
|
||||
# DATABASE_TIMEOUT=30
|
||||
|
||||
## Database idle timeout
|
||||
@@ -173,7 +173,7 @@
|
||||
## Defaults to daily. Set blank to disable this job. Also without EVENTS_DAYS_RETAIN set, this job will not start.
|
||||
# EVENT_CLEANUP_SCHEDULE="0 10 0 * * *"
|
||||
## Number of days to retain events stored in the database.
|
||||
## If unset (the default), events are kept indefinitely and the scheduled job is disabled!
|
||||
## If unset (the default) or 0, events are kept indefinitely and the scheduled job is disabled!
|
||||
# EVENTS_DAYS_RETAIN=
|
||||
##
|
||||
## Cron schedule of the job that cleans old auth requests from the auth request.
|
||||
@@ -233,7 +233,7 @@
|
||||
# USER_SEND_LIMIT=
|
||||
|
||||
## Number of days to wait before auto-deleting a trashed item.
|
||||
## If unset (the default), trashed items are not auto-deleted.
|
||||
## If unset (the default) or 0, trashed items are not auto-deleted.
|
||||
## This setting applies globally, so make sure to inform all users of any changes to this setting.
|
||||
# TRASH_AUTO_DELETE_DAYS=
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ jobs:
|
||||
|
||||
# Login to Docker Hub
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -121,7 +121,7 @@ jobs:
|
||||
|
||||
# Login to GitHub Container Registry
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -137,7 +137,7 @@ jobs:
|
||||
|
||||
# Login to Quay.io
|
||||
- name: Login to Quay.io
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
|
||||
with:
|
||||
registry: quay.io
|
||||
username: ${{ secrets.QUAY_USERNAME }}
|
||||
@@ -272,7 +272,7 @@ jobs:
|
||||
|
||||
# Login to Docker Hub
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -287,7 +287,7 @@ jobs:
|
||||
|
||||
# Login to GitHub Container Registry
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -303,7 +303,7 @@ jobs:
|
||||
|
||||
# Login to Quay.io
|
||||
- name: Login to Quay.io
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
|
||||
with:
|
||||
registry: quay.io
|
||||
username: ${{ secrets.QUAY_USERNAME }}
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run zizmor
|
||||
uses: zizmorcore/zizmor-action@85b2b9fb8e3325e7a23d7d0a9d7ba5ae0f999e3e # v0.7.1
|
||||
uses: zizmorcore/zizmor-action@53290badfa369cb0bf7ad16342cb62c2fa47ac7b # v0.8.0
|
||||
with:
|
||||
# intentionally not scanning the entire repository,
|
||||
# since it contains integration tests.
|
||||
|
||||
Generated
+123
-179
@@ -323,15 +323,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "asyncband"
|
||||
version = "0.7.3"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5161fb43cb8f231f06983cb2b8f9ee9cee14be0b1356d8383398cc0f312800d1"
|
||||
|
||||
[[package]]
|
||||
name = "atomic"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba"
|
||||
checksum = "5d2050892d4691cf110586070786712b504985a4d37ed64a9cd8a402718e1706"
|
||||
|
||||
[[package]]
|
||||
name = "atomic"
|
||||
@@ -415,7 +409,7 @@ dependencies = [
|
||||
"bytes-utils",
|
||||
"fastrand",
|
||||
"http 1.5.0",
|
||||
"http-body 1.1.0",
|
||||
"http-body",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"tracing",
|
||||
@@ -424,9 +418,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-sso"
|
||||
version = "1.116.0"
|
||||
version = "1.117.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f26d2c93a4e6e7c133dc4090dc2273cfd92894fee8b883256427b57cacc3a384"
|
||||
checksum = "5316813498fa9c403877007934ae448aa44b18136f58051f8fa3c5b79b44bdc6"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"aws-credential-types",
|
||||
@@ -449,9 +443,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-ssooidc"
|
||||
version = "1.118.0"
|
||||
version = "1.119.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6fa86283d1c6d5c82f1fb2d82f7704d8f7a7bf5f0591813114240d296f6a11c"
|
||||
checksum = "31db35e5662787b786daba15f431e4393c7ea512b70fa112bd048d1f3ecf877a"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"aws-credential-types",
|
||||
@@ -474,9 +468,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-sts"
|
||||
version = "1.121.0"
|
||||
version = "1.122.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97ff04fe350fd756c1aa45544128e88f72ffb6281cad628e5a4add464cbbab8b"
|
||||
checksum = "a10491638d9e560db90c484dbc77d73b8ad202780cdc9b9e92180603acb07ada"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"aws-credential-types",
|
||||
@@ -500,9 +494,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sigv4"
|
||||
version = "1.6.2"
|
||||
version = "1.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0a3ccec0607ae3839cd76830df5ed62468ac1330acdb3dc4ec0502404113d2b6"
|
||||
checksum = "9d3d48355cb86b15ccbb38b8bc6b87500c824ee4ed8cbc10a961b435242b5e16"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-smithy-http",
|
||||
@@ -543,7 +537,7 @@ dependencies = [
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"http 1.5.0",
|
||||
"http-body 1.1.0",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
@@ -598,7 +592,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"fastrand",
|
||||
"http 1.5.0",
|
||||
"http-body 1.1.0",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
@@ -654,7 +648,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"bytes-utils",
|
||||
"http 1.5.0",
|
||||
"http-body 1.1.0",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"itoa",
|
||||
"num-integer",
|
||||
@@ -815,9 +809,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brotli"
|
||||
version = "9.0.0"
|
||||
version = "9.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8b851b75c23ca7873623d612fe49bd1989aeb03d08fb9432187eb253d3d4c6b"
|
||||
checksum = "6737ce8b81460bf9407909f73ff50bf13f7cb8577e1e76715f57ed0eba58ba44"
|
||||
dependencies = [
|
||||
"alloc-no-stdlib",
|
||||
"alloc-stdlib",
|
||||
@@ -826,9 +820,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brotli-decompressor"
|
||||
version = "6.0.1"
|
||||
version = "6.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "941cd9bd4ddab83cb46fa5a2d428f1c857b24ac78cb876cf7beb710840934bd7"
|
||||
checksum = "34c6783f42c21c952ad7112b354f0ba2f10760e9decea5b1b60523d6e270faaf"
|
||||
dependencies = [
|
||||
"alloc-no-stdlib",
|
||||
"alloc-stdlib",
|
||||
@@ -914,9 +908,9 @@ checksum = "f5813789573ae815c8b4be58c4428e0e7ae05f0227678ba9de332ded585b9159"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.9.0"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877bf67ac8e52449eb2f9e98a344ea163beb70dd1f9eebe3cc5357dcfeaee4df"
|
||||
checksum = "86e055fa6e3e40ad6187ba25b6855eeb635d5271f8e0f0e1e9a5826b8d75bea0"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
@@ -1242,9 +1236,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ctutils"
|
||||
version = "0.4.3"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03bb0e1cc970d482d121d9a1744999169b69a07470b3d644a7894e53fcaf4574"
|
||||
checksum = "38d28fb6aa7dd680b3ffd1a3e64e115c177bdba63c0ae7f9cd48129ff05c4fa0"
|
||||
dependencies = [
|
||||
"cmov",
|
||||
]
|
||||
@@ -1527,9 +1521,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "devise"
|
||||
version = "0.4.2"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1d90b0c4c777a2cad215e3c7be59ac7c15adf45cf76317009b7d096d46f651d"
|
||||
checksum = "143afb21c617f66c792ef2d75f9fb4c9dd774ff206bcc24df08a1bda294a37bc"
|
||||
dependencies = [
|
||||
"devise_codegen",
|
||||
"devise_core",
|
||||
@@ -1537,9 +1531,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "devise_codegen"
|
||||
version = "0.4.2"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71b28680d8be17a570a2334922518be6adc3f58ecc880cbb404eaeb8624fd867"
|
||||
checksum = "50b3187e4283046e01d16d724ed05326427aa9c1c3a376548ef31820e6eb9717"
|
||||
dependencies = [
|
||||
"devise_core",
|
||||
"quote",
|
||||
@@ -1547,15 +1541,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "devise_core"
|
||||
version = "0.4.2"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b035a542cf7abf01f2e3c4d5a7acbaebfefe120ae4efc7bde3df98186e4b8af7"
|
||||
checksum = "970e4b2fcd605ffa79715510663a9fe76b87f5483d59857399129cf047f73d27"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"proc-macro2",
|
||||
"proc-macro2-diagnostics",
|
||||
"proc-macro2-diagnostics 0.11.0",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
"syn 3.0.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1893,8 +1887,8 @@ version = "0.10.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3"
|
||||
dependencies = [
|
||||
"atomic 0.6.1",
|
||||
"pear",
|
||||
"atomic",
|
||||
"pear 0.2.9",
|
||||
"serde",
|
||||
"toml 0.8.23",
|
||||
"uncased",
|
||||
@@ -2430,17 +2424,6 @@ dependencies = [
|
||||
"itoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http 0.2.12",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body"
|
||||
version = "1.1.0"
|
||||
@@ -2460,7 +2443,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"http 1.5.0",
|
||||
"http-body 1.1.0",
|
||||
"http-body",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
@@ -2485,29 +2468,6 @@ dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "0.14.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"http 0.2.12",
|
||||
"http-body 0.4.6",
|
||||
"httparse",
|
||||
"httpdate",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"socket2 0.5.10",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"want",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.12.0"
|
||||
@@ -2520,8 +2480,9 @@ dependencies = [
|
||||
"futures-core",
|
||||
"h2",
|
||||
"http 1.5.0",
|
||||
"http-body 1.1.0",
|
||||
"http-body",
|
||||
"httparse",
|
||||
"httpdate",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"smallvec",
|
||||
@@ -2536,11 +2497,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53"
|
||||
dependencies = [
|
||||
"http 1.5.0",
|
||||
"hyper 1.12.0",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"rustls 0.23.46",
|
||||
"rustls",
|
||||
"tokio",
|
||||
"tokio-rustls 0.26.6",
|
||||
"tokio-rustls",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
@@ -2555,14 +2516,14 @@ dependencies = [
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"http 1.5.0",
|
||||
"http-body 1.1.0",
|
||||
"http-body",
|
||||
"httparse",
|
||||
"hyper 1.12.0",
|
||||
"hyper",
|
||||
"ipnet",
|
||||
"libc",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2 0.6.5",
|
||||
"socket2",
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
@@ -2739,7 +2700,7 @@ version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222"
|
||||
dependencies = [
|
||||
"socket2 0.6.5",
|
||||
"socket2",
|
||||
"widestring",
|
||||
"windows-registry",
|
||||
"windows-result",
|
||||
@@ -2996,12 +2957,12 @@ dependencies = [
|
||||
"nom 8.0.0",
|
||||
"percent-encoding",
|
||||
"quoted_printable",
|
||||
"rustls 0.23.46",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
"serde",
|
||||
"socket2 0.6.5",
|
||||
"socket2",
|
||||
"tokio",
|
||||
"tokio-rustls 0.26.6",
|
||||
"tokio-rustls",
|
||||
"tracing",
|
||||
"url",
|
||||
]
|
||||
@@ -3484,7 +3445,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"futures",
|
||||
"http 1.5.0",
|
||||
"http-body 1.1.0",
|
||||
"http-body",
|
||||
"opendal-core",
|
||||
"reqwest",
|
||||
]
|
||||
@@ -3715,7 +3676,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bdeeaa00ce488657faba8ebf44ab9361f9365a97bd39ffb8a60663f57ff4b467"
|
||||
dependencies = [
|
||||
"inlinable_string",
|
||||
"pear_codegen",
|
||||
"pear_codegen 0.2.9",
|
||||
"yansi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pear"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4bdc582f5ece4663ba62cb9cc81b9f86aa23884ee92cddb1d5e75a94991119cb"
|
||||
dependencies = [
|
||||
"inlinable_string",
|
||||
"pear_codegen 0.3.0",
|
||||
"yansi",
|
||||
]
|
||||
|
||||
@@ -3726,11 +3698,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4bab5b985dc082b345f812b7df84e1bef27e7207b39e448439ba8bd69c93f147"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"proc-macro2-diagnostics",
|
||||
"proc-macro2-diagnostics 0.10.1",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pear_codegen"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b4262794e70dac544a759fd50088c6331e7a2ba4fc204b4b6e29c9b5257f9c4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"proc-macro2-diagnostics 0.11.0",
|
||||
"quote",
|
||||
"syn 3.0.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pem"
|
||||
version = "3.0.6"
|
||||
@@ -4042,6 +4026,18 @@ dependencies = [
|
||||
"yansi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2-diagnostics"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df921e1ef974ebc8dd9effdb0c037a96928ae8cbf871438e02d6fd67204b595e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.9",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psl-types"
|
||||
version = "2.0.11"
|
||||
@@ -4250,6 +4246,12 @@ dependencies = [
|
||||
"syn 3.0.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ref-swap"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09c30c54dffee5b40af088d5d50aa3455c91a0127164b51f0215efc4cb28fb3c"
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.13.1"
|
||||
@@ -4379,9 +4381,9 @@ dependencies = [
|
||||
"futures-util",
|
||||
"h2",
|
||||
"http 1.5.0",
|
||||
"http-body 1.1.0",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper 1.12.0",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"hyper-util",
|
||||
"js-sys",
|
||||
@@ -4389,7 +4391,7 @@ dependencies = [
|
||||
"mime",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustls 0.23.46",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"rustls-platform-verifier",
|
||||
"serde",
|
||||
@@ -4397,7 +4399,7 @@ dependencies = [
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-rustls 0.26.6",
|
||||
"tokio-rustls",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
"tower-http",
|
||||
@@ -4459,18 +4461,19 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rocket"
|
||||
version = "0.5.1"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a516907296a31df7dc04310e7043b61d71954d703b603cc6867a026d7e72d73f"
|
||||
checksum = "c17431360eb641a861453b57107ee1b425a799f49ec1b0fe9b8a45fe437447ce"
|
||||
dependencies = [
|
||||
"async-stream",
|
||||
"async-trait",
|
||||
"atomic 0.5.3",
|
||||
"binascii",
|
||||
"bytes",
|
||||
"either",
|
||||
"figment",
|
||||
"futures",
|
||||
"http-body-util",
|
||||
"hyper-util",
|
||||
"indexmap 2.14.2",
|
||||
"log",
|
||||
"memchr",
|
||||
@@ -4478,8 +4481,8 @@ dependencies = [
|
||||
"num_cpus",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"rand 0.8.8",
|
||||
"ref-cast",
|
||||
"ref-swap",
|
||||
"rocket_codegen",
|
||||
"rocket_http",
|
||||
"serde",
|
||||
@@ -4497,48 +4500,50 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rocket_codegen"
|
||||
version = "0.5.1"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "575d32d7ec1a9770108c879fc7c47815a80073f96ca07ff9525a94fcede1dd46"
|
||||
checksum = "352bd0b773ca6f5bea6cd18efc2fd7f04fcd34c75c242dad6ef1bbd729cc619d"
|
||||
dependencies = [
|
||||
"devise",
|
||||
"glob",
|
||||
"indexmap 2.14.2",
|
||||
"proc-macro2",
|
||||
"proc-macro2-diagnostics 0.11.0",
|
||||
"quote",
|
||||
"rocket_http",
|
||||
"syn 2.0.119",
|
||||
"syn 3.0.9",
|
||||
"unicode-xid",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rocket_http"
|
||||
version = "0.5.1"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e274915a20ee3065f611c044bd63c40757396b6dbc057d6046aec27f14f882b9"
|
||||
checksum = "847886ef963dff77126f110fd2b4401f5149440ab32973bef16c4a2cc83e525f"
|
||||
dependencies = [
|
||||
"cookie",
|
||||
"either",
|
||||
"futures",
|
||||
"http 0.2.12",
|
||||
"hyper 0.14.32",
|
||||
"hyper",
|
||||
"indexmap 2.14.2",
|
||||
"log",
|
||||
"memchr",
|
||||
"pear",
|
||||
"pear 0.3.0",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"ref-cast",
|
||||
"rustls 0.21.12",
|
||||
"rustls-pemfile",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki",
|
||||
"serde",
|
||||
"smallvec",
|
||||
"stable-pattern",
|
||||
"state",
|
||||
"time",
|
||||
"tokio",
|
||||
"tokio-rustls 0.24.1",
|
||||
"tokio-rustls",
|
||||
"uncased",
|
||||
]
|
||||
|
||||
@@ -4643,18 +4648,6 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.21.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e"
|
||||
dependencies = [
|
||||
"log",
|
||||
"ring",
|
||||
"rustls-webpki 0.101.7",
|
||||
"sct",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.46"
|
||||
@@ -4665,7 +4658,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki 0.103.16",
|
||||
"rustls-webpki",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
@@ -4682,15 +4675,6 @@ dependencies = [
|
||||
"security-framework",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pemfile"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
|
||||
dependencies = [
|
||||
"base64 0.21.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.15.1"
|
||||
@@ -4711,10 +4695,10 @@ dependencies = [
|
||||
"jni",
|
||||
"log",
|
||||
"once_cell",
|
||||
"rustls 0.23.46",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
"rustls-platform-verifier-android",
|
||||
"rustls-webpki 0.103.16",
|
||||
"rustls-webpki",
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"webpki-root-certs",
|
||||
@@ -4727,16 +4711,6 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eec689c0bc40ff2458a5977b6619cb718087084a18e02a131c599b62d05e1a5f"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.101.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.16"
|
||||
@@ -4823,16 +4797,6 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "sct"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sec1"
|
||||
version = "0.7.3"
|
||||
@@ -5159,19 +5123,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.16.3"
|
||||
version = "1.16.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.5.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
checksum = "d0d5d4b1981b8bbbb7aed204cae5726d683d5d6ac7e74ec54122ea28f315e7c8"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
@@ -5509,7 +5463,7 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2 0.6.5",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
@@ -5525,23 +5479,13 @@ dependencies = [
|
||||
"syn 3.0.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.24.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
|
||||
dependencies = [
|
||||
"rustls 0.21.12",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.26.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9cc2678c2cdd569ef8215e2afd7954ada2ae20b4fdd2c5fe6139a3b02d105db"
|
||||
dependencies = [
|
||||
"rustls 0.23.46",
|
||||
"rustls",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
@@ -5715,7 +5659,7 @@ dependencies = [
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"http 1.5.0",
|
||||
"http-body 1.1.0",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
@@ -5913,9 +5857,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.28.0"
|
||||
version = "1.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cc1186384beb7dd8eedea376413fd654937285ea6c9cfbb928dc3043ea4b606"
|
||||
checksum = "4a0e2d11620c50695fe12bbbfde96b3477fe606e7284c22d505c12ce51214514"
|
||||
dependencies = [
|
||||
"getrandom 0.4.3",
|
||||
"js-sys",
|
||||
@@ -5996,7 +5940,7 @@ dependencies = [
|
||||
"rocket",
|
||||
"rocket_ws",
|
||||
"rpassword",
|
||||
"rustls 0.23.46",
|
||||
"rustls",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -6228,9 +6172,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "webpki-root-certs"
|
||||
version = "1.0.10"
|
||||
version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "58ec80e73fd8baf57bf0d9615554c75348bda990d6d5bbeab964dd068319fecb"
|
||||
checksum = "e49d0e65abc00c142e8f9428da991da9f1a250c39be97459901eb12b9b49f62a"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
+2
-2
@@ -80,7 +80,7 @@ num-derive = "0.5.1"
|
||||
bigdecimal = "0.4.11"
|
||||
|
||||
# Web framework
|
||||
rocket = { version = "0.5.1", default-features = false, features = ["json", "tls"] }
|
||||
rocket = { version = "0.5.2", default-features = false, features = ["json", "tls"] }
|
||||
rocket_ws = { version = "0.1.1" }
|
||||
|
||||
# WebSockets libraries
|
||||
@@ -129,7 +129,7 @@ rustls = { version = "0.23.46", features = ["ring", "std"], default-features = f
|
||||
subtle = "2.6.1"
|
||||
|
||||
# UUID generation
|
||||
uuid = { version = "1.28.0", features = ["v4"] }
|
||||
uuid = { version = "1.29.0", features = ["v4"] }
|
||||
|
||||
# Date and time libraries
|
||||
chrono = { version = "0.4.45", default-features = false, features = ["clock", "serde"] }
|
||||
|
||||
+9
-2
@@ -201,12 +201,15 @@ fn post_admin_login(
|
||||
// If the token is invalid, redirect to login page
|
||||
if validate_token(&data.token) {
|
||||
// If the token received is valid, generate JWT and save it as a cookie
|
||||
let claims = generate_admin_claims();
|
||||
let claims = match generate_admin_claims() {
|
||||
Ok(claims) => claims,
|
||||
Err(e) => return Err(AdminResponse::Ok(render_admin_login(Some(e.message()), redirect.as_deref()))),
|
||||
};
|
||||
let jwt = encode_jwt(&claims);
|
||||
|
||||
let cookie = Cookie::build((COOKIE_NAME, jwt))
|
||||
.path(admin_path())
|
||||
.max_age(time::Duration::minutes(CONFIG.admin_session_lifetime()))
|
||||
.max_age(time::Duration::minutes(i64::from(CONFIG.admin_session_lifetime())))
|
||||
.same_site(SameSite::Strict)
|
||||
.http_only(true)
|
||||
.secure(secure.https);
|
||||
@@ -779,6 +782,8 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> A
|
||||
.into_keys()
|
||||
.collect();
|
||||
|
||||
let invalid_env_vars = crate::util::INVALID_ENV_VARS.lock().unwrap().join(", ");
|
||||
|
||||
let diagnostics_json = json!({
|
||||
"dns_resolved": dns_resolved,
|
||||
"current_release": VERSION,
|
||||
@@ -801,7 +806,9 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> A
|
||||
"db_version": get_sql_server_version(&conn).await,
|
||||
"admin_url": format!("{}/diagnostics", admin_url()),
|
||||
"overrides": &CONFIG.get_overrides().join(", "),
|
||||
"config_file_status": &CONFIG.get_config_file_status(),
|
||||
"template_overrides": check_template_overrides().join(", "),
|
||||
"invalid_env_vars": invalid_env_vars,
|
||||
"invalid_feature_flags": invalid_feature_flags,
|
||||
"host_arch": env::consts::ARCH,
|
||||
"host_os": env::consts::OS,
|
||||
|
||||
+60
-6
@@ -1018,14 +1018,53 @@ struct ShareSelectedCipherData {
|
||||
collection_ids: Vec<CollectionId>,
|
||||
}
|
||||
|
||||
// A bulk share returns cipherMini models, not the cipherDetails models used by
|
||||
// the single-share route. Keep the encrypted legacy fields and a future v2
|
||||
// `data` blob, but do not expose details-only fields in this response.
|
||||
fn cipher_mini_from_json(mut cipher: Value) -> Value {
|
||||
const FIELDS: &[&str] = &[
|
||||
"id",
|
||||
"organizationId",
|
||||
"type",
|
||||
"data",
|
||||
"partialData",
|
||||
"name",
|
||||
"notes",
|
||||
"login",
|
||||
"card",
|
||||
"identity",
|
||||
"secureNote",
|
||||
"sshKey",
|
||||
"bankAccount",
|
||||
"driversLicense",
|
||||
"passport",
|
||||
"fields",
|
||||
"passwordHistory",
|
||||
"attachments",
|
||||
"organizationUseTotp",
|
||||
"revisionDate",
|
||||
"creationDate",
|
||||
"deletedDate",
|
||||
"reprompt",
|
||||
"key",
|
||||
];
|
||||
|
||||
let Some(object) = cipher.as_object_mut() else {
|
||||
return json!({ "object": "cipherMini" });
|
||||
};
|
||||
object.retain(|field, _| FIELDS.contains(&field.as_str()));
|
||||
object.insert("object".into(), "cipherMini".into());
|
||||
cipher
|
||||
}
|
||||
|
||||
#[put("/ciphers/share", data = "<data>")]
|
||||
async fn put_cipher_share_selected(
|
||||
data: Json<ShareSelectedCipherData>,
|
||||
headers: Headers,
|
||||
conn: DbConn,
|
||||
nt: Notify<'_>,
|
||||
) -> EmptyResult {
|
||||
let mut data: ShareSelectedCipherData = data.into_inner();
|
||||
) -> JsonResult {
|
||||
let data: ShareSelectedCipherData = data.into_inner();
|
||||
|
||||
if data.ciphers.is_empty() {
|
||||
err!("You must select at least one cipher.")
|
||||
@@ -1041,23 +1080,38 @@ async fn put_cipher_share_selected(
|
||||
}
|
||||
}
|
||||
|
||||
while let Some(cipher) = data.ciphers.pop() {
|
||||
let mut responses = Vec::with_capacity(data.ciphers.len());
|
||||
for cipher in data.ciphers {
|
||||
let mut shared_cipher_data = ShareCipherData {
|
||||
cipher,
|
||||
collection_ids: data.collection_ids.clone(),
|
||||
};
|
||||
|
||||
if let Some(id) = shared_cipher_data.cipher.id.take() {
|
||||
share_cipher_by_uuid(&id, shared_cipher_data, &headers, &conn, &nt, Some(UpdateType::None)).await?
|
||||
let response =
|
||||
share_cipher_by_uuid(&id, shared_cipher_data, &headers, &conn, &nt, Some(UpdateType::None)).await?;
|
||||
|
||||
// Like upstream's VisibleToClient, withhold a cipher if sharing it
|
||||
// removed the user's access to it. Only then include its encrypted
|
||||
// mini response; the full single-share response stays internal.
|
||||
if let Some(shared_cipher) = Cipher::find_by_uuid(&id, &conn).await
|
||||
&& shared_cipher.is_accessible_to_user(&headers.user.uuid, &conn).await
|
||||
{
|
||||
responses.push(cipher_mini_from_json(response.into_inner()));
|
||||
}
|
||||
} else {
|
||||
err!("Request missing ids field")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Multi share actions do not send out a push for each cipher, we need to send a general sync here
|
||||
nt.send_user_update(UpdateType::SyncCiphers, &headers.user, headers.device.push_uuid.as_ref(), &conn).await;
|
||||
|
||||
Ok(())
|
||||
Ok(Json(json!({
|
||||
"data": responses,
|
||||
"object": "list",
|
||||
"continuationToken": null,
|
||||
})))
|
||||
}
|
||||
|
||||
async fn share_cipher_by_uuid(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use chrono::{TimeDelta, Utc};
|
||||
use chrono::Utc;
|
||||
use rocket::{Route, serde::json::Json};
|
||||
use serde_json::Value;
|
||||
|
||||
@@ -15,8 +15,8 @@ use crate::{
|
||||
db::{
|
||||
DbConn, DbPool,
|
||||
models::{
|
||||
Cipher, EmergencyAccess, EmergencyAccessId, EmergencyAccessStatus, EmergencyAccessType, Invitation,
|
||||
Membership, MembershipType, OrgPolicy, TwoFactor, User, UserId,
|
||||
Cipher, EMERGENCY_ACCESS_TIMEOUT_RANGE, EmergencyAccess, EmergencyAccessId, EmergencyAccessStatus,
|
||||
EmergencyAccessType, Invitation, Membership, MembershipType, OrgPolicy, TwoFactor, User, UserId,
|
||||
},
|
||||
},
|
||||
mail,
|
||||
@@ -136,19 +136,21 @@ async fn post_emergency_access(
|
||||
|
||||
let data: EmergencyAccessUpdateData = data.into_inner();
|
||||
|
||||
let Some(new_type) = EmergencyAccessType::from_str(&data.r#type.into_string()) else {
|
||||
err!("Invalid emergency access type.")
|
||||
};
|
||||
|
||||
if !(EMERGENCY_ACCESS_TIMEOUT_RANGE).contains(&data.wait_time_days) {
|
||||
err!("Emergency access wait time is out-of-bounds!")
|
||||
}
|
||||
|
||||
let Some(mut emergency_access) =
|
||||
EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &conn).await
|
||||
else {
|
||||
err!("Emergency access not valid.")
|
||||
};
|
||||
|
||||
let new_type = if let Some(new_type) = EmergencyAccessType::from_str(&data.r#type.into_string()) {
|
||||
new_type as i32
|
||||
} else {
|
||||
err!("Invalid emergency access type.")
|
||||
};
|
||||
|
||||
emergency_access.atype = new_type;
|
||||
emergency_access.atype = new_type as i32;
|
||||
emergency_access.wait_time_days = data.wait_time_days;
|
||||
if data.key_encrypted.is_some() {
|
||||
emergency_access.key_encrypted = data.key_encrypted;
|
||||
@@ -210,11 +212,11 @@ async fn send_invite(data: Json<EmergencyAccessInviteData>, headers: Headers, co
|
||||
let email = data.email.to_lowercase();
|
||||
let wait_time_days = data.wait_time_days;
|
||||
|
||||
let emergency_access_status = EmergencyAccessStatus::Invited as i32;
|
||||
if !(EMERGENCY_ACCESS_TIMEOUT_RANGE).contains(&wait_time_days) {
|
||||
err!("Emergency access wait time is out-of-bounds!")
|
||||
}
|
||||
|
||||
let new_type = if let Some(new_type) = EmergencyAccessType::from_str(&data.r#type.into_string()) {
|
||||
new_type as i32
|
||||
} else {
|
||||
let Some(new_type) = EmergencyAccessType::from_str(&data.r#type.into_string()) else {
|
||||
err!("Invalid emergency access type.")
|
||||
};
|
||||
|
||||
@@ -260,6 +262,8 @@ async fn send_invite(data: Json<EmergencyAccessInviteData>, headers: Headers, co
|
||||
err!(format!("Grantee user already invited: {}", &grantee_user.email))
|
||||
}
|
||||
|
||||
let new_type = new_type as i32;
|
||||
let emergency_access_status = EmergencyAccessStatus::Invited as i32;
|
||||
let mut new_emergency_access =
|
||||
EmergencyAccess::new(grantor_user.uuid, grantee_user.email, emergency_access_status, new_type, wait_time_days);
|
||||
new_emergency_access.save(&conn).await?;
|
||||
@@ -292,7 +296,7 @@ async fn resend_invite(emer_id: EmergencyAccessId, headers: Headers, conn: DbCon
|
||||
};
|
||||
|
||||
if emergency_access.status != EmergencyAccessStatus::Invited as i32 {
|
||||
err!("The grantee user is already accepted or confirmed to the organization");
|
||||
err!("The grantee user is already accepted or confirmed for emergency access");
|
||||
}
|
||||
|
||||
let Some(email) = emergency_access.email.clone() else {
|
||||
@@ -453,11 +457,24 @@ async fn initiate_emergency_access(emer_id: EmergencyAccessId, headers: Headers,
|
||||
let Some(mut emergency_access) =
|
||||
EmergencyAccess::find_by_uuid_and_grantee_uuid(&emer_id, &initiating_user.uuid, &conn).await
|
||||
else {
|
||||
err!("Emergency access not valid.")
|
||||
err!(
|
||||
"Emergency access not valid.",
|
||||
format!("Emergency access '{emer_id}', not found for user '{}'.", initiating_user.email)
|
||||
)
|
||||
};
|
||||
|
||||
if emergency_access.status != EmergencyAccessStatus::Confirmed as i32 {
|
||||
err!("Emergency access not valid.")
|
||||
err!(
|
||||
"Emergency access not valid.",
|
||||
format!("Emergency access '{emer_id}', not confirmed for user '{}'.", initiating_user.email)
|
||||
)
|
||||
}
|
||||
|
||||
if !EMERGENCY_ACCESS_TIMEOUT_RANGE.contains(&emergency_access.wait_time_days) {
|
||||
err!(
|
||||
"Emergency access not valid.",
|
||||
format!("Emergency access '{emer_id}' timeout range '{}' is invalid", emergency_access.wait_time_days)
|
||||
)
|
||||
}
|
||||
|
||||
let Some(grantor_user) = User::find_by_uuid(&emergency_access.grantor_uuid, &conn).await else {
|
||||
@@ -490,11 +507,17 @@ async fn approve_emergency_access(emer_id: EmergencyAccessId, headers: Headers,
|
||||
let Some(mut emergency_access) =
|
||||
EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &conn).await
|
||||
else {
|
||||
err!("Emergency access not valid.")
|
||||
err!(
|
||||
"Emergency access not valid.",
|
||||
format!("Emergency access '{emer_id}', not found for user '{}'.", headers.user.email)
|
||||
)
|
||||
};
|
||||
|
||||
if emergency_access.status != EmergencyAccessStatus::RecoveryInitiated as i32 {
|
||||
err!("Emergency access not valid.")
|
||||
err!(
|
||||
"Emergency access not valid.",
|
||||
format!("Emergency access '{emer_id}', not initiated for user '{}'.", headers.user.email)
|
||||
)
|
||||
}
|
||||
|
||||
let Some(grantor_user) = User::find_by_uuid(&headers.user.uuid, &conn).await else {
|
||||
@@ -747,53 +770,72 @@ pub async fn emergency_request_timeout_job(pool: DbPool) {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Ok(conn) = pool.get().await {
|
||||
let Ok(conn) = pool.get().await else {
|
||||
error!("Failed to get DB connection for emergency time-outs!");
|
||||
return;
|
||||
};
|
||||
|
||||
let emergency_access_list = EmergencyAccess::find_all_recoveries_initiated(&conn).await;
|
||||
|
||||
if emergency_access_list.is_empty() {
|
||||
debug!("No emergency request timeout to approve");
|
||||
return;
|
||||
}
|
||||
|
||||
let now = Utc::now().naive_utc();
|
||||
for mut emer in emergency_access_list {
|
||||
// The find_all_recoveries_initiated already checks if the recovery_initiated_at is not null (None)
|
||||
let recovery_allowed_at =
|
||||
emer.recovery_initiated_at.unwrap() + TimeDelta::try_days(i64::from(emer.wait_time_days)).unwrap();
|
||||
if recovery_allowed_at.le(&now) {
|
||||
match emer.is_recovery_allowed(&now) {
|
||||
Ok(true) => {} // Recovery is allowed
|
||||
Ok(false) => continue,
|
||||
Err(e) => {
|
||||
error!("Failed to check if recovery is allowed for '{}': {e}", emer.uuid);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Get grantor user to send Accepted email
|
||||
let Some(grantor_user) = User::find_by_uuid(&emer.grantor_uuid, &conn).await else {
|
||||
error!("Grantor user '{}' not found for emergency access '{}'", emer.grantor_uuid, emer.uuid);
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(grantee_uuid) = emer.grantee_uuid.as_ref() else {
|
||||
error!("Missing grantee for emergency access '{}'", emer.uuid);
|
||||
continue;
|
||||
};
|
||||
|
||||
// Get grantee user to send Accepted email
|
||||
let Some(grantee_user) = User::find_by_uuid(grantee_uuid, &conn).await else {
|
||||
error!("Grantee user '{grantee_uuid}' not found for emergency access '{}'", emer.uuid);
|
||||
continue;
|
||||
};
|
||||
|
||||
// Only update the access status
|
||||
// Updating the whole record could cause issues when the emergency_notification_reminder_job is also active
|
||||
emer.update_access_status_and_save(EmergencyAccessStatus::RecoveryApproved as i32, &now, &conn)
|
||||
.await
|
||||
.expect("Unable to update emergency access status");
|
||||
let status_initiated = EmergencyAccessStatus::RecoveryInitiated as i32;
|
||||
let status_approved = EmergencyAccessStatus::RecoveryApproved as i32;
|
||||
if let Err(e) = emer.update_access_status_and_save(status_initiated, status_approved, &now, &conn).await {
|
||||
error!("Unable to update emergency access status for '{}': {e}", emer.uuid);
|
||||
continue;
|
||||
}
|
||||
|
||||
if CONFIG.mail_enabled() {
|
||||
// get grantor user to send Accepted email
|
||||
let grantor_user =
|
||||
User::find_by_uuid(&emer.grantor_uuid, &conn).await.expect("Grantor user not found");
|
||||
|
||||
// get grantee user to send Accepted email
|
||||
let grantee_user =
|
||||
User::find_by_uuid(&emer.grantee_uuid.clone().expect("Grantee user invalid"), &conn)
|
||||
.await
|
||||
.expect("Grantee user not found");
|
||||
|
||||
mail::send_emergency_access_recovery_timed_out(
|
||||
if let Err(e) = mail::send_emergency_access_recovery_timed_out(
|
||||
&grantor_user.email,
|
||||
&grantee_user.name,
|
||||
emer.get_type_as_str(),
|
||||
)
|
||||
.await
|
||||
.expect("Error on sending email");
|
||||
{
|
||||
error!("Failed to send emergency access recovery time-out email for '{}': {e}", emer.uuid);
|
||||
}
|
||||
|
||||
mail::send_emergency_access_recovery_approved(&grantee_user.email, &grantor_user.name)
|
||||
.await
|
||||
.expect("Error on sending email");
|
||||
if let Err(e) = mail::send_emergency_access_recovery_approved(&grantee_user.email, &grantor_user.name).await
|
||||
{
|
||||
error!("Failed to send emergency access recovery approved email for '{}': {e}", emer.uuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error!("Failed to get DB connection while searching emergency request timed out");
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn emergency_notification_reminder_job(pool: DbPool) {
|
||||
@@ -802,55 +844,85 @@ pub async fn emergency_notification_reminder_job(pool: DbPool) {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Ok(conn) = pool.get().await {
|
||||
let Ok(conn) = pool.get().await else {
|
||||
error!("Failed to get DB connection for emergency reminders!");
|
||||
return;
|
||||
};
|
||||
|
||||
let emergency_access_list = EmergencyAccess::find_all_recoveries_initiated(&conn).await;
|
||||
|
||||
if emergency_access_list.is_empty() {
|
||||
debug!("No emergency request reminder notification to send");
|
||||
return;
|
||||
}
|
||||
|
||||
let now = Utc::now().naive_utc();
|
||||
for mut emer in emergency_access_list {
|
||||
// The find_all_recoveries_initiated already checks if the recovery_initiated_at is not null (None)
|
||||
// Calculate the day before the recovery will become active
|
||||
let final_recovery_reminder_at =
|
||||
emer.recovery_initiated_at.unwrap() + TimeDelta::try_days(i64::from(emer.wait_time_days - 1)).unwrap();
|
||||
// Calculate if a day has passed since the previous notification, else no notification has been sent before
|
||||
let next_recovery_reminder_at = if let Some(last_notification_at) = emer.last_notification_at {
|
||||
last_notification_at + TimeDelta::try_days(1).unwrap()
|
||||
let Ok(deadline_at) = emer.deadline_at() else {
|
||||
error!("Invalid deadline for emergency access '{}'", emer.uuid);
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(final_reminder_at) = deadline_at.checked_sub_days(chrono::Days::new(1)) else {
|
||||
error!("Cannot calculate reminder date for emergency access '{}'", emer.uuid);
|
||||
continue;
|
||||
};
|
||||
|
||||
// Only send reminders during the last 24 hours, else continue
|
||||
if now < final_reminder_at || now >= deadline_at {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if we did not sent a recovery mail in the past 24h already, else skip it.
|
||||
// In case something goes wrong with the date calculation or `last_notification_at` is None we use now.
|
||||
// This is to prevent possible missed reminders, this might cause an extra reminder,
|
||||
// but that is better then no reminder at all.
|
||||
let next_reminder_at = if let Some(last_notification_at) = emer.last_notification_at {
|
||||
last_notification_at.checked_add_days(chrono::Days::new(1)).unwrap_or(now)
|
||||
} else {
|
||||
now
|
||||
};
|
||||
if final_recovery_reminder_at.le(&now) && next_recovery_reminder_at.le(&now) {
|
||||
|
||||
// Only send a reminder if the `next_reminder_at` is less then or equal to `now`
|
||||
// Else we can skip the current emergency access this run.
|
||||
if next_reminder_at.gt(&now) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get grantor user to send Reminder email
|
||||
let Some(grantor_user) = User::find_by_uuid(&emer.grantor_uuid, &conn).await else {
|
||||
error!("Grantor user '{}' not found for emergency access '{}'", emer.grantor_uuid, emer.uuid);
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(grantee_uuid) = emer.grantee_uuid.as_ref() else {
|
||||
error!("Missing grantee for emergency access '{}'", emer.uuid);
|
||||
continue;
|
||||
};
|
||||
|
||||
// Get grantee user to send Reminder email
|
||||
let Some(grantee_user) = User::find_by_uuid(grantee_uuid, &conn).await else {
|
||||
error!("Grantee user '{grantee_uuid}' not found for emergency access '{}'", emer.uuid);
|
||||
continue;
|
||||
};
|
||||
|
||||
// Only update the last notification date
|
||||
// Updating the whole record could cause issues when the emergency_request_timeout_job is also active
|
||||
emer.update_last_notification_date_and_save(&now, &conn)
|
||||
.await
|
||||
.expect("Unable to update emergency access notification date");
|
||||
if let Err(e) = emer.update_last_notification_date_and_save(&now, &conn).await {
|
||||
error!("Unable to update emergency access reminder date for '{}': {e}", emer.uuid);
|
||||
continue;
|
||||
}
|
||||
|
||||
if CONFIG.mail_enabled() {
|
||||
// get grantor user to send Accepted email
|
||||
let grantor_user =
|
||||
User::find_by_uuid(&emer.grantor_uuid, &conn).await.expect("Grantor user not found");
|
||||
|
||||
// get grantee user to send Accepted email
|
||||
let grantee_user =
|
||||
User::find_by_uuid(&emer.grantee_uuid.clone().expect("Grantee user invalid"), &conn)
|
||||
.await
|
||||
.expect("Grantee user not found");
|
||||
|
||||
mail::send_emergency_access_recovery_reminder(
|
||||
if CONFIG.mail_enabled()
|
||||
&& let Err(e) = mail::send_emergency_access_recovery_reminder(
|
||||
&grantor_user.email,
|
||||
&grantee_user.name,
|
||||
emer.get_type_as_str(),
|
||||
"1", // This notification is only triggered one day before the activation
|
||||
)
|
||||
.await
|
||||
.expect("Error on sending email");
|
||||
{
|
||||
error!("Failed to send emergency access reminder email for '{}': {e}", emer.uuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error!("Failed to get DB connection while searching emergency notification reminder");
|
||||
}
|
||||
}
|
||||
@@ -347,14 +347,17 @@ async fn log_event_impl(
|
||||
|
||||
pub async fn event_cleanup_job(pool: DbPool) {
|
||||
debug!("Start events cleanup job");
|
||||
if CONFIG.events_days_retain().is_none() {
|
||||
debug!("events_days_retain is not configured, abort");
|
||||
if CONFIG.events_days_retain().is_none_or(|d| d == 0) {
|
||||
debug!("events_days_retain is not configured or set to 0, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
if let Ok(conn) = pool.get().await {
|
||||
Event::clean_events(&conn).await.ok();
|
||||
} else {
|
||||
let Ok(conn) = pool.get().await else {
|
||||
error!("Failed to get DB connection while trying to cleanup the events table");
|
||||
return;
|
||||
};
|
||||
|
||||
if let Err(e) = Event::clean_events(&conn).await {
|
||||
error!("Failed to cleanup the events table: {e}");
|
||||
}
|
||||
}
|
||||
@@ -294,7 +294,7 @@ pub async fn validate_email_code_str(
|
||||
|
||||
if !crypto::ct_eq(issued_token, token) {
|
||||
email_data.add_attempt();
|
||||
if email_data.attempts >= CONFIG.email_attempts_limit() {
|
||||
if email_data.attempts >= u64::from(CONFIG.email_attempts_limit()) {
|
||||
email_data.reset_token();
|
||||
}
|
||||
twofactor.data = email_data.to_json();
|
||||
@@ -313,7 +313,7 @@ pub async fn validate_email_code_str(
|
||||
twofactor.save(conn).await?;
|
||||
|
||||
let dt = DateTime::from_timestamp(email_data.token_sent, 0).expect("Email token timestamp invalid.").naive_utc();
|
||||
let max_time = CONFIG.email_expiration_time().cast_signed();
|
||||
let max_time = i64::from(CONFIG.email_expiration_time());
|
||||
if dt + TimeDelta::try_seconds(max_time).unwrap() < Utc::now().naive_utc() {
|
||||
err!(
|
||||
"Token has expired",
|
||||
|
||||
@@ -196,7 +196,7 @@ pub async fn enforce_2fa_policy_for_org(
|
||||
pub async fn send_incomplete_2fa_notifications(pool: DbPool) {
|
||||
debug!("Sending notifications for incomplete 2FA logins");
|
||||
|
||||
if CONFIG.incomplete_2fa_time_limit() <= 0 || !CONFIG.mail_enabled() {
|
||||
if CONFIG.incomplete_2fa_time_limit() == 0 || !CONFIG.mail_enabled() {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -206,11 +206,21 @@ pub async fn send_incomplete_2fa_notifications(pool: DbPool) {
|
||||
};
|
||||
|
||||
let now = Utc::now().naive_utc();
|
||||
let time_limit = TimeDelta::try_minutes(CONFIG.incomplete_2fa_time_limit()).unwrap();
|
||||
let time_before = now - time_limit;
|
||||
let Some(time_limit) = TimeDelta::try_minutes(i64::from(CONFIG.incomplete_2fa_time_limit())) else {
|
||||
error!("Failed to calculate the time limit for sending incomplete 2fa attempts");
|
||||
return;
|
||||
};
|
||||
let Some(time_before) = now.checked_sub_signed(time_limit) else {
|
||||
error!("Failed to calculate the timestamp for sending incomplete 2fa attempts");
|
||||
return;
|
||||
};
|
||||
|
||||
let incomplete_logins = TwoFactorIncomplete::find_logins_before(&time_before, &conn).await;
|
||||
for login in incomplete_logins {
|
||||
let user = User::find_by_uuid(&login.user_uuid, &conn).await.expect("User not found");
|
||||
let Some(user) = User::find_by_uuid(&login.user_uuid, &conn).await else {
|
||||
error!("User '{}', not found!", login.user_uuid);
|
||||
continue;
|
||||
};
|
||||
info!(
|
||||
"User {} did not complete a 2FA login within the configured time limit. IP: {}",
|
||||
user.email, login.ip_address
|
||||
|
||||
@@ -128,23 +128,24 @@ pub async fn validate_protected_action_otp(
|
||||
.await
|
||||
.map_res("Protected action token not found, try sending the code again or restart the process")?;
|
||||
let mut pa_data = ProtectedActionData::from_json(&pa.data)?;
|
||||
pa_data.add_attempt();
|
||||
pa.data = pa_data.to_json();
|
||||
|
||||
// Fail after x attempts if the token has been used too many times.
|
||||
// Don't delete it, as we use it to keep track of attempts.
|
||||
if pa_data.attempts >= CONFIG.email_attempts_limit() {
|
||||
if pa_data.attempts >= u64::from(CONFIG.email_attempts_limit()) {
|
||||
err!("Token has expired")
|
||||
}
|
||||
|
||||
// Check if the token has expired (Using the email 2fa expiration time)
|
||||
let max_time = CONFIG.email_expiration_time().cast_signed();
|
||||
let max_time = i64::from(CONFIG.email_expiration_time());
|
||||
if pa_data.time_since_sent().num_seconds() > max_time {
|
||||
pa.delete(conn).await?;
|
||||
err!("Token has expired")
|
||||
}
|
||||
|
||||
if !crypto::ct_eq(&pa_data.token, otp) {
|
||||
pa_data.add_attempt();
|
||||
pa.data = pa_data.to_json();
|
||||
|
||||
pa.save(conn).await?;
|
||||
err!("Token is invalid")
|
||||
}
|
||||
|
||||
+3
-3
@@ -62,7 +62,7 @@ static CLIENT: LazyLock<Client> = LazyLock::new(|| {
|
||||
// Generate the cookie store
|
||||
let cookie_store = Arc::new(Jar::default());
|
||||
|
||||
let icon_download_timeout = Duration::from_secs(CONFIG.icon_download_timeout());
|
||||
let icon_download_timeout = Duration::from_secs(u64::from(CONFIG.icon_download_timeout()));
|
||||
let pool_idle_timeout = Duration::from_secs(10);
|
||||
// Reuse the client between requests
|
||||
get_reqwest_client_builder(true)
|
||||
@@ -193,14 +193,14 @@ async fn get_cached_icon(path: &str) -> Option<Vec<u8>> {
|
||||
None
|
||||
}
|
||||
|
||||
async fn file_is_expired(path: &str, ttl: u64) -> Result<bool, Error> {
|
||||
async fn file_is_expired(path: &str, ttl: u32) -> Result<bool, Error> {
|
||||
let operator = CONFIG.opendal_operator_for_path_type(&PathType::IconCache)?;
|
||||
let meta = operator.stat(path).await?;
|
||||
let modified =
|
||||
meta.last_modified().ok_or_else(|| std::io::Error::other(format!("No last modified time for `{path}`")))?;
|
||||
let age = SystemTime::now().duration_since(modified.into())?;
|
||||
|
||||
Ok(ttl > 0 && ttl <= age.as_secs())
|
||||
Ok(ttl > 0 && u64::from(ttl) <= age.as_secs())
|
||||
}
|
||||
|
||||
async fn icon_is_negcached(path: &str) -> bool {
|
||||
|
||||
+2
-2
@@ -447,9 +447,9 @@ async fn password_login(data: ConnectData, user_id: &mut Option<UserId>, conn: &
|
||||
if user.verified_at.is_none() && CONFIG.mail_enabled() && CONFIG.signups_verify() {
|
||||
if user.last_verifying_at.is_none()
|
||||
|| now.signed_duration_since(user.last_verifying_at.unwrap()).num_seconds()
|
||||
> CONFIG.signups_verify_resend_time().cast_signed()
|
||||
> i64::from(CONFIG.signups_verify_resend_time())
|
||||
{
|
||||
let resend_limit = CONFIG.signups_verify_resend_limit().cast_signed();
|
||||
let resend_limit = i32::from(CONFIG.signups_verify_resend_limit());
|
||||
if resend_limit == 0 || user.login_verify_count < resend_limit {
|
||||
// We want to send another email verification if we require signups to verify
|
||||
// their email address, and we haven't sent them a reminder in a while...
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ pub use crate::api::{
|
||||
},
|
||||
web::catchers as web_catchers,
|
||||
web::routes as web_routes,
|
||||
web::{invalidate_css_cache, static_files},
|
||||
web::{STATIC_FILES, invalidate_css_cache},
|
||||
};
|
||||
use crate::{
|
||||
CONFIG,
|
||||
|
||||
@@ -212,7 +212,7 @@ fn websockets_hub<'r>(
|
||||
if last_received.elapsed() > WS_CLIENT_TIMEOUT {
|
||||
break;
|
||||
}
|
||||
yield Message::Ping(create_ping());
|
||||
yield Message::Ping(PING.to_vec());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -295,7 +295,7 @@ fn anonymous_websockets_hub<'r>(ws: WebSocket, token: String, ip: ClientIp) -> R
|
||||
if last_received.elapsed() > WS_CLIENT_TIMEOUT {
|
||||
break;
|
||||
}
|
||||
yield Message::Ping(create_ping());
|
||||
yield Message::Ping(PING.to_vec());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -359,6 +359,7 @@ fn convert_option<T: Into<Value>>(option: Option<T>) -> Value {
|
||||
|
||||
const RECORD_SEPARATOR: u8 = 0x1e;
|
||||
const INITIAL_RESPONSE: [u8; 3] = [0x7b, 0x7d, RECORD_SEPARATOR]; // {, }, <RS>
|
||||
const PING: [u8; 3] = [0x02, 0x91, 0x06];
|
||||
|
||||
#[derive(Deserialize, Copy, Clone, Eq, PartialEq)]
|
||||
struct InitialMessage<'a> {
|
||||
@@ -691,10 +692,6 @@ fn create_anonymous_update(payload: Vec<(Value, Value)>, ut: UpdateType, user_id
|
||||
serialize(&value)
|
||||
}
|
||||
|
||||
fn create_ping() -> Vec<u8> {
|
||||
serialize(&Value::Array(vec![6.into()]))
|
||||
}
|
||||
|
||||
// https://github.com/bitwarden/server/blob/375af7c43b10d9da03525d41452f95de3f921541/src/Core/Enums/PushType.cs
|
||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||
pub enum UpdateType {
|
||||
|
||||
+62
-32
@@ -1,13 +1,14 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
path::{Path, PathBuf},
|
||||
sync::{Arc, RwLock},
|
||||
sync::{Arc, LazyLock, RwLock},
|
||||
};
|
||||
|
||||
use rocket::{
|
||||
Catcher, Route,
|
||||
fs::NamedFile,
|
||||
http::ContentType,
|
||||
response::{Redirect, content::RawCss as Css, content::RawHtml as Html},
|
||||
response::{Redirect, content::RawHtml as Html},
|
||||
serde::json::Json,
|
||||
};
|
||||
use serde_json::Value;
|
||||
@@ -79,12 +80,12 @@ pub fn invalidate_css_cache() {
|
||||
}
|
||||
|
||||
#[get("/css/vaultwarden.css")]
|
||||
fn vaultwarden_css() -> EtagCached<Css<String>> {
|
||||
fn vaultwarden_css() -> EtagCached<String> {
|
||||
// If reload_templates is false, and we already have the CSS Cached, return this
|
||||
if !CONFIG.reload_templates()
|
||||
&& let Some(cached) = CSS_CACHE.read().unwrap().as_ref()
|
||||
{
|
||||
return EtagCached::new(Css(cached.css.clone()), &cached.etag);
|
||||
return EtagCached::new(cached.css.clone(), ContentType::CSS, &cached.etag);
|
||||
}
|
||||
|
||||
// Else, there is either no cache, or reload_templates is true and we need to rebuild the CSS
|
||||
@@ -147,7 +148,7 @@ fn vaultwarden_css() -> EtagCached<Css<String>> {
|
||||
}
|
||||
|
||||
// Etag Caching will let the browser send us an etag to verify and send new content if needed
|
||||
EtagCached::new(Css(cached.css.clone()), &cached.etag)
|
||||
EtagCached::new(cached.css.clone(), ContentType::CSS, &cached.etag)
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
@@ -256,7 +257,7 @@ fn alive_head(_conn: DbConn) -> EmptyResult {
|
||||
// This endpoint/function is used during development and development only.
|
||||
// It allows to easily develop the admin interface by always loading the files from disk instead from a slice of bytes
|
||||
// This will only be active during a debug build and only when `RELOAD_TEMPLATES` is set to `true`
|
||||
// NOTE: Do not forget to add any new files added to the `static_files` function below!
|
||||
// NOTE: Do not forget to add any new files added to the `STATIC_FILES` HashMap below!
|
||||
#[cfg(debug_assertions)]
|
||||
#[get("/vw_static/<filename>", rank = 1)]
|
||||
pub async fn static_files_dev(filename: PathBuf) -> Option<NamedFile> {
|
||||
@@ -276,32 +277,61 @@ pub async fn static_files_dev(filename: PathBuf) -> Option<NamedFile> {
|
||||
None
|
||||
}
|
||||
|
||||
pub struct StaticFile {
|
||||
pub content_type: ContentType,
|
||||
pub bytes: &'static [u8],
|
||||
pub etag: String,
|
||||
}
|
||||
|
||||
pub static STATIC_FILES: LazyLock<HashMap<&'static str, StaticFile>> = LazyLock::new(|| {
|
||||
[
|
||||
("404.png", ContentType::PNG, &include_bytes!("../static/images/404.png")[..]),
|
||||
("mail-github.png", ContentType::PNG, &include_bytes!("../static/images/mail-github.png")[..]),
|
||||
("logo-gray.png", ContentType::PNG, &include_bytes!("../static/images/logo-gray.png")[..]),
|
||||
("error-x.svg", ContentType::SVG, &include_bytes!("../static/images/error-x.svg")[..]),
|
||||
("hibp.png", ContentType::PNG, &include_bytes!("../static/images/hibp.png")[..]),
|
||||
("vaultwarden-icon.png", ContentType::PNG, &include_bytes!("../static/images/vaultwarden-icon.png")[..]),
|
||||
("vaultwarden-favicon.png", ContentType::PNG, &include_bytes!("../static/images/vaultwarden-favicon.png")[..]),
|
||||
("404.css", ContentType::CSS, &include_bytes!("../static/scripts/404.css")[..]),
|
||||
("admin.css", ContentType::CSS, &include_bytes!("../static/scripts/admin.css")[..]),
|
||||
("admin.js", ContentType::JavaScript, &include_bytes!("../static/scripts/admin.js")[..]),
|
||||
("admin_settings.js", ContentType::JavaScript, &include_bytes!("../static/scripts/admin_settings.js")[..]),
|
||||
("admin_users.js", ContentType::JavaScript, &include_bytes!("../static/scripts/admin_users.js")[..]),
|
||||
(
|
||||
"admin_organizations.js",
|
||||
ContentType::JavaScript,
|
||||
&include_bytes!("../static/scripts/admin_organizations.js")[..],
|
||||
),
|
||||
(
|
||||
"admin_diagnostics.js",
|
||||
ContentType::JavaScript,
|
||||
&include_bytes!("../static/scripts/admin_diagnostics.js")[..],
|
||||
),
|
||||
("bootstrap.css", ContentType::CSS, &include_bytes!("../static/scripts/bootstrap.css")[..]),
|
||||
("bootstrap.bundle.js", ContentType::JavaScript, &include_bytes!("../static/scripts/bootstrap.bundle.js")[..]),
|
||||
("jdenticon-3.3.0.js", ContentType::JavaScript, &include_bytes!("../static/scripts/jdenticon-3.3.0.js")[..]),
|
||||
("datatables.js", ContentType::JavaScript, &include_bytes!("../static/scripts/datatables.js")[..]),
|
||||
("datatables.css", ContentType::CSS, &include_bytes!("../static/scripts/datatables.css")[..]),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(name, content_type, bytes)| {
|
||||
(
|
||||
name,
|
||||
StaticFile {
|
||||
content_type,
|
||||
bytes,
|
||||
etag: sha256_hex(bytes),
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
|
||||
#[get("/vw_static/<filename>", rank = 2)]
|
||||
pub fn static_files(filename: &str) -> Result<(ContentType, &'static [u8]), Error> {
|
||||
match filename {
|
||||
"404.png" => Ok((ContentType::PNG, include_bytes!("../static/images/404.png"))),
|
||||
"mail-github.png" => Ok((ContentType::PNG, include_bytes!("../static/images/mail-github.png"))),
|
||||
"logo-gray.png" => Ok((ContentType::PNG, include_bytes!("../static/images/logo-gray.png"))),
|
||||
"error-x.svg" => Ok((ContentType::SVG, include_bytes!("../static/images/error-x.svg"))),
|
||||
"hibp.png" => Ok((ContentType::PNG, include_bytes!("../static/images/hibp.png"))),
|
||||
"vaultwarden-icon.png" => Ok((ContentType::PNG, include_bytes!("../static/images/vaultwarden-icon.png"))),
|
||||
"vaultwarden-favicon.png" => Ok((ContentType::PNG, include_bytes!("../static/images/vaultwarden-favicon.png"))),
|
||||
"404.css" => Ok((ContentType::CSS, include_bytes!("../static/scripts/404.css"))),
|
||||
"admin.css" => Ok((ContentType::CSS, include_bytes!("../static/scripts/admin.css"))),
|
||||
"admin.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/admin.js"))),
|
||||
"admin_settings.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/admin_settings.js"))),
|
||||
"admin_users.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/admin_users.js"))),
|
||||
"admin_organizations.js" => {
|
||||
Ok((ContentType::JavaScript, include_bytes!("../static/scripts/admin_organizations.js")))
|
||||
}
|
||||
"admin_diagnostics.js" => {
|
||||
Ok((ContentType::JavaScript, include_bytes!("../static/scripts/admin_diagnostics.js")))
|
||||
}
|
||||
"bootstrap.css" => Ok((ContentType::CSS, include_bytes!("../static/scripts/bootstrap.css"))),
|
||||
"bootstrap.bundle.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/bootstrap.bundle.js"))),
|
||||
"jdenticon-3.3.0.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/jdenticon-3.3.0.js"))),
|
||||
"datatables.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/datatables.js"))),
|
||||
"datatables.css" => Ok((ContentType::CSS, include_bytes!("../static/scripts/datatables.css"))),
|
||||
_ => err!(format!("Static file not found: {filename}")),
|
||||
pub fn static_files(filename: &str) -> Result<EtagCached<&'static [u8]>, Error> {
|
||||
if let Some(file) = STATIC_FILES.get(filename) {
|
||||
Ok(EtagCached::new(file.bytes, file.content_type.clone(), &file.etag))
|
||||
} else {
|
||||
err!(format!("Static file not found: {filename}"))
|
||||
}
|
||||
}
|
||||
+42
-24
@@ -314,6 +314,17 @@ impl LoginJwtClaims {
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_invite_expire_hours(now: &DateTime<Utc>) -> Result<DateTime<Utc>, Error> {
|
||||
let expire_hours = i64::from(CONFIG.invitation_expiration_hours());
|
||||
let Some(hours_delta) = TimeDelta::try_hours(expire_hours) else {
|
||||
err!("Unable to calculate time delta for invite expire hours. Check `INVITATION_EXPIRATION_HOURS`.");
|
||||
};
|
||||
let Some(exp) = now.checked_add_signed(hours_delta) else {
|
||||
err!("Unable to calculate invitation expire date. Check `INVITATION_EXPIRATION_HOURS`.");
|
||||
};
|
||||
Ok(exp)
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct InviteJwtClaims {
|
||||
// Not before
|
||||
@@ -337,19 +348,19 @@ pub fn generate_invite_claims(
|
||||
org_id: OrganizationId,
|
||||
member_id: MembershipId,
|
||||
invited_by_email: Option<String>,
|
||||
) -> InviteJwtClaims {
|
||||
) -> Result<InviteJwtClaims, Error> {
|
||||
let time_now = Utc::now();
|
||||
let expire_hours = i64::from(CONFIG.invitation_expiration_hours());
|
||||
InviteJwtClaims {
|
||||
let exp = calculate_invite_expire_hours(&time_now)?;
|
||||
Ok(InviteJwtClaims {
|
||||
nbf: time_now.timestamp(),
|
||||
exp: (time_now + TimeDelta::try_hours(expire_hours).unwrap()).timestamp(),
|
||||
exp: exp.timestamp(),
|
||||
iss: JWT_INVITE_ISSUER.to_string(),
|
||||
sub: user_id,
|
||||
email,
|
||||
org_id,
|
||||
member_id,
|
||||
invited_by_email,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -375,19 +386,19 @@ pub fn generate_emergency_access_invite_claims(
|
||||
emer_id: EmergencyAccessId,
|
||||
grantor_name: String,
|
||||
grantor_email: String,
|
||||
) -> EmergencyAccessInviteJwtClaims {
|
||||
) -> Result<EmergencyAccessInviteJwtClaims, Error> {
|
||||
let time_now = Utc::now();
|
||||
let expire_hours = i64::from(CONFIG.invitation_expiration_hours());
|
||||
EmergencyAccessInviteJwtClaims {
|
||||
let exp = calculate_invite_expire_hours(&time_now)?;
|
||||
Ok(EmergencyAccessInviteJwtClaims {
|
||||
nbf: time_now.timestamp(),
|
||||
exp: (time_now + TimeDelta::try_hours(expire_hours).unwrap()).timestamp(),
|
||||
exp: exp.timestamp(),
|
||||
iss: JWT_EMERGENCY_ACCESS_INVITE_ISSUER.to_string(),
|
||||
sub: user_id,
|
||||
email,
|
||||
emer_id,
|
||||
grantor_name,
|
||||
grantor_email,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -521,36 +532,43 @@ impl BasicJwtClaims {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_delete_claims(uuid: String) -> BasicJwtClaims {
|
||||
pub fn generate_delete_claims(uuid: String) -> Result<BasicJwtClaims, Error> {
|
||||
let time_now = Utc::now();
|
||||
let expire_hours = i64::from(CONFIG.invitation_expiration_hours());
|
||||
BasicJwtClaims {
|
||||
let exp = calculate_invite_expire_hours(&time_now)?;
|
||||
Ok(BasicJwtClaims {
|
||||
nbf: time_now.timestamp(),
|
||||
exp: (time_now + TimeDelta::try_hours(expire_hours).unwrap()).timestamp(),
|
||||
exp: exp.timestamp(),
|
||||
iss: JWT_DELETE_ISSUER.to_string(),
|
||||
sub: uuid,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn generate_verify_email_claims(user_id: &UserId) -> BasicJwtClaims {
|
||||
pub fn generate_verify_email_claims(user_id: &UserId) -> Result<BasicJwtClaims, Error> {
|
||||
let time_now = Utc::now();
|
||||
let expire_hours = i64::from(CONFIG.invitation_expiration_hours());
|
||||
BasicJwtClaims {
|
||||
let exp = calculate_invite_expire_hours(&time_now)?;
|
||||
Ok(BasicJwtClaims {
|
||||
nbf: time_now.timestamp(),
|
||||
exp: (time_now + TimeDelta::try_hours(expire_hours).unwrap()).timestamp(),
|
||||
exp: exp.timestamp(),
|
||||
iss: JWT_VERIFYEMAIL_ISSUER.to_string(),
|
||||
sub: user_id.to_string(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn generate_admin_claims() -> BasicJwtClaims {
|
||||
pub fn generate_admin_claims() -> Result<BasicJwtClaims, Error> {
|
||||
let time_now = Utc::now();
|
||||
BasicJwtClaims {
|
||||
let lifetime_minutes = i64::from(CONFIG.admin_session_lifetime());
|
||||
let Some(minutes_delta) = TimeDelta::try_minutes(lifetime_minutes) else {
|
||||
err!("Unable to calculate time delta for admin session lifetime. Check `ADMIN_SESSION_LIFETIME`.");
|
||||
};
|
||||
let Some(exp) = time_now.checked_add_signed(minutes_delta) else {
|
||||
err!("Unable to calculate admin session lifetime. Check `ADMIN_SESSION_LIFETIME`.");
|
||||
};
|
||||
Ok(BasicJwtClaims {
|
||||
nbf: time_now.timestamp(),
|
||||
exp: (time_now + TimeDelta::try_minutes(CONFIG.admin_session_lifetime()).unwrap()).timestamp(),
|
||||
exp: exp.timestamp(),
|
||||
iss: JWT_ADMIN_ISSUER.to_string(),
|
||||
sub: "admin_panel".to_owned(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn generate_send_claims(send_id: &SendId, file_id: &SendFileId) -> BasicJwtClaims {
|
||||
|
||||
+107
-33
@@ -111,6 +111,7 @@ macro_rules! make_config {
|
||||
_usr: ConfigBuilder,
|
||||
|
||||
_overrides: Vec<&'static str>,
|
||||
_config_file_status: ConfigFileStatus,
|
||||
}
|
||||
|
||||
// Custom Deserialize for ConfigBuilder, mainly based upon https://serde.rs/deserialize-struct.html
|
||||
@@ -259,11 +260,18 @@ macro_rules! make_config {
|
||||
builder
|
||||
}
|
||||
|
||||
async fn from_file() -> Result<Self, Error> {
|
||||
async fn from_file() -> Result<Option<Self>, Error> {
|
||||
let operator = storage::operator_for_path(&CONFIG_FILE_PARENT_DIR)?;
|
||||
let config_bytes = operator.read(&CONFIG_FILENAME).await?;
|
||||
let config_bytes = match operator.read(&CONFIG_FILENAME).await {
|
||||
Ok(bytes) => bytes,
|
||||
// No config file is fine, should not return any error
|
||||
Err(e) if e.kind() == opendal::ErrorKind::NotFound => return Ok(None),
|
||||
// Any other error might be an issue and should be passed back to the caller
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
let builder = serde_json::from_slice(&config_bytes.to_vec())?;
|
||||
println!("[INFO] Using saved config from `{}` for configuration.\n", *CONFIG_FILE);
|
||||
serde_json::from_slice(&config_bytes.to_vec()).map_err(Into::into)
|
||||
Ok(Some(builder))
|
||||
}
|
||||
|
||||
fn clear_non_editable(&mut self) {
|
||||
@@ -440,6 +448,8 @@ macro_rules! make_config {
|
||||
"domain_origin",
|
||||
"domain_path",
|
||||
"domain",
|
||||
"duo_ikey",
|
||||
"duo_host",
|
||||
"helo_name",
|
||||
"org_creation_users",
|
||||
"signups_domains_whitelist",
|
||||
@@ -451,6 +461,8 @@ macro_rules! make_config {
|
||||
"sso_authority",
|
||||
"sso_callback_path",
|
||||
"sso_client_id",
|
||||
"yubico_client_id",
|
||||
"yubico_server",
|
||||
];
|
||||
|
||||
let cfg = {
|
||||
@@ -483,6 +495,10 @@ macro_rules! make_config {
|
||||
};
|
||||
overrides
|
||||
}
|
||||
|
||||
pub fn get_config_file_status(&self) -> ConfigFileStatus {
|
||||
self.inner.read().unwrap()._config_file_status
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -539,7 +555,7 @@ make_config! {
|
||||
jobs {
|
||||
/// Job scheduler poll interval |> How often the job scheduler thread checks for jobs to run.
|
||||
/// Set to 0 to globally disable scheduled jobs.
|
||||
job_poll_interval_ms: u64, false, def, 30_000;
|
||||
job_poll_interval_ms: u32, false, def, 30_000;
|
||||
/// Send purge schedule |> Cron schedule of the job that checks for Sends past their deletion date.
|
||||
/// Defaults to hourly. Set blank to disable this job.
|
||||
send_purge_schedule: String, false, def, "0 5 * * * *".to_owned();
|
||||
@@ -598,16 +614,16 @@ make_config! {
|
||||
user_send_limit: i64, true, option;
|
||||
|
||||
/// Trash auto-delete days |> Number of days to wait before auto-deleting a trashed item.
|
||||
/// If unset, trashed items are not auto-deleted. This setting applies globally, so make
|
||||
/// If unset or 0, trashed items are not auto-deleted. This setting applies globally, so make
|
||||
/// sure to inform all users of any changes to this setting.
|
||||
trash_auto_delete_days: i64, true, option;
|
||||
trash_auto_delete_days: u16, true, option;
|
||||
|
||||
/// Incomplete 2FA time limit |> Number of minutes to wait before a 2FA-enabled login is
|
||||
/// considered incomplete, resulting in an email notification. An incomplete 2FA login is one
|
||||
/// where the correct master password was provided but the required 2FA step was not completed,
|
||||
/// which potentially indicates a master password compromise. Set to 0 to disable this check.
|
||||
/// This setting applies globally to all users.
|
||||
incomplete_2fa_time_limit: i64, true, def, 3;
|
||||
incomplete_2fa_time_limit: u16, true, def, 3;
|
||||
|
||||
/// Disable icon downloads |> Set to true to disable icon downloading in the internal icon service.
|
||||
/// This still serves existing icons from $ICON_CACHE_FOLDER, without generating any external
|
||||
@@ -620,9 +636,9 @@ make_config! {
|
||||
/// this will prevent logins from succeeding until the address has been verified
|
||||
signups_verify: bool, true, def, false;
|
||||
/// If signups require email verification, automatically re-send verification email if it hasn't been sent for a while (in seconds)
|
||||
signups_verify_resend_time: u64, true, def, 3_600;
|
||||
signups_verify_resend_time: u32, true, def, 3_600;
|
||||
/// If signups require email verification, limit how many emails are automatically sent when login is attempted (0 means no limit)
|
||||
signups_verify_resend_limit: u32, true, def, 6;
|
||||
signups_verify_resend_limit: u16, true, def, 6;
|
||||
/// Email domain whitelist |> Allow signups only from this list of comma-separated domains, even when signups are otherwise disabled
|
||||
signups_domains_whitelist: String, true, def, String::new();
|
||||
/// Enable event logging |> Enables event logging for organizations.
|
||||
@@ -634,7 +650,7 @@ make_config! {
|
||||
invitations_allowed: bool, true, def, true;
|
||||
/// Invitation token expiration time (in hours) |> The number of hours after which an organization invite token, emergency access invite token,
|
||||
/// email verification token and deletion request token will expire (must be at least 1)
|
||||
invitation_expiration_hours: u32, false, def, 120;
|
||||
invitation_expiration_hours: u16, false, def, 120;
|
||||
/// Enable emergency access |> Controls whether users can enable emergency access to their accounts. This setting applies globally to all users.
|
||||
emergency_access_allowed: bool, true, def, true;
|
||||
/// Allow email change |> Controls whether users can change their email. This setting applies globally to all users.
|
||||
@@ -655,8 +671,8 @@ make_config! {
|
||||
/// Invitation organization name |> Name shown in the invitation emails that don't come from a specific organization
|
||||
invitation_org_name: String, true, def, "Vaultwarden".to_owned();
|
||||
|
||||
/// Events days retain |> Number of days to retain events stored in the database. If unset, events are kept indefinitely.
|
||||
events_days_retain: i64, false, option;
|
||||
/// Events days retain |> Number of days to retain events stored in the database. If unset or 0, events are kept indefinitely.
|
||||
events_days_retain: u16, false, option;
|
||||
},
|
||||
|
||||
client {
|
||||
@@ -694,13 +710,13 @@ make_config! {
|
||||
/// Temporary redirects are useful while testing different icon services, but once a service
|
||||
/// has been decided on, consider using permanent redirects for cacheability. The legacy codes
|
||||
/// are currently better supported by the Bitwarden clients.
|
||||
icon_redirect_code: u32, true, def, 302;
|
||||
icon_redirect_code: u16, true, def, 302;
|
||||
/// Positive icon cache expiry |> Number of seconds to consider that an already cached icon is fresh. After this period, the icon will be refreshed
|
||||
icon_cache_ttl: u64, true, def, 2_592_000;
|
||||
icon_cache_ttl: u32, true, def, 2_592_000;
|
||||
/// Negative icon cache expiry |> Number of seconds before trying to download an icon that failed again.
|
||||
icon_cache_negttl: u64, true, def, 259_200;
|
||||
icon_cache_negttl: u32, true, def, 259_200;
|
||||
/// Icon download timeout |> Number of seconds when to stop attempting to download an icon.
|
||||
icon_download_timeout: u64, true, def, 10;
|
||||
icon_download_timeout: u16, true, def, 10;
|
||||
|
||||
/// [Deprecated] Icon blacklist Regex |> Use `http_request_block_regex` instead
|
||||
icon_blacklist_regex: String, false, option;
|
||||
@@ -751,11 +767,11 @@ make_config! {
|
||||
/// Max database connection retries |> Number of times to retry the database connection during startup, with 1 second between each retry, set to 0 to retry indefinitely
|
||||
db_connection_retries: u32, false, def, 15;
|
||||
|
||||
/// Timeout when acquiring database connection
|
||||
database_timeout: u64, false, def, 30;
|
||||
/// Timeout in seconds when acquiring database connection
|
||||
database_timeout: u32, false, def, 30;
|
||||
|
||||
/// Timeout in seconds before idle connections to the database are closed
|
||||
database_idle_timeout: u64, false, def, 600;
|
||||
database_idle_timeout: u32, false, def, 600;
|
||||
|
||||
/// Database connection max pool size
|
||||
database_max_conns: u32, false, def, 10;
|
||||
@@ -776,22 +792,22 @@ make_config! {
|
||||
allowed_connect_src: String, true, def, String::new();
|
||||
|
||||
/// Seconds between login requests |> Number of seconds, on average, between login and 2FA requests from the same IP address before rate limiting kicks in
|
||||
login_ratelimit_seconds: u64, false, def, 60;
|
||||
login_ratelimit_seconds: u16, false, def, 60;
|
||||
/// Max burst size for login requests |> Allow a burst of requests of up to this size, while maintaining the average indicated by `login_ratelimit_seconds`. Note that this applies to both the login and the 2FA, so it's recommended to allow a burst size of at least 2
|
||||
login_ratelimit_max_burst: u32, false, def, 10;
|
||||
login_ratelimit_max_burst: u16, false, def, 10;
|
||||
|
||||
/// Seconds between unauthenticated requests |> Number of seconds, on average, between requests from the same IP address to any of the rate limited unauthenticated endpoints
|
||||
unauthenticated_ratelimit_seconds: u64, false, def, 60;
|
||||
unauthenticated_ratelimit_seconds: u16, false, def, 60;
|
||||
/// Max burst size for unauthenticated requests |> Allow a burst of requests of up to this size, while maintaining the average indicated by `unauthenticated_ratelimit_seconds`. This is shared between several endpoints, so it needs to be more lenient than the login one
|
||||
unauthenticated_ratelimit_max_burst: u32, false, def, 50;
|
||||
unauthenticated_ratelimit_max_burst: u16, false, def, 50;
|
||||
|
||||
/// Seconds between admin login requests |> Number of seconds, on average, between admin requests from the same IP address before rate limiting kicks in
|
||||
admin_ratelimit_seconds: u64, false, def, 300;
|
||||
admin_ratelimit_seconds: u16, false, def, 300;
|
||||
/// Max burst size for admin login requests |> Allow a burst of requests of up to this size, while maintaining the average indicated by `admin_ratelimit_seconds`
|
||||
admin_ratelimit_max_burst: u32, false, def, 3;
|
||||
admin_ratelimit_max_burst: u16, false, def, 3;
|
||||
|
||||
/// Admin session lifetime |> Set the lifetime of admin sessions to this value (in minutes).
|
||||
admin_session_lifetime: i64, true, def, 20;
|
||||
admin_session_lifetime: u32, true, def, 20;
|
||||
|
||||
/// Enable groups (BETA!) (Know the risks!) |> Enables groups support for organizations (Currently contains known issues!).
|
||||
org_groups_enabled: bool, false, def, false;
|
||||
@@ -844,8 +860,8 @@ make_config! {
|
||||
sso_master_password_policy: String, true, option;
|
||||
/// Use SSO only for auth not the session lifecycle |> Use default Vaultwarden session lifecycle (Idle refresh token valid for 30days)
|
||||
sso_auth_only_not_session: bool, true, def, false;
|
||||
/// Client cache for discovery endpoint. |> Duration in seconds (0 or less to disable). More details: https://github.com/dani-garcia/vaultwarden/wiki/Enabling-SSO-support-using-OpenId-Connect#client-cache
|
||||
sso_client_cache_expiration: u64, true, def, 0;
|
||||
/// Client cache for discovery endpoint. |> Duration in seconds (0 to disable). More details: https://github.com/dani-garcia/vaultwarden/wiki/Enabling-SSO-support-using-OpenId-Connect#client-cache
|
||||
sso_client_cache_expiration: u32, true, def, 0;
|
||||
/// Log all tokens |> `LOG_LEVEL=debug` or `LOG_LEVEL=info,vaultwarden::sso=debug` is required
|
||||
sso_debug_tokens: bool, true, def, false;
|
||||
},
|
||||
@@ -903,7 +919,7 @@ make_config! {
|
||||
/// SMTP Auth mechanism |> Defaults for SSL is "Plain" and "Login" and nothing for Non-SSL connections. Possible values: ["Plain", "Login", "Xoauth2"]. Multiple options need to be separated by a comma ','.
|
||||
smtp_auth_mechanism: String, true, option;
|
||||
/// SMTP connection timeout |> Number of seconds when to stop trying to connect to the SMTP server
|
||||
smtp_timeout: u64, true, def, 15;
|
||||
smtp_timeout: u16, true, def, 15;
|
||||
/// Server name sent during HELO |> By default this value should be the machine's hostname, but might need to be changed in case it trips some anti-spam filters
|
||||
helo_name: String, true, option;
|
||||
/// Embed images as email attachments.
|
||||
@@ -925,9 +941,9 @@ make_config! {
|
||||
/// Email token size |> Number of digits in an email 2FA token (min: 6, max: 255). Note that the Bitwarden clients are hardcoded to mention 6 digit codes regardless of this setting.
|
||||
email_token_size: u8, true, def, 6;
|
||||
/// Token expiration time |> Maximum time in seconds a token is valid. The time the user has to open email client and copy token.
|
||||
email_expiration_time: u64, true, def, 600;
|
||||
email_expiration_time: u32, true, def, 600;
|
||||
/// Maximum attempts |> Maximum attempts before an email token is reset and a new email will need to be sent
|
||||
email_attempts_limit: u64, true, def, 3;
|
||||
email_attempts_limit: u8, true, def, 3;
|
||||
/// Setup email 2FA at signup |> Setup email 2FA provider on registration regardless of any organization policy
|
||||
email_2fa_enforce_on_verified_invite: bool, true, def, false;
|
||||
/// Auto-enable 2FA (Know the risks!) |> Automatically setup email 2FA as fallback provider when needed
|
||||
@@ -983,7 +999,15 @@ fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> {
|
||||
}
|
||||
|
||||
if cfg.database_min_conns > cfg.database_max_conns {
|
||||
err!("`DATABASE_MIN_CONNS` must be smaller than or equal to `DATABASE_MAX_CONNS`.");
|
||||
err!("`DATABASE_MIN_CONNS` must be less than or equal to `DATABASE_MAX_CONNS`.");
|
||||
}
|
||||
|
||||
if cfg.database_timeout < 1 {
|
||||
err!("`DATABASE_TIMEOUT` must be at least 1 second");
|
||||
}
|
||||
|
||||
if cfg.database_idle_timeout < 1 {
|
||||
err!("`DATABASE_IDLE_TIMEOUT` must be at least 1 second");
|
||||
}
|
||||
|
||||
if let Some(log_file) = &cfg.log_file
|
||||
@@ -1274,6 +1298,30 @@ fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> {
|
||||
err!("`PURGE_INCOMPLETE_SSO_AUTH` is not a valid cron expression")
|
||||
}
|
||||
|
||||
if cfg.login_ratelimit_seconds < 1 {
|
||||
err!("`LOGIN_RATELIMIT_SECONDS` must be at least 1 second");
|
||||
}
|
||||
|
||||
if cfg.login_ratelimit_max_burst < 1 {
|
||||
err!("`LOGIN_RATELIMIT_MAX_BURST` must be at least 1");
|
||||
}
|
||||
|
||||
if cfg.admin_ratelimit_seconds < 1 {
|
||||
err!("`ADMIN_RATELIMIT_SECONDS` must be at least 1 second");
|
||||
}
|
||||
|
||||
if cfg.admin_ratelimit_max_burst < 1 {
|
||||
err!("`ADMIN_RATELIMIT_MAX_BURST` must be at least 1");
|
||||
}
|
||||
|
||||
if cfg.unauthenticated_ratelimit_seconds < 1 {
|
||||
err!("`UNAUTHENTICATED_RATELIMIT_SECONDS` must be at least 1 second");
|
||||
}
|
||||
|
||||
if cfg.unauthenticated_ratelimit_max_burst < 1 {
|
||||
err!("`UNAUTHENTICATED_RATELIMIT_MAX_BURST` must be at least 1");
|
||||
}
|
||||
|
||||
if !cfg.disable_admin_token {
|
||||
match cfg.admin_token.as_ref() {
|
||||
Some(t) if t.starts_with("$argon2") => {
|
||||
@@ -1443,11 +1491,34 @@ pub const SUPPORTED_FEATURE_FLAGS: &[&str] = &[
|
||||
"pm-32009-new-item-types",
|
||||
];
|
||||
|
||||
#[derive(Clone, Copy, Serialize)]
|
||||
pub enum ConfigFileStatus {
|
||||
NotFound,
|
||||
Loaded,
|
||||
Error,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub async fn load() -> Result<Self, Error> {
|
||||
// Loading from env and file
|
||||
let env = ConfigBuilder::from_env();
|
||||
let usr = ConfigBuilder::from_file().await.unwrap_or_default();
|
||||
let (usr, config_file_status) = match ConfigBuilder::from_file().await {
|
||||
Ok(Some(usr)) => (usr, ConfigFileStatus::Loaded),
|
||||
Ok(None) => (ConfigBuilder::default(), ConfigFileStatus::NotFound),
|
||||
Err(e) => {
|
||||
println!(
|
||||
"[ERROR] Failed to load configuration from `{}`\n\
|
||||
#####################################################################################\n\
|
||||
# The file seems to exist but might contain invalid JSON or invalid values. #\n\
|
||||
# Check the syntax and make sure every value is valid and within its allowed range. #\n\
|
||||
# This file is not used until those errors are resolved. #\n\
|
||||
#####################################################################################\n\
|
||||
{e:?}\n",
|
||||
*CONFIG_FILE
|
||||
);
|
||||
(ConfigBuilder::default(), ConfigFileStatus::Error)
|
||||
}
|
||||
};
|
||||
|
||||
// Create merged config, config file overwrites env
|
||||
let mut overrides = Vec::new();
|
||||
@@ -1467,6 +1538,7 @@ impl Config {
|
||||
_env: env,
|
||||
_usr: usr,
|
||||
_overrides: overrides,
|
||||
_config_file_status: config_file_status,
|
||||
}),
|
||||
})
|
||||
}
|
||||
@@ -1500,6 +1572,7 @@ impl Config {
|
||||
writer.config = config;
|
||||
writer._usr = builder;
|
||||
writer._overrides = overrides;
|
||||
writer._config_file_status = ConfigFileStatus::Loaded;
|
||||
}
|
||||
|
||||
//Save to file
|
||||
@@ -1591,6 +1664,7 @@ impl Config {
|
||||
writer.config = config;
|
||||
writer._usr = usr;
|
||||
writer._overrides = Vec::new();
|
||||
writer._config_file_status = ConfigFileStatus::NotFound;
|
||||
}
|
||||
|
||||
// Invalidate CSS Cache because several config items might have impact on the rendered CSS
|
||||
|
||||
+3
-3
@@ -212,8 +212,8 @@ impl DbPool {
|
||||
let pool = Pool::builder()
|
||||
.max_size(max_conns)
|
||||
.min_idle(Some(CONFIG.database_min_conns()))
|
||||
.idle_timeout(Some(Duration::from_secs(CONFIG.database_idle_timeout())))
|
||||
.connection_timeout(Duration::from_secs(CONFIG.database_timeout()))
|
||||
.idle_timeout(Some(Duration::from_secs(u64::from(CONFIG.database_idle_timeout()))))
|
||||
.connection_timeout(Duration::from_secs(u64::from(CONFIG.database_timeout())))
|
||||
.connection_customizer(Box::new(DbConnOptions {
|
||||
init_stmts: conn_type.get_init_stmts(),
|
||||
}))
|
||||
@@ -233,7 +233,7 @@ impl DbPool {
|
||||
|
||||
// Get a connection from the pool
|
||||
pub async fn get(&self) -> Result<DbConn, Error> {
|
||||
let duration = Duration::from_secs(CONFIG.database_timeout());
|
||||
let duration = Duration::from_secs(u64::from(CONFIG.database_timeout()));
|
||||
let permit = match timeout(duration, Arc::clone(&self.semaphore).acquire_owned()).await {
|
||||
Ok(p) => p.expect("Semaphore should be open"),
|
||||
Err(_) => {
|
||||
|
||||
@@ -197,7 +197,7 @@ impl AuthRequest {
|
||||
pub async fn purge_expired_auth_requests(conn: &DbConn) {
|
||||
// delete auth requests older than 15 minutes which is functionally equivalent to upstream:
|
||||
// https://github.com/bitwarden/server/blob/f8ee2270409f7a13125cd414c450740af605a175/src/Sql/dbo/Auth/Stored%20Procedures/AuthRequest_DeleteIfExpired.sql
|
||||
let expiry_time = Utc::now().naive_utc() - chrono::TimeDelta::try_minutes(15).unwrap();
|
||||
let expiry_time = Utc::now().naive_utc() - chrono::TimeDelta::minutes(15);
|
||||
for auth_request in Self::find_created_before(&expiry_time, conn).await {
|
||||
auth_request.delete(conn).await.ok();
|
||||
}
|
||||
|
||||
+13
-3
@@ -494,14 +494,24 @@ impl Cipher {
|
||||
|
||||
/// Purge all ciphers that are old enough to be auto-deleted.
|
||||
pub async fn purge_trash(conn: &DbConn) {
|
||||
if let Some(auto_delete_days) = CONFIG.trash_auto_delete_days() {
|
||||
let Some(auto_delete_days) = CONFIG.trash_auto_delete_days().filter(|&d| d != 0) else {
|
||||
debug!("trash_auto_delete_days is not configured or set to 0, skipping");
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(days) = TimeDelta::try_days(i64::from(auto_delete_days)) else {
|
||||
error!("Unable to calculate auto delete days!");
|
||||
return;
|
||||
};
|
||||
let now = Utc::now().naive_utc();
|
||||
let dt = now - TimeDelta::try_days(auto_delete_days).unwrap();
|
||||
let Some(dt) = now.checked_sub_signed(days) else {
|
||||
error!("Unable to calculate trash purge date!");
|
||||
return;
|
||||
};
|
||||
for cipher in Self::find_deleted_before(&dt, conn).await {
|
||||
cipher.delete(conn).await.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn move_to_folder(
|
||||
&self,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use chrono::{NaiveDateTime, Utc};
|
||||
use chrono::{NaiveDateTime, TimeDelta, Utc};
|
||||
use derive_more::{AsRef, Deref, Display, From};
|
||||
use diesel::prelude::*;
|
||||
use serde_json::Value;
|
||||
@@ -112,8 +112,26 @@ impl EmergencyAccess {
|
||||
"object": "emergencyAccessGranteeDetails",
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn deadline_at(&self) -> Result<NaiveDateTime, crate::Error> {
|
||||
if !(EMERGENCY_ACCESS_TIMEOUT_RANGE).contains(&self.wait_time_days) {
|
||||
err!("Emergency access wait time is out-of-bounds!")
|
||||
}
|
||||
|
||||
let initiated_at = self.recovery_initiated_at.map_res("Emergency access initiation date missing!")?;
|
||||
|
||||
TimeDelta::try_days(i64::from(self.wait_time_days))
|
||||
.and_then(|w| initiated_at.checked_add_signed(w))
|
||||
.map_res("Emergency access deadline is invalid")
|
||||
}
|
||||
|
||||
pub fn is_recovery_allowed(&self, now: &NaiveDateTime) -> Result<bool, crate::Error> {
|
||||
Ok(self.deadline_at()?.le(now))
|
||||
}
|
||||
}
|
||||
|
||||
pub const EMERGENCY_ACCESS_TIMEOUT_RANGE: std::ops::RangeInclusive<i32> = 1..=90;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum EmergencyAccessType {
|
||||
View = 0,
|
||||
@@ -169,27 +187,42 @@ impl EmergencyAccess {
|
||||
|
||||
pub async fn update_access_status_and_save(
|
||||
&mut self,
|
||||
status: i32,
|
||||
from_status: i32,
|
||||
to_status: i32,
|
||||
date: &NaiveDateTime,
|
||||
conn: &DbConn,
|
||||
) -> EmptyResult {
|
||||
// Update the grantee so that it will refresh it's status.
|
||||
User::update_uuid_revision(self.grantee_uuid.as_ref().expect("Error getting grantee"), conn).await;
|
||||
self.status = status;
|
||||
date.clone_into(&mut self.updated_at);
|
||||
|
||||
conn.run(move |conn| {
|
||||
let uuid = self.uuid.clone();
|
||||
let updated_at = *date;
|
||||
let res = conn
|
||||
.run(move |conn| {
|
||||
crate::util::retry(
|
||||
|| {
|
||||
diesel::update(emergency_access::table.filter(emergency_access::uuid.eq(&self.uuid)))
|
||||
.set((emergency_access::status.eq(status), emergency_access::updated_at.eq(date)))
|
||||
diesel::update(
|
||||
emergency_access::table
|
||||
.filter(emergency_access::uuid.eq(&uuid))
|
||||
.filter(emergency_access::status.eq(from_status)),
|
||||
)
|
||||
.set((emergency_access::status.eq(to_status), emergency_access::updated_at.eq(updated_at)))
|
||||
.execute(conn)
|
||||
},
|
||||
10,
|
||||
)
|
||||
.map_res("Error updating emergency access status")
|
||||
})
|
||||
.await
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Ok(1) => {} // One record updated, which is valid
|
||||
Ok(_) => err!("Emergency access status was changed already"),
|
||||
Err(e) => err!("Error updating emergency access status", format!("{e}")),
|
||||
}
|
||||
|
||||
self.status = to_status;
|
||||
self.updated_at = updated_at;
|
||||
|
||||
// Update the grantee so that it will refresh it's status.
|
||||
User::update_uuid_revision(self.grantee_uuid.as_ref().expect("Error getting grantee"), conn).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update_last_notification_date_and_save(&mut self, date: &NaiveDateTime, conn: &DbConn) -> EmptyResult {
|
||||
|
||||
+11
-5
@@ -347,17 +347,23 @@ impl Event {
|
||||
}
|
||||
|
||||
pub async fn clean_events(conn: &DbConn) -> EmptyResult {
|
||||
if let Some(days_to_retain) = CONFIG.events_days_retain() {
|
||||
let dt = Utc::now().naive_utc() - TimeDelta::try_days(days_to_retain).unwrap();
|
||||
let Some(days_to_retain) = CONFIG.events_days_retain().filter(|&d| d != 0) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let Some(days_to_retain) = TimeDelta::try_days(i64::from(days_to_retain)) else {
|
||||
err!("Unable to calculate the event retention date, check 'EVENTS_DAYS_RETAIN'")
|
||||
};
|
||||
|
||||
let Some(dt) = Utc::now().naive_utc().checked_sub_signed(days_to_retain) else {
|
||||
err!("Unable to calculate the event retention date, check 'EVENTS_DAYS_RETAIN'")
|
||||
};
|
||||
conn.run(move |conn| {
|
||||
diesel::delete(event::table.filter(event::event_date.lt(dt)))
|
||||
.execute(conn)
|
||||
.map_res("Error cleaning old events")
|
||||
})
|
||||
.await
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -137,15 +137,15 @@ impl CollectionGroup {
|
||||
}
|
||||
|
||||
pub fn to_json_details_for_group(&self) -> Value {
|
||||
// If both read_only and hide_passwords are false, then manage should be true
|
||||
// You can't have an entry with read_only and manage, or hide_passwords and manage
|
||||
// Or an entry with everything to false
|
||||
// For backwards compatibility and migration proposes we keep checking read_only and hide_password
|
||||
// Return the stored manage flag. Deriving it from !read_only && !hide_passwords turned the group
|
||||
// permission "Can edit items" (readOnly=false, hidePasswords=false, manage=false) into "Can manage"
|
||||
// in GET /organizations/<org>/collections/details, while GET /groups/<id>/details reported the stored
|
||||
// value. Saving the collection access dialog then persisted manage=true.
|
||||
json!({
|
||||
"id": self.groups_uuid,
|
||||
"readOnly": self.read_only,
|
||||
"hidePasswords": self.hide_passwords,
|
||||
"manage": self.manage || (!self.read_only && !self.hide_passwords),
|
||||
"manage": self.manage,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,9 @@ pub use self::auth_request::{AuthRequest, AuthRequestId};
|
||||
pub use self::cipher::{Cipher, CipherId, RepromptType};
|
||||
pub use self::collection::{Collection, CollectionCipher, CollectionId, CollectionUser};
|
||||
pub use self::device::{Device, DeviceId, DeviceType, DeviceWithAuthRequest, PushId};
|
||||
pub use self::emergency_access::{EmergencyAccess, EmergencyAccessId, EmergencyAccessStatus, EmergencyAccessType};
|
||||
pub use self::emergency_access::{
|
||||
EMERGENCY_ACCESS_TIMEOUT_RANGE, EmergencyAccess, EmergencyAccessId, EmergencyAccessStatus, EmergencyAccessType,
|
||||
};
|
||||
pub use self::event::{Event, EventType};
|
||||
pub use self::favorite::Favorite;
|
||||
pub use self::folder::{Folder, FolderCipher, FolderId};
|
||||
|
||||
@@ -523,7 +523,8 @@ impl Membership {
|
||||
|
||||
// These are per user
|
||||
"userId": self.user_uuid,
|
||||
"key": self.akey,
|
||||
// Accepted (not yet confirmed) members have no key yet; send null like upstream, not ""
|
||||
"key": (!self.akey.is_empty()).then_some(&self.akey),
|
||||
"status": self.status,
|
||||
"type": membership_type,
|
||||
"enabled": true,
|
||||
|
||||
@@ -37,7 +37,7 @@ impl TwoFactorIncomplete {
|
||||
ip: &ClientIp,
|
||||
conn: &DbConn,
|
||||
) -> EmptyResult {
|
||||
if CONFIG.incomplete_2fa_time_limit() <= 0 || !CONFIG.mail_enabled() {
|
||||
if CONFIG.incomplete_2fa_time_limit() == 0 || !CONFIG.mail_enabled() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ impl TwoFactorIncomplete {
|
||||
}
|
||||
|
||||
pub async fn mark_complete(user_uuid: &UserId, device_uuid: &DeviceId, conn: &DbConn) -> EmptyResult {
|
||||
if CONFIG.incomplete_2fa_time_limit() <= 0 || !CONFIG.mail_enabled() {
|
||||
if CONFIG.incomplete_2fa_time_limit() == 0 || !CONFIG.mail_enabled() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -425,12 +425,12 @@ macro_rules! err_json {
|
||||
macro_rules! err_handler {
|
||||
($expr:expr) => {{
|
||||
error!(target: "auth", "Unauthorized Error: {}", $expr);
|
||||
return ::rocket::request::Outcome::Error((rocket::http::Status::Unauthorized, $expr));
|
||||
return ::rocket::request::Outcome::Error((::rocket::http::Status::Unauthorized, $expr));
|
||||
}};
|
||||
($usr_msg:expr, $log_value:expr) => {{
|
||||
let usr_msg = $usr_msg;
|
||||
let log_value = $log_value;
|
||||
error!(target: "auth", "Unauthorized Error: {usr_msg}. {log_value}");
|
||||
return ::rocket::request::Outcome::Error((rocket::http::Status::Unauthorized, usr_msg));
|
||||
return ::rocket::request::Outcome::Error((::rocket::http::Status::Unauthorized, usr_msg));
|
||||
}};
|
||||
}
|
||||
+8
-8
@@ -36,7 +36,7 @@ fn smtp_transport() -> AsyncSmtpTransport<Tokio1Executor> {
|
||||
|
||||
let smtp_client = AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous(host.as_str())
|
||||
.port(CONFIG.smtp_port())
|
||||
.timeout(Some(Duration::from_secs(CONFIG.smtp_timeout())));
|
||||
.timeout(Some(Duration::from_secs(u64::from(CONFIG.smtp_timeout()))));
|
||||
|
||||
// Determine security
|
||||
let smtp_client = if CONFIG.smtp_security() == *"off" {
|
||||
@@ -169,7 +169,7 @@ pub async fn send_password_hint(address: &str, hint: Option<String>) -> EmptyRes
|
||||
}
|
||||
|
||||
pub async fn send_delete_account(address: &str, user_id: &UserId) -> EmptyResult {
|
||||
let claims = generate_delete_claims(user_id.to_string());
|
||||
let claims = generate_delete_claims(user_id.to_string())?;
|
||||
let delete_token = encode_jwt(&claims);
|
||||
|
||||
let (subject, body_html, body_text) = get_text(
|
||||
@@ -187,7 +187,7 @@ pub async fn send_delete_account(address: &str, user_id: &UserId) -> EmptyResult
|
||||
}
|
||||
|
||||
pub async fn send_verify_email(address: &str, user_id: &UserId) -> EmptyResult {
|
||||
let claims = generate_verify_email_claims(user_id);
|
||||
let claims = generate_verify_email_claims(user_id)?;
|
||||
let verify_email_token = encode_jwt(&claims);
|
||||
|
||||
let (subject, body_html, body_text) = get_text(
|
||||
@@ -237,7 +237,7 @@ pub async fn send_welcome(address: &str) -> EmptyResult {
|
||||
}
|
||||
|
||||
pub async fn send_welcome_must_verify(address: &str, user_id: &UserId) -> EmptyResult {
|
||||
let claims = generate_verify_email_claims(user_id);
|
||||
let claims = generate_verify_email_claims(user_id)?;
|
||||
let verify_email_token = encode_jwt(&claims);
|
||||
|
||||
let (subject, body_html, body_text) = get_text(
|
||||
@@ -292,7 +292,7 @@ pub async fn send_invite(
|
||||
org_id.clone(),
|
||||
member_id.clone(),
|
||||
invited_by_email,
|
||||
);
|
||||
)?;
|
||||
let invite_token = encode_jwt(&claims);
|
||||
let mut query = url::Url::parse("https://query.builder").unwrap();
|
||||
{
|
||||
@@ -351,7 +351,7 @@ pub async fn send_emergency_access_invite(
|
||||
emer_id.clone(),
|
||||
String::from(grantor_name),
|
||||
String::from(grantor_email),
|
||||
);
|
||||
)?;
|
||||
|
||||
// Build the query here to ensure proper escaping
|
||||
let mut query = url::Url::parse("https://query.builder").unwrap();
|
||||
@@ -738,8 +738,8 @@ async fn send_email(address: &str, subject: &str, body_html: String, body_text:
|
||||
let smtp_from = Address::from_str(&CONFIG.smtp_from())?;
|
||||
|
||||
let body = if CONFIG.smtp_embed_images() {
|
||||
let logo_gray_body = Body::new(crate::api::static_files("logo-gray.png").unwrap().1.to_vec());
|
||||
let mail_github_body = Body::new(crate::api::static_files("mail-github.png").unwrap().1.to_vec());
|
||||
let logo_gray_body = Body::new(crate::api::STATIC_FILES["logo-gray.png"].bytes.to_vec());
|
||||
let mail_github_body = Body::new(crate::api::STATIC_FILES["mail-github.png"].bytes.to_vec());
|
||||
MultiPart::alternative().singlepart(SinglePart::plain(body_text)).multipart(
|
||||
MultiPart::related()
|
||||
.singlepart(SinglePart::html(body_html))
|
||||
|
||||
+5
-3
@@ -595,7 +595,7 @@ async fn launch_rocket(pool: db::DbPool, extra_debug: bool) -> Result<(), Error>
|
||||
.manage(Arc::clone(&WS_ANONYMOUS_SUBSCRIPTIONS))
|
||||
.attach(util::AppHeaders())
|
||||
.attach(util::Cors())
|
||||
.attach(util::BetterLogging(extra_debug))
|
||||
.attach(util::BetterLogging::new(extra_debug, basepath))
|
||||
.ignite()
|
||||
.await?;
|
||||
|
||||
@@ -729,7 +729,7 @@ fn schedule_jobs(pool: db::DbPool) {
|
||||
// Cleanup the event table of records x days old.
|
||||
if CONFIG.org_events_enabled()
|
||||
&& !CONFIG.event_cleanup_schedule().is_empty()
|
||||
&& CONFIG.events_days_retain().is_some()
|
||||
&& CONFIG.events_days_retain().is_some_and(|d| d != 0)
|
||||
{
|
||||
sched.add(Job::new(CONFIG.event_cleanup_schedule().parse().unwrap(), || {
|
||||
runtime.spawn(api::event_cleanup_job(pool.clone()));
|
||||
@@ -754,7 +754,9 @@ fn schedule_jobs(pool: db::DbPool) {
|
||||
// tick, the one that was added earlier will run first.
|
||||
loop {
|
||||
sched.tick();
|
||||
runtime.block_on(tokio::time::sleep(tokio::time::Duration::from_millis(CONFIG.job_poll_interval_ms())));
|
||||
runtime.block_on(tokio::time::sleep(tokio::time::Duration::from_millis(u64::from(
|
||||
CONFIG.job_poll_interval_ms(),
|
||||
))));
|
||||
}
|
||||
})
|
||||
.expect("Error spawning job scheduler thread");
|
||||
|
||||
+6
-6
@@ -7,20 +7,20 @@ use crate::{CONFIG, Error};
|
||||
type Limiter<T = IpAddr> = RateLimiter<T, DashMapStateStore<T>, DefaultClock>;
|
||||
|
||||
static LIMITER_LOGIN: LazyLock<Limiter> = LazyLock::new(|| {
|
||||
let seconds = Duration::from_secs(CONFIG.login_ratelimit_seconds());
|
||||
let burst = NonZeroU32::new(CONFIG.login_ratelimit_max_burst()).expect("Non-zero login ratelimit burst");
|
||||
let seconds = Duration::from_secs(u64::from(CONFIG.login_ratelimit_seconds()));
|
||||
let burst = NonZeroU32::new(u32::from(CONFIG.login_ratelimit_max_burst())).expect("Non-zero login ratelimit burst");
|
||||
RateLimiter::keyed(Quota::with_period(seconds).expect("Non-zero login ratelimit seconds").allow_burst(burst))
|
||||
});
|
||||
|
||||
static LIMITER_ADMIN: LazyLock<Limiter> = LazyLock::new(|| {
|
||||
let seconds = Duration::from_secs(CONFIG.admin_ratelimit_seconds());
|
||||
let burst = NonZeroU32::new(CONFIG.admin_ratelimit_max_burst()).expect("Non-zero admin ratelimit burst");
|
||||
let seconds = Duration::from_secs(u64::from(CONFIG.admin_ratelimit_seconds()));
|
||||
let burst = NonZeroU32::new(u32::from(CONFIG.admin_ratelimit_max_burst())).expect("Non-zero admin ratelimit burst");
|
||||
RateLimiter::keyed(Quota::with_period(seconds).expect("Non-zero admin ratelimit seconds").allow_burst(burst))
|
||||
});
|
||||
|
||||
static LIMITER_UNAUTHENTICATED: LazyLock<Limiter> = LazyLock::new(|| {
|
||||
let seconds = Duration::from_secs(CONFIG.unauthenticated_ratelimit_seconds());
|
||||
let burst = NonZeroU32::new(CONFIG.unauthenticated_ratelimit_max_burst())
|
||||
let seconds = Duration::from_secs(u64::from(CONFIG.unauthenticated_ratelimit_seconds()));
|
||||
let burst = NonZeroU32::new(u32::from(CONFIG.unauthenticated_ratelimit_max_burst()))
|
||||
.expect("Non-zero unauthenticated ratelimit burst");
|
||||
RateLimiter::keyed(
|
||||
Quota::with_period(seconds).expect("Non-zero unauthenticated ratelimit seconds").allow_burst(burst),
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ static CLIENT_CACHE_KEY: LazyLock<String> = LazyLock::new(|| "sso-client".to_own
|
||||
static CLIENT_CACHE: LazyLock<moka::sync::Cache<String, Client>> = LazyLock::new(|| {
|
||||
moka::sync::Cache::builder()
|
||||
.max_capacity(1)
|
||||
.time_to_live(Duration::from_secs(CONFIG.sso_client_cache_expiration()))
|
||||
.time_to_live(Duration::from_secs(u64::from(CONFIG.sso_client_cache_expiration())))
|
||||
.build()
|
||||
});
|
||||
static REFRESH_CACHE: LazyLock<moka::future::Cache<String, Result<RefreshTokenResponse, String>>> =
|
||||
|
||||
+9
-2
@@ -93,7 +93,9 @@ async function generateSupportString(event, dj) {
|
||||
supportString += `* Running within a container: ${yn(dj.running_within_container)} (Base: ${dj.container_base_image})\n`;
|
||||
supportString += `* Database type: ${dj.db_type}\n`;
|
||||
supportString += `* Database version: ${dj.db_version}\n`;
|
||||
supportString += `* Uses config.json: ${yn(dj.overrides !== "")}\n`;
|
||||
supportString += `* Uses config.json: ${yn(dj.config_file_status !== "NotFound")}`;
|
||||
supportString += dj.config_file_status !== "NotFound" ? ` (Valid: ${chk(dj.config_file_status === "Loaded")})\n` : "\n";
|
||||
supportString += `* Environment variables are valid: ${chk(dj.invalid_env_vars == "")}\n`;
|
||||
supportString += `* Uses custom templates: ${yn(dj.template_overrides !== "")}${template_overrides}\n`;
|
||||
supportString += `* Uses a reverse proxy: ${yn(dj.ip_header_exists)}\n`;
|
||||
if (dj.ip_header_exists) {
|
||||
@@ -116,7 +118,7 @@ async function generateSupportString(event, dj) {
|
||||
}
|
||||
supportString += `* HTTP Response Checks: ${chk(httpResponseCheck)}\n`;
|
||||
if (dj.invalid_feature_flags != "") {
|
||||
supportString += "* Invalid feature flags: true\n";
|
||||
supportString += "* Invalid feature flags: true :x:\n";
|
||||
}
|
||||
|
||||
const jsonResponse = await fetch(`${BASE_URL}/admin/diagnostics/config`, {
|
||||
@@ -132,6 +134,11 @@ async function generateSupportString(event, dj) {
|
||||
supportString += "\n### Config & Details (Generated via diagnostics page)\n\n";
|
||||
supportString += "<details><summary>Show Config & Details</summary>\n";
|
||||
|
||||
// Show invalid environment variables if they exists
|
||||
if (dj.invalid_env_vars != "") {
|
||||
supportString += `\n**Invalid environment variables:** ${dj.invalid_env_vars}\n`;
|
||||
}
|
||||
|
||||
// Add overrides if they exists
|
||||
if (dj.overrides != "") {
|
||||
supportString += `\n**Environment settings which are overridden:** ${dj.overrides}\n`;
|
||||
|
||||
@@ -69,12 +69,26 @@
|
||||
</dd>
|
||||
<dt class="col-sm-5">Uses config.json</dt>
|
||||
<dd class="col-sm-7">
|
||||
{{#if page_data.overrides}}
|
||||
{{#if (eq page_data.config_file_status "Loaded")}}
|
||||
<span class="d-inline"><b>Yes</b></span>
|
||||
{{#if page_data.overrides}}
|
||||
<span class="badge bg-info text-dark abbr-badge" title="Environment variables are overwritten by a config.json.
{{page_data.overrides}}">Details</span>
|
||||
{{/if}}
|
||||
{{#unless page_data.overrides}}
|
||||
<span class="d-block"><b>No</b></span>
|
||||
{{~else if (eq page_data.config_file_status "Error")}}
|
||||
<span class="d-inline"><b>Errors</b></span>
|
||||
<span class="badge bg-danger abbr-badge" title="Config file could not be loaded and was ignored. Check server logs for details.">Details</span>
|
||||
{{~else}}
|
||||
<span class="d-inline"><b>No</b></span>
|
||||
{{/if}}
|
||||
</dd>
|
||||
<dt class="col-sm-5">Environment variables are valid</dt>
|
||||
<dd class="col-sm-7">
|
||||
{{#if page_data.invalid_env_vars}}
|
||||
<span class="d-inline"><b>No</b></span>
|
||||
<span class="badge bg-danger abbr-badge" title="Invalid environment variables are found and defaults are used instead.
{{page_data.invalid_env_vars}}">Details</span>
|
||||
{{/if}}
|
||||
{{#unless page_data.invalid_env_vars}}
|
||||
<span class="d-block"><b>Yes</b></span>
|
||||
{{/unless}}
|
||||
</dd>
|
||||
<dt class="col-sm-5">Uses custom templates</dt>
|
||||
|
||||
+77
-32
@@ -210,7 +210,7 @@ impl Fairing for Cors {
|
||||
pub struct Cached<R> {
|
||||
response: R,
|
||||
is_immutable: bool,
|
||||
ttl: u64,
|
||||
ttl: u32,
|
||||
}
|
||||
|
||||
impl<R> Cached<R> {
|
||||
@@ -230,7 +230,7 @@ impl<R> Cached<R> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ttl(response: R, ttl: u64, is_immutable: bool) -> Cached<R> {
|
||||
pub fn ttl(response: R, ttl: u32, is_immutable: bool) -> Cached<R> {
|
||||
Self {
|
||||
response,
|
||||
is_immutable,
|
||||
@@ -251,7 +251,7 @@ impl<'r, R: 'r + Responder<'r, 'static> + Send> Responder<'r, 'static> for Cache
|
||||
res.set_raw_header("Cache-Control", cache_control_header);
|
||||
|
||||
let time_now = Local::now();
|
||||
let expiry_time = time_now + chrono::TimeDelta::try_seconds(self.ttl.try_into().unwrap()).unwrap();
|
||||
let expiry_time = time_now + chrono::TimeDelta::try_seconds(self.ttl.into()).unwrap();
|
||||
res.set_raw_header("Expires", format_datetime_http(&expiry_time));
|
||||
Ok(res)
|
||||
}
|
||||
@@ -259,14 +259,16 @@ impl<'r, R: 'r + Responder<'r, 'static> + Send> Responder<'r, 'static> for Cache
|
||||
|
||||
pub struct EtagCached<R> {
|
||||
response: R,
|
||||
content_type: ContentType,
|
||||
etag: String,
|
||||
}
|
||||
|
||||
impl<R> EtagCached<R> {
|
||||
/// An `etag` response should always be quoted
|
||||
pub fn new(response: R, etag: &str) -> Self {
|
||||
pub fn new(response: R, content_type: ContentType, etag: &str) -> Self {
|
||||
Self {
|
||||
response,
|
||||
content_type,
|
||||
etag: format!("\"{etag}\""),
|
||||
}
|
||||
}
|
||||
@@ -288,7 +290,8 @@ impl<'r, R: 'r + Responder<'r, 'static> + Send> Responder<'r, 'static> for EtagC
|
||||
self.response.respond_to(request)?
|
||||
};
|
||||
|
||||
// Both 200 (OK) and 304 (Not Modified) need to return the etag and cache-control
|
||||
// Both 200 (OK) and 304 (Not Modified) need to return the Content-Type Etag and Cache-Control
|
||||
res.set_header(self.content_type);
|
||||
res.set_raw_header("Etag", self.etag);
|
||||
res.set_raw_header("Cache-Control", "public, no-cache");
|
||||
Ok(res)
|
||||
@@ -299,19 +302,55 @@ impl<'r, R: 'r + Responder<'r, 'static> + Send> Responder<'r, 'static> for EtagC
|
||||
// Effectively ignores, any static file route, and the alive endpoint
|
||||
const LOGGED_ROUTES: [&str; 7] = ["/api", "/admin", "/identity", "/icons", "/attachments", "/events", "/notifications"];
|
||||
|
||||
// Boolean is extra debug, when true, we ignore the whitelist above and also print the mounts
|
||||
pub struct BetterLogging(pub bool);
|
||||
// When extra is true, we ignore the whitelist above and also print the mounts
|
||||
pub struct BetterLogging {
|
||||
extra: bool,
|
||||
log_requests: bool,
|
||||
log_response: bool,
|
||||
domain_path: String,
|
||||
}
|
||||
|
||||
impl BetterLogging {
|
||||
pub fn new(extra: bool, domain_path: &str) -> Self {
|
||||
Self {
|
||||
extra,
|
||||
log_requests: log_enabled!(target: "request", log::Level::Info),
|
||||
log_response: log_enabled!(target: "response", log::Level::Info),
|
||||
domain_path: rocket::http::RawStr::new(&domain_path).percent_decode_lossy().into_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
fn log_path<'p>(&self, method: Method, path: &'p rocket::http::RawStr) -> Option<std::borrow::Cow<'p, str>> {
|
||||
if !self.extra && method == Method::Options {
|
||||
return None;
|
||||
}
|
||||
let path = path.percent_decode_lossy();
|
||||
if self.extra {
|
||||
return Some(path);
|
||||
}
|
||||
let subpath = path.strip_prefix(&self.domain_path).unwrap_or(&path);
|
||||
LOGGED_ROUTES.iter().any(|r| subpath.starts_with(r)).then_some(path)
|
||||
}
|
||||
}
|
||||
|
||||
#[rocket::async_trait]
|
||||
impl Fairing for BetterLogging {
|
||||
fn info(&self) -> Info {
|
||||
let mut kind = Kind::Liftoff;
|
||||
if self.log_requests {
|
||||
kind = kind | Kind::Request;
|
||||
}
|
||||
if self.log_response {
|
||||
kind = kind | Kind::Response;
|
||||
}
|
||||
Info {
|
||||
name: "Better Logging",
|
||||
kind: Kind::Liftoff | Kind::Request | Kind::Response,
|
||||
kind,
|
||||
}
|
||||
}
|
||||
|
||||
async fn on_liftoff(&self, rocket: &Rocket<Orbit>) {
|
||||
if self.0 {
|
||||
if self.extra {
|
||||
info!(target: "routes", "Routes loaded:");
|
||||
let mut routes: Vec<_> = rocket.routes().collect();
|
||||
routes.sort_by_key(|r| r.uri.path());
|
||||
@@ -335,36 +374,28 @@ impl Fairing for BetterLogging {
|
||||
}
|
||||
|
||||
async fn on_request(&self, request: &mut Request<'_>, _data: &mut Data<'_>) {
|
||||
let method = request.method();
|
||||
if !self.0 && method == Method::Options {
|
||||
return;
|
||||
}
|
||||
let uri = request.uri();
|
||||
let uri_path = uri.path();
|
||||
let uri_path_str = uri_path.url_decode_lossy();
|
||||
let uri_subpath = uri_path_str.strip_prefix(&CONFIG.domain_path()).unwrap_or(&uri_path_str);
|
||||
if self.0 || LOGGED_ROUTES.iter().any(|r| uri_subpath.starts_with(r)) {
|
||||
let method = request.method();
|
||||
let Some(path) = self.log_path(method, uri.path().raw()) else {
|
||||
return;
|
||||
};
|
||||
match uri.query() {
|
||||
Some(q) => info!(target: "request", "{method} {uri_path_str}?{}", &q[..q.len().min(30)]),
|
||||
None => info!(target: "request", "{method} {uri_path_str}"),
|
||||
Some(q) => {
|
||||
let q = q.as_str();
|
||||
info!(target: "request", "{method} {}?{}", path.escape_debug(), q[..q.floor_char_boundary(30)].escape_debug());
|
||||
}
|
||||
None => info!(target: "request", "{method} {}", path.escape_debug()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn on_response<'r>(&self, request: &'r Request<'_>, response: &mut Response<'r>) {
|
||||
if !self.0 && request.method() == Method::Options {
|
||||
if self.log_path(request.method(), request.uri().path().raw()).is_none() {
|
||||
return;
|
||||
}
|
||||
let uri_path = request.uri().path();
|
||||
let uri_path_str = uri_path.url_decode_lossy();
|
||||
let uri_subpath = uri_path_str.strip_prefix(&CONFIG.domain_path()).unwrap_or(&uri_path_str);
|
||||
if self.0 || LOGGED_ROUTES.iter().any(|r| uri_subpath.starts_with(r)) {
|
||||
let status = response.status();
|
||||
if let Some(route) = request.route() {
|
||||
info!(target: "response", "{route} => {status}");
|
||||
} else {
|
||||
info!(target: "response", "{status}");
|
||||
}
|
||||
match request.route() {
|
||||
Some(r) => info!(target: "response", "{r} => {status}"),
|
||||
None => info!(target: "response", "{status}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -447,20 +478,34 @@ pub fn get_env_str_value(key: &str) -> Option<String> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_env<V>(key: &str) -> Option<V>
|
||||
pub static INVALID_ENV_VARS: std::sync::Mutex<Vec<&'static str>> = std::sync::Mutex::new(Vec::new());
|
||||
|
||||
pub fn get_env<V>(key: &'static str) -> Option<V>
|
||||
where
|
||||
V: FromStr,
|
||||
{
|
||||
try_parse_string(get_env_str_value(key))
|
||||
let value = get_env_str_value(key);
|
||||
let value_is_some = value.is_some();
|
||||
let parsed: Option<V> = try_parse_string(value);
|
||||
if parsed.is_none() && value_is_some {
|
||||
println!("[ERROR] `{key}` contains an invalid value for its type and is ignored, the default is used instead.");
|
||||
INVALID_ENV_VARS.lock().unwrap().push(key);
|
||||
}
|
||||
parsed
|
||||
}
|
||||
|
||||
pub fn get_env_bool(key: &str) -> Option<bool> {
|
||||
pub fn get_env_bool(key: &'static str) -> Option<bool> {
|
||||
const TRUE_VALUES: &[&str] = &["true", "t", "yes", "y", "1"];
|
||||
const FALSE_VALUES: &[&str] = &["false", "f", "no", "n", "0"];
|
||||
|
||||
match get_env_str_value(key) {
|
||||
Some(val) if TRUE_VALUES.contains(&val.to_lowercase().as_ref()) => Some(true),
|
||||
Some(val) if FALSE_VALUES.contains(&val.to_lowercase().as_ref()) => Some(false),
|
||||
Some(_) => {
|
||||
println!("[ERROR] `{key}` contains an invalid boolean value and is ignored, the default is used instead.");
|
||||
INVALID_ENV_VARS.lock().unwrap().push(key);
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user