mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2026-03-23 10:19:21 -07:00
Compare commits
120 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66bff73ebf | ||
|
|
83d5432cbf | ||
|
|
f579a4154c | ||
|
|
f5a19c5f8b | ||
|
|
aa9bc1f785 | ||
|
|
f162e85e44 | ||
|
|
33ef70c192 | ||
|
|
3d2df6ce11 | ||
|
|
6cdcb3b297 | ||
|
|
d1af468700 | ||
|
|
ae1c53f4e5 | ||
|
|
bc57c4b193 | ||
|
|
61ae4c9cf5 | ||
|
|
8d7b3db33d | ||
|
|
e9ec3741ae | ||
|
|
dacd50f3f1 | ||
|
|
9412112639 | ||
|
|
aaeae16983 | ||
|
|
d892880dd2 | ||
|
|
4395e8e888 | ||
|
|
3dbfc484a5 | ||
|
|
4ec2507073 | ||
|
|
ab65d7989b | ||
|
|
8707728cdb | ||
|
|
631d022e17 | ||
|
|
211f4492fa | ||
|
|
61f9081827 | ||
|
|
a8e5384c4a | ||
|
|
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 | ||
|
|
0b28ab3be1 | ||
|
|
c5bcc340fa | ||
|
|
bff54fbfdb | ||
|
|
867c6ba056 | ||
|
|
d1ecf03f44 | ||
|
|
fc43608eec | ||
|
|
15dd05c78d | ||
|
|
aa6f774f65 | ||
|
|
379f885354 | ||
|
|
39a5f2dbe8 | ||
|
|
0daaa9b175 | ||
|
|
0c085d21ce | ||
|
|
dcaaa430f0 | ||
|
|
2cda54ceff | ||
|
|
525e6bb65a | ||
|
|
62cebebd3d |
@@ -30,6 +30,10 @@
|
|||||||
## Define the size of the connection pool used for connecting to the database.
|
## Define the size of the connection pool used for connecting to the database.
|
||||||
# DATABASE_MAX_CONNS=10
|
# DATABASE_MAX_CONNS=10
|
||||||
|
|
||||||
|
## Database timeout
|
||||||
|
## Timeout when acquiring database connection
|
||||||
|
# DATABASE_TIMEOUT=30
|
||||||
|
|
||||||
## Database connection initialization
|
## Database connection initialization
|
||||||
## Allows SQL statements to be run whenever a new database connection is created.
|
## Allows SQL statements to be run whenever a new database connection is created.
|
||||||
## This is mainly useful for connection-scoped pragmas.
|
## This is mainly useful for connection-scoped pragmas.
|
||||||
@@ -72,6 +76,13 @@
|
|||||||
# WEBSOCKET_ADDRESS=0.0.0.0
|
# WEBSOCKET_ADDRESS=0.0.0.0
|
||||||
# WEBSOCKET_PORT=3012
|
# 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_URI=https://push.bitwarden.com
|
||||||
|
|
||||||
## Controls whether users are allowed to create Bitwarden Sends.
|
## Controls whether users are allowed to create Bitwarden Sends.
|
||||||
## This setting applies globally to all users.
|
## This setting applies globally to all users.
|
||||||
## To control this on a per-org basis instead, use the "Disable Send" org policy.
|
## To control this on a per-org basis instead, use the "Disable Send" org policy.
|
||||||
@@ -264,6 +275,8 @@
|
|||||||
## For details see: https://github.com/dani-garcia/vaultwarden/wiki/Enabling-admin-page#secure-the-admin_token
|
## 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
|
## If not set, the admin panel is disabled
|
||||||
## New Argon2 PHC string
|
## 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'
|
# ADMIN_TOKEN='$argon2id$v=19$m=65540,t=3,p=4$MmeKRnGK5RW5mJS7h3TOL89GrpLPXJPAtTK8FTqj9HM$DqsstvoSAETl9YhnsXbf43WeaUwJC6JhViIvuPoig78'
|
||||||
## Old plain text string (Will generate warnings in favor of Argon2)
|
## Old plain text string (Will generate warnings in favor of Argon2)
|
||||||
# ADMIN_TOKEN=Vy2VyYTTsKPv8W5aEOWUbB/Bt3DEKePbHmI4m9VcemUMS2rEviDowNAFqYi1xjmp
|
# ADMIN_TOKEN=Vy2VyYTTsKPv8W5aEOWUbB/Bt3DEKePbHmI4m9VcemUMS2rEviDowNAFqYi1xjmp
|
||||||
|
|||||||
19
.github/workflows/build.yml
vendored
19
.github/workflows/build.yml
vendored
@@ -24,13 +24,13 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
timeout-minutes: 120
|
timeout-minutes: 120
|
||||||
# Make warnings errors, this is to prevent warnings slipping through.
|
# Make warnings errors, this is to prevent warnings slipping through.
|
||||||
# This is done globally to prevent rebuilds when the RUSTFLAGS env variable changes.
|
# This is done globally to prevent rebuilds when the RUSTFLAGS env variable changes.
|
||||||
env:
|
env:
|
||||||
RUSTFLAGS: "-D warnings"
|
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:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -43,13 +43,13 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
# Checkout the repo
|
# Checkout the repo
|
||||||
- name: "Checkout"
|
- name: "Checkout"
|
||||||
uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0
|
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||||
# End Checkout the repo
|
# End Checkout the repo
|
||||||
|
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
- name: "Install dependencies Ubuntu"
|
- name: "Install dependencies Ubuntu"
|
||||||
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends openssl sqlite build-essential libmariadb-dev-compat libpq-dev libssl-dev pkg-config
|
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends openssl build-essential libmariadb-dev-compat libpq-dev libssl-dev pkg-config
|
||||||
# End Install dependencies
|
# End Install dependencies
|
||||||
|
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ jobs:
|
|||||||
|
|
||||||
# Only install the clippy and rustfmt components on the default rust-toolchain
|
# Only install the clippy and rustfmt components on the default rust-toolchain
|
||||||
- name: "Install rust-toolchain version"
|
- 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' }}
|
if: ${{ matrix.channel == 'rust-toolchain' }}
|
||||||
with:
|
with:
|
||||||
toolchain: "${{steps.toolchain.outputs.RUST_TOOLCHAIN}}"
|
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
|
# Install the any other channel to be used for which we do not execute clippy and rustfmt
|
||||||
- name: "Install MSRV version"
|
- 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' }}
|
if: ${{ matrix.channel != 'rust-toolchain' }}
|
||||||
with:
|
with:
|
||||||
toolchain: "${{steps.toolchain.outputs.RUST_TOOLCHAIN}}"
|
toolchain: "${{steps.toolchain.outputs.RUST_TOOLCHAIN}}"
|
||||||
@@ -89,7 +89,12 @@ jobs:
|
|||||||
|
|
||||||
|
|
||||||
# Enable Rust Caching
|
# Enable Rust Caching
|
||||||
- uses: Swatinem/rust-cache@6fd3edff6979b79f87531400ad694fb7f2c84b1f # v2.2.1
|
- uses: Swatinem/rust-cache@dd05243424bd5c0e585e4b55eb2d7615cdd32f1f # v2.5.1
|
||||||
|
with:
|
||||||
|
# Use a custom prefix-key to force a fresh start. This is sometimes needed with bigger changes.
|
||||||
|
# Like changing the build host from Ubuntu 20.04 to 22.04 for example.
|
||||||
|
# Only update when really needed! Use a <year>.<month>[.<inc>] format.
|
||||||
|
prefix-key: "v2023.07-rust"
|
||||||
# End Enable Rust Caching
|
# End Enable Rust Caching
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
4
.github/workflows/hadolint.yml
vendored
4
.github/workflows/hadolint.yml
vendored
@@ -8,12 +8,12 @@ on: [
|
|||||||
jobs:
|
jobs:
|
||||||
hadolint:
|
hadolint:
|
||||||
name: Validate Dockerfile syntax
|
name: Validate Dockerfile syntax
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
steps:
|
steps:
|
||||||
# Checkout the repo
|
# Checkout the repo
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0
|
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||||
# End Checkout the repo
|
# End Checkout the repo
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
12
.github/workflows/release.yml
vendored
12
.github/workflows/release.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
|||||||
# Some checks to determine if we need to continue with building a new docker.
|
# Some checks to determine if we need to continue with building a new docker.
|
||||||
# We will skip this check if we are creating a tag, because that has the same hash as a previous run already.
|
# We will skip this check if we are creating a tag, because that has the same hash as a previous run already.
|
||||||
skip_check:
|
skip_check:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
if: ${{ github.repository == 'dani-garcia/vaultwarden' }}
|
if: ${{ github.repository == 'dani-garcia/vaultwarden' }}
|
||||||
outputs:
|
outputs:
|
||||||
should_skip: ${{ steps.skip_check.outputs.should_skip }}
|
should_skip: ${{ steps.skip_check.outputs.should_skip }}
|
||||||
@@ -38,7 +38,7 @@ jobs:
|
|||||||
if: ${{ startsWith(github.ref, 'refs/heads/') }}
|
if: ${{ startsWith(github.ref, 'refs/heads/') }}
|
||||||
|
|
||||||
docker-build:
|
docker-build:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
timeout-minutes: 120
|
timeout-minutes: 120
|
||||||
needs: skip_check
|
needs: skip_check
|
||||||
# Start a local docker registry to be used to generate multi-arch images.
|
# Start a local docker registry to be used to generate multi-arch images.
|
||||||
@@ -73,7 +73,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
# Checkout the repo
|
# Checkout the repo
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0
|
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -92,7 +92,7 @@ jobs:
|
|||||||
|
|
||||||
# Login to Docker Hub
|
# Login to Docker Hub
|
||||||
- name: 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:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
@@ -100,7 +100,7 @@ jobs:
|
|||||||
|
|
||||||
# Login to GitHub Container Registry
|
# Login to GitHub Container Registry
|
||||||
- name: 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:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
@@ -109,7 +109,7 @@ jobs:
|
|||||||
|
|
||||||
# Login to Quay.io
|
# Login to Quay.io
|
||||||
- name: 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:
|
with:
|
||||||
registry: quay.io
|
registry: quay.io
|
||||||
username: ${{ secrets.QUAY_USERNAME }}
|
username: ${{ secrets.QUAY_USERNAME }}
|
||||||
|
|||||||
@@ -7,3 +7,5 @@ ignored:
|
|||||||
- DL3059
|
- DL3059
|
||||||
trustedRegistries:
|
trustedRegistries:
|
||||||
- docker.io
|
- docker.io
|
||||||
|
- ghcr.io
|
||||||
|
- quay.io
|
||||||
|
|||||||
1357
Cargo.lock
generated
1357
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
82
Cargo.toml
82
Cargo.toml
@@ -3,7 +3,7 @@ name = "vaultwarden"
|
|||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
authors = ["Daniel García <dani-garcia@users.noreply.github.com>"]
|
authors = ["Daniel García <dani-garcia@users.noreply.github.com>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.66.1"
|
rust-version = "1.69.0"
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
repository = "https://github.com/dani-garcia/vaultwarden"
|
repository = "https://github.com/dani-garcia/vaultwarden"
|
||||||
@@ -36,11 +36,11 @@ unstable = []
|
|||||||
|
|
||||||
[target."cfg(not(windows))".dependencies]
|
[target."cfg(not(windows))".dependencies]
|
||||||
# Logging
|
# Logging
|
||||||
syslog = "6.0.1" # Needs to be v4 until fern is updated
|
syslog = "6.1.0"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# Logging
|
# Logging
|
||||||
log = "0.4.17"
|
log = "0.4.19"
|
||||||
fern = { version = "0.6.2", features = ["syslog-6"] }
|
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
|
tracing = { version = "0.1.37", features = ["log"] } # Needed to have lettre and webauthn-rs trace logging to work
|
||||||
|
|
||||||
@@ -48,55 +48,57 @@ tracing = { version = "0.1.37", features = ["log"] } # Needed to have lettre and
|
|||||||
dotenvy = { version = "0.15.7", default-features = false }
|
dotenvy = { version = "0.15.7", default-features = false }
|
||||||
|
|
||||||
# Lazy initialization
|
# Lazy initialization
|
||||||
once_cell = "1.17.1"
|
once_cell = "1.18.0"
|
||||||
|
|
||||||
# Numerical libraries
|
# Numerical libraries
|
||||||
num-traits = "0.2.15"
|
num-traits = "0.2.16"
|
||||||
num-derive = "0.3.3"
|
num-derive = "0.4.0"
|
||||||
|
|
||||||
# Web framework
|
# Web framework
|
||||||
rocket = { version = "0.5.0-rc.3", features = ["tls", "json"], default-features = false }
|
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
|
# WebSockets libraries
|
||||||
tokio-tungstenite = "0.18.0"
|
tokio-tungstenite = "0.19.0"
|
||||||
rmpv = "1.0.0" # MessagePack library
|
rmpv = "1.0.1" # MessagePack library
|
||||||
|
|
||||||
# Concurrent HashMap used for WebSocket messaging and favicons
|
# Concurrent HashMap used for WebSocket messaging and favicons
|
||||||
dashmap = "5.4.0"
|
dashmap = "5.5.0"
|
||||||
|
|
||||||
# Async futures
|
# Async futures
|
||||||
futures = "0.3.27"
|
futures = "0.3.28"
|
||||||
tokio = { version = "1.26.0", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal"] }
|
tokio = { version = "1.30.0", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal"] }
|
||||||
|
|
||||||
# A generic serialization/deserialization framework
|
# A generic serialization/deserialization framework
|
||||||
serde = { version = "1.0.158", features = ["derive"] }
|
serde = { version = "1.0.183", features = ["derive"] }
|
||||||
serde_json = "1.0.94"
|
serde_json = "1.0.104"
|
||||||
|
|
||||||
# A safe, extensible ORM and Query builder
|
# A safe, extensible ORM and Query builder
|
||||||
diesel = { version = "2.0.3", features = ["chrono", "r2d2"] }
|
diesel = { version = "2.1.0", features = ["chrono", "r2d2"] }
|
||||||
diesel_migrations = "2.0.0"
|
diesel_migrations = "2.1.0"
|
||||||
diesel_logger = { version = "0.2.0", optional = true }
|
diesel_logger = { version = "0.3.0", optional = true }
|
||||||
|
|
||||||
# Bundled/Static SQLite
|
# 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
|
# Crypto-related libraries
|
||||||
rand = { version = "0.8.5", features = ["small_rng"] }
|
rand = { version = "0.8.5", features = ["small_rng"] }
|
||||||
ring = "0.16.20"
|
ring = "0.16.20"
|
||||||
|
|
||||||
# UUID generation
|
# UUID generation
|
||||||
uuid = { version = "1.3.0", features = ["v4"] }
|
uuid = { version = "1.4.1", features = ["v4"] }
|
||||||
|
|
||||||
# Date and time libraries
|
# Date and time libraries
|
||||||
chrono = { version = "0.4.24", features = ["clock", "serde"], default-features = false }
|
chrono = { version = "0.4.26", features = ["clock", "serde"], default-features = false }
|
||||||
chrono-tz = "0.8.1"
|
chrono-tz = "0.8.3"
|
||||||
time = "0.3.20"
|
time = "0.3.25"
|
||||||
|
|
||||||
# Job scheduler
|
# Job scheduler
|
||||||
job_scheduler_ng = "2.0.4"
|
job_scheduler_ng = "2.0.4"
|
||||||
|
|
||||||
# Data encoding library Hex/Base32/Base64
|
# Data encoding library Hex/Base32/Base64
|
||||||
data-encoding = "2.3.3"
|
data-encoding = "2.4.0"
|
||||||
|
|
||||||
# JWT library
|
# JWT library
|
||||||
jsonwebtoken = "8.3.0"
|
jsonwebtoken = "8.3.0"
|
||||||
@@ -111,56 +113,60 @@ yubico = { version = "0.11.0", features = ["online-tokio"], default-features = f
|
|||||||
webauthn-rs = "0.3.2"
|
webauthn-rs = "0.3.2"
|
||||||
|
|
||||||
# Handling of URL's for WebAuthn and favicons
|
# Handling of URL's for WebAuthn and favicons
|
||||||
url = "2.3.1"
|
url = "2.4.0"
|
||||||
|
|
||||||
# Email libraries
|
# Email libraries
|
||||||
lettre = { version = "0.10.3", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "tokio1-native-tls", "hostname", "tracing", "tokio1"], default-features = false }
|
lettre = { version = "0.10.4", 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
|
percent-encoding = "2.3.0" # URL encoding library used for URL's in the emails
|
||||||
email_address = "0.2.4"
|
email_address = "0.2.4"
|
||||||
|
|
||||||
# HTML Template library
|
# 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)
|
# HTTP client (Used for favicons, version check, DUO and HIBP API)
|
||||||
reqwest = { version = "0.11.15", features = ["stream", "json", "gzip", "brotli", "socks", "cookies", "trust-dns"] }
|
reqwest = { version = "0.11.18", features = ["stream", "json", "deflate", "gzip", "brotli", "socks", "cookies", "trust-dns", "native-tls-alpn"] }
|
||||||
|
|
||||||
# Favicon extraction libraries
|
# Favicon extraction libraries
|
||||||
html5gum = "0.5.2"
|
html5gum = "0.5.7"
|
||||||
regex = { version = "1.7.3", features = ["std", "perf", "unicode-perl"], default-features = false }
|
regex = { version = "1.9.3", features = ["std", "perf", "unicode-perl"], default-features = false }
|
||||||
data-url = "0.2.0"
|
data-url = "0.3.0"
|
||||||
bytes = "1.4.0"
|
bytes = "1.4.0"
|
||||||
|
|
||||||
# Cache function results (Used for version check and favicon fetching)
|
# 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
|
# Used for custom short lived cookie jar during favicon extraction
|
||||||
cookie = "0.16.2"
|
cookie = "0.16.2"
|
||||||
cookie_store = "0.19.0"
|
cookie_store = "0.19.1"
|
||||||
|
|
||||||
# Used by U2F, JWT and PostgreSQL
|
# Used by U2F, JWT and PostgreSQL
|
||||||
openssl = "0.10.48"
|
openssl = "0.10.56"
|
||||||
|
|
||||||
# CLI argument parsing
|
# CLI argument parsing
|
||||||
pico-args = "0.5.0"
|
pico-args = "0.5.0"
|
||||||
|
|
||||||
# Macro ident concatenation
|
# Macro ident concatenation
|
||||||
paste = "1.0.12"
|
paste = "1.0.14"
|
||||||
governor = "0.5.1"
|
governor = "0.6.0"
|
||||||
|
|
||||||
# Check client versions for specific features.
|
# Check client versions for specific features.
|
||||||
semver = "1.0.17"
|
semver = "1.0.18"
|
||||||
|
|
||||||
# Allow overriding the default memory allocator
|
# Allow overriding the default memory allocator
|
||||||
# Mainly used for the musl builds, since the default musl malloc is very slow
|
# Mainly used for the musl builds, since the default musl malloc is very slow
|
||||||
mimalloc = { version = "0.1.34", features = ["secure"], default-features = false, optional = true }
|
mimalloc = { version = "0.1.37", features = ["secure"], default-features = false, optional = true }
|
||||||
which = "4.4.0"
|
which = "4.4.0"
|
||||||
|
|
||||||
# Argon2 library with support for the PHC format
|
# Argon2 library with support for the PHC format
|
||||||
argon2 = "0.5.0"
|
argon2 = "0.5.1"
|
||||||
|
|
||||||
# Reading a password from the cli for generating the Argon2id ADMIN_TOKEN
|
# Reading a password from the cli for generating the Argon2id ADMIN_TOKEN
|
||||||
rpassword = "7.2.0"
|
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
|
# Strip debuginfo from the release builds
|
||||||
# Also enable thin LTO for some optimizations
|
# Also enable thin LTO for some optimizations
|
||||||
[profile.release]
|
[profile.release]
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ Pull the docker image and mount a volume from the host for persistent storage:
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker pull vaultwarden/server:latest
|
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.
|
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
|
// Combined version
|
||||||
if let Some(exact) = exact_tag {
|
if let Some(exact) = exact_tag {
|
||||||
Ok(exact)
|
Ok(exact)
|
||||||
} else if &branch != "main" && &branch != "master" {
|
} else if &branch != "main" && &branch != "master" && &branch != "HEAD" {
|
||||||
Ok(format!("{last_tag}-{rev_short} ({branch})"))
|
Ok(format!("{last_tag}-{rev_short} ({branch})"))
|
||||||
} else {
|
} else {
|
||||||
Ok(format!("{last_tag}-{rev_short}"))
|
Ok(format!("{last_tag}-{rev_short}"))
|
||||||
|
|||||||
@@ -2,40 +2,42 @@
|
|||||||
|
|
||||||
# This file was generated using a Jinja2 template.
|
# This file was generated using a Jinja2 template.
|
||||||
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
||||||
|
{% set rust_version = "1.71.1" %}
|
||||||
{% set build_stage_base_image = "rust:1.68.1-bullseye" %}
|
{% set debian_version = "bookworm" %}
|
||||||
|
{% set alpine_version = "3.17" %}
|
||||||
|
{% set build_stage_base_image = "docker.io/library/rust:%s-%s" % (rust_version, debian_version) %}
|
||||||
{% if "alpine" in target_file %}
|
{% if "alpine" in target_file %}
|
||||||
{% if "amd64" in target_file %}
|
{% if "amd64" in target_file %}
|
||||||
{% set build_stage_base_image = "blackdex/rust-musl:x86_64-musl-stable-1.68.1" %}
|
{% set build_stage_base_image = "docker.io/blackdex/rust-musl:x86_64-musl-stable-%s-openssl3" % rust_version %}
|
||||||
{% set runtime_stage_base_image = "alpine:3.17" %}
|
{% set runtime_stage_base_image = "docker.io/library/alpine:%s" % alpine_version %}
|
||||||
{% set package_arch_target = "x86_64-unknown-linux-musl" %}
|
{% set package_arch_target = "x86_64-unknown-linux-musl" %}
|
||||||
{% elif "armv7" in target_file %}
|
{% elif "armv7" in target_file %}
|
||||||
{% set build_stage_base_image = "blackdex/rust-musl:armv7-musleabihf-stable-1.68.1" %}
|
{% set build_stage_base_image = "docker.io/blackdex/rust-musl:armv7-musleabihf-stable-%s-openssl3" % rust_version %}
|
||||||
{% set runtime_stage_base_image = "balenalib/armv7hf-alpine:3.17" %}
|
{% set runtime_stage_base_image = "docker.io/balenalib/armv7hf-alpine:%s" % alpine_version %}
|
||||||
{% set package_arch_target = "armv7-unknown-linux-musleabihf" %}
|
{% set package_arch_target = "armv7-unknown-linux-musleabihf" %}
|
||||||
{% elif "armv6" in target_file %}
|
{% elif "armv6" in target_file %}
|
||||||
{% set build_stage_base_image = "blackdex/rust-musl:arm-musleabi-stable-1.68.1" %}
|
{% set build_stage_base_image = "docker.io/blackdex/rust-musl:arm-musleabi-stable-%s-openssl3" % rust_version %}
|
||||||
{% set runtime_stage_base_image = "balenalib/rpi-alpine:3.17" %}
|
{% set runtime_stage_base_image = "docker.io/balenalib/rpi-alpine:%s" % alpine_version %}
|
||||||
{% set package_arch_target = "arm-unknown-linux-musleabi" %}
|
{% set package_arch_target = "arm-unknown-linux-musleabi" %}
|
||||||
{% elif "arm64" in target_file %}
|
{% elif "arm64" in target_file %}
|
||||||
{% set build_stage_base_image = "blackdex/rust-musl:aarch64-musl-stable-1.68.1" %}
|
{% set build_stage_base_image = "docker.io/blackdex/rust-musl:aarch64-musl-stable-%s-openssl3" % rust_version %}
|
||||||
{% set runtime_stage_base_image = "balenalib/aarch64-alpine:3.17" %}
|
{% set runtime_stage_base_image = "docker.io/balenalib/aarch64-alpine:%s" % alpine_version %}
|
||||||
{% set package_arch_target = "aarch64-unknown-linux-musl" %}
|
{% set package_arch_target = "aarch64-unknown-linux-musl" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% elif "amd64" in target_file %}
|
{% elif "amd64" in target_file %}
|
||||||
{% set runtime_stage_base_image = "debian:bullseye-slim" %}
|
{% set runtime_stage_base_image = "docker.io/library/debian:%s-slim" % debian_version %}
|
||||||
{% elif "arm64" in target_file %}
|
{% elif "arm64" in target_file %}
|
||||||
{% set runtime_stage_base_image = "balenalib/aarch64-debian:bullseye" %}
|
{% set runtime_stage_base_image = "docker.io/balenalib/aarch64-debian:%s" % debian_version %}
|
||||||
{% set package_arch_name = "arm64" %}
|
{% set package_arch_name = "arm64" %}
|
||||||
{% set package_arch_target = "aarch64-unknown-linux-gnu" %}
|
{% set package_arch_target = "aarch64-unknown-linux-gnu" %}
|
||||||
{% set package_cross_compiler = "aarch64-linux-gnu" %}
|
{% set package_cross_compiler = "aarch64-linux-gnu" %}
|
||||||
{% elif "armv6" in target_file %}
|
{% elif "armv6" in target_file %}
|
||||||
{% set runtime_stage_base_image = "balenalib/rpi-debian:bullseye" %}
|
{% set runtime_stage_base_image = "docker.io/balenalib/rpi-debian:%s" % debian_version %}
|
||||||
{% set package_arch_name = "armel" %}
|
{% set package_arch_name = "armel" %}
|
||||||
{% set package_arch_target = "arm-unknown-linux-gnueabi" %}
|
{% set package_arch_target = "arm-unknown-linux-gnueabi" %}
|
||||||
{% set package_cross_compiler = "arm-linux-gnueabi" %}
|
{% set package_cross_compiler = "arm-linux-gnueabi" %}
|
||||||
{% elif "armv7" in target_file %}
|
{% elif "armv7" in target_file %}
|
||||||
{% set runtime_stage_base_image = "balenalib/armv7hf-debian:bullseye" %}
|
{% set runtime_stage_base_image = "docker.io/balenalib/armv7hf-debian:%s" % debian_version %}
|
||||||
{% set package_arch_name = "armhf" %}
|
{% set package_arch_name = "armhf" %}
|
||||||
{% set package_arch_target = "armv7-unknown-linux-gnueabihf" %}
|
{% set package_arch_target = "armv7-unknown-linux-gnueabihf" %}
|
||||||
{% set package_cross_compiler = "arm-linux-gnueabihf" %}
|
{% set package_cross_compiler = "arm-linux-gnueabihf" %}
|
||||||
@@ -59,8 +61,8 @@
|
|||||||
# https://docs.docker.com/develop/develop-images/multistage-build/
|
# https://docs.docker.com/develop/develop-images/multistage-build/
|
||||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
# https://whitfin.io/speeding-up-rust-docker-builds/
|
||||||
####################### VAULT BUILD IMAGE #######################
|
####################### VAULT BUILD IMAGE #######################
|
||||||
{% set vault_version = "v2023.3.0b" %}
|
{% set vault_version = "v2023.7.1" %}
|
||||||
{% set vault_image_digest = "sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee" %}
|
{% set vault_image_digest = "sha256:b306f38fe0d54fa3d79059a737f8e1803da44ddc5f273c2aecdd6a4886211b0f" %}
|
||||||
# The web-vault digest specifies a particular web-vault build on Docker Hub.
|
# The web-vault digest specifies a particular web-vault build on Docker Hub.
|
||||||
# Using the digest instead of the tag name provides better security,
|
# 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
|
# as the digest of an image is immutable, whereas a tag name can later
|
||||||
@@ -70,15 +72,15 @@
|
|||||||
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
# - 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.
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
# - From the command line:
|
# - From the command line:
|
||||||
# $ docker pull vaultwarden/web-vault:{{ vault_version }}
|
# $ docker pull docker.io/vaultwarden/web-vault:{{ vault_version }}
|
||||||
# $ docker image inspect --format "{{ '{{' }}.RepoDigests}}" vaultwarden/web-vault:{{ vault_version }}
|
# $ docker image inspect --format "{{ '{{' }}.RepoDigests}}" docker.io/vaultwarden/web-vault:{{ vault_version }}
|
||||||
# [vaultwarden/web-vault@{{ vault_image_digest }}]
|
# [docker.io/vaultwarden/web-vault@{{ vault_image_digest }}]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{ '{{' }}.RepoTags}}" vaultwarden/web-vault@{{ vault_image_digest }}
|
# $ docker image inspect --format "{{ '{{' }}.RepoTags}}" docker.io/vaultwarden/web-vault@{{ vault_image_digest }}
|
||||||
# [vaultwarden/web-vault:{{ vault_version }}]
|
# [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 ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM {{ build_stage_base_image }} as build
|
FROM {{ build_stage_base_image }} as build
|
||||||
@@ -89,6 +91,7 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
|||||||
TZ=UTC \
|
TZ=UTC \
|
||||||
TERM=xterm-256color \
|
TERM=xterm-256color \
|
||||||
CARGO_HOME="/root/.cargo" \
|
CARGO_HOME="/root/.cargo" \
|
||||||
|
REGISTRIES_CRATES_IO_PROTOCOL=sparse \
|
||||||
USER="root"
|
USER="root"
|
||||||
|
|
||||||
# Create CARGO_HOME folder and don't download rust docs
|
# Create CARGO_HOME folder and don't download rust docs
|
||||||
@@ -96,19 +99,21 @@ RUN {{ mount_rust_cache -}} mkdir -pv "${CARGO_HOME}" \
|
|||||||
&& rustup set profile minimal
|
&& rustup set profile minimal
|
||||||
|
|
||||||
{% if "alpine" in target_file %}
|
{% if "alpine" in target_file %}
|
||||||
|
# Use PostgreSQL v15 during Alpine/MUSL builds instead of the default v11
|
||||||
|
# Debian Bookworm already contains libpq v15
|
||||||
|
ENV PQ_LIB_DIR="/usr/local/musl/pq15/lib"
|
||||||
{% if "armv6" in target_file %}
|
{% if "armv6" in target_file %}
|
||||||
# To be able to build the armv6 image with mimalloc we need to specifically specify the libatomic.a file location
|
# To be able to build the armv6 image with mimalloc we need to tell the linker to also look for libatomic
|
||||||
ENV RUSTFLAGS='-Clink-arg=/usr/local/musl/{{ package_arch_target }}/lib/libatomic.a'
|
ENV RUSTFLAGS='-Clink-arg=-latomic'
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% elif "arm" in target_file %}
|
{% elif "arm" in target_file %}
|
||||||
# Install build dependencies for the {{ package_arch_name }} architecture
|
# Install build dependencies for the {{ package_arch_name }} architecture
|
||||||
RUN dpkg --add-architecture {{ package_arch_name }} \
|
RUN {{ mount_rust_cache -}} dpkg --add-architecture {{ package_arch_name }} \
|
||||||
&& apt-get update \
|
&& apt-get update \
|
||||||
&& apt-get install -y \
|
&& apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
gcc-{{ package_cross_compiler }} \
|
gcc-{{ package_cross_compiler }} \
|
||||||
libc6-dev{{ package_arch_prefix }} \
|
libc6-dev{{ package_arch_prefix }} \
|
||||||
libcap2-bin \
|
|
||||||
libmariadb-dev{{ package_arch_prefix }} \
|
libmariadb-dev{{ package_arch_prefix }} \
|
||||||
libmariadb-dev-compat{{ package_arch_prefix }} \
|
libmariadb-dev-compat{{ package_arch_prefix }} \
|
||||||
libmariadb3{{ package_arch_prefix }} \
|
libmariadb3{{ package_arch_prefix }} \
|
||||||
@@ -131,7 +136,6 @@ ENV CC_{{ package_arch_target | replace("-", "_") }}="/usr/bin/{{ package_cross_
|
|||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y \
|
&& apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
libcap2-bin \
|
|
||||||
libmariadb-dev \
|
libmariadb-dev \
|
||||||
libpq-dev
|
libpq-dev
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -174,18 +178,6 @@ RUN touch src/main.rs
|
|||||||
# your actual source files being built
|
# your actual source files being built
|
||||||
RUN {{ mount_rust_cache -}} cargo build --features ${DB} --release{{ package_arch_target_param }}
|
RUN {{ mount_rust_cache -}} cargo build --features ${DB} --release{{ package_arch_target_param }}
|
||||||
|
|
||||||
{% if "buildkit" in target_file %}
|
|
||||||
# Add the `cap_net_bind_service` capability to allow listening on
|
|
||||||
# privileged (< 1024) ports even when running as a non-root user.
|
|
||||||
# This is only done if building with BuildKit; with the legacy
|
|
||||||
# builder, the `COPY` instruction doesn't carry over capabilities.
|
|
||||||
{% if package_arch_target is defined %}
|
|
||||||
RUN setcap cap_net_bind_service=+ep target/{{ package_arch_target }}/release/vaultwarden
|
|
||||||
{% else %}
|
|
||||||
RUN setcap cap_net_bind_service=+ep target/release/vaultwarden
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
# Create a new stage with a minimal image
|
# Create a new stage with a minimal image
|
||||||
# because we already have a binary built
|
# because we already have a binary built
|
||||||
@@ -223,13 +215,6 @@ RUN mkdir /data \
|
|||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if "armv6" in target_file and "alpine" not in target_file %}
|
|
||||||
# In the Balena Bullseye images for armv6/rpi-debian there is a missing symlink.
|
|
||||||
# This symlink was there in the buster images, and for some reason this is needed.
|
|
||||||
RUN ln -v -s /lib/ld-linux-armhf.so.3 /lib/ld-linux.so.3
|
|
||||||
|
|
||||||
{% endif -%}
|
|
||||||
|
|
||||||
{% if "amd64" not in target_file %}
|
{% if "amd64" not in target_file %}
|
||||||
RUN [ "cross-build-end" ]
|
RUN [ "cross-build-end" ]
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
# This file was generated using a Jinja2 template.
|
# This file was generated using a Jinja2 template.
|
||||||
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
||||||
|
|
||||||
# Using multistage build:
|
# Using multistage build:
|
||||||
# https://docs.docker.com/develop/develop-images/multistage-build/
|
# https://docs.docker.com/develop/develop-images/multistage-build/
|
||||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
# https://whitfin.io/speeding-up-rust-docker-builds/
|
||||||
@@ -16,18 +15,18 @@
|
|||||||
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
# - 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.
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
# - From the command line:
|
# - From the command line:
|
||||||
# $ docker pull vaultwarden/web-vault:v2023.3.0b
|
# $ docker pull docker.io/vaultwarden/web-vault:v2023.7.1
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b
|
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2023.7.1
|
||||||
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee]
|
# [docker.io/vaultwarden/web-vault@sha256:b306f38fe0d54fa3d79059a737f8e1803da44ddc5f273c2aecdd6a4886211b0f]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee
|
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:b306f38fe0d54fa3d79059a737f8e1803da44ddc5f273c2aecdd6a4886211b0f
|
||||||
# [vaultwarden/web-vault:v2023.3.0b]
|
# [docker.io/vaultwarden/web-vault:v2023.7.1]
|
||||||
#
|
#
|
||||||
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault
|
FROM docker.io/vaultwarden/web-vault@sha256:b306f38fe0d54fa3d79059a737f8e1803da44ddc5f273c2aecdd6a4886211b0f as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM rust:1.68.1-bullseye as build
|
FROM docker.io/library/rust:1.71.1-bookworm as build
|
||||||
|
|
||||||
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
||||||
ENV DEBIAN_FRONTEND=noninteractive \
|
ENV DEBIAN_FRONTEND=noninteractive \
|
||||||
@@ -35,6 +34,7 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
|||||||
TZ=UTC \
|
TZ=UTC \
|
||||||
TERM=xterm-256color \
|
TERM=xterm-256color \
|
||||||
CARGO_HOME="/root/.cargo" \
|
CARGO_HOME="/root/.cargo" \
|
||||||
|
REGISTRIES_CRATES_IO_PROTOCOL=sparse \
|
||||||
USER="root"
|
USER="root"
|
||||||
|
|
||||||
# Create CARGO_HOME folder and don't download rust docs
|
# Create CARGO_HOME folder and don't download rust docs
|
||||||
@@ -45,7 +45,6 @@ RUN mkdir -pv "${CARGO_HOME}" \
|
|||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y \
|
&& apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
libcap2-bin \
|
|
||||||
libmariadb-dev \
|
libmariadb-dev \
|
||||||
libpq-dev
|
libpq-dev
|
||||||
|
|
||||||
@@ -79,11 +78,10 @@ RUN touch src/main.rs
|
|||||||
# your actual source files being built
|
# your actual source files being built
|
||||||
RUN cargo build --features ${DB} --release
|
RUN cargo build --features ${DB} --release
|
||||||
|
|
||||||
|
|
||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
# Create a new stage with a minimal image
|
# Create a new stage with a minimal image
|
||||||
# because we already have a binary built
|
# because we already have a binary built
|
||||||
FROM debian:bullseye-slim
|
FROM docker.io/library/debian:bookworm-slim
|
||||||
|
|
||||||
ENV ROCKET_PROFILE="release" \
|
ENV ROCKET_PROFILE="release" \
|
||||||
ROCKET_ADDRESS=0.0.0.0 \
|
ROCKET_ADDRESS=0.0.0.0 \
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
# This file was generated using a Jinja2 template.
|
# This file was generated using a Jinja2 template.
|
||||||
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
||||||
|
|
||||||
# Using multistage build:
|
# Using multistage build:
|
||||||
# https://docs.docker.com/develop/develop-images/multistage-build/
|
# https://docs.docker.com/develop/develop-images/multistage-build/
|
||||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
# https://whitfin.io/speeding-up-rust-docker-builds/
|
||||||
@@ -16,18 +15,18 @@
|
|||||||
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
# - 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.
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
# - From the command line:
|
# - From the command line:
|
||||||
# $ docker pull vaultwarden/web-vault:v2023.3.0b
|
# $ docker pull docker.io/vaultwarden/web-vault:v2023.7.1
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b
|
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2023.7.1
|
||||||
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee]
|
# [docker.io/vaultwarden/web-vault@sha256:b306f38fe0d54fa3d79059a737f8e1803da44ddc5f273c2aecdd6a4886211b0f]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee
|
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:b306f38fe0d54fa3d79059a737f8e1803da44ddc5f273c2aecdd6a4886211b0f
|
||||||
# [vaultwarden/web-vault:v2023.3.0b]
|
# [docker.io/vaultwarden/web-vault:v2023.7.1]
|
||||||
#
|
#
|
||||||
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault
|
FROM docker.io/vaultwarden/web-vault@sha256:b306f38fe0d54fa3d79059a737f8e1803da44ddc5f273c2aecdd6a4886211b0f as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM blackdex/rust-musl:x86_64-musl-stable-1.68.1 as build
|
FROM docker.io/blackdex/rust-musl:x86_64-musl-stable-1.71.1-openssl3 as build
|
||||||
|
|
||||||
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
||||||
ENV DEBIAN_FRONTEND=noninteractive \
|
ENV DEBIAN_FRONTEND=noninteractive \
|
||||||
@@ -35,12 +34,16 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
|||||||
TZ=UTC \
|
TZ=UTC \
|
||||||
TERM=xterm-256color \
|
TERM=xterm-256color \
|
||||||
CARGO_HOME="/root/.cargo" \
|
CARGO_HOME="/root/.cargo" \
|
||||||
|
REGISTRIES_CRATES_IO_PROTOCOL=sparse \
|
||||||
USER="root"
|
USER="root"
|
||||||
|
|
||||||
# Create CARGO_HOME folder and don't download rust docs
|
# Create CARGO_HOME folder and don't download rust docs
|
||||||
RUN mkdir -pv "${CARGO_HOME}" \
|
RUN mkdir -pv "${CARGO_HOME}" \
|
||||||
&& rustup set profile minimal
|
&& rustup set profile minimal
|
||||||
|
|
||||||
|
# Use PostgreSQL v15 during Alpine/MUSL builds instead of the default v11
|
||||||
|
# Debian Bookworm already contains libpq v15
|
||||||
|
ENV PQ_LIB_DIR="/usr/local/musl/pq15/lib"
|
||||||
|
|
||||||
# Creates a dummy project used to grab dependencies
|
# Creates a dummy project used to grab dependencies
|
||||||
RUN USER=root cargo new --bin /app
|
RUN USER=root cargo new --bin /app
|
||||||
@@ -74,11 +77,10 @@ RUN touch src/main.rs
|
|||||||
# your actual source files being built
|
# your actual source files being built
|
||||||
RUN cargo build --features ${DB} --release --target=x86_64-unknown-linux-musl
|
RUN cargo build --features ${DB} --release --target=x86_64-unknown-linux-musl
|
||||||
|
|
||||||
|
|
||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
# Create a new stage with a minimal image
|
# Create a new stage with a minimal image
|
||||||
# because we already have a binary built
|
# because we already have a binary built
|
||||||
FROM alpine:3.17
|
FROM docker.io/library/alpine:3.17
|
||||||
|
|
||||||
ENV ROCKET_PROFILE="release" \
|
ENV ROCKET_PROFILE="release" \
|
||||||
ROCKET_ADDRESS=0.0.0.0 \
|
ROCKET_ADDRESS=0.0.0.0 \
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
# This file was generated using a Jinja2 template.
|
# This file was generated using a Jinja2 template.
|
||||||
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
||||||
|
|
||||||
# Using multistage build:
|
# Using multistage build:
|
||||||
# https://docs.docker.com/develop/develop-images/multistage-build/
|
# https://docs.docker.com/develop/develop-images/multistage-build/
|
||||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
# https://whitfin.io/speeding-up-rust-docker-builds/
|
||||||
@@ -16,18 +15,18 @@
|
|||||||
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
# - 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.
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
# - From the command line:
|
# - From the command line:
|
||||||
# $ docker pull vaultwarden/web-vault:v2023.3.0b
|
# $ docker pull docker.io/vaultwarden/web-vault:v2023.7.1
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b
|
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2023.7.1
|
||||||
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee]
|
# [docker.io/vaultwarden/web-vault@sha256:b306f38fe0d54fa3d79059a737f8e1803da44ddc5f273c2aecdd6a4886211b0f]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee
|
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:b306f38fe0d54fa3d79059a737f8e1803da44ddc5f273c2aecdd6a4886211b0f
|
||||||
# [vaultwarden/web-vault:v2023.3.0b]
|
# [docker.io/vaultwarden/web-vault:v2023.7.1]
|
||||||
#
|
#
|
||||||
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault
|
FROM docker.io/vaultwarden/web-vault@sha256:b306f38fe0d54fa3d79059a737f8e1803da44ddc5f273c2aecdd6a4886211b0f as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM rust:1.68.1-bullseye as build
|
FROM docker.io/library/rust:1.71.1-bookworm as build
|
||||||
|
|
||||||
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
||||||
ENV DEBIAN_FRONTEND=noninteractive \
|
ENV DEBIAN_FRONTEND=noninteractive \
|
||||||
@@ -35,6 +34,7 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
|||||||
TZ=UTC \
|
TZ=UTC \
|
||||||
TERM=xterm-256color \
|
TERM=xterm-256color \
|
||||||
CARGO_HOME="/root/.cargo" \
|
CARGO_HOME="/root/.cargo" \
|
||||||
|
REGISTRIES_CRATES_IO_PROTOCOL=sparse \
|
||||||
USER="root"
|
USER="root"
|
||||||
|
|
||||||
# Create CARGO_HOME folder and don't download rust docs
|
# Create CARGO_HOME folder and don't download rust docs
|
||||||
@@ -45,7 +45,6 @@ RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.
|
|||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y \
|
&& apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
libcap2-bin \
|
|
||||||
libmariadb-dev \
|
libmariadb-dev \
|
||||||
libpq-dev
|
libpq-dev
|
||||||
|
|
||||||
@@ -79,16 +78,10 @@ RUN touch src/main.rs
|
|||||||
# your actual source files being built
|
# your actual source files being built
|
||||||
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry cargo build --features ${DB} --release
|
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry cargo build --features ${DB} --release
|
||||||
|
|
||||||
# Add the `cap_net_bind_service` capability to allow listening on
|
|
||||||
# privileged (< 1024) ports even when running as a non-root user.
|
|
||||||
# This is only done if building with BuildKit; with the legacy
|
|
||||||
# builder, the `COPY` instruction doesn't carry over capabilities.
|
|
||||||
RUN setcap cap_net_bind_service=+ep target/release/vaultwarden
|
|
||||||
|
|
||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
# Create a new stage with a minimal image
|
# Create a new stage with a minimal image
|
||||||
# because we already have a binary built
|
# because we already have a binary built
|
||||||
FROM debian:bullseye-slim
|
FROM docker.io/library/debian:bookworm-slim
|
||||||
|
|
||||||
ENV ROCKET_PROFILE="release" \
|
ENV ROCKET_PROFILE="release" \
|
||||||
ROCKET_ADDRESS=0.0.0.0 \
|
ROCKET_ADDRESS=0.0.0.0 \
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
# This file was generated using a Jinja2 template.
|
# This file was generated using a Jinja2 template.
|
||||||
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
||||||
|
|
||||||
# Using multistage build:
|
# Using multistage build:
|
||||||
# https://docs.docker.com/develop/develop-images/multistage-build/
|
# https://docs.docker.com/develop/develop-images/multistage-build/
|
||||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
# https://whitfin.io/speeding-up-rust-docker-builds/
|
||||||
@@ -16,18 +15,18 @@
|
|||||||
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
# - 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.
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
# - From the command line:
|
# - From the command line:
|
||||||
# $ docker pull vaultwarden/web-vault:v2023.3.0b
|
# $ docker pull docker.io/vaultwarden/web-vault:v2023.7.1
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b
|
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2023.7.1
|
||||||
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee]
|
# [docker.io/vaultwarden/web-vault@sha256:b306f38fe0d54fa3d79059a737f8e1803da44ddc5f273c2aecdd6a4886211b0f]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee
|
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:b306f38fe0d54fa3d79059a737f8e1803da44ddc5f273c2aecdd6a4886211b0f
|
||||||
# [vaultwarden/web-vault:v2023.3.0b]
|
# [docker.io/vaultwarden/web-vault:v2023.7.1]
|
||||||
#
|
#
|
||||||
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault
|
FROM docker.io/vaultwarden/web-vault@sha256:b306f38fe0d54fa3d79059a737f8e1803da44ddc5f273c2aecdd6a4886211b0f as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM blackdex/rust-musl:x86_64-musl-stable-1.68.1 as build
|
FROM docker.io/blackdex/rust-musl:x86_64-musl-stable-1.71.1-openssl3 as build
|
||||||
|
|
||||||
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
||||||
ENV DEBIAN_FRONTEND=noninteractive \
|
ENV DEBIAN_FRONTEND=noninteractive \
|
||||||
@@ -35,12 +34,16 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
|||||||
TZ=UTC \
|
TZ=UTC \
|
||||||
TERM=xterm-256color \
|
TERM=xterm-256color \
|
||||||
CARGO_HOME="/root/.cargo" \
|
CARGO_HOME="/root/.cargo" \
|
||||||
|
REGISTRIES_CRATES_IO_PROTOCOL=sparse \
|
||||||
USER="root"
|
USER="root"
|
||||||
|
|
||||||
# Create CARGO_HOME folder and don't download rust docs
|
# Create CARGO_HOME folder and don't download rust docs
|
||||||
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry mkdir -pv "${CARGO_HOME}" \
|
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry mkdir -pv "${CARGO_HOME}" \
|
||||||
&& rustup set profile minimal
|
&& rustup set profile minimal
|
||||||
|
|
||||||
|
# Use PostgreSQL v15 during Alpine/MUSL builds instead of the default v11
|
||||||
|
# Debian Bookworm already contains libpq v15
|
||||||
|
ENV PQ_LIB_DIR="/usr/local/musl/pq15/lib"
|
||||||
|
|
||||||
# Creates a dummy project used to grab dependencies
|
# Creates a dummy project used to grab dependencies
|
||||||
RUN USER=root cargo new --bin /app
|
RUN USER=root cargo new --bin /app
|
||||||
@@ -74,16 +77,10 @@ RUN touch src/main.rs
|
|||||||
# your actual source files being built
|
# your actual source files being built
|
||||||
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry cargo build --features ${DB} --release --target=x86_64-unknown-linux-musl
|
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry cargo build --features ${DB} --release --target=x86_64-unknown-linux-musl
|
||||||
|
|
||||||
# Add the `cap_net_bind_service` capability to allow listening on
|
|
||||||
# privileged (< 1024) ports even when running as a non-root user.
|
|
||||||
# This is only done if building with BuildKit; with the legacy
|
|
||||||
# builder, the `COPY` instruction doesn't carry over capabilities.
|
|
||||||
RUN setcap cap_net_bind_service=+ep target/x86_64-unknown-linux-musl/release/vaultwarden
|
|
||||||
|
|
||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
# Create a new stage with a minimal image
|
# Create a new stage with a minimal image
|
||||||
# because we already have a binary built
|
# because we already have a binary built
|
||||||
FROM alpine:3.17
|
FROM docker.io/library/alpine:3.17
|
||||||
|
|
||||||
ENV ROCKET_PROFILE="release" \
|
ENV ROCKET_PROFILE="release" \
|
||||||
ROCKET_ADDRESS=0.0.0.0 \
|
ROCKET_ADDRESS=0.0.0.0 \
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
# This file was generated using a Jinja2 template.
|
# This file was generated using a Jinja2 template.
|
||||||
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
||||||
|
|
||||||
# Using multistage build:
|
# Using multistage build:
|
||||||
# https://docs.docker.com/develop/develop-images/multistage-build/
|
# https://docs.docker.com/develop/develop-images/multistage-build/
|
||||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
# https://whitfin.io/speeding-up-rust-docker-builds/
|
||||||
@@ -16,18 +15,18 @@
|
|||||||
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
# - 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.
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
# - From the command line:
|
# - From the command line:
|
||||||
# $ docker pull vaultwarden/web-vault:v2023.3.0b
|
# $ docker pull docker.io/vaultwarden/web-vault:v2023.7.1
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b
|
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2023.7.1
|
||||||
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee]
|
# [docker.io/vaultwarden/web-vault@sha256:b306f38fe0d54fa3d79059a737f8e1803da44ddc5f273c2aecdd6a4886211b0f]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee
|
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:b306f38fe0d54fa3d79059a737f8e1803da44ddc5f273c2aecdd6a4886211b0f
|
||||||
# [vaultwarden/web-vault:v2023.3.0b]
|
# [docker.io/vaultwarden/web-vault:v2023.7.1]
|
||||||
#
|
#
|
||||||
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault
|
FROM docker.io/vaultwarden/web-vault@sha256:b306f38fe0d54fa3d79059a737f8e1803da44ddc5f273c2aecdd6a4886211b0f as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM rust:1.68.1-bullseye as build
|
FROM docker.io/library/rust:1.71.1-bookworm as build
|
||||||
|
|
||||||
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
||||||
ENV DEBIAN_FRONTEND=noninteractive \
|
ENV DEBIAN_FRONTEND=noninteractive \
|
||||||
@@ -35,6 +34,7 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
|||||||
TZ=UTC \
|
TZ=UTC \
|
||||||
TERM=xterm-256color \
|
TERM=xterm-256color \
|
||||||
CARGO_HOME="/root/.cargo" \
|
CARGO_HOME="/root/.cargo" \
|
||||||
|
REGISTRIES_CRATES_IO_PROTOCOL=sparse \
|
||||||
USER="root"
|
USER="root"
|
||||||
|
|
||||||
# Create CARGO_HOME folder and don't download rust docs
|
# Create CARGO_HOME folder and don't download rust docs
|
||||||
@@ -48,7 +48,6 @@ RUN dpkg --add-architecture arm64 \
|
|||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
gcc-aarch64-linux-gnu \
|
gcc-aarch64-linux-gnu \
|
||||||
libc6-dev:arm64 \
|
libc6-dev:arm64 \
|
||||||
libcap2-bin \
|
|
||||||
libmariadb-dev:arm64 \
|
libmariadb-dev:arm64 \
|
||||||
libmariadb-dev-compat:arm64 \
|
libmariadb-dev-compat:arm64 \
|
||||||
libmariadb3:arm64 \
|
libmariadb3:arm64 \
|
||||||
@@ -98,11 +97,10 @@ RUN touch src/main.rs
|
|||||||
# your actual source files being built
|
# your actual source files being built
|
||||||
RUN cargo build --features ${DB} --release --target=aarch64-unknown-linux-gnu
|
RUN cargo build --features ${DB} --release --target=aarch64-unknown-linux-gnu
|
||||||
|
|
||||||
|
|
||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
# Create a new stage with a minimal image
|
# Create a new stage with a minimal image
|
||||||
# because we already have a binary built
|
# because we already have a binary built
|
||||||
FROM balenalib/aarch64-debian:bullseye
|
FROM docker.io/balenalib/aarch64-debian:bookworm
|
||||||
|
|
||||||
ENV ROCKET_PROFILE="release" \
|
ENV ROCKET_PROFILE="release" \
|
||||||
ROCKET_ADDRESS=0.0.0.0 \
|
ROCKET_ADDRESS=0.0.0.0 \
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
# This file was generated using a Jinja2 template.
|
# This file was generated using a Jinja2 template.
|
||||||
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
||||||
|
|
||||||
# Using multistage build:
|
# Using multistage build:
|
||||||
# https://docs.docker.com/develop/develop-images/multistage-build/
|
# https://docs.docker.com/develop/develop-images/multistage-build/
|
||||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
# https://whitfin.io/speeding-up-rust-docker-builds/
|
||||||
@@ -16,18 +15,18 @@
|
|||||||
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
# - 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.
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
# - From the command line:
|
# - From the command line:
|
||||||
# $ docker pull vaultwarden/web-vault:v2023.3.0b
|
# $ docker pull docker.io/vaultwarden/web-vault:v2023.7.1
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b
|
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2023.7.1
|
||||||
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee]
|
# [docker.io/vaultwarden/web-vault@sha256:b306f38fe0d54fa3d79059a737f8e1803da44ddc5f273c2aecdd6a4886211b0f]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee
|
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:b306f38fe0d54fa3d79059a737f8e1803da44ddc5f273c2aecdd6a4886211b0f
|
||||||
# [vaultwarden/web-vault:v2023.3.0b]
|
# [docker.io/vaultwarden/web-vault:v2023.7.1]
|
||||||
#
|
#
|
||||||
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault
|
FROM docker.io/vaultwarden/web-vault@sha256:b306f38fe0d54fa3d79059a737f8e1803da44ddc5f273c2aecdd6a4886211b0f as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM blackdex/rust-musl:aarch64-musl-stable-1.68.1 as build
|
FROM docker.io/blackdex/rust-musl:aarch64-musl-stable-1.71.1-openssl3 as build
|
||||||
|
|
||||||
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
||||||
ENV DEBIAN_FRONTEND=noninteractive \
|
ENV DEBIAN_FRONTEND=noninteractive \
|
||||||
@@ -35,12 +34,16 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
|||||||
TZ=UTC \
|
TZ=UTC \
|
||||||
TERM=xterm-256color \
|
TERM=xterm-256color \
|
||||||
CARGO_HOME="/root/.cargo" \
|
CARGO_HOME="/root/.cargo" \
|
||||||
|
REGISTRIES_CRATES_IO_PROTOCOL=sparse \
|
||||||
USER="root"
|
USER="root"
|
||||||
|
|
||||||
# Create CARGO_HOME folder and don't download rust docs
|
# Create CARGO_HOME folder and don't download rust docs
|
||||||
RUN mkdir -pv "${CARGO_HOME}" \
|
RUN mkdir -pv "${CARGO_HOME}" \
|
||||||
&& rustup set profile minimal
|
&& rustup set profile minimal
|
||||||
|
|
||||||
|
# Use PostgreSQL v15 during Alpine/MUSL builds instead of the default v11
|
||||||
|
# Debian Bookworm already contains libpq v15
|
||||||
|
ENV PQ_LIB_DIR="/usr/local/musl/pq15/lib"
|
||||||
|
|
||||||
# Creates a dummy project used to grab dependencies
|
# Creates a dummy project used to grab dependencies
|
||||||
RUN USER=root cargo new --bin /app
|
RUN USER=root cargo new --bin /app
|
||||||
@@ -74,11 +77,10 @@ RUN touch src/main.rs
|
|||||||
# your actual source files being built
|
# your actual source files being built
|
||||||
RUN cargo build --features ${DB} --release --target=aarch64-unknown-linux-musl
|
RUN cargo build --features ${DB} --release --target=aarch64-unknown-linux-musl
|
||||||
|
|
||||||
|
|
||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
# Create a new stage with a minimal image
|
# Create a new stage with a minimal image
|
||||||
# because we already have a binary built
|
# 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" \
|
ENV ROCKET_PROFILE="release" \
|
||||||
ROCKET_ADDRESS=0.0.0.0 \
|
ROCKET_ADDRESS=0.0.0.0 \
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
# This file was generated using a Jinja2 template.
|
# This file was generated using a Jinja2 template.
|
||||||
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
||||||
|
|
||||||
# Using multistage build:
|
# Using multistage build:
|
||||||
# https://docs.docker.com/develop/develop-images/multistage-build/
|
# https://docs.docker.com/develop/develop-images/multistage-build/
|
||||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
# https://whitfin.io/speeding-up-rust-docker-builds/
|
||||||
@@ -16,18 +15,18 @@
|
|||||||
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
# - 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.
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
# - From the command line:
|
# - From the command line:
|
||||||
# $ docker pull vaultwarden/web-vault:v2023.3.0b
|
# $ docker pull docker.io/vaultwarden/web-vault:v2023.7.1
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b
|
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2023.7.1
|
||||||
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee]
|
# [docker.io/vaultwarden/web-vault@sha256:b306f38fe0d54fa3d79059a737f8e1803da44ddc5f273c2aecdd6a4886211b0f]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee
|
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:b306f38fe0d54fa3d79059a737f8e1803da44ddc5f273c2aecdd6a4886211b0f
|
||||||
# [vaultwarden/web-vault:v2023.3.0b]
|
# [docker.io/vaultwarden/web-vault:v2023.7.1]
|
||||||
#
|
#
|
||||||
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault
|
FROM docker.io/vaultwarden/web-vault@sha256:b306f38fe0d54fa3d79059a737f8e1803da44ddc5f273c2aecdd6a4886211b0f as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM rust:1.68.1-bullseye as build
|
FROM docker.io/library/rust:1.71.1-bookworm as build
|
||||||
|
|
||||||
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
||||||
ENV DEBIAN_FRONTEND=noninteractive \
|
ENV DEBIAN_FRONTEND=noninteractive \
|
||||||
@@ -35,6 +34,7 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
|||||||
TZ=UTC \
|
TZ=UTC \
|
||||||
TERM=xterm-256color \
|
TERM=xterm-256color \
|
||||||
CARGO_HOME="/root/.cargo" \
|
CARGO_HOME="/root/.cargo" \
|
||||||
|
REGISTRIES_CRATES_IO_PROTOCOL=sparse \
|
||||||
USER="root"
|
USER="root"
|
||||||
|
|
||||||
# Create CARGO_HOME folder and don't download rust docs
|
# Create CARGO_HOME folder and don't download rust docs
|
||||||
@@ -42,13 +42,12 @@ RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.
|
|||||||
&& rustup set profile minimal
|
&& rustup set profile minimal
|
||||||
|
|
||||||
# Install build dependencies for the arm64 architecture
|
# Install build dependencies for the arm64 architecture
|
||||||
RUN dpkg --add-architecture arm64 \
|
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry dpkg --add-architecture arm64 \
|
||||||
&& apt-get update \
|
&& apt-get update \
|
||||||
&& apt-get install -y \
|
&& apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
gcc-aarch64-linux-gnu \
|
gcc-aarch64-linux-gnu \
|
||||||
libc6-dev:arm64 \
|
libc6-dev:arm64 \
|
||||||
libcap2-bin \
|
|
||||||
libmariadb-dev:arm64 \
|
libmariadb-dev:arm64 \
|
||||||
libmariadb-dev-compat:arm64 \
|
libmariadb-dev-compat:arm64 \
|
||||||
libmariadb3:arm64 \
|
libmariadb3:arm64 \
|
||||||
@@ -98,16 +97,10 @@ RUN touch src/main.rs
|
|||||||
# your actual source files being built
|
# your actual source files being built
|
||||||
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry cargo build --features ${DB} --release --target=aarch64-unknown-linux-gnu
|
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry cargo build --features ${DB} --release --target=aarch64-unknown-linux-gnu
|
||||||
|
|
||||||
# Add the `cap_net_bind_service` capability to allow listening on
|
|
||||||
# privileged (< 1024) ports even when running as a non-root user.
|
|
||||||
# This is only done if building with BuildKit; with the legacy
|
|
||||||
# builder, the `COPY` instruction doesn't carry over capabilities.
|
|
||||||
RUN setcap cap_net_bind_service=+ep target/aarch64-unknown-linux-gnu/release/vaultwarden
|
|
||||||
|
|
||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
# Create a new stage with a minimal image
|
# Create a new stage with a minimal image
|
||||||
# because we already have a binary built
|
# because we already have a binary built
|
||||||
FROM balenalib/aarch64-debian:bullseye
|
FROM docker.io/balenalib/aarch64-debian:bookworm
|
||||||
|
|
||||||
ENV ROCKET_PROFILE="release" \
|
ENV ROCKET_PROFILE="release" \
|
||||||
ROCKET_ADDRESS=0.0.0.0 \
|
ROCKET_ADDRESS=0.0.0.0 \
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
# This file was generated using a Jinja2 template.
|
# This file was generated using a Jinja2 template.
|
||||||
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
||||||
|
|
||||||
# Using multistage build:
|
# Using multistage build:
|
||||||
# https://docs.docker.com/develop/develop-images/multistage-build/
|
# https://docs.docker.com/develop/develop-images/multistage-build/
|
||||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
# https://whitfin.io/speeding-up-rust-docker-builds/
|
||||||
@@ -16,18 +15,18 @@
|
|||||||
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
# - 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.
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
# - From the command line:
|
# - From the command line:
|
||||||
# $ docker pull vaultwarden/web-vault:v2023.3.0b
|
# $ docker pull docker.io/vaultwarden/web-vault:v2023.7.1
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b
|
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2023.7.1
|
||||||
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee]
|
# [docker.io/vaultwarden/web-vault@sha256:b306f38fe0d54fa3d79059a737f8e1803da44ddc5f273c2aecdd6a4886211b0f]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee
|
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:b306f38fe0d54fa3d79059a737f8e1803da44ddc5f273c2aecdd6a4886211b0f
|
||||||
# [vaultwarden/web-vault:v2023.3.0b]
|
# [docker.io/vaultwarden/web-vault:v2023.7.1]
|
||||||
#
|
#
|
||||||
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault
|
FROM docker.io/vaultwarden/web-vault@sha256:b306f38fe0d54fa3d79059a737f8e1803da44ddc5f273c2aecdd6a4886211b0f as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM blackdex/rust-musl:aarch64-musl-stable-1.68.1 as build
|
FROM docker.io/blackdex/rust-musl:aarch64-musl-stable-1.71.1-openssl3 as build
|
||||||
|
|
||||||
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
||||||
ENV DEBIAN_FRONTEND=noninteractive \
|
ENV DEBIAN_FRONTEND=noninteractive \
|
||||||
@@ -35,12 +34,16 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
|||||||
TZ=UTC \
|
TZ=UTC \
|
||||||
TERM=xterm-256color \
|
TERM=xterm-256color \
|
||||||
CARGO_HOME="/root/.cargo" \
|
CARGO_HOME="/root/.cargo" \
|
||||||
|
REGISTRIES_CRATES_IO_PROTOCOL=sparse \
|
||||||
USER="root"
|
USER="root"
|
||||||
|
|
||||||
# Create CARGO_HOME folder and don't download rust docs
|
# Create CARGO_HOME folder and don't download rust docs
|
||||||
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry mkdir -pv "${CARGO_HOME}" \
|
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry mkdir -pv "${CARGO_HOME}" \
|
||||||
&& rustup set profile minimal
|
&& rustup set profile minimal
|
||||||
|
|
||||||
|
# Use PostgreSQL v15 during Alpine/MUSL builds instead of the default v11
|
||||||
|
# Debian Bookworm already contains libpq v15
|
||||||
|
ENV PQ_LIB_DIR="/usr/local/musl/pq15/lib"
|
||||||
|
|
||||||
# Creates a dummy project used to grab dependencies
|
# Creates a dummy project used to grab dependencies
|
||||||
RUN USER=root cargo new --bin /app
|
RUN USER=root cargo new --bin /app
|
||||||
@@ -74,16 +77,10 @@ RUN touch src/main.rs
|
|||||||
# your actual source files being built
|
# your actual source files being built
|
||||||
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry cargo build --features ${DB} --release --target=aarch64-unknown-linux-musl
|
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry cargo build --features ${DB} --release --target=aarch64-unknown-linux-musl
|
||||||
|
|
||||||
# Add the `cap_net_bind_service` capability to allow listening on
|
|
||||||
# privileged (< 1024) ports even when running as a non-root user.
|
|
||||||
# This is only done if building with BuildKit; with the legacy
|
|
||||||
# builder, the `COPY` instruction doesn't carry over capabilities.
|
|
||||||
RUN setcap cap_net_bind_service=+ep target/aarch64-unknown-linux-musl/release/vaultwarden
|
|
||||||
|
|
||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
# Create a new stage with a minimal image
|
# Create a new stage with a minimal image
|
||||||
# because we already have a binary built
|
# 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" \
|
ENV ROCKET_PROFILE="release" \
|
||||||
ROCKET_ADDRESS=0.0.0.0 \
|
ROCKET_ADDRESS=0.0.0.0 \
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
# This file was generated using a Jinja2 template.
|
# This file was generated using a Jinja2 template.
|
||||||
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
||||||
|
|
||||||
# Using multistage build:
|
# Using multistage build:
|
||||||
# https://docs.docker.com/develop/develop-images/multistage-build/
|
# https://docs.docker.com/develop/develop-images/multistage-build/
|
||||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
# https://whitfin.io/speeding-up-rust-docker-builds/
|
||||||
@@ -16,18 +15,18 @@
|
|||||||
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
# - 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.
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
# - From the command line:
|
# - From the command line:
|
||||||
# $ docker pull vaultwarden/web-vault:v2023.3.0b
|
# $ docker pull docker.io/vaultwarden/web-vault:v2023.7.1
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b
|
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2023.7.1
|
||||||
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee]
|
# [docker.io/vaultwarden/web-vault@sha256:b306f38fe0d54fa3d79059a737f8e1803da44ddc5f273c2aecdd6a4886211b0f]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee
|
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:b306f38fe0d54fa3d79059a737f8e1803da44ddc5f273c2aecdd6a4886211b0f
|
||||||
# [vaultwarden/web-vault:v2023.3.0b]
|
# [docker.io/vaultwarden/web-vault:v2023.7.1]
|
||||||
#
|
#
|
||||||
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault
|
FROM docker.io/vaultwarden/web-vault@sha256:b306f38fe0d54fa3d79059a737f8e1803da44ddc5f273c2aecdd6a4886211b0f as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM rust:1.68.1-bullseye as build
|
FROM docker.io/library/rust:1.71.1-bookworm as build
|
||||||
|
|
||||||
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
||||||
ENV DEBIAN_FRONTEND=noninteractive \
|
ENV DEBIAN_FRONTEND=noninteractive \
|
||||||
@@ -35,6 +34,7 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
|||||||
TZ=UTC \
|
TZ=UTC \
|
||||||
TERM=xterm-256color \
|
TERM=xterm-256color \
|
||||||
CARGO_HOME="/root/.cargo" \
|
CARGO_HOME="/root/.cargo" \
|
||||||
|
REGISTRIES_CRATES_IO_PROTOCOL=sparse \
|
||||||
USER="root"
|
USER="root"
|
||||||
|
|
||||||
# Create CARGO_HOME folder and don't download rust docs
|
# Create CARGO_HOME folder and don't download rust docs
|
||||||
@@ -48,7 +48,6 @@ RUN dpkg --add-architecture armel \
|
|||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
gcc-arm-linux-gnueabi \
|
gcc-arm-linux-gnueabi \
|
||||||
libc6-dev:armel \
|
libc6-dev:armel \
|
||||||
libcap2-bin \
|
|
||||||
libmariadb-dev:armel \
|
libmariadb-dev:armel \
|
||||||
libmariadb-dev-compat:armel \
|
libmariadb-dev-compat:armel \
|
||||||
libmariadb3:armel \
|
libmariadb3:armel \
|
||||||
@@ -98,11 +97,10 @@ RUN touch src/main.rs
|
|||||||
# your actual source files being built
|
# your actual source files being built
|
||||||
RUN cargo build --features ${DB} --release --target=arm-unknown-linux-gnueabi
|
RUN cargo build --features ${DB} --release --target=arm-unknown-linux-gnueabi
|
||||||
|
|
||||||
|
|
||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
# Create a new stage with a minimal image
|
# Create a new stage with a minimal image
|
||||||
# because we already have a binary built
|
# because we already have a binary built
|
||||||
FROM balenalib/rpi-debian:bullseye
|
FROM docker.io/balenalib/rpi-debian:bookworm
|
||||||
|
|
||||||
ENV ROCKET_PROFILE="release" \
|
ENV ROCKET_PROFILE="release" \
|
||||||
ROCKET_ADDRESS=0.0.0.0 \
|
ROCKET_ADDRESS=0.0.0.0 \
|
||||||
@@ -122,10 +120,6 @@ RUN mkdir /data \
|
|||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# In the Balena Bullseye images for armv6/rpi-debian there is a missing symlink.
|
|
||||||
# This symlink was there in the buster images, and for some reason this is needed.
|
|
||||||
RUN ln -v -s /lib/ld-linux-armhf.so.3 /lib/ld-linux.so.3
|
|
||||||
|
|
||||||
RUN [ "cross-build-end" ]
|
RUN [ "cross-build-end" ]
|
||||||
|
|
||||||
VOLUME /data
|
VOLUME /data
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
# This file was generated using a Jinja2 template.
|
# This file was generated using a Jinja2 template.
|
||||||
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
||||||
|
|
||||||
# Using multistage build:
|
# Using multistage build:
|
||||||
# https://docs.docker.com/develop/develop-images/multistage-build/
|
# https://docs.docker.com/develop/develop-images/multistage-build/
|
||||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
# https://whitfin.io/speeding-up-rust-docker-builds/
|
||||||
@@ -16,18 +15,18 @@
|
|||||||
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
# - 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.
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
# - From the command line:
|
# - From the command line:
|
||||||
# $ docker pull vaultwarden/web-vault:v2023.3.0b
|
# $ docker pull docker.io/vaultwarden/web-vault:v2023.7.1
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b
|
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2023.7.1
|
||||||
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee]
|
# [docker.io/vaultwarden/web-vault@sha256:b306f38fe0d54fa3d79059a737f8e1803da44ddc5f273c2aecdd6a4886211b0f]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee
|
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:b306f38fe0d54fa3d79059a737f8e1803da44ddc5f273c2aecdd6a4886211b0f
|
||||||
# [vaultwarden/web-vault:v2023.3.0b]
|
# [docker.io/vaultwarden/web-vault:v2023.7.1]
|
||||||
#
|
#
|
||||||
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault
|
FROM docker.io/vaultwarden/web-vault@sha256:b306f38fe0d54fa3d79059a737f8e1803da44ddc5f273c2aecdd6a4886211b0f as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM blackdex/rust-musl:arm-musleabi-stable-1.68.1 as build
|
FROM docker.io/blackdex/rust-musl:arm-musleabi-stable-1.71.1-openssl3 as build
|
||||||
|
|
||||||
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
||||||
ENV DEBIAN_FRONTEND=noninteractive \
|
ENV DEBIAN_FRONTEND=noninteractive \
|
||||||
@@ -35,14 +34,18 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
|||||||
TZ=UTC \
|
TZ=UTC \
|
||||||
TERM=xterm-256color \
|
TERM=xterm-256color \
|
||||||
CARGO_HOME="/root/.cargo" \
|
CARGO_HOME="/root/.cargo" \
|
||||||
|
REGISTRIES_CRATES_IO_PROTOCOL=sparse \
|
||||||
USER="root"
|
USER="root"
|
||||||
|
|
||||||
# Create CARGO_HOME folder and don't download rust docs
|
# Create CARGO_HOME folder and don't download rust docs
|
||||||
RUN mkdir -pv "${CARGO_HOME}" \
|
RUN mkdir -pv "${CARGO_HOME}" \
|
||||||
&& rustup set profile minimal
|
&& rustup set profile minimal
|
||||||
|
|
||||||
# To be able to build the armv6 image with mimalloc we need to specifically specify the libatomic.a file location
|
# Use PostgreSQL v15 during Alpine/MUSL builds instead of the default v11
|
||||||
ENV RUSTFLAGS='-Clink-arg=/usr/local/musl/arm-unknown-linux-musleabi/lib/libatomic.a'
|
# Debian Bookworm already contains libpq v15
|
||||||
|
ENV PQ_LIB_DIR="/usr/local/musl/pq15/lib"
|
||||||
|
# To be able to build the armv6 image with mimalloc we need to tell the linker to also look for libatomic
|
||||||
|
ENV RUSTFLAGS='-Clink-arg=-latomic'
|
||||||
|
|
||||||
# Creates a dummy project used to grab dependencies
|
# Creates a dummy project used to grab dependencies
|
||||||
RUN USER=root cargo new --bin /app
|
RUN USER=root cargo new --bin /app
|
||||||
@@ -76,11 +79,10 @@ RUN touch src/main.rs
|
|||||||
# your actual source files being built
|
# your actual source files being built
|
||||||
RUN cargo build --features ${DB} --release --target=arm-unknown-linux-musleabi
|
RUN cargo build --features ${DB} --release --target=arm-unknown-linux-musleabi
|
||||||
|
|
||||||
|
|
||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
# Create a new stage with a minimal image
|
# Create a new stage with a minimal image
|
||||||
# because we already have a binary built
|
# 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" \
|
ENV ROCKET_PROFILE="release" \
|
||||||
ROCKET_ADDRESS=0.0.0.0 \
|
ROCKET_ADDRESS=0.0.0.0 \
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
# This file was generated using a Jinja2 template.
|
# This file was generated using a Jinja2 template.
|
||||||
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
||||||
|
|
||||||
# Using multistage build:
|
# Using multistage build:
|
||||||
# https://docs.docker.com/develop/develop-images/multistage-build/
|
# https://docs.docker.com/develop/develop-images/multistage-build/
|
||||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
# https://whitfin.io/speeding-up-rust-docker-builds/
|
||||||
@@ -16,18 +15,18 @@
|
|||||||
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
# - 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.
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
# - From the command line:
|
# - From the command line:
|
||||||
# $ docker pull vaultwarden/web-vault:v2023.3.0b
|
# $ docker pull docker.io/vaultwarden/web-vault:v2023.7.1
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b
|
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2023.7.1
|
||||||
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee]
|
# [docker.io/vaultwarden/web-vault@sha256:b306f38fe0d54fa3d79059a737f8e1803da44ddc5f273c2aecdd6a4886211b0f]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee
|
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:b306f38fe0d54fa3d79059a737f8e1803da44ddc5f273c2aecdd6a4886211b0f
|
||||||
# [vaultwarden/web-vault:v2023.3.0b]
|
# [docker.io/vaultwarden/web-vault:v2023.7.1]
|
||||||
#
|
#
|
||||||
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault
|
FROM docker.io/vaultwarden/web-vault@sha256:b306f38fe0d54fa3d79059a737f8e1803da44ddc5f273c2aecdd6a4886211b0f as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM rust:1.68.1-bullseye as build
|
FROM docker.io/library/rust:1.71.1-bookworm as build
|
||||||
|
|
||||||
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
||||||
ENV DEBIAN_FRONTEND=noninteractive \
|
ENV DEBIAN_FRONTEND=noninteractive \
|
||||||
@@ -35,6 +34,7 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
|||||||
TZ=UTC \
|
TZ=UTC \
|
||||||
TERM=xterm-256color \
|
TERM=xterm-256color \
|
||||||
CARGO_HOME="/root/.cargo" \
|
CARGO_HOME="/root/.cargo" \
|
||||||
|
REGISTRIES_CRATES_IO_PROTOCOL=sparse \
|
||||||
USER="root"
|
USER="root"
|
||||||
|
|
||||||
# Create CARGO_HOME folder and don't download rust docs
|
# Create CARGO_HOME folder and don't download rust docs
|
||||||
@@ -42,13 +42,12 @@ RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.
|
|||||||
&& rustup set profile minimal
|
&& rustup set profile minimal
|
||||||
|
|
||||||
# Install build dependencies for the armel architecture
|
# Install build dependencies for the armel architecture
|
||||||
RUN dpkg --add-architecture armel \
|
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry dpkg --add-architecture armel \
|
||||||
&& apt-get update \
|
&& apt-get update \
|
||||||
&& apt-get install -y \
|
&& apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
gcc-arm-linux-gnueabi \
|
gcc-arm-linux-gnueabi \
|
||||||
libc6-dev:armel \
|
libc6-dev:armel \
|
||||||
libcap2-bin \
|
|
||||||
libmariadb-dev:armel \
|
libmariadb-dev:armel \
|
||||||
libmariadb-dev-compat:armel \
|
libmariadb-dev-compat:armel \
|
||||||
libmariadb3:armel \
|
libmariadb3:armel \
|
||||||
@@ -98,16 +97,10 @@ RUN touch src/main.rs
|
|||||||
# your actual source files being built
|
# your actual source files being built
|
||||||
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry cargo build --features ${DB} --release --target=arm-unknown-linux-gnueabi
|
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry cargo build --features ${DB} --release --target=arm-unknown-linux-gnueabi
|
||||||
|
|
||||||
# Add the `cap_net_bind_service` capability to allow listening on
|
|
||||||
# privileged (< 1024) ports even when running as a non-root user.
|
|
||||||
# This is only done if building with BuildKit; with the legacy
|
|
||||||
# builder, the `COPY` instruction doesn't carry over capabilities.
|
|
||||||
RUN setcap cap_net_bind_service=+ep target/arm-unknown-linux-gnueabi/release/vaultwarden
|
|
||||||
|
|
||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
# Create a new stage with a minimal image
|
# Create a new stage with a minimal image
|
||||||
# because we already have a binary built
|
# because we already have a binary built
|
||||||
FROM balenalib/rpi-debian:bullseye
|
FROM docker.io/balenalib/rpi-debian:bookworm
|
||||||
|
|
||||||
ENV ROCKET_PROFILE="release" \
|
ENV ROCKET_PROFILE="release" \
|
||||||
ROCKET_ADDRESS=0.0.0.0 \
|
ROCKET_ADDRESS=0.0.0.0 \
|
||||||
@@ -127,10 +120,6 @@ RUN mkdir /data \
|
|||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# In the Balena Bullseye images for armv6/rpi-debian there is a missing symlink.
|
|
||||||
# This symlink was there in the buster images, and for some reason this is needed.
|
|
||||||
RUN ln -v -s /lib/ld-linux-armhf.so.3 /lib/ld-linux.so.3
|
|
||||||
|
|
||||||
RUN [ "cross-build-end" ]
|
RUN [ "cross-build-end" ]
|
||||||
|
|
||||||
VOLUME /data
|
VOLUME /data
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
# This file was generated using a Jinja2 template.
|
# This file was generated using a Jinja2 template.
|
||||||
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
||||||
|
|
||||||
# Using multistage build:
|
# Using multistage build:
|
||||||
# https://docs.docker.com/develop/develop-images/multistage-build/
|
# https://docs.docker.com/develop/develop-images/multistage-build/
|
||||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
# https://whitfin.io/speeding-up-rust-docker-builds/
|
||||||
@@ -16,18 +15,18 @@
|
|||||||
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
# - 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.
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
# - From the command line:
|
# - From the command line:
|
||||||
# $ docker pull vaultwarden/web-vault:v2023.3.0b
|
# $ docker pull docker.io/vaultwarden/web-vault:v2023.7.1
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b
|
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2023.7.1
|
||||||
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee]
|
# [docker.io/vaultwarden/web-vault@sha256:b306f38fe0d54fa3d79059a737f8e1803da44ddc5f273c2aecdd6a4886211b0f]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee
|
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:b306f38fe0d54fa3d79059a737f8e1803da44ddc5f273c2aecdd6a4886211b0f
|
||||||
# [vaultwarden/web-vault:v2023.3.0b]
|
# [docker.io/vaultwarden/web-vault:v2023.7.1]
|
||||||
#
|
#
|
||||||
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault
|
FROM docker.io/vaultwarden/web-vault@sha256:b306f38fe0d54fa3d79059a737f8e1803da44ddc5f273c2aecdd6a4886211b0f as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM blackdex/rust-musl:arm-musleabi-stable-1.68.1 as build
|
FROM docker.io/blackdex/rust-musl:arm-musleabi-stable-1.71.1-openssl3 as build
|
||||||
|
|
||||||
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
||||||
ENV DEBIAN_FRONTEND=noninteractive \
|
ENV DEBIAN_FRONTEND=noninteractive \
|
||||||
@@ -35,14 +34,18 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
|||||||
TZ=UTC \
|
TZ=UTC \
|
||||||
TERM=xterm-256color \
|
TERM=xterm-256color \
|
||||||
CARGO_HOME="/root/.cargo" \
|
CARGO_HOME="/root/.cargo" \
|
||||||
|
REGISTRIES_CRATES_IO_PROTOCOL=sparse \
|
||||||
USER="root"
|
USER="root"
|
||||||
|
|
||||||
# Create CARGO_HOME folder and don't download rust docs
|
# Create CARGO_HOME folder and don't download rust docs
|
||||||
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry mkdir -pv "${CARGO_HOME}" \
|
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry mkdir -pv "${CARGO_HOME}" \
|
||||||
&& rustup set profile minimal
|
&& rustup set profile minimal
|
||||||
|
|
||||||
# To be able to build the armv6 image with mimalloc we need to specifically specify the libatomic.a file location
|
# Use PostgreSQL v15 during Alpine/MUSL builds instead of the default v11
|
||||||
ENV RUSTFLAGS='-Clink-arg=/usr/local/musl/arm-unknown-linux-musleabi/lib/libatomic.a'
|
# Debian Bookworm already contains libpq v15
|
||||||
|
ENV PQ_LIB_DIR="/usr/local/musl/pq15/lib"
|
||||||
|
# To be able to build the armv6 image with mimalloc we need to tell the linker to also look for libatomic
|
||||||
|
ENV RUSTFLAGS='-Clink-arg=-latomic'
|
||||||
|
|
||||||
# Creates a dummy project used to grab dependencies
|
# Creates a dummy project used to grab dependencies
|
||||||
RUN USER=root cargo new --bin /app
|
RUN USER=root cargo new --bin /app
|
||||||
@@ -76,16 +79,10 @@ RUN touch src/main.rs
|
|||||||
# your actual source files being built
|
# your actual source files being built
|
||||||
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry cargo build --features ${DB} --release --target=arm-unknown-linux-musleabi
|
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry cargo build --features ${DB} --release --target=arm-unknown-linux-musleabi
|
||||||
|
|
||||||
# Add the `cap_net_bind_service` capability to allow listening on
|
|
||||||
# privileged (< 1024) ports even when running as a non-root user.
|
|
||||||
# This is only done if building with BuildKit; with the legacy
|
|
||||||
# builder, the `COPY` instruction doesn't carry over capabilities.
|
|
||||||
RUN setcap cap_net_bind_service=+ep target/arm-unknown-linux-musleabi/release/vaultwarden
|
|
||||||
|
|
||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
# Create a new stage with a minimal image
|
# Create a new stage with a minimal image
|
||||||
# because we already have a binary built
|
# 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" \
|
ENV ROCKET_PROFILE="release" \
|
||||||
ROCKET_ADDRESS=0.0.0.0 \
|
ROCKET_ADDRESS=0.0.0.0 \
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
# This file was generated using a Jinja2 template.
|
# This file was generated using a Jinja2 template.
|
||||||
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
||||||
|
|
||||||
# Using multistage build:
|
# Using multistage build:
|
||||||
# https://docs.docker.com/develop/develop-images/multistage-build/
|
# https://docs.docker.com/develop/develop-images/multistage-build/
|
||||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
# https://whitfin.io/speeding-up-rust-docker-builds/
|
||||||
@@ -16,18 +15,18 @@
|
|||||||
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
# - 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.
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
# - From the command line:
|
# - From the command line:
|
||||||
# $ docker pull vaultwarden/web-vault:v2023.3.0b
|
# $ docker pull docker.io/vaultwarden/web-vault:v2023.7.1
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b
|
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2023.7.1
|
||||||
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee]
|
# [docker.io/vaultwarden/web-vault@sha256:b306f38fe0d54fa3d79059a737f8e1803da44ddc5f273c2aecdd6a4886211b0f]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee
|
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:b306f38fe0d54fa3d79059a737f8e1803da44ddc5f273c2aecdd6a4886211b0f
|
||||||
# [vaultwarden/web-vault:v2023.3.0b]
|
# [docker.io/vaultwarden/web-vault:v2023.7.1]
|
||||||
#
|
#
|
||||||
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault
|
FROM docker.io/vaultwarden/web-vault@sha256:b306f38fe0d54fa3d79059a737f8e1803da44ddc5f273c2aecdd6a4886211b0f as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM rust:1.68.1-bullseye as build
|
FROM docker.io/library/rust:1.71.1-bookworm as build
|
||||||
|
|
||||||
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
||||||
ENV DEBIAN_FRONTEND=noninteractive \
|
ENV DEBIAN_FRONTEND=noninteractive \
|
||||||
@@ -35,6 +34,7 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
|||||||
TZ=UTC \
|
TZ=UTC \
|
||||||
TERM=xterm-256color \
|
TERM=xterm-256color \
|
||||||
CARGO_HOME="/root/.cargo" \
|
CARGO_HOME="/root/.cargo" \
|
||||||
|
REGISTRIES_CRATES_IO_PROTOCOL=sparse \
|
||||||
USER="root"
|
USER="root"
|
||||||
|
|
||||||
# Create CARGO_HOME folder and don't download rust docs
|
# Create CARGO_HOME folder and don't download rust docs
|
||||||
@@ -48,7 +48,6 @@ RUN dpkg --add-architecture armhf \
|
|||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
gcc-arm-linux-gnueabihf \
|
gcc-arm-linux-gnueabihf \
|
||||||
libc6-dev:armhf \
|
libc6-dev:armhf \
|
||||||
libcap2-bin \
|
|
||||||
libmariadb-dev:armhf \
|
libmariadb-dev:armhf \
|
||||||
libmariadb-dev-compat:armhf \
|
libmariadb-dev-compat:armhf \
|
||||||
libmariadb3:armhf \
|
libmariadb3:armhf \
|
||||||
@@ -98,11 +97,10 @@ RUN touch src/main.rs
|
|||||||
# your actual source files being built
|
# your actual source files being built
|
||||||
RUN cargo build --features ${DB} --release --target=armv7-unknown-linux-gnueabihf
|
RUN cargo build --features ${DB} --release --target=armv7-unknown-linux-gnueabihf
|
||||||
|
|
||||||
|
|
||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
# Create a new stage with a minimal image
|
# Create a new stage with a minimal image
|
||||||
# because we already have a binary built
|
# because we already have a binary built
|
||||||
FROM balenalib/armv7hf-debian:bullseye
|
FROM docker.io/balenalib/armv7hf-debian:bookworm
|
||||||
|
|
||||||
ENV ROCKET_PROFILE="release" \
|
ENV ROCKET_PROFILE="release" \
|
||||||
ROCKET_ADDRESS=0.0.0.0 \
|
ROCKET_ADDRESS=0.0.0.0 \
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
# This file was generated using a Jinja2 template.
|
# This file was generated using a Jinja2 template.
|
||||||
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
||||||
|
|
||||||
# Using multistage build:
|
# Using multistage build:
|
||||||
# https://docs.docker.com/develop/develop-images/multistage-build/
|
# https://docs.docker.com/develop/develop-images/multistage-build/
|
||||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
# https://whitfin.io/speeding-up-rust-docker-builds/
|
||||||
@@ -16,18 +15,18 @@
|
|||||||
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
# - 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.
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
# - From the command line:
|
# - From the command line:
|
||||||
# $ docker pull vaultwarden/web-vault:v2023.3.0b
|
# $ docker pull docker.io/vaultwarden/web-vault:v2023.7.1
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b
|
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2023.7.1
|
||||||
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee]
|
# [docker.io/vaultwarden/web-vault@sha256:b306f38fe0d54fa3d79059a737f8e1803da44ddc5f273c2aecdd6a4886211b0f]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee
|
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:b306f38fe0d54fa3d79059a737f8e1803da44ddc5f273c2aecdd6a4886211b0f
|
||||||
# [vaultwarden/web-vault:v2023.3.0b]
|
# [docker.io/vaultwarden/web-vault:v2023.7.1]
|
||||||
#
|
#
|
||||||
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault
|
FROM docker.io/vaultwarden/web-vault@sha256:b306f38fe0d54fa3d79059a737f8e1803da44ddc5f273c2aecdd6a4886211b0f as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM blackdex/rust-musl:armv7-musleabihf-stable-1.68.1 as build
|
FROM docker.io/blackdex/rust-musl:armv7-musleabihf-stable-1.71.1-openssl3 as build
|
||||||
|
|
||||||
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
||||||
ENV DEBIAN_FRONTEND=noninteractive \
|
ENV DEBIAN_FRONTEND=noninteractive \
|
||||||
@@ -35,12 +34,16 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
|||||||
TZ=UTC \
|
TZ=UTC \
|
||||||
TERM=xterm-256color \
|
TERM=xterm-256color \
|
||||||
CARGO_HOME="/root/.cargo" \
|
CARGO_HOME="/root/.cargo" \
|
||||||
|
REGISTRIES_CRATES_IO_PROTOCOL=sparse \
|
||||||
USER="root"
|
USER="root"
|
||||||
|
|
||||||
# Create CARGO_HOME folder and don't download rust docs
|
# Create CARGO_HOME folder and don't download rust docs
|
||||||
RUN mkdir -pv "${CARGO_HOME}" \
|
RUN mkdir -pv "${CARGO_HOME}" \
|
||||||
&& rustup set profile minimal
|
&& rustup set profile minimal
|
||||||
|
|
||||||
|
# Use PostgreSQL v15 during Alpine/MUSL builds instead of the default v11
|
||||||
|
# Debian Bookworm already contains libpq v15
|
||||||
|
ENV PQ_LIB_DIR="/usr/local/musl/pq15/lib"
|
||||||
|
|
||||||
# Creates a dummy project used to grab dependencies
|
# Creates a dummy project used to grab dependencies
|
||||||
RUN USER=root cargo new --bin /app
|
RUN USER=root cargo new --bin /app
|
||||||
@@ -74,11 +77,10 @@ RUN touch src/main.rs
|
|||||||
# your actual source files being built
|
# your actual source files being built
|
||||||
RUN cargo build --features ${DB} --release --target=armv7-unknown-linux-musleabihf
|
RUN cargo build --features ${DB} --release --target=armv7-unknown-linux-musleabihf
|
||||||
|
|
||||||
|
|
||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
# Create a new stage with a minimal image
|
# Create a new stage with a minimal image
|
||||||
# because we already have a binary built
|
# 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" \
|
ENV ROCKET_PROFILE="release" \
|
||||||
ROCKET_ADDRESS=0.0.0.0 \
|
ROCKET_ADDRESS=0.0.0.0 \
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
# This file was generated using a Jinja2 template.
|
# This file was generated using a Jinja2 template.
|
||||||
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
||||||
|
|
||||||
# Using multistage build:
|
# Using multistage build:
|
||||||
# https://docs.docker.com/develop/develop-images/multistage-build/
|
# https://docs.docker.com/develop/develop-images/multistage-build/
|
||||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
# https://whitfin.io/speeding-up-rust-docker-builds/
|
||||||
@@ -16,18 +15,18 @@
|
|||||||
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
# - 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.
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
# - From the command line:
|
# - From the command line:
|
||||||
# $ docker pull vaultwarden/web-vault:v2023.3.0b
|
# $ docker pull docker.io/vaultwarden/web-vault:v2023.7.1
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b
|
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2023.7.1
|
||||||
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee]
|
# [docker.io/vaultwarden/web-vault@sha256:b306f38fe0d54fa3d79059a737f8e1803da44ddc5f273c2aecdd6a4886211b0f]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee
|
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:b306f38fe0d54fa3d79059a737f8e1803da44ddc5f273c2aecdd6a4886211b0f
|
||||||
# [vaultwarden/web-vault:v2023.3.0b]
|
# [docker.io/vaultwarden/web-vault:v2023.7.1]
|
||||||
#
|
#
|
||||||
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault
|
FROM docker.io/vaultwarden/web-vault@sha256:b306f38fe0d54fa3d79059a737f8e1803da44ddc5f273c2aecdd6a4886211b0f as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM rust:1.68.1-bullseye as build
|
FROM docker.io/library/rust:1.71.1-bookworm as build
|
||||||
|
|
||||||
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
||||||
ENV DEBIAN_FRONTEND=noninteractive \
|
ENV DEBIAN_FRONTEND=noninteractive \
|
||||||
@@ -35,6 +34,7 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
|||||||
TZ=UTC \
|
TZ=UTC \
|
||||||
TERM=xterm-256color \
|
TERM=xterm-256color \
|
||||||
CARGO_HOME="/root/.cargo" \
|
CARGO_HOME="/root/.cargo" \
|
||||||
|
REGISTRIES_CRATES_IO_PROTOCOL=sparse \
|
||||||
USER="root"
|
USER="root"
|
||||||
|
|
||||||
# Create CARGO_HOME folder and don't download rust docs
|
# Create CARGO_HOME folder and don't download rust docs
|
||||||
@@ -42,13 +42,12 @@ RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.
|
|||||||
&& rustup set profile minimal
|
&& rustup set profile minimal
|
||||||
|
|
||||||
# Install build dependencies for the armhf architecture
|
# Install build dependencies for the armhf architecture
|
||||||
RUN dpkg --add-architecture armhf \
|
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry dpkg --add-architecture armhf \
|
||||||
&& apt-get update \
|
&& apt-get update \
|
||||||
&& apt-get install -y \
|
&& apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
gcc-arm-linux-gnueabihf \
|
gcc-arm-linux-gnueabihf \
|
||||||
libc6-dev:armhf \
|
libc6-dev:armhf \
|
||||||
libcap2-bin \
|
|
||||||
libmariadb-dev:armhf \
|
libmariadb-dev:armhf \
|
||||||
libmariadb-dev-compat:armhf \
|
libmariadb-dev-compat:armhf \
|
||||||
libmariadb3:armhf \
|
libmariadb3:armhf \
|
||||||
@@ -98,16 +97,10 @@ RUN touch src/main.rs
|
|||||||
# your actual source files being built
|
# your actual source files being built
|
||||||
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry cargo build --features ${DB} --release --target=armv7-unknown-linux-gnueabihf
|
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry cargo build --features ${DB} --release --target=armv7-unknown-linux-gnueabihf
|
||||||
|
|
||||||
# Add the `cap_net_bind_service` capability to allow listening on
|
|
||||||
# privileged (< 1024) ports even when running as a non-root user.
|
|
||||||
# This is only done if building with BuildKit; with the legacy
|
|
||||||
# builder, the `COPY` instruction doesn't carry over capabilities.
|
|
||||||
RUN setcap cap_net_bind_service=+ep target/armv7-unknown-linux-gnueabihf/release/vaultwarden
|
|
||||||
|
|
||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
# Create a new stage with a minimal image
|
# Create a new stage with a minimal image
|
||||||
# because we already have a binary built
|
# because we already have a binary built
|
||||||
FROM balenalib/armv7hf-debian:bullseye
|
FROM docker.io/balenalib/armv7hf-debian:bookworm
|
||||||
|
|
||||||
ENV ROCKET_PROFILE="release" \
|
ENV ROCKET_PROFILE="release" \
|
||||||
ROCKET_ADDRESS=0.0.0.0 \
|
ROCKET_ADDRESS=0.0.0.0 \
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
# This file was generated using a Jinja2 template.
|
# This file was generated using a Jinja2 template.
|
||||||
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
||||||
|
|
||||||
# Using multistage build:
|
# Using multistage build:
|
||||||
# https://docs.docker.com/develop/develop-images/multistage-build/
|
# https://docs.docker.com/develop/develop-images/multistage-build/
|
||||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
# https://whitfin.io/speeding-up-rust-docker-builds/
|
||||||
@@ -16,18 +15,18 @@
|
|||||||
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
# - 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.
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
# - From the command line:
|
# - From the command line:
|
||||||
# $ docker pull vaultwarden/web-vault:v2023.3.0b
|
# $ docker pull docker.io/vaultwarden/web-vault:v2023.7.1
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b
|
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2023.7.1
|
||||||
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee]
|
# [docker.io/vaultwarden/web-vault@sha256:b306f38fe0d54fa3d79059a737f8e1803da44ddc5f273c2aecdd6a4886211b0f]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee
|
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:b306f38fe0d54fa3d79059a737f8e1803da44ddc5f273c2aecdd6a4886211b0f
|
||||||
# [vaultwarden/web-vault:v2023.3.0b]
|
# [docker.io/vaultwarden/web-vault:v2023.7.1]
|
||||||
#
|
#
|
||||||
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault
|
FROM docker.io/vaultwarden/web-vault@sha256:b306f38fe0d54fa3d79059a737f8e1803da44ddc5f273c2aecdd6a4886211b0f as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM blackdex/rust-musl:armv7-musleabihf-stable-1.68.1 as build
|
FROM docker.io/blackdex/rust-musl:armv7-musleabihf-stable-1.71.1-openssl3 as build
|
||||||
|
|
||||||
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
||||||
ENV DEBIAN_FRONTEND=noninteractive \
|
ENV DEBIAN_FRONTEND=noninteractive \
|
||||||
@@ -35,12 +34,16 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
|||||||
TZ=UTC \
|
TZ=UTC \
|
||||||
TERM=xterm-256color \
|
TERM=xterm-256color \
|
||||||
CARGO_HOME="/root/.cargo" \
|
CARGO_HOME="/root/.cargo" \
|
||||||
|
REGISTRIES_CRATES_IO_PROTOCOL=sparse \
|
||||||
USER="root"
|
USER="root"
|
||||||
|
|
||||||
# Create CARGO_HOME folder and don't download rust docs
|
# Create CARGO_HOME folder and don't download rust docs
|
||||||
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry mkdir -pv "${CARGO_HOME}" \
|
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry mkdir -pv "${CARGO_HOME}" \
|
||||||
&& rustup set profile minimal
|
&& rustup set profile minimal
|
||||||
|
|
||||||
|
# Use PostgreSQL v15 during Alpine/MUSL builds instead of the default v11
|
||||||
|
# Debian Bookworm already contains libpq v15
|
||||||
|
ENV PQ_LIB_DIR="/usr/local/musl/pq15/lib"
|
||||||
|
|
||||||
# Creates a dummy project used to grab dependencies
|
# Creates a dummy project used to grab dependencies
|
||||||
RUN USER=root cargo new --bin /app
|
RUN USER=root cargo new --bin /app
|
||||||
@@ -74,16 +77,10 @@ RUN touch src/main.rs
|
|||||||
# your actual source files being built
|
# your actual source files being built
|
||||||
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry cargo build --features ${DB} --release --target=armv7-unknown-linux-musleabihf
|
RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.cargo/registry cargo build --features ${DB} --release --target=armv7-unknown-linux-musleabihf
|
||||||
|
|
||||||
# Add the `cap_net_bind_service` capability to allow listening on
|
|
||||||
# privileged (< 1024) ports even when running as a non-root user.
|
|
||||||
# This is only done if building with BuildKit; with the legacy
|
|
||||||
# builder, the `COPY` instruction doesn't carry over capabilities.
|
|
||||||
RUN setcap cap_net_bind_service=+ep target/armv7-unknown-linux-musleabihf/release/vaultwarden
|
|
||||||
|
|
||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
# Create a new stage with a minimal image
|
# Create a new stage with a minimal image
|
||||||
# because we already have a binary built
|
# 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" \
|
ENV ROCKET_PROFILE="release" \
|
||||||
ROCKET_ADDRESS=0.0.0.0 \
|
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,19 @@
|
|||||||
|
CREATE TABLE auth_requests (
|
||||||
|
uuid CHAR(36) NOT NULL PRIMARY KEY,
|
||||||
|
user_uuid CHAR(36) NOT NULL,
|
||||||
|
organization_uuid CHAR(36),
|
||||||
|
request_device_identifier CHAR(36) NOT NULL,
|
||||||
|
device_type INTEGER NOT NULL,
|
||||||
|
request_ip TEXT NOT NULL,
|
||||||
|
response_device_id CHAR(36),
|
||||||
|
access_code TEXT NOT NULL,
|
||||||
|
public_key TEXT NOT NULL,
|
||||||
|
enc_key TEXT NOT NULL,
|
||||||
|
master_password_hash TEXT NOT NULL,
|
||||||
|
approved BOOLEAN,
|
||||||
|
creation_date DATETIME NOT NULL,
|
||||||
|
response_date DATETIME,
|
||||||
|
authentication_date DATETIME,
|
||||||
|
FOREIGN KEY(user_uuid) REFERENCES users(uuid),
|
||||||
|
FOREIGN KEY(organization_uuid) REFERENCES organizations(uuid)
|
||||||
|
);
|
||||||
@@ -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,19 @@
|
|||||||
|
CREATE TABLE auth_requests (
|
||||||
|
uuid CHAR(36) NOT NULL PRIMARY KEY,
|
||||||
|
user_uuid CHAR(36) NOT NULL,
|
||||||
|
organization_uuid CHAR(36),
|
||||||
|
request_device_identifier CHAR(36) NOT NULL,
|
||||||
|
device_type INTEGER NOT NULL,
|
||||||
|
request_ip TEXT NOT NULL,
|
||||||
|
response_device_id CHAR(36),
|
||||||
|
access_code TEXT NOT NULL,
|
||||||
|
public_key TEXT NOT NULL,
|
||||||
|
enc_key TEXT NOT NULL,
|
||||||
|
master_password_hash TEXT NOT NULL,
|
||||||
|
approved BOOLEAN,
|
||||||
|
creation_date TIMESTAMP NOT NULL,
|
||||||
|
response_date TIMESTAMP,
|
||||||
|
authentication_date TIMESTAMP,
|
||||||
|
FOREIGN KEY(user_uuid) REFERENCES users(uuid),
|
||||||
|
FOREIGN KEY(organization_uuid) REFERENCES organizations(uuid)
|
||||||
|
);
|
||||||
@@ -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,19 @@
|
|||||||
|
CREATE TABLE auth_requests (
|
||||||
|
uuid TEXT NOT NULL PRIMARY KEY,
|
||||||
|
user_uuid TEXT NOT NULL,
|
||||||
|
organization_uuid TEXT,
|
||||||
|
request_device_identifier TEXT NOT NULL,
|
||||||
|
device_type INTEGER NOT NULL,
|
||||||
|
request_ip TEXT NOT NULL,
|
||||||
|
response_device_id TEXT,
|
||||||
|
access_code TEXT NOT NULL,
|
||||||
|
public_key TEXT NOT NULL,
|
||||||
|
enc_key TEXT NOT NULL,
|
||||||
|
master_password_hash TEXT NOT NULL,
|
||||||
|
approved BOOLEAN,
|
||||||
|
creation_date DATETIME NOT NULL,
|
||||||
|
response_date DATETIME,
|
||||||
|
authentication_date DATETIME,
|
||||||
|
FOREIGN KEY(user_uuid) REFERENCES users(uuid),
|
||||||
|
FOREIGN KEY(organization_uuid) REFERENCES organizations(uuid)
|
||||||
|
);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE collections ADD COLUMN external_id TEXT;
|
||||||
@@ -1 +1 @@
|
|||||||
1.68.1
|
1.71.1
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use rocket::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
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},
|
auth::{decode_admin, encode_jwt, generate_admin_claims, ClientIp},
|
||||||
config::ConfigBuilder,
|
config::ConfigBuilder,
|
||||||
db::{backup_database, get_sql_server_version, models::*, DbConn, DbConnType},
|
db::{backup_database, get_sql_server_version, models::*, DbConn, DbConnType},
|
||||||
@@ -36,6 +36,7 @@ pub fn routes() -> Vec<Route> {
|
|||||||
get_user_by_mail_json,
|
get_user_by_mail_json,
|
||||||
post_admin_login,
|
post_admin_login,
|
||||||
admin_page,
|
admin_page,
|
||||||
|
admin_page_login,
|
||||||
invite_user,
|
invite_user,
|
||||||
logout,
|
logout,
|
||||||
delete_user,
|
delete_user,
|
||||||
@@ -53,7 +54,8 @@ pub fn routes() -> Vec<Route> {
|
|||||||
organizations_overview,
|
organizations_overview,
|
||||||
delete_organization,
|
delete_organization,
|
||||||
diagnostics,
|
diagnostics,
|
||||||
get_diagnostics_config
|
get_diagnostics_config,
|
||||||
|
resend_user_invite,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,6 +257,11 @@ fn admin_page(_token: AdminToken) -> ApiResult<Html<String>> {
|
|||||||
render_admin_page()
|
render_admin_page()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[get("/", rank = 2)]
|
||||||
|
fn admin_page_login() -> ApiResult<Html<String>> {
|
||||||
|
render_admin_login(None, None)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
struct InviteData {
|
struct InviteData {
|
||||||
@@ -348,8 +355,8 @@ async fn users_overview(_token: AdminToken, mut conn: DbConn) -> ApiResult<Html<
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/users/by-mail/<mail>")]
|
#[get("/users/by-mail/<mail>")]
|
||||||
async fn get_user_by_mail_json(mail: String, _token: AdminToken, mut conn: DbConn) -> JsonResult {
|
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 {
|
if let Some(u) = User::find_by_mail(mail, &mut conn).await {
|
||||||
let mut usr = u.to_json(&mut conn).await;
|
let mut usr = u.to_json(&mut conn).await;
|
||||||
usr["UserEnabled"] = json!(u.enabled);
|
usr["UserEnabled"] = json!(u.enabled);
|
||||||
usr["CreatedAt"] = json!(format_naive_datetime_local(&u.created_at, DT_FMT));
|
usr["CreatedAt"] = json!(format_naive_datetime_local(&u.created_at, DT_FMT));
|
||||||
@@ -360,8 +367,8 @@ async fn get_user_by_mail_json(mail: String, _token: AdminToken, mut conn: DbCon
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/users/<uuid>")]
|
#[get("/users/<uuid>")]
|
||||||
async fn get_user_json(uuid: String, _token: AdminToken, mut conn: DbConn) -> JsonResult {
|
async fn get_user_json(uuid: &str, _token: AdminToken, mut conn: DbConn) -> JsonResult {
|
||||||
let u = get_user_or_404(&uuid, &mut conn).await?;
|
let u = get_user_or_404(uuid, &mut conn).await?;
|
||||||
let mut usr = u.to_json(&mut conn).await;
|
let mut usr = u.to_json(&mut conn).await;
|
||||||
usr["UserEnabled"] = json!(u.enabled);
|
usr["UserEnabled"] = json!(u.enabled);
|
||||||
usr["CreatedAt"] = json!(format_naive_datetime_local(&u.created_at, DT_FMT));
|
usr["CreatedAt"] = json!(format_naive_datetime_local(&u.created_at, DT_FMT));
|
||||||
@@ -369,18 +376,18 @@ async fn get_user_json(uuid: String, _token: AdminToken, mut conn: DbConn) -> Js
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/users/<uuid>/delete")]
|
#[post("/users/<uuid>/delete")]
|
||||||
async fn delete_user(uuid: String, token: AdminToken, mut conn: DbConn) -> EmptyResult {
|
async fn delete_user(uuid: &str, token: AdminToken, mut conn: DbConn) -> EmptyResult {
|
||||||
let user = get_user_or_404(&uuid, &mut conn).await?;
|
let user = get_user_or_404(uuid, &mut conn).await?;
|
||||||
|
|
||||||
// Get the user_org records before deleting the actual user
|
// 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;
|
let res = user.delete(&mut conn).await;
|
||||||
|
|
||||||
for user_org in user_orgs {
|
for user_org in user_orgs {
|
||||||
log_event(
|
log_event(
|
||||||
EventType::OrganizationUserRemoved as i32,
|
EventType::OrganizationUserRemoved as i32,
|
||||||
&user_org.uuid,
|
&user_org.uuid,
|
||||||
user_org.org_uuid,
|
&user_org.org_uuid,
|
||||||
String::from(ACTING_ADMIN_USER),
|
String::from(ACTING_ADMIN_USER),
|
||||||
14, // Use UnknownBrowser type
|
14, // Use UnknownBrowser type
|
||||||
&token.ip.ip,
|
&token.ip.ip,
|
||||||
@@ -393,21 +400,29 @@ async fn delete_user(uuid: String, token: AdminToken, mut conn: DbConn) -> Empty
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/users/<uuid>/deauth")]
|
#[post("/users/<uuid>/deauth")]
|
||||||
async fn deauth_user(uuid: String, _token: AdminToken, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
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?;
|
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;
|
|
||||||
|
|
||||||
nt.send_logout(&user, None).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")]
|
#[post("/users/<uuid>/disable")]
|
||||||
async fn disable_user(uuid: String, _token: AdminToken, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
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?;
|
let mut user = get_user_or_404(uuid, &mut conn).await?;
|
||||||
Device::delete_all_by_user(&user.uuid, &mut conn).await?;
|
Device::delete_all_by_user(&user.uuid, &mut conn).await?;
|
||||||
user.reset_security_stamp();
|
user.reset_security_stamp();
|
||||||
user.enabled = false;
|
user.enabled = false;
|
||||||
@@ -420,21 +435,39 @@ async fn disable_user(uuid: String, _token: AdminToken, mut conn: DbConn, nt: No
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/users/<uuid>/enable")]
|
#[post("/users/<uuid>/enable")]
|
||||||
async fn enable_user(uuid: String, _token: AdminToken, mut conn: DbConn) -> EmptyResult {
|
async fn enable_user(uuid: &str, _token: AdminToken, mut conn: DbConn) -> EmptyResult {
|
||||||
let mut user = get_user_or_404(&uuid, &mut conn).await?;
|
let mut user = get_user_or_404(uuid, &mut conn).await?;
|
||||||
user.enabled = true;
|
user.enabled = true;
|
||||||
|
|
||||||
user.save(&mut conn).await
|
user.save(&mut conn).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/users/<uuid>/remove-2fa")]
|
#[post("/users/<uuid>/remove-2fa")]
|
||||||
async fn remove_2fa(uuid: String, _token: AdminToken, mut conn: DbConn) -> EmptyResult {
|
async fn remove_2fa(uuid: &str, _token: AdminToken, mut conn: DbConn) -> EmptyResult {
|
||||||
let mut user = get_user_or_404(&uuid, &mut conn).await?;
|
let mut user = get_user_or_404(uuid, &mut conn).await?;
|
||||||
TwoFactor::delete_all_by_user(&user.uuid, &mut conn).await?;
|
TwoFactor::delete_all_by_user(&user.uuid, &mut conn).await?;
|
||||||
user.totp_recover = None;
|
user.totp_recover = None;
|
||||||
user.save(&mut conn).await
|
user.save(&mut conn).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[post("/users/<uuid>/invite/resend")]
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
if CONFIG.mail_enabled() {
|
||||||
|
mail::send_invite(&user.email, &user.uuid, None, None, &CONFIG.invitation_org_name(), None).await
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err_code!("User doesn't exist", Status::NotFound.code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
struct UserOrgTypeData {
|
struct UserOrgTypeData {
|
||||||
user_type: NumberOrString,
|
user_type: NumberOrString,
|
||||||
@@ -481,7 +514,7 @@ async fn update_user_org_type(data: Json<UserOrgTypeData>, token: AdminToken, mu
|
|||||||
log_event(
|
log_event(
|
||||||
EventType::OrganizationUserUpdated as i32,
|
EventType::OrganizationUserUpdated as i32,
|
||||||
&user_to_edit.uuid,
|
&user_to_edit.uuid,
|
||||||
data.org_uuid,
|
&data.org_uuid,
|
||||||
String::from(ACTING_ADMIN_USER),
|
String::from(ACTING_ADMIN_USER),
|
||||||
14, // Use UnknownBrowser type
|
14, // Use UnknownBrowser type
|
||||||
&token.ip.ip,
|
&token.ip.ip,
|
||||||
@@ -519,8 +552,8 @@ async fn organizations_overview(_token: AdminToken, mut conn: DbConn) -> ApiResu
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/organizations/<uuid>/delete")]
|
#[post("/organizations/<uuid>/delete")]
|
||||||
async fn delete_organization(uuid: String, _token: AdminToken, mut conn: DbConn) -> EmptyResult {
|
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")?;
|
let org = Organization::find_by_uuid(uuid, &mut conn).await.map_res("Organization doesn't exist")?;
|
||||||
org.delete(&mut conn).await
|
org.delete(&mut conn).await
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -742,7 +775,17 @@ impl<'r> FromRequest<'r> for AdminToken {
|
|||||||
|
|
||||||
let access_token = match cookies.get(COOKIE_NAME) {
|
let access_token = match cookies.get(COOKIE_NAME) {
|
||||||
Some(cookie) => cookie.value(),
|
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() {
|
if decode_admin(access_token).is_err() {
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
|
use crate::db::DbPool;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use rocket::serde::json::Json;
|
use rocket::serde::json::Json;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{
|
api::{
|
||||||
core::log_user_event, EmptyResult, JsonResult, JsonUpcase, Notify, NumberOrString, PasswordData, UpdateType,
|
core::log_user_event, register_push_device, unregister_push_device, AnonymousNotify, EmptyResult, JsonResult,
|
||||||
|
JsonUpcase, Notify, NumberOrString, PasswordData, UpdateType,
|
||||||
},
|
},
|
||||||
auth::{decode_delete, decode_invite, decode_verify_email, Headers},
|
auth::{decode_delete, decode_invite, decode_verify_email, ClientHeaders, Headers},
|
||||||
crypto,
|
crypto,
|
||||||
db::{models::*, DbConn},
|
db::{models::*, DbConn},
|
||||||
mail, CONFIG,
|
mail, CONFIG,
|
||||||
@@ -35,6 +37,7 @@ pub fn routes() -> Vec<rocket::Route> {
|
|||||||
post_verify_email_token,
|
post_verify_email_token,
|
||||||
post_delete_recover,
|
post_delete_recover,
|
||||||
post_delete_recover_token,
|
post_delete_recover_token,
|
||||||
|
post_device_token,
|
||||||
delete_account,
|
delete_account,
|
||||||
post_delete_account,
|
post_delete_account,
|
||||||
revision_date,
|
revision_date,
|
||||||
@@ -46,6 +49,14 @@ pub fn routes() -> Vec<rocket::Route> {
|
|||||||
get_known_device,
|
get_known_device,
|
||||||
get_known_device_from_path,
|
get_known_device_from_path,
|
||||||
put_avatar,
|
put_avatar,
|
||||||
|
put_device_token,
|
||||||
|
put_clear_device_token,
|
||||||
|
post_clear_device_token,
|
||||||
|
post_auth_request,
|
||||||
|
get_auth_request,
|
||||||
|
put_auth_request,
|
||||||
|
get_auth_request_response,
|
||||||
|
get_auth_requests,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,7 +144,7 @@ pub async fn _register(data: JsonUpcase<RegisterData>, mut conn: DbConn) -> Json
|
|||||||
err!("Registration email does not match invite email")
|
err!("Registration email does not match invite email")
|
||||||
}
|
}
|
||||||
} else if Invitation::take(&email, &mut conn).await {
|
} 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.status = UserOrgStatus::Accepted as i32;
|
||||||
user_org.save(&mut conn).await?;
|
user_org.save(&mut conn).await?;
|
||||||
}
|
}
|
||||||
@@ -169,8 +180,8 @@ pub async fn _register(data: JsonUpcase<RegisterData>, mut conn: DbConn) -> Json
|
|||||||
user.client_kdf_iter = client_kdf_iter;
|
user.client_kdf_iter = client_kdf_iter;
|
||||||
}
|
}
|
||||||
|
|
||||||
user.client_kdf_parallelism = data.KdfMemory;
|
user.client_kdf_memory = data.KdfMemory;
|
||||||
user.client_kdf_memory = data.KdfParallelism;
|
user.client_kdf_parallelism = data.KdfParallelism;
|
||||||
|
|
||||||
user.set_password(&data.MasterPasswordHash, Some(data.Key), true, None);
|
user.set_password(&data.MasterPasswordHash, Some(data.Key), true, None);
|
||||||
user.password_hint = password_hint;
|
user.password_hint = password_hint;
|
||||||
@@ -266,8 +277,8 @@ async fn put_avatar(data: JsonUpcase<AvatarData>, headers: Headers, mut conn: Db
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/users/<uuid>/public-key")]
|
#[get("/users/<uuid>/public-key")]
|
||||||
async fn get_public_keys(uuid: String, _headers: Headers, mut conn: DbConn) -> JsonResult {
|
async fn get_public_keys(uuid: &str, _headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||||
let user = match User::find_by_uuid(&uuid, &mut conn).await {
|
let user = match User::find_by_uuid(uuid, &mut conn).await {
|
||||||
Some(user) => user,
|
Some(user) => user,
|
||||||
None => err!("User doesn't exist"),
|
None => err!("User doesn't exist"),
|
||||||
};
|
};
|
||||||
@@ -389,6 +400,9 @@ async fn post_kdf(data: JsonUpcase<ChangeKdfData>, headers: Headers, mut conn: D
|
|||||||
} else {
|
} else {
|
||||||
err!("Argon2 parallelism parameter is required.")
|
err!("Argon2 parallelism parameter is required.")
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
user.client_kdf_memory = None;
|
||||||
|
user.client_kdf_parallelism = None;
|
||||||
}
|
}
|
||||||
user.client_kdf_iter = data.KdfIterations;
|
user.client_kdf_iter = data.KdfIterations;
|
||||||
user.client_kdf_type = data.Kdf;
|
user.client_kdf_type = data.Kdf;
|
||||||
@@ -803,16 +817,13 @@ pub async fn _prelogin(data: JsonUpcase<PreloginData>, mut conn: DbConn) -> Json
|
|||||||
None => (User::CLIENT_KDF_TYPE_DEFAULT, User::CLIENT_KDF_ITER_DEFAULT, None, None),
|
None => (User::CLIENT_KDF_TYPE_DEFAULT, User::CLIENT_KDF_ITER_DEFAULT, None, None),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut result = json!({
|
let result = json!({
|
||||||
"Kdf": kdf_type,
|
"Kdf": kdf_type,
|
||||||
"KdfIterations": kdf_iter,
|
"KdfIterations": kdf_iter,
|
||||||
|
"KdfMemory": kdf_mem,
|
||||||
|
"KdfParallelism": kdf_para,
|
||||||
});
|
});
|
||||||
|
|
||||||
if kdf_type == UserKdfType::Argon2id as i32 {
|
|
||||||
result["KdfMemory"] = Value::Number(kdf_mem.expect("Argon2 memory parameter is required.").into());
|
|
||||||
result["KdfParallelism"] = Value::Number(kdf_para.expect("Argon2 parallelism parameter is required.").into());
|
|
||||||
}
|
|
||||||
|
|
||||||
Json(result)
|
Json(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -874,18 +885,18 @@ async fn rotate_api_key(data: JsonUpcase<SecretVerificationRequest>, headers: He
|
|||||||
|
|
||||||
// This variant is deprecated: https://github.com/bitwarden/server/pull/2682
|
// This variant is deprecated: https://github.com/bitwarden/server/pull/2682
|
||||||
#[get("/devices/knowndevice/<email>/<uuid>")]
|
#[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
|
// This endpoint doesn't have auth header
|
||||||
let mut result = false;
|
let mut result = false;
|
||||||
if let Some(user) = User::find_by_mail(&email, &mut conn).await {
|
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();
|
result = Device::find_by_uuid_and_user(uuid, &user.uuid, &mut conn).await.is_some();
|
||||||
}
|
}
|
||||||
Ok(Json(json!(result)))
|
Ok(Json(json!(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/devices/knowndevice")]
|
#[get("/devices/knowndevice")]
|
||||||
async fn get_known_device(device: KnownDevice, conn: DbConn) -> JsonResult {
|
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 {
|
struct KnownDevice {
|
||||||
@@ -899,7 +910,7 @@ impl<'r> FromRequest<'r> for KnownDevice {
|
|||||||
|
|
||||||
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||||
let email = if let Some(email_b64) = req.headers().get_one("X-Request-Email") {
|
let email = if let Some(email_b64) = req.headers().get_one("X-Request-Email") {
|
||||||
let email_bytes = match data_encoding::BASE64URL.decode(email_b64.as_bytes()) {
|
let email_bytes = match data_encoding::BASE64URL_NOPAD.decode(email_b64.as_bytes()) {
|
||||||
Ok(bytes) => bytes,
|
Ok(bytes) => bytes,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
return Outcome::Failure((
|
return Outcome::Failure((
|
||||||
@@ -930,3 +941,272 @@ 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
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
struct AuthRequestRequest {
|
||||||
|
accessCode: String,
|
||||||
|
deviceIdentifier: String,
|
||||||
|
email: String,
|
||||||
|
publicKey: String,
|
||||||
|
#[serde(alias = "type")]
|
||||||
|
_type: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/auth-requests", data = "<data>")]
|
||||||
|
async fn post_auth_request(
|
||||||
|
data: Json<AuthRequestRequest>,
|
||||||
|
headers: ClientHeaders,
|
||||||
|
mut conn: DbConn,
|
||||||
|
nt: Notify<'_>,
|
||||||
|
) -> JsonResult {
|
||||||
|
let data = data.into_inner();
|
||||||
|
|
||||||
|
let user = match User::find_by_mail(&data.email, &mut conn).await {
|
||||||
|
Some(user) => user,
|
||||||
|
None => {
|
||||||
|
err!("AuthRequest doesn't exist")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut auth_request = AuthRequest::new(
|
||||||
|
user.uuid.clone(),
|
||||||
|
data.deviceIdentifier.clone(),
|
||||||
|
headers.device_type,
|
||||||
|
headers.ip.ip.to_string(),
|
||||||
|
data.accessCode,
|
||||||
|
data.publicKey,
|
||||||
|
);
|
||||||
|
auth_request.save(&mut conn).await?;
|
||||||
|
|
||||||
|
nt.send_auth_request(&user.uuid, &auth_request.uuid, &data.deviceIdentifier, &mut conn).await;
|
||||||
|
|
||||||
|
Ok(Json(json!({
|
||||||
|
"id": auth_request.uuid,
|
||||||
|
"publicKey": auth_request.public_key,
|
||||||
|
"requestDeviceType": DeviceType::from_i32(auth_request.device_type).to_string(),
|
||||||
|
"requestIpAddress": auth_request.request_ip,
|
||||||
|
"key": null,
|
||||||
|
"masterPasswordHash": null,
|
||||||
|
"creationDate": auth_request.creation_date.and_utc(),
|
||||||
|
"responseDate": null,
|
||||||
|
"requestApproved": false,
|
||||||
|
"origin": CONFIG.domain_origin(),
|
||||||
|
"object": "auth-request"
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/auth-requests/<uuid>")]
|
||||||
|
async fn get_auth_request(uuid: &str, mut conn: DbConn) -> JsonResult {
|
||||||
|
let auth_request = match AuthRequest::find_by_uuid(uuid, &mut conn).await {
|
||||||
|
Some(auth_request) => auth_request,
|
||||||
|
None => {
|
||||||
|
err!("AuthRequest doesn't exist")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let response_date_utc = auth_request.response_date.map(|response_date| response_date.and_utc());
|
||||||
|
|
||||||
|
Ok(Json(json!(
|
||||||
|
{
|
||||||
|
"id": uuid,
|
||||||
|
"publicKey": auth_request.public_key,
|
||||||
|
"requestDeviceType": DeviceType::from_i32(auth_request.device_type).to_string(),
|
||||||
|
"requestIpAddress": auth_request.request_ip,
|
||||||
|
"key": auth_request.enc_key,
|
||||||
|
"masterPasswordHash": auth_request.master_password_hash,
|
||||||
|
"creationDate": auth_request.creation_date.and_utc(),
|
||||||
|
"responseDate": response_date_utc,
|
||||||
|
"requestApproved": auth_request.approved,
|
||||||
|
"origin": CONFIG.domain_origin(),
|
||||||
|
"object":"auth-request"
|
||||||
|
}
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
struct AuthResponseRequest {
|
||||||
|
deviceIdentifier: String,
|
||||||
|
key: String,
|
||||||
|
masterPasswordHash: String,
|
||||||
|
requestApproved: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[put("/auth-requests/<uuid>", data = "<data>")]
|
||||||
|
async fn put_auth_request(
|
||||||
|
uuid: &str,
|
||||||
|
data: Json<AuthResponseRequest>,
|
||||||
|
mut conn: DbConn,
|
||||||
|
ant: AnonymousNotify<'_>,
|
||||||
|
nt: Notify<'_>,
|
||||||
|
) -> JsonResult {
|
||||||
|
let data = data.into_inner();
|
||||||
|
let mut auth_request: AuthRequest = match AuthRequest::find_by_uuid(uuid, &mut conn).await {
|
||||||
|
Some(auth_request) => auth_request,
|
||||||
|
None => {
|
||||||
|
err!("AuthRequest doesn't exist")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
auth_request.approved = Some(data.requestApproved);
|
||||||
|
auth_request.enc_key = data.key;
|
||||||
|
auth_request.master_password_hash = data.masterPasswordHash;
|
||||||
|
auth_request.response_device_id = Some(data.deviceIdentifier.clone());
|
||||||
|
auth_request.save(&mut conn).await?;
|
||||||
|
|
||||||
|
if auth_request.approved.unwrap_or(false) {
|
||||||
|
ant.send_auth_response(&auth_request.user_uuid, &auth_request.uuid).await;
|
||||||
|
nt.send_auth_response(&auth_request.user_uuid, &auth_request.uuid, data.deviceIdentifier, &mut conn).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
let response_date_utc = auth_request.response_date.map(|response_date| response_date.and_utc());
|
||||||
|
|
||||||
|
Ok(Json(json!(
|
||||||
|
{
|
||||||
|
"id": uuid,
|
||||||
|
"publicKey": auth_request.public_key,
|
||||||
|
"requestDeviceType": DeviceType::from_i32(auth_request.device_type).to_string(),
|
||||||
|
"requestIpAddress": auth_request.request_ip,
|
||||||
|
"key": auth_request.enc_key,
|
||||||
|
"masterPasswordHash": auth_request.master_password_hash,
|
||||||
|
"creationDate": auth_request.creation_date.and_utc(),
|
||||||
|
"responseDate": response_date_utc,
|
||||||
|
"requestApproved": auth_request.approved,
|
||||||
|
"origin": CONFIG.domain_origin(),
|
||||||
|
"object":"auth-request"
|
||||||
|
}
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/auth-requests/<uuid>/response?<code>")]
|
||||||
|
async fn get_auth_request_response(uuid: &str, code: &str, mut conn: DbConn) -> JsonResult {
|
||||||
|
let auth_request = match AuthRequest::find_by_uuid(uuid, &mut conn).await {
|
||||||
|
Some(auth_request) => auth_request,
|
||||||
|
None => {
|
||||||
|
err!("AuthRequest doesn't exist")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if !auth_request.check_access_code(code) {
|
||||||
|
err!("Access code invalid doesn't exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
let response_date_utc = auth_request.response_date.map(|response_date| response_date.and_utc());
|
||||||
|
|
||||||
|
Ok(Json(json!(
|
||||||
|
{
|
||||||
|
"id": uuid,
|
||||||
|
"publicKey": auth_request.public_key,
|
||||||
|
"requestDeviceType": DeviceType::from_i32(auth_request.device_type).to_string(),
|
||||||
|
"requestIpAddress": auth_request.request_ip,
|
||||||
|
"key": auth_request.enc_key,
|
||||||
|
"masterPasswordHash": auth_request.master_password_hash,
|
||||||
|
"creationDate": auth_request.creation_date.and_utc(),
|
||||||
|
"responseDate": response_date_utc,
|
||||||
|
"requestApproved": auth_request.approved,
|
||||||
|
"origin": CONFIG.domain_origin(),
|
||||||
|
"object":"auth-request"
|
||||||
|
}
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/auth-requests")]
|
||||||
|
async fn get_auth_requests(headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||||
|
let auth_requests = AuthRequest::find_by_user(&headers.user.uuid, &mut conn).await;
|
||||||
|
|
||||||
|
Ok(Json(json!({
|
||||||
|
"data": auth_requests
|
||||||
|
.iter()
|
||||||
|
.filter(|request| request.approved.is_none())
|
||||||
|
.map(|request| {
|
||||||
|
let response_date_utc = request.response_date.map(|response_date| response_date.and_utc());
|
||||||
|
|
||||||
|
json!({
|
||||||
|
"id": request.uuid,
|
||||||
|
"publicKey": request.public_key,
|
||||||
|
"requestDeviceType": DeviceType::from_i32(request.device_type).to_string(),
|
||||||
|
"requestIpAddress": request.request_ip,
|
||||||
|
"key": request.enc_key,
|
||||||
|
"masterPasswordHash": request.master_password_hash,
|
||||||
|
"creationDate": request.creation_date.and_utc(),
|
||||||
|
"responseDate": response_date_utc,
|
||||||
|
"requestApproved": request.approved,
|
||||||
|
"origin": CONFIG.domain_origin(),
|
||||||
|
"object":"auth-request"
|
||||||
|
})
|
||||||
|
}).collect::<Vec<Value>>(),
|
||||||
|
"continuationToken": null,
|
||||||
|
"object": "list"
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn purge_auth_requests(pool: DbPool) {
|
||||||
|
debug!("Purging auth requests");
|
||||||
|
if let Ok(mut conn) = pool.get().await {
|
||||||
|
AuthRequest::purge_expired_auth_requests(&mut conn).await;
|
||||||
|
} else {
|
||||||
|
error!("Failed to get DB connection while purging trashed ciphers")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -172,8 +172,8 @@ async fn get_ciphers(headers: Headers, mut conn: DbConn) -> Json<Value> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/ciphers/<uuid>")]
|
#[get("/ciphers/<uuid>")]
|
||||||
async fn get_cipher(uuid: String, headers: Headers, mut conn: DbConn) -> JsonResult {
|
async fn get_cipher(uuid: &str, 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,
|
Some(cipher) => cipher,
|
||||||
None => err!("Cipher doesn't exist"),
|
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")]
|
#[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
|
// TODO: Implement this correctly
|
||||||
get_cipher(uuid, headers, conn).await
|
get_cipher(uuid, headers, conn).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/ciphers/<uuid>/details")]
|
#[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
|
get_cipher(uuid, headers, conn).await
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,7 +210,8 @@ pub struct CipherData {
|
|||||||
Login = 1,
|
Login = 1,
|
||||||
SecureNote = 2,
|
SecureNote = 2,
|
||||||
Card = 3,
|
Card = 3,
|
||||||
Identity = 4
|
Identity = 4,
|
||||||
|
Fido2Key = 5
|
||||||
*/
|
*/
|
||||||
pub Type: i32,
|
pub Type: i32,
|
||||||
pub Name: String,
|
pub Name: String,
|
||||||
@@ -222,6 +223,7 @@ pub struct CipherData {
|
|||||||
SecureNote: Option<Value>,
|
SecureNote: Option<Value>,
|
||||||
Card: Option<Value>,
|
Card: Option<Value>,
|
||||||
Identity: Option<Value>,
|
Identity: Option<Value>,
|
||||||
|
Fido2Key: Option<Value>,
|
||||||
|
|
||||||
Favorite: Option<bool>,
|
Favorite: Option<bool>,
|
||||||
Reprompt: Option<i32>,
|
Reprompt: Option<i32>,
|
||||||
@@ -464,6 +466,7 @@ pub async fn update_cipher_from_data(
|
|||||||
2 => data.SecureNote,
|
2 => data.SecureNote,
|
||||||
3 => data.Card,
|
3 => data.Card,
|
||||||
4 => data.Identity,
|
4 => data.Identity,
|
||||||
|
5 => data.Fido2Key,
|
||||||
_ => err!("Invalid type"),
|
_ => err!("Invalid type"),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -503,7 +506,7 @@ pub async fn update_cipher_from_data(
|
|||||||
log_event(
|
log_event(
|
||||||
event_type as i32,
|
event_type as i32,
|
||||||
&cipher.uuid,
|
&cipher.uuid,
|
||||||
String::from(org_uuid),
|
org_uuid,
|
||||||
headers.user.uuid.clone(),
|
headers.user.uuid.clone(),
|
||||||
headers.device.atype,
|
headers.device.atype,
|
||||||
&headers.ip.ip,
|
&headers.ip.ip,
|
||||||
@@ -511,10 +514,9 @@ pub async fn update_cipher_from_data(
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
nt.send_cipher_update(ut, cipher, &cipher.update_users_revision(conn).await, &headers.device.uuid, None, conn)
|
||||||
nt.send_cipher_update(ut, cipher, &cipher.update_users_revision(conn).await, &headers.device.uuid).await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -580,13 +582,14 @@ async fn post_ciphers_import(
|
|||||||
let mut user = headers.user;
|
let mut user = headers.user;
|
||||||
user.update_revision(&mut conn).await?;
|
user.update_revision(&mut conn).await?;
|
||||||
nt.send_user_update(UpdateType::SyncVault, &user).await;
|
nt.send_user_update(UpdateType::SyncVault, &user).await;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Called when an org admin modifies an existing org cipher.
|
/// Called when an org admin modifies an existing org cipher.
|
||||||
#[put("/ciphers/<uuid>/admin", data = "<data>")]
|
#[put("/ciphers/<uuid>/admin", data = "<data>")]
|
||||||
async fn put_cipher_admin(
|
async fn put_cipher_admin(
|
||||||
uuid: String,
|
uuid: &str,
|
||||||
data: JsonUpcase<CipherData>,
|
data: JsonUpcase<CipherData>,
|
||||||
headers: Headers,
|
headers: Headers,
|
||||||
conn: DbConn,
|
conn: DbConn,
|
||||||
@@ -597,7 +600,7 @@ async fn put_cipher_admin(
|
|||||||
|
|
||||||
#[post("/ciphers/<uuid>/admin", data = "<data>")]
|
#[post("/ciphers/<uuid>/admin", data = "<data>")]
|
||||||
async fn post_cipher_admin(
|
async fn post_cipher_admin(
|
||||||
uuid: String,
|
uuid: &str,
|
||||||
data: JsonUpcase<CipherData>,
|
data: JsonUpcase<CipherData>,
|
||||||
headers: Headers,
|
headers: Headers,
|
||||||
conn: DbConn,
|
conn: DbConn,
|
||||||
@@ -608,7 +611,7 @@ async fn post_cipher_admin(
|
|||||||
|
|
||||||
#[post("/ciphers/<uuid>", data = "<data>")]
|
#[post("/ciphers/<uuid>", data = "<data>")]
|
||||||
async fn post_cipher(
|
async fn post_cipher(
|
||||||
uuid: String,
|
uuid: &str,
|
||||||
data: JsonUpcase<CipherData>,
|
data: JsonUpcase<CipherData>,
|
||||||
headers: Headers,
|
headers: Headers,
|
||||||
conn: DbConn,
|
conn: DbConn,
|
||||||
@@ -619,7 +622,7 @@ async fn post_cipher(
|
|||||||
|
|
||||||
#[put("/ciphers/<uuid>", data = "<data>")]
|
#[put("/ciphers/<uuid>", data = "<data>")]
|
||||||
async fn put_cipher(
|
async fn put_cipher(
|
||||||
uuid: String,
|
uuid: &str,
|
||||||
data: JsonUpcase<CipherData>,
|
data: JsonUpcase<CipherData>,
|
||||||
headers: Headers,
|
headers: Headers,
|
||||||
mut conn: DbConn,
|
mut conn: DbConn,
|
||||||
@@ -627,7 +630,7 @@ async fn put_cipher(
|
|||||||
) -> JsonResult {
|
) -> JsonResult {
|
||||||
let data: CipherData = data.into_inner().data;
|
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,
|
Some(cipher) => cipher,
|
||||||
None => err!("Cipher doesn't exist"),
|
None => err!("Cipher doesn't exist"),
|
||||||
};
|
};
|
||||||
@@ -648,7 +651,7 @@ async fn put_cipher(
|
|||||||
|
|
||||||
#[post("/ciphers/<uuid>/partial", data = "<data>")]
|
#[post("/ciphers/<uuid>/partial", data = "<data>")]
|
||||||
async fn post_cipher_partial(
|
async fn post_cipher_partial(
|
||||||
uuid: String,
|
uuid: &str,
|
||||||
data: JsonUpcase<PartialCipherData>,
|
data: JsonUpcase<PartialCipherData>,
|
||||||
headers: Headers,
|
headers: Headers,
|
||||||
conn: DbConn,
|
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
|
// Only update the folder and favorite for the user, since this cipher is read-only
|
||||||
#[put("/ciphers/<uuid>/partial", data = "<data>")]
|
#[put("/ciphers/<uuid>/partial", data = "<data>")]
|
||||||
async fn put_cipher_partial(
|
async fn put_cipher_partial(
|
||||||
uuid: String,
|
uuid: &str,
|
||||||
data: JsonUpcase<PartialCipherData>,
|
data: JsonUpcase<PartialCipherData>,
|
||||||
headers: Headers,
|
headers: Headers,
|
||||||
mut conn: DbConn,
|
mut conn: DbConn,
|
||||||
) -> JsonResult {
|
) -> JsonResult {
|
||||||
let data: PartialCipherData = data.into_inner().data;
|
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,
|
Some(cipher) => cipher,
|
||||||
None => err!("Cipher doesn't exist"),
|
None => err!("Cipher doesn't exist"),
|
||||||
};
|
};
|
||||||
@@ -698,44 +701,48 @@ struct CollectionsAdminData {
|
|||||||
|
|
||||||
#[put("/ciphers/<uuid>/collections", data = "<data>")]
|
#[put("/ciphers/<uuid>/collections", data = "<data>")]
|
||||||
async fn put_collections_update(
|
async fn put_collections_update(
|
||||||
uuid: String,
|
uuid: &str,
|
||||||
data: JsonUpcase<CollectionsAdminData>,
|
data: JsonUpcase<CollectionsAdminData>,
|
||||||
headers: Headers,
|
headers: Headers,
|
||||||
conn: DbConn,
|
conn: DbConn,
|
||||||
|
nt: Notify<'_>,
|
||||||
) -> EmptyResult {
|
) -> EmptyResult {
|
||||||
post_collections_admin(uuid, data, headers, conn).await
|
post_collections_admin(uuid, data, headers, conn, nt).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/ciphers/<uuid>/collections", data = "<data>")]
|
#[post("/ciphers/<uuid>/collections", data = "<data>")]
|
||||||
async fn post_collections_update(
|
async fn post_collections_update(
|
||||||
uuid: String,
|
uuid: &str,
|
||||||
data: JsonUpcase<CollectionsAdminData>,
|
data: JsonUpcase<CollectionsAdminData>,
|
||||||
headers: Headers,
|
headers: Headers,
|
||||||
conn: DbConn,
|
conn: DbConn,
|
||||||
|
nt: Notify<'_>,
|
||||||
) -> EmptyResult {
|
) -> 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>")]
|
#[put("/ciphers/<uuid>/collections-admin", data = "<data>")]
|
||||||
async fn put_collections_admin(
|
async fn put_collections_admin(
|
||||||
uuid: String,
|
uuid: &str,
|
||||||
data: JsonUpcase<CollectionsAdminData>,
|
data: JsonUpcase<CollectionsAdminData>,
|
||||||
headers: Headers,
|
headers: Headers,
|
||||||
conn: DbConn,
|
conn: DbConn,
|
||||||
|
nt: Notify<'_>,
|
||||||
) -> EmptyResult {
|
) -> 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>")]
|
#[post("/ciphers/<uuid>/collections-admin", data = "<data>")]
|
||||||
async fn post_collections_admin(
|
async fn post_collections_admin(
|
||||||
uuid: String,
|
uuid: &str,
|
||||||
data: JsonUpcase<CollectionsAdminData>,
|
data: JsonUpcase<CollectionsAdminData>,
|
||||||
headers: Headers,
|
headers: Headers,
|
||||||
mut conn: DbConn,
|
mut conn: DbConn,
|
||||||
|
nt: Notify<'_>,
|
||||||
) -> EmptyResult {
|
) -> EmptyResult {
|
||||||
let data: CollectionsAdminData = data.into_inner().data;
|
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,
|
Some(cipher) => cipher,
|
||||||
None => err!("Cipher doesn't exist"),
|
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(
|
log_event(
|
||||||
EventType::CipherUpdatedCollections as i32,
|
EventType::CipherUpdatedCollections as i32,
|
||||||
&cipher.uuid,
|
&cipher.uuid,
|
||||||
cipher.organization_uuid.unwrap(),
|
&cipher.organization_uuid.unwrap(),
|
||||||
headers.user.uuid.clone(),
|
headers.user.uuid.clone(),
|
||||||
headers.device.atype,
|
headers.device.atype,
|
||||||
&headers.ip.ip,
|
&headers.ip.ip,
|
||||||
@@ -790,7 +807,7 @@ struct ShareCipherData {
|
|||||||
|
|
||||||
#[post("/ciphers/<uuid>/share", data = "<data>")]
|
#[post("/ciphers/<uuid>/share", data = "<data>")]
|
||||||
async fn post_cipher_share(
|
async fn post_cipher_share(
|
||||||
uuid: String,
|
uuid: &str,
|
||||||
data: JsonUpcase<ShareCipherData>,
|
data: JsonUpcase<ShareCipherData>,
|
||||||
headers: Headers,
|
headers: Headers,
|
||||||
mut conn: DbConn,
|
mut conn: DbConn,
|
||||||
@@ -798,12 +815,12 @@ async fn post_cipher_share(
|
|||||||
) -> JsonResult {
|
) -> JsonResult {
|
||||||
let data: ShareCipherData = data.into_inner().data;
|
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>")]
|
#[put("/ciphers/<uuid>/share", data = "<data>")]
|
||||||
async fn put_cipher_share(
|
async fn put_cipher_share(
|
||||||
uuid: String,
|
uuid: &str,
|
||||||
data: JsonUpcase<ShareCipherData>,
|
data: JsonUpcase<ShareCipherData>,
|
||||||
headers: Headers,
|
headers: Headers,
|
||||||
mut conn: DbConn,
|
mut conn: DbConn,
|
||||||
@@ -811,7 +828,7 @@ async fn put_cipher_share(
|
|||||||
) -> JsonResult {
|
) -> JsonResult {
|
||||||
let data: ShareCipherData = data.into_inner().data;
|
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)]
|
#[derive(Deserialize)]
|
||||||
@@ -916,8 +933,17 @@ async fn share_cipher_by_uuid(
|
|||||||
/// their object storage service. For self-hosted instances, it basically just
|
/// their object storage service. For self-hosted instances, it basically just
|
||||||
/// redirects to the same location as before the v2 API.
|
/// redirects to the same location as before the v2 API.
|
||||||
#[get("/ciphers/<uuid>/attachment/<attachment_id>")]
|
#[get("/ciphers/<uuid>/attachment/<attachment_id>")]
|
||||||
async fn get_attachment(uuid: String, attachment_id: String, headers: Headers, mut conn: DbConn) -> JsonResult {
|
async fn get_attachment(uuid: &str, attachment_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||||
match Attachment::find_by_id(&attachment_id, &mut conn).await {
|
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(attachment) if uuid == attachment.cipher_uuid => Ok(Json(attachment.to_json(&headers.host))),
|
||||||
Some(_) => err!("Attachment doesn't belong to cipher"),
|
Some(_) => err!("Attachment doesn't belong to cipher"),
|
||||||
None => err!("Attachment doesn't exist"),
|
None => err!("Attachment doesn't exist"),
|
||||||
@@ -944,12 +970,12 @@ enum FileUploadType {
|
|||||||
/// For self-hosted instances, it's another API on the local instance.
|
/// For self-hosted instances, it's another API on the local instance.
|
||||||
#[post("/ciphers/<uuid>/attachment/v2", data = "<data>")]
|
#[post("/ciphers/<uuid>/attachment/v2", data = "<data>")]
|
||||||
async fn post_attachment_v2(
|
async fn post_attachment_v2(
|
||||||
uuid: String,
|
uuid: &str,
|
||||||
data: JsonUpcase<AttachmentRequestData>,
|
data: JsonUpcase<AttachmentRequestData>,
|
||||||
headers: Headers,
|
headers: Headers,
|
||||||
mut conn: DbConn,
|
mut conn: DbConn,
|
||||||
) -> JsonResult {
|
) -> 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,
|
Some(cipher) => cipher,
|
||||||
None => err!("Cipher doesn't exist"),
|
None => err!("Cipher doesn't exist"),
|
||||||
};
|
};
|
||||||
@@ -995,13 +1021,13 @@ struct UploadData<'f> {
|
|||||||
/// database record, which is passed in as `attachment`.
|
/// database record, which is passed in as `attachment`.
|
||||||
async fn save_attachment(
|
async fn save_attachment(
|
||||||
mut attachment: Option<Attachment>,
|
mut attachment: Option<Attachment>,
|
||||||
cipher_uuid: String,
|
cipher_uuid: &str,
|
||||||
data: Form<UploadData<'_>>,
|
data: Form<UploadData<'_>>,
|
||||||
headers: &Headers,
|
headers: &Headers,
|
||||||
mut conn: DbConn,
|
mut conn: DbConn,
|
||||||
nt: Notify<'_>,
|
nt: Notify<'_>,
|
||||||
) -> Result<(Cipher, DbConn), crate::error::Error> {
|
) -> 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,
|
Some(cipher) => cipher,
|
||||||
None => err!("Cipher doesn't exist"),
|
None => err!("Cipher doesn't exist"),
|
||||||
};
|
};
|
||||||
@@ -1058,7 +1084,7 @@ async fn save_attachment(
|
|||||||
None => crypto::generate_attachment_id(), // Legacy API
|
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);
|
let file_path = folder_path.join(&file_id);
|
||||||
tokio::fs::create_dir_all(&folder_path).await?;
|
tokio::fs::create_dir_all(&folder_path).await?;
|
||||||
|
|
||||||
@@ -1094,7 +1120,8 @@ async fn save_attachment(
|
|||||||
if data.key.is_none() {
|
if data.key.is_none() {
|
||||||
err!("No attachment key provided")
|
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");
|
attachment.save(&mut conn).await.expect("Error saving attachment");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1107,6 +1134,8 @@ async fn save_attachment(
|
|||||||
&cipher,
|
&cipher,
|
||||||
&cipher.update_users_revision(&mut conn).await,
|
&cipher.update_users_revision(&mut conn).await,
|
||||||
&headers.device.uuid,
|
&headers.device.uuid,
|
||||||
|
None,
|
||||||
|
&mut conn,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -1114,7 +1143,7 @@ async fn save_attachment(
|
|||||||
log_event(
|
log_event(
|
||||||
EventType::CipherAttachmentCreated as i32,
|
EventType::CipherAttachmentCreated as i32,
|
||||||
&cipher.uuid,
|
&cipher.uuid,
|
||||||
String::from(org_uuid),
|
org_uuid,
|
||||||
headers.user.uuid.clone(),
|
headers.user.uuid.clone(),
|
||||||
headers.device.atype,
|
headers.device.atype,
|
||||||
&headers.ip.ip,
|
&headers.ip.ip,
|
||||||
@@ -1132,14 +1161,14 @@ async fn save_attachment(
|
|||||||
/// with this one.
|
/// with this one.
|
||||||
#[post("/ciphers/<uuid>/attachment/<attachment_id>", format = "multipart/form-data", data = "<data>", rank = 1)]
|
#[post("/ciphers/<uuid>/attachment/<attachment_id>", format = "multipart/form-data", data = "<data>", rank = 1)]
|
||||||
async fn post_attachment_v2_data(
|
async fn post_attachment_v2_data(
|
||||||
uuid: String,
|
uuid: &str,
|
||||||
attachment_id: String,
|
attachment_id: &str,
|
||||||
data: Form<UploadData<'_>>,
|
data: Form<UploadData<'_>>,
|
||||||
headers: Headers,
|
headers: Headers,
|
||||||
mut conn: DbConn,
|
mut conn: DbConn,
|
||||||
nt: Notify<'_>,
|
nt: Notify<'_>,
|
||||||
) -> EmptyResult {
|
) -> 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(attachment) if uuid == attachment.cipher_uuid => Some(attachment),
|
||||||
Some(_) => err!("Attachment doesn't belong to cipher"),
|
Some(_) => err!("Attachment doesn't belong to cipher"),
|
||||||
None => err!("Attachment doesn't exist"),
|
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.
|
/// Legacy API for creating an attachment associated with a cipher.
|
||||||
#[post("/ciphers/<uuid>/attachment", format = "multipart/form-data", data = "<data>")]
|
#[post("/ciphers/<uuid>/attachment", format = "multipart/form-data", data = "<data>")]
|
||||||
async fn post_attachment(
|
async fn post_attachment(
|
||||||
uuid: String,
|
uuid: &str,
|
||||||
data: Form<UploadData<'_>>,
|
data: Form<UploadData<'_>>,
|
||||||
headers: Headers,
|
headers: Headers,
|
||||||
conn: DbConn,
|
conn: DbConn,
|
||||||
@@ -1170,7 +1199,7 @@ async fn post_attachment(
|
|||||||
|
|
||||||
#[post("/ciphers/<uuid>/attachment-admin", format = "multipart/form-data", data = "<data>")]
|
#[post("/ciphers/<uuid>/attachment-admin", format = "multipart/form-data", data = "<data>")]
|
||||||
async fn post_attachment_admin(
|
async fn post_attachment_admin(
|
||||||
uuid: String,
|
uuid: &str,
|
||||||
data: Form<UploadData<'_>>,
|
data: Form<UploadData<'_>>,
|
||||||
headers: Headers,
|
headers: Headers,
|
||||||
conn: DbConn,
|
conn: DbConn,
|
||||||
@@ -1181,21 +1210,21 @@ async fn post_attachment_admin(
|
|||||||
|
|
||||||
#[post("/ciphers/<uuid>/attachment/<attachment_id>/share", format = "multipart/form-data", data = "<data>")]
|
#[post("/ciphers/<uuid>/attachment/<attachment_id>/share", format = "multipart/form-data", data = "<data>")]
|
||||||
async fn post_attachment_share(
|
async fn post_attachment_share(
|
||||||
uuid: String,
|
uuid: &str,
|
||||||
attachment_id: String,
|
attachment_id: &str,
|
||||||
data: Form<UploadData<'_>>,
|
data: Form<UploadData<'_>>,
|
||||||
headers: Headers,
|
headers: Headers,
|
||||||
mut conn: DbConn,
|
mut conn: DbConn,
|
||||||
nt: Notify<'_>,
|
nt: Notify<'_>,
|
||||||
) -> JsonResult {
|
) -> 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_attachment(uuid, data, headers, conn, nt).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/ciphers/<uuid>/attachment/<attachment_id>/delete-admin")]
|
#[post("/ciphers/<uuid>/attachment/<attachment_id>/delete-admin")]
|
||||||
async fn delete_attachment_post_admin(
|
async fn delete_attachment_post_admin(
|
||||||
uuid: String,
|
uuid: &str,
|
||||||
attachment_id: String,
|
attachment_id: &str,
|
||||||
headers: Headers,
|
headers: Headers,
|
||||||
conn: DbConn,
|
conn: DbConn,
|
||||||
nt: Notify<'_>,
|
nt: Notify<'_>,
|
||||||
@@ -1205,8 +1234,8 @@ async fn delete_attachment_post_admin(
|
|||||||
|
|
||||||
#[post("/ciphers/<uuid>/attachment/<attachment_id>/delete")]
|
#[post("/ciphers/<uuid>/attachment/<attachment_id>/delete")]
|
||||||
async fn delete_attachment_post(
|
async fn delete_attachment_post(
|
||||||
uuid: String,
|
uuid: &str,
|
||||||
attachment_id: String,
|
attachment_id: &str,
|
||||||
headers: Headers,
|
headers: Headers,
|
||||||
conn: DbConn,
|
conn: DbConn,
|
||||||
nt: Notify<'_>,
|
nt: Notify<'_>,
|
||||||
@@ -1216,58 +1245,58 @@ async fn delete_attachment_post(
|
|||||||
|
|
||||||
#[delete("/ciphers/<uuid>/attachment/<attachment_id>")]
|
#[delete("/ciphers/<uuid>/attachment/<attachment_id>")]
|
||||||
async fn delete_attachment(
|
async fn delete_attachment(
|
||||||
uuid: String,
|
uuid: &str,
|
||||||
attachment_id: String,
|
attachment_id: &str,
|
||||||
headers: Headers,
|
headers: Headers,
|
||||||
mut conn: DbConn,
|
mut conn: DbConn,
|
||||||
nt: Notify<'_>,
|
nt: Notify<'_>,
|
||||||
) -> EmptyResult {
|
) -> 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")]
|
#[delete("/ciphers/<uuid>/attachment/<attachment_id>/admin")]
|
||||||
async fn delete_attachment_admin(
|
async fn delete_attachment_admin(
|
||||||
uuid: String,
|
uuid: &str,
|
||||||
attachment_id: String,
|
attachment_id: &str,
|
||||||
headers: Headers,
|
headers: Headers,
|
||||||
mut conn: DbConn,
|
mut conn: DbConn,
|
||||||
nt: Notify<'_>,
|
nt: Notify<'_>,
|
||||||
) -> EmptyResult {
|
) -> 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")]
|
#[post("/ciphers/<uuid>/delete")]
|
||||||
async fn delete_cipher_post(uuid: String, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
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
|
_delete_cipher_by_uuid(uuid, &headers, &mut conn, false, &nt).await
|
||||||
// permanent delete
|
// permanent delete
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/ciphers/<uuid>/delete-admin")]
|
#[post("/ciphers/<uuid>/delete-admin")]
|
||||||
async fn delete_cipher_post_admin(uuid: String, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
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
|
_delete_cipher_by_uuid(uuid, &headers, &mut conn, false, &nt).await
|
||||||
// permanent delete
|
// permanent delete
|
||||||
}
|
}
|
||||||
|
|
||||||
#[put("/ciphers/<uuid>/delete")]
|
#[put("/ciphers/<uuid>/delete")]
|
||||||
async fn delete_cipher_put(uuid: String, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
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
|
_delete_cipher_by_uuid(uuid, &headers, &mut conn, true, &nt).await
|
||||||
// soft delete
|
// soft delete
|
||||||
}
|
}
|
||||||
|
|
||||||
#[put("/ciphers/<uuid>/delete-admin")]
|
#[put("/ciphers/<uuid>/delete-admin")]
|
||||||
async fn delete_cipher_put_admin(uuid: String, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
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_cipher_by_uuid(uuid, &headers, &mut conn, true, &nt).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[delete("/ciphers/<uuid>")]
|
#[delete("/ciphers/<uuid>")]
|
||||||
async fn delete_cipher(uuid: String, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
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
|
_delete_cipher_by_uuid(uuid, &headers, &mut conn, false, &nt).await
|
||||||
// permanent delete
|
// permanent delete
|
||||||
}
|
}
|
||||||
|
|
||||||
#[delete("/ciphers/<uuid>/admin")]
|
#[delete("/ciphers/<uuid>/admin")]
|
||||||
async fn delete_cipher_admin(uuid: String, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
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
|
_delete_cipher_by_uuid(uuid, &headers, &mut conn, false, &nt).await
|
||||||
// permanent delete
|
// permanent delete
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1332,13 +1361,13 @@ async fn delete_cipher_selected_put_admin(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[put("/ciphers/<uuid>/restore")]
|
#[put("/ciphers/<uuid>/restore")]
|
||||||
async fn restore_cipher_put(uuid: String, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult {
|
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
|
_restore_cipher_by_uuid(uuid, &headers, &mut conn, &nt).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[put("/ciphers/<uuid>/restore-admin")]
|
#[put("/ciphers/<uuid>/restore-admin")]
|
||||||
async fn restore_cipher_put_admin(uuid: String, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult {
|
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
|
_restore_cipher_by_uuid(uuid, &headers, &mut conn, &nt).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[put("/ciphers/restore", data = "<data>")]
|
#[put("/ciphers/restore", data = "<data>")]
|
||||||
@@ -1392,7 +1421,15 @@ async fn move_cipher_selected(
|
|||||||
// Move cipher
|
// Move cipher
|
||||||
cipher.move_to_folder(data.FolderId.clone(), &user_uuid, &mut conn).await?;
|
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(())
|
Ok(())
|
||||||
@@ -1444,7 +1481,7 @@ async fn delete_all(
|
|||||||
log_event(
|
log_event(
|
||||||
EventType::OrganizationPurgedVault as i32,
|
EventType::OrganizationPurgedVault as i32,
|
||||||
&org_data.org_id,
|
&org_data.org_id,
|
||||||
org_data.org_id.clone(),
|
&org_data.org_id,
|
||||||
user.uuid,
|
user.uuid,
|
||||||
headers.device.atype,
|
headers.device.atype,
|
||||||
&headers.ip.ip,
|
&headers.ip.ip,
|
||||||
@@ -1473,6 +1510,7 @@ async fn delete_all(
|
|||||||
|
|
||||||
user.update_revision(&mut conn).await?;
|
user.update_revision(&mut conn).await?;
|
||||||
nt.send_user_update(UpdateType::SyncVault, &user).await;
|
nt.send_user_update(UpdateType::SyncVault, &user).await;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1502,6 +1540,8 @@ async fn _delete_cipher_by_uuid(
|
|||||||
&cipher,
|
&cipher,
|
||||||
&cipher.update_users_revision(conn).await,
|
&cipher.update_users_revision(conn).await,
|
||||||
&headers.device.uuid,
|
&headers.device.uuid,
|
||||||
|
None,
|
||||||
|
conn,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
} else {
|
} else {
|
||||||
@@ -1511,6 +1551,8 @@ async fn _delete_cipher_by_uuid(
|
|||||||
&cipher,
|
&cipher,
|
||||||
&cipher.update_users_revision(conn).await,
|
&cipher.update_users_revision(conn).await,
|
||||||
&headers.device.uuid,
|
&headers.device.uuid,
|
||||||
|
None,
|
||||||
|
conn,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
@@ -1524,7 +1566,7 @@ async fn _delete_cipher_by_uuid(
|
|||||||
log_event(
|
log_event(
|
||||||
event_type,
|
event_type,
|
||||||
&cipher.uuid,
|
&cipher.uuid,
|
||||||
org_uuid,
|
&org_uuid,
|
||||||
headers.user.uuid.clone(),
|
headers.user.uuid.clone(),
|
||||||
headers.device.atype,
|
headers.device.atype,
|
||||||
&headers.ip.ip,
|
&headers.ip.ip,
|
||||||
@@ -1580,13 +1622,16 @@ async fn _restore_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &mut DbCon
|
|||||||
&cipher,
|
&cipher,
|
||||||
&cipher.update_users_revision(conn).await,
|
&cipher.update_users_revision(conn).await,
|
||||||
&headers.device.uuid,
|
&headers.device.uuid,
|
||||||
|
None,
|
||||||
|
conn,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
if let Some(org_uuid) = &cipher.organization_uuid {
|
if let Some(org_uuid) = &cipher.organization_uuid {
|
||||||
log_event(
|
log_event(
|
||||||
EventType::CipherRestored as i32,
|
EventType::CipherRestored as i32,
|
||||||
&cipher.uuid.clone(),
|
&cipher.uuid.clone(),
|
||||||
String::from(org_uuid),
|
org_uuid,
|
||||||
headers.user.uuid.clone(),
|
headers.user.uuid.clone(),
|
||||||
headers.device.atype,
|
headers.device.atype,
|
||||||
&headers.ip.ip,
|
&headers.ip.ip,
|
||||||
@@ -1661,13 +1706,16 @@ async fn _delete_cipher_attachment_by_id(
|
|||||||
&cipher,
|
&cipher,
|
||||||
&cipher.update_users_revision(conn).await,
|
&cipher.update_users_revision(conn).await,
|
||||||
&headers.device.uuid,
|
&headers.device.uuid,
|
||||||
|
None,
|
||||||
|
conn,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
if let Some(org_uuid) = cipher.organization_uuid {
|
if let Some(org_uuid) = cipher.organization_uuid {
|
||||||
log_event(
|
log_event(
|
||||||
EventType::CipherAttachmentDeleted as i32,
|
EventType::CipherAttachmentDeleted as i32,
|
||||||
&cipher.uuid,
|
&cipher.uuid,
|
||||||
org_uuid,
|
&org_uuid,
|
||||||
headers.user.uuid.clone(),
|
headers.user.uuid.clone(),
|
||||||
headers.device.atype,
|
headers.device.atype,
|
||||||
&headers.ip.ip,
|
&headers.ip.ip,
|
||||||
|
|||||||
@@ -71,10 +71,10 @@ async fn get_grantees(headers: Headers, mut conn: DbConn) -> JsonResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/emergency-access/<emer_id>")]
|
#[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()?;
|
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)),
|
Some(emergency_access) => Ok(Json(emergency_access.to_json_grantee_details(&mut conn).await)),
|
||||||
None => err!("Emergency access not valid."),
|
None => err!("Emergency access not valid."),
|
||||||
}
|
}
|
||||||
@@ -93,17 +93,13 @@ struct EmergencyAccessUpdateData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[put("/emergency-access/<emer_id>", data = "<data>")]
|
#[put("/emergency-access/<emer_id>", data = "<data>")]
|
||||||
async fn put_emergency_access(
|
async fn put_emergency_access(emer_id: &str, data: JsonUpcase<EmergencyAccessUpdateData>, conn: DbConn) -> JsonResult {
|
||||||
emer_id: String,
|
|
||||||
data: JsonUpcase<EmergencyAccessUpdateData>,
|
|
||||||
conn: DbConn,
|
|
||||||
) -> JsonResult {
|
|
||||||
post_emergency_access(emer_id, data, conn).await
|
post_emergency_access(emer_id, data, conn).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/emergency-access/<emer_id>", data = "<data>")]
|
#[post("/emergency-access/<emer_id>", data = "<data>")]
|
||||||
async fn post_emergency_access(
|
async fn post_emergency_access(
|
||||||
emer_id: String,
|
emer_id: &str,
|
||||||
data: JsonUpcase<EmergencyAccessUpdateData>,
|
data: JsonUpcase<EmergencyAccessUpdateData>,
|
||||||
mut conn: DbConn,
|
mut conn: DbConn,
|
||||||
) -> JsonResult {
|
) -> JsonResult {
|
||||||
@@ -111,7 +107,7 @@ async fn post_emergency_access(
|
|||||||
|
|
||||||
let data: EmergencyAccessUpdateData = data.into_inner().data;
|
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,
|
Some(emergency_access) => emergency_access,
|
||||||
None => err!("Emergency access not valid."),
|
None => err!("Emergency access not valid."),
|
||||||
};
|
};
|
||||||
@@ -136,12 +132,12 @@ async fn post_emergency_access(
|
|||||||
// region delete
|
// region delete
|
||||||
|
|
||||||
#[delete("/emergency-access/<emer_id>")]
|
#[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()?;
|
check_emergency_access_allowed()?;
|
||||||
|
|
||||||
let grantor_user = headers.user;
|
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) => {
|
Some(emer) => {
|
||||||
if emer.grantor_uuid != grantor_user.uuid && emer.grantee_uuid != Some(grantor_user.uuid) {
|
if emer.grantor_uuid != grantor_user.uuid && emer.grantee_uuid != Some(grantor_user.uuid) {
|
||||||
err!("Emergency access not valid.")
|
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")]
|
#[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
|
delete_emergency_access(emer_id, headers, conn).await
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,7 +239,7 @@ async fn send_invite(data: JsonUpcase<EmergencyAccessInviteData>, headers: Heade
|
|||||||
} else {
|
} else {
|
||||||
// Automatically mark user as accepted if no email invites
|
// Automatically mark user as accepted if no email invites
|
||||||
match User::find_by_mail(&email, &mut conn).await {
|
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,
|
Ok(v) => v,
|
||||||
Err(e) => err!(e.to_string()),
|
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")]
|
#[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()?;
|
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,
|
Some(emer) => emer,
|
||||||
None => err!("Emergency access not valid."),
|
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
|
// 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,
|
Ok(v) => v,
|
||||||
Err(e) => err!(e.to_string()),
|
Err(e) => err!(e.to_string()),
|
||||||
}
|
}
|
||||||
@@ -315,12 +311,7 @@ struct AcceptData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/emergency-access/<emer_id>/accept", data = "<data>")]
|
#[post("/emergency-access/<emer_id>/accept", data = "<data>")]
|
||||||
async fn accept_invite(
|
async fn accept_invite(emer_id: &str, data: JsonUpcase<AcceptData>, headers: Headers, mut conn: DbConn) -> EmptyResult {
|
||||||
emer_id: String,
|
|
||||||
data: JsonUpcase<AcceptData>,
|
|
||||||
headers: Headers,
|
|
||||||
mut conn: DbConn,
|
|
||||||
) -> EmptyResult {
|
|
||||||
check_emergency_access_allowed()?;
|
check_emergency_access_allowed()?;
|
||||||
|
|
||||||
let data: AcceptData = data.into_inner().data;
|
let data: AcceptData = data.into_inner().data;
|
||||||
@@ -341,7 +332,7 @@ async fn accept_invite(
|
|||||||
None => err!("Invited user not found"),
|
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,
|
Some(emer) => emer,
|
||||||
None => err!("Emergency access not valid."),
|
None => err!("Emergency access not valid."),
|
||||||
};
|
};
|
||||||
@@ -356,7 +347,7 @@ async fn accept_invite(
|
|||||||
&& grantor_user.name == claims.grantor_name
|
&& grantor_user.name == claims.grantor_name
|
||||||
&& grantor_user.email == claims.grantor_email
|
&& 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,
|
Ok(v) => v,
|
||||||
Err(e) => err!(e.to_string()),
|
Err(e) => err!(e.to_string()),
|
||||||
}
|
}
|
||||||
@@ -372,7 +363,7 @@ async fn accept_invite(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn accept_invite_process(
|
async fn accept_invite_process(
|
||||||
grantee_uuid: String,
|
grantee_uuid: &str,
|
||||||
emergency_access: &mut EmergencyAccess,
|
emergency_access: &mut EmergencyAccess,
|
||||||
grantee_email: &str,
|
grantee_email: &str,
|
||||||
conn: &mut DbConn,
|
conn: &mut DbConn,
|
||||||
@@ -386,7 +377,7 @@ async fn accept_invite_process(
|
|||||||
}
|
}
|
||||||
|
|
||||||
emergency_access.status = EmergencyAccessStatus::Accepted as i32;
|
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.email = None;
|
||||||
emergency_access.save(conn).await
|
emergency_access.save(conn).await
|
||||||
}
|
}
|
||||||
@@ -399,7 +390,7 @@ struct ConfirmData {
|
|||||||
|
|
||||||
#[post("/emergency-access/<emer_id>/confirm", data = "<data>")]
|
#[post("/emergency-access/<emer_id>/confirm", data = "<data>")]
|
||||||
async fn confirm_emergency_access(
|
async fn confirm_emergency_access(
|
||||||
emer_id: String,
|
emer_id: &str,
|
||||||
data: JsonUpcase<ConfirmData>,
|
data: JsonUpcase<ConfirmData>,
|
||||||
headers: Headers,
|
headers: Headers,
|
||||||
mut conn: DbConn,
|
mut conn: DbConn,
|
||||||
@@ -410,7 +401,7 @@ async fn confirm_emergency_access(
|
|||||||
let data: ConfirmData = data.into_inner().data;
|
let data: ConfirmData = data.into_inner().data;
|
||||||
let key = data.Key;
|
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,
|
Some(emer) => emer,
|
||||||
None => err!("Emergency access not valid."),
|
None => err!("Emergency access not valid."),
|
||||||
};
|
};
|
||||||
@@ -452,11 +443,11 @@ async fn confirm_emergency_access(
|
|||||||
// region access emergency access
|
// region access emergency access
|
||||||
|
|
||||||
#[post("/emergency-access/<emer_id>/initiate")]
|
#[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()?;
|
check_emergency_access_allowed()?;
|
||||||
|
|
||||||
let initiating_user = headers.user;
|
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,
|
Some(emer) => emer,
|
||||||
None => err!("Emergency access not valid."),
|
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")]
|
#[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()?;
|
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,
|
Some(emer) => emer,
|
||||||
None => err!("Emergency access not valid."),
|
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")]
|
#[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()?;
|
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,
|
Some(emer) => emer,
|
||||||
None => err!("Emergency access not valid."),
|
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
|
// region action
|
||||||
|
|
||||||
#[post("/emergency-access/<emer_id>/view")]
|
#[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()?;
|
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,
|
Some(emer) => emer,
|
||||||
None => err!("Emergency access not valid."),
|
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.")
|
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")]
|
#[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()?;
|
check_emergency_access_allowed()?;
|
||||||
|
|
||||||
let requesting_user = headers.user;
|
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,
|
Some(emer) => emer,
|
||||||
None => err!("Emergency access not valid."),
|
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.")
|
err!("Emergency access not valid.")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -628,21 +619,15 @@ async fn takeover_emergency_access(emer_id: String, headers: Headers, mut conn:
|
|||||||
None => err!("Grantor user not found."),
|
None => err!("Grantor user not found."),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut result = json!({
|
let result = json!({
|
||||||
"Kdf": grantor_user.client_kdf_type,
|
"Kdf": grantor_user.client_kdf_type,
|
||||||
"KdfIterations": grantor_user.client_kdf_iter,
|
"KdfIterations": grantor_user.client_kdf_iter,
|
||||||
|
"KdfMemory": grantor_user.client_kdf_memory,
|
||||||
|
"KdfParallelism": grantor_user.client_kdf_parallelism,
|
||||||
"KeyEncrypted": &emergency_access.key_encrypted,
|
"KeyEncrypted": &emergency_access.key_encrypted,
|
||||||
"Object": "emergencyAccessTakeover",
|
"Object": "emergencyAccessTakeover",
|
||||||
});
|
});
|
||||||
|
|
||||||
if grantor_user.client_kdf_type == UserKdfType::Argon2id as i32 {
|
|
||||||
result["KdfMemory"] =
|
|
||||||
Value::Number(grantor_user.client_kdf_memory.expect("Argon2 memory parameter is required.").into());
|
|
||||||
result["KdfParallelism"] = Value::Number(
|
|
||||||
grantor_user.client_kdf_parallelism.expect("Argon2 parallelism parameter is required.").into(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Json(result))
|
Ok(Json(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -655,7 +640,7 @@ struct EmergencyAccessPasswordData {
|
|||||||
|
|
||||||
#[post("/emergency-access/<emer_id>/password", data = "<data>")]
|
#[post("/emergency-access/<emer_id>/password", data = "<data>")]
|
||||||
async fn password_emergency_access(
|
async fn password_emergency_access(
|
||||||
emer_id: String,
|
emer_id: &str,
|
||||||
data: JsonUpcase<EmergencyAccessPasswordData>,
|
data: JsonUpcase<EmergencyAccessPasswordData>,
|
||||||
headers: Headers,
|
headers: Headers,
|
||||||
mut conn: DbConn,
|
mut conn: DbConn,
|
||||||
@@ -667,12 +652,12 @@ async fn password_emergency_access(
|
|||||||
//let key = &data.Key;
|
//let key = &data.Key;
|
||||||
|
|
||||||
let requesting_user = headers.user;
|
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,
|
Some(emer) => emer,
|
||||||
None => err!("Emergency access not valid."),
|
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.")
|
err!("Emergency access not valid.")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -700,14 +685,14 @@ async fn password_emergency_access(
|
|||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
#[get("/emergency-access/<emer_id>/policies")]
|
#[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 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,
|
Some(emer) => emer,
|
||||||
None => err!("Emergency access not valid."),
|
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.")
|
err!("Emergency access not valid.")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -728,10 +713,11 @@ async fn policies_emergency_access(emer_id: String, headers: Headers, mut conn:
|
|||||||
|
|
||||||
fn is_valid_request(
|
fn is_valid_request(
|
||||||
emergency_access: &EmergencyAccess,
|
emergency_access: &EmergencyAccess,
|
||||||
requesting_user_uuid: String,
|
requesting_user_uuid: &str,
|
||||||
requested_access_type: EmergencyAccessType,
|
requested_access_type: EmergencyAccessType,
|
||||||
) -> bool {
|
) -> 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.status == EmergencyAccessStatus::RecoveryApproved as i32
|
||||||
&& emergency_access.atype == requested_access_type 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
|
// Upstream: https://github.com/bitwarden/server/blob/9ecf69d9cabce732cf2c57976dd9afa5728578fb/src/Api/Controllers/EventsController.cs#LL84C35-L84C41
|
||||||
#[get("/organizations/<org_id>/events?<data..>")]
|
#[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.
|
// Return an empty vec when we org events are disabled.
|
||||||
// This prevents client errors
|
// This prevents client errors
|
||||||
let events_json: Vec<Value> = if !CONFIG.org_events_enabled() {
|
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)
|
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
|
.await
|
||||||
.iter()
|
.iter()
|
||||||
.map(|e| e.to_json())
|
.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..>")]
|
#[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.
|
// Return an empty vec when we org events are disabled.
|
||||||
// This prevents client errors
|
// This prevents client errors
|
||||||
let events_json: Vec<Value> = if !CONFIG.org_events_enabled() {
|
let events_json: Vec<Value> = if !CONFIG.org_events_enabled() {
|
||||||
Vec::with_capacity(0)
|
Vec::with_capacity(0)
|
||||||
} else {
|
} else {
|
||||||
let mut events_json = Vec::with_capacity(0);
|
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 start_date = parse_date(&data.start);
|
||||||
let end_date = if let Some(before_date) = &data.continuation_token {
|
let end_date = if let Some(before_date) = &data.continuation_token {
|
||||||
parse_date(before_date)
|
parse_date(before_date)
|
||||||
@@ -75,7 +75,7 @@ async fn get_cipher_events(cipher_id: String, data: EventRange, headers: Headers
|
|||||||
parse_date(&data.end)
|
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
|
.await
|
||||||
.iter()
|
.iter()
|
||||||
.map(|e| e.to_json())
|
.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..>")]
|
#[get("/organizations/<org_id>/users/<user_org_id>/events?<data..>")]
|
||||||
async fn get_user_events(
|
async fn get_user_events(
|
||||||
org_id: String,
|
org_id: &str,
|
||||||
user_org_id: String,
|
user_org_id: &str,
|
||||||
data: EventRange,
|
data: EventRange,
|
||||||
_headers: AdminHeaders,
|
_headers: AdminHeaders,
|
||||||
mut conn: DbConn,
|
mut conn: DbConn,
|
||||||
@@ -111,7 +111,7 @@ async fn get_user_events(
|
|||||||
parse_date(&data.end)
|
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
|
.await
|
||||||
.iter()
|
.iter()
|
||||||
.map(|e| e.to_json())
|
.map(|e| e.to_json())
|
||||||
@@ -185,7 +185,7 @@ async fn post_events_collect(data: JsonUpcaseVec<EventCollection>, headers: Head
|
|||||||
_log_event(
|
_log_event(
|
||||||
event.Type,
|
event.Type,
|
||||||
org_uuid,
|
org_uuid,
|
||||||
String::from(org_uuid),
|
org_uuid,
|
||||||
&headers.user.uuid,
|
&headers.user.uuid,
|
||||||
headers.device.atype,
|
headers.device.atype,
|
||||||
Some(event_date),
|
Some(event_date),
|
||||||
@@ -202,7 +202,7 @@ async fn post_events_collect(data: JsonUpcaseVec<EventCollection>, headers: Head
|
|||||||
_log_event(
|
_log_event(
|
||||||
event.Type,
|
event.Type,
|
||||||
cipher_uuid,
|
cipher_uuid,
|
||||||
org_uuid,
|
&org_uuid,
|
||||||
&headers.user.uuid,
|
&headers.user.uuid,
|
||||||
headers.device.atype,
|
headers.device.atype,
|
||||||
Some(event_date),
|
Some(event_date),
|
||||||
@@ -262,7 +262,7 @@ async fn _log_user_event(
|
|||||||
pub async fn log_event(
|
pub async fn log_event(
|
||||||
event_type: i32,
|
event_type: i32,
|
||||||
source_uuid: &str,
|
source_uuid: &str,
|
||||||
org_uuid: String,
|
org_uuid: &str,
|
||||||
act_user_uuid: String,
|
act_user_uuid: String,
|
||||||
device_type: i32,
|
device_type: i32,
|
||||||
ip: &IpAddr,
|
ip: &IpAddr,
|
||||||
@@ -278,7 +278,7 @@ pub async fn log_event(
|
|||||||
async fn _log_event(
|
async fn _log_event(
|
||||||
event_type: i32,
|
event_type: i32,
|
||||||
source_uuid: &str,
|
source_uuid: &str,
|
||||||
org_uuid: String,
|
org_uuid: &str,
|
||||||
act_user_uuid: &str,
|
act_user_uuid: &str,
|
||||||
device_type: i32,
|
device_type: i32,
|
||||||
event_date: Option<NaiveDateTime>,
|
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.act_user_uuid = Some(String::from(act_user_uuid));
|
||||||
event.device_type = Some(device_type);
|
event.device_type = Some(device_type);
|
||||||
event.ip_address = Some(ip.to_string());
|
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>")]
|
#[get("/folders/<uuid>")]
|
||||||
async fn get_folder(uuid: String, headers: Headers, mut conn: DbConn) -> JsonResult {
|
async fn get_folder(uuid: &str, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||||
let folder = match Folder::find_by_uuid(&uuid, &mut conn).await {
|
let folder = match Folder::find_by_uuid(uuid, &mut conn).await {
|
||||||
Some(folder) => folder,
|
Some(folder) => folder,
|
||||||
_ => err!("Invalid 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);
|
let mut folder = Folder::new(headers.user.uuid, data.Name);
|
||||||
|
|
||||||
folder.save(&mut conn).await?;
|
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()))
|
Ok(Json(folder.to_json()))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/folders/<uuid>", data = "<data>")]
|
#[post("/folders/<uuid>", data = "<data>")]
|
||||||
async fn post_folder(
|
async fn post_folder(
|
||||||
uuid: String,
|
uuid: &str,
|
||||||
data: JsonUpcase<FolderData>,
|
data: JsonUpcase<FolderData>,
|
||||||
headers: Headers,
|
headers: Headers,
|
||||||
conn: DbConn,
|
conn: DbConn,
|
||||||
@@ -68,7 +68,7 @@ async fn post_folder(
|
|||||||
|
|
||||||
#[put("/folders/<uuid>", data = "<data>")]
|
#[put("/folders/<uuid>", data = "<data>")]
|
||||||
async fn put_folder(
|
async fn put_folder(
|
||||||
uuid: String,
|
uuid: &str,
|
||||||
data: JsonUpcase<FolderData>,
|
data: JsonUpcase<FolderData>,
|
||||||
headers: Headers,
|
headers: Headers,
|
||||||
mut conn: DbConn,
|
mut conn: DbConn,
|
||||||
@@ -76,7 +76,7 @@ async fn put_folder(
|
|||||||
) -> JsonResult {
|
) -> JsonResult {
|
||||||
let data: FolderData = data.into_inner().data;
|
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,
|
Some(folder) => folder,
|
||||||
_ => err!("Invalid folder"),
|
_ => err!("Invalid folder"),
|
||||||
};
|
};
|
||||||
@@ -88,19 +88,19 @@ async fn put_folder(
|
|||||||
folder.name = data.Name;
|
folder.name = data.Name;
|
||||||
|
|
||||||
folder.save(&mut conn).await?;
|
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()))
|
Ok(Json(folder.to_json()))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/folders/<uuid>/delete")]
|
#[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_folder(uuid, headers, conn, nt).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[delete("/folders/<uuid>")]
|
#[delete("/folders/<uuid>")]
|
||||||
async fn delete_folder(uuid: String, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
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 {
|
let folder = match Folder::find_by_uuid(uuid, &mut conn).await {
|
||||||
Some(folder) => folder,
|
Some(folder) => folder,
|
||||||
_ => err!("Invalid 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
|
// Delete the actual folder entry
|
||||||
folder.delete(&mut conn).await?;
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ mod emergency_access;
|
|||||||
mod events;
|
mod events;
|
||||||
mod folders;
|
mod folders;
|
||||||
mod organizations;
|
mod organizations;
|
||||||
|
mod public;
|
||||||
mod sends;
|
mod sends;
|
||||||
pub mod two_factor;
|
pub mod two_factor;
|
||||||
|
|
||||||
|
pub use accounts::purge_auth_requests;
|
||||||
pub use ciphers::{purge_trashed_ciphers, CipherData, CipherSyncData, CipherSyncType};
|
pub use ciphers::{purge_trashed_ciphers, CipherData, CipherSyncData, CipherSyncType};
|
||||||
pub use emergency_access::{emergency_notification_reminder_job, emergency_request_timeout_job};
|
pub use emergency_access::{emergency_notification_reminder_job, emergency_request_timeout_job};
|
||||||
pub use events::{event_cleanup_job, log_event, log_user_event};
|
pub use events::{event_cleanup_job, log_event, log_user_event};
|
||||||
@@ -14,7 +16,6 @@ pub use sends::purge_sends;
|
|||||||
pub use two_factor::send_incomplete_2fa_notifications;
|
pub use two_factor::send_incomplete_2fa_notifications;
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
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 eq_domains_routes = routes![get_eq_domains, post_eq_domains, put_eq_domains];
|
||||||
let mut hibp_routes = routes![hibp_breach];
|
let mut hibp_routes = routes![hibp_breach];
|
||||||
let mut meta_routes = routes![alive, now, version, config];
|
let mut meta_routes = routes![alive, now, version, config];
|
||||||
@@ -28,7 +29,7 @@ pub fn routes() -> Vec<Route> {
|
|||||||
routes.append(&mut organizations::routes());
|
routes.append(&mut organizations::routes());
|
||||||
routes.append(&mut two_factor::routes());
|
routes.append(&mut two_factor::routes());
|
||||||
routes.append(&mut sends::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 eq_domains_routes);
|
||||||
routes.append(&mut hibp_routes);
|
routes.append(&mut hibp_routes);
|
||||||
routes.append(&mut meta_routes);
|
routes.append(&mut meta_routes);
|
||||||
@@ -57,37 +58,6 @@ use crate::{
|
|||||||
util::get_reqwest_client,
|
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)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
struct GlobalDomain {
|
struct GlobalDomain {
|
||||||
@@ -170,7 +140,7 @@ async fn put_eq_domains(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/hibp/breach?<username>")]
|
#[get("/hibp/breach?<username>")]
|
||||||
async fn hibp_breach(username: String) -> JsonResult {
|
async fn hibp_breach(username: &str) -> JsonResult {
|
||||||
let url = format!(
|
let url = format!(
|
||||||
"https://haveibeenpwned.com/api/v3/breachedaccount/{username}?truncateResponse=false&includeUnverified=false"
|
"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() || user.password_hash.is_empty() {
|
||||||
|
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>")]
|
#[get("/sends/<uuid>")]
|
||||||
async fn get_send(uuid: String, headers: Headers, mut conn: DbConn) -> JsonResult {
|
async fn get_send(uuid: &str, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||||
let send = match Send::find_by_uuid(&uuid, &mut conn).await {
|
let send = match Send::find_by_uuid(uuid, &mut conn).await {
|
||||||
Some(send) => send,
|
Some(send) => send,
|
||||||
None => err!("Send not found"),
|
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)?;
|
let mut send = create_send(data, headers.user.uuid)?;
|
||||||
send.save(&mut conn).await?;
|
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()))
|
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
|
// Save the changes in the database
|
||||||
send.save(&mut conn).await?;
|
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()))
|
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
|
// 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>")]
|
#[post("/sends/<send_uuid>/file/<file_id>", format = "multipart/form-data", data = "<data>")]
|
||||||
async fn post_send_file_v2_data(
|
async fn post_send_file_v2_data(
|
||||||
send_uuid: String,
|
send_uuid: &str,
|
||||||
file_id: String,
|
file_id: &str,
|
||||||
data: Form<UploadDataV2<'_>>,
|
data: Form<UploadDataV2<'_>>,
|
||||||
headers: Headers,
|
headers: Headers,
|
||||||
mut conn: DbConn,
|
mut conn: DbConn,
|
||||||
@@ -326,20 +340,30 @@ async fn post_send_file_v2_data(
|
|||||||
|
|
||||||
let mut data = data.into_inner();
|
let mut data = data.into_inner();
|
||||||
|
|
||||||
if let Some(send) = Send::find_by_uuid(&send_uuid, &mut conn).await {
|
let Some(send) = Send::find_by_uuid(send_uuid, &mut conn).await else { err!("Send not found. Unable to save the file.") };
|
||||||
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 {
|
let Some(send_user_id) = &send.user_uuid else {err!("Sends are only supported for users at the moment")};
|
||||||
data.data.move_copy_to(file_path).await?
|
if send_user_id != &headers.user.uuid {
|
||||||
}
|
err!("Send doesn't belong to user");
|
||||||
|
|
||||||
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 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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,13 +375,13 @@ pub struct SendAccessData {
|
|||||||
|
|
||||||
#[post("/sends/access/<access_id>", data = "<data>")]
|
#[post("/sends/access/<access_id>", data = "<data>")]
|
||||||
async fn post_access(
|
async fn post_access(
|
||||||
access_id: String,
|
access_id: &str,
|
||||||
data: JsonUpcase<SendAccessData>,
|
data: JsonUpcase<SendAccessData>,
|
||||||
mut conn: DbConn,
|
mut conn: DbConn,
|
||||||
ip: ClientIp,
|
ip: ClientIp,
|
||||||
nt: Notify<'_>,
|
nt: Notify<'_>,
|
||||||
) -> JsonResult {
|
) -> 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,
|
Some(s) => s,
|
||||||
None => err_code!(SEND_INACCESSIBLE_MSG, 404),
|
None => err_code!(SEND_INACCESSIBLE_MSG, 404),
|
||||||
};
|
};
|
||||||
@@ -397,21 +421,28 @@ async fn post_access(
|
|||||||
|
|
||||||
send.save(&mut conn).await?;
|
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))
|
Ok(Json(send.to_json_access(&mut conn).await))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/sends/<send_id>/access/file/<file_id>", data = "<data>")]
|
#[post("/sends/<send_id>/access/file/<file_id>", data = "<data>")]
|
||||||
async fn post_access_file(
|
async fn post_access_file(
|
||||||
send_id: String,
|
send_id: &str,
|
||||||
file_id: String,
|
file_id: &str,
|
||||||
data: JsonUpcase<SendAccessData>,
|
data: JsonUpcase<SendAccessData>,
|
||||||
host: Host,
|
host: Host,
|
||||||
mut conn: DbConn,
|
mut conn: DbConn,
|
||||||
nt: Notify<'_>,
|
nt: Notify<'_>,
|
||||||
) -> JsonResult {
|
) -> 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,
|
Some(s) => s,
|
||||||
None => err_code!(SEND_INACCESSIBLE_MSG, 404),
|
None => err_code!(SEND_INACCESSIBLE_MSG, 404),
|
||||||
};
|
};
|
||||||
@@ -448,9 +479,16 @@ async fn post_access_file(
|
|||||||
|
|
||||||
send.save(&mut conn).await?;
|
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);
|
let token = crate::auth::encode_jwt(&token_claims);
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"Object": "send-fileDownload",
|
"Object": "send-fileDownload",
|
||||||
@@ -460,8 +498,8 @@ async fn post_access_file(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/sends/<send_id>/<file_id>?<t>")]
|
#[get("/sends/<send_id>/<file_id>?<t>")]
|
||||||
async fn download_send(send_id: SafeString, file_id: SafeString, t: String) -> Option<NamedFile> {
|
async fn download_send(send_id: SafeString, file_id: SafeString, t: &str) -> Option<NamedFile> {
|
||||||
if let Ok(claims) = crate::auth::decode_send(&t) {
|
if let Ok(claims) = crate::auth::decode_send(t) {
|
||||||
if claims.sub == format!("{send_id}/{file_id}") {
|
if claims.sub == format!("{send_id}/{file_id}") {
|
||||||
return NamedFile::open(Path::new(&CONFIG.sends_folder()).join(send_id).join(file_id)).await.ok();
|
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>")]
|
#[put("/sends/<id>", data = "<data>")]
|
||||||
async fn put_send(
|
async fn put_send(
|
||||||
id: String,
|
id: &str,
|
||||||
data: JsonUpcase<SendData>,
|
data: JsonUpcase<SendData>,
|
||||||
headers: Headers,
|
headers: Headers,
|
||||||
mut conn: DbConn,
|
mut conn: DbConn,
|
||||||
@@ -482,7 +520,7 @@ async fn put_send(
|
|||||||
let data: SendData = data.into_inner().data;
|
let data: SendData = data.into_inner().data;
|
||||||
enforce_disable_hide_email_policy(&data, &headers, &mut conn).await?;
|
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,
|
Some(s) => s,
|
||||||
None => err!("Send not found"),
|
None => err!("Send not found"),
|
||||||
};
|
};
|
||||||
@@ -530,14 +568,21 @@ async fn put_send(
|
|||||||
}
|
}
|
||||||
|
|
||||||
send.save(&mut conn).await?;
|
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()))
|
Ok(Json(send.to_json()))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[delete("/sends/<id>")]
|
#[delete("/sends/<id>")]
|
||||||
async fn delete_send(id: String, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
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 {
|
let send = match Send::find_by_uuid(id, &mut conn).await {
|
||||||
Some(s) => s,
|
Some(s) => s,
|
||||||
None => err!("Send not found"),
|
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?;
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[put("/sends/<id>/remove-password")]
|
#[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?;
|
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,
|
Some(s) => s,
|
||||||
None => err!("Send not found"),
|
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.set_password(None);
|
||||||
send.save(&mut conn).await?;
|
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()))
|
Ok(Json(send.to_json()))
|
||||||
}
|
}
|
||||||
|
|||||||
253
src/api/icons.rs
253
src/api/icons.rs
@@ -19,7 +19,7 @@ use tokio::{
|
|||||||
net::lookup_host,
|
net::lookup_host,
|
||||||
};
|
};
|
||||||
|
|
||||||
use html5gum::{Emitter, EndTag, HtmlString, InfallibleTokenizer, Readable, StartTag, StringReader, Tokenizer};
|
use html5gum::{Emitter, HtmlString, InfallibleTokenizer, Readable, StringReader, Tokenizer};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
error::Error,
|
error::Error,
|
||||||
@@ -46,10 +46,15 @@ static CLIENT: Lazy<Client> = Lazy::new(|| {
|
|||||||
// Generate the cookie store
|
// Generate the cookie store
|
||||||
let cookie_store = Arc::new(Jar::default());
|
let cookie_store = Arc::new(Jar::default());
|
||||||
|
|
||||||
|
let icon_download_timeout = Duration::from_secs(CONFIG.icon_download_timeout());
|
||||||
|
let pool_idle_timeout = Duration::from_secs(10);
|
||||||
// Reuse the client between requests
|
// Reuse the client between requests
|
||||||
let client = get_reqwest_client_builder()
|
let client = get_reqwest_client_builder()
|
||||||
.cookie_provider(Arc::clone(&cookie_store))
|
.cookie_provider(Arc::clone(&cookie_store))
|
||||||
.timeout(Duration::from_secs(CONFIG.icon_download_timeout()))
|
.timeout(icon_download_timeout)
|
||||||
|
.pool_max_idle_per_host(5) // Configure the Hyper Pool to only have max 5 idle connections
|
||||||
|
.pool_idle_timeout(pool_idle_timeout) // Configure the Hyper Pool to timeout after 10 seconds
|
||||||
|
.trust_dns(true)
|
||||||
.default_headers(default_headers.clone());
|
.default_headers(default_headers.clone());
|
||||||
|
|
||||||
match client.build() {
|
match client.build() {
|
||||||
@@ -58,9 +63,11 @@ static CLIENT: Lazy<Client> = Lazy::new(|| {
|
|||||||
error!("Possible trust-dns error, trying with trust-dns disabled: '{e}'");
|
error!("Possible trust-dns error, trying with trust-dns disabled: '{e}'");
|
||||||
get_reqwest_client_builder()
|
get_reqwest_client_builder()
|
||||||
.cookie_provider(cookie_store)
|
.cookie_provider(cookie_store)
|
||||||
.timeout(Duration::from_secs(CONFIG.icon_download_timeout()))
|
.timeout(icon_download_timeout)
|
||||||
.default_headers(default_headers)
|
.pool_max_idle_per_host(5) // Configure the Hyper Pool to only have max 5 idle connections
|
||||||
|
.pool_idle_timeout(pool_idle_timeout) // Configure the Hyper Pool to timeout after 10 seconds
|
||||||
.trust_dns(false)
|
.trust_dns(false)
|
||||||
|
.default_headers(default_headers)
|
||||||
.build()
|
.build()
|
||||||
.expect("Failed to build client")
|
.expect("Failed to build client")
|
||||||
}
|
}
|
||||||
@@ -97,15 +104,15 @@ async fn icon_redirect(domain: &str, template: &str) -> Option<Redirect> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/<domain>/icon.png")]
|
#[get("/<domain>/icon.png")]
|
||||||
async fn icon_external(domain: String) -> Option<Redirect> {
|
async fn icon_external(domain: &str) -> Option<Redirect> {
|
||||||
icon_redirect(&domain, &CONFIG._icon_service_url()).await
|
icon_redirect(domain, &CONFIG._icon_service_url()).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/<domain>/icon.png")]
|
#[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");
|
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);
|
warn!("Invalid domain: {}", domain);
|
||||||
return Cached::ttl(
|
return Cached::ttl(
|
||||||
(ContentType::new("image", "png"), FALLBACK_ICON.to_vec()),
|
(ContentType::new("image", "png"), FALLBACK_ICON.to_vec()),
|
||||||
@@ -114,7 +121,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)) => {
|
Some((icon, icon_type)) => {
|
||||||
Cached::ttl((ContentType::new("image", icon_type), icon), CONFIG.icon_cache_ttl(), true)
|
Cached::ttl((ContentType::new("image", icon_type), icon), CONFIG.icon_cache_ttl(), true)
|
||||||
}
|
}
|
||||||
@@ -258,7 +265,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Clone)]
|
||||||
enum DomainBlacklistReason {
|
enum DomainBlacklistReason {
|
||||||
Regex,
|
Regex,
|
||||||
IP,
|
IP,
|
||||||
@@ -415,38 +422,34 @@ fn get_favicons_node(
|
|||||||
const TAG_LINK: &[u8] = b"link";
|
const TAG_LINK: &[u8] = b"link";
|
||||||
const TAG_BASE: &[u8] = b"base";
|
const TAG_BASE: &[u8] = b"base";
|
||||||
const TAG_HEAD: &[u8] = b"head";
|
const TAG_HEAD: &[u8] = b"head";
|
||||||
const ATTR_REL: &[u8] = b"rel";
|
|
||||||
const ATTR_HREF: &[u8] = b"href";
|
const ATTR_HREF: &[u8] = b"href";
|
||||||
const ATTR_SIZES: &[u8] = b"sizes";
|
const ATTR_SIZES: &[u8] = b"sizes";
|
||||||
|
|
||||||
let mut base_url = url.clone();
|
let mut base_url = url.clone();
|
||||||
let mut icon_tags: Vec<StartTag> = Vec::new();
|
let mut icon_tags: Vec<Tag> = Vec::new();
|
||||||
for token in dom {
|
for token in dom {
|
||||||
match token {
|
let tag_name: &[u8] = &token.tag.name;
|
||||||
FaviconToken::StartTag(tag) => {
|
match tag_name {
|
||||||
if *tag.name == TAG_LINK
|
TAG_LINK => {
|
||||||
&& tag.attributes.contains_key(ATTR_REL)
|
icon_tags.push(token.tag);
|
||||||
&& tag.attributes.contains_key(ATTR_HREF)
|
|
||||||
{
|
|
||||||
let rel_value = std::str::from_utf8(tag.attributes.get(ATTR_REL).unwrap())
|
|
||||||
.unwrap_or_default()
|
|
||||||
.to_ascii_lowercase();
|
|
||||||
if rel_value.contains("icon") && !rel_value.contains("mask-icon") {
|
|
||||||
icon_tags.push(tag);
|
|
||||||
}
|
|
||||||
} else if *tag.name == TAG_BASE && tag.attributes.contains_key(ATTR_HREF) {
|
|
||||||
let href = std::str::from_utf8(tag.attributes.get(ATTR_HREF).unwrap()).unwrap_or_default();
|
|
||||||
debug!("Found base href: {href}");
|
|
||||||
base_url = match base_url.join(href) {
|
|
||||||
Ok(inner_url) => inner_url,
|
|
||||||
_ => url.clone(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
FaviconToken::EndTag(tag) => {
|
TAG_BASE => {
|
||||||
if *tag.name == TAG_HEAD {
|
base_url = if let Some(href) = token.tag.attributes.get(ATTR_HREF) {
|
||||||
break;
|
let href = std::str::from_utf8(href).unwrap_or_default();
|
||||||
}
|
debug!("Found base href: {href}");
|
||||||
|
match base_url.join(href) {
|
||||||
|
Ok(inner_url) => inner_url,
|
||||||
|
_ => continue,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
TAG_HEAD if token.closing => {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -682,7 +685,7 @@ async fn download_icon(domain: &str) -> Result<(Bytes, Option<&str>), Error> {
|
|||||||
|
|
||||||
for icon in icon_result.iconlist.iter().take(5) {
|
for icon in icon_result.iconlist.iter().take(5) {
|
||||||
if icon.href.starts_with("data:image") {
|
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
|
// Check if we are able to decode the data uri
|
||||||
let mut body = BytesMut::new();
|
let mut body = BytesMut::new();
|
||||||
match datauri.decode::<_, ()>(|bytes| {
|
match datauri.decode::<_, ()>(|bytes| {
|
||||||
@@ -820,43 +823,64 @@ impl reqwest::cookie::CookieStore for Jar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Custom FaviconEmitter for the html5gum parser.
|
/// Custom FaviconEmitter for the html5gum parser.
|
||||||
/// The FaviconEmitter is using an almost 1:1 copy of the DefaultEmitter with some small changes.
|
/// The FaviconEmitter is using an optimized version of the DefaultEmitter.
|
||||||
/// This prevents emitting tags like comments, doctype and also strings between the tags.
|
/// This prevents emitting tags like comments, doctype and also strings between the tags.
|
||||||
|
/// But it will also only emit the tags we need and only if they have the correct attributes
|
||||||
/// Therefor parsing the HTML content is faster.
|
/// Therefor parsing the HTML content is faster.
|
||||||
use std::collections::{BTreeSet, VecDeque};
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Default)]
|
||||||
enum FaviconToken {
|
pub struct Tag {
|
||||||
StartTag(StartTag),
|
/// The tag's name, such as `"link"` or `"base"`.
|
||||||
EndTag(EndTag),
|
pub name: HtmlString,
|
||||||
|
|
||||||
|
/// A mapping for any HTML attributes this start tag may have.
|
||||||
|
///
|
||||||
|
/// Duplicate attributes are ignored after the first one as per WHATWG spec.
|
||||||
|
pub attributes: BTreeMap<HtmlString, HtmlString>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
struct FaviconToken {
|
||||||
|
tag: Tag,
|
||||||
|
closing: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
struct FaviconEmitter {
|
struct FaviconEmitter {
|
||||||
current_token: Option<FaviconToken>,
|
current_token: Option<FaviconToken>,
|
||||||
last_start_tag: HtmlString,
|
last_start_tag: HtmlString,
|
||||||
current_attribute: Option<(HtmlString, HtmlString)>,
|
current_attribute: Option<(HtmlString, HtmlString)>,
|
||||||
seen_attributes: BTreeSet<HtmlString>,
|
emit_token: bool,
|
||||||
emitted_tokens: VecDeque<FaviconToken>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FaviconEmitter {
|
impl FaviconEmitter {
|
||||||
fn emit_token(&mut self, token: FaviconToken) {
|
fn flush_current_attribute(&mut self, emit_current_tag: bool) {
|
||||||
self.emitted_tokens.push_front(token);
|
const ATTR_HREF: &[u8] = b"href";
|
||||||
}
|
const ATTR_REL: &[u8] = b"rel";
|
||||||
|
const TAG_LINK: &[u8] = b"link";
|
||||||
|
const TAG_BASE: &[u8] = b"base";
|
||||||
|
const TAG_HEAD: &[u8] = b"head";
|
||||||
|
|
||||||
fn flush_current_attribute(&mut self) {
|
if let Some(ref mut token) = self.current_token {
|
||||||
if let Some((k, v)) = self.current_attribute.take() {
|
let tag_name: &[u8] = &token.tag.name;
|
||||||
match self.current_token {
|
|
||||||
Some(FaviconToken::StartTag(ref mut tag)) => {
|
if self.current_attribute.is_some() && (tag_name == TAG_BASE || tag_name == TAG_LINK) {
|
||||||
tag.attributes.entry(k).and_modify(|_| {}).or_insert(v);
|
let (k, v) = self.current_attribute.take().unwrap();
|
||||||
}
|
token.tag.attributes.entry(k).and_modify(|_| {}).or_insert(v);
|
||||||
Some(FaviconToken::EndTag(_)) => {
|
}
|
||||||
self.seen_attributes.insert(k);
|
|
||||||
}
|
let tag_attr = &token.tag.attributes;
|
||||||
_ => {
|
match tag_name {
|
||||||
debug_assert!(false);
|
TAG_HEAD if token.closing => self.emit_token = true,
|
||||||
|
TAG_BASE if tag_attr.contains_key(ATTR_HREF) => self.emit_token = true,
|
||||||
|
TAG_LINK if emit_current_tag && tag_attr.contains_key(ATTR_REL) && tag_attr.contains_key(ATTR_HREF) => {
|
||||||
|
let rel_value =
|
||||||
|
std::str::from_utf8(token.tag.attributes.get(ATTR_REL).unwrap()).unwrap_or_default();
|
||||||
|
if rel_value.contains("icon") && !rel_value.contains("mask-icon") {
|
||||||
|
self.emit_token = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
_ => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -871,86 +895,71 @@ impl Emitter for FaviconEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn pop_token(&mut self) -> Option<Self::Token> {
|
fn pop_token(&mut self) -> Option<Self::Token> {
|
||||||
self.emitted_tokens.pop_back()
|
if self.emit_token {
|
||||||
}
|
self.emit_token = false;
|
||||||
|
return self.current_token.take();
|
||||||
fn init_start_tag(&mut self) {
|
|
||||||
self.current_token = Some(FaviconToken::StartTag(StartTag::default()));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn init_end_tag(&mut self) {
|
|
||||||
self.current_token = Some(FaviconToken::EndTag(EndTag::default()));
|
|
||||||
self.seen_attributes.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn emit_current_tag(&mut self) -> Option<html5gum::State> {
|
|
||||||
self.flush_current_attribute();
|
|
||||||
let mut token = self.current_token.take().unwrap();
|
|
||||||
let mut emit = false;
|
|
||||||
match token {
|
|
||||||
FaviconToken::EndTag(ref mut tag) => {
|
|
||||||
// Always clean seen attributes
|
|
||||||
self.seen_attributes.clear();
|
|
||||||
|
|
||||||
// Only trigger an emit for the </head> tag.
|
|
||||||
// This is matched, and will break the for-loop.
|
|
||||||
if *tag.name == b"head" {
|
|
||||||
emit = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
FaviconToken::StartTag(ref mut tag) => {
|
|
||||||
// Only trriger an emit for <link> and <base> tags.
|
|
||||||
// These are the only tags we want to parse.
|
|
||||||
if *tag.name == b"link" || *tag.name == b"base" {
|
|
||||||
self.set_last_start_tag(Some(&tag.name));
|
|
||||||
emit = true;
|
|
||||||
} else {
|
|
||||||
self.set_last_start_tag(None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only emit the tags we want to parse.
|
|
||||||
if emit {
|
|
||||||
self.emit_token(token);
|
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn init_start_tag(&mut self) {
|
||||||
|
self.current_token = Some(FaviconToken {
|
||||||
|
tag: Tag::default(),
|
||||||
|
closing: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_end_tag(&mut self) {
|
||||||
|
self.current_token = Some(FaviconToken {
|
||||||
|
tag: Tag::default(),
|
||||||
|
closing: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit_current_tag(&mut self) -> Option<html5gum::State> {
|
||||||
|
self.flush_current_attribute(true);
|
||||||
|
self.last_start_tag.clear();
|
||||||
|
if self.current_token.is_some() && !self.current_token.as_ref().unwrap().closing {
|
||||||
|
self.last_start_tag.extend(&*self.current_token.as_ref().unwrap().tag.name);
|
||||||
|
}
|
||||||
|
html5gum::naive_next_state(&self.last_start_tag)
|
||||||
|
}
|
||||||
|
|
||||||
fn push_tag_name(&mut self, s: &[u8]) {
|
fn push_tag_name(&mut self, s: &[u8]) {
|
||||||
match self.current_token {
|
if let Some(ref mut token) = self.current_token {
|
||||||
Some(
|
token.tag.name.extend(s);
|
||||||
FaviconToken::StartTag(StartTag {
|
|
||||||
ref mut name,
|
|
||||||
..
|
|
||||||
})
|
|
||||||
| FaviconToken::EndTag(EndTag {
|
|
||||||
ref mut name,
|
|
||||||
..
|
|
||||||
}),
|
|
||||||
) => {
|
|
||||||
name.extend(s);
|
|
||||||
}
|
|
||||||
_ => debug_assert!(false),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init_attribute(&mut self) {
|
fn init_attribute(&mut self) {
|
||||||
self.flush_current_attribute();
|
self.flush_current_attribute(false);
|
||||||
self.current_attribute = Some(Default::default());
|
self.current_attribute = match &self.current_token {
|
||||||
|
Some(token) => {
|
||||||
|
let tag_name: &[u8] = &token.tag.name;
|
||||||
|
match tag_name {
|
||||||
|
b"link" | b"head" | b"base" => Some(Default::default()),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push_attribute_name(&mut self, s: &[u8]) {
|
fn push_attribute_name(&mut self, s: &[u8]) {
|
||||||
self.current_attribute.as_mut().unwrap().0.extend(s);
|
if let Some(attr) = &mut self.current_attribute {
|
||||||
|
attr.0.extend(s)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push_attribute_value(&mut self, s: &[u8]) {
|
fn push_attribute_value(&mut self, s: &[u8]) {
|
||||||
self.current_attribute.as_mut().unwrap().1.extend(s);
|
if let Some(attr) = &mut self.current_attribute {
|
||||||
|
attr.1.extend(s)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_is_appropriate_end_tag_token(&mut self) -> bool {
|
fn current_is_appropriate_end_tag_token(&mut self) -> bool {
|
||||||
match self.current_token {
|
match &self.current_token {
|
||||||
Some(FaviconToken::EndTag(ref tag)) => !self.last_start_tag.is_empty() && self.last_start_tag == tag.name,
|
Some(token) if token.closing => !self.last_start_tag.is_empty() && self.last_start_tag == token.tag.name,
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ use crate::{
|
|||||||
core::two_factor::{duo, email, email::EmailTokenData, yubikey},
|
core::two_factor::{duo, email, email::EmailTokenData, yubikey},
|
||||||
ApiResult, EmptyResult, JsonResult, JsonUpcase,
|
ApiResult, EmptyResult, JsonResult, JsonUpcase,
|
||||||
},
|
},
|
||||||
auth::{ClientHeaders, ClientIp},
|
auth::{generate_organization_api_key_login_claims, ClientHeaders, ClientIp},
|
||||||
db::{models::*, DbConn},
|
db::{models::*, DbConn},
|
||||||
error::MapResult,
|
error::MapResult,
|
||||||
mail, util, CONFIG,
|
mail, util, CONFIG,
|
||||||
@@ -107,7 +107,7 @@ async fn _refresh_login(data: ConnectData, conn: &mut DbConn) -> JsonResult {
|
|||||||
let (access_token, expires_in) = device.refresh_tokens(&user, orgs, scope_vec);
|
let (access_token, expires_in) = device.refresh_tokens(&user, orgs, scope_vec);
|
||||||
device.save(conn).await?;
|
device.save(conn).await?;
|
||||||
|
|
||||||
let mut result = json!({
|
let result = json!({
|
||||||
"access_token": access_token,
|
"access_token": access_token,
|
||||||
"expires_in": expires_in,
|
"expires_in": expires_in,
|
||||||
"token_type": "Bearer",
|
"token_type": "Bearer",
|
||||||
@@ -117,18 +117,13 @@ async fn _refresh_login(data: ConnectData, conn: &mut DbConn) -> JsonResult {
|
|||||||
|
|
||||||
"Kdf": user.client_kdf_type,
|
"Kdf": user.client_kdf_type,
|
||||||
"KdfIterations": user.client_kdf_iter,
|
"KdfIterations": user.client_kdf_iter,
|
||||||
|
"KdfMemory": user.client_kdf_memory,
|
||||||
|
"KdfParallelism": user.client_kdf_parallelism,
|
||||||
"ResetMasterPassword": false, // TODO: according to official server seems something like: user.password_hash.is_empty(), but would need testing
|
"ResetMasterPassword": false, // TODO: according to official server seems something like: user.password_hash.is_empty(), but would need testing
|
||||||
"scope": scope,
|
"scope": scope,
|
||||||
"unofficialServer": true,
|
"unofficialServer": true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if user.client_kdf_type == UserKdfType::Argon2id as i32 {
|
|
||||||
result["KdfMemory"] =
|
|
||||||
Value::Number(user.client_kdf_memory.expect("Argon2 memory parameter is required.").into());
|
|
||||||
result["KdfParallelism"] =
|
|
||||||
Value::Number(user.client_kdf_parallelism.expect("Argon2 parallelism parameter is required.").into());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Json(result))
|
Ok(Json(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,7 +155,27 @@ async fn _password_login(
|
|||||||
|
|
||||||
// Check password
|
// Check password
|
||||||
let password = data.password.as_ref().unwrap();
|
let password = data.password.as_ref().unwrap();
|
||||||
if !user.check_valid_password(password) {
|
if let Some(auth_request_uuid) = data.auth_request.clone() {
|
||||||
|
if let Some(auth_request) = AuthRequest::find_by_uuid(auth_request_uuid.as_str(), conn).await {
|
||||||
|
if !auth_request.check_access_code(password) {
|
||||||
|
err!(
|
||||||
|
"Username or access code is incorrect. Try again",
|
||||||
|
format!("IP: {}. Username: {}.", ip.ip, username),
|
||||||
|
ErrorEvent {
|
||||||
|
event: EventType::UserFailedLogIn,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err!(
|
||||||
|
"Auth request not found. Try again.",
|
||||||
|
format!("IP: {}. Username: {}.", ip.ip, username),
|
||||||
|
ErrorEvent {
|
||||||
|
event: EventType::UserFailedLogIn,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if !user.check_valid_password(password) {
|
||||||
err!(
|
err!(
|
||||||
"Username or password is incorrect. Try again",
|
"Username or password is incorrect. Try again",
|
||||||
format!("IP: {}. Username: {}.", ip.ip, username),
|
format!("IP: {}. Username: {}.", ip.ip, username),
|
||||||
@@ -260,22 +275,21 @@ async fn _password_login(
|
|||||||
|
|
||||||
"Kdf": user.client_kdf_type,
|
"Kdf": user.client_kdf_type,
|
||||||
"KdfIterations": user.client_kdf_iter,
|
"KdfIterations": user.client_kdf_iter,
|
||||||
|
"KdfMemory": user.client_kdf_memory,
|
||||||
|
"KdfParallelism": user.client_kdf_parallelism,
|
||||||
"ResetMasterPassword": false,// TODO: Same as above
|
"ResetMasterPassword": false,// TODO: Same as above
|
||||||
"scope": scope,
|
"scope": scope,
|
||||||
"unofficialServer": true,
|
"unofficialServer": true,
|
||||||
|
"UserDecryptionOptions": {
|
||||||
|
"HasMasterPassword": !user.password_hash.is_empty(),
|
||||||
|
"Object": "userDecryptionOptions"
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(token) = twofactor_token {
|
if let Some(token) = twofactor_token {
|
||||||
result["TwoFactorToken"] = Value::String(token);
|
result["TwoFactorToken"] = Value::String(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.client_kdf_type == UserKdfType::Argon2id as i32 {
|
|
||||||
result["KdfMemory"] =
|
|
||||||
Value::Number(user.client_kdf_memory.expect("Argon2 memory parameter is required.").into());
|
|
||||||
result["KdfParallelism"] =
|
|
||||||
Value::Number(user.client_kdf_parallelism.expect("Argon2 parallelism parameter is required.").into());
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("User {} logged in successfully. IP: {}", username, ip.ip);
|
info!("User {} logged in successfully. IP: {}", username, ip.ip);
|
||||||
Ok(Json(result))
|
Ok(Json(result))
|
||||||
}
|
}
|
||||||
@@ -286,16 +300,23 @@ async fn _api_key_login(
|
|||||||
conn: &mut DbConn,
|
conn: &mut DbConn,
|
||||||
ip: &ClientIp,
|
ip: &ClientIp,
|
||||||
) -> JsonResult {
|
) -> 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
|
// Ratelimit the login
|
||||||
crate::ratelimit::check_limit_login(&ip.ip)?;
|
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
|
// Get the user via the client_id
|
||||||
let client_id = data.client_id.as_ref().unwrap();
|
let client_id = data.client_id.as_ref().unwrap();
|
||||||
let client_user_uuid = match client_id.strip_prefix("user.") {
|
let client_user_uuid = match client_id.strip_prefix("user.") {
|
||||||
@@ -352,6 +373,7 @@ async fn _api_key_login(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Common
|
// Common
|
||||||
|
let scope_vec = vec!["api".into()];
|
||||||
let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, conn).await;
|
let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, conn).await;
|
||||||
let (access_token, expires_in) = device.refresh_tokens(&user, orgs, scope_vec);
|
let (access_token, expires_in) = device.refresh_tokens(&user, orgs, scope_vec);
|
||||||
device.save(conn).await?;
|
device.save(conn).await?;
|
||||||
@@ -360,7 +382,7 @@ async fn _api_key_login(
|
|||||||
|
|
||||||
// Note: No refresh_token is returned. The CLI just repeats the
|
// Note: No refresh_token is returned. The CLI just repeats the
|
||||||
// client_credentials login flow when the existing token expires.
|
// client_credentials login flow when the existing token expires.
|
||||||
let mut result = json!({
|
let result = json!({
|
||||||
"access_token": access_token,
|
"access_token": access_token,
|
||||||
"expires_in": expires_in,
|
"expires_in": expires_in,
|
||||||
"token_type": "Bearer",
|
"token_type": "Bearer",
|
||||||
@@ -369,19 +391,44 @@ async fn _api_key_login(
|
|||||||
|
|
||||||
"Kdf": user.client_kdf_type,
|
"Kdf": user.client_kdf_type,
|
||||||
"KdfIterations": user.client_kdf_iter,
|
"KdfIterations": user.client_kdf_iter,
|
||||||
|
"KdfMemory": user.client_kdf_memory,
|
||||||
|
"KdfParallelism": user.client_kdf_parallelism,
|
||||||
"ResetMasterPassword": false, // TODO: Same as above
|
"ResetMasterPassword": false, // TODO: Same as above
|
||||||
"scope": scope,
|
"scope": "api",
|
||||||
"unofficialServer": true,
|
"unofficialServer": true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if user.client_kdf_type == UserKdfType::Argon2id as i32 {
|
Ok(Json(result))
|
||||||
result["KdfMemory"] =
|
}
|
||||||
Value::Number(user.client_kdf_memory.expect("Argon2 memory parameter is required.").into());
|
|
||||||
result["KdfParallelism"] =
|
async fn _organization_api_key_login(data: ConnectData, conn: &mut DbConn, ip: &ClientIp) -> JsonResult {
|
||||||
Value::Number(user.client_kdf_parallelism.expect("Argon2 parallelism parameter is required.").into());
|
// 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))
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Json(result))
|
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
|
/// Retrieves an existing device or creates a new device from ConnectData and the User
|
||||||
@@ -623,6 +670,8 @@ struct ConnectData {
|
|||||||
#[field(name = uncased("two_factor_remember"))]
|
#[field(name = uncased("two_factor_remember"))]
|
||||||
#[field(name = uncased("twofactorremember"))]
|
#[field(name = uncased("twofactorremember"))]
|
||||||
two_factor_remember: Option<i32>,
|
two_factor_remember: Option<i32>,
|
||||||
|
#[field(name = uncased("authrequest"))]
|
||||||
|
auth_request: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _check_is_some<T>(value: &Option<T>, msg: &str) -> EmptyResult {
|
fn _check_is_some<T>(value: &Option<T>, msg: &str) -> EmptyResult {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ pub mod core;
|
|||||||
mod icons;
|
mod icons;
|
||||||
mod identity;
|
mod identity;
|
||||||
mod notifications;
|
mod notifications;
|
||||||
|
mod push;
|
||||||
mod web;
|
mod web;
|
||||||
|
|
||||||
use rocket::serde::json::Json;
|
use rocket::serde::json::Json;
|
||||||
@@ -12,6 +13,7 @@ pub use crate::api::{
|
|||||||
admin::catchers as admin_catchers,
|
admin::catchers as admin_catchers,
|
||||||
admin::routes as admin_routes,
|
admin::routes as admin_routes,
|
||||||
core::catchers as core_catchers,
|
core::catchers as core_catchers,
|
||||||
|
core::purge_auth_requests,
|
||||||
core::purge_sends,
|
core::purge_sends,
|
||||||
core::purge_trashed_ciphers,
|
core::purge_trashed_ciphers,
|
||||||
core::routes as core_routes,
|
core::routes as core_routes,
|
||||||
@@ -21,7 +23,11 @@ pub use crate::api::{
|
|||||||
icons::routes as icons_routes,
|
icons::routes as icons_routes,
|
||||||
identity::routes as identity_routes,
|
identity::routes as identity_routes,
|
||||||
notifications::routes as notifications_routes,
|
notifications::routes as notifications_routes,
|
||||||
notifications::{start_notification_server, Notify, UpdateType},
|
notifications::{start_notification_server, AnonymousNotify, Notify, UpdateType, WS_ANONYMOUS_SUBSCRIPTIONS},
|
||||||
|
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::catchers as web_catchers,
|
||||||
web::routes as web_routes,
|
web::routes as web_routes,
|
||||||
web::static_files,
|
web::static_files,
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
use std::{
|
use std::{
|
||||||
net::SocketAddr,
|
net::{IpAddr, SocketAddr},
|
||||||
sync::{
|
sync::Arc,
|
||||||
atomic::{AtomicBool, Ordering},
|
|
||||||
Arc,
|
|
||||||
},
|
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
use chrono::NaiveDateTime;
|
use chrono::{NaiveDateTime, Utc};
|
||||||
use futures::{SinkExt, StreamExt};
|
|
||||||
use rmpv::Value;
|
use rmpv::Value;
|
||||||
use rocket::Route;
|
use rocket::{
|
||||||
|
futures::{SinkExt, StreamExt},
|
||||||
|
Route,
|
||||||
|
};
|
||||||
use tokio::{
|
use tokio::{
|
||||||
net::{TcpListener, TcpStream},
|
net::{TcpListener, TcpStream},
|
||||||
sync::mpsc::Sender,
|
sync::mpsc::Sender,
|
||||||
@@ -21,34 +20,236 @@ use tokio_tungstenite::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api::EmptyResult,
|
auth::{ClientIp, WsAccessTokenHeader},
|
||||||
db::models::{Cipher, Folder, Send, User},
|
db::{
|
||||||
|
models::{Cipher, Folder, Send as DbSend, User},
|
||||||
|
DbConn,
|
||||||
|
},
|
||||||
Error, CONFIG,
|
Error, CONFIG,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
|
static WS_USERS: Lazy<Arc<WebSocketUsers>> = Lazy::new(|| {
|
||||||
|
Arc::new(WebSocketUsers {
|
||||||
|
map: Arc::new(dashmap::DashMap::new()),
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
pub static WS_ANONYMOUS_SUBSCRIPTIONS: Lazy<Arc<AnonymousWebSocketSubscriptions>> = Lazy::new(|| {
|
||||||
|
Arc::new(AnonymousWebSocketSubscriptions {
|
||||||
|
map: Arc::new(dashmap::DashMap::new()),
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
push::push_auth_request, push::push_auth_response, push_cipher_update, push_folder_update, push_logout,
|
||||||
|
push_send_update, push_user_update,
|
||||||
|
};
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
routes![websockets_err]
|
routes![websockets_hub, anonymous_websockets_hub]
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/hub")]
|
#[derive(FromForm, Debug)]
|
||||||
fn websockets_err() -> EmptyResult {
|
struct WsAccessToken {
|
||||||
static SHOW_WEBSOCKETS_MSG: AtomicBool = AtomicBool::new(true);
|
access_token: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
if CONFIG.websocket_enabled()
|
struct WSEntryMapGuard {
|
||||||
&& SHOW_WEBSOCKETS_MSG.compare_exchange(true, false, Ordering::Relaxed, Ordering::Relaxed).is_ok()
|
users: Arc<WebSocketUsers>,
|
||||||
{
|
user_uuid: String,
|
||||||
err!(
|
entry_uuid: uuid::Uuid,
|
||||||
"
|
addr: IpAddr,
|
||||||
###########################################################
|
}
|
||||||
'/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.
|
impl WSEntryMapGuard {
|
||||||
###########################################################################################\n"
|
fn new(users: Arc<WebSocketUsers>, user_uuid: String, entry_uuid: uuid::Uuid, addr: IpAddr) -> Self {
|
||||||
)
|
Self {
|
||||||
} else {
|
users,
|
||||||
Err(Error::empty())
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WSAnonymousEntryMapGuard {
|
||||||
|
subscriptions: Arc<AnonymousWebSocketSubscriptions>,
|
||||||
|
token: String,
|
||||||
|
addr: IpAddr,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WSAnonymousEntryMapGuard {
|
||||||
|
fn new(subscriptions: Arc<AnonymousWebSocketSubscriptions>, token: String, addr: IpAddr) -> Self {
|
||||||
|
Self {
|
||||||
|
subscriptions,
|
||||||
|
token,
|
||||||
|
addr,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for WSAnonymousEntryMapGuard {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
info!("Closing WS connection from {}", self.addr);
|
||||||
|
self.subscriptions.map.remove(&self.token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/hub?<data..>")]
|
||||||
|
fn websockets_hub<'r>(
|
||||||
|
ws: rocket_ws::WebSocket,
|
||||||
|
data: WsAccessToken,
|
||||||
|
ip: ClientIp,
|
||||||
|
header_token: WsAccessTokenHeader,
|
||||||
|
) -> Result<rocket_ws::Stream!['r], Error> {
|
||||||
|
let addr = ip.ip;
|
||||||
|
info!("Accepting Rocket WS connection from {addr}");
|
||||||
|
|
||||||
|
let token = if let Some(token) = data.access_token {
|
||||||
|
token
|
||||||
|
} else if let Some(token) = header_token.access_token {
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/anonymous-hub?<token..>")]
|
||||||
|
fn anonymous_websockets_hub<'r>(
|
||||||
|
ws: rocket_ws::WebSocket,
|
||||||
|
token: String,
|
||||||
|
ip: ClientIp,
|
||||||
|
) -> Result<rocket_ws::Stream!['r], Error> {
|
||||||
|
let addr = ip.ip;
|
||||||
|
info!("Accepting Anonymous Rocket WS connection from {addr}");
|
||||||
|
|
||||||
|
let (mut rx, guard) = {
|
||||||
|
let subscriptions = Arc::clone(&WS_ANONYMOUS_SUBSCRIPTIONS);
|
||||||
|
|
||||||
|
// Add a channel to send messages to this client to the map
|
||||||
|
let (tx, rx) = tokio::sync::mpsc::channel::<Message>(100);
|
||||||
|
subscriptions.map.insert(token.clone(), tx);
|
||||||
|
|
||||||
|
// Once the guard goes out of scope, the connection will have been closed and the entry will be deleted from the map
|
||||||
|
(rx, WSAnonymousEntryMapGuard::new(subscriptions, token, 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
|
// Websockets server
|
||||||
//
|
//
|
||||||
@@ -127,8 +328,8 @@ impl WebSocketUsers {
|
|||||||
async fn send_update(&self, user_uuid: &str, data: &[u8]) {
|
async fn send_update(&self, user_uuid: &str, data: &[u8]) {
|
||||||
if let Some(user) = self.map.get(user_uuid).map(|v| v.clone()) {
|
if let Some(user) = self.map.get(user_uuid).map(|v| v.clone()) {
|
||||||
for (_, sender) in user.iter() {
|
for (_, sender) in user.iter() {
|
||||||
if sender.send(Message::binary(data)).await.is_err() {
|
if let Err(e) = sender.send(Message::binary(data)).await {
|
||||||
// TODO: Delete from map here too?
|
error!("Error sending WS update {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -143,19 +344,33 @@ impl WebSocketUsers {
|
|||||||
);
|
);
|
||||||
|
|
||||||
self.send_update(&user.uuid, &data).await;
|
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>) {
|
pub async fn send_logout(&self, user: &User, acting_device_uuid: Option<String>) {
|
||||||
let data = create_update(
|
let data = create_update(
|
||||||
vec![("UserId".into(), user.uuid.clone().into()), ("Date".into(), serialize_date(user.updated_at))],
|
vec![("UserId".into(), user.uuid.clone().into()), ("Date".into(), serialize_date(user.updated_at))],
|
||||||
UpdateType::LogOut,
|
UpdateType::LogOut,
|
||||||
acting_device_uuid,
|
acting_device_uuid.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
self.send_update(&user.uuid, &data).await;
|
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(
|
let data = create_update(
|
||||||
vec![
|
vec![
|
||||||
("Id".into(), folder.uuid.clone().into()),
|
("Id".into(), folder.uuid.clone().into()),
|
||||||
@@ -167,6 +382,10 @@ impl WebSocketUsers {
|
|||||||
);
|
);
|
||||||
|
|
||||||
self.send_update(&folder.user_uuid, &data).await;
|
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(
|
pub async fn send_cipher_update(
|
||||||
@@ -175,17 +394,29 @@ impl WebSocketUsers {
|
|||||||
cipher: &Cipher,
|
cipher: &Cipher,
|
||||||
user_uuids: &[String],
|
user_uuids: &[String],
|
||||||
acting_device_uuid: &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());
|
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(
|
let data = create_update(
|
||||||
vec![
|
vec![
|
||||||
("Id".into(), cipher.uuid.clone().into()),
|
("Id".into(), cipher.uuid.clone().into()),
|
||||||
("UserId".into(), user_uuid),
|
("UserId".into(), user_uuid),
|
||||||
("OrganizationId".into(), org_uuid),
|
("OrganizationId".into(), org_uuid),
|
||||||
("CollectionIds".into(), Value::Nil),
|
("CollectionIds".into(), collection_uuids),
|
||||||
("RevisionDate".into(), serialize_date(cipher.updated_at)),
|
("RevisionDate".into(), revision_date),
|
||||||
],
|
],
|
||||||
ut,
|
ut,
|
||||||
Some(acting_device_uuid.into()),
|
Some(acting_device_uuid.into()),
|
||||||
@@ -194,9 +425,20 @@ impl WebSocketUsers {
|
|||||||
for uuid in user_uuids {
|
for uuid in user_uuids {
|
||||||
self.send_update(uuid, &data).await;
|
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 user_uuid = convert_option(send.user_uuid.clone());
|
||||||
|
|
||||||
let data = create_update(
|
let data = create_update(
|
||||||
@@ -212,6 +454,72 @@ impl WebSocketUsers {
|
|||||||
for uuid in user_uuids {
|
for uuid in user_uuids {
|
||||||
self.send_update(uuid, &data).await;
|
self.send_update(uuid, &data).await;
|
||||||
}
|
}
|
||||||
|
if CONFIG.push_enabled() && user_uuids.len() == 1 {
|
||||||
|
push_send_update(ut, send, acting_device_uuid, conn).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn send_auth_request(
|
||||||
|
&self,
|
||||||
|
user_uuid: &String,
|
||||||
|
auth_request_uuid: &String,
|
||||||
|
acting_device_uuid: &String,
|
||||||
|
conn: &mut DbConn,
|
||||||
|
) {
|
||||||
|
let data = create_update(
|
||||||
|
vec![("Id".into(), auth_request_uuid.clone().into()), ("UserId".into(), user_uuid.clone().into())],
|
||||||
|
UpdateType::AuthRequest,
|
||||||
|
Some(acting_device_uuid.to_string()),
|
||||||
|
);
|
||||||
|
self.send_update(user_uuid, &data).await;
|
||||||
|
|
||||||
|
if CONFIG.push_enabled() {
|
||||||
|
push_auth_request(user_uuid.to_string(), auth_request_uuid.to_string(), conn).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn send_auth_response(
|
||||||
|
&self,
|
||||||
|
user_uuid: &String,
|
||||||
|
auth_response_uuid: &str,
|
||||||
|
approving_device_uuid: String,
|
||||||
|
conn: &mut DbConn,
|
||||||
|
) {
|
||||||
|
let data = create_update(
|
||||||
|
vec![("Id".into(), auth_response_uuid.to_owned().into()), ("UserId".into(), user_uuid.clone().into())],
|
||||||
|
UpdateType::AuthRequestResponse,
|
||||||
|
approving_device_uuid.clone().into(),
|
||||||
|
);
|
||||||
|
self.send_update(auth_response_uuid, &data).await;
|
||||||
|
|
||||||
|
if CONFIG.push_enabled() {
|
||||||
|
push_auth_response(user_uuid.to_string(), auth_response_uuid.to_string(), approving_device_uuid, conn)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AnonymousWebSocketSubscriptions {
|
||||||
|
map: Arc<dashmap::DashMap<String, Sender<Message>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AnonymousWebSocketSubscriptions {
|
||||||
|
async fn send_update(&self, token: &str, data: &[u8]) {
|
||||||
|
if let Some(sender) = self.map.get(token).map(|v| v.clone()) {
|
||||||
|
if let Err(e) = sender.send(Message::binary(data)).await {
|
||||||
|
error!("Error sending WS update {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn send_auth_response(&self, user_uuid: &String, auth_response_uuid: &str) {
|
||||||
|
let data = create_anonymous_update(
|
||||||
|
vec![("Id".into(), auth_response_uuid.to_owned().into()), ("UserId".into(), user_uuid.clone().into())],
|
||||||
|
UpdateType::AuthRequestResponse,
|
||||||
|
user_uuid.to_string(),
|
||||||
|
);
|
||||||
|
self.send_update(auth_response_uuid, &data).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,12 +556,30 @@ fn create_update(payload: Vec<(Value, Value)>, ut: UpdateType, acting_device_uui
|
|||||||
serialize(value)
|
serialize(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create_anonymous_update(payload: Vec<(Value, Value)>, ut: UpdateType, user_id: String) -> Vec<u8> {
|
||||||
|
use rmpv::Value as V;
|
||||||
|
|
||||||
|
let value = V::Array(vec![
|
||||||
|
1.into(),
|
||||||
|
V::Map(vec![]),
|
||||||
|
V::Nil,
|
||||||
|
"AuthRequestResponseRecieved".into(),
|
||||||
|
V::Array(vec![V::Map(vec![
|
||||||
|
("Type".into(), (ut as i32).into()),
|
||||||
|
("Payload".into(), payload.into()),
|
||||||
|
("UserId".into(), user_id.into()),
|
||||||
|
])]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
serialize(value)
|
||||||
|
}
|
||||||
|
|
||||||
fn create_ping() -> Vec<u8> {
|
fn create_ping() -> Vec<u8> {
|
||||||
serialize(Value::Array(vec![6.into()]))
|
serialize(Value::Array(vec![6.into()]))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
#[derive(Eq, PartialEq)]
|
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||||
pub enum UpdateType {
|
pub enum UpdateType {
|
||||||
SyncCipherUpdate = 0,
|
SyncCipherUpdate = 0,
|
||||||
SyncCipherCreate = 1,
|
SyncCipherCreate = 1,
|
||||||
@@ -280,15 +606,13 @@ pub enum UpdateType {
|
|||||||
None = 100,
|
None = 100,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Notify<'a> = &'a rocket::State<WebSocketUsers>;
|
pub type Notify<'a> = &'a rocket::State<Arc<WebSocketUsers>>;
|
||||||
|
pub type AnonymousNotify<'a> = &'a rocket::State<Arc<AnonymousWebSocketSubscriptions>>;
|
||||||
pub fn start_notification_server() -> WebSocketUsers {
|
|
||||||
let users = WebSocketUsers {
|
|
||||||
map: Arc::new(dashmap::DashMap::new()),
|
|
||||||
};
|
|
||||||
|
|
||||||
|
pub fn start_notification_server() -> Arc<WebSocketUsers> {
|
||||||
|
let users = Arc::clone(&WS_USERS);
|
||||||
if CONFIG.websocket_enabled() {
|
if CONFIG.websocket_enabled() {
|
||||||
let users2 = users.clone();
|
let users2 = Arc::<WebSocketUsers>::clone(&users);
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let addr = (CONFIG.websocket_address(), CONFIG.websocket_port());
|
let addr = (CONFIG.websocket_address(), CONFIG.websocket_port());
|
||||||
info!("Starting WebSockets server on {}:{}", addr.0, addr.1);
|
info!("Starting WebSockets server on {}:{}", addr.0, addr.1);
|
||||||
@@ -300,7 +624,7 @@ pub fn start_notification_server() -> WebSocketUsers {
|
|||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
Ok((stream, addr)) = listener.accept() => {
|
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 => {
|
_ = &mut shutdown_rx => {
|
||||||
@@ -316,7 +640,7 @@ pub fn start_notification_server() -> WebSocketUsers {
|
|||||||
users
|
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;
|
let mut user_uuid: Option<String> = None;
|
||||||
|
|
||||||
info!("Accepting WS connection from {addr}");
|
info!("Accepting WS connection from {addr}");
|
||||||
@@ -336,41 +660,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");
|
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 (mut rx, guard) = {
|
||||||
let entry_uuid = uuid::Uuid::new_v4();
|
// Add a channel to send messages to this client to the map
|
||||||
let (tx, mut rx) = tokio::sync::mpsc::channel(100);
|
let entry_uuid = uuid::Uuid::new_v4();
|
||||||
users.map.entry(user_uuid.clone()).or_default().push((entry_uuid, tx));
|
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));
|
let mut interval = tokio::time::interval(Duration::from_secs(15));
|
||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
res = stream.next() => {
|
res = stream.next() => {
|
||||||
match res {
|
match res {
|
||||||
Some(Ok(message)) => {
|
Some(Ok(message)) => {
|
||||||
// Respond to any pings
|
match message {
|
||||||
if let Message::Ping(ping) = message {
|
// Respond to any pings
|
||||||
if stream.send(Message::Pong(ping)).await.is_err() {
|
Message::Ping(ping) => stream.send(Message::Pong(ping)).await?,
|
||||||
break;
|
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;
|
// Just echo anything else the client sends
|
||||||
} else if let Message::Pong(_) = message {
|
_ => stream.send(message).await?,
|
||||||
/* 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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => break,
|
_ => break,
|
||||||
@@ -379,27 +701,15 @@ async fn handle_connection(stream: TcpStream, users: WebSocketUsers, addr: Socke
|
|||||||
|
|
||||||
res = rx.recv() => {
|
res = rx.recv() => {
|
||||||
match res {
|
match res {
|
||||||
Some(res) => {
|
Some(res) => stream.send(res).await?,
|
||||||
if stream.send(res).await.is_err() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None => break,
|
None => break,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_= interval.tick() => {
|
_ = interval.tick() => stream.send(Message::Ping(create_ping())).await?
|
||||||
if stream.send(Message::Ping(create_ping())).await.is_err() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("Closing WS connection from {addr}");
|
|
||||||
|
|
||||||
// Delete from map
|
|
||||||
users.map.entry(user_uuid).or_default().retain(|(uuid, _)| uuid != &entry_uuid);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
294
src/api/push.rs
Normal file
294
src/api/push.rs
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
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);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn push_auth_request(user_uuid: String, auth_request_uuid: String, conn: &mut crate::db::DbConn) {
|
||||||
|
if Device::check_user_has_push_device(user_uuid.as_str(), conn).await {
|
||||||
|
tokio::task::spawn(send_to_push_relay(json!({
|
||||||
|
"userId": user_uuid,
|
||||||
|
"organizationId": (),
|
||||||
|
"deviceId": null,
|
||||||
|
"identifier": null,
|
||||||
|
"type": UpdateType::AuthRequest as i32,
|
||||||
|
"payload": {
|
||||||
|
"id": auth_request_uuid,
|
||||||
|
"userId": user_uuid,
|
||||||
|
}
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn push_auth_response(
|
||||||
|
user_uuid: String,
|
||||||
|
auth_request_uuid: String,
|
||||||
|
approving_device_uuid: String,
|
||||||
|
conn: &mut crate::db::DbConn,
|
||||||
|
) {
|
||||||
|
if Device::check_user_has_push_device(user_uuid.as_str(), conn).await {
|
||||||
|
tokio::task::spawn(send_to_push_relay(json!({
|
||||||
|
"userId": user_uuid,
|
||||||
|
"organizationId": (),
|
||||||
|
"deviceId": approving_device_uuid,
|
||||||
|
"identifier": approving_device_uuid,
|
||||||
|
"type": UpdateType::AuthRequestResponse as i32,
|
||||||
|
"payload": {
|
||||||
|
"id": auth_request_uuid,
|
||||||
|
"userId": user_uuid,
|
||||||
|
}
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ use serde_json::Value;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{core::now, ApiResult, EmptyResult},
|
api::{core::now, ApiResult, EmptyResult},
|
||||||
|
auth::decode_file_download,
|
||||||
error::Error,
|
error::Error,
|
||||||
util::{Cached, SafeString},
|
util::{Cached, SafeString},
|
||||||
CONFIG,
|
CONFIG,
|
||||||
@@ -13,11 +14,17 @@ use crate::{
|
|||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
// If addding more routes here, consider also adding them to
|
// If addding more routes here, consider also adding them to
|
||||||
// crate::utils::LOGGED_ROUTES to make sure they appear in the log
|
// crate::utils::LOGGED_ROUTES to make sure they appear in the log
|
||||||
|
let mut routes = routes![attachments, alive, alive_head, static_files];
|
||||||
if CONFIG.web_vault_enabled() {
|
if CONFIG.web_vault_enabled() {
|
||||||
routes![web_index, web_index_head, app_id, web_files, attachments, alive, alive_head, static_files]
|
routes.append(&mut routes![web_index, web_index_head, app_id, web_files]);
|
||||||
} else {
|
|
||||||
routes![attachments, alive, alive_head, static_files]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
if CONFIG.reload_templates() {
|
||||||
|
routes.append(&mut routes![_static_files_dev]);
|
||||||
|
}
|
||||||
|
|
||||||
|
routes
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn catchers() -> Vec<Catcher> {
|
pub fn catchers() -> Vec<Catcher> {
|
||||||
@@ -91,8 +98,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)
|
Cached::long(NamedFile::open(Path::new(&CONFIG.web_vault_folder()).join(p)).await.ok(), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/attachments/<uuid>/<file_id>")]
|
#[get("/attachments/<uuid>/<file_id>?<token>")]
|
||||||
async fn attachments(uuid: SafeString, file_id: SafeString) -> Option<NamedFile> {
|
async fn attachments(uuid: SafeString, file_id: SafeString, token: String) -> Option<NamedFile> {
|
||||||
|
let Ok(claims) = 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()
|
NamedFile::open(Path::new(&CONFIG.attachments_folder()).join(uuid).join(file_id)).await.ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,9 +122,32 @@ fn alive_head(_conn: DbConn) -> EmptyResult {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/vw_static/<filename>")]
|
// This endpoint/function is used during development and development only.
|
||||||
pub fn static_files(filename: String) -> Result<(ContentType, &'static [u8]), Error> {
|
// It allows to easily develop the admin interface by always loading the files from disk instead from a slice of bytes
|
||||||
match filename.as_ref() {
|
// This will only be active during a debug build and only when `RELOAD_TEMPLATES` is set to `true`
|
||||||
|
// NOTE: Do not forget to add any new files added to the `static_files` function below!
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
#[get("/vw_static/<filename>", rank = 1)]
|
||||||
|
pub async fn _static_files_dev(filename: PathBuf) -> Option<NamedFile> {
|
||||||
|
warn!("LOADING STATIC FILES FROM DISK");
|
||||||
|
let file = filename.to_str().unwrap_or_default();
|
||||||
|
let ext = filename.extension().unwrap_or_default();
|
||||||
|
|
||||||
|
let path = if ext == "png" || ext == "svg" {
|
||||||
|
tokio::fs::canonicalize(Path::new(file!()).parent().unwrap().join("../static/images/").join(file)).await
|
||||||
|
} else {
|
||||||
|
tokio::fs::canonicalize(Path::new(file!()).parent().unwrap().join("../static/scripts/").join(file)).await
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Ok(path) = path {
|
||||||
|
return NamedFile::open(path).await.ok();
|
||||||
|
};
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/vw_static/<filename>", rank = 2)]
|
||||||
|
pub fn static_files(filename: &str) -> Result<(ContentType, &'static [u8]), Error> {
|
||||||
|
match filename {
|
||||||
"404.png" => Ok((ContentType::PNG, include_bytes!("../static/images/404.png"))),
|
"404.png" => Ok((ContentType::PNG, include_bytes!("../static/images/404.png"))),
|
||||||
"mail-github.png" => Ok((ContentType::PNG, include_bytes!("../static/images/mail-github.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"))),
|
"logo-gray.png" => Ok((ContentType::PNG, include_bytes!("../static/images/logo-gray.png"))),
|
||||||
@@ -132,12 +167,12 @@ pub fn static_files(filename: String) -> Result<(ContentType, &'static [u8]), Er
|
|||||||
Ok((ContentType::JavaScript, include_bytes!("../static/scripts/admin_diagnostics.js")))
|
Ok((ContentType::JavaScript, include_bytes!("../static/scripts/admin_diagnostics.js")))
|
||||||
}
|
}
|
||||||
"bootstrap.css" => Ok((ContentType::CSS, include_bytes!("../static/scripts/bootstrap.css"))),
|
"bootstrap.css" => Ok((ContentType::CSS, include_bytes!("../static/scripts/bootstrap.css"))),
|
||||||
"bootstrap-native.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/bootstrap-native.js"))),
|
"bootstrap.bundle.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/bootstrap.bundle.js"))),
|
||||||
"jdenticon.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/jdenticon.js"))),
|
"jdenticon.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/jdenticon.js"))),
|
||||||
"datatables.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/datatables.js"))),
|
"datatables.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/datatables.js"))),
|
||||||
"datatables.css" => Ok((ContentType::CSS, include_bytes!("../static/scripts/datatables.css"))),
|
"datatables.css" => Ok((ContentType::CSS, include_bytes!("../static/scripts/datatables.css"))),
|
||||||
"jquery-3.6.3.slim.js" => {
|
"jquery-3.7.0.slim.js" => {
|
||||||
Ok((ContentType::JavaScript, include_bytes!("../static/scripts/jquery-3.6.3.slim.js")))
|
Ok((ContentType::JavaScript, include_bytes!("../static/scripts/jquery-3.7.0.slim.js")))
|
||||||
}
|
}
|
||||||
_ => err!(format!("Static file not found: {filename}")),
|
_ => err!(format!("Static file not found: {filename}")),
|
||||||
}
|
}
|
||||||
|
|||||||
146
src/auth.rs
146
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_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_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_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(|| {
|
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}"))
|
let key =
|
||||||
});
|
std::fs::read(CONFIG.private_rsa_key()).unwrap_or_else(|e| panic!("Error loading private RSA Key. \n{e}"));
|
||||||
static PUBLIC_RSA_KEY_VEC: Lazy<Vec<u8>> = Lazy::new(|| {
|
EncodingKey::from_rsa_pem(&key).unwrap_or_else(|e| panic!("Error decoding private RSA Key.\n{e}"))
|
||||||
std::fs::read(CONFIG.public_rsa_key()).unwrap_or_else(|e| panic!("Error loading public RSA Key.\n{e}"))
|
|
||||||
});
|
});
|
||||||
static PUBLIC_RSA_KEY: Lazy<DecodingKey> = Lazy::new(|| {
|
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() {
|
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())
|
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)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct LoginJwtClaims {
|
pub struct LoginJwtClaims {
|
||||||
// Not before
|
// 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)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct BasicJwtClaims {
|
pub struct BasicJwtClaims {
|
||||||
// Not before
|
// Not before
|
||||||
@@ -446,32 +507,34 @@ pub struct OrgHeaders {
|
|||||||
pub ip: ClientIp,
|
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]
|
#[rocket::async_trait]
|
||||||
impl<'r> FromRequest<'r> for OrgHeaders {
|
impl<'r> FromRequest<'r> for OrgHeaders {
|
||||||
type Error = &'static str;
|
type Error = &'static str;
|
||||||
|
|
||||||
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||||
let headers = try_outcome!(Headers::from_request(request).await);
|
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) => {
|
Some(org_id) => {
|
||||||
let mut conn = match DbConn::from_request(request).await {
|
let mut conn = match DbConn::from_request(request).await {
|
||||||
Outcome::Success(conn) => conn,
|
Outcome::Success(conn) => conn,
|
||||||
@@ -479,7 +542,7 @@ impl<'r> FromRequest<'r> for OrgHeaders {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let user = headers.user;
|
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) => {
|
Some(user) => {
|
||||||
if user.status == UserOrgStatus::Confirmed as i32 {
|
if user.status == UserOrgStatus::Confirmed as i32 {
|
||||||
user
|
user
|
||||||
@@ -503,7 +566,7 @@ impl<'r> FromRequest<'r> for OrgHeaders {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
org_user,
|
org_user,
|
||||||
org_id,
|
org_id: String::from(org_id),
|
||||||
ip: headers.ip,
|
ip: headers.ip,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -762,3 +825,26 @@ impl<'r> FromRequest<'r> for ClientIp {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct WsAccessTokenHeader {
|
||||||
|
pub access_token: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rocket::async_trait]
|
||||||
|
impl<'r> FromRequest<'r> for WsAccessTokenHeader {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||||
|
let headers = request.headers();
|
||||||
|
|
||||||
|
// Get access_token
|
||||||
|
let access_token = match headers.get_one("Authorization") {
|
||||||
|
Some(a) => a.rsplit("Bearer ").next().map(String::from),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
Outcome::Success(Self {
|
||||||
|
access_token,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -377,6 +377,16 @@ make_config! {
|
|||||||
/// Websocket port
|
/// Websocket port
|
||||||
websocket_port: u16, false, def, 3012;
|
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 {
|
jobs {
|
||||||
/// Job scheduler poll interval |> How often the job scheduler thread checks for jobs to run.
|
/// Job scheduler poll interval |> How often the job scheduler thread checks for jobs to run.
|
||||||
/// Set to 0 to globally disable scheduled jobs.
|
/// Set to 0 to globally disable scheduled jobs.
|
||||||
@@ -399,6 +409,10 @@ make_config! {
|
|||||||
/// Event cleanup schedule |> Cron schedule of the job that cleans old events from the event table.
|
/// Event cleanup schedule |> Cron schedule of the job that cleans old events from the event table.
|
||||||
/// Defaults to daily. Set blank to disable this job.
|
/// Defaults to daily. Set blank to disable this job.
|
||||||
event_cleanup_schedule: String, false, def, "0 10 0 * * *".to_string();
|
event_cleanup_schedule: String, false, def, "0 10 0 * * *".to_string();
|
||||||
|
/// Auth Request cleanup schedule |> Cron schedule of the job that cleans old auth requests from the auth request.
|
||||||
|
/// Defaults to every minute. Set blank to disable this job.
|
||||||
|
auth_request_purge_schedule: String, false, def, "30 * * * * *".to_string();
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/// General settings
|
/// General settings
|
||||||
@@ -476,13 +490,13 @@ make_config! {
|
|||||||
/// provides unauthenticated access to potentially sensitive data.
|
/// provides unauthenticated access to potentially sensitive data.
|
||||||
show_password_hint: bool, true, def, false;
|
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;
|
admin_token: Pass, true, option;
|
||||||
|
|
||||||
/// Invitation organization name |> Name shown in the invitation emails that don't come from a specific organization
|
/// Invitation organization name |> Name shown in the invitation emails that don't come from a specific organization
|
||||||
invitation_org_name: String, true, def, "Vaultwarden".to_string();
|
invitation_org_name: String, true, def, "Vaultwarden".to_string();
|
||||||
|
|
||||||
/// Events days retain |> Number of days to retain events stored in the database. If unset, events are kept indefently.
|
/// Events days retain |> Number of days to retain events stored in the database. If unset, events are kept indefinitely.
|
||||||
events_days_retain: i64, false, option;
|
events_days_retain: i64, false, option;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -510,7 +524,7 @@ make_config! {
|
|||||||
/// has been decided on, consider using permanent redirects for cacheability. The legacy codes
|
/// has been decided on, consider using permanent redirects for cacheability. The legacy codes
|
||||||
/// are currently better supported by the Bitwarden clients.
|
/// are currently better supported by the Bitwarden clients.
|
||||||
icon_redirect_code: u32, true, def, 302;
|
icon_redirect_code: u32, true, def, 302;
|
||||||
/// Positive icon cache expiry |> Number of seconds to consider that an already cached icon is fresh. After this period, the icon will be redownloaded
|
/// Positive icon cache expiry |> Number of seconds to consider that an already cached icon is fresh. After this period, the icon will be refreshed
|
||||||
icon_cache_ttl: u64, true, def, 2_592_000;
|
icon_cache_ttl: u64, true, def, 2_592_000;
|
||||||
/// Negative icon cache expiry |> Number of seconds before trying to download an icon that failed again.
|
/// Negative icon cache expiry |> Number of seconds before trying to download an icon that failed again.
|
||||||
icon_cache_negttl: u64, true, def, 259_200;
|
icon_cache_negttl: u64, true, def, 259_200;
|
||||||
@@ -520,7 +534,7 @@ make_config! {
|
|||||||
/// Useful to hide other servers in the local network. Check the WIKI for more details
|
/// Useful to hide other servers in the local network. Check the WIKI for more details
|
||||||
icon_blacklist_regex: String, true, option;
|
icon_blacklist_regex: String, true, option;
|
||||||
/// Icon blacklist non global IPs |> Any IP which is not defined as a global IP will be blacklisted.
|
/// Icon blacklist non global IPs |> Any IP which is not defined as a global IP will be blacklisted.
|
||||||
/// Usefull to secure your internal environment: See https://en.wikipedia.org/wiki/Reserved_IP_addresses for a list of IPs which it will block
|
/// Useful to secure your internal environment: See https://en.wikipedia.org/wiki/Reserved_IP_addresses for a list of IPs which it will block
|
||||||
icon_blacklist_non_global_ips: bool, true, def, true;
|
icon_blacklist_non_global_ips: bool, true, def, true;
|
||||||
|
|
||||||
/// Disable Two-Factor remember |> Enabling this would force the users to use a second factor to login every time.
|
/// Disable Two-Factor remember |> Enabling this would force the users to use a second factor to login every time.
|
||||||
@@ -556,7 +570,7 @@ make_config! {
|
|||||||
/// Max database connection retries |> Number of times to retry the database connection during startup, with 1 second between each retry, set to 0 to retry indefinitely
|
/// Max database connection retries |> Number of times to retry the database connection during startup, with 1 second between each retry, set to 0 to retry indefinitely
|
||||||
db_connection_retries: u32, false, def, 15;
|
db_connection_retries: u32, false, def, 15;
|
||||||
|
|
||||||
/// Timeout when aquiring database connection
|
/// Timeout when acquiring database connection
|
||||||
database_timeout: u64, false, def, 30;
|
database_timeout: u64, false, def, 30;
|
||||||
|
|
||||||
/// Database connection pool size
|
/// Database connection pool size
|
||||||
@@ -603,7 +617,7 @@ make_config! {
|
|||||||
/// Global Duo settings (Note that users can override them)
|
/// Global Duo settings (Note that users can override them)
|
||||||
duo: _enable_duo {
|
duo: _enable_duo {
|
||||||
/// Enabled
|
/// Enabled
|
||||||
_enable_duo: bool, true, def, false;
|
_enable_duo: bool, true, def, true;
|
||||||
/// Integration Key
|
/// Integration Key
|
||||||
duo_ikey: String, true, option;
|
duo_ikey: String, true, option;
|
||||||
/// Secret Key
|
/// Secret Key
|
||||||
@@ -724,6 +738,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
|
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())
|
||||||
&& !(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())
|
||||||
@@ -872,6 +897,10 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
|
|||||||
err!("`EVENT_CLEANUP_SCHEDULE` is not a valid cron expression")
|
err!("`EVENT_CLEANUP_SCHEDULE` is not a valid cron expression")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !cfg.auth_request_purge_schedule.is_empty() && cfg.auth_request_purge_schedule.parse::<Schedule>().is_err() {
|
||||||
|
err!("`AUTH_REQUEST_PURGE_SCHEDULE` is not a valid cron expression")
|
||||||
|
}
|
||||||
|
|
||||||
if !cfg.disable_admin_token {
|
if !cfg.disable_admin_token {
|
||||||
match cfg.admin_token.as_ref() {
|
match cfg.admin_token.as_ref() {
|
||||||
Some(t) if t.starts_with("$argon2") => {
|
Some(t) if t.starts_with("$argon2") => {
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ impl Attachment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_url(&self, host: &str) -> String {
|
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 {
|
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::db::DbConn;
|
||||||
|
|
||||||
use crate::api::EmptyResult;
|
use crate::api::EmptyResult;
|
||||||
|
|||||||
148
src/db/models/auth_request.rs
Normal file
148
src/db/models/auth_request.rs
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
use crate::crypto::ct_eq;
|
||||||
|
use chrono::{NaiveDateTime, Utc};
|
||||||
|
|
||||||
|
db_object! {
|
||||||
|
#[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset, Deserialize, Serialize)]
|
||||||
|
#[diesel(table_name = auth_requests)]
|
||||||
|
#[diesel(treat_none_as_null = true)]
|
||||||
|
#[diesel(primary_key(uuid))]
|
||||||
|
pub struct AuthRequest {
|
||||||
|
pub uuid: String,
|
||||||
|
pub user_uuid: String,
|
||||||
|
pub organization_uuid: Option<String>,
|
||||||
|
|
||||||
|
pub request_device_identifier: String,
|
||||||
|
pub device_type: i32, // https://github.com/bitwarden/server/blob/master/src/Core/Enums/DeviceType.cs
|
||||||
|
|
||||||
|
pub request_ip: String,
|
||||||
|
pub response_device_id: Option<String>,
|
||||||
|
|
||||||
|
pub access_code: String,
|
||||||
|
pub public_key: String,
|
||||||
|
|
||||||
|
pub enc_key: String,
|
||||||
|
|
||||||
|
pub master_password_hash: String,
|
||||||
|
pub approved: Option<bool>,
|
||||||
|
pub creation_date: NaiveDateTime,
|
||||||
|
pub response_date: Option<NaiveDateTime>,
|
||||||
|
|
||||||
|
pub authentication_date: Option<NaiveDateTime>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AuthRequest {
|
||||||
|
pub fn new(
|
||||||
|
user_uuid: String,
|
||||||
|
request_device_identifier: String,
|
||||||
|
device_type: i32,
|
||||||
|
request_ip: String,
|
||||||
|
access_code: String,
|
||||||
|
public_key: String,
|
||||||
|
) -> Self {
|
||||||
|
let now = Utc::now().naive_utc();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
uuid: crate::util::get_uuid(),
|
||||||
|
user_uuid,
|
||||||
|
organization_uuid: None,
|
||||||
|
|
||||||
|
request_device_identifier,
|
||||||
|
device_type,
|
||||||
|
request_ip,
|
||||||
|
response_device_id: None,
|
||||||
|
access_code,
|
||||||
|
public_key,
|
||||||
|
enc_key: String::new(),
|
||||||
|
master_password_hash: String::new(),
|
||||||
|
approved: None,
|
||||||
|
creation_date: now,
|
||||||
|
response_date: None,
|
||||||
|
authentication_date: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
use crate::db::DbConn;
|
||||||
|
|
||||||
|
use crate::api::EmptyResult;
|
||||||
|
use crate::error::MapResult;
|
||||||
|
|
||||||
|
impl AuthRequest {
|
||||||
|
pub async fn save(&mut self, conn: &mut DbConn) -> EmptyResult {
|
||||||
|
db_run! { conn:
|
||||||
|
sqlite, mysql {
|
||||||
|
match diesel::replace_into(auth_requests::table)
|
||||||
|
.values(AuthRequestDb::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(auth_requests::table)
|
||||||
|
.filter(auth_requests::uuid.eq(&self.uuid))
|
||||||
|
.set(AuthRequestDb::to_db(self))
|
||||||
|
.execute(conn)
|
||||||
|
.map_res("Error auth_request")
|
||||||
|
}
|
||||||
|
Err(e) => Err(e.into()),
|
||||||
|
}.map_res("Error auth_request")
|
||||||
|
}
|
||||||
|
postgresql {
|
||||||
|
let value = AuthRequestDb::to_db(self);
|
||||||
|
diesel::insert_into(auth_requests::table)
|
||||||
|
.values(&value)
|
||||||
|
.on_conflict(auth_requests::uuid)
|
||||||
|
.do_update()
|
||||||
|
.set(&value)
|
||||||
|
.execute(conn)
|
||||||
|
.map_res("Error saving auth_request")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn find_by_uuid(uuid: &str, conn: &mut DbConn) -> Option<Self> {
|
||||||
|
db_run! {conn: {
|
||||||
|
auth_requests::table
|
||||||
|
.filter(auth_requests::uuid.eq(uuid))
|
||||||
|
.first::<AuthRequestDb>(conn)
|
||||||
|
.ok()
|
||||||
|
.from_db()
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn find_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
|
||||||
|
db_run! {conn: {
|
||||||
|
auth_requests::table
|
||||||
|
.filter(auth_requests::user_uuid.eq(user_uuid))
|
||||||
|
.load::<AuthRequestDb>(conn).expect("Error loading auth_requests").from_db()
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn find_created_before(dt: &NaiveDateTime, conn: &mut DbConn) -> Vec<Self> {
|
||||||
|
db_run! {conn: {
|
||||||
|
auth_requests::table
|
||||||
|
.filter(auth_requests::creation_date.lt(dt))
|
||||||
|
.load::<AuthRequestDb>(conn).expect("Error loading auth_requests").from_db()
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete(&self, conn: &mut DbConn) -> EmptyResult {
|
||||||
|
db_run! { conn: {
|
||||||
|
diesel::delete(auth_requests::table.filter(auth_requests::uuid.eq(&self.uuid)))
|
||||||
|
.execute(conn)
|
||||||
|
.map_res("Error deleting auth request")
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check_access_code(&self, access_code: &str) -> bool {
|
||||||
|
ct_eq(&self.access_code, access_code)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn purge_expired_auth_requests(conn: &mut DbConn) {
|
||||||
|
let expiry_time = Utc::now().naive_utc() - chrono::Duration::minutes(5); //after 5 minutes, clients reject the request
|
||||||
|
for auth_request in Self::find_created_before(&expiry_time, conn).await {
|
||||||
|
auth_request.delete(conn).await.ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,7 +27,8 @@ db_object! {
|
|||||||
Login = 1,
|
Login = 1,
|
||||||
SecureNote = 2,
|
SecureNote = 2,
|
||||||
Card = 3,
|
Card = 3,
|
||||||
Identity = 4
|
Identity = 4,
|
||||||
|
Fido2key = 5
|
||||||
*/
|
*/
|
||||||
pub atype: i32,
|
pub atype: i32,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@@ -223,6 +224,7 @@ impl Cipher {
|
|||||||
"SecureNote": null,
|
"SecureNote": null,
|
||||||
"Card": null,
|
"Card": null,
|
||||||
"Identity": null,
|
"Identity": null,
|
||||||
|
"Fido2Key": null,
|
||||||
});
|
});
|
||||||
|
|
||||||
// These values are only needed for user/default syncs
|
// These values are only needed for user/default syncs
|
||||||
@@ -251,6 +253,7 @@ impl Cipher {
|
|||||||
2 => "SecureNote",
|
2 => "SecureNote",
|
||||||
3 => "Card",
|
3 => "Card",
|
||||||
4 => "Identity",
|
4 => "Identity",
|
||||||
|
5 => "Fido2Key",
|
||||||
_ => panic!("Wrong type"),
|
_ => panic!("Wrong type"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ db_object! {
|
|||||||
pub uuid: String,
|
pub uuid: String,
|
||||||
pub org_uuid: String,
|
pub org_uuid: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
pub external_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Identifiable, Queryable, Insertable)]
|
#[derive(Identifiable, Queryable, Insertable)]
|
||||||
@@ -33,18 +34,21 @@ db_object! {
|
|||||||
|
|
||||||
/// Local methods
|
/// Local methods
|
||||||
impl Collection {
|
impl Collection {
|
||||||
pub fn new(org_uuid: String, name: String) -> Self {
|
pub fn new(org_uuid: String, name: String, external_id: Option<String>) -> Self {
|
||||||
Self {
|
let mut new_model = Self {
|
||||||
uuid: crate::util::get_uuid(),
|
uuid: crate::util::get_uuid(),
|
||||||
|
|
||||||
org_uuid,
|
org_uuid,
|
||||||
name,
|
name,
|
||||||
}
|
external_id: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
new_model.set_external_id(external_id);
|
||||||
|
new_model
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_json(&self) -> Value {
|
pub fn to_json(&self) -> Value {
|
||||||
json!({
|
json!({
|
||||||
"ExternalId": null, // Not support by us
|
"ExternalId": self.external_id,
|
||||||
"Id": self.uuid,
|
"Id": self.uuid,
|
||||||
"OrganizationId": self.org_uuid,
|
"OrganizationId": self.org_uuid,
|
||||||
"Name": self.name,
|
"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(
|
pub async fn to_json_details(
|
||||||
&self,
|
&self,
|
||||||
user_uuid: &str,
|
user_uuid: &str,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use chrono::{NaiveDateTime, Utc};
|
use chrono::{NaiveDateTime, Utc};
|
||||||
|
|
||||||
use crate::CONFIG;
|
use crate::{crypto, CONFIG};
|
||||||
|
use core::fmt;
|
||||||
|
|
||||||
db_object! {
|
db_object! {
|
||||||
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
|
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
|
||||||
@@ -15,7 +16,8 @@ db_object! {
|
|||||||
pub user_uuid: String,
|
pub user_uuid: String,
|
||||||
|
|
||||||
pub name: 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 push_token: Option<String>,
|
||||||
|
|
||||||
pub refresh_token: String,
|
pub refresh_token: String,
|
||||||
@@ -38,6 +40,7 @@ impl Device {
|
|||||||
name,
|
name,
|
||||||
atype,
|
atype,
|
||||||
|
|
||||||
|
push_uuid: None,
|
||||||
push_token: None,
|
push_token: None,
|
||||||
refresh_token: String::new(),
|
refresh_token: String::new(),
|
||||||
twofactor_remember: None,
|
twofactor_remember: None,
|
||||||
@@ -45,9 +48,7 @@ impl Device {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn refresh_twofactor_remember(&mut self) -> String {
|
pub fn refresh_twofactor_remember(&mut self) -> String {
|
||||||
use crate::crypto;
|
|
||||||
use data_encoding::BASE64;
|
use data_encoding::BASE64;
|
||||||
|
|
||||||
let twofactor_remember = crypto::encode_random_bytes::<180>(BASE64);
|
let twofactor_remember = crypto::encode_random_bytes::<180>(BASE64);
|
||||||
self.twofactor_remember = Some(twofactor_remember.clone());
|
self.twofactor_remember = Some(twofactor_remember.clone());
|
||||||
|
|
||||||
@@ -66,9 +67,7 @@ impl Device {
|
|||||||
) -> (String, i64) {
|
) -> (String, i64) {
|
||||||
// If there is no refresh token, we create one
|
// If there is no refresh token, we create one
|
||||||
if self.refresh_token.is_empty() {
|
if self.refresh_token.is_empty() {
|
||||||
use crate::crypto;
|
|
||||||
use data_encoding::BASE64URL;
|
use data_encoding::BASE64URL;
|
||||||
|
|
||||||
self.refresh_token = crypto::encode_random_bytes::<64>(BASE64URL);
|
self.refresh_token = crypto::encode_random_bytes::<64>(BASE64URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,6 +154,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> {
|
pub async fn find_by_refresh_token(refresh_token: &str, conn: &mut DbConn) -> Option<Self> {
|
||||||
db_run! { conn: {
|
db_run! { conn: {
|
||||||
devices::table
|
devices::table
|
||||||
@@ -175,4 +203,113 @@ impl Device {
|
|||||||
.from_db()
|
.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
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum DeviceType {
|
||||||
|
Android = 0,
|
||||||
|
Ios = 1,
|
||||||
|
ChromeExtension = 2,
|
||||||
|
FirefoxExtension = 3,
|
||||||
|
OperaExtension = 4,
|
||||||
|
EdgeExtension = 5,
|
||||||
|
WindowsDesktop = 6,
|
||||||
|
MacOsDesktop = 7,
|
||||||
|
LinuxDesktop = 8,
|
||||||
|
ChromeBrowser = 9,
|
||||||
|
FirefoxBrowser = 10,
|
||||||
|
OperaBrowser = 11,
|
||||||
|
EdgeBrowser = 12,
|
||||||
|
IEBrowser = 13,
|
||||||
|
UnknownBrowser = 14,
|
||||||
|
AndroidAmazon = 15,
|
||||||
|
Uwp = 16,
|
||||||
|
SafariBrowser = 17,
|
||||||
|
VivaldiBrowser = 18,
|
||||||
|
VivaldiExtension = 19,
|
||||||
|
SafariExtension = 20,
|
||||||
|
Sdk = 21,
|
||||||
|
Server = 22,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for DeviceType {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
DeviceType::Android => write!(f, "Android"),
|
||||||
|
DeviceType::Ios => write!(f, "iOS"),
|
||||||
|
DeviceType::ChromeExtension => write!(f, "Chrome Extension"),
|
||||||
|
DeviceType::FirefoxExtension => write!(f, "Firefox Extension"),
|
||||||
|
DeviceType::OperaExtension => write!(f, "Opera Extension"),
|
||||||
|
DeviceType::EdgeExtension => write!(f, "Edge Extension"),
|
||||||
|
DeviceType::WindowsDesktop => write!(f, "Windows Desktop"),
|
||||||
|
DeviceType::MacOsDesktop => write!(f, "MacOS Desktop"),
|
||||||
|
DeviceType::LinuxDesktop => write!(f, "Linux Desktop"),
|
||||||
|
DeviceType::ChromeBrowser => write!(f, "Chrome Browser"),
|
||||||
|
DeviceType::FirefoxBrowser => write!(f, "Firefox Browser"),
|
||||||
|
DeviceType::OperaBrowser => write!(f, "Opera Browser"),
|
||||||
|
DeviceType::EdgeBrowser => write!(f, "Edge Browser"),
|
||||||
|
DeviceType::IEBrowser => write!(f, "Internet Explorer"),
|
||||||
|
DeviceType::UnknownBrowser => write!(f, "Unknown Browser"),
|
||||||
|
DeviceType::AndroidAmazon => write!(f, "Android Amazon"),
|
||||||
|
DeviceType::Uwp => write!(f, "UWP"),
|
||||||
|
DeviceType::SafariBrowser => write!(f, "Safari Browser"),
|
||||||
|
DeviceType::VivaldiBrowser => write!(f, "Vivaldi Browser"),
|
||||||
|
DeviceType::VivaldiExtension => write!(f, "Vivaldi Extension"),
|
||||||
|
DeviceType::SafariExtension => write!(f, "Safari Extension"),
|
||||||
|
DeviceType::Sdk => write!(f, "SDK"),
|
||||||
|
DeviceType::Server => write!(f, "Server"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DeviceType {
|
||||||
|
pub fn from_i32(value: i32) -> DeviceType {
|
||||||
|
match value {
|
||||||
|
0 => DeviceType::Android,
|
||||||
|
1 => DeviceType::Ios,
|
||||||
|
2 => DeviceType::ChromeExtension,
|
||||||
|
3 => DeviceType::FirefoxExtension,
|
||||||
|
4 => DeviceType::OperaExtension,
|
||||||
|
5 => DeviceType::EdgeExtension,
|
||||||
|
6 => DeviceType::WindowsDesktop,
|
||||||
|
7 => DeviceType::MacOsDesktop,
|
||||||
|
8 => DeviceType::LinuxDesktop,
|
||||||
|
9 => DeviceType::ChromeBrowser,
|
||||||
|
10 => DeviceType::FirefoxBrowser,
|
||||||
|
11 => DeviceType::OperaBrowser,
|
||||||
|
12 => DeviceType::EdgeBrowser,
|
||||||
|
13 => DeviceType::IEBrowser,
|
||||||
|
14 => DeviceType::UnknownBrowser,
|
||||||
|
15 => DeviceType::AndroidAmazon,
|
||||||
|
16 => DeviceType::Uwp,
|
||||||
|
17 => DeviceType::SafariBrowser,
|
||||||
|
18 => DeviceType::VivaldiBrowser,
|
||||||
|
19 => DeviceType::VivaldiExtension,
|
||||||
|
20 => DeviceType::SafariExtension,
|
||||||
|
21 => DeviceType::Sdk,
|
||||||
|
22 => DeviceType::Server,
|
||||||
|
_ => DeviceType::UnknownBrowser,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ db_object! {
|
|||||||
pub organizations_uuid: String,
|
pub organizations_uuid: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub access_all: bool,
|
pub access_all: bool,
|
||||||
external_id: Option<String>,
|
pub external_id: Option<String>,
|
||||||
pub creation_date: NaiveDateTime,
|
pub creation_date: NaiveDateTime,
|
||||||
pub revision_date: NaiveDateTime,
|
pub revision_date: NaiveDateTime,
|
||||||
}
|
}
|
||||||
@@ -94,22 +94,11 @@ impl Group {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_external_id(&mut self, external_id: Option<String>) {
|
pub fn set_external_id(&mut self, external_id: Option<String>) {
|
||||||
//Check if external id is empty. We don't want to have
|
// Check if external_id is empty. We do not want to have empty strings in the database
|
||||||
//empty strings in the database
|
self.external_id = match external_id {
|
||||||
match external_id {
|
Some(external_id) if !external_id.trim().is_empty() => Some(external_id),
|
||||||
Some(external_id) => {
|
_ => None,
|
||||||
if external_id.is_empty() {
|
};
|
||||||
self.external_id = None;
|
|
||||||
} else {
|
|
||||||
self.external_id = Some(external_id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => self.external_id = None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_external_id(&self) -> Option<String> {
|
|
||||||
self.external_id.clone()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,6 +203,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
|
//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> {
|
pub async fn gather_user_organizations_full_access(user_uuid: &str, conn: &mut DbConn) -> Vec<String> {
|
||||||
db_run! { conn: {
|
db_run! { conn: {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
mod attachment;
|
mod attachment;
|
||||||
|
mod auth_request;
|
||||||
mod cipher;
|
mod cipher;
|
||||||
mod collection;
|
mod collection;
|
||||||
mod device;
|
mod device;
|
||||||
@@ -15,16 +16,17 @@ mod two_factor_incomplete;
|
|||||||
mod user;
|
mod user;
|
||||||
|
|
||||||
pub use self::attachment::Attachment;
|
pub use self::attachment::Attachment;
|
||||||
|
pub use self::auth_request::AuthRequest;
|
||||||
pub use self::cipher::Cipher;
|
pub use self::cipher::Cipher;
|
||||||
pub use self::collection::{Collection, CollectionCipher, CollectionUser};
|
pub use self::collection::{Collection, CollectionCipher, CollectionUser};
|
||||||
pub use self::device::Device;
|
pub use self::device::{Device, DeviceType};
|
||||||
pub use self::emergency_access::{EmergencyAccess, EmergencyAccessStatus, EmergencyAccessType};
|
pub use self::emergency_access::{EmergencyAccess, EmergencyAccessStatus, EmergencyAccessType};
|
||||||
pub use self::event::{Event, EventType};
|
pub use self::event::{Event, EventType};
|
||||||
pub use self::favorite::Favorite;
|
pub use self::favorite::Favorite;
|
||||||
pub use self::folder::{Folder, FolderCipher};
|
pub use self::folder::{Folder, FolderCipher};
|
||||||
pub use self::group::{CollectionGroup, Group, GroupUser};
|
pub use self::group::{CollectionGroup, Group, GroupUser};
|
||||||
pub use self::org_policy::{OrgPolicy, OrgPolicyErr, OrgPolicyType};
|
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::send::{Send, SendType};
|
||||||
pub use self::two_factor::{TwoFactor, TwoFactorType};
|
pub use self::two_factor::{TwoFactor, TwoFactorType};
|
||||||
pub use self::two_factor_incomplete::TwoFactorIncomplete;
|
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 {
|
match OrgPolicy::find_by_org_and_type(org_uuid, OrgPolicyType::ResetPassword, conn).await {
|
||||||
Some(policy) => match serde_json::from_str::<UpCase<ResetPasswordDataModel>>(&policy.data) {
|
Some(policy) => match serde_json::from_str::<UpCase<ResetPasswordDataModel>>(&policy.data) {
|
||||||
Ok(opts) => {
|
Ok(opts) => {
|
||||||
return opts.data.AutoEnrollEnabled;
|
return policy.enabled && opts.data.AutoEnrollEnabled;
|
||||||
}
|
}
|
||||||
_ => error!("Failed to deserialize ResetPasswordDataModel: {}", policy.data),
|
_ => error!("Failed to deserialize ResetPasswordDataModel: {}", policy.data),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use chrono::{NaiveDateTime, Utc};
|
||||||
use num_traits::FromPrimitive;
|
use num_traits::FromPrimitive;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
@@ -31,6 +32,17 @@ db_object! {
|
|||||||
pub atype: i32,
|
pub atype: i32,
|
||||||
pub reset_password_key: Option<String>,
|
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
|
// https://github.com/bitwarden/server/blob/b86a04cef9f1e1b82cf18e49fc94e017c641130c/src/Core/Enums/OrganizationUserStatusType.cs
|
||||||
@@ -157,7 +169,7 @@ impl Organization {
|
|||||||
"UseSso": false, // Not supported
|
"UseSso": false, // Not supported
|
||||||
// "UseKeyConnector": false, // Not supported
|
// "UseKeyConnector": false, // Not supported
|
||||||
"SelfHost": true,
|
"SelfHost": true,
|
||||||
"UseApi": false, // Not supported
|
"UseApi": true,
|
||||||
"HasPublicAndPrivateKeys": self.private_key.is_some() && self.public_key.is_some(),
|
"HasPublicAndPrivateKeys": self.private_key.is_some() && self.public_key.is_some(),
|
||||||
"UseResetPassword": CONFIG.mail_enabled(),
|
"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::db::DbConn;
|
||||||
|
|
||||||
use crate::api::EmptyResult;
|
use crate::api::EmptyResult;
|
||||||
@@ -311,7 +340,7 @@ impl UserOrganization {
|
|||||||
"UseTotp": true,
|
"UseTotp": true,
|
||||||
// "UseScim": false, // Not supported (Not AGPLv3 Licensed)
|
// "UseScim": false, // Not supported (Not AGPLv3 Licensed)
|
||||||
"UsePolicies": true,
|
"UsePolicies": true,
|
||||||
"UseApi": false, // Not supported
|
"UseApi": true,
|
||||||
"SelfHost": true,
|
"SelfHost": true,
|
||||||
"HasPublicAndPrivateKeys": org.private_key.is_some() && org.public_key.is_some(),
|
"HasPublicAndPrivateKeys": org.private_key.is_some() && org.public_key.is_some(),
|
||||||
"ResetPasswordEnrolled": self.reset_password_key.is_some(),
|
"ResetPasswordEnrolled": self.reset_password_key.is_some(),
|
||||||
@@ -405,6 +434,7 @@ impl UserOrganization {
|
|||||||
"UserId": self.user_uuid,
|
"UserId": self.user_uuid,
|
||||||
"Name": user.name,
|
"Name": user.name,
|
||||||
"Email": user.email,
|
"Email": user.email,
|
||||||
|
"ExternalId": user.external_id,
|
||||||
"Groups": groups,
|
"Groups": groups,
|
||||||
"Collections": collections,
|
"Collections": collections,
|
||||||
|
|
||||||
@@ -412,7 +442,7 @@ impl UserOrganization {
|
|||||||
"Type": self.atype,
|
"Type": self.atype,
|
||||||
"AccessAll": self.access_all,
|
"AccessAll": self.access_all,
|
||||||
"TwoFactorEnabled": twofactor_enabled,
|
"TwoFactorEnabled": twofactor_enabled,
|
||||||
"ResetPasswordEnrolled":self.reset_password_key.is_some(),
|
"ResetPasswordEnrolled": self.reset_password_key.is_some(),
|
||||||
|
|
||||||
"Object": "organizationUserUserDetails",
|
"Object": "organizationUserUserDetails",
|
||||||
})
|
})
|
||||||
@@ -481,7 +511,7 @@ impl UserOrganization {
|
|||||||
.set(UserOrganizationDb::to_db(self))
|
.set(UserOrganizationDb::to_db(self))
|
||||||
.execute(conn)
|
.execute(conn)
|
||||||
.map_res("Error adding user to organization")
|
.map_res("Error adding user to organization")
|
||||||
}
|
},
|
||||||
Err(e) => Err(e.into()),
|
Err(e) => Err(e.into()),
|
||||||
}.map_res("Error adding user to organization")
|
}.map_res("Error adding user to organization")
|
||||||
}
|
}
|
||||||
@@ -715,6 +745,7 @@ impl UserOrganization {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
.select(users_organizations::all_columns)
|
.select(users_organizations::all_columns)
|
||||||
|
.distinct()
|
||||||
.load::<UserOrganizationDb>(conn).expect("Error loading user organizations").from_db()
|
.load::<UserOrganizationDb>(conn).expect("Error loading user organizations").from_db()
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
@@ -749,6 +780,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, organization_api_key::org_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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ db_object! {
|
|||||||
pub api_key: Option<String>,
|
pub api_key: Option<String>,
|
||||||
|
|
||||||
pub avatar_color: Option<String>,
|
pub avatar_color: Option<String>,
|
||||||
|
|
||||||
|
pub external_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Identifiable, Queryable, Insertable)]
|
#[derive(Identifiable, Queryable, Insertable)]
|
||||||
@@ -126,6 +128,8 @@ impl User {
|
|||||||
api_key: None,
|
api_key: None,
|
||||||
|
|
||||||
avatar_color: 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))
|
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
|
/// Set the password hash generated
|
||||||
/// And resets the security_stamp. Based upon the allow_next_route the security_stamp will be different.
|
/// 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> {
|
pub async fn get_all(conn: &mut DbConn) -> Vec<Self> {
|
||||||
db_run! {conn: {
|
db_run! {conn: {
|
||||||
users::table.load::<UserDb>(conn).expect("Error loading users").from_db()
|
users::table.load::<UserDb>(conn).expect("Error loading users").from_db()
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ table! {
|
|||||||
uuid -> Text,
|
uuid -> Text,
|
||||||
org_uuid -> Text,
|
org_uuid -> Text,
|
||||||
name -> Text,
|
name -> Text,
|
||||||
|
external_id -> Nullable<Text>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,6 +50,7 @@ table! {
|
|||||||
user_uuid -> Text,
|
user_uuid -> Text,
|
||||||
name -> Text,
|
name -> Text,
|
||||||
atype -> Integer,
|
atype -> Integer,
|
||||||
|
push_uuid -> Nullable<Text>,
|
||||||
push_token -> Nullable<Text>,
|
push_token -> Nullable<Text>,
|
||||||
refresh_token -> Text,
|
refresh_token -> Text,
|
||||||
twofactor_remember -> Nullable<Text>,
|
twofactor_remember -> Nullable<Text>,
|
||||||
@@ -203,6 +205,7 @@ table! {
|
|||||||
client_kdf_parallelism -> Nullable<Integer>,
|
client_kdf_parallelism -> Nullable<Integer>,
|
||||||
api_key -> Nullable<Text>,
|
api_key -> Nullable<Text>,
|
||||||
avatar_color -> 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! {
|
table! {
|
||||||
emergency_access (uuid) {
|
emergency_access (uuid) {
|
||||||
uuid -> Text,
|
uuid -> Text,
|
||||||
@@ -273,6 +286,26 @@ table! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
auth_requests (uuid) {
|
||||||
|
uuid -> Text,
|
||||||
|
user_uuid -> Text,
|
||||||
|
organization_uuid -> Nullable<Text>,
|
||||||
|
request_device_identifier -> Text,
|
||||||
|
device_type -> Integer,
|
||||||
|
request_ip -> Text,
|
||||||
|
response_device_id -> Nullable<Text>,
|
||||||
|
access_code -> Text,
|
||||||
|
public_key -> Text,
|
||||||
|
enc_key -> Text,
|
||||||
|
master_password_hash -> Text,
|
||||||
|
approved -> Nullable<Bool>,
|
||||||
|
creation_date -> Timestamp,
|
||||||
|
response_date -> Nullable<Timestamp>,
|
||||||
|
authentication_date -> Nullable<Timestamp>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
joinable!(attachments -> ciphers (cipher_uuid));
|
joinable!(attachments -> ciphers (cipher_uuid));
|
||||||
joinable!(ciphers -> organizations (organization_uuid));
|
joinable!(ciphers -> organizations (organization_uuid));
|
||||||
joinable!(ciphers -> users (user_uuid));
|
joinable!(ciphers -> users (user_uuid));
|
||||||
@@ -291,6 +324,7 @@ joinable!(users_collections -> collections (collection_uuid));
|
|||||||
joinable!(users_collections -> users (user_uuid));
|
joinable!(users_collections -> users (user_uuid));
|
||||||
joinable!(users_organizations -> organizations (org_uuid));
|
joinable!(users_organizations -> organizations (org_uuid));
|
||||||
joinable!(users_organizations -> users (user_uuid));
|
joinable!(users_organizations -> users (user_uuid));
|
||||||
|
joinable!(organization_api_key -> organizations (org_uuid));
|
||||||
joinable!(emergency_access -> users (grantor_uuid));
|
joinable!(emergency_access -> users (grantor_uuid));
|
||||||
joinable!(groups -> organizations (organizations_uuid));
|
joinable!(groups -> organizations (organizations_uuid));
|
||||||
joinable!(groups_users -> users_organizations (users_organizations_uuid));
|
joinable!(groups_users -> users_organizations (users_organizations_uuid));
|
||||||
@@ -298,6 +332,7 @@ joinable!(groups_users -> groups (groups_uuid));
|
|||||||
joinable!(collections_groups -> collections (collections_uuid));
|
joinable!(collections_groups -> collections (collections_uuid));
|
||||||
joinable!(collections_groups -> groups (groups_uuid));
|
joinable!(collections_groups -> groups (groups_uuid));
|
||||||
joinable!(event -> users_organizations (uuid));
|
joinable!(event -> users_organizations (uuid));
|
||||||
|
joinable!(auth_requests -> users (user_uuid));
|
||||||
|
|
||||||
allow_tables_to_appear_in_same_query!(
|
allow_tables_to_appear_in_same_query!(
|
||||||
attachments,
|
attachments,
|
||||||
@@ -315,9 +350,11 @@ allow_tables_to_appear_in_same_query!(
|
|||||||
users,
|
users,
|
||||||
users_collections,
|
users_collections,
|
||||||
users_organizations,
|
users_organizations,
|
||||||
|
organization_api_key,
|
||||||
emergency_access,
|
emergency_access,
|
||||||
groups,
|
groups,
|
||||||
groups_users,
|
groups_users,
|
||||||
collections_groups,
|
collections_groups,
|
||||||
event,
|
event,
|
||||||
|
auth_requests,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ table! {
|
|||||||
uuid -> Text,
|
uuid -> Text,
|
||||||
org_uuid -> Text,
|
org_uuid -> Text,
|
||||||
name -> Text,
|
name -> Text,
|
||||||
|
external_id -> Nullable<Text>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,6 +50,7 @@ table! {
|
|||||||
user_uuid -> Text,
|
user_uuid -> Text,
|
||||||
name -> Text,
|
name -> Text,
|
||||||
atype -> Integer,
|
atype -> Integer,
|
||||||
|
push_uuid -> Nullable<Text>,
|
||||||
push_token -> Nullable<Text>,
|
push_token -> Nullable<Text>,
|
||||||
refresh_token -> Text,
|
refresh_token -> Text,
|
||||||
twofactor_remember -> Nullable<Text>,
|
twofactor_remember -> Nullable<Text>,
|
||||||
@@ -203,6 +205,7 @@ table! {
|
|||||||
client_kdf_parallelism -> Nullable<Integer>,
|
client_kdf_parallelism -> Nullable<Integer>,
|
||||||
api_key -> Nullable<Text>,
|
api_key -> Nullable<Text>,
|
||||||
avatar_color -> 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! {
|
table! {
|
||||||
emergency_access (uuid) {
|
emergency_access (uuid) {
|
||||||
uuid -> Text,
|
uuid -> Text,
|
||||||
@@ -273,6 +286,26 @@ table! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
auth_requests (uuid) {
|
||||||
|
uuid -> Text,
|
||||||
|
user_uuid -> Text,
|
||||||
|
organization_uuid -> Nullable<Text>,
|
||||||
|
request_device_identifier -> Text,
|
||||||
|
device_type -> Integer,
|
||||||
|
request_ip -> Text,
|
||||||
|
response_device_id -> Nullable<Text>,
|
||||||
|
access_code -> Text,
|
||||||
|
public_key -> Text,
|
||||||
|
enc_key -> Text,
|
||||||
|
master_password_hash -> Text,
|
||||||
|
approved -> Nullable<Bool>,
|
||||||
|
creation_date -> Timestamp,
|
||||||
|
response_date -> Nullable<Timestamp>,
|
||||||
|
authentication_date -> Nullable<Timestamp>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
joinable!(attachments -> ciphers (cipher_uuid));
|
joinable!(attachments -> ciphers (cipher_uuid));
|
||||||
joinable!(ciphers -> organizations (organization_uuid));
|
joinable!(ciphers -> organizations (organization_uuid));
|
||||||
joinable!(ciphers -> users (user_uuid));
|
joinable!(ciphers -> users (user_uuid));
|
||||||
@@ -291,6 +324,7 @@ joinable!(users_collections -> collections (collection_uuid));
|
|||||||
joinable!(users_collections -> users (user_uuid));
|
joinable!(users_collections -> users (user_uuid));
|
||||||
joinable!(users_organizations -> organizations (org_uuid));
|
joinable!(users_organizations -> organizations (org_uuid));
|
||||||
joinable!(users_organizations -> users (user_uuid));
|
joinable!(users_organizations -> users (user_uuid));
|
||||||
|
joinable!(organization_api_key -> organizations (org_uuid));
|
||||||
joinable!(emergency_access -> users (grantor_uuid));
|
joinable!(emergency_access -> users (grantor_uuid));
|
||||||
joinable!(groups -> organizations (organizations_uuid));
|
joinable!(groups -> organizations (organizations_uuid));
|
||||||
joinable!(groups_users -> users_organizations (users_organizations_uuid));
|
joinable!(groups_users -> users_organizations (users_organizations_uuid));
|
||||||
@@ -298,6 +332,7 @@ joinable!(groups_users -> groups (groups_uuid));
|
|||||||
joinable!(collections_groups -> collections (collections_uuid));
|
joinable!(collections_groups -> collections (collections_uuid));
|
||||||
joinable!(collections_groups -> groups (groups_uuid));
|
joinable!(collections_groups -> groups (groups_uuid));
|
||||||
joinable!(event -> users_organizations (uuid));
|
joinable!(event -> users_organizations (uuid));
|
||||||
|
joinable!(auth_requests -> users (user_uuid));
|
||||||
|
|
||||||
allow_tables_to_appear_in_same_query!(
|
allow_tables_to_appear_in_same_query!(
|
||||||
attachments,
|
attachments,
|
||||||
@@ -315,9 +350,11 @@ allow_tables_to_appear_in_same_query!(
|
|||||||
users,
|
users,
|
||||||
users_collections,
|
users_collections,
|
||||||
users_organizations,
|
users_organizations,
|
||||||
|
organization_api_key,
|
||||||
emergency_access,
|
emergency_access,
|
||||||
groups,
|
groups,
|
||||||
groups_users,
|
groups_users,
|
||||||
collections_groups,
|
collections_groups,
|
||||||
event,
|
event,
|
||||||
|
auth_requests,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ table! {
|
|||||||
uuid -> Text,
|
uuid -> Text,
|
||||||
org_uuid -> Text,
|
org_uuid -> Text,
|
||||||
name -> Text,
|
name -> Text,
|
||||||
|
external_id -> Nullable<Text>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,6 +50,7 @@ table! {
|
|||||||
user_uuid -> Text,
|
user_uuid -> Text,
|
||||||
name -> Text,
|
name -> Text,
|
||||||
atype -> Integer,
|
atype -> Integer,
|
||||||
|
push_uuid -> Nullable<Text>,
|
||||||
push_token -> Nullable<Text>,
|
push_token -> Nullable<Text>,
|
||||||
refresh_token -> Text,
|
refresh_token -> Text,
|
||||||
twofactor_remember -> Nullable<Text>,
|
twofactor_remember -> Nullable<Text>,
|
||||||
@@ -203,6 +205,7 @@ table! {
|
|||||||
client_kdf_parallelism -> Nullable<Integer>,
|
client_kdf_parallelism -> Nullable<Integer>,
|
||||||
api_key -> Nullable<Text>,
|
api_key -> Nullable<Text>,
|
||||||
avatar_color -> 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! {
|
table! {
|
||||||
emergency_access (uuid) {
|
emergency_access (uuid) {
|
||||||
uuid -> Text,
|
uuid -> Text,
|
||||||
@@ -273,6 +286,26 @@ table! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
auth_requests (uuid) {
|
||||||
|
uuid -> Text,
|
||||||
|
user_uuid -> Text,
|
||||||
|
organization_uuid -> Nullable<Text>,
|
||||||
|
request_device_identifier -> Text,
|
||||||
|
device_type -> Integer,
|
||||||
|
request_ip -> Text,
|
||||||
|
response_device_id -> Nullable<Text>,
|
||||||
|
access_code -> Text,
|
||||||
|
public_key -> Text,
|
||||||
|
enc_key -> Text,
|
||||||
|
master_password_hash -> Text,
|
||||||
|
approved -> Nullable<Bool>,
|
||||||
|
creation_date -> Timestamp,
|
||||||
|
response_date -> Nullable<Timestamp>,
|
||||||
|
authentication_date -> Nullable<Timestamp>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
joinable!(attachments -> ciphers (cipher_uuid));
|
joinable!(attachments -> ciphers (cipher_uuid));
|
||||||
joinable!(ciphers -> organizations (organization_uuid));
|
joinable!(ciphers -> organizations (organization_uuid));
|
||||||
joinable!(ciphers -> users (user_uuid));
|
joinable!(ciphers -> users (user_uuid));
|
||||||
@@ -292,6 +325,7 @@ joinable!(users_collections -> users (user_uuid));
|
|||||||
joinable!(users_organizations -> organizations (org_uuid));
|
joinable!(users_organizations -> organizations (org_uuid));
|
||||||
joinable!(users_organizations -> users (user_uuid));
|
joinable!(users_organizations -> users (user_uuid));
|
||||||
joinable!(users_organizations -> ciphers (org_uuid));
|
joinable!(users_organizations -> ciphers (org_uuid));
|
||||||
|
joinable!(organization_api_key -> organizations (org_uuid));
|
||||||
joinable!(emergency_access -> users (grantor_uuid));
|
joinable!(emergency_access -> users (grantor_uuid));
|
||||||
joinable!(groups -> organizations (organizations_uuid));
|
joinable!(groups -> organizations (organizations_uuid));
|
||||||
joinable!(groups_users -> users_organizations (users_organizations_uuid));
|
joinable!(groups_users -> users_organizations (users_organizations_uuid));
|
||||||
@@ -299,6 +333,7 @@ joinable!(groups_users -> groups (groups_uuid));
|
|||||||
joinable!(collections_groups -> collections (collections_uuid));
|
joinable!(collections_groups -> collections (collections_uuid));
|
||||||
joinable!(collections_groups -> groups (groups_uuid));
|
joinable!(collections_groups -> groups (groups_uuid));
|
||||||
joinable!(event -> users_organizations (uuid));
|
joinable!(event -> users_organizations (uuid));
|
||||||
|
joinable!(auth_requests -> users (user_uuid));
|
||||||
|
|
||||||
allow_tables_to_appear_in_same_query!(
|
allow_tables_to_appear_in_same_query!(
|
||||||
attachments,
|
attachments,
|
||||||
@@ -316,9 +351,11 @@ allow_tables_to_appear_in_same_query!(
|
|||||||
users,
|
users,
|
||||||
users_collections,
|
users_collections,
|
||||||
users_organizations,
|
users_organizations,
|
||||||
|
organization_api_key,
|
||||||
emergency_access,
|
emergency_access,
|
||||||
groups,
|
groups,
|
||||||
groups_users,
|
groups_users,
|
||||||
collections_groups,
|
collections_groups,
|
||||||
event,
|
event,
|
||||||
|
auth_requests,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -573,8 +573,8 @@ async fn send_email(address: &str, subject: &str, body_html: String, body_text:
|
|||||||
let smtp_from = &CONFIG.smtp_from();
|
let smtp_from = &CONFIG.smtp_from();
|
||||||
|
|
||||||
let body = if CONFIG.smtp_embed_images() {
|
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 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".to_string()).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::alternative().singlepart(SinglePart::plain(body_text)).multipart(
|
||||||
MultiPart::related()
|
MultiPart::related()
|
||||||
.singlepart(SinglePart::html(body_html))
|
.singlepart(SinglePart::html(body_html))
|
||||||
|
|||||||
19
src/main.rs
19
src/main.rs
@@ -82,9 +82,12 @@ mod mail;
|
|||||||
mod ratelimit;
|
mod ratelimit;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
|
use crate::api::purge_auth_requests;
|
||||||
|
use crate::api::WS_ANONYMOUS_SUBSCRIPTIONS;
|
||||||
pub use config::CONFIG;
|
pub use config::CONFIG;
|
||||||
pub use error::{Error, MapResult};
|
pub use error::{Error, MapResult};
|
||||||
use rocket::data::{Limits, ToByteUnit};
|
use rocket::data::{Limits, ToByteUnit};
|
||||||
|
use std::sync::Arc;
|
||||||
pub use util::is_running_in_docker;
|
pub use util::is_running_in_docker;
|
||||||
|
|
||||||
#[rocket::main]
|
#[rocket::main]
|
||||||
@@ -250,7 +253,7 @@ fn init_logging(level: log::LevelFilter) -> Result<(), fern::InitError> {
|
|||||||
log::LevelFilter::Off
|
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.
|
// Else this will bloat the log output with useless messages.
|
||||||
let rocket_underscore_level = if level >= log::LevelFilter::Debug {
|
let rocket_underscore_level = if level >= log::LevelFilter::Debug {
|
||||||
log::LevelFilter::Warn
|
log::LevelFilter::Warn
|
||||||
@@ -264,8 +267,13 @@ fn init_logging(level: log::LevelFilter) -> Result<(), fern::InitError> {
|
|||||||
.level_for("rustls::session", log::LevelFilter::Off)
|
.level_for("rustls::session", log::LevelFilter::Off)
|
||||||
// Hide failed to close stream messages
|
// Hide failed to close stream messages
|
||||||
.level_for("hyper::server", log::LevelFilter::Warn)
|
.level_for("hyper::server", log::LevelFilter::Warn)
|
||||||
// Silence rocket logs
|
// Silence Rocket `_` logs
|
||||||
.level_for("_", rocket_underscore_level)
|
.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::launch_", log::LevelFilter::Error)
|
.level_for("rocket::launch_", log::LevelFilter::Error)
|
||||||
.level_for("rocket::rocket", log::LevelFilter::Warn)
|
.level_for("rocket::rocket", log::LevelFilter::Warn)
|
||||||
@@ -528,6 +536,7 @@ async fn launch_rocket(pool: db::DbPool, extra_debug: bool) -> Result<(), Error>
|
|||||||
.register([basepath, "/admin"].concat(), api::admin_catchers())
|
.register([basepath, "/admin"].concat(), api::admin_catchers())
|
||||||
.manage(pool)
|
.manage(pool)
|
||||||
.manage(api::start_notification_server())
|
.manage(api::start_notification_server())
|
||||||
|
.manage(Arc::clone(&WS_ANONYMOUS_SUBSCRIPTIONS))
|
||||||
.attach(util::AppHeaders())
|
.attach(util::AppHeaders())
|
||||||
.attach(util::Cors())
|
.attach(util::Cors())
|
||||||
.attach(util::BetterLogging(extra_debug))
|
.attach(util::BetterLogging(extra_debug))
|
||||||
@@ -603,6 +612,12 @@ fn schedule_jobs(pool: db::DbPool) {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !CONFIG.auth_request_purge_schedule().is_empty() {
|
||||||
|
sched.add(Job::new(CONFIG.auth_request_purge_schedule().parse().unwrap(), || {
|
||||||
|
runtime.spawn(purge_auth_requests(pool.clone()));
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
// Cleanup the event table of records x days old.
|
// Cleanup the event table of records x days old.
|
||||||
if CONFIG.org_events_enabled()
|
if CONFIG.org_events_enabled()
|
||||||
&& !CONFIG.event_cleanup_schedule().is_empty()
|
&& !CONFIG.event_cleanup_schedule().is_empty()
|
||||||
|
|||||||
@@ -952,5 +952,24 @@
|
|||||||
"jira.com"
|
"jira.com"
|
||||||
],
|
],
|
||||||
"Excluded": false
|
"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;
|
min-width: 85px;
|
||||||
max-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;
|
min-width: 35px;
|
||||||
max-width: 40px;
|
max-width: 40px;
|
||||||
}
|
}
|
||||||
@@ -53,4 +53,4 @@ img {
|
|||||||
}
|
}
|
||||||
.vw-copy-toast {
|
.vw-copy-toast {
|
||||||
width: 15rem;
|
width: 15rem;
|
||||||
}
|
}
|
||||||
|
|||||||
98
src/static/scripts/admin.js
vendored
98
src/static/scripts/admin.js
vendored
@@ -3,16 +3,17 @@
|
|||||||
/* exported BASE_URL, _post */
|
/* exported BASE_URL, _post */
|
||||||
|
|
||||||
function getBaseUrl() {
|
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:
|
// `window.location.href` should have one of the following forms:
|
||||||
//
|
//
|
||||||
// - `https://vaultwarden.example.com/base/path/`
|
// - `https://vaultwarden.example.com/base/path/admin`
|
||||||
// - `https://vaultwarden.example.com/base/path/#/some/route[?queryParam=...]`
|
// - `https://vaultwarden.example.com/base/path/admin/#/some/route[?queryParam=...]`
|
||||||
//
|
//
|
||||||
// We want to get to just `https://vaultwarden.example.com/base/path`.
|
// We want to get to just `https://vaultwarden.example.com/base/path`.
|
||||||
const baseUrl = window.location.href;
|
const pathname = window.location.pathname;
|
||||||
const adminPos = baseUrl.indexOf("/admin");
|
const adminPos = pathname.indexOf("/admin");
|
||||||
return baseUrl.substring(0, adminPos != -1 ? adminPos : baseUrl.length);
|
const newPathname = pathname.substring(0, adminPos != -1 ? adminPos : pathname.length);
|
||||||
|
return `${window.location.origin}${newPathname}`;
|
||||||
}
|
}
|
||||||
const BASE_URL = getBaseUrl();
|
const BASE_URL = getBaseUrl();
|
||||||
|
|
||||||
@@ -36,36 +37,107 @@ function _post(url, successMsg, errMsg, body, reload_page = true) {
|
|||||||
mode: "same-origin",
|
mode: "same-origin",
|
||||||
credentials: "same-origin",
|
credentials: "same-origin",
|
||||||
headers: { "Content-Type": "application/json" }
|
headers: { "Content-Type": "application/json" }
|
||||||
}).then( resp => {
|
}).then(resp => {
|
||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
msg(successMsg, reload_page);
|
msg(successMsg, reload_page);
|
||||||
// Abuse the catch handler by setting error to false and continue
|
// Abuse the catch handler by setting error to false and continue
|
||||||
return Promise.reject({error: false});
|
return Promise.reject({ error: false });
|
||||||
}
|
}
|
||||||
respStatus = resp.status;
|
respStatus = resp.status;
|
||||||
respStatusText = resp.statusText;
|
respStatusText = resp.statusText;
|
||||||
return resp.text();
|
return resp.text();
|
||||||
}).then( respText => {
|
}).then(respText => {
|
||||||
try {
|
try {
|
||||||
const respJson = JSON.parse(respText);
|
const respJson = JSON.parse(respText);
|
||||||
if (respJson.ErrorModel && respJson.ErrorModel.Message) {
|
if (respJson.ErrorModel && respJson.ErrorModel.Message) {
|
||||||
return respJson.ErrorModel.Message;
|
return respJson.ErrorModel.Message;
|
||||||
} else {
|
} else {
|
||||||
return Promise.reject({body:`${respStatus} - ${respStatusText}\n\nUnknown error`, error: true});
|
return Promise.reject({ body: `${respStatus} - ${respStatusText}\n\nUnknown error`, error: true });
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return Promise.reject({body:`${respStatus} - ${respStatusText}\n\n[Catch] ${e}`, error: true});
|
return Promise.reject({ body: `${respStatus} - ${respStatusText}\n\n[Catch] ${e}`, error: true });
|
||||||
}
|
}
|
||||||
}).then( apiMsg => {
|
}).then(apiMsg => {
|
||||||
msg(`${errMsg}\n${apiMsg}`, reload_page);
|
msg(`${errMsg}\n${apiMsg}`, reload_page);
|
||||||
}).catch( e => {
|
}).catch(e => {
|
||||||
if (e.error === false) { return true; }
|
if (e.error === false) { return true; }
|
||||||
else { msg(`${errMsg}\n${e.body}`, reload_page); }
|
else { msg(`${errMsg}\n${e.body}`, reload_page); }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bootstrap Theme Selector
|
||||||
|
const getStoredTheme = () => localStorage.getItem("theme");
|
||||||
|
const setStoredTheme = theme => localStorage.setItem("theme", theme);
|
||||||
|
|
||||||
|
const getPreferredTheme = () => {
|
||||||
|
const storedTheme = getStoredTheme();
|
||||||
|
if (storedTheme) {
|
||||||
|
return storedTheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||||
|
};
|
||||||
|
|
||||||
|
const setTheme = theme => {
|
||||||
|
if (theme === "auto" && window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
||||||
|
document.documentElement.setAttribute("data-bs-theme", "dark");
|
||||||
|
} else {
|
||||||
|
document.documentElement.setAttribute("data-bs-theme", theme);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
setTheme(getPreferredTheme());
|
||||||
|
|
||||||
|
const showActiveTheme = (theme, focus = false) => {
|
||||||
|
const themeSwitcher = document.querySelector("#bd-theme");
|
||||||
|
|
||||||
|
if (!themeSwitcher) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const themeSwitcherText = document.querySelector("#bd-theme-text");
|
||||||
|
const activeThemeIcon = document.querySelector(".theme-icon-active use");
|
||||||
|
const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`);
|
||||||
|
const svgOfActiveBtn = btnToActive.querySelector("span use").innerText;
|
||||||
|
|
||||||
|
document.querySelectorAll("[data-bs-theme-value]").forEach(element => {
|
||||||
|
element.classList.remove("active");
|
||||||
|
element.setAttribute("aria-pressed", "false");
|
||||||
|
});
|
||||||
|
|
||||||
|
btnToActive.classList.add("active");
|
||||||
|
btnToActive.setAttribute("aria-pressed", "true");
|
||||||
|
activeThemeIcon.innerText = svgOfActiveBtn;
|
||||||
|
const themeSwitcherLabel = `${themeSwitcherText.textContent} (${btnToActive.dataset.bsThemeValue})`;
|
||||||
|
themeSwitcher.setAttribute("aria-label", themeSwitcherLabel);
|
||||||
|
|
||||||
|
if (focus) {
|
||||||
|
themeSwitcher.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => {
|
||||||
|
const storedTheme = getStoredTheme();
|
||||||
|
if (storedTheme !== "light" && storedTheme !== "dark") {
|
||||||
|
setTheme(getPreferredTheme());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
// onLoad events
|
// onLoad events
|
||||||
document.addEventListener("DOMContentLoaded", (/*event*/) => {
|
document.addEventListener("DOMContentLoaded", (/*event*/) => {
|
||||||
|
showActiveTheme(getPreferredTheme());
|
||||||
|
|
||||||
|
document.querySelectorAll("[data-bs-theme-value]")
|
||||||
|
.forEach(toggle => {
|
||||||
|
toggle.addEventListener("click", () => {
|
||||||
|
const theme = toggle.getAttribute("data-bs-theme-value");
|
||||||
|
setStoredTheme(theme);
|
||||||
|
setTheme(theme);
|
||||||
|
showActiveTheme(theme, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// get current URL path and assign "active" class to the correct nav-item
|
// get current URL path and assign "active" class to the correct nav-item
|
||||||
const pathname = window.location.pathname;
|
const pathname = window.location.pathname;
|
||||||
if (pathname === "") return;
|
if (pathname === "") return;
|
||||||
|
|||||||
4
src/static/scripts/admin_diagnostics.js
vendored
4
src/static/scripts/admin_diagnostics.js
vendored
@@ -1,6 +1,6 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
/* eslint-env es2017, browser */
|
/* eslint-env es2017, browser */
|
||||||
/* global BASE_URL:readable, BSN:readable */
|
/* global BASE_URL:readable, bootstrap:readable */
|
||||||
|
|
||||||
var dnsCheck = false;
|
var dnsCheck = false;
|
||||||
var timeCheck = false;
|
var timeCheck = false;
|
||||||
@@ -135,7 +135,7 @@ function copyToClipboard(event) {
|
|||||||
document.execCommand("copy");
|
document.execCommand("copy");
|
||||||
tmpCopyEl.remove();
|
tmpCopyEl.remove();
|
||||||
|
|
||||||
new BSN.Toast("#toastClipboardCopy").show();
|
new bootstrap.Toast("#toastClipboardCopy").show();
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkTimeDrift(utcTimeA, utcTimeB, statusPrefix) {
|
function checkTimeDrift(utcTimeA, utcTimeB, statusPrefix) {
|
||||||
|
|||||||
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 });
|
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",
|
"SMTP Test email sent correctly",
|
||||||
"Error sending SMTP test email",
|
"Error sending SMTP test email",
|
||||||
data, false
|
data, false
|
||||||
@@ -45,7 +45,7 @@ function getFormData() {
|
|||||||
|
|
||||||
function saveConfig(event) {
|
function saveConfig(event) {
|
||||||
const data = JSON.stringify(getFormData());
|
const data = JSON.stringify(getFormData());
|
||||||
_post(`${BASE_URL}/admin/config/`,
|
_post(`${BASE_URL}/admin/config`,
|
||||||
"Config saved correctly",
|
"Config saved correctly",
|
||||||
"Error saving config",
|
"Error saving config",
|
||||||
data
|
data
|
||||||
|
|||||||
37
src/static/scripts/admin_users.js
vendored
37
src/static/scripts/admin_users.js
vendored
@@ -113,29 +113,48 @@ function inviteUser(event) {
|
|||||||
"email": email.value
|
"email": email.value
|
||||||
});
|
});
|
||||||
email.value = "";
|
email.value = "";
|
||||||
_post(`${BASE_URL}/admin/invite/`,
|
_post(`${BASE_URL}/admin/invite`,
|
||||||
"User invited correctly",
|
"User invited correctly",
|
||||||
"Error inviting user",
|
"Error inviting user",
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resendUserInvite (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
const id = event.target.parentNode.dataset.vwUserUuid;
|
||||||
|
const email = event.target.parentNode.dataset.vwUserEmail;
|
||||||
|
if (!id || !email) {
|
||||||
|
alert("Required parameters not found!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const confirmed = confirm(`Are you sure you want to resend invitation for "${email}"?`);
|
||||||
|
if (confirmed) {
|
||||||
|
_post(`${BASE_URL}/admin/users/${id}/invite/resend`,
|
||||||
|
"Invite sent successfully",
|
||||||
|
"Error resend invite"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const ORG_TYPES = {
|
const ORG_TYPES = {
|
||||||
"0": {
|
"0": {
|
||||||
"name": "Owner",
|
"name": "Owner",
|
||||||
"color": "orange"
|
"bg": "orange",
|
||||||
|
"font": "black"
|
||||||
},
|
},
|
||||||
"1": {
|
"1": {
|
||||||
"name": "Admin",
|
"name": "Admin",
|
||||||
"color": "blueviolet"
|
"bg": "blueviolet"
|
||||||
},
|
},
|
||||||
"2": {
|
"2": {
|
||||||
"name": "User",
|
"name": "User",
|
||||||
"color": "blue"
|
"bg": "blue"
|
||||||
},
|
},
|
||||||
"3": {
|
"3": {
|
||||||
"name": "Manager",
|
"name": "Manager",
|
||||||
"color": "green"
|
"bg": "green"
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -209,7 +228,10 @@ function initUserTable() {
|
|||||||
// Color all the org buttons per type
|
// Color all the org buttons per type
|
||||||
document.querySelectorAll("button[data-vw-org-type]").forEach(function(e) {
|
document.querySelectorAll("button[data-vw-org-type]").forEach(function(e) {
|
||||||
const orgType = ORG_TYPES[e.dataset.vwOrgType];
|
const orgType = ORG_TYPES[e.dataset.vwOrgType];
|
||||||
e.style.backgroundColor = orgType.color;
|
e.style.backgroundColor = orgType.bg;
|
||||||
|
if (orgType.font !== undefined) {
|
||||||
|
e.style.color = orgType.font;
|
||||||
|
}
|
||||||
e.title = orgType.name;
|
e.title = orgType.name;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -228,6 +250,9 @@ function initUserTable() {
|
|||||||
document.querySelectorAll("button[vw-enable-user]").forEach(btn => {
|
document.querySelectorAll("button[vw-enable-user]").forEach(btn => {
|
||||||
btn.addEventListener("click", enableUser);
|
btn.addEventListener("click", enableUser);
|
||||||
});
|
});
|
||||||
|
document.querySelectorAll("button[vw-resend-user-invite]").forEach(btn => {
|
||||||
|
btn.addEventListener("click", resendUserInvite);
|
||||||
|
});
|
||||||
|
|
||||||
if (jdenticon) {
|
if (jdenticon) {
|
||||||
jdenticon();
|
jdenticon();
|
||||||
|
|||||||
5991
src/static/scripts/bootstrap-native.js
vendored
5991
src/static/scripts/bootstrap-native.js
vendored
File diff suppressed because it is too large
Load Diff
6313
src/static/scripts/bootstrap.bundle.js
vendored
Normal file
6313
src/static/scripts/bootstrap.bundle.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
2379
src/static/scripts/bootstrap.css
vendored
2379
src/static/scripts/bootstrap.css
vendored
File diff suppressed because it is too large
Load Diff
58
src/static/scripts/datatables.css
vendored
58
src/static/scripts/datatables.css
vendored
@@ -4,10 +4,10 @@
|
|||||||
*
|
*
|
||||||
* To rebuild or modify this file with the latest versions of the included
|
* To rebuild or modify this file with the latest versions of the included
|
||||||
* software please visit:
|
* software please visit:
|
||||||
* https://datatables.net/download/#bs5/dt-1.13.2
|
* https://datatables.net/download/#bs5/dt-1.13.6
|
||||||
*
|
*
|
||||||
* Included libraries:
|
* Included libraries:
|
||||||
* DataTables 1.13.2
|
* DataTables 1.13.6
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@charset "UTF-8";
|
@charset "UTF-8";
|
||||||
@@ -15,6 +15,13 @@
|
|||||||
--dt-row-selected: 13, 110, 253;
|
--dt-row-selected: 13, 110, 253;
|
||||||
--dt-row-selected-text: 255, 255, 255;
|
--dt-row-selected-text: 255, 255, 255;
|
||||||
--dt-row-selected-link: 9, 10, 11;
|
--dt-row-selected-link: 9, 10, 11;
|
||||||
|
--dt-row-stripe: 0, 0, 0;
|
||||||
|
--dt-row-hover: 0, 0, 0;
|
||||||
|
--dt-column-ordering: 0, 0, 0;
|
||||||
|
--dt-html-background: white;
|
||||||
|
}
|
||||||
|
:root.dark {
|
||||||
|
--dt-html-background: rgb(33, 37, 41);
|
||||||
}
|
}
|
||||||
|
|
||||||
table.dataTable td.dt-control {
|
table.dataTable td.dt-control {
|
||||||
@@ -22,25 +29,19 @@ table.dataTable td.dt-control {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
table.dataTable td.dt-control:before {
|
table.dataTable td.dt-control:before {
|
||||||
height: 1em;
|
|
||||||
width: 1em;
|
|
||||||
margin-top: -9px;
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
color: white;
|
color: rgba(0, 0, 0, 0.5);
|
||||||
border: 0.15em solid white;
|
content: "►";
|
||||||
border-radius: 1em;
|
|
||||||
box-shadow: 0 0 0.2em #444;
|
|
||||||
box-sizing: content-box;
|
|
||||||
text-align: center;
|
|
||||||
text-indent: 0 !important;
|
|
||||||
font-family: "Courier New", Courier, monospace;
|
|
||||||
line-height: 1em;
|
|
||||||
content: "+";
|
|
||||||
background-color: #31b131;
|
|
||||||
}
|
}
|
||||||
table.dataTable tr.dt-hasChild td.dt-control:before {
|
table.dataTable tr.dt-hasChild td.dt-control:before {
|
||||||
content: "-";
|
content: "▼";
|
||||||
background-color: #d33333;
|
}
|
||||||
|
|
||||||
|
html.dark table.dataTable td.dt-control:before {
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
html.dark table.dataTable tr.dt-hasChild td.dt-control:before {
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
table.dataTable thead > tr > th.sorting, table.dataTable thead > tr > th.sorting_asc, table.dataTable thead > tr > th.sorting_desc, table.dataTable thead > tr > th.sorting_asc_disabled, table.dataTable thead > tr > th.sorting_desc_disabled,
|
table.dataTable thead > tr > th.sorting, table.dataTable thead > tr > th.sorting_asc, table.dataTable thead > tr > th.sorting_desc, table.dataTable thead > tr > th.sorting_asc_disabled, table.dataTable thead > tr > th.sorting_desc_disabled,
|
||||||
@@ -79,6 +80,7 @@ table.dataTable thead > tr > td.sorting_asc_disabled:before,
|
|||||||
table.dataTable thead > tr > td.sorting_desc_disabled:before {
|
table.dataTable thead > tr > td.sorting_desc_disabled:before {
|
||||||
bottom: 50%;
|
bottom: 50%;
|
||||||
content: "▲";
|
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 > th.sorting:after, table.dataTable thead > tr > th.sorting_asc:after, table.dataTable thead > tr > th.sorting_desc:after, table.dataTable thead > tr > th.sorting_asc_disabled:after, table.dataTable thead > tr > th.sorting_desc_disabled:after,
|
||||||
table.dataTable thead > tr > td.sorting:after,
|
table.dataTable thead > tr > td.sorting:after,
|
||||||
@@ -88,6 +90,7 @@ table.dataTable thead > tr > td.sorting_asc_disabled:after,
|
|||||||
table.dataTable thead > tr > td.sorting_desc_disabled:after {
|
table.dataTable thead > tr > td.sorting_desc_disabled:after {
|
||||||
top: 50%;
|
top: 50%;
|
||||||
content: "▼";
|
content: "▼";
|
||||||
|
content: "▼"/"";
|
||||||
}
|
}
|
||||||
table.dataTable thead > tr > th.sorting_asc:before, table.dataTable thead > tr > th.sorting_desc:after,
|
table.dataTable thead > tr > th.sorting_asc:before, table.dataTable thead > tr > th.sorting_desc:after,
|
||||||
table.dataTable thead > tr > td.sorting_asc:before,
|
table.dataTable thead > tr > td.sorting_asc:before,
|
||||||
@@ -104,9 +107,9 @@ table.dataTable thead > tr > td:active {
|
|||||||
outline: none;
|
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 > 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:before,
|
||||||
div.dataTables_scrollBody table.dataTable thead > tr > td:after {
|
div.dataTables_scrollBody > table.dataTable > thead > tr > td:after {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,7 +135,8 @@ div.dataTables_processing > div:last-child > div {
|
|||||||
width: 13px;
|
width: 13px;
|
||||||
height: 13px;
|
height: 13px;
|
||||||
border-radius: 50%;
|
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);
|
animation-timing-function: cubic-bezier(0, 1, 1, 0);
|
||||||
}
|
}
|
||||||
div.dataTables_processing > div:last-child > div:nth-child(1) {
|
div.dataTables_processing > div:last-child > div:nth-child(1) {
|
||||||
@@ -300,14 +304,14 @@ table.dataTable > tbody > tr.selected a {
|
|||||||
color: rgb(var(--dt-row-selected-link));
|
color: rgb(var(--dt-row-selected-link));
|
||||||
}
|
}
|
||||||
table.dataTable.table-striped > tbody > tr.odd > * {
|
table.dataTable.table-striped > tbody > tr.odd > * {
|
||||||
box-shadow: inset 0 0 0 9999px rgba(0, 0, 0, 0.05);
|
box-shadow: inset 0 0 0 9999px rgba(var(--dt-row-stripe), 0.05);
|
||||||
}
|
}
|
||||||
table.dataTable.table-striped > tbody > tr.odd.selected > * {
|
table.dataTable.table-striped > tbody > tr.odd.selected > * {
|
||||||
box-shadow: inset 0 0 0 9999px rgba(13, 110, 253, 0.95);
|
box-shadow: inset 0 0 0 9999px rgba(13, 110, 253, 0.95);
|
||||||
box-shadow: inset 0 0 0 9999px rgba(var(--dt-row-selected), 0.95);
|
box-shadow: inset 0 0 0 9999px rgba(var(--dt-row-selected), 0.95);
|
||||||
}
|
}
|
||||||
table.dataTable.table-hover > tbody > tr:hover > * {
|
table.dataTable.table-hover > tbody > tr:hover > * {
|
||||||
box-shadow: inset 0 0 0 9999px rgba(0, 0, 0, 0.075);
|
box-shadow: inset 0 0 0 9999px rgba(var(--dt-row-hover), 0.075);
|
||||||
}
|
}
|
||||||
table.dataTable.table-hover > tbody > tr.selected:hover > * {
|
table.dataTable.table-hover > tbody > tr.selected:hover > * {
|
||||||
box-shadow: inset 0 0 0 9999px rgba(13, 110, 253, 0.975);
|
box-shadow: inset 0 0 0 9999px rgba(13, 110, 253, 0.975);
|
||||||
@@ -438,4 +442,10 @@ div.table-responsive > div.dataTables_wrapper > div.row > div[class^=col-]:last-
|
|||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme=dark] {
|
||||||
|
--dt-row-hover: 255, 255, 255;
|
||||||
|
--dt-row-stripe: 255, 255, 255;
|
||||||
|
--dt-column-ordering: 255, 255, 255;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
212
src/static/scripts/datatables.js
vendored
212
src/static/scripts/datatables.js
vendored
@@ -4,20 +4,20 @@
|
|||||||
*
|
*
|
||||||
* To rebuild or modify this file with the latest versions of the included
|
* To rebuild or modify this file with the latest versions of the included
|
||||||
* software please visit:
|
* software please visit:
|
||||||
* https://datatables.net/download/#bs5/dt-1.13.2
|
* https://datatables.net/download/#bs5/dt-1.13.6
|
||||||
*
|
*
|
||||||
* Included libraries:
|
* Included libraries:
|
||||||
* DataTables 1.13.2
|
* DataTables 1.13.6
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/*! DataTables 1.13.2
|
/*! DataTables 1.13.6
|
||||||
* ©2008-2023 SpryMedia Ltd - datatables.net/license
|
* ©2008-2023 SpryMedia Ltd - datatables.net/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary DataTables
|
* @summary DataTables
|
||||||
* @description Paginate, search and order HTML tables
|
* @description Paginate, search and order HTML tables
|
||||||
* @version 1.13.2
|
* @version 1.13.6
|
||||||
* @author SpryMedia Ltd
|
* @author SpryMedia Ltd
|
||||||
* @contact www.datatables.net
|
* @contact www.datatables.net
|
||||||
* @copyright SpryMedia Ltd.
|
* @copyright SpryMedia Ltd.
|
||||||
@@ -46,21 +46,28 @@
|
|||||||
}
|
}
|
||||||
else if ( typeof exports === 'object' ) {
|
else if ( typeof exports === 'object' ) {
|
||||||
// CommonJS
|
// CommonJS
|
||||||
module.exports = function (root, $) {
|
// jQuery's factory checks for a global window - if it isn't present then it
|
||||||
if ( ! root ) {
|
// returns a factory function that expects the window object
|
||||||
// CommonJS environments without a window global must pass a
|
var jq = require('jquery');
|
||||||
// root. This will give an error otherwise
|
|
||||||
root = window;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( ! $ ) {
|
if (typeof window === 'undefined') {
|
||||||
$ = typeof window !== 'undefined' ? // jQuery's factory checks for a global window
|
module.exports = function (root, $) {
|
||||||
require('jquery') :
|
if ( ! root ) {
|
||||||
require('jquery')( 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 {
|
else {
|
||||||
// Browser
|
// Browser
|
||||||
@@ -73,6 +80,12 @@
|
|||||||
|
|
||||||
var DataTable = function ( selector, options )
|
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
|
// When creating with `new`, create a new DataTable, returning the API instance
|
||||||
if (this instanceof DataTable) {
|
if (this instanceof DataTable) {
|
||||||
return $(selector).DataTable(options);
|
return $(selector).DataTable(options);
|
||||||
@@ -1177,6 +1190,7 @@
|
|||||||
type: sort !== null ? i+'.@data-'+sort : undefined,
|
type: sort !== null ? i+'.@data-'+sort : undefined,
|
||||||
filter: filter !== null ? i+'.@data-'+filter : undefined
|
filter: filter !== null ? i+'.@data-'+filter : undefined
|
||||||
};
|
};
|
||||||
|
col._isArrayHost = true;
|
||||||
|
|
||||||
_fnColumnOptions( oSettings, i );
|
_fnColumnOptions( oSettings, i );
|
||||||
}
|
}
|
||||||
@@ -1382,7 +1396,7 @@
|
|||||||
|
|
||||||
|
|
||||||
var _isNumber = function ( d, decimalPoint, formatted ) {
|
var _isNumber = function ( d, decimalPoint, formatted ) {
|
||||||
let type = typeof d;
|
var type = typeof d;
|
||||||
var strType = type === 'string';
|
var strType = type === 'string';
|
||||||
|
|
||||||
if ( type === 'number' || type === 'bigint') {
|
if ( type === 'number' || type === 'bigint') {
|
||||||
@@ -1516,7 +1530,9 @@
|
|||||||
|
|
||||||
|
|
||||||
var _stripHtml = function ( d ) {
|
var _stripHtml = function ( d ) {
|
||||||
return d.replace( _re_html, '' );
|
return d
|
||||||
|
.replace( _re_html, '' ) // Complete tags
|
||||||
|
.replace(/<script/i, ''); // Safety for incomplete script tag
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -1890,7 +1906,10 @@
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( data === null || data[ a[i] ] === undefined ) {
|
if (data === null || data[ a[i] ] === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
else if ( data === undefined || data[ a[i] ] === undefined ) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2337,6 +2356,12 @@
|
|||||||
oCol.aDataSort = [ oOptions.iDataSort ];
|
oCol.aDataSort = [ oOptions.iDataSort ];
|
||||||
}
|
}
|
||||||
_fnMap( oCol, oOptions, "aDataSort" );
|
_fnMap( oCol, oOptions, "aDataSort" );
|
||||||
|
|
||||||
|
// Fall back to the aria-label attribute on the table header if no ariaTitle is
|
||||||
|
// provided.
|
||||||
|
if (! oCol.ariaTitle) {
|
||||||
|
oCol.ariaTitle = th.attr("aria-label");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Cache the data get and set functions for speed */
|
/* Cache the data get and set functions for speed */
|
||||||
@@ -2365,7 +2390,7 @@
|
|||||||
|
|
||||||
// Indicate if DataTables should read DOM data as an object or array
|
// Indicate if DataTables should read DOM data as an object or array
|
||||||
// Used in _fnGetRowElements
|
// Used in _fnGetRowElements
|
||||||
if ( typeof mDataSrc !== 'number' ) {
|
if ( typeof mDataSrc !== 'number' && ! oCol._isArrayHost ) {
|
||||||
oSettings._rowReadObject = true;
|
oSettings._rowReadObject = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4061,11 +4086,16 @@
|
|||||||
settings.iDraw++;
|
settings.iDraw++;
|
||||||
_fnProcessingDisplay( settings, true );
|
_fnProcessingDisplay( settings, true );
|
||||||
|
|
||||||
|
// Keep track of drawHold state to handle scrolling after the Ajax call
|
||||||
|
var drawHold = settings._drawHold;
|
||||||
|
|
||||||
_fnBuildAjax(
|
_fnBuildAjax(
|
||||||
settings,
|
settings,
|
||||||
_fnAjaxParameters( settings ),
|
_fnAjaxParameters( settings ),
|
||||||
function(json) {
|
function(json) {
|
||||||
|
settings._drawHold = drawHold;
|
||||||
_fnAjaxUpdateDraw( settings, json );
|
_fnAjaxUpdateDraw( settings, json );
|
||||||
|
settings._drawHold = false;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -4329,7 +4359,7 @@
|
|||||||
_fnThrottle( searchFn, searchDelay ) :
|
_fnThrottle( searchFn, searchDelay ) :
|
||||||
searchFn
|
searchFn
|
||||||
)
|
)
|
||||||
.on( 'mouseup', function(e) {
|
.on( 'mouseup.DT', function(e) {
|
||||||
// Edge fix! Edge 17 does not trigger anything other than mouse events when clicking
|
// Edge fix! Edge 17 does not trigger anything other than mouse events when clicking
|
||||||
// on the clear icon (Edge bug 17584515). This is safe in other browsers as `searchFn`
|
// on the clear icon (Edge bug 17584515). This is safe in other browsers as `searchFn`
|
||||||
// checks the value to see if it has changed. In other browsers it won't have.
|
// checks the value to see if it has changed. In other browsers it won't have.
|
||||||
@@ -4395,7 +4425,7 @@
|
|||||||
if ( _fnDataSource( oSettings ) != 'ssp' )
|
if ( _fnDataSource( oSettings ) != 'ssp' )
|
||||||
{
|
{
|
||||||
/* Global filter */
|
/* Global filter */
|
||||||
_fnFilter( oSettings, oInput.sSearch, iForce, fnRegex(oInput), oInput.bSmart, oInput.bCaseInsensitive, oInput.return );
|
_fnFilter( oSettings, oInput.sSearch, iForce, fnRegex(oInput), oInput.bSmart, oInput.bCaseInsensitive );
|
||||||
fnSaveFilter( oInput );
|
fnSaveFilter( oInput );
|
||||||
|
|
||||||
/* Now do the individual column filter */
|
/* Now do the individual column filter */
|
||||||
@@ -4564,11 +4594,15 @@
|
|||||||
*
|
*
|
||||||
* ^(?=.*?\bone\b)(?=.*?\btwo three\b)(?=.*?\bfour\b).*$
|
* ^(?=.*?\bone\b)(?=.*?\btwo three\b)(?=.*?\bfour\b).*$
|
||||||
*/
|
*/
|
||||||
var a = $.map( search.match( /"[^"]+"|[^ ]+/g ) || [''], function ( word ) {
|
var a = $.map( search.match( /["\u201C][^"\u201D]+["\u201D]|[^ ]+/g ) || [''], function ( word ) {
|
||||||
if ( word.charAt(0) === '"' ) {
|
if ( word.charAt(0) === '"' ) {
|
||||||
var m = word.match( /^"(.*)"$/ );
|
var m = word.match( /^"(.*)"$/ );
|
||||||
word = m ? m[1] : word;
|
word = m ? m[1] : word;
|
||||||
}
|
}
|
||||||
|
else if ( word.charAt(0) === '\u201C' ) {
|
||||||
|
var m = word.match( /^\u201C(.*)\u201D$/ );
|
||||||
|
word = m ? m[1] : word;
|
||||||
|
}
|
||||||
|
|
||||||
return word.replace('"', '');
|
return word.replace('"', '');
|
||||||
} );
|
} );
|
||||||
@@ -5119,7 +5153,8 @@
|
|||||||
{
|
{
|
||||||
return $('<div/>', {
|
return $('<div/>', {
|
||||||
'id': ! settings.aanFeatures.r ? settings.sTableId+'_processing' : null,
|
'id': ! settings.aanFeatures.r ? settings.sTableId+'_processing' : null,
|
||||||
'class': settings.oClasses.sProcessing
|
'class': settings.oClasses.sProcessing,
|
||||||
|
'role': 'status'
|
||||||
} )
|
} )
|
||||||
.html( settings.oLanguage.sProcessing )
|
.html( settings.oLanguage.sProcessing )
|
||||||
.append('<div><div></div><div></div><div></div><div></div></div>')
|
.append('<div><div></div><div></div><div></div><div></div></div>')
|
||||||
@@ -9367,6 +9402,52 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the jQuery or window object to be used by DataTables
|
||||||
|
*
|
||||||
|
* @param {*} module Library / container object
|
||||||
|
* @param {string} [type] Library or container type `lib`, `win` or `datetime`.
|
||||||
|
* If not provided, automatic detection is attempted.
|
||||||
|
*/
|
||||||
|
DataTable.use = function (module, type) {
|
||||||
|
if (type === 'lib' || module.fn) {
|
||||||
|
$ = module;
|
||||||
|
}
|
||||||
|
else if (type == 'win' || module.document) {
|
||||||
|
window = module;
|
||||||
|
document = module.document;
|
||||||
|
}
|
||||||
|
else if (type === 'datetime' || module.type === 'DateTime') {
|
||||||
|
DataTable.DateTime = module;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
* Provide a common method for plug-ins to check the version of DataTables being
|
||||||
* used, in order to ensure compatibility.
|
* used, in order to ensure compatibility.
|
||||||
@@ -9698,7 +9779,9 @@
|
|||||||
resolved._;
|
resolved._;
|
||||||
}
|
}
|
||||||
|
|
||||||
return resolved.replace( '%d', plural ); // nb: plural might be undefined,
|
return typeof resolved === 'string'
|
||||||
|
? resolved.replace( '%d', plural ) // nb: plural might be undefined,
|
||||||
|
: resolved;
|
||||||
} );
|
} );
|
||||||
/**
|
/**
|
||||||
* Version string for plug-ins to check compatibility. Allowed format is
|
* Version string for plug-ins to check compatibility. Allowed format is
|
||||||
@@ -9708,7 +9791,7 @@
|
|||||||
* @type string
|
* @type string
|
||||||
* @default Version number
|
* @default Version number
|
||||||
*/
|
*/
|
||||||
DataTable.version = "1.13.2";
|
DataTable.version = "1.13.6";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Private data store, containing all of the settings objects that are
|
* Private data store, containing all of the settings objects that are
|
||||||
@@ -14132,7 +14215,7 @@
|
|||||||
*
|
*
|
||||||
* @type string
|
* @type string
|
||||||
*/
|
*/
|
||||||
build:"bs5/dt-1.13.2",
|
build:"bs5/dt-1.13.6",
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -14773,7 +14856,7 @@
|
|||||||
var btnDisplay, btnClass;
|
var btnDisplay, btnClass;
|
||||||
|
|
||||||
var attach = function( container, buttons ) {
|
var attach = function( container, buttons ) {
|
||||||
var i, ien, node, button, tabIndex;
|
var i, ien, node, button;
|
||||||
var disabledClass = classes.sPageButtonDisabled;
|
var disabledClass = classes.sPageButtonDisabled;
|
||||||
var clickHandler = function ( e ) {
|
var clickHandler = function ( e ) {
|
||||||
_fnPageChange( settings, e.data.action, true );
|
_fnPageChange( settings, e.data.action, true );
|
||||||
@@ -14788,9 +14871,10 @@
|
|||||||
attach( inner, button );
|
attach( inner, button );
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
var disabled = false;
|
||||||
|
|
||||||
btnDisplay = null;
|
btnDisplay = null;
|
||||||
btnClass = button;
|
btnClass = button;
|
||||||
tabIndex = settings.iTabIndex;
|
|
||||||
|
|
||||||
switch ( button ) {
|
switch ( button ) {
|
||||||
case 'ellipsis':
|
case 'ellipsis':
|
||||||
@@ -14801,8 +14885,7 @@
|
|||||||
btnDisplay = lang.sFirst;
|
btnDisplay = lang.sFirst;
|
||||||
|
|
||||||
if ( page === 0 ) {
|
if ( page === 0 ) {
|
||||||
tabIndex = -1;
|
disabled = true;
|
||||||
btnClass += ' ' + disabledClass;
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -14810,8 +14893,7 @@
|
|||||||
btnDisplay = lang.sPrevious;
|
btnDisplay = lang.sPrevious;
|
||||||
|
|
||||||
if ( page === 0 ) {
|
if ( page === 0 ) {
|
||||||
tabIndex = -1;
|
disabled = true;
|
||||||
btnClass += ' ' + disabledClass;
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -14819,8 +14901,7 @@
|
|||||||
btnDisplay = lang.sNext;
|
btnDisplay = lang.sNext;
|
||||||
|
|
||||||
if ( pages === 0 || page === pages-1 ) {
|
if ( pages === 0 || page === pages-1 ) {
|
||||||
tabIndex = -1;
|
disabled = true;
|
||||||
btnClass += ' ' + disabledClass;
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -14828,8 +14909,7 @@
|
|||||||
btnDisplay = lang.sLast;
|
btnDisplay = lang.sLast;
|
||||||
|
|
||||||
if ( pages === 0 || page === pages-1 ) {
|
if ( pages === 0 || page === pages-1 ) {
|
||||||
tabIndex = -1;
|
disabled = true;
|
||||||
btnClass += ' ' + disabledClass;
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -14842,18 +14922,20 @@
|
|||||||
|
|
||||||
if ( btnDisplay !== null ) {
|
if ( btnDisplay !== null ) {
|
||||||
var tag = settings.oInit.pagingTag || 'a';
|
var tag = settings.oInit.pagingTag || 'a';
|
||||||
var disabled = btnClass.indexOf(disabledClass) !== -1;
|
|
||||||
|
if (disabled) {
|
||||||
|
btnClass += ' ' + disabledClass;
|
||||||
|
}
|
||||||
|
|
||||||
node = $('<'+tag+'>', {
|
node = $('<'+tag+'>', {
|
||||||
'class': classes.sPageButton+' '+btnClass,
|
'class': classes.sPageButton+' '+btnClass,
|
||||||
'aria-controls': settings.sTableId,
|
'aria-controls': settings.sTableId,
|
||||||
'aria-disabled': disabled ? 'true' : null,
|
'aria-disabled': disabled ? 'true' : null,
|
||||||
'aria-label': aria[ button ],
|
'aria-label': aria[ button ],
|
||||||
'aria-role': 'link',
|
'role': 'link',
|
||||||
'aria-current': btnClass === classes.sPageButtonActive ? 'page' : null,
|
'aria-current': btnClass === classes.sPageButtonActive ? 'page' : null,
|
||||||
'data-dt-idx': button,
|
'data-dt-idx': button,
|
||||||
'tabindex': tabIndex,
|
'tabindex': disabled ? -1 : settings.iTabIndex,
|
||||||
'id': idx === 0 && typeof button === 'string' ?
|
'id': idx === 0 && typeof button === 'string' ?
|
||||||
settings.sTableId +'_'+ button :
|
settings.sTableId +'_'+ button :
|
||||||
null
|
null
|
||||||
@@ -14984,7 +15066,7 @@
|
|||||||
return -Infinity;
|
return -Infinity;
|
||||||
}
|
}
|
||||||
|
|
||||||
let type = typeof d;
|
var type = typeof d;
|
||||||
|
|
||||||
if (type === 'number' || type === 'bigint') {
|
if (type === 'number' || type === 'bigint') {
|
||||||
return d;
|
return d;
|
||||||
@@ -15358,7 +15440,7 @@
|
|||||||
var __thousands = ',';
|
var __thousands = ',';
|
||||||
var __decimal = '.';
|
var __decimal = '.';
|
||||||
|
|
||||||
if (Intl) {
|
if (window.Intl !== undefined) {
|
||||||
try {
|
try {
|
||||||
var num = new Intl.NumberFormat().formatToParts(100000.1);
|
var num = new Intl.NumberFormat().formatToParts(100000.1);
|
||||||
|
|
||||||
@@ -15654,25 +15736,33 @@
|
|||||||
}
|
}
|
||||||
else if ( typeof exports === 'object' ) {
|
else if ( typeof exports === 'object' ) {
|
||||||
// CommonJS
|
// CommonJS
|
||||||
module.exports = function (root, $) {
|
var jq = require('jquery');
|
||||||
if ( ! root ) {
|
var cjsRequires = function (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 );
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( ! $.fn.dataTable ) {
|
if ( ! $.fn.dataTable ) {
|
||||||
require('datatables.net')(root, $);
|
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 {
|
else {
|
||||||
// Browser
|
// Browser
|
||||||
@@ -15791,10 +15881,10 @@ DataTable.ext.renderer.pageButton.bootstrap = function ( settings, host, idx, bu
|
|||||||
'aria-controls': settings.sTableId,
|
'aria-controls': settings.sTableId,
|
||||||
'aria-disabled': disabled ? 'true' : null,
|
'aria-disabled': disabled ? 'true' : null,
|
||||||
'aria-label': aria[ button ],
|
'aria-label': aria[ button ],
|
||||||
'aria-role': 'link',
|
'role': 'link',
|
||||||
'aria-current': btnClass === 'active' ? 'page' : null,
|
'aria-current': btnClass === 'active' ? 'page' : null,
|
||||||
'data-dt-idx': button,
|
'data-dt-idx': button,
|
||||||
'tabindex': settings.iTabIndex,
|
'tabindex': disabled ? -1 : settings.iTabIndex,
|
||||||
'class': 'page-link'
|
'class': 'page-link'
|
||||||
} )
|
} )
|
||||||
.html( btnDisplay )
|
.html( btnDisplay )
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en" data-bs-theme="auto">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
@@ -10,17 +10,17 @@
|
|||||||
<link rel="stylesheet" href="{{urlpath}}/vw_static/admin.css" />
|
<link rel="stylesheet" href="{{urlpath}}/vw_static/admin.css" />
|
||||||
<script src="{{urlpath}}/vw_static/admin.js"></script>
|
<script src="{{urlpath}}/vw_static/admin.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-light">
|
<body>
|
||||||
<nav class="navbar navbar-expand-md navbar-dark bg-dark mb-4 shadow fixed-top">
|
<nav class="navbar navbar-expand-md navbar-dark bg-dark mb-4 shadow fixed-top">
|
||||||
<div class="container-xl">
|
<div class="container-xl">
|
||||||
<a class="navbar-brand" href="{{urlpath}}/admin"><img class="vaultwarden-icon" src="{{urlpath}}/vw_static/vaultwarden-icon.png" alt="V">aultwarden Admin</a>
|
<a class="navbar-brand" href="{{urlpath}}/admin"><img class="vaultwarden-icon" src="{{urlpath}}/vw_static/vaultwarden-icon.png" alt="V">aultwarden Admin</a>
|
||||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarCollapse"
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarCollapse"
|
||||||
aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
|
aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
<div class="collapse navbar-collapse" id="navbarCollapse">
|
<div class="collapse navbar-collapse" id="navbarCollapse">
|
||||||
<ul class="navbar-nav me-auto">
|
<ul class="navbar-nav me-auto">
|
||||||
{{#if logged_in}}
|
{{#if logged_in}}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{{urlpath}}/admin">Settings</a>
|
<a class="nav-link" href="{{urlpath}}/admin">Settings</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -33,15 +33,59 @@
|
|||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{{urlpath}}/admin/diagnostics">Diagnostics</a>
|
<a class="nav-link" href="{{urlpath}}/admin/diagnostics">Diagnostics</a>
|
||||||
</li>
|
</li>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{{urlpath}}/" target="_blank" rel="noreferrer">Vault</a>
|
<a class="nav-link" href="{{urlpath}}/" target="_blank" rel="noreferrer">Vault</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<button
|
||||||
|
class="btn btn-link nav-link py-0 px-0 px-md-2 dropdown-toggle d-flex align-items-center"
|
||||||
|
id="bd-theme" type="button" aria-expanded="false" data-bs-toggle="dropdown"
|
||||||
|
data-bs-display="static" aria-label="Toggle theme (auto)">
|
||||||
|
<span class="my-1 fs-4 theme-icon-active">
|
||||||
|
<use>☯</use>
|
||||||
|
</span>
|
||||||
|
<span class="d-md-none ms-2" id="bd-theme-text">Toggle theme</span>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="bd-theme-text">
|
||||||
|
<li>
|
||||||
|
<button type="button" class="dropdown-item d-flex align-items-center"
|
||||||
|
data-bs-theme-value="light" aria-pressed="false">
|
||||||
|
<span class="me-2 fs-4 theme-icon">
|
||||||
|
<use>☀</use>
|
||||||
|
</span>
|
||||||
|
Light
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button type="button" class="dropdown-item d-flex align-items-center"
|
||||||
|
data-bs-theme-value="dark" aria-pressed="false">
|
||||||
|
<span class="me-2 fs-4 theme-icon">
|
||||||
|
<use>★</use>
|
||||||
|
</span>
|
||||||
|
Dark
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button type="button" class="dropdown-item d-flex align-items-center active"
|
||||||
|
data-bs-theme-value="auto" aria-pressed="true">
|
||||||
|
<span class="me-2 fs-4 theme-icon">
|
||||||
|
<use>☯</use>
|
||||||
|
</span>
|
||||||
|
Auto
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
{{#if logged_in}}
|
{{#if logged_in}}
|
||||||
<a class="btn btn-sm btn-secondary" href="{{urlpath}}/admin/logout">Log Out</a>
|
<a class="btn btn-sm btn-secondary" href="{{urlpath}}/admin/logout">Log Out</a>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -49,6 +93,6 @@
|
|||||||
{{> (lookup this "page_content") }}
|
{{> (lookup this "page_content") }}
|
||||||
|
|
||||||
<!-- This script needs to be at the bottom, else it will fail! -->
|
<!-- This script needs to be at the bottom, else it will fail! -->
|
||||||
<script src="{{urlpath}}/vw_static/bootstrap-native.js"></script>
|
<script src="{{urlpath}}/vw_static/bootstrap.bundle.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<main class="container-xl">
|
<main class="container-xl">
|
||||||
<div id="diagnostics-block" class="my-3 p-3 bg-white rounded shadow">
|
<div id="diagnostics-block" class="my-3 p-3 rounded shadow">
|
||||||
<h6 class="border-bottom pb-2 mb-2">Diagnostics</h6>
|
<h6 class="border-bottom pb-2 mb-2">Diagnostics</h6>
|
||||||
|
|
||||||
<h3>Versions</h3>
|
<h3>Versions</h3>
|
||||||
@@ -8,8 +8,8 @@
|
|||||||
<dl class="row">
|
<dl class="row">
|
||||||
<dt class="col-sm-5">Server Installed
|
<dt class="col-sm-5">Server Installed
|
||||||
<span class="badge bg-success d-none" id="server-success" title="Latest version is installed.">Ok</span>
|
<span class="badge bg-success d-none" id="server-success" title="Latest version is installed.">Ok</span>
|
||||||
<span class="badge bg-warning d-none" id="server-warning" title="There seems to be an update available.">Update</span>
|
<span class="badge bg-warning text-dark d-none" id="server-warning" title="There seems to be an update available.">Update</span>
|
||||||
<span class="badge bg-info d-none" id="server-branch" title="This is a branched version.">Branched</span>
|
<span class="badge bg-info text-dark d-none" id="server-branch" title="This is a branched version.">Branched</span>
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="col-sm-7">
|
<dd class="col-sm-7">
|
||||||
<span id="server-installed">{{page_data.current_release}}</span>
|
<span id="server-installed">{{page_data.current_release}}</span>
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
<main class="container-xl">
|
<main class="container-xl">
|
||||||
{{#if error}}
|
{{#if error}}
|
||||||
<div class="align-items-center p-3 mb-3 text-white-50 bg-warning rounded shadow">
|
<div class="align-items-center p-3 mb-3 text-opacity-50 text-dark bg-warning rounded shadow">
|
||||||
<div>
|
<div>
|
||||||
<h6 class="mb-0 text-white">{{error}}</h6>
|
<h6 class="mb-0 text-dark">{{error}}</h6>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
<div class="align-items-center p-3 mb-3 text-white-50 bg-danger rounded shadow">
|
<div class="align-items-center p-3 mb-3 text-opacity-75 text-light bg-danger rounded shadow">
|
||||||
<div>
|
<div>
|
||||||
<h6 class="mb-0 text-white">Authentication key needed to continue</h6>
|
<h6 class="mb-0 text-light">Authentication key needed to continue</h6>
|
||||||
<small>Please provide it below:</small>
|
<small>Please provide it below:</small>
|
||||||
|
|
||||||
<form class="form-inline" method="post" action="{{urlpath}}/admin">
|
<form class="form-inline" method="post" action="{{urlpath}}/admin">
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
{{#if redirect}}
|
{{#if redirect}}
|
||||||
<input type="hidden" id="redirect" name="redirect" value="/{{redirect}}">
|
<input type="hidden" id="redirect" name="redirect" value="/{{redirect}}">
|
||||||
{{/if}}
|
{{/if}}
|
||||||
<button type="submit" class="btn btn-primary">Enter</button>
|
<button type="submit" class="btn btn-primary mt-2">Enter</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<main class="container-xl">
|
<main class="container-xl">
|
||||||
<div id="organizations-block" class="my-3 p-3 bg-white rounded shadow">
|
<div id="organizations-block" class="my-3 p-3 rounded shadow">
|
||||||
<h6 class="border-bottom pb-2 mb-3">Organizations</h6>
|
<h6 class="border-bottom pb-2 mb-3">Organizations</h6>
|
||||||
<div class="table-responsive-xl small">
|
<div class="table-responsive-xl small">
|
||||||
<table id="orgs-table" class="table table-sm table-striped table-hover">
|
<table id="orgs-table" class="table table-sm table-striped table-hover">
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th class="vw-org-details">Organization</th>
|
<th class="vw-org-details">Organization</th>
|
||||||
<th class="vw-users">Users</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-attachments">Attachments</th>
|
||||||
<th class="vw-misc">Misc</th>
|
<th class="vw-misc">Misc</th>
|
||||||
<th class="vw-actions">Actions</th>
|
<th class="vw-actions">Actions</th>
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<link rel="stylesheet" href="{{urlpath}}/vw_static/datatables.css" />
|
<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.7.0.slim.js"></script>
|
||||||
<script src="{{urlpath}}/vw_static/datatables.js"></script>
|
<script src="{{urlpath}}/vw_static/datatables.js"></script>
|
||||||
<script src="{{urlpath}}/vw_static/admin_organizations.js"></script>
|
<script src="{{urlpath}}/vw_static/admin_organizations.js"></script>
|
||||||
<script src="{{urlpath}}/vw_static/jdenticon.js"></script>
|
<script src="{{urlpath}}/vw_static/jdenticon.js"></script>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user