mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2025-09-10 10:45:57 +03:00
Compare commits
32 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
040e2a7bb0 | ||
|
d184c8f08c | ||
|
7d6dec6413 | ||
|
de01111082 | ||
|
0bd8f607cb | ||
|
21efc0800d | ||
|
1031c2e286 | ||
|
1bf85201e7 | ||
|
6ceed9284d | ||
|
25d99e3506 | ||
|
dca14285fd | ||
|
66baa5e7d8 | ||
|
248e561b3f | ||
|
55623ad9c6 | ||
|
e9acd8bd3c | ||
|
544b7229e8 | ||
|
978f009293 | ||
|
92f1530e96 | ||
|
2b824e8096 | ||
|
059661be48 | ||
|
0f3f97cc76 | ||
|
aa0fe7785a | ||
|
65d11a9720 | ||
|
c722006385 | ||
|
aaab7f9640 | ||
|
cbdb5657f1 | ||
|
669b9db758 | ||
|
3466a8040e | ||
|
7d47155d83 | ||
|
9e26014b4d | ||
|
339612c917 | ||
|
9eebbf3b9f |
@@ -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
|
||||||
|
@@ -425,6 +425,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 ###
|
||||||
########################
|
########################
|
||||||
|
66
.github/ISSUE_TEMPLATE/bug_report.md
vendored
66
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -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
167
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal 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 [](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.
|
10
.github/ISSUE_TEMPLATE/config.yml
vendored
10
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -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.
|
||||||
|
5
.github/workflows/build.yml
vendored
5
.github/workflows/build.yml
vendored
@@ -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.
|
||||||
@@ -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}}"
|
||||||
|
28
.github/workflows/hadolint.yml
vendored
28
.github/workflows/hadolint.yml
vendored
@@ -8,7 +8,7 @@ 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
|
||||||
@@ -16,6 +16,18 @@ jobs:
|
|||||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||||
# End Checkout the repo
|
# End Checkout the repo
|
||||||
|
|
||||||
|
# Start Docker Buildx
|
||||||
|
- name: Setup Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3.6.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
|
||||||
|
20
.github/workflows/release.yml
vendored
20
.github/workflows/release.yml
vendored
@@ -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' }}
|
||||||
@@ -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@988b5a0280414f521da01fcc63a27aeeb4b104db # v3.6.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@76cc8060bdff6d632a465001e4cf300684c5472c # v5.7.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@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
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@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
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@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
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@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
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
|
||||||
|
2
.github/workflows/releasecache-cleanup.yml
vendored
2
.github/workflows/releasecache-cleanup.yml
vendored
@@ -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:
|
||||||
|
2
.github/workflows/trivy.yml
vendored
2
.github/workflows/trivy.yml
vendored
@@ -17,7 +17,7 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
trivy-scan:
|
trivy-scan:
|
||||||
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
|
||||||
|
956
Cargo.lock
generated
956
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
58
Cargo.toml
58
Cargo.toml
@@ -18,17 +18,17 @@ 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
|
||||||
@@ -63,23 +63,23 @@ 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.30"
|
||||||
tokio = { version = "1.39.2", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] }
|
tokio = { version = "1.40.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.210", features = ["derive"] }
|
||||||
serde_json = "1.0.122"
|
serde_json = "1.0.128"
|
||||||
|
|
||||||
# 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"] }
|
||||||
@@ -90,7 +90,7 @@ uuid = { version = "1.10.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.9", 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.7", 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.10.6", features = ["std", "perf", "unicode-perl"], default-features = false }
|
||||||
data-url = "0.3.1"
|
data-url = "0.3.1"
|
||||||
bytes = "1.7.1"
|
bytes = "1.7.2"
|
||||||
|
|
||||||
# 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"] }
|
||||||
@@ -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"
|
||||||
|
12
SECURITY.md
12
SECURITY.md
@@ -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  to report a security issue.
|
- You can send an  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
|
||||||
|
@@ -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.81.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
|
||||||
|
@@ -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.81.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.81.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.81.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.81.0 AS build_armv6
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
# hadolint ignore=DL3006
|
# hadolint ignore=DL3006
|
||||||
|
@@ -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.81.0-slim-bookworm AS build
|
||||||
COPY --from=xx / /
|
COPY --from=xx / /
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
ARG TARGETVARIANT
|
ARG TARGETVARIANT
|
||||||
|
@@ -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
|
||||||
|
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `twofactor_incomplete` DROP COLUMN `device_type`;
|
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE twofactor_incomplete ADD COLUMN device_type INTEGER NOT NULL DEFAULT 14; -- 14 = Unknown Browser
|
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE twofactor_incomplete DROP COLUMN device_type;
|
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE twofactor_incomplete ADD COLUMN device_type INTEGER NOT NULL DEFAULT 14; -- 14 = Unknown Browser
|
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `twofactor_incomplete` DROP COLUMN `device_type`;
|
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE twofactor_incomplete ADD COLUMN device_type INTEGER NOT NULL DEFAULT 14; -- 14 = Unknown Browser
|
@@ -1,4 +1,4 @@
|
|||||||
[toolchain]
|
[toolchain]
|
||||||
channel = "1.80.1"
|
channel = "1.81.0"
|
||||||
components = [ "rustfmt", "clippy" ]
|
components = [ "rustfmt", "clippy" ]
|
||||||
profile = "minimal"
|
profile = "minimal"
|
||||||
|
@@ -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)");
|
||||||
}
|
}
|
||||||
|
@@ -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(),
|
||||||
|
@@ -233,7 +233,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 +287,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 +563,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 +703,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>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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,
|
||||||
|
@@ -509,7 +509,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>")]
|
||||||
@@ -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")]
|
||||||
|
@@ -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,15 +123,8 @@ 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,
|
.await?;
|
||||||
&user.uuid,
|
|
||||||
Some(org_id.clone()),
|
|
||||||
Some(new_org_user.uuid),
|
|
||||||
&org_name,
|
|
||||||
Some(org_email),
|
|
||||||
)
|
|
||||||
.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") {
|
||||||
|
@@ -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!(
|
||||||
|
@@ -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:?}")),
|
||||||
};
|
};
|
||||||
|
@@ -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"),
|
||||||
|
@@ -269,8 +269,14 @@ 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(
|
||||||
.await
|
&user.email,
|
||||||
|
&login.ip_address,
|
||||||
|
&login.login_time,
|
||||||
|
&login.device_name,
|
||||||
|
&DeviceType::from_i32(login.device_type).to_string(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
if let Err(e) = login.delete(&mut conn).await {
|
if let Err(e) = login.delete(&mut conn).await {
|
||||||
|
@@ -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"),
|
||||||
|
@@ -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() {
|
||||||
|
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -135,6 +135,18 @@ async fn _refresh_login(data: ConnectData, conn: &mut DbConn) -> JsonResult {
|
|||||||
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 +265,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 +294,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,9 +339,7 @@ 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,
|
"unofficialServer": true,
|
||||||
@@ -381,7 +421,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() {
|
||||||
@@ -495,7 +535,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
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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());
|
||||||
|
@@ -331,7 +331,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)
|
||||||
}};
|
}};
|
||||||
@@ -349,10 +349,10 @@ macro_rules! make_config {
|
|||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// Where action applied when the value wasn't provided and can be:
|
// Where action applied when the value wasn't provided and can be:
|
||||||
// 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 +515,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 +524,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 +624,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 +695,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!
|
||||||
@@ -1220,7 +1225,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 +1256,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)
|
||||||
|
@@ -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,23 +366,31 @@ 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)
|
||||||
let db_url = CONFIG.database_url();
|
|
||||||
let db_path = Path::new(&db_url).parent().unwrap().to_string_lossy();
|
|
||||||
let file_date = chrono::Utc::now().format("%Y%m%d_%H%M%S").to_string();
|
|
||||||
diesel::sql_query(format!("VACUUM INTO '{db_path}/db_{file_date}.sqlite3'")).execute(conn)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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_path = std::path::Path::new(&db_url).parent().unwrap();
|
||||||
|
let backup_file = db_path
|
||||||
|
.join(format!("db_{}.sqlite3", chrono::Utc::now().format("%Y%m%d_%H%M%S")))
|
||||||
|
.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
|
||||||
pub async fn get_sql_server_version(conn: &mut DbConn) -> String {
|
pub async fn get_sql_server_version(conn: &mut DbConn) -> String {
|
||||||
db_run! {@raw conn:
|
db_run! {@raw conn:
|
||||||
|
@@ -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,48 @@ 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()
|
|
||||||
})
|
|
||||||
.map(|d| d.into_iter().map(|d| d.data).collect())
|
|
||||||
.unwrap_or_default();
|
|
||||||
let password_history_json: Vec<_> = self
|
|
||||||
.password_history
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|s| {
|
|
||||||
serde_json::from_str::<Vec<LowerCase<Value>>>(s)
|
|
||||||
.inspect_err(|e| warn!("Error parsing password history {:?}", e))
|
|
||||||
.ok()
|
.ok()
|
||||||
})
|
})
|
||||||
.map(|d| d.into_iter().map(|d| d.data).collect())
|
.map(|d| d.into_iter().map(|d| d.data).collect())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let password_history_json: Vec<_> = self
|
||||||
|
.password_history
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|s| {
|
||||||
|
serde_json::from_str::<Vec<LowerCase<Value>>>(s)
|
||||||
|
.inspect_err(|e| warn!("Error parsing password history {e:?} for {}", self.uuid))
|
||||||
|
.ok()
|
||||||
|
})
|
||||||
|
.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();
|
||||||
|
|
||||||
// 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,10 +230,13 @@ 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.
|
||||||
|
@@ -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 false, but they can manage all collections anyway
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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::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 {
|
||||||
|
@@ -342,9 +342,11 @@ 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 {
|
||||||
return policy.enabled;
|
if let Some(policy) = OrgPolicy::find_by_org_and_type(&membership.org_uuid, policy_type, conn).await {
|
||||||
|
return policy.enabled;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -453,15 +457,47 @@ impl UserOrganization {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let collections: Vec<Value> = if include_collections {
|
let collections: Vec<Value> = if include_collections {
|
||||||
CollectionUser::find_by_organization_and_user_uuid(&self.org_uuid, &self.user_uuid, conn)
|
// 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)
|
||||||
|
.await
|
||||||
|
.into_iter()
|
||||||
|
.map(|cu| (cu.collection_uuid.clone(), cu))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Get all collection groups for this user to prevent there inclusion
|
||||||
|
let cg: HashSet<String> = CollectionGroup::find_by_user(&self.user_uuid, conn)
|
||||||
.await
|
.await
|
||||||
.iter()
|
.into_iter()
|
||||||
.map(|cu| {
|
.map(|cg| cg.collections_uuid)
|
||||||
json!({
|
.collect();
|
||||||
"id": cu.collection_uuid,
|
|
||||||
"readOnly": cu.read_only,
|
Collection::find_by_organization_and_user_uuid(&self.org_uuid, &self.user_uuid, conn)
|
||||||
"hidePasswords": cu.hide_passwords,
|
.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 {
|
||||||
@@ -474,6 +510,7 @@ impl UserOrganization {
|
|||||||
"name": user.name,
|
"name": user.name,
|
||||||
"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,
|
||||||
|
|
||||||
@@ -595,7 +632,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);
|
||||||
}
|
}
|
||||||
|
@@ -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()),
|
||||||
))
|
))
|
||||||
|
@@ -144,14 +144,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
|
||||||
|
@@ -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,
|
||||||
}
|
}
|
||||||
|
@@ -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,
|
||||||
}
|
}
|
||||||
|
@@ -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,
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
|
@@ -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 {
|
||||||
|
55
src/mail.rs
55
src/mail.rs
@@ -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(),
|
||||||
}),
|
}),
|
||||||
|
71
src/main.rs
71
src/main.rs
@@ -38,6 +38,7 @@ use std::{
|
|||||||
use tokio::{
|
use tokio::{
|
||||||
fs::File,
|
fs::File,
|
||||||
io::{AsyncBufReadExt, BufReader},
|
io::{AsyncBufReadExt, BufReader},
|
||||||
|
signal::unix::SignalKind,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
@@ -83,7 +84,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 +98,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 +118,13 @@ 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}");
|
let web_vault_version = util::get_web_vault_version();
|
||||||
|
println!("Vaultwarden {version}");
|
||||||
|
println!("Web-Vault {web_vault_version}");
|
||||||
exit(0);
|
exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +168,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 +179,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!(
|
||||||
"\
|
"\
|
||||||
@@ -346,7 +385,7 @@ fn init_logging() -> Result<log::LevelFilter, Error> {
|
|||||||
}
|
}
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
{
|
{
|
||||||
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])?);
|
||||||
}
|
}
|
||||||
@@ -560,7 +599,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(())
|
||||||
|
4
src/static/scripts/admin.js
vendored
4
src/static/scripts/admin.js
vendored
@@ -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 });
|
||||||
}
|
}
|
||||||
|
@@ -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>
|
||||||
|
@@ -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 }}
|
||||||
|
@@ -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;">
|
||||||
|
@@ -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 }}
|
||||||
|
@@ -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;">
|
||||||
@@ -28,4 +33,4 @@ New Device Logged In From {{{device}}}
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
{{> email/email_footer }}
|
{{> email/email_footer }}
|
||||||
|
@@ -3,8 +3,8 @@ 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.
|
||||||
{{> email/email_footer_text }}
|
{{> email/email_footer_text }}
|
||||||
|
@@ -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>
|
||||||
@@ -21,4 +21,4 @@ Join {{{org_name}}}
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
{{> email/email_footer }}
|
{{> email/email_footer }}
|
||||||
|
30
src/util.rs
30
src/util.rs
@@ -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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user