Compare commits

..

44 Commits

Author SHA1 Message Date
Mathijs van Veluw
33bae5fbe9 Update crates and fix Mail issue (#5125)
- Updated all the crates
  Including in this update is an update from lettre, which solves an issue with some specific SMTP mail providers.
2024-10-24 19:13:20 +02:00
Daniel
f60502a17e Add documentation for the extension-refresh feature flag (#5112) 2024-10-21 00:05:11 +02:00
Mathijs van Veluw
13f4b66e62 Hide user name on invite status (#5110)
A possible user disclosure when you invite an user into an organization which already has an account on the same instance.
This was because we always returned the user's name.
To prevent this, this PR only returns the user's name if the status is accepted or higher, else we will return null.
This is the same as Bitwarden does.

Resolves a reported issue.

Also resolved a new `nightly` reported clippy regarding a regex within a loop.
2024-10-19 18:22:21 +02:00
Daniel
c967d0ddc1 Add extension-refresh feature flag (#5106)
- in case people want to try out the new extension design
2024-10-19 18:21:00 +02:00
Mathijs van Veluw
ae6ed0ece8 Fix collection management and match some json output (#5095)
- Fixed collection management to be usable from the Password Manager UI
- Checked and brought in-to-sync with upstream several json responses
- Fixed a small issue with the `fields` response when it was empty

Signed-off-by: BlackDex <black.dex@gmail.com>
2024-10-18 20:37:32 +02:00
Daniel
b7c254eb30 Update Rust to 1.82.0 (#5099)
- raise MSRV to 1.80.0
- also update the crates
2024-10-18 20:34:31 +02:00
Mathijs van Veluw
a47b484172 Fix org invite url being html encoded (#5100)
Ever since we changed to pass the full url as a template value handlebars now html-encodes this.
This causes issues with the plain/text mails, but it also could potentially cause issues with the text/html templates.

This PR encloses the template values inside triple braces `{{{ }}}` which prevents html-encoding.
Since the URL is generated via the `url` crate the values are percent-encoded anyway.

Fixes #5097

Signed-off-by: BlackDex <black.dex@gmail.com>
2024-10-18 20:34:11 +02:00
Mathijs van Veluw
65629a99f0 Fix field type to actually be hidden (#5082)
In an oversight i forgot to set the type to a hidden type if converting the int was not possible.
This fixes that.

Signed-off-by: BlackDex <black.dex@gmail.com>
2024-10-13 20:32:15 +02:00
Mathijs van Veluw
49c5dec9b6 Fix iOS sync by converting field types to int (#5081)
It seems the iOS clients are not able to handle the `type` key within the `fields` array when they are of the type string.
All other clients seem to handle this just fine though.

This PR fixes this by validating it is a number, if this is not the case, try to convert the string to a number, or return the default of `1`.
`1` is used as this is the type `hidden` and should prevent accidental data disclosure.

Fixes #5069

Possibly Fixes #5016
Possibly Fixes #5002

Signed-off-by: BlackDex <black.dex@gmail.com>
2024-10-13 20:25:09 +02:00
Mathijs van Veluw
cd195ff243 Fix --version from failing without config (#5055)
* Fix `--version` from failing without config

Since we added the option to show the web-vault version also when running `--version` this causes the config to always be validated.
While this is not very bad in general, it could cause the command to quit during the config validation, and not show the version, but also errors.
This is probably not very useful for this specific command, unlike the `--backup` for example.

To fix this, and preventing the config from being validated, i added an AtomicBool to check if we need to validate the config on first load.
This prevents errors, and will just show the Vaultwarden version, and if possible the web-vault version too.

Fixes #5046

Signed-off-by: BlackDex <black.dex@gmail.com>

* Adjusted the code bsaed upon review

Signed-off-by: BlackDex <black.dex@gmail.com>

---------

Signed-off-by: BlackDex <black.dex@gmail.com>
2024-10-11 18:58:25 +02:00
Mathijs van Veluw
e3541763fd Updates and collection management fixes (#5072)
* Fix collections not editable by managers

Since a newer version of the web-vault we use manager were not able to create sub collections anymore.
This was because of some missing details in the response of some json objects.

This commit fixes this by using the `to_json_details` instead of the `to_json`

Fixes #5066
Fixes #5044

* Update crates and GitHub Actions

- Updated all the crates
- Updated all the GHA dependencies
- Configured the trivy workflow to only run on the main repo and not on forks
  Also selected a random new scheduled date so it will not run at the same time as all other forks.
  The two changes should help running this scan every day without failing, and also prevent the same for new or updated forks.
2024-10-11 18:42:40 +02:00
Mathijs van Veluw
f0efec7c96 Fix compiling for Windows targets (#5053)
The `unix::signal` was also included during Windows compilations.
This of course will not work. Fix this by only including it for `unix` targets.

Also changed all other conditional compilation options to use `cfg(unix)` instead of `cfg(not(windows))`.
The latter may also include `wasm` for example, or any other future target family.
This way we will only match `unix`

Fixes #5052
2024-10-06 13:49:00 +02:00
Mathijs van Veluw
040e2a7bb0 Add extra linting (#4977)
* Add extra linting

Added extra linting for some code styles.
Also added the Rust Edition 2024 lints.

Closes #4974

Signed-off-by: BlackDex <black.dex@gmail.com>

* Adjusted according to comments

Signed-off-by: BlackDex <black.dex@gmail.com>

---------

Signed-off-by: BlackDex <black.dex@gmail.com>
2024-09-23 20:25:32 +02:00
Daniel García
d184c8f08c Fix keyword collision in Rust 2024 and add new api/config value (#4975)
* Avoid keyword collision with gen in Rust 2024

* Include new api/config setting to disable user registration, not yet used by clients

* Actually qualify CONFIG
2024-09-20 21:39:00 +02:00
Mathijs van Veluw
7d6dec6413 Fix encrypted lastUsedDate (#4972)
It appears that some password histories have an encrypted value on the `lastUsedDate`
Instead of only checking if it is a string, also check if it is a valid RFC Date/Time String.
If not, set it also to epoch 0.

Signed-off-by: BlackDex <black.dex@gmail.com>
2024-09-20 20:38:42 +02:00
Mathijs van Veluw
de01111082 Fix Device Type column for 2FA migration (#4971) 2024-09-20 12:06:06 +02:00
Stefan Melmuk
0bd8f607cb remove backtics from postgresql migrations (#4968) 2024-09-19 19:18:09 +02:00
Daniel
21efc0800d Actually use Device Type for mails (#4916)
- match Bitwarden behaviour
- add a different segment in mails for Device Name
2024-09-18 19:03:15 +02:00
Stefan Melmuk
1031c2e286 fix 2fa policy check on registration (#4956) 2024-09-18 19:00:10 +02:00
Mathijs van Veluw
1bf85201e7 Fix Pw History null dates (#4966)
It seemed to have been possible to have `null` date values.
This PR fixes this by setting the epoch start date if either the date does not exists or is not a string.

This should solve sync issues with the new native mobile clients.

Fixes https://github.com/dani-garcia/vaultwarden/pull/4932#issuecomment-2357581292

Signed-off-by: BlackDex <black.dex@gmail.com>
2024-09-18 18:57:08 +02:00
Stefan Melmuk
6ceed9284d fix invitation link via /admin (#4950) 2024-09-13 22:08:59 +02:00
Mathijs van Veluw
25d99e3506 Fix collection update from native client (#4937) 2024-09-10 21:33:59 +02:00
Mathijs van Veluw
dca14285fd Fix sync with new native clients (#4932) 2024-09-09 11:36:37 +02:00
Daniel
66baa5e7d8 Update Rust version & crates (#4928) 2024-09-07 10:39:29 +02:00
Timshel
248e561b3f Add orgUserHasExistingUser parameters to org invite (#4827) 2024-09-01 15:55:41 +02:00
Mathijs van Veluw
55623ad9c6 Update web-vault, crates and gha (#4909)
- Updated the web-vault to fix an issue with personal export.
  Thanks to @stefan0xC for patching this.
  Fixes #4875
- Updated crates to there latest version
- Updated the GitHub Actions
- Updated the xx image to the latest version

Signed-off-by: BlackDex <black.dex@gmail.com>
2024-09-01 15:53:03 +02:00
Mathijs van Veluw
e9acd8bd3c Add a CLI feature to backup the SQLite DB (#4906)
* Add a CLI feature to backup the SQLite DB

Many users request to add the sqlite3 binary to the container image.
This isn't really ideal as that might bring in other dependencies and will only bloat the image.
There main reason is to create a backup of the database.

While there already was a feature within the admin interface to do so (or by using the admin API call), this might not be easy.

This PR adds several ways to generate a backup.
1. By calling the Vaultwarden binary with the `backup` command like:
  - `/vaultwarden backup`
  - `docker exec -it vaultwarden /vaultwarden backup`
2. By sending the USR1 signal to the running process like:
  - `kill -s USR1 $(pidof vaultwarden)
  - `killall -s USR1 vaultwarden)

This should help users to more easily create backups of there SQLite database.

Also added the Web-Vault version number when using `-v/--version` to the output.

Signed-off-by: BlackDex <black.dex@gmail.com>

* Spelling and small adjustments

Signed-off-by: BlackDex <black.dex@gmail.com>

---------

Signed-off-by: BlackDex <black.dex@gmail.com>
2024-09-01 15:52:29 +02:00
Mathijs van Veluw
544b7229e8 Allow enforcing Single Org with pw reset policy (#4903)
* Allow enforcing Single Org with pw reset policy

Bitwarden only allows the Reset Password policy to be set when the Single Org policy is enabled already.
This PR adds a check so that this can be enforced when a config option is enabled.

Since Vaultwarden encouraged to use multiple orgs when groups were not available yet we should not enable this by default now.
This might be something to do in the future.

When enabled, it will prevent the Reset Password policy to be enabled if the Single Org policy is not enabled.
It will also prevent the Single Org policy to be disabled if the Reset Password policy is enabled.

Fixes #4855

Signed-off-by: BlackDex <black.dex@gmail.com>

* Removed some extra if checks

Signed-off-by: BlackDex <black.dex@gmail.com>

---------

Signed-off-by: BlackDex <black.dex@gmail.com>
2024-08-30 21:37:59 +02:00
Mathijs van Veluw
978f009293 Allow Org Master-Pw policy enforcement (#4899)
* Allow Org Master-Pw policy enforcement

We didn't returned the master password policy for the user.
If the `Require existing members to change their passwords` check was enabled this should trigger the login to show a change password dialog.

All the master password policies are merged into one during the login response and it will contain the max values and all `true` values which are set by all the different orgs if a user is an accepted member.

Fixes #4507

Signed-off-by: BlackDex <black.dex@gmail.com>

* Use .reduce instead of .fold

Signed-off-by: BlackDex <black.dex@gmail.com>

---------

Signed-off-by: BlackDex <black.dex@gmail.com>
2024-08-27 19:37:51 +02:00
Mathijs van Veluw
92f1530e96 Allow custom umask setting (#4896)
To provide a way to add more security regarding file/folder permissions
this PR adds a way to allow setting a custom `UMASK` variable.

This allows people to set a more secure default like only allowing the
owner the the process/container to read/write files and folders.

Examples:
 - `UMASK=022` File: 644 | Folder: 755 (Default of the containers)
   This means Owner read/write and group/world read-only
 - `UMASK=027` File: 640 | Folder: 750
   This means Owner read/write, group read-only, world no access
 - `UMASK=077` File: 600 | Folder: 700
   This measn Owner read/write and group/world no access

resolves #4571

Signed-off-by: BlackDex <black.dex@gmail.com>
2024-08-27 19:37:17 +02:00
Mathijs van Veluw
2b824e8096 Updated security readme (#4892)
Update the security readme with a new GPG security key and some small
other changes.

Signed-off-by: BlackDex <black.dex@gmail.com>
2024-08-24 18:59:27 +02:00
Mathijs van Veluw
059661be48 Update crates (GHSA-wq9x-qwcq-mmgf) (#4889)
- Updated crates
- Fixed MSRV to actually be N-2
- Changed some features to use the `dep:` prefix.
  This is needed for edition-2024 anyway although that will be a while before we can use that.

Signed-off-by: BlackDex <black.dex@gmail.com>
2024-08-23 22:06:11 +02:00
Mathijs van Veluw
0f3f97cc76 Update issue template (#4882)
Updated the issue template a bit regarding some remarks in the previous pr.

Also made it so that collapsing all items will show all the specific
item id's instead of there types. Easy for editiing :).

Signed-off-by: BlackDex <black.dex@gmail.com>
2024-08-23 22:03:57 +02:00
philomathic_life
aa0fe7785a Remove version from server config info (#4885) 2024-08-22 21:42:30 +02:00
Timshel
65d11a9720 Switch to Whitelisting in .dockerignore (#4856) 2024-08-21 21:59:17 +02:00
Mathijs van Veluw
c722006385 Fix Login with device (#4878)
Fixed an issue with login with device for the new Bitwrden Beta clients.
They seem to not support ISO8601 milli date/time, only micro.

Also updated the device display names to match Upstream and added the
CLI devices which were missing.

Signed-off-by: BlackDex <black.dex@gmail.com>
2024-08-21 21:57:52 +02:00
Stefan Melmuk
aaab7f9640 remove overzealous sanity check (#4879)
when cloning an item from an organization to the personal vault
the client sends the collection id of the cloned item
2024-08-21 21:54:13 +02:00
Mathijs van Veluw
cbdb5657f1 Update issue template (#4876)
Updated the issue template to use a form and guide users to provide all
information useful to troublshoot issues

Als updated links to prefer the usage of GitHub Discussions.

Signed-off-by: BlackDex <black.dex@gmail.com>
2024-08-20 21:25:28 +02:00
Mathijs van Veluw
669b9db758 Fix Vaultwarden Admin page error messages (#4869)
Since the change to camelCase variables the error messages in the
Vaultwarden Admin were not shown correctly anymore.

This PR fixes this by changing the case of the json key's.
Also updated the save and delete of the config to provide a more
descriptive error instead of only `Io` or which ever other error might
occure.

Fixes #4834
2024-08-18 21:04:22 +02:00
Timshel
3466a8040e Remove unecessary email normalization (#4840) 2024-08-17 22:48:59 +02:00
Daniel
7d47155d83 Update email footer padding values (#4838)
- looks better, the Github logo was too close to the bottom
- also fix a minor issue in the new device log in HTML template
2024-08-17 22:48:10 +02:00
Mathijs van Veluw
9e26014b4d Fix manager in web-vault v2024.6.2 for collections (#4860)
The web-vault v2024.6.2 we use needs some extra information to allow
managers to actually be able to manage collections.

The v2024.6.2 web-vault has somewhat of a mixture of the newer roles and
older manager roles. To at least fix this for the web-vault we bundle
these changes will make the manager able to manage.

For future web-vaults we would need a lot more changes to be done to fix
this in a better way though.

Fixes #4844
2024-08-15 12:36:00 +02:00
Mathijs van Veluw
339612c917 Fix Duo Redirect not using path (#4862)
The URL crate treats `https://domain.tld/path` differently then
`https://domain.tld/path/` the latter will make sure a `.join()` will
append the given path instead of using the base as a relative path.

Fixes #4858
2024-08-15 12:29:51 +02:00
Mathijs van Veluw
9eebbf3b9f Update GitHub Action Workflows (#4849) 2024-08-13 12:52:07 +02:00
67 changed files with 1626 additions and 1006 deletions

View File

@@ -1,40 +1,15 @@
# Local build artifacts // Ignore everything
target *
# Data folder // Allow what is needed
data !.git
# Misc
.env
.env.template
.gitattributes
.gitignore
rustfmt.toml
# IDE files
.vscode
.idea
.editorconfig
*.iml
# Documentation
.github
*.md
*.txt
*.yml
*.yaml
# Docker
hooks
tools
Dockerfile
.dockerignore
docker/**
!docker/healthcheck.sh !docker/healthcheck.sh
!docker/start.sh !docker/start.sh
!migrations
!src
# Web vault !build.rs
web-vault !Cargo.lock
!Cargo.toml
# Vaultwarden Resources !rustfmt.toml
resources !rust-toolchain.toml

View File

@@ -347,6 +347,7 @@
## - "autofill-overlay": Add an overlay menu to form fields for quick access to credentials. ## - "autofill-overlay": Add an overlay menu to form fields for quick access to credentials.
## - "autofill-v2": Use the new autofill implementation. ## - "autofill-v2": Use the new autofill implementation.
## - "browser-fileless-import": Directly import credentials from other providers without a file. ## - "browser-fileless-import": Directly import credentials from other providers without a file.
## - "extension-refresh": Temporarily enable the new extension design until general availability (should be used with the beta Chrome extension)
## - "fido2-vault-credentials": Enable the use of FIDO2 security keys as second factor. ## - "fido2-vault-credentials": Enable the use of FIDO2 security keys as second factor.
# EXPERIMENTAL_CLIENT_FEATURE_FLAGS=fido2-vault-credentials # EXPERIMENTAL_CLIENT_FEATURE_FLAGS=fido2-vault-credentials
@@ -425,6 +426,12 @@
## KNOW WHAT YOU ARE DOING! ## KNOW WHAT YOU ARE DOING!
# INCREASE_NOTE_SIZE_LIMIT=false # INCREASE_NOTE_SIZE_LIMIT=false
## Enforce Single Org with Reset Password Policy
## Enforce that the Single Org policy is enabled before setting the Reset Password policy
## Bitwarden enforces this by default. In Vaultwarden we encouraged to use multiple organizations because groups were not available.
## Setting this to true will enforce the Single Org Policy to be enabled before you can enable the Reset Password policy.
# ENFORCE_SINGLE_ORG_WITH_RESET_PW_POLICY=false
######################## ########################
### MFA/2FA settings ### ### MFA/2FA settings ###
######################## ########################

View File

@@ -1,66 +0,0 @@
---
name: Bug report
about: Use this ONLY for bugs in vaultwarden itself. Use the Discourse forum (link below) to request features or get help with usage/configuration. If in doubt, use the forum.
title: ''
labels: ''
assignees: ''
---
<!--
# ###
NOTE: Please update to the latest version of vaultwarden before reporting an issue!
This saves you and us a lot of time and troubleshooting.
See:
* https://github.com/dani-garcia/vaultwarden/issues/1180
* https://github.com/dani-garcia/vaultwarden/wiki/Updating-the-vaultwarden-image
# ###
-->
<!--
Please fill out the following template to make solving your problem easier and faster for us.
This is only a guideline. If you think that parts are unnecessary for your issue, feel free to remove them.
Remember to hide/redact personal or confidential information,
such as passwords, IP addresses, and DNS names as appropriate.
-->
### Subject of the issue
<!-- Describe your issue here. -->
### Deployment environment
<!--
=========================================================================================
Preferably, use the `Generate Support String` button on the admin page's Diagnostics tab.
That will auto-generate most of the info requested in this section.
=========================================================================================
-->
<!-- The version number, obtained from the logs (at startup) or the admin diagnostics page -->
<!-- This is NOT the version number shown on the web vault, which is versioned separately from vaultwarden -->
<!-- Remember to check if your issue exists on the latest version first! -->
* vaultwarden version:
<!-- How the server was installed: Docker image, OS package, built from source, etc. -->
* Install method:
* Clients used: <!-- web vault, desktop, Android, iOS, etc. (if applicable) -->
* Reverse proxy and version: <!-- if applicable -->
* MySQL/MariaDB or PostgreSQL version: <!-- if applicable -->
* Other relevant details:
### Steps to reproduce
<!-- Tell us how to reproduce this issue. What parameters did you set (differently from the defaults)
and how did you start vaultwarden? -->
### Expected behaviour
<!-- Tell us what you expected to happen -->
### Actual behaviour
<!-- Tell us what actually happened -->
### Troubleshooting data
<!-- Share any log files, screenshots, or other relevant troubleshooting data -->

167
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,167 @@
name: Bug Report
description: File a bug report
labels: ["bug"]
body:
#
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
Please *do not* submit feature requests or ask for help on how to configure Vaultwarden here.
The [GitHub Discussions](https://github.com/dani-garcia/vaultwarden/discussions/) has sections for Questions and Ideas.
Also, make sure you are running [![GitHub Release](https://img.shields.io/github/release/dani-garcia/vaultwarden.svg)](https://github.com/dani-garcia/vaultwarden/releases/latest) of Vaultwarden!
And search for existing open or closed issues or discussions regarding your topic before posting.
Be sure to check and validate the Vaultwarden Admin Diagnostics (`/admin/diagnostics`) page for any errors!
See here [how to enable the admin page](https://github.com/dani-garcia/vaultwarden/wiki/Enabling-admin-page).
#
- id: support-string
type: textarea
attributes:
label: Vaultwarden Support String
description: Output of the **Generate Support String** from the `/admin/diagnostics` page.
placeholder: |
1. Go to the Vaultwarden Admin of your instance https://example.domain.tld/admin/diagnostics
2. Click on `Generate Support String`
3. Click on `Copy To Clipboard`
4. Replace this text by pasting it into this textarea without any modifications
validations:
required: true
#
- id: version
type: input
attributes:
label: Vaultwarden Build Version
description: What version of Vaultwarden are you running?
placeholder: ex. v1.31.0 or v1.32.0-3466a804
validations:
required: true
#
- id: deployment
type: dropdown
attributes:
label: Deployment method
description: How did you deploy Vaultwarden?
multiple: false
options:
- Official Container Image
- Build from source
- OS Package (apt, yum/dnf, pacman, apk, nix, ...)
- Manually Extracted from Container Image
- Downloaded from GitHub Actions Release Workflow
- Other method
validations:
required: true
#
- id: deployment-other
type: textarea
attributes:
label: Custom deployment method
description: If you deployed Vaultwarden via any other method, please describe how.
#
- id: reverse-proxy
type: input
attributes:
label: Reverse Proxy
description: Are you using a reverse proxy, if so which and what version?
placeholder: ex. nginx 1.26.2, caddy 2.8.4, traefik 3.1.2, haproxy 3.0
validations:
required: true
#
- id: os
type: dropdown
attributes:
label: Host/Server Operating System
description: On what operating system are you running the Vaultwarden server?
multiple: false
options:
- Linux
- NAS/SAN
- Cloud
- Windows
- macOS
- Other
validations:
required: true
#
- id: os-version
type: input
attributes:
label: Operating System Version
description: What version of the operating system(s) are you seeing the problem on?
placeholder: ex. Arch Linux, Ubuntu 24.04, Kubernetes, Synology DSM 7.x, Windows 11
#
- id: clients
type: dropdown
attributes:
label: Clients
description: What client(s) are you seeing the problem on?
multiple: true
options:
- Web Vault
- Browser Extension
- CLI
- Desktop
- Android
- iOS
validations:
required: true
#
- id: client-version
type: input
attributes:
label: Client Version
description: What version(s) of the client(s) are you seeing the problem on?
placeholder: ex. CLI v2024.7.2, Firefox 130 - v2024.7.0
#
- id: reproduce
type: textarea
attributes:
label: Steps To Reproduce
description: How can we reproduce the behavior.
value: |
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. Click on '...'
5. Etc '...'
validations:
required: true
#
- id: expected
type: textarea
attributes:
label: Expected Result
description: A clear and concise description of what you expected to happen.
validations:
required: true
#
- id: actual
type: textarea
attributes:
label: Actual Result
description: A clear and concise description of what is happening.
validations:
required: true
#
- id: logs
type: textarea
attributes:
label: Logs
description: Provide the logs generated by Vaultwarden during the time this issue occurs.
render: text
#
- id: screenshots
type: textarea
attributes:
label: Screenshots or Videos
description: If applicable, add screenshots and/or a short video to help explain your problem.
#
- id: additional-context
type: textarea
attributes:
label: Additional Context
description: Add any other context about the problem here.

View File

@@ -1,8 +1,8 @@
blank_issues_enabled: false blank_issues_enabled: false
contact_links: contact_links:
- name: Discourse forum for vaultwarden - name: GitHub Discussions for Vaultwarden
url: https://vaultwarden.discourse.group/
about: Use this forum to request features or get help with usage/configuration.
- name: GitHub Discussions for vaultwarden
url: https://github.com/dani-garcia/vaultwarden/discussions url: https://github.com/dani-garcia/vaultwarden/discussions
about: An alternative to the Discourse forum, if this is easier for you. about: Use the discussions to request features or get help with usage/configuration.
- name: Discourse forum for Vaultwarden
url: https://vaultwarden.discourse.group/
about: An alternative to the GitHub Discussions, if this is easier for you.

View File

@@ -28,6 +28,7 @@ on:
jobs: jobs:
build: build:
# We use Ubuntu 22.04 here because this matches the library versions used within the Debian docker containers
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
timeout-minutes: 120 timeout-minutes: 120
# Make warnings errors, this is to prevent warnings slipping through. # Make warnings errors, this is to prevent warnings slipping through.
@@ -46,7 +47,7 @@ jobs:
steps: steps:
# Checkout the repo # Checkout the repo
- name: "Checkout" - name: "Checkout"
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 #v4.1.7 uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 #v4.2.1
# End Checkout the repo # End Checkout the repo
@@ -74,7 +75,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@21dc36fb71dd22e3317045c0c31a3f4249868b17 # master @ Jun 13, 2024, 6:20 PM GMT+2 uses: dtolnay/rust-toolchain@7b1c307e0dcbda6122208f10795a713336a9b35a # master @ Aug 8, 2024, 7:36 PM 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}}"
@@ -84,7 +85,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@21dc36fb71dd22e3317045c0c31a3f4249868b17 # master @ Jun 13, 2024, 6:20 PM GMT+2 uses: dtolnay/rust-toolchain@7b1c307e0dcbda6122208f10795a713336a9b35a # master @ Aug 8, 2024, 7:36 PM 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}}"

View File

@@ -8,14 +8,26 @@ on: [
jobs: jobs:
hadolint: hadolint:
name: Validate Dockerfile syntax name: Validate Dockerfile syntax
runs-on: ubuntu-22.04 runs-on: ubuntu-24.04
timeout-minutes: 30 timeout-minutes: 30
steps: steps:
# Checkout the repo # Checkout the repo
- name: Checkout - name: Checkout
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 #v4.2.1
# End Checkout the repo # End Checkout the repo
# Start Docker Buildx
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1
# https://github.com/moby/buildkit/issues/3969
# Also set max parallelism to 2, the default of 4 breaks GitHub Actions and causes OOMKills
with:
buildkitd-config-inline: |
[worker.oci]
max-parallelism = 2
driver-opts: |
network=host
# Download hadolint - https://github.com/hadolint/hadolint/releases # Download hadolint - https://github.com/hadolint/hadolint/releases
- name: Download hadolint - name: Download hadolint
shell: bash shell: bash
@@ -26,8 +38,18 @@ jobs:
HADOLINT_VERSION: 2.12.0 HADOLINT_VERSION: 2.12.0
# End Download hadolint # End Download hadolint
# Test Dockerfiles # Test Dockerfiles with hadolint
- name: Run hadolint - name: Run hadolint
shell: bash shell: bash
run: hadolint docker/Dockerfile.{debian,alpine} run: hadolint docker/Dockerfile.{debian,alpine}
# End Test Dockerfiles # End Test Dockerfiles with hadolint
# Test Dockerfiles with docker build checks
- name: Run docker build check
shell: bash
run: |
echo "Checking docker/Dockerfile.debian"
docker build --check . -f docker/Dockerfile.debian
echo "Checking docker/Dockerfile.alpine"
docker build --check . -f docker/Dockerfile.alpine
# End Test Dockerfiles with docker build checks

View File

@@ -13,7 +13,7 @@ jobs:
# Some checks to determine if we need to continue with building a new docker. # Some checks to determine if we need to continue with building a new docker.
# We will skip this check if we are creating a tag, because that has the same hash as a previous run already. # We will skip this check if we are creating a tag, because that has the same hash as a previous run already.
skip_check: skip_check:
runs-on: ubuntu-22.04 runs-on: ubuntu-24.04
if: ${{ github.repository == 'dani-garcia/vaultwarden' }} if: ${{ github.repository == 'dani-garcia/vaultwarden' }}
outputs: outputs:
should_skip: ${{ steps.skip_check.outputs.should_skip }} should_skip: ${{ steps.skip_check.outputs.should_skip }}
@@ -27,7 +27,7 @@ jobs:
if: ${{ github.ref_type == 'branch' }} if: ${{ github.ref_type == 'branch' }}
docker-build: docker-build:
runs-on: ubuntu-22.04 runs-on: ubuntu-24.04
timeout-minutes: 120 timeout-minutes: 120
needs: skip_check needs: skip_check
if: ${{ needs.skip_check.outputs.should_skip != 'true' && github.repository == 'dani-garcia/vaultwarden' }} if: ${{ needs.skip_check.outputs.should_skip != 'true' && github.repository == 'dani-garcia/vaultwarden' }}
@@ -58,7 +58,7 @@ jobs:
steps: steps:
# Checkout the repo # Checkout the repo
- name: Checkout - name: Checkout
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 #v4.2.1
with: with:
fetch-depth: 0 fetch-depth: 0
@@ -69,13 +69,13 @@ jobs:
# Start Docker Buildx # Start Docker Buildx
- name: Setup Docker Buildx - name: Setup Docker Buildx
uses: docker/setup-buildx-action@aa33708b10e362ff993539393ff100fa93ed6a27 # v3.5.0 uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1
# https://github.com/moby/buildkit/issues/3969 # https://github.com/moby/buildkit/issues/3969
# Also set max parallelism to 3, the default of 4 breaks GitHub Actions and causes OOMKills # Also set max parallelism to 2, the default of 4 breaks GitHub Actions and causes OOMKills
with: with:
buildkitd-config-inline: | buildkitd-config-inline: |
[worker.oci] [worker.oci]
max-parallelism = 3 max-parallelism = 2
driver-opts: | driver-opts: |
network=host network=host
@@ -165,7 +165,7 @@ jobs:
echo "CONTAINER_REGISTRIES=${CONTAINER_REGISTRIES:+${CONTAINER_REGISTRIES},}localhost:5000/vaultwarden/server" | tee -a "${GITHUB_ENV}" echo "CONTAINER_REGISTRIES=${CONTAINER_REGISTRIES:+${CONTAINER_REGISTRIES},}localhost:5000/vaultwarden/server" | tee -a "${GITHUB_ENV}"
- name: Bake ${{ matrix.base_image }} containers - name: Bake ${{ matrix.base_image }} containers
uses: docker/bake-action@a4d7f0b5b91c14a296d792d4ec53a9db17f02e67 # v5.5.0 uses: docker/bake-action@2e3d19baedb14545e5d41222653874f25d5b4dfb # v5.10.0
env: env:
BASE_TAGS: "${{ env.BASE_TAGS }}" BASE_TAGS: "${{ env.BASE_TAGS }}"
SOURCE_COMMIT: "${{ env.SOURCE_COMMIT }}" SOURCE_COMMIT: "${{ env.SOURCE_COMMIT }}"
@@ -223,28 +223,28 @@ jobs:
# Upload artifacts to Github Actions # Upload artifacts to Github Actions
- name: "Upload amd64 artifact" - name: "Upload amd64 artifact"
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
if: ${{ matrix.base_image == 'alpine' }} if: ${{ matrix.base_image == 'alpine' }}
with: with:
name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-amd64 name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-amd64
path: vaultwarden-amd64 path: vaultwarden-amd64
- name: "Upload arm64 artifact" - name: "Upload arm64 artifact"
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
if: ${{ matrix.base_image == 'alpine' }} if: ${{ matrix.base_image == 'alpine' }}
with: with:
name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-arm64 name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-arm64
path: vaultwarden-arm64 path: vaultwarden-arm64
- name: "Upload armv7 artifact" - name: "Upload armv7 artifact"
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
if: ${{ matrix.base_image == 'alpine' }} if: ${{ matrix.base_image == 'alpine' }}
with: with:
name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-armv7 name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-armv7
path: vaultwarden-armv7 path: vaultwarden-armv7
- name: "Upload armv6 artifact" - name: "Upload armv6 artifact"
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
if: ${{ matrix.base_image == 'alpine' }} if: ${{ matrix.base_image == 'alpine' }}
with: with:
name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-armv6 name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-armv6

View File

@@ -13,7 +13,7 @@ name: Cleanup
jobs: jobs:
releasecache-cleanup: releasecache-cleanup:
name: Releasecache Cleanup name: Releasecache Cleanup
runs-on: ubuntu-22.04 runs-on: ubuntu-24.04
continue-on-error: true continue-on-error: true
timeout-minutes: 30 timeout-minutes: 30
steps: steps:

View File

@@ -9,15 +9,18 @@ on:
pull_request: pull_request:
branches: [ "main" ] branches: [ "main" ]
schedule: schedule:
- cron: '00 12 * * *' - cron: '08 11 * * *'
permissions: permissions:
contents: read contents: read
jobs: jobs:
trivy-scan: trivy-scan:
# Only run this in the master repo and not on forks
# When all forks run this at the same time, it is causing `Too Many Requests` issues
if: ${{ github.repository == 'dani-garcia/vaultwarden' }}
name: Check name: Check
runs-on: ubuntu-22.04 runs-on: ubuntu-24.04
timeout-minutes: 30 timeout-minutes: 30
permissions: permissions:
contents: read contents: read
@@ -25,10 +28,10 @@ jobs:
actions: read actions: read
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 #v4.1.7 uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 #v4.2.1
- name: Run Trivy vulnerability scanner - name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@6e7b7d1fd3e4fef0c5fa8cce1229c54b2c9bd0d8 # v0.24.0 uses: aquasecurity/trivy-action@5681af892cd0f4997658e2bacc62bd0a894cf564 # v0.27.0
with: with:
scan-type: repo scan-type: repo
ignore-unfixed: true ignore-unfixed: true
@@ -37,6 +40,6 @@ jobs:
severity: CRITICAL,HIGH severity: CRITICAL,HIGH
- name: Upload Trivy scan results to GitHub Security tab - name: Upload Trivy scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@2bbafcdd7fbf96243689e764c2f15d9735164f33 # v3.25.10 uses: github/codeql-action/upload-sarif@2bbafcdd7fbf96243689e764c2f15d9735164f33 # v3.26.6
with: with:
sarif_file: 'trivy-results.sarif' sarif_file: 'trivy-results.sarif'

1203
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.79.0" rust-version = "1.80.0"
resolver = "2" resolver = "2"
repository = "https://github.com/dani-garcia/vaultwarden" repository = "https://github.com/dani-garcia/vaultwarden"
@@ -18,37 +18,37 @@ build = "build.rs"
enable_syslog = [] enable_syslog = []
mysql = ["diesel/mysql", "diesel_migrations/mysql"] mysql = ["diesel/mysql", "diesel_migrations/mysql"]
postgresql = ["diesel/postgres", "diesel_migrations/postgres"] postgresql = ["diesel/postgres", "diesel_migrations/postgres"]
sqlite = ["diesel/sqlite", "diesel_migrations/sqlite", "libsqlite3-sys"] sqlite = ["diesel/sqlite", "diesel_migrations/sqlite", "dep:libsqlite3-sys"]
# Enable to use a vendored and statically linked openssl # Enable to use a vendored and statically linked openssl
vendored_openssl = ["openssl/vendored"] vendored_openssl = ["openssl/vendored"]
# Enable MiMalloc memory allocator to replace the default malloc # Enable MiMalloc memory allocator to replace the default malloc
# This can improve performance for Alpine builds # This can improve performance for Alpine builds
enable_mimalloc = ["mimalloc"] enable_mimalloc = ["dep:mimalloc"]
# This is a development dependency, and should only be used during development! # This is a development dependency, and should only be used during development!
# It enables the usage of the diesel_logger crate, which is able to output the generated queries. # It enables the usage of the diesel_logger crate, which is able to output the generated queries.
# You also need to set an env variable `QUERY_LOGGER=1` to fully activate this so you do not have to re-compile # You also need to set an env variable `QUERY_LOGGER=1` to fully activate this so you do not have to re-compile
# if you want to turn off the logging for a specific run. # if you want to turn off the logging for a specific run.
query_logger = ["diesel_logger"] query_logger = ["dep:diesel_logger"]
# Enable unstable features, requires nightly # Enable unstable features, requires nightly
# Currently only used to enable rusts official ip support # Currently only used to enable rusts official ip support
unstable = [] unstable = []
[target."cfg(not(windows))".dependencies] [target."cfg(unix)".dependencies]
# Logging # Logging
syslog = "6.1.1" syslog = "6.1.1"
[dependencies] [dependencies]
# Logging # Logging
log = "0.4.22" log = "0.4.22"
fern = { version = "0.6.2", features = ["syslog-6", "reopen-1"] } fern = { version = "0.7.0", features = ["syslog-6", "reopen-1"] }
tracing = { version = "0.1.40", features = ["log"] } # Needed to have lettre and webauthn-rs trace logging to work tracing = { version = "0.1.40", features = ["log"] } # Needed to have lettre and webauthn-rs trace logging to work
# A `dotenv` implementation for Rust # A `dotenv` implementation for Rust
dotenvy = { version = "0.15.7", default-features = false } dotenvy = { version = "0.15.7", default-features = false }
# Lazy initialization # Lazy initialization
once_cell = "1.19.0" once_cell = "1.20.2"
# Numerical libraries # Numerical libraries
num-traits = "0.2.19" num-traits = "0.2.19"
@@ -63,34 +63,34 @@ rocket_ws = { version ="0.1.1" }
rmpv = "1.3.0" # MessagePack library rmpv = "1.3.0" # MessagePack library
# Concurrent HashMap used for WebSocket messaging and favicons # Concurrent HashMap used for WebSocket messaging and favicons
dashmap = "6.0.1" dashmap = "6.1.0"
# Async futures # Async futures
futures = "0.3.30" futures = "0.3.31"
tokio = { version = "1.39.2", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] } tokio = { version = "1.41.0", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] }
# A generic serialization/deserialization framework # A generic serialization/deserialization framework
serde = { version = "1.0.204", features = ["derive"] } serde = { version = "1.0.213", features = ["derive"] }
serde_json = "1.0.122" serde_json = "1.0.132"
# A safe, extensible ORM and Query builder # A safe, extensible ORM and Query builder
diesel = { version = "2.2.2", features = ["chrono", "r2d2", "numeric"] } diesel = { version = "2.2.4", features = ["chrono", "r2d2", "numeric"] }
diesel_migrations = "2.2.0" diesel_migrations = "2.2.0"
diesel_logger = { version = "0.3.0", optional = true } diesel_logger = { version = "0.3.0", optional = true }
# Bundled/Static SQLite # Bundled/Static SQLite
libsqlite3-sys = { version = "0.29.0", features = ["bundled"], optional = true } libsqlite3-sys = { version = "0.30.1", 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.17.8" ring = "0.17.8"
# UUID generation # UUID generation
uuid = { version = "1.10.0", features = ["v4"] } uuid = { version = "1.11.0", features = ["v4"] }
# Date and time libraries # Date and time libraries
chrono = { version = "0.4.38", features = ["clock", "serde"], default-features = false } chrono = { version = "0.4.38", features = ["clock", "serde"], default-features = false }
chrono-tz = "0.9.0" chrono-tz = "0.10.0"
time = "0.3.36" time = "0.3.36"
# Job scheduler # Job scheduler
@@ -115,22 +115,22 @@ webauthn-rs = "0.3.2"
url = "2.5.2" url = "2.5.2"
# Email libraries # Email libraries
lettre = { version = "0.11.7", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "tokio1-native-tls", "hostname", "tracing", "tokio1"], default-features = false } lettre = { version = "0.11.10", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "tokio1-native-tls", "hostname", "tracing", "tokio1"], default-features = false }
percent-encoding = "2.3.1" # URL encoding library used for URL's in the emails percent-encoding = "2.3.1" # URL encoding library used for URL's in the emails
email_address = "0.2.9" email_address = "0.2.9"
# HTML Template library # HTML Template library
handlebars = { version = "6.0.0", features = ["dir_source"] } handlebars = { version = "6.1.0", 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.12.5", features = ["native-tls-alpn", "stream", "json", "gzip", "brotli", "socks", "cookies"] } reqwest = { version = "0.12.8", features = ["native-tls-alpn", "stream", "json", "gzip", "brotli", "socks", "cookies"] }
hickory-resolver = "0.24.1" hickory-resolver = "0.24.1"
# Favicon extraction libraries # Favicon extraction libraries
html5gum = "0.5.7" html5gum = "0.5.7"
regex = { version = "1.10.6", features = ["std", "perf", "unicode-perl"], default-features = false } regex = { version = "1.11.0", features = ["std", "perf", "unicode-perl"], default-features = false }
data-url = "0.3.1" data-url = "0.3.1"
bytes = "1.7.1" bytes = "1.8.0"
# Cache function results (Used for version check and favicon fetching) # Cache function results (Used for version check and favicon fetching)
cached = { version = "0.53.1", features = ["async"] } cached = { version = "0.53.1", features = ["async"] }
@@ -140,14 +140,14 @@ cookie = "0.18.1"
cookie_store = "0.21.0" cookie_store = "0.21.0"
# Used by U2F, JWT and PostgreSQL # Used by U2F, JWT and PostgreSQL
openssl = "0.10.66" openssl = "0.10.68"
# 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.15" paste = "1.0.15"
governor = "0.6.3" governor = "0.7.0"
# Check client versions for specific features. # Check client versions for specific features.
semver = "1.0.23" semver = "1.0.23"
@@ -155,7 +155,7 @@ semver = "1.0.23"
# 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.43", features = ["secure"], default-features = false, optional = true } mimalloc = { version = "0.1.43", features = ["secure"], default-features = false, optional = true }
which = "6.0.2" which = "6.0.3"
# Argon2 library with support for the PHC format # Argon2 library with support for the PHC format
argon2 = "0.5.3" argon2 = "0.5.3"
@@ -198,33 +198,46 @@ lto = "thin"
codegen-units = 16 codegen-units = 16
# Linting config # Linting config
# https://doc.rust-lang.org/rustc/lints/groups.html
[lints.rust] [lints.rust]
# Forbid # Forbid
unsafe_code = "forbid" unsafe_code = "forbid"
non_ascii_idents = "forbid" non_ascii_idents = "forbid"
# Deny # Deny
deprecated_in_future = "deny"
future_incompatible = { level = "deny", priority = -1 } future_incompatible = { level = "deny", priority = -1 }
keyword_idents = { level = "deny", priority = -1 }
let_underscore = { level = "deny", priority = -1 }
noop_method_call = "deny" noop_method_call = "deny"
refining_impl_trait = { level = "deny", priority = -1 }
rust_2018_idioms = { level = "deny", priority = -1 } rust_2018_idioms = { level = "deny", priority = -1 }
rust_2021_compatibility = { level = "deny", priority = -1 } rust_2021_compatibility = { level = "deny", priority = -1 }
# rust_2024_compatibility = { level = "deny", priority = -1 } # Enable once we are at MSRV 1.81.0
single_use_lifetimes = "deny"
trivial_casts = "deny" trivial_casts = "deny"
trivial_numeric_casts = "deny" trivial_numeric_casts = "deny"
unused = { level = "deny", priority = -1 } unused = { level = "deny", priority = -1 }
unused_import_braces = "deny" unused_import_braces = "deny"
unused_lifetimes = "deny" unused_lifetimes = "deny"
deprecated_in_future = "deny" unused_qualifications = "deny"
variant_size_differences = "deny"
# The lints below are part of the rust_2024_compatibility group
static-mut-refs = "deny"
unsafe-op-in-unsafe-fn = "deny"
# https://rust-lang.github.io/rust-clippy/stable/index.html
[lints.clippy] [lints.clippy]
# Allow # Warn
# We need this since Rust v1.76+, since it has some bugs dbg_macro = "warn"
# https://github.com/rust-lang/rust-clippy/issues/12016 todo = "warn"
blocks_in_conditions = "allow"
# Deny # Deny
case_sensitive_file_extension_comparisons = "deny"
cast_lossless = "deny" cast_lossless = "deny"
clone_on_ref_ptr = "deny" clone_on_ref_ptr = "deny"
equatable_if_let = "deny" equatable_if_let = "deny"
filter_map_next = "deny"
float_cmp_const = "deny" float_cmp_const = "deny"
inefficient_to_string = "deny" inefficient_to_string = "deny"
iter_on_empty_collections = "deny" iter_on_empty_collections = "deny"
@@ -234,13 +247,18 @@ macro_use_imports = "deny"
manual_assert = "deny" manual_assert = "deny"
manual_instant_elapsed = "deny" manual_instant_elapsed = "deny"
manual_string_new = "deny" manual_string_new = "deny"
match_on_vec_items = "deny"
match_wildcard_for_single_variants = "deny" match_wildcard_for_single_variants = "deny"
mem_forget = "deny" mem_forget = "deny"
needless_continue = "deny"
needless_lifetimes = "deny" needless_lifetimes = "deny"
option_option = "deny"
string_add_assign = "deny" string_add_assign = "deny"
string_to_string = "deny" string_to_string = "deny"
unnecessary_join = "deny" unnecessary_join = "deny"
unnecessary_self_imports = "deny" unnecessary_self_imports = "deny"
unnested_or_patterns = "deny"
unused_async = "deny" unused_async = "deny"
unused_self = "deny"
verbose_file_reads = "deny" verbose_file_reads = "deny"
zero_sized_map_values = "deny" zero_sized_map_values = "deny"

View File

@@ -39,7 +39,11 @@ Thank you for helping keep Vaultwarden and our users safe!
# How to contact us # How to contact us
- You can contact us on Matrix https://matrix.to/#/#vaultwarden:matrix.org (user: `@danig:matrix.org`) - You can contact us on Matrix https://matrix.to/#/#vaultwarden:matrix.org (users: `@danig:matrix.org` and/or `@blackdex:matrix.org`)
- You can send an ![security-contact](/.github/security-contact.gif) to report a security issue. - You can send an ![security-contact](/.github/security-contact.gif) to report a security issue.<br>
- If you want to send an encrypted email you can use the following GPG key:<br> If you want to send an encrypted email you can use the following GPG key: 13BB3A34C9E380258CE43D595CB150B31F6426BC<br>
https://keyserver.ubuntu.com/pks/lookup?search=0xB9B7A108373276BF3C0406F9FC8A7D14C3CD543A&fingerprint=on&op=index It can be found on several public GPG key servers.<br>
* https://keys.openpgp.org/search?q=security%40vaultwarden.org
* https://keys.mailvelope.com/pks/lookup?op=get&search=security%40vaultwarden.org
* https://pgpkeys.eu/pks/lookup?search=security%40vaultwarden.org&fingerprint=on&op=index
* https://keyserver.ubuntu.com/pks/lookup?search=security%40vaultwarden.org&fingerprint=on&op=index

View File

@@ -1,10 +1,11 @@
--- ---
vault_version: "v2024.6.2b" vault_version: "v2024.6.2c"
vault_image_digest: "sha256:25a8b48792a3d2c16ddaea493eee93a0b6785648f2d8782c7537d198cb41be55" vault_image_digest: "sha256:409ab328ca931439cb916b388a4bb784bd44220717aaf74cf71620c23e34fc2b"
# Cross Compile Docker Helper Scripts v1.4.0 # Cross Compile Docker Helper Scripts v1.5.0
# We use the linux/amd64 platform shell scripts since there is no difference between the different platform scripts # We use the linux/amd64 platform shell scripts since there is no difference between the different platform scripts
xx_image_digest: "sha256:0cd3f05c72d6c9b038eb135f91376ee1169ef3a330d34e418e65e2a5c2e9c0d4" # https://github.com/tonistiigi/xx | https://hub.docker.com/r/tonistiigi/xx/tags
rust_version: 1.80.1 # Rust version to be used xx_image_digest: "sha256:1978e7a58a1777cb0ef0dde76bad60b7914b21da57cfa88047875e4f364297aa"
rust_version: 1.82.0 # Rust version to be used
debian_version: bookworm # Debian release name to be used debian_version: bookworm # Debian release name to be used
alpine_version: "3.20" # Alpine version to be used alpine_version: "3.20" # Alpine version to be used
# For which platforms/architectures will we try to build images # For which platforms/architectures will we try to build images

View File

@@ -19,23 +19,23 @@
# - 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 docker.io/vaultwarden/web-vault:v2024.6.2b # $ docker pull docker.io/vaultwarden/web-vault:v2024.6.2c
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2024.6.2b # $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2024.6.2c
# [docker.io/vaultwarden/web-vault@sha256:25a8b48792a3d2c16ddaea493eee93a0b6785648f2d8782c7537d198cb41be55] # [docker.io/vaultwarden/web-vault@sha256:409ab328ca931439cb916b388a4bb784bd44220717aaf74cf71620c23e34fc2b]
# #
# - Conversely, to get the tag name from the digest: # - Conversely, to get the tag name from the digest:
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:25a8b48792a3d2c16ddaea493eee93a0b6785648f2d8782c7537d198cb41be55 # $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:409ab328ca931439cb916b388a4bb784bd44220717aaf74cf71620c23e34fc2b
# [docker.io/vaultwarden/web-vault:v2024.6.2b] # [docker.io/vaultwarden/web-vault:v2024.6.2c]
# #
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:25a8b48792a3d2c16ddaea493eee93a0b6785648f2d8782c7537d198cb41be55 AS vault FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:409ab328ca931439cb916b388a4bb784bd44220717aaf74cf71620c23e34fc2b AS vault
########################## ALPINE BUILD IMAGES ########################## ########################## ALPINE BUILD IMAGES ##########################
## NOTE: The Alpine Base Images do not support other platforms then linux/amd64 ## NOTE: The Alpine Base Images do not support other platforms then linux/amd64
## And for Alpine we define all build images here, they will only be loaded when actually used ## And for Alpine we define all build images here, they will only be loaded when actually used
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.80.1 AS build_amd64 FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.82.0 AS build_amd64
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.80.1 AS build_arm64 FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.82.0 AS build_arm64
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.80.1 AS build_armv7 FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.82.0 AS build_armv7
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.80.1 AS build_armv6 FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.82.0 AS build_armv6
########################## BUILD IMAGE ########################## ########################## BUILD IMAGE ##########################
# hadolint ignore=DL3006 # hadolint ignore=DL3006

View File

@@ -19,24 +19,24 @@
# - 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 docker.io/vaultwarden/web-vault:v2024.6.2b # $ docker pull docker.io/vaultwarden/web-vault:v2024.6.2c
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2024.6.2b # $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2024.6.2c
# [docker.io/vaultwarden/web-vault@sha256:25a8b48792a3d2c16ddaea493eee93a0b6785648f2d8782c7537d198cb41be55] # [docker.io/vaultwarden/web-vault@sha256:409ab328ca931439cb916b388a4bb784bd44220717aaf74cf71620c23e34fc2b]
# #
# - Conversely, to get the tag name from the digest: # - Conversely, to get the tag name from the digest:
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:25a8b48792a3d2c16ddaea493eee93a0b6785648f2d8782c7537d198cb41be55 # $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:409ab328ca931439cb916b388a4bb784bd44220717aaf74cf71620c23e34fc2b
# [docker.io/vaultwarden/web-vault:v2024.6.2b] # [docker.io/vaultwarden/web-vault:v2024.6.2c]
# #
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:25a8b48792a3d2c16ddaea493eee93a0b6785648f2d8782c7537d198cb41be55 AS vault FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:409ab328ca931439cb916b388a4bb784bd44220717aaf74cf71620c23e34fc2b AS vault
########################## Cross Compile Docker Helper Scripts ########################## ########################## Cross Compile Docker Helper Scripts ##########################
## We use the linux/amd64 no matter which Build Platform, since these are all bash scripts ## We use the linux/amd64 no matter which Build Platform, since these are all bash scripts
## And these bash scripts do not have any significant difference if at all ## And these bash scripts do not have any significant difference if at all
FROM --platform=linux/amd64 docker.io/tonistiigi/xx@sha256:0cd3f05c72d6c9b038eb135f91376ee1169ef3a330d34e418e65e2a5c2e9c0d4 AS xx FROM --platform=linux/amd64 docker.io/tonistiigi/xx@sha256:1978e7a58a1777cb0ef0dde76bad60b7914b21da57cfa88047875e4f364297aa AS xx
########################## BUILD IMAGE ########################## ########################## BUILD IMAGE ##########################
# hadolint ignore=DL3006 # hadolint ignore=DL3006
FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.80.1-slim-bookworm AS build FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.82.0-slim-bookworm AS build
COPY --from=xx / / COPY --from=xx / /
ARG TARGETARCH ARG TARGETARCH
ARG TARGETVARIANT ARG TARGETVARIANT

View File

@@ -1,5 +1,9 @@
#!/bin/sh #!/bin/sh
if [ -n "${UMASK}" ]; then
umask "${UMASK}"
fi
if [ -r /etc/vaultwarden.sh ]; then if [ -r /etc/vaultwarden.sh ]; then
. /etc/vaultwarden.sh . /etc/vaultwarden.sh
elif [ -r /etc/bitwarden_rs.sh ]; then elif [ -r /etc/bitwarden_rs.sh ]; then

View File

@@ -0,0 +1 @@
ALTER TABLE `twofactor_incomplete` DROP COLUMN `device_type`;

View File

@@ -0,0 +1 @@
ALTER TABLE twofactor_incomplete ADD COLUMN device_type INTEGER NOT NULL DEFAULT 14; -- 14 = Unknown Browser

View File

@@ -0,0 +1 @@
ALTER TABLE twofactor_incomplete DROP COLUMN device_type;

View File

@@ -0,0 +1 @@
ALTER TABLE twofactor_incomplete ADD COLUMN device_type INTEGER NOT NULL DEFAULT 14; -- 14 = Unknown Browser

View File

@@ -0,0 +1 @@
ALTER TABLE `twofactor_incomplete` DROP COLUMN `device_type`;

View File

@@ -0,0 +1 @@
ALTER TABLE twofactor_incomplete ADD COLUMN device_type INTEGER NOT NULL DEFAULT 14; -- 14 = Unknown Browser

View File

@@ -1,4 +1,4 @@
[toolchain] [toolchain]
channel = "1.80.1" channel = "1.82.0"
components = [ "rustfmt", "clippy" ] components = [ "rustfmt", "clippy" ]
profile = "minimal" profile = "minimal"

View File

@@ -25,7 +25,8 @@ use crate::{
http_client::make_http_request, http_client::make_http_request,
mail, mail,
util::{ util::{
container_base_image, format_naive_datetime_local, get_display_size, is_running_in_container, NumberOrString, container_base_image, format_naive_datetime_local, get_display_size, get_web_vault_version,
is_running_in_container, NumberOrString,
}, },
CONFIG, VERSION, CONFIG, VERSION,
}; };
@@ -196,7 +197,7 @@ fn post_admin_login(
let cookie = Cookie::build((COOKIE_NAME, jwt)) let cookie = Cookie::build((COOKIE_NAME, jwt))
.path(admin_path()) .path(admin_path())
.max_age(rocket::time::Duration::minutes(CONFIG.admin_session_lifetime())) .max_age(time::Duration::minutes(CONFIG.admin_session_lifetime()))
.same_site(SameSite::Strict) .same_site(SameSite::Strict)
.http_only(true) .http_only(true)
.secure(secure.https); .secure(secure.https);
@@ -297,7 +298,7 @@ async fn invite_user(data: Json<InviteData>, _token: AdminToken, mut conn: DbCon
async fn _generate_invite(user: &User, conn: &mut DbConn) -> EmptyResult { async fn _generate_invite(user: &User, conn: &mut DbConn) -> EmptyResult {
if CONFIG.mail_enabled() { if CONFIG.mail_enabled() {
mail::send_invite(&user.email, &user.uuid, None, None, &CONFIG.invitation_org_name(), None).await mail::send_invite(user, None, None, &CONFIG.invitation_org_name(), None).await
} else { } else {
let invitation = Invitation::new(&user.email); let invitation = Invitation::new(&user.email);
invitation.save(conn).await invitation.save(conn).await
@@ -473,7 +474,7 @@ async fn resend_user_invite(uuid: &str, _token: AdminToken, mut conn: DbConn) ->
} }
if CONFIG.mail_enabled() { if CONFIG.mail_enabled() {
mail::send_invite(&user.email, &user.uuid, None, None, &CONFIG.invitation_org_name(), None).await mail::send_invite(&user, None, None, &CONFIG.invitation_org_name(), None).await
} else { } else {
Ok(()) Ok(())
} }
@@ -575,11 +576,6 @@ async fn delete_organization(uuid: &str, _token: AdminToken, mut conn: DbConn) -
org.delete(&mut conn).await org.delete(&mut conn).await
} }
#[derive(Deserialize)]
struct WebVaultVersion {
version: String,
}
#[derive(Deserialize)] #[derive(Deserialize)]
struct GitRelease { struct GitRelease {
tag_name: String, tag_name: String,
@@ -679,18 +675,6 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn)
use chrono::prelude::*; use chrono::prelude::*;
use std::net::ToSocketAddrs; use std::net::ToSocketAddrs;
// Get current running versions
let web_vault_version: WebVaultVersion =
match std::fs::read_to_string(format!("{}/{}", CONFIG.web_vault_folder(), "vw-version.json")) {
Ok(s) => serde_json::from_str(&s)?,
_ => match std::fs::read_to_string(format!("{}/{}", CONFIG.web_vault_folder(), "version.json")) {
Ok(s) => serde_json::from_str(&s)?,
_ => WebVaultVersion {
version: String::from("Version file missing"),
},
},
};
// Execute some environment checks // Execute some environment checks
let running_within_container = is_running_in_container(); let running_within_container = is_running_in_container();
let has_http_access = has_http_access().await; let has_http_access = has_http_access().await;
@@ -710,13 +694,16 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn)
let ip_header_name = &ip_header.0.unwrap_or_default(); let ip_header_name = &ip_header.0.unwrap_or_default();
// Get current running versions
let web_vault_version = get_web_vault_version();
let diagnostics_json = json!({ let diagnostics_json = json!({
"dns_resolved": dns_resolved, "dns_resolved": dns_resolved,
"current_release": VERSION, "current_release": VERSION,
"latest_release": latest_release, "latest_release": latest_release,
"latest_commit": latest_commit, "latest_commit": latest_commit,
"web_vault_enabled": &CONFIG.web_vault_enabled(), "web_vault_enabled": &CONFIG.web_vault_enabled(),
"web_vault_version": web_vault_version.version.trim_start_matches('v'), "web_vault_version": web_vault_version,
"latest_web_build": latest_web_build, "latest_web_build": latest_web_build,
"running_within_container": running_within_container, "running_within_container": running_within_container,
"container_base_image": if running_within_container { container_base_image() } else { "Not applicable" }, "container_base_image": if running_within_container { container_base_image() } else { "Not applicable" },
@@ -730,8 +717,8 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn)
"db_version": get_sql_server_version(&mut conn).await, "db_version": get_sql_server_version(&mut conn).await,
"admin_url": format!("{}/diagnostics", admin_url()), "admin_url": format!("{}/diagnostics", admin_url()),
"overrides": &CONFIG.get_overrides().join(", "), "overrides": &CONFIG.get_overrides().join(", "),
"host_arch": std::env::consts::ARCH, "host_arch": env::consts::ARCH,
"host_os": std::env::consts::OS, "host_os": env::consts::OS,
"server_time_local": Local::now().format("%Y-%m-%d %H:%M:%S %Z").to_string(), "server_time_local": Local::now().format("%Y-%m-%d %H:%M:%S %Z").to_string(),
"server_time": Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string(), // Run the server date/time check as late as possible to minimize the time difference "server_time": Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string(), // Run the server date/time check as late as possible to minimize the time difference
"ntp_time": get_ntp_time(has_http_access).await, // Run the ntp check as late as possible to minimize the time difference "ntp_time": get_ntp_time(has_http_access).await, // Run the ntp check as late as possible to minimize the time difference
@@ -750,18 +737,27 @@ fn get_diagnostics_config(_token: AdminToken) -> Json<Value> {
#[post("/config", data = "<data>")] #[post("/config", data = "<data>")]
fn post_config(data: Json<ConfigBuilder>, _token: AdminToken) -> EmptyResult { fn post_config(data: Json<ConfigBuilder>, _token: AdminToken) -> EmptyResult {
let data: ConfigBuilder = data.into_inner(); let data: ConfigBuilder = data.into_inner();
CONFIG.update_config(data) if let Err(e) = CONFIG.update_config(data) {
err!(format!("Unable to save config: {e:?}"))
}
Ok(())
} }
#[post("/config/delete")] #[post("/config/delete")]
fn delete_config(_token: AdminToken) -> EmptyResult { fn delete_config(_token: AdminToken) -> EmptyResult {
CONFIG.delete_user_config() if let Err(e) = CONFIG.delete_user_config() {
err!(format!("Unable to delete config: {e:?}"))
}
Ok(())
} }
#[post("/config/backup_db")] #[post("/config/backup_db")]
async fn backup_db(_token: AdminToken, mut conn: DbConn) -> EmptyResult { async fn backup_db(_token: AdminToken, mut conn: DbConn) -> ApiResult<String> {
if *CAN_BACKUP { if *CAN_BACKUP {
backup_database(&mut conn).await match backup_database(&mut conn).await {
Ok(f) => Ok(format!("Backup to '{f}' was successful")),
Err(e) => err!(format!("Backup was unsuccessful {e}")),
}
} else { } else {
err!("Can't back up current DB (Only SQLite supports this feature)"); err!("Can't back up current DB (Only SQLite supports this feature)");
} }

View File

@@ -1,5 +1,5 @@
use crate::db::DbPool; use crate::db::DbPool;
use chrono::Utc; use chrono::{SecondsFormat, Utc};
use rocket::serde::json::Json; use rocket::serde::json::Json;
use serde_json::Value; use serde_json::Value;
@@ -112,7 +112,7 @@ async fn is_email_2fa_required(org_user_uuid: Option<String>, conn: &mut DbConn)
return true; return true;
} }
if org_user_uuid.is_some() { if org_user_uuid.is_some() {
return OrgPolicy::is_enabled_by_org(&org_user_uuid.unwrap(), OrgPolicyType::TwoFactorAuthentication, conn) return OrgPolicy::is_enabled_for_member(&org_user_uuid.unwrap(), OrgPolicyType::TwoFactorAuthentication, conn)
.await; .await;
} }
false false
@@ -223,7 +223,7 @@ pub async fn _register(data: Json<RegisterData>, mut conn: DbConn) -> JsonResult
} }
if verified_by_invite && is_email_2fa_required(data.organization_user_id, &mut conn).await { if verified_by_invite && is_email_2fa_required(data.organization_user_id, &mut conn).await {
let _ = email::activate_email_2fa(&user, &mut conn).await; email::activate_email_2fa(&user, &mut conn).await.ok();
} }
} }
@@ -232,7 +232,7 @@ pub async fn _register(data: Json<RegisterData>, mut conn: DbConn) -> JsonResult
// accept any open emergency access invitations // accept any open emergency access invitations
if !CONFIG.mail_enabled() && CONFIG.emergency_access_allowed() { if !CONFIG.mail_enabled() && CONFIG.emergency_access_allowed() {
for mut emergency_invite in EmergencyAccess::find_all_invited_by_grantee_email(&user.email, &mut conn).await { for mut emergency_invite in EmergencyAccess::find_all_invited_by_grantee_email(&user.email, &mut conn).await {
let _ = emergency_invite.accept_invite(&user.uuid, &user.email, &mut conn).await; emergency_invite.accept_invite(&user.uuid, &user.email, &mut conn).await.ok();
} }
} }
@@ -490,7 +490,7 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn,
// Bitwarden does not process the import if there is one item invalid. // Bitwarden does not process the import if there is one item invalid.
// Since we check for the size of the encrypted note length, we need to do that here to pre-validate it. // Since we check for the size of the encrypted note length, we need to do that here to pre-validate it.
// TODO: See if we can optimize the whole cipher adding/importing and prevent duplicate code and checks. // TODO: See if we can optimize the whole cipher adding/importing and prevent duplicate code and checks.
Cipher::validate_notes(&data.ciphers)?; Cipher::validate_cipher_data(&data.ciphers)?;
let user_uuid = &headers.user.uuid; let user_uuid = &headers.user.uuid;
@@ -1038,7 +1038,7 @@ async fn put_device_token(uuid: &str, data: Json<PushToken>, headers: Headers, m
return Ok(()); return Ok(());
} else { } else {
// Try to unregister already registered device // Try to unregister already registered device
let _ = unregister_push_device(device.push_uuid).await; unregister_push_device(device.push_uuid).await.ok();
} }
// clear the push_uuid // clear the push_uuid
device.push_uuid = None; device.push_uuid = None;
@@ -1123,7 +1123,7 @@ async fn post_auth_request(
"requestIpAddress": auth_request.request_ip, "requestIpAddress": auth_request.request_ip,
"key": null, "key": null,
"masterPasswordHash": null, "masterPasswordHash": null,
"creationDate": auth_request.creation_date.and_utc(), "creationDate": auth_request.creation_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true),
"responseDate": null, "responseDate": null,
"requestApproved": false, "requestApproved": false,
"origin": CONFIG.domain_origin(), "origin": CONFIG.domain_origin(),
@@ -1140,7 +1140,9 @@ async fn get_auth_request(uuid: &str, mut conn: DbConn) -> JsonResult {
} }
}; };
let response_date_utc = auth_request.response_date.map(|response_date| response_date.and_utc()); let response_date_utc = auth_request
.response_date
.map(|response_date| response_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true));
Ok(Json(json!( Ok(Json(json!(
{ {
@@ -1150,7 +1152,7 @@ async fn get_auth_request(uuid: &str, mut conn: DbConn) -> JsonResult {
"requestIpAddress": auth_request.request_ip, "requestIpAddress": auth_request.request_ip,
"key": auth_request.enc_key, "key": auth_request.enc_key,
"masterPasswordHash": auth_request.master_password_hash, "masterPasswordHash": auth_request.master_password_hash,
"creationDate": auth_request.creation_date.and_utc(), "creationDate": auth_request.creation_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true),
"responseDate": response_date_utc, "responseDate": response_date_utc,
"requestApproved": auth_request.approved, "requestApproved": auth_request.approved,
"origin": CONFIG.domain_origin(), "origin": CONFIG.domain_origin(),
@@ -1195,7 +1197,9 @@ async fn put_auth_request(
nt.send_auth_response(&auth_request.user_uuid, &auth_request.uuid, data.device_identifier, &mut conn).await; nt.send_auth_response(&auth_request.user_uuid, &auth_request.uuid, data.device_identifier, &mut conn).await;
} }
let response_date_utc = auth_request.response_date.map(|response_date| response_date.and_utc()); let response_date_utc = auth_request
.response_date
.map(|response_date| response_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true));
Ok(Json(json!( Ok(Json(json!(
{ {
@@ -1205,7 +1209,7 @@ async fn put_auth_request(
"requestIpAddress": auth_request.request_ip, "requestIpAddress": auth_request.request_ip,
"key": auth_request.enc_key, "key": auth_request.enc_key,
"masterPasswordHash": auth_request.master_password_hash, "masterPasswordHash": auth_request.master_password_hash,
"creationDate": auth_request.creation_date.and_utc(), "creationDate": auth_request.creation_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true),
"responseDate": response_date_utc, "responseDate": response_date_utc,
"requestApproved": auth_request.approved, "requestApproved": auth_request.approved,
"origin": CONFIG.domain_origin(), "origin": CONFIG.domain_origin(),
@@ -1227,7 +1231,9 @@ async fn get_auth_request_response(uuid: &str, code: &str, mut conn: DbConn) ->
err!("Access code invalid doesn't exist") err!("Access code invalid doesn't exist")
} }
let response_date_utc = auth_request.response_date.map(|response_date| response_date.and_utc()); let response_date_utc = auth_request
.response_date
.map(|response_date| response_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true));
Ok(Json(json!( Ok(Json(json!(
{ {
@@ -1237,7 +1243,7 @@ async fn get_auth_request_response(uuid: &str, code: &str, mut conn: DbConn) ->
"requestIpAddress": auth_request.request_ip, "requestIpAddress": auth_request.request_ip,
"key": auth_request.enc_key, "key": auth_request.enc_key,
"masterPasswordHash": auth_request.master_password_hash, "masterPasswordHash": auth_request.master_password_hash,
"creationDate": auth_request.creation_date.and_utc(), "creationDate": auth_request.creation_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true),
"responseDate": response_date_utc, "responseDate": response_date_utc,
"requestApproved": auth_request.approved, "requestApproved": auth_request.approved,
"origin": CONFIG.domain_origin(), "origin": CONFIG.domain_origin(),
@@ -1255,7 +1261,7 @@ async fn get_auth_requests(headers: Headers, mut conn: DbConn) -> JsonResult {
.iter() .iter()
.filter(|request| request.approved.is_none()) .filter(|request| request.approved.is_none())
.map(|request| { .map(|request| {
let response_date_utc = request.response_date.map(|response_date| response_date.and_utc()); let response_date_utc = request.response_date.map(|response_date| response_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true));
json!({ json!({
"id": request.uuid, "id": request.uuid,
@@ -1264,7 +1270,7 @@ async fn get_auth_requests(headers: Headers, mut conn: DbConn) -> JsonResult {
"requestIpAddress": request.request_ip, "requestIpAddress": request.request_ip,
"key": request.enc_key, "key": request.enc_key,
"masterPasswordHash": request.master_password_hash, "masterPasswordHash": request.master_password_hash,
"creationDate": request.creation_date.and_utc(), "creationDate": request.creation_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true),
"responseDate": response_date_utc, "responseDate": response_date_utc,
"requestApproved": request.approved, "requestApproved": request.approved,
"origin": CONFIG.domain_origin(), "origin": CONFIG.domain_origin(),

View File

@@ -150,7 +150,6 @@ async fn sync(data: SyncData, headers: Headers, mut conn: DbConn) -> Json<Value>
"ciphers": ciphers_json, "ciphers": ciphers_json,
"domains": domains_json, "domains": domains_json,
"sends": sends_json, "sends": sends_json,
"unofficialServer": true,
"object": "sync" "object": "sync"
})) }))
} }
@@ -233,7 +232,7 @@ pub struct CipherData {
favorite: Option<bool>, favorite: Option<bool>,
reprompt: Option<i32>, reprompt: Option<i32>,
password_history: Option<Value>, pub password_history: Option<Value>,
// These are used during key rotation // These are used during key rotation
// 'Attachments' is unused, contains map of {id: filename} // 'Attachments' is unused, contains map of {id: filename}
@@ -287,10 +286,6 @@ async fn post_ciphers_create(
if data.cipher.organization_id.is_some() && data.collection_ids.is_empty() { if data.cipher.organization_id.is_some() && data.collection_ids.is_empty() {
err!("You must select at least one collection."); err!("You must select at least one collection.");
} }
// reverse sanity check to prevent corruptions
if !data.collection_ids.is_empty() && data.cipher.organization_id.is_none() {
err!("The client has not provided an organization id!");
}
// This check is usually only needed in update_cipher_from_data(), but we // This check is usually only needed in update_cipher_from_data(), but we
// need it here as well to avoid creating an empty cipher in the call to // need it here as well to avoid creating an empty cipher in the call to
@@ -567,7 +562,7 @@ async fn post_ciphers_import(
// Bitwarden does not process the import if there is one item invalid. // Bitwarden does not process the import if there is one item invalid.
// Since we check for the size of the encrypted note length, we need to do that here to pre-validate it. // Since we check for the size of the encrypted note length, we need to do that here to pre-validate it.
// TODO: See if we can optimize the whole cipher adding/importing and prevent duplicate code and checks. // TODO: See if we can optimize the whole cipher adding/importing and prevent duplicate code and checks.
Cipher::validate_notes(&data.ciphers)?; Cipher::validate_cipher_data(&data.ciphers)?;
// Read and create the folders // Read and create the folders
let existing_folders: Vec<String> = let existing_folders: Vec<String> =
@@ -707,6 +702,7 @@ async fn put_cipher_partial(
#[derive(Deserialize)] #[derive(Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct CollectionsAdminData { struct CollectionsAdminData {
#[serde(alias = "CollectionIds")]
collection_ids: Vec<String>, collection_ids: Vec<String>,
} }

View File

@@ -202,8 +202,10 @@ fn config() -> Json<Value> {
"gitHash": option_env!("GIT_REV"), "gitHash": option_env!("GIT_REV"),
"server": { "server": {
"name": "Vaultwarden", "name": "Vaultwarden",
"url": "https://github.com/dani-garcia/vaultwarden", "url": "https://github.com/dani-garcia/vaultwarden"
"version": crate::VERSION },
"settings": {
"disableUserRegistration": !crate::CONFIG.signups_allowed() && crate::CONFIG.signups_domains_whitelist().is_empty(),
}, },
"environment": { "environment": {
"vault": domain, "vault": domain,

View File

@@ -358,11 +358,12 @@ async fn get_org_collections_details(org_id: &str, headers: ManagerHeadersLoose,
Vec::with_capacity(0) Vec::with_capacity(0)
}; };
let mut json_object = col.to_json(); let mut json_object = col.to_json_details(&headers.user.uuid, None, &mut conn).await;
json_object["assigned"] = json!(assigned); json_object["assigned"] = json!(assigned);
json_object["users"] = json!(users); json_object["users"] = json!(users);
json_object["groups"] = json!(groups); json_object["groups"] = json!(groups);
json_object["object"] = json!("collectionAccessDetails"); json_object["object"] = json!("collectionAccessDetails");
json_object["unmanaged"] = json!(false);
data.push(json_object) data.push(json_object)
} }
@@ -509,7 +510,7 @@ async fn post_organization_collection_update(
CollectionUser::save(&org_user.user_uuid, col_id, user.read_only, user.hide_passwords, &mut conn).await?; CollectionUser::save(&org_user.user_uuid, col_id, user.read_only, user.hide_passwords, &mut conn).await?;
} }
Ok(Json(collection.to_json())) Ok(Json(collection.to_json_details(&headers.user.uuid, None, &mut conn).await))
} }
#[delete("/organizations/<org_id>/collections/<col_id>/user/<org_user_id>")] #[delete("/organizations/<org_id>/collections/<col_id>/user/<org_user_id>")]
@@ -680,7 +681,7 @@ async fn get_org_collection_detail(
let assigned = Collection::can_access_collection(&user_org, &collection.uuid, &mut conn).await; let assigned = Collection::can_access_collection(&user_org, &collection.uuid, &mut conn).await;
let mut json_object = collection.to_json(); let mut json_object = collection.to_json_details(&headers.user.uuid, None, &mut conn).await;
json_object["assigned"] = json!(assigned); json_object["assigned"] = json!(assigned);
json_object["users"] = json!(users); json_object["users"] = json!(users);
json_object["groups"] = json!(groups); json_object["groups"] = json!(groups);
@@ -872,20 +873,19 @@ async fn send_invite(org_id: &str, data: Json<InviteData>, headers: AdminHeaders
} }
for email in data.emails.iter() { for email in data.emails.iter() {
let email = email.to_lowercase();
let mut user_org_status = UserOrgStatus::Invited as i32; let mut user_org_status = UserOrgStatus::Invited as i32;
let user = match User::find_by_mail(&email, &mut conn).await { let user = match User::find_by_mail(email, &mut conn).await {
None => { None => {
if !CONFIG.invitations_allowed() { if !CONFIG.invitations_allowed() {
err!(format!("User does not exist: {email}")) err!(format!("User does not exist: {email}"))
} }
if !CONFIG.is_email_domain_allowed(&email) { if !CONFIG.is_email_domain_allowed(email) {
err!("Email domain not eligible for invitations") err!("Email domain not eligible for invitations")
} }
if !CONFIG.mail_enabled() { if !CONFIG.mail_enabled() {
let invitation = Invitation::new(&email); let invitation = Invitation::new(email);
invitation.save(&mut conn).await?; invitation.save(&mut conn).await?;
} }
@@ -956,8 +956,7 @@ async fn send_invite(org_id: &str, data: Json<InviteData>, headers: AdminHeaders
}; };
mail::send_invite( mail::send_invite(
&email, &user,
&user.uuid,
Some(String::from(org_id)), Some(String::from(org_id)),
Some(new_user.uuid), Some(new_user.uuid),
&org_name, &org_name,
@@ -1033,8 +1032,7 @@ async fn _reinvite_user(org_id: &str, user_org: &str, invited_by_email: &str, co
if CONFIG.mail_enabled() { if CONFIG.mail_enabled() {
mail::send_invite( mail::send_invite(
&user.email, &user,
&user.uuid,
Some(org_id.to_string()), Some(org_id.to_string()),
Some(user_org.uuid), Some(user_org.uuid),
&org_name, &org_name,
@@ -1598,7 +1596,7 @@ async fn post_org_import(
// Bitwarden does not process the import if there is one item invalid. // Bitwarden does not process the import if there is one item invalid.
// Since we check for the size of the encrypted note length, we need to do that here to pre-validate it. // Since we check for the size of the encrypted note length, we need to do that here to pre-validate it.
// TODO: See if we can optimize the whole cipher adding/importing and prevent duplicate code and checks. // TODO: See if we can optimize the whole cipher adding/importing and prevent duplicate code and checks.
Cipher::validate_notes(&data.ciphers)?; Cipher::validate_cipher_data(&data.ciphers)?;
let mut collections = Vec::new(); let mut collections = Vec::new();
for coll in data.collections { for coll in data.collections {
@@ -1722,7 +1720,7 @@ async fn list_policies_token(org_id: &str, token: &str, mut conn: DbConn) -> Jso
return Ok(Json(json!({}))); return Ok(Json(json!({})));
} }
let invite = crate::auth::decode_invite(token)?; let invite = decode_invite(token)?;
let invite_org_id = match invite.org_id { let invite_org_id = match invite.org_id {
Some(invite_org_id) => invite_org_id, Some(invite_org_id) => invite_org_id,
@@ -1782,6 +1780,38 @@ async fn put_policy(
None => err!("Invalid or unsupported policy type"), None => err!("Invalid or unsupported policy type"),
}; };
// Bitwarden only allows the Reset Password policy when Single Org policy is enabled
// Vaultwarden encouraged to use multiple orgs instead of groups because groups were not available in the past
// Now that groups are available we can enforce this option when wanted.
// We put this behind a config option to prevent breaking current installation.
// Maybe we want to enable this by default in the future, but currently it is disabled by default.
if CONFIG.enforce_single_org_with_reset_pw_policy() {
if pol_type_enum == OrgPolicyType::ResetPassword && data.enabled {
let single_org_policy_enabled =
match OrgPolicy::find_by_org_and_type(org_id, OrgPolicyType::SingleOrg, &mut conn).await {
Some(p) => p.enabled,
None => false,
};
if !single_org_policy_enabled {
err!("Single Organization policy is not enabled. It is mandatory for this policy to be enabled.")
}
}
// Also prevent the Single Org Policy to be disabled if the Reset Password policy is enabled
if pol_type_enum == OrgPolicyType::SingleOrg && !data.enabled {
let reset_pw_policy_enabled =
match OrgPolicy::find_by_org_and_type(org_id, OrgPolicyType::ResetPassword, &mut conn).await {
Some(p) => p.enabled,
None => false,
};
if reset_pw_policy_enabled {
err!("Account recovery policy is enabled. It is not allowed to disable this policy.")
}
}
}
// When enabling the TwoFactorAuthentication policy, revoke all members that do not have 2FA // When enabling the TwoFactorAuthentication policy, revoke all members that do not have 2FA
if pol_type_enum == OrgPolicyType::TwoFactorAuthentication && data.enabled { if pol_type_enum == OrgPolicyType::TwoFactorAuthentication && data.enabled {
two_factor::enforce_2fa_policy_for_org( two_factor::enforce_2fa_policy_for_org(
@@ -2005,8 +2035,7 @@ async fn import(org_id: &str, data: Json<OrgImportData>, headers: Headers, mut c
}; };
mail::send_invite( mail::send_invite(
&user_data.email, &user,
&user.uuid,
Some(String::from(org_id)), Some(String::from(org_id)),
Some(new_org_user.uuid), Some(new_org_user.uuid),
&org_name, &org_name,
@@ -2276,13 +2305,14 @@ async fn _restore_organization_user(
} }
#[get("/organizations/<org_id>/groups")] #[get("/organizations/<org_id>/groups")]
async fn get_groups(org_id: &str, _headers: ManagerHeadersLoose, mut conn: DbConn) -> JsonResult { async fn get_groups(org_id: &str, headers: ManagerHeadersLoose, mut conn: DbConn) -> JsonResult {
let groups: Vec<Value> = if CONFIG.org_groups_enabled() { let groups: Vec<Value> = if CONFIG.org_groups_enabled() {
// Group::find_by_organization(&org_id, &mut conn).await.iter().map(Group::to_json).collect::<Value>() // Group::find_by_organization(&org_id, &mut conn).await.iter().map(Group::to_json).collect::<Value>()
let groups = Group::find_by_organization(org_id, &mut conn).await; let groups = Group::find_by_organization(org_id, &mut conn).await;
let mut groups_json = Vec::with_capacity(groups.len()); let mut groups_json = Vec::with_capacity(groups.len());
for g in groups { for g in groups {
groups_json.push(g.to_json_details(&mut conn).await) groups_json.push(g.to_json_details(&headers.org_user.atype, &mut conn).await)
} }
groups_json groups_json
} else { } else {
@@ -2470,7 +2500,7 @@ async fn add_update_group(
} }
#[get("/organizations/<_org_id>/groups/<group_id>/details")] #[get("/organizations/<_org_id>/groups/<group_id>/details")]
async fn get_group_details(_org_id: &str, group_id: &str, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult { async fn get_group_details(_org_id: &str, group_id: &str, headers: AdminHeaders, mut conn: DbConn) -> JsonResult {
if !CONFIG.org_groups_enabled() { if !CONFIG.org_groups_enabled() {
err!("Group support is disabled"); err!("Group support is disabled");
} }
@@ -2480,7 +2510,7 @@ async fn get_group_details(_org_id: &str, group_id: &str, _headers: AdminHeaders
_ => err!("Group could not be found!"), _ => err!("Group could not be found!"),
}; };
Ok(Json(group.to_json_details(&mut conn).await)) Ok(Json(group.to_json_details(&(headers.org_user_type as i32), &mut conn).await))
} }
#[post("/organizations/<org_id>/groups/<group_id>/delete")] #[post("/organizations/<org_id>/groups/<group_id>/delete")]

View File

@@ -1,6 +1,6 @@
use chrono::Utc; use chrono::Utc;
use rocket::{ use rocket::{
request::{self, FromRequest, Outcome}, request::{FromRequest, Outcome},
serde::json::Json, serde::json::Json,
Request, Route, Request, Route,
}; };
@@ -123,14 +123,7 @@ async fn ldap_import(data: Json<OrgImportData>, token: PublicToken, mut conn: Db
None => err!("Error looking up organization"), None => err!("Error looking up organization"),
}; };
mail::send_invite( mail::send_invite(&user, Some(org_id.clone()), Some(new_org_user.uuid), &org_name, Some(org_email))
&user_data.email,
&user.uuid,
Some(org_id.clone()),
Some(new_org_user.uuid),
&org_name,
Some(org_email),
)
.await?; .await?;
} }
} }
@@ -199,7 +192,7 @@ pub struct PublicToken(String);
impl<'r> FromRequest<'r> for PublicToken { impl<'r> FromRequest<'r> for PublicToken {
type Error = &'static str; type Error = &'static str;
async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, Self::Error> { async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
let headers = request.headers(); let headers = request.headers();
// Get access_token // Get access_token
let access_token: &str = match headers.get_one("Authorization") { let access_token: &str = match headers.get_one("Authorization") {

View File

@@ -281,10 +281,6 @@ fn sign_duo_values(key: &str, email: &str, ikey: &str, prefix: &str, expire: i64
} }
pub async fn validate_duo_login(email: &str, response: &str, conn: &mut DbConn) -> EmptyResult { pub async fn validate_duo_login(email: &str, response: &str, conn: &mut DbConn) -> EmptyResult {
// email is as entered by the user, so it needs to be normalized before
// comparison with auth_user below.
let email = &email.to_lowercase();
let split: Vec<&str> = response.split(':').collect(); let split: Vec<&str> = response.split(':').collect();
if split.len() != 2 { if split.len() != 2 {
err!( err!(

View File

@@ -357,7 +357,7 @@ pub async fn purge_duo_contexts(pool: DbPool) {
// Construct the url that Duo should redirect users to. // Construct the url that Duo should redirect users to.
fn make_callback_url(client_name: &str) -> Result<String, Error> { fn make_callback_url(client_name: &str) -> Result<String, Error> {
// Get the location of this application as defined in the config. // Get the location of this application as defined in the config.
let base = match Url::parse(CONFIG.domain().as_str()) { let base = match Url::parse(&format!("{}/", CONFIG.domain())) {
Ok(url) => url, Ok(url) => url,
Err(e) => err!(format!("Error parsing configured domain URL (check your domain configuration): {e:?}")), Err(e) => err!(format!("Error parsing configured domain URL (check your domain configuration): {e:?}")),
}; };

View File

@@ -292,7 +292,7 @@ impl EmailTokenData {
} }
pub fn from_json(string: &str) -> Result<EmailTokenData, Error> { pub fn from_json(string: &str) -> Result<EmailTokenData, Error> {
let res: Result<EmailTokenData, crate::serde_json::Error> = serde_json::from_str(string); let res: Result<EmailTokenData, serde_json::Error> = serde_json::from_str(string);
match res { match res {
Ok(x) => Ok(x), Ok(x) => Ok(x),
Err(_) => err!("Could not decode EmailTokenData from string"), Err(_) => err!("Could not decode EmailTokenData from string"),

View File

@@ -269,7 +269,13 @@ pub async fn send_incomplete_2fa_notifications(pool: DbPool) {
"User {} did not complete a 2FA login within the configured time limit. IP: {}", "User {} did not complete a 2FA login within the configured time limit. IP: {}",
user.email, login.ip_address user.email, login.ip_address
); );
match mail::send_incomplete_2fa_login(&user.email, &login.ip_address, &login.login_time, &login.device_name) match mail::send_incomplete_2fa_login(
&user.email,
&login.ip_address,
&login.login_time,
&login.device_name,
&DeviceType::from_i32(login.device_type).to_string(),
)
.await .await
{ {
Ok(_) => { Ok(_) => {

View File

@@ -42,7 +42,7 @@ impl ProtectedActionData {
} }
pub fn from_json(string: &str) -> Result<Self, Error> { pub fn from_json(string: &str) -> Result<Self, Error> {
let res: Result<Self, crate::serde_json::Error> = serde_json::from_str(string); let res: Result<Self, serde_json::Error> = serde_json::from_str(string);
match res { match res {
Ok(x) => Ok(x), Ok(x) => Ok(x),
Err(_) => err!("Could not decode ProtectedActionData from string"), Err(_) => err!("Could not decode ProtectedActionData from string"),

View File

@@ -49,7 +49,7 @@ fn parse_yubikeys(data: &EnableYubikeyData) -> Vec<String> {
data_keys.iter().filter_map(|e| e.as_ref().cloned()).collect() data_keys.iter().filter_map(|e| e.as_ref().cloned()).collect()
} }
fn jsonify_yubikeys(yubikeys: Vec<String>) -> serde_json::Value { fn jsonify_yubikeys(yubikeys: Vec<String>) -> Value {
let mut result = Value::Object(serde_json::Map::new()); let mut result = Value::Object(serde_json::Map::new());
for (i, key) in yubikeys.into_iter().enumerate() { for (i, key) in yubikeys.into_iter().enumerate() {

View File

@@ -1,4 +1,5 @@
use std::{ use std::{
collections::HashMap,
net::IpAddr, net::IpAddr,
sync::Arc, sync::Arc,
time::{Duration, SystemTime}, time::{Duration, SystemTime},
@@ -446,6 +447,9 @@ async fn get_page_with_referer(url: &str, referer: &str) -> Result<Response, Err
/// priority2 = get_icon_priority("https://example.com/path/to/a/favicon.ico", ""); /// priority2 = get_icon_priority("https://example.com/path/to/a/favicon.ico", "");
/// ``` /// ```
fn get_icon_priority(href: &str, sizes: &str) -> u8 { fn get_icon_priority(href: &str, sizes: &str) -> u8 {
static PRIORITY_MAP: Lazy<HashMap<&'static str, u8>> =
Lazy::new(|| [(".png", 10), (".jpg", 20), (".jpeg", 20)].into_iter().collect());
// Check if there is a dimension set // Check if there is a dimension set
let (width, height) = parse_sizes(sizes); let (width, height) = parse_sizes(sizes);
@@ -470,13 +474,9 @@ fn get_icon_priority(href: &str, sizes: &str) -> u8 {
200 200
} }
} else { } else {
// Change priority by file extension match href.rsplit_once('.') {
if href.ends_with(".png") { Some((_, extension)) => PRIORITY_MAP.get(&*extension.to_ascii_lowercase()).copied().unwrap_or(30),
10 None => 30,
} else if href.ends_with(".jpg") || href.ends_with(".jpeg") {
20
} else {
30
} }
} }
} }
@@ -623,7 +623,7 @@ use cookie_store::CookieStore;
pub struct Jar(std::sync::RwLock<CookieStore>); pub struct Jar(std::sync::RwLock<CookieStore>);
impl reqwest::cookie::CookieStore for Jar { impl reqwest::cookie::CookieStore for Jar {
fn set_cookies(&self, cookie_headers: &mut dyn Iterator<Item = &header::HeaderValue>, url: &url::Url) { fn set_cookies(&self, cookie_headers: &mut dyn Iterator<Item = &HeaderValue>, url: &url::Url) {
use cookie::{Cookie as RawCookie, ParseError as RawCookieParseError}; use cookie::{Cookie as RawCookie, ParseError as RawCookieParseError};
use time::Duration; use time::Duration;
@@ -642,7 +642,7 @@ impl reqwest::cookie::CookieStore for Jar {
cookie_store.store_response_cookies(cookies, url); cookie_store.store_response_cookies(cookies, url);
} }
fn cookies(&self, url: &url::Url) -> Option<header::HeaderValue> { fn cookies(&self, url: &url::Url) -> Option<HeaderValue> {
let cookie_store = self.0.read().unwrap(); let cookie_store = self.0.read().unwrap();
let s = cookie_store let s = cookie_store
.get_request_values(url) .get_request_values(url)
@@ -654,7 +654,7 @@ impl reqwest::cookie::CookieStore for Jar {
return None; return None;
} }
header::HeaderValue::from_maybe_shared(Bytes::from(s)).ok() HeaderValue::from_maybe_shared(Bytes::from(s)).ok()
} }
} }

View File

@@ -120,21 +120,25 @@ async fn _refresh_login(data: ConnectData, conn: &mut DbConn) -> JsonResult {
"expires_in": expires_in, "expires_in": expires_in,
"token_type": "Bearer", "token_type": "Bearer",
"refresh_token": device.refresh_token, "refresh_token": device.refresh_token,
"Key": user.akey,
"PrivateKey": user.private_key,
"Kdf": user.client_kdf_type,
"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
"scope": scope, "scope": scope,
"unofficialServer": true,
}); });
Ok(Json(result)) Ok(Json(result))
} }
#[derive(Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct MasterPasswordPolicy {
min_complexity: u8,
min_length: u32,
require_lower: bool,
require_upper: bool,
require_numbers: bool,
require_special: bool,
enforce_on_login: bool,
}
async fn _password_login( async fn _password_login(
data: ConnectData, data: ConnectData,
user_uuid: &mut Option<String>, user_uuid: &mut Option<String>,
@@ -253,7 +257,7 @@ async fn _password_login(
let twofactor_token = twofactor_auth(&user, &data, &mut device, ip, conn).await?; let twofactor_token = twofactor_auth(&user, &data, &mut device, ip, conn).await?;
if CONFIG.mail_enabled() && new_device { if CONFIG.mail_enabled() && new_device {
if let Err(e) = mail::send_new_device_logged_in(&user.email, &ip.ip.to_string(), &now, &device.name).await { if let Err(e) = mail::send_new_device_logged_in(&user.email, &ip.ip.to_string(), &now, &device).await {
error!("Error sending new device email: {:#?}", e); error!("Error sending new device email: {:#?}", e);
if CONFIG.require_device_email() { if CONFIG.require_device_email() {
@@ -282,6 +286,36 @@ async fn _password_login(
let (access_token, expires_in) = device.refresh_tokens(&user, scope_vec); let (access_token, expires_in) = device.refresh_tokens(&user, scope_vec);
device.save(conn).await?; device.save(conn).await?;
// Fetch all valid Master Password Policies and merge them into one with all true's and larges numbers as one policy
let master_password_policies: Vec<MasterPasswordPolicy> =
OrgPolicy::find_accepted_and_confirmed_by_user_and_active_policy(
&user.uuid,
OrgPolicyType::MasterPassword,
conn,
)
.await
.into_iter()
.filter_map(|p| serde_json::from_str(&p.data).ok())
.collect();
let master_password_policy = if !master_password_policies.is_empty() {
let mut mpp_json = json!(master_password_policies.into_iter().reduce(|acc, policy| {
MasterPasswordPolicy {
min_complexity: acc.min_complexity.max(policy.min_complexity),
min_length: acc.min_length.max(policy.min_length),
require_lower: acc.require_lower || policy.require_lower,
require_upper: acc.require_upper || policy.require_upper,
require_numbers: acc.require_numbers || policy.require_numbers,
require_special: acc.require_special || policy.require_special,
enforce_on_login: acc.enforce_on_login || policy.enforce_on_login,
}
}));
mpp_json["object"] = json!("masterPasswordPolicy");
mpp_json
} else {
json!({"object": "masterPasswordPolicy"})
};
let mut result = json!({ let mut result = json!({
"access_token": access_token, "access_token": access_token,
"expires_in": expires_in, "expires_in": expires_in,
@@ -297,12 +331,9 @@ async fn _password_login(
"KdfParallelism": user.client_kdf_parallelism, "KdfParallelism": user.client_kdf_parallelism,
"ResetMasterPassword": false, // TODO: Same as above "ResetMasterPassword": false, // TODO: Same as above
"ForcePasswordReset": false, "ForcePasswordReset": false,
"MasterPasswordPolicy": { "MasterPasswordPolicy": master_password_policy,
"object": "masterPasswordPolicy",
},
"scope": scope, "scope": scope,
"unofficialServer": true,
"UserDecryptionOptions": { "UserDecryptionOptions": {
"HasMasterPassword": !user.password_hash.is_empty(), "HasMasterPassword": !user.password_hash.is_empty(),
"Object": "userDecryptionOptions" "Object": "userDecryptionOptions"
@@ -381,7 +412,7 @@ async fn _user_api_key_login(
if CONFIG.mail_enabled() && new_device { if CONFIG.mail_enabled() && new_device {
let now = Utc::now().naive_utc(); let now = Utc::now().naive_utc();
if let Err(e) = mail::send_new_device_logged_in(&user.email, &ip.ip.to_string(), &now, &device.name).await { if let Err(e) = mail::send_new_device_logged_in(&user.email, &ip.ip.to_string(), &now, &device).await {
error!("Error sending new device email: {:#?}", e); error!("Error sending new device email: {:#?}", e);
if CONFIG.require_device_email() { if CONFIG.require_device_email() {
@@ -421,9 +452,8 @@ async fn _user_api_key_login(
"KdfIterations": user.client_kdf_iter, "KdfIterations": user.client_kdf_iter,
"KdfMemory": user.client_kdf_memory, "KdfMemory": user.client_kdf_memory,
"KdfParallelism": user.client_kdf_parallelism, "KdfParallelism": user.client_kdf_parallelism,
"ResetMasterPassword": false, // TODO: Same as above "ResetMasterPassword": false, // TODO: according to official server seems something like: user.password_hash.is_empty(), but would need testing
"scope": "api", "scope": "api",
"unofficialServer": true,
}); });
Ok(Json(result)) Ok(Json(result))
@@ -455,7 +485,6 @@ async fn _organization_api_key_login(data: ConnectData, conn: &mut DbConn, ip: &
"expires_in": 3600, "expires_in": 3600,
"token_type": "Bearer", "token_type": "Bearer",
"scope": "api.organization", "scope": "api.organization",
"unofficialServer": true,
}))) })))
} }
@@ -495,7 +524,7 @@ async fn twofactor_auth(
return Ok(None); return Ok(None);
} }
TwoFactorIncomplete::mark_incomplete(&user.uuid, &device.uuid, &device.name, ip, conn).await?; TwoFactorIncomplete::mark_incomplete(&user.uuid, &device.uuid, &device.name, device.atype, ip, conn).await?;
let twofactor_ids: Vec<_> = twofactors.iter().map(|tf| tf.atype).collect(); let twofactor_ids: Vec<_> = twofactors.iter().map(|tf| tf.atype).collect();
let selected_id = data.two_factor_provider.unwrap_or(twofactor_ids[0]); // If we aren't given a two factor provider, assume the first one let selected_id = data.two_factor_provider.unwrap_or(twofactor_ids[0]); // If we aren't given a two factor provider, assume the first one

View File

@@ -428,7 +428,7 @@ impl WebSocketUsers {
let (user_uuid, collection_uuids, revision_date) = if let Some(collection_uuids) = collection_uuids { let (user_uuid, collection_uuids, revision_date) = if let Some(collection_uuids) = collection_uuids {
( (
Value::Nil, Value::Nil,
Value::Array(collection_uuids.into_iter().map(|v| v.into()).collect::<Vec<rmpv::Value>>()), Value::Array(collection_uuids.into_iter().map(|v| v.into()).collect::<Vec<Value>>()),
serialize_date(Utc::now().naive_utc()), serialize_date(Utc::now().naive_utc()),
) )
} else { } else {

View File

@@ -35,8 +35,8 @@ static JWT_FILE_DOWNLOAD_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|file_do
static PRIVATE_RSA_KEY: OnceCell<EncodingKey> = OnceCell::new(); static PRIVATE_RSA_KEY: OnceCell<EncodingKey> = OnceCell::new();
static PUBLIC_RSA_KEY: OnceCell<DecodingKey> = OnceCell::new(); static PUBLIC_RSA_KEY: OnceCell<DecodingKey> = OnceCell::new();
pub fn initialize_keys() -> Result<(), crate::error::Error> { pub fn initialize_keys() -> Result<(), Error> {
fn read_key(create_if_missing: bool) -> Result<(Rsa<openssl::pkey::Private>, Vec<u8>), crate::error::Error> { fn read_key(create_if_missing: bool) -> Result<(Rsa<openssl::pkey::Private>, Vec<u8>), Error> {
let mut priv_key_buffer = Vec::with_capacity(2048); let mut priv_key_buffer = Vec::with_capacity(2048);
let mut priv_key_file = File::options() let mut priv_key_file = File::options()
@@ -53,7 +53,7 @@ pub fn initialize_keys() -> Result<(), crate::error::Error> {
Rsa::private_key_from_pem(&priv_key_buffer[..bytes_read])? Rsa::private_key_from_pem(&priv_key_buffer[..bytes_read])?
} else if create_if_missing { } else if create_if_missing {
// Only create the key if the file doesn't exist or is empty // Only create the key if the file doesn't exist or is empty
let rsa_key = openssl::rsa::Rsa::generate(2048)?; let rsa_key = Rsa::generate(2048)?;
priv_key_buffer = rsa_key.private_key_to_pem()?; priv_key_buffer = rsa_key.private_key_to_pem()?;
priv_key_file.write_all(&priv_key_buffer)?; priv_key_file.write_all(&priv_key_buffer)?;
info!("Private key '{}' created correctly", CONFIG.private_rsa_key()); info!("Private key '{}' created correctly", CONFIG.private_rsa_key());

View File

@@ -1,6 +1,9 @@
use std::env::consts::EXE_SUFFIX; use std::env::consts::EXE_SUFFIX;
use std::process::exit; use std::process::exit;
use std::sync::RwLock; use std::sync::{
atomic::{AtomicBool, Ordering},
RwLock,
};
use job_scheduler_ng::Schedule; use job_scheduler_ng::Schedule;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
@@ -17,6 +20,8 @@ static CONFIG_FILE: Lazy<String> = Lazy::new(|| {
get_env("CONFIG_FILE").unwrap_or_else(|| format!("{data_folder}/config.json")) get_env("CONFIG_FILE").unwrap_or_else(|| format!("{data_folder}/config.json"))
}); });
pub static SKIP_CONFIG_VALIDATION: AtomicBool = AtomicBool::new(false);
pub static CONFIG: Lazy<Config> = Lazy::new(|| { pub static CONFIG: Lazy<Config> = Lazy::new(|| {
Config::load().unwrap_or_else(|e| { Config::load().unwrap_or_else(|e| {
println!("Error loading config:\n {e:?}\n"); println!("Error loading config:\n {e:?}\n");
@@ -331,7 +336,7 @@ macro_rules! make_config {
} }
} }
}}; }};
( @build $value:expr, $config:expr, gen, $default_fn:expr ) => {{ ( @build $value:expr, $config:expr, generated, $default_fn:expr ) => {{
let f: &dyn Fn(&ConfigItems) -> _ = &$default_fn; let f: &dyn Fn(&ConfigItems) -> _ = &$default_fn;
f($config) f($config)
}}; }};
@@ -352,7 +357,7 @@ macro_rules! make_config {
// def: Use a default value // def: Use a default value
// auto: Value is auto generated based on other values // auto: Value is auto generated based on other values
// option: Value is optional // option: Value is optional
// gen: Value is always autogenerated and it's original value ignored // generated: Value is always autogenerated and it's original value ignored
make_config! { make_config! {
folders { folders {
/// Data folder |> Main data folder /// Data folder |> Main data folder
@@ -515,7 +520,7 @@ make_config! {
/// Set to the string "none" (without quotes), to disable any headers and just use the remote IP /// Set to the string "none" (without quotes), to disable any headers and just use the remote IP
ip_header: String, true, def, "X-Real-IP".to_string(); ip_header: String, true, def, "X-Real-IP".to_string();
/// Internal IP header property, used to avoid recomputing each time /// Internal IP header property, used to avoid recomputing each time
_ip_header_enabled: bool, false, gen, |c| &c.ip_header.trim().to_lowercase() != "none"; _ip_header_enabled: bool, false, generated, |c| &c.ip_header.trim().to_lowercase() != "none";
/// Icon service |> The predefined icon services are: internal, bitwarden, duckduckgo, google. /// Icon service |> The predefined icon services are: internal, bitwarden, duckduckgo, google.
/// To specify a custom icon service, set a URL template with exactly one instance of `{}`, /// To specify a custom icon service, set a URL template with exactly one instance of `{}`,
/// which is replaced with the domain. For example: `https://icon.example.com/domain/{}`. /// which is replaced with the domain. For example: `https://icon.example.com/domain/{}`.
@@ -524,9 +529,9 @@ make_config! {
/// corresponding icon at the external service. /// corresponding icon at the external service.
icon_service: String, false, def, "internal".to_string(); icon_service: String, false, def, "internal".to_string();
/// _icon_service_url /// _icon_service_url
_icon_service_url: String, false, gen, |c| generate_icon_service_url(&c.icon_service); _icon_service_url: String, false, generated, |c| generate_icon_service_url(&c.icon_service);
/// _icon_service_csp /// _icon_service_csp
_icon_service_csp: String, false, gen, |c| generate_icon_service_csp(&c.icon_service, &c._icon_service_url); _icon_service_csp: String, false, generated, |c| generate_icon_service_csp(&c.icon_service, &c._icon_service_url);
/// Icon redirect code |> The HTTP status code to use for redirects to an external icon service. /// Icon redirect code |> The HTTP status code to use for redirects to an external icon service.
/// The supported codes are 301 (legacy permanent), 302 (legacy temporary), 307 (temporary), and 308 (permanent). /// The supported codes are 301 (legacy permanent), 302 (legacy temporary), 307 (temporary), and 308 (permanent).
/// Temporary redirects are useful while testing different icon services, but once a service /// Temporary redirects are useful while testing different icon services, but once a service
@@ -624,7 +629,12 @@ make_config! {
/// WARNING: This could cause issues with clients. Also exports will not work on Bitwarden servers! /// WARNING: This could cause issues with clients. Also exports will not work on Bitwarden servers!
increase_note_size_limit: bool, true, def, false; increase_note_size_limit: bool, true, def, false;
/// Generated max_note_size value to prevent if..else matching during every check /// Generated max_note_size value to prevent if..else matching during every check
_max_note_size: usize, false, gen, |c| if c.increase_note_size_limit {100_000} else {10_000}; _max_note_size: usize, false, generated, |c| if c.increase_note_size_limit {100_000} else {10_000};
/// Enforce Single Org with Reset Password Policy |> Enforce that the Single Org policy is enabled before setting the Reset Password policy
/// Bitwarden enforces this by default. In Vaultwarden we encouraged to use multiple organizations because groups were not available.
/// Setting this to true will enforce the Single Org Policy to be enabled before you can enable the Reset Password policy.
enforce_single_org_with_reset_pw_policy: bool, false, def, false;
}, },
/// Yubikey settings /// Yubikey settings
@@ -690,7 +700,7 @@ make_config! {
/// Embed images as email attachments. /// Embed images as email attachments.
smtp_embed_images: bool, true, def, true; smtp_embed_images: bool, true, def, true;
/// _smtp_img_src /// _smtp_img_src
_smtp_img_src: String, false, gen, |c| generate_smtp_img_src(c.smtp_embed_images, &c.domain); _smtp_img_src: String, false, generated, |c| generate_smtp_img_src(c.smtp_embed_images, &c.domain);
/// Enable SMTP debugging (Know the risks!) |> DANGEROUS: Enabling this will output very detailed SMTP messages. This could contain sensitive information like passwords and usernames! Only enable this during troubleshooting! /// Enable SMTP debugging (Know the risks!) |> DANGEROUS: Enabling this will output very detailed SMTP messages. This could contain sensitive information like passwords and usernames! Only enable this during troubleshooting!
smtp_debug: bool, false, def, false; smtp_debug: bool, false, def, false;
/// Accept Invalid Certs (Know the risks!) |> DANGEROUS: Allow invalid certificates. This option introduces significant vulnerabilities to man-in-the-middle attacks! /// Accept Invalid Certs (Know the risks!) |> DANGEROUS: Allow invalid certificates. This option introduces significant vulnerabilities to man-in-the-middle attacks!
@@ -802,7 +812,7 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
// TODO: deal with deprecated flags so they can be removed from this list, cf. #4263 // TODO: deal with deprecated flags so they can be removed from this list, cf. #4263
const KNOWN_FLAGS: &[&str] = const KNOWN_FLAGS: &[&str] =
&["autofill-overlay", "autofill-v2", "browser-fileless-import", "fido2-vault-credentials"]; &["autofill-overlay", "autofill-v2", "browser-fileless-import", "extension-refresh", "fido2-vault-credentials"];
let configured_flags = parse_experimental_client_feature_flags(&cfg.experimental_client_feature_flags); let configured_flags = parse_experimental_client_feature_flags(&cfg.experimental_client_feature_flags);
let invalid_flags: Vec<_> = configured_flags.keys().filter(|flag| !KNOWN_FLAGS.contains(&flag.as_str())).collect(); let invalid_flags: Vec<_> = configured_flags.keys().filter(|flag| !KNOWN_FLAGS.contains(&flag.as_str())).collect();
if !invalid_flags.is_empty() { if !invalid_flags.is_empty() {
@@ -1100,7 +1110,9 @@ impl Config {
// Fill any missing with defaults // Fill any missing with defaults
let config = builder.build(); let config = builder.build();
if !SKIP_CONFIG_VALIDATION.load(Ordering::Relaxed) {
validate_config(&config)?; validate_config(&config)?;
}
Ok(Config { Ok(Config {
inner: RwLock::new(Inner { inner: RwLock::new(Inner {
@@ -1220,7 +1232,7 @@ impl Config {
} }
pub fn private_rsa_key(&self) -> String { pub fn private_rsa_key(&self) -> String {
format!("{}.pem", CONFIG.rsa_key_filename()) format!("{}.pem", self.rsa_key_filename())
} }
pub fn mail_enabled(&self) -> bool { pub fn mail_enabled(&self) -> bool {
let inner = &self.inner.read().unwrap().config; let inner = &self.inner.read().unwrap().config;
@@ -1251,12 +1263,8 @@ impl Config {
token.is_some() && !token.unwrap().trim().is_empty() token.is_some() && !token.unwrap().trim().is_empty()
} }
pub fn render_template<T: serde::ser::Serialize>( pub fn render_template<T: serde::ser::Serialize>(&self, name: &str, data: &T) -> Result<String, Error> {
&self, if self.reload_templates() {
name: &str,
data: &T,
) -> Result<String, crate::error::Error> {
if CONFIG.reload_templates() {
warn!("RELOADING TEMPLATES"); warn!("RELOADING TEMPLATES");
let hb = load_templates(CONFIG.templates_folder()); let hb = load_templates(CONFIG.templates_folder());
hb.render(name, data).map_err(Into::into) hb.render(name, data).map_err(Into::into)

View File

@@ -300,19 +300,17 @@ pub trait FromDb {
impl<T: FromDb> FromDb for Vec<T> { impl<T: FromDb> FromDb for Vec<T> {
type Output = Vec<T::Output>; type Output = Vec<T::Output>;
#[allow(clippy::wrong_self_convention)]
#[inline(always)] #[inline(always)]
fn from_db(self) -> Self::Output { fn from_db(self) -> Self::Output {
self.into_iter().map(crate::db::FromDb::from_db).collect() self.into_iter().map(FromDb::from_db).collect()
} }
} }
impl<T: FromDb> FromDb for Option<T> { impl<T: FromDb> FromDb for Option<T> {
type Output = Option<T::Output>; type Output = Option<T::Output>;
#[allow(clippy::wrong_self_convention)]
#[inline(always)] #[inline(always)]
fn from_db(self) -> Self::Output { fn from_db(self) -> Self::Output {
self.map(crate::db::FromDb::from_db) self.map(FromDb::from_db)
} }
} }
@@ -368,21 +366,29 @@ pub mod models;
/// Creates a back-up of the sqlite database /// Creates a back-up of the sqlite database
/// MySQL/MariaDB and PostgreSQL are not supported. /// MySQL/MariaDB and PostgreSQL are not supported.
pub async fn backup_database(conn: &mut DbConn) -> Result<(), Error> { pub async fn backup_database(conn: &mut DbConn) -> Result<String, Error> {
db_run! {@raw conn: db_run! {@raw conn:
postgresql, mysql { postgresql, mysql {
let _ = conn; let _ = conn;
err!("PostgreSQL and MySQL/MariaDB do not support this backup feature"); err!("PostgreSQL and MySQL/MariaDB do not support this backup feature");
} }
sqlite { sqlite {
use std::path::Path; backup_sqlite_database(conn)
}
}
}
#[cfg(sqlite)]
pub fn backup_sqlite_database(conn: &mut diesel::sqlite::SqliteConnection) -> Result<String, Error> {
use diesel::RunQueryDsl;
let db_url = CONFIG.database_url(); let db_url = CONFIG.database_url();
let db_path = Path::new(&db_url).parent().unwrap().to_string_lossy(); let db_path = std::path::Path::new(&db_url).parent().unwrap();
let file_date = chrono::Utc::now().format("%Y%m%d_%H%M%S").to_string(); let backup_file = db_path
diesel::sql_query(format!("VACUUM INTO '{db_path}/db_{file_date}.sqlite3'")).execute(conn)?; .join(format!("db_{}.sqlite3", chrono::Utc::now().format("%Y%m%d_%H%M%S")))
Ok(()) .to_string_lossy()
} .into_owned();
} diesel::sql_query(format!("VACUUM INTO '{backup_file}'")).execute(conn)?;
Ok(backup_file)
} }
/// Get the SQL Server version /// Get the SQL Server version

View File

@@ -1,6 +1,6 @@
use crate::util::LowerCase; use crate::util::LowerCase;
use crate::CONFIG; use crate::CONFIG;
use chrono::{NaiveDateTime, TimeDelta, Utc}; use chrono::{DateTime, NaiveDateTime, TimeDelta, Utc};
use serde_json::Value; use serde_json::Value;
use super::{ use super::{
@@ -79,19 +79,39 @@ impl Cipher {
} }
} }
pub fn validate_notes(cipher_data: &[CipherData]) -> EmptyResult { pub fn validate_cipher_data(cipher_data: &[CipherData]) -> EmptyResult {
let mut validation_errors = serde_json::Map::new(); let mut validation_errors = serde_json::Map::new();
let max_note_size = CONFIG._max_note_size(); let max_note_size = CONFIG._max_note_size();
let max_note_size_msg = let max_note_size_msg =
format!("The field Notes exceeds the maximum encrypted value length of {} characters.", &max_note_size); format!("The field Notes exceeds the maximum encrypted value length of {} characters.", &max_note_size);
for (index, cipher) in cipher_data.iter().enumerate() { for (index, cipher) in cipher_data.iter().enumerate() {
// Validate the note size and if it is exceeded return a warning
if let Some(note) = &cipher.notes { if let Some(note) = &cipher.notes {
if note.len() > max_note_size { if note.len() > max_note_size {
validation_errors validation_errors
.insert(format!("Ciphers[{index}].Notes"), serde_json::to_value([&max_note_size_msg]).unwrap()); .insert(format!("Ciphers[{index}].Notes"), serde_json::to_value([&max_note_size_msg]).unwrap());
} }
} }
// Validate the password history if it contains `null` values and if so, return a warning
if let Some(Value::Array(password_history)) = &cipher.password_history {
for pwh in password_history {
if let Value::Object(pwo) = pwh {
if pwo.get("password").is_some_and(|p| !p.is_string()) {
validation_errors.insert(
format!("Ciphers[{index}].Notes"),
serde_json::to_value([
"The password history contains a `null` value. Only strings are allowed.",
])
.unwrap(),
);
break;
} }
}
}
}
}
if !validation_errors.is_empty() { if !validation_errors.is_empty() {
let err_json = json!({ let err_json = json!({
"message": "The model state is invalid.", "message": "The model state is invalid.",
@@ -153,27 +173,68 @@ impl Cipher {
.as_ref() .as_ref()
.and_then(|s| { .and_then(|s| {
serde_json::from_str::<Vec<LowerCase<Value>>>(s) serde_json::from_str::<Vec<LowerCase<Value>>>(s)
.inspect_err(|e| warn!("Error parsing fields {:?}", e)) .inspect_err(|e| warn!("Error parsing fields {e:?} for {}", self.uuid))
.ok() .ok()
}) })
.map(|d| d.into_iter().map(|d| d.data).collect()) .map(|d| {
d.into_iter()
.map(|mut f| {
// Check if the `type` key is a number, strings break some clients
// The fallback type is the hidden type `1`. this should prevent accidental data disclosure
// If not try to convert the string value to a number and fallback to `1`
// If it is both not a number and not a string, fallback to `1`
match f.data.get("type") {
Some(t) if t.is_number() => {}
Some(t) if t.is_string() => {
let type_num = &t.as_str().unwrap_or("1").parse::<u8>().unwrap_or(1);
f.data["type"] = json!(type_num);
}
_ => {
f.data["type"] = json!(1);
}
}
f.data
})
.collect()
})
.unwrap_or_default(); .unwrap_or_default();
let password_history_json: Vec<_> = self let password_history_json: Vec<_> = self
.password_history .password_history
.as_ref() .as_ref()
.and_then(|s| { .and_then(|s| {
serde_json::from_str::<Vec<LowerCase<Value>>>(s) serde_json::from_str::<Vec<LowerCase<Value>>>(s)
.inspect_err(|e| warn!("Error parsing password history {:?}", e)) .inspect_err(|e| warn!("Error parsing password history {e:?} for {}", self.uuid))
.ok() .ok()
}) })
.map(|d| d.into_iter().map(|d| d.data).collect()) .map(|d| {
// Check every password history item if they are valid and return it.
// If a password field has the type `null` skip it, it breaks newer Bitwarden clients
// A second check is done to verify the lastUsedDate exists and is a valid DateTime string, if not the epoch start time will be used
d.into_iter()
.filter_map(|d| match d.data.get("password") {
Some(p) if p.is_string() => Some(d.data),
_ => None,
})
.map(|d| match d.get("lastUsedDate").and_then(|l| l.as_str()) {
Some(l) if DateTime::parse_from_rfc3339(l).is_ok() => d,
_ => {
let mut d = d;
d["lastUsedDate"] = json!("1970-01-01T00:00:00.000Z");
d
}
})
.collect()
})
.unwrap_or_default(); .unwrap_or_default();
// Get the type_data or a default to an empty json object '{}'. // Get the type_data or a default to an empty json object '{}'.
// If not passing an empty object, mobile clients will crash. // If not passing an empty object, mobile clients will crash.
let mut type_data_json = serde_json::from_str::<LowerCase<Value>>(&self.data) let mut type_data_json =
.map(|d| d.data) serde_json::from_str::<LowerCase<Value>>(&self.data).map(|d| d.data).unwrap_or_else(|_| {
.unwrap_or_else(|_| Value::Object(serde_json::Map::new())); warn!("Error parsing data field for {}", self.uuid);
Value::Object(serde_json::Map::new())
});
// NOTE: This was marked as *Backwards Compatibility Code*, but as of January 2021 this is still being used by upstream // NOTE: This was marked as *Backwards Compatibility Code*, but as of January 2021 this is still being used by upstream
// Set the first element of the Uris array as Uri, this is needed several (mobile) clients. // Set the first element of the Uris array as Uri, this is needed several (mobile) clients.
@@ -189,18 +250,21 @@ impl Cipher {
// Fix secure note issues when data is invalid // Fix secure note issues when data is invalid
// This breaks at least the native mobile clients // This breaks at least the native mobile clients
if self.atype == 2 if self.atype == 2 {
&& (self.data.is_empty() || self.data.eq("{}") || self.data.to_ascii_lowercase().eq("{\"type\":null}")) match type_data_json {
{ Value::Object(ref t) if t.get("type").is_some_and(|t| t.is_number()) => {}
_ => {
type_data_json = json!({"type": 0}); type_data_json = json!({"type": 0});
} }
}
}
// Clone the type_data and add some default value. // Clone the type_data and add some default value.
let mut data_json = type_data_json.clone(); let mut data_json = type_data_json.clone();
// NOTE: This was marked as *Backwards Compatibility Code*, but as of January 2021 this is still being used by upstream // NOTE: This was marked as *Backwards Compatibility Code*, but as of January 2021 this is still being used by upstream
// data_json should always contain the following keys with every atype // data_json should always contain the following keys with every atype
data_json["fields"] = Value::Array(fields_json.clone()); data_json["fields"] = json!(fields_json);
data_json["name"] = json!(self.name); data_json["name"] = json!(self.name);
data_json["notes"] = json!(self.notes); data_json["notes"] = json!(self.notes);
data_json["passwordHistory"] = Value::Array(password_history_json.clone()); data_json["passwordHistory"] = Value::Array(password_history_json.clone());

View File

@@ -78,28 +78,46 @@ impl Collection {
cipher_sync_data: Option<&crate::api::core::CipherSyncData>, cipher_sync_data: Option<&crate::api::core::CipherSyncData>,
conn: &mut DbConn, conn: &mut DbConn,
) -> Value { ) -> Value {
let (read_only, hide_passwords) = if let Some(cipher_sync_data) = cipher_sync_data { let (read_only, hide_passwords, can_manage) = if let Some(cipher_sync_data) = cipher_sync_data {
match cipher_sync_data.user_organizations.get(&self.org_uuid) { match cipher_sync_data.user_organizations.get(&self.org_uuid) {
Some(uo) if uo.has_full_access() => (false, false), // Only for Manager types Bitwarden returns true for the can_manage option
Some(_) => { // Owners and Admins always have true
Some(uo) if uo.has_full_access() => (false, false, uo.atype >= UserOrgType::Manager),
Some(uo) => {
// Only let a manager manage collections when the have full read/write access
let is_manager = uo.atype == UserOrgType::Manager;
if let Some(uc) = cipher_sync_data.user_collections.get(&self.uuid) { if let Some(uc) = cipher_sync_data.user_collections.get(&self.uuid) {
(uc.read_only, uc.hide_passwords) (uc.read_only, uc.hide_passwords, is_manager && !uc.read_only && !uc.hide_passwords)
} else if let Some(cg) = cipher_sync_data.user_collections_groups.get(&self.uuid) { } else if let Some(cg) = cipher_sync_data.user_collections_groups.get(&self.uuid) {
(cg.read_only, cg.hide_passwords) (cg.read_only, cg.hide_passwords, is_manager && !cg.read_only && !cg.hide_passwords)
} else { } else {
(false, false) (false, false, false)
} }
} }
_ => (true, true), _ => (true, true, false),
} }
} else { } else {
(!self.is_writable_by_user(user_uuid, conn).await, self.hide_passwords_for_user(user_uuid, conn).await) match UserOrganization::find_confirmed_by_user_and_org(user_uuid, &self.org_uuid, conn).await {
Some(ou) if ou.has_full_access() => (false, false, ou.atype >= UserOrgType::Manager),
Some(ou) => {
let is_manager = ou.atype == UserOrgType::Manager;
let read_only = !self.is_writable_by_user(user_uuid, conn).await;
let hide_passwords = self.hide_passwords_for_user(user_uuid, conn).await;
(read_only, hide_passwords, is_manager && !read_only && !hide_passwords)
}
_ => (
!self.is_writable_by_user(user_uuid, conn).await,
self.hide_passwords_for_user(user_uuid, conn).await,
false,
),
}
}; };
let mut json_object = self.to_json(); let mut json_object = self.to_json();
json_object["object"] = json!("collectionDetails"); json_object["object"] = json!("collectionDetails");
json_object["readOnly"] = json!(read_only); json_object["readOnly"] = json!(read_only);
json_object["hidePasswords"] = json!(hide_passwords); json_object["hidePasswords"] = json!(hide_passwords);
json_object["manage"] = json!(can_manage);
json_object json_object
} }

View File

@@ -16,7 +16,7 @@ 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/dcc199bcce4aa2d5621f6fab80f1b49d8b143418/src/Core/Enums/DeviceType.cs
pub push_uuid: Option<String>, pub push_uuid: Option<String>,
pub push_token: Option<String>, pub push_token: Option<String>,
@@ -267,6 +267,9 @@ pub enum DeviceType {
SafariExtension = 20, SafariExtension = 20,
Sdk = 21, Sdk = 21,
Server = 22, Server = 22,
WindowsCLI = 23,
MacOsCLI = 24,
LinuxCLI = 25,
} }
impl fmt::Display for DeviceType { impl fmt::Display for DeviceType {
@@ -278,23 +281,26 @@ impl fmt::Display for DeviceType {
DeviceType::FirefoxExtension => write!(f, "Firefox Extension"), DeviceType::FirefoxExtension => write!(f, "Firefox Extension"),
DeviceType::OperaExtension => write!(f, "Opera Extension"), DeviceType::OperaExtension => write!(f, "Opera Extension"),
DeviceType::EdgeExtension => write!(f, "Edge Extension"), DeviceType::EdgeExtension => write!(f, "Edge Extension"),
DeviceType::WindowsDesktop => write!(f, "Windows Desktop"), DeviceType::WindowsDesktop => write!(f, "Windows"),
DeviceType::MacOsDesktop => write!(f, "MacOS Desktop"), DeviceType::MacOsDesktop => write!(f, "macOS"),
DeviceType::LinuxDesktop => write!(f, "Linux Desktop"), DeviceType::LinuxDesktop => write!(f, "Linux"),
DeviceType::ChromeBrowser => write!(f, "Chrome Browser"), DeviceType::ChromeBrowser => write!(f, "Chrome"),
DeviceType::FirefoxBrowser => write!(f, "Firefox Browser"), DeviceType::FirefoxBrowser => write!(f, "Firefox"),
DeviceType::OperaBrowser => write!(f, "Opera Browser"), DeviceType::OperaBrowser => write!(f, "Opera"),
DeviceType::EdgeBrowser => write!(f, "Edge Browser"), DeviceType::EdgeBrowser => write!(f, "Edge"),
DeviceType::IEBrowser => write!(f, "Internet Explorer"), DeviceType::IEBrowser => write!(f, "Internet Explorer"),
DeviceType::UnknownBrowser => write!(f, "Unknown Browser"), DeviceType::UnknownBrowser => write!(f, "Unknown Browser"),
DeviceType::AndroidAmazon => write!(f, "Android Amazon"), DeviceType::AndroidAmazon => write!(f, "Android"),
DeviceType::Uwp => write!(f, "UWP"), DeviceType::Uwp => write!(f, "UWP"),
DeviceType::SafariBrowser => write!(f, "Safari Browser"), DeviceType::SafariBrowser => write!(f, "Safari"),
DeviceType::VivaldiBrowser => write!(f, "Vivaldi Browser"), DeviceType::VivaldiBrowser => write!(f, "Vivaldi"),
DeviceType::VivaldiExtension => write!(f, "Vivaldi Extension"), DeviceType::VivaldiExtension => write!(f, "Vivaldi Extension"),
DeviceType::SafariExtension => write!(f, "Safari Extension"), DeviceType::SafariExtension => write!(f, "Safari Extension"),
DeviceType::Sdk => write!(f, "SDK"), DeviceType::Sdk => write!(f, "SDK"),
DeviceType::Server => write!(f, "Server"), DeviceType::Server => write!(f, "Server"),
DeviceType::WindowsCLI => write!(f, "Windows CLI"),
DeviceType::MacOsCLI => write!(f, "macOS CLI"),
DeviceType::LinuxCLI => write!(f, "Linux CLI"),
} }
} }
} }
@@ -325,6 +331,9 @@ impl DeviceType {
20 => DeviceType::SafariExtension, 20 => DeviceType::SafariExtension,
21 => DeviceType::Sdk, 21 => DeviceType::Sdk,
22 => DeviceType::Server, 22 => DeviceType::Server,
23 => DeviceType::WindowsCLI,
24 => DeviceType::MacOsCLI,
25 => DeviceType::LinuxCLI,
_ => DeviceType::UnknownBrowser, _ => DeviceType::UnknownBrowser,
} }
} }

View File

@@ -26,7 +26,7 @@ db_object! {
} }
} }
/// Local methods // Local methods
impl EmergencyAccess { impl EmergencyAccess {
pub fn new(grantor_uuid: String, email: String, status: i32, atype: i32, wait_time_days: i32) -> Self { pub fn new(grantor_uuid: String, email: String, status: i32, atype: i32, wait_time_days: i32) -> Self {
@@ -89,7 +89,7 @@ impl EmergencyAccess {
Some(user) => user, Some(user) => user,
None => { None => {
// remove outstanding invitations which should not exist // remove outstanding invitations which should not exist
let _ = Self::delete_all_by_grantee_email(email, conn).await; Self::delete_all_by_grantee_email(email, conn).await.ok();
return None; return None;
} }
} }

View File

@@ -1,3 +1,7 @@
use super::{User, UserOrgType, UserOrganization};
use crate::api::EmptyResult;
use crate::db::DbConn;
use crate::error::MapResult;
use chrono::{NaiveDateTime, Utc}; use chrono::{NaiveDateTime, Utc};
use serde_json::Value; use serde_json::Value;
@@ -69,7 +73,7 @@ impl Group {
}) })
} }
pub async fn to_json_details(&self, conn: &mut DbConn) -> Value { pub async fn to_json_details(&self, user_org_type: &i32, conn: &mut DbConn) -> Value {
let collections_groups: Vec<Value> = CollectionGroup::find_by_group(&self.uuid, conn) let collections_groups: Vec<Value> = CollectionGroup::find_by_group(&self.uuid, conn)
.await .await
.iter() .iter()
@@ -77,7 +81,8 @@ impl Group {
json!({ json!({
"id": entry.collections_uuid, "id": entry.collections_uuid,
"readOnly": entry.read_only, "readOnly": entry.read_only,
"hidePasswords": entry.hide_passwords "hidePasswords": entry.hide_passwords,
"manage": *user_org_type >= UserOrgType::Admin || (*user_org_type == UserOrgType::Manager && !entry.read_only && !entry.hide_passwords)
}) })
}) })
.collect(); .collect();
@@ -122,13 +127,6 @@ impl GroupUser {
} }
} }
use crate::db::DbConn;
use crate::api::EmptyResult;
use crate::error::MapResult;
use super::{User, UserOrganization};
/// Database methods /// Database methods
impl Group { impl Group {
pub async fn save(&mut self, conn: &mut DbConn) -> EmptyResult { pub async fn save(&mut self, conn: &mut DbConn) -> EmptyResult {

View File

@@ -342,10 +342,12 @@ impl OrgPolicy {
false false
} }
pub async fn is_enabled_by_org(org_uuid: &str, policy_type: OrgPolicyType, conn: &mut DbConn) -> bool { pub async fn is_enabled_for_member(org_user_uuid: &str, policy_type: OrgPolicyType, conn: &mut DbConn) -> bool {
if let Some(policy) = OrgPolicy::find_by_org_and_type(org_uuid, policy_type, conn).await { if let Some(membership) = UserOrganization::find_by_uuid(org_user_uuid, conn).await {
if let Some(policy) = OrgPolicy::find_by_org_and_type(&membership.org_uuid, policy_type, conn).await {
return policy.enabled; return policy.enabled;
} }
}
false false
} }
} }

View File

@@ -1,9 +1,13 @@
use chrono::{NaiveDateTime, Utc}; 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,
collections::{HashMap, HashSet},
};
use super::{CollectionUser, Group, GroupUser, OrgPolicy, OrgPolicyType, TwoFactor, User}; use super::{CollectionUser, Group, GroupUser, OrgPolicy, OrgPolicyType, TwoFactor, User};
use crate::db::models::{Collection, CollectionGroup};
use crate::CONFIG; use crate::CONFIG;
db_object! { db_object! {
@@ -112,7 +116,7 @@ impl PartialOrd<i32> for UserOrgType {
} }
fn ge(&self, other: &i32) -> bool { fn ge(&self, other: &i32) -> bool {
matches!(self.partial_cmp(other), Some(Ordering::Greater) | Some(Ordering::Equal)) matches!(self.partial_cmp(other), Some(Ordering::Greater | Ordering::Equal))
} }
} }
@@ -135,7 +139,7 @@ impl PartialOrd<UserOrgType> for i32 {
} }
fn le(&self, other: &UserOrgType) -> bool { fn le(&self, other: &UserOrgType) -> bool {
matches!(self.partial_cmp(other), Some(Ordering::Less) | Some(Ordering::Equal) | None) matches!(self.partial_cmp(other), Some(Ordering::Less | Ordering::Equal) | None)
} }
} }
@@ -157,7 +161,6 @@ impl Organization {
"identifier": null, // not supported by us "identifier": null, // not supported by us
"name": self.name, "name": self.name,
"seats": null, "seats": null,
"maxAutoscaleSeats": null,
"maxCollections": null, "maxCollections": null,
"maxStorageGb": i16::MAX, // The value doesn't matter, we don't check server-side "maxStorageGb": i16::MAX, // The value doesn't matter, we don't check server-side
"use2fa": true, "use2fa": true,
@@ -229,6 +232,14 @@ impl UserOrganization {
false false
} }
/// Return the status of the user in an unrevoked state
pub fn get_unrevoked_status(&self) -> i32 {
if self.status <= UserOrgStatus::Revoked as i32 {
return self.status + ACTIVATE_REVOKE_DIFF;
}
self.status
}
pub fn set_external_id(&mut self, external_id: Option<String>) -> bool { pub fn set_external_id(&mut self, external_id: Option<String>) -> bool {
//Check if external id is empty. We don't want to have //Check if external id is empty. We don't want to have
//empty strings in the database //empty strings in the database
@@ -370,7 +381,6 @@ impl UserOrganization {
"identifier": null, // Not supported "identifier": null, // Not supported
"name": org.name, "name": org.name,
"seats": null, "seats": null,
"maxAutoscaleSeats": null,
"maxCollections": null, "maxCollections": null,
"usersGetPremium": true, "usersGetPremium": true,
"use2fa": true, "use2fa": true,
@@ -407,7 +417,7 @@ impl UserOrganization {
"familySponsorshipValidUntil": null, "familySponsorshipValidUntil": null,
"familySponsorshipToDelete": null, "familySponsorshipToDelete": null,
"accessSecretsManager": false, "accessSecretsManager": false,
"limitCollectionCreationDeletion": true, "limitCollectionCreationDeletion": false, // This should be set to true only when we can handle roles like createNewCollections
"allowAdminAccessToAllCollectionItems": true, "allowAdminAccessToAllCollectionItems": true,
"flexibleCollections": false, "flexibleCollections": false,
@@ -453,27 +463,79 @@ impl UserOrganization {
}; };
let collections: Vec<Value> = if include_collections { let collections: Vec<Value> = if include_collections {
// Get all collections for the user here already to prevent more queries
let cu: HashMap<String, CollectionUser> =
CollectionUser::find_by_organization_and_user_uuid(&self.org_uuid, &self.user_uuid, conn) CollectionUser::find_by_organization_and_user_uuid(&self.org_uuid, &self.user_uuid, conn)
.await .await
.iter() .into_iter()
.map(|cu| { .map(|cu| (cu.collection_uuid.clone(), cu))
json!({ .collect();
"id": cu.collection_uuid,
"readOnly": cu.read_only, // Get all collection groups for this user to prevent there inclusion
"hidePasswords": cu.hide_passwords, let cg: HashSet<String> = CollectionGroup::find_by_user(&self.user_uuid, conn)
}) .await
.into_iter()
.map(|cg| cg.collections_uuid)
.collect();
Collection::find_by_organization_and_user_uuid(&self.org_uuid, &self.user_uuid, conn)
.await
.into_iter()
.filter_map(|c| {
let (read_only, hide_passwords, can_manage) = if self.has_full_access() {
(false, false, self.atype >= UserOrgType::Manager)
} else if let Some(cu) = cu.get(&c.uuid) {
(
cu.read_only,
cu.hide_passwords,
self.atype == UserOrgType::Manager && !cu.read_only && !cu.hide_passwords,
)
// If previous checks failed it might be that this user has access via a group, but we should not return those elements here
// Those are returned via a special group endpoint
} else if cg.contains(&c.uuid) {
return None;
} else {
(true, true, false)
};
Some(json!({
"id": c.uuid,
"readOnly": read_only,
"hidePasswords": hide_passwords,
"manage": can_manage,
}))
}) })
.collect() .collect()
} else { } else {
Vec::with_capacity(0) Vec::with_capacity(0)
}; };
let permissions = json!({
// TODO: Add support for Custom User Roles
// See: https://bitwarden.com/help/article/user-types-access-control/#custom-role
"accessEventLogs": false,
"accessImportExport": false,
"accessReports": false,
"createNewCollections": false,
"editAnyCollection": false,
"deleteAnyCollection": false,
"editAssignedCollections": false,
"deleteAssignedCollections": false,
"manageGroups": false,
"managePolicies": false,
"manageSso": false, // Not supported
"manageUsers": false,
"manageResetPassword": false,
"manageScim": false // Not supported (Not AGPLv3 Licensed)
});
json!({ json!({
"id": self.uuid, "id": self.uuid,
"userId": self.user_uuid, "userId": self.user_uuid,
"name": user.name, "name": if self.get_unrevoked_status() >= UserOrgStatus::Accepted as i32 { Some(user.name) } else { None },
"email": user.email, "email": user.email,
"externalId": self.external_id, "externalId": self.external_id,
"avatarColor": user.avatar_color,
"groups": groups, "groups": groups,
"collections": collections, "collections": collections,
@@ -482,6 +544,13 @@ impl UserOrganization {
"accessAll": self.access_all, "accessAll": self.access_all,
"twoFactorEnabled": twofactor_enabled, "twoFactorEnabled": twofactor_enabled,
"resetPasswordEnrolled": self.reset_password_key.is_some(), "resetPasswordEnrolled": self.reset_password_key.is_some(),
"hasMasterPassword": !user.password_hash.is_empty(),
"permissions": permissions,
"ssoBound": false, // Not supported
"usesKeyConnector": false, // Not supported
"accessSecretsManager": false, // Not supported (Not AGPLv3 Licensed)
"object": "organizationUserUserDetails", "object": "organizationUserUserDetails",
}) })
@@ -595,7 +664,7 @@ impl UserOrganization {
} }
pub async fn find_by_email_and_org(email: &str, org_id: &str, conn: &mut DbConn) -> Option<UserOrganization> { pub async fn find_by_email_and_org(email: &str, org_id: &str, conn: &mut DbConn) -> Option<UserOrganization> {
if let Some(user) = super::User::find_by_mail(email, conn).await { if let Some(user) = User::find_by_mail(email, conn).await {
if let Some(user_org) = UserOrganization::find_by_user_and_org(&user.uuid, org_id, conn).await { if let Some(user_org) = UserOrganization::find_by_user_and_org(&user.uuid, org_id, conn).await {
return Some(user_org); return Some(user_org);
} }

View File

@@ -13,6 +13,7 @@ db_object! {
// must complete 2FA login before being added into the devices table. // must complete 2FA login before being added into the devices table.
pub device_uuid: String, pub device_uuid: String,
pub device_name: String, pub device_name: String,
pub device_type: i32,
pub login_time: NaiveDateTime, pub login_time: NaiveDateTime,
pub ip_address: String, pub ip_address: String,
} }
@@ -23,6 +24,7 @@ impl TwoFactorIncomplete {
user_uuid: &str, user_uuid: &str,
device_uuid: &str, device_uuid: &str,
device_name: &str, device_name: &str,
device_type: i32,
ip: &ClientIp, ip: &ClientIp,
conn: &mut DbConn, conn: &mut DbConn,
) -> EmptyResult { ) -> EmptyResult {
@@ -44,6 +46,7 @@ impl TwoFactorIncomplete {
twofactor_incomplete::user_uuid.eq(user_uuid), twofactor_incomplete::user_uuid.eq(user_uuid),
twofactor_incomplete::device_uuid.eq(device_uuid), twofactor_incomplete::device_uuid.eq(device_uuid),
twofactor_incomplete::device_name.eq(device_name), twofactor_incomplete::device_name.eq(device_name),
twofactor_incomplete::device_type.eq(device_type),
twofactor_incomplete::login_time.eq(Utc::now().naive_utc()), twofactor_incomplete::login_time.eq(Utc::now().naive_utc()),
twofactor_incomplete::ip_address.eq(ip.ip.to_string()), twofactor_incomplete::ip_address.eq(ip.ip.to_string()),
)) ))

View File

@@ -1,3 +1,4 @@
use crate::util::{format_date, get_uuid, retry};
use chrono::{NaiveDateTime, TimeDelta, Utc}; use chrono::{NaiveDateTime, TimeDelta, Utc};
use serde_json::Value; use serde_json::Value;
@@ -90,7 +91,7 @@ impl User {
let email = email.to_lowercase(); let email = email.to_lowercase();
Self { Self {
uuid: crate::util::get_uuid(), uuid: get_uuid(),
enabled: true, enabled: true,
created_at: now, created_at: now,
updated_at: now, updated_at: now,
@@ -107,7 +108,7 @@ impl User {
salt: crypto::get_random_bytes::<64>().to_vec(), salt: crypto::get_random_bytes::<64>().to_vec(),
password_iterations: CONFIG.password_iterations(), password_iterations: CONFIG.password_iterations(),
security_stamp: crate::util::get_uuid(), security_stamp: get_uuid(),
stamp_exception: None, stamp_exception: None,
password_hint: None, password_hint: None,
@@ -144,14 +145,14 @@ impl User {
pub fn check_valid_recovery_code(&self, recovery_code: &str) -> bool { pub fn check_valid_recovery_code(&self, recovery_code: &str) -> bool {
if let Some(ref totp_recover) = self.totp_recover { if let Some(ref totp_recover) = self.totp_recover {
crate::crypto::ct_eq(recovery_code, totp_recover.to_lowercase()) crypto::ct_eq(recovery_code, totp_recover.to_lowercase())
} else { } else {
false false
} }
} }
pub fn check_valid_api_key(&self, key: &str) -> bool { pub fn check_valid_api_key(&self, key: &str) -> bool {
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 crypto::ct_eq(api_key, key))
} }
/// Set the password hash generated /// Set the password hash generated
@@ -188,7 +189,7 @@ impl User {
} }
pub fn reset_security_stamp(&mut self) { pub fn reset_security_stamp(&mut self) {
self.security_stamp = crate::util::get_uuid(); self.security_stamp = get_uuid();
} }
/// Set the stamp_exception to only allow a subsequent request matching a specific route using the current security-stamp. /// Set the stamp_exception to only allow a subsequent request matching a specific route using the current security-stamp.
@@ -259,6 +260,7 @@ impl User {
"forcePasswordReset": false, "forcePasswordReset": false,
"avatarColor": self.avatar_color, "avatarColor": self.avatar_color,
"usesKeyConnector": false, "usesKeyConnector": false,
"creationDate": format_date(&self.created_at),
"object": "profile", "object": "profile",
}) })
} }
@@ -340,7 +342,7 @@ impl User {
let updated_at = Utc::now().naive_utc(); let updated_at = Utc::now().naive_utc();
db_run! {conn: { db_run! {conn: {
crate::util::retry(|| { retry(|| {
diesel::update(users::table) diesel::update(users::table)
.set(users::updated_at.eq(updated_at)) .set(users::updated_at.eq(updated_at))
.execute(conn) .execute(conn)
@@ -357,7 +359,7 @@ impl User {
async fn _update_revision(uuid: &str, date: &NaiveDateTime, conn: &mut DbConn) -> EmptyResult { async fn _update_revision(uuid: &str, date: &NaiveDateTime, conn: &mut DbConn) -> EmptyResult {
db_run! {conn: { db_run! {conn: {
crate::util::retry(|| { retry(|| {
diesel::update(users::table.filter(users::uuid.eq(uuid))) diesel::update(users::table.filter(users::uuid.eq(uuid)))
.set(users::updated_at.eq(date)) .set(users::updated_at.eq(date))
.execute(conn) .execute(conn)

View File

@@ -169,6 +169,7 @@ table! {
user_uuid -> Text, user_uuid -> Text,
device_uuid -> Text, device_uuid -> Text,
device_name -> Text, device_name -> Text,
device_type -> Integer,
login_time -> Timestamp, login_time -> Timestamp,
ip_address -> Text, ip_address -> Text,
} }

View File

@@ -169,6 +169,7 @@ table! {
user_uuid -> Text, user_uuid -> Text,
device_uuid -> Text, device_uuid -> Text,
device_name -> Text, device_name -> Text,
device_type -> Integer,
login_time -> Timestamp, login_time -> Timestamp,
ip_address -> Text, ip_address -> Text,
} }

View File

@@ -169,6 +169,7 @@ table! {
user_uuid -> Text, user_uuid -> Text,
device_uuid -> Text, device_uuid -> Text,
device_name -> Text, device_name -> Text,
device_type -> Integer,
login_time -> Timestamp, login_time -> Timestamp,
ip_address -> Text, ip_address -> Text,
} }

View File

@@ -209,7 +209,7 @@ use rocket::http::{ContentType, Status};
use rocket::request::Request; use rocket::request::Request;
use rocket::response::{self, Responder, Response}; use rocket::response::{self, Responder, Response};
impl<'r> Responder<'r, 'static> for Error { impl Responder<'_, 'static> for Error {
fn respond_to(self, _: &Request<'_>) -> response::Result<'static> { fn respond_to(self, _: &Request<'_>) -> response::Result<'static> {
match self.error { match self.error {
ErrorKind::Empty(_) => {} // Don't print the error in this situation ErrorKind::Empty(_) => {} // Don't print the error in this situation

View File

@@ -102,9 +102,9 @@ fn should_block_address_regex(domain_or_ip: &str) -> bool {
fn should_block_host(host: Host<&str>) -> Result<(), CustomHttpClientError> { fn should_block_host(host: Host<&str>) -> Result<(), CustomHttpClientError> {
let (ip, host_str): (Option<IpAddr>, String) = match host { let (ip, host_str): (Option<IpAddr>, String) = match host {
url::Host::Ipv4(ip) => (Some(ip.into()), ip.to_string()), Host::Ipv4(ip) => (Some(ip.into()), ip.to_string()),
url::Host::Ipv6(ip) => (Some(ip.into()), ip.to_string()), Host::Ipv6(ip) => (Some(ip.into()), ip.to_string()),
url::Host::Domain(d) => (None, d.to_string()), Host::Domain(d) => (None, d.to_string()),
}; };
if let Some(ip) = ip { if let Some(ip) = ip {

View File

@@ -17,6 +17,7 @@ use crate::{
encode_jwt, generate_delete_claims, generate_emergency_access_invite_claims, generate_invite_claims, encode_jwt, generate_delete_claims, generate_emergency_access_invite_claims, generate_invite_claims,
generate_verify_email_claims, generate_verify_email_claims,
}, },
db::models::{Device, DeviceType, User},
error::Error, error::Error,
CONFIG, CONFIG,
}; };
@@ -229,37 +230,51 @@ pub async fn send_single_org_removed_from_org(address: &str, org_name: &str) ->
} }
pub async fn send_invite( pub async fn send_invite(
address: &str, user: &User,
uuid: &str,
org_id: Option<String>, org_id: Option<String>,
org_user_id: Option<String>, org_user_id: Option<String>,
org_name: &str, org_name: &str,
invited_by_email: Option<String>, invited_by_email: Option<String>,
) -> EmptyResult { ) -> EmptyResult {
let claims = generate_invite_claims( let claims = generate_invite_claims(
uuid.to_string(), user.uuid.clone(),
String::from(address), user.email.clone(),
org_id.clone(), org_id.clone(),
org_user_id.clone(), org_user_id.clone(),
invited_by_email, invited_by_email,
); );
let invite_token = encode_jwt(&claims); let invite_token = encode_jwt(&claims);
let mut query = url::Url::parse("https://query.builder").unwrap();
{
let mut query_params = query.query_pairs_mut();
query_params
.append_pair("email", &user.email)
.append_pair("organizationName", org_name)
.append_pair("organizationId", org_id.as_deref().unwrap_or("_"))
.append_pair("organizationUserId", org_user_id.as_deref().unwrap_or("_"))
.append_pair("token", &invite_token);
if user.private_key.is_some() {
query_params.append_pair("orgUserHasExistingUser", "true");
}
}
let query_string = match query.query() {
None => err!(format!("Failed to build invite URL query parameters")),
Some(query) => query,
};
// `url.Url` would place the anchor `#` after the query parameters
let url = format!("{}/#/accept-organization/?{}", CONFIG.domain(), query_string);
let (subject, body_html, body_text) = get_text( let (subject, body_html, body_text) = get_text(
"email/send_org_invite", "email/send_org_invite",
json!({ json!({
"url": CONFIG.domain(), "url": url,
"img_src": CONFIG._smtp_img_src(), "img_src": CONFIG._smtp_img_src(),
"org_id": org_id.as_deref().unwrap_or("_"),
"org_user_id": org_user_id.as_deref().unwrap_or("_"),
"email": percent_encode(address.as_bytes(), NON_ALPHANUMERIC).to_string(),
"org_name_encoded": percent_encode(org_name.as_bytes(), NON_ALPHANUMERIC).to_string(),
"org_name": org_name, "org_name": org_name,
"token": invite_token,
}), }),
)?; )?;
send_email(address, &subject, body_html, body_text).await send_email(&user.email, &subject, body_html, body_text).await
} }
pub async fn send_emergency_access_invite( pub async fn send_emergency_access_invite(
@@ -427,9 +442,8 @@ pub async fn send_invite_confirmed(address: &str, org_name: &str) -> EmptyResult
send_email(address, &subject, body_html, body_text).await send_email(address, &subject, body_html, body_text).await
} }
pub async fn send_new_device_logged_in(address: &str, ip: &str, dt: &NaiveDateTime, device: &str) -> EmptyResult { pub async fn send_new_device_logged_in(address: &str, ip: &str, dt: &NaiveDateTime, device: &Device) -> EmptyResult {
use crate::util::upcase_first; use crate::util::upcase_first;
let device = upcase_first(device);
let fmt = "%A, %B %_d, %Y at %r %Z"; let fmt = "%A, %B %_d, %Y at %r %Z";
let (subject, body_html, body_text) = get_text( let (subject, body_html, body_text) = get_text(
@@ -438,7 +452,8 @@ pub async fn send_new_device_logged_in(address: &str, ip: &str, dt: &NaiveDateTi
"url": CONFIG.domain(), "url": CONFIG.domain(),
"img_src": CONFIG._smtp_img_src(), "img_src": CONFIG._smtp_img_src(),
"ip": ip, "ip": ip,
"device": device, "device_name": upcase_first(&device.name),
"device_type": DeviceType::from_i32(device.atype).to_string(),
"datetime": crate::util::format_naive_datetime_local(dt, fmt), "datetime": crate::util::format_naive_datetime_local(dt, fmt),
}), }),
)?; )?;
@@ -446,9 +461,14 @@ pub async fn send_new_device_logged_in(address: &str, ip: &str, dt: &NaiveDateTi
send_email(address, &subject, body_html, body_text).await send_email(address, &subject, body_html, body_text).await
} }
pub async fn send_incomplete_2fa_login(address: &str, ip: &str, dt: &NaiveDateTime, device: &str) -> EmptyResult { pub async fn send_incomplete_2fa_login(
address: &str,
ip: &str,
dt: &NaiveDateTime,
device_name: &str,
device_type: &str,
) -> EmptyResult {
use crate::util::upcase_first; use crate::util::upcase_first;
let device = upcase_first(device);
let fmt = "%A, %B %_d, %Y at %r %Z"; let fmt = "%A, %B %_d, %Y at %r %Z";
let (subject, body_html, body_text) = get_text( let (subject, body_html, body_text) = get_text(
@@ -457,7 +477,8 @@ pub async fn send_incomplete_2fa_login(address: &str, ip: &str, dt: &NaiveDateTi
"url": CONFIG.domain(), "url": CONFIG.domain(),
"img_src": CONFIG._smtp_img_src(), "img_src": CONFIG._smtp_img_src(),
"ip": ip, "ip": ip,
"device": device, "device_name": upcase_first(device_name),
"device_type": device_type,
"datetime": crate::util::format_naive_datetime_local(dt, fmt), "datetime": crate::util::format_naive_datetime_local(dt, fmt),
"time_limit": CONFIG.incomplete_2fa_time_limit(), "time_limit": CONFIG.incomplete_2fa_time_limit(),
}), }),

View File

@@ -40,6 +40,9 @@ use tokio::{
io::{AsyncBufReadExt, BufReader}, io::{AsyncBufReadExt, BufReader},
}; };
#[cfg(unix)]
use tokio::signal::unix::SignalKind;
#[macro_use] #[macro_use]
mod error; mod error;
mod api; mod api;
@@ -59,7 +62,7 @@ use crate::api::{WS_ANONYMOUS_SUBSCRIPTIONS, WS_USERS};
pub use config::CONFIG; pub use config::CONFIG;
pub use error::{Error, MapResult}; pub use error::{Error, MapResult};
use rocket::data::{Limits, ToByteUnit}; use rocket::data::{Limits, ToByteUnit};
use std::sync::Arc; use std::sync::{atomic::Ordering, Arc};
pub use util::is_running_in_container; pub use util::is_running_in_container;
#[rocket::main] #[rocket::main]
@@ -83,7 +86,7 @@ async fn main() -> Result<(), Error> {
let pool = create_db_pool().await; let pool = create_db_pool().await;
schedule_jobs(pool.clone()); schedule_jobs(pool.clone());
crate::db::models::TwoFactor::migrate_u2f_to_webauthn(&mut pool.get().await.unwrap()).await.unwrap(); db::models::TwoFactor::migrate_u2f_to_webauthn(&mut pool.get().await.unwrap()).await.unwrap();
let extra_debug = matches!(level, log::LevelFilter::Trace | log::LevelFilter::Debug); let extra_debug = matches!(level, log::LevelFilter::Trace | log::LevelFilter::Debug);
launch_rocket(pool, extra_debug).await // Blocks until program termination. launch_rocket(pool, extra_debug).await // Blocks until program termination.
@@ -97,10 +100,12 @@ USAGE:
FLAGS: FLAGS:
-h, --help Prints help information -h, --help Prints help information
-v, --version Prints the app version -v, --version Prints the app and web-vault version
COMMAND: COMMAND:
hash [--preset {bitwarden|owasp}] Generate an Argon2id PHC ADMIN_TOKEN hash [--preset {bitwarden|owasp}] Generate an Argon2id PHC ADMIN_TOKEN
backup Create a backup of the SQLite database
You can also send the USR1 signal to trigger a backup
PRESETS: m= t= p= PRESETS: m= t= p=
bitwarden (default) 64MiB, 3 Iterations, 4 Threads bitwarden (default) 64MiB, 3 Iterations, 4 Threads
@@ -115,11 +120,14 @@ fn parse_args() {
let version = VERSION.unwrap_or("(Version info from Git not present)"); let version = VERSION.unwrap_or("(Version info from Git not present)");
if pargs.contains(["-h", "--help"]) { if pargs.contains(["-h", "--help"]) {
println!("vaultwarden {version}"); println!("Vaultwarden {version}");
print!("{HELP}"); print!("{HELP}");
exit(0); exit(0);
} else if pargs.contains(["-v", "--version"]) { } else if pargs.contains(["-v", "--version"]) {
println!("vaultwarden {version}"); config::SKIP_CONFIG_VALIDATION.store(true, Ordering::Relaxed);
let web_vault_version = util::get_web_vault_version();
println!("Vaultwarden {version}");
println!("Web-Vault {web_vault_version}");
exit(0); exit(0);
} }
@@ -163,7 +171,7 @@ fn parse_args() {
} }
let argon2 = Argon2::new(Argon2id, V0x13, argon2_params.build().unwrap()); let argon2 = Argon2::new(Argon2id, V0x13, argon2_params.build().unwrap());
let salt = SaltString::encode_b64(&crate::crypto::get_random_bytes::<32>()).unwrap(); let salt = SaltString::encode_b64(&crypto::get_random_bytes::<32>()).unwrap();
let argon2_timer = tokio::time::Instant::now(); let argon2_timer = tokio::time::Instant::now();
if let Ok(password_hash) = argon2.hash_password(password.as_bytes(), &salt) { if let Ok(password_hash) = argon2.hash_password(password.as_bytes(), &salt) {
@@ -174,13 +182,47 @@ fn parse_args() {
argon2_timer.elapsed() argon2_timer.elapsed()
); );
} else { } else {
error!("Unable to generate Argon2id PHC hash."); println!("Unable to generate Argon2id PHC hash.");
exit(1); exit(1);
} }
} else if command == "backup" {
match backup_sqlite() {
Ok(f) => {
println!("Backup to '{f}' was successful");
exit(0);
}
Err(e) => {
println!("Backup failed. {e:?}");
exit(1);
}
}
} }
exit(0); exit(0);
} }
} }
fn backup_sqlite() -> Result<String, Error> {
#[cfg(sqlite)]
{
use crate::db::{backup_sqlite_database, DbConnType};
if DbConnType::from_url(&CONFIG.database_url()).map(|t| t == DbConnType::sqlite).unwrap_or(false) {
use diesel::Connection;
let url = CONFIG.database_url();
// Establish a connection to the sqlite database
let mut conn = diesel::sqlite::SqliteConnection::establish(&url)?;
let backup_file = backup_sqlite_database(&mut conn)?;
Ok(backup_file)
} else {
err_silent!("The database type is not SQLite. Backups only works for SQLite databases")
}
}
#[cfg(not(sqlite))]
{
err_silent!("The 'sqlite' feature is not enabled. Backups only works for SQLite databases")
}
}
fn launch_info() { fn launch_info() {
println!( println!(
"\ "\
@@ -344,15 +386,15 @@ fn init_logging() -> Result<log::LevelFilter, Error> {
{ {
logger = logger.chain(fern::log_file(log_file)?); logger = logger.chain(fern::log_file(log_file)?);
} }
#[cfg(not(windows))] #[cfg(unix)]
{ {
const SIGHUP: i32 = tokio::signal::unix::SignalKind::hangup().as_raw_value(); const SIGHUP: i32 = SignalKind::hangup().as_raw_value();
let path = Path::new(&log_file); let path = Path::new(&log_file);
logger = logger.chain(fern::log_reopen1(path, [SIGHUP])?); logger = logger.chain(fern::log_reopen1(path, [SIGHUP])?);
} }
} }
#[cfg(not(windows))] #[cfg(unix)]
{ {
if cfg!(feature = "enable_syslog") || CONFIG.use_syslog() { if cfg!(feature = "enable_syslog") || CONFIG.use_syslog() {
logger = chain_syslog(logger); logger = chain_syslog(logger);
@@ -402,7 +444,7 @@ fn init_logging() -> Result<log::LevelFilter, Error> {
Ok(level) Ok(level)
} }
#[cfg(not(windows))] #[cfg(unix)]
fn chain_syslog(logger: fern::Dispatch) -> fern::Dispatch { fn chain_syslog(logger: fern::Dispatch) -> fern::Dispatch {
let syslog_fmt = syslog::Formatter3164 { let syslog_fmt = syslog::Formatter3164 {
facility: syslog::Facility::LOG_USER, facility: syslog::Facility::LOG_USER,
@@ -474,10 +516,10 @@ async fn container_data_folder_is_persistent(data_folder: &str) -> bool {
format!(" /{data_folder} ") format!(" /{data_folder} ")
}; };
let mut lines = BufReader::new(mountinfo).lines(); let mut lines = BufReader::new(mountinfo).lines();
let re = regex::Regex::new(r"/volumes/[a-z0-9]{64}/_data /").unwrap();
while let Some(line) = lines.next_line().await.unwrap_or_default() { while let Some(line) = lines.next_line().await.unwrap_or_default() {
// Only execute a regex check if we find the base match // Only execute a regex check if we find the base match
if line.contains(&data_folder_match) { if line.contains(&data_folder_match) {
let re = regex::Regex::new(r"/volumes/[a-z0-9]{64}/_data /").unwrap();
if re.is_match(&line) { if re.is_match(&line) {
return false; return false;
} }
@@ -560,7 +602,23 @@ async fn launch_rocket(pool: db::DbPool, extra_debug: bool) -> Result<(), Error>
CONFIG.shutdown(); CONFIG.shutdown();
}); });
let _ = instance.launch().await?; #[cfg(unix)]
{
tokio::spawn(async move {
let mut signal_user1 = tokio::signal::unix::signal(SignalKind::user_defined1()).unwrap();
loop {
// If we need more signals to act upon, we might want to use select! here.
// With only one item to listen for this is enough.
let _ = signal_user1.recv().await;
match backup_sqlite() {
Ok(f) => info!("Backup to '{f}' was successful"),
Err(e) => error!("Backup failed. {e:?}"),
}
}
});
}
instance.launch().await?;
info!("Vaultwarden process exited!"); info!("Vaultwarden process exited!");
Ok(()) Ok(())

View File

@@ -49,8 +49,8 @@ function _post(url, successMsg, errMsg, body, reload_page = true) {
}).then(respText => { }).then(respText => {
try { try {
const respJson = JSON.parse(respText); const respJson = JSON.parse(respText);
if (respJson.ErrorModel && respJson.ErrorModel.Message) { if (respJson.errorModel && respJson.errorModel.message) {
return respJson.ErrorModel.Message; return respJson.errorModel.message;
} else { } else {
return Promise.reject({ body: `${respStatus} - ${respStatusText}\n\nUnknown error`, error: true }); return Promise.reject({ body: `${respStatus} - ${respStatusText}\n\nUnknown error`, error: true });
} }

View File

@@ -7,7 +7,7 @@
<table class="footer" cellpadding="0" cellspacing="0" width="100%" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; width: 100%;"> <table class="footer" cellpadding="0" cellspacing="0" width="100%" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; width: 100%;">
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;"> <tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
<td class="aligncenter social-icons" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 15px 0 0 0;" valign="top"> <td class="aligncenter social-icons" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 5px 0 20px 0;" valign="top">
<table cellpadding="0" cellspacing="0" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto;"> <table cellpadding="0" cellspacing="0" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto;">
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;"> <tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
<td style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 0 10px;" valign="top"><a href="https://github.com/dani-garcia/vaultwarden" target="_blank" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; text-decoration: underline;"><img src="{{img_src}}mail-github.png" alt="GitHub" width="30" height="30" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;" /></a></td> <td style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 0 10px;" valign="top"><a href="https://github.com/dani-garcia/vaultwarden" target="_blank" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; text-decoration: underline;"><img src="{{img_src}}mail-github.png" alt="GitHub" width="30" height="30" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;" /></a></td>

View File

@@ -1,10 +1,11 @@
Incomplete Two-Step Login From {{{device}}} Incomplete Two-Step Login From {{{device_name}}}
<!----------------> <!---------------->
Someone attempted to log into your account with the correct master password, but did not provide the correct token or action required to complete the two-step login process within {{time_limit}} minutes of the initial login attempt. Someone attempted to log into your account with the correct master password, but did not provide the correct token or action required to complete the two-step login process within {{time_limit}} minutes of the initial login attempt.
* Date: {{datetime}} * Date: {{datetime}}
* IP Address: {{ip}} * IP Address: {{ip}}
* Device Type: {{device}} * Device Name: {{device_name}}
* Device Type: {{device_type}}
If this was not you or someone you authorized, then you should change your master password as soon as possible, as it is likely to be compromised. If this was not you or someone you authorized, then you should change your master password as soon as possible, as it is likely to be compromised.
{{> email/email_footer_text }} {{> email/email_footer_text }}

View File

@@ -1,4 +1,4 @@
Incomplete Two-Step Login From {{{device}}} Incomplete Two-Step Login From {{{device_name}}}
<!----------------> <!---------------->
{{> email/email_header }} {{> email/email_header }}
<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;">
@@ -19,7 +19,12 @@ Incomplete Two-Step Login From {{{device}}}
</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;">
<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;" valign="top"> <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;" valign="top">
<b>Device Type:</b> {{device}} <b>Device Name:</b> {{device_name}}
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<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;" valign="top">
<b>Device Type:</b> {{device_type}}
</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,10 +1,11 @@
New Device Logged In From {{{device}}} New Device Logged In From {{{device_name}}}
<!----------------> <!---------------->
Your account was just logged into from a new device. Your account was just logged into from a new device.
* Date: {{datetime}} * Date: {{datetime}}
* IP Address: {{ip}} * IP Address: {{ip}}
* Device Type: {{device}} * Device Name: {{device_name}}
* Device Type: {{device_type}}
You can deauthorize all devices that have access to your account from the web vault ( {{url}} ) under Settings > My Account > Deauthorize Sessions. You can deauthorize all devices that have access to your account from the web vault ( {{url}} ) under Settings > My Account > Deauthorize Sessions.
{{> email/email_footer_text }} {{> email/email_footer_text }}

View File

@@ -1,4 +1,4 @@
New Device Logged In From {{{device}}} New Device Logged In From {{{device_name}}}
<!----------------> <!---------------->
{{> email/email_header }} {{> email/email_header }}
<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;">
@@ -9,7 +9,7 @@ New Device Logged In From {{{device}}}
</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;">
<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;" valign="top"> <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;" valign="top">
<b>Date</b>: {{datetime}} <b>Date:</b> {{datetime}}
</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;">
@@ -19,7 +19,12 @@ New Device Logged In From {{{device}}}
</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;">
<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;" valign="top"> <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;" valign="top">
<b>Device Type:</b> {{device}} <b>Device Name:</b> {{device_name}}
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<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;" valign="top">
<b>Device Type:</b> {{device_type}}
</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

@@ -3,7 +3,7 @@ Join {{{org_name}}}
You have been invited to join the *{{org_name}}* organization. You have been invited to join the *{{org_name}}* organization.
Click here to join: {{url}}/#/accept-organization/?organizationId={{org_id}}&organizationUserId={{org_user_id}}&email={{email}}&organizationName={{org_name_encoded}}&token={{token}} Click here to join: {{{url}}}
If you do not wish to join this organization, you can safely ignore this email. If you do not wish to join this organization, you can safely ignore this email.

View File

@@ -9,7 +9,7 @@ Join {{{org_name}}}
</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;">
<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">
<a href="{{url}}/#/accept-organization/?organizationId={{org_id}}&organizationUserId={{org_user_id}}&email={{email}}&organizationName={{org_name_encoded}}&token={{token}}" <a href="{{{url}}}"
clicktracking=off target="_blank" style="color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #3c8dbc; border-color: #3c8dbc; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> clicktracking=off target="_blank" style="color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #3c8dbc; border-color: #3c8dbc; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
Join Organization Now Join Organization Now
</a> </a>

View File

@@ -213,7 +213,7 @@ impl<'r, R: 'r + Responder<'r, 'static> + Send> Responder<'r, 'static> for Cache
}; };
res.set_raw_header("Cache-Control", cache_control_header); res.set_raw_header("Cache-Control", cache_control_header);
let time_now = chrono::Local::now(); let time_now = Local::now();
let expiry_time = time_now + chrono::TimeDelta::try_seconds(self.ttl.try_into().unwrap()).unwrap(); let expiry_time = time_now + chrono::TimeDelta::try_seconds(self.ttl.try_into().unwrap()).unwrap();
res.set_raw_header("Expires", format_datetime_http(&expiry_time)); res.set_raw_header("Expires", format_datetime_http(&expiry_time));
Ok(res) Ok(res)
@@ -222,8 +222,8 @@ impl<'r, R: 'r + Responder<'r, 'static> + Send> Responder<'r, 'static> for Cache
pub struct SafeString(String); pub struct SafeString(String);
impl std::fmt::Display for SafeString { impl fmt::Display for SafeString {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f) self.0.fmt(f)
} }
} }
@@ -513,6 +513,28 @@ pub fn container_base_image() -> &'static str {
} }
} }
#[derive(Deserialize)]
struct WebVaultVersion {
version: String,
}
pub fn get_web_vault_version() -> String {
let version_files = [
format!("{}/vw-version.json", CONFIG.web_vault_folder()),
format!("{}/version.json", CONFIG.web_vault_folder()),
];
for version_file in version_files {
if let Ok(version_str) = std::fs::read_to_string(&version_file) {
if let Ok(version) = serde_json::from_str::<WebVaultVersion>(&version_str) {
return String::from(version.version.trim_start_matches('v'));
}
}
}
String::from("Version file missing")
}
// //
// Deserialization methods // Deserialization methods
// //
@@ -590,7 +612,7 @@ impl<'de> Visitor<'de> for LowerCaseVisitor {
fn _process_key(key: &str) -> String { fn _process_key(key: &str) -> String {
match key.to_lowercase().as_ref() { match key.to_lowercase().as_ref() {
"ssn" => "ssn".into(), "ssn" => "ssn".into(),
_ => self::lcase_first(key), _ => lcase_first(key),
} }
} }