mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2025-09-10 10:45:57 +03:00
Compare commits
76 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
1c7338c7c4 | ||
|
08f37b9935 | ||
|
4826ddca4c | ||
|
2b32b6f78c | ||
|
a6cfdddfd8 | ||
|
814ce9a6ac | ||
|
1bee46f64b | ||
|
556d945396 | ||
|
664b480c71 | ||
|
84e901b7d2 | ||
|
839b2bc950 | ||
|
6050c8dac5 | ||
|
0a6b797e6e | ||
|
fb6f441a4f | ||
|
9876aedd67 | ||
|
19e671ff25 | ||
|
60964c07e6 | ||
|
e4894524e4 | ||
|
e7f083dee9 | ||
|
1074315a87 | ||
|
c56bf38079 | ||
|
3c0cac623d | ||
|
550794b127 | ||
|
e818a0bf37 | ||
|
2aedff50e8 | ||
|
84a23008f4 | ||
|
44e9e1a58e | ||
|
e4606431d1 | ||
|
5b7d7390b0 | ||
|
a05187c0ff | ||
|
8e34495e73 | ||
|
4219249e11 | ||
|
bd883de70e | ||
|
2d66292350 | ||
|
adf67a8ee8 | ||
|
f40f5b8399 | ||
|
2d6ca0ea95 | ||
|
06a10e2c5a | ||
|
445680fb84 | ||
|
83376544d8 | ||
|
04a17dcdef | ||
|
0851561392 | ||
|
95cd6deda6 | ||
|
636f16dc66 | ||
|
9e5b049dca | ||
|
23aa9088f3 | ||
|
4f0ed06b06 | ||
|
349c97efaf | ||
|
8b05a5d192 | ||
|
83bf77d713 | ||
|
4d5c047ddc | ||
|
147c9c7b50 | ||
|
6515a2fcad | ||
|
4a2ed553df | ||
|
ba492c0602 | ||
|
1ec049e2b5 | ||
|
0fb8563b13 | ||
|
f906f6230a | ||
|
951ba55123 | ||
|
18abf226be | ||
|
393645617e | ||
|
5bf243b675 | ||
|
cfba8347a3 | ||
|
55c1b6e8d5 | ||
|
3d7e80a7aa | ||
|
5866338de4 | ||
|
271e3ae757 | ||
|
48cc31a59f | ||
|
6a7cee4e7e | ||
|
f850dbb310 | ||
|
07099df41a | ||
|
0c0a80720e | ||
|
ae437f70a3 | ||
|
3d11f4cd16 | ||
|
3bd4e42fb0 | ||
|
89e94b1d91 |
@@ -72,6 +72,13 @@
|
||||
# WEBSOCKET_ADDRESS=0.0.0.0
|
||||
# WEBSOCKET_PORT=3012
|
||||
|
||||
## Enables push notifications (requires key and id from https://bitwarden.com/host)
|
||||
# PUSH_ENABLED=true
|
||||
# PUSH_INSTALLATION_ID=CHANGEME
|
||||
# PUSH_INSTALLATION_KEY=CHANGEME
|
||||
## Don't change this unless you know what you're doing.
|
||||
# PUSH_RELAY_BASE_URI=https://push.bitwarden.com
|
||||
|
||||
## Controls whether users are allowed to create Bitwarden Sends.
|
||||
## This setting applies globally to all users.
|
||||
## To control this on a per-org basis instead, use the "Disable Send" org policy.
|
||||
@@ -264,6 +271,8 @@
|
||||
## For details see: https://github.com/dani-garcia/vaultwarden/wiki/Enabling-admin-page#secure-the-admin_token
|
||||
## If not set, the admin panel is disabled
|
||||
## New Argon2 PHC string
|
||||
## Note that for some environments, like docker-compose you need to escape all the dollar signs `$` with an extra dollar sign like `$$`
|
||||
## Also, use single quotes (') instead of double quotes (") to enclose the string when needed
|
||||
# ADMIN_TOKEN='$argon2id$v=19$m=65540,t=3,p=4$MmeKRnGK5RW5mJS7h3TOL89GrpLPXJPAtTK8FTqj9HM$DqsstvoSAETl9YhnsXbf43WeaUwJC6JhViIvuPoig78'
|
||||
## Old plain text string (Will generate warnings in favor of Argon2)
|
||||
# ADMIN_TOKEN=Vy2VyYTTsKPv8W5aEOWUbB/Bt3DEKePbHmI4m9VcemUMS2rEviDowNAFqYi1xjmp
|
||||
|
10
.github/workflows/build.yml
vendored
10
.github/workflows/build.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
# This is done globally to prevent rebuilds when the RUSTFLAGS env variable changes.
|
||||
env:
|
||||
RUSTFLAGS: "-D warnings"
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: git # Use the old git protocol until it is stable probably in 1.68 or 1.69. MSRV needs to be at this before removed.
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
steps:
|
||||
# Checkout the repo
|
||||
- name: "Checkout"
|
||||
uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
# End Checkout the repo
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
|
||||
# Only install the clippy and rustfmt components on the default rust-toolchain
|
||||
- name: "Install rust-toolchain version"
|
||||
uses: dtolnay/rust-toolchain@fc3253060d0c959bea12a59f10f8391454a0b02d # master @ 2023-03-21 - 06:36 GMT+1
|
||||
uses: dtolnay/rust-toolchain@b44cb146d03e8d870c57ab64b80f04586349ca5d # master @ 2023-03-28 - 06:32 GMT+2
|
||||
if: ${{ matrix.channel == 'rust-toolchain' }}
|
||||
with:
|
||||
toolchain: "${{steps.toolchain.outputs.RUST_TOOLCHAIN}}"
|
||||
@@ -81,7 +81,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@fc3253060d0c959bea12a59f10f8391454a0b02d # master @ 2023-03-21 - 06:36 GMT+1
|
||||
uses: dtolnay/rust-toolchain@b44cb146d03e8d870c57ab64b80f04586349ca5d # master @ 2023-03-28 - 06:32 GMT+2
|
||||
if: ${{ matrix.channel != 'rust-toolchain' }}
|
||||
with:
|
||||
toolchain: "${{steps.toolchain.outputs.RUST_TOOLCHAIN}}"
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
|
||||
|
||||
# Enable Rust Caching
|
||||
- uses: Swatinem/rust-cache@6fd3edff6979b79f87531400ad694fb7f2c84b1f # v2.2.1
|
||||
- uses: Swatinem/rust-cache@2656b87321093db1cb55fbd73183d195214fdfd1 # v2.5.0
|
||||
# End Enable Rust Caching
|
||||
|
||||
|
||||
|
2
.github/workflows/hadolint.yml
vendored
2
.github/workflows/hadolint.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
steps:
|
||||
# Checkout the repo
|
||||
- name: Checkout
|
||||
uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
# End Checkout the repo
|
||||
|
||||
|
||||
|
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@@ -73,7 +73,7 @@ jobs:
|
||||
steps:
|
||||
# Checkout the repo
|
||||
- name: Checkout
|
||||
uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -92,7 +92,7 @@ jobs:
|
||||
|
||||
# Login to Docker Hub
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a # v2.1.0
|
||||
uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -100,7 +100,7 @@ jobs:
|
||||
|
||||
# Login to GitHub Container Registry
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a # v2.1.0
|
||||
uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -109,7 +109,7 @@ jobs:
|
||||
|
||||
# Login to Quay.io
|
||||
- name: Login to Quay.io
|
||||
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a # v2.1.0
|
||||
uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0
|
||||
with:
|
||||
registry: quay.io
|
||||
username: ${{ secrets.QUAY_USERNAME }}
|
||||
|
1147
Cargo.lock
generated
1147
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
69
Cargo.toml
69
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.66.1"
|
||||
rust-version = "1.68.2"
|
||||
resolver = "2"
|
||||
|
||||
repository = "https://github.com/dani-garcia/vaultwarden"
|
||||
@@ -36,11 +36,11 @@ unstable = []
|
||||
|
||||
[target."cfg(not(windows))".dependencies]
|
||||
# Logging
|
||||
syslog = "6.0.1" # Needs to be v4 until fern is updated
|
||||
syslog = "6.1.0"
|
||||
|
||||
[dependencies]
|
||||
# Logging
|
||||
log = "0.4.17"
|
||||
log = "0.4.19"
|
||||
fern = { version = "0.6.2", features = ["syslog-6"] }
|
||||
tracing = { version = "0.1.37", features = ["log"] } # Needed to have lettre and webauthn-rs trace logging to work
|
||||
|
||||
@@ -48,17 +48,19 @@ tracing = { version = "0.1.37", features = ["log"] } # Needed to have lettre and
|
||||
dotenvy = { version = "0.15.7", default-features = false }
|
||||
|
||||
# Lazy initialization
|
||||
once_cell = "1.17.1"
|
||||
once_cell = "1.18.0"
|
||||
|
||||
# Numerical libraries
|
||||
num-traits = "0.2.15"
|
||||
num-derive = "0.3.3"
|
||||
num-derive = "0.4.0"
|
||||
|
||||
# Web framework
|
||||
rocket = { version = "0.5.0-rc.3", features = ["tls", "json"], default-features = false }
|
||||
# rocket_ws = { version ="0.1.0-rc.3" }
|
||||
rocket_ws = { git = 'https://github.com/SergioBenitez/Rocket', rev = "ce441b5f46fdf5cd99cb32b8b8638835e4c2a5fa" } # v0.5 branch
|
||||
|
||||
# WebSockets libraries
|
||||
tokio-tungstenite = "0.18.0"
|
||||
tokio-tungstenite = "0.19.0"
|
||||
rmpv = "1.0.0" # MessagePack library
|
||||
|
||||
# Concurrent HashMap used for WebSocket messaging and favicons
|
||||
@@ -66,37 +68,37 @@ dashmap = "5.4.0"
|
||||
|
||||
# Async futures
|
||||
futures = "0.3.28"
|
||||
tokio = { version = "1.27.0", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal"] }
|
||||
tokio = { version = "1.29.1", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal"] }
|
||||
|
||||
# A generic serialization/deserialization framework
|
||||
serde = { version = "1.0.159", features = ["derive"] }
|
||||
serde_json = "1.0.95"
|
||||
serde = { version = "1.0.166", features = ["derive"] }
|
||||
serde_json = "1.0.99"
|
||||
|
||||
# A safe, extensible ORM and Query builder
|
||||
diesel = { version = "2.0.3", features = ["chrono", "r2d2"] }
|
||||
diesel_migrations = "2.0.0"
|
||||
diesel_logger = { version = "0.2.0", optional = true }
|
||||
diesel = { version = "2.1.0", features = ["chrono", "r2d2"] }
|
||||
diesel_migrations = "2.1.0"
|
||||
diesel_logger = { version = "0.3.0", optional = true }
|
||||
|
||||
# Bundled/Static SQLite
|
||||
libsqlite3-sys = { version = "0.25.2", features = ["bundled"], optional = true }
|
||||
libsqlite3-sys = { version = "0.26.0", features = ["bundled"], optional = true }
|
||||
|
||||
# Crypto-related libraries
|
||||
rand = { version = "0.8.5", features = ["small_rng"] }
|
||||
ring = "0.16.20"
|
||||
|
||||
# UUID generation
|
||||
uuid = { version = "1.3.0", features = ["v4"] }
|
||||
uuid = { version = "1.4.0", features = ["v4"] }
|
||||
|
||||
# Date and time libraries
|
||||
chrono = { version = "0.4.24", features = ["clock", "serde"], default-features = false }
|
||||
chrono-tz = "0.8.1"
|
||||
time = "0.3.20"
|
||||
chrono = { version = "0.4.26", features = ["clock", "serde"], default-features = false }
|
||||
chrono-tz = "0.8.3"
|
||||
time = "0.3.22"
|
||||
|
||||
# Job scheduler
|
||||
job_scheduler_ng = "2.0.4"
|
||||
|
||||
# Data encoding library Hex/Base32/Base64
|
||||
data-encoding = "2.3.3"
|
||||
data-encoding = "2.4.0"
|
||||
|
||||
# JWT library
|
||||
jsonwebtoken = "8.3.0"
|
||||
@@ -111,40 +113,40 @@ yubico = { version = "0.11.0", features = ["online-tokio"], default-features = f
|
||||
webauthn-rs = "0.3.2"
|
||||
|
||||
# Handling of URL's for WebAuthn and favicons
|
||||
url = "2.3.1"
|
||||
url = "2.4.0"
|
||||
|
||||
# Email libraries
|
||||
lettre = { version = "0.10.3", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "tokio1-native-tls", "hostname", "tracing", "tokio1"], default-features = false }
|
||||
percent-encoding = "2.2.0" # URL encoding library used for URL's in the emails
|
||||
lettre = { version = "0.10.4", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "tokio1-native-tls", "hostname", "tracing", "tokio1"], default-features = false }
|
||||
percent-encoding = "2.3.0" # URL encoding library used for URL's in the emails
|
||||
email_address = "0.2.4"
|
||||
|
||||
# HTML Template library
|
||||
handlebars = { version = "4.3.6", features = ["dir_source"] }
|
||||
handlebars = { version = "4.3.7", features = ["dir_source"] }
|
||||
|
||||
# HTTP client (Used for favicons, version check, DUO and HIBP API)
|
||||
reqwest = { version = "0.11.16", features = ["stream", "json", "gzip", "brotli", "socks", "cookies", "trust-dns"] }
|
||||
reqwest = { version = "0.11.18", features = ["stream", "json", "gzip", "brotli", "socks", "cookies", "trust-dns"] }
|
||||
|
||||
# Favicon extraction libraries
|
||||
html5gum = "0.5.2"
|
||||
regex = { version = "1.7.3", features = ["std", "perf", "unicode-perl"], default-features = false }
|
||||
data-url = "0.2.0"
|
||||
html5gum = "0.5.3"
|
||||
regex = { version = "1.8.4", features = ["std", "perf", "unicode-perl"], default-features = false }
|
||||
data-url = "0.3.0"
|
||||
bytes = "1.4.0"
|
||||
|
||||
# Cache function results (Used for version check and favicon fetching)
|
||||
cached = "0.42.0"
|
||||
cached = "0.44.0"
|
||||
|
||||
# Used for custom short lived cookie jar during favicon extraction
|
||||
cookie = "0.16.2"
|
||||
cookie_store = "0.19.0"
|
||||
cookie_store = "0.19.1"
|
||||
|
||||
# Used by U2F, JWT and PostgreSQL
|
||||
openssl = "0.10.48"
|
||||
openssl = "0.10.55"
|
||||
|
||||
# CLI argument parsing
|
||||
pico-args = "0.5.0"
|
||||
|
||||
# Macro ident concatenation
|
||||
paste = "1.0.12"
|
||||
paste = "1.0.13"
|
||||
governor = "0.5.1"
|
||||
|
||||
# Check client versions for specific features.
|
||||
@@ -152,8 +154,7 @@ semver = "1.0.17"
|
||||
|
||||
# Allow overriding the default memory allocator
|
||||
# Mainly used for the musl builds, since the default musl malloc is very slow
|
||||
mimalloc = { version = "=0.1.34", features = ["secure"], default-features = false, optional = true }
|
||||
libmimalloc-sys = "=0.1.30"
|
||||
mimalloc = { version = "0.1.37", features = ["secure"], default-features = false, optional = true }
|
||||
which = "4.4.0"
|
||||
|
||||
# Argon2 library with support for the PHC format
|
||||
@@ -162,6 +163,10 @@ argon2 = "0.5.0"
|
||||
# Reading a password from the cli for generating the Argon2id ADMIN_TOKEN
|
||||
rpassword = "7.2.0"
|
||||
|
||||
[patch.crates-io]
|
||||
rocket = { git = 'https://github.com/SergioBenitez/Rocket', rev = 'ce441b5f46fdf5cd99cb32b8b8638835e4c2a5fa' } # v0.5 branch
|
||||
# rocket_ws = { git = 'https://github.com/SergioBenitez/Rocket', rev = 'ce441b5f46fdf5cd99cb32b8b8638835e4c2a5fa' } # v0.5 branch
|
||||
|
||||
# Strip debuginfo from the release builds
|
||||
# Also enable thin LTO for some optimizations
|
||||
[profile.release]
|
||||
|
@@ -38,7 +38,7 @@ Pull the docker image and mount a volume from the host for persistent storage:
|
||||
|
||||
```sh
|
||||
docker pull vaultwarden/server:latest
|
||||
docker run -d --name vaultwarden -v /vw-data/:/data/ -p 80:80 vaultwarden/server:latest
|
||||
docker run -d --name vaultwarden -v /vw-data/:/data/ --restart unless-stopped -p 80:80 vaultwarden/server:latest
|
||||
```
|
||||
This will preserve any persistent data under /vw-data/, you can adapt the path to whatever suits you.
|
||||
|
||||
|
2
build.rs
2
build.rs
@@ -72,7 +72,7 @@ fn version_from_git_info() -> Result<String, std::io::Error> {
|
||||
// Combined version
|
||||
if let Some(exact) = exact_tag {
|
||||
Ok(exact)
|
||||
} else if &branch != "main" && &branch != "master" {
|
||||
} else if &branch != "main" && &branch != "master" && &branch != "HEAD" {
|
||||
Ok(format!("{last_tag}-{rev_short} ({branch})"))
|
||||
} else {
|
||||
Ok(format!("{last_tag}-{rev_short}"))
|
||||
|
@@ -2,42 +2,42 @@
|
||||
|
||||
# This file was generated using a Jinja2 template.
|
||||
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
||||
{% set rust_version = "1.68.2" %}
|
||||
{% set rust_version = "1.70.0" %}
|
||||
{% set debian_version = "bullseye" %}
|
||||
{% set alpine_version = "3.17" %}
|
||||
{% set build_stage_base_image = "rust:%s-%s" % (rust_version, debian_version) %}
|
||||
{% set build_stage_base_image = "docker.io/library/rust:%s-%s" % (rust_version, debian_version) %}
|
||||
{% if "alpine" in target_file %}
|
||||
{% if "amd64" in target_file %}
|
||||
{% set build_stage_base_image = "blackdex/rust-musl:x86_64-musl-stable-%s" % rust_version %}
|
||||
{% set runtime_stage_base_image = "alpine:%s" % alpine_version %}
|
||||
{% set build_stage_base_image = "docker.io/blackdex/rust-musl:x86_64-musl-stable-%s" % rust_version %}
|
||||
{% set runtime_stage_base_image = "docker.io/library/alpine:%s" % alpine_version %}
|
||||
{% set package_arch_target = "x86_64-unknown-linux-musl" %}
|
||||
{% elif "armv7" in target_file %}
|
||||
{% set build_stage_base_image = "blackdex/rust-musl:armv7-musleabihf-stable-%s" % rust_version %}
|
||||
{% set runtime_stage_base_image = "balenalib/armv7hf-alpine:%s" % alpine_version %}
|
||||
{% set build_stage_base_image = "docker.io/blackdex/rust-musl:armv7-musleabihf-stable-%s" % rust_version %}
|
||||
{% set runtime_stage_base_image = "docker.io/balenalib/armv7hf-alpine:%s" % alpine_version %}
|
||||
{% set package_arch_target = "armv7-unknown-linux-musleabihf" %}
|
||||
{% elif "armv6" in target_file %}
|
||||
{% set build_stage_base_image = "blackdex/rust-musl:arm-musleabi-stable-%s" % rust_version %}
|
||||
{% set runtime_stage_base_image = "balenalib/rpi-alpine:%s" % alpine_version %}
|
||||
{% set build_stage_base_image = "docker.io/blackdex/rust-musl:arm-musleabi-stable-%s" % rust_version %}
|
||||
{% set runtime_stage_base_image = "docker.io/balenalib/rpi-alpine:%s" % alpine_version %}
|
||||
{% set package_arch_target = "arm-unknown-linux-musleabi" %}
|
||||
{% elif "arm64" in target_file %}
|
||||
{% set build_stage_base_image = "blackdex/rust-musl:aarch64-musl-stable-%s" % rust_version %}
|
||||
{% set runtime_stage_base_image = "balenalib/aarch64-alpine:%s" % alpine_version %}
|
||||
{% set build_stage_base_image = "docker.io/blackdex/rust-musl:aarch64-musl-stable-%s" % rust_version %}
|
||||
{% set runtime_stage_base_image = "docker.io/balenalib/aarch64-alpine:%s" % alpine_version %}
|
||||
{% set package_arch_target = "aarch64-unknown-linux-musl" %}
|
||||
{% endif %}
|
||||
{% elif "amd64" in target_file %}
|
||||
{% set runtime_stage_base_image = "debian:%s-slim" % debian_version %}
|
||||
{% set runtime_stage_base_image = "docker.io/library/debian:%s-slim" % debian_version %}
|
||||
{% elif "arm64" in target_file %}
|
||||
{% set runtime_stage_base_image = "balenalib/aarch64-debian:%s" % debian_version %}
|
||||
{% set runtime_stage_base_image = "docker.io/balenalib/aarch64-debian:%s" % debian_version %}
|
||||
{% set package_arch_name = "arm64" %}
|
||||
{% set package_arch_target = "aarch64-unknown-linux-gnu" %}
|
||||
{% set package_cross_compiler = "aarch64-linux-gnu" %}
|
||||
{% elif "armv6" in target_file %}
|
||||
{% set runtime_stage_base_image = "balenalib/rpi-debian:%s" % debian_version %}
|
||||
{% set runtime_stage_base_image = "docker.io/balenalib/rpi-debian:%s" % debian_version %}
|
||||
{% set package_arch_name = "armel" %}
|
||||
{% set package_arch_target = "arm-unknown-linux-gnueabi" %}
|
||||
{% set package_cross_compiler = "arm-linux-gnueabi" %}
|
||||
{% elif "armv7" in target_file %}
|
||||
{% set runtime_stage_base_image = "balenalib/armv7hf-debian:%s" % debian_version %}
|
||||
{% set runtime_stage_base_image = "docker.io/balenalib/armv7hf-debian:%s" % debian_version %}
|
||||
{% set package_arch_name = "armhf" %}
|
||||
{% set package_arch_target = "armv7-unknown-linux-gnueabihf" %}
|
||||
{% set package_cross_compiler = "arm-linux-gnueabihf" %}
|
||||
@@ -61,8 +61,8 @@
|
||||
# https://docs.docker.com/develop/develop-images/multistage-build/
|
||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
||||
####################### VAULT BUILD IMAGE #######################
|
||||
{% set vault_version = "v2023.3.0b" %}
|
||||
{% set vault_image_digest = "sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee" %}
|
||||
{% set vault_version = "v2023.5.0" %}
|
||||
{% set vault_image_digest = "sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085" %}
|
||||
# The web-vault digest specifies a particular web-vault build on Docker Hub.
|
||||
# Using the digest instead of the tag name provides better security,
|
||||
# as the digest of an image is immutable, whereas a tag name can later
|
||||
@@ -72,15 +72,15 @@
|
||||
# - 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 vaultwarden/web-vault:{{ vault_version }}
|
||||
# $ docker image inspect --format "{{ '{{' }}.RepoDigests}}" vaultwarden/web-vault:{{ vault_version }}
|
||||
# [vaultwarden/web-vault@{{ vault_image_digest }}]
|
||||
# $ docker pull docker.io/vaultwarden/web-vault:{{ vault_version }}
|
||||
# $ docker image inspect --format "{{ '{{' }}.RepoDigests}}" docker.io/vaultwarden/web-vault:{{ vault_version }}
|
||||
# [docker.io/vaultwarden/web-vault@{{ vault_image_digest }}]
|
||||
#
|
||||
# - Conversely, to get the tag name from the digest:
|
||||
# $ docker image inspect --format "{{ '{{' }}.RepoTags}}" vaultwarden/web-vault@{{ vault_image_digest }}
|
||||
# [vaultwarden/web-vault:{{ vault_version }}]
|
||||
# $ docker image inspect --format "{{ '{{' }}.RepoTags}}" docker.io/vaultwarden/web-vault@{{ vault_image_digest }}
|
||||
# [docker.io/vaultwarden/web-vault:{{ vault_version }}]
|
||||
#
|
||||
FROM vaultwarden/web-vault@{{ vault_image_digest }} as vault
|
||||
FROM docker.io/vaultwarden/web-vault@{{ vault_image_digest }} as vault
|
||||
|
||||
########################## BUILD IMAGE ##########################
|
||||
FROM {{ build_stage_base_image }} as build
|
||||
|
@@ -15,18 +15,18 @@
|
||||
# - 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 vaultwarden/web-vault:v2023.3.0b
|
||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b
|
||||
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee]
|
||||
# $ docker pull docker.io/vaultwarden/web-vault:v2023.5.0
|
||||
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2023.5.0
|
||||
# [docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085]
|
||||
#
|
||||
# - Conversely, to get the tag name from the digest:
|
||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee
|
||||
# [vaultwarden/web-vault:v2023.3.0b]
|
||||
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085
|
||||
# [docker.io/vaultwarden/web-vault:v2023.5.0]
|
||||
#
|
||||
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault
|
||||
FROM docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085 as vault
|
||||
|
||||
########################## BUILD IMAGE ##########################
|
||||
FROM rust:1.68.2-bullseye as build
|
||||
FROM docker.io/library/rust:1.70.0-bullseye as build
|
||||
|
||||
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
||||
ENV DEBIAN_FRONTEND=noninteractive \
|
||||
@@ -80,7 +80,7 @@ RUN cargo build --features ${DB} --release
|
||||
######################## RUNTIME IMAGE ########################
|
||||
# Create a new stage with a minimal image
|
||||
# because we already have a binary built
|
||||
FROM debian:bullseye-slim
|
||||
FROM docker.io/library/debian:bullseye-slim
|
||||
|
||||
ENV ROCKET_PROFILE="release" \
|
||||
ROCKET_ADDRESS=0.0.0.0 \
|
||||
|
@@ -15,18 +15,18 @@
|
||||
# - 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 vaultwarden/web-vault:v2023.3.0b
|
||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b
|
||||
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee]
|
||||
# $ docker pull docker.io/vaultwarden/web-vault:v2023.5.0
|
||||
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2023.5.0
|
||||
# [docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085]
|
||||
#
|
||||
# - Conversely, to get the tag name from the digest:
|
||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee
|
||||
# [vaultwarden/web-vault:v2023.3.0b]
|
||||
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085
|
||||
# [docker.io/vaultwarden/web-vault:v2023.5.0]
|
||||
#
|
||||
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault
|
||||
FROM docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085 as vault
|
||||
|
||||
########################## BUILD IMAGE ##########################
|
||||
FROM blackdex/rust-musl:x86_64-musl-stable-1.68.2 as build
|
||||
FROM docker.io/blackdex/rust-musl:x86_64-musl-stable-1.70.0 as build
|
||||
|
||||
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
||||
ENV DEBIAN_FRONTEND=noninteractive \
|
||||
@@ -76,7 +76,7 @@ RUN cargo build --features ${DB} --release --target=x86_64-unknown-linux-musl
|
||||
######################## RUNTIME IMAGE ########################
|
||||
# Create a new stage with a minimal image
|
||||
# because we already have a binary built
|
||||
FROM alpine:3.17
|
||||
FROM docker.io/library/alpine:3.17
|
||||
|
||||
ENV ROCKET_PROFILE="release" \
|
||||
ROCKET_ADDRESS=0.0.0.0 \
|
||||
|
@@ -15,18 +15,18 @@
|
||||
# - 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 vaultwarden/web-vault:v2023.3.0b
|
||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b
|
||||
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee]
|
||||
# $ docker pull docker.io/vaultwarden/web-vault:v2023.5.0
|
||||
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2023.5.0
|
||||
# [docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085]
|
||||
#
|
||||
# - Conversely, to get the tag name from the digest:
|
||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee
|
||||
# [vaultwarden/web-vault:v2023.3.0b]
|
||||
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085
|
||||
# [docker.io/vaultwarden/web-vault:v2023.5.0]
|
||||
#
|
||||
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault
|
||||
FROM docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085 as vault
|
||||
|
||||
########################## BUILD IMAGE ##########################
|
||||
FROM rust:1.68.2-bullseye as build
|
||||
FROM docker.io/library/rust:1.70.0-bullseye as build
|
||||
|
||||
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
||||
ENV DEBIAN_FRONTEND=noninteractive \
|
||||
@@ -80,7 +80,7 @@ RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.
|
||||
######################## RUNTIME IMAGE ########################
|
||||
# Create a new stage with a minimal image
|
||||
# because we already have a binary built
|
||||
FROM debian:bullseye-slim
|
||||
FROM docker.io/library/debian:bullseye-slim
|
||||
|
||||
ENV ROCKET_PROFILE="release" \
|
||||
ROCKET_ADDRESS=0.0.0.0 \
|
||||
|
@@ -15,18 +15,18 @@
|
||||
# - 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 vaultwarden/web-vault:v2023.3.0b
|
||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b
|
||||
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee]
|
||||
# $ docker pull docker.io/vaultwarden/web-vault:v2023.5.0
|
||||
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2023.5.0
|
||||
# [docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085]
|
||||
#
|
||||
# - Conversely, to get the tag name from the digest:
|
||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee
|
||||
# [vaultwarden/web-vault:v2023.3.0b]
|
||||
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085
|
||||
# [docker.io/vaultwarden/web-vault:v2023.5.0]
|
||||
#
|
||||
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault
|
||||
FROM docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085 as vault
|
||||
|
||||
########################## BUILD IMAGE ##########################
|
||||
FROM blackdex/rust-musl:x86_64-musl-stable-1.68.2 as build
|
||||
FROM docker.io/blackdex/rust-musl:x86_64-musl-stable-1.70.0 as build
|
||||
|
||||
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
||||
ENV DEBIAN_FRONTEND=noninteractive \
|
||||
@@ -76,7 +76,7 @@ RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.
|
||||
######################## RUNTIME IMAGE ########################
|
||||
# Create a new stage with a minimal image
|
||||
# because we already have a binary built
|
||||
FROM alpine:3.17
|
||||
FROM docker.io/library/alpine:3.17
|
||||
|
||||
ENV ROCKET_PROFILE="release" \
|
||||
ROCKET_ADDRESS=0.0.0.0 \
|
||||
|
@@ -15,18 +15,18 @@
|
||||
# - 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 vaultwarden/web-vault:v2023.3.0b
|
||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b
|
||||
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee]
|
||||
# $ docker pull docker.io/vaultwarden/web-vault:v2023.5.0
|
||||
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2023.5.0
|
||||
# [docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085]
|
||||
#
|
||||
# - Conversely, to get the tag name from the digest:
|
||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee
|
||||
# [vaultwarden/web-vault:v2023.3.0b]
|
||||
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085
|
||||
# [docker.io/vaultwarden/web-vault:v2023.5.0]
|
||||
#
|
||||
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault
|
||||
FROM docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085 as vault
|
||||
|
||||
########################## BUILD IMAGE ##########################
|
||||
FROM rust:1.68.2-bullseye as build
|
||||
FROM docker.io/library/rust:1.70.0-bullseye as build
|
||||
|
||||
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
||||
ENV DEBIAN_FRONTEND=noninteractive \
|
||||
@@ -99,7 +99,7 @@ RUN cargo build --features ${DB} --release --target=aarch64-unknown-linux-gnu
|
||||
######################## RUNTIME IMAGE ########################
|
||||
# Create a new stage with a minimal image
|
||||
# because we already have a binary built
|
||||
FROM balenalib/aarch64-debian:bullseye
|
||||
FROM docker.io/balenalib/aarch64-debian:bullseye
|
||||
|
||||
ENV ROCKET_PROFILE="release" \
|
||||
ROCKET_ADDRESS=0.0.0.0 \
|
||||
|
@@ -15,18 +15,18 @@
|
||||
# - 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 vaultwarden/web-vault:v2023.3.0b
|
||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b
|
||||
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee]
|
||||
# $ docker pull docker.io/vaultwarden/web-vault:v2023.5.0
|
||||
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2023.5.0
|
||||
# [docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085]
|
||||
#
|
||||
# - Conversely, to get the tag name from the digest:
|
||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee
|
||||
# [vaultwarden/web-vault:v2023.3.0b]
|
||||
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085
|
||||
# [docker.io/vaultwarden/web-vault:v2023.5.0]
|
||||
#
|
||||
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault
|
||||
FROM docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085 as vault
|
||||
|
||||
########################## BUILD IMAGE ##########################
|
||||
FROM blackdex/rust-musl:aarch64-musl-stable-1.68.2 as build
|
||||
FROM docker.io/blackdex/rust-musl:aarch64-musl-stable-1.70.0 as build
|
||||
|
||||
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
||||
ENV DEBIAN_FRONTEND=noninteractive \
|
||||
@@ -76,7 +76,7 @@ RUN cargo build --features ${DB} --release --target=aarch64-unknown-linux-musl
|
||||
######################## RUNTIME IMAGE ########################
|
||||
# Create a new stage with a minimal image
|
||||
# because we already have a binary built
|
||||
FROM balenalib/aarch64-alpine:3.17
|
||||
FROM docker.io/balenalib/aarch64-alpine:3.17
|
||||
|
||||
ENV ROCKET_PROFILE="release" \
|
||||
ROCKET_ADDRESS=0.0.0.0 \
|
||||
|
@@ -15,18 +15,18 @@
|
||||
# - 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 vaultwarden/web-vault:v2023.3.0b
|
||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b
|
||||
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee]
|
||||
# $ docker pull docker.io/vaultwarden/web-vault:v2023.5.0
|
||||
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2023.5.0
|
||||
# [docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085]
|
||||
#
|
||||
# - Conversely, to get the tag name from the digest:
|
||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee
|
||||
# [vaultwarden/web-vault:v2023.3.0b]
|
||||
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085
|
||||
# [docker.io/vaultwarden/web-vault:v2023.5.0]
|
||||
#
|
||||
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault
|
||||
FROM docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085 as vault
|
||||
|
||||
########################## BUILD IMAGE ##########################
|
||||
FROM rust:1.68.2-bullseye as build
|
||||
FROM docker.io/library/rust:1.70.0-bullseye as build
|
||||
|
||||
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
||||
ENV DEBIAN_FRONTEND=noninteractive \
|
||||
@@ -99,7 +99,7 @@ RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.
|
||||
######################## RUNTIME IMAGE ########################
|
||||
# Create a new stage with a minimal image
|
||||
# because we already have a binary built
|
||||
FROM balenalib/aarch64-debian:bullseye
|
||||
FROM docker.io/balenalib/aarch64-debian:bullseye
|
||||
|
||||
ENV ROCKET_PROFILE="release" \
|
||||
ROCKET_ADDRESS=0.0.0.0 \
|
||||
|
@@ -15,18 +15,18 @@
|
||||
# - 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 vaultwarden/web-vault:v2023.3.0b
|
||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b
|
||||
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee]
|
||||
# $ docker pull docker.io/vaultwarden/web-vault:v2023.5.0
|
||||
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2023.5.0
|
||||
# [docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085]
|
||||
#
|
||||
# - Conversely, to get the tag name from the digest:
|
||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee
|
||||
# [vaultwarden/web-vault:v2023.3.0b]
|
||||
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085
|
||||
# [docker.io/vaultwarden/web-vault:v2023.5.0]
|
||||
#
|
||||
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault
|
||||
FROM docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085 as vault
|
||||
|
||||
########################## BUILD IMAGE ##########################
|
||||
FROM blackdex/rust-musl:aarch64-musl-stable-1.68.2 as build
|
||||
FROM docker.io/blackdex/rust-musl:aarch64-musl-stable-1.70.0 as build
|
||||
|
||||
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
||||
ENV DEBIAN_FRONTEND=noninteractive \
|
||||
@@ -76,7 +76,7 @@ RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.
|
||||
######################## RUNTIME IMAGE ########################
|
||||
# Create a new stage with a minimal image
|
||||
# because we already have a binary built
|
||||
FROM balenalib/aarch64-alpine:3.17
|
||||
FROM docker.io/balenalib/aarch64-alpine:3.17
|
||||
|
||||
ENV ROCKET_PROFILE="release" \
|
||||
ROCKET_ADDRESS=0.0.0.0 \
|
||||
|
@@ -15,18 +15,18 @@
|
||||
# - 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 vaultwarden/web-vault:v2023.3.0b
|
||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b
|
||||
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee]
|
||||
# $ docker pull docker.io/vaultwarden/web-vault:v2023.5.0
|
||||
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2023.5.0
|
||||
# [docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085]
|
||||
#
|
||||
# - Conversely, to get the tag name from the digest:
|
||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee
|
||||
# [vaultwarden/web-vault:v2023.3.0b]
|
||||
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085
|
||||
# [docker.io/vaultwarden/web-vault:v2023.5.0]
|
||||
#
|
||||
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault
|
||||
FROM docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085 as vault
|
||||
|
||||
########################## BUILD IMAGE ##########################
|
||||
FROM rust:1.68.2-bullseye as build
|
||||
FROM docker.io/library/rust:1.70.0-bullseye as build
|
||||
|
||||
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
||||
ENV DEBIAN_FRONTEND=noninteractive \
|
||||
@@ -99,7 +99,7 @@ RUN cargo build --features ${DB} --release --target=arm-unknown-linux-gnueabi
|
||||
######################## RUNTIME IMAGE ########################
|
||||
# Create a new stage with a minimal image
|
||||
# because we already have a binary built
|
||||
FROM balenalib/rpi-debian:bullseye
|
||||
FROM docker.io/balenalib/rpi-debian:bullseye
|
||||
|
||||
ENV ROCKET_PROFILE="release" \
|
||||
ROCKET_ADDRESS=0.0.0.0 \
|
||||
|
@@ -15,18 +15,18 @@
|
||||
# - 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 vaultwarden/web-vault:v2023.3.0b
|
||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b
|
||||
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee]
|
||||
# $ docker pull docker.io/vaultwarden/web-vault:v2023.5.0
|
||||
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2023.5.0
|
||||
# [docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085]
|
||||
#
|
||||
# - Conversely, to get the tag name from the digest:
|
||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee
|
||||
# [vaultwarden/web-vault:v2023.3.0b]
|
||||
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085
|
||||
# [docker.io/vaultwarden/web-vault:v2023.5.0]
|
||||
#
|
||||
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault
|
||||
FROM docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085 as vault
|
||||
|
||||
########################## BUILD IMAGE ##########################
|
||||
FROM blackdex/rust-musl:arm-musleabi-stable-1.68.2 as build
|
||||
FROM docker.io/blackdex/rust-musl:arm-musleabi-stable-1.70.0 as build
|
||||
|
||||
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
||||
ENV DEBIAN_FRONTEND=noninteractive \
|
||||
@@ -78,7 +78,7 @@ RUN cargo build --features ${DB} --release --target=arm-unknown-linux-musleabi
|
||||
######################## RUNTIME IMAGE ########################
|
||||
# Create a new stage with a minimal image
|
||||
# because we already have a binary built
|
||||
FROM balenalib/rpi-alpine:3.17
|
||||
FROM docker.io/balenalib/rpi-alpine:3.17
|
||||
|
||||
ENV ROCKET_PROFILE="release" \
|
||||
ROCKET_ADDRESS=0.0.0.0 \
|
||||
|
@@ -15,18 +15,18 @@
|
||||
# - 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 vaultwarden/web-vault:v2023.3.0b
|
||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b
|
||||
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee]
|
||||
# $ docker pull docker.io/vaultwarden/web-vault:v2023.5.0
|
||||
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2023.5.0
|
||||
# [docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085]
|
||||
#
|
||||
# - Conversely, to get the tag name from the digest:
|
||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee
|
||||
# [vaultwarden/web-vault:v2023.3.0b]
|
||||
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085
|
||||
# [docker.io/vaultwarden/web-vault:v2023.5.0]
|
||||
#
|
||||
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault
|
||||
FROM docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085 as vault
|
||||
|
||||
########################## BUILD IMAGE ##########################
|
||||
FROM rust:1.68.2-bullseye as build
|
||||
FROM docker.io/library/rust:1.70.0-bullseye as build
|
||||
|
||||
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
||||
ENV DEBIAN_FRONTEND=noninteractive \
|
||||
@@ -99,7 +99,7 @@ RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.
|
||||
######################## RUNTIME IMAGE ########################
|
||||
# Create a new stage with a minimal image
|
||||
# because we already have a binary built
|
||||
FROM balenalib/rpi-debian:bullseye
|
||||
FROM docker.io/balenalib/rpi-debian:bullseye
|
||||
|
||||
ENV ROCKET_PROFILE="release" \
|
||||
ROCKET_ADDRESS=0.0.0.0 \
|
||||
|
@@ -15,18 +15,18 @@
|
||||
# - 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 vaultwarden/web-vault:v2023.3.0b
|
||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b
|
||||
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee]
|
||||
# $ docker pull docker.io/vaultwarden/web-vault:v2023.5.0
|
||||
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2023.5.0
|
||||
# [docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085]
|
||||
#
|
||||
# - Conversely, to get the tag name from the digest:
|
||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee
|
||||
# [vaultwarden/web-vault:v2023.3.0b]
|
||||
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085
|
||||
# [docker.io/vaultwarden/web-vault:v2023.5.0]
|
||||
#
|
||||
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault
|
||||
FROM docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085 as vault
|
||||
|
||||
########################## BUILD IMAGE ##########################
|
||||
FROM blackdex/rust-musl:arm-musleabi-stable-1.68.2 as build
|
||||
FROM docker.io/blackdex/rust-musl:arm-musleabi-stable-1.70.0 as build
|
||||
|
||||
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
||||
ENV DEBIAN_FRONTEND=noninteractive \
|
||||
@@ -78,7 +78,7 @@ RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.
|
||||
######################## RUNTIME IMAGE ########################
|
||||
# Create a new stage with a minimal image
|
||||
# because we already have a binary built
|
||||
FROM balenalib/rpi-alpine:3.17
|
||||
FROM docker.io/balenalib/rpi-alpine:3.17
|
||||
|
||||
ENV ROCKET_PROFILE="release" \
|
||||
ROCKET_ADDRESS=0.0.0.0 \
|
||||
|
@@ -15,18 +15,18 @@
|
||||
# - 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 vaultwarden/web-vault:v2023.3.0b
|
||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b
|
||||
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee]
|
||||
# $ docker pull docker.io/vaultwarden/web-vault:v2023.5.0
|
||||
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2023.5.0
|
||||
# [docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085]
|
||||
#
|
||||
# - Conversely, to get the tag name from the digest:
|
||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee
|
||||
# [vaultwarden/web-vault:v2023.3.0b]
|
||||
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085
|
||||
# [docker.io/vaultwarden/web-vault:v2023.5.0]
|
||||
#
|
||||
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault
|
||||
FROM docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085 as vault
|
||||
|
||||
########################## BUILD IMAGE ##########################
|
||||
FROM rust:1.68.2-bullseye as build
|
||||
FROM docker.io/library/rust:1.70.0-bullseye as build
|
||||
|
||||
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
||||
ENV DEBIAN_FRONTEND=noninteractive \
|
||||
@@ -99,7 +99,7 @@ RUN cargo build --features ${DB} --release --target=armv7-unknown-linux-gnueabih
|
||||
######################## RUNTIME IMAGE ########################
|
||||
# Create a new stage with a minimal image
|
||||
# because we already have a binary built
|
||||
FROM balenalib/armv7hf-debian:bullseye
|
||||
FROM docker.io/balenalib/armv7hf-debian:bullseye
|
||||
|
||||
ENV ROCKET_PROFILE="release" \
|
||||
ROCKET_ADDRESS=0.0.0.0 \
|
||||
|
@@ -15,18 +15,18 @@
|
||||
# - 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 vaultwarden/web-vault:v2023.3.0b
|
||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b
|
||||
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee]
|
||||
# $ docker pull docker.io/vaultwarden/web-vault:v2023.5.0
|
||||
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2023.5.0
|
||||
# [docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085]
|
||||
#
|
||||
# - Conversely, to get the tag name from the digest:
|
||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee
|
||||
# [vaultwarden/web-vault:v2023.3.0b]
|
||||
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085
|
||||
# [docker.io/vaultwarden/web-vault:v2023.5.0]
|
||||
#
|
||||
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault
|
||||
FROM docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085 as vault
|
||||
|
||||
########################## BUILD IMAGE ##########################
|
||||
FROM blackdex/rust-musl:armv7-musleabihf-stable-1.68.2 as build
|
||||
FROM docker.io/blackdex/rust-musl:armv7-musleabihf-stable-1.70.0 as build
|
||||
|
||||
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
||||
ENV DEBIAN_FRONTEND=noninteractive \
|
||||
@@ -76,7 +76,7 @@ RUN cargo build --features ${DB} --release --target=armv7-unknown-linux-musleabi
|
||||
######################## RUNTIME IMAGE ########################
|
||||
# Create a new stage with a minimal image
|
||||
# because we already have a binary built
|
||||
FROM balenalib/armv7hf-alpine:3.17
|
||||
FROM docker.io/balenalib/armv7hf-alpine:3.17
|
||||
|
||||
ENV ROCKET_PROFILE="release" \
|
||||
ROCKET_ADDRESS=0.0.0.0 \
|
||||
|
@@ -15,18 +15,18 @@
|
||||
# - 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 vaultwarden/web-vault:v2023.3.0b
|
||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b
|
||||
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee]
|
||||
# $ docker pull docker.io/vaultwarden/web-vault:v2023.5.0
|
||||
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2023.5.0
|
||||
# [docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085]
|
||||
#
|
||||
# - Conversely, to get the tag name from the digest:
|
||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee
|
||||
# [vaultwarden/web-vault:v2023.3.0b]
|
||||
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085
|
||||
# [docker.io/vaultwarden/web-vault:v2023.5.0]
|
||||
#
|
||||
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault
|
||||
FROM docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085 as vault
|
||||
|
||||
########################## BUILD IMAGE ##########################
|
||||
FROM rust:1.68.2-bullseye as build
|
||||
FROM docker.io/library/rust:1.70.0-bullseye as build
|
||||
|
||||
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
||||
ENV DEBIAN_FRONTEND=noninteractive \
|
||||
@@ -99,7 +99,7 @@ RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.
|
||||
######################## RUNTIME IMAGE ########################
|
||||
# Create a new stage with a minimal image
|
||||
# because we already have a binary built
|
||||
FROM balenalib/armv7hf-debian:bullseye
|
||||
FROM docker.io/balenalib/armv7hf-debian:bullseye
|
||||
|
||||
ENV ROCKET_PROFILE="release" \
|
||||
ROCKET_ADDRESS=0.0.0.0 \
|
||||
|
@@ -15,18 +15,18 @@
|
||||
# - 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 vaultwarden/web-vault:v2023.3.0b
|
||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b
|
||||
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee]
|
||||
# $ docker pull docker.io/vaultwarden/web-vault:v2023.5.0
|
||||
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2023.5.0
|
||||
# [docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085]
|
||||
#
|
||||
# - Conversely, to get the tag name from the digest:
|
||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee
|
||||
# [vaultwarden/web-vault:v2023.3.0b]
|
||||
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085
|
||||
# [docker.io/vaultwarden/web-vault:v2023.5.0]
|
||||
#
|
||||
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault
|
||||
FROM docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085 as vault
|
||||
|
||||
########################## BUILD IMAGE ##########################
|
||||
FROM blackdex/rust-musl:armv7-musleabihf-stable-1.68.2 as build
|
||||
FROM docker.io/blackdex/rust-musl:armv7-musleabihf-stable-1.70.0 as build
|
||||
|
||||
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
||||
ENV DEBIAN_FRONTEND=noninteractive \
|
||||
@@ -76,7 +76,7 @@ RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.
|
||||
######################## RUNTIME IMAGE ########################
|
||||
# Create a new stage with a minimal image
|
||||
# because we already have a binary built
|
||||
FROM balenalib/armv7hf-alpine:3.17
|
||||
FROM docker.io/balenalib/armv7hf-alpine:3.17
|
||||
|
||||
ENV ROCKET_PROFILE="release" \
|
||||
ROCKET_ADDRESS=0.0.0.0 \
|
||||
|
@@ -0,0 +1 @@
|
||||
ALTER TABLE devices ADD COLUMN push_uuid TEXT;
|
@@ -0,0 +1,10 @@
|
||||
CREATE TABLE organization_api_key (
|
||||
uuid CHAR(36) NOT NULL,
|
||||
org_uuid CHAR(36) NOT NULL REFERENCES organizations(uuid),
|
||||
atype INTEGER NOT NULL,
|
||||
api_key VARCHAR(255) NOT NULL,
|
||||
revision_date DATETIME NOT NULL,
|
||||
PRIMARY KEY(uuid, org_uuid)
|
||||
);
|
||||
|
||||
ALTER TABLE users ADD COLUMN external_id TEXT;
|
@@ -0,0 +1 @@
|
||||
ALTER TABLE collections ADD COLUMN external_id TEXT;
|
@@ -0,0 +1 @@
|
||||
ALTER TABLE devices ADD COLUMN push_uuid TEXT;
|
@@ -0,0 +1,10 @@
|
||||
CREATE TABLE organization_api_key (
|
||||
uuid CHAR(36) NOT NULL,
|
||||
org_uuid CHAR(36) NOT NULL REFERENCES organizations(uuid),
|
||||
atype INTEGER NOT NULL,
|
||||
api_key VARCHAR(255),
|
||||
revision_date TIMESTAMP NOT NULL,
|
||||
PRIMARY KEY(uuid, org_uuid)
|
||||
);
|
||||
|
||||
ALTER TABLE users ADD COLUMN external_id TEXT;
|
@@ -0,0 +1 @@
|
||||
ALTER TABLE collections ADD COLUMN external_id TEXT;
|
@@ -0,0 +1 @@
|
||||
ALTER TABLE devices ADD COLUMN push_uuid TEXT;
|
@@ -0,0 +1,11 @@
|
||||
CREATE TABLE organization_api_key (
|
||||
uuid TEXT NOT NULL,
|
||||
org_uuid TEXT NOT NULL,
|
||||
atype INTEGER NOT NULL,
|
||||
api_key TEXT NOT NULL,
|
||||
revision_date DATETIME NOT NULL,
|
||||
PRIMARY KEY(uuid, org_uuid),
|
||||
FOREIGN KEY(org_uuid) REFERENCES organizations(uuid)
|
||||
);
|
||||
|
||||
ALTER TABLE users ADD COLUMN external_id TEXT;
|
@@ -0,0 +1 @@
|
||||
ALTER TABLE collections ADD COLUMN external_id TEXT;
|
@@ -1 +1 @@
|
||||
1.68.2
|
||||
1.70.0
|
||||
|
@@ -13,7 +13,7 @@ use rocket::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
api::{core::log_event, ApiResult, EmptyResult, JsonResult, Notify, NumberOrString},
|
||||
api::{core::log_event, unregister_push_device, ApiResult, EmptyResult, JsonResult, Notify, NumberOrString},
|
||||
auth::{decode_admin, encode_jwt, generate_admin_claims, ClientIp},
|
||||
config::ConfigBuilder,
|
||||
db::{backup_database, get_sql_server_version, models::*, DbConn, DbConnType},
|
||||
@@ -36,6 +36,7 @@ pub fn routes() -> Vec<Route> {
|
||||
get_user_by_mail_json,
|
||||
post_admin_login,
|
||||
admin_page,
|
||||
admin_page_login,
|
||||
invite_user,
|
||||
logout,
|
||||
delete_user,
|
||||
@@ -256,6 +257,11 @@ fn admin_page(_token: AdminToken) -> ApiResult<Html<String>> {
|
||||
render_admin_page()
|
||||
}
|
||||
|
||||
#[get("/", rank = 2)]
|
||||
fn admin_page_login() -> ApiResult<Html<String>> {
|
||||
render_admin_login(None, None)
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[allow(non_snake_case)]
|
||||
struct InviteData {
|
||||
@@ -349,8 +355,8 @@ async fn users_overview(_token: AdminToken, mut conn: DbConn) -> ApiResult<Html<
|
||||
}
|
||||
|
||||
#[get("/users/by-mail/<mail>")]
|
||||
async fn get_user_by_mail_json(mail: String, _token: AdminToken, mut conn: DbConn) -> JsonResult {
|
||||
if let Some(u) = User::find_by_mail(&mail, &mut conn).await {
|
||||
async fn get_user_by_mail_json(mail: &str, _token: AdminToken, mut conn: DbConn) -> JsonResult {
|
||||
if let Some(u) = User::find_by_mail(mail, &mut conn).await {
|
||||
let mut usr = u.to_json(&mut conn).await;
|
||||
usr["UserEnabled"] = json!(u.enabled);
|
||||
usr["CreatedAt"] = json!(format_naive_datetime_local(&u.created_at, DT_FMT));
|
||||
@@ -361,8 +367,8 @@ async fn get_user_by_mail_json(mail: String, _token: AdminToken, mut conn: DbCon
|
||||
}
|
||||
|
||||
#[get("/users/<uuid>")]
|
||||
async fn get_user_json(uuid: String, _token: AdminToken, mut conn: DbConn) -> JsonResult {
|
||||
let u = get_user_or_404(&uuid, &mut conn).await?;
|
||||
async fn get_user_json(uuid: &str, _token: AdminToken, mut conn: DbConn) -> JsonResult {
|
||||
let u = get_user_or_404(uuid, &mut conn).await?;
|
||||
let mut usr = u.to_json(&mut conn).await;
|
||||
usr["UserEnabled"] = json!(u.enabled);
|
||||
usr["CreatedAt"] = json!(format_naive_datetime_local(&u.created_at, DT_FMT));
|
||||
@@ -370,18 +376,18 @@ async fn get_user_json(uuid: String, _token: AdminToken, mut conn: DbConn) -> Js
|
||||
}
|
||||
|
||||
#[post("/users/<uuid>/delete")]
|
||||
async fn delete_user(uuid: String, token: AdminToken, mut conn: DbConn) -> EmptyResult {
|
||||
let user = get_user_or_404(&uuid, &mut conn).await?;
|
||||
async fn delete_user(uuid: &str, token: AdminToken, mut conn: DbConn) -> EmptyResult {
|
||||
let user = get_user_or_404(uuid, &mut conn).await?;
|
||||
|
||||
// Get the user_org records before deleting the actual user
|
||||
let user_orgs = UserOrganization::find_any_state_by_user(&uuid, &mut conn).await;
|
||||
let user_orgs = UserOrganization::find_any_state_by_user(uuid, &mut conn).await;
|
||||
let res = user.delete(&mut conn).await;
|
||||
|
||||
for user_org in user_orgs {
|
||||
log_event(
|
||||
EventType::OrganizationUserRemoved as i32,
|
||||
&user_org.uuid,
|
||||
user_org.org_uuid,
|
||||
&user_org.org_uuid,
|
||||
String::from(ACTING_ADMIN_USER),
|
||||
14, // Use UnknownBrowser type
|
||||
&token.ip.ip,
|
||||
@@ -394,21 +400,29 @@ async fn delete_user(uuid: String, token: AdminToken, mut conn: DbConn) -> Empty
|
||||
}
|
||||
|
||||
#[post("/users/<uuid>/deauth")]
|
||||
async fn deauth_user(uuid: String, _token: AdminToken, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
||||
let mut user = get_user_or_404(&uuid, &mut conn).await?;
|
||||
Device::delete_all_by_user(&user.uuid, &mut conn).await?;
|
||||
user.reset_security_stamp();
|
||||
|
||||
let save_result = user.save(&mut conn).await;
|
||||
async fn deauth_user(uuid: &str, _token: AdminToken, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
||||
let mut user = get_user_or_404(uuid, &mut conn).await?;
|
||||
|
||||
nt.send_logout(&user, None).await;
|
||||
|
||||
save_result
|
||||
if CONFIG.push_enabled() {
|
||||
for device in Device::find_push_devices_by_user(&user.uuid, &mut conn).await {
|
||||
match unregister_push_device(device.uuid).await {
|
||||
Ok(r) => r,
|
||||
Err(e) => error!("Unable to unregister devices from Bitwarden server: {}", e),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Device::delete_all_by_user(&user.uuid, &mut conn).await?;
|
||||
user.reset_security_stamp();
|
||||
|
||||
user.save(&mut conn).await
|
||||
}
|
||||
|
||||
#[post("/users/<uuid>/disable")]
|
||||
async fn disable_user(uuid: String, _token: AdminToken, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
||||
let mut user = get_user_or_404(&uuid, &mut conn).await?;
|
||||
async fn disable_user(uuid: &str, _token: AdminToken, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
||||
let mut user = get_user_or_404(uuid, &mut conn).await?;
|
||||
Device::delete_all_by_user(&user.uuid, &mut conn).await?;
|
||||
user.reset_security_stamp();
|
||||
user.enabled = false;
|
||||
@@ -421,24 +435,24 @@ async fn disable_user(uuid: String, _token: AdminToken, mut conn: DbConn, nt: No
|
||||
}
|
||||
|
||||
#[post("/users/<uuid>/enable")]
|
||||
async fn enable_user(uuid: String, _token: AdminToken, mut conn: DbConn) -> EmptyResult {
|
||||
let mut user = get_user_or_404(&uuid, &mut conn).await?;
|
||||
async fn enable_user(uuid: &str, _token: AdminToken, mut conn: DbConn) -> EmptyResult {
|
||||
let mut user = get_user_or_404(uuid, &mut conn).await?;
|
||||
user.enabled = true;
|
||||
|
||||
user.save(&mut conn).await
|
||||
}
|
||||
|
||||
#[post("/users/<uuid>/remove-2fa")]
|
||||
async fn remove_2fa(uuid: String, _token: AdminToken, mut conn: DbConn) -> EmptyResult {
|
||||
let mut user = get_user_or_404(&uuid, &mut conn).await?;
|
||||
async fn remove_2fa(uuid: &str, _token: AdminToken, mut conn: DbConn) -> EmptyResult {
|
||||
let mut user = get_user_or_404(uuid, &mut conn).await?;
|
||||
TwoFactor::delete_all_by_user(&user.uuid, &mut conn).await?;
|
||||
user.totp_recover = None;
|
||||
user.save(&mut conn).await
|
||||
}
|
||||
|
||||
#[post("/users/<uuid>/invite/resend")]
|
||||
async fn resend_user_invite(uuid: String, _token: AdminToken, mut conn: DbConn) -> EmptyResult {
|
||||
if let Some(user) = User::find_by_uuid(&uuid, &mut conn).await {
|
||||
async fn resend_user_invite(uuid: &str, _token: AdminToken, mut conn: DbConn) -> EmptyResult {
|
||||
if let Some(user) = User::find_by_uuid(uuid, &mut conn).await {
|
||||
//TODO: replace this with user.status check when it will be available (PR#3397)
|
||||
if !user.password_hash.is_empty() {
|
||||
err_code!("User already accepted invitation", Status::BadRequest.code);
|
||||
@@ -500,7 +514,7 @@ async fn update_user_org_type(data: Json<UserOrgTypeData>, token: AdminToken, mu
|
||||
log_event(
|
||||
EventType::OrganizationUserUpdated as i32,
|
||||
&user_to_edit.uuid,
|
||||
data.org_uuid,
|
||||
&data.org_uuid,
|
||||
String::from(ACTING_ADMIN_USER),
|
||||
14, // Use UnknownBrowser type
|
||||
&token.ip.ip,
|
||||
@@ -538,8 +552,8 @@ async fn organizations_overview(_token: AdminToken, mut conn: DbConn) -> ApiResu
|
||||
}
|
||||
|
||||
#[post("/organizations/<uuid>/delete")]
|
||||
async fn delete_organization(uuid: String, _token: AdminToken, mut conn: DbConn) -> EmptyResult {
|
||||
let org = Organization::find_by_uuid(&uuid, &mut conn).await.map_res("Organization doesn't exist")?;
|
||||
async fn delete_organization(uuid: &str, _token: AdminToken, mut conn: DbConn) -> EmptyResult {
|
||||
let org = Organization::find_by_uuid(uuid, &mut conn).await.map_res("Organization doesn't exist")?;
|
||||
org.delete(&mut conn).await
|
||||
}
|
||||
|
||||
@@ -761,7 +775,17 @@ impl<'r> FromRequest<'r> for AdminToken {
|
||||
|
||||
let access_token = match cookies.get(COOKIE_NAME) {
|
||||
Some(cookie) => cookie.value(),
|
||||
None => return Outcome::Failure((Status::Unauthorized, "Unauthorized")),
|
||||
None => {
|
||||
let requested_page =
|
||||
request.segments::<std::path::PathBuf>(0..).unwrap_or_default().display().to_string();
|
||||
// When the requested page is empty, it is `/admin`, in that case, Forward, so it will render the login page
|
||||
// Else, return a 401 failure, which will be caught
|
||||
if requested_page.is_empty() {
|
||||
return Outcome::Forward(Status::Unauthorized);
|
||||
} else {
|
||||
return Outcome::Failure((Status::Unauthorized, "Unauthorized"));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if decode_admin(access_token).is_err() {
|
||||
|
@@ -4,7 +4,8 @@ use serde_json::Value;
|
||||
|
||||
use crate::{
|
||||
api::{
|
||||
core::log_user_event, EmptyResult, JsonResult, JsonUpcase, Notify, NumberOrString, PasswordData, UpdateType,
|
||||
core::log_user_event, register_push_device, unregister_push_device, EmptyResult, JsonResult, JsonUpcase,
|
||||
Notify, NumberOrString, PasswordData, UpdateType,
|
||||
},
|
||||
auth::{decode_delete, decode_invite, decode_verify_email, Headers},
|
||||
crypto,
|
||||
@@ -35,6 +36,7 @@ pub fn routes() -> Vec<rocket::Route> {
|
||||
post_verify_email_token,
|
||||
post_delete_recover,
|
||||
post_delete_recover_token,
|
||||
post_device_token,
|
||||
delete_account,
|
||||
post_delete_account,
|
||||
revision_date,
|
||||
@@ -46,6 +48,9 @@ pub fn routes() -> Vec<rocket::Route> {
|
||||
get_known_device,
|
||||
get_known_device_from_path,
|
||||
put_avatar,
|
||||
put_device_token,
|
||||
put_clear_device_token,
|
||||
post_clear_device_token,
|
||||
]
|
||||
}
|
||||
|
||||
@@ -133,7 +138,7 @@ pub async fn _register(data: JsonUpcase<RegisterData>, mut conn: DbConn) -> Json
|
||||
err!("Registration email does not match invite email")
|
||||
}
|
||||
} else if Invitation::take(&email, &mut conn).await {
|
||||
for mut user_org in UserOrganization::find_invited_by_user(&user.uuid, &mut conn).await.iter_mut() {
|
||||
for user_org in UserOrganization::find_invited_by_user(&user.uuid, &mut conn).await.iter_mut() {
|
||||
user_org.status = UserOrgStatus::Accepted as i32;
|
||||
user_org.save(&mut conn).await?;
|
||||
}
|
||||
@@ -266,8 +271,8 @@ async fn put_avatar(data: JsonUpcase<AvatarData>, headers: Headers, mut conn: Db
|
||||
}
|
||||
|
||||
#[get("/users/<uuid>/public-key")]
|
||||
async fn get_public_keys(uuid: String, _headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
let user = match User::find_by_uuid(&uuid, &mut conn).await {
|
||||
async fn get_public_keys(uuid: &str, _headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
let user = match User::find_by_uuid(uuid, &mut conn).await {
|
||||
Some(user) => user,
|
||||
None => err!("User doesn't exist"),
|
||||
};
|
||||
@@ -874,18 +879,18 @@ async fn rotate_api_key(data: JsonUpcase<SecretVerificationRequest>, headers: He
|
||||
|
||||
// This variant is deprecated: https://github.com/bitwarden/server/pull/2682
|
||||
#[get("/devices/knowndevice/<email>/<uuid>")]
|
||||
async fn get_known_device_from_path(email: String, uuid: String, mut conn: DbConn) -> JsonResult {
|
||||
async fn get_known_device_from_path(email: &str, uuid: &str, mut conn: DbConn) -> JsonResult {
|
||||
// This endpoint doesn't have auth header
|
||||
let mut result = false;
|
||||
if let Some(user) = User::find_by_mail(&email, &mut conn).await {
|
||||
result = Device::find_by_uuid_and_user(&uuid, &user.uuid, &mut conn).await.is_some();
|
||||
if let Some(user) = User::find_by_mail(email, &mut conn).await {
|
||||
result = Device::find_by_uuid_and_user(uuid, &user.uuid, &mut conn).await.is_some();
|
||||
}
|
||||
Ok(Json(json!(result)))
|
||||
}
|
||||
|
||||
#[get("/devices/knowndevice")]
|
||||
async fn get_known_device(device: KnownDevice, conn: DbConn) -> JsonResult {
|
||||
get_known_device_from_path(device.email, device.uuid, conn).await
|
||||
get_known_device_from_path(&device.email, &device.uuid, conn).await
|
||||
}
|
||||
|
||||
struct KnownDevice {
|
||||
@@ -930,3 +935,64 @@ impl<'r> FromRequest<'r> for KnownDevice {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
struct PushToken {
|
||||
PushToken: String,
|
||||
}
|
||||
|
||||
#[post("/devices/identifier/<uuid>/token", data = "<data>")]
|
||||
async fn post_device_token(uuid: &str, data: JsonUpcase<PushToken>, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||
put_device_token(uuid, data, headers, conn).await
|
||||
}
|
||||
|
||||
#[put("/devices/identifier/<uuid>/token", data = "<data>")]
|
||||
async fn put_device_token(uuid: &str, data: JsonUpcase<PushToken>, headers: Headers, mut conn: DbConn) -> EmptyResult {
|
||||
if !CONFIG.push_enabled() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let data = data.into_inner().data;
|
||||
let token = data.PushToken;
|
||||
let mut device = match Device::find_by_uuid_and_user(&headers.device.uuid, &headers.user.uuid, &mut conn).await {
|
||||
Some(device) => device,
|
||||
None => err!(format!("Error: device {uuid} should be present before a token can be assigned")),
|
||||
};
|
||||
device.push_token = Some(token);
|
||||
if device.push_uuid.is_none() {
|
||||
device.push_uuid = Some(uuid::Uuid::new_v4().to_string());
|
||||
}
|
||||
if let Err(e) = device.save(&mut conn).await {
|
||||
err!(format!("An error occured while trying to save the device push token: {e}"));
|
||||
}
|
||||
if let Err(e) = register_push_device(headers.user.uuid, device).await {
|
||||
err!(format!("An error occured while proceeding registration of a device: {e}"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[put("/devices/identifier/<uuid>/clear-token")]
|
||||
async fn put_clear_device_token(uuid: &str, mut conn: DbConn) -> EmptyResult {
|
||||
// This only clears push token
|
||||
// https://github.com/bitwarden/core/blob/master/src/Api/Controllers/DevicesController.cs#L109
|
||||
// https://github.com/bitwarden/core/blob/master/src/Core/Services/Implementations/DeviceService.cs#L37
|
||||
// This is somehow not implemented in any app, added it in case it is required
|
||||
if !CONFIG.push_enabled() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(device) = Device::find_by_uuid(uuid, &mut conn).await {
|
||||
Device::clear_push_token_by_uuid(uuid, &mut conn).await?;
|
||||
unregister_push_device(device.uuid).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// On upstream server, both PUT and POST are declared. Implementing the POST method in case it would be useful somewhere
|
||||
#[post("/devices/identifier/<uuid>/clear-token")]
|
||||
async fn post_clear_device_token(uuid: &str, conn: DbConn) -> EmptyResult {
|
||||
put_clear_device_token(uuid, conn).await
|
||||
}
|
||||
|
@@ -172,8 +172,8 @@ async fn get_ciphers(headers: Headers, mut conn: DbConn) -> Json<Value> {
|
||||
}
|
||||
|
||||
#[get("/ciphers/<uuid>")]
|
||||
async fn get_cipher(uuid: String, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
let cipher = match Cipher::find_by_uuid(&uuid, &mut conn).await {
|
||||
async fn get_cipher(uuid: &str, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
let cipher = match Cipher::find_by_uuid(uuid, &mut conn).await {
|
||||
Some(cipher) => cipher,
|
||||
None => err!("Cipher doesn't exist"),
|
||||
};
|
||||
@@ -186,13 +186,13 @@ async fn get_cipher(uuid: String, headers: Headers, mut conn: DbConn) -> JsonRes
|
||||
}
|
||||
|
||||
#[get("/ciphers/<uuid>/admin")]
|
||||
async fn get_cipher_admin(uuid: String, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
async fn get_cipher_admin(uuid: &str, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
// TODO: Implement this correctly
|
||||
get_cipher(uuid, headers, conn).await
|
||||
}
|
||||
|
||||
#[get("/ciphers/<uuid>/details")]
|
||||
async fn get_cipher_details(uuid: String, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
async fn get_cipher_details(uuid: &str, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
get_cipher(uuid, headers, conn).await
|
||||
}
|
||||
|
||||
@@ -210,7 +210,8 @@ pub struct CipherData {
|
||||
Login = 1,
|
||||
SecureNote = 2,
|
||||
Card = 3,
|
||||
Identity = 4
|
||||
Identity = 4,
|
||||
Fido2Key = 5
|
||||
*/
|
||||
pub Type: i32,
|
||||
pub Name: String,
|
||||
@@ -222,6 +223,7 @@ pub struct CipherData {
|
||||
SecureNote: Option<Value>,
|
||||
Card: Option<Value>,
|
||||
Identity: Option<Value>,
|
||||
Fido2Key: Option<Value>,
|
||||
|
||||
Favorite: Option<bool>,
|
||||
Reprompt: Option<i32>,
|
||||
@@ -464,6 +466,7 @@ pub async fn update_cipher_from_data(
|
||||
2 => data.SecureNote,
|
||||
3 => data.Card,
|
||||
4 => data.Identity,
|
||||
5 => data.Fido2Key,
|
||||
_ => err!("Invalid type"),
|
||||
};
|
||||
|
||||
@@ -503,7 +506,7 @@ pub async fn update_cipher_from_data(
|
||||
log_event(
|
||||
event_type as i32,
|
||||
&cipher.uuid,
|
||||
String::from(org_uuid),
|
||||
org_uuid,
|
||||
headers.user.uuid.clone(),
|
||||
headers.device.atype,
|
||||
&headers.ip.ip,
|
||||
@@ -511,10 +514,9 @@ pub async fn update_cipher_from_data(
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
nt.send_cipher_update(ut, cipher, &cipher.update_users_revision(conn).await, &headers.device.uuid).await;
|
||||
nt.send_cipher_update(ut, cipher, &cipher.update_users_revision(conn).await, &headers.device.uuid, None, conn)
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -580,13 +582,14 @@ async fn post_ciphers_import(
|
||||
let mut user = headers.user;
|
||||
user.update_revision(&mut conn).await?;
|
||||
nt.send_user_update(UpdateType::SyncVault, &user).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Called when an org admin modifies an existing org cipher.
|
||||
#[put("/ciphers/<uuid>/admin", data = "<data>")]
|
||||
async fn put_cipher_admin(
|
||||
uuid: String,
|
||||
uuid: &str,
|
||||
data: JsonUpcase<CipherData>,
|
||||
headers: Headers,
|
||||
conn: DbConn,
|
||||
@@ -597,7 +600,7 @@ async fn put_cipher_admin(
|
||||
|
||||
#[post("/ciphers/<uuid>/admin", data = "<data>")]
|
||||
async fn post_cipher_admin(
|
||||
uuid: String,
|
||||
uuid: &str,
|
||||
data: JsonUpcase<CipherData>,
|
||||
headers: Headers,
|
||||
conn: DbConn,
|
||||
@@ -608,7 +611,7 @@ async fn post_cipher_admin(
|
||||
|
||||
#[post("/ciphers/<uuid>", data = "<data>")]
|
||||
async fn post_cipher(
|
||||
uuid: String,
|
||||
uuid: &str,
|
||||
data: JsonUpcase<CipherData>,
|
||||
headers: Headers,
|
||||
conn: DbConn,
|
||||
@@ -619,7 +622,7 @@ async fn post_cipher(
|
||||
|
||||
#[put("/ciphers/<uuid>", data = "<data>")]
|
||||
async fn put_cipher(
|
||||
uuid: String,
|
||||
uuid: &str,
|
||||
data: JsonUpcase<CipherData>,
|
||||
headers: Headers,
|
||||
mut conn: DbConn,
|
||||
@@ -627,7 +630,7 @@ async fn put_cipher(
|
||||
) -> JsonResult {
|
||||
let data: CipherData = data.into_inner().data;
|
||||
|
||||
let mut cipher = match Cipher::find_by_uuid(&uuid, &mut conn).await {
|
||||
let mut cipher = match Cipher::find_by_uuid(uuid, &mut conn).await {
|
||||
Some(cipher) => cipher,
|
||||
None => err!("Cipher doesn't exist"),
|
||||
};
|
||||
@@ -648,7 +651,7 @@ async fn put_cipher(
|
||||
|
||||
#[post("/ciphers/<uuid>/partial", data = "<data>")]
|
||||
async fn post_cipher_partial(
|
||||
uuid: String,
|
||||
uuid: &str,
|
||||
data: JsonUpcase<PartialCipherData>,
|
||||
headers: Headers,
|
||||
conn: DbConn,
|
||||
@@ -659,14 +662,14 @@ async fn post_cipher_partial(
|
||||
// Only update the folder and favorite for the user, since this cipher is read-only
|
||||
#[put("/ciphers/<uuid>/partial", data = "<data>")]
|
||||
async fn put_cipher_partial(
|
||||
uuid: String,
|
||||
uuid: &str,
|
||||
data: JsonUpcase<PartialCipherData>,
|
||||
headers: Headers,
|
||||
mut conn: DbConn,
|
||||
) -> JsonResult {
|
||||
let data: PartialCipherData = data.into_inner().data;
|
||||
|
||||
let cipher = match Cipher::find_by_uuid(&uuid, &mut conn).await {
|
||||
let cipher = match Cipher::find_by_uuid(uuid, &mut conn).await {
|
||||
Some(cipher) => cipher,
|
||||
None => err!("Cipher doesn't exist"),
|
||||
};
|
||||
@@ -698,44 +701,48 @@ struct CollectionsAdminData {
|
||||
|
||||
#[put("/ciphers/<uuid>/collections", data = "<data>")]
|
||||
async fn put_collections_update(
|
||||
uuid: String,
|
||||
uuid: &str,
|
||||
data: JsonUpcase<CollectionsAdminData>,
|
||||
headers: Headers,
|
||||
conn: DbConn,
|
||||
nt: Notify<'_>,
|
||||
) -> EmptyResult {
|
||||
post_collections_admin(uuid, data, headers, conn).await
|
||||
post_collections_admin(uuid, data, headers, conn, nt).await
|
||||
}
|
||||
|
||||
#[post("/ciphers/<uuid>/collections", data = "<data>")]
|
||||
async fn post_collections_update(
|
||||
uuid: String,
|
||||
uuid: &str,
|
||||
data: JsonUpcase<CollectionsAdminData>,
|
||||
headers: Headers,
|
||||
conn: DbConn,
|
||||
nt: Notify<'_>,
|
||||
) -> EmptyResult {
|
||||
post_collections_admin(uuid, data, headers, conn).await
|
||||
post_collections_admin(uuid, data, headers, conn, nt).await
|
||||
}
|
||||
|
||||
#[put("/ciphers/<uuid>/collections-admin", data = "<data>")]
|
||||
async fn put_collections_admin(
|
||||
uuid: String,
|
||||
uuid: &str,
|
||||
data: JsonUpcase<CollectionsAdminData>,
|
||||
headers: Headers,
|
||||
conn: DbConn,
|
||||
nt: Notify<'_>,
|
||||
) -> EmptyResult {
|
||||
post_collections_admin(uuid, data, headers, conn).await
|
||||
post_collections_admin(uuid, data, headers, conn, nt).await
|
||||
}
|
||||
|
||||
#[post("/ciphers/<uuid>/collections-admin", data = "<data>")]
|
||||
async fn post_collections_admin(
|
||||
uuid: String,
|
||||
uuid: &str,
|
||||
data: JsonUpcase<CollectionsAdminData>,
|
||||
headers: Headers,
|
||||
mut conn: DbConn,
|
||||
nt: Notify<'_>,
|
||||
) -> EmptyResult {
|
||||
let data: CollectionsAdminData = data.into_inner().data;
|
||||
|
||||
let cipher = match Cipher::find_by_uuid(&uuid, &mut conn).await {
|
||||
let cipher = match Cipher::find_by_uuid(uuid, &mut conn).await {
|
||||
Some(cipher) => cipher,
|
||||
None => err!("Cipher doesn't exist"),
|
||||
};
|
||||
@@ -767,10 +774,20 @@ async fn post_collections_admin(
|
||||
}
|
||||
}
|
||||
|
||||
nt.send_cipher_update(
|
||||
UpdateType::SyncCipherUpdate,
|
||||
&cipher,
|
||||
&cipher.update_users_revision(&mut conn).await,
|
||||
&headers.device.uuid,
|
||||
Some(Vec::from_iter(posted_collections)),
|
||||
&mut conn,
|
||||
)
|
||||
.await;
|
||||
|
||||
log_event(
|
||||
EventType::CipherUpdatedCollections as i32,
|
||||
&cipher.uuid,
|
||||
cipher.organization_uuid.unwrap(),
|
||||
&cipher.organization_uuid.unwrap(),
|
||||
headers.user.uuid.clone(),
|
||||
headers.device.atype,
|
||||
&headers.ip.ip,
|
||||
@@ -790,7 +807,7 @@ struct ShareCipherData {
|
||||
|
||||
#[post("/ciphers/<uuid>/share", data = "<data>")]
|
||||
async fn post_cipher_share(
|
||||
uuid: String,
|
||||
uuid: &str,
|
||||
data: JsonUpcase<ShareCipherData>,
|
||||
headers: Headers,
|
||||
mut conn: DbConn,
|
||||
@@ -798,12 +815,12 @@ async fn post_cipher_share(
|
||||
) -> JsonResult {
|
||||
let data: ShareCipherData = data.into_inner().data;
|
||||
|
||||
share_cipher_by_uuid(&uuid, data, &headers, &mut conn, &nt).await
|
||||
share_cipher_by_uuid(uuid, data, &headers, &mut conn, &nt).await
|
||||
}
|
||||
|
||||
#[put("/ciphers/<uuid>/share", data = "<data>")]
|
||||
async fn put_cipher_share(
|
||||
uuid: String,
|
||||
uuid: &str,
|
||||
data: JsonUpcase<ShareCipherData>,
|
||||
headers: Headers,
|
||||
mut conn: DbConn,
|
||||
@@ -811,7 +828,7 @@ async fn put_cipher_share(
|
||||
) -> JsonResult {
|
||||
let data: ShareCipherData = data.into_inner().data;
|
||||
|
||||
share_cipher_by_uuid(&uuid, data, &headers, &mut conn, &nt).await
|
||||
share_cipher_by_uuid(uuid, data, &headers, &mut conn, &nt).await
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -916,8 +933,17 @@ async fn share_cipher_by_uuid(
|
||||
/// their object storage service. For self-hosted instances, it basically just
|
||||
/// redirects to the same location as before the v2 API.
|
||||
#[get("/ciphers/<uuid>/attachment/<attachment_id>")]
|
||||
async fn get_attachment(uuid: String, attachment_id: String, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
match Attachment::find_by_id(&attachment_id, &mut conn).await {
|
||||
async fn get_attachment(uuid: &str, attachment_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
let cipher = match Cipher::find_by_uuid(uuid, &mut conn).await {
|
||||
Some(cipher) => cipher,
|
||||
None => err!("Cipher doesn't exist"),
|
||||
};
|
||||
|
||||
if !cipher.is_accessible_to_user(&headers.user.uuid, &mut conn).await {
|
||||
err!("Cipher is not accessible")
|
||||
}
|
||||
|
||||
match Attachment::find_by_id(attachment_id, &mut conn).await {
|
||||
Some(attachment) if uuid == attachment.cipher_uuid => Ok(Json(attachment.to_json(&headers.host))),
|
||||
Some(_) => err!("Attachment doesn't belong to cipher"),
|
||||
None => err!("Attachment doesn't exist"),
|
||||
@@ -944,12 +970,12 @@ enum FileUploadType {
|
||||
/// For self-hosted instances, it's another API on the local instance.
|
||||
#[post("/ciphers/<uuid>/attachment/v2", data = "<data>")]
|
||||
async fn post_attachment_v2(
|
||||
uuid: String,
|
||||
uuid: &str,
|
||||
data: JsonUpcase<AttachmentRequestData>,
|
||||
headers: Headers,
|
||||
mut conn: DbConn,
|
||||
) -> JsonResult {
|
||||
let cipher = match Cipher::find_by_uuid(&uuid, &mut conn).await {
|
||||
let cipher = match Cipher::find_by_uuid(uuid, &mut conn).await {
|
||||
Some(cipher) => cipher,
|
||||
None => err!("Cipher doesn't exist"),
|
||||
};
|
||||
@@ -995,13 +1021,13 @@ struct UploadData<'f> {
|
||||
/// database record, which is passed in as `attachment`.
|
||||
async fn save_attachment(
|
||||
mut attachment: Option<Attachment>,
|
||||
cipher_uuid: String,
|
||||
cipher_uuid: &str,
|
||||
data: Form<UploadData<'_>>,
|
||||
headers: &Headers,
|
||||
mut conn: DbConn,
|
||||
nt: Notify<'_>,
|
||||
) -> Result<(Cipher, DbConn), crate::error::Error> {
|
||||
let cipher = match Cipher::find_by_uuid(&cipher_uuid, &mut conn).await {
|
||||
let cipher = match Cipher::find_by_uuid(cipher_uuid, &mut conn).await {
|
||||
Some(cipher) => cipher,
|
||||
None => err!("Cipher doesn't exist"),
|
||||
};
|
||||
@@ -1058,7 +1084,7 @@ async fn save_attachment(
|
||||
None => crypto::generate_attachment_id(), // Legacy API
|
||||
};
|
||||
|
||||
let folder_path = tokio::fs::canonicalize(&CONFIG.attachments_folder()).await?.join(&cipher_uuid);
|
||||
let folder_path = tokio::fs::canonicalize(&CONFIG.attachments_folder()).await?.join(cipher_uuid);
|
||||
let file_path = folder_path.join(&file_id);
|
||||
tokio::fs::create_dir_all(&folder_path).await?;
|
||||
|
||||
@@ -1094,7 +1120,8 @@ async fn save_attachment(
|
||||
if data.key.is_none() {
|
||||
err!("No attachment key provided")
|
||||
}
|
||||
let attachment = Attachment::new(file_id, cipher_uuid.clone(), encrypted_filename.unwrap(), size, data.key);
|
||||
let attachment =
|
||||
Attachment::new(file_id, String::from(cipher_uuid), encrypted_filename.unwrap(), size, data.key);
|
||||
attachment.save(&mut conn).await.expect("Error saving attachment");
|
||||
}
|
||||
|
||||
@@ -1107,6 +1134,8 @@ async fn save_attachment(
|
||||
&cipher,
|
||||
&cipher.update_users_revision(&mut conn).await,
|
||||
&headers.device.uuid,
|
||||
None,
|
||||
&mut conn,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -1114,7 +1143,7 @@ async fn save_attachment(
|
||||
log_event(
|
||||
EventType::CipherAttachmentCreated as i32,
|
||||
&cipher.uuid,
|
||||
String::from(org_uuid),
|
||||
org_uuid,
|
||||
headers.user.uuid.clone(),
|
||||
headers.device.atype,
|
||||
&headers.ip.ip,
|
||||
@@ -1132,14 +1161,14 @@ async fn save_attachment(
|
||||
/// with this one.
|
||||
#[post("/ciphers/<uuid>/attachment/<attachment_id>", format = "multipart/form-data", data = "<data>", rank = 1)]
|
||||
async fn post_attachment_v2_data(
|
||||
uuid: String,
|
||||
attachment_id: String,
|
||||
uuid: &str,
|
||||
attachment_id: &str,
|
||||
data: Form<UploadData<'_>>,
|
||||
headers: Headers,
|
||||
mut conn: DbConn,
|
||||
nt: Notify<'_>,
|
||||
) -> EmptyResult {
|
||||
let attachment = match Attachment::find_by_id(&attachment_id, &mut conn).await {
|
||||
let attachment = match Attachment::find_by_id(attachment_id, &mut conn).await {
|
||||
Some(attachment) if uuid == attachment.cipher_uuid => Some(attachment),
|
||||
Some(_) => err!("Attachment doesn't belong to cipher"),
|
||||
None => err!("Attachment doesn't exist"),
|
||||
@@ -1153,7 +1182,7 @@ async fn post_attachment_v2_data(
|
||||
/// Legacy API for creating an attachment associated with a cipher.
|
||||
#[post("/ciphers/<uuid>/attachment", format = "multipart/form-data", data = "<data>")]
|
||||
async fn post_attachment(
|
||||
uuid: String,
|
||||
uuid: &str,
|
||||
data: Form<UploadData<'_>>,
|
||||
headers: Headers,
|
||||
conn: DbConn,
|
||||
@@ -1170,7 +1199,7 @@ async fn post_attachment(
|
||||
|
||||
#[post("/ciphers/<uuid>/attachment-admin", format = "multipart/form-data", data = "<data>")]
|
||||
async fn post_attachment_admin(
|
||||
uuid: String,
|
||||
uuid: &str,
|
||||
data: Form<UploadData<'_>>,
|
||||
headers: Headers,
|
||||
conn: DbConn,
|
||||
@@ -1181,21 +1210,21 @@ async fn post_attachment_admin(
|
||||
|
||||
#[post("/ciphers/<uuid>/attachment/<attachment_id>/share", format = "multipart/form-data", data = "<data>")]
|
||||
async fn post_attachment_share(
|
||||
uuid: String,
|
||||
attachment_id: String,
|
||||
uuid: &str,
|
||||
attachment_id: &str,
|
||||
data: Form<UploadData<'_>>,
|
||||
headers: Headers,
|
||||
mut conn: DbConn,
|
||||
nt: Notify<'_>,
|
||||
) -> JsonResult {
|
||||
_delete_cipher_attachment_by_id(&uuid, &attachment_id, &headers, &mut conn, &nt).await?;
|
||||
_delete_cipher_attachment_by_id(uuid, attachment_id, &headers, &mut conn, &nt).await?;
|
||||
post_attachment(uuid, data, headers, conn, nt).await
|
||||
}
|
||||
|
||||
#[post("/ciphers/<uuid>/attachment/<attachment_id>/delete-admin")]
|
||||
async fn delete_attachment_post_admin(
|
||||
uuid: String,
|
||||
attachment_id: String,
|
||||
uuid: &str,
|
||||
attachment_id: &str,
|
||||
headers: Headers,
|
||||
conn: DbConn,
|
||||
nt: Notify<'_>,
|
||||
@@ -1205,8 +1234,8 @@ async fn delete_attachment_post_admin(
|
||||
|
||||
#[post("/ciphers/<uuid>/attachment/<attachment_id>/delete")]
|
||||
async fn delete_attachment_post(
|
||||
uuid: String,
|
||||
attachment_id: String,
|
||||
uuid: &str,
|
||||
attachment_id: &str,
|
||||
headers: Headers,
|
||||
conn: DbConn,
|
||||
nt: Notify<'_>,
|
||||
@@ -1216,58 +1245,58 @@ async fn delete_attachment_post(
|
||||
|
||||
#[delete("/ciphers/<uuid>/attachment/<attachment_id>")]
|
||||
async fn delete_attachment(
|
||||
uuid: String,
|
||||
attachment_id: String,
|
||||
uuid: &str,
|
||||
attachment_id: &str,
|
||||
headers: Headers,
|
||||
mut conn: DbConn,
|
||||
nt: Notify<'_>,
|
||||
) -> EmptyResult {
|
||||
_delete_cipher_attachment_by_id(&uuid, &attachment_id, &headers, &mut conn, &nt).await
|
||||
_delete_cipher_attachment_by_id(uuid, attachment_id, &headers, &mut conn, &nt).await
|
||||
}
|
||||
|
||||
#[delete("/ciphers/<uuid>/attachment/<attachment_id>/admin")]
|
||||
async fn delete_attachment_admin(
|
||||
uuid: String,
|
||||
attachment_id: String,
|
||||
uuid: &str,
|
||||
attachment_id: &str,
|
||||
headers: Headers,
|
||||
mut conn: DbConn,
|
||||
nt: Notify<'_>,
|
||||
) -> EmptyResult {
|
||||
_delete_cipher_attachment_by_id(&uuid, &attachment_id, &headers, &mut conn, &nt).await
|
||||
_delete_cipher_attachment_by_id(uuid, attachment_id, &headers, &mut conn, &nt).await
|
||||
}
|
||||
|
||||
#[post("/ciphers/<uuid>/delete")]
|
||||
async fn delete_cipher_post(uuid: String, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
||||
_delete_cipher_by_uuid(&uuid, &headers, &mut conn, false, &nt).await
|
||||
async fn delete_cipher_post(uuid: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
||||
_delete_cipher_by_uuid(uuid, &headers, &mut conn, false, &nt).await
|
||||
// permanent delete
|
||||
}
|
||||
|
||||
#[post("/ciphers/<uuid>/delete-admin")]
|
||||
async fn delete_cipher_post_admin(uuid: String, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
||||
_delete_cipher_by_uuid(&uuid, &headers, &mut conn, false, &nt).await
|
||||
async fn delete_cipher_post_admin(uuid: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
||||
_delete_cipher_by_uuid(uuid, &headers, &mut conn, false, &nt).await
|
||||
// permanent delete
|
||||
}
|
||||
|
||||
#[put("/ciphers/<uuid>/delete")]
|
||||
async fn delete_cipher_put(uuid: String, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
||||
_delete_cipher_by_uuid(&uuid, &headers, &mut conn, true, &nt).await
|
||||
async fn delete_cipher_put(uuid: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
||||
_delete_cipher_by_uuid(uuid, &headers, &mut conn, true, &nt).await
|
||||
// soft delete
|
||||
}
|
||||
|
||||
#[put("/ciphers/<uuid>/delete-admin")]
|
||||
async fn delete_cipher_put_admin(uuid: String, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
||||
_delete_cipher_by_uuid(&uuid, &headers, &mut conn, true, &nt).await
|
||||
async fn delete_cipher_put_admin(uuid: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
||||
_delete_cipher_by_uuid(uuid, &headers, &mut conn, true, &nt).await
|
||||
}
|
||||
|
||||
#[delete("/ciphers/<uuid>")]
|
||||
async fn delete_cipher(uuid: String, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
||||
_delete_cipher_by_uuid(&uuid, &headers, &mut conn, false, &nt).await
|
||||
async fn delete_cipher(uuid: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
||||
_delete_cipher_by_uuid(uuid, &headers, &mut conn, false, &nt).await
|
||||
// permanent delete
|
||||
}
|
||||
|
||||
#[delete("/ciphers/<uuid>/admin")]
|
||||
async fn delete_cipher_admin(uuid: String, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
||||
_delete_cipher_by_uuid(&uuid, &headers, &mut conn, false, &nt).await
|
||||
async fn delete_cipher_admin(uuid: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
||||
_delete_cipher_by_uuid(uuid, &headers, &mut conn, false, &nt).await
|
||||
// permanent delete
|
||||
}
|
||||
|
||||
@@ -1332,13 +1361,13 @@ async fn delete_cipher_selected_put_admin(
|
||||
}
|
||||
|
||||
#[put("/ciphers/<uuid>/restore")]
|
||||
async fn restore_cipher_put(uuid: String, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult {
|
||||
_restore_cipher_by_uuid(&uuid, &headers, &mut conn, &nt).await
|
||||
async fn restore_cipher_put(uuid: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult {
|
||||
_restore_cipher_by_uuid(uuid, &headers, &mut conn, &nt).await
|
||||
}
|
||||
|
||||
#[put("/ciphers/<uuid>/restore-admin")]
|
||||
async fn restore_cipher_put_admin(uuid: String, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult {
|
||||
_restore_cipher_by_uuid(&uuid, &headers, &mut conn, &nt).await
|
||||
async fn restore_cipher_put_admin(uuid: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult {
|
||||
_restore_cipher_by_uuid(uuid, &headers, &mut conn, &nt).await
|
||||
}
|
||||
|
||||
#[put("/ciphers/restore", data = "<data>")]
|
||||
@@ -1392,7 +1421,15 @@ async fn move_cipher_selected(
|
||||
// Move cipher
|
||||
cipher.move_to_folder(data.FolderId.clone(), &user_uuid, &mut conn).await?;
|
||||
|
||||
nt.send_cipher_update(UpdateType::SyncCipherUpdate, &cipher, &[user_uuid.clone()], &headers.device.uuid).await;
|
||||
nt.send_cipher_update(
|
||||
UpdateType::SyncCipherUpdate,
|
||||
&cipher,
|
||||
&[user_uuid.clone()],
|
||||
&headers.device.uuid,
|
||||
None,
|
||||
&mut conn,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -1444,7 +1481,7 @@ async fn delete_all(
|
||||
log_event(
|
||||
EventType::OrganizationPurgedVault as i32,
|
||||
&org_data.org_id,
|
||||
org_data.org_id.clone(),
|
||||
&org_data.org_id,
|
||||
user.uuid,
|
||||
headers.device.atype,
|
||||
&headers.ip.ip,
|
||||
@@ -1473,6 +1510,7 @@ async fn delete_all(
|
||||
|
||||
user.update_revision(&mut conn).await?;
|
||||
nt.send_user_update(UpdateType::SyncVault, &user).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1502,6 +1540,8 @@ async fn _delete_cipher_by_uuid(
|
||||
&cipher,
|
||||
&cipher.update_users_revision(conn).await,
|
||||
&headers.device.uuid,
|
||||
None,
|
||||
conn,
|
||||
)
|
||||
.await;
|
||||
} else {
|
||||
@@ -1511,6 +1551,8 @@ async fn _delete_cipher_by_uuid(
|
||||
&cipher,
|
||||
&cipher.update_users_revision(conn).await,
|
||||
&headers.device.uuid,
|
||||
None,
|
||||
conn,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@@ -1524,7 +1566,7 @@ async fn _delete_cipher_by_uuid(
|
||||
log_event(
|
||||
event_type,
|
||||
&cipher.uuid,
|
||||
org_uuid,
|
||||
&org_uuid,
|
||||
headers.user.uuid.clone(),
|
||||
headers.device.atype,
|
||||
&headers.ip.ip,
|
||||
@@ -1580,13 +1622,16 @@ async fn _restore_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &mut DbCon
|
||||
&cipher,
|
||||
&cipher.update_users_revision(conn).await,
|
||||
&headers.device.uuid,
|
||||
None,
|
||||
conn,
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Some(org_uuid) = &cipher.organization_uuid {
|
||||
log_event(
|
||||
EventType::CipherRestored as i32,
|
||||
&cipher.uuid.clone(),
|
||||
String::from(org_uuid),
|
||||
org_uuid,
|
||||
headers.user.uuid.clone(),
|
||||
headers.device.atype,
|
||||
&headers.ip.ip,
|
||||
@@ -1661,13 +1706,16 @@ async fn _delete_cipher_attachment_by_id(
|
||||
&cipher,
|
||||
&cipher.update_users_revision(conn).await,
|
||||
&headers.device.uuid,
|
||||
None,
|
||||
conn,
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Some(org_uuid) = cipher.organization_uuid {
|
||||
log_event(
|
||||
EventType::CipherAttachmentDeleted as i32,
|
||||
&cipher.uuid,
|
||||
org_uuid,
|
||||
&org_uuid,
|
||||
headers.user.uuid.clone(),
|
||||
headers.device.atype,
|
||||
&headers.ip.ip,
|
||||
|
@@ -71,10 +71,10 @@ async fn get_grantees(headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
}
|
||||
|
||||
#[get("/emergency-access/<emer_id>")]
|
||||
async fn get_emergency_access(emer_id: String, mut conn: DbConn) -> JsonResult {
|
||||
async fn get_emergency_access(emer_id: &str, mut conn: DbConn) -> JsonResult {
|
||||
check_emergency_access_allowed()?;
|
||||
|
||||
match EmergencyAccess::find_by_uuid(&emer_id, &mut conn).await {
|
||||
match EmergencyAccess::find_by_uuid(emer_id, &mut conn).await {
|
||||
Some(emergency_access) => Ok(Json(emergency_access.to_json_grantee_details(&mut conn).await)),
|
||||
None => err!("Emergency access not valid."),
|
||||
}
|
||||
@@ -93,17 +93,13 @@ struct EmergencyAccessUpdateData {
|
||||
}
|
||||
|
||||
#[put("/emergency-access/<emer_id>", data = "<data>")]
|
||||
async fn put_emergency_access(
|
||||
emer_id: String,
|
||||
data: JsonUpcase<EmergencyAccessUpdateData>,
|
||||
conn: DbConn,
|
||||
) -> JsonResult {
|
||||
async fn put_emergency_access(emer_id: &str, data: JsonUpcase<EmergencyAccessUpdateData>, conn: DbConn) -> JsonResult {
|
||||
post_emergency_access(emer_id, data, conn).await
|
||||
}
|
||||
|
||||
#[post("/emergency-access/<emer_id>", data = "<data>")]
|
||||
async fn post_emergency_access(
|
||||
emer_id: String,
|
||||
emer_id: &str,
|
||||
data: JsonUpcase<EmergencyAccessUpdateData>,
|
||||
mut conn: DbConn,
|
||||
) -> JsonResult {
|
||||
@@ -111,7 +107,7 @@ async fn post_emergency_access(
|
||||
|
||||
let data: EmergencyAccessUpdateData = data.into_inner().data;
|
||||
|
||||
let mut emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &mut conn).await {
|
||||
let mut emergency_access = match EmergencyAccess::find_by_uuid(emer_id, &mut conn).await {
|
||||
Some(emergency_access) => emergency_access,
|
||||
None => err!("Emergency access not valid."),
|
||||
};
|
||||
@@ -136,12 +132,12 @@ async fn post_emergency_access(
|
||||
// region delete
|
||||
|
||||
#[delete("/emergency-access/<emer_id>")]
|
||||
async fn delete_emergency_access(emer_id: String, headers: Headers, mut conn: DbConn) -> EmptyResult {
|
||||
async fn delete_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> EmptyResult {
|
||||
check_emergency_access_allowed()?;
|
||||
|
||||
let grantor_user = headers.user;
|
||||
|
||||
let emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &mut conn).await {
|
||||
let emergency_access = match EmergencyAccess::find_by_uuid(emer_id, &mut conn).await {
|
||||
Some(emer) => {
|
||||
if emer.grantor_uuid != grantor_user.uuid && emer.grantee_uuid != Some(grantor_user.uuid) {
|
||||
err!("Emergency access not valid.")
|
||||
@@ -155,7 +151,7 @@ async fn delete_emergency_access(emer_id: String, headers: Headers, mut conn: Db
|
||||
}
|
||||
|
||||
#[post("/emergency-access/<emer_id>/delete")]
|
||||
async fn post_delete_emergency_access(emer_id: String, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||
async fn post_delete_emergency_access(emer_id: &str, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||
delete_emergency_access(emer_id, headers, conn).await
|
||||
}
|
||||
|
||||
@@ -243,7 +239,7 @@ async fn send_invite(data: JsonUpcase<EmergencyAccessInviteData>, headers: Heade
|
||||
} 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 {
|
||||
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()),
|
||||
},
|
||||
@@ -255,10 +251,10 @@ async fn send_invite(data: JsonUpcase<EmergencyAccessInviteData>, headers: Heade
|
||||
}
|
||||
|
||||
#[post("/emergency-access/<emer_id>/reinvite")]
|
||||
async fn resend_invite(emer_id: String, headers: Headers, mut conn: DbConn) -> EmptyResult {
|
||||
async fn resend_invite(emer_id: &str, headers: Headers, mut conn: DbConn) -> EmptyResult {
|
||||
check_emergency_access_allowed()?;
|
||||
|
||||
let mut emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &mut conn).await {
|
||||
let mut emergency_access = match EmergencyAccess::find_by_uuid(emer_id, &mut conn).await {
|
||||
Some(emer) => emer,
|
||||
None => err!("Emergency access not valid."),
|
||||
};
|
||||
@@ -299,7 +295,7 @@ async fn resend_invite(emer_id: String, headers: Headers, mut conn: DbConn) -> E
|
||||
}
|
||||
|
||||
// Automatically mark user as accepted if no email invites
|
||||
match accept_invite_process(grantee_user.uuid, &mut emergency_access, &email, &mut conn).await {
|
||||
match accept_invite_process(&grantee_user.uuid, &mut emergency_access, &email, &mut conn).await {
|
||||
Ok(v) => v,
|
||||
Err(e) => err!(e.to_string()),
|
||||
}
|
||||
@@ -315,12 +311,7 @@ struct AcceptData {
|
||||
}
|
||||
|
||||
#[post("/emergency-access/<emer_id>/accept", data = "<data>")]
|
||||
async fn accept_invite(
|
||||
emer_id: String,
|
||||
data: JsonUpcase<AcceptData>,
|
||||
headers: Headers,
|
||||
mut conn: DbConn,
|
||||
) -> EmptyResult {
|
||||
async fn accept_invite(emer_id: &str, data: JsonUpcase<AcceptData>, headers: Headers, mut conn: DbConn) -> EmptyResult {
|
||||
check_emergency_access_allowed()?;
|
||||
|
||||
let data: AcceptData = data.into_inner().data;
|
||||
@@ -341,7 +332,7 @@ async fn accept_invite(
|
||||
None => err!("Invited user not found"),
|
||||
};
|
||||
|
||||
let mut emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &mut conn).await {
|
||||
let mut emergency_access = match EmergencyAccess::find_by_uuid(emer_id, &mut conn).await {
|
||||
Some(emer) => emer,
|
||||
None => err!("Emergency access not valid."),
|
||||
};
|
||||
@@ -356,7 +347,7 @@ async fn accept_invite(
|
||||
&& 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 {
|
||||
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()),
|
||||
}
|
||||
@@ -372,7 +363,7 @@ async fn accept_invite(
|
||||
}
|
||||
|
||||
async fn accept_invite_process(
|
||||
grantee_uuid: String,
|
||||
grantee_uuid: &str,
|
||||
emergency_access: &mut EmergencyAccess,
|
||||
grantee_email: &str,
|
||||
conn: &mut DbConn,
|
||||
@@ -386,7 +377,7 @@ async fn accept_invite_process(
|
||||
}
|
||||
|
||||
emergency_access.status = EmergencyAccessStatus::Accepted as i32;
|
||||
emergency_access.grantee_uuid = Some(grantee_uuid);
|
||||
emergency_access.grantee_uuid = Some(String::from(grantee_uuid));
|
||||
emergency_access.email = None;
|
||||
emergency_access.save(conn).await
|
||||
}
|
||||
@@ -399,7 +390,7 @@ struct ConfirmData {
|
||||
|
||||
#[post("/emergency-access/<emer_id>/confirm", data = "<data>")]
|
||||
async fn confirm_emergency_access(
|
||||
emer_id: String,
|
||||
emer_id: &str,
|
||||
data: JsonUpcase<ConfirmData>,
|
||||
headers: Headers,
|
||||
mut conn: DbConn,
|
||||
@@ -410,7 +401,7 @@ async fn confirm_emergency_access(
|
||||
let data: ConfirmData = data.into_inner().data;
|
||||
let key = data.Key;
|
||||
|
||||
let mut emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &mut conn).await {
|
||||
let mut emergency_access = match EmergencyAccess::find_by_uuid(emer_id, &mut conn).await {
|
||||
Some(emer) => emer,
|
||||
None => err!("Emergency access not valid."),
|
||||
};
|
||||
@@ -452,11 +443,11 @@ async fn confirm_emergency_access(
|
||||
// region access emergency access
|
||||
|
||||
#[post("/emergency-access/<emer_id>/initiate")]
|
||||
async fn initiate_emergency_access(emer_id: String, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
async fn initiate_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
check_emergency_access_allowed()?;
|
||||
|
||||
let initiating_user = headers.user;
|
||||
let mut emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &mut conn).await {
|
||||
let mut emergency_access = match EmergencyAccess::find_by_uuid(emer_id, &mut conn).await {
|
||||
Some(emer) => emer,
|
||||
None => err!("Emergency access not valid."),
|
||||
};
|
||||
@@ -492,10 +483,10 @@ async fn initiate_emergency_access(emer_id: String, headers: Headers, mut conn:
|
||||
}
|
||||
|
||||
#[post("/emergency-access/<emer_id>/approve")]
|
||||
async fn approve_emergency_access(emer_id: String, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
async fn approve_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
check_emergency_access_allowed()?;
|
||||
|
||||
let mut emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &mut conn).await {
|
||||
let mut emergency_access = match EmergencyAccess::find_by_uuid(emer_id, &mut conn).await {
|
||||
Some(emer) => emer,
|
||||
None => err!("Emergency access not valid."),
|
||||
};
|
||||
@@ -530,10 +521,10 @@ async fn approve_emergency_access(emer_id: String, headers: Headers, mut conn: D
|
||||
}
|
||||
|
||||
#[post("/emergency-access/<emer_id>/reject")]
|
||||
async fn reject_emergency_access(emer_id: String, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
async fn reject_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
check_emergency_access_allowed()?;
|
||||
|
||||
let mut emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &mut conn).await {
|
||||
let mut emergency_access = match EmergencyAccess::find_by_uuid(emer_id, &mut conn).await {
|
||||
Some(emer) => emer,
|
||||
None => err!("Emergency access not valid."),
|
||||
};
|
||||
@@ -573,15 +564,15 @@ async fn reject_emergency_access(emer_id: String, headers: Headers, mut conn: Db
|
||||
// region action
|
||||
|
||||
#[post("/emergency-access/<emer_id>/view")]
|
||||
async fn view_emergency_access(emer_id: String, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
async fn view_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
check_emergency_access_allowed()?;
|
||||
|
||||
let emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &mut conn).await {
|
||||
let emergency_access = match EmergencyAccess::find_by_uuid(emer_id, &mut conn).await {
|
||||
Some(emer) => emer,
|
||||
None => err!("Emergency access not valid."),
|
||||
};
|
||||
|
||||
if !is_valid_request(&emergency_access, headers.user.uuid, EmergencyAccessType::View) {
|
||||
if !is_valid_request(&emergency_access, &headers.user.uuid, EmergencyAccessType::View) {
|
||||
err!("Emergency access not valid.")
|
||||
}
|
||||
|
||||
@@ -610,16 +601,16 @@ async fn view_emergency_access(emer_id: String, headers: Headers, mut conn: DbCo
|
||||
}
|
||||
|
||||
#[post("/emergency-access/<emer_id>/takeover")]
|
||||
async fn takeover_emergency_access(emer_id: String, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
async fn takeover_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
check_emergency_access_allowed()?;
|
||||
|
||||
let requesting_user = headers.user;
|
||||
let emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &mut conn).await {
|
||||
let emergency_access = match EmergencyAccess::find_by_uuid(emer_id, &mut conn).await {
|
||||
Some(emer) => emer,
|
||||
None => err!("Emergency access not valid."),
|
||||
};
|
||||
|
||||
if !is_valid_request(&emergency_access, requesting_user.uuid, EmergencyAccessType::Takeover) {
|
||||
if !is_valid_request(&emergency_access, &requesting_user.uuid, EmergencyAccessType::Takeover) {
|
||||
err!("Emergency access not valid.")
|
||||
}
|
||||
|
||||
@@ -649,7 +640,7 @@ struct EmergencyAccessPasswordData {
|
||||
|
||||
#[post("/emergency-access/<emer_id>/password", data = "<data>")]
|
||||
async fn password_emergency_access(
|
||||
emer_id: String,
|
||||
emer_id: &str,
|
||||
data: JsonUpcase<EmergencyAccessPasswordData>,
|
||||
headers: Headers,
|
||||
mut conn: DbConn,
|
||||
@@ -661,12 +652,12 @@ async fn password_emergency_access(
|
||||
//let key = &data.Key;
|
||||
|
||||
let requesting_user = headers.user;
|
||||
let emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &mut conn).await {
|
||||
let emergency_access = match EmergencyAccess::find_by_uuid(emer_id, &mut conn).await {
|
||||
Some(emer) => emer,
|
||||
None => err!("Emergency access not valid."),
|
||||
};
|
||||
|
||||
if !is_valid_request(&emergency_access, requesting_user.uuid, EmergencyAccessType::Takeover) {
|
||||
if !is_valid_request(&emergency_access, &requesting_user.uuid, EmergencyAccessType::Takeover) {
|
||||
err!("Emergency access not valid.")
|
||||
}
|
||||
|
||||
@@ -694,14 +685,14 @@ async fn password_emergency_access(
|
||||
// endregion
|
||||
|
||||
#[get("/emergency-access/<emer_id>/policies")]
|
||||
async fn policies_emergency_access(emer_id: String, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
async fn policies_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
let requesting_user = headers.user;
|
||||
let emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &mut conn).await {
|
||||
let emergency_access = match EmergencyAccess::find_by_uuid(emer_id, &mut conn).await {
|
||||
Some(emer) => emer,
|
||||
None => err!("Emergency access not valid."),
|
||||
};
|
||||
|
||||
if !is_valid_request(&emergency_access, requesting_user.uuid, EmergencyAccessType::Takeover) {
|
||||
if !is_valid_request(&emergency_access, &requesting_user.uuid, EmergencyAccessType::Takeover) {
|
||||
err!("Emergency access not valid.")
|
||||
}
|
||||
|
||||
@@ -722,10 +713,11 @@ async fn policies_emergency_access(emer_id: String, headers: Headers, mut conn:
|
||||
|
||||
fn is_valid_request(
|
||||
emergency_access: &EmergencyAccess,
|
||||
requesting_user_uuid: String,
|
||||
requesting_user_uuid: &str,
|
||||
requested_access_type: EmergencyAccessType,
|
||||
) -> bool {
|
||||
emergency_access.grantee_uuid == Some(requesting_user_uuid)
|
||||
emergency_access.grantee_uuid.is_some()
|
||||
&& emergency_access.grantee_uuid.as_ref().unwrap() == requesting_user_uuid
|
||||
&& emergency_access.status == EmergencyAccessStatus::RecoveryApproved as i32
|
||||
&& emergency_access.atype == requested_access_type as i32
|
||||
}
|
||||
|
@@ -32,7 +32,7 @@ struct EventRange {
|
||||
|
||||
// Upstream: https://github.com/bitwarden/server/blob/9ecf69d9cabce732cf2c57976dd9afa5728578fb/src/Api/Controllers/EventsController.cs#LL84C35-L84C41
|
||||
#[get("/organizations/<org_id>/events?<data..>")]
|
||||
async fn get_org_events(org_id: String, data: EventRange, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult {
|
||||
async fn get_org_events(org_id: &str, data: EventRange, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult {
|
||||
// Return an empty vec when we org events are disabled.
|
||||
// This prevents client errors
|
||||
let events_json: Vec<Value> = if !CONFIG.org_events_enabled() {
|
||||
@@ -45,7 +45,7 @@ async fn get_org_events(org_id: String, data: EventRange, _headers: AdminHeaders
|
||||
parse_date(&data.end)
|
||||
};
|
||||
|
||||
Event::find_by_organization_uuid(&org_id, &start_date, &end_date, &mut conn)
|
||||
Event::find_by_organization_uuid(org_id, &start_date, &end_date, &mut conn)
|
||||
.await
|
||||
.iter()
|
||||
.map(|e| e.to_json())
|
||||
@@ -60,14 +60,14 @@ async fn get_org_events(org_id: String, data: EventRange, _headers: AdminHeaders
|
||||
}
|
||||
|
||||
#[get("/ciphers/<cipher_id>/events?<data..>")]
|
||||
async fn get_cipher_events(cipher_id: String, data: EventRange, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
async fn get_cipher_events(cipher_id: &str, data: EventRange, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
// Return an empty vec when we org events are disabled.
|
||||
// This prevents client errors
|
||||
let events_json: Vec<Value> = if !CONFIG.org_events_enabled() {
|
||||
Vec::with_capacity(0)
|
||||
} else {
|
||||
let mut events_json = Vec::with_capacity(0);
|
||||
if UserOrganization::user_has_ge_admin_access_to_cipher(&headers.user.uuid, &cipher_id, &mut conn).await {
|
||||
if UserOrganization::user_has_ge_admin_access_to_cipher(&headers.user.uuid, cipher_id, &mut conn).await {
|
||||
let start_date = parse_date(&data.start);
|
||||
let end_date = if let Some(before_date) = &data.continuation_token {
|
||||
parse_date(before_date)
|
||||
@@ -75,7 +75,7 @@ async fn get_cipher_events(cipher_id: String, data: EventRange, headers: Headers
|
||||
parse_date(&data.end)
|
||||
};
|
||||
|
||||
events_json = Event::find_by_cipher_uuid(&cipher_id, &start_date, &end_date, &mut conn)
|
||||
events_json = Event::find_by_cipher_uuid(cipher_id, &start_date, &end_date, &mut conn)
|
||||
.await
|
||||
.iter()
|
||||
.map(|e| e.to_json())
|
||||
@@ -93,8 +93,8 @@ async fn get_cipher_events(cipher_id: String, data: EventRange, headers: Headers
|
||||
|
||||
#[get("/organizations/<org_id>/users/<user_org_id>/events?<data..>")]
|
||||
async fn get_user_events(
|
||||
org_id: String,
|
||||
user_org_id: String,
|
||||
org_id: &str,
|
||||
user_org_id: &str,
|
||||
data: EventRange,
|
||||
_headers: AdminHeaders,
|
||||
mut conn: DbConn,
|
||||
@@ -111,7 +111,7 @@ async fn get_user_events(
|
||||
parse_date(&data.end)
|
||||
};
|
||||
|
||||
Event::find_by_org_and_user_org(&org_id, &user_org_id, &start_date, &end_date, &mut conn)
|
||||
Event::find_by_org_and_user_org(org_id, user_org_id, &start_date, &end_date, &mut conn)
|
||||
.await
|
||||
.iter()
|
||||
.map(|e| e.to_json())
|
||||
@@ -185,7 +185,7 @@ async fn post_events_collect(data: JsonUpcaseVec<EventCollection>, headers: Head
|
||||
_log_event(
|
||||
event.Type,
|
||||
org_uuid,
|
||||
String::from(org_uuid),
|
||||
org_uuid,
|
||||
&headers.user.uuid,
|
||||
headers.device.atype,
|
||||
Some(event_date),
|
||||
@@ -202,7 +202,7 @@ async fn post_events_collect(data: JsonUpcaseVec<EventCollection>, headers: Head
|
||||
_log_event(
|
||||
event.Type,
|
||||
cipher_uuid,
|
||||
org_uuid,
|
||||
&org_uuid,
|
||||
&headers.user.uuid,
|
||||
headers.device.atype,
|
||||
Some(event_date),
|
||||
@@ -262,7 +262,7 @@ async fn _log_user_event(
|
||||
pub async fn log_event(
|
||||
event_type: i32,
|
||||
source_uuid: &str,
|
||||
org_uuid: String,
|
||||
org_uuid: &str,
|
||||
act_user_uuid: String,
|
||||
device_type: i32,
|
||||
ip: &IpAddr,
|
||||
@@ -278,7 +278,7 @@ pub async fn log_event(
|
||||
async fn _log_event(
|
||||
event_type: i32,
|
||||
source_uuid: &str,
|
||||
org_uuid: String,
|
||||
org_uuid: &str,
|
||||
act_user_uuid: &str,
|
||||
device_type: i32,
|
||||
event_date: Option<NaiveDateTime>,
|
||||
@@ -314,7 +314,7 @@ async fn _log_event(
|
||||
_ => {}
|
||||
}
|
||||
|
||||
event.org_uuid = Some(org_uuid);
|
||||
event.org_uuid = Some(String::from(org_uuid));
|
||||
event.act_user_uuid = Some(String::from(act_user_uuid));
|
||||
event.device_type = Some(device_type);
|
||||
event.ip_address = Some(ip.to_string());
|
||||
|
@@ -24,8 +24,8 @@ async fn get_folders(headers: Headers, mut conn: DbConn) -> Json<Value> {
|
||||
}
|
||||
|
||||
#[get("/folders/<uuid>")]
|
||||
async fn get_folder(uuid: String, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
let folder = match Folder::find_by_uuid(&uuid, &mut conn).await {
|
||||
async fn get_folder(uuid: &str, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
let folder = match Folder::find_by_uuid(uuid, &mut conn).await {
|
||||
Some(folder) => folder,
|
||||
_ => err!("Invalid folder"),
|
||||
};
|
||||
@@ -50,14 +50,14 @@ async fn post_folders(data: JsonUpcase<FolderData>, headers: Headers, mut conn:
|
||||
let mut folder = Folder::new(headers.user.uuid, data.Name);
|
||||
|
||||
folder.save(&mut conn).await?;
|
||||
nt.send_folder_update(UpdateType::SyncFolderCreate, &folder, &headers.device.uuid).await;
|
||||
nt.send_folder_update(UpdateType::SyncFolderCreate, &folder, &headers.device.uuid, &mut conn).await;
|
||||
|
||||
Ok(Json(folder.to_json()))
|
||||
}
|
||||
|
||||
#[post("/folders/<uuid>", data = "<data>")]
|
||||
async fn post_folder(
|
||||
uuid: String,
|
||||
uuid: &str,
|
||||
data: JsonUpcase<FolderData>,
|
||||
headers: Headers,
|
||||
conn: DbConn,
|
||||
@@ -68,7 +68,7 @@ async fn post_folder(
|
||||
|
||||
#[put("/folders/<uuid>", data = "<data>")]
|
||||
async fn put_folder(
|
||||
uuid: String,
|
||||
uuid: &str,
|
||||
data: JsonUpcase<FolderData>,
|
||||
headers: Headers,
|
||||
mut conn: DbConn,
|
||||
@@ -76,7 +76,7 @@ async fn put_folder(
|
||||
) -> JsonResult {
|
||||
let data: FolderData = data.into_inner().data;
|
||||
|
||||
let mut folder = match Folder::find_by_uuid(&uuid, &mut conn).await {
|
||||
let mut folder = match Folder::find_by_uuid(uuid, &mut conn).await {
|
||||
Some(folder) => folder,
|
||||
_ => err!("Invalid folder"),
|
||||
};
|
||||
@@ -88,19 +88,19 @@ async fn put_folder(
|
||||
folder.name = data.Name;
|
||||
|
||||
folder.save(&mut conn).await?;
|
||||
nt.send_folder_update(UpdateType::SyncFolderUpdate, &folder, &headers.device.uuid).await;
|
||||
nt.send_folder_update(UpdateType::SyncFolderUpdate, &folder, &headers.device.uuid, &mut conn).await;
|
||||
|
||||
Ok(Json(folder.to_json()))
|
||||
}
|
||||
|
||||
#[post("/folders/<uuid>/delete")]
|
||||
async fn delete_folder_post(uuid: String, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
||||
async fn delete_folder_post(uuid: &str, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
||||
delete_folder(uuid, headers, conn, nt).await
|
||||
}
|
||||
|
||||
#[delete("/folders/<uuid>")]
|
||||
async fn delete_folder(uuid: String, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
||||
let folder = match Folder::find_by_uuid(&uuid, &mut conn).await {
|
||||
async fn delete_folder(uuid: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
||||
let folder = match Folder::find_by_uuid(uuid, &mut conn).await {
|
||||
Some(folder) => folder,
|
||||
_ => err!("Invalid folder"),
|
||||
};
|
||||
@@ -112,6 +112,6 @@ async fn delete_folder(uuid: String, headers: Headers, mut conn: DbConn, nt: Not
|
||||
// Delete the actual folder entry
|
||||
folder.delete(&mut conn).await?;
|
||||
|
||||
nt.send_folder_update(UpdateType::SyncFolderDelete, &folder, &headers.device.uuid).await;
|
||||
nt.send_folder_update(UpdateType::SyncFolderDelete, &folder, &headers.device.uuid, &mut conn).await;
|
||||
Ok(())
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ mod emergency_access;
|
||||
mod events;
|
||||
mod folders;
|
||||
mod organizations;
|
||||
mod public;
|
||||
mod sends;
|
||||
pub mod two_factor;
|
||||
|
||||
@@ -14,7 +15,6 @@ pub use sends::purge_sends;
|
||||
pub use two_factor::send_incomplete_2fa_notifications;
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
let mut device_token_routes = routes![clear_device_token, put_device_token];
|
||||
let mut eq_domains_routes = routes![get_eq_domains, post_eq_domains, put_eq_domains];
|
||||
let mut hibp_routes = routes![hibp_breach];
|
||||
let mut meta_routes = routes![alive, now, version, config];
|
||||
@@ -28,7 +28,7 @@ pub fn routes() -> Vec<Route> {
|
||||
routes.append(&mut organizations::routes());
|
||||
routes.append(&mut two_factor::routes());
|
||||
routes.append(&mut sends::routes());
|
||||
routes.append(&mut device_token_routes);
|
||||
routes.append(&mut public::routes());
|
||||
routes.append(&mut eq_domains_routes);
|
||||
routes.append(&mut hibp_routes);
|
||||
routes.append(&mut meta_routes);
|
||||
@@ -57,37 +57,6 @@ use crate::{
|
||||
util::get_reqwest_client,
|
||||
};
|
||||
|
||||
#[put("/devices/identifier/<uuid>/clear-token")]
|
||||
fn clear_device_token(uuid: String) -> &'static str {
|
||||
// This endpoint doesn't have auth header
|
||||
|
||||
let _ = uuid;
|
||||
// uuid is not related to deviceId
|
||||
|
||||
// This only clears push token
|
||||
// https://github.com/bitwarden/core/blob/master/src/Api/Controllers/DevicesController.cs#L109
|
||||
// https://github.com/bitwarden/core/blob/master/src/Core/Services/Implementations/DeviceService.cs#L37
|
||||
""
|
||||
}
|
||||
|
||||
#[put("/devices/identifier/<uuid>/token", data = "<data>")]
|
||||
fn put_device_token(uuid: String, data: JsonUpcase<Value>, headers: Headers) -> Json<Value> {
|
||||
let _data: Value = data.into_inner().data;
|
||||
// Data has a single string value "PushToken"
|
||||
let _ = uuid;
|
||||
// uuid is not related to deviceId
|
||||
|
||||
// TODO: This should save the push token, but we don't have push functionality
|
||||
|
||||
Json(json!({
|
||||
"Id": headers.device.uuid,
|
||||
"Name": headers.device.name,
|
||||
"Type": headers.device.atype,
|
||||
"Identifier": headers.device.uuid,
|
||||
"CreationDate": crate::util::format_date(&headers.device.created_at),
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[allow(non_snake_case)]
|
||||
struct GlobalDomain {
|
||||
@@ -170,7 +139,7 @@ async fn put_eq_domains(
|
||||
}
|
||||
|
||||
#[get("/hibp/breach?<username>")]
|
||||
async fn hibp_breach(username: String) -> JsonResult {
|
||||
async fn hibp_breach(username: &str) -> JsonResult {
|
||||
let url = format!(
|
||||
"https://haveibeenpwned.com/api/v3/breachedaccount/{username}?truncateResponse=false&includeUnverified=false"
|
||||
);
|
||||
|
File diff suppressed because it is too large
Load Diff
238
src/api/core/public.rs
Normal file
238
src/api/core/public.rs
Normal file
@@ -0,0 +1,238 @@
|
||||
use chrono::Utc;
|
||||
use rocket::{
|
||||
request::{self, FromRequest, Outcome},
|
||||
Request, Route,
|
||||
};
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::{
|
||||
api::{EmptyResult, JsonUpcase},
|
||||
auth,
|
||||
db::{models::*, DbConn},
|
||||
mail, CONFIG,
|
||||
};
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![ldap_import]
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
struct OrgImportGroupData {
|
||||
Name: String,
|
||||
ExternalId: String,
|
||||
MemberExternalIds: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
struct OrgImportUserData {
|
||||
Email: String,
|
||||
ExternalId: String,
|
||||
Deleted: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
struct OrgImportData {
|
||||
Groups: Vec<OrgImportGroupData>,
|
||||
Members: Vec<OrgImportUserData>,
|
||||
OverwriteExisting: bool,
|
||||
// LargeImport: bool, // For now this will not be used, upstream uses this to prevent syncs of more then 2000 users or groups without the flag set.
|
||||
}
|
||||
|
||||
#[post("/public/organization/import", data = "<data>")]
|
||||
async fn ldap_import(data: JsonUpcase<OrgImportData>, token: PublicToken, mut conn: DbConn) -> EmptyResult {
|
||||
// Most of the logic for this function can be found here
|
||||
// https://github.com/bitwarden/server/blob/fd892b2ff4547648a276734fb2b14a8abae2c6f5/src/Core/Services/Implementations/OrganizationService.cs#L1797
|
||||
|
||||
let org_id = token.0;
|
||||
let data = data.into_inner().data;
|
||||
|
||||
for user_data in &data.Members {
|
||||
if user_data.Deleted {
|
||||
// If user is marked for deletion and it exists, revoke it
|
||||
if let Some(mut user_org) =
|
||||
UserOrganization::find_by_email_and_org(&user_data.Email, &org_id, &mut conn).await
|
||||
{
|
||||
user_org.revoke();
|
||||
user_org.save(&mut conn).await?;
|
||||
}
|
||||
|
||||
// If user is part of the organization, restore it
|
||||
} else if let Some(mut user_org) =
|
||||
UserOrganization::find_by_email_and_org(&user_data.Email, &org_id, &mut conn).await
|
||||
{
|
||||
if user_org.status < UserOrgStatus::Revoked as i32 {
|
||||
user_org.restore();
|
||||
user_org.save(&mut conn).await?;
|
||||
}
|
||||
} else {
|
||||
// If user is not part of the organization
|
||||
let user = match User::find_by_mail(&user_data.Email, &mut conn).await {
|
||||
Some(user) => user, // exists in vaultwarden
|
||||
None => {
|
||||
// doesn't exist in vaultwarden
|
||||
let mut new_user = User::new(user_data.Email.clone());
|
||||
new_user.set_external_id(Some(user_data.ExternalId.clone()));
|
||||
new_user.save(&mut conn).await?;
|
||||
|
||||
if !CONFIG.mail_enabled() {
|
||||
let invitation = Invitation::new(&new_user.email);
|
||||
invitation.save(&mut conn).await?;
|
||||
}
|
||||
new_user
|
||||
}
|
||||
};
|
||||
let user_org_status = if CONFIG.mail_enabled() {
|
||||
UserOrgStatus::Invited as i32
|
||||
} else {
|
||||
UserOrgStatus::Accepted as i32 // Automatically mark user as accepted if no email invites
|
||||
};
|
||||
|
||||
let mut new_org_user = UserOrganization::new(user.uuid.clone(), org_id.clone());
|
||||
new_org_user.access_all = false;
|
||||
new_org_user.atype = UserOrgType::User as i32;
|
||||
new_org_user.status = user_org_status;
|
||||
|
||||
new_org_user.save(&mut conn).await?;
|
||||
|
||||
if CONFIG.mail_enabled() {
|
||||
let (org_name, org_email) = match Organization::find_by_uuid(&org_id, &mut conn).await {
|
||||
Some(org) => (org.name, org.billing_email),
|
||||
None => err!("Error looking up organization"),
|
||||
};
|
||||
|
||||
mail::send_invite(
|
||||
&user_data.Email,
|
||||
&user.uuid,
|
||||
Some(org_id.clone()),
|
||||
Some(new_org_user.uuid),
|
||||
&org_name,
|
||||
Some(org_email),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if CONFIG.org_groups_enabled() {
|
||||
for group_data in &data.Groups {
|
||||
let group_uuid = match Group::find_by_external_id(&group_data.ExternalId, &mut conn).await {
|
||||
Some(group) => group.uuid,
|
||||
None => {
|
||||
let mut group =
|
||||
Group::new(org_id.clone(), group_data.Name.clone(), false, Some(group_data.ExternalId.clone()));
|
||||
group.save(&mut conn).await?;
|
||||
group.uuid
|
||||
}
|
||||
};
|
||||
|
||||
GroupUser::delete_all_by_group(&group_uuid, &mut conn).await?;
|
||||
|
||||
for ext_id in &group_data.MemberExternalIds {
|
||||
if let Some(user) = User::find_by_external_id(ext_id, &mut conn).await {
|
||||
if let Some(user_org) = UserOrganization::find_by_user_and_org(&user.uuid, &org_id, &mut conn).await
|
||||
{
|
||||
let mut group_user = GroupUser::new(group_uuid.clone(), user_org.uuid.clone());
|
||||
group_user.save(&mut conn).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!("Group support is disabled, groups will not be imported!");
|
||||
}
|
||||
|
||||
// If this flag is enabled, any user that isn't provided in the Users list will be removed (by default they will be kept unless they have Deleted == true)
|
||||
if data.OverwriteExisting {
|
||||
// Generate a HashSet to quickly verify if a member is listed or not.
|
||||
let sync_members: HashSet<String> = data.Members.into_iter().map(|m| m.ExternalId).collect();
|
||||
for user_org in UserOrganization::find_by_org(&org_id, &mut conn).await {
|
||||
if let Some(user_external_id) =
|
||||
User::find_by_uuid(&user_org.user_uuid, &mut conn).await.map(|u| u.external_id)
|
||||
{
|
||||
if user_external_id.is_some() && !sync_members.contains(&user_external_id.unwrap()) {
|
||||
if user_org.atype == UserOrgType::Owner && user_org.status == UserOrgStatus::Confirmed as i32 {
|
||||
// Removing owner, check that there is at least one other confirmed owner
|
||||
if UserOrganization::count_confirmed_by_org_and_type(&org_id, UserOrgType::Owner, &mut conn)
|
||||
.await
|
||||
<= 1
|
||||
{
|
||||
warn!("Can't delete the last owner");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
user_org.delete(&mut conn).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub struct PublicToken(String);
|
||||
|
||||
#[rocket::async_trait]
|
||||
impl<'r> FromRequest<'r> for PublicToken {
|
||||
type Error = &'static str;
|
||||
|
||||
async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
||||
let headers = request.headers();
|
||||
// Get access_token
|
||||
let access_token: &str = match headers.get_one("Authorization") {
|
||||
Some(a) => match a.rsplit("Bearer ").next() {
|
||||
Some(split) => split,
|
||||
None => err_handler!("No access token provided"),
|
||||
},
|
||||
None => err_handler!("No access token provided"),
|
||||
};
|
||||
// Check JWT token is valid and get device and user from it
|
||||
let claims = match auth::decode_api_org(access_token) {
|
||||
Ok(claims) => claims,
|
||||
Err(_) => err_handler!("Invalid claim"),
|
||||
};
|
||||
// Check if time is between claims.nbf and claims.exp
|
||||
let time_now = Utc::now().naive_utc().timestamp();
|
||||
if time_now < claims.nbf {
|
||||
err_handler!("Token issued in the future");
|
||||
}
|
||||
if time_now > claims.exp {
|
||||
err_handler!("Token expired");
|
||||
}
|
||||
// Check if claims.iss is host|claims.scope[0]
|
||||
let host = match auth::Host::from_request(request).await {
|
||||
Outcome::Success(host) => host,
|
||||
_ => err_handler!("Error getting Host"),
|
||||
};
|
||||
let complete_host = format!("{}|{}", host.host, claims.scope[0]);
|
||||
if complete_host != claims.iss {
|
||||
err_handler!("Token not issued by this server");
|
||||
}
|
||||
|
||||
// Check if claims.sub is org_api_key.uuid
|
||||
// Check if claims.client_sub is org_api_key.org_uuid
|
||||
let conn = match DbConn::from_request(request).await {
|
||||
Outcome::Success(conn) => conn,
|
||||
_ => err_handler!("Error getting DB"),
|
||||
};
|
||||
let org_uuid = match claims.client_id.strip_prefix("organization.") {
|
||||
Some(uuid) => uuid,
|
||||
None => err_handler!("Malformed client_id"),
|
||||
};
|
||||
let org_api_key = match OrganizationApiKey::find_by_org_uuid(org_uuid, &conn).await {
|
||||
Some(org_api_key) => org_api_key,
|
||||
None => err_handler!("Invalid client_id"),
|
||||
};
|
||||
if org_api_key.org_uuid != claims.client_sub {
|
||||
err_handler!("Token not issued for this org");
|
||||
}
|
||||
if org_api_key.uuid != claims.sub {
|
||||
err_handler!("Token not issued for this client");
|
||||
}
|
||||
|
||||
Outcome::Success(PublicToken(claims.client_sub))
|
||||
}
|
||||
}
|
@@ -154,8 +154,8 @@ async fn get_sends(headers: Headers, mut conn: DbConn) -> Json<Value> {
|
||||
}
|
||||
|
||||
#[get("/sends/<uuid>")]
|
||||
async fn get_send(uuid: String, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
let send = match Send::find_by_uuid(&uuid, &mut conn).await {
|
||||
async fn get_send(uuid: &str, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
let send = match Send::find_by_uuid(uuid, &mut conn).await {
|
||||
Some(send) => send,
|
||||
None => err!("Send not found"),
|
||||
};
|
||||
@@ -180,7 +180,14 @@ async fn post_send(data: JsonUpcase<SendData>, headers: Headers, mut conn: DbCon
|
||||
|
||||
let mut send = create_send(data, headers.user.uuid)?;
|
||||
send.save(&mut conn).await?;
|
||||
nt.send_send_update(UpdateType::SyncSendCreate, &send, &send.update_users_revision(&mut conn).await).await;
|
||||
nt.send_send_update(
|
||||
UpdateType::SyncSendCreate,
|
||||
&send,
|
||||
&send.update_users_revision(&mut conn).await,
|
||||
&headers.device.uuid,
|
||||
&mut conn,
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(Json(send.to_json()))
|
||||
}
|
||||
@@ -252,7 +259,14 @@ async fn post_send_file(data: Form<UploadData<'_>>, headers: Headers, mut conn:
|
||||
|
||||
// Save the changes in the database
|
||||
send.save(&mut conn).await?;
|
||||
nt.send_send_update(UpdateType::SyncSendCreate, &send, &send.update_users_revision(&mut conn).await).await;
|
||||
nt.send_send_update(
|
||||
UpdateType::SyncSendCreate,
|
||||
&send,
|
||||
&send.update_users_revision(&mut conn).await,
|
||||
&headers.device.uuid,
|
||||
&mut conn,
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(Json(send.to_json()))
|
||||
}
|
||||
@@ -315,8 +329,8 @@ async fn post_send_file_v2(data: JsonUpcase<SendData>, headers: Headers, mut con
|
||||
// https://github.com/bitwarden/server/blob/d0c793c95181dfb1b447eb450f85ba0bfd7ef643/src/Api/Controllers/SendsController.cs#L243
|
||||
#[post("/sends/<send_uuid>/file/<file_id>", format = "multipart/form-data", data = "<data>")]
|
||||
async fn post_send_file_v2_data(
|
||||
send_uuid: String,
|
||||
file_id: String,
|
||||
send_uuid: &str,
|
||||
file_id: &str,
|
||||
data: Form<UploadDataV2<'_>>,
|
||||
headers: Headers,
|
||||
mut conn: DbConn,
|
||||
@@ -326,20 +340,30 @@ async fn post_send_file_v2_data(
|
||||
|
||||
let mut data = data.into_inner();
|
||||
|
||||
if let Some(send) = Send::find_by_uuid(&send_uuid, &mut conn).await {
|
||||
let folder_path = tokio::fs::canonicalize(&CONFIG.sends_folder()).await?.join(&send_uuid);
|
||||
let file_path = folder_path.join(&file_id);
|
||||
tokio::fs::create_dir_all(&folder_path).await?;
|
||||
let Some(send) = Send::find_by_uuid(send_uuid, &mut conn).await else { err!("Send not found. Unable to save the file.") };
|
||||
|
||||
if let Err(_err) = data.data.persist_to(&file_path).await {
|
||||
data.data.move_copy_to(file_path).await?
|
||||
}
|
||||
|
||||
nt.send_send_update(UpdateType::SyncSendCreate, &send, &send.update_users_revision(&mut conn).await).await;
|
||||
} else {
|
||||
err!("Send not found. Unable to save the file.");
|
||||
let Some(send_user_id) = &send.user_uuid else {err!("Sends are only supported for users at the moment")};
|
||||
if send_user_id != &headers.user.uuid {
|
||||
err!("Send doesn't belong to user");
|
||||
}
|
||||
|
||||
let folder_path = tokio::fs::canonicalize(&CONFIG.sends_folder()).await?.join(send_uuid);
|
||||
let file_path = folder_path.join(file_id);
|
||||
tokio::fs::create_dir_all(&folder_path).await?;
|
||||
|
||||
if let Err(_err) = data.data.persist_to(&file_path).await {
|
||||
data.data.move_copy_to(file_path).await?
|
||||
}
|
||||
|
||||
nt.send_send_update(
|
||||
UpdateType::SyncSendCreate,
|
||||
&send,
|
||||
&send.update_users_revision(&mut conn).await,
|
||||
&headers.device.uuid,
|
||||
&mut conn,
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -351,13 +375,13 @@ pub struct SendAccessData {
|
||||
|
||||
#[post("/sends/access/<access_id>", data = "<data>")]
|
||||
async fn post_access(
|
||||
access_id: String,
|
||||
access_id: &str,
|
||||
data: JsonUpcase<SendAccessData>,
|
||||
mut conn: DbConn,
|
||||
ip: ClientIp,
|
||||
nt: Notify<'_>,
|
||||
) -> JsonResult {
|
||||
let mut send = match Send::find_by_access_id(&access_id, &mut conn).await {
|
||||
let mut send = match Send::find_by_access_id(access_id, &mut conn).await {
|
||||
Some(s) => s,
|
||||
None => err_code!(SEND_INACCESSIBLE_MSG, 404),
|
||||
};
|
||||
@@ -397,21 +421,28 @@ async fn post_access(
|
||||
|
||||
send.save(&mut conn).await?;
|
||||
|
||||
nt.send_send_update(UpdateType::SyncSendUpdate, &send, &send.update_users_revision(&mut conn).await).await;
|
||||
nt.send_send_update(
|
||||
UpdateType::SyncSendUpdate,
|
||||
&send,
|
||||
&send.update_users_revision(&mut conn).await,
|
||||
&String::from("00000000-0000-0000-0000-000000000000"),
|
||||
&mut conn,
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(Json(send.to_json_access(&mut conn).await))
|
||||
}
|
||||
|
||||
#[post("/sends/<send_id>/access/file/<file_id>", data = "<data>")]
|
||||
async fn post_access_file(
|
||||
send_id: String,
|
||||
file_id: String,
|
||||
send_id: &str,
|
||||
file_id: &str,
|
||||
data: JsonUpcase<SendAccessData>,
|
||||
host: Host,
|
||||
mut conn: DbConn,
|
||||
nt: Notify<'_>,
|
||||
) -> JsonResult {
|
||||
let mut send = match Send::find_by_uuid(&send_id, &mut conn).await {
|
||||
let mut send = match Send::find_by_uuid(send_id, &mut conn).await {
|
||||
Some(s) => s,
|
||||
None => err_code!(SEND_INACCESSIBLE_MSG, 404),
|
||||
};
|
||||
@@ -448,9 +479,16 @@ async fn post_access_file(
|
||||
|
||||
send.save(&mut conn).await?;
|
||||
|
||||
nt.send_send_update(UpdateType::SyncSendUpdate, &send, &send.update_users_revision(&mut conn).await).await;
|
||||
nt.send_send_update(
|
||||
UpdateType::SyncSendUpdate,
|
||||
&send,
|
||||
&send.update_users_revision(&mut conn).await,
|
||||
&String::from("00000000-0000-0000-0000-000000000000"),
|
||||
&mut conn,
|
||||
)
|
||||
.await;
|
||||
|
||||
let token_claims = crate::auth::generate_send_claims(&send_id, &file_id);
|
||||
let token_claims = crate::auth::generate_send_claims(send_id, file_id);
|
||||
let token = crate::auth::encode_jwt(&token_claims);
|
||||
Ok(Json(json!({
|
||||
"Object": "send-fileDownload",
|
||||
@@ -460,8 +498,8 @@ async fn post_access_file(
|
||||
}
|
||||
|
||||
#[get("/sends/<send_id>/<file_id>?<t>")]
|
||||
async fn download_send(send_id: SafeString, file_id: SafeString, t: String) -> Option<NamedFile> {
|
||||
if let Ok(claims) = crate::auth::decode_send(&t) {
|
||||
async fn download_send(send_id: SafeString, file_id: SafeString, t: &str) -> Option<NamedFile> {
|
||||
if let Ok(claims) = crate::auth::decode_send(t) {
|
||||
if claims.sub == format!("{send_id}/{file_id}") {
|
||||
return NamedFile::open(Path::new(&CONFIG.sends_folder()).join(send_id).join(file_id)).await.ok();
|
||||
}
|
||||
@@ -471,7 +509,7 @@ async fn download_send(send_id: SafeString, file_id: SafeString, t: String) -> O
|
||||
|
||||
#[put("/sends/<id>", data = "<data>")]
|
||||
async fn put_send(
|
||||
id: String,
|
||||
id: &str,
|
||||
data: JsonUpcase<SendData>,
|
||||
headers: Headers,
|
||||
mut conn: DbConn,
|
||||
@@ -482,7 +520,7 @@ async fn put_send(
|
||||
let data: SendData = data.into_inner().data;
|
||||
enforce_disable_hide_email_policy(&data, &headers, &mut conn).await?;
|
||||
|
||||
let mut send = match Send::find_by_uuid(&id, &mut conn).await {
|
||||
let mut send = match Send::find_by_uuid(id, &mut conn).await {
|
||||
Some(s) => s,
|
||||
None => err!("Send not found"),
|
||||
};
|
||||
@@ -530,14 +568,21 @@ async fn put_send(
|
||||
}
|
||||
|
||||
send.save(&mut conn).await?;
|
||||
nt.send_send_update(UpdateType::SyncSendUpdate, &send, &send.update_users_revision(&mut conn).await).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()))
|
||||
}
|
||||
|
||||
#[delete("/sends/<id>")]
|
||||
async fn delete_send(id: String, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
||||
let send = match Send::find_by_uuid(&id, &mut conn).await {
|
||||
async fn delete_send(id: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
||||
let send = match Send::find_by_uuid(id, &mut conn).await {
|
||||
Some(s) => s,
|
||||
None => err!("Send not found"),
|
||||
};
|
||||
@@ -547,16 +592,23 @@ async fn delete_send(id: String, headers: Headers, mut conn: DbConn, nt: Notify<
|
||||
}
|
||||
|
||||
send.delete(&mut conn).await?;
|
||||
nt.send_send_update(UpdateType::SyncSendDelete, &send, &send.update_users_revision(&mut conn).await).await;
|
||||
nt.send_send_update(
|
||||
UpdateType::SyncSendDelete,
|
||||
&send,
|
||||
&send.update_users_revision(&mut conn).await,
|
||||
&headers.device.uuid,
|
||||
&mut conn,
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[put("/sends/<id>/remove-password")]
|
||||
async fn put_remove_password(id: String, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult {
|
||||
async fn put_remove_password(id: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult {
|
||||
enforce_disable_send_policy(&headers, &mut conn).await?;
|
||||
|
||||
let mut send = match Send::find_by_uuid(&id, &mut conn).await {
|
||||
let mut send = match Send::find_by_uuid(id, &mut conn).await {
|
||||
Some(s) => s,
|
||||
None => err!("Send not found"),
|
||||
};
|
||||
@@ -567,7 +619,14 @@ async fn put_remove_password(id: String, headers: Headers, mut conn: DbConn, nt:
|
||||
|
||||
send.set_password(None);
|
||||
send.save(&mut conn).await?;
|
||||
nt.send_send_update(UpdateType::SyncSendUpdate, &send, &send.update_users_revision(&mut conn).await).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()))
|
||||
}
|
||||
|
@@ -97,15 +97,15 @@ async fn icon_redirect(domain: &str, template: &str) -> Option<Redirect> {
|
||||
}
|
||||
|
||||
#[get("/<domain>/icon.png")]
|
||||
async fn icon_external(domain: String) -> Option<Redirect> {
|
||||
icon_redirect(&domain, &CONFIG._icon_service_url()).await
|
||||
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: String) -> Cached<(ContentType, Vec<u8>)> {
|
||||
async fn icon_internal(domain: &str) -> Cached<(ContentType, Vec<u8>)> {
|
||||
const FALLBACK_ICON: &[u8] = include_bytes!("../static/images/fallback-icon.png");
|
||||
|
||||
if !is_valid_domain(&domain) {
|
||||
if !is_valid_domain(domain) {
|
||||
warn!("Invalid domain: {}", domain);
|
||||
return Cached::ttl(
|
||||
(ContentType::new("image", "png"), FALLBACK_ICON.to_vec()),
|
||||
@@ -114,7 +114,7 @@ async fn icon_internal(domain: String) -> Cached<(ContentType, Vec<u8>)> {
|
||||
);
|
||||
}
|
||||
|
||||
match get_icon(&domain).await {
|
||||
match get_icon(domain).await {
|
||||
Some((icon, icon_type)) => {
|
||||
Cached::ttl((ContentType::new("image", icon_type), icon), CONFIG.icon_cache_ttl(), true)
|
||||
}
|
||||
@@ -682,7 +682,7 @@ async fn download_icon(domain: &str) -> Result<(Bytes, Option<&str>), Error> {
|
||||
|
||||
for icon in icon_result.iconlist.iter().take(5) {
|
||||
if icon.href.starts_with("data:image") {
|
||||
let datauri = DataUrl::process(&icon.href).unwrap();
|
||||
let Ok(datauri) = DataUrl::process(&icon.href) else {continue};
|
||||
// Check if we are able to decode the data uri
|
||||
let mut body = BytesMut::new();
|
||||
match datauri.decode::<_, ()>(|bytes| {
|
||||
@@ -891,6 +891,7 @@ impl Emitter for FaviconEmitter {
|
||||
FaviconToken::EndTag(ref mut tag) => {
|
||||
// Always clean seen attributes
|
||||
self.seen_attributes.clear();
|
||||
self.set_last_start_tag(None);
|
||||
|
||||
// Only trigger an emit for the </head> tag.
|
||||
// This is matched, and will break the for-loop.
|
||||
|
@@ -14,7 +14,7 @@ use crate::{
|
||||
core::two_factor::{duo, email, email::EmailTokenData, yubikey},
|
||||
ApiResult, EmptyResult, JsonResult, JsonUpcase,
|
||||
},
|
||||
auth::{ClientHeaders, ClientIp},
|
||||
auth::{generate_organization_api_key_login_claims, ClientHeaders, ClientIp},
|
||||
db::{models::*, DbConn},
|
||||
error::MapResult,
|
||||
mail, util, CONFIG,
|
||||
@@ -276,16 +276,23 @@ async fn _api_key_login(
|
||||
conn: &mut DbConn,
|
||||
ip: &ClientIp,
|
||||
) -> JsonResult {
|
||||
// Validate scope
|
||||
let scope = data.scope.as_ref().unwrap();
|
||||
if scope != "api" {
|
||||
err!("Scope not supported")
|
||||
}
|
||||
let scope_vec = vec!["api".into()];
|
||||
|
||||
// Ratelimit the login
|
||||
crate::ratelimit::check_limit_login(&ip.ip)?;
|
||||
|
||||
// Validate scope
|
||||
match data.scope.as_ref().unwrap().as_ref() {
|
||||
"api" => _user_api_key_login(data, user_uuid, conn, ip).await,
|
||||
"api.organization" => _organization_api_key_login(data, conn, ip).await,
|
||||
_ => err!("Scope not supported"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn _user_api_key_login(
|
||||
data: ConnectData,
|
||||
user_uuid: &mut Option<String>,
|
||||
conn: &mut DbConn,
|
||||
ip: &ClientIp,
|
||||
) -> JsonResult {
|
||||
// Get the user via the client_id
|
||||
let client_id = data.client_id.as_ref().unwrap();
|
||||
let client_user_uuid = match client_id.strip_prefix("user.") {
|
||||
@@ -342,6 +349,7 @@ async fn _api_key_login(
|
||||
}
|
||||
|
||||
// Common
|
||||
let scope_vec = vec!["api".into()];
|
||||
let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, conn).await;
|
||||
let (access_token, expires_in) = device.refresh_tokens(&user, orgs, scope_vec);
|
||||
device.save(conn).await?;
|
||||
@@ -362,13 +370,43 @@ async fn _api_key_login(
|
||||
"KdfMemory": user.client_kdf_memory,
|
||||
"KdfParallelism": user.client_kdf_parallelism,
|
||||
"ResetMasterPassword": false, // TODO: Same as above
|
||||
"scope": scope,
|
||||
"scope": "api",
|
||||
"unofficialServer": true,
|
||||
});
|
||||
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
async fn _organization_api_key_login(data: ConnectData, conn: &mut DbConn, ip: &ClientIp) -> JsonResult {
|
||||
// Get the org via the client_id
|
||||
let client_id = data.client_id.as_ref().unwrap();
|
||||
let org_uuid = match client_id.strip_prefix("organization.") {
|
||||
Some(uuid) => uuid,
|
||||
None => err!("Malformed client_id", format!("IP: {}.", ip.ip)),
|
||||
};
|
||||
let org_api_key = match OrganizationApiKey::find_by_org_uuid(org_uuid, conn).await {
|
||||
Some(org_api_key) => org_api_key,
|
||||
None => err!("Invalid client_id", format!("IP: {}.", ip.ip)),
|
||||
};
|
||||
|
||||
// Check API key.
|
||||
let client_secret = data.client_secret.as_ref().unwrap();
|
||||
if !org_api_key.check_valid_api_key(client_secret) {
|
||||
err!("Incorrect client_secret", format!("IP: {}. Organization: {}.", ip.ip, org_api_key.org_uuid))
|
||||
}
|
||||
|
||||
let claim = generate_organization_api_key_login_claims(org_api_key.uuid, org_api_key.org_uuid);
|
||||
let access_token = crate::auth::encode_jwt(&claim);
|
||||
|
||||
Ok(Json(json!({
|
||||
"access_token": access_token,
|
||||
"expires_in": 3600,
|
||||
"token_type": "Bearer",
|
||||
"scope": "api.organization",
|
||||
"unofficialServer": true,
|
||||
})))
|
||||
}
|
||||
|
||||
/// Retrieves an existing device or creates a new device from ConnectData and the User
|
||||
async fn get_device(data: &ConnectData, conn: &mut DbConn, user: &User) -> (Device, bool) {
|
||||
// On iOS, device_type sends "iOS", on others it sends a number
|
||||
|
@@ -3,6 +3,7 @@ pub mod core;
|
||||
mod icons;
|
||||
mod identity;
|
||||
mod notifications;
|
||||
mod push;
|
||||
mod web;
|
||||
|
||||
use rocket::serde::json::Json;
|
||||
@@ -22,6 +23,10 @@ pub use crate::api::{
|
||||
identity::routes as identity_routes,
|
||||
notifications::routes as notifications_routes,
|
||||
notifications::{start_notification_server, Notify, UpdateType},
|
||||
push::{
|
||||
push_cipher_update, push_folder_update, push_logout, push_send_update, push_user_update, register_push_device,
|
||||
unregister_push_device,
|
||||
},
|
||||
web::catchers as web_catchers,
|
||||
web::routes as web_routes,
|
||||
web::static_files,
|
||||
|
@@ -1,16 +1,15 @@
|
||||
use std::{
|
||||
net::SocketAddr,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
},
|
||||
net::{IpAddr, SocketAddr},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use chrono::NaiveDateTime;
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use chrono::{NaiveDateTime, Utc};
|
||||
use rmpv::Value;
|
||||
use rocket::Route;
|
||||
use rocket::{
|
||||
futures::{SinkExt, StreamExt},
|
||||
Route,
|
||||
};
|
||||
use tokio::{
|
||||
net::{TcpListener, TcpStream},
|
||||
sync::mpsc::Sender,
|
||||
@@ -21,34 +20,130 @@ use tokio_tungstenite::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
api::EmptyResult,
|
||||
db::models::{Cipher, Folder, Send, User},
|
||||
auth::ClientIp,
|
||||
db::{
|
||||
models::{Cipher, Folder, Send as DbSend, User},
|
||||
DbConn,
|
||||
},
|
||||
Error, CONFIG,
|
||||
};
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
static WS_USERS: Lazy<Arc<WebSocketUsers>> = Lazy::new(|| {
|
||||
Arc::new(WebSocketUsers {
|
||||
map: Arc::new(dashmap::DashMap::new()),
|
||||
})
|
||||
});
|
||||
|
||||
use super::{push_cipher_update, push_folder_update, push_logout, push_send_update, push_user_update};
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![websockets_err]
|
||||
routes![websockets_hub]
|
||||
}
|
||||
|
||||
#[get("/hub")]
|
||||
fn websockets_err() -> EmptyResult {
|
||||
static SHOW_WEBSOCKETS_MSG: AtomicBool = AtomicBool::new(true);
|
||||
#[derive(FromForm, Debug)]
|
||||
struct WsAccessToken {
|
||||
access_token: Option<String>,
|
||||
}
|
||||
|
||||
if CONFIG.websocket_enabled()
|
||||
&& SHOW_WEBSOCKETS_MSG.compare_exchange(true, false, Ordering::Relaxed, Ordering::Relaxed).is_ok()
|
||||
{
|
||||
err!(
|
||||
"
|
||||
###########################################################
|
||||
'/notifications/hub' should be proxied to the websocket server or notifications won't work.
|
||||
Go to the Wiki for more info, or disable WebSockets setting WEBSOCKET_ENABLED=false.
|
||||
###########################################################################################\n"
|
||||
)
|
||||
} else {
|
||||
Err(Error::empty())
|
||||
struct WSEntryMapGuard {
|
||||
users: Arc<WebSocketUsers>,
|
||||
user_uuid: String,
|
||||
entry_uuid: uuid::Uuid,
|
||||
addr: IpAddr,
|
||||
}
|
||||
|
||||
impl WSEntryMapGuard {
|
||||
fn new(users: Arc<WebSocketUsers>, user_uuid: String, entry_uuid: uuid::Uuid, addr: IpAddr) -> Self {
|
||||
Self {
|
||||
users,
|
||||
user_uuid,
|
||||
entry_uuid,
|
||||
addr,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for WSEntryMapGuard {
|
||||
fn drop(&mut self) {
|
||||
info!("Closing WS connection from {}", self.addr);
|
||||
if let Some(mut entry) = self.users.map.get_mut(&self.user_uuid) {
|
||||
entry.retain(|(uuid, _)| uuid != &self.entry_uuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/hub?<data..>")]
|
||||
fn websockets_hub<'r>(
|
||||
ws: rocket_ws::WebSocket,
|
||||
data: WsAccessToken,
|
||||
ip: ClientIp,
|
||||
) -> Result<rocket_ws::Stream!['r], Error> {
|
||||
let addr = ip.ip;
|
||||
info!("Accepting Rocket WS connection from {addr}");
|
||||
|
||||
let Some(token) = data.access_token else { err_code!("Invalid claim", 401) };
|
||||
let Ok(claims) = crate::auth::decode_login(&token) else { err_code!("Invalid token", 401) };
|
||||
|
||||
let (mut rx, guard) = {
|
||||
let users = Arc::clone(&WS_USERS);
|
||||
|
||||
// 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(claims.sub.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, claims.sub, entry_uuid, addr))
|
||||
};
|
||||
|
||||
Ok({
|
||||
rocket_ws::Stream! { ws => {
|
||||
let mut ws = ws;
|
||||
let _guard = guard;
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(15));
|
||||
loop {
|
||||
tokio::select! {
|
||||
res = ws.next() => {
|
||||
match res {
|
||||
Some(Ok(message)) => {
|
||||
match message {
|
||||
// Respond to any pings
|
||||
Message::Ping(ping) => yield Message::Pong(ping),
|
||||
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) {
|
||||
yield Message::binary(INITIAL_RESPONSE);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Just echo anything else the client sends
|
||||
_ => yield message,
|
||||
}
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
|
||||
res = rx.recv() => {
|
||||
match res {
|
||||
Some(res) => yield res,
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
|
||||
_ = interval.tick() => yield Message::Ping(create_ping())
|
||||
}
|
||||
}
|
||||
}}
|
||||
})
|
||||
}
|
||||
|
||||
//
|
||||
// Websockets server
|
||||
//
|
||||
@@ -127,8 +222,8 @@ impl WebSocketUsers {
|
||||
async fn send_update(&self, user_uuid: &str, data: &[u8]) {
|
||||
if let Some(user) = self.map.get(user_uuid).map(|v| v.clone()) {
|
||||
for (_, sender) in user.iter() {
|
||||
if sender.send(Message::binary(data)).await.is_err() {
|
||||
// TODO: Delete from map here too?
|
||||
if let Err(e) = sender.send(Message::binary(data)).await {
|
||||
error!("Error sending WS update {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -143,19 +238,33 @@ impl WebSocketUsers {
|
||||
);
|
||||
|
||||
self.send_update(&user.uuid, &data).await;
|
||||
|
||||
if CONFIG.push_enabled() {
|
||||
push_user_update(ut, user);
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send_logout(&self, user: &User, acting_device_uuid: Option<String>) {
|
||||
let data = create_update(
|
||||
vec![("UserId".into(), user.uuid.clone().into()), ("Date".into(), serialize_date(user.updated_at))],
|
||||
UpdateType::LogOut,
|
||||
acting_device_uuid,
|
||||
acting_device_uuid.clone(),
|
||||
);
|
||||
|
||||
self.send_update(&user.uuid, &data).await;
|
||||
|
||||
if CONFIG.push_enabled() {
|
||||
push_logout(user, acting_device_uuid);
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send_folder_update(&self, ut: UpdateType, folder: &Folder, acting_device_uuid: &String) {
|
||||
pub async fn send_folder_update(
|
||||
&self,
|
||||
ut: UpdateType,
|
||||
folder: &Folder,
|
||||
acting_device_uuid: &String,
|
||||
conn: &mut DbConn,
|
||||
) {
|
||||
let data = create_update(
|
||||
vec![
|
||||
("Id".into(), folder.uuid.clone().into()),
|
||||
@@ -167,6 +276,10 @@ impl WebSocketUsers {
|
||||
);
|
||||
|
||||
self.send_update(&folder.user_uuid, &data).await;
|
||||
|
||||
if CONFIG.push_enabled() {
|
||||
push_folder_update(ut, folder, acting_device_uuid, conn).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send_cipher_update(
|
||||
@@ -175,17 +288,29 @@ impl WebSocketUsers {
|
||||
cipher: &Cipher,
|
||||
user_uuids: &[String],
|
||||
acting_device_uuid: &String,
|
||||
collection_uuids: Option<Vec<String>>,
|
||||
conn: &mut DbConn,
|
||||
) {
|
||||
let user_uuid = convert_option(cipher.user_uuid.clone());
|
||||
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.
|
||||
let (user_uuid, collection_uuids, revision_date) = if let Some(collection_uuids) = collection_uuids {
|
||||
(
|
||||
Value::Nil,
|
||||
Value::Array(collection_uuids.into_iter().map(|v| v.into()).collect::<Vec<rmpv::Value>>()),
|
||||
serialize_date(Utc::now().naive_utc()),
|
||||
)
|
||||
} else {
|
||||
(convert_option(cipher.user_uuid.clone()), Value::Nil, serialize_date(cipher.updated_at))
|
||||
};
|
||||
|
||||
let data = create_update(
|
||||
vec![
|
||||
("Id".into(), cipher.uuid.clone().into()),
|
||||
("UserId".into(), user_uuid),
|
||||
("OrganizationId".into(), org_uuid),
|
||||
("CollectionIds".into(), Value::Nil),
|
||||
("RevisionDate".into(), serialize_date(cipher.updated_at)),
|
||||
("CollectionIds".into(), collection_uuids),
|
||||
("RevisionDate".into(), revision_date),
|
||||
],
|
||||
ut,
|
||||
Some(acting_device_uuid.into()),
|
||||
@@ -194,9 +319,20 @@ impl WebSocketUsers {
|
||||
for uuid in user_uuids {
|
||||
self.send_update(uuid, &data).await;
|
||||
}
|
||||
|
||||
if CONFIG.push_enabled() && user_uuids.len() == 1 {
|
||||
push_cipher_update(ut, cipher, acting_device_uuid, conn).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send_send_update(&self, ut: UpdateType, send: &Send, user_uuids: &[String]) {
|
||||
pub async fn send_send_update(
|
||||
&self,
|
||||
ut: UpdateType,
|
||||
send: &DbSend,
|
||||
user_uuids: &[String],
|
||||
acting_device_uuid: &String,
|
||||
conn: &mut DbConn,
|
||||
) {
|
||||
let user_uuid = convert_option(send.user_uuid.clone());
|
||||
|
||||
let data = create_update(
|
||||
@@ -212,6 +348,9 @@ impl WebSocketUsers {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,7 +392,7 @@ fn create_ping() -> Vec<u8> {
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Eq, PartialEq)]
|
||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||
pub enum UpdateType {
|
||||
SyncCipherUpdate = 0,
|
||||
SyncCipherCreate = 1,
|
||||
@@ -280,15 +419,12 @@ pub enum UpdateType {
|
||||
None = 100,
|
||||
}
|
||||
|
||||
pub type Notify<'a> = &'a rocket::State<WebSocketUsers>;
|
||||
|
||||
pub fn start_notification_server() -> WebSocketUsers {
|
||||
let users = WebSocketUsers {
|
||||
map: Arc::new(dashmap::DashMap::new()),
|
||||
};
|
||||
pub type Notify<'a> = &'a rocket::State<Arc<WebSocketUsers>>;
|
||||
|
||||
pub fn start_notification_server() -> Arc<WebSocketUsers> {
|
||||
let users = Arc::clone(&WS_USERS);
|
||||
if CONFIG.websocket_enabled() {
|
||||
let users2 = users.clone();
|
||||
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);
|
||||
@@ -300,7 +436,7 @@ pub fn start_notification_server() -> WebSocketUsers {
|
||||
loop {
|
||||
tokio::select! {
|
||||
Ok((stream, addr)) = listener.accept() => {
|
||||
tokio::spawn(handle_connection(stream, users2.clone(), addr));
|
||||
tokio::spawn(handle_connection(stream, Arc::<WebSocketUsers>::clone(&users2), addr));
|
||||
}
|
||||
|
||||
_ = &mut shutdown_rx => {
|
||||
@@ -316,7 +452,7 @@ pub fn start_notification_server() -> WebSocketUsers {
|
||||
users
|
||||
}
|
||||
|
||||
async fn handle_connection(stream: TcpStream, users: WebSocketUsers, addr: SocketAddr) -> Result<(), Error> {
|
||||
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}");
|
||||
@@ -336,41 +472,39 @@ async fn handle_connection(stream: TcpStream, users: WebSocketUsers, addr: Socke
|
||||
|
||||
let user_uuid = user_uuid.expect("User UUID should be set after the handshake");
|
||||
|
||||
// Add a channel to send messages to this client to the map
|
||||
let entry_uuid = uuid::Uuid::new_v4();
|
||||
let (tx, mut rx) = tokio::sync::mpsc::channel(100);
|
||||
users.map.entry(user_uuid.clone()).or_default().push((entry_uuid, tx));
|
||||
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)) => {
|
||||
// Respond to any pings
|
||||
if let Message::Ping(ping) = message {
|
||||
if stream.send(Message::Pong(ping)).await.is_err() {
|
||||
break;
|
||||
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;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
} else if let Message::Pong(_) = message {
|
||||
/* Ignored */
|
||||
continue;
|
||||
}
|
||||
|
||||
// We should receive an initial message with the protocol and version, and we will reply to it
|
||||
if let Message::Text(ref message) = 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
|
||||
if stream.send(message).await.is_err() {
|
||||
break;
|
||||
// Just echo anything else the client sends
|
||||
_ => stream.send(message).await?,
|
||||
}
|
||||
}
|
||||
_ => break,
|
||||
@@ -379,27 +513,15 @@ async fn handle_connection(stream: TcpStream, users: WebSocketUsers, addr: Socke
|
||||
|
||||
res = rx.recv() => {
|
||||
match res {
|
||||
Some(res) => {
|
||||
if stream.send(res).await.is_err() {
|
||||
break;
|
||||
}
|
||||
},
|
||||
Some(res) => stream.send(res).await?,
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
|
||||
_= interval.tick() => {
|
||||
if stream.send(Message::Ping(create_ping())).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
_ = interval.tick() => stream.send(Message::Ping(create_ping())).await?
|
||||
}
|
||||
}
|
||||
|
||||
info!("Closing WS connection from {addr}");
|
||||
|
||||
// Delete from map
|
||||
users.map.entry(user_uuid).or_default().retain(|(uuid, _)| uuid != &entry_uuid);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
257
src/api/push.rs
Normal file
257
src/api/push.rs
Normal file
@@ -0,0 +1,257 @@
|
||||
use reqwest::header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE};
|
||||
use serde_json::Value;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::{
|
||||
api::{ApiResult, EmptyResult, UpdateType},
|
||||
db::models::{Cipher, Device, Folder, Send, User},
|
||||
util::get_reqwest_client,
|
||||
CONFIG,
|
||||
};
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct AuthPushToken {
|
||||
access_token: String,
|
||||
expires_in: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct LocalAuthPushToken {
|
||||
access_token: String,
|
||||
valid_until: Instant,
|
||||
}
|
||||
|
||||
async fn get_auth_push_token() -> ApiResult<String> {
|
||||
static PUSH_TOKEN: Lazy<RwLock<LocalAuthPushToken>> = Lazy::new(|| {
|
||||
RwLock::new(LocalAuthPushToken {
|
||||
access_token: String::new(),
|
||||
valid_until: Instant::now(),
|
||||
})
|
||||
});
|
||||
let push_token = PUSH_TOKEN.read().await;
|
||||
|
||||
if push_token.valid_until.saturating_duration_since(Instant::now()).as_secs() > 0 {
|
||||
debug!("Auth Push token still valid, no need for a new one");
|
||||
return Ok(push_token.access_token.clone());
|
||||
}
|
||||
drop(push_token); // Drop the read lock now
|
||||
|
||||
let installation_id = CONFIG.push_installation_id();
|
||||
let client_id = format!("installation.{installation_id}");
|
||||
let client_secret = CONFIG.push_installation_key();
|
||||
|
||||
let params = [
|
||||
("grant_type", "client_credentials"),
|
||||
("scope", "api.push"),
|
||||
("client_id", &client_id),
|
||||
("client_secret", &client_secret),
|
||||
];
|
||||
|
||||
let res = match get_reqwest_client().post("https://identity.bitwarden.com/connect/token").form(¶ms).send().await
|
||||
{
|
||||
Ok(r) => r,
|
||||
Err(e) => err!(format!("Error getting push token from bitwarden server: {e}")),
|
||||
};
|
||||
|
||||
let json_pushtoken = match res.json::<AuthPushToken>().await {
|
||||
Ok(r) => r,
|
||||
Err(e) => err!(format!("Unexpected push token received from bitwarden server: {e}")),
|
||||
};
|
||||
|
||||
let mut push_token = PUSH_TOKEN.write().await;
|
||||
push_token.valid_until = Instant::now()
|
||||
.checked_add(Duration::new((json_pushtoken.expires_in / 2) as u64, 0)) // Token valid for half the specified time
|
||||
.unwrap();
|
||||
|
||||
push_token.access_token = json_pushtoken.access_token;
|
||||
|
||||
debug!("Token still valid for {}", push_token.valid_until.saturating_duration_since(Instant::now()).as_secs());
|
||||
Ok(push_token.access_token.clone())
|
||||
}
|
||||
|
||||
pub async fn register_push_device(user_uuid: String, device: Device) -> EmptyResult {
|
||||
if !CONFIG.push_enabled() {
|
||||
return Ok(());
|
||||
}
|
||||
let auth_push_token = get_auth_push_token().await?;
|
||||
|
||||
//Needed to register a device for push to bitwarden :
|
||||
let data = json!({
|
||||
"userId": user_uuid,
|
||||
"deviceId": device.push_uuid,
|
||||
"identifier": device.uuid,
|
||||
"type": device.atype,
|
||||
"pushToken": device.push_token
|
||||
});
|
||||
|
||||
let auth_header = format!("Bearer {}", &auth_push_token);
|
||||
|
||||
get_reqwest_client()
|
||||
.post(CONFIG.push_relay_uri() + "/push/register")
|
||||
.header(CONTENT_TYPE, "application/json")
|
||||
.header(ACCEPT, "application/json")
|
||||
.header(AUTHORIZATION, auth_header)
|
||||
.json(&data)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn unregister_push_device(uuid: String) -> EmptyResult {
|
||||
if !CONFIG.push_enabled() {
|
||||
return Ok(());
|
||||
}
|
||||
let auth_push_token = get_auth_push_token().await?;
|
||||
|
||||
let auth_header = format!("Bearer {}", &auth_push_token);
|
||||
|
||||
match get_reqwest_client()
|
||||
.delete(CONFIG.push_relay_uri() + "/push/" + &uuid)
|
||||
.header(AUTHORIZATION, auth_header)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(r) => r,
|
||||
Err(e) => err!(format!("An error occured during device unregistration: {e}")),
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn push_cipher_update(
|
||||
ut: UpdateType,
|
||||
cipher: &Cipher,
|
||||
acting_device_uuid: &String,
|
||||
conn: &mut crate::db::DbConn,
|
||||
) {
|
||||
// We shouldn't send a push notification on cipher update if the cipher belongs to an organization, this isn't implemented in the upstream server too.
|
||||
if cipher.organization_uuid.is_some() {
|
||||
return;
|
||||
};
|
||||
let user_uuid = match &cipher.user_uuid {
|
||||
Some(c) => c,
|
||||
None => {
|
||||
debug!("Cipher has no uuid");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if Device::check_user_has_push_device(user_uuid, conn).await {
|
||||
send_to_push_relay(json!({
|
||||
"userId": user_uuid,
|
||||
"organizationId": (),
|
||||
"deviceId": acting_device_uuid,
|
||||
"identifier": acting_device_uuid,
|
||||
"type": ut as i32,
|
||||
"payload": {
|
||||
"id": cipher.uuid,
|
||||
"userId": cipher.user_uuid,
|
||||
"organizationId": (),
|
||||
"revisionDate": cipher.updated_at
|
||||
}
|
||||
}))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_logout(user: &User, acting_device_uuid: Option<String>) {
|
||||
let acting_device_uuid: Value = acting_device_uuid.map(|v| v.into()).unwrap_or_else(|| Value::Null);
|
||||
|
||||
tokio::task::spawn(send_to_push_relay(json!({
|
||||
"userId": user.uuid,
|
||||
"organizationId": (),
|
||||
"deviceId": acting_device_uuid,
|
||||
"identifier": acting_device_uuid,
|
||||
"type": UpdateType::LogOut as i32,
|
||||
"payload": {
|
||||
"userId": user.uuid,
|
||||
"date": user.updated_at
|
||||
}
|
||||
})));
|
||||
}
|
||||
|
||||
pub fn push_user_update(ut: UpdateType, user: &User) {
|
||||
tokio::task::spawn(send_to_push_relay(json!({
|
||||
"userId": user.uuid,
|
||||
"organizationId": (),
|
||||
"deviceId": (),
|
||||
"identifier": (),
|
||||
"type": ut as i32,
|
||||
"payload": {
|
||||
"userId": user.uuid,
|
||||
"date": user.updated_at
|
||||
}
|
||||
})));
|
||||
}
|
||||
|
||||
pub async fn push_folder_update(
|
||||
ut: UpdateType,
|
||||
folder: &Folder,
|
||||
acting_device_uuid: &String,
|
||||
conn: &mut crate::db::DbConn,
|
||||
) {
|
||||
if Device::check_user_has_push_device(&folder.user_uuid, conn).await {
|
||||
tokio::task::spawn(send_to_push_relay(json!({
|
||||
"userId": folder.user_uuid,
|
||||
"organizationId": (),
|
||||
"deviceId": acting_device_uuid,
|
||||
"identifier": acting_device_uuid,
|
||||
"type": ut as i32,
|
||||
"payload": {
|
||||
"id": folder.uuid,
|
||||
"userId": folder.user_uuid,
|
||||
"revisionDate": folder.updated_at
|
||||
}
|
||||
})));
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn push_send_update(ut: UpdateType, send: &Send, acting_device_uuid: &String, conn: &mut crate::db::DbConn) {
|
||||
if let Some(s) = &send.user_uuid {
|
||||
if Device::check_user_has_push_device(s, conn).await {
|
||||
tokio::task::spawn(send_to_push_relay(json!({
|
||||
"userId": send.user_uuid,
|
||||
"organizationId": (),
|
||||
"deviceId": acting_device_uuid,
|
||||
"identifier": acting_device_uuid,
|
||||
"type": ut as i32,
|
||||
"payload": {
|
||||
"id": send.uuid,
|
||||
"userId": send.user_uuid,
|
||||
"revisionDate": send.revision_date
|
||||
}
|
||||
})));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_to_push_relay(notification_data: Value) {
|
||||
if !CONFIG.push_enabled() {
|
||||
return;
|
||||
}
|
||||
|
||||
let auth_push_token = match get_auth_push_token().await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
debug!("Could not get the auth push token: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let auth_header = format!("Bearer {}", &auth_push_token);
|
||||
|
||||
if let Err(e) = get_reqwest_client()
|
||||
.post(CONFIG.push_relay_uri() + "/push/send")
|
||||
.header(ACCEPT, "application/json")
|
||||
.header(CONTENT_TYPE, "application/json")
|
||||
.header(AUTHORIZATION, &auth_header)
|
||||
.json(¬ification_data)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
error!("An error occured while sending a send update to the push relay: {}", e);
|
||||
};
|
||||
}
|
@@ -5,6 +5,7 @@ use serde_json::Value;
|
||||
|
||||
use crate::{
|
||||
api::{core::now, ApiResult, EmptyResult},
|
||||
auth::decode_file_download,
|
||||
error::Error,
|
||||
util::{Cached, SafeString},
|
||||
CONFIG,
|
||||
@@ -91,8 +92,13 @@ async fn web_files(p: PathBuf) -> Cached<Option<NamedFile>> {
|
||||
Cached::long(NamedFile::open(Path::new(&CONFIG.web_vault_folder()).join(p)).await.ok(), true)
|
||||
}
|
||||
|
||||
#[get("/attachments/<uuid>/<file_id>")]
|
||||
async fn attachments(uuid: SafeString, file_id: SafeString) -> Option<NamedFile> {
|
||||
#[get("/attachments/<uuid>/<file_id>?<token>")]
|
||||
async fn attachments(uuid: SafeString, file_id: SafeString, token: String) -> Option<NamedFile> {
|
||||
let Ok(claims) = dbg!(decode_file_download(&token)) else { return None };
|
||||
if claims.sub != *uuid || claims.file_id != *file_id {
|
||||
return None;
|
||||
}
|
||||
|
||||
NamedFile::open(Path::new(&CONFIG.attachments_folder()).join(uuid).join(file_id)).await.ok()
|
||||
}
|
||||
|
||||
@@ -111,8 +117,8 @@ fn alive_head(_conn: DbConn) -> EmptyResult {
|
||||
}
|
||||
|
||||
#[get("/vw_static/<filename>")]
|
||||
pub fn static_files(filename: String) -> Result<(ContentType, &'static [u8]), Error> {
|
||||
match filename.as_ref() {
|
||||
pub fn static_files(filename: &str) -> Result<(ContentType, &'static [u8]), Error> {
|
||||
match filename {
|
||||
"404.png" => Ok((ContentType::PNG, include_bytes!("../static/images/404.png"))),
|
||||
"mail-github.png" => Ok((ContentType::PNG, include_bytes!("../static/images/mail-github.png"))),
|
||||
"logo-gray.png" => Ok((ContentType::PNG, include_bytes!("../static/images/logo-gray.png"))),
|
||||
@@ -136,8 +142,8 @@ pub fn static_files(filename: String) -> Result<(ContentType, &'static [u8]), Er
|
||||
"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.6.3.slim.js" => {
|
||||
Ok((ContentType::JavaScript, include_bytes!("../static/scripts/jquery-3.6.3.slim.js")))
|
||||
"jquery-3.6.4.slim.js" => {
|
||||
Ok((ContentType::JavaScript, include_bytes!("../static/scripts/jquery-3.6.4.slim.js")))
|
||||
}
|
||||
_ => err!(format!("Static file not found: {filename}")),
|
||||
}
|
||||
|
123
src/auth.rs
123
src/auth.rs
@@ -23,18 +23,17 @@ static JWT_DELETE_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|delete", CONFI
|
||||
static JWT_VERIFYEMAIL_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|verifyemail", CONFIG.domain_origin()));
|
||||
static JWT_ADMIN_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|admin", CONFIG.domain_origin()));
|
||||
static JWT_SEND_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|send", CONFIG.domain_origin()));
|
||||
static JWT_ORG_API_KEY_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|api.organization", CONFIG.domain_origin()));
|
||||
static JWT_FILE_DOWNLOAD_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|file_download", CONFIG.domain_origin()));
|
||||
|
||||
static PRIVATE_RSA_KEY_VEC: Lazy<Vec<u8>> = Lazy::new(|| {
|
||||
std::fs::read(CONFIG.private_rsa_key()).unwrap_or_else(|e| panic!("Error loading private RSA Key.\n{e}"))
|
||||
});
|
||||
static PRIVATE_RSA_KEY: Lazy<EncodingKey> = Lazy::new(|| {
|
||||
EncodingKey::from_rsa_pem(&PRIVATE_RSA_KEY_VEC).unwrap_or_else(|e| panic!("Error decoding private RSA Key.\n{e}"))
|
||||
});
|
||||
static PUBLIC_RSA_KEY_VEC: Lazy<Vec<u8>> = Lazy::new(|| {
|
||||
std::fs::read(CONFIG.public_rsa_key()).unwrap_or_else(|e| panic!("Error loading public RSA Key.\n{e}"))
|
||||
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(|| {
|
||||
DecodingKey::from_rsa_pem(&PUBLIC_RSA_KEY_VEC).unwrap_or_else(|e| panic!("Error decoding public RSA Key.\n{e}"))
|
||||
let key = std::fs::read(CONFIG.public_rsa_key()).unwrap_or_else(|e| panic!("Error loading public RSA Key. \n{e}"));
|
||||
DecodingKey::from_rsa_pem(&key).unwrap_or_else(|e| panic!("Error decoding public RSA Key.\n{e}"))
|
||||
});
|
||||
|
||||
pub fn load_keys() {
|
||||
@@ -96,6 +95,14 @@ pub fn decode_send(token: &str) -> Result<BasicJwtClaims, Error> {
|
||||
decode_jwt(token, JWT_SEND_ISSUER.to_string())
|
||||
}
|
||||
|
||||
pub fn decode_api_org(token: &str) -> Result<OrgApiKeyLoginJwtClaims, Error> {
|
||||
decode_jwt(token, JWT_ORG_API_KEY_ISSUER.to_string())
|
||||
}
|
||||
|
||||
pub fn decode_file_download(token: &str) -> Result<FileDownloadClaims, Error> {
|
||||
decode_jwt(token, JWT_FILE_DOWNLOAD_ISSUER.to_string())
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct LoginJwtClaims {
|
||||
// Not before
|
||||
@@ -203,6 +210,60 @@ pub fn generate_emergency_access_invite_claims(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct OrgApiKeyLoginJwtClaims {
|
||||
// Not before
|
||||
pub nbf: i64,
|
||||
// Expiration time
|
||||
pub exp: i64,
|
||||
// Issuer
|
||||
pub iss: String,
|
||||
// Subject
|
||||
pub sub: String,
|
||||
|
||||
pub client_id: String,
|
||||
pub client_sub: String,
|
||||
pub scope: Vec<String>,
|
||||
}
|
||||
|
||||
pub fn generate_organization_api_key_login_claims(uuid: String, org_id: String) -> OrgApiKeyLoginJwtClaims {
|
||||
let time_now = Utc::now().naive_utc();
|
||||
OrgApiKeyLoginJwtClaims {
|
||||
nbf: time_now.timestamp(),
|
||||
exp: (time_now + Duration::hours(1)).timestamp(),
|
||||
iss: JWT_ORG_API_KEY_ISSUER.to_string(),
|
||||
sub: uuid,
|
||||
client_id: format!("organization.{org_id}"),
|
||||
client_sub: org_id,
|
||||
scope: vec!["api.organization".into()],
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct FileDownloadClaims {
|
||||
// Not before
|
||||
pub nbf: i64,
|
||||
// Expiration time
|
||||
pub exp: i64,
|
||||
// Issuer
|
||||
pub iss: String,
|
||||
// Subject
|
||||
pub sub: String,
|
||||
|
||||
pub file_id: String,
|
||||
}
|
||||
|
||||
pub fn generate_file_download_claims(uuid: String, file_id: String) -> FileDownloadClaims {
|
||||
let time_now = Utc::now().naive_utc();
|
||||
FileDownloadClaims {
|
||||
nbf: time_now.timestamp(),
|
||||
exp: (time_now + Duration::minutes(5)).timestamp(),
|
||||
iss: JWT_FILE_DOWNLOAD_ISSUER.to_string(),
|
||||
sub: uuid,
|
||||
file_id,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct BasicJwtClaims {
|
||||
// Not before
|
||||
@@ -446,32 +507,34 @@ pub struct OrgHeaders {
|
||||
pub ip: ClientIp,
|
||||
}
|
||||
|
||||
// org_id is usually the second path param ("/organizations/<org_id>"),
|
||||
// but there are cases where it is a query value.
|
||||
// First check the path, if this is not a valid uuid, try the query values.
|
||||
fn get_org_id(request: &Request<'_>) -> Option<String> {
|
||||
if let Some(Ok(org_id)) = request.param::<String>(1) {
|
||||
if uuid::Uuid::parse_str(&org_id).is_ok() {
|
||||
return Some(org_id);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(Ok(org_id)) = request.query_value::<String>("organizationId") {
|
||||
if uuid::Uuid::parse_str(&org_id).is_ok() {
|
||||
return Some(org_id);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[rocket::async_trait]
|
||||
impl<'r> FromRequest<'r> for OrgHeaders {
|
||||
type Error = &'static str;
|
||||
|
||||
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||
let headers = try_outcome!(Headers::from_request(request).await);
|
||||
match get_org_id(request) {
|
||||
|
||||
// org_id is usually the second path param ("/organizations/<org_id>"),
|
||||
// but there are cases where it is a query value.
|
||||
// First check the path, if this is not a valid uuid, try the query values.
|
||||
let url_org_id: Option<&str> = {
|
||||
let mut url_org_id = None;
|
||||
if let Some(Ok(org_id)) = request.param::<&str>(1) {
|
||||
if uuid::Uuid::parse_str(org_id).is_ok() {
|
||||
url_org_id = Some(org_id);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(Ok(org_id)) = request.query_value::<&str>("organizationId") {
|
||||
if uuid::Uuid::parse_str(org_id).is_ok() {
|
||||
url_org_id = Some(org_id);
|
||||
}
|
||||
}
|
||||
|
||||
url_org_id
|
||||
};
|
||||
|
||||
match url_org_id {
|
||||
Some(org_id) => {
|
||||
let mut conn = match DbConn::from_request(request).await {
|
||||
Outcome::Success(conn) => conn,
|
||||
@@ -479,7 +542,7 @@ impl<'r> FromRequest<'r> for OrgHeaders {
|
||||
};
|
||||
|
||||
let user = headers.user;
|
||||
let org_user = match UserOrganization::find_by_user_and_org(&user.uuid, &org_id, &mut conn).await {
|
||||
let org_user = match UserOrganization::find_by_user_and_org(&user.uuid, org_id, &mut conn).await {
|
||||
Some(user) => {
|
||||
if user.status == UserOrgStatus::Confirmed as i32 {
|
||||
user
|
||||
@@ -503,7 +566,7 @@ impl<'r> FromRequest<'r> for OrgHeaders {
|
||||
}
|
||||
},
|
||||
org_user,
|
||||
org_id,
|
||||
org_id: String::from(org_id),
|
||||
ip: headers.ip,
|
||||
})
|
||||
}
|
||||
|
@@ -377,6 +377,16 @@ make_config! {
|
||||
/// Websocket port
|
||||
websocket_port: u16, false, def, 3012;
|
||||
},
|
||||
push {
|
||||
/// Enable push notifications
|
||||
push_enabled: bool, false, def, false;
|
||||
/// Push relay base uri
|
||||
push_relay_uri: String, false, def, "https://push.bitwarden.com".to_string();
|
||||
/// Installation id |> The installation id from https://bitwarden.com/host
|
||||
push_installation_id: Pass, false, def, String::new();
|
||||
/// Installation key |> The installation key from https://bitwarden.com/host
|
||||
push_installation_key: Pass, false, def, String::new();
|
||||
},
|
||||
jobs {
|
||||
/// Job scheduler poll interval |> How often the job scheduler thread checks for jobs to run.
|
||||
/// Set to 0 to globally disable scheduled jobs.
|
||||
@@ -476,7 +486,7 @@ make_config! {
|
||||
/// provides unauthenticated access to potentially sensitive data.
|
||||
show_password_hint: bool, true, def, false;
|
||||
|
||||
/// Admin page token |> The token used to authenticate in this very same page. Changing it here won't deauthorize the current session
|
||||
/// Admin token/Argon2 PHC |> The plain text token or Argon2 PHC string used to authenticate in this very same page. Changing it here will not deauthorize the current session!
|
||||
admin_token: Pass, true, option;
|
||||
|
||||
/// Invitation organization name |> Name shown in the invitation emails that don't come from a specific organization
|
||||
@@ -603,7 +613,7 @@ make_config! {
|
||||
/// Global Duo settings (Note that users can override them)
|
||||
duo: _enable_duo {
|
||||
/// Enabled
|
||||
_enable_duo: bool, true, def, false;
|
||||
_enable_duo: bool, true, def, true;
|
||||
/// Integration Key
|
||||
duo_ikey: String, true, option;
|
||||
/// Secret Key
|
||||
@@ -724,6 +734,17 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.push_enabled && (cfg.push_installation_id == String::new() || cfg.push_installation_key == String::new()) {
|
||||
err!(
|
||||
"Misconfigured Push Notification service\n\
|
||||
########################################################################################\n\
|
||||
# It looks like you enabled Push Notification feature, but didn't configure it #\n\
|
||||
# properly. Make sure the installation id and key from https://bitwarden.com/host are #\n\
|
||||
# added to your configuration. #\n\
|
||||
########################################################################################\n"
|
||||
)
|
||||
}
|
||||
|
||||
if cfg._enable_duo
|
||||
&& (cfg.duo_host.is_some() || cfg.duo_ikey.is_some() || cfg.duo_skey.is_some())
|
||||
&& !(cfg.duo_host.is_some() && cfg.duo_ikey.is_some() && cfg.duo_skey.is_some())
|
||||
|
@@ -35,7 +35,8 @@ impl Attachment {
|
||||
}
|
||||
|
||||
pub fn get_url(&self, host: &str) -> String {
|
||||
format!("{}/attachments/{}/{}", host, self.cipher_uuid, self.id)
|
||||
let token = encode_jwt(&generate_file_download_claims(self.cipher_uuid.clone(), self.id.clone()));
|
||||
format!("{}/attachments/{}/{}?token={}", host, self.cipher_uuid, self.id, token)
|
||||
}
|
||||
|
||||
pub fn to_json(&self, host: &str) -> Value {
|
||||
@@ -51,6 +52,7 @@ impl Attachment {
|
||||
}
|
||||
}
|
||||
|
||||
use crate::auth::{encode_jwt, generate_file_download_claims};
|
||||
use crate::db::DbConn;
|
||||
|
||||
use crate::api::EmptyResult;
|
||||
|
@@ -27,7 +27,8 @@ db_object! {
|
||||
Login = 1,
|
||||
SecureNote = 2,
|
||||
Card = 3,
|
||||
Identity = 4
|
||||
Identity = 4,
|
||||
Fido2key = 5
|
||||
*/
|
||||
pub atype: i32,
|
||||
pub name: String,
|
||||
@@ -223,6 +224,7 @@ impl Cipher {
|
||||
"SecureNote": null,
|
||||
"Card": null,
|
||||
"Identity": null,
|
||||
"Fido2Key": null,
|
||||
});
|
||||
|
||||
// These values are only needed for user/default syncs
|
||||
@@ -251,6 +253,7 @@ impl Cipher {
|
||||
2 => "SecureNote",
|
||||
3 => "Card",
|
||||
4 => "Identity",
|
||||
5 => "Fido2Key",
|
||||
_ => panic!("Wrong type"),
|
||||
};
|
||||
|
||||
|
@@ -10,6 +10,7 @@ db_object! {
|
||||
pub uuid: String,
|
||||
pub org_uuid: String,
|
||||
pub name: String,
|
||||
pub external_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Identifiable, Queryable, Insertable)]
|
||||
@@ -33,18 +34,21 @@ db_object! {
|
||||
|
||||
/// Local methods
|
||||
impl Collection {
|
||||
pub fn new(org_uuid: String, name: String) -> Self {
|
||||
Self {
|
||||
pub fn new(org_uuid: String, name: String, external_id: Option<String>) -> Self {
|
||||
let mut new_model = Self {
|
||||
uuid: crate::util::get_uuid(),
|
||||
|
||||
org_uuid,
|
||||
name,
|
||||
}
|
||||
external_id: None,
|
||||
};
|
||||
|
||||
new_model.set_external_id(external_id);
|
||||
new_model
|
||||
}
|
||||
|
||||
pub fn to_json(&self) -> Value {
|
||||
json!({
|
||||
"ExternalId": null, // Not support by us
|
||||
"ExternalId": self.external_id,
|
||||
"Id": self.uuid,
|
||||
"OrganizationId": self.org_uuid,
|
||||
"Name": self.name,
|
||||
@@ -52,6 +56,21 @@ impl Collection {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_external_id(&mut self, external_id: Option<String>) {
|
||||
//Check if external id is empty. We don't want to have
|
||||
//empty strings in the database
|
||||
match external_id {
|
||||
Some(external_id) => {
|
||||
if external_id.is_empty() {
|
||||
self.external_id = None;
|
||||
} else {
|
||||
self.external_id = Some(external_id)
|
||||
}
|
||||
}
|
||||
None => self.external_id = None,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn to_json_details(
|
||||
&self,
|
||||
user_uuid: &str,
|
||||
|
@@ -1,6 +1,6 @@
|
||||
use chrono::{NaiveDateTime, Utc};
|
||||
|
||||
use crate::CONFIG;
|
||||
use crate::{crypto, CONFIG};
|
||||
|
||||
db_object! {
|
||||
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
|
||||
@@ -15,7 +15,8 @@ db_object! {
|
||||
pub user_uuid: String,
|
||||
|
||||
pub name: String,
|
||||
pub atype: i32, // https://github.com/bitwarden/server/blob/master/src/Core/Enums/DeviceType.cs
|
||||
pub atype: i32, // https://github.com/bitwarden/server/blob/master/src/Core/Enums/DeviceType.cs
|
||||
pub push_uuid: Option<String>,
|
||||
pub push_token: Option<String>,
|
||||
|
||||
pub refresh_token: String,
|
||||
@@ -38,6 +39,7 @@ impl Device {
|
||||
name,
|
||||
atype,
|
||||
|
||||
push_uuid: None,
|
||||
push_token: None,
|
||||
refresh_token: String::new(),
|
||||
twofactor_remember: None,
|
||||
@@ -45,9 +47,7 @@ impl Device {
|
||||
}
|
||||
|
||||
pub fn refresh_twofactor_remember(&mut self) -> String {
|
||||
use crate::crypto;
|
||||
use data_encoding::BASE64;
|
||||
|
||||
let twofactor_remember = crypto::encode_random_bytes::<180>(BASE64);
|
||||
self.twofactor_remember = Some(twofactor_remember.clone());
|
||||
|
||||
@@ -66,9 +66,7 @@ impl Device {
|
||||
) -> (String, i64) {
|
||||
// If there is no refresh token, we create one
|
||||
if self.refresh_token.is_empty() {
|
||||
use crate::crypto;
|
||||
use data_encoding::BASE64URL;
|
||||
|
||||
self.refresh_token = crypto::encode_random_bytes::<64>(BASE64URL);
|
||||
}
|
||||
|
||||
@@ -155,6 +153,35 @@ impl Device {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn find_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
|
||||
db_run! { conn: {
|
||||
devices::table
|
||||
.filter(devices::user_uuid.eq(user_uuid))
|
||||
.load::<DeviceDb>(conn)
|
||||
.expect("Error loading devices")
|
||||
.from_db()
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn find_by_uuid(uuid: &str, conn: &mut DbConn) -> Option<Self> {
|
||||
db_run! { conn: {
|
||||
devices::table
|
||||
.filter(devices::uuid.eq(uuid))
|
||||
.first::<DeviceDb>(conn)
|
||||
.ok()
|
||||
.from_db()
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn clear_push_token_by_uuid(uuid: &str, conn: &mut DbConn) -> EmptyResult {
|
||||
db_run! { conn: {
|
||||
diesel::update(devices::table)
|
||||
.filter(devices::uuid.eq(uuid))
|
||||
.set(devices::push_token.eq::<Option<String>>(None))
|
||||
.execute(conn)
|
||||
.map_res("Error removing push token")
|
||||
}}
|
||||
}
|
||||
pub async fn find_by_refresh_token(refresh_token: &str, conn: &mut DbConn) -> Option<Self> {
|
||||
db_run! { conn: {
|
||||
devices::table
|
||||
@@ -175,4 +202,26 @@ impl Device {
|
||||
.from_db()
|
||||
}}
|
||||
}
|
||||
pub async fn find_push_devices_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
|
||||
db_run! { conn: {
|
||||
devices::table
|
||||
.filter(devices::user_uuid.eq(user_uuid))
|
||||
.filter(devices::push_token.is_not_null())
|
||||
.load::<DeviceDb>(conn)
|
||||
.expect("Error loading push devices")
|
||||
.from_db()
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn check_user_has_push_device(user_uuid: &str, conn: &mut DbConn) -> bool {
|
||||
db_run! { conn: {
|
||||
devices::table
|
||||
.filter(devices::user_uuid.eq(user_uuid))
|
||||
.filter(devices::push_token.is_not_null())
|
||||
.count()
|
||||
.first::<i64>(conn)
|
||||
.ok()
|
||||
.unwrap_or(0) != 0
|
||||
}}
|
||||
}
|
||||
}
|
||||
|
@@ -10,7 +10,7 @@ db_object! {
|
||||
pub organizations_uuid: String,
|
||||
pub name: String,
|
||||
pub access_all: bool,
|
||||
external_id: Option<String>,
|
||||
pub external_id: Option<String>,
|
||||
pub creation_date: NaiveDateTime,
|
||||
pub revision_date: NaiveDateTime,
|
||||
}
|
||||
@@ -107,10 +107,6 @@ impl Group {
|
||||
None => self.external_id = None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_external_id(&self) -> Option<String> {
|
||||
self.external_id.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl CollectionGroup {
|
||||
@@ -214,6 +210,15 @@ impl Group {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn find_by_external_id(id: &str, conn: &mut DbConn) -> Option<Self> {
|
||||
db_run! { conn: {
|
||||
groups::table
|
||||
.filter(groups::external_id.eq(id))
|
||||
.first::<GroupDb>(conn)
|
||||
.ok()
|
||||
.from_db()
|
||||
}}
|
||||
}
|
||||
//Returns all organizations the user has full access to
|
||||
pub async fn gather_user_organizations_full_access(user_uuid: &str, conn: &mut DbConn) -> Vec<String> {
|
||||
db_run! { conn: {
|
||||
|
@@ -24,7 +24,7 @@ pub use self::favorite::Favorite;
|
||||
pub use self::folder::{Folder, FolderCipher};
|
||||
pub use self::group::{CollectionGroup, Group, GroupUser};
|
||||
pub use self::org_policy::{OrgPolicy, OrgPolicyErr, OrgPolicyType};
|
||||
pub use self::organization::{Organization, UserOrgStatus, UserOrgType, UserOrganization};
|
||||
pub use self::organization::{Organization, OrganizationApiKey, UserOrgStatus, UserOrgType, UserOrganization};
|
||||
pub use self::send::{Send, SendType};
|
||||
pub use self::two_factor::{TwoFactor, TwoFactorType};
|
||||
pub use self::two_factor_incomplete::TwoFactorIncomplete;
|
||||
|
@@ -309,7 +309,7 @@ impl OrgPolicy {
|
||||
match OrgPolicy::find_by_org_and_type(org_uuid, OrgPolicyType::ResetPassword, conn).await {
|
||||
Some(policy) => match serde_json::from_str::<UpCase<ResetPasswordDataModel>>(&policy.data) {
|
||||
Ok(opts) => {
|
||||
return opts.data.AutoEnrollEnabled;
|
||||
return policy.enabled && opts.data.AutoEnrollEnabled;
|
||||
}
|
||||
_ => error!("Failed to deserialize ResetPasswordDataModel: {}", policy.data),
|
||||
},
|
||||
|
@@ -1,3 +1,4 @@
|
||||
use chrono::{NaiveDateTime, Utc};
|
||||
use num_traits::FromPrimitive;
|
||||
use serde_json::Value;
|
||||
use std::cmp::Ordering;
|
||||
@@ -31,6 +32,17 @@ db_object! {
|
||||
pub atype: i32,
|
||||
pub reset_password_key: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
|
||||
#[diesel(table_name = organization_api_key)]
|
||||
#[diesel(primary_key(uuid, org_uuid))]
|
||||
pub struct OrganizationApiKey {
|
||||
pub uuid: String,
|
||||
pub org_uuid: String,
|
||||
pub atype: i32,
|
||||
pub api_key: String,
|
||||
pub revision_date: NaiveDateTime,
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/bitwarden/server/blob/b86a04cef9f1e1b82cf18e49fc94e017c641130c/src/Core/Enums/OrganizationUserStatusType.cs
|
||||
@@ -157,7 +169,7 @@ impl Organization {
|
||||
"UseSso": false, // Not supported
|
||||
// "UseKeyConnector": false, // Not supported
|
||||
"SelfHost": true,
|
||||
"UseApi": false, // Not supported
|
||||
"UseApi": true,
|
||||
"HasPublicAndPrivateKeys": self.private_key.is_some() && self.public_key.is_some(),
|
||||
"UseResetPassword": CONFIG.mail_enabled(),
|
||||
|
||||
@@ -212,6 +224,23 @@ impl UserOrganization {
|
||||
}
|
||||
}
|
||||
|
||||
impl OrganizationApiKey {
|
||||
pub fn new(org_uuid: String, api_key: String) -> Self {
|
||||
Self {
|
||||
uuid: crate::util::get_uuid(),
|
||||
|
||||
org_uuid,
|
||||
atype: 0, // Type 0 is the default and only type we support currently
|
||||
api_key,
|
||||
revision_date: Utc::now().naive_utc(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_valid_api_key(&self, api_key: &str) -> bool {
|
||||
crate::crypto::ct_eq(&self.api_key, api_key)
|
||||
}
|
||||
}
|
||||
|
||||
use crate::db::DbConn;
|
||||
|
||||
use crate::api::EmptyResult;
|
||||
@@ -311,7 +340,7 @@ impl UserOrganization {
|
||||
"UseTotp": true,
|
||||
// "UseScim": false, // Not supported (Not AGPLv3 Licensed)
|
||||
"UsePolicies": true,
|
||||
"UseApi": false, // Not supported
|
||||
"UseApi": true,
|
||||
"SelfHost": true,
|
||||
"HasPublicAndPrivateKeys": org.private_key.is_some() && org.public_key.is_some(),
|
||||
"ResetPasswordEnrolled": self.reset_password_key.is_some(),
|
||||
@@ -481,7 +510,7 @@ impl UserOrganization {
|
||||
.set(UserOrganizationDb::to_db(self))
|
||||
.execute(conn)
|
||||
.map_res("Error adding user to organization")
|
||||
}
|
||||
},
|
||||
Err(e) => Err(e.into()),
|
||||
}.map_res("Error adding user to organization")
|
||||
}
|
||||
@@ -750,6 +779,50 @@ impl UserOrganization {
|
||||
}
|
||||
}
|
||||
|
||||
impl OrganizationApiKey {
|
||||
pub async fn save(&self, conn: &DbConn) -> EmptyResult {
|
||||
db_run! { conn:
|
||||
sqlite, mysql {
|
||||
match diesel::replace_into(organization_api_key::table)
|
||||
.values(OrganizationApiKeyDb::to_db(self))
|
||||
.execute(conn)
|
||||
{
|
||||
Ok(_) => Ok(()),
|
||||
// Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first.
|
||||
Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => {
|
||||
diesel::update(organization_api_key::table)
|
||||
.filter(organization_api_key::uuid.eq(&self.uuid))
|
||||
.set(OrganizationApiKeyDb::to_db(self))
|
||||
.execute(conn)
|
||||
.map_res("Error saving organization")
|
||||
}
|
||||
Err(e) => Err(e.into()),
|
||||
}.map_res("Error saving organization")
|
||||
|
||||
}
|
||||
postgresql {
|
||||
let value = OrganizationApiKeyDb::to_db(self);
|
||||
diesel::insert_into(organization_api_key::table)
|
||||
.values(&value)
|
||||
.on_conflict(organization_api_key::uuid)
|
||||
.do_update()
|
||||
.set(&value)
|
||||
.execute(conn)
|
||||
.map_res("Error saving organization")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn find_by_org_uuid(org_uuid: &str, conn: &DbConn) -> Option<Self> {
|
||||
db_run! { conn: {
|
||||
organization_api_key::table
|
||||
.filter(organization_api_key::org_uuid.eq(org_uuid))
|
||||
.first::<OrganizationApiKeyDb>(conn)
|
||||
.ok().from_db()
|
||||
}}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
@@ -50,6 +50,8 @@ db_object! {
|
||||
pub api_key: Option<String>,
|
||||
|
||||
pub avatar_color: Option<String>,
|
||||
|
||||
pub external_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Identifiable, Queryable, Insertable)]
|
||||
@@ -126,6 +128,8 @@ impl User {
|
||||
api_key: None,
|
||||
|
||||
avatar_color: None,
|
||||
|
||||
external_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,6 +154,18 @@ impl User {
|
||||
matches!(self.api_key, Some(ref api_key) if crate::crypto::ct_eq(api_key, key))
|
||||
}
|
||||
|
||||
pub fn set_external_id(&mut self, external_id: Option<String>) {
|
||||
//Check if external id is empty. We don't want to have
|
||||
//empty strings in the database
|
||||
let mut ext_id: Option<String> = None;
|
||||
if let Some(external_id) = external_id {
|
||||
if !external_id.is_empty() {
|
||||
ext_id = Some(external_id);
|
||||
}
|
||||
}
|
||||
self.external_id = ext_id;
|
||||
}
|
||||
|
||||
/// Set the password hash generated
|
||||
/// And resets the security_stamp. Based upon the allow_next_route the security_stamp will be different.
|
||||
///
|
||||
@@ -376,6 +392,12 @@ impl User {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn find_by_external_id(id: &str, conn: &mut DbConn) -> Option<Self> {
|
||||
db_run! {conn: {
|
||||
users::table.filter(users::external_id.eq(id)).first::<UserDb>(conn).ok().from_db()
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn get_all(conn: &mut DbConn) -> Vec<Self> {
|
||||
db_run! {conn: {
|
||||
users::table.load::<UserDb>(conn).expect("Error loading users").from_db()
|
||||
|
@@ -38,6 +38,7 @@ table! {
|
||||
uuid -> Text,
|
||||
org_uuid -> Text,
|
||||
name -> Text,
|
||||
external_id -> Nullable<Text>,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +50,7 @@ table! {
|
||||
user_uuid -> Text,
|
||||
name -> Text,
|
||||
atype -> Integer,
|
||||
push_uuid -> Nullable<Text>,
|
||||
push_token -> Nullable<Text>,
|
||||
refresh_token -> Text,
|
||||
twofactor_remember -> Nullable<Text>,
|
||||
@@ -203,6 +205,7 @@ table! {
|
||||
client_kdf_parallelism -> Nullable<Integer>,
|
||||
api_key -> Nullable<Text>,
|
||||
avatar_color -> Nullable<Text>,
|
||||
external_id -> Nullable<Text>,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,6 +231,16 @@ table! {
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
organization_api_key (uuid, org_uuid) {
|
||||
uuid -> Text,
|
||||
org_uuid -> Text,
|
||||
atype -> Integer,
|
||||
api_key -> Text,
|
||||
revision_date -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
emergency_access (uuid) {
|
||||
uuid -> Text,
|
||||
@@ -291,6 +304,7 @@ joinable!(users_collections -> collections (collection_uuid));
|
||||
joinable!(users_collections -> users (user_uuid));
|
||||
joinable!(users_organizations -> organizations (org_uuid));
|
||||
joinable!(users_organizations -> users (user_uuid));
|
||||
joinable!(organization_api_key -> organizations (org_uuid));
|
||||
joinable!(emergency_access -> users (grantor_uuid));
|
||||
joinable!(groups -> organizations (organizations_uuid));
|
||||
joinable!(groups_users -> users_organizations (users_organizations_uuid));
|
||||
@@ -315,6 +329,7 @@ allow_tables_to_appear_in_same_query!(
|
||||
users,
|
||||
users_collections,
|
||||
users_organizations,
|
||||
organization_api_key,
|
||||
emergency_access,
|
||||
groups,
|
||||
groups_users,
|
||||
|
@@ -38,6 +38,7 @@ table! {
|
||||
uuid -> Text,
|
||||
org_uuid -> Text,
|
||||
name -> Text,
|
||||
external_id -> Nullable<Text>,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +50,7 @@ table! {
|
||||
user_uuid -> Text,
|
||||
name -> Text,
|
||||
atype -> Integer,
|
||||
push_uuid -> Nullable<Text>,
|
||||
push_token -> Nullable<Text>,
|
||||
refresh_token -> Text,
|
||||
twofactor_remember -> Nullable<Text>,
|
||||
@@ -203,6 +205,7 @@ table! {
|
||||
client_kdf_parallelism -> Nullable<Integer>,
|
||||
api_key -> Nullable<Text>,
|
||||
avatar_color -> Nullable<Text>,
|
||||
external_id -> Nullable<Text>,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,6 +231,16 @@ table! {
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
organization_api_key (uuid, org_uuid) {
|
||||
uuid -> Text,
|
||||
org_uuid -> Text,
|
||||
atype -> Integer,
|
||||
api_key -> Text,
|
||||
revision_date -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
emergency_access (uuid) {
|
||||
uuid -> Text,
|
||||
@@ -291,6 +304,7 @@ joinable!(users_collections -> collections (collection_uuid));
|
||||
joinable!(users_collections -> users (user_uuid));
|
||||
joinable!(users_organizations -> organizations (org_uuid));
|
||||
joinable!(users_organizations -> users (user_uuid));
|
||||
joinable!(organization_api_key -> organizations (org_uuid));
|
||||
joinable!(emergency_access -> users (grantor_uuid));
|
||||
joinable!(groups -> organizations (organizations_uuid));
|
||||
joinable!(groups_users -> users_organizations (users_organizations_uuid));
|
||||
@@ -315,6 +329,7 @@ allow_tables_to_appear_in_same_query!(
|
||||
users,
|
||||
users_collections,
|
||||
users_organizations,
|
||||
organization_api_key,
|
||||
emergency_access,
|
||||
groups,
|
||||
groups_users,
|
||||
|
@@ -38,6 +38,7 @@ table! {
|
||||
uuid -> Text,
|
||||
org_uuid -> Text,
|
||||
name -> Text,
|
||||
external_id -> Nullable<Text>,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +50,7 @@ table! {
|
||||
user_uuid -> Text,
|
||||
name -> Text,
|
||||
atype -> Integer,
|
||||
push_uuid -> Nullable<Text>,
|
||||
push_token -> Nullable<Text>,
|
||||
refresh_token -> Text,
|
||||
twofactor_remember -> Nullable<Text>,
|
||||
@@ -203,6 +205,7 @@ table! {
|
||||
client_kdf_parallelism -> Nullable<Integer>,
|
||||
api_key -> Nullable<Text>,
|
||||
avatar_color -> Nullable<Text>,
|
||||
external_id -> Nullable<Text>,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,6 +231,16 @@ table! {
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
organization_api_key (uuid, org_uuid) {
|
||||
uuid -> Text,
|
||||
org_uuid -> Text,
|
||||
atype -> Integer,
|
||||
api_key -> Text,
|
||||
revision_date -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
emergency_access (uuid) {
|
||||
uuid -> Text,
|
||||
@@ -292,6 +305,7 @@ joinable!(users_collections -> users (user_uuid));
|
||||
joinable!(users_organizations -> organizations (org_uuid));
|
||||
joinable!(users_organizations -> users (user_uuid));
|
||||
joinable!(users_organizations -> ciphers (org_uuid));
|
||||
joinable!(organization_api_key -> organizations (org_uuid));
|
||||
joinable!(emergency_access -> users (grantor_uuid));
|
||||
joinable!(groups -> organizations (organizations_uuid));
|
||||
joinable!(groups_users -> users_organizations (users_organizations_uuid));
|
||||
@@ -316,6 +330,7 @@ allow_tables_to_appear_in_same_query!(
|
||||
users,
|
||||
users_collections,
|
||||
users_organizations,
|
||||
organization_api_key,
|
||||
emergency_access,
|
||||
groups,
|
||||
groups_users,
|
||||
|
@@ -573,8 +573,8 @@ async fn send_email(address: &str, subject: &str, body_html: String, body_text:
|
||||
let smtp_from = &CONFIG.smtp_from();
|
||||
|
||||
let body = if CONFIG.smtp_embed_images() {
|
||||
let logo_gray_body = Body::new(crate::api::static_files("logo-gray.png".to_string()).unwrap().1.to_vec());
|
||||
let mail_github_body = Body::new(crate::api::static_files("mail-github.png".to_string()).unwrap().1.to_vec());
|
||||
let logo_gray_body = Body::new(crate::api::static_files("logo-gray.png").unwrap().1.to_vec());
|
||||
let mail_github_body = Body::new(crate::api::static_files("mail-github.png").unwrap().1.to_vec());
|
||||
MultiPart::alternative().singlepart(SinglePart::plain(body_text)).multipart(
|
||||
MultiPart::related()
|
||||
.singlepart(SinglePart::html(body_html))
|
||||
|
@@ -250,7 +250,7 @@ fn init_logging(level: log::LevelFilter) -> Result<(), fern::InitError> {
|
||||
log::LevelFilter::Off
|
||||
};
|
||||
|
||||
// Only show rocket underscore `_` logs when the level is Debug or higher
|
||||
// Only show Rocket underscore `_` logs when the level is Debug or higher
|
||||
// Else this will bloat the log output with useless messages.
|
||||
let rocket_underscore_level = if level >= log::LevelFilter::Debug {
|
||||
log::LevelFilter::Warn
|
||||
@@ -264,8 +264,13 @@ fn init_logging(level: log::LevelFilter) -> Result<(), fern::InitError> {
|
||||
.level_for("rustls::session", log::LevelFilter::Off)
|
||||
// Hide failed to close stream messages
|
||||
.level_for("hyper::server", log::LevelFilter::Warn)
|
||||
// Silence rocket logs
|
||||
// Silence Rocket `_` logs
|
||||
.level_for("_", rocket_underscore_level)
|
||||
.level_for("rocket::response::responder::_", rocket_underscore_level)
|
||||
.level_for("rocket::server::_", rocket_underscore_level)
|
||||
.level_for("vaultwarden::api::admin::_", rocket_underscore_level)
|
||||
.level_for("vaultwarden::api::notifications::_", rocket_underscore_level)
|
||||
// Silence Rocket logs
|
||||
.level_for("rocket::launch", log::LevelFilter::Error)
|
||||
.level_for("rocket::launch_", log::LevelFilter::Error)
|
||||
.level_for("rocket::rocket", log::LevelFilter::Warn)
|
||||
|
@@ -952,5 +952,24 @@
|
||||
"jira.com"
|
||||
],
|
||||
"Excluded": false
|
||||
},
|
||||
{
|
||||
"Type": 90,
|
||||
"Domains": [
|
||||
"pinterest.com",
|
||||
"pinterest.com.au",
|
||||
"pinterest.cl",
|
||||
"pinterest.de",
|
||||
"pinterest.dk",
|
||||
"pinterest.es",
|
||||
"pinterest.fr",
|
||||
"pinterest.co.uk",
|
||||
"pinterest.jp",
|
||||
"pinterest.co.kr",
|
||||
"pinterest.nz",
|
||||
"pinterest.pt",
|
||||
"pinterest.se"
|
||||
],
|
||||
"Excluded": false
|
||||
}
|
||||
]
|
4
src/static/scripts/admin.css
vendored
4
src/static/scripts/admin.css
vendored
@@ -25,7 +25,7 @@ img {
|
||||
min-width: 85px;
|
||||
max-width: 85px;
|
||||
}
|
||||
#users-table .vw-ciphers, #orgs-table .vw-users, #orgs-table .vw-ciphers {
|
||||
#users-table .vw-entries, #orgs-table .vw-users, #orgs-table .vw-entries {
|
||||
min-width: 35px;
|
||||
max-width: 40px;
|
||||
}
|
||||
@@ -53,4 +53,4 @@ img {
|
||||
}
|
||||
.vw-copy-toast {
|
||||
width: 15rem;
|
||||
}
|
||||
}
|
||||
|
13
src/static/scripts/admin.js
vendored
13
src/static/scripts/admin.js
vendored
@@ -3,16 +3,17 @@
|
||||
/* exported BASE_URL, _post */
|
||||
|
||||
function getBaseUrl() {
|
||||
// If the base URL is `https://vaultwarden.example.com/base/path/`,
|
||||
// If the base URL is `https://vaultwarden.example.com/base/path/admin/`,
|
||||
// `window.location.href` should have one of the following forms:
|
||||
//
|
||||
// - `https://vaultwarden.example.com/base/path/`
|
||||
// - `https://vaultwarden.example.com/base/path/#/some/route[?queryParam=...]`
|
||||
// - `https://vaultwarden.example.com/base/path/admin`
|
||||
// - `https://vaultwarden.example.com/base/path/admin/#/some/route[?queryParam=...]`
|
||||
//
|
||||
// We want to get to just `https://vaultwarden.example.com/base/path`.
|
||||
const baseUrl = window.location.href;
|
||||
const adminPos = baseUrl.indexOf("/admin");
|
||||
return baseUrl.substring(0, adminPos != -1 ? adminPos : baseUrl.length);
|
||||
const pathname = window.location.pathname;
|
||||
const adminPos = pathname.indexOf("/admin");
|
||||
const newPathname = pathname.substring(0, adminPos != -1 ? adminPos : pathname.length);
|
||||
return `${window.location.origin}${newPathname}`;
|
||||
}
|
||||
const BASE_URL = getBaseUrl();
|
||||
|
||||
|
4
src/static/scripts/admin_settings.js
vendored
4
src/static/scripts/admin_settings.js
vendored
@@ -19,7 +19,7 @@ function smtpTest(event) {
|
||||
}
|
||||
|
||||
const data = JSON.stringify({ "email": test_email.value });
|
||||
_post(`${BASE_URL}/admin/test/smtp/`,
|
||||
_post(`${BASE_URL}/admin/test/smtp`,
|
||||
"SMTP Test email sent correctly",
|
||||
"Error sending SMTP test email",
|
||||
data, false
|
||||
@@ -45,7 +45,7 @@ function getFormData() {
|
||||
|
||||
function saveConfig(event) {
|
||||
const data = JSON.stringify(getFormData());
|
||||
_post(`${BASE_URL}/admin/config/`,
|
||||
_post(`${BASE_URL}/admin/config`,
|
||||
"Config saved correctly",
|
||||
"Error saving config",
|
||||
data
|
||||
|
2
src/static/scripts/admin_users.js
vendored
2
src/static/scripts/admin_users.js
vendored
@@ -113,7 +113,7 @@ function inviteUser(event) {
|
||||
"email": email.value
|
||||
});
|
||||
email.value = "";
|
||||
_post(`${BASE_URL}/admin/invite/`,
|
||||
_post(`${BASE_URL}/admin/invite`,
|
||||
"User invited correctly",
|
||||
"Error inviting user",
|
||||
data
|
||||
|
15
src/static/scripts/datatables.css
vendored
15
src/static/scripts/datatables.css
vendored
@@ -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.2
|
||||
* https://datatables.net/download/#bs5/dt-1.13.4
|
||||
*
|
||||
* Included libraries:
|
||||
* DataTables 1.13.2
|
||||
* DataTables 1.13.4
|
||||
*/
|
||||
|
||||
@charset "UTF-8";
|
||||
@@ -79,6 +79,7 @@ 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,
|
||||
@@ -88,6 +89,7 @@ table.dataTable thead > tr > td.sorting_asc_disabled:after,
|
||||
table.dataTable thead > tr > td.sorting_desc_disabled:after {
|
||||
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,
|
||||
@@ -104,9 +106,9 @@ 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 {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -132,7 +134,8 @@ div.dataTables_processing > div:last-child > div {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
border-radius: 50%;
|
||||
background: 13 110 253;
|
||||
background: rgb(13, 110, 253);
|
||||
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) {
|
||||
|
137
src/static/scripts/datatables.js
vendored
137
src/static/scripts/datatables.js
vendored
@@ -4,20 +4,20 @@
|
||||
*
|
||||
* To rebuild or modify this file with the latest versions of the included
|
||||
* software please visit:
|
||||
* https://datatables.net/download/#bs5/dt-1.13.2
|
||||
* https://datatables.net/download/#bs5/dt-1.13.4
|
||||
*
|
||||
* Included libraries:
|
||||
* DataTables 1.13.2
|
||||
* DataTables 1.13.4
|
||||
*/
|
||||
|
||||
/*! DataTables 1.13.2
|
||||
/*! DataTables 1.13.4
|
||||
* ©2008-2023 SpryMedia Ltd - datatables.net/license
|
||||
*/
|
||||
|
||||
/**
|
||||
* @summary DataTables
|
||||
* @description Paginate, search and order HTML tables
|
||||
* @version 1.13.2
|
||||
* @version 1.13.4
|
||||
* @author SpryMedia Ltd
|
||||
* @contact www.datatables.net
|
||||
* @copyright SpryMedia Ltd.
|
||||
@@ -46,21 +46,28 @@
|
||||
}
|
||||
else if ( typeof exports === 'object' ) {
|
||||
// CommonJS
|
||||
module.exports = function (root, $) {
|
||||
if ( ! root ) {
|
||||
// CommonJS environments without a window global must pass a
|
||||
// root. This will give an error otherwise
|
||||
root = window;
|
||||
}
|
||||
// jQuery's factory checks for a global window - if it isn't present then it
|
||||
// returns a factory function that expects the window object
|
||||
var jq = require('jquery');
|
||||
|
||||
if ( ! $ ) {
|
||||
$ = typeof window !== 'undefined' ? // jQuery's factory checks for a global window
|
||||
require('jquery') :
|
||||
require('jquery')( root );
|
||||
}
|
||||
if (typeof window !== 'undefined') {
|
||||
module.exports = function (root, $) {
|
||||
if ( ! root ) {
|
||||
// CommonJS environments without a window global must pass a
|
||||
// root. This will give an error otherwise
|
||||
root = window;
|
||||
}
|
||||
|
||||
return factory( $, root, root.document );
|
||||
};
|
||||
if ( ! $ ) {
|
||||
$ = jq( root );
|
||||
}
|
||||
|
||||
return factory( $, root, root.document );
|
||||
};
|
||||
}
|
||||
else {
|
||||
return factory( jq, window, window.document );
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Browser
|
||||
@@ -73,6 +80,12 @@
|
||||
|
||||
var DataTable = function ( selector, options )
|
||||
{
|
||||
// Check if called with a window or jQuery object for DOM less applications
|
||||
// This is for backwards compatibility
|
||||
if (DataTable.factory(selector, options)) {
|
||||
return DataTable;
|
||||
}
|
||||
|
||||
// When creating with `new`, create a new DataTable, returning the API instance
|
||||
if (this instanceof DataTable) {
|
||||
return $(selector).DataTable(options);
|
||||
@@ -1177,6 +1190,7 @@
|
||||
type: sort !== null ? i+'.@data-'+sort : undefined,
|
||||
filter: filter !== null ? i+'.@data-'+filter : undefined
|
||||
};
|
||||
col._isArrayHost = true;
|
||||
|
||||
_fnColumnOptions( oSettings, i );
|
||||
}
|
||||
@@ -2365,7 +2379,7 @@
|
||||
|
||||
// Indicate if DataTables should read DOM data as an object or array
|
||||
// Used in _fnGetRowElements
|
||||
if ( typeof mDataSrc !== 'number' ) {
|
||||
if ( typeof mDataSrc !== 'number' && ! oCol._isArrayHost ) {
|
||||
oSettings._rowReadObject = true;
|
||||
}
|
||||
|
||||
@@ -5119,7 +5133,8 @@
|
||||
{
|
||||
return $('<div/>', {
|
||||
'id': ! settings.aanFeatures.r ? settings.sTableId+'_processing' : null,
|
||||
'class': settings.oClasses.sProcessing
|
||||
'class': settings.oClasses.sProcessing,
|
||||
'role': 'status'
|
||||
} )
|
||||
.html( settings.oLanguage.sProcessing )
|
||||
.append('<div><div></div><div></div><div></div><div></div></div>')
|
||||
@@ -9367,6 +9382,48 @@
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Set the jQuery or window object to be used by DataTables
|
||||
*
|
||||
* @param {*} module Library / container object
|
||||
* @param {string} type Library or container type `lib` or `win`.
|
||||
*/
|
||||
DataTable.use = function (module, type) {
|
||||
if (type === 'lib' || module.fn) {
|
||||
$ = module;
|
||||
}
|
||||
else if (type == 'win' || module.document) {
|
||||
window = module;
|
||||
document = module.document;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* CommonJS factory function pass through. This will check if the arguments
|
||||
* given are a window object or a jQuery object. If so they are set
|
||||
* accordingly.
|
||||
* @param {*} root Window
|
||||
* @param {*} jq jQUery
|
||||
* @returns {boolean} Indicator
|
||||
*/
|
||||
DataTable.factory = function (root, jq) {
|
||||
var is = false;
|
||||
|
||||
// Test if the first parameter is a window object
|
||||
if (root && root.document) {
|
||||
window = root;
|
||||
document = root.document;
|
||||
}
|
||||
|
||||
// Test if the second parameter is a jQuery object
|
||||
if (jq && jq.fn && jq.fn.jquery) {
|
||||
$ = jq;
|
||||
is = true;
|
||||
}
|
||||
|
||||
return is;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a common method for plug-ins to check the version of DataTables being
|
||||
* used, in order to ensure compatibility.
|
||||
@@ -9708,7 +9765,7 @@
|
||||
* @type string
|
||||
* @default Version number
|
||||
*/
|
||||
DataTable.version = "1.13.2";
|
||||
DataTable.version = "1.13.4";
|
||||
|
||||
/**
|
||||
* Private data store, containing all of the settings objects that are
|
||||
@@ -14132,7 +14189,7 @@
|
||||
*
|
||||
* @type string
|
||||
*/
|
||||
build:"bs5/dt-1.13.2",
|
||||
build:"bs5/dt-1.13.4",
|
||||
|
||||
|
||||
/**
|
||||
@@ -15654,25 +15711,33 @@
|
||||
}
|
||||
else if ( typeof exports === 'object' ) {
|
||||
// CommonJS
|
||||
module.exports = function (root, $) {
|
||||
if ( ! root ) {
|
||||
// CommonJS environments without a window global must pass a
|
||||
// root. This will give an error otherwise
|
||||
root = window;
|
||||
}
|
||||
|
||||
if ( ! $ ) {
|
||||
$ = typeof window !== 'undefined' ? // jQuery's factory checks for a global window
|
||||
require('jquery') :
|
||||
require('jquery')( root );
|
||||
}
|
||||
|
||||
var jq = require('jquery');
|
||||
var cjsRequires = function (root, $) {
|
||||
if ( ! $.fn.dataTable ) {
|
||||
require('datatables.net')(root, $);
|
||||
}
|
||||
|
||||
return factory( $, root, root.document );
|
||||
};
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
module.exports = function (root, $) {
|
||||
if ( ! root ) {
|
||||
// CommonJS environments without a window global must pass a
|
||||
// root. This will give an error otherwise
|
||||
root = window;
|
||||
}
|
||||
|
||||
if ( ! $ ) {
|
||||
$ = jq( root );
|
||||
}
|
||||
|
||||
cjsRequires( root, $ );
|
||||
return factory( $, root, root.document );
|
||||
};
|
||||
}
|
||||
else {
|
||||
cjsRequires( window, jq );
|
||||
module.exports = factory( jq, window, window.document );
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Browser
|
||||
|
@@ -1,5 +1,5 @@
|
||||
/*!
|
||||
* jQuery JavaScript Library v3.6.3 -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/Tween,-effects/animatedSelector
|
||||
* jQuery JavaScript Library v3.6.4 -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/
|
||||
*
|
||||
* Includes Sizzle.js
|
||||
@@ -9,7 +9,7 @@
|
||||
* Released under the MIT license
|
||||
* https://jquery.org/license
|
||||
*
|
||||
* Date: 2022-12-20T21:28Z
|
||||
* Date: 2023-03-08T15:29Z
|
||||
*/
|
||||
( function( global, factory ) {
|
||||
|
||||
@@ -151,7 +151,7 @@ function toType( obj ) {
|
||||
|
||||
|
||||
var
|
||||
version = "3.6.3 -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/Tween,-effects/animatedSelector",
|
||||
version = "3.6.4 -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",
|
||||
|
||||
// Define a local copy of jQuery
|
||||
jQuery = function( selector, context ) {
|
||||
@@ -522,14 +522,14 @@ function isArrayLike( obj ) {
|
||||
}
|
||||
var Sizzle =
|
||||
/*!
|
||||
* Sizzle CSS Selector Engine v2.3.9
|
||||
* Sizzle CSS Selector Engine v2.3.10
|
||||
* https://sizzlejs.com/
|
||||
*
|
||||
* Copyright JS Foundation and other contributors
|
||||
* Released under the MIT license
|
||||
* https://js.foundation/
|
||||
*
|
||||
* Date: 2022-12-19
|
||||
* Date: 2023-02-14
|
||||
*/
|
||||
( function( window ) {
|
||||
var i,
|
||||
@@ -633,7 +633,7 @@ var i,
|
||||
whitespace + "+$", "g" ),
|
||||
|
||||
rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ),
|
||||
rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace +
|
||||
rleadingCombinator = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace +
|
||||
"*" ),
|
||||
rdescend = new RegExp( whitespace + "|>" ),
|
||||
|
||||
@@ -850,7 +850,7 @@ function Sizzle( selector, context, results, seed ) {
|
||||
// as such selectors are not recognized by querySelectorAll.
|
||||
// Thanks to Andrew Dupont for this technique.
|
||||
if ( nodeType === 1 &&
|
||||
( rdescend.test( selector ) || rcombinators.test( selector ) ) ) {
|
||||
( rdescend.test( selector ) || rleadingCombinator.test( selector ) ) ) {
|
||||
|
||||
// Expand context for sibling selectors
|
||||
newContext = rsibling.test( selector ) && testContext( context.parentNode ) ||
|
||||
@@ -879,27 +879,6 @@ function Sizzle( selector, context, results, seed ) {
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
// `qSA` may not throw for unrecognized parts using forgiving parsing:
|
||||
// https://drafts.csswg.org/selectors/#forgiving-selector
|
||||
// like the `:has()` pseudo-class:
|
||||
// https://drafts.csswg.org/selectors/#relational
|
||||
// `CSS.supports` is still expected to return `false` then:
|
||||
// https://drafts.csswg.org/css-conditional-4/#typedef-supports-selector-fn
|
||||
// https://drafts.csswg.org/css-conditional-4/#dfn-support-selector
|
||||
if ( support.cssSupportsSelector &&
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
!CSS.supports( "selector(:is(" + newSelector + "))" ) ) {
|
||||
|
||||
// Support: IE 11+
|
||||
// Throw to get to the same code path as an error directly in qSA.
|
||||
// Note: once we only support browser supporting
|
||||
// `CSS.supports('selector(...)')`, we can most likely drop
|
||||
// the `try-catch`. IE doesn't implement the API.
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
push.apply( results,
|
||||
newContext.querySelectorAll( newSelector )
|
||||
);
|
||||
@@ -1195,29 +1174,22 @@ setDocument = Sizzle.setDocument = function( node ) {
|
||||
!el.querySelectorAll( ":scope fieldset div" ).length;
|
||||
} );
|
||||
|
||||
// Support: Chrome 105+, Firefox 104+, Safari 15.4+
|
||||
// Make sure forgiving mode is not used in `CSS.supports( "selector(...)" )`.
|
||||
//
|
||||
// `:is()` uses a forgiving selector list as an argument and is widely
|
||||
// implemented, so it's a good one to test against.
|
||||
support.cssSupportsSelector = assert( function() {
|
||||
/* eslint-disable no-undef */
|
||||
|
||||
return CSS.supports( "selector(*)" ) &&
|
||||
|
||||
// Support: Firefox 78-81 only
|
||||
// In old Firefox, `:is()` didn't use forgiving parsing. In that case,
|
||||
// fail this test as there's no selector to test against that.
|
||||
// `CSS.supports` uses unforgiving parsing
|
||||
document.querySelectorAll( ":is(:jqfake)" ) &&
|
||||
|
||||
// `*` is needed as Safari & newer Chrome implemented something in between
|
||||
// for `:has()` - it throws in `qSA` if it only contains an unsupported
|
||||
// argument but multiple ones, one of which is supported, are fine.
|
||||
// We want to play safe in case `:is()` gets the same treatment.
|
||||
!CSS.supports( "selector(:is(*,:jqfake))" );
|
||||
|
||||
/* eslint-enable */
|
||||
// Support: Chrome 105 - 110+, Safari 15.4 - 16.3+
|
||||
// Make sure the the `:has()` argument is parsed unforgivingly.
|
||||
// We include `*` in the test to detect buggy implementations that are
|
||||
// _selectively_ forgiving (specifically when the list includes at least
|
||||
// one valid selector).
|
||||
// Note that we treat complete lack of support for `:has()` as if it were
|
||||
// spec-compliant support, which is fine because use of `:has()` in such
|
||||
// environments will fail in the qSA path and fall back to jQuery traversal
|
||||
// anyway.
|
||||
support.cssHas = assert( function() {
|
||||
try {
|
||||
document.querySelector( ":has(*,:jqfake)" );
|
||||
return false;
|
||||
} catch ( e ) {
|
||||
return true;
|
||||
}
|
||||
} );
|
||||
|
||||
/* Attributes
|
||||
@@ -1486,14 +1458,14 @@ setDocument = Sizzle.setDocument = function( node ) {
|
||||
} );
|
||||
}
|
||||
|
||||
if ( !support.cssSupportsSelector ) {
|
||||
if ( !support.cssHas ) {
|
||||
|
||||
// Support: Chrome 105+, Safari 15.4+
|
||||
// `:has()` uses a forgiving selector list as an argument so our regular
|
||||
// `try-catch` mechanism fails to catch `:has()` with arguments not supported
|
||||
// natively like `:has(:contains("Foo"))`. Where supported & spec-compliant,
|
||||
// we now use `CSS.supports("selector(:is(SELECTOR_TO_BE_TESTED))")`, but
|
||||
// outside that we mark `:has` as buggy.
|
||||
// Support: Chrome 105 - 110+, Safari 15.4 - 16.3+
|
||||
// Our regular `try-catch` mechanism fails to detect natively-unsupported
|
||||
// pseudo-classes inside `:has()` (such as `:has(:contains("Foo"))`)
|
||||
// in browsers that parse the `:has()` argument as a forgiving selector list.
|
||||
// https://drafts.csswg.org/selectors/#relational now requires the argument
|
||||
// to be parsed unforgivingly, but browsers have not yet fully adjusted.
|
||||
rbuggyQSA.push( ":has" );
|
||||
}
|
||||
|
||||
@@ -2406,7 +2378,7 @@ tokenize = Sizzle.tokenize = function( selector, parseOnly ) {
|
||||
matched = false;
|
||||
|
||||
// Combinators
|
||||
if ( ( match = rcombinators.exec( soFar ) ) ) {
|
||||
if ( ( match = rleadingCombinator.exec( soFar ) ) ) {
|
||||
matched = match.shift();
|
||||
tokens.push( {
|
||||
value: matched,
|
@@ -7,7 +7,7 @@
|
||||
<tr>
|
||||
<th class="vw-org-details">Organization</th>
|
||||
<th class="vw-users">Users</th>
|
||||
<th class="vw-ciphers">Ciphers</th>
|
||||
<th class="vw-entries">Entries</th>
|
||||
<th class="vw-attachments">Attachments</th>
|
||||
<th class="vw-misc">Misc</th>
|
||||
<th class="vw-actions">Actions</th>
|
||||
@@ -59,7 +59,7 @@
|
||||
</main>
|
||||
|
||||
<link rel="stylesheet" href="{{urlpath}}/vw_static/datatables.css" />
|
||||
<script src="{{urlpath}}/vw_static/jquery-3.6.3.slim.js"></script>
|
||||
<script src="{{urlpath}}/vw_static/jquery-3.6.4.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>
|
||||
|
@@ -8,7 +8,7 @@
|
||||
<th class="vw-account-details">User</th>
|
||||
<th class="vw-created-at">Created at</th>
|
||||
<th class="vw-last-active">Last Active</th>
|
||||
<th class="vw-ciphers">Ciphers</th>
|
||||
<th class="vw-entries">Entries</th>
|
||||
<th class="vw-attachments">Attachments</th>
|
||||
<th class="vw-organizations">Organizations</th>
|
||||
<th class="vw-actions">Actions</th>
|
||||
@@ -140,7 +140,7 @@
|
||||
</main>
|
||||
|
||||
<link rel="stylesheet" href="{{urlpath}}/vw_static/datatables.css" />
|
||||
<script src="{{urlpath}}/vw_static/jquery-3.6.3.slim.js"></script>
|
||||
<script src="{{urlpath}}/vw_static/jquery-3.6.4.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>
|
||||
|
@@ -1,6 +1,4 @@
|
||||
Master Password Has Been Changed
|
||||
<!---------------->
|
||||
The master password for {{user_name}} has been changed by an administrator in your {{org_name}} organization. If you did not initiate this request, please reach out to your administrator immediately.
|
||||
|
||||
===
|
||||
Github: https://github.com/dani-garcia/vaultwarden
|
||||
{{> email/email_footer_text }}
|
||||
|
@@ -3,6 +3,4 @@ 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.
|
||||
|
||||
===
|
||||
Github: https://github.com/dani-garcia/vaultwarden
|
||||
{{> email/email_footer_text }}
|
||||
|
@@ -4,7 +4,7 @@ Delete Your Account
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
|
||||
click the link below to delete your account.
|
||||
Click the link below to delete your account.
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
|
@@ -1,5 +1,5 @@
|
||||
Invitation to {{{org_name}}} accepted
|
||||
<!---------------->
|
||||
Your invitation for *{{email}}* to join *{{org_name}}* was accepted.
|
||||
This email is to notify you that {{email}} has accepted your invitation to join {{org_name}}.
|
||||
Please log in via {{url}} to the vaultwarden server and confirm them from the organization management page.
|
||||
{{> email/email_footer_text }}
|
@@ -1,5 +1,5 @@
|
||||
Invitation to {{{org_name}}} confirmed
|
||||
<!---------------->
|
||||
Your invitation to join *{{org_name}}* was confirmed.
|
||||
It will now appear under the Organizations the next time you log in to the web vault at {{url}}.
|
||||
This email is to notify you that you have been confirmed as a user of {{org_name}}.
|
||||
Any collections and logins being shared with you by this organization will now appear in your Vaultwarden vault at {{url}}.
|
||||
{{> email/email_footer_text }}
|
@@ -4,6 +4,4 @@ You have been removed from organization *{{org_name}}* because your account does
|
||||
|
||||
|
||||
You can enable Two-step Login in your account settings.
|
||||
|
||||
===
|
||||
Github: https://github.com/dani-garcia/vaultwarden
|
||||
{{> email/email_footer_text }}
|
||||
|
@@ -1,5 +1,4 @@
|
||||
You have been removed from {{{org_name}}}
|
||||
<!---------------->
|
||||
Your user account has been removed from the *{{org_name}}* organization because you are a part of another organization. The {{org_name}} organization has enabled a policy that prevents users from being a part of multiple organizations. Before you can re-join this organization you need to leave all other organizations or join with a different account.
|
||||
===
|
||||
Github: https://github.com/dani-garcia/vaultwarden
|
||||
{{> email/email_footer_text }}
|
||||
|
15
src/util.rs
15
src/util.rs
@@ -1,7 +1,10 @@
|
||||
//
|
||||
// Web Headers and caching
|
||||
//
|
||||
use std::io::{Cursor, ErrorKind};
|
||||
use std::{
|
||||
io::{Cursor, ErrorKind},
|
||||
ops::Deref,
|
||||
};
|
||||
|
||||
use rocket::{
|
||||
fairing::{Fairing, Info, Kind},
|
||||
@@ -209,6 +212,14 @@ impl std::fmt::Display for SafeString {
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for SafeString {
|
||||
type Target = String;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<Path> for SafeString {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &Path {
|
||||
@@ -231,7 +242,7 @@ impl<'r> FromParam<'r> for SafeString {
|
||||
|
||||
// Log all the routes from the main paths list, and the attachments endpoint
|
||||
// Effectively ignores, any static file route, and the alive endpoint
|
||||
const LOGGED_ROUTES: [&str; 5] = ["/api", "/admin", "/identity", "/icons", "/attachments"];
|
||||
const LOGGED_ROUTES: [&str; 7] = ["/api", "/admin", "/identity", "/icons", "/attachments", "/events", "/notifications"];
|
||||
|
||||
// Boolean is extra debug, when true, we ignore the whitelist above and also print the mounts
|
||||
pub struct BetterLogging(pub bool);
|
||||
|
Reference in New Issue
Block a user