Browse Source

Merge branch 'main' into main

pull/6388/head
Henning 4 weeks ago
committed by GitHub
parent
commit
96fe363ee5
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 43
      .github/workflows/build.yml
  2. 2
      .github/workflows/check-templates.yml
  3. 7
      .github/workflows/hadolint.yml
  4. 28
      .github/workflows/release.yml
  5. 2
      .github/workflows/releasecache-cleanup.yml
  6. 8
      .github/workflows/trivy.yml
  7. 22
      .github/workflows/typos.yml
  8. 4
      .github/workflows/zizmor.yml
  9. 14
      .pre-commit-config.yaml
  10. 26
      .typos.toml
  11. 1343
      Cargo.lock
  12. 137
      Cargo.toml
  13. 6
      build.rs
  14. 10
      docker/DockerSettings.yaml
  15. 26
      docker/Dockerfile.alpine
  16. 16
      docker/Dockerfile.debian
  17. 6
      docker/Dockerfile.j2
  18. 4
      docker/README.md
  19. 10
      macros/Cargo.toml
  20. 2
      playwright/README.md
  21. 35
      playwright/compose/keycloak/Dockerfile
  22. 12
      playwright/compose/keycloak/setup.sh
  23. 8
      playwright/compose/playwright/Dockerfile
  24. 9
      playwright/docker-compose.yml
  25. 2
      playwright/global-utils.ts
  26. 544
      playwright/package-lock.json
  27. 14
      playwright/package.json
  28. 2
      rust-toolchain.toml
  29. 231
      src/api/admin.rs
  30. 271
      src/api/core/accounts.rs
  31. 347
      src/api/core/ciphers.rs
  32. 174
      src/api/core/emergency_access.rs
  33. 41
      src/api/core/events.rs
  34. 35
      src/api/core/folders.rs
  35. 18
      src/api/core/mod.rs
  36. 681
      src/api/core/organizations.rs
  37. 55
      src/api/core/public.rs
  38. 150
      src/api/core/sends.rs
  39. 42
      src/api/core/two_factor/authenticator.rs
  40. 24
      src/api/core/two_factor/duo.rs
  41. 10
      src/api/core/two_factor/duo_oidc.rs
  42. 44
      src/api/core/two_factor/email.rs
  43. 58
      src/api/core/two_factor/mod.rs
  44. 53
      src/api/core/two_factor/protected_actions.rs
  45. 49
      src/api/core/two_factor/webauthn.rs
  46. 18
      src/api/core/two_factor/yubikey.rs
  47. 22
      src/api/icons.rs
  48. 65
      src/api/identity.rs
  49. 2
      src/api/mod.rs
  50. 64
      src/api/notifications.rs
  51. 36
      src/api/push.rs
  52. 82
      src/auth.rs
  53. 344
      src/config.rs
  54. 4
      src/crypto.rs
  55. 579
      src/db/mod.rs
  56. 74
      src/db/models/attachment.rs
  57. 94
      src/db/models/auth_request.rs
  58. 233
      src/db/models/cipher.rs
  59. 184
      src/db/models/collection.rs
  60. 110
      src/db/models/device.rs
  61. 139
      src/db/models/emergency_access.rs
  62. 89
      src/db/models/event.rs
  63. 30
      src/db/models/favorite.rs
  64. 85
      src/db/models/folder.rs
  65. 157
      src/db/models/group.rs
  66. 74
      src/db/models/org_policy.rs
  67. 331
      src/db/models/organization.rs
  68. 128
      src/db/models/send.rs
  69. 34
      src/db/models/sso_nonce.rs
  70. 59
      src/db/models/two_factor.rs
  71. 100
      src/db/models/two_factor_duo_context.rs
  72. 60
      src/db/models/two_factor_incomplete.rs
  73. 238
      src/db/models/user.rs
  74. 57
      src/db/query_logger.rs
  75. 0
      src/db/schema.rs
  76. 395
      src/db/schemas/mysql/schema.rs
  77. 395
      src/db/schemas/sqlite/schema.rs
  78. 184
      src/error.rs
  79. 20
      src/http_client.rs
  80. 4
      src/mail.rs
  81. 77
      src/main.rs
  82. 7
      src/ratelimit.rs
  83. 32
      src/sso.rs
  84. 18
      src/sso_client.rs
  85. 2
      src/util.rs

43
.github/workflows/build.yml

@ -14,6 +14,7 @@ on:
- "diesel.toml"
- "docker/Dockerfile.j2"
- "docker/DockerSettings.yaml"
- "macros/**"
pull_request:
paths:
@ -27,13 +28,11 @@ on:
- "diesel.toml"
- "docker/Dockerfile.j2"
- "docker/DockerSettings.yaml"
- "macros/**"
jobs:
build:
name: Build and Test ${{ matrix.channel }}
permissions:
actions: write
contents: read
runs-on: ubuntu-24.04
timeout-minutes: 120
# Make warnings errors, this is to prevent warnings slipping through.
@ -69,9 +68,9 @@ jobs:
CHANNEL: ${{ matrix.channel }}
run: |
if [[ "${CHANNEL}" == 'rust-toolchain' ]]; then
RUST_TOOLCHAIN="$(grep -oP 'channel.*"(\K.*?)(?=")' rust-toolchain.toml)"
RUST_TOOLCHAIN="$(grep -m1 -oP 'channel.*"(\K.*?)(?=")' rust-toolchain.toml)"
elif [[ "${CHANNEL}" == 'msrv' ]]; then
RUST_TOOLCHAIN="$(grep -oP 'rust-version.*"(\K.*?)(?=")' Cargo.toml)"
RUST_TOOLCHAIN="$(grep -m1 -oP 'rust-version\s.*"(\K.*?)(?=")' Cargo.toml)"
else
RUST_TOOLCHAIN="${CHANNEL}"
fi
@ -81,7 +80,7 @@ jobs:
# Only install the clippy and rustfmt components on the default rust-toolchain
- name: "Install rust-toolchain version"
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master @ Aug 23, 2025, 3:20 AM GMT+2
uses: dtolnay/rust-toolchain@6d653acede28d24f02e3cd41383119e8b1b35921 # master @ Sep 16, 2025, 8:37 PM GMT+2
if: ${{ matrix.channel == 'rust-toolchain' }}
with:
toolchain: "${{steps.toolchain.outputs.RUST_TOOLCHAIN}}"
@ -91,7 +90,7 @@ jobs:
# 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@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master @ Aug 23, 2025, 3:20 AM GMT+2
uses: dtolnay/rust-toolchain@6d653acede28d24f02e3cd41383119e8b1b35921 # master @ Sep 16, 2025, 8:37 PM GMT+2
if: ${{ matrix.channel != 'rust-toolchain' }}
with:
toolchain: "${{steps.toolchain.outputs.RUST_TOOLCHAIN}}"
@ -116,60 +115,60 @@ jobs:
# Enable Rust Caching
- name: Rust Caching
uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with:
# Use a custom prefix-key to force a fresh start. This is sometimes needed with bigger changes.
# Like changing the build host from Ubuntu 20.04 to 22.04 for example.
# Only update when really needed! Use a <year>.<month>[.<inc>] format.
prefix-key: "v2023.07-rust"
prefix-key: "v2025.09-rust"
# End Enable Rust Caching
# Run cargo tests
# First test all features together, afterwards test them separately.
- name: "test features: sqlite,mysql,postgresql,enable_mimalloc,query_logger"
id: test_sqlite_mysql_postgresql_mimalloc_logger
- name: "test features: sqlite,mysql,postgresql,enable_mimalloc,s3"
id: test_sqlite_mysql_postgresql_mimalloc_s3
if: ${{ !cancelled() }}
run: |
cargo test --features sqlite,mysql,postgresql,enable_mimalloc,query_logger
cargo test --profile ci --features sqlite,mysql,postgresql,enable_mimalloc,s3
- name: "test features: sqlite,mysql,postgresql,enable_mimalloc"
id: test_sqlite_mysql_postgresql_mimalloc
if: ${{ !cancelled() }}
run: |
cargo test --features sqlite,mysql,postgresql,enable_mimalloc
cargo test --profile ci --features sqlite,mysql,postgresql,enable_mimalloc
- name: "test features: sqlite,mysql,postgresql"
id: test_sqlite_mysql_postgresql
if: ${{ !cancelled() }}
run: |
cargo test --features sqlite,mysql,postgresql
cargo test --profile ci --features sqlite,mysql,postgresql
- name: "test features: sqlite"
id: test_sqlite
if: ${{ !cancelled() }}
run: |
cargo test --features sqlite
cargo test --profile ci --features sqlite
- name: "test features: mysql"
id: test_mysql
if: ${{ !cancelled() }}
run: |
cargo test --features mysql
cargo test --profile ci --features mysql
- name: "test features: postgresql"
id: test_postgresql
if: ${{ !cancelled() }}
run: |
cargo test --features postgresql
cargo test --profile ci --features postgresql
# End Run cargo tests
# Run cargo clippy, and fail on warnings
- name: "clippy features: sqlite,mysql,postgresql,enable_mimalloc"
- name: "clippy features: sqlite,mysql,postgresql,enable_mimalloc,s3"
id: clippy
if: ${{ !cancelled() && matrix.channel == 'rust-toolchain' }}
run: |
cargo clippy --features sqlite,mysql,postgresql,enable_mimalloc
cargo clippy --profile ci --features sqlite,mysql,postgresql,enable_mimalloc,s3
# End Run cargo clippy
@ -187,7 +186,7 @@ jobs:
- name: "Some checks failed"
if: ${{ failure() }}
env:
TEST_DB_M_L: ${{ steps.test_sqlite_mysql_postgresql_mimalloc_logger.outcome }}
TEST_DB_M_S3: ${{ steps.test_sqlite_mysql_postgresql_mimalloc_s3.outcome }}
TEST_DB_M: ${{ steps.test_sqlite_mysql_postgresql_mimalloc.outcome }}
TEST_DB: ${{ steps.test_sqlite_mysql_postgresql.outcome }}
TEST_SQLITE: ${{ steps.test_sqlite.outcome }}
@ -200,13 +199,13 @@ jobs:
echo "" >> "${GITHUB_STEP_SUMMARY}"
echo "|Job|Status|" >> "${GITHUB_STEP_SUMMARY}"
echo "|---|------|" >> "${GITHUB_STEP_SUMMARY}"
echo "|test (sqlite,mysql,postgresql,enable_mimalloc,query_logger)|${TEST_DB_M_L}|" >> "${GITHUB_STEP_SUMMARY}"
echo "|test (sqlite,mysql,postgresql,enable_mimalloc,s3)|${TEST_DB_M_S3}|" >> "${GITHUB_STEP_SUMMARY}"
echo "|test (sqlite,mysql,postgresql,enable_mimalloc)|${TEST_DB_M}|" >> "${GITHUB_STEP_SUMMARY}"
echo "|test (sqlite,mysql,postgresql)|${TEST_DB}|" >> "${GITHUB_STEP_SUMMARY}"
echo "|test (sqlite)|${TEST_SQLITE}|" >> "${GITHUB_STEP_SUMMARY}"
echo "|test (mysql)|${TEST_MYSQL}|" >> "${GITHUB_STEP_SUMMARY}"
echo "|test (postgresql)|${TEST_POSTGRESQL}|" >> "${GITHUB_STEP_SUMMARY}"
echo "|clippy (sqlite,mysql,postgresql,enable_mimalloc)|${CLIPPY}|" >> "${GITHUB_STEP_SUMMARY}"
echo "|clippy (sqlite,mysql,postgresql,enable_mimalloc,s3)|${CLIPPY}|" >> "${GITHUB_STEP_SUMMARY}"
echo "|fmt|${FMT}|" >> "${GITHUB_STEP_SUMMARY}"
echo "" >> "${GITHUB_STEP_SUMMARY}"
echo "Please check the failed jobs and fix where needed." >> "${GITHUB_STEP_SUMMARY}"

2
.github/workflows/check-templates.yml

@ -6,8 +6,6 @@ on: [ push, pull_request ]
jobs:
docker-templates:
name: Validate docker templates
permissions:
contents: read
runs-on: ubuntu-24.04
timeout-minutes: 30

7
.github/workflows/hadolint.yml

@ -1,13 +1,12 @@
name: Hadolint
permissions: {}
on: [ push, pull_request ]
permissions: {}
jobs:
hadolint:
name: Validate Dockerfile syntax
permissions:
contents: read
runs-on: ubuntu-24.04
timeout-minutes: 30
@ -31,7 +30,7 @@ jobs:
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
env:
HADOLINT_VERSION: 2.12.0
HADOLINT_VERSION: 2.14.0
# End Download hadolint
# Checkout the repo
- name: Checkout

28
.github/workflows/release.yml

@ -21,10 +21,10 @@ jobs:
name: Build Vaultwarden containers
if: ${{ github.repository == 'dani-garcia/vaultwarden' }}
permissions:
packages: write
packages: write # Needed to upload packages and artifacts
contents: read
attestations: write
id-token: write
attestations: write # Needed to generate an artifact attestation for a build
id-token: write # Needed to mint the OIDC token necessary to request a Sigstore signing certificate
runs-on: ubuntu-24.04
timeout-minutes: 120
# Start a local docker registry to extract the compiled binaries to upload as artifacts and attest them
@ -103,7 +103,7 @@ jobs:
# Login to Docker Hub
- name: Login to Docker Hub
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
@ -119,7 +119,7 @@ jobs:
# Login to GitHub Container Registry
- name: Login to GitHub Container Registry
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@ -136,7 +136,7 @@ jobs:
# Login to Quay.io
- name: Login to Quay.io
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: quay.io
username: ${{ secrets.QUAY_USERNAME }}
@ -204,7 +204,7 @@ jobs:
# Attest container images
- name: Attest - docker.io - ${{ matrix.base_image }}
if: ${{ env.HAVE_DOCKERHUB_LOGIN == 'true' && steps.bake_vw.outputs.metadata != ''}}
uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0
uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0
with:
subject-name: ${{ vars.DOCKERHUB_REPO }}
subject-digest: ${{ env.DIGEST_SHA }}
@ -212,7 +212,7 @@ jobs:
- name: Attest - ghcr.io - ${{ matrix.base_image }}
if: ${{ env.HAVE_GHCR_LOGIN == 'true' && steps.bake_vw.outputs.metadata != ''}}
uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0
uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0
with:
subject-name: ${{ vars.GHCR_REPO }}
subject-digest: ${{ env.DIGEST_SHA }}
@ -220,7 +220,7 @@ jobs:
- name: Attest - quay.io - ${{ matrix.base_image }}
if: ${{ env.HAVE_QUAY_LOGIN == 'true' && steps.bake_vw.outputs.metadata != ''}}
uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0
uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0
with:
subject-name: ${{ vars.QUAY_REPO }}
subject-digest: ${{ env.DIGEST_SHA }}
@ -275,31 +275,31 @@ jobs:
# Upload artifacts to Github Actions and Attest the binaries
- name: "Upload amd64 artifact ${{ matrix.base_image }}"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-amd64-${{ matrix.base_image }}
path: vaultwarden-amd64-${{ matrix.base_image }}
- name: "Upload arm64 artifact ${{ matrix.base_image }}"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-arm64-${{ matrix.base_image }}
path: vaultwarden-arm64-${{ matrix.base_image }}
- name: "Upload armv7 artifact ${{ matrix.base_image }}"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-armv7-${{ matrix.base_image }}
path: vaultwarden-armv7-${{ matrix.base_image }}
- name: "Upload armv6 artifact ${{ matrix.base_image }}"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-armv6-${{ matrix.base_image }}
path: vaultwarden-armv6-${{ matrix.base_image }}
- name: "Attest artifacts ${{ matrix.base_image }}"
uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0
uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0
with:
subject-path: vaultwarden-*
# End Upload artifacts to Github Actions

2
.github/workflows/releasecache-cleanup.yml

@ -16,7 +16,7 @@ jobs:
releasecache-cleanup:
name: Releasecache Cleanup
permissions:
packages: write
packages: write # To be able to cleanup old caches
runs-on: ubuntu-24.04
continue-on-error: true
timeout-minutes: 30

8
.github/workflows/trivy.yml

@ -23,9 +23,7 @@ jobs:
if: ${{ github.repository == 'dani-garcia/vaultwarden' }}
name: Trivy Scan
permissions:
contents: read
actions: read
security-events: write
security-events: write # To write the security report
runs-on: ubuntu-24.04
timeout-minutes: 30
@ -36,7 +34,7 @@ jobs:
persist-credentials: false
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # v0.33.0 + b6643a2
uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # 0.33.1
env:
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
@ -48,6 +46,6 @@ jobs:
severity: CRITICAL,HIGH
- name: Upload Trivy scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@3c3833e0f8c1c83d449a7478aa59c036a9165498 # v3.29.11
uses: github/codeql-action/upload-sarif@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
with:
sarif_file: 'trivy-results.sarif'

22
.github/workflows/typos.yml

@ -0,0 +1,22 @@
name: Code Spell Checking
on: [ push, pull_request ]
permissions: {}
jobs:
typos:
name: Run typos spell checking
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
# Checkout the repo
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
with:
persist-credentials: false
# End Checkout the repo
# When this version is updated, do not forget to update this in `.pre-commit-config.yaml` too
- name: Spell Check Repo
uses: crate-ci/typos@07d900b8fa1097806b8adb6391b0d3e0ac2fdea7 # v1.39.0

4
.github/workflows/zizmor.yml

@ -13,7 +13,7 @@ jobs:
name: Run zizmor
runs-on: ubuntu-latest
permissions:
security-events: write
security-events: write # To write the security report
steps:
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
@ -21,7 +21,7 @@ jobs:
persist-credentials: false
- name: Run zizmor
uses: zizmorcore/zizmor-action@5ca5fc7a4779c5263a3ffa0e1f693009994446d1 # v0.1.2
uses: zizmorcore/zizmor-action@e673c3917a1aef3c65c972347ed84ccd013ecda4 # v0.2.0
with:
# intentionally not scanning the entire repository,
# since it contains integration tests.

14
.pre-commit-config.yaml

@ -1,7 +1,7 @@
---
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # v6.0.0
hooks:
- id: check-yaml
- id: check-json
@ -22,14 +22,15 @@ repos:
description: Format files with cargo fmt.
entry: cargo fmt
language: system
types: [rust]
always_run: true
pass_filenames: false
args: ["--", "--check"]
- id: cargo-test
name: cargo test
description: Test the package for errors.
entry: cargo test
language: system
args: ["--features", "sqlite,mysql,postgresql,enable_mimalloc", "--"]
args: ["--features", "sqlite,mysql,postgresql", "--"]
types_or: [rust, file]
files: (Cargo.toml|Cargo.lock|rust-toolchain.toml|rustfmt.toml|.*\.rs$)
pass_filenames: false
@ -38,7 +39,7 @@ repos:
description: Lint Rust sources
entry: cargo clippy
language: system
args: ["--features", "sqlite,mysql,postgresql,enable_mimalloc", "--", "-D", "warnings"]
args: ["--features", "sqlite,mysql,postgresql", "--", "-D", "warnings"]
types_or: [rust, file]
files: (Cargo.toml|Cargo.lock|rust-toolchain.toml|rustfmt.toml|.*\.rs$)
pass_filenames: false
@ -50,3 +51,8 @@ repos:
args:
- "-c"
- "cd docker && make"
# 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: 07d900b8fa1097806b8adb6391b0d3e0ac2fdea7 # v1.39.0
hooks:
- id: typos

26
.typos.toml

@ -0,0 +1,26 @@
[files]
extend-exclude = [
".git/",
"playwright/",
"*.js", # Ignore all JavaScript files
"!admin*.js", # Except our own JavaScript files
]
ignore-hidden = false
[default]
extend-ignore-re = [
# We use this in place of the reserved type identifier at some places
"typ",
# In SMTP it's called HELO, so ignore it
"(?i)helo_name",
"Server name sent during.+HELO",
# COSE Is short for CBOR Object Signing and Encryption, ignore these specific items
"COSEKey",
"COSEAlgorithm",
# Ignore this specific string as it's valid
"Ensure they are valid OTPs",
# This word is misspelled upstream
# 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
"AuthRequestResponseRecieved",
]

1343
Cargo.lock

File diff suppressed because it is too large

137
Cargo.toml

@ -1,3 +1,10 @@
[workspace.package]
edition = "2021"
rust-version = "1.89.0"
license = "AGPL-3.0-only"
repository = "https://github.com/dani-garcia/vaultwarden"
publish = false
[workspace]
members = ["macros"]
@ -5,18 +12,21 @@ members = ["macros"]
name = "vaultwarden"
version = "1.0.0"
authors = ["Daniel García <dani-garcia@users.noreply.github.com>"]
edition = "2021"
rust-version = "1.87.0"
resolver = "2"
repository = "https://github.com/dani-garcia/vaultwarden"
readme = "README.md"
license = "AGPL-3.0-only"
publish = false
build = "build.rs"
resolver = "2"
repository.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
publish.workspace = true
[features]
# default = ["sqlite"]
default = [
# "sqlite",
# "mysql",
# "postgresql",
]
# Empty to keep compatibility, prefer to set USE_SYSLOG=true
enable_syslog = []
mysql = ["diesel/mysql", "diesel_migrations/mysql"]
@ -27,11 +37,6 @@ vendored_openssl = ["openssl/vendored"]
# Enable MiMalloc memory allocator to replace the default malloc
# This can improve performance for Alpine builds
enable_mimalloc = ["dep:mimalloc"]
# This is a development dependency, and should only be used during development!
# It enables the usage of the diesel_logger crate, which is able to output the generated queries.
# You also need to set an env variable `QUERY_LOGGER=1` to fully activate this so you do not have to re-compile
# if you want to turn off the logging for a specific run.
query_logger = ["dep:diesel_logger"]
s3 = ["opendal/services-s3", "dep:aws-config", "dep:aws-credential-types", "dep:aws-smithy-runtime-api", "dep:anyhow", "dep:http", "dep:reqsign"]
# OIDC specific features
@ -50,20 +55,17 @@ syslog = "7.0.0"
macros = { path = "./macros" }
# Logging
log = "0.4.27"
log = "0.4.28"
fern = { version = "0.7.1", features = ["syslog-7", "reopen-1"] }
tracing = { version = "0.1.41", features = ["log"] } # Needed to have lettre and webauthn-rs trace logging to work
# A `dotenv` implementation for Rust
dotenvy = { version = "0.15.7", default-features = false }
# Lazy initialization
once_cell = "1.21.3"
# Numerical libraries
num-traits = "0.2.19"
num-derive = "0.4.2"
bigdecimal = "0.4.8"
bigdecimal = "0.4.9"
# Web framework
rocket = { version = "0.5.1", features = ["tls", "json"], default-features = false }
@ -77,17 +79,16 @@ dashmap = "6.1.0"
# Async futures
futures = "0.3.31"
tokio = { version = "1.47.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.16", features = ["compat"]}
# A generic serialization/deserialization framework
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.143"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.145"
# A safe, extensible ORM and Query builder
diesel = { version = "2.2.12", features = ["chrono", "r2d2", "numeric"] }
diesel_migrations = "2.2.0"
diesel_logger = { version = "0.4.0", optional = true }
diesel = { version = "2.3.3", features = ["chrono", "r2d2", "numeric"] }
diesel_migrations = "2.3.0"
derive_more = { version = "2.0.1", features = ["from", "into", "as_ref", "deref", "display"] }
diesel-derive-newtype = "2.1.2"
@ -101,15 +102,15 @@ ring = "0.17.14"
subtle = "2.6.1"
# UUID generation
uuid = { version = "1.18.0", features = ["v4"] }
uuid = { version = "1.18.1", features = ["v4"] }
# Date and time libraries
chrono = { version = "0.4.41", features = ["clock", "serde"], default-features = false }
chrono = { version = "0.4.42", features = ["clock", "serde"], default-features = false }
chrono-tz = "0.10.4"
time = "0.3.41"
time = "0.3.44"
# Job scheduler
job_scheduler_ng = "2.3.0"
job_scheduler_ng = "2.4.0"
# Data encoding library Hex/Base32/Base64
data-encoding = "2.9.0"
@ -126,15 +127,15 @@ yubico = { package = "yubico_ng", version = "0.14.1", features = ["online-tokio"
# WebAuthn libraries
# danger-allow-state-serialisation is needed to save the state in the db
# danger-credential-internals is needed to support U2F to Webauthn migration
webauthn-rs = { version = "0.5.2", features = ["danger-allow-state-serialisation", "danger-credential-internals"] }
webauthn-rs-proto = "0.5.2"
webauthn-rs-core = "0.5.2"
webauthn-rs = { version = "0.5.3", features = ["danger-allow-state-serialisation", "danger-credential-internals"] }
webauthn-rs-proto = "0.5.3"
webauthn-rs-core = "0.5.3"
# Handling of URL's for WebAuthn and favicons
url = "2.5.7"
# Email libraries
lettre = { version = "0.11.18", 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
email_address = "0.2.9"
@ -142,12 +143,12 @@ email_address = "0.2.9"
handlebars = { version = "6.3.2", features = ["dir_source"] }
# HTTP client (Used for favicons, version check, DUO and HIBP API)
reqwest = { version = "0.12.23", 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.24", 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.25.2"
# Favicon extraction libraries
html5gum = "0.8.0"
regex = { version = "1.11.2", 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"
bytes = "1.10.1"
svg-hush = "0.9.5"
@ -157,10 +158,10 @@ cached = { version = "0.56.0", features = ["async"] }
# Used for custom short lived cookie jar during favicon extraction
cookie = "0.18.1"
cookie_store = "0.21.1"
cookie_store = "0.22.0"
# Used by U2F, JWT and PostgreSQL
openssl = "0.10.73"
openssl = "0.10.74"
# CLI argument parsing
pico-args = "0.5.0"
@ -174,7 +175,7 @@ openidconnect = { version = "4.0.1", features = ["reqwest", "native-tls"] }
mini-moka = "0.10.3"
# Check client versions for specific features.
semver = "1.0.26"
semver = "1.0.27"
# Allow overriding the default memory allocator
# Mainly used for the musl builds, since the default musl malloc is very slow
@ -192,13 +193,13 @@ rpassword = "7.4.0"
grass_compiler = { version = "0.13.4", default-features = false }
# File are accessed through Apache OpenDAL
opendal = { version = "0.54.0", features = ["services-fs"], default-features = false }
opendal = { version = "0.54.1", features = ["services-fs"], default-features = false }
# For retrieving AWS credentials, including temporary SSO credentials
anyhow = { version = "1.0.99", optional = true }
aws-config = { version = "1.8.5", features = ["behavior-version-latest", "rt-tokio", "credentials-process", "sso"], default-features = false, optional = true }
aws-credential-types = { version = "1.2.5", optional = true }
aws-smithy-runtime-api = { version = "1.9.0", optional = true }
anyhow = { version = "1.0.100", optional = true }
aws-config = { version = "1.8.8", features = ["behavior-version-latest", "rt-tokio", "credentials-process", "sso"], default-features = false, optional = true }
aws-credential-types = { version = "1.2.8", optional = true }
aws-smithy-runtime-api = { version = "1.9.2", optional = true }
http = { version = "1.3.1", optional = true }
reqsign = { version = "0.16.5", optional = true }
@ -209,23 +210,13 @@ reqsign = { version = "0.16.5", optional = true }
strip = "debuginfo"
lto = "fat"
codegen-units = 1
# A little bit of a speedup
[profile.dev]
split-debuginfo = "unpacked"
# Always build argon2 using opt-level 3
# This is a huge speed improvement during testing
[profile.dev.package.argon2]
opt-level = 3
debug = false
# Optimize for size
[profile.release-micro]
inherits = "release"
opt-level = "z"
strip = "symbols"
lto = "fat"
codegen-units = 1
opt-level = "z"
panic = "abort"
# Profile for systems with low resources
@ -236,6 +227,32 @@ strip = "symbols"
lto = "thin"
codegen-units = 16
# Used for profiling and debugging like valgrind or heaptrack
# Inherits release to be sure all optimizations have been done
[profile.dbg]
inherits = "release"
strip = "none"
split-debuginfo = "off"
debug = "full"
# A little bit of a speedup for generic building
[profile.dev]
split-debuginfo = "unpacked"
debug = "line-tables-only"
# Used for CI builds to improve compile time
[profile.ci]
inherits = "dev"
debug = false
debug-assertions = false
strip = "symbols"
panic = "abort"
# Always build argon2 using opt-level 3
# This is a huge speed improvement during testing
[profile.dev.package.argon2]
opt-level = 3
# Linting config
# https://doc.rust-lang.org/rustc/lints/groups.html
[workspace.lints.rust]
@ -245,15 +262,16 @@ non_ascii_idents = "forbid"
# Deny
deprecated_in_future = "deny"
deprecated_safe = { level = "deny", priority = -1 }
future_incompatible = { level = "deny", priority = -1 }
keyword_idents = { level = "deny", priority = -1 }
let_underscore = { level = "deny", priority = -1 }
nonstandard_style = { level = "deny", priority = -1 }
noop_method_call = "deny"
refining_impl_trait = { level = "deny", priority = -1 }
rust_2018_idioms = { level = "deny", priority = -1 }
rust_2021_compatibility = { level = "deny", priority = -1 }
rust_2024_compatibility = { level = "deny", priority = -1 }
edition_2024_expr_fragment_specifier = "allow" # Once changed to Rust 2024 this should be removed and macro's should be validated again
single_use_lifetimes = "deny"
trivial_casts = "deny"
trivial_numeric_casts = "deny"
@ -263,7 +281,8 @@ unused_lifetimes = "deny"
unused_qualifications = "deny"
variant_size_differences = "deny"
# Allow the following lints since these cause issues with Rust v1.84.0 or newer
# Building Vaultwarden with Rust v1.85.0 and edition 2024 also works without issues
# Building Vaultwarden with Rust v1.85.0 with edition 2024 also works without issues
edition_2024_expr_fragment_specifier = "allow" # Once changed to Rust 2024 this should be removed and macro's should be validated again
if_let_rescope = "allow"
tail_expr_drop_order = "allow"
@ -277,10 +296,12 @@ todo = "warn"
result_large_err = "allow"
# Deny
branches_sharing_code = "deny"
case_sensitive_file_extension_comparisons = "deny"
cast_lossless = "deny"
clone_on_ref_ptr = "deny"
equatable_if_let = "deny"
excessive_precision = "deny"
filter_map_next = "deny"
float_cmp_const = "deny"
implicit_clone = "deny"
@ -294,15 +315,19 @@ manual_instant_elapsed = "deny"
manual_string_new = "deny"
match_wildcard_for_single_variants = "deny"
mem_forget = "deny"
needless_borrow = "deny"
needless_collect = "deny"
needless_continue = "deny"
needless_lifetimes = "deny"
option_option = "deny"
redundant_clone = "deny"
string_add_assign = "deny"
unnecessary_join = "deny"
unnecessary_self_imports = "deny"
unnested_or_patterns = "deny"
unused_async = "deny"
unused_self = "deny"
useless_let_if_seq = "deny"
verbose_file_reads = "deny"
zero_sized_map_values = "deny"

6
build.rs

@ -9,8 +9,6 @@ fn main() {
println!("cargo:rustc-cfg=mysql");
#[cfg(feature = "postgresql")]
println!("cargo:rustc-cfg=postgresql");
#[cfg(feature = "query_logger")]
println!("cargo:rustc-cfg=query_logger");
#[cfg(feature = "s3")]
println!("cargo:rustc-cfg=s3");
@ -24,7 +22,6 @@ fn main() {
println!("cargo::rustc-check-cfg=cfg(sqlite)");
println!("cargo::rustc-check-cfg=cfg(mysql)");
println!("cargo::rustc-check-cfg=cfg(postgresql)");
println!("cargo::rustc-check-cfg=cfg(query_logger)");
println!("cargo::rustc-check-cfg=cfg(s3)");
// Rerun when these paths are changed.
@ -34,9 +31,6 @@ fn main() {
println!("cargo:rerun-if-changed=.git/index");
println!("cargo:rerun-if-changed=.git/refs/tags");
#[cfg(all(not(debug_assertions), feature = "query_logger"))]
compile_error!("Query Logging is only allowed during development, it is not intended for production usage!");
// Support $BWRS_VERSION for legacy compatibility, but default to $VW_VERSION.
// If neither exist, read from git.
let maybe_vaultwarden_version =

10
docker/DockerSettings.yaml

@ -1,11 +1,11 @@
---
vault_version: "v2025.9.1"
vault_image_digest: "sha256:15a126ca967cd2efc4c9625fec49f0b972a3f7d7d81d7770bb0a2502d5e4b8a4"
# Cross Compile Docker Helper Scripts v1.6.1
vault_version: "v2025.10.1"
vault_image_digest: "sha256:50662dccf4908ac2128cd44981c52fcb4e3e8dd56f21823c8d5e91267ff741fa"
# Cross Compile Docker Helper Scripts v1.8.0
# 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
xx_image_digest: "sha256:9c207bead753dda9430bdd15425c6518fc7a03d866103c516a2c6889188f5894"
rust_version: 1.89.0 # Rust version to be used
xx_image_digest: "sha256:add602d55daca18914838a78221f6bbe4284114b452c86a48f96d59aeb00f5c6"
rust_version: 1.91.0 # Rust version to be used
debian_version: trixie # Debian release name to be used
alpine_version: "3.22" # Alpine version to be used
# For which platforms/architectures will we try to build images

26
docker/Dockerfile.alpine

@ -19,23 +19,23 @@
# - 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.
# - From the command line:
# $ docker pull docker.io/vaultwarden/web-vault:v2025.9.1
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.9.1
# [docker.io/vaultwarden/web-vault@sha256:15a126ca967cd2efc4c9625fec49f0b972a3f7d7d81d7770bb0a2502d5e4b8a4]
# $ docker pull docker.io/vaultwarden/web-vault:v2025.10.1
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.10.1
# [docker.io/vaultwarden/web-vault@sha256:50662dccf4908ac2128cd44981c52fcb4e3e8dd56f21823c8d5e91267ff741fa]
#
# - Conversely, to get the tag name from the digest:
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:15a126ca967cd2efc4c9625fec49f0b972a3f7d7d81d7770bb0a2502d5e4b8a4
# [docker.io/vaultwarden/web-vault:v2025.9.1]
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:50662dccf4908ac2128cd44981c52fcb4e3e8dd56f21823c8d5e91267ff741fa
# [docker.io/vaultwarden/web-vault:v2025.10.1]
#
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:15a126ca967cd2efc4c9625fec49f0b972a3f7d7d81d7770bb0a2502d5e4b8a4 AS vault
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:50662dccf4908ac2128cd44981c52fcb4e3e8dd56f21823c8d5e91267ff741fa AS vault
########################## ALPINE BUILD IMAGES ##########################
## NOTE: The Alpine Base Images do not support other platforms then linux/amd64
## And for Alpine we define all build images here, they will only be loaded when actually used
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.89.0 AS build_amd64
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.89.0 AS build_arm64
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.89.0 AS build_armv7
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.89.0 AS build_armv6
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.91.0 AS build_amd64
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.91.0 AS build_arm64
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.91.0 AS build_armv7
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.91.0 AS build_armv6
########################## BUILD IMAGE ##########################
# hadolint ignore=DL3006
@ -53,9 +53,9 @@ ENV DEBIAN_FRONTEND=noninteractive \
TERM=xterm-256color \
CARGO_HOME="/root/.cargo" \
USER="root" \
# Use PostgreSQL v15 during Alpine/MUSL builds instead of the default v11
# Debian Bookworm already contains libpq v15
PQ_LIB_DIR="/usr/local/musl/pq15/lib"
# Use PostgreSQL v17 during Alpine/MUSL builds instead of the default v16
# Debian Trixie uses libpq v17
PQ_LIB_DIR="/usr/local/musl/pq17/lib"
# Create CARGO_HOME folder and don't download rust docs

16
docker/Dockerfile.debian

@ -19,24 +19,24 @@
# - 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.
# - From the command line:
# $ docker pull docker.io/vaultwarden/web-vault:v2025.9.1
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.9.1
# [docker.io/vaultwarden/web-vault@sha256:15a126ca967cd2efc4c9625fec49f0b972a3f7d7d81d7770bb0a2502d5e4b8a4]
# $ docker pull docker.io/vaultwarden/web-vault:v2025.10.1
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.10.1
# [docker.io/vaultwarden/web-vault@sha256:50662dccf4908ac2128cd44981c52fcb4e3e8dd56f21823c8d5e91267ff741fa]
#
# - Conversely, to get the tag name from the digest:
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:15a126ca967cd2efc4c9625fec49f0b972a3f7d7d81d7770bb0a2502d5e4b8a4
# [docker.io/vaultwarden/web-vault:v2025.9.1]
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:50662dccf4908ac2128cd44981c52fcb4e3e8dd56f21823c8d5e91267ff741fa
# [docker.io/vaultwarden/web-vault:v2025.10.1]
#
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:15a126ca967cd2efc4c9625fec49f0b972a3f7d7d81d7770bb0a2502d5e4b8a4 AS vault
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:50662dccf4908ac2128cd44981c52fcb4e3e8dd56f21823c8d5e91267ff741fa AS vault
########################## Cross Compile Docker Helper Scripts ##########################
## We use the linux/amd64 no matter which Build Platform, since these are all bash scripts
## And these bash scripts do not have any significant difference if at all
FROM --platform=linux/amd64 docker.io/tonistiigi/xx@sha256:9c207bead753dda9430bdd15425c6518fc7a03d866103c516a2c6889188f5894 AS xx
FROM --platform=linux/amd64 docker.io/tonistiigi/xx@sha256:add602d55daca18914838a78221f6bbe4284114b452c86a48f96d59aeb00f5c6 AS xx
########################## BUILD IMAGE ##########################
# hadolint ignore=DL3006
FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.89.0-slim-trixie AS build
FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.91.0-slim-trixie AS build
COPY --from=xx / /
ARG TARGETARCH
ARG TARGETVARIANT

6
docker/Dockerfile.j2

@ -63,9 +63,9 @@ ENV DEBIAN_FRONTEND=noninteractive \
CARGO_HOME="/root/.cargo" \
USER="root"
{%- if base == "alpine" %} \
# Use PostgreSQL v15 during Alpine/MUSL builds instead of the default v11
# Debian Bookworm already contains libpq v15
PQ_LIB_DIR="/usr/local/musl/pq15/lib"
# Use PostgreSQL v17 during Alpine/MUSL builds instead of the default v16
# Debian Trixie uses libpq v17
PQ_LIB_DIR="/usr/local/musl/pq17/lib"
{% endif %}
{% if base == "debian" %}

4
docker/README.md

@ -116,7 +116,7 @@ docker/bake.sh
```
You can append both `alpine` and `debian` with `-amd64`, `-arm64`, `-armv7` or `-armv6`, which will trigger a build for that specific platform.<br>
This will also append those values to the tag so you can see the builded container when running `docker images`.
This will also append those values to the tag so you can see the built container when running `docker images`.
You can also append extra arguments after the target if you want. This can be useful for example to print what bake will use.
```bash
@ -162,7 +162,7 @@ You can append extra arguments after the target if you want. This can be useful
For the podman builds you can, just like the `bake.sh` script, also append the architecture to build for that specific platform.<br>
### Testing podman builded images
### Testing podman built images
The command to start a podman built container is almost the same as for the docker/bake built containers. The images start with `localhost/`, so you need to prepend that.

10
macros/Cargo.toml

@ -1,7 +1,11 @@
[package]
name = "macros"
version = "0.1.0"
edition = "2021"
repository.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
publish.workspace = true
[lib]
name = "macros"
@ -9,8 +13,8 @@ path = "src/lib.rs"
proc-macro = true
[dependencies]
quote = "1.0.40"
syn = "2.0.105"
quote = "1.0.41"
syn = "2.0.108"
[lints]
workspace = true

2
playwright/README.md

@ -15,7 +15,7 @@ It is possible to run `Playwright` outside of the container, this removes the ne
You will additionally need `nodejs` then run:
```bash
npm install
npm ci --ignore-scripts
npx playwright install-deps
npx playwright install firefox
```

35
playwright/compose/keycloak/Dockerfile

@ -1,40 +1,19 @@
FROM docker.io/library/debian:bookworm-slim as build
FROM docker.io/library/debian:trixie-slim
ENV DEBIAN_FRONTEND=noninteractive
ARG KEYCLOAK_VERSION
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
RUN apt-get update \
&& apt-get install -y ca-certificates curl wget \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /
RUN wget -c https://github.com/keycloak/keycloak/releases/download/${KEYCLOAK_VERSION}/keycloak-${KEYCLOAK_VERSION}.tar.gz -O - | tar -xz
FROM docker.io/library/debian:bookworm-slim
ENV DEBIAN_FRONTEND=noninteractive
ARG KEYCLOAK_VERSION
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
RUN apt-get update \
&& apt-get install -y ca-certificates curl wget \
&& rm -rf /var/lib/apt/lists/*
ARG JAVA_URL
ARG JAVA_VERSION
ENV JAVA_VERSION=${JAVA_VERSION}
RUN mkdir -p /opt/openjdk && cd /opt/openjdk \
&& wget -c "${JAVA_URL}" -O - | tar -xz
RUN apt-get update && apt-get install -y ca-certificates curl jq openjdk-21-jdk-headless wget
WORKDIR /
RUN wget -c https://github.com/keycloak/keycloak/releases/download/${KEYCLOAK_VERSION}/keycloak-${KEYCLOAK_VERSION}.tar.gz -O - | tar -xz \
&& mkdir -p /opt/keycloak \
&& mv /keycloak-${KEYCLOAK_VERSION}/bin /opt/keycloak/bin \
&& rm -rf /keycloak-${KEYCLOAK_VERSION}
COPY setup.sh /setup.sh
COPY --from=build /keycloak-${KEYCLOAK_VERSION}/bin /opt/keycloak/bin
CMD "/setup.sh"

12
playwright/compose/keycloak/setup.sh

@ -1,7 +1,6 @@
#!/bin/bash
export PATH=/opt/keycloak/bin:/opt/openjdk/jdk-${JAVA_VERSION}/bin:$PATH
export JAVA_HOME=/opt/openjdk/jdk-${JAVA_VERSION}
export PATH=/opt/keycloak/bin:$PATH
STATUS_CODE=0
while [[ "$STATUS_CODE" != "404" ]] ; do
@ -34,3 +33,12 @@ kcadm.sh update users/$TEST_USER3_ID/reset-password -r "$TEST_REALM" -s type=pas
# Dummy realm to mark end of setup
kcadm.sh create realms -s realm="$DUMMY_REALM" -s enabled=true -s "accessTokenLifespan=600"
# TO DEBUG uncomment the following line to keep the setup container running
# sleep 3600
# THEN in another terminal:
# docker exec -it keycloakSetup-dev /bin/bash
# export PATH=$PATH:/opt/keycloak/bin
# kcadm.sh config credentials --server "http://${KC_HTTP_HOST}:${KC_HTTP_PORT}" --realm master --user "$KEYCLOAK_ADMIN" --password "$KEYCLOAK_ADMIN_PASSWORD" --client admin-cli
# ENJOY
# Doc: https://wjw465150.gitbooks.io/keycloak-documentation/content/server_admin/topics/admin-cli.html

8
playwright/compose/playwright/Dockerfile

@ -1,4 +1,4 @@
FROM docker.io/library/debian:bookworm-slim
FROM docker.io/library/debian:trixie-slim
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
@ -8,7 +8,7 @@ RUN apt-get update \
&& apt-get install -y ca-certificates curl \
&& curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc \
&& chmod a+r /etc/apt/keyrings/docker.asc \
&& echo "deb [signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian bookworm stable" | tee /etc/apt/sources.list.d/docker.list \
&& echo "deb [signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian trixie stable" | tee /etc/apt/sources.list.d/docker.list \
&& apt-get update \
&& apt-get install -y --no-install-recommends \
containerd.io \
@ -27,8 +27,8 @@ RUN apt-get update \
RUN mkdir /playwright
WORKDIR /playwright
COPY package.json .
RUN npm install && npx playwright install-deps && npx playwright install firefox
COPY package.json package-lock.json .
RUN npm ci --ignore-scripts && npx playwright install-deps && npx playwright install firefox
COPY docker-compose.yml test.env ./
COPY compose ./compose

9
playwright/docker-compose.yml

@ -30,9 +30,10 @@ services:
- SMTP_FROM
- SMTP_DEBUG
- SSO_DEBUG_TOKENS
- SSO_FRONTEND
- SSO_ENABLED
- SSO_FRONTEND
- SSO_ONLY
- SSO_SCOPES
restart: "no"
depends_on:
- VaultwardenPrebuild
@ -100,7 +101,7 @@ services:
Keycloak:
profiles: ["keycloak", "vaultwarden"]
container_name: keycloak-${ENV:-dev}
image: quay.io/keycloak/keycloak:25.0.4
image: quay.io/keycloak/keycloak:26.3.4
network_mode: "host"
command:
- start-dev
@ -114,9 +115,7 @@ services:
context: compose/keycloak
dockerfile: Dockerfile
args:
KEYCLOAK_VERSION: 25.0.4
JAVA_URL: https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_linux-x64_bin.tar.gz
JAVA_VERSION: 21.0.2
KEYCLOAK_VERSION: 26.3.4
network_mode: "host"
depends_on:
- Keycloak

2
playwright/global-utils.ts

@ -10,7 +10,7 @@ const fs = require("fs");
const { spawn } = require('node:child_process');
export function loadEnv(){
var myEnv = dotenv.config({ path: 'test.env' });
var myEnv = dotenv.config({ path: 'test.env', quiet: true });
dotenvExpand.expand(myEnv);
return {

544
playwright/package-lock.json

@ -9,34 +9,53 @@
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"mysql2": "^3.14.3",
"otpauth": "^9.4.0",
"pg": "^8.16.3"
"mysql2": "3.15.0",
"otpauth": "9.4.1",
"pg": "8.16.3"
},
"devDependencies": {
"@playwright/test": "^1.54.2",
"dotenv": "^16.6.1",
"dotenv-expand": "^12.0.2",
"maildev": "npm:@timshel_npm/maildev@^3.2.1"
"@playwright/test": "1.55.1",
"dotenv": "17.2.2",
"dotenv-expand": "12.0.3",
"maildev": "npm:@timshel_npm/maildev@^3.2.3"
}
},
"node_modules/@asamuzakjp/css-color": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
"integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.0.5.tgz",
"integrity": "sha512-lMrXidNhPGsDjytDy11Vwlb6OIGrT3CmLg3VWNFyWkLWtijKl7xjvForlh8vuj0SHGjgl4qZEQzUmYTeQA2JFQ==",
"dev": true,
"dependencies": {
"@csstools/css-calc": "^2.1.3",
"@csstools/css-color-parser": "^3.0.9",
"@csstools/css-parser-algorithms": "^3.0.4",
"@csstools/css-tokenizer": "^3.0.3",
"lru-cache": "^10.4.3"
"@csstools/css-calc": "^2.1.4",
"@csstools/css-color-parser": "^3.1.0",
"@csstools/css-parser-algorithms": "^3.0.5",
"@csstools/css-tokenizer": "^3.0.4",
"lru-cache": "^11.2.1"
}
},
"node_modules/@asamuzakjp/dom-selector": {
"version": "6.5.6",
"resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.5.6.tgz",
"integrity": "sha512-Mj3Hu9ymlsERd7WOsUKNUZnJYL4IZ/I9wVVYgtvOsWYiEFbkQ4G7VRIh2USxTVW4BBDIsLG+gBUgqOqf2Kvqow==",
"dev": true,
"dependencies": {
"@asamuzakjp/nwsapi": "^2.3.9",
"bidi-js": "^1.0.3",
"css-tree": "^3.1.0",
"is-potential-custom-element-name": "^1.0.1",
"lru-cache": "^11.2.1"
}
},
"node_modules/@asamuzakjp/nwsapi": {
"version": "2.3.9",
"resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz",
"integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==",
"dev": true
},
"node_modules/@csstools/color-helpers": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz",
"integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==",
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
"integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
"dev": true,
"funding": [
{
@ -76,9 +95,9 @@
}
},
"node_modules/@csstools/css-color-parser": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.10.tgz",
"integrity": "sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==",
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
"integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
"dev": true,
"funding": [
{
@ -91,7 +110,7 @@
}
],
"dependencies": {
"@csstools/color-helpers": "^5.0.2",
"@csstools/color-helpers": "^5.1.0",
"@csstools/css-calc": "^2.1.4"
},
"engines": {
@ -124,6 +143,28 @@
"@csstools/css-tokenizer": "^3.0.4"
}
},
"node_modules/@csstools/css-syntax-patches-for-csstree": {
"version": "1.0.14",
"resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.14.tgz",
"integrity": "sha512-zSlIxa20WvMojjpCSy8WrNpcZ61RqfTfX3XTaOeVlGJrt/8HF3YbzgFZa01yTbT4GWQLwfTcC3EB8i3XnB647Q==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"engines": {
"node": ">=18"
},
"peerDependencies": {
"postcss": "^8.4"
}
},
"node_modules/@csstools/css-tokenizer": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
@ -144,9 +185,9 @@
}
},
"node_modules/@noble/hashes": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.1.tgz",
"integrity": "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==",
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
"engines": {
"node": "^14.21.3 || >=16"
},
@ -155,12 +196,12 @@
}
},
"node_modules/@playwright/test": {
"version": "1.54.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.54.2.tgz",
"integrity": "sha512-A+znathYxPf+72riFd1r1ovOLqsIIB0jKIoPjyK2kqEIe30/6jF6BC7QNluHuwUmsD2tv1XZVugN8GqfTMOxsA==",
"version": "1.55.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.55.1.tgz",
"integrity": "sha512-IVAh/nOJaw6W9g+RJVlIQJ6gSiER+ae6mKQ5CX1bERzQgbC1VSeBlwdvczT7pxb0GWiyrxH4TGKbMfDb4Sq/ig==",
"dev": true,
"dependencies": {
"playwright": "1.54.2"
"playwright": "1.55.1"
},
"bin": {
"playwright": "cli.js"
@ -208,12 +249,12 @@
}
},
"node_modules/@types/node": {
"version": "24.2.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.1.tgz",
"integrity": "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==",
"version": "24.5.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.2.tgz",
"integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==",
"dev": true,
"dependencies": {
"undici-types": "~7.10.0"
"undici-types": "~7.12.0"
}
},
"node_modules/@types/trusted-types": {
@ -292,6 +333,15 @@
"node": "^4.5.0 || >= 5.9"
}
},
"node_modules/bidi-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
"integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
"dev": true,
"dependencies": {
"require-from-string": "^2.0.2"
}
},
"node_modules/body-parser": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
@ -313,9 +363,9 @@
}
},
"node_modules/body-parser/node_modules/debug": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dev": true,
"dependencies": {
"ms": "^2.1.3"
@ -374,12 +424,12 @@
}
},
"node_modules/commander": {
"version": "13.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz",
"integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==",
"version": "14.0.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-14.0.1.tgz",
"integrity": "sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A==",
"dev": true,
"engines": {
"node": ">=18"
"node": ">=20"
}
},
"node_modules/compressible": {
@ -464,30 +514,44 @@
"node": ">= 0.10"
}
},
"node_modules/css-tree": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz",
"integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==",
"dev": true,
"dependencies": {
"mdn-data": "2.12.2",
"source-map-js": "^1.0.1"
},
"engines": {
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
}
},
"node_modules/cssstyle": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
"integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.1.tgz",
"integrity": "sha512-g5PC9Aiph9eiczFpcgUhd9S4UUO3F+LHGRIi5NUMZ+4xtoIYbHNZwZnWA2JsFGe8OU8nl4WyaEFiZuGuxlutJQ==",
"dev": true,
"dependencies": {
"@asamuzakjp/css-color": "^3.2.0",
"rrweb-cssom": "^0.8.0"
"@asamuzakjp/css-color": "^4.0.3",
"@csstools/css-syntax-patches-for-csstree": "^1.0.14",
"css-tree": "^3.1.0"
},
"engines": {
"node": ">=18"
"node": ">=20"
}
},
"node_modules/data-urls": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
"integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.0.tgz",
"integrity": "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==",
"dev": true,
"dependencies": {
"whatwg-mimetype": "^4.0.0",
"whatwg-url": "^14.0.0"
"whatwg-url": "^15.0.0"
},
"engines": {
"node": ">=18"
"node": ">=20"
}
},
"node_modules/debug": {
@ -573,9 +637,9 @@
}
},
"node_modules/dompurify": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz",
"integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==",
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz",
"integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==",
"dev": true,
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
@ -596,9 +660,9 @@
}
},
"node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
"version": "17.2.2",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.2.tgz",
"integrity": "sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q==",
"dev": true,
"engines": {
"node": ">=12"
@ -608,9 +672,9 @@
}
},
"node_modules/dotenv-expand": {
"version": "12.0.2",
"resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.2.tgz",
"integrity": "sha512-lXpXz2ZE1cea1gL4sz2Ipj8y4PiVjytYr3Ij0SWoms1PGxIv7m2CRKuRuCRtHdVuvM/hNJPMxt5PbhboNC4dPQ==",
"version": "12.0.3",
"resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.3.tgz",
"integrity": "sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==",
"dev": true,
"dependencies": {
"dotenv": "^16.4.5"
@ -622,6 +686,18 @@
"url": "https://dotenvx.com"
}
},
"node_modules/dotenv-expand/node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@ -876,9 +952,9 @@
}
},
"node_modules/express/node_modules/debug": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dev": true,
"dependencies": {
"ms": "^2.1.3"
@ -916,9 +992,9 @@
}
},
"node_modules/finalhandler/node_modules/debug": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dev": true,
"dependencies": {
"ms": "^2.1.3"
@ -1155,9 +1231,9 @@
}
},
"node_modules/http-proxy-agent/node_modules/debug": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dev": true,
"dependencies": {
"ms": "^2.1.3"
@ -1191,9 +1267,9 @@
}
},
"node_modules/https-proxy-agent/node_modules/debug": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dev": true,
"dependencies": {
"ms": "^2.1.3"
@ -1217,6 +1293,7 @@
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"dev": true,
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
@ -1263,34 +1340,34 @@
"integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="
},
"node_modules/jsdom": {
"version": "26.1.0",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
"version": "27.0.0",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.0.0.tgz",
"integrity": "sha512-lIHeR1qlIRrIN5VMccd8tI2Sgw6ieYXSVktcSHaNe3Z5nE/tcPQYQWOq00wxMvYOsz+73eAkNenVvmPC6bba9A==",
"dev": true,
"dependencies": {
"cssstyle": "^4.2.1",
"data-urls": "^5.0.0",
"@asamuzakjp/dom-selector": "^6.5.4",
"cssstyle": "^5.3.0",
"data-urls": "^6.0.0",
"decimal.js": "^10.5.0",
"html-encoding-sniffer": "^4.0.0",
"http-proxy-agent": "^7.0.2",
"https-proxy-agent": "^7.0.6",
"is-potential-custom-element-name": "^1.0.1",
"nwsapi": "^2.2.16",
"parse5": "^7.2.1",
"parse5": "^7.3.0",
"rrweb-cssom": "^0.8.0",
"saxes": "^6.0.0",
"symbol-tree": "^3.2.4",
"tough-cookie": "^5.1.1",
"tough-cookie": "^6.0.0",
"w3c-xmlserializer": "^5.0.0",
"webidl-conversions": "^7.0.0",
"webidl-conversions": "^8.0.0",
"whatwg-encoding": "^3.1.1",
"whatwg-mimetype": "^4.0.0",
"whatwg-url": "^14.1.1",
"ws": "^8.18.0",
"whatwg-url": "^15.0.0",
"ws": "^8.18.2",
"xml-name-validator": "^5.0.0"
},
"engines": {
"node": ">=18"
"node": ">=20"
},
"peerDependencies": {
"canvas": "^3.0.0"
@ -1349,10 +1426,13 @@
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="
},
"node_modules/lru-cache": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"dev": true
"version": "11.2.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.1.tgz",
"integrity": "sha512-r8LA6i4LP4EeWOhqBaZZjDWwehd1xUJPCJd9Sv300H0ZmcUER4+JPh7bqqZeqs1o5pgtgvXm+d9UGrB5zZGDiQ==",
"dev": true,
"engines": {
"node": "20 || >=22"
}
},
"node_modules/lru.min": {
"version": "1.1.2",
@ -1370,32 +1450,32 @@
},
"node_modules/maildev": {
"name": "@timshel_npm/maildev",
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/@timshel_npm/maildev/-/maildev-3.2.1.tgz",
"integrity": "sha512-CE9ccmo8JgCgZGR5FfU+eTCks13Ux4ogrG7G3TNq1W/5NXKSPLT/MZpvC5kDfoeQwplDxWe5a/nld67D0DcauA==",
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/@timshel_npm/maildev/-/maildev-3.2.3.tgz",
"integrity": "sha512-CNxMz4Obw7nL8MZbx4y1YUFeqqAQk+Qwm51tcBV5lRBotMlXKeYhuEcayb1v66nUwq832bUfKF4hyQpJixFZrw==",
"dev": true,
"dependencies": {
"@types/mailparser": "^3.4.6",
"@types/mailparser": "3.4.6",
"addressparser": "1.0.1",
"async": "^3.2.6",
"commander": "^13.1.0",
"compression": "^1.8.1",
"cors": "^2.8.5",
"dompurify": "^3.2.6",
"express": "^5.1.0",
"jsdom": "^26.1.0",
"mailparser": "^3.7.4",
"mime": "^3.0.0",
"nodemailer": "^7.0.5",
"smtp-server": "^3.14.0",
"socket.io": "^4.8.1",
"async": "3.2.6",
"commander": "14.0.1",
"compression": "1.8.1",
"cors": "2.8.5",
"dompurify": "3.2.7",
"express": "5.1.0",
"jsdom": "27.0.0",
"mailparser": "3.7.4",
"mime": "4.1.0",
"nodemailer": "7.0.6",
"smtp-server": "3.14.0",
"socket.io": "4.8.1",
"wildstring": "1.0.9"
},
"bin": {
"maildev": "bin/maildev"
},
"engines": {
"node": ">=18.0.0"
"node": ">=20.0.0"
}
},
"node_modules/mailparser": {
@ -1445,6 +1525,12 @@
"node": ">= 0.4"
}
},
"node_modules/mdn-data": {
"version": "2.12.2",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz",
"integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==",
"dev": true
},
"node_modules/media-typer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
@ -1467,15 +1553,18 @@
}
},
"node_modules/mime": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
"integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==",
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-4.1.0.tgz",
"integrity": "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==",
"dev": true,
"funding": [
"https://github.com/sponsors/broofa"
],
"bin": {
"mime": "cli.js"
"mime": "bin/cli.js"
},
"engines": {
"node": ">=10.0.0"
"node": ">=16"
}
},
"node_modules/mime-db": {
@ -1506,14 +1595,14 @@
"dev": true
},
"node_modules/mysql2": {
"version": "3.14.3",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.14.3.tgz",
"integrity": "sha512-fD6MLV8XJ1KiNFIF0bS7Msl8eZyhlTDCDl75ajU5SJtpdx9ZPEACulJcqJWr1Y8OYyxsFc4j3+nflpmhxCU5aQ==",
"version": "3.15.0",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.0.tgz",
"integrity": "sha512-tT6pomf5Z/I7Jzxu8sScgrYBMK9bUFWd7Kbo6Fs1L0M13OOIJ/ZobGKS3Z7tQ8Re4lj+LnLXIQVZZxa3fhYKzA==",
"dependencies": {
"aws-ssl-profiles": "^1.1.1",
"denque": "^2.1.0",
"generate-function": "^2.3.1",
"iconv-lite": "^0.6.3",
"iconv-lite": "^0.7.0",
"long": "^5.2.1",
"lru.min": "^1.0.0",
"named-placeholders": "^1.1.3",
@ -1524,6 +1613,21 @@
"node": ">= 8.0"
}
},
"node_modules/mysql2/node_modules/iconv-lite": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
"integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/named-placeholders": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz",
@ -1543,6 +1647,25 @@
"node": ">=12"
}
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"peer": true,
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/negotiator": {
"version": "0.6.4",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
@ -1553,20 +1676,14 @@
}
},
"node_modules/nodemailer": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.5.tgz",
"integrity": "sha512-nsrh2lO3j4GkLLXoeEksAMgAOqxOv6QumNRVQTJwKH4nuiww6iC2y7GyANs9kRAxCexg3+lTWM3PZ91iLlVjfg==",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.6.tgz",
"integrity": "sha512-F44uVzgwo49xboqbFgBGkRaiMgtoBrBEWCVincJPK9+S9Adkzt/wXCLKbf7dxucmxfTI5gHGB+bEmdyzN6QKjw==",
"dev": true,
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/nwsapi": {
"version": "2.2.21",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.21.tgz",
"integrity": "sha512-o6nIY3qwiSXl7/LuOU0Dmuctd34Yay0yeuZRLFmDPrrdHpXKFndPj3hM+YEPVHYC5fx2otBx4Ilc/gyYSAUaIA==",
"dev": true
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@ -1619,11 +1736,11 @@
}
},
"node_modules/otpauth": {
"version": "9.4.0",
"resolved": "https://registry.npmjs.org/otpauth/-/otpauth-9.4.0.tgz",
"integrity": "sha512-fHIfzIG5RqCkK9cmV8WU+dPQr9/ebR5QOwGZn2JAr1RQF+lmAuLL2YdtdqvmBjNmgJlYk3KZ4a0XokaEhg1Jsw==",
"version": "9.4.1",
"resolved": "https://registry.npmjs.org/otpauth/-/otpauth-9.4.1.tgz",
"integrity": "sha512-+iVvys36CFsyXEqfNftQm1II7SW23W1wx9RwNk0Cd97lbvorqAhBDksb/0bYry087QMxjiuBS0wokdoZ0iUeAw==",
"dependencies": {
"@noble/hashes": "1.7.1"
"@noble/hashes": "1.8.0"
},
"funding": {
"url": "https://github.com/hectorm/otpauth?sponsor=1"
@ -1676,12 +1793,13 @@
}
},
"node_modules/path-to-regexp": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz",
"integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==",
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
"integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
"dev": true,
"engines": {
"node": ">=16"
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/peberminta": {
@ -1774,13 +1892,20 @@
"split2": "^4.1.0"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"peer": true
},
"node_modules/playwright": {
"version": "1.54.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.54.2.tgz",
"integrity": "sha512-Hu/BMoA1NAdRUuulyvQC0pEqZ4vQbGfn8f7wPXcnqQmM+zct9UliKxsIkLNmz/ku7LElUNqmaiv1TG/aL5ACsw==",
"version": "1.55.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.1.tgz",
"integrity": "sha512-cJW4Xd/G3v5ovXtJJ52MAOclqeac9S/aGGgRzLabuF8TnIb6xHvMzKIa6JmrRzUkeXJgfL1MhukP0NK6l39h3A==",
"dev": true,
"dependencies": {
"playwright-core": "1.54.2"
"playwright-core": "1.55.1"
},
"bin": {
"playwright": "cli.js"
@ -1793,9 +1918,9 @@
}
},
"node_modules/playwright-core": {
"version": "1.54.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.54.2.tgz",
"integrity": "sha512-n5r4HFbMmWsB4twG7tJLDN9gmBUeSPcsBZiWSE4DnYz9mJMAFqr2ID7+eGC9kpEnxExJ1epttwR59LEWCk8mtA==",
"version": "1.55.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.1.tgz",
"integrity": "sha512-Z6Mh9mkwX+zxSlHqdr5AOcJnfp+xUWLCt9uKV18fhzA8eyxUd8NUWzAjxUh55RZKSYwDGX0cfaySdhZJGMoJ+w==",
"dev": true,
"bin": {
"playwright-core": "cli.js"
@ -1804,6 +1929,35 @@
"node": ">=18"
}
},
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
@ -1895,18 +2049,43 @@
}
},
"node_modules/raw-body": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz",
"integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==",
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz",
"integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==",
"dev": true,
"dependencies": {
"bytes": "3.1.2",
"http-errors": "2.0.0",
"iconv-lite": "0.6.3",
"iconv-lite": "0.7.0",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8"
"node": ">= 0.10"
}
},
"node_modules/raw-body/node_modules/iconv-lite": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
"integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
"dev": true,
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/router": {
@ -1926,9 +2105,9 @@
}
},
"node_modules/router/node_modules/debug": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dev": true,
"dependencies": {
"ms": "^2.1.3"
@ -2026,9 +2205,9 @@
}
},
"node_modules/send/node_modules/debug": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dev": true,
"dependencies": {
"ms": "^2.1.3"
@ -2344,6 +2523,15 @@
"node": ">= 0.6"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/split2": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
@ -2385,21 +2573,21 @@
}
},
"node_modules/tldts": {
"version": "6.1.86",
"resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
"integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
"version": "7.0.16",
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.16.tgz",
"integrity": "sha512-5bdPHSwbKTeHmXrgecID4Ljff8rQjv7g8zKQPkCozRo2HWWni+p310FSn5ImI+9kWw9kK4lzOB5q/a6iv0IJsw==",
"dev": true,
"dependencies": {
"tldts-core": "^6.1.86"
"tldts-core": "^7.0.16"
},
"bin": {
"tldts": "bin/cli.js"
}
},
"node_modules/tldts-core": {
"version": "6.1.86",
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
"integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
"version": "7.0.16",
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.16.tgz",
"integrity": "sha512-XHhPmHxphLi+LGbH0G/O7dmUH9V65OY20R7vH8gETHsp5AZCjBk9l8sqmRKLaGOxnETU7XNSDUPtewAy/K6jbA==",
"dev": true
},
"node_modules/toidentifier": {
@ -2412,27 +2600,27 @@
}
},
"node_modules/tough-cookie": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
"integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz",
"integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==",
"dev": true,
"dependencies": {
"tldts": "^6.1.32"
"tldts": "^7.0.5"
},
"engines": {
"node": ">=16"
}
},
"node_modules/tr46": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz",
"integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==",
"dev": true,
"dependencies": {
"punycode": "^2.3.1"
},
"engines": {
"node": ">=18"
"node": ">=20"
}
},
"node_modules/type-is": {
@ -2456,9 +2644,9 @@
"dev": true
},
"node_modules/undici-types": {
"version": "7.10.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz",
"integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==",
"version": "7.12.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz",
"integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==",
"dev": true
},
"node_modules/unpipe": {
@ -2492,12 +2680,12 @@
}
},
"node_modules/webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz",
"integrity": "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==",
"dev": true,
"engines": {
"node": ">=12"
"node": ">=20"
}
},
"node_modules/whatwg-encoding": {
@ -2522,16 +2710,16 @@
}
},
"node_modules/whatwg-url": {
"version": "14.2.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
"version": "15.1.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz",
"integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==",
"dev": true,
"dependencies": {
"tr46": "^5.1.0",
"webidl-conversions": "^7.0.0"
"tr46": "^6.0.0",
"webidl-conversions": "^8.0.0"
},
"engines": {
"node": ">=18"
"node": ">=20"
}
},
"node_modules/wildstring": {

14
playwright/package.json

@ -8,14 +8,14 @@
"author": "",
"license": "ISC",
"devDependencies": {
"@playwright/test": "^1.54.2",
"dotenv": "^16.6.1",
"dotenv-expand": "^12.0.2",
"maildev": "npm:@timshel_npm/maildev@^3.2.1"
"@playwright/test": "1.55.1",
"dotenv": "17.2.2",
"dotenv-expand": "12.0.3",
"maildev": "npm:@timshel_npm/maildev@^3.2.3"
},
"dependencies": {
"mysql2": "^3.14.3",
"otpauth": "^9.4.0",
"pg": "^8.16.3"
"mysql2": "3.15.0",
"otpauth": "9.4.1",
"pg": "8.16.3"
}
}

2
rust-toolchain.toml

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

231
src/api/admin.rs

@ -1,22 +1,21 @@
use data_encoding::BASE64URL_NOPAD;
use once_cell::sync::Lazy;
use reqwest::Method;
use serde::de::DeserializeOwned;
use serde_json::Value;
use std::collections::HashMap;
use std::env;
use std::{env, sync::LazyLock};
use std::sync::RwLock;
use std::time::{SystemTime, UNIX_EPOCH};
use url::Url;
use rocket::serde::json::Json;
use reqwest::Method;
use rocket::{
form::Form,
http::{Cookie, CookieJar, MediaType, SameSite, Status},
request::{FromRequest, Outcome, Request},
response::{content::RawHtml as Html, Redirect},
serde::json::Json,
Catcher, Route,
};
use serde::de::DeserializeOwned;
use serde_json::Value;
use crate::{
api::{
@ -25,7 +24,14 @@ use crate::{
},
auth::{decode_admin, encode_jwt, generate_admin_claims, ClientIp, Secure},
config::ConfigBuilder,
db::{backup_database, get_sql_server_version, models::*, DbConn, DbConnType},
db::{
backup_sqlite, get_sql_server_version,
models::{
Attachment, Cipher, Collection, Device, Event, EventType, Group, Invitation, Membership, MembershipId,
MembershipType, OrgPolicy, OrgPolicyErr, Organization, OrganizationId, SsoUser, TwoFactor, User, UserId,
},
DbConn, DbConnType, ACTIVE_DB_TYPE,
},
error::{Error, MapResult},
http_client::make_http_request,
mail,
@ -83,21 +89,25 @@ pub fn catchers() -> Vec<Catcher> {
}
}
static DB_TYPE: Lazy<&str> = Lazy::new(|| {
DbConnType::from_url(&CONFIG.database_url())
.map(|t| match t {
DbConnType::sqlite => "SQLite",
DbConnType::mysql => "MySQL",
DbConnType::postgresql => "PostgreSQL",
})
.unwrap_or("Unknown")
static DB_TYPE: LazyLock<&str> = LazyLock::new(|| match ACTIVE_DB_TYPE.get() {
#[cfg(mysql)]
Some(DbConnType::Mysql) => "MySQL",
#[cfg(postgresql)]
Some(DbConnType::Postgresql) => "PostgreSQL",
#[cfg(sqlite)]
Some(DbConnType::Sqlite) => "SQLite",
_ => "Unknown",
});
static CAN_BACKUP: Lazy<bool> =
Lazy::new(|| DbConnType::from_url(&CONFIG.database_url()).map(|t| t == DbConnType::sqlite).unwrap_or(false));
#[cfg(sqlite)]
static CAN_BACKUP: LazyLock<bool> =
LazyLock::new(|| ACTIVE_DB_TYPE.get().map(|t| *t == DbConnType::Sqlite).unwrap_or(false));
#[cfg(not(sqlite))]
static CAN_BACKUP: LazyLock<bool> = LazyLock::new(|| false);
// OAuth2 state storage for CSRF protection (state -> expiration timestamp)
static OAUTH2_STATES: Lazy<RwLock<HashMap<String, u64>>> = Lazy::new(|| RwLock::new(HashMap::new()));
static OAUTH2_STATES: LazyLock<RwLock<HashMap<String, u64>>> =
LazyLock::new(|| RwLock::new(HashMap::new()));
#[get("/")]
fn admin_disabled() -> &'static str {
@ -159,10 +169,10 @@ fn admin_login(request: &Request<'_>) -> ApiResult<Html<String>> {
err_code!("Authorization failed.", Status::Unauthorized.code);
}
let redirect = request.segments::<std::path::PathBuf>(0..).unwrap_or_default().display().to_string();
render_admin_login(None, Some(redirect))
render_admin_login(None, Some(&redirect))
}
fn render_admin_login(msg: Option<&str>, redirect: Option<String>) -> ApiResult<Html<String>> {
fn render_admin_login(msg: Option<&str>, redirect: Option<&str>) -> ApiResult<Html<String>> {
// If there is an error, show it
let msg = msg.map(|msg| format!("Error: {msg}"));
let json = json!({
@ -196,14 +206,17 @@ fn post_admin_login(
if crate::ratelimit::check_limit_admin(&ip.ip).is_err() {
return Err(AdminResponse::TooManyRequests(render_admin_login(
Some("Too many requests, try again later."),
redirect,
redirect.as_deref(),
)));
}
// If the token is invalid, redirect to login page
if !_validate_token(&data.token) {
error!("Invalid admin token. IP: {}", ip.ip);
Err(AdminResponse::Unauthorized(render_admin_login(Some("Invalid admin token, please try again."), redirect)))
Err(AdminResponse::Unauthorized(render_admin_login(
Some("Invalid admin token, please try again."),
redirect.as_deref(),
)))
} else {
// If the token received is valid, generate JWT and save it as a cookie
let claims = generate_admin_claims();
@ -295,7 +308,7 @@ struct InviteData {
email: String,
}
async fn get_user_or_404(user_id: &UserId, conn: &mut DbConn) -> ApiResult<User> {
async fn get_user_or_404(user_id: &UserId, conn: &DbConn) -> ApiResult<User> {
if let Some(user) = User::find_by_uuid(user_id, conn).await {
Ok(user)
} else {
@ -304,15 +317,15 @@ async fn get_user_or_404(user_id: &UserId, conn: &mut DbConn) -> ApiResult<User>
}
#[post("/invite", format = "application/json", data = "<data>")]
async fn invite_user(data: Json<InviteData>, _token: AdminToken, mut conn: DbConn) -> JsonResult {
async fn invite_user(data: Json<InviteData>, _token: AdminToken, conn: DbConn) -> JsonResult {
let data: InviteData = data.into_inner();
if User::find_by_mail(&data.email, &mut conn).await.is_some() {
if User::find_by_mail(&data.email, &conn).await.is_some() {
err_code!("User already exists", Status::Conflict.code)
}
let mut user = User::new(data.email, None);
let mut user = User::new(&data.email, None);
async fn _generate_invite(user: &User, conn: &mut DbConn) -> EmptyResult {
async fn _generate_invite(user: &User, conn: &DbConn) -> EmptyResult {
if CONFIG.mail_enabled() {
let org_id: OrganizationId = FAKE_ADMIN_UUID.to_string().into();
let member_id: MembershipId = FAKE_ADMIN_UUID.to_string().into();
@ -323,10 +336,10 @@ async fn invite_user(data: Json<InviteData>, _token: AdminToken, mut conn: DbCon
}
}
_generate_invite(&user, &mut conn).await.map_err(|e| e.with_code(Status::InternalServerError.code))?;
user.save(&mut conn).await.map_err(|e| e.with_code(Status::InternalServerError.code))?;
_generate_invite(&user, &conn).await.map_err(|e| e.with_code(Status::InternalServerError.code))?;
user.save(&conn).await.map_err(|e| e.with_code(Status::InternalServerError.code))?;
Ok(Json(user.to_json(&mut conn).await))
Ok(Json(user.to_json(&conn).await))
}
#[post("/test/smtp", format = "application/json", data = "<data>")]
@ -480,14 +493,14 @@ fn logout(cookies: &CookieJar<'_>) -> Redirect {
}
#[get("/users")]
async fn get_users_json(_token: AdminToken, mut conn: DbConn) -> Json<Value> {
let users = User::get_all(&mut conn).await;
async fn get_users_json(_token: AdminToken, conn: DbConn) -> Json<Value> {
let users = User::get_all(&conn).await;
let mut users_json = Vec::with_capacity(users.len());
for (u, _) in users {
let mut usr = u.to_json(&mut conn).await;
let mut usr = u.to_json(&conn).await;
usr["userEnabled"] = json!(u.enabled);
usr["createdAt"] = json!(format_naive_datetime_local(&u.created_at, DT_FMT));
usr["lastActive"] = match u.last_active(&mut conn).await {
usr["lastActive"] = match u.last_active(&conn).await {
Some(dt) => json!(format_naive_datetime_local(&dt, DT_FMT)),
None => json!(None::<String>),
};
@ -498,17 +511,17 @@ async fn get_users_json(_token: AdminToken, mut conn: DbConn) -> Json<Value> {
}
#[get("/users/overview")]
async fn users_overview(_token: AdminToken, mut conn: DbConn) -> ApiResult<Html<String>> {
let users = User::get_all(&mut conn).await;
async fn users_overview(_token: AdminToken, conn: DbConn) -> ApiResult<Html<String>> {
let users = User::get_all(&conn).await;
let mut users_json = Vec::with_capacity(users.len());
for (u, sso_u) in users {
let mut usr = u.to_json(&mut conn).await;
usr["cipher_count"] = json!(Cipher::count_owned_by_user(&u.uuid, &mut conn).await);
usr["attachment_count"] = json!(Attachment::count_by_user(&u.uuid, &mut conn).await);
usr["attachment_size"] = json!(get_display_size(Attachment::size_by_user(&u.uuid, &mut conn).await));
let mut usr = u.to_json(&conn).await;
usr["cipher_count"] = json!(Cipher::count_owned_by_user(&u.uuid, &conn).await);
usr["attachment_count"] = json!(Attachment::count_by_user(&u.uuid, &conn).await);
usr["attachment_size"] = json!(get_display_size(Attachment::size_by_user(&u.uuid, &conn).await));
usr["user_enabled"] = json!(u.enabled);
usr["created_at"] = json!(format_naive_datetime_local(&u.created_at, DT_FMT));
usr["last_active"] = match u.last_active(&mut conn).await {
usr["last_active"] = match u.last_active(&conn).await {
Some(dt) => json!(format_naive_datetime_local(&dt, DT_FMT)),
None => json!("Never"),
};
@ -523,9 +536,9 @@ async fn users_overview(_token: AdminToken, mut conn: DbConn) -> ApiResult<Html<
}
#[get("/users/by-mail/<mail>")]
async fn get_user_by_mail_json(mail: &str, _token: AdminToken, mut conn: DbConn) -> JsonResult {
if let Some(u) = User::find_by_mail(mail, &mut conn).await {
let mut usr = u.to_json(&mut conn).await;
async fn get_user_by_mail_json(mail: &str, _token: AdminToken, conn: DbConn) -> JsonResult {
if let Some(u) = User::find_by_mail(mail, &conn).await {
let mut usr = u.to_json(&conn).await;
usr["userEnabled"] = json!(u.enabled);
usr["createdAt"] = json!(format_naive_datetime_local(&u.created_at, DT_FMT));
Ok(Json(usr))
@ -535,21 +548,21 @@ async fn get_user_by_mail_json(mail: &str, _token: AdminToken, mut conn: DbConn)
}
#[get("/users/<user_id>")]
async fn get_user_json(user_id: UserId, _token: AdminToken, mut conn: DbConn) -> JsonResult {
let u = get_user_or_404(&user_id, &mut conn).await?;
let mut usr = u.to_json(&mut conn).await;
async fn get_user_json(user_id: UserId, _token: AdminToken, conn: DbConn) -> JsonResult {
let u = get_user_or_404(&user_id, &conn).await?;
let mut usr = u.to_json(&conn).await;
usr["userEnabled"] = json!(u.enabled);
usr["createdAt"] = json!(format_naive_datetime_local(&u.created_at, DT_FMT));
Ok(Json(usr))
}
#[post("/users/<user_id>/delete", format = "application/json")]
async fn delete_user(user_id: UserId, token: AdminToken, mut conn: DbConn) -> EmptyResult {
let user = get_user_or_404(&user_id, &mut conn).await?;
async fn delete_user(user_id: UserId, token: AdminToken, conn: DbConn) -> EmptyResult {
let user = get_user_or_404(&user_id, &conn).await?;
// Get the membership records before deleting the actual user
let memberships = Membership::find_any_state_by_user(&user_id, &mut conn).await;
let res = user.delete(&mut conn).await;
let memberships = Membership::find_any_state_by_user(&user_id, &conn).await;
let res = user.delete(&conn).await;
for membership in memberships {
log_event(
@ -559,7 +572,7 @@ async fn delete_user(user_id: UserId, token: AdminToken, mut conn: DbConn) -> Em
&ACTING_ADMIN_USER.into(),
14, // Use UnknownBrowser type
&token.ip.ip,
&mut conn,
&conn,
)
.await;
}
@ -568,9 +581,9 @@ async fn delete_user(user_id: UserId, token: AdminToken, mut conn: DbConn) -> Em
}
#[delete("/users/<user_id>/sso", format = "application/json")]
async fn delete_sso_user(user_id: UserId, token: AdminToken, mut conn: DbConn) -> EmptyResult {
let memberships = Membership::find_any_state_by_user(&user_id, &mut conn).await;
let res = SsoUser::delete(&user_id, &mut conn).await;
async fn delete_sso_user(user_id: UserId, token: AdminToken, conn: DbConn) -> EmptyResult {
let memberships = Membership::find_any_state_by_user(&user_id, &conn).await;
let res = SsoUser::delete(&user_id, &conn).await;
for membership in memberships {
log_event(
@ -580,7 +593,7 @@ async fn delete_sso_user(user_id: UserId, token: AdminToken, mut conn: DbConn) -
&ACTING_ADMIN_USER.into(),
14, // Use UnknownBrowser type
&token.ip.ip,
&mut conn,
&conn,
)
.await;
}
@ -589,13 +602,13 @@ async fn delete_sso_user(user_id: UserId, token: AdminToken, mut conn: DbConn) -
}
#[post("/users/<user_id>/deauth", format = "application/json")]
async fn deauth_user(user_id: UserId, _token: AdminToken, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
let mut user = get_user_or_404(&user_id, &mut conn).await?;
async fn deauth_user(user_id: UserId, _token: AdminToken, conn: DbConn, nt: Notify<'_>) -> EmptyResult {
let mut user = get_user_or_404(&user_id, &conn).await?;
nt.send_logout(&user, None, &mut conn).await;
nt.send_logout(&user, None, &conn).await;
if CONFIG.push_enabled() {
for device in Device::find_push_devices_by_user(&user.uuid, &mut conn).await {
for device in Device::find_push_devices_by_user(&user.uuid, &conn).await {
match unregister_push_device(&device.push_uuid).await {
Ok(r) => r,
Err(e) => error!("Unable to unregister devices from Bitwarden server: {e}"),
@ -603,46 +616,46 @@ async fn deauth_user(user_id: UserId, _token: AdminToken, mut conn: DbConn, nt:
}
}
Device::delete_all_by_user(&user.uuid, &mut conn).await?;
Device::delete_all_by_user(&user.uuid, &conn).await?;
user.reset_security_stamp();
user.save(&mut conn).await
user.save(&conn).await
}
#[post("/users/<user_id>/disable", format = "application/json")]
async fn disable_user(user_id: UserId, _token: AdminToken, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
let mut user = get_user_or_404(&user_id, &mut conn).await?;
Device::delete_all_by_user(&user.uuid, &mut conn).await?;
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?;
Device::delete_all_by_user(&user.uuid, &conn).await?;
user.reset_security_stamp();
user.enabled = false;
let save_result = user.save(&mut conn).await;
let save_result = user.save(&conn).await;
nt.send_logout(&user, None, &mut conn).await;
nt.send_logout(&user, None, &conn).await;
save_result
}
#[post("/users/<user_id>/enable", format = "application/json")]
async fn enable_user(user_id: UserId, _token: AdminToken, mut conn: DbConn) -> EmptyResult {
let mut user = get_user_or_404(&user_id, &mut conn).await?;
async fn enable_user(user_id: UserId, _token: AdminToken, conn: DbConn) -> EmptyResult {
let mut user = get_user_or_404(&user_id, &conn).await?;
user.enabled = true;
user.save(&mut conn).await
user.save(&conn).await
}
#[post("/users/<user_id>/remove-2fa", format = "application/json")]
async fn remove_2fa(user_id: UserId, token: AdminToken, mut conn: DbConn) -> EmptyResult {
let mut user = get_user_or_404(&user_id, &mut conn).await?;
TwoFactor::delete_all_by_user(&user.uuid, &mut conn).await?;
two_factor::enforce_2fa_policy(&user, &ACTING_ADMIN_USER.into(), 14, &token.ip.ip, &mut conn).await?;
async fn remove_2fa(user_id: UserId, token: AdminToken, conn: DbConn) -> EmptyResult {
let mut user = get_user_or_404(&user_id, &conn).await?;
TwoFactor::delete_all_by_user(&user.uuid, &conn).await?;
two_factor::enforce_2fa_policy(&user, &ACTING_ADMIN_USER.into(), 14, &token.ip.ip, &conn).await?;
user.totp_recover = None;
user.save(&mut conn).await
user.save(&conn).await
}
#[post("/users/<user_id>/invite/resend", format = "application/json")]
async fn resend_user_invite(user_id: UserId, _token: AdminToken, mut conn: DbConn) -> EmptyResult {
if let Some(user) = User::find_by_uuid(&user_id, &mut conn).await {
async fn resend_user_invite(user_id: UserId, _token: AdminToken, conn: DbConn) -> EmptyResult {
if let Some(user) = User::find_by_uuid(&user_id, &conn).await {
//TODO: replace this with user.status check when it will be available (PR#3397)
if !user.password_hash.is_empty() {
err_code!("User already accepted invitation", Status::BadRequest.code);
@ -668,10 +681,10 @@ struct MembershipTypeData {
}
#[post("/users/org_type", format = "application/json", data = "<data>")]
async fn update_membership_type(data: Json<MembershipTypeData>, token: AdminToken, mut conn: DbConn) -> EmptyResult {
async fn update_membership_type(data: Json<MembershipTypeData>, token: AdminToken, conn: DbConn) -> EmptyResult {
let data: MembershipTypeData = data.into_inner();
let Some(mut member_to_edit) = Membership::find_by_user_and_org(&data.user_uuid, &data.org_uuid, &mut conn).await
let Some(mut member_to_edit) = Membership::find_by_user_and_org(&data.user_uuid, &data.org_uuid, &conn).await
else {
err!("The specified user isn't member of the organization")
};
@ -683,7 +696,7 @@ async fn update_membership_type(data: Json<MembershipTypeData>, token: AdminToke
if member_to_edit.atype == MembershipType::Owner && new_type != MembershipType::Owner {
// Removing owner permission, check that there is at least one other confirmed owner
if Membership::count_confirmed_by_org_and_type(&data.org_uuid, MembershipType::Owner, &mut conn).await <= 1 {
if Membership::count_confirmed_by_org_and_type(&data.org_uuid, MembershipType::Owner, &conn).await <= 1 {
err!("Can't change the type of the last owner")
}
}
@ -691,11 +704,11 @@ async fn update_membership_type(data: Json<MembershipTypeData>, token: AdminToke
// This check is also done at api::organizations::{accept_invite, _confirm_invite, _activate_member, edit_member}, update_membership_type
// It returns different error messages per function.
if new_type < MembershipType::Admin {
match OrgPolicy::is_user_allowed(&member_to_edit.user_uuid, &member_to_edit.org_uuid, true, &mut conn).await {
match OrgPolicy::is_user_allowed(&member_to_edit.user_uuid, &member_to_edit.org_uuid, true, &conn).await {
Ok(_) => {}
Err(OrgPolicyErr::TwoFactorMissing) => {
if CONFIG.email_2fa_auto_fallback() {
two_factor::email::find_and_activate_email_2fa(&member_to_edit.user_uuid, &mut conn).await?;
two_factor::email::find_and_activate_email_2fa(&member_to_edit.user_uuid, &conn).await?;
} else {
err!("You cannot modify this user to this type because they have not setup 2FA");
}
@ -713,32 +726,32 @@ async fn update_membership_type(data: Json<MembershipTypeData>, token: AdminToke
&ACTING_ADMIN_USER.into(),
14, // Use UnknownBrowser type
&token.ip.ip,
&mut conn,
&conn,
)
.await;
member_to_edit.atype = new_type;
member_to_edit.save(&mut conn).await
member_to_edit.save(&conn).await
}
#[post("/users/update_revision", format = "application/json")]
async fn update_revision_users(_token: AdminToken, mut conn: DbConn) -> EmptyResult {
User::update_all_revisions(&mut conn).await
async fn update_revision_users(_token: AdminToken, conn: DbConn) -> EmptyResult {
User::update_all_revisions(&conn).await
}
#[get("/organizations/overview")]
async fn organizations_overview(_token: AdminToken, mut conn: DbConn) -> ApiResult<Html<String>> {
let organizations = Organization::get_all(&mut conn).await;
async fn organizations_overview(_token: AdminToken, conn: DbConn) -> ApiResult<Html<String>> {
let organizations = Organization::get_all(&conn).await;
let mut organizations_json = Vec::with_capacity(organizations.len());
for o in organizations {
let mut org = o.to_json();
org["user_count"] = json!(Membership::count_by_org(&o.uuid, &mut conn).await);
org["cipher_count"] = json!(Cipher::count_by_org(&o.uuid, &mut conn).await);
org["collection_count"] = json!(Collection::count_by_org(&o.uuid, &mut conn).await);
org["group_count"] = json!(Group::count_by_org(&o.uuid, &mut conn).await);
org["event_count"] = json!(Event::count_by_org(&o.uuid, &mut conn).await);
org["attachment_count"] = json!(Attachment::count_by_org(&o.uuid, &mut conn).await);
org["attachment_size"] = json!(get_display_size(Attachment::size_by_org(&o.uuid, &mut conn).await));
org["user_count"] = json!(Membership::count_by_org(&o.uuid, &conn).await);
org["cipher_count"] = json!(Cipher::count_by_org(&o.uuid, &conn).await);
org["collection_count"] = json!(Collection::count_by_org(&o.uuid, &conn).await);
org["group_count"] = json!(Group::count_by_org(&o.uuid, &conn).await);
org["event_count"] = json!(Event::count_by_org(&o.uuid, &conn).await);
org["attachment_count"] = json!(Attachment::count_by_org(&o.uuid, &conn).await);
org["attachment_size"] = json!(get_display_size(Attachment::size_by_org(&o.uuid, &conn).await));
organizations_json.push(org);
}
@ -747,9 +760,9 @@ async fn organizations_overview(_token: AdminToken, mut conn: DbConn) -> ApiResu
}
#[post("/organizations/<org_id>/delete", format = "application/json")]
async fn delete_organization(org_id: OrganizationId, _token: AdminToken, mut conn: DbConn) -> EmptyResult {
let org = Organization::find_by_uuid(&org_id, &mut conn).await.map_res("Organization doesn't exist")?;
org.delete(&mut conn).await
async fn delete_organization(org_id: OrganizationId, _token: AdminToken, conn: DbConn) -> EmptyResult {
let org = Organization::find_by_uuid(&org_id, &conn).await.map_res("Organization doesn't exist")?;
org.delete(&conn).await
}
#[derive(Deserialize)]
@ -837,7 +850,7 @@ async fn get_ntp_time(has_http_access: bool) -> String {
}
#[get("/diagnostics")]
async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn) -> ApiResult<Html<String>> {
async fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> ApiResult<Html<String>> {
use chrono::prelude::*;
use std::net::ToSocketAddrs;
@ -891,7 +904,7 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn)
"uses_proxy": uses_proxy,
"enable_websocket": &CONFIG.enable_websocket(),
"db_type": *DB_TYPE,
"db_version": get_sql_server_version(&mut conn).await,
"db_version": get_sql_server_version(&conn).await,
"admin_url": format!("{}/diagnostics", admin_url()),
"overrides": &CONFIG.get_overrides().join(", "),
"host_arch": env::consts::ARCH,
@ -935,9 +948,9 @@ async fn delete_config(_token: AdminToken) -> EmptyResult {
}
#[post("/config/backup_db", format = "application/json")]
async fn backup_db(_token: AdminToken, mut conn: DbConn) -> ApiResult<String> {
fn backup_db(_token: AdminToken) -> ApiResult<String> {
if *CAN_BACKUP {
match backup_database(&mut conn).await {
match backup_sqlite() {
Ok(f) => Ok(format!("Backup to '{f}' was successful")),
Err(e) => err!(format!("Backup was unsuccessful {e}")),
}
@ -960,11 +973,7 @@ impl<'r> FromRequest<'r> for AdminToken {
_ => err_handler!("Error getting Client IP"),
};
if CONFIG.disable_admin_token() {
Outcome::Success(Self {
ip,
})
} else {
if !CONFIG.disable_admin_token() {
let cookies = request.cookies();
let access_token = match cookies.get(COOKIE_NAME) {
@ -988,10 +997,10 @@ impl<'r> FromRequest<'r> for AdminToken {
error!("Invalid or expired admin JWT. IP: {}.", &ip.ip);
return Outcome::Error((Status::Unauthorized, "Session expired"));
}
Outcome::Success(Self {
ip,
})
}
Outcome::Success(Self {
ip,
})
}
}

271
src/api/core/accounts.rs

@ -13,7 +13,14 @@ use crate::{
},
auth::{decode_delete, decode_invite, decode_verify_email, ClientHeaders, Headers},
crypto,
db::{models::*, DbConn},
db::{
models::{
AuthRequest, AuthRequestId, Cipher, CipherId, Device, DeviceId, DeviceType, EmergencyAccess,
EmergencyAccessId, EventType, Folder, FolderId, Invitation, Membership, MembershipId, OrgPolicy,
OrgPolicyType, Organization, OrganizationId, Send, SendId, User, UserId, UserKdfType,
},
DbConn,
},
mail,
util::{format_date, NumberOrString},
CONFIG,
@ -143,7 +150,7 @@ fn enforce_password_hint_setting(password_hint: &Option<String>) -> EmptyResult
}
Ok(())
}
async fn is_email_2fa_required(member_id: Option<MembershipId>, conn: &mut DbConn) -> bool {
async fn is_email_2fa_required(member_id: Option<MembershipId>, conn: &DbConn) -> bool {
if !CONFIG._enable_email_2fa() {
return false;
}
@ -161,7 +168,7 @@ async fn register(data: Json<RegisterData>, conn: DbConn) -> JsonResult {
_register(data, false, conn).await
}
pub async fn _register(data: Json<RegisterData>, email_verification: bool, mut conn: DbConn) -> JsonResult {
pub async fn _register(data: Json<RegisterData>, email_verification: bool, conn: DbConn) -> JsonResult {
let mut data: RegisterData = data.into_inner();
let email = data.email.to_lowercase();
@ -243,7 +250,7 @@ pub async fn _register(data: Json<RegisterData>, email_verification: bool, mut c
let password_hint = clean_password_hint(&data.master_password_hint);
enforce_password_hint_setting(&password_hint)?;
let mut user = match User::find_by_mail(&email, &mut conn).await {
let mut user = match User::find_by_mail(&email, &conn).await {
Some(user) => {
if !user.password_hash.is_empty() {
err!("Registration not allowed or user already exists")
@ -258,12 +265,12 @@ pub async fn _register(data: Json<RegisterData>, email_verification: bool, mut c
} else {
err!("Registration email does not match invite email")
}
} else if Invitation::take(&email, &mut conn).await {
Membership::accept_user_invitations(&user.uuid, &mut conn).await?;
} else if Invitation::take(&email, &conn).await {
Membership::accept_user_invitations(&user.uuid, &conn).await?;
user
} else if CONFIG.is_signup_allowed(&email)
|| (CONFIG.emergency_access_allowed()
&& EmergencyAccess::find_invited_by_grantee_email(&email, &mut conn).await.is_some())
&& EmergencyAccess::find_invited_by_grantee_email(&email, &conn).await.is_some())
{
user
} else {
@ -274,11 +281,11 @@ pub async fn _register(data: Json<RegisterData>, email_verification: bool, mut c
// Order is important here; the invitation check must come first
// because the vaultwarden admin can invite anyone, regardless
// of other signup restrictions.
if Invitation::take(&email, &mut conn).await
if Invitation::take(&email, &conn).await
|| CONFIG.is_signup_allowed(&email)
|| pending_emergency_access.is_some()
{
User::new(email.clone(), None)
User::new(&email, None)
} else {
err!("Registration not allowed or user already exists")
}
@ -286,9 +293,9 @@ pub async fn _register(data: Json<RegisterData>, email_verification: bool, mut c
};
// Make sure we don't leave a lingering invitation.
Invitation::take(&email, &mut conn).await;
Invitation::take(&email, &conn).await;
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);
user.password_hint = password_hint;
@ -317,17 +324,17 @@ pub async fn _register(data: Json<RegisterData>, email_verification: bool, mut c
error!("Error sending welcome email: {e:#?}");
}
if email_verified && is_email_2fa_required(data.organization_user_id, &mut conn).await {
email::activate_email_2fa(&user, &mut conn).await.ok();
if email_verified && is_email_2fa_required(data.organization_user_id, &conn).await {
email::activate_email_2fa(&user, &conn).await.ok();
}
}
user.save(&mut conn).await?;
user.save(&conn).await?;
// accept any open emergency access invitations
if !CONFIG.mail_enabled() && CONFIG.emergency_access_allowed() {
for mut emergency_invite in EmergencyAccess::find_all_invited_by_grantee_email(&user.email, &mut conn).await {
emergency_invite.accept_invite(&user.uuid, &user.email, &mut conn).await.ok();
for mut emergency_invite in EmergencyAccess::find_all_invited_by_grantee_email(&user.email, &conn).await {
emergency_invite.accept_invite(&user.uuid, &user.email, &conn).await.ok();
}
}
@ -338,7 +345,7 @@ pub async fn _register(data: Json<RegisterData>, email_verification: bool, mut c
}
#[post("/accounts/set-password", data = "<data>")]
async fn post_set_password(data: Json<SetPasswordData>, headers: Headers, mut conn: DbConn) -> JsonResult {
async fn post_set_password(data: Json<SetPasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
let data: SetPasswordData = data.into_inner();
let mut user = headers.user;
@ -351,7 +358,7 @@ async fn post_set_password(data: Json<SetPasswordData>, headers: Headers, mut co
let password_hint = clean_password_hint(&data.master_password_hint);
enforce_password_hint_setting(&password_hint)?;
set_kdf_data(&mut user, data.kdf)?;
set_kdf_data(&mut user, &data.kdf)?;
user.set_password(
&data.master_password_hash,
@ -368,30 +375,30 @@ async fn post_set_password(data: Json<SetPasswordData>, headers: Headers, mut co
if let Some(identifier) = data.org_identifier {
if identifier != crate::sso::FAKE_IDENTIFIER {
let org = match Organization::find_by_uuid(&identifier.into(), &mut conn).await {
let org = match Organization::find_by_uuid(&identifier.into(), &conn).await {
None => err!("Failed to retrieve the associated organization"),
Some(org) => org,
};
let membership = match Membership::find_by_user_and_org(&user.uuid, &org.uuid, &mut conn).await {
let membership = match Membership::find_by_user_and_org(&user.uuid, &org.uuid, &conn).await {
None => err!("Failed to retrieve the invitation"),
Some(org) => org,
};
accept_org_invite(&user, membership, None, &mut conn).await?;
accept_org_invite(&user, membership, None, &conn).await?;
}
}
if CONFIG.mail_enabled() {
mail::send_welcome(&user.email.to_lowercase()).await?;
} else {
Membership::accept_user_invitations(&user.uuid, &mut conn).await?;
Membership::accept_user_invitations(&user.uuid, &conn).await?;
}
log_user_event(EventType::UserChangedPassword as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &mut conn)
log_user_event(EventType::UserChangedPassword as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &conn)
.await;
user.save(&mut conn).await?;
user.save(&conn).await?;
Ok(Json(json!({
"Object": "set-password",
@ -400,8 +407,8 @@ async fn post_set_password(data: Json<SetPasswordData>, headers: Headers, mut co
}
#[get("/accounts/profile")]
async fn profile(headers: Headers, mut conn: DbConn) -> Json<Value> {
Json(headers.user.to_json(&mut conn).await)
async fn profile(headers: Headers, conn: DbConn) -> Json<Value> {
Json(headers.user.to_json(&conn).await)
}
#[derive(Debug, Deserialize)]
@ -417,7 +424,7 @@ async fn put_profile(data: Json<ProfileData>, headers: Headers, conn: DbConn) ->
}
#[post("/accounts/profile", data = "<data>")]
async fn post_profile(data: Json<ProfileData>, headers: Headers, mut conn: DbConn) -> JsonResult {
async fn post_profile(data: Json<ProfileData>, headers: Headers, conn: DbConn) -> JsonResult {
let data: ProfileData = data.into_inner();
// Check if the length of the username exceeds 50 characters (Same is Upstream Bitwarden)
@ -429,8 +436,8 @@ async fn post_profile(data: Json<ProfileData>, headers: Headers, mut conn: DbCon
let mut user = headers.user;
user.name = data.name;
user.save(&mut conn).await?;
Ok(Json(user.to_json(&mut conn).await))
user.save(&conn).await?;
Ok(Json(user.to_json(&conn).await))
}
#[derive(Deserialize)]
@ -440,7 +447,7 @@ struct AvatarData {
}
#[put("/accounts/avatar", data = "<data>")]
async fn put_avatar(data: Json<AvatarData>, headers: Headers, mut conn: DbConn) -> JsonResult {
async fn put_avatar(data: Json<AvatarData>, headers: Headers, conn: DbConn) -> JsonResult {
let data: AvatarData = data.into_inner();
// It looks like it only supports the 6 hex color format.
@ -455,13 +462,13 @@ async fn put_avatar(data: Json<AvatarData>, headers: Headers, mut conn: DbConn)
let mut user = headers.user;
user.avatar_color = data.avatar_color;
user.save(&mut conn).await?;
Ok(Json(user.to_json(&mut conn).await))
user.save(&conn).await?;
Ok(Json(user.to_json(&conn).await))
}
#[get("/users/<user_id>/public-key")]
async fn get_public_keys(user_id: UserId, _headers: Headers, mut conn: DbConn) -> JsonResult {
let user = match User::find_by_uuid(&user_id, &mut conn).await {
async fn get_public_keys(user_id: UserId, _headers: Headers, conn: DbConn) -> JsonResult {
let user = match User::find_by_uuid(&user_id, &conn).await {
Some(user) if user.public_key.is_some() => user,
Some(_) => err_code!("User has no public_key", Status::NotFound.code),
None => err_code!("User doesn't exist", Status::NotFound.code),
@ -475,7 +482,7 @@ async fn get_public_keys(user_id: UserId, _headers: Headers, mut conn: DbConn) -
}
#[post("/accounts/keys", data = "<data>")]
async fn post_keys(data: Json<KeysData>, headers: Headers, mut conn: DbConn) -> JsonResult {
async fn post_keys(data: Json<KeysData>, headers: Headers, conn: DbConn) -> JsonResult {
let data: KeysData = data.into_inner();
let mut user = headers.user;
@ -483,7 +490,7 @@ async fn post_keys(data: Json<KeysData>, headers: Headers, mut conn: DbConn) ->
user.private_key = Some(data.encrypted_private_key);
user.public_key = Some(data.public_key);
user.save(&mut conn).await?;
user.save(&conn).await?;
Ok(Json(json!({
"privateKey": user.private_key,
@ -502,7 +509,7 @@ struct ChangePassData {
}
#[post("/accounts/password", data = "<data>")]
async fn post_password(data: Json<ChangePassData>, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
async fn post_password(data: Json<ChangePassData>, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult {
let data: ChangePassData = data.into_inner();
let mut user = headers.user;
@ -513,7 +520,7 @@ async fn post_password(data: Json<ChangePassData>, headers: Headers, mut conn: D
user.password_hint = clean_password_hint(&data.master_password_hint);
enforce_password_hint_setting(&user.password_hint)?;
log_user_event(EventType::UserChangedPassword as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &mut conn)
log_user_event(EventType::UserChangedPassword as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &conn)
.await;
user.set_password(
@ -528,12 +535,12 @@ async fn post_password(data: Json<ChangePassData>, headers: Headers, mut conn: D
]),
);
let save_result = user.save(&mut conn).await;
let save_result = user.save(&conn).await;
// 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.
// Adding the device uuid will prevent this.
nt.send_logout(&user, Some(headers.device.uuid.clone()), &mut conn).await;
nt.send_logout(&user, Some(headers.device.uuid.clone()), &conn).await;
save_result
}
@ -549,7 +556,7 @@ struct ChangeKdfData {
key: String,
}
fn set_kdf_data(user: &mut User, data: KDFData) -> EmptyResult {
fn set_kdf_data(user: &mut User, data: &KDFData) -> EmptyResult {
if data.kdf == UserKdfType::Pbkdf2 as i32 && data.kdf_iterations < 100_000 {
err!("PBKDF2 KDF iterations must be at least 100000.")
}
@ -585,7 +592,7 @@ fn set_kdf_data(user: &mut User, data: KDFData) -> EmptyResult {
}
#[post("/accounts/kdf", data = "<data>")]
async fn post_kdf(data: Json<ChangeKdfData>, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
async fn post_kdf(data: Json<ChangeKdfData>, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult {
let data: ChangeKdfData = data.into_inner();
let mut user = headers.user;
@ -593,12 +600,12 @@ async fn post_kdf(data: Json<ChangeKdfData>, headers: Headers, mut conn: DbConn,
err!("Invalid password")
}
set_kdf_data(&mut user, data.kdf)?;
set_kdf_data(&mut user, &data.kdf)?;
user.set_password(&data.new_master_password_hash, Some(data.key), true, None);
let save_result = user.save(&mut conn).await;
let save_result = user.save(&conn).await;
nt.send_logout(&user, Some(headers.device.uuid.clone()), &mut conn).await;
nt.send_logout(&user, Some(headers.device.uuid.clone()), &conn).await;
save_result
}
@ -753,7 +760,7 @@ fn validate_keydata(
}
#[post("/accounts/key-management/rotate-user-account-keys", data = "<data>")]
async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
async fn post_rotatekey(data: Json<KeyData>, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult {
// TODO: See if we can wrap everything within a SQL Transaction. If something fails it should revert everything.
let data: KeyData = data.into_inner();
@ -771,13 +778,13 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn,
// TODO: Ideally we'd do everything after this point in a single transaction.
let mut existing_ciphers = Cipher::find_owned_by_user(user_id, &mut conn).await;
let mut existing_folders = Folder::find_by_user(user_id, &mut conn).await;
let mut existing_emergency_access = EmergencyAccess::find_all_by_grantor_uuid(user_id, &mut conn).await;
let mut existing_memberships = Membership::find_by_user(user_id, &mut conn).await;
let mut existing_ciphers = Cipher::find_owned_by_user(user_id, &conn).await;
let mut existing_folders = Folder::find_by_user(user_id, &conn).await;
let mut existing_emergency_access = EmergencyAccess::find_all_confirmed_by_grantor_uuid(user_id, &conn).await;
let mut existing_memberships = Membership::find_by_user(user_id, &conn).await;
// We only rotate the reset password key if it is set.
existing_memberships.retain(|m| m.reset_password_key.is_some());
let mut existing_sends = Send::find_by_user(user_id, &mut conn).await;
let mut existing_sends = Send::find_by_user(user_id, &conn).await;
validate_keydata(
&data,
@ -799,7 +806,7 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn,
};
saved_folder.name = folder_data.name;
saved_folder.save(&mut conn).await?
saved_folder.save(&conn).await?
}
}
@ -812,7 +819,7 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn,
};
saved_emergency_access.key_encrypted = Some(emergency_access_data.key_encrypted);
saved_emergency_access.save(&mut conn).await?
saved_emergency_access.save(&conn).await?
}
// Update reset password data
@ -824,7 +831,7 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn,
};
membership.reset_password_key = Some(reset_password_data.reset_password_key);
membership.save(&mut conn).await?
membership.save(&conn).await?
}
// Update send data
@ -833,7 +840,7 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn,
err!("Send doesn't exist")
};
update_send_from_data(send, send_data, &headers, &mut conn, &nt, UpdateType::None).await?;
update_send_from_data(send, send_data, &headers, &conn, &nt, UpdateType::None).await?;
}
// Update cipher data
@ -849,7 +856,7 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn,
// Prevent triggering cipher updates via WebSockets by settings UpdateType::None
// The user sessions are invalidated because all the ciphers were re-encrypted and thus triggering an update could cause issues.
// We force the users to logout after the user has been saved to try and prevent these issues.
update_cipher_from_data(saved_cipher, cipher_data, &headers, None, &mut conn, &nt, UpdateType::None).await?
update_cipher_from_data(saved_cipher, cipher_data, &headers, None, &conn, &nt, UpdateType::None).await?
}
}
@ -864,28 +871,28 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn,
None,
);
let save_result = user.save(&mut conn).await;
let save_result = user.save(&conn).await;
// 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.
// Adding the device uuid will prevent this.
nt.send_logout(&user, Some(headers.device.uuid.clone()), &mut conn).await;
nt.send_logout(&user, Some(headers.device.uuid.clone()), &conn).await;
save_result
}
#[post("/accounts/security-stamp", data = "<data>")]
async fn post_sstamp(data: Json<PasswordOrOtpData>, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
async fn post_sstamp(data: Json<PasswordOrOtpData>, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult {
let data: PasswordOrOtpData = data.into_inner();
let mut user = headers.user;
data.validate(&user, true, &mut conn).await?;
data.validate(&user, true, &conn).await?;
Device::delete_all_by_user(&user.uuid, &mut conn).await?;
Device::delete_all_by_user(&user.uuid, &conn).await?;
user.reset_security_stamp();
let save_result = user.save(&mut conn).await;
let save_result = user.save(&conn).await;
nt.send_logout(&user, None, &mut conn).await;
nt.send_logout(&user, None, &conn).await;
save_result
}
@ -898,7 +905,7 @@ struct EmailTokenData {
}
#[post("/accounts/email-token", data = "<data>")]
async fn post_email_token(data: Json<EmailTokenData>, headers: Headers, mut conn: DbConn) -> EmptyResult {
async fn post_email_token(data: Json<EmailTokenData>, headers: Headers, conn: DbConn) -> EmptyResult {
if !CONFIG.email_change_allowed() {
err!("Email change is not allowed.");
}
@ -910,7 +917,7 @@ async fn post_email_token(data: Json<EmailTokenData>, headers: Headers, mut conn
err!("Invalid password")
}
if let Some(existing_user) = User::find_by_mail(&data.new_email, &mut conn).await {
if let Some(existing_user) = User::find_by_mail(&data.new_email, &conn).await {
if CONFIG.mail_enabled() {
// check if existing_user has already registered
if existing_user.password_hash.is_empty() {
@ -945,7 +952,7 @@ async fn post_email_token(data: Json<EmailTokenData>, headers: Headers, mut conn
user.email_new = Some(data.new_email);
user.email_new_token = Some(token);
user.save(&mut conn).await
user.save(&conn).await
}
#[derive(Deserialize)]
@ -960,7 +967,7 @@ struct ChangeEmailData {
}
#[post("/accounts/email", data = "<data>")]
async fn post_email(data: Json<ChangeEmailData>, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
async fn post_email(data: Json<ChangeEmailData>, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult {
if !CONFIG.email_change_allowed() {
err!("Email change is not allowed.");
}
@ -972,7 +979,7 @@ async fn post_email(data: Json<ChangeEmailData>, headers: Headers, mut conn: DbC
err!("Invalid password")
}
if User::find_by_mail(&data.new_email, &mut conn).await.is_some() {
if User::find_by_mail(&data.new_email, &conn).await.is_some() {
err!("Email already in use");
}
@ -1006,9 +1013,9 @@ async fn post_email(data: Json<ChangeEmailData>, headers: Headers, mut conn: DbC
user.set_password(&data.new_master_password_hash, Some(data.key), true, None);
let save_result = user.save(&mut conn).await;
let save_result = user.save(&conn).await;
nt.send_logout(&user, None, &mut conn).await;
nt.send_logout(&user, None, &conn).await;
save_result
}
@ -1036,10 +1043,10 @@ struct VerifyEmailTokenData {
}
#[post("/accounts/verify-email-token", data = "<data>")]
async fn post_verify_email_token(data: Json<VerifyEmailTokenData>, mut conn: DbConn) -> EmptyResult {
async fn post_verify_email_token(data: Json<VerifyEmailTokenData>, conn: DbConn) -> EmptyResult {
let data: VerifyEmailTokenData = data.into_inner();
let Some(mut user) = User::find_by_uuid(&data.user_id, &mut conn).await else {
let Some(mut user) = User::find_by_uuid(&data.user_id, &conn).await else {
err!("User doesn't exist")
};
@ -1052,7 +1059,7 @@ async fn post_verify_email_token(data: Json<VerifyEmailTokenData>, mut conn: DbC
user.verified_at = Some(Utc::now().naive_utc());
user.last_verifying_at = None;
user.login_verify_count = 0;
if let Err(e) = user.save(&mut conn).await {
if let Err(e) = user.save(&conn).await {
error!("Error saving email verification: {e:#?}");
}
@ -1066,11 +1073,11 @@ struct DeleteRecoverData {
}
#[post("/accounts/delete-recover", data = "<data>")]
async fn post_delete_recover(data: Json<DeleteRecoverData>, mut conn: DbConn) -> EmptyResult {
async fn post_delete_recover(data: Json<DeleteRecoverData>, conn: DbConn) -> EmptyResult {
let data: DeleteRecoverData = data.into_inner();
if CONFIG.mail_enabled() {
if let Some(user) = User::find_by_mail(&data.email, &mut conn).await {
if let Some(user) = User::find_by_mail(&data.email, &conn).await {
if let Err(e) = mail::send_delete_account(&user.email, &user.uuid).await {
error!("Error sending delete account email: {e:#?}");
}
@ -1093,21 +1100,21 @@ struct DeleteRecoverTokenData {
}
#[post("/accounts/delete-recover-token", data = "<data>")]
async fn post_delete_recover_token(data: Json<DeleteRecoverTokenData>, mut conn: DbConn) -> EmptyResult {
async fn post_delete_recover_token(data: Json<DeleteRecoverTokenData>, conn: DbConn) -> EmptyResult {
let data: DeleteRecoverTokenData = data.into_inner();
let Ok(claims) = decode_delete(&data.token) else {
err!("Invalid claim")
};
let Some(user) = User::find_by_uuid(&data.user_id, &mut conn).await else {
let Some(user) = User::find_by_uuid(&data.user_id, &conn).await else {
err!("User doesn't exist")
};
if claims.sub != *user.uuid {
err!("Invalid claim");
}
user.delete(&mut conn).await
user.delete(&conn).await
}
#[post("/accounts/delete", data = "<data>")]
@ -1116,13 +1123,13 @@ async fn post_delete_account(data: Json<PasswordOrOtpData>, headers: Headers, co
}
#[delete("/accounts", data = "<data>")]
async fn delete_account(data: Json<PasswordOrOtpData>, headers: Headers, mut conn: DbConn) -> EmptyResult {
async fn delete_account(data: Json<PasswordOrOtpData>, headers: Headers, conn: DbConn) -> EmptyResult {
let data: PasswordOrOtpData = data.into_inner();
let user = headers.user;
data.validate(&user, true, &mut conn).await?;
data.validate(&user, true, &conn).await?;
user.delete(&mut conn).await
user.delete(&conn).await
}
#[get("/accounts/revision-date")]
@ -1138,7 +1145,7 @@ struct PasswordHintData {
}
#[post("/accounts/password-hint", data = "<data>")]
async fn password_hint(data: Json<PasswordHintData>, mut conn: DbConn) -> EmptyResult {
async fn password_hint(data: Json<PasswordHintData>, conn: DbConn) -> EmptyResult {
if !CONFIG.password_hints_allowed() || (!CONFIG.mail_enabled() && !CONFIG.show_password_hint()) {
err!("This server is not configured to provide password hints.");
}
@ -1148,7 +1155,7 @@ async fn password_hint(data: Json<PasswordHintData>, mut conn: DbConn) -> EmptyR
let data: PasswordHintData = data.into_inner();
let email = &data.email;
match User::find_by_mail(email, &mut conn).await {
match User::find_by_mail(email, &conn).await {
None => {
// To prevent user enumeration, act as if the user exists.
if CONFIG.mail_enabled() {
@ -1190,10 +1197,10 @@ async fn prelogin(data: Json<PreloginData>, conn: DbConn) -> Json<Value> {
_prelogin(data, conn).await
}
pub async fn _prelogin(data: Json<PreloginData>, mut conn: DbConn) -> Json<Value> {
pub async fn _prelogin(data: Json<PreloginData>, conn: DbConn) -> Json<Value> {
let data: PreloginData = data.into_inner();
let (kdf_type, kdf_iter, kdf_mem, kdf_para) = match User::find_by_mail(&data.email, &mut conn).await {
let (kdf_type, kdf_iter, kdf_mem, kdf_para) = match User::find_by_mail(&data.email, &conn).await {
Some(user) => (user.client_kdf_type, user.client_kdf_iter, user.client_kdf_memory, user.client_kdf_parallelism),
None => (User::CLIENT_KDF_TYPE_DEFAULT, User::CLIENT_KDF_ITER_DEFAULT, None, None),
};
@ -1214,7 +1221,7 @@ struct SecretVerificationRequest {
}
// Change the KDF Iterations if necessary
pub async fn kdf_upgrade(user: &mut User, pwd_hash: &str, conn: &mut DbConn) -> ApiResult<()> {
pub async fn kdf_upgrade(user: &mut User, pwd_hash: &str, conn: &DbConn) -> ApiResult<()> {
if user.password_iterations < CONFIG.password_iterations() {
user.password_iterations = CONFIG.password_iterations();
user.set_password(pwd_hash, None, false, None);
@ -1227,7 +1234,7 @@ pub async fn kdf_upgrade(user: &mut User, pwd_hash: &str, conn: &mut DbConn) ->
}
#[post("/accounts/verify-password", data = "<data>")]
async fn verify_password(data: Json<SecretVerificationRequest>, headers: Headers, mut conn: DbConn) -> JsonResult {
async fn verify_password(data: Json<SecretVerificationRequest>, headers: Headers, conn: DbConn) -> JsonResult {
let data: SecretVerificationRequest = data.into_inner();
let mut user = headers.user;
@ -1235,22 +1242,22 @@ async fn verify_password(data: Json<SecretVerificationRequest>, headers: Headers
err!("Invalid password")
}
kdf_upgrade(&mut user, &data.master_password_hash, &mut conn).await?;
kdf_upgrade(&mut user, &data.master_password_hash, &conn).await?;
Ok(Json(master_password_policy(&user, &conn).await))
}
async fn _api_key(data: Json<PasswordOrOtpData>, rotate: bool, headers: Headers, mut conn: DbConn) -> JsonResult {
async fn _api_key(data: Json<PasswordOrOtpData>, rotate: bool, headers: Headers, conn: DbConn) -> JsonResult {
use crate::util::format_date;
let data: PasswordOrOtpData = data.into_inner();
let mut user = headers.user;
data.validate(&user, true, &mut conn).await?;
data.validate(&user, true, &conn).await?;
if rotate || user.api_key.is_none() {
user.api_key = Some(crypto::generate_api_key());
user.save(&mut conn).await.expect("Error saving API key");
user.save(&conn).await.expect("Error saving API key");
}
Ok(Json(json!({
@ -1271,11 +1278,12 @@ async fn rotate_api_key(data: Json<PasswordOrOtpData>, headers: Headers, conn: D
}
#[get("/devices/knowndevice")]
async fn get_known_device(device: KnownDevice, mut conn: DbConn) -> JsonResult {
let mut result = false;
if let Some(user) = User::find_by_mail(&device.email, &mut conn).await {
result = Device::find_by_uuid_and_user(&device.uuid, &user.uuid, &mut conn).await.is_some();
}
async fn get_known_device(device: KnownDevice, conn: DbConn) -> JsonResult {
let result = if let Some(user) = User::find_by_mail(&device.email, &conn).await {
Device::find_by_uuid_and_user(&device.uuid, &user.uuid, &conn).await.is_some()
} else {
false
};
Ok(Json(json!(result)))
}
@ -1317,8 +1325,8 @@ impl<'r> FromRequest<'r> for KnownDevice {
}
#[get("/devices")]
async fn get_all_devices(headers: Headers, mut conn: DbConn) -> JsonResult {
let devices = Device::find_with_auth_request_by_user(&headers.user.uuid, &mut conn).await;
async fn get_all_devices(headers: Headers, conn: DbConn) -> JsonResult {
let devices = Device::find_with_auth_request_by_user(&headers.user.uuid, &conn).await;
let devices = devices.iter().map(|device| device.to_json()).collect::<Vec<Value>>();
Ok(Json(json!({
@ -1329,8 +1337,8 @@ async fn get_all_devices(headers: Headers, mut conn: DbConn) -> JsonResult {
}
#[get("/devices/identifier/<device_id>")]
async fn get_device(device_id: DeviceId, headers: Headers, mut conn: DbConn) -> JsonResult {
let Some(device) = Device::find_by_uuid_and_user(&device_id, &headers.user.uuid, &mut conn).await else {
async fn get_device(device_id: DeviceId, headers: Headers, conn: DbConn) -> JsonResult {
let Some(device) = Device::find_by_uuid_and_user(&device_id, &headers.user.uuid, &conn).await else {
err!("No device found");
};
Ok(Json(device.to_json()))
@ -1348,17 +1356,11 @@ async fn post_device_token(device_id: DeviceId, data: Json<PushToken>, headers:
}
#[put("/devices/identifier/<device_id>/token", data = "<data>")]
async fn put_device_token(
device_id: DeviceId,
data: Json<PushToken>,
headers: Headers,
mut conn: DbConn,
) -> EmptyResult {
async fn put_device_token(device_id: DeviceId, data: Json<PushToken>, headers: Headers, conn: DbConn) -> EmptyResult {
let data = data.into_inner();
let token = data.push_token;
let Some(mut device) = Device::find_by_uuid_and_user(&headers.device.uuid, &headers.user.uuid, &mut conn).await
else {
let Some(mut device) = Device::find_by_uuid_and_user(&headers.device.uuid, &headers.user.uuid, &conn).await else {
err!(format!("Error: device {device_id} should be present before a token can be assigned"))
};
@ -1371,17 +1373,17 @@ async fn put_device_token(
}
device.push_token = Some(token);
if let Err(e) = device.save(&mut conn).await {
if let Err(e) = device.save(&conn).await {
err!(format!("An error occurred while trying to save the device push token: {e}"));
}
register_push_device(&mut device, &mut conn).await?;
register_push_device(&mut device, &conn).await?;
Ok(())
}
#[put("/devices/identifier/<device_id>/clear-token")]
async fn put_clear_device_token(device_id: DeviceId, mut conn: DbConn) -> EmptyResult {
async fn put_clear_device_token(device_id: DeviceId, conn: DbConn) -> EmptyResult {
// This only clears push token
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Controllers/DevicesController.cs#L215
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/Services/Implementations/DeviceService.cs#L37
@ -1393,8 +1395,8 @@ async fn put_clear_device_token(device_id: DeviceId, mut conn: DbConn) -> EmptyR
return Ok(());
}
if let Some(device) = Device::find_by_uuid(&device_id, &mut conn).await {
Device::clear_push_token_by_uuid(&device_id, &mut conn).await?;
if let Some(device) = Device::find_by_uuid(&device_id, &conn).await {
Device::clear_push_token_by_uuid(&device_id, &conn).await?;
unregister_push_device(&device.push_uuid).await?;
}
@ -1423,17 +1425,17 @@ struct AuthRequestRequest {
async fn post_auth_request(
data: Json<AuthRequestRequest>,
client_headers: ClientHeaders,
mut conn: DbConn,
conn: DbConn,
nt: Notify<'_>,
) -> JsonResult {
let data = data.into_inner();
let Some(user) = User::find_by_mail(&data.email, &mut conn).await else {
let Some(user) = User::find_by_mail(&data.email, &conn).await else {
err!("AuthRequest doesn't exist", "User not found")
};
// Validate device uuid and type
let device = match Device::find_by_uuid_and_user(&data.device_identifier, &user.uuid, &mut conn).await {
let device = match Device::find_by_uuid_and_user(&data.device_identifier, &user.uuid, &conn).await {
Some(device) if device.atype == client_headers.device_type => device,
_ => err!("AuthRequest doesn't exist", "Device verification failed"),
};
@ -1446,16 +1448,16 @@ async fn post_auth_request(
data.access_code,
data.public_key,
);
auth_request.save(&mut conn).await?;
auth_request.save(&conn).await?;
nt.send_auth_request(&user.uuid, &auth_request.uuid, &device, &mut conn).await;
nt.send_auth_request(&user.uuid, &auth_request.uuid, &device, &conn).await;
log_user_event(
EventType::UserRequestedDeviceApproval as i32,
&user.uuid,
client_headers.device_type,
&client_headers.ip.ip,
&mut conn,
&conn,
)
.await;
@ -1475,8 +1477,8 @@ async fn post_auth_request(
}
#[get("/auth-requests/<auth_request_id>")]
async fn get_auth_request(auth_request_id: AuthRequestId, headers: Headers, mut conn: DbConn) -> JsonResult {
let Some(auth_request) = AuthRequest::find_by_uuid_and_user(&auth_request_id, &headers.user.uuid, &mut conn).await
async fn get_auth_request(auth_request_id: AuthRequestId, headers: Headers, conn: DbConn) -> JsonResult {
let Some(auth_request) = AuthRequest::find_by_uuid_and_user(&auth_request_id, &headers.user.uuid, &conn).await
else {
err!("AuthRequest doesn't exist", "Record not found or user uuid does not match")
};
@ -1512,13 +1514,12 @@ async fn put_auth_request(
auth_request_id: AuthRequestId,
data: Json<AuthResponseRequest>,
headers: Headers,
mut conn: DbConn,
conn: DbConn,
ant: AnonymousNotify<'_>,
nt: Notify<'_>,
) -> JsonResult {
let data = data.into_inner();
let Some(mut auth_request) =
AuthRequest::find_by_uuid_and_user(&auth_request_id, &headers.user.uuid, &mut conn).await
let Some(mut auth_request) = AuthRequest::find_by_uuid_and_user(&auth_request_id, &headers.user.uuid, &conn).await
else {
err!("AuthRequest doesn't exist", "Record not found or user uuid does not match")
};
@ -1540,28 +1541,28 @@ async fn put_auth_request(
auth_request.master_password_hash = data.master_password_hash;
auth_request.response_device_id = Some(data.device_identifier.clone());
auth_request.response_date = Some(response_date);
auth_request.save(&mut conn).await?;
auth_request.save(&conn).await?;
ant.send_auth_response(&auth_request.user_uuid, &auth_request.uuid).await;
nt.send_auth_response(&auth_request.user_uuid, &auth_request.uuid, &headers.device, &mut conn).await;
nt.send_auth_response(&auth_request.user_uuid, &auth_request.uuid, &headers.device, &conn).await;
log_user_event(
EventType::OrganizationUserApprovedAuthRequest as i32,
&headers.user.uuid,
headers.device.atype,
&headers.ip.ip,
&mut conn,
&conn,
)
.await;
} else {
// If denied, there's no reason to keep the request
auth_request.delete(&mut conn).await?;
auth_request.delete(&conn).await?;
log_user_event(
EventType::OrganizationUserRejectedAuthRequest as i32,
&headers.user.uuid,
headers.device.atype,
&headers.ip.ip,
&mut conn,
&conn,
)
.await;
}
@ -1586,9 +1587,9 @@ async fn get_auth_request_response(
auth_request_id: AuthRequestId,
code: &str,
client_headers: ClientHeaders,
mut conn: DbConn,
conn: DbConn,
) -> JsonResult {
let Some(auth_request) = AuthRequest::find_by_uuid(&auth_request_id, &mut conn).await else {
let Some(auth_request) = AuthRequest::find_by_uuid(&auth_request_id, &conn).await else {
err!("AuthRequest doesn't exist", "User not found")
};
@ -1624,8 +1625,8 @@ async fn get_auth_requests(headers: Headers, conn: DbConn) -> JsonResult {
}
#[get("/auth-requests/pending")]
async fn get_auth_requests_pending(headers: Headers, mut conn: DbConn) -> JsonResult {
let auth_requests = AuthRequest::find_by_user(&headers.user.uuid, &mut conn).await;
async fn get_auth_requests_pending(headers: Headers, conn: DbConn) -> JsonResult {
let auth_requests = AuthRequest::find_by_user(&headers.user.uuid, &conn).await;
Ok(Json(json!({
"data": auth_requests
@ -1655,8 +1656,8 @@ async fn get_auth_requests_pending(headers: Headers, mut conn: DbConn) -> JsonRe
pub async fn purge_auth_requests(pool: DbPool) {
debug!("Purging auth requests");
if let Ok(mut conn) = pool.get().await {
AuthRequest::purge_expired_auth_requests(&mut conn).await;
if let Ok(conn) = pool.get().await {
AuthRequest::purge_expired_auth_requests(&conn).await;
} else {
error!("Failed to get DB connection while purging trashed ciphers")
}

347
src/api/core/ciphers.rs

@ -17,7 +17,14 @@ use crate::{
auth::Headers,
config::PathType,
crypto,
db::{models::*, DbConn, DbPool},
db::{
models::{
Attachment, AttachmentId, Cipher, CipherId, Collection, CollectionCipher, CollectionGroup, CollectionId,
CollectionUser, EventType, Favorite, Folder, FolderCipher, FolderId, Group, Membership, MembershipType,
OrgPolicy, OrgPolicyType, OrganizationId, RepromptType, Send, UserId,
},
DbConn, DbPool,
},
CONFIG,
};
@ -93,8 +100,8 @@ pub fn routes() -> Vec<Route> {
pub async fn purge_trashed_ciphers(pool: DbPool) {
debug!("Purging trashed ciphers");
if let Ok(mut conn) = pool.get().await {
Cipher::purge_trash(&mut conn).await;
if let Ok(conn) = pool.get().await {
Cipher::purge_trash(&conn).await;
} else {
error!("Failed to get DB connection while purging trashed ciphers")
}
@ -107,11 +114,11 @@ struct SyncData {
}
#[get("/sync?<data..>")]
async fn sync(data: SyncData, headers: Headers, client_version: Option<ClientVersion>, mut conn: DbConn) -> JsonResult {
let user_json = headers.user.to_json(&mut conn).await;
async fn sync(data: SyncData, headers: Headers, client_version: Option<ClientVersion>, conn: DbConn) -> JsonResult {
let user_json = headers.user.to_json(&conn).await;
// Get all ciphers which are visible by the user
let mut ciphers = Cipher::find_by_user_visible(&headers.user.uuid, &mut conn).await;
let mut ciphers = Cipher::find_by_user_visible(&headers.user.uuid, &conn).await;
// Filter out SSH keys if the client version is less than 2024.12.0
let show_ssh_keys = if let Some(client_version) = client_version {
@ -124,31 +131,30 @@ async fn sync(data: SyncData, headers: Headers, client_version: Option<ClientVer
ciphers.retain(|c| c.atype != 5);
}
let cipher_sync_data = CipherSyncData::new(&headers.user.uuid, CipherSyncType::User, &mut conn).await;
let cipher_sync_data = CipherSyncData::new(&headers.user.uuid, CipherSyncType::User, &conn).await;
// Lets generate the ciphers_json using all the gathered info
let mut ciphers_json = Vec::with_capacity(ciphers.len());
for c in ciphers {
ciphers_json.push(
c.to_json(&headers.host, &headers.user.uuid, Some(&cipher_sync_data), CipherSyncType::User, &mut conn)
.await?,
c.to_json(&headers.host, &headers.user.uuid, Some(&cipher_sync_data), CipherSyncType::User, &conn).await?,
);
}
let collections = Collection::find_by_user_uuid(headers.user.uuid.clone(), &mut conn).await;
let collections = Collection::find_by_user_uuid(headers.user.uuid.clone(), &conn).await;
let mut collections_json = Vec::with_capacity(collections.len());
for c in collections {
collections_json.push(c.to_json_details(&headers.user.uuid, Some(&cipher_sync_data), &mut conn).await);
collections_json.push(c.to_json_details(&headers.user.uuid, Some(&cipher_sync_data), &conn).await);
}
let folders_json: Vec<Value> =
Folder::find_by_user(&headers.user.uuid, &mut conn).await.iter().map(Folder::to_json).collect();
Folder::find_by_user(&headers.user.uuid, &conn).await.iter().map(Folder::to_json).collect();
let sends_json: Vec<Value> =
Send::find_by_user(&headers.user.uuid, &mut conn).await.iter().map(Send::to_json).collect();
Send::find_by_user(&headers.user.uuid, &conn).await.iter().map(Send::to_json).collect();
let policies_json: Vec<Value> =
OrgPolicy::find_confirmed_by_user(&headers.user.uuid, &mut conn).await.iter().map(OrgPolicy::to_json).collect();
OrgPolicy::find_confirmed_by_user(&headers.user.uuid, &conn).await.iter().map(OrgPolicy::to_json).collect();
let domains_json = if data.exclude_domains {
Value::Null
@ -169,15 +175,14 @@ async fn sync(data: SyncData, headers: Headers, client_version: Option<ClientVer
}
#[get("/ciphers")]
async fn get_ciphers(headers: Headers, mut conn: DbConn) -> JsonResult {
let ciphers = Cipher::find_by_user_visible(&headers.user.uuid, &mut conn).await;
let cipher_sync_data = CipherSyncData::new(&headers.user.uuid, CipherSyncType::User, &mut conn).await;
async fn get_ciphers(headers: Headers, conn: DbConn) -> JsonResult {
let ciphers = Cipher::find_by_user_visible(&headers.user.uuid, &conn).await;
let cipher_sync_data = CipherSyncData::new(&headers.user.uuid, CipherSyncType::User, &conn).await;
let mut ciphers_json = Vec::with_capacity(ciphers.len());
for c in ciphers {
ciphers_json.push(
c.to_json(&headers.host, &headers.user.uuid, Some(&cipher_sync_data), CipherSyncType::User, &mut conn)
.await?,
c.to_json(&headers.host, &headers.user.uuid, Some(&cipher_sync_data), CipherSyncType::User, &conn).await?,
);
}
@ -189,16 +194,16 @@ async fn get_ciphers(headers: Headers, mut conn: DbConn) -> JsonResult {
}
#[get("/ciphers/<cipher_id>")]
async fn get_cipher(cipher_id: CipherId, headers: Headers, mut conn: DbConn) -> JsonResult {
let Some(cipher) = Cipher::find_by_uuid(&cipher_id, &mut conn).await else {
async fn get_cipher(cipher_id: CipherId, headers: Headers, conn: DbConn) -> 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, &mut conn).await {
if !cipher.is_accessible_to_user(&headers.user.uuid, &conn).await {
err!("Cipher is not owned by user")
}
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await?))
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &conn).await?))
}
#[get("/ciphers/<cipher_id>/admin")]
@ -291,7 +296,7 @@ async fn post_ciphers_admin(data: Json<ShareCipherData>, headers: Headers, conn:
async fn post_ciphers_create(
data: Json<ShareCipherData>,
headers: Headers,
mut conn: DbConn,
conn: DbConn,
nt: Notify<'_>,
) -> JsonResult {
let mut data: ShareCipherData = data.into_inner();
@ -305,11 +310,11 @@ async fn post_ciphers_create(
// This check is usually only needed in update_cipher_from_data(), but we
// need it here as well to avoid creating an empty cipher in the call to
// cipher.save() below.
enforce_personal_ownership_policy(Some(&data.cipher), &headers, &mut conn).await?;
enforce_personal_ownership_policy(Some(&data.cipher), &headers, &conn).await?;
let mut cipher = Cipher::new(data.cipher.r#type, data.cipher.name.clone());
cipher.user_uuid = Some(headers.user.uuid.clone());
cipher.save(&mut conn).await?;
cipher.save(&conn).await?;
// When cloning a cipher, the Bitwarden clients seem to set this field
// based on the cipher being cloned (when creating a new cipher, it's set
@ -319,12 +324,12 @@ async fn post_ciphers_create(
// or otherwise), we can just ignore this field entirely.
data.cipher.last_known_revision_date = None;
share_cipher_by_uuid(&cipher.uuid, data, &headers, &mut conn, &nt, None).await
share_cipher_by_uuid(&cipher.uuid, data, &headers, &conn, &nt, None).await
}
/// Called when creating a new user-owned cipher.
#[post("/ciphers", data = "<data>")]
async fn post_ciphers(data: Json<CipherData>, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult {
async fn post_ciphers(data: Json<CipherData>, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult {
let mut data: CipherData = data.into_inner();
// The web/browser clients set this field to null as expected, but the
@ -334,9 +339,9 @@ async fn post_ciphers(data: Json<CipherData>, headers: Headers, mut conn: DbConn
data.last_known_revision_date = None;
let mut cipher = Cipher::new(data.r#type, data.name.clone());
update_cipher_from_data(&mut cipher, data, &headers, None, &mut conn, &nt, UpdateType::SyncCipherCreate).await?;
update_cipher_from_data(&mut cipher, data, &headers, None, &conn, &nt, UpdateType::SyncCipherCreate).await?;
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await?))
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &conn).await?))
}
/// Enforces the personal ownership policy on user-owned ciphers, if applicable.
@ -346,11 +351,7 @@ async fn post_ciphers(data: Json<CipherData>, headers: Headers, mut conn: DbConn
/// allowed to delete or share such ciphers to an org, however.
///
/// Ref: https://bitwarden.com/help/article/policies/#personal-ownership
async fn enforce_personal_ownership_policy(
data: Option<&CipherData>,
headers: &Headers,
conn: &mut DbConn,
) -> EmptyResult {
async fn enforce_personal_ownership_policy(data: Option<&CipherData>, headers: &Headers, conn: &DbConn) -> EmptyResult {
if data.is_none() || data.unwrap().organization_id.is_none() {
let user_id = &headers.user.uuid;
let policy_type = OrgPolicyType::PersonalOwnership;
@ -366,7 +367,7 @@ pub async fn update_cipher_from_data(
data: CipherData,
headers: &Headers,
shared_to_collections: Option<Vec<CollectionId>>,
conn: &mut DbConn,
conn: &DbConn,
nt: &Notify<'_>,
ut: UpdateType,
) -> EmptyResult {
@ -559,13 +560,8 @@ struct RelationsData {
}
#[post("/ciphers/import", data = "<data>")]
async fn post_ciphers_import(
data: Json<ImportData>,
headers: Headers,
mut conn: DbConn,
nt: Notify<'_>,
) -> EmptyResult {
enforce_personal_ownership_policy(None, &headers, &mut conn).await?;
async fn post_ciphers_import(data: Json<ImportData>, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult {
enforce_personal_ownership_policy(None, &headers, &conn).await?;
let data: ImportData = data.into_inner();
@ -577,14 +573,14 @@ async fn post_ciphers_import(
// Read and create the folders
let existing_folders: HashSet<Option<FolderId>> =
Folder::find_by_user(&headers.user.uuid, &mut conn).await.into_iter().map(|f| Some(f.uuid)).collect();
Folder::find_by_user(&headers.user.uuid, &conn).await.into_iter().map(|f| Some(f.uuid)).collect();
let mut folders: Vec<FolderId> = Vec::with_capacity(data.folders.len());
for folder in data.folders.into_iter() {
let folder_id = if existing_folders.contains(&folder.id) {
folder.id.unwrap()
} else {
let mut new_folder = Folder::new(headers.user.uuid.clone(), folder.name);
new_folder.save(&mut conn).await?;
new_folder.save(&conn).await?;
new_folder.uuid
};
@ -604,12 +600,12 @@ async fn post_ciphers_import(
cipher_data.folder_id = folder_id;
let mut cipher = Cipher::new(cipher_data.r#type, cipher_data.name.clone());
update_cipher_from_data(&mut cipher, cipher_data, &headers, None, &mut conn, &nt, UpdateType::None).await?;
update_cipher_from_data(&mut cipher, cipher_data, &headers, None, &conn, &nt, UpdateType::None).await?;
}
let mut user = headers.user;
user.update_revision(&mut conn).await?;
nt.send_user_update(UpdateType::SyncVault, &user, &headers.device.push_uuid, &mut conn).await;
user.update_revision(&conn).await?;
nt.send_user_update(UpdateType::SyncVault, &user, &headers.device.push_uuid, &conn).await;
Ok(())
}
@ -653,12 +649,12 @@ async fn put_cipher(
cipher_id: CipherId,
data: Json<CipherData>,
headers: Headers,
mut conn: DbConn,
conn: DbConn,
nt: Notify<'_>,
) -> JsonResult {
let data: CipherData = data.into_inner();
let Some(mut cipher) = Cipher::find_by_uuid(&cipher_id, &mut conn).await else {
let Some(mut cipher) = Cipher::find_by_uuid(&cipher_id, &conn).await else {
err!("Cipher doesn't exist")
};
@ -667,13 +663,13 @@ async fn put_cipher(
// cipher itself, so the user shouldn't need write access to change these.
// Interestingly, upstream Bitwarden doesn't properly handle this either.
if !cipher.is_write_accessible_to_user(&headers.user.uuid, &mut conn).await {
if !cipher.is_write_accessible_to_user(&headers.user.uuid, &conn).await {
err!("Cipher is not write accessible")
}
update_cipher_from_data(&mut cipher, data, &headers, None, &mut conn, &nt, UpdateType::SyncCipherUpdate).await?;
update_cipher_from_data(&mut cipher, data, &headers, None, &conn, &nt, UpdateType::SyncCipherUpdate).await?;
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await?))
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &conn).await?))
}
#[post("/ciphers/<cipher_id>/partial", data = "<data>")]
@ -692,26 +688,26 @@ async fn put_cipher_partial(
cipher_id: CipherId,
data: Json<PartialCipherData>,
headers: Headers,
mut conn: DbConn,
conn: DbConn,
) -> JsonResult {
let data: PartialCipherData = data.into_inner();
let Some(cipher) = Cipher::find_by_uuid(&cipher_id, &mut conn).await else {
let Some(cipher) = Cipher::find_by_uuid(&cipher_id, &conn).await else {
err!("Cipher doesn't exist")
};
if let Some(ref folder_id) = data.folder_id {
if Folder::find_by_uuid_and_user(folder_id, &headers.user.uuid, &mut 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");
}
}
// Move cipher
cipher.move_to_folder(data.folder_id.clone(), &headers.user.uuid, &mut conn).await?;
cipher.move_to_folder(data.folder_id.clone(), &headers.user.uuid, &conn).await?;
// Update favorite
cipher.set_favorite(Some(data.favorite), &headers.user.uuid, &mut conn).await?;
cipher.set_favorite(Some(data.favorite), &headers.user.uuid, &conn).await?;
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await?))
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &conn).await?))
}
#[derive(Deserialize)]
@ -764,35 +760,34 @@ async fn post_collections_update(
cipher_id: CipherId,
data: Json<CollectionsAdminData>,
headers: Headers,
mut conn: DbConn,
conn: DbConn,
nt: Notify<'_>,
) -> JsonResult {
let data: CollectionsAdminData = data.into_inner();
let Some(cipher) = Cipher::find_by_uuid(&cipher_id, &mut conn).await else {
let Some(cipher) = Cipher::find_by_uuid(&cipher_id, &conn).await else {
err!("Cipher doesn't exist")
};
if !cipher.is_in_editable_collection_by_user(&headers.user.uuid, &mut conn).await {
if !cipher.is_in_editable_collection_by_user(&headers.user.uuid, &conn).await {
err!("Collection cannot be changed")
}
let posted_collections = HashSet::<CollectionId>::from_iter(data.collection_ids);
let current_collections =
HashSet::<CollectionId>::from_iter(cipher.get_collections(headers.user.uuid.clone(), &mut conn).await);
HashSet::<CollectionId>::from_iter(cipher.get_collections(headers.user.uuid.clone(), &conn).await);
for collection in posted_collections.symmetric_difference(&current_collections) {
match Collection::find_by_uuid_and_org(collection, cipher.organization_uuid.as_ref().unwrap(), &mut conn).await
{
match Collection::find_by_uuid_and_org(collection, cipher.organization_uuid.as_ref().unwrap(), &conn).await {
None => err!("Invalid collection ID provided"),
Some(collection) => {
if collection.is_writable_by_user(&headers.user.uuid, &mut conn).await {
if collection.is_writable_by_user(&headers.user.uuid, &conn).await {
if posted_collections.contains(&collection.uuid) {
// Add to collection
CollectionCipher::save(&cipher.uuid, &collection.uuid, &mut conn).await?;
CollectionCipher::save(&cipher.uuid, &collection.uuid, &conn).await?;
} else {
// Remove from collection
CollectionCipher::delete(&cipher.uuid, &collection.uuid, &mut conn).await?;
CollectionCipher::delete(&cipher.uuid, &collection.uuid, &conn).await?;
}
} else {
err!("No rights to modify the collection")
@ -804,10 +799,10 @@ async fn post_collections_update(
nt.send_cipher_update(
UpdateType::SyncCipherUpdate,
&cipher,
&cipher.update_users_revision(&mut conn).await,
&cipher.update_users_revision(&conn).await,
&headers.device,
Some(Vec::from_iter(posted_collections)),
&mut conn,
&conn,
)
.await;
@ -818,11 +813,11 @@ async fn post_collections_update(
&headers.user.uuid,
headers.device.atype,
&headers.ip.ip,
&mut conn,
&conn,
)
.await;
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await?))
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &conn).await?))
}
#[put("/ciphers/<cipher_id>/collections-admin", data = "<data>")]
@ -841,35 +836,34 @@ async fn post_collections_admin(
cipher_id: CipherId,
data: Json<CollectionsAdminData>,
headers: Headers,
mut conn: DbConn,
conn: DbConn,
nt: Notify<'_>,
) -> EmptyResult {
let data: CollectionsAdminData = data.into_inner();
let Some(cipher) = Cipher::find_by_uuid(&cipher_id, &mut conn).await else {
let Some(cipher) = Cipher::find_by_uuid(&cipher_id, &conn).await else {
err!("Cipher doesn't exist")
};
if !cipher.is_in_editable_collection_by_user(&headers.user.uuid, &mut conn).await {
if !cipher.is_in_editable_collection_by_user(&headers.user.uuid, &conn).await {
err!("Collection cannot be changed")
}
let posted_collections = HashSet::<CollectionId>::from_iter(data.collection_ids);
let current_collections =
HashSet::<CollectionId>::from_iter(cipher.get_admin_collections(headers.user.uuid.clone(), &mut conn).await);
HashSet::<CollectionId>::from_iter(cipher.get_admin_collections(headers.user.uuid.clone(), &conn).await);
for collection in posted_collections.symmetric_difference(&current_collections) {
match Collection::find_by_uuid_and_org(collection, cipher.organization_uuid.as_ref().unwrap(), &mut conn).await
{
match Collection::find_by_uuid_and_org(collection, cipher.organization_uuid.as_ref().unwrap(), &conn).await {
None => err!("Invalid collection ID provided"),
Some(collection) => {
if collection.is_writable_by_user(&headers.user.uuid, &mut conn).await {
if collection.is_writable_by_user(&headers.user.uuid, &conn).await {
if posted_collections.contains(&collection.uuid) {
// Add to collection
CollectionCipher::save(&cipher.uuid, &collection.uuid, &mut conn).await?;
CollectionCipher::save(&cipher.uuid, &collection.uuid, &conn).await?;
} else {
// Remove from collection
CollectionCipher::delete(&cipher.uuid, &collection.uuid, &mut conn).await?;
CollectionCipher::delete(&cipher.uuid, &collection.uuid, &conn).await?;
}
} else {
err!("No rights to modify the collection")
@ -881,10 +875,10 @@ async fn post_collections_admin(
nt.send_cipher_update(
UpdateType::SyncCipherUpdate,
&cipher,
&cipher.update_users_revision(&mut conn).await,
&cipher.update_users_revision(&conn).await,
&headers.device,
Some(Vec::from_iter(posted_collections)),
&mut conn,
&conn,
)
.await;
@ -895,7 +889,7 @@ async fn post_collections_admin(
&headers.user.uuid,
headers.device.atype,
&headers.ip.ip,
&mut conn,
&conn,
)
.await;
@ -916,12 +910,12 @@ async fn post_cipher_share(
cipher_id: CipherId,
data: Json<ShareCipherData>,
headers: Headers,
mut conn: DbConn,
conn: DbConn,
nt: Notify<'_>,
) -> JsonResult {
let data: ShareCipherData = data.into_inner();
share_cipher_by_uuid(&cipher_id, data, &headers, &mut conn, &nt, None).await
share_cipher_by_uuid(&cipher_id, data, &headers, &conn, &nt, None).await
}
#[put("/ciphers/<cipher_id>/share", data = "<data>")]
@ -929,12 +923,12 @@ async fn put_cipher_share(
cipher_id: CipherId,
data: Json<ShareCipherData>,
headers: Headers,
mut conn: DbConn,
conn: DbConn,
nt: Notify<'_>,
) -> JsonResult {
let data: ShareCipherData = data.into_inner();
share_cipher_by_uuid(&cipher_id, data, &headers, &mut conn, &nt, None).await
share_cipher_by_uuid(&cipher_id, data, &headers, &conn, &nt, None).await
}
#[derive(Deserialize)]
@ -948,7 +942,7 @@ struct ShareSelectedCipherData {
async fn put_cipher_share_selected(
data: Json<ShareSelectedCipherData>,
headers: Headers,
mut conn: DbConn,
conn: DbConn,
nt: Notify<'_>,
) -> EmptyResult {
let mut data: ShareSelectedCipherData = data.into_inner();
@ -975,14 +969,14 @@ async fn put_cipher_share_selected(
match shared_cipher_data.cipher.id.take() {
Some(id) => {
share_cipher_by_uuid(&id, shared_cipher_data, &headers, &mut conn, &nt, Some(UpdateType::None)).await?
share_cipher_by_uuid(&id, shared_cipher_data, &headers, &conn, &nt, Some(UpdateType::None)).await?
}
None => err!("Request missing ids field"),
};
}
// 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, &mut conn).await;
nt.send_user_update(UpdateType::SyncCiphers, &headers.user, &headers.device.push_uuid, &conn).await;
Ok(())
}
@ -991,7 +985,7 @@ async fn share_cipher_by_uuid(
cipher_id: &CipherId,
data: ShareCipherData,
headers: &Headers,
conn: &mut DbConn,
conn: &DbConn,
nt: &Notify<'_>,
override_ut: Option<UpdateType>,
) -> JsonResult {
@ -1050,17 +1044,17 @@ async fn get_attachment(
cipher_id: CipherId,
attachment_id: AttachmentId,
headers: Headers,
mut conn: DbConn,
conn: DbConn,
) -> JsonResult {
let Some(cipher) = Cipher::find_by_uuid(&cipher_id, &mut conn).await else {
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, &mut conn).await {
if !cipher.is_accessible_to_user(&headers.user.uuid, &conn).await {
err!("Cipher is not accessible")
}
match Attachment::find_by_id(&attachment_id, &mut conn).await {
match Attachment::find_by_id(&attachment_id, &conn).await {
Some(attachment) if cipher_id == attachment.cipher_uuid => Ok(Json(attachment.to_json(&headers.host).await?)),
Some(_) => err!("Attachment doesn't belong to cipher"),
None => err!("Attachment doesn't exist"),
@ -1090,13 +1084,13 @@ async fn post_attachment_v2(
cipher_id: CipherId,
data: Json<AttachmentRequestData>,
headers: Headers,
mut conn: DbConn,
conn: DbConn,
) -> JsonResult {
let Some(cipher) = Cipher::find_by_uuid(&cipher_id, &mut conn).await else {
let Some(cipher) = Cipher::find_by_uuid(&cipher_id, &conn).await else {
err!("Cipher doesn't exist")
};
if !cipher.is_write_accessible_to_user(&headers.user.uuid, &mut conn).await {
if !cipher.is_write_accessible_to_user(&headers.user.uuid, &conn).await {
err!("Cipher is not write accessible")
}
@ -1109,7 +1103,7 @@ async fn post_attachment_v2(
let attachment_id = crypto::generate_attachment_id();
let attachment =
Attachment::new(attachment_id.clone(), cipher.uuid.clone(), data.file_name, file_size, Some(data.key));
attachment.save(&mut conn).await.expect("Error saving attachment");
attachment.save(&conn).await.expect("Error saving attachment");
let url = format!("/ciphers/{}/attachment/{attachment_id}", cipher.uuid);
let response_key = match data.admin_request {
@ -1122,7 +1116,7 @@ async fn post_attachment_v2(
"attachmentId": attachment_id,
"url": url,
"fileUploadType": FileUploadType::Direct as i32,
response_key: cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await?,
response_key: cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &conn).await?,
})))
}
@ -1145,7 +1139,7 @@ async fn save_attachment(
cipher_id: CipherId,
data: Form<UploadData<'_>>,
headers: &Headers,
mut conn: DbConn,
conn: DbConn,
nt: Notify<'_>,
) -> Result<(Cipher, DbConn), crate::error::Error> {
let data = data.into_inner();
@ -1157,11 +1151,11 @@ async fn save_attachment(
err!("Attachment size can't be negative")
}
let Some(cipher) = Cipher::find_by_uuid(&cipher_id, &mut conn).await else {
let Some(cipher) = Cipher::find_by_uuid(&cipher_id, &conn).await else {
err!("Cipher doesn't exist")
};
if !cipher.is_write_accessible_to_user(&headers.user.uuid, &mut conn).await {
if !cipher.is_write_accessible_to_user(&headers.user.uuid, &conn).await {
err!("Cipher is not write accessible")
}
@ -1176,7 +1170,7 @@ async fn save_attachment(
match CONFIG.user_attachment_limit() {
Some(0) => err!("Attachments are disabled"),
Some(limit_kb) => {
let already_used = Attachment::size_by_user(user_id, &mut conn).await;
let already_used = Attachment::size_by_user(user_id, &conn).await;
let left = limit_kb
.checked_mul(1024)
.and_then(|l| l.checked_sub(already_used))
@ -1198,7 +1192,7 @@ async fn save_attachment(
match CONFIG.org_attachment_limit() {
Some(0) => err!("Attachments are disabled"),
Some(limit_kb) => {
let already_used = Attachment::size_by_org(org_id, &mut conn).await;
let already_used = Attachment::size_by_org(org_id, &conn).await;
let left = limit_kb
.checked_mul(1024)
.and_then(|l| l.checked_sub(already_used))
@ -1249,10 +1243,10 @@ async fn save_attachment(
if size != attachment.file_size {
// Update the attachment with the actual file size.
attachment.file_size = size;
attachment.save(&mut conn).await.expect("Error updating attachment");
attachment.save(&conn).await.expect("Error updating attachment");
}
} else {
attachment.delete(&mut conn).await.ok();
attachment.delete(&conn).await.ok();
err!(format!("Attachment size mismatch (expected within [{min_size}, {max_size}], got {size})"));
}
@ -1272,18 +1266,18 @@ async fn save_attachment(
}
let attachment =
Attachment::new(file_id.clone(), cipher_id.clone(), encrypted_filename.unwrap(), size, data.key);
attachment.save(&mut conn).await.expect("Error saving attachment");
attachment.save(&conn).await.expect("Error saving attachment");
}
save_temp_file(PathType::Attachments, &format!("{cipher_id}/{file_id}"), data.data, true).await?;
save_temp_file(&PathType::Attachments, &format!("{cipher_id}/{file_id}"), data.data, true).await?;
nt.send_cipher_update(
UpdateType::SyncCipherUpdate,
&cipher,
&cipher.update_users_revision(&mut conn).await,
&cipher.update_users_revision(&conn).await,
&headers.device,
None,
&mut conn,
&conn,
)
.await;
@ -1295,7 +1289,7 @@ async fn save_attachment(
&headers.user.uuid,
headers.device.atype,
&headers.ip.ip,
&mut conn,
&conn,
)
.await;
}
@ -1313,10 +1307,10 @@ async fn post_attachment_v2_data(
attachment_id: AttachmentId,
data: Form<UploadData<'_>>,
headers: Headers,
mut conn: DbConn,
conn: DbConn,
nt: Notify<'_>,
) -> EmptyResult {
let attachment = match Attachment::find_by_id(&attachment_id, &mut conn).await {
let attachment = match Attachment::find_by_id(&attachment_id, &conn).await {
Some(attachment) if cipher_id == attachment.cipher_uuid => Some(attachment),
Some(_) => err!("Attachment doesn't belong to cipher"),
None => err!("Attachment doesn't exist"),
@ -1340,9 +1334,9 @@ async fn post_attachment(
// the attachment database record as well as saving the data to disk.
let attachment = None;
let (cipher, mut conn) = save_attachment(attachment, cipher_id, data, &headers, conn, nt).await?;
let (cipher, conn) = save_attachment(attachment, cipher_id, data, &headers, conn, nt).await?;
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await?))
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &conn).await?))
}
#[post("/ciphers/<cipher_id>/attachment-admin", format = "multipart/form-data", data = "<data>")]
@ -1362,10 +1356,10 @@ async fn post_attachment_share(
attachment_id: AttachmentId,
data: Form<UploadData<'_>>,
headers: Headers,
mut conn: DbConn,
conn: DbConn,
nt: Notify<'_>,
) -> JsonResult {
_delete_cipher_attachment_by_id(&cipher_id, &attachment_id, &headers, &mut conn, &nt).await?;
_delete_cipher_attachment_by_id(&cipher_id, &attachment_id, &headers, &conn, &nt).await?;
post_attachment(cipher_id, data, headers, conn, nt).await
}
@ -1396,10 +1390,10 @@ async fn delete_attachment(
cipher_id: CipherId,
attachment_id: AttachmentId,
headers: Headers,
mut conn: DbConn,
conn: DbConn,
nt: Notify<'_>,
) -> JsonResult {
_delete_cipher_attachment_by_id(&cipher_id, &attachment_id, &headers, &mut conn, &nt).await
_delete_cipher_attachment_by_id(&cipher_id, &attachment_id, &headers, &conn, &nt).await
}
#[delete("/ciphers/<cipher_id>/attachment/<attachment_id>/admin")]
@ -1407,55 +1401,45 @@ async fn delete_attachment_admin(
cipher_id: CipherId,
attachment_id: AttachmentId,
headers: Headers,
mut conn: DbConn,
conn: DbConn,
nt: Notify<'_>,
) -> JsonResult {
_delete_cipher_attachment_by_id(&cipher_id, &attachment_id, &headers, &mut conn, &nt).await
_delete_cipher_attachment_by_id(&cipher_id, &attachment_id, &headers, &conn, &nt).await
}
#[post("/ciphers/<cipher_id>/delete")]
async fn delete_cipher_post(cipher_id: CipherId, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
_delete_cipher_by_uuid(&cipher_id, &headers, &mut conn, &CipherDeleteOptions::HardSingle, &nt).await
async fn delete_cipher_post(cipher_id: CipherId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult {
_delete_cipher_by_uuid(&cipher_id, &headers, &conn, &CipherDeleteOptions::HardSingle, &nt).await
// permanent delete
}
#[post("/ciphers/<cipher_id>/delete-admin")]
async fn delete_cipher_post_admin(
cipher_id: CipherId,
headers: Headers,
mut conn: DbConn,
nt: Notify<'_>,
) -> EmptyResult {
_delete_cipher_by_uuid(&cipher_id, &headers, &mut conn, &CipherDeleteOptions::HardSingle, &nt).await
async fn delete_cipher_post_admin(cipher_id: CipherId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult {
_delete_cipher_by_uuid(&cipher_id, &headers, &conn, &CipherDeleteOptions::HardSingle, &nt).await
// permanent delete
}
#[put("/ciphers/<cipher_id>/delete")]
async fn delete_cipher_put(cipher_id: CipherId, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
_delete_cipher_by_uuid(&cipher_id, &headers, &mut conn, &CipherDeleteOptions::SoftSingle, &nt).await
async fn delete_cipher_put(cipher_id: CipherId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult {
_delete_cipher_by_uuid(&cipher_id, &headers, &conn, &CipherDeleteOptions::SoftSingle, &nt).await
// soft delete
}
#[put("/ciphers/<cipher_id>/delete-admin")]
async fn delete_cipher_put_admin(
cipher_id: CipherId,
headers: Headers,
mut conn: DbConn,
nt: Notify<'_>,
) -> EmptyResult {
_delete_cipher_by_uuid(&cipher_id, &headers, &mut conn, &CipherDeleteOptions::SoftSingle, &nt).await
async fn delete_cipher_put_admin(cipher_id: CipherId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult {
_delete_cipher_by_uuid(&cipher_id, &headers, &conn, &CipherDeleteOptions::SoftSingle, &nt).await
// soft delete
}
#[delete("/ciphers/<cipher_id>")]
async fn delete_cipher(cipher_id: CipherId, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
_delete_cipher_by_uuid(&cipher_id, &headers, &mut conn, &CipherDeleteOptions::HardSingle, &nt).await
async fn delete_cipher(cipher_id: CipherId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult {
_delete_cipher_by_uuid(&cipher_id, &headers, &conn, &CipherDeleteOptions::HardSingle, &nt).await
// permanent delete
}
#[delete("/ciphers/<cipher_id>/admin")]
async fn delete_cipher_admin(cipher_id: CipherId, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
_delete_cipher_by_uuid(&cipher_id, &headers, &mut conn, &CipherDeleteOptions::HardSingle, &nt).await
async fn delete_cipher_admin(cipher_id: CipherId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult {
_delete_cipher_by_uuid(&cipher_id, &headers, &conn, &CipherDeleteOptions::HardSingle, &nt).await
// permanent delete
}
@ -1526,38 +1510,33 @@ async fn delete_cipher_selected_put_admin(
}
#[put("/ciphers/<cipher_id>/restore")]
async fn restore_cipher_put(cipher_id: CipherId, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult {
_restore_cipher_by_uuid(&cipher_id, &headers, false, &mut conn, &nt).await
async fn restore_cipher_put(cipher_id: CipherId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult {
_restore_cipher_by_uuid(&cipher_id, &headers, false, &conn, &nt).await
}
#[put("/ciphers/<cipher_id>/restore-admin")]
async fn restore_cipher_put_admin(
cipher_id: CipherId,
headers: Headers,
mut conn: DbConn,
nt: Notify<'_>,
) -> JsonResult {
_restore_cipher_by_uuid(&cipher_id, &headers, false, &mut conn, &nt).await
async fn restore_cipher_put_admin(cipher_id: CipherId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult {
_restore_cipher_by_uuid(&cipher_id, &headers, false, &conn, &nt).await
}
#[put("/ciphers/restore-admin", data = "<data>")]
async fn restore_cipher_selected_admin(
data: Json<CipherIdsData>,
headers: Headers,
mut conn: DbConn,
conn: DbConn,
nt: Notify<'_>,
) -> JsonResult {
_restore_multiple_ciphers(data, &headers, &mut conn, &nt).await
_restore_multiple_ciphers(data, &headers, &conn, &nt).await
}
#[put("/ciphers/restore", data = "<data>")]
async fn restore_cipher_selected(
data: Json<CipherIdsData>,
headers: Headers,
mut conn: DbConn,
conn: DbConn,
nt: Notify<'_>,
) -> JsonResult {
_restore_multiple_ciphers(data, &headers, &mut conn, &nt).await
_restore_multiple_ciphers(data, &headers, &conn, &nt).await
}
#[derive(Deserialize)]
@ -1571,14 +1550,14 @@ struct MoveCipherData {
async fn move_cipher_selected(
data: Json<MoveCipherData>,
headers: Headers,
mut conn: DbConn,
conn: DbConn,
nt: Notify<'_>,
) -> EmptyResult {
let data = data.into_inner();
let user_id = &headers.user.uuid;
if let Some(ref folder_id) = data.folder_id {
if Folder::find_by_uuid_and_user(folder_id, user_id, &mut conn).await.is_none() {
if Folder::find_by_uuid_and_user(folder_id, user_id, &conn).await.is_none() {
err!("Invalid folder", "Folder does not exist or belongs to another user");
}
}
@ -1588,10 +1567,10 @@ async fn move_cipher_selected(
// TODO: Convert this to use a single query (or at least less) to update all items
// Find all ciphers a user has access to, all others will be ignored
let accessible_ciphers = Cipher::find_by_user_and_ciphers(user_id, &data.ids, &mut conn).await;
let accessible_ciphers = Cipher::find_by_user_and_ciphers(user_id, &data.ids, &conn).await;
let accessible_ciphers_count = accessible_ciphers.len();
for cipher in accessible_ciphers {
cipher.move_to_folder(data.folder_id.clone(), user_id, &mut conn).await?;
cipher.move_to_folder(data.folder_id.clone(), user_id, &conn).await?;
if cipher_count == 1 {
single_cipher = Some(cipher);
}
@ -1604,12 +1583,12 @@ async fn move_cipher_selected(
std::slice::from_ref(user_id),
&headers.device,
None,
&mut conn,
&conn,
)
.await;
} else {
// 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, &mut conn).await;
nt.send_user_update(UpdateType::SyncCiphers, &headers.user, &headers.device.push_uuid, &conn).await;
}
if cipher_count != accessible_ciphers_count {
@ -1642,23 +1621,23 @@ async fn delete_all(
organization: Option<OrganizationIdData>,
data: Json<PasswordOrOtpData>,
headers: Headers,
mut conn: DbConn,
conn: DbConn,
nt: Notify<'_>,
) -> EmptyResult {
let data: PasswordOrOtpData = data.into_inner();
let mut user = headers.user;
data.validate(&user, true, &mut conn).await?;
data.validate(&user, true, &conn).await?;
match organization {
Some(org_data) => {
// Organization ID in query params, purging organization vault
match Membership::find_by_user_and_org(&user.uuid, &org_data.org_id, &mut conn).await {
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, &mut conn).await?;
nt.send_user_update(UpdateType::SyncVault, &user, &headers.device.push_uuid, &mut conn).await;
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,
@ -1667,7 +1646,7 @@ async fn delete_all(
&user.uuid,
headers.device.atype,
&headers.ip.ip,
&mut conn,
&conn,
)
.await;
@ -1681,17 +1660,17 @@ async fn delete_all(
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, &mut conn).await {
cipher.delete(&mut conn).await?;
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, &mut conn).await {
f.delete(&mut conn).await?;
for f in Folder::find_by_user(&user.uuid, &conn).await {
f.delete(&conn).await?;
}
user.update_revision(&mut conn).await?;
nt.send_user_update(UpdateType::SyncVault, &user, &headers.device.push_uuid, &mut conn).await;
user.update_revision(&conn).await?;
nt.send_user_update(UpdateType::SyncVault, &user, &headers.device.push_uuid, &conn).await;
Ok(())
}
@ -1709,7 +1688,7 @@ pub enum CipherDeleteOptions {
async fn _delete_cipher_by_uuid(
cipher_id: &CipherId,
headers: &Headers,
conn: &mut DbConn,
conn: &DbConn,
delete_options: &CipherDeleteOptions,
nt: &Notify<'_>,
) -> EmptyResult {
@ -1775,20 +1754,20 @@ struct CipherIdsData {
async fn _delete_multiple_ciphers(
data: Json<CipherIdsData>,
headers: Headers,
mut conn: DbConn,
conn: DbConn,
delete_options: CipherDeleteOptions,
nt: Notify<'_>,
) -> EmptyResult {
let data = data.into_inner();
for cipher_id in data.ids {
if let error @ Err(_) = _delete_cipher_by_uuid(&cipher_id, &headers, &mut conn, &delete_options, &nt).await {
if let error @ Err(_) = _delete_cipher_by_uuid(&cipher_id, &headers, &conn, &delete_options, &nt).await {
return error;
};
}
// 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, &mut conn).await;
nt.send_user_update(UpdateType::SyncCiphers, &headers.user, &headers.device.push_uuid, &conn).await;
Ok(())
}
@ -1797,7 +1776,7 @@ async fn _restore_cipher_by_uuid(
cipher_id: &CipherId,
headers: &Headers,
multi_restore: bool,
conn: &mut DbConn,
conn: &DbConn,
nt: &Notify<'_>,
) -> JsonResult {
let Some(mut cipher) = Cipher::find_by_uuid(cipher_id, conn).await else {
@ -1842,7 +1821,7 @@ async fn _restore_cipher_by_uuid(
async fn _restore_multiple_ciphers(
data: Json<CipherIdsData>,
headers: &Headers,
conn: &mut DbConn,
conn: &DbConn,
nt: &Notify<'_>,
) -> JsonResult {
let data = data.into_inner();
@ -1869,7 +1848,7 @@ async fn _delete_cipher_attachment_by_id(
cipher_id: &CipherId,
attachment_id: &AttachmentId,
headers: &Headers,
conn: &mut DbConn,
conn: &DbConn,
nt: &Notify<'_>,
) -> JsonResult {
let Some(attachment) = Attachment::find_by_id(attachment_id, conn).await else {
@ -1938,7 +1917,7 @@ pub enum CipherSyncType {
}
impl CipherSyncData {
pub async fn new(user_id: &UserId, sync_type: CipherSyncType, conn: &mut DbConn) -> Self {
pub async fn new(user_id: &UserId, sync_type: CipherSyncType, conn: &DbConn) -> Self {
let cipher_folders: HashMap<CipherId, FolderId>;
let cipher_favorites: HashSet<CipherId>;
match sync_type {

174
src/api/core/emergency_access.rs

@ -8,7 +8,13 @@ use crate::{
EmptyResult, JsonResult,
},
auth::{decode_emergency_access_invite, Headers},
db::{models::*, DbConn, DbPool},
db::{
models::{
Cipher, EmergencyAccess, EmergencyAccessId, EmergencyAccessStatus, EmergencyAccessType, Invitation,
Membership, MembershipType, OrgPolicy, TwoFactor, User, UserId,
},
DbConn, DbPool,
},
mail,
util::NumberOrString,
CONFIG,
@ -40,7 +46,7 @@ pub fn routes() -> Vec<Route> {
// region get
#[get("/emergency-access/trusted")]
async fn get_contacts(headers: Headers, mut conn: DbConn) -> Json<Value> {
async fn get_contacts(headers: Headers, conn: DbConn) -> Json<Value> {
if !CONFIG.emergency_access_allowed() {
return Json(json!({
"data": [{
@ -58,10 +64,10 @@ async fn get_contacts(headers: Headers, mut conn: DbConn) -> Json<Value> {
"continuationToken": null
}));
}
let emergency_access_list = EmergencyAccess::find_all_by_grantor_uuid(&headers.user.uuid, &mut conn).await;
let emergency_access_list = EmergencyAccess::find_all_by_grantor_uuid(&headers.user.uuid, &conn).await;
let mut emergency_access_list_json = Vec::with_capacity(emergency_access_list.len());
for ea in emergency_access_list {
if let Some(grantee) = ea.to_json_grantee_details(&mut conn).await {
if let Some(grantee) = ea.to_json_grantee_details(&conn).await {
emergency_access_list_json.push(grantee)
}
}
@ -74,15 +80,15 @@ async fn get_contacts(headers: Headers, mut conn: DbConn) -> Json<Value> {
}
#[get("/emergency-access/granted")]
async fn get_grantees(headers: Headers, mut conn: DbConn) -> Json<Value> {
async fn get_grantees(headers: Headers, conn: DbConn) -> Json<Value> {
let emergency_access_list = if CONFIG.emergency_access_allowed() {
EmergencyAccess::find_all_by_grantee_uuid(&headers.user.uuid, &mut conn).await
EmergencyAccess::find_all_by_grantee_uuid(&headers.user.uuid, &conn).await
} else {
Vec::new()
};
let mut emergency_access_list_json = Vec::with_capacity(emergency_access_list.len());
for ea in emergency_access_list {
emergency_access_list_json.push(ea.to_json_grantor_details(&mut conn).await);
emergency_access_list_json.push(ea.to_json_grantor_details(&conn).await);
}
Json(json!({
@ -93,12 +99,12 @@ async fn get_grantees(headers: Headers, mut conn: DbConn) -> Json<Value> {
}
#[get("/emergency-access/<emer_id>")]
async fn get_emergency_access(emer_id: EmergencyAccessId, headers: Headers, mut conn: DbConn) -> JsonResult {
async fn get_emergency_access(emer_id: EmergencyAccessId, headers: Headers, conn: DbConn) -> JsonResult {
check_emergency_access_enabled()?;
match EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &mut conn).await {
match EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &conn).await {
Some(emergency_access) => Ok(Json(
emergency_access.to_json_grantee_details(&mut conn).await.expect("Grantee user should exist but does not!"),
emergency_access.to_json_grantee_details(&conn).await.expect("Grantee user should exist but does not!"),
)),
None => err!("Emergency access not valid."),
}
@ -131,14 +137,14 @@ async fn post_emergency_access(
emer_id: EmergencyAccessId,
data: Json<EmergencyAccessUpdateData>,
headers: Headers,
mut conn: DbConn,
conn: DbConn,
) -> JsonResult {
check_emergency_access_enabled()?;
let data: EmergencyAccessUpdateData = data.into_inner();
let Some(mut emergency_access) =
EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &mut conn).await
EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &conn).await
else {
err!("Emergency access not valid.")
};
@ -154,7 +160,7 @@ async fn post_emergency_access(
emergency_access.key_encrypted = data.key_encrypted;
}
emergency_access.save(&mut conn).await?;
emergency_access.save(&conn).await?;
Ok(Json(emergency_access.to_json()))
}
@ -163,12 +169,12 @@ async fn post_emergency_access(
// region delete
#[delete("/emergency-access/<emer_id>")]
async fn delete_emergency_access(emer_id: EmergencyAccessId, headers: Headers, mut conn: DbConn) -> EmptyResult {
async fn delete_emergency_access(emer_id: EmergencyAccessId, headers: Headers, conn: DbConn) -> EmptyResult {
check_emergency_access_enabled()?;
let emergency_access = match (
EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &mut conn).await,
EmergencyAccess::find_by_uuid_and_grantee_uuid(&emer_id, &headers.user.uuid, &mut conn).await,
EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &conn).await,
EmergencyAccess::find_by_uuid_and_grantee_uuid(&emer_id, &headers.user.uuid, &conn).await,
) {
(Some(grantor_emer), None) => {
info!("Grantor deleted emergency access {emer_id}");
@ -181,7 +187,7 @@ async fn delete_emergency_access(emer_id: EmergencyAccessId, headers: Headers, m
_ => err!("Emergency access not valid."),
};
emergency_access.delete(&mut conn).await?;
emergency_access.delete(&conn).await?;
Ok(())
}
@ -203,7 +209,7 @@ struct EmergencyAccessInviteData {
}
#[post("/emergency-access/invite", data = "<data>")]
async fn send_invite(data: Json<EmergencyAccessInviteData>, headers: Headers, mut conn: DbConn) -> EmptyResult {
async fn send_invite(data: Json<EmergencyAccessInviteData>, headers: Headers, conn: DbConn) -> EmptyResult {
check_emergency_access_enabled()?;
let data: EmergencyAccessInviteData = data.into_inner();
@ -224,7 +230,7 @@ async fn send_invite(data: Json<EmergencyAccessInviteData>, headers: Headers, mu
err!("You can not set yourself as an emergency contact.")
}
let (grantee_user, new_user) = match User::find_by_mail(&email, &mut conn).await {
let (grantee_user, new_user) = match User::find_by_mail(&email, &conn).await {
None => {
if !CONFIG.invitations_allowed() {
err!(format!("Grantee user does not exist: {email}"))
@ -236,11 +242,11 @@ async fn send_invite(data: Json<EmergencyAccessInviteData>, headers: Headers, mu
if !CONFIG.mail_enabled() {
let invitation = Invitation::new(&email);
invitation.save(&mut conn).await?;
invitation.save(&conn).await?;
}
let mut user = User::new(email.clone(), None);
user.save(&mut conn).await?;
let mut user = User::new(&email, None);
user.save(&conn).await?;
(user, true)
}
Some(user) if user.password_hash.is_empty() => (user, true),
@ -251,7 +257,7 @@ async fn send_invite(data: Json<EmergencyAccessInviteData>, headers: Headers, mu
&grantor_user.uuid,
&grantee_user.uuid,
&grantee_user.email,
&mut conn,
&conn,
)
.await
.is_some()
@ -261,7 +267,7 @@ async fn send_invite(data: Json<EmergencyAccessInviteData>, headers: Headers, mu
let mut new_emergency_access =
EmergencyAccess::new(grantor_user.uuid, grantee_user.email, emergency_access_status, new_type, wait_time_days);
new_emergency_access.save(&mut conn).await?;
new_emergency_access.save(&conn).await?;
if CONFIG.mail_enabled() {
mail::send_emergency_access_invite(
@ -274,18 +280,18 @@ async fn send_invite(data: Json<EmergencyAccessInviteData>, headers: Headers, mu
.await?;
} else if !new_user {
// if mail is not enabled immediately accept the invitation for existing users
new_emergency_access.accept_invite(&grantee_user.uuid, &email, &mut conn).await?;
new_emergency_access.accept_invite(&grantee_user.uuid, &email, &conn).await?;
}
Ok(())
}
#[post("/emergency-access/<emer_id>/reinvite")]
async fn resend_invite(emer_id: EmergencyAccessId, headers: Headers, mut conn: DbConn) -> EmptyResult {
async fn resend_invite(emer_id: EmergencyAccessId, headers: Headers, conn: DbConn) -> EmptyResult {
check_emergency_access_enabled()?;
let Some(mut emergency_access) =
EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &mut conn).await
EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &conn).await
else {
err!("Emergency access not valid.")
};
@ -298,7 +304,7 @@ async fn resend_invite(emer_id: EmergencyAccessId, headers: Headers, mut conn: D
err!("Email not valid.")
};
let Some(grantee_user) = User::find_by_mail(&email, &mut conn).await else {
let Some(grantee_user) = User::find_by_mail(&email, &conn).await else {
err!("Grantee user not found.")
};
@ -315,10 +321,10 @@ async fn resend_invite(emer_id: EmergencyAccessId, headers: Headers, mut conn: D
.await?;
} else if !grantee_user.password_hash.is_empty() {
// accept the invitation for existing user
emergency_access.accept_invite(&grantee_user.uuid, &email, &mut conn).await?;
} else if CONFIG.invitations_allowed() && Invitation::find_by_mail(&email, &mut conn).await.is_none() {
emergency_access.accept_invite(&grantee_user.uuid, &email, &conn).await?;
} else if CONFIG.invitations_allowed() && Invitation::find_by_mail(&email, &conn).await.is_none() {
let invitation = Invitation::new(&email);
invitation.save(&mut conn).await?;
invitation.save(&conn).await?;
}
Ok(())
@ -335,7 +341,7 @@ async fn accept_invite(
emer_id: EmergencyAccessId,
data: Json<AcceptData>,
headers: Headers,
mut conn: DbConn,
conn: DbConn,
) -> EmptyResult {
check_emergency_access_enabled()?;
@ -349,9 +355,9 @@ async fn accept_invite(
err!("Claim email does not match current users email")
}
let grantee_user = match User::find_by_mail(&claims.email, &mut conn).await {
let grantee_user = match User::find_by_mail(&claims.email, &conn).await {
Some(user) => {
Invitation::take(&claims.email, &mut conn).await;
Invitation::take(&claims.email, &conn).await;
user
}
None => err!("Invited user not found"),
@ -360,13 +366,13 @@ async fn accept_invite(
// We need to search for the uuid in combination with the email, since we do not yet store the uuid of the grantee in the database.
// The uuid of the grantee gets stored once accepted.
let Some(mut emergency_access) =
EmergencyAccess::find_by_uuid_and_grantee_email(&emer_id, &headers.user.email, &mut conn).await
EmergencyAccess::find_by_uuid_and_grantee_email(&emer_id, &headers.user.email, &conn).await
else {
err!("Emergency access not valid.")
};
// get grantor user to send Accepted email
let Some(grantor_user) = User::find_by_uuid(&emergency_access.grantor_uuid, &mut conn).await else {
let Some(grantor_user) = User::find_by_uuid(&emergency_access.grantor_uuid, &conn).await else {
err!("Grantor user not found.")
};
@ -374,7 +380,7 @@ async fn accept_invite(
&& grantor_user.name == claims.grantor_name
&& grantor_user.email == claims.grantor_email
{
emergency_access.accept_invite(&grantee_user.uuid, &grantee_user.email, &mut conn).await?;
emergency_access.accept_invite(&grantee_user.uuid, &grantee_user.email, &conn).await?;
if CONFIG.mail_enabled() {
mail::send_emergency_access_invite_accepted(&grantor_user.email, &grantee_user.email).await?;
@ -397,7 +403,7 @@ async fn confirm_emergency_access(
emer_id: EmergencyAccessId,
data: Json<ConfirmData>,
headers: Headers,
mut conn: DbConn,
conn: DbConn,
) -> JsonResult {
check_emergency_access_enabled()?;
@ -406,7 +412,7 @@ async fn confirm_emergency_access(
let key = data.key;
let Some(mut emergency_access) =
EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &confirming_user.uuid, &mut conn).await
EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &confirming_user.uuid, &conn).await
else {
err!("Emergency access not valid.")
};
@ -417,12 +423,12 @@ async fn confirm_emergency_access(
err!("Emergency access not valid.")
}
let Some(grantor_user) = User::find_by_uuid(&confirming_user.uuid, &mut conn).await else {
let Some(grantor_user) = User::find_by_uuid(&confirming_user.uuid, &conn).await else {
err!("Grantor user not found.")
};
if let Some(grantee_uuid) = emergency_access.grantee_uuid.as_ref() {
let Some(grantee_user) = User::find_by_uuid(grantee_uuid, &mut conn).await else {
let Some(grantee_user) = User::find_by_uuid(grantee_uuid, &conn).await else {
err!("Grantee user not found.")
};
@ -430,7 +436,7 @@ async fn confirm_emergency_access(
emergency_access.key_encrypted = Some(key);
emergency_access.email = None;
emergency_access.save(&mut conn).await?;
emergency_access.save(&conn).await?;
if CONFIG.mail_enabled() {
mail::send_emergency_access_invite_confirmed(&grantee_user.email, &grantor_user.name).await?;
@ -446,12 +452,12 @@ async fn confirm_emergency_access(
// region access emergency access
#[post("/emergency-access/<emer_id>/initiate")]
async fn initiate_emergency_access(emer_id: EmergencyAccessId, headers: Headers, mut conn: DbConn) -> JsonResult {
async fn initiate_emergency_access(emer_id: EmergencyAccessId, headers: Headers, conn: DbConn) -> JsonResult {
check_emergency_access_enabled()?;
let initiating_user = headers.user;
let Some(mut emergency_access) =
EmergencyAccess::find_by_uuid_and_grantee_uuid(&emer_id, &initiating_user.uuid, &mut conn).await
EmergencyAccess::find_by_uuid_and_grantee_uuid(&emer_id, &initiating_user.uuid, &conn).await
else {
err!("Emergency access not valid.")
};
@ -460,7 +466,7 @@ async fn initiate_emergency_access(emer_id: EmergencyAccessId, headers: Headers,
err!("Emergency access not valid.")
}
let Some(grantor_user) = User::find_by_uuid(&emergency_access.grantor_uuid, &mut conn).await else {
let Some(grantor_user) = User::find_by_uuid(&emergency_access.grantor_uuid, &conn).await else {
err!("Grantor user not found.")
};
@ -469,7 +475,7 @@ async fn initiate_emergency_access(emer_id: EmergencyAccessId, headers: Headers,
emergency_access.updated_at = now;
emergency_access.recovery_initiated_at = Some(now);
emergency_access.last_notification_at = Some(now);
emergency_access.save(&mut conn).await?;
emergency_access.save(&conn).await?;
if CONFIG.mail_enabled() {
mail::send_emergency_access_recovery_initiated(
@ -484,11 +490,11 @@ async fn initiate_emergency_access(emer_id: EmergencyAccessId, headers: Headers,
}
#[post("/emergency-access/<emer_id>/approve")]
async fn approve_emergency_access(emer_id: EmergencyAccessId, headers: Headers, mut conn: DbConn) -> JsonResult {
async fn approve_emergency_access(emer_id: EmergencyAccessId, headers: Headers, conn: DbConn) -> JsonResult {
check_emergency_access_enabled()?;
let Some(mut emergency_access) =
EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &mut conn).await
EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &conn).await
else {
err!("Emergency access not valid.")
};
@ -497,17 +503,17 @@ async fn approve_emergency_access(emer_id: EmergencyAccessId, headers: Headers,
err!("Emergency access not valid.")
}
let Some(grantor_user) = User::find_by_uuid(&headers.user.uuid, &mut conn).await else {
let Some(grantor_user) = User::find_by_uuid(&headers.user.uuid, &conn).await else {
err!("Grantor user not found.")
};
if let Some(grantee_uuid) = emergency_access.grantee_uuid.as_ref() {
let Some(grantee_user) = User::find_by_uuid(grantee_uuid, &mut conn).await else {
let Some(grantee_user) = User::find_by_uuid(grantee_uuid, &conn).await else {
err!("Grantee user not found.")
};
emergency_access.status = EmergencyAccessStatus::RecoveryApproved as i32;
emergency_access.save(&mut conn).await?;
emergency_access.save(&conn).await?;
if CONFIG.mail_enabled() {
mail::send_emergency_access_recovery_approved(&grantee_user.email, &grantor_user.name).await?;
@ -519,11 +525,11 @@ async fn approve_emergency_access(emer_id: EmergencyAccessId, headers: Headers,
}
#[post("/emergency-access/<emer_id>/reject")]
async fn reject_emergency_access(emer_id: EmergencyAccessId, headers: Headers, mut conn: DbConn) -> JsonResult {
async fn reject_emergency_access(emer_id: EmergencyAccessId, headers: Headers, conn: DbConn) -> JsonResult {
check_emergency_access_enabled()?;
let Some(mut emergency_access) =
EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &mut conn).await
EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &conn).await
else {
err!("Emergency access not valid.")
};
@ -535,12 +541,12 @@ async fn reject_emergency_access(emer_id: EmergencyAccessId, headers: Headers, m
}
if let Some(grantee_uuid) = emergency_access.grantee_uuid.as_ref() {
let Some(grantee_user) = User::find_by_uuid(grantee_uuid, &mut conn).await else {
let Some(grantee_user) = User::find_by_uuid(grantee_uuid, &conn).await else {
err!("Grantee user not found.")
};
emergency_access.status = EmergencyAccessStatus::Confirmed as i32;
emergency_access.save(&mut conn).await?;
emergency_access.save(&conn).await?;
if CONFIG.mail_enabled() {
mail::send_emergency_access_recovery_rejected(&grantee_user.email, &headers.user.name).await?;
@ -556,11 +562,11 @@ async fn reject_emergency_access(emer_id: EmergencyAccessId, headers: Headers, m
// region action
#[post("/emergency-access/<emer_id>/view")]
async fn view_emergency_access(emer_id: EmergencyAccessId, headers: Headers, mut conn: DbConn) -> JsonResult {
async fn view_emergency_access(emer_id: EmergencyAccessId, headers: Headers, conn: DbConn) -> JsonResult {
check_emergency_access_enabled()?;
let Some(emergency_access) =
EmergencyAccess::find_by_uuid_and_grantee_uuid(&emer_id, &headers.user.uuid, &mut conn).await
EmergencyAccess::find_by_uuid_and_grantee_uuid(&emer_id, &headers.user.uuid, &conn).await
else {
err!("Emergency access not valid.")
};
@ -569,8 +575,8 @@ async fn view_emergency_access(emer_id: EmergencyAccessId, headers: Headers, mut
err!("Emergency access not valid.")
}
let ciphers = Cipher::find_owned_by_user(&emergency_access.grantor_uuid, &mut conn).await;
let cipher_sync_data = CipherSyncData::new(&emergency_access.grantor_uuid, CipherSyncType::User, &mut conn).await;
let ciphers = Cipher::find_owned_by_user(&emergency_access.grantor_uuid, &conn).await;
let cipher_sync_data = CipherSyncData::new(&emergency_access.grantor_uuid, CipherSyncType::User, &conn).await;
let mut ciphers_json = Vec::with_capacity(ciphers.len());
for c in ciphers {
@ -580,7 +586,7 @@ async fn view_emergency_access(emer_id: EmergencyAccessId, headers: Headers, mut
&emergency_access.grantor_uuid,
Some(&cipher_sync_data),
CipherSyncType::User,
&mut conn,
&conn,
)
.await?,
);
@ -594,12 +600,12 @@ async fn view_emergency_access(emer_id: EmergencyAccessId, headers: Headers, mut
}
#[post("/emergency-access/<emer_id>/takeover")]
async fn takeover_emergency_access(emer_id: EmergencyAccessId, headers: Headers, mut conn: DbConn) -> JsonResult {
async fn takeover_emergency_access(emer_id: EmergencyAccessId, headers: Headers, conn: DbConn) -> JsonResult {
check_emergency_access_enabled()?;
let requesting_user = headers.user;
let Some(emergency_access) =
EmergencyAccess::find_by_uuid_and_grantee_uuid(&emer_id, &requesting_user.uuid, &mut conn).await
EmergencyAccess::find_by_uuid_and_grantee_uuid(&emer_id, &requesting_user.uuid, &conn).await
else {
err!("Emergency access not valid.")
};
@ -608,7 +614,7 @@ async fn takeover_emergency_access(emer_id: EmergencyAccessId, headers: Headers,
err!("Emergency access not valid.")
}
let Some(grantor_user) = User::find_by_uuid(&emergency_access.grantor_uuid, &mut conn).await else {
let Some(grantor_user) = User::find_by_uuid(&emergency_access.grantor_uuid, &conn).await else {
err!("Grantor user not found.")
};
@ -636,7 +642,7 @@ async fn password_emergency_access(
emer_id: EmergencyAccessId,
data: Json<EmergencyAccessPasswordData>,
headers: Headers,
mut conn: DbConn,
conn: DbConn,
) -> EmptyResult {
check_emergency_access_enabled()?;
@ -646,7 +652,7 @@ async fn password_emergency_access(
let requesting_user = headers.user;
let Some(emergency_access) =
EmergencyAccess::find_by_uuid_and_grantee_uuid(&emer_id, &requesting_user.uuid, &mut conn).await
EmergencyAccess::find_by_uuid_and_grantee_uuid(&emer_id, &requesting_user.uuid, &conn).await
else {
err!("Emergency access not valid.")
};
@ -655,21 +661,21 @@ async fn password_emergency_access(
err!("Emergency access not valid.")
}
let Some(mut grantor_user) = User::find_by_uuid(&emergency_access.grantor_uuid, &mut conn).await else {
let Some(mut grantor_user) = User::find_by_uuid(&emergency_access.grantor_uuid, &conn).await else {
err!("Grantor user not found.")
};
// change grantor_user password
grantor_user.set_password(new_master_password_hash, Some(data.key), true, None);
grantor_user.save(&mut conn).await?;
grantor_user.save(&conn).await?;
// Disable TwoFactor providers since they will otherwise block logins
TwoFactor::delete_all_by_user(&grantor_user.uuid, &mut conn).await?;
TwoFactor::delete_all_by_user(&grantor_user.uuid, &conn).await?;
// Remove grantor from all organisations unless Owner
for member in Membership::find_any_state_by_user(&grantor_user.uuid, &mut conn).await {
for member in Membership::find_any_state_by_user(&grantor_user.uuid, &conn).await {
if member.atype != MembershipType::Owner as i32 {
member.delete(&mut conn).await?;
member.delete(&conn).await?;
}
}
Ok(())
@ -678,10 +684,10 @@ async fn password_emergency_access(
// endregion
#[get("/emergency-access/<emer_id>/policies")]
async fn policies_emergency_access(emer_id: EmergencyAccessId, headers: Headers, mut conn: DbConn) -> JsonResult {
async fn policies_emergency_access(emer_id: EmergencyAccessId, headers: Headers, conn: DbConn) -> JsonResult {
let requesting_user = headers.user;
let Some(emergency_access) =
EmergencyAccess::find_by_uuid_and_grantee_uuid(&emer_id, &requesting_user.uuid, &mut conn).await
EmergencyAccess::find_by_uuid_and_grantee_uuid(&emer_id, &requesting_user.uuid, &conn).await
else {
err!("Emergency access not valid.")
};
@ -690,11 +696,11 @@ async fn policies_emergency_access(emer_id: EmergencyAccessId, headers: Headers,
err!("Emergency access not valid.")
}
let Some(grantor_user) = User::find_by_uuid(&emergency_access.grantor_uuid, &mut conn).await else {
let Some(grantor_user) = User::find_by_uuid(&emergency_access.grantor_uuid, &conn).await else {
err!("Grantor user not found.")
};
let policies = OrgPolicy::find_confirmed_by_user(&grantor_user.uuid, &mut conn);
let policies = OrgPolicy::find_confirmed_by_user(&grantor_user.uuid, &conn);
let policies_json: Vec<Value> = policies.await.iter().map(OrgPolicy::to_json).collect();
Ok(Json(json!({
@ -728,8 +734,8 @@ pub async fn emergency_request_timeout_job(pool: DbPool) {
return;
}
if let Ok(mut conn) = pool.get().await {
let emergency_access_list = EmergencyAccess::find_all_recoveries_initiated(&mut conn).await;
if let Ok(conn) = pool.get().await {
let emergency_access_list = EmergencyAccess::find_all_recoveries_initiated(&conn).await;
if emergency_access_list.is_empty() {
debug!("No emergency request timeout to approve");
@ -743,18 +749,18 @@ pub async fn emergency_request_timeout_job(pool: DbPool) {
if recovery_allowed_at.le(&now) {
// Only update the access status
// Updating the whole record could cause issues when the emergency_notification_reminder_job is also active
emer.update_access_status_and_save(EmergencyAccessStatus::RecoveryApproved as i32, &now, &mut conn)
emer.update_access_status_and_save(EmergencyAccessStatus::RecoveryApproved as i32, &now, &conn)
.await
.expect("Unable to update emergency access status");
if CONFIG.mail_enabled() {
// get grantor user to send Accepted email
let grantor_user =
User::find_by_uuid(&emer.grantor_uuid, &mut conn).await.expect("Grantor user not found");
User::find_by_uuid(&emer.grantor_uuid, &conn).await.expect("Grantor user not found");
// get grantee user to send Accepted email
let grantee_user =
User::find_by_uuid(&emer.grantee_uuid.clone().expect("Grantee user invalid"), &mut conn)
User::find_by_uuid(&emer.grantee_uuid.clone().expect("Grantee user invalid"), &conn)
.await
.expect("Grantee user not found");
@ -783,8 +789,8 @@ pub async fn emergency_notification_reminder_job(pool: DbPool) {
return;
}
if let Ok(mut conn) = pool.get().await {
let emergency_access_list = EmergencyAccess::find_all_recoveries_initiated(&mut conn).await;
if let Ok(conn) = pool.get().await {
let emergency_access_list = EmergencyAccess::find_all_recoveries_initiated(&conn).await;
if emergency_access_list.is_empty() {
debug!("No emergency request reminder notification to send");
@ -805,18 +811,18 @@ pub async fn emergency_notification_reminder_job(pool: DbPool) {
if final_recovery_reminder_at.le(&now) && next_recovery_reminder_at.le(&now) {
// Only update the last notification date
// Updating the whole record could cause issues when the emergency_request_timeout_job is also active
emer.update_last_notification_date_and_save(&now, &mut conn)
emer.update_last_notification_date_and_save(&now, &conn)
.await
.expect("Unable to update emergency access notification date");
if CONFIG.mail_enabled() {
// get grantor user to send Accepted email
let grantor_user =
User::find_by_uuid(&emer.grantor_uuid, &mut conn).await.expect("Grantor user not found");
User::find_by_uuid(&emer.grantor_uuid, &conn).await.expect("Grantor user not found");
// get grantee user to send Accepted email
let grantee_user =
User::find_by_uuid(&emer.grantee_uuid.clone().expect("Grantee user invalid"), &mut conn)
User::find_by_uuid(&emer.grantee_uuid.clone().expect("Grantee user invalid"), &conn)
.await
.expect("Grantee user not found");

41
src/api/core/events.rs

@ -31,12 +31,7 @@ struct EventRange {
// Upstream: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/AdminConsole/Controllers/EventsController.cs#L87
#[get("/organizations/<org_id>/events?<data..>")]
async fn get_org_events(
org_id: OrganizationId,
data: EventRange,
headers: AdminHeaders,
mut conn: DbConn,
) -> JsonResult {
async fn get_org_events(org_id: OrganizationId, data: EventRange, headers: AdminHeaders, conn: DbConn) -> JsonResult {
if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
@ -53,7 +48,7 @@ async fn get_org_events(
parse_date(&data.end)
};
Event::find_by_organization_uuid(&org_id, &start_date, &end_date, &mut conn)
Event::find_by_organization_uuid(&org_id, &start_date, &end_date, &conn)
.await
.iter()
.map(|e| e.to_json())
@ -68,14 +63,14 @@ async fn get_org_events(
}
#[get("/ciphers/<cipher_id>/events?<data..>")]
async fn get_cipher_events(cipher_id: CipherId, data: EventRange, headers: Headers, mut conn: DbConn) -> JsonResult {
async fn get_cipher_events(cipher_id: CipherId, data: EventRange, headers: Headers, conn: DbConn) -> JsonResult {
// Return an empty vec when we org events are disabled.
// This prevents client errors
let events_json: Vec<Value> = if !CONFIG.org_events_enabled() {
Vec::with_capacity(0)
} else {
let mut events_json = Vec::with_capacity(0);
if Membership::user_has_ge_admin_access_to_cipher(&headers.user.uuid, &cipher_id, &mut conn).await {
if Membership::user_has_ge_admin_access_to_cipher(&headers.user.uuid, &cipher_id, &conn).await {
let start_date = parse_date(&data.start);
let end_date = if let Some(before_date) = &data.continuation_token {
parse_date(before_date)
@ -83,7 +78,7 @@ async fn get_cipher_events(cipher_id: CipherId, data: EventRange, headers: Heade
parse_date(&data.end)
};
events_json = Event::find_by_cipher_uuid(&cipher_id, &start_date, &end_date, &mut conn)
events_json = Event::find_by_cipher_uuid(&cipher_id, &start_date, &end_date, &conn)
.await
.iter()
.map(|e| e.to_json())
@ -105,7 +100,7 @@ async fn get_user_events(
member_id: MembershipId,
data: EventRange,
headers: AdminHeaders,
mut conn: DbConn,
conn: DbConn,
) -> JsonResult {
if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
@ -122,7 +117,7 @@ async fn get_user_events(
parse_date(&data.end)
};
Event::find_by_org_and_member(&org_id, &member_id, &start_date, &end_date, &mut conn)
Event::find_by_org_and_member(&org_id, &member_id, &start_date, &end_date, &conn)
.await
.iter()
.map(|e| e.to_json())
@ -172,7 +167,7 @@ struct EventCollection {
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Events/Controllers/CollectController.cs
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Services/Implementations/EventService.cs
#[post("/collect", format = "application/json", data = "<data>")]
async fn post_events_collect(data: Json<Vec<EventCollection>>, headers: Headers, mut conn: DbConn) -> EmptyResult {
async fn post_events_collect(data: Json<Vec<EventCollection>>, headers: Headers, conn: DbConn) -> EmptyResult {
if !CONFIG.org_events_enabled() {
return Ok(());
}
@ -187,7 +182,7 @@ async fn post_events_collect(data: Json<Vec<EventCollection>>, headers: Headers,
headers.device.atype,
Some(event_date),
&headers.ip.ip,
&mut conn,
&conn,
)
.await;
}
@ -201,14 +196,14 @@ async fn post_events_collect(data: Json<Vec<EventCollection>>, headers: Headers,
headers.device.atype,
Some(event_date),
&headers.ip.ip,
&mut conn,
&conn,
)
.await;
}
}
_ => {
if let Some(cipher_uuid) = &event.cipher_id {
if let Some(cipher) = Cipher::find_by_uuid(cipher_uuid, &mut conn).await {
if let Some(cipher) = Cipher::find_by_uuid(cipher_uuid, &conn).await {
if let Some(org_id) = cipher.organization_uuid {
_log_event(
event.r#type,
@ -218,7 +213,7 @@ async fn post_events_collect(data: Json<Vec<EventCollection>>, headers: Headers,
headers.device.atype,
Some(event_date),
&headers.ip.ip,
&mut conn,
&conn,
)
.await;
}
@ -230,7 +225,7 @@ async fn post_events_collect(data: Json<Vec<EventCollection>>, headers: Headers,
Ok(())
}
pub async fn log_user_event(event_type: i32, user_id: &UserId, device_type: i32, ip: &IpAddr, conn: &mut DbConn) {
pub async fn log_user_event(event_type: i32, user_id: &UserId, device_type: i32, ip: &IpAddr, conn: &DbConn) {
if !CONFIG.org_events_enabled() {
return;
}
@ -243,7 +238,7 @@ async fn _log_user_event(
device_type: i32,
event_date: Option<NaiveDateTime>,
ip: &IpAddr,
conn: &mut DbConn,
conn: &DbConn,
) {
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
@ -278,7 +273,7 @@ pub async fn log_event(
act_user_id: &UserId,
device_type: i32,
ip: &IpAddr,
conn: &mut DbConn,
conn: &DbConn,
) {
if !CONFIG.org_events_enabled() {
return;
@ -295,7 +290,7 @@ async fn _log_event(
device_type: i32,
event_date: Option<NaiveDateTime>,
ip: &IpAddr,
conn: &mut DbConn,
conn: &DbConn,
) {
// Create a new empty event
let mut event = Event::new(event_type, event_date);
@ -340,8 +335,8 @@ pub async fn event_cleanup_job(pool: DbPool) {
return;
}
if let Ok(mut conn) = pool.get().await {
Event::clean_events(&mut conn).await.ok();
if let Ok(conn) = pool.get().await {
Event::clean_events(&conn).await.ok();
} else {
error!("Failed to get DB connection while trying to cleanup the events table")
}

35
src/api/core/folders.rs

@ -4,7 +4,10 @@ use serde_json::Value;
use crate::{
api::{EmptyResult, JsonResult, Notify, UpdateType},
auth::Headers,
db::{models::*, DbConn},
db::{
models::{Folder, FolderId},
DbConn,
},
};
pub fn routes() -> Vec<rocket::Route> {
@ -12,8 +15,8 @@ pub fn routes() -> Vec<rocket::Route> {
}
#[get("/folders")]
async fn get_folders(headers: Headers, mut conn: DbConn) -> Json<Value> {
let folders = Folder::find_by_user(&headers.user.uuid, &mut conn).await;
async fn get_folders(headers: Headers, conn: DbConn) -> Json<Value> {
let folders = Folder::find_by_user(&headers.user.uuid, &conn).await;
let folders_json: Vec<Value> = folders.iter().map(Folder::to_json).collect();
Json(json!({
@ -24,8 +27,8 @@ async fn get_folders(headers: Headers, mut conn: DbConn) -> Json<Value> {
}
#[get("/folders/<folder_id>")]
async fn get_folder(folder_id: FolderId, headers: Headers, mut conn: DbConn) -> JsonResult {
match Folder::find_by_uuid_and_user(&folder_id, &headers.user.uuid, &mut conn).await {
async fn get_folder(folder_id: FolderId, headers: Headers, conn: DbConn) -> JsonResult {
match Folder::find_by_uuid_and_user(&folder_id, &headers.user.uuid, &conn).await {
Some(folder) => Ok(Json(folder.to_json())),
_ => err!("Invalid folder", "Folder does not exist or belongs to another user"),
}
@ -39,13 +42,13 @@ pub struct FolderData {
}
#[post("/folders", data = "<data>")]
async fn post_folders(data: Json<FolderData>, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult {
async fn post_folders(data: Json<FolderData>, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult {
let data: FolderData = data.into_inner();
let mut folder = Folder::new(headers.user.uuid, data.name);
folder.save(&mut conn).await?;
nt.send_folder_update(UpdateType::SyncFolderCreate, &folder, &headers.device, &mut conn).await;
folder.save(&conn).await?;
nt.send_folder_update(UpdateType::SyncFolderCreate, &folder, &headers.device, &conn).await;
Ok(Json(folder.to_json()))
}
@ -66,19 +69,19 @@ async fn put_folder(
folder_id: FolderId,
data: Json<FolderData>,
headers: Headers,
mut conn: DbConn,
conn: DbConn,
nt: Notify<'_>,
) -> JsonResult {
let data: FolderData = data.into_inner();
let Some(mut folder) = Folder::find_by_uuid_and_user(&folder_id, &headers.user.uuid, &mut conn).await else {
let Some(mut folder) = Folder::find_by_uuid_and_user(&folder_id, &headers.user.uuid, &conn).await else {
err!("Invalid folder", "Folder does not exist or belongs to another user")
};
folder.name = data.name;
folder.save(&mut conn).await?;
nt.send_folder_update(UpdateType::SyncFolderUpdate, &folder, &headers.device, &mut conn).await;
folder.save(&conn).await?;
nt.send_folder_update(UpdateType::SyncFolderUpdate, &folder, &headers.device, &conn).await;
Ok(Json(folder.to_json()))
}
@ -89,14 +92,14 @@ async fn delete_folder_post(folder_id: FolderId, headers: Headers, conn: DbConn,
}
#[delete("/folders/<folder_id>")]
async fn delete_folder(folder_id: FolderId, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
let Some(folder) = Folder::find_by_uuid_and_user(&folder_id, &headers.user.uuid, &mut conn).await else {
async fn delete_folder(folder_id: FolderId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult {
let Some(folder) = Folder::find_by_uuid_and_user(&folder_id, &headers.user.uuid, &conn).await else {
err!("Invalid folder", "Folder does not exist or belongs to another user")
};
// Delete the actual folder entry
folder.delete(&mut conn).await?;
folder.delete(&conn).await?;
nt.send_folder_update(UpdateType::SyncFolderDelete, &folder, &headers.device, &mut conn).await;
nt.send_folder_update(UpdateType::SyncFolderDelete, &folder, &headers.device, &conn).await;
Ok(())
}

18
src/api/core/mod.rs

@ -52,7 +52,10 @@ use rocket::{serde::json::Json, serde::json::Value, Catcher, Route};
use crate::{
api::{EmptyResult, JsonResult, Notify, UpdateType},
auth::Headers,
db::{models::*, DbConn},
db::{
models::{Membership, MembershipStatus, MembershipType, OrgPolicy, OrgPolicyErr, Organization, User},
DbConn,
},
error::Error,
http_client::make_http_request,
mail,
@ -106,12 +109,7 @@ struct EquivDomainData {
}
#[post("/settings/domains", data = "<data>")]
async fn post_eq_domains(
data: Json<EquivDomainData>,
headers: Headers,
mut conn: DbConn,
nt: Notify<'_>,
) -> JsonResult {
async fn post_eq_domains(data: Json<EquivDomainData>, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult {
let data: EquivDomainData = data.into_inner();
let excluded_globals = data.excluded_global_equivalent_domains.unwrap_or_default();
@ -123,9 +121,9 @@ async fn post_eq_domains(
user.excluded_globals = to_string(&excluded_globals).unwrap_or_else(|_| "[]".to_string());
user.equivalent_domains = to_string(&equivalent_domains).unwrap_or_else(|_| "[]".to_string());
user.save(&mut conn).await?;
user.save(&conn).await?;
nt.send_user_update(UpdateType::SyncSettings, &user, &headers.device.push_uuid, &mut conn).await;
nt.send_user_update(UpdateType::SyncSettings, &user, &headers.device.push_uuid, &conn).await;
Ok(Json(json!({})))
}
@ -265,7 +263,7 @@ async fn accept_org_invite(
user: &User,
mut member: Membership,
reset_password_key: Option<String>,
conn: &mut DbConn,
conn: &DbConn,
) -> EmptyResult {
if member.status != MembershipStatus::Invited as i32 {
err!("User already accepted the invitation");

681
src/api/core/organizations.rs

File diff suppressed because it is too large

55
src/api/core/public.rs

@ -10,7 +10,13 @@ use std::collections::HashSet;
use crate::{
api::EmptyResult,
auth,
db::{models::*, DbConn},
db::{
models::{
Group, GroupUser, Invitation, Membership, MembershipStatus, MembershipType, Organization,
OrganizationApiKey, OrganizationId, User,
},
DbConn,
},
mail, CONFIG,
};
@ -44,7 +50,7 @@ struct OrgImportData {
}
#[post("/public/organization/import", data = "<data>")]
async fn ldap_import(data: Json<OrgImportData>, token: PublicToken, mut conn: DbConn) -> EmptyResult {
async fn ldap_import(data: Json<OrgImportData>, token: PublicToken, conn: DbConn) -> EmptyResult {
// Most of the logic for this function can be found here
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs#L1203
@ -55,13 +61,12 @@ async fn ldap_import(data: Json<OrgImportData>, token: PublicToken, mut conn: Db
let mut user_created: bool = false;
if user_data.deleted {
// If user is marked for deletion and it exists, revoke it
if let Some(mut member) = Membership::find_by_email_and_org(&user_data.email, &org_id, &mut conn).await {
if let Some(mut member) = Membership::find_by_email_and_org(&user_data.email, &org_id, &conn).await {
// Only revoke a user if it is not the last confirmed owner
let revoked = if member.atype == MembershipType::Owner
&& member.status == MembershipStatus::Confirmed as i32
{
if Membership::count_confirmed_by_org_and_type(&org_id, MembershipType::Owner, &mut conn).await <= 1
{
if Membership::count_confirmed_by_org_and_type(&org_id, MembershipType::Owner, &conn).await <= 1 {
warn!("Can't revoke the last owner");
false
} else {
@ -73,27 +78,27 @@ async fn ldap_import(data: Json<OrgImportData>, token: PublicToken, mut conn: Db
let ext_modified = member.set_external_id(Some(user_data.external_id.clone()));
if revoked || ext_modified {
member.save(&mut conn).await?;
member.save(&conn).await?;
}
}
// If user is part of the organization, restore it
} else if let Some(mut member) = Membership::find_by_email_and_org(&user_data.email, &org_id, &mut conn).await {
} else if let Some(mut member) = Membership::find_by_email_and_org(&user_data.email, &org_id, &conn).await {
let restored = member.restore();
let ext_modified = member.set_external_id(Some(user_data.external_id.clone()));
if restored || ext_modified {
member.save(&mut conn).await?;
member.save(&conn).await?;
}
} else {
// If user is not part of the organization
let user = match User::find_by_mail(&user_data.email, &mut conn).await {
let user = match User::find_by_mail(&user_data.email, &conn).await {
Some(user) => user, // exists in vaultwarden
None => {
// User does not exist yet
let mut new_user = User::new(user_data.email.clone(), None);
new_user.save(&mut conn).await?;
let mut new_user = User::new(&user_data.email, None);
new_user.save(&conn).await?;
if !CONFIG.mail_enabled() {
Invitation::new(&new_user.email).save(&mut conn).await?;
Invitation::new(&new_user.email).save(&conn).await?;
}
user_created = true;
new_user
@ -105,7 +110,7 @@ async fn ldap_import(data: Json<OrgImportData>, token: PublicToken, mut conn: Db
MembershipStatus::Accepted as i32 // Automatically mark user as accepted if no email invites
};
let (org_name, org_email) = match Organization::find_by_uuid(&org_id, &mut conn).await {
let (org_name, org_email) = match Organization::find_by_uuid(&org_id, &conn).await {
Some(org) => (org.name, org.billing_email),
None => err!("Error looking up organization"),
};
@ -116,7 +121,7 @@ async fn ldap_import(data: Json<OrgImportData>, token: PublicToken, mut conn: Db
new_member.atype = MembershipType::User as i32;
new_member.status = member_status;
new_member.save(&mut conn).await?;
new_member.save(&conn).await?;
if CONFIG.mail_enabled() {
if let Err(e) =
@ -124,9 +129,9 @@ async fn ldap_import(data: Json<OrgImportData>, token: PublicToken, mut conn: Db
{
// Upon error delete the user, invite and org member records when needed
if user_created {
user.delete(&mut conn).await?;
user.delete(&conn).await?;
} else {
new_member.delete(&mut conn).await?;
new_member.delete(&conn).await?;
}
err!(format!("Error sending invite: {e:?} "));
@ -137,8 +142,7 @@ async fn ldap_import(data: Json<OrgImportData>, token: PublicToken, mut conn: Db
if CONFIG.org_groups_enabled() {
for group_data in &data.groups {
let group_uuid = match Group::find_by_external_id_and_org(&group_data.external_id, &org_id, &mut conn).await
{
let group_uuid = match Group::find_by_external_id_and_org(&group_data.external_id, &org_id, &conn).await {
Some(group) => group.uuid,
None => {
let mut group = Group::new(
@ -147,17 +151,17 @@ async fn ldap_import(data: Json<OrgImportData>, token: PublicToken, mut conn: Db
false,
Some(group_data.external_id.clone()),
);
group.save(&mut conn).await?;
group.save(&conn).await?;
group.uuid
}
};
GroupUser::delete_all_by_group(&group_uuid, &mut conn).await?;
GroupUser::delete_all_by_group(&group_uuid, &conn).await?;
for ext_id in &group_data.member_external_ids {
if let Some(member) = Membership::find_by_external_id_and_org(ext_id, &org_id, &mut conn).await {
if let Some(member) = Membership::find_by_external_id_and_org(ext_id, &org_id, &conn).await {
let mut group_user = GroupUser::new(group_uuid.clone(), member.uuid.clone());
group_user.save(&mut conn).await?;
group_user.save(&conn).await?;
}
}
}
@ -169,19 +173,18 @@ async fn ldap_import(data: Json<OrgImportData>, token: PublicToken, mut conn: Db
if data.overwrite_existing {
// Generate a HashSet to quickly verify if a member is listed or not.
let sync_members: HashSet<String> = data.members.into_iter().map(|m| m.external_id).collect();
for member in Membership::find_by_org(&org_id, &mut conn).await {
for member in Membership::find_by_org(&org_id, &conn).await {
if let Some(ref user_external_id) = member.external_id {
if !sync_members.contains(user_external_id) {
if member.atype == MembershipType::Owner && member.status == MembershipStatus::Confirmed as i32 {
// Removing owner, check that there is at least one other confirmed owner
if Membership::count_confirmed_by_org_and_type(&org_id, MembershipType::Owner, &mut conn).await
<= 1
if Membership::count_confirmed_by_org_and_type(&org_id, MembershipType::Owner, &conn).await <= 1
{
warn!("Can't delete the last owner");
continue;
}
}
member.delete(&mut conn).await?;
member.delete(&conn).await?;
}
}
}

150
src/api/core/sends.rs

@ -1,26 +1,28 @@
use std::path::Path;
use std::time::Duration;
use std::{path::Path, sync::LazyLock, time::Duration};
use chrono::{DateTime, TimeDelta, Utc};
use num_traits::ToPrimitive;
use once_cell::sync::Lazy;
use rocket::form::Form;
use rocket::fs::NamedFile;
use rocket::fs::TempFile;
use rocket::serde::json::Json;
use rocket::{
form::Form,
fs::{NamedFile, TempFile},
serde::json::Json,
};
use serde_json::Value;
use crate::{
api::{ApiResult, EmptyResult, JsonResult, Notify, UpdateType},
auth::{ClientIp, Headers, Host},
config::PathType,
db::{models::*, DbConn, DbPool},
db::{
models::{Device, OrgPolicy, OrgPolicyType, Send, SendFileId, SendId, SendType, UserId},
DbConn, DbPool,
},
util::{save_temp_file, NumberOrString},
CONFIG,
};
const SEND_INACCESSIBLE_MSG: &str = "Send does not exist or is no longer available";
static ANON_PUSH_DEVICE: Lazy<Device> = Lazy::new(|| {
static ANON_PUSH_DEVICE: LazyLock<Device> = LazyLock::new(|| {
let dt = crate::util::parse_date("1970-01-01T00:00:00.000000Z");
Device {
uuid: String::from("00000000-0000-0000-0000-000000000000").into(),
@ -58,8 +60,8 @@ pub fn routes() -> Vec<rocket::Route> {
pub async fn purge_sends(pool: DbPool) {
debug!("Purging sends");
if let Ok(mut conn) = pool.get().await {
Send::purge(&mut conn).await;
if let Ok(conn) = pool.get().await {
Send::purge(&conn).await;
} else {
error!("Failed to get DB connection while purging sends")
}
@ -96,7 +98,7 @@ pub struct SendData {
///
/// There is also a Vaultwarden-specific `sends_allowed` config setting that
/// controls this policy globally.
async fn enforce_disable_send_policy(headers: &Headers, conn: &mut DbConn) -> EmptyResult {
async fn enforce_disable_send_policy(headers: &Headers, conn: &DbConn) -> EmptyResult {
let user_id = &headers.user.uuid;
if !CONFIG.sends_allowed()
|| OrgPolicy::is_applicable_to_user(user_id, OrgPolicyType::DisableSend, None, conn).await
@ -112,7 +114,7 @@ async fn enforce_disable_send_policy(headers: &Headers, conn: &mut DbConn) -> Em
/// but is allowed to remove this option from an existing Send.
///
/// Ref: https://bitwarden.com/help/article/policies/#send-options
async fn enforce_disable_hide_email_policy(data: &SendData, headers: &Headers, conn: &mut DbConn) -> EmptyResult {
async fn enforce_disable_hide_email_policy(data: &SendData, headers: &Headers, conn: &DbConn) -> EmptyResult {
let user_id = &headers.user.uuid;
let hide_email = data.hide_email.unwrap_or(false);
if hide_email && OrgPolicy::is_hide_email_disabled(user_id, conn).await {
@ -164,8 +166,8 @@ fn create_send(data: SendData, user_id: UserId) -> ApiResult<Send> {
}
#[get("/sends")]
async fn get_sends(headers: Headers, mut conn: DbConn) -> Json<Value> {
let sends = Send::find_by_user(&headers.user.uuid, &mut conn);
async fn get_sends(headers: Headers, conn: DbConn) -> Json<Value> {
let sends = Send::find_by_user(&headers.user.uuid, &conn);
let sends_json: Vec<Value> = sends.await.iter().map(|s| s.to_json()).collect();
Json(json!({
@ -176,32 +178,32 @@ async fn get_sends(headers: Headers, mut conn: DbConn) -> Json<Value> {
}
#[get("/sends/<send_id>")]
async fn get_send(send_id: SendId, headers: Headers, mut conn: DbConn) -> JsonResult {
match Send::find_by_uuid_and_user(&send_id, &headers.user.uuid, &mut conn).await {
async fn get_send(send_id: SendId, headers: Headers, conn: DbConn) -> JsonResult {
match Send::find_by_uuid_and_user(&send_id, &headers.user.uuid, &conn).await {
Some(send) => Ok(Json(send.to_json())),
None => err!("Send not found", "Invalid send uuid or does not belong to user"),
}
}
#[post("/sends", data = "<data>")]
async fn post_send(data: Json<SendData>, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult {
enforce_disable_send_policy(&headers, &mut conn).await?;
async fn post_send(data: Json<SendData>, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult {
enforce_disable_send_policy(&headers, &conn).await?;
let data: SendData = data.into_inner();
enforce_disable_hide_email_policy(&data, &headers, &mut conn).await?;
enforce_disable_hide_email_policy(&data, &headers, &conn).await?;
if data.r#type == SendType::File as i32 {
err!("File sends should use /api/sends/file")
}
let mut send = create_send(data, headers.user.uuid)?;
send.save(&mut conn).await?;
send.save(&conn).await?;
nt.send_send_update(
UpdateType::SyncSendCreate,
&send,
&send.update_users_revision(&mut conn).await,
&send.update_users_revision(&conn).await,
&headers.device,
&mut conn,
&conn,
)
.await;
@ -225,8 +227,8 @@ struct UploadDataV2<'f> {
// 2025: This endpoint doesn't seem to exists anymore in the latest version
// See: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Tools/Controllers/SendsController.cs
#[post("/sends/file", format = "multipart/form-data", data = "<data>")]
async fn post_send_file(data: Form<UploadData<'_>>, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult {
enforce_disable_send_policy(&headers, &mut conn).await?;
async fn post_send_file(data: Form<UploadData<'_>>, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult {
enforce_disable_send_policy(&headers, &conn).await?;
let UploadData {
model,
@ -241,12 +243,12 @@ async fn post_send_file(data: Form<UploadData<'_>>, headers: Headers, mut conn:
err!("Send size can't be negative")
}
enforce_disable_hide_email_policy(&model, &headers, &mut conn).await?;
enforce_disable_hide_email_policy(&model, &headers, &conn).await?;
let size_limit = match CONFIG.user_send_limit() {
Some(0) => err!("File uploads are disabled"),
Some(limit_kb) => {
let Some(already_used) = Send::size_by_user(&headers.user.uuid, &mut conn).await else {
let Some(already_used) = Send::size_by_user(&headers.user.uuid, &conn).await else {
err!("Existing sends overflow")
};
let Some(left) = limit_kb.checked_mul(1024).and_then(|l| l.checked_sub(already_used)) else {
@ -271,7 +273,7 @@ async fn post_send_file(data: Form<UploadData<'_>>, headers: Headers, mut conn:
let file_id = crate::crypto::generate_send_file_id();
save_temp_file(PathType::Sends, &format!("{}/{file_id}", send.uuid), data, true).await?;
save_temp_file(&PathType::Sends, &format!("{}/{file_id}", send.uuid), data, true).await?;
let mut data_value: Value = serde_json::from_str(&send.data)?;
if let Some(o) = data_value.as_object_mut() {
@ -282,13 +284,13 @@ async fn post_send_file(data: Form<UploadData<'_>>, headers: Headers, mut conn:
send.data = serde_json::to_string(&data_value)?;
// Save the changes in the database
send.save(&mut conn).await?;
send.save(&conn).await?;
nt.send_send_update(
UpdateType::SyncSendCreate,
&send,
&send.update_users_revision(&mut conn).await,
&send.update_users_revision(&conn).await,
&headers.device,
&mut conn,
&conn,
)
.await;
@ -297,8 +299,8 @@ async fn post_send_file(data: Form<UploadData<'_>>, headers: Headers, mut conn:
// Upstream: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Tools/Controllers/SendsController.cs#L165
#[post("/sends/file/v2", data = "<data>")]
async fn post_send_file_v2(data: Json<SendData>, headers: Headers, mut conn: DbConn) -> JsonResult {
enforce_disable_send_policy(&headers, &mut conn).await?;
async fn post_send_file_v2(data: Json<SendData>, headers: Headers, conn: DbConn) -> JsonResult {
enforce_disable_send_policy(&headers, &conn).await?;
let data = data.into_inner();
@ -306,7 +308,7 @@ async fn post_send_file_v2(data: Json<SendData>, headers: Headers, mut conn: DbC
err!("Send content is not a file");
}
enforce_disable_hide_email_policy(&data, &headers, &mut conn).await?;
enforce_disable_hide_email_policy(&data, &headers, &conn).await?;
let file_length = match &data.file_length {
Some(m) => m.into_i64()?,
@ -319,7 +321,7 @@ async fn post_send_file_v2(data: Json<SendData>, headers: Headers, mut conn: DbC
let size_limit = match CONFIG.user_send_limit() {
Some(0) => err!("File uploads are disabled"),
Some(limit_kb) => {
let Some(already_used) = Send::size_by_user(&headers.user.uuid, &mut conn).await else {
let Some(already_used) = Send::size_by_user(&headers.user.uuid, &conn).await else {
err!("Existing sends overflow")
};
let Some(left) = limit_kb.checked_mul(1024).and_then(|l| l.checked_sub(already_used)) else {
@ -348,7 +350,7 @@ async fn post_send_file_v2(data: Json<SendData>, headers: Headers, mut conn: DbC
o.insert(String::from("sizeName"), Value::String(crate::util::get_display_size(file_length)));
}
send.data = serde_json::to_string(&data_value)?;
send.save(&mut conn).await?;
send.save(&conn).await?;
Ok(Json(json!({
"fileUploadType": 0, // 0 == Direct | 1 == Azure
@ -373,14 +375,14 @@ async fn post_send_file_v2_data(
file_id: SendFileId,
data: Form<UploadDataV2<'_>>,
headers: Headers,
mut conn: DbConn,
conn: DbConn,
nt: Notify<'_>,
) -> EmptyResult {
enforce_disable_send_policy(&headers, &mut conn).await?;
enforce_disable_send_policy(&headers, &conn).await?;
let data = data.into_inner();
let Some(send) = Send::find_by_uuid_and_user(&send_id, &headers.user.uuid, &mut conn).await else {
let Some(send) = Send::find_by_uuid_and_user(&send_id, &headers.user.uuid, &conn).await else {
err!("Send not found. Unable to save the file.", "Invalid send uuid or does not belong to user.")
};
@ -423,14 +425,14 @@ async fn post_send_file_v2_data(
let file_path = format!("{send_id}/{file_id}");
save_temp_file(PathType::Sends, &file_path, data.data, false).await?;
save_temp_file(&PathType::Sends, &file_path, data.data, false).await?;
nt.send_send_update(
UpdateType::SyncSendCreate,
&send,
&send.update_users_revision(&mut conn).await,
&send.update_users_revision(&conn).await,
&headers.device,
&mut conn,
&conn,
)
.await;
@ -447,11 +449,11 @@ pub struct SendAccessData {
async fn post_access(
access_id: &str,
data: Json<SendAccessData>,
mut conn: DbConn,
conn: DbConn,
ip: ClientIp,
nt: Notify<'_>,
) -> JsonResult {
let Some(mut send) = Send::find_by_access_id(access_id, &mut conn).await else {
let Some(mut send) = Send::find_by_access_id(access_id, &conn).await else {
err_code!(SEND_INACCESSIBLE_MSG, 404)
};
@ -488,18 +490,18 @@ async fn post_access(
send.access_count += 1;
}
send.save(&mut conn).await?;
send.save(&conn).await?;
nt.send_send_update(
UpdateType::SyncSendUpdate,
&send,
&send.update_users_revision(&mut conn).await,
&send.update_users_revision(&conn).await,
&ANON_PUSH_DEVICE,
&mut conn,
&conn,
)
.await;
Ok(Json(send.to_json_access(&mut conn).await))
Ok(Json(send.to_json_access(&conn).await))
}
#[post("/sends/<send_id>/access/file/<file_id>", data = "<data>")]
@ -508,10 +510,10 @@ async fn post_access_file(
file_id: SendFileId,
data: Json<SendAccessData>,
host: Host,
mut conn: DbConn,
conn: DbConn,
nt: Notify<'_>,
) -> JsonResult {
let Some(mut send) = Send::find_by_uuid(&send_id, &mut conn).await else {
let Some(mut send) = Send::find_by_uuid(&send_id, &conn).await else {
err_code!(SEND_INACCESSIBLE_MSG, 404)
};
@ -545,14 +547,14 @@ async fn post_access_file(
send.access_count += 1;
send.save(&mut conn).await?;
send.save(&conn).await?;
nt.send_send_update(
UpdateType::SyncSendUpdate,
&send,
&send.update_users_revision(&mut conn).await,
&send.update_users_revision(&conn).await,
&ANON_PUSH_DEVICE,
&mut conn,
&conn,
)
.await;
@ -564,7 +566,7 @@ async fn post_access_file(
}
async fn download_url(host: &Host, send_id: &SendId, file_id: &SendFileId) -> Result<String, crate::Error> {
let operator = CONFIG.opendal_operator_for_path_type(PathType::Sends)?;
let operator = CONFIG.opendal_operator_for_path_type(&PathType::Sends)?;
if operator.info().scheme() == opendal::Scheme::Fs {
let token_claims = crate::auth::generate_send_claims(send_id, file_id);
@ -587,23 +589,17 @@ async fn download_send(send_id: SendId, file_id: SendFileId, t: &str) -> Option<
}
#[put("/sends/<send_id>", data = "<data>")]
async fn put_send(
send_id: SendId,
data: Json<SendData>,
headers: Headers,
mut conn: DbConn,
nt: Notify<'_>,
) -> JsonResult {
enforce_disable_send_policy(&headers, &mut conn).await?;
async fn put_send(send_id: SendId, data: Json<SendData>, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult {
enforce_disable_send_policy(&headers, &conn).await?;
let data: SendData = data.into_inner();
enforce_disable_hide_email_policy(&data, &headers, &mut conn).await?;
enforce_disable_hide_email_policy(&data, &headers, &conn).await?;
let Some(mut send) = Send::find_by_uuid_and_user(&send_id, &headers.user.uuid, &mut conn).await else {
let Some(mut send) = Send::find_by_uuid_and_user(&send_id, &headers.user.uuid, &conn).await else {
err!("Send not found", "Send send_id is invalid or does not belong to user")
};
update_send_from_data(&mut send, data, &headers, &mut conn, &nt, UpdateType::SyncSendUpdate).await?;
update_send_from_data(&mut send, data, &headers, &conn, &nt, UpdateType::SyncSendUpdate).await?;
Ok(Json(send.to_json()))
}
@ -612,7 +608,7 @@ pub async fn update_send_from_data(
send: &mut Send,
data: SendData,
headers: &Headers,
conn: &mut DbConn,
conn: &DbConn,
nt: &Notify<'_>,
ut: UpdateType,
) -> EmptyResult {
@ -667,18 +663,18 @@ pub async fn update_send_from_data(
}
#[delete("/sends/<send_id>")]
async fn delete_send(send_id: SendId, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
let Some(send) = Send::find_by_uuid_and_user(&send_id, &headers.user.uuid, &mut conn).await else {
async fn delete_send(send_id: SendId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult {
let Some(send) = Send::find_by_uuid_and_user(&send_id, &headers.user.uuid, &conn).await else {
err!("Send not found", "Invalid send uuid, or does not belong to user")
};
send.delete(&mut conn).await?;
send.delete(&conn).await?;
nt.send_send_update(
UpdateType::SyncSendDelete,
&send,
&send.update_users_revision(&mut conn).await,
&send.update_users_revision(&conn).await,
&headers.device,
&mut conn,
&conn,
)
.await;
@ -686,21 +682,21 @@ async fn delete_send(send_id: SendId, headers: Headers, mut conn: DbConn, nt: No
}
#[put("/sends/<send_id>/remove-password")]
async fn put_remove_password(send_id: SendId, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult {
enforce_disable_send_policy(&headers, &mut conn).await?;
async fn put_remove_password(send_id: SendId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult {
enforce_disable_send_policy(&headers, &conn).await?;
let Some(mut send) = Send::find_by_uuid_and_user(&send_id, &headers.user.uuid, &mut conn).await else {
let Some(mut send) = Send::find_by_uuid_and_user(&send_id, &headers.user.uuid, &conn).await else {
err!("Send not found", "Invalid send uuid, or does not belong to user")
};
send.set_password(None);
send.save(&mut conn).await?;
send.save(&conn).await?;
nt.send_send_update(
UpdateType::SyncSendUpdate,
&send,
&send.update_users_revision(&mut conn).await,
&send.update_users_revision(&conn).await,
&headers.device,
&mut conn,
&conn,
)
.await;

42
src/api/core/two_factor/authenticator.rs

@ -20,18 +20,18 @@ pub fn routes() -> Vec<Route> {
}
#[post("/two-factor/get-authenticator", data = "<data>")]
async fn generate_authenticator(data: Json<PasswordOrOtpData>, headers: Headers, mut conn: DbConn) -> JsonResult {
async fn generate_authenticator(data: Json<PasswordOrOtpData>, headers: Headers, conn: DbConn) -> JsonResult {
let data: PasswordOrOtpData = data.into_inner();
let user = headers.user;
data.validate(&user, false, &mut conn).await?;
data.validate(&user, false, &conn).await?;
let type_ = TwoFactorType::Authenticator as i32;
let twofactor = TwoFactor::find_by_user_and_type(&user.uuid, type_, &mut conn).await;
let twofactor = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn).await;
let (enabled, key) = match twofactor {
Some(tf) => (true, tf.data),
_ => (false, crypto::encode_random_bytes::<20>(BASE32)),
_ => (false, crypto::encode_random_bytes::<20>(&BASE32)),
};
// Upstream seems to also return `userVerificationToken`, but doesn't seem to be used at all.
@ -55,7 +55,7 @@ struct EnableAuthenticatorData {
}
#[post("/two-factor/authenticator", data = "<data>")]
async fn activate_authenticator(data: Json<EnableAuthenticatorData>, headers: Headers, mut conn: DbConn) -> JsonResult {
async fn activate_authenticator(data: Json<EnableAuthenticatorData>, headers: Headers, conn: DbConn) -> JsonResult {
let data: EnableAuthenticatorData = data.into_inner();
let key = data.key;
let token = data.token.into_string();
@ -66,7 +66,7 @@ async fn activate_authenticator(data: Json<EnableAuthenticatorData>, headers: He
master_password_hash: data.master_password_hash,
otp: data.otp,
}
.validate(&user, true, &mut conn)
.validate(&user, true, &conn)
.await?;
// Validate key as base32 and 20 bytes length
@ -80,11 +80,11 @@ async fn activate_authenticator(data: Json<EnableAuthenticatorData>, headers: He
}
// Validate the token provided with the key, and save new twofactor
validate_totp_code(&user.uuid, &token, &key.to_uppercase(), &headers.ip, &mut conn).await?;
validate_totp_code(&user.uuid, &token, &key.to_uppercase(), &headers.ip, &conn).await?;
_generate_recover_code(&mut user, &mut conn).await;
_generate_recover_code(&mut user, &conn).await;
log_user_event(EventType::UserUpdated2fa as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &mut conn).await;
log_user_event(EventType::UserUpdated2fa as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &conn).await;
Ok(Json(json!({
"enabled": true,
@ -103,7 +103,7 @@ pub async fn validate_totp_code_str(
totp_code: &str,
secret: &str,
ip: &ClientIp,
conn: &mut DbConn,
conn: &DbConn,
) -> EmptyResult {
if !totp_code.chars().all(char::is_numeric) {
err!("TOTP code is not a number");
@ -117,7 +117,7 @@ pub async fn validate_totp_code(
totp_code: &str,
secret: &str,
ip: &ClientIp,
conn: &mut DbConn,
conn: &DbConn,
) -> EmptyResult {
use totp_lite::{totp_custom, Sha1};
@ -189,7 +189,7 @@ struct DisableAuthenticatorData {
}
#[delete("/two-factor/authenticator", data = "<data>")]
async fn disable_authenticator(data: Json<DisableAuthenticatorData>, headers: Headers, mut conn: DbConn) -> JsonResult {
async fn disable_authenticator(data: Json<DisableAuthenticatorData>, headers: Headers, conn: DbConn) -> JsonResult {
let user = headers.user;
let type_ = data.r#type.into_i32()?;
@ -197,24 +197,18 @@ async fn disable_authenticator(data: Json<DisableAuthenticatorData>, headers: He
err!("Invalid password");
}
if let Some(twofactor) = TwoFactor::find_by_user_and_type(&user.uuid, type_, &mut conn).await {
if let Some(twofactor) = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn).await {
if twofactor.data == data.key {
twofactor.delete(&mut conn).await?;
log_user_event(
EventType::UserDisabled2fa as i32,
&user.uuid,
headers.device.atype,
&headers.ip.ip,
&mut conn,
)
.await;
twofactor.delete(&conn).await?;
log_user_event(EventType::UserDisabled2fa as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &conn)
.await;
} else {
err!(format!("TOTP key for user {} does not match recorded value, cannot deactivate", &user.email));
}
}
if TwoFactor::find_by_user(&user.uuid, &mut conn).await.is_empty() {
super::enforce_2fa_policy(&user, &user.uuid, headers.device.atype, &headers.ip.ip, &mut conn).await?;
if TwoFactor::find_by_user(&user.uuid, &conn).await.is_empty() {
super::enforce_2fa_policy(&user, &user.uuid, headers.device.atype, &headers.ip.ip, &conn).await?;
}
Ok(Json(json!({

24
src/api/core/two_factor/duo.rs

@ -92,13 +92,13 @@ impl DuoStatus {
const DISABLED_MESSAGE_DEFAULT: &str = "<To use the global Duo keys, please leave these fields untouched>";
#[post("/two-factor/get-duo", data = "<data>")]
async fn get_duo(data: Json<PasswordOrOtpData>, headers: Headers, mut conn: DbConn) -> JsonResult {
async fn get_duo(data: Json<PasswordOrOtpData>, headers: Headers, conn: DbConn) -> JsonResult {
let data: PasswordOrOtpData = data.into_inner();
let user = headers.user;
data.validate(&user, false, &mut conn).await?;
data.validate(&user, false, &conn).await?;
let data = get_user_duo_data(&user.uuid, &mut conn).await;
let data = get_user_duo_data(&user.uuid, &conn).await;
let (enabled, data) = match data {
DuoStatus::Global(_) => (true, Some(DuoData::secret())),
@ -158,7 +158,7 @@ fn check_duo_fields_custom(data: &EnableDuoData) -> bool {
}
#[post("/two-factor/duo", data = "<data>")]
async fn activate_duo(data: Json<EnableDuoData>, headers: Headers, mut conn: DbConn) -> JsonResult {
async fn activate_duo(data: Json<EnableDuoData>, headers: Headers, conn: DbConn) -> JsonResult {
let data: EnableDuoData = data.into_inner();
let mut user = headers.user;
@ -166,7 +166,7 @@ async fn activate_duo(data: Json<EnableDuoData>, headers: Headers, mut conn: DbC
master_password_hash: data.master_password_hash.clone(),
otp: data.otp.clone(),
}
.validate(&user, true, &mut conn)
.validate(&user, true, &conn)
.await?;
let (data, data_str) = if check_duo_fields_custom(&data) {
@ -180,11 +180,11 @@ async fn activate_duo(data: Json<EnableDuoData>, headers: Headers, mut conn: DbC
let type_ = TwoFactorType::Duo;
let twofactor = TwoFactor::new(user.uuid.clone(), type_, data_str);
twofactor.save(&mut conn).await?;
twofactor.save(&conn).await?;
_generate_recover_code(&mut user, &mut conn).await;
_generate_recover_code(&mut user, &conn).await;
log_user_event(EventType::UserUpdated2fa as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &mut conn).await;
log_user_event(EventType::UserUpdated2fa as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &conn).await;
Ok(Json(json!({
"enabled": true,
@ -231,7 +231,7 @@ const AUTH_PREFIX: &str = "AUTH";
const DUO_PREFIX: &str = "TX";
const APP_PREFIX: &str = "APP";
async fn get_user_duo_data(user_id: &UserId, conn: &mut DbConn) -> DuoStatus {
async fn get_user_duo_data(user_id: &UserId, conn: &DbConn) -> DuoStatus {
let type_ = TwoFactorType::Duo as i32;
// If the user doesn't have an entry, disabled
@ -254,7 +254,7 @@ async fn get_user_duo_data(user_id: &UserId, conn: &mut DbConn) -> DuoStatus {
}
// let (ik, sk, ak, host) = get_duo_keys();
pub(crate) async fn get_duo_keys_email(email: &str, conn: &mut DbConn) -> ApiResult<(String, String, String, String)> {
pub(crate) async fn get_duo_keys_email(email: &str, conn: &DbConn) -> ApiResult<(String, String, String, String)> {
let data = match User::find_by_mail(email, conn).await {
Some(u) => get_user_duo_data(&u.uuid, conn).await.data(),
_ => DuoData::global(),
@ -264,7 +264,7 @@ pub(crate) async fn get_duo_keys_email(email: &str, conn: &mut DbConn) -> ApiRes
Ok((data.ik, data.sk, CONFIG.get_duo_akey().await, data.host))
}
pub async fn generate_duo_signature(email: &str, conn: &mut DbConn) -> ApiResult<(String, String)> {
pub async fn generate_duo_signature(email: &str, conn: &DbConn) -> ApiResult<(String, String)> {
let now = Utc::now().timestamp();
let (ik, sk, ak, host) = get_duo_keys_email(email, conn).await?;
@ -282,7 +282,7 @@ fn sign_duo_values(key: &str, email: &str, ikey: &str, prefix: &str, expire: i64
format!("{cookie}|{}", crypto::hmac_sign(key, &cookie))
}
pub async fn validate_duo_login(email: &str, response: &str, conn: &mut DbConn) -> EmptyResult {
pub async fn validate_duo_login(email: &str, response: &str, conn: &DbConn) -> EmptyResult {
let split: Vec<&str> = response.split(':').collect();
if split.len() != 2 {
err!(

10
src/api/core/two_factor/duo_oidc.rs

@ -317,7 +317,7 @@ struct DuoAuthContext {
// Given a state string, retrieve the associated Duo auth context and
// delete the retrieved state from the database.
async fn extract_context(state: &str, conn: &mut DbConn) -> Option<DuoAuthContext> {
async fn extract_context(state: &str, conn: &DbConn) -> Option<DuoAuthContext> {
let ctx: TwoFactorDuoContext = match TwoFactorDuoContext::find_by_state(state, conn).await {
Some(c) => c,
None => return None,
@ -344,8 +344,8 @@ async fn extract_context(state: &str, conn: &mut DbConn) -> Option<DuoAuthContex
// Task to clean up expired Duo authentication contexts that may have accumulated in the database.
pub async fn purge_duo_contexts(pool: DbPool) {
debug!("Purging Duo authentication contexts");
if let Ok(mut conn) = pool.get().await {
TwoFactorDuoContext::purge_expired_duo_contexts(&mut conn).await;
if let Ok(conn) = pool.get().await {
TwoFactorDuoContext::purge_expired_duo_contexts(&conn).await;
} else {
error!("Failed to get DB connection while purging expired Duo authentications")
}
@ -380,7 +380,7 @@ pub async fn get_duo_auth_url(
email: &str,
client_id: &str,
device_identifier: &DeviceId,
conn: &mut DbConn,
conn: &DbConn,
) -> Result<String, Error> {
let (ik, sk, _, host) = get_duo_keys_email(email, conn).await?;
@ -418,7 +418,7 @@ pub async fn validate_duo_login(
two_factor_token: &str,
client_id: &str,
device_identifier: &DeviceId,
conn: &mut DbConn,
conn: &DbConn,
) -> EmptyResult {
// Result supplied to us by clients in the form "<authz code>|<state>"
let split: Vec<&str> = two_factor_token.split('|').collect();

44
src/api/core/two_factor/email.rs

@ -39,13 +39,13 @@ struct SendEmailLoginData {
/// User is trying to login and wants to use email 2FA.
/// Does not require Bearer token
#[post("/two-factor/send-email-login", data = "<data>")] // JsonResult
async fn send_email_login(data: Json<SendEmailLoginData>, mut conn: DbConn) -> EmptyResult {
async fn send_email_login(data: Json<SendEmailLoginData>, conn: DbConn) -> EmptyResult {
let data: SendEmailLoginData = data.into_inner();
use crate::db::models::User;
// Get the user
let Some(user) = User::find_by_device_id(&data.device_identifier, &mut conn).await else {
let Some(user) = User::find_by_device_id(&data.device_identifier, &conn).await else {
err!("Cannot find user. Try again.")
};
@ -53,13 +53,13 @@ async fn send_email_login(data: Json<SendEmailLoginData>, mut conn: DbConn) -> E
err!("Email 2FA is disabled")
}
send_token(&user.uuid, &mut conn).await?;
send_token(&user.uuid, &conn).await?;
Ok(())
}
/// Generate the token, save the data for later verification and send email to user
pub async fn send_token(user_id: &UserId, conn: &mut DbConn) -> EmptyResult {
pub async fn send_token(user_id: &UserId, conn: &DbConn) -> EmptyResult {
let type_ = TwoFactorType::Email as i32;
let mut twofactor = TwoFactor::find_by_user_and_type(user_id, type_, conn).await.map_res("Two factor not found")?;
@ -77,14 +77,14 @@ pub async fn send_token(user_id: &UserId, conn: &mut DbConn) -> EmptyResult {
/// When user clicks on Manage email 2FA show the user the related information
#[post("/two-factor/get-email", data = "<data>")]
async fn get_email(data: Json<PasswordOrOtpData>, headers: Headers, mut conn: DbConn) -> JsonResult {
async fn get_email(data: Json<PasswordOrOtpData>, headers: Headers, conn: DbConn) -> JsonResult {
let data: PasswordOrOtpData = data.into_inner();
let user = headers.user;
data.validate(&user, false, &mut conn).await?;
data.validate(&user, false, &conn).await?;
let (enabled, mfa_email) =
match TwoFactor::find_by_user_and_type(&user.uuid, TwoFactorType::Email as i32, &mut conn).await {
match TwoFactor::find_by_user_and_type(&user.uuid, TwoFactorType::Email as i32, &conn).await {
Some(x) => {
let twofactor_data = EmailTokenData::from_json(&x.data)?;
(true, json!(twofactor_data.email))
@ -110,7 +110,7 @@ struct SendEmailData {
/// Send a verification email to the specified email address to check whether it exists/belongs to user.
#[post("/two-factor/send-email", data = "<data>")]
async fn send_email(data: Json<SendEmailData>, headers: Headers, mut conn: DbConn) -> EmptyResult {
async fn send_email(data: Json<SendEmailData>, headers: Headers, conn: DbConn) -> EmptyResult {
let data: SendEmailData = data.into_inner();
let user = headers.user;
@ -118,7 +118,7 @@ async fn send_email(data: Json<SendEmailData>, headers: Headers, mut conn: DbCon
master_password_hash: data.master_password_hash,
otp: data.otp,
}
.validate(&user, false, &mut conn)
.validate(&user, false, &conn)
.await?;
if !CONFIG._enable_email_2fa() {
@ -127,8 +127,8 @@ async fn send_email(data: Json<SendEmailData>, headers: Headers, mut conn: DbCon
let type_ = TwoFactorType::Email as i32;
if let Some(tf) = TwoFactor::find_by_user_and_type(&user.uuid, type_, &mut conn).await {
tf.delete(&mut conn).await?;
if let Some(tf) = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn).await {
tf.delete(&conn).await?;
}
let generated_token = crypto::generate_email_token(CONFIG.email_token_size());
@ -136,7 +136,7 @@ async fn send_email(data: Json<SendEmailData>, headers: Headers, mut conn: DbCon
// Uses EmailVerificationChallenge as type to show that it's not verified yet.
let twofactor = TwoFactor::new(user.uuid, TwoFactorType::EmailVerificationChallenge, twofactor_data.to_json());
twofactor.save(&mut conn).await?;
twofactor.save(&conn).await?;
mail::send_token(&twofactor_data.email, &twofactor_data.last_token.map_res("Token is empty")?).await?;
@ -154,7 +154,7 @@ struct EmailData {
/// Verify email belongs to user and can be used for 2FA email codes.
#[put("/two-factor/email", data = "<data>")]
async fn email(data: Json<EmailData>, headers: Headers, mut conn: DbConn) -> JsonResult {
async fn email(data: Json<EmailData>, headers: Headers, conn: DbConn) -> JsonResult {
let data: EmailData = data.into_inner();
let mut user = headers.user;
@ -163,12 +163,12 @@ async fn email(data: Json<EmailData>, headers: Headers, mut conn: DbConn) -> Jso
master_password_hash: data.master_password_hash,
otp: data.otp,
}
.validate(&user, true, &mut conn)
.validate(&user, true, &conn)
.await?;
let type_ = TwoFactorType::EmailVerificationChallenge as i32;
let mut twofactor =
TwoFactor::find_by_user_and_type(&user.uuid, type_, &mut conn).await.map_res("Two factor not found")?;
TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn).await.map_res("Two factor not found")?;
let mut email_data = EmailTokenData::from_json(&twofactor.data)?;
@ -183,11 +183,11 @@ async fn email(data: Json<EmailData>, headers: Headers, mut conn: DbConn) -> Jso
email_data.reset_token();
twofactor.atype = TwoFactorType::Email as i32;
twofactor.data = email_data.to_json();
twofactor.save(&mut conn).await?;
twofactor.save(&conn).await?;
_generate_recover_code(&mut user, &mut conn).await;
_generate_recover_code(&mut user, &conn).await;
log_user_event(EventType::UserUpdated2fa as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &mut conn).await;
log_user_event(EventType::UserUpdated2fa as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &conn).await;
Ok(Json(json!({
"email": email_data.email,
@ -202,7 +202,7 @@ pub async fn validate_email_code_str(
token: &str,
data: &str,
ip: &std::net::IpAddr,
conn: &mut DbConn,
conn: &DbConn,
) -> EmptyResult {
let mut email_data = EmailTokenData::from_json(data)?;
let mut twofactor = TwoFactor::find_by_user_and_type(user_id, TwoFactorType::Email as i32, conn)
@ -286,7 +286,7 @@ impl EmailTokenData {
}
pub fn add_attempt(&mut self) {
self.attempts += 1;
self.attempts = self.attempts.saturating_add(1);
}
pub fn to_json(&self) -> String {
@ -302,7 +302,7 @@ impl EmailTokenData {
}
}
pub async fn activate_email_2fa(user: &User, conn: &mut DbConn) -> EmptyResult {
pub async fn activate_email_2fa(user: &User, conn: &DbConn) -> EmptyResult {
if user.verified_at.is_none() {
err!("Auto-enabling of email 2FA failed because the users email address has not been verified!");
}
@ -332,7 +332,7 @@ pub fn obscure_email(email: &str) -> String {
format!("{new_name}@{domain}")
}
pub async fn find_and_activate_email_2fa(user_id: &UserId, conn: &mut DbConn) -> EmptyResult {
pub async fn find_and_activate_email_2fa(user_id: &UserId, conn: &DbConn) -> EmptyResult {
if let Some(user) = User::find_by_uuid(user_id, conn).await {
activate_email_2fa(&user, conn).await
} else {

58
src/api/core/two_factor/mod.rs

@ -11,7 +11,13 @@ use crate::{
},
auth::{ClientHeaders, Headers},
crypto,
db::{models::*, DbConn, DbPool},
db::{
models::{
DeviceType, EventType, Membership, MembershipType, OrgPolicyType, Organization, OrganizationId, TwoFactor,
TwoFactorIncomplete, User, UserId,
},
DbConn, DbPool,
},
mail,
util::NumberOrString,
CONFIG,
@ -46,8 +52,8 @@ pub fn routes() -> Vec<Route> {
}
#[get("/two-factor")]
async fn get_twofactor(headers: Headers, mut conn: DbConn) -> Json<Value> {
let twofactors = TwoFactor::find_by_user(&headers.user.uuid, &mut conn).await;
async fn get_twofactor(headers: Headers, conn: DbConn) -> Json<Value> {
let twofactors = TwoFactor::find_by_user(&headers.user.uuid, &conn).await;
let twofactors_json: Vec<Value> = twofactors.iter().map(TwoFactor::to_json_provider).collect();
Json(json!({
@ -58,11 +64,11 @@ async fn get_twofactor(headers: Headers, mut conn: DbConn) -> Json<Value> {
}
#[post("/two-factor/get-recover", data = "<data>")]
async fn get_recover(data: Json<PasswordOrOtpData>, headers: Headers, mut conn: DbConn) -> JsonResult {
async fn get_recover(data: Json<PasswordOrOtpData>, headers: Headers, conn: DbConn) -> JsonResult {
let data: PasswordOrOtpData = data.into_inner();
let user = headers.user;
data.validate(&user, true, &mut conn).await?;
data.validate(&user, true, &conn).await?;
Ok(Json(json!({
"code": user.totp_recover,
@ -79,13 +85,13 @@ struct RecoverTwoFactor {
}
#[post("/two-factor/recover", data = "<data>")]
async fn recover(data: Json<RecoverTwoFactor>, client_headers: ClientHeaders, mut conn: DbConn) -> JsonResult {
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, &mut conn).await else {
let Some(mut user) = User::find_by_mail(&data.email, &conn).await else {
err!("Username or password is incorrect. Try again.")
};
@ -100,27 +106,27 @@ async fn recover(data: Json<RecoverTwoFactor>, client_headers: ClientHeaders, mu
}
// Remove all twofactors from the user
TwoFactor::delete_all_by_user(&user.uuid, &mut conn).await?;
enforce_2fa_policy(&user, &user.uuid, client_headers.device_type, &client_headers.ip.ip, &mut conn).await?;
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,
&mut conn,
&conn,
)
.await;
// Remove the recovery code, not needed without twofactors
user.totp_recover = None;
user.save(&mut conn).await?;
user.save(&conn).await?;
Ok(Json(Value::Object(serde_json::Map::new())))
}
async fn _generate_recover_code(user: &mut User, conn: &mut DbConn) {
async fn _generate_recover_code(user: &mut User, conn: &DbConn) {
if user.totp_recover.is_none() {
let totp_recover = crypto::encode_random_bytes::<20>(BASE32);
let totp_recover = crypto::encode_random_bytes::<20>(&BASE32);
user.totp_recover = Some(totp_recover);
user.save(conn).await.ok();
}
@ -135,7 +141,7 @@ struct DisableTwoFactorData {
}
#[post("/two-factor/disable", data = "<data>")]
async fn disable_twofactor(data: Json<DisableTwoFactorData>, headers: Headers, mut conn: DbConn) -> JsonResult {
async fn disable_twofactor(data: Json<DisableTwoFactorData>, headers: Headers, conn: DbConn) -> JsonResult {
let data: DisableTwoFactorData = data.into_inner();
let user = headers.user;
@ -144,19 +150,19 @@ async fn disable_twofactor(data: Json<DisableTwoFactorData>, headers: Headers, m
master_password_hash: data.master_password_hash,
otp: data.otp,
}
.validate(&user, true, &mut conn)
.validate(&user, true, &conn)
.await?;
let type_ = data.r#type.into_i32()?;
if let Some(twofactor) = TwoFactor::find_by_user_and_type(&user.uuid, type_, &mut conn).await {
twofactor.delete(&mut conn).await?;
log_user_event(EventType::UserDisabled2fa as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &mut conn)
if let Some(twofactor) = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn).await {
twofactor.delete(&conn).await?;
log_user_event(EventType::UserDisabled2fa as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &conn)
.await;
}
if TwoFactor::find_by_user(&user.uuid, &mut conn).await.is_empty() {
enforce_2fa_policy(&user, &user.uuid, headers.device.atype, &headers.ip.ip, &mut conn).await?;
if TwoFactor::find_by_user(&user.uuid, &conn).await.is_empty() {
enforce_2fa_policy(&user, &user.uuid, headers.device.atype, &headers.ip.ip, &conn).await?;
}
Ok(Json(json!({
@ -176,7 +182,7 @@ pub async fn enforce_2fa_policy(
act_user_id: &UserId,
device_type: i32,
ip: &std::net::IpAddr,
conn: &mut DbConn,
conn: &DbConn,
) -> EmptyResult {
for member in
Membership::find_by_user_and_policy(&user.uuid, OrgPolicyType::TwoFactorAuthentication, conn).await.into_iter()
@ -212,7 +218,7 @@ pub async fn enforce_2fa_policy_for_org(
act_user_id: &UserId,
device_type: i32,
ip: &std::net::IpAddr,
conn: &mut DbConn,
conn: &DbConn,
) -> EmptyResult {
let org = Organization::find_by_uuid(org_id, conn).await.unwrap();
for member in Membership::find_confirmed_by_org(org_id, conn).await.into_iter() {
@ -249,7 +255,7 @@ pub async fn send_incomplete_2fa_notifications(pool: DbPool) {
return;
}
let mut conn = match pool.get().await {
let conn = match pool.get().await {
Ok(conn) => conn,
_ => {
error!("Failed to get DB connection in send_incomplete_2fa_notifications()");
@ -260,9 +266,9 @@ pub async fn send_incomplete_2fa_notifications(pool: DbPool) {
let now = Utc::now().naive_utc();
let time_limit = TimeDelta::try_minutes(CONFIG.incomplete_2fa_time_limit()).unwrap();
let time_before = now - time_limit;
let incomplete_logins = TwoFactorIncomplete::find_logins_before(&time_before, &mut conn).await;
let incomplete_logins = TwoFactorIncomplete::find_logins_before(&time_before, &conn).await;
for login in incomplete_logins {
let user = User::find_by_uuid(&login.user_uuid, &mut conn).await.expect("User not found");
let user = User::find_by_uuid(&login.user_uuid, &conn).await.expect("User not found");
info!(
"User {} did not complete a 2FA login within the configured time limit. IP: {}",
user.email, login.ip_address
@ -277,7 +283,7 @@ pub async fn send_incomplete_2fa_notifications(pool: DbPool) {
.await
{
Ok(_) => {
if let Err(e) = login.delete(&mut conn).await {
if let Err(e) = login.delete(&conn).await {
error!("Error deleting incomplete 2FA record: {e:#?}");
}
}

53
src/api/core/two_factor/protected_actions.rs

@ -1,4 +1,4 @@
use chrono::{DateTime, TimeDelta, Utc};
use chrono::{naive::serde::ts_seconds, NaiveDateTime, TimeDelta, Utc};
use rocket::{serde::json::Json, Route};
use crate::{
@ -23,16 +23,17 @@ pub struct ProtectedActionData {
/// Token issued to validate the protected action
pub token: String,
/// UNIX timestamp of token issue.
pub token_sent: i64,
#[serde(with = "ts_seconds")]
pub token_sent: NaiveDateTime,
// The total amount of attempts
pub attempts: u8,
pub attempts: u64,
}
impl ProtectedActionData {
pub fn new(token: String) -> Self {
Self {
token,
token_sent: Utc::now().timestamp(),
token_sent: Utc::now().naive_utc(),
attempts: 0,
}
}
@ -50,12 +51,16 @@ impl ProtectedActionData {
}
pub fn add_attempt(&mut self) {
self.attempts += 1;
self.attempts = self.attempts.saturating_add(1);
}
pub fn time_since_sent(&self) -> TimeDelta {
Utc::now().naive_utc() - self.token_sent
}
}
#[post("/accounts/request-otp")]
async fn request_otp(headers: Headers, mut conn: DbConn) -> EmptyResult {
async fn request_otp(headers: Headers, conn: DbConn) -> EmptyResult {
if !CONFIG.mail_enabled() {
err!("Email is disabled for this server. Either enable email or login using your master password instead of login via device.");
}
@ -63,10 +68,16 @@ async fn request_otp(headers: Headers, mut conn: DbConn) -> EmptyResult {
let user = headers.user;
// Only one Protected Action per user is allowed to take place, delete the previous one
if let Some(pa) =
TwoFactor::find_by_user_and_type(&user.uuid, TwoFactorType::ProtectedActions as i32, &mut conn).await
if let Some(pa) = TwoFactor::find_by_user_and_type(&user.uuid, TwoFactorType::ProtectedActions as i32, &conn).await
{
pa.delete(&mut conn).await?;
let pa_data = ProtectedActionData::from_json(&pa.data)?;
let elapsed = pa_data.time_since_sent().num_seconds();
let delay = 30;
if elapsed < delay {
err!(format!("Please wait {} seconds before requesting another code.", (delay - elapsed)));
}
pa.delete(&conn).await?;
}
let generated_token = crypto::generate_email_token(CONFIG.email_token_size());
@ -74,7 +85,7 @@ async fn request_otp(headers: Headers, mut conn: DbConn) -> EmptyResult {
// Uses EmailVerificationChallenge as type to show that it's not verified yet.
let twofactor = TwoFactor::new(user.uuid, TwoFactorType::ProtectedActions, pa_data.to_json());
twofactor.save(&mut conn).await?;
twofactor.save(&conn).await?;
mail::send_protected_action_token(&user.email, &pa_data.token).await?;
@ -89,7 +100,7 @@ struct ProtectedActionVerify {
}
#[post("/accounts/verify-otp", data = "<data>")]
async fn verify_otp(data: Json<ProtectedActionVerify>, headers: Headers, mut conn: DbConn) -> EmptyResult {
async fn verify_otp(data: Json<ProtectedActionVerify>, headers: Headers, conn: DbConn) -> EmptyResult {
if !CONFIG.mail_enabled() {
err!("Email is disabled for this server. Either enable email or login using your master password instead of login via device.");
}
@ -99,33 +110,31 @@ async fn verify_otp(data: Json<ProtectedActionVerify>, headers: Headers, mut con
// Delete the token after one validation attempt
// This endpoint only gets called for the vault export, and doesn't need a second attempt
validate_protected_action_otp(&data.otp, &user.uuid, true, &mut conn).await
validate_protected_action_otp(&data.otp, &user.uuid, true, &conn).await
}
pub async fn validate_protected_action_otp(
otp: &str,
user_id: &UserId,
delete_if_valid: bool,
conn: &mut DbConn,
conn: &DbConn,
) -> EmptyResult {
let pa = TwoFactor::find_by_user_and_type(user_id, TwoFactorType::ProtectedActions as i32, conn)
let mut pa = TwoFactor::find_by_user_and_type(user_id, TwoFactorType::ProtectedActions as i32, conn)
.await
.map_res("Protected action token not found, try sending the code again or restart the process")?;
let mut pa_data = ProtectedActionData::from_json(&pa.data)?;
pa_data.add_attempt();
// Delete the token after x attempts if it has been used too many times
// We use the 6, which should be more then enough for invalid attempts and multiple valid checks
if pa_data.attempts > 6 {
pa.delete(conn).await?;
pa.data = pa_data.to_json();
// Fail after x attempts if the token has been used too many times.
// Don't delete it, as we use it to keep track of attempts.
if pa_data.attempts >= CONFIG.email_attempts_limit() {
err!("Token has expired")
}
// Check if the token has expired (Using the email 2fa expiration time)
let date =
DateTime::from_timestamp(pa_data.token_sent, 0).expect("Protected Action token timestamp invalid.").naive_utc();
let max_time = CONFIG.email_expiration_time() as i64;
if date + TimeDelta::try_seconds(max_time).unwrap() < Utc::now().naive_utc() {
if pa_data.time_since_sent().num_seconds() > max_time {
pa.delete(conn).await?;
err!("Token has expired")
}

49
src/api/core/two_factor/webauthn.rs

@ -107,7 +107,7 @@ impl WebauthnRegistration {
}
#[post("/two-factor/get-webauthn", data = "<data>")]
async fn get_webauthn(data: Json<PasswordOrOtpData>, headers: Headers, mut conn: DbConn) -> JsonResult {
async fn get_webauthn(data: Json<PasswordOrOtpData>, headers: Headers, conn: DbConn) -> JsonResult {
if !CONFIG.domain_set() {
err!("`DOMAIN` environment variable is not set. Webauthn disabled")
}
@ -115,9 +115,9 @@ async fn get_webauthn(data: Json<PasswordOrOtpData>, headers: Headers, mut conn:
let data: PasswordOrOtpData = data.into_inner();
let user = headers.user;
data.validate(&user, false, &mut conn).await?;
data.validate(&user, false, &conn).await?;
let (enabled, registrations) = get_webauthn_registrations(&user.uuid, &mut conn).await?;
let (enabled, registrations) = get_webauthn_registrations(&user.uuid, &conn).await?;
let registrations_json: Vec<Value> = registrations.iter().map(WebauthnRegistration::to_json).collect();
Ok(Json(json!({
@ -128,13 +128,13 @@ async fn get_webauthn(data: Json<PasswordOrOtpData>, headers: Headers, mut conn:
}
#[post("/two-factor/get-webauthn-challenge", data = "<data>")]
async fn generate_webauthn_challenge(data: Json<PasswordOrOtpData>, headers: Headers, mut conn: DbConn) -> JsonResult {
async fn generate_webauthn_challenge(data: Json<PasswordOrOtpData>, headers: Headers, conn: DbConn) -> JsonResult {
let data: PasswordOrOtpData = data.into_inner();
let user = headers.user;
data.validate(&user, false, &mut conn).await?;
data.validate(&user, false, &conn).await?;
let registrations = get_webauthn_registrations(&user.uuid, &mut conn)
let registrations = get_webauthn_registrations(&user.uuid, &conn)
.await?
.1
.into_iter()
@ -153,7 +153,7 @@ async fn generate_webauthn_challenge(data: Json<PasswordOrOtpData>, headers: Hea
state["rs"]["extensions"].as_object_mut().unwrap().clear();
let type_ = TwoFactorType::WebauthnRegisterChallenge;
TwoFactor::new(user.uuid.clone(), type_, serde_json::to_string(&state)?).save(&mut conn).await?;
TwoFactor::new(user.uuid.clone(), type_, serde_json::to_string(&state)?).save(&conn).await?;
// Because for this flow we abuse the passkeys as 2FA, and use it more like a securitykey
// we need to modify some of the default settings defined by `start_passkey_registration()`.
@ -252,7 +252,7 @@ impl From<PublicKeyCredentialCopy> for PublicKeyCredential {
}
#[post("/two-factor/webauthn", data = "<data>")]
async fn activate_webauthn(data: Json<EnableWebauthnData>, headers: Headers, mut conn: DbConn) -> JsonResult {
async fn activate_webauthn(data: Json<EnableWebauthnData>, headers: Headers, conn: DbConn) -> JsonResult {
let data: EnableWebauthnData = data.into_inner();
let mut user = headers.user;
@ -260,15 +260,15 @@ async fn activate_webauthn(data: Json<EnableWebauthnData>, headers: Headers, mut
master_password_hash: data.master_password_hash,
otp: data.otp,
}
.validate(&user, true, &mut conn)
.validate(&user, true, &conn)
.await?;
// Retrieve and delete the saved challenge state
let type_ = TwoFactorType::WebauthnRegisterChallenge as i32;
let state = match TwoFactor::find_by_user_and_type(&user.uuid, type_, &mut conn).await {
let state = match TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn).await {
Some(tf) => {
let state: PasskeyRegistration = serde_json::from_str(&tf.data)?;
tf.delete(&mut conn).await?;
tf.delete(&conn).await?;
state
}
None => err!("Can't recover challenge"),
@ -277,7 +277,7 @@ async fn activate_webauthn(data: Json<EnableWebauthnData>, headers: Headers, mut
// Verify the credentials with the saved state
let credential = WEBAUTHN.finish_passkey_registration(&data.device_response.into(), &state)?;
let mut registrations: Vec<_> = get_webauthn_registrations(&user.uuid, &mut conn).await?.1;
let mut registrations: Vec<_> = get_webauthn_registrations(&user.uuid, &conn).await?.1;
// TODO: Check for repeated ID's
registrations.push(WebauthnRegistration {
id: data.id.into_i32()?,
@ -289,11 +289,11 @@ async fn activate_webauthn(data: Json<EnableWebauthnData>, headers: Headers, mut
// Save the registrations and return them
TwoFactor::new(user.uuid.clone(), TwoFactorType::Webauthn, serde_json::to_string(&registrations)?)
.save(&mut conn)
.save(&conn)
.await?;
_generate_recover_code(&mut user, &mut conn).await;
_generate_recover_code(&mut user, &conn).await;
log_user_event(EventType::UserUpdated2fa as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &mut conn).await;
log_user_event(EventType::UserUpdated2fa as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &conn).await;
let keys_json: Vec<Value> = registrations.iter().map(WebauthnRegistration::to_json).collect();
Ok(Json(json!({
@ -316,14 +316,14 @@ struct DeleteU2FData {
}
#[delete("/two-factor/webauthn", data = "<data>")]
async fn delete_webauthn(data: Json<DeleteU2FData>, headers: Headers, mut conn: DbConn) -> JsonResult {
async fn delete_webauthn(data: Json<DeleteU2FData>, headers: Headers, conn: DbConn) -> JsonResult {
let id = data.id.into_i32()?;
if !headers.user.check_valid_password(&data.master_password_hash) {
err!("Invalid password");
}
let Some(mut tf) =
TwoFactor::find_by_user_and_type(&headers.user.uuid, TwoFactorType::Webauthn as i32, &mut conn).await
TwoFactor::find_by_user_and_type(&headers.user.uuid, TwoFactorType::Webauthn as i32, &conn).await
else {
err!("Webauthn data not found!")
};
@ -336,12 +336,11 @@ async fn delete_webauthn(data: Json<DeleteU2FData>, headers: Headers, mut conn:
let removed_item = data.remove(item_pos);
tf.data = serde_json::to_string(&data)?;
tf.save(&mut conn).await?;
tf.save(&conn).await?;
drop(tf);
// If entry is migrated from u2f, delete the u2f entry as well
if let Some(mut u2f) =
TwoFactor::find_by_user_and_type(&headers.user.uuid, TwoFactorType::U2f as i32, &mut conn).await
if let Some(mut u2f) = TwoFactor::find_by_user_and_type(&headers.user.uuid, TwoFactorType::U2f as i32, &conn).await
{
let mut data: Vec<U2FRegistration> = match serde_json::from_str(&u2f.data) {
Ok(d) => d,
@ -352,7 +351,7 @@ async fn delete_webauthn(data: Json<DeleteU2FData>, headers: Headers, mut conn:
let new_data_str = serde_json::to_string(&data)?;
u2f.data = new_data_str;
u2f.save(&mut conn).await?;
u2f.save(&conn).await?;
}
let keys_json: Vec<Value> = data.iter().map(WebauthnRegistration::to_json).collect();
@ -366,7 +365,7 @@ async fn delete_webauthn(data: Json<DeleteU2FData>, headers: Headers, mut conn:
pub async fn get_webauthn_registrations(
user_id: &UserId,
conn: &mut DbConn,
conn: &DbConn,
) -> Result<(bool, Vec<WebauthnRegistration>), Error> {
let type_ = TwoFactorType::Webauthn as i32;
match TwoFactor::find_by_user_and_type(user_id, type_, conn).await {
@ -375,7 +374,7 @@ pub async fn get_webauthn_registrations(
}
}
pub async fn generate_webauthn_login(user_id: &UserId, conn: &mut DbConn) -> JsonResult {
pub async fn generate_webauthn_login(user_id: &UserId, conn: &DbConn) -> JsonResult {
// Load saved credentials
let creds: Vec<Passkey> =
get_webauthn_registrations(user_id, conn).await?.1.into_iter().map(|r| r.credential).collect();
@ -415,7 +414,7 @@ pub async fn generate_webauthn_login(user_id: &UserId, conn: &mut DbConn) -> Jso
Ok(Json(serde_json::to_value(response.public_key)?))
}
pub async fn validate_webauthn_login(user_id: &UserId, response: &str, conn: &mut DbConn) -> EmptyResult {
pub async fn validate_webauthn_login(user_id: &UserId, response: &str, conn: &DbConn) -> EmptyResult {
let type_ = TwoFactorType::WebauthnLoginChallenge as i32;
let mut state = match TwoFactor::find_by_user_and_type(user_id, type_, conn).await {
Some(tf) => {
@ -469,7 +468,7 @@ async fn check_and_update_backup_eligible(
rsp: &PublicKeyCredential,
registrations: &mut Vec<WebauthnRegistration>,
state: &mut PasskeyAuthentication,
conn: &mut DbConn,
conn: &DbConn,
) -> EmptyResult {
// The feature flags from the response
// For details see: https://www.w3.org/TR/webauthn-3/#sctn-authenticator-data

18
src/api/core/two_factor/yubikey.rs

@ -83,19 +83,19 @@ async fn verify_yubikey_otp(otp: String) -> EmptyResult {
}
#[post("/two-factor/get-yubikey", data = "<data>")]
async fn generate_yubikey(data: Json<PasswordOrOtpData>, headers: Headers, mut conn: DbConn) -> JsonResult {
async fn generate_yubikey(data: Json<PasswordOrOtpData>, headers: Headers, conn: DbConn) -> JsonResult {
// Make sure the credentials are set
get_yubico_credentials()?;
let data: PasswordOrOtpData = data.into_inner();
let user = headers.user;
data.validate(&user, false, &mut conn).await?;
data.validate(&user, false, &conn).await?;
let user_id = &user.uuid;
let yubikey_type = TwoFactorType::YubiKey as i32;
let r = TwoFactor::find_by_user_and_type(user_id, yubikey_type, &mut conn).await;
let r = TwoFactor::find_by_user_and_type(user_id, yubikey_type, &conn).await;
if let Some(r) = r {
let yubikey_metadata: YubikeyMetadata = serde_json::from_str(&r.data)?;
@ -116,7 +116,7 @@ async fn generate_yubikey(data: Json<PasswordOrOtpData>, headers: Headers, mut c
}
#[post("/two-factor/yubikey", data = "<data>")]
async fn activate_yubikey(data: Json<EnableYubikeyData>, headers: Headers, mut conn: DbConn) -> JsonResult {
async fn activate_yubikey(data: Json<EnableYubikeyData>, headers: Headers, conn: DbConn) -> JsonResult {
let data: EnableYubikeyData = data.into_inner();
let mut user = headers.user;
@ -124,12 +124,12 @@ async fn activate_yubikey(data: Json<EnableYubikeyData>, headers: Headers, mut c
master_password_hash: data.master_password_hash.clone(),
otp: data.otp.clone(),
}
.validate(&user, true, &mut conn)
.validate(&user, true, &conn)
.await?;
// Check if we already have some data
let mut yubikey_data =
match TwoFactor::find_by_user_and_type(&user.uuid, TwoFactorType::YubiKey as i32, &mut conn).await {
match TwoFactor::find_by_user_and_type(&user.uuid, TwoFactorType::YubiKey as i32, &conn).await {
Some(data) => data,
None => TwoFactor::new(user.uuid.clone(), TwoFactorType::YubiKey, String::new()),
};
@ -160,11 +160,11 @@ async fn activate_yubikey(data: Json<EnableYubikeyData>, headers: Headers, mut c
};
yubikey_data.data = serde_json::to_string(&yubikey_metadata).unwrap();
yubikey_data.save(&mut conn).await?;
yubikey_data.save(&conn).await?;
_generate_recover_code(&mut user, &mut conn).await;
_generate_recover_code(&mut user, &conn).await;
log_user_event(EventType::UserUpdated2fa as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &mut conn).await;
log_user_event(EventType::UserUpdated2fa as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &conn).await;
let mut result = jsonify_yubikeys(yubikey_metadata.keys);

22
src/api/icons.rs

@ -1,13 +1,13 @@
use std::{
collections::HashMap,
net::IpAddr,
sync::Arc,
sync::{Arc, LazyLock},
time::{Duration, SystemTime},
};
use bytes::{Bytes, BytesMut};
use futures::{stream::StreamExt, TryFutureExt};
use once_cell::sync::Lazy;
use html5gum::{Emitter, HtmlString, Readable, StringReader, Tokenizer};
use regex::Regex;
use reqwest::{
header::{self, HeaderMap, HeaderValue},
@ -16,8 +16,6 @@ use reqwest::{
use rocket::{http::ContentType, response::Redirect, Route};
use svg_hush::{data_url_filter, Filter};
use html5gum::{Emitter, HtmlString, Readable, StringReader, Tokenizer};
use crate::{
config::PathType,
error::Error,
@ -33,7 +31,7 @@ pub fn routes() -> Vec<Route> {
}
}
static CLIENT: Lazy<Client> = Lazy::new(|| {
static CLIENT: LazyLock<Client> = LazyLock::new(|| {
// Generate the default headers
let mut default_headers = HeaderMap::new();
default_headers.insert(
@ -78,7 +76,7 @@ static CLIENT: Lazy<Client> = Lazy::new(|| {
});
// Build Regex only once since this takes a lot of time.
static ICON_SIZE_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?x)(\d+)\D*(\d+)").unwrap());
static ICON_SIZE_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?x)(\d+)\D*(\d+)").unwrap());
// 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.
@ -220,7 +218,7 @@ async fn get_cached_icon(path: &str) -> Option<Vec<u8>> {
}
// Try to read the cached icon, and return it if it exists
if let Ok(operator) = CONFIG.opendal_operator_for_path_type(PathType::IconCache) {
if let Ok(operator) = CONFIG.opendal_operator_for_path_type(&PathType::IconCache) {
if let Ok(buf) = operator.read(path).await {
return Some(buf.to_vec());
}
@ -230,7 +228,7 @@ async fn get_cached_icon(path: &str) -> Option<Vec<u8>> {
}
async fn file_is_expired(path: &str, ttl: u64) -> Result<bool, Error> {
let operator = CONFIG.opendal_operator_for_path_type(PathType::IconCache)?;
let operator = CONFIG.opendal_operator_for_path_type(&PathType::IconCache)?;
let meta = operator.stat(path).await?;
let modified =
meta.last_modified().ok_or_else(|| std::io::Error::other(format!("No last modified time for `{path}`")))?;
@ -246,7 +244,7 @@ async fn icon_is_negcached(path: &str) -> bool {
match expired {
// No longer negatively cached, drop the marker
Ok(true) => {
match CONFIG.opendal_operator_for_path_type(PathType::IconCache) {
match CONFIG.opendal_operator_for_path_type(&PathType::IconCache) {
Ok(operator) => {
if let Err(e) = operator.delete(&miss_indicator).await {
error!("Could not remove negative cache indicator for icon {path:?}: {e:?}");
@ -462,8 +460,8 @@ async fn get_page_with_referer(url: &str, referer: &str) -> Result<Response, Err
/// priority2 = get_icon_priority("https://example.com/path/to/a/favicon.ico", "");
/// ```
fn get_icon_priority(href: &str, sizes: &str) -> u8 {
static PRIORITY_MAP: Lazy<HashMap<&'static str, u8>> =
Lazy::new(|| [(".png", 10), (".jpg", 20), (".jpeg", 20)].into_iter().collect());
static PRIORITY_MAP: LazyLock<HashMap<&'static str, u8>> =
LazyLock::new(|| [(".png", 10), (".jpg", 20), (".jpeg", 20)].into_iter().collect());
// Check if there is a dimension set
let (width, height) = parse_sizes(sizes);
@ -597,7 +595,7 @@ async fn download_icon(domain: &str) -> Result<(Bytes, Option<&str>), Error> {
}
async fn save_icon(path: &str, icon: Vec<u8>) {
let operator = match CONFIG.opendal_operator_for_path_type(PathType::IconCache) {
let operator = match CONFIG.opendal_operator_for_path_type(&PathType::IconCache) {
Ok(operator) => operator,
Err(e) => {
warn!("Failed to get OpenDAL operator while saving icon: {e}");

65
src/api/identity.rs

@ -22,7 +22,13 @@ use crate::{
},
auth,
auth::{generate_organization_api_key_login_claims, AuthMethod, ClientHeaders, ClientIp, ClientVersion},
db::{models::*, DbConn},
db::{
models::{
AuthRequest, AuthRequestId, Device, DeviceId, EventType, Invitation, OrganizationApiKey, OrganizationId,
SsoNonce, SsoUser, TwoFactor, TwoFactorIncomplete, TwoFactorType, User, UserId,
},
DbConn,
},
error::MapResult,
mail, sso,
sso::{OIDCCode, OIDCState},
@ -48,7 +54,7 @@ async fn login(
data: Form<ConnectData>,
client_header: ClientHeaders,
client_version: Option<ClientVersion>,
mut conn: DbConn,
conn: DbConn,
) -> JsonResult {
let data: ConnectData = data.into_inner();
@ -57,7 +63,7 @@ async fn login(
let login_result = match data.grant_type.as_ref() {
"refresh_token" => {
_check_is_some(&data.refresh_token, "refresh_token cannot be blank")?;
_refresh_login(data, &mut 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" => {
@ -70,7 +76,7 @@ async fn login(
_check_is_some(&data.device_name, "device_name cannot be blank")?;
_check_is_some(&data.device_type, "device_type cannot be blank")?;
_password_login(data, &mut user_id, &mut conn, &client_header.ip, &client_version).await
_password_login(data, &mut user_id, &conn, &client_header.ip, &client_version).await
}
"client_credentials" => {
_check_is_some(&data.client_id, "client_id cannot be blank")?;
@ -81,7 +87,7 @@ async fn login(
_check_is_some(&data.device_name, "device_name cannot be blank")?;
_check_is_some(&data.device_type, "device_type cannot be blank")?;
_api_key_login(data, &mut user_id, &mut conn, &client_header.ip).await
_api_key_login(data, &mut user_id, &conn, &client_header.ip).await
}
"authorization_code" if CONFIG.sso_enabled() => {
_check_is_some(&data.client_id, "client_id cannot be blank")?;
@ -91,7 +97,7 @@ async fn login(
_check_is_some(&data.device_name, "device_name cannot be blank")?;
_check_is_some(&data.device_type, "device_type cannot be blank")?;
_sso_login(data, &mut user_id, &mut conn, &client_header.ip, &client_version).await
_sso_login(data, &mut user_id, &conn, &client_header.ip, &client_version).await
}
"authorization_code" => err!("SSO sign-in is not available"),
t => err!("Invalid type", t),
@ -105,20 +111,14 @@ async fn login(
&user_id,
client_header.device_type,
&client_header.ip.ip,
&mut conn,
&conn,
)
.await;
}
Err(e) => {
if let Some(ev) = e.get_event() {
log_user_event(
ev.event as i32,
&user_id,
client_header.device_type,
&client_header.ip.ip,
&mut conn,
)
.await
log_user_event(ev.event as i32, &user_id, client_header.device_type, &client_header.ip.ip, &conn)
.await
}
}
}
@ -128,7 +128,7 @@ async fn login(
}
// Return Status::Unauthorized to trigger logout
async fn _refresh_login(data: ConnectData, conn: &mut DbConn, ip: &ClientIp) -> JsonResult {
async fn _refresh_login(data: ConnectData, conn: &DbConn, ip: &ClientIp) -> JsonResult {
// Extract token
let refresh_token = match data.refresh_token {
Some(token) => token,
@ -166,7 +166,7 @@ async fn _refresh_login(data: ConnectData, conn: &mut DbConn, ip: &ClientIp) ->
async fn _sso_login(
data: ConnectData,
user_id: &mut Option<UserId>,
conn: &mut DbConn,
conn: &DbConn,
ip: &ClientIp,
client_version: &Option<ClientVersion>,
) -> JsonResult {
@ -248,7 +248,7 @@ async fn _sso_login(
_ => (),
}
let mut user = User::new(user_infos.email, user_infos.user_name);
let mut user = User::new(&user_infos.email, user_infos.user_name);
user.verified_at = Some(now);
user.save(conn).await?;
@ -319,7 +319,7 @@ async fn _sso_login(
async fn _password_login(
data: ConnectData,
user_id: &mut Option<UserId>,
conn: &mut DbConn,
conn: &DbConn,
ip: &ClientIp,
client_version: &Option<ClientVersion>,
) -> JsonResult {
@ -444,7 +444,7 @@ async fn authenticated_response(
auth_tokens: auth::AuthTokens,
twofactor_token: Option<String>,
now: &NaiveDateTime,
conn: &mut DbConn,
conn: &DbConn,
ip: &ClientIp,
) -> JsonResult {
if CONFIG.mail_enabled() && device.is_new() {
@ -504,12 +504,7 @@ async fn authenticated_response(
Ok(Json(result))
}
async fn _api_key_login(
data: ConnectData,
user_id: &mut Option<UserId>,
conn: &mut DbConn,
ip: &ClientIp,
) -> JsonResult {
async fn _api_key_login(data: ConnectData, user_id: &mut Option<UserId>, conn: &DbConn, ip: &ClientIp) -> JsonResult {
// Ratelimit the login
crate::ratelimit::check_limit_login(&ip.ip)?;
@ -524,7 +519,7 @@ async fn _api_key_login(
async fn _user_api_key_login(
data: ConnectData,
user_id: &mut Option<UserId>,
conn: &mut DbConn,
conn: &DbConn,
ip: &ClientIp,
) -> JsonResult {
// Get the user via the client_id
@ -614,7 +609,7 @@ async fn _user_api_key_login(
Ok(Json(result))
}
async fn _organization_api_key_login(data: ConnectData, conn: &mut DbConn, ip: &ClientIp) -> JsonResult {
async fn _organization_api_key_login(data: ConnectData, conn: &DbConn, ip: &ClientIp) -> JsonResult {
// Get the org via the client_id
let client_id = data.client_id.as_ref().unwrap();
let Some(org_id) = client_id.strip_prefix("organization.") else {
@ -643,7 +638,7 @@ async fn _organization_api_key_login(data: ConnectData, conn: &mut DbConn, ip: &
}
/// Retrieves an existing device or creates a new device from ConnectData and the User
async fn get_device(data: &ConnectData, conn: &mut DbConn, user: &User) -> ApiResult<Device> {
async fn get_device(data: &ConnectData, conn: &DbConn, user: &User) -> ApiResult<Device> {
// On iOS, device_type sends "iOS", on others it sends a number
// When unknown or unable to parse, return 14, which is 'Unknown Browser'
let device_type = util::try_parse_string(data.device_type.as_ref()).unwrap_or(14);
@ -663,7 +658,7 @@ async fn twofactor_auth(
device: &mut Device,
ip: &ClientIp,
client_version: &Option<ClientVersion>,
conn: &mut DbConn,
conn: &DbConn,
) -> ApiResult<Option<String>> {
let twofactors = TwoFactor::find_by_user(&user.uuid, conn).await;
@ -780,7 +775,7 @@ async fn _json_err_twofactor(
user_id: &UserId,
data: &ConnectData,
client_version: &Option<ClientVersion>,
conn: &mut DbConn,
conn: &DbConn,
) -> ApiResult<Value> {
let mut result = json!({
"error" : "invalid_grant",
@ -905,13 +900,13 @@ enum RegisterVerificationResponse {
#[post("/accounts/register/send-verification-email", data = "<data>")]
async fn register_verification_email(
data: Json<RegisterVerificationData>,
mut conn: DbConn,
conn: DbConn,
) -> ApiResult<RegisterVerificationResponse> {
let data = data.into_inner();
// the registration can only continue if signup is allowed or there exists an invitation
if !(CONFIG.is_signup_allowed(&data.email)
|| (!CONFIG.mail_enabled() && Invitation::find_by_mail(&data.email, &mut conn).await.is_some()))
|| (!CONFIG.mail_enabled() && Invitation::find_by_mail(&data.email, &conn).await.is_some()))
{
err!("Registration not allowed or user already exists")
}
@ -922,7 +917,7 @@ async fn register_verification_email(
let token = auth::encode_jwt(&token_claims);
if should_send_mail {
let user = User::find_by_mail(&data.email, &mut conn).await;
let user = User::find_by_mail(&data.email, &conn).await;
if user.filter(|u| u.private_key.is_some()).is_some() {
// There is still a timing side channel here in that the code
// paths that send mail take noticeably longer than ones that
@ -1066,7 +1061,7 @@ async fn oidcsignin_redirect(
wrapper: impl FnOnce(OIDCState) -> sso::OIDCCodeWrapper,
conn: &DbConn,
) -> ApiResult<Redirect> {
let state = sso::decode_state(base64_state)?;
let state = sso::decode_state(&base64_state)?;
let code = sso::encode_code_claims(wrapper(state.clone()));
let nonce = match SsoNonce::find(&state, conn).await {

2
src/api/mod.rs

@ -55,7 +55,7 @@ impl PasswordOrOtpData {
/// Tokens used via this struct can be used multiple times during the process
/// First for the validation to continue, after that to enable or validate the following actions
/// This is different per caller, so it can be adjusted to delete the token or not
pub async fn validate(&self, user: &User, delete_if_valid: bool, conn: &mut DbConn) -> EmptyResult {
pub async fn validate(&self, user: &User, delete_if_valid: bool, conn: &DbConn) -> EmptyResult {
use crate::api::core::two_factor::protected_actions::validate_protected_action_otp;
match (self.master_password_hash.as_deref(), self.otp.as_deref()) {

64
src/api/notifications.rs

@ -1,11 +1,14 @@
use std::{net::IpAddr, sync::Arc, time::Duration};
use std::{
net::IpAddr,
sync::{Arc, LazyLock},
time::Duration,
};
use chrono::{NaiveDateTime, Utc};
use rmpv::Value;
use rocket::{futures::StreamExt, Route};
use tokio::sync::mpsc::Sender;
use rocket_ws::{Message, WebSocket};
use tokio::sync::mpsc::Sender;
use crate::{
auth::{ClientIp, WsAccessTokenHeader},
@ -16,15 +19,13 @@ use crate::{
Error, CONFIG,
};
use once_cell::sync::Lazy;
pub static WS_USERS: Lazy<Arc<WebSocketUsers>> = Lazy::new(|| {
pub static WS_USERS: LazyLock<Arc<WebSocketUsers>> = LazyLock::new(|| {
Arc::new(WebSocketUsers {
map: Arc::new(dashmap::DashMap::new()),
})
});
pub static WS_ANONYMOUS_SUBSCRIPTIONS: Lazy<Arc<AnonymousWebSocketSubscriptions>> = Lazy::new(|| {
pub static WS_ANONYMOUS_SUBSCRIPTIONS: LazyLock<Arc<AnonymousWebSocketSubscriptions>> = LazyLock::new(|| {
Arc::new(AnonymousWebSocketSubscriptions {
map: Arc::new(dashmap::DashMap::new()),
})
@ -35,7 +36,7 @@ use super::{
push_send_update, push_user_update,
};
static NOTIFICATIONS_DISABLED: Lazy<bool> = Lazy::new(|| !CONFIG.enable_websocket() && !CONFIG.push_enabled());
static NOTIFICATIONS_DISABLED: LazyLock<bool> = LazyLock::new(|| !CONFIG.enable_websocket() && !CONFIG.push_enabled());
pub fn routes() -> Vec<Route> {
if CONFIG.enable_websocket() {
@ -109,8 +110,7 @@ fn websockets_hub<'r>(
ip: ClientIp,
header_token: WsAccessTokenHeader,
) -> Result<rocket_ws::Stream!['r], Error> {
let addr = ip.ip;
info!("Accepting Rocket WS connection from {addr}");
info!("Accepting Rocket WS connection from {}", ip.ip);
let token = if let Some(token) = data.access_token {
token
@ -133,7 +133,7 @@ fn websockets_hub<'r>(
users.map.entry(claims.sub.to_string()).or_default().push((entry_uuid, tx));
// Once the guard goes out of scope, the connection will have been closed and the entry will be deleted from the map
(rx, WSEntryMapGuard::new(users, claims.sub, entry_uuid, addr))
(rx, WSEntryMapGuard::new(users, claims.sub, entry_uuid, ip.ip))
};
Ok({
@ -189,8 +189,7 @@ fn websockets_hub<'r>(
#[allow(tail_expr_drop_order)]
#[get("/anonymous-hub?<token..>")]
fn anonymous_websockets_hub<'r>(ws: WebSocket, token: String, ip: ClientIp) -> Result<rocket_ws::Stream!['r], Error> {
let addr = ip.ip;
info!("Accepting Anonymous Rocket WS connection from {addr}");
info!("Accepting Anonymous Rocket WS connection from {}", ip.ip);
let (mut rx, guard) = {
let subscriptions = Arc::clone(&WS_ANONYMOUS_SUBSCRIPTIONS);
@ -200,7 +199,7 @@ fn anonymous_websockets_hub<'r>(ws: WebSocket, token: String, ip: ClientIp) -> R
subscriptions.map.insert(token.clone(), tx);
// Once the guard goes out of scope, the connection will have been closed and the entry will be deleted from the map
(rx, WSAnonymousEntryMapGuard::new(subscriptions, token, addr))
(rx, WSAnonymousEntryMapGuard::new(subscriptions, token, ip.ip))
};
Ok({
@ -257,11 +256,11 @@ fn anonymous_websockets_hub<'r>(ws: WebSocket, token: String, ip: ClientIp) -> R
// Websockets server
//
fn serialize(val: Value) -> Vec<u8> {
fn serialize(val: &Value) -> Vec<u8> {
use rmpv::encode::write_value;
let mut buf = Vec::new();
write_value(&mut buf, &val).expect("Error encoding MsgPack");
write_value(&mut buf, val).expect("Error encoding MsgPack");
// Add size bytes at the start
// Extracted from BinaryMessageFormat.js
@ -339,7 +338,7 @@ impl WebSocketUsers {
}
// 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: &mut 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
if *NOTIFICATIONS_DISABLED {
return;
@ -359,7 +358,7 @@ impl WebSocketUsers {
}
}
pub async fn send_logout(&self, user: &User, acting_device_id: Option<DeviceId>, conn: &mut 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
if *NOTIFICATIONS_DISABLED {
return;
@ -379,7 +378,7 @@ impl WebSocketUsers {
}
}
pub async fn send_folder_update(&self, ut: UpdateType, folder: &Folder, device: &Device, conn: &mut DbConn) {
pub async fn send_folder_update(&self, ut: UpdateType, folder: &Folder, device: &Device, conn: &DbConn) {
// Skip any processing if both WebSockets and Push are not active
if *NOTIFICATIONS_DISABLED {
return;
@ -410,7 +409,7 @@ impl WebSocketUsers {
user_ids: &[UserId],
device: &Device,
collection_uuids: Option<Vec<CollectionId>>,
conn: &mut DbConn,
conn: &DbConn,
) {
// Skip any processing if both WebSockets and Push are not active
if *NOTIFICATIONS_DISABLED {
@ -458,7 +457,7 @@ impl WebSocketUsers {
send: &DbSend,
user_ids: &[UserId],
device: &Device,
conn: &mut DbConn,
conn: &DbConn,
) {
// Skip any processing if both WebSockets and Push are not active
if *NOTIFICATIONS_DISABLED {
@ -486,13 +485,7 @@ impl WebSocketUsers {
}
}
pub async fn send_auth_request(
&self,
user_id: &UserId,
auth_request_uuid: &str,
device: &Device,
conn: &mut DbConn,
) {
pub async fn send_auth_request(&self, user_id: &UserId, auth_request_uuid: &str, device: &Device, conn: &DbConn) {
// Skip any processing if both WebSockets and Push are not active
if *NOTIFICATIONS_DISABLED {
return;
@ -516,7 +509,7 @@ impl WebSocketUsers {
user_id: &UserId,
auth_request_id: &AuthRequestId,
device: &Device,
conn: &mut DbConn,
conn: &DbConn,
) {
// Skip any processing if both WebSockets and Push are not active
if *NOTIFICATIONS_DISABLED {
@ -558,7 +551,7 @@ impl AnonymousWebSocketSubscriptions {
let data = create_anonymous_update(
vec![("Id".into(), auth_request_id.to_string().into()), ("UserId".into(), user_id.to_string().into())],
UpdateType::AuthRequestResponse,
user_id.clone(),
user_id,
);
self.send_update(auth_request_id, &data).await;
}
@ -594,16 +587,19 @@ fn create_update(payload: Vec<(Value, Value)>, ut: UpdateType, acting_device_id:
])]),
]);
serialize(value)
serialize(&value)
}
fn create_anonymous_update(payload: Vec<(Value, Value)>, ut: UpdateType, user_id: UserId) -> Vec<u8> {
fn create_anonymous_update(payload: Vec<(Value, Value)>, ut: UpdateType, user_id: &UserId) -> Vec<u8> {
use rmpv::Value as V;
let value = V::Array(vec![
1.into(),
V::Map(vec![]),
V::Nil,
// This word is misspelled, but upstream has this too
// 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
"AuthRequestResponseRecieved".into(),
V::Array(vec![V::Map(vec![
("Type".into(), (ut as i32).into()),
@ -612,11 +608,11 @@ fn create_anonymous_update(payload: Vec<(Value, Value)>, ut: UpdateType, user_id
])]),
]);
serialize(value)
serialize(&value)
}
fn create_ping() -> Vec<u8> {
serialize(Value::Array(vec![6.into()]))
serialize(&Value::Array(vec![6.into()]))
}
// https://github.com/bitwarden/server/blob/375af7c43b10d9da03525d41452f95de3f921541/src/Core/Enums/PushType.cs

36
src/api/push.rs

@ -1,3 +1,8 @@
use std::{
sync::LazyLock,
time::{Duration, Instant},
};
use reqwest::{
header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE},
Method,
@ -7,15 +12,15 @@ use tokio::sync::RwLock;
use crate::{
api::{ApiResult, EmptyResult, UpdateType},
db::models::{AuthRequestId, Cipher, Device, DeviceId, Folder, PushId, Send, User, UserId},
db::{
models::{AuthRequestId, Cipher, Device, DeviceId, Folder, PushId, Send, User, UserId},
DbConn,
},
http_client::make_http_request,
util::{format_date, get_uuid},
CONFIG,
};
use once_cell::sync::Lazy;
use std::time::{Duration, Instant};
#[derive(Deserialize)]
struct AuthPushToken {
access_token: String,
@ -29,7 +34,7 @@ struct LocalAuthPushToken {
}
async fn get_auth_api_token() -> ApiResult<String> {
static API_TOKEN: Lazy<RwLock<LocalAuthPushToken>> = Lazy::new(|| {
static API_TOKEN: LazyLock<RwLock<LocalAuthPushToken>> = LazyLock::new(|| {
RwLock::new(LocalAuthPushToken {
access_token: String::new(),
valid_until: Instant::now(),
@ -79,7 +84,7 @@ async fn get_auth_api_token() -> ApiResult<String> {
Ok(api_token.access_token.clone())
}
pub async fn register_push_device(device: &mut Device, conn: &mut crate::db::DbConn) -> EmptyResult {
pub async fn register_push_device(device: &mut Device, conn: &DbConn) -> EmptyResult {
if !CONFIG.push_enabled() || !device.is_push_device() {
return Ok(());
}
@ -152,7 +157,7 @@ pub async fn unregister_push_device(push_id: &Option<PushId>) -> EmptyResult {
Ok(())
}
pub async fn push_cipher_update(ut: UpdateType, cipher: &Cipher, device: &Device, conn: &mut crate::db::DbConn) {
pub async fn push_cipher_update(ut: UpdateType, cipher: &Cipher, device: &Device, conn: &DbConn) {
// We shouldn't send a push notification on cipher update if the cipher belongs to an organization, this isn't implemented in the upstream server too.
if cipher.organization_uuid.is_some() {
return;
@ -183,7 +188,7 @@ pub async fn push_cipher_update(ut: UpdateType, cipher: &Cipher, device: &Device
}
}
pub async fn push_logout(user: &User, acting_device_id: Option<DeviceId>, conn: &mut crate::db::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 {
@ -203,7 +208,7 @@ pub async fn push_logout(user: &User, acting_device_id: Option<DeviceId>, conn:
}
}
pub async fn push_user_update(ut: UpdateType, user: &User, push_uuid: &Option<PushId>, conn: &mut crate::db::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 {
tokio::task::spawn(send_to_push_relay(json!({
"userId": user.uuid,
@ -221,7 +226,7 @@ pub async fn push_user_update(ut: UpdateType, user: &User, push_uuid: &Option<Pu
}
}
pub async fn push_folder_update(ut: UpdateType, folder: &Folder, device: &Device, conn: &mut crate::db::DbConn) {
pub async fn push_folder_update(ut: UpdateType, folder: &Folder, device: &Device, conn: &DbConn) {
if Device::check_user_has_push_device(&folder.user_uuid, conn).await {
tokio::task::spawn(send_to_push_relay(json!({
"userId": folder.user_uuid,
@ -240,7 +245,7 @@ pub async fn push_folder_update(ut: UpdateType, folder: &Folder, device: &Device
}
}
pub async fn push_send_update(ut: UpdateType, send: &Send, device: &Device, conn: &mut crate::db::DbConn) {
pub async fn push_send_update(ut: UpdateType, send: &Send, device: &Device, conn: &DbConn) {
if let Some(s) = &send.user_uuid {
if Device::check_user_has_push_device(s, conn).await {
tokio::task::spawn(send_to_push_relay(json!({
@ -296,7 +301,7 @@ async fn send_to_push_relay(notification_data: Value) {
};
}
pub async fn push_auth_request(user_id: &UserId, auth_request_id: &str, device: &Device, conn: &mut crate::db::DbConn) {
pub async fn push_auth_request(user_id: &UserId, auth_request_id: &str, device: &Device, conn: &DbConn) {
if Device::check_user_has_push_device(user_id, conn).await {
tokio::task::spawn(send_to_push_relay(json!({
"userId": user_id,
@ -314,12 +319,7 @@ pub async fn push_auth_request(user_id: &UserId, auth_request_id: &str, device:
}
}
pub async fn push_auth_response(
user_id: &UserId,
auth_request_id: &AuthRequestId,
device: &Device,
conn: &mut crate::db::DbConn,
) {
pub async fn push_auth_response(user_id: &UserId, auth_request_id: &AuthRequestId, device: &Device, conn: &DbConn) {
if Device::check_user_has_push_device(user_id, conn).await {
tokio::task::spawn(send_to_push_relay(json!({
"userId": user_id,

82
src/auth.rs

@ -1,12 +1,15 @@
// JWT Handling
use std::{
env,
net::IpAddr,
sync::{LazyLock, OnceLock},
};
use chrono::{DateTime, TimeDelta, Utc};
use jsonwebtoken::{errors::ErrorKind, Algorithm, DecodingKey, EncodingKey, Header};
use num_traits::FromPrimitive;
use once_cell::sync::{Lazy, OnceCell};
use openssl::rsa::Rsa;
use serde::de::DeserializeOwned;
use serde::ser::Serialize;
use std::{env, net::IpAddr};
use crate::{
api::ApiResult,
@ -22,27 +25,30 @@ use crate::{
const JWT_ALGORITHM: Algorithm = Algorithm::RS256;
// Limit when BitWarden consider the token as expired
pub static BW_EXPIRATION: Lazy<TimeDelta> = Lazy::new(|| TimeDelta::try_minutes(5).unwrap());
pub static DEFAULT_REFRESH_VALIDITY: Lazy<TimeDelta> = Lazy::new(|| TimeDelta::try_days(30).unwrap());
pub static MOBILE_REFRESH_VALIDITY: Lazy<TimeDelta> = Lazy::new(|| TimeDelta::try_days(90).unwrap());
pub static DEFAULT_ACCESS_VALIDITY: Lazy<TimeDelta> = Lazy::new(|| TimeDelta::try_hours(2).unwrap());
static JWT_HEADER: Lazy<Header> = Lazy::new(|| Header::new(JWT_ALGORITHM));
pub static JWT_LOGIN_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|login", CONFIG.domain_origin()));
static JWT_INVITE_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|invite", CONFIG.domain_origin()));
static JWT_EMERGENCY_ACCESS_INVITE_ISSUER: Lazy<String> =
Lazy::new(|| format!("{}|emergencyaccessinvite", CONFIG.domain_origin()));
static JWT_DELETE_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|delete", CONFIG.domain_origin()));
static JWT_VERIFYEMAIL_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|verifyemail", CONFIG.domain_origin()));
static JWT_ADMIN_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|admin", CONFIG.domain_origin()));
static JWT_SEND_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|send", CONFIG.domain_origin()));
static JWT_ORG_API_KEY_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|api.organization", CONFIG.domain_origin()));
static JWT_FILE_DOWNLOAD_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|file_download", CONFIG.domain_origin()));
static JWT_REGISTER_VERIFY_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|register_verify", CONFIG.domain_origin()));
static PRIVATE_RSA_KEY: OnceCell<EncodingKey> = OnceCell::new();
static PUBLIC_RSA_KEY: OnceCell<DecodingKey> = OnceCell::new();
pub static BW_EXPIRATION: LazyLock<TimeDelta> = LazyLock::new(|| TimeDelta::try_minutes(5).unwrap());
pub static DEFAULT_REFRESH_VALIDITY: LazyLock<TimeDelta> = LazyLock::new(|| TimeDelta::try_days(30).unwrap());
pub static MOBILE_REFRESH_VALIDITY: LazyLock<TimeDelta> = LazyLock::new(|| TimeDelta::try_days(90).unwrap());
pub static DEFAULT_ACCESS_VALIDITY: LazyLock<TimeDelta> = LazyLock::new(|| TimeDelta::try_hours(2).unwrap());
static JWT_HEADER: LazyLock<Header> = LazyLock::new(|| Header::new(JWT_ALGORITHM));
pub static JWT_LOGIN_ISSUER: LazyLock<String> = LazyLock::new(|| format!("{}|login", CONFIG.domain_origin()));
static JWT_INVITE_ISSUER: LazyLock<String> = LazyLock::new(|| format!("{}|invite", CONFIG.domain_origin()));
static JWT_EMERGENCY_ACCESS_INVITE_ISSUER: LazyLock<String> =
LazyLock::new(|| format!("{}|emergencyaccessinvite", CONFIG.domain_origin()));
static JWT_DELETE_ISSUER: LazyLock<String> = LazyLock::new(|| format!("{}|delete", CONFIG.domain_origin()));
static JWT_VERIFYEMAIL_ISSUER: LazyLock<String> = LazyLock::new(|| format!("{}|verifyemail", CONFIG.domain_origin()));
static JWT_ADMIN_ISSUER: LazyLock<String> = LazyLock::new(|| format!("{}|admin", CONFIG.domain_origin()));
static JWT_SEND_ISSUER: LazyLock<String> = LazyLock::new(|| format!("{}|send", CONFIG.domain_origin()));
static JWT_ORG_API_KEY_ISSUER: LazyLock<String> =
LazyLock::new(|| format!("{}|api.organization", CONFIG.domain_origin()));
static JWT_FILE_DOWNLOAD_ISSUER: LazyLock<String> =
LazyLock::new(|| format!("{}|file_download", CONFIG.domain_origin()));
static JWT_REGISTER_VERIFY_ISSUER: LazyLock<String> =
LazyLock::new(|| format!("{}|register_verify", CONFIG.domain_origin()));
static PRIVATE_RSA_KEY: OnceLock<EncodingKey> = OnceLock::new();
static PUBLIC_RSA_KEY: OnceLock<DecodingKey> = OnceLock::new();
pub async fn initialize_keys() -> Result<(), Error> {
use std::io::Error;
@ -54,7 +60,7 @@ pub async fn initialize_keys() -> Result<(), Error> {
.ok_or_else(|| Error::other("Private RSA key path filename is not valid UTF-8"))?
.to_string();
let operator = CONFIG.opendal_operator_for_path_type(PathType::RsaKey).map_err(Error::other)?;
let operator = CONFIG.opendal_operator_for_path_type(&PathType::RsaKey).map_err(Error::other)?;
let priv_key_buffer = match operator.read(&rsa_key_filename).await {
Ok(buffer) => Some(buffer),
@ -457,7 +463,7 @@ pub fn generate_delete_claims(uuid: String) -> BasicJwtClaims {
}
}
pub fn generate_verify_email_claims(user_id: UserId) -> BasicJwtClaims {
pub fn generate_verify_email_claims(user_id: &UserId) -> BasicJwtClaims {
let time_now = Utc::now();
let expire_hours = i64::from(CONFIG.invitation_expiration_hours());
BasicJwtClaims {
@ -604,16 +610,16 @@ impl<'r> FromRequest<'r> for Headers {
let device_id = claims.device;
let user_id = claims.sub;
let mut conn = match DbConn::from_request(request).await {
let conn = match DbConn::from_request(request).await {
Outcome::Success(conn) => conn,
_ => err_handler!("Error getting DB"),
};
let Some(device) = Device::find_by_uuid_and_user(&device_id, &user_id, &mut conn).await else {
let Some(device) = Device::find_by_uuid_and_user(&device_id, &user_id, &conn).await else {
err_handler!("Invalid device id")
};
let Some(user) = User::find_by_uuid(&user_id, &mut conn).await else {
let Some(user) = User::find_by_uuid(&user_id, &conn).await else {
err_handler!("Device has no user associated")
};
@ -633,7 +639,7 @@ impl<'r> FromRequest<'r> for Headers {
// This prevents checking this stamp exception for new requests.
let mut user = user;
user.reset_stamp_exception();
if let Err(e) = user.save(&mut conn).await {
if let Err(e) = user.save(&conn).await {
error!("Error updating user: {e:#?}");
}
err_handler!("Stamp exception is expired")
@ -696,9 +702,9 @@ impl<'r> FromRequest<'r> for OrgHeaders {
// 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.clone())
Some(org_id)
} else if let Some(Ok(org_id)) = request.query_value::<OrganizationId>("organizationId") {
Some(org_id.clone())
Some(org_id)
} else {
None
}
@ -706,13 +712,13 @@ impl<'r> FromRequest<'r> for OrgHeaders {
match url_org_id {
Some(org_id) if uuid::Uuid::parse_str(&org_id).is_ok() => {
let mut conn = match DbConn::from_request(request).await {
let conn = match DbConn::from_request(request).await {
Outcome::Success(conn) => conn,
_ => err_handler!("Error getting DB"),
};
let user = headers.user;
let Some(membership) = Membership::find_by_user_and_org(&user.uuid, &org_id, &mut conn).await else {
let Some(membership) = Membership::find_by_user_and_org(&user.uuid, &org_id, &conn).await else {
err_handler!("The current user isn't member of the organization");
};
@ -815,12 +821,12 @@ impl<'r> FromRequest<'r> for ManagerHeaders {
if headers.is_confirmed_and_manager() {
match get_col_id(request) {
Some(col_id) => {
let mut conn = match DbConn::from_request(request).await {
let conn = match DbConn::from_request(request).await {
Outcome::Success(conn) => conn,
_ => err_handler!("Error getting DB"),
};
if !Collection::can_access_collection(&headers.membership, &col_id, &mut 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")
}
}
@ -896,7 +902,7 @@ impl ManagerHeaders {
pub async fn from_loose(
h: ManagerHeadersLoose,
collections: &Vec<CollectionId>,
conn: &mut DbConn,
conn: &DbConn,
) -> Result<ManagerHeaders, Error> {
for col_id in collections {
if uuid::Uuid::parse_str(col_id.as_ref()).is_err() {
@ -1200,7 +1206,7 @@ pub async fn refresh_tokens(
ip: &ClientIp,
refresh_token: &str,
client_id: Option<String>,
conn: &mut DbConn,
conn: &DbConn,
) -> ApiResult<(Device, AuthTokens)> {
let refresh_claims = match decode_refresh(refresh_token) {
Err(err) => {

344
src/config.rs

@ -1,5 +1,6 @@
use std::{
env::consts::EXE_SUFFIX,
fmt,
process::exit,
sync::{
atomic::{AtomicBool, Ordering},
@ -8,16 +9,15 @@ use std::{
};
use job_scheduler_ng::Schedule;
use once_cell::sync::Lazy;
use reqwest::Url;
use serde::de::{self, Deserialize, Deserializer, MapAccess, Visitor};
use crate::{
db::DbConnType,
error::Error,
util::{get_env, get_env_bool, get_web_vault_version, is_valid_email, parse_experimental_client_feature_flags},
};
static CONFIG_FILE: Lazy<String> = Lazy::new(|| {
static CONFIG_FILE: LazyLock<String> = LazyLock::new(|| {
let data_folder = get_env("DATA_FOLDER").unwrap_or_else(|| String::from("data"));
get_env("CONFIG_FILE").unwrap_or_else(|| format!("{data_folder}/config.json"))
});
@ -34,7 +34,7 @@ static CONFIG_FILENAME: LazyLock<String> = LazyLock::new(|| {
pub static SKIP_CONFIG_VALIDATION: AtomicBool = AtomicBool::new(false);
pub static CONFIG: Lazy<Config> = Lazy::new(|| {
pub static CONFIG: LazyLock<Config> = LazyLock::new(|| {
std::thread::spawn(|| {
let rt = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap_or_else(|e| {
println!("Error loading config:\n {e:?}\n");
@ -56,6 +56,41 @@ pub static CONFIG: Lazy<Config> = Lazy::new(|| {
pub type Pass = String;
macro_rules! make_config {
// Support string print
( @supportstr $name:ident, $value:expr, Pass, option ) => { serde_json::to_value(&$value.as_ref().map(|_| String::from("***"))).unwrap() }; // Optional pass, we map to an Option<String> with "***"
( @supportstr $name:ident, $value:expr, Pass, $none_action:ident ) => { "***".into() }; // Required pass, we return "***"
( @supportstr $name:ident, $value:expr, $ty:ty, option ) => { serde_json::to_value(&$value).unwrap() }; // Optional other or string, we convert to json
( @supportstr $name:ident, $value:expr, String, $none_action:ident ) => { $value.as_str().into() }; // Required string value, we convert to json
( @supportstr $name:ident, $value:expr, $ty:ty, $none_action:ident ) => { ($value).into() }; // Required other value, we return as is or convert to json
// Group or empty string
( @show ) => { "" };
( @show $lit:literal ) => { $lit };
// Wrap the optionals in an Option type
( @type $ty:ty, option) => { Option<$ty> };
( @type $ty:ty, $id:ident) => { $ty };
// Generate the values depending on none_action
( @build $value:expr, $config:expr, option, ) => { $value };
( @build $value:expr, $config:expr, def, $default:expr ) => { $value.unwrap_or($default) };
( @build $value:expr, $config:expr, auto, $default_fn:expr ) => {{
match $value {
Some(v) => v,
None => {
let f: &dyn Fn(&ConfigItems) -> _ = &$default_fn;
f($config)
}
}
}};
( @build $value:expr, $config:expr, generated, $default_fn:expr ) => {{
let f: &dyn Fn(&ConfigItems) -> _ = &$default_fn;
f($config)
}};
( @getenv $name:expr, bool ) => { get_env_bool($name) };
( @getenv $name:expr, $ty:ident ) => { get_env($name) };
($(
$(#[doc = $groupdoc:literal])?
$group:ident $(: $group_enabled:ident)? {
@ -75,10 +110,103 @@ macro_rules! make_config {
_env: ConfigBuilder,
_usr: ConfigBuilder,
_overrides: Vec<String>,
_overrides: Vec<&'static str>,
}
// Custom Deserialize for ConfigBuilder, mainly based upon https://serde.rs/deserialize-struct.html
// This deserialize doesn't care if there are keys missing, or if there are duplicate keys
// In case of duplicate keys (which should never be possible unless manually edited), the last value is used!
// Main reason for this is removing the `visit_seq` function, which causes a lot of code generation not needed or used for this struct.
impl<'de> Deserialize<'de> for ConfigBuilder {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
const FIELDS: &[&str] = &[
$($(
stringify!($name),
)+)+
];
#[allow(non_camel_case_types)]
enum Field {
$($(
$name,
)+)+
__ignore,
}
impl<'de> Deserialize<'de> for Field {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct FieldVisitor;
impl Visitor<'_> for FieldVisitor {
type Value = Field;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("ConfigBuilder field identifier")
}
#[inline]
fn visit_str<E>(self, value: &str) -> Result<Field, E>
where
E: de::Error,
{
match value {
$($(
stringify!($name) => Ok(Field::$name),
)+)+
_ => Ok(Field::__ignore),
}
}
}
deserializer.deserialize_identifier(FieldVisitor)
}
}
struct ConfigBuilderVisitor;
impl<'de> Visitor<'de> for ConfigBuilderVisitor {
type Value = ConfigBuilder;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("struct ConfigBuilder")
}
#[inline]
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
let mut builder = ConfigBuilder::default();
while let Some(key) = map.next_key()? {
match key {
$($(
Field::$name => {
if builder.$name.is_some() {
return Err(de::Error::duplicate_field(stringify!($name)));
}
builder.$name = map.next_value()?;
}
)+)+
Field::__ignore => {
let _ = map.next_value::<de::IgnoredAny>()?;
}
}
}
Ok(builder)
}
}
deserializer.deserialize_struct("ConfigBuilder", FIELDS, ConfigBuilderVisitor)
}
}
#[derive(Clone, Default, Deserialize, Serialize)]
#[derive(Clone, Default, Serialize)]
pub struct ConfigBuilder {
$($(
#[serde(skip_serializing_if = "Option::is_none")]
@ -87,7 +215,6 @@ macro_rules! make_config {
}
impl ConfigBuilder {
#[allow(clippy::field_reassign_with_default)]
fn from_env() -> Self {
let env_file = get_env("ENV_FILE").unwrap_or_else(|| String::from(".env"));
match dotenvy::from_path(&env_file) {
@ -149,14 +276,14 @@ macro_rules! make_config {
/// Merges the values of both builders into a new builder.
/// If both have the same element, `other` wins.
fn merge(&self, other: &Self, show_overrides: bool, overrides: &mut Vec<String>) -> Self {
fn merge(&self, other: &Self, show_overrides: bool, overrides: &mut Vec<&str>) -> Self {
let mut builder = self.clone();
$($(
if let v @Some(_) = &other.$name {
builder.$name = v.clone();
if self.$name.is_some() {
overrides.push(pastey::paste!(stringify!([<$name:upper>])).into());
overrides.push(pastey::paste!(stringify!([<$name:upper>])));
}
}
)+)+
@ -175,7 +302,7 @@ macro_rules! make_config {
let mut config = ConfigItems::default();
let _domain_set = self.domain.is_some();
$($(
config.$name = make_config!{ @build self.$name.clone(), &config, $none_action, $($default)? };
config.$name = make_config! { @build self.$name.clone(), &config, $none_action, $($default)? };
)+)+
config.domain_set = _domain_set;
@ -195,24 +322,51 @@ macro_rules! make_config {
}
#[derive(Clone, Default)]
struct ConfigItems { $($( $name: make_config!{@type $ty, $none_action}, )+)+ }
struct ConfigItems { $($( $name: make_config! {@type $ty, $none_action}, )+)+ }
#[derive(Serialize)]
struct ElementDoc {
name: &'static str,
description: &'static str,
}
#[derive(Serialize)]
struct ElementData {
editable: bool,
name: &'static str,
value: serde_json::Value,
default: serde_json::Value,
#[serde(rename = "type")]
r#type: &'static str,
doc: ElementDoc,
overridden: bool,
}
#[derive(Serialize)]
pub struct GroupData {
group: &'static str,
grouptoggle: &'static str,
groupdoc: &'static str,
elements: Vec<ElementData>,
}
#[allow(unused)]
impl Config {
$($(
$(#[doc = $doc])+
pub fn $name(&self) -> make_config!{@type $ty, $none_action} {
pub fn $name(&self) -> make_config! {@type $ty, $none_action} {
self.inner.read().unwrap().config.$name.clone()
}
)+)+
pub fn prepare_json(&self) -> serde_json::Value {
let (def, cfg, overridden) = {
// Lock the inner as short as possible and clone what is needed to prevent deadlocks
let inner = &self.inner.read().unwrap();
(inner._env.build(), inner.config.clone(), inner._overrides.clone())
};
fn _get_form_type(rust_type: &str) -> &'static str {
fn _get_form_type(rust_type: &'static str) -> &'static str {
match rust_type {
"Pass" => "password",
"String" => "text",
@ -221,48 +375,36 @@ macro_rules! make_config {
}
}
fn _get_doc(doc: &str) -> serde_json::Value {
let mut split = doc.split("|>").map(str::trim);
// We do not use the json!() macro here since that causes a lot of macro recursion.
// This slows down compile time and it also causes issues with rust-analyzer
serde_json::Value::Object({
let mut doc_json = serde_json::Map::new();
doc_json.insert("name".into(), serde_json::to_value(split.next()).unwrap());
doc_json.insert("description".into(), serde_json::to_value(split.next()).unwrap());
doc_json
})
fn _get_doc(doc_str: &'static str) -> ElementDoc {
let mut split = doc_str.split("|>").map(str::trim);
ElementDoc {
name: split.next().unwrap_or_default(),
description: split.next().unwrap_or_default(),
}
}
// We do not use the json!() macro here since that causes a lot of macro recursion.
// This slows down compile time and it also causes issues with rust-analyzer
serde_json::Value::Array(<[_]>::into_vec(Box::new([
$(
serde_json::Value::Object({
let mut group = serde_json::Map::new();
group.insert("group".into(), (stringify!($group)).into());
group.insert("grouptoggle".into(), (stringify!($($group_enabled)?)).into());
group.insert("groupdoc".into(), (make_config!{ @show $($groupdoc)? }).into());
group.insert("elements".into(), serde_json::Value::Array(<[_]>::into_vec(Box::new([
$(
serde_json::Value::Object({
let mut element = serde_json::Map::new();
element.insert("editable".into(), ($editable).into());
element.insert("name".into(), (stringify!($name)).into());
element.insert("value".into(), serde_json::to_value(cfg.$name).unwrap());
element.insert("default".into(), serde_json::to_value(def.$name).unwrap());
element.insert("type".into(), (_get_form_type(stringify!($ty))).into());
element.insert("doc".into(), (_get_doc(concat!($($doc),+))).into());
element.insert("overridden".into(), (overridden.contains(&pastey::paste!(stringify!([<$name:upper>])).into())).into());
element
}),
)+
]))));
group
}),
)+
])))
let data: Vec<GroupData> = vec![
$( // This repetition is for each group
GroupData {
group: stringify!($group),
grouptoggle: stringify!($($group_enabled)?),
groupdoc: (make_config! { @show $($groupdoc)? }),
elements: vec![
$( // This repetition is for each element within a group
ElementData {
editable: $editable,
name: stringify!($name),
value: serde_json::to_value(&cfg.$name).unwrap_or_default(),
default: serde_json::to_value(&def.$name).unwrap_or_default(),
r#type: _get_form_type(stringify!($ty)),
doc: _get_doc(concat!($($doc),+)),
overridden: overridden.contains(&pastey::paste!(stringify!([<$name:upper>]))),
},
)+], // End of elements repetition
},
)+]; // End of groups repetition
serde_json::to_value(data).unwrap()
}
pub fn get_support_json(&self) -> serde_json::Value {
@ -270,8 +412,8 @@ macro_rules! make_config {
// Pass types will always be masked and no need to put them in the list.
// Besides Pass, only String types will be masked via _privacy_mask.
const PRIVACY_CONFIG: &[&str] = &[
"allowed_iframe_ancestors",
"allowed_connect_src",
"allowed_iframe_ancestors",
"database_url",
"domain_origin",
"domain_path",
@ -279,16 +421,18 @@ macro_rules! make_config {
"helo_name",
"org_creation_users",
"signups_domains_whitelist",
"_smtp_img_src",
"smtp_from_name",
"smtp_from",
"smtp_host",
"smtp_username",
"_smtp_img_src",
"sso_client_id",
"sso_authority",
"sso_callback_path",
"sso_client_id",
];
let cfg = {
// Lock the inner as short as possible and clone what is needed to prevent deadlocks
let inner = &self.inner.read().unwrap();
inner.config.clone()
};
@ -318,13 +462,21 @@ macro_rules! make_config {
serde_json::Value::Object({
let mut json = serde_json::Map::new();
$($(
json.insert(stringify!($name).into(), make_config!{ @supportstr $name, cfg.$name, $ty, $none_action });
json.insert(String::from(stringify!($name)), make_config! { @supportstr $name, cfg.$name, $ty, $none_action });
)+)+;
// Loop through all privacy sensitive keys and mask them
for mask_key in PRIVACY_CONFIG {
if let Some(value) = json.get_mut(*mask_key) {
if let Some(s) = value.as_str() {
*value = _privacy_mask(s).into();
}
}
}
json
})
}
pub fn get_overrides(&self) -> Vec<String> {
pub fn get_overrides(&self) -> Vec<&'static str> {
let overrides = {
let inner = &self.inner.read().unwrap();
inner._overrides.clone()
@ -333,55 +485,6 @@ macro_rules! make_config {
}
}
};
// Support string print
( @supportstr $name:ident, $value:expr, Pass, option ) => { serde_json::to_value($value.as_ref().map(|_| String::from("***"))).unwrap() }; // Optional pass, we map to an Option<String> with "***"
( @supportstr $name:ident, $value:expr, Pass, $none_action:ident ) => { "***".into() }; // Required pass, we return "***"
( @supportstr $name:ident, $value:expr, String, option ) => { // Optional other value, we return as is or convert to string to apply the privacy config
if PRIVACY_CONFIG.contains(&stringify!($name)) {
serde_json::to_value($value.as_ref().map(|x| _privacy_mask(x) )).unwrap()
} else {
serde_json::to_value($value).unwrap()
}
};
( @supportstr $name:ident, $value:expr, String, $none_action:ident ) => { // Required other value, we return as is or convert to string to apply the privacy config
if PRIVACY_CONFIG.contains(&stringify!($name)) {
_privacy_mask(&$value).into()
} else {
($value).into()
}
};
( @supportstr $name:ident, $value:expr, $ty:ty, option ) => { serde_json::to_value($value).unwrap() }; // Optional other value, we return as is or convert to string to apply the privacy config
( @supportstr $name:ident, $value:expr, $ty:ty, $none_action:ident ) => { ($value).into() }; // Required other value, we return as is or convert to string to apply the privacy config
// Group or empty string
( @show ) => { "" };
( @show $lit:literal ) => { $lit };
// Wrap the optionals in an Option type
( @type $ty:ty, option) => { Option<$ty> };
( @type $ty:ty, $id:ident) => { $ty };
// Generate the values depending on none_action
( @build $value:expr, $config:expr, option, ) => { $value };
( @build $value:expr, $config:expr, def, $default:expr ) => { $value.unwrap_or($default) };
( @build $value:expr, $config:expr, auto, $default_fn:expr ) => {{
match $value {
Some(v) => v,
None => {
let f: &dyn Fn(&ConfigItems) -> _ = &$default_fn;
f($config)
}
}
}};
( @build $value:expr, $config:expr, generated, $default_fn:expr ) => {{
let f: &dyn Fn(&ConfigItems) -> _ = &$default_fn;
f($config)
}};
( @getenv $name:expr, bool ) => { get_env_bool($name) };
( @getenv $name:expr, $ty:ident ) => { get_env($name) };
}
//STRUCTURE:
@ -827,12 +930,19 @@ make_config! {
fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
// Validate connection URL is valid and DB feature is enabled
let url = &cfg.database_url;
if DbConnType::from_url(url)? == DbConnType::sqlite && url.contains('/') {
let path = std::path::Path::new(&url);
if let Some(parent) = path.parent() {
if !parent.is_dir() {
err!(format!("SQLite database directory `{}` does not exist or is not a directory", parent.display()));
#[cfg(sqlite)]
{
use crate::db::DbConnType;
let url = &cfg.database_url;
if DbConnType::from_url(url)? == DbConnType::Sqlite && url.contains('/') {
let path = std::path::Path::new(&url);
if let Some(parent) = path.parent() {
if !parent.is_dir() {
err!(format!(
"SQLite database directory `{}` does not exist or is not a directory",
parent.display()
));
}
}
}
}
@ -1566,7 +1676,7 @@ impl Config {
if let Some(akey) = self._duo_akey() {
akey
} else {
let akey_s = crate::crypto::encode_random_bytes::<64>(data_encoding::BASE64);
let akey_s = crate::crypto::encode_random_bytes::<64>(&data_encoding::BASE64);
// Save the new value
let builder = ConfigBuilder {
@ -1590,7 +1700,7 @@ impl Config {
token.is_some() && !token.unwrap().trim().is_empty()
}
pub fn opendal_operator_for_path_type(&self, path_type: PathType) -> Result<opendal::Operator, Error> {
pub fn opendal_operator_for_path_type(&self, path_type: &PathType) -> Result<opendal::Operator, Error> {
let path = match path_type {
PathType::Data => self.data_folder(),
PathType::IconCache => self.icon_cache_folder(),
@ -1784,7 +1894,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.
// The default is based upon the version since this feature is added.
static WEB_VAULT_VERSION: Lazy<semver::Version> = Lazy::new(|| {
static WEB_VAULT_VERSION: LazyLock<semver::Version> = LazyLock::new(|| {
let vault_version = get_web_vault_version();
// Use a single regex capture to extract version components
let re = regex::Regex::new(r"(\d{4})\.(\d{1,2})\.(\d{1,2})").unwrap();
@ -1800,7 +1910,7 @@ static WEB_VAULT_VERSION: Lazy<semver::Version> = Lazy::new(|| {
// Configure the Vaultwarden 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.
static VW_VERSION: Lazy<semver::Version> = Lazy::new(|| {
static VW_VERSION: LazyLock<semver::Version> = LazyLock::new(|| {
let vw_version = crate::VERSION.unwrap_or("1.32.5");
// Use a single regex capture to extract version components
let re = regex::Regex::new(r"(\d{1})\.(\d{1,2})\.(\d{1,2})").unwrap();

4
src/crypto.rs

@ -48,7 +48,7 @@ pub fn get_random_bytes<const N: usize>() -> [u8; N] {
}
/// Encode random bytes using the provided function.
pub fn encode_random_bytes<const N: usize>(e: Encoding) -> String {
pub fn encode_random_bytes<const N: usize>(e: &Encoding) -> String {
e.encode(&get_random_bytes::<N>())
}
@ -81,7 +81,7 @@ pub fn get_random_string_alphanum(num_chars: usize) -> String {
}
pub fn generate_id<const N: usize>() -> String {
encode_random_bytes::<N>(HEXLOWER)
encode_random_bytes::<N>(&HEXLOWER)
}
pub fn generate_send_file_id() -> String {

579
src/db/mod.rs

@ -1,8 +1,14 @@
use std::{sync::Arc, time::Duration};
mod query_logger;
use std::{
sync::{Arc, OnceLock},
time::Duration,
};
use diesel::{
connection::SimpleConnection,
r2d2::{ConnectionManager, CustomizeConnection, Pool, PooledConnection},
r2d2::{CustomizeConnection, Pool, PooledConnection},
Connection, RunQueryDsl,
};
use rocket::{
@ -21,20 +27,7 @@ use crate::{
CONFIG,
};
#[cfg(sqlite)]
#[path = "schemas/sqlite/schema.rs"]
pub mod __sqlite_schema;
#[cfg(mysql)]
#[path = "schemas/mysql/schema.rs"]
pub mod __mysql_schema;
#[cfg(postgresql)]
#[path = "schemas/postgresql/schema.rs"]
pub mod __postgresql_schema;
// These changes are based on Rocket 0.5-rc wrapper of Diesel: https://github.com/SergioBenitez/Rocket/blob/v0.5-rc/contrib/sync_db_pools
// A wrapper around spawn_blocking that propagates panics to the calling code.
pub async fn run_blocking<F, R>(job: F) -> R
where
@ -51,162 +44,230 @@ where
}
// This is used to generate the main DbConn and DbPool enums, which contain one variant for each database supported
macro_rules! generate_connections {
( $( $name:ident: $ty:ty ),+ ) => {
#[allow(non_camel_case_types, dead_code)]
#[derive(Eq, PartialEq)]
pub enum DbConnType { $( $name, )+ }
pub struct DbConn {
conn: Arc<Mutex<Option<DbConnInner>>>,
permit: Option<OwnedSemaphorePermit>,
}
#[derive(diesel::MultiConnection)]
pub enum DbConnInner {
#[cfg(mysql)]
Mysql(diesel::mysql::MysqlConnection),
#[cfg(postgresql)]
Postgresql(diesel::pg::PgConnection),
#[cfg(sqlite)]
Sqlite(diesel::sqlite::SqliteConnection),
}
#[allow(non_camel_case_types)]
pub enum DbConnInner { $( #[cfg($name)] $name(PooledConnection<ConnectionManager< $ty >>), )+ }
/// Custom connection manager that implements manual connection establishment
pub struct DbConnManager {
database_url: String,
}
#[derive(Debug)]
pub struct DbConnOptions {
pub init_stmts: String,
impl DbConnManager {
pub fn new(database_url: &str) -> Self {
Self {
database_url: database_url.to_string(),
}
}
$( // Based on <https://stackoverflow.com/a/57717533>.
#[cfg($name)]
impl CustomizeConnection<$ty, diesel::r2d2::Error> for DbConnOptions {
fn on_acquire(&self, conn: &mut $ty) -> Result<(), diesel::r2d2::Error> {
if !self.init_stmts.is_empty() {
conn.batch_execute(&self.init_stmts).map_err(diesel::r2d2::Error::QueryError)?;
}
Ok(())
fn establish_connection(&self) -> Result<DbConnInner, diesel::r2d2::Error> {
match DbConnType::from_url(&self.database_url) {
#[cfg(mysql)]
Ok(DbConnType::Mysql) => {
let conn = diesel::mysql::MysqlConnection::establish(&self.database_url)?;
Ok(DbConnInner::Mysql(conn))
}
#[cfg(postgresql)]
Ok(DbConnType::Postgresql) => {
let conn = diesel::pg::PgConnection::establish(&self.database_url)?;
Ok(DbConnInner::Postgresql(conn))
}
#[cfg(sqlite)]
Ok(DbConnType::Sqlite) => {
let conn = diesel::sqlite::SqliteConnection::establish(&self.database_url)?;
Ok(DbConnInner::Sqlite(conn))
}
})+
#[derive(Clone)]
pub struct DbPool {
// This is an 'Option' so that we can drop the pool in a 'spawn_blocking'.
pool: Option<DbPoolInner>,
semaphore: Arc<Semaphore>
Err(e) => Err(diesel::r2d2::Error::ConnectionError(diesel::ConnectionError::InvalidConnectionUrl(
format!("Unable to estabilsh a connection: {e:?}"),
))),
}
}
}
impl diesel::r2d2::ManageConnection for DbConnManager {
type Connection = DbConnInner;
type Error = diesel::r2d2::Error;
fn connect(&self) -> Result<Self::Connection, Self::Error> {
self.establish_connection()
}
#[allow(non_camel_case_types)]
#[derive(Clone)]
pub enum DbPoolInner { $( #[cfg($name)] $name(Pool<ConnectionManager< $ty >>), )+ }
fn is_valid(&self, conn: &mut Self::Connection) -> Result<(), Self::Error> {
use diesel::r2d2::R2D2Connection;
conn.ping().map_err(diesel::r2d2::Error::QueryError)
}
impl Drop for DbConn {
fn drop(&mut self) {
let conn = Arc::clone(&self.conn);
let permit = self.permit.take();
fn has_broken(&self, conn: &mut Self::Connection) -> bool {
use diesel::r2d2::R2D2Connection;
conn.is_broken()
}
}
// Since connection can't be on the stack in an async fn during an
// await, we have to spawn a new blocking-safe thread...
tokio::task::spawn_blocking(move || {
// And then re-enter the runtime to wait on the async mutex, but in a blocking fashion.
let mut conn = tokio::runtime::Handle::current().block_on(conn.lock_owned());
#[derive(Eq, PartialEq)]
pub enum DbConnType {
#[cfg(mysql)]
Mysql,
#[cfg(postgresql)]
Postgresql,
#[cfg(sqlite)]
Sqlite,
}
if let Some(conn) = conn.take() {
drop(conn);
}
pub static ACTIVE_DB_TYPE: OnceLock<DbConnType> = OnceLock::new();
// Drop permit after the connection is dropped
drop(permit);
});
}
pub struct DbConn {
conn: Arc<Mutex<Option<PooledConnection<DbConnManager>>>>,
permit: Option<OwnedSemaphorePermit>,
}
#[derive(Debug)]
pub struct DbConnOptions {
pub init_stmts: String,
}
impl CustomizeConnection<DbConnInner, diesel::r2d2::Error> for DbConnOptions {
fn on_acquire(&self, conn: &mut DbConnInner) -> Result<(), diesel::r2d2::Error> {
if !self.init_stmts.is_empty() {
conn.batch_execute(&self.init_stmts).map_err(diesel::r2d2::Error::QueryError)?;
}
Ok(())
}
}
#[derive(Clone)]
pub struct DbPool {
// This is an 'Option' so that we can drop the pool in a 'spawn_blocking'.
pool: Option<Pool<DbConnManager>>,
semaphore: Arc<Semaphore>,
}
impl Drop for DbPool {
fn drop(&mut self) {
let pool = self.pool.take();
tokio::task::spawn_blocking(move || drop(pool));
impl Drop for DbConn {
fn drop(&mut self) {
let conn = Arc::clone(&self.conn);
let permit = self.permit.take();
// Since connection can't be on the stack in an async fn during an
// await, we have to spawn a new blocking-safe thread...
tokio::task::spawn_blocking(move || {
// And then re-enter the runtime to wait on the async mutex, but in a blocking fashion.
let mut conn = tokio::runtime::Handle::current().block_on(conn.lock_owned());
if let Some(conn) = conn.take() {
drop(conn);
}
// Drop permit after the connection is dropped
drop(permit);
});
}
}
impl Drop for DbPool {
fn drop(&mut self) {
let pool = self.pool.take();
// Only use spawn_blocking if the Tokio runtime is still available
// Otherwise the pool will be dropped on the current thread
if let Ok(handle) = tokio::runtime::Handle::try_current() {
handle.spawn_blocking(move || drop(pool));
}
}
}
impl DbPool {
// For the given database URL, guess its type, run migrations, create pool, and return it
pub fn from_config() -> Result<Self, Error> {
let db_url = CONFIG.database_url();
let conn_type = DbConnType::from_url(&db_url)?;
// Only set the default instrumentation if the log level is specifically set to either warn, info or debug
if log_enabled!(target: "vaultwarden::db::query_logger", log::Level::Warn)
|| log_enabled!(target: "vaultwarden::db::query_logger", log::Level::Info)
|| log_enabled!(target: "vaultwarden::db::query_logger", log::Level::Debug)
{
drop(diesel::connection::set_default_instrumentation(query_logger::simple_logger));
}
impl DbPool {
// For the given database URL, guess its type, run migrations, create pool, and return it
pub fn from_config() -> Result<Self, Error> {
let url = CONFIG.database_url();
let conn_type = DbConnType::from_url(&url)?;
match conn_type { $(
DbConnType::$name => {
#[cfg($name)]
{
pastey::paste!{ [< $name _migrations >]::run_migrations()?; }
let manager = ConnectionManager::new(&url);
let pool = Pool::builder()
.max_size(CONFIG.database_max_conns())
.min_idle(Some(CONFIG.database_min_conns()))
.idle_timeout(Some(Duration::from_secs(CONFIG.database_idle_timeout())))
.connection_timeout(Duration::from_secs(CONFIG.database_timeout()))
.connection_customizer(Box::new(DbConnOptions{
init_stmts: conn_type.get_init_stmts()
}))
.build(manager)
.map_res("Failed to create pool")?;
Ok(DbPool {
pool: Some(DbPoolInner::$name(pool)),
semaphore: Arc::new(Semaphore::new(CONFIG.database_max_conns() as usize)),
})
}
#[cfg(not($name))]
unreachable!("Trying to use a DB backend when it's feature is disabled")
},
)+ }
match conn_type {
#[cfg(mysql)]
DbConnType::Mysql => {
mysql_migrations::run_migrations(&db_url)?;
}
// Get a connection from the pool
pub async fn get(&self) -> Result<DbConn, Error> {
let duration = Duration::from_secs(CONFIG.database_timeout());
let permit = match timeout(duration, Arc::clone(&self.semaphore).acquire_owned()).await {
Ok(p) => p.expect("Semaphore should be open"),
Err(_) => {
err!("Timeout waiting for database connection");
}
};
match self.pool.as_ref().expect("DbPool.pool should always be Some()") { $(
#[cfg($name)]
DbPoolInner::$name(p) => {
let pool = p.clone();
let c = run_blocking(move || pool.get_timeout(duration)).await.map_res("Error retrieving connection from pool")?;
Ok(DbConn {
conn: Arc::new(Mutex::new(Some(DbConnInner::$name(c)))),
permit: Some(permit)
})
},
)+ }
#[cfg(postgresql)]
DbConnType::Postgresql => {
postgresql_migrations::run_migrations(&db_url)?;
}
#[cfg(sqlite)]
DbConnType::Sqlite => {
sqlite_migrations::run_migrations(&db_url)?;
}
}
};
}
#[cfg(not(query_logger))]
generate_connections! {
sqlite: diesel::sqlite::SqliteConnection,
mysql: diesel::mysql::MysqlConnection,
postgresql: diesel::pg::PgConnection
}
let max_conns = CONFIG.database_max_conns();
let manager = DbConnManager::new(&db_url);
let pool = Pool::builder()
.max_size(max_conns)
.min_idle(Some(CONFIG.database_min_conns()))
.idle_timeout(Some(Duration::from_secs(CONFIG.database_idle_timeout())))
.connection_timeout(Duration::from_secs(CONFIG.database_timeout()))
.connection_customizer(Box::new(DbConnOptions {
init_stmts: conn_type.get_init_stmts(),
}))
.build(manager)
.map_res("Failed to create pool")?;
// Set a global to determine the database more easily throughout the rest of the code
if ACTIVE_DB_TYPE.set(conn_type).is_err() {
error!("Tried to set the active database connection type more than once.")
}
#[cfg(query_logger)]
generate_connections! {
sqlite: diesel_logger::LoggingConnection<diesel::sqlite::SqliteConnection>,
mysql: diesel_logger::LoggingConnection<diesel::mysql::MysqlConnection>,
postgresql: diesel_logger::LoggingConnection<diesel::pg::PgConnection>
Ok(DbPool {
pool: Some(pool),
semaphore: Arc::new(Semaphore::new(max_conns as usize)),
})
}
// Get a connection from the pool
pub async fn get(&self) -> Result<DbConn, Error> {
let duration = Duration::from_secs(CONFIG.database_timeout());
let permit = match timeout(duration, Arc::clone(&self.semaphore).acquire_owned()).await {
Ok(p) => p.expect("Semaphore should be open"),
Err(_) => {
err!("Timeout waiting for database connection");
}
};
let p = self.pool.as_ref().expect("DbPool.pool should always be Some()");
let pool = p.clone();
let c =
run_blocking(move || pool.get_timeout(duration)).await.map_res("Error retrieving connection from pool")?;
Ok(DbConn {
conn: Arc::new(Mutex::new(Some(c))),
permit: Some(permit),
})
}
}
impl DbConnType {
pub fn from_url(url: &str) -> Result<DbConnType, Error> {
pub fn from_url(url: &str) -> Result<Self, Error> {
// Mysql
if url.starts_with("mysql:") {
if url.len() > 6 && &url[..6] == "mysql:" {
#[cfg(mysql)]
return Ok(DbConnType::mysql);
return Ok(DbConnType::Mysql);
#[cfg(not(mysql))]
err!("`DATABASE_URL` is a MySQL URL, but the 'mysql' feature is not enabled")
// Postgres
} else if url.starts_with("postgresql:") || url.starts_with("postgres:") {
// Postgresql
} else if url.len() > 11 && (&url[..11] == "postgresql:" || &url[..9] == "postgres:") {
#[cfg(postgresql)]
return Ok(DbConnType::postgresql);
return Ok(DbConnType::Postgresql);
#[cfg(not(postgresql))]
err!("`DATABASE_URL` is a PostgreSQL URL, but the 'postgresql' feature is not enabled")
@ -214,7 +275,7 @@ impl DbConnType {
//Sqlite
} else {
#[cfg(sqlite)]
return Ok(DbConnType::sqlite);
return Ok(DbConnType::Sqlite);
#[cfg(not(sqlite))]
err!("`DATABASE_URL` looks like a SQLite URL, but 'sqlite' feature is not enabled")
@ -232,175 +293,106 @@ impl DbConnType {
pub fn default_init_stmts(&self) -> String {
match self {
Self::sqlite => "PRAGMA busy_timeout = 5000; PRAGMA synchronous = NORMAL;".to_string(),
Self::mysql => String::new(),
Self::postgresql => String::new(),
#[cfg(mysql)]
Self::Mysql => String::new(),
#[cfg(postgresql)]
Self::Postgresql => String::new(),
#[cfg(sqlite)]
Self::Sqlite => "PRAGMA busy_timeout = 5000; PRAGMA synchronous = NORMAL;".to_string(),
}
}
}
#[macro_export]
macro_rules! db_run {
// Same for all dbs
( $conn:ident: $body:block ) => {
db_run! { $conn: sqlite, mysql, postgresql $body }
};
( @raw $conn:ident: $body:block ) => {
db_run! { @raw $conn: sqlite, mysql, postgresql $body }
};
// Different code for each db
( $conn:ident: $( $($db:ident),+ $body:block )+ ) => {{
#[allow(unused)] use diesel::prelude::*;
#[allow(unused)] use $crate::db::FromDb;
let conn = $conn.conn.clone();
impl DbConn {
pub async fn run<F, R>(&self, f: F) -> R
where
F: FnOnce(&mut DbConnInner) -> R + Send,
R: Send + 'static,
{
let conn = Arc::clone(&self.conn);
let mut conn = conn.lock_owned().await;
match conn.as_mut().expect("internal invariant broken: self.connection is Some") {
$($(
#[cfg($db)]
$crate::db::DbConnInner::$db($conn) => {
pastey::paste! {
#[allow(unused)] use $crate::db::[<__ $db _schema>]::{self as schema, *};
#[allow(unused)] use [<__ $db _model>]::*;
}
let conn = conn.as_mut().expect("Internal invariant broken: self.conn is Some");
tokio::task::block_in_place(move || { $body }) // Run blocking can't be used due to the 'static limitation, use block_in_place instead
},
)+)+
}
}};
( @raw $conn:ident: $( $($db:ident),+ $body:block )+ ) => {{
#[allow(unused)] use diesel::prelude::*;
#[allow(unused)] use $crate::db::FromDb;
let conn = $conn.conn.clone();
let mut conn = conn.lock_owned().await;
match conn.as_mut().expect("internal invariant broken: self.connection is Some") {
$($(
#[cfg($db)]
$crate::db::DbConnInner::$db($conn) => {
pastey::paste! {
#[allow(unused)] use $crate::db::[<__ $db _schema>]::{self as schema, *};
// @ RAW: #[allow(unused)] use [<__ $db _model>]::*;
}
tokio::task::block_in_place(move || { $body }) // Run blocking can't be used due to the 'static limitation, use block_in_place instead
},
)+)+
}
}};
}
pub trait FromDb {
type Output;
#[allow(clippy::wrong_self_convention)]
fn from_db(self) -> Self::Output;
}
impl<T: FromDb> FromDb for Vec<T> {
type Output = Vec<T::Output>;
#[inline(always)]
fn from_db(self) -> Self::Output {
self.into_iter().map(FromDb::from_db).collect()
// Run blocking can't be used due to the 'static limitation, use block_in_place instead
tokio::task::block_in_place(move || f(conn))
}
}
impl<T: FromDb> FromDb for Option<T> {
type Output = Option<T::Output>;
#[inline(always)]
fn from_db(self) -> Self::Output {
self.map(FromDb::from_db)
}
}
// For each struct eg. Cipher, we create a CipherDb inside a module named __$db_model (where $db is sqlite, mysql or postgresql),
// to implement the Diesel traits. We also provide methods to convert between them and the basic structs. Later, that module will be auto imported when using db_run!
#[macro_export]
macro_rules! db_object {
( $(
$( #[$attr:meta] )*
pub struct $name:ident {
$( $( #[$field_attr:meta] )* $vis:vis $field:ident : $typ:ty ),+
$(,)?
}
)+ ) => {
// Create the normal struct, without attributes
$( pub struct $name { $( /*$( #[$field_attr] )**/ $vis $field : $typ, )+ } )+
#[cfg(sqlite)]
pub mod __sqlite_model { $( db_object! { @db sqlite | $( #[$attr] )* | $name | $( $( #[$field_attr] )* $field : $typ ),+ } )+ }
#[cfg(mysql)]
pub mod __mysql_model { $( db_object! { @db mysql | $( #[$attr] )* | $name | $( $( #[$field_attr] )* $field : $typ ),+ } )+ }
#[cfg(postgresql)]
pub mod __postgresql_model { $( db_object! { @db postgresql | $( #[$attr] )* | $name | $( $( #[$field_attr] )* $field : $typ ),+ } )+ }
macro_rules! db_run {
( $conn:ident: $body:block ) => {
$conn.run(move |$conn| $body).await
};
( @db $db:ident | $( #[$attr:meta] )* | $name:ident | $( $( #[$field_attr:meta] )* $vis:vis $field:ident : $typ:ty),+) => {
pastey::paste! {
#[allow(unused)] use super::*;
#[allow(unused)] use diesel::prelude::*;
#[allow(unused)] use $crate::db::[<__ $db _schema>]::*;
$( #[$attr] )*
pub struct [<$name Db>] { $(
$( #[$field_attr] )* $vis $field : $typ,
)+ }
impl [<$name Db>] {
#[allow(clippy::wrong_self_convention)]
#[inline(always)] pub fn to_db(x: &super::$name) -> Self { Self { $( $field: x.$field.clone(), )+ } }
}
impl $crate::db::FromDb for [<$name Db>] {
type Output = super::$name;
#[allow(clippy::wrong_self_convention)]
#[inline(always)] fn from_db(self) -> Self::Output { super::$name { $( $field: self.$field, )+ } }
}
}
( $conn:ident: $( $($db:ident),+ $body:block )+ ) => {
$conn.run(move |$conn| {
match $conn {
$($(
#[cfg($db)]
pastey::paste!(&mut $crate::db::DbConnInner::[<$db:camel>](ref mut $conn)) => {
$body
},
)+)+}
}).await
};
}
pub mod schema;
// Reexport the models, needs to be after the macros are defined so it can access them
pub mod models;
/// Creates a back-up of the sqlite database
/// MySQL/MariaDB and PostgreSQL are not supported.
pub async fn backup_database(conn: &mut DbConn) -> Result<String, Error> {
db_run! {@raw conn:
postgresql, mysql {
let _ = conn;
err!("PostgreSQL and MySQL/MariaDB do not support this backup feature");
}
sqlite {
let db_url = CONFIG.database_url();
let db_path = std::path::Path::new(&db_url).parent().unwrap();
let backup_file = db_path
.join(format!("db_{}.sqlite3", chrono::Utc::now().format("%Y%m%d_%H%M%S")))
.to_string_lossy()
.into_owned();
diesel::sql_query(format!("VACUUM INTO '{backup_file}'")).execute(conn)?;
Ok(backup_file)
#[cfg(sqlite)]
pub fn backup_sqlite() -> Result<String, Error> {
use diesel::Connection;
use std::{fs::File, io::Write};
let db_url = CONFIG.database_url();
if DbConnType::from_url(&CONFIG.database_url()).map(|t| t == DbConnType::Sqlite).unwrap_or(false) {
// Since we do not allow any schema for sqlite database_url's like `file:` or `sqlite:` to be set, we can assume here it isn't
// This way we can set a readonly flag on the opening mode without issues.
let mut conn = diesel::sqlite::SqliteConnection::establish(&format!("sqlite://{db_url}?mode=ro"))?;
let db_path = std::path::Path::new(&db_url).parent().unwrap();
let backup_file = db_path
.join(format!("db_{}.sqlite3", chrono::Utc::now().format("%Y%m%d_%H%M%S")))
.to_string_lossy()
.into_owned();
match File::create(backup_file.clone()) {
Ok(mut f) => {
let serialized_db = conn.serialize_database_to_buffer();
f.write_all(serialized_db.as_slice()).expect("Error writing SQLite backup");
Ok(backup_file)
}
Err(e) => {
err_silent!(format!("Unable to save SQLite backup: {e:?}"))
}
}
} else {
err_silent!("The database type is not SQLite. Backups only works for SQLite databases")
}
}
#[cfg(not(sqlite))]
pub fn backup_sqlite() -> Result<String, Error> {
err_silent!("The database type is not SQLite. Backups only works for SQLite databases")
}
/// Get the SQL Server version
pub async fn get_sql_server_version(conn: &mut DbConn) -> String {
db_run! {@raw conn:
postgresql, mysql {
define_sql_function!{
fn version() -> diesel::sql_types::Text;
}
diesel::select(version()).get_result::<String>(conn).unwrap_or_else(|_| "Unknown".to_string())
pub async fn get_sql_server_version(conn: &DbConn) -> String {
db_run! { conn:
postgresql,mysql {
diesel::select(diesel::dsl::sql::<diesel::sql_types::Text>("version();"))
.get_result::<String>(conn)
.unwrap_or_else(|_| "Unknown".to_string())
}
sqlite {
define_sql_function!{
fn sqlite_version() -> diesel::sql_types::Text;
}
diesel::select(sqlite_version()).get_result::<String>(conn).unwrap_or_else(|_| "Unknown".to_string())
diesel::select(diesel::dsl::sql::<diesel::sql_types::Text>("sqlite_version();"))
.get_result::<String>(conn)
.unwrap_or_else(|_| "Unknown".to_string())
}
}
}
@ -428,16 +420,14 @@ impl<'r> FromRequest<'r> for DbConn {
// https://docs.rs/diesel_migrations/*/diesel_migrations/macro.embed_migrations.html
#[cfg(sqlite)]
mod sqlite_migrations {
use diesel::{Connection, RunQueryDsl};
use diesel_migrations::{EmbeddedMigrations, MigrationHarness};
pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations/sqlite");
pub fn run_migrations() -> Result<(), super::Error> {
use diesel::{Connection, RunQueryDsl};
let url = crate::CONFIG.database_url();
pub fn run_migrations(db_url: &str) -> Result<(), super::Error> {
// Establish a connection to the sqlite database (this will create a new one, if it does
// not exist, and exit if there is an error).
let mut connection = diesel::sqlite::SqliteConnection::establish(&url)?;
let mut connection = diesel::sqlite::SqliteConnection::establish(db_url)?;
// Run the migrations after successfully establishing a connection
// Disable Foreign Key Checks during migration
@ -458,15 +448,15 @@ mod sqlite_migrations {
#[cfg(mysql)]
mod mysql_migrations {
use diesel::{Connection, RunQueryDsl};
use diesel_migrations::{EmbeddedMigrations, MigrationHarness};
pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations/mysql");
pub fn run_migrations() -> Result<(), super::Error> {
use diesel::{Connection, RunQueryDsl};
pub fn run_migrations(db_url: &str) -> Result<(), super::Error> {
// Make sure the database is up to date (create if it doesn't exist, or run the migrations)
let mut connection = diesel::mysql::MysqlConnection::establish(&crate::CONFIG.database_url())?;
// Disable Foreign Key Checks during migration
let mut connection = diesel::mysql::MysqlConnection::establish(db_url)?;
// Disable Foreign Key Checks during migration
// Scoped to a connection/session.
diesel::sql_query("SET FOREIGN_KEY_CHECKS = 0")
.execute(&mut connection)
@ -479,13 +469,14 @@ mod mysql_migrations {
#[cfg(postgresql)]
mod postgresql_migrations {
use diesel::Connection;
use diesel_migrations::{EmbeddedMigrations, MigrationHarness};
pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations/postgresql");
pub fn run_migrations() -> Result<(), super::Error> {
use diesel::Connection;
pub fn run_migrations(db_url: &str) -> Result<(), super::Error> {
// Make sure the database is up to date (create if it doesn't exist, or run the migrations)
let mut connection = diesel::pg::PgConnection::establish(&crate::CONFIG.database_url())?;
let mut connection = diesel::pg::PgConnection::establish(db_url)?;
connection.run_pending_migrations(MIGRATIONS).expect("Error running migrations");
Ok(())
}

74
src/db/models/attachment.rs

@ -1,25 +1,24 @@
use std::time::Duration;
use bigdecimal::{BigDecimal, ToPrimitive};
use derive_more::{AsRef, Deref, Display};
use diesel::prelude::*;
use serde_json::Value;
use std::time::Duration;
use super::{CipherId, OrganizationId, UserId};
use crate::db::schema::{attachments, ciphers};
use crate::{config::PathType, CONFIG};
use macros::IdFromParam;
db_object! {
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = attachments)]
#[diesel(treat_none_as_null = true)]
#[diesel(primary_key(id))]
pub struct Attachment {
pub id: AttachmentId,
pub cipher_uuid: CipherId,
pub file_name: String, // encrypted
pub file_size: i64,
pub akey: Option<String>,
}
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = attachments)]
#[diesel(treat_none_as_null = true)]
#[diesel(primary_key(id))]
pub struct Attachment {
pub id: AttachmentId,
pub cipher_uuid: CipherId,
pub file_name: String, // encrypted
pub file_size: i64,
pub akey: Option<String>,
}
/// Local methods
@ -45,7 +44,7 @@ impl Attachment {
}
pub async fn get_url(&self, host: &str) -> Result<String, crate::Error> {
let operator = CONFIG.opendal_operator_for_path_type(PathType::Attachments)?;
let operator = CONFIG.opendal_operator_for_path_type(&PathType::Attachments)?;
if operator.info().scheme() == opendal::Scheme::Fs {
let token = encode_jwt(&generate_file_download_claims(self.cipher_uuid.clone(), self.id.clone()));
@ -76,11 +75,11 @@ use crate::error::MapResult;
/// Database methods
impl Attachment {
pub async fn save(&self, conn: &mut DbConn) -> EmptyResult {
pub async fn save(&self, conn: &DbConn) -> EmptyResult {
db_run! { conn:
sqlite, mysql {
match diesel::replace_into(attachments::table)
.values(AttachmentDb::to_db(self))
.values(self)
.execute(conn)
{
Ok(_) => Ok(()),
@ -88,7 +87,7 @@ impl Attachment {
Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => {
diesel::update(attachments::table)
.filter(attachments::id.eq(&self.id))
.set(AttachmentDb::to_db(self))
.set(self)
.execute(conn)
.map_res("Error saving attachment")
}
@ -96,29 +95,29 @@ impl Attachment {
}.map_res("Error saving attachment")
}
postgresql {
let value = AttachmentDb::to_db(self);
diesel::insert_into(attachments::table)
.values(&value)
.values(self)
.on_conflict(attachments::id)
.do_update()
.set(&value)
.set(self)
.execute(conn)
.map_res("Error saving attachment")
}
}
}
pub async fn delete(&self, conn: &mut DbConn) -> EmptyResult {
pub async fn delete(&self, conn: &DbConn) -> EmptyResult {
db_run! { conn: {
crate::util::retry(
|| diesel::delete(attachments::table.filter(attachments::id.eq(&self.id))).execute(conn),
crate::util::retry(||
diesel::delete(attachments::table.filter(attachments::id.eq(&self.id)))
.execute(conn),
10,
)
.map(|_| ())
.map_res("Error deleting attachment")
}}?;
let operator = CONFIG.opendal_operator_for_path_type(PathType::Attachments)?;
let operator = CONFIG.opendal_operator_for_path_type(&PathType::Attachments)?;
let file_path = self.get_file_path();
if let Err(e) = operator.delete(&file_path).await {
@ -132,34 +131,32 @@ impl Attachment {
Ok(())
}
pub async fn delete_all_by_cipher(cipher_uuid: &CipherId, conn: &mut DbConn) -> EmptyResult {
pub async fn delete_all_by_cipher(cipher_uuid: &CipherId, conn: &DbConn) -> EmptyResult {
for attachment in Attachment::find_by_cipher(cipher_uuid, conn).await {
attachment.delete(conn).await?;
}
Ok(())
}
pub async fn find_by_id(id: &AttachmentId, conn: &mut DbConn) -> Option<Self> {
pub async fn find_by_id(id: &AttachmentId, conn: &DbConn) -> Option<Self> {
db_run! { conn: {
attachments::table
.filter(attachments::id.eq(id.to_lowercase()))
.first::<AttachmentDb>(conn)
.first::<Self>(conn)
.ok()
.from_db()
}}
}
pub async fn find_by_cipher(cipher_uuid: &CipherId, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_cipher(cipher_uuid: &CipherId, conn: &DbConn) -> Vec<Self> {
db_run! { conn: {
attachments::table
.filter(attachments::cipher_uuid.eq(cipher_uuid))
.load::<AttachmentDb>(conn)
.load::<Self>(conn)
.expect("Error loading attachments")
.from_db()
}}
}
pub async fn size_by_user(user_uuid: &UserId, conn: &mut DbConn) -> i64 {
pub async fn size_by_user(user_uuid: &UserId, conn: &DbConn) -> i64 {
db_run! { conn: {
let result: Option<BigDecimal> = attachments::table
.left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid)))
@ -176,7 +173,7 @@ impl Attachment {
}}
}
pub async fn count_by_user(user_uuid: &UserId, conn: &mut DbConn) -> i64 {
pub async fn count_by_user(user_uuid: &UserId, conn: &DbConn) -> i64 {
db_run! { conn: {
attachments::table
.left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid)))
@ -187,7 +184,7 @@ impl Attachment {
}}
}
pub async fn size_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> i64 {
pub async fn size_by_org(org_uuid: &OrganizationId, conn: &DbConn) -> i64 {
db_run! { conn: {
let result: Option<BigDecimal> = attachments::table
.left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid)))
@ -204,7 +201,7 @@ impl Attachment {
}}
}
pub async fn count_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> i64 {
pub async fn count_by_org(org_uuid: &OrganizationId, conn: &DbConn) -> i64 {
db_run! { conn: {
attachments::table
.left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid)))
@ -221,7 +218,7 @@ impl Attachment {
pub async fn find_all_by_user_and_orgs(
user_uuid: &UserId,
org_uuids: &Vec<OrganizationId>,
conn: &mut DbConn,
conn: &DbConn,
) -> Vec<Self> {
db_run! { conn: {
attachments::table
@ -229,9 +226,8 @@ impl Attachment {
.filter(ciphers::user_uuid.eq(user_uuid))
.or_filter(ciphers::organization_uuid.eq_any(org_uuids))
.select(attachments::all_columns)
.load::<AttachmentDb>(conn)
.load::<Self>(conn)
.expect("Error loading attachments")
.from_db()
}}
}
}

94
src/db/models/auth_request.rs

@ -1,38 +1,38 @@
use super::{DeviceId, OrganizationId, UserId};
use crate::db::schema::auth_requests;
use crate::{crypto::ct_eq, util::format_date};
use chrono::{NaiveDateTime, Utc};
use derive_more::{AsRef, Deref, Display, From};
use diesel::prelude::*;
use macros::UuidFromParam;
use serde_json::Value;
db_object! {
#[derive(Identifiable, Queryable, Insertable, AsChangeset, Deserialize, Serialize)]
#[diesel(table_name = auth_requests)]
#[diesel(treat_none_as_null = true)]
#[diesel(primary_key(uuid))]
pub struct AuthRequest {
pub uuid: AuthRequestId,
pub user_uuid: UserId,
pub organization_uuid: Option<OrganizationId>,
#[derive(Identifiable, Queryable, Insertable, AsChangeset, Deserialize, Serialize)]
#[diesel(table_name = auth_requests)]
#[diesel(treat_none_as_null = true)]
#[diesel(primary_key(uuid))]
pub struct AuthRequest {
pub uuid: AuthRequestId,
pub user_uuid: UserId,
pub organization_uuid: Option<OrganizationId>,
pub request_device_identifier: DeviceId,
pub device_type: i32, // https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/Enums/DeviceType.cs
pub request_device_identifier: DeviceId,
pub device_type: i32, // https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/Enums/DeviceType.cs
pub request_ip: String,
pub response_device_id: Option<DeviceId>,
pub request_ip: String,
pub response_device_id: Option<DeviceId>,
pub access_code: String,
pub public_key: String,
pub access_code: String,
pub public_key: String,
pub enc_key: Option<String>,
pub enc_key: Option<String>,
pub master_password_hash: Option<String>,
pub approved: Option<bool>,
pub creation_date: NaiveDateTime,
pub response_date: Option<NaiveDateTime>,
pub master_password_hash: Option<String>,
pub approved: Option<bool>,
pub creation_date: NaiveDateTime,
pub response_date: Option<NaiveDateTime>,
pub authentication_date: Option<NaiveDateTime>,
}
pub authentication_date: Option<NaiveDateTime>,
}
impl AuthRequest {
@ -80,11 +80,11 @@ use crate::api::EmptyResult;
use crate::error::MapResult;
impl AuthRequest {
pub async fn save(&mut self, conn: &mut DbConn) -> EmptyResult {
pub async fn save(&mut self, conn: &DbConn) -> EmptyResult {
db_run! { conn:
sqlite, mysql {
match diesel::replace_into(auth_requests::table)
.values(AuthRequestDb::to_db(self))
.values(&*self)
.execute(conn)
{
Ok(_) => Ok(()),
@ -92,7 +92,7 @@ impl AuthRequest {
Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => {
diesel::update(auth_requests::table)
.filter(auth_requests::uuid.eq(&self.uuid))
.set(AuthRequestDb::to_db(self))
.set(&*self)
.execute(conn)
.map_res("Error auth_request")
}
@ -100,71 +100,71 @@ impl AuthRequest {
}.map_res("Error auth_request")
}
postgresql {
let value = AuthRequestDb::to_db(self);
diesel::insert_into(auth_requests::table)
.values(&value)
.values(&*self)
.on_conflict(auth_requests::uuid)
.do_update()
.set(&value)
.set(&*self)
.execute(conn)
.map_res("Error saving auth_request")
}
}
}
pub async fn find_by_uuid(uuid: &AuthRequestId, conn: &mut DbConn) -> Option<Self> {
db_run! {conn: {
pub async fn find_by_uuid(uuid: &AuthRequestId, conn: &DbConn) -> Option<Self> {
db_run! { conn: {
auth_requests::table
.filter(auth_requests::uuid.eq(uuid))
.first::<AuthRequestDb>(conn)
.first::<Self>(conn)
.ok()
.from_db()
}}
}
pub async fn find_by_uuid_and_user(uuid: &AuthRequestId, user_uuid: &UserId, conn: &mut DbConn) -> Option<Self> {
db_run! {conn: {
pub async fn find_by_uuid_and_user(uuid: &AuthRequestId, user_uuid: &UserId, conn: &DbConn) -> Option<Self> {
db_run! { conn: {
auth_requests::table
.filter(auth_requests::uuid.eq(uuid))
.filter(auth_requests::user_uuid.eq(user_uuid))
.first::<AuthRequestDb>(conn)
.first::<Self>(conn)
.ok()
.from_db()
}}
}
pub async fn find_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
db_run! {conn: {
pub async fn find_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<Self> {
db_run! { conn: {
auth_requests::table
.filter(auth_requests::user_uuid.eq(user_uuid))
.load::<AuthRequestDb>(conn).expect("Error loading auth_requests").from_db()
.load::<Self>(conn)
.expect("Error loading auth_requests")
}}
}
pub async fn find_by_user_and_requested_device(
user_uuid: &UserId,
device_uuid: &DeviceId,
conn: &mut DbConn,
conn: &DbConn,
) -> Option<Self> {
db_run! {conn: {
db_run! { conn: {
auth_requests::table
.filter(auth_requests::user_uuid.eq(user_uuid))
.filter(auth_requests::request_device_identifier.eq(device_uuid))
.filter(auth_requests::approved.is_null())
.order_by(auth_requests::creation_date.desc())
.first::<AuthRequestDb>(conn).ok().from_db()
.first::<Self>(conn)
.ok()
}}
}
pub async fn find_created_before(dt: &NaiveDateTime, conn: &mut DbConn) -> Vec<Self> {
db_run! {conn: {
pub async fn find_created_before(dt: &NaiveDateTime, conn: &DbConn) -> Vec<Self> {
db_run! { conn: {
auth_requests::table
.filter(auth_requests::creation_date.lt(dt))
.load::<AuthRequestDb>(conn).expect("Error loading auth_requests").from_db()
.load::<Self>(conn)
.expect("Error loading auth_requests")
}}
}
pub async fn delete(&self, conn: &mut DbConn) -> EmptyResult {
pub async fn delete(&self, conn: &DbConn) -> EmptyResult {
db_run! { conn: {
diesel::delete(auth_requests::table.filter(auth_requests::uuid.eq(&self.uuid)))
.execute(conn)
@ -176,7 +176,7 @@ impl AuthRequest {
ct_eq(&self.access_code, access_code)
}
pub async fn purge_expired_auth_requests(conn: &mut DbConn) {
pub async fn purge_expired_auth_requests(conn: &DbConn) {
let expiry_time = Utc::now().naive_utc() - chrono::TimeDelta::try_minutes(5).unwrap(); //after 5 minutes, clients reject the request
for auth_request in Self::find_created_before(&expiry_time, conn).await {
auth_request.delete(conn).await.ok();

233
src/db/models/cipher.rs

@ -1,7 +1,12 @@
use crate::db::schema::{
ciphers, ciphers_collections, collections, collections_groups, folders, folders_ciphers, groups, groups_users,
users_collections, users_organizations,
};
use crate::util::LowerCase;
use crate::CONFIG;
use chrono::{NaiveDateTime, TimeDelta, Utc};
use derive_more::{AsRef, Deref, Display, From};
use diesel::prelude::*;
use serde_json::Value;
use super::{
@ -13,39 +18,37 @@ use macros::UuidFromParam;
use std::borrow::Cow;
db_object! {
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = ciphers)]
#[diesel(treat_none_as_null = true)]
#[diesel(primary_key(uuid))]
pub struct Cipher {
pub uuid: CipherId,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
pub user_uuid: Option<UserId>,
pub organization_uuid: Option<OrganizationId>,
pub key: Option<String>,
/*
Login = 1,
SecureNote = 2,
Card = 3,
Identity = 4,
SshKey = 5
*/
pub atype: i32,
pub name: String,
pub notes: Option<String>,
pub fields: Option<String>,
pub data: String,
pub password_history: Option<String>,
pub deleted_at: Option<NaiveDateTime>,
pub reprompt: Option<i32>,
}
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = ciphers)]
#[diesel(treat_none_as_null = true)]
#[diesel(primary_key(uuid))]
pub struct Cipher {
pub uuid: CipherId,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
pub user_uuid: Option<UserId>,
pub organization_uuid: Option<OrganizationId>,
pub key: Option<String>,
/*
Login = 1,
SecureNote = 2,
Card = 3,
Identity = 4,
SshKey = 5
*/
pub atype: i32,
pub name: String,
pub notes: Option<String>,
pub fields: Option<String>,
pub data: String,
pub password_history: Option<String>,
pub deleted_at: Option<NaiveDateTime>,
pub reprompt: Option<i32>,
}
pub enum RepromptType {
@ -140,7 +143,7 @@ impl Cipher {
user_uuid: &UserId,
cipher_sync_data: Option<&CipherSyncData>,
sync_type: CipherSyncType,
conn: &mut DbConn,
conn: &DbConn,
) -> Result<Value, crate::Error> {
use crate::util::{format_date, validate_and_format_date};
@ -402,7 +405,7 @@ impl Cipher {
Ok(json_object)
}
pub async fn update_users_revision(&self, conn: &mut DbConn) -> Vec<UserId> {
pub async fn update_users_revision(&self, conn: &DbConn) -> Vec<UserId> {
let mut user_uuids = Vec::new();
match self.user_uuid {
Some(ref user_uuid) => {
@ -430,14 +433,14 @@ impl Cipher {
user_uuids
}
pub async fn save(&mut self, conn: &mut DbConn) -> EmptyResult {
pub async fn save(&mut self, conn: &DbConn) -> EmptyResult {
self.update_users_revision(conn).await;
self.updated_at = Utc::now().naive_utc();
db_run! { conn:
sqlite, mysql {
match diesel::replace_into(ciphers::table)
.values(CipherDb::to_db(self))
.values(&*self)
.execute(conn)
{
Ok(_) => Ok(()),
@ -445,7 +448,7 @@ impl Cipher {
Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => {
diesel::update(ciphers::table)
.filter(ciphers::uuid.eq(&self.uuid))
.set(CipherDb::to_db(self))
.set(&*self)
.execute(conn)
.map_res("Error saving cipher")
}
@ -453,19 +456,18 @@ impl Cipher {
}.map_res("Error saving cipher")
}
postgresql {
let value = CipherDb::to_db(self);
diesel::insert_into(ciphers::table)
.values(&value)
.values(&*self)
.on_conflict(ciphers::uuid)
.do_update()
.set(&value)
.set(&*self)
.execute(conn)
.map_res("Error saving cipher")
}
}
}
pub async fn delete(&self, conn: &mut DbConn) -> EmptyResult {
pub async fn delete(&self, conn: &DbConn) -> EmptyResult {
self.update_users_revision(conn).await;
FolderCipher::delete_all_by_cipher(&self.uuid, conn).await?;
@ -480,7 +482,7 @@ impl Cipher {
}}
}
pub async fn delete_all_by_organization(org_uuid: &OrganizationId, conn: &mut DbConn) -> EmptyResult {
pub async fn delete_all_by_organization(org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult {
// TODO: Optimize this by executing a DELETE directly on the database, instead of first fetching.
for cipher in Self::find_by_org(org_uuid, conn).await {
cipher.delete(conn).await?;
@ -488,7 +490,7 @@ impl Cipher {
Ok(())
}
pub async fn delete_all_by_user(user_uuid: &UserId, conn: &mut DbConn) -> EmptyResult {
pub async fn delete_all_by_user(user_uuid: &UserId, conn: &DbConn) -> EmptyResult {
for cipher in Self::find_owned_by_user(user_uuid, conn).await {
cipher.delete(conn).await?;
}
@ -496,7 +498,7 @@ impl Cipher {
}
/// Purge all ciphers that are old enough to be auto-deleted.
pub async fn purge_trash(conn: &mut DbConn) {
pub async fn purge_trash(conn: &DbConn) {
if let Some(auto_delete_days) = CONFIG.trash_auto_delete_days() {
let now = Utc::now().naive_utc();
let dt = now - TimeDelta::try_days(auto_delete_days).unwrap();
@ -510,7 +512,7 @@ impl Cipher {
&self,
folder_uuid: Option<FolderId>,
user_uuid: &UserId,
conn: &mut DbConn,
conn: &DbConn,
) -> EmptyResult {
User::update_uuid_revision(user_uuid, conn).await;
@ -550,7 +552,7 @@ impl Cipher {
&self,
user_uuid: &UserId,
cipher_sync_data: Option<&CipherSyncData>,
conn: &mut DbConn,
conn: &DbConn,
) -> bool {
if let Some(ref org_uuid) = self.organization_uuid {
if let Some(cipher_sync_data) = cipher_sync_data {
@ -569,7 +571,7 @@ impl Cipher {
&self,
user_uuid: &UserId,
cipher_sync_data: Option<&CipherSyncData>,
conn: &mut DbConn,
conn: &DbConn,
) -> bool {
if !CONFIG.org_groups_enabled() {
return false;
@ -593,7 +595,7 @@ impl Cipher {
&self,
user_uuid: &UserId,
cipher_sync_data: Option<&CipherSyncData>,
conn: &mut DbConn,
conn: &DbConn,
) -> Option<(bool, bool, bool)> {
// Check whether this cipher is directly owned by the user, or is in
// a collection that the user has full access to. If so, there are no
@ -659,12 +661,8 @@ impl Cipher {
Some((read_only, hide_passwords, manage))
}
async fn get_user_collections_access_flags(
&self,
user_uuid: &UserId,
conn: &mut DbConn,
) -> Vec<(bool, bool, bool)> {
db_run! {conn: {
async fn get_user_collections_access_flags(&self, user_uuid: &UserId, conn: &DbConn) -> Vec<(bool, bool, bool)> {
db_run! { conn: {
// Check whether this cipher is in any collections accessible to the
// user. If so, retrieve the access flags for each collection.
ciphers::table
@ -680,15 +678,11 @@ impl Cipher {
}}
}
async fn get_group_collections_access_flags(
&self,
user_uuid: &UserId,
conn: &mut DbConn,
) -> Vec<(bool, bool, bool)> {
async fn get_group_collections_access_flags(&self, user_uuid: &UserId, conn: &DbConn) -> Vec<(bool, bool, bool)> {
if !CONFIG.org_groups_enabled() {
return Vec::new();
}
db_run! {conn: {
db_run! { conn: {
ciphers::table
.filter(ciphers::uuid.eq(&self.uuid))
.inner_join(ciphers_collections::table.on(
@ -710,7 +704,7 @@ impl Cipher {
}}
}
pub async fn is_write_accessible_to_user(&self, user_uuid: &UserId, conn: &mut DbConn) -> bool {
pub async fn is_write_accessible_to_user(&self, user_uuid: &UserId, conn: &DbConn) -> bool {
match self.get_access_restrictions(user_uuid, None, conn).await {
Some((read_only, _hide_passwords, manage)) => !read_only || manage,
None => false,
@ -719,32 +713,32 @@ impl Cipher {
// used for checking if collection can be edited (only if user has access to a collection they
// can write to and also passwords are not hidden to prevent privilege escalation)
pub async fn is_in_editable_collection_by_user(&self, user_uuid: &UserId, conn: &mut DbConn) -> bool {
pub async fn is_in_editable_collection_by_user(&self, user_uuid: &UserId, conn: &DbConn) -> bool {
match self.get_access_restrictions(user_uuid, None, conn).await {
Some((read_only, hide_passwords, manage)) => (!read_only && !hide_passwords) || manage,
None => false,
}
}
pub async fn is_accessible_to_user(&self, user_uuid: &UserId, conn: &mut DbConn) -> bool {
pub async fn is_accessible_to_user(&self, user_uuid: &UserId, conn: &DbConn) -> bool {
self.get_access_restrictions(user_uuid, None, conn).await.is_some()
}
// Returns whether this cipher is a favorite of the specified user.
pub async fn is_favorite(&self, user_uuid: &UserId, conn: &mut DbConn) -> bool {
pub async fn is_favorite(&self, user_uuid: &UserId, conn: &DbConn) -> bool {
Favorite::is_favorite(&self.uuid, user_uuid, conn).await
}
// Sets whether this cipher is a favorite of the specified user.
pub async fn set_favorite(&self, favorite: Option<bool>, user_uuid: &UserId, conn: &mut DbConn) -> EmptyResult {
pub async fn set_favorite(&self, favorite: Option<bool>, user_uuid: &UserId, conn: &DbConn) -> EmptyResult {
match favorite {
None => Ok(()), // No change requested.
Some(status) => Favorite::set_favorite(status, &self.uuid, user_uuid, conn).await,
}
}
pub async fn get_folder_uuid(&self, user_uuid: &UserId, conn: &mut DbConn) -> Option<FolderId> {
db_run! {conn: {
pub async fn get_folder_uuid(&self, user_uuid: &UserId, conn: &DbConn) -> Option<FolderId> {
db_run! { conn: {
folders_ciphers::table
.inner_join(folders::table)
.filter(folders::user_uuid.eq(&user_uuid))
@ -755,28 +749,26 @@ impl Cipher {
}}
}
pub async fn find_by_uuid(uuid: &CipherId, conn: &mut DbConn) -> Option<Self> {
db_run! {conn: {
pub async fn find_by_uuid(uuid: &CipherId, conn: &DbConn) -> Option<Self> {
db_run! { conn: {
ciphers::table
.filter(ciphers::uuid.eq(uuid))
.first::<CipherDb>(conn)
.first::<Self>(conn)
.ok()
.from_db()
}}
}
pub async fn find_by_uuid_and_org(
cipher_uuid: &CipherId,
org_uuid: &OrganizationId,
conn: &mut DbConn,
conn: &DbConn,
) -> Option<Self> {
db_run! {conn: {
db_run! { conn: {
ciphers::table
.filter(ciphers::uuid.eq(cipher_uuid))
.filter(ciphers::organization_uuid.eq(org_uuid))
.first::<CipherDb>(conn)
.first::<Self>(conn)
.ok()
.from_db()
}}
}
@ -796,10 +788,10 @@ impl Cipher {
user_uuid: &UserId,
visible_only: bool,
cipher_uuids: &Vec<CipherId>,
conn: &mut DbConn,
conn: &DbConn,
) -> Vec<Self> {
if CONFIG.org_groups_enabled() {
db_run! {conn: {
db_run! { conn: {
let mut query = ciphers::table
.left_join(ciphers_collections::table.on(
ciphers::uuid.eq(ciphers_collections::cipher_uuid)
@ -848,10 +840,11 @@ impl Cipher {
query
.select(ciphers::all_columns)
.distinct()
.load::<CipherDb>(conn).expect("Error loading ciphers").from_db()
.load::<Self>(conn)
.expect("Error loading ciphers")
}}
} else {
db_run! {conn: {
db_run! { conn: {
let mut query = ciphers::table
.left_join(ciphers_collections::table.on(
ciphers::uuid.eq(ciphers_collections::cipher_uuid)
@ -887,46 +880,44 @@ impl Cipher {
query
.select(ciphers::all_columns)
.distinct()
.load::<CipherDb>(conn).expect("Error loading ciphers").from_db()
.load::<Self>(conn)
.expect("Error loading ciphers")
}}
}
}
// Find all ciphers visible to the specified user.
pub async fn find_by_user_visible(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_user_visible(user_uuid: &UserId, conn: &DbConn) -> Vec<Self> {
Self::find_by_user(user_uuid, true, &vec![], conn).await
}
pub async fn find_by_user_and_ciphers(
user_uuid: &UserId,
cipher_uuids: &Vec<CipherId>,
conn: &mut DbConn,
conn: &DbConn,
) -> Vec<Self> {
Self::find_by_user(user_uuid, true, cipher_uuids, conn).await
}
pub async fn find_by_user_and_cipher(
user_uuid: &UserId,
cipher_uuid: &CipherId,
conn: &mut DbConn,
) -> Option<Self> {
pub async fn find_by_user_and_cipher(user_uuid: &UserId, cipher_uuid: &CipherId, conn: &DbConn) -> Option<Self> {
Self::find_by_user(user_uuid, true, &vec![cipher_uuid.clone()], conn).await.pop()
}
// Find all ciphers directly owned by the specified user.
pub async fn find_owned_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
db_run! {conn: {
pub async fn find_owned_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<Self> {
db_run! { conn: {
ciphers::table
.filter(
ciphers::user_uuid.eq(user_uuid)
.and(ciphers::organization_uuid.is_null())
)
.load::<CipherDb>(conn).expect("Error loading ciphers").from_db()
.load::<Self>(conn)
.expect("Error loading ciphers")
}}
}
pub async fn count_owned_by_user(user_uuid: &UserId, conn: &mut DbConn) -> i64 {
db_run! {conn: {
pub async fn count_owned_by_user(user_uuid: &UserId, conn: &DbConn) -> i64 {
db_run! { conn: {
ciphers::table
.filter(ciphers::user_uuid.eq(user_uuid))
.count()
@ -936,16 +927,17 @@ impl Cipher {
}}
}
pub async fn find_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> Vec<Self> {
db_run! {conn: {
pub async fn find_by_org(org_uuid: &OrganizationId, conn: &DbConn) -> Vec<Self> {
db_run! { conn: {
ciphers::table
.filter(ciphers::organization_uuid.eq(org_uuid))
.load::<CipherDb>(conn).expect("Error loading ciphers").from_db()
.load::<Self>(conn)
.expect("Error loading ciphers")
}}
}
pub async fn count_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> i64 {
db_run! {conn: {
pub async fn count_by_org(org_uuid: &OrganizationId, conn: &DbConn) -> i64 {
db_run! { conn: {
ciphers::table
.filter(ciphers::organization_uuid.eq(org_uuid))
.count()
@ -955,27 +947,29 @@ impl Cipher {
}}
}
pub async fn find_by_folder(folder_uuid: &FolderId, conn: &mut DbConn) -> Vec<Self> {
db_run! {conn: {
pub async fn find_by_folder(folder_uuid: &FolderId, conn: &DbConn) -> Vec<Self> {
db_run! { conn: {
folders_ciphers::table.inner_join(ciphers::table)
.filter(folders_ciphers::folder_uuid.eq(folder_uuid))
.select(ciphers::all_columns)
.load::<CipherDb>(conn).expect("Error loading ciphers").from_db()
.load::<Self>(conn)
.expect("Error loading ciphers")
}}
}
/// Find all ciphers that were deleted before the specified datetime.
pub async fn find_deleted_before(dt: &NaiveDateTime, conn: &mut DbConn) -> Vec<Self> {
db_run! {conn: {
pub async fn find_deleted_before(dt: &NaiveDateTime, conn: &DbConn) -> Vec<Self> {
db_run! { conn: {
ciphers::table
.filter(ciphers::deleted_at.lt(dt))
.load::<CipherDb>(conn).expect("Error loading ciphers").from_db()
.load::<Self>(conn)
.expect("Error loading ciphers")
}}
}
pub async fn get_collections(&self, user_uuid: UserId, conn: &mut DbConn) -> Vec<CollectionId> {
pub async fn get_collections(&self, user_uuid: UserId, conn: &DbConn) -> Vec<CollectionId> {
if CONFIG.org_groups_enabled() {
db_run! {conn: {
db_run! { conn: {
ciphers_collections::table
.filter(ciphers_collections::cipher_uuid.eq(&self.uuid))
.inner_join(collections::table.on(
@ -1005,10 +999,11 @@ impl Cipher {
.and(collections_groups::read_only.eq(false)))
)
.select(ciphers_collections::collection_uuid)
.load::<CollectionId>(conn).unwrap_or_default()
.load::<CollectionId>(conn)
.unwrap_or_default()
}}
} else {
db_run! {conn: {
db_run! { conn: {
ciphers_collections::table
.filter(ciphers_collections::cipher_uuid.eq(&self.uuid))
.inner_join(collections::table.on(
@ -1027,14 +1022,15 @@ impl Cipher {
.and(users_collections::read_only.eq(false)))
)
.select(ciphers_collections::collection_uuid)
.load::<CollectionId>(conn).unwrap_or_default()
.load::<CollectionId>(conn)
.unwrap_or_default()
}}
}
}
pub async fn get_admin_collections(&self, user_uuid: UserId, conn: &mut DbConn) -> Vec<CollectionId> {
pub async fn get_admin_collections(&self, user_uuid: UserId, conn: &DbConn) -> Vec<CollectionId> {
if CONFIG.org_groups_enabled() {
db_run! {conn: {
db_run! { conn: {
ciphers_collections::table
.filter(ciphers_collections::cipher_uuid.eq(&self.uuid))
.inner_join(collections::table.on(
@ -1065,10 +1061,11 @@ impl Cipher {
.or(users_organizations::atype.le(MembershipType::Admin as i32)) // User is admin or owner
)
.select(ciphers_collections::collection_uuid)
.load::<CollectionId>(conn).unwrap_or_default()
.load::<CollectionId>(conn)
.unwrap_or_default()
}}
} else {
db_run! {conn: {
db_run! { conn: {
ciphers_collections::table
.filter(ciphers_collections::cipher_uuid.eq(&self.uuid))
.inner_join(collections::table.on(
@ -1088,7 +1085,8 @@ impl Cipher {
.or(users_organizations::atype.le(MembershipType::Admin as i32)) // User is admin or owner
)
.select(ciphers_collections::collection_uuid)
.load::<CollectionId>(conn).unwrap_or_default()
.load::<CollectionId>(conn)
.unwrap_or_default()
}}
}
}
@ -1097,9 +1095,9 @@ impl Cipher {
/// This is used during a full sync so we only need one query for all collections accessible.
pub async fn get_collections_with_cipher_by_user(
user_uuid: UserId,
conn: &mut DbConn,
conn: &DbConn,
) -> Vec<(CipherId, CollectionId)> {
db_run! {conn: {
db_run! { conn: {
ciphers_collections::table
.inner_join(collections::table.on(
collections::uuid.eq(ciphers_collections::collection_uuid)
@ -1132,7 +1130,8 @@ impl Cipher {
.or_filter(collections_groups::collections_uuid.is_not_null()) //Access via group
.select(ciphers_collections::all_columns)
.distinct()
.load::<(CipherId, CollectionId)>(conn).unwrap_or_default()
.load::<(CipherId, CollectionId)>(conn)
.unwrap_or_default()
}}
}
}

184
src/db/models/collection.rs

@ -5,39 +5,41 @@ use super::{
CipherId, CollectionGroup, GroupUser, Membership, MembershipId, MembershipStatus, MembershipType, OrganizationId,
User, UserId,
};
use crate::db::schema::{
ciphers_collections, collections, collections_groups, groups, groups_users, users_collections, users_organizations,
};
use crate::CONFIG;
use diesel::prelude::*;
use macros::UuidFromParam;
db_object! {
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = collections)]
#[diesel(treat_none_as_null = true)]
#[diesel(primary_key(uuid))]
pub struct Collection {
pub uuid: CollectionId,
pub org_uuid: OrganizationId,
pub name: String,
pub external_id: Option<String>,
}
#[derive(Identifiable, Queryable, Insertable)]
#[diesel(table_name = users_collections)]
#[diesel(primary_key(user_uuid, collection_uuid))]
pub struct CollectionUser {
pub user_uuid: UserId,
pub collection_uuid: CollectionId,
pub read_only: bool,
pub hide_passwords: bool,
pub manage: bool,
}
#[derive(Identifiable, Queryable, Insertable)]
#[diesel(table_name = ciphers_collections)]
#[diesel(primary_key(cipher_uuid, collection_uuid))]
pub struct CollectionCipher {
pub cipher_uuid: CipherId,
pub collection_uuid: CollectionId,
}
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = collections)]
#[diesel(treat_none_as_null = true)]
#[diesel(primary_key(uuid))]
pub struct Collection {
pub uuid: CollectionId,
pub org_uuid: OrganizationId,
pub name: String,
pub external_id: Option<String>,
}
#[derive(Identifiable, Queryable, Insertable)]
#[diesel(table_name = users_collections)]
#[diesel(primary_key(user_uuid, collection_uuid))]
pub struct CollectionUser {
pub user_uuid: UserId,
pub collection_uuid: CollectionId,
pub read_only: bool,
pub hide_passwords: bool,
pub manage: bool,
}
#[derive(Identifiable, Queryable, Insertable)]
#[diesel(table_name = ciphers_collections)]
#[diesel(primary_key(cipher_uuid, collection_uuid))]
pub struct CollectionCipher {
pub cipher_uuid: CipherId,
pub collection_uuid: CollectionId,
}
/// Local methods
@ -83,7 +85,7 @@ impl Collection {
&self,
user_uuid: &UserId,
cipher_sync_data: Option<&crate::api::core::CipherSyncData>,
conn: &mut DbConn,
conn: &DbConn,
) -> Value {
let (read_only, hide_passwords, manage) = if let Some(cipher_sync_data) = cipher_sync_data {
match cipher_sync_data.members.get(&self.org_uuid) {
@ -135,7 +137,7 @@ impl Collection {
json_object
}
pub async fn can_access_collection(member: &Membership, col_id: &CollectionId, conn: &mut DbConn) -> bool {
pub async fn can_access_collection(member: &Membership, col_id: &CollectionId, conn: &DbConn) -> bool {
member.has_status(MembershipStatus::Confirmed)
&& (member.has_full_access()
|| CollectionUser::has_access_to_collection_by_user(col_id, &member.user_uuid, conn).await
@ -152,13 +154,13 @@ use crate::error::MapResult;
/// Database methods
impl Collection {
pub async fn save(&self, conn: &mut DbConn) -> EmptyResult {
pub async fn save(&self, conn: &DbConn) -> EmptyResult {
self.update_users_revision(conn).await;
db_run! { conn:
sqlite, mysql {
match diesel::replace_into(collections::table)
.values(CollectionDb::to_db(self))
.values(self)
.execute(conn)
{
Ok(_) => Ok(()),
@ -166,7 +168,7 @@ impl Collection {
Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => {
diesel::update(collections::table)
.filter(collections::uuid.eq(&self.uuid))
.set(CollectionDb::to_db(self))
.set(self)
.execute(conn)
.map_res("Error saving collection")
}
@ -174,19 +176,18 @@ impl Collection {
}.map_res("Error saving collection")
}
postgresql {
let value = CollectionDb::to_db(self);
diesel::insert_into(collections::table)
.values(&value)
.values(self)
.on_conflict(collections::uuid)
.do_update()
.set(&value)
.set(self)
.execute(conn)
.map_res("Error saving collection")
}
}
}
pub async fn delete(self, conn: &mut DbConn) -> EmptyResult {
pub async fn delete(self, conn: &DbConn) -> EmptyResult {
self.update_users_revision(conn).await;
CollectionCipher::delete_all_by_collection(&self.uuid, conn).await?;
CollectionUser::delete_all_by_collection(&self.uuid, conn).await?;
@ -199,30 +200,29 @@ impl Collection {
}}
}
pub async fn delete_all_by_organization(org_uuid: &OrganizationId, conn: &mut DbConn) -> EmptyResult {
pub async fn delete_all_by_organization(org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult {
for collection in Self::find_by_organization(org_uuid, conn).await {
collection.delete(conn).await?;
}
Ok(())
}
pub async fn update_users_revision(&self, conn: &mut DbConn) {
pub async fn update_users_revision(&self, conn: &DbConn) {
for member in Membership::find_by_collection_and_org(&self.uuid, &self.org_uuid, conn).await.iter() {
User::update_uuid_revision(&member.user_uuid, conn).await;
}
}
pub async fn find_by_uuid(uuid: &CollectionId, conn: &mut DbConn) -> Option<Self> {
pub async fn find_by_uuid(uuid: &CollectionId, conn: &DbConn) -> Option<Self> {
db_run! { conn: {
collections::table
.filter(collections::uuid.eq(uuid))
.first::<CollectionDb>(conn)
.first::<Self>(conn)
.ok()
.from_db()
}}
}
pub async fn find_by_user_uuid(user_uuid: UserId, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_user_uuid(user_uuid: UserId, conn: &DbConn) -> Vec<Self> {
if CONFIG.org_groups_enabled() {
db_run! { conn: {
collections::table
@ -263,7 +263,8 @@ impl Collection {
)
.select(collections::all_columns)
.distinct()
.load::<CollectionDb>(conn).expect("Error loading collections").from_db()
.load::<Self>(conn)
.expect("Error loading collections")
}}
} else {
db_run! { conn: {
@ -288,7 +289,8 @@ impl Collection {
)
.select(collections::all_columns)
.distinct()
.load::<CollectionDb>(conn).expect("Error loading collections").from_db()
.load::<Self>(conn)
.expect("Error loading collections")
}}
}
}
@ -296,7 +298,7 @@ impl Collection {
pub async fn find_by_organization_and_user_uuid(
org_uuid: &OrganizationId,
user_uuid: &UserId,
conn: &mut DbConn,
conn: &DbConn,
) -> Vec<Self> {
Self::find_by_user_uuid(user_uuid.to_owned(), conn)
.await
@ -305,17 +307,16 @@ impl Collection {
.collect()
}
pub async fn find_by_organization(org_uuid: &OrganizationId, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_organization(org_uuid: &OrganizationId, conn: &DbConn) -> Vec<Self> {
db_run! { conn: {
collections::table
.filter(collections::org_uuid.eq(org_uuid))
.load::<CollectionDb>(conn)
.load::<Self>(conn)
.expect("Error loading collections")
.from_db()
}}
}
pub async fn count_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> i64 {
pub async fn count_by_org(org_uuid: &OrganizationId, conn: &DbConn) -> i64 {
db_run! { conn: {
collections::table
.filter(collections::org_uuid.eq(org_uuid))
@ -326,23 +327,18 @@ impl Collection {
}}
}
pub async fn find_by_uuid_and_org(
uuid: &CollectionId,
org_uuid: &OrganizationId,
conn: &mut DbConn,
) -> Option<Self> {
pub async fn find_by_uuid_and_org(uuid: &CollectionId, org_uuid: &OrganizationId, conn: &DbConn) -> Option<Self> {
db_run! { conn: {
collections::table
.filter(collections::uuid.eq(uuid))
.filter(collections::org_uuid.eq(org_uuid))
.select(collections::all_columns)
.first::<CollectionDb>(conn)
.first::<Self>(conn)
.ok()
.from_db()
}}
}
pub async fn find_by_uuid_and_user(uuid: &CollectionId, user_uuid: UserId, conn: &mut DbConn) -> Option<Self> {
pub async fn find_by_uuid_and_user(uuid: &CollectionId, user_uuid: UserId, conn: &DbConn) -> Option<Self> {
if CONFIG.org_groups_enabled() {
db_run! { conn: {
collections::table
@ -380,8 +376,8 @@ impl Collection {
)
)
).select(collections::all_columns)
.first::<CollectionDb>(conn).ok()
.from_db()
.first::<Self>(conn)
.ok()
}}
} else {
db_run! { conn: {
@ -403,13 +399,13 @@ impl Collection {
users_organizations::atype.le(MembershipType::Admin as i32) // Org admin or owner
))
).select(collections::all_columns)
.first::<CollectionDb>(conn).ok()
.from_db()
.first::<Self>(conn)
.ok()
}}
}
}
pub async fn is_writable_by_user(&self, user_uuid: &UserId, conn: &mut DbConn) -> bool {
pub async fn is_writable_by_user(&self, user_uuid: &UserId, conn: &DbConn) -> bool {
let user_uuid = user_uuid.to_string();
if CONFIG.org_groups_enabled() {
db_run! { conn: {
@ -471,7 +467,7 @@ impl Collection {
}
}
pub async fn hide_passwords_for_user(&self, user_uuid: &UserId, conn: &mut DbConn) -> bool {
pub async fn hide_passwords_for_user(&self, user_uuid: &UserId, conn: &DbConn) -> bool {
let user_uuid = user_uuid.to_string();
db_run! { conn: {
collections::table
@ -517,7 +513,7 @@ impl Collection {
}}
}
pub async fn is_manageable_by_user(&self, user_uuid: &UserId, conn: &mut DbConn) -> bool {
pub async fn is_manageable_by_user(&self, user_uuid: &UserId, conn: &DbConn) -> bool {
let user_uuid = user_uuid.to_string();
db_run! { conn: {
collections::table
@ -569,7 +565,7 @@ impl CollectionUser {
pub async fn find_by_organization_and_user_uuid(
org_uuid: &OrganizationId,
user_uuid: &UserId,
conn: &mut DbConn,
conn: &DbConn,
) -> Vec<Self> {
db_run! { conn: {
users_collections::table
@ -577,15 +573,14 @@ impl CollectionUser {
.inner_join(collections::table.on(collections::uuid.eq(users_collections::collection_uuid)))
.filter(collections::org_uuid.eq(org_uuid))
.select(users_collections::all_columns)
.load::<CollectionUserDb>(conn)
.load::<Self>(conn)
.expect("Error loading users_collections")
.from_db()
}}
}
pub async fn find_by_organization_swap_user_uuid_with_member_uuid(
org_uuid: &OrganizationId,
conn: &mut DbConn,
conn: &DbConn,
) -> Vec<CollectionMembership> {
let col_users = db_run! { conn: {
users_collections::table
@ -594,9 +589,8 @@ impl CollectionUser {
.inner_join(users_organizations::table.on(users_organizations::user_uuid.eq(users_collections::user_uuid)))
.filter(users_organizations::org_uuid.eq(org_uuid))
.select((users_organizations::uuid, users_collections::collection_uuid, users_collections::read_only, users_collections::hide_passwords, users_collections::manage))
.load::<CollectionUserDb>(conn)
.load::<Self>(conn)
.expect("Error loading users_collections")
.from_db()
}};
col_users.into_iter().map(|c| c.into()).collect()
}
@ -607,7 +601,7 @@ impl CollectionUser {
read_only: bool,
hide_passwords: bool,
manage: bool,
conn: &mut DbConn,
conn: &DbConn,
) -> EmptyResult {
User::update_uuid_revision(user_uuid, conn).await;
@ -664,7 +658,7 @@ impl CollectionUser {
}
}
pub async fn delete(self, conn: &mut DbConn) -> EmptyResult {
pub async fn delete(self, conn: &DbConn) -> EmptyResult {
User::update_uuid_revision(&self.user_uuid, conn).await;
db_run! { conn: {
@ -678,21 +672,20 @@ impl CollectionUser {
}}
}
pub async fn find_by_collection(collection_uuid: &CollectionId, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_collection(collection_uuid: &CollectionId, conn: &DbConn) -> Vec<Self> {
db_run! { conn: {
users_collections::table
.filter(users_collections::collection_uuid.eq(collection_uuid))
.select(users_collections::all_columns)
.load::<CollectionUserDb>(conn)
.load::<Self>(conn)
.expect("Error loading users_collections")
.from_db()
}}
}
pub async fn find_by_org_and_coll_swap_user_uuid_with_member_uuid(
org_uuid: &OrganizationId,
collection_uuid: &CollectionId,
conn: &mut DbConn,
conn: &DbConn,
) -> Vec<CollectionMembership> {
let col_users = db_run! { conn: {
users_collections::table
@ -700,9 +693,8 @@ impl CollectionUser {
.filter(users_organizations::org_uuid.eq(org_uuid))
.inner_join(users_organizations::table.on(users_organizations::user_uuid.eq(users_collections::user_uuid)))
.select((users_organizations::uuid, users_collections::collection_uuid, users_collections::read_only, users_collections::hide_passwords, users_collections::manage))
.load::<CollectionUserDb>(conn)
.load::<Self>(conn)
.expect("Error loading users_collections")
.from_db()
}};
col_users.into_iter().map(|c| c.into()).collect()
}
@ -710,31 +702,29 @@ impl CollectionUser {
pub async fn find_by_collection_and_user(
collection_uuid: &CollectionId,
user_uuid: &UserId,
conn: &mut DbConn,
conn: &DbConn,
) -> Option<Self> {
db_run! { conn: {
users_collections::table
.filter(users_collections::collection_uuid.eq(collection_uuid))
.filter(users_collections::user_uuid.eq(user_uuid))
.select(users_collections::all_columns)
.first::<CollectionUserDb>(conn)
.first::<Self>(conn)
.ok()
.from_db()
}}
}
pub async fn find_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<Self> {
db_run! { conn: {
users_collections::table
.filter(users_collections::user_uuid.eq(user_uuid))
.select(users_collections::all_columns)
.load::<CollectionUserDb>(conn)
.load::<Self>(conn)
.expect("Error loading users_collections")
.from_db()
}}
}
pub async fn delete_all_by_collection(collection_uuid: &CollectionId, conn: &mut DbConn) -> EmptyResult {
pub async fn delete_all_by_collection(collection_uuid: &CollectionId, conn: &DbConn) -> EmptyResult {
for collection in CollectionUser::find_by_collection(collection_uuid, conn).await.iter() {
User::update_uuid_revision(&collection.user_uuid, conn).await;
}
@ -749,7 +739,7 @@ impl CollectionUser {
pub async fn delete_all_by_user_and_org(
user_uuid: &UserId,
org_uuid: &OrganizationId,
conn: &mut DbConn,
conn: &DbConn,
) -> EmptyResult {
let collectionusers = Self::find_by_organization_and_user_uuid(org_uuid, user_uuid, conn).await;
@ -766,18 +756,14 @@ impl CollectionUser {
}}
}
pub async fn has_access_to_collection_by_user(
col_id: &CollectionId,
user_uuid: &UserId,
conn: &mut DbConn,
) -> bool {
pub async fn has_access_to_collection_by_user(col_id: &CollectionId, user_uuid: &UserId, conn: &DbConn) -> bool {
Self::find_by_collection_and_user(col_id, user_uuid, conn).await.is_some()
}
}
/// Database methods
impl CollectionCipher {
pub async fn save(cipher_uuid: &CipherId, collection_uuid: &CollectionId, conn: &mut DbConn) -> EmptyResult {
pub async fn save(cipher_uuid: &CipherId, collection_uuid: &CollectionId, conn: &DbConn) -> EmptyResult {
Self::update_users_revision(collection_uuid, conn).await;
db_run! { conn:
@ -807,7 +793,7 @@ impl CollectionCipher {
}
}
pub async fn delete(cipher_uuid: &CipherId, collection_uuid: &CollectionId, conn: &mut DbConn) -> EmptyResult {
pub async fn delete(cipher_uuid: &CipherId, collection_uuid: &CollectionId, conn: &DbConn) -> EmptyResult {
Self::update_users_revision(collection_uuid, conn).await;
db_run! { conn: {
@ -821,7 +807,7 @@ impl CollectionCipher {
}}
}
pub async fn delete_all_by_cipher(cipher_uuid: &CipherId, conn: &mut DbConn) -> EmptyResult {
pub async fn delete_all_by_cipher(cipher_uuid: &CipherId, conn: &DbConn) -> EmptyResult {
db_run! { conn: {
diesel::delete(ciphers_collections::table.filter(ciphers_collections::cipher_uuid.eq(cipher_uuid)))
.execute(conn)
@ -829,7 +815,7 @@ impl CollectionCipher {
}}
}
pub async fn delete_all_by_collection(collection_uuid: &CollectionId, conn: &mut DbConn) -> EmptyResult {
pub async fn delete_all_by_collection(collection_uuid: &CollectionId, conn: &DbConn) -> EmptyResult {
db_run! { conn: {
diesel::delete(ciphers_collections::table.filter(ciphers_collections::collection_uuid.eq(collection_uuid)))
.execute(conn)
@ -837,7 +823,7 @@ impl CollectionCipher {
}}
}
pub async fn update_users_revision(collection_uuid: &CollectionId, conn: &mut DbConn) {
pub async fn update_users_revision(collection_uuid: &CollectionId, conn: &DbConn) {
if let Some(collection) = Collection::find_by_uuid(collection_uuid, conn).await {
collection.update_users_revision(conn).await;
}

110
src/db/models/device.rs

@ -5,32 +5,32 @@ use derive_more::{Display, From};
use serde_json::Value;
use super::{AuthRequest, UserId};
use crate::db::schema::devices;
use crate::{
crypto,
util::{format_date, get_uuid},
};
use diesel::prelude::*;
use macros::{IdFromParam, UuidFromParam};
db_object! {
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = devices)]
#[diesel(treat_none_as_null = true)]
#[diesel(primary_key(uuid, user_uuid))]
pub struct Device {
pub uuid: DeviceId,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
pub user_uuid: UserId,
pub name: String,
pub atype: i32, // https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/Enums/DeviceType.cs
pub push_uuid: Option<PushId>,
pub push_token: Option<String>,
pub refresh_token: String,
pub twofactor_remember: Option<String>,
}
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = devices)]
#[diesel(treat_none_as_null = true)]
#[diesel(primary_key(uuid, user_uuid))]
pub struct Device {
pub uuid: DeviceId,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
pub user_uuid: UserId,
pub name: String,
pub atype: i32, // https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/Enums/DeviceType.cs
pub push_uuid: Option<PushId>,
pub push_token: Option<String>,
pub refresh_token: String,
pub twofactor_remember: Option<String>,
}
/// Local methods
@ -48,7 +48,7 @@ impl Device {
}
pub fn refresh_twofactor_remember(&mut self) -> String {
let twofactor_remember = crypto::encode_random_bytes::<180>(BASE64);
let twofactor_remember = crypto::encode_random_bytes::<180>(&BASE64);
self.twofactor_remember = Some(twofactor_remember.clone());
twofactor_remember
@ -115,13 +115,7 @@ use crate::error::MapResult;
/// Database methods
impl Device {
pub async fn new(
uuid: DeviceId,
user_uuid: UserId,
name: String,
atype: i32,
conn: &mut DbConn,
) -> ApiResult<Device> {
pub async fn new(uuid: DeviceId, user_uuid: UserId, name: String, atype: i32, conn: &DbConn) -> ApiResult<Device> {
let now = Utc::now().naive_utc();
let device = Self {
@ -135,25 +129,31 @@ impl Device {
push_uuid: Some(PushId(get_uuid())),
push_token: None,
refresh_token: crypto::encode_random_bytes::<64>(BASE64URL),
refresh_token: crypto::encode_random_bytes::<64>(&BASE64URL),
twofactor_remember: None,
};
device.inner_save(conn).await.map(|()| device)
}
async fn inner_save(&self, conn: &mut DbConn) -> EmptyResult {
async fn inner_save(&self, conn: &DbConn) -> EmptyResult {
db_run! { conn:
sqlite, mysql {
crate::util::retry(
|| diesel::replace_into(devices::table).values(DeviceDb::to_db(self)).execute(conn),
crate::util::retry(||
diesel::replace_into(devices::table)
.values(self)
.execute(conn),
10,
).map_res("Error saving device")
}
postgresql {
let value = DeviceDb::to_db(self);
crate::util::retry(
|| diesel::insert_into(devices::table).values(&value).on_conflict((devices::uuid, devices::user_uuid)).do_update().set(&value).execute(conn),
crate::util::retry(||
diesel::insert_into(devices::table)
.values(self)
.on_conflict((devices::uuid, devices::user_uuid))
.do_update()
.set(self)
.execute(conn),
10,
).map_res("Error saving device")
}
@ -161,12 +161,12 @@ impl Device {
}
// Should only be called after user has passed authentication
pub async fn save(&mut self, conn: &mut DbConn) -> EmptyResult {
pub async fn save(&mut self, conn: &DbConn) -> EmptyResult {
self.updated_at = Utc::now().naive_utc();
self.inner_save(conn).await
}
pub async fn delete_all_by_user(user_uuid: &UserId, conn: &mut DbConn) -> EmptyResult {
pub async fn delete_all_by_user(user_uuid: &UserId, conn: &DbConn) -> EmptyResult {
db_run! { conn: {
diesel::delete(devices::table.filter(devices::user_uuid.eq(user_uuid)))
.execute(conn)
@ -174,18 +174,17 @@ impl Device {
}}
}
pub async fn find_by_uuid_and_user(uuid: &DeviceId, user_uuid: &UserId, conn: &mut DbConn) -> Option<Self> {
pub async fn find_by_uuid_and_user(uuid: &DeviceId, user_uuid: &UserId, conn: &DbConn) -> Option<Self> {
db_run! { conn: {
devices::table
.filter(devices::uuid.eq(uuid))
.filter(devices::user_uuid.eq(user_uuid))
.first::<DeviceDb>(conn)
.first::<Self>(conn)
.ok()
.from_db()
}}
}
pub async fn find_with_auth_request_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<DeviceWithAuthRequest> {
pub async fn find_with_auth_request_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<DeviceWithAuthRequest> {
let devices = Self::find_by_user(user_uuid, conn).await;
let mut result = Vec::new();
for device in devices {
@ -195,27 +194,25 @@ impl Device {
result
}
pub async fn find_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<Self> {
db_run! { conn: {
devices::table
.filter(devices::user_uuid.eq(user_uuid))
.load::<DeviceDb>(conn)
.load::<Self>(conn)
.expect("Error loading devices")
.from_db()
}}
}
pub async fn find_by_uuid(uuid: &DeviceId, conn: &mut DbConn) -> Option<Self> {
pub async fn find_by_uuid(uuid: &DeviceId, conn: &DbConn) -> Option<Self> {
db_run! { conn: {
devices::table
.filter(devices::uuid.eq(uuid))
.first::<DeviceDb>(conn)
.first::<Self>(conn)
.ok()
.from_db()
}}
}
pub async fn clear_push_token_by_uuid(uuid: &DeviceId, conn: &mut DbConn) -> EmptyResult {
pub async fn clear_push_token_by_uuid(uuid: &DeviceId, conn: &DbConn) -> EmptyResult {
db_run! { conn: {
diesel::update(devices::table)
.filter(devices::uuid.eq(uuid))
@ -224,39 +221,36 @@ impl Device {
.map_res("Error removing push token")
}}
}
pub async fn find_by_refresh_token(refresh_token: &str, conn: &mut DbConn) -> Option<Self> {
pub async fn find_by_refresh_token(refresh_token: &str, conn: &DbConn) -> Option<Self> {
db_run! { conn: {
devices::table
.filter(devices::refresh_token.eq(refresh_token))
.first::<DeviceDb>(conn)
.first::<Self>(conn)
.ok()
.from_db()
}}
}
pub async fn find_latest_active_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Option<Self> {
pub async fn find_latest_active_by_user(user_uuid: &UserId, conn: &DbConn) -> Option<Self> {
db_run! { conn: {
devices::table
.filter(devices::user_uuid.eq(user_uuid))
.order(devices::updated_at.desc())
.first::<DeviceDb>(conn)
.first::<Self>(conn)
.ok()
.from_db()
}}
}
pub async fn find_push_devices_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_push_devices_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<Self> {
db_run! { conn: {
devices::table
.filter(devices::user_uuid.eq(user_uuid))
.filter(devices::push_token.is_not_null())
.load::<DeviceDb>(conn)
.load::<Self>(conn)
.expect("Error loading push devices")
.from_db()
}}
}
pub async fn check_user_has_push_device(user_uuid: &UserId, conn: &mut DbConn) -> bool {
pub async fn check_user_has_push_device(user_uuid: &UserId, conn: &DbConn) -> bool {
db_run! { conn: {
devices::table
.filter(devices::user_uuid.eq(user_uuid))

139
src/db/models/emergency_access.rs

@ -3,32 +3,31 @@ use derive_more::{AsRef, Deref, Display, From};
use serde_json::Value;
use super::{User, UserId};
use crate::db::schema::emergency_access;
use crate::{api::EmptyResult, db::DbConn, error::MapResult};
use diesel::prelude::*;
use macros::UuidFromParam;
db_object! {
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = emergency_access)]
#[diesel(treat_none_as_null = true)]
#[diesel(primary_key(uuid))]
pub struct EmergencyAccess {
pub uuid: EmergencyAccessId,
pub grantor_uuid: UserId,
pub grantee_uuid: Option<UserId>,
pub email: Option<String>,
pub key_encrypted: Option<String>,
pub atype: i32, //EmergencyAccessType
pub status: i32, //EmergencyAccessStatus
pub wait_time_days: i32,
pub recovery_initiated_at: Option<NaiveDateTime>,
pub last_notification_at: Option<NaiveDateTime>,
pub updated_at: NaiveDateTime,
pub created_at: NaiveDateTime,
}
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = emergency_access)]
#[diesel(treat_none_as_null = true)]
#[diesel(primary_key(uuid))]
pub struct EmergencyAccess {
pub uuid: EmergencyAccessId,
pub grantor_uuid: UserId,
pub grantee_uuid: Option<UserId>,
pub email: Option<String>,
pub key_encrypted: Option<String>,
pub atype: i32, //EmergencyAccessType
pub status: i32, //EmergencyAccessStatus
pub wait_time_days: i32,
pub recovery_initiated_at: Option<NaiveDateTime>,
pub last_notification_at: Option<NaiveDateTime>,
pub updated_at: NaiveDateTime,
pub created_at: NaiveDateTime,
}
// Local methods
impl EmergencyAccess {
pub fn new(grantor_uuid: UserId, email: String, status: i32, atype: i32, wait_time_days: i32) -> Self {
let now = Utc::now().naive_utc();
@ -67,7 +66,7 @@ impl EmergencyAccess {
})
}
pub async fn to_json_grantor_details(&self, conn: &mut DbConn) -> Value {
pub async fn to_json_grantor_details(&self, conn: &DbConn) -> Value {
let grantor_user = User::find_by_uuid(&self.grantor_uuid, conn).await.expect("Grantor user not found.");
json!({
@ -83,7 +82,7 @@ impl EmergencyAccess {
})
}
pub async fn to_json_grantee_details(&self, conn: &mut 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 {
User::find_by_uuid(grantee_uuid, conn).await.expect("Grantee user not found.")
} else if let Some(email) = self.email.as_deref() {
@ -140,14 +139,14 @@ pub enum EmergencyAccessStatus {
// region Database methods
impl EmergencyAccess {
pub async fn save(&mut self, conn: &mut DbConn) -> EmptyResult {
pub async fn save(&mut self, conn: &DbConn) -> EmptyResult {
User::update_uuid_revision(&self.grantor_uuid, conn).await;
self.updated_at = Utc::now().naive_utc();
db_run! { conn:
sqlite, mysql {
match diesel::replace_into(emergency_access::table)
.values(EmergencyAccessDb::to_db(self))
.values(&*self)
.execute(conn)
{
Ok(_) => Ok(()),
@ -155,7 +154,7 @@ impl EmergencyAccess {
Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => {
diesel::update(emergency_access::table)
.filter(emergency_access::uuid.eq(&self.uuid))
.set(EmergencyAccessDb::to_db(self))
.set(&*self)
.execute(conn)
.map_res("Error updating emergency access")
}
@ -163,12 +162,11 @@ impl EmergencyAccess {
}.map_res("Error saving emergency access")
}
postgresql {
let value = EmergencyAccessDb::to_db(self);
diesel::insert_into(emergency_access::table)
.values(&value)
.values(&*self)
.on_conflict(emergency_access::uuid)
.do_update()
.set(&value)
.set(&*self)
.execute(conn)
.map_res("Error saving emergency access")
}
@ -179,14 +177,14 @@ impl EmergencyAccess {
&mut self,
status: i32,
date: &NaiveDateTime,
conn: &mut DbConn,
conn: &DbConn,
) -> EmptyResult {
// Update the grantee so that it will refresh it's status.
User::update_uuid_revision(self.grantee_uuid.as_ref().expect("Error getting grantee"), conn).await;
self.status = status;
date.clone_into(&mut self.updated_at);
db_run! {conn: {
db_run! { conn: {
crate::util::retry(|| {
diesel::update(emergency_access::table.filter(emergency_access::uuid.eq(&self.uuid)))
.set((emergency_access::status.eq(status), emergency_access::updated_at.eq(date)))
@ -196,15 +194,11 @@ impl EmergencyAccess {
}}
}
pub async fn update_last_notification_date_and_save(
&mut self,
date: &NaiveDateTime,
conn: &mut DbConn,
) -> EmptyResult {
pub async fn update_last_notification_date_and_save(&mut self, date: &NaiveDateTime, conn: &DbConn) -> EmptyResult {
self.last_notification_at = Some(date.to_owned());
date.clone_into(&mut self.updated_at);
db_run! {conn: {
db_run! { conn: {
crate::util::retry(|| {
diesel::update(emergency_access::table.filter(emergency_access::uuid.eq(&self.uuid)))
.set((emergency_access::last_notification_at.eq(date), emergency_access::updated_at.eq(date)))
@ -214,7 +208,7 @@ impl EmergencyAccess {
}}
}
pub async fn delete_all_by_user(user_uuid: &UserId, conn: &mut DbConn) -> EmptyResult {
pub async fn delete_all_by_user(user_uuid: &UserId, conn: &DbConn) -> EmptyResult {
for ea in Self::find_all_by_grantor_uuid(user_uuid, conn).await {
ea.delete(conn).await?;
}
@ -224,14 +218,14 @@ impl EmergencyAccess {
Ok(())
}
pub async fn delete_all_by_grantee_email(grantee_email: &str, conn: &mut DbConn) -> EmptyResult {
pub async fn delete_all_by_grantee_email(grantee_email: &str, conn: &DbConn) -> EmptyResult {
for ea in Self::find_all_invited_by_grantee_email(grantee_email, conn).await {
ea.delete(conn).await?;
}
Ok(())
}
pub async fn delete(self, conn: &mut DbConn) -> EmptyResult {
pub async fn delete(self, conn: &DbConn) -> EmptyResult {
User::update_uuid_revision(&self.grantor_uuid, conn).await;
db_run! { conn: {
@ -245,109 +239,118 @@ impl EmergencyAccess {
grantor_uuid: &UserId,
grantee_uuid: &UserId,
email: &str,
conn: &mut DbConn,
conn: &DbConn,
) -> Option<Self> {
db_run! { conn: {
emergency_access::table
.filter(emergency_access::grantor_uuid.eq(grantor_uuid))
.filter(emergency_access::grantee_uuid.eq(grantee_uuid).or(emergency_access::email.eq(email)))
.first::<EmergencyAccessDb>(conn)
.ok().from_db()
.first::<Self>(conn)
.ok()
}}
}
pub async fn find_all_recoveries_initiated(conn: &mut DbConn) -> Vec<Self> {
pub async fn find_all_recoveries_initiated(conn: &DbConn) -> Vec<Self> {
db_run! { conn: {
emergency_access::table
.filter(emergency_access::status.eq(EmergencyAccessStatus::RecoveryInitiated as i32))
.filter(emergency_access::recovery_initiated_at.is_not_null())
.load::<EmergencyAccessDb>(conn).expect("Error loading emergency_access").from_db()
.load::<Self>(conn)
.expect("Error loading emergency_access")
}}
}
pub async fn find_by_uuid_and_grantor_uuid(
uuid: &EmergencyAccessId,
grantor_uuid: &UserId,
conn: &mut DbConn,
conn: &DbConn,
) -> Option<Self> {
db_run! { conn: {
emergency_access::table
.filter(emergency_access::uuid.eq(uuid))
.filter(emergency_access::grantor_uuid.eq(grantor_uuid))
.first::<EmergencyAccessDb>(conn)
.ok().from_db()
.first::<Self>(conn)
.ok()
}}
}
pub async fn find_by_uuid_and_grantee_uuid(
uuid: &EmergencyAccessId,
grantee_uuid: &UserId,
conn: &mut DbConn,
conn: &DbConn,
) -> Option<Self> {
db_run! { conn: {
emergency_access::table
.filter(emergency_access::uuid.eq(uuid))
.filter(emergency_access::grantee_uuid.eq(grantee_uuid))
.first::<EmergencyAccessDb>(conn)
.ok().from_db()
.first::<Self>(conn)
.ok()
}}
}
pub async fn find_by_uuid_and_grantee_email(
uuid: &EmergencyAccessId,
grantee_email: &str,
conn: &mut DbConn,
conn: &DbConn,
) -> Option<Self> {
db_run! { conn: {
emergency_access::table
.filter(emergency_access::uuid.eq(uuid))
.filter(emergency_access::email.eq(grantee_email))
.first::<EmergencyAccessDb>(conn)
.ok().from_db()
.first::<Self>(conn)
.ok()
}}
}
pub async fn find_all_by_grantee_uuid(grantee_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_all_by_grantee_uuid(grantee_uuid: &UserId, conn: &DbConn) -> Vec<Self> {
db_run! { conn: {
emergency_access::table
.filter(emergency_access::grantee_uuid.eq(grantee_uuid))
.load::<EmergencyAccessDb>(conn).expect("Error loading emergency_access").from_db()
.load::<Self>(conn)
.expect("Error loading emergency_access")
}}
}
pub async fn find_invited_by_grantee_email(grantee_email: &str, conn: &mut DbConn) -> Option<Self> {
pub async fn find_invited_by_grantee_email(grantee_email: &str, conn: &DbConn) -> Option<Self> {
db_run! { conn: {
emergency_access::table
.filter(emergency_access::email.eq(grantee_email))
.filter(emergency_access::status.eq(EmergencyAccessStatus::Invited as i32))
.first::<EmergencyAccessDb>(conn)
.ok().from_db()
.first::<Self>(conn)
.ok()
}}
}
pub async fn find_all_invited_by_grantee_email(grantee_email: &str, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_all_invited_by_grantee_email(grantee_email: &str, conn: &DbConn) -> Vec<Self> {
db_run! { conn: {
emergency_access::table
.filter(emergency_access::email.eq(grantee_email))
.filter(emergency_access::status.eq(EmergencyAccessStatus::Invited as i32))
.load::<EmergencyAccessDb>(conn).expect("Error loading emergency_access").from_db()
.load::<Self>(conn)
.expect("Error loading emergency_access")
}}
}
pub async fn find_all_by_grantor_uuid(grantor_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_all_by_grantor_uuid(grantor_uuid: &UserId, conn: &DbConn) -> Vec<Self> {
db_run! { conn: {
emergency_access::table
.filter(emergency_access::grantor_uuid.eq(grantor_uuid))
.load::<EmergencyAccessDb>(conn).expect("Error loading emergency_access").from_db()
.load::<Self>(conn)
.expect("Error loading emergency_access")
}}
}
pub async fn accept_invite(
&mut self,
grantee_uuid: &UserId,
grantee_email: &str,
conn: &mut DbConn,
) -> EmptyResult {
pub async fn find_all_confirmed_by_grantor_uuid(grantor_uuid: &UserId, conn: &DbConn) -> Vec<Self> {
db_run! { conn: {
emergency_access::table
.filter(emergency_access::grantor_uuid.eq(grantor_uuid))
.filter(emergency_access::status.ge(EmergencyAccessStatus::Confirmed as i32))
.load::<Self>(conn)
.expect("Error loading emergency_access")
}}
}
pub async fn accept_invite(&mut self, grantee_uuid: &UserId, grantee_email: &str, conn: &DbConn) -> EmptyResult {
if self.email.is_none() || self.email.as_ref().unwrap() != grantee_email {
err!("User email does not match invite.");
}

89
src/db/models/event.rs

@ -3,37 +3,37 @@ use chrono::{NaiveDateTime, TimeDelta, Utc};
use serde_json::Value;
use super::{CipherId, CollectionId, GroupId, MembershipId, OrgPolicyId, OrganizationId, UserId};
use crate::db::schema::{event, users_organizations};
use crate::{api::EmptyResult, db::DbConn, error::MapResult, CONFIG};
use diesel::prelude::*;
// https://bitwarden.com/help/event-logs/
db_object! {
// Upstream: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Services/Implementations/EventService.cs
// Upstream: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/AdminConsole/Public/Models/Response/EventResponseModel.cs
// Upstream SQL: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Sql/dbo/Tables/Event.sql
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = event)]
#[diesel(treat_none_as_null = true)]
#[diesel(primary_key(uuid))]
pub struct Event {
pub uuid: EventId,
pub event_type: i32, // EventType
pub user_uuid: Option<UserId>,
pub org_uuid: Option<OrganizationId>,
pub cipher_uuid: Option<CipherId>,
pub collection_uuid: Option<CollectionId>,
pub group_uuid: Option<GroupId>,
pub org_user_uuid: Option<MembershipId>,
pub act_user_uuid: Option<UserId>,
// Upstream enum: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/Enums/DeviceType.cs
pub device_type: Option<i32>,
pub ip_address: Option<String>,
pub event_date: NaiveDateTime,
pub policy_uuid: Option<OrgPolicyId>,
pub provider_uuid: Option<String>,
pub provider_user_uuid: Option<String>,
pub provider_org_uuid: Option<String>,
}
// Upstream: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Services/Implementations/EventService.cs
// Upstream: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/AdminConsole/Public/Models/Response/EventResponseModel.cs
// Upstream SQL: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Sql/dbo/Tables/Event.sql
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = event)]
#[diesel(treat_none_as_null = true)]
#[diesel(primary_key(uuid))]
pub struct Event {
pub uuid: EventId,
pub event_type: i32, // EventType
pub user_uuid: Option<UserId>,
pub org_uuid: Option<OrganizationId>,
pub cipher_uuid: Option<CipherId>,
pub collection_uuid: Option<CollectionId>,
pub group_uuid: Option<GroupId>,
pub org_user_uuid: Option<MembershipId>,
pub act_user_uuid: Option<UserId>,
// Upstream enum: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/Enums/DeviceType.cs
pub device_type: Option<i32>,
pub ip_address: Option<String>,
pub event_date: NaiveDateTime,
pub policy_uuid: Option<OrgPolicyId>,
pub provider_uuid: Option<String>,
pub provider_user_uuid: Option<String>,
pub provider_org_uuid: Option<String>,
}
// Upstream enum: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Enums/EventType.cs
@ -193,27 +193,27 @@ impl Event {
/// #############
/// Basic Queries
pub async fn save(&self, conn: &mut DbConn) -> EmptyResult {
pub async fn save(&self, conn: &DbConn) -> EmptyResult {
db_run! { conn:
sqlite, mysql {
diesel::replace_into(event::table)
.values(EventDb::to_db(self))
.values(self)
.execute(conn)
.map_res("Error saving event")
}
postgresql {
diesel::insert_into(event::table)
.values(EventDb::to_db(self))
.values(self)
.on_conflict(event::uuid)
.do_update()
.set(EventDb::to_db(self))
.set(self)
.execute(conn)
.map_res("Error saving event")
}
}
}
pub async fn save_user_event(events: Vec<Event>, conn: &mut DbConn) -> EmptyResult {
pub async fn save_user_event(events: Vec<Event>, conn: &DbConn) -> EmptyResult {
// Special save function which is able to handle multiple events.
// SQLite doesn't support the DEFAULT argument, and does not support inserting multiple values at the same time.
// MySQL and PostgreSQL do.
@ -224,14 +224,13 @@ impl Event {
sqlite {
for event in events {
diesel::insert_or_ignore_into(event::table)
.values(EventDb::to_db(&event))
.values(&event)
.execute(conn)
.unwrap_or_default();
}
Ok(())
}
mysql {
let events: Vec<EventDb> = events.iter().map(EventDb::to_db).collect();
diesel::insert_or_ignore_into(event::table)
.values(&events)
.execute(conn)
@ -239,7 +238,6 @@ impl Event {
Ok(())
}
postgresql {
let events: Vec<EventDb> = events.iter().map(EventDb::to_db).collect();
diesel::insert_into(event::table)
.values(&events)
.on_conflict_do_nothing()
@ -250,7 +248,7 @@ impl Event {
}
}
pub async fn delete(self, conn: &mut DbConn) -> EmptyResult {
pub async fn delete(self, conn: &DbConn) -> EmptyResult {
db_run! { conn: {
diesel::delete(event::table.filter(event::uuid.eq(self.uuid)))
.execute(conn)
@ -264,7 +262,7 @@ impl Event {
org_uuid: &OrganizationId,
start: &NaiveDateTime,
end: &NaiveDateTime,
conn: &mut DbConn,
conn: &DbConn,
) -> Vec<Self> {
db_run! { conn: {
event::table
@ -272,13 +270,12 @@ impl Event {
.filter(event::event_date.between(start, end))
.order_by(event::event_date.desc())
.limit(Self::PAGE_SIZE)
.load::<EventDb>(conn)
.load::<Self>(conn)
.expect("Error filtering events")
.from_db()
}}
}
pub async fn count_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> i64 {
pub async fn count_by_org(org_uuid: &OrganizationId, conn: &DbConn) -> i64 {
db_run! { conn: {
event::table
.filter(event::org_uuid.eq(org_uuid))
@ -294,7 +291,7 @@ impl Event {
member_uuid: &MembershipId,
start: &NaiveDateTime,
end: &NaiveDateTime,
conn: &mut DbConn,
conn: &DbConn,
) -> Vec<Self> {
db_run! { conn: {
event::table
@ -305,9 +302,8 @@ impl Event {
.select(event::all_columns)
.order_by(event::event_date.desc())
.limit(Self::PAGE_SIZE)
.load::<EventDb>(conn)
.load::<Self>(conn)
.expect("Error filtering events")
.from_db()
}}
}
@ -315,7 +311,7 @@ impl Event {
cipher_uuid: &CipherId,
start: &NaiveDateTime,
end: &NaiveDateTime,
conn: &mut DbConn,
conn: &DbConn,
) -> Vec<Self> {
db_run! { conn: {
event::table
@ -323,13 +319,12 @@ impl Event {
.filter(event::event_date.between(start, end))
.order_by(event::event_date.desc())
.limit(Self::PAGE_SIZE)
.load::<EventDb>(conn)
.load::<Self>(conn)
.expect("Error filtering events")
.from_db()
}}
}
pub async fn clean_events(conn: &mut DbConn) -> EmptyResult {
pub async fn clean_events(conn: &DbConn) -> EmptyResult {
if let Some(days_to_retain) = CONFIG.events_days_retain() {
let dt = Utc::now().naive_utc() - TimeDelta::try_days(days_to_retain).unwrap();
db_run! { conn: {

30
src/db/models/favorite.rs

@ -1,13 +1,13 @@
use super::{CipherId, User, UserId};
use crate::db::schema::favorites;
use diesel::prelude::*;
db_object! {
#[derive(Identifiable, Queryable, Insertable)]
#[diesel(table_name = favorites)]
#[diesel(primary_key(user_uuid, cipher_uuid))]
pub struct Favorite {
pub user_uuid: UserId,
pub cipher_uuid: CipherId,
}
#[derive(Identifiable, Queryable, Insertable)]
#[diesel(table_name = favorites)]
#[diesel(primary_key(user_uuid, cipher_uuid))]
pub struct Favorite {
pub user_uuid: UserId,
pub cipher_uuid: CipherId,
}
use crate::db::DbConn;
@ -17,14 +17,16 @@ use crate::error::MapResult;
impl Favorite {
// Returns whether the specified cipher is a favorite of the specified user.
pub async fn is_favorite(cipher_uuid: &CipherId, user_uuid: &UserId, conn: &mut DbConn) -> bool {
pub async fn is_favorite(cipher_uuid: &CipherId, user_uuid: &UserId, conn: &DbConn) -> bool {
db_run! { conn: {
let query = favorites::table
.filter(favorites::cipher_uuid.eq(cipher_uuid))
.filter(favorites::user_uuid.eq(user_uuid))
.count();
query.first::<i64>(conn).ok().unwrap_or(0) != 0
query.first::<i64>(conn)
.ok()
.unwrap_or(0) != 0
}}
}
@ -33,7 +35,7 @@ impl Favorite {
favorite: bool,
cipher_uuid: &CipherId,
user_uuid: &UserId,
conn: &mut DbConn,
conn: &DbConn,
) -> EmptyResult {
let (old, new) = (Self::is_favorite(cipher_uuid, user_uuid, conn).await, favorite);
match (old, new) {
@ -67,7 +69,7 @@ impl Favorite {
}
// Delete all favorite entries associated with the specified cipher.
pub async fn delete_all_by_cipher(cipher_uuid: &CipherId, conn: &mut DbConn) -> EmptyResult {
pub async fn delete_all_by_cipher(cipher_uuid: &CipherId, conn: &DbConn) -> EmptyResult {
db_run! { conn: {
diesel::delete(favorites::table.filter(favorites::cipher_uuid.eq(cipher_uuid)))
.execute(conn)
@ -76,7 +78,7 @@ impl Favorite {
}
// Delete all favorite entries associated with the specified user.
pub async fn delete_all_by_user(user_uuid: &UserId, conn: &mut DbConn) -> EmptyResult {
pub async fn delete_all_by_user(user_uuid: &UserId, conn: &DbConn) -> EmptyResult {
db_run! { conn: {
diesel::delete(favorites::table.filter(favorites::user_uuid.eq(user_uuid)))
.execute(conn)
@ -86,7 +88,7 @@ impl Favorite {
/// Return a vec with (cipher_uuid) this will only contain favorite flagged ciphers
/// This is used during a full sync so we only need one query for all favorite cipher matches.
pub async fn get_all_cipher_uuid_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<CipherId> {
pub async fn get_all_cipher_uuid_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<CipherId> {
db_run! { conn: {
favorites::table
.filter(favorites::user_uuid.eq(user_uuid))

85
src/db/models/folder.rs

@ -3,27 +3,27 @@ use derive_more::{AsRef, Deref, Display, From};
use serde_json::Value;
use super::{CipherId, User, UserId};
use crate::db::schema::{folders, folders_ciphers};
use diesel::prelude::*;
use macros::UuidFromParam;
db_object! {
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = folders)]
#[diesel(primary_key(uuid))]
pub struct Folder {
pub uuid: FolderId,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
pub user_uuid: UserId,
pub name: String,
}
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = folders)]
#[diesel(primary_key(uuid))]
pub struct Folder {
pub uuid: FolderId,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
pub user_uuid: UserId,
pub name: String,
}
#[derive(Identifiable, Queryable, Insertable)]
#[diesel(table_name = folders_ciphers)]
#[diesel(primary_key(cipher_uuid, folder_uuid))]
pub struct FolderCipher {
pub cipher_uuid: CipherId,
pub folder_uuid: FolderId,
}
#[derive(Identifiable, Queryable, Insertable)]
#[diesel(table_name = folders_ciphers)]
#[diesel(primary_key(cipher_uuid, folder_uuid))]
pub struct FolderCipher {
pub cipher_uuid: CipherId,
pub folder_uuid: FolderId,
}
/// Local methods
@ -69,14 +69,14 @@ use crate::error::MapResult;
/// Database methods
impl Folder {
pub async fn save(&mut self, conn: &mut DbConn) -> EmptyResult {
pub async fn save(&mut self, conn: &DbConn) -> EmptyResult {
User::update_uuid_revision(&self.user_uuid, conn).await;
self.updated_at = Utc::now().naive_utc();
db_run! { conn:
sqlite, mysql {
match diesel::replace_into(folders::table)
.values(FolderDb::to_db(self))
.values(&*self)
.execute(conn)
{
Ok(_) => Ok(()),
@ -84,7 +84,7 @@ impl Folder {
Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => {
diesel::update(folders::table)
.filter(folders::uuid.eq(&self.uuid))
.set(FolderDb::to_db(self))
.set(&*self)
.execute(conn)
.map_res("Error saving folder")
}
@ -92,19 +92,18 @@ impl Folder {
}.map_res("Error saving folder")
}
postgresql {
let value = FolderDb::to_db(self);
diesel::insert_into(folders::table)
.values(&value)
.values(&*self)
.on_conflict(folders::uuid)
.do_update()
.set(&value)
.set(&*self)
.execute(conn)
.map_res("Error saving folder")
}
}
}
pub async fn delete(&self, conn: &mut DbConn) -> EmptyResult {
pub async fn delete(&self, conn: &DbConn) -> EmptyResult {
User::update_uuid_revision(&self.user_uuid, conn).await;
FolderCipher::delete_all_by_folder(&self.uuid, conn).await?;
@ -115,50 +114,48 @@ impl Folder {
}}
}
pub async fn delete_all_by_user(user_uuid: &UserId, conn: &mut DbConn) -> EmptyResult {
pub async fn delete_all_by_user(user_uuid: &UserId, conn: &DbConn) -> EmptyResult {
for folder in Self::find_by_user(user_uuid, conn).await {
folder.delete(conn).await?;
}
Ok(())
}
pub async fn find_by_uuid_and_user(uuid: &FolderId, user_uuid: &UserId, conn: &mut DbConn) -> Option<Self> {
pub async fn find_by_uuid_and_user(uuid: &FolderId, user_uuid: &UserId, conn: &DbConn) -> Option<Self> {
db_run! { conn: {
folders::table
.filter(folders::uuid.eq(uuid))
.filter(folders::user_uuid.eq(user_uuid))
.first::<FolderDb>(conn)
.first::<Self>(conn)
.ok()
.from_db()
}}
}
pub async fn find_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<Self> {
db_run! { conn: {
folders::table
.filter(folders::user_uuid.eq(user_uuid))
.load::<FolderDb>(conn)
.load::<Self>(conn)
.expect("Error loading folders")
.from_db()
}}
}
}
impl FolderCipher {
pub async fn save(&self, conn: &mut DbConn) -> EmptyResult {
pub async fn save(&self, conn: &DbConn) -> EmptyResult {
db_run! { conn:
sqlite, mysql {
// Not checking for ForeignKey Constraints here.
// Table folders_ciphers does not have ForeignKey Constraints which would cause conflicts.
// This table has no constraints pointing to itself, but only to others.
diesel::replace_into(folders_ciphers::table)
.values(FolderCipherDb::to_db(self))
.values(self)
.execute(conn)
.map_res("Error adding cipher to folder")
}
postgresql {
diesel::insert_into(folders_ciphers::table)
.values(FolderCipherDb::to_db(self))
.values(self)
.on_conflict((folders_ciphers::cipher_uuid, folders_ciphers::folder_uuid))
.do_nothing()
.execute(conn)
@ -167,7 +164,7 @@ impl FolderCipher {
}
}
pub async fn delete(self, conn: &mut DbConn) -> EmptyResult {
pub async fn delete(self, conn: &DbConn) -> EmptyResult {
db_run! { conn: {
diesel::delete(
folders_ciphers::table
@ -179,7 +176,7 @@ impl FolderCipher {
}}
}
pub async fn delete_all_by_cipher(cipher_uuid: &CipherId, conn: &mut DbConn) -> EmptyResult {
pub async fn delete_all_by_cipher(cipher_uuid: &CipherId, conn: &DbConn) -> EmptyResult {
db_run! { conn: {
diesel::delete(folders_ciphers::table.filter(folders_ciphers::cipher_uuid.eq(cipher_uuid)))
.execute(conn)
@ -187,7 +184,7 @@ impl FolderCipher {
}}
}
pub async fn delete_all_by_folder(folder_uuid: &FolderId, conn: &mut DbConn) -> EmptyResult {
pub async fn delete_all_by_folder(folder_uuid: &FolderId, conn: &DbConn) -> EmptyResult {
db_run! { conn: {
diesel::delete(folders_ciphers::table.filter(folders_ciphers::folder_uuid.eq(folder_uuid)))
.execute(conn)
@ -198,31 +195,29 @@ impl FolderCipher {
pub async fn find_by_folder_and_cipher(
folder_uuid: &FolderId,
cipher_uuid: &CipherId,
conn: &mut DbConn,
conn: &DbConn,
) -> Option<Self> {
db_run! { conn: {
folders_ciphers::table
.filter(folders_ciphers::folder_uuid.eq(folder_uuid))
.filter(folders_ciphers::cipher_uuid.eq(cipher_uuid))
.first::<FolderCipherDb>(conn)
.first::<Self>(conn)
.ok()
.from_db()
}}
}
pub async fn find_by_folder(folder_uuid: &FolderId, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_folder(folder_uuid: &FolderId, conn: &DbConn) -> Vec<Self> {
db_run! { conn: {
folders_ciphers::table
.filter(folders_ciphers::folder_uuid.eq(folder_uuid))
.load::<FolderCipherDb>(conn)
.load::<Self>(conn)
.expect("Error loading folders")
.from_db()
}}
}
/// Return a vec with (cipher_uuid, folder_uuid)
/// This is used during a full sync so we only need one query for all folder matches.
pub async fn find_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<(CipherId, FolderId)> {
pub async fn find_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<(CipherId, FolderId)> {
db_run! { conn: {
folders_ciphers::table
.inner_join(folders::table)

157
src/db/models/group.rs

@ -1,45 +1,45 @@
use super::{CollectionId, Membership, MembershipId, OrganizationId, User, UserId};
use crate::api::EmptyResult;
use crate::db::schema::{collections_groups, groups, groups_users, users_organizations};
use crate::db::DbConn;
use crate::error::MapResult;
use chrono::{NaiveDateTime, Utc};
use derive_more::{AsRef, Deref, Display, From};
use diesel::prelude::*;
use macros::UuidFromParam;
use serde_json::Value;
db_object! {
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = groups)]
#[diesel(treat_none_as_null = true)]
#[diesel(primary_key(uuid))]
pub struct Group {
pub uuid: GroupId,
pub organizations_uuid: OrganizationId,
pub name: String,
pub access_all: bool,
pub external_id: Option<String>,
pub creation_date: NaiveDateTime,
pub revision_date: NaiveDateTime,
}
#[derive(Identifiable, Queryable, Insertable)]
#[diesel(table_name = collections_groups)]
#[diesel(primary_key(collections_uuid, groups_uuid))]
pub struct CollectionGroup {
pub collections_uuid: CollectionId,
pub groups_uuid: GroupId,
pub read_only: bool,
pub hide_passwords: bool,
pub manage: bool,
}
#[derive(Identifiable, Queryable, Insertable)]
#[diesel(table_name = groups_users)]
#[diesel(primary_key(groups_uuid, users_organizations_uuid))]
pub struct GroupUser {
pub groups_uuid: GroupId,
pub users_organizations_uuid: MembershipId
}
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = groups)]
#[diesel(treat_none_as_null = true)]
#[diesel(primary_key(uuid))]
pub struct Group {
pub uuid: GroupId,
pub organizations_uuid: OrganizationId,
pub name: String,
pub access_all: bool,
pub external_id: Option<String>,
pub creation_date: NaiveDateTime,
pub revision_date: NaiveDateTime,
}
#[derive(Identifiable, Queryable, Insertable)]
#[diesel(table_name = collections_groups)]
#[diesel(primary_key(collections_uuid, groups_uuid))]
pub struct CollectionGroup {
pub collections_uuid: CollectionId,
pub groups_uuid: GroupId,
pub read_only: bool,
pub hide_passwords: bool,
pub manage: bool,
}
#[derive(Identifiable, Queryable, Insertable)]
#[diesel(table_name = groups_users)]
#[diesel(primary_key(groups_uuid, users_organizations_uuid))]
pub struct GroupUser {
pub groups_uuid: GroupId,
pub users_organizations_uuid: MembershipId,
}
/// Local methods
@ -77,7 +77,7 @@ impl Group {
})
}
pub async fn to_json_details(&self, conn: &mut DbConn) -> Value {
pub async fn to_json_details(&self, conn: &DbConn) -> Value {
// 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
// Or an entry with everything to false
@ -156,13 +156,13 @@ impl GroupUser {
/// Database methods
impl Group {
pub async fn save(&mut self, conn: &mut DbConn) -> EmptyResult {
pub async fn save(&mut self, conn: &DbConn) -> EmptyResult {
self.revision_date = Utc::now().naive_utc();
db_run! { conn:
sqlite, mysql {
match diesel::replace_into(groups::table)
.values(GroupDb::to_db(self))
.values(&*self)
.execute(conn)
{
Ok(_) => Ok(()),
@ -170,7 +170,7 @@ impl Group {
Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => {
diesel::update(groups::table)
.filter(groups::uuid.eq(&self.uuid))
.set(GroupDb::to_db(self))
.set(&*self)
.execute(conn)
.map_res("Error saving group")
}
@ -178,36 +178,34 @@ impl Group {
}.map_res("Error saving group")
}
postgresql {
let value = GroupDb::to_db(self);
diesel::insert_into(groups::table)
.values(&value)
.values(&*self)
.on_conflict(groups::uuid)
.do_update()
.set(&value)
.set(&*self)
.execute(conn)
.map_res("Error saving group")
}
}
}
pub async fn delete_all_by_organization(org_uuid: &OrganizationId, conn: &mut 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 {
group.delete(conn).await?;
}
Ok(())
}
pub async fn find_by_organization(org_uuid: &OrganizationId, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_organization(org_uuid: &OrganizationId, conn: &DbConn) -> Vec<Self> {
db_run! { conn: {
groups::table
.filter(groups::organizations_uuid.eq(org_uuid))
.load::<GroupDb>(conn)
.load::<Self>(conn)
.expect("Error loading groups")
.from_db()
}}
}
pub async fn count_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> i64 {
pub async fn count_by_org(org_uuid: &OrganizationId, conn: &DbConn) -> i64 {
db_run! { conn: {
groups::table
.filter(groups::organizations_uuid.eq(org_uuid))
@ -218,33 +216,31 @@ impl Group {
}}
}
pub async fn find_by_uuid_and_org(uuid: &GroupId, org_uuid: &OrganizationId, conn: &mut DbConn) -> Option<Self> {
pub async fn find_by_uuid_and_org(uuid: &GroupId, org_uuid: &OrganizationId, conn: &DbConn) -> Option<Self> {
db_run! { conn: {
groups::table
.filter(groups::uuid.eq(uuid))
.filter(groups::organizations_uuid.eq(org_uuid))
.first::<GroupDb>(conn)
.first::<Self>(conn)
.ok()
.from_db()
}}
}
pub async fn find_by_external_id_and_org(
external_id: &str,
org_uuid: &OrganizationId,
conn: &mut DbConn,
conn: &DbConn,
) -> Option<Self> {
db_run! { conn: {
groups::table
.filter(groups::external_id.eq(external_id))
.filter(groups::organizations_uuid.eq(org_uuid))
.first::<GroupDb>(conn)
.first::<Self>(conn)
.ok()
.from_db()
}}
}
//Returns all organizations the user has full access to
pub async fn get_orgs_by_user_with_full_access(user_uuid: &UserId, conn: &mut DbConn) -> Vec<OrganizationId> {
pub async fn get_orgs_by_user_with_full_access(user_uuid: &UserId, conn: &DbConn) -> Vec<OrganizationId> {
db_run! { conn: {
groups_users::table
.inner_join(users_organizations::table.on(
@ -262,7 +258,7 @@ impl Group {
}}
}
pub async fn is_in_full_access_group(user_uuid: &UserId, org_uuid: &OrganizationId, conn: &mut DbConn) -> bool {
pub async fn is_in_full_access_group(user_uuid: &UserId, org_uuid: &OrganizationId, conn: &DbConn) -> bool {
db_run! { conn: {
groups::table
.inner_join(groups_users::table.on(
@ -280,7 +276,7 @@ impl Group {
}}
}
pub async fn delete(&self, conn: &mut DbConn) -> EmptyResult {
pub async fn delete(&self, conn: &DbConn) -> EmptyResult {
CollectionGroup::delete_all_by_group(&self.uuid, conn).await?;
GroupUser::delete_all_by_group(&self.uuid, conn).await?;
@ -291,14 +287,14 @@ impl Group {
}}
}
pub async fn update_revision(uuid: &GroupId, conn: &mut DbConn) {
pub async fn update_revision(uuid: &GroupId, conn: &DbConn) {
if let Err(e) = Self::_update_revision(uuid, &Utc::now().naive_utc(), conn).await {
warn!("Failed to update revision for {uuid}: {e:#?}");
}
}
async fn _update_revision(uuid: &GroupId, date: &NaiveDateTime, conn: &mut DbConn) -> EmptyResult {
db_run! {conn: {
async fn _update_revision(uuid: &GroupId, date: &NaiveDateTime, conn: &DbConn) -> EmptyResult {
db_run! { conn: {
crate::util::retry(|| {
diesel::update(groups::table.filter(groups::uuid.eq(uuid)))
.set(groups::revision_date.eq(date))
@ -310,7 +306,7 @@ impl Group {
}
impl CollectionGroup {
pub async fn save(&mut self, conn: &mut DbConn) -> EmptyResult {
pub async fn save(&mut self, conn: &DbConn) -> EmptyResult {
let group_users = GroupUser::find_by_group(&self.groups_uuid, conn).await;
for group_user in group_users {
group_user.update_user_revision(conn).await;
@ -369,17 +365,16 @@ impl CollectionGroup {
}
}
pub async fn find_by_group(group_uuid: &GroupId, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_group(group_uuid: &GroupId, conn: &DbConn) -> Vec<Self> {
db_run! { conn: {
collections_groups::table
.filter(collections_groups::groups_uuid.eq(group_uuid))
.load::<CollectionGroupDb>(conn)
.load::<Self>(conn)
.expect("Error loading collection groups")
.from_db()
}}
}
pub async fn find_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<Self> {
db_run! { conn: {
collections_groups::table
.inner_join(groups_users::table.on(
@ -390,24 +385,22 @@ impl CollectionGroup {
))
.filter(users_organizations::user_uuid.eq(user_uuid))
.select(collections_groups::all_columns)
.load::<CollectionGroupDb>(conn)
.load::<Self>(conn)
.expect("Error loading user collection groups")
.from_db()
}}
}
pub async fn find_by_collection(collection_uuid: &CollectionId, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_collection(collection_uuid: &CollectionId, conn: &DbConn) -> Vec<Self> {
db_run! { conn: {
collections_groups::table
.filter(collections_groups::collections_uuid.eq(collection_uuid))
.select(collections_groups::all_columns)
.load::<CollectionGroupDb>(conn)
.load::<Self>(conn)
.expect("Error loading collection groups")
.from_db()
}}
}
pub async fn delete(&self, conn: &mut DbConn) -> EmptyResult {
pub async fn delete(&self, conn: &DbConn) -> EmptyResult {
let group_users = GroupUser::find_by_group(&self.groups_uuid, conn).await;
for group_user in group_users {
group_user.update_user_revision(conn).await;
@ -422,7 +415,7 @@ impl CollectionGroup {
}}
}
pub async fn delete_all_by_group(group_uuid: &GroupId, conn: &mut DbConn) -> EmptyResult {
pub async fn delete_all_by_group(group_uuid: &GroupId, conn: &DbConn) -> EmptyResult {
let group_users = GroupUser::find_by_group(group_uuid, conn).await;
for group_user in group_users {
group_user.update_user_revision(conn).await;
@ -436,7 +429,7 @@ impl CollectionGroup {
}}
}
pub async fn delete_all_by_collection(collection_uuid: &CollectionId, conn: &mut DbConn) -> EmptyResult {
pub async fn delete_all_by_collection(collection_uuid: &CollectionId, conn: &DbConn) -> EmptyResult {
let collection_assigned_to_groups = CollectionGroup::find_by_collection(collection_uuid, conn).await;
for collection_assigned_to_group in collection_assigned_to_groups {
let group_users = GroupUser::find_by_group(&collection_assigned_to_group.groups_uuid, conn).await;
@ -455,7 +448,7 @@ impl CollectionGroup {
}
impl GroupUser {
pub async fn save(&mut self, conn: &mut DbConn) -> EmptyResult {
pub async fn save(&mut self, conn: &DbConn) -> EmptyResult {
self.update_user_revision(conn).await;
db_run! { conn:
@ -501,30 +494,28 @@ impl GroupUser {
}
}
pub async fn find_by_group(group_uuid: &GroupId, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_group(group_uuid: &GroupId, conn: &DbConn) -> Vec<Self> {
db_run! { conn: {
groups_users::table
.filter(groups_users::groups_uuid.eq(group_uuid))
.load::<GroupUserDb>(conn)
.load::<Self>(conn)
.expect("Error loading group users")
.from_db()
}}
}
pub async fn find_by_member(member_uuid: &MembershipId, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_member(member_uuid: &MembershipId, conn: &DbConn) -> Vec<Self> {
db_run! { conn: {
groups_users::table
.filter(groups_users::users_organizations_uuid.eq(member_uuid))
.load::<GroupUserDb>(conn)
.load::<Self>(conn)
.expect("Error loading groups for user")
.from_db()
}}
}
pub async fn has_access_to_collection_by_member(
collection_uuid: &CollectionId,
member_uuid: &MembershipId,
conn: &mut DbConn,
conn: &DbConn,
) -> bool {
db_run! { conn: {
groups_users::table
@ -542,7 +533,7 @@ impl GroupUser {
pub async fn has_full_access_by_member(
org_uuid: &OrganizationId,
member_uuid: &MembershipId,
conn: &mut DbConn,
conn: &DbConn,
) -> bool {
db_run! { conn: {
groups_users::table
@ -558,7 +549,7 @@ impl GroupUser {
}}
}
pub async fn update_user_revision(&self, conn: &mut DbConn) {
pub async fn update_user_revision(&self, conn: &DbConn) {
match Membership::find_by_uuid(&self.users_organizations_uuid, conn).await {
Some(member) => User::update_uuid_revision(&member.user_uuid, conn).await,
None => warn!("Member could not be found!"),
@ -568,7 +559,7 @@ impl GroupUser {
pub async fn delete_by_group_and_member(
group_uuid: &GroupId,
member_uuid: &MembershipId,
conn: &mut DbConn,
conn: &DbConn,
) -> EmptyResult {
match Membership::find_by_uuid(member_uuid, conn).await {
Some(member) => User::update_uuid_revision(&member.user_uuid, conn).await,
@ -584,7 +575,7 @@ impl GroupUser {
}}
}
pub async fn delete_all_by_group(group_uuid: &GroupId, conn: &mut DbConn) -> EmptyResult {
pub async fn delete_all_by_group(group_uuid: &GroupId, conn: &DbConn) -> EmptyResult {
let group_users = GroupUser::find_by_group(group_uuid, conn).await;
for group_user in group_users {
group_user.update_user_revision(conn).await;
@ -598,7 +589,7 @@ impl GroupUser {
}}
}
pub async fn delete_all_by_member(member_uuid: &MembershipId, conn: &mut DbConn) -> EmptyResult {
pub async fn delete_all_by_member(member_uuid: &MembershipId, conn: &DbConn) -> EmptyResult {
match Membership::find_by_uuid(member_uuid, conn).await {
Some(member) => User::update_uuid_revision(&member.user_uuid, conn).await,
None => warn!("Member could not be found!"),

74
src/db/models/org_policy.rs

@ -3,22 +3,22 @@ use serde::Deserialize;
use serde_json::Value;
use crate::api::EmptyResult;
use crate::db::schema::{org_policies, users_organizations};
use crate::db::DbConn;
use crate::error::MapResult;
use diesel::prelude::*;
use super::{Membership, MembershipId, MembershipStatus, MembershipType, OrganizationId, TwoFactor, UserId};
db_object! {
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = org_policies)]
#[diesel(primary_key(uuid))]
pub struct OrgPolicy {
pub uuid: OrgPolicyId,
pub org_uuid: OrganizationId,
pub atype: i32,
pub enabled: bool,
pub data: String,
}
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = org_policies)]
#[diesel(primary_key(uuid))]
pub struct OrgPolicy {
pub uuid: OrgPolicyId,
pub org_uuid: OrganizationId,
pub atype: i32,
pub enabled: bool,
pub data: String,
}
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Enums/PolicyType.cs
@ -107,11 +107,11 @@ impl OrgPolicy {
/// Database methods
impl OrgPolicy {
pub async fn save(&self, conn: &mut DbConn) -> EmptyResult {
pub async fn save(&self, conn: &DbConn) -> EmptyResult {
db_run! { conn:
sqlite, mysql {
match diesel::replace_into(org_policies::table)
.values(OrgPolicyDb::to_db(self))
.values(self)
.execute(conn)
{
Ok(_) => Ok(()),
@ -119,7 +119,7 @@ impl OrgPolicy {
Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => {
diesel::update(org_policies::table)
.filter(org_policies::uuid.eq(&self.uuid))
.set(OrgPolicyDb::to_db(self))
.set(self)
.execute(conn)
.map_res("Error saving org_policy")
}
@ -127,7 +127,6 @@ impl OrgPolicy {
}.map_res("Error saving org_policy")
}
postgresql {
let value = OrgPolicyDb::to_db(self);
// We need to make sure we're not going to violate the unique constraint on org_uuid and atype.
// This happens automatically on other DBMS backends due to replace_into(). PostgreSQL does
// not support multiple constraints on ON CONFLICT clauses.
@ -140,17 +139,17 @@ impl OrgPolicy {
.map_res("Error deleting org_policy for insert")?;
diesel::insert_into(org_policies::table)
.values(&value)
.values(self)
.on_conflict(org_policies::uuid)
.do_update()
.set(&value)
.set(self)
.execute(conn)
.map_res("Error saving org_policy")
}
}
}
pub async fn delete(self, conn: &mut DbConn) -> EmptyResult {
pub async fn delete(self, conn: &DbConn) -> EmptyResult {
db_run! { conn: {
diesel::delete(org_policies::table.filter(org_policies::uuid.eq(self.uuid)))
.execute(conn)
@ -158,17 +157,16 @@ impl OrgPolicy {
}}
}
pub async fn find_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_org(org_uuid: &OrganizationId, conn: &DbConn) -> Vec<Self> {
db_run! { conn: {
org_policies::table
.filter(org_policies::org_uuid.eq(org_uuid))
.load::<OrgPolicyDb>(conn)
.load::<Self>(conn)
.expect("Error loading org_policy")
.from_db()
}}
}
pub async fn find_confirmed_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_confirmed_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<Self> {
db_run! { conn: {
org_policies::table
.inner_join(
@ -180,28 +178,26 @@ impl OrgPolicy {
users_organizations::status.eq(MembershipStatus::Confirmed as i32)
)
.select(org_policies::all_columns)
.load::<OrgPolicyDb>(conn)
.load::<Self>(conn)
.expect("Error loading org_policy")
.from_db()
}}
}
pub async fn find_by_org_and_type(
org_uuid: &OrganizationId,
policy_type: OrgPolicyType,
conn: &mut DbConn,
conn: &DbConn,
) -> Option<Self> {
db_run! { conn: {
org_policies::table
.filter(org_policies::org_uuid.eq(org_uuid))
.filter(org_policies::atype.eq(policy_type as i32))
.first::<OrgPolicyDb>(conn)
.first::<Self>(conn)
.ok()
.from_db()
}}
}
pub async fn delete_all_by_organization(org_uuid: &OrganizationId, conn: &mut DbConn) -> EmptyResult {
pub async fn delete_all_by_organization(org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult {
db_run! { conn: {
diesel::delete(org_policies::table.filter(org_policies::org_uuid.eq(org_uuid)))
.execute(conn)
@ -230,16 +226,15 @@ impl OrgPolicy {
.filter(org_policies::atype.eq(policy_type as i32))
.filter(org_policies::enabled.eq(true))
.select(org_policies::all_columns)
.load::<OrgPolicyDb>(conn)
.load::<Self>(conn)
.expect("Error loading org_policy")
.from_db()
}}
}
pub async fn find_confirmed_by_user_and_active_policy(
user_uuid: &UserId,
policy_type: OrgPolicyType,
conn: &mut DbConn,
conn: &DbConn,
) -> Vec<Self> {
db_run! { conn: {
org_policies::table
@ -254,9 +249,8 @@ impl OrgPolicy {
.filter(org_policies::atype.eq(policy_type as i32))
.filter(org_policies::enabled.eq(true))
.select(org_policies::all_columns)
.load::<OrgPolicyDb>(conn)
.load::<Self>(conn)
.expect("Error loading org_policy")
.from_db()
}}
}
@ -267,7 +261,7 @@ impl OrgPolicy {
user_uuid: &UserId,
policy_type: OrgPolicyType,
exclude_org_uuid: Option<&OrganizationId>,
conn: &mut DbConn,
conn: &DbConn,
) -> bool {
for policy in
OrgPolicy::find_accepted_and_confirmed_by_user_and_active_policy(user_uuid, policy_type, conn).await
@ -290,7 +284,7 @@ impl OrgPolicy {
user_uuid: &UserId,
org_uuid: &OrganizationId,
exclude_current_org: bool,
conn: &mut DbConn,
conn: &DbConn,
) -> OrgPolicyResult {
// Enforce TwoFactor/TwoStep login
if TwoFactor::find_by_user(user_uuid, conn).await.is_empty() {
@ -316,7 +310,7 @@ impl OrgPolicy {
Ok(())
}
pub async fn org_is_reset_password_auto_enroll(org_uuid: &OrganizationId, conn: &mut DbConn) -> bool {
pub async fn org_is_reset_password_auto_enroll(org_uuid: &OrganizationId, conn: &DbConn) -> bool {
match OrgPolicy::find_by_org_and_type(org_uuid, OrgPolicyType::ResetPassword, conn).await {
Some(policy) => match serde_json::from_str::<ResetPasswordDataModel>(&policy.data) {
Ok(opts) => {
@ -332,7 +326,7 @@ impl OrgPolicy {
/// Returns true if the user belongs to an org that has enabled the `DisableHideEmail`
/// option of the `Send Options` policy, and the user is not an owner or admin of that org.
pub async fn is_hide_email_disabled(user_uuid: &UserId, conn: &mut DbConn) -> bool {
pub async fn is_hide_email_disabled(user_uuid: &UserId, conn: &DbConn) -> bool {
for policy in
OrgPolicy::find_confirmed_by_user_and_active_policy(user_uuid, OrgPolicyType::SendOptions, conn).await
{
@ -352,11 +346,7 @@ impl OrgPolicy {
false
}
pub async fn is_enabled_for_member(
member_uuid: &MembershipId,
policy_type: OrgPolicyType,
conn: &mut DbConn,
) -> bool {
pub async fn is_enabled_for_member(member_uuid: &MembershipId, policy_type: OrgPolicyType, conn: &DbConn) -> bool {
if let Some(member) = Membership::find_by_uuid(member_uuid, conn).await {
if let Some(policy) = OrgPolicy::find_by_org_and_type(&member.org_uuid, policy_type, conn).await {
return policy.enabled;

331
src/db/models/organization.rs

@ -1,5 +1,6 @@
use chrono::{NaiveDateTime, Utc};
use derive_more::{AsRef, Deref, Display, From};
use diesel::prelude::*;
use num_traits::FromPrimitive;
use serde_json::Value;
use std::{
@ -11,51 +12,53 @@ use super::{
CipherId, Collection, CollectionGroup, CollectionId, CollectionUser, Group, GroupId, GroupUser, OrgPolicy,
OrgPolicyType, TwoFactor, User, UserId,
};
use crate::db::schema::{
ciphers, ciphers_collections, collections_groups, groups, groups_users, org_policies, organization_api_key,
organizations, users, users_collections, users_organizations,
};
use crate::CONFIG;
use macros::UuidFromParam;
db_object! {
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = organizations)]
#[diesel(treat_none_as_null = true)]
#[diesel(primary_key(uuid))]
pub struct Organization {
pub uuid: OrganizationId,
pub name: String,
pub billing_email: String,
pub private_key: Option<String>,
pub public_key: Option<String>,
}
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = users_organizations)]
#[diesel(treat_none_as_null = true)]
#[diesel(primary_key(uuid))]
pub struct Membership {
pub uuid: MembershipId,
pub user_uuid: UserId,
pub org_uuid: OrganizationId,
pub invited_by_email: Option<String>,
pub access_all: bool,
pub akey: String,
pub status: i32,
pub atype: i32,
pub reset_password_key: Option<String>,
pub external_id: Option<String>,
}
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = organization_api_key)]
#[diesel(primary_key(uuid, org_uuid))]
pub struct OrganizationApiKey {
pub uuid: OrgApiKeyId,
pub org_uuid: OrganizationId,
pub atype: i32,
pub api_key: String,
pub revision_date: NaiveDateTime,
}
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = organizations)]
#[diesel(treat_none_as_null = true)]
#[diesel(primary_key(uuid))]
pub struct Organization {
pub uuid: OrganizationId,
pub name: String,
pub billing_email: String,
pub private_key: Option<String>,
pub public_key: Option<String>,
}
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = users_organizations)]
#[diesel(treat_none_as_null = true)]
#[diesel(primary_key(uuid))]
pub struct Membership {
pub uuid: MembershipId,
pub user_uuid: UserId,
pub org_uuid: OrganizationId,
pub invited_by_email: Option<String>,
pub access_all: bool,
pub akey: String,
pub status: i32,
pub atype: i32,
pub reset_password_key: Option<String>,
pub external_id: Option<String>,
}
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = organization_api_key)]
#[diesel(primary_key(uuid, org_uuid))]
pub struct OrganizationApiKey {
pub uuid: OrgApiKeyId,
pub org_uuid: OrganizationId,
pub atype: i32,
pub api_key: String,
pub revision_date: NaiveDateTime,
}
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Enums/OrganizationUserStatusType.cs
@ -169,7 +172,7 @@ impl PartialOrd<MembershipType> for i32 {
/// Local methods
impl Organization {
pub fn new(name: String, billing_email: String, private_key: Option<String>, public_key: Option<String>) -> Self {
pub fn new(name: String, billing_email: &str, private_key: Option<String>, public_key: Option<String>) -> Self {
let billing_email = billing_email.to_lowercase();
Self {
uuid: OrganizationId(crate::util::get_uuid()),
@ -325,7 +328,7 @@ use crate::error::MapResult;
/// Database methods
impl Organization {
pub async fn save(&self, conn: &mut DbConn) -> EmptyResult {
pub async fn save(&self, conn: &DbConn) -> EmptyResult {
if !crate::util::is_valid_email(&self.billing_email) {
err!(format!("BillingEmail {} is not a valid email address", self.billing_email))
}
@ -337,7 +340,7 @@ impl Organization {
db_run! { conn:
sqlite, mysql {
match diesel::replace_into(organizations::table)
.values(OrganizationDb::to_db(self))
.values(self)
.execute(conn)
{
Ok(_) => Ok(()),
@ -345,7 +348,7 @@ impl Organization {
Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => {
diesel::update(organizations::table)
.filter(organizations::uuid.eq(&self.uuid))
.set(OrganizationDb::to_db(self))
.set(self)
.execute(conn)
.map_res("Error saving organization")
}
@ -354,19 +357,18 @@ impl Organization {
}
postgresql {
let value = OrganizationDb::to_db(self);
diesel::insert_into(organizations::table)
.values(&value)
.values(self)
.on_conflict(organizations::uuid)
.do_update()
.set(&value)
.set(self)
.execute(conn)
.map_res("Error saving organization")
}
}
}
pub async fn delete(self, conn: &mut DbConn) -> EmptyResult {
pub async fn delete(self, conn: &DbConn) -> EmptyResult {
use super::{Cipher, Collection};
Cipher::delete_all_by_organization(&self.uuid, conn).await?;
@ -383,31 +385,33 @@ impl Organization {
}}
}
pub async fn find_by_uuid(uuid: &OrganizationId, conn: &mut DbConn) -> Option<Self> {
pub async fn find_by_uuid(uuid: &OrganizationId, conn: &DbConn) -> Option<Self> {
db_run! { conn: {
organizations::table
.filter(organizations::uuid.eq(uuid))
.first::<OrganizationDb>(conn)
.ok().from_db()
.first::<Self>(conn)
.ok()
}}
}
pub async fn find_by_name(name: &str, conn: &mut DbConn) -> Option<Self> {
pub async fn find_by_name(name: &str, conn: &DbConn) -> Option<Self> {
db_run! { conn: {
organizations::table
.filter(organizations::name.eq(name))
.first::<OrganizationDb>(conn)
.ok().from_db()
.first::<Self>(conn)
.ok()
}}
}
pub async fn get_all(conn: &mut DbConn) -> Vec<Self> {
pub async fn get_all(conn: &DbConn) -> Vec<Self> {
db_run! { conn: {
organizations::table.load::<OrganizationDb>(conn).expect("Error loading organizations").from_db()
organizations::table
.load::<Self>(conn)
.expect("Error loading organizations")
}}
}
pub async fn find_main_org_user_email(user_email: &str, conn: &mut DbConn) -> Option<Organization> {
pub async fn find_main_org_user_email(user_email: &str, conn: &DbConn) -> Option<Self> {
let lower_mail = user_email.to_lowercase();
db_run! { conn: {
@ -418,12 +422,12 @@ impl Organization {
.filter(users_organizations::status.ne(MembershipStatus::Revoked as i32))
.order(users_organizations::atype.asc())
.select(organizations::all_columns)
.first::<OrganizationDb>(conn)
.ok().from_db()
.first::<Self>(conn)
.ok()
}}
}
pub async fn find_org_user_email(user_email: &str, conn: &mut DbConn) -> Vec<Organization> {
pub async fn find_org_user_email(user_email: &str, conn: &DbConn) -> Vec<Self> {
let lower_mail = user_email.to_lowercase();
db_run! { conn: {
@ -434,15 +438,14 @@ impl Organization {
.filter(users_organizations::status.ne(MembershipStatus::Revoked as i32))
.order(users_organizations::atype.asc())
.select(organizations::all_columns)
.load::<OrganizationDb>(conn)
.load::<Self>(conn)
.expect("Error loading user orgs")
.from_db()
}}
}
}
impl Membership {
pub async fn to_json(&self, conn: &mut DbConn) -> Value {
pub async fn to_json(&self, conn: &DbConn) -> Value {
let org = Organization::find_by_uuid(&self.org_uuid, conn).await.unwrap();
// HACK: Convert the manager type to a custom type
@ -533,12 +536,7 @@ impl Membership {
})
}
pub async fn to_json_user_details(
&self,
include_collections: bool,
include_groups: bool,
conn: &mut DbConn,
) -> Value {
pub async fn to_json_user_details(&self, include_collections: bool, include_groups: bool, conn: &DbConn) -> Value {
let user = User::find_by_uuid(&self.user_uuid, conn).await.unwrap();
// Because BitWarden want the status to be -1 for revoked users we need to catch that here.
@ -680,7 +678,7 @@ impl Membership {
})
}
pub async fn to_json_details(&self, conn: &mut DbConn) -> Value {
pub async fn to_json_details(&self, conn: &DbConn) -> Value {
let coll_uuids = if self.access_all {
vec![] // If we have complete access, no need to fill the array
} else {
@ -720,7 +718,7 @@ impl Membership {
})
}
pub async fn to_json_mini_details(&self, conn: &mut DbConn) -> Value {
pub async fn to_json_mini_details(&self, conn: &DbConn) -> Value {
let user = User::find_by_uuid(&self.user_uuid, conn).await.unwrap();
// Because Bitwarden wants the status to be -1 for revoked users we need to catch that here.
@ -742,13 +740,13 @@ impl Membership {
})
}
pub async fn save(&self, conn: &mut DbConn) -> EmptyResult {
pub async fn save(&self, conn: &DbConn) -> EmptyResult {
User::update_uuid_revision(&self.user_uuid, conn).await;
db_run! { conn:
sqlite, mysql {
match diesel::replace_into(users_organizations::table)
.values(MembershipDb::to_db(self))
.values(self)
.execute(conn)
{
Ok(_) => Ok(()),
@ -756,7 +754,7 @@ impl Membership {
Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => {
diesel::update(users_organizations::table)
.filter(users_organizations::uuid.eq(&self.uuid))
.set(MembershipDb::to_db(self))
.set(self)
.execute(conn)
.map_res("Error adding user to organization")
},
@ -764,19 +762,18 @@ impl Membership {
}.map_res("Error adding user to organization")
}
postgresql {
let value = MembershipDb::to_db(self);
diesel::insert_into(users_organizations::table)
.values(&value)
.values(self)
.on_conflict(users_organizations::uuid)
.do_update()
.set(&value)
.set(self)
.execute(conn)
.map_res("Error adding user to organization")
}
}
}
pub async fn delete(self, conn: &mut DbConn) -> EmptyResult {
pub async fn delete(self, conn: &DbConn) -> EmptyResult {
User::update_uuid_revision(&self.user_uuid, conn).await;
CollectionUser::delete_all_by_user_and_org(&self.user_uuid, &self.org_uuid, conn).await?;
@ -789,25 +786,21 @@ impl Membership {
}}
}
pub async fn delete_all_by_organization(org_uuid: &OrganizationId, conn: &mut DbConn) -> EmptyResult {
pub async fn delete_all_by_organization(org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult {
for member in Self::find_by_org(org_uuid, conn).await {
member.delete(conn).await?;
}
Ok(())
}
pub async fn delete_all_by_user(user_uuid: &UserId, conn: &mut DbConn) -> EmptyResult {
pub async fn delete_all_by_user(user_uuid: &UserId, conn: &DbConn) -> EmptyResult {
for member in Self::find_any_state_by_user(user_uuid, conn).await {
member.delete(conn).await?;
}
Ok(())
}
pub async fn find_by_email_and_org(
email: &str,
org_uuid: &OrganizationId,
conn: &mut DbConn,
) -> Option<Membership> {
pub async fn find_by_email_and_org(email: &str, org_uuid: &OrganizationId, conn: &DbConn) -> Option<Membership> {
if let Some(user) = User::find_by_mail(email, conn).await {
if let Some(member) = Membership::find_by_user_and_org(&user.uuid, org_uuid, conn).await {
return Some(member);
@ -829,52 +822,48 @@ impl Membership {
(self.access_all || self.atype >= MembershipType::Admin) && self.has_status(MembershipStatus::Confirmed)
}
pub async fn find_by_uuid(uuid: &MembershipId, conn: &mut DbConn) -> Option<Self> {
pub async fn find_by_uuid(uuid: &MembershipId, conn: &DbConn) -> Option<Self> {
db_run! { conn: {
users_organizations::table
.filter(users_organizations::uuid.eq(uuid))
.first::<MembershipDb>(conn)
.ok().from_db()
.first::<Self>(conn)
.ok()
}}
}
pub async fn find_by_uuid_and_org(
uuid: &MembershipId,
org_uuid: &OrganizationId,
conn: &mut DbConn,
) -> Option<Self> {
pub async fn find_by_uuid_and_org(uuid: &MembershipId, org_uuid: &OrganizationId, conn: &DbConn) -> Option<Self> {
db_run! { conn: {
users_organizations::table
.filter(users_organizations::uuid.eq(uuid))
.filter(users_organizations::org_uuid.eq(org_uuid))
.first::<MembershipDb>(conn)
.ok().from_db()
.first::<Self>(conn)
.ok()
}}
}
pub async fn find_confirmed_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_confirmed_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<Self> {
db_run! { conn: {
users_organizations::table
.filter(users_organizations::user_uuid.eq(user_uuid))
.filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
.load::<MembershipDb>(conn)
.unwrap_or_default().from_db()
.load::<Self>(conn)
.unwrap_or_default()
}}
}
pub async fn find_invited_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_invited_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<Self> {
db_run! { conn: {
users_organizations::table
.filter(users_organizations::user_uuid.eq(user_uuid))
.filter(users_organizations::status.eq(MembershipStatus::Invited as i32))
.load::<MembershipDb>(conn)
.unwrap_or_default().from_db()
.load::<Self>(conn)
.unwrap_or_default()
}}
}
// Should be used only when email are disabled.
// In Organizations::send_invite status is set to Accepted only if the user has a password.
pub async fn accept_user_invitations(user_uuid: &UserId, conn: &mut DbConn) -> EmptyResult {
pub async fn accept_user_invitations(user_uuid: &UserId, conn: &DbConn) -> EmptyResult {
db_run! { conn: {
diesel::update(users_organizations::table)
.filter(users_organizations::user_uuid.eq(user_uuid))
@ -885,16 +874,16 @@ impl Membership {
}}
}
pub async fn find_any_state_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_any_state_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<Self> {
db_run! { conn: {
users_organizations::table
.filter(users_organizations::user_uuid.eq(user_uuid))
.load::<MembershipDb>(conn)
.unwrap_or_default().from_db()
.load::<Self>(conn)
.unwrap_or_default()
}}
}
pub async fn count_accepted_and_confirmed_by_user(user_uuid: &UserId, conn: &mut DbConn) -> i64 {
pub async fn count_accepted_and_confirmed_by_user(user_uuid: &UserId, conn: &DbConn) -> i64 {
db_run! { conn: {
users_organizations::table
.filter(users_organizations::user_uuid.eq(user_uuid))
@ -905,27 +894,27 @@ impl Membership {
}}
}
pub async fn find_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_org(org_uuid: &OrganizationId, conn: &DbConn) -> Vec<Self> {
db_run! { conn: {
users_organizations::table
.filter(users_organizations::org_uuid.eq(org_uuid))
.load::<MembershipDb>(conn)
.expect("Error loading user organizations").from_db()
.load::<Self>(conn)
.expect("Error loading user organizations")
}}
}
pub async fn find_confirmed_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_confirmed_by_org(org_uuid: &OrganizationId, conn: &DbConn) -> Vec<Self> {
db_run! { conn: {
users_organizations::table
.filter(users_organizations::org_uuid.eq(org_uuid))
.filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
.load::<MembershipDb>(conn)
.unwrap_or_default().from_db()
.load::<Self>(conn)
.unwrap_or_default()
}}
}
// Get all users which are either owner or admin, or a manager which can manage/access all
pub async fn find_confirmed_and_manage_all_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_confirmed_and_manage_all_by_org(org_uuid: &OrganizationId, conn: &DbConn) -> Vec<Self> {
db_run! { conn: {
users_organizations::table
.filter(users_organizations::org_uuid.eq(org_uuid))
@ -934,12 +923,12 @@ impl Membership {
users_organizations::atype.eq_any(vec![MembershipType::Owner as i32, MembershipType::Admin as i32])
.or(users_organizations::atype.eq(MembershipType::Manager as i32).and(users_organizations::access_all.eq(true)))
)
.load::<MembershipDb>(conn)
.unwrap_or_default().from_db()
.load::<Self>(conn)
.unwrap_or_default()
}}
}
pub async fn count_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> i64 {
pub async fn count_by_org(org_uuid: &OrganizationId, conn: &DbConn) -> i64 {
db_run! { conn: {
users_organizations::table
.filter(users_organizations::org_uuid.eq(org_uuid))
@ -950,24 +939,20 @@ impl Membership {
}}
}
pub async fn find_by_org_and_type(
org_uuid: &OrganizationId,
atype: MembershipType,
conn: &mut DbConn,
) -> Vec<Self> {
pub async fn find_by_org_and_type(org_uuid: &OrganizationId, atype: MembershipType, conn: &DbConn) -> Vec<Self> {
db_run! { conn: {
users_organizations::table
.filter(users_organizations::org_uuid.eq(org_uuid))
.filter(users_organizations::atype.eq(atype as i32))
.load::<MembershipDb>(conn)
.expect("Error loading user organizations").from_db()
.load::<Self>(conn)
.expect("Error loading user organizations")
}}
}
pub async fn count_confirmed_by_org_and_type(
org_uuid: &OrganizationId,
atype: MembershipType,
conn: &mut DbConn,
conn: &DbConn,
) -> i64 {
db_run! { conn: {
users_organizations::table
@ -980,24 +965,20 @@ impl Membership {
}}
}
pub async fn find_by_user_and_org(
user_uuid: &UserId,
org_uuid: &OrganizationId,
conn: &mut DbConn,
) -> Option<Self> {
pub async fn find_by_user_and_org(user_uuid: &UserId, org_uuid: &OrganizationId, conn: &DbConn) -> Option<Self> {
db_run! { conn: {
users_organizations::table
.filter(users_organizations::user_uuid.eq(user_uuid))
.filter(users_organizations::org_uuid.eq(org_uuid))
.first::<MembershipDb>(conn)
.ok().from_db()
.first::<Self>(conn)
.ok()
}}
}
pub async fn find_confirmed_by_user_and_org(
user_uuid: &UserId,
org_uuid: &OrganizationId,
conn: &mut DbConn,
conn: &DbConn,
) -> Option<Self> {
db_run! { conn: {
users_organizations::table
@ -1006,21 +987,21 @@ impl Membership {
.filter(
users_organizations::status.eq(MembershipStatus::Confirmed as i32)
)
.first::<MembershipDb>(conn)
.ok().from_db()
.first::<Self>(conn)
.ok()
}}
}
pub async fn find_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<Self> {
db_run! { conn: {
users_organizations::table
.filter(users_organizations::user_uuid.eq(user_uuid))
.load::<MembershipDb>(conn)
.expect("Error loading user organizations").from_db()
.load::<Self>(conn)
.expect("Error loading user organizations")
}}
}
pub async fn get_orgs_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<OrganizationId> {
pub async fn get_orgs_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<OrganizationId> {
db_run! { conn: {
users_organizations::table
.filter(users_organizations::user_uuid.eq(user_uuid))
@ -1030,11 +1011,7 @@ impl Membership {
}}
}
pub async fn find_by_user_and_policy(
user_uuid: &UserId,
policy_type: OrgPolicyType,
conn: &mut DbConn,
) -> Vec<Self> {
pub async fn find_by_user_and_policy(user_uuid: &UserId, policy_type: OrgPolicyType, conn: &DbConn) -> Vec<Self> {
db_run! { conn: {
users_organizations::table
.inner_join(
@ -1048,16 +1025,12 @@ impl Membership {
users_organizations::status.eq(MembershipStatus::Confirmed as i32)
)
.select(users_organizations::all_columns)
.load::<MembershipDb>(conn)
.unwrap_or_default().from_db()
.load::<Self>(conn)
.unwrap_or_default()
}}
}
pub async fn find_by_cipher_and_org(
cipher_uuid: &CipherId,
org_uuid: &OrganizationId,
conn: &mut DbConn,
) -> Vec<Self> {
pub async fn find_by_cipher_and_org(cipher_uuid: &CipherId, org_uuid: &OrganizationId, conn: &DbConn) -> Vec<Self> {
db_run! { conn: {
users_organizations::table
.filter(users_organizations::org_uuid.eq(org_uuid))
@ -1076,14 +1049,15 @@ impl Membership {
)
.select(users_organizations::all_columns)
.distinct()
.load::<MembershipDb>(conn).expect("Error loading user organizations").from_db()
.load::<Self>(conn)
.expect("Error loading user organizations")
}}
}
pub async fn find_by_cipher_and_org_with_group(
cipher_uuid: &CipherId,
org_uuid: &OrganizationId,
conn: &mut DbConn,
conn: &DbConn,
) -> Vec<Self> {
db_run! { conn: {
users_organizations::table
@ -1106,15 +1080,12 @@ impl Membership {
)
.select(users_organizations::all_columns)
.distinct()
.load::<MembershipDb>(conn).expect("Error loading user organizations with groups").from_db()
.load::<Self>(conn)
.expect("Error loading user organizations with groups")
}}
}
pub async fn user_has_ge_admin_access_to_cipher(
user_uuid: &UserId,
cipher_uuid: &CipherId,
conn: &mut DbConn,
) -> bool {
pub async fn user_has_ge_admin_access_to_cipher(user_uuid: &UserId, cipher_uuid: &CipherId, conn: &DbConn) -> bool {
db_run! { conn: {
users_organizations::table
.inner_join(ciphers::table.on(ciphers::uuid.eq(cipher_uuid).and(ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable()))))
@ -1122,14 +1093,15 @@ impl Membership {
.filter(users_organizations::atype.eq_any(vec![MembershipType::Owner as i32, MembershipType::Admin as i32]))
.count()
.first::<i64>(conn)
.ok().unwrap_or(0) != 0
.ok()
.unwrap_or(0) != 0
}}
}
pub async fn find_by_collection_and_org(
collection_uuid: &CollectionId,
org_uuid: &OrganizationId,
conn: &mut DbConn,
conn: &DbConn,
) -> Vec<Self> {
db_run! { conn: {
users_organizations::table
@ -1143,33 +1115,31 @@ impl Membership {
)
)
.select(users_organizations::all_columns)
.load::<MembershipDb>(conn).expect("Error loading user organizations").from_db()
.load::<Self>(conn)
.expect("Error loading user organizations")
}}
}
pub async fn find_by_external_id_and_org(
ext_id: &str,
org_uuid: &OrganizationId,
conn: &mut DbConn,
) -> Option<Self> {
db_run! {conn: {
pub async fn find_by_external_id_and_org(ext_id: &str, org_uuid: &OrganizationId, conn: &DbConn) -> Option<Self> {
db_run! { conn: {
users_organizations::table
.filter(
users_organizations::external_id.eq(ext_id)
.and(users_organizations::org_uuid.eq(org_uuid))
)
.first::<MembershipDb>(conn).ok().from_db()
.first::<Self>(conn)
.ok()
}}
}
pub async fn find_main_user_org(user_uuid: &str, conn: &mut DbConn) -> Option<Self> {
pub async fn find_main_user_org(user_uuid: &str, conn: &DbConn) -> Option<Self> {
db_run! { conn: {
users_organizations::table
.filter(users_organizations::user_uuid.eq(user_uuid))
.filter(users_organizations::status.ne(MembershipStatus::Revoked as i32))
.order(users_organizations::atype.asc())
.first::<MembershipDb>(conn)
.ok().from_db()
.first::<Self>(conn)
.ok()
}}
}
}
@ -1179,7 +1149,7 @@ impl OrganizationApiKey {
db_run! { conn:
sqlite, mysql {
match diesel::replace_into(organization_api_key::table)
.values(OrganizationApiKeyDb::to_db(self))
.values(self)
.execute(conn)
{
Ok(_) => Ok(()),
@ -1187,7 +1157,7 @@ impl OrganizationApiKey {
Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => {
diesel::update(organization_api_key::table)
.filter(organization_api_key::uuid.eq(&self.uuid))
.set(OrganizationApiKeyDb::to_db(self))
.set(self)
.execute(conn)
.map_res("Error saving organization")
}
@ -1196,12 +1166,11 @@ impl OrganizationApiKey {
}
postgresql {
let value = OrganizationApiKeyDb::to_db(self);
diesel::insert_into(organization_api_key::table)
.values(&value)
.values(self)
.on_conflict((organization_api_key::uuid, organization_api_key::org_uuid))
.do_update()
.set(&value)
.set(self)
.execute(conn)
.map_res("Error saving organization")
}
@ -1212,12 +1181,12 @@ impl OrganizationApiKey {
db_run! { conn: {
organization_api_key::table
.filter(organization_api_key::org_uuid.eq(org_uuid))
.first::<OrganizationApiKeyDb>(conn)
.ok().from_db()
.first::<Self>(conn)
.ok()
}}
}
pub async fn delete_all_by_organization(org_uuid: &OrganizationId, conn: &mut DbConn) -> EmptyResult {
pub async fn delete_all_by_organization(org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult {
db_run! { conn: {
diesel::delete(organization_api_key::table.filter(organization_api_key::org_uuid.eq(org_uuid)))
.execute(conn)

128
src/db/models/send.rs

@ -4,40 +4,40 @@ use serde_json::Value;
use crate::{config::PathType, util::LowerCase, CONFIG};
use super::{OrganizationId, User, UserId};
use crate::db::schema::sends;
use diesel::prelude::*;
use id::SendId;
db_object! {
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = sends)]
#[diesel(treat_none_as_null = true)]
#[diesel(primary_key(uuid))]
pub struct Send {
pub uuid: SendId,
pub user_uuid: Option<UserId>,
pub organization_uuid: Option<OrganizationId>,
pub name: String,
pub notes: Option<String>,
pub atype: i32,
pub data: String,
pub akey: String,
pub password_hash: Option<Vec<u8>>,
password_salt: Option<Vec<u8>>,
password_iter: Option<i32>,
pub max_access_count: Option<i32>,
pub access_count: i32,
pub creation_date: NaiveDateTime,
pub revision_date: NaiveDateTime,
pub expiration_date: Option<NaiveDateTime>,
pub deletion_date: NaiveDateTime,
pub disabled: bool,
pub hide_email: Option<bool>,
}
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = sends)]
#[diesel(treat_none_as_null = true)]
#[diesel(primary_key(uuid))]
pub struct Send {
pub uuid: SendId,
pub user_uuid: Option<UserId>,
pub organization_uuid: Option<OrganizationId>,
pub name: String,
pub notes: Option<String>,
pub atype: i32,
pub data: String,
pub akey: String,
pub password_hash: Option<Vec<u8>>,
password_salt: Option<Vec<u8>>,
password_iter: Option<i32>,
pub max_access_count: Option<i32>,
pub access_count: i32,
pub creation_date: NaiveDateTime,
pub revision_date: NaiveDateTime,
pub expiration_date: Option<NaiveDateTime>,
pub deletion_date: NaiveDateTime,
pub disabled: bool,
pub hide_email: Option<bool>,
}
#[derive(Copy, Clone, PartialEq, Eq, num_derive::FromPrimitive)]
@ -103,7 +103,7 @@ impl Send {
}
}
pub async fn creator_identifier(&self, conn: &mut DbConn) -> Option<String> {
pub async fn creator_identifier(&self, conn: &DbConn) -> Option<String> {
if let Some(hide_email) = self.hide_email {
if hide_email {
return None;
@ -155,7 +155,7 @@ impl Send {
})
}
pub async fn to_json_access(&self, conn: &mut DbConn) -> Value {
pub async fn to_json_access(&self, conn: &DbConn) -> Value {
use crate::util::format_date;
let mut data = serde_json::from_str::<LowerCase<Value>>(&self.data).map(|d| d.data).unwrap_or_default();
@ -187,14 +187,14 @@ use crate::error::MapResult;
use crate::util::NumberOrString;
impl Send {
pub async fn save(&mut self, conn: &mut DbConn) -> EmptyResult {
pub async fn save(&mut self, conn: &DbConn) -> EmptyResult {
self.update_users_revision(conn).await;
self.revision_date = Utc::now().naive_utc();
db_run! { conn:
sqlite, mysql {
match diesel::replace_into(sends::table)
.values(SendDb::to_db(self))
.values(&*self)
.execute(conn)
{
Ok(_) => Ok(()),
@ -202,7 +202,7 @@ impl Send {
Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => {
diesel::update(sends::table)
.filter(sends::uuid.eq(&self.uuid))
.set(SendDb::to_db(self))
.set(&*self)
.execute(conn)
.map_res("Error saving send")
}
@ -210,23 +210,22 @@ impl Send {
}.map_res("Error saving send")
}
postgresql {
let value = SendDb::to_db(self);
diesel::insert_into(sends::table)
.values(&value)
.values(&*self)
.on_conflict(sends::uuid)
.do_update()
.set(&value)
.set(&*self)
.execute(conn)
.map_res("Error saving send")
}
}
}
pub async fn delete(&self, conn: &mut DbConn) -> EmptyResult {
pub async fn delete(&self, conn: &DbConn) -> EmptyResult {
self.update_users_revision(conn).await;
if self.atype == SendType::File as i32 {
let operator = CONFIG.opendal_operator_for_path_type(PathType::Sends)?;
let operator = CONFIG.opendal_operator_for_path_type(&PathType::Sends)?;
operator.remove_all(&self.uuid).await.ok();
}
@ -238,13 +237,13 @@ impl Send {
}
/// Purge all sends that are past their deletion date.
pub async fn purge(conn: &mut DbConn) {
pub async fn purge(conn: &DbConn) {
for send in Self::find_by_past_deletion_date(conn).await {
send.delete(conn).await.ok();
}
}
pub async fn update_users_revision(&self, conn: &mut DbConn) -> Vec<UserId> {
pub async fn update_users_revision(&self, conn: &DbConn) -> Vec<UserId> {
let mut user_uuids = Vec::new();
match &self.user_uuid {
Some(user_uuid) => {
@ -258,14 +257,14 @@ impl Send {
user_uuids
}
pub async fn delete_all_by_user(user_uuid: &UserId, conn: &mut DbConn) -> EmptyResult {
pub async fn delete_all_by_user(user_uuid: &UserId, conn: &DbConn) -> EmptyResult {
for send in Self::find_by_user(user_uuid, conn).await {
send.delete(conn).await?;
}
Ok(())
}
pub async fn find_by_access_id(access_id: &str, conn: &mut DbConn) -> Option<Self> {
pub async fn find_by_access_id(access_id: &str, conn: &DbConn) -> Option<Self> {
use data_encoding::BASE64URL_NOPAD;
use uuid::Uuid;
@ -281,36 +280,35 @@ impl Send {
Self::find_by_uuid(&uuid, conn).await
}
pub async fn find_by_uuid(uuid: &SendId, conn: &mut DbConn) -> Option<Self> {
db_run! {conn: {
pub async fn find_by_uuid(uuid: &SendId, conn: &DbConn) -> Option<Self> {
db_run! { conn: {
sends::table
.filter(sends::uuid.eq(uuid))
.first::<SendDb>(conn)
.first::<Self>(conn)
.ok()
.from_db()
}}
}
pub async fn find_by_uuid_and_user(uuid: &SendId, user_uuid: &UserId, conn: &mut DbConn) -> Option<Self> {
db_run! {conn: {
pub async fn find_by_uuid_and_user(uuid: &SendId, user_uuid: &UserId, conn: &DbConn) -> Option<Self> {
db_run! { conn: {
sends::table
.filter(sends::uuid.eq(uuid))
.filter(sends::user_uuid.eq(user_uuid))
.first::<SendDb>(conn)
.first::<Self>(conn)
.ok()
.from_db()
}}
}
pub async fn find_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
db_run! {conn: {
pub async fn find_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<Self> {
db_run! { conn: {
sends::table
.filter(sends::user_uuid.eq(user_uuid))
.load::<SendDb>(conn).expect("Error loading sends").from_db()
.load::<Self>(conn)
.expect("Error loading sends")
}}
}
pub async fn size_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Option<i64> {
pub async fn size_by_user(user_uuid: &UserId, conn: &DbConn) -> Option<i64> {
let sends = Self::find_by_user(user_uuid, conn).await;
#[derive(serde::Deserialize)]
@ -333,20 +331,22 @@ impl Send {
Some(total)
}
pub async fn find_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> Vec<Self> {
db_run! {conn: {
pub async fn find_by_org(org_uuid: &OrganizationId, conn: &DbConn) -> Vec<Self> {
db_run! { conn: {
sends::table
.filter(sends::organization_uuid.eq(org_uuid))
.load::<SendDb>(conn).expect("Error loading sends").from_db()
.load::<Self>(conn)
.expect("Error loading sends")
}}
}
pub async fn find_by_past_deletion_date(conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_past_deletion_date(conn: &DbConn) -> Vec<Self> {
let now = Utc::now().naive_utc();
db_run! {conn: {
db_run! { conn: {
sends::table
.filter(sends::deletion_date.lt(now))
.load::<SendDb>(conn).expect("Error loading sends").from_db()
.load::<Self>(conn)
.expect("Error loading sends")
}}
}
}

34
src/db/models/sso_nonce.rs

@ -1,21 +1,21 @@
use chrono::{NaiveDateTime, Utc};
use crate::api::EmptyResult;
use crate::db::schema::sso_nonce;
use crate::db::{DbConn, DbPool};
use crate::error::MapResult;
use crate::sso::{OIDCState, NONCE_EXPIRATION};
use diesel::prelude::*;
db_object! {
#[derive(Identifiable, Queryable, Insertable)]
#[diesel(table_name = sso_nonce)]
#[diesel(primary_key(state))]
pub struct SsoNonce {
pub state: OIDCState,
pub nonce: String,
pub verifier: Option<String>,
pub redirect_uri: String,
pub created_at: NaiveDateTime,
}
#[derive(Identifiable, Queryable, Insertable)]
#[diesel(table_name = sso_nonce)]
#[diesel(primary_key(state))]
pub struct SsoNonce {
pub state: OIDCState,
pub nonce: String,
pub verifier: Option<String>,
pub redirect_uri: String,
pub created_at: NaiveDateTime,
}
/// Local methods
@ -35,25 +35,24 @@ impl SsoNonce {
/// Database methods
impl SsoNonce {
pub async fn save(&self, conn: &mut DbConn) -> EmptyResult {
pub async fn save(&self, conn: &DbConn) -> EmptyResult {
db_run! { conn:
sqlite, mysql {
diesel::replace_into(sso_nonce::table)
.values(SsoNonceDb::to_db(self))
.values(self)
.execute(conn)
.map_res("Error saving SSO nonce")
}
postgresql {
let value = SsoNonceDb::to_db(self);
diesel::insert_into(sso_nonce::table)
.values(&value)
.values(self)
.execute(conn)
.map_res("Error saving SSO nonce")
}
}
}
pub async fn delete(state: &OIDCState, conn: &mut DbConn) -> EmptyResult {
pub async fn delete(state: &OIDCState, conn: &DbConn) -> EmptyResult {
db_run! { conn: {
diesel::delete(sso_nonce::table.filter(sso_nonce::state.eq(state)))
.execute(conn)
@ -67,9 +66,8 @@ impl SsoNonce {
sso_nonce::table
.filter(sso_nonce::state.eq(state))
.filter(sso_nonce::created_at.ge(oldest))
.first::<SsoNonceDb>(conn)
.first::<Self>(conn)
.ok()
.from_db()
}}
}

59
src/db/models/two_factor.rs

@ -1,23 +1,23 @@
use super::UserId;
use crate::api::core::two_factor::webauthn::WebauthnRegistration;
use crate::db::schema::twofactor;
use crate::{api::EmptyResult, db::DbConn, error::MapResult};
use diesel::prelude::*;
use serde_json::Value;
use webauthn_rs::prelude::{Credential, ParsedAttestation};
use webauthn_rs_core::proto::CredentialV3;
use webauthn_rs_proto::{AttestationFormat, RegisteredExtensions};
db_object! {
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = twofactor)]
#[diesel(primary_key(uuid))]
pub struct TwoFactor {
pub uuid: TwoFactorId,
pub user_uuid: UserId,
pub atype: i32,
pub enabled: bool,
pub data: String,
pub last_used: i64,
}
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = twofactor)]
#[diesel(primary_key(uuid))]
pub struct TwoFactor {
pub uuid: TwoFactorId,
pub user_uuid: UserId,
pub atype: i32,
pub enabled: bool,
pub data: String,
pub last_used: i64,
}
#[allow(dead_code)]
@ -76,11 +76,11 @@ impl TwoFactor {
/// Database methods
impl TwoFactor {
pub async fn save(&self, conn: &mut DbConn) -> EmptyResult {
pub async fn save(&self, conn: &DbConn) -> EmptyResult {
db_run! { conn:
sqlite, mysql {
match diesel::replace_into(twofactor::table)
.values(TwoFactorDb::to_db(self))
.values(self)
.execute(conn)
{
Ok(_) => Ok(()),
@ -88,7 +88,7 @@ impl TwoFactor {
Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => {
diesel::update(twofactor::table)
.filter(twofactor::uuid.eq(&self.uuid))
.set(TwoFactorDb::to_db(self))
.set(self)
.execute(conn)
.map_res("Error saving twofactor")
}
@ -96,7 +96,6 @@ impl TwoFactor {
}.map_res("Error saving twofactor")
}
postgresql {
let value = TwoFactorDb::to_db(self);
// We need to make sure we're not going to violate the unique constraint on user_uuid and atype.
// This happens automatically on other DBMS backends due to replace_into(). PostgreSQL does
// not support multiple constraints on ON CONFLICT clauses.
@ -105,17 +104,17 @@ impl TwoFactor {
.map_res("Error deleting twofactor for insert")?;
diesel::insert_into(twofactor::table)
.values(&value)
.values(self)
.on_conflict(twofactor::uuid)
.do_update()
.set(&value)
.set(self)
.execute(conn)
.map_res("Error saving twofactor")
}
}
}
pub async fn delete(self, conn: &mut DbConn) -> EmptyResult {
pub async fn delete(self, conn: &DbConn) -> EmptyResult {
db_run! { conn: {
diesel::delete(twofactor::table.filter(twofactor::uuid.eq(self.uuid)))
.execute(conn)
@ -123,29 +122,27 @@ impl TwoFactor {
}}
}
pub async fn find_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<Self> {
db_run! { conn: {
twofactor::table
.filter(twofactor::user_uuid.eq(user_uuid))
.filter(twofactor::atype.lt(1000)) // Filter implementation types
.load::<TwoFactorDb>(conn)
.load::<Self>(conn)
.expect("Error loading twofactor")
.from_db()
}}
}
pub async fn find_by_user_and_type(user_uuid: &UserId, atype: i32, conn: &mut DbConn) -> Option<Self> {
pub async fn find_by_user_and_type(user_uuid: &UserId, atype: i32, conn: &DbConn) -> Option<Self> {
db_run! { conn: {
twofactor::table
.filter(twofactor::user_uuid.eq(user_uuid))
.filter(twofactor::atype.eq(atype))
.first::<TwoFactorDb>(conn)
.first::<Self>(conn)
.ok()
.from_db()
}}
}
pub async fn delete_all_by_user(user_uuid: &UserId, conn: &mut DbConn) -> EmptyResult {
pub async fn delete_all_by_user(user_uuid: &UserId, conn: &DbConn) -> EmptyResult {
db_run! { conn: {
diesel::delete(twofactor::table.filter(twofactor::user_uuid.eq(user_uuid)))
.execute(conn)
@ -153,13 +150,12 @@ impl TwoFactor {
}}
}
pub async fn migrate_u2f_to_webauthn(conn: &mut DbConn) -> EmptyResult {
pub async fn migrate_u2f_to_webauthn(conn: &DbConn) -> EmptyResult {
let u2f_factors = db_run! { conn: {
twofactor::table
.filter(twofactor::atype.eq(TwoFactorType::U2f as i32))
.load::<TwoFactorDb>(conn)
.load::<Self>(conn)
.expect("Error loading twofactor")
.from_db()
}};
use crate::api::core::two_factor::webauthn::U2FRegistration;
@ -231,13 +227,12 @@ impl TwoFactor {
Ok(())
}
pub async fn migrate_credential_to_passkey(conn: &mut DbConn) -> EmptyResult {
pub async fn migrate_credential_to_passkey(conn: &DbConn) -> EmptyResult {
let webauthn_factors = db_run! { conn: {
twofactor::table
.filter(twofactor::atype.eq(TwoFactorType::Webauthn as i32))
.load::<TwoFactorDb>(conn)
.load::<Self>(conn)
.expect("Error loading twofactor")
.from_db()
}};
for webauthn_factor in webauthn_factors {

100
src/db/models/two_factor_duo_context.rs

@ -1,33 +1,30 @@
use chrono::Utc;
use crate::db::schema::twofactor_duo_ctx;
use crate::{api::EmptyResult, db::DbConn, error::MapResult};
use diesel::prelude::*;
db_object! {
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = twofactor_duo_ctx)]
#[diesel(primary_key(state))]
pub struct TwoFactorDuoContext {
pub state: String,
pub user_email: String,
pub nonce: String,
pub exp: i64,
}
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = twofactor_duo_ctx)]
#[diesel(primary_key(state))]
pub struct TwoFactorDuoContext {
pub state: String,
pub user_email: String,
pub nonce: String,
pub exp: i64,
}
impl TwoFactorDuoContext {
pub async fn find_by_state(state: &str, conn: &mut DbConn) -> Option<Self> {
db_run! {
conn: {
twofactor_duo_ctx::table
.filter(twofactor_duo_ctx::state.eq(state))
.first::<TwoFactorDuoContextDb>(conn)
.ok()
.from_db()
}
}
pub async fn find_by_state(state: &str, conn: &DbConn) -> Option<Self> {
db_run! { conn: {
twofactor_duo_ctx::table
.filter(twofactor_duo_ctx::state.eq(state))
.first::<Self>(conn)
.ok()
}}
}
pub async fn save(state: &str, user_email: &str, nonce: &str, ttl: i64, conn: &mut DbConn) -> EmptyResult {
pub async fn save(state: &str, user_email: &str, nonce: &str, ttl: i64, conn: &DbConn) -> EmptyResult {
// A saved context should never be changed, only created or deleted.
let exists = Self::find_by_state(state, conn).await;
if exists.is_some() {
@ -36,47 +33,40 @@ impl TwoFactorDuoContext {
let exp = Utc::now().timestamp() + ttl;
db_run! {
conn: {
diesel::insert_into(twofactor_duo_ctx::table)
.values((
twofactor_duo_ctx::state.eq(state),
twofactor_duo_ctx::user_email.eq(user_email),
twofactor_duo_ctx::nonce.eq(nonce),
twofactor_duo_ctx::exp.eq(exp)
))
.execute(conn)
.map_res("Error saving context to twofactor_duo_ctx")
}
}
db_run! { conn: {
diesel::insert_into(twofactor_duo_ctx::table)
.values((
twofactor_duo_ctx::state.eq(state),
twofactor_duo_ctx::user_email.eq(user_email),
twofactor_duo_ctx::nonce.eq(nonce),
twofactor_duo_ctx::exp.eq(exp)
))
.execute(conn)
.map_res("Error saving context to twofactor_duo_ctx")
}}
}
pub async fn find_expired(conn: &mut DbConn) -> Vec<Self> {
pub async fn find_expired(conn: &DbConn) -> Vec<Self> {
let now = Utc::now().timestamp();
db_run! {
conn: {
twofactor_duo_ctx::table
.filter(twofactor_duo_ctx::exp.lt(now))
.load::<TwoFactorDuoContextDb>(conn)
.expect("Error finding expired contexts in twofactor_duo_ctx")
.from_db()
}
}
db_run! { conn: {
twofactor_duo_ctx::table
.filter(twofactor_duo_ctx::exp.lt(now))
.load::<Self>(conn)
.expect("Error finding expired contexts in twofactor_duo_ctx")
}}
}
pub async fn delete(&self, conn: &mut DbConn) -> EmptyResult {
db_run! {
conn: {
diesel::delete(
twofactor_duo_ctx::table
.filter(twofactor_duo_ctx::state.eq(&self.state)))
.execute(conn)
.map_res("Error deleting from twofactor_duo_ctx")
}
}
pub async fn delete(&self, conn: &DbConn) -> EmptyResult {
db_run! { conn: {
diesel::delete(
twofactor_duo_ctx::table
.filter(twofactor_duo_ctx::state.eq(&self.state)))
.execute(conn)
.map_res("Error deleting from twofactor_duo_ctx")
}}
}
pub async fn purge_expired_duo_contexts(conn: &mut DbConn) {
pub async fn purge_expired_duo_contexts(conn: &DbConn) {
for context in Self::find_expired(conn).await {
context.delete(conn).await.ok();
}

60
src/db/models/two_factor_incomplete.rs

@ -1,5 +1,6 @@
use chrono::{NaiveDateTime, Utc};
use crate::db::schema::twofactor_incomplete;
use crate::{
api::EmptyResult,
auth::ClientIp,
@ -10,22 +11,21 @@ use crate::{
error::MapResult,
CONFIG,
};
use diesel::prelude::*;
db_object! {
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = twofactor_incomplete)]
#[diesel(primary_key(user_uuid, device_uuid))]
pub struct TwoFactorIncomplete {
pub user_uuid: UserId,
// This device UUID is simply what's claimed by the device. It doesn't
// necessarily correspond to any UUID in the devices table, since a device
// must complete 2FA login before being added into the devices table.
pub device_uuid: DeviceId,
pub device_name: String,
pub device_type: i32,
pub login_time: NaiveDateTime,
pub ip_address: String,
}
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = twofactor_incomplete)]
#[diesel(primary_key(user_uuid, device_uuid))]
pub struct TwoFactorIncomplete {
pub user_uuid: UserId,
// This device UUID is simply what's claimed by the device. It doesn't
// necessarily correspond to any UUID in the devices table, since a device
// must complete 2FA login before being added into the devices table.
pub device_uuid: DeviceId,
pub device_name: String,
pub device_type: i32,
pub login_time: NaiveDateTime,
pub ip_address: String,
}
impl TwoFactorIncomplete {
@ -35,7 +35,7 @@ impl TwoFactorIncomplete {
device_name: &str,
device_type: i32,
ip: &ClientIp,
conn: &mut DbConn,
conn: &DbConn,
) -> EmptyResult {
if CONFIG.incomplete_2fa_time_limit() <= 0 || !CONFIG.mail_enabled() {
return Ok(());
@ -64,7 +64,7 @@ impl TwoFactorIncomplete {
}}
}
pub async fn mark_complete(user_uuid: &UserId, device_uuid: &DeviceId, conn: &mut DbConn) -> EmptyResult {
pub async fn mark_complete(user_uuid: &UserId, device_uuid: &DeviceId, conn: &DbConn) -> EmptyResult {
if CONFIG.incomplete_2fa_time_limit() <= 0 || !CONFIG.mail_enabled() {
return Ok(());
}
@ -72,40 +72,30 @@ impl TwoFactorIncomplete {
Self::delete_by_user_and_device(user_uuid, device_uuid, conn).await
}
pub async fn find_by_user_and_device(
user_uuid: &UserId,
device_uuid: &DeviceId,
conn: &mut DbConn,
) -> Option<Self> {
pub async fn find_by_user_and_device(user_uuid: &UserId, device_uuid: &DeviceId, conn: &DbConn) -> Option<Self> {
db_run! { conn: {
twofactor_incomplete::table
.filter(twofactor_incomplete::user_uuid.eq(user_uuid))
.filter(twofactor_incomplete::device_uuid.eq(device_uuid))
.first::<TwoFactorIncompleteDb>(conn)
.first::<Self>(conn)
.ok()
.from_db()
}}
}
pub async fn find_logins_before(dt: &NaiveDateTime, conn: &mut DbConn) -> Vec<Self> {
db_run! {conn: {
pub async fn find_logins_before(dt: &NaiveDateTime, conn: &DbConn) -> Vec<Self> {
db_run! { conn: {
twofactor_incomplete::table
.filter(twofactor_incomplete::login_time.lt(dt))
.load::<TwoFactorIncompleteDb>(conn)
.load::<Self>(conn)
.expect("Error loading twofactor_incomplete")
.from_db()
}}
}
pub async fn delete(self, conn: &mut DbConn) -> EmptyResult {
pub async fn delete(self, conn: &DbConn) -> EmptyResult {
Self::delete_by_user_and_device(&self.user_uuid, &self.device_uuid, conn).await
}
pub async fn delete_by_user_and_device(
user_uuid: &UserId,
device_uuid: &DeviceId,
conn: &mut DbConn,
) -> EmptyResult {
pub async fn delete_by_user_and_device(user_uuid: &UserId, device_uuid: &DeviceId, conn: &DbConn) -> EmptyResult {
db_run! { conn: {
diesel::delete(twofactor_incomplete::table
.filter(twofactor_incomplete::user_uuid.eq(user_uuid))
@ -115,7 +105,7 @@ impl TwoFactorIncomplete {
}}
}
pub async fn delete_all_by_user(user_uuid: &UserId, conn: &mut DbConn) -> EmptyResult {
pub async fn delete_all_by_user(user_uuid: &UserId, conn: &DbConn) -> EmptyResult {
db_run! { conn: {
diesel::delete(twofactor_incomplete::table.filter(twofactor_incomplete::user_uuid.eq(user_uuid)))
.execute(conn)

238
src/db/models/user.rs

@ -1,5 +1,7 @@
use crate::db::schema::{devices, invitations, sso_users, users};
use chrono::{NaiveDateTime, TimeDelta, Utc};
use derive_more::{AsRef, Deref, Display, From};
use diesel::prelude::*;
use serde_json::Value;
use super::{
@ -17,70 +19,68 @@ use crate::{
};
use macros::UuidFromParam;
db_object! {
#[derive(Identifiable, Queryable, Insertable, AsChangeset, Selectable)]
#[diesel(table_name = users)]
#[diesel(treat_none_as_null = true)]
#[diesel(primary_key(uuid))]
pub struct User {
pub uuid: UserId,
pub enabled: bool,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
pub verified_at: Option<NaiveDateTime>,
pub last_verifying_at: Option<NaiveDateTime>,
pub login_verify_count: i32,
pub email: String,
pub email_new: Option<String>,
pub email_new_token: Option<String>,
pub name: String,
pub password_hash: Vec<u8>,
pub salt: Vec<u8>,
pub password_iterations: i32,
pub password_hint: Option<String>,
pub akey: String,
pub private_key: Option<String>,
pub public_key: Option<String>,
#[diesel(column_name = "totp_secret")] // Note, this is only added to the UserDb structs, not to User
_totp_secret: Option<String>,
pub totp_recover: Option<String>,
pub security_stamp: String,
pub stamp_exception: Option<String>,
pub equivalent_domains: String,
pub excluded_globals: String,
pub client_kdf_type: i32,
pub client_kdf_iter: i32,
pub client_kdf_memory: Option<i32>,
pub client_kdf_parallelism: Option<i32>,
pub api_key: Option<String>,
pub avatar_color: Option<String>,
pub external_id: Option<String>, // Todo: Needs to be removed in the future, this is not used anymore.
}
#[derive(Identifiable, Queryable, Insertable, AsChangeset, Selectable)]
#[diesel(table_name = users)]
#[diesel(treat_none_as_null = true)]
#[diesel(primary_key(uuid))]
pub struct User {
pub uuid: UserId,
pub enabled: bool,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
pub verified_at: Option<NaiveDateTime>,
pub last_verifying_at: Option<NaiveDateTime>,
pub login_verify_count: i32,
pub email: String,
pub email_new: Option<String>,
pub email_new_token: Option<String>,
pub name: String,
pub password_hash: Vec<u8>,
pub salt: Vec<u8>,
pub password_iterations: i32,
pub password_hint: Option<String>,
pub akey: String,
pub private_key: Option<String>,
pub public_key: Option<String>,
#[diesel(column_name = "totp_secret")] // Note, this is only added to the UserDb structs, not to User
_totp_secret: Option<String>,
pub totp_recover: Option<String>,
#[derive(Identifiable, Queryable, Insertable)]
#[diesel(table_name = invitations)]
#[diesel(primary_key(email))]
pub struct Invitation {
pub email: String,
}
pub security_stamp: String,
pub stamp_exception: Option<String>,
#[derive(Identifiable, Queryable, Insertable, Selectable)]
#[diesel(table_name = sso_users)]
#[diesel(primary_key(user_uuid))]
pub struct SsoUser {
pub user_uuid: UserId,
pub identifier: OIDCIdentifier,
}
pub equivalent_domains: String,
pub excluded_globals: String,
pub client_kdf_type: i32,
pub client_kdf_iter: i32,
pub client_kdf_memory: Option<i32>,
pub client_kdf_parallelism: Option<i32>,
pub api_key: Option<String>,
pub avatar_color: Option<String>,
pub external_id: Option<String>, // Todo: Needs to be removed in the future, this is not used anymore.
}
#[derive(Identifiable, Queryable, Insertable)]
#[diesel(table_name = invitations)]
#[diesel(primary_key(email))]
pub struct Invitation {
pub email: String,
}
#[derive(Identifiable, Queryable, Insertable, Selectable)]
#[diesel(table_name = sso_users)]
#[diesel(primary_key(user_uuid))]
pub struct SsoUser {
pub user_uuid: UserId,
pub identifier: OIDCIdentifier,
}
pub enum UserKdfType {
@ -106,7 +106,7 @@ impl User {
pub const CLIENT_KDF_TYPE_DEFAULT: i32 = UserKdfType::Pbkdf2 as i32;
pub const CLIENT_KDF_ITER_DEFAULT: i32 = 600_000;
pub fn new(email: String, name: Option<String>) -> Self {
pub fn new(email: &str, name: Option<String>) -> Self {
let now = Utc::now().naive_utc();
let email = email.to_lowercase();
@ -236,7 +236,7 @@ impl User {
/// Database methods
impl User {
pub async fn to_json(&self, conn: &mut DbConn) -> Value {
pub async fn to_json(&self, conn: &DbConn) -> Value {
let mut orgs_json = Vec::new();
for c in Membership::find_confirmed_by_user(&self.uuid, conn).await {
orgs_json.push(c.to_json(conn).await);
@ -275,38 +275,36 @@ impl User {
})
}
pub async fn save(&mut self, conn: &mut DbConn) -> EmptyResult {
pub async fn save(&mut self, conn: &DbConn) -> EmptyResult {
if !crate::util::is_valid_email(&self.email) {
err!(format!("User email {} is not a valid email address", self.email))
}
self.updated_at = Utc::now().naive_utc();
db_run! {conn:
db_run! { conn:
mysql {
let value = UserDb::to_db(self);
diesel::insert_into(users::table)
.values(&value)
.values(&*self)
.on_conflict(diesel::dsl::DuplicatedKeys)
.do_update()
.set(&value)
.set(&*self)
.execute(conn)
.map_res("Error saving user")
}
postgresql, sqlite {
let value = UserDb::to_db(self);
diesel::insert_into(users::table) // Insert or update
.values(&value)
.values(&*self)
.on_conflict(users::uuid)
.do_update()
.set(&value)
.set(&*self)
.execute(conn)
.map_res("Error saving user")
}
}
}
pub async fn delete(self, conn: &mut DbConn) -> EmptyResult {
pub async fn delete(self, conn: &DbConn) -> EmptyResult {
for member in Membership::find_confirmed_by_user(&self.uuid, conn).await {
if member.atype == MembershipType::Owner
&& Membership::count_confirmed_by_org_and_type(&member.org_uuid, MembershipType::Owner, conn).await <= 1
@ -327,23 +325,23 @@ impl User {
TwoFactorIncomplete::delete_all_by_user(&self.uuid, conn).await?;
Invitation::take(&self.email, conn).await; // Delete invitation if any
db_run! {conn: {
db_run! { conn: {
diesel::delete(users::table.filter(users::uuid.eq(self.uuid)))
.execute(conn)
.map_res("Error deleting user")
}}
}
pub async fn update_uuid_revision(uuid: &UserId, conn: &mut DbConn) {
pub async fn update_uuid_revision(uuid: &UserId, conn: &DbConn) {
if let Err(e) = Self::_update_revision(uuid, &Utc::now().naive_utc(), conn).await {
warn!("Failed to update revision for {uuid}: {e:#?}");
}
}
pub async fn update_all_revisions(conn: &mut DbConn) -> EmptyResult {
pub async fn update_all_revisions(conn: &DbConn) -> EmptyResult {
let updated_at = Utc::now().naive_utc();
db_run! {conn: {
db_run! { conn: {
retry(|| {
diesel::update(users::table)
.set(users::updated_at.eq(updated_at))
@ -353,14 +351,14 @@ impl User {
}}
}
pub async fn update_revision(&mut self, conn: &mut DbConn) -> EmptyResult {
pub async fn update_revision(&mut self, conn: &DbConn) -> EmptyResult {
self.updated_at = Utc::now().naive_utc();
Self::_update_revision(&self.uuid, &self.updated_at, conn).await
}
async fn _update_revision(uuid: &UserId, date: &NaiveDateTime, conn: &mut DbConn) -> EmptyResult {
db_run! {conn: {
async fn _update_revision(uuid: &UserId, date: &NaiveDateTime, conn: &DbConn) -> EmptyResult {
db_run! { conn: {
retry(|| {
diesel::update(users::table.filter(users::uuid.eq(uuid)))
.set(users::updated_at.eq(date))
@ -370,49 +368,49 @@ impl User {
}}
}
pub async fn find_by_mail(mail: &str, conn: &mut DbConn) -> Option<Self> {
pub async fn find_by_mail(mail: &str, conn: &DbConn) -> Option<Self> {
let lower_mail = mail.to_lowercase();
db_run! {conn: {
db_run! { conn: {
users::table
.filter(users::email.eq(lower_mail))
.first::<UserDb>(conn)
.first::<Self>(conn)
.ok()
.from_db()
}}
}
pub async fn find_by_uuid(uuid: &UserId, conn: &mut DbConn) -> Option<Self> {
db_run! {conn: {
users::table.filter(users::uuid.eq(uuid)).first::<UserDb>(conn).ok().from_db()
pub async fn find_by_uuid(uuid: &UserId, conn: &DbConn) -> Option<Self> {
db_run! { conn: {
users::table
.filter(users::uuid.eq(uuid))
.first::<Self>(conn)
.ok()
}}
}
pub async fn find_by_device_id(device_uuid: &DeviceId, conn: &mut DbConn) -> Option<Self> {
pub async fn find_by_device_id(device_uuid: &DeviceId, conn: &DbConn) -> Option<Self> {
db_run! { conn: {
users::table
.inner_join(devices::table.on(devices::user_uuid.eq(users::uuid)))
.filter(devices::uuid.eq(device_uuid))
.select(users::all_columns)
.first::<UserDb>(conn)
.first::<Self>(conn)
.ok()
.from_db()
}}
}
pub async fn get_all(conn: &mut DbConn) -> Vec<(User, Option<SsoUser>)> {
db_run! {conn: {
pub async fn get_all(conn: &DbConn) -> Vec<(Self, Option<SsoUser>)> {
db_run! { conn: {
users::table
.left_join(sso_users::table)
.select(<(UserDb, Option<SsoUserDb>)>::as_select())
.select(<(Self, Option<SsoUser>)>::as_select())
.load(conn)
.expect("Error loading groups for user")
.into_iter()
.map(|(user, sso_user)| { (user.from_db(), sso_user.from_db()) })
.collect()
}}
}
pub async fn last_active(&self, conn: &mut DbConn) -> Option<NaiveDateTime> {
pub async fn last_active(&self, conn: &DbConn) -> Option<NaiveDateTime> {
match Device::find_latest_active_by_user(&self.uuid, conn).await {
Some(device) => Some(device.updated_at),
None => None,
@ -428,23 +426,23 @@ impl Invitation {
}
}
pub async fn save(&self, conn: &mut DbConn) -> EmptyResult {
pub async fn save(&self, conn: &DbConn) -> EmptyResult {
if !crate::util::is_valid_email(&self.email) {
err!(format!("Invitation email {} is not a valid email address", self.email))
}
db_run! {conn:
db_run! { conn:
sqlite, mysql {
// Not checking for ForeignKey Constraints here
// Table invitations does not have any ForeignKey Constraints.
diesel::replace_into(invitations::table)
.values(InvitationDb::to_db(self))
.values(self)
.execute(conn)
.map_res("Error saving invitation")
}
postgresql {
diesel::insert_into(invitations::table)
.values(InvitationDb::to_db(self))
.values(self)
.on_conflict(invitations::email)
.do_nothing()
.execute(conn)
@ -453,26 +451,25 @@ impl Invitation {
}
}
pub async fn delete(self, conn: &mut DbConn) -> EmptyResult {
db_run! {conn: {
pub async fn delete(self, conn: &DbConn) -> EmptyResult {
db_run! { conn: {
diesel::delete(invitations::table.filter(invitations::email.eq(self.email)))
.execute(conn)
.map_res("Error deleting invitation")
}}
}
pub async fn find_by_mail(mail: &str, conn: &mut DbConn) -> Option<Self> {
pub async fn find_by_mail(mail: &str, conn: &DbConn) -> Option<Self> {
let lower_mail = mail.to_lowercase();
db_run! {conn: {
db_run! { conn: {
invitations::table
.filter(invitations::email.eq(lower_mail))
.first::<InvitationDb>(conn)
.first::<Self>(conn)
.ok()
.from_db()
}}
}
pub async fn take(mail: &str, conn: &mut DbConn) -> bool {
pub async fn take(mail: &str, conn: &DbConn) -> bool {
match Self::find_by_mail(mail, conn).await {
Some(invitation) => invitation.delete(conn).await.is_ok(),
None => false,
@ -501,52 +498,49 @@ impl Invitation {
pub struct UserId(String);
impl SsoUser {
pub async fn save(&self, conn: &mut DbConn) -> EmptyResult {
pub async fn save(&self, conn: &DbConn) -> EmptyResult {
db_run! { conn:
sqlite, mysql {
diesel::replace_into(sso_users::table)
.values(SsoUserDb::to_db(self))
.values(self)
.execute(conn)
.map_res("Error saving SSO user")
}
postgresql {
let value = SsoUserDb::to_db(self);
diesel::insert_into(sso_users::table)
.values(&value)
.values(self)
.execute(conn)
.map_res("Error saving SSO user")
}
}
}
pub async fn find_by_identifier(identifier: &str, conn: &DbConn) -> Option<(User, SsoUser)> {
db_run! {conn: {
pub async fn find_by_identifier(identifier: &str, conn: &DbConn) -> Option<(User, Self)> {
db_run! { conn: {
users::table
.inner_join(sso_users::table)
.select(<(UserDb, SsoUserDb)>::as_select())
.select(<(User, Self)>::as_select())
.filter(sso_users::identifier.eq(identifier))
.first::<(UserDb, SsoUserDb)>(conn)
.first::<(User, Self)>(conn)
.ok()
.map(|(user, sso_user)| { (user.from_db(), sso_user.from_db()) })
}}
}
pub async fn find_by_mail(mail: &str, conn: &DbConn) -> Option<(User, Option<SsoUser>)> {
pub async fn find_by_mail(mail: &str, conn: &DbConn) -> Option<(User, Option<Self>)> {
let lower_mail = mail.to_lowercase();
db_run! {conn: {
db_run! { conn: {
users::table
.left_join(sso_users::table)
.select(<(UserDb, Option<SsoUserDb>)>::as_select())
.select(<(User, Option<Self>)>::as_select())
.filter(users::email.eq(lower_mail))
.first::<(UserDb, Option<SsoUserDb>)>(conn)
.first::<(User, Option<Self>)>(conn)
.ok()
.map(|(user, sso_user)| { (user.from_db(), sso_user.from_db()) })
}}
}
pub async fn delete(user_uuid: &UserId, conn: &mut DbConn) -> EmptyResult {
db_run! {conn: {
pub async fn delete(user_uuid: &UserId, conn: &DbConn) -> EmptyResult {
db_run! { conn: {
diesel::delete(sso_users::table.filter(sso_users::user_uuid.eq(user_uuid)))
.execute(conn)
.map_res("Error deleting sso user")

57
src/db/query_logger.rs

@ -0,0 +1,57 @@
use diesel::connection::{Instrumentation, InstrumentationEvent};
use std::{cell::RefCell, collections::HashMap, time::Instant};
thread_local! {
static QUERY_PERF_TRACKER: RefCell<HashMap<String, Instant>> = RefCell::new(HashMap::new());
}
pub fn simple_logger() -> Option<Box<dyn Instrumentation>> {
Some(Box::new(|event: InstrumentationEvent<'_>| match event {
InstrumentationEvent::StartEstablishConnection {
url,
..
} => {
debug!("Establishing connection: {url}")
}
InstrumentationEvent::FinishEstablishConnection {
url,
error,
..
} => {
if let Some(e) = error {
error!("Error during establishing a connection with {url}: {e:?}")
} else {
debug!("Connection established: {url}")
}
}
InstrumentationEvent::StartQuery {
query,
..
} => {
let query_string = format!("{query:?}");
let start = Instant::now();
QUERY_PERF_TRACKER.with_borrow_mut(|map| {
map.insert(query_string, start);
});
}
InstrumentationEvent::FinishQuery {
query,
..
} => {
let query_string = format!("{query:?}");
QUERY_PERF_TRACKER.with_borrow_mut(|map| {
if let Some(start) = map.remove(&query_string) {
let duration = start.elapsed();
if duration.as_secs() >= 5 {
warn!("SLOW QUERY [{:.2}s]: {}", duration.as_secs_f32(), query_string);
} else if duration.as_secs() >= 1 {
info!("SLOW QUERY [{:.2}s]: {}", duration.as_secs_f32(), query_string);
} else {
debug!("QUERY [{:?}]: {}", duration, query_string);
}
}
});
}
_ => {}
}))
}

0
src/db/schemas/postgresql/schema.rs → src/db/schema.rs

395
src/db/schemas/mysql/schema.rs

@ -1,395 +0,0 @@
table! {
attachments (id) {
id -> Text,
cipher_uuid -> Text,
file_name -> Text,
file_size -> BigInt,
akey -> Nullable<Text>,
}
}
table! {
ciphers (uuid) {
uuid -> Text,
created_at -> Datetime,
updated_at -> Datetime,
user_uuid -> Nullable<Text>,
organization_uuid -> Nullable<Text>,
key -> Nullable<Text>,
atype -> Integer,
name -> Text,
notes -> Nullable<Text>,
fields -> Nullable<Text>,
data -> Text,
password_history -> Nullable<Text>,
deleted_at -> Nullable<Datetime>,
reprompt -> Nullable<Integer>,
}
}
table! {
ciphers_collections (cipher_uuid, collection_uuid) {
cipher_uuid -> Text,
collection_uuid -> Text,
}
}
table! {
collections (uuid) {
uuid -> Text,
org_uuid -> Text,
name -> Text,
external_id -> Nullable<Text>,
}
}
table! {
devices (uuid, user_uuid) {
uuid -> Text,
created_at -> Datetime,
updated_at -> Datetime,
user_uuid -> Text,
name -> Text,
atype -> Integer,
push_uuid -> Nullable<Text>,
push_token -> Nullable<Text>,
refresh_token -> Text,
twofactor_remember -> Nullable<Text>,
}
}
table! {
event (uuid) {
uuid -> Varchar,
event_type -> Integer,
user_uuid -> Nullable<Varchar>,
org_uuid -> Nullable<Varchar>,
cipher_uuid -> Nullable<Varchar>,
collection_uuid -> Nullable<Varchar>,
group_uuid -> Nullable<Varchar>,
org_user_uuid -> Nullable<Varchar>,
act_user_uuid -> Nullable<Varchar>,
device_type -> Nullable<Integer>,
ip_address -> Nullable<Text>,
event_date -> Timestamp,
policy_uuid -> Nullable<Varchar>,
provider_uuid -> Nullable<Varchar>,
provider_user_uuid -> Nullable<Varchar>,
provider_org_uuid -> Nullable<Varchar>,
}
}
table! {
favorites (user_uuid, cipher_uuid) {
user_uuid -> Text,
cipher_uuid -> Text,
}
}
table! {
folders (uuid) {
uuid -> Text,
created_at -> Datetime,
updated_at -> Datetime,
user_uuid -> Text,
name -> Text,
}
}
table! {
folders_ciphers (cipher_uuid, folder_uuid) {
cipher_uuid -> Text,
folder_uuid -> Text,
}
}
table! {
invitations (email) {
email -> Text,
}
}
table! {
org_policies (uuid) {
uuid -> Text,
org_uuid -> Text,
atype -> Integer,
enabled -> Bool,
data -> Text,
}
}
table! {
organizations (uuid) {
uuid -> Text,
name -> Text,
billing_email -> Text,
private_key -> Nullable<Text>,
public_key -> Nullable<Text>,
}
}
table! {
sends (uuid) {
uuid -> Text,
user_uuid -> Nullable<Text>,
organization_uuid -> Nullable<Text>,
name -> Text,
notes -> Nullable<Text>,
atype -> Integer,
data -> Text,
akey -> Text,
password_hash -> Nullable<Binary>,
password_salt -> Nullable<Binary>,
password_iter -> Nullable<Integer>,
max_access_count -> Nullable<Integer>,
access_count -> Integer,
creation_date -> Datetime,
revision_date -> Datetime,
expiration_date -> Nullable<Datetime>,
deletion_date -> Datetime,
disabled -> Bool,
hide_email -> Nullable<Bool>,
}
}
table! {
twofactor (uuid) {
uuid -> Text,
user_uuid -> Text,
atype -> Integer,
enabled -> Bool,
data -> Text,
last_used -> BigInt,
}
}
table! {
twofactor_incomplete (user_uuid, device_uuid) {
user_uuid -> Text,
device_uuid -> Text,
device_name -> Text,
device_type -> Integer,
login_time -> Timestamp,
ip_address -> Text,
}
}
table! {
twofactor_duo_ctx (state) {
state -> Text,
user_email -> Text,
nonce -> Text,
exp -> BigInt,
}
}
table! {
users (uuid) {
uuid -> Text,
enabled -> Bool,
created_at -> Datetime,
updated_at -> Datetime,
verified_at -> Nullable<Datetime>,
last_verifying_at -> Nullable<Datetime>,
login_verify_count -> Integer,
email -> Text,
email_new -> Nullable<Text>,
email_new_token -> Nullable<Text>,
name -> Text,
password_hash -> Binary,
salt -> Binary,
password_iterations -> Integer,
password_hint -> Nullable<Text>,
akey -> Text,
private_key -> Nullable<Text>,
public_key -> Nullable<Text>,
totp_secret -> Nullable<Text>,
totp_recover -> Nullable<Text>,
security_stamp -> Text,
stamp_exception -> Nullable<Text>,
equivalent_domains -> Text,
excluded_globals -> Text,
client_kdf_type -> Integer,
client_kdf_iter -> Integer,
client_kdf_memory -> Nullable<Integer>,
client_kdf_parallelism -> Nullable<Integer>,
api_key -> Nullable<Text>,
avatar_color -> Nullable<Text>,
external_id -> Nullable<Text>,
}
}
table! {
users_collections (user_uuid, collection_uuid) {
user_uuid -> Text,
collection_uuid -> Text,
read_only -> Bool,
hide_passwords -> Bool,
manage -> Bool,
}
}
table! {
users_organizations (uuid) {
uuid -> Text,
user_uuid -> Text,
org_uuid -> Text,
invited_by_email -> Nullable<Text>,
access_all -> Bool,
akey -> Text,
status -> Integer,
atype -> Integer,
reset_password_key -> Nullable<Text>,
external_id -> Nullable<Text>,
}
}
table! {
organization_api_key (uuid, org_uuid) {
uuid -> Text,
org_uuid -> Text,
atype -> Integer,
api_key -> Text,
revision_date -> Timestamp,
}
}
table! {
sso_nonce (state) {
state -> Text,
nonce -> Text,
verifier -> Nullable<Text>,
redirect_uri -> Text,
created_at -> Timestamp,
}
}
table! {
sso_users (user_uuid) {
user_uuid -> Text,
identifier -> Text,
}
}
table! {
emergency_access (uuid) {
uuid -> Text,
grantor_uuid -> Text,
grantee_uuid -> Nullable<Text>,
email -> Nullable<Text>,
key_encrypted -> Nullable<Text>,
atype -> Integer,
status -> Integer,
wait_time_days -> Integer,
recovery_initiated_at -> Nullable<Timestamp>,
last_notification_at -> Nullable<Timestamp>,
updated_at -> Timestamp,
created_at -> Timestamp,
}
}
table! {
groups (uuid) {
uuid -> Text,
organizations_uuid -> Text,
name -> Text,
access_all -> Bool,
external_id -> Nullable<Text>,
creation_date -> Timestamp,
revision_date -> Timestamp,
}
}
table! {
groups_users (groups_uuid, users_organizations_uuid) {
groups_uuid -> Text,
users_organizations_uuid -> Text,
}
}
table! {
collections_groups (collections_uuid, groups_uuid) {
collections_uuid -> Text,
groups_uuid -> Text,
read_only -> Bool,
hide_passwords -> Bool,
manage -> Bool,
}
}
table! {
auth_requests (uuid) {
uuid -> Text,
user_uuid -> Text,
organization_uuid -> Nullable<Text>,
request_device_identifier -> Text,
device_type -> Integer,
request_ip -> Text,
response_device_id -> Nullable<Text>,
access_code -> Text,
public_key -> Text,
enc_key -> Nullable<Text>,
master_password_hash -> Nullable<Text>,
approved -> Nullable<Bool>,
creation_date -> Timestamp,
response_date -> Nullable<Timestamp>,
authentication_date -> Nullable<Timestamp>,
}
}
joinable!(attachments -> ciphers (cipher_uuid));
joinable!(ciphers -> organizations (organization_uuid));
joinable!(ciphers -> users (user_uuid));
joinable!(ciphers_collections -> ciphers (cipher_uuid));
joinable!(ciphers_collections -> collections (collection_uuid));
joinable!(collections -> organizations (org_uuid));
joinable!(devices -> users (user_uuid));
joinable!(folders -> users (user_uuid));
joinable!(folders_ciphers -> ciphers (cipher_uuid));
joinable!(folders_ciphers -> folders (folder_uuid));
joinable!(org_policies -> organizations (org_uuid));
joinable!(sends -> organizations (organization_uuid));
joinable!(sends -> users (user_uuid));
joinable!(twofactor -> users (user_uuid));
joinable!(users_collections -> collections (collection_uuid));
joinable!(users_collections -> users (user_uuid));
joinable!(users_organizations -> organizations (org_uuid));
joinable!(users_organizations -> users (user_uuid));
joinable!(users_organizations -> ciphers (org_uuid));
joinable!(organization_api_key -> organizations (org_uuid));
joinable!(emergency_access -> users (grantor_uuid));
joinable!(groups -> organizations (organizations_uuid));
joinable!(groups_users -> users_organizations (users_organizations_uuid));
joinable!(groups_users -> groups (groups_uuid));
joinable!(collections_groups -> collections (collections_uuid));
joinable!(collections_groups -> groups (groups_uuid));
joinable!(event -> users_organizations (uuid));
joinable!(auth_requests -> users (user_uuid));
joinable!(sso_users -> users (user_uuid));
allow_tables_to_appear_in_same_query!(
attachments,
ciphers,
ciphers_collections,
collections,
devices,
folders,
folders_ciphers,
invitations,
org_policies,
organizations,
sends,
sso_users,
twofactor,
users,
users_collections,
users_organizations,
organization_api_key,
emergency_access,
groups,
groups_users,
collections_groups,
event,
auth_requests,
);

395
src/db/schemas/sqlite/schema.rs

@ -1,395 +0,0 @@
table! {
attachments (id) {
id -> Text,
cipher_uuid -> Text,
file_name -> Text,
file_size -> BigInt,
akey -> Nullable<Text>,
}
}
table! {
ciphers (uuid) {
uuid -> Text,
created_at -> Timestamp,
updated_at -> Timestamp,
user_uuid -> Nullable<Text>,
organization_uuid -> Nullable<Text>,
key -> Nullable<Text>,
atype -> Integer,
name -> Text,
notes -> Nullable<Text>,
fields -> Nullable<Text>,
data -> Text,
password_history -> Nullable<Text>,
deleted_at -> Nullable<Timestamp>,
reprompt -> Nullable<Integer>,
}
}
table! {
ciphers_collections (cipher_uuid, collection_uuid) {
cipher_uuid -> Text,
collection_uuid -> Text,
}
}
table! {
collections (uuid) {
uuid -> Text,
org_uuid -> Text,
name -> Text,
external_id -> Nullable<Text>,
}
}
table! {
devices (uuid, user_uuid) {
uuid -> Text,
created_at -> Timestamp,
updated_at -> Timestamp,
user_uuid -> Text,
name -> Text,
atype -> Integer,
push_uuid -> Nullable<Text>,
push_token -> Nullable<Text>,
refresh_token -> Text,
twofactor_remember -> Nullable<Text>,
}
}
table! {
event (uuid) {
uuid -> Text,
event_type -> Integer,
user_uuid -> Nullable<Text>,
org_uuid -> Nullable<Text>,
cipher_uuid -> Nullable<Text>,
collection_uuid -> Nullable<Text>,
group_uuid -> Nullable<Text>,
org_user_uuid -> Nullable<Text>,
act_user_uuid -> Nullable<Text>,
device_type -> Nullable<Integer>,
ip_address -> Nullable<Text>,
event_date -> Timestamp,
policy_uuid -> Nullable<Text>,
provider_uuid -> Nullable<Text>,
provider_user_uuid -> Nullable<Text>,
provider_org_uuid -> Nullable<Text>,
}
}
table! {
favorites (user_uuid, cipher_uuid) {
user_uuid -> Text,
cipher_uuid -> Text,
}
}
table! {
folders (uuid) {
uuid -> Text,
created_at -> Timestamp,
updated_at -> Timestamp,
user_uuid -> Text,
name -> Text,
}
}
table! {
folders_ciphers (cipher_uuid, folder_uuid) {
cipher_uuid -> Text,
folder_uuid -> Text,
}
}
table! {
invitations (email) {
email -> Text,
}
}
table! {
org_policies (uuid) {
uuid -> Text,
org_uuid -> Text,
atype -> Integer,
enabled -> Bool,
data -> Text,
}
}
table! {
organizations (uuid) {
uuid -> Text,
name -> Text,
billing_email -> Text,
private_key -> Nullable<Text>,
public_key -> Nullable<Text>,
}
}
table! {
sends (uuid) {
uuid -> Text,
user_uuid -> Nullable<Text>,
organization_uuid -> Nullable<Text>,
name -> Text,
notes -> Nullable<Text>,
atype -> Integer,
data -> Text,
akey -> Text,
password_hash -> Nullable<Binary>,
password_salt -> Nullable<Binary>,
password_iter -> Nullable<Integer>,
max_access_count -> Nullable<Integer>,
access_count -> Integer,
creation_date -> Timestamp,
revision_date -> Timestamp,
expiration_date -> Nullable<Timestamp>,
deletion_date -> Timestamp,
disabled -> Bool,
hide_email -> Nullable<Bool>,
}
}
table! {
twofactor (uuid) {
uuid -> Text,
user_uuid -> Text,
atype -> Integer,
enabled -> Bool,
data -> Text,
last_used -> BigInt,
}
}
table! {
twofactor_incomplete (user_uuid, device_uuid) {
user_uuid -> Text,
device_uuid -> Text,
device_name -> Text,
device_type -> Integer,
login_time -> Timestamp,
ip_address -> Text,
}
}
table! {
twofactor_duo_ctx (state) {
state -> Text,
user_email -> Text,
nonce -> Text,
exp -> BigInt,
}
}
table! {
users (uuid) {
uuid -> Text,
enabled -> Bool,
created_at -> Timestamp,
updated_at -> Timestamp,
verified_at -> Nullable<Timestamp>,
last_verifying_at -> Nullable<Timestamp>,
login_verify_count -> Integer,
email -> Text,
email_new -> Nullable<Text>,
email_new_token -> Nullable<Text>,
name -> Text,
password_hash -> Binary,
salt -> Binary,
password_iterations -> Integer,
password_hint -> Nullable<Text>,
akey -> Text,
private_key -> Nullable<Text>,
public_key -> Nullable<Text>,
totp_secret -> Nullable<Text>,
totp_recover -> Nullable<Text>,
security_stamp -> Text,
stamp_exception -> Nullable<Text>,
equivalent_domains -> Text,
excluded_globals -> Text,
client_kdf_type -> Integer,
client_kdf_iter -> Integer,
client_kdf_memory -> Nullable<Integer>,
client_kdf_parallelism -> Nullable<Integer>,
api_key -> Nullable<Text>,
avatar_color -> Nullable<Text>,
external_id -> Nullable<Text>,
}
}
table! {
users_collections (user_uuid, collection_uuid) {
user_uuid -> Text,
collection_uuid -> Text,
read_only -> Bool,
hide_passwords -> Bool,
manage -> Bool,
}
}
table! {
users_organizations (uuid) {
uuid -> Text,
user_uuid -> Text,
org_uuid -> Text,
invited_by_email -> Nullable<Text>,
access_all -> Bool,
akey -> Text,
status -> Integer,
atype -> Integer,
reset_password_key -> Nullable<Text>,
external_id -> Nullable<Text>,
}
}
table! {
organization_api_key (uuid, org_uuid) {
uuid -> Text,
org_uuid -> Text,
atype -> Integer,
api_key -> Text,
revision_date -> Timestamp,
}
}
table! {
sso_nonce (state) {
state -> Text,
nonce -> Text,
verifier -> Nullable<Text>,
redirect_uri -> Text,
created_at -> Timestamp,
}
}
table! {
sso_users (user_uuid) {
user_uuid -> Text,
identifier -> Text,
}
}
table! {
emergency_access (uuid) {
uuid -> Text,
grantor_uuid -> Text,
grantee_uuid -> Nullable<Text>,
email -> Nullable<Text>,
key_encrypted -> Nullable<Text>,
atype -> Integer,
status -> Integer,
wait_time_days -> Integer,
recovery_initiated_at -> Nullable<Timestamp>,
last_notification_at -> Nullable<Timestamp>,
updated_at -> Timestamp,
created_at -> Timestamp,
}
}
table! {
groups (uuid) {
uuid -> Text,
organizations_uuid -> Text,
name -> Text,
access_all -> Bool,
external_id -> Nullable<Text>,
creation_date -> Timestamp,
revision_date -> Timestamp,
}
}
table! {
groups_users (groups_uuid, users_organizations_uuid) {
groups_uuid -> Text,
users_organizations_uuid -> Text,
}
}
table! {
collections_groups (collections_uuid, groups_uuid) {
collections_uuid -> Text,
groups_uuid -> Text,
read_only -> Bool,
hide_passwords -> Bool,
manage -> Bool,
}
}
table! {
auth_requests (uuid) {
uuid -> Text,
user_uuid -> Text,
organization_uuid -> Nullable<Text>,
request_device_identifier -> Text,
device_type -> Integer,
request_ip -> Text,
response_device_id -> Nullable<Text>,
access_code -> Text,
public_key -> Text,
enc_key -> Nullable<Text>,
master_password_hash -> Nullable<Text>,
approved -> Nullable<Bool>,
creation_date -> Timestamp,
response_date -> Nullable<Timestamp>,
authentication_date -> Nullable<Timestamp>,
}
}
joinable!(attachments -> ciphers (cipher_uuid));
joinable!(ciphers -> organizations (organization_uuid));
joinable!(ciphers -> users (user_uuid));
joinable!(ciphers_collections -> ciphers (cipher_uuid));
joinable!(ciphers_collections -> collections (collection_uuid));
joinable!(collections -> organizations (org_uuid));
joinable!(devices -> users (user_uuid));
joinable!(folders -> users (user_uuid));
joinable!(folders_ciphers -> ciphers (cipher_uuid));
joinable!(folders_ciphers -> folders (folder_uuid));
joinable!(org_policies -> organizations (org_uuid));
joinable!(sends -> organizations (organization_uuid));
joinable!(sends -> users (user_uuid));
joinable!(twofactor -> users (user_uuid));
joinable!(users_collections -> collections (collection_uuid));
joinable!(users_collections -> users (user_uuid));
joinable!(users_organizations -> organizations (org_uuid));
joinable!(users_organizations -> users (user_uuid));
joinable!(users_organizations -> ciphers (org_uuid));
joinable!(organization_api_key -> organizations (org_uuid));
joinable!(emergency_access -> users (grantor_uuid));
joinable!(groups -> organizations (organizations_uuid));
joinable!(groups_users -> users_organizations (users_organizations_uuid));
joinable!(groups_users -> groups (groups_uuid));
joinable!(collections_groups -> collections (collections_uuid));
joinable!(collections_groups -> groups (groups_uuid));
joinable!(event -> users_organizations (uuid));
joinable!(auth_requests -> users (user_uuid));
joinable!(sso_users -> users (user_uuid));
allow_tables_to_appear_in_same_query!(
attachments,
ciphers,
ciphers_collections,
collections,
devices,
folders,
folders_ciphers,
invitations,
org_policies,
organizations,
sends,
sso_users,
twofactor,
users,
users_collections,
users_organizations,
organization_api_key,
emergency_access,
groups,
groups_users,
collections_groups,
event,
auth_requests,
);

184
src/error.rs

@ -3,6 +3,7 @@
//
use crate::db::models::EventType;
use crate::http_client::CustomHttpClientError;
use serde::ser::{Serialize, SerializeStruct, Serializer};
use std::error::Error as StdError;
macro_rules! make_error {
@ -38,7 +39,8 @@ macro_rules! make_error {
};
}
use diesel::r2d2::PoolError as R2d2Err;
use diesel::r2d2::Error as R2d2Err;
use diesel::r2d2::PoolError as R2d2PoolErr;
use diesel::result::Error as DieselErr;
use diesel::ConnectionError as DieselConErr;
use handlebars::RenderError as HbErr;
@ -72,18 +74,19 @@ make_error! {
Empty(Empty): _no_source, _serialize,
// Used to represent err! calls
Simple(String): _no_source, _api_error,
Compact(Compact): _no_source, _api_error_small,
Compact(Compact): _no_source, _compact_api_error,
// Used in our custom http client to handle non-global IPs and blocked domains
CustomHttpClient(CustomHttpClientError): _has_source, _api_error,
// Used for special return values, like 2FA errors
Json(Value): _no_source, _serialize,
Db(DieselErr): _has_source, _api_error,
R2d2(R2d2Err): _has_source, _api_error,
Serde(SerdeErr): _has_source, _api_error,
JWt(JwtErr): _has_source, _api_error,
Handlebars(HbErr): _has_source, _api_error,
Json(Value): _no_source, _serialize,
Db(DieselErr): _has_source, _api_error,
R2d2(R2d2Err): _has_source, _api_error,
R2d2Pool(R2d2PoolErr): _has_source, _api_error,
Serde(SerdeErr): _has_source, _api_error,
JWt(JwtErr): _has_source, _api_error,
Handlebars(HbErr): _has_source, _api_error,
Io(IoErr): _has_source, _api_error,
Time(TimeErr): _has_source, _api_error,
@ -134,6 +137,10 @@ impl Error {
(usr_msg, log_msg.into()).into()
}
pub fn new_msg<M: Into<String> + Clone>(usr_msg: M) -> Self {
(usr_msg.clone(), usr_msg.into()).into()
}
pub fn empty() -> Self {
Empty {}.into()
}
@ -200,38 +207,97 @@ fn _no_source<T, S>(_: T) -> Option<S> {
None
}
fn _serialize(e: &impl serde::Serialize, _msg: &str) -> String {
fn _serialize(e: &impl Serialize, _msg: &str) -> String {
serde_json::to_string(e).unwrap()
}
/// This will serialize the default ApiErrorResponse
/// It will add the needed fields which are mostly empty or have multiple copies of the message
/// This is more efficient than having a larger struct and use the Serialize derive
/// It also prevents using `json!()` calls to create the final output
impl Serialize for ApiErrorResponse<'_> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
#[derive(serde::Serialize)]
struct ErrorModel<'a> {
message: &'a str,
object: &'static str,
}
let mut state = serializer.serialize_struct("ApiErrorResponse", 9)?;
state.serialize_field("message", self.0.message)?;
let mut validation_errors = std::collections::HashMap::with_capacity(1);
validation_errors.insert("", vec![self.0.message]);
state.serialize_field("validationErrors", &validation_errors)?;
let error_model = ErrorModel {
message: self.0.message,
object: "error",
};
state.serialize_field("errorModel", &error_model)?;
state.serialize_field("error", "")?;
state.serialize_field("error_description", "")?;
state.serialize_field("exceptionMessage", &None::<()>)?;
state.serialize_field("exceptionStackTrace", &None::<()>)?;
state.serialize_field("innerExceptionMessage", &None::<()>)?;
state.serialize_field("object", "error")?;
state.end()
}
}
/// This will serialize the smaller CompactApiErrorResponse
/// It will add the needed fields which are mostly empty
/// This is more efficient than having a larger struct and use the Serialize derive
/// It also prevents using `json!()` calls to create the final output
impl Serialize for CompactApiErrorResponse<'_> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut state = serializer.serialize_struct("CompactApiErrorResponse", 6)?;
state.serialize_field("message", self.0.message)?;
state.serialize_field("validationErrors", &None::<()>)?;
state.serialize_field("exceptionMessage", &None::<()>)?;
state.serialize_field("exceptionStackTrace", &None::<()>)?;
state.serialize_field("innerExceptionMessage", &None::<()>)?;
state.serialize_field("object", "error")?;
state.end()
}
}
/// Main API Error struct template
/// This struct which we can be used by both ApiErrorResponse and CompactApiErrorResponse
/// is small and doesn't contain unneeded empty fields. This is more memory efficient, but also less code to compile
struct ApiErrorMsg<'a> {
message: &'a str,
}
/// Default API Error response struct
/// The custom serialization adds all other needed fields
struct ApiErrorResponse<'a>(ApiErrorMsg<'a>);
/// Compact API Error response struct used for some newer error responses
/// The custom serialization adds all other needed fields
struct CompactApiErrorResponse<'a>(ApiErrorMsg<'a>);
fn _api_error(_: &impl std::any::Any, msg: &str) -> String {
let json = json!({
"message": msg,
"error": "",
"error_description": "",
"validationErrors": {"": [ msg ]},
"errorModel": {
"message": msg,
"object": "error"
},
"exceptionMessage": null,
"exceptionStackTrace": null,
"innerExceptionMessage": null,
"object": "error"
});
_serialize(&json, "")
let response = ApiErrorMsg {
message: msg,
};
serde_json::to_string(&ApiErrorResponse(response)).unwrap()
}
fn _api_error_small(_: &impl std::any::Any, msg: &str) -> String {
let json = json!({
"message": msg,
"validationErrors": null,
"exceptionMessage": null,
"exceptionStackTrace": null,
"innerExceptionMessage": null,
"object": "error"
});
_serialize(&json, "")
fn _compact_api_error(_: &impl std::any::Any, msg: &str) -> String {
let response = ApiErrorMsg {
message: msg,
};
serde_json::to_string(&CompactApiErrorResponse(response)).unwrap()
}
//
@ -262,34 +328,41 @@ impl Responder<'_, 'static> for Error {
#[macro_export]
macro_rules! err {
($kind:ident, $msg:expr) => {{
error!("{}", $msg);
return Err($crate::error::Error::new($msg, $msg).with_kind($crate::error::ErrorKind::$kind($crate::error::$kind {})));
let msg = $msg;
error!("{msg}");
return Err($crate::error::Error::new_msg(msg).with_kind($crate::error::ErrorKind::$kind($crate::error::$kind {})));
}};
($msg:expr) => {{
error!("{}", $msg);
return Err($crate::error::Error::new($msg, $msg));
let msg = $msg;
error!("{msg}");
return Err($crate::error::Error::new_msg(msg));
}};
($msg:expr, ErrorEvent $err_event:tt) => {{
error!("{}", $msg);
return Err($crate::error::Error::new($msg, $msg).with_event($crate::error::ErrorEvent $err_event));
let msg = $msg;
error!("{msg}");
return Err($crate::error::Error::new_msg(msg).with_event($crate::error::ErrorEvent $err_event));
}};
($usr_msg:expr, $log_value:expr) => {{
error!("{}. {}", $usr_msg, $log_value);
return Err($crate::error::Error::new($usr_msg, $log_value));
let usr_msg = $usr_msg;
let log_value = $log_value;
error!("{usr_msg}. {log_value}");
return Err($crate::error::Error::new(usr_msg, log_value));
}};
($usr_msg:expr, $log_value:expr, ErrorEvent $err_event:tt) => {{
error!("{}. {}", $usr_msg, $log_value);
return Err($crate::error::Error::new($usr_msg, $log_value).with_event($crate::error::ErrorEvent $err_event));
let usr_msg = $usr_msg;
let log_value = $log_value;
error!("{usr_msg}. {log_value}");
return Err($crate::error::Error::new(usr_msg, log_value).with_event($crate::error::ErrorEvent $err_event));
}};
}
#[macro_export]
macro_rules! err_silent {
($msg:expr) => {{
return Err($crate::error::Error::new($msg, $msg));
return Err($crate::error::Error::new_msg($msg));
}};
($msg:expr, ErrorEvent $err_event:tt) => {{
return Err($crate::error::Error::new($msg, $msg).with_event($crate::error::ErrorEvent $err_event));
return Err($crate::error::Error::new_msg($msg).with_event($crate::error::ErrorEvent $err_event));
}};
($usr_msg:expr, $log_value:expr) => {{
return Err($crate::error::Error::new($usr_msg, $log_value));
@ -302,12 +375,15 @@ macro_rules! err_silent {
#[macro_export]
macro_rules! err_code {
($msg:expr, $err_code:expr) => {{
error!("{}", $msg);
return Err($crate::error::Error::new($msg, $msg).with_code($err_code));
let msg = $msg;
error!("{msg}");
return Err($crate::error::Error::new_msg(msg).with_code($err_code));
}};
($usr_msg:expr, $log_value:expr, $err_code:expr) => {{
error!("{}. {}", $usr_msg, $log_value);
return Err($crate::error::Error::new($usr_msg, $log_value).with_code($err_code));
let usr_msg = $usr_msg;
let log_value = $log_value;
error!("{usr_msg}. {log_value}");
return Err($crate::error::Error::new(usr_msg, log_value).with_code($err_code));
}};
}
@ -315,7 +391,7 @@ macro_rules! err_code {
macro_rules! err_discard {
($msg:expr, $data:expr) => {{
std::io::copy(&mut $data.open(), &mut std::io::sink()).ok();
return Err($crate::error::Error::new($msg, $msg));
return Err($crate::error::Error::new_msg($msg));
}};
($usr_msg:expr, $log_value:expr, $data:expr) => {{
std::io::copy(&mut $data.open(), &mut std::io::sink()).ok();
@ -340,7 +416,9 @@ macro_rules! err_handler {
return ::rocket::request::Outcome::Error((rocket::http::Status::Unauthorized, $expr));
}};
($usr_msg:expr, $log_value:expr) => {{
error!(target: "auth", "Unauthorized Error: {}. {}", $usr_msg, $log_value);
return ::rocket::request::Outcome::Error((rocket::http::Status::Unauthorized, $usr_msg));
let usr_msg = $usr_msg;
let log_value = $log_value;
error!(target: "auth", "Unauthorized Error: {usr_msg}. {log_value}");
return ::rocket::request::Outcome::Error((rocket::http::Status::Unauthorized, usr_msg));
}};
}

20
src/http_client.rs

@ -2,12 +2,11 @@ use std::{
fmt,
net::{IpAddr, SocketAddr},
str::FromStr,
sync::{Arc, Mutex},
sync::{Arc, LazyLock, Mutex},
time::Duration,
};
use hickory_resolver::{name_server::TokioConnectionProvider, TokioResolver};
use once_cell::sync::Lazy;
use regex::Regex;
use reqwest::{
dns::{Name, Resolve, Resolving},
@ -25,9 +24,10 @@ pub fn make_http_request(method: reqwest::Method, url: &str) -> Result<reqwest::
err!("Invalid host");
};
should_block_host(host)?;
should_block_host(&host)?;
static INSTANCE: Lazy<Client> = Lazy::new(|| get_reqwest_client_builder().build().expect("Failed to build client"));
static INSTANCE: LazyLock<Client> =
LazyLock::new(|| get_reqwest_client_builder().build().expect("Failed to build client"));
Ok(INSTANCE.request(method, url))
}
@ -45,7 +45,7 @@ pub fn get_reqwest_client_builder() -> ClientBuilder {
return attempt.error("Invalid host");
};
if let Err(e) = should_block_host(host) {
if let Err(e) = should_block_host(&host) {
return attempt.error(e);
}
@ -100,11 +100,11 @@ fn should_block_address_regex(domain_or_ip: &str) -> bool {
is_match
}
fn should_block_host(host: Host<&str>) -> Result<(), CustomHttpClientError> {
fn should_block_host(host: &Host<&str>) -> Result<(), CustomHttpClientError> {
let (ip, host_str): (Option<IpAddr>, String) = match host {
Host::Ipv4(ip) => (Some(ip.into()), ip.to_string()),
Host::Ipv6(ip) => (Some(ip.into()), ip.to_string()),
Host::Domain(d) => (None, d.to_string()),
Host::Ipv4(ip) => (Some(IpAddr::V4(*ip)), ip.to_string()),
Host::Ipv6(ip) => (Some(IpAddr::V6(*ip)), ip.to_string()),
Host::Domain(d) => (None, (*d).to_string()),
};
if let Some(ip) = ip {
@ -179,7 +179,7 @@ type BoxError = Box<dyn std::error::Error + Send + Sync>;
impl CustomDnsResolver {
fn instance() -> Arc<Self> {
static INSTANCE: Lazy<Arc<CustomDnsResolver>> = Lazy::new(CustomDnsResolver::new);
static INSTANCE: LazyLock<Arc<CustomDnsResolver>> = LazyLock::new(CustomDnsResolver::new);
Arc::clone(&*INSTANCE)
}

4
src/mail.rs

@ -303,7 +303,7 @@ pub async fn send_delete_account(address: &str, user_id: &UserId) -> EmptyResult
}
pub async fn send_verify_email(address: &str, user_id: &UserId) -> EmptyResult {
let claims = generate_verify_email_claims(user_id.clone());
let claims = generate_verify_email_claims(user_id);
let verify_email_token = encode_jwt(&claims);
let (subject, body_html, body_text) = get_text(
@ -354,7 +354,7 @@ pub async fn send_welcome(address: &str) -> EmptyResult {
}
pub async fn send_welcome_must_verify(address: &str, user_id: &UserId) -> EmptyResult {
let claims = generate_verify_email_claims(user_id.clone());
let claims = generate_verify_email_claims(user_id);
let verify_email_token = encode_jwt(&claims);
let (subject, body_html, body_text) = get_text(

77
src/main.rs

@ -1,9 +1,8 @@
#![cfg_attr(feature = "unstable", feature(ip))]
// The recursion_limit is mainly triggered by the json!() macro.
// The more key/value pairs there are the more recursion occurs.
// We want to keep this as low as possible, but not higher then 128.
// If you go above 128 it will cause rust-analyzer to fail,
#![recursion_limit = "200"]
// We want to keep this as low as possible!
#![recursion_limit = "165"]
// When enabled use MiMalloc as malloc instead of the default malloc
#[cfg(feature = "enable_mimalloc")]
@ -71,7 +70,7 @@ pub use util::is_running_in_container;
#[rocket::main]
async fn main() -> Result<(), Error> {
parse_args().await;
parse_args();
launch_info();
let level = init_logging()?;
@ -87,8 +86,8 @@ async fn main() -> Result<(), Error> {
let pool = create_db_pool().await;
schedule_jobs(pool.clone());
db::models::TwoFactor::migrate_u2f_to_webauthn(&mut pool.get().await.unwrap()).await.unwrap();
db::models::TwoFactor::migrate_credential_to_passkey(&mut pool.get().await.unwrap()).await.unwrap();
db::models::TwoFactor::migrate_u2f_to_webauthn(&pool.get().await.unwrap()).await.unwrap();
db::models::TwoFactor::migrate_credential_to_passkey(&pool.get().await.unwrap()).await.unwrap();
let extra_debug = matches!(level, log::LevelFilter::Trace | log::LevelFilter::Debug);
launch_rocket(pool, extra_debug).await // Blocks until program termination.
@ -117,7 +116,7 @@ PRESETS: m= t= p=
pub const VERSION: Option<&str> = option_env!("VW_VERSION");
async fn parse_args() {
fn parse_args() {
let mut pargs = pico_args::Arguments::from_env();
let version = VERSION.unwrap_or("(Version info from Git not present)");
@ -188,7 +187,7 @@ async fn parse_args() {
exit(1);
}
} else if command == "backup" {
match backup_sqlite().await {
match db::backup_sqlite() {
Ok(f) => {
println!("Backup to '{f}' was successful");
exit(0);
@ -203,23 +202,6 @@ async fn parse_args() {
}
}
async fn backup_sqlite() -> Result<String, Error> {
use crate::db::{backup_database, DbConnType};
if DbConnType::from_url(&CONFIG.database_url()).map(|t| t == DbConnType::sqlite).unwrap_or(false) {
// Establish a connection to the sqlite database
let mut conn = db::DbPool::from_config()
.expect("SQLite database connection failed")
.get()
.await
.expect("Unable to get SQLite db pool");
let backup_file = backup_database(&mut conn).await?;
Ok(backup_file)
} else {
err_silent!("The database type is not SQLite. Backups only works for SQLite databases")
}
}
fn launch_info() {
println!(
"\
@ -285,13 +267,6 @@ fn init_logging() -> Result<log::LevelFilter, Error> {
log::LevelFilter::Off
};
let diesel_logger_level: log::LevelFilter =
if cfg!(feature = "query_logger") && std::env::var("QUERY_LOGGER").is_ok() {
log::LevelFilter::Debug
} else {
log::LevelFilter::Off
};
// Only show Rocket underscore `_` logs when the level is Debug or higher
// Else this will bloat the log output with useless messages.
let rocket_underscore_level = if level >= log::LevelFilter::Debug {
@ -342,9 +317,15 @@ fn init_logging() -> Result<log::LevelFilter, Error> {
// Variable level for hickory used by reqwest
("hickory_resolver::name_server::name_server", hickory_level),
("hickory_proto::xfer", hickory_level),
("diesel_logger", diesel_logger_level),
// SMTP
("lettre::transport::smtp", smtp_log_level),
// Set query_logger default to Off, but can be overwritten manually
// You can set LOG_LEVEL=info,vaultwarden::db::query_logger=<LEVEL> to overwrite it.
// This makes it possible to do the following:
// warn = Print slow queries only, 5 seconds or longer
// info = Print slow queries only, 1 second or longer
// debug = Print all queries
("vaultwarden::db::query_logger", log::LevelFilter::Off),
]);
for (path, level) in levels_override.into_iter() {
@ -467,7 +448,7 @@ async fn check_data_folder() {
if data_folder.starts_with("s3://") {
if let Err(e) = CONFIG
.opendal_operator_for_path_type(PathType::Data)
.opendal_operator_for_path_type(&PathType::Data)
.unwrap_or_else(|e| {
error!("Failed to create S3 operator for data folder '{data_folder}': {e:?}");
exit(1);
@ -614,20 +595,24 @@ async fn launch_rocket(pool: db::DbPool, extra_debug: bool) -> Result<(), Error>
CONFIG.shutdown();
});
#[cfg(unix)]
#[cfg(all(unix, sqlite))]
{
tokio::spawn(async move {
let mut signal_user1 = tokio::signal::unix::signal(SignalKind::user_defined1()).unwrap();
loop {
// If we need more signals to act upon, we might want to use select! here.
// With only one item to listen for this is enough.
let _ = signal_user1.recv().await;
match backup_sqlite().await {
Ok(f) => info!("Backup to '{f}' was successful"),
Err(e) => error!("Backup failed. {e:?}"),
if db::ACTIVE_DB_TYPE.get() != Some(&db::DbConnType::Sqlite) {
debug!("PostgreSQL and MySQL/MariaDB do not support this backup feature, skip adding USR1 signal.");
} else {
tokio::spawn(async move {
let mut signal_user1 = tokio::signal::unix::signal(SignalKind::user_defined1()).unwrap();
loop {
// If we need more signals to act upon, we might want to use select! here.
// With only one item to listen for this is enough.
let _ = signal_user1.recv().await;
match db::backup_sqlite() {
Ok(f) => info!("Backup to '{f}' was successful"),
Err(e) => error!("Backup failed. {e:?}"),
}
}
}
});
});
}
}
instance.launch().await?;

7
src/ratelimit.rs

@ -1,5 +1,4 @@
use once_cell::sync::Lazy;
use std::{net::IpAddr, num::NonZeroU32, time::Duration};
use std::{net::IpAddr, num::NonZeroU32, sync::LazyLock, time::Duration};
use governor::{clock::DefaultClock, state::keyed::DashMapStateStore, Quota, RateLimiter};
@ -7,13 +6,13 @@ use crate::{Error, CONFIG};
type Limiter<T = IpAddr> = RateLimiter<T, DashMapStateStore<T>, DefaultClock>;
static LIMITER_LOGIN: Lazy<Limiter> = Lazy::new(|| {
static LIMITER_LOGIN: LazyLock<Limiter> = LazyLock::new(|| {
let seconds = Duration::from_secs(CONFIG.login_ratelimit_seconds());
let burst = NonZeroU32::new(CONFIG.login_ratelimit_max_burst()).expect("Non-zero login ratelimit burst");
RateLimiter::keyed(Quota::with_period(seconds).expect("Non-zero login ratelimit seconds").allow_burst(burst))
});
static LIMITER_ADMIN: Lazy<Limiter> = Lazy::new(|| {
static LIMITER_ADMIN: LazyLock<Limiter> = LazyLock::new(|| {
let seconds = Duration::from_secs(CONFIG.admin_ratelimit_seconds());
let burst = NonZeroU32::new(CONFIG.admin_ratelimit_max_burst()).expect("Non-zero admin ratelimit burst");
RateLimiter::keyed(Quota::with_period(seconds).expect("Non-zero admin ratelimit seconds").allow_burst(burst))

32
src/sso.rs

@ -1,12 +1,11 @@
use std::{sync::LazyLock, time::Duration};
use chrono::Utc;
use derive_more::{AsRef, Deref, Display, From};
use mini_moka::sync::Cache;
use regex::Regex;
use std::time::Duration;
use url::Url;
use mini_moka::sync::Cache;
use once_cell::sync::Lazy;
use crate::{
api::ApiResult,
auth,
@ -21,12 +20,12 @@ use crate::{
pub static FAKE_IDENTIFIER: &str = "VW_DUMMY_IDENTIFIER_FOR_OIDC";
static AC_CACHE: Lazy<Cache<OIDCState, AuthenticatedUser>> =
Lazy::new(|| Cache::builder().max_capacity(1000).time_to_live(Duration::from_secs(10 * 60)).build());
static AC_CACHE: LazyLock<Cache<OIDCState, AuthenticatedUser>> =
LazyLock::new(|| Cache::builder().max_capacity(1000).time_to_live(Duration::from_secs(10 * 60)).build());
static SSO_JWT_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|sso", CONFIG.domain_origin()));
static SSO_JWT_ISSUER: LazyLock<String> = LazyLock::new(|| format!("{}|sso", CONFIG.domain_origin()));
pub static NONCE_EXPIRATION: Lazy<chrono::Duration> = Lazy::new(|| chrono::TimeDelta::try_minutes(10).unwrap());
pub static NONCE_EXPIRATION: LazyLock<chrono::Duration> = LazyLock::new(|| chrono::TimeDelta::try_minutes(10).unwrap());
#[derive(
Clone,
@ -151,7 +150,7 @@ fn decode_token_claims(token_name: &str, token: &str) -> ApiResult<BasicTokenCla
}
}
pub fn decode_state(base64_state: String) -> ApiResult<OIDCState> {
pub fn decode_state(base64_state: &str) -> ApiResult<OIDCState> {
let state = match data_encoding::BASE64.decode(base64_state.as_bytes()) {
Ok(vec) => match String::from_utf8(vec) {
Ok(valid) => OIDCState(valid),
@ -165,12 +164,7 @@ pub fn decode_state(base64_state: String) -> ApiResult<OIDCState> {
// The `nonce` allow to protect against replay attacks
// redirect_uri from: https://github.com/bitwarden/server/blob/main/src/Identity/IdentityServer/ApiClient.cs
pub async fn authorize_url(
state: OIDCState,
client_id: &str,
raw_redirect_uri: &str,
mut conn: DbConn,
) -> ApiResult<Url> {
pub async fn authorize_url(state: OIDCState, client_id: &str, raw_redirect_uri: &str, conn: DbConn) -> ApiResult<Url> {
let redirect_uri = match client_id {
"web" | "browser" => format!("{}/sso-connector.html", CONFIG.domain()),
"desktop" | "mobile" => "bitwarden://sso-callback".to_string(),
@ -185,7 +179,7 @@ pub async fn authorize_url(
};
let (auth_url, nonce) = Client::authorize_url(state, redirect_uri).await?;
nonce.save(&mut conn).await?;
nonce.save(&conn).await?;
Ok(auth_url)
}
@ -235,7 +229,7 @@ pub struct UserInformation {
pub user_name: Option<String>,
}
async fn decode_code_claims(code: &str, conn: &mut DbConn) -> ApiResult<(OIDCCode, OIDCState)> {
async fn decode_code_claims(code: &str, conn: &DbConn) -> ApiResult<(OIDCCode, OIDCState)> {
match auth::decode_jwt::<OIDCCodeClaims>(code, SSO_JWT_ISSUER.to_string()) {
Ok(code_claims) => match code_claims.code {
OIDCCodeWrapper::Ok {
@ -265,7 +259,7 @@ async fn decode_code_claims(code: &str, conn: &mut DbConn) -> ApiResult<(OIDCCod
// - second time we will rely on the `AC_CACHE` since the `code` has already been exchanged.
// The `nonce` will ensure that the user is authorized only once.
// We return only the `UserInformation` to force calling `redeem` to obtain the `refresh_token`.
pub async fn exchange_code(wrapped_code: &str, conn: &mut DbConn) -> ApiResult<UserInformation> {
pub async fn exchange_code(wrapped_code: &str, conn: &DbConn) -> ApiResult<UserInformation> {
use openidconnect::OAuth2TokenResponse;
let (code, state) = decode_code_claims(wrapped_code, conn).await?;
@ -330,7 +324,7 @@ pub async fn exchange_code(wrapped_code: &str, conn: &mut DbConn) -> ApiResult<U
}
// User has passed 2FA flow we can delete `nonce` and clear the cache.
pub async fn redeem(state: &OIDCState, conn: &mut DbConn) -> ApiResult<AuthenticatedUser> {
pub async fn redeem(state: &OIDCState, conn: &DbConn) -> ApiResult<AuthenticatedUser> {
if let Err(err) = SsoNonce::delete(state, conn).await {
error!("Failed to delete database sso_nonce using {state}: {err}")
}

18
src/sso_client.rs

@ -1,13 +1,9 @@
use regex::Regex;
use std::borrow::Cow;
use std::time::Duration;
use url::Url;
use std::{borrow::Cow, sync::LazyLock, time::Duration};
use mini_moka::sync::Cache;
use once_cell::sync::Lazy;
use openidconnect::core::*;
use openidconnect::reqwest;
use openidconnect::*;
use openidconnect::{core::*, reqwest, *};
use regex::Regex;
use url::Url;
use crate::{
api::{ApiResult, EmptyResult},
@ -16,8 +12,8 @@ use crate::{
CONFIG,
};
static CLIENT_CACHE_KEY: Lazy<String> = Lazy::new(|| "sso-client".to_string());
static CLIENT_CACHE: Lazy<Cache<String, Client>> = Lazy::new(|| {
static CLIENT_CACHE_KEY: LazyLock<String> = LazyLock::new(|| "sso-client".to_string());
static CLIENT_CACHE: LazyLock<Cache<String, Client>> = LazyLock::new(|| {
Cache::builder().max_capacity(1).time_to_live(Duration::from_secs(CONFIG.sso_client_cache_expiration())).build()
});
@ -162,7 +158,7 @@ impl Client {
if CONFIG.sso_pkce() {
match nonce.verifier {
None => err!(format!("Missing verifier in the DB nonce table")),
Some(secret) => exchange = exchange.set_pkce_verifier(PkceCodeVerifier::new(secret.clone())),
Some(secret) => exchange = exchange.set_pkce_verifier(PkceCodeVerifier::new(secret)),
}
}

2
src/util.rs

@ -842,7 +842,7 @@ pub fn is_global(ip: std::net::IpAddr) -> bool {
/// Saves a Rocket temporary file to the OpenDAL Operator at the given path.
pub async fn save_temp_file(
path_type: PathType,
path_type: &PathType,
path: &str,
temp_file: rocket::fs::TempFile<'_>,
overwrite: bool,

Loading…
Cancel
Save