Compare commits

...

92 Commits

Author SHA1 Message Date
Mathijs van Veluw
1c7338c7c4 Merge pull request #3659 from BlackDex/fix-org-creation
Fix org creation regresion
2023-07-06 10:39:59 +02:00
BlackDex
08f37b9935 Fix org creation regresion
A previous PR added a field which isn't there on the initial creation of
an org. This PR fixes that.
2023-07-06 10:14:04 +02:00
Daniel García
4826ddca4c Merge pull request #3651 from tessus/fix/branch-on-HEAD
fix version when compiled at a specific commit
2023-07-05 18:45:08 +02:00
Helmut K. C. Tessarek
2b32b6f78c fix version when compiled at a specific commit
When a specific commit is checked out from the main branch, the vaultwarden
version is reported as `vaultwarden x.y.z-githash (HEAD)`.
This is a problem, because the admin interface reports this as a version from
a branch called HEAD, while in reality the commit was from the main branch.
2023-07-04 18:08:52 -04:00
Daniel García
a6cfdddfd8 Merge pull request #3649 from BlackDex/update-crates
Update crates and small clippy fix
2023-07-04 20:56:05 +02:00
Daniel García
814ce9a6ac Merge pull request #3632 from sirux88/fix-reset-password-check-issue
fix missing password check while manual reset password enrollment
2023-07-04 20:55:34 +02:00
Daniel García
1bee46f64b Merge pull request #3623 from fashberg/main
Added-External_id for Collections
2023-07-04 20:54:36 +02:00
Daniel García
556d945396 Merge pull request #3620 from DenuxPlays/main
Updated docker run command
2023-07-04 20:54:05 +02:00
Daniel García
664b480c71 Merge pull request #3609 from farodin91/add-user-to-collection-during-creation
add user to collection during creation
2023-07-04 20:53:46 +02:00
Jan Jansen
84e901b7d2 add user to collection during creation
Signed-off-by: Jan Jansen <jan.jansen@gdata.de>
2023-07-04 20:27:37 +02:00
Folke Ashberg
839b2bc950 fix format error 2023-07-04 20:26:03 +02:00
Folke Ashberg
6050c8dac5 Added-External_id for Collections 2023-07-04 20:26:03 +02:00
BlackDex
0a6b797e6e Update crates and small clippy fix
- Update all crates
- Remove async which is reported by clippy in v1.72.0
2023-07-04 20:12:50 +02:00
sirux88
fb6f441a4f fixed unnecessary variable usage 2023-07-04 18:57:49 +02:00
sirux88
9876aedd67 added password check for manual reset
password enrollment endpoint
2023-07-04 18:57:49 +02:00
Daniel García
19e671ff25 Fix dataurl parse panic when icon is malformed 2023-07-03 20:20:26 +02:00
Daniel García
60964c07e6 Add some extra access checks for attachments and groups 2023-07-03 19:58:14 +02:00
Timon Klinkert
e4894524e4 updated docker run command 2023-06-26 00:31:40 +02:00
Daniel García
e7f083dee9 Merge pull request #3593 from GeekCornerGH/feature/store-passkeys-in-the-vault
feat: Support for storing passkeys in the vault
2023-06-22 19:06:55 +02:00
GeekCornerGH
1074315a87 feat: Support for storing passkeys in the vault 2023-06-22 18:48:13 +02:00
Daniel García
c56bf38079 Merge pull request #3608 from BlackDex/fix-issue-3607
Fix send access regression
2023-06-22 17:58:15 +02:00
BlackDex
3c0cac623d Fix send access regression
In a previous commit push notifications for mobile were added, but this
introduced a header guard which caused issues with anonymous endpoints.

This PR fixes this by using a uuid with only 0's.

Fixes #3607
2023-06-22 16:40:26 +02:00
Mathijs van Veluw
550794b127 Merge pull request #3606 from farodin91/add-group-import-on-invite
Add group import on invite
2023-06-22 11:57:49 +02:00
Jan Jansen
e818a0bf37 Add group import on invite
Fixes #3599

Signed-off-by: Jan Jansen <jan.jansen@gdata.de>
2023-06-22 11:10:43 +02:00
Daniel García
2aedff50e8 Merge pull request #3603 from BlackDex/update-crates-and-workflows
Update crates and workflow
2023-06-21 23:29:15 +02:00
BlackDex
84a23008f4 Update crates and workflow
- Updated all the crates
- Updated workflow actions
- Set cargo registry to sparse
2023-06-21 22:01:05 +02:00
Mathijs van Veluw
44e9e1a58e Merge pull request #3578 from quexten/fix/mobile-push-to-empty-uuid
Add mobile push device filter to non-null push uuid
2023-06-18 17:40:10 +02:00
Bernd Schoolmann
e4606431d1 Fix mobile push blocking requests and spamming push server 2023-06-16 23:34:16 +02:00
Mathijs van Veluw
5b7d7390b0 Merge pull request #3568 from BlackDex/org-api-key-refresh
Implement the Organization API Key support for the new Directory Connector v2022
2023-06-13 09:05:22 +02:00
BlackDex
a05187c0ff Some code changes and optimizations
Some cleanups and optimizations done on the code generated by @Kurnihil
2023-06-13 08:51:07 +02:00
BlackDex
8e34495e73 Merge and modify PR from @Kurnihil
Merging a PR from @Kurnihil into the already rebased branch.
Made some small changes to make it work with newer changes.

Some finetuning is probably still needed.

Co-authored-by: Daniele Andrei <daniele.andrei@geo-satis.com>
Co-authored-by: Kurnihil
2023-06-13 08:51:07 +02:00
BlackDex
4219249e11 Add support for Organization token
This is a WIP for adding organization token login support.
It has basic token login and verification support, but that's about it.

This branch is a refresh of the previous version, and will contain code
from a PR based upon my previous branch.
2023-06-13 08:48:18 +02:00
Mathijs van Veluw
bd883de70e Merge pull request #3304 from GeekCornerGH/feature/push-notifications
feat: Implement Push Notifications sync
2023-06-12 23:45:03 +02:00
GeekCornerGH
2d66292350 feat: Push Notifications
Co-authored-by: samb-devel <125741162+samb-devel@users.noreply.github.com>
Co-authored-by: Zoruk <Zoruk@users.noreply.github.com>
2023-06-11 13:28:18 +02:00
Mathijs van Veluw
adf67a8ee8 Merge pull request #3563 from tessus/update/rust-and-crates
Update Rust and Crates
2023-06-04 22:39:40 +02:00
Helmut K. C. Tessarek
f40f5b8399 update web-vault to v2023.5.0 2023-06-04 16:15:10 -04:00
Helmut K. C. Tessarek
2d6ca0ea95 Update a few more crates 2023-06-04 16:14:51 -04:00
Helmut K. C. Tessarek
06a10e2c5a Update Rust and Crates 2023-06-03 17:04:45 -04:00
Mathijs van Veluw
445680fb84 Merge pull request #3546 from BlackDex/GH-3534
Fix collection change ws notifications
2023-05-26 18:03:45 +02:00
BlackDex
83376544d8 Fix collection change ws notifications
When chaning a collection this did not got notified via WebSockets.
This PR adds this feature and resolves #3534
2023-05-26 17:42:00 +02:00
Mathijs van Veluw
04a17dcdef Merge pull request #3548 from BlackDex/update-crates
Update crates and GH Workflow
2023-05-26 17:41:03 +02:00
BlackDex
0851561392 Update crates and GH Workflow
- Updated crates
- Updated GHA where needed
2023-05-26 17:26:09 +02:00
Mathijs van Veluw
95cd6deda6 Merge pull request #3547 from BlackDex/GH-3540
Prevent 401 on main admin page
2023-05-26 17:25:48 +02:00
BlackDex
636f16dc66 Prevent 401 on main admin page
When you are not loggedin, and have no cookie etc.. we always returned a 401.
This was mainly to allow the login page on all the sub pages, and after
login being redirected to the requested page, for these pages a 401 is a
valid response, since, you do not have access.

But for the main `/admin` page, it should just respond with a `200` and
show the login page.

This PR fixes this flow and response. It should prevent people using
Fail2ban, or other tools being triggered by only accessing the login page.

Resolves #3540
2023-05-25 23:40:36 +02:00
Mathijs van Veluw
9e5b049dca Merge pull request #3532 from jjlin/global-domains
Sync global_domains.json (Pinterest)
2023-05-17 21:20:48 +02:00
Jeremy Lin
23aa9088f3 Sync global_domains.json to bitwarden/server@8dda73a (Pinterest) 2023-05-17 12:04:31 -07:00
Mathijs van Veluw
4f0ed06b06 Merge pull request #3522 from stefan0xC/update-to-v2023.4.2
update web-vault to v2023.4.2
2023-05-12 09:48:56 +02:00
Stefan Melmuk
349c97efaf update crates 2023-05-12 09:31:29 +02:00
Stefan Melmuk
8b05a5d192 update web-vault to v2023.4.2 2023-05-12 08:05:35 +02:00
Mathijs van Veluw
83bf77d713 Merge pull request #3513 from stefan0xC/fix-empty-policy
policy data should be `null` not an empty object
2023-05-09 12:00:10 +02:00
Stefan Melmuk
4d5c047ddc policy data should be null not an empty object 2023-05-09 11:14:46 +02:00
Daniel García
147c9c7b50 Merge pull request #3505 from gitouche-sur-osm/dockerfile-fqin
Use fully qualified image names in Dockerfile
2023-05-08 21:00:30 +02:00
Daniel García
6515a2fcad Merge pull request #3502 from BlackDex/fix-trailing-slash
Use Rocket `v0.5` branch to fix endpoints
2023-05-08 21:00:19 +02:00
BlackDex
4a2ed553df Use Rocket v0.5 branch to fix endpoints
There now is a `v0.5` branch which will be the final release version
when the time is there. Switched to this instead of the `master` branch
which contains other fixes and enhancements as well (for `v0.6`).

This should solve all the endpoint issue we were having.
2023-05-06 19:46:55 +02:00
Gitouche
ba492c0602 Use fully qualified image names in Dockerfile 2023-05-03 18:31:28 +02:00
Daniel García
1ec049e2b5 Update web vault to v2023.4.0 2023-05-01 19:49:48 +02:00
Daniel García
0fb8563b13 Merge pull request #3491 from BlackDex/rocket_changes
Change `String` to `&str` for all Rocket functions and some other fixes
2023-04-30 23:53:15 +02:00
BlackDex
f906f6230a Change String to &str for all Rocket functions
During setting the latest commit hash for Rocket and updating all the
other crates, there were some messages regarding the usage of `String`
for the Rocket endpoint function calls. I acted upon this message and
changed all `String` types to `&str` and modified the code where needed.

This ended up in less alloc calls, and probably also a bit less memory usage.

- Updated all the crates and commit hashes
- Modified all `String` to `&str` where applicable
2023-04-30 17:18:12 +02:00
BlackDex
951ba55123 Prevent some ::_ logs from outputting 2023-04-30 17:17:43 +02:00
BlackDex
18abf226be Fix admin post endpoints 2023-04-30 17:09:42 +02:00
Daniel García
393645617e Merge pull request #3469 from BlackDex/rust-and-crate-updates
Update Rust and Crates
2023-04-24 18:54:35 +02:00
Daniel García
5bf243b675 Merge pull request #3475 from vilgotf/inline-statics
inline static rsa keys
2023-04-24 18:54:19 +02:00
BlackDex
cfba8347a3 Update Rust and Crates
- Updated Rust to v1.69.0
- Updated MSRV to v1.67.1
- Updated crates
- Updated GitHub Actions
2023-04-24 14:10:58 +02:00
Tim Vilgot Mikael Fredenberg
55c1b6e8d5 inline static rsa keys 2023-04-23 21:34:26 +02:00
Daniel García
3d7e80a7aa Merge pull request #3440 from BlackDex/switch-ws-to-streams
Small update to Rocket WebSockets
2023-04-17 20:26:03 +02:00
Daniel García
5866338de4 Merge pull request #3439 from kennymc-c/main
Fixed missing footer_text and a few inconsistencies in email templates
2023-04-17 20:25:36 +02:00
kennymc-c
271e3ae757 Changed permissions back to 644 2023-04-12 18:06:46 +02:00
BlackDex
48cc31a59f Small update to Rocket WebSockets
Switched from channels to stream. This is able to use yield, and the
code looks a bit nicer this way.

Also updated all the crates.
2023-04-12 15:59:05 +02:00
kennymc-c
6a7cee4e7e Fixed footer to footer_text 2023-04-11 22:00:10 +02:00
kennymc-c
f850dbb310 Fixed some missing footer_text partials and a few inconsistencies between plain text and html email templates 2023-04-11 21:27:38 +02:00
Daniel García
07099df41a Merge pull request #3436 from BlackDex/fix-admin-base-url
Several config and admin interface fixes
2023-04-10 21:11:44 +02:00
Daniel García
0c0a80720e Merge pull request #3404 from BlackDex/websockets-via-rocket
WebSockets via Rocket's Upgrade connection
2023-04-10 21:10:29 +02:00
BlackDex
ae437f70a3 Several config and admin interface fixes
- Fixed issue with domains starting with `admin`
- Fixed issue with DUO not being enabled globally anymore (regression)
- Renamed `Ciphers` to `Entries` in overview
- Improved `ADMIN_TOKEN` description
- Updated jquery-slim and datatables

Resolves #3382
Resolves #3415
Resolves discussion on #3288
2023-04-10 20:39:51 +02:00
BlackDex
3d11f4cd16 WebSockets via Rocket's Upgrade connection
This PR implements a (not yet fully released) new feature of Rocket which allows WebSockets/Upgrade connections.
No more need for multiple ports to be opened for Vaultwarden.
No explicit need for a reverse proxy to get WebSockets to work (Although I still suggest to use a reverse proxy).

- Using a git revision for Rocket, since `rocket_ws` is not yet released.
- Updated other crates as well.
- Added a connection guard to clear the WS connection from the Users list.

Fixes #685
Fixes #2917
Fixes #1424
2023-04-10 16:58:58 +02:00
Daniel García
3bd4e42fb0 Merge pull request #3427 from stefan0xC/check-if-policies-enabled
check if reset password policy is enabled
2023-04-09 19:02:27 +02:00
Stefan Melmuk
89e94b1d91 check if reset policy is enabled 2023-04-06 22:34:05 +02:00
Daniel García
0b28ab3be1 Merge pull request #3403 from BlackDex/update-dockerfile-and-rust
Revert setcap, update rust and crates
2023-04-02 15:39:36 +02:00
Daniel García
c5bcc340fa Merge pull request #3405 from BlackDex/fix-multiple-websocket-messages
Fix sending out multiple websocket notifications
2023-04-02 15:24:00 +02:00
BlackDex
bff54fbfdb Fix sending out multiple websocket notifications
For some reason I encountered a strange bug which resulted in sending
out multiple websocket notifications for the exact same user.

Added a `distinct()` for the query to filter out multiple uuid's.
2023-04-02 15:23:36 +02:00
Daniel García
867c6ba056 Merge pull request #3398 from stefan0xC/dont-expect-kdf-memory-or-parallelism
always return KdfMemory and KdfParallelism
2023-04-02 15:22:42 +02:00
Daniel García
d1ecf03f44 Merge pull request #3397 from nikolaevn/feature/add-admin-reinvite-endpoint
support `/users/<uuid>/invite/resend` admin api
2023-04-02 15:21:51 +02:00
BlackDex
fc43608eec Revert setcap, update rust and crates
- Revert #3170 as discussed in #3387
  In hindsight it's better to not have this feature
- Update Dockerfile.j2 for easy version changes.
  Just change it in one place instead of multiple
- Updated to Rust to latest patched version
- Updated crates to latest available
- Pinned mimalloc to an older version, as it breaks on musl builds
2023-04-02 15:19:59 +02:00
Daniel García
15dd05c78d Merge pull request #3390 from BlackDex/fix-abort-pw-reset-on-mail-error
Fix abort on pw reset mail error
2023-04-02 15:19:53 +02:00
Nikolay Nikolaev
aa6f774f65 add check user state 2023-03-31 14:03:37 +03:00
Nikolay Nikolaev
379f885354 add mail check 2023-03-31 13:00:57 +03:00
Stefan Melmuk
39a5f2dbe8 clear kdf memory and parallelism with pbkdf2
when changing back from argon2id to PBKDF2 the unused parameters
should be set to 0.

also fix small bug in _register
2023-03-31 07:31:40 +02:00
Stefan Melmuk
0daaa9b175 always return KdfMemory and KdfParallelism
the client will ignore the value of theses fields in case of `PBKDF2`
(whether they are unset or left from trying out `Argon2id` as KDF).

with `Argon2id` those fields should never be `null` but always in a
valid state. if they are `null` (how would that even happen?) the
client still assumes default values for `Argon2id` (i.e. m=64 and p=4)
and if they are set to something else login will fail anyway.
2023-03-31 01:10:28 +02:00
Nikolay Nikolaev
0c085d21ce fmt 2023-03-30 16:04:35 +03:00
Nikolay Nikolaev
dcaaa430f0 support /users/<uuid>/invite/resend admin api 2023-03-30 15:23:16 +03:00
BlackDex
2cda54ceff Fix password reset issues
There was used a wrong macro to produce an error message when mailing
the user his password was reset failed. It was using `error!()` which
does not return an `Err` and aborts the rest of the code.

This resulted in the users password still being resetted, but not being
notified. This PR fixes this by using `err!()`. Also, do not set the
user object as mutable until it really is needed.

Second, when a user was using the new Argon2id KDF with custom values
like memory and parallelism, that would have rendered the password
incorrect. The endpoint which should return all the data did not
returned all the new Argon2id values.

Fixes #3388

Co-authored-by: Stefan Melmuk <509385+stefan0xC@users.noreply.github.com>
2023-03-30 09:41:13 +02:00
Daniel García
525e6bb65a Merge pull request #3376 from jjlin/knowndevices-nopad
Decode knowndevice `X-Request-Email` as base64url with no padding
2023-03-27 09:32:25 +02:00
Jeremy Lin
62cebebd3d Decode knowndevice X-Request-Email as base64url with no padding
The clients end up removing the padding characters [1][2].

[1] https://github.com/bitwarden/clients/blob/web-v2023.3.0/libs/common/src/misc/utils.ts#L141-L143
[2] https://github.com/bitwarden/mobile/blob/v2023.3.1/src/Core/Utilities/CoreHelpers.cs#L227-L234
2023-03-27 00:03:54 -07:00
95 changed files with 2997 additions and 1737 deletions

View File

@@ -72,6 +72,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_BASE_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 +271,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

View File

@@ -30,7 +30,7 @@ jobs:
# 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,7 +43,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
# End Checkout the repo # End Checkout the repo
@@ -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,7 @@ jobs:
# Enable Rust Caching # Enable Rust Caching
- uses: Swatinem/rust-cache@6fd3edff6979b79f87531400ad694fb7f2c84b1f # v2.2.1 - uses: Swatinem/rust-cache@2656b87321093db1cb55fbd73183d195214fdfd1 # v2.5.0
# End Enable Rust Caching # End Enable Rust Caching

View File

@@ -13,7 +13,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
# End Checkout the repo # End Checkout the repo

View File

@@ -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 }}

View File

@@ -7,3 +7,5 @@ ignored:
- DL3059 - DL3059
trustedRegistries: trustedRegistries:
- docker.io - docker.io
- ghcr.io
- quay.io

1194
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -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.68.2"
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.15"
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.0" # MessagePack library
# Concurrent HashMap used for WebSocket messaging and favicons # Concurrent HashMap used for WebSocket messaging and favicons
dashmap = "5.4.0" dashmap = "5.4.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.29.1", 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.166", features = ["derive"] }
serde_json = "1.0.94" serde_json = "1.0.99"
# 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.0", 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.22"
# 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,40 +113,40 @@ 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", "gzip", "brotli", "socks", "cookies", "trust-dns"] }
# Favicon extraction libraries # Favicon extraction libraries
html5gum = "0.5.2" html5gum = "0.5.3"
regex = { version = "1.7.3", features = ["std", "perf", "unicode-perl"], default-features = false } regex = { version = "1.8.4", 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.55"
# 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.13"
governor = "0.5.1" governor = "0.5.1"
# Check client versions for specific features. # Check client versions for specific features.
@@ -152,7 +154,7 @@ semver = "1.0.17"
# 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
@@ -161,6 +163,10 @@ argon2 = "0.5.0"
# 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]

View File

@@ -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.

View File

@@ -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}"))

View File

@@ -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.70.0" %}
{% set build_stage_base_image = "rust:1.68.1-bullseye" %} {% set debian_version = "bullseye" %}
{% 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" % 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" % 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" % 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" % 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.5.0" %}
{% set vault_image_digest = "sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee" %} {% set vault_image_digest = "sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085" %}
# 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
@@ -108,7 +110,6 @@ RUN dpkg --add-architecture {{ package_arch_name }} \
--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 +132,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 +174,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

View File

@@ -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.5.0
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b # $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2023.5.0
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee] # [docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085]
# #
# - 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:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085
# [vaultwarden/web-vault:v2023.3.0b] # [docker.io/vaultwarden/web-vault:v2023.5.0]
# #
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault FROM docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085 as vault
########################## BUILD IMAGE ########################## ########################## BUILD IMAGE ##########################
FROM rust:1.68.1-bullseye as build FROM docker.io/library/rust:1.70.0-bullseye as build
# Build time options to avoid dpkg warnings and help with reproducible builds. # Build time options to avoid dpkg warnings and help with reproducible builds.
ENV DEBIAN_FRONTEND=noninteractive \ ENV DEBIAN_FRONTEND=noninteractive \
@@ -45,7 +44,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 +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 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:bullseye-slim
ENV ROCKET_PROFILE="release" \ ENV ROCKET_PROFILE="release" \
ROCKET_ADDRESS=0.0.0.0 \ ROCKET_ADDRESS=0.0.0.0 \

View File

@@ -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.5.0
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b # $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2023.5.0
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee] # [docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085]
# #
# - 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:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085
# [vaultwarden/web-vault:v2023.3.0b] # [docker.io/vaultwarden/web-vault:v2023.5.0]
# #
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault FROM docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085 as vault
########################## BUILD IMAGE ########################## ########################## 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.70.0 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 \
@@ -74,11 +73,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 \

View File

@@ -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.5.0
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b # $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2023.5.0
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee] # [docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085]
# #
# - 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:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085
# [vaultwarden/web-vault:v2023.3.0b] # [docker.io/vaultwarden/web-vault:v2023.5.0]
# #
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault FROM docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085 as vault
########################## BUILD IMAGE ########################## ########################## BUILD IMAGE ##########################
FROM rust:1.68.1-bullseye as build FROM docker.io/library/rust:1.70.0-bullseye as build
# Build time options to avoid dpkg warnings and help with reproducible builds. # Build time options to avoid dpkg warnings and help with reproducible builds.
ENV DEBIAN_FRONTEND=noninteractive \ ENV DEBIAN_FRONTEND=noninteractive \
@@ -45,7 +44,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 +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 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:bullseye-slim
ENV ROCKET_PROFILE="release" \ ENV ROCKET_PROFILE="release" \
ROCKET_ADDRESS=0.0.0.0 \ ROCKET_ADDRESS=0.0.0.0 \

View File

@@ -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.5.0
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b # $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2023.5.0
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee] # [docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085]
# #
# - 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:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085
# [vaultwarden/web-vault:v2023.3.0b] # [docker.io/vaultwarden/web-vault:v2023.5.0]
# #
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault FROM docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085 as vault
########################## BUILD IMAGE ########################## ########################## 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.70.0 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 \
@@ -74,16 +73,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 \

View File

@@ -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.5.0
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b # $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2023.5.0
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee] # [docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085]
# #
# - 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:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085
# [vaultwarden/web-vault:v2023.3.0b] # [docker.io/vaultwarden/web-vault:v2023.5.0]
# #
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault FROM docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085 as vault
########################## BUILD IMAGE ########################## ########################## BUILD IMAGE ##########################
FROM rust:1.68.1-bullseye as build FROM docker.io/library/rust:1.70.0-bullseye as build
# Build time options to avoid dpkg warnings and help with reproducible builds. # Build time options to avoid dpkg warnings and help with reproducible builds.
ENV DEBIAN_FRONTEND=noninteractive \ ENV DEBIAN_FRONTEND=noninteractive \
@@ -48,7 +47,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 +96,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:bullseye
ENV ROCKET_PROFILE="release" \ ENV ROCKET_PROFILE="release" \
ROCKET_ADDRESS=0.0.0.0 \ ROCKET_ADDRESS=0.0.0.0 \

View File

@@ -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.5.0
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b # $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2023.5.0
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee] # [docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085]
# #
# - 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:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085
# [vaultwarden/web-vault:v2023.3.0b] # [docker.io/vaultwarden/web-vault:v2023.5.0]
# #
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault FROM docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085 as vault
########################## BUILD IMAGE ########################## ########################## BUILD IMAGE ##########################
FROM blackdex/rust-musl:aarch64-musl-stable-1.68.1 as build FROM docker.io/blackdex/rust-musl:aarch64-musl-stable-1.70.0 as build
# Build time options to avoid dpkg warnings and help with reproducible builds. # Build time options to avoid dpkg warnings and help with reproducible builds.
ENV DEBIAN_FRONTEND=noninteractive \ ENV DEBIAN_FRONTEND=noninteractive \
@@ -74,11 +73,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 \

View File

@@ -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.5.0
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b # $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2023.5.0
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee] # [docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085]
# #
# - 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:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085
# [vaultwarden/web-vault:v2023.3.0b] # [docker.io/vaultwarden/web-vault:v2023.5.0]
# #
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault FROM docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085 as vault
########################## BUILD IMAGE ########################## ########################## BUILD IMAGE ##########################
FROM rust:1.68.1-bullseye as build FROM docker.io/library/rust:1.70.0-bullseye as build
# Build time options to avoid dpkg warnings and help with reproducible builds. # Build time options to avoid dpkg warnings and help with reproducible builds.
ENV DEBIAN_FRONTEND=noninteractive \ ENV DEBIAN_FRONTEND=noninteractive \
@@ -48,7 +47,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,16 +96,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:bullseye
ENV ROCKET_PROFILE="release" \ ENV ROCKET_PROFILE="release" \
ROCKET_ADDRESS=0.0.0.0 \ ROCKET_ADDRESS=0.0.0.0 \

View File

@@ -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.5.0
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b # $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2023.5.0
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee] # [docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085]
# #
# - 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:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085
# [vaultwarden/web-vault:v2023.3.0b] # [docker.io/vaultwarden/web-vault:v2023.5.0]
# #
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault FROM docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085 as vault
########################## BUILD IMAGE ########################## ########################## BUILD IMAGE ##########################
FROM blackdex/rust-musl:aarch64-musl-stable-1.68.1 as build FROM docker.io/blackdex/rust-musl:aarch64-musl-stable-1.70.0 as build
# Build time options to avoid dpkg warnings and help with reproducible builds. # Build time options to avoid dpkg warnings and help with reproducible builds.
ENV DEBIAN_FRONTEND=noninteractive \ ENV DEBIAN_FRONTEND=noninteractive \
@@ -74,16 +73,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 \

View File

@@ -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.5.0
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b # $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2023.5.0
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee] # [docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085]
# #
# - 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:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085
# [vaultwarden/web-vault:v2023.3.0b] # [docker.io/vaultwarden/web-vault:v2023.5.0]
# #
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault FROM docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085 as vault
########################## BUILD IMAGE ########################## ########################## BUILD IMAGE ##########################
FROM rust:1.68.1-bullseye as build FROM docker.io/library/rust:1.70.0-bullseye as build
# Build time options to avoid dpkg warnings and help with reproducible builds. # Build time options to avoid dpkg warnings and help with reproducible builds.
ENV DEBIAN_FRONTEND=noninteractive \ ENV DEBIAN_FRONTEND=noninteractive \
@@ -48,7 +47,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 +96,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:bullseye
ENV ROCKET_PROFILE="release" \ ENV ROCKET_PROFILE="release" \
ROCKET_ADDRESS=0.0.0.0 \ ROCKET_ADDRESS=0.0.0.0 \

View File

@@ -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.5.0
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b # $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2023.5.0
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee] # [docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085]
# #
# - 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:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085
# [vaultwarden/web-vault:v2023.3.0b] # [docker.io/vaultwarden/web-vault:v2023.5.0]
# #
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault FROM docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085 as vault
########################## BUILD IMAGE ########################## ########################## BUILD IMAGE ##########################
FROM blackdex/rust-musl:arm-musleabi-stable-1.68.1 as build FROM docker.io/blackdex/rust-musl:arm-musleabi-stable-1.70.0 as build
# Build time options to avoid dpkg warnings and help with reproducible builds. # Build time options to avoid dpkg warnings and help with reproducible builds.
ENV DEBIAN_FRONTEND=noninteractive \ ENV DEBIAN_FRONTEND=noninteractive \
@@ -76,11 +75,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 \

View File

@@ -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.5.0
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b # $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2023.5.0
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee] # [docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085]
# #
# - 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:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085
# [vaultwarden/web-vault:v2023.3.0b] # [docker.io/vaultwarden/web-vault:v2023.5.0]
# #
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault FROM docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085 as vault
########################## BUILD IMAGE ########################## ########################## BUILD IMAGE ##########################
FROM rust:1.68.1-bullseye as build FROM docker.io/library/rust:1.70.0-bullseye as build
# Build time options to avoid dpkg warnings and help with reproducible builds. # Build time options to avoid dpkg warnings and help with reproducible builds.
ENV DEBIAN_FRONTEND=noninteractive \ ENV DEBIAN_FRONTEND=noninteractive \
@@ -48,7 +47,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,16 +96,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:bullseye
ENV ROCKET_PROFILE="release" \ ENV ROCKET_PROFILE="release" \
ROCKET_ADDRESS=0.0.0.0 \ ROCKET_ADDRESS=0.0.0.0 \

View File

@@ -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.5.0
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b # $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2023.5.0
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee] # [docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085]
# #
# - 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:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085
# [vaultwarden/web-vault:v2023.3.0b] # [docker.io/vaultwarden/web-vault:v2023.5.0]
# #
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault FROM docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085 as vault
########################## BUILD IMAGE ########################## ########################## BUILD IMAGE ##########################
FROM blackdex/rust-musl:arm-musleabi-stable-1.68.1 as build FROM docker.io/blackdex/rust-musl:arm-musleabi-stable-1.70.0 as build
# Build time options to avoid dpkg warnings and help with reproducible builds. # Build time options to avoid dpkg warnings and help with reproducible builds.
ENV DEBIAN_FRONTEND=noninteractive \ ENV DEBIAN_FRONTEND=noninteractive \
@@ -76,16 +75,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 \

View File

@@ -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.5.0
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b # $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2023.5.0
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee] # [docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085]
# #
# - 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:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085
# [vaultwarden/web-vault:v2023.3.0b] # [docker.io/vaultwarden/web-vault:v2023.5.0]
# #
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault FROM docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085 as vault
########################## BUILD IMAGE ########################## ########################## BUILD IMAGE ##########################
FROM rust:1.68.1-bullseye as build FROM docker.io/library/rust:1.70.0-bullseye as build
# Build time options to avoid dpkg warnings and help with reproducible builds. # Build time options to avoid dpkg warnings and help with reproducible builds.
ENV DEBIAN_FRONTEND=noninteractive \ ENV DEBIAN_FRONTEND=noninteractive \
@@ -48,7 +47,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 +96,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:bullseye
ENV ROCKET_PROFILE="release" \ ENV ROCKET_PROFILE="release" \
ROCKET_ADDRESS=0.0.0.0 \ ROCKET_ADDRESS=0.0.0.0 \

View File

@@ -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.5.0
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b # $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2023.5.0
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee] # [docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085]
# #
# - 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:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085
# [vaultwarden/web-vault:v2023.3.0b] # [docker.io/vaultwarden/web-vault:v2023.5.0]
# #
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault FROM docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085 as vault
########################## BUILD IMAGE ########################## ########################## BUILD IMAGE ##########################
FROM blackdex/rust-musl:armv7-musleabihf-stable-1.68.1 as build FROM docker.io/blackdex/rust-musl:armv7-musleabihf-stable-1.70.0 as build
# Build time options to avoid dpkg warnings and help with reproducible builds. # Build time options to avoid dpkg warnings and help with reproducible builds.
ENV DEBIAN_FRONTEND=noninteractive \ ENV DEBIAN_FRONTEND=noninteractive \
@@ -74,11 +73,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 \

View File

@@ -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.5.0
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b # $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2023.5.0
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee] # [docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085]
# #
# - 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:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085
# [vaultwarden/web-vault:v2023.3.0b] # [docker.io/vaultwarden/web-vault:v2023.5.0]
# #
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault FROM docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085 as vault
########################## BUILD IMAGE ########################## ########################## BUILD IMAGE ##########################
FROM rust:1.68.1-bullseye as build FROM docker.io/library/rust:1.70.0-bullseye as build
# Build time options to avoid dpkg warnings and help with reproducible builds. # Build time options to avoid dpkg warnings and help with reproducible builds.
ENV DEBIAN_FRONTEND=noninteractive \ ENV DEBIAN_FRONTEND=noninteractive \
@@ -48,7 +47,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,16 +96,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:bullseye
ENV ROCKET_PROFILE="release" \ ENV ROCKET_PROFILE="release" \
ROCKET_ADDRESS=0.0.0.0 \ ROCKET_ADDRESS=0.0.0.0 \

View File

@@ -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.5.0
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2023.3.0b # $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2023.5.0
# [vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee] # [docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085]
# #
# - 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:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085
# [vaultwarden/web-vault:v2023.3.0b] # [docker.io/vaultwarden/web-vault:v2023.5.0]
# #
FROM vaultwarden/web-vault@sha256:aa6ba791911a815ea570ec2ddc59992481c6ba8fbb65eed4f7074b463430d3ee as vault FROM docker.io/vaultwarden/web-vault@sha256:e5b5e99d132d50dc73176afb65f41cf3b834fb06bfa1d621ac16c705c3f10085 as vault
########################## BUILD IMAGE ########################## ########################## BUILD IMAGE ##########################
FROM blackdex/rust-musl:armv7-musleabihf-stable-1.68.1 as build FROM docker.io/blackdex/rust-musl:armv7-musleabihf-stable-1.70.0 as build
# Build time options to avoid dpkg warnings and help with reproducible builds. # Build time options to avoid dpkg warnings and help with reproducible builds.
ENV DEBIAN_FRONTEND=noninteractive \ ENV DEBIAN_FRONTEND=noninteractive \
@@ -74,16 +73,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 \

View File

@@ -0,0 +1 @@
ALTER TABLE devices ADD COLUMN push_uuid TEXT;

View File

@@ -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;

View File

@@ -0,0 +1 @@
ALTER TABLE collections ADD COLUMN external_id TEXT;

View File

@@ -0,0 +1 @@
ALTER TABLE devices ADD COLUMN push_uuid TEXT;

View File

@@ -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;

View File

@@ -0,0 +1 @@
ALTER TABLE collections ADD COLUMN external_id TEXT;

View File

@@ -0,0 +1 @@
ALTER TABLE devices ADD COLUMN push_uuid TEXT;

View File

@@ -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;

View File

@@ -0,0 +1 @@
ALTER TABLE collections ADD COLUMN external_id TEXT;

View File

@@ -1 +1 @@
1.68.1 1.70.0

View File

@@ -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() {

View File

@@ -4,7 +4,8 @@ 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, EmptyResult, JsonResult, JsonUpcase,
Notify, NumberOrString, PasswordData, UpdateType,
}, },
auth::{decode_delete, decode_invite, decode_verify_email, Headers}, auth::{decode_delete, decode_invite, decode_verify_email, Headers},
crypto, crypto,
@@ -35,6 +36,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 +48,9 @@ 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,
] ]
} }
@@ -133,7 +138,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 +174,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 +271,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 +394,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 +811,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 +879,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 +904,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 +935,64 @@ impl<'r> FromRequest<'r> for KnownDevice {
}) })
} }
} }
#[derive(Deserialize)]
#[allow(non_snake_case)]
struct PushToken {
PushToken: String,
}
#[post("/devices/identifier/<uuid>/token", data = "<data>")]
async fn post_device_token(uuid: &str, data: JsonUpcase<PushToken>, headers: Headers, conn: DbConn) -> EmptyResult {
put_device_token(uuid, data, headers, conn).await
}
#[put("/devices/identifier/<uuid>/token", data = "<data>")]
async fn put_device_token(uuid: &str, data: JsonUpcase<PushToken>, headers: Headers, mut conn: DbConn) -> EmptyResult {
if !CONFIG.push_enabled() {
return Ok(());
}
let data = data.into_inner().data;
let token = data.PushToken;
let mut device = match Device::find_by_uuid_and_user(&headers.device.uuid, &headers.user.uuid, &mut conn).await {
Some(device) => device,
None => err!(format!("Error: device {uuid} should be present before a token can be assigned")),
};
device.push_token = Some(token);
if device.push_uuid.is_none() {
device.push_uuid = Some(uuid::Uuid::new_v4().to_string());
}
if let Err(e) = device.save(&mut conn).await {
err!(format!("An error occured while trying to save the device push token: {e}"));
}
if let Err(e) = register_push_device(headers.user.uuid, device).await {
err!(format!("An error occured while proceeding registration of a device: {e}"));
}
Ok(())
}
#[put("/devices/identifier/<uuid>/clear-token")]
async fn put_clear_device_token(uuid: &str, mut conn: DbConn) -> EmptyResult {
// This only clears push token
// https://github.com/bitwarden/core/blob/master/src/Api/Controllers/DevicesController.cs#L109
// https://github.com/bitwarden/core/blob/master/src/Core/Services/Implementations/DeviceService.cs#L37
// This is somehow not implemented in any app, added it in case it is required
if !CONFIG.push_enabled() {
return Ok(());
}
if let Some(device) = Device::find_by_uuid(uuid, &mut conn).await {
Device::clear_push_token_by_uuid(uuid, &mut conn).await?;
unregister_push_device(device.uuid).await?;
}
Ok(())
}
// On upstream server, both PUT and POST are declared. Implementing the POST method in case it would be useful somewhere
#[post("/devices/identifier/<uuid>/clear-token")]
async fn post_clear_device_token(uuid: &str, conn: DbConn) -> EmptyResult {
put_clear_device_token(uuid, conn).await
}

View File

@@ -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,

View File

@@ -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
} }

View File

@@ -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());

View File

@@ -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(())
} }

View File

@@ -4,6 +4,7 @@ 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;
@@ -14,7 +15,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 +28,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 +57,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 +139,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
View File

@@ -0,0 +1,238 @@
use chrono::Utc;
use rocket::{
request::{self, FromRequest, Outcome},
Request, Route,
};
use std::collections::HashSet;
use crate::{
api::{EmptyResult, JsonUpcase},
auth,
db::{models::*, DbConn},
mail, CONFIG,
};
pub fn routes() -> Vec<Route> {
routes![ldap_import]
}
#[derive(Deserialize)]
#[allow(non_snake_case)]
struct OrgImportGroupData {
Name: String,
ExternalId: String,
MemberExternalIds: Vec<String>,
}
#[derive(Deserialize)]
#[allow(non_snake_case)]
struct OrgImportUserData {
Email: String,
ExternalId: String,
Deleted: bool,
}
#[derive(Deserialize)]
#[allow(non_snake_case)]
struct OrgImportData {
Groups: Vec<OrgImportGroupData>,
Members: Vec<OrgImportUserData>,
OverwriteExisting: bool,
// LargeImport: bool, // For now this will not be used, upstream uses this to prevent syncs of more then 2000 users or groups without the flag set.
}
#[post("/public/organization/import", data = "<data>")]
async fn ldap_import(data: JsonUpcase<OrgImportData>, token: PublicToken, mut conn: DbConn) -> EmptyResult {
// Most of the logic for this function can be found here
// https://github.com/bitwarden/server/blob/fd892b2ff4547648a276734fb2b14a8abae2c6f5/src/Core/Services/Implementations/OrganizationService.cs#L1797
let org_id = token.0;
let data = data.into_inner().data;
for user_data in &data.Members {
if user_data.Deleted {
// If user is marked for deletion and it exists, revoke it
if let Some(mut user_org) =
UserOrganization::find_by_email_and_org(&user_data.Email, &org_id, &mut conn).await
{
user_org.revoke();
user_org.save(&mut conn).await?;
}
// If user is part of the organization, restore it
} else if let Some(mut user_org) =
UserOrganization::find_by_email_and_org(&user_data.Email, &org_id, &mut conn).await
{
if user_org.status < UserOrgStatus::Revoked as i32 {
user_org.restore();
user_org.save(&mut conn).await?;
}
} else {
// If user is not part of the organization
let user = match User::find_by_mail(&user_data.Email, &mut conn).await {
Some(user) => user, // exists in vaultwarden
None => {
// doesn't exist in vaultwarden
let mut new_user = User::new(user_data.Email.clone());
new_user.set_external_id(Some(user_data.ExternalId.clone()));
new_user.save(&mut conn).await?;
if !CONFIG.mail_enabled() {
let invitation = Invitation::new(&new_user.email);
invitation.save(&mut conn).await?;
}
new_user
}
};
let user_org_status = if CONFIG.mail_enabled() {
UserOrgStatus::Invited as i32
} else {
UserOrgStatus::Accepted as i32 // Automatically mark user as accepted if no email invites
};
let mut new_org_user = UserOrganization::new(user.uuid.clone(), org_id.clone());
new_org_user.access_all = false;
new_org_user.atype = UserOrgType::User as i32;
new_org_user.status = user_org_status;
new_org_user.save(&mut conn).await?;
if CONFIG.mail_enabled() {
let (org_name, org_email) = match Organization::find_by_uuid(&org_id, &mut conn).await {
Some(org) => (org.name, org.billing_email),
None => err!("Error looking up organization"),
};
mail::send_invite(
&user_data.Email,
&user.uuid,
Some(org_id.clone()),
Some(new_org_user.uuid),
&org_name,
Some(org_email),
)
.await?;
}
}
}
if CONFIG.org_groups_enabled() {
for group_data in &data.Groups {
let group_uuid = match Group::find_by_external_id(&group_data.ExternalId, &mut conn).await {
Some(group) => group.uuid,
None => {
let mut group =
Group::new(org_id.clone(), group_data.Name.clone(), false, Some(group_data.ExternalId.clone()));
group.save(&mut conn).await?;
group.uuid
}
};
GroupUser::delete_all_by_group(&group_uuid, &mut conn).await?;
for ext_id in &group_data.MemberExternalIds {
if let Some(user) = User::find_by_external_id(ext_id, &mut conn).await {
if let Some(user_org) = UserOrganization::find_by_user_and_org(&user.uuid, &org_id, &mut conn).await
{
let mut group_user = GroupUser::new(group_uuid.clone(), user_org.uuid.clone());
group_user.save(&mut conn).await?;
}
}
}
}
} else {
warn!("Group support is disabled, groups will not be imported!");
}
// If this flag is enabled, any user that isn't provided in the Users list will be removed (by default they will be kept unless they have Deleted == true)
if data.OverwriteExisting {
// Generate a HashSet to quickly verify if a member is listed or not.
let sync_members: HashSet<String> = data.Members.into_iter().map(|m| m.ExternalId).collect();
for user_org in UserOrganization::find_by_org(&org_id, &mut conn).await {
if let Some(user_external_id) =
User::find_by_uuid(&user_org.user_uuid, &mut conn).await.map(|u| u.external_id)
{
if user_external_id.is_some() && !sync_members.contains(&user_external_id.unwrap()) {
if user_org.atype == UserOrgType::Owner && user_org.status == UserOrgStatus::Confirmed as i32 {
// Removing owner, check that there is at least one other confirmed owner
if UserOrganization::count_confirmed_by_org_and_type(&org_id, UserOrgType::Owner, &mut conn)
.await
<= 1
{
warn!("Can't delete the last owner");
continue;
}
}
user_org.delete(&mut conn).await?;
}
}
}
}
Ok(())
}
pub struct PublicToken(String);
#[rocket::async_trait]
impl<'r> FromRequest<'r> for PublicToken {
type Error = &'static str;
async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
let headers = request.headers();
// Get access_token
let access_token: &str = match headers.get_one("Authorization") {
Some(a) => match a.rsplit("Bearer ").next() {
Some(split) => split,
None => err_handler!("No access token provided"),
},
None => err_handler!("No access token provided"),
};
// Check JWT token is valid and get device and user from it
let claims = match auth::decode_api_org(access_token) {
Ok(claims) => claims,
Err(_) => err_handler!("Invalid claim"),
};
// Check if time is between claims.nbf and claims.exp
let time_now = Utc::now().naive_utc().timestamp();
if time_now < claims.nbf {
err_handler!("Token issued in the future");
}
if time_now > claims.exp {
err_handler!("Token expired");
}
// Check if claims.iss is host|claims.scope[0]
let host = match auth::Host::from_request(request).await {
Outcome::Success(host) => host,
_ => err_handler!("Error getting Host"),
};
let complete_host = format!("{}|{}", host.host, claims.scope[0]);
if complete_host != claims.iss {
err_handler!("Token not issued by this server");
}
// Check if claims.sub is org_api_key.uuid
// Check if claims.client_sub is org_api_key.org_uuid
let conn = match DbConn::from_request(request).await {
Outcome::Success(conn) => conn,
_ => err_handler!("Error getting DB"),
};
let org_uuid = match claims.client_id.strip_prefix("organization.") {
Some(uuid) => uuid,
None => err_handler!("Malformed client_id"),
};
let org_api_key = match OrganizationApiKey::find_by_org_uuid(org_uuid, &conn).await {
Some(org_api_key) => org_api_key,
None => err_handler!("Invalid client_id"),
};
if org_api_key.org_uuid != claims.client_sub {
err_handler!("Token not issued for this org");
}
if org_api_key.uuid != claims.sub {
err_handler!("Token not issued for this client");
}
Outcome::Success(PublicToken(claims.client_sub))
}
}

View File

@@ -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()))
} }

View File

@@ -97,15 +97,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 +114,7 @@ async fn icon_internal(domain: String) -> Cached<(ContentType, Vec<u8>)> {
); );
} }
match get_icon(&domain).await { match get_icon(domain).await {
Some((icon, icon_type)) => { 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)
} }
@@ -682,7 +682,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| {
@@ -891,6 +891,7 @@ impl Emitter for FaviconEmitter {
FaviconToken::EndTag(ref mut tag) => { FaviconToken::EndTag(ref mut tag) => {
// Always clean seen attributes // Always clean seen attributes
self.seen_attributes.clear(); self.seen_attributes.clear();
self.set_last_start_tag(None);
// Only trigger an emit for the </head> tag. // Only trigger an emit for the </head> tag.
// This is matched, and will break the for-loop. // This is matched, and will break the for-loop.

View File

@@ -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))
} }
@@ -260,6 +255,8 @@ 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,
@@ -269,13 +266,6 @@ async fn _password_login(
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 +276,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 +349,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 +358,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 +367,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

View File

@@ -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;
@@ -22,6 +23,10 @@ pub use crate::api::{
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, Notify, UpdateType},
push::{
push_cipher_update, push_folder_update, push_logout, push_send_update, push_user_update, register_push_device,
unregister_push_device,
},
web::catchers as web_catchers, web::catchers as web_catchers,
web::routes as web_routes, web::routes as web_routes,
web::static_files, web::static_files,

View File

@@ -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,130 @@ use tokio_tungstenite::{
}; };
use crate::{ use crate::{
api::EmptyResult, auth::ClientIp,
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()),
})
});
use super::{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]
} }
#[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);
}
}
}
#[get("/hub?<data..>")]
fn websockets_hub<'r>(
ws: rocket_ws::WebSocket,
data: WsAccessToken,
ip: ClientIp,
) -> Result<rocket_ws::Stream!['r], Error> {
let addr = ip.ip;
info!("Accepting Rocket WS connection from {addr}");
let Some(token) = data.access_token else { err_code!("Invalid claim", 401) };
let Ok(claims) = crate::auth::decode_login(&token) else { err_code!("Invalid token", 401) };
let (mut rx, guard) = {
let users = Arc::clone(&WS_USERS);
// Add a channel to send messages to this client to the map
let entry_uuid = uuid::Uuid::new_v4();
let (tx, rx) = tokio::sync::mpsc::channel::<Message>(100);
users.map.entry(claims.sub.clone()).or_default().push((entry_uuid, tx));
// Once the guard goes out of scope, the connection will have been closed and the entry will be deleted from the map
(rx, WSEntryMapGuard::new(users, claims.sub, entry_uuid, addr))
};
Ok({
rocket_ws::Stream! { ws => {
let mut ws = ws;
let _guard = guard;
let mut interval = tokio::time::interval(Duration::from_secs(15));
loop {
tokio::select! {
res = ws.next() => {
match res {
Some(Ok(message)) => {
match message {
// Respond to any pings
Message::Ping(ping) => yield Message::Pong(ping),
Message::Pong(_) => {/* Ignored */},
// We should receive an initial message with the protocol and version, and we will reply to it
Message::Text(ref message) => {
let msg = message.strip_suffix(RECORD_SEPARATOR as char).unwrap_or(message);
if serde_json::from_str(msg).ok() == Some(INITIAL_MESSAGE) {
yield Message::binary(INITIAL_RESPONSE);
continue;
}
}
// Just echo anything else the client sends
_ => yield message,
}
}
_ => break,
}
}
res = rx.recv() => {
match res {
Some(res) => yield res,
None => break,
}
}
_ = interval.tick() => yield Message::Ping(create_ping())
}
}
}}
})
}
// //
// Websockets server // Websockets server
// //
@@ -127,8 +222,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 +238,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 +276,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 +288,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 +319,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 +348,9 @@ 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;
}
} }
} }
@@ -253,7 +392,7 @@ fn create_ping() -> Vec<u8> {
} }
#[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 +419,12 @@ 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 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 +436,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 +452,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 +472,39 @@ async fn handle_connection(stream: TcpStream, users: WebSocketUsers, addr: Socke
let user_uuid = user_uuid.expect("User UUID should be set after the handshake"); 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 +513,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(())
} }

257
src/api/push.rs Normal file
View File

@@ -0,0 +1,257 @@
use reqwest::header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE};
use serde_json::Value;
use tokio::sync::RwLock;
use crate::{
api::{ApiResult, EmptyResult, UpdateType},
db::models::{Cipher, Device, Folder, Send, User},
util::get_reqwest_client,
CONFIG,
};
use once_cell::sync::Lazy;
use std::time::{Duration, Instant};
#[derive(Deserialize)]
struct AuthPushToken {
access_token: String,
expires_in: i32,
}
#[derive(Debug)]
struct LocalAuthPushToken {
access_token: String,
valid_until: Instant,
}
async fn get_auth_push_token() -> ApiResult<String> {
static PUSH_TOKEN: Lazy<RwLock<LocalAuthPushToken>> = Lazy::new(|| {
RwLock::new(LocalAuthPushToken {
access_token: String::new(),
valid_until: Instant::now(),
})
});
let push_token = PUSH_TOKEN.read().await;
if push_token.valid_until.saturating_duration_since(Instant::now()).as_secs() > 0 {
debug!("Auth Push token still valid, no need for a new one");
return Ok(push_token.access_token.clone());
}
drop(push_token); // Drop the read lock now
let installation_id = CONFIG.push_installation_id();
let client_id = format!("installation.{installation_id}");
let client_secret = CONFIG.push_installation_key();
let params = [
("grant_type", "client_credentials"),
("scope", "api.push"),
("client_id", &client_id),
("client_secret", &client_secret),
];
let res = match get_reqwest_client().post("https://identity.bitwarden.com/connect/token").form(&params).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(&notification_data)
.send()
.await
{
error!("An error occured while sending a send update to the push relay: {}", e);
};
}

View File

@@ -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,
@@ -91,8 +92,13 @@ async fn web_files(p: PathBuf) -> Cached<Option<NamedFile>> {
Cached::long(NamedFile::open(Path::new(&CONFIG.web_vault_folder()).join(p)).await.ok(), true) 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) = dbg!(decode_file_download(&token)) else { return None };
if claims.sub != *uuid || claims.file_id != *file_id {
return None;
}
NamedFile::open(Path::new(&CONFIG.attachments_folder()).join(uuid).join(file_id)).await.ok() NamedFile::open(Path::new(&CONFIG.attachments_folder()).join(uuid).join(file_id)).await.ok()
} }
@@ -111,8 +117,8 @@ fn alive_head(_conn: DbConn) -> EmptyResult {
} }
#[get("/vw_static/<filename>")] #[get("/vw_static/<filename>")]
pub fn static_files(filename: String) -> Result<(ContentType, &'static [u8]), Error> { pub fn static_files(filename: &str) -> Result<(ContentType, &'static [u8]), Error> {
match filename.as_ref() { 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"))),
@@ -136,8 +142,8 @@ pub fn static_files(filename: String) -> Result<(ContentType, &'static [u8]), Er
"jdenticon.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/jdenticon.js"))), "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.6.4.slim.js" => {
Ok((ContentType::JavaScript, include_bytes!("../static/scripts/jquery-3.6.3.slim.js"))) Ok((ContentType::JavaScript, include_bytes!("../static/scripts/jquery-3.6.4.slim.js")))
} }
_ => err!(format!("Static file not found: {filename}")), _ => err!(format!("Static file not found: {filename}")),
} }

View File

@@ -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,
}) })
} }

View File

@@ -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.
@@ -476,7 +486,7 @@ 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
@@ -603,7 +613,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 +734,17 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
} }
} }
if cfg.push_enabled && (cfg.push_installation_id == String::new() || cfg.push_installation_key == String::new()) {
err!(
"Misconfigured Push Notification service\n\
########################################################################################\n\
# It looks like you enabled Push Notification feature, but didn't configure it #\n\
# properly. Make sure the installation id and key from https://bitwarden.com/host are #\n\
# added to your configuration. #\n\
########################################################################################\n"
)
}
if cfg._enable_duo 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())

View File

@@ -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;

View File

@@ -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"),
}; };

View File

@@ -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,

View File

@@ -1,6 +1,6 @@
use chrono::{NaiveDateTime, Utc}; use chrono::{NaiveDateTime, Utc};
use crate::CONFIG; use crate::{crypto, CONFIG};
db_object! { db_object! {
#[derive(Identifiable, Queryable, Insertable, AsChangeset)] #[derive(Identifiable, Queryable, Insertable, AsChangeset)]
@@ -15,7 +15,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 +39,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 +47,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 +66,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 +153,35 @@ impl Device {
}} }}
} }
pub async fn find_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
db_run! { conn: {
devices::table
.filter(devices::user_uuid.eq(user_uuid))
.load::<DeviceDb>(conn)
.expect("Error loading devices")
.from_db()
}}
}
pub async fn find_by_uuid(uuid: &str, conn: &mut DbConn) -> Option<Self> {
db_run! { conn: {
devices::table
.filter(devices::uuid.eq(uuid))
.first::<DeviceDb>(conn)
.ok()
.from_db()
}}
}
pub async fn clear_push_token_by_uuid(uuid: &str, conn: &mut DbConn) -> EmptyResult {
db_run! { conn: {
diesel::update(devices::table)
.filter(devices::uuid.eq(uuid))
.set(devices::push_token.eq::<Option<String>>(None))
.execute(conn)
.map_res("Error removing push token")
}}
}
pub async fn find_by_refresh_token(refresh_token: &str, conn: &mut DbConn) -> Option<Self> { 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 +202,26 @@ 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
}}
}
} }

View File

@@ -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,
} }
@@ -107,10 +107,6 @@ impl Group {
None => self.external_id = None, None => self.external_id = None,
} }
} }
pub fn get_external_id(&self) -> Option<String> {
self.external_id.clone()
}
} }
impl CollectionGroup { impl CollectionGroup {
@@ -214,6 +210,15 @@ impl Group {
}} }}
} }
pub async fn find_by_external_id(id: &str, conn: &mut DbConn) -> Option<Self> {
db_run! { conn: {
groups::table
.filter(groups::external_id.eq(id))
.first::<GroupDb>(conn)
.ok()
.from_db()
}}
}
//Returns all organizations the user has full access to //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: {

View File

@@ -24,7 +24,7 @@ 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;

View File

@@ -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),
}, },

View File

@@ -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(),
@@ -481,7 +510,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 +744,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 +779,50 @@ impl UserOrganization {
} }
} }
impl OrganizationApiKey {
pub async fn save(&self, conn: &DbConn) -> EmptyResult {
db_run! { conn:
sqlite, mysql {
match diesel::replace_into(organization_api_key::table)
.values(OrganizationApiKeyDb::to_db(self))
.execute(conn)
{
Ok(_) => Ok(()),
// Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first.
Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => {
diesel::update(organization_api_key::table)
.filter(organization_api_key::uuid.eq(&self.uuid))
.set(OrganizationApiKeyDb::to_db(self))
.execute(conn)
.map_res("Error saving organization")
}
Err(e) => Err(e.into()),
}.map_res("Error saving organization")
}
postgresql {
let value = OrganizationApiKeyDb::to_db(self);
diesel::insert_into(organization_api_key::table)
.values(&value)
.on_conflict(organization_api_key::uuid)
.do_update()
.set(&value)
.execute(conn)
.map_res("Error saving organization")
}
}
}
pub async fn find_by_org_uuid(org_uuid: &str, conn: &DbConn) -> Option<Self> {
db_run! { conn: {
organization_api_key::table
.filter(organization_api_key::org_uuid.eq(org_uuid))
.first::<OrganizationApiKeyDb>(conn)
.ok().from_db()
}}
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@@ -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()

View File

@@ -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,
@@ -291,6 +304,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));
@@ -315,6 +329,7 @@ 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,

View File

@@ -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,
@@ -291,6 +304,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));
@@ -315,6 +329,7 @@ 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,

View File

@@ -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,
@@ -292,6 +305,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));
@@ -316,6 +330,7 @@ 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,

View File

@@ -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))

View File

@@ -250,7 +250,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 +264,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)

View File

@@ -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
} }
] ]

View File

@@ -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;
} }

View File

@@ -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();

View File

@@ -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

View File

@@ -113,13 +113,31 @@ 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",
@@ -228,6 +246,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();

View File

@@ -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.4
* *
* Included libraries: * Included libraries:
* DataTables 1.13.2 * DataTables 1.13.4
*/ */
@charset "UTF-8"; @charset "UTF-8";
@@ -79,6 +79,7 @@ table.dataTable thead > tr > td.sorting_asc_disabled:before,
table.dataTable thead > tr > td.sorting_desc_disabled:before { 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 +89,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 +106,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 +134,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) {

View File

@@ -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.4
* *
* Included libraries: * Included libraries:
* DataTables 1.13.2 * DataTables 1.13.4
*/ */
/*! DataTables 1.13.2 /*! DataTables 1.13.4
* ©2008-2023 SpryMedia Ltd - datatables.net/license * ©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.4
* @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 );
} }
@@ -2365,7 +2379,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;
} }
@@ -5119,7 +5133,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 +9382,48 @@
/**
* Set the jQuery or window object to be used by DataTables
*
* @param {*} module Library / container object
* @param {string} type Library or container type `lib` or `win`.
*/
DataTable.use = function (module, type) {
if (type === 'lib' || module.fn) {
$ = module;
}
else if (type == 'win' || module.document) {
window = module;
document = module.document;
}
}
/**
* CommonJS factory function pass through. This will check if the arguments
* given are a window object or a jQuery object. If so they are set
* accordingly.
* @param {*} root Window
* @param {*} jq jQUery
* @returns {boolean} Indicator
*/
DataTable.factory = function (root, jq) {
var is = false;
// Test if the first parameter is a window object
if (root && root.document) {
window = root;
document = root.document;
}
// Test if the second parameter is a jQuery object
if (jq && jq.fn && jq.fn.jquery) {
$ = jq;
is = true;
}
return is;
}
/** /**
* Provide a common method for plug-ins to check the version of DataTables being * 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.
@@ -9708,7 +9765,7 @@
* @type string * @type string
* @default Version number * @default Version number
*/ */
DataTable.version = "1.13.2"; DataTable.version = "1.13.4";
/** /**
* Private data store, containing all of the settings objects that are * Private data store, containing all of the settings objects that are
@@ -14132,7 +14189,7 @@
* *
* @type string * @type string
*/ */
build:"bs5/dt-1.13.2", build:"bs5/dt-1.13.4",
/** /**
@@ -15654,25 +15711,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

View File

@@ -1,5 +1,5 @@
/*! /*!
* jQuery JavaScript Library v3.6.3 -ajax,-ajax/jsonp,-ajax/load,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-deprecated/ajax-event-alias,-effects,-effects/Tween,-effects/animatedSelector * jQuery JavaScript Library v3.6.4 -ajax,-ajax/jsonp,-ajax/load,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-deprecated/ajax-event-alias,-effects,-effects/animatedSelector,-effects/Tween
* https://jquery.com/ * https://jquery.com/
* *
* Includes Sizzle.js * Includes Sizzle.js
@@ -9,7 +9,7 @@
* Released under the MIT license * Released under the MIT license
* https://jquery.org/license * https://jquery.org/license
* *
* Date: 2022-12-20T21:28Z * Date: 2023-03-08T15:29Z
*/ */
( function( global, factory ) { ( function( global, factory ) {
@@ -151,7 +151,7 @@ function toType( obj ) {
var var
version = "3.6.3 -ajax,-ajax/jsonp,-ajax/load,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-deprecated/ajax-event-alias,-effects,-effects/Tween,-effects/animatedSelector", version = "3.6.4 -ajax,-ajax/jsonp,-ajax/load,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-deprecated/ajax-event-alias,-effects,-effects/animatedSelector,-effects/Tween",
// Define a local copy of jQuery // Define a local copy of jQuery
jQuery = function( selector, context ) { jQuery = function( selector, context ) {
@@ -522,14 +522,14 @@ function isArrayLike( obj ) {
} }
var Sizzle = var Sizzle =
/*! /*!
* Sizzle CSS Selector Engine v2.3.9 * Sizzle CSS Selector Engine v2.3.10
* https://sizzlejs.com/ * https://sizzlejs.com/
* *
* Copyright JS Foundation and other contributors * Copyright JS Foundation and other contributors
* Released under the MIT license * Released under the MIT license
* https://js.foundation/ * https://js.foundation/
* *
* Date: 2022-12-19 * Date: 2023-02-14
*/ */
( function( window ) { ( function( window ) {
var i, var i,
@@ -633,7 +633,7 @@ var i,
whitespace + "+$", "g" ), whitespace + "+$", "g" ),
rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ),
rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + rleadingCombinator = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace +
"*" ), "*" ),
rdescend = new RegExp( whitespace + "|>" ), rdescend = new RegExp( whitespace + "|>" ),
@@ -850,7 +850,7 @@ function Sizzle( selector, context, results, seed ) {
// as such selectors are not recognized by querySelectorAll. // as such selectors are not recognized by querySelectorAll.
// Thanks to Andrew Dupont for this technique. // Thanks to Andrew Dupont for this technique.
if ( nodeType === 1 && if ( nodeType === 1 &&
( rdescend.test( selector ) || rcombinators.test( selector ) ) ) { ( rdescend.test( selector ) || rleadingCombinator.test( selector ) ) ) {
// Expand context for sibling selectors // Expand context for sibling selectors
newContext = rsibling.test( selector ) && testContext( context.parentNode ) || newContext = rsibling.test( selector ) && testContext( context.parentNode ) ||
@@ -879,27 +879,6 @@ function Sizzle( selector, context, results, seed ) {
} }
try { try {
// `qSA` may not throw for unrecognized parts using forgiving parsing:
// https://drafts.csswg.org/selectors/#forgiving-selector
// like the `:has()` pseudo-class:
// https://drafts.csswg.org/selectors/#relational
// `CSS.supports` is still expected to return `false` then:
// https://drafts.csswg.org/css-conditional-4/#typedef-supports-selector-fn
// https://drafts.csswg.org/css-conditional-4/#dfn-support-selector
if ( support.cssSupportsSelector &&
// eslint-disable-next-line no-undef
!CSS.supports( "selector(:is(" + newSelector + "))" ) ) {
// Support: IE 11+
// Throw to get to the same code path as an error directly in qSA.
// Note: once we only support browser supporting
// `CSS.supports('selector(...)')`, we can most likely drop
// the `try-catch`. IE doesn't implement the API.
throw new Error();
}
push.apply( results, push.apply( results,
newContext.querySelectorAll( newSelector ) newContext.querySelectorAll( newSelector )
); );
@@ -1195,29 +1174,22 @@ setDocument = Sizzle.setDocument = function( node ) {
!el.querySelectorAll( ":scope fieldset div" ).length; !el.querySelectorAll( ":scope fieldset div" ).length;
} ); } );
// Support: Chrome 105+, Firefox 104+, Safari 15.4+ // Support: Chrome 105 - 110+, Safari 15.4 - 16.3+
// Make sure forgiving mode is not used in `CSS.supports( "selector(...)" )`. // Make sure the the `:has()` argument is parsed unforgivingly.
// // We include `*` in the test to detect buggy implementations that are
// `:is()` uses a forgiving selector list as an argument and is widely // _selectively_ forgiving (specifically when the list includes at least
// implemented, so it's a good one to test against. // one valid selector).
support.cssSupportsSelector = assert( function() { // Note that we treat complete lack of support for `:has()` as if it were
/* eslint-disable no-undef */ // spec-compliant support, which is fine because use of `:has()` in such
// environments will fail in the qSA path and fall back to jQuery traversal
return CSS.supports( "selector(*)" ) && // anyway.
support.cssHas = assert( function() {
// Support: Firefox 78-81 only try {
// In old Firefox, `:is()` didn't use forgiving parsing. In that case, document.querySelector( ":has(*,:jqfake)" );
// fail this test as there's no selector to test against that. return false;
// `CSS.supports` uses unforgiving parsing } catch ( e ) {
document.querySelectorAll( ":is(:jqfake)" ) && return true;
}
// `*` is needed as Safari & newer Chrome implemented something in between
// for `:has()` - it throws in `qSA` if it only contains an unsupported
// argument but multiple ones, one of which is supported, are fine.
// We want to play safe in case `:is()` gets the same treatment.
!CSS.supports( "selector(:is(*,:jqfake))" );
/* eslint-enable */
} ); } );
/* Attributes /* Attributes
@@ -1486,14 +1458,14 @@ setDocument = Sizzle.setDocument = function( node ) {
} ); } );
} }
if ( !support.cssSupportsSelector ) { if ( !support.cssHas ) {
// Support: Chrome 105+, Safari 15.4+ // Support: Chrome 105 - 110+, Safari 15.4 - 16.3+
// `:has()` uses a forgiving selector list as an argument so our regular // Our regular `try-catch` mechanism fails to detect natively-unsupported
// `try-catch` mechanism fails to catch `:has()` with arguments not supported // pseudo-classes inside `:has()` (such as `:has(:contains("Foo"))`)
// natively like `:has(:contains("Foo"))`. Where supported & spec-compliant, // in browsers that parse the `:has()` argument as a forgiving selector list.
// we now use `CSS.supports("selector(:is(SELECTOR_TO_BE_TESTED))")`, but // https://drafts.csswg.org/selectors/#relational now requires the argument
// outside that we mark `:has` as buggy. // to be parsed unforgivingly, but browsers have not yet fully adjusted.
rbuggyQSA.push( ":has" ); rbuggyQSA.push( ":has" );
} }
@@ -2406,7 +2378,7 @@ tokenize = Sizzle.tokenize = function( selector, parseOnly ) {
matched = false; matched = false;
// Combinators // Combinators
if ( ( match = rcombinators.exec( soFar ) ) ) { if ( ( match = rleadingCombinator.exec( soFar ) ) ) {
matched = match.shift(); matched = match.shift();
tokens.push( { tokens.push( {
value: matched, value: matched,

View File

@@ -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.6.4.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>

View File

@@ -8,7 +8,7 @@
<th class="vw-account-details">User</th> <th class="vw-account-details">User</th>
<th class="vw-created-at">Created at</th> <th class="vw-created-at">Created at</th>
<th class="vw-last-active">Last Active</th> <th class="vw-last-active">Last Active</th>
<th class="vw-ciphers">Ciphers</th> <th class="vw-entries">Entries</th>
<th class="vw-attachments">Attachments</th> <th class="vw-attachments">Attachments</th>
<th class="vw-organizations">Organizations</th> <th class="vw-organizations">Organizations</th>
<th class="vw-actions">Actions</th> <th class="vw-actions">Actions</th>
@@ -72,6 +72,9 @@
{{else}} {{else}}
<button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-enable-user>Enable User</button><br> <button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-enable-user>Enable User</button><br>
{{/if}} {{/if}}
{{#case _Status 1}}
<button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-resend-user-invite>Resend invite</button><br>
{{/case}}
</span> </span>
</td> </td>
</tr> </tr>
@@ -137,7 +140,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.6.4.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_users.js"></script> <script src="{{urlpath}}/vw_static/admin_users.js"></script>
<script src="{{urlpath}}/vw_static/jdenticon.js"></script> <script src="{{urlpath}}/vw_static/jdenticon.js"></script>

View File

@@ -1,6 +1,4 @@
Master Password Has Been Changed Master Password Has Been Changed
<!----------------> <!---------------->
The master password for {{user_name}} has been changed by an administrator in your {{org_name}} organization. If you did not initiate this request, please reach out to your administrator immediately. The master password for {{user_name}} has been changed by an administrator in your {{org_name}} organization. If you did not initiate this request, please reach out to your administrator immediately.
{{> email/email_footer_text }}
===
Github: https://github.com/dani-garcia/vaultwarden

View File

@@ -3,6 +3,4 @@ Your Email Change
To finalize changing your email address enter the following code in web vault: {{token}} To finalize changing your email address enter the following code in web vault: {{token}}
If you did not try to change an email address, you can safely ignore this email. If you did not try to change an email address, you can safely ignore this email.
{{> email/email_footer_text }}
===
Github: https://github.com/dani-garcia/vaultwarden

View File

@@ -4,7 +4,7 @@ Delete Your Account
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> <table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center"> <td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
click the link below to delete your account. Click the link below to delete your account.
</td> </td>
</tr> </tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">

View File

@@ -1,5 +1,5 @@
Invitation to {{{org_name}}} accepted Invitation to {{{org_name}}} accepted
<!----------------> <!---------------->
Your invitation for *{{email}}* to join *{{org_name}}* was accepted. This email is to notify you that {{email}} has accepted your invitation to join {{org_name}}.
Please log in via {{url}} to the vaultwarden server and confirm them from the organization management page. Please log in via {{url}} to the vaultwarden server and confirm them from the organization management page.
{{> email/email_footer_text }} {{> email/email_footer_text }}

View File

@@ -1,5 +1,5 @@
Invitation to {{{org_name}}} confirmed Invitation to {{{org_name}}} confirmed
<!----------------> <!---------------->
Your invitation to join *{{org_name}}* was confirmed. This email is to notify you that you have been confirmed as a user of {{org_name}}.
It will now appear under the Organizations the next time you log in to the web vault at {{url}}. Any collections and logins being shared with you by this organization will now appear in your Vaultwarden vault at {{url}}.
{{> email/email_footer_text }} {{> email/email_footer_text }}

View File

@@ -4,6 +4,4 @@ You have been removed from organization *{{org_name}}* because your account does
You can enable Two-step Login in your account settings. You can enable Two-step Login in your account settings.
{{> email/email_footer_text }}
===
Github: https://github.com/dani-garcia/vaultwarden

View File

@@ -1,5 +1,4 @@
You have been removed from {{{org_name}}} You have been removed from {{{org_name}}}
<!----------------> <!---------------->
Your user account has been removed from the *{{org_name}}* organization because you are a part of another organization. The {{org_name}} organization has enabled a policy that prevents users from being a part of multiple organizations. Before you can re-join this organization you need to leave all other organizations or join with a different account. Your user account has been removed from the *{{org_name}}* organization because you are a part of another organization. The {{org_name}} organization has enabled a policy that prevents users from being a part of multiple organizations. Before you can re-join this organization you need to leave all other organizations or join with a different account.
=== {{> email/email_footer_text }}
Github: https://github.com/dani-garcia/vaultwarden

View File

@@ -1,7 +1,10 @@
// //
// Web Headers and caching // Web Headers and caching
// //
use std::io::{Cursor, ErrorKind}; use std::{
io::{Cursor, ErrorKind},
ops::Deref,
};
use rocket::{ use rocket::{
fairing::{Fairing, Info, Kind}, fairing::{Fairing, Info, Kind},
@@ -209,6 +212,14 @@ impl std::fmt::Display for SafeString {
} }
} }
impl Deref for SafeString {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl AsRef<Path> for SafeString { impl AsRef<Path> for SafeString {
#[inline] #[inline]
fn as_ref(&self) -> &Path { fn as_ref(&self) -> &Path {
@@ -231,7 +242,7 @@ impl<'r> FromParam<'r> for SafeString {
// Log all the routes from the main paths list, and the attachments endpoint // Log all the routes from the main paths list, and the attachments endpoint
// Effectively ignores, any static file route, and the alive endpoint // Effectively ignores, any static file route, and the alive endpoint
const LOGGED_ROUTES: [&str; 5] = ["/api", "/admin", "/identity", "/icons", "/attachments"]; const LOGGED_ROUTES: [&str; 7] = ["/api", "/admin", "/identity", "/icons", "/attachments", "/events", "/notifications"];
// Boolean is extra debug, when true, we ignore the whitelist above and also print the mounts // Boolean is extra debug, when true, we ignore the whitelist above and also print the mounts
pub struct BetterLogging(pub bool); pub struct BetterLogging(pub bool);