Spiegel von
https://github.com/dani-garcia/vaultwarden.git
synchronisiert 2024-05-19 15:30:05 +02:00
Commits vergleichen
32 Commits
Autor | SHA1 | Datum | |
---|---|---|---|
0fe93edea6 | |||
e9aa5a545e | |||
9dcc738f85 | |||
84a7c7da5d | |||
ca9234ed86 | |||
27dc67fadd | |||
2ad33ec97f | |||
e1a8df96db | |||
e42a37c6c1 | |||
129b835ac7 | |||
2d98aa3045 | |||
93636eb3c3 | |||
1e42755187 | |||
ce8efcc48f | |||
79ce5b49bc | |||
7c3cad197c | |||
000c606029 | |||
29144b2ce0 | |||
ea04b6f151 | |||
3427217686 | |||
a1fbd6d729 | |||
2cbfe6fa5b | |||
d86c4f2c23 | |||
6d73f30b4f | |||
d0c22b9fc9 | |||
d6b97090fa | |||
94b077cb2d | |||
bb2412d033 | |||
b9bdc9b8e2 | |||
897bdf8343 | |||
569add453d | |||
77cd5b5954 |
|
@ -84,12 +84,8 @@
|
|||
### WebSocket ###
|
||||
#################
|
||||
|
||||
## Enables websocket notifications
|
||||
# WEBSOCKET_ENABLED=false
|
||||
|
||||
## Controls the WebSocket server address and port
|
||||
# WEBSOCKET_ADDRESS=0.0.0.0
|
||||
# WEBSOCKET_PORT=3012
|
||||
## Enable websocket notifications
|
||||
# ENABLE_WEBSOCKET=true
|
||||
|
||||
##########################
|
||||
### Push notifications ###
|
||||
|
@ -448,6 +444,11 @@
|
|||
##
|
||||
## Maximum attempts before an email token is reset and a new email will need to be sent.
|
||||
# EMAIL_ATTEMPTS_LIMIT=3
|
||||
##
|
||||
## Setup email 2FA regardless of any organization policy
|
||||
# EMAIL_2FA_ENFORCE_ON_VERIFIED_INVITE=false
|
||||
## Automatically setup email 2FA as fallback provider when needed
|
||||
# EMAIL_2FA_AUTO_FALLBACK=false
|
||||
|
||||
## Other MFA/2FA settings
|
||||
## Disable 2FA remember
|
||||
|
@ -477,12 +478,19 @@
|
|||
# SMTP_HOST=smtp.domain.tld
|
||||
# SMTP_FROM=vaultwarden@domain.tld
|
||||
# SMTP_FROM_NAME=Vaultwarden
|
||||
# SMTP_SECURITY=starttls # ("starttls", "force_tls", "off") Enable a secure connection. Default is "starttls" (Explicit - ports 587 or 25), "force_tls" (Implicit - port 465) or "off", no encryption (port 25)
|
||||
# SMTP_PORT=587 # Ports 587 (submission) and 25 (smtp) are standard without encryption and with encryption via STARTTLS (Explicit TLS). Port 465 (submissions) is used for encrypted submission (Implicit TLS).
|
||||
# SMTP_USERNAME=username
|
||||
# SMTP_PASSWORD=password
|
||||
# SMTP_TIMEOUT=15
|
||||
|
||||
## Choose the type of secure connection for SMTP. The default is "starttls".
|
||||
## The available options are:
|
||||
## - "starttls": The default port is 587.
|
||||
## - "force_tls": The default port is 465.
|
||||
## - "off": The default port is 25.
|
||||
## Ports 587 (submission) and 25 (smtp) are standard without encryption and with encryption via STARTTLS (Explicit TLS). Port 465 (submissions) is used for encrypted submission (Implicit TLS).
|
||||
# SMTP_SECURITY=starttls
|
||||
# SMTP_PORT=587
|
||||
|
||||
# Whether to send mail via the `sendmail` command
|
||||
# USE_SENDMAIL=false
|
||||
# Which sendmail command to use. The one found in the $PATH is used if not specified.
|
||||
|
@ -524,7 +532,8 @@
|
|||
## Rocket specific settings
|
||||
## See https://rocket.rs/v0.5/guide/configuration/ for more details.
|
||||
# ROCKET_ADDRESS=0.0.0.0
|
||||
# ROCKET_PORT=80 # Defaults to 80 in the Docker images, or 8000 otherwise.
|
||||
## The default port is 8000, unless running in a Docker container, in which case it is 80.
|
||||
# ROCKET_PORT=8000
|
||||
# ROCKET_TLS={certs="/path/to/certs.pem",key="/path/to/key.pem"}
|
||||
|
||||
|
||||
|
|
6
.github/workflows/build.yml
gevendort
6
.github/workflows/build.yml
gevendort
|
@ -46,7 +46,7 @@ jobs:
|
|||
steps:
|
||||
# Checkout the repo
|
||||
- name: "Checkout"
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1
|
||||
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b #v4.1.4
|
||||
# End Checkout the repo
|
||||
|
||||
|
||||
|
@ -74,7 +74,7 @@ jobs:
|
|||
|
||||
# Only install the clippy and rustfmt components on the default rust-toolchain
|
||||
- name: "Install rust-toolchain version"
|
||||
uses: dtolnay/rust-toolchain@be73d7920c329f220ce78e0234b8f96b7ae60248 # master @ 2023-12-07 - 10:22 PM GMT+1
|
||||
uses: dtolnay/rust-toolchain@bb45937a053e097f8591208d8e74c90db1873d07 # master @ Apr 14, 2024, 9:02 PM GMT+2
|
||||
if: ${{ matrix.channel == 'rust-toolchain' }}
|
||||
with:
|
||||
toolchain: "${{steps.toolchain.outputs.RUST_TOOLCHAIN}}"
|
||||
|
@ -84,7 +84,7 @@ jobs:
|
|||
|
||||
# Install the any other channel to be used for which we do not execute clippy and rustfmt
|
||||
- name: "Install MSRV version"
|
||||
uses: dtolnay/rust-toolchain@be73d7920c329f220ce78e0234b8f96b7ae60248 # master @ 2023-12-07 - 10:22 PM GMT+1
|
||||
uses: dtolnay/rust-toolchain@bb45937a053e097f8591208d8e74c90db1873d07 # master @ Apr 14, 2024, 9:02 PM GMT+2
|
||||
if: ${{ matrix.channel != 'rust-toolchain' }}
|
||||
with:
|
||||
toolchain: "${{steps.toolchain.outputs.RUST_TOOLCHAIN}}"
|
||||
|
|
2
.github/workflows/hadolint.yml
gevendort
2
.github/workflows/hadolint.yml
gevendort
|
@ -13,7 +13,7 @@ jobs:
|
|||
steps:
|
||||
# Checkout the repo
|
||||
- name: Checkout
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
# End Checkout the repo
|
||||
|
||||
# Download hadolint - https://github.com/hadolint/hadolint/releases
|
||||
|
|
36
.github/workflows/release.yml
gevendort
36
.github/workflows/release.yml
gevendort
|
@ -2,20 +2,10 @@ name: Release
|
|||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- ".github/workflows/release.yml"
|
||||
- "src/**"
|
||||
- "migrations/**"
|
||||
- "docker/**"
|
||||
- "Cargo.*"
|
||||
- "build.rs"
|
||||
- "diesel.toml"
|
||||
- "rust-toolchain.toml"
|
||||
|
||||
branches: # Only on paths above
|
||||
branches:
|
||||
- main
|
||||
|
||||
tags: # Always, regardless of paths above
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
|
@ -68,7 +58,7 @@ jobs:
|
|||
steps:
|
||||
# Checkout the repo
|
||||
- name: Checkout
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
@ -79,11 +69,11 @@ jobs:
|
|||
|
||||
# Start Docker Buildx
|
||||
- name: Setup Docker Buildx
|
||||
uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0
|
||||
uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb # v3.3.0
|
||||
# https://github.com/moby/buildkit/issues/3969
|
||||
# Also set max parallelism to 2, the default of 4 breaks GitHub Actions
|
||||
with:
|
||||
config-inline: |
|
||||
buildkitd-config-inline: |
|
||||
[worker.oci]
|
||||
max-parallelism = 2
|
||||
driver-opts: |
|
||||
|
@ -112,7 +102,7 @@ jobs:
|
|||
|
||||
# Login to Docker Hub
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0
|
||||
uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 # v3.1.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
@ -126,7 +116,7 @@ jobs:
|
|||
|
||||
# Login to GitHub Container Registry
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0
|
||||
uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 # v3.1.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
|
@ -147,7 +137,7 @@ jobs:
|
|||
|
||||
# Login to Quay.io
|
||||
- name: Login to Quay.io
|
||||
uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0
|
||||
uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 # v3.1.0
|
||||
with:
|
||||
registry: quay.io
|
||||
username: ${{ secrets.QUAY_USERNAME }}
|
||||
|
@ -181,7 +171,7 @@ jobs:
|
|||
echo "CONTAINER_REGISTRIES=${CONTAINER_REGISTRIES:+${CONTAINER_REGISTRIES},}localhost:5000/vaultwarden/server" | tee -a "${GITHUB_ENV}"
|
||||
|
||||
- name: Bake ${{ matrix.base_image }} containers
|
||||
uses: docker/bake-action@849707117b03d39aba7924c50a10376a69e88d7d # v4.1.0
|
||||
uses: docker/bake-action@73b0efa7a0e8ac276e0a8d5c580698a942ff10b5 # v4.4.0
|
||||
env:
|
||||
BASE_TAGS: "${{ env.BASE_TAGS }}"
|
||||
SOURCE_COMMIT: "${{ env.SOURCE_COMMIT }}"
|
||||
|
@ -239,28 +229,28 @@ jobs:
|
|||
|
||||
# Upload artifacts to Github Actions
|
||||
- name: "Upload amd64 artifact"
|
||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||
if: ${{ matrix.base_image == 'alpine' }}
|
||||
with:
|
||||
name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-amd64
|
||||
path: vaultwarden-amd64
|
||||
|
||||
- name: "Upload arm64 artifact"
|
||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||
if: ${{ matrix.base_image == 'alpine' }}
|
||||
with:
|
||||
name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-arm64
|
||||
path: vaultwarden-arm64
|
||||
|
||||
- name: "Upload armv7 artifact"
|
||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||
if: ${{ matrix.base_image == 'alpine' }}
|
||||
with:
|
||||
name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-armv7
|
||||
path: vaultwarden-armv7
|
||||
|
||||
- name: "Upload armv6 artifact"
|
||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||
if: ${{ matrix.base_image == 'alpine' }}
|
||||
with:
|
||||
name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-armv6
|
||||
|
|
3
.github/workflows/releasecache-cleanup.yml
gevendort
3
.github/workflows/releasecache-cleanup.yml
gevendort
|
@ -14,10 +14,11 @@ jobs:
|
|||
releasecache-cleanup:
|
||||
name: Releasecache Cleanup
|
||||
runs-on: ubuntu-22.04
|
||||
continue-on-error: true
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Delete vaultwarden-buildcache containers
|
||||
uses: actions/delete-package-versions@0d39a63126868f5eefaa47169615edd3c0f61e20 # v4.1.1
|
||||
uses: actions/delete-package-versions@e5bc658cc4c965c472efe991f8beea3981499c55 # v5.0.0
|
||||
with:
|
||||
package-name: 'vaultwarden-buildcache'
|
||||
package-type: 'container'
|
||||
|
|
6
.github/workflows/trivy.yml
gevendort
6
.github/workflows/trivy.yml
gevendort
|
@ -25,10 +25,10 @@ jobs:
|
|||
actions: read
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1
|
||||
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b #v4.1.4
|
||||
|
||||
- name: Run Trivy vulnerability scanner
|
||||
uses: aquasecurity/trivy-action@d43c1f16c00cfd3978dde6c07f4bbcf9eb6993ca # v0.16.1
|
||||
uses: aquasecurity/trivy-action@d710430a6722f083d3b36b8339ff66b32f22ee55 # v0.19.0
|
||||
with:
|
||||
scan-type: repo
|
||||
ignore-unfixed: true
|
||||
|
@ -37,6 +37,6 @@ jobs:
|
|||
severity: CRITICAL,HIGH
|
||||
|
||||
- name: Upload Trivy scan results to GitHub Security tab
|
||||
uses: github/codeql-action/upload-sarif@b7bf0a3ed3ecfa44160715d7c442788f65f0f923 # v3.23.2
|
||||
uses: github/codeql-action/upload-sarif@2bbafcdd7fbf96243689e764c2f15d9735164f33 # v3.25.3
|
||||
with:
|
||||
sarif_file: 'trivy-results.sarif'
|
||||
|
|
1302
Cargo.lock
generiert
1302
Cargo.lock
generiert
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
123
Cargo.toml
123
Cargo.toml
|
@ -3,7 +3,7 @@ name = "vaultwarden"
|
|||
version = "1.0.0"
|
||||
authors = ["Daniel García <dani-garcia@users.noreply.github.com>"]
|
||||
edition = "2021"
|
||||
rust-version = "1.73.0"
|
||||
rust-version = "1.75.0"
|
||||
resolver = "2"
|
||||
|
||||
repository = "https://github.com/dani-garcia/vaultwarden"
|
||||
|
@ -36,11 +36,11 @@ unstable = []
|
|||
|
||||
[target."cfg(not(windows))".dependencies]
|
||||
# Logging
|
||||
syslog = "6.1.0"
|
||||
syslog = "6.1.1"
|
||||
|
||||
[dependencies]
|
||||
# Logging
|
||||
log = "0.4.20"
|
||||
log = "0.4.21"
|
||||
fern = { version = "0.6.2", features = ["syslog-6", "reopen-1"] }
|
||||
tracing = { version = "0.1.40", features = ["log"] } # Needed to have lettre and webauthn-rs trace logging to work
|
||||
|
||||
|
@ -51,57 +51,56 @@ dotenvy = { version = "0.15.7", default-features = false }
|
|||
once_cell = "1.19.0"
|
||||
|
||||
# Numerical libraries
|
||||
num-traits = "0.2.17"
|
||||
num-derive = "0.4.1"
|
||||
bigdecimal = "0.4.2"
|
||||
num-traits = "0.2.18"
|
||||
num-derive = "0.4.2"
|
||||
bigdecimal = "0.4.3"
|
||||
|
||||
# Web framework
|
||||
rocket = { version = "0.5.0", features = ["tls", "json"], default-features = false }
|
||||
rocket_ws = { version ="0.1.0" }
|
||||
|
||||
# WebSockets libraries
|
||||
tokio-tungstenite = "0.20.1"
|
||||
rmpv = "1.0.1" # MessagePack library
|
||||
rmpv = "1.0.2" # MessagePack library
|
||||
|
||||
# Concurrent HashMap used for WebSocket messaging and favicons
|
||||
dashmap = "5.5.3"
|
||||
|
||||
# Async futures
|
||||
futures = "0.3.30"
|
||||
tokio = { version = "1.35.1", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal"] }
|
||||
tokio = { version = "1.37.0", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] }
|
||||
|
||||
# A generic serialization/deserialization framework
|
||||
serde = { version = "1.0.195", features = ["derive"] }
|
||||
serde_json = "1.0.111"
|
||||
serde = { version = "1.0.198", features = ["derive"] }
|
||||
serde_json = "1.0.116"
|
||||
|
||||
# A safe, extensible ORM and Query builder
|
||||
diesel = { version = "2.1.4", features = ["chrono", "r2d2", "numeric"] }
|
||||
diesel = { version = "2.1.6", features = ["chrono", "r2d2", "numeric"] }
|
||||
diesel_migrations = "2.1.0"
|
||||
diesel_logger = { version = "0.3.0", optional = true }
|
||||
|
||||
# Bundled/Static SQLite
|
||||
libsqlite3-sys = { version = "0.27.0", features = ["bundled"], optional = true }
|
||||
libsqlite3-sys = { version = "0.28.0", features = ["bundled"], optional = true }
|
||||
|
||||
# Crypto-related libraries
|
||||
rand = { version = "0.8.5", features = ["small_rng"] }
|
||||
ring = "0.17.7"
|
||||
ring = "0.17.8"
|
||||
|
||||
# UUID generation
|
||||
uuid = { version = "1.7.0", features = ["v4"] }
|
||||
uuid = { version = "1.8.0", features = ["v4"] }
|
||||
|
||||
# Date and time libraries
|
||||
chrono = { version = "0.4.33", features = ["clock", "serde"], default-features = false }
|
||||
chrono-tz = "0.8.5"
|
||||
time = "0.3.31"
|
||||
chrono = { version = "0.4.38", features = ["clock", "serde"], default-features = false }
|
||||
chrono-tz = "0.9.0"
|
||||
time = "0.3.36"
|
||||
|
||||
# Job scheduler
|
||||
job_scheduler_ng = "2.0.4"
|
||||
job_scheduler_ng = "2.0.5"
|
||||
|
||||
# Data encoding library Hex/Base32/Base64
|
||||
data-encoding = "2.5.0"
|
||||
|
||||
# JWT library
|
||||
jsonwebtoken = "9.2.0"
|
||||
jsonwebtoken = "9.3.0"
|
||||
|
||||
# TOTP library
|
||||
totp-lite = "2.0.1"
|
||||
|
@ -116,46 +115,47 @@ webauthn-rs = "0.3.2"
|
|||
url = "2.5.0"
|
||||
|
||||
# Email libraries
|
||||
lettre = { version = "0.11.3", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "tokio1-native-tls", "hostname", "tracing", "tokio1"], default-features = false }
|
||||
lettre = { version = "0.11.7", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "tokio1-native-tls", "hostname", "tracing", "tokio1"], default-features = false }
|
||||
percent-encoding = "2.3.1" # URL encoding library used for URL's in the emails
|
||||
email_address = "0.2.4"
|
||||
|
||||
# HTML Template library
|
||||
handlebars = { version = "5.1.1", features = ["dir_source"] }
|
||||
handlebars = { version = "5.1.2", features = ["dir_source"] }
|
||||
|
||||
# HTTP client (Used for favicons, version check, DUO and HIBP API)
|
||||
reqwest = { version = "0.11.23", features = ["stream", "json", "gzip", "brotli", "socks", "cookies", "trust-dns", "native-tls-alpn"] }
|
||||
reqwest = { version = "0.12.4", features = ["native-tls-alpn", "stream", "json", "gzip", "brotli", "socks", "cookies"] }
|
||||
hickory-resolver = "0.24.1"
|
||||
|
||||
# Favicon extraction libraries
|
||||
html5gum = "0.5.7"
|
||||
regex = { version = "1.10.3", features = ["std", "perf", "unicode-perl"], default-features = false }
|
||||
regex = { version = "1.10.4", features = ["std", "perf", "unicode-perl"], default-features = false }
|
||||
data-url = "0.3.1"
|
||||
bytes = "1.5.0"
|
||||
bytes = "1.6.0"
|
||||
|
||||
# Cache function results (Used for version check and favicon fetching)
|
||||
cached = { version = "0.48.1", features = ["async"] }
|
||||
cached = { version = "0.50.0", features = ["async"] }
|
||||
|
||||
# Used for custom short lived cookie jar during favicon extraction
|
||||
cookie = "0.16.2"
|
||||
cookie_store = "0.19.1"
|
||||
cookie = "0.18.1"
|
||||
cookie_store = "0.21.0"
|
||||
|
||||
# Used by U2F, JWT and PostgreSQL
|
||||
openssl = "0.10.63"
|
||||
openssl = "0.10.64"
|
||||
|
||||
# CLI argument parsing
|
||||
pico-args = "0.5.0"
|
||||
|
||||
# Macro ident concatenation
|
||||
paste = "1.0.14"
|
||||
governor = "0.6.0"
|
||||
governor = "0.6.3"
|
||||
|
||||
# Check client versions for specific features.
|
||||
semver = "1.0.21"
|
||||
semver = "1.0.22"
|
||||
|
||||
# Allow overriding the default memory allocator
|
||||
# Mainly used for the musl builds, since the default musl malloc is very slow
|
||||
mimalloc = { version = "0.1.39", features = ["secure"], default-features = false, optional = true }
|
||||
which = "6.0.0"
|
||||
mimalloc = { version = "0.1.41", features = ["secure"], default-features = false, optional = true }
|
||||
which = "6.0.1"
|
||||
|
||||
# Argon2 library with support for the PHC format
|
||||
argon2 = "0.5.3"
|
||||
|
@ -190,3 +190,60 @@ strip = "symbols"
|
|||
lto = "fat"
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
|
||||
# Profile for systems with low resources
|
||||
# It will use less resources during build
|
||||
[profile.release-low]
|
||||
inherits = "release"
|
||||
strip = "symbols"
|
||||
lto = "thin"
|
||||
codegen-units = 16
|
||||
|
||||
# Linting config
|
||||
[lints.rust]
|
||||
# Forbid
|
||||
unsafe_code = "forbid"
|
||||
non_ascii_idents = "forbid"
|
||||
|
||||
# Deny
|
||||
future_incompatible = { level = "deny", priority = -1 }
|
||||
noop_method_call = "deny"
|
||||
pointer_structural_match = "deny"
|
||||
rust_2018_idioms = { level = "deny", priority = -1 }
|
||||
rust_2021_compatibility = { level = "deny", priority = -1 }
|
||||
trivial_casts = "deny"
|
||||
trivial_numeric_casts = "deny"
|
||||
unused = { level = "deny", priority = -1 }
|
||||
unused_import_braces = "deny"
|
||||
unused_lifetimes = "deny"
|
||||
deprecated_in_future = "deny"
|
||||
|
||||
[lints.clippy]
|
||||
# Allow
|
||||
# We need this since Rust v1.76+, since it has some bugs
|
||||
# https://github.com/rust-lang/rust-clippy/issues/12016
|
||||
blocks_in_conditions = "allow"
|
||||
|
||||
# Deny
|
||||
cast_lossless = "deny"
|
||||
clone_on_ref_ptr = "deny"
|
||||
equatable_if_let = "deny"
|
||||
float_cmp_const = "deny"
|
||||
inefficient_to_string = "deny"
|
||||
iter_on_empty_collections = "deny"
|
||||
iter_on_single_items = "deny"
|
||||
linkedlist = "deny"
|
||||
macro_use_imports = "deny"
|
||||
manual_assert = "deny"
|
||||
manual_instant_elapsed = "deny"
|
||||
manual_string_new = "deny"
|
||||
match_wildcard_for_single_variants = "deny"
|
||||
mem_forget = "deny"
|
||||
needless_lifetimes = "deny"
|
||||
string_add_assign = "deny"
|
||||
string_to_string = "deny"
|
||||
unnecessary_join = "deny"
|
||||
unnecessary_self_imports = "deny"
|
||||
unused_async = "deny"
|
||||
verbose_file_reads = "deny"
|
||||
zero_sized_map_values = "deny"
|
||||
|
|
10
build.rs
10
build.rs
|
@ -49,11 +49,11 @@ fn run(args: &[&str]) -> Result<String, std::io::Error> {
|
|||
|
||||
/// This method reads info from Git, namely tags, branch, and revision
|
||||
/// To access these values, use:
|
||||
/// - env!("GIT_EXACT_TAG")
|
||||
/// - env!("GIT_LAST_TAG")
|
||||
/// - env!("GIT_BRANCH")
|
||||
/// - env!("GIT_REV")
|
||||
/// - env!("VW_VERSION")
|
||||
/// - `env!("GIT_EXACT_TAG")`
|
||||
/// - `env!("GIT_LAST_TAG")`
|
||||
/// - `env!("GIT_BRANCH")`
|
||||
/// - `env!("GIT_REV")`
|
||||
/// - `env!("VW_VERSION")`
|
||||
fn version_from_git_info() -> Result<String, std::io::Error> {
|
||||
// The exact tag for the current commit, can be empty when
|
||||
// the current commit doesn't have an associated tag
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
---
|
||||
vault_version: "v2024.1.2"
|
||||
vault_image_digest: "sha256:ac07a71cbcd199e3c9a0639c04234ba2f1ba16cfa2a45b08a7ae27eb82f8e13b"
|
||||
# Cross Compile Docker Helper Scripts v1.3.0
|
||||
vault_version: "v2024.3.1"
|
||||
vault_image_digest: "sha256:689b1e706f29e1858a5c7e0ec82e40fac793322e5e0ac9102ab09c2620207cd5"
|
||||
# Cross Compile Docker Helper Scripts v1.4.0
|
||||
# We use the linux/amd64 platform shell scripts since there is no difference between the different platform scripts
|
||||
xx_image_digest: "sha256:c9609ace652bbe51dd4ce90e0af9d48a4590f1214246da5bc70e46f6dd586edc"
|
||||
rust_version: 1.75.0 # Rust version to be used
|
||||
xx_image_digest: "sha256:0cd3f05c72d6c9b038eb135f91376ee1169ef3a330d34e418e65e2a5c2e9c0d4"
|
||||
rust_version: 1.77.2 # Rust version to be used
|
||||
debian_version: bookworm # Debian release name to be used
|
||||
alpine_version: 3.19 # Alpine version to be used
|
||||
# For which platforms/architectures will we try to build images
|
||||
|
|
|
@ -18,23 +18,23 @@
|
|||
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
||||
# click the tag name to view the digest of the image it currently points to.
|
||||
# - From the command line:
|
||||
# $ docker pull docker.io/vaultwarden/web-vault:v2024.1.2
|
||||
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2024.1.2
|
||||
# [docker.io/vaultwarden/web-vault@sha256:ac07a71cbcd199e3c9a0639c04234ba2f1ba16cfa2a45b08a7ae27eb82f8e13b]
|
||||
# $ docker pull docker.io/vaultwarden/web-vault:v2024.3.1
|
||||
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2024.3.1
|
||||
# [docker.io/vaultwarden/web-vault@sha256:689b1e706f29e1858a5c7e0ec82e40fac793322e5e0ac9102ab09c2620207cd5]
|
||||
#
|
||||
# - Conversely, to get the tag name from the digest:
|
||||
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:ac07a71cbcd199e3c9a0639c04234ba2f1ba16cfa2a45b08a7ae27eb82f8e13b
|
||||
# [docker.io/vaultwarden/web-vault:v2024.1.2]
|
||||
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:689b1e706f29e1858a5c7e0ec82e40fac793322e5e0ac9102ab09c2620207cd5
|
||||
# [docker.io/vaultwarden/web-vault:v2024.3.1]
|
||||
#
|
||||
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:ac07a71cbcd199e3c9a0639c04234ba2f1ba16cfa2a45b08a7ae27eb82f8e13b as vault
|
||||
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:689b1e706f29e1858a5c7e0ec82e40fac793322e5e0ac9102ab09c2620207cd5 as vault
|
||||
|
||||
########################## ALPINE BUILD IMAGES ##########################
|
||||
## NOTE: The Alpine Base Images do not support other platforms then linux/amd64
|
||||
## And for Alpine we define all build images here, they will only be loaded when actually used
|
||||
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.75.0 as build_amd64
|
||||
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.75.0 as build_arm64
|
||||
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.75.0 as build_armv7
|
||||
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.75.0 as build_armv6
|
||||
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.77.2 as build_amd64
|
||||
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.77.2 as build_arm64
|
||||
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.77.2 as build_armv7
|
||||
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.77.2 as build_armv6
|
||||
|
||||
########################## BUILD IMAGE ##########################
|
||||
# hadolint ignore=DL3006
|
||||
|
@ -65,13 +65,14 @@ RUN mkdir -pv "${CARGO_HOME}" \
|
|||
RUN USER=root cargo new --bin /app
|
||||
WORKDIR /app
|
||||
|
||||
# Shared variables across Debian and Alpine
|
||||
# Environment variables for Cargo on Alpine based builds
|
||||
RUN echo "export CARGO_TARGET=${RUST_MUSL_CROSS_TARGET}" >> /env-cargo && \
|
||||
# To be able to build the armv6 image with mimalloc we need to tell the linker to also look for libatomic
|
||||
if [[ "${TARGETARCH}${TARGETVARIANT}" == "armv6" ]] ; then echo "export RUSTFLAGS='-Clink-arg=-latomic'" >> /env-cargo ; fi && \
|
||||
# Output the current contents of the file
|
||||
cat /env-cargo
|
||||
|
||||
# Configure the DB ARG as late as possible to not invalidate the cached layers above
|
||||
# Enable MiMalloc to improve performance on Alpine builds
|
||||
ARG DB=sqlite,mysql,postgresql,enable_mimalloc
|
||||
|
||||
|
|
|
@ -18,24 +18,24 @@
|
|||
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
||||
# click the tag name to view the digest of the image it currently points to.
|
||||
# - From the command line:
|
||||
# $ docker pull docker.io/vaultwarden/web-vault:v2024.1.2
|
||||
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2024.1.2
|
||||
# [docker.io/vaultwarden/web-vault@sha256:ac07a71cbcd199e3c9a0639c04234ba2f1ba16cfa2a45b08a7ae27eb82f8e13b]
|
||||
# $ docker pull docker.io/vaultwarden/web-vault:v2024.3.1
|
||||
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2024.3.1
|
||||
# [docker.io/vaultwarden/web-vault@sha256:689b1e706f29e1858a5c7e0ec82e40fac793322e5e0ac9102ab09c2620207cd5]
|
||||
#
|
||||
# - Conversely, to get the tag name from the digest:
|
||||
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:ac07a71cbcd199e3c9a0639c04234ba2f1ba16cfa2a45b08a7ae27eb82f8e13b
|
||||
# [docker.io/vaultwarden/web-vault:v2024.1.2]
|
||||
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:689b1e706f29e1858a5c7e0ec82e40fac793322e5e0ac9102ab09c2620207cd5
|
||||
# [docker.io/vaultwarden/web-vault:v2024.3.1]
|
||||
#
|
||||
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:ac07a71cbcd199e3c9a0639c04234ba2f1ba16cfa2a45b08a7ae27eb82f8e13b as vault
|
||||
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:689b1e706f29e1858a5c7e0ec82e40fac793322e5e0ac9102ab09c2620207cd5 as vault
|
||||
|
||||
########################## Cross Compile Docker Helper Scripts ##########################
|
||||
## We use the linux/amd64 no matter which Build Platform, since these are all bash scripts
|
||||
## And these bash scripts do not have any significant difference if at all
|
||||
FROM --platform=linux/amd64 docker.io/tonistiigi/xx@sha256:c9609ace652bbe51dd4ce90e0af9d48a4590f1214246da5bc70e46f6dd586edc AS xx
|
||||
FROM --platform=linux/amd64 docker.io/tonistiigi/xx@sha256:0cd3f05c72d6c9b038eb135f91376ee1169ef3a330d34e418e65e2a5c2e9c0d4 AS xx
|
||||
|
||||
########################## BUILD IMAGE ##########################
|
||||
# hadolint ignore=DL3006
|
||||
FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.75.0-slim-bookworm as build
|
||||
FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.77.2-slim-bookworm as build
|
||||
COPY --from=xx / /
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
|
@ -88,9 +88,17 @@ RUN mkdir -pv "${CARGO_HOME}" \
|
|||
RUN USER=root cargo new --bin /app
|
||||
WORKDIR /app
|
||||
|
||||
# Environment variables for cargo across Debian and Alpine
|
||||
# Environment variables for Cargo on Debian based builds
|
||||
ARG ARCH_OPENSSL_LIB_DIR \
|
||||
ARCH_OPENSSL_INCLUDE_DIR
|
||||
|
||||
RUN source /env-cargo && \
|
||||
if xx-info is-cross ; then \
|
||||
# Some special variables if needed to override some build paths
|
||||
if [[ -n "${ARCH_OPENSSL_LIB_DIR}" && -n "${ARCH_OPENSSL_INCLUDE_DIR}" ]]; then \
|
||||
echo "export $(echo "${CARGO_TARGET}" | tr '[:lower:]' '[:upper:]' | tr - _)_OPENSSL_LIB_DIR=${ARCH_OPENSSL_LIB_DIR}" >> /env-cargo && \
|
||||
echo "export $(echo "${CARGO_TARGET}" | tr '[:lower:]' '[:upper:]' | tr - _)_OPENSSL_INCLUDE_DIR=${ARCH_OPENSSL_INCLUDE_DIR}" >> /env-cargo ; \
|
||||
fi && \
|
||||
# We can't use xx-cargo since that uses clang, which doesn't work for our libraries.
|
||||
# Because of this we generate the needed environment variables here which we can load in the needed steps.
|
||||
echo "export CC_$(echo "${CARGO_TARGET}" | tr '[:upper:]' '[:lower:]' | tr - _)=/usr/bin/$(xx-info)-gcc" >> /env-cargo && \
|
||||
|
|
|
@ -108,9 +108,17 @@ RUN USER=root cargo new --bin /app
|
|||
WORKDIR /app
|
||||
|
||||
{% if base == "debian" %}
|
||||
# Environment variables for cargo across Debian and Alpine
|
||||
# Environment variables for Cargo on Debian based builds
|
||||
ARG ARCH_OPENSSL_LIB_DIR \
|
||||
ARCH_OPENSSL_INCLUDE_DIR
|
||||
|
||||
RUN source /env-cargo && \
|
||||
if xx-info is-cross ; then \
|
||||
# Some special variables if needed to override some build paths
|
||||
if [[ -n "${ARCH_OPENSSL_LIB_DIR}" && -n "${ARCH_OPENSSL_INCLUDE_DIR}" ]]; then \
|
||||
echo "export $(echo "${CARGO_TARGET}" | tr '[:lower:]' '[:upper:]' | tr - _)_OPENSSL_LIB_DIR=${ARCH_OPENSSL_LIB_DIR}" >> /env-cargo && \
|
||||
echo "export $(echo "${CARGO_TARGET}" | tr '[:lower:]' '[:upper:]' | tr - _)_OPENSSL_INCLUDE_DIR=${ARCH_OPENSSL_INCLUDE_DIR}" >> /env-cargo ; \
|
||||
fi && \
|
||||
# We can't use xx-cargo since that uses clang, which doesn't work for our libraries.
|
||||
# Because of this we generate the needed environment variables here which we can load in the needed steps.
|
||||
echo "export CC_$(echo "${CARGO_TARGET}" | tr '[:upper:]' '[:lower:]' | tr - _)=/usr/bin/$(xx-info)-gcc" >> /env-cargo && \
|
||||
|
@ -126,13 +134,14 @@ RUN source /env-cargo && \
|
|||
# Configure the DB ARG as late as possible to not invalidate the cached layers above
|
||||
ARG DB=sqlite,mysql,postgresql
|
||||
{% elif base == "alpine" %}
|
||||
# Shared variables across Debian and Alpine
|
||||
# Environment variables for Cargo on Alpine based builds
|
||||
RUN echo "export CARGO_TARGET=${RUST_MUSL_CROSS_TARGET}" >> /env-cargo && \
|
||||
# To be able to build the armv6 image with mimalloc we need to tell the linker to also look for libatomic
|
||||
if [[ "${TARGETARCH}${TARGETVARIANT}" == "armv6" ]] ; then echo "export RUSTFLAGS='-Clink-arg=-latomic'" >> /env-cargo ; fi && \
|
||||
# Output the current contents of the file
|
||||
cat /env-cargo
|
||||
|
||||
# Configure the DB ARG as late as possible to not invalidate the cached layers above
|
||||
# Enable MiMalloc to improve performance on Alpine builds
|
||||
ARG DB=sqlite,mysql,postgresql,enable_mimalloc
|
||||
{% endif %}
|
||||
|
|
|
@ -11,6 +11,11 @@ With just these two files we can build both Debian and Alpine images for the fol
|
|||
- armv7 (linux/arm/v7)
|
||||
- armv6 (linux/arm/v6)
|
||||
|
||||
Some unsupported platforms for Debian based images. These are not built and tested by default and are only provided to make it easier for users to build for these architectures.
|
||||
- 386 (linux/386)
|
||||
- ppc64le (linux/ppc64le)
|
||||
- s390x (linux/s390x)
|
||||
|
||||
To build these containers you need to enable QEMU binfmt support to be able to run/emulate architectures which are different then your host.<br>
|
||||
This ensures the container build process can run binaries from other architectures.<br>
|
||||
|
||||
|
|
|
@ -125,6 +125,40 @@ target "debian-armv6" {
|
|||
tags = generate_tags("", "-armv6")
|
||||
}
|
||||
|
||||
// ==== Start of unsupported Debian architecture targets ===
|
||||
// These are provided just to help users build for these rare platforms
|
||||
// They will not be built by default
|
||||
target "debian-386" {
|
||||
inherits = ["debian"]
|
||||
platforms = ["linux/386"]
|
||||
tags = generate_tags("", "-386")
|
||||
args = {
|
||||
ARCH_OPENSSL_LIB_DIR = "/usr/lib/i386-linux-gnu"
|
||||
ARCH_OPENSSL_INCLUDE_DIR = "/usr/include/i386-linux-gnu"
|
||||
}
|
||||
}
|
||||
|
||||
target "debian-ppc64le" {
|
||||
inherits = ["debian"]
|
||||
platforms = ["linux/ppc64le"]
|
||||
tags = generate_tags("", "-ppc64le")
|
||||
args = {
|
||||
ARCH_OPENSSL_LIB_DIR = "/usr/lib/powerpc64le-linux-gnu"
|
||||
ARCH_OPENSSL_INCLUDE_DIR = "/usr/include/powerpc64le-linux-gnu"
|
||||
}
|
||||
}
|
||||
|
||||
target "debian-s390x" {
|
||||
inherits = ["debian"]
|
||||
platforms = ["linux/s390x"]
|
||||
tags = generate_tags("", "-s390x")
|
||||
args = {
|
||||
ARCH_OPENSSL_LIB_DIR = "/usr/lib/s390x-linux-gnu"
|
||||
ARCH_OPENSSL_INCLUDE_DIR = "/usr/include/s390x-linux-gnu"
|
||||
}
|
||||
}
|
||||
// ==== End of unsupported Debian architecture targets ===
|
||||
|
||||
// A Group to build all platforms individually for local testing
|
||||
group "debian-all" {
|
||||
targets = ["debian-amd64", "debian-arm64", "debian-armv7", "debian-armv6"]
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE twofactor MODIFY last_used BIGINT NOT NULL;
|
|
@ -0,0 +1,3 @@
|
|||
ALTER TABLE twofactor
|
||||
ALTER COLUMN last_used TYPE BIGINT,
|
||||
ALTER COLUMN last_used SET NOT NULL;
|
|
@ -0,0 +1 @@
|
|||
-- Integer size in SQLite is already i64, so we don't need to do anything
|
|
@ -1,4 +1,4 @@
|
|||
[toolchain]
|
||||
channel = "1.75.0"
|
||||
channel = "1.77.2"
|
||||
components = [ "rustfmt", "clippy" ]
|
||||
profile = "minimal"
|
||||
|
|
|
@ -23,8 +23,8 @@ use crate::{
|
|||
error::{Error, MapResult},
|
||||
mail,
|
||||
util::{
|
||||
docker_base_image, format_naive_datetime_local, get_display_size, get_reqwest_client, is_running_in_docker,
|
||||
NumberOrString,
|
||||
container_base_image, format_naive_datetime_local, get_display_size, get_reqwest_client,
|
||||
is_running_in_container, NumberOrString,
|
||||
},
|
||||
CONFIG, VERSION,
|
||||
};
|
||||
|
@ -510,7 +510,11 @@ async fn update_user_org_type(data: Json<UserOrgTypeData>, token: AdminToken, mu
|
|||
match OrgPolicy::is_user_allowed(&user_to_edit.user_uuid, &user_to_edit.org_uuid, true, &mut conn).await {
|
||||
Ok(_) => {}
|
||||
Err(OrgPolicyErr::TwoFactorMissing) => {
|
||||
err!("You cannot modify this user to this type because it has no two-step login method activated");
|
||||
if CONFIG.email_2fa_auto_fallback() {
|
||||
two_factor::email::find_and_activate_email_2fa(&user_to_edit.user_uuid, &mut conn).await?;
|
||||
} else {
|
||||
err!("You cannot modify this user to this type because they have not setup 2FA");
|
||||
}
|
||||
}
|
||||
Err(OrgPolicyErr::SingleOrgEnforced) => {
|
||||
err!("You cannot modify this user to this type because it is a member of an organization which forbids it");
|
||||
|
@ -608,7 +612,7 @@ use cached::proc_macro::cached;
|
|||
/// Cache this function to prevent API call rate limit. Github only allows 60 requests per hour, and we use 3 here already.
|
||||
/// It will cache this function for 300 seconds (5 minutes) which should prevent the exhaustion of the rate limit.
|
||||
#[cached(time = 300, sync_writes = true)]
|
||||
async fn get_release_info(has_http_access: bool, running_within_docker: bool) -> (String, String, String) {
|
||||
async fn get_release_info(has_http_access: bool, running_within_container: bool) -> (String, String, String) {
|
||||
// If the HTTP Check failed, do not even attempt to check for new versions since we were not able to connect with github.com anyway.
|
||||
if has_http_access {
|
||||
(
|
||||
|
@ -625,9 +629,9 @@ async fn get_release_info(has_http_access: bool, running_within_docker: bool) ->
|
|||
}
|
||||
_ => "-".to_string(),
|
||||
},
|
||||
// Do not fetch the web-vault version when running within Docker.
|
||||
// Do not fetch the web-vault version when running within a container.
|
||||
// The web-vault version is embedded within the container it self, and should not be updated manually
|
||||
if running_within_docker {
|
||||
if running_within_container {
|
||||
"-".to_string()
|
||||
} else {
|
||||
match get_json_api::<GitRelease>(
|
||||
|
@ -681,7 +685,7 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn)
|
|||
};
|
||||
|
||||
// Execute some environment checks
|
||||
let running_within_docker = is_running_in_docker();
|
||||
let running_within_container = is_running_in_container();
|
||||
let has_http_access = has_http_access().await;
|
||||
let uses_proxy = env::var_os("HTTP_PROXY").is_some()
|
||||
|| env::var_os("http_proxy").is_some()
|
||||
|
@ -695,12 +699,9 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn)
|
|||
};
|
||||
|
||||
let (latest_release, latest_commit, latest_web_build) =
|
||||
get_release_info(has_http_access, running_within_docker).await;
|
||||
get_release_info(has_http_access, running_within_container).await;
|
||||
|
||||
let ip_header_name = match &ip_header.0 {
|
||||
Some(h) => h,
|
||||
_ => "",
|
||||
};
|
||||
let ip_header_name = &ip_header.0.unwrap_or_default();
|
||||
|
||||
let diagnostics_json = json!({
|
||||
"dns_resolved": dns_resolved,
|
||||
|
@ -710,11 +711,11 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn)
|
|||
"web_vault_enabled": &CONFIG.web_vault_enabled(),
|
||||
"web_vault_version": web_vault_version.version.trim_start_matches('v'),
|
||||
"latest_web_build": latest_web_build,
|
||||
"running_within_docker": running_within_docker,
|
||||
"docker_base_image": if running_within_docker { docker_base_image() } else { "Not applicable" },
|
||||
"running_within_container": running_within_container,
|
||||
"container_base_image": if running_within_container { container_base_image() } else { "Not applicable" },
|
||||
"has_http_access": has_http_access,
|
||||
"ip_header_exists": &ip_header.0.is_some(),
|
||||
"ip_header_match": ip_header_name == CONFIG.ip_header(),
|
||||
"ip_header_exists": !ip_header_name.is_empty(),
|
||||
"ip_header_match": ip_header_name.eq(&CONFIG.ip_header()),
|
||||
"ip_header_name": ip_header_name,
|
||||
"ip_header_config": &CONFIG.ip_header(),
|
||||
"uses_proxy": uses_proxy,
|
||||
|
|
|
@ -5,8 +5,9 @@ use serde_json::Value;
|
|||
|
||||
use crate::{
|
||||
api::{
|
||||
core::log_user_event, register_push_device, unregister_push_device, AnonymousNotify, EmptyResult, JsonResult,
|
||||
JsonUpcase, Notify, PasswordOrOtpData, UpdateType,
|
||||
core::{log_user_event, two_factor::email},
|
||||
register_push_device, unregister_push_device, AnonymousNotify, EmptyResult, JsonResult, JsonUpcase, Notify,
|
||||
PasswordOrOtpData, UpdateType,
|
||||
},
|
||||
auth::{decode_delete, decode_invite, decode_verify_email, ClientHeaders, Headers},
|
||||
crypto,
|
||||
|
@ -104,6 +105,19 @@ fn enforce_password_hint_setting(password_hint: &Option<String>) -> EmptyResult
|
|||
}
|
||||
Ok(())
|
||||
}
|
||||
async fn is_email_2fa_required(org_user_uuid: Option<String>, conn: &mut DbConn) -> bool {
|
||||
if !CONFIG._enable_email_2fa() {
|
||||
return false;
|
||||
}
|
||||
if CONFIG.email_2fa_enforce_on_verified_invite() {
|
||||
return true;
|
||||
}
|
||||
if org_user_uuid.is_some() {
|
||||
return OrgPolicy::is_enabled_by_org(&org_user_uuid.unwrap(), OrgPolicyType::TwoFactorAuthentication, conn)
|
||||
.await;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
#[post("/accounts/register", data = "<data>")]
|
||||
async fn register(data: JsonUpcase<RegisterData>, conn: DbConn) -> JsonResult {
|
||||
|
@ -152,7 +166,8 @@ pub async fn _register(data: JsonUpcase<RegisterData>, mut conn: DbConn) -> Json
|
|||
}
|
||||
user
|
||||
} else if CONFIG.is_signup_allowed(&email)
|
||||
|| EmergencyAccess::find_invited_by_grantee_email(&email, &mut conn).await.is_some()
|
||||
|| (CONFIG.emergency_access_allowed()
|
||||
&& EmergencyAccess::find_invited_by_grantee_email(&email, &mut conn).await.is_some())
|
||||
{
|
||||
user
|
||||
} else {
|
||||
|
@ -203,14 +218,25 @@ pub async fn _register(data: JsonUpcase<RegisterData>, mut conn: DbConn) -> Json
|
|||
if let Err(e) = mail::send_welcome_must_verify(&user.email, &user.uuid).await {
|
||||
error!("Error sending welcome email: {:#?}", e);
|
||||
}
|
||||
|
||||
user.last_verifying_at = Some(user.created_at);
|
||||
} else if let Err(e) = mail::send_welcome(&user.email).await {
|
||||
error!("Error sending welcome email: {:#?}", e);
|
||||
}
|
||||
|
||||
if verified_by_invite && is_email_2fa_required(data.OrganizationUserId, &mut conn).await {
|
||||
let _ = email::activate_email_2fa(&user, &mut conn).await;
|
||||
}
|
||||
}
|
||||
|
||||
user.save(&mut conn).await?;
|
||||
|
||||
// accept any open emergency access invitations
|
||||
if !CONFIG.mail_enabled() && CONFIG.emergency_access_allowed() {
|
||||
for mut emergency_invite in EmergencyAccess::find_all_invited_by_grantee_email(&user.email, &mut conn).await {
|
||||
let _ = emergency_invite.accept_invite(&user.uuid, &user.email, &mut conn).await;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Json(json!({
|
||||
"Object": "register",
|
||||
"CaptchaBypassToken": "",
|
||||
|
@ -420,24 +446,46 @@ async fn post_kdf(data: JsonUpcase<ChangeKdfData>, headers: Headers, mut conn: D
|
|||
#[derive(Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
struct UpdateFolderData {
|
||||
Id: String,
|
||||
// There is a bug in 2024.3.x which adds a `null` item.
|
||||
// To bypass this we allow a Option here, but skip it during the updates
|
||||
// See: https://github.com/bitwarden/clients/issues/8453
|
||||
Id: Option<String>,
|
||||
Name: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
struct UpdateEmergencyAccessData {
|
||||
Id: String,
|
||||
KeyEncrypted: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
struct UpdateResetPasswordData {
|
||||
OrganizationId: String,
|
||||
ResetPasswordKey: String,
|
||||
}
|
||||
|
||||
use super::ciphers::CipherData;
|
||||
use super::sends::{update_send_from_data, SendData};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
struct KeyData {
|
||||
Ciphers: Vec<CipherData>,
|
||||
Folders: Vec<UpdateFolderData>,
|
||||
Sends: Vec<SendData>,
|
||||
EmergencyAccessKeys: Vec<UpdateEmergencyAccessData>,
|
||||
ResetPasswordKeys: Vec<UpdateResetPasswordData>,
|
||||
Key: String,
|
||||
PrivateKey: String,
|
||||
MasterPasswordHash: String,
|
||||
PrivateKey: String,
|
||||
}
|
||||
|
||||
#[post("/accounts/key", data = "<data>")]
|
||||
async fn post_rotatekey(data: JsonUpcase<KeyData>, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
||||
// TODO: See if we can wrap everything within a SQL Transaction. If something fails it should revert everything.
|
||||
let data: KeyData = data.into_inner().data;
|
||||
|
||||
if !headers.user.check_valid_password(&data.MasterPasswordHash) {
|
||||
|
@ -454,37 +502,83 @@ async fn post_rotatekey(data: JsonUpcase<KeyData>, headers: Headers, mut conn: D
|
|||
|
||||
// Update folder data
|
||||
for folder_data in data.Folders {
|
||||
let mut saved_folder = match Folder::find_by_uuid(&folder_data.Id, &mut conn).await {
|
||||
Some(folder) => folder,
|
||||
None => err!("Folder doesn't exist"),
|
||||
// Skip `null` folder id entries.
|
||||
// See: https://github.com/bitwarden/clients/issues/8453
|
||||
if let Some(folder_id) = folder_data.Id {
|
||||
let mut saved_folder = match Folder::find_by_uuid(&folder_id, &mut conn).await {
|
||||
Some(folder) => folder,
|
||||
None => err!("Folder doesn't exist"),
|
||||
};
|
||||
|
||||
if &saved_folder.user_uuid != user_uuid {
|
||||
err!("The folder is not owned by the user")
|
||||
}
|
||||
|
||||
saved_folder.name = folder_data.Name;
|
||||
saved_folder.save(&mut conn).await?
|
||||
}
|
||||
}
|
||||
|
||||
// Update emergency access data
|
||||
for emergency_access_data in data.EmergencyAccessKeys {
|
||||
let mut saved_emergency_access = match EmergencyAccess::find_by_uuid(&emergency_access_data.Id, &mut conn).await
|
||||
{
|
||||
Some(emergency_access) => emergency_access,
|
||||
None => err!("Emergency access doesn't exist"),
|
||||
};
|
||||
|
||||
if &saved_folder.user_uuid != user_uuid {
|
||||
err!("The folder is not owned by the user")
|
||||
if &saved_emergency_access.grantor_uuid != user_uuid {
|
||||
err!("The emergency access is not owned by the user")
|
||||
}
|
||||
|
||||
saved_folder.name = folder_data.Name;
|
||||
saved_folder.save(&mut conn).await?
|
||||
saved_emergency_access.key_encrypted = Some(emergency_access_data.KeyEncrypted);
|
||||
saved_emergency_access.save(&mut conn).await?
|
||||
}
|
||||
|
||||
// Update reset password data
|
||||
for reset_password_data in data.ResetPasswordKeys {
|
||||
let mut user_org =
|
||||
match UserOrganization::find_by_user_and_org(user_uuid, &reset_password_data.OrganizationId, &mut conn)
|
||||
.await
|
||||
{
|
||||
Some(reset_password) => reset_password,
|
||||
None => err!("Reset password doesn't exist"),
|
||||
};
|
||||
|
||||
user_org.reset_password_key = Some(reset_password_data.ResetPasswordKey);
|
||||
user_org.save(&mut conn).await?
|
||||
}
|
||||
|
||||
// Update send data
|
||||
for send_data in data.Sends {
|
||||
let mut send = match Send::find_by_uuid(send_data.Id.as_ref().unwrap(), &mut conn).await {
|
||||
Some(send) => send,
|
||||
None => err!("Send doesn't exist"),
|
||||
};
|
||||
|
||||
update_send_from_data(&mut send, send_data, &headers, &mut conn, &nt, UpdateType::None).await?;
|
||||
}
|
||||
|
||||
// Update cipher data
|
||||
use super::ciphers::update_cipher_from_data;
|
||||
|
||||
for cipher_data in data.Ciphers {
|
||||
let mut saved_cipher = match Cipher::find_by_uuid(cipher_data.Id.as_ref().unwrap(), &mut conn).await {
|
||||
Some(cipher) => cipher,
|
||||
None => err!("Cipher doesn't exist"),
|
||||
};
|
||||
if cipher_data.OrganizationId.is_none() {
|
||||
let mut saved_cipher = match Cipher::find_by_uuid(cipher_data.Id.as_ref().unwrap(), &mut conn).await {
|
||||
Some(cipher) => cipher,
|
||||
None => err!("Cipher doesn't exist"),
|
||||
};
|
||||
|
||||
if saved_cipher.user_uuid.as_ref().unwrap() != user_uuid {
|
||||
err!("The cipher is not owned by the user")
|
||||
if saved_cipher.user_uuid.as_ref().unwrap() != user_uuid {
|
||||
err!("The cipher is not owned by the user")
|
||||
}
|
||||
|
||||
// Prevent triggering cipher updates via WebSockets by settings UpdateType::None
|
||||
// The user sessions are invalidated because all the ciphers were re-encrypted and thus triggering an update could cause issues.
|
||||
// We force the users to logout after the user has been saved to try and prevent these issues.
|
||||
update_cipher_from_data(&mut saved_cipher, cipher_data, &headers, None, &mut conn, &nt, UpdateType::None)
|
||||
.await?
|
||||
}
|
||||
|
||||
// Prevent triggering cipher updates via WebSockets by settings UpdateType::None
|
||||
// The user sessions are invalidated because all the ciphers were re-encrypted and thus triggering an update could cause issues.
|
||||
// We force the users to logout after the user has been saved to try and prevent these issues.
|
||||
update_cipher_from_data(&mut saved_cipher, cipher_data, &headers, false, &mut conn, &nt, UpdateType::None)
|
||||
.await?
|
||||
}
|
||||
|
||||
// Update user data
|
||||
|
@ -559,6 +653,8 @@ async fn post_email_token(data: JsonUpcase<EmailTokenData>, headers: Headers, mu
|
|||
if let Err(e) = mail::send_change_email(&data.NewEmail, &token).await {
|
||||
error!("Error sending change-email email: {:#?}", e);
|
||||
}
|
||||
} else {
|
||||
debug!("Email change request for user ({}) to email ({}) with token ({})", user.uuid, data.NewEmail, token);
|
||||
}
|
||||
|
||||
user.email_new = Some(data.NewEmail);
|
||||
|
@ -753,7 +849,7 @@ async fn delete_account(data: JsonUpcase<PasswordOrOtpData>, headers: Headers, m
|
|||
|
||||
#[get("/accounts/revision-date")]
|
||||
fn revision_date(headers: Headers) -> JsonResult {
|
||||
let revision_date = headers.user.updated_at.timestamp_millis();
|
||||
let revision_date = headers.user.updated_at.and_utc().timestamp_millis();
|
||||
Ok(Json(json!(revision_date)))
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ use rocket::{
|
|||
};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::util::NumberOrString;
|
||||
use crate::{
|
||||
api::{self, core::log_event, EmptyResult, JsonResult, JsonUpcase, Notify, PasswordOrOtpData, UpdateType},
|
||||
auth::Headers,
|
||||
|
@ -205,7 +206,7 @@ pub struct CipherData {
|
|||
// Folder id is not included in import
|
||||
FolderId: Option<String>,
|
||||
// TODO: Some of these might appear all the time, no need for Option
|
||||
OrganizationId: Option<String>,
|
||||
pub OrganizationId: Option<String>,
|
||||
|
||||
Key: Option<String>,
|
||||
|
||||
|
@ -321,7 +322,7 @@ async fn post_ciphers(data: JsonUpcase<CipherData>, headers: Headers, mut conn:
|
|||
data.LastKnownRevisionDate = None;
|
||||
|
||||
let mut cipher = Cipher::new(data.Type, data.Name.clone());
|
||||
update_cipher_from_data(&mut cipher, data, &headers, false, &mut conn, &nt, UpdateType::SyncCipherCreate).await?;
|
||||
update_cipher_from_data(&mut cipher, data, &headers, None, &mut conn, &nt, UpdateType::SyncCipherCreate).await?;
|
||||
|
||||
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await))
|
||||
}
|
||||
|
@ -352,7 +353,7 @@ pub async fn update_cipher_from_data(
|
|||
cipher: &mut Cipher,
|
||||
data: CipherData,
|
||||
headers: &Headers,
|
||||
shared_to_collection: bool,
|
||||
shared_to_collections: Option<Vec<String>>,
|
||||
conn: &mut DbConn,
|
||||
nt: &Notify<'_>,
|
||||
ut: UpdateType,
|
||||
|
@ -391,7 +392,7 @@ pub async fn update_cipher_from_data(
|
|||
match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, conn).await {
|
||||
None => err!("You don't have permission to add item to organization"),
|
||||
Some(org_user) => {
|
||||
if shared_to_collection
|
||||
if shared_to_collections.is_some()
|
||||
|| org_user.has_full_access()
|
||||
|| cipher.is_write_accessible_to_user(&headers.user.uuid, conn).await
|
||||
{
|
||||
|
@ -518,8 +519,15 @@ pub async fn update_cipher_from_data(
|
|||
)
|
||||
.await;
|
||||
}
|
||||
nt.send_cipher_update(ut, cipher, &cipher.update_users_revision(conn).await, &headers.device.uuid, None, conn)
|
||||
.await;
|
||||
nt.send_cipher_update(
|
||||
ut,
|
||||
cipher,
|
||||
&cipher.update_users_revision(conn).await,
|
||||
&headers.device.uuid,
|
||||
shared_to_collections,
|
||||
conn,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -580,7 +588,7 @@ async fn post_ciphers_import(
|
|||
cipher_data.FolderId = folder_uuid;
|
||||
|
||||
let mut cipher = Cipher::new(cipher_data.Type, cipher_data.Name.clone());
|
||||
update_cipher_from_data(&mut cipher, cipher_data, &headers, false, &mut conn, &nt, UpdateType::None).await?;
|
||||
update_cipher_from_data(&mut cipher, cipher_data, &headers, None, &mut conn, &nt, UpdateType::None).await?;
|
||||
}
|
||||
|
||||
let mut user = headers.user;
|
||||
|
@ -648,7 +656,7 @@ async fn put_cipher(
|
|||
err!("Cipher is not write accessible")
|
||||
}
|
||||
|
||||
update_cipher_from_data(&mut cipher, data, &headers, false, &mut conn, &nt, UpdateType::SyncCipherUpdate).await?;
|
||||
update_cipher_from_data(&mut cipher, data, &headers, None, &mut conn, &nt, UpdateType::SyncCipherUpdate).await?;
|
||||
|
||||
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await))
|
||||
}
|
||||
|
@ -898,7 +906,7 @@ async fn share_cipher_by_uuid(
|
|||
None => err!("Cipher doesn't exist"),
|
||||
};
|
||||
|
||||
let mut shared_to_collection = false;
|
||||
let mut shared_to_collections = vec![];
|
||||
|
||||
if let Some(organization_uuid) = &data.Cipher.OrganizationId {
|
||||
for uuid in &data.CollectionIds {
|
||||
|
@ -907,7 +915,7 @@ async fn share_cipher_by_uuid(
|
|||
Some(collection) => {
|
||||
if collection.is_writable_by_user(&headers.user.uuid, conn).await {
|
||||
CollectionCipher::save(&cipher.uuid, &collection.uuid, conn).await?;
|
||||
shared_to_collection = true;
|
||||
shared_to_collections.push(collection.uuid);
|
||||
} else {
|
||||
err!("No rights to modify the collection")
|
||||
}
|
||||
|
@ -923,7 +931,7 @@ async fn share_cipher_by_uuid(
|
|||
UpdateType::SyncCipherCreate
|
||||
};
|
||||
|
||||
update_cipher_from_data(&mut cipher, data.Cipher, headers, shared_to_collection, conn, nt, ut).await?;
|
||||
update_cipher_from_data(&mut cipher, data.Cipher, headers, Some(shared_to_collections), conn, nt, ut).await?;
|
||||
|
||||
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, conn).await))
|
||||
}
|
||||
|
@ -957,7 +965,7 @@ async fn get_attachment(uuid: &str, attachment_id: &str, headers: Headers, mut c
|
|||
struct AttachmentRequestData {
|
||||
Key: String,
|
||||
FileName: String,
|
||||
FileSize: i64,
|
||||
FileSize: NumberOrString,
|
||||
AdminRequest: Option<bool>, // true when attaching from an org vault view
|
||||
}
|
||||
|
||||
|
@ -987,12 +995,14 @@ async fn post_attachment_v2(
|
|||
}
|
||||
|
||||
let data: AttachmentRequestData = data.into_inner().data;
|
||||
if data.FileSize < 0 {
|
||||
let file_size = data.FileSize.into_i64()?;
|
||||
|
||||
if file_size < 0 {
|
||||
err!("Attachment size can't be negative")
|
||||
}
|
||||
let attachment_id = crypto::generate_attachment_id();
|
||||
let attachment =
|
||||
Attachment::new(attachment_id.clone(), cipher.uuid.clone(), data.FileName, data.FileSize, Some(data.Key));
|
||||
Attachment::new(attachment_id.clone(), cipher.uuid.clone(), data.FileName, file_size, Some(data.Key));
|
||||
attachment.save(&mut conn).await.expect("Error saving attachment");
|
||||
|
||||
let url = format!("/ciphers/{}/attachment/{}", cipher.uuid, attachment_id);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use chrono::{Duration, Utc};
|
||||
use chrono::{TimeDelta, Utc};
|
||||
use rocket::{serde::json::Json, Route};
|
||||
use serde_json::Value;
|
||||
|
||||
|
@ -61,7 +61,9 @@ async fn get_contacts(headers: Headers, mut conn: DbConn) -> Json<Value> {
|
|||
let emergency_access_list = EmergencyAccess::find_all_by_grantor_uuid(&headers.user.uuid, &mut conn).await;
|
||||
let mut emergency_access_list_json = Vec::with_capacity(emergency_access_list.len());
|
||||
for ea in emergency_access_list {
|
||||
emergency_access_list_json.push(ea.to_json_grantee_details(&mut conn).await);
|
||||
if let Some(grantee) = ea.to_json_grantee_details(&mut conn).await {
|
||||
emergency_access_list_json.push(grantee)
|
||||
}
|
||||
}
|
||||
|
||||
Json(json!({
|
||||
|
@ -95,7 +97,9 @@ async fn get_emergency_access(emer_id: &str, mut conn: DbConn) -> JsonResult {
|
|||
check_emergency_access_enabled()?;
|
||||
|
||||
match EmergencyAccess::find_by_uuid(emer_id, &mut conn).await {
|
||||
Some(emergency_access) => Ok(Json(emergency_access.to_json_grantee_details(&mut conn).await)),
|
||||
Some(emergency_access) => Ok(Json(
|
||||
emergency_access.to_json_grantee_details(&mut conn).await.expect("Grantee user should exist but does not!"),
|
||||
)),
|
||||
None => err!("Emergency access not valid."),
|
||||
}
|
||||
}
|
||||
|
@ -209,7 +213,7 @@ async fn send_invite(data: JsonUpcase<EmergencyAccessInviteData>, headers: Heade
|
|||
err!("You can not set yourself as an emergency contact.")
|
||||
}
|
||||
|
||||
let grantee_user = match User::find_by_mail(&email, &mut conn).await {
|
||||
let (grantee_user, new_user) = match User::find_by_mail(&email, &mut conn).await {
|
||||
None => {
|
||||
if !CONFIG.invitations_allowed() {
|
||||
err!(format!("Grantee user does not exist: {}", &email))
|
||||
|
@ -226,9 +230,10 @@ async fn send_invite(data: JsonUpcase<EmergencyAccessInviteData>, headers: Heade
|
|||
|
||||
let mut user = User::new(email.clone());
|
||||
user.save(&mut conn).await?;
|
||||
user
|
||||
(user, true)
|
||||
}
|
||||
Some(user) => user,
|
||||
Some(user) if user.password_hash.is_empty() => (user, true),
|
||||
Some(user) => (user, false),
|
||||
};
|
||||
|
||||
if EmergencyAccess::find_by_grantor_uuid_and_grantee_uuid_or_email(
|
||||
|
@ -256,15 +261,9 @@ async fn send_invite(data: JsonUpcase<EmergencyAccessInviteData>, headers: Heade
|
|||
&grantor_user.email,
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
// Automatically mark user as accepted if no email invites
|
||||
match User::find_by_mail(&email, &mut conn).await {
|
||||
Some(user) => match accept_invite_process(&user.uuid, &mut new_emergency_access, &email, &mut conn).await {
|
||||
Ok(v) => v,
|
||||
Err(e) => err!(e.to_string()),
|
||||
},
|
||||
None => err!("Grantee user not found."),
|
||||
}
|
||||
} else if !new_user {
|
||||
// if mail is not enabled immediately accept the invitation for existing users
|
||||
new_emergency_access.accept_invite(&grantee_user.uuid, &email, &mut conn).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -308,17 +307,12 @@ async fn resend_invite(emer_id: &str, headers: Headers, mut conn: DbConn) -> Emp
|
|||
&grantor_user.email,
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
if Invitation::find_by_mail(&email, &mut conn).await.is_none() {
|
||||
let invitation = Invitation::new(&email);
|
||||
invitation.save(&mut conn).await?;
|
||||
}
|
||||
|
||||
// Automatically mark user as accepted if no email invites
|
||||
match accept_invite_process(&grantee_user.uuid, &mut emergency_access, &email, &mut conn).await {
|
||||
Ok(v) => v,
|
||||
Err(e) => err!(e.to_string()),
|
||||
}
|
||||
} else if !grantee_user.password_hash.is_empty() {
|
||||
// accept the invitation for existing user
|
||||
emergency_access.accept_invite(&grantee_user.uuid, &email, &mut conn).await?;
|
||||
} else if CONFIG.invitations_allowed() && Invitation::find_by_mail(&email, &mut conn).await.is_none() {
|
||||
let invitation = Invitation::new(&email);
|
||||
invitation.save(&mut conn).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -367,10 +361,7 @@ async fn accept_invite(emer_id: &str, data: JsonUpcase<AcceptData>, headers: Hea
|
|||
&& grantor_user.name == claims.grantor_name
|
||||
&& grantor_user.email == claims.grantor_email
|
||||
{
|
||||
match accept_invite_process(&grantee_user.uuid, &mut emergency_access, &grantee_user.email, &mut conn).await {
|
||||
Ok(v) => v,
|
||||
Err(e) => err!(e.to_string()),
|
||||
}
|
||||
emergency_access.accept_invite(&grantee_user.uuid, &grantee_user.email, &mut conn).await?;
|
||||
|
||||
if CONFIG.mail_enabled() {
|
||||
mail::send_emergency_access_invite_accepted(&grantor_user.email, &grantee_user.email).await?;
|
||||
|
@ -382,26 +373,6 @@ async fn accept_invite(emer_id: &str, data: JsonUpcase<AcceptData>, headers: Hea
|
|||
}
|
||||
}
|
||||
|
||||
async fn accept_invite_process(
|
||||
grantee_uuid: &str,
|
||||
emergency_access: &mut EmergencyAccess,
|
||||
grantee_email: &str,
|
||||
conn: &mut DbConn,
|
||||
) -> EmptyResult {
|
||||
if emergency_access.email.is_none() || emergency_access.email.as_ref().unwrap() != grantee_email {
|
||||
err!("User email does not match invite.");
|
||||
}
|
||||
|
||||
if emergency_access.status == EmergencyAccessStatus::Accepted as i32 {
|
||||
err!("Emergency contact already accepted.");
|
||||
}
|
||||
|
||||
emergency_access.status = EmergencyAccessStatus::Accepted as i32;
|
||||
emergency_access.grantee_uuid = Some(String::from(grantee_uuid));
|
||||
emergency_access.email = None;
|
||||
emergency_access.save(conn).await
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
struct ConfirmData {
|
||||
|
@ -766,7 +737,7 @@ pub async fn emergency_request_timeout_job(pool: DbPool) {
|
|||
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() + Duration::days(i64::from(emer.wait_time_days));
|
||||
emer.recovery_initiated_at.unwrap() + TimeDelta::try_days(i64::from(emer.wait_time_days)).unwrap();
|
||||
if recovery_allowed_at.le(&now) {
|
||||
// Only update the access status
|
||||
// Updating the whole record could cause issues when the emergency_notification_reminder_job is also active
|
||||
|
@ -822,10 +793,10 @@ pub async fn emergency_notification_reminder_job(pool: DbPool) {
|
|||
// 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() + Duration::days(i64::from(emer.wait_time_days - 1));
|
||||
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 + Duration::days(1)
|
||||
last_notification_at + TimeDelta::try_days(1).unwrap()
|
||||
} else {
|
||||
now
|
||||
};
|
||||
|
|
|
@ -125,7 +125,7 @@ async fn get_user_events(
|
|||
})))
|
||||
}
|
||||
|
||||
fn get_continuation_token(events_json: &Vec<Value>) -> Option<&str> {
|
||||
fn get_continuation_token(events_json: &[Value]) -> Option<&str> {
|
||||
// When the length of the vec equals the max page_size there probably is more data
|
||||
// When it is less, then all events are loaded.
|
||||
if events_json.len() as i64 == Event::PAGE_SIZE {
|
||||
|
@ -289,7 +289,7 @@ async fn _log_event(
|
|||
let mut event = Event::new(event_type, event_date);
|
||||
match event_type {
|
||||
// 1000..=1099 Are user events, they need to be logged via log_user_event()
|
||||
// Collection Events
|
||||
// Cipher Events
|
||||
1100..=1199 => {
|
||||
event.cipher_uuid = Some(String::from(source_uuid));
|
||||
}
|
||||
|
|
|
@ -191,14 +191,17 @@ fn version() -> Json<&'static str> {
|
|||
#[get("/config")]
|
||||
fn config() -> Json<Value> {
|
||||
let domain = crate::CONFIG.domain();
|
||||
let feature_states = parse_experimental_client_feature_flags(&crate::CONFIG.experimental_client_feature_flags());
|
||||
let mut feature_states =
|
||||
parse_experimental_client_feature_flags(&crate::CONFIG.experimental_client_feature_flags());
|
||||
// Force the new key rotation feature
|
||||
feature_states.insert("key-rotation-improvements".to_string(), true);
|
||||
Json(json!({
|
||||
// Note: The clients use this version to handle backwards compatibility concerns
|
||||
// This means they expect a version that closely matches the Bitwarden server version
|
||||
// We should make sure that we keep this updated when we support the new server features
|
||||
// Version history:
|
||||
// - Individual cipher key encryption: 2023.9.1
|
||||
"version": "2023.9.1",
|
||||
"version": "2024.2.0",
|
||||
"gitHash": option_env!("GIT_REV"),
|
||||
"server": {
|
||||
"name": "Vaultwarden",
|
||||
|
|
|
@ -320,9 +320,29 @@ async fn get_org_collections_details(org_id: &str, headers: ManagerHeadersLoose,
|
|||
None => err!("User is not part of organization"),
|
||||
};
|
||||
|
||||
// get all collection memberships for the current organization
|
||||
let coll_users = CollectionUser::find_by_organization(org_id, &mut conn).await;
|
||||
|
||||
// check if current user has full access to the organization (either directly or via any group)
|
||||
let has_full_access_to_org = user_org.access_all
|
||||
|| (CONFIG.org_groups_enabled()
|
||||
&& GroupUser::has_full_access_by_member(org_id, &user_org.uuid, &mut conn).await);
|
||||
|
||||
for col in Collection::find_by_organization(org_id, &mut conn).await {
|
||||
// check whether the current user has access to the given collection
|
||||
let assigned = has_full_access_to_org
|
||||
|| CollectionUser::has_access_to_collection_by_user(&col.uuid, &user_org.user_uuid, &mut conn).await
|
||||
|| (CONFIG.org_groups_enabled()
|
||||
&& GroupUser::has_access_to_collection_by_member(&col.uuid, &user_org.uuid, &mut conn).await);
|
||||
|
||||
// get the users assigned directly to the given collection
|
||||
let users: Vec<Value> = coll_users
|
||||
.iter()
|
||||
.filter(|collection_user| collection_user.collection_uuid == col.uuid)
|
||||
.map(|collection_user| SelectionReadOnly::to_collection_user_details_read_only(collection_user).to_json())
|
||||
.collect();
|
||||
|
||||
// get the group details for the given collection
|
||||
let groups: Vec<Value> = if CONFIG.org_groups_enabled() {
|
||||
CollectionGroup::find_by_collection(&col.uuid, &mut conn)
|
||||
.await
|
||||
|
@ -332,29 +352,9 @@ async fn get_org_collections_details(org_id: &str, headers: ManagerHeadersLoose,
|
|||
})
|
||||
.collect()
|
||||
} else {
|
||||
// The Bitwarden clients seem to call this API regardless of whether groups are enabled,
|
||||
// so just act as if there are no groups.
|
||||
Vec::with_capacity(0)
|
||||
};
|
||||
|
||||
let mut assigned = false;
|
||||
let users: Vec<Value> = coll_users
|
||||
.iter()
|
||||
.filter(|collection_user| collection_user.collection_uuid == col.uuid)
|
||||
.map(|collection_user| {
|
||||
// Remember `user_uuid` is swapped here with the `user_org.uuid` with a join during the `CollectionUser::find_by_organization` call.
|
||||
// We check here if the current user is assigned to this collection or not.
|
||||
if collection_user.user_uuid == user_org.uuid {
|
||||
assigned = true;
|
||||
}
|
||||
SelectionReadOnly::to_collection_user_details_read_only(collection_user).to_json()
|
||||
})
|
||||
.collect();
|
||||
|
||||
if user_org.access_all {
|
||||
assigned = true;
|
||||
}
|
||||
|
||||
let mut json_object = col.to_json();
|
||||
json_object["Assigned"] = json!(assigned);
|
||||
json_object["Users"] = json!(users);
|
||||
|
@ -664,24 +664,16 @@ async fn get_org_collection_detail(
|
|||
Vec::with_capacity(0)
|
||||
};
|
||||
|
||||
let mut assigned = false;
|
||||
let users: Vec<Value> =
|
||||
CollectionUser::find_by_collection_swap_user_uuid_with_org_user_uuid(&collection.uuid, &mut conn)
|
||||
.await
|
||||
.iter()
|
||||
.map(|collection_user| {
|
||||
// Remember `user_uuid` is swapped here with the `user_org.uuid` with a join during the `find_by_collection_swap_user_uuid_with_org_user_uuid` call.
|
||||
// We check here if the current user is assigned to this collection or not.
|
||||
if collection_user.user_uuid == user_org.uuid {
|
||||
assigned = true;
|
||||
}
|
||||
SelectionReadOnly::to_collection_user_details_read_only(collection_user).to_json()
|
||||
})
|
||||
.collect();
|
||||
|
||||
if user_org.access_all {
|
||||
assigned = true;
|
||||
}
|
||||
let assigned = Collection::can_access_collection(&user_org, &collection.uuid, &mut conn).await;
|
||||
|
||||
let mut json_object = collection.to_json();
|
||||
json_object["Assigned"] = json!(assigned);
|
||||
|
@ -1071,7 +1063,7 @@ async fn accept_invite(
|
|||
let claims = decode_invite(&data.Token)?;
|
||||
|
||||
match User::find_by_mail(&claims.email, &mut conn).await {
|
||||
Some(_) => {
|
||||
Some(user) => {
|
||||
Invitation::take(&claims.email, &mut conn).await;
|
||||
|
||||
if let (Some(user_org), Some(org)) = (&claims.user_org_id, &claims.org_id) {
|
||||
|
@ -1095,7 +1087,11 @@ async fn accept_invite(
|
|||
match OrgPolicy::is_user_allowed(&user_org.user_uuid, org_id, false, &mut conn).await {
|
||||
Ok(_) => {}
|
||||
Err(OrgPolicyErr::TwoFactorMissing) => {
|
||||
err!("You cannot join this organization until you enable two-step login on your user account");
|
||||
if CONFIG.email_2fa_auto_fallback() {
|
||||
two_factor::email::activate_email_2fa(&user, &mut conn).await?;
|
||||
} else {
|
||||
err!("You cannot join this organization until you enable two-step login on your user account");
|
||||
}
|
||||
}
|
||||
Err(OrgPolicyErr::SingleOrgEnforced) => {
|
||||
err!("You cannot join this organization because you are a member of an organization which forbids it");
|
||||
|
@ -1220,10 +1216,14 @@ async fn _confirm_invite(
|
|||
match OrgPolicy::is_user_allowed(&user_to_confirm.user_uuid, org_id, true, conn).await {
|
||||
Ok(_) => {}
|
||||
Err(OrgPolicyErr::TwoFactorMissing) => {
|
||||
err!("You cannot confirm this user because it has no two-step login method activated");
|
||||
if CONFIG.email_2fa_auto_fallback() {
|
||||
two_factor::email::find_and_activate_email_2fa(&user_to_confirm.user_uuid, conn).await?;
|
||||
} else {
|
||||
err!("You cannot confirm this user because they have not setup 2FA");
|
||||
}
|
||||
}
|
||||
Err(OrgPolicyErr::SingleOrgEnforced) => {
|
||||
err!("You cannot confirm this user because it is a member of an organization which forbids it");
|
||||
err!("You cannot confirm this user because they are a member of an organization which forbids it");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1351,10 +1351,14 @@ async fn edit_user(
|
|||
match OrgPolicy::is_user_allowed(&user_to_edit.user_uuid, org_id, true, &mut conn).await {
|
||||
Ok(_) => {}
|
||||
Err(OrgPolicyErr::TwoFactorMissing) => {
|
||||
err!("You cannot modify this user to this type because it has no two-step login method activated");
|
||||
if CONFIG.email_2fa_auto_fallback() {
|
||||
two_factor::email::find_and_activate_email_2fa(&user_to_edit.user_uuid, &mut conn).await?;
|
||||
} else {
|
||||
err!("You cannot modify this user to this type because they have not setup 2FA");
|
||||
}
|
||||
}
|
||||
Err(OrgPolicyErr::SingleOrgEnforced) => {
|
||||
err!("You cannot modify this user to this type because it is a member of an organization which forbids it");
|
||||
err!("You cannot modify this user to this type because they are a member of an organization which forbids it");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1598,7 +1602,7 @@ async fn post_org_import(
|
|||
let mut ciphers = Vec::new();
|
||||
for cipher_data in data.Ciphers {
|
||||
let mut cipher = Cipher::new(cipher_data.Type, cipher_data.Name.clone());
|
||||
update_cipher_from_data(&mut cipher, cipher_data, &headers, false, &mut conn, &nt, UpdateType::None).await.ok();
|
||||
update_cipher_from_data(&mut cipher, cipher_data, &headers, None, &mut conn, &nt, UpdateType::None).await.ok();
|
||||
ciphers.push(cipher);
|
||||
}
|
||||
|
||||
|
@ -2151,10 +2155,14 @@ async fn _restore_organization_user(
|
|||
match OrgPolicy::is_user_allowed(&user_org.user_uuid, org_id, false, conn).await {
|
||||
Ok(_) => {}
|
||||
Err(OrgPolicyErr::TwoFactorMissing) => {
|
||||
err!("You cannot restore this user because it has no two-step login method activated");
|
||||
if CONFIG.email_2fa_auto_fallback() {
|
||||
two_factor::email::find_and_activate_email_2fa(&user_org.user_uuid, conn).await?;
|
||||
} else {
|
||||
err!("You cannot restore this user because they have not setup 2FA");
|
||||
}
|
||||
}
|
||||
Err(OrgPolicyErr::SingleOrgEnforced) => {
|
||||
err!("You cannot restore this user because it is a member of an organization which forbids it");
|
||||
err!("You cannot restore this user because they are a member of an organization which forbids it");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2223,7 +2231,7 @@ impl GroupRequest {
|
|||
}
|
||||
|
||||
pub fn update_group(&self, mut group: Group) -> Group {
|
||||
group.name = self.Name.clone();
|
||||
group.name.clone_from(&self.Name);
|
||||
group.access_all = self.AccessAll.unwrap_or(false);
|
||||
// Group Updates do not support changing the external_id
|
||||
// These input fields are in a disabled state, and can only be updated/added via ldap_import
|
||||
|
@ -2659,6 +2667,7 @@ async fn delete_group_user(
|
|||
struct OrganizationUserResetPasswordEnrollmentRequest {
|
||||
ResetPasswordKey: Option<String>,
|
||||
MasterPasswordHash: Option<String>,
|
||||
Otp: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
@ -2841,14 +2850,12 @@ async fn put_reset_password_enrollment(
|
|||
}
|
||||
|
||||
if reset_request.ResetPasswordKey.is_some() {
|
||||
match reset_request.MasterPasswordHash {
|
||||
Some(password) => {
|
||||
if !headers.user.check_valid_password(&password) {
|
||||
err!("Invalid or wrong password")
|
||||
}
|
||||
}
|
||||
None => err!("No password provided"),
|
||||
};
|
||||
PasswordOrOtpData {
|
||||
MasterPasswordHash: reset_request.MasterPasswordHash,
|
||||
Otp: reset_request.Otp,
|
||||
}
|
||||
.validate(&headers.user, true, &mut conn)
|
||||
.await?;
|
||||
}
|
||||
|
||||
org_user.reset_password_key = reset_request.ResetPasswordKey;
|
||||
|
|
|
@ -209,7 +209,7 @@ impl<'r> FromRequest<'r> for PublicToken {
|
|||
Err(_) => err_handler!("Invalid claim"),
|
||||
};
|
||||
// Check if time is between claims.nbf and claims.exp
|
||||
let time_now = Utc::now().naive_utc().timestamp();
|
||||
let time_now = Utc::now().timestamp();
|
||||
if time_now < claims.nbf {
|
||||
err_handler!("Token issued in the future");
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use std::path::Path;
|
||||
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use chrono::{DateTime, TimeDelta, Utc};
|
||||
use num_traits::ToPrimitive;
|
||||
use rocket::form::Form;
|
||||
use rocket::fs::NamedFile;
|
||||
|
@ -49,7 +49,7 @@ pub async fn purge_sends(pool: DbPool) {
|
|||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
struct SendData {
|
||||
pub struct SendData {
|
||||
Type: i32,
|
||||
Key: String,
|
||||
Password: Option<String>,
|
||||
|
@ -65,6 +65,9 @@ struct SendData {
|
|||
Text: Option<Value>,
|
||||
File: Option<Value>,
|
||||
FileLength: Option<NumberOrString>,
|
||||
|
||||
// Used for key rotations
|
||||
pub Id: Option<String>,
|
||||
}
|
||||
|
||||
/// Enforces the `Disable Send` policy. A non-owner/admin user belonging to
|
||||
|
@ -119,7 +122,7 @@ fn create_send(data: SendData, user_uuid: String) -> ApiResult<Send> {
|
|||
err!("Send data not provided");
|
||||
};
|
||||
|
||||
if data.DeletionDate > Utc::now() + Duration::days(31) {
|
||||
if data.DeletionDate > Utc::now() + TimeDelta::try_days(31).unwrap() {
|
||||
err!(
|
||||
"You cannot have a Send with a deletion date that far into the future. Adjust the Deletion Date to a value less than 31 days from now and try again."
|
||||
);
|
||||
|
@ -549,6 +552,19 @@ async fn put_send(
|
|||
None => err!("Send not found"),
|
||||
};
|
||||
|
||||
update_send_from_data(&mut send, data, &headers, &mut conn, &nt, UpdateType::SyncSendUpdate).await?;
|
||||
|
||||
Ok(Json(send.to_json()))
|
||||
}
|
||||
|
||||
pub async fn update_send_from_data(
|
||||
send: &mut Send,
|
||||
data: SendData,
|
||||
headers: &Headers,
|
||||
conn: &mut DbConn,
|
||||
nt: &Notify<'_>,
|
||||
ut: UpdateType,
|
||||
) -> EmptyResult {
|
||||
if send.user_uuid.as_ref() != Some(&headers.user.uuid) {
|
||||
err!("Send is not owned by user")
|
||||
}
|
||||
|
@ -557,6 +573,12 @@ async fn put_send(
|
|||
err!("Sends can't change type")
|
||||
}
|
||||
|
||||
if data.DeletionDate > Utc::now() + TimeDelta::try_days(31).unwrap() {
|
||||
err!(
|
||||
"You cannot have a Send with a deletion date that far into the future. Adjust the Deletion Date to a value less than 31 days from now and try again."
|
||||
);
|
||||
}
|
||||
|
||||
// When updating a file Send, we receive nulls in the File field, as it's immutable,
|
||||
// so we only need to update the data field in the Text case
|
||||
if data.Type == SendType::Text as i32 {
|
||||
|
@ -569,11 +591,6 @@ async fn put_send(
|
|||
send.data = data_str;
|
||||
}
|
||||
|
||||
if data.DeletionDate > Utc::now() + Duration::days(31) {
|
||||
err!(
|
||||
"You cannot have a Send with a deletion date that far into the future. Adjust the Deletion Date to a value less than 31 days from now and try again."
|
||||
);
|
||||
}
|
||||
send.name = data.Name;
|
||||
send.akey = data.Key;
|
||||
send.deletion_date = data.DeletionDate.naive_utc();
|
||||
|
@ -591,17 +608,11 @@ async fn put_send(
|
|||
send.set_password(Some(&password));
|
||||
}
|
||||
|
||||
send.save(&mut conn).await?;
|
||||
nt.send_send_update(
|
||||
UpdateType::SyncSendUpdate,
|
||||
&send,
|
||||
&send.update_users_revision(&mut conn).await,
|
||||
&headers.device.uuid,
|
||||
&mut conn,
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(Json(send.to_json()))
|
||||
send.save(conn).await?;
|
||||
if ut != UpdateType::None {
|
||||
nt.send_send_update(ut, send, &send.update_users_revision(conn).await, &headers.device.uuid, conn).await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[delete("/sends/<id>")]
|
||||
|
|
|
@ -156,8 +156,8 @@ pub async fn validate_totp_code(
|
|||
let time = (current_timestamp + step * 30i64) as u64;
|
||||
let generated = totp_custom::<Sha1>(30, 6, &decoded_secret, time);
|
||||
|
||||
// Check the the given code equals the generated and if the time_step is larger then the one last used.
|
||||
if generated == totp_code && time_step > i64::from(twofactor.last_used) {
|
||||
// Check the given code equals the generated and if the time_step is larger then the one last used.
|
||||
if generated == totp_code && time_step > twofactor.last_used {
|
||||
// If the step does not equals 0 the time is drifted either server or client side.
|
||||
if step != 0 {
|
||||
warn!("TOTP Time drift detected. The step offset is {}", step);
|
||||
|
@ -165,10 +165,10 @@ pub async fn validate_totp_code(
|
|||
|
||||
// Save the last used time step so only totp time steps higher then this one are allowed.
|
||||
// This will also save a newly created twofactor if the code is correct.
|
||||
twofactor.last_used = time_step as i32;
|
||||
twofactor.last_used = time_step;
|
||||
twofactor.save(conn).await?;
|
||||
return Ok(());
|
||||
} else if generated == totp_code && time_step <= i64::from(twofactor.last_used) {
|
||||
} else if generated == totp_code && time_step <= twofactor.last_used {
|
||||
warn!("This TOTP or a TOTP code within {} steps back or forward has already been used!", steps);
|
||||
err!(
|
||||
format!("Invalid TOTP code! Server time: {} IP: {}", current_time.format("%F %T UTC"), ip.ip),
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use chrono::{Duration, NaiveDateTime, Utc};
|
||||
use chrono::{DateTime, TimeDelta, Utc};
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::Route;
|
||||
|
||||
|
@ -10,7 +10,7 @@ use crate::{
|
|||
auth::Headers,
|
||||
crypto,
|
||||
db::{
|
||||
models::{EventType, TwoFactor, TwoFactorType},
|
||||
models::{EventType, TwoFactor, TwoFactorType, User},
|
||||
DbConn,
|
||||
},
|
||||
error::{Error, MapResult},
|
||||
|
@ -232,9 +232,9 @@ pub async fn validate_email_code_str(user_uuid: &str, token: &str, data: &str, c
|
|||
twofactor.data = email_data.to_json();
|
||||
twofactor.save(conn).await?;
|
||||
|
||||
let date = NaiveDateTime::from_timestamp_opt(email_data.token_sent, 0).expect("Email token timestamp invalid.");
|
||||
let date = DateTime::from_timestamp(email_data.token_sent, 0).expect("Email token timestamp invalid.").naive_utc();
|
||||
let max_time = CONFIG.email_expiration_time() as i64;
|
||||
if date + Duration::seconds(max_time) < Utc::now().naive_utc() {
|
||||
if date + TimeDelta::try_seconds(max_time).unwrap() < Utc::now().naive_utc() {
|
||||
err!(
|
||||
"Token has expired",
|
||||
ErrorEvent {
|
||||
|
@ -265,14 +265,14 @@ impl EmailTokenData {
|
|||
EmailTokenData {
|
||||
email,
|
||||
last_token: Some(token),
|
||||
token_sent: Utc::now().naive_utc().timestamp(),
|
||||
token_sent: Utc::now().timestamp(),
|
||||
attempts: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_token(&mut self, token: String) {
|
||||
self.last_token = Some(token);
|
||||
self.token_sent = Utc::now().naive_utc().timestamp();
|
||||
self.token_sent = Utc::now().timestamp();
|
||||
}
|
||||
|
||||
pub fn reset_token(&mut self) {
|
||||
|
@ -297,6 +297,15 @@ impl EmailTokenData {
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn activate_email_2fa(user: &User, conn: &mut DbConn) -> EmptyResult {
|
||||
if user.verified_at.is_none() {
|
||||
err!("Auto-enabling of email 2FA failed because the users email address has not been verified!");
|
||||
}
|
||||
let twofactor_data = EmailTokenData::new(user.email.clone(), String::new());
|
||||
let twofactor = TwoFactor::new(user.uuid.clone(), TwoFactorType::Email, twofactor_data.to_json());
|
||||
twofactor.save(conn).await
|
||||
}
|
||||
|
||||
/// Takes an email address and obscures it by replacing it with asterisks except two characters.
|
||||
pub fn obscure_email(email: &str) -> String {
|
||||
let split: Vec<&str> = email.rsplitn(2, '@').collect();
|
||||
|
@ -318,6 +327,14 @@ pub fn obscure_email(email: &str) -> String {
|
|||
format!("{}@{}", new_name, &domain)
|
||||
}
|
||||
|
||||
pub async fn find_and_activate_email_2fa(user_uuid: &str, conn: &mut DbConn) -> EmptyResult {
|
||||
if let Some(user) = User::find_by_uuid(user_uuid, conn).await {
|
||||
activate_email_2fa(&user, conn).await
|
||||
} else {
|
||||
err!("User not found!");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use chrono::{Duration, Utc};
|
||||
use chrono::{TimeDelta, Utc};
|
||||
use data_encoding::BASE32;
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::Route;
|
||||
|
@ -259,7 +259,7 @@ pub async fn send_incomplete_2fa_notifications(pool: DbPool) {
|
|||
};
|
||||
|
||||
let now = Utc::now().naive_utc();
|
||||
let time_limit = Duration::minutes(CONFIG.incomplete_2fa_time_limit());
|
||||
let time_limit = TimeDelta::try_minutes(CONFIG.incomplete_2fa_time_limit()).unwrap();
|
||||
let time_before = now - time_limit;
|
||||
let incomplete_logins = TwoFactorIncomplete::find_logins_before(&time_before, &mut conn).await;
|
||||
for login in incomplete_logins {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use chrono::{Duration, NaiveDateTime, Utc};
|
||||
use chrono::{DateTime, TimeDelta, Utc};
|
||||
use rocket::Route;
|
||||
|
||||
use crate::{
|
||||
|
@ -32,7 +32,7 @@ impl ProtectedActionData {
|
|||
pub fn new(token: String) -> Self {
|
||||
Self {
|
||||
token,
|
||||
token_sent: Utc::now().naive_utc().timestamp(),
|
||||
token_sent: Utc::now().timestamp(),
|
||||
attempts: 0,
|
||||
}
|
||||
}
|
||||
|
@ -122,9 +122,9 @@ pub async fn validate_protected_action_otp(
|
|||
|
||||
// Check if the token has expired (Using the email 2fa expiration time)
|
||||
let date =
|
||||
NaiveDateTime::from_timestamp_opt(pa_data.token_sent, 0).expect("Protected Action token timestamp invalid.");
|
||||
DateTime::from_timestamp(pa_data.token_sent, 0).expect("Protected Action token timestamp invalid.").naive_utc();
|
||||
let max_time = CONFIG.email_expiration_time() as i64;
|
||||
if date + Duration::seconds(max_time) < Utc::now().naive_utc() {
|
||||
if date + TimeDelta::try_seconds(max_time).unwrap() < Utc::now().naive_utc() {
|
||||
pa.delete(conn).await?;
|
||||
err!("Token has expired")
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use rocket::serde::json::Json;
|
||||
use rocket::Route;
|
||||
use serde_json::Value;
|
||||
use yubico::{config::Config, verify};
|
||||
use yubico::{config::Config, verify_async};
|
||||
|
||||
use crate::{
|
||||
api::{
|
||||
|
@ -74,13 +74,10 @@ async fn verify_yubikey_otp(otp: String) -> EmptyResult {
|
|||
let config = Config::default().set_client_id(yubico_id).set_key(yubico_secret);
|
||||
|
||||
match CONFIG.yubico_server() {
|
||||
Some(server) => {
|
||||
tokio::task::spawn_blocking(move || verify(otp, config.set_api_hosts(vec![server]))).await.unwrap()
|
||||
}
|
||||
None => tokio::task::spawn_blocking(move || verify(otp, config)).await.unwrap(),
|
||||
Some(server) => verify_async(otp, config.set_api_hosts(vec![server])).await,
|
||||
None => verify_async(otp, config).await,
|
||||
}
|
||||
.map_res("Failed to verify OTP")
|
||||
.and(Ok(()))
|
||||
}
|
||||
|
||||
#[post("/two-factor/get-yubikey", data = "<data>")]
|
||||
|
@ -194,10 +191,6 @@ pub async fn validate_yubikey_login(response: &str, twofactor_data: &str) -> Emp
|
|||
err!("Given Yubikey is not registered");
|
||||
}
|
||||
|
||||
let result = verify_yubikey_otp(response.to_owned()).await;
|
||||
|
||||
match result {
|
||||
Ok(_answer) => Ok(()),
|
||||
Err(_e) => err!("Failed to verify Yubikey against OTP server"),
|
||||
}
|
||||
verify_yubikey_otp(response.to_owned()).await.map_res("Failed to verify Yubikey against OTP server")?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
320
src/api/icons.rs
320
src/api/icons.rs
|
@ -1,6 +1,6 @@
|
|||
use std::{
|
||||
net::IpAddr,
|
||||
sync::Arc,
|
||||
sync::{Arc, Mutex},
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
|
||||
|
@ -16,14 +16,13 @@ use rocket::{http::ContentType, response::Redirect, Route};
|
|||
use tokio::{
|
||||
fs::{create_dir_all, remove_file, symlink_metadata, File},
|
||||
io::{AsyncReadExt, AsyncWriteExt},
|
||||
net::lookup_host,
|
||||
};
|
||||
|
||||
use html5gum::{Emitter, HtmlString, InfallibleTokenizer, Readable, StringReader, Tokenizer};
|
||||
|
||||
use crate::{
|
||||
error::Error,
|
||||
util::{get_reqwest_client_builder, Cached},
|
||||
util::{get_reqwest_client_builder, Cached, CustomDnsResolver, CustomResolverError},
|
||||
CONFIG,
|
||||
};
|
||||
|
||||
|
@ -49,48 +48,32 @@ static CLIENT: Lazy<Client> = Lazy::new(|| {
|
|||
let icon_download_timeout = Duration::from_secs(CONFIG.icon_download_timeout());
|
||||
let pool_idle_timeout = Duration::from_secs(10);
|
||||
// Reuse the client between requests
|
||||
let client = get_reqwest_client_builder()
|
||||
get_reqwest_client_builder()
|
||||
.cookie_provider(Arc::clone(&cookie_store))
|
||||
.timeout(icon_download_timeout)
|
||||
.pool_max_idle_per_host(5) // Configure the Hyper Pool to only have max 5 idle connections
|
||||
.pool_idle_timeout(pool_idle_timeout) // Configure the Hyper Pool to timeout after 10 seconds
|
||||
.trust_dns(true)
|
||||
.default_headers(default_headers.clone());
|
||||
|
||||
match client.build() {
|
||||
Ok(client) => client,
|
||||
Err(e) => {
|
||||
error!("Possible trust-dns error, trying with trust-dns disabled: '{e}'");
|
||||
get_reqwest_client_builder()
|
||||
.cookie_provider(cookie_store)
|
||||
.timeout(icon_download_timeout)
|
||||
.pool_max_idle_per_host(5) // Configure the Hyper Pool to only have max 5 idle connections
|
||||
.pool_idle_timeout(pool_idle_timeout) // Configure the Hyper Pool to timeout after 10 seconds
|
||||
.trust_dns(false)
|
||||
.default_headers(default_headers)
|
||||
.build()
|
||||
.expect("Failed to build client")
|
||||
}
|
||||
}
|
||||
.dns_resolver(CustomDnsResolver::instance())
|
||||
.default_headers(default_headers.clone())
|
||||
.build()
|
||||
.expect("Failed to build client")
|
||||
});
|
||||
|
||||
// Build Regex only once since this takes a lot of time.
|
||||
static ICON_SIZE_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?x)(\d+)\D*(\d+)").unwrap());
|
||||
|
||||
// Special HashMap which holds the user defined Regex to speedup matching the regex.
|
||||
static ICON_BLACKLIST_REGEX: Lazy<dashmap::DashMap<String, Regex>> = Lazy::new(dashmap::DashMap::new);
|
||||
|
||||
async fn icon_redirect(domain: &str, template: &str) -> Option<Redirect> {
|
||||
#[get("/<domain>/icon.png")]
|
||||
fn icon_external(domain: &str) -> Option<Redirect> {
|
||||
if !is_valid_domain(domain) {
|
||||
warn!("Invalid domain: {}", domain);
|
||||
return None;
|
||||
}
|
||||
|
||||
if check_domain_blacklist_reason(domain).await.is_some() {
|
||||
if is_domain_blacklisted(domain) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let url = template.replace("{}", domain);
|
||||
let url = CONFIG._icon_service_url().replace("{}", domain);
|
||||
match CONFIG.icon_redirect_code() {
|
||||
301 => Some(Redirect::moved(url)), // legacy permanent redirect
|
||||
302 => Some(Redirect::found(url)), // legacy temporary redirect
|
||||
|
@ -103,11 +86,6 @@ async fn icon_redirect(domain: &str, template: &str) -> Option<Redirect> {
|
|||
}
|
||||
}
|
||||
|
||||
#[get("/<domain>/icon.png")]
|
||||
async fn icon_external(domain: &str) -> Option<Redirect> {
|
||||
icon_redirect(domain, &CONFIG._icon_service_url()).await
|
||||
}
|
||||
|
||||
#[get("/<domain>/icon.png")]
|
||||
async fn icon_internal(domain: &str) -> Cached<(ContentType, Vec<u8>)> {
|
||||
const FALLBACK_ICON: &[u8] = include_bytes!("../static/images/fallback-icon.png");
|
||||
|
@ -166,153 +144,28 @@ fn is_valid_domain(domain: &str) -> bool {
|
|||
true
|
||||
}
|
||||
|
||||
/// TODO: This is extracted from IpAddr::is_global, which is unstable:
|
||||
/// https://doc.rust-lang.org/nightly/std/net/enum.IpAddr.html#method.is_global
|
||||
/// Remove once https://github.com/rust-lang/rust/issues/27709 is merged
|
||||
#[allow(clippy::nonminimal_bool)]
|
||||
#[cfg(not(feature = "unstable"))]
|
||||
fn is_global(ip: IpAddr) -> bool {
|
||||
match ip {
|
||||
IpAddr::V4(ip) => {
|
||||
// check if this address is 192.0.0.9 or 192.0.0.10. These addresses are the only two
|
||||
// globally routable addresses in the 192.0.0.0/24 range.
|
||||
if u32::from(ip) == 0xc0000009 || u32::from(ip) == 0xc000000a {
|
||||
return true;
|
||||
}
|
||||
!ip.is_private()
|
||||
&& !ip.is_loopback()
|
||||
&& !ip.is_link_local()
|
||||
&& !ip.is_broadcast()
|
||||
&& !ip.is_documentation()
|
||||
&& !(ip.octets()[0] == 100 && (ip.octets()[1] & 0b1100_0000 == 0b0100_0000))
|
||||
&& !(ip.octets()[0] == 192 && ip.octets()[1] == 0 && ip.octets()[2] == 0)
|
||||
&& !(ip.octets()[0] & 240 == 240 && !ip.is_broadcast())
|
||||
&& !(ip.octets()[0] == 198 && (ip.octets()[1] & 0xfe) == 18)
|
||||
// Make sure the address is not in 0.0.0.0/8
|
||||
&& ip.octets()[0] != 0
|
||||
}
|
||||
IpAddr::V6(ip) => {
|
||||
if ip.is_multicast() && ip.segments()[0] & 0x000f == 14 {
|
||||
true
|
||||
} else {
|
||||
!ip.is_multicast()
|
||||
&& !ip.is_loopback()
|
||||
&& !((ip.segments()[0] & 0xffc0) == 0xfe80)
|
||||
&& !((ip.segments()[0] & 0xfe00) == 0xfc00)
|
||||
&& !ip.is_unspecified()
|
||||
&& !((ip.segments()[0] == 0x2001) && (ip.segments()[1] == 0xdb8))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn is_domain_blacklisted(domain: &str) -> bool {
|
||||
let Some(config_blacklist) = CONFIG.icon_blacklist_regex() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
#[cfg(feature = "unstable")]
|
||||
fn is_global(ip: IpAddr) -> bool {
|
||||
ip.is_global()
|
||||
}
|
||||
// Compiled domain blacklist
|
||||
static COMPILED_BLACKLIST: Mutex<Option<(String, Regex)>> = Mutex::new(None);
|
||||
let mut guard = COMPILED_BLACKLIST.lock().unwrap();
|
||||
|
||||
/// These are some tests to check that the implementations match
|
||||
/// The IPv4 can be all checked in 5 mins or so and they are correct as of nightly 2020-07-11
|
||||
/// The IPV6 can't be checked in a reasonable time, so we check about ten billion random ones, so far correct
|
||||
/// Note that the is_global implementation is subject to change as new IP RFCs are created
|
||||
///
|
||||
/// To run while showing progress output:
|
||||
/// cargo test --features sqlite,unstable -- --nocapture --ignored
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "unstable")]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_ipv4_global() {
|
||||
for a in 0..u8::MAX {
|
||||
println!("Iter: {}/255", a);
|
||||
for b in 0..u8::MAX {
|
||||
for c in 0..u8::MAX {
|
||||
for d in 0..u8::MAX {
|
||||
let ip = IpAddr::V4(std::net::Ipv4Addr::new(a, b, c, d));
|
||||
assert_eq!(ip.is_global(), is_global(ip))
|
||||
}
|
||||
}
|
||||
}
|
||||
// If the stored regex is up to date, use it
|
||||
if let Some((value, regex)) = &*guard {
|
||||
if value == &config_blacklist {
|
||||
return regex.is_match(domain);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_ipv6_global() {
|
||||
use ring::rand::{SecureRandom, SystemRandom};
|
||||
let mut v = [0u8; 16];
|
||||
let rand = SystemRandom::new();
|
||||
for i in 0..1_000 {
|
||||
println!("Iter: {}/1_000", i);
|
||||
for _ in 0..10_000_000 {
|
||||
rand.fill(&mut v).expect("Error generating random values");
|
||||
let ip = IpAddr::V6(std::net::Ipv6Addr::new(
|
||||
(v[14] as u16) << 8 | v[15] as u16,
|
||||
(v[12] as u16) << 8 | v[13] as u16,
|
||||
(v[10] as u16) << 8 | v[11] as u16,
|
||||
(v[8] as u16) << 8 | v[9] as u16,
|
||||
(v[6] as u16) << 8 | v[7] as u16,
|
||||
(v[4] as u16) << 8 | v[5] as u16,
|
||||
(v[2] as u16) << 8 | v[3] as u16,
|
||||
(v[0] as u16) << 8 | v[1] as u16,
|
||||
));
|
||||
assert_eq!(ip.is_global(), is_global(ip))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// If we don't have a regex stored, or it's not up to date, recreate it
|
||||
let regex = Regex::new(&config_blacklist).unwrap();
|
||||
let is_match = regex.is_match(domain);
|
||||
*guard = Some((config_blacklist, regex));
|
||||
|
||||
#[derive(Clone)]
|
||||
enum DomainBlacklistReason {
|
||||
Regex,
|
||||
IP,
|
||||
}
|
||||
|
||||
use cached::proc_macro::cached;
|
||||
#[cached(key = "String", convert = r#"{ domain.to_string() }"#, size = 16, time = 60)]
|
||||
async fn check_domain_blacklist_reason(domain: &str) -> Option<DomainBlacklistReason> {
|
||||
// First check the blacklist regex if there is a match.
|
||||
// This prevents the blocked domain(s) from being leaked via a DNS lookup.
|
||||
if let Some(blacklist) = CONFIG.icon_blacklist_regex() {
|
||||
// Use the pre-generate Regex stored in a Lazy HashMap if there's one, else generate it.
|
||||
let is_match = if let Some(regex) = ICON_BLACKLIST_REGEX.get(&blacklist) {
|
||||
regex.is_match(domain)
|
||||
} else {
|
||||
// Clear the current list if the previous key doesn't exists.
|
||||
// To prevent growing of the HashMap after someone has changed it via the admin interface.
|
||||
if ICON_BLACKLIST_REGEX.len() >= 1 {
|
||||
ICON_BLACKLIST_REGEX.clear();
|
||||
}
|
||||
|
||||
// Generate the regex to store in too the Lazy Static HashMap.
|
||||
let blacklist_regex = Regex::new(&blacklist).unwrap();
|
||||
let is_match = blacklist_regex.is_match(domain);
|
||||
ICON_BLACKLIST_REGEX.insert(blacklist.clone(), blacklist_regex);
|
||||
|
||||
is_match
|
||||
};
|
||||
|
||||
if is_match {
|
||||
debug!("Blacklisted domain: {} matched ICON_BLACKLIST_REGEX", domain);
|
||||
return Some(DomainBlacklistReason::Regex);
|
||||
}
|
||||
}
|
||||
|
||||
if CONFIG.icon_blacklist_non_global_ips() {
|
||||
if let Ok(s) = lookup_host((domain, 0)).await {
|
||||
for addr in s {
|
||||
if !is_global(addr.ip()) {
|
||||
debug!("IP {} for domain '{}' is not a global IP!", addr.ip(), domain);
|
||||
return Some(DomainBlacklistReason::IP);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
is_match
|
||||
}
|
||||
|
||||
async fn get_icon(domain: &str) -> Option<(Vec<u8>, String)> {
|
||||
|
@ -342,6 +195,13 @@ async fn get_icon(domain: &str) -> Option<(Vec<u8>, String)> {
|
|||
Some((icon.to_vec(), icon_type.unwrap_or("x-icon").to_string()))
|
||||
}
|
||||
Err(e) => {
|
||||
// If this error comes from the custom resolver, this means this is a blacklisted domain
|
||||
// or non global IP, don't save the miss file in this case to avoid leaking it
|
||||
if let Some(error) = CustomResolverError::downcast_ref(&e) {
|
||||
warn!("{error}");
|
||||
return None;
|
||||
}
|
||||
|
||||
warn!("Unable to download icon: {:?}", e);
|
||||
let miss_indicator = path + ".miss";
|
||||
save_icon(&miss_indicator, &[]).await;
|
||||
|
@ -491,42 +351,48 @@ async fn get_icon_url(domain: &str) -> Result<IconUrlResult, Error> {
|
|||
let ssldomain = format!("https://{domain}");
|
||||
let httpdomain = format!("http://{domain}");
|
||||
|
||||
// First check the domain as given during the request for both HTTPS and HTTP.
|
||||
let resp = match get_page(&ssldomain).or_else(|_| get_page(&httpdomain)).await {
|
||||
Ok(c) => Ok(c),
|
||||
Err(e) => {
|
||||
let mut sub_resp = Err(e);
|
||||
// First check the domain as given during the request for HTTPS.
|
||||
let resp = match get_page(&ssldomain).await {
|
||||
Err(e) if CustomResolverError::downcast_ref(&e).is_none() => {
|
||||
// If we get an error that is not caused by the blacklist, we retry with HTTP
|
||||
match get_page(&httpdomain).await {
|
||||
mut sub_resp @ Err(_) => {
|
||||
// When the domain is not an IP, and has more then one dot, remove all subdomains.
|
||||
let is_ip = domain.parse::<IpAddr>();
|
||||
if is_ip.is_err() && domain.matches('.').count() > 1 {
|
||||
let mut domain_parts = domain.split('.');
|
||||
let base_domain = format!(
|
||||
"{base}.{tld}",
|
||||
tld = domain_parts.next_back().unwrap(),
|
||||
base = domain_parts.next_back().unwrap()
|
||||
);
|
||||
if is_valid_domain(&base_domain) {
|
||||
let sslbase = format!("https://{base_domain}");
|
||||
let httpbase = format!("http://{base_domain}");
|
||||
debug!("[get_icon_url]: Trying without subdomains '{base_domain}'");
|
||||
|
||||
// When the domain is not an IP, and has more then one dot, remove all subdomains.
|
||||
let is_ip = domain.parse::<IpAddr>();
|
||||
if is_ip.is_err() && domain.matches('.').count() > 1 {
|
||||
let mut domain_parts = domain.split('.');
|
||||
let base_domain = format!(
|
||||
"{base}.{tld}",
|
||||
tld = domain_parts.next_back().unwrap(),
|
||||
base = domain_parts.next_back().unwrap()
|
||||
);
|
||||
if is_valid_domain(&base_domain) {
|
||||
let sslbase = format!("https://{base_domain}");
|
||||
let httpbase = format!("http://{base_domain}");
|
||||
debug!("[get_icon_url]: Trying without subdomains '{base_domain}'");
|
||||
sub_resp = get_page(&sslbase).or_else(|_| get_page(&httpbase)).await;
|
||||
}
|
||||
|
||||
sub_resp = get_page(&sslbase).or_else(|_| get_page(&httpbase)).await;
|
||||
}
|
||||
|
||||
// When the domain is not an IP, and has less then 2 dots, try to add www. infront of it.
|
||||
} else if is_ip.is_err() && domain.matches('.').count() < 2 {
|
||||
let www_domain = format!("www.{domain}");
|
||||
if is_valid_domain(&www_domain) {
|
||||
let sslwww = format!("https://{www_domain}");
|
||||
let httpwww = format!("http://{www_domain}");
|
||||
debug!("[get_icon_url]: Trying with www. prefix '{www_domain}'");
|
||||
|
||||
sub_resp = get_page(&sslwww).or_else(|_| get_page(&httpwww)).await;
|
||||
// When the domain is not an IP, and has less then 2 dots, try to add www. infront of it.
|
||||
} else if is_ip.is_err() && domain.matches('.').count() < 2 {
|
||||
let www_domain = format!("www.{domain}");
|
||||
if is_valid_domain(&www_domain) {
|
||||
let sslwww = format!("https://{www_domain}");
|
||||
let httpwww = format!("http://{www_domain}");
|
||||
debug!("[get_icon_url]: Trying with www. prefix '{www_domain}'");
|
||||
|
||||
sub_resp = get_page(&sslwww).or_else(|_| get_page(&httpwww)).await;
|
||||
}
|
||||
}
|
||||
sub_resp
|
||||
}
|
||||
res => res,
|
||||
}
|
||||
sub_resp
|
||||
}
|
||||
|
||||
// If we get a result or a blacklist error, just continue
|
||||
res => res,
|
||||
};
|
||||
|
||||
// Create the iconlist
|
||||
|
@ -573,21 +439,12 @@ async fn get_page(url: &str) -> Result<Response, Error> {
|
|||
}
|
||||
|
||||
async fn get_page_with_referer(url: &str, referer: &str) -> Result<Response, Error> {
|
||||
match check_domain_blacklist_reason(url::Url::parse(url).unwrap().host_str().unwrap_or_default()).await {
|
||||
Some(DomainBlacklistReason::Regex) => warn!("Favicon '{}' is from a blacklisted domain!", url),
|
||||
Some(DomainBlacklistReason::IP) => warn!("Favicon '{}' is hosted on a non-global IP!", url),
|
||||
None => (),
|
||||
}
|
||||
|
||||
let mut client = CLIENT.get(url);
|
||||
if !referer.is_empty() {
|
||||
client = client.header("Referer", referer)
|
||||
}
|
||||
|
||||
match client.send().await {
|
||||
Ok(c) => c.error_for_status().map_err(Into::into),
|
||||
Err(e) => err_silent!(format!("{e}")),
|
||||
}
|
||||
Ok(client.send().await?.error_for_status()?)
|
||||
}
|
||||
|
||||
/// Returns a Integer with the priority of the type of the icon which to prefer.
|
||||
|
@ -670,12 +527,6 @@ fn parse_sizes(sizes: &str) -> (u16, u16) {
|
|||
}
|
||||
|
||||
async fn download_icon(domain: &str) -> Result<(Bytes, Option<&str>), Error> {
|
||||
match check_domain_blacklist_reason(domain).await {
|
||||
Some(DomainBlacklistReason::Regex) => err_silent!("Domain is blacklisted", domain),
|
||||
Some(DomainBlacklistReason::IP) => err_silent!("Host resolves to a non-global IP", domain),
|
||||
None => (),
|
||||
}
|
||||
|
||||
let icon_result = get_icon_url(domain).await?;
|
||||
|
||||
let mut buffer = Bytes::new();
|
||||
|
@ -711,22 +562,19 @@ async fn download_icon(domain: &str) -> Result<(Bytes, Option<&str>), Error> {
|
|||
_ => debug!("Extracted icon from data:image uri is invalid"),
|
||||
};
|
||||
} else {
|
||||
match get_page_with_referer(&icon.href, &icon_result.referer).await {
|
||||
Ok(res) => {
|
||||
buffer = stream_to_bytes_limit(res, 5120 * 1024).await?; // 5120KB/5MB for each icon max (Same as icons.bitwarden.net)
|
||||
let res = get_page_with_referer(&icon.href, &icon_result.referer).await?;
|
||||
|
||||
// Check if the icon type is allowed, else try an icon from the list.
|
||||
icon_type = get_icon_type(&buffer);
|
||||
if icon_type.is_none() {
|
||||
buffer.clear();
|
||||
debug!("Icon from {}, is not a valid image type", icon.href);
|
||||
continue;
|
||||
}
|
||||
info!("Downloaded icon from {}", icon.href);
|
||||
break;
|
||||
}
|
||||
Err(e) => debug!("{:?}", e),
|
||||
};
|
||||
buffer = stream_to_bytes_limit(res, 5120 * 1024).await?; // 5120KB/5MB for each icon max (Same as icons.bitwarden.net)
|
||||
|
||||
// Check if the icon type is allowed, else try an icon from the list.
|
||||
icon_type = get_icon_type(&buffer);
|
||||
if icon_type.is_none() {
|
||||
buffer.clear();
|
||||
debug!("Icon from {}, is not a valid image type", icon.href);
|
||||
continue;
|
||||
}
|
||||
info!("Downloaded icon from {}", icon.href);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -295,7 +295,12 @@ async fn _password_login(
|
|||
"KdfIterations": user.client_kdf_iter,
|
||||
"KdfMemory": user.client_kdf_memory,
|
||||
"KdfParallelism": user.client_kdf_parallelism,
|
||||
"ResetMasterPassword": false,// TODO: Same as above
|
||||
"ResetMasterPassword": false, // TODO: Same as above
|
||||
"ForcePasswordReset": false,
|
||||
"MasterPasswordPolicy": {
|
||||
"object": "masterPasswordPolicy",
|
||||
},
|
||||
|
||||
"scope": scope,
|
||||
"unofficialServer": true,
|
||||
"UserDecryptionOptions": {
|
||||
|
|
|
@ -20,10 +20,10 @@ pub use crate::api::{
|
|||
core::two_factor::send_incomplete_2fa_notifications,
|
||||
core::{emergency_notification_reminder_job, emergency_request_timeout_job},
|
||||
core::{event_cleanup_job, events_routes as core_events_routes},
|
||||
icons::routes as icons_routes,
|
||||
icons::{is_domain_blacklisted, routes as icons_routes},
|
||||
identity::routes as identity_routes,
|
||||
notifications::routes as notifications_routes,
|
||||
notifications::{start_notification_server, AnonymousNotify, Notify, UpdateType, WS_ANONYMOUS_SUBSCRIPTIONS},
|
||||
notifications::{AnonymousNotify, Notify, UpdateType, WS_ANONYMOUS_SUBSCRIPTIONS, WS_USERS},
|
||||
push::{
|
||||
push_cipher_update, push_folder_update, push_logout, push_send_update, push_user_update, register_push_device,
|
||||
unregister_push_device,
|
||||
|
|
|
@ -1,23 +1,11 @@
|
|||
use std::{
|
||||
net::{IpAddr, SocketAddr},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use std::{net::IpAddr, sync::Arc, time::Duration};
|
||||
|
||||
use chrono::{NaiveDateTime, Utc};
|
||||
use rmpv::Value;
|
||||
use rocket::{
|
||||
futures::{SinkExt, StreamExt},
|
||||
Route,
|
||||
};
|
||||
use tokio::{
|
||||
net::{TcpListener, TcpStream},
|
||||
sync::mpsc::Sender,
|
||||
};
|
||||
use tokio_tungstenite::{
|
||||
accept_hdr_async,
|
||||
tungstenite::{handshake, Message},
|
||||
};
|
||||
use rocket::{futures::StreamExt, Route};
|
||||
use tokio::sync::mpsc::Sender;
|
||||
|
||||
use rocket_ws::{Message, WebSocket};
|
||||
|
||||
use crate::{
|
||||
auth::{ClientIp, WsAccessTokenHeader},
|
||||
|
@ -30,7 +18,7 @@ use crate::{
|
|||
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
static WS_USERS: Lazy<Arc<WebSocketUsers>> = Lazy::new(|| {
|
||||
pub static WS_USERS: Lazy<Arc<WebSocketUsers>> = Lazy::new(|| {
|
||||
Arc::new(WebSocketUsers {
|
||||
map: Arc::new(dashmap::DashMap::new()),
|
||||
})
|
||||
|
@ -47,8 +35,15 @@ use super::{
|
|||
push_send_update, push_user_update,
|
||||
};
|
||||
|
||||
static NOTIFICATIONS_DISABLED: Lazy<bool> = Lazy::new(|| !CONFIG.enable_websocket() && !CONFIG.push_enabled());
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![websockets_hub, anonymous_websockets_hub]
|
||||
if CONFIG.enable_websocket() {
|
||||
routes![websockets_hub, anonymous_websockets_hub]
|
||||
} else {
|
||||
info!("WebSocket are disabled, realtime sync functionality will not work!");
|
||||
routes![]
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
|
@ -108,7 +103,7 @@ impl Drop for WSAnonymousEntryMapGuard {
|
|||
|
||||
#[get("/hub?<data..>")]
|
||||
fn websockets_hub<'r>(
|
||||
ws: rocket_ws::WebSocket,
|
||||
ws: WebSocket,
|
||||
data: WsAccessToken,
|
||||
ip: ClientIp,
|
||||
header_token: WsAccessTokenHeader,
|
||||
|
@ -192,11 +187,7 @@ fn websockets_hub<'r>(
|
|||
}
|
||||
|
||||
#[get("/anonymous-hub?<token..>")]
|
||||
fn anonymous_websockets_hub<'r>(
|
||||
ws: rocket_ws::WebSocket,
|
||||
token: String,
|
||||
ip: ClientIp,
|
||||
) -> Result<rocket_ws::Stream!['r], Error> {
|
||||
fn anonymous_websockets_hub<'r>(ws: WebSocket, token: String, ip: ClientIp) -> Result<rocket_ws::Stream!['r], Error> {
|
||||
let addr = ip.ip;
|
||||
info!("Accepting Anonymous Rocket WS connection from {addr}");
|
||||
|
||||
|
@ -297,8 +288,8 @@ fn serialize(val: Value) -> Vec<u8> {
|
|||
}
|
||||
|
||||
fn serialize_date(date: NaiveDateTime) -> Value {
|
||||
let seconds: i64 = date.timestamp();
|
||||
let nanos: i64 = date.timestamp_subsec_nanos().into();
|
||||
let seconds: i64 = date.and_utc().timestamp();
|
||||
let nanos: i64 = date.and_utc().timestamp_subsec_nanos().into();
|
||||
let timestamp = nanos << 34 | seconds;
|
||||
|
||||
let bs = timestamp.to_be_bytes();
|
||||
|
@ -349,13 +340,19 @@ impl WebSocketUsers {
|
|||
|
||||
// NOTE: The last modified date needs to be updated before calling these methods
|
||||
pub async fn send_user_update(&self, ut: UpdateType, user: &User) {
|
||||
// Skip any processing if both WebSockets and Push are not active
|
||||
if *NOTIFICATIONS_DISABLED {
|
||||
return;
|
||||
}
|
||||
let data = create_update(
|
||||
vec![("UserId".into(), user.uuid.clone().into()), ("Date".into(), serialize_date(user.updated_at))],
|
||||
ut,
|
||||
None,
|
||||
);
|
||||
|
||||
self.send_update(&user.uuid, &data).await;
|
||||
if CONFIG.enable_websocket() {
|
||||
self.send_update(&user.uuid, &data).await;
|
||||
}
|
||||
|
||||
if CONFIG.push_enabled() {
|
||||
push_user_update(ut, user);
|
||||
|
@ -363,13 +360,19 @@ impl WebSocketUsers {
|
|||
}
|
||||
|
||||
pub async fn send_logout(&self, user: &User, acting_device_uuid: Option<String>) {
|
||||
// Skip any processing if both WebSockets and Push are not active
|
||||
if *NOTIFICATIONS_DISABLED {
|
||||
return;
|
||||
}
|
||||
let data = create_update(
|
||||
vec![("UserId".into(), user.uuid.clone().into()), ("Date".into(), serialize_date(user.updated_at))],
|
||||
UpdateType::LogOut,
|
||||
acting_device_uuid.clone(),
|
||||
);
|
||||
|
||||
self.send_update(&user.uuid, &data).await;
|
||||
if CONFIG.enable_websocket() {
|
||||
self.send_update(&user.uuid, &data).await;
|
||||
}
|
||||
|
||||
if CONFIG.push_enabled() {
|
||||
push_logout(user, acting_device_uuid);
|
||||
|
@ -383,6 +386,10 @@ impl WebSocketUsers {
|
|||
acting_device_uuid: &String,
|
||||
conn: &mut DbConn,
|
||||
) {
|
||||
// Skip any processing if both WebSockets and Push are not active
|
||||
if *NOTIFICATIONS_DISABLED {
|
||||
return;
|
||||
}
|
||||
let data = create_update(
|
||||
vec![
|
||||
("Id".into(), folder.uuid.clone().into()),
|
||||
|
@ -393,7 +400,9 @@ impl WebSocketUsers {
|
|||
Some(acting_device_uuid.into()),
|
||||
);
|
||||
|
||||
self.send_update(&folder.user_uuid, &data).await;
|
||||
if CONFIG.enable_websocket() {
|
||||
self.send_update(&folder.user_uuid, &data).await;
|
||||
}
|
||||
|
||||
if CONFIG.push_enabled() {
|
||||
push_folder_update(ut, folder, acting_device_uuid, conn).await;
|
||||
|
@ -409,6 +418,10 @@ impl WebSocketUsers {
|
|||
collection_uuids: Option<Vec<String>>,
|
||||
conn: &mut DbConn,
|
||||
) {
|
||||
// Skip any processing if both WebSockets and Push are not active
|
||||
if *NOTIFICATIONS_DISABLED {
|
||||
return;
|
||||
}
|
||||
let org_uuid = convert_option(cipher.organization_uuid.clone());
|
||||
// Depending if there are collections provided or not, we need to have different values for the following variables.
|
||||
// The user_uuid should be `null`, and the revision date should be set to now, else the clients won't sync the collection change.
|
||||
|
@ -434,8 +447,10 @@ impl WebSocketUsers {
|
|||
Some(acting_device_uuid.into()),
|
||||
);
|
||||
|
||||
for uuid in user_uuids {
|
||||
self.send_update(uuid, &data).await;
|
||||
if CONFIG.enable_websocket() {
|
||||
for uuid in user_uuids {
|
||||
self.send_update(uuid, &data).await;
|
||||
}
|
||||
}
|
||||
|
||||
if CONFIG.push_enabled() && user_uuids.len() == 1 {
|
||||
|
@ -451,6 +466,10 @@ impl WebSocketUsers {
|
|||
acting_device_uuid: &String,
|
||||
conn: &mut DbConn,
|
||||
) {
|
||||
// Skip any processing if both WebSockets and Push are not active
|
||||
if *NOTIFICATIONS_DISABLED {
|
||||
return;
|
||||
}
|
||||
let user_uuid = convert_option(send.user_uuid.clone());
|
||||
|
||||
let data = create_update(
|
||||
|
@ -463,8 +482,10 @@ impl WebSocketUsers {
|
|||
None,
|
||||
);
|
||||
|
||||
for uuid in user_uuids {
|
||||
self.send_update(uuid, &data).await;
|
||||
if CONFIG.enable_websocket() {
|
||||
for uuid in user_uuids {
|
||||
self.send_update(uuid, &data).await;
|
||||
}
|
||||
}
|
||||
if CONFIG.push_enabled() && user_uuids.len() == 1 {
|
||||
push_send_update(ut, send, acting_device_uuid, conn).await;
|
||||
|
@ -478,12 +499,18 @@ impl WebSocketUsers {
|
|||
acting_device_uuid: &String,
|
||||
conn: &mut DbConn,
|
||||
) {
|
||||
// Skip any processing if both WebSockets and Push are not active
|
||||
if *NOTIFICATIONS_DISABLED {
|
||||
return;
|
||||
}
|
||||
let data = create_update(
|
||||
vec![("Id".into(), auth_request_uuid.clone().into()), ("UserId".into(), user_uuid.clone().into())],
|
||||
UpdateType::AuthRequest,
|
||||
Some(acting_device_uuid.to_string()),
|
||||
);
|
||||
self.send_update(user_uuid, &data).await;
|
||||
if CONFIG.enable_websocket() {
|
||||
self.send_update(user_uuid, &data).await;
|
||||
}
|
||||
|
||||
if CONFIG.push_enabled() {
|
||||
push_auth_request(user_uuid.to_string(), auth_request_uuid.to_string(), conn).await;
|
||||
|
@ -497,12 +524,18 @@ impl WebSocketUsers {
|
|||
approving_device_uuid: String,
|
||||
conn: &mut DbConn,
|
||||
) {
|
||||
// Skip any processing if both WebSockets and Push are not active
|
||||
if *NOTIFICATIONS_DISABLED {
|
||||
return;
|
||||
}
|
||||
let data = create_update(
|
||||
vec![("Id".into(), auth_response_uuid.to_owned().into()), ("UserId".into(), user_uuid.clone().into())],
|
||||
UpdateType::AuthRequestResponse,
|
||||
approving_device_uuid.clone().into(),
|
||||
);
|
||||
self.send_update(auth_response_uuid, &data).await;
|
||||
if CONFIG.enable_websocket() {
|
||||
self.send_update(auth_response_uuid, &data).await;
|
||||
}
|
||||
|
||||
if CONFIG.push_enabled() {
|
||||
push_auth_response(user_uuid.to_string(), auth_response_uuid.to_string(), approving_device_uuid, conn)
|
||||
|
@ -526,6 +559,9 @@ impl AnonymousWebSocketSubscriptions {
|
|||
}
|
||||
|
||||
pub async fn send_auth_response(&self, user_uuid: &String, auth_response_uuid: &str) {
|
||||
if !CONFIG.enable_websocket() {
|
||||
return;
|
||||
}
|
||||
let data = create_anonymous_update(
|
||||
vec![("Id".into(), auth_response_uuid.to_owned().into()), ("UserId".into(), user_uuid.clone().into())],
|
||||
UpdateType::AuthRequestResponse,
|
||||
|
@ -620,127 +656,3 @@ pub enum UpdateType {
|
|||
|
||||
pub type Notify<'a> = &'a rocket::State<Arc<WebSocketUsers>>;
|
||||
pub type AnonymousNotify<'a> = &'a rocket::State<Arc<AnonymousWebSocketSubscriptions>>;
|
||||
|
||||
pub fn start_notification_server() -> Arc<WebSocketUsers> {
|
||||
let users = Arc::clone(&WS_USERS);
|
||||
if CONFIG.websocket_enabled() {
|
||||
let users2 = Arc::<WebSocketUsers>::clone(&users);
|
||||
tokio::spawn(async move {
|
||||
let addr = (CONFIG.websocket_address(), CONFIG.websocket_port());
|
||||
info!("Starting WebSockets server on {}:{}", addr.0, addr.1);
|
||||
let listener = TcpListener::bind(addr).await.expect("Can't listen on websocket port");
|
||||
|
||||
let (shutdown_tx, mut shutdown_rx) = tokio::sync::oneshot::channel::<()>();
|
||||
CONFIG.set_ws_shutdown_handle(shutdown_tx);
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
Ok((stream, addr)) = listener.accept() => {
|
||||
tokio::spawn(handle_connection(stream, Arc::<WebSocketUsers>::clone(&users2), addr));
|
||||
}
|
||||
|
||||
_ = &mut shutdown_rx => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!("Shutting down WebSockets server!")
|
||||
});
|
||||
}
|
||||
|
||||
users
|
||||
}
|
||||
|
||||
async fn handle_connection(stream: TcpStream, users: Arc<WebSocketUsers>, addr: SocketAddr) -> Result<(), Error> {
|
||||
let mut user_uuid: Option<String> = None;
|
||||
|
||||
info!("Accepting WS connection from {addr}");
|
||||
|
||||
// Accept connection, do initial handshake, validate auth token and get the user ID
|
||||
use handshake::server::{Request, Response};
|
||||
let mut stream = accept_hdr_async(stream, |req: &Request, res: Response| {
|
||||
if let Some(token) = get_request_token(req) {
|
||||
if let Ok(claims) = crate::auth::decode_login(&token) {
|
||||
user_uuid = Some(claims.sub);
|
||||
return Ok(res);
|
||||
}
|
||||
}
|
||||
Err(Response::builder().status(401).body(None).unwrap())
|
||||
})
|
||||
.await?;
|
||||
|
||||
let user_uuid = user_uuid.expect("User UUID should be set after the handshake");
|
||||
|
||||
let (mut rx, guard) = {
|
||||
// Add a channel to send messages to this client to the map
|
||||
let entry_uuid = uuid::Uuid::new_v4();
|
||||
let (tx, rx) = tokio::sync::mpsc::channel::<Message>(100);
|
||||
users.map.entry(user_uuid.clone()).or_default().push((entry_uuid, tx));
|
||||
|
||||
// Once the guard goes out of scope, the connection will have been closed and the entry will be deleted from the map
|
||||
(rx, WSEntryMapGuard::new(users, user_uuid, entry_uuid, addr.ip()))
|
||||
};
|
||||
|
||||
let _guard = guard;
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(15));
|
||||
loop {
|
||||
tokio::select! {
|
||||
res = stream.next() => {
|
||||
match res {
|
||||
Some(Ok(message)) => {
|
||||
match message {
|
||||
// Respond to any pings
|
||||
Message::Ping(ping) => stream.send(Message::Pong(ping)).await?,
|
||||
Message::Pong(_) => {/* Ignored */},
|
||||
|
||||
// We should receive an initial message with the protocol and version, and we will reply to it
|
||||
Message::Text(ref message) => {
|
||||
let msg = message.strip_suffix(RECORD_SEPARATOR as char).unwrap_or(message);
|
||||
|
||||
if serde_json::from_str(msg).ok() == Some(INITIAL_MESSAGE) {
|
||||
stream.send(Message::binary(INITIAL_RESPONSE)).await?;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Just echo anything else the client sends
|
||||
_ => stream.send(message).await?,
|
||||
}
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
|
||||
res = rx.recv() => {
|
||||
match res {
|
||||
Some(res) => stream.send(res).await?,
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
|
||||
_ = interval.tick() => stream.send(Message::Ping(create_ping())).await?
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_request_token(req: &handshake::server::Request) -> Option<String> {
|
||||
const ACCESS_TOKEN_KEY: &str = "access_token=";
|
||||
|
||||
if let Some(Ok(auth)) = req.headers().get("Authorization").map(|a| a.to_str()) {
|
||||
if let Some(token_part) = auth.strip_prefix("Bearer ") {
|
||||
return Some(token_part.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(params) = req.uri().query() {
|
||||
let params_iter = params.split('&').take(1);
|
||||
for val in params_iter {
|
||||
if let Some(stripped) = val.strip_prefix(ACCESS_TOKEN_KEY) {
|
||||
return Some(stripped.to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
|
|
@ -114,11 +114,11 @@ pub async fn register_push_device(device: &mut Device, conn: &mut crate::db::DbC
|
|||
.await?
|
||||
.error_for_status()
|
||||
{
|
||||
err!(format!("An error occured while proceeding registration of a device: {e}"));
|
||||
err!(format!("An error occurred while proceeding registration of a device: {e}"));
|
||||
}
|
||||
|
||||
if let Err(e) = device.save(conn).await {
|
||||
err!(format!("An error occured while trying to save the (registered) device push uuid: {e}"));
|
||||
err!(format!("An error occurred while trying to save the (registered) device push uuid: {e}"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -173,8 +173,8 @@ pub fn static_files(filename: &str) -> Result<(ContentType, &'static [u8]), Erro
|
|||
"jdenticon.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/jdenticon.js"))),
|
||||
"datatables.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/datatables.js"))),
|
||||
"datatables.css" => Ok((ContentType::CSS, include_bytes!("../static/scripts/datatables.css"))),
|
||||
"jquery-3.7.0.slim.js" => {
|
||||
Ok((ContentType::JavaScript, include_bytes!("../static/scripts/jquery-3.7.0.slim.js")))
|
||||
"jquery-3.7.1.slim.js" => {
|
||||
Ok((ContentType::JavaScript, include_bytes!("../static/scripts/jquery-3.7.1.slim.js")))
|
||||
}
|
||||
_ => err!(format!("Static file not found: {filename}")),
|
||||
}
|
||||
|
|
112
src/auth.rs
112
src/auth.rs
|
@ -1,10 +1,11 @@
|
|||
// JWT Handling
|
||||
//
|
||||
use chrono::{Duration, Utc};
|
||||
use chrono::{TimeDelta, Utc};
|
||||
use num_traits::FromPrimitive;
|
||||
use once_cell::sync::Lazy;
|
||||
use once_cell::sync::{Lazy, OnceCell};
|
||||
|
||||
use jsonwebtoken::{self, errors::ErrorKind, Algorithm, DecodingKey, EncodingKey, Header};
|
||||
use jsonwebtoken::{errors::ErrorKind, Algorithm, DecodingKey, EncodingKey, Header};
|
||||
use openssl::rsa::Rsa;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::ser::Serialize;
|
||||
|
||||
|
@ -12,7 +13,7 @@ use crate::{error::Error, CONFIG};
|
|||
|
||||
const JWT_ALGORITHM: Algorithm = Algorithm::RS256;
|
||||
|
||||
pub static DEFAULT_VALIDITY: Lazy<Duration> = Lazy::new(|| Duration::hours(2));
|
||||
pub static DEFAULT_VALIDITY: Lazy<TimeDelta> = Lazy::new(|| TimeDelta::try_hours(2).unwrap());
|
||||
static JWT_HEADER: Lazy<Header> = Lazy::new(|| Header::new(JWT_ALGORITHM));
|
||||
|
||||
pub static JWT_LOGIN_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|login", CONFIG.domain_origin()));
|
||||
|
@ -26,23 +27,46 @@ static JWT_SEND_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|send", CONFIG.do
|
|||
static JWT_ORG_API_KEY_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|api.organization", CONFIG.domain_origin()));
|
||||
static JWT_FILE_DOWNLOAD_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|file_download", CONFIG.domain_origin()));
|
||||
|
||||
static PRIVATE_RSA_KEY: Lazy<EncodingKey> = Lazy::new(|| {
|
||||
let key =
|
||||
std::fs::read(CONFIG.private_rsa_key()).unwrap_or_else(|e| panic!("Error loading private RSA Key. \n{e}"));
|
||||
EncodingKey::from_rsa_pem(&key).unwrap_or_else(|e| panic!("Error decoding private RSA Key.\n{e}"))
|
||||
});
|
||||
static PUBLIC_RSA_KEY: Lazy<DecodingKey> = Lazy::new(|| {
|
||||
let key = std::fs::read(CONFIG.public_rsa_key()).unwrap_or_else(|e| panic!("Error loading public RSA Key. \n{e}"));
|
||||
DecodingKey::from_rsa_pem(&key).unwrap_or_else(|e| panic!("Error decoding public RSA Key.\n{e}"))
|
||||
});
|
||||
static PRIVATE_RSA_KEY: OnceCell<EncodingKey> = OnceCell::new();
|
||||
static PUBLIC_RSA_KEY: OnceCell<DecodingKey> = OnceCell::new();
|
||||
|
||||
pub fn load_keys() {
|
||||
Lazy::force(&PRIVATE_RSA_KEY);
|
||||
Lazy::force(&PUBLIC_RSA_KEY);
|
||||
pub fn initialize_keys() -> Result<(), crate::error::Error> {
|
||||
let mut priv_key_buffer = Vec::with_capacity(2048);
|
||||
|
||||
let priv_key = {
|
||||
let mut priv_key_file =
|
||||
File::options().create(true).truncate(false).read(true).write(true).open(CONFIG.private_rsa_key())?;
|
||||
|
||||
#[allow(clippy::verbose_file_reads)]
|
||||
let bytes_read = priv_key_file.read_to_end(&mut priv_key_buffer)?;
|
||||
|
||||
if bytes_read > 0 {
|
||||
Rsa::private_key_from_pem(&priv_key_buffer[..bytes_read])?
|
||||
} else {
|
||||
// Only create the key if the file doesn't exist or is empty
|
||||
let rsa_key = openssl::rsa::Rsa::generate(2048)?;
|
||||
priv_key_buffer = rsa_key.private_key_to_pem()?;
|
||||
priv_key_file.write_all(&priv_key_buffer)?;
|
||||
info!("Private key created correctly.");
|
||||
rsa_key
|
||||
}
|
||||
};
|
||||
|
||||
let pub_key_buffer = priv_key.public_key_to_pem()?;
|
||||
|
||||
let enc = EncodingKey::from_rsa_pem(&priv_key_buffer)?;
|
||||
let dec: DecodingKey = DecodingKey::from_rsa_pem(&pub_key_buffer)?;
|
||||
if PRIVATE_RSA_KEY.set(enc).is_err() {
|
||||
err!("PRIVATE_RSA_KEY must only be initialized once")
|
||||
}
|
||||
if PUBLIC_RSA_KEY.set(dec).is_err() {
|
||||
err!("PUBLIC_RSA_KEY must only be initialized once")
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn encode_jwt<T: Serialize>(claims: &T) -> String {
|
||||
match jsonwebtoken::encode(&JWT_HEADER, claims, &PRIVATE_RSA_KEY) {
|
||||
match jsonwebtoken::encode(&JWT_HEADER, claims, PRIVATE_RSA_KEY.wait()) {
|
||||
Ok(token) => token,
|
||||
Err(e) => panic!("Error encoding jwt {e}"),
|
||||
}
|
||||
|
@ -56,7 +80,7 @@ fn decode_jwt<T: DeserializeOwned>(token: &str, issuer: String) -> Result<T, Err
|
|||
validation.set_issuer(&[issuer]);
|
||||
|
||||
let token = token.replace(char::is_whitespace, "");
|
||||
match jsonwebtoken::decode(&token, &PUBLIC_RSA_KEY, &validation) {
|
||||
match jsonwebtoken::decode(&token, PUBLIC_RSA_KEY.wait(), &validation) {
|
||||
Ok(d) => Ok(d.claims),
|
||||
Err(err) => match *err.kind() {
|
||||
ErrorKind::InvalidToken => err!("Token is invalid"),
|
||||
|
@ -164,11 +188,11 @@ pub fn generate_invite_claims(
|
|||
user_org_id: Option<String>,
|
||||
invited_by_email: Option<String>,
|
||||
) -> InviteJwtClaims {
|
||||
let time_now = Utc::now().naive_utc();
|
||||
let time_now = Utc::now();
|
||||
let expire_hours = i64::from(CONFIG.invitation_expiration_hours());
|
||||
InviteJwtClaims {
|
||||
nbf: time_now.timestamp(),
|
||||
exp: (time_now + Duration::hours(expire_hours)).timestamp(),
|
||||
exp: (time_now + TimeDelta::try_hours(expire_hours).unwrap()).timestamp(),
|
||||
iss: JWT_INVITE_ISSUER.to_string(),
|
||||
sub: uuid,
|
||||
email,
|
||||
|
@ -202,11 +226,11 @@ pub fn generate_emergency_access_invite_claims(
|
|||
grantor_name: String,
|
||||
grantor_email: String,
|
||||
) -> EmergencyAccessInviteJwtClaims {
|
||||
let time_now = Utc::now().naive_utc();
|
||||
let time_now = Utc::now();
|
||||
let expire_hours = i64::from(CONFIG.invitation_expiration_hours());
|
||||
EmergencyAccessInviteJwtClaims {
|
||||
nbf: time_now.timestamp(),
|
||||
exp: (time_now + Duration::hours(expire_hours)).timestamp(),
|
||||
exp: (time_now + TimeDelta::try_hours(expire_hours).unwrap()).timestamp(),
|
||||
iss: JWT_EMERGENCY_ACCESS_INVITE_ISSUER.to_string(),
|
||||
sub: uuid,
|
||||
email,
|
||||
|
@ -233,10 +257,10 @@ pub struct OrgApiKeyLoginJwtClaims {
|
|||
}
|
||||
|
||||
pub fn generate_organization_api_key_login_claims(uuid: String, org_id: String) -> OrgApiKeyLoginJwtClaims {
|
||||
let time_now = Utc::now().naive_utc();
|
||||
let time_now = Utc::now();
|
||||
OrgApiKeyLoginJwtClaims {
|
||||
nbf: time_now.timestamp(),
|
||||
exp: (time_now + Duration::hours(1)).timestamp(),
|
||||
exp: (time_now + TimeDelta::try_hours(1).unwrap()).timestamp(),
|
||||
iss: JWT_ORG_API_KEY_ISSUER.to_string(),
|
||||
sub: uuid,
|
||||
client_id: format!("organization.{org_id}"),
|
||||
|
@ -260,10 +284,10 @@ pub struct FileDownloadClaims {
|
|||
}
|
||||
|
||||
pub fn generate_file_download_claims(uuid: String, file_id: String) -> FileDownloadClaims {
|
||||
let time_now = Utc::now().naive_utc();
|
||||
let time_now = Utc::now();
|
||||
FileDownloadClaims {
|
||||
nbf: time_now.timestamp(),
|
||||
exp: (time_now + Duration::minutes(5)).timestamp(),
|
||||
exp: (time_now + TimeDelta::try_minutes(5).unwrap()).timestamp(),
|
||||
iss: JWT_FILE_DOWNLOAD_ISSUER.to_string(),
|
||||
sub: uuid,
|
||||
file_id,
|
||||
|
@ -283,42 +307,42 @@ pub struct BasicJwtClaims {
|
|||
}
|
||||
|
||||
pub fn generate_delete_claims(uuid: String) -> BasicJwtClaims {
|
||||
let time_now = Utc::now().naive_utc();
|
||||
let time_now = Utc::now();
|
||||
let expire_hours = i64::from(CONFIG.invitation_expiration_hours());
|
||||
BasicJwtClaims {
|
||||
nbf: time_now.timestamp(),
|
||||
exp: (time_now + Duration::hours(expire_hours)).timestamp(),
|
||||
exp: (time_now + TimeDelta::try_hours(expire_hours).unwrap()).timestamp(),
|
||||
iss: JWT_DELETE_ISSUER.to_string(),
|
||||
sub: uuid,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_verify_email_claims(uuid: String) -> BasicJwtClaims {
|
||||
let time_now = Utc::now().naive_utc();
|
||||
let time_now = Utc::now();
|
||||
let expire_hours = i64::from(CONFIG.invitation_expiration_hours());
|
||||
BasicJwtClaims {
|
||||
nbf: time_now.timestamp(),
|
||||
exp: (time_now + Duration::hours(expire_hours)).timestamp(),
|
||||
exp: (time_now + TimeDelta::try_hours(expire_hours).unwrap()).timestamp(),
|
||||
iss: JWT_VERIFYEMAIL_ISSUER.to_string(),
|
||||
sub: uuid,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_admin_claims() -> BasicJwtClaims {
|
||||
let time_now = Utc::now().naive_utc();
|
||||
let time_now = Utc::now();
|
||||
BasicJwtClaims {
|
||||
nbf: time_now.timestamp(),
|
||||
exp: (time_now + Duration::minutes(CONFIG.admin_session_lifetime())).timestamp(),
|
||||
exp: (time_now + TimeDelta::try_minutes(CONFIG.admin_session_lifetime()).unwrap()).timestamp(),
|
||||
iss: JWT_ADMIN_ISSUER.to_string(),
|
||||
sub: "admin_panel".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_send_claims(send_id: &str, file_id: &str) -> BasicJwtClaims {
|
||||
let time_now = Utc::now().naive_utc();
|
||||
let time_now = Utc::now();
|
||||
BasicJwtClaims {
|
||||
nbf: time_now.timestamp(),
|
||||
exp: (time_now + Duration::minutes(2)).timestamp(),
|
||||
exp: (time_now + TimeDelta::try_minutes(2).unwrap()).timestamp(),
|
||||
iss: JWT_SEND_ISSUER.to_string(),
|
||||
sub: format!("{send_id}/{file_id}"),
|
||||
}
|
||||
|
@ -367,10 +391,8 @@ impl<'r> FromRequest<'r> for Host {
|
|||
|
||||
let host = if let Some(host) = headers.get_one("X-Forwarded-Host") {
|
||||
host
|
||||
} else if let Some(host) = headers.get_one("Host") {
|
||||
host
|
||||
} else {
|
||||
""
|
||||
headers.get_one("Host").unwrap_or_default()
|
||||
};
|
||||
|
||||
format!("{protocol}://{host}")
|
||||
|
@ -475,7 +497,7 @@ impl<'r> FromRequest<'r> for Headers {
|
|||
// Check if the stamp exception has expired first.
|
||||
// Then, check if the current route matches any of the allowed routes.
|
||||
// After that check the stamp in exception matches the one in the claims.
|
||||
if Utc::now().naive_utc().timestamp() > stamp_exception.expire {
|
||||
if Utc::now().timestamp() > stamp_exception.expire {
|
||||
// If the stamp exception has been expired remove it from the database.
|
||||
// This prevents checking this stamp exception for new requests.
|
||||
let mut user = user;
|
||||
|
@ -667,7 +689,7 @@ impl<'r> FromRequest<'r> for ManagerHeaders {
|
|||
_ => err_handler!("Error getting DB"),
|
||||
};
|
||||
|
||||
if !can_access_collection(&headers.org_user, &col_id, &mut conn).await {
|
||||
if !Collection::can_access_collection(&headers.org_user, &col_id, &mut conn).await {
|
||||
err_handler!("The current user isn't a manager for this collection")
|
||||
}
|
||||
}
|
||||
|
@ -740,10 +762,6 @@ impl From<ManagerHeadersLoose> for Headers {
|
|||
}
|
||||
}
|
||||
}
|
||||
async fn can_access_collection(org_user: &UserOrganization, col_id: &str, conn: &mut DbConn) -> bool {
|
||||
org_user.has_full_access()
|
||||
|| Collection::has_access_by_collection_and_user_uuid(col_id, &org_user.user_uuid, conn).await
|
||||
}
|
||||
|
||||
impl ManagerHeaders {
|
||||
pub async fn from_loose(
|
||||
|
@ -755,7 +773,7 @@ impl ManagerHeaders {
|
|||
if uuid::Uuid::parse_str(col_id).is_err() {
|
||||
err!("Collection Id is malformed!");
|
||||
}
|
||||
if !can_access_collection(&h.org_user, col_id, conn).await {
|
||||
if !Collection::can_access_collection(&h.org_user, col_id, conn).await {
|
||||
err!("You don't have access to all collections!");
|
||||
}
|
||||
}
|
||||
|
@ -799,7 +817,11 @@ impl<'r> FromRequest<'r> for OwnerHeaders {
|
|||
//
|
||||
// Client IP address detection
|
||||
//
|
||||
use std::net::IpAddr;
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{Read, Write},
|
||||
net::IpAddr,
|
||||
};
|
||||
|
||||
pub struct ClientIp {
|
||||
pub ip: IpAddr,
|
||||
|
|
|
@ -39,7 +39,6 @@ macro_rules! make_config {
|
|||
|
||||
struct Inner {
|
||||
rocket_shutdown_handle: Option<rocket::Shutdown>,
|
||||
ws_shutdown_handle: Option<tokio::sync::oneshot::Sender<()>>,
|
||||
|
||||
templates: Handlebars<'static>,
|
||||
config: ConfigItems,
|
||||
|
@ -361,7 +360,7 @@ make_config! {
|
|||
/// Sends folder
|
||||
sends_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "sends");
|
||||
/// Temp folder |> Used for storing temporary file uploads
|
||||
tmp_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "tmp");
|
||||
tmp_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "tmp");
|
||||
/// Templates folder
|
||||
templates_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "templates");
|
||||
/// Session JWT key
|
||||
|
@ -371,11 +370,7 @@ make_config! {
|
|||
},
|
||||
ws {
|
||||
/// Enable websocket notifications
|
||||
websocket_enabled: bool, false, def, false;
|
||||
/// Websocket address
|
||||
websocket_address: String, false, def, "0.0.0.0".to_string();
|
||||
/// Websocket port
|
||||
websocket_port: u16, false, def, 3012;
|
||||
enable_websocket: bool, false, def, true;
|
||||
},
|
||||
push {
|
||||
/// Enable push notifications
|
||||
|
@ -691,6 +686,10 @@ make_config! {
|
|||
email_expiration_time: u64, true, def, 600;
|
||||
/// Maximum attempts |> Maximum attempts before an email token is reset and a new email will need to be sent
|
||||
email_attempts_limit: u64, true, def, 3;
|
||||
/// Automatically enforce at login |> Setup email 2FA provider 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
|
||||
email_2fa_auto_fallback: bool, true, def, false;
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -893,6 +892,13 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
|
|||
err!("To enable email 2FA, a mail transport must be configured")
|
||||
}
|
||||
|
||||
if !cfg._enable_email_2fa && cfg.email_2fa_enforce_on_verified_invite {
|
||||
err!("To enforce email 2FA on verified invitations, email 2fa has to be enabled!");
|
||||
}
|
||||
if !cfg._enable_email_2fa && cfg.email_2fa_auto_fallback {
|
||||
err!("To use email 2FA as automatic fallback, email 2fa has to be enabled!");
|
||||
}
|
||||
|
||||
// Check if the icon blacklist regex is valid
|
||||
if let Some(ref r) = cfg.icon_blacklist_regex {
|
||||
let validate_regex = regex::Regex::new(r);
|
||||
|
@ -1071,7 +1077,6 @@ impl Config {
|
|||
Ok(Config {
|
||||
inner: RwLock::new(Inner {
|
||||
rocket_shutdown_handle: None,
|
||||
ws_shutdown_handle: None,
|
||||
templates: load_templates(&config.templates_folder),
|
||||
config,
|
||||
_env,
|
||||
|
@ -1164,7 +1169,7 @@ impl Config {
|
|||
}
|
||||
|
||||
pub fn delete_user_config(&self) -> Result<(), Error> {
|
||||
crate::util::delete_file(&CONFIG_FILE)?;
|
||||
std::fs::remove_file(&*CONFIG_FILE)?;
|
||||
|
||||
// Empty user config
|
||||
let usr = ConfigBuilder::default();
|
||||
|
@ -1189,9 +1194,6 @@ impl Config {
|
|||
pub fn private_rsa_key(&self) -> String {
|
||||
format!("{}.pem", CONFIG.rsa_key_filename())
|
||||
}
|
||||
pub fn public_rsa_key(&self) -> String {
|
||||
format!("{}.pub.pem", CONFIG.rsa_key_filename())
|
||||
}
|
||||
pub fn mail_enabled(&self) -> bool {
|
||||
let inner = &self.inner.read().unwrap().config;
|
||||
inner._enable_smtp && (inner.smtp_host.is_some() || inner.use_sendmail)
|
||||
|
@ -1240,16 +1242,8 @@ impl Config {
|
|||
self.inner.write().unwrap().rocket_shutdown_handle = Some(handle);
|
||||
}
|
||||
|
||||
pub fn set_ws_shutdown_handle(&self, handle: tokio::sync::oneshot::Sender<()>) {
|
||||
self.inner.write().unwrap().ws_shutdown_handle = Some(handle);
|
||||
}
|
||||
|
||||
pub fn shutdown(&self) {
|
||||
if let Ok(mut c) = self.inner.write() {
|
||||
if let Some(handle) = c.ws_shutdown_handle.take() {
|
||||
handle.send(()).ok();
|
||||
}
|
||||
|
||||
if let Some(handle) = c.rocket_shutdown_handle.take() {
|
||||
handle.notify();
|
||||
}
|
||||
|
|
|
@ -103,7 +103,7 @@ impl Attachment {
|
|||
|
||||
let file_path = &self.get_file_path();
|
||||
|
||||
match crate::util::delete_file(file_path) {
|
||||
match std::fs::remove_file(file_path) {
|
||||
// Ignore "file not found" errors. This can happen when the
|
||||
// upstream caller has already cleaned up the file as part of
|
||||
// its own error handling.
|
||||
|
|
|
@ -140,7 +140,7 @@ impl AuthRequest {
|
|||
}
|
||||
|
||||
pub async fn purge_expired_auth_requests(conn: &mut DbConn) {
|
||||
let expiry_time = Utc::now().naive_utc() - chrono::Duration::minutes(5); //after 5 minutes, clients reject the request
|
||||
let expiry_time = Utc::now().naive_utc() - chrono::TimeDelta::try_minutes(5).unwrap(); //after 5 minutes, clients reject the request
|
||||
for auth_request in Self::find_created_before(&expiry_time, conn).await {
|
||||
auth_request.delete(conn).await.ok();
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use crate::CONFIG;
|
||||
use chrono::{Duration, NaiveDateTime, Utc};
|
||||
use chrono::{NaiveDateTime, TimeDelta, Utc};
|
||||
use serde_json::Value;
|
||||
|
||||
use super::{
|
||||
|
@ -361,7 +361,7 @@ impl Cipher {
|
|||
pub async fn purge_trash(conn: &mut DbConn) {
|
||||
if let Some(auto_delete_days) = CONFIG.trash_auto_delete_days() {
|
||||
let now = Utc::now().naive_utc();
|
||||
let dt = now - Duration::days(auto_delete_days);
|
||||
let dt = now - TimeDelta::try_days(auto_delete_days).unwrap();
|
||||
for cipher in Self::find_deleted_before(&dt, conn).await {
|
||||
cipher.delete(conn).await.ok();
|
||||
}
|
||||
|
@ -431,7 +431,7 @@ impl Cipher {
|
|||
}
|
||||
if let Some(ref org_uuid) = self.organization_uuid {
|
||||
if let Some(cipher_sync_data) = cipher_sync_data {
|
||||
return cipher_sync_data.user_group_full_access_for_organizations.get(org_uuid).is_some();
|
||||
return cipher_sync_data.user_group_full_access_for_organizations.contains(org_uuid);
|
||||
} else {
|
||||
return Group::is_in_full_access_group(user_uuid, org_uuid, conn).await;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use serde_json::Value;
|
||||
|
||||
use super::{CollectionGroup, User, UserOrgStatus, UserOrgType, UserOrganization};
|
||||
use super::{CollectionGroup, GroupUser, User, UserOrgStatus, UserOrgType, UserOrganization};
|
||||
use crate::CONFIG;
|
||||
|
||||
db_object! {
|
||||
|
@ -102,6 +102,15 @@ impl Collection {
|
|||
json_object["HidePasswords"] = json!(hide_passwords);
|
||||
json_object
|
||||
}
|
||||
|
||||
pub async fn can_access_collection(org_user: &UserOrganization, col_id: &str, conn: &mut DbConn) -> bool {
|
||||
org_user.has_status(UserOrgStatus::Confirmed)
|
||||
&& (org_user.has_full_access()
|
||||
|| CollectionUser::has_access_to_collection_by_user(col_id, &org_user.user_uuid, conn).await
|
||||
|| (CONFIG.org_groups_enabled()
|
||||
&& (GroupUser::has_full_access_by_member(&org_user.org_uuid, &org_user.uuid, conn).await
|
||||
|| GroupUser::has_access_to_collection_by_member(col_id, &org_user.uuid, conn).await)))
|
||||
}
|
||||
}
|
||||
|
||||
use crate::db::DbConn;
|
||||
|
@ -252,17 +261,6 @@ impl Collection {
|
|||
}
|
||||
}
|
||||
|
||||
// Check if a user has access to a specific collection
|
||||
// FIXME: This needs to be reviewed. The query used by `find_by_user_uuid` could be adjusted to filter when needed.
|
||||
// For now this is a good solution without making to much changes.
|
||||
pub async fn has_access_by_collection_and_user_uuid(
|
||||
collection_uuid: &str,
|
||||
user_uuid: &str,
|
||||
conn: &mut DbConn,
|
||||
) -> bool {
|
||||
Self::find_by_user_uuid(user_uuid.to_owned(), conn).await.into_iter().any(|c| c.uuid == collection_uuid)
|
||||
}
|
||||
|
||||
pub async fn find_by_organization_and_user_uuid(org_uuid: &str, user_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
|
||||
Self::find_by_user_uuid(user_uuid.to_owned(), conn)
|
||||
.await
|
||||
|
@ -644,6 +642,10 @@ impl CollectionUser {
|
|||
Ok(())
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn has_access_to_collection_by_user(col_id: &str, user_uuid: &str, conn: &mut DbConn) -> bool {
|
||||
Self::find_by_collection_and_user(col_id, user_uuid, conn).await.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
/// Database methods
|
||||
|
|
|
@ -67,8 +67,8 @@ impl Device {
|
|||
}
|
||||
|
||||
// Update the expiration of the device and the last update date
|
||||
let time_now = Utc::now().naive_utc();
|
||||
self.updated_at = time_now;
|
||||
let time_now = Utc::now();
|
||||
self.updated_at = time_now.naive_utc();
|
||||
|
||||
// ---
|
||||
// Disabled these keys to be added to the JWT since they could cause the JWT to get too large
|
||||
|
|
|
@ -81,25 +81,32 @@ impl EmergencyAccess {
|
|||
})
|
||||
}
|
||||
|
||||
pub async fn to_json_grantee_details(&self, conn: &mut DbConn) -> Value {
|
||||
pub async fn to_json_grantee_details(&self, conn: &mut DbConn) -> Option<Value> {
|
||||
let grantee_user = if let Some(grantee_uuid) = self.grantee_uuid.as_deref() {
|
||||
Some(User::find_by_uuid(grantee_uuid, conn).await.expect("Grantee user not found."))
|
||||
User::find_by_uuid(grantee_uuid, conn).await.expect("Grantee user not found.")
|
||||
} else if let Some(email) = self.email.as_deref() {
|
||||
Some(User::find_by_mail(email, conn).await.expect("Grantee user not found."))
|
||||
match User::find_by_mail(email, conn).await {
|
||||
Some(user) => user,
|
||||
None => {
|
||||
// remove outstanding invitations which should not exist
|
||||
let _ = Self::delete_all_by_grantee_email(email, conn).await;
|
||||
return None;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
return None;
|
||||
};
|
||||
|
||||
json!({
|
||||
Some(json!({
|
||||
"Id": self.uuid,
|
||||
"Status": self.status,
|
||||
"Type": self.atype,
|
||||
"WaitTimeDays": self.wait_time_days,
|
||||
"GranteeId": grantee_user.as_ref().map_or("", |u| &u.uuid),
|
||||
"Email": grantee_user.as_ref().map_or("", |u| &u.email),
|
||||
"Name": grantee_user.as_ref().map_or("", |u| &u.name),
|
||||
"GranteeId": grantee_user.uuid,
|
||||
"Email": grantee_user.email,
|
||||
"Name": grantee_user.name,
|
||||
"Object": "emergencyAccessGranteeDetails",
|
||||
})
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -174,7 +181,7 @@ impl EmergencyAccess {
|
|||
// 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;
|
||||
self.updated_at = date.to_owned();
|
||||
date.clone_into(&mut self.updated_at);
|
||||
|
||||
db_run! {conn: {
|
||||
crate::util::retry(|| {
|
||||
|
@ -192,7 +199,7 @@ impl EmergencyAccess {
|
|||
conn: &mut DbConn,
|
||||
) -> EmptyResult {
|
||||
self.last_notification_at = Some(date.to_owned());
|
||||
self.updated_at = date.to_owned();
|
||||
date.clone_into(&mut self.updated_at);
|
||||
|
||||
db_run! {conn: {
|
||||
crate::util::retry(|| {
|
||||
|
@ -214,6 +221,13 @@ impl EmergencyAccess {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_all_by_grantee_email(grantee_email: &str, conn: &mut DbConn) -> EmptyResult {
|
||||
for ea in Self::find_all_invited_by_grantee_email(grantee_email, conn).await {
|
||||
ea.delete(conn).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete(self, conn: &mut DbConn) -> EmptyResult {
|
||||
User::update_uuid_revision(&self.grantor_uuid, conn).await;
|
||||
|
||||
|
@ -285,6 +299,15 @@ impl EmergencyAccess {
|
|||
}}
|
||||
}
|
||||
|
||||
pub async fn find_all_invited_by_grantee_email(grantee_email: &str, conn: &mut DbConn) -> Vec<Self> {
|
||||
db_run! { conn: {
|
||||
emergency_access::table
|
||||
.filter(emergency_access::email.eq(grantee_email))
|
||||
.filter(emergency_access::status.eq(EmergencyAccessStatus::Invited as i32))
|
||||
.load::<EmergencyAccessDb>(conn).expect("Error loading emergency_access").from_db()
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn find_all_by_grantor_uuid(grantor_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
|
||||
db_run! { conn: {
|
||||
emergency_access::table
|
||||
|
@ -292,6 +315,21 @@ impl EmergencyAccess {
|
|||
.load::<EmergencyAccessDb>(conn).expect("Error loading emergency_access").from_db()
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn accept_invite(&mut self, grantee_uuid: &str, grantee_email: &str, conn: &mut DbConn) -> EmptyResult {
|
||||
if self.email.is_none() || self.email.as_ref().unwrap() != grantee_email {
|
||||
err!("User email does not match invite.");
|
||||
}
|
||||
|
||||
if self.status == EmergencyAccessStatus::Accepted as i32 {
|
||||
err!("Emergency contact already accepted.");
|
||||
}
|
||||
|
||||
self.status = EmergencyAccessStatus::Accepted as i32;
|
||||
self.grantee_uuid = Some(String::from(grantee_uuid));
|
||||
self.email = None;
|
||||
self.save(conn).await
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
|
|
@ -3,7 +3,7 @@ use serde_json::Value;
|
|||
|
||||
use crate::{api::EmptyResult, error::MapResult, CONFIG};
|
||||
|
||||
use chrono::{Duration, NaiveDateTime, Utc};
|
||||
use chrono::{NaiveDateTime, TimeDelta, Utc};
|
||||
|
||||
// https://bitwarden.com/help/event-logs/
|
||||
|
||||
|
@ -316,7 +316,7 @@ impl Event {
|
|||
|
||||
pub async fn clean_events(conn: &mut DbConn) -> EmptyResult {
|
||||
if let Some(days_to_retain) = CONFIG.events_days_retain() {
|
||||
let dt = Utc::now().naive_utc() - Duration::days(days_to_retain);
|
||||
let dt = Utc::now().naive_utc() - TimeDelta::try_days(days_to_retain).unwrap();
|
||||
db_run! { conn: {
|
||||
diesel::delete(event::table.filter(event::event_date.lt(dt)))
|
||||
.execute(conn)
|
||||
|
|
|
@ -486,6 +486,39 @@ impl GroupUser {
|
|||
}}
|
||||
}
|
||||
|
||||
pub async fn has_access_to_collection_by_member(
|
||||
collection_uuid: &str,
|
||||
member_uuid: &str,
|
||||
conn: &mut DbConn,
|
||||
) -> bool {
|
||||
db_run! { conn: {
|
||||
groups_users::table
|
||||
.inner_join(collections_groups::table.on(
|
||||
collections_groups::groups_uuid.eq(groups_users::groups_uuid)
|
||||
))
|
||||
.filter(collections_groups::collections_uuid.eq(collection_uuid))
|
||||
.filter(groups_users::users_organizations_uuid.eq(member_uuid))
|
||||
.count()
|
||||
.first::<i64>(conn)
|
||||
.unwrap_or(0) != 0
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn has_full_access_by_member(org_uuid: &str, member_uuid: &str, conn: &mut DbConn) -> bool {
|
||||
db_run! { conn: {
|
||||
groups_users::table
|
||||
.inner_join(groups::table.on(
|
||||
groups::uuid.eq(groups_users::groups_uuid)
|
||||
))
|
||||
.filter(groups::organizations_uuid.eq(org_uuid))
|
||||
.filter(groups::access_all.eq(true))
|
||||
.filter(groups_users::users_organizations_uuid.eq(member_uuid))
|
||||
.count()
|
||||
.first::<i64>(conn)
|
||||
.unwrap_or(0) != 0
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn update_user_revision(&self, conn: &mut DbConn) {
|
||||
match UserOrganization::find_by_uuid(&self.users_organizations_uuid, conn).await {
|
||||
Some(user) => User::update_uuid_revision(&user.user_uuid, conn).await,
|
||||
|
|
|
@ -340,4 +340,11 @@ impl OrgPolicy {
|
|||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub async fn is_enabled_by_org(org_uuid: &str, policy_type: OrgPolicyType, conn: &mut DbConn) -> bool {
|
||||
if let Some(policy) = OrgPolicy::find_by_org_and_type(org_uuid, policy_type, conn).await {
|
||||
return policy.enabled;
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -344,6 +344,25 @@ impl UserOrganization {
|
|||
pub async fn to_json(&self, conn: &mut DbConn) -> Value {
|
||||
let org = Organization::find_by_uuid(&self.org_uuid, conn).await.unwrap();
|
||||
|
||||
let permissions = json!({
|
||||
// TODO: Add support for Custom User Roles
|
||||
// See: https://bitwarden.com/help/article/user-types-access-control/#custom-role
|
||||
"accessEventLogs": false,
|
||||
"accessImportExport": false,
|
||||
"accessReports": false,
|
||||
"createNewCollections": false,
|
||||
"editAnyCollection": false,
|
||||
"deleteAnyCollection": false,
|
||||
"editAssignedCollections": false,
|
||||
"deleteAssignedCollections": false,
|
||||
"manageGroups": false,
|
||||
"managePolicies": false,
|
||||
"manageSso": false, // Not supported
|
||||
"manageUsers": false,
|
||||
"manageResetPassword": false,
|
||||
"manageScim": false // Not supported (Not AGPLv3 Licensed)
|
||||
});
|
||||
|
||||
// https://github.com/bitwarden/server/blob/13d1e74d6960cf0d042620b72d85bf583a4236f7/src/Api/Models/Response/ProfileOrganizationResponseModel.cs
|
||||
json!({
|
||||
"Id": self.org_uuid,
|
||||
|
@ -371,27 +390,7 @@ impl UserOrganization {
|
|||
// "KeyConnectorEnabled": false,
|
||||
// "KeyConnectorUrl": null,
|
||||
|
||||
// TODO: Add support for Custom User Roles
|
||||
// See: https://bitwarden.com/help/article/user-types-access-control/#custom-role
|
||||
// "Permissions": {
|
||||
// "AccessEventLogs": false,
|
||||
// "AccessImportExport": false,
|
||||
// "AccessReports": false,
|
||||
// "ManageAllCollections": false,
|
||||
// "CreateNewCollections": false,
|
||||
// "EditAnyCollection": false,
|
||||
// "DeleteAnyCollection": false,
|
||||
// "ManageAssignedCollections": false,
|
||||
// "editAssignedCollections": false,
|
||||
// "deleteAssignedCollections": false,
|
||||
// "ManageCiphers": false,
|
||||
// "ManageGroups": false,
|
||||
// "ManagePolicies": false,
|
||||
// "ManageResetPassword": false,
|
||||
// "ManageSso": false, // Not supported
|
||||
// "ManageUsers": false,
|
||||
// "ManageScim": false, // Not supported (Not AGPLv3 Licensed)
|
||||
// },
|
||||
"permissions": permissions,
|
||||
|
||||
"MaxStorageGb": 10, // The value doesn't matter, we don't check server-side
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ db_object! {
|
|||
pub atype: i32,
|
||||
pub enabled: bool,
|
||||
pub data: String,
|
||||
pub last_used: i32,
|
||||
pub last_used: i64,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use chrono::{Duration, NaiveDateTime, Utc};
|
||||
use chrono::{NaiveDateTime, TimeDelta, Utc};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::crypto;
|
||||
|
@ -202,7 +202,7 @@ impl User {
|
|||
let stamp_exception = UserStampException {
|
||||
routes: route_exception,
|
||||
security_stamp: self.security_stamp.clone(),
|
||||
expire: (Utc::now().naive_utc() + Duration::minutes(2)).timestamp(),
|
||||
expire: (Utc::now() + TimeDelta::try_minutes(2).unwrap()).timestamp(),
|
||||
};
|
||||
self.stamp_exception = Some(serde_json::to_string(&stamp_exception).unwrap_or_default());
|
||||
}
|
||||
|
@ -246,6 +246,7 @@ impl User {
|
|||
"Email": self.email,
|
||||
"EmailVerified": !CONFIG.mail_enabled() || self.verified_at.is_some(),
|
||||
"Premium": true,
|
||||
"PremiumFromOrganization": false,
|
||||
"MasterPasswordHint": self.password_hint,
|
||||
"Culture": "en-US",
|
||||
"TwoFactorEnabled": twofactor_enabled,
|
||||
|
@ -257,6 +258,7 @@ impl User {
|
|||
"ProviderOrganizations": [],
|
||||
"ForcePasswordReset": false,
|
||||
"AvatarColor": self.avatar_color,
|
||||
"UsesKeyConnector": false,
|
||||
"Object": "profile",
|
||||
})
|
||||
}
|
||||
|
@ -311,6 +313,7 @@ impl User {
|
|||
|
||||
Send::delete_all_by_user(&self.uuid, conn).await?;
|
||||
EmergencyAccess::delete_all_by_user(&self.uuid, conn).await?;
|
||||
EmergencyAccess::delete_all_by_grantee_email(&self.email, conn).await?;
|
||||
UserOrganization::delete_all_by_user(&self.uuid, conn).await?;
|
||||
Cipher::delete_all_by_user(&self.uuid, conn).await?;
|
||||
Favorite::delete_all_by_user(&self.uuid, conn).await?;
|
||||
|
|
|
@ -160,7 +160,7 @@ table! {
|
|||
atype -> Integer,
|
||||
enabled -> Bool,
|
||||
data -> Text,
|
||||
last_used -> Integer,
|
||||
last_used -> BigInt,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -160,7 +160,7 @@ table! {
|
|||
atype -> Integer,
|
||||
enabled -> Bool,
|
||||
data -> Text,
|
||||
last_used -> Integer,
|
||||
last_used -> BigInt,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -160,7 +160,7 @@ table! {
|
|||
atype -> Integer,
|
||||
enabled -> Bool,
|
||||
data -> Text,
|
||||
last_used -> Integer,
|
||||
last_used -> BigInt,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -52,7 +52,6 @@ use rocket::error::Error as RocketErr;
|
|||
use serde_json::{Error as SerdeErr, Value};
|
||||
use std::io::Error as IoErr;
|
||||
use std::time::SystemTimeError as TimeErr;
|
||||
use tokio_tungstenite::tungstenite::Error as TungstError;
|
||||
use webauthn_rs::error::WebauthnError as WebauthnErr;
|
||||
use yubico::yubicoerror::YubicoError as YubiErr;
|
||||
|
||||
|
@ -91,7 +90,6 @@ make_error! {
|
|||
|
||||
DieselCon(DieselConErr): _has_source, _api_error,
|
||||
Webauthn(WebauthnErr): _has_source, _api_error,
|
||||
WebSocket(TungstError): _has_source, _api_error,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Error {
|
||||
|
|
92
src/main.rs
92
src/main.rs
|
@ -1,40 +1,9 @@
|
|||
#![forbid(unsafe_code, non_ascii_idents)]
|
||||
#![deny(
|
||||
rust_2018_idioms,
|
||||
rust_2021_compatibility,
|
||||
noop_method_call,
|
||||
pointer_structural_match,
|
||||
trivial_casts,
|
||||
trivial_numeric_casts,
|
||||
unused_import_braces,
|
||||
clippy::cast_lossless,
|
||||
clippy::clone_on_ref_ptr,
|
||||
clippy::equatable_if_let,
|
||||
clippy::float_cmp_const,
|
||||
clippy::inefficient_to_string,
|
||||
clippy::iter_on_empty_collections,
|
||||
clippy::iter_on_single_items,
|
||||
clippy::linkedlist,
|
||||
clippy::macro_use_imports,
|
||||
clippy::manual_assert,
|
||||
clippy::manual_instant_elapsed,
|
||||
clippy::manual_string_new,
|
||||
clippy::match_wildcard_for_single_variants,
|
||||
clippy::mem_forget,
|
||||
clippy::string_add_assign,
|
||||
clippy::string_to_string,
|
||||
clippy::unnecessary_join,
|
||||
clippy::unnecessary_self_imports,
|
||||
clippy::unused_async,
|
||||
clippy::verbose_file_reads,
|
||||
clippy::zero_sized_map_values
|
||||
)]
|
||||
#![cfg_attr(feature = "unstable", feature(ip))]
|
||||
// The recursion_limit is mainly triggered by the json!() macro.
|
||||
// The more key/value pairs there are the more recursion occurs.
|
||||
// We want to keep this as low as possible, but not higher then 128.
|
||||
// If you go above 128 it will cause rust-analyzer to fail,
|
||||
#![recursion_limit = "103"]
|
||||
#![recursion_limit = "90"]
|
||||
|
||||
// When enabled use MiMalloc as malloc instead of the default malloc
|
||||
#[cfg(feature = "enable_mimalloc")]
|
||||
|
@ -83,12 +52,12 @@ mod ratelimit;
|
|||
mod util;
|
||||
|
||||
use crate::api::purge_auth_requests;
|
||||
use crate::api::WS_ANONYMOUS_SUBSCRIPTIONS;
|
||||
use crate::api::{WS_ANONYMOUS_SUBSCRIPTIONS, WS_USERS};
|
||||
pub use config::CONFIG;
|
||||
pub use error::{Error, MapResult};
|
||||
use rocket::data::{Limits, ToByteUnit};
|
||||
use std::sync::Arc;
|
||||
pub use util::is_running_in_docker;
|
||||
pub use util::is_running_in_container;
|
||||
|
||||
#[rocket::main]
|
||||
async fn main() -> Result<(), Error> {
|
||||
|
@ -96,13 +65,17 @@ async fn main() -> Result<(), Error> {
|
|||
launch_info();
|
||||
|
||||
use log::LevelFilter as LF;
|
||||
let level = LF::from_str(&CONFIG.log_level()).expect("Valid log level");
|
||||
let level = LF::from_str(&CONFIG.log_level()).unwrap_or_else(|_| {
|
||||
let valid_log_levels = LF::iter().map(|lvl| lvl.as_str().to_lowercase()).collect::<Vec<String>>().join(", ");
|
||||
println!("Log level must be one of the following: {valid_log_levels}");
|
||||
exit(1);
|
||||
});
|
||||
init_logging(level).ok();
|
||||
|
||||
let extra_debug = matches!(level, LF::Trace | LF::Debug);
|
||||
|
||||
check_data_folder().await;
|
||||
check_rsa_keys().unwrap_or_else(|_| {
|
||||
auth::initialize_keys().unwrap_or_else(|_| {
|
||||
error!("Error creating keys, exiting...");
|
||||
exit(1);
|
||||
});
|
||||
|
@ -238,9 +211,9 @@ fn launch_info() {
|
|||
}
|
||||
|
||||
fn init_logging(level: log::LevelFilter) -> Result<(), fern::InitError> {
|
||||
// Depending on the main log level we either want to disable or enable logging for trust-dns.
|
||||
// Else if there are timeouts it will clutter the logs since trust-dns uses warn for this.
|
||||
let trust_dns_level = if level >= log::LevelFilter::Debug {
|
||||
// Depending on the main log level we either want to disable or enable logging for hickory.
|
||||
// Else if there are timeouts it will clutter the logs since hickory uses warn for this.
|
||||
let hickory_level = if level >= log::LevelFilter::Debug {
|
||||
level
|
||||
} else {
|
||||
log::LevelFilter::Off
|
||||
|
@ -293,9 +266,9 @@ fn init_logging(level: log::LevelFilter) -> Result<(), fern::InitError> {
|
|||
.level_for("handlebars::render", handlebars_level)
|
||||
// Prevent cookie_store logs
|
||||
.level_for("cookie_store", log::LevelFilter::Off)
|
||||
// Variable level for trust-dns used by reqwest
|
||||
.level_for("trust_dns_resolver::name_server::name_server", trust_dns_level)
|
||||
.level_for("trust_dns_proto::xfer", trust_dns_level)
|
||||
// Variable level for hickory used by reqwest
|
||||
.level_for("hickory_resolver::name_server::name_server", hickory_level)
|
||||
.level_for("hickory_proto::xfer", hickory_level)
|
||||
.level_for("diesel_logger", diesel_logger_level)
|
||||
.chain(std::io::stdout());
|
||||
|
||||
|
@ -415,7 +388,7 @@ async fn check_data_folder() {
|
|||
let path = Path::new(data_folder);
|
||||
if !path.exists() {
|
||||
error!("Data folder '{}' doesn't exist.", data_folder);
|
||||
if is_running_in_docker() {
|
||||
if is_running_in_container() {
|
||||
error!("Verify that your data volume is mounted at the correct location.");
|
||||
} else {
|
||||
error!("Create the data folder and try again.");
|
||||
|
@ -427,9 +400,9 @@ async fn check_data_folder() {
|
|||
exit(1);
|
||||
}
|
||||
|
||||
if is_running_in_docker()
|
||||
if is_running_in_container()
|
||||
&& std::env::var("I_REALLY_WANT_VOLATILE_STORAGE").is_err()
|
||||
&& !docker_data_folder_is_persistent(data_folder).await
|
||||
&& !container_data_folder_is_persistent(data_folder).await
|
||||
{
|
||||
error!(
|
||||
"No persistent volume!\n\
|
||||
|
@ -448,7 +421,7 @@ async fn check_data_folder() {
|
|||
/// A none persistent volume in either Docker or Podman is represented by a 64 alphanumerical string.
|
||||
/// If we detect this string, we will alert about not having a persistent self defined volume.
|
||||
/// This probably means that someone forgot to add `-v /path/to/vaultwarden_data/:/data`
|
||||
async fn docker_data_folder_is_persistent(data_folder: &str) -> bool {
|
||||
async fn container_data_folder_is_persistent(data_folder: &str) -> bool {
|
||||
if let Ok(mountinfo) = File::open("/proc/self/mountinfo").await {
|
||||
// Since there can only be one mountpoint to the DATA_FOLDER
|
||||
// We do a basic check for this mountpoint surrounded by a space.
|
||||
|
@ -475,31 +448,6 @@ async fn docker_data_folder_is_persistent(data_folder: &str) -> bool {
|
|||
true
|
||||
}
|
||||
|
||||
fn check_rsa_keys() -> Result<(), crate::error::Error> {
|
||||
// If the RSA keys don't exist, try to create them
|
||||
let priv_path = CONFIG.private_rsa_key();
|
||||
let pub_path = CONFIG.public_rsa_key();
|
||||
|
||||
if !util::file_exists(&priv_path) {
|
||||
let rsa_key = openssl::rsa::Rsa::generate(2048)?;
|
||||
|
||||
let priv_key = rsa_key.private_key_to_pem()?;
|
||||
crate::util::write_file(&priv_path, &priv_key)?;
|
||||
info!("Private key created correctly.");
|
||||
}
|
||||
|
||||
if !util::file_exists(&pub_path) {
|
||||
let rsa_key = openssl::rsa::Rsa::private_key_from_pem(&std::fs::read(&priv_path)?)?;
|
||||
|
||||
let pub_key = rsa_key.public_key_to_pem()?;
|
||||
crate::util::write_file(&pub_path, &pub_key)?;
|
||||
info!("Public key created correctly.");
|
||||
}
|
||||
|
||||
auth::load_keys();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_web_vault() {
|
||||
if !CONFIG.web_vault_enabled() {
|
||||
return;
|
||||
|
@ -553,7 +501,7 @@ async fn launch_rocket(pool: db::DbPool, extra_debug: bool) -> Result<(), Error>
|
|||
.register([basepath, "/api"].concat(), api::core_catchers())
|
||||
.register([basepath, "/admin"].concat(), api::admin_catchers())
|
||||
.manage(pool)
|
||||
.manage(api::start_notification_server())
|
||||
.manage(Arc::clone(&WS_USERS))
|
||||
.manage(Arc::clone(&WS_ANONYMOUS_SUBSCRIPTIONS))
|
||||
.attach(util::AppHeaders())
|
||||
.attach(util::Cors())
|
||||
|
|
4
src/static/scripts/admin_diagnostics.js
gevendort
4
src/static/scripts/admin_diagnostics.js
gevendort
|
@ -77,7 +77,7 @@ async function generateSupportString(event, dj) {
|
|||
supportString += `* Vaultwarden version: v${dj.current_release}\n`;
|
||||
supportString += `* Web-vault version: v${dj.web_vault_version}\n`;
|
||||
supportString += `* OS/Arch: ${dj.host_os}/${dj.host_arch}\n`;
|
||||
supportString += `* Running within Docker: ${dj.running_within_docker} (Base: ${dj.docker_base_image})\n`;
|
||||
supportString += `* Running within a container: ${dj.running_within_container} (Base: ${dj.container_base_image})\n`;
|
||||
supportString += "* Environment settings overridden: ";
|
||||
if (dj.overrides != "") {
|
||||
supportString += "true\n";
|
||||
|
@ -179,7 +179,7 @@ function initVersionCheck(dj) {
|
|||
}
|
||||
checkVersions("server", serverInstalled, serverLatest, serverLatestCommit);
|
||||
|
||||
if (!dj.running_within_docker) {
|
||||
if (!dj.running_within_container) {
|
||||
const webInstalled = dj.web_vault_version;
|
||||
const webLatest = dj.latest_web_build;
|
||||
checkVersions("web", webInstalled, webLatest);
|
||||
|
|
10
src/static/scripts/bootstrap.bundle.js
gevendort
10
src/static/scripts/bootstrap.bundle.js
gevendort
|
@ -1,5 +1,5 @@
|
|||
/*!
|
||||
* Bootstrap v5.3.1 (https://getbootstrap.com/)
|
||||
* Bootstrap v5.3.2 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2023 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
*/
|
||||
|
@ -648,7 +648,7 @@
|
|||
* Constants
|
||||
*/
|
||||
|
||||
const VERSION = '5.3.1';
|
||||
const VERSION = '5.3.2';
|
||||
|
||||
/**
|
||||
* Class definition
|
||||
|
@ -729,9 +729,9 @@
|
|||
if (hrefAttribute.includes('#') && !hrefAttribute.startsWith('#')) {
|
||||
hrefAttribute = `#${hrefAttribute.split('#')[1]}`;
|
||||
}
|
||||
selector = hrefAttribute && hrefAttribute !== '#' ? hrefAttribute.trim() : null;
|
||||
selector = hrefAttribute && hrefAttribute !== '#' ? parseSelector(hrefAttribute.trim()) : null;
|
||||
}
|
||||
return parseSelector(selector);
|
||||
return selector;
|
||||
};
|
||||
const SelectorEngine = {
|
||||
find(selector, element = document.documentElement) {
|
||||
|
@ -5866,7 +5866,7 @@
|
|||
const CLASS_DROPDOWN = 'dropdown';
|
||||
const SELECTOR_DROPDOWN_TOGGLE = '.dropdown-toggle';
|
||||
const SELECTOR_DROPDOWN_MENU = '.dropdown-menu';
|
||||
const NOT_SELECTOR_DROPDOWN_TOGGLE = ':not(.dropdown-toggle)';
|
||||
const NOT_SELECTOR_DROPDOWN_TOGGLE = `:not(${SELECTOR_DROPDOWN_TOGGLE})`;
|
||||
const SELECTOR_TAB_PANEL = '.list-group, .nav, [role="tablist"]';
|
||||
const SELECTOR_OUTER = '.nav-item, .list-group-item';
|
||||
const SELECTOR_INNER = `.nav-link${NOT_SELECTOR_DROPDOWN_TOGGLE}, .list-group-item${NOT_SELECTOR_DROPDOWN_TOGGLE}, [role="tab"]${NOT_SELECTOR_DROPDOWN_TOGGLE}`;
|
||||
|
|
81
src/static/scripts/bootstrap.css
gevendort
81
src/static/scripts/bootstrap.css
gevendort
|
@ -1,6 +1,6 @@
|
|||
@charset "UTF-8";
|
||||
/*!
|
||||
* Bootstrap v5.3.1 (https://getbootstrap.com/)
|
||||
* Bootstrap v5.3.2 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2023 The Bootstrap Authors
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
*/
|
||||
|
@ -99,6 +99,7 @@
|
|||
--bs-link-hover-color: #0a58ca;
|
||||
--bs-link-hover-color-rgb: 10, 88, 202;
|
||||
--bs-code-color: #d63384;
|
||||
--bs-highlight-color: #212529;
|
||||
--bs-highlight-bg: #fff3cd;
|
||||
--bs-border-width: 1px;
|
||||
--bs-border-style: solid;
|
||||
|
@ -170,6 +171,8 @@
|
|||
--bs-link-color-rgb: 110, 168, 254;
|
||||
--bs-link-hover-color-rgb: 139, 185, 254;
|
||||
--bs-code-color: #e685b5;
|
||||
--bs-highlight-color: #dee2e6;
|
||||
--bs-highlight-bg: #664d03;
|
||||
--bs-border-color: #495057;
|
||||
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
|
||||
--bs-form-valid-color: #75b798;
|
||||
|
@ -325,6 +328,7 @@ small, .small {
|
|||
|
||||
mark, .mark {
|
||||
padding: 0.1875em;
|
||||
color: var(--bs-highlight-color);
|
||||
background-color: var(--bs-highlight-bg);
|
||||
}
|
||||
|
||||
|
@ -819,7 +823,7 @@ progress {
|
|||
|
||||
.row-cols-3 > * {
|
||||
flex: 0 0 auto;
|
||||
width: 33.3333333333%;
|
||||
width: 33.33333333%;
|
||||
}
|
||||
|
||||
.row-cols-4 > * {
|
||||
|
@ -834,7 +838,7 @@ progress {
|
|||
|
||||
.row-cols-6 > * {
|
||||
flex: 0 0 auto;
|
||||
width: 16.6666666667%;
|
||||
width: 16.66666667%;
|
||||
}
|
||||
|
||||
.col-auto {
|
||||
|
@ -1024,7 +1028,7 @@ progress {
|
|||
}
|
||||
.row-cols-sm-3 > * {
|
||||
flex: 0 0 auto;
|
||||
width: 33.3333333333%;
|
||||
width: 33.33333333%;
|
||||
}
|
||||
.row-cols-sm-4 > * {
|
||||
flex: 0 0 auto;
|
||||
|
@ -1036,7 +1040,7 @@ progress {
|
|||
}
|
||||
.row-cols-sm-6 > * {
|
||||
flex: 0 0 auto;
|
||||
width: 16.6666666667%;
|
||||
width: 16.66666667%;
|
||||
}
|
||||
.col-sm-auto {
|
||||
flex: 0 0 auto;
|
||||
|
@ -1193,7 +1197,7 @@ progress {
|
|||
}
|
||||
.row-cols-md-3 > * {
|
||||
flex: 0 0 auto;
|
||||
width: 33.3333333333%;
|
||||
width: 33.33333333%;
|
||||
}
|
||||
.row-cols-md-4 > * {
|
||||
flex: 0 0 auto;
|
||||
|
@ -1205,7 +1209,7 @@ progress {
|
|||
}
|
||||
.row-cols-md-6 > * {
|
||||
flex: 0 0 auto;
|
||||
width: 16.6666666667%;
|
||||
width: 16.66666667%;
|
||||
}
|
||||
.col-md-auto {
|
||||
flex: 0 0 auto;
|
||||
|
@ -1362,7 +1366,7 @@ progress {
|
|||
}
|
||||
.row-cols-lg-3 > * {
|
||||
flex: 0 0 auto;
|
||||
width: 33.3333333333%;
|
||||
width: 33.33333333%;
|
||||
}
|
||||
.row-cols-lg-4 > * {
|
||||
flex: 0 0 auto;
|
||||
|
@ -1374,7 +1378,7 @@ progress {
|
|||
}
|
||||
.row-cols-lg-6 > * {
|
||||
flex: 0 0 auto;
|
||||
width: 16.6666666667%;
|
||||
width: 16.66666667%;
|
||||
}
|
||||
.col-lg-auto {
|
||||
flex: 0 0 auto;
|
||||
|
@ -1531,7 +1535,7 @@ progress {
|
|||
}
|
||||
.row-cols-xl-3 > * {
|
||||
flex: 0 0 auto;
|
||||
width: 33.3333333333%;
|
||||
width: 33.33333333%;
|
||||
}
|
||||
.row-cols-xl-4 > * {
|
||||
flex: 0 0 auto;
|
||||
|
@ -1543,7 +1547,7 @@ progress {
|
|||
}
|
||||
.row-cols-xl-6 > * {
|
||||
flex: 0 0 auto;
|
||||
width: 16.6666666667%;
|
||||
width: 16.66666667%;
|
||||
}
|
||||
.col-xl-auto {
|
||||
flex: 0 0 auto;
|
||||
|
@ -1700,7 +1704,7 @@ progress {
|
|||
}
|
||||
.row-cols-xxl-3 > * {
|
||||
flex: 0 0 auto;
|
||||
width: 33.3333333333%;
|
||||
width: 33.33333333%;
|
||||
}
|
||||
.row-cols-xxl-4 > * {
|
||||
flex: 0 0 auto;
|
||||
|
@ -1712,7 +1716,7 @@ progress {
|
|||
}
|
||||
.row-cols-xxl-6 > * {
|
||||
flex: 0 0 auto;
|
||||
width: 16.6666666667%;
|
||||
width: 16.66666667%;
|
||||
}
|
||||
.col-xxl-auto {
|
||||
flex: 0 0 auto;
|
||||
|
@ -1856,16 +1860,16 @@ progress {
|
|||
--bs-table-bg-type: initial;
|
||||
--bs-table-color-state: initial;
|
||||
--bs-table-bg-state: initial;
|
||||
--bs-table-color: var(--bs-body-color);
|
||||
--bs-table-color: var(--bs-emphasis-color);
|
||||
--bs-table-bg: var(--bs-body-bg);
|
||||
--bs-table-border-color: var(--bs-border-color);
|
||||
--bs-table-accent-bg: transparent;
|
||||
--bs-table-striped-color: var(--bs-body-color);
|
||||
--bs-table-striped-bg: rgba(0, 0, 0, 0.05);
|
||||
--bs-table-active-color: var(--bs-body-color);
|
||||
--bs-table-active-bg: rgba(0, 0, 0, 0.1);
|
||||
--bs-table-hover-color: var(--bs-body-color);
|
||||
--bs-table-hover-bg: rgba(0, 0, 0, 0.075);
|
||||
--bs-table-striped-color: var(--bs-emphasis-color);
|
||||
--bs-table-striped-bg: rgba(var(--bs-emphasis-color-rgb), 0.05);
|
||||
--bs-table-active-color: var(--bs-emphasis-color);
|
||||
--bs-table-active-bg: rgba(var(--bs-emphasis-color-rgb), 0.1);
|
||||
--bs-table-hover-color: var(--bs-emphasis-color);
|
||||
--bs-table-hover-bg: rgba(var(--bs-emphasis-color-rgb), 0.075);
|
||||
width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
vertical-align: top;
|
||||
|
@ -1934,7 +1938,7 @@ progress {
|
|||
.table-primary {
|
||||
--bs-table-color: #000;
|
||||
--bs-table-bg: #cfe2ff;
|
||||
--bs-table-border-color: #bacbe6;
|
||||
--bs-table-border-color: #a6b5cc;
|
||||
--bs-table-striped-bg: #c5d7f2;
|
||||
--bs-table-striped-color: #000;
|
||||
--bs-table-active-bg: #bacbe6;
|
||||
|
@ -1948,7 +1952,7 @@ progress {
|
|||
.table-secondary {
|
||||
--bs-table-color: #000;
|
||||
--bs-table-bg: #e2e3e5;
|
||||
--bs-table-border-color: #cbccce;
|
||||
--bs-table-border-color: #b5b6b7;
|
||||
--bs-table-striped-bg: #d7d8da;
|
||||
--bs-table-striped-color: #000;
|
||||
--bs-table-active-bg: #cbccce;
|
||||
|
@ -1962,7 +1966,7 @@ progress {
|
|||
.table-success {
|
||||
--bs-table-color: #000;
|
||||
--bs-table-bg: #d1e7dd;
|
||||
--bs-table-border-color: #bcd0c7;
|
||||
--bs-table-border-color: #a7b9b1;
|
||||
--bs-table-striped-bg: #c7dbd2;
|
||||
--bs-table-striped-color: #000;
|
||||
--bs-table-active-bg: #bcd0c7;
|
||||
|
@ -1976,7 +1980,7 @@ progress {
|
|||
.table-info {
|
||||
--bs-table-color: #000;
|
||||
--bs-table-bg: #cff4fc;
|
||||
--bs-table-border-color: #badce3;
|
||||
--bs-table-border-color: #a6c3ca;
|
||||
--bs-table-striped-bg: #c5e8ef;
|
||||
--bs-table-striped-color: #000;
|
||||
--bs-table-active-bg: #badce3;
|
||||
|
@ -1990,7 +1994,7 @@ progress {
|
|||
.table-warning {
|
||||
--bs-table-color: #000;
|
||||
--bs-table-bg: #fff3cd;
|
||||
--bs-table-border-color: #e6dbb9;
|
||||
--bs-table-border-color: #ccc2a4;
|
||||
--bs-table-striped-bg: #f2e7c3;
|
||||
--bs-table-striped-color: #000;
|
||||
--bs-table-active-bg: #e6dbb9;
|
||||
|
@ -2004,7 +2008,7 @@ progress {
|
|||
.table-danger {
|
||||
--bs-table-color: #000;
|
||||
--bs-table-bg: #f8d7da;
|
||||
--bs-table-border-color: #dfc2c4;
|
||||
--bs-table-border-color: #c6acae;
|
||||
--bs-table-striped-bg: #eccccf;
|
||||
--bs-table-striped-color: #000;
|
||||
--bs-table-active-bg: #dfc2c4;
|
||||
|
@ -2018,7 +2022,7 @@ progress {
|
|||
.table-light {
|
||||
--bs-table-color: #000;
|
||||
--bs-table-bg: #f8f9fa;
|
||||
--bs-table-border-color: #dfe0e1;
|
||||
--bs-table-border-color: #c6c7c8;
|
||||
--bs-table-striped-bg: #ecedee;
|
||||
--bs-table-striped-color: #000;
|
||||
--bs-table-active-bg: #dfe0e1;
|
||||
|
@ -2032,7 +2036,7 @@ progress {
|
|||
.table-dark {
|
||||
--bs-table-color: #fff;
|
||||
--bs-table-bg: #212529;
|
||||
--bs-table-border-color: #373b3e;
|
||||
--bs-table-border-color: #4d5154;
|
||||
--bs-table-striped-bg: #2c3034;
|
||||
--bs-table-striped-color: #fff;
|
||||
--bs-table-active-bg: #373b3e;
|
||||
|
@ -2388,6 +2392,7 @@ textarea.form-control-lg {
|
|||
|
||||
.form-check-input {
|
||||
--bs-form-check-bg: var(--bs-body-bg);
|
||||
flex-shrink: 0;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
margin-top: 0.25em;
|
||||
|
@ -2544,7 +2549,7 @@ textarea.form-control-lg {
|
|||
height: 0.5rem;
|
||||
color: transparent;
|
||||
cursor: pointer;
|
||||
background-color: var(--bs-tertiary-bg);
|
||||
background-color: var(--bs-secondary-bg);
|
||||
border-color: transparent;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
@ -2573,7 +2578,7 @@ textarea.form-control-lg {
|
|||
height: 0.5rem;
|
||||
color: transparent;
|
||||
cursor: pointer;
|
||||
background-color: var(--bs-tertiary-bg);
|
||||
background-color: var(--bs-secondary-bg);
|
||||
border-color: transparent;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
@ -3431,7 +3436,7 @@ textarea.form-control-lg {
|
|||
--bs-dropdown-inner-border-radius: calc(var(--bs-border-radius) - var(--bs-border-width));
|
||||
--bs-dropdown-divider-bg: var(--bs-border-color-translucent);
|
||||
--bs-dropdown-divider-margin-y: 0.5rem;
|
||||
--bs-dropdown-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
--bs-dropdown-box-shadow: var(--bs-box-shadow);
|
||||
--bs-dropdown-link-color: var(--bs-body-color);
|
||||
--bs-dropdown-link-hover-color: var(--bs-body-color);
|
||||
--bs-dropdown-link-hover-bg: var(--bs-tertiary-bg);
|
||||
|
@ -5473,7 +5478,7 @@ textarea.form-control-lg {
|
|||
--bs-modal-border-color: var(--bs-border-color-translucent);
|
||||
--bs-modal-border-width: var(--bs-border-width);
|
||||
--bs-modal-border-radius: var(--bs-border-radius-lg);
|
||||
--bs-modal-box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
--bs-modal-box-shadow: var(--bs-box-shadow-sm);
|
||||
--bs-modal-inner-border-radius: calc(var(--bs-border-radius-lg) - (var(--bs-border-width)));
|
||||
--bs-modal-header-padding-x: 1rem;
|
||||
--bs-modal-header-padding-y: 1rem;
|
||||
|
@ -5614,7 +5619,7 @@ textarea.form-control-lg {
|
|||
@media (min-width: 576px) {
|
||||
.modal {
|
||||
--bs-modal-margin: 1.75rem;
|
||||
--bs-modal-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
--bs-modal-box-shadow: var(--bs-box-shadow);
|
||||
}
|
||||
.modal-dialog {
|
||||
max-width: var(--bs-modal-width);
|
||||
|
@ -5866,7 +5871,7 @@ textarea.form-control-lg {
|
|||
--bs-popover-border-color: var(--bs-border-color-translucent);
|
||||
--bs-popover-border-radius: var(--bs-border-radius-lg);
|
||||
--bs-popover-inner-border-radius: calc(var(--bs-border-radius-lg) - var(--bs-border-width));
|
||||
--bs-popover-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
--bs-popover-box-shadow: var(--bs-box-shadow);
|
||||
--bs-popover-header-padding-x: 1rem;
|
||||
--bs-popover-header-padding-y: 0.5rem;
|
||||
--bs-popover-header-font-size: 1rem;
|
||||
|
@ -6301,7 +6306,7 @@ textarea.form-control-lg {
|
|||
--bs-offcanvas-bg: var(--bs-body-bg);
|
||||
--bs-offcanvas-border-width: var(--bs-border-width);
|
||||
--bs-offcanvas-border-color: var(--bs-border-color-translucent);
|
||||
--bs-offcanvas-box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
--bs-offcanvas-box-shadow: var(--bs-box-shadow-sm);
|
||||
--bs-offcanvas-transition: transform 0.3s ease-in-out;
|
||||
--bs-offcanvas-title-line-height: 1.5;
|
||||
}
|
||||
|
@ -7380,15 +7385,15 @@ textarea.form-control-lg {
|
|||
}
|
||||
|
||||
.shadow {
|
||||
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
|
||||
box-shadow: var(--bs-box-shadow) !important;
|
||||
}
|
||||
|
||||
.shadow-sm {
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important;
|
||||
box-shadow: var(--bs-box-shadow-sm) !important;
|
||||
}
|
||||
|
||||
.shadow-lg {
|
||||
box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.175) !important;
|
||||
box-shadow: var(--bs-box-shadow-lg) !important;
|
||||
}
|
||||
|
||||
.shadow-none {
|
||||
|
|
348
src/static/scripts/datatables.css
gevendort
348
src/static/scripts/datatables.css
gevendort
|
@ -4,10 +4,10 @@
|
|||
*
|
||||
* To rebuild or modify this file with the latest versions of the included
|
||||
* software please visit:
|
||||
* https://datatables.net/download/#bs5/dt-1.13.6
|
||||
* https://datatables.net/download/#bs5/dt-2.0.0
|
||||
*
|
||||
* Included libraries:
|
||||
* DataTables 1.13.6
|
||||
* DataTables 2.0.0
|
||||
*/
|
||||
|
||||
@charset "UTF-8";
|
||||
|
@ -30,76 +30,124 @@ table.dataTable td.dt-control {
|
|||
}
|
||||
table.dataTable td.dt-control:before {
|
||||
display: inline-block;
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
content: "►";
|
||||
box-sizing: border-box;
|
||||
content: "";
|
||||
border-top: 5px solid transparent;
|
||||
border-left: 10px solid rgba(0, 0, 0, 0.5);
|
||||
border-bottom: 5px solid transparent;
|
||||
border-right: 0px solid transparent;
|
||||
}
|
||||
table.dataTable tr.dt-hasChild td.dt-control:before {
|
||||
content: "▼";
|
||||
border-top: 10px solid rgba(0, 0, 0, 0.5);
|
||||
border-left: 5px solid transparent;
|
||||
border-bottom: 0px solid transparent;
|
||||
border-right: 5px solid transparent;
|
||||
}
|
||||
|
||||
html.dark table.dataTable td.dt-control:before {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
html.dark table.dataTable td.dt-control:before,
|
||||
:root[data-bs-theme=dark] table.dataTable td.dt-control:before {
|
||||
border-left-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
html.dark table.dataTable tr.dt-hasChild td.dt-control:before {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
html.dark table.dataTable tr.dt-hasChild td.dt-control:before,
|
||||
:root[data-bs-theme=dark] table.dataTable tr.dt-hasChild td.dt-control:before {
|
||||
border-top-color: rgba(255, 255, 255, 0.5);
|
||||
border-left-color: transparent;
|
||||
}
|
||||
|
||||
table.dataTable thead > tr > th.sorting, table.dataTable thead > tr > th.sorting_asc, table.dataTable thead > tr > th.sorting_desc, table.dataTable thead > tr > th.sorting_asc_disabled, table.dataTable thead > tr > th.sorting_desc_disabled,
|
||||
table.dataTable thead > tr > td.sorting,
|
||||
table.dataTable thead > tr > td.sorting_asc,
|
||||
table.dataTable thead > tr > td.sorting_desc,
|
||||
table.dataTable thead > tr > td.sorting_asc_disabled,
|
||||
table.dataTable thead > tr > td.sorting_desc_disabled {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
padding-right: 26px;
|
||||
div.dt-scroll-body thead tr,
|
||||
div.dt-scroll-body tfoot tr {
|
||||
height: 0;
|
||||
}
|
||||
table.dataTable thead > tr > th.sorting:before, table.dataTable thead > tr > th.sorting:after, table.dataTable thead > tr > th.sorting_asc:before, table.dataTable thead > tr > th.sorting_asc:after, table.dataTable thead > tr > th.sorting_desc:before, table.dataTable thead > tr > th.sorting_desc:after, table.dataTable thead > tr > th.sorting_asc_disabled:before, table.dataTable thead > tr > th.sorting_asc_disabled:after, table.dataTable thead > tr > th.sorting_desc_disabled:before, table.dataTable thead > tr > th.sorting_desc_disabled:after,
|
||||
table.dataTable thead > tr > td.sorting:before,
|
||||
table.dataTable thead > tr > td.sorting:after,
|
||||
table.dataTable thead > tr > td.sorting_asc:before,
|
||||
table.dataTable thead > tr > td.sorting_asc:after,
|
||||
table.dataTable thead > tr > td.sorting_desc:before,
|
||||
table.dataTable thead > tr > td.sorting_desc:after,
|
||||
table.dataTable thead > tr > td.sorting_asc_disabled:before,
|
||||
table.dataTable thead > tr > td.sorting_asc_disabled:after,
|
||||
table.dataTable thead > tr > td.sorting_desc_disabled:before,
|
||||
table.dataTable thead > tr > td.sorting_desc_disabled:after {
|
||||
div.dt-scroll-body thead tr th, div.dt-scroll-body thead tr td,
|
||||
div.dt-scroll-body tfoot tr th,
|
||||
div.dt-scroll-body tfoot tr td {
|
||||
height: 0 !important;
|
||||
padding-top: 0px !important;
|
||||
padding-bottom: 0px !important;
|
||||
border-top-width: 0px !important;
|
||||
border-bottom-width: 0px !important;
|
||||
}
|
||||
div.dt-scroll-body thead tr th div.dt-scroll-sizing, div.dt-scroll-body thead tr td div.dt-scroll-sizing,
|
||||
div.dt-scroll-body tfoot tr th div.dt-scroll-sizing,
|
||||
div.dt-scroll-body tfoot tr td div.dt-scroll-sizing {
|
||||
height: 0 !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
table.dataTable thead > tr > th:active,
|
||||
table.dataTable thead > tr > td:active {
|
||||
outline: none;
|
||||
}
|
||||
table.dataTable thead > tr > th.dt-orderable-asc span.dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-asc span.dt-column-order:before,
|
||||
table.dataTable thead > tr > td.dt-orderable-asc span.dt-column-order:before,
|
||||
table.dataTable thead > tr > td.dt-ordering-asc span.dt-column-order:before {
|
||||
position: absolute;
|
||||
display: block;
|
||||
opacity: 0.125;
|
||||
right: 10px;
|
||||
line-height: 9px;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
table.dataTable thead > tr > th.sorting:before, table.dataTable thead > tr > th.sorting_asc:before, table.dataTable thead > tr > th.sorting_desc:before, table.dataTable thead > tr > th.sorting_asc_disabled:before, table.dataTable thead > tr > th.sorting_desc_disabled:before,
|
||||
table.dataTable thead > tr > td.sorting:before,
|
||||
table.dataTable thead > tr > td.sorting_asc:before,
|
||||
table.dataTable thead > tr > td.sorting_desc:before,
|
||||
table.dataTable thead > tr > td.sorting_asc_disabled:before,
|
||||
table.dataTable thead > tr > td.sorting_desc_disabled:before {
|
||||
bottom: 50%;
|
||||
content: "▲";
|
||||
content: "▲"/"";
|
||||
}
|
||||
table.dataTable thead > tr > th.sorting:after, table.dataTable thead > tr > th.sorting_asc:after, table.dataTable thead > tr > th.sorting_desc:after, table.dataTable thead > tr > th.sorting_asc_disabled:after, table.dataTable thead > tr > th.sorting_desc_disabled:after,
|
||||
table.dataTable thead > tr > td.sorting:after,
|
||||
table.dataTable thead > tr > td.sorting_asc:after,
|
||||
table.dataTable thead > tr > td.sorting_desc:after,
|
||||
table.dataTable thead > tr > td.sorting_asc_disabled:after,
|
||||
table.dataTable thead > tr > td.sorting_desc_disabled:after {
|
||||
table.dataTable thead > tr > th.dt-orderable-desc span.dt-column-order:after, table.dataTable thead > tr > th.dt-ordering-desc span.dt-column-order:after,
|
||||
table.dataTable thead > tr > td.dt-orderable-desc span.dt-column-order:after,
|
||||
table.dataTable thead > tr > td.dt-ordering-desc span.dt-column-order:after {
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: 50%;
|
||||
content: "▼";
|
||||
content: "▼"/"";
|
||||
}
|
||||
table.dataTable thead > tr > th.sorting_asc:before, table.dataTable thead > tr > th.sorting_desc:after,
|
||||
table.dataTable thead > tr > td.sorting_asc:before,
|
||||
table.dataTable thead > tr > td.sorting_desc:after {
|
||||
table.dataTable thead > tr > th.dt-orderable-asc, table.dataTable thead > tr > th.dt-orderable-desc, table.dataTable thead > tr > th.dt-ordering-asc, table.dataTable thead > tr > th.dt-ordering-desc,
|
||||
table.dataTable thead > tr > td.dt-orderable-asc,
|
||||
table.dataTable thead > tr > td.dt-orderable-desc,
|
||||
table.dataTable thead > tr > td.dt-ordering-asc,
|
||||
table.dataTable thead > tr > td.dt-ordering-desc {
|
||||
position: relative;
|
||||
padding-right: 30px;
|
||||
}
|
||||
table.dataTable thead > tr > th.dt-orderable-asc span.dt-column-order, table.dataTable thead > tr > th.dt-orderable-desc span.dt-column-order, table.dataTable thead > tr > th.dt-ordering-asc span.dt-column-order, table.dataTable thead > tr > th.dt-ordering-desc span.dt-column-order,
|
||||
table.dataTable thead > tr > td.dt-orderable-asc span.dt-column-order,
|
||||
table.dataTable thead > tr > td.dt-orderable-desc span.dt-column-order,
|
||||
table.dataTable thead > tr > td.dt-ordering-asc span.dt-column-order,
|
||||
table.dataTable thead > tr > td.dt-ordering-desc span.dt-column-order {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 12px;
|
||||
}
|
||||
table.dataTable thead > tr > th.dt-orderable-asc span.dt-column-order:before, table.dataTable thead > tr > th.dt-orderable-asc span.dt-column-order:after, table.dataTable thead > tr > th.dt-orderable-desc span.dt-column-order:before, table.dataTable thead > tr > th.dt-orderable-desc span.dt-column-order:after, table.dataTable thead > tr > th.dt-ordering-asc span.dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-asc span.dt-column-order:after, table.dataTable thead > tr > th.dt-ordering-desc span.dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-desc span.dt-column-order:after,
|
||||
table.dataTable thead > tr > td.dt-orderable-asc span.dt-column-order:before,
|
||||
table.dataTable thead > tr > td.dt-orderable-asc span.dt-column-order:after,
|
||||
table.dataTable thead > tr > td.dt-orderable-desc span.dt-column-order:before,
|
||||
table.dataTable thead > tr > td.dt-orderable-desc span.dt-column-order:after,
|
||||
table.dataTable thead > tr > td.dt-ordering-asc span.dt-column-order:before,
|
||||
table.dataTable thead > tr > td.dt-ordering-asc span.dt-column-order:after,
|
||||
table.dataTable thead > tr > td.dt-ordering-desc span.dt-column-order:before,
|
||||
table.dataTable thead > tr > td.dt-ordering-desc span.dt-column-order:after {
|
||||
left: 0;
|
||||
opacity: 0.125;
|
||||
line-height: 9px;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
table.dataTable thead > tr > th.dt-orderable-asc, table.dataTable thead > tr > th.dt-orderable-desc,
|
||||
table.dataTable thead > tr > td.dt-orderable-asc,
|
||||
table.dataTable thead > tr > td.dt-orderable-desc {
|
||||
cursor: pointer;
|
||||
}
|
||||
table.dataTable thead > tr > th.dt-orderable-asc:hover, table.dataTable thead > tr > th.dt-orderable-desc:hover,
|
||||
table.dataTable thead > tr > td.dt-orderable-asc:hover,
|
||||
table.dataTable thead > tr > td.dt-orderable-desc:hover {
|
||||
outline: 2px solid rgba(0, 0, 0, 0.05);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
table.dataTable thead > tr > th.dt-ordering-asc span.dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-desc span.dt-column-order:after,
|
||||
table.dataTable thead > tr > td.dt-ordering-asc span.dt-column-order:before,
|
||||
table.dataTable thead > tr > td.dt-ordering-desc span.dt-column-order:after {
|
||||
opacity: 0.6;
|
||||
}
|
||||
table.dataTable thead > tr > th.sorting_desc_disabled:after, table.dataTable thead > tr > th.sorting_asc_disabled:before,
|
||||
table.dataTable thead > tr > td.sorting_desc_disabled:after,
|
||||
table.dataTable thead > tr > td.sorting_asc_disabled:before {
|
||||
table.dataTable thead > tr > th.sorting_desc_disabled span.dt-column-order:after, table.dataTable thead > tr > th.sorting_asc_disabled span.dt-column-order:before,
|
||||
table.dataTable thead > tr > td.sorting_desc_disabled span.dt-column-order:after,
|
||||
table.dataTable thead > tr > td.sorting_asc_disabled span.dt-column-order:before {
|
||||
display: none;
|
||||
}
|
||||
table.dataTable thead > tr > th:active,
|
||||
|
@ -107,29 +155,39 @@ table.dataTable thead > tr > td:active {
|
|||
outline: none;
|
||||
}
|
||||
|
||||
div.dataTables_scrollBody > table.dataTable > thead > tr > th:before, div.dataTables_scrollBody > table.dataTable > thead > tr > th:after,
|
||||
div.dataTables_scrollBody > table.dataTable > thead > tr > td:before,
|
||||
div.dataTables_scrollBody > table.dataTable > thead > tr > td:after {
|
||||
display: none;
|
||||
div.dt-scroll-body > table.dataTable > thead > tr > th,
|
||||
div.dt-scroll-body > table.dataTable > thead > tr > td {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
div.dataTables_processing {
|
||||
:root.dark table.dataTable thead > tr > th.dt-orderable-asc:hover, :root.dark table.dataTable thead > tr > th.dt-orderable-desc:hover,
|
||||
:root.dark table.dataTable thead > tr > td.dt-orderable-asc:hover,
|
||||
:root.dark table.dataTable thead > tr > td.dt-orderable-desc:hover,
|
||||
:root[data-bs-theme=dark] table.dataTable thead > tr > th.dt-orderable-asc:hover,
|
||||
:root[data-bs-theme=dark] table.dataTable thead > tr > th.dt-orderable-desc:hover,
|
||||
:root[data-bs-theme=dark] table.dataTable thead > tr > td.dt-orderable-asc:hover,
|
||||
:root[data-bs-theme=dark] table.dataTable thead > tr > td.dt-orderable-desc:hover {
|
||||
outline: 2px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
div.dt-processing {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 200px;
|
||||
margin-left: -100px;
|
||||
margin-top: -26px;
|
||||
margin-top: -22px;
|
||||
text-align: center;
|
||||
padding: 2px;
|
||||
z-index: 10;
|
||||
}
|
||||
div.dataTables_processing > div:last-child {
|
||||
div.dt-processing > div:last-child {
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 15px;
|
||||
margin: 1em auto;
|
||||
}
|
||||
div.dataTables_processing > div:last-child > div {
|
||||
div.dt-processing > div:last-child > div {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 13px;
|
||||
|
@ -139,19 +197,19 @@ div.dataTables_processing > div:last-child > div {
|
|||
background: rgb(var(--dt-row-selected));
|
||||
animation-timing-function: cubic-bezier(0, 1, 1, 0);
|
||||
}
|
||||
div.dataTables_processing > div:last-child > div:nth-child(1) {
|
||||
div.dt-processing > div:last-child > div:nth-child(1) {
|
||||
left: 8px;
|
||||
animation: datatables-loader-1 0.6s infinite;
|
||||
}
|
||||
div.dataTables_processing > div:last-child > div:nth-child(2) {
|
||||
div.dt-processing > div:last-child > div:nth-child(2) {
|
||||
left: 8px;
|
||||
animation: datatables-loader-2 0.6s infinite;
|
||||
}
|
||||
div.dataTables_processing > div:last-child > div:nth-child(3) {
|
||||
div.dt-processing > div:last-child > div:nth-child(3) {
|
||||
left: 32px;
|
||||
animation: datatables-loader-2 0.6s infinite;
|
||||
}
|
||||
div.dataTables_processing > div:last-child > div:nth-child(4) {
|
||||
div.dt-processing > div:last-child > div:nth-child(4) {
|
||||
left: 56px;
|
||||
animation: datatables-loader-3 0.6s infinite;
|
||||
}
|
||||
|
@ -183,13 +241,16 @@ div.dataTables_processing > div:last-child > div:nth-child(4) {
|
|||
table.dataTable.nowrap th, table.dataTable.nowrap td {
|
||||
white-space: nowrap;
|
||||
}
|
||||
table.dataTable th,
|
||||
table.dataTable td {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
table.dataTable th.dt-left,
|
||||
table.dataTable td.dt-left {
|
||||
text-align: left;
|
||||
}
|
||||
table.dataTable th.dt-center,
|
||||
table.dataTable td.dt-center,
|
||||
table.dataTable td.dataTables_empty {
|
||||
table.dataTable td.dt-center {
|
||||
text-align: center;
|
||||
}
|
||||
table.dataTable th.dt-right,
|
||||
|
@ -204,6 +265,16 @@ table.dataTable th.dt-nowrap,
|
|||
table.dataTable td.dt-nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
table.dataTable th.dt-empty,
|
||||
table.dataTable td.dt-empty {
|
||||
text-align: center;
|
||||
vertical-align: top;
|
||||
}
|
||||
table.dataTable th.dt-type-numeric, table.dataTable th.dt-type-date,
|
||||
table.dataTable td.dt-type-numeric,
|
||||
table.dataTable td.dt-type-date {
|
||||
text-align: right;
|
||||
}
|
||||
table.dataTable thead th,
|
||||
table.dataTable thead td,
|
||||
table.dataTable tfoot th,
|
||||
|
@ -266,179 +337,150 @@ table.dataTable tbody td.dt-body-nowrap {
|
|||
* ©2020 SpryMedia Ltd, all rights reserved.
|
||||
* License: MIT datatables.net/license/mit
|
||||
*/
|
||||
table.dataTable {
|
||||
table.table.dataTable {
|
||||
clear: both;
|
||||
margin-top: 6px !important;
|
||||
margin-bottom: 6px !important;
|
||||
max-width: none !important;
|
||||
border-collapse: separate !important;
|
||||
margin-bottom: 0;
|
||||
max-width: none;
|
||||
border-spacing: 0;
|
||||
}
|
||||
table.dataTable td,
|
||||
table.dataTable th {
|
||||
-webkit-box-sizing: content-box;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
table.dataTable td.dataTables_empty,
|
||||
table.dataTable th.dataTables_empty {
|
||||
text-align: center;
|
||||
}
|
||||
table.dataTable.nowrap th,
|
||||
table.dataTable.nowrap td {
|
||||
white-space: nowrap;
|
||||
}
|
||||
table.dataTable.table-striped > tbody > tr:nth-of-type(2n+1) > * {
|
||||
table.table.dataTable.table-striped > tbody > tr:nth-of-type(2n+1) > * {
|
||||
box-shadow: none;
|
||||
}
|
||||
table.dataTable > tbody > tr {
|
||||
table.table.dataTable > :not(caption) > * > * {
|
||||
background-color: transparent;
|
||||
}
|
||||
table.dataTable > tbody > tr.selected > * {
|
||||
table.table.dataTable > tbody > tr {
|
||||
background-color: transparent;
|
||||
}
|
||||
table.table.dataTable > tbody > tr.selected > * {
|
||||
box-shadow: inset 0 0 0 9999px rgb(13, 110, 253);
|
||||
box-shadow: inset 0 0 0 9999px rgb(var(--dt-row-selected));
|
||||
color: rgb(255, 255, 255);
|
||||
color: rgb(var(--dt-row-selected-text));
|
||||
}
|
||||
table.dataTable > tbody > tr.selected a {
|
||||
table.table.dataTable > tbody > tr.selected a {
|
||||
color: rgb(9, 10, 11);
|
||||
color: rgb(var(--dt-row-selected-link));
|
||||
}
|
||||
table.dataTable.table-striped > tbody > tr.odd > * {
|
||||
table.table.dataTable.table-striped > tbody > tr:nth-of-type(2n+1) > * {
|
||||
box-shadow: inset 0 0 0 9999px rgba(var(--dt-row-stripe), 0.05);
|
||||
}
|
||||
table.dataTable.table-striped > tbody > tr.odd.selected > * {
|
||||
table.table.dataTable.table-striped > tbody > tr:nth-of-type(2n+1).selected > * {
|
||||
box-shadow: inset 0 0 0 9999px rgba(13, 110, 253, 0.95);
|
||||
box-shadow: inset 0 0 0 9999px rgba(var(--dt-row-selected), 0.95);
|
||||
}
|
||||
table.dataTable.table-hover > tbody > tr:hover > * {
|
||||
table.table.dataTable.table-hover > tbody > tr:hover > * {
|
||||
box-shadow: inset 0 0 0 9999px rgba(var(--dt-row-hover), 0.075);
|
||||
}
|
||||
table.dataTable.table-hover > tbody > tr.selected:hover > * {
|
||||
table.table.dataTable.table-hover > tbody > tr.selected:hover > * {
|
||||
box-shadow: inset 0 0 0 9999px rgba(13, 110, 253, 0.975);
|
||||
box-shadow: inset 0 0 0 9999px rgba(var(--dt-row-selected), 0.975);
|
||||
}
|
||||
|
||||
div.dataTables_wrapper div.dataTables_length label {
|
||||
div.dt-container div.dt-length label {
|
||||
font-weight: normal;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
}
|
||||
div.dataTables_wrapper div.dataTables_length select {
|
||||
div.dt-container div.dt-length select {
|
||||
width: auto;
|
||||
display: inline-block;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
div.dataTables_wrapper div.dataTables_filter {
|
||||
div.dt-container div.dt-search {
|
||||
text-align: right;
|
||||
}
|
||||
div.dataTables_wrapper div.dataTables_filter label {
|
||||
div.dt-container div.dt-search label {
|
||||
font-weight: normal;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
}
|
||||
div.dataTables_wrapper div.dataTables_filter input {
|
||||
div.dt-container div.dt-search input {
|
||||
margin-left: 0.5em;
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
}
|
||||
div.dataTables_wrapper div.dataTables_info {
|
||||
div.dt-container div.dt-info {
|
||||
padding-top: 0.85em;
|
||||
}
|
||||
div.dataTables_wrapper div.dataTables_paginate {
|
||||
div.dt-container div.dt-paging {
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
text-align: right;
|
||||
}
|
||||
div.dataTables_wrapper div.dataTables_paginate ul.pagination {
|
||||
div.dt-container div.dt-paging ul.pagination {
|
||||
margin: 2px 0;
|
||||
white-space: nowrap;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
div.dataTables_wrapper div.dt-row {
|
||||
div.dt-container div.dt-row {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
div.dataTables_scrollHead table.dataTable {
|
||||
div.dt-scroll-head table.dataTable {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
div.dataTables_scrollBody > table {
|
||||
div.dt-scroll-body {
|
||||
border-bottom-color: var(--bs-border-color);
|
||||
border-bottom-width: var(--bs-border-width);
|
||||
border-bottom-style: solid;
|
||||
}
|
||||
div.dt-scroll-body > table {
|
||||
border-top: none;
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
div.dataTables_scrollBody > table > thead .sorting:before,
|
||||
div.dataTables_scrollBody > table > thead .sorting_asc:before,
|
||||
div.dataTables_scrollBody > table > thead .sorting_desc:before,
|
||||
div.dataTables_scrollBody > table > thead .sorting:after,
|
||||
div.dataTables_scrollBody > table > thead .sorting_asc:after,
|
||||
div.dataTables_scrollBody > table > thead .sorting_desc:after {
|
||||
display: none;
|
||||
div.dt-scroll-body > table > tbody > tr:first-child {
|
||||
border-top-width: 0;
|
||||
}
|
||||
div.dataTables_scrollBody > table > tbody tr:first-child th,
|
||||
div.dataTables_scrollBody > table > tbody tr:first-child td {
|
||||
border-top: none;
|
||||
div.dt-scroll-body > table > thead > tr {
|
||||
border-width: 0 !important;
|
||||
}
|
||||
div.dt-scroll-body > table > tbody > tr:last-child > * {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
div.dataTables_scrollFoot > .dataTables_scrollFootInner {
|
||||
div.dt-scroll-foot > .dt-scroll-footInner {
|
||||
box-sizing: content-box;
|
||||
}
|
||||
div.dataTables_scrollFoot > .dataTables_scrollFootInner > table {
|
||||
div.dt-scroll-foot > .dt-scroll-footInner > table {
|
||||
margin-top: 0 !important;
|
||||
border-top: none;
|
||||
}
|
||||
div.dt-scroll-foot > .dt-scroll-footInner > table > tfoot > tr:first-child {
|
||||
border-top-width: 0 !important;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 767px) {
|
||||
div.dataTables_wrapper div.dataTables_length,
|
||||
div.dataTables_wrapper div.dataTables_filter,
|
||||
div.dataTables_wrapper div.dataTables_info,
|
||||
div.dataTables_wrapper div.dataTables_paginate {
|
||||
div.dt-container div.dt-length,
|
||||
div.dt-container div.dt-search,
|
||||
div.dt-container div.dt-info,
|
||||
div.dt-container div.dt-paging {
|
||||
text-align: center;
|
||||
}
|
||||
div.dataTables_wrapper div.dataTables_paginate ul.pagination {
|
||||
div.dt-container .row {
|
||||
--bs-gutter-y: 0.5rem;
|
||||
}
|
||||
div.dt-container div.dt-paging ul.pagination {
|
||||
justify-content: center !important;
|
||||
}
|
||||
}
|
||||
table.dataTable.table-sm > thead > tr > th:not(.sorting_disabled) {
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
table.table-bordered.dataTable {
|
||||
border-right-width: 0;
|
||||
}
|
||||
table.table-bordered.dataTable thead tr:first-child th,
|
||||
table.table-bordered.dataTable thead tr:first-child td {
|
||||
border-top-width: 1px;
|
||||
}
|
||||
table.table-bordered.dataTable th,
|
||||
table.table-bordered.dataTable td {
|
||||
border-left-width: 0;
|
||||
}
|
||||
table.table-bordered.dataTable th:first-child, table.table-bordered.dataTable th:first-child,
|
||||
table.table-bordered.dataTable td:first-child,
|
||||
table.table-bordered.dataTable td:first-child {
|
||||
border-left-width: 1px;
|
||||
}
|
||||
table.table-bordered.dataTable th:last-child, table.table-bordered.dataTable th:last-child,
|
||||
table.table-bordered.dataTable td:last-child,
|
||||
table.table-bordered.dataTable td:last-child {
|
||||
border-right-width: 1px;
|
||||
}
|
||||
table.table-bordered.dataTable th,
|
||||
table.table-bordered.dataTable td {
|
||||
border-bottom-width: 1px;
|
||||
table.dataTable.table-sm > thead > tr > th:not(.sorting_disabled):before, table.dataTable.table-sm > thead > tr > th:not(.sorting_disabled):after {
|
||||
right: 5px;
|
||||
}
|
||||
|
||||
div.dataTables_scrollHead table.table-bordered {
|
||||
div.dt-scroll-head table.table-bordered {
|
||||
border-bottom-width: 0;
|
||||
}
|
||||
|
||||
div.table-responsive > div.dataTables_wrapper > div.row {
|
||||
div.table-responsive > div.dt-container > div.row {
|
||||
margin: 0;
|
||||
}
|
||||
div.table-responsive > div.dataTables_wrapper > div.row > div[class^=col-]:first-child {
|
||||
div.table-responsive > div.dt-container > div.row > div[class^=col-]:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
div.table-responsive > div.dataTables_wrapper > div.row > div[class^=col-]:last-child {
|
||||
div.table-responsive > div.dt-container > div.row > div[class^=col-]:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
|
|
11387
src/static/scripts/datatables.js
gevendort
11387
src/static/scripts/datatables.js
gevendort
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
|
@ -1,12 +1,12 @@
|
|||
/*!
|
||||
* jQuery JavaScript Library v3.7.0 -ajax,-ajax/jsonp,-ajax/load,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-deprecated/ajax-event-alias,-effects,-effects/animatedSelector,-effects/Tween
|
||||
* jQuery JavaScript Library v3.7.1 -ajax,-ajax/jsonp,-ajax/load,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-deprecated/ajax-event-alias,-effects,-effects/animatedSelector,-effects/Tween
|
||||
* https://jquery.com/
|
||||
*
|
||||
* Copyright OpenJS Foundation and other contributors
|
||||
* Released under the MIT license
|
||||
* https://jquery.org/license
|
||||
*
|
||||
* Date: 2023-05-11T18:29Z
|
||||
* Date: 2023-08-28T13:37Z
|
||||
*/
|
||||
( function( global, factory ) {
|
||||
|
||||
|
@ -147,7 +147,7 @@ function toType( obj ) {
|
|||
|
||||
|
||||
|
||||
var version = "3.7.0 -ajax,-ajax/jsonp,-ajax/load,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-deprecated/ajax-event-alias,-effects,-effects/animatedSelector,-effects/Tween",
|
||||
var version = "3.7.1 -ajax,-ajax/jsonp,-ajax/load,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-deprecated/ajax-event-alias,-effects,-effects/animatedSelector,-effects/Tween",
|
||||
|
||||
rhtmlSuffix = /HTML$/i,
|
||||
|
||||
|
@ -411,9 +411,14 @@ jQuery.extend( {
|
|||
// Do not traverse comment nodes
|
||||
ret += jQuery.text( node );
|
||||
}
|
||||
} else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) {
|
||||
}
|
||||
if ( nodeType === 1 || nodeType === 11 ) {
|
||||
return elem.textContent;
|
||||
} else if ( nodeType === 3 || nodeType === 4 ) {
|
||||
}
|
||||
if ( nodeType === 9 ) {
|
||||
return elem.documentElement.textContent;
|
||||
}
|
||||
if ( nodeType === 3 || nodeType === 4 ) {
|
||||
return elem.nodeValue;
|
||||
}
|
||||
|
||||
|
@ -1126,12 +1131,17 @@ function setDocument( node ) {
|
|||
documentElement.msMatchesSelector;
|
||||
|
||||
// Support: IE 9 - 11+, Edge 12 - 18+
|
||||
// Accessing iframe documents after unload throws "permission denied" errors (see trac-13936)
|
||||
// Support: IE 11+, Edge 17 - 18+
|
||||
// IE/Edge sometimes throw a "Permission denied" error when strict-comparing
|
||||
// two documents; shallow comparisons work.
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if ( preferredDoc != document &&
|
||||
// Accessing iframe documents after unload throws "permission denied" errors
|
||||
// (see trac-13936).
|
||||
// Limit the fix to IE & Edge Legacy; despite Edge 15+ implementing `matches`,
|
||||
// all IE 9+ and Edge Legacy versions implement `msMatchesSelector` as well.
|
||||
if ( documentElement.msMatchesSelector &&
|
||||
|
||||
// Support: IE 11+, Edge 17 - 18+
|
||||
// IE/Edge sometimes throw a "Permission denied" error when strict-comparing
|
||||
// two documents; shallow comparisons work.
|
||||
// eslint-disable-next-line eqeqeq
|
||||
preferredDoc != document &&
|
||||
( subWindow = document.defaultView ) && subWindow.top !== subWindow ) {
|
||||
|
||||
// Support: IE 9 - 11+, Edge 12 - 18+
|
||||
|
@ -2694,12 +2704,12 @@ jQuery.find = find;
|
|||
jQuery.expr[ ":" ] = jQuery.expr.pseudos;
|
||||
jQuery.unique = jQuery.uniqueSort;
|
||||
|
||||
// These have always been private, but they used to be documented
|
||||
// as part of Sizzle so let's maintain them in the 3.x line
|
||||
// for backwards compatibility purposes.
|
||||
// These have always been private, but they used to be documented as part of
|
||||
// Sizzle so let's maintain them for now for backwards compatibility purposes.
|
||||
find.compile = compile;
|
||||
find.select = select;
|
||||
find.setDocument = setDocument;
|
||||
find.tokenize = tokenize;
|
||||
|
||||
find.escape = jQuery.escapeSelector;
|
||||
find.getText = jQuery.text;
|
||||
|
@ -5913,7 +5923,7 @@ function domManip( collection, args, callback, ignored ) {
|
|||
if ( hasScripts ) {
|
||||
doc = scripts[ scripts.length - 1 ].ownerDocument;
|
||||
|
||||
// Reenable scripts
|
||||
// Re-enable scripts
|
||||
jQuery.map( scripts, restoreScript );
|
||||
|
||||
// Evaluate executable scripts on first document insertion
|
||||
|
@ -6370,7 +6380,7 @@ var rboxStyle = new RegExp( cssExpand.join( "|" ), "i" );
|
|||
trChild = document.createElement( "div" );
|
||||
|
||||
table.style.cssText = "position:absolute;left:-11111px;border-collapse:separate";
|
||||
tr.style.cssText = "border:1px solid";
|
||||
tr.style.cssText = "box-sizing:content-box;border:1px solid";
|
||||
|
||||
// Support: Chrome 86+
|
||||
// Height set through cssText does not get applied.
|
||||
|
@ -6382,7 +6392,7 @@ var rboxStyle = new RegExp( cssExpand.join( "|" ), "i" );
|
|||
// In our bodyBackground.html iframe,
|
||||
// display for all div elements is set to "inline",
|
||||
// which causes a problem only in Android 8 Chrome 86.
|
||||
// Ensuring the div is display: block
|
||||
// Ensuring the div is `display: block`
|
||||
// gets around this issue.
|
||||
trChild.style.display = "block";
|
||||
|
||||
|
@ -8451,7 +8461,9 @@ jQuery.fn.extend( {
|
|||
},
|
||||
|
||||
hover: function( fnOver, fnOut ) {
|
||||
return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver );
|
||||
return this
|
||||
.on( "mouseenter", fnOver )
|
||||
.on( "mouseleave", fnOut || fnOver );
|
||||
}
|
||||
} );
|
||||
|
|
@ -28,7 +28,7 @@
|
|||
<dd class="col-sm-7">
|
||||
<span id="web-installed">{{page_data.web_vault_version}}</span>
|
||||
</dd>
|
||||
{{#unless page_data.running_within_docker}}
|
||||
{{#unless page_data.running_within_container}}
|
||||
<dt class="col-sm-5">Web Latest
|
||||
<span class="badge bg-secondary d-none" id="web-failed" title="Unable to determine latest version.">Unknown</span>
|
||||
</dt>
|
||||
|
@ -59,12 +59,12 @@
|
|||
<dd class="col-sm-7">
|
||||
<span class="d-block"><b>{{ page_data.host_os }} / {{ page_data.host_arch }}</b></span>
|
||||
</dd>
|
||||
<dt class="col-sm-5">Running within Docker</dt>
|
||||
<dt class="col-sm-5">Running within a container</dt>
|
||||
<dd class="col-sm-7">
|
||||
{{#if page_data.running_within_docker}}
|
||||
<span class="d-block"><b>Yes (Base: {{ page_data.docker_base_image }})</b></span>
|
||||
{{#if page_data.running_within_container}}
|
||||
<span class="d-block"><b>Yes (Base: {{ page_data.container_base_image }})</b></span>
|
||||
{{/if}}
|
||||
{{#unless page_data.running_within_docker}}
|
||||
{{#unless page_data.running_within_container}}
|
||||
<span class="d-block"><b>No</b></span>
|
||||
{{/unless}}
|
||||
</dd>
|
||||
|
|
|
@ -59,7 +59,7 @@
|
|||
</main>
|
||||
|
||||
<link rel="stylesheet" href="{{urlpath}}/vw_static/datatables.css" />
|
||||
<script src="{{urlpath}}/vw_static/jquery-3.7.0.slim.js"></script>
|
||||
<script src="{{urlpath}}/vw_static/jquery-3.7.1.slim.js"></script>
|
||||
<script src="{{urlpath}}/vw_static/datatables.js"></script>
|
||||
<script src="{{urlpath}}/vw_static/admin_organizations.js"></script>
|
||||
<script src="{{urlpath}}/vw_static/jdenticon.js"></script>
|
||||
|
|
|
@ -140,7 +140,7 @@
|
|||
</main>
|
||||
|
||||
<link rel="stylesheet" href="{{urlpath}}/vw_static/datatables.css" />
|
||||
<script src="{{urlpath}}/vw_static/jquery-3.7.0.slim.js"></script>
|
||||
<script src="{{urlpath}}/vw_static/jquery-3.7.1.slim.js"></script>
|
||||
<script src="{{urlpath}}/vw_static/datatables.js"></script>
|
||||
<script src="{{urlpath}}/vw_static/admin_users.js"></script>
|
||||
<script src="{{urlpath}}/vw_static/jdenticon.js"></script>
|
||||
|
|
|
@ -2,5 +2,5 @@ Your Email Change
|
|||
<!---------------->
|
||||
To finalize changing your email address enter the following code in web vault: {{token}}
|
||||
|
||||
If you did not try to change an email address, you can safely ignore this email.
|
||||
If you did not try to change your email address, contact your administrator.
|
||||
{{> email/email_footer_text }}
|
||||
|
|
|
@ -9,7 +9,7 @@ Your Email Change
|
|||
</tr>
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
|
||||
If you did not try to change an email address, you can safely ignore this email.
|
||||
If you did not try to change your email address, contact your administrator.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
316
src/util.rs
316
src/util.rs
|
@ -1,13 +1,10 @@
|
|||
//
|
||||
// Web Headers and caching
|
||||
//
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
io::{Cursor, ErrorKind},
|
||||
ops::Deref,
|
||||
};
|
||||
use std::{collections::HashMap, io::Cursor, ops::Deref, path::Path};
|
||||
|
||||
use num_traits::ToPrimitive;
|
||||
use once_cell::sync::Lazy;
|
||||
use rocket::{
|
||||
fairing::{Fairing, Info, Kind},
|
||||
http::{ContentType, Header, HeaderMap, Method, Status},
|
||||
|
@ -218,7 +215,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 = chrono::Local::now();
|
||||
let expiry_time = time_now + chrono::Duration::seconds(self.ttl.try_into().unwrap());
|
||||
let expiry_time = time_now + chrono::TimeDelta::try_seconds(self.ttl.try_into().unwrap()).unwrap();
|
||||
res.set_raw_header("Expires", format_datetime_http(&expiry_time));
|
||||
Ok(res)
|
||||
}
|
||||
|
@ -334,40 +331,6 @@ impl Fairing for BetterLogging {
|
|||
}
|
||||
}
|
||||
|
||||
//
|
||||
// File handling
|
||||
//
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
io::Result as IOResult,
|
||||
path::Path,
|
||||
};
|
||||
|
||||
pub fn file_exists(path: &str) -> bool {
|
||||
Path::new(path).exists()
|
||||
}
|
||||
|
||||
pub fn write_file(path: &str, content: &[u8]) -> Result<(), crate::error::Error> {
|
||||
use std::io::Write;
|
||||
let mut f = match File::create(path) {
|
||||
Ok(file) => file,
|
||||
Err(e) => {
|
||||
if e.kind() == ErrorKind::PermissionDenied {
|
||||
error!("Can't create '{}': Permission denied", path);
|
||||
}
|
||||
return Err(From::from(e));
|
||||
}
|
||||
};
|
||||
|
||||
f.write_all(content)?;
|
||||
f.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delete_file(path: &str) -> IOResult<()> {
|
||||
fs::remove_file(path)
|
||||
}
|
||||
|
||||
pub fn get_display_size(size: i64) -> String {
|
||||
const UNITS: [&str; 6] = ["bytes", "KB", "MB", "GB", "TB", "PB"];
|
||||
|
||||
|
@ -444,7 +407,7 @@ pub fn get_env_str_value(key: &str) -> Option<String> {
|
|||
match (value_from_env, value_file) {
|
||||
(Ok(_), Ok(_)) => panic!("You should not define both {key} and {key_file}!"),
|
||||
(Ok(v_env), Err(_)) => Some(v_env),
|
||||
(Err(_), Ok(v_file)) => match fs::read_to_string(v_file) {
|
||||
(Err(_), Ok(v_file)) => match std::fs::read_to_string(v_file) {
|
||||
Ok(content) => Some(content.trim().to_string()),
|
||||
Err(e) => panic!("Failed to load {key}: {e:?}"),
|
||||
},
|
||||
|
@ -531,14 +494,17 @@ pub fn parse_date(date: &str) -> NaiveDateTime {
|
|||
// Deployment environment methods
|
||||
//
|
||||
|
||||
/// Returns true if the program is running in Docker or Podman.
|
||||
pub fn is_running_in_docker() -> bool {
|
||||
Path::new("/.dockerenv").exists() || Path::new("/run/.containerenv").exists()
|
||||
/// Returns true if the program is running in Docker, Podman or Kubernetes.
|
||||
pub fn is_running_in_container() -> bool {
|
||||
Path::new("/.dockerenv").exists()
|
||||
|| Path::new("/run/.containerenv").exists()
|
||||
|| Path::new("/run/secrets/kubernetes.io").exists()
|
||||
|| Path::new("/var/run/secrets/kubernetes.io").exists()
|
||||
}
|
||||
|
||||
/// Simple check to determine on which docker base image vaultwarden is running.
|
||||
/// Simple check to determine on which container base image vaultwarden is running.
|
||||
/// We build images based upon Debian or Alpine, so these we check here.
|
||||
pub fn docker_base_image() -> &'static str {
|
||||
pub fn container_base_image() -> &'static str {
|
||||
if Path::new("/etc/debian_version").exists() {
|
||||
"Debian"
|
||||
} else if Path::new("/etc/alpine-release").exists() {
|
||||
|
@ -555,7 +521,7 @@ pub fn docker_base_image() -> &'static str {
|
|||
use std::fmt;
|
||||
|
||||
use serde::de::{self, DeserializeOwned, Deserializer, MapAccess, SeqAccess, Visitor};
|
||||
use serde_json::{self, Value};
|
||||
use serde_json::Value;
|
||||
|
||||
pub type JsonMap = serde_json::Map<String, Value>;
|
||||
|
||||
|
@ -736,14 +702,9 @@ where
|
|||
|
||||
use reqwest::{header, Client, ClientBuilder};
|
||||
|
||||
pub fn get_reqwest_client() -> Client {
|
||||
match get_reqwest_client_builder().build() {
|
||||
Ok(client) => client,
|
||||
Err(e) => {
|
||||
error!("Possible trust-dns error, trying with trust-dns disabled: '{e}'");
|
||||
get_reqwest_client_builder().trust_dns(false).build().expect("Failed to build client")
|
||||
}
|
||||
}
|
||||
pub fn get_reqwest_client() -> &'static Client {
|
||||
static INSTANCE: Lazy<Client> = Lazy::new(|| get_reqwest_client_builder().build().expect("Failed to build client"));
|
||||
&INSTANCE
|
||||
}
|
||||
|
||||
pub fn get_reqwest_client_builder() -> ClientBuilder {
|
||||
|
@ -802,3 +763,248 @@ pub fn parse_experimental_client_feature_flags(experimental_client_feature_flags
|
|||
|
||||
feature_states
|
||||
}
|
||||
|
||||
mod dns_resolver {
|
||||
use std::{
|
||||
fmt,
|
||||
net::{IpAddr, SocketAddr},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use hickory_resolver::{system_conf::read_system_conf, TokioAsyncResolver};
|
||||
use once_cell::sync::Lazy;
|
||||
use reqwest::dns::{Name, Resolve, Resolving};
|
||||
|
||||
use crate::{util::is_global, CONFIG};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum CustomResolverError {
|
||||
Blacklist {
|
||||
domain: String,
|
||||
},
|
||||
NonGlobalIp {
|
||||
domain: String,
|
||||
ip: IpAddr,
|
||||
},
|
||||
}
|
||||
|
||||
impl CustomResolverError {
|
||||
pub fn downcast_ref(e: &dyn std::error::Error) -> Option<&Self> {
|
||||
let mut source = e.source();
|
||||
|
||||
while let Some(err) = source {
|
||||
source = err.source();
|
||||
if let Some(err) = err.downcast_ref::<CustomResolverError>() {
|
||||
return Some(err);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for CustomResolverError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Blacklist {
|
||||
domain,
|
||||
} => write!(f, "Blacklisted domain: {domain} matched ICON_BLACKLIST_REGEX"),
|
||||
Self::NonGlobalIp {
|
||||
domain,
|
||||
ip,
|
||||
} => write!(f, "IP {ip} for domain '{domain}' is not a global IP!"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for CustomResolverError {}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum CustomDnsResolver {
|
||||
Default(),
|
||||
Hickory(Arc<TokioAsyncResolver>),
|
||||
}
|
||||
type BoxError = Box<dyn std::error::Error + Send + Sync>;
|
||||
|
||||
impl CustomDnsResolver {
|
||||
pub fn instance() -> Arc<Self> {
|
||||
static INSTANCE: Lazy<Arc<CustomDnsResolver>> = Lazy::new(CustomDnsResolver::new);
|
||||
Arc::clone(&*INSTANCE)
|
||||
}
|
||||
|
||||
fn new() -> Arc<Self> {
|
||||
match read_system_conf() {
|
||||
Ok((config, opts)) => {
|
||||
let resolver = TokioAsyncResolver::tokio(config.clone(), opts.clone());
|
||||
Arc::new(Self::Hickory(Arc::new(resolver)))
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Error creating Hickory resolver, falling back to default: {e:?}");
|
||||
Arc::new(Self::Default())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Note that we get an iterator of addresses, but we only grab the first one for convenience
|
||||
async fn resolve_domain(&self, name: &str) -> Result<Option<SocketAddr>, BoxError> {
|
||||
pre_resolve(name)?;
|
||||
|
||||
let result = match self {
|
||||
Self::Default() => tokio::net::lookup_host(name).await?.next(),
|
||||
Self::Hickory(r) => r.lookup_ip(name).await?.iter().next().map(|a| SocketAddr::new(a, 0)),
|
||||
};
|
||||
|
||||
if let Some(addr) = &result {
|
||||
post_resolve(name, addr.ip())?;
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
fn pre_resolve(name: &str) -> Result<(), CustomResolverError> {
|
||||
if crate::api::is_domain_blacklisted(name) {
|
||||
return Err(CustomResolverError::Blacklist {
|
||||
domain: name.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn post_resolve(name: &str, ip: IpAddr) -> Result<(), CustomResolverError> {
|
||||
if CONFIG.icon_blacklist_non_global_ips() && !is_global(ip) {
|
||||
Err(CustomResolverError::NonGlobalIp {
|
||||
domain: name.to_string(),
|
||||
ip,
|
||||
})
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve for CustomDnsResolver {
|
||||
fn resolve(&self, name: Name) -> Resolving {
|
||||
let this = self.clone();
|
||||
Box::pin(async move {
|
||||
let name = name.as_str();
|
||||
let result = this.resolve_domain(name).await?;
|
||||
Ok::<reqwest::dns::Addrs, _>(Box::new(result.into_iter()))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub use dns_resolver::{CustomDnsResolver, CustomResolverError};
|
||||
|
||||
/// TODO: This is extracted from IpAddr::is_global, which is unstable:
|
||||
/// https://doc.rust-lang.org/nightly/std/net/enum.IpAddr.html#method.is_global
|
||||
/// Remove once https://github.com/rust-lang/rust/issues/27709 is merged
|
||||
#[allow(clippy::nonminimal_bool)]
|
||||
#[cfg(any(not(feature = "unstable"), test))]
|
||||
pub fn is_global_hardcoded(ip: std::net::IpAddr) -> bool {
|
||||
match ip {
|
||||
std::net::IpAddr::V4(ip) => {
|
||||
!(ip.octets()[0] == 0 // "This network"
|
||||
|| ip.is_private()
|
||||
|| (ip.octets()[0] == 100 && (ip.octets()[1] & 0b1100_0000 == 0b0100_0000)) //ip.is_shared()
|
||||
|| ip.is_loopback()
|
||||
|| ip.is_link_local()
|
||||
// addresses reserved for future protocols (`192.0.0.0/24`)
|
||||
||(ip.octets()[0] == 192 && ip.octets()[1] == 0 && ip.octets()[2] == 0)
|
||||
|| ip.is_documentation()
|
||||
|| (ip.octets()[0] == 198 && (ip.octets()[1] & 0xfe) == 18) // ip.is_benchmarking()
|
||||
|| (ip.octets()[0] & 240 == 240 && !ip.is_broadcast()) //ip.is_reserved()
|
||||
|| ip.is_broadcast())
|
||||
}
|
||||
std::net::IpAddr::V6(ip) => {
|
||||
!(ip.is_unspecified()
|
||||
|| ip.is_loopback()
|
||||
// IPv4-mapped Address (`::ffff:0:0/96`)
|
||||
|| matches!(ip.segments(), [0, 0, 0, 0, 0, 0xffff, _, _])
|
||||
// IPv4-IPv6 Translat. (`64:ff9b:1::/48`)
|
||||
|| matches!(ip.segments(), [0x64, 0xff9b, 1, _, _, _, _, _])
|
||||
// Discard-Only Address Block (`100::/64`)
|
||||
|| matches!(ip.segments(), [0x100, 0, 0, 0, _, _, _, _])
|
||||
// IETF Protocol Assignments (`2001::/23`)
|
||||
|| (matches!(ip.segments(), [0x2001, b, _, _, _, _, _, _] if b < 0x200)
|
||||
&& !(
|
||||
// Port Control Protocol Anycast (`2001:1::1`)
|
||||
u128::from_be_bytes(ip.octets()) == 0x2001_0001_0000_0000_0000_0000_0000_0001
|
||||
// Traversal Using Relays around NAT Anycast (`2001:1::2`)
|
||||
|| u128::from_be_bytes(ip.octets()) == 0x2001_0001_0000_0000_0000_0000_0000_0002
|
||||
// AMT (`2001:3::/32`)
|
||||
|| matches!(ip.segments(), [0x2001, 3, _, _, _, _, _, _])
|
||||
// AS112-v6 (`2001:4:112::/48`)
|
||||
|| matches!(ip.segments(), [0x2001, 4, 0x112, _, _, _, _, _])
|
||||
// ORCHIDv2 (`2001:20::/28`)
|
||||
|| matches!(ip.segments(), [0x2001, b, _, _, _, _, _, _] if (0x20..=0x2F).contains(&b))
|
||||
))
|
||||
|| ((ip.segments()[0] == 0x2001) && (ip.segments()[1] == 0xdb8)) // ip.is_documentation()
|
||||
|| ((ip.segments()[0] & 0xfe00) == 0xfc00) //ip.is_unique_local()
|
||||
|| ((ip.segments()[0] & 0xffc0) == 0xfe80)) //ip.is_unicast_link_local()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "unstable"))]
|
||||
pub use is_global_hardcoded as is_global;
|
||||
|
||||
#[cfg(feature = "unstable")]
|
||||
#[inline(always)]
|
||||
pub fn is_global(ip: std::net::IpAddr) -> bool {
|
||||
ip.is_global()
|
||||
}
|
||||
|
||||
/// These are some tests to check that the implementations match
|
||||
/// The IPv4 can be all checked in 30 seconds or so and they are correct as of nightly 2023-07-17
|
||||
/// The IPV6 can't be checked in a reasonable time, so we check over a hundred billion random ones, so far correct
|
||||
/// Note that the is_global implementation is subject to change as new IP RFCs are created
|
||||
///
|
||||
/// To run while showing progress output:
|
||||
/// cargo +nightly test --release --features sqlite,unstable -- --nocapture --ignored
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "unstable")]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::net::IpAddr;
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_ipv4_global() {
|
||||
for a in 0..u8::MAX {
|
||||
println!("Iter: {}/255", a);
|
||||
for b in 0..u8::MAX {
|
||||
for c in 0..u8::MAX {
|
||||
for d in 0..u8::MAX {
|
||||
let ip = IpAddr::V4(std::net::Ipv4Addr::new(a, b, c, d));
|
||||
assert_eq!(ip.is_global(), is_global_hardcoded(ip), "IP mismatch: {}", ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_ipv6_global() {
|
||||
use rand::Rng;
|
||||
|
||||
std::thread::scope(|s| {
|
||||
for t in 0..16 {
|
||||
let handle = s.spawn(move || {
|
||||
let mut v = [0u8; 16];
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
for i in 0..20 {
|
||||
println!("Thread {t} Iter: {i}/50");
|
||||
for _ in 0..500_000_000 {
|
||||
rng.fill(&mut v);
|
||||
let ip = IpAddr::V6(std::net::Ipv6Addr::from(v));
|
||||
assert_eq!(ip.is_global(), is_global_hardcoded(ip), "IP mismatch: {ip}");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,19 +10,19 @@ import urllib.request
|
|||
|
||||
from collections import OrderedDict
|
||||
|
||||
if not (2 <= len(sys.argv) <= 3):
|
||||
print("usage: %s <OUTPUT-FILE> [GIT-REF]" % sys.argv[0])
|
||||
if not 2 <= len(sys.argv) <= 3:
|
||||
print(f"usage: {sys.argv[0]} <OUTPUT-FILE> [GIT-REF]")
|
||||
print()
|
||||
print("This script generates a global equivalent domains JSON file from")
|
||||
print("the upstream Bitwarden source repo.")
|
||||
sys.exit(1)
|
||||
|
||||
OUTPUT_FILE = sys.argv[1]
|
||||
GIT_REF = 'master' if len(sys.argv) == 2 else sys.argv[2]
|
||||
GIT_REF = 'main' if len(sys.argv) == 2 else sys.argv[2]
|
||||
|
||||
BASE_URL = 'https://github.com/bitwarden/server/raw/%s' % GIT_REF
|
||||
ENUMS_URL = '%s/src/Core/Enums/GlobalEquivalentDomainsType.cs' % BASE_URL
|
||||
DOMAIN_LISTS_URL = '%s/src/Core/Utilities/StaticStore.cs' % BASE_URL
|
||||
BASE_URL = f'https://github.com/bitwarden/server/raw/{GIT_REF}'
|
||||
ENUMS_URL = f'{BASE_URL}/src/Core/Enums/GlobalEquivalentDomainsType.cs'
|
||||
DOMAIN_LISTS_URL = f'{BASE_URL}/src/Core/Utilities/StaticStore.cs'
|
||||
|
||||
# Enum lines look like:
|
||||
#
|
||||
|
@ -77,5 +77,5 @@ for name, domain_list in domain_lists.items():
|
|||
global_domains.append(entry)
|
||||
|
||||
# Write out the global domains JSON file.
|
||||
with open(OUTPUT_FILE, 'w') as f:
|
||||
with open(file=OUTPUT_FILE, mode='w', encoding='utf-8') as f:
|
||||
json.dump(global_domains, f, indent=2)
|
||||
|
|
Laden …
In neuem Issue referenzieren