mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2026-05-15 08:36:53 -04:00
Compare commits
1 commit
main
...
cached-con
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c28eab74dd |
95 changed files with 8954 additions and 8295 deletions
|
|
@ -372,22 +372,16 @@
|
||||||
## Note that clients cache the /api/config endpoint for about 1 hour and it could take some time before they are enabled or disabled!
|
## Note that clients cache the /api/config endpoint for about 1 hour and it could take some time before they are enabled or disabled!
|
||||||
##
|
##
|
||||||
## The following flags are available:
|
## The following flags are available:
|
||||||
## - "pm-5594-safari-account-switching": Enable account switching in Safari. (Safari >= 2026.2.0)
|
## - "inline-menu-positioning-improvements": Enable the use of inline menu password generator and identity suggestions in the browser extension.
|
||||||
## - "ssh-agent": Enable SSH agent support on Desktop. (Desktop >= 2024.12.0)
|
## - "inline-menu-totp": Enable the use of inline menu TOTP codes in the browser extension.
|
||||||
## - "ssh-agent-v2": Enable newer SSH agent support. (Desktop >= 2026.2.1)
|
## - "ssh-agent": Enable SSH agent support on Desktop. (Needs desktop >=2024.12.0)
|
||||||
## - "ssh-key-vault-item": Enable the creation and use of SSH key vault items. (Clients >= 2024.12.0)
|
## - "ssh-key-vault-item": Enable the creation and use of SSH key vault items. (Needs clients >=2024.12.0)
|
||||||
## - "pm-25373-windows-biometrics-v2": Enable the new implementation of biometrics on Windows. (Desktop >= 2025.11.0)
|
## - "pm-25373-windows-biometrics-v2": Enable the new implementation of biometrics on Windows. (Needs desktop >= 2025.11.0)
|
||||||
## - "anon-addy-self-host-alias": Enable configuring self-hosted Anon Addy alias generator. (Android >= 2025.3.0, iOS >= 2025.4.0)
|
## - "export-attachments": Enable support for exporting attachments (Clients >=2025.4.0)
|
||||||
## - "simple-login-self-host-alias": Enable configuring self-hosted Simple Login alias generator. (Android >= 2025.3.0, iOS >= 2025.4.0)
|
## - "anon-addy-self-host-alias": Enable configuring self-hosted Anon Addy alias generator. (Needs Android >=2025.3.0, iOS >=2025.4.0)
|
||||||
## - "mutual-tls": Enable the use of mutual TLS on Android (Clients >= 2025.2.0)
|
## - "simple-login-self-host-alias": Enable configuring self-hosted Simple Login alias generator. (Needs Android >=2025.3.0, iOS >=2025.4.0)
|
||||||
## - "cxp-import-mobile": Enable the import via CXP on iOS (Clients >= 2025.9.2)
|
## - "mutual-tls": Enable the use of mutual TLS on Android (Client >= 2025.2.0)
|
||||||
## - "cxp-export-mobile": Enable the export via CXP on iOS (Clients >= 2025.9.2)
|
# EXPERIMENTAL_CLIENT_FEATURE_FLAGS=fido2-vault-credentials
|
||||||
## - "pm-30529-webauthn-related-origins":
|
|
||||||
## - "desktop-ui-migration-milestone-1": Special feature flag for desktop UI (Desktop >= 2026.2.0)
|
|
||||||
## - "desktop-ui-migration-milestone-2": Special feature flag for desktop UI (Desktop >= 2026.2.0)
|
|
||||||
## - "desktop-ui-migration-milestone-3": Special feature flag for desktop UI (Desktop >= 2026.2.0)
|
|
||||||
## - "desktop-ui-migration-milestone-4": Special feature flag for desktop UI (Desktop >= 2026.2.0)
|
|
||||||
# EXPERIMENTAL_CLIENT_FEATURE_FLAGS=
|
|
||||||
|
|
||||||
## Require new device emails. When a user logs in an email is required to be sent.
|
## Require new device emails. When a user logs in an email is required to be sent.
|
||||||
## If sending the email fails the login attempt will fail!!
|
## If sending the email fails the login attempt will fail!!
|
||||||
|
|
|
||||||
1
.gitattributes
vendored
1
.gitattributes
vendored
|
|
@ -1,2 +1,3 @@
|
||||||
# Ignore vendored scripts in GitHub stats
|
# Ignore vendored scripts in GitHub stats
|
||||||
src/static/scripts/* linguist-vendored
|
src/static/scripts/* linguist-vendored
|
||||||
|
|
||||||
|
|
|
||||||
46
.github/workflows/build.yml
vendored
46
.github/workflows/build.yml
vendored
|
|
@ -1,10 +1,6 @@
|
||||||
name: Build
|
name: Build
|
||||||
permissions: {}
|
permissions: {}
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
paths:
|
paths:
|
||||||
|
|
@ -34,10 +30,6 @@ on:
|
||||||
- "docker/DockerSettings.yaml"
|
- "docker/DockerSettings.yaml"
|
||||||
- "macros/**"
|
- "macros/**"
|
||||||
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build and Test ${{ matrix.channel }}
|
name: Build and Test ${{ matrix.channel }}
|
||||||
|
|
@ -62,7 +54,7 @@ jobs:
|
||||||
|
|
||||||
# Checkout the repo
|
# Checkout the repo
|
||||||
- name: "Checkout"
|
- name: "Checkout"
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 #v6.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
@ -71,6 +63,7 @@ jobs:
|
||||||
# Determine rust-toolchain version
|
# Determine rust-toolchain version
|
||||||
- name: Init Variables
|
- name: Init Variables
|
||||||
id: toolchain
|
id: toolchain
|
||||||
|
shell: bash
|
||||||
env:
|
env:
|
||||||
CHANNEL: ${{ matrix.channel }}
|
CHANNEL: ${{ matrix.channel }}
|
||||||
run: |
|
run: |
|
||||||
|
|
@ -85,23 +78,32 @@ jobs:
|
||||||
# End Determine rust-toolchain version
|
# End Determine rust-toolchain version
|
||||||
|
|
||||||
|
|
||||||
- name: "Install toolchain ${{steps.toolchain.outputs.RUST_TOOLCHAIN}} as default"
|
# Only install the clippy and rustfmt components on the default rust-toolchain
|
||||||
|
- name: "Install rust-toolchain version"
|
||||||
|
uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master @ Dec 16, 2025, 6:11 PM GMT+1
|
||||||
|
if: ${{ matrix.channel == 'rust-toolchain' }}
|
||||||
|
with:
|
||||||
|
toolchain: "${{steps.toolchain.outputs.RUST_TOOLCHAIN}}"
|
||||||
|
components: clippy, rustfmt
|
||||||
|
# End Uses the rust-toolchain file to determine version
|
||||||
|
|
||||||
|
|
||||||
|
# Install the any other channel to be used for which we do not execute clippy and rustfmt
|
||||||
|
- name: "Install MSRV version"
|
||||||
|
uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master @ Dec 16, 2025, 6:11 PM GMT+1
|
||||||
|
if: ${{ matrix.channel != 'rust-toolchain' }}
|
||||||
|
with:
|
||||||
|
toolchain: "${{steps.toolchain.outputs.RUST_TOOLCHAIN}}"
|
||||||
|
# End Install the MSRV channel to be used
|
||||||
|
|
||||||
|
# Set the current matrix toolchain version as default
|
||||||
|
- name: "Set toolchain ${{steps.toolchain.outputs.RUST_TOOLCHAIN}} as default"
|
||||||
env:
|
env:
|
||||||
CHANNEL: ${{ matrix.channel }}
|
|
||||||
RUST_TOOLCHAIN: ${{steps.toolchain.outputs.RUST_TOOLCHAIN}}
|
RUST_TOOLCHAIN: ${{steps.toolchain.outputs.RUST_TOOLCHAIN}}
|
||||||
run: |
|
run: |
|
||||||
# Remove the rust-toolchain.toml
|
# Remove the rust-toolchain.toml
|
||||||
rm rust-toolchain.toml
|
rm rust-toolchain.toml
|
||||||
|
# Set the default
|
||||||
# Install the correct toolchain version
|
|
||||||
rustup toolchain install "${RUST_TOOLCHAIN}" --profile minimal --no-self-update
|
|
||||||
|
|
||||||
# If this matrix is the `rust-toolchain` flow, also install rustfmt and clippy
|
|
||||||
if [[ "${CHANNEL}" == 'rust-toolchain' ]]; then
|
|
||||||
rustup component add --toolchain "${RUST_TOOLCHAIN}" rustfmt clippy
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Set as the default toolchain
|
|
||||||
rustup default "${RUST_TOOLCHAIN}"
|
rustup default "${RUST_TOOLCHAIN}"
|
||||||
|
|
||||||
# Show environment
|
# Show environment
|
||||||
|
|
@ -113,7 +115,7 @@ jobs:
|
||||||
|
|
||||||
# Enable Rust Caching
|
# Enable Rust Caching
|
||||||
- name: Rust Caching
|
- name: Rust Caching
|
||||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||||
with:
|
with:
|
||||||
# Use a custom prefix-key to force a fresh start. This is sometimes needed with bigger changes.
|
# Use a custom prefix-key to force a fresh start. This is sometimes needed with bigger changes.
|
||||||
# Like changing the build host from Ubuntu 20.04 to 22.04 for example.
|
# Like changing the build host from Ubuntu 20.04 to 22.04 for example.
|
||||||
|
|
|
||||||
10
.github/workflows/check-templates.yml
vendored
10
.github/workflows/check-templates.yml
vendored
|
|
@ -1,16 +1,8 @@
|
||||||
name: Check templates
|
name: Check templates
|
||||||
permissions: {}
|
permissions: {}
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
on: [ push, pull_request ]
|
on: [ push, pull_request ]
|
||||||
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docker-templates:
|
docker-templates:
|
||||||
name: Validate docker templates
|
name: Validate docker templates
|
||||||
|
|
@ -20,7 +12,7 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
# Checkout the repo
|
# Checkout the repo
|
||||||
- name: "Checkout"
|
- name: "Checkout"
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 #v6.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
# End Checkout the repo
|
# End Checkout the repo
|
||||||
|
|
|
||||||
16
.github/workflows/hadolint.yml
vendored
16
.github/workflows/hadolint.yml
vendored
|
|
@ -1,15 +1,8 @@
|
||||||
name: Hadolint
|
name: Hadolint
|
||||||
permissions: {}
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
on: [ push, pull_request ]
|
on: [ push, pull_request ]
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
hadolint:
|
hadolint:
|
||||||
|
|
@ -20,7 +13,7 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
# Start Docker Buildx
|
# Start Docker Buildx
|
||||||
- name: Setup Docker Buildx
|
- name: Setup Docker Buildx
|
||||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||||
# https://github.com/moby/buildkit/issues/3969
|
# https://github.com/moby/buildkit/issues/3969
|
||||||
# Also set max parallelism to 2, 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:
|
||||||
|
|
@ -32,6 +25,7 @@ jobs:
|
||||||
|
|
||||||
# Download hadolint - https://github.com/hadolint/hadolint/releases
|
# Download hadolint - https://github.com/hadolint/hadolint/releases
|
||||||
- name: Download hadolint
|
- name: Download hadolint
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
sudo curl -L https://github.com/hadolint/hadolint/releases/download/v${HADOLINT_VERSION}/hadolint-$(uname -s)-$(uname -m) -o /usr/local/bin/hadolint && \
|
sudo curl -L https://github.com/hadolint/hadolint/releases/download/v${HADOLINT_VERSION}/hadolint-$(uname -s)-$(uname -m) -o /usr/local/bin/hadolint && \
|
||||||
sudo chmod +x /usr/local/bin/hadolint
|
sudo chmod +x /usr/local/bin/hadolint
|
||||||
|
|
@ -40,18 +34,20 @@ jobs:
|
||||||
# End Download hadolint
|
# End Download hadolint
|
||||||
# Checkout the repo
|
# Checkout the repo
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 #v6.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
# End Checkout the repo
|
# End Checkout the repo
|
||||||
|
|
||||||
# Test Dockerfiles with hadolint
|
# Test Dockerfiles with hadolint
|
||||||
- name: Run hadolint
|
- name: Run hadolint
|
||||||
|
shell: bash
|
||||||
run: hadolint docker/Dockerfile.{debian,alpine}
|
run: hadolint docker/Dockerfile.{debian,alpine}
|
||||||
# End Test Dockerfiles with hadolint
|
# End Test Dockerfiles with hadolint
|
||||||
|
|
||||||
# Test Dockerfiles with docker build checks
|
# Test Dockerfiles with docker build checks
|
||||||
- name: Run docker build check
|
- name: Run docker build check
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
echo "Checking docker/Dockerfile.debian"
|
echo "Checking docker/Dockerfile.debian"
|
||||||
docker build --check . -f docker/Dockerfile.debian
|
docker build --check . -f docker/Dockerfile.debian
|
||||||
|
|
|
||||||
147
.github/workflows/release.yml
vendored
147
.github/workflows/release.yml
vendored
|
|
@ -1,12 +1,6 @@
|
||||||
name: Release
|
name: Release
|
||||||
permissions: {}
|
permissions: {}
|
||||||
|
|
||||||
concurrency:
|
|
||||||
# Apply concurrency control only on the upstream repo
|
|
||||||
group: ${{ github.repository == 'dani-garcia/vaultwarden' && format('{0}-{1}', github.workflow, github.ref) || github.run_id }}
|
|
||||||
# Don't cancel other runs when creating a tag
|
|
||||||
cancel-in-progress: ${{ github.ref_type == 'branch' }}
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
|
|
@ -16,31 +10,33 @@ on:
|
||||||
# https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet
|
# https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet
|
||||||
- '[1-2].[0-9]+.[0-9]+'
|
- '[1-2].[0-9]+.[0-9]+'
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
# Apply concurrency control only on the upstream repo
|
||||||
|
group: ${{ github.repository == 'dani-garcia/vaultwarden' && format('{0}-{1}', github.workflow, github.ref) || github.run_id }}
|
||||||
|
# Don't cancel other runs when creating a tag
|
||||||
|
cancel-in-progress: ${{ github.ref_type == 'branch' }}
|
||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
# A "release" environment must be created in the repository settings
|
env:
|
||||||
# (Settings > Environments > New environment) with the following
|
# The *_REPO variables need to be configured as repository variables
|
||||||
# variables and secrets configured as needed.
|
# Append `/settings/variables/actions` to your repo url
|
||||||
#
|
# DOCKERHUB_REPO needs to be 'index.docker.io/<user>/<repo>'
|
||||||
# Variables (only set the ones for registries you want to push to):
|
# Check for Docker hub credentials in secrets
|
||||||
# DOCKERHUB_REPO: 'index.docker.io/<user>/<repo>'
|
HAVE_DOCKERHUB_LOGIN: ${{ vars.DOCKERHUB_REPO != '' && secrets.DOCKERHUB_USERNAME != '' && secrets.DOCKERHUB_TOKEN != '' }}
|
||||||
# QUAY_REPO: 'quay.io/<user>/<repo>'
|
# GHCR_REPO needs to be 'ghcr.io/<user>/<repo>'
|
||||||
# GHCR_REPO: 'ghcr.io/<user>/<repo>'
|
# Check for Github credentials in secrets
|
||||||
#
|
HAVE_GHCR_LOGIN: ${{ vars.GHCR_REPO != '' && github.repository_owner != '' && secrets.GITHUB_TOKEN != '' }}
|
||||||
# Secrets (only required when the corresponding *_REPO variable is set):
|
# QUAY_REPO needs to be 'quay.io/<user>/<repo>'
|
||||||
# DOCKERHUB_REPO => DOCKERHUB_USERNAME, DOCKERHUB_TOKEN
|
# Check for Quay.io credentials in secrets
|
||||||
# QUAY_REPO => QUAY_USERNAME, QUAY_TOKEN
|
HAVE_QUAY_LOGIN: ${{ vars.QUAY_REPO != '' && secrets.QUAY_USERNAME != '' && secrets.QUAY_TOKEN != '' }}
|
||||||
# GITHUB_TOKEN is provided automatically
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docker-build:
|
docker-build:
|
||||||
name: Build Vaultwarden containers
|
name: Build Vaultwarden containers
|
||||||
if: ${{ github.repository == 'dani-garcia/vaultwarden' }}
|
if: ${{ github.repository == 'dani-garcia/vaultwarden' }}
|
||||||
environment:
|
|
||||||
name: release
|
|
||||||
deployment: false
|
|
||||||
permissions:
|
permissions:
|
||||||
packages: write # Needed to upload packages and artifacts
|
packages: write # Needed to upload packages and artifacts
|
||||||
contents: read
|
contents: read
|
||||||
|
|
@ -58,13 +54,13 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Initialize QEMU binfmt support
|
- name: Initialize QEMU binfmt support
|
||||||
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||||
with:
|
with:
|
||||||
platforms: "arm64,arm"
|
platforms: "arm64,arm"
|
||||||
|
|
||||||
# Start Docker Buildx
|
# Start Docker Buildx
|
||||||
- name: Setup Docker Buildx
|
- name: Setup Docker Buildx
|
||||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||||
# https://github.com/moby/buildkit/issues/3969
|
# https://github.com/moby/buildkit/issues/3969
|
||||||
# Also set max parallelism to 2, 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:
|
||||||
|
|
@ -77,7 +73,7 @@ jobs:
|
||||||
|
|
||||||
# Checkout the repo
|
# Checkout the repo
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 #v6.0.0
|
||||||
# We need fetch-depth of 0 so we also get all the tag metadata
|
# We need fetch-depth of 0 so we also get all the tag metadata
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
@ -106,14 +102,14 @@ jobs:
|
||||||
|
|
||||||
# Login to Docker Hub
|
# Login to Docker Hub
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
if: ${{ vars.DOCKERHUB_REPO != '' }}
|
if: ${{ env.HAVE_DOCKERHUB_LOGIN == 'true' }}
|
||||||
|
|
||||||
- name: Add registry for DockerHub
|
- name: Add registry for DockerHub
|
||||||
if: ${{ vars.DOCKERHUB_REPO != '' }}
|
if: ${{ env.HAVE_DOCKERHUB_LOGIN == 'true' }}
|
||||||
env:
|
env:
|
||||||
DOCKERHUB_REPO: ${{ vars.DOCKERHUB_REPO }}
|
DOCKERHUB_REPO: ${{ vars.DOCKERHUB_REPO }}
|
||||||
run: |
|
run: |
|
||||||
|
|
@ -121,15 +117,15 @@ jobs:
|
||||||
|
|
||||||
# Login to GitHub Container Registry
|
# Login to GitHub Container Registry
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
if: ${{ vars.GHCR_REPO != '' }}
|
if: ${{ env.HAVE_GHCR_LOGIN == 'true' }}
|
||||||
|
|
||||||
- name: Add registry for ghcr.io
|
- name: Add registry for ghcr.io
|
||||||
if: ${{ vars.GHCR_REPO != '' }}
|
if: ${{ env.HAVE_GHCR_LOGIN == 'true' }}
|
||||||
env:
|
env:
|
||||||
GHCR_REPO: ${{ vars.GHCR_REPO }}
|
GHCR_REPO: ${{ vars.GHCR_REPO }}
|
||||||
run: |
|
run: |
|
||||||
|
|
@ -137,15 +133,15 @@ jobs:
|
||||||
|
|
||||||
# Login to Quay.io
|
# Login to Quay.io
|
||||||
- name: Login to Quay.io
|
- name: Login to Quay.io
|
||||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||||
with:
|
with:
|
||||||
registry: quay.io
|
registry: quay.io
|
||||||
username: ${{ secrets.QUAY_USERNAME }}
|
username: ${{ secrets.QUAY_USERNAME }}
|
||||||
password: ${{ secrets.QUAY_TOKEN }}
|
password: ${{ secrets.QUAY_TOKEN }}
|
||||||
if: ${{ vars.QUAY_REPO != '' }}
|
if: ${{ env.HAVE_QUAY_LOGIN == 'true' }}
|
||||||
|
|
||||||
- name: Add registry for Quay.io
|
- name: Add registry for Quay.io
|
||||||
if: ${{ vars.QUAY_REPO != '' }}
|
if: ${{ env.HAVE_QUAY_LOGIN == 'true' }}
|
||||||
env:
|
env:
|
||||||
QUAY_REPO: ${{ vars.QUAY_REPO }}
|
QUAY_REPO: ${{ vars.QUAY_REPO }}
|
||||||
run: |
|
run: |
|
||||||
|
|
@ -159,7 +155,7 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
#
|
#
|
||||||
# Check if there is a GitHub Container Registry Login and use it for caching
|
# Check if there is a GitHub Container Registry Login and use it for caching
|
||||||
if [[ -n "${GHCR_REPO}" ]]; then
|
if [[ -n "${HAVE_GHCR_LOGIN}" ]]; then
|
||||||
echo "BAKE_CACHE_FROM=type=registry,ref=${GHCR_REPO}-buildcache:${BASE_IMAGE}-${NORMALIZED_ARCH}" | tee -a "${GITHUB_ENV}"
|
echo "BAKE_CACHE_FROM=type=registry,ref=${GHCR_REPO}-buildcache:${BASE_IMAGE}-${NORMALIZED_ARCH}" | tee -a "${GITHUB_ENV}"
|
||||||
echo "BAKE_CACHE_TO=type=registry,ref=${GHCR_REPO}-buildcache:${BASE_IMAGE}-${NORMALIZED_ARCH},compression=zstd,mode=max" | tee -a "${GITHUB_ENV}"
|
echo "BAKE_CACHE_TO=type=registry,ref=${GHCR_REPO}-buildcache:${BASE_IMAGE}-${NORMALIZED_ARCH},compression=zstd,mode=max" | tee -a "${GITHUB_ENV}"
|
||||||
else
|
else
|
||||||
|
|
@ -185,7 +181,7 @@ jobs:
|
||||||
|
|
||||||
- name: Bake ${{ matrix.base_image }} containers
|
- name: Bake ${{ matrix.base_image }} containers
|
||||||
id: bake_vw
|
id: bake_vw
|
||||||
uses: docker/bake-action@a66e1c87e2eca0503c343edf1d208c716d54b8a8 # v7.1.0
|
uses: docker/bake-action@5be5f02ff8819ecd3092ea6b2e6261c31774f2b4 # v6.10.0
|
||||||
env:
|
env:
|
||||||
BASE_TAGS: "${{ steps.determine-version.outputs.BASE_TAGS }}"
|
BASE_TAGS: "${{ steps.determine-version.outputs.BASE_TAGS }}"
|
||||||
SOURCE_COMMIT: "${{ env.SOURCE_COMMIT }}"
|
SOURCE_COMMIT: "${{ env.SOURCE_COMMIT }}"
|
||||||
|
|
@ -222,7 +218,7 @@ jobs:
|
||||||
touch "${RUNNER_TEMP}/digests/${digest#sha256:}"
|
touch "${RUNNER_TEMP}/digests/${digest#sha256:}"
|
||||||
|
|
||||||
- name: Upload digest
|
- name: Upload digest
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||||
with:
|
with:
|
||||||
name: digests-${{ env.NORMALIZED_ARCH }}-${{ matrix.base_image }}
|
name: digests-${{ env.NORMALIZED_ARCH }}-${{ matrix.base_image }}
|
||||||
path: ${{ runner.temp }}/digests/*
|
path: ${{ runner.temp }}/digests/*
|
||||||
|
|
@ -237,12 +233,12 @@ jobs:
|
||||||
|
|
||||||
# Upload artifacts to Github Actions and Attest the binaries
|
# Upload artifacts to Github Actions and Attest the binaries
|
||||||
- name: Attest binaries
|
- name: Attest binaries
|
||||||
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
|
uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3.1.0
|
||||||
with:
|
with:
|
||||||
subject-path: vaultwarden-${{ env.NORMALIZED_ARCH }}
|
subject-path: vaultwarden-${{ env.NORMALIZED_ARCH }}
|
||||||
|
|
||||||
- name: Upload binaries as artifacts
|
- name: Upload binaries as artifacts
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||||
with:
|
with:
|
||||||
name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-${{ env.NORMALIZED_ARCH }}-${{ matrix.base_image }}
|
name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-${{ env.NORMALIZED_ARCH }}-${{ matrix.base_image }}
|
||||||
path: vaultwarden-${{ env.NORMALIZED_ARCH }}
|
path: vaultwarden-${{ env.NORMALIZED_ARCH }}
|
||||||
|
|
@ -251,9 +247,6 @@ jobs:
|
||||||
name: Merge manifests
|
name: Merge manifests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: docker-build
|
needs: docker-build
|
||||||
environment:
|
|
||||||
name: release
|
|
||||||
deployment: false
|
|
||||||
permissions:
|
permissions:
|
||||||
packages: write # Needed to upload packages and artifacts
|
packages: write # Needed to upload packages and artifacts
|
||||||
attestations: write # Needed to generate an artifact attestation for a build
|
attestations: write # Needed to generate an artifact attestation for a build
|
||||||
|
|
@ -264,7 +257,7 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Download digests
|
- name: Download digests
|
||||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||||
with:
|
with:
|
||||||
path: ${{ runner.temp }}/digests
|
path: ${{ runner.temp }}/digests
|
||||||
pattern: digests-*-${{ matrix.base_image }}
|
pattern: digests-*-${{ matrix.base_image }}
|
||||||
|
|
@ -272,14 +265,14 @@ jobs:
|
||||||
|
|
||||||
# Login to Docker Hub
|
# Login to Docker Hub
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
if: ${{ vars.DOCKERHUB_REPO != '' }}
|
if: ${{ env.HAVE_DOCKERHUB_LOGIN == 'true' }}
|
||||||
|
|
||||||
- name: Add registry for DockerHub
|
- name: Add registry for DockerHub
|
||||||
if: ${{ vars.DOCKERHUB_REPO != '' }}
|
if: ${{ env.HAVE_DOCKERHUB_LOGIN == 'true' }}
|
||||||
env:
|
env:
|
||||||
DOCKERHUB_REPO: ${{ vars.DOCKERHUB_REPO }}
|
DOCKERHUB_REPO: ${{ vars.DOCKERHUB_REPO }}
|
||||||
run: |
|
run: |
|
||||||
|
|
@ -287,15 +280,15 @@ jobs:
|
||||||
|
|
||||||
# Login to GitHub Container Registry
|
# Login to GitHub Container Registry
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
if: ${{ vars.GHCR_REPO != '' }}
|
if: ${{ env.HAVE_GHCR_LOGIN == 'true' }}
|
||||||
|
|
||||||
- name: Add registry for ghcr.io
|
- name: Add registry for ghcr.io
|
||||||
if: ${{ vars.GHCR_REPO != '' }}
|
if: ${{ env.HAVE_GHCR_LOGIN == 'true' }}
|
||||||
env:
|
env:
|
||||||
GHCR_REPO: ${{ vars.GHCR_REPO }}
|
GHCR_REPO: ${{ vars.GHCR_REPO }}
|
||||||
run: |
|
run: |
|
||||||
|
|
@ -303,15 +296,15 @@ jobs:
|
||||||
|
|
||||||
# Login to Quay.io
|
# Login to Quay.io
|
||||||
- name: Login to Quay.io
|
- name: Login to Quay.io
|
||||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||||
with:
|
with:
|
||||||
registry: quay.io
|
registry: quay.io
|
||||||
username: ${{ secrets.QUAY_USERNAME }}
|
username: ${{ secrets.QUAY_USERNAME }}
|
||||||
password: ${{ secrets.QUAY_TOKEN }}
|
password: ${{ secrets.QUAY_TOKEN }}
|
||||||
if: ${{ vars.QUAY_REPO != '' }}
|
if: ${{ env.HAVE_QUAY_LOGIN == 'true' }}
|
||||||
|
|
||||||
- name: Add registry for Quay.io
|
- name: Add registry for Quay.io
|
||||||
if: ${{ vars.QUAY_REPO != '' }}
|
if: ${{ env.HAVE_QUAY_LOGIN == 'true' }}
|
||||||
env:
|
env:
|
||||||
QUAY_REPO: ${{ vars.QUAY_REPO }}
|
QUAY_REPO: ${{ vars.QUAY_REPO }}
|
||||||
run: |
|
run: |
|
||||||
|
|
@ -320,43 +313,45 @@ jobs:
|
||||||
# Determine Base Tags
|
# Determine Base Tags
|
||||||
- name: Determine Base Tags
|
- name: Determine Base Tags
|
||||||
env:
|
env:
|
||||||
BASE_IMAGE_TAG: "${{ matrix.base_image != 'debian' && format('-{0}', matrix.base_image) || '' }}"
|
|
||||||
REF_TYPE: ${{ github.ref_type }}
|
REF_TYPE: ${{ github.ref_type }}
|
||||||
run: |
|
run: |
|
||||||
# Check which main tag we are going to build determined by ref_type
|
# Check which main tag we are going to build determined by ref_type
|
||||||
if [[ "${REF_TYPE}" == "tag" ]]; then
|
if [[ "${REF_TYPE}" == "tag" ]]; then
|
||||||
echo "BASE_TAGS=latest${BASE_IMAGE_TAG},${GITHUB_REF#refs/*/}${BASE_IMAGE_TAG}${BASE_IMAGE_TAG//-/,}" | tee -a "${GITHUB_ENV}"
|
echo "BASE_TAGS=latest,${GITHUB_REF#refs/*/}" | tee -a "${GITHUB_ENV}"
|
||||||
elif [[ "${REF_TYPE}" == "branch" ]]; then
|
elif [[ "${REF_TYPE}" == "branch" ]]; then
|
||||||
echo "BASE_TAGS=testing${BASE_IMAGE_TAG}" | tee -a "${GITHUB_ENV}"
|
echo "BASE_TAGS=testing" | tee -a "${GITHUB_ENV}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Create manifest list, push it and extract digest SHA
|
- name: Create manifest list, push it and extract digest SHA
|
||||||
working-directory: ${{ runner.temp }}/digests
|
working-directory: ${{ runner.temp }}/digests
|
||||||
env:
|
env:
|
||||||
|
BASE_IMAGE_TAG: "${{ matrix.base_image != 'debian' && format('-{0}', matrix.base_image) || '' }}"
|
||||||
BASE_TAGS: "${{ env.BASE_TAGS }}"
|
BASE_TAGS: "${{ env.BASE_TAGS }}"
|
||||||
CONTAINER_REGISTRIES: "${{ env.CONTAINER_REGISTRIES }}"
|
CONTAINER_REGISTRIES: "${{ env.CONTAINER_REGISTRIES }}"
|
||||||
run: |
|
run: |
|
||||||
|
set +e
|
||||||
IFS=',' read -ra IMAGES <<< "${CONTAINER_REGISTRIES}"
|
IFS=',' read -ra IMAGES <<< "${CONTAINER_REGISTRIES}"
|
||||||
IFS=',' read -ra TAGS <<< "${BASE_TAGS}"
|
IFS=',' read -ra TAGS <<< "${BASE_TAGS}"
|
||||||
|
|
||||||
TAG_ARGS=()
|
|
||||||
for img in "${IMAGES[@]}"; do
|
for img in "${IMAGES[@]}"; do
|
||||||
for tag in "${TAGS[@]}"; do
|
for tag in "${TAGS[@]}"; do
|
||||||
TAG_ARGS+=("-t" "${img}:${tag}")
|
echo "Creating manifest for ${img}:${tag}${BASE_IMAGE_TAG}"
|
||||||
|
|
||||||
|
OUTPUT=$(docker buildx imagetools create \
|
||||||
|
-t "${img}:${tag}${BASE_IMAGE_TAG}" \
|
||||||
|
$(printf "${img}@sha256:%s " *) 2>&1)
|
||||||
|
STATUS=$?
|
||||||
|
|
||||||
|
if [ ${STATUS} -ne 0 ]; then
|
||||||
|
echo "Manifest creation failed for ${img}:${tag}${BASE_IMAGE_TAG}"
|
||||||
|
echo "${OUTPUT}"
|
||||||
|
exit ${STATUS}
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Manifest created for ${img}:${tag}${BASE_IMAGE_TAG}"
|
||||||
|
echo "${OUTPUT}"
|
||||||
done
|
done
|
||||||
done
|
done
|
||||||
|
set -e
|
||||||
echo "Creating manifest"
|
|
||||||
if ! OUTPUT=$(docker buildx imagetools create \
|
|
||||||
"${TAG_ARGS[@]}" \
|
|
||||||
$(printf "${IMAGES[0]}@sha256:%s " *) 2>&1); then
|
|
||||||
echo "Manifest creation failed"
|
|
||||||
echo "${OUTPUT}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Manifest created successfully"
|
|
||||||
echo "${OUTPUT}"
|
|
||||||
|
|
||||||
# Extract digest SHA for subsequent steps
|
# Extract digest SHA for subsequent steps
|
||||||
GET_DIGEST_SHA="$(echo "${OUTPUT}" | grep -oE 'sha256:[a-f0-9]{64}' | tail -1)"
|
GET_DIGEST_SHA="$(echo "${OUTPUT}" | grep -oE 'sha256:[a-f0-9]{64}' | tail -1)"
|
||||||
|
|
@ -364,24 +359,24 @@ jobs:
|
||||||
|
|
||||||
# Attest container images
|
# Attest container images
|
||||||
- name: Attest - docker.io - ${{ matrix.base_image }}
|
- name: Attest - docker.io - ${{ matrix.base_image }}
|
||||||
if: ${{ vars.DOCKERHUB_REPO != '' && env.DIGEST_SHA != ''}}
|
if: ${{ env.HAVE_DOCKERHUB_LOGIN == 'true' && env.DIGEST_SHA != ''}}
|
||||||
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
|
uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3.1.0
|
||||||
with:
|
with:
|
||||||
subject-name: ${{ vars.DOCKERHUB_REPO }}
|
subject-name: ${{ vars.DOCKERHUB_REPO }}
|
||||||
subject-digest: ${{ env.DIGEST_SHA }}
|
subject-digest: ${{ env.DIGEST_SHA }}
|
||||||
push-to-registry: true
|
push-to-registry: true
|
||||||
|
|
||||||
- name: Attest - ghcr.io - ${{ matrix.base_image }}
|
- name: Attest - ghcr.io - ${{ matrix.base_image }}
|
||||||
if: ${{ vars.GHCR_REPO != '' && env.DIGEST_SHA != ''}}
|
if: ${{ env.HAVE_GHCR_LOGIN == 'true' && env.DIGEST_SHA != ''}}
|
||||||
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
|
uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3.1.0
|
||||||
with:
|
with:
|
||||||
subject-name: ${{ vars.GHCR_REPO }}
|
subject-name: ${{ vars.GHCR_REPO }}
|
||||||
subject-digest: ${{ env.DIGEST_SHA }}
|
subject-digest: ${{ env.DIGEST_SHA }}
|
||||||
push-to-registry: true
|
push-to-registry: true
|
||||||
|
|
||||||
- name: Attest - quay.io - ${{ matrix.base_image }}
|
- name: Attest - quay.io - ${{ matrix.base_image }}
|
||||||
if: ${{ vars.QUAY_REPO != '' && env.DIGEST_SHA != ''}}
|
if: ${{ env.HAVE_QUAY_LOGIN == 'true' && env.DIGEST_SHA != ''}}
|
||||||
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
|
uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3.1.0
|
||||||
with:
|
with:
|
||||||
subject-name: ${{ vars.QUAY_REPO }}
|
subject-name: ${{ vars.QUAY_REPO }}
|
||||||
subject-digest: ${{ env.DIGEST_SHA }}
|
subject-digest: ${{ env.DIGEST_SHA }}
|
||||||
|
|
|
||||||
4
.github/workflows/releasecache-cleanup.yml
vendored
4
.github/workflows/releasecache-cleanup.yml
vendored
|
|
@ -1,10 +1,6 @@
|
||||||
name: Cleanup
|
name: Cleanup
|
||||||
permissions: {}
|
permissions: {}
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}
|
|
||||||
cancel-in-progress: false
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
|
|
|
||||||
10
.github/workflows/trivy.yml
vendored
10
.github/workflows/trivy.yml
vendored
|
|
@ -1,10 +1,6 @@
|
||||||
name: Trivy
|
name: Trivy
|
||||||
permissions: {}
|
permissions: {}
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
|
|
@ -33,12 +29,12 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 #v6.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Run Trivy vulnerability scanner
|
- name: Run Trivy vulnerability scanner
|
||||||
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
|
uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # 0.33.1
|
||||||
env:
|
env:
|
||||||
TRIVY_DB_REPOSITORY: docker.io/aquasec/trivy-db:2,public.ecr.aws/aquasecurity/trivy-db:2,ghcr.io/aquasecurity/trivy-db:2
|
TRIVY_DB_REPOSITORY: docker.io/aquasec/trivy-db:2,public.ecr.aws/aquasecurity/trivy-db:2,ghcr.io/aquasecurity/trivy-db:2
|
||||||
TRIVY_JAVA_DB_REPOSITORY: docker.io/aquasec/trivy-java-db:1,public.ecr.aws/aquasecurity/trivy-java-db:1,ghcr.io/aquasecurity/trivy-java-db:1
|
TRIVY_JAVA_DB_REPOSITORY: docker.io/aquasec/trivy-java-db:1,public.ecr.aws/aquasecurity/trivy-java-db:1,ghcr.io/aquasecurity/trivy-java-db:1
|
||||||
|
|
@ -50,6 +46,6 @@ jobs:
|
||||||
severity: CRITICAL,HIGH
|
severity: CRITICAL,HIGH
|
||||||
|
|
||||||
- name: Upload Trivy scan results to GitHub Security tab
|
- name: Upload Trivy scan results to GitHub Security tab
|
||||||
uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
|
uses: github/codeql-action/upload-sarif@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
|
||||||
with:
|
with:
|
||||||
sarif_file: 'trivy-results.sarif'
|
sarif_file: 'trivy-results.sarif'
|
||||||
|
|
|
||||||
10
.github/workflows/typos.yml
vendored
10
.github/workflows/typos.yml
vendored
|
|
@ -1,11 +1,7 @@
|
||||||
name: Code Spell Checking
|
name: Code Spell Checking
|
||||||
permissions: {}
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
on: [ push, pull_request ]
|
on: [ push, pull_request ]
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
typos:
|
typos:
|
||||||
|
|
@ -16,11 +12,11 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
# Checkout the repo
|
# Checkout the repo
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 #v6.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
# End Checkout the repo
|
# End Checkout the repo
|
||||||
|
|
||||||
# When this version is updated, do not forget to update this in `.pre-commit-config.yaml` too
|
# When this version is updated, do not forget to update this in `.pre-commit-config.yaml` too
|
||||||
- name: Spell Check Repo
|
- name: Spell Check Repo
|
||||||
uses: crate-ci/typos@7c572958218557a3272c2d6719629443b5cc26fd # v1.45.2
|
uses: crate-ci/typos@2d0ce569feab1f8752f1dde43cc2f2aa53236e06 # v1.40.0
|
||||||
|
|
|
||||||
11
.github/workflows/zizmor.yml
vendored
11
.github/workflows/zizmor.yml
vendored
|
|
@ -1,9 +1,4 @@
|
||||||
name: Security Analysis with zizmor
|
name: Security Analysis with zizmor
|
||||||
permissions: {}
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
|
@ -11,6 +6,8 @@ on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: ["**"]
|
branches: ["**"]
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
zizmor:
|
zizmor:
|
||||||
name: Run zizmor
|
name: Run zizmor
|
||||||
|
|
@ -19,12 +16,12 @@ jobs:
|
||||||
security-events: write # To write the security report
|
security-events: write # To write the security report
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 #v6.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Run zizmor
|
- name: Run zizmor
|
||||||
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
|
uses: zizmorcore/zizmor-action@e639db99335bc9038abc0e066dfcd72e23d26fb4 # v0.3.0
|
||||||
with:
|
with:
|
||||||
# intentionally not scanning the entire repository,
|
# intentionally not scanning the entire repository,
|
||||||
# since it contains integration tests.
|
# since it contains integration tests.
|
||||||
|
|
|
||||||
|
|
@ -1,60 +1,58 @@
|
||||||
---
|
---
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # v6.0.0
|
rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # v6.0.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
- id: check-json
|
- id: check-json
|
||||||
- id: check-toml
|
- id: check-toml
|
||||||
- id: mixed-line-ending
|
- id: mixed-line-ending
|
||||||
args: [ "--fix=no" ]
|
args: ["--fix=no"]
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
exclude: "(.*js$|.*css$)"
|
exclude: "(.*js$|.*css$)"
|
||||||
- id: check-case-conflict
|
- id: check-case-conflict
|
||||||
- id: check-merge-conflict
|
- id: check-merge-conflict
|
||||||
- id: detect-private-key
|
- id: detect-private-key
|
||||||
- id: check-symlinks
|
- id: check-symlinks
|
||||||
- id: forbid-submodules
|
- id: forbid-submodules
|
||||||
|
- repo: local
|
||||||
# When this version is updated, do not forget to update this in `.github/workflows/typos.yaml` too
|
|
||||||
- repo: https://github.com/crate-ci/typos
|
|
||||||
rev: 7c572958218557a3272c2d6719629443b5cc26fd # v1.45.2
|
|
||||||
hooks:
|
hooks:
|
||||||
- id: typos
|
- id: fmt
|
||||||
|
name: fmt
|
||||||
- repo: local
|
description: Format files with cargo fmt.
|
||||||
hooks:
|
entry: cargo fmt
|
||||||
- id: fmt
|
language: system
|
||||||
name: fmt
|
always_run: true
|
||||||
description: Format files with cargo fmt.
|
pass_filenames: false
|
||||||
entry: cargo fmt
|
args: ["--", "--check"]
|
||||||
language: system
|
- id: cargo-test
|
||||||
always_run: true
|
name: cargo test
|
||||||
pass_filenames: false
|
description: Test the package for errors.
|
||||||
args: [ "--", "--check" ]
|
entry: cargo test
|
||||||
- id: cargo-test
|
language: system
|
||||||
name: cargo test
|
args: ["--features", "sqlite,mysql,postgresql", "--"]
|
||||||
description: Test the package for errors.
|
types_or: [rust, file]
|
||||||
entry: cargo test
|
files: (Cargo.toml|Cargo.lock|rust-toolchain.toml|rustfmt.toml|.*\.rs$)
|
||||||
language: system
|
pass_filenames: false
|
||||||
args: [ "--features", "sqlite,mysql,postgresql", "--" ]
|
- id: cargo-clippy
|
||||||
types_or: [ rust, file ]
|
name: cargo clippy
|
||||||
files: (Cargo.toml|Cargo.lock|rust-toolchain.toml|rustfmt.toml|.*\.rs$)
|
description: Lint Rust sources
|
||||||
pass_filenames: false
|
entry: cargo clippy
|
||||||
- id: cargo-clippy
|
language: system
|
||||||
name: cargo clippy
|
args: ["--features", "sqlite,mysql,postgresql", "--", "-D", "warnings"]
|
||||||
description: Lint Rust sources
|
types_or: [rust, file]
|
||||||
entry: cargo clippy
|
files: (Cargo.toml|Cargo.lock|rust-toolchain.toml|rustfmt.toml|.*\.rs$)
|
||||||
language: system
|
pass_filenames: false
|
||||||
args: [ "--features", "sqlite,mysql,postgresql", "--", "-D", "warnings" ]
|
- id: check-docker-templates
|
||||||
types_or: [ rust, file ]
|
name: check docker templates
|
||||||
files: (Cargo.toml|Cargo.lock|rust-toolchain.toml|rustfmt.toml|.*\.rs$)
|
description: Check if the Docker templates are updated
|
||||||
pass_filenames: false
|
language: system
|
||||||
- id: check-docker-templates
|
entry: sh
|
||||||
name: check docker templates
|
args:
|
||||||
description: Check if the Docker templates are updated
|
- "-c"
|
||||||
language: system
|
- "cd docker && make"
|
||||||
entry: sh
|
# When this version is updated, do not forget to update this in `.github/workflows/typos.yaml` too
|
||||||
args:
|
- repo: https://github.com/crate-ci/typos
|
||||||
- "-c"
|
rev: 2d0ce569feab1f8752f1dde43cc2f2aa53236e06 # v1.40.0
|
||||||
- "cd docker && make"
|
hooks:
|
||||||
|
- id: typos
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,4 @@ extend-ignore-re = [
|
||||||
# https://github.com/bitwarden/server/blob/dff9f1cf538198819911cf2c20f8cda3307701c5/src/Notifications/HubHelpers.cs#L86
|
# https://github.com/bitwarden/server/blob/dff9f1cf538198819911cf2c20f8cda3307701c5/src/Notifications/HubHelpers.cs#L86
|
||||||
# https://github.com/bitwarden/clients/blob/9612a4ac45063e372a6fbe87eb253c7cb3c588fb/libs/common/src/auth/services/anonymous-hub.service.ts#L45
|
# https://github.com/bitwarden/clients/blob/9612a4ac45063e372a6fbe87eb253c7cb3c588fb/libs/common/src/auth/services/anonymous-hub.service.ts#L45
|
||||||
"AuthRequestResponseRecieved",
|
"AuthRequestResponseRecieved",
|
||||||
# Ignore Punycode/IDN tests
|
|
||||||
"xn--.+"
|
|
||||||
]
|
]
|
||||||
|
|
|
||||||
1817
Cargo.lock
generated
1817
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
90
Cargo.toml
90
Cargo.toml
|
|
@ -1,6 +1,6 @@
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.93.0"
|
rust-version = "1.90.0"
|
||||||
license = "AGPL-3.0-only"
|
license = "AGPL-3.0-only"
|
||||||
repository = "https://github.com/dani-garcia/vaultwarden"
|
repository = "https://github.com/dani-garcia/vaultwarden"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
@ -23,17 +23,15 @@ publish.workspace = true
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = [
|
default = [
|
||||||
# "sqlite" or "sqlite_system",
|
# "sqlite",
|
||||||
# "mysql",
|
# "mysql",
|
||||||
# "postgresql",
|
# "postgresql",
|
||||||
]
|
]
|
||||||
# Empty to keep compatibility, prefer to set USE_SYSLOG=true
|
# Empty to keep compatibility, prefer to set USE_SYSLOG=true
|
||||||
enable_syslog = []
|
enable_syslog = []
|
||||||
# Please enable at least one of these DB backends.
|
|
||||||
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_system = ["diesel/sqlite", "diesel_migrations/sqlite"]
|
sqlite = ["diesel/sqlite", "diesel_migrations/sqlite", "dep:libsqlite3-sys"]
|
||||||
sqlite = ["sqlite_system", "libsqlite3-sys/bundled"] # Alternative to the above, statically linked SQLite into the binary instead of dynamically.
|
|
||||||
# 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
|
||||||
|
|
@ -67,59 +65,59 @@ dotenvy = { version = "0.15.7", default-features = false }
|
||||||
# Numerical libraries
|
# Numerical libraries
|
||||||
num-traits = "0.2.19"
|
num-traits = "0.2.19"
|
||||||
num-derive = "0.4.2"
|
num-derive = "0.4.2"
|
||||||
bigdecimal = "0.4.10"
|
bigdecimal = "0.4.9"
|
||||||
|
|
||||||
# Web framework
|
# Web framework
|
||||||
rocket = { version = "0.5.1", features = ["tls", "json"], default-features = false }
|
rocket = { version = "0.5.1", features = ["tls", "json"], default-features = false }
|
||||||
rocket_ws = { version ="0.1.1" }
|
rocket_ws = { version ="0.1.1" }
|
||||||
|
|
||||||
# WebSockets libraries
|
# WebSockets libraries
|
||||||
rmpv = "1.3.1" # 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.1.0"
|
dashmap = "6.1.0"
|
||||||
|
|
||||||
# Async futures
|
# Async futures
|
||||||
futures = "0.3.32"
|
futures = "0.3.31"
|
||||||
tokio = { version = "1.52.1", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] }
|
tokio = { version = "1.48.0", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] }
|
||||||
tokio-util = { version = "0.7.18", features = ["compat"]}
|
tokio-util = { version = "0.7.17", features = ["compat"]}
|
||||||
|
|
||||||
# A generic serialization/deserialization framework
|
# A generic serialization/deserialization framework
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
serde_json = "1.0.149"
|
serde_json = "1.0.145"
|
||||||
|
|
||||||
# A safe, extensible ORM and Query builder
|
# A safe, extensible ORM and Query builder
|
||||||
# Currently pinned diesel to v2.3.3 as newer version break MySQL/MariaDB compatibility
|
# Currently pinned diesel to v2.3.3 as newer version break MySQL/MariaDB compatibility
|
||||||
diesel = { version = "2.3.9", features = ["chrono", "r2d2", "numeric"] }
|
diesel = { version = "2.3.5", features = ["chrono", "r2d2", "numeric"] }
|
||||||
diesel_migrations = "2.3.2"
|
diesel_migrations = "2.3.1"
|
||||||
|
|
||||||
derive_more = { version = "2.1.1", features = ["from", "into", "as_ref", "deref", "display"] }
|
derive_more = { version = "2.1.1", features = ["from", "into", "as_ref", "deref", "display"] }
|
||||||
diesel-derive-newtype = "2.1.2"
|
diesel-derive-newtype = "2.1.2"
|
||||||
|
|
||||||
# SQLite, statically bundled unless the `sqlite_system` feature is enabled
|
# Bundled/Static SQLite
|
||||||
libsqlite3-sys = { version = "0.37.0", optional = true }
|
libsqlite3-sys = { version = "0.35.0", features = ["bundled"], optional = true }
|
||||||
|
|
||||||
# Crypto-related libraries
|
# Crypto-related libraries
|
||||||
rand = "0.10.1"
|
rand = "0.9.2"
|
||||||
ring = "0.17.14"
|
ring = "0.17.14"
|
||||||
subtle = "2.6.1"
|
subtle = "2.6.1"
|
||||||
|
|
||||||
# UUID generation
|
# UUID generation
|
||||||
uuid = { version = "1.23.1", features = ["v4"] }
|
uuid = { version = "1.19.0", features = ["v4"] }
|
||||||
|
|
||||||
# Date and time libraries
|
# Date and time libraries
|
||||||
chrono = { version = "0.4.44", features = ["clock", "serde"], default-features = false }
|
chrono = { version = "0.4.42", features = ["clock", "serde"], default-features = false }
|
||||||
chrono-tz = "0.10.4"
|
chrono-tz = "0.10.4"
|
||||||
time = "0.3.47"
|
time = "0.3.44"
|
||||||
|
|
||||||
# Job scheduler
|
# Job scheduler
|
||||||
job_scheduler_ng = "2.4.0"
|
job_scheduler_ng = "2.4.0"
|
||||||
|
|
||||||
# Data encoding library Hex/Base32/Base64
|
# Data encoding library Hex/Base32/Base64
|
||||||
data-encoding = "2.11.0"
|
data-encoding = "2.9.0"
|
||||||
|
|
||||||
# JWT library
|
# JWT library
|
||||||
jsonwebtoken = { version = "10.3.0", features = ["use_pem", "rust_crypto"], default-features = false }
|
jsonwebtoken = { version = "10.2.0", features = ["use_pem", "rust_crypto"], default-features = false }
|
||||||
|
|
||||||
# TOTP library
|
# TOTP library
|
||||||
totp-lite = "2.0.1"
|
totp-lite = "2.0.1"
|
||||||
|
|
@ -130,67 +128,67 @@ yubico = { package = "yubico_ng", version = "0.14.1", features = ["online-tokio"
|
||||||
# WebAuthn libraries
|
# WebAuthn libraries
|
||||||
# danger-allow-state-serialisation is needed to save the state in the db
|
# danger-allow-state-serialisation is needed to save the state in the db
|
||||||
# danger-credential-internals is needed to support U2F to Webauthn migration
|
# danger-credential-internals is needed to support U2F to Webauthn migration
|
||||||
webauthn-rs = { version = "0.5.5", features = ["danger-allow-state-serialisation", "danger-credential-internals"] }
|
webauthn-rs = { version = "0.5.4", features = ["danger-allow-state-serialisation", "danger-credential-internals"] }
|
||||||
webauthn-rs-proto = "0.5.5"
|
webauthn-rs-proto = "0.5.4"
|
||||||
webauthn-rs-core = "0.5.5"
|
webauthn-rs-core = "0.5.4"
|
||||||
|
|
||||||
# Handling of URL's for WebAuthn and favicons
|
# Handling of URL's for WebAuthn and favicons
|
||||||
url = "2.5.8"
|
url = "2.5.7"
|
||||||
|
|
||||||
# Email libraries
|
# Email libraries
|
||||||
lettre = { version = "0.11.21", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "hostname", "tracing", "tokio1-rustls", "ring", "rustls-native-certs"], default-features = false }
|
lettre = { version = "0.11.19", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "hostname", "tracing", "tokio1-rustls", "ring", "rustls-native-certs"], default-features = false }
|
||||||
percent-encoding = "2.3.2" # URL encoding library used for URL's in the emails
|
percent-encoding = "2.3.2" # 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.4.0", features = ["dir_source"] }
|
handlebars = { version = "6.3.2", 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.28", features = ["rustls-tls", "rustls-tls-native-roots", "stream", "json", "deflate", "gzip", "brotli", "zstd", "socks", "cookies", "charset", "http2", "system-proxy"], default-features = false}
|
reqwest = { version = "0.12.28", features = ["rustls-tls", "rustls-tls-native-roots", "stream", "json", "deflate", "gzip", "brotli", "zstd", "socks", "cookies", "charset", "http2", "system-proxy"], default-features = false}
|
||||||
hickory-resolver = "0.26.1"
|
hickory-resolver = "0.25.2"
|
||||||
|
|
||||||
# Favicon extraction libraries
|
# Favicon extraction libraries
|
||||||
html5gum = "0.8.3"
|
html5gum = "0.8.3"
|
||||||
regex = { version = "1.12.3", features = ["std", "perf", "unicode-perl"], default-features = false }
|
regex = { version = "1.12.2", features = ["std", "perf", "unicode-perl"], default-features = false }
|
||||||
data-url = "0.3.2"
|
data-url = "0.3.2"
|
||||||
bytes = "1.11.1"
|
bytes = "1.11.0"
|
||||||
svg-hush = "0.9.6"
|
svg-hush = "0.9.5"
|
||||||
|
|
||||||
# Cache function results (Used for version check and favicon fetching)
|
# Cache function results (Used for version check and favicon fetching)
|
||||||
cached = { version = "0.59.0", features = ["async"] }
|
cached = { version = "0.56.0", features = ["async"] }
|
||||||
|
|
||||||
# Used for custom short lived cookie jar during favicon extraction
|
# Used for custom short lived cookie jar during favicon extraction
|
||||||
cookie = "0.18.1"
|
cookie = "0.18.1"
|
||||||
cookie_store = "0.22.1"
|
cookie_store = "0.22.0"
|
||||||
|
|
||||||
# Used by U2F, JWT and PostgreSQL
|
# Used by U2F, JWT and PostgreSQL
|
||||||
openssl = "0.10.78"
|
openssl = "0.10.75"
|
||||||
|
|
||||||
# CLI argument parsing
|
# CLI argument parsing
|
||||||
pico-args = "0.5.0"
|
pico-args = "0.5.0"
|
||||||
|
|
||||||
# Macro ident concatenation
|
# Macro ident concatenation
|
||||||
pastey = "0.2.2"
|
pastey = "0.2.1"
|
||||||
governor = "0.10.4"
|
governor = "0.10.4"
|
||||||
|
|
||||||
# OIDC for SSO
|
# OIDC for SSO
|
||||||
openidconnect = { version = "4.0.1", features = ["reqwest", "rustls-tls"] }
|
openidconnect = { version = "4.0.1", features = ["reqwest", "native-tls"] }
|
||||||
moka = { version = "0.12.15", features = ["future"] }
|
mini-moka = "0.10.3"
|
||||||
|
|
||||||
# Check client versions for specific features.
|
# Check client versions for specific features.
|
||||||
semver = "1.0.28"
|
semver = "1.0.27"
|
||||||
|
|
||||||
# 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.50", features = ["secure"], default-features = false, optional = true }
|
mimalloc = { version = "0.1.48", features = ["secure"], default-features = false, optional = true }
|
||||||
|
|
||||||
which = "8.0.2"
|
which = "8.0.0"
|
||||||
|
|
||||||
# Argon2 library with support for the PHC format
|
# Argon2 library with support for the PHC format
|
||||||
argon2 = "0.5.3"
|
argon2 = "0.5.3"
|
||||||
|
|
||||||
# Reading a password from the cli for generating the Argon2id ADMIN_TOKEN
|
# Reading a password from the cli for generating the Argon2id ADMIN_TOKEN
|
||||||
rpassword = "7.5.1"
|
rpassword = "7.4.0"
|
||||||
|
|
||||||
# Loading a dynamic CSS Stylesheet
|
# Loading a dynamic CSS Stylesheet
|
||||||
grass_compiler = { version = "0.13.4", default-features = false }
|
grass_compiler = { version = "0.13.4", default-features = false }
|
||||||
|
|
@ -199,10 +197,10 @@ grass_compiler = { version = "0.13.4", default-features = false }
|
||||||
opendal = { version = "0.55.0", features = ["services-fs"], default-features = false }
|
opendal = { version = "0.55.0", features = ["services-fs"], default-features = false }
|
||||||
|
|
||||||
# For retrieving AWS credentials, including temporary SSO credentials
|
# For retrieving AWS credentials, including temporary SSO credentials
|
||||||
anyhow = { version = "1.0.102", optional = true }
|
anyhow = { version = "1.0.100", optional = true }
|
||||||
aws-config = { version = "1.8.16", features = ["behavior-version-latest", "rt-tokio", "credentials-process", "sso"], default-features = false, optional = true }
|
aws-config = { version = "1.8.12", features = ["behavior-version-latest", "rt-tokio", "credentials-process", "sso"], default-features = false, optional = true }
|
||||||
aws-credential-types = { version = "1.2.14", optional = true }
|
aws-credential-types = { version = "1.2.11", optional = true }
|
||||||
aws-smithy-runtime-api = { version = "1.12.0", optional = true }
|
aws-smithy-runtime-api = { version = "1.9.3", optional = true }
|
||||||
http = { version = "1.4.0", optional = true }
|
http = { version = "1.4.0", optional = true }
|
||||||
reqsign = { version = "0.16.5", optional = true }
|
reqsign = { version = "0.16.5", optional = true }
|
||||||
|
|
||||||
|
|
@ -303,7 +301,6 @@ branches_sharing_code = "deny"
|
||||||
case_sensitive_file_extension_comparisons = "deny"
|
case_sensitive_file_extension_comparisons = "deny"
|
||||||
cast_lossless = "deny"
|
cast_lossless = "deny"
|
||||||
clone_on_ref_ptr = "deny"
|
clone_on_ref_ptr = "deny"
|
||||||
duration_suboptimal_units = "deny"
|
|
||||||
equatable_if_let = "deny"
|
equatable_if_let = "deny"
|
||||||
excessive_precision = "deny"
|
excessive_precision = "deny"
|
||||||
filter_map_next = "deny"
|
filter_map_next = "deny"
|
||||||
|
|
@ -325,7 +322,6 @@ needless_continue = "deny"
|
||||||
needless_lifetimes = "deny"
|
needless_lifetimes = "deny"
|
||||||
option_option = "deny"
|
option_option = "deny"
|
||||||
redundant_clone = "deny"
|
redundant_clone = "deny"
|
||||||
ref_option = "deny"
|
|
||||||
string_add_assign = "deny"
|
string_add_assign = "deny"
|
||||||
unnecessary_join = "deny"
|
unnecessary_join = "deny"
|
||||||
unnecessary_self_imports = "deny"
|
unnecessary_self_imports = "deny"
|
||||||
|
|
|
||||||
|
|
@ -59,9 +59,8 @@ A nearly complete implementation of the Bitwarden Client API is provided, includ
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> The web-vault requires the use of HTTPS and a secure context for the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API). <br>
|
> The web-vault requires the use a secure context for the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API).
|
||||||
> That means it will only work if you [enable HTTPS](https://github.com/dani-garcia/vaultwarden/wiki/Enabling-HTTPS). <br>
|
> That means it will only work via `http://localhost:8000` (using the port from the example below) or if you [enable HTTPS](https://github.com/dani-garcia/vaultwarden/wiki/Enabling-HTTPS).
|
||||||
> We also suggest to use a [reverse proxy](https://github.com/dani-garcia/vaultwarden/wiki/Proxy-examples).
|
|
||||||
|
|
||||||
The recommended way to install and use Vaultwarden is via our container images which are published to [ghcr.io](https://github.com/dani-garcia/vaultwarden/pkgs/container/vaultwarden), [docker.io](https://hub.docker.com/r/vaultwarden/server) and [quay.io](https://quay.io/repository/vaultwarden/server).
|
The recommended way to install and use Vaultwarden is via our container images which are published to [ghcr.io](https://github.com/dani-garcia/vaultwarden/pkgs/container/vaultwarden), [docker.io](https://hub.docker.com/r/vaultwarden/server) and [quay.io](https://quay.io/repository/vaultwarden/server).
|
||||||
See [which container image to use](https://github.com/dani-garcia/vaultwarden/wiki/Which-container-image-to-use) for an explanation of the provided tags.
|
See [which container image to use](https://github.com/dani-garcia/vaultwarden/wiki/Which-container-image-to-use) for an explanation of the provided tags.
|
||||||
|
|
|
||||||
12
build.rs
12
build.rs
|
|
@ -2,21 +2,21 @@ use std::env;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
// These allow using e.g. #[cfg(mysql)] instead of #[cfg(feature = "mysql")], which helps when trying to add them through macros
|
// This allow using #[cfg(sqlite)] instead of #[cfg(feature = "sqlite")], which helps when trying to add them through macros
|
||||||
#[cfg(feature = "sqlite_system")] // The `sqlite` feature implies this one.
|
#[cfg(feature = "sqlite")]
|
||||||
println!("cargo:rustc-cfg=sqlite");
|
println!("cargo:rustc-cfg=sqlite");
|
||||||
#[cfg(feature = "mysql")]
|
#[cfg(feature = "mysql")]
|
||||||
println!("cargo:rustc-cfg=mysql");
|
println!("cargo:rustc-cfg=mysql");
|
||||||
#[cfg(feature = "postgresql")]
|
#[cfg(feature = "postgresql")]
|
||||||
println!("cargo:rustc-cfg=postgresql");
|
println!("cargo:rustc-cfg=postgresql");
|
||||||
#[cfg(not(any(feature = "sqlite_system", feature = "mysql", feature = "postgresql")))]
|
#[cfg(feature = "s3")]
|
||||||
|
println!("cargo:rustc-cfg=s3");
|
||||||
|
|
||||||
|
#[cfg(not(any(feature = "sqlite", feature = "mysql", feature = "postgresql")))]
|
||||||
compile_error!(
|
compile_error!(
|
||||||
"You need to enable one DB backend. To build with previous defaults do: cargo build --features sqlite"
|
"You need to enable one DB backend. To build with previous defaults do: cargo build --features sqlite"
|
||||||
);
|
);
|
||||||
|
|
||||||
#[cfg(feature = "s3")]
|
|
||||||
println!("cargo:rustc-cfg=s3");
|
|
||||||
|
|
||||||
// Use check-cfg to let cargo know which cfg's we define,
|
// Use check-cfg to let cargo know which cfg's we define,
|
||||||
// and avoid warnings when they are used in the code.
|
// and avoid warnings when they are used in the code.
|
||||||
println!("cargo::rustc-check-cfg=cfg(sqlite)");
|
println!("cargo::rustc-check-cfg=cfg(sqlite)");
|
||||||
|
|
|
||||||
|
|
@ -2,4 +2,4 @@
|
||||||
# see diesel.rs/guides/configuring-diesel-cli
|
# see diesel.rs/guides/configuring-diesel-cli
|
||||||
|
|
||||||
[print_schema]
|
[print_schema]
|
||||||
file = "src/db/schema.rs"
|
file = "src/db/schema.rs"
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
---
|
---
|
||||||
vault_version: "v2026.4.1"
|
vault_version: "v2025.12.0"
|
||||||
vault_image_digest: "sha256:ca2a4251c4e63c9ad428262b4dd452789a1b9f6fce71da351e93dceed0d2edbe"
|
vault_image_digest: "sha256:bb7303efafdb7e2b41bee2c772e14f67676ae2c9047bd7bba80d3544d4162613"
|
||||||
# Cross Compile Docker Helper Scripts v1.9.0
|
# Cross Compile Docker Helper Scripts v1.9.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
|
||||||
# https://github.com/tonistiigi/xx | https://hub.docker.com/r/tonistiigi/xx/tags
|
# https://github.com/tonistiigi/xx | https://hub.docker.com/r/tonistiigi/xx/tags
|
||||||
xx_image_digest: "sha256:c64defb9ed5a91eacb37f96ccc3d4cd72521c4bd18d5442905b95e2226b0e707"
|
xx_image_digest: "sha256:c64defb9ed5a91eacb37f96ccc3d4cd72521c4bd18d5442905b95e2226b0e707"
|
||||||
rust_version: 1.95.0 # Rust version to be used
|
rust_version: 1.92.0 # Rust version to be used
|
||||||
debian_version: trixie # Debian release name to be used
|
debian_version: trixie # Debian release name to be used
|
||||||
alpine_version: "3.23" # Alpine version to be used
|
alpine_version: "3.23" # 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:v2026.4.1
|
# $ docker pull docker.io/vaultwarden/web-vault:v2025.12.0
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2026.4.1
|
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.12.0
|
||||||
# [docker.io/vaultwarden/web-vault@sha256:ca2a4251c4e63c9ad428262b4dd452789a1b9f6fce71da351e93dceed0d2edbe]
|
# [docker.io/vaultwarden/web-vault@sha256:bb7303efafdb7e2b41bee2c772e14f67676ae2c9047bd7bba80d3544d4162613]
|
||||||
#
|
#
|
||||||
# - 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:ca2a4251c4e63c9ad428262b4dd452789a1b9f6fce71da351e93dceed0d2edbe
|
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:bb7303efafdb7e2b41bee2c772e14f67676ae2c9047bd7bba80d3544d4162613
|
||||||
# [docker.io/vaultwarden/web-vault:v2026.4.1]
|
# [docker.io/vaultwarden/web-vault:v2025.12.0]
|
||||||
#
|
#
|
||||||
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:ca2a4251c4e63c9ad428262b4dd452789a1b9f6fce71da351e93dceed0d2edbe AS vault
|
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:bb7303efafdb7e2b41bee2c772e14f67676ae2c9047bd7bba80d3544d4162613 AS vault
|
||||||
|
|
||||||
########################## ALPINE BUILD IMAGES ##########################
|
########################## ALPINE BUILD IMAGES ##########################
|
||||||
## NOTE: The Alpine Base Images do not support other platforms then linux/amd64 and linux/arm64
|
## NOTE: The Alpine Base Images do not support other platforms then linux/amd64 and linux/arm64
|
||||||
## 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=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.95.0 AS build_amd64
|
FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.92.0 AS build_amd64
|
||||||
FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.95.0 AS build_arm64
|
FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.92.0 AS build_arm64
|
||||||
FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.95.0 AS build_armv7
|
FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.92.0 AS build_armv7
|
||||||
FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.95.0 AS build_armv6
|
FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.92.0 AS build_armv6
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
# hadolint ignore=DL3006
|
# hadolint ignore=DL3006
|
||||||
|
|
|
||||||
|
|
@ -19,15 +19,15 @@
|
||||||
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
||||||
# click the tag name to view the digest of the image it currently points to.
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
# - From the command line:
|
# - From the command line:
|
||||||
# $ docker pull docker.io/vaultwarden/web-vault:v2026.4.1
|
# $ docker pull docker.io/vaultwarden/web-vault:v2025.12.0
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2026.4.1
|
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.12.0
|
||||||
# [docker.io/vaultwarden/web-vault@sha256:ca2a4251c4e63c9ad428262b4dd452789a1b9f6fce71da351e93dceed0d2edbe]
|
# [docker.io/vaultwarden/web-vault@sha256:bb7303efafdb7e2b41bee2c772e14f67676ae2c9047bd7bba80d3544d4162613]
|
||||||
#
|
#
|
||||||
# - 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:ca2a4251c4e63c9ad428262b4dd452789a1b9f6fce71da351e93dceed0d2edbe
|
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:bb7303efafdb7e2b41bee2c772e14f67676ae2c9047bd7bba80d3544d4162613
|
||||||
# [docker.io/vaultwarden/web-vault:v2026.4.1]
|
# [docker.io/vaultwarden/web-vault:v2025.12.0]
|
||||||
#
|
#
|
||||||
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:ca2a4251c4e63c9ad428262b4dd452789a1b9f6fce71da351e93dceed0d2edbe AS vault
|
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:bb7303efafdb7e2b41bee2c772e14f67676ae2c9047bd7bba80d3544d4162613 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
|
||||||
|
|
@ -36,7 +36,7 @@ FROM --platform=linux/amd64 docker.io/tonistiigi/xx@sha256:c64defb9ed5a91eacb37f
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
# hadolint ignore=DL3006
|
# hadolint ignore=DL3006
|
||||||
FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.95.0-slim-trixie AS build
|
FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.92.0-slim-trixie AS build
|
||||||
COPY --from=xx / /
|
COPY --from=xx / /
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
ARG TARGETVARIANT
|
ARG TARGETVARIANT
|
||||||
|
|
|
||||||
|
|
@ -19,13 +19,13 @@
|
||||||
# - 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:{{ vault_version | replace('+', '_') }}
|
# $ docker pull docker.io/vaultwarden/web-vault:{{ vault_version }}
|
||||||
# $ docker image inspect --format "{{ '{{' }}.RepoDigests}}" docker.io/vaultwarden/web-vault:{{ vault_version | replace('+', '_') }}
|
# $ docker image inspect --format "{{ '{{' }}.RepoDigests}}" docker.io/vaultwarden/web-vault:{{ vault_version }}
|
||||||
# [docker.io/vaultwarden/web-vault@{{ vault_image_digest }}]
|
# [docker.io/vaultwarden/web-vault@{{ vault_image_digest }}]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{ '{{' }}.RepoTags}}" docker.io/vaultwarden/web-vault@{{ vault_image_digest }}
|
# $ docker image inspect --format "{{ '{{' }}.RepoTags}}" docker.io/vaultwarden/web-vault@{{ vault_image_digest }}
|
||||||
# [docker.io/vaultwarden/web-vault:{{ vault_version | replace('+', '_') }}]
|
# [docker.io/vaultwarden/web-vault:{{ vault_version }}]
|
||||||
#
|
#
|
||||||
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@{{ vault_image_digest }} AS vault
|
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@{{ vault_image_digest }} AS vault
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@ path = "src/lib.rs"
|
||||||
proc-macro = true
|
proc-macro = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
quote = "1.0.45"
|
quote = "1.0.42"
|
||||||
syn = "2.0.117"
|
syn = "2.0.111"
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
DROP TABLE IF EXISTS archives;
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
DROP TABLE IF EXISTS archives;
|
|
||||||
|
|
||||||
CREATE TABLE archives (
|
|
||||||
user_uuid CHAR(36) NOT NULL,
|
|
||||||
cipher_uuid CHAR(36) NOT NULL,
|
|
||||||
archived_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
PRIMARY KEY (user_uuid, cipher_uuid),
|
|
||||||
FOREIGN KEY (user_uuid) REFERENCES users (uuid) ON DELETE CASCADE,
|
|
||||||
FOREIGN KEY (cipher_uuid) REFERENCES ciphers (uuid) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
ALTER TABLE sso_auth DROP COLUMN binding_hash;
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
ALTER TABLE sso_auth ADD COLUMN binding_hash TEXT;
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
DROP TABLE IF EXISTS archives;
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
DROP TABLE IF EXISTS archives;
|
|
||||||
|
|
||||||
CREATE TABLE archives (
|
|
||||||
user_uuid CHAR(36) NOT NULL REFERENCES users (uuid) ON DELETE CASCADE,
|
|
||||||
cipher_uuid CHAR(36) NOT NULL REFERENCES ciphers (uuid) ON DELETE CASCADE,
|
|
||||||
archived_at TIMESTAMP NOT NULL DEFAULT now(),
|
|
||||||
PRIMARY KEY (user_uuid, cipher_uuid)
|
|
||||||
);
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
ALTER TABLE sso_auth DROP COLUMN binding_hash;
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
ALTER TABLE sso_auth ADD COLUMN binding_hash TEXT;
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
DROP TABLE IF EXISTS archives;
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
DROP TABLE IF EXISTS archives;
|
|
||||||
|
|
||||||
CREATE TABLE archives (
|
|
||||||
user_uuid CHAR(36) NOT NULL REFERENCES users (uuid) ON DELETE CASCADE,
|
|
||||||
cipher_uuid CHAR(36) NOT NULL REFERENCES ciphers (uuid) ON DELETE CASCADE,
|
|
||||||
archived_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
PRIMARY KEY (user_uuid, cipher_uuid)
|
|
||||||
);
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
ALTER TABLE sso_auth DROP COLUMN binding_hash;
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
ALTER TABLE sso_auth ADD COLUMN binding_hash TEXT;
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
[toolchain]
|
[toolchain]
|
||||||
channel = "1.95.0"
|
channel = "1.92.0"
|
||||||
components = [ "rustfmt", "clippy" ]
|
components = [ "rustfmt", "clippy" ]
|
||||||
profile = "minimal"
|
profile = "minimal"
|
||||||
|
|
|
||||||
109
src/api/admin.rs
109
src/api/admin.rs
|
|
@ -30,10 +30,9 @@ use crate::{
|
||||||
error::{Error, MapResult},
|
error::{Error, MapResult},
|
||||||
http_client::make_http_request,
|
http_client::make_http_request,
|
||||||
mail,
|
mail,
|
||||||
sso::FAKE_SSO_IDENTIFIER,
|
|
||||||
util::{
|
util::{
|
||||||
container_base_image, format_naive_datetime_local, get_active_web_release, get_display_size,
|
container_base_image, format_naive_datetime_local, get_display_size, get_web_vault_version,
|
||||||
is_running_in_container, parse_experimental_client_feature_flags, FeatureFlagFilter, NumberOrString,
|
is_running_in_container, NumberOrString,
|
||||||
},
|
},
|
||||||
CONFIG, VERSION,
|
CONFIG, VERSION,
|
||||||
};
|
};
|
||||||
|
|
@ -316,11 +315,7 @@ async fn invite_user(data: Json<InviteData>, _token: AdminToken, conn: DbConn) -
|
||||||
|
|
||||||
async fn _generate_invite(user: &User, conn: &DbConn) -> EmptyResult {
|
async fn _generate_invite(user: &User, conn: &DbConn) -> EmptyResult {
|
||||||
if CONFIG.mail_enabled() {
|
if CONFIG.mail_enabled() {
|
||||||
let org_id: OrganizationId = if CONFIG.sso_enabled() {
|
let org_id: OrganizationId = FAKE_ADMIN_UUID.to_string().into();
|
||||||
FAKE_SSO_IDENTIFIER.into()
|
|
||||||
} else {
|
|
||||||
FAKE_ADMIN_UUID.into()
|
|
||||||
};
|
|
||||||
let member_id: MembershipId = FAKE_ADMIN_UUID.to_string().into();
|
let member_id: MembershipId = FAKE_ADMIN_UUID.to_string().into();
|
||||||
mail::send_invite(user, org_id, member_id, &CONFIG.invitation_org_name(), None).await
|
mail::send_invite(user, org_id, member_id, &CONFIG.invitation_org_name(), None).await
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -469,7 +464,7 @@ async fn deauth_user(user_id: UserId, _token: AdminToken, conn: DbConn, nt: Noti
|
||||||
|
|
||||||
if CONFIG.push_enabled() {
|
if CONFIG.push_enabled() {
|
||||||
for device in Device::find_push_devices_by_user(&user.uuid, &conn).await {
|
for device in Device::find_push_devices_by_user(&user.uuid, &conn).await {
|
||||||
match unregister_push_device(device.push_uuid.as_ref()).await {
|
match unregister_push_device(&device.push_uuid).await {
|
||||||
Ok(r) => r,
|
Ok(r) => r,
|
||||||
Err(e) => error!("Unable to unregister devices from Bitwarden server: {e}"),
|
Err(e) => error!("Unable to unregister devices from Bitwarden server: {e}"),
|
||||||
};
|
};
|
||||||
|
|
@ -477,7 +472,7 @@ async fn deauth_user(user_id: UserId, _token: AdminToken, conn: DbConn, nt: Noti
|
||||||
}
|
}
|
||||||
|
|
||||||
Device::delete_all_by_user(&user.uuid, &conn).await?;
|
Device::delete_all_by_user(&user.uuid, &conn).await?;
|
||||||
user.reset_security_stamp(&conn).await?;
|
user.reset_security_stamp();
|
||||||
|
|
||||||
user.save(&conn).await
|
user.save(&conn).await
|
||||||
}
|
}
|
||||||
|
|
@ -485,15 +480,14 @@ async fn deauth_user(user_id: UserId, _token: AdminToken, conn: DbConn, nt: Noti
|
||||||
#[post("/users/<user_id>/disable", format = "application/json")]
|
#[post("/users/<user_id>/disable", format = "application/json")]
|
||||||
async fn disable_user(user_id: UserId, _token: AdminToken, conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
async fn disable_user(user_id: UserId, _token: AdminToken, conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
||||||
let mut user = get_user_or_404(&user_id, &conn).await?;
|
let mut user = get_user_or_404(&user_id, &conn).await?;
|
||||||
user.reset_security_stamp(&conn).await?;
|
Device::delete_all_by_user(&user.uuid, &conn).await?;
|
||||||
|
user.reset_security_stamp();
|
||||||
user.enabled = false;
|
user.enabled = false;
|
||||||
|
|
||||||
let save_result = user.save(&conn).await;
|
let save_result = user.save(&conn).await;
|
||||||
|
|
||||||
nt.send_logout(&user, None, &conn).await;
|
nt.send_logout(&user, None, &conn).await;
|
||||||
|
|
||||||
Device::delete_all_by_user(&user.uuid, &conn).await?;
|
|
||||||
|
|
||||||
save_result
|
save_result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -523,11 +517,7 @@ async fn resend_user_invite(user_id: UserId, _token: AdminToken, conn: DbConn) -
|
||||||
}
|
}
|
||||||
|
|
||||||
if CONFIG.mail_enabled() {
|
if CONFIG.mail_enabled() {
|
||||||
let org_id: OrganizationId = if CONFIG.sso_enabled() {
|
let org_id: OrganizationId = FAKE_ADMIN_UUID.to_string().into();
|
||||||
FAKE_SSO_IDENTIFIER.into()
|
|
||||||
} else {
|
|
||||||
FAKE_ADMIN_UUID.into()
|
|
||||||
};
|
|
||||||
let member_id: MembershipId = FAKE_ADMIN_UUID.to_string().into();
|
let member_id: MembershipId = FAKE_ADMIN_UUID.to_string().into();
|
||||||
mail::send_invite(&user, org_id, member_id, &CONFIG.invitation_org_name(), None).await
|
mail::send_invite(&user, org_id, member_id, &CONFIG.invitation_org_name(), None).await
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -647,6 +637,7 @@ use cached::proc_macro::cached;
|
||||||
/// Cache this function to prevent API call rate limit. Github only allows 60 requests per hour, and we use 3 here already
|
/// Cache this function to prevent API call rate limit. Github only allows 60 requests per hour, and we use 3 here already
|
||||||
/// It will cache this function for 600 seconds (10 minutes) which should prevent the exhaustion of the rate limit
|
/// It will cache this function for 600 seconds (10 minutes) which should prevent the exhaustion of the rate limit
|
||||||
/// Any cache will be lost if Vaultwarden is restarted
|
/// Any cache will be lost if Vaultwarden is restarted
|
||||||
|
use std::time::Duration; // Needed for cached
|
||||||
#[cached(time = 600, sync_writes = "default")]
|
#[cached(time = 600, sync_writes = "default")]
|
||||||
async fn get_release_info(has_http_access: bool) -> (String, String, String) {
|
async fn get_release_info(has_http_access: bool) -> (String, String, String) {
|
||||||
// If the HTTP Check failed, do not even attempt to check for new versions since we were not able to connect with github.com anyway.
|
// If the HTTP Check failed, do not even attempt to check for new versions since we were not able to connect with github.com anyway.
|
||||||
|
|
@ -698,26 +689,6 @@ async fn get_ntp_time(has_http_access: bool) -> String {
|
||||||
String::from("Unable to fetch NTP time.")
|
String::from("Unable to fetch NTP time.")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn web_vault_compare(active: &str, latest: &str) -> i8 {
|
|
||||||
use semver::Version;
|
|
||||||
use std::cmp::Ordering;
|
|
||||||
|
|
||||||
let active_semver = Version::parse(active).unwrap_or_else(|e| {
|
|
||||||
warn!("Unable to parse active web-vault version '{active}': {e}");
|
|
||||||
Version::parse("2025.1.1").unwrap()
|
|
||||||
});
|
|
||||||
let latest_semver = Version::parse(latest).unwrap_or_else(|e| {
|
|
||||||
warn!("Unable to parse latest web-vault version '{latest}': {e}");
|
|
||||||
Version::parse("2025.1.1").unwrap()
|
|
||||||
});
|
|
||||||
|
|
||||||
match active_semver.cmp(&latest_semver) {
|
|
||||||
Ordering::Less => -1,
|
|
||||||
Ordering::Equal => 0,
|
|
||||||
Ordering::Greater => 1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/diagnostics")]
|
#[get("/diagnostics")]
|
||||||
async fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> ApiResult<Html<String>> {
|
async fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> ApiResult<Html<String>> {
|
||||||
use chrono::prelude::*;
|
use chrono::prelude::*;
|
||||||
|
|
@ -737,28 +708,32 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> A
|
||||||
_ => "Unable to resolve domain name.".to_string(),
|
_ => "Unable to resolve domain name.".to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let (latest_vw_release, latest_vw_commit, latest_web_release) = get_release_info(has_http_access).await;
|
let (latest_release, latest_commit, latest_web_build) = get_release_info(has_http_access).await;
|
||||||
let active_web_release = get_active_web_release();
|
|
||||||
let web_vault_compare = web_vault_compare(&active_web_release, &latest_web_release);
|
|
||||||
|
|
||||||
let ip_header_name = &ip_header.0.unwrap_or_default();
|
let ip_header_name = &ip_header.0.unwrap_or_default();
|
||||||
|
|
||||||
let invalid_feature_flags: Vec<String> = parse_experimental_client_feature_flags(
|
// Get current running versions
|
||||||
&CONFIG.experimental_client_feature_flags(),
|
let web_vault_version = get_web_vault_version();
|
||||||
FeatureFlagFilter::InvalidOnly,
|
|
||||||
)
|
// Check if the running version is newer than the latest stable released version
|
||||||
.into_keys()
|
let web_vault_pre_release = if let Ok(web_ver_match) = semver::VersionReq::parse(&format!(">{latest_web_build}")) {
|
||||||
.collect();
|
web_ver_match.matches(
|
||||||
|
&semver::Version::parse(&web_vault_version).unwrap_or_else(|_| semver::Version::parse("2025.1.1").unwrap()),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
error!("Unable to parse latest_web_build: '{latest_web_build}'");
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
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_vw_release,
|
"latest_release": latest_release,
|
||||||
"latest_commit": latest_vw_commit,
|
"latest_commit": latest_commit,
|
||||||
"web_vault_enabled": &CONFIG.web_vault_enabled(),
|
"web_vault_enabled": &CONFIG.web_vault_enabled(),
|
||||||
"active_web_release": active_web_release,
|
"web_vault_version": web_vault_version,
|
||||||
"latest_web_release": latest_web_release,
|
"latest_web_build": latest_web_build,
|
||||||
"web_vault_compare": web_vault_compare,
|
"web_vault_pre_release": web_vault_pre_release,
|
||||||
"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" },
|
||||||
"has_http_access": has_http_access,
|
"has_http_access": has_http_access,
|
||||||
|
|
@ -772,7 +747,6 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> A
|
||||||
"db_version": get_sql_server_version(&conn).await,
|
"db_version": get_sql_server_version(&conn).await,
|
||||||
"admin_url": format!("{}/diagnostics", admin_url()),
|
"admin_url": format!("{}/diagnostics", admin_url()),
|
||||||
"overrides": &CONFIG.get_overrides().join(", "),
|
"overrides": &CONFIG.get_overrides().join(", "),
|
||||||
"invalid_feature_flags": invalid_feature_flags,
|
|
||||||
"host_arch": env::consts::ARCH,
|
"host_arch": env::consts::ARCH,
|
||||||
"host_os": env::consts::OS,
|
"host_os": env::consts::OS,
|
||||||
"tz_env": env::var("TZ").unwrap_or_default(),
|
"tz_env": env::var("TZ").unwrap_or_default(),
|
||||||
|
|
@ -870,32 +844,3 @@ impl<'r> FromRequest<'r> for AdminToken {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn validate_web_vault_compare() {
|
|
||||||
// web_vault_compare(active, latest)
|
|
||||||
// Test normal versions
|
|
||||||
assert!(web_vault_compare("2025.12.0", "2025.12.1") == -1);
|
|
||||||
assert!(web_vault_compare("2025.12.1", "2025.12.1") == 0);
|
|
||||||
assert!(web_vault_compare("2025.12.2", "2025.12.1") == 1);
|
|
||||||
|
|
||||||
// Test patched/+build.n versions
|
|
||||||
// Newer latest version
|
|
||||||
assert!(web_vault_compare("2025.12.0+build.1", "2025.12.1") == -1);
|
|
||||||
assert!(web_vault_compare("2025.12.1", "2025.12.1+build.1") == -1);
|
|
||||||
assert!(web_vault_compare("2025.12.0+build.1", "2025.12.1+build.1") == -1);
|
|
||||||
assert!(web_vault_compare("2025.12.1+build.1", "2025.12.1+build.2") == -1);
|
|
||||||
// Equal versions
|
|
||||||
assert!(web_vault_compare("2025.12.1+build.1", "2025.12.1+build.1") == 0);
|
|
||||||
assert!(web_vault_compare("2025.12.2+build.2", "2025.12.2+build.2") == 0);
|
|
||||||
// Newer active version
|
|
||||||
assert!(web_vault_compare("2025.12.1+build.1", "2025.12.1") == 1);
|
|
||||||
assert!(web_vault_compare("2025.12.2", "2025.12.1+build.1") == 1);
|
|
||||||
assert!(web_vault_compare("2025.12.2+build.1", "2025.12.1+build.1") == 1);
|
|
||||||
assert!(web_vault_compare("2025.12.1+build.3", "2025.12.1+build.2") == 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ use crate::{
|
||||||
DbConn,
|
DbConn,
|
||||||
},
|
},
|
||||||
mail,
|
mail,
|
||||||
util::{deser_opt_nonempty_str, format_date, NumberOrString},
|
util::{format_date, NumberOrString},
|
||||||
CONFIG,
|
CONFIG,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -33,6 +33,7 @@ use rocket::{
|
||||||
|
|
||||||
pub fn routes() -> Vec<rocket::Route> {
|
pub fn routes() -> Vec<rocket::Route> {
|
||||||
routes![
|
routes![
|
||||||
|
register,
|
||||||
profile,
|
profile,
|
||||||
put_profile,
|
put_profile,
|
||||||
post_profile,
|
post_profile,
|
||||||
|
|
@ -106,6 +107,7 @@ pub struct RegisterData {
|
||||||
|
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
organization_user_id: Option<MembershipId>,
|
organization_user_id: Option<MembershipId>,
|
||||||
|
|
||||||
// Used only from the register/finish endpoint
|
// Used only from the register/finish endpoint
|
||||||
|
|
@ -137,7 +139,7 @@ struct KeysData {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Trims whitespace from password hints, and converts blank password hints to `None`.
|
/// Trims whitespace from password hints, and converts blank password hints to `None`.
|
||||||
fn clean_password_hint(password_hint: Option<&String>) -> Option<String> {
|
fn clean_password_hint(password_hint: &Option<String>) -> Option<String> {
|
||||||
match password_hint {
|
match password_hint {
|
||||||
None => None,
|
None => None,
|
||||||
Some(h) => match h.trim() {
|
Some(h) => match h.trim() {
|
||||||
|
|
@ -147,7 +149,7 @@ fn clean_password_hint(password_hint: Option<&String>) -> Option<String> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn enforce_password_hint_setting(password_hint: Option<&String>) -> EmptyResult {
|
fn enforce_password_hint_setting(password_hint: &Option<String>) -> EmptyResult {
|
||||||
if password_hint.is_some() && !CONFIG.password_hints_allowed() {
|
if password_hint.is_some() && !CONFIG.password_hints_allowed() {
|
||||||
err!("Password hints have been disabled by the administrator. Remove the hint and try again.");
|
err!("Password hints have been disabled by the administrator. Remove the hint and try again.");
|
||||||
}
|
}
|
||||||
|
|
@ -166,6 +168,11 @@ async fn is_email_2fa_required(member_id: Option<MembershipId>, conn: &DbConn) -
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[post("/accounts/register", data = "<data>")]
|
||||||
|
async fn register(data: Json<RegisterData>, conn: DbConn) -> JsonResult {
|
||||||
|
_register(data, false, conn).await
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn _register(data: Json<RegisterData>, email_verification: bool, conn: DbConn) -> JsonResult {
|
pub async fn _register(data: Json<RegisterData>, email_verification: bool, conn: DbConn) -> JsonResult {
|
||||||
let mut data: RegisterData = data.into_inner();
|
let mut data: RegisterData = data.into_inner();
|
||||||
let email = data.email.to_lowercase();
|
let email = data.email.to_lowercase();
|
||||||
|
|
@ -245,8 +252,8 @@ pub async fn _register(data: Json<RegisterData>, email_verification: bool, conn:
|
||||||
|
|
||||||
// Check against the password hint setting here so if it fails, the user
|
// Check against the password hint setting here so if it fails, the user
|
||||||
// can retry without losing their invitation below.
|
// can retry without losing their invitation below.
|
||||||
let password_hint = clean_password_hint(data.master_password_hint.as_ref());
|
let password_hint = clean_password_hint(&data.master_password_hint);
|
||||||
enforce_password_hint_setting(password_hint.as_ref())?;
|
enforce_password_hint_setting(&password_hint)?;
|
||||||
|
|
||||||
let mut user = match User::find_by_mail(&email, &conn).await {
|
let mut user = match User::find_by_mail(&email, &conn).await {
|
||||||
Some(user) => {
|
Some(user) => {
|
||||||
|
|
@ -295,7 +302,7 @@ pub async fn _register(data: Json<RegisterData>, email_verification: bool, conn:
|
||||||
|
|
||||||
set_kdf_data(&mut user, &data.kdf)?;
|
set_kdf_data(&mut user, &data.kdf)?;
|
||||||
|
|
||||||
user.set_password(&data.master_password_hash, Some(data.key), true, None, &conn).await?;
|
user.set_password(&data.master_password_hash, Some(data.key), true, None);
|
||||||
user.password_hint = password_hint;
|
user.password_hint = password_hint;
|
||||||
|
|
||||||
// Add extra fields if present
|
// Add extra fields if present
|
||||||
|
|
@ -353,8 +360,8 @@ async fn post_set_password(data: Json<SetPasswordData>, headers: Headers, conn:
|
||||||
|
|
||||||
// Check against the password hint setting here so if it fails,
|
// Check against the password hint setting here so if it fails,
|
||||||
// the user can retry without losing their invitation below.
|
// the user can retry without losing their invitation below.
|
||||||
let password_hint = clean_password_hint(data.master_password_hint.as_ref());
|
let password_hint = clean_password_hint(&data.master_password_hint);
|
||||||
enforce_password_hint_setting(password_hint.as_ref())?;
|
enforce_password_hint_setting(&password_hint)?;
|
||||||
|
|
||||||
set_kdf_data(&mut user, &data.kdf)?;
|
set_kdf_data(&mut user, &data.kdf)?;
|
||||||
|
|
||||||
|
|
@ -363,9 +370,7 @@ async fn post_set_password(data: Json<SetPasswordData>, headers: Headers, conn:
|
||||||
Some(data.key),
|
Some(data.key),
|
||||||
false,
|
false,
|
||||||
Some(vec![String::from("revision_date")]), // We need to allow revision-date to use the old security_timestamp
|
Some(vec![String::from("revision_date")]), // We need to allow revision-date to use the old security_timestamp
|
||||||
&conn,
|
);
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
user.password_hint = password_hint;
|
user.password_hint = password_hint;
|
||||||
|
|
||||||
if let Some(keys) = data.keys {
|
if let Some(keys) = data.keys {
|
||||||
|
|
@ -374,13 +379,15 @@ async fn post_set_password(data: Json<SetPasswordData>, headers: Headers, conn:
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(identifier) = data.org_identifier {
|
if let Some(identifier) = data.org_identifier {
|
||||||
if identifier != crate::sso::FAKE_SSO_IDENTIFIER && identifier != crate::api::admin::FAKE_ADMIN_UUID {
|
if identifier != crate::sso::FAKE_IDENTIFIER && identifier != crate::api::admin::FAKE_ADMIN_UUID {
|
||||||
let Some(org) = Organization::find_by_uuid(&identifier.into(), &conn).await else {
|
let org = match Organization::find_by_uuid(&identifier.into(), &conn).await {
|
||||||
err!("Failed to retrieve the associated organization")
|
None => err!("Failed to retrieve the associated organization"),
|
||||||
|
Some(org) => org,
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(membership) = Membership::find_by_user_and_org(&user.uuid, &org.uuid, &conn).await else {
|
let membership = match Membership::find_by_user_and_org(&user.uuid, &org.uuid, &conn).await {
|
||||||
err!("Failed to retrieve the invitation")
|
None => err!("Failed to retrieve the invitation"),
|
||||||
|
Some(org) => org,
|
||||||
};
|
};
|
||||||
|
|
||||||
accept_org_invite(&user, membership, None, &conn).await?;
|
accept_org_invite(&user, membership, None, &conn).await?;
|
||||||
|
|
@ -515,8 +522,8 @@ async fn post_password(data: Json<ChangePassData>, headers: Headers, conn: DbCon
|
||||||
err!("Invalid password")
|
err!("Invalid password")
|
||||||
}
|
}
|
||||||
|
|
||||||
user.password_hint = clean_password_hint(data.master_password_hint.as_ref());
|
user.password_hint = clean_password_hint(&data.master_password_hint);
|
||||||
enforce_password_hint_setting(user.password_hint.as_ref())?;
|
enforce_password_hint_setting(&user.password_hint)?;
|
||||||
|
|
||||||
log_user_event(EventType::UserChangedPassword as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &conn)
|
log_user_event(EventType::UserChangedPassword as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &conn)
|
||||||
.await;
|
.await;
|
||||||
|
|
@ -531,16 +538,14 @@ async fn post_password(data: Json<ChangePassData>, headers: Headers, conn: DbCon
|
||||||
String::from("get_public_keys"),
|
String::from("get_public_keys"),
|
||||||
String::from("get_api_webauthn"),
|
String::from("get_api_webauthn"),
|
||||||
]),
|
]),
|
||||||
&conn,
|
);
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let save_result = user.save(&conn).await;
|
let save_result = user.save(&conn).await;
|
||||||
|
|
||||||
// Prevent logging out the client where the user requested this endpoint from.
|
// Prevent logging out the client where the user requested this endpoint from.
|
||||||
// If you do logout the user it will causes issues at the client side.
|
// If you do logout the user it will causes issues at the client side.
|
||||||
// Adding the device uuid will prevent this.
|
// Adding the device uuid will prevent this.
|
||||||
nt.send_logout(&user, Some(&headers.device), &conn).await;
|
nt.send_logout(&user, Some(headers.device.uuid.clone()), &conn).await;
|
||||||
|
|
||||||
save_result
|
save_result
|
||||||
}
|
}
|
||||||
|
|
@ -580,6 +585,7 @@ fn set_kdf_data(user: &mut User, data: &KDFData) -> EmptyResult {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct AuthenticationData {
|
struct AuthenticationData {
|
||||||
|
|
@ -588,6 +594,7 @@ struct AuthenticationData {
|
||||||
master_password_authentication_hash: String,
|
master_password_authentication_hash: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct UnlockData {
|
struct UnlockData {
|
||||||
|
|
@ -596,12 +603,11 @@ struct UnlockData {
|
||||||
master_key_wrapped_user_key: String,
|
master_key_wrapped_user_key: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct ChangeKdfData {
|
struct ChangeKdfData {
|
||||||
#[allow(dead_code)]
|
|
||||||
new_master_password_hash: String,
|
new_master_password_hash: String,
|
||||||
#[allow(dead_code)]
|
|
||||||
key: String,
|
key: String,
|
||||||
authentication_data: AuthenticationData,
|
authentication_data: AuthenticationData,
|
||||||
unlock_data: UnlockData,
|
unlock_data: UnlockData,
|
||||||
|
|
@ -633,12 +639,10 @@ async fn post_kdf(data: Json<ChangeKdfData>, headers: Headers, conn: DbConn, nt:
|
||||||
Some(data.unlock_data.master_key_wrapped_user_key),
|
Some(data.unlock_data.master_key_wrapped_user_key),
|
||||||
true,
|
true,
|
||||||
None,
|
None,
|
||||||
&conn,
|
);
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
let save_result = user.save(&conn).await;
|
let save_result = user.save(&conn).await;
|
||||||
|
|
||||||
nt.send_logout(&user, Some(&headers.device), &conn).await;
|
nt.send_logout(&user, Some(headers.device.uuid.clone()), &conn).await;
|
||||||
|
|
||||||
save_result
|
save_result
|
||||||
}
|
}
|
||||||
|
|
@ -649,7 +653,6 @@ struct UpdateFolderData {
|
||||||
// There is a bug in 2024.3.x which adds a `null` item.
|
// There is a bug in 2024.3.x which adds a `null` item.
|
||||||
// To bypass this we allow a Option here, but skip it during the updates
|
// To bypass this we allow a Option here, but skip it during the updates
|
||||||
// See: https://github.com/bitwarden/clients/issues/8453
|
// See: https://github.com/bitwarden/clients/issues/8453
|
||||||
#[serde(default, deserialize_with = "deser_opt_nonempty_str")]
|
|
||||||
id: Option<FolderId>,
|
id: Option<FolderId>,
|
||||||
name: String,
|
name: String,
|
||||||
}
|
}
|
||||||
|
|
@ -903,16 +906,14 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, conn: DbConn, nt:
|
||||||
Some(data.account_unlock_data.master_password_unlock_data.master_key_encrypted_user_key),
|
Some(data.account_unlock_data.master_password_unlock_data.master_key_encrypted_user_key),
|
||||||
true,
|
true,
|
||||||
None,
|
None,
|
||||||
&conn,
|
);
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let save_result = user.save(&conn).await;
|
let save_result = user.save(&conn).await;
|
||||||
|
|
||||||
// Prevent logging out the client where the user requested this endpoint from.
|
// Prevent logging out the client where the user requested this endpoint from.
|
||||||
// If you do logout the user it will causes issues at the client side.
|
// If you do logout the user it will causes issues at the client side.
|
||||||
// Adding the device uuid will prevent this.
|
// Adding the device uuid will prevent this.
|
||||||
nt.send_logout(&user, Some(&headers.device), &conn).await;
|
nt.send_logout(&user, Some(headers.device.uuid.clone()), &conn).await;
|
||||||
|
|
||||||
save_result
|
save_result
|
||||||
}
|
}
|
||||||
|
|
@ -924,13 +925,12 @@ async fn post_sstamp(data: Json<PasswordOrOtpData>, headers: Headers, conn: DbCo
|
||||||
|
|
||||||
data.validate(&user, true, &conn).await?;
|
data.validate(&user, true, &conn).await?;
|
||||||
|
|
||||||
user.reset_security_stamp(&conn).await?;
|
Device::delete_all_by_user(&user.uuid, &conn).await?;
|
||||||
|
user.reset_security_stamp();
|
||||||
let save_result = user.save(&conn).await;
|
let save_result = user.save(&conn).await;
|
||||||
|
|
||||||
nt.send_logout(&user, None, &conn).await;
|
nt.send_logout(&user, None, &conn).await;
|
||||||
|
|
||||||
Device::delete_all_by_user(&user.uuid, &conn).await?;
|
|
||||||
|
|
||||||
save_result
|
save_result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1048,7 +1048,7 @@ async fn post_email(data: Json<ChangeEmailData>, headers: Headers, conn: DbConn,
|
||||||
user.email_new = None;
|
user.email_new = None;
|
||||||
user.email_new_token = None;
|
user.email_new_token = None;
|
||||||
|
|
||||||
user.set_password(&data.new_master_password_hash, Some(data.key), true, None, &conn).await?;
|
user.set_password(&data.new_master_password_hash, Some(data.key), true, None);
|
||||||
|
|
||||||
let save_result = user.save(&conn).await;
|
let save_result = user.save(&conn).await;
|
||||||
|
|
||||||
|
|
@ -1199,9 +1199,10 @@ async fn password_hint(data: Json<PasswordHintData>, conn: DbConn) -> EmptyResul
|
||||||
// There is still a timing side channel here in that the code
|
// There is still a timing side channel here in that the code
|
||||||
// paths that send mail take noticeably longer than ones that
|
// paths that send mail take noticeably longer than ones that
|
||||||
// don't. Add a randomized sleep to mitigate this somewhat.
|
// don't. Add a randomized sleep to mitigate this somewhat.
|
||||||
use rand::{rngs::SmallRng, RngExt};
|
use rand::{rngs::SmallRng, Rng, SeedableRng};
|
||||||
let mut rng: SmallRng = rand::make_rng();
|
let mut rng = SmallRng::from_os_rng();
|
||||||
let sleep_ms = rng.random_range(900..=1100) as u64;
|
let delta: i32 = 100;
|
||||||
|
let sleep_ms = (1_000 + rng.random_range(-delta..=delta)) as u64;
|
||||||
tokio::time::sleep(tokio::time::Duration::from_millis(sleep_ms)).await;
|
tokio::time::sleep(tokio::time::Duration::from_millis(sleep_ms)).await;
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1260,7 +1261,7 @@ struct SecretVerificationRequest {
|
||||||
pub async fn kdf_upgrade(user: &mut User, pwd_hash: &str, conn: &DbConn) -> ApiResult<()> {
|
pub async fn kdf_upgrade(user: &mut User, pwd_hash: &str, conn: &DbConn) -> ApiResult<()> {
|
||||||
if user.password_iterations < CONFIG.password_iterations() {
|
if user.password_iterations < CONFIG.password_iterations() {
|
||||||
user.password_iterations = CONFIG.password_iterations();
|
user.password_iterations = CONFIG.password_iterations();
|
||||||
user.set_password(pwd_hash, None, false, None, conn).await?;
|
user.set_password(pwd_hash, None, false, None);
|
||||||
|
|
||||||
if let Err(e) = user.save(conn).await {
|
if let Err(e) = user.save(conn).await {
|
||||||
error!("Error updating user: {e:#?}");
|
error!("Error updating user: {e:#?}");
|
||||||
|
|
@ -1334,11 +1335,6 @@ impl<'r> FromRequest<'r> for KnownDevice {
|
||||||
|
|
||||||
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||||
let email = if let Some(email_b64) = req.headers().get_one("X-Request-Email") {
|
let email = if let Some(email_b64) = req.headers().get_one("X-Request-Email") {
|
||||||
// Bitwarden seems to send padded Base64 strings since 2026.2.1
|
|
||||||
// Since these values are not streamed and Headers are always split by newlines
|
|
||||||
// we can safely ignore padding here and remove any '=' appended.
|
|
||||||
let email_b64 = email_b64.trim_end_matches('=');
|
|
||||||
|
|
||||||
let Ok(email_bytes) = data_encoding::BASE64URL_NOPAD.decode(email_b64.as_bytes()) else {
|
let Ok(email_bytes) = data_encoding::BASE64URL_NOPAD.decode(email_b64.as_bytes()) else {
|
||||||
return Outcome::Error((Status::BadRequest, "X-Request-Email value failed to decode as base64url"));
|
return Outcome::Error((Status::BadRequest, "X-Request-Email value failed to decode as base64url"));
|
||||||
};
|
};
|
||||||
|
|
@ -1438,7 +1434,7 @@ async fn put_clear_device_token(device_id: DeviceId, conn: DbConn) -> EmptyResul
|
||||||
|
|
||||||
if let Some(device) = Device::find_by_uuid(&device_id, &conn).await {
|
if let Some(device) = Device::find_by_uuid(&device_id, &conn).await {
|
||||||
Device::clear_push_token_by_uuid(&device_id, &conn).await?;
|
Device::clear_push_token_by_uuid(&device_id, &conn).await?;
|
||||||
unregister_push_device(device.push_uuid.as_ref()).await?;
|
unregister_push_device(&device.push_uuid).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -1708,6 +1704,6 @@ pub async fn purge_auth_requests(pool: DbPool) {
|
||||||
if let Ok(conn) = pool.get().await {
|
if let Ok(conn) = pool.get().await {
|
||||||
AuthRequest::purge_expired_auth_requests(&conn).await;
|
AuthRequest::purge_expired_auth_requests(&conn).await;
|
||||||
} else {
|
} else {
|
||||||
error!("Failed to get DB connection while purging auth requests")
|
error!("Failed to get DB connection while purging trashed ciphers")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,17 +11,17 @@ use rocket::{
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::auth::ClientVersion;
|
use crate::auth::ClientVersion;
|
||||||
use crate::util::{deser_opt_nonempty_str, save_temp_file, NumberOrString};
|
use crate::util::{save_temp_file, NumberOrString};
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{self, core::log_event, EmptyResult, JsonResult, Notify, PasswordOrOtpData, UpdateType},
|
api::{self, core::log_event, EmptyResult, JsonResult, Notify, PasswordOrOtpData, UpdateType},
|
||||||
auth::{Headers, OrgIdGuard, OwnerHeaders},
|
auth::Headers,
|
||||||
config::PathType,
|
config::PathType,
|
||||||
crypto,
|
crypto,
|
||||||
db::{
|
db::{
|
||||||
models::{
|
models::{
|
||||||
Archive, Attachment, AttachmentId, Cipher, CipherId, Collection, CollectionCipher, CollectionGroup,
|
Attachment, AttachmentId, Cipher, CipherId, Collection, CollectionCipher, CollectionGroup, CollectionId,
|
||||||
CollectionId, CollectionUser, EventType, Favorite, Folder, FolderCipher, FolderId, Group, Membership,
|
CollectionUser, EventType, Favorite, Folder, FolderCipher, FolderId, Group, Membership, MembershipType,
|
||||||
MembershipType, OrgPolicy, OrgPolicyType, OrganizationId, RepromptType, Send, UserId,
|
OrgPolicy, OrgPolicyType, OrganizationId, RepromptType, Send, UserId,
|
||||||
},
|
},
|
||||||
DbConn, DbPool,
|
DbConn, DbPool,
|
||||||
},
|
},
|
||||||
|
|
@ -86,8 +86,7 @@ pub fn routes() -> Vec<Route> {
|
||||||
restore_cipher_put_admin,
|
restore_cipher_put_admin,
|
||||||
restore_cipher_selected,
|
restore_cipher_selected,
|
||||||
restore_cipher_selected_admin,
|
restore_cipher_selected_admin,
|
||||||
purge_org_vault,
|
delete_all,
|
||||||
purge_personal_vault,
|
|
||||||
move_cipher_selected,
|
move_cipher_selected,
|
||||||
move_cipher_selected_put,
|
move_cipher_selected_put,
|
||||||
put_collections2_update,
|
put_collections2_update,
|
||||||
|
|
@ -96,10 +95,6 @@ pub fn routes() -> Vec<Route> {
|
||||||
post_collections_update,
|
post_collections_update,
|
||||||
post_collections_admin,
|
post_collections_admin,
|
||||||
put_collections_admin,
|
put_collections_admin,
|
||||||
archive_cipher_put,
|
|
||||||
archive_cipher_selected,
|
|
||||||
unarchive_cipher_put,
|
|
||||||
unarchive_cipher_selected,
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -252,7 +247,6 @@ pub struct CipherData {
|
||||||
// Id is optional as it is included only in bulk share
|
// Id is optional as it is included only in bulk share
|
||||||
pub id: Option<CipherId>,
|
pub id: Option<CipherId>,
|
||||||
// Folder id is not included in import
|
// Folder id is not included in import
|
||||||
#[serde(default, deserialize_with = "deser_opt_nonempty_str")]
|
|
||||||
pub folder_id: Option<FolderId>,
|
pub folder_id: Option<FolderId>,
|
||||||
// TODO: Some of these might appear all the time, no need for Option
|
// TODO: Some of these might appear all the time, no need for Option
|
||||||
#[serde(alias = "organizationID")]
|
#[serde(alias = "organizationID")]
|
||||||
|
|
@ -297,13 +291,11 @@ pub struct CipherData {
|
||||||
// when using older client versions, or if the operation doesn't involve
|
// when using older client versions, or if the operation doesn't involve
|
||||||
// updating an existing cipher.
|
// updating an existing cipher.
|
||||||
last_known_revision_date: Option<String>,
|
last_known_revision_date: Option<String>,
|
||||||
archived_date: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct PartialCipherData {
|
pub struct PartialCipherData {
|
||||||
#[serde(default, deserialize_with = "deser_opt_nonempty_str")]
|
|
||||||
folder_id: Option<FolderId>,
|
folder_id: Option<FolderId>,
|
||||||
favorite: bool,
|
favorite: bool,
|
||||||
}
|
}
|
||||||
|
|
@ -433,7 +425,7 @@ pub async fn update_cipher_from_data(
|
||||||
let transfer_cipher = cipher.organization_uuid.is_none() && data.organization_id.is_some();
|
let transfer_cipher = cipher.organization_uuid.is_none() && data.organization_id.is_some();
|
||||||
|
|
||||||
if let Some(org_id) = data.organization_id {
|
if let Some(org_id) = data.organization_id {
|
||||||
match Membership::find_confirmed_by_user_and_org(&headers.user.uuid, &org_id, conn).await {
|
match Membership::find_by_user_and_org(&headers.user.uuid, &org_id, conn).await {
|
||||||
None => err!("You don't have permission to add item to organization"),
|
None => err!("You don't have permission to add item to organization"),
|
||||||
Some(member) => {
|
Some(member) => {
|
||||||
if shared_to_collections.is_some()
|
if shared_to_collections.is_some()
|
||||||
|
|
@ -539,13 +531,6 @@ pub async fn update_cipher_from_data(
|
||||||
cipher.move_to_folder(data.folder_id, &headers.user.uuid, conn).await?;
|
cipher.move_to_folder(data.folder_id, &headers.user.uuid, conn).await?;
|
||||||
cipher.set_favorite(data.favorite, &headers.user.uuid, conn).await?;
|
cipher.set_favorite(data.favorite, &headers.user.uuid, conn).await?;
|
||||||
|
|
||||||
if let Some(dt_str) = data.archived_date {
|
|
||||||
match NaiveDateTime::parse_from_str(&dt_str, "%+") {
|
|
||||||
Ok(dt) => cipher.set_archived_at(dt, &headers.user.uuid, conn).await?,
|
|
||||||
Err(err) => warn!("Error parsing ArchivedDate '{dt_str}': {err}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ut != UpdateType::None {
|
if ut != UpdateType::None {
|
||||||
// Only log events for organizational ciphers
|
// Only log events for organizational ciphers
|
||||||
if let Some(org_id) = &cipher.organization_uuid {
|
if let Some(org_id) = &cipher.organization_uuid {
|
||||||
|
|
@ -642,7 +627,7 @@ async fn post_ciphers_import(data: Json<ImportData>, headers: Headers, conn: DbC
|
||||||
|
|
||||||
let mut user = headers.user;
|
let mut user = headers.user;
|
||||||
user.update_revision(&conn).await?;
|
user.update_revision(&conn).await?;
|
||||||
nt.send_user_update(UpdateType::SyncVault, &user, headers.device.push_uuid.as_ref(), &conn).await;
|
nt.send_user_update(UpdateType::SyncVault, &user, &headers.device.push_uuid, &conn).await;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -730,13 +715,9 @@ async fn put_cipher_partial(
|
||||||
let data: PartialCipherData = data.into_inner();
|
let data: PartialCipherData = data.into_inner();
|
||||||
|
|
||||||
let Some(cipher) = Cipher::find_by_uuid(&cipher_id, &conn).await else {
|
let Some(cipher) = Cipher::find_by_uuid(&cipher_id, &conn).await else {
|
||||||
err!("Cipher does not exist")
|
err!("Cipher doesn't exist")
|
||||||
};
|
};
|
||||||
|
|
||||||
if !cipher.is_accessible_to_user(&headers.user.uuid, &conn).await {
|
|
||||||
err!("Cipher does not exist", "Cipher is not accessible for the current user")
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(ref folder_id) = data.folder_id {
|
if let Some(ref folder_id) = data.folder_id {
|
||||||
if Folder::find_by_uuid_and_user(folder_id, &headers.user.uuid, &conn).await.is_none() {
|
if Folder::find_by_uuid_and_user(folder_id, &headers.user.uuid, &conn).await.is_none() {
|
||||||
err!("Invalid folder", "Folder does not exist or belongs to another user");
|
err!("Invalid folder", "Folder does not exist or belongs to another user");
|
||||||
|
|
@ -814,16 +795,12 @@ async fn post_collections_update(
|
||||||
err!("Collection cannot be changed")
|
err!("Collection cannot be changed")
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(ref org_uuid) = cipher.organization_uuid else {
|
|
||||||
err!("Cipher is not owned by an organization")
|
|
||||||
};
|
|
||||||
|
|
||||||
let posted_collections = HashSet::<CollectionId>::from_iter(data.collection_ids);
|
let posted_collections = HashSet::<CollectionId>::from_iter(data.collection_ids);
|
||||||
let current_collections =
|
let current_collections =
|
||||||
HashSet::<CollectionId>::from_iter(cipher.get_collections(headers.user.uuid.clone(), &conn).await);
|
HashSet::<CollectionId>::from_iter(cipher.get_collections(headers.user.uuid.clone(), &conn).await);
|
||||||
|
|
||||||
for collection in posted_collections.symmetric_difference(¤t_collections) {
|
for collection in posted_collections.symmetric_difference(¤t_collections) {
|
||||||
match Collection::find_by_uuid_and_org(collection, org_uuid, &conn).await {
|
match Collection::find_by_uuid_and_org(collection, cipher.organization_uuid.as_ref().unwrap(), &conn).await {
|
||||||
None => err!("Invalid collection ID provided"),
|
None => err!("Invalid collection ID provided"),
|
||||||
Some(collection) => {
|
Some(collection) => {
|
||||||
if collection.is_writable_by_user(&headers.user.uuid, &conn).await {
|
if collection.is_writable_by_user(&headers.user.uuid, &conn).await {
|
||||||
|
|
@ -854,7 +831,7 @@ async fn post_collections_update(
|
||||||
log_event(
|
log_event(
|
||||||
EventType::CipherUpdatedCollections as i32,
|
EventType::CipherUpdatedCollections as i32,
|
||||||
&cipher.uuid,
|
&cipher.uuid,
|
||||||
org_uuid,
|
&cipher.organization_uuid.clone().unwrap(),
|
||||||
&headers.user.uuid,
|
&headers.user.uuid,
|
||||||
headers.device.atype,
|
headers.device.atype,
|
||||||
&headers.ip.ip,
|
&headers.ip.ip,
|
||||||
|
|
@ -894,16 +871,12 @@ async fn post_collections_admin(
|
||||||
err!("Collection cannot be changed")
|
err!("Collection cannot be changed")
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(ref org_uuid) = cipher.organization_uuid else {
|
|
||||||
err!("Cipher is not owned by an organization")
|
|
||||||
};
|
|
||||||
|
|
||||||
let posted_collections = HashSet::<CollectionId>::from_iter(data.collection_ids);
|
let posted_collections = HashSet::<CollectionId>::from_iter(data.collection_ids);
|
||||||
let current_collections =
|
let current_collections =
|
||||||
HashSet::<CollectionId>::from_iter(cipher.get_admin_collections(headers.user.uuid.clone(), &conn).await);
|
HashSet::<CollectionId>::from_iter(cipher.get_admin_collections(headers.user.uuid.clone(), &conn).await);
|
||||||
|
|
||||||
for collection in posted_collections.symmetric_difference(¤t_collections) {
|
for collection in posted_collections.symmetric_difference(¤t_collections) {
|
||||||
match Collection::find_by_uuid_and_org(collection, org_uuid, &conn).await {
|
match Collection::find_by_uuid_and_org(collection, cipher.organization_uuid.as_ref().unwrap(), &conn).await {
|
||||||
None => err!("Invalid collection ID provided"),
|
None => err!("Invalid collection ID provided"),
|
||||||
Some(collection) => {
|
Some(collection) => {
|
||||||
if collection.is_writable_by_user(&headers.user.uuid, &conn).await {
|
if collection.is_writable_by_user(&headers.user.uuid, &conn).await {
|
||||||
|
|
@ -934,7 +907,7 @@ async fn post_collections_admin(
|
||||||
log_event(
|
log_event(
|
||||||
EventType::CipherUpdatedCollections as i32,
|
EventType::CipherUpdatedCollections as i32,
|
||||||
&cipher.uuid,
|
&cipher.uuid,
|
||||||
org_uuid,
|
&cipher.organization_uuid.unwrap(),
|
||||||
&headers.user.uuid,
|
&headers.user.uuid,
|
||||||
headers.device.atype,
|
headers.device.atype,
|
||||||
&headers.ip.ip,
|
&headers.ip.ip,
|
||||||
|
|
@ -1025,7 +998,7 @@ async fn put_cipher_share_selected(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Multi share actions do not send out a push for each cipher, we need to send a general sync here
|
// Multi share actions do not send out a push for each cipher, we need to send a general sync here
|
||||||
nt.send_user_update(UpdateType::SyncCiphers, &headers.user, headers.device.push_uuid.as_ref(), &conn).await;
|
nt.send_user_update(UpdateType::SyncCiphers, &headers.user, &headers.device.push_uuid, &conn).await;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -1591,7 +1564,6 @@ async fn restore_cipher_selected(
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct MoveCipherData {
|
struct MoveCipherData {
|
||||||
#[serde(default, deserialize_with = "deser_opt_nonempty_str")]
|
|
||||||
folder_id: Option<FolderId>,
|
folder_id: Option<FolderId>,
|
||||||
ids: Vec<CipherId>,
|
ids: Vec<CipherId>,
|
||||||
}
|
}
|
||||||
|
|
@ -1638,7 +1610,7 @@ async fn move_cipher_selected(
|
||||||
.await;
|
.await;
|
||||||
} else {
|
} else {
|
||||||
// Multi move actions do not send out a push for each cipher, we need to send a general sync here
|
// Multi move actions do not send out a push for each cipher, we need to send a general sync here
|
||||||
nt.send_user_update(UpdateType::SyncCiphers, &headers.user, headers.device.push_uuid.as_ref(), &conn).await;
|
nt.send_user_update(UpdateType::SyncCiphers, &headers.user, &headers.device.push_uuid, &conn).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
if cipher_count != accessible_ciphers_count {
|
if cipher_count != accessible_ciphers_count {
|
||||||
|
|
@ -1666,51 +1638,9 @@ struct OrganizationIdData {
|
||||||
org_id: OrganizationId,
|
org_id: OrganizationId,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the OrgIdGuard here, to ensure there an organization id present.
|
|
||||||
// If there is no organization id present, it should be forwarded to purge_personal_vault.
|
|
||||||
// This guard needs to be the first argument, else OwnerHeaders will be triggered which will logout the user.
|
|
||||||
#[post("/ciphers/purge?<organization..>", data = "<data>")]
|
#[post("/ciphers/purge?<organization..>", data = "<data>")]
|
||||||
async fn purge_org_vault(
|
async fn delete_all(
|
||||||
_org_id_guard: OrgIdGuard,
|
organization: Option<OrganizationIdData>,
|
||||||
organization: OrganizationIdData,
|
|
||||||
data: Json<PasswordOrOtpData>,
|
|
||||||
headers: OwnerHeaders,
|
|
||||||
conn: DbConn,
|
|
||||||
nt: Notify<'_>,
|
|
||||||
) -> EmptyResult {
|
|
||||||
if organization.org_id != headers.org_id {
|
|
||||||
err!("Organization not found", "Organization id's do not match");
|
|
||||||
}
|
|
||||||
|
|
||||||
let data: PasswordOrOtpData = data.into_inner();
|
|
||||||
let user = headers.user;
|
|
||||||
|
|
||||||
data.validate(&user, true, &conn).await?;
|
|
||||||
|
|
||||||
match Membership::find_confirmed_by_user_and_org(&user.uuid, &organization.org_id, &conn).await {
|
|
||||||
Some(member) if member.atype == MembershipType::Owner => {
|
|
||||||
Cipher::delete_all_by_organization(&organization.org_id, &conn).await?;
|
|
||||||
nt.send_user_update(UpdateType::SyncVault, &user, headers.device.push_uuid.as_ref(), &conn).await;
|
|
||||||
|
|
||||||
log_event(
|
|
||||||
EventType::OrganizationPurgedVault as i32,
|
|
||||||
&organization.org_id,
|
|
||||||
&organization.org_id,
|
|
||||||
&user.uuid,
|
|
||||||
headers.device.atype,
|
|
||||||
&headers.ip.ip,
|
|
||||||
&conn,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
_ => err!("You don't have permission to purge the organization vault"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/ciphers/purge", data = "<data>")]
|
|
||||||
async fn purge_personal_vault(
|
|
||||||
data: Json<PasswordOrOtpData>,
|
data: Json<PasswordOrOtpData>,
|
||||||
headers: Headers,
|
headers: Headers,
|
||||||
conn: DbConn,
|
conn: DbConn,
|
||||||
|
|
@ -1721,48 +1651,52 @@ async fn purge_personal_vault(
|
||||||
|
|
||||||
data.validate(&user, true, &conn).await?;
|
data.validate(&user, true, &conn).await?;
|
||||||
|
|
||||||
for cipher in Cipher::find_owned_by_user(&user.uuid, &conn).await {
|
match organization {
|
||||||
cipher.delete(&conn).await?;
|
Some(org_data) => {
|
||||||
|
// Organization ID in query params, purging organization vault
|
||||||
|
match Membership::find_by_user_and_org(&user.uuid, &org_data.org_id, &conn).await {
|
||||||
|
None => err!("You don't have permission to purge the organization vault"),
|
||||||
|
Some(member) => {
|
||||||
|
if member.atype == MembershipType::Owner {
|
||||||
|
Cipher::delete_all_by_organization(&org_data.org_id, &conn).await?;
|
||||||
|
nt.send_user_update(UpdateType::SyncVault, &user, &headers.device.push_uuid, &conn).await;
|
||||||
|
|
||||||
|
log_event(
|
||||||
|
EventType::OrganizationPurgedVault as i32,
|
||||||
|
&org_data.org_id,
|
||||||
|
&org_data.org_id,
|
||||||
|
&user.uuid,
|
||||||
|
headers.device.atype,
|
||||||
|
&headers.ip.ip,
|
||||||
|
&conn,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
err!("You don't have permission to purge the organization vault");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// No organization ID in query params, purging user vault
|
||||||
|
// Delete ciphers and their attachments
|
||||||
|
for cipher in Cipher::find_owned_by_user(&user.uuid, &conn).await {
|
||||||
|
cipher.delete(&conn).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete folders
|
||||||
|
for f in Folder::find_by_user(&user.uuid, &conn).await {
|
||||||
|
f.delete(&conn).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
user.update_revision(&conn).await?;
|
||||||
|
nt.send_user_update(UpdateType::SyncVault, &user, &headers.device.push_uuid, &conn).await;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for f in Folder::find_by_user(&user.uuid, &conn).await {
|
|
||||||
f.delete(&conn).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
user.update_revision(&conn).await?;
|
|
||||||
nt.send_user_update(UpdateType::SyncVault, &user, headers.device.push_uuid.as_ref(), &conn).await;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[put("/ciphers/<cipher_id>/archive")]
|
|
||||||
async fn archive_cipher_put(cipher_id: CipherId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult {
|
|
||||||
archive_cipher(&cipher_id, &headers, false, &conn, &nt).await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[put("/ciphers/archive", data = "<data>")]
|
|
||||||
async fn archive_cipher_selected(
|
|
||||||
data: Json<CipherIdsData>,
|
|
||||||
headers: Headers,
|
|
||||||
conn: DbConn,
|
|
||||||
nt: Notify<'_>,
|
|
||||||
) -> JsonResult {
|
|
||||||
archive_multiple_ciphers(data, &headers, &conn, &nt).await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[put("/ciphers/<cipher_id>/unarchive")]
|
|
||||||
async fn unarchive_cipher_put(cipher_id: CipherId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult {
|
|
||||||
unarchive_cipher(&cipher_id, &headers, false, &conn, &nt).await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[put("/ciphers/unarchive", data = "<data>")]
|
|
||||||
async fn unarchive_cipher_selected(
|
|
||||||
data: Json<CipherIdsData>,
|
|
||||||
headers: Headers,
|
|
||||||
conn: DbConn,
|
|
||||||
nt: Notify<'_>,
|
|
||||||
) -> JsonResult {
|
|
||||||
unarchive_multiple_ciphers(data, &headers, &conn, &nt).await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
|
|
@ -1855,7 +1789,7 @@ async fn _delete_multiple_ciphers(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Multi delete actions do not send out a push for each cipher, we need to send a general sync here
|
// Multi delete actions do not send out a push for each cipher, we need to send a general sync here
|
||||||
nt.send_user_update(UpdateType::SyncCiphers, &headers.user, headers.device.push_uuid.as_ref(), &conn).await;
|
nt.send_user_update(UpdateType::SyncCiphers, &headers.user, &headers.device.push_uuid, &conn).await;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -1923,7 +1857,7 @@ async fn _restore_multiple_ciphers(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Multi move actions do not send out a push for each cipher, we need to send a general sync here
|
// Multi move actions do not send out a push for each cipher, we need to send a general sync here
|
||||||
nt.send_user_update(UpdateType::SyncCiphers, &headers.user, headers.device.push_uuid.as_ref(), conn).await;
|
nt.send_user_update(UpdateType::SyncCiphers, &headers.user, &headers.device.push_uuid, conn).await;
|
||||||
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"data": ciphers,
|
"data": ciphers,
|
||||||
|
|
@ -1983,122 +1917,6 @@ async fn _delete_cipher_attachment_by_id(
|
||||||
Ok(Json(json!({"cipher":cipher_json})))
|
Ok(Json(json!({"cipher":cipher_json})))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn archive_cipher(
|
|
||||||
cipher_id: &CipherId,
|
|
||||||
headers: &Headers,
|
|
||||||
multi_archive: bool,
|
|
||||||
conn: &DbConn,
|
|
||||||
nt: &Notify<'_>,
|
|
||||||
) -> JsonResult {
|
|
||||||
let Some(cipher) = Cipher::find_by_uuid(cipher_id, conn).await else {
|
|
||||||
err!("Cipher doesn't exist")
|
|
||||||
};
|
|
||||||
|
|
||||||
if !cipher.is_accessible_to_user(&headers.user.uuid, conn).await {
|
|
||||||
err!("Cipher is not accessible for the current user")
|
|
||||||
}
|
|
||||||
|
|
||||||
cipher.set_archived_at(Utc::now().naive_utc(), &headers.user.uuid, conn).await?;
|
|
||||||
|
|
||||||
if !multi_archive {
|
|
||||||
nt.send_cipher_update(
|
|
||||||
UpdateType::SyncCipherUpdate,
|
|
||||||
&cipher,
|
|
||||||
&cipher.update_users_revision(conn).await,
|
|
||||||
&headers.device,
|
|
||||||
None,
|
|
||||||
conn,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, conn).await?))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn unarchive_cipher(
|
|
||||||
cipher_id: &CipherId,
|
|
||||||
headers: &Headers,
|
|
||||||
multi_unarchive: bool,
|
|
||||||
conn: &DbConn,
|
|
||||||
nt: &Notify<'_>,
|
|
||||||
) -> JsonResult {
|
|
||||||
let Some(cipher) = Cipher::find_by_uuid(cipher_id, conn).await else {
|
|
||||||
err!("Cipher doesn't exist")
|
|
||||||
};
|
|
||||||
|
|
||||||
if !cipher.is_accessible_to_user(&headers.user.uuid, conn).await {
|
|
||||||
err!("Cipher is not accessible for the current user")
|
|
||||||
}
|
|
||||||
|
|
||||||
cipher.unarchive(&headers.user.uuid, conn).await?;
|
|
||||||
|
|
||||||
if !multi_unarchive {
|
|
||||||
nt.send_cipher_update(
|
|
||||||
UpdateType::SyncCipherUpdate,
|
|
||||||
&cipher,
|
|
||||||
&cipher.update_users_revision(conn).await,
|
|
||||||
&headers.device,
|
|
||||||
None,
|
|
||||||
conn,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, conn).await?))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn archive_multiple_ciphers(
|
|
||||||
data: Json<CipherIdsData>,
|
|
||||||
headers: &Headers,
|
|
||||||
conn: &DbConn,
|
|
||||||
nt: &Notify<'_>,
|
|
||||||
) -> JsonResult {
|
|
||||||
let data = data.into_inner();
|
|
||||||
|
|
||||||
let mut ciphers: Vec<Value> = Vec::new();
|
|
||||||
for cipher_id in data.ids {
|
|
||||||
match archive_cipher(&cipher_id, headers, true, conn, nt).await {
|
|
||||||
Ok(json) => ciphers.push(json.into_inner()),
|
|
||||||
err => return err,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Multi archive does not send out a push for each cipher, we need to send a general sync here
|
|
||||||
nt.send_user_update(UpdateType::SyncCiphers, &headers.user, headers.device.push_uuid.as_ref(), conn).await;
|
|
||||||
|
|
||||||
Ok(Json(json!({
|
|
||||||
"data": ciphers,
|
|
||||||
"object": "list",
|
|
||||||
"continuationToken": null
|
|
||||||
})))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn unarchive_multiple_ciphers(
|
|
||||||
data: Json<CipherIdsData>,
|
|
||||||
headers: &Headers,
|
|
||||||
conn: &DbConn,
|
|
||||||
nt: &Notify<'_>,
|
|
||||||
) -> JsonResult {
|
|
||||||
let data = data.into_inner();
|
|
||||||
|
|
||||||
let mut ciphers: Vec<Value> = Vec::new();
|
|
||||||
for cipher_id in data.ids {
|
|
||||||
match unarchive_cipher(&cipher_id, headers, true, conn, nt).await {
|
|
||||||
Ok(json) => ciphers.push(json.into_inner()),
|
|
||||||
err => return err,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Multi unarchive does not send out a push for each cipher, we need to send a general sync here
|
|
||||||
nt.send_user_update(UpdateType::SyncCiphers, &headers.user, headers.device.push_uuid.as_ref(), conn).await;
|
|
||||||
|
|
||||||
Ok(Json(json!({
|
|
||||||
"data": ciphers,
|
|
||||||
"object": "list",
|
|
||||||
"continuationToken": null
|
|
||||||
})))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This will hold all the necessary data to improve a full sync of all the ciphers
|
/// This will hold all the necessary data to improve a full sync of all the ciphers
|
||||||
/// It can be used during the `Cipher::to_json()` call.
|
/// It can be used during the `Cipher::to_json()` call.
|
||||||
/// It will prevent the so called N+1 SQL issue by running just a few queries which will hold all the data needed.
|
/// It will prevent the so called N+1 SQL issue by running just a few queries which will hold all the data needed.
|
||||||
|
|
@ -2108,7 +1926,6 @@ pub struct CipherSyncData {
|
||||||
pub cipher_folders: HashMap<CipherId, FolderId>,
|
pub cipher_folders: HashMap<CipherId, FolderId>,
|
||||||
pub cipher_favorites: HashSet<CipherId>,
|
pub cipher_favorites: HashSet<CipherId>,
|
||||||
pub cipher_collections: HashMap<CipherId, Vec<CollectionId>>,
|
pub cipher_collections: HashMap<CipherId, Vec<CollectionId>>,
|
||||||
pub cipher_archives: HashMap<CipherId, NaiveDateTime>,
|
|
||||||
pub members: HashMap<OrganizationId, Membership>,
|
pub members: HashMap<OrganizationId, Membership>,
|
||||||
pub user_collections: HashMap<CollectionId, CollectionUser>,
|
pub user_collections: HashMap<CollectionId, CollectionUser>,
|
||||||
pub user_collections_groups: HashMap<CollectionId, CollectionGroup>,
|
pub user_collections_groups: HashMap<CollectionId, CollectionGroup>,
|
||||||
|
|
@ -2125,25 +1942,20 @@ impl CipherSyncData {
|
||||||
pub async fn new(user_id: &UserId, sync_type: CipherSyncType, conn: &DbConn) -> Self {
|
pub async fn new(user_id: &UserId, sync_type: CipherSyncType, conn: &DbConn) -> Self {
|
||||||
let cipher_folders: HashMap<CipherId, FolderId>;
|
let cipher_folders: HashMap<CipherId, FolderId>;
|
||||||
let cipher_favorites: HashSet<CipherId>;
|
let cipher_favorites: HashSet<CipherId>;
|
||||||
let cipher_archives: HashMap<CipherId, NaiveDateTime>;
|
|
||||||
match sync_type {
|
match sync_type {
|
||||||
// User Sync supports Folders, Favorites, and Archives
|
// User Sync supports Folders and Favorites
|
||||||
CipherSyncType::User => {
|
CipherSyncType::User => {
|
||||||
// Generate a HashMap with the Cipher UUID as key and the Folder UUID as value
|
// Generate a HashMap with the Cipher UUID as key and the Folder UUID as value
|
||||||
cipher_folders = FolderCipher::find_by_user(user_id, conn).await.into_iter().collect();
|
cipher_folders = FolderCipher::find_by_user(user_id, conn).await.into_iter().collect();
|
||||||
|
|
||||||
// Generate a HashSet of all the Cipher UUID's which are marked as favorite
|
// Generate a HashSet of all the Cipher UUID's which are marked as favorite
|
||||||
cipher_favorites = Favorite::get_all_cipher_uuid_by_user(user_id, conn).await.into_iter().collect();
|
cipher_favorites = Favorite::get_all_cipher_uuid_by_user(user_id, conn).await.into_iter().collect();
|
||||||
|
|
||||||
// Generate a HashMap with the Cipher UUID as key and the archived date time as value
|
|
||||||
cipher_archives = Archive::find_by_user(user_id, conn).await.into_iter().collect();
|
|
||||||
}
|
}
|
||||||
// Organization Sync does not support Folders, Favorites, or Archives.
|
// Organization Sync does not support Folders and Favorites.
|
||||||
// If these are set, it will cause issues in the web-vault.
|
// If these are set, it will cause issues in the web-vault.
|
||||||
CipherSyncType::Organization => {
|
CipherSyncType::Organization => {
|
||||||
cipher_folders = HashMap::with_capacity(0);
|
cipher_folders = HashMap::with_capacity(0);
|
||||||
cipher_favorites = HashSet::with_capacity(0);
|
cipher_favorites = HashSet::with_capacity(0);
|
||||||
cipher_archives = HashMap::with_capacity(0);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2164,11 +1976,8 @@ impl CipherSyncData {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate a HashMap with the Organization UUID as key and the Membership record
|
// Generate a HashMap with the Organization UUID as key and the Membership record
|
||||||
let members: HashMap<OrganizationId, Membership> = Membership::find_confirmed_by_user(user_id, conn)
|
let members: HashMap<OrganizationId, Membership> =
|
||||||
.await
|
Membership::find_by_user(user_id, conn).await.into_iter().map(|m| (m.org_uuid.clone(), m)).collect();
|
||||||
.into_iter()
|
|
||||||
.map(|m| (m.org_uuid.clone(), m))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Generate a HashMap with the User_Collections UUID as key and the CollectionUser record
|
// Generate a HashMap with the User_Collections UUID as key and the CollectionUser record
|
||||||
let user_collections: HashMap<CollectionId, CollectionUser> = CollectionUser::find_by_user(user_id, conn)
|
let user_collections: HashMap<CollectionId, CollectionUser> = CollectionUser::find_by_user(user_id, conn)
|
||||||
|
|
@ -2206,7 +2015,6 @@ impl CipherSyncData {
|
||||||
};
|
};
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
cipher_archives,
|
|
||||||
cipher_attachments,
|
cipher_attachments,
|
||||||
cipher_folders,
|
cipher_folders,
|
||||||
cipher_favorites,
|
cipher_favorites,
|
||||||
|
|
|
||||||
|
|
@ -653,7 +653,7 @@ async fn password_emergency_access(
|
||||||
};
|
};
|
||||||
|
|
||||||
// change grantor_user password
|
// change grantor_user password
|
||||||
grantor_user.set_password(new_master_password_hash, Some(data.key), true, None, &conn).await?;
|
grantor_user.set_password(new_master_password_hash, Some(data.key), true, None);
|
||||||
grantor_user.save(&conn).await?;
|
grantor_user.save(&conn).await?;
|
||||||
|
|
||||||
// Disable TwoFactor providers since they will otherwise block logins
|
// Disable TwoFactor providers since they will otherwise block logins
|
||||||
|
|
|
||||||
|
|
@ -240,7 +240,7 @@ async fn _log_user_event(
|
||||||
ip: &IpAddr,
|
ip: &IpAddr,
|
||||||
conn: &DbConn,
|
conn: &DbConn,
|
||||||
) {
|
) {
|
||||||
let memberships = Membership::find_confirmed_by_user(user_id, conn).await;
|
let memberships = Membership::find_by_user(user_id, conn).await;
|
||||||
let mut events: Vec<Event> = Vec::with_capacity(memberships.len() + 1); // We need an event per org and one without an org
|
let mut events: Vec<Event> = Vec::with_capacity(memberships.len() + 1); // We need an event per org and one without an org
|
||||||
|
|
||||||
// Upstream saves the event also without any org_id.
|
// Upstream saves the event also without any org_id.
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ use crate::{
|
||||||
models::{Folder, FolderId},
|
models::{Folder, FolderId},
|
||||||
DbConn,
|
DbConn,
|
||||||
},
|
},
|
||||||
util::deser_opt_nonempty_str,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn routes() -> Vec<rocket::Route> {
|
pub fn routes() -> Vec<rocket::Route> {
|
||||||
|
|
@ -39,7 +38,6 @@ async fn get_folder(folder_id: FolderId, headers: Headers, conn: DbConn) -> Json
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct FolderData {
|
pub struct FolderData {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
#[serde(default, deserialize_with = "deser_opt_nonempty_str")]
|
|
||||||
pub id: Option<FolderId>,
|
pub id: Option<FolderId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,8 +59,7 @@ use crate::{
|
||||||
error::Error,
|
error::Error,
|
||||||
http_client::make_http_request,
|
http_client::make_http_request,
|
||||||
mail,
|
mail,
|
||||||
util::{parse_experimental_client_feature_flags, FeatureFlagFilter},
|
util::parse_experimental_client_feature_flags,
|
||||||
CONFIG,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
|
@ -124,7 +123,7 @@ async fn post_eq_domains(data: Json<EquivDomainData>, headers: Headers, conn: Db
|
||||||
|
|
||||||
user.save(&conn).await?;
|
user.save(&conn).await?;
|
||||||
|
|
||||||
nt.send_user_update(UpdateType::SyncSettings, &user, headers.device.push_uuid.as_ref(), &conn).await;
|
nt.send_user_update(UpdateType::SyncSettings, &user, &headers.device.push_uuid, &conn).await;
|
||||||
|
|
||||||
Ok(Json(json!({})))
|
Ok(Json(json!({})))
|
||||||
}
|
}
|
||||||
|
|
@ -137,7 +136,7 @@ async fn put_eq_domains(data: Json<EquivDomainData>, headers: Headers, conn: DbC
|
||||||
#[get("/hibp/breach?<username>")]
|
#[get("/hibp/breach?<username>")]
|
||||||
async fn hibp_breach(username: &str, _headers: Headers) -> JsonResult {
|
async fn hibp_breach(username: &str, _headers: Headers) -> JsonResult {
|
||||||
let username: String = url::form_urlencoded::byte_serialize(username.as_bytes()).collect();
|
let username: String = url::form_urlencoded::byte_serialize(username.as_bytes()).collect();
|
||||||
if let Some(api_key) = CONFIG.hibp_api_key() {
|
if let Some(api_key) = crate::CONFIG.hibp_api_key() {
|
||||||
let url = format!(
|
let url = format!(
|
||||||
"https://haveibeenpwned.com/api/v3/breachedaccount/{username}?truncateResponse=false&includeUnverified=false"
|
"https://haveibeenpwned.com/api/v3/breachedaccount/{username}?truncateResponse=false&includeUnverified=false"
|
||||||
);
|
);
|
||||||
|
|
@ -198,17 +197,19 @@ fn get_api_webauthn(_headers: Headers) -> Json<Value> {
|
||||||
|
|
||||||
#[get("/config")]
|
#[get("/config")]
|
||||||
fn config() -> Json<Value> {
|
fn config() -> Json<Value> {
|
||||||
let domain = CONFIG.domain();
|
let domain = crate::CONFIG.domain();
|
||||||
// Official available feature flags can be found here:
|
// Official available feature flags can be found here:
|
||||||
// Server (v2026.2.1): https://github.com/bitwarden/server/blob/0e42725d0837bd1c0dabd864ff621a579959744b/src/Core/Constants.cs#L135
|
// Server (v2025.6.2): https://github.com/bitwarden/server/blob/d094be3267f2030bd0dc62106bc6871cf82682f5/src/Core/Constants.cs#L103
|
||||||
// Client (v2026.2.1): https://github.com/bitwarden/clients/blob/f96380c3138291a028bdd2c7a5fee540d5c98ba5/libs/common/src/enums/feature-flag.enum.ts#L12
|
// Client (web-v2025.6.1): https://github.com/bitwarden/clients/blob/747c2fd6a1c348a57a76e4a7de8128466ffd3c01/libs/common/src/enums/feature-flag.enum.ts#L12
|
||||||
// Android (v2026.2.1): https://github.com/bitwarden/android/blob/6902c19c0093fa476bbf74ccaa70c9f14afbb82f/core/src/main/kotlin/com/bitwarden/core/data/manager/model/FlagKey.kt#L31
|
// Android (v2025.6.0): https://github.com/bitwarden/android/blob/b5b022caaad33390c31b3021b2c1205925b0e1a2/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/model/FlagKey.kt#L22
|
||||||
// iOS (v2026.2.1): https://github.com/bitwarden/ios/blob/cdd9ba1770ca2ffc098d02d12cc3208e3a830454/BitwardenShared/Core/Platform/Models/Enum/FeatureFlag.swift#L7
|
// iOS (v2025.6.0): https://github.com/bitwarden/ios/blob/ff06d9c6cc8da89f78f37f376495800201d7261a/BitwardenShared/Core/Platform/Models/Enum/FeatureFlag.swift#L7
|
||||||
let mut feature_states = parse_experimental_client_feature_flags(
|
let mut feature_states =
|
||||||
&CONFIG.experimental_client_feature_flags(),
|
parse_experimental_client_feature_flags(&crate::CONFIG.experimental_client_feature_flags());
|
||||||
FeatureFlagFilter::ValidOnly,
|
feature_states.insert("duo-redirect".to_string(), true);
|
||||||
);
|
feature_states.insert("email-verification".to_string(), true);
|
||||||
feature_states.insert("pm-19148-innovation-archive".to_string(), true);
|
feature_states.insert("unauth-ui-refresh".to_string(), true);
|
||||||
|
feature_states.insert("enable-pm-flight-recorder".to_string(), true);
|
||||||
|
feature_states.insert("mobile-error-reporting".to_string(), true);
|
||||||
|
|
||||||
Json(json!({
|
Json(json!({
|
||||||
// Note: The clients use this version to handle backwards compatibility concerns
|
// Note: The clients use this version to handle backwards compatibility concerns
|
||||||
|
|
@ -224,7 +225,7 @@ fn config() -> Json<Value> {
|
||||||
"url": "https://github.com/dani-garcia/vaultwarden"
|
"url": "https://github.com/dani-garcia/vaultwarden"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"disableUserRegistration": CONFIG.is_signup_disabled()
|
"disableUserRegistration": crate::CONFIG.is_signup_disabled()
|
||||||
},
|
},
|
||||||
"environment": {
|
"environment": {
|
||||||
"vault": domain,
|
"vault": domain,
|
||||||
|
|
@ -277,7 +278,7 @@ async fn accept_org_invite(
|
||||||
|
|
||||||
member.save(conn).await?;
|
member.save(conn).await?;
|
||||||
|
|
||||||
if CONFIG.mail_enabled() {
|
if crate::CONFIG.mail_enabled() {
|
||||||
let org = match Organization::find_by_uuid(&member.org_uuid, conn).await {
|
let org = match Organization::find_by_uuid(&member.org_uuid, conn).await {
|
||||||
Some(org) => org,
|
Some(org) => org,
|
||||||
None => err!("Organization not found."),
|
None => err!("Organization not found."),
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -156,7 +156,7 @@ async fn ldap_import(data: Json<OrgImportData>, token: PublicToken, conn: DbConn
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
GroupUser::delete_all_by_group(&group_uuid, &org_id, &conn).await?;
|
GroupUser::delete_all_by_group(&group_uuid, &conn).await?;
|
||||||
|
|
||||||
for ext_id in &group_data.member_external_ids {
|
for ext_id in &group_data.member_external_ids {
|
||||||
if let Some(member) = Membership::find_by_external_id_and_org(ext_id, &org_id, &conn).await {
|
if let Some(member) = Membership::find_by_external_id_and_org(ext_id, &org_id, &conn).await {
|
||||||
|
|
|
||||||
|
|
@ -574,7 +574,7 @@ async fn download_url(host: &Host, send_id: &SendId, file_id: &SendFileId) -> Re
|
||||||
|
|
||||||
Ok(format!("{}/api/sends/{send_id}/{file_id}?t={token}", &host.host))
|
Ok(format!("{}/api/sends/{send_id}/{file_id}?t={token}", &host.host))
|
||||||
} else {
|
} else {
|
||||||
Ok(operator.presign_read(&format!("{send_id}/{file_id}"), Duration::from_mins(5)).await?.uri().to_string())
|
Ok(operator.presign_read(&format!("{send_id}/{file_id}"), Duration::from_secs(5 * 60)).await?.uri().to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,10 @@ use crate::{
|
||||||
core::{log_user_event, two_factor::_generate_recover_code},
|
core::{log_user_event, two_factor::_generate_recover_code},
|
||||||
EmptyResult, JsonResult, PasswordOrOtpData,
|
EmptyResult, JsonResult, PasswordOrOtpData,
|
||||||
},
|
},
|
||||||
auth::{ClientHeaders, Headers},
|
auth::Headers,
|
||||||
crypto,
|
crypto,
|
||||||
db::{
|
db::{
|
||||||
models::{AuthRequest, AuthRequestId, DeviceId, EventType, TwoFactor, TwoFactorType, User, UserId},
|
models::{DeviceId, EventType, TwoFactor, TwoFactorType, User, UserId},
|
||||||
DbConn,
|
DbConn,
|
||||||
},
|
},
|
||||||
error::{Error, MapResult},
|
error::{Error, MapResult},
|
||||||
|
|
@ -30,63 +30,35 @@ struct SendEmailLoginData {
|
||||||
email: Option<String>,
|
email: Option<String>,
|
||||||
#[serde(alias = "MasterPasswordHash")]
|
#[serde(alias = "MasterPasswordHash")]
|
||||||
master_password_hash: Option<String>,
|
master_password_hash: Option<String>,
|
||||||
auth_request_id: Option<AuthRequestId>,
|
|
||||||
auth_request_access_code: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// User is trying to login and wants to use email 2FA.
|
/// User is trying to login and wants to use email 2FA.
|
||||||
/// Does not require Bearer token
|
/// Does not require Bearer token
|
||||||
#[post("/two-factor/send-email-login", data = "<data>")] // JsonResult
|
#[post("/two-factor/send-email-login", data = "<data>")] // JsonResult
|
||||||
async fn send_email_login(data: Json<SendEmailLoginData>, client_headers: ClientHeaders, conn: DbConn) -> EmptyResult {
|
async fn send_email_login(data: Json<SendEmailLoginData>, conn: DbConn) -> EmptyResult {
|
||||||
let data: SendEmailLoginData = data.into_inner();
|
let data: SendEmailLoginData = data.into_inner();
|
||||||
|
|
||||||
if !CONFIG._enable_email_2fa() {
|
if !CONFIG._enable_email_2fa() {
|
||||||
err!("Email 2FA is disabled")
|
err!("Email 2FA is disabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ratelimit the login
|
|
||||||
crate::ratelimit::check_limit_login(&client_headers.ip.ip)?;
|
|
||||||
|
|
||||||
// Get the user
|
// Get the user
|
||||||
let email = match &data.email {
|
let email = match &data.email {
|
||||||
Some(email) if !email.is_empty() => Some(email),
|
Some(email) if !email.is_empty() => Some(email),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
let master_password_hash = match &data.master_password_hash {
|
|
||||||
Some(password_hash) if !password_hash.is_empty() => Some(password_hash),
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
let auth_request_id = match &data.auth_request_id {
|
|
||||||
Some(auth_request_id) if !auth_request_id.is_empty() => Some(auth_request_id),
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let user = if let Some(email) = email {
|
let user = if let Some(email) = email {
|
||||||
|
let Some(master_password_hash) = &data.master_password_hash else {
|
||||||
|
err!("No password hash has been submitted.")
|
||||||
|
};
|
||||||
|
|
||||||
let Some(user) = User::find_by_mail(email, &conn).await else {
|
let Some(user) = User::find_by_mail(email, &conn).await else {
|
||||||
err!("Username or password is incorrect. Try again.")
|
err!("Username or password is incorrect. Try again.")
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(master_password_hash) = master_password_hash {
|
// Check password
|
||||||
// Check password
|
if !user.check_valid_password(master_password_hash) {
|
||||||
if !user.check_valid_password(master_password_hash) {
|
err!("Username or password is incorrect. Try again.")
|
||||||
err!("Username or password is incorrect. Try again.")
|
|
||||||
}
|
|
||||||
} else if let Some(auth_request_id) = auth_request_id {
|
|
||||||
let Some(auth_request) = AuthRequest::find_by_uuid(auth_request_id, &conn).await else {
|
|
||||||
err!("AuthRequest doesn't exist", "User not found")
|
|
||||||
};
|
|
||||||
let Some(code) = &data.auth_request_access_code else {
|
|
||||||
err!("no auth request access code")
|
|
||||||
};
|
|
||||||
|
|
||||||
if auth_request.device_type != client_headers.device_type
|
|
||||||
|| auth_request.request_ip != client_headers.ip.ip.to_string()
|
|
||||||
|| !auth_request.check_access_code(code)
|
|
||||||
{
|
|
||||||
err!("AuthRequest doesn't exist", "Invalid device, IP or code")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
err!("No password hash has been submitted.")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
user
|
user
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
use chrono::{TimeDelta, Utc};
|
use chrono::{TimeDelta, Utc};
|
||||||
use data_encoding::BASE32;
|
use data_encoding::BASE32;
|
||||||
use num_traits::FromPrimitive;
|
|
||||||
use rocket::serde::json::Json;
|
use rocket::serde::json::Json;
|
||||||
use rocket::Route;
|
use rocket::Route;
|
||||||
use serde::Deserialize;
|
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
@ -11,12 +9,12 @@ use crate::{
|
||||||
core::{log_event, log_user_event},
|
core::{log_event, log_user_event},
|
||||||
EmptyResult, JsonResult, PasswordOrOtpData,
|
EmptyResult, JsonResult, PasswordOrOtpData,
|
||||||
},
|
},
|
||||||
auth::Headers,
|
auth::{ClientHeaders, Headers},
|
||||||
crypto,
|
crypto,
|
||||||
db::{
|
db::{
|
||||||
models::{
|
models::{
|
||||||
DeviceType, EventType, Membership, MembershipType, OrgPolicyType, Organization, OrganizationId, TwoFactor,
|
DeviceType, EventType, Membership, MembershipType, OrgPolicyType, Organization, OrganizationId, TwoFactor,
|
||||||
TwoFactorIncomplete, TwoFactorType, User, UserId,
|
TwoFactorIncomplete, User, UserId,
|
||||||
},
|
},
|
||||||
DbConn, DbPool,
|
DbConn, DbPool,
|
||||||
},
|
},
|
||||||
|
|
@ -33,47 +31,11 @@ pub mod protected_actions;
|
||||||
pub mod webauthn;
|
pub mod webauthn;
|
||||||
pub mod yubikey;
|
pub mod yubikey;
|
||||||
|
|
||||||
fn has_global_duo_credentials() -> bool {
|
|
||||||
CONFIG._enable_duo() && CONFIG.duo_host().is_some() && CONFIG.duo_ikey().is_some() && CONFIG.duo_skey().is_some()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_twofactor_provider_usable(provider_type: TwoFactorType, provider_data: Option<&str>) -> bool {
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct DuoProviderData {
|
|
||||||
host: String,
|
|
||||||
ik: String,
|
|
||||||
sk: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
match provider_type {
|
|
||||||
TwoFactorType::Authenticator => true,
|
|
||||||
TwoFactorType::Email => CONFIG._enable_email_2fa(),
|
|
||||||
TwoFactorType::Duo | TwoFactorType::OrganizationDuo => {
|
|
||||||
provider_data
|
|
||||||
.and_then(|raw| serde_json::from_str::<DuoProviderData>(raw).ok())
|
|
||||||
.is_some_and(|duo| !duo.host.is_empty() && !duo.ik.is_empty() && !duo.sk.is_empty())
|
|
||||||
|| has_global_duo_credentials()
|
|
||||||
}
|
|
||||||
TwoFactorType::YubiKey => {
|
|
||||||
CONFIG._enable_yubico() && CONFIG.yubico_client_id().is_some() && CONFIG.yubico_secret_key().is_some()
|
|
||||||
}
|
|
||||||
TwoFactorType::Webauthn => CONFIG.is_webauthn_2fa_supported(),
|
|
||||||
TwoFactorType::Remember => !CONFIG.disable_2fa_remember(),
|
|
||||||
TwoFactorType::RecoveryCode => true,
|
|
||||||
TwoFactorType::U2f
|
|
||||||
| TwoFactorType::U2fRegisterChallenge
|
|
||||||
| TwoFactorType::U2fLoginChallenge
|
|
||||||
| TwoFactorType::EmailVerificationChallenge
|
|
||||||
| TwoFactorType::WebauthnRegisterChallenge
|
|
||||||
| TwoFactorType::WebauthnLoginChallenge
|
|
||||||
| TwoFactorType::ProtectedActions => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
let mut routes = routes![
|
let mut routes = routes![
|
||||||
get_twofactor,
|
get_twofactor,
|
||||||
get_recover,
|
get_recover,
|
||||||
|
recover,
|
||||||
disable_twofactor,
|
disable_twofactor,
|
||||||
disable_twofactor_put,
|
disable_twofactor_put,
|
||||||
get_device_verification_settings,
|
get_device_verification_settings,
|
||||||
|
|
@ -92,13 +54,7 @@ pub fn routes() -> Vec<Route> {
|
||||||
#[get("/two-factor")]
|
#[get("/two-factor")]
|
||||||
async fn get_twofactor(headers: Headers, conn: DbConn) -> Json<Value> {
|
async fn get_twofactor(headers: Headers, conn: DbConn) -> Json<Value> {
|
||||||
let twofactors = TwoFactor::find_by_user(&headers.user.uuid, &conn).await;
|
let twofactors = TwoFactor::find_by_user(&headers.user.uuid, &conn).await;
|
||||||
let twofactors_json: Vec<Value> = twofactors
|
let twofactors_json: Vec<Value> = twofactors.iter().map(TwoFactor::to_json_provider).collect();
|
||||||
.iter()
|
|
||||||
.filter_map(|tf| {
|
|
||||||
let provider_type = TwoFactorType::from_i32(tf.atype)?;
|
|
||||||
is_twofactor_provider_usable(provider_type, Some(&tf.data)).then(|| TwoFactor::to_json_provider(tf))
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Json(json!({
|
Json(json!({
|
||||||
"data": twofactors_json,
|
"data": twofactors_json,
|
||||||
|
|
@ -120,6 +76,54 @@ async fn get_recover(data: Json<PasswordOrOtpData>, headers: Headers, conn: DbCo
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct RecoverTwoFactor {
|
||||||
|
master_password_hash: String,
|
||||||
|
email: String,
|
||||||
|
recovery_code: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/two-factor/recover", data = "<data>")]
|
||||||
|
async fn recover(data: Json<RecoverTwoFactor>, client_headers: ClientHeaders, conn: DbConn) -> JsonResult {
|
||||||
|
let data: RecoverTwoFactor = data.into_inner();
|
||||||
|
|
||||||
|
use crate::db::models::User;
|
||||||
|
|
||||||
|
// Get the user
|
||||||
|
let Some(mut user) = User::find_by_mail(&data.email, &conn).await else {
|
||||||
|
err!("Username or password is incorrect. Try again.")
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check password
|
||||||
|
if !user.check_valid_password(&data.master_password_hash) {
|
||||||
|
err!("Username or password is incorrect. Try again.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if recovery code is correct
|
||||||
|
if !user.check_valid_recovery_code(&data.recovery_code) {
|
||||||
|
err!("Recovery code is incorrect. Try again.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove all twofactors from the user
|
||||||
|
TwoFactor::delete_all_by_user(&user.uuid, &conn).await?;
|
||||||
|
enforce_2fa_policy(&user, &user.uuid, client_headers.device_type, &client_headers.ip.ip, &conn).await?;
|
||||||
|
|
||||||
|
log_user_event(
|
||||||
|
EventType::UserRecovered2fa as i32,
|
||||||
|
&user.uuid,
|
||||||
|
client_headers.device_type,
|
||||||
|
&client_headers.ip.ip,
|
||||||
|
&conn,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Remove the recovery code, not needed without twofactors
|
||||||
|
user.totp_recover = None;
|
||||||
|
user.save(&conn).await?;
|
||||||
|
Ok(Json(Value::Object(serde_json::Map::new())))
|
||||||
|
}
|
||||||
|
|
||||||
async fn _generate_recover_code(user: &mut User, conn: &DbConn) {
|
async fn _generate_recover_code(user: &mut User, conn: &DbConn) {
|
||||||
if user.totp_recover.is_none() {
|
if user.totp_recover.is_none() {
|
||||||
let totp_recover = crypto::encode_random_bytes::<20>(&BASE32);
|
let totp_recover = crypto::encode_random_bytes::<20>(&BASE32);
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ static WEBAUTHN: LazyLock<Webauthn> = LazyLock::new(|| {
|
||||||
let webauthn = WebauthnBuilder::new(&rp_id, &rp_origin)
|
let webauthn = WebauthnBuilder::new(&rp_id, &rp_origin)
|
||||||
.expect("Creating WebauthnBuilder failed")
|
.expect("Creating WebauthnBuilder failed")
|
||||||
.rp_name(&domain)
|
.rp_name(&domain)
|
||||||
.timeout(Duration::from_mins(1));
|
.timeout(Duration::from_millis(60000));
|
||||||
|
|
||||||
webauthn.build().expect("Building Webauthn failed")
|
webauthn.build().expect("Building Webauthn failed")
|
||||||
});
|
});
|
||||||
|
|
@ -108,8 +108,8 @@ impl WebauthnRegistration {
|
||||||
|
|
||||||
#[post("/two-factor/get-webauthn", data = "<data>")]
|
#[post("/two-factor/get-webauthn", data = "<data>")]
|
||||||
async fn get_webauthn(data: Json<PasswordOrOtpData>, headers: Headers, conn: DbConn) -> JsonResult {
|
async fn get_webauthn(data: Json<PasswordOrOtpData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||||
if !CONFIG.is_webauthn_2fa_supported() {
|
if !CONFIG.domain_set() {
|
||||||
err!("Configured `DOMAIN` is not compatible with Webauthn")
|
err!("`DOMAIN` environment variable is not set. Webauthn disabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
let data: PasswordOrOtpData = data.into_inner();
|
let data: PasswordOrOtpData = data.into_inner();
|
||||||
|
|
@ -144,7 +144,7 @@ async fn generate_webauthn_challenge(data: Json<PasswordOrOtpData>, headers: Hea
|
||||||
let (mut challenge, state) = WEBAUTHN.start_passkey_registration(
|
let (mut challenge, state) = WEBAUTHN.start_passkey_registration(
|
||||||
Uuid::from_str(&user.uuid).expect("Failed to parse UUID"), // Should never fail
|
Uuid::from_str(&user.uuid).expect("Failed to parse UUID"), // Should never fail
|
||||||
&user.email,
|
&user.email,
|
||||||
user.display_name(),
|
&user.name,
|
||||||
Some(registrations),
|
Some(registrations),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
|
@ -438,7 +438,7 @@ pub async fn validate_webauthn_login(user_id: &UserId, response: &str, conn: &Db
|
||||||
// We need to check for and update the backup_eligible flag when needed.
|
// We need to check for and update the backup_eligible flag when needed.
|
||||||
// Vaultwarden did not have knowledge of this flag prior to migrating to webauthn-rs v0.5.x
|
// Vaultwarden did not have knowledge of this flag prior to migrating to webauthn-rs v0.5.x
|
||||||
// Because of this we check the flag at runtime and update the registrations and state when needed
|
// Because of this we check the flag at runtime and update the registrations and state when needed
|
||||||
let backup_flags_updated = check_and_update_backup_eligible(&rsp, &mut registrations, &mut state)?;
|
check_and_update_backup_eligible(user_id, &rsp, &mut registrations, &mut state, conn).await?;
|
||||||
|
|
||||||
let authentication_result = WEBAUTHN.finish_passkey_authentication(&rsp, &state)?;
|
let authentication_result = WEBAUTHN.finish_passkey_authentication(&rsp, &state)?;
|
||||||
|
|
||||||
|
|
@ -446,8 +446,7 @@ pub async fn validate_webauthn_login(user_id: &UserId, response: &str, conn: &Db
|
||||||
if ct_eq(reg.credential.cred_id(), authentication_result.cred_id()) {
|
if ct_eq(reg.credential.cred_id(), authentication_result.cred_id()) {
|
||||||
// If the cred id matches and the credential is updated, Some(true) is returned
|
// If the cred id matches and the credential is updated, Some(true) is returned
|
||||||
// In those cases, update the record, else leave it alone
|
// In those cases, update the record, else leave it alone
|
||||||
let credential_updated = reg.credential.update_credential(&authentication_result) == Some(true);
|
if reg.credential.update_credential(&authentication_result) == Some(true) {
|
||||||
if credential_updated || backup_flags_updated {
|
|
||||||
TwoFactor::new(user_id.clone(), TwoFactorType::Webauthn, serde_json::to_string(®istrations)?)
|
TwoFactor::new(user_id.clone(), TwoFactorType::Webauthn, serde_json::to_string(®istrations)?)
|
||||||
.save(conn)
|
.save(conn)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
@ -464,11 +463,13 @@ pub async fn validate_webauthn_login(user_id: &UserId, response: &str, conn: &Db
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_and_update_backup_eligible(
|
async fn check_and_update_backup_eligible(
|
||||||
|
user_id: &UserId,
|
||||||
rsp: &PublicKeyCredential,
|
rsp: &PublicKeyCredential,
|
||||||
registrations: &mut Vec<WebauthnRegistration>,
|
registrations: &mut Vec<WebauthnRegistration>,
|
||||||
state: &mut PasskeyAuthentication,
|
state: &mut PasskeyAuthentication,
|
||||||
) -> Result<bool, Error> {
|
conn: &DbConn,
|
||||||
|
) -> EmptyResult {
|
||||||
// The feature flags from the response
|
// The feature flags from the response
|
||||||
// For details see: https://www.w3.org/TR/webauthn-3/#sctn-authenticator-data
|
// For details see: https://www.w3.org/TR/webauthn-3/#sctn-authenticator-data
|
||||||
const FLAG_BACKUP_ELIGIBLE: u8 = 0b0000_1000;
|
const FLAG_BACKUP_ELIGIBLE: u8 = 0b0000_1000;
|
||||||
|
|
@ -485,7 +486,16 @@ fn check_and_update_backup_eligible(
|
||||||
let rsp_id = rsp.raw_id.as_slice();
|
let rsp_id = rsp.raw_id.as_slice();
|
||||||
for reg in &mut *registrations {
|
for reg in &mut *registrations {
|
||||||
if ct_eq(reg.credential.cred_id().as_slice(), rsp_id) {
|
if ct_eq(reg.credential.cred_id().as_slice(), rsp_id) {
|
||||||
|
// Try to update the key, and if needed also update the database, before the actual state check is done
|
||||||
if reg.set_backup_eligible(backup_eligible, backup_state) {
|
if reg.set_backup_eligible(backup_eligible, backup_state) {
|
||||||
|
TwoFactor::new(
|
||||||
|
user_id.clone(),
|
||||||
|
TwoFactorType::Webauthn,
|
||||||
|
serde_json::to_string(®istrations)?,
|
||||||
|
)
|
||||||
|
.save(conn)
|
||||||
|
.await?;
|
||||||
|
|
||||||
// We also need to adjust the current state which holds the challenge used to start the authentication verification
|
// We also need to adjust the current state which holds the challenge used to start the authentication verification
|
||||||
// Because Vaultwarden supports multiple keys, we need to loop through the deserialized state and check which key to update
|
// Because Vaultwarden supports multiple keys, we need to loop through the deserialized state and check which key to update
|
||||||
let mut raw_state = serde_json::to_value(&state)?;
|
let mut raw_state = serde_json::to_value(&state)?;
|
||||||
|
|
@ -507,12 +517,11 @@ fn check_and_update_backup_eligible(
|
||||||
}
|
}
|
||||||
|
|
||||||
*state = serde_json::from_value(raw_state)?;
|
*state = serde_json::from_value(raw_state)?;
|
||||||
return Ok(true);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(false)
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
136
src/api/icons.rs
136
src/api/icons.rs
|
|
@ -19,7 +19,7 @@ use svg_hush::{data_url_filter, Filter};
|
||||||
use crate::{
|
use crate::{
|
||||||
config::PathType,
|
config::PathType,
|
||||||
error::Error,
|
error::Error,
|
||||||
http_client::{get_reqwest_client_builder, get_valid_host, should_block_host, CustomHttpClientError},
|
http_client::{get_reqwest_client_builder, should_block_address, CustomHttpClientError},
|
||||||
util::Cached,
|
util::Cached,
|
||||||
CONFIG,
|
CONFIG,
|
||||||
};
|
};
|
||||||
|
|
@ -81,19 +81,19 @@ static ICON_SIZE_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?x)(\d+
|
||||||
// The function name `icon_external` is checked in the `on_response` function in `AppHeaders`
|
// The function name `icon_external` is checked in the `on_response` function in `AppHeaders`
|
||||||
// It is used to prevent sending a specific header which breaks icon downloads.
|
// It is used to prevent sending a specific header which breaks icon downloads.
|
||||||
// If this function needs to be renamed, also adjust the code in `util.rs`
|
// If this function needs to be renamed, also adjust the code in `util.rs`
|
||||||
#[get("/<host>/icon.png")]
|
#[get("/<domain>/icon.png")]
|
||||||
fn icon_external(host: &str) -> Cached<Option<Redirect>> {
|
fn icon_external(domain: &str) -> Cached<Option<Redirect>> {
|
||||||
let Ok(host) = get_valid_host(host) else {
|
if !is_valid_domain(domain) {
|
||||||
warn!("Invalid host: {host}");
|
warn!("Invalid domain: {domain}");
|
||||||
return Cached::ttl(None, CONFIG.icon_cache_negttl(), true);
|
|
||||||
};
|
|
||||||
|
|
||||||
if should_block_host(&host).is_err() {
|
|
||||||
warn!("Blocked address: {host}");
|
|
||||||
return Cached::ttl(None, CONFIG.icon_cache_negttl(), true);
|
return Cached::ttl(None, CONFIG.icon_cache_negttl(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
let url = CONFIG._icon_service_url().replace("{}", &host.to_string());
|
if should_block_address(domain) {
|
||||||
|
warn!("Blocked address: {domain}");
|
||||||
|
return Cached::ttl(None, CONFIG.icon_cache_negttl(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = CONFIG._icon_service_url().replace("{}", domain);
|
||||||
let redir = match CONFIG.icon_redirect_code() {
|
let redir = match CONFIG.icon_redirect_code() {
|
||||||
301 => Some(Redirect::moved(url)), // legacy permanent redirect
|
301 => Some(Redirect::moved(url)), // legacy permanent redirect
|
||||||
302 => Some(Redirect::found(url)), // legacy temporary redirect
|
302 => Some(Redirect::found(url)), // legacy temporary redirect
|
||||||
|
|
@ -107,21 +107,12 @@ fn icon_external(host: &str) -> Cached<Option<Redirect>> {
|
||||||
Cached::ttl(redir, CONFIG.icon_cache_ttl(), true)
|
Cached::ttl(redir, CONFIG.icon_cache_ttl(), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/<host>/icon.png")]
|
#[get("/<domain>/icon.png")]
|
||||||
async fn icon_internal(host: &str) -> Cached<(ContentType, Vec<u8>)> {
|
async fn icon_internal(domain: &str) -> Cached<(ContentType, Vec<u8>)> {
|
||||||
const FALLBACK_ICON: &[u8] = include_bytes!("../static/images/fallback-icon.png");
|
const FALLBACK_ICON: &[u8] = include_bytes!("../static/images/fallback-icon.png");
|
||||||
|
|
||||||
let Ok(host) = get_valid_host(host) else {
|
if !is_valid_domain(domain) {
|
||||||
warn!("Invalid host: {host}");
|
warn!("Invalid domain: {domain}");
|
||||||
return Cached::ttl(
|
|
||||||
(ContentType::new("image", "png"), FALLBACK_ICON.to_vec()),
|
|
||||||
CONFIG.icon_cache_negttl(),
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
if should_block_host(&host).is_err() {
|
|
||||||
warn!("Blocked address: {host}");
|
|
||||||
return Cached::ttl(
|
return Cached::ttl(
|
||||||
(ContentType::new("image", "png"), FALLBACK_ICON.to_vec()),
|
(ContentType::new("image", "png"), FALLBACK_ICON.to_vec()),
|
||||||
CONFIG.icon_cache_negttl(),
|
CONFIG.icon_cache_negttl(),
|
||||||
|
|
@ -129,7 +120,16 @@ async fn icon_internal(host: &str) -> Cached<(ContentType, Vec<u8>)> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
match get_icon(&host.to_string()).await {
|
if should_block_address(domain) {
|
||||||
|
warn!("Blocked address: {domain}");
|
||||||
|
return Cached::ttl(
|
||||||
|
(ContentType::new("image", "png"), FALLBACK_ICON.to_vec()),
|
||||||
|
CONFIG.icon_cache_negttl(),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
match get_icon(domain).await {
|
||||||
Some((icon, icon_type)) => {
|
Some((icon, icon_type)) => {
|
||||||
Cached::ttl((ContentType::new("image", icon_type), icon), CONFIG.icon_cache_ttl(), true)
|
Cached::ttl((ContentType::new("image", icon_type), icon), CONFIG.icon_cache_ttl(), true)
|
||||||
}
|
}
|
||||||
|
|
@ -137,6 +137,42 @@ async fn icon_internal(host: &str) -> Cached<(ContentType, Vec<u8>)> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns if the domain provided is valid or not.
|
||||||
|
///
|
||||||
|
/// This does some manual checks and makes use of Url to do some basic checking.
|
||||||
|
/// domains can't be larger then 63 characters (not counting multiple subdomains) according to the RFC's, but we limit the total size to 255.
|
||||||
|
fn is_valid_domain(domain: &str) -> bool {
|
||||||
|
const ALLOWED_CHARS: &str = "-.";
|
||||||
|
|
||||||
|
// If parsing the domain fails using Url, it will not work with reqwest.
|
||||||
|
if let Err(parse_error) = url::Url::parse(format!("https://{domain}").as_str()) {
|
||||||
|
debug!("Domain parse error: '{domain}' - {parse_error:?}");
|
||||||
|
return false;
|
||||||
|
} else if domain.is_empty()
|
||||||
|
|| domain.contains("..")
|
||||||
|
|| domain.starts_with('.')
|
||||||
|
|| domain.starts_with('-')
|
||||||
|
|| domain.ends_with('-')
|
||||||
|
{
|
||||||
|
debug!(
|
||||||
|
"Domain validation error: '{domain}' is either empty, contains '..', starts with an '.', starts or ends with a '-'"
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
} else if domain.len() > 255 {
|
||||||
|
debug!("Domain validation error: '{domain}' exceeds 255 characters");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for c in domain.chars() {
|
||||||
|
if !c.is_alphanumeric() && !ALLOWED_CHARS.contains(c) {
|
||||||
|
debug!("Domain validation error: '{domain}' contains an invalid character '{c}'");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
async fn get_icon(domain: &str) -> Option<(Vec<u8>, String)> {
|
async fn get_icon(domain: &str) -> Option<(Vec<u8>, String)> {
|
||||||
let path = format!("{domain}.png");
|
let path = format!("{domain}.png");
|
||||||
|
|
||||||
|
|
@ -331,7 +367,7 @@ async fn get_icon_url(domain: &str) -> Result<IconUrlResult, Error> {
|
||||||
tld = domain_parts.next_back().unwrap(),
|
tld = domain_parts.next_back().unwrap(),
|
||||||
base = domain_parts.next_back().unwrap()
|
base = domain_parts.next_back().unwrap()
|
||||||
);
|
);
|
||||||
if get_valid_host(&base_domain).is_ok() {
|
if is_valid_domain(&base_domain) {
|
||||||
let sslbase = format!("https://{base_domain}");
|
let sslbase = format!("https://{base_domain}");
|
||||||
let httpbase = format!("http://{base_domain}");
|
let httpbase = format!("http://{base_domain}");
|
||||||
debug!("[get_icon_url]: Trying without subdomains '{base_domain}'");
|
debug!("[get_icon_url]: Trying without subdomains '{base_domain}'");
|
||||||
|
|
@ -342,7 +378,7 @@ async fn get_icon_url(domain: &str) -> Result<IconUrlResult, Error> {
|
||||||
// When the domain is not an IP, and has less then 2 dots, try to add www. infront of it.
|
// When the domain is not an IP, and has less then 2 dots, try to add www. infront of it.
|
||||||
} else if is_ip.is_err() && domain.matches('.').count() < 2 {
|
} else if is_ip.is_err() && domain.matches('.').count() < 2 {
|
||||||
let www_domain = format!("www.{domain}");
|
let www_domain = format!("www.{domain}");
|
||||||
if get_valid_host(&www_domain).is_ok() {
|
if is_valid_domain(&www_domain) {
|
||||||
let sslwww = format!("https://{www_domain}");
|
let sslwww = format!("https://{www_domain}");
|
||||||
let httpwww = format!("http://{www_domain}");
|
let httpwww = format!("http://{www_domain}");
|
||||||
debug!("[get_icon_url]: Trying with www. prefix '{www_domain}'");
|
debug!("[get_icon_url]: Trying with www. prefix '{www_domain}'");
|
||||||
|
|
@ -477,11 +513,13 @@ fn parse_sizes(sizes: &str) -> (u16, u16) {
|
||||||
|
|
||||||
if !sizes.is_empty() {
|
if !sizes.is_empty() {
|
||||||
match ICON_SIZE_REGEX.captures(sizes.trim()) {
|
match ICON_SIZE_REGEX.captures(sizes.trim()) {
|
||||||
Some(dimensions) if dimensions.len() >= 3 => {
|
None => {}
|
||||||
width = dimensions[1].parse::<u16>().unwrap_or_default();
|
Some(dimensions) => {
|
||||||
height = dimensions[2].parse::<u16>().unwrap_or_default();
|
if dimensions.len() >= 3 {
|
||||||
|
width = dimensions[1].parse::<u16>().unwrap_or_default();
|
||||||
|
height = dimensions[2].parse::<u16>().unwrap_or_default();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -496,8 +534,7 @@ async fn download_icon(domain: &str) -> Result<(Bytes, Option<&str>), Error> {
|
||||||
|
|
||||||
use data_url::DataUrl;
|
use data_url::DataUrl;
|
||||||
|
|
||||||
let mut icons = icon_result.iconlist.iter().take(5).peekable();
|
for icon in icon_result.iconlist.iter().take(5) {
|
||||||
while let Some(icon) = icons.next() {
|
|
||||||
if icon.href.starts_with("data:image") {
|
if icon.href.starts_with("data:image") {
|
||||||
let Ok(datauri) = DataUrl::process(&icon.href) else {
|
let Ok(datauri) = DataUrl::process(&icon.href) else {
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -525,23 +562,11 @@ async fn download_icon(domain: &str) -> Result<(Bytes, Option<&str>), Error> {
|
||||||
_ => debug!("Extracted icon from data:image uri is invalid"),
|
_ => debug!("Extracted icon from data:image uri is invalid"),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
debug!("Trying {}", icon.href);
|
let res = get_page_with_referer(&icon.href, &icon_result.referer).await?;
|
||||||
// Make sure all icons are checked before returning error
|
|
||||||
let res = match get_page_with_referer(&icon.href, &icon_result.referer).await {
|
|
||||||
Ok(r) => r,
|
|
||||||
Err(e) if icons.peek().is_none() => return Err(e),
|
|
||||||
Err(e) if CustomHttpClientError::downcast_ref(&e).is_some() => return Err(e), // If blacklisted stop immediately instead of checking the rest of the icons. see explanation and actual handling inside get_icon()
|
|
||||||
Err(e) => {
|
|
||||||
warn!("Unable to download icon: {e:?}");
|
|
||||||
|
|
||||||
// Continue to next icon
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
buffer = stream_to_bytes_limit(res, 5120 * 1024).await?; // 5120KB/5MB for each icon max (Same as icons.bitwarden.net)
|
buffer = stream_to_bytes_limit(res, 5120 * 1024).await?; // 5120KB/5MB for each icon max (Same as icons.bitwarden.net)
|
||||||
|
|
||||||
// Check if the icon type is allowed, else try another icon from the list.
|
// Check if the icon type is allowed, else try an icon from the list.
|
||||||
icon_type = get_icon_type(&buffer);
|
icon_type = get_icon_type(&buffer);
|
||||||
if icon_type.is_none() {
|
if icon_type.is_none() {
|
||||||
buffer.clear();
|
buffer.clear();
|
||||||
|
|
@ -595,17 +620,14 @@ fn get_icon_type(bytes: &[u8]) -> Option<&'static str> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
// Some details can be found here:
|
|
||||||
// - https://www.garykessler.net/library/file_sigs_GCK_latest.html
|
|
||||||
// - https://en.wikipedia.org/wiki/List_of_file_signatures
|
|
||||||
match bytes {
|
match bytes {
|
||||||
[137, 80, 78, 71, 13, 10, 26, 10, ..] => Some("png"),
|
[137, 80, 78, 71, ..] => Some("png"),
|
||||||
[0, 0, 1, 0, n1, n2, ..] if u16::from_le_bytes([*n1, *n2]) > 0 => Some("x-icon"), // https://en.wikipedia.org/wiki/ICO_(file_format)
|
[0, 0, 1, 0, ..] => Some("x-icon"),
|
||||||
[82, 73, 70, 70, _, _, _, _, 87, 69, 66, 80, ..] => Some("webp"), // Only match WebP Images
|
[82, 73, 70, 70, ..] => Some("webp"),
|
||||||
[255, 216, 255, b, ..] if *b >= 0xC0 => Some("jpeg"),
|
[255, 216, 255, ..] => Some("jpeg"),
|
||||||
[71, 73, 70, 56, 55 | 57, 97, ..] => Some("gif"),
|
[71, 73, 70, 56, ..] => Some("gif"),
|
||||||
[66, 77, _, _, _, _, 0, 0, 0, 0, ..] => Some("bmp"), // https://en.wikipedia.org/wiki/BMP_file_format
|
[66, 77, ..] => Some("bmp"),
|
||||||
[60, 115, 118, 103, ..] => Some("svg+xml"), // Normal svg
|
[60, 115, 118, 103, ..] => Some("svg+xml"), // Normal svg
|
||||||
[60, 63, 120, 109, 108, ..] => check_svg_after_xml_declaration(bytes), // An svg starting with <?xml
|
[60, 63, 120, 109, 108, ..] => check_svg_after_xml_declaration(bytes), // An svg starting with <?xml
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use chrono::Utc;
|
||||||
use num_traits::FromPrimitive;
|
use num_traits::FromPrimitive;
|
||||||
use rocket::{
|
use rocket::{
|
||||||
form::{Form, FromForm},
|
form::{Form, FromForm},
|
||||||
http::{Cookie, CookieJar, SameSite},
|
http::Status,
|
||||||
response::Redirect,
|
response::Redirect,
|
||||||
serde::json::Json,
|
serde::json::Json,
|
||||||
Route,
|
Route,
|
||||||
|
|
@ -12,20 +12,16 @@ use serde_json::Value;
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{
|
api::{
|
||||||
core::{
|
core::{
|
||||||
accounts::{_prelogin, _register, kdf_upgrade, PreloginData, RegisterData},
|
accounts::{PreloginData, RegisterData, _prelogin, _register, kdf_upgrade},
|
||||||
log_user_event,
|
log_user_event,
|
||||||
two_factor::{
|
two_factor::{authenticator, duo, duo_oidc, email, enforce_2fa_policy, webauthn, yubikey},
|
||||||
authenticator, duo, duo_oidc, email, enforce_2fa_policy, is_twofactor_provider_usable, webauthn,
|
|
||||||
yubikey,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
master_password_policy,
|
master_password_policy,
|
||||||
push::register_push_device,
|
push::register_push_device,
|
||||||
ApiResult, EmptyResult, JsonResult,
|
ApiResult, EmptyResult, JsonResult,
|
||||||
},
|
},
|
||||||
auth,
|
auth,
|
||||||
auth::{generate_organization_api_key_login_claims, AuthMethod, ClientHeaders, ClientIp, ClientVersion, Secure},
|
auth::{generate_organization_api_key_login_claims, AuthMethod, ClientHeaders, ClientIp, ClientVersion},
|
||||||
crypto,
|
|
||||||
db::{
|
db::{
|
||||||
models::{
|
models::{
|
||||||
AuthRequest, AuthRequestId, Device, DeviceId, EventType, Invitation, OIDCCodeWrapper, OrganizationApiKey,
|
AuthRequest, AuthRequestId, Device, DeviceId, EventType, Invitation, OIDCCodeWrapper, OrganizationApiKey,
|
||||||
|
|
@ -43,7 +39,6 @@ pub fn routes() -> Vec<Route> {
|
||||||
routes![
|
routes![
|
||||||
login,
|
login,
|
||||||
prelogin,
|
prelogin,
|
||||||
prelogin_password,
|
|
||||||
identity_register,
|
identity_register,
|
||||||
register_verification_email,
|
register_verification_email,
|
||||||
register_finish,
|
register_finish,
|
||||||
|
|
@ -67,43 +62,43 @@ async fn login(
|
||||||
|
|
||||||
let login_result = match data.grant_type.as_ref() {
|
let login_result = match data.grant_type.as_ref() {
|
||||||
"refresh_token" => {
|
"refresh_token" => {
|
||||||
_check_is_some(data.refresh_token.as_ref(), "refresh_token cannot be blank")?;
|
_check_is_some(&data.refresh_token, "refresh_token cannot be blank")?;
|
||||||
_refresh_login(data, &conn, &client_header.ip).await
|
_refresh_login(data, &conn, &client_header.ip).await
|
||||||
}
|
}
|
||||||
"password" if CONFIG.sso_enabled() && CONFIG.sso_only() => err!("SSO sign-in is required"),
|
"password" if CONFIG.sso_enabled() && CONFIG.sso_only() => err!("SSO sign-in is required"),
|
||||||
"password" => {
|
"password" => {
|
||||||
_check_is_some(data.client_id.as_ref(), "client_id cannot be blank")?;
|
_check_is_some(&data.client_id, "client_id cannot be blank")?;
|
||||||
_check_is_some(data.password.as_ref(), "password cannot be blank")?;
|
_check_is_some(&data.password, "password cannot be blank")?;
|
||||||
_check_is_some(data.scope.as_ref(), "scope cannot be blank")?;
|
_check_is_some(&data.scope, "scope cannot be blank")?;
|
||||||
_check_is_some(data.username.as_ref(), "username cannot be blank")?;
|
_check_is_some(&data.username, "username cannot be blank")?;
|
||||||
|
|
||||||
_check_is_some(data.device_identifier.as_ref(), "device_identifier cannot be blank")?;
|
_check_is_some(&data.device_identifier, "device_identifier cannot be blank")?;
|
||||||
_check_is_some(data.device_name.as_ref(), "device_name cannot be blank")?;
|
_check_is_some(&data.device_name, "device_name cannot be blank")?;
|
||||||
_check_is_some(data.device_type.as_ref(), "device_type cannot be blank")?;
|
_check_is_some(&data.device_type, "device_type cannot be blank")?;
|
||||||
|
|
||||||
_password_login(data, &mut user_id, &conn, &client_header.ip, client_version.as_ref()).await
|
_password_login(data, &mut user_id, &conn, &client_header.ip, &client_version).await
|
||||||
}
|
}
|
||||||
"client_credentials" => {
|
"client_credentials" => {
|
||||||
_check_is_some(data.client_id.as_ref(), "client_id cannot be blank")?;
|
_check_is_some(&data.client_id, "client_id cannot be blank")?;
|
||||||
_check_is_some(data.client_secret.as_ref(), "client_secret cannot be blank")?;
|
_check_is_some(&data.client_secret, "client_secret cannot be blank")?;
|
||||||
_check_is_some(data.scope.as_ref(), "scope cannot be blank")?;
|
_check_is_some(&data.scope, "scope cannot be blank")?;
|
||||||
|
|
||||||
_check_is_some(data.device_identifier.as_ref(), "device_identifier cannot be blank")?;
|
_check_is_some(&data.device_identifier, "device_identifier cannot be blank")?;
|
||||||
_check_is_some(data.device_name.as_ref(), "device_name cannot be blank")?;
|
_check_is_some(&data.device_name, "device_name cannot be blank")?;
|
||||||
_check_is_some(data.device_type.as_ref(), "device_type cannot be blank")?;
|
_check_is_some(&data.device_type, "device_type cannot be blank")?;
|
||||||
|
|
||||||
_api_key_login(data, &mut user_id, &conn, &client_header.ip).await
|
_api_key_login(data, &mut user_id, &conn, &client_header.ip).await
|
||||||
}
|
}
|
||||||
"authorization_code" if CONFIG.sso_enabled() => {
|
"authorization_code" if CONFIG.sso_enabled() => {
|
||||||
_check_is_some(data.client_id.as_ref(), "client_id cannot be blank")?;
|
_check_is_some(&data.client_id, "client_id cannot be blank")?;
|
||||||
_check_is_some(data.code.as_ref(), "code cannot be blank")?;
|
_check_is_some(&data.code, "code cannot be blank")?;
|
||||||
_check_is_some(data.code_verifier.as_ref(), "code verifier cannot be blank")?;
|
_check_is_some(&data.code_verifier, "code verifier cannot be blank")?;
|
||||||
|
|
||||||
_check_is_some(data.device_identifier.as_ref(), "device_identifier cannot be blank")?;
|
_check_is_some(&data.device_identifier, "device_identifier cannot be blank")?;
|
||||||
_check_is_some(data.device_name.as_ref(), "device_name cannot be blank")?;
|
_check_is_some(&data.device_name, "device_name cannot be blank")?;
|
||||||
_check_is_some(data.device_type.as_ref(), "device_type cannot be blank")?;
|
_check_is_some(&data.device_type, "device_type cannot be blank")?;
|
||||||
|
|
||||||
_sso_login(data, &mut user_id, &conn, &client_header.ip, client_version.as_ref()).await
|
_sso_login(data, &mut user_id, &conn, &client_header.ip, &client_version).await
|
||||||
}
|
}
|
||||||
"authorization_code" => err!("SSO sign-in is not available"),
|
"authorization_code" => err!("SSO sign-in is not available"),
|
||||||
t => err!("Invalid type", t),
|
t => err!("Invalid type", t),
|
||||||
|
|
@ -133,14 +128,12 @@ async fn login(
|
||||||
login_result
|
login_result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Return Status::Unauthorized to trigger logout
|
||||||
async fn _refresh_login(data: ConnectData, conn: &DbConn, ip: &ClientIp) -> JsonResult {
|
async fn _refresh_login(data: ConnectData, conn: &DbConn, ip: &ClientIp) -> JsonResult {
|
||||||
// When a refresh token is invalid or missing we need to respond with an HTTP BadRequest (400)
|
// Extract token
|
||||||
// It also needs to return a json which holds at least a key `error` with the value `invalid_grant`
|
let refresh_token = match data.refresh_token {
|
||||||
// See the link below for details
|
Some(token) => token,
|
||||||
// https://github.com/bitwarden/clients/blob/2ee158e720a5e7dbe3641caf80b569e97a1dd91b/libs/common/src/services/api.service.ts#L1786-L1797
|
None => err_code!("Missing refresh_token", Status::Unauthorized.code),
|
||||||
|
|
||||||
let Some(refresh_token) = data.refresh_token else {
|
|
||||||
err_json!(json!({"error": "invalid_grant"}), "Missing refresh_token")
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---
|
// ---
|
||||||
|
|
@ -151,10 +144,7 @@ async fn _refresh_login(data: ConnectData, conn: &DbConn, ip: &ClientIp) -> Json
|
||||||
// let members = Membership::find_confirmed_by_user(&user.uuid, conn).await;
|
// let members = Membership::find_confirmed_by_user(&user.uuid, conn).await;
|
||||||
match auth::refresh_tokens(ip, &refresh_token, data.client_id, conn).await {
|
match auth::refresh_tokens(ip, &refresh_token, data.client_id, conn).await {
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
err_json!(
|
err_code!(format!("Unable to refresh login credentials: {}", err.message()), Status::Unauthorized.code)
|
||||||
json!({"error": "invalid_grant"}),
|
|
||||||
format!("Unable to refresh login credentials: {}", err.message())
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
Ok((mut device, auth_tokens)) => {
|
Ok((mut device, auth_tokens)) => {
|
||||||
// Save to update `device.updated_at` to track usage and toggle new status
|
// Save to update `device.updated_at` to track usage and toggle new status
|
||||||
|
|
@ -179,7 +169,7 @@ async fn _sso_login(
|
||||||
user_id: &mut Option<UserId>,
|
user_id: &mut Option<UserId>,
|
||||||
conn: &DbConn,
|
conn: &DbConn,
|
||||||
ip: &ClientIp,
|
ip: &ClientIp,
|
||||||
client_version: Option<&ClientVersion>,
|
client_version: &Option<ClientVersion>,
|
||||||
) -> JsonResult {
|
) -> JsonResult {
|
||||||
AuthMethod::Sso.check_scope(data.scope.as_ref())?;
|
AuthMethod::Sso.check_scope(data.scope.as_ref())?;
|
||||||
|
|
||||||
|
|
@ -230,33 +220,7 @@ async fn _sso_login(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Some((user, None)) => match user_infos.email_verified {
|
Some((user, None)) => Some((user, None)),
|
||||||
None if !CONFIG.sso_allow_unknown_email_verification() => {
|
|
||||||
error!(
|
|
||||||
"Login failure ({}), existing non SSO user ({}) with same email ({}) and email verification status is unknown",
|
|
||||||
user_infos.identifier, user.uuid, user.email
|
|
||||||
);
|
|
||||||
err_silent!(
|
|
||||||
"Email verification status is unknown",
|
|
||||||
ErrorEvent {
|
|
||||||
event: EventType::UserFailedLogIn
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Some(false) => {
|
|
||||||
error!(
|
|
||||||
"Login failure ({}), existing non SSO user ({}) with same email ({}) and email is not verified",
|
|
||||||
user_infos.identifier, user.uuid, user.email
|
|
||||||
);
|
|
||||||
err_silent!(
|
|
||||||
"Email is not verified by the SSO provider",
|
|
||||||
ErrorEvent {
|
|
||||||
event: EventType::UserFailedLogIn
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
_ => Some((user, None)),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
Some((user, sso_user)) => Some((user, Some(sso_user))),
|
Some((user, sso_user)) => Some((user, Some(sso_user))),
|
||||||
};
|
};
|
||||||
|
|
@ -302,7 +266,7 @@ async fn _sso_login(
|
||||||
Some((user, _)) if !user.enabled => {
|
Some((user, _)) if !user.enabled => {
|
||||||
err!(
|
err!(
|
||||||
"This user has been disabled",
|
"This user has been disabled",
|
||||||
format!("IP: {}. Username: {}.", ip.ip, user.display_name()),
|
format!("IP: {}. Username: {}.", ip.ip, user.name),
|
||||||
ErrorEvent {
|
ErrorEvent {
|
||||||
event: EventType::UserFailedLogIn
|
event: EventType::UserFailedLogIn
|
||||||
}
|
}
|
||||||
|
|
@ -348,7 +312,7 @@ async fn _password_login(
|
||||||
user_id: &mut Option<UserId>,
|
user_id: &mut Option<UserId>,
|
||||||
conn: &DbConn,
|
conn: &DbConn,
|
||||||
ip: &ClientIp,
|
ip: &ClientIp,
|
||||||
client_version: Option<&ClientVersion>,
|
client_version: &Option<ClientVersion>,
|
||||||
) -> JsonResult {
|
) -> JsonResult {
|
||||||
// Validate scope
|
// Validate scope
|
||||||
AuthMethod::Password.check_scope(data.scope.as_ref())?;
|
AuthMethod::Password.check_scope(data.scope.as_ref())?;
|
||||||
|
|
@ -518,18 +482,14 @@ async fn authenticated_response(
|
||||||
Value::Null
|
Value::Null
|
||||||
};
|
};
|
||||||
|
|
||||||
let account_keys = if user.private_key.is_some() {
|
let account_keys = json!({
|
||||||
json!({
|
"publicKeyEncryptionKeyPair": {
|
||||||
"publicKeyEncryptionKeyPair": {
|
"wrappedPrivateKey": user.private_key,
|
||||||
"wrappedPrivateKey": user.private_key,
|
"publicKey": user.public_key,
|
||||||
"publicKey": user.public_key,
|
"Object": "publicKeyEncryptionKeyPair"
|
||||||
"Object": "publicKeyEncryptionKeyPair"
|
},
|
||||||
},
|
"Object": "privateKeys"
|
||||||
"Object": "privateKeys"
|
});
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Value::Null
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut result = json!({
|
let mut result = json!({
|
||||||
"access_token": auth_tokens.access_token(),
|
"access_token": auth_tokens.access_token(),
|
||||||
|
|
@ -561,7 +521,7 @@ async fn authenticated_response(
|
||||||
result["TwoFactorToken"] = Value::String(token);
|
result["TwoFactorToken"] = Value::String(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("User {} logged in successfully. IP: {}", user.display_name(), ip.ip);
|
info!("User {} logged in successfully. IP: {}", &user.name, ip.ip);
|
||||||
Ok(Json(result))
|
Ok(Json(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -650,38 +610,6 @@ async fn _user_api_key_login(
|
||||||
|
|
||||||
info!("User {} logged in successfully via API key. IP: {}", user.email, ip.ip);
|
info!("User {} logged in successfully via API key. IP: {}", user.email, ip.ip);
|
||||||
|
|
||||||
let has_master_password = !user.password_hash.is_empty();
|
|
||||||
let master_password_unlock = if has_master_password {
|
|
||||||
json!({
|
|
||||||
"Kdf": {
|
|
||||||
"KdfType": user.client_kdf_type,
|
|
||||||
"Iterations": user.client_kdf_iter,
|
|
||||||
"Memory": user.client_kdf_memory,
|
|
||||||
"Parallelism": user.client_kdf_parallelism
|
|
||||||
},
|
|
||||||
// This field is named inconsistently and will be removed and replaced by the "wrapped" variant in the apps.
|
|
||||||
// https://github.com/bitwarden/android/blob/release/2025.12-rc41/network/src/main/kotlin/com/bitwarden/network/model/MasterPasswordUnlockDataJson.kt#L22-L26
|
|
||||||
"MasterKeyEncryptedUserKey": user.akey,
|
|
||||||
"MasterKeyWrappedUserKey": user.akey,
|
|
||||||
"Salt": user.email
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Value::Null
|
|
||||||
};
|
|
||||||
|
|
||||||
let account_keys = if user.private_key.is_some() {
|
|
||||||
json!({
|
|
||||||
"publicKeyEncryptionKeyPair": {
|
|
||||||
"wrappedPrivateKey": user.private_key,
|
|
||||||
"publicKey": user.public_key,
|
|
||||||
"Object": "publicKeyEncryptionKeyPair"
|
|
||||||
},
|
|
||||||
"Object": "privateKeys"
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Value::Null
|
|
||||||
};
|
|
||||||
|
|
||||||
// Note: No refresh_token is returned. The CLI just repeats the
|
// Note: No refresh_token is returned. The CLI just repeats the
|
||||||
// client_credentials login flow when the existing token expires.
|
// client_credentials login flow when the existing token expires.
|
||||||
let result = json!({
|
let result = json!({
|
||||||
|
|
@ -696,14 +624,7 @@ async fn _user_api_key_login(
|
||||||
"KdfMemory": user.client_kdf_memory,
|
"KdfMemory": user.client_kdf_memory,
|
||||||
"KdfParallelism": user.client_kdf_parallelism,
|
"KdfParallelism": user.client_kdf_parallelism,
|
||||||
"ResetMasterPassword": false, // TODO: according to official server seems something like: user.password_hash.is_empty(), but would need testing
|
"ResetMasterPassword": false, // TODO: according to official server seems something like: user.password_hash.is_empty(), but would need testing
|
||||||
"ForcePasswordReset": false,
|
|
||||||
"scope": AuthMethod::UserApiKey.scope(),
|
"scope": AuthMethod::UserApiKey.scope(),
|
||||||
"AccountKeys": account_keys,
|
|
||||||
"UserDecryptionOptions": {
|
|
||||||
"HasMasterPassword": has_master_password,
|
|
||||||
"MasterPasswordUnlock": master_password_unlock,
|
|
||||||
"Object": "userDecryptionOptions"
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(Json(result))
|
Ok(Json(result))
|
||||||
|
|
@ -762,7 +683,7 @@ async fn twofactor_auth(
|
||||||
data: &ConnectData,
|
data: &ConnectData,
|
||||||
device: &mut Device,
|
device: &mut Device,
|
||||||
ip: &ClientIp,
|
ip: &ClientIp,
|
||||||
client_version: Option<&ClientVersion>,
|
client_version: &Option<ClientVersion>,
|
||||||
conn: &DbConn,
|
conn: &DbConn,
|
||||||
) -> ApiResult<Option<String>> {
|
) -> ApiResult<Option<String>> {
|
||||||
let twofactors = TwoFactor::find_by_user(&user.uuid, conn).await;
|
let twofactors = TwoFactor::find_by_user(&user.uuid, conn).await;
|
||||||
|
|
@ -775,27 +696,8 @@ async fn twofactor_auth(
|
||||||
|
|
||||||
TwoFactorIncomplete::mark_incomplete(&user.uuid, &device.uuid, &device.name, device.atype, ip, conn).await?;
|
TwoFactorIncomplete::mark_incomplete(&user.uuid, &device.uuid, &device.name, device.atype, ip, conn).await?;
|
||||||
|
|
||||||
let twofactor_ids: Vec<_> = twofactors
|
let twofactor_ids: Vec<_> = twofactors.iter().map(|tf| tf.atype).collect();
|
||||||
.iter()
|
|
||||||
.filter_map(|tf| {
|
|
||||||
let provider_type = TwoFactorType::from_i32(tf.atype)?;
|
|
||||||
(tf.enabled && is_twofactor_provider_usable(provider_type, Some(&tf.data))).then_some(tf.atype)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
if twofactor_ids.is_empty() {
|
|
||||||
err!("No enabled and usable two factor providers are available for this account")
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
||||||
// Ignore Remember and RecoveryCode Types during this check, these are special
|
|
||||||
if ![TwoFactorType::Remember as i32, TwoFactorType::RecoveryCode as i32].contains(&selected_id)
|
|
||||||
&& !twofactor_ids.contains(&selected_id)
|
|
||||||
{
|
|
||||||
err_json!(
|
|
||||||
_json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, conn).await?,
|
|
||||||
"Invalid two factor provider"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
let twofactor_code = match data.two_factor_token {
|
let twofactor_code = match data.two_factor_token {
|
||||||
Some(ref code) => code,
|
Some(ref code) => code,
|
||||||
|
|
@ -812,6 +714,7 @@ async fn twofactor_auth(
|
||||||
use crate::crypto::ct_eq;
|
use crate::crypto::ct_eq;
|
||||||
|
|
||||||
let selected_data = _selected_data(selected_twofactor);
|
let selected_data = _selected_data(selected_twofactor);
|
||||||
|
let mut remember = data.two_factor_remember.unwrap_or(0);
|
||||||
|
|
||||||
match TwoFactorType::from_i32(selected_id) {
|
match TwoFactorType::from_i32(selected_id) {
|
||||||
Some(TwoFactorType::Authenticator) => {
|
Some(TwoFactorType::Authenticator) => {
|
||||||
|
|
@ -843,23 +746,13 @@ async fn twofactor_auth(
|
||||||
}
|
}
|
||||||
Some(TwoFactorType::Remember) => {
|
Some(TwoFactorType::Remember) => {
|
||||||
match device.twofactor_remember {
|
match device.twofactor_remember {
|
||||||
// When a 2FA Remember token is used, check and validate this JWT token, if it is valid, just continue
|
Some(ref code) if !CONFIG.disable_2fa_remember() && ct_eq(code, twofactor_code) => {
|
||||||
// If it is invalid we need to trigger the 2FA Login prompt
|
remember = 1; // Make sure we also return the token here, otherwise it will only remember the first time
|
||||||
Some(ref token)
|
}
|
||||||
if !CONFIG.disable_2fa_remember()
|
|
||||||
&& (ct_eq(token, twofactor_code)
|
|
||||||
&& auth::decode_2fa_remember(twofactor_code)
|
|
||||||
.is_ok_and(|t| t.sub == device.uuid && t.user_uuid == user.uuid)) => {}
|
|
||||||
_ => {
|
_ => {
|
||||||
// Always delete the current twofactor remember token here if it exists
|
|
||||||
if device.twofactor_remember.is_some() {
|
|
||||||
device.delete_twofactor_remember();
|
|
||||||
// We need to save here, since we send a err_json!() which prevents saving `device` at a later stage
|
|
||||||
device.save(true, conn).await?;
|
|
||||||
}
|
|
||||||
err_json!(
|
err_json!(
|
||||||
_json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, conn).await?,
|
_json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, conn).await?,
|
||||||
"2FA Remember token not provided or expired"
|
"2FA Remember token not provided"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -890,10 +783,10 @@ async fn twofactor_auth(
|
||||||
|
|
||||||
TwoFactorIncomplete::mark_complete(&user.uuid, &device.uuid, conn).await?;
|
TwoFactorIncomplete::mark_complete(&user.uuid, &device.uuid, conn).await?;
|
||||||
|
|
||||||
let remember = data.two_factor_remember.unwrap_or(0);
|
|
||||||
let two_factor = if !CONFIG.disable_2fa_remember() && remember == 1 {
|
let two_factor = if !CONFIG.disable_2fa_remember() && remember == 1 {
|
||||||
Some(device.refresh_twofactor_remember())
|
Some(device.refresh_twofactor_remember())
|
||||||
} else {
|
} else {
|
||||||
|
device.delete_twofactor_remember();
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
Ok(two_factor)
|
Ok(two_factor)
|
||||||
|
|
@ -907,7 +800,7 @@ async fn _json_err_twofactor(
|
||||||
providers: &[i32],
|
providers: &[i32],
|
||||||
user_id: &UserId,
|
user_id: &UserId,
|
||||||
data: &ConnectData,
|
data: &ConnectData,
|
||||||
client_version: Option<&ClientVersion>,
|
client_version: &Option<ClientVersion>,
|
||||||
conn: &DbConn,
|
conn: &DbConn,
|
||||||
) -> ApiResult<Value> {
|
) -> ApiResult<Value> {
|
||||||
let mut result = json!({
|
let mut result = json!({
|
||||||
|
|
@ -926,7 +819,7 @@ async fn _json_err_twofactor(
|
||||||
match TwoFactorType::from_i32(*provider) {
|
match TwoFactorType::from_i32(*provider) {
|
||||||
Some(TwoFactorType::Authenticator) => { /* Nothing to do for TOTP */ }
|
Some(TwoFactorType::Authenticator) => { /* Nothing to do for TOTP */ }
|
||||||
|
|
||||||
Some(TwoFactorType::Webauthn) if CONFIG.is_webauthn_2fa_supported() => {
|
Some(TwoFactorType::Webauthn) if CONFIG.domain_set() => {
|
||||||
let request = webauthn::generate_webauthn_login(user_id, conn).await?;
|
let request = webauthn::generate_webauthn_login(user_id, conn).await?;
|
||||||
result["TwoFactorProviders2"][provider.to_string()] = request.0;
|
result["TwoFactorProviders2"][provider.to_string()] = request.0;
|
||||||
}
|
}
|
||||||
|
|
@ -1011,11 +904,6 @@ async fn prelogin(data: Json<PreloginData>, conn: DbConn) -> Json<Value> {
|
||||||
_prelogin(data, conn).await
|
_prelogin(data, conn).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/accounts/prelogin/password", data = "<data>")]
|
|
||||||
async fn prelogin_password(data: Json<PreloginData>, conn: DbConn) -> Json<Value> {
|
|
||||||
_prelogin(data, conn).await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/accounts/register", data = "<data>")]
|
#[post("/accounts/register", data = "<data>")]
|
||||||
async fn identity_register(data: Json<RegisterData>, conn: DbConn) -> JsonResult {
|
async fn identity_register(data: Json<RegisterData>, conn: DbConn) -> JsonResult {
|
||||||
_register(data, false, conn).await
|
_register(data, false, conn).await
|
||||||
|
|
@ -1031,7 +919,6 @@ struct RegisterVerificationData {
|
||||||
|
|
||||||
#[derive(rocket::Responder)]
|
#[derive(rocket::Responder)]
|
||||||
enum RegisterVerificationResponse {
|
enum RegisterVerificationResponse {
|
||||||
#[response(status = 204)]
|
|
||||||
NoContent(()),
|
NoContent(()),
|
||||||
Token(Json<String>),
|
Token(Json<String>),
|
||||||
}
|
}
|
||||||
|
|
@ -1059,11 +946,12 @@ async fn register_verification_email(
|
||||||
let user = User::find_by_mail(&data.email, &conn).await;
|
let user = User::find_by_mail(&data.email, &conn).await;
|
||||||
if user.filter(|u| u.private_key.is_some()).is_some() {
|
if user.filter(|u| u.private_key.is_some()).is_some() {
|
||||||
// There is still a timing side channel here in that the code
|
// There is still a timing side channel here in that the code
|
||||||
// paths that send mail take noticeably longer than ones that don't.
|
// paths that send mail take noticeably longer than ones that
|
||||||
// Add a randomized sleep to mitigate this somewhat.
|
// don't. Add a randomized sleep to mitigate this somewhat.
|
||||||
use rand::{rngs::SmallRng, RngExt};
|
use rand::{rngs::SmallRng, Rng, SeedableRng};
|
||||||
let mut rng: SmallRng = rand::make_rng();
|
let mut rng = SmallRng::from_os_rng();
|
||||||
let sleep_ms = rng.random_range(900..=1100) as u64;
|
let delta: i32 = 100;
|
||||||
|
let sleep_ms = (1_000 + rng.random_range(-delta..=delta)) as u64;
|
||||||
tokio::time::sleep(tokio::time::Duration::from_millis(sleep_ms)).await;
|
tokio::time::sleep(tokio::time::Duration::from_millis(sleep_ms)).await;
|
||||||
} else {
|
} else {
|
||||||
mail::send_register_verify_email(&data.email, &token).await?;
|
mail::send_register_verify_email(&data.email, &token).await?;
|
||||||
|
|
@ -1142,7 +1030,7 @@ struct ConnectData {
|
||||||
#[field(name = uncased("code_verifier"))]
|
#[field(name = uncased("code_verifier"))]
|
||||||
code_verifier: Option<OIDCCodeVerifier>,
|
code_verifier: Option<OIDCCodeVerifier>,
|
||||||
}
|
}
|
||||||
fn _check_is_some<T>(value: Option<&T>, msg: &str) -> EmptyResult {
|
fn _check_is_some<T>(value: &Option<T>, msg: &str) -> EmptyResult {
|
||||||
if value.is_none() {
|
if value.is_none() {
|
||||||
err!(msg)
|
err!(msg)
|
||||||
}
|
}
|
||||||
|
|
@ -1161,16 +1049,13 @@ fn prevalidate() -> JsonResult {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const SSO_BINDING_COOKIE: &str = "VW_SSO_BINDING";
|
|
||||||
|
|
||||||
#[get("/connect/oidc-signin?<code>&<state>", rank = 1)]
|
#[get("/connect/oidc-signin?<code>&<state>", rank = 1)]
|
||||||
async fn oidcsignin(code: OIDCCode, state: String, cookies: &CookieJar<'_>, mut conn: DbConn) -> ApiResult<Redirect> {
|
async fn oidcsignin(code: OIDCCode, state: String, mut conn: DbConn) -> ApiResult<Redirect> {
|
||||||
_oidcsignin_redirect(
|
_oidcsignin_redirect(
|
||||||
state,
|
state,
|
||||||
OIDCCodeWrapper::Ok {
|
OIDCCodeWrapper::Ok {
|
||||||
code,
|
code,
|
||||||
},
|
},
|
||||||
cookies,
|
|
||||||
&mut conn,
|
&mut conn,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
|
@ -1183,7 +1068,6 @@ async fn oidcsignin_error(
|
||||||
state: String,
|
state: String,
|
||||||
error: String,
|
error: String,
|
||||||
error_description: Option<String>,
|
error_description: Option<String>,
|
||||||
cookies: &CookieJar<'_>,
|
|
||||||
mut conn: DbConn,
|
mut conn: DbConn,
|
||||||
) -> ApiResult<Redirect> {
|
) -> ApiResult<Redirect> {
|
||||||
_oidcsignin_redirect(
|
_oidcsignin_redirect(
|
||||||
|
|
@ -1192,7 +1076,6 @@ async fn oidcsignin_error(
|
||||||
error,
|
error,
|
||||||
error_description,
|
error_description,
|
||||||
},
|
},
|
||||||
cookies,
|
|
||||||
&mut conn,
|
&mut conn,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
|
@ -1204,7 +1087,6 @@ async fn oidcsignin_error(
|
||||||
async fn _oidcsignin_redirect(
|
async fn _oidcsignin_redirect(
|
||||||
base64_state: String,
|
base64_state: String,
|
||||||
code_response: OIDCCodeWrapper,
|
code_response: OIDCCodeWrapper,
|
||||||
cookies: &CookieJar<'_>,
|
|
||||||
conn: &mut DbConn,
|
conn: &mut DbConn,
|
||||||
) -> ApiResult<Redirect> {
|
) -> ApiResult<Redirect> {
|
||||||
let state = sso::decode_state(&base64_state)?;
|
let state = sso::decode_state(&base64_state)?;
|
||||||
|
|
@ -1213,17 +1095,6 @@ async fn _oidcsignin_redirect(
|
||||||
None => err!(format!("Cannot retrieve sso_auth for {state}")),
|
None => err!(format!("Cannot retrieve sso_auth for {state}")),
|
||||||
Some(sso_auth) => sso_auth,
|
Some(sso_auth) => sso_auth,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Browser-binding check
|
|
||||||
// The cookie was set on /connect/authorize and must come from the same browser that initiated the flow.
|
|
||||||
let cookie_value = cookies.get(SSO_BINDING_COOKIE).map(|c| c.value().to_string());
|
|
||||||
let provided_hash = cookie_value.as_deref().map(|v| crypto::sha256_hex(v.as_bytes()));
|
|
||||||
match (sso_auth.binding_hash.as_deref(), provided_hash.as_deref()) {
|
|
||||||
(Some(expected), Some(actual)) if crypto::ct_eq(expected, actual) => {}
|
|
||||||
_ => err!(format!("SSO session binding mismatch for {state}")),
|
|
||||||
}
|
|
||||||
cookies.remove(Cookie::build(SSO_BINDING_COOKIE).path("/identity/connect/").build());
|
|
||||||
|
|
||||||
sso_auth.code_response = Some(code_response);
|
sso_auth.code_response = Some(code_response);
|
||||||
sso_auth.updated_at = Utc::now().naive_utc();
|
sso_auth.updated_at = Utc::now().naive_utc();
|
||||||
sso_auth.save(conn).await?;
|
sso_auth.save(conn).await?;
|
||||||
|
|
@ -1270,7 +1141,7 @@ struct AuthorizeData {
|
||||||
|
|
||||||
// The `redirect_uri` will change depending of the client (web, android, ios ..)
|
// The `redirect_uri` will change depending of the client (web, android, ios ..)
|
||||||
#[get("/connect/authorize?<data..>")]
|
#[get("/connect/authorize?<data..>")]
|
||||||
async fn authorize(data: AuthorizeData, cookies: &CookieJar<'_>, secure: Secure, conn: DbConn) -> ApiResult<Redirect> {
|
async fn authorize(data: AuthorizeData, conn: DbConn) -> ApiResult<Redirect> {
|
||||||
let AuthorizeData {
|
let AuthorizeData {
|
||||||
client_id,
|
client_id,
|
||||||
redirect_uri,
|
redirect_uri,
|
||||||
|
|
@ -1284,23 +1155,7 @@ async fn authorize(data: AuthorizeData, cookies: &CookieJar<'_>, secure: Secure,
|
||||||
err!("Unsupported code challenge method");
|
err!("Unsupported code challenge method");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate browser-binding token. Stored hashed in DB; raw value handed to the browser as a cookie.
|
let auth_url = sso::authorize_url(state, code_challenge, &client_id, &redirect_uri, conn).await?;
|
||||||
// Validated on /connect/oidc-signin
|
|
||||||
let binding_token = data_encoding::BASE64URL_NOPAD.encode(&crypto::get_random_bytes::<32>());
|
|
||||||
let binding_hash = crypto::sha256_hex(binding_token.as_bytes());
|
|
||||||
|
|
||||||
let auth_url =
|
|
||||||
sso::authorize_url(state, code_challenge, &client_id, &redirect_uri, Some(binding_hash), conn).await?;
|
|
||||||
|
|
||||||
cookies.add(
|
|
||||||
Cookie::build((SSO_BINDING_COOKIE, binding_token))
|
|
||||||
.path("/identity/connect/")
|
|
||||||
.max_age(time::Duration::seconds(sso::SSO_AUTH_EXPIRATION.num_seconds()))
|
|
||||||
.same_site(SameSite::Lax) // Lax is needed because the IdP runs on a different FQDN
|
|
||||||
.http_only(true)
|
|
||||||
.secure(secure.https)
|
|
||||||
.build(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(Redirect::temporary(String::from(auth_url)))
|
Ok(Redirect::temporary(String::from(auth_url)))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,6 @@ pub type EmptyResult = ApiResult<()>;
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct PasswordOrOtpData {
|
struct PasswordOrOtpData {
|
||||||
#[serde(alias = "MasterPasswordHash")]
|
|
||||||
master_password_hash: Option<String>,
|
master_password_hash: Option<String>,
|
||||||
otp: Option<String>,
|
otp: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -338,7 +338,7 @@ impl WebSocketUsers {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: The last modified date needs to be updated before calling these methods
|
// NOTE: The last modified date needs to be updated before calling these methods
|
||||||
pub async fn send_user_update(&self, ut: UpdateType, user: &User, push_uuid: Option<&PushId>, conn: &DbConn) {
|
pub async fn send_user_update(&self, ut: UpdateType, user: &User, push_uuid: &Option<PushId>, conn: &DbConn) {
|
||||||
// Skip any processing if both WebSockets and Push are not active
|
// Skip any processing if both WebSockets and Push are not active
|
||||||
if *NOTIFICATIONS_DISABLED {
|
if *NOTIFICATIONS_DISABLED {
|
||||||
return;
|
return;
|
||||||
|
|
@ -358,16 +358,15 @@ impl WebSocketUsers {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn send_logout(&self, user: &User, acting_device: Option<&Device>, conn: &DbConn) {
|
pub async fn send_logout(&self, user: &User, acting_device_id: Option<DeviceId>, conn: &DbConn) {
|
||||||
// Skip any processing if both WebSockets and Push are not active
|
// Skip any processing if both WebSockets and Push are not active
|
||||||
if *NOTIFICATIONS_DISABLED {
|
if *NOTIFICATIONS_DISABLED {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let acting_device_id = acting_device.map(|d| d.uuid.clone());
|
|
||||||
let data = create_update(
|
let data = create_update(
|
||||||
vec![("UserId".into(), user.uuid.to_string().into()), ("Date".into(), serialize_date(user.updated_at))],
|
vec![("UserId".into(), user.uuid.to_string().into()), ("Date".into(), serialize_date(user.updated_at))],
|
||||||
UpdateType::LogOut,
|
UpdateType::LogOut,
|
||||||
acting_device_id,
|
acting_device_id.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
if CONFIG.enable_websocket() {
|
if CONFIG.enable_websocket() {
|
||||||
|
|
@ -375,7 +374,7 @@ impl WebSocketUsers {
|
||||||
}
|
}
|
||||||
|
|
||||||
if CONFIG.push_enabled() {
|
if CONFIG.push_enabled() {
|
||||||
push_logout(user, acting_device, conn).await;
|
push_logout(user, acting_device_id.clone(), conn).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ use tokio::sync::RwLock;
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{ApiResult, EmptyResult, UpdateType},
|
api::{ApiResult, EmptyResult, UpdateType},
|
||||||
db::{
|
db::{
|
||||||
models::{AuthRequestId, Cipher, Device, Folder, PushId, Send, User, UserId},
|
models::{AuthRequestId, Cipher, Device, DeviceId, Folder, PushId, Send, User, UserId},
|
||||||
DbConn,
|
DbConn,
|
||||||
},
|
},
|
||||||
http_client::make_http_request,
|
http_client::make_http_request,
|
||||||
|
|
@ -135,7 +135,7 @@ pub async fn register_push_device(device: &mut Device, conn: &DbConn) -> EmptyRe
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn unregister_push_device(push_id: Option<&PushId>) -> EmptyResult {
|
pub async fn unregister_push_device(push_id: &Option<PushId>) -> EmptyResult {
|
||||||
if !CONFIG.push_enabled() || push_id.is_none() {
|
if !CONFIG.push_enabled() || push_id.is_none() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
@ -188,13 +188,15 @@ pub async fn push_cipher_update(ut: UpdateType, cipher: &Cipher, device: &Device
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn push_logout(user: &User, acting_device: Option<&Device>, conn: &DbConn) {
|
pub async fn push_logout(user: &User, acting_device_id: Option<DeviceId>, conn: &DbConn) {
|
||||||
|
let acting_device_id: Value = acting_device_id.map(|v| v.to_string().into()).unwrap_or_else(|| Value::Null);
|
||||||
|
|
||||||
if Device::check_user_has_push_device(&user.uuid, conn).await {
|
if Device::check_user_has_push_device(&user.uuid, conn).await {
|
||||||
tokio::task::spawn(send_to_push_relay(json!({
|
tokio::task::spawn(send_to_push_relay(json!({
|
||||||
"userId": user.uuid,
|
"userId": user.uuid,
|
||||||
"organizationId": (),
|
"organizationId": (),
|
||||||
"deviceId": acting_device.and_then(|d| d.push_uuid.as_ref()),
|
"deviceId": acting_device_id,
|
||||||
"identifier": acting_device.map(|d| &d.uuid),
|
"identifier": acting_device_id,
|
||||||
"type": UpdateType::LogOut as i32,
|
"type": UpdateType::LogOut as i32,
|
||||||
"payload": {
|
"payload": {
|
||||||
"userId": user.uuid,
|
"userId": user.uuid,
|
||||||
|
|
@ -206,7 +208,7 @@ pub async fn push_logout(user: &User, acting_device: Option<&Device>, conn: &DbC
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn push_user_update(ut: UpdateType, user: &User, push_uuid: Option<&PushId>, conn: &DbConn) {
|
pub async fn push_user_update(ut: UpdateType, user: &User, push_uuid: &Option<PushId>, conn: &DbConn) {
|
||||||
if Device::check_user_has_push_device(&user.uuid, conn).await {
|
if Device::check_user_has_push_device(&user.uuid, conn).await {
|
||||||
tokio::task::spawn(send_to_push_relay(json!({
|
tokio::task::spawn(send_to_push_relay(json!({
|
||||||
"userId": user.uuid,
|
"userId": user.uuid,
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ use serde_json::Value;
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{core::now, ApiResult, EmptyResult},
|
api::{core::now, ApiResult, EmptyResult},
|
||||||
auth::decode_file_download,
|
auth::decode_file_download,
|
||||||
|
config::CachedConfigOperation,
|
||||||
db::models::{AttachmentId, CipherId},
|
db::models::{AttachmentId, CipherId},
|
||||||
error::Error,
|
error::Error,
|
||||||
util::Cached,
|
util::Cached,
|
||||||
|
|
@ -52,21 +53,18 @@ fn not_found() -> ApiResult<Html<String>> {
|
||||||
Ok(Html(text))
|
Ok(Html(text))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/css/vaultwarden.css")]
|
static VAULTWARDEN_CSS_CACHE: CachedConfigOperation<String> = CachedConfigOperation::new(|config| {
|
||||||
fn vaultwarden_css() -> Cached<Css<String>> {
|
|
||||||
let css_options = json!({
|
let css_options = json!({
|
||||||
"emergency_access_allowed": CONFIG.emergency_access_allowed(),
|
"emergency_access_allowed": config.emergency_access_allowed(),
|
||||||
"load_user_scss": true,
|
"load_user_scss": true,
|
||||||
"mail_2fa_enabled": CONFIG._enable_email_2fa(),
|
"mail_2fa_enabled": config._enable_email_2fa(),
|
||||||
"mail_enabled": CONFIG.mail_enabled(),
|
"mail_enabled": config.mail_enabled(),
|
||||||
"sends_allowed": CONFIG.sends_allowed(),
|
"sends_allowed": config.sends_allowed(),
|
||||||
"remember_2fa_disabled": CONFIG.disable_2fa_remember(),
|
"signup_disabled": config.is_signup_disabled(),
|
||||||
"password_hints_allowed": CONFIG.password_hints_allowed(),
|
"sso_enabled": config.sso_enabled(),
|
||||||
"signup_disabled": CONFIG.is_signup_disabled(),
|
"sso_only": config.sso_enabled() && config.sso_only(),
|
||||||
"sso_enabled": CONFIG.sso_enabled(),
|
"yubico_enabled": config._enable_yubico() && config.yubico_client_id().is_some() && config.yubico_secret_key().is_some(),
|
||||||
"sso_only": CONFIG.sso_enabled() && CONFIG.sso_only(),
|
"webauthn_2fa_supported": config.is_webauthn_2fa_supported(),
|
||||||
"webauthn_2fa_supported": CONFIG.is_webauthn_2fa_supported(),
|
|
||||||
"yubico_enabled": CONFIG._enable_yubico() && CONFIG.yubico_client_id().is_some() && CONFIG.yubico_secret_key().is_some(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let scss = match CONFIG.render_template("scss/vaultwarden.scss", &css_options) {
|
let scss = match CONFIG.render_template("scss/vaultwarden.scss", &css_options) {
|
||||||
|
|
@ -80,7 +78,7 @@ fn vaultwarden_css() -> Cached<Css<String>> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let css = match grass_compiler::from_string(
|
match grass_compiler::from_string(
|
||||||
scss,
|
scss,
|
||||||
&grass_compiler::Options::default().style(grass_compiler::OutputStyle::Compressed),
|
&grass_compiler::Options::default().style(grass_compiler::OutputStyle::Compressed),
|
||||||
) {
|
) {
|
||||||
|
|
@ -99,10 +97,12 @@ fn vaultwarden_css() -> Cached<Css<String>> {
|
||||||
)
|
)
|
||||||
.expect("SCSS to compile")
|
.expect("SCSS to compile")
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Cache for one day should be enough and not too much
|
#[get("/css/vaultwarden.css")]
|
||||||
Cached::ttl(Css(css), 86_400, false)
|
fn vaultwarden_css() -> Css<String> {
|
||||||
|
Css(CONFIG.cached_operation(&VAULTWARDEN_CSS_CACHE))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/")]
|
#[get("/")]
|
||||||
|
|
@ -240,8 +240,8 @@ pub fn static_files(filename: &str) -> Result<(ContentType, &'static [u8]), Erro
|
||||||
"jdenticon-3.3.0.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/jdenticon-3.3.0.js"))),
|
"jdenticon-3.3.0.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/jdenticon-3.3.0.js"))),
|
||||||
"datatables.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/datatables.js"))),
|
"datatables.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/datatables.js"))),
|
||||||
"datatables.css" => Ok((ContentType::CSS, include_bytes!("../static/scripts/datatables.css"))),
|
"datatables.css" => Ok((ContentType::CSS, include_bytes!("../static/scripts/datatables.css"))),
|
||||||
"jquery-4.0.0.slim.js" => {
|
"jquery-3.7.1.slim.js" => {
|
||||||
Ok((ContentType::JavaScript, include_bytes!("../static/scripts/jquery-4.0.0.slim.js")))
|
Ok((ContentType::JavaScript, include_bytes!("../static/scripts/jquery-3.7.1.slim.js")))
|
||||||
}
|
}
|
||||||
_ => err!(format!("Static file not found: {filename}")),
|
_ => err!(format!("Static file not found: {filename}")),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
103
src/auth.rs
103
src/auth.rs
|
|
@ -46,7 +46,6 @@ static JWT_FILE_DOWNLOAD_ISSUER: LazyLock<String> =
|
||||||
LazyLock::new(|| format!("{}|file_download", CONFIG.domain_origin()));
|
LazyLock::new(|| format!("{}|file_download", CONFIG.domain_origin()));
|
||||||
static JWT_REGISTER_VERIFY_ISSUER: LazyLock<String> =
|
static JWT_REGISTER_VERIFY_ISSUER: LazyLock<String> =
|
||||||
LazyLock::new(|| format!("{}|register_verify", CONFIG.domain_origin()));
|
LazyLock::new(|| format!("{}|register_verify", CONFIG.domain_origin()));
|
||||||
static JWT_2FA_REMEMBER_ISSUER: LazyLock<String> = LazyLock::new(|| format!("{}|2faremember", CONFIG.domain_origin()));
|
|
||||||
|
|
||||||
static PRIVATE_RSA_KEY: OnceLock<EncodingKey> = OnceLock::new();
|
static PRIVATE_RSA_KEY: OnceLock<EncodingKey> = OnceLock::new();
|
||||||
static PUBLIC_RSA_KEY: OnceLock<DecodingKey> = OnceLock::new();
|
static PUBLIC_RSA_KEY: OnceLock<DecodingKey> = OnceLock::new();
|
||||||
|
|
@ -161,10 +160,6 @@ pub fn decode_register_verify(token: &str) -> Result<RegisterVerifyClaims, Error
|
||||||
decode_jwt(token, JWT_REGISTER_VERIFY_ISSUER.to_string())
|
decode_jwt(token, JWT_REGISTER_VERIFY_ISSUER.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn decode_2fa_remember(token: &str) -> Result<TwoFactorRememberClaims, Error> {
|
|
||||||
decode_jwt(token, JWT_2FA_REMEMBER_ISSUER.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct LoginJwtClaims {
|
pub struct LoginJwtClaims {
|
||||||
// Not before
|
// Not before
|
||||||
|
|
@ -445,31 +440,6 @@ pub fn generate_register_verify_claims(email: String, name: Option<String>, veri
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
pub struct TwoFactorRememberClaims {
|
|
||||||
// Not before
|
|
||||||
pub nbf: i64,
|
|
||||||
// Expiration time
|
|
||||||
pub exp: i64,
|
|
||||||
// Issuer
|
|
||||||
pub iss: String,
|
|
||||||
// Subject
|
|
||||||
pub sub: DeviceId,
|
|
||||||
// UserId
|
|
||||||
pub user_uuid: UserId,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn generate_2fa_remember_claims(device_uuid: DeviceId, user_uuid: UserId) -> TwoFactorRememberClaims {
|
|
||||||
let time_now = Utc::now();
|
|
||||||
TwoFactorRememberClaims {
|
|
||||||
nbf: time_now.timestamp(),
|
|
||||||
exp: (time_now + TimeDelta::try_days(30).unwrap()).timestamp(),
|
|
||||||
iss: JWT_2FA_REMEMBER_ISSUER.to_string(),
|
|
||||||
sub: device_uuid,
|
|
||||||
user_uuid,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct BasicJwtClaims {
|
pub struct BasicJwtClaims {
|
||||||
// Not before
|
// Not before
|
||||||
|
|
@ -704,9 +674,10 @@ pub struct OrgHeaders {
|
||||||
|
|
||||||
impl OrgHeaders {
|
impl OrgHeaders {
|
||||||
fn is_member(&self) -> bool {
|
fn is_member(&self) -> bool {
|
||||||
// Only allow not revoked members, we can not use the Confirmed status here
|
// NOTE: we don't care about MembershipStatus at the moment because this is only used
|
||||||
// as some endpoints can be triggered by invited users during joining
|
// where an invited, accepted or confirmed user is expected if this ever changes or
|
||||||
self.membership_status != MembershipStatus::Revoked && self.membership_type >= MembershipType::User
|
// if from_i32 is changed to return Some(Revoked) this check needs to be changed accordingly
|
||||||
|
self.membership_type >= MembershipType::User
|
||||||
}
|
}
|
||||||
fn is_confirmed_and_admin(&self) -> bool {
|
fn is_confirmed_and_admin(&self) -> bool {
|
||||||
self.membership_status == MembershipStatus::Confirmed && self.membership_type >= MembershipType::Admin
|
self.membership_status == MembershipStatus::Confirmed && self.membership_type >= MembershipType::Admin
|
||||||
|
|
@ -719,36 +690,6 @@ impl OrgHeaders {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// org_id is usually the second path param ("/organizations/<org_id>"),
|
|
||||||
// but there are cases where it is a query value.
|
|
||||||
// First check the path, if this is not a valid uuid, try the query values.
|
|
||||||
fn get_org_id(request: &Request<'_>) -> Option<OrganizationId> {
|
|
||||||
if let Some(Ok(org_id)) = request.param::<OrganizationId>(1) {
|
|
||||||
Some(org_id)
|
|
||||||
} else if let Some(Ok(org_id)) = request.query_value::<OrganizationId>("organizationId") {
|
|
||||||
Some(org_id)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Special Guard to ensure that there is an organization id present
|
|
||||||
// If there is no org id trigger the Outcome::Forward.
|
|
||||||
// This is useful for endpoints which work for both organization and personal vaults, like purge.
|
|
||||||
pub struct OrgIdGuard;
|
|
||||||
|
|
||||||
#[rocket::async_trait]
|
|
||||||
impl<'r> FromRequest<'r> for OrgIdGuard {
|
|
||||||
type Error = &'static str;
|
|
||||||
|
|
||||||
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
|
||||||
match get_org_id(request) {
|
|
||||||
Some(_) => Outcome::Success(OrgIdGuard),
|
|
||||||
None => Outcome::Forward(rocket::http::Status::NotFound),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rocket::async_trait]
|
#[rocket::async_trait]
|
||||||
impl<'r> FromRequest<'r> for OrgHeaders {
|
impl<'r> FromRequest<'r> for OrgHeaders {
|
||||||
type Error = &'static str;
|
type Error = &'static str;
|
||||||
|
|
@ -756,8 +697,18 @@ impl<'r> FromRequest<'r> for OrgHeaders {
|
||||||
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||||
let headers = try_outcome!(Headers::from_request(request).await);
|
let headers = try_outcome!(Headers::from_request(request).await);
|
||||||
|
|
||||||
// Extract the org_id from the request
|
// org_id is usually the second path param ("/organizations/<org_id>"),
|
||||||
let url_org_id = get_org_id(request);
|
// but there are cases where it is a query value.
|
||||||
|
// First check the path, if this is not a valid uuid, try the query values.
|
||||||
|
let url_org_id: Option<OrganizationId> = {
|
||||||
|
if let Some(Ok(org_id)) = request.param::<OrganizationId>(1) {
|
||||||
|
Some(org_id)
|
||||||
|
} else if let Some(Ok(org_id)) = request.query_value::<OrganizationId>("organizationId") {
|
||||||
|
Some(org_id)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
match url_org_id {
|
match url_org_id {
|
||||||
Some(org_id) if uuid::Uuid::parse_str(&org_id).is_ok() => {
|
Some(org_id) if uuid::Uuid::parse_str(&org_id).is_ok() => {
|
||||||
|
|
@ -875,7 +826,7 @@ impl<'r> FromRequest<'r> for ManagerHeaders {
|
||||||
_ => err_handler!("Error getting DB"),
|
_ => err_handler!("Error getting DB"),
|
||||||
};
|
};
|
||||||
|
|
||||||
if !Collection::is_coll_manageable_by_user(&col_id, &headers.membership.user_uuid, &conn).await {
|
if !Collection::can_access_collection(&headers.membership, &col_id, &conn).await {
|
||||||
err_handler!("The current user isn't a manager for this collection")
|
err_handler!("The current user isn't a manager for this collection")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -957,8 +908,8 @@ impl ManagerHeaders {
|
||||||
if uuid::Uuid::parse_str(col_id.as_ref()).is_err() {
|
if uuid::Uuid::parse_str(col_id.as_ref()).is_err() {
|
||||||
err!("Collection Id is malformed!");
|
err!("Collection Id is malformed!");
|
||||||
}
|
}
|
||||||
if !Collection::is_coll_manageable_by_user(col_id, &h.membership.user_uuid, conn).await {
|
if !Collection::can_access_collection(&h.membership, col_id, conn).await {
|
||||||
err!("Collection not found", "The current user isn't a manager for this collection")
|
err!("You don't have access to all collections!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1259,20 +1210,8 @@ pub async fn refresh_tokens(
|
||||||
) -> ApiResult<(Device, AuthTokens)> {
|
) -> ApiResult<(Device, AuthTokens)> {
|
||||||
let refresh_claims = match decode_refresh(refresh_token) {
|
let refresh_claims = match decode_refresh(refresh_token) {
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("Failed to decode {} refresh_token: {refresh_token}: {err:?}", ip.ip);
|
debug!("Failed to decode {} refresh_token: {refresh_token}", ip.ip);
|
||||||
//err_silent!(format!("Impossible to read refresh_token: {}", err.message()))
|
err_silent!(format!("Impossible to read refresh_token: {}", err.message()))
|
||||||
|
|
||||||
// If the token failed to decode, it was probably one of the old style tokens that was just a Base64 string.
|
|
||||||
// We can generate a claim for them for backwards compatibility. Note that the password refresh claims don't
|
|
||||||
// check expiration or issuer, so they're not included here.
|
|
||||||
RefreshJwtClaims {
|
|
||||||
nbf: 0,
|
|
||||||
exp: 0,
|
|
||||||
iss: String::new(),
|
|
||||||
sub: AuthMethod::Password,
|
|
||||||
device_token: refresh_token.into(),
|
|
||||||
token: None,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Ok(claims) => claims,
|
Ok(claims) => claims,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
182
src/config.rs
182
src/config.rs
|
|
@ -3,7 +3,7 @@ use std::{
|
||||||
fmt,
|
fmt,
|
||||||
process::exit,
|
process::exit,
|
||||||
sync::{
|
sync::{
|
||||||
atomic::{AtomicBool, Ordering},
|
atomic::{AtomicBool, AtomicUsize, Ordering},
|
||||||
LazyLock, RwLock,
|
LazyLock, RwLock,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -14,10 +14,7 @@ use serde::de::{self, Deserialize, Deserializer, MapAccess, Visitor};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
error::Error,
|
error::Error,
|
||||||
util::{
|
util::{get_env, get_env_bool, get_web_vault_version, is_valid_email, parse_experimental_client_feature_flags},
|
||||||
get_active_web_release, get_env, get_env_bool, is_valid_email, parse_experimental_client_feature_flags,
|
|
||||||
FeatureFlagFilter,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static CONFIG_FILE: LazyLock<String> = LazyLock::new(|| {
|
static CONFIG_FILE: LazyLock<String> = LazyLock::new(|| {
|
||||||
|
|
@ -106,6 +103,7 @@ macro_rules! make_config {
|
||||||
|
|
||||||
struct Inner {
|
struct Inner {
|
||||||
rocket_shutdown_handle: Option<rocket::Shutdown>,
|
rocket_shutdown_handle: Option<rocket::Shutdown>,
|
||||||
|
revision: usize,
|
||||||
|
|
||||||
templates: Handlebars<'static>,
|
templates: Handlebars<'static>,
|
||||||
config: ConfigItems,
|
config: ConfigItems,
|
||||||
|
|
@ -325,7 +323,7 @@ macro_rules! make_config {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
struct ConfigItems { $($( $name: make_config! {@type $ty, $none_action}, )+)+ }
|
struct ConfigItems { $($( pub $name: make_config! {@type $ty, $none_action}, )+)+ }
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct ElementDoc {
|
struct ElementDoc {
|
||||||
|
|
@ -923,7 +921,7 @@ make_config! {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> {
|
fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
|
||||||
// Validate connection URL is valid and DB feature is enabled
|
// Validate connection URL is valid and DB feature is enabled
|
||||||
#[cfg(sqlite)]
|
#[cfg(sqlite)]
|
||||||
{
|
{
|
||||||
|
|
@ -1029,17 +1027,33 @@ fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let invalid_flags =
|
// Server (v2025.6.2): https://github.com/bitwarden/server/blob/d094be3267f2030bd0dc62106bc6871cf82682f5/src/Core/Constants.cs#L103
|
||||||
parse_experimental_client_feature_flags(&cfg.experimental_client_feature_flags, FeatureFlagFilter::InvalidOnly);
|
// Client (web-v2025.6.1): https://github.com/bitwarden/clients/blob/747c2fd6a1c348a57a76e4a7de8128466ffd3c01/libs/common/src/enums/feature-flag.enum.ts#L12
|
||||||
|
// Android (v2025.6.0): https://github.com/bitwarden/android/blob/b5b022caaad33390c31b3021b2c1205925b0e1a2/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/model/FlagKey.kt#L22
|
||||||
|
// iOS (v2025.6.0): https://github.com/bitwarden/ios/blob/ff06d9c6cc8da89f78f37f376495800201d7261a/BitwardenShared/Core/Platform/Models/Enum/FeatureFlag.swift#L7
|
||||||
|
//
|
||||||
|
// NOTE: Move deprecated flags to the utils::parse_experimental_client_feature_flags() DEPRECATED_FLAGS const!
|
||||||
|
const KNOWN_FLAGS: &[&str] = &[
|
||||||
|
// Autofill Team
|
||||||
|
"inline-menu-positioning-improvements",
|
||||||
|
"inline-menu-totp",
|
||||||
|
"ssh-agent",
|
||||||
|
// Key Management Team
|
||||||
|
"ssh-key-vault-item",
|
||||||
|
"pm-25373-windows-biometrics-v2",
|
||||||
|
// Tools
|
||||||
|
"export-attachments",
|
||||||
|
// Mobile Team
|
||||||
|
"anon-addy-self-host-alias",
|
||||||
|
"simple-login-self-host-alias",
|
||||||
|
"mutual-tls",
|
||||||
|
];
|
||||||
|
let configured_flags = parse_experimental_client_feature_flags(&cfg.experimental_client_feature_flags);
|
||||||
|
let invalid_flags: Vec<_> = configured_flags.keys().filter(|flag| !KNOWN_FLAGS.contains(&flag.as_str())).collect();
|
||||||
if !invalid_flags.is_empty() {
|
if !invalid_flags.is_empty() {
|
||||||
let feature_flags_error = format!("Unrecognized experimental client feature flags: {:?}.\n\
|
err!(format!("Unrecognized experimental client feature flags: {invalid_flags:?}.\n\n\
|
||||||
Please ensure all feature flags are spelled correctly and that they are supported in this version.\n\
|
Please ensure all feature flags are spelled correctly and that they are supported in this version.\n\
|
||||||
Supported flags: {:?}\n", invalid_flags, SUPPORTED_FEATURE_FLAGS);
|
Supported flags: {KNOWN_FLAGS:?}"));
|
||||||
if on_update {
|
|
||||||
err!(feature_flags_error);
|
|
||||||
} else {
|
|
||||||
println!("[WARNING] {feature_flags_error}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_FILESIZE_KB: i64 = i64::MAX >> 10;
|
const MAX_FILESIZE_KB: i64 = i64::MAX >> 10;
|
||||||
|
|
@ -1076,7 +1090,7 @@ fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> {
|
||||||
|
|
||||||
validate_internal_sso_issuer_url(&cfg.sso_authority)?;
|
validate_internal_sso_issuer_url(&cfg.sso_authority)?;
|
||||||
validate_internal_sso_redirect_url(&cfg.sso_callback_path)?;
|
validate_internal_sso_redirect_url(&cfg.sso_callback_path)?;
|
||||||
validate_sso_master_password_policy(cfg.sso_master_password_policy.as_ref())?;
|
validate_sso_master_password_policy(&cfg.sso_master_password_policy)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg._enable_yubico {
|
if cfg._enable_yubico {
|
||||||
|
|
@ -1271,7 +1285,7 @@ fn validate_internal_sso_redirect_url(sso_callback_path: &String) -> Result<open
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_sso_master_password_policy(
|
fn validate_sso_master_password_policy(
|
||||||
sso_master_password_policy: Option<&String>,
|
sso_master_password_policy: &Option<String>,
|
||||||
) -> Result<Option<serde_json::Value>, Error> {
|
) -> Result<Option<serde_json::Value>, Error> {
|
||||||
let policy = sso_master_password_policy.as_ref().map(|mpp| serde_json::from_str::<serde_json::Value>(mpp));
|
let policy = sso_master_password_policy.as_ref().map(|mpp| serde_json::from_str::<serde_json::Value>(mpp));
|
||||||
|
|
||||||
|
|
@ -1312,16 +1326,12 @@ fn generate_smtp_img_src(embed_images: bool, domain: &str) -> String {
|
||||||
if embed_images {
|
if embed_images {
|
||||||
"cid:".to_string()
|
"cid:".to_string()
|
||||||
} else {
|
} else {
|
||||||
// normalize base_url
|
format!("{domain}/vw_static/")
|
||||||
let base_url = domain.trim_end_matches('/');
|
|
||||||
format!("{base_url}/vw_static/")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_sso_callback_path(domain: &str) -> String {
|
fn generate_sso_callback_path(domain: &str) -> String {
|
||||||
// normalize base_url
|
format!("{domain}/identity/connect/oidc-signin")
|
||||||
let base_url = domain.trim_end_matches('/');
|
|
||||||
format!("{base_url}/identity/connect/oidc-signin")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate the correct URL for the icon service.
|
/// Generate the correct URL for the icon service.
|
||||||
|
|
@ -1458,34 +1468,22 @@ pub enum PathType {
|
||||||
RsaKey,
|
RsaKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Official available feature flags can be found here:
|
pub struct CachedConfigOperation<T: Clone> {
|
||||||
// Server (v2026.2.1): https://github.com/bitwarden/server/blob/0e42725d0837bd1c0dabd864ff621a579959744b/src/Core/Constants.cs#L135
|
generator: fn(&Config) -> T,
|
||||||
// Client (v2026.2.1): https://github.com/bitwarden/clients/blob/f96380c3138291a028bdd2c7a5fee540d5c98ba5/libs/common/src/enums/feature-flag.enum.ts#L12
|
value_cache: RwLock<Option<T>>,
|
||||||
// Android (v2026.2.1): https://github.com/bitwarden/android/blob/6902c19c0093fa476bbf74ccaa70c9f14afbb82f/core/src/main/kotlin/com/bitwarden/core/data/manager/model/FlagKey.kt#L31
|
revision: AtomicUsize,
|
||||||
// iOS (v2026.2.1): https://github.com/bitwarden/ios/blob/cdd9ba1770ca2ffc098d02d12cc3208e3a830454/BitwardenShared/Core/Platform/Models/Enum/FeatureFlag.swift#L7
|
}
|
||||||
pub const SUPPORTED_FEATURE_FLAGS: &[&str] = &[
|
|
||||||
// Architecture
|
impl<T: Clone> CachedConfigOperation<T> {
|
||||||
"desktop-ui-migration-milestone-1",
|
#[allow(private_interfaces)]
|
||||||
"desktop-ui-migration-milestone-2",
|
pub const fn new(generator: fn(&Config) -> T) -> Self {
|
||||||
"desktop-ui-migration-milestone-3",
|
CachedConfigOperation {
|
||||||
"desktop-ui-migration-milestone-4",
|
generator,
|
||||||
// Auth Team
|
value_cache: RwLock::new(None),
|
||||||
"pm-5594-safari-account-switching",
|
revision: AtomicUsize::new(0),
|
||||||
// Autofill Team
|
}
|
||||||
"ssh-agent",
|
}
|
||||||
"ssh-agent-v2",
|
}
|
||||||
// Key Management Team
|
|
||||||
"ssh-key-vault-item",
|
|
||||||
"pm-25373-windows-biometrics-v2",
|
|
||||||
// Mobile Team
|
|
||||||
"anon-addy-self-host-alias",
|
|
||||||
"simple-login-self-host-alias",
|
|
||||||
"mutual-tls",
|
|
||||||
"cxp-import-mobile",
|
|
||||||
"cxp-export-mobile",
|
|
||||||
// Platform Team
|
|
||||||
"pm-30529-webauthn-related-origins",
|
|
||||||
];
|
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
pub async fn load() -> Result<Self, Error> {
|
pub async fn load() -> Result<Self, Error> {
|
||||||
|
|
@ -1500,12 +1498,13 @@ impl Config {
|
||||||
// Fill any missing with defaults
|
// Fill any missing with defaults
|
||||||
let config = builder.build();
|
let config = builder.build();
|
||||||
if !SKIP_CONFIG_VALIDATION.load(Ordering::Relaxed) {
|
if !SKIP_CONFIG_VALIDATION.load(Ordering::Relaxed) {
|
||||||
validate_config(&config, false)?;
|
validate_config(&config)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Config {
|
Ok(Config {
|
||||||
inner: RwLock::new(Inner {
|
inner: RwLock::new(Inner {
|
||||||
rocket_shutdown_handle: None,
|
rocket_shutdown_handle: None,
|
||||||
|
revision: 1,
|
||||||
templates: load_templates(&config.templates_folder),
|
templates: load_templates(&config.templates_folder),
|
||||||
config,
|
config,
|
||||||
_env,
|
_env,
|
||||||
|
|
@ -1536,7 +1535,7 @@ impl Config {
|
||||||
let env = &self.inner.read().unwrap()._env;
|
let env = &self.inner.read().unwrap()._env;
|
||||||
env.merge(&builder, false, &mut overrides).build()
|
env.merge(&builder, false, &mut overrides).build()
|
||||||
};
|
};
|
||||||
validate_config(&config, true)?;
|
validate_config(&config)?;
|
||||||
|
|
||||||
// Save both the user and the combined config
|
// Save both the user and the combined config
|
||||||
{
|
{
|
||||||
|
|
@ -1544,6 +1543,7 @@ impl Config {
|
||||||
writer.config = config;
|
writer.config = config;
|
||||||
writer._usr = builder;
|
writer._usr = builder;
|
||||||
writer._overrides = overrides;
|
writer._overrides = overrides;
|
||||||
|
writer.revision += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
//Save to file
|
//Save to file
|
||||||
|
|
@ -1562,6 +1562,51 @@ impl Config {
|
||||||
self.update_config(builder, false).await
|
self.update_config(builder, false).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn delete_user_config(&self) -> Result<(), Error> {
|
||||||
|
let operator = opendal_operator_for_path(&CONFIG_FILE_PARENT_DIR)?;
|
||||||
|
operator.delete(&CONFIG_FILENAME).await?;
|
||||||
|
|
||||||
|
// Empty user config
|
||||||
|
let usr = ConfigBuilder::default();
|
||||||
|
|
||||||
|
// Config now is env + defaults
|
||||||
|
let config = {
|
||||||
|
let env = &self.inner.read().unwrap()._env;
|
||||||
|
env.build()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save configs
|
||||||
|
{
|
||||||
|
let mut writer = self.inner.write().unwrap();
|
||||||
|
writer.config = config;
|
||||||
|
writer._usr = usr;
|
||||||
|
writer._overrides = Vec::new();
|
||||||
|
writer.revision += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cached_operation<T: Clone>(&self, operation: &CachedConfigOperation<T>) -> T {
|
||||||
|
let config_revision = self.inner.read().unwrap().revision;
|
||||||
|
let cache_revision = operation.revision.load(Ordering::Relaxed);
|
||||||
|
|
||||||
|
// If the current revision matches the cached revision, return the cached value
|
||||||
|
if cache_revision == config_revision {
|
||||||
|
let reader = operation.value_cache.read().unwrap();
|
||||||
|
return reader.as_ref().unwrap().clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, compute the value, update the cache and revision, and return the new value
|
||||||
|
let value = (operation.generator)(&CONFIG);
|
||||||
|
{
|
||||||
|
let mut writer = operation.value_cache.write().unwrap();
|
||||||
|
*writer = Some(value.clone());
|
||||||
|
operation.revision.store(config_revision, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
value
|
||||||
|
}
|
||||||
|
|
||||||
/// Tests whether an email's domain is allowed. A domain is allowed if it
|
/// Tests whether an email's domain is allowed. A domain is allowed if it
|
||||||
/// is in signups_domains_whitelist, or if no whitelist is set (so there
|
/// is in signups_domains_whitelist, or if no whitelist is set (so there
|
||||||
/// are no domain restrictions in effect).
|
/// are no domain restrictions in effect).
|
||||||
|
|
@ -1611,33 +1656,10 @@ impl Config {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete_user_config(&self) -> Result<(), Error> {
|
|
||||||
let operator = opendal_operator_for_path(&CONFIG_FILE_PARENT_DIR)?;
|
|
||||||
operator.delete(&CONFIG_FILENAME).await?;
|
|
||||||
|
|
||||||
// Empty user config
|
|
||||||
let usr = ConfigBuilder::default();
|
|
||||||
|
|
||||||
// Config now is env + defaults
|
|
||||||
let config = {
|
|
||||||
let env = &self.inner.read().unwrap()._env;
|
|
||||||
env.build()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Save configs
|
|
||||||
{
|
|
||||||
let mut writer = self.inner.write().unwrap();
|
|
||||||
writer.config = config;
|
|
||||||
writer._usr = usr;
|
|
||||||
writer._overrides = Vec::new();
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn private_rsa_key(&self) -> String {
|
pub fn private_rsa_key(&self) -> String {
|
||||||
format!("{}.pem", self.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;
|
||||||
inner._enable_smtp && (inner.smtp_host.is_some() || inner.use_sendmail)
|
inner._enable_smtp && (inner.smtp_host.is_some() || inner.use_sendmail)
|
||||||
|
|
@ -1725,7 +1747,7 @@ impl Config {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sso_master_password_policy_value(&self) -> Option<serde_json::Value> {
|
pub fn sso_master_password_policy_value(&self) -> Option<serde_json::Value> {
|
||||||
validate_sso_master_password_policy(self.sso_master_password_policy().as_ref()).ok().flatten()
|
validate_sso_master_password_policy(&self.sso_master_password_policy()).ok().flatten()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sso_scopes_vec(&self) -> Vec<String> {
|
pub fn sso_scopes_vec(&self) -> Vec<String> {
|
||||||
|
|
@ -1865,7 +1887,7 @@ fn to_json<'reg, 'rc>(
|
||||||
// Configure the web-vault version as an integer so it can be used as a comparison smaller or greater then.
|
// Configure the web-vault version as an integer so it can be used as a comparison smaller or greater then.
|
||||||
// The default is based upon the version since this feature is added.
|
// The default is based upon the version since this feature is added.
|
||||||
static WEB_VAULT_VERSION: LazyLock<semver::Version> = LazyLock::new(|| {
|
static WEB_VAULT_VERSION: LazyLock<semver::Version> = LazyLock::new(|| {
|
||||||
let vault_version = get_active_web_release();
|
let vault_version = get_web_vault_version();
|
||||||
// Use a single regex capture to extract version components
|
// Use a single regex capture to extract version components
|
||||||
let re = regex::Regex::new(r"(\d{4})\.(\d{1,2})\.(\d{1,2})").unwrap();
|
let re = regex::Regex::new(r"(\d{4})\.(\d{1,2})\.(\d{1,2})").unwrap();
|
||||||
re.captures(&vault_version)
|
re.captures(&vault_version)
|
||||||
|
|
|
||||||
|
|
@ -55,13 +55,13 @@ pub fn encode_random_bytes<const N: usize>(e: &Encoding) -> String {
|
||||||
/// Generates a random string over a specified alphabet.
|
/// Generates a random string over a specified alphabet.
|
||||||
pub fn get_random_string(alphabet: &[u8], num_chars: usize) -> String {
|
pub fn get_random_string(alphabet: &[u8], num_chars: usize) -> String {
|
||||||
// Ref: https://rust-lang-nursery.github.io/rust-cookbook/algorithms/randomness.html
|
// Ref: https://rust-lang-nursery.github.io/rust-cookbook/algorithms/randomness.html
|
||||||
use rand::RngExt;
|
use rand::Rng;
|
||||||
let mut rng = rand::rng();
|
let mut rng = rand::rng();
|
||||||
|
|
||||||
(0..num_chars)
|
(0..num_chars)
|
||||||
.map(|_| {
|
.map(|_| {
|
||||||
let i = rng.random_range(0..alphabet.len());
|
let i = rng.random_range(0..alphabet.len());
|
||||||
char::from(alphabet[i])
|
alphabet[i] as char
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
@ -113,10 +113,3 @@ pub fn ct_eq<T: AsRef<[u8]>, U: AsRef<[u8]>>(a: T, b: U) -> bool {
|
||||||
use subtle::ConstantTimeEq;
|
use subtle::ConstantTimeEq;
|
||||||
a.as_ref().ct_eq(b.as_ref()).into()
|
a.as_ref().ct_eq(b.as_ref()).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
|
||||||
// SHA256
|
|
||||||
//
|
|
||||||
pub fn sha256_hex(data: &[u8]) -> String {
|
|
||||||
HEXLOWER.encode(digest::digest(&digest::SHA256, data).as_ref())
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -387,6 +387,7 @@ pub mod models;
|
||||||
#[cfg(sqlite)]
|
#[cfg(sqlite)]
|
||||||
pub fn backup_sqlite() -> Result<String, Error> {
|
pub fn backup_sqlite() -> Result<String, Error> {
|
||||||
use diesel::Connection;
|
use diesel::Connection;
|
||||||
|
use std::{fs::File, io::Write};
|
||||||
|
|
||||||
let db_url = CONFIG.database_url();
|
let db_url = CONFIG.database_url();
|
||||||
if DbConnType::from_url(&CONFIG.database_url()).map(|t| t == DbConnType::Sqlite).unwrap_or(false) {
|
if DbConnType::from_url(&CONFIG.database_url()).map(|t| t == DbConnType::Sqlite).unwrap_or(false) {
|
||||||
|
|
@ -400,13 +401,16 @@ pub fn backup_sqlite() -> Result<String, Error> {
|
||||||
.to_string_lossy()
|
.to_string_lossy()
|
||||||
.into_owned();
|
.into_owned();
|
||||||
|
|
||||||
diesel::sql_query("VACUUM INTO ?")
|
match File::create(backup_file.clone()) {
|
||||||
.bind::<diesel::sql_types::Text, _>(&backup_file)
|
Ok(mut f) => {
|
||||||
.execute(&mut conn)
|
let serialized_db = conn.serialize_database_to_buffer();
|
||||||
.map(|_| ())
|
f.write_all(serialized_db.as_slice()).expect("Error writing SQLite backup");
|
||||||
.map_res("VACUUM INTO failed")?;
|
Ok(backup_file)
|
||||||
|
}
|
||||||
Ok(backup_file)
|
Err(e) => {
|
||||||
|
err_silent!(format!("Unable to save SQLite backup: {e:?}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
err_silent!("The database type is not SQLite. Backups only works for SQLite databases")
|
err_silent!("The database type is not SQLite. Backups only works for SQLite databases")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,91 +0,0 @@
|
||||||
use chrono::NaiveDateTime;
|
|
||||||
use diesel::prelude::*;
|
|
||||||
|
|
||||||
use super::{CipherId, User, UserId};
|
|
||||||
use crate::api::EmptyResult;
|
|
||||||
use crate::db::schema::archives;
|
|
||||||
use crate::db::DbConn;
|
|
||||||
use crate::error::MapResult;
|
|
||||||
|
|
||||||
#[derive(Identifiable, Queryable, Insertable)]
|
|
||||||
#[diesel(table_name = archives)]
|
|
||||||
#[diesel(primary_key(user_uuid, cipher_uuid))]
|
|
||||||
pub struct Archive {
|
|
||||||
pub user_uuid: UserId,
|
|
||||||
pub cipher_uuid: CipherId,
|
|
||||||
pub archived_at: NaiveDateTime,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Archive {
|
|
||||||
// Returns the date the specified cipher was archived
|
|
||||||
pub async fn get_archived_at(cipher_uuid: &CipherId, user_uuid: &UserId, conn: &DbConn) -> Option<NaiveDateTime> {
|
|
||||||
db_run! { conn: {
|
|
||||||
archives::table
|
|
||||||
.filter(archives::cipher_uuid.eq(cipher_uuid))
|
|
||||||
.filter(archives::user_uuid.eq(user_uuid))
|
|
||||||
.select(archives::archived_at)
|
|
||||||
.first::<NaiveDateTime>(conn).ok()
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Saves (inserts or updates) an archive record with the provided timestamp
|
|
||||||
pub async fn save(
|
|
||||||
user_uuid: &UserId,
|
|
||||||
cipher_uuid: &CipherId,
|
|
||||||
archived_at: NaiveDateTime,
|
|
||||||
conn: &DbConn,
|
|
||||||
) -> EmptyResult {
|
|
||||||
User::update_uuid_revision(user_uuid, conn).await;
|
|
||||||
db_run! { conn:
|
|
||||||
sqlite, mysql {
|
|
||||||
diesel::replace_into(archives::table)
|
|
||||||
.values((
|
|
||||||
archives::user_uuid.eq(user_uuid),
|
|
||||||
archives::cipher_uuid.eq(cipher_uuid),
|
|
||||||
archives::archived_at.eq(archived_at),
|
|
||||||
))
|
|
||||||
.execute(conn)
|
|
||||||
.map_res("Error saving archive")
|
|
||||||
}
|
|
||||||
postgresql {
|
|
||||||
diesel::insert_into(archives::table)
|
|
||||||
.values((
|
|
||||||
archives::user_uuid.eq(user_uuid),
|
|
||||||
archives::cipher_uuid.eq(cipher_uuid),
|
|
||||||
archives::archived_at.eq(archived_at),
|
|
||||||
))
|
|
||||||
.on_conflict((archives::user_uuid, archives::cipher_uuid))
|
|
||||||
.do_update()
|
|
||||||
.set(archives::archived_at.eq(archived_at))
|
|
||||||
.execute(conn)
|
|
||||||
.map_res("Error saving archive")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deletes an archive record for a specific cipher
|
|
||||||
pub async fn delete_by_cipher(user_uuid: &UserId, cipher_uuid: &CipherId, conn: &DbConn) -> EmptyResult {
|
|
||||||
User::update_uuid_revision(user_uuid, conn).await;
|
|
||||||
db_run! { conn: {
|
|
||||||
diesel::delete(
|
|
||||||
archives::table
|
|
||||||
.filter(archives::user_uuid.eq(user_uuid))
|
|
||||||
.filter(archives::cipher_uuid.eq(cipher_uuid))
|
|
||||||
)
|
|
||||||
.execute(conn)
|
|
||||||
.map_res("Error deleting archive")
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return a vec with (cipher_uuid, archived_at)
|
|
||||||
/// This is used during a full sync so we only need one query for all archive matches
|
|
||||||
pub async fn find_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<(CipherId, NaiveDateTime)> {
|
|
||||||
db_run! { conn: {
|
|
||||||
archives::table
|
|
||||||
.filter(archives::user_uuid.eq(user_uuid))
|
|
||||||
.select((archives::cipher_uuid, archives::archived_at))
|
|
||||||
.load::<(CipherId, NaiveDateTime)>(conn)
|
|
||||||
.unwrap_or_default()
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -50,7 +50,7 @@ impl Attachment {
|
||||||
let token = encode_jwt(&generate_file_download_claims(self.cipher_uuid.clone(), self.id.clone()));
|
let token = encode_jwt(&generate_file_download_claims(self.cipher_uuid.clone(), self.id.clone()));
|
||||||
Ok(format!("{host}/attachments/{}/{}?token={token}", self.cipher_uuid, self.id))
|
Ok(format!("{host}/attachments/{}/{}?token={token}", self.cipher_uuid, self.id))
|
||||||
} else {
|
} else {
|
||||||
Ok(operator.presign_read(&self.get_file_path(), Duration::from_mins(5)).await?.uri().to_string())
|
Ok(operator.presign_read(&self.get_file_path(), Duration::from_secs(5 * 60)).await?.uri().to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -177,9 +177,7 @@ impl AuthRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn purge_expired_auth_requests(conn: &DbConn) {
|
pub async fn purge_expired_auth_requests(conn: &DbConn) {
|
||||||
// delete auth requests older than 15 minutes which is functionally equivalent to upstream:
|
let expiry_time = Utc::now().naive_utc() - chrono::TimeDelta::try_minutes(5).unwrap(); //after 5 minutes, clients reject the request
|
||||||
// https://github.com/bitwarden/server/blob/f8ee2270409f7a13125cd414c450740af605a175/src/Sql/dbo/Auth/Stored%20Procedures/AuthRequest_DeleteIfExpired.sql
|
|
||||||
let expiry_time = Utc::now().naive_utc() - chrono::TimeDelta::try_minutes(15).unwrap();
|
|
||||||
for auth_request in Self::find_created_before(&expiry_time, conn).await {
|
for auth_request in Self::find_created_before(&expiry_time, conn).await {
|
||||||
auth_request.delete(conn).await.ok();
|
auth_request.delete(conn).await.ok();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@ use diesel::prelude::*;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
Archive, Attachment, CollectionCipher, CollectionId, Favorite, FolderCipher, FolderId, Group, Membership,
|
Attachment, CollectionCipher, CollectionId, Favorite, FolderCipher, FolderId, Group, Membership, MembershipStatus,
|
||||||
MembershipStatus, MembershipType, OrganizationId, User, UserId,
|
MembershipType, OrganizationId, User, UserId,
|
||||||
};
|
};
|
||||||
use crate::api::core::{CipherData, CipherSyncData, CipherSyncType};
|
use crate::api::core::{CipherData, CipherSyncData, CipherSyncType};
|
||||||
use macros::UuidFromParam;
|
use macros::UuidFromParam;
|
||||||
|
|
@ -380,11 +380,6 @@ impl Cipher {
|
||||||
} else {
|
} else {
|
||||||
self.is_favorite(user_uuid, conn).await
|
self.is_favorite(user_uuid, conn).await
|
||||||
});
|
});
|
||||||
json_object["archivedDate"] = json!(if let Some(cipher_sync_data) = cipher_sync_data {
|
|
||||||
cipher_sync_data.cipher_archives.get(&self.uuid).map_or(Value::Null, |d| Value::String(format_date(d)))
|
|
||||||
} else {
|
|
||||||
self.get_archived_at(user_uuid, conn).await.map_or(Value::Null, |d| Value::String(format_date(&d)))
|
|
||||||
});
|
|
||||||
// These values are true by default, but can be false if the
|
// These values are true by default, but can be false if the
|
||||||
// cipher belongs to a collection or group where the org owner has enabled
|
// cipher belongs to a collection or group where the org owner has enabled
|
||||||
// the "Read Only" or "Hide Passwords" restrictions for the user.
|
// the "Read Only" or "Hide Passwords" restrictions for the user.
|
||||||
|
|
@ -403,7 +398,7 @@ impl Cipher {
|
||||||
3 => "card",
|
3 => "card",
|
||||||
4 => "identity",
|
4 => "identity",
|
||||||
5 => "sshKey",
|
5 => "sshKey",
|
||||||
_ => err!(format!("Cipher {} has an invalid type {}", self.uuid, self.atype)),
|
_ => panic!("Wrong type"),
|
||||||
};
|
};
|
||||||
|
|
||||||
json_object[key] = type_data_json;
|
json_object[key] = type_data_json;
|
||||||
|
|
@ -564,7 +559,7 @@ impl Cipher {
|
||||||
if let Some(cached_member) = cipher_sync_data.members.get(org_uuid) {
|
if let Some(cached_member) = cipher_sync_data.members.get(org_uuid) {
|
||||||
return cached_member.has_full_access();
|
return cached_member.has_full_access();
|
||||||
}
|
}
|
||||||
} else if let Some(member) = Membership::find_confirmed_by_user_and_org(user_uuid, org_uuid, conn).await {
|
} else if let Some(member) = Membership::find_by_user_and_org(user_uuid, org_uuid, conn).await {
|
||||||
return member.has_full_access();
|
return member.has_full_access();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -673,12 +668,10 @@ impl Cipher {
|
||||||
ciphers::table
|
ciphers::table
|
||||||
.filter(ciphers::uuid.eq(&self.uuid))
|
.filter(ciphers::uuid.eq(&self.uuid))
|
||||||
.inner_join(ciphers_collections::table.on(
|
.inner_join(ciphers_collections::table.on(
|
||||||
ciphers::uuid.eq(ciphers_collections::cipher_uuid)
|
ciphers::uuid.eq(ciphers_collections::cipher_uuid)))
|
||||||
))
|
|
||||||
.inner_join(users_collections::table.on(
|
.inner_join(users_collections::table.on(
|
||||||
ciphers_collections::collection_uuid.eq(users_collections::collection_uuid)
|
ciphers_collections::collection_uuid.eq(users_collections::collection_uuid)
|
||||||
.and(users_collections::user_uuid.eq(user_uuid))
|
.and(users_collections::user_uuid.eq(user_uuid))))
|
||||||
))
|
|
||||||
.select((users_collections::read_only, users_collections::hide_passwords, users_collections::manage))
|
.select((users_collections::read_only, users_collections::hide_passwords, users_collections::manage))
|
||||||
.load::<(bool, bool, bool)>(conn)
|
.load::<(bool, bool, bool)>(conn)
|
||||||
.expect("Error getting user access restrictions")
|
.expect("Error getting user access restrictions")
|
||||||
|
|
@ -704,9 +697,6 @@ impl Cipher {
|
||||||
.inner_join(users_organizations::table.on(
|
.inner_join(users_organizations::table.on(
|
||||||
users_organizations::uuid.eq(groups_users::users_organizations_uuid)
|
users_organizations::uuid.eq(groups_users::users_organizations_uuid)
|
||||||
))
|
))
|
||||||
.inner_join(groups::table.on(groups::uuid.eq(collections_groups::groups_uuid)
|
|
||||||
.and(groups::organizations_uuid.eq(users_organizations::org_uuid))
|
|
||||||
))
|
|
||||||
.filter(users_organizations::user_uuid.eq(user_uuid))
|
.filter(users_organizations::user_uuid.eq(user_uuid))
|
||||||
.select((collections_groups::read_only, collections_groups::hide_passwords, collections_groups::manage))
|
.select((collections_groups::read_only, collections_groups::hide_passwords, collections_groups::manage))
|
||||||
.load::<(bool, bool, bool)>(conn)
|
.load::<(bool, bool, bool)>(conn)
|
||||||
|
|
@ -747,18 +737,6 @@ impl Cipher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_archived_at(&self, user_uuid: &UserId, conn: &DbConn) -> Option<NaiveDateTime> {
|
|
||||||
Archive::get_archived_at(&self.uuid, user_uuid, conn).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn set_archived_at(&self, archived_at: NaiveDateTime, user_uuid: &UserId, conn: &DbConn) -> EmptyResult {
|
|
||||||
Archive::save(user_uuid, &self.uuid, archived_at, conn).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn unarchive(&self, user_uuid: &UserId, conn: &DbConn) -> EmptyResult {
|
|
||||||
Archive::delete_by_cipher(user_uuid, &self.uuid, conn).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_folder_uuid(&self, user_uuid: &UserId, conn: &DbConn) -> Option<FolderId> {
|
pub async fn get_folder_uuid(&self, user_uuid: &UserId, conn: &DbConn) -> Option<FolderId> {
|
||||||
db_run! { conn: {
|
db_run! { conn: {
|
||||||
folders_ciphers::table
|
folders_ciphers::table
|
||||||
|
|
@ -817,28 +795,28 @@ impl Cipher {
|
||||||
let mut query = ciphers::table
|
let mut query = ciphers::table
|
||||||
.left_join(ciphers_collections::table.on(
|
.left_join(ciphers_collections::table.on(
|
||||||
ciphers::uuid.eq(ciphers_collections::cipher_uuid)
|
ciphers::uuid.eq(ciphers_collections::cipher_uuid)
|
||||||
))
|
))
|
||||||
.left_join(users_organizations::table.on(
|
.left_join(users_organizations::table.on(
|
||||||
ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable())
|
ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable())
|
||||||
.and(users_organizations::user_uuid.eq(user_uuid))
|
.and(users_organizations::user_uuid.eq(user_uuid))
|
||||||
.and(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
|
.and(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
|
||||||
))
|
))
|
||||||
.left_join(users_collections::table.on(
|
.left_join(users_collections::table.on(
|
||||||
ciphers_collections::collection_uuid.eq(users_collections::collection_uuid)
|
ciphers_collections::collection_uuid.eq(users_collections::collection_uuid)
|
||||||
// Ensure that users_collections::user_uuid is NULL for unconfirmed users.
|
// Ensure that users_collections::user_uuid is NULL for unconfirmed users.
|
||||||
.and(users_organizations::user_uuid.eq(users_collections::user_uuid))
|
.and(users_organizations::user_uuid.eq(users_collections::user_uuid))
|
||||||
))
|
))
|
||||||
.left_join(groups_users::table.on(
|
.left_join(groups_users::table.on(
|
||||||
groups_users::users_organizations_uuid.eq(users_organizations::uuid)
|
groups_users::users_organizations_uuid.eq(users_organizations::uuid)
|
||||||
))
|
))
|
||||||
.left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid)
|
.left_join(groups::table.on(
|
||||||
// Ensure that group and membership belong to the same org
|
groups::uuid.eq(groups_users::groups_uuid)
|
||||||
.and(groups::organizations_uuid.eq(users_organizations::org_uuid))
|
))
|
||||||
))
|
|
||||||
.left_join(collections_groups::table.on(
|
.left_join(collections_groups::table.on(
|
||||||
collections_groups::collections_uuid.eq(ciphers_collections::collection_uuid)
|
collections_groups::collections_uuid.eq(ciphers_collections::collection_uuid).and(
|
||||||
.and(collections_groups::groups_uuid.eq(groups::uuid))
|
collections_groups::groups_uuid.eq(groups::uuid)
|
||||||
))
|
)
|
||||||
|
))
|
||||||
.filter(ciphers::user_uuid.eq(user_uuid)) // Cipher owner
|
.filter(ciphers::user_uuid.eq(user_uuid)) // Cipher owner
|
||||||
.or_filter(users_organizations::access_all.eq(true)) // access_all in org
|
.or_filter(users_organizations::access_all.eq(true)) // access_all in org
|
||||||
.or_filter(users_collections::user_uuid.eq(user_uuid)) // Access to collection
|
.or_filter(users_collections::user_uuid.eq(user_uuid)) // Access to collection
|
||||||
|
|
@ -1008,9 +986,7 @@ impl Cipher {
|
||||||
.left_join(groups_users::table.on(
|
.left_join(groups_users::table.on(
|
||||||
groups_users::users_organizations_uuid.eq(users_organizations::uuid)
|
groups_users::users_organizations_uuid.eq(users_organizations::uuid)
|
||||||
))
|
))
|
||||||
.left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid)
|
.left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid)))
|
||||||
.and(groups::organizations_uuid.eq(users_organizations::org_uuid))
|
|
||||||
))
|
|
||||||
.left_join(collections_groups::table.on(
|
.left_join(collections_groups::table.on(
|
||||||
collections_groups::collections_uuid.eq(ciphers_collections::collection_uuid)
|
collections_groups::collections_uuid.eq(ciphers_collections::collection_uuid)
|
||||||
.and(collections_groups::groups_uuid.eq(groups::uuid))
|
.and(collections_groups::groups_uuid.eq(groups::uuid))
|
||||||
|
|
@ -1071,9 +1047,7 @@ impl Cipher {
|
||||||
.left_join(groups_users::table.on(
|
.left_join(groups_users::table.on(
|
||||||
groups_users::users_organizations_uuid.eq(users_organizations::uuid)
|
groups_users::users_organizations_uuid.eq(users_organizations::uuid)
|
||||||
))
|
))
|
||||||
.left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid)
|
.left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid)))
|
||||||
.and(groups::organizations_uuid.eq(users_organizations::org_uuid))
|
|
||||||
))
|
|
||||||
.left_join(collections_groups::table.on(
|
.left_join(collections_groups::table.on(
|
||||||
collections_groups::collections_uuid.eq(ciphers_collections::collection_uuid)
|
collections_groups::collections_uuid.eq(ciphers_collections::collection_uuid)
|
||||||
.and(collections_groups::groups_uuid.eq(groups::uuid))
|
.and(collections_groups::groups_uuid.eq(groups::uuid))
|
||||||
|
|
@ -1141,8 +1115,8 @@ impl Cipher {
|
||||||
.left_join(groups_users::table.on(
|
.left_join(groups_users::table.on(
|
||||||
groups_users::users_organizations_uuid.eq(users_organizations::uuid)
|
groups_users::users_organizations_uuid.eq(users_organizations::uuid)
|
||||||
))
|
))
|
||||||
.left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid)
|
.left_join(groups::table.on(
|
||||||
.and(groups::organizations_uuid.eq(users_organizations::org_uuid))
|
groups::uuid.eq(groups_users::groups_uuid)
|
||||||
))
|
))
|
||||||
.left_join(collections_groups::table.on(
|
.left_join(collections_groups::table.on(
|
||||||
collections_groups::collections_uuid.eq(ciphers_collections::collection_uuid).and(
|
collections_groups::collections_uuid.eq(ciphers_collections::collection_uuid).and(
|
||||||
|
|
|
||||||
|
|
@ -191,7 +191,7 @@ impl Collection {
|
||||||
self.update_users_revision(conn).await;
|
self.update_users_revision(conn).await;
|
||||||
CollectionCipher::delete_all_by_collection(&self.uuid, conn).await?;
|
CollectionCipher::delete_all_by_collection(&self.uuid, conn).await?;
|
||||||
CollectionUser::delete_all_by_collection(&self.uuid, conn).await?;
|
CollectionUser::delete_all_by_collection(&self.uuid, conn).await?;
|
||||||
CollectionGroup::delete_all_by_collection(&self.uuid, &self.org_uuid, conn).await?;
|
CollectionGroup::delete_all_by_collection(&self.uuid, conn).await?;
|
||||||
|
|
||||||
db_run! { conn: {
|
db_run! { conn: {
|
||||||
diesel::delete(collections::table.filter(collections::uuid.eq(self.uuid)))
|
diesel::delete(collections::table.filter(collections::uuid.eq(self.uuid)))
|
||||||
|
|
@ -239,8 +239,8 @@ impl Collection {
|
||||||
.left_join(groups_users::table.on(
|
.left_join(groups_users::table.on(
|
||||||
groups_users::users_organizations_uuid.eq(users_organizations::uuid)
|
groups_users::users_organizations_uuid.eq(users_organizations::uuid)
|
||||||
))
|
))
|
||||||
.left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid)
|
.left_join(groups::table.on(
|
||||||
.and(groups::organizations_uuid.eq(users_organizations::org_uuid))
|
groups::uuid.eq(groups_users::groups_uuid)
|
||||||
))
|
))
|
||||||
.left_join(collections_groups::table.on(
|
.left_join(collections_groups::table.on(
|
||||||
collections_groups::groups_uuid.eq(groups_users::groups_uuid).and(
|
collections_groups::groups_uuid.eq(groups_users::groups_uuid).and(
|
||||||
|
|
@ -355,8 +355,8 @@ impl Collection {
|
||||||
.left_join(groups_users::table.on(
|
.left_join(groups_users::table.on(
|
||||||
groups_users::users_organizations_uuid.eq(users_organizations::uuid)
|
groups_users::users_organizations_uuid.eq(users_organizations::uuid)
|
||||||
))
|
))
|
||||||
.left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid)
|
.left_join(groups::table.on(
|
||||||
.and(groups::organizations_uuid.eq(users_organizations::org_uuid))
|
groups::uuid.eq(groups_users::groups_uuid)
|
||||||
))
|
))
|
||||||
.left_join(collections_groups::table.on(
|
.left_join(collections_groups::table.on(
|
||||||
collections_groups::groups_uuid.eq(groups_users::groups_uuid).and(
|
collections_groups::groups_uuid.eq(groups_users::groups_uuid).and(
|
||||||
|
|
@ -422,8 +422,8 @@ impl Collection {
|
||||||
.left_join(groups_users::table.on(
|
.left_join(groups_users::table.on(
|
||||||
groups_users::users_organizations_uuid.eq(users_organizations::uuid)
|
groups_users::users_organizations_uuid.eq(users_organizations::uuid)
|
||||||
))
|
))
|
||||||
.left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid)
|
.left_join(groups::table.on(
|
||||||
.and(groups::organizations_uuid.eq(users_organizations::org_uuid))
|
groups::uuid.eq(groups_users::groups_uuid)
|
||||||
))
|
))
|
||||||
.left_join(collections_groups::table.on(
|
.left_join(collections_groups::table.on(
|
||||||
collections_groups::groups_uuid.eq(groups_users::groups_uuid)
|
collections_groups::groups_uuid.eq(groups_users::groups_uuid)
|
||||||
|
|
@ -484,8 +484,8 @@ impl Collection {
|
||||||
.left_join(groups_users::table.on(
|
.left_join(groups_users::table.on(
|
||||||
groups_users::users_organizations_uuid.eq(users_organizations::uuid)
|
groups_users::users_organizations_uuid.eq(users_organizations::uuid)
|
||||||
))
|
))
|
||||||
.left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid)
|
.left_join(groups::table.on(
|
||||||
.and(groups::organizations_uuid.eq(users_organizations::org_uuid))
|
groups::uuid.eq(groups_users::groups_uuid)
|
||||||
))
|
))
|
||||||
.left_join(collections_groups::table.on(
|
.left_join(collections_groups::table.on(
|
||||||
collections_groups::groups_uuid.eq(groups_users::groups_uuid).and(
|
collections_groups::groups_uuid.eq(groups_users::groups_uuid).and(
|
||||||
|
|
@ -513,8 +513,7 @@ impl Collection {
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn is_coll_manageable_by_user(uuid: &CollectionId, user_uuid: &UserId, conn: &DbConn) -> bool {
|
pub async fn is_manageable_by_user(&self, user_uuid: &UserId, conn: &DbConn) -> bool {
|
||||||
let uuid = uuid.to_string();
|
|
||||||
let user_uuid = user_uuid.to_string();
|
let user_uuid = user_uuid.to_string();
|
||||||
db_run! { conn: {
|
db_run! { conn: {
|
||||||
collections::table
|
collections::table
|
||||||
|
|
@ -531,17 +530,17 @@ impl Collection {
|
||||||
.left_join(groups_users::table.on(
|
.left_join(groups_users::table.on(
|
||||||
groups_users::users_organizations_uuid.eq(users_organizations::uuid)
|
groups_users::users_organizations_uuid.eq(users_organizations::uuid)
|
||||||
))
|
))
|
||||||
.left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid)
|
.left_join(groups::table.on(
|
||||||
.and(groups::organizations_uuid.eq(users_organizations::org_uuid))
|
groups::uuid.eq(groups_users::groups_uuid)
|
||||||
))
|
))
|
||||||
.left_join(collections_groups::table.on(
|
.left_join(collections_groups::table.on(
|
||||||
collections_groups::groups_uuid.eq(groups_users::groups_uuid).and(
|
collections_groups::groups_uuid.eq(groups_users::groups_uuid).and(
|
||||||
collections_groups::collections_uuid.eq(collections::uuid)
|
collections_groups::collections_uuid.eq(collections::uuid)
|
||||||
)
|
)
|
||||||
))
|
))
|
||||||
.filter(collections::uuid.eq(&uuid))
|
.filter(collections::uuid.eq(&self.uuid))
|
||||||
.filter(
|
.filter(
|
||||||
users_collections::collection_uuid.eq(&uuid).and(users_collections::manage.eq(true)).or(// Directly accessed collection
|
users_collections::collection_uuid.eq(&self.uuid).and(users_collections::manage.eq(true)).or(// Directly accessed collection
|
||||||
users_organizations::access_all.eq(true).or( // access_all in Organization
|
users_organizations::access_all.eq(true).or( // access_all in Organization
|
||||||
users_organizations::atype.le(MembershipType::Admin as i32) // Org admin or owner
|
users_organizations::atype.le(MembershipType::Admin as i32) // Org admin or owner
|
||||||
)).or(
|
)).or(
|
||||||
|
|
@ -559,10 +558,6 @@ impl Collection {
|
||||||
.unwrap_or(0) != 0
|
.unwrap_or(0) != 0
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn is_manageable_by_user(&self, user_uuid: &UserId, conn: &DbConn) -> bool {
|
|
||||||
Self::is_coll_manageable_by_user(&self.uuid, user_uuid, conn).await
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Database methods
|
/// Database methods
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use chrono::{NaiveDateTime, Utc};
|
use chrono::{NaiveDateTime, Utc};
|
||||||
|
|
||||||
use data_encoding::BASE64URL;
|
use data_encoding::{BASE64, BASE64URL};
|
||||||
use derive_more::{Display, From};
|
use derive_more::{Display, From};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
|
|
@ -25,7 +25,7 @@ pub struct Device {
|
||||||
pub user_uuid: UserId,
|
pub user_uuid: UserId,
|
||||||
|
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub atype: i32, // https://github.com/bitwarden/server/blob/8d547dcc280babab70dd4a3c94ced6a34b12dfbf/src/Core/Enums/DeviceType.cs
|
pub atype: i32, // https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/Enums/DeviceType.cs
|
||||||
pub push_uuid: Option<PushId>,
|
pub push_uuid: Option<PushId>,
|
||||||
pub push_token: Option<String>,
|
pub push_token: Option<String>,
|
||||||
|
|
||||||
|
|
@ -49,16 +49,11 @@ impl Device {
|
||||||
|
|
||||||
push_uuid: Some(PushId(get_uuid())),
|
push_uuid: Some(PushId(get_uuid())),
|
||||||
push_token: None,
|
push_token: None,
|
||||||
refresh_token: Device::generate_refresh_token(),
|
refresh_token: crypto::encode_random_bytes::<64>(&BASE64URL),
|
||||||
twofactor_remember: None,
|
twofactor_remember: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub fn generate_refresh_token() -> String {
|
|
||||||
crypto::encode_random_bytes::<64>(&BASE64URL)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_json(&self) -> Value {
|
pub fn to_json(&self) -> Value {
|
||||||
json!({
|
json!({
|
||||||
"id": self.uuid,
|
"id": self.uuid,
|
||||||
|
|
@ -72,13 +67,10 @@ impl Device {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn refresh_twofactor_remember(&mut self) -> String {
|
pub fn refresh_twofactor_remember(&mut self) -> String {
|
||||||
use crate::auth::{encode_jwt, generate_2fa_remember_claims};
|
let twofactor_remember = crypto::encode_random_bytes::<180>(&BASE64);
|
||||||
|
self.twofactor_remember = Some(twofactor_remember.clone());
|
||||||
|
|
||||||
let two_factor_remember_claim = generate_2fa_remember_claims(self.uuid.clone(), self.user_uuid.clone());
|
twofactor_remember
|
||||||
let two_factor_remember_string = encode_jwt(&two_factor_remember_claim);
|
|
||||||
self.twofactor_remember = Some(two_factor_remember_string.clone());
|
|
||||||
|
|
||||||
two_factor_remember_string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_twofactor_remember(&mut self) {
|
pub fn delete_twofactor_remember(&mut self) {
|
||||||
|
|
@ -265,17 +257,6 @@ impl Device {
|
||||||
.unwrap_or(0) != 0
|
.unwrap_or(0) != 0
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn rotate_refresh_tokens_by_user(user_uuid: &UserId, conn: &DbConn) -> EmptyResult {
|
|
||||||
// Generate a new token per device.
|
|
||||||
// We cannot do a single UPDATE with one value because each device needs a unique token.
|
|
||||||
let devices = Self::find_by_user(user_uuid, conn).await;
|
|
||||||
for mut device in devices {
|
|
||||||
device.refresh_token = Device::generate_refresh_token();
|
|
||||||
device.save(false, conn).await?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Display)]
|
#[derive(Display)]
|
||||||
|
|
@ -332,8 +313,6 @@ pub enum DeviceType {
|
||||||
MacOsCLI = 24,
|
MacOsCLI = 24,
|
||||||
#[display("Linux CLI")]
|
#[display("Linux CLI")]
|
||||||
LinuxCLI = 25,
|
LinuxCLI = 25,
|
||||||
#[display("DuckDuckGo")]
|
|
||||||
DuckDuckGoBrowser = 26,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DeviceType {
|
impl DeviceType {
|
||||||
|
|
@ -365,7 +344,6 @@ impl DeviceType {
|
||||||
23 => DeviceType::WindowsCLI,
|
23 => DeviceType::WindowsCLI,
|
||||||
24 => DeviceType::MacOsCLI,
|
24 => DeviceType::MacOsCLI,
|
||||||
25 => DeviceType::LinuxCLI,
|
25 => DeviceType::LinuxCLI,
|
||||||
26 => DeviceType::DuckDuckGoBrowser,
|
|
||||||
_ => DeviceType::UnknownBrowser,
|
_ => DeviceType::UnknownBrowser,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -85,8 +85,7 @@ impl EmergencyAccess {
|
||||||
pub async fn to_json_grantee_details(&self, conn: &DbConn) -> Option<Value> {
|
pub async fn to_json_grantee_details(&self, conn: &DbConn) -> Option<Value> {
|
||||||
let grantee_user = if let Some(grantee_uuid) = &self.grantee_uuid {
|
let grantee_user = if let Some(grantee_uuid) = &self.grantee_uuid {
|
||||||
User::find_by_uuid(grantee_uuid, conn).await.expect("Grantee user not found.")
|
User::find_by_uuid(grantee_uuid, conn).await.expect("Grantee user not found.")
|
||||||
} else {
|
} else if let Some(email) = self.email.as_deref() {
|
||||||
let email = self.email.as_deref()?;
|
|
||||||
match User::find_by_mail(email, conn).await {
|
match User::find_by_mail(email, conn).await {
|
||||||
Some(user) => user,
|
Some(user) => user,
|
||||||
None => {
|
None => {
|
||||||
|
|
@ -95,6 +94,8 @@ impl EmergencyAccess {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(json!({
|
Some(json!({
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use super::{CollectionId, Membership, MembershipId, OrganizationId, User, UserId};
|
use super::{CollectionId, Membership, MembershipId, OrganizationId, User, UserId};
|
||||||
use crate::api::EmptyResult;
|
use crate::api::EmptyResult;
|
||||||
use crate::db::schema::{collections, collections_groups, groups, groups_users, users_organizations};
|
use crate::db::schema::{collections_groups, groups, groups_users, users_organizations};
|
||||||
use crate::db::DbConn;
|
use crate::db::DbConn;
|
||||||
use crate::error::MapResult;
|
use crate::error::MapResult;
|
||||||
use chrono::{NaiveDateTime, Utc};
|
use chrono::{NaiveDateTime, Utc};
|
||||||
|
|
@ -81,7 +81,7 @@ impl Group {
|
||||||
// If both read_only and hide_passwords are false, then manage should be true
|
// If both read_only and hide_passwords are false, then manage should be true
|
||||||
// You can't have an entry with read_only and manage, or hide_passwords and manage
|
// You can't have an entry with read_only and manage, or hide_passwords and manage
|
||||||
// Or an entry with everything to false
|
// Or an entry with everything to false
|
||||||
let collections_groups: Vec<Value> = CollectionGroup::find_by_group(&self.uuid, &self.organizations_uuid, conn)
|
let collections_groups: Vec<Value> = CollectionGroup::find_by_group(&self.uuid, conn)
|
||||||
.await
|
.await
|
||||||
.iter()
|
.iter()
|
||||||
.map(|entry| {
|
.map(|entry| {
|
||||||
|
|
@ -191,7 +191,7 @@ impl Group {
|
||||||
|
|
||||||
pub async fn delete_all_by_organization(org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult {
|
pub async fn delete_all_by_organization(org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult {
|
||||||
for group in Self::find_by_organization(org_uuid, conn).await {
|
for group in Self::find_by_organization(org_uuid, conn).await {
|
||||||
group.delete(org_uuid, conn).await?;
|
group.delete(conn).await?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -246,8 +246,8 @@ impl Group {
|
||||||
.inner_join(users_organizations::table.on(
|
.inner_join(users_organizations::table.on(
|
||||||
users_organizations::uuid.eq(groups_users::users_organizations_uuid)
|
users_organizations::uuid.eq(groups_users::users_organizations_uuid)
|
||||||
))
|
))
|
||||||
.inner_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid)
|
.inner_join(groups::table.on(
|
||||||
.and(groups::organizations_uuid.eq(users_organizations::org_uuid))
|
groups::uuid.eq(groups_users::groups_uuid)
|
||||||
))
|
))
|
||||||
.filter(users_organizations::user_uuid.eq(user_uuid))
|
.filter(users_organizations::user_uuid.eq(user_uuid))
|
||||||
.filter(groups::access_all.eq(true))
|
.filter(groups::access_all.eq(true))
|
||||||
|
|
@ -276,9 +276,9 @@ impl Group {
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete(&self, org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult {
|
pub async fn delete(&self, conn: &DbConn) -> EmptyResult {
|
||||||
CollectionGroup::delete_all_by_group(&self.uuid, org_uuid, conn).await?;
|
CollectionGroup::delete_all_by_group(&self.uuid, conn).await?;
|
||||||
GroupUser::delete_all_by_group(&self.uuid, org_uuid, conn).await?;
|
GroupUser::delete_all_by_group(&self.uuid, conn).await?;
|
||||||
|
|
||||||
db_run! { conn: {
|
db_run! { conn: {
|
||||||
diesel::delete(groups::table.filter(groups::uuid.eq(&self.uuid)))
|
diesel::delete(groups::table.filter(groups::uuid.eq(&self.uuid)))
|
||||||
|
|
@ -306,8 +306,8 @@ impl Group {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CollectionGroup {
|
impl CollectionGroup {
|
||||||
pub async fn save(&mut self, org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult {
|
pub async fn save(&mut self, conn: &DbConn) -> EmptyResult {
|
||||||
let group_users = GroupUser::find_by_group(&self.groups_uuid, org_uuid, conn).await;
|
let group_users = GroupUser::find_by_group(&self.groups_uuid, conn).await;
|
||||||
for group_user in group_users {
|
for group_user in group_users {
|
||||||
group_user.update_user_revision(conn).await;
|
group_user.update_user_revision(conn).await;
|
||||||
}
|
}
|
||||||
|
|
@ -365,19 +365,10 @@ impl CollectionGroup {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn find_by_group(group_uuid: &GroupId, org_uuid: &OrganizationId, conn: &DbConn) -> Vec<Self> {
|
pub async fn find_by_group(group_uuid: &GroupId, conn: &DbConn) -> Vec<Self> {
|
||||||
db_run! { conn: {
|
db_run! { conn: {
|
||||||
collections_groups::table
|
collections_groups::table
|
||||||
.inner_join(groups::table.on(
|
|
||||||
groups::uuid.eq(collections_groups::groups_uuid)
|
|
||||||
))
|
|
||||||
.inner_join(collections::table.on(
|
|
||||||
collections::uuid.eq(collections_groups::collections_uuid)
|
|
||||||
.and(collections::org_uuid.eq(groups::organizations_uuid))
|
|
||||||
))
|
|
||||||
.filter(collections_groups::groups_uuid.eq(group_uuid))
|
.filter(collections_groups::groups_uuid.eq(group_uuid))
|
||||||
.filter(collections::org_uuid.eq(org_uuid))
|
|
||||||
.select(collections_groups::all_columns)
|
|
||||||
.load::<Self>(conn)
|
.load::<Self>(conn)
|
||||||
.expect("Error loading collection groups")
|
.expect("Error loading collection groups")
|
||||||
}}
|
}}
|
||||||
|
|
@ -392,13 +383,6 @@ impl CollectionGroup {
|
||||||
.inner_join(users_organizations::table.on(
|
.inner_join(users_organizations::table.on(
|
||||||
users_organizations::uuid.eq(groups_users::users_organizations_uuid)
|
users_organizations::uuid.eq(groups_users::users_organizations_uuid)
|
||||||
))
|
))
|
||||||
.inner_join(groups::table.on(groups::uuid.eq(collections_groups::groups_uuid)
|
|
||||||
.and(groups::organizations_uuid.eq(users_organizations::org_uuid))
|
|
||||||
))
|
|
||||||
.inner_join(collections::table.on(
|
|
||||||
collections::uuid.eq(collections_groups::collections_uuid)
|
|
||||||
.and(collections::org_uuid.eq(groups::organizations_uuid))
|
|
||||||
))
|
|
||||||
.filter(users_organizations::user_uuid.eq(user_uuid))
|
.filter(users_organizations::user_uuid.eq(user_uuid))
|
||||||
.select(collections_groups::all_columns)
|
.select(collections_groups::all_columns)
|
||||||
.load::<Self>(conn)
|
.load::<Self>(conn)
|
||||||
|
|
@ -410,20 +394,14 @@ impl CollectionGroup {
|
||||||
db_run! { conn: {
|
db_run! { conn: {
|
||||||
collections_groups::table
|
collections_groups::table
|
||||||
.filter(collections_groups::collections_uuid.eq(collection_uuid))
|
.filter(collections_groups::collections_uuid.eq(collection_uuid))
|
||||||
.inner_join(collections::table.on(
|
|
||||||
collections::uuid.eq(collections_groups::collections_uuid)
|
|
||||||
))
|
|
||||||
.inner_join(groups::table.on(groups::uuid.eq(collections_groups::groups_uuid)
|
|
||||||
.and(groups::organizations_uuid.eq(collections::org_uuid))
|
|
||||||
))
|
|
||||||
.select(collections_groups::all_columns)
|
.select(collections_groups::all_columns)
|
||||||
.load::<Self>(conn)
|
.load::<Self>(conn)
|
||||||
.expect("Error loading collection groups")
|
.expect("Error loading collection groups")
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete(&self, org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult {
|
pub async fn delete(&self, conn: &DbConn) -> EmptyResult {
|
||||||
let group_users = GroupUser::find_by_group(&self.groups_uuid, org_uuid, conn).await;
|
let group_users = GroupUser::find_by_group(&self.groups_uuid, conn).await;
|
||||||
for group_user in group_users {
|
for group_user in group_users {
|
||||||
group_user.update_user_revision(conn).await;
|
group_user.update_user_revision(conn).await;
|
||||||
}
|
}
|
||||||
|
|
@ -437,8 +415,8 @@ impl CollectionGroup {
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete_all_by_group(group_uuid: &GroupId, org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult {
|
pub async fn delete_all_by_group(group_uuid: &GroupId, conn: &DbConn) -> EmptyResult {
|
||||||
let group_users = GroupUser::find_by_group(group_uuid, org_uuid, conn).await;
|
let group_users = GroupUser::find_by_group(group_uuid, conn).await;
|
||||||
for group_user in group_users {
|
for group_user in group_users {
|
||||||
group_user.update_user_revision(conn).await;
|
group_user.update_user_revision(conn).await;
|
||||||
}
|
}
|
||||||
|
|
@ -451,14 +429,10 @@ impl CollectionGroup {
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete_all_by_collection(
|
pub async fn delete_all_by_collection(collection_uuid: &CollectionId, conn: &DbConn) -> EmptyResult {
|
||||||
collection_uuid: &CollectionId,
|
|
||||||
org_uuid: &OrganizationId,
|
|
||||||
conn: &DbConn,
|
|
||||||
) -> EmptyResult {
|
|
||||||
let collection_assigned_to_groups = CollectionGroup::find_by_collection(collection_uuid, conn).await;
|
let collection_assigned_to_groups = CollectionGroup::find_by_collection(collection_uuid, conn).await;
|
||||||
for collection_assigned_to_group in collection_assigned_to_groups {
|
for collection_assigned_to_group in collection_assigned_to_groups {
|
||||||
let group_users = GroupUser::find_by_group(&collection_assigned_to_group.groups_uuid, org_uuid, conn).await;
|
let group_users = GroupUser::find_by_group(&collection_assigned_to_group.groups_uuid, conn).await;
|
||||||
for group_user in group_users {
|
for group_user in group_users {
|
||||||
group_user.update_user_revision(conn).await;
|
group_user.update_user_revision(conn).await;
|
||||||
}
|
}
|
||||||
|
|
@ -520,19 +494,10 @@ impl GroupUser {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn find_by_group(group_uuid: &GroupId, org_uuid: &OrganizationId, conn: &DbConn) -> Vec<Self> {
|
pub async fn find_by_group(group_uuid: &GroupId, conn: &DbConn) -> Vec<Self> {
|
||||||
db_run! { conn: {
|
db_run! { conn: {
|
||||||
groups_users::table
|
groups_users::table
|
||||||
.inner_join(groups::table.on(
|
|
||||||
groups::uuid.eq(groups_users::groups_uuid)
|
|
||||||
))
|
|
||||||
.inner_join(users_organizations::table.on(
|
|
||||||
users_organizations::uuid.eq(groups_users::users_organizations_uuid)
|
|
||||||
.and(users_organizations::org_uuid.eq(groups::organizations_uuid))
|
|
||||||
))
|
|
||||||
.filter(groups_users::groups_uuid.eq(group_uuid))
|
.filter(groups_users::groups_uuid.eq(group_uuid))
|
||||||
.filter(groups::organizations_uuid.eq(org_uuid))
|
|
||||||
.select(groups_users::all_columns)
|
|
||||||
.load::<Self>(conn)
|
.load::<Self>(conn)
|
||||||
.expect("Error loading group users")
|
.expect("Error loading group users")
|
||||||
}}
|
}}
|
||||||
|
|
@ -557,13 +522,6 @@ impl GroupUser {
|
||||||
.inner_join(collections_groups::table.on(
|
.inner_join(collections_groups::table.on(
|
||||||
collections_groups::groups_uuid.eq(groups_users::groups_uuid)
|
collections_groups::groups_uuid.eq(groups_users::groups_uuid)
|
||||||
))
|
))
|
||||||
.inner_join(groups::table.on(
|
|
||||||
groups::uuid.eq(groups_users::groups_uuid)
|
|
||||||
))
|
|
||||||
.inner_join(collections::table.on(
|
|
||||||
collections::uuid.eq(collections_groups::collections_uuid)
|
|
||||||
.and(collections::org_uuid.eq(groups::organizations_uuid))
|
|
||||||
))
|
|
||||||
.filter(collections_groups::collections_uuid.eq(collection_uuid))
|
.filter(collections_groups::collections_uuid.eq(collection_uuid))
|
||||||
.filter(groups_users::users_organizations_uuid.eq(member_uuid))
|
.filter(groups_users::users_organizations_uuid.eq(member_uuid))
|
||||||
.count()
|
.count()
|
||||||
|
|
@ -617,8 +575,8 @@ impl GroupUser {
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete_all_by_group(group_uuid: &GroupId, org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult {
|
pub async fn delete_all_by_group(group_uuid: &GroupId, conn: &DbConn) -> EmptyResult {
|
||||||
let group_users = GroupUser::find_by_group(group_uuid, org_uuid, conn).await;
|
let group_users = GroupUser::find_by_group(group_uuid, conn).await;
|
||||||
for group_user in group_users {
|
for group_user in group_users {
|
||||||
group_user.update_user_revision(conn).await;
|
group_user.update_user_revision(conn).await;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
mod archive;
|
|
||||||
mod attachment;
|
mod attachment;
|
||||||
mod auth_request;
|
mod auth_request;
|
||||||
mod cipher;
|
mod cipher;
|
||||||
|
|
@ -18,7 +17,6 @@ mod two_factor_duo_context;
|
||||||
mod two_factor_incomplete;
|
mod two_factor_incomplete;
|
||||||
mod user;
|
mod user;
|
||||||
|
|
||||||
pub use self::archive::Archive;
|
|
||||||
pub use self::attachment::{Attachment, AttachmentId};
|
pub use self::attachment::{Attachment, AttachmentId};
|
||||||
pub use self::auth_request::{AuthRequest, AuthRequestId};
|
pub use self::auth_request::{AuthRequest, AuthRequestId};
|
||||||
pub use self::cipher::{Cipher, CipherId, RepromptType};
|
pub use self::cipher::{Cipher, CipherId, RepromptType};
|
||||||
|
|
|
||||||
|
|
@ -269,7 +269,7 @@ impl OrgPolicy {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(user) = Membership::find_confirmed_by_user_and_org(user_uuid, &policy.org_uuid, conn).await {
|
if let Some(user) = Membership::find_by_user_and_org(user_uuid, &policy.org_uuid, conn).await {
|
||||||
if user.atype < MembershipType::Admin {
|
if user.atype < MembershipType::Admin {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -332,7 +332,7 @@ impl OrgPolicy {
|
||||||
for policy in
|
for policy in
|
||||||
OrgPolicy::find_confirmed_by_user_and_active_policy(user_uuid, OrgPolicyType::SendOptions, conn).await
|
OrgPolicy::find_confirmed_by_user_and_active_policy(user_uuid, OrgPolicyType::SendOptions, conn).await
|
||||||
{
|
{
|
||||||
if let Some(user) = Membership::find_confirmed_by_user_and_org(user_uuid, &policy.org_uuid, conn).await {
|
if let Some(user) = Membership::find_by_user_and_org(user_uuid, &policy.org_uuid, conn).await {
|
||||||
if user.atype < MembershipType::Admin {
|
if user.atype < MembershipType::Admin {
|
||||||
match serde_json::from_str::<SendOptionsPolicyData>(&policy.data) {
|
match serde_json::from_str::<SendOptionsPolicyData>(&policy.data) {
|
||||||
Ok(opts) => {
|
Ok(opts) => {
|
||||||
|
|
|
||||||
|
|
@ -514,8 +514,7 @@ impl Membership {
|
||||||
"familySponsorshipValidUntil": null,
|
"familySponsorshipValidUntil": null,
|
||||||
"familySponsorshipToDelete": null,
|
"familySponsorshipToDelete": null,
|
||||||
"accessSecretsManager": false,
|
"accessSecretsManager": false,
|
||||||
// limit collection creation to managers with access_all permission to prevent issues
|
"limitCollectionCreation": self.atype < MembershipType::Manager, // If less then a manager return true, to limit collection creations
|
||||||
"limitCollectionCreation": self.atype < MembershipType::Manager || !self.access_all,
|
|
||||||
"limitCollectionDeletion": true,
|
"limitCollectionDeletion": true,
|
||||||
"limitItemDeletion": false,
|
"limitItemDeletion": false,
|
||||||
"allowAdminAccessToAllCollectionItems": true,
|
"allowAdminAccessToAllCollectionItems": true,
|
||||||
|
|
@ -1074,9 +1073,7 @@ impl Membership {
|
||||||
.left_join(collections_groups::table.on(
|
.left_join(collections_groups::table.on(
|
||||||
collections_groups::groups_uuid.eq(groups_users::groups_uuid)
|
collections_groups::groups_uuid.eq(groups_users::groups_uuid)
|
||||||
))
|
))
|
||||||
.left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid)
|
.left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid)))
|
||||||
.and(groups::organizations_uuid.eq(users_organizations::org_uuid))
|
|
||||||
))
|
|
||||||
.left_join(ciphers_collections::table.on(
|
.left_join(ciphers_collections::table.on(
|
||||||
ciphers_collections::collection_uuid.eq(collections_groups::collections_uuid).and(ciphers_collections::cipher_uuid.eq(&cipher_uuid))
|
ciphers_collections::collection_uuid.eq(collections_groups::collections_uuid).and(ciphers_collections::cipher_uuid.eq(&cipher_uuid))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,16 +46,6 @@ pub enum SendType {
|
||||||
File = 1,
|
File = 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum SendAuthType {
|
|
||||||
#[allow(dead_code)]
|
|
||||||
// Send requires email OTP verification
|
|
||||||
Email = 0, // Not yet supported by Vaultwarden
|
|
||||||
// Send requires a password
|
|
||||||
Password = 1,
|
|
||||||
// Send requires no auth
|
|
||||||
None = 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Send {
|
impl Send {
|
||||||
pub fn new(atype: i32, name: String, data: String, akey: String, deletion_date: NaiveDateTime) -> Self {
|
pub fn new(atype: i32, name: String, data: String, akey: String, deletion_date: NaiveDateTime) -> Self {
|
||||||
let now = Utc::now().naive_utc();
|
let now = Utc::now().naive_utc();
|
||||||
|
|
@ -155,7 +145,6 @@ impl Send {
|
||||||
"maxAccessCount": self.max_access_count,
|
"maxAccessCount": self.max_access_count,
|
||||||
"accessCount": self.access_count,
|
"accessCount": self.access_count,
|
||||||
"password": self.password_hash.as_deref().map(|h| BASE64URL_NOPAD.encode(h)),
|
"password": self.password_hash.as_deref().map(|h| BASE64URL_NOPAD.encode(h)),
|
||||||
"authType": if self.password_hash.is_some() { SendAuthType::Password as i32 } else { SendAuthType::None as i32 },
|
|
||||||
"disabled": self.disabled,
|
"disabled": self.disabled,
|
||||||
"hideEmail": self.hide_email,
|
"hideEmail": self.hide_email,
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,18 +54,11 @@ pub struct SsoAuth {
|
||||||
pub auth_response: Option<OIDCAuthenticatedUser>,
|
pub auth_response: Option<OIDCAuthenticatedUser>,
|
||||||
pub created_at: NaiveDateTime,
|
pub created_at: NaiveDateTime,
|
||||||
pub updated_at: NaiveDateTime,
|
pub updated_at: NaiveDateTime,
|
||||||
pub binding_hash: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Local methods
|
/// Local methods
|
||||||
impl SsoAuth {
|
impl SsoAuth {
|
||||||
pub fn new(
|
pub fn new(state: OIDCState, client_challenge: OIDCCodeChallenge, nonce: String, redirect_uri: String) -> Self {
|
||||||
state: OIDCState,
|
|
||||||
client_challenge: OIDCCodeChallenge,
|
|
||||||
nonce: String,
|
|
||||||
redirect_uri: String,
|
|
||||||
binding_hash: Option<String>,
|
|
||||||
) -> Self {
|
|
||||||
let now = Utc::now().naive_utc();
|
let now = Utc::now().naive_utc();
|
||||||
|
|
||||||
SsoAuth {
|
SsoAuth {
|
||||||
|
|
@ -77,7 +70,6 @@ impl SsoAuth {
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
code_response: None,
|
code_response: None,
|
||||||
auth_response: None,
|
auth_response: None,
|
||||||
binding_hash,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ pub struct TwoFactor {
|
||||||
pub last_used: i64,
|
pub last_used: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
#[derive(num_derive::FromPrimitive)]
|
#[derive(num_derive::FromPrimitive)]
|
||||||
pub enum TwoFactorType {
|
pub enum TwoFactorType {
|
||||||
Authenticator = 0,
|
Authenticator = 0,
|
||||||
|
|
|
||||||
|
|
@ -185,14 +185,13 @@ impl User {
|
||||||
/// These routes are able to use the previous stamp id for the next 2 minutes.
|
/// These routes are able to use the previous stamp id for the next 2 minutes.
|
||||||
/// After these 2 minutes this stamp will expire.
|
/// After these 2 minutes this stamp will expire.
|
||||||
///
|
///
|
||||||
pub async fn set_password(
|
pub fn set_password(
|
||||||
&mut self,
|
&mut self,
|
||||||
password: &str,
|
password: &str,
|
||||||
new_key: Option<String>,
|
new_key: Option<String>,
|
||||||
reset_security_stamp: bool,
|
reset_security_stamp: bool,
|
||||||
allow_next_route: Option<Vec<String>>,
|
allow_next_route: Option<Vec<String>>,
|
||||||
conn: &DbConn,
|
) {
|
||||||
) -> EmptyResult {
|
|
||||||
self.password_hash = crypto::hash_password(password.as_bytes(), &self.salt, self.password_iterations as u32);
|
self.password_hash = crypto::hash_password(password.as_bytes(), &self.salt, self.password_iterations as u32);
|
||||||
|
|
||||||
if let Some(route) = allow_next_route {
|
if let Some(route) = allow_next_route {
|
||||||
|
|
@ -204,15 +203,12 @@ impl User {
|
||||||
}
|
}
|
||||||
|
|
||||||
if reset_security_stamp {
|
if reset_security_stamp {
|
||||||
self.reset_security_stamp(conn).await?;
|
self.reset_security_stamp()
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn reset_security_stamp(&mut self, conn: &DbConn) -> EmptyResult {
|
pub fn reset_security_stamp(&mut self) {
|
||||||
self.security_stamp = get_uuid();
|
self.security_stamp = get_uuid();
|
||||||
Device::rotate_refresh_tokens_by_user(&self.uuid, conn).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the stamp_exception to only allow a subsequent request matching a specific route using the current security-stamp.
|
/// Set the stamp_exception to only allow a subsequent request matching a specific route using the current security-stamp.
|
||||||
|
|
@ -235,15 +231,6 @@ impl User {
|
||||||
pub fn reset_stamp_exception(&mut self) {
|
pub fn reset_stamp_exception(&mut self) {
|
||||||
self.stamp_exception = None;
|
self.stamp_exception = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn display_name(&self) -> &str {
|
|
||||||
// default to email if name is empty
|
|
||||||
if !&self.name.is_empty() {
|
|
||||||
&self.name
|
|
||||||
} else {
|
|
||||||
&self.email
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Database methods
|
/// Database methods
|
||||||
|
|
|
||||||
|
|
@ -265,7 +265,6 @@ table! {
|
||||||
auth_response -> Nullable<Text>,
|
auth_response -> Nullable<Text>,
|
||||||
created_at -> Timestamp,
|
created_at -> Timestamp,
|
||||||
updated_at -> Timestamp,
|
updated_at -> Timestamp,
|
||||||
binding_hash -> Nullable<Text>,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -342,16 +341,6 @@ table! {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
table! {
|
|
||||||
archives (user_uuid, cipher_uuid) {
|
|
||||||
user_uuid -> Text,
|
|
||||||
cipher_uuid -> Text,
|
|
||||||
archived_at -> Timestamp,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
joinable!(archives -> users (user_uuid));
|
|
||||||
joinable!(archives -> ciphers (cipher_uuid));
|
|
||||||
joinable!(attachments -> ciphers (cipher_uuid));
|
joinable!(attachments -> ciphers (cipher_uuid));
|
||||||
joinable!(ciphers -> organizations (organization_uuid));
|
joinable!(ciphers -> organizations (organization_uuid));
|
||||||
joinable!(ciphers -> users (user_uuid));
|
joinable!(ciphers -> users (user_uuid));
|
||||||
|
|
@ -383,7 +372,6 @@ joinable!(auth_requests -> users (user_uuid));
|
||||||
joinable!(sso_users -> users (user_uuid));
|
joinable!(sso_users -> users (user_uuid));
|
||||||
|
|
||||||
allow_tables_to_appear_in_same_query!(
|
allow_tables_to_appear_in_same_query!(
|
||||||
archives,
|
|
||||||
attachments,
|
attachments,
|
||||||
ciphers,
|
ciphers,
|
||||||
ciphers_collections,
|
ciphers_collections,
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
use std::{
|
use std::{
|
||||||
fmt,
|
fmt,
|
||||||
net::{IpAddr, SocketAddr},
|
net::{IpAddr, SocketAddr},
|
||||||
|
str::FromStr,
|
||||||
sync::{Arc, LazyLock, Mutex},
|
sync::{Arc, LazyLock, Mutex},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
use hickory_resolver::{net::runtime::TokioRuntimeProvider, TokioResolver};
|
use hickory_resolver::{name_server::TokioConnectionProvider, TokioResolver};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use reqwest::{
|
use reqwest::{
|
||||||
dns::{Name, Resolve, Resolving},
|
dns::{Name, Resolve, Resolving},
|
||||||
|
|
@ -58,6 +59,16 @@ pub fn get_reqwest_client_builder() -> ClientBuilder {
|
||||||
.timeout(Duration::from_secs(10))
|
.timeout(Duration::from_secs(10))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn should_block_address(domain_or_ip: &str) -> bool {
|
||||||
|
if let Ok(ip) = IpAddr::from_str(domain_or_ip) {
|
||||||
|
if should_block_ip(ip) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
should_block_address_regex(domain_or_ip)
|
||||||
|
}
|
||||||
|
|
||||||
fn should_block_ip(ip: IpAddr) -> bool {
|
fn should_block_ip(ip: IpAddr) -> bool {
|
||||||
if !CONFIG.http_request_block_non_global_ips() {
|
if !CONFIG.http_request_block_non_global_ips() {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -89,54 +100,11 @@ fn should_block_address_regex(domain_or_ip: &str) -> bool {
|
||||||
is_match
|
is_match
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_valid_host(host: &str) -> Result<Host, CustomHttpClientError> {
|
fn should_block_host(host: &Host<&str>) -> Result<(), CustomHttpClientError> {
|
||||||
let Ok(host) = Host::parse(host) else {
|
|
||||||
return Err(CustomHttpClientError::Invalid {
|
|
||||||
domain: host.to_string(),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Some extra checks to validate hosts
|
|
||||||
match host {
|
|
||||||
Host::Domain(ref domain) => {
|
|
||||||
// Host::parse() does not verify length or all possible invalid characters
|
|
||||||
// We do some extra checks here to prevent issues
|
|
||||||
if domain.len() > 253 {
|
|
||||||
debug!("Domain validation error: '{domain}' exceeds 253 characters");
|
|
||||||
return Err(CustomHttpClientError::Invalid {
|
|
||||||
domain: host.to_string(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if !domain.split('.').all(|label| {
|
|
||||||
!label.is_empty()
|
|
||||||
// Labels can't be longer than 63 chars
|
|
||||||
&& label.len() <= 63
|
|
||||||
// Labels are not allowed to start or end with a hyphen `-`
|
|
||||||
&& !label.starts_with('-')
|
|
||||||
&& !label.ends_with('-')
|
|
||||||
// Only ASCII Alphanumeric characters are allowed
|
|
||||||
// We already received a punycoded domain back, so no unicode should exists here
|
|
||||||
&& label.chars().all(|c| c.is_ascii_alphanumeric() || c == '-')
|
|
||||||
}) {
|
|
||||||
debug!(
|
|
||||||
"Domain validation error: '{domain}' labels contain invalid characters or exceed the maximum length"
|
|
||||||
);
|
|
||||||
return Err(CustomHttpClientError::Invalid {
|
|
||||||
domain: host.to_string(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Host::Ipv4(_) | Host::Ipv6(_) => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(host)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn should_block_host<S: AsRef<str>>(host: &Host<S>) -> Result<(), CustomHttpClientError> {
|
|
||||||
let (ip, host_str): (Option<IpAddr>, String) = match host {
|
let (ip, host_str): (Option<IpAddr>, String) = match host {
|
||||||
Host::Ipv4(ip) => (Some(IpAddr::V4(*ip)), ip.to_string()),
|
Host::Ipv4(ip) => (Some(IpAddr::V4(*ip)), ip.to_string()),
|
||||||
Host::Ipv6(ip) => (Some(IpAddr::V6(*ip)), ip.to_string()),
|
Host::Ipv6(ip) => (Some(IpAddr::V6(*ip)), ip.to_string()),
|
||||||
Host::Domain(d) => (None, d.as_ref().to_string()),
|
Host::Domain(d) => (None, (*d).to_string()),
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(ip) = ip {
|
if let Some(ip) = ip {
|
||||||
|
|
@ -166,9 +134,6 @@ pub enum CustomHttpClientError {
|
||||||
domain: Option<String>,
|
domain: Option<String>,
|
||||||
ip: IpAddr,
|
ip: IpAddr,
|
||||||
},
|
},
|
||||||
Invalid {
|
|
||||||
domain: String,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CustomHttpClientError {
|
impl CustomHttpClientError {
|
||||||
|
|
@ -190,7 +155,7 @@ impl fmt::Display for CustomHttpClientError {
|
||||||
match self {
|
match self {
|
||||||
Self::Blocked {
|
Self::Blocked {
|
||||||
domain,
|
domain,
|
||||||
} => write!(f, "Blocked domain: '{domain}' matched HTTP_REQUEST_BLOCK_REGEX"),
|
} => write!(f, "Blocked domain: {domain} matched HTTP_REQUEST_BLOCK_REGEX"),
|
||||||
Self::NonGlobalIp {
|
Self::NonGlobalIp {
|
||||||
domain: Some(domain),
|
domain: Some(domain),
|
||||||
ip,
|
ip,
|
||||||
|
|
@ -198,10 +163,7 @@ impl fmt::Display for CustomHttpClientError {
|
||||||
Self::NonGlobalIp {
|
Self::NonGlobalIp {
|
||||||
domain: None,
|
domain: None,
|
||||||
ip,
|
ip,
|
||||||
} => write!(f, "IP '{ip}' is not a global IP!"),
|
} => write!(f, "IP {ip} is not a global IP!"),
|
||||||
Self::Invalid {
|
|
||||||
domain,
|
|
||||||
} => write!(f, "Invalid host: '{domain}' contains invalid characters or exceeds the maximum length"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -222,46 +184,40 @@ impl CustomDnsResolver {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new() -> Arc<Self> {
|
fn new() -> Arc<Self> {
|
||||||
TokioResolver::builder(TokioRuntimeProvider::default())
|
match TokioResolver::builder(TokioConnectionProvider::default()) {
|
||||||
.and_then(|mut builder| {
|
Ok(mut builder) => {
|
||||||
// Hickory's default since v0.26 is `Ipv6AndIpv4`, which sorts IPv6 first
|
if CONFIG.dns_prefer_ipv6() {
|
||||||
// This might cause issues on IPv4 only systems or containers
|
builder.options_mut().ip_strategy = hickory_resolver::config::LookupIpStrategy::Ipv6thenIpv4;
|
||||||
// Unless someone enabled DNS_PREFER_IPV6, use Ipv4AndIpv6, which returns IPv4 first which was our previous default
|
|
||||||
if !CONFIG.dns_prefer_ipv6() {
|
|
||||||
builder.options_mut().ip_strategy = hickory_resolver::config::LookupIpStrategy::Ipv4AndIpv6;
|
|
||||||
}
|
}
|
||||||
builder.build()
|
let resolver = builder.build();
|
||||||
})
|
Arc::new(Self::Hickory(Arc::new(resolver)))
|
||||||
.inspect_err(|e| warn!("Error creating Hickory resolver, falling back to default: {e:?}"))
|
}
|
||||||
.map(|resolver| Arc::new(Self::Hickory(Arc::new(resolver))))
|
Err(e) => {
|
||||||
.unwrap_or_else(|_| Arc::new(Self::Default()))
|
warn!("Error creating Hickory resolver, falling back to default: {e:?}");
|
||||||
|
Arc::new(Self::Default())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note that we get an iterator of addresses, but we only grab the first one for convenience
|
// Note that we get an iterator of addresses, but we only grab the first one for convenience
|
||||||
async fn resolve_domain(&self, name: &str) -> Result<Vec<SocketAddr>, BoxError> {
|
async fn resolve_domain(&self, name: &str) -> Result<Option<SocketAddr>, BoxError> {
|
||||||
pre_resolve(name)?;
|
pre_resolve(name)?;
|
||||||
|
|
||||||
let results: Vec<SocketAddr> = match self {
|
let result = match self {
|
||||||
Self::Default() => tokio::net::lookup_host((name, 0)).await?.collect(),
|
Self::Default() => tokio::net::lookup_host(name).await?.next(),
|
||||||
Self::Hickory(r) => r.lookup_ip(name).await?.iter().map(|i| SocketAddr::new(i, 0)).collect(),
|
Self::Hickory(r) => r.lookup_ip(name).await?.iter().next().map(|a| SocketAddr::new(a, 0)),
|
||||||
};
|
};
|
||||||
|
|
||||||
for addr in &results {
|
if let Some(addr) = &result {
|
||||||
post_resolve(name, addr.ip())?;
|
post_resolve(name, addr.ip())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(results)
|
Ok(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pre_resolve(name: &str) -> Result<(), CustomHttpClientError> {
|
fn pre_resolve(name: &str) -> Result<(), CustomHttpClientError> {
|
||||||
let Ok(host) = get_valid_host(name) else {
|
if should_block_address(name) {
|
||||||
return Err(CustomHttpClientError::Invalid {
|
|
||||||
domain: name.to_string(),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if should_block_host(&host).is_err() {
|
|
||||||
return Err(CustomHttpClientError::Blocked {
|
return Err(CustomHttpClientError::Blocked {
|
||||||
domain: name.to_string(),
|
domain: name.to_string(),
|
||||||
});
|
});
|
||||||
|
|
@ -286,11 +242,8 @@ impl Resolve for CustomDnsResolver {
|
||||||
let this = self.clone();
|
let this = self.clone();
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let name = name.as_str();
|
let name = name.as_str();
|
||||||
let results = this.resolve_domain(name).await?;
|
let result = this.resolve_domain(name).await?;
|
||||||
if results.is_empty() {
|
Ok::<reqwest::dns::Addrs, _>(Box::new(result.into_iter()))
|
||||||
warn!("Unable to resolve {name} to any valid IP address");
|
|
||||||
}
|
|
||||||
Ok::<reqwest::dns::Addrs, _>(Box::new(results.into_iter()))
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -352,209 +305,3 @@ pub(crate) mod aws {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::util::is_global_hardcoded;
|
|
||||||
use std::net::Ipv4Addr;
|
|
||||||
use url::Host;
|
|
||||||
|
|
||||||
// ===
|
|
||||||
// IPv4 numeric-format normalization
|
|
||||||
fn parse_to_ip(s: &str) -> Option<IpAddr> {
|
|
||||||
match Host::parse(s).ok()? {
|
|
||||||
Host::Ipv4(v4) => Some(IpAddr::V4(v4)),
|
|
||||||
Host::Ipv6(v6) => Some(IpAddr::V6(v6)),
|
|
||||||
Host::Domain(_) => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn dotted_decimal_loopback_normalizes() {
|
|
||||||
let ip = parse_to_ip("127.0.0.1").unwrap();
|
|
||||||
assert_eq!(ip, IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));
|
|
||||||
assert!(!is_global_hardcoded(ip));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn single_decimal_loopback_normalizes() {
|
|
||||||
// 127.0.0.1 == 2130706433
|
|
||||||
let ip = parse_to_ip("2130706433").unwrap();
|
|
||||||
assert_eq!(ip, IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));
|
|
||||||
assert!(!is_global_hardcoded(ip));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn hex_loopback_normalizes() {
|
|
||||||
let ip = parse_to_ip("0x7f000001").unwrap();
|
|
||||||
assert_eq!(ip, IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));
|
|
||||||
assert!(!is_global_hardcoded(ip));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn dotted_hex_loopback_normalizes() {
|
|
||||||
let ip = parse_to_ip("0x7f.0.0.1").unwrap();
|
|
||||||
assert_eq!(ip, IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));
|
|
||||||
assert!(!is_global_hardcoded(ip));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn octal_loopback_normalizes() {
|
|
||||||
// 017700000001 == 127.0.0.1
|
|
||||||
let ip = parse_to_ip("017700000001").unwrap();
|
|
||||||
assert_eq!(ip, IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));
|
|
||||||
assert!(!is_global_hardcoded(ip));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn dotted_octal_loopback_normalizes() {
|
|
||||||
let ip = parse_to_ip("0177.0.0.01").unwrap();
|
|
||||||
assert_eq!(ip, IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));
|
|
||||||
assert!(!is_global_hardcoded(ip));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn aws_metadata_decimal_blocked() {
|
|
||||||
// 169.254.169.254 == 2852039166 (link-local, AWS IMDS)
|
|
||||||
let ip = parse_to_ip("2852039166").unwrap();
|
|
||||||
assert_eq!(ip, IpAddr::V4(Ipv4Addr::new(169, 254, 169, 254)));
|
|
||||||
assert!(!is_global_hardcoded(ip));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn rfc1918_hex_blocked() {
|
|
||||||
// 10.0.0.1
|
|
||||||
let ip = parse_to_ip("0x0a000001").unwrap();
|
|
||||||
assert!(!is_global_hardcoded(ip));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn public_ip_decimal_allowed() {
|
|
||||||
// 8.8.8.8 == 134744072
|
|
||||||
let ip = parse_to_ip("134744072").unwrap();
|
|
||||||
assert_eq!(ip, IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8)));
|
|
||||||
assert!(is_global_hardcoded(ip));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===
|
|
||||||
// get_valid_host integration: numeric forms become Host::Ipv4
|
|
||||||
#[test]
|
|
||||||
fn get_valid_host_normalizes_decimal_int() {
|
|
||||||
let h = get_valid_host("2130706433").expect("valid");
|
|
||||||
assert!(matches!(h, Host::Ipv4(ip) if ip == Ipv4Addr::new(127, 0, 0, 1)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn get_valid_host_normalizes_hex() {
|
|
||||||
let h = get_valid_host("0x7f000001").expect("valid");
|
|
||||||
assert!(matches!(h, Host::Ipv4(ip) if ip == Ipv4Addr::new(127, 0, 0, 1)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn get_valid_host_normalizes_octal() {
|
|
||||||
let h = get_valid_host("017700000001").expect("valid");
|
|
||||||
assert!(matches!(h, Host::Ipv4(ip) if ip == Ipv4Addr::new(127, 0, 0, 1)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===
|
|
||||||
// IPv6 formats
|
|
||||||
#[test]
|
|
||||||
fn ipv6_loopback_blocked() {
|
|
||||||
let h = get_valid_host("[::1]").expect("valid");
|
|
||||||
let Host::Ipv6(ip) = h else {
|
|
||||||
panic!("expected v6")
|
|
||||||
};
|
|
||||||
assert!(!is_global_hardcoded(IpAddr::V6(ip)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn ipv4_mapped_in_ipv6_loopback_blocked() {
|
|
||||||
// ::ffff:127.0.0.1 — v4-mapped form; is_global_hardcoded blocks via ::ffff:0:0/96
|
|
||||||
let h = get_valid_host("[::ffff:127.0.0.1]").expect("valid");
|
|
||||||
let Host::Ipv6(ip) = h else {
|
|
||||||
panic!("expected v6")
|
|
||||||
};
|
|
||||||
assert!(!is_global_hardcoded(IpAddr::V6(ip)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn ipv6_unique_local_blocked() {
|
|
||||||
let h = get_valid_host("[fc00::1]").expect("valid");
|
|
||||||
let Host::Ipv6(ip) = h else {
|
|
||||||
panic!("expected v6")
|
|
||||||
};
|
|
||||||
assert!(!is_global_hardcoded(IpAddr::V6(ip)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===
|
|
||||||
// Punycode / IDN
|
|
||||||
#[test]
|
|
||||||
fn punycode_passthrough() {
|
|
||||||
let h = get_valid_host("xn--deadbeafcaf-lbb.test").expect("valid");
|
|
||||||
match h {
|
|
||||||
Host::Domain(d) => assert_eq!(d, "xn--deadbeafcaf-lbb.test"),
|
|
||||||
_ => panic!("expected domain"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn idn_unicode_gets_punycoded() {
|
|
||||||
let h = get_valid_host("deadbeafcafé.test").expect("valid");
|
|
||||||
match h {
|
|
||||||
Host::Domain(d) => assert_eq!(d, "xn--deadbeafcaf-lbb.test"),
|
|
||||||
_ => panic!("expected domain"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn idn_unicode_gets_punycoded_tld() {
|
|
||||||
let h = get_valid_host("deadbeaf.café").expect("valid");
|
|
||||||
match h {
|
|
||||||
Host::Domain(d) => assert_eq!(d, "deadbeaf.xn--caf-dma"),
|
|
||||||
_ => panic!("expected domain"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn idn_emoji_gets_punycoded() {
|
|
||||||
let h = get_valid_host("xn--t88h.test").expect("valid"); // 🛡️.test
|
|
||||||
match h {
|
|
||||||
Host::Domain(d) => assert_eq!(d, "xn--t88h.test"),
|
|
||||||
_ => panic!("expected domain"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn idn_unicode_to_punycode_roundtrip() {
|
|
||||||
let from_unicode = get_valid_host("🛡️.test").expect("valid");
|
|
||||||
let from_puny = get_valid_host("xn--t88h.test").expect("valid");
|
|
||||||
match (from_unicode, from_puny) {
|
|
||||||
(Host::Domain(a), Host::Domain(b)) => assert_eq!(a, b),
|
|
||||||
_ => panic!("expected domains"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn invalid_punycode_rejected() {
|
|
||||||
// bare invalid punycode
|
|
||||||
assert!(get_valid_host("xn--").is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn underscore_in_label_rejected() {
|
|
||||||
assert!(get_valid_host("dead_beaf.cafe").is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn label_too_long_rejected() {
|
|
||||||
let label = "a".repeat(64);
|
|
||||||
assert!(get_valid_host(&format!("{label}.test")).is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn domain_too_long_rejected() {
|
|
||||||
let big = "a.".repeat(130) + "test"; // > 253
|
|
||||||
assert!(get_valid_host(&big).is_err());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -302,10 +302,10 @@ pub async fn send_invite(
|
||||||
.append_pair("organizationUserId", &member_id)
|
.append_pair("organizationUserId", &member_id)
|
||||||
.append_pair("token", &invite_token);
|
.append_pair("token", &invite_token);
|
||||||
|
|
||||||
if CONFIG.sso_enabled() && CONFIG.sso_only() {
|
if CONFIG.sso_enabled() {
|
||||||
|
query_params.append_pair("orgUserHasExistingUser", "false");
|
||||||
query_params.append_pair("orgSsoIdentifier", &org_id);
|
query_params.append_pair("orgSsoIdentifier", &org_id);
|
||||||
}
|
} else if user.private_key.is_some() {
|
||||||
if user.private_key.is_some() {
|
|
||||||
query_params.append_pair("orgUserHasExistingUser", "true");
|
query_params.append_pair("orgUserHasExistingUser", "true");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
43
src/main.rs
43
src/main.rs
|
|
@ -126,7 +126,7 @@ fn parse_args() {
|
||||||
exit(0);
|
exit(0);
|
||||||
} else if pargs.contains(["-v", "--version"]) {
|
} else if pargs.contains(["-v", "--version"]) {
|
||||||
config::SKIP_CONFIG_VALIDATION.store(true, Ordering::Relaxed);
|
config::SKIP_CONFIG_VALIDATION.store(true, Ordering::Relaxed);
|
||||||
let web_vault_version = util::get_active_web_release();
|
let web_vault_version = util::get_web_vault_version();
|
||||||
println!("Vaultwarden {version}");
|
println!("Vaultwarden {version}");
|
||||||
println!("Web-Vault {web_vault_version}");
|
println!("Web-Vault {web_vault_version}");
|
||||||
exit(0);
|
exit(0);
|
||||||
|
|
@ -558,12 +558,6 @@ async fn launch_rocket(pool: db::DbPool, extra_debug: bool) -> Result<(), Error>
|
||||||
let basepath = &CONFIG.domain_path();
|
let basepath = &CONFIG.domain_path();
|
||||||
|
|
||||||
let mut config = rocket::Config::from(rocket::Config::figment());
|
let mut config = rocket::Config::from(rocket::Config::figment());
|
||||||
|
|
||||||
// We install our own signal handlers below; disable Rocket's built-in handlers
|
|
||||||
config.shutdown.ctrlc = false;
|
|
||||||
#[cfg(unix)]
|
|
||||||
config.shutdown.signals.clear();
|
|
||||||
|
|
||||||
config.temp_dir = canonicalize(CONFIG.tmp_folder()).unwrap().into();
|
config.temp_dir = canonicalize(CONFIG.tmp_folder()).unwrap().into();
|
||||||
config.cli_colors = false; // Make sure Rocket does not color any values for logging.
|
config.cli_colors = false; // Make sure Rocket does not color any values for logging.
|
||||||
config.limits = Limits::new()
|
config.limits = Limits::new()
|
||||||
|
|
@ -595,7 +589,11 @@ async fn launch_rocket(pool: db::DbPool, extra_debug: bool) -> Result<(), Error>
|
||||||
|
|
||||||
CONFIG.set_rocket_shutdown_handle(instance.shutdown());
|
CONFIG.set_rocket_shutdown_handle(instance.shutdown());
|
||||||
|
|
||||||
spawn_shutdown_signal_handler();
|
tokio::spawn(async move {
|
||||||
|
tokio::signal::ctrl_c().await.expect("Error setting Ctrl-C handler");
|
||||||
|
info!("Exiting Vaultwarden!");
|
||||||
|
CONFIG.shutdown();
|
||||||
|
});
|
||||||
|
|
||||||
#[cfg(all(unix, sqlite))]
|
#[cfg(all(unix, sqlite))]
|
||||||
{
|
{
|
||||||
|
|
@ -623,35 +621,6 @@ async fn launch_rocket(pool: db::DbPool, extra_debug: bool) -> Result<(), Error>
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
fn spawn_shutdown_signal_handler() {
|
|
||||||
tokio::spawn(async move {
|
|
||||||
use tokio::signal::unix::signal;
|
|
||||||
|
|
||||||
let mut sigint = signal(SignalKind::interrupt()).expect("Error setting SIGINT handler");
|
|
||||||
let mut sigterm = signal(SignalKind::terminate()).expect("Error setting SIGTERM handler");
|
|
||||||
let mut sigquit = signal(SignalKind::quit()).expect("Error setting SIGQUIT handler");
|
|
||||||
|
|
||||||
let signal_name = tokio::select! {
|
|
||||||
_ = sigint.recv() => "SIGINT",
|
|
||||||
_ = sigterm.recv() => "SIGTERM",
|
|
||||||
_ = sigquit.recv() => "SIGQUIT",
|
|
||||||
};
|
|
||||||
|
|
||||||
info!("Received {signal_name}, initiating graceful shutdown");
|
|
||||||
CONFIG.shutdown();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(unix))]
|
|
||||||
fn spawn_shutdown_signal_handler() {
|
|
||||||
tokio::spawn(async move {
|
|
||||||
tokio::signal::ctrl_c().await.expect("Error setting Ctrl-C handler");
|
|
||||||
info!("Received Ctrl-C, initiating graceful shutdown");
|
|
||||||
CONFIG.shutdown();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn schedule_jobs(pool: db::DbPool) {
|
fn schedule_jobs(pool: db::DbPool) {
|
||||||
if CONFIG.job_poll_interval_ms() == 0 {
|
if CONFIG.job_poll_interval_ms() == 0 {
|
||||||
info!("Job scheduler disabled.");
|
info!("Job scheduler disabled.");
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ use crate::{
|
||||||
CONFIG,
|
CONFIG,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub static FAKE_SSO_IDENTIFIER: &str = "00000000-01DC-01DC-01DC-000000000000";
|
pub static FAKE_IDENTIFIER: &str = "VW_DUMMY_IDENTIFIER_FOR_OIDC";
|
||||||
|
|
||||||
static SSO_JWT_ISSUER: LazyLock<String> = LazyLock::new(|| format!("{}|sso", CONFIG.domain_origin()));
|
static SSO_JWT_ISSUER: LazyLock<String> = LazyLock::new(|| format!("{}|sso", CONFIG.domain_origin()));
|
||||||
|
|
||||||
|
|
@ -188,7 +188,6 @@ pub async fn authorize_url(
|
||||||
client_challenge: OIDCCodeChallenge,
|
client_challenge: OIDCCodeChallenge,
|
||||||
client_id: &str,
|
client_id: &str,
|
||||||
raw_redirect_uri: &str,
|
raw_redirect_uri: &str,
|
||||||
binding_hash: Option<String>,
|
|
||||||
conn: DbConn,
|
conn: DbConn,
|
||||||
) -> ApiResult<Url> {
|
) -> ApiResult<Url> {
|
||||||
let redirect_uri = match client_id {
|
let redirect_uri = match client_id {
|
||||||
|
|
@ -204,7 +203,7 @@ pub async fn authorize_url(
|
||||||
_ => err!(format!("Unsupported client {client_id}")),
|
_ => err!(format!("Unsupported client {client_id}")),
|
||||||
};
|
};
|
||||||
|
|
||||||
let (auth_url, sso_auth) = Client::authorize_url(state, client_challenge, redirect_uri, binding_hash).await?;
|
let (auth_url, sso_auth) = Client::authorize_url(state, client_challenge, redirect_uri).await?;
|
||||||
sso_auth.save(&conn).await?;
|
sso_auth.save(&conn).await?;
|
||||||
Ok(auth_url)
|
Ok(auth_url)
|
||||||
}
|
}
|
||||||
|
|
@ -284,7 +283,7 @@ pub async fn exchange_code(
|
||||||
|
|
||||||
let email_verified = id_claims.email_verified().or(user_info.email_verified());
|
let email_verified = id_claims.email_verified().or(user_info.email_verified());
|
||||||
|
|
||||||
let user_name = id_claims.preferred_username().or(user_info.preferred_username()).map(|un| un.to_string());
|
let user_name = id_claims.preferred_username().map(|un| un.to_string());
|
||||||
|
|
||||||
let refresh_token = token_response.refresh_token().map(|t| t.secret());
|
let refresh_token = token_response.refresh_token().map(|t| t.secret());
|
||||||
if refresh_token.is_none() && CONFIG.sso_scopes_vec().contains(&"offline_access".to_string()) {
|
if refresh_token.is_none() && CONFIG.sso_scopes_vec().contains(&"offline_access".to_string()) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use std::{borrow::Cow, sync::LazyLock, time::Duration};
|
use std::{borrow::Cow, sync::LazyLock, time::Duration};
|
||||||
|
|
||||||
|
use mini_moka::sync::Cache;
|
||||||
use openidconnect::{core::*, reqwest, *};
|
use openidconnect::{core::*, reqwest, *};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
@ -12,14 +13,9 @@ use crate::{
|
||||||
};
|
};
|
||||||
|
|
||||||
static CLIENT_CACHE_KEY: LazyLock<String> = LazyLock::new(|| "sso-client".to_string());
|
static CLIENT_CACHE_KEY: LazyLock<String> = LazyLock::new(|| "sso-client".to_string());
|
||||||
static CLIENT_CACHE: LazyLock<moka::sync::Cache<String, Client>> = LazyLock::new(|| {
|
static CLIENT_CACHE: LazyLock<Cache<String, Client>> = LazyLock::new(|| {
|
||||||
moka::sync::Cache::builder()
|
Cache::builder().max_capacity(1).time_to_live(Duration::from_secs(CONFIG.sso_client_cache_expiration())).build()
|
||||||
.max_capacity(1)
|
|
||||||
.time_to_live(Duration::from_secs(CONFIG.sso_client_cache_expiration()))
|
|
||||||
.build()
|
|
||||||
});
|
});
|
||||||
static REFRESH_CACHE: LazyLock<moka::future::Cache<String, Result<RefreshTokenResponse, String>>> =
|
|
||||||
LazyLock::new(|| moka::future::Cache::builder().max_capacity(1000).time_to_live(Duration::from_secs(30)).build());
|
|
||||||
|
|
||||||
/// OpenID Connect Core client.
|
/// OpenID Connect Core client.
|
||||||
pub type CustomClient = openidconnect::Client<
|
pub type CustomClient = openidconnect::Client<
|
||||||
|
|
@ -42,8 +38,6 @@ pub type CustomClient = openidconnect::Client<
|
||||||
EndpointSet,
|
EndpointSet,
|
||||||
>;
|
>;
|
||||||
|
|
||||||
pub type RefreshTokenResponse = (Option<String>, String, Option<Duration>);
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Client {
|
pub struct Client {
|
||||||
pub http_client: reqwest::Client,
|
pub http_client: reqwest::Client,
|
||||||
|
|
@ -117,7 +111,6 @@ impl Client {
|
||||||
state: OIDCState,
|
state: OIDCState,
|
||||||
client_challenge: OIDCCodeChallenge,
|
client_challenge: OIDCCodeChallenge,
|
||||||
redirect_uri: String,
|
redirect_uri: String,
|
||||||
binding_hash: Option<String>,
|
|
||||||
) -> ApiResult<(Url, SsoAuth)> {
|
) -> ApiResult<(Url, SsoAuth)> {
|
||||||
let scopes = CONFIG.sso_scopes_vec().into_iter().map(Scope::new);
|
let scopes = CONFIG.sso_scopes_vec().into_iter().map(Scope::new);
|
||||||
let base64_state = data_encoding::BASE64.encode(state.to_string().as_bytes());
|
let base64_state = data_encoding::BASE64.encode(state.to_string().as_bytes());
|
||||||
|
|
@ -140,7 +133,7 @@ impl Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
let (auth_url, _, nonce) = auth_req.url();
|
let (auth_url, _, nonce) = auth_req.url();
|
||||||
Ok((auth_url, SsoAuth::new(state, client_challenge, nonce.secret().clone(), redirect_uri, binding_hash)))
|
Ok((auth_url, SsoAuth::new(state, client_challenge, nonce.secret().clone(), redirect_uri)))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn exchange_code(
|
pub async fn exchange_code(
|
||||||
|
|
@ -238,29 +231,23 @@ impl Client {
|
||||||
verifier
|
verifier
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn exchange_refresh_token(refresh_token: String) -> ApiResult<RefreshTokenResponse> {
|
pub async fn exchange_refresh_token(
|
||||||
let client = Client::cached().await?;
|
refresh_token: String,
|
||||||
|
) -> ApiResult<(Option<String>, String, Option<Duration>)> {
|
||||||
REFRESH_CACHE
|
|
||||||
.get_with(refresh_token.clone(), async move { client._exchange_refresh_token(refresh_token).await })
|
|
||||||
.await
|
|
||||||
.map_err(Into::into)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn _exchange_refresh_token(&self, refresh_token: String) -> Result<RefreshTokenResponse, String> {
|
|
||||||
let rt = RefreshToken::new(refresh_token);
|
let rt = RefreshToken::new(refresh_token);
|
||||||
|
|
||||||
match self.core_client.exchange_refresh_token(&rt).request_async(&self.http_client).await {
|
let client = Client::cached().await?;
|
||||||
Err(err) => {
|
let token_response =
|
||||||
error!("Request to exchange_refresh_token endpoint failed: {err}");
|
match client.core_client.exchange_refresh_token(&rt).request_async(&client.http_client).await {
|
||||||
Err(format!("Request to exchange_refresh_token endpoint failed: {err}"))
|
Err(err) => err!(format!("Request to exchange_refresh_token endpoint failed: {:?}", err)),
|
||||||
}
|
Ok(token_response) => token_response,
|
||||||
Ok(token_response) => Ok((
|
};
|
||||||
token_response.refresh_token().map(|token| token.secret().clone()),
|
|
||||||
token_response.access_token().secret().clone(),
|
Ok((
|
||||||
token_response.expires_in(),
|
token_response.refresh_token().map(|token| token.secret().clone()),
|
||||||
)),
|
token_response.access_token().secret().clone(),
|
||||||
}
|
token_response.expires_in(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -111,8 +111,7 @@
|
||||||
"microsoftstore.com",
|
"microsoftstore.com",
|
||||||
"xbox.com",
|
"xbox.com",
|
||||||
"azure.com",
|
"azure.com",
|
||||||
"windowsazure.com",
|
"windowsazure.com"
|
||||||
"cloud.microsoft"
|
|
||||||
],
|
],
|
||||||
"excluded": false
|
"excluded": false
|
||||||
},
|
},
|
||||||
|
|
@ -972,13 +971,5 @@
|
||||||
"pinterest.se"
|
"pinterest.se"
|
||||||
],
|
],
|
||||||
"excluded": false
|
"excluded": false
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": 91,
|
|
||||||
"domains": [
|
|
||||||
"twitter.com",
|
|
||||||
"x.com"
|
|
||||||
],
|
|
||||||
"excluded": false
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
15
src/static/scripts/admin.css
vendored
15
src/static/scripts/admin.css
vendored
|
|
@ -1,17 +1,6 @@
|
||||||
body {
|
body {
|
||||||
padding-top: 75px;
|
padding-top: 75px;
|
||||||
}
|
}
|
||||||
/* Some extra width's for the main layout */
|
|
||||||
@media (min-width: 1600px) {
|
|
||||||
.container-xxl {
|
|
||||||
max-width: 1520px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media (min-width: 1800px) {
|
|
||||||
.container-xxl {
|
|
||||||
max-width: 1720px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
img {
|
img {
|
||||||
width: 48px;
|
width: 48px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
|
|
@ -49,8 +38,8 @@ img {
|
||||||
max-width: 130px;
|
max-width: 130px;
|
||||||
}
|
}
|
||||||
#users-table .vw-actions, #orgs-table .vw-actions {
|
#users-table .vw-actions, #orgs-table .vw-actions {
|
||||||
min-width: 170px;
|
min-width: 155px;
|
||||||
max-width: 180px;
|
max-width: 160px;
|
||||||
}
|
}
|
||||||
#users-table .vw-org-cell {
|
#users-table .vw-org-cell {
|
||||||
max-height: 120px;
|
max-height: 120px;
|
||||||
|
|
|
||||||
19
src/static/scripts/admin_diagnostics.js
vendored
19
src/static/scripts/admin_diagnostics.js
vendored
|
|
@ -29,7 +29,7 @@ function isValidIp(ip) {
|
||||||
return ipv4Regex.test(ip) || ipv6Regex.test(ip);
|
return ipv4Regex.test(ip) || ipv6Regex.test(ip);
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkVersions(platform, installed, latest, commit=null, compare_order=0) {
|
function checkVersions(platform, installed, latest, commit=null, pre_release=false) {
|
||||||
if (installed === "-" || latest === "-") {
|
if (installed === "-" || latest === "-") {
|
||||||
document.getElementById(`${platform}-failed`).classList.remove("d-none");
|
document.getElementById(`${platform}-failed`).classList.remove("d-none");
|
||||||
return;
|
return;
|
||||||
|
|
@ -37,7 +37,7 @@ function checkVersions(platform, installed, latest, commit=null, compare_order=0
|
||||||
|
|
||||||
// Only check basic versions, no commit revisions
|
// Only check basic versions, no commit revisions
|
||||||
if (commit === null || installed.indexOf("-") === -1) {
|
if (commit === null || installed.indexOf("-") === -1) {
|
||||||
if (platform === "web" && compare_order === 1) {
|
if (platform === "web" && pre_release === true) {
|
||||||
document.getElementById(`${platform}-prerelease`).classList.remove("d-none");
|
document.getElementById(`${platform}-prerelease`).classList.remove("d-none");
|
||||||
} else if (installed == latest) {
|
} else if (installed == latest) {
|
||||||
document.getElementById(`${platform}-success`).classList.remove("d-none");
|
document.getElementById(`${platform}-success`).classList.remove("d-none");
|
||||||
|
|
@ -83,7 +83,7 @@ async function generateSupportString(event, dj) {
|
||||||
let supportString = "### Your environment (Generated via diagnostics page)\n\n";
|
let supportString = "### Your environment (Generated via diagnostics page)\n\n";
|
||||||
|
|
||||||
supportString += `* Vaultwarden version: v${dj.current_release}\n`;
|
supportString += `* Vaultwarden version: v${dj.current_release}\n`;
|
||||||
supportString += `* Web-vault version: v${dj.active_web_release}\n`;
|
supportString += `* Web-vault version: v${dj.web_vault_version}\n`;
|
||||||
supportString += `* OS/Arch: ${dj.host_os}/${dj.host_arch}\n`;
|
supportString += `* OS/Arch: ${dj.host_os}/${dj.host_arch}\n`;
|
||||||
supportString += `* Running within a container: ${dj.running_within_container} (Base: ${dj.container_base_image})\n`;
|
supportString += `* Running within a container: ${dj.running_within_container} (Base: ${dj.container_base_image})\n`;
|
||||||
supportString += `* Database type: ${dj.db_type}\n`;
|
supportString += `* Database type: ${dj.db_type}\n`;
|
||||||
|
|
@ -109,9 +109,6 @@ async function generateSupportString(event, dj) {
|
||||||
supportString += "* Websocket Check: disabled\n";
|
supportString += "* Websocket Check: disabled\n";
|
||||||
}
|
}
|
||||||
supportString += `* HTTP Response Checks: ${httpResponseCheck}\n`;
|
supportString += `* HTTP Response Checks: ${httpResponseCheck}\n`;
|
||||||
if (dj.invalid_feature_flags != "") {
|
|
||||||
supportString += `* Invalid feature flags: true\n`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const jsonResponse = await fetch(`${BASE_URL}/admin/diagnostics/config`, {
|
const jsonResponse = await fetch(`${BASE_URL}/admin/diagnostics/config`, {
|
||||||
"headers": { "Accept": "application/json" }
|
"headers": { "Accept": "application/json" }
|
||||||
|
|
@ -131,10 +128,6 @@ async function generateSupportString(event, dj) {
|
||||||
supportString += `\n**Environment settings which are overridden:** ${dj.overrides}\n`;
|
supportString += `\n**Environment settings which are overridden:** ${dj.overrides}\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dj.invalid_feature_flags != "") {
|
|
||||||
supportString += `\n**Invalid feature flags:** ${dj.invalid_feature_flags}\n`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add http response check messages if they exists
|
// Add http response check messages if they exists
|
||||||
if (httpResponseCheck === false) {
|
if (httpResponseCheck === false) {
|
||||||
supportString += "\n**Failed HTTP Checks:**\n";
|
supportString += "\n**Failed HTTP Checks:**\n";
|
||||||
|
|
@ -215,9 +208,9 @@ function initVersionCheck(dj) {
|
||||||
}
|
}
|
||||||
checkVersions("server", serverInstalled, serverLatest, serverLatestCommit);
|
checkVersions("server", serverInstalled, serverLatest, serverLatestCommit);
|
||||||
|
|
||||||
const webInstalled = dj.active_web_release;
|
const webInstalled = dj.web_vault_version;
|
||||||
const webLatest = dj.latest_web_release;
|
const webLatest = dj.latest_web_build;
|
||||||
checkVersions("web", webInstalled, webLatest, null, dj.web_vault_compare);
|
checkVersions("web", webInstalled, webLatest, null, dj.web_vault_pre_release);
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkDns(dns_resolved) {
|
function checkDns(dns_resolved) {
|
||||||
|
|
|
||||||
111
src/static/scripts/datatables.css
vendored
111
src/static/scripts/datatables.css
vendored
|
|
@ -4,10 +4,10 @@
|
||||||
*
|
*
|
||||||
* To rebuild or modify this file with the latest versions of the included
|
* To rebuild or modify this file with the latest versions of the included
|
||||||
* software please visit:
|
* software please visit:
|
||||||
* https://datatables.net/download/#bs5/dt-2.3.7
|
* https://datatables.net/download/#bs5/dt-2.3.5
|
||||||
*
|
*
|
||||||
* Included libraries:
|
* Included libraries:
|
||||||
* DataTables 2.3.7
|
* DataTables 2.3.5
|
||||||
*/
|
*/
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
|
|
@ -88,42 +88,42 @@ table.dataTable thead > tr > th:active,
|
||||||
table.dataTable thead > tr > td:active {
|
table.dataTable thead > tr > td:active {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
table.dataTable thead > tr > th.dt-orderable-asc .dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-asc .dt-column-order:before,
|
table.dataTable thead > tr > th.dt-orderable-asc span.dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-asc span.dt-column-order:before,
|
||||||
table.dataTable thead > tr > td.dt-orderable-asc .dt-column-order:before,
|
table.dataTable thead > tr > td.dt-orderable-asc span.dt-column-order:before,
|
||||||
table.dataTable thead > tr > td.dt-ordering-asc .dt-column-order:before {
|
table.dataTable thead > tr > td.dt-ordering-asc span.dt-column-order:before {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: block;
|
display: block;
|
||||||
bottom: 50%;
|
bottom: 50%;
|
||||||
content: "\25B2";
|
content: "\25B2";
|
||||||
content: "\25B2"/"";
|
content: "\25B2"/"";
|
||||||
}
|
}
|
||||||
table.dataTable thead > tr > th.dt-orderable-desc .dt-column-order:after, table.dataTable thead > tr > th.dt-ordering-desc .dt-column-order:after,
|
table.dataTable thead > tr > th.dt-orderable-desc span.dt-column-order:after, table.dataTable thead > tr > th.dt-ordering-desc span.dt-column-order:after,
|
||||||
table.dataTable thead > tr > td.dt-orderable-desc .dt-column-order:after,
|
table.dataTable thead > tr > td.dt-orderable-desc span.dt-column-order:after,
|
||||||
table.dataTable thead > tr > td.dt-ordering-desc .dt-column-order:after {
|
table.dataTable thead > tr > td.dt-ordering-desc span.dt-column-order:after {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: block;
|
display: block;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
content: "\25BC";
|
content: "\25BC";
|
||||||
content: "\25BC"/"";
|
content: "\25BC"/"";
|
||||||
}
|
}
|
||||||
table.dataTable thead > tr > th.dt-orderable-asc .dt-column-order, table.dataTable thead > tr > th.dt-orderable-desc .dt-column-order, table.dataTable thead > tr > th.dt-ordering-asc .dt-column-order, table.dataTable thead > tr > th.dt-ordering-desc .dt-column-order,
|
table.dataTable thead > tr > th.dt-orderable-asc span.dt-column-order, table.dataTable thead > tr > th.dt-orderable-desc span.dt-column-order, table.dataTable thead > tr > th.dt-ordering-asc span.dt-column-order, table.dataTable thead > tr > th.dt-ordering-desc span.dt-column-order,
|
||||||
table.dataTable thead > tr > td.dt-orderable-asc .dt-column-order,
|
table.dataTable thead > tr > td.dt-orderable-asc span.dt-column-order,
|
||||||
table.dataTable thead > tr > td.dt-orderable-desc .dt-column-order,
|
table.dataTable thead > tr > td.dt-orderable-desc span.dt-column-order,
|
||||||
table.dataTable thead > tr > td.dt-ordering-asc .dt-column-order,
|
table.dataTable thead > tr > td.dt-ordering-asc span.dt-column-order,
|
||||||
table.dataTable thead > tr > td.dt-ordering-desc .dt-column-order {
|
table.dataTable thead > tr > td.dt-ordering-desc span.dt-column-order {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 12px;
|
width: 12px;
|
||||||
height: 20px;
|
height: 24px;
|
||||||
}
|
}
|
||||||
table.dataTable thead > tr > th.dt-orderable-asc .dt-column-order:before, table.dataTable thead > tr > th.dt-orderable-asc .dt-column-order:after, table.dataTable thead > tr > th.dt-orderable-desc .dt-column-order:before, table.dataTable thead > tr > th.dt-orderable-desc .dt-column-order:after, table.dataTable thead > tr > th.dt-ordering-asc .dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-asc .dt-column-order:after, table.dataTable thead > tr > th.dt-ordering-desc .dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-desc .dt-column-order:after,
|
table.dataTable thead > tr > th.dt-orderable-asc span.dt-column-order:before, table.dataTable thead > tr > th.dt-orderable-asc span.dt-column-order:after, table.dataTable thead > tr > th.dt-orderable-desc span.dt-column-order:before, table.dataTable thead > tr > th.dt-orderable-desc span.dt-column-order:after, table.dataTable thead > tr > th.dt-ordering-asc span.dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-asc span.dt-column-order:after, table.dataTable thead > tr > th.dt-ordering-desc span.dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-desc span.dt-column-order:after,
|
||||||
table.dataTable thead > tr > td.dt-orderable-asc .dt-column-order:before,
|
table.dataTable thead > tr > td.dt-orderable-asc span.dt-column-order:before,
|
||||||
table.dataTable thead > tr > td.dt-orderable-asc .dt-column-order:after,
|
table.dataTable thead > tr > td.dt-orderable-asc span.dt-column-order:after,
|
||||||
table.dataTable thead > tr > td.dt-orderable-desc .dt-column-order:before,
|
table.dataTable thead > tr > td.dt-orderable-desc span.dt-column-order:before,
|
||||||
table.dataTable thead > tr > td.dt-orderable-desc .dt-column-order:after,
|
table.dataTable thead > tr > td.dt-orderable-desc span.dt-column-order:after,
|
||||||
table.dataTable thead > tr > td.dt-ordering-asc .dt-column-order:before,
|
table.dataTable thead > tr > td.dt-ordering-asc span.dt-column-order:before,
|
||||||
table.dataTable thead > tr > td.dt-ordering-asc .dt-column-order:after,
|
table.dataTable thead > tr > td.dt-ordering-asc span.dt-column-order:after,
|
||||||
table.dataTable thead > tr > td.dt-ordering-desc .dt-column-order:before,
|
table.dataTable thead > tr > td.dt-ordering-desc span.dt-column-order:before,
|
||||||
table.dataTable thead > tr > td.dt-ordering-desc .dt-column-order:after {
|
table.dataTable thead > tr > td.dt-ordering-desc span.dt-column-order:after {
|
||||||
left: 0;
|
left: 0;
|
||||||
opacity: 0.125;
|
opacity: 0.125;
|
||||||
line-height: 9px;
|
line-height: 9px;
|
||||||
|
|
@ -140,15 +140,15 @@ table.dataTable thead > tr > td.dt-orderable-desc:hover {
|
||||||
outline: 2px solid rgba(0, 0, 0, 0.05);
|
outline: 2px solid rgba(0, 0, 0, 0.05);
|
||||||
outline-offset: -2px;
|
outline-offset: -2px;
|
||||||
}
|
}
|
||||||
table.dataTable thead > tr > th.dt-ordering-asc .dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-desc .dt-column-order:after,
|
table.dataTable thead > tr > th.dt-ordering-asc span.dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-desc span.dt-column-order:after,
|
||||||
table.dataTable thead > tr > td.dt-ordering-asc .dt-column-order:before,
|
table.dataTable thead > tr > td.dt-ordering-asc span.dt-column-order:before,
|
||||||
table.dataTable thead > tr > td.dt-ordering-desc .dt-column-order:after {
|
table.dataTable thead > tr > td.dt-ordering-desc span.dt-column-order:after {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
table.dataTable thead > tr > th.dt-orderable-none:not(.dt-ordering-asc, .dt-ordering-desc) .dt-column-order:empty, table.dataTable thead > tr > th.sorting_desc_disabled .dt-column-order:after, table.dataTable thead > tr > th.sorting_asc_disabled .dt-column-order:before,
|
table.dataTable thead > tr > th.dt-orderable-none:not(.dt-ordering-asc, .dt-ordering-desc) span.dt-column-order:empty, table.dataTable thead > tr > th.sorting_desc_disabled span.dt-column-order:after, table.dataTable thead > tr > th.sorting_asc_disabled span.dt-column-order:before,
|
||||||
table.dataTable thead > tr > td.dt-orderable-none:not(.dt-ordering-asc, .dt-ordering-desc) .dt-column-order:empty,
|
table.dataTable thead > tr > td.dt-orderable-none:not(.dt-ordering-asc, .dt-ordering-desc) span.dt-column-order:empty,
|
||||||
table.dataTable thead > tr > td.sorting_desc_disabled .dt-column-order:after,
|
table.dataTable thead > tr > td.sorting_desc_disabled span.dt-column-order:after,
|
||||||
table.dataTable thead > tr > td.sorting_asc_disabled .dt-column-order:before {
|
table.dataTable thead > tr > td.sorting_asc_disabled span.dt-column-order:before {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
table.dataTable thead > tr > th:active,
|
table.dataTable thead > tr > th:active,
|
||||||
|
|
@ -169,24 +169,24 @@ table.dataTable tfoot > tr > td div.dt-column-footer {
|
||||||
align-items: var(--dt-header-align-items);
|
align-items: var(--dt-header-align-items);
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
table.dataTable thead > tr > th div.dt-column-header .dt-column-title,
|
table.dataTable thead > tr > th div.dt-column-header span.dt-column-title,
|
||||||
table.dataTable thead > tr > th div.dt-column-footer .dt-column-title,
|
table.dataTable thead > tr > th div.dt-column-footer span.dt-column-title,
|
||||||
table.dataTable thead > tr > td div.dt-column-header .dt-column-title,
|
table.dataTable thead > tr > td div.dt-column-header span.dt-column-title,
|
||||||
table.dataTable thead > tr > td div.dt-column-footer .dt-column-title,
|
table.dataTable thead > tr > td div.dt-column-footer span.dt-column-title,
|
||||||
table.dataTable tfoot > tr > th div.dt-column-header .dt-column-title,
|
table.dataTable tfoot > tr > th div.dt-column-header span.dt-column-title,
|
||||||
table.dataTable tfoot > tr > th div.dt-column-footer .dt-column-title,
|
table.dataTable tfoot > tr > th div.dt-column-footer span.dt-column-title,
|
||||||
table.dataTable tfoot > tr > td div.dt-column-header .dt-column-title,
|
table.dataTable tfoot > tr > td div.dt-column-header span.dt-column-title,
|
||||||
table.dataTable tfoot > tr > td div.dt-column-footer .dt-column-title {
|
table.dataTable tfoot > tr > td div.dt-column-footer span.dt-column-title {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
table.dataTable thead > tr > th div.dt-column-header .dt-column-title:empty,
|
table.dataTable thead > tr > th div.dt-column-header span.dt-column-title:empty,
|
||||||
table.dataTable thead > tr > th div.dt-column-footer .dt-column-title:empty,
|
table.dataTable thead > tr > th div.dt-column-footer span.dt-column-title:empty,
|
||||||
table.dataTable thead > tr > td div.dt-column-header .dt-column-title:empty,
|
table.dataTable thead > tr > td div.dt-column-header span.dt-column-title:empty,
|
||||||
table.dataTable thead > tr > td div.dt-column-footer .dt-column-title:empty,
|
table.dataTable thead > tr > td div.dt-column-footer span.dt-column-title:empty,
|
||||||
table.dataTable tfoot > tr > th div.dt-column-header .dt-column-title:empty,
|
table.dataTable tfoot > tr > th div.dt-column-header span.dt-column-title:empty,
|
||||||
table.dataTable tfoot > tr > th div.dt-column-footer .dt-column-title:empty,
|
table.dataTable tfoot > tr > th div.dt-column-footer span.dt-column-title:empty,
|
||||||
table.dataTable tfoot > tr > td div.dt-column-header .dt-column-title:empty,
|
table.dataTable tfoot > tr > td div.dt-column-header span.dt-column-title:empty,
|
||||||
table.dataTable tfoot > tr > td div.dt-column-footer .dt-column-title:empty {
|
table.dataTable tfoot > tr > td div.dt-column-footer span.dt-column-title:empty {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -588,16 +588,16 @@ table.dataTable.table-sm > thead > tr td.dt-ordering-asc,
|
||||||
table.dataTable.table-sm > thead > tr td.dt-ordering-desc {
|
table.dataTable.table-sm > thead > tr td.dt-ordering-desc {
|
||||||
padding-right: 0.25rem;
|
padding-right: 0.25rem;
|
||||||
}
|
}
|
||||||
table.dataTable.table-sm > thead > tr th.dt-orderable-asc .dt-column-order, table.dataTable.table-sm > thead > tr th.dt-orderable-desc .dt-column-order, table.dataTable.table-sm > thead > tr th.dt-ordering-asc .dt-column-order, table.dataTable.table-sm > thead > tr th.dt-ordering-desc .dt-column-order,
|
table.dataTable.table-sm > thead > tr th.dt-orderable-asc span.dt-column-order, table.dataTable.table-sm > thead > tr th.dt-orderable-desc span.dt-column-order, table.dataTable.table-sm > thead > tr th.dt-ordering-asc span.dt-column-order, table.dataTable.table-sm > thead > tr th.dt-ordering-desc span.dt-column-order,
|
||||||
table.dataTable.table-sm > thead > tr td.dt-orderable-asc .dt-column-order,
|
table.dataTable.table-sm > thead > tr td.dt-orderable-asc span.dt-column-order,
|
||||||
table.dataTable.table-sm > thead > tr td.dt-orderable-desc .dt-column-order,
|
table.dataTable.table-sm > thead > tr td.dt-orderable-desc span.dt-column-order,
|
||||||
table.dataTable.table-sm > thead > tr td.dt-ordering-asc .dt-column-order,
|
table.dataTable.table-sm > thead > tr td.dt-ordering-asc span.dt-column-order,
|
||||||
table.dataTable.table-sm > thead > tr td.dt-ordering-desc .dt-column-order {
|
table.dataTable.table-sm > thead > tr td.dt-ordering-desc span.dt-column-order {
|
||||||
right: 0.25rem;
|
right: 0.25rem;
|
||||||
}
|
}
|
||||||
table.dataTable.table-sm > thead > tr th.dt-type-date .dt-column-order, table.dataTable.table-sm > thead > tr th.dt-type-numeric .dt-column-order,
|
table.dataTable.table-sm > thead > tr th.dt-type-date span.dt-column-order, table.dataTable.table-sm > thead > tr th.dt-type-numeric span.dt-column-order,
|
||||||
table.dataTable.table-sm > thead > tr td.dt-type-date .dt-column-order,
|
table.dataTable.table-sm > thead > tr td.dt-type-date span.dt-column-order,
|
||||||
table.dataTable.table-sm > thead > tr td.dt-type-numeric .dt-column-order {
|
table.dataTable.table-sm > thead > tr td.dt-type-numeric span.dt-column-order {
|
||||||
left: 0.25rem;
|
left: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -606,8 +606,7 @@ div.dt-scroll-head table.table-bordered {
|
||||||
}
|
}
|
||||||
|
|
||||||
div.table-responsive > div.dt-container > div.row {
|
div.table-responsive > div.dt-container > div.row {
|
||||||
margin-left: 0;
|
margin: 0;
|
||||||
margin-right: 0;
|
|
||||||
}
|
}
|
||||||
div.table-responsive > div.dt-container > div.row > div[class^=col-]:first-child {
|
div.table-responsive > div.dt-container > div.row > div[class^=col-]:first-child {
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
|
|
|
||||||
84
src/static/scripts/datatables.js
vendored
84
src/static/scripts/datatables.js
vendored
|
|
@ -4,13 +4,13 @@
|
||||||
*
|
*
|
||||||
* To rebuild or modify this file with the latest versions of the included
|
* To rebuild or modify this file with the latest versions of the included
|
||||||
* software please visit:
|
* software please visit:
|
||||||
* https://datatables.net/download/#bs5/dt-2.3.7
|
* https://datatables.net/download/#bs5/dt-2.3.5
|
||||||
*
|
*
|
||||||
* Included libraries:
|
* Included libraries:
|
||||||
* DataTables 2.3.7
|
* DataTables 2.3.5
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/*! DataTables 2.3.7
|
/*! DataTables 2.3.5
|
||||||
* © SpryMedia Ltd - datatables.net/license
|
* © SpryMedia Ltd - datatables.net/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
@ -186,7 +186,7 @@
|
||||||
"sDestroyWidth": $this[0].style.width,
|
"sDestroyWidth": $this[0].style.width,
|
||||||
"sInstance": sId,
|
"sInstance": sId,
|
||||||
"sTableId": sId,
|
"sTableId": sId,
|
||||||
colgroup: $('<colgroup>'),
|
colgroup: $('<colgroup>').prependTo(this),
|
||||||
fastData: function (row, column, type) {
|
fastData: function (row, column, type) {
|
||||||
return _fnGetCellData(oSettings, row, column, type);
|
return _fnGetCellData(oSettings, row, column, type);
|
||||||
}
|
}
|
||||||
|
|
@ -259,7 +259,6 @@
|
||||||
"orderHandler",
|
"orderHandler",
|
||||||
"titleRow",
|
"titleRow",
|
||||||
"typeDetect",
|
"typeDetect",
|
||||||
"columnTitleTag",
|
|
||||||
[ "iCookieDuration", "iStateDuration" ], // backwards compat
|
[ "iCookieDuration", "iStateDuration" ], // backwards compat
|
||||||
[ "oSearch", "oPreviousSearch" ],
|
[ "oSearch", "oPreviousSearch" ],
|
||||||
[ "aoSearchCols", "aoPreSearchCols" ],
|
[ "aoSearchCols", "aoPreSearchCols" ],
|
||||||
|
|
@ -424,7 +423,7 @@
|
||||||
|
|
||||||
if ( oSettings.caption ) {
|
if ( oSettings.caption ) {
|
||||||
if ( caption.length === 0 ) {
|
if ( caption.length === 0 ) {
|
||||||
caption = $('<caption/>').prependTo( $this );
|
caption = $('<caption/>').appendTo( $this );
|
||||||
}
|
}
|
||||||
|
|
||||||
caption.html( oSettings.caption );
|
caption.html( oSettings.caption );
|
||||||
|
|
@ -437,14 +436,6 @@
|
||||||
oSettings.captionNode = caption[0];
|
oSettings.captionNode = caption[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Place the colgroup element in the correct location for the HTML structure
|
|
||||||
if (caption.length) {
|
|
||||||
oSettings.colgroup.insertAfter(caption);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
oSettings.colgroup.prependTo(oSettings.nTable);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( thead.length === 0 ) {
|
if ( thead.length === 0 ) {
|
||||||
thead = $('<thead/>').appendTo($this);
|
thead = $('<thead/>').appendTo($this);
|
||||||
}
|
}
|
||||||
|
|
@ -525,7 +516,7 @@
|
||||||
*
|
*
|
||||||
* @type string
|
* @type string
|
||||||
*/
|
*/
|
||||||
builder: "bs5/dt-2.3.7",
|
builder: "bs5/dt-2.3.5",
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Buttons. For use with the Buttons extension for DataTables. This is
|
* Buttons. For use with the Buttons extension for DataTables. This is
|
||||||
|
|
@ -1301,7 +1292,7 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
// Replaceable function in api.util
|
// Replaceable function in api.util
|
||||||
var _stripHtml = function (input, replacement) {
|
var _stripHtml = function (input) {
|
||||||
if (! input || typeof input !== 'string') {
|
if (! input || typeof input !== 'string') {
|
||||||
return input;
|
return input;
|
||||||
}
|
}
|
||||||
|
|
@ -1313,7 +1304,7 @@
|
||||||
|
|
||||||
var previous;
|
var previous;
|
||||||
|
|
||||||
input = input.replace(_re_html, replacement || ''); // Complete tags
|
input = input.replace(_re_html, ''); // Complete tags
|
||||||
|
|
||||||
// Safety for incomplete script tag - use do / while to ensure that
|
// Safety for incomplete script tag - use do / while to ensure that
|
||||||
// we get all instances
|
// we get all instances
|
||||||
|
|
@ -1778,7 +1769,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
stripHtml: function (mixed, replacement) {
|
stripHtml: function (mixed) {
|
||||||
var type = typeof mixed;
|
var type = typeof mixed;
|
||||||
|
|
||||||
if (type === 'function') {
|
if (type === 'function') {
|
||||||
|
|
@ -1786,7 +1777,7 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
else if (type === 'string') {
|
else if (type === 'string') {
|
||||||
return _stripHtml(mixed, replacement);
|
return _stripHtml(mixed);
|
||||||
}
|
}
|
||||||
return mixed;
|
return mixed;
|
||||||
},
|
},
|
||||||
|
|
@ -3388,7 +3379,7 @@
|
||||||
colspan++;
|
colspan++;
|
||||||
}
|
}
|
||||||
|
|
||||||
var titleSpan = $('.dt-column-title', cell);
|
var titleSpan = $('span.dt-column-title', cell);
|
||||||
|
|
||||||
structure[row][column] = {
|
structure[row][column] = {
|
||||||
cell: cell,
|
cell: cell,
|
||||||
|
|
@ -4102,8 +4093,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wrap the column title so we can write to it in future
|
// Wrap the column title so we can write to it in future
|
||||||
if ( $('.dt-column-title', cell).length === 0) {
|
if ( $('span.dt-column-title', cell).length === 0) {
|
||||||
$(document.createElement(settings.columnTitleTag))
|
$('<span>')
|
||||||
.addClass('dt-column-title')
|
.addClass('dt-column-title')
|
||||||
.append(cell.childNodes)
|
.append(cell.childNodes)
|
||||||
.appendTo(cell);
|
.appendTo(cell);
|
||||||
|
|
@ -4114,9 +4105,9 @@
|
||||||
isHeader &&
|
isHeader &&
|
||||||
jqCell.filter(':not([data-dt-order=disable])').length !== 0 &&
|
jqCell.filter(':not([data-dt-order=disable])').length !== 0 &&
|
||||||
jqCell.parent(':not([data-dt-order=disable])').length !== 0 &&
|
jqCell.parent(':not([data-dt-order=disable])').length !== 0 &&
|
||||||
$('.dt-column-order', cell).length === 0
|
$('span.dt-column-order', cell).length === 0
|
||||||
) {
|
) {
|
||||||
$(document.createElement(settings.columnTitleTag))
|
$('<span>')
|
||||||
.addClass('dt-column-order')
|
.addClass('dt-column-order')
|
||||||
.appendTo(cell);
|
.appendTo(cell);
|
||||||
}
|
}
|
||||||
|
|
@ -4125,7 +4116,7 @@
|
||||||
// layout for those elements
|
// layout for those elements
|
||||||
var headerFooter = isHeader ? 'header' : 'footer';
|
var headerFooter = isHeader ? 'header' : 'footer';
|
||||||
|
|
||||||
if ( $('div.dt-column-' + headerFooter, cell).length === 0) {
|
if ( $('span.dt-column-' + headerFooter, cell).length === 0) {
|
||||||
$('<div>')
|
$('<div>')
|
||||||
.addClass('dt-column-' + headerFooter)
|
.addClass('dt-column-' + headerFooter)
|
||||||
.append(cell.childNodes)
|
.append(cell.childNodes)
|
||||||
|
|
@ -4282,10 +4273,6 @@
|
||||||
// Custom Ajax option to submit the parameters as a JSON string
|
// Custom Ajax option to submit the parameters as a JSON string
|
||||||
if (baseAjax.submitAs === 'json' && typeof data === 'object') {
|
if (baseAjax.submitAs === 'json' && typeof data === 'object') {
|
||||||
baseAjax.data = JSON.stringify(data);
|
baseAjax.data = JSON.stringify(data);
|
||||||
|
|
||||||
if (!baseAjax.contentType) {
|
|
||||||
baseAjax.contentType = 'application/json; charset=utf-8';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof ajax === 'function') {
|
if (typeof ajax === 'function') {
|
||||||
|
|
@ -5544,7 +5531,7 @@
|
||||||
var autoClass = _ext.type.className[column.sType];
|
var autoClass = _ext.type.className[column.sType];
|
||||||
var padding = column.sContentPadding || (scrollX ? '-' : '');
|
var padding = column.sContentPadding || (scrollX ? '-' : '');
|
||||||
var text = longest + padding;
|
var text = longest + padding;
|
||||||
var insert = longest.indexOf('<') === -1 && longest.indexOf('&') === -1
|
var insert = longest.indexOf('<') === -1
|
||||||
? document.createTextNode(text)
|
? document.createTextNode(text)
|
||||||
: text
|
: text
|
||||||
|
|
||||||
|
|
@ -5732,20 +5719,15 @@
|
||||||
.replace(/id=".*?"/g, '')
|
.replace(/id=".*?"/g, '')
|
||||||
.replace(/name=".*?"/g, '');
|
.replace(/name=".*?"/g, '');
|
||||||
|
|
||||||
// Don't want Javascript at all in these calculation cells.
|
var s = _stripHtml(cellString)
|
||||||
cellString = cellString.replace(/<script.*?<\/script>/gi, ' ');
|
|
||||||
|
|
||||||
var noHtml = _stripHtml(cellString, ' ')
|
|
||||||
.replace( / /g, ' ' );
|
.replace( / /g, ' ' );
|
||||||
|
|
||||||
// The length is calculated on the text only, but we keep the HTML
|
|
||||||
// in the string so it can be used in the calculation table
|
|
||||||
collection.push({
|
collection.push({
|
||||||
str: cellString,
|
str: s,
|
||||||
len: noHtml.length
|
len: s.length
|
||||||
});
|
});
|
||||||
|
|
||||||
allStrings.push(noHtml);
|
allStrings.push(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Order and then cut down to the size we need
|
// Order and then cut down to the size we need
|
||||||
|
|
@ -8800,7 +8782,7 @@
|
||||||
// Automatic - find the _last_ unique cell from the top that is not empty (last for
|
// Automatic - find the _last_ unique cell from the top that is not empty (last for
|
||||||
// backwards compatibility)
|
// backwards compatibility)
|
||||||
for (var i=0 ; i<header.length ; i++) {
|
for (var i=0 ; i<header.length ; i++) {
|
||||||
if (header[i][column].unique && $('.dt-column-title', header[i][column].cell).text()) {
|
if (header[i][column].unique && $('span.dt-column-title', header[i][column].cell).text()) {
|
||||||
target = i;
|
target = i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -8896,10 +8878,6 @@
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (col.responsiveVisible === false) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Selector
|
// Selector
|
||||||
if (match[1]) {
|
if (match[1]) {
|
||||||
return $(nodes[idx]).filter(match[1]).length > 0 ? idx : null;
|
return $(nodes[idx]).filter(match[1]).length > 0 ? idx : null;
|
||||||
|
|
@ -9111,7 +9089,7 @@
|
||||||
title = undefined;
|
title = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
var span = $('.dt-column-title', this.column(column).header(row));
|
var span = $('span.dt-column-title', this.column(column).header(row));
|
||||||
|
|
||||||
if (title !== undefined) {
|
if (title !== undefined) {
|
||||||
span.html(title);
|
span.html(title);
|
||||||
|
|
@ -10285,8 +10263,8 @@
|
||||||
|
|
||||||
// Needed for header and footer, so pulled into its own function
|
// Needed for header and footer, so pulled into its own function
|
||||||
function cleanHeader(node, className) {
|
function cleanHeader(node, className) {
|
||||||
$(node).find('.dt-column-order').remove();
|
$(node).find('span.dt-column-order').remove();
|
||||||
$(node).find('.dt-column-title').each(function () {
|
$(node).find('span.dt-column-title').each(function () {
|
||||||
var title = $(this).html();
|
var title = $(this).html();
|
||||||
$(this).parent().parent().append(title);
|
$(this).parent().parent().append(title);
|
||||||
$(this).remove();
|
$(this).remove();
|
||||||
|
|
@ -10304,7 +10282,7 @@
|
||||||
* @type string
|
* @type string
|
||||||
* @default Version number
|
* @default Version number
|
||||||
*/
|
*/
|
||||||
DataTable.version = "2.3.7";
|
DataTable.version = "2.3.5";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Private data store, containing all of the settings objects that are
|
* Private data store, containing all of the settings objects that are
|
||||||
|
|
@ -11472,10 +11450,7 @@
|
||||||
iDeferLoading: null,
|
iDeferLoading: null,
|
||||||
|
|
||||||
/** Event listeners */
|
/** Event listeners */
|
||||||
on: null,
|
on: null
|
||||||
|
|
||||||
/** Title wrapper element type */
|
|
||||||
columnTitleTag: 'span'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
_fnHungarianMap( DataTable.defaults );
|
_fnHungarianMap( DataTable.defaults );
|
||||||
|
|
@ -12439,10 +12414,7 @@
|
||||||
orderHandler: true,
|
orderHandler: true,
|
||||||
|
|
||||||
/** Title row indicator */
|
/** Title row indicator */
|
||||||
titleRow: null,
|
titleRow: null
|
||||||
|
|
||||||
/** Title wrapper element type */
|
|
||||||
columnTitleTag: 'span'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
2900
src/static/scripts/jdenticon-3.3.0.js
vendored
2900
src/static/scripts/jdenticon-3.3.0.js
vendored
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -27,7 +27,7 @@
|
||||||
</symbol>
|
</symbol>
|
||||||
</svg>
|
</svg>
|
||||||
<nav class="navbar navbar-expand-md navbar-dark bg-dark mb-4 shadow fixed-top">
|
<nav class="navbar navbar-expand-md navbar-dark bg-dark mb-4 shadow fixed-top">
|
||||||
<div class="container-xxl">
|
<div class="container-xl">
|
||||||
<a class="navbar-brand" href="{{urlpath}}/admin"><img class="vaultwarden-icon" src="{{urlpath}}/vw_static/vaultwarden-icon.png" alt="V">aultwarden Admin</a>
|
<a class="navbar-brand" href="{{urlpath}}/admin"><img class="vaultwarden-icon" src="{{urlpath}}/vw_static/vaultwarden-icon.png" alt="V">aultwarden Admin</a>
|
||||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarCollapse"
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarCollapse"
|
||||||
aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
|
aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<main class="container-xxl">
|
<main class="container-xl">
|
||||||
<div id="diagnostics-block" class="my-3 p-3 rounded shadow">
|
<div id="diagnostics-block" class="my-3 p-3 rounded shadow">
|
||||||
<h6 class="border-bottom pb-2 mb-2">Diagnostics</h6>
|
<h6 class="border-bottom pb-2 mb-2">Diagnostics</h6>
|
||||||
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
<dl class="row">
|
<dl class="row">
|
||||||
<dt class="col-sm-5">Server Installed
|
<dt class="col-sm-5">Server Installed
|
||||||
<span class="badge bg-success d-none abbr-badge" id="server-success" title="Latest version is installed.">Ok</span>
|
<span class="badge bg-success d-none abbr-badge" id="server-success" title="Latest version is installed.">Ok</span>
|
||||||
<span class="badge bg-warning text-dark d-none abbr-badge" id="server-warning" title="An update is available.">Update</span>
|
<span class="badge bg-warning text-dark d-none abbr-badge" id="server-warning" title="There seems to be an update available.">Update</span>
|
||||||
<span class="badge bg-info text-dark d-none abbr-badge" id="server-branch" title="This is a branched version.">Branched</span>
|
<span class="badge bg-info text-dark d-none abbr-badge" id="server-branch" title="This is a branched version.">Branched</span>
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="col-sm-7">
|
<dd class="col-sm-7">
|
||||||
|
|
@ -23,17 +23,17 @@
|
||||||
{{#if page_data.web_vault_enabled}}
|
{{#if page_data.web_vault_enabled}}
|
||||||
<dt class="col-sm-5">Web Installed
|
<dt class="col-sm-5">Web Installed
|
||||||
<span class="badge bg-success d-none abbr-badge" id="web-success" title="Latest version is installed.">Ok</span>
|
<span class="badge bg-success d-none abbr-badge" id="web-success" title="Latest version is installed.">Ok</span>
|
||||||
<span class="badge bg-warning text-dark d-none abbr-badge" id="web-warning" title="An update is available.">Update</span>
|
<span class="badge bg-warning text-dark d-none abbr-badge" id="web-warning" title="There seems to be an update available.">Update</span>
|
||||||
<span class="badge bg-info text-dark d-none abbr-badge" id="web-prerelease" title="You are using a pre-release version.">Pre-Release</span>
|
<span class="badge bg-info text-dark d-none abbr-badge" id="web-prerelease" title="You seem to be using a pre-release version.">Pre-Release</span>
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="col-sm-7">
|
<dd class="col-sm-7">
|
||||||
<span id="web-installed">{{page_data.active_web_release}}</span>
|
<span id="web-installed">{{page_data.web_vault_version}}</span>
|
||||||
</dd>
|
</dd>
|
||||||
<dt class="col-sm-5">Web Latest
|
<dt class="col-sm-5">Web Latest
|
||||||
<span class="badge bg-secondary d-none abbr-badge" id="web-failed" title="Unable to determine latest version.">Unknown</span>
|
<span class="badge bg-secondary d-none abbr-badge" id="web-failed" title="Unable to determine latest version.">Unknown</span>
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="col-sm-7">
|
<dd class="col-sm-7">
|
||||||
<span id="web-latest">{{page_data.latest_web_release}}</span>
|
<span id="web-latest">{{page_data.latest_web_build}}</span>
|
||||||
</dd>
|
</dd>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#unless page_data.web_vault_enabled}}
|
{{#unless page_data.web_vault_enabled}}
|
||||||
|
|
@ -194,14 +194,6 @@
|
||||||
<dd class="col-sm-7">
|
<dd class="col-sm-7">
|
||||||
<span id="http-response-errors" class="d-block"></span>
|
<span id="http-response-errors" class="d-block"></span>
|
||||||
</dd>
|
</dd>
|
||||||
{{#if page_data.invalid_feature_flags}}
|
|
||||||
<dt class="col-sm-5">Invalid Feature Flags
|
|
||||||
<span class="badge bg-warning text-dark abbr-badge" id="feature-flag-warning" title="Some feature flags are invalid or outdated!">Warning</span>
|
|
||||||
</dt>
|
|
||||||
<dd class="col-sm-7">
|
|
||||||
<span id="feature-flags" class="d-block"><b>Flags:</b> <span id="feature-flags-string">{{page_data.invalid_feature_flags}}</span></span>
|
|
||||||
</dd>
|
|
||||||
{{/if}}
|
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<main class="container-xxl">
|
<main class="container-xl">
|
||||||
{{#if error}}
|
{{#if error}}
|
||||||
<div class="align-items-center p-3 mb-3 text-opacity-50 text-dark bg-warning rounded shadow">
|
<div class="align-items-center p-3 mb-3 text-opacity-50 text-dark bg-warning rounded shadow">
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<main class="container-xxl">
|
<main class="container-xl">
|
||||||
<div id="organizations-block" class="my-3 p-3 rounded shadow">
|
<div id="organizations-block" class="my-3 p-3 rounded shadow">
|
||||||
<h6 class="border-bottom pb-2 mb-3">Organizations</h6>
|
<h6 class="border-bottom pb-2 mb-3">Organizations</h6>
|
||||||
<div class="table-responsive-xl small">
|
<div class="table-responsive-xl small">
|
||||||
|
|
@ -59,7 +59,7 @@
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<link rel="stylesheet" href="{{urlpath}}/vw_static/datatables.css" />
|
<link rel="stylesheet" href="{{urlpath}}/vw_static/datatables.css" />
|
||||||
<script src="{{urlpath}}/vw_static/jquery-4.0.0.slim.js"></script>
|
<script src="{{urlpath}}/vw_static/jquery-3.7.1.slim.js"></script>
|
||||||
<script src="{{urlpath}}/vw_static/datatables.js"></script>
|
<script src="{{urlpath}}/vw_static/datatables.js"></script>
|
||||||
<script src="{{urlpath}}/vw_static/admin_organizations.js"></script>
|
<script src="{{urlpath}}/vw_static/admin_organizations.js"></script>
|
||||||
<script src="{{urlpath}}/vw_static/jdenticon-3.3.0.js"></script>
|
<script src="{{urlpath}}/vw_static/jdenticon-3.3.0.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<main class="container-xxl">
|
<main class="container-xl">
|
||||||
<div id="admin_token_warning" class="alert alert-warning alert-dismissible fade show d-none">
|
<div id="admin_token_warning" class="alert alert-warning alert-dismissible fade show d-none">
|
||||||
<button type="button" class="btn-close" data-bs-target="admin_token_warning" data-bs-dismiss="alert" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-target="admin_token_warning" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
You are using a plain text `ADMIN_TOKEN` which is insecure.<br>
|
You are using a plain text `ADMIN_TOKEN` which is insecure.<br>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<main class="container-xxl">
|
<main class="container-xl">
|
||||||
<div id="users-block" class="my-3 p-3 rounded shadow">
|
<div id="users-block" class="my-3 p-3 rounded shadow">
|
||||||
<h6 class="border-bottom pb-2 mb-3">Registered Users</h6>
|
<h6 class="border-bottom pb-2 mb-3">Registered Users</h6>
|
||||||
<div class="table-responsive-xl small">
|
<div class="table-responsive-xl small">
|
||||||
|
|
@ -43,7 +43,7 @@
|
||||||
</td>
|
</td>
|
||||||
{{#if ../sso_enabled}}
|
{{#if ../sso_enabled}}
|
||||||
<td>
|
<td>
|
||||||
<span class="d-block text-break text-wrap">{{sso_identifier}}</span>
|
<span class="d-block">{{sso_identifier}}</span>
|
||||||
</td>
|
</td>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
<td>
|
<td>
|
||||||
|
|
@ -153,7 +153,7 @@
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<link rel="stylesheet" href="{{urlpath}}/vw_static/datatables.css" />
|
<link rel="stylesheet" href="{{urlpath}}/vw_static/datatables.css" />
|
||||||
<script src="{{urlpath}}/vw_static/jquery-4.0.0.slim.js"></script>
|
<script src="{{urlpath}}/vw_static/jquery-3.7.1.slim.js"></script>
|
||||||
<script src="{{urlpath}}/vw_static/datatables.js"></script>
|
<script src="{{urlpath}}/vw_static/datatables.js"></script>
|
||||||
<script src="{{urlpath}}/vw_static/admin_users.js"></script>
|
<script src="{{urlpath}}/vw_static/admin_users.js"></script>
|
||||||
<script src="{{urlpath}}/vw_static/jdenticon-3.3.0.js"></script>
|
<script src="{{urlpath}}/vw_static/jdenticon-3.3.0.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -137,14 +137,6 @@ bit-nav-logo bit-nav-item .bwi-shield {
|
||||||
app-user-layout app-danger-zone button:nth-child(1) {
|
app-user-layout app-danger-zone button:nth-child(1) {
|
||||||
@extend %vw-hide;
|
@extend %vw-hide;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hide unsupported Forwarding email alias options */
|
|
||||||
ng-dropdown-panel div.ng-dropdown-panel-items div:has(> [title="Firefox Relay"]) {
|
|
||||||
@extend %vw-hide;
|
|
||||||
}
|
|
||||||
ng-dropdown-panel div.ng-dropdown-panel-items div:has(> [title="DuckDuckGo"]) {
|
|
||||||
@extend %vw-hide;
|
|
||||||
}
|
|
||||||
/**** END Static Vaultwarden Changes ****/
|
/**** END Static Vaultwarden Changes ****/
|
||||||
/**** START Dynamic Vaultwarden Changes ****/
|
/**** START Dynamic Vaultwarden Changes ****/
|
||||||
{{#if signup_disabled}}
|
{{#if signup_disabled}}
|
||||||
|
|
@ -166,13 +158,6 @@ app-root a[routerlink="/signup"] {
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#if remember_2fa_disabled}}
|
|
||||||
/* Hide checkbox to remember 2FA token for 30 days */
|
|
||||||
app-two-factor-auth > form > bit-form-control {
|
|
||||||
@extend %vw-hide;
|
|
||||||
}
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#unless mail_2fa_enabled}}
|
{{#unless mail_2fa_enabled}}
|
||||||
/* Hide `Email` 2FA if mail is not enabled */
|
/* Hide `Email` 2FA if mail is not enabled */
|
||||||
.providers-2fa-1 {
|
.providers-2fa-1 {
|
||||||
|
|
@ -207,19 +192,6 @@ bit-nav-item[route="sends"] {
|
||||||
@extend %vw-hide;
|
@extend %vw-hide;
|
||||||
}
|
}
|
||||||
{{/unless}}
|
{{/unless}}
|
||||||
|
|
||||||
{{#unless password_hints_allowed}}
|
|
||||||
/* Hide password hints if not allowed */
|
|
||||||
a[routerlink="/hint"],
|
|
||||||
{{#if (webver "<2025.12.2")}}
|
|
||||||
app-change-password > form > .form-group:nth-child(5),
|
|
||||||
auth-input-password > form > bit-form-field:nth-child(4) {
|
|
||||||
{{else}}
|
|
||||||
.vw-password-hint {
|
|
||||||
{{/if}}
|
|
||||||
@extend %vw-hide;
|
|
||||||
}
|
|
||||||
{{/unless}}
|
|
||||||
/**** End Dynamic Vaultwarden Changes ****/
|
/**** End Dynamic Vaultwarden Changes ****/
|
||||||
/**** Include a special user stylesheet for custom changes ****/
|
/**** Include a special user stylesheet for custom changes ****/
|
||||||
{{#if load_user_scss}}
|
{{#if load_user_scss}}
|
||||||
|
|
|
||||||
79
src/util.rs
79
src/util.rs
|
|
@ -16,10 +16,7 @@ use tokio::{
|
||||||
time::{sleep, Duration},
|
time::{sleep, Duration},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{config::PathType, CONFIG};
|
||||||
config::{PathType, SUPPORTED_FEATURE_FLAGS},
|
|
||||||
CONFIG,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct AppHeaders();
|
pub struct AppHeaders();
|
||||||
|
|
||||||
|
|
@ -156,11 +153,9 @@ impl Cors {
|
||||||
fn get_allowed_origin(headers: &HeaderMap<'_>) -> Option<String> {
|
fn get_allowed_origin(headers: &HeaderMap<'_>) -> Option<String> {
|
||||||
let origin = Cors::get_header(headers, "Origin");
|
let origin = Cors::get_header(headers, "Origin");
|
||||||
let safari_extension_origin = "file://";
|
let safari_extension_origin = "file://";
|
||||||
let desktop_custom_file_origin = "bw-desktop-file://bundle";
|
|
||||||
|
|
||||||
if origin == CONFIG.domain_origin()
|
if origin == CONFIG.domain_origin()
|
||||||
|| origin == safari_extension_origin
|
|| origin == safari_extension_origin
|
||||||
|| origin == desktop_custom_file_origin
|
|
||||||
|| (CONFIG.sso_enabled() && origin == CONFIG.sso_authority())
|
|| (CONFIG.sso_enabled() && origin == CONFIG.sso_authority())
|
||||||
{
|
{
|
||||||
Some(origin)
|
Some(origin)
|
||||||
|
|
@ -536,7 +531,7 @@ struct WebVaultVersion {
|
||||||
version: String,
|
version: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_active_web_release() -> String {
|
pub fn get_web_vault_version() -> String {
|
||||||
let version_files = [
|
let version_files = [
|
||||||
format!("{}/vw-version.json", CONFIG.web_vault_folder()),
|
format!("{}/vw-version.json", CONFIG.web_vault_folder()),
|
||||||
format!("{}/version.json", CONFIG.web_vault_folder()),
|
format!("{}/version.json", CONFIG.web_vault_folder()),
|
||||||
|
|
@ -634,21 +629,6 @@ fn _process_key(key: &str) -> String {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deser_opt_nonempty_str<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
T: From<String>,
|
|
||||||
{
|
|
||||||
use serde::Deserialize;
|
|
||||||
Ok(Option::<String>::deserialize(deserializer)?.and_then(|s| {
|
|
||||||
if s.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(T::from(s))
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
pub enum NumberOrString {
|
pub enum NumberOrString {
|
||||||
|
|
@ -734,7 +714,7 @@ where
|
||||||
|
|
||||||
warn!("Can't connect to database, retrying: {e:?}");
|
warn!("Can't connect to database, retrying: {e:?}");
|
||||||
|
|
||||||
sleep(Duration::from_secs(1)).await;
|
sleep(Duration::from_millis(1_000)).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -783,28 +763,21 @@ pub fn convert_json_key_lcase_first(src_json: Value) -> Value {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum FeatureFlagFilter {
|
|
||||||
#[allow(dead_code)]
|
|
||||||
Unfiltered,
|
|
||||||
ValidOnly,
|
|
||||||
InvalidOnly,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parses the experimental client feature flags string into a HashMap.
|
/// Parses the experimental client feature flags string into a HashMap.
|
||||||
pub fn parse_experimental_client_feature_flags(
|
pub fn parse_experimental_client_feature_flags(experimental_client_feature_flags: &str) -> HashMap<String, bool> {
|
||||||
experimental_client_feature_flags: &str,
|
// These flags could still be configured, but are deprecated and not used anymore
|
||||||
filter_mode: FeatureFlagFilter,
|
// To prevent old installations from starting filter these out and not error out
|
||||||
) -> HashMap<String, bool> {
|
const DEPRECATED_FLAGS: &[&str] =
|
||||||
|
&["autofill-overlay", "autofill-v2", "browser-fileless-import", "extension-refresh", "fido2-vault-credentials"];
|
||||||
experimental_client_feature_flags
|
experimental_client_feature_flags
|
||||||
.split(',')
|
.split(',')
|
||||||
.map(str::trim)
|
.filter_map(|f| {
|
||||||
.filter(|flag| !flag.is_empty())
|
let flag = f.trim();
|
||||||
.filter(|flag| match filter_mode {
|
if !flag.is_empty() && !DEPRECATED_FLAGS.contains(&flag) {
|
||||||
FeatureFlagFilter::Unfiltered => true,
|
return Some((flag.to_owned(), true));
|
||||||
FeatureFlagFilter::ValidOnly => SUPPORTED_FEATURE_FLAGS.contains(flag),
|
}
|
||||||
FeatureFlagFilter::InvalidOnly => !SUPPORTED_FEATURE_FLAGS.contains(flag),
|
None
|
||||||
})
|
})
|
||||||
.map(|flag| (flag.to_owned(), true))
|
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -818,18 +791,14 @@ pub fn is_global_hardcoded(ip: std::net::IpAddr) -> bool {
|
||||||
std::net::IpAddr::V4(ip) => {
|
std::net::IpAddr::V4(ip) => {
|
||||||
!(ip.octets()[0] == 0 // "This network"
|
!(ip.octets()[0] == 0 // "This network"
|
||||||
|| ip.is_private()
|
|| ip.is_private()
|
||||||
|| (ip.octets()[0] == 100 && (ip.octets()[1] & 0b1100_0000 == 0b0100_0000)) // ip.is_shared()
|
|| (ip.octets()[0] == 100 && (ip.octets()[1] & 0b1100_0000 == 0b0100_0000)) //ip.is_shared()
|
||||||
|| ip.is_loopback()
|
|| ip.is_loopback()
|
||||||
|| ip.is_link_local()
|
|| ip.is_link_local()
|
||||||
// addresses reserved for future protocols (`192.0.0.0/24`)
|
// addresses reserved for future protocols (`192.0.0.0/24`)
|
||||||
// .9 and .10 are documented as globally reachable so they're excluded
|
||(ip.octets()[0] == 192 && ip.octets()[1] == 0 && ip.octets()[2] == 0)
|
||||||
|| (
|
|
||||||
ip.octets()[0] == 192 && ip.octets()[1] == 0 && ip.octets()[2] == 0
|
|
||||||
&& ip.octets()[3] != 9 && ip.octets()[3] != 10
|
|
||||||
)
|
|
||||||
|| ip.is_documentation()
|
|| ip.is_documentation()
|
||||||
|| (ip.octets()[0] == 198 && (ip.octets()[1] & 0xfe) == 18) // ip.is_benchmarking()
|
|| (ip.octets()[0] == 198 && (ip.octets()[1] & 0xfe) == 18) // ip.is_benchmarking()
|
||||||
|| (ip.octets()[0] & 240 == 240 && !ip.is_broadcast()) // ip.is_reserved()
|
|| (ip.octets()[0] & 240 == 240 && !ip.is_broadcast()) //ip.is_reserved()
|
||||||
|| ip.is_broadcast())
|
|| ip.is_broadcast())
|
||||||
}
|
}
|
||||||
std::net::IpAddr::V6(ip) => {
|
std::net::IpAddr::V6(ip) => {
|
||||||
|
|
@ -853,17 +822,11 @@ pub fn is_global_hardcoded(ip: std::net::IpAddr) -> bool {
|
||||||
// AS112-v6 (`2001:4:112::/48`)
|
// AS112-v6 (`2001:4:112::/48`)
|
||||||
|| matches!(ip.segments(), [0x2001, 4, 0x112, _, _, _, _, _])
|
|| matches!(ip.segments(), [0x2001, 4, 0x112, _, _, _, _, _])
|
||||||
// ORCHIDv2 (`2001:20::/28`)
|
// ORCHIDv2 (`2001:20::/28`)
|
||||||
// Drone Remote ID Protocol Entity Tags (DETs) Prefix (`2001:30::/28`)`
|
|| matches!(ip.segments(), [0x2001, b, _, _, _, _, _, _] if (0x20..=0x2F).contains(&b))
|
||||||
|| matches!(ip.segments(), [0x2001, b, _, _, _, _, _, _] if (0x20..=0x3F).contains(&b))
|
|
||||||
))
|
))
|
||||||
// 6to4 (`2002::/16`) – it's not explicitly documented as globally reachable,
|
|| ((ip.segments()[0] == 0x2001) && (ip.segments()[1] == 0xdb8)) // ip.is_documentation()
|
||||||
// IANA says N/A.
|
|| ((ip.segments()[0] & 0xfe00) == 0xfc00) //ip.is_unique_local()
|
||||||
|| matches!(ip.segments(), [0x2002, _, _, _, _, _, _, _])
|
|| ((ip.segments()[0] & 0xffc0) == 0xfe80)) //ip.is_unicast_link_local()
|
||||||
|| matches!(ip.segments(), [0x2001, 0xdb8, ..] | [0x3fff, 0..=0x0fff, ..]) // ip.is_documentation()
|
|
||||||
// Segment Routing (SRv6) SIDs (`5f00::/16`)
|
|
||||||
|| matches!(ip.segments(), [0x5f00, ..])
|
|
||||||
|| ip.is_unique_local()
|
|
||||||
|| ip.is_unicast_link_local())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -79,4 +79,3 @@ for name, domain_list in domain_lists.items():
|
||||||
# Write out the global domains JSON file.
|
# Write out the global domains JSON file.
|
||||||
with open(file=OUTPUT_FILE, mode='w', encoding='utf-8') as f:
|
with open(file=OUTPUT_FILE, mode='w', encoding='utf-8') as f:
|
||||||
json.dump(global_domains, f, indent=2)
|
json.dump(global_domains, f, indent=2)
|
||||||
f.write("\n")
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue