mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2025-11-21 20:34:09 +02:00
Compare commits
71 Commits
1.32.6
...
bf04c64759
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf04c64759 | ||
|
|
2f1d86b7f1 | ||
|
|
ff97bcfdda | ||
|
|
73f2441d1a | ||
|
|
ad8484a2d5 | ||
|
|
9813e480c0 | ||
|
|
bfe172702a | ||
|
|
df42b6d6b0 | ||
|
|
2697fe8aba | ||
|
|
674e444d67 | ||
|
|
0d16da440d | ||
|
|
66cf179bca | ||
|
|
025bb90f8f | ||
|
|
d5039d9c17 | ||
|
|
e7c796a660 | ||
|
|
bbbd2f6d15 | ||
|
|
a2d7895586 | ||
|
|
8a0cb1137e | ||
|
|
f960bf59bb | ||
|
|
3a1f1bae00 | ||
|
|
8dfe805954 | ||
|
|
07b869b3ef | ||
|
|
2a18665288 | ||
|
|
71952a4ab5 | ||
|
|
994d157064 | ||
|
|
1dae6093c9 | ||
|
|
6edceb5f7a | ||
|
|
359a4a088a | ||
|
|
3baffeee9a | ||
|
|
d5c353427d | ||
|
|
1f868b8d22 | ||
|
|
8d1df08b81 | ||
|
|
3b6bccde97 | ||
|
|
d2b36642a6 | ||
|
|
a02fb0fd24 | ||
|
|
1109293992 | ||
|
|
3c29f82974 | ||
|
|
663f88e717 | ||
|
|
a3dccee243 | ||
|
|
c0ebe0d982 | ||
|
|
1b46c80389 | ||
|
|
2c549984c0 | ||
|
|
ecab7a50ea | ||
|
|
2903a3a13a | ||
|
|
952992c85b | ||
|
|
c0be36a17f | ||
|
|
d1dee04615 | ||
|
|
ef2695de0c | ||
|
|
29f2b433f0 | ||
|
|
07f80346b4 | ||
|
|
4f68eafa3e | ||
|
|
327d369188 | ||
|
|
ca7483df85 | ||
|
|
16b6d2a71e | ||
|
|
871a3f214a | ||
|
|
10d12676cf | ||
|
|
dec3a9603a | ||
|
|
86aaf27659 | ||
|
|
bc913d1156 | ||
|
|
ef4bff09eb | ||
|
|
4816f77fd7 | ||
|
|
dfd9e65396 | ||
|
|
b1481c7c1a | ||
|
|
d9e0d68f20 | ||
|
|
08183fc999 | ||
|
|
d9b043d32c | ||
|
|
ed4ad67e73 | ||
|
|
a523c82f5f | ||
|
|
4d6d3443ae | ||
|
|
9cd400db6c | ||
|
|
fd51230044 |
@@ -5,6 +5,7 @@
|
||||
!.git
|
||||
!docker/healthcheck.sh
|
||||
!docker/start.sh
|
||||
!macros
|
||||
!migrations
|
||||
!src
|
||||
|
||||
|
||||
@@ -229,7 +229,8 @@
|
||||
# SIGNUPS_ALLOWED=true
|
||||
|
||||
## Controls if new users need to verify their email address upon registration
|
||||
## Note that setting this option to true prevents logins until the email address has been verified!
|
||||
## On new client versions, this will require the user to verify their email at signup time.
|
||||
## On older clients, it will require the user to verify their email before they can log in.
|
||||
## The welcome email will include a verification link, and login attempts will periodically
|
||||
## trigger another verification email to be sent.
|
||||
# SIGNUPS_VERIFY=false
|
||||
@@ -350,8 +351,14 @@
|
||||
## - "browser-fileless-import": Directly import credentials from other providers without a file.
|
||||
## - "extension-refresh": Temporarily enable the new extension design until general availability (should be used with the beta Chrome extension)
|
||||
## - "fido2-vault-credentials": Enable the use of FIDO2 security keys as second factor.
|
||||
## - "inline-menu-positioning-improvements": Enable the use of inline menu password generator and identity suggestions in the browser extension.
|
||||
## - "ssh-key-vault-item": Enable the creation and use of SSH key vault items. (Needs clients >=2024.12.0)
|
||||
## - "ssh-agent": Enable SSH agent support on Desktop. (Needs desktop >=2024.12.0)
|
||||
## - "anon-addy-self-host-alias": Enable configuring self-hosted Anon Addy alias generator. (Needs Android >=2025.3.0, iOS >=2025.4.0)
|
||||
## - "simple-login-self-host-alias": Enable configuring self-hosted Simple Login alias generator. (Needs Android >=2025.3.0, iOS >=2025.4.0)
|
||||
## - "mutual-tls": Enable the use of mutual TLS on Android (Client >= 2025.2.0)
|
||||
## - "export-attachments": Enable support for exporting attachments (Clients >=2025.4.0)
|
||||
## - "inline-menu-totp": Enable the use of inline menu TOTP codes in the browser extension.
|
||||
# EXPERIMENTAL_CLIENT_FEATURE_FLAGS=fido2-vault-credentials
|
||||
|
||||
## Require new device emails. When a user logs in an email is required to be sent.
|
||||
@@ -410,6 +417,14 @@
|
||||
## Multiple values must be separated with a whitespace.
|
||||
# ALLOWED_IFRAME_ANCESTORS=
|
||||
|
||||
## Allowed connect-src (Know the risks!)
|
||||
## https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/connect-src
|
||||
## Allows other domains to URLs which can be loaded using script interfaces like the Forwarded email alias feature
|
||||
## This adds the configured value to the 'Content-Security-Policy' headers 'connect-src' value.
|
||||
## Multiple values must be separated with a whitespace. And only HTTPS values are allowed.
|
||||
## Example: "https://my-addy-io.domain.tld https://my-simplelogin.domain.tld"
|
||||
# ALLOWED_CONNECT_SRC=""
|
||||
|
||||
## Number of seconds, on average, between login requests from the same IP address before rate limiting kicks in.
|
||||
# LOGIN_RATELIMIT_SECONDS=60
|
||||
## Allow a burst of requests of up to this size, while maintaining the average indicated by `LOGIN_RATELIMIT_SECONDS`.
|
||||
@@ -477,7 +492,7 @@
|
||||
## Maximum attempts before an email token is reset and a new email will need to be sent.
|
||||
# EMAIL_ATTEMPTS_LIMIT=3
|
||||
##
|
||||
## Setup email 2FA regardless of any organization policy
|
||||
## Setup email 2FA on registration regardless of any organization policy
|
||||
# EMAIL_2FA_ENFORCE_ON_VERIFIED_INVITE=false
|
||||
## Automatically setup email 2FA as fallback provider when needed
|
||||
# EMAIL_2FA_AUTO_FALLBACK=false
|
||||
|
||||
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -1,3 +1,5 @@
|
||||
/.github @dani-garcia @BlackDex
|
||||
/.github/** @dani-garcia @BlackDex
|
||||
/.github/CODEOWNERS @dani-garcia @BlackDex
|
||||
/.github/workflows/** @dani-garcia @BlackDex
|
||||
/SECURITY.md @dani-garcia @BlackDex
|
||||
|
||||
95
.github/workflows/build.yml
vendored
95
.github/workflows/build.yml
vendored
@@ -1,4 +1,5 @@
|
||||
name: Build
|
||||
permissions: {}
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -13,6 +14,7 @@ on:
|
||||
- "diesel.toml"
|
||||
- "docker/Dockerfile.j2"
|
||||
- "docker/DockerSettings.yaml"
|
||||
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/build.yml"
|
||||
@@ -28,13 +30,17 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build and Test ${{ matrix.channel }}
|
||||
permissions:
|
||||
actions: write
|
||||
contents: read
|
||||
# We use Ubuntu 22.04 here because this matches the library versions used within the Debian docker containers
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 120
|
||||
# Make warnings errors, this is to prevent warnings slipping through.
|
||||
# This is done globally to prevent rebuilds when the RUSTFLAGS env variable changes.
|
||||
env:
|
||||
RUSTFLAGS: "-D warnings"
|
||||
RUSTFLAGS: "-Dwarnings"
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -42,20 +48,19 @@ jobs:
|
||||
- "rust-toolchain" # The version defined in rust-toolchain
|
||||
- "msrv" # The supported MSRV
|
||||
|
||||
name: Build and Test ${{ matrix.channel }}
|
||||
|
||||
steps:
|
||||
# Checkout the repo
|
||||
- name: "Checkout"
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
|
||||
# End Checkout the repo
|
||||
|
||||
|
||||
# Install dependencies
|
||||
- name: "Install dependencies Ubuntu"
|
||||
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends openssl build-essential libmariadb-dev-compat libpq-dev libssl-dev pkg-config
|
||||
# End Install dependencies
|
||||
|
||||
# Checkout the repo
|
||||
- name: "Checkout"
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
# End Checkout the repo
|
||||
|
||||
# Determine rust-toolchain version
|
||||
- name: Init Variables
|
||||
@@ -75,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@315e265cd78dad1e1dcf3a5074f6d6c47029d5aa # master @ Nov 18, 2024, 5:36 AM GMT+1
|
||||
uses: dtolnay/rust-toolchain@56f84321dbccf38fb67ce29ab63e4754056677e0 # master @ Mar 18, 2025, 8:14 PM GMT+1
|
||||
if: ${{ matrix.channel == 'rust-toolchain' }}
|
||||
with:
|
||||
toolchain: "${{steps.toolchain.outputs.RUST_TOOLCHAIN}}"
|
||||
@@ -85,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@315e265cd78dad1e1dcf3a5074f6d6c47029d5aa # master @ Nov 18, 2024, 5:36 AM GMT+1
|
||||
uses: dtolnay/rust-toolchain@56f84321dbccf38fb67ce29ab63e4754056677e0 # master @ Mar 18, 2025, 8:14 PM GMT+1
|
||||
if: ${{ matrix.channel != 'rust-toolchain' }}
|
||||
with:
|
||||
toolchain: "${{steps.toolchain.outputs.RUST_TOOLCHAIN}}"
|
||||
@@ -93,11 +98,13 @@ jobs:
|
||||
|
||||
# Set the current matrix toolchain version as default
|
||||
- name: "Set toolchain ${{steps.toolchain.outputs.RUST_TOOLCHAIN}} as default"
|
||||
env:
|
||||
RUST_TOOLCHAIN: ${{steps.toolchain.outputs.RUST_TOOLCHAIN}}
|
||||
run: |
|
||||
# Remove the rust-toolchain.toml
|
||||
rm rust-toolchain.toml
|
||||
# Set the default
|
||||
rustup default ${{steps.toolchain.outputs.RUST_TOOLCHAIN}}
|
||||
rustup default "${RUST_TOOLCHAIN}"
|
||||
|
||||
# Show environment
|
||||
- name: "Show environment"
|
||||
@@ -107,7 +114,8 @@ jobs:
|
||||
# End Show environment
|
||||
|
||||
# Enable Rust Caching
|
||||
- uses: Swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2.7.5
|
||||
- name: Rust Caching
|
||||
uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
|
||||
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.
|
||||
@@ -119,37 +127,37 @@ jobs:
|
||||
# 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
|
||||
if: $${{ always() }}
|
||||
if: ${{ !cancelled() }}
|
||||
run: |
|
||||
cargo test --features sqlite,mysql,postgresql,enable_mimalloc,query_logger
|
||||
|
||||
- name: "test features: sqlite,mysql,postgresql,enable_mimalloc"
|
||||
id: test_sqlite_mysql_postgresql_mimalloc
|
||||
if: $${{ always() }}
|
||||
if: ${{ !cancelled() }}
|
||||
run: |
|
||||
cargo test --features sqlite,mysql,postgresql,enable_mimalloc
|
||||
|
||||
- name: "test features: sqlite,mysql,postgresql"
|
||||
id: test_sqlite_mysql_postgresql
|
||||
if: $${{ always() }}
|
||||
if: ${{ !cancelled() }}
|
||||
run: |
|
||||
cargo test --features sqlite,mysql,postgresql
|
||||
|
||||
- name: "test features: sqlite"
|
||||
id: test_sqlite
|
||||
if: $${{ always() }}
|
||||
if: ${{ !cancelled() }}
|
||||
run: |
|
||||
cargo test --features sqlite
|
||||
|
||||
- name: "test features: mysql"
|
||||
id: test_mysql
|
||||
if: $${{ always() }}
|
||||
if: ${{ !cancelled() }}
|
||||
run: |
|
||||
cargo test --features mysql
|
||||
|
||||
- name: "test features: postgresql"
|
||||
id: test_postgresql
|
||||
if: $${{ always() }}
|
||||
if: ${{ !cancelled() }}
|
||||
run: |
|
||||
cargo test --features postgresql
|
||||
# End Run cargo tests
|
||||
@@ -158,16 +166,16 @@ jobs:
|
||||
# Run cargo clippy, and fail on warnings
|
||||
- name: "clippy features: sqlite,mysql,postgresql,enable_mimalloc"
|
||||
id: clippy
|
||||
if: ${{ always() && matrix.channel == 'rust-toolchain' }}
|
||||
if: ${{ !cancelled() && matrix.channel == 'rust-toolchain' }}
|
||||
run: |
|
||||
cargo clippy --features sqlite,mysql,postgresql,enable_mimalloc -- -D warnings
|
||||
cargo clippy --features sqlite,mysql,postgresql,enable_mimalloc
|
||||
# End Run cargo clippy
|
||||
|
||||
|
||||
# Run cargo fmt (Only run on rust-toolchain defined version)
|
||||
- name: "check formatting"
|
||||
id: formatting
|
||||
if: ${{ always() && matrix.channel == 'rust-toolchain' }}
|
||||
if: ${{ !cancelled() && matrix.channel == 'rust-toolchain' }}
|
||||
run: |
|
||||
cargo fmt --all -- --check
|
||||
# End Run cargo fmt
|
||||
@@ -177,22 +185,31 @@ jobs:
|
||||
# This is useful so all test/clippy/fmt actions are done, and they can all be addressed
|
||||
- name: "Some checks failed"
|
||||
if: ${{ failure() }}
|
||||
env:
|
||||
TEST_DB_M_L: ${{ steps.test_sqlite_mysql_postgresql_mimalloc_logger.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 }}
|
||||
TEST_MYSQL: ${{ steps.test_mysql.outcome }}
|
||||
TEST_POSTGRESQL: ${{ steps.test_postgresql.outcome }}
|
||||
CLIPPY: ${{ steps.clippy.outcome }}
|
||||
FMT: ${{ steps.formatting.outcome }}
|
||||
run: |
|
||||
echo "### :x: Checks Failed!" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|Job|Status|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|---|------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|test (sqlite,mysql,postgresql,enable_mimalloc,query_logger)|${{ steps.test_sqlite_mysql_postgresql_mimalloc_logger.outcome }}|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|test (sqlite,mysql,postgresql,enable_mimalloc)|${{ steps.test_sqlite_mysql_postgresql_mimalloc.outcome }}|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|test (sqlite,mysql,postgresql)|${{ steps.test_sqlite_mysql_postgresql.outcome }}|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|test (sqlite)|${{ steps.test_sqlite.outcome }}|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|test (mysql)|${{ steps.test_mysql.outcome }}|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|test (postgresql)|${{ steps.test_postgresql.outcome }}|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|clippy (sqlite,mysql,postgresql,enable_mimalloc)|${{ steps.clippy.outcome }}|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|fmt|${{ steps.formatting.outcome }}|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Please check the failed jobs and fix where needed." >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### :x: Checks Failed!" >> "${GITHUB_STEP_SUMMARY}"
|
||||
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)|${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 "|fmt|${FMT}|" >> "${GITHUB_STEP_SUMMARY}"
|
||||
echo "" >> "${GITHUB_STEP_SUMMARY}"
|
||||
echo "Please check the failed jobs and fix where needed." >> "${GITHUB_STEP_SUMMARY}"
|
||||
echo "" >> "${GITHUB_STEP_SUMMARY}"
|
||||
exit 1
|
||||
|
||||
|
||||
@@ -201,5 +218,5 @@ jobs:
|
||||
- name: "All checks passed"
|
||||
if: ${{ success() }}
|
||||
run: |
|
||||
echo "### :tada: Checks Passed!" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### :tada: Checks Passed!" >> "${GITHUB_STEP_SUMMARY}"
|
||||
echo "" >> "${GITHUB_STEP_SUMMARY}"
|
||||
|
||||
28
.github/workflows/check-templates.yml
vendored
Normal file
28
.github/workflows/check-templates.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
name: Check templates
|
||||
permissions: {}
|
||||
|
||||
on: [ push, pull_request ]
|
||||
|
||||
jobs:
|
||||
docker-templates:
|
||||
permissions:
|
||||
contents: read
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
# Checkout the repo
|
||||
- name: "Checkout"
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
# End Checkout the repo
|
||||
|
||||
- name: Run make to rebuild templates
|
||||
working-directory: docker
|
||||
run: make
|
||||
|
||||
- name: Check for unstaged changes
|
||||
working-directory: docker
|
||||
run: git diff --exit-code
|
||||
continue-on-error: false
|
||||
22
.github/workflows/hadolint.yml
vendored
22
.github/workflows/hadolint.yml
vendored
@@ -1,24 +1,20 @@
|
||||
name: Hadolint
|
||||
permissions: {}
|
||||
|
||||
on: [
|
||||
push,
|
||||
pull_request
|
||||
]
|
||||
on: [ push, pull_request ]
|
||||
|
||||
jobs:
|
||||
hadolint:
|
||||
name: Validate Dockerfile syntax
|
||||
permissions:
|
||||
contents: read
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
# Checkout the repo
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
|
||||
# End Checkout the repo
|
||||
|
||||
steps:
|
||||
# Start Docker Buildx
|
||||
- name: Setup Docker Buildx
|
||||
uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1
|
||||
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
|
||||
# https://github.com/moby/buildkit/issues/3969
|
||||
# Also set max parallelism to 2, the default of 4 breaks GitHub Actions and causes OOMKills
|
||||
with:
|
||||
@@ -37,6 +33,12 @@ jobs:
|
||||
env:
|
||||
HADOLINT_VERSION: 2.12.0
|
||||
# End Download hadolint
|
||||
# Checkout the repo
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
# End Checkout the repo
|
||||
|
||||
# Test Dockerfiles with hadolint
|
||||
- name: Run hadolint
|
||||
|
||||
194
.github/workflows/release.yml
vendored
194
.github/workflows/release.yml
vendored
@@ -1,4 +1,5 @@
|
||||
name: Release
|
||||
permissions: {}
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -6,17 +7,23 @@ on:
|
||||
- main
|
||||
|
||||
tags:
|
||||
- '*'
|
||||
# https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet
|
||||
- '[1-2].[0-9]+.[0-9]+'
|
||||
|
||||
jobs:
|
||||
# https://github.com/marketplace/actions/skip-duplicate-actions
|
||||
# Some checks to determine if we need to continue with building a new docker.
|
||||
# We will skip this check if we are creating a tag, because that has the same hash as a previous run already.
|
||||
skip_check:
|
||||
runs-on: ubuntu-24.04
|
||||
# Only run this in the upstream repo and not on forks
|
||||
if: ${{ github.repository == 'dani-garcia/vaultwarden' }}
|
||||
name: Cancel older jobs when running
|
||||
permissions:
|
||||
actions: write
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
should_skip: ${{ steps.skip_check.outputs.should_skip }}
|
||||
|
||||
steps:
|
||||
- name: Skip Duplicates Actions
|
||||
id: skip_check
|
||||
@@ -27,11 +34,17 @@ jobs:
|
||||
if: ${{ github.ref_type == 'branch' }}
|
||||
|
||||
docker-build:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 120
|
||||
needs: skip_check
|
||||
if: ${{ needs.skip_check.outputs.should_skip != 'true' && github.repository == 'dani-garcia/vaultwarden' }}
|
||||
# Start a local docker registry to extract the final Alpine static build binaries
|
||||
name: Build Vaultwarden containers
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
attestations: write
|
||||
id-token: write
|
||||
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
|
||||
services:
|
||||
registry:
|
||||
image: registry:2
|
||||
@@ -56,37 +69,42 @@ jobs:
|
||||
base_image: ["debian","alpine"]
|
||||
|
||||
steps:
|
||||
# Checkout the repo
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Initialize QEMU binfmt support
|
||||
uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0
|
||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
||||
with:
|
||||
platforms: "arm64,arm"
|
||||
|
||||
# Start Docker Buildx
|
||||
- name: Setup Docker Buildx
|
||||
uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1
|
||||
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
|
||||
# https://github.com/moby/buildkit/issues/3969
|
||||
# Also set max parallelism to 2, the default of 4 breaks GitHub Actions and causes OOMKills
|
||||
with:
|
||||
cache-binary: false
|
||||
buildkitd-config-inline: |
|
||||
[worker.oci]
|
||||
max-parallelism = 2
|
||||
driver-opts: |
|
||||
network=host
|
||||
|
||||
# Checkout the repo
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
|
||||
# We need fetch-depth of 0 so we also get all the tag metadata
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
# Determine Base Tags and Source Version
|
||||
- name: Determine Base Tags and Source Version
|
||||
shell: bash
|
||||
env:
|
||||
REF_TYPE: ${{ github.ref_type }}
|
||||
run: |
|
||||
# Check which main tag we are going to build determined by github.ref_type
|
||||
if [[ "${{ github.ref_type }}" == "tag" ]]; then
|
||||
# Check which main tag we are going to build determined by ref_type
|
||||
if [[ "${REF_TYPE}" == "tag" ]]; then
|
||||
echo "BASE_TAGS=latest,${GITHUB_REF#refs/*/}" | tee -a "${GITHUB_ENV}"
|
||||
elif [[ "${{ github.ref_type }}" == "branch" ]]; then
|
||||
elif [[ "${REF_TYPE}" == "branch" ]]; then
|
||||
echo "BASE_TAGS=testing" | tee -a "${GITHUB_ENV}"
|
||||
fi
|
||||
|
||||
@@ -102,7 +120,7 @@ jobs:
|
||||
|
||||
# Login to Docker Hub
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -111,12 +129,14 @@ jobs:
|
||||
- name: Add registry for DockerHub
|
||||
if: ${{ env.HAVE_DOCKERHUB_LOGIN == 'true' }}
|
||||
shell: bash
|
||||
env:
|
||||
DOCKERHUB_REPO: ${{ vars.DOCKERHUB_REPO }}
|
||||
run: |
|
||||
echo "CONTAINER_REGISTRIES=${{ vars.DOCKERHUB_REPO }}" | tee -a "${GITHUB_ENV}"
|
||||
echo "CONTAINER_REGISTRIES=${DOCKERHUB_REPO}" | tee -a "${GITHUB_ENV}"
|
||||
|
||||
# Login to GitHub Container Registry
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -126,12 +146,14 @@ jobs:
|
||||
- name: Add registry for ghcr.io
|
||||
if: ${{ env.HAVE_GHCR_LOGIN == 'true' }}
|
||||
shell: bash
|
||||
env:
|
||||
GHCR_REPO: ${{ vars.GHCR_REPO }}
|
||||
run: |
|
||||
echo "CONTAINER_REGISTRIES=${CONTAINER_REGISTRIES:+${CONTAINER_REGISTRIES},}${{ vars.GHCR_REPO }}" | tee -a "${GITHUB_ENV}"
|
||||
echo "CONTAINER_REGISTRIES=${CONTAINER_REGISTRIES:+${CONTAINER_REGISTRIES},}${GHCR_REPO}" | tee -a "${GITHUB_ENV}"
|
||||
|
||||
# Login to Quay.io
|
||||
- name: Login to Quay.io
|
||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
with:
|
||||
registry: quay.io
|
||||
username: ${{ secrets.QUAY_USERNAME }}
|
||||
@@ -141,17 +163,22 @@ jobs:
|
||||
- name: Add registry for Quay.io
|
||||
if: ${{ env.HAVE_QUAY_LOGIN == 'true' }}
|
||||
shell: bash
|
||||
env:
|
||||
QUAY_REPO: ${{ vars.QUAY_REPO }}
|
||||
run: |
|
||||
echo "CONTAINER_REGISTRIES=${CONTAINER_REGISTRIES:+${CONTAINER_REGISTRIES},}${{ vars.QUAY_REPO }}" | tee -a "${GITHUB_ENV}"
|
||||
echo "CONTAINER_REGISTRIES=${CONTAINER_REGISTRIES:+${CONTAINER_REGISTRIES},}${QUAY_REPO}" | tee -a "${GITHUB_ENV}"
|
||||
|
||||
- name: Configure build cache from/to
|
||||
shell: bash
|
||||
env:
|
||||
GHCR_REPO: ${{ vars.GHCR_REPO }}
|
||||
BASE_IMAGE: ${{ matrix.base_image }}
|
||||
run: |
|
||||
#
|
||||
# Check if there is a GitHub Container Registry Login and use it for caching
|
||||
if [[ -n "${HAVE_GHCR_LOGIN}" ]]; then
|
||||
echo "BAKE_CACHE_FROM=type=registry,ref=${{ vars.GHCR_REPO }}-buildcache:${{ matrix.base_image }}" | tee -a "${GITHUB_ENV}"
|
||||
echo "BAKE_CACHE_TO=type=registry,ref=${{ vars.GHCR_REPO }}-buildcache:${{ matrix.base_image }},compression=zstd,mode=max" | tee -a "${GITHUB_ENV}"
|
||||
echo "BAKE_CACHE_FROM=type=registry,ref=${GHCR_REPO}-buildcache:${BASE_IMAGE}" | tee -a "${GITHUB_ENV}"
|
||||
echo "BAKE_CACHE_TO=type=registry,ref=${GHCR_REPO}-buildcache:${BASE_IMAGE},compression=zstd,mode=max" | tee -a "${GITHUB_ENV}"
|
||||
else
|
||||
echo "BAKE_CACHE_FROM="
|
||||
echo "BAKE_CACHE_TO="
|
||||
@@ -159,13 +186,13 @@ jobs:
|
||||
#
|
||||
|
||||
- name: Add localhost registry
|
||||
if: ${{ matrix.base_image == 'alpine' }}
|
||||
shell: bash
|
||||
run: |
|
||||
echo "CONTAINER_REGISTRIES=${CONTAINER_REGISTRIES:+${CONTAINER_REGISTRIES},}localhost:5000/vaultwarden/server" | tee -a "${GITHUB_ENV}"
|
||||
|
||||
- name: Bake ${{ matrix.base_image }} containers
|
||||
uses: docker/bake-action@2e3d19baedb14545e5d41222653874f25d5b4dfb # v5.10.0
|
||||
id: bake_vw
|
||||
uses: docker/bake-action@4ba453fbc2db7735392b93edf935aaf9b1e8f747 # v6.5.0
|
||||
env:
|
||||
BASE_TAGS: "${{ env.BASE_TAGS }}"
|
||||
SOURCE_COMMIT: "${{ env.SOURCE_COMMIT }}"
|
||||
@@ -175,78 +202,119 @@ jobs:
|
||||
with:
|
||||
pull: true
|
||||
push: true
|
||||
source: .
|
||||
files: docker/docker-bake.hcl
|
||||
targets: "${{ matrix.base_image }}-multi"
|
||||
set: |
|
||||
*.cache-from=${{ env.BAKE_CACHE_FROM }}
|
||||
*.cache-to=${{ env.BAKE_CACHE_TO }}
|
||||
|
||||
- name: Extract digest SHA
|
||||
shell: bash
|
||||
env:
|
||||
BAKE_METADATA: ${{ steps.bake_vw.outputs.metadata }}
|
||||
run: |
|
||||
GET_DIGEST_SHA="$(jq -r '.["${{ matrix.base_image }}-multi"]."containerimage.digest"' <<< "${BAKE_METADATA}")"
|
||||
echo "DIGEST_SHA=${GET_DIGEST_SHA}" | tee -a "${GITHUB_ENV}"
|
||||
|
||||
# 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@c074443f1aee8d4aeeae555aebba3282517141b2 # v2.2.3
|
||||
with:
|
||||
subject-name: ${{ vars.DOCKERHUB_REPO }}
|
||||
subject-digest: ${{ env.DIGEST_SHA }}
|
||||
push-to-registry: true
|
||||
|
||||
- name: Attest - ghcr.io - ${{ matrix.base_image }}
|
||||
if: ${{ env.HAVE_GHCR_LOGIN == 'true' && steps.bake_vw.outputs.metadata != ''}}
|
||||
uses: actions/attest-build-provenance@c074443f1aee8d4aeeae555aebba3282517141b2 # v2.2.3
|
||||
with:
|
||||
subject-name: ${{ vars.GHCR_REPO }}
|
||||
subject-digest: ${{ env.DIGEST_SHA }}
|
||||
push-to-registry: true
|
||||
|
||||
- name: Attest - quay.io - ${{ matrix.base_image }}
|
||||
if: ${{ env.HAVE_QUAY_LOGIN == 'true' && steps.bake_vw.outputs.metadata != ''}}
|
||||
uses: actions/attest-build-provenance@c074443f1aee8d4aeeae555aebba3282517141b2 # v2.2.3
|
||||
with:
|
||||
subject-name: ${{ vars.QUAY_REPO }}
|
||||
subject-digest: ${{ env.DIGEST_SHA }}
|
||||
push-to-registry: true
|
||||
|
||||
|
||||
# Extract the Alpine binaries from the containers
|
||||
- name: Extract binaries
|
||||
if: ${{ matrix.base_image == 'alpine' }}
|
||||
shell: bash
|
||||
env:
|
||||
REF_TYPE: ${{ github.ref_type }}
|
||||
run: |
|
||||
# Check which main tag we are going to build determined by github.ref_type
|
||||
if [[ "${{ github.ref_type }}" == "tag" ]]; then
|
||||
# Check which main tag we are going to build determined by ref_type
|
||||
if [[ "${REF_TYPE}" == "tag" ]]; then
|
||||
EXTRACT_TAG="latest"
|
||||
elif [[ "${{ github.ref_type }}" == "branch" ]]; then
|
||||
elif [[ "${REF_TYPE}" == "branch" ]]; then
|
||||
EXTRACT_TAG="testing"
|
||||
fi
|
||||
|
||||
# Check which base_image was used and append -alpine if needed
|
||||
if [[ "${{ matrix.base_image }}" == "alpine" ]]; then
|
||||
EXTRACT_TAG="${EXTRACT_TAG}-alpine"
|
||||
fi
|
||||
|
||||
# After each extraction the image is removed.
|
||||
# This is needed because using different platforms doesn't trigger a new pull/download
|
||||
|
||||
# Extract amd64 binary
|
||||
docker create --name amd64 --platform=linux/amd64 "localhost:5000/vaultwarden/server:${EXTRACT_TAG}-alpine"
|
||||
docker cp amd64:/vaultwarden vaultwarden-amd64
|
||||
docker create --name amd64 --platform=linux/amd64 "localhost:5000/vaultwarden/server:${EXTRACT_TAG}"
|
||||
docker cp amd64:/vaultwarden vaultwarden-amd64-${{ matrix.base_image }}
|
||||
docker rm --force amd64
|
||||
docker rmi --force "localhost:5000/vaultwarden/server:${EXTRACT_TAG}-alpine"
|
||||
docker rmi --force "localhost:5000/vaultwarden/server:${EXTRACT_TAG}"
|
||||
|
||||
# Extract arm64 binary
|
||||
docker create --name arm64 --platform=linux/arm64 "localhost:5000/vaultwarden/server:${EXTRACT_TAG}-alpine"
|
||||
docker cp arm64:/vaultwarden vaultwarden-arm64
|
||||
docker create --name arm64 --platform=linux/arm64 "localhost:5000/vaultwarden/server:${EXTRACT_TAG}"
|
||||
docker cp arm64:/vaultwarden vaultwarden-arm64-${{ matrix.base_image }}
|
||||
docker rm --force arm64
|
||||
docker rmi --force "localhost:5000/vaultwarden/server:${EXTRACT_TAG}-alpine"
|
||||
docker rmi --force "localhost:5000/vaultwarden/server:${EXTRACT_TAG}"
|
||||
|
||||
# Extract armv7 binary
|
||||
docker create --name armv7 --platform=linux/arm/v7 "localhost:5000/vaultwarden/server:${EXTRACT_TAG}-alpine"
|
||||
docker cp armv7:/vaultwarden vaultwarden-armv7
|
||||
docker create --name armv7 --platform=linux/arm/v7 "localhost:5000/vaultwarden/server:${EXTRACT_TAG}"
|
||||
docker cp armv7:/vaultwarden vaultwarden-armv7-${{ matrix.base_image }}
|
||||
docker rm --force armv7
|
||||
docker rmi --force "localhost:5000/vaultwarden/server:${EXTRACT_TAG}-alpine"
|
||||
docker rmi --force "localhost:5000/vaultwarden/server:${EXTRACT_TAG}"
|
||||
|
||||
# Extract armv6 binary
|
||||
docker create --name armv6 --platform=linux/arm/v6 "localhost:5000/vaultwarden/server:${EXTRACT_TAG}-alpine"
|
||||
docker cp armv6:/vaultwarden vaultwarden-armv6
|
||||
docker create --name armv6 --platform=linux/arm/v6 "localhost:5000/vaultwarden/server:${EXTRACT_TAG}"
|
||||
docker cp armv6:/vaultwarden vaultwarden-armv6-${{ matrix.base_image }}
|
||||
docker rm --force armv6
|
||||
docker rmi --force "localhost:5000/vaultwarden/server:${EXTRACT_TAG}-alpine"
|
||||
docker rmi --force "localhost:5000/vaultwarden/server:${EXTRACT_TAG}"
|
||||
|
||||
# Upload artifacts to Github Actions
|
||||
- name: "Upload amd64 artifact"
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
if: ${{ matrix.base_image == 'alpine' }}
|
||||
# Upload artifacts to Github Actions and Attest the binaries
|
||||
- name: "Upload amd64 artifact ${{ matrix.base_image }}"
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-amd64
|
||||
path: vaultwarden-amd64
|
||||
name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-amd64-${{ matrix.base_image }}
|
||||
path: vaultwarden-amd64-${{ matrix.base_image }}
|
||||
|
||||
- name: "Upload arm64 artifact"
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
if: ${{ matrix.base_image == 'alpine' }}
|
||||
- name: "Upload arm64 artifact ${{ matrix.base_image }}"
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-arm64
|
||||
path: vaultwarden-arm64
|
||||
name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-arm64-${{ matrix.base_image }}
|
||||
path: vaultwarden-arm64-${{ matrix.base_image }}
|
||||
|
||||
- name: "Upload armv7 artifact"
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
if: ${{ matrix.base_image == 'alpine' }}
|
||||
- name: "Upload armv7 artifact ${{ matrix.base_image }}"
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-armv7
|
||||
path: vaultwarden-armv7
|
||||
name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-armv7-${{ matrix.base_image }}
|
||||
path: vaultwarden-armv7-${{ matrix.base_image }}
|
||||
|
||||
- name: "Upload armv6 artifact"
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
if: ${{ matrix.base_image == 'alpine' }}
|
||||
- name: "Upload armv6 artifact ${{ matrix.base_image }}"
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-armv6
|
||||
path: vaultwarden-armv6
|
||||
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@c074443f1aee8d4aeeae555aebba3282517141b2 # v2.2.3
|
||||
with:
|
||||
subject-path: vaultwarden-*
|
||||
# End Upload artifacts to Github Actions
|
||||
|
||||
6
.github/workflows/releasecache-cleanup.yml
vendored
6
.github/workflows/releasecache-cleanup.yml
vendored
@@ -1,3 +1,6 @@
|
||||
name: Cleanup
|
||||
permissions: {}
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
@@ -9,10 +12,11 @@ on:
|
||||
schedule:
|
||||
- cron: '0 1 * * FRI'
|
||||
|
||||
name: Cleanup
|
||||
jobs:
|
||||
releasecache-cleanup:
|
||||
name: Releasecache Cleanup
|
||||
permissions:
|
||||
packages: write
|
||||
runs-on: ubuntu-24.04
|
||||
continue-on-error: true
|
||||
timeout-minutes: 30
|
||||
|
||||
27
.github/workflows/trivy.yml
vendored
27
.github/workflows/trivy.yml
vendored
@@ -1,37 +1,42 @@
|
||||
name: trivy
|
||||
name: Trivy
|
||||
permissions: {}
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
branches:
|
||||
- main
|
||||
|
||||
schedule:
|
||||
- cron: '08 11 * * *'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
trivy-scan:
|
||||
# Only run this in the master repo and not on forks
|
||||
# Only run this in the upstream repo and not on forks
|
||||
# When all forks run this at the same time, it is causing `Too Many Requests` issues
|
||||
if: ${{ github.repository == 'dani-garcia/vaultwarden' }}
|
||||
name: Check
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
name: Trivy Scan
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
actions: read
|
||||
security-events: write
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run Trivy vulnerability scanner
|
||||
uses: aquasecurity/trivy-action@18f2510ee396bbf400402947b394f2dd8c87dbb0 # v0.29.0
|
||||
uses: aquasecurity/trivy-action@6c175e9c4083a92bbca2f9724c8a5e33bc2d97a5 # v0.30.0
|
||||
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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.6.0
|
||||
rev: v5.0.0
|
||||
hooks:
|
||||
- id: check-yaml
|
||||
- id: check-json
|
||||
@@ -31,7 +31,7 @@ repos:
|
||||
language: system
|
||||
args: ["--features", "sqlite,mysql,postgresql,enable_mimalloc", "--"]
|
||||
types_or: [rust, file]
|
||||
files: (Cargo.toml|Cargo.lock|rust-toolchain|.*\.rs$)
|
||||
files: (Cargo.toml|Cargo.lock|rust-toolchain.toml|rustfmt.toml|.*\.rs$)
|
||||
pass_filenames: false
|
||||
- id: cargo-clippy
|
||||
name: cargo clippy
|
||||
@@ -40,5 +40,13 @@ repos:
|
||||
language: system
|
||||
args: ["--features", "sqlite,mysql,postgresql,enable_mimalloc", "--", "-D", "warnings"]
|
||||
types_or: [rust, file]
|
||||
files: (Cargo.toml|Cargo.lock|rust-toolchain|clippy.toml|.*\.rs$)
|
||||
files: (Cargo.toml|Cargo.lock|rust-toolchain.toml|rustfmt.toml|.*\.rs$)
|
||||
pass_filenames: false
|
||||
- id: check-docker-templates
|
||||
name: check docker templates
|
||||
description: Check if the Docker templates are updated
|
||||
language: system
|
||||
entry: sh
|
||||
args:
|
||||
- "-c"
|
||||
- "cd docker && make"
|
||||
|
||||
1531
Cargo.lock
generated
1531
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
98
Cargo.toml
98
Cargo.toml
@@ -1,9 +1,12 @@
|
||||
[workspace]
|
||||
members = ["macros"]
|
||||
|
||||
[package]
|
||||
name = "vaultwarden"
|
||||
version = "1.0.0"
|
||||
authors = ["Daniel García <dani-garcia@users.noreply.github.com>"]
|
||||
edition = "2021"
|
||||
rust-version = "1.82.0"
|
||||
rust-version = "1.85.0"
|
||||
resolver = "2"
|
||||
|
||||
repository = "https://github.com/dani-garcia/vaultwarden"
|
||||
@@ -39,21 +42,23 @@ unstable = []
|
||||
syslog = "7.0.0"
|
||||
|
||||
[dependencies]
|
||||
macros = { path = "./macros" }
|
||||
|
||||
# Logging
|
||||
log = "0.4.22"
|
||||
fern = { version = "0.7.0", features = ["syslog-7", "reopen-1"] }
|
||||
log = "0.4.27"
|
||||
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.20.2"
|
||||
once_cell = "1.21.3"
|
||||
|
||||
# Numerical libraries
|
||||
num-traits = "0.2.19"
|
||||
num-derive = "0.4.2"
|
||||
bigdecimal = "0.4.7"
|
||||
bigdecimal = "0.4.8"
|
||||
|
||||
# Web framework
|
||||
rocket = { version = "0.5.1", features = ["tls", "json"], default-features = false }
|
||||
@@ -67,46 +72,50 @@ dashmap = "6.1.0"
|
||||
|
||||
# Async futures
|
||||
futures = "0.3.31"
|
||||
tokio = { version = "1.42.0", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] }
|
||||
tokio = { version = "1.45.0", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] }
|
||||
|
||||
# A generic serialization/deserialization framework
|
||||
serde = { version = "1.0.215", features = ["derive"] }
|
||||
serde_json = "1.0.133"
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_json = "1.0.140"
|
||||
|
||||
# A safe, extensible ORM and Query builder
|
||||
diesel = { version = "2.2.6", features = ["chrono", "r2d2", "numeric"] }
|
||||
diesel = { version = "2.2.10", features = ["chrono", "r2d2", "numeric"] }
|
||||
diesel_migrations = "2.2.0"
|
||||
diesel_logger = { version = "0.4.0", optional = true }
|
||||
|
||||
derive_more = { version = "2.0.1", features = ["from", "into", "as_ref", "deref", "display"] }
|
||||
diesel-derive-newtype = "2.1.2"
|
||||
|
||||
# Bundled/Static SQLite
|
||||
libsqlite3-sys = { version = "0.30.1", features = ["bundled"], optional = true }
|
||||
libsqlite3-sys = { version = "0.33.0", features = ["bundled"], optional = true }
|
||||
|
||||
# Crypto-related libraries
|
||||
rand = { version = "0.8.5", features = ["small_rng"] }
|
||||
ring = "0.17.8"
|
||||
rand = "0.9.1"
|
||||
ring = "0.17.14"
|
||||
subtle = "2.6.1"
|
||||
|
||||
# UUID generation
|
||||
uuid = { version = "1.11.0", features = ["v4"] }
|
||||
uuid = { version = "1.16.0", features = ["v4"] }
|
||||
|
||||
# Date and time libraries
|
||||
chrono = { version = "0.4.39", features = ["clock", "serde"], default-features = false }
|
||||
chrono-tz = "0.10.0"
|
||||
time = "0.3.37"
|
||||
chrono = { version = "0.4.41", features = ["clock", "serde"], default-features = false }
|
||||
chrono-tz = "0.10.3"
|
||||
time = "0.3.41"
|
||||
|
||||
# Job scheduler
|
||||
job_scheduler_ng = "2.0.5"
|
||||
job_scheduler_ng = "2.2.0"
|
||||
|
||||
# Data encoding library Hex/Base32/Base64
|
||||
data-encoding = "2.6.0"
|
||||
data-encoding = "2.9.0"
|
||||
|
||||
# JWT library
|
||||
jsonwebtoken = "9.3.0"
|
||||
jsonwebtoken = "9.3.1"
|
||||
|
||||
# TOTP library
|
||||
totp-lite = "2.0.1"
|
||||
|
||||
# Yubico Library
|
||||
yubico = { version = "0.12.0", features = ["online-tokio"], default-features = false }
|
||||
yubico = { package = "yubico_ng", version = "0.13.0", features = ["online-tokio"], default-features = false }
|
||||
|
||||
# WebAuthn libraries
|
||||
webauthn-rs = "0.3.2"
|
||||
@@ -115,65 +124,60 @@ webauthn-rs = "0.3.2"
|
||||
url = "2.5.4"
|
||||
|
||||
# Email libraries
|
||||
lettre = { version = "0.11.11", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "tokio1-native-tls", "hostname", "tracing", "tokio1"], default-features = false }
|
||||
lettre = { version = "0.11.16", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "tokio1-native-tls", "hostname", "tracing", "tokio1"], default-features = false }
|
||||
percent-encoding = "2.3.1" # URL encoding library used for URL's in the emails
|
||||
email_address = "0.2.9"
|
||||
|
||||
# HTML Template library
|
||||
handlebars = { version = "6.2.0", features = ["dir_source"] }
|
||||
handlebars = { version = "6.3.2", features = ["dir_source"] }
|
||||
|
||||
# HTTP client (Used for favicons, version check, DUO and HIBP API)
|
||||
reqwest = { version = "0.12.9", features = ["native-tls-alpn", "stream", "json", "gzip", "brotli", "socks", "cookies"] }
|
||||
hickory-resolver = "0.24.2"
|
||||
reqwest = { version = "0.12.15", features = ["native-tls-alpn", "stream", "json", "gzip", "brotli", "socks", "cookies"] }
|
||||
hickory-resolver = "0.25.2"
|
||||
|
||||
# Favicon extraction libraries
|
||||
html5gum = "0.7.0"
|
||||
regex = { version = "1.11.1", features = ["std", "perf", "unicode-perl"], default-features = false }
|
||||
data-url = "0.3.1"
|
||||
bytes = "1.9.0"
|
||||
bytes = "1.10.1"
|
||||
|
||||
# Cache function results (Used for version check and favicon fetching)
|
||||
cached = { version = "0.54.0", features = ["async"] }
|
||||
cached = { version = "0.55.1", features = ["async"] }
|
||||
|
||||
# Used for custom short lived cookie jar during favicon extraction
|
||||
cookie = "0.18.1"
|
||||
cookie_store = "0.21.1"
|
||||
|
||||
# Used by U2F, JWT and PostgreSQL
|
||||
openssl = "0.10.68"
|
||||
openssl = "0.10.72"
|
||||
|
||||
# CLI argument parsing
|
||||
pico-args = "0.5.0"
|
||||
|
||||
# Macro ident concatenation
|
||||
paste = "1.0.15"
|
||||
governor = "0.7.0"
|
||||
pastey = "0.1.0"
|
||||
governor = "0.10.0"
|
||||
|
||||
# Check client versions for specific features.
|
||||
semver = "1.0.23"
|
||||
semver = "1.0.26"
|
||||
|
||||
# Allow overriding the default memory allocator
|
||||
# Mainly used for the musl builds, since the default musl malloc is very slow
|
||||
mimalloc = { version = "0.1.43", features = ["secure"], default-features = false, optional = true }
|
||||
which = "7.0.0"
|
||||
mimalloc = { version = "0.1.46", features = ["secure"], default-features = false, optional = true }
|
||||
|
||||
which = "7.0.3"
|
||||
|
||||
# Argon2 library with support for the PHC format
|
||||
argon2 = "0.5.3"
|
||||
|
||||
# Reading a password from the cli for generating the Argon2id ADMIN_TOKEN
|
||||
rpassword = "7.3.1"
|
||||
rpassword = "7.4.0"
|
||||
|
||||
# Loading a dynamic CSS Stylesheet
|
||||
grass_compiler = { version = "0.13.4", default-features = false }
|
||||
|
||||
[patch.crates-io]
|
||||
# Patch fern to support syslog v7
|
||||
fern = { git = "https://github.com/daboross/fern", rev = "3e775ccfafe7d24baee39826d38011981b2e55b5" }
|
||||
# Patch yubico to remove duplicate crates of older versions
|
||||
yubico = { git = "https://github.com/BlackDex/yubico-rs", rev = "00df14811f58155c0f02e3ab10f1570ed3e115c6" }
|
||||
|
||||
# Strip debuginfo from the release builds
|
||||
# The symbols are the provide better panic traces
|
||||
# The debug symbols are to provide better panic traces
|
||||
# Also enable fat LTO and use 1 codegen unit for optimizations
|
||||
[profile.release]
|
||||
strip = "debuginfo"
|
||||
@@ -208,7 +212,7 @@ codegen-units = 16
|
||||
|
||||
# Linting config
|
||||
# https://doc.rust-lang.org/rustc/lints/groups.html
|
||||
[lints.rust]
|
||||
[workspace.lints.rust]
|
||||
# Forbid
|
||||
unsafe_code = "forbid"
|
||||
non_ascii_idents = "forbid"
|
||||
@@ -232,13 +236,20 @@ unused_import_braces = "deny"
|
||||
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
|
||||
if_let_rescope = "allow"
|
||||
tail_expr_drop_order = "allow"
|
||||
|
||||
# https://rust-lang.github.io/rust-clippy/stable/index.html
|
||||
[lints.clippy]
|
||||
[workspace.lints.clippy]
|
||||
# Warn
|
||||
dbg_macro = "warn"
|
||||
todo = "warn"
|
||||
|
||||
# Ignore/Allow
|
||||
result_large_err = "allow"
|
||||
|
||||
# Deny
|
||||
case_sensitive_file_extension_comparisons = "deny"
|
||||
cast_lossless = "deny"
|
||||
@@ -269,3 +280,6 @@ unused_async = "deny"
|
||||
unused_self = "deny"
|
||||
verbose_file_reads = "deny"
|
||||
zero_sized_map_values = "deny"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
4
build.rs
4
build.rs
@@ -48,8 +48,8 @@ fn main() {
|
||||
fn run(args: &[&str]) -> Result<String, std::io::Error> {
|
||||
let out = Command::new(args[0]).args(&args[1..]).output()?;
|
||||
if !out.status.success() {
|
||||
use std::io::{Error, ErrorKind};
|
||||
return Err(Error::new(ErrorKind::Other, "Command not successful"));
|
||||
use std::io::Error;
|
||||
return Err(Error::other("Command not successful"));
|
||||
}
|
||||
Ok(String::from_utf8(out.stdout).unwrap().trim().to_string())
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
---
|
||||
vault_version: "v2024.6.2c"
|
||||
vault_image_digest: "sha256:409ab328ca931439cb916b388a4bb784bd44220717aaf74cf71620c23e34fc2b"
|
||||
# Cross Compile Docker Helper Scripts v1.5.0
|
||||
vault_version: "v2025.5.0"
|
||||
vault_image_digest: "sha256:a0a377b810e66a4ebf1416f732d2be06f3262bf5a5238695af88d3ec6871cc0e"
|
||||
# Cross Compile Docker Helper Scripts v1.6.1
|
||||
# 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:1978e7a58a1777cb0ef0dde76bad60b7914b21da57cfa88047875e4f364297aa"
|
||||
rust_version: 1.83.0 # Rust version to be used
|
||||
xx_image_digest: "sha256:9c207bead753dda9430bdd15425c6518fc7a03d866103c516a2c6889188f5894"
|
||||
rust_version: 1.87.0 # Rust version to be used
|
||||
debian_version: bookworm # Debian release name to be used
|
||||
alpine_version: "3.21" # Alpine version to be used
|
||||
# For which platforms/architectures will we try to build images
|
||||
|
||||
@@ -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:v2024.6.2c
|
||||
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2024.6.2c
|
||||
# [docker.io/vaultwarden/web-vault@sha256:409ab328ca931439cb916b388a4bb784bd44220717aaf74cf71620c23e34fc2b]
|
||||
# $ docker pull docker.io/vaultwarden/web-vault:v2025.5.0
|
||||
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.5.0
|
||||
# [docker.io/vaultwarden/web-vault@sha256:a0a377b810e66a4ebf1416f732d2be06f3262bf5a5238695af88d3ec6871cc0e]
|
||||
#
|
||||
# - Conversely, to get the tag name from the digest:
|
||||
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:409ab328ca931439cb916b388a4bb784bd44220717aaf74cf71620c23e34fc2b
|
||||
# [docker.io/vaultwarden/web-vault:v2024.6.2c]
|
||||
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:a0a377b810e66a4ebf1416f732d2be06f3262bf5a5238695af88d3ec6871cc0e
|
||||
# [docker.io/vaultwarden/web-vault:v2025.5.0]
|
||||
#
|
||||
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:409ab328ca931439cb916b388a4bb784bd44220717aaf74cf71620c23e34fc2b AS vault
|
||||
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:a0a377b810e66a4ebf1416f732d2be06f3262bf5a5238695af88d3ec6871cc0e 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.83.0 AS build_amd64
|
||||
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.83.0 AS build_arm64
|
||||
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.83.0 AS build_armv7
|
||||
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.83.0 AS build_armv6
|
||||
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.87.0 AS build_amd64
|
||||
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.87.0 AS build_arm64
|
||||
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.87.0 AS build_armv7
|
||||
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.87.0 AS build_armv6
|
||||
|
||||
########################## BUILD IMAGE ##########################
|
||||
# hadolint ignore=DL3006
|
||||
@@ -76,6 +76,7 @@ RUN source /env-cargo && \
|
||||
|
||||
# Copies over *only* your manifests and build files
|
||||
COPY ./Cargo.* ./rust-toolchain.toml ./build.rs ./
|
||||
COPY ./macros ./macros
|
||||
|
||||
ARG CARGO_PROFILE=release
|
||||
|
||||
|
||||
@@ -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:v2024.6.2c
|
||||
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2024.6.2c
|
||||
# [docker.io/vaultwarden/web-vault@sha256:409ab328ca931439cb916b388a4bb784bd44220717aaf74cf71620c23e34fc2b]
|
||||
# $ docker pull docker.io/vaultwarden/web-vault:v2025.5.0
|
||||
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.5.0
|
||||
# [docker.io/vaultwarden/web-vault@sha256:a0a377b810e66a4ebf1416f732d2be06f3262bf5a5238695af88d3ec6871cc0e]
|
||||
#
|
||||
# - Conversely, to get the tag name from the digest:
|
||||
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:409ab328ca931439cb916b388a4bb784bd44220717aaf74cf71620c23e34fc2b
|
||||
# [docker.io/vaultwarden/web-vault:v2024.6.2c]
|
||||
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:a0a377b810e66a4ebf1416f732d2be06f3262bf5a5238695af88d3ec6871cc0e
|
||||
# [docker.io/vaultwarden/web-vault:v2025.5.0]
|
||||
#
|
||||
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:409ab328ca931439cb916b388a4bb784bd44220717aaf74cf71620c23e34fc2b AS vault
|
||||
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:a0a377b810e66a4ebf1416f732d2be06f3262bf5a5238695af88d3ec6871cc0e 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:1978e7a58a1777cb0ef0dde76bad60b7914b21da57cfa88047875e4f364297aa AS xx
|
||||
FROM --platform=linux/amd64 docker.io/tonistiigi/xx@sha256:9c207bead753dda9430bdd15425c6518fc7a03d866103c516a2c6889188f5894 AS xx
|
||||
|
||||
########################## BUILD IMAGE ##########################
|
||||
# hadolint ignore=DL3006
|
||||
FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.83.0-slim-bookworm AS build
|
||||
FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.87.0-slim-bookworm AS build
|
||||
COPY --from=xx / /
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
@@ -89,24 +89,24 @@ RUN USER=root cargo new --bin /app
|
||||
WORKDIR /app
|
||||
|
||||
# Environment variables for Cargo on Debian based builds
|
||||
ARG ARCH_OPENSSL_LIB_DIR \
|
||||
ARCH_OPENSSL_INCLUDE_DIR
|
||||
ARG TARGET_PKG_CONFIG_PATH
|
||||
|
||||
RUN source /env-cargo && \
|
||||
if xx-info is-cross ; then \
|
||||
# Some special variables if needed to override some build paths
|
||||
if [[ -n "${ARCH_OPENSSL_LIB_DIR}" && -n "${ARCH_OPENSSL_INCLUDE_DIR}" ]]; then \
|
||||
echo "export $(echo "${CARGO_TARGET}" | tr '[:lower:]' '[:upper:]' | tr - _)_OPENSSL_LIB_DIR=${ARCH_OPENSSL_LIB_DIR}" >> /env-cargo && \
|
||||
echo "export $(echo "${CARGO_TARGET}" | tr '[:lower:]' '[:upper:]' | tr - _)_OPENSSL_INCLUDE_DIR=${ARCH_OPENSSL_INCLUDE_DIR}" >> /env-cargo ; \
|
||||
fi && \
|
||||
# We can't use xx-cargo since that uses clang, which doesn't work for our libraries.
|
||||
# Because of this we generate the needed environment variables here which we can load in the needed steps.
|
||||
echo "export CC_$(echo "${CARGO_TARGET}" | tr '[:upper:]' '[:lower:]' | tr - _)=/usr/bin/$(xx-info)-gcc" >> /env-cargo && \
|
||||
echo "export CARGO_TARGET_$(echo "${CARGO_TARGET}" | tr '[:lower:]' '[:upper:]' | tr - _)_LINKER=/usr/bin/$(xx-info)-gcc" >> /env-cargo && \
|
||||
echo "export PKG_CONFIG=/usr/bin/$(xx-info)-pkg-config" >> /env-cargo && \
|
||||
echo "export CROSS_COMPILE=1" >> /env-cargo && \
|
||||
echo "export OPENSSL_INCLUDE_DIR=/usr/include/$(xx-info)" >> /env-cargo && \
|
||||
echo "export OPENSSL_LIB_DIR=/usr/lib/$(xx-info)" >> /env-cargo ; \
|
||||
echo "export PKG_CONFIG_ALLOW_CROSS=1" >> /env-cargo && \
|
||||
# For some architectures `xx-info` returns a triple which doesn't matches the path on disk
|
||||
# In those cases you can override this by setting the `TARGET_PKG_CONFIG_PATH` build-arg
|
||||
if [[ -n "${TARGET_PKG_CONFIG_PATH}" ]]; then \
|
||||
echo "export TARGET_PKG_CONFIG_PATH=${TARGET_PKG_CONFIG_PATH}" >> /env-cargo ; \
|
||||
else \
|
||||
echo "export PKG_CONFIG_PATH=/usr/lib/$(xx-info)/pkgconfig" >> /env-cargo ; \
|
||||
fi && \
|
||||
echo "# End of env-cargo" >> /env-cargo ; \
|
||||
fi && \
|
||||
# Output the current contents of the file
|
||||
cat /env-cargo
|
||||
@@ -116,6 +116,7 @@ RUN source /env-cargo && \
|
||||
|
||||
# Copies over *only* your manifests and build files
|
||||
COPY ./Cargo.* ./rust-toolchain.toml ./build.rs ./
|
||||
COPY ./macros ./macros
|
||||
|
||||
ARG CARGO_PROFILE=release
|
||||
|
||||
|
||||
@@ -109,24 +109,24 @@ WORKDIR /app
|
||||
|
||||
{% if base == "debian" %}
|
||||
# Environment variables for Cargo on Debian based builds
|
||||
ARG ARCH_OPENSSL_LIB_DIR \
|
||||
ARCH_OPENSSL_INCLUDE_DIR
|
||||
ARG TARGET_PKG_CONFIG_PATH
|
||||
|
||||
RUN source /env-cargo && \
|
||||
if xx-info is-cross ; then \
|
||||
# Some special variables if needed to override some build paths
|
||||
if [[ -n "${ARCH_OPENSSL_LIB_DIR}" && -n "${ARCH_OPENSSL_INCLUDE_DIR}" ]]; then \
|
||||
echo "export $(echo "${CARGO_TARGET}" | tr '[:lower:]' '[:upper:]' | tr - _)_OPENSSL_LIB_DIR=${ARCH_OPENSSL_LIB_DIR}" >> /env-cargo && \
|
||||
echo "export $(echo "${CARGO_TARGET}" | tr '[:lower:]' '[:upper:]' | tr - _)_OPENSSL_INCLUDE_DIR=${ARCH_OPENSSL_INCLUDE_DIR}" >> /env-cargo ; \
|
||||
fi && \
|
||||
# We can't use xx-cargo since that uses clang, which doesn't work for our libraries.
|
||||
# Because of this we generate the needed environment variables here which we can load in the needed steps.
|
||||
echo "export CC_$(echo "${CARGO_TARGET}" | tr '[:upper:]' '[:lower:]' | tr - _)=/usr/bin/$(xx-info)-gcc" >> /env-cargo && \
|
||||
echo "export CARGO_TARGET_$(echo "${CARGO_TARGET}" | tr '[:lower:]' '[:upper:]' | tr - _)_LINKER=/usr/bin/$(xx-info)-gcc" >> /env-cargo && \
|
||||
echo "export PKG_CONFIG=/usr/bin/$(xx-info)-pkg-config" >> /env-cargo && \
|
||||
echo "export CROSS_COMPILE=1" >> /env-cargo && \
|
||||
echo "export OPENSSL_INCLUDE_DIR=/usr/include/$(xx-info)" >> /env-cargo && \
|
||||
echo "export OPENSSL_LIB_DIR=/usr/lib/$(xx-info)" >> /env-cargo ; \
|
||||
echo "export PKG_CONFIG_ALLOW_CROSS=1" >> /env-cargo && \
|
||||
# For some architectures `xx-info` returns a triple which doesn't matches the path on disk
|
||||
# In those cases you can override this by setting the `TARGET_PKG_CONFIG_PATH` build-arg
|
||||
if [[ -n "${TARGET_PKG_CONFIG_PATH}" ]]; then \
|
||||
echo "export TARGET_PKG_CONFIG_PATH=${TARGET_PKG_CONFIG_PATH}" >> /env-cargo ; \
|
||||
else \
|
||||
echo "export PKG_CONFIG_PATH=/usr/lib/$(xx-info)/pkgconfig" >> /env-cargo ; \
|
||||
fi && \
|
||||
echo "# End of env-cargo" >> /env-cargo ; \
|
||||
fi && \
|
||||
# Output the current contents of the file
|
||||
cat /env-cargo
|
||||
@@ -143,6 +143,7 @@ RUN source /env-cargo && \
|
||||
|
||||
# Copies over *only* your manifests and build files
|
||||
COPY ./Cargo.* ./rust-toolchain.toml ./build.rs ./
|
||||
COPY ./macros ./macros
|
||||
|
||||
ARG CARGO_PROFILE=release
|
||||
|
||||
|
||||
@@ -133,8 +133,7 @@ target "debian-386" {
|
||||
platforms = ["linux/386"]
|
||||
tags = generate_tags("", "-386")
|
||||
args = {
|
||||
ARCH_OPENSSL_LIB_DIR = "/usr/lib/i386-linux-gnu"
|
||||
ARCH_OPENSSL_INCLUDE_DIR = "/usr/include/i386-linux-gnu"
|
||||
TARGET_PKG_CONFIG_PATH = "/usr/lib/i386-linux-gnu/pkgconfig"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,20 +141,12 @@ target "debian-ppc64le" {
|
||||
inherits = ["debian"]
|
||||
platforms = ["linux/ppc64le"]
|
||||
tags = generate_tags("", "-ppc64le")
|
||||
args = {
|
||||
ARCH_OPENSSL_LIB_DIR = "/usr/lib/powerpc64le-linux-gnu"
|
||||
ARCH_OPENSSL_INCLUDE_DIR = "/usr/include/powerpc64le-linux-gnu"
|
||||
}
|
||||
}
|
||||
|
||||
target "debian-s390x" {
|
||||
inherits = ["debian"]
|
||||
platforms = ["linux/s390x"]
|
||||
tags = generate_tags("", "-s390x")
|
||||
args = {
|
||||
ARCH_OPENSSL_LIB_DIR = "/usr/lib/s390x-linux-gnu"
|
||||
ARCH_OPENSSL_INCLUDE_DIR = "/usr/include/s390x-linux-gnu"
|
||||
}
|
||||
}
|
||||
// ==== End of unsupported Debian architecture targets ===
|
||||
|
||||
|
||||
16
macros/Cargo.toml
Normal file
16
macros/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "macros"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
name = "macros"
|
||||
path = "src/lib.rs"
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
quote = "1.0.40"
|
||||
syn = "2.0.101"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
56
macros/src/lib.rs
Normal file
56
macros/src/lib.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
|
||||
#[proc_macro_derive(UuidFromParam)]
|
||||
pub fn derive_uuid_from_param(input: TokenStream) -> TokenStream {
|
||||
let ast = syn::parse(input).unwrap();
|
||||
|
||||
impl_derive_uuid_macro(&ast)
|
||||
}
|
||||
|
||||
fn impl_derive_uuid_macro(ast: &syn::DeriveInput) -> TokenStream {
|
||||
let name = &ast.ident;
|
||||
let gen_derive = quote! {
|
||||
#[automatically_derived]
|
||||
impl<'r> rocket::request::FromParam<'r> for #name {
|
||||
type Error = ();
|
||||
|
||||
#[inline(always)]
|
||||
fn from_param(param: &'r str) -> Result<Self, Self::Error> {
|
||||
if uuid::Uuid::parse_str(param).is_ok() {
|
||||
Ok(Self(param.to_string()))
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
gen_derive.into()
|
||||
}
|
||||
|
||||
#[proc_macro_derive(IdFromParam)]
|
||||
pub fn derive_id_from_param(input: TokenStream) -> TokenStream {
|
||||
let ast = syn::parse(input).unwrap();
|
||||
|
||||
impl_derive_safestring_macro(&ast)
|
||||
}
|
||||
|
||||
fn impl_derive_safestring_macro(ast: &syn::DeriveInput) -> TokenStream {
|
||||
let name = &ast.ident;
|
||||
let gen_derive = quote! {
|
||||
#[automatically_derived]
|
||||
impl<'r> rocket::request::FromParam<'r> for #name {
|
||||
type Error = ();
|
||||
|
||||
#[inline(always)]
|
||||
fn from_param(param: &'r str) -> Result<Self, Self::Error> {
|
||||
if param.chars().all(|c| matches!(c, 'a'..='z' | 'A'..='Z' |'0'..='9' | '-')) {
|
||||
Ok(Self(param.to_string()))
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
gen_derive.into()
|
||||
}
|
||||
5
migrations/mysql/2025-01-09-172300_add_manage/up.sql
Normal file
5
migrations/mysql/2025-01-09-172300_add_manage/up.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
ALTER TABLE users_collections
|
||||
ADD COLUMN manage BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
|
||||
ALTER TABLE collections_groups
|
||||
ADD COLUMN manage BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
@@ -0,0 +1,5 @@
|
||||
ALTER TABLE users_collections
|
||||
ADD COLUMN manage BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
|
||||
ALTER TABLE collections_groups
|
||||
ADD COLUMN manage BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
5
migrations/sqlite/2025-01-09-172300_add_manage/up.sql
Normal file
5
migrations/sqlite/2025-01-09-172300_add_manage/up.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
ALTER TABLE users_collections
|
||||
ADD COLUMN manage BOOLEAN NOT NULL DEFAULT 0; -- FALSE
|
||||
|
||||
ALTER TABLE collections_groups
|
||||
ADD COLUMN manage BOOLEAN NOT NULL DEFAULT 0; -- FALSE
|
||||
@@ -1,4 +1,4 @@
|
||||
[toolchain]
|
||||
channel = "1.83.0"
|
||||
channel = "1.87.0"
|
||||
components = [ "rustfmt", "clippy" ]
|
||||
profile = "minimal"
|
||||
|
||||
155
src/api/admin.rs
155
src/api/admin.rs
@@ -50,7 +50,7 @@ pub fn routes() -> Vec<Route> {
|
||||
disable_user,
|
||||
enable_user,
|
||||
remove_2fa,
|
||||
update_user_org_type,
|
||||
update_membership_type,
|
||||
update_revision_users,
|
||||
post_config,
|
||||
delete_config,
|
||||
@@ -99,9 +99,10 @@ const DT_FMT: &str = "%Y-%m-%d %H:%M:%S %Z";
|
||||
const BASE_TEMPLATE: &str = "admin/base";
|
||||
|
||||
const ACTING_ADMIN_USER: &str = "vaultwarden-admin-00000-000000000000";
|
||||
pub const FAKE_ADMIN_UUID: &str = "00000000-0000-0000-0000-000000000000";
|
||||
|
||||
fn admin_path() -> String {
|
||||
format!("{}{}", CONFIG.domain_path(), ADMIN_PATH)
|
||||
format!("{}{ADMIN_PATH}", CONFIG.domain_path())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -170,7 +171,7 @@ struct LoginForm {
|
||||
redirect: Option<String>,
|
||||
}
|
||||
|
||||
#[post("/", data = "<data>")]
|
||||
#[post("/", format = "application/x-www-form-urlencoded", data = "<data>")]
|
||||
fn post_admin_login(
|
||||
data: Form<LoginForm>,
|
||||
cookies: &CookieJar<'_>,
|
||||
@@ -205,7 +206,7 @@ fn post_admin_login(
|
||||
|
||||
cookies.add(cookie);
|
||||
if let Some(redirect) = redirect {
|
||||
Ok(Redirect::to(format!("{}{}", admin_path(), redirect)))
|
||||
Ok(Redirect::to(format!("{}{redirect}", admin_path())))
|
||||
} else {
|
||||
Err(AdminResponse::Ok(render_admin_page()))
|
||||
}
|
||||
@@ -280,15 +281,15 @@ struct InviteData {
|
||||
email: String,
|
||||
}
|
||||
|
||||
async fn get_user_or_404(uuid: &str, conn: &mut DbConn) -> ApiResult<User> {
|
||||
if let Some(user) = User::find_by_uuid(uuid, conn).await {
|
||||
async fn get_user_or_404(user_id: &UserId, conn: &mut DbConn) -> ApiResult<User> {
|
||||
if let Some(user) = User::find_by_uuid(user_id, conn).await {
|
||||
Ok(user)
|
||||
} else {
|
||||
err_code!("User doesn't exist", Status::NotFound.code);
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/invite", data = "<data>")]
|
||||
#[post("/invite", format = "application/json", data = "<data>")]
|
||||
async fn invite_user(data: Json<InviteData>, _token: AdminToken, mut conn: DbConn) -> JsonResult {
|
||||
let data: InviteData = data.into_inner();
|
||||
if User::find_by_mail(&data.email, &mut conn).await.is_some() {
|
||||
@@ -299,7 +300,9 @@ async fn invite_user(data: Json<InviteData>, _token: AdminToken, mut conn: DbCon
|
||||
|
||||
async fn _generate_invite(user: &User, conn: &mut DbConn) -> EmptyResult {
|
||||
if CONFIG.mail_enabled() {
|
||||
mail::send_invite(user, None, None, &CONFIG.invitation_org_name(), None).await
|
||||
let org_id: OrganizationId = FAKE_ADMIN_UUID.to_string().into();
|
||||
let member_id: MembershipId = FAKE_ADMIN_UUID.to_string().into();
|
||||
mail::send_invite(user, org_id, member_id, &CONFIG.invitation_org_name(), None).await
|
||||
} else {
|
||||
let invitation = Invitation::new(&user.email);
|
||||
invitation.save(conn).await
|
||||
@@ -312,7 +315,7 @@ async fn invite_user(data: Json<InviteData>, _token: AdminToken, mut conn: DbCon
|
||||
Ok(Json(user.to_json(&mut conn).await))
|
||||
}
|
||||
|
||||
#[post("/test/smtp", data = "<data>")]
|
||||
#[post("/test/smtp", format = "application/json", data = "<data>")]
|
||||
async fn test_smtp(data: Json<InviteData>, _token: AdminToken) -> EmptyResult {
|
||||
let data: InviteData = data.into_inner();
|
||||
|
||||
@@ -381,29 +384,29 @@ async fn get_user_by_mail_json(mail: &str, _token: AdminToken, mut conn: DbConn)
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/users/<uuid>")]
|
||||
async fn get_user_json(uuid: &str, _token: AdminToken, mut conn: DbConn) -> JsonResult {
|
||||
let u = get_user_or_404(uuid, &mut conn).await?;
|
||||
#[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;
|
||||
usr["userEnabled"] = json!(u.enabled);
|
||||
usr["createdAt"] = json!(format_naive_datetime_local(&u.created_at, DT_FMT));
|
||||
Ok(Json(usr))
|
||||
}
|
||||
|
||||
#[post("/users/<uuid>/delete")]
|
||||
async fn delete_user(uuid: &str, token: AdminToken, mut conn: DbConn) -> EmptyResult {
|
||||
let user = get_user_or_404(uuid, &mut conn).await?;
|
||||
#[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?;
|
||||
|
||||
// Get the user_org records before deleting the actual user
|
||||
let user_orgs = UserOrganization::find_any_state_by_user(uuid, &mut 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;
|
||||
|
||||
for user_org in user_orgs {
|
||||
for membership in memberships {
|
||||
log_event(
|
||||
EventType::OrganizationUserRemoved as i32,
|
||||
&user_org.uuid,
|
||||
&user_org.org_uuid,
|
||||
ACTING_ADMIN_USER,
|
||||
EventType::OrganizationUserDeleted as i32,
|
||||
&membership.uuid,
|
||||
&membership.org_uuid,
|
||||
&ACTING_ADMIN_USER.into(),
|
||||
14, // Use UnknownBrowser type
|
||||
&token.ip.ip,
|
||||
&mut conn,
|
||||
@@ -414,9 +417,9 @@ async fn delete_user(uuid: &str, token: AdminToken, mut conn: DbConn) -> EmptyRe
|
||||
res
|
||||
}
|
||||
|
||||
#[post("/users/<uuid>/deauth")]
|
||||
async fn deauth_user(uuid: &str, _token: AdminToken, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
||||
let mut user = get_user_or_404(uuid, &mut conn).await?;
|
||||
#[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?;
|
||||
|
||||
nt.send_logout(&user, None).await;
|
||||
|
||||
@@ -424,7 +427,7 @@ async fn deauth_user(uuid: &str, _token: AdminToken, mut conn: DbConn, nt: Notif
|
||||
for device in Device::find_push_devices_by_user(&user.uuid, &mut conn).await {
|
||||
match unregister_push_device(device.push_uuid).await {
|
||||
Ok(r) => r,
|
||||
Err(e) => error!("Unable to unregister devices from Bitwarden server: {}", e),
|
||||
Err(e) => error!("Unable to unregister devices from Bitwarden server: {e}"),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -435,9 +438,9 @@ async fn deauth_user(uuid: &str, _token: AdminToken, mut conn: DbConn, nt: Notif
|
||||
user.save(&mut conn).await
|
||||
}
|
||||
|
||||
#[post("/users/<uuid>/disable")]
|
||||
async fn disable_user(uuid: &str, _token: AdminToken, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
||||
let mut user = get_user_or_404(uuid, &mut conn).await?;
|
||||
#[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?;
|
||||
user.reset_security_stamp();
|
||||
user.enabled = false;
|
||||
@@ -449,33 +452,35 @@ async fn disable_user(uuid: &str, _token: AdminToken, mut conn: DbConn, nt: Noti
|
||||
save_result
|
||||
}
|
||||
|
||||
#[post("/users/<uuid>/enable")]
|
||||
async fn enable_user(uuid: &str, _token: AdminToken, mut conn: DbConn) -> EmptyResult {
|
||||
let mut user = get_user_or_404(uuid, &mut conn).await?;
|
||||
#[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?;
|
||||
user.enabled = true;
|
||||
|
||||
user.save(&mut conn).await
|
||||
}
|
||||
|
||||
#[post("/users/<uuid>/remove-2fa")]
|
||||
async fn remove_2fa(uuid: &str, token: AdminToken, mut conn: DbConn) -> EmptyResult {
|
||||
let mut user = get_user_or_404(uuid, &mut 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, 14, &token.ip.ip, &mut conn).await?;
|
||||
two_factor::enforce_2fa_policy(&user, &ACTING_ADMIN_USER.into(), 14, &token.ip.ip, &mut conn).await?;
|
||||
user.totp_recover = None;
|
||||
user.save(&mut conn).await
|
||||
}
|
||||
|
||||
#[post("/users/<uuid>/invite/resend")]
|
||||
async fn resend_user_invite(uuid: &str, _token: AdminToken, mut conn: DbConn) -> EmptyResult {
|
||||
if let Some(user) = User::find_by_uuid(uuid, &mut conn).await {
|
||||
#[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 {
|
||||
//TODO: replace this with user.status check when it will be available (PR#3397)
|
||||
if !user.password_hash.is_empty() {
|
||||
err_code!("User already accepted invitation", Status::BadRequest.code);
|
||||
}
|
||||
|
||||
if CONFIG.mail_enabled() {
|
||||
mail::send_invite(&user, None, None, &CONFIG.invitation_org_name(), None).await
|
||||
let org_id: OrganizationId = FAKE_ADMIN_UUID.to_string().into();
|
||||
let member_id: MembershipId = FAKE_ADMIN_UUID.to_string().into();
|
||||
mail::send_invite(&user, org_id, member_id, &CONFIG.invitation_org_name(), None).await
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
@@ -485,42 +490,41 @@ async fn resend_user_invite(uuid: &str, _token: AdminToken, mut conn: DbConn) ->
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct UserOrgTypeData {
|
||||
struct MembershipTypeData {
|
||||
user_type: NumberOrString,
|
||||
user_uuid: String,
|
||||
org_uuid: String,
|
||||
user_uuid: UserId,
|
||||
org_uuid: OrganizationId,
|
||||
}
|
||||
|
||||
#[post("/users/org_type", data = "<data>")]
|
||||
async fn update_user_org_type(data: Json<UserOrgTypeData>, token: AdminToken, mut conn: DbConn) -> EmptyResult {
|
||||
let data: UserOrgTypeData = data.into_inner();
|
||||
#[post("/users/org_type", format = "application/json", data = "<data>")]
|
||||
async fn update_membership_type(data: Json<MembershipTypeData>, token: AdminToken, mut conn: DbConn) -> EmptyResult {
|
||||
let data: MembershipTypeData = data.into_inner();
|
||||
|
||||
let mut user_to_edit =
|
||||
match UserOrganization::find_by_user_and_org(&data.user_uuid, &data.org_uuid, &mut conn).await {
|
||||
Some(user) => user,
|
||||
None => err!("The specified user isn't member of the organization"),
|
||||
};
|
||||
let Some(mut member_to_edit) = Membership::find_by_user_and_org(&data.user_uuid, &data.org_uuid, &mut conn).await
|
||||
else {
|
||||
err!("The specified user isn't member of the organization")
|
||||
};
|
||||
|
||||
let new_type = match UserOrgType::from_str(&data.user_type.into_string()) {
|
||||
let new_type = match MembershipType::from_str(&data.user_type.into_string()) {
|
||||
Some(new_type) => new_type as i32,
|
||||
None => err!("Invalid type"),
|
||||
};
|
||||
|
||||
if user_to_edit.atype == UserOrgType::Owner && new_type != UserOrgType::Owner {
|
||||
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 UserOrganization::count_confirmed_by_org_and_type(&data.org_uuid, UserOrgType::Owner, &mut conn).await <= 1 {
|
||||
if Membership::count_confirmed_by_org_and_type(&data.org_uuid, MembershipType::Owner, &mut conn).await <= 1 {
|
||||
err!("Can't change the type of the last owner")
|
||||
}
|
||||
}
|
||||
|
||||
// This check is also done at api::organizations::{accept_invite(), _confirm_invite, _activate_user(), edit_user()}, update_user_org_type
|
||||
// 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 < UserOrgType::Admin {
|
||||
match OrgPolicy::is_user_allowed(&user_to_edit.user_uuid, &user_to_edit.org_uuid, true, &mut conn).await {
|
||||
if new_type < MembershipType::Admin {
|
||||
match OrgPolicy::is_user_allowed(&member_to_edit.user_uuid, &member_to_edit.org_uuid, true, &mut conn).await {
|
||||
Ok(_) => {}
|
||||
Err(OrgPolicyErr::TwoFactorMissing) => {
|
||||
if CONFIG.email_2fa_auto_fallback() {
|
||||
two_factor::email::find_and_activate_email_2fa(&user_to_edit.user_uuid, &mut conn).await?;
|
||||
two_factor::email::find_and_activate_email_2fa(&member_to_edit.user_uuid, &mut conn).await?;
|
||||
} else {
|
||||
err!("You cannot modify this user to this type because they have not setup 2FA");
|
||||
}
|
||||
@@ -533,20 +537,20 @@ async fn update_user_org_type(data: Json<UserOrgTypeData>, token: AdminToken, mu
|
||||
|
||||
log_event(
|
||||
EventType::OrganizationUserUpdated as i32,
|
||||
&user_to_edit.uuid,
|
||||
&member_to_edit.uuid,
|
||||
&data.org_uuid,
|
||||
ACTING_ADMIN_USER,
|
||||
&ACTING_ADMIN_USER.into(),
|
||||
14, // Use UnknownBrowser type
|
||||
&token.ip.ip,
|
||||
&mut conn,
|
||||
)
|
||||
.await;
|
||||
|
||||
user_to_edit.atype = new_type;
|
||||
user_to_edit.save(&mut conn).await
|
||||
member_to_edit.atype = new_type;
|
||||
member_to_edit.save(&mut conn).await
|
||||
}
|
||||
|
||||
#[post("/users/update_revision")]
|
||||
#[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
|
||||
}
|
||||
@@ -557,7 +561,7 @@ async fn organizations_overview(_token: AdminToken, mut conn: DbConn) -> ApiResu
|
||||
let mut organizations_json = Vec::with_capacity(organizations.len());
|
||||
for o in organizations {
|
||||
let mut org = o.to_json();
|
||||
org["user_count"] = json!(UserOrganization::count_by_org(&o.uuid, &mut conn).await);
|
||||
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);
|
||||
@@ -571,9 +575,9 @@ async fn organizations_overview(_token: AdminToken, mut conn: DbConn) -> ApiResu
|
||||
Ok(Html(text))
|
||||
}
|
||||
|
||||
#[post("/organizations/<uuid>/delete")]
|
||||
async fn delete_organization(uuid: &str, _token: AdminToken, mut conn: DbConn) -> EmptyResult {
|
||||
let org = Organization::find_by_uuid(uuid, &mut conn).await.map_res("Organization doesn't exist")?;
|
||||
#[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
|
||||
}
|
||||
|
||||
@@ -602,9 +606,8 @@ async fn get_json_api<T: DeserializeOwned>(url: &str) -> Result<T, Error> {
|
||||
}
|
||||
|
||||
async fn has_http_access() -> bool {
|
||||
let req = match make_http_request(Method::HEAD, "https://github.com/dani-garcia/vaultwarden") {
|
||||
Ok(r) => r,
|
||||
Err(_) => return false,
|
||||
let Ok(req) = make_http_request(Method::HEAD, "https://github.com/dani-garcia/vaultwarden") else {
|
||||
return false;
|
||||
};
|
||||
match req.send().await {
|
||||
Ok(r) => r.status().is_success(),
|
||||
@@ -615,7 +618,7 @@ async fn has_http_access() -> bool {
|
||||
use cached::proc_macro::cached;
|
||||
/// Cache this function to prevent API call rate limit. Github only allows 60 requests per hour, and we use 3 here already.
|
||||
/// It will cache this function for 300 seconds (5 minutes) which should prevent the exhaustion of the rate limit.
|
||||
#[cached(time = 300, sync_writes = true)]
|
||||
#[cached(time = 300, sync_writes = "default")]
|
||||
async fn get_release_info(has_http_access: bool, running_within_container: bool) -> (String, String, String) {
|
||||
// If the HTTP Check failed, do not even attempt to check for new versions since we were not able to connect with github.com anyway.
|
||||
if has_http_access {
|
||||
@@ -730,7 +733,7 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn)
|
||||
Ok(Html(text))
|
||||
}
|
||||
|
||||
#[get("/diagnostics/config")]
|
||||
#[get("/diagnostics/config", format = "application/json")]
|
||||
fn get_diagnostics_config(_token: AdminToken) -> Json<Value> {
|
||||
let support_json = CONFIG.get_support_json();
|
||||
Json(support_json)
|
||||
@@ -741,16 +744,16 @@ fn get_diagnostics_http(code: u16, _token: AdminToken) -> EmptyResult {
|
||||
err_code!(format!("Testing error {code} response"), code);
|
||||
}
|
||||
|
||||
#[post("/config", data = "<data>")]
|
||||
#[post("/config", format = "application/json", data = "<data>")]
|
||||
fn post_config(data: Json<ConfigBuilder>, _token: AdminToken) -> EmptyResult {
|
||||
let data: ConfigBuilder = data.into_inner();
|
||||
if let Err(e) = CONFIG.update_config(data) {
|
||||
if let Err(e) = CONFIG.update_config(data, true) {
|
||||
err!(format!("Unable to save config: {e:?}"))
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[post("/config/delete")]
|
||||
#[post("/config/delete", format = "application/json")]
|
||||
fn delete_config(_token: AdminToken) -> EmptyResult {
|
||||
if let Err(e) = CONFIG.delete_user_config() {
|
||||
err!(format!("Unable to delete config: {e:?}"))
|
||||
@@ -758,7 +761,7 @@ fn delete_config(_token: AdminToken) -> EmptyResult {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[post("/config/backup_db")]
|
||||
#[post("/config/backup_db", format = "application/json")]
|
||||
async fn backup_db(_token: AdminToken, mut conn: DbConn) -> ApiResult<String> {
|
||||
if *CAN_BACKUP {
|
||||
match backup_database(&mut conn).await {
|
||||
|
||||
@@ -30,6 +30,7 @@ pub fn routes() -> Vec<rocket::Route> {
|
||||
profile,
|
||||
put_profile,
|
||||
post_profile,
|
||||
put_avatar,
|
||||
get_public_keys,
|
||||
post_keys,
|
||||
post_password,
|
||||
@@ -42,9 +43,8 @@ pub fn routes() -> Vec<rocket::Route> {
|
||||
post_verify_email_token,
|
||||
post_delete_recover,
|
||||
post_delete_recover_token,
|
||||
post_device_token,
|
||||
delete_account,
|
||||
post_delete_account,
|
||||
delete_account,
|
||||
revision_date,
|
||||
password_hint,
|
||||
prelogin,
|
||||
@@ -52,7 +52,9 @@ pub fn routes() -> Vec<rocket::Route> {
|
||||
api_key,
|
||||
rotate_api_key,
|
||||
get_known_device,
|
||||
put_avatar,
|
||||
get_all_devices,
|
||||
get_device,
|
||||
post_device_token,
|
||||
put_device_token,
|
||||
put_clear_device_token,
|
||||
post_clear_device_token,
|
||||
@@ -68,18 +70,31 @@ pub fn routes() -> Vec<rocket::Route> {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RegisterData {
|
||||
email: String,
|
||||
|
||||
kdf: Option<i32>,
|
||||
kdf_iterations: Option<i32>,
|
||||
kdf_memory: Option<i32>,
|
||||
kdf_parallelism: Option<i32>,
|
||||
|
||||
#[serde(alias = "userSymmetricKey")]
|
||||
key: String,
|
||||
#[serde(alias = "userAsymmetricKeys")]
|
||||
keys: Option<KeysData>,
|
||||
|
||||
master_password_hash: String,
|
||||
master_password_hint: Option<String>,
|
||||
|
||||
name: Option<String>,
|
||||
token: Option<String>,
|
||||
|
||||
#[allow(dead_code)]
|
||||
organization_user_id: Option<String>,
|
||||
organization_user_id: Option<MembershipId>,
|
||||
|
||||
// Used only from the register/finish endpoint
|
||||
email_verification_token: Option<String>,
|
||||
accept_emergency_access_id: Option<EmergencyAccessId>,
|
||||
accept_emergency_access_invite_token: Option<String>,
|
||||
#[serde(alias = "token")]
|
||||
org_invite_token: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -106,15 +121,15 @@ fn enforce_password_hint_setting(password_hint: &Option<String>) -> EmptyResult
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
async fn is_email_2fa_required(org_user_uuid: Option<String>, conn: &mut DbConn) -> bool {
|
||||
async fn is_email_2fa_required(member_id: Option<MembershipId>, conn: &mut DbConn) -> bool {
|
||||
if !CONFIG._enable_email_2fa() {
|
||||
return false;
|
||||
}
|
||||
if CONFIG.email_2fa_enforce_on_verified_invite() {
|
||||
return true;
|
||||
}
|
||||
if org_user_uuid.is_some() {
|
||||
return OrgPolicy::is_enabled_for_member(&org_user_uuid.unwrap(), OrgPolicyType::TwoFactorAuthentication, conn)
|
||||
if member_id.is_some() {
|
||||
return OrgPolicy::is_enabled_for_member(&member_id.unwrap(), OrgPolicyType::TwoFactorAuthentication, conn)
|
||||
.await;
|
||||
}
|
||||
false
|
||||
@@ -122,13 +137,78 @@ async fn is_email_2fa_required(org_user_uuid: Option<String>, conn: &mut DbConn)
|
||||
|
||||
#[post("/accounts/register", data = "<data>")]
|
||||
async fn register(data: Json<RegisterData>, conn: DbConn) -> JsonResult {
|
||||
_register(data, conn).await
|
||||
_register(data, false, conn).await
|
||||
}
|
||||
|
||||
pub async fn _register(data: Json<RegisterData>, mut conn: DbConn) -> JsonResult {
|
||||
let data: RegisterData = data.into_inner();
|
||||
pub async fn _register(data: Json<RegisterData>, email_verification: bool, mut conn: DbConn) -> JsonResult {
|
||||
let mut data: RegisterData = data.into_inner();
|
||||
let email = data.email.to_lowercase();
|
||||
|
||||
let mut email_verified = false;
|
||||
|
||||
let mut pending_emergency_access = None;
|
||||
|
||||
// First, validate the provided verification tokens
|
||||
if email_verification {
|
||||
match (
|
||||
&data.email_verification_token,
|
||||
&data.accept_emergency_access_id,
|
||||
&data.accept_emergency_access_invite_token,
|
||||
&data.organization_user_id,
|
||||
&data.org_invite_token,
|
||||
) {
|
||||
// Normal user registration, when email verification is required
|
||||
(Some(email_verification_token), None, None, None, None) => {
|
||||
let claims = crate::auth::decode_register_verify(email_verification_token)?;
|
||||
if claims.sub != data.email {
|
||||
err!("Email verification token does not match email");
|
||||
}
|
||||
|
||||
// During this call we don't get the name, so extract it from the claims
|
||||
if claims.name.is_some() {
|
||||
data.name = claims.name;
|
||||
}
|
||||
email_verified = claims.verified;
|
||||
}
|
||||
// Emergency access registration
|
||||
(None, Some(accept_emergency_access_id), Some(accept_emergency_access_invite_token), None, None) => {
|
||||
if !CONFIG.emergency_access_allowed() {
|
||||
err!("Emergency access is not enabled.")
|
||||
}
|
||||
|
||||
let claims = crate::auth::decode_emergency_access_invite(accept_emergency_access_invite_token)?;
|
||||
|
||||
if claims.email != data.email {
|
||||
err!("Claim email does not match email")
|
||||
}
|
||||
if &claims.emer_id != accept_emergency_access_id {
|
||||
err!("Claim emer_id does not match accept_emergency_access_id")
|
||||
}
|
||||
|
||||
pending_emergency_access = Some((accept_emergency_access_id, claims));
|
||||
email_verified = true;
|
||||
}
|
||||
// Org invite
|
||||
(None, None, None, Some(organization_user_id), Some(org_invite_token)) => {
|
||||
let claims = decode_invite(org_invite_token)?;
|
||||
|
||||
if claims.email != data.email {
|
||||
err!("Claim email does not match email")
|
||||
}
|
||||
|
||||
if &claims.member_id != organization_user_id {
|
||||
err!("Claim org_user_id does not match organization_user_id")
|
||||
}
|
||||
|
||||
email_verified = true;
|
||||
}
|
||||
|
||||
_ => {
|
||||
err!("Registration is missing required parameters")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the length of the username exceeds 50 characters (Same is Upstream Bitwarden)
|
||||
// This also prevents issues with very long usernames causing to large JWT's. See #2419
|
||||
if let Some(ref name) = data.name {
|
||||
@@ -142,28 +222,25 @@ pub async fn _register(data: Json<RegisterData>, mut conn: DbConn) -> JsonResult
|
||||
let password_hint = clean_password_hint(&data.master_password_hint);
|
||||
enforce_password_hint_setting(&password_hint)?;
|
||||
|
||||
let mut verified_by_invite = false;
|
||||
|
||||
let mut user = match User::find_by_mail(&email, &mut conn).await {
|
||||
Some(mut user) => {
|
||||
Some(user) => {
|
||||
if !user.password_hash.is_empty() {
|
||||
err!("Registration not allowed or user already exists")
|
||||
}
|
||||
|
||||
if let Some(token) = data.token {
|
||||
if let Some(token) = data.org_invite_token {
|
||||
let claims = decode_invite(&token)?;
|
||||
if claims.email == email {
|
||||
// Verify the email address when signing up via a valid invite token
|
||||
verified_by_invite = true;
|
||||
user.verified_at = Some(Utc::now().naive_utc());
|
||||
email_verified = true;
|
||||
user
|
||||
} else {
|
||||
err!("Registration email does not match invite email")
|
||||
}
|
||||
} else if Invitation::take(&email, &mut conn).await {
|
||||
for user_org in UserOrganization::find_invited_by_user(&user.uuid, &mut conn).await.iter_mut() {
|
||||
user_org.status = UserOrgStatus::Accepted as i32;
|
||||
user_org.save(&mut conn).await?;
|
||||
for membership in Membership::find_invited_by_user(&user.uuid, &mut conn).await.iter_mut() {
|
||||
membership.status = MembershipStatus::Accepted as i32;
|
||||
membership.save(&mut conn).await?;
|
||||
}
|
||||
user
|
||||
} else if CONFIG.is_signup_allowed(&email)
|
||||
@@ -179,7 +256,10 @@ pub async fn _register(data: Json<RegisterData>, mut conn: DbConn) -> JsonResult
|
||||
// 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 || CONFIG.is_signup_allowed(&email) {
|
||||
if Invitation::take(&email, &mut conn).await
|
||||
|| CONFIG.is_signup_allowed(&email)
|
||||
|| pending_emergency_access.is_some()
|
||||
{
|
||||
User::new(email.clone())
|
||||
} else {
|
||||
err!("Registration not allowed or user already exists")
|
||||
@@ -214,17 +294,21 @@ pub async fn _register(data: Json<RegisterData>, mut conn: DbConn) -> JsonResult
|
||||
user.public_key = Some(keys.public_key);
|
||||
}
|
||||
|
||||
if email_verified {
|
||||
user.verified_at = Some(Utc::now().naive_utc());
|
||||
}
|
||||
|
||||
if CONFIG.mail_enabled() {
|
||||
if CONFIG.signups_verify() && !verified_by_invite {
|
||||
if CONFIG.signups_verify() && !email_verified {
|
||||
if let Err(e) = mail::send_welcome_must_verify(&user.email, &user.uuid).await {
|
||||
error!("Error sending welcome email: {:#?}", e);
|
||||
error!("Error sending welcome email: {e:#?}");
|
||||
}
|
||||
user.last_verifying_at = Some(user.created_at);
|
||||
} else if let Err(e) = mail::send_welcome(&user.email).await {
|
||||
error!("Error sending welcome email: {:#?}", e);
|
||||
error!("Error sending welcome email: {e:#?}");
|
||||
}
|
||||
|
||||
if verified_by_invite && is_email_2fa_required(data.organization_user_id, &mut conn).await {
|
||||
if email_verified && is_email_2fa_required(data.organization_user_id, &mut conn).await {
|
||||
email::activate_email_2fa(&user, &mut conn).await.ok();
|
||||
}
|
||||
}
|
||||
@@ -305,9 +389,9 @@ async fn put_avatar(data: Json<AvatarData>, headers: Headers, mut conn: DbConn)
|
||||
Ok(Json(user.to_json(&mut conn).await))
|
||||
}
|
||||
|
||||
#[get("/users/<uuid>/public-key")]
|
||||
async fn get_public_keys(uuid: &str, _headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
let user = match User::find_by_uuid(uuid, &mut 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 {
|
||||
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),
|
||||
@@ -366,7 +450,12 @@ async fn post_password(data: Json<ChangePassData>, headers: Headers, mut conn: D
|
||||
&data.new_master_password_hash,
|
||||
Some(data.key),
|
||||
true,
|
||||
Some(vec![String::from("post_rotatekey"), String::from("get_contacts"), String::from("get_public_keys")]),
|
||||
Some(vec![
|
||||
String::from("post_rotatekey"),
|
||||
String::from("get_contacts"),
|
||||
String::from("get_public_keys"),
|
||||
String::from("get_api_webauthn"),
|
||||
]),
|
||||
);
|
||||
|
||||
let save_result = user.save(&mut conn).await;
|
||||
@@ -374,7 +463,7 @@ async fn post_password(data: Json<ChangePassData>, headers: Headers, mut conn: D
|
||||
// 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)).await;
|
||||
nt.send_logout(&user, Some(headers.device.uuid.clone())).await;
|
||||
|
||||
save_result
|
||||
}
|
||||
@@ -434,7 +523,7 @@ async fn post_kdf(data: Json<ChangeKdfData>, headers: Headers, mut conn: DbConn,
|
||||
user.set_password(&data.new_master_password_hash, Some(data.key), true, None);
|
||||
let save_result = user.save(&mut conn).await;
|
||||
|
||||
nt.send_logout(&user, Some(headers.device.uuid)).await;
|
||||
nt.send_logout(&user, Some(headers.device.uuid.clone())).await;
|
||||
|
||||
save_result
|
||||
}
|
||||
@@ -445,21 +534,21 @@ struct UpdateFolderData {
|
||||
// There is a bug in 2024.3.x which adds a `null` item.
|
||||
// To bypass this we allow a Option here, but skip it during the updates
|
||||
// See: https://github.com/bitwarden/clients/issues/8453
|
||||
id: Option<String>,
|
||||
id: Option<FolderId>,
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct UpdateEmergencyAccessData {
|
||||
id: String,
|
||||
id: EmergencyAccessId,
|
||||
key_encrypted: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct UpdateResetPasswordData {
|
||||
organization_id: String,
|
||||
organization_id: OrganizationId,
|
||||
reset_password_key: String,
|
||||
}
|
||||
|
||||
@@ -484,48 +573,49 @@ fn validate_keydata(
|
||||
existing_ciphers: &[Cipher],
|
||||
existing_folders: &[Folder],
|
||||
existing_emergency_access: &[EmergencyAccess],
|
||||
existing_user_orgs: &[UserOrganization],
|
||||
existing_memberships: &[Membership],
|
||||
existing_sends: &[Send],
|
||||
) -> EmptyResult {
|
||||
// Check that we're correctly rotating all the user's ciphers
|
||||
let existing_cipher_ids = existing_ciphers.iter().map(|c| c.uuid.as_str()).collect::<HashSet<_>>();
|
||||
let existing_cipher_ids = existing_ciphers.iter().map(|c| &c.uuid).collect::<HashSet<&CipherId>>();
|
||||
let provided_cipher_ids = data
|
||||
.ciphers
|
||||
.iter()
|
||||
.filter(|c| c.organization_id.is_none())
|
||||
.filter_map(|c| c.id.as_deref())
|
||||
.collect::<HashSet<_>>();
|
||||
.filter_map(|c| c.id.as_ref())
|
||||
.collect::<HashSet<&CipherId>>();
|
||||
if !provided_cipher_ids.is_superset(&existing_cipher_ids) {
|
||||
err!("All existing ciphers must be included in the rotation")
|
||||
}
|
||||
|
||||
// Check that we're correctly rotating all the user's folders
|
||||
let existing_folder_ids = existing_folders.iter().map(|f| f.uuid.as_str()).collect::<HashSet<_>>();
|
||||
let provided_folder_ids = data.folders.iter().filter_map(|f| f.id.as_deref()).collect::<HashSet<_>>();
|
||||
let existing_folder_ids = existing_folders.iter().map(|f| &f.uuid).collect::<HashSet<&FolderId>>();
|
||||
let provided_folder_ids = data.folders.iter().filter_map(|f| f.id.as_ref()).collect::<HashSet<&FolderId>>();
|
||||
if !provided_folder_ids.is_superset(&existing_folder_ids) {
|
||||
err!("All existing folders must be included in the rotation")
|
||||
}
|
||||
|
||||
// Check that we're correctly rotating all the user's emergency access keys
|
||||
let existing_emergency_access_ids =
|
||||
existing_emergency_access.iter().map(|ea| ea.uuid.as_str()).collect::<HashSet<_>>();
|
||||
existing_emergency_access.iter().map(|ea| &ea.uuid).collect::<HashSet<&EmergencyAccessId>>();
|
||||
let provided_emergency_access_ids =
|
||||
data.emergency_access_keys.iter().map(|ea| ea.id.as_str()).collect::<HashSet<_>>();
|
||||
data.emergency_access_keys.iter().map(|ea| &ea.id).collect::<HashSet<&EmergencyAccessId>>();
|
||||
if !provided_emergency_access_ids.is_superset(&existing_emergency_access_ids) {
|
||||
err!("All existing emergency access keys must be included in the rotation")
|
||||
}
|
||||
|
||||
// Check that we're correctly rotating all the user's reset password keys
|
||||
let existing_reset_password_ids = existing_user_orgs.iter().map(|uo| uo.org_uuid.as_str()).collect::<HashSet<_>>();
|
||||
let existing_reset_password_ids =
|
||||
existing_memberships.iter().map(|m| &m.org_uuid).collect::<HashSet<&OrganizationId>>();
|
||||
let provided_reset_password_ids =
|
||||
data.reset_password_keys.iter().map(|rp| rp.organization_id.as_str()).collect::<HashSet<_>>();
|
||||
data.reset_password_keys.iter().map(|rp| &rp.organization_id).collect::<HashSet<&OrganizationId>>();
|
||||
if !provided_reset_password_ids.is_superset(&existing_reset_password_ids) {
|
||||
err!("All existing reset password keys must be included in the rotation")
|
||||
}
|
||||
|
||||
// Check that we're correctly rotating all the user's sends
|
||||
let existing_send_ids = existing_sends.iter().map(|s| s.uuid.as_str()).collect::<HashSet<_>>();
|
||||
let provided_send_ids = data.sends.iter().filter_map(|s| s.id.as_deref()).collect::<HashSet<_>>();
|
||||
let existing_send_ids = existing_sends.iter().map(|s| &s.uuid).collect::<HashSet<&SendId>>();
|
||||
let provided_send_ids = data.sends.iter().filter_map(|s| s.id.as_ref()).collect::<HashSet<&SendId>>();
|
||||
if !provided_send_ids.is_superset(&existing_send_ids) {
|
||||
err!("All existing sends must be included in the rotation")
|
||||
}
|
||||
@@ -548,24 +638,24 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn,
|
||||
// TODO: See if we can optimize the whole cipher adding/importing and prevent duplicate code and checks.
|
||||
Cipher::validate_cipher_data(&data.ciphers)?;
|
||||
|
||||
let user_uuid = &headers.user.uuid;
|
||||
let user_id = &headers.user.uuid;
|
||||
|
||||
// TODO: Ideally we'd do everything after this point in a single transaction.
|
||||
|
||||
let mut existing_ciphers = Cipher::find_owned_by_user(user_uuid, &mut conn).await;
|
||||
let mut existing_folders = Folder::find_by_user(user_uuid, &mut conn).await;
|
||||
let mut existing_emergency_access = EmergencyAccess::find_all_by_grantor_uuid(user_uuid, &mut conn).await;
|
||||
let mut existing_user_orgs = UserOrganization::find_by_user(user_uuid, &mut conn).await;
|
||||
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;
|
||||
// We only rotate the reset password key if it is set.
|
||||
existing_user_orgs.retain(|uo| uo.reset_password_key.is_some());
|
||||
let mut existing_sends = Send::find_by_user(user_uuid, &mut conn).await;
|
||||
existing_memberships.retain(|m| m.reset_password_key.is_some());
|
||||
let mut existing_sends = Send::find_by_user(user_id, &mut conn).await;
|
||||
|
||||
validate_keydata(
|
||||
&data,
|
||||
&existing_ciphers,
|
||||
&existing_folders,
|
||||
&existing_emergency_access,
|
||||
&existing_user_orgs,
|
||||
&existing_memberships,
|
||||
&existing_sends,
|
||||
)?;
|
||||
|
||||
@@ -574,9 +664,8 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn,
|
||||
// Skip `null` folder id entries.
|
||||
// See: https://github.com/bitwarden/clients/issues/8453
|
||||
if let Some(folder_id) = folder_data.id {
|
||||
let saved_folder = match existing_folders.iter_mut().find(|f| f.uuid == folder_id) {
|
||||
Some(folder) => folder,
|
||||
None => err!("Folder doesn't exist"),
|
||||
let Some(saved_folder) = existing_folders.iter_mut().find(|f| f.uuid == folder_id) else {
|
||||
err!("Folder doesn't exist")
|
||||
};
|
||||
|
||||
saved_folder.name = folder_data.name;
|
||||
@@ -586,11 +675,11 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn,
|
||||
|
||||
// Update emergency access data
|
||||
for emergency_access_data in data.emergency_access_keys {
|
||||
let saved_emergency_access =
|
||||
match existing_emergency_access.iter_mut().find(|ea| ea.uuid == emergency_access_data.id) {
|
||||
Some(emergency_access) => emergency_access,
|
||||
None => err!("Emergency access doesn't exist or is not owned by the user"),
|
||||
};
|
||||
let Some(saved_emergency_access) =
|
||||
existing_emergency_access.iter_mut().find(|ea| ea.uuid == emergency_access_data.id)
|
||||
else {
|
||||
err!("Emergency access doesn't exist or is not owned by the user")
|
||||
};
|
||||
|
||||
saved_emergency_access.key_encrypted = Some(emergency_access_data.key_encrypted);
|
||||
saved_emergency_access.save(&mut conn).await?
|
||||
@@ -598,21 +687,20 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn,
|
||||
|
||||
// Update reset password data
|
||||
for reset_password_data in data.reset_password_keys {
|
||||
let user_org = match existing_user_orgs.iter_mut().find(|uo| uo.org_uuid == reset_password_data.organization_id)
|
||||
{
|
||||
Some(reset_password) => reset_password,
|
||||
None => err!("Reset password doesn't exist"),
|
||||
let Some(membership) =
|
||||
existing_memberships.iter_mut().find(|m| m.org_uuid == reset_password_data.organization_id)
|
||||
else {
|
||||
err!("Reset password doesn't exist")
|
||||
};
|
||||
|
||||
user_org.reset_password_key = Some(reset_password_data.reset_password_key);
|
||||
user_org.save(&mut conn).await?
|
||||
membership.reset_password_key = Some(reset_password_data.reset_password_key);
|
||||
membership.save(&mut conn).await?
|
||||
}
|
||||
|
||||
// Update send data
|
||||
for send_data in data.sends {
|
||||
let send = match existing_sends.iter_mut().find(|s| &s.uuid == send_data.id.as_ref().unwrap()) {
|
||||
Some(send) => send,
|
||||
None => err!("Send doesn't exist"),
|
||||
let Some(send) = existing_sends.iter_mut().find(|s| &s.uuid == send_data.id.as_ref().unwrap()) else {
|
||||
err!("Send doesn't exist")
|
||||
};
|
||||
|
||||
update_send_from_data(send, send_data, &headers, &mut conn, &nt, UpdateType::None).await?;
|
||||
@@ -623,9 +711,9 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn,
|
||||
|
||||
for cipher_data in data.ciphers {
|
||||
if cipher_data.organization_id.is_none() {
|
||||
let saved_cipher = match existing_ciphers.iter_mut().find(|c| &c.uuid == cipher_data.id.as_ref().unwrap()) {
|
||||
Some(cipher) => cipher,
|
||||
None => err!("Cipher doesn't exist"),
|
||||
let Some(saved_cipher) = existing_ciphers.iter_mut().find(|c| &c.uuid == cipher_data.id.as_ref().unwrap())
|
||||
else {
|
||||
err!("Cipher doesn't exist")
|
||||
};
|
||||
|
||||
// Prevent triggering cipher updates via WebSockets by settings UpdateType::None
|
||||
@@ -647,7 +735,7 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn,
|
||||
// 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)).await;
|
||||
nt.send_logout(&user, Some(headers.device.uuid.clone())).await;
|
||||
|
||||
save_result
|
||||
}
|
||||
@@ -700,10 +788,10 @@ async fn post_email_token(data: Json<EmailTokenData>, headers: Headers, mut conn
|
||||
|
||||
if CONFIG.mail_enabled() {
|
||||
if let Err(e) = mail::send_change_email(&data.new_email, &token).await {
|
||||
error!("Error sending change-email email: {:#?}", e);
|
||||
error!("Error sending change-email email: {e:#?}");
|
||||
}
|
||||
} else {
|
||||
debug!("Email change request for user ({}) to email ({}) with token ({})", user.uuid, data.new_email, token);
|
||||
debug!("Email change request for user ({}) to email ({}) with token ({token})", user.uuid, data.new_email);
|
||||
}
|
||||
|
||||
user.email_new = Some(data.new_email);
|
||||
@@ -785,7 +873,7 @@ async fn post_verify_email(headers: Headers) -> EmptyResult {
|
||||
}
|
||||
|
||||
if let Err(e) = mail::send_verify_email(&user.email, &user.uuid).await {
|
||||
error!("Error sending verify_email email: {:#?}", e);
|
||||
error!("Error sending verify_email email: {e:#?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -794,7 +882,7 @@ async fn post_verify_email(headers: Headers) -> EmptyResult {
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct VerifyEmailTokenData {
|
||||
user_id: String,
|
||||
user_id: UserId,
|
||||
token: String,
|
||||
}
|
||||
|
||||
@@ -802,23 +890,21 @@ struct VerifyEmailTokenData {
|
||||
async fn post_verify_email_token(data: Json<VerifyEmailTokenData>, mut conn: DbConn) -> EmptyResult {
|
||||
let data: VerifyEmailTokenData = data.into_inner();
|
||||
|
||||
let mut user = match User::find_by_uuid(&data.user_id, &mut conn).await {
|
||||
Some(user) => user,
|
||||
None => err!("User doesn't exist"),
|
||||
let Some(mut user) = User::find_by_uuid(&data.user_id, &mut conn).await else {
|
||||
err!("User doesn't exist")
|
||||
};
|
||||
|
||||
let claims = match decode_verify_email(&data.token) {
|
||||
Ok(claims) => claims,
|
||||
Err(_) => err!("Invalid claim"),
|
||||
let Ok(claims) = decode_verify_email(&data.token) else {
|
||||
err!("Invalid claim")
|
||||
};
|
||||
if claims.sub != user.uuid {
|
||||
if claims.sub != *user.uuid {
|
||||
err!("Invalid claim");
|
||||
}
|
||||
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 {
|
||||
error!("Error saving email verification: {:#?}", e);
|
||||
error!("Error saving email verification: {e:#?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -837,7 +923,7 @@ async fn post_delete_recover(data: Json<DeleteRecoverData>, mut conn: DbConn) ->
|
||||
if CONFIG.mail_enabled() {
|
||||
if let Some(user) = User::find_by_mail(&data.email, &mut conn).await {
|
||||
if let Err(e) = mail::send_delete_account(&user.email, &user.uuid).await {
|
||||
error!("Error sending delete account email: {:#?}", e);
|
||||
error!("Error sending delete account email: {e:#?}");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -853,7 +939,7 @@ async fn post_delete_recover(data: Json<DeleteRecoverData>, mut conn: DbConn) ->
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct DeleteRecoverTokenData {
|
||||
user_id: String,
|
||||
user_id: UserId,
|
||||
token: String,
|
||||
}
|
||||
|
||||
@@ -861,16 +947,15 @@ struct DeleteRecoverTokenData {
|
||||
async fn post_delete_recover_token(data: Json<DeleteRecoverTokenData>, mut conn: DbConn) -> EmptyResult {
|
||||
let data: DeleteRecoverTokenData = data.into_inner();
|
||||
|
||||
let user = match User::find_by_uuid(&data.user_id, &mut conn).await {
|
||||
Some(user) => user,
|
||||
None => err!("User doesn't exist"),
|
||||
let Ok(claims) = decode_delete(&data.token) else {
|
||||
err!("Invalid claim")
|
||||
};
|
||||
|
||||
let claims = match decode_delete(&data.token) {
|
||||
Ok(claims) => claims,
|
||||
Err(_) => err!("Invalid claim"),
|
||||
let Some(user) = User::find_by_uuid(&data.user_id, &mut conn).await else {
|
||||
err!("User doesn't exist")
|
||||
};
|
||||
if claims.sub != user.uuid {
|
||||
|
||||
if claims.sub != *user.uuid {
|
||||
err!("Invalid claim");
|
||||
}
|
||||
user.delete(&mut conn).await
|
||||
@@ -922,9 +1007,9 @@ async fn password_hint(data: Json<PasswordHintData>, mut conn: DbConn) -> EmptyR
|
||||
// paths that send mail take noticeably longer than ones that
|
||||
// don't. Add a randomized sleep to mitigate this somewhat.
|
||||
use rand::{rngs::SmallRng, Rng, SeedableRng};
|
||||
let mut rng = SmallRng::from_entropy();
|
||||
let mut rng = SmallRng::from_os_rng();
|
||||
let delta: i32 = 100;
|
||||
let sleep_ms = (1_000 + rng.gen_range(-delta..=delta)) as u64;
|
||||
let sleep_ms = (1_000 + rng.random_range(-delta..=delta)) as u64;
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(sleep_ms)).await;
|
||||
Ok(())
|
||||
} else {
|
||||
@@ -1032,7 +1117,7 @@ async fn get_known_device(device: KnownDevice, mut conn: DbConn) -> JsonResult {
|
||||
|
||||
struct KnownDevice {
|
||||
email: String,
|
||||
uuid: String,
|
||||
uuid: DeviceId,
|
||||
}
|
||||
|
||||
#[rocket::async_trait]
|
||||
@@ -1041,11 +1126,8 @@ impl<'r> FromRequest<'r> for KnownDevice {
|
||||
|
||||
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||
let email = if let Some(email_b64) = req.headers().get_one("X-Request-Email") {
|
||||
let email_bytes = match data_encoding::BASE64URL_NOPAD.decode(email_b64.as_bytes()) {
|
||||
Ok(bytes) => bytes,
|
||||
Err(_) => {
|
||||
return Outcome::Error((Status::BadRequest, "X-Request-Email value failed to decode as base64url"));
|
||||
}
|
||||
let Ok(email_bytes) = data_encoding::BASE64URL_NOPAD.decode(email_b64.as_bytes()) else {
|
||||
return Outcome::Error((Status::BadRequest, "X-Request-Email value failed to decode as base64url"));
|
||||
};
|
||||
match String::from_utf8(email_bytes) {
|
||||
Ok(email) => email,
|
||||
@@ -1058,7 +1140,7 @@ impl<'r> FromRequest<'r> for KnownDevice {
|
||||
};
|
||||
|
||||
let uuid = if let Some(uuid) = req.headers().get_one("X-Device-Identifier") {
|
||||
uuid.to_string()
|
||||
uuid.to_string().into()
|
||||
} else {
|
||||
return Outcome::Error((Status::BadRequest, "X-Device-Identifier value is required"));
|
||||
};
|
||||
@@ -1070,32 +1152,57 @@ 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;
|
||||
let devices = devices.iter().map(|device| device.to_json()).collect::<Vec<Value>>();
|
||||
|
||||
Ok(Json(json!({
|
||||
"data": devices,
|
||||
"continuationToken": null,
|
||||
"object": "list"
|
||||
})))
|
||||
}
|
||||
|
||||
#[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 {
|
||||
err!("No device found");
|
||||
};
|
||||
Ok(Json(device.to_json()))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct PushToken {
|
||||
push_token: String,
|
||||
}
|
||||
|
||||
#[post("/devices/identifier/<uuid>/token", data = "<data>")]
|
||||
async fn post_device_token(uuid: &str, data: Json<PushToken>, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||
put_device_token(uuid, data, headers, conn).await
|
||||
#[post("/devices/identifier/<device_id>/token", data = "<data>")]
|
||||
async fn post_device_token(device_id: DeviceId, data: Json<PushToken>, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||
put_device_token(device_id, data, headers, conn).await
|
||||
}
|
||||
|
||||
#[put("/devices/identifier/<uuid>/token", data = "<data>")]
|
||||
async fn put_device_token(uuid: &str, data: Json<PushToken>, headers: Headers, mut conn: DbConn) -> EmptyResult {
|
||||
#[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 {
|
||||
let data = data.into_inner();
|
||||
let token = data.push_token;
|
||||
|
||||
let mut device = match Device::find_by_uuid_and_user(&headers.device.uuid, &headers.user.uuid, &mut conn).await {
|
||||
Some(device) => device,
|
||||
None => err!(format!("Error: device {uuid} should be present before a token can be assigned")),
|
||||
let Some(mut device) = Device::find_by_uuid_and_user(&headers.device.uuid, &headers.user.uuid, &mut conn).await
|
||||
else {
|
||||
err!(format!("Error: device {device_id} should be present before a token can be assigned"))
|
||||
};
|
||||
|
||||
// if the device already has been registered
|
||||
if device.is_registered() {
|
||||
// check if the new token is the same as the registered token
|
||||
if device.push_token.is_some() && device.push_token.unwrap() == token.clone() {
|
||||
debug!("Device {} is already registered and token is the same", uuid);
|
||||
debug!("Device {device_id} is already registered and token is the same");
|
||||
return Ok(());
|
||||
} else {
|
||||
// Try to unregister already registered device
|
||||
@@ -1114,8 +1221,8 @@ async fn put_device_token(uuid: &str, data: Json<PushToken>, headers: Headers, m
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[put("/devices/identifier/<uuid>/clear-token")]
|
||||
async fn put_clear_device_token(uuid: &str, mut conn: DbConn) -> EmptyResult {
|
||||
#[put("/devices/identifier/<device_id>/clear-token")]
|
||||
async fn put_clear_device_token(device_id: DeviceId, mut conn: DbConn) -> EmptyResult {
|
||||
// This only clears push token
|
||||
// https://github.com/bitwarden/core/blob/master/src/Api/Controllers/DevicesController.cs#L109
|
||||
// https://github.com/bitwarden/core/blob/master/src/Core/Services/Implementations/DeviceService.cs#L37
|
||||
@@ -1124,8 +1231,8 @@ async fn put_clear_device_token(uuid: &str, mut conn: DbConn) -> EmptyResult {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(device) = Device::find_by_uuid(uuid, &mut conn).await {
|
||||
Device::clear_push_token_by_uuid(uuid, &mut conn).await?;
|
||||
if let Some(device) = Device::find_by_uuid(&device_id, &mut conn).await {
|
||||
Device::clear_push_token_by_uuid(&device_id, &mut conn).await?;
|
||||
unregister_push_device(device.push_uuid).await?;
|
||||
}
|
||||
|
||||
@@ -1133,16 +1240,16 @@ async fn put_clear_device_token(uuid: &str, mut conn: DbConn) -> EmptyResult {
|
||||
}
|
||||
|
||||
// On upstream server, both PUT and POST are declared. Implementing the POST method in case it would be useful somewhere
|
||||
#[post("/devices/identifier/<uuid>/clear-token")]
|
||||
async fn post_clear_device_token(uuid: &str, conn: DbConn) -> EmptyResult {
|
||||
put_clear_device_token(uuid, conn).await
|
||||
#[post("/devices/identifier/<device_id>/clear-token")]
|
||||
async fn post_clear_device_token(device_id: DeviceId, conn: DbConn) -> EmptyResult {
|
||||
put_clear_device_token(device_id, conn).await
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct AuthRequestRequest {
|
||||
access_code: String,
|
||||
device_identifier: String,
|
||||
device_identifier: DeviceId,
|
||||
email: String,
|
||||
public_key: String,
|
||||
// Not used for now
|
||||
@@ -1159,9 +1266,8 @@ async fn post_auth_request(
|
||||
) -> JsonResult {
|
||||
let data = data.into_inner();
|
||||
|
||||
let user = match User::find_by_mail(&data.email, &mut conn).await {
|
||||
Some(user) => user,
|
||||
None => err!("AuthRequest doesn't exist", "User not found"),
|
||||
let Some(user) = User::find_by_mail(&data.email, &mut conn).await else {
|
||||
err!("AuthRequest doesn't exist", "User not found")
|
||||
};
|
||||
|
||||
// Validate device uuid and type
|
||||
@@ -1182,6 +1288,15 @@ async fn post_auth_request(
|
||||
|
||||
nt.send_auth_request(&user.uuid, &auth_request.uuid, &data.device_identifier, &mut conn).await;
|
||||
|
||||
log_user_event(
|
||||
EventType::UserRequestedDeviceApproval as i32,
|
||||
&user.uuid,
|
||||
client_headers.device_type,
|
||||
&client_headers.ip.ip,
|
||||
&mut conn,
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(Json(json!({
|
||||
"id": auth_request.uuid,
|
||||
"publicKey": auth_request.public_key,
|
||||
@@ -1197,21 +1312,17 @@ async fn post_auth_request(
|
||||
})))
|
||||
}
|
||||
|
||||
#[get("/auth-requests/<uuid>")]
|
||||
async fn get_auth_request(uuid: &str, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
let auth_request = match AuthRequest::find_by_uuid(uuid, &mut conn).await {
|
||||
Some(auth_request) => auth_request,
|
||||
None => err!("AuthRequest doesn't exist", "Record not found"),
|
||||
#[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
|
||||
else {
|
||||
err!("AuthRequest doesn't exist", "Record not found or user uuid does not match")
|
||||
};
|
||||
|
||||
if headers.user.uuid != auth_request.user_uuid {
|
||||
err!("AuthRequest doesn't exist", "User uuid's do not match")
|
||||
}
|
||||
|
||||
let response_date_utc = auth_request.response_date.map(|response_date| format_date(&response_date));
|
||||
|
||||
Ok(Json(json!({
|
||||
"id": uuid,
|
||||
"id": &auth_request_id,
|
||||
"publicKey": auth_request.public_key,
|
||||
"requestDeviceType": DeviceType::from_i32(auth_request.device_type).to_string(),
|
||||
"requestIpAddress": auth_request.request_ip,
|
||||
@@ -1228,15 +1339,15 @@ async fn get_auth_request(uuid: &str, headers: Headers, mut conn: DbConn) -> Jso
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct AuthResponseRequest {
|
||||
device_identifier: String,
|
||||
device_identifier: DeviceId,
|
||||
key: String,
|
||||
master_password_hash: Option<String>,
|
||||
request_approved: bool,
|
||||
}
|
||||
|
||||
#[put("/auth-requests/<uuid>", data = "<data>")]
|
||||
#[put("/auth-requests/<auth_request_id>", data = "<data>")]
|
||||
async fn put_auth_request(
|
||||
uuid: &str,
|
||||
auth_request_id: AuthRequestId,
|
||||
data: Json<AuthResponseRequest>,
|
||||
headers: Headers,
|
||||
mut conn: DbConn,
|
||||
@@ -1244,15 +1355,12 @@ async fn put_auth_request(
|
||||
nt: Notify<'_>,
|
||||
) -> JsonResult {
|
||||
let data = data.into_inner();
|
||||
let mut auth_request: AuthRequest = match AuthRequest::find_by_uuid(uuid, &mut conn).await {
|
||||
Some(auth_request) => auth_request,
|
||||
None => err!("AuthRequest doesn't exist", "Record not found"),
|
||||
let Some(mut auth_request) =
|
||||
AuthRequest::find_by_uuid_and_user(&auth_request_id, &headers.user.uuid, &mut conn).await
|
||||
else {
|
||||
err!("AuthRequest doesn't exist", "Record not found or user uuid does not match")
|
||||
};
|
||||
|
||||
if headers.user.uuid != auth_request.user_uuid {
|
||||
err!("AuthRequest doesn't exist", "User uuid's do not match")
|
||||
}
|
||||
|
||||
if auth_request.approved.is_some() {
|
||||
err!("An authentication request with the same device already exists")
|
||||
}
|
||||
@@ -1269,14 +1377,31 @@ async fn put_auth_request(
|
||||
auth_request.save(&mut 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, data.device_identifier, &mut conn).await;
|
||||
nt.send_auth_response(&auth_request.user_uuid, &auth_request.uuid, &data.device_identifier, &mut conn).await;
|
||||
|
||||
log_user_event(
|
||||
EventType::OrganizationUserApprovedAuthRequest as i32,
|
||||
&headers.user.uuid,
|
||||
headers.device.atype,
|
||||
&headers.ip.ip,
|
||||
&mut conn,
|
||||
)
|
||||
.await;
|
||||
} else {
|
||||
// If denied, there's no reason to keep the request
|
||||
auth_request.delete(&mut conn).await?;
|
||||
log_user_event(
|
||||
EventType::OrganizationUserRejectedAuthRequest as i32,
|
||||
&headers.user.uuid,
|
||||
headers.device.atype,
|
||||
&headers.ip.ip,
|
||||
&mut conn,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(Json(json!({
|
||||
"id": uuid,
|
||||
"id": &auth_request_id,
|
||||
"publicKey": auth_request.public_key,
|
||||
"requestDeviceType": DeviceType::from_i32(auth_request.device_type).to_string(),
|
||||
"requestIpAddress": auth_request.request_ip,
|
||||
@@ -1290,16 +1415,15 @@ async fn put_auth_request(
|
||||
})))
|
||||
}
|
||||
|
||||
#[get("/auth-requests/<uuid>/response?<code>")]
|
||||
#[get("/auth-requests/<auth_request_id>/response?<code>")]
|
||||
async fn get_auth_request_response(
|
||||
uuid: &str,
|
||||
auth_request_id: AuthRequestId,
|
||||
code: &str,
|
||||
client_headers: ClientHeaders,
|
||||
mut conn: DbConn,
|
||||
) -> JsonResult {
|
||||
let auth_request = match AuthRequest::find_by_uuid(uuid, &mut conn).await {
|
||||
Some(auth_request) => auth_request,
|
||||
None => err!("AuthRequest doesn't exist", "User not found"),
|
||||
let Some(auth_request) = AuthRequest::find_by_uuid(&auth_request_id, &mut conn).await else {
|
||||
err!("AuthRequest doesn't exist", "User not found")
|
||||
};
|
||||
|
||||
if auth_request.device_type != client_headers.device_type
|
||||
@@ -1312,7 +1436,7 @@ async fn get_auth_request_response(
|
||||
let response_date_utc = auth_request.response_date.map(|response_date| format_date(&response_date));
|
||||
|
||||
Ok(Json(json!({
|
||||
"id": uuid,
|
||||
"id": &auth_request_id,
|
||||
"publicKey": auth_request.public_key,
|
||||
"requestDeviceType": DeviceType::from_i32(auth_request.device_type).to_string(),
|
||||
"requestIpAddress": auth_request.request_ip,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -93,10 +93,10 @@ async fn get_grantees(headers: Headers, mut conn: DbConn) -> Json<Value> {
|
||||
}
|
||||
|
||||
#[get("/emergency-access/<emer_id>")]
|
||||
async fn get_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
async fn get_emergency_access(emer_id: EmergencyAccessId, headers: Headers, mut 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, &mut conn).await {
|
||||
Some(emergency_access) => Ok(Json(
|
||||
emergency_access.to_json_grantee_details(&mut conn).await.expect("Grantee user should exist but does not!"),
|
||||
)),
|
||||
@@ -118,7 +118,7 @@ struct EmergencyAccessUpdateData {
|
||||
|
||||
#[put("/emergency-access/<emer_id>", data = "<data>")]
|
||||
async fn put_emergency_access(
|
||||
emer_id: &str,
|
||||
emer_id: EmergencyAccessId,
|
||||
data: Json<EmergencyAccessUpdateData>,
|
||||
headers: Headers,
|
||||
conn: DbConn,
|
||||
@@ -128,7 +128,7 @@ async fn put_emergency_access(
|
||||
|
||||
#[post("/emergency-access/<emer_id>", data = "<data>")]
|
||||
async fn post_emergency_access(
|
||||
emer_id: &str,
|
||||
emer_id: EmergencyAccessId,
|
||||
data: Json<EmergencyAccessUpdateData>,
|
||||
headers: Headers,
|
||||
mut conn: DbConn,
|
||||
@@ -137,11 +137,11 @@ async fn post_emergency_access(
|
||||
|
||||
let data: EmergencyAccessUpdateData = data.into_inner();
|
||||
|
||||
let mut emergency_access =
|
||||
match EmergencyAccess::find_by_uuid_and_grantor_uuid(emer_id, &headers.user.uuid, &mut conn).await {
|
||||
Some(emergency_access) => emergency_access,
|
||||
None => err!("Emergency access not valid."),
|
||||
};
|
||||
let Some(mut emergency_access) =
|
||||
EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &mut conn).await
|
||||
else {
|
||||
err!("Emergency access not valid.")
|
||||
};
|
||||
|
||||
let new_type = match EmergencyAccessType::from_str(&data.r#type.into_string()) {
|
||||
Some(new_type) => new_type as i32,
|
||||
@@ -163,12 +163,12 @@ async fn post_emergency_access(
|
||||
// region delete
|
||||
|
||||
#[delete("/emergency-access/<emer_id>")]
|
||||
async fn delete_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> EmptyResult {
|
||||
async fn delete_emergency_access(emer_id: EmergencyAccessId, headers: Headers, mut 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, &mut conn).await,
|
||||
EmergencyAccess::find_by_uuid_and_grantee_uuid(&emer_id, &headers.user.uuid, &mut conn).await,
|
||||
) {
|
||||
(Some(grantor_emer), None) => {
|
||||
info!("Grantor deleted emergency access {emer_id}");
|
||||
@@ -186,7 +186,7 @@ async fn delete_emergency_access(emer_id: &str, headers: Headers, mut conn: DbCo
|
||||
}
|
||||
|
||||
#[post("/emergency-access/<emer_id>/delete")]
|
||||
async fn post_delete_emergency_access(emer_id: &str, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||
async fn post_delete_emergency_access(emer_id: EmergencyAccessId, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||
delete_emergency_access(emer_id, headers, conn).await
|
||||
}
|
||||
|
||||
@@ -227,7 +227,7 @@ async fn send_invite(data: Json<EmergencyAccessInviteData>, headers: Headers, mu
|
||||
let (grantee_user, new_user) = match User::find_by_mail(&email, &mut conn).await {
|
||||
None => {
|
||||
if !CONFIG.invitations_allowed() {
|
||||
err!(format!("Grantee user does not exist: {}", &email))
|
||||
err!(format!("Grantee user does not exist: {email}"))
|
||||
}
|
||||
|
||||
if !CONFIG.is_email_domain_allowed(&email) {
|
||||
@@ -266,8 +266,8 @@ async fn send_invite(data: Json<EmergencyAccessInviteData>, headers: Headers, mu
|
||||
if CONFIG.mail_enabled() {
|
||||
mail::send_emergency_access_invite(
|
||||
&new_emergency_access.email.expect("Grantee email does not exists"),
|
||||
&grantee_user.uuid,
|
||||
&new_emergency_access.uuid,
|
||||
grantee_user.uuid,
|
||||
new_emergency_access.uuid,
|
||||
&grantor_user.name,
|
||||
&grantor_user.email,
|
||||
)
|
||||
@@ -281,27 +281,25 @@ async fn send_invite(data: Json<EmergencyAccessInviteData>, headers: Headers, mu
|
||||
}
|
||||
|
||||
#[post("/emergency-access/<emer_id>/reinvite")]
|
||||
async fn resend_invite(emer_id: &str, headers: Headers, mut conn: DbConn) -> EmptyResult {
|
||||
async fn resend_invite(emer_id: EmergencyAccessId, headers: Headers, mut conn: DbConn) -> EmptyResult {
|
||||
check_emergency_access_enabled()?;
|
||||
|
||||
let mut emergency_access =
|
||||
match EmergencyAccess::find_by_uuid_and_grantor_uuid(emer_id, &headers.user.uuid, &mut conn).await {
|
||||
Some(emer) => emer,
|
||||
None => err!("Emergency access not valid."),
|
||||
};
|
||||
let Some(mut emergency_access) =
|
||||
EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &mut conn).await
|
||||
else {
|
||||
err!("Emergency access not valid.")
|
||||
};
|
||||
|
||||
if emergency_access.status != EmergencyAccessStatus::Invited as i32 {
|
||||
err!("The grantee user is already accepted or confirmed to the organization");
|
||||
}
|
||||
|
||||
let email = match emergency_access.email.clone() {
|
||||
Some(email) => email,
|
||||
None => err!("Email not valid."),
|
||||
let Some(email) = emergency_access.email.clone() else {
|
||||
err!("Email not valid.")
|
||||
};
|
||||
|
||||
let grantee_user = match User::find_by_mail(&email, &mut conn).await {
|
||||
Some(user) => user,
|
||||
None => err!("Grantee user not found."),
|
||||
let Some(grantee_user) = User::find_by_mail(&email, &mut conn).await else {
|
||||
err!("Grantee user not found.")
|
||||
};
|
||||
|
||||
let grantor_user = headers.user;
|
||||
@@ -309,8 +307,8 @@ async fn resend_invite(emer_id: &str, headers: Headers, mut conn: DbConn) -> Emp
|
||||
if CONFIG.mail_enabled() {
|
||||
mail::send_emergency_access_invite(
|
||||
&email,
|
||||
&grantor_user.uuid,
|
||||
&emergency_access.uuid,
|
||||
grantor_user.uuid,
|
||||
emergency_access.uuid,
|
||||
&grantor_user.name,
|
||||
&grantor_user.email,
|
||||
)
|
||||
@@ -333,7 +331,12 @@ struct AcceptData {
|
||||
}
|
||||
|
||||
#[post("/emergency-access/<emer_id>/accept", data = "<data>")]
|
||||
async fn accept_invite(emer_id: &str, data: Json<AcceptData>, headers: Headers, mut conn: DbConn) -> EmptyResult {
|
||||
async fn accept_invite(
|
||||
emer_id: EmergencyAccessId,
|
||||
data: Json<AcceptData>,
|
||||
headers: Headers,
|
||||
mut conn: DbConn,
|
||||
) -> EmptyResult {
|
||||
check_emergency_access_enabled()?;
|
||||
|
||||
let data: AcceptData = data.into_inner();
|
||||
@@ -356,16 +359,15 @@ async fn accept_invite(emer_id: &str, data: Json<AcceptData>, headers: Headers,
|
||||
|
||||
// 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 mut emergency_access =
|
||||
match EmergencyAccess::find_by_uuid_and_grantee_email(emer_id, &headers.user.email, &mut conn).await {
|
||||
Some(emer) => emer,
|
||||
None => err!("Emergency access not valid."),
|
||||
};
|
||||
let Some(mut emergency_access) =
|
||||
EmergencyAccess::find_by_uuid_and_grantee_email(&emer_id, &headers.user.email, &mut conn).await
|
||||
else {
|
||||
err!("Emergency access not valid.")
|
||||
};
|
||||
|
||||
// get grantor user to send Accepted email
|
||||
let grantor_user = match User::find_by_uuid(&emergency_access.grantor_uuid, &mut conn).await {
|
||||
Some(user) => user,
|
||||
None => err!("Grantor user not found."),
|
||||
let Some(grantor_user) = User::find_by_uuid(&emergency_access.grantor_uuid, &mut conn).await else {
|
||||
err!("Grantor user not found.")
|
||||
};
|
||||
|
||||
if emer_id == claims.emer_id
|
||||
@@ -392,7 +394,7 @@ struct ConfirmData {
|
||||
|
||||
#[post("/emergency-access/<emer_id>/confirm", data = "<data>")]
|
||||
async fn confirm_emergency_access(
|
||||
emer_id: &str,
|
||||
emer_id: EmergencyAccessId,
|
||||
data: Json<ConfirmData>,
|
||||
headers: Headers,
|
||||
mut conn: DbConn,
|
||||
@@ -403,11 +405,11 @@ async fn confirm_emergency_access(
|
||||
let data: ConfirmData = data.into_inner();
|
||||
let key = data.key;
|
||||
|
||||
let mut emergency_access =
|
||||
match EmergencyAccess::find_by_uuid_and_grantor_uuid(emer_id, &confirming_user.uuid, &mut conn).await {
|
||||
Some(emer) => emer,
|
||||
None => err!("Emergency access not valid."),
|
||||
};
|
||||
let Some(mut emergency_access) =
|
||||
EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &confirming_user.uuid, &mut conn).await
|
||||
else {
|
||||
err!("Emergency access not valid.")
|
||||
};
|
||||
|
||||
if emergency_access.status != EmergencyAccessStatus::Accepted as i32
|
||||
|| emergency_access.grantor_uuid != confirming_user.uuid
|
||||
@@ -415,15 +417,13 @@ async fn confirm_emergency_access(
|
||||
err!("Emergency access not valid.")
|
||||
}
|
||||
|
||||
let grantor_user = match User::find_by_uuid(&confirming_user.uuid, &mut conn).await {
|
||||
Some(user) => user,
|
||||
None => err!("Grantor user not found."),
|
||||
let Some(grantor_user) = User::find_by_uuid(&confirming_user.uuid, &mut conn).await else {
|
||||
err!("Grantor user not found.")
|
||||
};
|
||||
|
||||
if let Some(grantee_uuid) = emergency_access.grantee_uuid.as_ref() {
|
||||
let grantee_user = match User::find_by_uuid(grantee_uuid, &mut conn).await {
|
||||
Some(user) => user,
|
||||
None => err!("Grantee user not found."),
|
||||
let Some(grantee_user) = User::find_by_uuid(grantee_uuid, &mut conn).await else {
|
||||
err!("Grantee user not found.")
|
||||
};
|
||||
|
||||
emergency_access.status = EmergencyAccessStatus::Confirmed as i32;
|
||||
@@ -446,23 +446,22 @@ async fn confirm_emergency_access(
|
||||
// region access emergency access
|
||||
|
||||
#[post("/emergency-access/<emer_id>/initiate")]
|
||||
async fn initiate_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
async fn initiate_emergency_access(emer_id: EmergencyAccessId, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
check_emergency_access_enabled()?;
|
||||
|
||||
let initiating_user = headers.user;
|
||||
let mut emergency_access =
|
||||
match EmergencyAccess::find_by_uuid_and_grantee_uuid(emer_id, &initiating_user.uuid, &mut conn).await {
|
||||
Some(emer) => emer,
|
||||
None => err!("Emergency access not valid."),
|
||||
};
|
||||
let Some(mut emergency_access) =
|
||||
EmergencyAccess::find_by_uuid_and_grantee_uuid(&emer_id, &initiating_user.uuid, &mut conn).await
|
||||
else {
|
||||
err!("Emergency access not valid.")
|
||||
};
|
||||
|
||||
if emergency_access.status != EmergencyAccessStatus::Confirmed as i32 {
|
||||
err!("Emergency access not valid.")
|
||||
}
|
||||
|
||||
let grantor_user = match User::find_by_uuid(&emergency_access.grantor_uuid, &mut conn).await {
|
||||
Some(user) => user,
|
||||
None => err!("Grantor user not found."),
|
||||
let Some(grantor_user) = User::find_by_uuid(&emergency_access.grantor_uuid, &mut conn).await else {
|
||||
err!("Grantor user not found.")
|
||||
};
|
||||
|
||||
let now = Utc::now().naive_utc();
|
||||
@@ -485,28 +484,26 @@ async fn initiate_emergency_access(emer_id: &str, headers: Headers, mut conn: Db
|
||||
}
|
||||
|
||||
#[post("/emergency-access/<emer_id>/approve")]
|
||||
async fn approve_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
async fn approve_emergency_access(emer_id: EmergencyAccessId, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
check_emergency_access_enabled()?;
|
||||
|
||||
let mut emergency_access =
|
||||
match EmergencyAccess::find_by_uuid_and_grantor_uuid(emer_id, &headers.user.uuid, &mut conn).await {
|
||||
Some(emer) => emer,
|
||||
None => err!("Emergency access not valid."),
|
||||
};
|
||||
let Some(mut emergency_access) =
|
||||
EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &mut conn).await
|
||||
else {
|
||||
err!("Emergency access not valid.")
|
||||
};
|
||||
|
||||
if emergency_access.status != EmergencyAccessStatus::RecoveryInitiated as i32 {
|
||||
err!("Emergency access not valid.")
|
||||
}
|
||||
|
||||
let grantor_user = match User::find_by_uuid(&headers.user.uuid, &mut conn).await {
|
||||
Some(user) => user,
|
||||
None => err!("Grantor user not found."),
|
||||
let Some(grantor_user) = User::find_by_uuid(&headers.user.uuid, &mut conn).await else {
|
||||
err!("Grantor user not found.")
|
||||
};
|
||||
|
||||
if let Some(grantee_uuid) = emergency_access.grantee_uuid.as_ref() {
|
||||
let grantee_user = match User::find_by_uuid(grantee_uuid, &mut conn).await {
|
||||
Some(user) => user,
|
||||
None => err!("Grantee user not found."),
|
||||
let Some(grantee_user) = User::find_by_uuid(grantee_uuid, &mut conn).await else {
|
||||
err!("Grantee user not found.")
|
||||
};
|
||||
|
||||
emergency_access.status = EmergencyAccessStatus::RecoveryApproved as i32;
|
||||
@@ -522,14 +519,14 @@ async fn approve_emergency_access(emer_id: &str, headers: Headers, mut conn: DbC
|
||||
}
|
||||
|
||||
#[post("/emergency-access/<emer_id>/reject")]
|
||||
async fn reject_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
async fn reject_emergency_access(emer_id: EmergencyAccessId, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
check_emergency_access_enabled()?;
|
||||
|
||||
let mut emergency_access =
|
||||
match EmergencyAccess::find_by_uuid_and_grantor_uuid(emer_id, &headers.user.uuid, &mut conn).await {
|
||||
Some(emer) => emer,
|
||||
None => err!("Emergency access not valid."),
|
||||
};
|
||||
let Some(mut emergency_access) =
|
||||
EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &mut conn).await
|
||||
else {
|
||||
err!("Emergency access not valid.")
|
||||
};
|
||||
|
||||
if emergency_access.status != EmergencyAccessStatus::RecoveryInitiated as i32
|
||||
&& emergency_access.status != EmergencyAccessStatus::RecoveryApproved as i32
|
||||
@@ -538,9 +535,8 @@ async fn reject_emergency_access(emer_id: &str, headers: Headers, mut conn: DbCo
|
||||
}
|
||||
|
||||
if let Some(grantee_uuid) = emergency_access.grantee_uuid.as_ref() {
|
||||
let grantee_user = match User::find_by_uuid(grantee_uuid, &mut conn).await {
|
||||
Some(user) => user,
|
||||
None => err!("Grantee user not found."),
|
||||
let Some(grantee_user) = User::find_by_uuid(grantee_uuid, &mut conn).await else {
|
||||
err!("Grantee user not found.")
|
||||
};
|
||||
|
||||
emergency_access.status = EmergencyAccessStatus::Confirmed as i32;
|
||||
@@ -560,14 +556,14 @@ async fn reject_emergency_access(emer_id: &str, headers: Headers, mut conn: DbCo
|
||||
// region action
|
||||
|
||||
#[post("/emergency-access/<emer_id>/view")]
|
||||
async fn view_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
async fn view_emergency_access(emer_id: EmergencyAccessId, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
check_emergency_access_enabled()?;
|
||||
|
||||
let emergency_access =
|
||||
match EmergencyAccess::find_by_uuid_and_grantee_uuid(emer_id, &headers.user.uuid, &mut conn).await {
|
||||
Some(emer) => emer,
|
||||
None => err!("Emergency access not valid."),
|
||||
};
|
||||
let Some(emergency_access) =
|
||||
EmergencyAccess::find_by_uuid_and_grantee_uuid(&emer_id, &headers.user.uuid, &mut conn).await
|
||||
else {
|
||||
err!("Emergency access not valid.")
|
||||
};
|
||||
|
||||
if !is_valid_request(&emergency_access, &headers.user.uuid, EmergencyAccessType::View) {
|
||||
err!("Emergency access not valid.")
|
||||
@@ -598,23 +594,22 @@ async fn view_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn
|
||||
}
|
||||
|
||||
#[post("/emergency-access/<emer_id>/takeover")]
|
||||
async fn takeover_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
async fn takeover_emergency_access(emer_id: EmergencyAccessId, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
check_emergency_access_enabled()?;
|
||||
|
||||
let requesting_user = headers.user;
|
||||
let emergency_access =
|
||||
match EmergencyAccess::find_by_uuid_and_grantee_uuid(emer_id, &requesting_user.uuid, &mut conn).await {
|
||||
Some(emer) => emer,
|
||||
None => err!("Emergency access not valid."),
|
||||
};
|
||||
let Some(emergency_access) =
|
||||
EmergencyAccess::find_by_uuid_and_grantee_uuid(&emer_id, &requesting_user.uuid, &mut conn).await
|
||||
else {
|
||||
err!("Emergency access not valid.")
|
||||
};
|
||||
|
||||
if !is_valid_request(&emergency_access, &requesting_user.uuid, EmergencyAccessType::Takeover) {
|
||||
err!("Emergency access not valid.")
|
||||
}
|
||||
|
||||
let grantor_user = match User::find_by_uuid(&emergency_access.grantor_uuid, &mut conn).await {
|
||||
Some(user) => user,
|
||||
None => err!("Grantor user not found."),
|
||||
let Some(grantor_user) = User::find_by_uuid(&emergency_access.grantor_uuid, &mut conn).await else {
|
||||
err!("Grantor user not found.")
|
||||
};
|
||||
|
||||
let result = json!({
|
||||
@@ -638,7 +633,7 @@ struct EmergencyAccessPasswordData {
|
||||
|
||||
#[post("/emergency-access/<emer_id>/password", data = "<data>")]
|
||||
async fn password_emergency_access(
|
||||
emer_id: &str,
|
||||
emer_id: EmergencyAccessId,
|
||||
data: Json<EmergencyAccessPasswordData>,
|
||||
headers: Headers,
|
||||
mut conn: DbConn,
|
||||
@@ -650,19 +645,18 @@ async fn password_emergency_access(
|
||||
//let key = &data.Key;
|
||||
|
||||
let requesting_user = headers.user;
|
||||
let emergency_access =
|
||||
match EmergencyAccess::find_by_uuid_and_grantee_uuid(emer_id, &requesting_user.uuid, &mut conn).await {
|
||||
Some(emer) => emer,
|
||||
None => err!("Emergency access not valid."),
|
||||
};
|
||||
let Some(emergency_access) =
|
||||
EmergencyAccess::find_by_uuid_and_grantee_uuid(&emer_id, &requesting_user.uuid, &mut conn).await
|
||||
else {
|
||||
err!("Emergency access not valid.")
|
||||
};
|
||||
|
||||
if !is_valid_request(&emergency_access, &requesting_user.uuid, EmergencyAccessType::Takeover) {
|
||||
err!("Emergency access not valid.")
|
||||
}
|
||||
|
||||
let mut grantor_user = match User::find_by_uuid(&emergency_access.grantor_uuid, &mut conn).await {
|
||||
Some(user) => user,
|
||||
None => err!("Grantor user not found."),
|
||||
let Some(mut grantor_user) = User::find_by_uuid(&emergency_access.grantor_uuid, &mut conn).await else {
|
||||
err!("Grantor user not found.")
|
||||
};
|
||||
|
||||
// change grantor_user password
|
||||
@@ -673,9 +667,9 @@ async fn password_emergency_access(
|
||||
TwoFactor::delete_all_by_user(&grantor_user.uuid, &mut conn).await?;
|
||||
|
||||
// Remove grantor from all organisations unless Owner
|
||||
for user_org in UserOrganization::find_any_state_by_user(&grantor_user.uuid, &mut conn).await {
|
||||
if user_org.atype != UserOrgType::Owner as i32 {
|
||||
user_org.delete(&mut conn).await?;
|
||||
for member in Membership::find_any_state_by_user(&grantor_user.uuid, &mut conn).await {
|
||||
if member.atype != MembershipType::Owner as i32 {
|
||||
member.delete(&mut conn).await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -684,21 +678,20 @@ async fn password_emergency_access(
|
||||
// endregion
|
||||
|
||||
#[get("/emergency-access/<emer_id>/policies")]
|
||||
async fn policies_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
async fn policies_emergency_access(emer_id: EmergencyAccessId, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
let requesting_user = headers.user;
|
||||
let emergency_access =
|
||||
match EmergencyAccess::find_by_uuid_and_grantee_uuid(emer_id, &requesting_user.uuid, &mut conn).await {
|
||||
Some(emer) => emer,
|
||||
None => err!("Emergency access not valid."),
|
||||
};
|
||||
let Some(emergency_access) =
|
||||
EmergencyAccess::find_by_uuid_and_grantee_uuid(&emer_id, &requesting_user.uuid, &mut conn).await
|
||||
else {
|
||||
err!("Emergency access not valid.")
|
||||
};
|
||||
|
||||
if !is_valid_request(&emergency_access, &requesting_user.uuid, EmergencyAccessType::Takeover) {
|
||||
err!("Emergency access not valid.")
|
||||
}
|
||||
|
||||
let grantor_user = match User::find_by_uuid(&emergency_access.grantor_uuid, &mut conn).await {
|
||||
Some(user) => user,
|
||||
None => err!("Grantor user not found."),
|
||||
let Some(grantor_user) = User::find_by_uuid(&emergency_access.grantor_uuid, &mut conn).await else {
|
||||
err!("Grantor user not found.")
|
||||
};
|
||||
|
||||
let policies = OrgPolicy::find_confirmed_by_user(&grantor_user.uuid, &mut conn);
|
||||
@@ -713,11 +706,11 @@ async fn policies_emergency_access(emer_id: &str, headers: Headers, mut conn: Db
|
||||
|
||||
fn is_valid_request(
|
||||
emergency_access: &EmergencyAccess,
|
||||
requesting_user_uuid: &str,
|
||||
requesting_user_id: &UserId,
|
||||
requested_access_type: EmergencyAccessType,
|
||||
) -> bool {
|
||||
emergency_access.grantee_uuid.is_some()
|
||||
&& emergency_access.grantee_uuid.as_ref().unwrap() == requesting_user_uuid
|
||||
&& emergency_access.grantee_uuid.as_ref().unwrap() == requesting_user_id
|
||||
&& emergency_access.status == EmergencyAccessStatus::RecoveryApproved as i32
|
||||
&& emergency_access.atype == requested_access_type as i32
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::{
|
||||
api::{EmptyResult, JsonResult},
|
||||
auth::{AdminHeaders, Headers},
|
||||
db::{
|
||||
models::{Cipher, Event, UserOrganization},
|
||||
models::{Cipher, CipherId, Event, Membership, MembershipId, OrganizationId, UserId},
|
||||
DbConn, DbPool,
|
||||
},
|
||||
util::parse_date,
|
||||
@@ -31,7 +31,16 @@ struct EventRange {
|
||||
|
||||
// Upstream: https://github.com/bitwarden/server/blob/9ecf69d9cabce732cf2c57976dd9afa5728578fb/src/Api/Controllers/EventsController.cs#LL84C35-L84C41
|
||||
#[get("/organizations/<org_id>/events?<data..>")]
|
||||
async fn get_org_events(org_id: &str, data: EventRange, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult {
|
||||
async fn get_org_events(
|
||||
org_id: OrganizationId,
|
||||
data: EventRange,
|
||||
headers: AdminHeaders,
|
||||
mut conn: DbConn,
|
||||
) -> JsonResult {
|
||||
if org_id != headers.org_id {
|
||||
err!("Organization not found", "Organization id's do not match");
|
||||
}
|
||||
|
||||
// Return an empty vec when we org events are disabled.
|
||||
// This prevents client errors
|
||||
let events_json: Vec<Value> = if !CONFIG.org_events_enabled() {
|
||||
@@ -44,7 +53,7 @@ async fn get_org_events(org_id: &str, data: EventRange, _headers: AdminHeaders,
|
||||
parse_date(&data.end)
|
||||
};
|
||||
|
||||
Event::find_by_organization_uuid(org_id, &start_date, &end_date, &mut conn)
|
||||
Event::find_by_organization_uuid(&org_id, &start_date, &end_date, &mut conn)
|
||||
.await
|
||||
.iter()
|
||||
.map(|e| e.to_json())
|
||||
@@ -59,14 +68,14 @@ async fn get_org_events(org_id: &str, data: EventRange, _headers: AdminHeaders,
|
||||
}
|
||||
|
||||
#[get("/ciphers/<cipher_id>/events?<data..>")]
|
||||
async fn get_cipher_events(cipher_id: &str, data: EventRange, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
async fn get_cipher_events(cipher_id: CipherId, data: EventRange, headers: Headers, mut 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 UserOrganization::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, &mut conn).await {
|
||||
let start_date = parse_date(&data.start);
|
||||
let end_date = if let Some(before_date) = &data.continuation_token {
|
||||
parse_date(before_date)
|
||||
@@ -74,7 +83,7 @@ async fn get_cipher_events(cipher_id: &str, data: EventRange, headers: Headers,
|
||||
parse_date(&data.end)
|
||||
};
|
||||
|
||||
events_json = Event::find_by_cipher_uuid(cipher_id, &start_date, &end_date, &mut conn)
|
||||
events_json = Event::find_by_cipher_uuid(&cipher_id, &start_date, &end_date, &mut conn)
|
||||
.await
|
||||
.iter()
|
||||
.map(|e| e.to_json())
|
||||
@@ -90,14 +99,17 @@ async fn get_cipher_events(cipher_id: &str, data: EventRange, headers: Headers,
|
||||
})))
|
||||
}
|
||||
|
||||
#[get("/organizations/<org_id>/users/<user_org_id>/events?<data..>")]
|
||||
#[get("/organizations/<org_id>/users/<member_id>/events?<data..>")]
|
||||
async fn get_user_events(
|
||||
org_id: &str,
|
||||
user_org_id: &str,
|
||||
org_id: OrganizationId,
|
||||
member_id: MembershipId,
|
||||
data: EventRange,
|
||||
_headers: AdminHeaders,
|
||||
headers: AdminHeaders,
|
||||
mut conn: DbConn,
|
||||
) -> JsonResult {
|
||||
if org_id != headers.org_id {
|
||||
err!("Organization not found", "Organization id's do not match");
|
||||
}
|
||||
// Return an empty vec when we org events are disabled.
|
||||
// This prevents client errors
|
||||
let events_json: Vec<Value> = if !CONFIG.org_events_enabled() {
|
||||
@@ -110,7 +122,7 @@ async fn get_user_events(
|
||||
parse_date(&data.end)
|
||||
};
|
||||
|
||||
Event::find_by_org_and_user_org(org_id, user_org_id, &start_date, &end_date, &mut conn)
|
||||
Event::find_by_org_and_member(&org_id, &member_id, &start_date, &end_date, &mut conn)
|
||||
.await
|
||||
.iter()
|
||||
.map(|e| e.to_json())
|
||||
@@ -152,8 +164,8 @@ struct EventCollection {
|
||||
date: String,
|
||||
|
||||
// Optional
|
||||
cipher_id: Option<String>,
|
||||
organization_id: Option<String>,
|
||||
cipher_id: Option<CipherId>,
|
||||
organization_id: Option<OrganizationId>,
|
||||
}
|
||||
|
||||
// Upstream:
|
||||
@@ -180,11 +192,11 @@ async fn post_events_collect(data: Json<Vec<EventCollection>>, headers: Headers,
|
||||
.await;
|
||||
}
|
||||
1600..=1699 => {
|
||||
if let Some(org_uuid) = &event.organization_id {
|
||||
if let Some(org_id) = &event.organization_id {
|
||||
_log_event(
|
||||
event.r#type,
|
||||
org_uuid,
|
||||
org_uuid,
|
||||
org_id,
|
||||
org_id,
|
||||
&headers.user.uuid,
|
||||
headers.device.atype,
|
||||
Some(event_date),
|
||||
@@ -197,11 +209,11 @@ async fn post_events_collect(data: Json<Vec<EventCollection>>, headers: Headers,
|
||||
_ => {
|
||||
if let Some(cipher_uuid) = &event.cipher_id {
|
||||
if let Some(cipher) = Cipher::find_by_uuid(cipher_uuid, &mut conn).await {
|
||||
if let Some(org_uuid) = cipher.organization_uuid {
|
||||
if let Some(org_id) = cipher.organization_uuid {
|
||||
_log_event(
|
||||
event.r#type,
|
||||
cipher_uuid,
|
||||
&org_uuid,
|
||||
&org_id,
|
||||
&headers.user.uuid,
|
||||
headers.device.atype,
|
||||
Some(event_date),
|
||||
@@ -218,38 +230,39 @@ async fn post_events_collect(data: Json<Vec<EventCollection>>, headers: Headers,
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn log_user_event(event_type: i32, user_uuid: &str, 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: &mut DbConn) {
|
||||
if !CONFIG.org_events_enabled() {
|
||||
return;
|
||||
}
|
||||
_log_user_event(event_type, user_uuid, device_type, None, ip, conn).await;
|
||||
_log_user_event(event_type, user_id, device_type, None, ip, conn).await;
|
||||
}
|
||||
|
||||
async fn _log_user_event(
|
||||
event_type: i32,
|
||||
user_uuid: &str,
|
||||
user_id: &UserId,
|
||||
device_type: i32,
|
||||
event_date: Option<NaiveDateTime>,
|
||||
ip: &IpAddr,
|
||||
conn: &mut DbConn,
|
||||
) {
|
||||
let orgs = UserOrganization::get_org_uuid_by_user(user_uuid, conn).await;
|
||||
let mut events: Vec<Event> = Vec::with_capacity(orgs.len() + 1); // We need an event per org and one without an org
|
||||
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
|
||||
|
||||
// Upstream saves the event also without any org_uuid.
|
||||
// Upstream saves the event also without any org_id.
|
||||
let mut event = Event::new(event_type, event_date);
|
||||
event.user_uuid = Some(String::from(user_uuid));
|
||||
event.act_user_uuid = Some(String::from(user_uuid));
|
||||
event.user_uuid = Some(user_id.clone());
|
||||
event.act_user_uuid = Some(user_id.clone());
|
||||
event.device_type = Some(device_type);
|
||||
event.ip_address = Some(ip.to_string());
|
||||
events.push(event);
|
||||
|
||||
// For each org a user is a member of store these events per org
|
||||
for org_uuid in orgs {
|
||||
for membership in memberships {
|
||||
let mut event = Event::new(event_type, event_date);
|
||||
event.user_uuid = Some(String::from(user_uuid));
|
||||
event.org_uuid = Some(org_uuid);
|
||||
event.act_user_uuid = Some(String::from(user_uuid));
|
||||
event.user_uuid = Some(user_id.clone());
|
||||
event.org_uuid = Some(membership.org_uuid);
|
||||
event.org_user_uuid = Some(membership.uuid);
|
||||
event.act_user_uuid = Some(user_id.clone());
|
||||
event.device_type = Some(device_type);
|
||||
event.ip_address = Some(ip.to_string());
|
||||
events.push(event);
|
||||
@@ -261,8 +274,8 @@ async fn _log_user_event(
|
||||
pub async fn log_event(
|
||||
event_type: i32,
|
||||
source_uuid: &str,
|
||||
org_uuid: &str,
|
||||
act_user_uuid: &str,
|
||||
org_id: &OrganizationId,
|
||||
act_user_id: &UserId,
|
||||
device_type: i32,
|
||||
ip: &IpAddr,
|
||||
conn: &mut DbConn,
|
||||
@@ -270,15 +283,15 @@ pub async fn log_event(
|
||||
if !CONFIG.org_events_enabled() {
|
||||
return;
|
||||
}
|
||||
_log_event(event_type, source_uuid, org_uuid, act_user_uuid, device_type, None, ip, conn).await;
|
||||
_log_event(event_type, source_uuid, org_id, act_user_id, device_type, None, ip, conn).await;
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn _log_event(
|
||||
event_type: i32,
|
||||
source_uuid: &str,
|
||||
org_uuid: &str,
|
||||
act_user_uuid: &str,
|
||||
org_id: &OrganizationId,
|
||||
act_user_id: &UserId,
|
||||
device_type: i32,
|
||||
event_date: Option<NaiveDateTime>,
|
||||
ip: &IpAddr,
|
||||
@@ -290,31 +303,31 @@ async fn _log_event(
|
||||
// 1000..=1099 Are user events, they need to be logged via log_user_event()
|
||||
// Cipher Events
|
||||
1100..=1199 => {
|
||||
event.cipher_uuid = Some(String::from(source_uuid));
|
||||
event.cipher_uuid = Some(source_uuid.to_string().into());
|
||||
}
|
||||
// Collection Events
|
||||
1300..=1399 => {
|
||||
event.collection_uuid = Some(String::from(source_uuid));
|
||||
event.collection_uuid = Some(source_uuid.to_string().into());
|
||||
}
|
||||
// Group Events
|
||||
1400..=1499 => {
|
||||
event.group_uuid = Some(String::from(source_uuid));
|
||||
event.group_uuid = Some(source_uuid.to_string().into());
|
||||
}
|
||||
// Org User Events
|
||||
1500..=1599 => {
|
||||
event.org_user_uuid = Some(String::from(source_uuid));
|
||||
event.org_user_uuid = Some(source_uuid.to_string().into());
|
||||
}
|
||||
// 1600..=1699 Are organizational events, and they do not need the source_uuid
|
||||
// Policy Events
|
||||
1700..=1799 => {
|
||||
event.policy_uuid = Some(String::from(source_uuid));
|
||||
event.policy_uuid = Some(source_uuid.to_string().into());
|
||||
}
|
||||
// Ignore others
|
||||
_ => {}
|
||||
}
|
||||
|
||||
event.org_uuid = Some(String::from(org_uuid));
|
||||
event.act_user_uuid = Some(String::from(act_user_uuid));
|
||||
event.org_uuid = Some(org_id.clone());
|
||||
event.act_user_uuid = Some(act_user_id.clone());
|
||||
event.device_type = Some(device_type);
|
||||
event.ip_address = Some(ip.to_string());
|
||||
event.save(conn).await.unwrap_or(());
|
||||
|
||||
@@ -23,25 +23,19 @@ async fn get_folders(headers: Headers, mut conn: DbConn) -> Json<Value> {
|
||||
}))
|
||||
}
|
||||
|
||||
#[get("/folders/<uuid>")]
|
||||
async fn get_folder(uuid: &str, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
let folder = match Folder::find_by_uuid(uuid, &mut conn).await {
|
||||
Some(folder) => folder,
|
||||
_ => err!("Invalid folder"),
|
||||
};
|
||||
|
||||
if folder.user_uuid != headers.user.uuid {
|
||||
err!("Folder belongs to another user")
|
||||
#[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 {
|
||||
Some(folder) => Ok(Json(folder.to_json())),
|
||||
_ => err!("Invalid folder", "Folder does not exist or belongs to another user"),
|
||||
}
|
||||
|
||||
Ok(Json(folder.to_json()))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FolderData {
|
||||
pub name: String,
|
||||
pub id: Option<String>,
|
||||
pub id: Option<FolderId>,
|
||||
}
|
||||
|
||||
#[post("/folders", data = "<data>")]
|
||||
@@ -56,14 +50,20 @@ async fn post_folders(data: Json<FolderData>, headers: Headers, mut conn: DbConn
|
||||
Ok(Json(folder.to_json()))
|
||||
}
|
||||
|
||||
#[post("/folders/<uuid>", data = "<data>")]
|
||||
async fn post_folder(uuid: &str, data: Json<FolderData>, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult {
|
||||
put_folder(uuid, data, headers, conn, nt).await
|
||||
#[post("/folders/<folder_id>", data = "<data>")]
|
||||
async fn post_folder(
|
||||
folder_id: FolderId,
|
||||
data: Json<FolderData>,
|
||||
headers: Headers,
|
||||
conn: DbConn,
|
||||
nt: Notify<'_>,
|
||||
) -> JsonResult {
|
||||
put_folder(folder_id, data, headers, conn, nt).await
|
||||
}
|
||||
|
||||
#[put("/folders/<uuid>", data = "<data>")]
|
||||
#[put("/folders/<folder_id>", data = "<data>")]
|
||||
async fn put_folder(
|
||||
uuid: &str,
|
||||
folder_id: FolderId,
|
||||
data: Json<FolderData>,
|
||||
headers: Headers,
|
||||
mut conn: DbConn,
|
||||
@@ -71,15 +71,10 @@ async fn put_folder(
|
||||
) -> JsonResult {
|
||||
let data: FolderData = data.into_inner();
|
||||
|
||||
let mut folder = match Folder::find_by_uuid(uuid, &mut conn).await {
|
||||
Some(folder) => folder,
|
||||
_ => err!("Invalid folder"),
|
||||
let Some(mut folder) = Folder::find_by_uuid_and_user(&folder_id, &headers.user.uuid, &mut conn).await else {
|
||||
err!("Invalid folder", "Folder does not exist or belongs to another user")
|
||||
};
|
||||
|
||||
if folder.user_uuid != headers.user.uuid {
|
||||
err!("Folder belongs to another user")
|
||||
}
|
||||
|
||||
folder.name = data.name;
|
||||
|
||||
folder.save(&mut conn).await?;
|
||||
@@ -88,22 +83,17 @@ async fn put_folder(
|
||||
Ok(Json(folder.to_json()))
|
||||
}
|
||||
|
||||
#[post("/folders/<uuid>/delete")]
|
||||
async fn delete_folder_post(uuid: &str, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
||||
delete_folder(uuid, headers, conn, nt).await
|
||||
#[post("/folders/<folder_id>/delete")]
|
||||
async fn delete_folder_post(folder_id: FolderId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
||||
delete_folder(folder_id, headers, conn, nt).await
|
||||
}
|
||||
|
||||
#[delete("/folders/<uuid>")]
|
||||
async fn delete_folder(uuid: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
||||
let folder = match Folder::find_by_uuid(uuid, &mut conn).await {
|
||||
Some(folder) => folder,
|
||||
_ => err!("Invalid folder"),
|
||||
#[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 {
|
||||
err!("Invalid folder", "Folder does not exist or belongs to another user")
|
||||
};
|
||||
|
||||
if folder.user_uuid != headers.user.uuid {
|
||||
err!("Folder belongs to another user")
|
||||
}
|
||||
|
||||
// Delete the actual folder entry
|
||||
folder.delete(&mut conn).await?;
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ pub use sends::purge_sends;
|
||||
pub fn routes() -> Vec<Route> {
|
||||
let mut eq_domains_routes = routes![get_eq_domains, post_eq_domains, put_eq_domains];
|
||||
let mut hibp_routes = routes![hibp_breach];
|
||||
let mut meta_routes = routes![alive, now, version, config];
|
||||
let mut meta_routes = routes![alive, now, version, config, get_api_webauthn];
|
||||
|
||||
let mut routes = Vec::new();
|
||||
routes.append(&mut accounts::routes());
|
||||
@@ -184,6 +184,18 @@ fn version() -> Json<&'static str> {
|
||||
Json(crate::VERSION.unwrap_or_default())
|
||||
}
|
||||
|
||||
#[get("/webauthn")]
|
||||
fn get_api_webauthn(_headers: Headers) -> Json<Value> {
|
||||
// Prevent a 404 error, which also causes key-rotation issues
|
||||
// It looks like this is used when login with passkeys is enabled, which Vaultwarden does not (yet) support
|
||||
// An empty list/data also works fine
|
||||
Json(json!({
|
||||
"object": "list",
|
||||
"data": [],
|
||||
"continuationToken": null
|
||||
}))
|
||||
}
|
||||
|
||||
#[get("/config")]
|
||||
fn config() -> Json<Value> {
|
||||
let domain = crate::CONFIG.domain();
|
||||
@@ -193,13 +205,16 @@ fn config() -> Json<Value> {
|
||||
feature_states.insert("key-rotation-improvements".to_string(), true);
|
||||
feature_states.insert("flexible-collections-v-1".to_string(), false);
|
||||
|
||||
feature_states.insert("email-verification".to_string(), true);
|
||||
feature_states.insert("unauth-ui-refresh".to_string(), true);
|
||||
|
||||
Json(json!({
|
||||
// Note: The clients use this version to handle backwards compatibility concerns
|
||||
// This means they expect a version that closely matches the Bitwarden server version
|
||||
// We should make sure that we keep this updated when we support the new server features
|
||||
// Version history:
|
||||
// - Individual cipher key encryption: 2023.9.1
|
||||
"version": "2024.2.0",
|
||||
// - Individual cipher key encryption: 2024.2.0
|
||||
"version": "2025.1.0",
|
||||
"gitHash": option_env!("GIT_REV"),
|
||||
"server": {
|
||||
"name": "Vaultwarden",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -52,40 +52,36 @@ async fn ldap_import(data: Json<OrgImportData>, token: PublicToken, mut conn: Db
|
||||
let data = data.into_inner();
|
||||
|
||||
for user_data in &data.members {
|
||||
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 user_org) =
|
||||
UserOrganization::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, &mut conn).await {
|
||||
// Only revoke a user if it is not the last confirmed owner
|
||||
let revoked = if user_org.atype == UserOrgType::Owner
|
||||
&& user_org.status == UserOrgStatus::Confirmed as i32
|
||||
let revoked = if member.atype == MembershipType::Owner
|
||||
&& member.status == MembershipStatus::Confirmed as i32
|
||||
{
|
||||
if UserOrganization::count_confirmed_by_org_and_type(&org_id, UserOrgType::Owner, &mut conn).await
|
||||
<= 1
|
||||
if Membership::count_confirmed_by_org_and_type(&org_id, MembershipType::Owner, &mut conn).await <= 1
|
||||
{
|
||||
warn!("Can't revoke the last owner");
|
||||
false
|
||||
} else {
|
||||
user_org.revoke()
|
||||
member.revoke()
|
||||
}
|
||||
} else {
|
||||
user_org.revoke()
|
||||
member.revoke()
|
||||
};
|
||||
|
||||
let ext_modified = user_org.set_external_id(Some(user_data.external_id.clone()));
|
||||
let ext_modified = member.set_external_id(Some(user_data.external_id.clone()));
|
||||
if revoked || ext_modified {
|
||||
user_org.save(&mut conn).await?;
|
||||
member.save(&mut conn).await?;
|
||||
}
|
||||
}
|
||||
// If user is part of the organization, restore it
|
||||
} else if let Some(mut user_org) =
|
||||
UserOrganization::find_by_email_and_org(&user_data.email, &org_id, &mut conn).await
|
||||
{
|
||||
let restored = user_org.restore();
|
||||
let ext_modified = user_org.set_external_id(Some(user_data.external_id.clone()));
|
||||
} else if let Some(mut member) = Membership::find_by_email_and_org(&user_data.email, &org_id, &mut conn).await {
|
||||
let restored = member.restore();
|
||||
let ext_modified = member.set_external_id(Some(user_data.external_id.clone()));
|
||||
if restored || ext_modified {
|
||||
user_org.save(&mut conn).await?;
|
||||
member.save(&mut conn).await?;
|
||||
}
|
||||
} else {
|
||||
// If user is not part of the organization
|
||||
@@ -97,25 +93,25 @@ async fn ldap_import(data: Json<OrgImportData>, token: PublicToken, mut conn: Db
|
||||
new_user.save(&mut conn).await?;
|
||||
|
||||
if !CONFIG.mail_enabled() {
|
||||
let invitation = Invitation::new(&new_user.email);
|
||||
invitation.save(&mut conn).await?;
|
||||
Invitation::new(&new_user.email).save(&mut conn).await?;
|
||||
}
|
||||
user_created = true;
|
||||
new_user
|
||||
}
|
||||
};
|
||||
let user_org_status = if CONFIG.mail_enabled() || user.password_hash.is_empty() {
|
||||
UserOrgStatus::Invited as i32
|
||||
let member_status = if CONFIG.mail_enabled() || user.password_hash.is_empty() {
|
||||
MembershipStatus::Invited as i32
|
||||
} else {
|
||||
UserOrgStatus::Accepted as i32 // Automatically mark user as accepted if no email invites
|
||||
MembershipStatus::Accepted as i32 // Automatically mark user as accepted if no email invites
|
||||
};
|
||||
|
||||
let mut new_org_user = UserOrganization::new(user.uuid.clone(), org_id.clone());
|
||||
new_org_user.set_external_id(Some(user_data.external_id.clone()));
|
||||
new_org_user.access_all = false;
|
||||
new_org_user.atype = UserOrgType::User as i32;
|
||||
new_org_user.status = user_org_status;
|
||||
let mut new_member = Membership::new(user.uuid.clone(), org_id.clone());
|
||||
new_member.set_external_id(Some(user_data.external_id.clone()));
|
||||
new_member.access_all = false;
|
||||
new_member.atype = MembershipType::User as i32;
|
||||
new_member.status = member_status;
|
||||
|
||||
new_org_user.save(&mut conn).await?;
|
||||
new_member.save(&mut conn).await?;
|
||||
|
||||
if CONFIG.mail_enabled() {
|
||||
let (org_name, org_email) = match Organization::find_by_uuid(&org_id, &mut conn).await {
|
||||
@@ -123,8 +119,18 @@ async fn ldap_import(data: Json<OrgImportData>, token: PublicToken, mut conn: Db
|
||||
None => err!("Error looking up organization"),
|
||||
};
|
||||
|
||||
mail::send_invite(&user, Some(org_id.clone()), Some(new_org_user.uuid), &org_name, Some(org_email))
|
||||
.await?;
|
||||
if let Err(e) =
|
||||
mail::send_invite(&user, org_id.clone(), new_member.uuid.clone(), &org_name, Some(org_email)).await
|
||||
{
|
||||
// Upon error delete the user, invite and org member records when needed
|
||||
if user_created {
|
||||
user.delete(&mut conn).await?;
|
||||
} else {
|
||||
new_member.delete(&mut conn).await?;
|
||||
}
|
||||
|
||||
err!(format!("Error sending invite: {e:?} "));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -149,9 +155,8 @@ async fn ldap_import(data: Json<OrgImportData>, token: PublicToken, mut conn: Db
|
||||
GroupUser::delete_all_by_group(&group_uuid, &mut conn).await?;
|
||||
|
||||
for ext_id in &group_data.member_external_ids {
|
||||
if let Some(user_org) = UserOrganization::find_by_external_id_and_org(ext_id, &org_id, &mut conn).await
|
||||
{
|
||||
let mut group_user = GroupUser::new(group_uuid.clone(), user_org.uuid.clone());
|
||||
if let Some(member) = Membership::find_by_external_id_and_org(ext_id, &org_id, &mut conn).await {
|
||||
let mut group_user = GroupUser::new(group_uuid.clone(), member.uuid.clone());
|
||||
group_user.save(&mut conn).await?;
|
||||
}
|
||||
}
|
||||
@@ -164,20 +169,19 @@ 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 user_org in UserOrganization::find_by_org(&org_id, &mut conn).await {
|
||||
if let Some(ref user_external_id) = user_org.external_id {
|
||||
for member in Membership::find_by_org(&org_id, &mut conn).await {
|
||||
if let Some(ref user_external_id) = member.external_id {
|
||||
if !sync_members.contains(user_external_id) {
|
||||
if user_org.atype == UserOrgType::Owner && user_org.status == UserOrgStatus::Confirmed as i32 {
|
||||
if member.atype == MembershipType::Owner && member.status == MembershipStatus::Confirmed as i32 {
|
||||
// Removing owner, check that there is at least one other confirmed owner
|
||||
if UserOrganization::count_confirmed_by_org_and_type(&org_id, UserOrgType::Owner, &mut conn)
|
||||
.await
|
||||
if Membership::count_confirmed_by_org_and_type(&org_id, MembershipType::Owner, &mut conn).await
|
||||
<= 1
|
||||
{
|
||||
warn!("Can't delete the last owner");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
user_org.delete(&mut conn).await?;
|
||||
member.delete(&mut conn).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -186,7 +190,7 @@ async fn ldap_import(data: Json<OrgImportData>, token: PublicToken, mut conn: Db
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub struct PublicToken(String);
|
||||
pub struct PublicToken(OrganizationId);
|
||||
|
||||
#[rocket::async_trait]
|
||||
impl<'r> FromRequest<'r> for PublicToken {
|
||||
@@ -203,9 +207,8 @@ impl<'r> FromRequest<'r> for PublicToken {
|
||||
None => err_handler!("No access token provided"),
|
||||
};
|
||||
// Check JWT token is valid and get device and user from it
|
||||
let claims = match auth::decode_api_org(access_token) {
|
||||
Ok(claims) => claims,
|
||||
Err(_) => err_handler!("Invalid claim"),
|
||||
let Ok(claims) = auth::decode_api_org(access_token) else {
|
||||
err_handler!("Invalid claim")
|
||||
};
|
||||
// Check if time is between claims.nbf and claims.exp
|
||||
let time_now = Utc::now().timestamp();
|
||||
@@ -227,13 +230,12 @@ impl<'r> FromRequest<'r> for PublicToken {
|
||||
Outcome::Success(conn) => conn,
|
||||
_ => err_handler!("Error getting DB"),
|
||||
};
|
||||
let org_uuid = match claims.client_id.strip_prefix("organization.") {
|
||||
Some(uuid) => uuid,
|
||||
None => err_handler!("Malformed client_id"),
|
||||
let Some(org_id) = claims.client_id.strip_prefix("organization.") else {
|
||||
err_handler!("Malformed client_id")
|
||||
};
|
||||
let org_api_key = match OrganizationApiKey::find_by_org_uuid(org_uuid, &conn).await {
|
||||
Some(org_api_key) => org_api_key,
|
||||
None => err_handler!("Invalid client_id"),
|
||||
let org_id: OrganizationId = org_id.to_string().into();
|
||||
let Some(org_api_key) = OrganizationApiKey::find_by_org_uuid(&org_id, &conn).await else {
|
||||
err_handler!("Invalid client_id")
|
||||
};
|
||||
if org_api_key.org_uuid != claims.client_sub {
|
||||
err_handler!("Token not issued for this org");
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::{
|
||||
api::{ApiResult, EmptyResult, JsonResult, Notify, UpdateType},
|
||||
auth::{ClientIp, Headers, Host},
|
||||
db::{models::*, DbConn, DbPool},
|
||||
util::{NumberOrString, SafeString},
|
||||
util::NumberOrString,
|
||||
CONFIG,
|
||||
};
|
||||
|
||||
@@ -67,7 +67,7 @@ pub struct SendData {
|
||||
file_length: Option<NumberOrString>,
|
||||
|
||||
// Used for key rotations
|
||||
pub id: Option<String>,
|
||||
pub id: Option<SendId>,
|
||||
}
|
||||
|
||||
/// Enforces the `Disable Send` policy. A non-owner/admin user belonging to
|
||||
@@ -79,9 +79,9 @@ 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 {
|
||||
let user_uuid = &headers.user.uuid;
|
||||
let user_id = &headers.user.uuid;
|
||||
if !CONFIG.sends_allowed()
|
||||
|| OrgPolicy::is_applicable_to_user(user_uuid, OrgPolicyType::DisableSend, None, conn).await
|
||||
|| OrgPolicy::is_applicable_to_user(user_id, OrgPolicyType::DisableSend, None, conn).await
|
||||
{
|
||||
err!("Due to an Enterprise Policy, you are only able to delete an existing Send.")
|
||||
}
|
||||
@@ -95,9 +95,9 @@ async fn enforce_disable_send_policy(headers: &Headers, conn: &mut DbConn) -> Em
|
||||
///
|
||||
/// Ref: https://bitwarden.com/help/article/policies/#send-options
|
||||
async fn enforce_disable_hide_email_policy(data: &SendData, headers: &Headers, conn: &mut DbConn) -> EmptyResult {
|
||||
let user_uuid = &headers.user.uuid;
|
||||
let user_id = &headers.user.uuid;
|
||||
let hide_email = data.hide_email.unwrap_or(false);
|
||||
if hide_email && OrgPolicy::is_hide_email_disabled(user_uuid, conn).await {
|
||||
if hide_email && OrgPolicy::is_hide_email_disabled(user_id, conn).await {
|
||||
err!(
|
||||
"Due to an Enterprise Policy, you are not allowed to hide your email address \
|
||||
from recipients when creating or editing a Send."
|
||||
@@ -106,7 +106,7 @@ async fn enforce_disable_hide_email_policy(data: &SendData, headers: &Headers, c
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_send(data: SendData, user_uuid: String) -> ApiResult<Send> {
|
||||
fn create_send(data: SendData, user_id: UserId) -> ApiResult<Send> {
|
||||
let data_val = if data.r#type == SendType::Text as i32 {
|
||||
data.text
|
||||
} else if data.r#type == SendType::File as i32 {
|
||||
@@ -129,7 +129,7 @@ fn create_send(data: SendData, user_uuid: String) -> ApiResult<Send> {
|
||||
}
|
||||
|
||||
let mut send = Send::new(data.r#type, data.name, data_str, data.key, data.deletion_date.naive_utc());
|
||||
send.user_uuid = Some(user_uuid);
|
||||
send.user_uuid = Some(user_id);
|
||||
send.notes = data.notes;
|
||||
send.max_access_count = match data.max_access_count {
|
||||
Some(m) => Some(m.into_i32()?),
|
||||
@@ -157,18 +157,12 @@ async fn get_sends(headers: Headers, mut conn: DbConn) -> Json<Value> {
|
||||
}))
|
||||
}
|
||||
|
||||
#[get("/sends/<uuid>")]
|
||||
async fn get_send(uuid: &str, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
let send = match Send::find_by_uuid(uuid, &mut conn).await {
|
||||
Some(send) => send,
|
||||
None => err!("Send not found"),
|
||||
};
|
||||
|
||||
if send.user_uuid.as_ref() != Some(&headers.user.uuid) {
|
||||
err!("Send is not owned by user")
|
||||
#[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 {
|
||||
Some(send) => Ok(Json(send.to_json())),
|
||||
None => err!("Send not found", "Invalid send uuid or does not belong to user"),
|
||||
}
|
||||
|
||||
Ok(Json(send.to_json()))
|
||||
}
|
||||
|
||||
#[post("/sends", data = "<data>")]
|
||||
@@ -255,7 +249,7 @@ async fn post_send_file(data: Form<UploadData<'_>>, headers: Headers, mut conn:
|
||||
err!("Send content is not a file");
|
||||
}
|
||||
|
||||
let file_id = crate::crypto::generate_send_id();
|
||||
let file_id = crate::crypto::generate_send_file_id();
|
||||
let folder_path = tokio::fs::canonicalize(&CONFIG.sends_folder()).await?.join(&send.uuid);
|
||||
let file_path = folder_path.join(&file_id);
|
||||
tokio::fs::create_dir_all(&folder_path).await?;
|
||||
@@ -330,7 +324,7 @@ async fn post_send_file_v2(data: Json<SendData>, headers: Headers, mut conn: DbC
|
||||
|
||||
let mut send = create_send(data, headers.user.uuid)?;
|
||||
|
||||
let file_id = crate::crypto::generate_send_id();
|
||||
let file_id = crate::crypto::generate_send_file_id();
|
||||
|
||||
let mut data_value: Value = serde_json::from_str(&send.data)?;
|
||||
if let Some(o) = data_value.as_object_mut() {
|
||||
@@ -344,7 +338,7 @@ async fn post_send_file_v2(data: Json<SendData>, headers: Headers, mut conn: DbC
|
||||
Ok(Json(json!({
|
||||
"fileUploadType": 0, // 0 == Direct | 1 == Azure
|
||||
"object": "send-fileUpload",
|
||||
"url": format!("/sends/{}/file/{}", send.uuid, file_id),
|
||||
"url": format!("/sends/{}/file/{file_id}", send.uuid),
|
||||
"sendResponse": send.to_json()
|
||||
})))
|
||||
}
|
||||
@@ -352,16 +346,16 @@ async fn post_send_file_v2(data: Json<SendData>, headers: Headers, mut conn: DbC
|
||||
#[derive(Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
pub struct SendFileData {
|
||||
id: String,
|
||||
id: SendFileId,
|
||||
size: u64,
|
||||
fileName: String,
|
||||
}
|
||||
|
||||
// https://github.com/bitwarden/server/blob/66f95d1c443490b653e5a15d32977e2f5a3f9e32/src/Api/Tools/Controllers/SendsController.cs#L250
|
||||
#[post("/sends/<send_uuid>/file/<file_id>", format = "multipart/form-data", data = "<data>")]
|
||||
#[post("/sends/<send_id>/file/<file_id>", format = "multipart/form-data", data = "<data>")]
|
||||
async fn post_send_file_v2_data(
|
||||
send_uuid: &str,
|
||||
file_id: &str,
|
||||
send_id: SendId,
|
||||
file_id: SendFileId,
|
||||
data: Form<UploadDataV2<'_>>,
|
||||
headers: Headers,
|
||||
mut conn: DbConn,
|
||||
@@ -371,28 +365,24 @@ async fn post_send_file_v2_data(
|
||||
|
||||
let mut data = data.into_inner();
|
||||
|
||||
let Some(send) = Send::find_by_uuid(send_uuid, &mut conn).await else {
|
||||
err!("Send not found. Unable to save the file.")
|
||||
let Some(send) = Send::find_by_uuid_and_user(&send_id, &headers.user.uuid, &mut conn).await else {
|
||||
err!("Send not found. Unable to save the file.", "Invalid send uuid or does not belong to user.")
|
||||
};
|
||||
|
||||
if send.atype != SendType::File as i32 {
|
||||
err!("Send is not a file type send.");
|
||||
}
|
||||
|
||||
let Some(send_user_id) = &send.user_uuid else {
|
||||
err!("Sends are only supported for users at the moment.")
|
||||
};
|
||||
|
||||
if send_user_id != &headers.user.uuid {
|
||||
err!("Send doesn't belong to user.");
|
||||
}
|
||||
|
||||
let Ok(send_data) = serde_json::from_str::<SendFileData>(&send.data) else {
|
||||
err!("Unable to decode send data as json.")
|
||||
};
|
||||
|
||||
match data.data.raw_name() {
|
||||
Some(raw_file_name) if raw_file_name.dangerous_unsafe_unsanitized_raw() == send_data.fileName => (),
|
||||
Some(raw_file_name)
|
||||
if raw_file_name.dangerous_unsafe_unsanitized_raw() == send_data.fileName
|
||||
// be less strict only if using CLI, cf. https://github.com/dani-garcia/vaultwarden/issues/5614
|
||||
|| (headers.device.is_cli() && send_data.fileName.ends_with(raw_file_name.dangerous_unsafe_unsanitized_raw().as_str())
|
||||
) => {}
|
||||
Some(raw_file_name) => err!(
|
||||
"Send file name does not match.",
|
||||
format!(
|
||||
@@ -416,7 +406,7 @@ async fn post_send_file_v2_data(
|
||||
err!("Send file size does not match.", format!("Expected a file size of {} got {size}", send_data.size));
|
||||
}
|
||||
|
||||
let folder_path = tokio::fs::canonicalize(&CONFIG.sends_folder()).await?.join(send_uuid);
|
||||
let folder_path = tokio::fs::canonicalize(&CONFIG.sends_folder()).await?.join(send_id);
|
||||
let file_path = folder_path.join(file_id);
|
||||
|
||||
// Check if the file already exists, if that is the case do not overwrite it
|
||||
@@ -456,9 +446,8 @@ async fn post_access(
|
||||
ip: ClientIp,
|
||||
nt: Notify<'_>,
|
||||
) -> JsonResult {
|
||||
let mut send = match Send::find_by_access_id(access_id, &mut conn).await {
|
||||
Some(s) => s,
|
||||
None => err_code!(SEND_INACCESSIBLE_MSG, 404),
|
||||
let Some(mut send) = Send::find_by_access_id(access_id, &mut conn).await else {
|
||||
err_code!(SEND_INACCESSIBLE_MSG, 404)
|
||||
};
|
||||
|
||||
if let Some(max_access_count) = send.max_access_count {
|
||||
@@ -500,7 +489,7 @@ async fn post_access(
|
||||
UpdateType::SyncSendUpdate,
|
||||
&send,
|
||||
&send.update_users_revision(&mut conn).await,
|
||||
&String::from("00000000-0000-0000-0000-000000000000"),
|
||||
&String::from("00000000-0000-0000-0000-000000000000").into(),
|
||||
&mut conn,
|
||||
)
|
||||
.await;
|
||||
@@ -510,16 +499,15 @@ async fn post_access(
|
||||
|
||||
#[post("/sends/<send_id>/access/file/<file_id>", data = "<data>")]
|
||||
async fn post_access_file(
|
||||
send_id: &str,
|
||||
file_id: &str,
|
||||
send_id: SendId,
|
||||
file_id: SendFileId,
|
||||
data: Json<SendAccessData>,
|
||||
host: Host,
|
||||
mut conn: DbConn,
|
||||
nt: Notify<'_>,
|
||||
) -> JsonResult {
|
||||
let mut send = match Send::find_by_uuid(send_id, &mut conn).await {
|
||||
Some(s) => s,
|
||||
None => err_code!(SEND_INACCESSIBLE_MSG, 404),
|
||||
let Some(mut send) = Send::find_by_uuid(&send_id, &mut conn).await else {
|
||||
err_code!(SEND_INACCESSIBLE_MSG, 404)
|
||||
};
|
||||
|
||||
if let Some(max_access_count) = send.max_access_count {
|
||||
@@ -558,22 +546,22 @@ async fn post_access_file(
|
||||
UpdateType::SyncSendUpdate,
|
||||
&send,
|
||||
&send.update_users_revision(&mut conn).await,
|
||||
&String::from("00000000-0000-0000-0000-000000000000"),
|
||||
&String::from("00000000-0000-0000-0000-000000000000").into(),
|
||||
&mut conn,
|
||||
)
|
||||
.await;
|
||||
|
||||
let token_claims = crate::auth::generate_send_claims(send_id, file_id);
|
||||
let token_claims = crate::auth::generate_send_claims(&send_id, &file_id);
|
||||
let token = crate::auth::encode_jwt(&token_claims);
|
||||
Ok(Json(json!({
|
||||
"object": "send-fileDownload",
|
||||
"id": file_id,
|
||||
"url": format!("{}/api/sends/{}/{}?t={}", &host.host, send_id, file_id, token)
|
||||
"url": format!("{}/api/sends/{send_id}/{file_id}?t={token}", &host.host)
|
||||
})))
|
||||
}
|
||||
|
||||
#[get("/sends/<send_id>/<file_id>?<t>")]
|
||||
async fn download_send(send_id: SafeString, file_id: SafeString, t: &str) -> Option<NamedFile> {
|
||||
async fn download_send(send_id: SendId, file_id: SendFileId, t: &str) -> Option<NamedFile> {
|
||||
if let Ok(claims) = crate::auth::decode_send(t) {
|
||||
if claims.sub == format!("{send_id}/{file_id}") {
|
||||
return NamedFile::open(Path::new(&CONFIG.sends_folder()).join(send_id).join(file_id)).await.ok();
|
||||
@@ -582,16 +570,21 @@ async fn download_send(send_id: SafeString, file_id: SafeString, t: &str) -> Opt
|
||||
None
|
||||
}
|
||||
|
||||
#[put("/sends/<id>", data = "<data>")]
|
||||
async fn put_send(id: &str, data: Json<SendData>, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult {
|
||||
#[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?;
|
||||
|
||||
let data: SendData = data.into_inner();
|
||||
enforce_disable_hide_email_policy(&data, &headers, &mut conn).await?;
|
||||
|
||||
let mut send = match Send::find_by_uuid(id, &mut conn).await {
|
||||
Some(s) => s,
|
||||
None => err!("Send not found"),
|
||||
let Some(mut send) = Send::find_by_uuid_and_user(&send_id, &headers.user.uuid, &mut 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?;
|
||||
@@ -657,17 +650,12 @@ pub async fn update_send_from_data(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[delete("/sends/<id>")]
|
||||
async fn delete_send(id: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
||||
let send = match Send::find_by_uuid(id, &mut conn).await {
|
||||
Some(s) => s,
|
||||
None => err!("Send not found"),
|
||||
#[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 {
|
||||
err!("Send not found", "Invalid send uuid, or does not belong to user")
|
||||
};
|
||||
|
||||
if send.user_uuid.as_ref() != Some(&headers.user.uuid) {
|
||||
err!("Send is not owned by user")
|
||||
}
|
||||
|
||||
send.delete(&mut conn).await?;
|
||||
nt.send_send_update(
|
||||
UpdateType::SyncSendDelete,
|
||||
@@ -681,19 +669,14 @@ async fn delete_send(id: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[put("/sends/<id>/remove-password")]
|
||||
async fn put_remove_password(id: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult {
|
||||
#[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?;
|
||||
|
||||
let mut send = match Send::find_by_uuid(id, &mut conn).await {
|
||||
Some(s) => s,
|
||||
None => err!("Send not found"),
|
||||
let Some(mut send) = Send::find_by_uuid_and_user(&send_id, &headers.user.uuid, &mut conn).await else {
|
||||
err!("Send not found", "Invalid send uuid, or does not belong to user")
|
||||
};
|
||||
|
||||
if send.user_uuid.as_ref() != Some(&headers.user.uuid) {
|
||||
err!("Send is not owned by user")
|
||||
}
|
||||
|
||||
send.set_password(None);
|
||||
send.save(&mut conn).await?;
|
||||
nt.send_send_update(
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::{
|
||||
auth::{ClientIp, Headers},
|
||||
crypto,
|
||||
db::{
|
||||
models::{EventType, TwoFactor, TwoFactorType},
|
||||
models::{EventType, TwoFactor, TwoFactorType, UserId},
|
||||
DbConn,
|
||||
},
|
||||
util::NumberOrString,
|
||||
@@ -16,7 +16,7 @@ use crate::{
|
||||
pub use crate::config::CONFIG;
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![generate_authenticator, activate_authenticator, activate_authenticator_put,]
|
||||
routes![generate_authenticator, activate_authenticator, activate_authenticator_put, disable_authenticator]
|
||||
}
|
||||
|
||||
#[post("/two-factor/get-authenticator", data = "<data>")]
|
||||
@@ -95,7 +95,7 @@ async fn activate_authenticator_put(data: Json<EnableAuthenticatorData>, headers
|
||||
}
|
||||
|
||||
pub async fn validate_totp_code_str(
|
||||
user_uuid: &str,
|
||||
user_id: &UserId,
|
||||
totp_code: &str,
|
||||
secret: &str,
|
||||
ip: &ClientIp,
|
||||
@@ -105,11 +105,11 @@ pub async fn validate_totp_code_str(
|
||||
err!("TOTP code is not a number");
|
||||
}
|
||||
|
||||
validate_totp_code(user_uuid, totp_code, secret, ip, conn).await
|
||||
validate_totp_code(user_id, totp_code, secret, ip, conn).await
|
||||
}
|
||||
|
||||
pub async fn validate_totp_code(
|
||||
user_uuid: &str,
|
||||
user_id: &UserId,
|
||||
totp_code: &str,
|
||||
secret: &str,
|
||||
ip: &ClientIp,
|
||||
@@ -117,16 +117,15 @@ pub async fn validate_totp_code(
|
||||
) -> EmptyResult {
|
||||
use totp_lite::{totp_custom, Sha1};
|
||||
|
||||
let decoded_secret = match BASE32.decode(secret.as_bytes()) {
|
||||
Ok(s) => s,
|
||||
Err(_) => err!("Invalid TOTP secret"),
|
||||
let Ok(decoded_secret) = BASE32.decode(secret.as_bytes()) else {
|
||||
err!("Invalid TOTP secret")
|
||||
};
|
||||
|
||||
let mut twofactor =
|
||||
match TwoFactor::find_by_user_and_type(user_uuid, TwoFactorType::Authenticator as i32, conn).await {
|
||||
Some(tf) => tf,
|
||||
_ => TwoFactor::new(user_uuid.to_string(), TwoFactorType::Authenticator, secret.to_string()),
|
||||
};
|
||||
let mut twofactor = match TwoFactor::find_by_user_and_type(user_id, TwoFactorType::Authenticator as i32, conn).await
|
||||
{
|
||||
Some(tf) => tf,
|
||||
_ => TwoFactor::new(user_id.clone(), TwoFactorType::Authenticator, secret.to_string()),
|
||||
};
|
||||
|
||||
// The amount of steps back and forward in time
|
||||
// Also check if we need to disable time drifted TOTP codes.
|
||||
@@ -149,7 +148,7 @@ pub async fn validate_totp_code(
|
||||
if generated == totp_code && time_step > twofactor.last_used {
|
||||
// If the step does not equals 0 the time is drifted either server or client side.
|
||||
if step != 0 {
|
||||
warn!("TOTP Time drift detected. The step offset is {}", step);
|
||||
warn!("TOTP Time drift detected. The step offset is {step}");
|
||||
}
|
||||
|
||||
// Save the last used time step so only totp time steps higher then this one are allowed.
|
||||
@@ -158,7 +157,7 @@ pub async fn validate_totp_code(
|
||||
twofactor.save(conn).await?;
|
||||
return Ok(());
|
||||
} else if generated == totp_code && time_step <= twofactor.last_used {
|
||||
warn!("This TOTP or a TOTP code within {} steps back or forward has already been used!", steps);
|
||||
warn!("This TOTP or a TOTP code within {steps} steps back or forward has already been used!");
|
||||
err!(
|
||||
format!("Invalid TOTP code! Server time: {} IP: {}", current_time.format("%F %T UTC"), ip.ip),
|
||||
ErrorEvent {
|
||||
@@ -176,3 +175,47 @@ pub async fn validate_totp_code(
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct DisableAuthenticatorData {
|
||||
key: String,
|
||||
master_password_hash: String,
|
||||
r#type: NumberOrString,
|
||||
}
|
||||
|
||||
#[delete("/two-factor/authenticator", data = "<data>")]
|
||||
async fn disable_authenticator(data: Json<DisableAuthenticatorData>, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
let user = headers.user;
|
||||
let type_ = data.r#type.into_i32()?;
|
||||
|
||||
if !user.check_valid_password(&data.master_password_hash) {
|
||||
err!("Invalid password");
|
||||
}
|
||||
|
||||
if let Some(twofactor) = TwoFactor::find_by_user_and_type(&user.uuid, type_, &mut 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;
|
||||
} 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?;
|
||||
}
|
||||
|
||||
Ok(Json(json!({
|
||||
"enabled": false,
|
||||
"keys": type_,
|
||||
"object": "twoFactorProvider"
|
||||
})))
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::{
|
||||
auth::Headers,
|
||||
crypto,
|
||||
db::{
|
||||
models::{EventType, TwoFactor, TwoFactorType, User},
|
||||
models::{EventType, TwoFactor, TwoFactorType, User, UserId},
|
||||
DbConn,
|
||||
},
|
||||
error::MapResult,
|
||||
@@ -26,8 +26,8 @@ pub fn routes() -> Vec<Route> {
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct DuoData {
|
||||
host: String, // Duo API hostname
|
||||
ik: String, // integration key
|
||||
sk: String, // secret key
|
||||
ik: String, // client id
|
||||
sk: String, // client secret
|
||||
}
|
||||
|
||||
impl DuoData {
|
||||
@@ -111,8 +111,8 @@ async fn get_duo(data: Json<PasswordOrOtpData>, headers: Headers, mut conn: DbCo
|
||||
json!({
|
||||
"enabled": enabled,
|
||||
"host": data.host,
|
||||
"secretKey": data.sk,
|
||||
"integrationKey": data.ik,
|
||||
"clientSecret": data.sk,
|
||||
"clientId": data.ik,
|
||||
"object": "twoFactorDuo"
|
||||
})
|
||||
} else {
|
||||
@@ -129,8 +129,8 @@ async fn get_duo(data: Json<PasswordOrOtpData>, headers: Headers, mut conn: DbCo
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct EnableDuoData {
|
||||
host: String,
|
||||
secret_key: String,
|
||||
integration_key: String,
|
||||
client_secret: String,
|
||||
client_id: String,
|
||||
master_password_hash: Option<String>,
|
||||
otp: Option<String>,
|
||||
}
|
||||
@@ -139,8 +139,8 @@ impl From<EnableDuoData> for DuoData {
|
||||
fn from(d: EnableDuoData) -> Self {
|
||||
Self {
|
||||
host: d.host,
|
||||
ik: d.integration_key,
|
||||
sk: d.secret_key,
|
||||
ik: d.client_id,
|
||||
sk: d.client_secret,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -151,7 +151,7 @@ fn check_duo_fields_custom(data: &EnableDuoData) -> bool {
|
||||
st.is_empty() || s == DISABLED_MESSAGE_DEFAULT
|
||||
}
|
||||
|
||||
!empty_or_default(&data.host) && !empty_or_default(&data.secret_key) && !empty_or_default(&data.integration_key)
|
||||
!empty_or_default(&data.host) && !empty_or_default(&data.client_secret) && !empty_or_default(&data.client_id)
|
||||
}
|
||||
|
||||
#[post("/two-factor/duo", data = "<data>")]
|
||||
@@ -186,8 +186,8 @@ async fn activate_duo(data: Json<EnableDuoData>, headers: Headers, mut conn: DbC
|
||||
Ok(Json(json!({
|
||||
"enabled": true,
|
||||
"host": data.host,
|
||||
"secretKey": data.sk,
|
||||
"integrationKey": data.ik,
|
||||
"clientSecret": data.sk,
|
||||
"clientId": data.ik,
|
||||
"object": "twoFactorDuo"
|
||||
})))
|
||||
}
|
||||
@@ -202,7 +202,7 @@ async fn duo_api_request(method: &str, path: &str, params: &str, data: &DuoData)
|
||||
use std::str::FromStr;
|
||||
|
||||
// https://duo.com/docs/authapi#api-details
|
||||
let url = format!("https://{}{}", &data.host, path);
|
||||
let url = format!("https://{}{path}", &data.host);
|
||||
let date = Utc::now().to_rfc2822();
|
||||
let username = &data.ik;
|
||||
let fields = [&date, method, &data.host, path, params];
|
||||
@@ -228,13 +228,12 @@ const AUTH_PREFIX: &str = "AUTH";
|
||||
const DUO_PREFIX: &str = "TX";
|
||||
const APP_PREFIX: &str = "APP";
|
||||
|
||||
async fn get_user_duo_data(uuid: &str, conn: &mut DbConn) -> DuoStatus {
|
||||
async fn get_user_duo_data(user_id: &UserId, conn: &mut DbConn) -> DuoStatus {
|
||||
let type_ = TwoFactorType::Duo as i32;
|
||||
|
||||
// If the user doesn't have an entry, disabled
|
||||
let twofactor = match TwoFactor::find_by_user_and_type(uuid, type_, conn).await {
|
||||
Some(t) => t,
|
||||
None => return DuoStatus::Disabled(DuoData::global().is_some()),
|
||||
let Some(twofactor) = TwoFactor::find_by_user_and_type(user_id, type_, conn).await else {
|
||||
return DuoStatus::Disabled(DuoData::global().is_some());
|
||||
};
|
||||
|
||||
// If the user has the required values, we use those
|
||||
@@ -275,9 +274,9 @@ pub async fn generate_duo_signature(email: &str, conn: &mut DbConn) -> ApiResult
|
||||
|
||||
fn sign_duo_values(key: &str, email: &str, ikey: &str, prefix: &str, expire: i64) -> String {
|
||||
let val = format!("{email}|{ikey}|{expire}");
|
||||
let cookie = format!("{}|{}", prefix, BASE64.encode(val.as_bytes()));
|
||||
let cookie = format!("{prefix}|{}", BASE64.encode(val.as_bytes()));
|
||||
|
||||
format!("{}|{}", cookie, crypto::hmac_sign(key, &cookie))
|
||||
format!("{cookie}|{}", crypto::hmac_sign(key, &cookie))
|
||||
}
|
||||
|
||||
pub async fn validate_duo_login(email: &str, response: &str, conn: &mut DbConn) -> EmptyResult {
|
||||
@@ -333,14 +332,12 @@ fn parse_duo_values(key: &str, val: &str, ikey: &str, prefix: &str, time: i64) -
|
||||
err!("Prefixes don't match")
|
||||
}
|
||||
|
||||
let cookie_vec = match BASE64.decode(u_b64.as_bytes()) {
|
||||
Ok(c) => c,
|
||||
Err(_) => err!("Invalid Duo cookie encoding"),
|
||||
let Ok(cookie_vec) = BASE64.decode(u_b64.as_bytes()) else {
|
||||
err!("Invalid Duo cookie encoding")
|
||||
};
|
||||
|
||||
let cookie = match String::from_utf8(cookie_vec) {
|
||||
Ok(c) => c,
|
||||
Err(_) => err!("Invalid Duo cookie encoding"),
|
||||
let Ok(cookie) = String::from_utf8(cookie_vec) else {
|
||||
err!("Invalid Duo cookie encoding")
|
||||
};
|
||||
|
||||
let cookie_split: Vec<&str> = cookie.split('|').collect();
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::{
|
||||
api::{core::two_factor::duo::get_duo_keys_email, EmptyResult},
|
||||
crypto,
|
||||
db::{
|
||||
models::{EventType, TwoFactorDuoContext},
|
||||
models::{DeviceId, EventType, TwoFactorDuoContext},
|
||||
DbConn, DbPool,
|
||||
},
|
||||
error::Error,
|
||||
@@ -182,7 +182,7 @@ impl DuoClient {
|
||||
HealthCheckResponse::HealthFail {
|
||||
message,
|
||||
message_detail,
|
||||
} => err!(format!("Duo health check FAIL response, msg: {}, detail: {}", message, message_detail)),
|
||||
} => err!(format!("Duo health check FAIL response, msg: {message}, detail: {message_detail}")),
|
||||
};
|
||||
|
||||
if health_stat != "OK" {
|
||||
@@ -275,7 +275,7 @@ impl DuoClient {
|
||||
|
||||
let status_code = res.status();
|
||||
if status_code != StatusCode::OK {
|
||||
err!(format!("Failure response from Duo: {}", status_code))
|
||||
err!(format!("Failure response from Duo: {status_code}"))
|
||||
}
|
||||
|
||||
let response: IdTokenResponse = match res.json::<IdTokenResponse>().await {
|
||||
@@ -379,7 +379,7 @@ fn make_callback_url(client_name: &str) -> Result<String, Error> {
|
||||
pub async fn get_duo_auth_url(
|
||||
email: &str,
|
||||
client_id: &str,
|
||||
device_identifier: &String,
|
||||
device_identifier: &DeviceId,
|
||||
conn: &mut DbConn,
|
||||
) -> Result<String, Error> {
|
||||
let (ik, sk, _, host) = get_duo_keys_email(email, conn).await?;
|
||||
@@ -417,7 +417,7 @@ pub async fn validate_duo_login(
|
||||
email: &str,
|
||||
two_factor_token: &str,
|
||||
client_id: &str,
|
||||
device_identifier: &str,
|
||||
device_identifier: &DeviceId,
|
||||
conn: &mut DbConn,
|
||||
) -> EmptyResult {
|
||||
// Result supplied to us by clients in the form "<authz code>|<state>"
|
||||
@@ -478,7 +478,7 @@ pub async fn validate_duo_login(
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
let d: Digest = digest(&SHA512_256, format!("{}{}", ctx.nonce, device_identifier).as_bytes());
|
||||
let d: Digest = digest(&SHA512_256, format!("{}{device_identifier}", ctx.nonce).as_bytes());
|
||||
let hash: String = HEXLOWER.encode(d.as_ref());
|
||||
|
||||
match client.exchange_authz_code_for_result(code, email, hash.as_str()).await {
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::{
|
||||
auth::Headers,
|
||||
crypto,
|
||||
db::{
|
||||
models::{EventType, TwoFactor, TwoFactorType, User},
|
||||
models::{EventType, TwoFactor, TwoFactorType, User, UserId},
|
||||
DbConn,
|
||||
},
|
||||
error::{Error, MapResult},
|
||||
@@ -40,9 +40,8 @@ async fn send_email_login(data: Json<SendEmailLoginData>, mut conn: DbConn) -> E
|
||||
use crate::db::models::User;
|
||||
|
||||
// Get the user
|
||||
let user = match User::find_by_mail(&data.email, &mut conn).await {
|
||||
Some(user) => user,
|
||||
None => err!("Username or password is incorrect. Try again."),
|
||||
let Some(user) = User::find_by_mail(&data.email, &mut conn).await else {
|
||||
err!("Username or password is incorrect. Try again.")
|
||||
};
|
||||
|
||||
// Check password
|
||||
@@ -60,10 +59,9 @@ async fn send_email_login(data: Json<SendEmailLoginData>, mut conn: DbConn) -> E
|
||||
}
|
||||
|
||||
/// Generate the token, save the data for later verification and send email to user
|
||||
pub async fn send_token(user_uuid: &str, conn: &mut DbConn) -> EmptyResult {
|
||||
pub async fn send_token(user_id: &UserId, conn: &mut DbConn) -> EmptyResult {
|
||||
let type_ = TwoFactorType::Email as i32;
|
||||
let mut twofactor =
|
||||
TwoFactor::find_by_user_and_type(user_uuid, type_, conn).await.map_res("Two factor not found")?;
|
||||
let mut twofactor = TwoFactor::find_by_user_and_type(user_id, type_, conn).await.map_res("Two factor not found")?;
|
||||
|
||||
let generated_token = crypto::generate_email_token(CONFIG.email_token_size());
|
||||
|
||||
@@ -174,9 +172,8 @@ async fn email(data: Json<EmailData>, headers: Headers, mut conn: DbConn) -> Jso
|
||||
|
||||
let mut email_data = EmailTokenData::from_json(&twofactor.data)?;
|
||||
|
||||
let issued_token = match &email_data.last_token {
|
||||
Some(t) => t,
|
||||
_ => err!("No token available"),
|
||||
let Some(issued_token) = &email_data.last_token else {
|
||||
err!("No token available")
|
||||
};
|
||||
|
||||
if !crypto::ct_eq(issued_token, data.token) {
|
||||
@@ -200,19 +197,24 @@ async fn email(data: Json<EmailData>, headers: Headers, mut conn: DbConn) -> Jso
|
||||
}
|
||||
|
||||
/// Validate the email code when used as TwoFactor token mechanism
|
||||
pub async fn validate_email_code_str(user_uuid: &str, token: &str, data: &str, conn: &mut DbConn) -> EmptyResult {
|
||||
pub async fn validate_email_code_str(
|
||||
user_id: &UserId,
|
||||
token: &str,
|
||||
data: &str,
|
||||
ip: &std::net::IpAddr,
|
||||
conn: &mut DbConn,
|
||||
) -> EmptyResult {
|
||||
let mut email_data = EmailTokenData::from_json(data)?;
|
||||
let mut twofactor = TwoFactor::find_by_user_and_type(user_uuid, TwoFactorType::Email as i32, conn)
|
||||
let mut twofactor = TwoFactor::find_by_user_and_type(user_id, TwoFactorType::Email as i32, conn)
|
||||
.await
|
||||
.map_res("Two factor not found")?;
|
||||
let issued_token = match &email_data.last_token {
|
||||
Some(t) => t,
|
||||
_ => err!(
|
||||
"No token available",
|
||||
let Some(issued_token) = &email_data.last_token else {
|
||||
err!(
|
||||
format!("No token available! IP: {ip}"),
|
||||
ErrorEvent {
|
||||
event: EventType::UserFailedLogIn2fa
|
||||
}
|
||||
),
|
||||
)
|
||||
};
|
||||
|
||||
if !crypto::ct_eq(issued_token, token) {
|
||||
@@ -224,7 +226,7 @@ pub async fn validate_email_code_str(user_uuid: &str, token: &str, data: &str, c
|
||||
twofactor.save(conn).await?;
|
||||
|
||||
err!(
|
||||
"Token is invalid",
|
||||
format!("Token is invalid! IP: {ip}"),
|
||||
ErrorEvent {
|
||||
event: EventType::UserFailedLogIn2fa
|
||||
}
|
||||
@@ -327,11 +329,11 @@ pub fn obscure_email(email: &str) -> String {
|
||||
}
|
||||
};
|
||||
|
||||
format!("{}@{}", new_name, &domain)
|
||||
format!("{new_name}@{domain}")
|
||||
}
|
||||
|
||||
pub async fn find_and_activate_email_2fa(user_uuid: &str, conn: &mut DbConn) -> EmptyResult {
|
||||
if let Some(user) = User::find_by_uuid(user_uuid, conn).await {
|
||||
pub async fn find_and_activate_email_2fa(user_id: &UserId, conn: &mut DbConn) -> EmptyResult {
|
||||
if let Some(user) = User::find_by_uuid(user_id, conn).await {
|
||||
activate_email_2fa(&user, conn).await
|
||||
} else {
|
||||
err!("User not found!");
|
||||
|
||||
@@ -85,9 +85,8 @@ async fn recover(data: Json<RecoverTwoFactor>, client_headers: ClientHeaders, mu
|
||||
use crate::db::models::User;
|
||||
|
||||
// Get the user
|
||||
let mut user = match User::find_by_mail(&data.email, &mut conn).await {
|
||||
Some(user) => user,
|
||||
None => err!("Username or password is incorrect. Try again."),
|
||||
let Some(mut user) = User::find_by_mail(&data.email, &mut conn).await else {
|
||||
err!("Username or password is incorrect. Try again.")
|
||||
};
|
||||
|
||||
// Check password
|
||||
@@ -174,17 +173,16 @@ async fn disable_twofactor_put(data: Json<DisableTwoFactorData>, headers: Header
|
||||
|
||||
pub async fn enforce_2fa_policy(
|
||||
user: &User,
|
||||
act_uuid: &str,
|
||||
act_user_id: &UserId,
|
||||
device_type: i32,
|
||||
ip: &std::net::IpAddr,
|
||||
conn: &mut DbConn,
|
||||
) -> EmptyResult {
|
||||
for member in UserOrganization::find_by_user_and_policy(&user.uuid, OrgPolicyType::TwoFactorAuthentication, conn)
|
||||
.await
|
||||
.into_iter()
|
||||
for member in
|
||||
Membership::find_by_user_and_policy(&user.uuid, OrgPolicyType::TwoFactorAuthentication, conn).await.into_iter()
|
||||
{
|
||||
// Policy only applies to non-Owner/non-Admin members who have accepted joining the org
|
||||
if member.atype < UserOrgType::Admin {
|
||||
if member.atype < MembershipType::Admin {
|
||||
if CONFIG.mail_enabled() {
|
||||
let org = Organization::find_by_uuid(&member.org_uuid, conn).await.unwrap();
|
||||
mail::send_2fa_removed_from_org(&user.email, &org.name).await?;
|
||||
@@ -197,7 +195,7 @@ pub async fn enforce_2fa_policy(
|
||||
EventType::OrganizationUserRevoked as i32,
|
||||
&member.uuid,
|
||||
&member.org_uuid,
|
||||
act_uuid,
|
||||
act_user_id,
|
||||
device_type,
|
||||
ip,
|
||||
conn,
|
||||
@@ -210,16 +208,16 @@ pub async fn enforce_2fa_policy(
|
||||
}
|
||||
|
||||
pub async fn enforce_2fa_policy_for_org(
|
||||
org_uuid: &str,
|
||||
act_uuid: &str,
|
||||
org_id: &OrganizationId,
|
||||
act_user_id: &UserId,
|
||||
device_type: i32,
|
||||
ip: &std::net::IpAddr,
|
||||
conn: &mut DbConn,
|
||||
) -> EmptyResult {
|
||||
let org = Organization::find_by_uuid(org_uuid, conn).await.unwrap();
|
||||
for member in UserOrganization::find_confirmed_by_org(org_uuid, conn).await.into_iter() {
|
||||
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() {
|
||||
// Don't enforce the policy for Admins and Owners.
|
||||
if member.atype < UserOrgType::Admin && TwoFactor::find_by_user(&member.user_uuid, conn).await.is_empty() {
|
||||
if member.atype < MembershipType::Admin && TwoFactor::find_by_user(&member.user_uuid, conn).await.is_empty() {
|
||||
if CONFIG.mail_enabled() {
|
||||
let user = User::find_by_uuid(&member.user_uuid, conn).await.unwrap();
|
||||
mail::send_2fa_removed_from_org(&user.email, &org.name).await?;
|
||||
@@ -231,8 +229,8 @@ pub async fn enforce_2fa_policy_for_org(
|
||||
log_event(
|
||||
EventType::OrganizationUserRevoked as i32,
|
||||
&member.uuid,
|
||||
org_uuid,
|
||||
act_uuid,
|
||||
org_id,
|
||||
act_user_id,
|
||||
device_type,
|
||||
ip,
|
||||
conn,
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::{
|
||||
auth::Headers,
|
||||
crypto,
|
||||
db::{
|
||||
models::{TwoFactor, TwoFactorType},
|
||||
models::{TwoFactor, TwoFactorType, UserId},
|
||||
DbConn,
|
||||
},
|
||||
error::{Error, MapResult},
|
||||
@@ -104,11 +104,11 @@ async fn verify_otp(data: Json<ProtectedActionVerify>, headers: Headers, mut con
|
||||
|
||||
pub async fn validate_protected_action_otp(
|
||||
otp: &str,
|
||||
user_uuid: &str,
|
||||
user_id: &UserId,
|
||||
delete_if_valid: bool,
|
||||
conn: &mut DbConn,
|
||||
) -> EmptyResult {
|
||||
let pa = TwoFactor::find_by_user_and_type(user_uuid, TwoFactorType::ProtectedActions as i32, conn)
|
||||
let 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)?;
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::{
|
||||
},
|
||||
auth::Headers,
|
||||
db::{
|
||||
models::{EventType, TwoFactor, TwoFactorType},
|
||||
models::{EventType, TwoFactor, TwoFactorType, UserId},
|
||||
DbConn,
|
||||
},
|
||||
error::Error,
|
||||
@@ -148,7 +148,7 @@ async fn generate_webauthn_challenge(data: Json<PasswordOrOtpData>, headers: Hea
|
||||
)?;
|
||||
|
||||
let type_ = TwoFactorType::WebauthnRegisterChallenge;
|
||||
TwoFactor::new(user.uuid, type_, serde_json::to_string(&state)?).save(&mut conn).await?;
|
||||
TwoFactor::new(user.uuid.clone(), type_, serde_json::to_string(&state)?).save(&mut conn).await?;
|
||||
|
||||
let mut challenge_value = serde_json::to_value(challenge.public_key)?;
|
||||
challenge_value["status"] = "ok".into();
|
||||
@@ -309,17 +309,16 @@ async fn delete_webauthn(data: Json<DeleteU2FData>, headers: Headers, mut conn:
|
||||
err!("Invalid password");
|
||||
}
|
||||
|
||||
let mut tf =
|
||||
match TwoFactor::find_by_user_and_type(&headers.user.uuid, TwoFactorType::Webauthn as i32, &mut conn).await {
|
||||
Some(tf) => tf,
|
||||
None => err!("Webauthn data not found!"),
|
||||
};
|
||||
let Some(mut tf) =
|
||||
TwoFactor::find_by_user_and_type(&headers.user.uuid, TwoFactorType::Webauthn as i32, &mut conn).await
|
||||
else {
|
||||
err!("Webauthn data not found!")
|
||||
};
|
||||
|
||||
let mut data: Vec<WebauthnRegistration> = serde_json::from_str(&tf.data)?;
|
||||
|
||||
let item_pos = match data.iter().position(|r| r.id == id) {
|
||||
Some(p) => p,
|
||||
None => err!("Webauthn entry not found"),
|
||||
let Some(item_pos) = data.iter().position(|r| r.id == id) else {
|
||||
err!("Webauthn entry not found")
|
||||
};
|
||||
|
||||
let removed_item = data.remove(item_pos);
|
||||
@@ -353,20 +352,20 @@ async fn delete_webauthn(data: Json<DeleteU2FData>, headers: Headers, mut conn:
|
||||
}
|
||||
|
||||
pub async fn get_webauthn_registrations(
|
||||
user_uuid: &str,
|
||||
user_id: &UserId,
|
||||
conn: &mut DbConn,
|
||||
) -> Result<(bool, Vec<WebauthnRegistration>), Error> {
|
||||
let type_ = TwoFactorType::Webauthn as i32;
|
||||
match TwoFactor::find_by_user_and_type(user_uuid, type_, conn).await {
|
||||
match TwoFactor::find_by_user_and_type(user_id, type_, conn).await {
|
||||
Some(tf) => Ok((tf.enabled, serde_json::from_str(&tf.data)?)),
|
||||
None => Ok((false, Vec::new())), // If no data, return empty list
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn generate_webauthn_login(user_uuid: &str, conn: &mut DbConn) -> JsonResult {
|
||||
pub async fn generate_webauthn_login(user_id: &UserId, conn: &mut DbConn) -> JsonResult {
|
||||
// Load saved credentials
|
||||
let creds: Vec<Credential> =
|
||||
get_webauthn_registrations(user_uuid, conn).await?.1.into_iter().map(|r| r.credential).collect();
|
||||
get_webauthn_registrations(user_id, conn).await?.1.into_iter().map(|r| r.credential).collect();
|
||||
|
||||
if creds.is_empty() {
|
||||
err!("No Webauthn devices registered")
|
||||
@@ -377,7 +376,7 @@ pub async fn generate_webauthn_login(user_uuid: &str, conn: &mut DbConn) -> Json
|
||||
let (response, state) = WebauthnConfig::load().generate_challenge_authenticate_options(creds, Some(ext))?;
|
||||
|
||||
// Save the challenge state for later validation
|
||||
TwoFactor::new(user_uuid.into(), TwoFactorType::WebauthnLoginChallenge, serde_json::to_string(&state)?)
|
||||
TwoFactor::new(user_id.clone(), TwoFactorType::WebauthnLoginChallenge, serde_json::to_string(&state)?)
|
||||
.save(conn)
|
||||
.await?;
|
||||
|
||||
@@ -385,9 +384,9 @@ pub async fn generate_webauthn_login(user_uuid: &str, conn: &mut DbConn) -> Json
|
||||
Ok(Json(serde_json::to_value(response.public_key)?))
|
||||
}
|
||||
|
||||
pub async fn validate_webauthn_login(user_uuid: &str, response: &str, conn: &mut DbConn) -> EmptyResult {
|
||||
pub async fn validate_webauthn_login(user_id: &UserId, response: &str, conn: &mut DbConn) -> EmptyResult {
|
||||
let type_ = TwoFactorType::WebauthnLoginChallenge as i32;
|
||||
let state = match TwoFactor::find_by_user_and_type(user_uuid, type_, conn).await {
|
||||
let state = match TwoFactor::find_by_user_and_type(user_id, type_, conn).await {
|
||||
Some(tf) => {
|
||||
let state: AuthenticationState = serde_json::from_str(&tf.data)?;
|
||||
tf.delete(conn).await?;
|
||||
@@ -404,7 +403,7 @@ pub async fn validate_webauthn_login(user_uuid: &str, response: &str, conn: &mut
|
||||
let rsp: PublicKeyCredentialCopy = serde_json::from_str(response)?;
|
||||
let rsp: PublicKeyCredential = rsp.into();
|
||||
|
||||
let mut registrations = get_webauthn_registrations(user_uuid, conn).await?.1;
|
||||
let mut registrations = get_webauthn_registrations(user_id, conn).await?.1;
|
||||
|
||||
// If the credential we received is migrated from U2F, enable the U2F compatibility
|
||||
//let use_u2f = registrations.iter().any(|r| r.migrated && r.credential.cred_id == rsp.raw_id.0);
|
||||
@@ -414,7 +413,7 @@ pub async fn validate_webauthn_login(user_uuid: &str, response: &str, conn: &mut
|
||||
if ®.credential.cred_id == cred_id {
|
||||
reg.credential.counter = auth_data.counter;
|
||||
|
||||
TwoFactor::new(user_uuid.to_string(), TwoFactorType::Webauthn, serde_json::to_string(®istrations)?)
|
||||
TwoFactor::new(user_id.clone(), TwoFactorType::Webauthn, serde_json::to_string(®istrations)?)
|
||||
.save(conn)
|
||||
.await?;
|
||||
return Ok(());
|
||||
|
||||
@@ -92,10 +92,10 @@ async fn generate_yubikey(data: Json<PasswordOrOtpData>, headers: Headers, mut c
|
||||
|
||||
data.validate(&user, false, &mut conn).await?;
|
||||
|
||||
let user_uuid = &user.uuid;
|
||||
let user_id = &user.uuid;
|
||||
let yubikey_type = TwoFactorType::YubiKey as i32;
|
||||
|
||||
let r = TwoFactor::find_by_user_and_type(user_uuid, yubikey_type, &mut conn).await;
|
||||
let r = TwoFactor::find_by_user_and_type(user_id, yubikey_type, &mut conn).await;
|
||||
|
||||
if let Some(r) = r {
|
||||
let yubikey_metadata: YubikeyMetadata = serde_json::from_str(&r.data)?;
|
||||
|
||||
@@ -63,15 +63,18 @@ 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());
|
||||
|
||||
// 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.
|
||||
// If this function needs to be renamed, also adjust the code in `util.rs`
|
||||
#[get("/<domain>/icon.png")]
|
||||
fn icon_external(domain: &str) -> Option<Redirect> {
|
||||
if !is_valid_domain(domain) {
|
||||
warn!("Invalid domain: {}", domain);
|
||||
warn!("Invalid domain: {domain}");
|
||||
return None;
|
||||
}
|
||||
|
||||
if should_block_address(domain) {
|
||||
warn!("Blocked address: {}", domain);
|
||||
warn!("Blocked address: {domain}");
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -93,7 +96,7 @@ async fn icon_internal(domain: &str) -> Cached<(ContentType, Vec<u8>)> {
|
||||
const FALLBACK_ICON: &[u8] = include_bytes!("../static/images/fallback-icon.png");
|
||||
|
||||
if !is_valid_domain(domain) {
|
||||
warn!("Invalid domain: {}", domain);
|
||||
warn!("Invalid domain: {domain}");
|
||||
return Cached::ttl(
|
||||
(ContentType::new("image", "png"), FALLBACK_ICON.to_vec()),
|
||||
CONFIG.icon_cache_negttl(),
|
||||
@@ -102,7 +105,7 @@ async fn icon_internal(domain: &str) -> Cached<(ContentType, Vec<u8>)> {
|
||||
}
|
||||
|
||||
if should_block_address(domain) {
|
||||
warn!("Blocked address: {}", domain);
|
||||
warn!("Blocked address: {domain}");
|
||||
return Cached::ttl(
|
||||
(ContentType::new("image", "png"), FALLBACK_ICON.to_vec()),
|
||||
CONFIG.icon_cache_negttl(),
|
||||
@@ -127,7 +130,7 @@ fn is_valid_domain(domain: &str) -> bool {
|
||||
|
||||
// If parsing the domain fails using Url, it will not work with reqwest.
|
||||
if let Err(parse_error) = url::Url::parse(format!("https://{domain}").as_str()) {
|
||||
debug!("Domain parse error: '{}' - {:?}", domain, parse_error);
|
||||
debug!("Domain parse error: '{domain}' - {parse_error:?}");
|
||||
return false;
|
||||
} else if domain.is_empty()
|
||||
|| domain.contains("..")
|
||||
@@ -136,18 +139,17 @@ fn is_valid_domain(domain: &str) -> bool {
|
||||
|| domain.ends_with('-')
|
||||
{
|
||||
debug!(
|
||||
"Domain validation error: '{}' is either empty, contains '..', starts with an '.', starts or ends with a '-'",
|
||||
domain
|
||||
"Domain validation error: '{domain}' is either empty, contains '..', starts with an '.', starts or ends with a '-'"
|
||||
);
|
||||
return false;
|
||||
} else if domain.len() > 255 {
|
||||
debug!("Domain validation error: '{}' exceeds 255 characters", domain);
|
||||
debug!("Domain validation error: '{domain}' exceeds 255 characters");
|
||||
return false;
|
||||
}
|
||||
|
||||
for c in domain.chars() {
|
||||
if !c.is_alphanumeric() && !ALLOWED_CHARS.contains(c) {
|
||||
debug!("Domain validation error: '{}' contains an invalid character '{}'", domain, c);
|
||||
debug!("Domain validation error: '{domain}' contains an invalid character '{c}'");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -156,7 +158,7 @@ fn is_valid_domain(domain: &str) -> bool {
|
||||
}
|
||||
|
||||
async fn get_icon(domain: &str) -> Option<(Vec<u8>, String)> {
|
||||
let path = format!("{}/{}.png", CONFIG.icon_cache_folder(), domain);
|
||||
let path = format!("{}/{domain}.png", CONFIG.icon_cache_folder());
|
||||
|
||||
// Check for expiration of negatively cached copy
|
||||
if icon_is_negcached(&path).await {
|
||||
@@ -164,10 +166,7 @@ async fn get_icon(domain: &str) -> Option<(Vec<u8>, String)> {
|
||||
}
|
||||
|
||||
if let Some(icon) = get_cached_icon(&path).await {
|
||||
let icon_type = match get_icon_type(&icon) {
|
||||
Some(x) => x,
|
||||
_ => "x-icon",
|
||||
};
|
||||
let icon_type = get_icon_type(&icon).unwrap_or("x-icon");
|
||||
return Some((icon, icon_type.to_string()));
|
||||
}
|
||||
|
||||
@@ -189,7 +188,7 @@ async fn get_icon(domain: &str) -> Option<(Vec<u8>, String)> {
|
||||
return None;
|
||||
}
|
||||
|
||||
warn!("Unable to download icon: {:?}", e);
|
||||
warn!("Unable to download icon: {e:?}");
|
||||
let miss_indicator = path + ".miss";
|
||||
save_icon(&miss_indicator, &[]).await;
|
||||
None
|
||||
@@ -231,7 +230,7 @@ async fn icon_is_negcached(path: &str) -> bool {
|
||||
// No longer negatively cached, drop the marker
|
||||
Ok(true) => {
|
||||
if let Err(e) = remove_file(&miss_indicator).await {
|
||||
error!("Could not remove negative cache indicator for icon {:?}: {:?}", path, e);
|
||||
error!("Could not remove negative cache indicator for icon {path:?}: {e:?}");
|
||||
}
|
||||
false
|
||||
}
|
||||
@@ -291,9 +290,7 @@ fn get_favicons_node(dom: Tokenizer<StringReader<'_>, FaviconEmitter>, icons: &m
|
||||
TAG_HEAD if token.closing => {
|
||||
break;
|
||||
}
|
||||
_ => {
|
||||
continue;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -533,10 +530,10 @@ async fn download_icon(domain: &str) -> Result<(Bytes, Option<&str>), Error> {
|
||||
// Check if the icon type is allowed, else try an icon from the list.
|
||||
icon_type = get_icon_type(&body);
|
||||
if icon_type.is_none() {
|
||||
debug!("Icon from {} data:image uri, is not a valid image type", domain);
|
||||
debug!("Icon from {domain} data:image uri, is not a valid image type");
|
||||
continue;
|
||||
}
|
||||
info!("Extracted icon from data:image uri for {}", domain);
|
||||
info!("Extracted icon from data:image uri for {domain}");
|
||||
buffer = body.freeze();
|
||||
break;
|
||||
}
|
||||
@@ -576,7 +573,7 @@ async fn save_icon(path: &str, icon: &[u8]) {
|
||||
create_dir_all(&CONFIG.icon_cache_folder()).await.expect("Error creating icon cache folder");
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Unable to save icon: {:?}", e);
|
||||
warn!("Unable to save icon: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,14 +24,14 @@ use crate::{
|
||||
};
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![login, prelogin, identity_register]
|
||||
routes![login, prelogin, identity_register, register_verification_email, register_finish]
|
||||
}
|
||||
|
||||
#[post("/connect/token", data = "<data>")]
|
||||
async fn login(data: Form<ConnectData>, client_header: ClientHeaders, mut conn: DbConn) -> JsonResult {
|
||||
let data: ConnectData = data.into_inner();
|
||||
|
||||
let mut user_uuid: Option<String> = None;
|
||||
let mut user_id: Option<UserId> = None;
|
||||
|
||||
let login_result = match data.grant_type.as_ref() {
|
||||
"refresh_token" => {
|
||||
@@ -48,7 +48,7 @@ async fn login(data: Form<ConnectData>, client_header: ClientHeaders, mut conn:
|
||||
_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_uuid, &mut conn, &client_header.ip).await
|
||||
_password_login(data, &mut user_id, &mut conn, &client_header.ip).await
|
||||
}
|
||||
"client_credentials" => {
|
||||
_check_is_some(&data.client_id, "client_id cannot be blank")?;
|
||||
@@ -59,17 +59,17 @@ async fn login(data: Form<ConnectData>, client_header: ClientHeaders, mut conn:
|
||||
_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_uuid, &mut conn, &client_header.ip).await
|
||||
_api_key_login(data, &mut user_id, &mut conn, &client_header.ip).await
|
||||
}
|
||||
t => err!("Invalid type", t),
|
||||
};
|
||||
|
||||
if let Some(user_uuid) = user_uuid {
|
||||
if let Some(user_id) = user_id {
|
||||
match &login_result {
|
||||
Ok(_) => {
|
||||
log_user_event(
|
||||
EventType::UserLoggedIn as i32,
|
||||
&user_uuid,
|
||||
&user_id,
|
||||
client_header.device_type,
|
||||
&client_header.ip.ip,
|
||||
&mut conn,
|
||||
@@ -80,7 +80,7 @@ async fn login(data: Form<ConnectData>, client_header: ClientHeaders, mut conn:
|
||||
if let Some(ev) = e.get_event() {
|
||||
log_user_event(
|
||||
ev.event as i32,
|
||||
&user_uuid,
|
||||
&user_id,
|
||||
client_header.device_type,
|
||||
&client_header.ip.ip,
|
||||
&mut conn,
|
||||
@@ -111,7 +111,7 @@ async fn _refresh_login(data: ConnectData, conn: &mut DbConn) -> JsonResult {
|
||||
// Because this might get used in the future, and is add by the Bitwarden Server, lets keep it, but then commented out
|
||||
// See: https://github.com/dani-garcia/vaultwarden/issues/4156
|
||||
// ---
|
||||
// let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, conn).await;
|
||||
// let members = Membership::find_confirmed_by_user(&user.uuid, conn).await;
|
||||
let (access_token, expires_in) = device.refresh_tokens(&user, scope_vec);
|
||||
device.save(conn).await?;
|
||||
|
||||
@@ -141,7 +141,7 @@ struct MasterPasswordPolicy {
|
||||
|
||||
async fn _password_login(
|
||||
data: ConnectData,
|
||||
user_uuid: &mut Option<String>,
|
||||
user_id: &mut Option<UserId>,
|
||||
conn: &mut DbConn,
|
||||
ip: &ClientIp,
|
||||
) -> JsonResult {
|
||||
@@ -157,19 +157,18 @@ async fn _password_login(
|
||||
|
||||
// Get the user
|
||||
let username = data.username.as_ref().unwrap().trim();
|
||||
let mut user = match User::find_by_mail(username, conn).await {
|
||||
Some(user) => user,
|
||||
None => err!("Username or password is incorrect. Try again", format!("IP: {}. Username: {}.", ip.ip, username)),
|
||||
let Some(mut user) = User::find_by_mail(username, conn).await else {
|
||||
err!("Username or password is incorrect. Try again", format!("IP: {}. Username: {username}.", ip.ip))
|
||||
};
|
||||
|
||||
// Set the user_uuid here to be passed back used for event logging.
|
||||
*user_uuid = Some(user.uuid.clone());
|
||||
// Set the user_id here to be passed back used for event logging.
|
||||
*user_id = Some(user.uuid.clone());
|
||||
|
||||
// Check if the user is disabled
|
||||
if !user.enabled {
|
||||
err!(
|
||||
"This user has been disabled",
|
||||
format!("IP: {}. Username: {}.", ip.ip, username),
|
||||
format!("IP: {}. Username: {username}.", ip.ip),
|
||||
ErrorEvent {
|
||||
event: EventType::UserFailedLogIn
|
||||
}
|
||||
@@ -179,11 +178,11 @@ async fn _password_login(
|
||||
let password = data.password.as_ref().unwrap();
|
||||
|
||||
// If we get an auth request, we don't check the user's password, but the access code of the auth request
|
||||
if let Some(ref auth_request_uuid) = data.auth_request {
|
||||
let Some(auth_request) = AuthRequest::find_by_uuid(auth_request_uuid.as_str(), conn).await else {
|
||||
if let Some(ref auth_request_id) = data.auth_request {
|
||||
let Some(auth_request) = AuthRequest::find_by_uuid_and_user(auth_request_id, &user.uuid, conn).await else {
|
||||
err!(
|
||||
"Auth request not found. Try again.",
|
||||
format!("IP: {}. Username: {}.", ip.ip, username),
|
||||
format!("IP: {}. Username: {username}.", ip.ip),
|
||||
ErrorEvent {
|
||||
event: EventType::UserFailedLogIn,
|
||||
}
|
||||
@@ -201,7 +200,7 @@ async fn _password_login(
|
||||
{
|
||||
err!(
|
||||
"Username or access code is incorrect. Try again",
|
||||
format!("IP: {}. Username: {}.", ip.ip, username),
|
||||
format!("IP: {}. Username: {username}.", ip.ip),
|
||||
ErrorEvent {
|
||||
event: EventType::UserFailedLogIn,
|
||||
}
|
||||
@@ -210,7 +209,7 @@ async fn _password_login(
|
||||
} else if !user.check_valid_password(password) {
|
||||
err!(
|
||||
"Username or password is incorrect. Try again",
|
||||
format!("IP: {}. Username: {}.", ip.ip, username),
|
||||
format!("IP: {}. Username: {username}.", ip.ip),
|
||||
ErrorEvent {
|
||||
event: EventType::UserFailedLogIn,
|
||||
}
|
||||
@@ -223,7 +222,7 @@ async fn _password_login(
|
||||
user.set_password(password, None, false, None);
|
||||
|
||||
if let Err(e) = user.save(conn).await {
|
||||
error!("Error updating user: {:#?}", e);
|
||||
error!("Error updating user: {e:#?}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,11 +241,11 @@ async fn _password_login(
|
||||
user.login_verify_count += 1;
|
||||
|
||||
if let Err(e) = user.save(conn).await {
|
||||
error!("Error updating user: {:#?}", e);
|
||||
error!("Error updating user: {e:#?}");
|
||||
}
|
||||
|
||||
if let Err(e) = mail::send_verify_email(&user.email, &user.uuid).await {
|
||||
error!("Error auto-sending email verification email: {:#?}", e);
|
||||
error!("Error auto-sending email verification email: {e:#?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -254,7 +253,7 @@ async fn _password_login(
|
||||
// We still want the login to fail until they actually verified the email address
|
||||
err!(
|
||||
"Please verify your email before trying again.",
|
||||
format!("IP: {}. Username: {}.", ip.ip, username),
|
||||
format!("IP: {}. Username: {username}.", ip.ip),
|
||||
ErrorEvent {
|
||||
event: EventType::UserFailedLogIn
|
||||
}
|
||||
@@ -267,7 +266,7 @@ async fn _password_login(
|
||||
|
||||
if CONFIG.mail_enabled() && new_device {
|
||||
if let Err(e) = mail::send_new_device_logged_in(&user.email, &ip.ip.to_string(), &now, &device).await {
|
||||
error!("Error sending new device email: {:#?}", e);
|
||||
error!("Error sending new device email: {e:#?}");
|
||||
|
||||
if CONFIG.require_device_email() {
|
||||
err!(
|
||||
@@ -291,7 +290,7 @@ async fn _password_login(
|
||||
// Because this might get used in the future, and is add by the Bitwarden Server, lets keep it, but then commented out
|
||||
// See: https://github.com/dani-garcia/vaultwarden/issues/4156
|
||||
// ---
|
||||
// let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, conn).await;
|
||||
// let members = Membership::find_confirmed_by_user(&user.uuid, conn).await;
|
||||
let (access_token, expires_in) = device.refresh_tokens(&user, scope_vec);
|
||||
device.save(conn).await?;
|
||||
|
||||
@@ -353,13 +352,13 @@ async fn _password_login(
|
||||
result["TwoFactorToken"] = Value::String(token);
|
||||
}
|
||||
|
||||
info!("User {} logged in successfully. IP: {}", username, ip.ip);
|
||||
info!("User {username} logged in successfully. IP: {}", ip.ip);
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
async fn _api_key_login(
|
||||
data: ConnectData,
|
||||
user_uuid: &mut Option<String>,
|
||||
user_id: &mut Option<UserId>,
|
||||
conn: &mut DbConn,
|
||||
ip: &ClientIp,
|
||||
) -> JsonResult {
|
||||
@@ -368,7 +367,7 @@ async fn _api_key_login(
|
||||
|
||||
// Validate scope
|
||||
match data.scope.as_ref().unwrap().as_ref() {
|
||||
"api" => _user_api_key_login(data, user_uuid, conn, ip).await,
|
||||
"api" => _user_api_key_login(data, user_id, conn, ip).await,
|
||||
"api.organization" => _organization_api_key_login(data, conn, ip).await,
|
||||
_ => err!("Scope not supported"),
|
||||
}
|
||||
@@ -376,23 +375,22 @@ async fn _api_key_login(
|
||||
|
||||
async fn _user_api_key_login(
|
||||
data: ConnectData,
|
||||
user_uuid: &mut Option<String>,
|
||||
user_id: &mut Option<UserId>,
|
||||
conn: &mut DbConn,
|
||||
ip: &ClientIp,
|
||||
) -> JsonResult {
|
||||
// Get the user via the client_id
|
||||
let client_id = data.client_id.as_ref().unwrap();
|
||||
let client_user_uuid = match client_id.strip_prefix("user.") {
|
||||
Some(uuid) => uuid,
|
||||
None => err!("Malformed client_id", format!("IP: {}.", ip.ip)),
|
||||
let Some(client_user_id) = client_id.strip_prefix("user.") else {
|
||||
err!("Malformed client_id", format!("IP: {}.", ip.ip))
|
||||
};
|
||||
let user = match User::find_by_uuid(client_user_uuid, conn).await {
|
||||
Some(user) => user,
|
||||
None => err!("Invalid client_id", format!("IP: {}.", ip.ip)),
|
||||
let client_user_id: UserId = client_user_id.into();
|
||||
let Some(user) = User::find_by_uuid(&client_user_id, conn).await else {
|
||||
err!("Invalid client_id", format!("IP: {}.", ip.ip))
|
||||
};
|
||||
|
||||
// Set the user_uuid here to be passed back used for event logging.
|
||||
*user_uuid = Some(user.uuid.clone());
|
||||
// Set the user_id here to be passed back used for event logging.
|
||||
*user_id = Some(user.uuid.clone());
|
||||
|
||||
// Check if the user is disabled
|
||||
if !user.enabled {
|
||||
@@ -422,7 +420,7 @@ async fn _user_api_key_login(
|
||||
if CONFIG.mail_enabled() && new_device {
|
||||
let now = Utc::now().naive_utc();
|
||||
if let Err(e) = mail::send_new_device_logged_in(&user.email, &ip.ip.to_string(), &now, &device).await {
|
||||
error!("Error sending new device email: {:#?}", e);
|
||||
error!("Error sending new device email: {e:#?}");
|
||||
|
||||
if CONFIG.require_device_email() {
|
||||
err!(
|
||||
@@ -442,7 +440,7 @@ async fn _user_api_key_login(
|
||||
// Because this might get used in the future, and is add by the Bitwarden Server, lets keep it, but then commented out
|
||||
// See: https://github.com/dani-garcia/vaultwarden/issues/4156
|
||||
// ---
|
||||
// let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, conn).await;
|
||||
// let members = Membership::find_confirmed_by_user(&user.uuid, conn).await;
|
||||
let (access_token, expires_in) = device.refresh_tokens(&user, scope_vec);
|
||||
device.save(conn).await?;
|
||||
|
||||
@@ -471,13 +469,12 @@ async fn _user_api_key_login(
|
||||
async fn _organization_api_key_login(data: ConnectData, conn: &mut DbConn, ip: &ClientIp) -> JsonResult {
|
||||
// Get the org via the client_id
|
||||
let client_id = data.client_id.as_ref().unwrap();
|
||||
let org_uuid = match client_id.strip_prefix("organization.") {
|
||||
Some(uuid) => uuid,
|
||||
None => err!("Malformed client_id", format!("IP: {}.", ip.ip)),
|
||||
let Some(org_id) = client_id.strip_prefix("organization.") else {
|
||||
err!("Malformed client_id", format!("IP: {}.", ip.ip))
|
||||
};
|
||||
let org_api_key = match OrganizationApiKey::find_by_org_uuid(org_uuid, conn).await {
|
||||
Some(org_api_key) => org_api_key,
|
||||
None => err!("Invalid client_id", format!("IP: {}.", ip.ip)),
|
||||
let org_id: OrganizationId = org_id.to_string().into();
|
||||
let Some(org_api_key) = OrganizationApiKey::find_by_org_uuid(&org_id, conn).await else {
|
||||
err!("Invalid client_id", format!("IP: {}.", ip.ip))
|
||||
};
|
||||
|
||||
// Check API key.
|
||||
@@ -578,7 +575,7 @@ async fn twofactor_auth(
|
||||
}
|
||||
}
|
||||
Some(TwoFactorType::Email) => {
|
||||
email::validate_email_code_str(&user.uuid, twofactor_code, &selected_data?, conn).await?
|
||||
email::validate_email_code_str(&user.uuid, twofactor_code, &selected_data?, &ip.ip, conn).await?
|
||||
}
|
||||
|
||||
Some(TwoFactorType::Remember) => {
|
||||
@@ -618,7 +615,7 @@ fn _selected_data(tf: Option<TwoFactor>) -> ApiResult<String> {
|
||||
|
||||
async fn _json_err_twofactor(
|
||||
providers: &[i32],
|
||||
user_uuid: &str,
|
||||
user_id: &UserId,
|
||||
data: &ConnectData,
|
||||
conn: &mut DbConn,
|
||||
) -> ApiResult<Value> {
|
||||
@@ -639,12 +636,12 @@ async fn _json_err_twofactor(
|
||||
Some(TwoFactorType::Authenticator) => { /* Nothing to do for TOTP */ }
|
||||
|
||||
Some(TwoFactorType::Webauthn) if CONFIG.domain_set() => {
|
||||
let request = webauthn::generate_webauthn_login(user_uuid, conn).await?;
|
||||
let request = webauthn::generate_webauthn_login(user_id, conn).await?;
|
||||
result["TwoFactorProviders2"][provider.to_string()] = request.0;
|
||||
}
|
||||
|
||||
Some(TwoFactorType::Duo) => {
|
||||
let email = match User::find_by_uuid(user_uuid, conn).await {
|
||||
let email = match User::find_by_uuid(user_id, conn).await {
|
||||
Some(u) => u.email,
|
||||
None => err!("User does not exist"),
|
||||
};
|
||||
@@ -676,9 +673,8 @@ async fn _json_err_twofactor(
|
||||
}
|
||||
|
||||
Some(tf_type @ TwoFactorType::YubiKey) => {
|
||||
let twofactor = match TwoFactor::find_by_user_and_type(user_uuid, tf_type as i32, conn).await {
|
||||
Some(tf) => tf,
|
||||
None => err!("No YubiKey devices registered"),
|
||||
let Some(twofactor) = TwoFactor::find_by_user_and_type(user_id, tf_type as i32, conn).await else {
|
||||
err!("No YubiKey devices registered")
|
||||
};
|
||||
|
||||
let yubikey_metadata: yubikey::YubikeyMetadata = serde_json::from_str(&twofactor.data)?;
|
||||
@@ -689,14 +685,13 @@ async fn _json_err_twofactor(
|
||||
}
|
||||
|
||||
Some(tf_type @ TwoFactorType::Email) => {
|
||||
let twofactor = match TwoFactor::find_by_user_and_type(user_uuid, tf_type as i32, conn).await {
|
||||
Some(tf) => tf,
|
||||
None => err!("No twofactor email registered"),
|
||||
let Some(twofactor) = TwoFactor::find_by_user_and_type(user_id, tf_type as i32, conn).await else {
|
||||
err!("No twofactor email registered")
|
||||
};
|
||||
|
||||
// Send email immediately if email is the only 2FA option
|
||||
if providers.len() == 1 {
|
||||
email::send_token(user_uuid, conn).await?
|
||||
email::send_token(user_id, conn).await?
|
||||
}
|
||||
|
||||
let email_data = email::EmailTokenData::from_json(&twofactor.data)?;
|
||||
@@ -719,7 +714,66 @@ async fn prelogin(data: Json<PreloginData>, conn: DbConn) -> Json<Value> {
|
||||
|
||||
#[post("/accounts/register", data = "<data>")]
|
||||
async fn identity_register(data: Json<RegisterData>, conn: DbConn) -> JsonResult {
|
||||
_register(data, conn).await
|
||||
_register(data, false, conn).await
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct RegisterVerificationData {
|
||||
email: String,
|
||||
name: Option<String>,
|
||||
// receiveMarketingEmails: bool,
|
||||
}
|
||||
|
||||
#[derive(rocket::Responder)]
|
||||
enum RegisterVerificationResponse {
|
||||
NoContent(()),
|
||||
Token(Json<String>),
|
||||
}
|
||||
|
||||
#[post("/accounts/register/send-verification-email", data = "<data>")]
|
||||
async fn register_verification_email(
|
||||
data: Json<RegisterVerificationData>,
|
||||
mut conn: DbConn,
|
||||
) -> ApiResult<RegisterVerificationResponse> {
|
||||
let data = data.into_inner();
|
||||
|
||||
if !CONFIG.is_signup_allowed(&data.email) {
|
||||
err!("Registration not allowed or user already exists")
|
||||
}
|
||||
|
||||
let should_send_mail = CONFIG.mail_enabled() && CONFIG.signups_verify();
|
||||
|
||||
let token_claims =
|
||||
crate::auth::generate_register_verify_claims(data.email.clone(), data.name.clone(), should_send_mail);
|
||||
let token = crate::auth::encode_jwt(&token_claims);
|
||||
|
||||
if should_send_mail {
|
||||
let user = User::find_by_mail(&data.email, &mut 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
|
||||
// don't. Add a randomized sleep to mitigate this somewhat.
|
||||
use rand::{rngs::SmallRng, Rng, SeedableRng};
|
||||
let mut rng = SmallRng::from_os_rng();
|
||||
let delta: i32 = 100;
|
||||
let sleep_ms = (1_000 + rng.random_range(-delta..=delta)) as u64;
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(sleep_ms)).await;
|
||||
} else {
|
||||
mail::send_register_verify_email(&data.email, &token).await?;
|
||||
}
|
||||
|
||||
Ok(RegisterVerificationResponse::NoContent(()))
|
||||
} else {
|
||||
// If email verification is not required, return the token directly
|
||||
// the clients will use this token to finish the registration
|
||||
Ok(RegisterVerificationResponse::Token(Json(token)))
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/accounts/register/finish", data = "<data>")]
|
||||
async fn register_finish(data: Json<RegisterData>, conn: DbConn) -> JsonResult {
|
||||
_register(data, true, conn).await
|
||||
}
|
||||
|
||||
// https://github.com/bitwarden/jslib/blob/master/common/src/models/request/tokenRequest.ts
|
||||
@@ -751,7 +805,7 @@ struct ConnectData {
|
||||
|
||||
#[field(name = uncased("device_identifier"))]
|
||||
#[field(name = uncased("deviceidentifier"))]
|
||||
device_identifier: Option<String>,
|
||||
device_identifier: Option<DeviceId>,
|
||||
#[field(name = uncased("device_name"))]
|
||||
#[field(name = uncased("devicename"))]
|
||||
device_name: Option<String>,
|
||||
@@ -774,7 +828,7 @@ struct ConnectData {
|
||||
#[field(name = uncased("twofactorremember"))]
|
||||
two_factor_remember: Option<i32>,
|
||||
#[field(name = uncased("authrequest"))]
|
||||
auth_request: Option<String>,
|
||||
auth_request: Option<AuthRequestId>,
|
||||
}
|
||||
|
||||
fn _check_is_some<T>(value: &Option<T>, msg: &str) -> EmptyResult {
|
||||
|
||||
@@ -10,7 +10,7 @@ use rocket_ws::{Message, WebSocket};
|
||||
use crate::{
|
||||
auth::{ClientIp, WsAccessTokenHeader},
|
||||
db::{
|
||||
models::{Cipher, Folder, Send as DbSend, User},
|
||||
models::{AuthRequestId, Cipher, CollectionId, DeviceId, Folder, Send as DbSend, User, UserId},
|
||||
DbConn,
|
||||
},
|
||||
Error, CONFIG,
|
||||
@@ -53,13 +53,13 @@ struct WsAccessToken {
|
||||
|
||||
struct WSEntryMapGuard {
|
||||
users: Arc<WebSocketUsers>,
|
||||
user_uuid: String,
|
||||
user_uuid: UserId,
|
||||
entry_uuid: uuid::Uuid,
|
||||
addr: IpAddr,
|
||||
}
|
||||
|
||||
impl WSEntryMapGuard {
|
||||
fn new(users: Arc<WebSocketUsers>, user_uuid: String, entry_uuid: uuid::Uuid, addr: IpAddr) -> Self {
|
||||
fn new(users: Arc<WebSocketUsers>, user_uuid: UserId, entry_uuid: uuid::Uuid, addr: IpAddr) -> Self {
|
||||
Self {
|
||||
users,
|
||||
user_uuid,
|
||||
@@ -72,7 +72,7 @@ impl WSEntryMapGuard {
|
||||
impl Drop for WSEntryMapGuard {
|
||||
fn drop(&mut self) {
|
||||
info!("Closing WS connection from {}", self.addr);
|
||||
if let Some(mut entry) = self.users.map.get_mut(&self.user_uuid) {
|
||||
if let Some(mut entry) = self.users.map.get_mut(self.user_uuid.as_ref()) {
|
||||
entry.retain(|(uuid, _)| uuid != &self.entry_uuid);
|
||||
}
|
||||
}
|
||||
@@ -101,6 +101,7 @@ impl Drop for WSAnonymousEntryMapGuard {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(tail_expr_drop_order)]
|
||||
#[get("/hub?<data..>")]
|
||||
fn websockets_hub<'r>(
|
||||
ws: WebSocket,
|
||||
@@ -129,7 +130,7 @@ fn websockets_hub<'r>(
|
||||
// Add a channel to send messages to this client to the map
|
||||
let entry_uuid = uuid::Uuid::new_v4();
|
||||
let (tx, rx) = tokio::sync::mpsc::channel::<Message>(100);
|
||||
users.map.entry(claims.sub.clone()).or_default().push((entry_uuid, tx));
|
||||
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))
|
||||
@@ -156,7 +157,6 @@ fn websockets_hub<'r>(
|
||||
|
||||
if serde_json::from_str(msg).ok() == Some(INITIAL_MESSAGE) {
|
||||
yield Message::binary(INITIAL_RESPONSE);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,6 +186,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;
|
||||
@@ -223,7 +224,6 @@ fn anonymous_websockets_hub<'r>(ws: WebSocket, token: String, ip: ClientIp) -> R
|
||||
|
||||
if serde_json::from_str(msg).ok() == Some(INITIAL_MESSAGE) {
|
||||
yield Message::binary(INITIAL_RESPONSE);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,7 +290,7 @@ fn serialize(val: Value) -> Vec<u8> {
|
||||
fn serialize_date(date: NaiveDateTime) -> Value {
|
||||
let seconds: i64 = date.and_utc().timestamp();
|
||||
let nanos: i64 = date.and_utc().timestamp_subsec_nanos().into();
|
||||
let timestamp = nanos << 34 | seconds;
|
||||
let timestamp = (nanos << 34) | seconds;
|
||||
|
||||
let bs = timestamp.to_be_bytes();
|
||||
|
||||
@@ -328,8 +328,8 @@ pub struct WebSocketUsers {
|
||||
}
|
||||
|
||||
impl WebSocketUsers {
|
||||
async fn send_update(&self, user_uuid: &str, data: &[u8]) {
|
||||
if let Some(user) = self.map.get(user_uuid).map(|v| v.clone()) {
|
||||
async fn send_update(&self, user_id: &UserId, data: &[u8]) {
|
||||
if let Some(user) = self.map.get(user_id.as_ref()).map(|v| v.clone()) {
|
||||
for (_, sender) in user.iter() {
|
||||
if let Err(e) = sender.send(Message::binary(data)).await {
|
||||
error!("Error sending WS update {e}");
|
||||
@@ -345,7 +345,7 @@ impl WebSocketUsers {
|
||||
return;
|
||||
}
|
||||
let data = create_update(
|
||||
vec![("UserId".into(), user.uuid.clone().into()), ("Date".into(), serialize_date(user.updated_at))],
|
||||
vec![("UserId".into(), user.uuid.to_string().into()), ("Date".into(), serialize_date(user.updated_at))],
|
||||
ut,
|
||||
None,
|
||||
);
|
||||
@@ -359,15 +359,15 @@ impl WebSocketUsers {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send_logout(&self, user: &User, acting_device_uuid: Option<String>) {
|
||||
pub async fn send_logout(&self, user: &User, acting_device_id: Option<DeviceId>) {
|
||||
// Skip any processing if both WebSockets and Push are not active
|
||||
if *NOTIFICATIONS_DISABLED {
|
||||
return;
|
||||
}
|
||||
let data = create_update(
|
||||
vec![("UserId".into(), user.uuid.clone().into()), ("Date".into(), serialize_date(user.updated_at))],
|
||||
vec![("UserId".into(), user.uuid.to_string().into()), ("Date".into(), serialize_date(user.updated_at))],
|
||||
UpdateType::LogOut,
|
||||
acting_device_uuid.clone(),
|
||||
acting_device_id.clone(),
|
||||
);
|
||||
|
||||
if CONFIG.enable_websocket() {
|
||||
@@ -375,7 +375,7 @@ impl WebSocketUsers {
|
||||
}
|
||||
|
||||
if CONFIG.push_enabled() {
|
||||
push_logout(user, acting_device_uuid);
|
||||
push_logout(user, acting_device_id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,7 +383,7 @@ impl WebSocketUsers {
|
||||
&self,
|
||||
ut: UpdateType,
|
||||
folder: &Folder,
|
||||
acting_device_uuid: &String,
|
||||
acting_device_id: &DeviceId,
|
||||
conn: &mut DbConn,
|
||||
) {
|
||||
// Skip any processing if both WebSockets and Push are not active
|
||||
@@ -392,12 +392,12 @@ impl WebSocketUsers {
|
||||
}
|
||||
let data = create_update(
|
||||
vec![
|
||||
("Id".into(), folder.uuid.clone().into()),
|
||||
("UserId".into(), folder.user_uuid.clone().into()),
|
||||
("Id".into(), folder.uuid.to_string().into()),
|
||||
("UserId".into(), folder.user_uuid.to_string().into()),
|
||||
("RevisionDate".into(), serialize_date(folder.updated_at)),
|
||||
],
|
||||
ut,
|
||||
Some(acting_device_uuid.into()),
|
||||
Some(acting_device_id.clone()),
|
||||
);
|
||||
|
||||
if CONFIG.enable_websocket() {
|
||||
@@ -405,7 +405,7 @@ impl WebSocketUsers {
|
||||
}
|
||||
|
||||
if CONFIG.push_enabled() {
|
||||
push_folder_update(ut, folder, acting_device_uuid, conn).await;
|
||||
push_folder_update(ut, folder, acting_device_id, conn).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -413,48 +413,48 @@ impl WebSocketUsers {
|
||||
&self,
|
||||
ut: UpdateType,
|
||||
cipher: &Cipher,
|
||||
user_uuids: &[String],
|
||||
acting_device_uuid: &String,
|
||||
collection_uuids: Option<Vec<String>>,
|
||||
user_ids: &[UserId],
|
||||
acting_device_id: &DeviceId,
|
||||
collection_uuids: Option<Vec<CollectionId>>,
|
||||
conn: &mut DbConn,
|
||||
) {
|
||||
// Skip any processing if both WebSockets and Push are not active
|
||||
if *NOTIFICATIONS_DISABLED {
|
||||
return;
|
||||
}
|
||||
let org_uuid = convert_option(cipher.organization_uuid.clone());
|
||||
let org_id = convert_option(cipher.organization_uuid.as_deref());
|
||||
// Depending if there are collections provided or not, we need to have different values for the following variables.
|
||||
// The user_uuid should be `null`, and the revision date should be set to now, else the clients won't sync the collection change.
|
||||
let (user_uuid, collection_uuids, revision_date) = if let Some(collection_uuids) = collection_uuids {
|
||||
let (user_id, collection_uuids, revision_date) = if let Some(collection_uuids) = collection_uuids {
|
||||
(
|
||||
Value::Nil,
|
||||
Value::Array(collection_uuids.into_iter().map(|v| v.into()).collect::<Vec<Value>>()),
|
||||
Value::Array(collection_uuids.into_iter().map(|v| v.to_string().into()).collect::<Vec<Value>>()),
|
||||
serialize_date(Utc::now().naive_utc()),
|
||||
)
|
||||
} else {
|
||||
(convert_option(cipher.user_uuid.clone()), Value::Nil, serialize_date(cipher.updated_at))
|
||||
(convert_option(cipher.user_uuid.as_deref()), Value::Nil, serialize_date(cipher.updated_at))
|
||||
};
|
||||
|
||||
let data = create_update(
|
||||
vec![
|
||||
("Id".into(), cipher.uuid.clone().into()),
|
||||
("UserId".into(), user_uuid),
|
||||
("OrganizationId".into(), org_uuid),
|
||||
("Id".into(), cipher.uuid.to_string().into()),
|
||||
("UserId".into(), user_id),
|
||||
("OrganizationId".into(), org_id),
|
||||
("CollectionIds".into(), collection_uuids),
|
||||
("RevisionDate".into(), revision_date),
|
||||
],
|
||||
ut,
|
||||
Some(acting_device_uuid.into()),
|
||||
Some(acting_device_id.clone()),
|
||||
);
|
||||
|
||||
if CONFIG.enable_websocket() {
|
||||
for uuid in user_uuids {
|
||||
for uuid in user_ids {
|
||||
self.send_update(uuid, &data).await;
|
||||
}
|
||||
}
|
||||
|
||||
if CONFIG.push_enabled() && user_uuids.len() == 1 {
|
||||
push_cipher_update(ut, cipher, acting_device_uuid, conn).await;
|
||||
if CONFIG.push_enabled() && user_ids.len() == 1 {
|
||||
push_cipher_update(ut, cipher, acting_device_id, conn).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -462,20 +462,20 @@ impl WebSocketUsers {
|
||||
&self,
|
||||
ut: UpdateType,
|
||||
send: &DbSend,
|
||||
user_uuids: &[String],
|
||||
acting_device_uuid: &String,
|
||||
user_ids: &[UserId],
|
||||
acting_device_id: &DeviceId,
|
||||
conn: &mut DbConn,
|
||||
) {
|
||||
// Skip any processing if both WebSockets and Push are not active
|
||||
if *NOTIFICATIONS_DISABLED {
|
||||
return;
|
||||
}
|
||||
let user_uuid = convert_option(send.user_uuid.clone());
|
||||
let user_id = convert_option(send.user_uuid.as_deref());
|
||||
|
||||
let data = create_update(
|
||||
vec![
|
||||
("Id".into(), send.uuid.clone().into()),
|
||||
("UserId".into(), user_uuid),
|
||||
("Id".into(), send.uuid.to_string().into()),
|
||||
("UserId".into(), user_id),
|
||||
("RevisionDate".into(), serialize_date(send.revision_date)),
|
||||
],
|
||||
ut,
|
||||
@@ -483,20 +483,20 @@ impl WebSocketUsers {
|
||||
);
|
||||
|
||||
if CONFIG.enable_websocket() {
|
||||
for uuid in user_uuids {
|
||||
for uuid in user_ids {
|
||||
self.send_update(uuid, &data).await;
|
||||
}
|
||||
}
|
||||
if CONFIG.push_enabled() && user_uuids.len() == 1 {
|
||||
push_send_update(ut, send, acting_device_uuid, conn).await;
|
||||
if CONFIG.push_enabled() && user_ids.len() == 1 {
|
||||
push_send_update(ut, send, acting_device_id, conn).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send_auth_request(
|
||||
&self,
|
||||
user_uuid: &String,
|
||||
auth_request_uuid: &String,
|
||||
acting_device_uuid: &String,
|
||||
user_id: &UserId,
|
||||
auth_request_uuid: &str,
|
||||
acting_device_id: &DeviceId,
|
||||
conn: &mut DbConn,
|
||||
) {
|
||||
// Skip any processing if both WebSockets and Push are not active
|
||||
@@ -504,24 +504,24 @@ impl WebSocketUsers {
|
||||
return;
|
||||
}
|
||||
let data = create_update(
|
||||
vec![("Id".into(), auth_request_uuid.clone().into()), ("UserId".into(), user_uuid.clone().into())],
|
||||
vec![("Id".into(), auth_request_uuid.to_owned().into()), ("UserId".into(), user_id.to_string().into())],
|
||||
UpdateType::AuthRequest,
|
||||
Some(acting_device_uuid.to_string()),
|
||||
Some(acting_device_id.clone()),
|
||||
);
|
||||
if CONFIG.enable_websocket() {
|
||||
self.send_update(user_uuid, &data).await;
|
||||
self.send_update(user_id, &data).await;
|
||||
}
|
||||
|
||||
if CONFIG.push_enabled() {
|
||||
push_auth_request(user_uuid.to_string(), auth_request_uuid.to_string(), conn).await;
|
||||
push_auth_request(user_id.clone(), auth_request_uuid.to_owned(), conn).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send_auth_response(
|
||||
&self,
|
||||
user_uuid: &String,
|
||||
auth_response_uuid: &str,
|
||||
approving_device_uuid: String,
|
||||
user_id: &UserId,
|
||||
auth_request_id: &AuthRequestId,
|
||||
approving_device_id: &DeviceId,
|
||||
conn: &mut DbConn,
|
||||
) {
|
||||
// Skip any processing if both WebSockets and Push are not active
|
||||
@@ -529,17 +529,16 @@ impl WebSocketUsers {
|
||||
return;
|
||||
}
|
||||
let data = create_update(
|
||||
vec![("Id".into(), auth_response_uuid.to_owned().into()), ("UserId".into(), user_uuid.clone().into())],
|
||||
vec![("Id".into(), auth_request_id.to_string().into()), ("UserId".into(), user_id.to_string().into())],
|
||||
UpdateType::AuthRequestResponse,
|
||||
approving_device_uuid.clone().into(),
|
||||
Some(approving_device_id.clone()),
|
||||
);
|
||||
if CONFIG.enable_websocket() {
|
||||
self.send_update(auth_response_uuid, &data).await;
|
||||
self.send_update(user_id, &data).await;
|
||||
}
|
||||
|
||||
if CONFIG.push_enabled() {
|
||||
push_auth_response(user_uuid.to_string(), auth_response_uuid.to_string(), approving_device_uuid, conn)
|
||||
.await;
|
||||
push_auth_response(user_id, auth_request_id, approving_device_id, conn).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -558,16 +557,16 @@ impl AnonymousWebSocketSubscriptions {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send_auth_response(&self, user_uuid: &String, auth_response_uuid: &str) {
|
||||
pub async fn send_auth_response(&self, user_id: &UserId, auth_request_id: &AuthRequestId) {
|
||||
if !CONFIG.enable_websocket() {
|
||||
return;
|
||||
}
|
||||
let data = create_anonymous_update(
|
||||
vec![("Id".into(), auth_response_uuid.to_owned().into()), ("UserId".into(), user_uuid.clone().into())],
|
||||
vec![("Id".into(), auth_request_id.to_string().into()), ("UserId".into(), user_id.to_string().into())],
|
||||
UpdateType::AuthRequestResponse,
|
||||
user_uuid.to_string(),
|
||||
user_id.clone(),
|
||||
);
|
||||
self.send_update(auth_response_uuid, &data).await;
|
||||
self.send_update(auth_request_id, &data).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -579,14 +578,14 @@ impl AnonymousWebSocketSubscriptions {
|
||||
"ReceiveMessage", // Target
|
||||
[ // Arguments
|
||||
{
|
||||
"ContextId": acting_device_uuid || Nil,
|
||||
"ContextId": acting_device_id || Nil,
|
||||
"Type": ut as i32,
|
||||
"Payload": {}
|
||||
}
|
||||
]
|
||||
]
|
||||
*/
|
||||
fn create_update(payload: Vec<(Value, Value)>, ut: UpdateType, acting_device_uuid: Option<String>) -> Vec<u8> {
|
||||
fn create_update(payload: Vec<(Value, Value)>, ut: UpdateType, acting_device_id: Option<DeviceId>) -> Vec<u8> {
|
||||
use rmpv::Value as V;
|
||||
|
||||
let value = V::Array(vec![
|
||||
@@ -595,7 +594,7 @@ fn create_update(payload: Vec<(Value, Value)>, ut: UpdateType, acting_device_uui
|
||||
V::Nil,
|
||||
"ReceiveMessage".into(),
|
||||
V::Array(vec![V::Map(vec![
|
||||
("ContextId".into(), acting_device_uuid.map(|v| v.into()).unwrap_or_else(|| V::Nil)),
|
||||
("ContextId".into(), acting_device_id.map(|v| v.to_string().into()).unwrap_or_else(|| V::Nil)),
|
||||
("Type".into(), (ut as i32).into()),
|
||||
("Payload".into(), payload.into()),
|
||||
])]),
|
||||
@@ -604,7 +603,7 @@ fn create_update(payload: Vec<(Value, Value)>, ut: UpdateType, acting_device_uui
|
||||
serialize(value)
|
||||
}
|
||||
|
||||
fn create_anonymous_update(payload: Vec<(Value, Value)>, ut: UpdateType, user_id: String) -> 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![
|
||||
@@ -615,7 +614,7 @@ fn create_anonymous_update(payload: Vec<(Value, Value)>, ut: UpdateType, user_id
|
||||
V::Array(vec![V::Map(vec![
|
||||
("Type".into(), (ut as i32).into()),
|
||||
("Payload".into(), payload.into()),
|
||||
("UserId".into(), user_id.into()),
|
||||
("UserId".into(), user_id.to_string().into()),
|
||||
])]),
|
||||
]);
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ use tokio::sync::RwLock;
|
||||
|
||||
use crate::{
|
||||
api::{ApiResult, EmptyResult, UpdateType},
|
||||
db::models::{Cipher, Device, Folder, Send, User},
|
||||
db::models::{AuthRequestId, Cipher, Device, DeviceId, Folder, Send, User, UserId},
|
||||
http_client::make_http_request,
|
||||
util::format_date,
|
||||
CONFIG,
|
||||
@@ -126,15 +126,15 @@ pub async fn register_push_device(device: &mut Device, conn: &mut crate::db::DbC
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn unregister_push_device(push_uuid: Option<String>) -> EmptyResult {
|
||||
if !CONFIG.push_enabled() || push_uuid.is_none() {
|
||||
pub async fn unregister_push_device(push_id: Option<String>) -> EmptyResult {
|
||||
if !CONFIG.push_enabled() || push_id.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
let auth_push_token = get_auth_push_token().await?;
|
||||
|
||||
let auth_header = format!("Bearer {}", &auth_push_token);
|
||||
|
||||
match make_http_request(Method::DELETE, &(CONFIG.push_relay_uri() + "/push/" + &push_uuid.unwrap()))?
|
||||
match make_http_request(Method::DELETE, &(CONFIG.push_relay_uri() + "/push/" + &push_id.unwrap()))?
|
||||
.header(AUTHORIZATION, auth_header)
|
||||
.send()
|
||||
.await
|
||||
@@ -148,27 +148,24 @@ pub async fn unregister_push_device(push_uuid: Option<String>) -> EmptyResult {
|
||||
pub async fn push_cipher_update(
|
||||
ut: UpdateType,
|
||||
cipher: &Cipher,
|
||||
acting_device_uuid: &String,
|
||||
acting_device_id: &DeviceId,
|
||||
conn: &mut crate::db::DbConn,
|
||||
) {
|
||||
// We shouldn't send a push notification on cipher update if the cipher belongs to an organization, this isn't implemented in the upstream server too.
|
||||
if cipher.organization_uuid.is_some() {
|
||||
return;
|
||||
};
|
||||
let user_uuid = match &cipher.user_uuid {
|
||||
Some(c) => c,
|
||||
None => {
|
||||
debug!("Cipher has no uuid");
|
||||
return;
|
||||
}
|
||||
let Some(user_id) = &cipher.user_uuid else {
|
||||
debug!("Cipher has no uuid");
|
||||
return;
|
||||
};
|
||||
|
||||
if Device::check_user_has_push_device(user_uuid, conn).await {
|
||||
if Device::check_user_has_push_device(user_id, conn).await {
|
||||
send_to_push_relay(json!({
|
||||
"userId": user_uuid,
|
||||
"userId": user_id,
|
||||
"organizationId": (),
|
||||
"deviceId": acting_device_uuid,
|
||||
"identifier": acting_device_uuid,
|
||||
"deviceId": acting_device_id,
|
||||
"identifier": acting_device_id,
|
||||
"type": ut as i32,
|
||||
"payload": {
|
||||
"Id": cipher.uuid,
|
||||
@@ -181,14 +178,14 @@ pub async fn push_cipher_update(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_logout(user: &User, acting_device_uuid: Option<String>) {
|
||||
let acting_device_uuid: Value = acting_device_uuid.map(|v| v.into()).unwrap_or_else(|| Value::Null);
|
||||
pub fn push_logout(user: &User, acting_device_id: Option<DeviceId>) {
|
||||
let acting_device_id: Value = acting_device_id.map(|v| v.to_string().into()).unwrap_or_else(|| Value::Null);
|
||||
|
||||
tokio::task::spawn(send_to_push_relay(json!({
|
||||
"userId": user.uuid,
|
||||
"organizationId": (),
|
||||
"deviceId": acting_device_uuid,
|
||||
"identifier": acting_device_uuid,
|
||||
"deviceId": acting_device_id,
|
||||
"identifier": acting_device_id,
|
||||
"type": UpdateType::LogOut as i32,
|
||||
"payload": {
|
||||
"UserId": user.uuid,
|
||||
@@ -214,15 +211,15 @@ pub fn push_user_update(ut: UpdateType, user: &User) {
|
||||
pub async fn push_folder_update(
|
||||
ut: UpdateType,
|
||||
folder: &Folder,
|
||||
acting_device_uuid: &String,
|
||||
acting_device_id: &DeviceId,
|
||||
conn: &mut crate::db::DbConn,
|
||||
) {
|
||||
if Device::check_user_has_push_device(&folder.user_uuid, conn).await {
|
||||
tokio::task::spawn(send_to_push_relay(json!({
|
||||
"userId": folder.user_uuid,
|
||||
"organizationId": (),
|
||||
"deviceId": acting_device_uuid,
|
||||
"identifier": acting_device_uuid,
|
||||
"deviceId": acting_device_id,
|
||||
"identifier": acting_device_id,
|
||||
"type": ut as i32,
|
||||
"payload": {
|
||||
"Id": folder.uuid,
|
||||
@@ -233,14 +230,14 @@ pub async fn push_folder_update(
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn push_send_update(ut: UpdateType, send: &Send, acting_device_uuid: &String, conn: &mut crate::db::DbConn) {
|
||||
pub async fn push_send_update(ut: UpdateType, send: &Send, acting_device_id: &DeviceId, conn: &mut crate::db::DbConn) {
|
||||
if let Some(s) = &send.user_uuid {
|
||||
if Device::check_user_has_push_device(s, conn).await {
|
||||
tokio::task::spawn(send_to_push_relay(json!({
|
||||
"userId": send.user_uuid,
|
||||
"organizationId": (),
|
||||
"deviceId": acting_device_uuid,
|
||||
"identifier": acting_device_uuid,
|
||||
"deviceId": acting_device_id,
|
||||
"identifier": acting_device_id,
|
||||
"type": ut as i32,
|
||||
"payload": {
|
||||
"Id": send.uuid,
|
||||
@@ -260,7 +257,7 @@ async fn send_to_push_relay(notification_data: Value) {
|
||||
let auth_push_token = match get_auth_push_token().await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
debug!("Could not get the auth push token: {}", e);
|
||||
debug!("Could not get the auth push token: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -270,7 +267,7 @@ async fn send_to_push_relay(notification_data: Value) {
|
||||
let req = match make_http_request(Method::POST, &(CONFIG.push_relay_uri() + "/push/send")) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
error!("An error occurred while sending a send update to the push relay: {}", e);
|
||||
error!("An error occurred while sending a send update to the push relay: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -283,42 +280,42 @@ async fn send_to_push_relay(notification_data: Value) {
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
error!("An error occurred while sending a send update to the push relay: {}", e);
|
||||
error!("An error occurred while sending a send update to the push relay: {e}");
|
||||
};
|
||||
}
|
||||
|
||||
pub async fn push_auth_request(user_uuid: String, auth_request_uuid: String, conn: &mut crate::db::DbConn) {
|
||||
if Device::check_user_has_push_device(user_uuid.as_str(), conn).await {
|
||||
pub async fn push_auth_request(user_id: UserId, auth_request_id: String, conn: &mut crate::db::DbConn) {
|
||||
if Device::check_user_has_push_device(&user_id, conn).await {
|
||||
tokio::task::spawn(send_to_push_relay(json!({
|
||||
"userId": user_uuid,
|
||||
"userId": user_id,
|
||||
"organizationId": (),
|
||||
"deviceId": null,
|
||||
"identifier": null,
|
||||
"type": UpdateType::AuthRequest as i32,
|
||||
"payload": {
|
||||
"Id": auth_request_uuid,
|
||||
"UserId": user_uuid,
|
||||
"Id": auth_request_id,
|
||||
"UserId": user_id,
|
||||
}
|
||||
})));
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn push_auth_response(
|
||||
user_uuid: String,
|
||||
auth_request_uuid: String,
|
||||
approving_device_uuid: String,
|
||||
user_id: &UserId,
|
||||
auth_request_id: &AuthRequestId,
|
||||
approving_device_id: &DeviceId,
|
||||
conn: &mut crate::db::DbConn,
|
||||
) {
|
||||
if Device::check_user_has_push_device(user_uuid.as_str(), conn).await {
|
||||
if Device::check_user_has_push_device(user_id, conn).await {
|
||||
tokio::task::spawn(send_to_push_relay(json!({
|
||||
"userId": user_uuid,
|
||||
"userId": user_id,
|
||||
"organizationId": (),
|
||||
"deviceId": approving_device_uuid,
|
||||
"identifier": approving_device_uuid,
|
||||
"deviceId": approving_device_id,
|
||||
"identifier": approving_device_id,
|
||||
"type": UpdateType::AuthRequestResponse as i32,
|
||||
"payload": {
|
||||
"Id": auth_request_uuid,
|
||||
"UserId": user_uuid,
|
||||
"Id": auth_request_id,
|
||||
"UserId": user_id,
|
||||
}
|
||||
})));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use once_cell::sync::Lazy;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use rocket::{
|
||||
@@ -13,8 +12,9 @@ use serde_json::Value;
|
||||
use crate::{
|
||||
api::{core::now, ApiResult, EmptyResult},
|
||||
auth::decode_file_download,
|
||||
db::models::{AttachmentId, CipherId},
|
||||
error::Error,
|
||||
util::{get_web_vault_version, Cached, SafeString},
|
||||
util::Cached,
|
||||
CONFIG,
|
||||
};
|
||||
|
||||
@@ -54,46 +54,10 @@ fn not_found() -> ApiResult<Html<String>> {
|
||||
|
||||
#[get("/css/vaultwarden.css")]
|
||||
fn vaultwarden_css() -> Cached<Css<String>> {
|
||||
// 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<u32> = Lazy::new(|| {
|
||||
let re = regex::Regex::new(r"(\d{4})\.(\d{1,2})\.(\d{1,2})").unwrap();
|
||||
let vault_version = get_web_vault_version();
|
||||
|
||||
let (major, minor, patch) = match re.captures(&vault_version) {
|
||||
Some(c) if c.len() == 4 => (
|
||||
c.get(1).unwrap().as_str().parse().unwrap(),
|
||||
c.get(2).unwrap().as_str().parse().unwrap(),
|
||||
c.get(3).unwrap().as_str().parse().unwrap(),
|
||||
),
|
||||
_ => (2024, 6, 2),
|
||||
};
|
||||
format!("{major}{minor:02}{patch:02}").parse::<u32>().unwrap()
|
||||
});
|
||||
|
||||
// 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<u32> = Lazy::new(|| {
|
||||
let re = regex::Regex::new(r"(\d{1})\.(\d{1,2})\.(\d{1,2})").unwrap();
|
||||
let vw_version = crate::VERSION.unwrap_or("1.32.1");
|
||||
|
||||
let (major, minor, patch) = match re.captures(vw_version) {
|
||||
Some(c) if c.len() == 4 => (
|
||||
c.get(1).unwrap().as_str().parse().unwrap(),
|
||||
c.get(2).unwrap().as_str().parse().unwrap(),
|
||||
c.get(3).unwrap().as_str().parse().unwrap(),
|
||||
),
|
||||
_ => (1, 32, 1),
|
||||
};
|
||||
format!("{major}{minor:02}{patch:02}").parse::<u32>().unwrap()
|
||||
});
|
||||
|
||||
let css_options = json!({
|
||||
"web_vault_version": *WEB_VAULT_VERSION,
|
||||
"vw_version": *VW_VERSION,
|
||||
"signup_disabled": !CONFIG.signups_allowed() && CONFIG.signups_domains_whitelist().is_empty(),
|
||||
"mail_enabled": CONFIG.mail_enabled(),
|
||||
"yubico_enabled": CONFIG._enable_yubico() && (CONFIG.yubico_client_id().is_some() == CONFIG.yubico_secret_key().is_some()),
|
||||
"yubico_enabled": CONFIG._enable_yubico() && CONFIG.yubico_client_id().is_some() && CONFIG.yubico_secret_key().is_some(),
|
||||
"emergency_access_allowed": CONFIG.emergency_access_allowed(),
|
||||
"sends_allowed": CONFIG.sends_allowed(),
|
||||
"load_user_scss": true,
|
||||
@@ -195,16 +159,16 @@ async fn web_files(p: PathBuf) -> Cached<Option<NamedFile>> {
|
||||
Cached::long(NamedFile::open(Path::new(&CONFIG.web_vault_folder()).join(p)).await.ok(), true)
|
||||
}
|
||||
|
||||
#[get("/attachments/<uuid>/<file_id>?<token>")]
|
||||
async fn attachments(uuid: SafeString, file_id: SafeString, token: String) -> Option<NamedFile> {
|
||||
#[get("/attachments/<cipher_id>/<file_id>?<token>")]
|
||||
async fn attachments(cipher_id: CipherId, file_id: AttachmentId, token: String) -> Option<NamedFile> {
|
||||
let Ok(claims) = decode_file_download(&token) else {
|
||||
return None;
|
||||
};
|
||||
if claims.sub != *uuid || claims.file_id != *file_id {
|
||||
if claims.sub != cipher_id || claims.file_id != file_id {
|
||||
return None;
|
||||
}
|
||||
|
||||
NamedFile::open(Path::new(&CONFIG.attachments_folder()).join(uuid).join(file_id)).await.ok()
|
||||
NamedFile::open(Path::new(&CONFIG.attachments_folder()).join(cipher_id.as_ref()).join(file_id.as_ref())).await.ok()
|
||||
}
|
||||
|
||||
// We use DbConn here to let the alive healthcheck also verify the database connection.
|
||||
|
||||
256
src/auth.rs
256
src/auth.rs
@@ -14,6 +14,10 @@ use std::{
|
||||
net::IpAddr,
|
||||
};
|
||||
|
||||
use crate::db::models::{
|
||||
AttachmentId, CipherId, CollectionId, DeviceId, EmergencyAccessId, MembershipId, OrgApiKeyId, OrganizationId,
|
||||
SendFileId, SendId, UserId,
|
||||
};
|
||||
use crate::{error::Error, CONFIG};
|
||||
|
||||
const JWT_ALGORITHM: Algorithm = Algorithm::RS256;
|
||||
@@ -31,6 +35,7 @@ static JWT_ADMIN_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|admin", CONFIG.
|
||||
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();
|
||||
@@ -141,6 +146,10 @@ pub fn decode_file_download(token: &str) -> Result<FileDownloadClaims, Error> {
|
||||
decode_jwt(token, JWT_FILE_DOWNLOAD_ISSUER.to_string())
|
||||
}
|
||||
|
||||
pub fn decode_register_verify(token: &str) -> Result<RegisterVerifyClaims, Error> {
|
||||
decode_jwt(token, JWT_REGISTER_VERIFY_ISSUER.to_string())
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct LoginJwtClaims {
|
||||
// Not before
|
||||
@@ -150,7 +159,7 @@ pub struct LoginJwtClaims {
|
||||
// Issuer
|
||||
pub iss: String,
|
||||
// Subject
|
||||
pub sub: String,
|
||||
pub sub: UserId,
|
||||
|
||||
pub premium: bool,
|
||||
pub name: String,
|
||||
@@ -171,7 +180,7 @@ pub struct LoginJwtClaims {
|
||||
// user security_stamp
|
||||
pub sstamp: String,
|
||||
// device uuid
|
||||
pub device: String,
|
||||
pub device: DeviceId,
|
||||
// [ "api", "offline_access" ]
|
||||
pub scope: Vec<String>,
|
||||
// [ "Application" ]
|
||||
@@ -187,19 +196,19 @@ pub struct InviteJwtClaims {
|
||||
// Issuer
|
||||
pub iss: String,
|
||||
// Subject
|
||||
pub sub: String,
|
||||
pub sub: UserId,
|
||||
|
||||
pub email: String,
|
||||
pub org_id: Option<String>,
|
||||
pub user_org_id: Option<String>,
|
||||
pub org_id: OrganizationId,
|
||||
pub member_id: MembershipId,
|
||||
pub invited_by_email: Option<String>,
|
||||
}
|
||||
|
||||
pub fn generate_invite_claims(
|
||||
uuid: String,
|
||||
user_id: UserId,
|
||||
email: String,
|
||||
org_id: Option<String>,
|
||||
user_org_id: Option<String>,
|
||||
org_id: OrganizationId,
|
||||
member_id: MembershipId,
|
||||
invited_by_email: Option<String>,
|
||||
) -> InviteJwtClaims {
|
||||
let time_now = Utc::now();
|
||||
@@ -208,10 +217,10 @@ pub fn generate_invite_claims(
|
||||
nbf: time_now.timestamp(),
|
||||
exp: (time_now + TimeDelta::try_hours(expire_hours).unwrap()).timestamp(),
|
||||
iss: JWT_INVITE_ISSUER.to_string(),
|
||||
sub: uuid,
|
||||
sub: user_id,
|
||||
email,
|
||||
org_id,
|
||||
user_org_id,
|
||||
member_id,
|
||||
invited_by_email,
|
||||
}
|
||||
}
|
||||
@@ -225,18 +234,18 @@ pub struct EmergencyAccessInviteJwtClaims {
|
||||
// Issuer
|
||||
pub iss: String,
|
||||
// Subject
|
||||
pub sub: String,
|
||||
pub sub: UserId,
|
||||
|
||||
pub email: String,
|
||||
pub emer_id: String,
|
||||
pub emer_id: EmergencyAccessId,
|
||||
pub grantor_name: String,
|
||||
pub grantor_email: String,
|
||||
}
|
||||
|
||||
pub fn generate_emergency_access_invite_claims(
|
||||
uuid: String,
|
||||
user_id: UserId,
|
||||
email: String,
|
||||
emer_id: String,
|
||||
emer_id: EmergencyAccessId,
|
||||
grantor_name: String,
|
||||
grantor_email: String,
|
||||
) -> EmergencyAccessInviteJwtClaims {
|
||||
@@ -246,7 +255,7 @@ pub fn generate_emergency_access_invite_claims(
|
||||
nbf: time_now.timestamp(),
|
||||
exp: (time_now + TimeDelta::try_hours(expire_hours).unwrap()).timestamp(),
|
||||
iss: JWT_EMERGENCY_ACCESS_INVITE_ISSUER.to_string(),
|
||||
sub: uuid,
|
||||
sub: user_id,
|
||||
email,
|
||||
emer_id,
|
||||
grantor_name,
|
||||
@@ -263,20 +272,23 @@ pub struct OrgApiKeyLoginJwtClaims {
|
||||
// Issuer
|
||||
pub iss: String,
|
||||
// Subject
|
||||
pub sub: String,
|
||||
pub sub: OrgApiKeyId,
|
||||
|
||||
pub client_id: String,
|
||||
pub client_sub: String,
|
||||
pub client_sub: OrganizationId,
|
||||
pub scope: Vec<String>,
|
||||
}
|
||||
|
||||
pub fn generate_organization_api_key_login_claims(uuid: String, org_id: String) -> OrgApiKeyLoginJwtClaims {
|
||||
pub fn generate_organization_api_key_login_claims(
|
||||
org_api_key_uuid: OrgApiKeyId,
|
||||
org_id: OrganizationId,
|
||||
) -> OrgApiKeyLoginJwtClaims {
|
||||
let time_now = Utc::now();
|
||||
OrgApiKeyLoginJwtClaims {
|
||||
nbf: time_now.timestamp(),
|
||||
exp: (time_now + TimeDelta::try_hours(1).unwrap()).timestamp(),
|
||||
iss: JWT_ORG_API_KEY_ISSUER.to_string(),
|
||||
sub: uuid,
|
||||
sub: org_api_key_uuid,
|
||||
client_id: format!("organization.{org_id}"),
|
||||
client_sub: org_id,
|
||||
scope: vec!["api.organization".into()],
|
||||
@@ -292,22 +304,49 @@ pub struct FileDownloadClaims {
|
||||
// Issuer
|
||||
pub iss: String,
|
||||
// Subject
|
||||
pub sub: String,
|
||||
pub sub: CipherId,
|
||||
|
||||
pub file_id: String,
|
||||
pub file_id: AttachmentId,
|
||||
}
|
||||
|
||||
pub fn generate_file_download_claims(uuid: String, file_id: String) -> FileDownloadClaims {
|
||||
pub fn generate_file_download_claims(cipher_id: CipherId, file_id: AttachmentId) -> FileDownloadClaims {
|
||||
let time_now = Utc::now();
|
||||
FileDownloadClaims {
|
||||
nbf: time_now.timestamp(),
|
||||
exp: (time_now + TimeDelta::try_minutes(5).unwrap()).timestamp(),
|
||||
iss: JWT_FILE_DOWNLOAD_ISSUER.to_string(),
|
||||
sub: uuid,
|
||||
sub: cipher_id,
|
||||
file_id,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct RegisterVerifyClaims {
|
||||
// Not before
|
||||
pub nbf: i64,
|
||||
// Expiration time
|
||||
pub exp: i64,
|
||||
// Issuer
|
||||
pub iss: String,
|
||||
// Subject
|
||||
pub sub: String,
|
||||
|
||||
pub name: Option<String>,
|
||||
pub verified: bool,
|
||||
}
|
||||
|
||||
pub fn generate_register_verify_claims(email: String, name: Option<String>, verified: bool) -> RegisterVerifyClaims {
|
||||
let time_now = Utc::now();
|
||||
RegisterVerifyClaims {
|
||||
nbf: time_now.timestamp(),
|
||||
exp: (time_now + TimeDelta::try_minutes(30).unwrap()).timestamp(),
|
||||
iss: JWT_REGISTER_VERIFY_ISSUER.to_string(),
|
||||
sub: email,
|
||||
name,
|
||||
verified,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct BasicJwtClaims {
|
||||
// Not before
|
||||
@@ -331,14 +370,14 @@ pub fn generate_delete_claims(uuid: String) -> BasicJwtClaims {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_verify_email_claims(uuid: String) -> 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 {
|
||||
nbf: time_now.timestamp(),
|
||||
exp: (time_now + TimeDelta::try_hours(expire_hours).unwrap()).timestamp(),
|
||||
iss: JWT_VERIFYEMAIL_ISSUER.to_string(),
|
||||
sub: uuid,
|
||||
sub: user_id.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -352,7 +391,7 @@ pub fn generate_admin_claims() -> BasicJwtClaims {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_send_claims(send_id: &str, file_id: &str) -> BasicJwtClaims {
|
||||
pub fn generate_send_claims(send_id: &SendId, file_id: &SendFileId) -> BasicJwtClaims {
|
||||
let time_now = Utc::now();
|
||||
BasicJwtClaims {
|
||||
nbf: time_now.timestamp(),
|
||||
@@ -371,7 +410,7 @@ use rocket::{
|
||||
};
|
||||
|
||||
use crate::db::{
|
||||
models::{Collection, Device, User, UserOrgStatus, UserOrgType, UserOrganization, UserStampException},
|
||||
models::{Collection, Device, Membership, MembershipStatus, MembershipType, User, UserStampException},
|
||||
DbConn,
|
||||
};
|
||||
|
||||
@@ -471,36 +510,32 @@ impl<'r> FromRequest<'r> for Headers {
|
||||
};
|
||||
|
||||
// Check JWT token is valid and get device and user from it
|
||||
let claims = match decode_login(access_token) {
|
||||
Ok(claims) => claims,
|
||||
Err(_) => err_handler!("Invalid claim"),
|
||||
let Ok(claims) = decode_login(access_token) else {
|
||||
err_handler!("Invalid claim")
|
||||
};
|
||||
|
||||
let device_uuid = claims.device;
|
||||
let user_uuid = claims.sub;
|
||||
let device_id = claims.device;
|
||||
let user_id = claims.sub;
|
||||
|
||||
let mut conn = match DbConn::from_request(request).await {
|
||||
Outcome::Success(conn) => conn,
|
||||
_ => err_handler!("Error getting DB"),
|
||||
};
|
||||
|
||||
let device = match Device::find_by_uuid_and_user(&device_uuid, &user_uuid, &mut conn).await {
|
||||
Some(device) => device,
|
||||
None => err_handler!("Invalid device id"),
|
||||
let Some(device) = Device::find_by_uuid_and_user(&device_id, &user_id, &mut conn).await else {
|
||||
err_handler!("Invalid device id")
|
||||
};
|
||||
|
||||
let user = match User::find_by_uuid(&user_uuid, &mut conn).await {
|
||||
Some(user) => user,
|
||||
None => err_handler!("Device has no user associated"),
|
||||
let Some(user) = User::find_by_uuid(&user_id, &mut conn).await else {
|
||||
err_handler!("Device has no user associated")
|
||||
};
|
||||
|
||||
if user.security_stamp != claims.sstamp {
|
||||
if let Some(stamp_exception) =
|
||||
user.stamp_exception.as_deref().and_then(|s| serde_json::from_str::<UserStampException>(s).ok())
|
||||
{
|
||||
let current_route = match request.route().and_then(|r| r.name.as_deref()) {
|
||||
Some(name) => name,
|
||||
_ => err_handler!("Error getting current route for stamp exception"),
|
||||
let Some(current_route) = request.route().and_then(|r| r.name.as_deref()) else {
|
||||
err_handler!("Error getting current route for stamp exception")
|
||||
};
|
||||
|
||||
// Check if the stamp exception has expired first.
|
||||
@@ -512,7 +547,7 @@ impl<'r> FromRequest<'r> for Headers {
|
||||
let mut user = user;
|
||||
user.reset_stamp_exception();
|
||||
if let Err(e) = user.save(&mut conn).await {
|
||||
error!("Error updating user: {:#?}", e);
|
||||
error!("Error updating user: {e:#?}");
|
||||
}
|
||||
err_handler!("Stamp exception is expired")
|
||||
} else if !stamp_exception.routes.contains(¤t_route.to_string()) {
|
||||
@@ -538,11 +573,30 @@ pub struct OrgHeaders {
|
||||
pub host: String,
|
||||
pub device: Device,
|
||||
pub user: User,
|
||||
pub org_user_type: UserOrgType,
|
||||
pub org_user: UserOrganization,
|
||||
pub membership_type: MembershipType,
|
||||
pub membership_status: MembershipStatus,
|
||||
pub membership: Membership,
|
||||
pub ip: ClientIp,
|
||||
}
|
||||
|
||||
impl OrgHeaders {
|
||||
fn is_member(&self) -> bool {
|
||||
// NOTE: we don't care about MembershipStatus at the moment because this is only used
|
||||
// where an invited, accepted or confirmed user is expected if this ever changes or
|
||||
// if from_i32 is changed to return Some(Revoked) this check needs to be changed accordingly
|
||||
self.membership_type >= MembershipType::User
|
||||
}
|
||||
fn is_confirmed_and_admin(&self) -> bool {
|
||||
self.membership_status == MembershipStatus::Confirmed && self.membership_type >= MembershipType::Admin
|
||||
}
|
||||
fn is_confirmed_and_manager(&self) -> bool {
|
||||
self.membership_status == MembershipStatus::Confirmed && self.membership_type >= MembershipType::Manager
|
||||
}
|
||||
fn is_confirmed_and_owner(&self) -> bool {
|
||||
self.membership_status == MembershipStatus::Confirmed && self.membership_type == MembershipType::Owner
|
||||
}
|
||||
}
|
||||
|
||||
#[rocket::async_trait]
|
||||
impl<'r> FromRequest<'r> for OrgHeaders {
|
||||
type Error = &'static str;
|
||||
@@ -553,55 +607,50 @@ impl<'r> FromRequest<'r> for OrgHeaders {
|
||||
// org_id is usually the second path param ("/organizations/<org_id>"),
|
||||
// but there are cases where it is a query value.
|
||||
// First check the path, if this is not a valid uuid, try the query values.
|
||||
let url_org_id: Option<&str> = {
|
||||
let mut url_org_id = None;
|
||||
if let Some(Ok(org_id)) = request.param::<&str>(1) {
|
||||
if uuid::Uuid::parse_str(org_id).is_ok() {
|
||||
url_org_id = Some(org_id);
|
||||
}
|
||||
let url_org_id: Option<OrganizationId> = {
|
||||
if let Some(Ok(org_id)) = request.param::<OrganizationId>(1) {
|
||||
Some(org_id.clone())
|
||||
} else if let Some(Ok(org_id)) = request.query_value::<OrganizationId>("organizationId") {
|
||||
Some(org_id.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
||||
if let Some(Ok(org_id)) = request.query_value::<&str>("organizationId") {
|
||||
if uuid::Uuid::parse_str(org_id).is_ok() {
|
||||
url_org_id = Some(org_id);
|
||||
}
|
||||
}
|
||||
|
||||
url_org_id
|
||||
};
|
||||
|
||||
match url_org_id {
|
||||
Some(org_id) => {
|
||||
Some(org_id) if uuid::Uuid::parse_str(&org_id).is_ok() => {
|
||||
let mut conn = match DbConn::from_request(request).await {
|
||||
Outcome::Success(conn) => conn,
|
||||
_ => err_handler!("Error getting DB"),
|
||||
};
|
||||
|
||||
let user = headers.user;
|
||||
let org_user = match UserOrganization::find_by_user_and_org(&user.uuid, org_id, &mut conn).await {
|
||||
Some(user) => {
|
||||
if user.status == UserOrgStatus::Confirmed as i32 {
|
||||
user
|
||||
} else {
|
||||
err_handler!("The current user isn't confirmed member of the organization")
|
||||
}
|
||||
}
|
||||
None => err_handler!("The current user isn't member of the organization"),
|
||||
let Some(membership) = Membership::find_by_user_and_org(&user.uuid, &org_id, &mut conn).await else {
|
||||
err_handler!("The current user isn't member of the organization");
|
||||
};
|
||||
|
||||
Outcome::Success(Self {
|
||||
host: headers.host,
|
||||
device: headers.device,
|
||||
user,
|
||||
org_user_type: {
|
||||
if let Some(org_usr_type) = UserOrgType::from_i32(org_user.atype) {
|
||||
org_usr_type
|
||||
membership_type: {
|
||||
if let Some(member_type) = MembershipType::from_i32(membership.atype) {
|
||||
member_type
|
||||
} else {
|
||||
// This should only happen if the DB is corrupted
|
||||
err_handler!("Unknown user type in the database")
|
||||
}
|
||||
},
|
||||
org_user,
|
||||
membership_status: {
|
||||
if let Some(member_status) = MembershipStatus::from_i32(membership.status) {
|
||||
// NOTE: add additional check for revoked if from_i32 is ever changed
|
||||
// to return Revoked status.
|
||||
member_status
|
||||
} else {
|
||||
err_handler!("User status is either revoked or invalid.")
|
||||
}
|
||||
},
|
||||
membership,
|
||||
ip: headers.ip,
|
||||
})
|
||||
}
|
||||
@@ -614,8 +663,9 @@ pub struct AdminHeaders {
|
||||
pub host: String,
|
||||
pub device: Device,
|
||||
pub user: User,
|
||||
pub org_user_type: UserOrgType,
|
||||
pub membership_type: MembershipType,
|
||||
pub ip: ClientIp,
|
||||
pub org_id: OrganizationId,
|
||||
}
|
||||
|
||||
#[rocket::async_trait]
|
||||
@@ -624,13 +674,14 @@ impl<'r> FromRequest<'r> for AdminHeaders {
|
||||
|
||||
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||
let headers = try_outcome!(OrgHeaders::from_request(request).await);
|
||||
if headers.org_user_type >= UserOrgType::Admin {
|
||||
if headers.is_confirmed_and_admin() {
|
||||
Outcome::Success(Self {
|
||||
host: headers.host,
|
||||
device: headers.device,
|
||||
user: headers.user,
|
||||
org_user_type: headers.org_user_type,
|
||||
membership_type: headers.membership_type,
|
||||
ip: headers.ip,
|
||||
org_id: headers.membership.org_uuid,
|
||||
})
|
||||
} else {
|
||||
err_handler!("You need to be Admin or Owner to call this endpoint")
|
||||
@@ -652,16 +703,16 @@ impl From<AdminHeaders> for Headers {
|
||||
// col_id is usually the fourth path param ("/organizations/<org_id>/collections/<col_id>"),
|
||||
// but there could be cases where it is a query value.
|
||||
// First check the path, if this is not a valid uuid, try the query values.
|
||||
fn get_col_id(request: &Request<'_>) -> Option<String> {
|
||||
fn get_col_id(request: &Request<'_>) -> Option<CollectionId> {
|
||||
if let Some(Ok(col_id)) = request.param::<String>(3) {
|
||||
if uuid::Uuid::parse_str(&col_id).is_ok() {
|
||||
return Some(col_id);
|
||||
return Some(col_id.into());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(Ok(col_id)) = request.query_value::<String>("collectionId") {
|
||||
if uuid::Uuid::parse_str(&col_id).is_ok() {
|
||||
return Some(col_id);
|
||||
return Some(col_id.into());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -676,6 +727,7 @@ pub struct ManagerHeaders {
|
||||
pub device: Device,
|
||||
pub user: User,
|
||||
pub ip: ClientIp,
|
||||
pub org_id: OrganizationId,
|
||||
}
|
||||
|
||||
#[rocket::async_trait]
|
||||
@@ -684,7 +736,7 @@ impl<'r> FromRequest<'r> for ManagerHeaders {
|
||||
|
||||
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||
let headers = try_outcome!(OrgHeaders::from_request(request).await);
|
||||
if headers.org_user_type >= UserOrgType::Manager {
|
||||
if headers.is_confirmed_and_manager() {
|
||||
match get_col_id(request) {
|
||||
Some(col_id) => {
|
||||
let mut conn = match DbConn::from_request(request).await {
|
||||
@@ -692,7 +744,7 @@ impl<'r> FromRequest<'r> for ManagerHeaders {
|
||||
_ => err_handler!("Error getting DB"),
|
||||
};
|
||||
|
||||
if !Collection::can_access_collection(&headers.org_user, &col_id, &mut conn).await {
|
||||
if !Collection::can_access_collection(&headers.membership, &col_id, &mut conn).await {
|
||||
err_handler!("The current user isn't a manager for this collection")
|
||||
}
|
||||
}
|
||||
@@ -704,6 +756,7 @@ impl<'r> FromRequest<'r> for ManagerHeaders {
|
||||
device: headers.device,
|
||||
user: headers.user,
|
||||
ip: headers.ip,
|
||||
org_id: headers.membership.org_uuid,
|
||||
})
|
||||
} else {
|
||||
err_handler!("You need to be a Manager, Admin or Owner to call this endpoint")
|
||||
@@ -728,7 +781,7 @@ pub struct ManagerHeadersLoose {
|
||||
pub host: String,
|
||||
pub device: Device,
|
||||
pub user: User,
|
||||
pub org_user: UserOrganization,
|
||||
pub membership: Membership,
|
||||
pub ip: ClientIp,
|
||||
}
|
||||
|
||||
@@ -738,12 +791,12 @@ impl<'r> FromRequest<'r> for ManagerHeadersLoose {
|
||||
|
||||
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||
let headers = try_outcome!(OrgHeaders::from_request(request).await);
|
||||
if headers.org_user_type >= UserOrgType::Manager {
|
||||
if headers.is_confirmed_and_manager() {
|
||||
Outcome::Success(Self {
|
||||
host: headers.host,
|
||||
device: headers.device,
|
||||
user: headers.user,
|
||||
org_user: headers.org_user,
|
||||
membership: headers.membership,
|
||||
ip: headers.ip,
|
||||
})
|
||||
} else {
|
||||
@@ -766,14 +819,14 @@ impl From<ManagerHeadersLoose> for Headers {
|
||||
impl ManagerHeaders {
|
||||
pub async fn from_loose(
|
||||
h: ManagerHeadersLoose,
|
||||
collections: &Vec<String>,
|
||||
collections: &Vec<CollectionId>,
|
||||
conn: &mut DbConn,
|
||||
) -> Result<ManagerHeaders, Error> {
|
||||
for col_id in collections {
|
||||
if uuid::Uuid::parse_str(col_id).is_err() {
|
||||
if uuid::Uuid::parse_str(col_id.as_ref()).is_err() {
|
||||
err!("Collection Id is malformed!");
|
||||
}
|
||||
if !Collection::can_access_collection(&h.org_user, col_id, conn).await {
|
||||
if !Collection::can_access_collection(&h.membership, col_id, conn).await {
|
||||
err!("You don't have access to all collections!");
|
||||
}
|
||||
}
|
||||
@@ -783,6 +836,7 @@ impl ManagerHeaders {
|
||||
device: h.device,
|
||||
user: h.user,
|
||||
ip: h.ip,
|
||||
org_id: h.membership.org_uuid,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -791,6 +845,7 @@ pub struct OwnerHeaders {
|
||||
pub device: Device,
|
||||
pub user: User,
|
||||
pub ip: ClientIp,
|
||||
pub org_id: OrganizationId,
|
||||
}
|
||||
|
||||
#[rocket::async_trait]
|
||||
@@ -799,11 +854,12 @@ impl<'r> FromRequest<'r> for OwnerHeaders {
|
||||
|
||||
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||
let headers = try_outcome!(OrgHeaders::from_request(request).await);
|
||||
if headers.org_user_type == UserOrgType::Owner {
|
||||
if headers.is_confirmed_and_owner() {
|
||||
Outcome::Success(Self {
|
||||
device: headers.device,
|
||||
user: headers.user,
|
||||
ip: headers.ip,
|
||||
org_id: headers.membership.org_uuid,
|
||||
})
|
||||
} else {
|
||||
err_handler!("You need to be Owner to call this endpoint")
|
||||
@@ -811,6 +867,30 @@ impl<'r> FromRequest<'r> for OwnerHeaders {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct OrgMemberHeaders {
|
||||
pub host: String,
|
||||
pub user: User,
|
||||
pub org_id: OrganizationId,
|
||||
}
|
||||
|
||||
#[rocket::async_trait]
|
||||
impl<'r> FromRequest<'r> for OrgMemberHeaders {
|
||||
type Error = &'static str;
|
||||
|
||||
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||
let headers = try_outcome!(OrgHeaders::from_request(request).await);
|
||||
if headers.is_member() {
|
||||
Outcome::Success(Self {
|
||||
host: headers.host,
|
||||
user: headers.user,
|
||||
org_id: headers.membership.org_uuid,
|
||||
})
|
||||
} else {
|
||||
err_handler!("You need to be a Member of the Organization to call this endpoint")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Client IP address detection
|
||||
//
|
||||
@@ -831,7 +911,7 @@ impl<'r> FromRequest<'r> for ClientIp {
|
||||
None => ip,
|
||||
}
|
||||
.parse()
|
||||
.map_err(|_| warn!("'{}' header is malformed: {}", CONFIG.ip_header(), ip))
|
||||
.map_err(|_| warn!("'{}' header is malformed: {ip}", CONFIG.ip_header()))
|
||||
.ok()
|
||||
})
|
||||
} else {
|
||||
|
||||
140
src/config.rs
140
src/config.rs
@@ -1,8 +1,10 @@
|
||||
use std::env::consts::EXE_SUFFIX;
|
||||
use std::process::exit;
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
RwLock,
|
||||
use std::{
|
||||
env::consts::EXE_SUFFIX,
|
||||
process::exit,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
RwLock,
|
||||
},
|
||||
};
|
||||
|
||||
use job_scheduler_ng::Schedule;
|
||||
@@ -12,7 +14,7 @@ use reqwest::Url;
|
||||
use crate::{
|
||||
db::DbConnType,
|
||||
error::Error,
|
||||
util::{get_env, get_env_bool, parse_experimental_client_feature_flags},
|
||||
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(|| {
|
||||
@@ -102,7 +104,7 @@ macro_rules! make_config {
|
||||
|
||||
let mut builder = ConfigBuilder::default();
|
||||
$($(
|
||||
builder.$name = make_config! { @getenv paste::paste!(stringify!([<$name:upper>])), $ty };
|
||||
builder.$name = make_config! { @getenv pastey::paste!(stringify!([<$name:upper>])), $ty };
|
||||
)+)+
|
||||
|
||||
builder
|
||||
@@ -114,6 +116,14 @@ macro_rules! make_config {
|
||||
serde_json::from_str(&config_str).map_err(Into::into)
|
||||
}
|
||||
|
||||
fn clear_non_editable(&mut self) {
|
||||
$($(
|
||||
if !$editable {
|
||||
self.$name = None;
|
||||
}
|
||||
)+)+
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
@@ -123,7 +133,7 @@ macro_rules! make_config {
|
||||
builder.$name = v.clone();
|
||||
|
||||
if self.$name.is_some() {
|
||||
overrides.push(paste::paste!(stringify!([<$name:upper>])).into());
|
||||
overrides.push(pastey::paste!(stringify!([<$name:upper>])).into());
|
||||
}
|
||||
}
|
||||
)+)+
|
||||
@@ -221,7 +231,7 @@ macro_rules! make_config {
|
||||
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(&paste::paste!(stringify!([<$name:upper>])).into())).into());
|
||||
element.insert("overridden".into(), (overridden.contains(&pastey::paste!(stringify!([<$name:upper>])).into())).into());
|
||||
element
|
||||
}),
|
||||
)+
|
||||
@@ -238,6 +248,7 @@ macro_rules! make_config {
|
||||
// Besides Pass, only String types will be masked via _privacy_mask.
|
||||
const PRIVACY_CONFIG: &[&str] = &[
|
||||
"allowed_iframe_ancestors",
|
||||
"allowed_connect_src",
|
||||
"database_url",
|
||||
"domain_origin",
|
||||
"domain_path",
|
||||
@@ -248,6 +259,7 @@ macro_rules! make_config {
|
||||
"smtp_from",
|
||||
"smtp_host",
|
||||
"smtp_username",
|
||||
"_smtp_img_src",
|
||||
];
|
||||
|
||||
let cfg = {
|
||||
@@ -363,19 +375,19 @@ make_config! {
|
||||
/// Data folder |> Main data folder
|
||||
data_folder: String, false, def, "data".to_string();
|
||||
/// Database URL
|
||||
database_url: String, false, auto, |c| format!("{}/{}", c.data_folder, "db.sqlite3");
|
||||
database_url: String, false, auto, |c| format!("{}/db.sqlite3", c.data_folder);
|
||||
/// Icon cache folder
|
||||
icon_cache_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "icon_cache");
|
||||
icon_cache_folder: String, false, auto, |c| format!("{}/icon_cache", c.data_folder);
|
||||
/// Attachments folder
|
||||
attachments_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "attachments");
|
||||
attachments_folder: String, false, auto, |c| format!("{}/attachments", c.data_folder);
|
||||
/// Sends folder
|
||||
sends_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "sends");
|
||||
sends_folder: String, false, auto, |c| format!("{}/sends", c.data_folder);
|
||||
/// Temp folder |> Used for storing temporary file uploads
|
||||
tmp_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "tmp");
|
||||
tmp_folder: String, false, auto, |c| format!("{}/tmp", c.data_folder);
|
||||
/// Templates folder
|
||||
templates_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "templates");
|
||||
templates_folder: String, false, auto, |c| format!("{}/templates", c.data_folder);
|
||||
/// Session JWT key
|
||||
rsa_key_filename: String, false, auto, |c| format!("{}/{}", c.data_folder, "rsa_key");
|
||||
rsa_key_filename: String, false, auto, |c| format!("{}/rsa_key", c.data_folder);
|
||||
/// Web vault folder
|
||||
web_vault_folder: String, false, def, "web-vault/".to_string();
|
||||
},
|
||||
@@ -472,7 +484,8 @@ make_config! {
|
||||
disable_icon_download: bool, true, def, false;
|
||||
/// Allow new signups |> Controls whether new users can register. Users can be invited by the vaultwarden admin even if this is disabled
|
||||
signups_allowed: bool, true, def, true;
|
||||
/// Require email verification on signups. This will prevent logins from succeeding until the address has been verified
|
||||
/// Require email verification on signups. On new client versions, this will require verification at signup time. On older clients,
|
||||
/// this will prevent logins from succeeding until the address has been verified
|
||||
signups_verify: bool, true, def, false;
|
||||
/// If signups require email verification, automatically re-send verification email if it hasn't been sent for a while (in seconds)
|
||||
signups_verify_resend_time: u64, true, def, 3_600;
|
||||
@@ -609,6 +622,9 @@ make_config! {
|
||||
/// Allowed iframe ancestors (Know the risks!) |> Allows other domains to embed the web vault into an iframe, useful for embedding into secure intranets
|
||||
allowed_iframe_ancestors: String, true, def, String::new();
|
||||
|
||||
/// Allowed connect-src (Know the risks!) |> Allows other domains to URLs which can be loaded using script interfaces like the Forwarded email alias feature
|
||||
allowed_connect_src: String, true, def, String::new();
|
||||
|
||||
/// Seconds between login requests |> Number of seconds, on average, between login and 2FA requests from the same IP address before rate limiting kicks in
|
||||
login_ratelimit_seconds: u64, false, def, 60;
|
||||
/// Max burst size for login requests |> Allow a burst of requests of up to this size, while maintaining the average indicated by `login_ratelimit_seconds`. Note that this applies to both the login and the 2FA, so it's recommended to allow a burst size of at least 2
|
||||
@@ -655,9 +671,9 @@ make_config! {
|
||||
_enable_duo: bool, true, def, true;
|
||||
/// Attempt to use deprecated iframe-based Traditional Prompt (Duo WebSDK 2)
|
||||
duo_use_iframe: bool, false, def, false;
|
||||
/// Integration Key
|
||||
/// Client Id
|
||||
duo_ikey: String, true, option;
|
||||
/// Secret Key
|
||||
/// Client Secret
|
||||
duo_skey: Pass, true, option;
|
||||
/// Host
|
||||
duo_host: String, true, option;
|
||||
@@ -672,7 +688,7 @@ make_config! {
|
||||
/// Use Sendmail |> Whether to send mail via the `sendmail` command
|
||||
use_sendmail: bool, true, def, false;
|
||||
/// Sendmail Command |> Which sendmail command to use. The one found in the $PATH is used if not specified.
|
||||
sendmail_command: String, true, option;
|
||||
sendmail_command: String, false, option;
|
||||
/// Host
|
||||
smtp_host: String, true, option;
|
||||
/// DEPRECATED smtp_ssl |> DEPRECATED - Please use SMTP_SECURITY
|
||||
@@ -719,7 +735,7 @@ make_config! {
|
||||
email_expiration_time: u64, true, def, 600;
|
||||
/// Maximum attempts |> Maximum attempts before an email token is reset and a new email will need to be sent
|
||||
email_attempts_limit: u64, true, def, 3;
|
||||
/// Automatically enforce at login |> Setup email 2FA provider regardless of any organization policy
|
||||
/// Setup email 2FA at signup |> Setup email 2FA provider on registration regardless of any organization policy
|
||||
email_2fa_enforce_on_verified_invite: bool, true, def, false;
|
||||
/// Auto-enable 2FA (Know the risks!) |> Automatically setup email 2FA as fallback provider when needed
|
||||
email_2fa_auto_fallback: bool, true, def, false;
|
||||
@@ -760,6 +776,13 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
|
||||
);
|
||||
}
|
||||
|
||||
let connect_src = cfg.allowed_connect_src.to_lowercase();
|
||||
for url in connect_src.split_whitespace() {
|
||||
if !url.starts_with("https://") || Url::parse(url).is_err() {
|
||||
err!("ALLOWED_CONNECT_SRC variable contains one or more invalid URLs. Only FQDN's starting with https are allowed");
|
||||
}
|
||||
}
|
||||
|
||||
let whitelist = &cfg.signups_domains_whitelist;
|
||||
if !whitelist.is_empty() && whitelist.split(',').any(|d| d.trim().is_empty()) {
|
||||
err!("`SIGNUPS_DOMAINS_WHITELIST` contains empty tokens");
|
||||
@@ -817,8 +840,14 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
|
||||
"browser-fileless-import",
|
||||
"extension-refresh",
|
||||
"fido2-vault-credentials",
|
||||
"inline-menu-positioning-improvements",
|
||||
"ssh-key-vault-item",
|
||||
"ssh-agent",
|
||||
"anon-addy-self-host-alias",
|
||||
"simple-login-self-host-alias",
|
||||
"mutual-tls",
|
||||
"export-attachments",
|
||||
"inline-menu-totp",
|
||||
];
|
||||
let configured_flags = parse_experimental_client_feature_flags(&cfg.experimental_client_feature_flags);
|
||||
let invalid_flags: Vec<_> = configured_flags.keys().filter(|flag| !KNOWN_FLAGS.contains(&flag.as_str())).collect();
|
||||
@@ -880,12 +909,12 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
|
||||
let command = cfg.sendmail_command.clone().unwrap_or_else(|| format!("sendmail{EXE_SUFFIX}"));
|
||||
|
||||
let mut path = std::path::PathBuf::from(&command);
|
||||
|
||||
// Check if we can find the sendmail command to execute when no absolute path is given
|
||||
if !path.is_absolute() {
|
||||
match which::which(&command) {
|
||||
Ok(result) => path = result,
|
||||
Err(_) => err!(format!("sendmail command {command:?} not found in $PATH")),
|
||||
}
|
||||
let Ok(which_path) = which::which(&command) else {
|
||||
err!(format!("sendmail command {command} not found in $PATH"))
|
||||
};
|
||||
path = which_path;
|
||||
}
|
||||
|
||||
match path.metadata() {
|
||||
@@ -919,8 +948,8 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
|
||||
}
|
||||
}
|
||||
|
||||
if (cfg.smtp_host.is_some() || cfg.use_sendmail) && !cfg.smtp_from.contains('@') {
|
||||
err!("SMTP_FROM does not contain a mandatory @ sign")
|
||||
if (cfg.smtp_host.is_some() || cfg.use_sendmail) && !is_valid_email(&cfg.smtp_from) {
|
||||
err!(format!("SMTP_FROM '{}' is not a valid email address", cfg.smtp_from))
|
||||
}
|
||||
|
||||
if cfg._enable_email_2fa && cfg.email_token_size < 6 {
|
||||
@@ -1133,12 +1162,17 @@ impl Config {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update_config(&self, other: ConfigBuilder) -> Result<(), Error> {
|
||||
pub fn update_config(&self, other: ConfigBuilder, ignore_non_editable: bool) -> Result<(), Error> {
|
||||
// Remove default values
|
||||
//let builder = other.remove(&self.inner.read().unwrap()._env);
|
||||
|
||||
// TODO: Remove values that are defaults, above only checks those set by env and not the defaults
|
||||
let builder = other;
|
||||
let mut builder = other;
|
||||
|
||||
// Remove values that are not editable
|
||||
if ignore_non_editable {
|
||||
builder.clear_non_editable();
|
||||
}
|
||||
|
||||
// Serialize now before we consume the builder
|
||||
let config_str = serde_json::to_string_pretty(&builder)?;
|
||||
@@ -1173,7 +1207,7 @@ impl Config {
|
||||
let mut _overrides = Vec::new();
|
||||
usr.merge(&other, false, &mut _overrides)
|
||||
};
|
||||
self.update_config(builder)
|
||||
self.update_config(builder, false)
|
||||
}
|
||||
|
||||
/// Tests whether an email's domain is allowed. A domain is allowed if it
|
||||
@@ -1182,7 +1216,7 @@ impl Config {
|
||||
pub fn is_email_domain_allowed(&self, email: &str) -> bool {
|
||||
let e: Vec<&str> = email.rsplitn(2, '@').collect();
|
||||
if e.len() != 2 || e[0].is_empty() || e[1].is_empty() {
|
||||
warn!("Failed to parse email address '{}'", email);
|
||||
warn!("Failed to parse email address '{email}'");
|
||||
return false;
|
||||
}
|
||||
let email_domain = e[0].to_lowercase();
|
||||
@@ -1314,6 +1348,8 @@ where
|
||||
// Register helpers
|
||||
hb.register_helper("case", Box::new(case_helper));
|
||||
hb.register_helper("to_json", Box::new(to_json));
|
||||
hb.register_helper("webver", Box::new(webver));
|
||||
hb.register_helper("vwver", Box::new(vwver));
|
||||
|
||||
macro_rules! reg {
|
||||
($name:expr) => {{
|
||||
@@ -1353,6 +1389,7 @@ where
|
||||
reg!("email/protected_action", ".html");
|
||||
reg!("email/pw_hint_none", ".html");
|
||||
reg!("email/pw_hint_some", ".html");
|
||||
reg!("email/register_verify_email", ".html");
|
||||
reg!("email/send_2fa_removed_from_org", ".html");
|
||||
reg!("email/send_emergency_access_invite", ".html");
|
||||
reg!("email/send_org_invite", ".html");
|
||||
@@ -1417,3 +1454,42 @@ fn to_json<'reg, 'rc>(
|
||||
out.write(&json)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// 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(|| {
|
||||
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();
|
||||
re.captures(&vault_version)
|
||||
.and_then(|c| {
|
||||
(c.len() == 4).then(|| {
|
||||
format!("{}.{}.{}", c.get(1).unwrap().as_str(), c.get(2).unwrap().as_str(), c.get(3).unwrap().as_str())
|
||||
})
|
||||
})
|
||||
.and_then(|v| semver::Version::parse(&v).ok())
|
||||
.unwrap_or_else(|| semver::Version::parse("2024.6.2").unwrap())
|
||||
});
|
||||
|
||||
// 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(|| {
|
||||
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();
|
||||
re.captures(vw_version)
|
||||
.and_then(|c| {
|
||||
(c.len() == 4).then(|| {
|
||||
format!("{}.{}.{}", c.get(1).unwrap().as_str(), c.get(2).unwrap().as_str(), c.get(3).unwrap().as_str())
|
||||
})
|
||||
})
|
||||
.and_then(|v| semver::Version::parse(&v).ok())
|
||||
.unwrap_or_else(|| semver::Version::parse("1.32.5").unwrap())
|
||||
});
|
||||
|
||||
handlebars::handlebars_helper!(webver: | web_vault_version: String |
|
||||
semver::VersionReq::parse(&web_vault_version).expect("Invalid web-vault version compare string").matches(&WEB_VAULT_VERSION)
|
||||
);
|
||||
handlebars::handlebars_helper!(vwver: | vw_version: String |
|
||||
semver::VersionReq::parse(&vw_version).expect("Invalid Vaultwarden version compare string").matches(&VW_VERSION)
|
||||
);
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::num::NonZeroU32;
|
||||
use data_encoding::{Encoding, HEXLOWER};
|
||||
use ring::{digest, hmac, pbkdf2};
|
||||
|
||||
static DIGEST_ALG: pbkdf2::Algorithm = pbkdf2::PBKDF2_HMAC_SHA256;
|
||||
const DIGEST_ALG: pbkdf2::Algorithm = pbkdf2::PBKDF2_HMAC_SHA256;
|
||||
const OUTPUT_LEN: usize = digest::SHA256_OUTPUT_LEN;
|
||||
|
||||
pub fn hash_password(secret: &[u8], salt: &[u8], iterations: u32) -> Vec<u8> {
|
||||
@@ -56,11 +56,11 @@ pub fn encode_random_bytes<const N: usize>(e: Encoding) -> String {
|
||||
pub fn get_random_string(alphabet: &[u8], num_chars: usize) -> String {
|
||||
// Ref: https://rust-lang-nursery.github.io/rust-cookbook/algorithms/randomness.html
|
||||
use rand::Rng;
|
||||
let mut rng = rand::thread_rng();
|
||||
let mut rng = rand::rng();
|
||||
|
||||
(0..num_chars)
|
||||
.map(|_| {
|
||||
let i = rng.gen_range(0..alphabet.len());
|
||||
let i = rng.random_range(0..alphabet.len());
|
||||
alphabet[i] as char
|
||||
})
|
||||
.collect()
|
||||
@@ -84,14 +84,15 @@ pub fn generate_id<const N: usize>() -> String {
|
||||
encode_random_bytes::<N>(HEXLOWER)
|
||||
}
|
||||
|
||||
pub fn generate_send_id() -> String {
|
||||
// Send IDs are globally scoped, so make them longer to avoid collisions.
|
||||
pub fn generate_send_file_id() -> String {
|
||||
// Send File IDs are globally scoped, so make them longer to avoid collisions.
|
||||
generate_id::<32>() // 256 bits
|
||||
}
|
||||
|
||||
pub fn generate_attachment_id() -> String {
|
||||
use crate::db::models::AttachmentId;
|
||||
pub fn generate_attachment_id() -> AttachmentId {
|
||||
// Attachment IDs are scoped to a cipher, so they can be smaller.
|
||||
generate_id::<10>() // 80 bits
|
||||
AttachmentId(generate_id::<10>()) // 80 bits
|
||||
}
|
||||
|
||||
/// Generates a numeric token for email-based verifications.
|
||||
@@ -109,7 +110,6 @@ pub fn generate_api_key() -> String {
|
||||
// Constant time compare
|
||||
//
|
||||
pub fn ct_eq<T: AsRef<[u8]>, U: AsRef<[u8]>>(a: T, b: U) -> bool {
|
||||
use ring::constant_time::verify_slices_are_equal;
|
||||
|
||||
verify_slices_are_equal(a.as_ref(), b.as_ref()).is_ok()
|
||||
use subtle::ConstantTimeEq;
|
||||
a.as_ref().ct_eq(b.as_ref()).into()
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ macro_rules! generate_connections {
|
||||
DbConnType::$name => {
|
||||
#[cfg($name)]
|
||||
{
|
||||
paste::paste!{ [< $name _migrations >]::run_migrations()?; }
|
||||
pastey::paste!{ [< $name _migrations >]::run_migrations()?; }
|
||||
let manager = ConnectionManager::new(&url);
|
||||
let pool = Pool::builder()
|
||||
.max_size(CONFIG.database_max_conns())
|
||||
@@ -259,7 +259,7 @@ macro_rules! db_run {
|
||||
$($(
|
||||
#[cfg($db)]
|
||||
$crate::db::DbConnInner::$db($conn) => {
|
||||
paste::paste! {
|
||||
pastey::paste! {
|
||||
#[allow(unused)] use $crate::db::[<__ $db _schema>]::{self as schema, *};
|
||||
#[allow(unused)] use [<__ $db _model>]::*;
|
||||
}
|
||||
@@ -280,7 +280,7 @@ macro_rules! db_run {
|
||||
$($(
|
||||
#[cfg($db)]
|
||||
$crate::db::DbConnInner::$db($conn) => {
|
||||
paste::paste! {
|
||||
pastey::paste! {
|
||||
#[allow(unused)] use $crate::db::[<__ $db _schema>]::{self as schema, *};
|
||||
// @ RAW: #[allow(unused)] use [<__ $db _model>]::*;
|
||||
}
|
||||
@@ -337,7 +337,7 @@ macro_rules! db_object {
|
||||
};
|
||||
|
||||
( @db $db:ident | $( #[$attr:meta] )* | $name:ident | $( $( #[$field_attr:meta] )* $vis:vis $field:ident : $typ:ty),+) => {
|
||||
paste::paste! {
|
||||
pastey::paste! {
|
||||
#[allow(unused)] use super::*;
|
||||
#[allow(unused)] use diesel::prelude::*;
|
||||
#[allow(unused)] use $crate::db::[<__ $db _schema>]::*;
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
use std::io::ErrorKind;
|
||||
|
||||
use bigdecimal::{BigDecimal, ToPrimitive};
|
||||
use derive_more::{AsRef, Deref, Display};
|
||||
use serde_json::Value;
|
||||
|
||||
use super::{CipherId, OrganizationId, UserId};
|
||||
use crate::CONFIG;
|
||||
use macros::IdFromParam;
|
||||
|
||||
db_object! {
|
||||
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
|
||||
@@ -11,8 +14,8 @@ db_object! {
|
||||
#[diesel(treat_none_as_null = true)]
|
||||
#[diesel(primary_key(id))]
|
||||
pub struct Attachment {
|
||||
pub id: String,
|
||||
pub cipher_uuid: String,
|
||||
pub id: AttachmentId,
|
||||
pub cipher_uuid: CipherId,
|
||||
pub file_name: String, // encrypted
|
||||
pub file_size: i64,
|
||||
pub akey: Option<String>,
|
||||
@@ -21,7 +24,13 @@ db_object! {
|
||||
|
||||
/// Local methods
|
||||
impl Attachment {
|
||||
pub const fn new(id: String, cipher_uuid: String, file_name: String, file_size: i64, akey: Option<String>) -> Self {
|
||||
pub const fn new(
|
||||
id: AttachmentId,
|
||||
cipher_uuid: CipherId,
|
||||
file_name: String,
|
||||
file_size: i64,
|
||||
akey: Option<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
cipher_uuid,
|
||||
@@ -37,7 +46,7 @@ impl Attachment {
|
||||
|
||||
pub fn get_url(&self, host: &str) -> String {
|
||||
let token = encode_jwt(&generate_file_download_claims(self.cipher_uuid.clone(), self.id.clone()));
|
||||
format!("{}/attachments/{}/{}?token={}", host, self.cipher_uuid, self.id, token)
|
||||
format!("{host}/attachments/{}/{}?token={token}", self.cipher_uuid, self.id)
|
||||
}
|
||||
|
||||
pub fn to_json(&self, host: &str) -> Value {
|
||||
@@ -108,7 +117,7 @@ impl Attachment {
|
||||
// upstream caller has already cleaned up the file as part of
|
||||
// its own error handling.
|
||||
Err(e) if e.kind() == ErrorKind::NotFound => {
|
||||
debug!("File '{}' already deleted.", file_path);
|
||||
debug!("File '{file_path}' already deleted.");
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => Err(e.into()),
|
||||
@@ -117,14 +126,14 @@ impl Attachment {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn delete_all_by_cipher(cipher_uuid: &str, conn: &mut DbConn) -> EmptyResult {
|
||||
pub async fn delete_all_by_cipher(cipher_uuid: &CipherId, conn: &mut 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: &str, conn: &mut DbConn) -> Option<Self> {
|
||||
pub async fn find_by_id(id: &AttachmentId, conn: &mut DbConn) -> Option<Self> {
|
||||
db_run! { conn: {
|
||||
attachments::table
|
||||
.filter(attachments::id.eq(id.to_lowercase()))
|
||||
@@ -134,7 +143,7 @@ impl Attachment {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn find_by_cipher(cipher_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
|
||||
pub async fn find_by_cipher(cipher_uuid: &CipherId, conn: &mut DbConn) -> Vec<Self> {
|
||||
db_run! { conn: {
|
||||
attachments::table
|
||||
.filter(attachments::cipher_uuid.eq(cipher_uuid))
|
||||
@@ -144,7 +153,7 @@ impl Attachment {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn size_by_user(user_uuid: &str, conn: &mut DbConn) -> i64 {
|
||||
pub async fn size_by_user(user_uuid: &UserId, conn: &mut DbConn) -> i64 {
|
||||
db_run! { conn: {
|
||||
let result: Option<BigDecimal> = attachments::table
|
||||
.left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid)))
|
||||
@@ -161,7 +170,7 @@ impl Attachment {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn count_by_user(user_uuid: &str, conn: &mut DbConn) -> i64 {
|
||||
pub async fn count_by_user(user_uuid: &UserId, conn: &mut DbConn) -> i64 {
|
||||
db_run! { conn: {
|
||||
attachments::table
|
||||
.left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid)))
|
||||
@@ -172,7 +181,7 @@ impl Attachment {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn size_by_org(org_uuid: &str, conn: &mut DbConn) -> i64 {
|
||||
pub async fn size_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> i64 {
|
||||
db_run! { conn: {
|
||||
let result: Option<BigDecimal> = attachments::table
|
||||
.left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid)))
|
||||
@@ -189,7 +198,7 @@ impl Attachment {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn count_by_org(org_uuid: &str, conn: &mut DbConn) -> i64 {
|
||||
pub async fn count_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> i64 {
|
||||
db_run! { conn: {
|
||||
attachments::table
|
||||
.left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid)))
|
||||
@@ -203,7 +212,11 @@ impl Attachment {
|
||||
// This will return all attachments linked to the user or org
|
||||
// There is no filtering done here if the user actually has access!
|
||||
// It is used to speed up the sync process, and the matching is done in a different part.
|
||||
pub async fn find_all_by_user_and_orgs(user_uuid: &str, org_uuids: &Vec<String>, conn: &mut DbConn) -> Vec<Self> {
|
||||
pub async fn find_all_by_user_and_orgs(
|
||||
user_uuid: &UserId,
|
||||
org_uuids: &Vec<OrganizationId>,
|
||||
conn: &mut DbConn,
|
||||
) -> Vec<Self> {
|
||||
db_run! { conn: {
|
||||
attachments::table
|
||||
.left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid)))
|
||||
@@ -216,3 +229,20 @@ impl Attachment {
|
||||
}}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone,
|
||||
Debug,
|
||||
AsRef,
|
||||
Deref,
|
||||
DieselNewType,
|
||||
Display,
|
||||
FromForm,
|
||||
Hash,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
IdFromParam,
|
||||
)]
|
||||
pub struct AttachmentId(pub String);
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
use crate::crypto::ct_eq;
|
||||
use super::{DeviceId, OrganizationId, UserId};
|
||||
use crate::{crypto::ct_eq, util::format_date};
|
||||
use chrono::{NaiveDateTime, Utc};
|
||||
use derive_more::{AsRef, Deref, Display, From};
|
||||
use macros::UuidFromParam;
|
||||
use serde_json::Value;
|
||||
|
||||
db_object! {
|
||||
#[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset, Deserialize, Serialize)]
|
||||
@@ -7,15 +11,15 @@ db_object! {
|
||||
#[diesel(treat_none_as_null = true)]
|
||||
#[diesel(primary_key(uuid))]
|
||||
pub struct AuthRequest {
|
||||
pub uuid: String,
|
||||
pub user_uuid: String,
|
||||
pub organization_uuid: Option<String>,
|
||||
pub uuid: AuthRequestId,
|
||||
pub user_uuid: UserId,
|
||||
pub organization_uuid: Option<OrganizationId>,
|
||||
|
||||
pub request_device_identifier: String,
|
||||
pub request_device_identifier: DeviceId,
|
||||
pub device_type: i32, // https://github.com/bitwarden/server/blob/master/src/Core/Enums/DeviceType.cs
|
||||
|
||||
pub request_ip: String,
|
||||
pub response_device_id: Option<String>,
|
||||
pub response_device_id: Option<DeviceId>,
|
||||
|
||||
pub access_code: String,
|
||||
pub public_key: String,
|
||||
@@ -33,8 +37,8 @@ db_object! {
|
||||
|
||||
impl AuthRequest {
|
||||
pub fn new(
|
||||
user_uuid: String,
|
||||
request_device_identifier: String,
|
||||
user_uuid: UserId,
|
||||
request_device_identifier: DeviceId,
|
||||
device_type: i32,
|
||||
request_ip: String,
|
||||
access_code: String,
|
||||
@@ -43,7 +47,7 @@ impl AuthRequest {
|
||||
let now = Utc::now().naive_utc();
|
||||
|
||||
Self {
|
||||
uuid: crate::util::get_uuid(),
|
||||
uuid: AuthRequestId(crate::util::get_uuid()),
|
||||
user_uuid,
|
||||
organization_uuid: None,
|
||||
|
||||
@@ -61,6 +65,13 @@ impl AuthRequest {
|
||||
authentication_date: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_json_for_pending_device(&self) -> Value {
|
||||
json!({
|
||||
"id": self.uuid,
|
||||
"creationDate": format_date(&self.creation_date),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
use crate::db::DbConn;
|
||||
@@ -101,7 +112,7 @@ impl AuthRequest {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn find_by_uuid(uuid: &str, conn: &mut DbConn) -> Option<Self> {
|
||||
pub async fn find_by_uuid(uuid: &AuthRequestId, conn: &mut DbConn) -> Option<Self> {
|
||||
db_run! {conn: {
|
||||
auth_requests::table
|
||||
.filter(auth_requests::uuid.eq(uuid))
|
||||
@@ -111,7 +122,18 @@ impl AuthRequest {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn find_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
|
||||
pub async fn find_by_uuid_and_user(uuid: &AuthRequestId, user_uuid: &UserId, conn: &mut 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)
|
||||
.ok()
|
||||
.from_db()
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn find_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
|
||||
db_run! {conn: {
|
||||
auth_requests::table
|
||||
.filter(auth_requests::user_uuid.eq(user_uuid))
|
||||
@@ -119,6 +141,21 @@ impl AuthRequest {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn find_by_user_and_requested_device(
|
||||
user_uuid: &UserId,
|
||||
device_uuid: &DeviceId,
|
||||
conn: &mut DbConn,
|
||||
) -> Option<Self> {
|
||||
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()
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn find_created_before(dt: &NaiveDateTime, conn: &mut DbConn) -> Vec<Self> {
|
||||
db_run! {conn: {
|
||||
auth_requests::table
|
||||
@@ -146,3 +183,21 @@ impl AuthRequest {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone,
|
||||
Debug,
|
||||
AsRef,
|
||||
Deref,
|
||||
DieselNewType,
|
||||
Display,
|
||||
From,
|
||||
FromForm,
|
||||
Hash,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
UuidFromParam,
|
||||
)]
|
||||
pub struct AuthRequestId(String);
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
use crate::util::LowerCase;
|
||||
use crate::CONFIG;
|
||||
use chrono::{NaiveDateTime, TimeDelta, Utc};
|
||||
use derive_more::{AsRef, Deref, Display, From};
|
||||
use serde_json::Value;
|
||||
|
||||
use super::{
|
||||
Attachment, CollectionCipher, Favorite, FolderCipher, Group, User, UserOrgStatus, UserOrgType, UserOrganization,
|
||||
Attachment, CollectionCipher, CollectionId, Favorite, FolderCipher, FolderId, Group, Membership, MembershipStatus,
|
||||
MembershipType, OrganizationId, User, UserId,
|
||||
};
|
||||
|
||||
use crate::api::core::{CipherData, CipherSyncData, CipherSyncType};
|
||||
use macros::UuidFromParam;
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
@@ -17,12 +19,12 @@ db_object! {
|
||||
#[diesel(treat_none_as_null = true)]
|
||||
#[diesel(primary_key(uuid))]
|
||||
pub struct Cipher {
|
||||
pub uuid: String,
|
||||
pub uuid: CipherId,
|
||||
pub created_at: NaiveDateTime,
|
||||
pub updated_at: NaiveDateTime,
|
||||
|
||||
pub user_uuid: Option<String>,
|
||||
pub organization_uuid: Option<String>,
|
||||
pub user_uuid: Option<UserId>,
|
||||
pub organization_uuid: Option<OrganizationId>,
|
||||
|
||||
pub key: Option<String>,
|
||||
|
||||
@@ -57,7 +59,7 @@ impl Cipher {
|
||||
let now = Utc::now().naive_utc();
|
||||
|
||||
Self {
|
||||
uuid: crate::util::get_uuid(),
|
||||
uuid: CipherId(crate::util::get_uuid()),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
|
||||
@@ -83,7 +85,7 @@ impl Cipher {
|
||||
let mut validation_errors = serde_json::Map::new();
|
||||
let max_note_size = CONFIG._max_note_size();
|
||||
let max_note_size_msg =
|
||||
format!("The field Notes exceeds the maximum encrypted value length of {} characters.", &max_note_size);
|
||||
format!("The field Notes exceeds the maximum encrypted value length of {max_note_size} characters.");
|
||||
for (index, cipher) in cipher_data.iter().enumerate() {
|
||||
// Validate the note size and if it is exceeded return a warning
|
||||
if let Some(note) = &cipher.notes {
|
||||
@@ -135,12 +137,12 @@ impl Cipher {
|
||||
pub async fn to_json(
|
||||
&self,
|
||||
host: &str,
|
||||
user_uuid: &str,
|
||||
user_uuid: &UserId,
|
||||
cipher_sync_data: Option<&CipherSyncData>,
|
||||
sync_type: CipherSyncType,
|
||||
conn: &mut DbConn,
|
||||
) -> Value {
|
||||
use crate::util::format_date;
|
||||
use crate::util::{format_date, validate_and_format_date};
|
||||
|
||||
let mut attachments_json: Value = Value::Null;
|
||||
if let Some(cipher_sync_data) = cipher_sync_data {
|
||||
@@ -156,16 +158,16 @@ impl Cipher {
|
||||
|
||||
// We don't need these values at all for Organizational syncs
|
||||
// Skip any other database calls if this is the case and just return false.
|
||||
let (read_only, hide_passwords) = if sync_type == CipherSyncType::User {
|
||||
let (read_only, hide_passwords, _) = if sync_type == CipherSyncType::User {
|
||||
match self.get_access_restrictions(user_uuid, cipher_sync_data, conn).await {
|
||||
Some((ro, hp)) => (ro, hp),
|
||||
Some((ro, hp, mn)) => (ro, hp, mn),
|
||||
None => {
|
||||
error!("Cipher ownership assertion failure");
|
||||
(true, true)
|
||||
(true, true, false)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
(false, false)
|
||||
(false, false, false)
|
||||
};
|
||||
|
||||
let fields_json: Vec<_> = self
|
||||
@@ -218,7 +220,7 @@ impl Cipher {
|
||||
})
|
||||
.map(|mut d| match d.get("lastUsedDate").and_then(|l| l.as_str()) {
|
||||
Some(l) => {
|
||||
d["lastUsedDate"] = json!(crate::util::validate_and_format_date(l));
|
||||
d["lastUsedDate"] = json!(validate_and_format_date(l));
|
||||
d
|
||||
}
|
||||
_ => {
|
||||
@@ -241,12 +243,28 @@ impl Cipher {
|
||||
// NOTE: This was marked as *Backwards Compatibility Code*, but as of January 2021 this is still being used by upstream
|
||||
// Set the first element of the Uris array as Uri, this is needed several (mobile) clients.
|
||||
if self.atype == 1 {
|
||||
if type_data_json["uris"].is_array() {
|
||||
let uri = type_data_json["uris"][0]["uri"].clone();
|
||||
type_data_json["uri"] = uri;
|
||||
} else {
|
||||
// Upstream always has an Uri key/value
|
||||
type_data_json["uri"] = Value::Null;
|
||||
// Upstream always has an `uri` key/value
|
||||
type_data_json["uri"] = Value::Null;
|
||||
if let Some(uris) = type_data_json["uris"].as_array_mut() {
|
||||
if !uris.is_empty() {
|
||||
// Fix uri match values first, they are only allowed to be a number or null
|
||||
// If it is a string, convert it to an int or null if that fails
|
||||
for uri in &mut *uris {
|
||||
if uri["match"].is_string() {
|
||||
let match_value = match uri["match"].as_str().unwrap_or_default().parse::<u8>() {
|
||||
Ok(n) => json!(n),
|
||||
_ => Value::Null,
|
||||
};
|
||||
uri["match"] = match_value;
|
||||
}
|
||||
}
|
||||
type_data_json["uri"] = uris[0]["uri"].clone();
|
||||
}
|
||||
}
|
||||
|
||||
// Check if `passwordRevisionDate` is a valid date, else convert it
|
||||
if let Some(pw_revision) = type_data_json["passwordRevisionDate"].as_str() {
|
||||
type_data_json["passwordRevisionDate"] = json!(validate_and_format_date(pw_revision));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,6 +279,19 @@ impl Cipher {
|
||||
}
|
||||
}
|
||||
|
||||
// Fix invalid SSH Entries
|
||||
// This breaks at least the native mobile client if invalid
|
||||
// The only way to fix this is by setting type_data_json to `null`
|
||||
// Opening this ssh-key in the mobile client will probably crash the client, but you can edit, save and afterwards delete it
|
||||
if self.atype == 5
|
||||
&& (type_data_json["keyFingerprint"].as_str().is_none_or(|v| v.is_empty())
|
||||
|| type_data_json["privateKey"].as_str().is_none_or(|v| v.is_empty())
|
||||
|| type_data_json["publicKey"].as_str().is_none_or(|v| v.is_empty()))
|
||||
{
|
||||
warn!("Error parsing ssh-key, mandatory fields are invalid for {}", self.uuid);
|
||||
type_data_json = Value::Null;
|
||||
}
|
||||
|
||||
// Clone the type_data and add some default value.
|
||||
let mut data_json = type_data_json.clone();
|
||||
|
||||
@@ -278,7 +309,7 @@ impl Cipher {
|
||||
Cow::from(Vec::with_capacity(0))
|
||||
}
|
||||
} else {
|
||||
Cow::from(self.get_admin_collections(user_uuid.to_string(), conn).await)
|
||||
Cow::from(self.get_admin_collections(user_uuid.clone(), conn).await)
|
||||
};
|
||||
|
||||
// There are three types of cipher response models in upstream
|
||||
@@ -327,7 +358,7 @@ impl Cipher {
|
||||
// Skip adding these fields in that case
|
||||
if sync_type == CipherSyncType::User {
|
||||
json_object["folderId"] = json!(if let Some(cipher_sync_data) = cipher_sync_data {
|
||||
cipher_sync_data.cipher_folders.get(&self.uuid).map(|c| c.to_string())
|
||||
cipher_sync_data.cipher_folders.get(&self.uuid).cloned()
|
||||
} else {
|
||||
self.get_folder_uuid(user_uuid, conn).await
|
||||
});
|
||||
@@ -356,7 +387,7 @@ impl Cipher {
|
||||
json_object
|
||||
}
|
||||
|
||||
pub async fn update_users_revision(&self, conn: &mut DbConn) -> Vec<String> {
|
||||
pub async fn update_users_revision(&self, conn: &mut DbConn) -> Vec<UserId> {
|
||||
let mut user_uuids = Vec::new();
|
||||
match self.user_uuid {
|
||||
Some(ref user_uuid) => {
|
||||
@@ -367,17 +398,16 @@ impl Cipher {
|
||||
// Belongs to Organization, need to update affected users
|
||||
if let Some(ref org_uuid) = self.organization_uuid {
|
||||
// users having access to the collection
|
||||
let mut collection_users =
|
||||
UserOrganization::find_by_cipher_and_org(&self.uuid, org_uuid, conn).await;
|
||||
let mut collection_users = Membership::find_by_cipher_and_org(&self.uuid, org_uuid, conn).await;
|
||||
if CONFIG.org_groups_enabled() {
|
||||
// members of a group having access to the collection
|
||||
let group_users =
|
||||
UserOrganization::find_by_cipher_and_org_with_group(&self.uuid, org_uuid, conn).await;
|
||||
Membership::find_by_cipher_and_org_with_group(&self.uuid, org_uuid, conn).await;
|
||||
collection_users.extend(group_users);
|
||||
}
|
||||
for user_org in collection_users {
|
||||
User::update_uuid_revision(&user_org.user_uuid, conn).await;
|
||||
user_uuids.push(user_org.user_uuid.clone())
|
||||
for member in collection_users {
|
||||
User::update_uuid_revision(&member.user_uuid, conn).await;
|
||||
user_uuids.push(member.user_uuid.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -435,7 +465,7 @@ impl Cipher {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn delete_all_by_organization(org_uuid: &str, conn: &mut DbConn) -> EmptyResult {
|
||||
pub async fn delete_all_by_organization(org_uuid: &OrganizationId, conn: &mut 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?;
|
||||
@@ -443,7 +473,7 @@ impl Cipher {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_all_by_user(user_uuid: &str, conn: &mut DbConn) -> EmptyResult {
|
||||
pub async fn delete_all_by_user(user_uuid: &UserId, conn: &mut DbConn) -> EmptyResult {
|
||||
for cipher in Self::find_owned_by_user(user_uuid, conn).await {
|
||||
cipher.delete(conn).await?;
|
||||
}
|
||||
@@ -461,52 +491,59 @@ impl Cipher {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn move_to_folder(&self, folder_uuid: Option<String>, user_uuid: &str, conn: &mut DbConn) -> EmptyResult {
|
||||
pub async fn move_to_folder(
|
||||
&self,
|
||||
folder_uuid: Option<FolderId>,
|
||||
user_uuid: &UserId,
|
||||
conn: &mut DbConn,
|
||||
) -> EmptyResult {
|
||||
User::update_uuid_revision(user_uuid, conn).await;
|
||||
|
||||
match (self.get_folder_uuid(user_uuid, conn).await, folder_uuid) {
|
||||
// No changes
|
||||
(None, None) => Ok(()),
|
||||
(Some(ref old), Some(ref new)) if old == new => Ok(()),
|
||||
(Some(ref old_folder), Some(ref new_folder)) if old_folder == new_folder => Ok(()),
|
||||
|
||||
// Add to folder
|
||||
(None, Some(new)) => FolderCipher::new(&new, &self.uuid).save(conn).await,
|
||||
(None, Some(new_folder)) => FolderCipher::new(new_folder, self.uuid.clone()).save(conn).await,
|
||||
|
||||
// Remove from folder
|
||||
(Some(old), None) => match FolderCipher::find_by_folder_and_cipher(&old, &self.uuid, conn).await {
|
||||
Some(old) => old.delete(conn).await,
|
||||
None => err!("Couldn't move from previous folder"),
|
||||
},
|
||||
(Some(old_folder), None) => {
|
||||
match FolderCipher::find_by_folder_and_cipher(&old_folder, &self.uuid, conn).await {
|
||||
Some(old_folder) => old_folder.delete(conn).await,
|
||||
None => err!("Couldn't move from previous folder"),
|
||||
}
|
||||
}
|
||||
|
||||
// Move to another folder
|
||||
(Some(old), Some(new)) => {
|
||||
if let Some(old) = FolderCipher::find_by_folder_and_cipher(&old, &self.uuid, conn).await {
|
||||
old.delete(conn).await?;
|
||||
(Some(old_folder), Some(new_folder)) => {
|
||||
if let Some(old_folder) = FolderCipher::find_by_folder_and_cipher(&old_folder, &self.uuid, conn).await {
|
||||
old_folder.delete(conn).await?;
|
||||
}
|
||||
FolderCipher::new(&new, &self.uuid).save(conn).await
|
||||
FolderCipher::new(new_folder, self.uuid.clone()).save(conn).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether this cipher is directly owned by the user.
|
||||
pub fn is_owned_by_user(&self, user_uuid: &str) -> bool {
|
||||
pub fn is_owned_by_user(&self, user_uuid: &UserId) -> bool {
|
||||
self.user_uuid.is_some() && self.user_uuid.as_ref().unwrap() == user_uuid
|
||||
}
|
||||
|
||||
/// Returns whether this cipher is owned by an org in which the user has full access.
|
||||
async fn is_in_full_access_org(
|
||||
&self,
|
||||
user_uuid: &str,
|
||||
user_uuid: &UserId,
|
||||
cipher_sync_data: Option<&CipherSyncData>,
|
||||
conn: &mut DbConn,
|
||||
) -> bool {
|
||||
if let Some(ref org_uuid) = self.organization_uuid {
|
||||
if let Some(cipher_sync_data) = cipher_sync_data {
|
||||
if let Some(cached_user_org) = cipher_sync_data.user_organizations.get(org_uuid) {
|
||||
return cached_user_org.has_full_access();
|
||||
if let Some(cached_member) = cipher_sync_data.members.get(org_uuid) {
|
||||
return cached_member.has_full_access();
|
||||
}
|
||||
} else if let Some(user_org) = UserOrganization::find_by_user_and_org(user_uuid, org_uuid, conn).await {
|
||||
return user_org.has_full_access();
|
||||
} else if let Some(member) = Membership::find_by_user_and_org(user_uuid, org_uuid, conn).await {
|
||||
return member.has_full_access();
|
||||
}
|
||||
}
|
||||
false
|
||||
@@ -515,7 +552,7 @@ impl Cipher {
|
||||
/// Returns whether this cipher is owned by an group in which the user has full access.
|
||||
async fn is_in_full_access_group(
|
||||
&self,
|
||||
user_uuid: &str,
|
||||
user_uuid: &UserId,
|
||||
cipher_sync_data: Option<&CipherSyncData>,
|
||||
conn: &mut DbConn,
|
||||
) -> bool {
|
||||
@@ -535,14 +572,14 @@ impl Cipher {
|
||||
/// Returns the user's access restrictions to this cipher. A return value
|
||||
/// of None means that this cipher does not belong to the user, and is
|
||||
/// not in any collection the user has access to. Otherwise, the user has
|
||||
/// access to this cipher, and Some(read_only, hide_passwords) represents
|
||||
/// access to this cipher, and Some(read_only, hide_passwords, manage) represents
|
||||
/// the access restrictions.
|
||||
pub async fn get_access_restrictions(
|
||||
&self,
|
||||
user_uuid: &str,
|
||||
user_uuid: &UserId,
|
||||
cipher_sync_data: Option<&CipherSyncData>,
|
||||
conn: &mut DbConn,
|
||||
) -> Option<(bool, bool)> {
|
||||
) -> 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
|
||||
// access restrictions.
|
||||
@@ -550,21 +587,21 @@ impl Cipher {
|
||||
|| self.is_in_full_access_org(user_uuid, cipher_sync_data, conn).await
|
||||
|| self.is_in_full_access_group(user_uuid, cipher_sync_data, conn).await
|
||||
{
|
||||
return Some((false, false));
|
||||
return Some((false, false, true));
|
||||
}
|
||||
|
||||
let rows = if let Some(cipher_sync_data) = cipher_sync_data {
|
||||
let mut rows: Vec<(bool, bool)> = Vec::new();
|
||||
let mut rows: Vec<(bool, bool, bool)> = Vec::new();
|
||||
if let Some(collections) = cipher_sync_data.cipher_collections.get(&self.uuid) {
|
||||
for collection in collections {
|
||||
//User permissions
|
||||
if let Some(uc) = cipher_sync_data.user_collections.get(collection) {
|
||||
rows.push((uc.read_only, uc.hide_passwords));
|
||||
if let Some(cu) = cipher_sync_data.user_collections.get(collection) {
|
||||
rows.push((cu.read_only, cu.hide_passwords, cu.manage));
|
||||
}
|
||||
|
||||
//Group permissions
|
||||
if let Some(cg) = cipher_sync_data.user_collections_groups.get(collection) {
|
||||
rows.push((cg.read_only, cg.hide_passwords));
|
||||
rows.push((cg.read_only, cg.hide_passwords, cg.manage));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -591,15 +628,21 @@ impl Cipher {
|
||||
// booleans and this behavior isn't portable anyway.
|
||||
let mut read_only = true;
|
||||
let mut hide_passwords = true;
|
||||
for (ro, hp) in rows.iter() {
|
||||
let mut manage = false;
|
||||
for (ro, hp, mn) in rows.iter() {
|
||||
read_only &= ro;
|
||||
hide_passwords &= hp;
|
||||
manage &= mn;
|
||||
}
|
||||
|
||||
Some((read_only, hide_passwords))
|
||||
Some((read_only, hide_passwords, manage))
|
||||
}
|
||||
|
||||
async fn get_user_collections_access_flags(&self, user_uuid: &str, conn: &mut DbConn) -> Vec<(bool, bool)> {
|
||||
async fn get_user_collections_access_flags(
|
||||
&self,
|
||||
user_uuid: &UserId,
|
||||
conn: &mut 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.
|
||||
@@ -610,13 +653,17 @@ impl Cipher {
|
||||
.inner_join(users_collections::table.on(
|
||||
ciphers_collections::collection_uuid.eq(users_collections::collection_uuid)
|
||||
.and(users_collections::user_uuid.eq(user_uuid))))
|
||||
.select((users_collections::read_only, users_collections::hide_passwords))
|
||||
.load::<(bool, bool)>(conn)
|
||||
.select((users_collections::read_only, users_collections::hide_passwords, users_collections::manage))
|
||||
.load::<(bool, bool, bool)>(conn)
|
||||
.expect("Error getting user access restrictions")
|
||||
}}
|
||||
}
|
||||
|
||||
async fn get_group_collections_access_flags(&self, user_uuid: &str, conn: &mut DbConn) -> Vec<(bool, bool)> {
|
||||
async fn get_group_collections_access_flags(
|
||||
&self,
|
||||
user_uuid: &UserId,
|
||||
conn: &mut DbConn,
|
||||
) -> Vec<(bool, bool, bool)> {
|
||||
if !CONFIG.org_groups_enabled() {
|
||||
return Vec::new();
|
||||
}
|
||||
@@ -636,49 +683,49 @@ impl Cipher {
|
||||
users_organizations::uuid.eq(groups_users::users_organizations_uuid)
|
||||
))
|
||||
.filter(users_organizations::user_uuid.eq(user_uuid))
|
||||
.select((collections_groups::read_only, collections_groups::hide_passwords))
|
||||
.load::<(bool, bool)>(conn)
|
||||
.select((collections_groups::read_only, collections_groups::hide_passwords, collections_groups::manage))
|
||||
.load::<(bool, bool, bool)>(conn)
|
||||
.expect("Error getting group access restrictions")
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn is_write_accessible_to_user(&self, user_uuid: &str, conn: &mut DbConn) -> bool {
|
||||
pub async fn is_write_accessible_to_user(&self, user_uuid: &UserId, conn: &mut DbConn) -> bool {
|
||||
match self.get_access_restrictions(user_uuid, None, conn).await {
|
||||
Some((read_only, _hide_passwords)) => !read_only,
|
||||
Some((read_only, _hide_passwords, manage)) => !read_only || manage,
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn is_accessible_to_user(&self, user_uuid: &str, conn: &mut DbConn) -> bool {
|
||||
pub async fn is_accessible_to_user(&self, user_uuid: &UserId, conn: &mut 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: &str, conn: &mut DbConn) -> bool {
|
||||
pub async fn is_favorite(&self, user_uuid: &UserId, conn: &mut 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: &str, conn: &mut DbConn) -> EmptyResult {
|
||||
pub async fn set_favorite(&self, favorite: Option<bool>, user_uuid: &UserId, conn: &mut 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: &str, conn: &mut DbConn) -> Option<String> {
|
||||
pub async fn get_folder_uuid(&self, user_uuid: &UserId, conn: &mut DbConn) -> Option<FolderId> {
|
||||
db_run! {conn: {
|
||||
folders_ciphers::table
|
||||
.inner_join(folders::table)
|
||||
.filter(folders::user_uuid.eq(&user_uuid))
|
||||
.filter(folders_ciphers::cipher_uuid.eq(&self.uuid))
|
||||
.select(folders_ciphers::folder_uuid)
|
||||
.first::<String>(conn)
|
||||
.first::<FolderId>(conn)
|
||||
.ok()
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn find_by_uuid(uuid: &str, conn: &mut DbConn) -> Option<Self> {
|
||||
pub async fn find_by_uuid(uuid: &CipherId, conn: &mut DbConn) -> Option<Self> {
|
||||
db_run! {conn: {
|
||||
ciphers::table
|
||||
.filter(ciphers::uuid.eq(uuid))
|
||||
@@ -688,7 +735,11 @@ impl Cipher {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn find_by_uuid_and_org(cipher_uuid: &str, org_uuid: &str, conn: &mut DbConn) -> Option<Self> {
|
||||
pub async fn find_by_uuid_and_org(
|
||||
cipher_uuid: &CipherId,
|
||||
org_uuid: &OrganizationId,
|
||||
conn: &mut DbConn,
|
||||
) -> Option<Self> {
|
||||
db_run! {conn: {
|
||||
ciphers::table
|
||||
.filter(ciphers::uuid.eq(cipher_uuid))
|
||||
@@ -711,7 +762,7 @@ impl Cipher {
|
||||
// true, then the non-interesting ciphers will not be returned. As a
|
||||
// result, those ciphers will not appear in "My Vault" for the org
|
||||
// owner/admin, but they can still be accessed via the org vault view.
|
||||
pub async fn find_by_user(user_uuid: &str, visible_only: bool, conn: &mut DbConn) -> Vec<Self> {
|
||||
pub async fn find_by_user(user_uuid: &UserId, visible_only: bool, conn: &mut DbConn) -> Vec<Self> {
|
||||
if CONFIG.org_groups_enabled() {
|
||||
db_run! {conn: {
|
||||
let mut query = ciphers::table
|
||||
@@ -721,7 +772,7 @@ impl Cipher {
|
||||
.left_join(users_organizations::table.on(
|
||||
ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable())
|
||||
.and(users_organizations::user_uuid.eq(user_uuid))
|
||||
.and(users_organizations::status.eq(UserOrgStatus::Confirmed as i32))
|
||||
.and(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
|
||||
))
|
||||
.left_join(users_collections::table.on(
|
||||
ciphers_collections::collection_uuid.eq(users_collections::collection_uuid)
|
||||
@@ -748,7 +799,7 @@ impl Cipher {
|
||||
|
||||
if !visible_only {
|
||||
query = query.or_filter(
|
||||
users_organizations::atype.le(UserOrgType::Admin as i32) // Org admin/owner
|
||||
users_organizations::atype.le(MembershipType::Admin as i32) // Org admin/owner
|
||||
);
|
||||
}
|
||||
|
||||
@@ -766,7 +817,7 @@ impl Cipher {
|
||||
.left_join(users_organizations::table.on(
|
||||
ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable())
|
||||
.and(users_organizations::user_uuid.eq(user_uuid))
|
||||
.and(users_organizations::status.eq(UserOrgStatus::Confirmed as i32))
|
||||
.and(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
|
||||
))
|
||||
.left_join(users_collections::table.on(
|
||||
ciphers_collections::collection_uuid.eq(users_collections::collection_uuid)
|
||||
@@ -780,7 +831,7 @@ impl Cipher {
|
||||
|
||||
if !visible_only {
|
||||
query = query.or_filter(
|
||||
users_organizations::atype.le(UserOrgType::Admin as i32) // Org admin/owner
|
||||
users_organizations::atype.le(MembershipType::Admin as i32) // Org admin/owner
|
||||
);
|
||||
}
|
||||
|
||||
@@ -793,12 +844,12 @@ impl Cipher {
|
||||
}
|
||||
|
||||
// Find all ciphers visible to the specified user.
|
||||
pub async fn find_by_user_visible(user_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
|
||||
pub async fn find_by_user_visible(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
|
||||
Self::find_by_user(user_uuid, true, conn).await
|
||||
}
|
||||
|
||||
// Find all ciphers directly owned by the specified user.
|
||||
pub async fn find_owned_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
|
||||
pub async fn find_owned_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
|
||||
db_run! {conn: {
|
||||
ciphers::table
|
||||
.filter(
|
||||
@@ -809,7 +860,7 @@ impl Cipher {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn count_owned_by_user(user_uuid: &str, conn: &mut DbConn) -> i64 {
|
||||
pub async fn count_owned_by_user(user_uuid: &UserId, conn: &mut DbConn) -> i64 {
|
||||
db_run! {conn: {
|
||||
ciphers::table
|
||||
.filter(ciphers::user_uuid.eq(user_uuid))
|
||||
@@ -820,7 +871,7 @@ impl Cipher {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn find_by_org(org_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
|
||||
pub async fn find_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> Vec<Self> {
|
||||
db_run! {conn: {
|
||||
ciphers::table
|
||||
.filter(ciphers::organization_uuid.eq(org_uuid))
|
||||
@@ -828,7 +879,7 @@ impl Cipher {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn count_by_org(org_uuid: &str, conn: &mut DbConn) -> i64 {
|
||||
pub async fn count_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> i64 {
|
||||
db_run! {conn: {
|
||||
ciphers::table
|
||||
.filter(ciphers::organization_uuid.eq(org_uuid))
|
||||
@@ -839,7 +890,7 @@ impl Cipher {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn find_by_folder(folder_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
|
||||
pub async fn find_by_folder(folder_uuid: &FolderId, conn: &mut DbConn) -> Vec<Self> {
|
||||
db_run! {conn: {
|
||||
folders_ciphers::table.inner_join(ciphers::table)
|
||||
.filter(folders_ciphers::folder_uuid.eq(folder_uuid))
|
||||
@@ -857,7 +908,7 @@ impl Cipher {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn get_collections(&self, user_id: String, conn: &mut DbConn) -> Vec<String> {
|
||||
pub async fn get_collections(&self, user_uuid: UserId, conn: &mut DbConn) -> Vec<CollectionId> {
|
||||
if CONFIG.org_groups_enabled() {
|
||||
db_run! {conn: {
|
||||
ciphers_collections::table
|
||||
@@ -867,11 +918,11 @@ impl Cipher {
|
||||
))
|
||||
.left_join(users_organizations::table.on(
|
||||
users_organizations::org_uuid.eq(collections::org_uuid)
|
||||
.and(users_organizations::user_uuid.eq(user_id.clone()))
|
||||
.and(users_organizations::user_uuid.eq(user_uuid.clone()))
|
||||
))
|
||||
.left_join(users_collections::table.on(
|
||||
users_collections::collection_uuid.eq(ciphers_collections::collection_uuid)
|
||||
.and(users_collections::user_uuid.eq(user_id.clone()))
|
||||
.and(users_collections::user_uuid.eq(user_uuid.clone()))
|
||||
))
|
||||
.left_join(groups_users::table.on(
|
||||
groups_users::users_organizations_uuid.eq(users_organizations::uuid)
|
||||
@@ -882,14 +933,14 @@ impl Cipher {
|
||||
.and(collections_groups::groups_uuid.eq(groups::uuid))
|
||||
))
|
||||
.filter(users_organizations::access_all.eq(true) // User has access all
|
||||
.or(users_collections::user_uuid.eq(user_id) // User has access to collection
|
||||
.or(users_collections::user_uuid.eq(user_uuid) // User has access to collection
|
||||
.and(users_collections::read_only.eq(false)))
|
||||
.or(groups::access_all.eq(true)) // Access via groups
|
||||
.or(collections_groups::collections_uuid.is_not_null() // Access via groups
|
||||
.and(collections_groups::read_only.eq(false)))
|
||||
)
|
||||
.select(ciphers_collections::collection_uuid)
|
||||
.load::<String>(conn).unwrap_or_default()
|
||||
.load::<CollectionId>(conn).unwrap_or_default()
|
||||
}}
|
||||
} else {
|
||||
db_run! {conn: {
|
||||
@@ -900,23 +951,23 @@ impl Cipher {
|
||||
))
|
||||
.inner_join(users_organizations::table.on(
|
||||
users_organizations::org_uuid.eq(collections::org_uuid)
|
||||
.and(users_organizations::user_uuid.eq(user_id.clone()))
|
||||
.and(users_organizations::user_uuid.eq(user_uuid.clone()))
|
||||
))
|
||||
.left_join(users_collections::table.on(
|
||||
users_collections::collection_uuid.eq(ciphers_collections::collection_uuid)
|
||||
.and(users_collections::user_uuid.eq(user_id.clone()))
|
||||
.and(users_collections::user_uuid.eq(user_uuid.clone()))
|
||||
))
|
||||
.filter(users_organizations::access_all.eq(true) // User has access all
|
||||
.or(users_collections::user_uuid.eq(user_id) // User has access to collection
|
||||
.or(users_collections::user_uuid.eq(user_uuid) // User has access to collection
|
||||
.and(users_collections::read_only.eq(false)))
|
||||
)
|
||||
.select(ciphers_collections::collection_uuid)
|
||||
.load::<String>(conn).unwrap_or_default()
|
||||
.load::<CollectionId>(conn).unwrap_or_default()
|
||||
}}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_admin_collections(&self, user_id: String, conn: &mut DbConn) -> Vec<String> {
|
||||
pub async fn get_admin_collections(&self, user_uuid: UserId, conn: &mut DbConn) -> Vec<CollectionId> {
|
||||
if CONFIG.org_groups_enabled() {
|
||||
db_run! {conn: {
|
||||
ciphers_collections::table
|
||||
@@ -926,11 +977,11 @@ impl Cipher {
|
||||
))
|
||||
.left_join(users_organizations::table.on(
|
||||
users_organizations::org_uuid.eq(collections::org_uuid)
|
||||
.and(users_organizations::user_uuid.eq(user_id.clone()))
|
||||
.and(users_organizations::user_uuid.eq(user_uuid.clone()))
|
||||
))
|
||||
.left_join(users_collections::table.on(
|
||||
users_collections::collection_uuid.eq(ciphers_collections::collection_uuid)
|
||||
.and(users_collections::user_uuid.eq(user_id.clone()))
|
||||
.and(users_collections::user_uuid.eq(user_uuid.clone()))
|
||||
))
|
||||
.left_join(groups_users::table.on(
|
||||
groups_users::users_organizations_uuid.eq(users_organizations::uuid)
|
||||
@@ -941,15 +992,15 @@ impl Cipher {
|
||||
.and(collections_groups::groups_uuid.eq(groups::uuid))
|
||||
))
|
||||
.filter(users_organizations::access_all.eq(true) // User has access all
|
||||
.or(users_collections::user_uuid.eq(user_id) // User has access to collection
|
||||
.or(users_collections::user_uuid.eq(user_uuid) // User has access to collection
|
||||
.and(users_collections::read_only.eq(false)))
|
||||
.or(groups::access_all.eq(true)) // Access via groups
|
||||
.or(collections_groups::collections_uuid.is_not_null() // Access via groups
|
||||
.and(collections_groups::read_only.eq(false)))
|
||||
.or(users_organizations::atype.le(UserOrgType::Admin as i32)) // User is admin or owner
|
||||
.or(users_organizations::atype.le(MembershipType::Admin as i32)) // User is admin or owner
|
||||
)
|
||||
.select(ciphers_collections::collection_uuid)
|
||||
.load::<String>(conn).unwrap_or_default()
|
||||
.load::<CollectionId>(conn).unwrap_or_default()
|
||||
}}
|
||||
} else {
|
||||
db_run! {conn: {
|
||||
@@ -960,26 +1011,29 @@ impl Cipher {
|
||||
))
|
||||
.inner_join(users_organizations::table.on(
|
||||
users_organizations::org_uuid.eq(collections::org_uuid)
|
||||
.and(users_organizations::user_uuid.eq(user_id.clone()))
|
||||
.and(users_organizations::user_uuid.eq(user_uuid.clone()))
|
||||
))
|
||||
.left_join(users_collections::table.on(
|
||||
users_collections::collection_uuid.eq(ciphers_collections::collection_uuid)
|
||||
.and(users_collections::user_uuid.eq(user_id.clone()))
|
||||
.and(users_collections::user_uuid.eq(user_uuid.clone()))
|
||||
))
|
||||
.filter(users_organizations::access_all.eq(true) // User has access all
|
||||
.or(users_collections::user_uuid.eq(user_id) // User has access to collection
|
||||
.or(users_collections::user_uuid.eq(user_uuid) // User has access to collection
|
||||
.and(users_collections::read_only.eq(false)))
|
||||
.or(users_organizations::atype.le(UserOrgType::Admin as i32)) // User is admin or owner
|
||||
.or(users_organizations::atype.le(MembershipType::Admin as i32)) // User is admin or owner
|
||||
)
|
||||
.select(ciphers_collections::collection_uuid)
|
||||
.load::<String>(conn).unwrap_or_default()
|
||||
.load::<CollectionId>(conn).unwrap_or_default()
|
||||
}}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a Vec with (cipher_uuid, collection_uuid)
|
||||
/// 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_id: String, conn: &mut DbConn) -> Vec<(String, String)> {
|
||||
pub async fn get_collections_with_cipher_by_user(
|
||||
user_uuid: UserId,
|
||||
conn: &mut DbConn,
|
||||
) -> Vec<(CipherId, CollectionId)> {
|
||||
db_run! {conn: {
|
||||
ciphers_collections::table
|
||||
.inner_join(collections::table.on(
|
||||
@@ -987,12 +1041,12 @@ impl Cipher {
|
||||
))
|
||||
.inner_join(users_organizations::table.on(
|
||||
users_organizations::org_uuid.eq(collections::org_uuid).and(
|
||||
users_organizations::user_uuid.eq(user_id.clone())
|
||||
users_organizations::user_uuid.eq(user_uuid.clone())
|
||||
)
|
||||
))
|
||||
.left_join(users_collections::table.on(
|
||||
users_collections::collection_uuid.eq(ciphers_collections::collection_uuid).and(
|
||||
users_collections::user_uuid.eq(user_id.clone())
|
||||
users_collections::user_uuid.eq(user_uuid.clone())
|
||||
)
|
||||
))
|
||||
.left_join(groups_users::table.on(
|
||||
@@ -1006,14 +1060,32 @@ impl Cipher {
|
||||
collections_groups::groups_uuid.eq(groups::uuid)
|
||||
)
|
||||
))
|
||||
.or_filter(users_collections::user_uuid.eq(user_id)) // User has access to collection
|
||||
.or_filter(users_collections::user_uuid.eq(user_uuid)) // User has access to collection
|
||||
.or_filter(users_organizations::access_all.eq(true)) // User has access all
|
||||
.or_filter(users_organizations::atype.le(UserOrgType::Admin as i32)) // User is admin or owner
|
||||
.or_filter(users_organizations::atype.le(MembershipType::Admin as i32)) // User is admin or owner
|
||||
.or_filter(groups::access_all.eq(true)) //Access via group
|
||||
.or_filter(collections_groups::collections_uuid.is_not_null()) //Access via group
|
||||
.select(ciphers_collections::all_columns)
|
||||
.distinct()
|
||||
.load::<(String, String)>(conn).unwrap_or_default()
|
||||
.load::<(CipherId, CollectionId)>(conn).unwrap_or_default()
|
||||
}}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone,
|
||||
Debug,
|
||||
AsRef,
|
||||
Deref,
|
||||
DieselNewType,
|
||||
Display,
|
||||
From,
|
||||
FromForm,
|
||||
Hash,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
UuidFromParam,
|
||||
)]
|
||||
pub struct CipherId(String);
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
use derive_more::{AsRef, Deref, Display, From};
|
||||
use serde_json::Value;
|
||||
|
||||
use super::{CollectionGroup, GroupUser, User, UserOrgStatus, UserOrgType, UserOrganization};
|
||||
use super::{
|
||||
CipherId, CollectionGroup, GroupUser, Membership, MembershipId, MembershipStatus, MembershipType, OrganizationId,
|
||||
User, UserId,
|
||||
};
|
||||
use crate::CONFIG;
|
||||
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: String,
|
||||
pub org_uuid: String,
|
||||
pub uuid: CollectionId,
|
||||
pub org_uuid: OrganizationId,
|
||||
pub name: String,
|
||||
pub external_id: Option<String>,
|
||||
}
|
||||
@@ -18,26 +24,27 @@ db_object! {
|
||||
#[diesel(table_name = users_collections)]
|
||||
#[diesel(primary_key(user_uuid, collection_uuid))]
|
||||
pub struct CollectionUser {
|
||||
pub user_uuid: String,
|
||||
pub collection_uuid: String,
|
||||
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: String,
|
||||
pub collection_uuid: String,
|
||||
pub cipher_uuid: CipherId,
|
||||
pub collection_uuid: CollectionId,
|
||||
}
|
||||
}
|
||||
|
||||
/// Local methods
|
||||
impl Collection {
|
||||
pub fn new(org_uuid: String, name: String, external_id: Option<String>) -> Self {
|
||||
pub fn new(org_uuid: OrganizationId, name: String, external_id: Option<String>) -> Self {
|
||||
let mut new_model = Self {
|
||||
uuid: crate::util::get_uuid(),
|
||||
uuid: CollectionId(crate::util::get_uuid()),
|
||||
org_uuid,
|
||||
name,
|
||||
external_id: None,
|
||||
@@ -74,22 +81,30 @@ impl Collection {
|
||||
|
||||
pub async fn to_json_details(
|
||||
&self,
|
||||
user_uuid: &str,
|
||||
user_uuid: &UserId,
|
||||
cipher_sync_data: Option<&crate::api::core::CipherSyncData>,
|
||||
conn: &mut DbConn,
|
||||
) -> Value {
|
||||
let (read_only, hide_passwords, can_manage) = if let Some(cipher_sync_data) = cipher_sync_data {
|
||||
match cipher_sync_data.user_organizations.get(&self.org_uuid) {
|
||||
// Only for Manager types Bitwarden returns true for the can_manage option
|
||||
// Owners and Admins always have true
|
||||
Some(uo) if uo.has_full_access() => (false, false, uo.atype >= UserOrgType::Manager),
|
||||
Some(uo) => {
|
||||
let (read_only, hide_passwords, manage) = if let Some(cipher_sync_data) = cipher_sync_data {
|
||||
match cipher_sync_data.members.get(&self.org_uuid) {
|
||||
// Only for Manager types Bitwarden returns true for the manage option
|
||||
// Owners and Admins always have true. Users are not able to have full access
|
||||
Some(m) if m.has_full_access() => (false, false, m.atype >= MembershipType::Manager),
|
||||
Some(m) => {
|
||||
// Only let a manager manage collections when the have full read/write access
|
||||
let is_manager = uo.atype == UserOrgType::Manager;
|
||||
if let Some(uc) = cipher_sync_data.user_collections.get(&self.uuid) {
|
||||
(uc.read_only, uc.hide_passwords, is_manager && !uc.read_only && !uc.hide_passwords)
|
||||
let is_manager = m.atype == MembershipType::Manager;
|
||||
if let Some(cu) = cipher_sync_data.user_collections.get(&self.uuid) {
|
||||
(
|
||||
cu.read_only,
|
||||
cu.hide_passwords,
|
||||
cu.manage || (is_manager && !cu.read_only && !cu.hide_passwords),
|
||||
)
|
||||
} else if let Some(cg) = cipher_sync_data.user_collections_groups.get(&self.uuid) {
|
||||
(cg.read_only, cg.hide_passwords, is_manager && !cg.read_only && !cg.hide_passwords)
|
||||
(
|
||||
cg.read_only,
|
||||
cg.hide_passwords,
|
||||
cg.manage || (is_manager && !cg.read_only && !cg.hide_passwords),
|
||||
)
|
||||
} else {
|
||||
(false, false, false)
|
||||
}
|
||||
@@ -97,19 +112,16 @@ impl Collection {
|
||||
_ => (true, true, false),
|
||||
}
|
||||
} else {
|
||||
match UserOrganization::find_confirmed_by_user_and_org(user_uuid, &self.org_uuid, conn).await {
|
||||
Some(ou) if ou.has_full_access() => (false, false, ou.atype >= UserOrgType::Manager),
|
||||
Some(ou) => {
|
||||
let is_manager = ou.atype == UserOrgType::Manager;
|
||||
match Membership::find_confirmed_by_user_and_org(user_uuid, &self.org_uuid, conn).await {
|
||||
Some(m) if m.has_full_access() => (false, false, m.atype >= MembershipType::Manager),
|
||||
Some(_) if self.is_manageable_by_user(user_uuid, conn).await => (false, false, true),
|
||||
Some(m) => {
|
||||
let is_manager = m.atype == MembershipType::Manager;
|
||||
let read_only = !self.is_writable_by_user(user_uuid, conn).await;
|
||||
let hide_passwords = self.hide_passwords_for_user(user_uuid, conn).await;
|
||||
(read_only, hide_passwords, is_manager && !read_only && !hide_passwords)
|
||||
}
|
||||
_ => (
|
||||
!self.is_writable_by_user(user_uuid, conn).await,
|
||||
self.hide_passwords_for_user(user_uuid, conn).await,
|
||||
false,
|
||||
),
|
||||
_ => (true, true, false),
|
||||
}
|
||||
};
|
||||
|
||||
@@ -117,17 +129,17 @@ impl Collection {
|
||||
json_object["object"] = json!("collectionDetails");
|
||||
json_object["readOnly"] = json!(read_only);
|
||||
json_object["hidePasswords"] = json!(hide_passwords);
|
||||
json_object["manage"] = json!(can_manage);
|
||||
json_object["manage"] = json!(manage);
|
||||
json_object
|
||||
}
|
||||
|
||||
pub async fn can_access_collection(org_user: &UserOrganization, col_id: &str, conn: &mut DbConn) -> bool {
|
||||
org_user.has_status(UserOrgStatus::Confirmed)
|
||||
&& (org_user.has_full_access()
|
||||
|| CollectionUser::has_access_to_collection_by_user(col_id, &org_user.user_uuid, conn).await
|
||||
pub async fn can_access_collection(member: &Membership, col_id: &CollectionId, conn: &mut 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
|
||||
|| (CONFIG.org_groups_enabled()
|
||||
&& (GroupUser::has_full_access_by_member(&org_user.org_uuid, &org_user.uuid, conn).await
|
||||
|| GroupUser::has_access_to_collection_by_member(col_id, &org_user.uuid, conn).await)))
|
||||
&& (GroupUser::has_full_access_by_member(&member.org_uuid, &member.uuid, conn).await
|
||||
|| GroupUser::has_access_to_collection_by_member(col_id, &member.uuid, conn).await)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +197,7 @@ impl Collection {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn delete_all_by_organization(org_uuid: &str, conn: &mut DbConn) -> EmptyResult {
|
||||
pub async fn delete_all_by_organization(org_uuid: &OrganizationId, conn: &mut DbConn) -> EmptyResult {
|
||||
for collection in Self::find_by_organization(org_uuid, conn).await {
|
||||
collection.delete(conn).await?;
|
||||
}
|
||||
@@ -193,12 +205,12 @@ impl Collection {
|
||||
}
|
||||
|
||||
pub async fn update_users_revision(&self, conn: &mut DbConn) {
|
||||
for user_org in UserOrganization::find_by_collection_and_org(&self.uuid, &self.org_uuid, conn).await.iter() {
|
||||
User::update_uuid_revision(&user_org.user_uuid, conn).await;
|
||||
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: &str, conn: &mut DbConn) -> Option<Self> {
|
||||
pub async fn find_by_uuid(uuid: &CollectionId, conn: &mut DbConn) -> Option<Self> {
|
||||
db_run! { conn: {
|
||||
collections::table
|
||||
.filter(collections::uuid.eq(uuid))
|
||||
@@ -208,7 +220,7 @@ impl Collection {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn find_by_user_uuid(user_uuid: String, conn: &mut DbConn) -> Vec<Self> {
|
||||
pub async fn find_by_user_uuid(user_uuid: UserId, conn: &mut DbConn) -> Vec<Self> {
|
||||
if CONFIG.org_groups_enabled() {
|
||||
db_run! { conn: {
|
||||
collections::table
|
||||
@@ -234,7 +246,7 @@ impl Collection {
|
||||
)
|
||||
))
|
||||
.filter(
|
||||
users_organizations::status.eq(UserOrgStatus::Confirmed as i32)
|
||||
users_organizations::status.eq(MembershipStatus::Confirmed as i32)
|
||||
)
|
||||
.filter(
|
||||
users_collections::user_uuid.eq(user_uuid).or( // Directly accessed collection
|
||||
@@ -265,7 +277,7 @@ impl Collection {
|
||||
)
|
||||
))
|
||||
.filter(
|
||||
users_organizations::status.eq(UserOrgStatus::Confirmed as i32)
|
||||
users_organizations::status.eq(MembershipStatus::Confirmed as i32)
|
||||
)
|
||||
.filter(
|
||||
users_collections::user_uuid.eq(user_uuid).or( // Directly accessed collection
|
||||
@@ -279,15 +291,19 @@ impl Collection {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn find_by_organization_and_user_uuid(org_uuid: &str, user_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
|
||||
pub async fn find_by_organization_and_user_uuid(
|
||||
org_uuid: &OrganizationId,
|
||||
user_uuid: &UserId,
|
||||
conn: &mut DbConn,
|
||||
) -> Vec<Self> {
|
||||
Self::find_by_user_uuid(user_uuid.to_owned(), conn)
|
||||
.await
|
||||
.into_iter()
|
||||
.filter(|c| c.org_uuid == org_uuid)
|
||||
.filter(|c| &c.org_uuid == org_uuid)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn find_by_organization(org_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
|
||||
pub async fn find_by_organization(org_uuid: &OrganizationId, conn: &mut DbConn) -> Vec<Self> {
|
||||
db_run! { conn: {
|
||||
collections::table
|
||||
.filter(collections::org_uuid.eq(org_uuid))
|
||||
@@ -297,7 +313,7 @@ impl Collection {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn count_by_org(org_uuid: &str, conn: &mut DbConn) -> i64 {
|
||||
pub async fn count_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> i64 {
|
||||
db_run! { conn: {
|
||||
collections::table
|
||||
.filter(collections::org_uuid.eq(org_uuid))
|
||||
@@ -308,7 +324,11 @@ impl Collection {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn find_by_uuid_and_org(uuid: &str, org_uuid: &str, conn: &mut DbConn) -> Option<Self> {
|
||||
pub async fn find_by_uuid_and_org(
|
||||
uuid: &CollectionId,
|
||||
org_uuid: &OrganizationId,
|
||||
conn: &mut DbConn,
|
||||
) -> Option<Self> {
|
||||
db_run! { conn: {
|
||||
collections::table
|
||||
.filter(collections::uuid.eq(uuid))
|
||||
@@ -320,7 +340,7 @@ impl Collection {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn find_by_uuid_and_user(uuid: &str, user_uuid: String, conn: &mut DbConn) -> Option<Self> {
|
||||
pub async fn find_by_uuid_and_user(uuid: &CollectionId, user_uuid: UserId, conn: &mut DbConn) -> Option<Self> {
|
||||
if CONFIG.org_groups_enabled() {
|
||||
db_run! { conn: {
|
||||
collections::table
|
||||
@@ -349,7 +369,7 @@ impl Collection {
|
||||
.filter(
|
||||
users_collections::collection_uuid.eq(uuid).or( // Directly accessed collection
|
||||
users_organizations::access_all.eq(true).or( // access_all in Organization
|
||||
users_organizations::atype.le(UserOrgType::Admin as i32) // Org admin or owner
|
||||
users_organizations::atype.le(MembershipType::Admin as i32) // Org admin or owner
|
||||
)).or(
|
||||
groups::access_all.eq(true) // access_all in groups
|
||||
).or( // access via groups
|
||||
@@ -378,7 +398,7 @@ impl Collection {
|
||||
.filter(
|
||||
users_collections::collection_uuid.eq(uuid).or( // Directly accessed collection
|
||||
users_organizations::access_all.eq(true).or( // access_all in Organization
|
||||
users_organizations::atype.le(UserOrgType::Admin as i32) // Org admin or owner
|
||||
users_organizations::atype.le(MembershipType::Admin as i32) // Org admin or owner
|
||||
))
|
||||
).select(collections::all_columns)
|
||||
.first::<CollectionDb>(conn).ok()
|
||||
@@ -387,7 +407,7 @@ impl Collection {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn is_writable_by_user(&self, user_uuid: &str, conn: &mut DbConn) -> bool {
|
||||
pub async fn is_writable_by_user(&self, user_uuid: &UserId, conn: &mut DbConn) -> bool {
|
||||
let user_uuid = user_uuid.to_string();
|
||||
if CONFIG.org_groups_enabled() {
|
||||
db_run! { conn: {
|
||||
@@ -411,7 +431,7 @@ impl Collection {
|
||||
collections_groups::groups_uuid.eq(groups_users::groups_uuid)
|
||||
.and(collections_groups::collections_uuid.eq(collections::uuid))
|
||||
))
|
||||
.filter(users_organizations::atype.le(UserOrgType::Admin as i32) // Org admin or owner
|
||||
.filter(users_organizations::atype.le(MembershipType::Admin as i32) // Org admin or owner
|
||||
.or(users_organizations::access_all.eq(true)) // access_all via membership
|
||||
.or(users_collections::collection_uuid.eq(&self.uuid) // write access given to collection
|
||||
.and(users_collections::read_only.eq(false)))
|
||||
@@ -436,7 +456,7 @@ impl Collection {
|
||||
users_collections::collection_uuid.eq(collections::uuid)
|
||||
.and(users_collections::user_uuid.eq(user_uuid))
|
||||
))
|
||||
.filter(users_organizations::atype.le(UserOrgType::Admin as i32) // Org admin or owner
|
||||
.filter(users_organizations::atype.le(MembershipType::Admin as i32) // Org admin or owner
|
||||
.or(users_organizations::access_all.eq(true)) // access_all via membership
|
||||
.or(users_collections::collection_uuid.eq(&self.uuid) // write access given to collection
|
||||
.and(users_collections::read_only.eq(false)))
|
||||
@@ -449,7 +469,7 @@ impl Collection {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn hide_passwords_for_user(&self, user_uuid: &str, conn: &mut DbConn) -> bool {
|
||||
pub async fn hide_passwords_for_user(&self, user_uuid: &UserId, conn: &mut DbConn) -> bool {
|
||||
let user_uuid = user_uuid.to_string();
|
||||
db_run! { conn: {
|
||||
collections::table
|
||||
@@ -478,7 +498,7 @@ impl Collection {
|
||||
.filter(
|
||||
users_collections::collection_uuid.eq(&self.uuid).and(users_collections::hide_passwords.eq(true)).or(// Directly accessed collection
|
||||
users_organizations::access_all.eq(true).or( // access_all in Organization
|
||||
users_organizations::atype.le(UserOrgType::Admin as i32) // Org admin or owner
|
||||
users_organizations::atype.le(MembershipType::Admin as i32) // Org admin or owner
|
||||
)).or(
|
||||
groups::access_all.eq(true) // access_all in groups
|
||||
).or( // access via groups
|
||||
@@ -494,11 +514,61 @@ impl Collection {
|
||||
.unwrap_or(0) != 0
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn is_manageable_by_user(&self, user_uuid: &UserId, conn: &mut DbConn) -> bool {
|
||||
let user_uuid = user_uuid.to_string();
|
||||
db_run! { conn: {
|
||||
collections::table
|
||||
.left_join(users_collections::table.on(
|
||||
users_collections::collection_uuid.eq(collections::uuid).and(
|
||||
users_collections::user_uuid.eq(user_uuid.clone())
|
||||
)
|
||||
))
|
||||
.left_join(users_organizations::table.on(
|
||||
collections::org_uuid.eq(users_organizations::org_uuid).and(
|
||||
users_organizations::user_uuid.eq(user_uuid)
|
||||
)
|
||||
))
|
||||
.left_join(groups_users::table.on(
|
||||
groups_users::users_organizations_uuid.eq(users_organizations::uuid)
|
||||
))
|
||||
.left_join(groups::table.on(
|
||||
groups::uuid.eq(groups_users::groups_uuid)
|
||||
))
|
||||
.left_join(collections_groups::table.on(
|
||||
collections_groups::groups_uuid.eq(groups_users::groups_uuid).and(
|
||||
collections_groups::collections_uuid.eq(collections::uuid)
|
||||
)
|
||||
))
|
||||
.filter(collections::uuid.eq(&self.uuid))
|
||||
.filter(
|
||||
users_collections::collection_uuid.eq(&self.uuid).and(users_collections::manage.eq(true)).or(// Directly accessed collection
|
||||
users_organizations::access_all.eq(true).or( // access_all in Organization
|
||||
users_organizations::atype.le(MembershipType::Admin as i32) // Org admin or owner
|
||||
)).or(
|
||||
groups::access_all.eq(true) // access_all in groups
|
||||
).or( // access via groups
|
||||
groups_users::users_organizations_uuid.eq(users_organizations::uuid).and(
|
||||
collections_groups::collections_uuid.is_not_null().and(
|
||||
collections_groups::manage.eq(true))
|
||||
)
|
||||
)
|
||||
)
|
||||
.count()
|
||||
.first::<i64>(conn)
|
||||
.ok()
|
||||
.unwrap_or(0) != 0
|
||||
}}
|
||||
}
|
||||
}
|
||||
|
||||
/// Database methods
|
||||
impl CollectionUser {
|
||||
pub async fn find_by_organization_and_user_uuid(org_uuid: &str, user_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
|
||||
pub async fn find_by_organization_and_user_uuid(
|
||||
org_uuid: &OrganizationId,
|
||||
user_uuid: &UserId,
|
||||
conn: &mut DbConn,
|
||||
) -> Vec<Self> {
|
||||
db_run! { conn: {
|
||||
users_collections::table
|
||||
.filter(users_collections::user_uuid.eq(user_uuid))
|
||||
@@ -511,24 +581,30 @@ impl CollectionUser {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn find_by_organization(org_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
|
||||
db_run! { conn: {
|
||||
pub async fn find_by_organization_swap_user_uuid_with_member_uuid(
|
||||
org_uuid: &OrganizationId,
|
||||
conn: &mut DbConn,
|
||||
) -> Vec<CollectionMembership> {
|
||||
let col_users = db_run! { conn: {
|
||||
users_collections::table
|
||||
.inner_join(collections::table.on(collections::uuid.eq(users_collections::collection_uuid)))
|
||||
.filter(collections::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))
|
||||
.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)
|
||||
.expect("Error loading users_collections")
|
||||
.from_db()
|
||||
}}
|
||||
}};
|
||||
col_users.into_iter().map(|c| c.into()).collect()
|
||||
}
|
||||
|
||||
pub async fn save(
|
||||
user_uuid: &str,
|
||||
collection_uuid: &str,
|
||||
user_uuid: &UserId,
|
||||
collection_uuid: &CollectionId,
|
||||
read_only: bool,
|
||||
hide_passwords: bool,
|
||||
manage: bool,
|
||||
conn: &mut DbConn,
|
||||
) -> EmptyResult {
|
||||
User::update_uuid_revision(user_uuid, conn).await;
|
||||
@@ -541,6 +617,7 @@ impl CollectionUser {
|
||||
users_collections::collection_uuid.eq(collection_uuid),
|
||||
users_collections::read_only.eq(read_only),
|
||||
users_collections::hide_passwords.eq(hide_passwords),
|
||||
users_collections::manage.eq(manage),
|
||||
))
|
||||
.execute(conn)
|
||||
{
|
||||
@@ -555,6 +632,7 @@ impl CollectionUser {
|
||||
users_collections::collection_uuid.eq(collection_uuid),
|
||||
users_collections::read_only.eq(read_only),
|
||||
users_collections::hide_passwords.eq(hide_passwords),
|
||||
users_collections::manage.eq(manage),
|
||||
))
|
||||
.execute(conn)
|
||||
.map_res("Error adding user to collection")
|
||||
@@ -569,12 +647,14 @@ impl CollectionUser {
|
||||
users_collections::collection_uuid.eq(collection_uuid),
|
||||
users_collections::read_only.eq(read_only),
|
||||
users_collections::hide_passwords.eq(hide_passwords),
|
||||
users_collections::manage.eq(manage),
|
||||
))
|
||||
.on_conflict((users_collections::user_uuid, users_collections::collection_uuid))
|
||||
.do_update()
|
||||
.set((
|
||||
users_collections::read_only.eq(read_only),
|
||||
users_collections::hide_passwords.eq(hide_passwords),
|
||||
users_collections::manage.eq(manage),
|
||||
))
|
||||
.execute(conn)
|
||||
.map_res("Error adding user to collection")
|
||||
@@ -596,7 +676,7 @@ impl CollectionUser {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn find_by_collection(collection_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
|
||||
pub async fn find_by_collection(collection_uuid: &CollectionId, conn: &mut DbConn) -> Vec<Self> {
|
||||
db_run! { conn: {
|
||||
users_collections::table
|
||||
.filter(users_collections::collection_uuid.eq(collection_uuid))
|
||||
@@ -607,24 +687,27 @@ impl CollectionUser {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn find_by_collection_swap_user_uuid_with_org_user_uuid(
|
||||
collection_uuid: &str,
|
||||
pub async fn find_by_org_and_coll_swap_user_uuid_with_member_uuid(
|
||||
org_uuid: &OrganizationId,
|
||||
collection_uuid: &CollectionId,
|
||||
conn: &mut DbConn,
|
||||
) -> Vec<Self> {
|
||||
db_run! { conn: {
|
||||
) -> Vec<CollectionMembership> {
|
||||
let col_users = db_run! { conn: {
|
||||
users_collections::table
|
||||
.filter(users_collections::collection_uuid.eq(collection_uuid))
|
||||
.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))
|
||||
.select((users_organizations::uuid, users_collections::collection_uuid, users_collections::read_only, users_collections::hide_passwords, users_collections::manage))
|
||||
.load::<CollectionUserDb>(conn)
|
||||
.expect("Error loading users_collections")
|
||||
.from_db()
|
||||
}}
|
||||
}};
|
||||
col_users.into_iter().map(|c| c.into()).collect()
|
||||
}
|
||||
|
||||
pub async fn find_by_collection_and_user(
|
||||
collection_uuid: &str,
|
||||
user_uuid: &str,
|
||||
collection_uuid: &CollectionId,
|
||||
user_uuid: &UserId,
|
||||
conn: &mut DbConn,
|
||||
) -> Option<Self> {
|
||||
db_run! { conn: {
|
||||
@@ -638,7 +721,7 @@ impl CollectionUser {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn find_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
|
||||
pub async fn find_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
|
||||
db_run! { conn: {
|
||||
users_collections::table
|
||||
.filter(users_collections::user_uuid.eq(user_uuid))
|
||||
@@ -649,7 +732,7 @@ impl CollectionUser {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn delete_all_by_collection(collection_uuid: &str, conn: &mut DbConn) -> EmptyResult {
|
||||
pub async fn delete_all_by_collection(collection_uuid: &CollectionId, conn: &mut DbConn) -> EmptyResult {
|
||||
for collection in CollectionUser::find_by_collection(collection_uuid, conn).await.iter() {
|
||||
User::update_uuid_revision(&collection.user_uuid, conn).await;
|
||||
}
|
||||
@@ -661,7 +744,11 @@ impl CollectionUser {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn delete_all_by_user_and_org(user_uuid: &str, org_uuid: &str, conn: &mut DbConn) -> EmptyResult {
|
||||
pub async fn delete_all_by_user_and_org(
|
||||
user_uuid: &UserId,
|
||||
org_uuid: &OrganizationId,
|
||||
conn: &mut DbConn,
|
||||
) -> EmptyResult {
|
||||
let collectionusers = Self::find_by_organization_and_user_uuid(org_uuid, user_uuid, conn).await;
|
||||
|
||||
db_run! { conn: {
|
||||
@@ -677,14 +764,18 @@ impl CollectionUser {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn has_access_to_collection_by_user(col_id: &str, user_uuid: &str, conn: &mut DbConn) -> bool {
|
||||
pub async fn has_access_to_collection_by_user(
|
||||
col_id: &CollectionId,
|
||||
user_uuid: &UserId,
|
||||
conn: &mut 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: &str, collection_uuid: &str, conn: &mut DbConn) -> EmptyResult {
|
||||
pub async fn save(cipher_uuid: &CipherId, collection_uuid: &CollectionId, conn: &mut DbConn) -> EmptyResult {
|
||||
Self::update_users_revision(collection_uuid, conn).await;
|
||||
|
||||
db_run! { conn:
|
||||
@@ -714,7 +805,7 @@ impl CollectionCipher {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete(cipher_uuid: &str, collection_uuid: &str, conn: &mut DbConn) -> EmptyResult {
|
||||
pub async fn delete(cipher_uuid: &CipherId, collection_uuid: &CollectionId, conn: &mut DbConn) -> EmptyResult {
|
||||
Self::update_users_revision(collection_uuid, conn).await;
|
||||
|
||||
db_run! { conn: {
|
||||
@@ -728,7 +819,7 @@ impl CollectionCipher {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn delete_all_by_cipher(cipher_uuid: &str, conn: &mut DbConn) -> EmptyResult {
|
||||
pub async fn delete_all_by_cipher(cipher_uuid: &CipherId, conn: &mut DbConn) -> EmptyResult {
|
||||
db_run! { conn: {
|
||||
diesel::delete(ciphers_collections::table.filter(ciphers_collections::cipher_uuid.eq(cipher_uuid)))
|
||||
.execute(conn)
|
||||
@@ -736,7 +827,7 @@ impl CollectionCipher {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn delete_all_by_collection(collection_uuid: &str, conn: &mut DbConn) -> EmptyResult {
|
||||
pub async fn delete_all_by_collection(collection_uuid: &CollectionId, conn: &mut DbConn) -> EmptyResult {
|
||||
db_run! { conn: {
|
||||
diesel::delete(ciphers_collections::table.filter(ciphers_collections::collection_uuid.eq(collection_uuid)))
|
||||
.execute(conn)
|
||||
@@ -744,9 +835,63 @@ impl CollectionCipher {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn update_users_revision(collection_uuid: &str, conn: &mut DbConn) {
|
||||
pub async fn update_users_revision(collection_uuid: &CollectionId, conn: &mut DbConn) {
|
||||
if let Some(collection) = Collection::find_by_uuid(collection_uuid, conn).await {
|
||||
collection.update_users_revision(conn).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Added in case we need the membership_uuid instead of the user_uuid
|
||||
pub struct CollectionMembership {
|
||||
pub membership_uuid: MembershipId,
|
||||
pub collection_uuid: CollectionId,
|
||||
pub read_only: bool,
|
||||
pub hide_passwords: bool,
|
||||
pub manage: bool,
|
||||
}
|
||||
|
||||
impl CollectionMembership {
|
||||
pub fn to_json_details_for_member(&self, membership_type: i32) -> Value {
|
||||
json!({
|
||||
"id": self.membership_uuid,
|
||||
"readOnly": self.read_only,
|
||||
"hidePasswords": self.hide_passwords,
|
||||
"manage": membership_type >= MembershipType::Admin
|
||||
|| self.manage
|
||||
|| (membership_type == MembershipType::Manager
|
||||
&& !self.read_only
|
||||
&& !self.hide_passwords),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CollectionUser> for CollectionMembership {
|
||||
fn from(c: CollectionUser) -> Self {
|
||||
Self {
|
||||
membership_uuid: c.user_uuid.to_string().into(),
|
||||
collection_uuid: c.collection_uuid,
|
||||
read_only: c.read_only,
|
||||
hide_passwords: c.hide_passwords,
|
||||
manage: c.manage,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone,
|
||||
Debug,
|
||||
AsRef,
|
||||
Deref,
|
||||
DieselNewType,
|
||||
Display,
|
||||
From,
|
||||
FromForm,
|
||||
Hash,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
UuidFromParam,
|
||||
)]
|
||||
pub struct CollectionId(String);
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
use chrono::{NaiveDateTime, Utc};
|
||||
use derive_more::{Display, From};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{crypto, CONFIG};
|
||||
use core::fmt;
|
||||
use super::{AuthRequest, UserId};
|
||||
use crate::{crypto, util::format_date, CONFIG};
|
||||
use macros::IdFromParam;
|
||||
|
||||
db_object! {
|
||||
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
|
||||
@@ -9,11 +12,11 @@ db_object! {
|
||||
#[diesel(treat_none_as_null = true)]
|
||||
#[diesel(primary_key(uuid, user_uuid))]
|
||||
pub struct Device {
|
||||
pub uuid: String,
|
||||
pub uuid: DeviceId,
|
||||
pub created_at: NaiveDateTime,
|
||||
pub updated_at: NaiveDateTime,
|
||||
|
||||
pub user_uuid: String,
|
||||
pub user_uuid: UserId,
|
||||
|
||||
pub name: String,
|
||||
pub atype: i32, // https://github.com/bitwarden/server/blob/dcc199bcce4aa2d5621f6fab80f1b49d8b143418/src/Core/Enums/DeviceType.cs
|
||||
@@ -21,14 +24,13 @@ db_object! {
|
||||
pub push_token: Option<String>,
|
||||
|
||||
pub refresh_token: String,
|
||||
|
||||
pub twofactor_remember: Option<String>,
|
||||
}
|
||||
}
|
||||
|
||||
/// Local methods
|
||||
impl Device {
|
||||
pub fn new(uuid: String, user_uuid: String, name: String, atype: i32) -> Self {
|
||||
pub fn new(uuid: DeviceId, user_uuid: UserId, name: String, atype: i32) -> Self {
|
||||
let now = Utc::now().naive_utc();
|
||||
|
||||
Self {
|
||||
@@ -47,6 +49,18 @@ impl Device {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_json(&self) -> Value {
|
||||
json!({
|
||||
"id": self.uuid,
|
||||
"name": self.name,
|
||||
"type": self.atype,
|
||||
"identifier": self.push_uuid,
|
||||
"creationDate": format_date(&self.created_at),
|
||||
"isTrusted": false,
|
||||
"object":"device"
|
||||
})
|
||||
}
|
||||
|
||||
pub fn refresh_twofactor_remember(&mut self) -> String {
|
||||
use data_encoding::BASE64;
|
||||
let twofactor_remember = crypto::encode_random_bytes::<180>(BASE64);
|
||||
@@ -75,12 +89,12 @@ impl Device {
|
||||
// Also These key/value pairs are not used anywhere by either Vaultwarden or Bitwarden Clients
|
||||
// Because these might get used in the future, and they are added by the Bitwarden Server, lets keep it, but then commented out
|
||||
// ---
|
||||
// fn arg: orgs: Vec<super::UserOrganization>,
|
||||
// fn arg: members: Vec<super::Membership>,
|
||||
// ---
|
||||
// let orgowner: Vec<_> = orgs.iter().filter(|o| o.atype == 0).map(|o| o.org_uuid.clone()).collect();
|
||||
// let orgadmin: Vec<_> = orgs.iter().filter(|o| o.atype == 1).map(|o| o.org_uuid.clone()).collect();
|
||||
// let orguser: Vec<_> = orgs.iter().filter(|o| o.atype == 2).map(|o| o.org_uuid.clone()).collect();
|
||||
// let orgmanager: Vec<_> = orgs.iter().filter(|o| o.atype == 3).map(|o| o.org_uuid.clone()).collect();
|
||||
// let orgowner: Vec<_> = members.iter().filter(|m| m.atype == 0).map(|o| o.org_uuid.clone()).collect();
|
||||
// let orgadmin: Vec<_> = members.iter().filter(|m| m.atype == 1).map(|o| o.org_uuid.clone()).collect();
|
||||
// let orguser: Vec<_> = members.iter().filter(|m| m.atype == 2).map(|o| o.org_uuid.clone()).collect();
|
||||
// let orgmanager: Vec<_> = members.iter().filter(|m| m.atype == 3).map(|o| o.org_uuid.clone()).collect();
|
||||
|
||||
// Create the JWT claims struct, to send to the client
|
||||
use crate::auth::{encode_jwt, LoginJwtClaims, DEFAULT_VALIDITY, JWT_LOGIN_ISSUER};
|
||||
@@ -121,8 +135,42 @@ impl Device {
|
||||
pub fn is_registered(&self) -> bool {
|
||||
self.push_uuid.is_some()
|
||||
}
|
||||
|
||||
pub fn is_cli(&self) -> bool {
|
||||
matches!(DeviceType::from_i32(self.atype), DeviceType::WindowsCLI | DeviceType::MacOsCLI | DeviceType::LinuxCLI)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DeviceWithAuthRequest {
|
||||
pub device: Device,
|
||||
pub pending_auth_request: Option<AuthRequest>,
|
||||
}
|
||||
|
||||
impl DeviceWithAuthRequest {
|
||||
pub fn to_json(&self) -> Value {
|
||||
let auth_request = match &self.pending_auth_request {
|
||||
Some(auth_request) => auth_request.to_json_for_pending_device(),
|
||||
None => Value::Null,
|
||||
};
|
||||
json!({
|
||||
"id": self.device.uuid,
|
||||
"name": self.device.name,
|
||||
"type": self.device.atype,
|
||||
"identifier": self.device.push_uuid,
|
||||
"creationDate": format_date(&self.device.created_at),
|
||||
"devicePendingAuthRequest": auth_request,
|
||||
"isTrusted": false,
|
||||
"object": "device",
|
||||
})
|
||||
}
|
||||
|
||||
pub fn from(c: Device, a: Option<AuthRequest>) -> Self {
|
||||
Self {
|
||||
device: c,
|
||||
pending_auth_request: a,
|
||||
}
|
||||
}
|
||||
}
|
||||
use crate::db::DbConn;
|
||||
|
||||
use crate::api::EmptyResult;
|
||||
@@ -150,7 +198,7 @@ impl Device {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_all_by_user(user_uuid: &str, conn: &mut DbConn) -> EmptyResult {
|
||||
pub async fn delete_all_by_user(user_uuid: &UserId, conn: &mut DbConn) -> EmptyResult {
|
||||
db_run! { conn: {
|
||||
diesel::delete(devices::table.filter(devices::user_uuid.eq(user_uuid)))
|
||||
.execute(conn)
|
||||
@@ -158,7 +206,7 @@ impl Device {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn find_by_uuid_and_user(uuid: &str, user_uuid: &str, conn: &mut DbConn) -> Option<Self> {
|
||||
pub async fn find_by_uuid_and_user(uuid: &DeviceId, user_uuid: &UserId, conn: &mut DbConn) -> Option<Self> {
|
||||
db_run! { conn: {
|
||||
devices::table
|
||||
.filter(devices::uuid.eq(uuid))
|
||||
@@ -169,7 +217,17 @@ impl Device {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn find_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
|
||||
pub async fn find_with_auth_request_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<DeviceWithAuthRequest> {
|
||||
let devices = Self::find_by_user(user_uuid, conn).await;
|
||||
let mut result = Vec::new();
|
||||
for device in devices {
|
||||
let auth_request = AuthRequest::find_by_user_and_requested_device(user_uuid, &device.uuid, conn).await;
|
||||
result.push(DeviceWithAuthRequest::from(device, auth_request));
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub async fn find_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
|
||||
db_run! { conn: {
|
||||
devices::table
|
||||
.filter(devices::user_uuid.eq(user_uuid))
|
||||
@@ -179,7 +237,7 @@ impl Device {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn find_by_uuid(uuid: &str, conn: &mut DbConn) -> Option<Self> {
|
||||
pub async fn find_by_uuid(uuid: &DeviceId, conn: &mut DbConn) -> Option<Self> {
|
||||
db_run! { conn: {
|
||||
devices::table
|
||||
.filter(devices::uuid.eq(uuid))
|
||||
@@ -189,7 +247,7 @@ impl Device {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn clear_push_token_by_uuid(uuid: &str, conn: &mut DbConn) -> EmptyResult {
|
||||
pub async fn clear_push_token_by_uuid(uuid: &DeviceId, conn: &mut DbConn) -> EmptyResult {
|
||||
db_run! { conn: {
|
||||
diesel::update(devices::table)
|
||||
.filter(devices::uuid.eq(uuid))
|
||||
@@ -208,7 +266,7 @@ impl Device {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn find_latest_active_by_user(user_uuid: &str, conn: &mut DbConn) -> Option<Self> {
|
||||
pub async fn find_latest_active_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Option<Self> {
|
||||
db_run! { conn: {
|
||||
devices::table
|
||||
.filter(devices::user_uuid.eq(user_uuid))
|
||||
@@ -219,7 +277,7 @@ impl Device {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn find_push_devices_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
|
||||
pub async fn find_push_devices_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
|
||||
db_run! { conn: {
|
||||
devices::table
|
||||
.filter(devices::user_uuid.eq(user_uuid))
|
||||
@@ -230,7 +288,7 @@ impl Device {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn check_user_has_push_device(user_uuid: &str, conn: &mut DbConn) -> bool {
|
||||
pub async fn check_user_has_push_device(user_uuid: &UserId, conn: &mut DbConn) -> bool {
|
||||
db_run! { conn: {
|
||||
devices::table
|
||||
.filter(devices::user_uuid.eq(user_uuid))
|
||||
@@ -243,68 +301,62 @@ impl Device {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Display)]
|
||||
pub enum DeviceType {
|
||||
#[display("Android")]
|
||||
Android = 0,
|
||||
#[display("iOS")]
|
||||
Ios = 1,
|
||||
#[display("Chrome Extension")]
|
||||
ChromeExtension = 2,
|
||||
#[display("Firefox Extension")]
|
||||
FirefoxExtension = 3,
|
||||
#[display("Opera Extension")]
|
||||
OperaExtension = 4,
|
||||
#[display("Edge Extension")]
|
||||
EdgeExtension = 5,
|
||||
#[display("Windows")]
|
||||
WindowsDesktop = 6,
|
||||
#[display("macOS")]
|
||||
MacOsDesktop = 7,
|
||||
#[display("Linux")]
|
||||
LinuxDesktop = 8,
|
||||
#[display("Chrome")]
|
||||
ChromeBrowser = 9,
|
||||
#[display("Firefox")]
|
||||
FirefoxBrowser = 10,
|
||||
#[display("Opera")]
|
||||
OperaBrowser = 11,
|
||||
#[display("Edge")]
|
||||
EdgeBrowser = 12,
|
||||
#[display("Internet Explorer")]
|
||||
IEBrowser = 13,
|
||||
#[display("Unknown Browser")]
|
||||
UnknownBrowser = 14,
|
||||
#[display("Android")]
|
||||
AndroidAmazon = 15,
|
||||
#[display("UWP")]
|
||||
Uwp = 16,
|
||||
#[display("Safari")]
|
||||
SafariBrowser = 17,
|
||||
#[display("Vivaldi")]
|
||||
VivaldiBrowser = 18,
|
||||
#[display("Vivaldi Extension")]
|
||||
VivaldiExtension = 19,
|
||||
#[display("Safari Extension")]
|
||||
SafariExtension = 20,
|
||||
#[display("SDK")]
|
||||
Sdk = 21,
|
||||
#[display("Server")]
|
||||
Server = 22,
|
||||
#[display("Windows CLI")]
|
||||
WindowsCLI = 23,
|
||||
#[display("macOS CLI")]
|
||||
MacOsCLI = 24,
|
||||
#[display("Linux CLI")]
|
||||
LinuxCLI = 25,
|
||||
}
|
||||
|
||||
impl fmt::Display for DeviceType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
DeviceType::Android => write!(f, "Android"),
|
||||
DeviceType::Ios => write!(f, "iOS"),
|
||||
DeviceType::ChromeExtension => write!(f, "Chrome Extension"),
|
||||
DeviceType::FirefoxExtension => write!(f, "Firefox Extension"),
|
||||
DeviceType::OperaExtension => write!(f, "Opera Extension"),
|
||||
DeviceType::EdgeExtension => write!(f, "Edge Extension"),
|
||||
DeviceType::WindowsDesktop => write!(f, "Windows"),
|
||||
DeviceType::MacOsDesktop => write!(f, "macOS"),
|
||||
DeviceType::LinuxDesktop => write!(f, "Linux"),
|
||||
DeviceType::ChromeBrowser => write!(f, "Chrome"),
|
||||
DeviceType::FirefoxBrowser => write!(f, "Firefox"),
|
||||
DeviceType::OperaBrowser => write!(f, "Opera"),
|
||||
DeviceType::EdgeBrowser => write!(f, "Edge"),
|
||||
DeviceType::IEBrowser => write!(f, "Internet Explorer"),
|
||||
DeviceType::UnknownBrowser => write!(f, "Unknown Browser"),
|
||||
DeviceType::AndroidAmazon => write!(f, "Android"),
|
||||
DeviceType::Uwp => write!(f, "UWP"),
|
||||
DeviceType::SafariBrowser => write!(f, "Safari"),
|
||||
DeviceType::VivaldiBrowser => write!(f, "Vivaldi"),
|
||||
DeviceType::VivaldiExtension => write!(f, "Vivaldi Extension"),
|
||||
DeviceType::SafariExtension => write!(f, "Safari Extension"),
|
||||
DeviceType::Sdk => write!(f, "SDK"),
|
||||
DeviceType::Server => write!(f, "Server"),
|
||||
DeviceType::WindowsCLI => write!(f, "Windows CLI"),
|
||||
DeviceType::MacOsCLI => write!(f, "macOS CLI"),
|
||||
DeviceType::LinuxCLI => write!(f, "Linux CLI"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DeviceType {
|
||||
pub fn from_i32(value: i32) -> DeviceType {
|
||||
match value {
|
||||
@@ -338,3 +390,8 @@ impl DeviceType {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Debug, DieselNewType, Display, From, FromForm, Hash, PartialEq, Eq, Serialize, Deserialize, IdFromParam,
|
||||
)]
|
||||
pub struct DeviceId(String);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use chrono::{NaiveDateTime, Utc};
|
||||
use derive_more::{AsRef, Deref, Display, From};
|
||||
use serde_json::Value;
|
||||
|
||||
use super::{User, UserId};
|
||||
use crate::{api::EmptyResult, db::DbConn, error::MapResult};
|
||||
|
||||
use super::User;
|
||||
use macros::UuidFromParam;
|
||||
|
||||
db_object! {
|
||||
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
|
||||
@@ -11,9 +12,9 @@ db_object! {
|
||||
#[diesel(treat_none_as_null = true)]
|
||||
#[diesel(primary_key(uuid))]
|
||||
pub struct EmergencyAccess {
|
||||
pub uuid: String,
|
||||
pub grantor_uuid: String,
|
||||
pub grantee_uuid: Option<String>,
|
||||
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
|
||||
@@ -29,11 +30,11 @@ db_object! {
|
||||
// Local methods
|
||||
|
||||
impl EmergencyAccess {
|
||||
pub fn new(grantor_uuid: String, email: String, status: i32, atype: i32, wait_time_days: i32) -> Self {
|
||||
pub fn new(grantor_uuid: UserId, email: String, status: i32, atype: i32, wait_time_days: i32) -> Self {
|
||||
let now = Utc::now().naive_utc();
|
||||
|
||||
Self {
|
||||
uuid: crate::util::get_uuid(),
|
||||
uuid: EmergencyAccessId(crate::util::get_uuid()),
|
||||
grantor_uuid,
|
||||
grantee_uuid: None,
|
||||
email: Some(email),
|
||||
@@ -82,7 +83,7 @@ impl EmergencyAccess {
|
||||
}
|
||||
|
||||
pub async fn to_json_grantee_details(&self, conn: &mut DbConn) -> Option<Value> {
|
||||
let grantee_user = if let Some(grantee_uuid) = self.grantee_uuid.as_deref() {
|
||||
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() {
|
||||
match User::find_by_mail(email, conn).await {
|
||||
@@ -211,7 +212,7 @@ impl EmergencyAccess {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn delete_all_by_user(user_uuid: &str, conn: &mut DbConn) -> EmptyResult {
|
||||
pub async fn delete_all_by_user(user_uuid: &UserId, conn: &mut DbConn) -> EmptyResult {
|
||||
for ea in Self::find_all_by_grantor_uuid(user_uuid, conn).await {
|
||||
ea.delete(conn).await?;
|
||||
}
|
||||
@@ -239,8 +240,8 @@ impl EmergencyAccess {
|
||||
}
|
||||
|
||||
pub async fn find_by_grantor_uuid_and_grantee_uuid_or_email(
|
||||
grantor_uuid: &str,
|
||||
grantee_uuid: &str,
|
||||
grantor_uuid: &UserId,
|
||||
grantee_uuid: &UserId,
|
||||
email: &str,
|
||||
conn: &mut DbConn,
|
||||
) -> Option<Self> {
|
||||
@@ -262,7 +263,11 @@ impl EmergencyAccess {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn find_by_uuid_and_grantor_uuid(uuid: &str, grantor_uuid: &str, conn: &mut DbConn) -> Option<Self> {
|
||||
pub async fn find_by_uuid_and_grantor_uuid(
|
||||
uuid: &EmergencyAccessId,
|
||||
grantor_uuid: &UserId,
|
||||
conn: &mut DbConn,
|
||||
) -> Option<Self> {
|
||||
db_run! { conn: {
|
||||
emergency_access::table
|
||||
.filter(emergency_access::uuid.eq(uuid))
|
||||
@@ -272,7 +277,11 @@ impl EmergencyAccess {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn find_by_uuid_and_grantee_uuid(uuid: &str, grantee_uuid: &str, conn: &mut DbConn) -> Option<Self> {
|
||||
pub async fn find_by_uuid_and_grantee_uuid(
|
||||
uuid: &EmergencyAccessId,
|
||||
grantee_uuid: &UserId,
|
||||
conn: &mut DbConn,
|
||||
) -> Option<Self> {
|
||||
db_run! { conn: {
|
||||
emergency_access::table
|
||||
.filter(emergency_access::uuid.eq(uuid))
|
||||
@@ -282,7 +291,11 @@ impl EmergencyAccess {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn find_by_uuid_and_grantee_email(uuid: &str, grantee_email: &str, conn: &mut DbConn) -> Option<Self> {
|
||||
pub async fn find_by_uuid_and_grantee_email(
|
||||
uuid: &EmergencyAccessId,
|
||||
grantee_email: &str,
|
||||
conn: &mut DbConn,
|
||||
) -> Option<Self> {
|
||||
db_run! { conn: {
|
||||
emergency_access::table
|
||||
.filter(emergency_access::uuid.eq(uuid))
|
||||
@@ -292,7 +305,7 @@ impl EmergencyAccess {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn find_all_by_grantee_uuid(grantee_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
|
||||
pub async fn find_all_by_grantee_uuid(grantee_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
|
||||
db_run! { conn: {
|
||||
emergency_access::table
|
||||
.filter(emergency_access::grantee_uuid.eq(grantee_uuid))
|
||||
@@ -319,7 +332,7 @@ impl EmergencyAccess {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn find_all_by_grantor_uuid(grantor_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
|
||||
pub async fn find_all_by_grantor_uuid(grantor_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
|
||||
db_run! { conn: {
|
||||
emergency_access::table
|
||||
.filter(emergency_access::grantor_uuid.eq(grantor_uuid))
|
||||
@@ -327,7 +340,12 @@ impl EmergencyAccess {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn accept_invite(&mut self, grantee_uuid: &str, grantee_email: &str, conn: &mut DbConn) -> EmptyResult {
|
||||
pub async fn accept_invite(
|
||||
&mut self,
|
||||
grantee_uuid: &UserId,
|
||||
grantee_email: &str,
|
||||
conn: &mut DbConn,
|
||||
) -> EmptyResult {
|
||||
if self.email.is_none() || self.email.as_ref().unwrap() != grantee_email {
|
||||
err!("User email does not match invite.");
|
||||
}
|
||||
@@ -337,10 +355,28 @@ impl EmergencyAccess {
|
||||
}
|
||||
|
||||
self.status = EmergencyAccessStatus::Accepted as i32;
|
||||
self.grantee_uuid = Some(String::from(grantee_uuid));
|
||||
self.grantee_uuid = Some(grantee_uuid.clone());
|
||||
self.email = None;
|
||||
self.save(conn).await
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
#[derive(
|
||||
Clone,
|
||||
Debug,
|
||||
AsRef,
|
||||
Deref,
|
||||
DieselNewType,
|
||||
Display,
|
||||
From,
|
||||
FromForm,
|
||||
Hash,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
UuidFromParam,
|
||||
)]
|
||||
pub struct EmergencyAccessId(String);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use crate::db::DbConn;
|
||||
use chrono::{NaiveDateTime, TimeDelta, Utc};
|
||||
//use derive_more::{AsRef, Deref, Display, From};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{api::EmptyResult, error::MapResult, CONFIG};
|
||||
|
||||
use chrono::{NaiveDateTime, TimeDelta, Utc};
|
||||
use super::{CipherId, CollectionId, GroupId, MembershipId, OrgPolicyId, OrganizationId, UserId};
|
||||
use crate::{api::EmptyResult, db::DbConn, error::MapResult, CONFIG};
|
||||
|
||||
// https://bitwarden.com/help/event-logs/
|
||||
|
||||
@@ -13,22 +13,23 @@ db_object! {
|
||||
// Upstream SQL: https://github.com/bitwarden/server/blob/8a22c0479e987e756ce7412c48a732f9002f0a2d/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: String,
|
||||
pub uuid: EventId,
|
||||
pub event_type: i32, // EventType
|
||||
pub user_uuid: Option<String>,
|
||||
pub org_uuid: Option<String>,
|
||||
pub cipher_uuid: Option<String>,
|
||||
pub collection_uuid: Option<String>,
|
||||
pub group_uuid: Option<String>,
|
||||
pub org_user_uuid: Option<String>,
|
||||
pub act_user_uuid: Option<String>,
|
||||
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/8a22c0479e987e756ce7412c48a732f9002f0a2d/src/Core/Enums/DeviceType.cs
|
||||
pub device_type: Option<i32>,
|
||||
pub ip_address: Option<String>,
|
||||
pub event_date: NaiveDateTime,
|
||||
pub policy_uuid: Option<String>,
|
||||
pub policy_uuid: Option<OrgPolicyId>,
|
||||
pub provider_uuid: Option<String>,
|
||||
pub provider_user_uuid: Option<String>,
|
||||
pub provider_org_uuid: Option<String>,
|
||||
@@ -49,6 +50,8 @@ pub enum EventType {
|
||||
UserClientExportedVault = 1007,
|
||||
// UserUpdatedTempPassword = 1008, // Not supported
|
||||
// UserMigratedKeyToKeyConnector = 1009, // Not supported
|
||||
UserRequestedDeviceApproval = 1010,
|
||||
// UserTdeOffboardingPasswordSet = 1011, // Not supported
|
||||
|
||||
// Cipher
|
||||
CipherCreated = 1100,
|
||||
@@ -69,6 +72,7 @@ pub enum EventType {
|
||||
CipherSoftDeleted = 1115,
|
||||
CipherRestored = 1116,
|
||||
CipherClientToggledCardNumberVisible = 1117,
|
||||
CipherClientToggledTOTPSeedVisible = 1118,
|
||||
|
||||
// Collection
|
||||
CollectionCreated = 1300,
|
||||
@@ -94,6 +98,10 @@ pub enum EventType {
|
||||
// OrganizationUserFirstSsoLogin = 1510, // Not supported
|
||||
OrganizationUserRevoked = 1511,
|
||||
OrganizationUserRestored = 1512,
|
||||
OrganizationUserApprovedAuthRequest = 1513,
|
||||
OrganizationUserRejectedAuthRequest = 1514,
|
||||
OrganizationUserDeleted = 1515,
|
||||
OrganizationUserLeft = 1516,
|
||||
|
||||
// Organization
|
||||
OrganizationUpdated = 1600,
|
||||
@@ -105,6 +113,7 @@ pub enum EventType {
|
||||
// OrganizationEnabledKeyConnector = 1606, // Not supported
|
||||
// OrganizationDisabledKeyConnector = 1607, // Not supported
|
||||
// OrganizationSponsorshipsSynced = 1608, // Not supported
|
||||
// OrganizationCollectionManagementUpdated = 1609, // Not supported
|
||||
|
||||
// Policy
|
||||
PolicyUpdated = 1700,
|
||||
@@ -117,6 +126,13 @@ pub enum EventType {
|
||||
// ProviderOrganizationAdded = 1901, // Not supported
|
||||
// ProviderOrganizationRemoved = 1902, // Not supported
|
||||
// ProviderOrganizationVaultAccessed = 1903, // Not supported
|
||||
|
||||
// OrganizationDomainAdded = 2000, // Not supported
|
||||
// OrganizationDomainRemoved = 2001, // Not supported
|
||||
// OrganizationDomainVerified = 2002, // Not supported
|
||||
// OrganizationDomainNotVerified = 2003, // Not supported
|
||||
|
||||
// SecretRetrieved = 2100, // Not supported
|
||||
}
|
||||
|
||||
/// Local methods
|
||||
@@ -128,7 +144,7 @@ impl Event {
|
||||
};
|
||||
|
||||
Self {
|
||||
uuid: crate::util::get_uuid(),
|
||||
uuid: EventId(crate::util::get_uuid()),
|
||||
event_type,
|
||||
user_uuid: None,
|
||||
org_uuid: None,
|
||||
@@ -246,7 +262,7 @@ impl Event {
|
||||
/// ##############
|
||||
/// Custom Queries
|
||||
pub async fn find_by_organization_uuid(
|
||||
org_uuid: &str,
|
||||
org_uuid: &OrganizationId,
|
||||
start: &NaiveDateTime,
|
||||
end: &NaiveDateTime,
|
||||
conn: &mut DbConn,
|
||||
@@ -263,7 +279,7 @@ impl Event {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn count_by_org(org_uuid: &str, conn: &mut DbConn) -> i64 {
|
||||
pub async fn count_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> i64 {
|
||||
db_run! { conn: {
|
||||
event::table
|
||||
.filter(event::org_uuid.eq(org_uuid))
|
||||
@@ -274,16 +290,16 @@ impl Event {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn find_by_org_and_user_org(
|
||||
org_uuid: &str,
|
||||
user_org_uuid: &str,
|
||||
pub async fn find_by_org_and_member(
|
||||
org_uuid: &OrganizationId,
|
||||
member_uuid: &MembershipId,
|
||||
start: &NaiveDateTime,
|
||||
end: &NaiveDateTime,
|
||||
conn: &mut DbConn,
|
||||
) -> Vec<Self> {
|
||||
db_run! { conn: {
|
||||
event::table
|
||||
.inner_join(users_organizations::table.on(users_organizations::uuid.eq(user_org_uuid)))
|
||||
.inner_join(users_organizations::table.on(users_organizations::uuid.eq(member_uuid)))
|
||||
.filter(event::org_uuid.eq(org_uuid))
|
||||
.filter(event::event_date.between(start, end))
|
||||
.filter(event::user_uuid.eq(users_organizations::user_uuid.nullable()).or(event::act_user_uuid.eq(users_organizations::user_uuid.nullable())))
|
||||
@@ -297,7 +313,7 @@ impl Event {
|
||||
}
|
||||
|
||||
pub async fn find_by_cipher_uuid(
|
||||
cipher_uuid: &str,
|
||||
cipher_uuid: &CipherId,
|
||||
start: &NaiveDateTime,
|
||||
end: &NaiveDateTime,
|
||||
conn: &mut DbConn,
|
||||
@@ -327,3 +343,6 @@ impl Event {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, DieselNewType, FromForm, Hash, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct EventId(String);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use super::User;
|
||||
use super::{CipherId, User, UserId};
|
||||
|
||||
db_object! {
|
||||
#[derive(Identifiable, Queryable, Insertable)]
|
||||
#[diesel(table_name = favorites)]
|
||||
#[diesel(primary_key(user_uuid, cipher_uuid))]
|
||||
pub struct Favorite {
|
||||
pub user_uuid: String,
|
||||
pub cipher_uuid: String,
|
||||
pub user_uuid: UserId,
|
||||
pub cipher_uuid: CipherId,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ 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: &str, user_uuid: &str, conn: &mut DbConn) -> bool {
|
||||
pub async fn is_favorite(cipher_uuid: &CipherId, user_uuid: &UserId, conn: &mut DbConn) -> bool {
|
||||
db_run! { conn: {
|
||||
let query = favorites::table
|
||||
.filter(favorites::cipher_uuid.eq(cipher_uuid))
|
||||
@@ -29,7 +29,12 @@ impl Favorite {
|
||||
}
|
||||
|
||||
// Sets whether the specified cipher is a favorite of the specified user.
|
||||
pub async fn set_favorite(favorite: bool, cipher_uuid: &str, user_uuid: &str, conn: &mut DbConn) -> EmptyResult {
|
||||
pub async fn set_favorite(
|
||||
favorite: bool,
|
||||
cipher_uuid: &CipherId,
|
||||
user_uuid: &UserId,
|
||||
conn: &mut DbConn,
|
||||
) -> EmptyResult {
|
||||
let (old, new) = (Self::is_favorite(cipher_uuid, user_uuid, conn).await, favorite);
|
||||
match (old, new) {
|
||||
(false, true) => {
|
||||
@@ -62,7 +67,7 @@ impl Favorite {
|
||||
}
|
||||
|
||||
// Delete all favorite entries associated with the specified cipher.
|
||||
pub async fn delete_all_by_cipher(cipher_uuid: &str, conn: &mut DbConn) -> EmptyResult {
|
||||
pub async fn delete_all_by_cipher(cipher_uuid: &CipherId, conn: &mut DbConn) -> EmptyResult {
|
||||
db_run! { conn: {
|
||||
diesel::delete(favorites::table.filter(favorites::cipher_uuid.eq(cipher_uuid)))
|
||||
.execute(conn)
|
||||
@@ -71,7 +76,7 @@ impl Favorite {
|
||||
}
|
||||
|
||||
// Delete all favorite entries associated with the specified user.
|
||||
pub async fn delete_all_by_user(user_uuid: &str, conn: &mut DbConn) -> EmptyResult {
|
||||
pub async fn delete_all_by_user(user_uuid: &UserId, conn: &mut DbConn) -> EmptyResult {
|
||||
db_run! { conn: {
|
||||
diesel::delete(favorites::table.filter(favorites::user_uuid.eq(user_uuid)))
|
||||
.execute(conn)
|
||||
@@ -81,12 +86,12 @@ 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: &str, conn: &mut DbConn) -> Vec<String> {
|
||||
pub async fn get_all_cipher_uuid_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<CipherId> {
|
||||
db_run! { conn: {
|
||||
favorites::table
|
||||
.filter(favorites::user_uuid.eq(user_uuid))
|
||||
.select(favorites::cipher_uuid)
|
||||
.load::<String>(conn)
|
||||
.load::<CipherId>(conn)
|
||||
.unwrap_or_default()
|
||||
}}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
use chrono::{NaiveDateTime, Utc};
|
||||
use derive_more::{AsRef, Deref, Display, From};
|
||||
use serde_json::Value;
|
||||
|
||||
use super::User;
|
||||
use super::{CipherId, User, UserId};
|
||||
use macros::UuidFromParam;
|
||||
|
||||
db_object! {
|
||||
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
|
||||
#[diesel(table_name = folders)]
|
||||
#[diesel(primary_key(uuid))]
|
||||
pub struct Folder {
|
||||
pub uuid: String,
|
||||
pub uuid: FolderId,
|
||||
pub created_at: NaiveDateTime,
|
||||
pub updated_at: NaiveDateTime,
|
||||
pub user_uuid: String,
|
||||
pub user_uuid: UserId,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
@@ -19,18 +21,18 @@ db_object! {
|
||||
#[diesel(table_name = folders_ciphers)]
|
||||
#[diesel(primary_key(cipher_uuid, folder_uuid))]
|
||||
pub struct FolderCipher {
|
||||
pub cipher_uuid: String,
|
||||
pub folder_uuid: String,
|
||||
pub cipher_uuid: CipherId,
|
||||
pub folder_uuid: FolderId,
|
||||
}
|
||||
}
|
||||
|
||||
/// Local methods
|
||||
impl Folder {
|
||||
pub fn new(user_uuid: String, name: String) -> Self {
|
||||
pub fn new(user_uuid: UserId, name: String) -> Self {
|
||||
let now = Utc::now().naive_utc();
|
||||
|
||||
Self {
|
||||
uuid: crate::util::get_uuid(),
|
||||
uuid: FolderId(crate::util::get_uuid()),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
|
||||
@@ -52,10 +54,10 @@ impl Folder {
|
||||
}
|
||||
|
||||
impl FolderCipher {
|
||||
pub fn new(folder_uuid: &str, cipher_uuid: &str) -> Self {
|
||||
pub fn new(folder_uuid: FolderId, cipher_uuid: CipherId) -> Self {
|
||||
Self {
|
||||
folder_uuid: folder_uuid.to_string(),
|
||||
cipher_uuid: cipher_uuid.to_string(),
|
||||
folder_uuid,
|
||||
cipher_uuid,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,24 +115,25 @@ impl Folder {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn delete_all_by_user(user_uuid: &str, conn: &mut DbConn) -> EmptyResult {
|
||||
pub async fn delete_all_by_user(user_uuid: &UserId, conn: &mut DbConn) -> EmptyResult {
|
||||
for folder in Self::find_by_user(user_uuid, conn).await {
|
||||
folder.delete(conn).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn find_by_uuid(uuid: &str, conn: &mut DbConn) -> Option<Self> {
|
||||
pub async fn find_by_uuid_and_user(uuid: &FolderId, user_uuid: &UserId, conn: &mut DbConn) -> Option<Self> {
|
||||
db_run! { conn: {
|
||||
folders::table
|
||||
.filter(folders::uuid.eq(uuid))
|
||||
.filter(folders::user_uuid.eq(user_uuid))
|
||||
.first::<FolderDb>(conn)
|
||||
.ok()
|
||||
.from_db()
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn find_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
|
||||
pub async fn find_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
|
||||
db_run! { conn: {
|
||||
folders::table
|
||||
.filter(folders::user_uuid.eq(user_uuid))
|
||||
@@ -176,7 +179,7 @@ impl FolderCipher {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn delete_all_by_cipher(cipher_uuid: &str, conn: &mut DbConn) -> EmptyResult {
|
||||
pub async fn delete_all_by_cipher(cipher_uuid: &CipherId, conn: &mut DbConn) -> EmptyResult {
|
||||
db_run! { conn: {
|
||||
diesel::delete(folders_ciphers::table.filter(folders_ciphers::cipher_uuid.eq(cipher_uuid)))
|
||||
.execute(conn)
|
||||
@@ -184,7 +187,7 @@ impl FolderCipher {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn delete_all_by_folder(folder_uuid: &str, conn: &mut DbConn) -> EmptyResult {
|
||||
pub async fn delete_all_by_folder(folder_uuid: &FolderId, conn: &mut DbConn) -> EmptyResult {
|
||||
db_run! { conn: {
|
||||
diesel::delete(folders_ciphers::table.filter(folders_ciphers::folder_uuid.eq(folder_uuid)))
|
||||
.execute(conn)
|
||||
@@ -192,7 +195,11 @@ impl FolderCipher {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn find_by_folder_and_cipher(folder_uuid: &str, cipher_uuid: &str, conn: &mut DbConn) -> Option<Self> {
|
||||
pub async fn find_by_folder_and_cipher(
|
||||
folder_uuid: &FolderId,
|
||||
cipher_uuid: &CipherId,
|
||||
conn: &mut DbConn,
|
||||
) -> Option<Self> {
|
||||
db_run! { conn: {
|
||||
folders_ciphers::table
|
||||
.filter(folders_ciphers::folder_uuid.eq(folder_uuid))
|
||||
@@ -203,7 +210,7 @@ impl FolderCipher {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn find_by_folder(folder_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
|
||||
pub async fn find_by_folder(folder_uuid: &FolderId, conn: &mut DbConn) -> Vec<Self> {
|
||||
db_run! { conn: {
|
||||
folders_ciphers::table
|
||||
.filter(folders_ciphers::folder_uuid.eq(folder_uuid))
|
||||
@@ -215,14 +222,32 @@ impl FolderCipher {
|
||||
|
||||
/// 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: &str, conn: &mut DbConn) -> Vec<(String, String)> {
|
||||
pub async fn find_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<(CipherId, FolderId)> {
|
||||
db_run! { conn: {
|
||||
folders_ciphers::table
|
||||
.inner_join(folders::table)
|
||||
.filter(folders::user_uuid.eq(user_uuid))
|
||||
.select(folders_ciphers::all_columns)
|
||||
.load::<(String, String)>(conn)
|
||||
.load::<(CipherId, FolderId)>(conn)
|
||||
.unwrap_or_default()
|
||||
}}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone,
|
||||
Debug,
|
||||
AsRef,
|
||||
Deref,
|
||||
DieselNewType,
|
||||
Display,
|
||||
From,
|
||||
FromForm,
|
||||
Hash,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
UuidFromParam,
|
||||
)]
|
||||
pub struct FolderId(String);
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
use super::{User, UserOrganization};
|
||||
use super::{CollectionId, Membership, MembershipId, OrganizationId, User, UserId};
|
||||
use crate::api::EmptyResult;
|
||||
use crate::db::DbConn;
|
||||
use crate::error::MapResult;
|
||||
use chrono::{NaiveDateTime, Utc};
|
||||
use derive_more::{AsRef, Deref, Display, From};
|
||||
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: String,
|
||||
pub organizations_uuid: String,
|
||||
pub uuid: GroupId,
|
||||
pub organizations_uuid: OrganizationId,
|
||||
pub name: String,
|
||||
pub access_all: bool,
|
||||
pub external_id: Option<String>,
|
||||
@@ -23,28 +26,34 @@ db_object! {
|
||||
#[diesel(table_name = collections_groups)]
|
||||
#[diesel(primary_key(collections_uuid, groups_uuid))]
|
||||
pub struct CollectionGroup {
|
||||
pub collections_uuid: String,
|
||||
pub groups_uuid: String,
|
||||
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: String,
|
||||
pub users_organizations_uuid: String
|
||||
pub groups_uuid: GroupId,
|
||||
pub users_organizations_uuid: MembershipId
|
||||
}
|
||||
}
|
||||
|
||||
/// Local methods
|
||||
impl Group {
|
||||
pub fn new(organizations_uuid: String, name: String, access_all: bool, external_id: Option<String>) -> Self {
|
||||
pub fn new(
|
||||
organizations_uuid: OrganizationId,
|
||||
name: String,
|
||||
access_all: bool,
|
||||
external_id: Option<String>,
|
||||
) -> Self {
|
||||
let now = Utc::now().naive_utc();
|
||||
|
||||
let mut new_model = Self {
|
||||
uuid: crate::util::get_uuid(),
|
||||
uuid: GroupId(crate::util::get_uuid()),
|
||||
organizations_uuid,
|
||||
name,
|
||||
access_all,
|
||||
@@ -74,6 +83,9 @@ impl Group {
|
||||
}
|
||||
|
||||
pub async fn to_json_details(&self, conn: &mut 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
|
||||
let collections_groups: Vec<Value> = CollectionGroup::find_by_group(&self.uuid, conn)
|
||||
.await
|
||||
.iter()
|
||||
@@ -82,7 +94,7 @@ impl Group {
|
||||
"id": entry.collections_uuid,
|
||||
"readOnly": entry.read_only,
|
||||
"hidePasswords": entry.hide_passwords,
|
||||
"manage": false
|
||||
"manage": entry.manage,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
@@ -108,18 +120,38 @@ impl Group {
|
||||
}
|
||||
|
||||
impl CollectionGroup {
|
||||
pub fn new(collections_uuid: String, groups_uuid: String, read_only: bool, hide_passwords: bool) -> Self {
|
||||
pub fn new(
|
||||
collections_uuid: CollectionId,
|
||||
groups_uuid: GroupId,
|
||||
read_only: bool,
|
||||
hide_passwords: bool,
|
||||
manage: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
collections_uuid,
|
||||
groups_uuid,
|
||||
read_only,
|
||||
hide_passwords,
|
||||
manage,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_json_details_for_group(&self) -> 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
|
||||
// For backwards compaibility and migration proposes we keep checking read_only and hide_password
|
||||
json!({
|
||||
"id": self.groups_uuid,
|
||||
"readOnly": self.read_only,
|
||||
"hidePasswords": self.hide_passwords,
|
||||
"manage": self.manage || (!self.read_only && !self.hide_passwords),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl GroupUser {
|
||||
pub fn new(groups_uuid: String, users_organizations_uuid: String) -> Self {
|
||||
pub fn new(groups_uuid: GroupId, users_organizations_uuid: MembershipId) -> Self {
|
||||
Self {
|
||||
groups_uuid,
|
||||
users_organizations_uuid,
|
||||
@@ -163,27 +195,27 @@ impl Group {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_all_by_organization(org_uuid: &str, conn: &mut DbConn) -> EmptyResult {
|
||||
pub async fn delete_all_by_organization(org_uuid: &OrganizationId, conn: &mut DbConn) -> EmptyResult {
|
||||
for group in Self::find_by_organization(org_uuid, conn).await {
|
||||
group.delete(conn).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn find_by_organization(organizations_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
|
||||
pub async fn find_by_organization(org_uuid: &OrganizationId, conn: &mut DbConn) -> Vec<Self> {
|
||||
db_run! { conn: {
|
||||
groups::table
|
||||
.filter(groups::organizations_uuid.eq(organizations_uuid))
|
||||
.filter(groups::organizations_uuid.eq(org_uuid))
|
||||
.load::<GroupDb>(conn)
|
||||
.expect("Error loading groups")
|
||||
.from_db()
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn count_by_org(organizations_uuid: &str, conn: &mut DbConn) -> i64 {
|
||||
pub async fn count_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> i64 {
|
||||
db_run! { conn: {
|
||||
groups::table
|
||||
.filter(groups::organizations_uuid.eq(organizations_uuid))
|
||||
.filter(groups::organizations_uuid.eq(org_uuid))
|
||||
.count()
|
||||
.first::<i64>(conn)
|
||||
.ok()
|
||||
@@ -191,17 +223,22 @@ impl Group {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn find_by_uuid(uuid: &str, conn: &mut DbConn) -> Option<Self> {
|
||||
pub async fn find_by_uuid_and_org(uuid: &GroupId, org_uuid: &OrganizationId, conn: &mut DbConn) -> Option<Self> {
|
||||
db_run! { conn: {
|
||||
groups::table
|
||||
.filter(groups::uuid.eq(uuid))
|
||||
.filter(groups::organizations_uuid.eq(org_uuid))
|
||||
.first::<GroupDb>(conn)
|
||||
.ok()
|
||||
.from_db()
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn find_by_external_id_and_org(external_id: &str, org_uuid: &str, conn: &mut DbConn) -> Option<Self> {
|
||||
pub async fn find_by_external_id_and_org(
|
||||
external_id: &str,
|
||||
org_uuid: &OrganizationId,
|
||||
conn: &mut DbConn,
|
||||
) -> Option<Self> {
|
||||
db_run! { conn: {
|
||||
groups::table
|
||||
.filter(groups::external_id.eq(external_id))
|
||||
@@ -212,7 +249,7 @@ impl Group {
|
||||
}}
|
||||
}
|
||||
//Returns all organizations the user has full access to
|
||||
pub async fn gather_user_organizations_full_access(user_uuid: &str, conn: &mut DbConn) -> Vec<String> {
|
||||
pub async fn get_orgs_by_user_with_full_access(user_uuid: &UserId, conn: &mut DbConn) -> Vec<OrganizationId> {
|
||||
db_run! { conn: {
|
||||
groups_users::table
|
||||
.inner_join(users_organizations::table.on(
|
||||
@@ -225,12 +262,12 @@ impl Group {
|
||||
.filter(groups::access_all.eq(true))
|
||||
.select(groups::organizations_uuid)
|
||||
.distinct()
|
||||
.load::<String>(conn)
|
||||
.load::<OrganizationId>(conn)
|
||||
.expect("Error loading organization group full access information for user")
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn is_in_full_access_group(user_uuid: &str, org_uuid: &str, conn: &mut DbConn) -> bool {
|
||||
pub async fn is_in_full_access_group(user_uuid: &UserId, org_uuid: &OrganizationId, conn: &mut DbConn) -> bool {
|
||||
db_run! { conn: {
|
||||
groups::table
|
||||
.inner_join(groups_users::table.on(
|
||||
@@ -259,13 +296,13 @@ impl Group {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn update_revision(uuid: &str, conn: &mut DbConn) {
|
||||
pub async fn update_revision(uuid: &GroupId, conn: &mut DbConn) {
|
||||
if let Err(e) = Self::_update_revision(uuid, &Utc::now().naive_utc(), conn).await {
|
||||
warn!("Failed to update revision for {}: {:#?}", uuid, e);
|
||||
warn!("Failed to update revision for {uuid}: {e:#?}");
|
||||
}
|
||||
}
|
||||
|
||||
async fn _update_revision(uuid: &str, date: &NaiveDateTime, conn: &mut DbConn) -> EmptyResult {
|
||||
async fn _update_revision(uuid: &GroupId, date: &NaiveDateTime, conn: &mut DbConn) -> EmptyResult {
|
||||
db_run! {conn: {
|
||||
crate::util::retry(|| {
|
||||
diesel::update(groups::table.filter(groups::uuid.eq(uuid)))
|
||||
@@ -292,6 +329,7 @@ impl CollectionGroup {
|
||||
collections_groups::groups_uuid.eq(&self.groups_uuid),
|
||||
collections_groups::read_only.eq(&self.read_only),
|
||||
collections_groups::hide_passwords.eq(&self.hide_passwords),
|
||||
collections_groups::manage.eq(&self.manage),
|
||||
))
|
||||
.execute(conn)
|
||||
{
|
||||
@@ -306,6 +344,7 @@ impl CollectionGroup {
|
||||
collections_groups::groups_uuid.eq(&self.groups_uuid),
|
||||
collections_groups::read_only.eq(&self.read_only),
|
||||
collections_groups::hide_passwords.eq(&self.hide_passwords),
|
||||
collections_groups::manage.eq(&self.manage),
|
||||
))
|
||||
.execute(conn)
|
||||
.map_res("Error adding group to collection")
|
||||
@@ -320,12 +359,14 @@ impl CollectionGroup {
|
||||
collections_groups::groups_uuid.eq(&self.groups_uuid),
|
||||
collections_groups::read_only.eq(self.read_only),
|
||||
collections_groups::hide_passwords.eq(self.hide_passwords),
|
||||
collections_groups::manage.eq(self.manage),
|
||||
))
|
||||
.on_conflict((collections_groups::collections_uuid, collections_groups::groups_uuid))
|
||||
.do_update()
|
||||
.set((
|
||||
collections_groups::read_only.eq(self.read_only),
|
||||
collections_groups::hide_passwords.eq(self.hide_passwords),
|
||||
collections_groups::manage.eq(self.manage),
|
||||
))
|
||||
.execute(conn)
|
||||
.map_res("Error adding group to collection")
|
||||
@@ -333,7 +374,7 @@ impl CollectionGroup {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn find_by_group(group_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
|
||||
pub async fn find_by_group(group_uuid: &GroupId, conn: &mut DbConn) -> Vec<Self> {
|
||||
db_run! { conn: {
|
||||
collections_groups::table
|
||||
.filter(collections_groups::groups_uuid.eq(group_uuid))
|
||||
@@ -343,7 +384,7 @@ impl CollectionGroup {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn find_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
|
||||
pub async fn find_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
|
||||
db_run! { conn: {
|
||||
collections_groups::table
|
||||
.inner_join(groups_users::table.on(
|
||||
@@ -360,7 +401,7 @@ impl CollectionGroup {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn find_by_collection(collection_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
|
||||
pub async fn find_by_collection(collection_uuid: &CollectionId, conn: &mut DbConn) -> Vec<Self> {
|
||||
db_run! { conn: {
|
||||
collections_groups::table
|
||||
.filter(collections_groups::collections_uuid.eq(collection_uuid))
|
||||
@@ -386,7 +427,7 @@ impl CollectionGroup {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn delete_all_by_group(group_uuid: &str, conn: &mut DbConn) -> EmptyResult {
|
||||
pub async fn delete_all_by_group(group_uuid: &GroupId, conn: &mut 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;
|
||||
@@ -400,7 +441,7 @@ impl CollectionGroup {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn delete_all_by_collection(collection_uuid: &str, conn: &mut DbConn) -> EmptyResult {
|
||||
pub async fn delete_all_by_collection(collection_uuid: &CollectionId, conn: &mut 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;
|
||||
@@ -465,7 +506,7 @@ impl GroupUser {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn find_by_group(group_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
|
||||
pub async fn find_by_group(group_uuid: &GroupId, conn: &mut DbConn) -> Vec<Self> {
|
||||
db_run! { conn: {
|
||||
groups_users::table
|
||||
.filter(groups_users::groups_uuid.eq(group_uuid))
|
||||
@@ -475,10 +516,10 @@ impl GroupUser {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn find_by_user(users_organizations_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
|
||||
pub async fn find_by_member(member_uuid: &MembershipId, conn: &mut DbConn) -> Vec<Self> {
|
||||
db_run! { conn: {
|
||||
groups_users::table
|
||||
.filter(groups_users::users_organizations_uuid.eq(users_organizations_uuid))
|
||||
.filter(groups_users::users_organizations_uuid.eq(member_uuid))
|
||||
.load::<GroupUserDb>(conn)
|
||||
.expect("Error loading groups for user")
|
||||
.from_db()
|
||||
@@ -486,8 +527,8 @@ impl GroupUser {
|
||||
}
|
||||
|
||||
pub async fn has_access_to_collection_by_member(
|
||||
collection_uuid: &str,
|
||||
member_uuid: &str,
|
||||
collection_uuid: &CollectionId,
|
||||
member_uuid: &MembershipId,
|
||||
conn: &mut DbConn,
|
||||
) -> bool {
|
||||
db_run! { conn: {
|
||||
@@ -503,7 +544,11 @@ impl GroupUser {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn has_full_access_by_member(org_uuid: &str, member_uuid: &str, conn: &mut DbConn) -> bool {
|
||||
pub async fn has_full_access_by_member(
|
||||
org_uuid: &OrganizationId,
|
||||
member_uuid: &MembershipId,
|
||||
conn: &mut DbConn,
|
||||
) -> bool {
|
||||
db_run! { conn: {
|
||||
groups_users::table
|
||||
.inner_join(groups::table.on(
|
||||
@@ -519,32 +564,32 @@ impl GroupUser {
|
||||
}
|
||||
|
||||
pub async fn update_user_revision(&self, conn: &mut DbConn) {
|
||||
match UserOrganization::find_by_uuid(&self.users_organizations_uuid, conn).await {
|
||||
Some(user) => User::update_uuid_revision(&user.user_uuid, conn).await,
|
||||
None => warn!("User could not be found!"),
|
||||
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!"),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_by_group_id_and_user_id(
|
||||
group_uuid: &str,
|
||||
users_organizations_uuid: &str,
|
||||
pub async fn delete_by_group_and_member(
|
||||
group_uuid: &GroupId,
|
||||
member_uuid: &MembershipId,
|
||||
conn: &mut DbConn,
|
||||
) -> EmptyResult {
|
||||
match UserOrganization::find_by_uuid(users_organizations_uuid, conn).await {
|
||||
Some(user) => User::update_uuid_revision(&user.user_uuid, conn).await,
|
||||
None => warn!("User could not be found!"),
|
||||
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!"),
|
||||
};
|
||||
|
||||
db_run! { conn: {
|
||||
diesel::delete(groups_users::table)
|
||||
.filter(groups_users::groups_uuid.eq(group_uuid))
|
||||
.filter(groups_users::users_organizations_uuid.eq(users_organizations_uuid))
|
||||
.filter(groups_users::users_organizations_uuid.eq(member_uuid))
|
||||
.execute(conn)
|
||||
.map_res("Error deleting group users")
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn delete_all_by_group(group_uuid: &str, conn: &mut DbConn) -> EmptyResult {
|
||||
pub async fn delete_all_by_group(group_uuid: &GroupId, conn: &mut 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;
|
||||
@@ -558,17 +603,35 @@ impl GroupUser {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn delete_all_by_user(users_organizations_uuid: &str, conn: &mut DbConn) -> EmptyResult {
|
||||
match UserOrganization::find_by_uuid(users_organizations_uuid, conn).await {
|
||||
Some(user) => User::update_uuid_revision(&user.user_uuid, conn).await,
|
||||
None => warn!("User could not be found!"),
|
||||
pub async fn delete_all_by_member(member_uuid: &MembershipId, conn: &mut 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!"),
|
||||
}
|
||||
|
||||
db_run! { conn: {
|
||||
diesel::delete(groups_users::table)
|
||||
.filter(groups_users::users_organizations_uuid.eq(users_organizations_uuid))
|
||||
.filter(groups_users::users_organizations_uuid.eq(member_uuid))
|
||||
.execute(conn)
|
||||
.map_res("Error deleting user groups")
|
||||
}}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone,
|
||||
Debug,
|
||||
AsRef,
|
||||
Deref,
|
||||
DieselNewType,
|
||||
Display,
|
||||
From,
|
||||
FromForm,
|
||||
Hash,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
UuidFromParam,
|
||||
)]
|
||||
pub struct GroupId(String);
|
||||
|
||||
@@ -16,20 +16,26 @@ mod two_factor_duo_context;
|
||||
mod two_factor_incomplete;
|
||||
mod user;
|
||||
|
||||
pub use self::attachment::Attachment;
|
||||
pub use self::auth_request::AuthRequest;
|
||||
pub use self::cipher::{Cipher, RepromptType};
|
||||
pub use self::collection::{Collection, CollectionCipher, CollectionUser};
|
||||
pub use self::device::{Device, DeviceType};
|
||||
pub use self::emergency_access::{EmergencyAccess, EmergencyAccessStatus, EmergencyAccessType};
|
||||
pub use self::attachment::{Attachment, AttachmentId};
|
||||
pub use self::auth_request::{AuthRequest, AuthRequestId};
|
||||
pub use self::cipher::{Cipher, CipherId, RepromptType};
|
||||
pub use self::collection::{Collection, CollectionCipher, CollectionId, CollectionUser};
|
||||
pub use self::device::{Device, DeviceId, DeviceType};
|
||||
pub use self::emergency_access::{EmergencyAccess, EmergencyAccessId, EmergencyAccessStatus, EmergencyAccessType};
|
||||
pub use self::event::{Event, EventType};
|
||||
pub use self::favorite::Favorite;
|
||||
pub use self::folder::{Folder, FolderCipher};
|
||||
pub use self::group::{CollectionGroup, Group, GroupUser};
|
||||
pub use self::org_policy::{OrgPolicy, OrgPolicyErr, OrgPolicyType};
|
||||
pub use self::organization::{Organization, OrganizationApiKey, UserOrgStatus, UserOrgType, UserOrganization};
|
||||
pub use self::send::{Send, SendType};
|
||||
pub use self::folder::{Folder, FolderCipher, FolderId};
|
||||
pub use self::group::{CollectionGroup, Group, GroupId, GroupUser};
|
||||
pub use self::org_policy::{OrgPolicy, OrgPolicyErr, OrgPolicyId, OrgPolicyType};
|
||||
pub use self::organization::{
|
||||
Membership, MembershipId, MembershipStatus, MembershipType, OrgApiKeyId, Organization, OrganizationApiKey,
|
||||
OrganizationId,
|
||||
};
|
||||
pub use self::send::{
|
||||
id::{SendFileId, SendId},
|
||||
Send, SendType,
|
||||
};
|
||||
pub use self::two_factor::{TwoFactor, TwoFactorType};
|
||||
pub use self::two_factor_duo_context::TwoFactorDuoContext;
|
||||
pub use self::two_factor_incomplete::TwoFactorIncomplete;
|
||||
pub use self::user::{Invitation, User, UserKdfType, UserStampException};
|
||||
pub use self::user::{Invitation, User, UserId, UserKdfType, UserStampException};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use derive_more::{AsRef, From};
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
|
||||
@@ -5,22 +6,22 @@ use crate::api::EmptyResult;
|
||||
use crate::db::DbConn;
|
||||
use crate::error::MapResult;
|
||||
|
||||
use super::{TwoFactor, UserOrgStatus, UserOrgType, UserOrganization};
|
||||
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: String,
|
||||
pub org_uuid: String,
|
||||
pub uuid: OrgPolicyId,
|
||||
pub org_uuid: OrganizationId,
|
||||
pub atype: i32,
|
||||
pub enabled: bool,
|
||||
pub data: String,
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/bitwarden/server/blob/b86a04cef9f1e1b82cf18e49fc94e017c641130c/src/Core/Enums/PolicyType.cs
|
||||
// https://github.com/bitwarden/server/blob/abfdf6f5cb0f1f1504dbaaaa0e04ce9cb60faf19/src/Core/AdminConsole/Enums/PolicyType.cs
|
||||
#[derive(Copy, Clone, Eq, PartialEq, num_derive::FromPrimitive)]
|
||||
pub enum OrgPolicyType {
|
||||
TwoFactorAuthentication = 0,
|
||||
@@ -34,6 +35,10 @@ pub enum OrgPolicyType {
|
||||
ResetPassword = 8,
|
||||
// MaximumVaultTimeout = 9, // Not supported (Not AGPLv3 Licensed)
|
||||
// DisablePersonalVaultExport = 10, // Not supported (Not AGPLv3 Licensed)
|
||||
// ActivateAutofill = 11,
|
||||
// AutomaticAppLogIn = 12,
|
||||
// FreeFamiliesSponsorshipPolicy = 13,
|
||||
RemoveUnlockWithPin = 14,
|
||||
}
|
||||
|
||||
// https://github.com/bitwarden/server/blob/5cbdee137921a19b1f722920f0fa3cd45af2ef0f/src/Core/Models/Data/Organizations/Policies/SendOptionsPolicyData.cs
|
||||
@@ -62,9 +67,9 @@ pub enum OrgPolicyErr {
|
||||
|
||||
/// Local methods
|
||||
impl OrgPolicy {
|
||||
pub fn new(org_uuid: String, atype: OrgPolicyType, data: String) -> Self {
|
||||
pub fn new(org_uuid: OrganizationId, atype: OrgPolicyType, data: String) -> Self {
|
||||
Self {
|
||||
uuid: crate::util::get_uuid(),
|
||||
uuid: OrgPolicyId(crate::util::get_uuid()),
|
||||
org_uuid,
|
||||
atype: atype as i32,
|
||||
enabled: false,
|
||||
@@ -142,17 +147,7 @@ impl OrgPolicy {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn find_by_uuid(uuid: &str, conn: &mut DbConn) -> Option<Self> {
|
||||
db_run! { conn: {
|
||||
org_policies::table
|
||||
.filter(org_policies::uuid.eq(uuid))
|
||||
.first::<OrgPolicyDb>(conn)
|
||||
.ok()
|
||||
.from_db()
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn find_by_org(org_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
|
||||
pub async fn find_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> Vec<Self> {
|
||||
db_run! { conn: {
|
||||
org_policies::table
|
||||
.filter(org_policies::org_uuid.eq(org_uuid))
|
||||
@@ -162,7 +157,7 @@ impl OrgPolicy {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn find_confirmed_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
|
||||
pub async fn find_confirmed_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
|
||||
db_run! { conn: {
|
||||
org_policies::table
|
||||
.inner_join(
|
||||
@@ -171,7 +166,7 @@ impl OrgPolicy {
|
||||
.and(users_organizations::user_uuid.eq(user_uuid)))
|
||||
)
|
||||
.filter(
|
||||
users_organizations::status.eq(UserOrgStatus::Confirmed as i32)
|
||||
users_organizations::status.eq(MembershipStatus::Confirmed as i32)
|
||||
)
|
||||
.select(org_policies::all_columns)
|
||||
.load::<OrgPolicyDb>(conn)
|
||||
@@ -180,7 +175,11 @@ impl OrgPolicy {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn find_by_org_and_type(org_uuid: &str, policy_type: OrgPolicyType, conn: &mut DbConn) -> Option<Self> {
|
||||
pub async fn find_by_org_and_type(
|
||||
org_uuid: &OrganizationId,
|
||||
policy_type: OrgPolicyType,
|
||||
conn: &mut DbConn,
|
||||
) -> Option<Self> {
|
||||
db_run! { conn: {
|
||||
org_policies::table
|
||||
.filter(org_policies::org_uuid.eq(org_uuid))
|
||||
@@ -191,7 +190,7 @@ impl OrgPolicy {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn delete_all_by_organization(org_uuid: &str, conn: &mut DbConn) -> EmptyResult {
|
||||
pub async fn delete_all_by_organization(org_uuid: &OrganizationId, conn: &mut DbConn) -> EmptyResult {
|
||||
db_run! { conn: {
|
||||
diesel::delete(org_policies::table.filter(org_policies::org_uuid.eq(org_uuid)))
|
||||
.execute(conn)
|
||||
@@ -200,7 +199,7 @@ impl OrgPolicy {
|
||||
}
|
||||
|
||||
pub async fn find_accepted_and_confirmed_by_user_and_active_policy(
|
||||
user_uuid: &str,
|
||||
user_uuid: &UserId,
|
||||
policy_type: OrgPolicyType,
|
||||
conn: &mut DbConn,
|
||||
) -> Vec<Self> {
|
||||
@@ -212,10 +211,10 @@ impl OrgPolicy {
|
||||
.and(users_organizations::user_uuid.eq(user_uuid)))
|
||||
)
|
||||
.filter(
|
||||
users_organizations::status.eq(UserOrgStatus::Accepted as i32)
|
||||
users_organizations::status.eq(MembershipStatus::Accepted as i32)
|
||||
)
|
||||
.or_filter(
|
||||
users_organizations::status.eq(UserOrgStatus::Confirmed as i32)
|
||||
users_organizations::status.eq(MembershipStatus::Confirmed as i32)
|
||||
)
|
||||
.filter(org_policies::atype.eq(policy_type as i32))
|
||||
.filter(org_policies::enabled.eq(true))
|
||||
@@ -227,7 +226,7 @@ impl OrgPolicy {
|
||||
}
|
||||
|
||||
pub async fn find_confirmed_by_user_and_active_policy(
|
||||
user_uuid: &str,
|
||||
user_uuid: &UserId,
|
||||
policy_type: OrgPolicyType,
|
||||
conn: &mut DbConn,
|
||||
) -> Vec<Self> {
|
||||
@@ -239,7 +238,7 @@ impl OrgPolicy {
|
||||
.and(users_organizations::user_uuid.eq(user_uuid)))
|
||||
)
|
||||
.filter(
|
||||
users_organizations::status.eq(UserOrgStatus::Confirmed as i32)
|
||||
users_organizations::status.eq(MembershipStatus::Confirmed as i32)
|
||||
)
|
||||
.filter(org_policies::atype.eq(policy_type as i32))
|
||||
.filter(org_policies::enabled.eq(true))
|
||||
@@ -254,21 +253,21 @@ impl OrgPolicy {
|
||||
/// and the user is not an owner or admin of that org. This is only useful for checking
|
||||
/// applicability of policy types that have these particular semantics.
|
||||
pub async fn is_applicable_to_user(
|
||||
user_uuid: &str,
|
||||
user_uuid: &UserId,
|
||||
policy_type: OrgPolicyType,
|
||||
exclude_org_uuid: Option<&str>,
|
||||
exclude_org_uuid: Option<&OrganizationId>,
|
||||
conn: &mut DbConn,
|
||||
) -> bool {
|
||||
for policy in
|
||||
OrgPolicy::find_accepted_and_confirmed_by_user_and_active_policy(user_uuid, policy_type, conn).await
|
||||
{
|
||||
// Check if we need to skip this organization.
|
||||
if exclude_org_uuid.is_some() && exclude_org_uuid.unwrap() == policy.org_uuid {
|
||||
if exclude_org_uuid.is_some() && *exclude_org_uuid.unwrap() == policy.org_uuid {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(user) = UserOrganization::find_by_user_and_org(user_uuid, &policy.org_uuid, conn).await {
|
||||
if user.atype < UserOrgType::Admin {
|
||||
if let Some(user) = Membership::find_by_user_and_org(user_uuid, &policy.org_uuid, conn).await {
|
||||
if user.atype < MembershipType::Admin {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -277,8 +276,8 @@ impl OrgPolicy {
|
||||
}
|
||||
|
||||
pub async fn is_user_allowed(
|
||||
user_uuid: &str,
|
||||
org_uuid: &str,
|
||||
user_uuid: &UserId,
|
||||
org_uuid: &OrganizationId,
|
||||
exclude_current_org: bool,
|
||||
conn: &mut DbConn,
|
||||
) -> OrgPolicyResult {
|
||||
@@ -306,7 +305,7 @@ impl OrgPolicy {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn org_is_reset_password_auto_enroll(org_uuid: &str, conn: &mut DbConn) -> bool {
|
||||
pub async fn org_is_reset_password_auto_enroll(org_uuid: &OrganizationId, conn: &mut 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) => {
|
||||
@@ -322,12 +321,12 @@ 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: &str, conn: &mut DbConn) -> bool {
|
||||
pub async fn is_hide_email_disabled(user_uuid: &UserId, conn: &mut DbConn) -> bool {
|
||||
for policy in
|
||||
OrgPolicy::find_confirmed_by_user_and_active_policy(user_uuid, OrgPolicyType::SendOptions, conn).await
|
||||
{
|
||||
if let Some(user) = UserOrganization::find_by_user_and_org(user_uuid, &policy.org_uuid, conn).await {
|
||||
if user.atype < UserOrgType::Admin {
|
||||
if let Some(user) = Membership::find_by_user_and_org(user_uuid, &policy.org_uuid, conn).await {
|
||||
if user.atype < MembershipType::Admin {
|
||||
match serde_json::from_str::<SendOptionsPolicyData>(&policy.data) {
|
||||
Ok(opts) => {
|
||||
if opts.disable_hide_email {
|
||||
@@ -342,12 +341,19 @@ impl OrgPolicy {
|
||||
false
|
||||
}
|
||||
|
||||
pub async fn is_enabled_for_member(org_user_uuid: &str, policy_type: OrgPolicyType, conn: &mut DbConn) -> bool {
|
||||
if let Some(membership) = UserOrganization::find_by_uuid(org_user_uuid, conn).await {
|
||||
if let Some(policy) = OrgPolicy::find_by_org_and_type(&membership.org_uuid, policy_type, conn).await {
|
||||
pub async fn is_enabled_for_member(
|
||||
member_uuid: &MembershipId,
|
||||
policy_type: OrgPolicyType,
|
||||
conn: &mut 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;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, AsRef, DieselNewType, From, FromForm, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct OrgPolicyId(String);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,8 @@ use serde_json::Value;
|
||||
|
||||
use crate::util::LowerCase;
|
||||
|
||||
use super::User;
|
||||
use super::{OrganizationId, User, UserId};
|
||||
use id::SendId;
|
||||
|
||||
db_object! {
|
||||
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
|
||||
@@ -11,11 +12,10 @@ db_object! {
|
||||
#[diesel(treat_none_as_null = true)]
|
||||
#[diesel(primary_key(uuid))]
|
||||
pub struct Send {
|
||||
pub uuid: String,
|
||||
|
||||
pub user_uuid: Option<String>,
|
||||
pub organization_uuid: Option<String>,
|
||||
pub uuid: SendId,
|
||||
|
||||
pub user_uuid: Option<UserId>,
|
||||
pub organization_uuid: Option<OrganizationId>,
|
||||
|
||||
pub name: String,
|
||||
pub notes: Option<String>,
|
||||
@@ -51,7 +51,7 @@ impl Send {
|
||||
let now = Utc::now().naive_utc();
|
||||
|
||||
Self {
|
||||
uuid: crate::util::get_uuid(),
|
||||
uuid: SendId::from(crate::util::get_uuid()),
|
||||
user_uuid: None,
|
||||
organization_uuid: None,
|
||||
|
||||
@@ -243,7 +243,7 @@ impl Send {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_users_revision(&self, conn: &mut DbConn) -> Vec<String> {
|
||||
pub async fn update_users_revision(&self, conn: &mut DbConn) -> Vec<UserId> {
|
||||
let mut user_uuids = Vec::new();
|
||||
match &self.user_uuid {
|
||||
Some(user_uuid) => {
|
||||
@@ -257,7 +257,7 @@ impl Send {
|
||||
user_uuids
|
||||
}
|
||||
|
||||
pub async fn delete_all_by_user(user_uuid: &str, conn: &mut DbConn) -> EmptyResult {
|
||||
pub async fn delete_all_by_user(user_uuid: &UserId, conn: &mut DbConn) -> EmptyResult {
|
||||
for send in Self::find_by_user(user_uuid, conn).await {
|
||||
send.delete(conn).await?;
|
||||
}
|
||||
@@ -268,20 +268,19 @@ impl Send {
|
||||
use data_encoding::BASE64URL_NOPAD;
|
||||
use uuid::Uuid;
|
||||
|
||||
let uuid_vec = match BASE64URL_NOPAD.decode(access_id.as_bytes()) {
|
||||
Ok(v) => v,
|
||||
Err(_) => return None,
|
||||
let Ok(uuid_vec) = BASE64URL_NOPAD.decode(access_id.as_bytes()) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let uuid = match Uuid::from_slice(&uuid_vec) {
|
||||
Ok(u) => u.to_string(),
|
||||
Ok(u) => SendId::from(u.to_string()),
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
Self::find_by_uuid(&uuid, conn).await
|
||||
}
|
||||
|
||||
pub async fn find_by_uuid(uuid: &str, conn: &mut DbConn) -> Option<Self> {
|
||||
pub async fn find_by_uuid(uuid: &SendId, conn: &mut DbConn) -> Option<Self> {
|
||||
db_run! {conn: {
|
||||
sends::table
|
||||
.filter(sends::uuid.eq(uuid))
|
||||
@@ -291,7 +290,18 @@ impl Send {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn find_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
|
||||
pub async fn find_by_uuid_and_user(uuid: &SendId, user_uuid: &UserId, conn: &mut DbConn) -> Option<Self> {
|
||||
db_run! {conn: {
|
||||
sends::table
|
||||
.filter(sends::uuid.eq(uuid))
|
||||
.filter(sends::user_uuid.eq(user_uuid))
|
||||
.first::<SendDb>(conn)
|
||||
.ok()
|
||||
.from_db()
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn find_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
|
||||
db_run! {conn: {
|
||||
sends::table
|
||||
.filter(sends::user_uuid.eq(user_uuid))
|
||||
@@ -299,7 +309,7 @@ impl Send {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn size_by_user(user_uuid: &str, conn: &mut DbConn) -> Option<i64> {
|
||||
pub async fn size_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Option<i64> {
|
||||
let sends = Self::find_by_user(user_uuid, conn).await;
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
@@ -322,7 +332,7 @@ impl Send {
|
||||
Some(total)
|
||||
}
|
||||
|
||||
pub async fn find_by_org(org_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
|
||||
pub async fn find_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> Vec<Self> {
|
||||
db_run! {conn: {
|
||||
sends::table
|
||||
.filter(sends::organization_uuid.eq(org_uuid))
|
||||
@@ -339,3 +349,48 @@ impl Send {
|
||||
}}
|
||||
}
|
||||
}
|
||||
|
||||
// separate namespace to avoid name collision with std::marker::Send
|
||||
pub mod id {
|
||||
use derive_more::{AsRef, Deref, Display, From};
|
||||
use macros::{IdFromParam, UuidFromParam};
|
||||
use std::marker::Send;
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(
|
||||
Clone,
|
||||
Debug,
|
||||
AsRef,
|
||||
Deref,
|
||||
DieselNewType,
|
||||
Display,
|
||||
From,
|
||||
FromForm,
|
||||
Hash,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
UuidFromParam,
|
||||
)]
|
||||
pub struct SendId(String);
|
||||
|
||||
impl AsRef<Path> for SendId {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &Path {
|
||||
Path::new(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Debug, AsRef, Deref, Display, From, FromForm, Hash, PartialEq, Eq, Serialize, Deserialize, IdFromParam,
|
||||
)]
|
||||
pub struct SendFileId(String);
|
||||
|
||||
impl AsRef<Path> for SendFileId {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &Path {
|
||||
Path::new(&self.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use serde_json::Value;
|
||||
|
||||
use super::UserId;
|
||||
use crate::{api::EmptyResult, db::DbConn, error::MapResult};
|
||||
|
||||
db_object! {
|
||||
@@ -7,8 +8,8 @@ db_object! {
|
||||
#[diesel(table_name = twofactor)]
|
||||
#[diesel(primary_key(uuid))]
|
||||
pub struct TwoFactor {
|
||||
pub uuid: String,
|
||||
pub user_uuid: String,
|
||||
pub uuid: TwoFactorId,
|
||||
pub user_uuid: UserId,
|
||||
pub atype: i32,
|
||||
pub enabled: bool,
|
||||
pub data: String,
|
||||
@@ -41,9 +42,9 @@ pub enum TwoFactorType {
|
||||
|
||||
/// Local methods
|
||||
impl TwoFactor {
|
||||
pub fn new(user_uuid: String, atype: TwoFactorType, data: String) -> Self {
|
||||
pub fn new(user_uuid: UserId, atype: TwoFactorType, data: String) -> Self {
|
||||
Self {
|
||||
uuid: crate::util::get_uuid(),
|
||||
uuid: TwoFactorId(crate::util::get_uuid()),
|
||||
user_uuid,
|
||||
atype: atype as i32,
|
||||
enabled: true,
|
||||
@@ -118,7 +119,7 @@ impl TwoFactor {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn find_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
|
||||
pub async fn find_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
|
||||
db_run! { conn: {
|
||||
twofactor::table
|
||||
.filter(twofactor::user_uuid.eq(user_uuid))
|
||||
@@ -129,7 +130,7 @@ impl TwoFactor {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn find_by_user_and_type(user_uuid: &str, atype: i32, conn: &mut DbConn) -> Option<Self> {
|
||||
pub async fn find_by_user_and_type(user_uuid: &UserId, atype: i32, conn: &mut DbConn) -> Option<Self> {
|
||||
db_run! { conn: {
|
||||
twofactor::table
|
||||
.filter(twofactor::user_uuid.eq(user_uuid))
|
||||
@@ -140,7 +141,7 @@ impl TwoFactor {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn delete_all_by_user(user_uuid: &str, conn: &mut DbConn) -> EmptyResult {
|
||||
pub async fn delete_all_by_user(user_uuid: &UserId, conn: &mut DbConn) -> EmptyResult {
|
||||
db_run! { conn: {
|
||||
diesel::delete(twofactor::table.filter(twofactor::user_uuid.eq(user_uuid)))
|
||||
.execute(conn)
|
||||
@@ -217,3 +218,6 @@ impl TwoFactor {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, DieselNewType, FromForm, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct TwoFactorId(String);
|
||||
|
||||
@@ -1,17 +1,26 @@
|
||||
use chrono::{NaiveDateTime, Utc};
|
||||
|
||||
use crate::{api::EmptyResult, auth::ClientIp, db::DbConn, error::MapResult, CONFIG};
|
||||
use crate::{
|
||||
api::EmptyResult,
|
||||
auth::ClientIp,
|
||||
db::{
|
||||
models::{DeviceId, UserId},
|
||||
DbConn,
|
||||
},
|
||||
error::MapResult,
|
||||
CONFIG,
|
||||
};
|
||||
|
||||
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: String,
|
||||
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: String,
|
||||
pub device_uuid: DeviceId,
|
||||
pub device_name: String,
|
||||
pub device_type: i32,
|
||||
pub login_time: NaiveDateTime,
|
||||
@@ -21,8 +30,8 @@ db_object! {
|
||||
|
||||
impl TwoFactorIncomplete {
|
||||
pub async fn mark_incomplete(
|
||||
user_uuid: &str,
|
||||
device_uuid: &str,
|
||||
user_uuid: &UserId,
|
||||
device_uuid: &DeviceId,
|
||||
device_name: &str,
|
||||
device_type: i32,
|
||||
ip: &ClientIp,
|
||||
@@ -55,7 +64,7 @@ impl TwoFactorIncomplete {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn mark_complete(user_uuid: &str, device_uuid: &str, conn: &mut DbConn) -> EmptyResult {
|
||||
pub async fn mark_complete(user_uuid: &UserId, device_uuid: &DeviceId, conn: &mut DbConn) -> EmptyResult {
|
||||
if CONFIG.incomplete_2fa_time_limit() <= 0 || !CONFIG.mail_enabled() {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -63,7 +72,11 @@ impl TwoFactorIncomplete {
|
||||
Self::delete_by_user_and_device(user_uuid, device_uuid, conn).await
|
||||
}
|
||||
|
||||
pub async fn find_by_user_and_device(user_uuid: &str, device_uuid: &str, conn: &mut DbConn) -> Option<Self> {
|
||||
pub async fn find_by_user_and_device(
|
||||
user_uuid: &UserId,
|
||||
device_uuid: &DeviceId,
|
||||
conn: &mut DbConn,
|
||||
) -> Option<Self> {
|
||||
db_run! { conn: {
|
||||
twofactor_incomplete::table
|
||||
.filter(twofactor_incomplete::user_uuid.eq(user_uuid))
|
||||
@@ -88,7 +101,11 @@ impl TwoFactorIncomplete {
|
||||
Self::delete_by_user_and_device(&self.user_uuid, &self.device_uuid, conn).await
|
||||
}
|
||||
|
||||
pub async fn delete_by_user_and_device(user_uuid: &str, device_uuid: &str, conn: &mut DbConn) -> EmptyResult {
|
||||
pub async fn delete_by_user_and_device(
|
||||
user_uuid: &UserId,
|
||||
device_uuid: &DeviceId,
|
||||
conn: &mut DbConn,
|
||||
) -> EmptyResult {
|
||||
db_run! { conn: {
|
||||
diesel::delete(twofactor_incomplete::table
|
||||
.filter(twofactor_incomplete::user_uuid.eq(user_uuid))
|
||||
@@ -98,7 +115,7 @@ impl TwoFactorIncomplete {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn delete_all_by_user(user_uuid: &str, conn: &mut DbConn) -> EmptyResult {
|
||||
pub async fn delete_all_by_user(user_uuid: &UserId, conn: &mut DbConn) -> EmptyResult {
|
||||
db_run! { conn: {
|
||||
diesel::delete(twofactor_incomplete::table.filter(twofactor_incomplete::user_uuid.eq(user_uuid)))
|
||||
.execute(conn)
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
use crate::util::{format_date, get_uuid, retry};
|
||||
use chrono::{NaiveDateTime, TimeDelta, Utc};
|
||||
use derive_more::{AsRef, Deref, Display, From};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::crypto;
|
||||
use crate::CONFIG;
|
||||
use super::{
|
||||
Cipher, Device, EmergencyAccess, Favorite, Folder, Membership, MembershipType, TwoFactor, TwoFactorIncomplete,
|
||||
};
|
||||
use crate::{
|
||||
api::EmptyResult,
|
||||
crypto,
|
||||
db::DbConn,
|
||||
error::MapResult,
|
||||
util::{format_date, get_uuid, retry},
|
||||
CONFIG,
|
||||
};
|
||||
use macros::UuidFromParam;
|
||||
|
||||
db_object! {
|
||||
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
|
||||
@@ -11,7 +21,7 @@ db_object! {
|
||||
#[diesel(treat_none_as_null = true)]
|
||||
#[diesel(primary_key(uuid))]
|
||||
pub struct User {
|
||||
pub uuid: String,
|
||||
pub uuid: UserId,
|
||||
pub enabled: bool,
|
||||
pub created_at: NaiveDateTime,
|
||||
pub updated_at: NaiveDateTime,
|
||||
@@ -91,7 +101,7 @@ impl User {
|
||||
let email = email.to_lowercase();
|
||||
|
||||
Self {
|
||||
uuid: get_uuid(),
|
||||
uuid: UserId(get_uuid()),
|
||||
enabled: true,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
@@ -163,8 +173,8 @@ impl User {
|
||||
/// * `password` - A str which contains a hashed version of the users master password.
|
||||
/// * `new_key` - A String which contains the new aKey value of the users master password.
|
||||
/// * `allow_next_route` - A Option<Vec<String>> with the function names of the next allowed (rocket) routes.
|
||||
/// These routes are able to use the previous stamp id for the next 2 minutes.
|
||||
/// After these 2 minutes this stamp will expire.
|
||||
/// These routes are able to use the previous stamp id for the next 2 minutes.
|
||||
/// After these 2 minutes this stamp will expire.
|
||||
///
|
||||
pub fn set_password(
|
||||
&mut self,
|
||||
@@ -196,8 +206,8 @@ impl User {
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `route_exception` - A Vec<String> with the function names of the next allowed (rocket) routes.
|
||||
/// These routes are able to use the previous stamp id for the next 2 minutes.
|
||||
/// After these 2 minutes this stamp will expire.
|
||||
/// These routes are able to use the previous stamp id for the next 2 minutes.
|
||||
/// After these 2 minutes this stamp will expire.
|
||||
///
|
||||
pub fn set_stamp_exception(&mut self, route_exception: Vec<String>) {
|
||||
let stamp_exception = UserStampException {
|
||||
@@ -214,20 +224,11 @@ impl User {
|
||||
}
|
||||
}
|
||||
|
||||
use super::{
|
||||
Cipher, Device, EmergencyAccess, Favorite, Folder, Send, TwoFactor, TwoFactorIncomplete, UserOrgType,
|
||||
UserOrganization,
|
||||
};
|
||||
use crate::db::DbConn;
|
||||
|
||||
use crate::api::EmptyResult;
|
||||
use crate::error::MapResult;
|
||||
|
||||
/// Database methods
|
||||
impl User {
|
||||
pub async fn to_json(&self, conn: &mut DbConn) -> Value {
|
||||
let mut orgs_json = Vec::new();
|
||||
for c in UserOrganization::find_confirmed_by_user(&self.uuid, conn).await {
|
||||
for c in Membership::find_confirmed_by_user(&self.uuid, conn).await {
|
||||
orgs_json.push(c.to_json(conn).await);
|
||||
}
|
||||
|
||||
@@ -266,8 +267,8 @@ impl User {
|
||||
}
|
||||
|
||||
pub async fn save(&mut self, conn: &mut DbConn) -> EmptyResult {
|
||||
if self.email.trim().is_empty() {
|
||||
err!("User email can't be empty")
|
||||
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();
|
||||
@@ -304,19 +305,18 @@ impl User {
|
||||
}
|
||||
|
||||
pub async fn delete(self, conn: &mut DbConn) -> EmptyResult {
|
||||
for user_org in UserOrganization::find_confirmed_by_user(&self.uuid, conn).await {
|
||||
if user_org.atype == UserOrgType::Owner
|
||||
&& UserOrganization::count_confirmed_by_org_and_type(&user_org.org_uuid, UserOrgType::Owner, conn).await
|
||||
<= 1
|
||||
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
|
||||
{
|
||||
err!("Can't delete last owner")
|
||||
}
|
||||
}
|
||||
|
||||
Send::delete_all_by_user(&self.uuid, conn).await?;
|
||||
super::Send::delete_all_by_user(&self.uuid, conn).await?;
|
||||
EmergencyAccess::delete_all_by_user(&self.uuid, conn).await?;
|
||||
EmergencyAccess::delete_all_by_grantee_email(&self.email, conn).await?;
|
||||
UserOrganization::delete_all_by_user(&self.uuid, conn).await?;
|
||||
Membership::delete_all_by_user(&self.uuid, conn).await?;
|
||||
Cipher::delete_all_by_user(&self.uuid, conn).await?;
|
||||
Favorite::delete_all_by_user(&self.uuid, conn).await?;
|
||||
Folder::delete_all_by_user(&self.uuid, conn).await?;
|
||||
@@ -332,9 +332,9 @@ impl User {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn update_uuid_revision(uuid: &str, conn: &mut DbConn) {
|
||||
pub async fn update_uuid_revision(uuid: &UserId, conn: &mut DbConn) {
|
||||
if let Err(e) = Self::_update_revision(uuid, &Utc::now().naive_utc(), conn).await {
|
||||
warn!("Failed to update revision for {}: {:#?}", uuid, e);
|
||||
warn!("Failed to update revision for {uuid}: {e:#?}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -357,7 +357,7 @@ impl User {
|
||||
Self::_update_revision(&self.uuid, &self.updated_at, conn).await
|
||||
}
|
||||
|
||||
async fn _update_revision(uuid: &str, date: &NaiveDateTime, conn: &mut DbConn) -> EmptyResult {
|
||||
async fn _update_revision(uuid: &UserId, date: &NaiveDateTime, conn: &mut DbConn) -> EmptyResult {
|
||||
db_run! {conn: {
|
||||
retry(|| {
|
||||
diesel::update(users::table.filter(users::uuid.eq(uuid)))
|
||||
@@ -379,7 +379,7 @@ impl User {
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn find_by_uuid(uuid: &str, conn: &mut DbConn) -> Option<Self> {
|
||||
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()
|
||||
}}
|
||||
@@ -408,8 +408,8 @@ impl Invitation {
|
||||
}
|
||||
|
||||
pub async fn save(&self, conn: &mut DbConn) -> EmptyResult {
|
||||
if self.email.trim().is_empty() {
|
||||
err!("Invitation email can't be empty")
|
||||
if !crate::util::is_valid_email(&self.email) {
|
||||
err!(format!("Invitation email {} is not a valid email address", self.email))
|
||||
}
|
||||
|
||||
db_run! {conn:
|
||||
@@ -458,3 +458,23 @@ impl Invitation {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone,
|
||||
Debug,
|
||||
DieselNewType,
|
||||
FromForm,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Hash,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
AsRef,
|
||||
Deref,
|
||||
Display,
|
||||
From,
|
||||
UuidFromParam,
|
||||
)]
|
||||
#[deref(forward)]
|
||||
#[from(forward)]
|
||||
pub struct UserId(String);
|
||||
|
||||
@@ -226,6 +226,7 @@ table! {
|
||||
collection_uuid -> Text,
|
||||
read_only -> Bool,
|
||||
hide_passwords -> Bool,
|
||||
manage -> Bool,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,6 +296,7 @@ table! {
|
||||
groups_uuid -> Text,
|
||||
read_only -> Bool,
|
||||
hide_passwords -> Bool,
|
||||
manage -> Bool,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -226,6 +226,7 @@ table! {
|
||||
collection_uuid -> Text,
|
||||
read_only -> Bool,
|
||||
hide_passwords -> Bool,
|
||||
manage -> Bool,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,6 +296,7 @@ table! {
|
||||
groups_uuid -> Text,
|
||||
read_only -> Bool,
|
||||
hide_passwords -> Bool,
|
||||
manage -> Bool,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -226,6 +226,7 @@ table! {
|
||||
collection_uuid -> Text,
|
||||
read_only -> Bool,
|
||||
hide_passwords -> Bool,
|
||||
manage -> Bool,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,6 +296,7 @@ table! {
|
||||
groups_uuid -> Text,
|
||||
read_only -> Bool,
|
||||
hide_passwords -> Bool,
|
||||
manage -> Bool,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -214,7 +214,7 @@ impl Responder<'_, 'static> for Error {
|
||||
match self.error {
|
||||
ErrorKind::Empty(_) => {} // Don't print the error in this situation
|
||||
ErrorKind::Simple(_) => {} // Don't print the error in this situation
|
||||
_ => error!(target: "error", "{:#?}", self),
|
||||
_ => error!(target: "error", "{self:#?}"),
|
||||
};
|
||||
|
||||
let code = Status::from_code(self.error_code).unwrap_or(Status::BadRequest);
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use hickory_resolver::{system_conf::read_system_conf, TokioAsyncResolver};
|
||||
use hickory_resolver::{name_server::TokioConnectionProvider, TokioResolver};
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use reqwest::{
|
||||
@@ -173,7 +173,7 @@ impl std::error::Error for CustomHttpClientError {}
|
||||
#[derive(Debug, Clone)]
|
||||
enum CustomDnsResolver {
|
||||
Default(),
|
||||
Hickory(Arc<TokioAsyncResolver>),
|
||||
Hickory(Arc<TokioResolver>),
|
||||
}
|
||||
type BoxError = Box<dyn std::error::Error + Send + Sync>;
|
||||
|
||||
@@ -184,9 +184,9 @@ impl CustomDnsResolver {
|
||||
}
|
||||
|
||||
fn new() -> Arc<Self> {
|
||||
match read_system_conf() {
|
||||
Ok((config, opts)) => {
|
||||
let resolver = TokioAsyncResolver::tokio(config.clone(), opts.clone());
|
||||
match TokioResolver::builder(TokioConnectionProvider::default()) {
|
||||
Ok(builder) => {
|
||||
let resolver = builder.build();
|
||||
Arc::new(Self::Hickory(Arc::new(resolver)))
|
||||
}
|
||||
Err(e) => {
|
||||
|
||||
98
src/mail.rs
98
src/mail.rs
@@ -1,7 +1,6 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use chrono::NaiveDateTime;
|
||||
use percent_encoding::{percent_encode, NON_ALPHANUMERIC};
|
||||
use std::{env::consts::EXE_SUFFIX, str::FromStr};
|
||||
|
||||
use lettre::{
|
||||
message::{Attachment, Body, Mailbox, Message, MultiPart, SinglePart},
|
||||
@@ -17,7 +16,7 @@ use crate::{
|
||||
encode_jwt, generate_delete_claims, generate_emergency_access_invite_claims, generate_invite_claims,
|
||||
generate_verify_email_claims,
|
||||
},
|
||||
db::models::{Device, DeviceType, User},
|
||||
db::models::{Device, DeviceType, EmergencyAccessId, MembershipId, OrganizationId, User, UserId},
|
||||
error::Error,
|
||||
CONFIG,
|
||||
};
|
||||
@@ -26,7 +25,7 @@ fn sendmail_transport() -> AsyncSendmailTransport<Tokio1Executor> {
|
||||
if let Some(command) = CONFIG.sendmail_command() {
|
||||
AsyncSendmailTransport::new_with_command(command)
|
||||
} else {
|
||||
AsyncSendmailTransport::new()
|
||||
AsyncSendmailTransport::new_with_command(format!("sendmail{EXE_SUFFIX}"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,7 +85,7 @@ fn smtp_transport() -> AsyncSmtpTransport<Tokio1Executor> {
|
||||
smtp_client.authentication(selected_mechanisms)
|
||||
} else {
|
||||
// Only show a warning, and return without setting an actual authentication mechanism
|
||||
warn!("No valid SMTP Auth mechanism found for '{}', using default values", mechanism);
|
||||
warn!("No valid SMTP Auth mechanism found for '{mechanism}', using default values");
|
||||
smtp_client
|
||||
}
|
||||
}
|
||||
@@ -166,8 +165,8 @@ pub async fn send_password_hint(address: &str, hint: Option<String>) -> EmptyRes
|
||||
send_email(address, &subject, body_html, body_text).await
|
||||
}
|
||||
|
||||
pub async fn send_delete_account(address: &str, uuid: &str) -> EmptyResult {
|
||||
let claims = generate_delete_claims(uuid.to_string());
|
||||
pub async fn send_delete_account(address: &str, user_id: &UserId) -> EmptyResult {
|
||||
let claims = generate_delete_claims(user_id.to_string());
|
||||
let delete_token = encode_jwt(&claims);
|
||||
|
||||
let (subject, body_html, body_text) = get_text(
|
||||
@@ -175,7 +174,7 @@ pub async fn send_delete_account(address: &str, uuid: &str) -> EmptyResult {
|
||||
json!({
|
||||
"url": CONFIG.domain(),
|
||||
"img_src": CONFIG._smtp_img_src(),
|
||||
"user_id": uuid,
|
||||
"user_id": user_id,
|
||||
"email": percent_encode(address.as_bytes(), NON_ALPHANUMERIC).to_string(),
|
||||
"token": delete_token,
|
||||
}),
|
||||
@@ -184,8 +183,8 @@ pub async fn send_delete_account(address: &str, uuid: &str) -> EmptyResult {
|
||||
send_email(address, &subject, body_html, body_text).await
|
||||
}
|
||||
|
||||
pub async fn send_verify_email(address: &str, uuid: &str) -> EmptyResult {
|
||||
let claims = generate_verify_email_claims(uuid.to_string());
|
||||
pub async fn send_verify_email(address: &str, user_id: &UserId) -> EmptyResult {
|
||||
let claims = generate_verify_email_claims(user_id.clone());
|
||||
let verify_email_token = encode_jwt(&claims);
|
||||
|
||||
let (subject, body_html, body_text) = get_text(
|
||||
@@ -193,7 +192,7 @@ pub async fn send_verify_email(address: &str, uuid: &str) -> EmptyResult {
|
||||
json!({
|
||||
"url": CONFIG.domain(),
|
||||
"img_src": CONFIG._smtp_img_src(),
|
||||
"user_id": uuid,
|
||||
"user_id": user_id,
|
||||
"email": percent_encode(address.as_bytes(), NON_ALPHANUMERIC).to_string(),
|
||||
"token": verify_email_token,
|
||||
}),
|
||||
@@ -202,6 +201,27 @@ pub async fn send_verify_email(address: &str, uuid: &str) -> EmptyResult {
|
||||
send_email(address, &subject, body_html, body_text).await
|
||||
}
|
||||
|
||||
pub async fn send_register_verify_email(email: &str, token: &str) -> EmptyResult {
|
||||
let mut query = url::Url::parse("https://query.builder").unwrap();
|
||||
query.query_pairs_mut().append_pair("email", email).append_pair("token", token);
|
||||
let query_string = match query.query() {
|
||||
None => err!("Failed to build verify URL query parameters"),
|
||||
Some(query) => query,
|
||||
};
|
||||
|
||||
let (subject, body_html, body_text) = get_text(
|
||||
"email/register_verify_email",
|
||||
json!({
|
||||
// `url.Url` would place the anchor `#` after the query parameters
|
||||
"url": format!("{}/#/finish-signup/?{query_string}", CONFIG.domain()),
|
||||
"img_src": CONFIG._smtp_img_src(),
|
||||
"email": email,
|
||||
}),
|
||||
)?;
|
||||
|
||||
send_email(email, &subject, body_html, body_text).await
|
||||
}
|
||||
|
||||
pub async fn send_welcome(address: &str) -> EmptyResult {
|
||||
let (subject, body_html, body_text) = get_text(
|
||||
"email/welcome",
|
||||
@@ -214,8 +234,8 @@ pub async fn send_welcome(address: &str) -> EmptyResult {
|
||||
send_email(address, &subject, body_html, body_text).await
|
||||
}
|
||||
|
||||
pub async fn send_welcome_must_verify(address: &str, uuid: &str) -> EmptyResult {
|
||||
let claims = generate_verify_email_claims(uuid.to_string());
|
||||
pub async fn send_welcome_must_verify(address: &str, user_id: &UserId) -> EmptyResult {
|
||||
let claims = generate_verify_email_claims(user_id.clone());
|
||||
let verify_email_token = encode_jwt(&claims);
|
||||
|
||||
let (subject, body_html, body_text) = get_text(
|
||||
@@ -223,7 +243,7 @@ pub async fn send_welcome_must_verify(address: &str, uuid: &str) -> EmptyResult
|
||||
json!({
|
||||
"url": CONFIG.domain(),
|
||||
"img_src": CONFIG._smtp_img_src(),
|
||||
"user_id": uuid,
|
||||
"user_id": user_id,
|
||||
"token": verify_email_token,
|
||||
}),
|
||||
)?;
|
||||
@@ -259,8 +279,8 @@ pub async fn send_single_org_removed_from_org(address: &str, org_name: &str) ->
|
||||
|
||||
pub async fn send_invite(
|
||||
user: &User,
|
||||
org_id: Option<String>,
|
||||
org_user_id: Option<String>,
|
||||
org_id: OrganizationId,
|
||||
member_id: MembershipId,
|
||||
org_name: &str,
|
||||
invited_by_email: Option<String>,
|
||||
) -> EmptyResult {
|
||||
@@ -268,7 +288,7 @@ pub async fn send_invite(
|
||||
user.uuid.clone(),
|
||||
user.email.clone(),
|
||||
org_id.clone(),
|
||||
org_user_id.clone(),
|
||||
member_id.clone(),
|
||||
invited_by_email,
|
||||
);
|
||||
let invite_token = encode_jwt(&claims);
|
||||
@@ -278,24 +298,23 @@ pub async fn send_invite(
|
||||
query_params
|
||||
.append_pair("email", &user.email)
|
||||
.append_pair("organizationName", org_name)
|
||||
.append_pair("organizationId", org_id.as_deref().unwrap_or("_"))
|
||||
.append_pair("organizationUserId", org_user_id.as_deref().unwrap_or("_"))
|
||||
.append_pair("organizationId", &org_id)
|
||||
.append_pair("organizationUserId", &member_id)
|
||||
.append_pair("token", &invite_token);
|
||||
if user.private_key.is_some() {
|
||||
query_params.append_pair("orgUserHasExistingUser", "true");
|
||||
}
|
||||
}
|
||||
|
||||
let query_string = match query.query() {
|
||||
None => err!("Failed to build invite URL query parameters"),
|
||||
Some(query) => query,
|
||||
let Some(query_string) = query.query() else {
|
||||
err!("Failed to build invite URL query parameters")
|
||||
};
|
||||
|
||||
let (subject, body_html, body_text) = get_text(
|
||||
"email/send_org_invite",
|
||||
json!({
|
||||
// `url.Url` would place the anchor `#` after the query parameters
|
||||
"url": format!("{}/#/accept-organization/?{}", CONFIG.domain(), query_string),
|
||||
"url": format!("{}/#/accept-organization/?{query_string}", CONFIG.domain()),
|
||||
"img_src": CONFIG._smtp_img_src(),
|
||||
"org_name": org_name,
|
||||
}),
|
||||
@@ -306,15 +325,15 @@ pub async fn send_invite(
|
||||
|
||||
pub async fn send_emergency_access_invite(
|
||||
address: &str,
|
||||
uuid: &str,
|
||||
emer_id: &str,
|
||||
user_id: UserId,
|
||||
emer_id: EmergencyAccessId,
|
||||
grantor_name: &str,
|
||||
grantor_email: &str,
|
||||
) -> EmptyResult {
|
||||
let claims = generate_emergency_access_invite_claims(
|
||||
String::from(uuid),
|
||||
user_id,
|
||||
String::from(address),
|
||||
String::from(emer_id),
|
||||
emer_id.clone(),
|
||||
String::from(grantor_name),
|
||||
String::from(grantor_email),
|
||||
);
|
||||
@@ -324,15 +343,14 @@ pub async fn send_emergency_access_invite(
|
||||
{
|
||||
let mut query_params = query.query_pairs_mut();
|
||||
query_params
|
||||
.append_pair("id", emer_id)
|
||||
.append_pair("id", &emer_id.to_string())
|
||||
.append_pair("name", grantor_name)
|
||||
.append_pair("email", address)
|
||||
.append_pair("token", &encode_jwt(&claims));
|
||||
}
|
||||
|
||||
let query_string = match query.query() {
|
||||
None => err!("Failed to build emergency invite URL query parameters"),
|
||||
Some(query) => query,
|
||||
let Some(query_string) = query.query() else {
|
||||
err!("Failed to build emergency invite URL query parameters")
|
||||
};
|
||||
|
||||
let (subject, body_html, body_text) = get_text(
|
||||
@@ -597,13 +615,13 @@ async fn send_with_selected_transport(email: Message) -> EmptyResult {
|
||||
// Match some common errors and make them more user friendly
|
||||
Err(e) => {
|
||||
if e.is_client() {
|
||||
debug!("Sendmail client error: {:#?}", e);
|
||||
debug!("Sendmail client error: {e:?}");
|
||||
err!(format!("Sendmail client error: {e}"));
|
||||
} else if e.is_response() {
|
||||
debug!("Sendmail response error: {:#?}", e);
|
||||
debug!("Sendmail response error: {e:?}");
|
||||
err!(format!("Sendmail response error: {e}"));
|
||||
} else {
|
||||
debug!("Sendmail error: {:#?}", e);
|
||||
debug!("Sendmail error: {e:?}");
|
||||
err!(format!("Sendmail error: {e}"));
|
||||
}
|
||||
}
|
||||
@@ -614,13 +632,13 @@ async fn send_with_selected_transport(email: Message) -> EmptyResult {
|
||||
// Match some common errors and make them more user friendly
|
||||
Err(e) => {
|
||||
if e.is_client() {
|
||||
debug!("SMTP client error: {:#?}", e);
|
||||
debug!("SMTP client error: {e:#?}");
|
||||
err!(format!("SMTP client error: {e}"));
|
||||
} else if e.is_transient() {
|
||||
debug!("SMTP 4xx error: {:#?}", e);
|
||||
debug!("SMTP 4xx error: {e:#?}");
|
||||
err!(format!("SMTP 4xx error: {e}"));
|
||||
} else if e.is_permanent() {
|
||||
debug!("SMTP 5xx error: {:#?}", e);
|
||||
debug!("SMTP 5xx error: {e:#?}");
|
||||
let mut msg = e.to_string();
|
||||
// Add a special check for 535 to add a more descriptive message
|
||||
if msg.contains("(535)") {
|
||||
@@ -628,13 +646,13 @@ async fn send_with_selected_transport(email: Message) -> EmptyResult {
|
||||
}
|
||||
err!(format!("SMTP 5xx error: {msg}"));
|
||||
} else if e.is_timeout() {
|
||||
debug!("SMTP timeout error: {:#?}", e);
|
||||
debug!("SMTP timeout error: {e:#?}");
|
||||
err!(format!("SMTP timeout error: {e}"));
|
||||
} else if e.is_tls() {
|
||||
debug!("SMTP encryption error: {:#?}", e);
|
||||
debug!("SMTP encryption error: {e:#?}");
|
||||
err!(format!("SMTP encryption error: {e}"));
|
||||
} else {
|
||||
debug!("SMTP error: {:#?}", e);
|
||||
debug!("SMTP error: {e:#?}");
|
||||
err!(format!("SMTP error: {e}"));
|
||||
}
|
||||
}
|
||||
|
||||
15
src/main.rs
15
src/main.rs
@@ -24,6 +24,8 @@ extern crate log;
|
||||
extern crate diesel;
|
||||
#[macro_use]
|
||||
extern crate diesel_migrations;
|
||||
#[macro_use]
|
||||
extern crate diesel_derive_newtype;
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
@@ -428,10 +430,7 @@ fn init_logging() -> Result<log::LevelFilter, Error> {
|
||||
}
|
||||
None => error!(
|
||||
target: "panic",
|
||||
"thread '{}' panicked at '{}'\n{:}",
|
||||
thread,
|
||||
msg,
|
||||
backtrace
|
||||
"thread '{thread}' panicked at '{msg}'\n{backtrace:}"
|
||||
),
|
||||
}
|
||||
}));
|
||||
@@ -451,7 +450,7 @@ fn chain_syslog(logger: fern::Dispatch) -> fern::Dispatch {
|
||||
match syslog::unix(syslog_fmt) {
|
||||
Ok(sl) => logger.chain(sl),
|
||||
Err(e) => {
|
||||
error!("Unable to connect to syslog: {:?}", e);
|
||||
error!("Unable to connect to syslog: {e:?}");
|
||||
logger
|
||||
}
|
||||
}
|
||||
@@ -467,7 +466,7 @@ async fn check_data_folder() {
|
||||
let data_folder = &CONFIG.data_folder();
|
||||
let path = Path::new(data_folder);
|
||||
if !path.exists() {
|
||||
error!("Data folder '{}' doesn't exist.", data_folder);
|
||||
error!("Data folder '{data_folder}' doesn't exist.");
|
||||
if is_running_in_container() {
|
||||
error!("Verify that your data volume is mounted at the correct location.");
|
||||
} else {
|
||||
@@ -476,7 +475,7 @@ async fn check_data_folder() {
|
||||
exit(1);
|
||||
}
|
||||
if !path.is_dir() {
|
||||
error!("Data folder '{}' is not a directory.", data_folder);
|
||||
error!("Data folder '{data_folder}' is not a directory.");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
@@ -550,7 +549,7 @@ async fn create_db_pool() -> db::DbPool {
|
||||
match util::retry_db(db::DbPool::from_config, CONFIG.db_connection_retries()).await {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
error!("Error creating database pool: {:?}", e);
|
||||
error!("Error creating database pool: {e:?}");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
5
src/static/scripts/admin_diagnostics.js
vendored
5
src/static/scripts/admin_diagnostics.js
vendored
@@ -236,8 +236,11 @@ function checkSecurityHeaders(headers, omit) {
|
||||
"referrer-policy": ["same-origin"],
|
||||
"x-xss-protection": ["0"],
|
||||
"x-robots-tag": ["noindex", "nofollow"],
|
||||
"cross-origin-resource-policy": ["same-origin"],
|
||||
"content-security-policy": [
|
||||
"default-src 'self'",
|
||||
"default-src 'none'",
|
||||
"font-src 'self'",
|
||||
"manifest-src 'self'",
|
||||
"base-uri 'self'",
|
||||
"form-action 'self'",
|
||||
"object-src 'self' blob:",
|
||||
|
||||
2
src/static/scripts/admin_users.js
vendored
2
src/static/scripts/admin_users.js
vendored
@@ -152,7 +152,7 @@ const ORG_TYPES = {
|
||||
"name": "User",
|
||||
"bg": "blue"
|
||||
},
|
||||
"3": {
|
||||
"4": {
|
||||
"name": "Manager",
|
||||
"bg": "green"
|
||||
},
|
||||
|
||||
27
src/static/scripts/bootstrap.bundle.js
vendored
27
src/static/scripts/bootstrap.bundle.js
vendored
@@ -1,6 +1,6 @@
|
||||
/*!
|
||||
* Bootstrap v5.3.3 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
|
||||
* Bootstrap v5.3.4 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
*/
|
||||
(function (global, factory) {
|
||||
@@ -205,7 +205,7 @@
|
||||
* @param {HTMLElement} element
|
||||
* @return void
|
||||
*
|
||||
* @see https://www.charistheo.io/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation
|
||||
* @see https://www.harrytheo.com/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation
|
||||
*/
|
||||
const reflow = element => {
|
||||
element.offsetHeight; // eslint-disable-line no-unused-expressions
|
||||
@@ -250,7 +250,7 @@
|
||||
});
|
||||
};
|
||||
const execute = (possibleCallback, args = [], defaultValue = possibleCallback) => {
|
||||
return typeof possibleCallback === 'function' ? possibleCallback(...args) : defaultValue;
|
||||
return typeof possibleCallback === 'function' ? possibleCallback.call(...args) : defaultValue;
|
||||
};
|
||||
const executeAfterTransition = (callback, transitionElement, waitForTransition = true) => {
|
||||
if (!waitForTransition) {
|
||||
@@ -572,7 +572,7 @@
|
||||
const bsKeys = Object.keys(element.dataset).filter(key => key.startsWith('bs') && !key.startsWith('bsConfig'));
|
||||
for (const key of bsKeys) {
|
||||
let pureKey = key.replace(/^bs/, '');
|
||||
pureKey = pureKey.charAt(0).toLowerCase() + pureKey.slice(1, pureKey.length);
|
||||
pureKey = pureKey.charAt(0).toLowerCase() + pureKey.slice(1);
|
||||
attributes[pureKey] = normalizeData(element.dataset[key]);
|
||||
}
|
||||
return attributes;
|
||||
@@ -647,7 +647,7 @@
|
||||
* Constants
|
||||
*/
|
||||
|
||||
const VERSION = '5.3.3';
|
||||
const VERSION = '5.3.4';
|
||||
|
||||
/**
|
||||
* Class definition
|
||||
@@ -2666,7 +2666,6 @@
|
||||
var popperOffsets = computeOffsets({
|
||||
reference: referenceClientRect,
|
||||
element: popperRect,
|
||||
strategy: 'absolute',
|
||||
placement: placement
|
||||
});
|
||||
var popperClientRect = rectToClientRect(Object.assign({}, popperRect, popperOffsets));
|
||||
@@ -2994,7 +2993,6 @@
|
||||
state.modifiersData[name] = computeOffsets({
|
||||
reference: state.rects.reference,
|
||||
element: state.rects.popper,
|
||||
strategy: 'absolute',
|
||||
placement: state.placement
|
||||
});
|
||||
} // eslint-disable-next-line import/no-unused-modules
|
||||
@@ -3701,7 +3699,7 @@
|
||||
}
|
||||
_createPopper() {
|
||||
if (typeof Popper === 'undefined') {
|
||||
throw new TypeError('Bootstrap\'s dropdowns require Popper (https://popper.js.org)');
|
||||
throw new TypeError('Bootstrap\'s dropdowns require Popper (https://popper.js.org/docs/v2/)');
|
||||
}
|
||||
let referenceElement = this._element;
|
||||
if (this._config.reference === 'parent') {
|
||||
@@ -3780,7 +3778,7 @@
|
||||
}
|
||||
return {
|
||||
...defaultBsPopperConfig,
|
||||
...execute(this._config.popperConfig, [defaultBsPopperConfig])
|
||||
...execute(this._config.popperConfig, [undefined, defaultBsPopperConfig])
|
||||
};
|
||||
}
|
||||
_selectMenuItem({
|
||||
@@ -4967,7 +4965,7 @@
|
||||
return this._config.sanitize ? sanitizeHtml(arg, this._config.allowList, this._config.sanitizeFn) : arg;
|
||||
}
|
||||
_resolvePossibleFunction(arg) {
|
||||
return execute(arg, [this]);
|
||||
return execute(arg, [undefined, this]);
|
||||
}
|
||||
_putElementInTemplate(element, templateElement) {
|
||||
if (this._config.html) {
|
||||
@@ -5066,7 +5064,7 @@
|
||||
class Tooltip extends BaseComponent {
|
||||
constructor(element, config) {
|
||||
if (typeof Popper === 'undefined') {
|
||||
throw new TypeError('Bootstrap\'s tooltips require Popper (https://popper.js.org)');
|
||||
throw new TypeError('Bootstrap\'s tooltips require Popper (https://popper.js.org/docs/v2/)');
|
||||
}
|
||||
super(element, config);
|
||||
|
||||
@@ -5112,7 +5110,6 @@
|
||||
if (!this._isEnabled) {
|
||||
return;
|
||||
}
|
||||
this._activeTrigger.click = !this._activeTrigger.click;
|
||||
if (this._isShown()) {
|
||||
this._leave();
|
||||
return;
|
||||
@@ -5300,7 +5297,7 @@
|
||||
return offset;
|
||||
}
|
||||
_resolvePossibleFunction(arg) {
|
||||
return execute(arg, [this._element]);
|
||||
return execute(arg, [this._element, this._element]);
|
||||
}
|
||||
_getPopperConfig(attachment) {
|
||||
const defaultBsPopperConfig = {
|
||||
@@ -5338,7 +5335,7 @@
|
||||
};
|
||||
return {
|
||||
...defaultBsPopperConfig,
|
||||
...execute(this._config.popperConfig, [defaultBsPopperConfig])
|
||||
...execute(this._config.popperConfig, [undefined, defaultBsPopperConfig])
|
||||
};
|
||||
}
|
||||
_setListeners() {
|
||||
|
||||
227
src/static/scripts/bootstrap.css
vendored
227
src/static/scripts/bootstrap.css
vendored
@@ -1,7 +1,7 @@
|
||||
@charset "UTF-8";
|
||||
/*!
|
||||
* Bootstrap v5.3.3 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2024 The Bootstrap Authors
|
||||
* Bootstrap v5.3.4 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2025 The Bootstrap Authors
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
*/
|
||||
:root,
|
||||
@@ -517,8 +517,8 @@ legend {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
line-height: inherit;
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
legend {
|
||||
@@ -601,9 +601,9 @@ progress {
|
||||
}
|
||||
|
||||
.display-1 {
|
||||
font-size: calc(1.625rem + 4.5vw);
|
||||
font-weight: 300;
|
||||
line-height: 1.2;
|
||||
font-size: calc(1.625rem + 4.5vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
.display-1 {
|
||||
@@ -612,9 +612,9 @@ progress {
|
||||
}
|
||||
|
||||
.display-2 {
|
||||
font-size: calc(1.575rem + 3.9vw);
|
||||
font-weight: 300;
|
||||
line-height: 1.2;
|
||||
font-size: calc(1.575rem + 3.9vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
.display-2 {
|
||||
@@ -623,9 +623,9 @@ progress {
|
||||
}
|
||||
|
||||
.display-3 {
|
||||
font-size: calc(1.525rem + 3.3vw);
|
||||
font-weight: 300;
|
||||
line-height: 1.2;
|
||||
font-size: calc(1.525rem + 3.3vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
.display-3 {
|
||||
@@ -634,9 +634,9 @@ progress {
|
||||
}
|
||||
|
||||
.display-4 {
|
||||
font-size: calc(1.475rem + 2.7vw);
|
||||
font-weight: 300;
|
||||
line-height: 1.2;
|
||||
font-size: calc(1.475rem + 2.7vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
.display-4 {
|
||||
@@ -645,9 +645,9 @@ progress {
|
||||
}
|
||||
|
||||
.display-5 {
|
||||
font-size: calc(1.425rem + 2.1vw);
|
||||
font-weight: 300;
|
||||
line-height: 1.2;
|
||||
font-size: calc(1.425rem + 2.1vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
.display-5 {
|
||||
@@ -656,9 +656,9 @@ progress {
|
||||
}
|
||||
|
||||
.display-6 {
|
||||
font-size: calc(1.375rem + 1.5vw);
|
||||
font-weight: 300;
|
||||
line-height: 1.2;
|
||||
font-size: calc(1.375rem + 1.5vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
.display-6 {
|
||||
@@ -803,7 +803,7 @@ progress {
|
||||
}
|
||||
|
||||
.col {
|
||||
flex: 1 0 0%;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
|
||||
.row-cols-auto > * {
|
||||
@@ -1012,7 +1012,7 @@ progress {
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.col-sm {
|
||||
flex: 1 0 0%;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
.row-cols-sm-auto > * {
|
||||
flex: 0 0 auto;
|
||||
@@ -1181,7 +1181,7 @@ progress {
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.col-md {
|
||||
flex: 1 0 0%;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
.row-cols-md-auto > * {
|
||||
flex: 0 0 auto;
|
||||
@@ -1350,7 +1350,7 @@ progress {
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.col-lg {
|
||||
flex: 1 0 0%;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
.row-cols-lg-auto > * {
|
||||
flex: 0 0 auto;
|
||||
@@ -1519,7 +1519,7 @@ progress {
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
.col-xl {
|
||||
flex: 1 0 0%;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
.row-cols-xl-auto > * {
|
||||
flex: 0 0 auto;
|
||||
@@ -1688,7 +1688,7 @@ progress {
|
||||
}
|
||||
@media (min-width: 1400px) {
|
||||
.col-xxl {
|
||||
flex: 1 0 0%;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
.row-cols-xxl-auto > * {
|
||||
flex: 0 0 auto;
|
||||
@@ -2607,9 +2607,11 @@ textarea.form-control-lg {
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
padding: 1rem 0.75rem;
|
||||
overflow: hidden;
|
||||
color: rgba(var(--bs-body-color-rgb), 0.65);
|
||||
text-align: start;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
@@ -2634,7 +2636,7 @@ textarea.form-control-lg {
|
||||
.form-floating > .form-control-plaintext::placeholder {
|
||||
color: transparent;
|
||||
}
|
||||
.form-floating > .form-control:not(:-moz-placeholder-shown), .form-floating > .form-control-plaintext:not(:-moz-placeholder-shown) {
|
||||
.form-floating > .form-control:not(:-moz-placeholder), .form-floating > .form-control-plaintext:not(:-moz-placeholder) {
|
||||
padding-top: 1.625rem;
|
||||
padding-bottom: 0.625rem;
|
||||
}
|
||||
@@ -2652,43 +2654,42 @@ textarea.form-control-lg {
|
||||
.form-floating > .form-select {
|
||||
padding-top: 1.625rem;
|
||||
padding-bottom: 0.625rem;
|
||||
padding-left: 0.75rem;
|
||||
}
|
||||
.form-floating > .form-control:not(:-moz-placeholder-shown) ~ label {
|
||||
color: rgba(var(--bs-body-color-rgb), 0.65);
|
||||
.form-floating > .form-control:not(:-moz-placeholder) ~ label {
|
||||
transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem);
|
||||
}
|
||||
.form-floating > .form-control:focus ~ label,
|
||||
.form-floating > .form-control:not(:placeholder-shown) ~ label,
|
||||
.form-floating > .form-control-plaintext ~ label,
|
||||
.form-floating > .form-select ~ label {
|
||||
color: rgba(var(--bs-body-color-rgb), 0.65);
|
||||
transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem);
|
||||
}
|
||||
.form-floating > .form-control:not(:-moz-placeholder-shown) ~ label::after {
|
||||
position: absolute;
|
||||
inset: 1rem 0.375rem;
|
||||
z-index: -1;
|
||||
height: 1.5em;
|
||||
content: "";
|
||||
background-color: var(--bs-body-bg);
|
||||
border-radius: var(--bs-border-radius);
|
||||
}
|
||||
.form-floating > .form-control:focus ~ label::after,
|
||||
.form-floating > .form-control:not(:placeholder-shown) ~ label::after,
|
||||
.form-floating > .form-control-plaintext ~ label::after,
|
||||
.form-floating > .form-select ~ label::after {
|
||||
position: absolute;
|
||||
inset: 1rem 0.375rem;
|
||||
z-index: -1;
|
||||
height: 1.5em;
|
||||
content: "";
|
||||
background-color: var(--bs-body-bg);
|
||||
border-radius: var(--bs-border-radius);
|
||||
}
|
||||
.form-floating > .form-control:-webkit-autofill ~ label {
|
||||
color: rgba(var(--bs-body-color-rgb), 0.65);
|
||||
transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem);
|
||||
}
|
||||
.form-floating > textarea:not(:-moz-placeholder) ~ label::after {
|
||||
position: absolute;
|
||||
inset: 1rem 0.375rem;
|
||||
z-index: -1;
|
||||
height: 1.5em;
|
||||
content: "";
|
||||
background-color: var(--bs-body-bg);
|
||||
border-radius: var(--bs-border-radius);
|
||||
}
|
||||
.form-floating > textarea:focus ~ label::after,
|
||||
.form-floating > textarea:not(:placeholder-shown) ~ label::after {
|
||||
position: absolute;
|
||||
inset: 1rem 0.375rem;
|
||||
z-index: -1;
|
||||
height: 1.5em;
|
||||
content: "";
|
||||
background-color: var(--bs-body-bg);
|
||||
border-radius: var(--bs-border-radius);
|
||||
}
|
||||
.form-floating > textarea:disabled ~ label::after {
|
||||
background-color: var(--bs-secondary-bg);
|
||||
}
|
||||
.form-floating > .form-control-plaintext ~ label {
|
||||
border-width: var(--bs-border-width) 0;
|
||||
}
|
||||
@@ -2696,10 +2697,6 @@ textarea.form-control-lg {
|
||||
.form-floating > .form-control:disabled ~ label {
|
||||
color: #6c757d;
|
||||
}
|
||||
.form-floating > :disabled ~ label::after,
|
||||
.form-floating > .form-control:disabled ~ label::after {
|
||||
background-color: var(--bs-secondary-bg);
|
||||
}
|
||||
|
||||
.input-group {
|
||||
position: relative;
|
||||
@@ -2782,7 +2779,7 @@ textarea.form-control-lg {
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
.input-group > :not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback) {
|
||||
margin-left: calc(var(--bs-border-width) * -1);
|
||||
margin-left: calc(-1 * var(--bs-border-width));
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
@@ -2824,7 +2821,7 @@ textarea.form-control-lg {
|
||||
.was-validated .form-control:valid, .form-control.is-valid {
|
||||
border-color: var(--bs-form-valid-border-color);
|
||||
padding-right: calc(1.5em + 0.75rem);
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1'/%3e%3c/svg%3e");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right calc(0.375em + 0.1875rem) center;
|
||||
background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
|
||||
@@ -2843,7 +2840,7 @@ textarea.form-control-lg {
|
||||
border-color: var(--bs-form-valid-border-color);
|
||||
}
|
||||
.was-validated .form-select:valid:not([multiple]):not([size]), .was-validated .form-select:valid:not([multiple])[size="1"], .form-select.is-valid:not([multiple]):not([size]), .form-select.is-valid:not([multiple])[size="1"] {
|
||||
--bs-form-select-bg-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");
|
||||
--bs-form-select-bg-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1'/%3e%3c/svg%3e");
|
||||
padding-right: 4.125rem;
|
||||
background-position: right 0.75rem center, center right 2.25rem;
|
||||
background-size: 16px 12px, calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
|
||||
@@ -3755,7 +3752,7 @@ textarea.form-control-lg {
|
||||
}
|
||||
.btn-group > :not(.btn-check:first-child) + .btn,
|
||||
.btn-group > .btn-group:not(:first-child) {
|
||||
margin-left: calc(var(--bs-border-width) * -1);
|
||||
margin-left: calc(-1 * var(--bs-border-width));
|
||||
}
|
||||
.btn-group > .btn:not(:last-child):not(.dropdown-toggle),
|
||||
.btn-group > .btn.dropdown-toggle-split:first-child,
|
||||
@@ -3802,14 +3799,15 @@ textarea.form-control-lg {
|
||||
}
|
||||
.btn-group-vertical > .btn:not(:first-child),
|
||||
.btn-group-vertical > .btn-group:not(:first-child) {
|
||||
margin-top: calc(var(--bs-border-width) * -1);
|
||||
margin-top: calc(-1 * var(--bs-border-width));
|
||||
}
|
||||
.btn-group-vertical > .btn:not(:last-child):not(.dropdown-toggle),
|
||||
.btn-group-vertical > .btn-group:not(:last-child) > .btn {
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
.btn-group-vertical > .btn ~ .btn,
|
||||
.btn-group-vertical > .btn:nth-child(n+3),
|
||||
.btn-group-vertical > :not(.btn-check) + .btn,
|
||||
.btn-group-vertical > .btn-group:not(:first-child) > .btn {
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
@@ -3933,8 +3931,8 @@ textarea.form-control-lg {
|
||||
|
||||
.nav-justified > .nav-link,
|
||||
.nav-justified .nav-item {
|
||||
flex-basis: 0;
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -4035,8 +4033,8 @@ textarea.form-control-lg {
|
||||
}
|
||||
|
||||
.navbar-collapse {
|
||||
flex-basis: 100%;
|
||||
flex-grow: 1;
|
||||
flex-basis: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@@ -4531,7 +4529,7 @@ textarea.form-control-lg {
|
||||
flex-flow: row wrap;
|
||||
}
|
||||
.card-group > .card {
|
||||
flex: 1 0 0%;
|
||||
flex: 1 0 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.card-group > .card + .card {
|
||||
@@ -4576,11 +4574,11 @@ textarea.form-control-lg {
|
||||
--bs-accordion-btn-padding-y: 1rem;
|
||||
--bs-accordion-btn-color: var(--bs-body-color);
|
||||
--bs-accordion-btn-bg: var(--bs-accordion-bg);
|
||||
--bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23212529' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='M2 5L8 11L14 5'/%3e%3c/svg%3e");
|
||||
--bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23212529' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='m2 5 6 6 6-6'/%3e%3c/svg%3e");
|
||||
--bs-accordion-btn-icon-width: 1.25rem;
|
||||
--bs-accordion-btn-icon-transform: rotate(-180deg);
|
||||
--bs-accordion-btn-icon-transition: transform 0.2s ease-in-out;
|
||||
--bs-accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23052c65' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='M2 5L8 11L14 5'/%3e%3c/svg%3e");
|
||||
--bs-accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23052c65' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='m2 5 6 6 6-6'/%3e%3c/svg%3e");
|
||||
--bs-accordion-btn-focus-box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
|
||||
--bs-accordion-body-padding-x: 1.25rem;
|
||||
--bs-accordion-body-padding-y: 1rem;
|
||||
@@ -4690,16 +4688,15 @@ textarea.form-control-lg {
|
||||
.accordion-flush > .accordion-item:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
.accordion-flush > .accordion-item > .accordion-header .accordion-button, .accordion-flush > .accordion-item > .accordion-header .accordion-button.collapsed {
|
||||
border-radius: 0;
|
||||
}
|
||||
.accordion-flush > .accordion-item > .accordion-collapse {
|
||||
.accordion-flush > .accordion-item > .accordion-collapse,
|
||||
.accordion-flush > .accordion-item > .accordion-header .accordion-button,
|
||||
.accordion-flush > .accordion-item > .accordion-header .accordion-button.collapsed {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
[data-bs-theme=dark] .accordion-button::after {
|
||||
--bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");
|
||||
--bs-accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");
|
||||
--bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708'/%3e%3c/svg%3e");
|
||||
--bs-accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708'/%3e%3c/svg%3e");
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
@@ -4803,7 +4800,7 @@ textarea.form-control-lg {
|
||||
}
|
||||
|
||||
.page-item:not(:first-child) .page-link {
|
||||
margin-left: calc(var(--bs-border-width) * -1);
|
||||
margin-left: calc(-1 * var(--bs-border-width));
|
||||
}
|
||||
.page-item:first-child .page-link {
|
||||
border-top-left-radius: var(--bs-pagination-border-radius);
|
||||
@@ -4952,7 +4949,7 @@ textarea.form-control-lg {
|
||||
|
||||
@keyframes progress-bar-stripes {
|
||||
0% {
|
||||
background-position-x: 1rem;
|
||||
background-position-x: var(--bs-progress-height);
|
||||
}
|
||||
}
|
||||
.progress,
|
||||
@@ -5046,22 +5043,6 @@ textarea.form-control-lg {
|
||||
counter-increment: section;
|
||||
}
|
||||
|
||||
.list-group-item-action {
|
||||
width: 100%;
|
||||
color: var(--bs-list-group-action-color);
|
||||
text-align: inherit;
|
||||
}
|
||||
.list-group-item-action:hover, .list-group-item-action:focus {
|
||||
z-index: 1;
|
||||
color: var(--bs-list-group-action-hover-color);
|
||||
text-decoration: none;
|
||||
background-color: var(--bs-list-group-action-hover-bg);
|
||||
}
|
||||
.list-group-item-action:active {
|
||||
color: var(--bs-list-group-action-active-color);
|
||||
background-color: var(--bs-list-group-action-active-bg);
|
||||
}
|
||||
|
||||
.list-group-item {
|
||||
position: relative;
|
||||
display: block;
|
||||
@@ -5098,6 +5079,22 @@ textarea.form-control-lg {
|
||||
border-top-width: var(--bs-list-group-border-width);
|
||||
}
|
||||
|
||||
.list-group-item-action {
|
||||
width: 100%;
|
||||
color: var(--bs-list-group-action-color);
|
||||
text-align: inherit;
|
||||
}
|
||||
.list-group-item-action:not(.active):hover, .list-group-item-action:not(.active):focus {
|
||||
z-index: 1;
|
||||
color: var(--bs-list-group-action-hover-color);
|
||||
text-decoration: none;
|
||||
background-color: var(--bs-list-group-action-hover-bg);
|
||||
}
|
||||
.list-group-item-action:not(.active):active {
|
||||
color: var(--bs-list-group-action-active-color);
|
||||
background-color: var(--bs-list-group-action-active-bg);
|
||||
}
|
||||
|
||||
.list-group-horizontal {
|
||||
flex-direction: row;
|
||||
}
|
||||
@@ -5357,19 +5354,19 @@ textarea.form-control-lg {
|
||||
|
||||
.btn-close {
|
||||
--bs-btn-close-color: #000;
|
||||
--bs-btn-close-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414z'/%3e%3c/svg%3e");
|
||||
--bs-btn-close-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414'/%3e%3c/svg%3e");
|
||||
--bs-btn-close-opacity: 0.5;
|
||||
--bs-btn-close-hover-opacity: 0.75;
|
||||
--bs-btn-close-focus-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
|
||||
--bs-btn-close-focus-opacity: 1;
|
||||
--bs-btn-close-disabled-opacity: 0.25;
|
||||
--bs-btn-close-white-filter: invert(1) grayscale(100%) brightness(200%);
|
||||
box-sizing: content-box;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
padding: 0.25em 0.25em;
|
||||
color: var(--bs-btn-close-color);
|
||||
background: transparent var(--bs-btn-close-bg) center/1em auto no-repeat;
|
||||
filter: var(--bs-btn-close-filter);
|
||||
border: 0;
|
||||
border-radius: 0.375rem;
|
||||
opacity: var(--bs-btn-close-opacity);
|
||||
@@ -5393,11 +5390,16 @@ textarea.form-control-lg {
|
||||
}
|
||||
|
||||
.btn-close-white {
|
||||
filter: var(--bs-btn-close-white-filter);
|
||||
--bs-btn-close-filter: invert(1) grayscale(100%) brightness(200%);
|
||||
}
|
||||
|
||||
[data-bs-theme=dark] .btn-close {
|
||||
filter: var(--bs-btn-close-white-filter);
|
||||
:root,
|
||||
[data-bs-theme=light] {
|
||||
--bs-btn-close-filter: ;
|
||||
}
|
||||
|
||||
[data-bs-theme=dark] {
|
||||
--bs-btn-close-filter: invert(1) grayscale(100%) brightness(200%);
|
||||
}
|
||||
|
||||
.toast {
|
||||
@@ -5474,7 +5476,7 @@ textarea.form-control-lg {
|
||||
--bs-modal-width: 500px;
|
||||
--bs-modal-padding: 1rem;
|
||||
--bs-modal-margin: 0.5rem;
|
||||
--bs-modal-color: ;
|
||||
--bs-modal-color: var(--bs-body-color);
|
||||
--bs-modal-bg: var(--bs-body-bg);
|
||||
--bs-modal-border-color: var(--bs-border-color-translucent);
|
||||
--bs-modal-border-width: var(--bs-border-width);
|
||||
@@ -5510,8 +5512,8 @@ textarea.form-control-lg {
|
||||
pointer-events: none;
|
||||
}
|
||||
.modal.fade .modal-dialog {
|
||||
transition: transform 0.3s ease-out;
|
||||
transform: translate(0, -50px);
|
||||
transition: transform 0.3s ease-out;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.modal.fade .modal-dialog {
|
||||
@@ -5586,7 +5588,10 @@ textarea.form-control-lg {
|
||||
}
|
||||
.modal-header .btn-close {
|
||||
padding: calc(var(--bs-modal-header-padding-y) * 0.5) calc(var(--bs-modal-header-padding-x) * 0.5);
|
||||
margin: calc(-0.5 * var(--bs-modal-header-padding-y)) calc(-0.5 * var(--bs-modal-header-padding-x)) calc(-0.5 * var(--bs-modal-header-padding-y)) auto;
|
||||
margin-top: calc(-0.5 * var(--bs-modal-header-padding-y));
|
||||
margin-right: calc(-0.5 * var(--bs-modal-header-padding-x));
|
||||
margin-bottom: calc(-0.5 * var(--bs-modal-header-padding-y));
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
@@ -6107,6 +6112,7 @@ textarea.form-control-lg {
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
background: none;
|
||||
filter: var(--bs-carousel-control-icon-filter);
|
||||
border: 0;
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.15s ease;
|
||||
@@ -6145,11 +6151,11 @@ textarea.form-control-lg {
|
||||
}
|
||||
|
||||
.carousel-control-prev-icon {
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e") /*rtl:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")*/;
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0'/%3e%3c/svg%3e") /*rtl:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708'/%3e%3c/svg%3e")*/;
|
||||
}
|
||||
|
||||
.carousel-control-next-icon {
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e") /*rtl:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")*/;
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708'/%3e%3c/svg%3e") /*rtl:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0'/%3e%3c/svg%3e")*/;
|
||||
}
|
||||
|
||||
.carousel-indicators {
|
||||
@@ -6175,7 +6181,7 @@ textarea.form-control-lg {
|
||||
margin-left: 3px;
|
||||
text-indent: -999px;
|
||||
cursor: pointer;
|
||||
background-color: #fff;
|
||||
background-color: var(--bs-carousel-indicator-active-bg);
|
||||
background-clip: padding-box;
|
||||
border: 0;
|
||||
border-top: 10px solid transparent;
|
||||
@@ -6199,31 +6205,27 @@ textarea.form-control-lg {
|
||||
left: 15%;
|
||||
padding-top: 1.25rem;
|
||||
padding-bottom: 1.25rem;
|
||||
color: #fff;
|
||||
color: var(--bs-carousel-caption-color);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.carousel-dark .carousel-control-prev-icon,
|
||||
.carousel-dark .carousel-control-next-icon {
|
||||
filter: invert(1) grayscale(100);
|
||||
}
|
||||
.carousel-dark .carousel-indicators [data-bs-target] {
|
||||
background-color: #000;
|
||||
}
|
||||
.carousel-dark .carousel-caption {
|
||||
color: #000;
|
||||
.carousel-dark {
|
||||
--bs-carousel-indicator-active-bg: #000;
|
||||
--bs-carousel-caption-color: #000;
|
||||
--bs-carousel-control-icon-filter: invert(1) grayscale(100);
|
||||
}
|
||||
|
||||
[data-bs-theme=dark] .carousel .carousel-control-prev-icon,
|
||||
[data-bs-theme=dark] .carousel .carousel-control-next-icon, [data-bs-theme=dark].carousel .carousel-control-prev-icon,
|
||||
[data-bs-theme=dark].carousel .carousel-control-next-icon {
|
||||
filter: invert(1) grayscale(100);
|
||||
:root,
|
||||
[data-bs-theme=light] {
|
||||
--bs-carousel-indicator-active-bg: #fff;
|
||||
--bs-carousel-caption-color: #fff;
|
||||
--bs-carousel-control-icon-filter: ;
|
||||
}
|
||||
[data-bs-theme=dark] .carousel .carousel-indicators [data-bs-target], [data-bs-theme=dark].carousel .carousel-indicators [data-bs-target] {
|
||||
background-color: #000;
|
||||
}
|
||||
[data-bs-theme=dark] .carousel .carousel-caption, [data-bs-theme=dark].carousel .carousel-caption {
|
||||
color: #000;
|
||||
|
||||
[data-bs-theme=dark] {
|
||||
--bs-carousel-indicator-active-bg: #000;
|
||||
--bs-carousel-caption-color: #000;
|
||||
--bs-carousel-control-icon-filter: invert(1) grayscale(100);
|
||||
}
|
||||
|
||||
.spinner-grow,
|
||||
@@ -6773,7 +6775,10 @@ textarea.form-control-lg {
|
||||
}
|
||||
.offcanvas-header .btn-close {
|
||||
padding: calc(var(--bs-offcanvas-padding-y) * 0.5) calc(var(--bs-offcanvas-padding-x) * 0.5);
|
||||
margin: calc(-0.5 * var(--bs-offcanvas-padding-y)) calc(-0.5 * var(--bs-offcanvas-padding-x)) calc(-0.5 * var(--bs-offcanvas-padding-y)) auto;
|
||||
margin-top: calc(-0.5 * var(--bs-offcanvas-padding-y));
|
||||
margin-right: calc(-0.5 * var(--bs-offcanvas-padding-x));
|
||||
margin-bottom: calc(-0.5 * var(--bs-offcanvas-padding-y));
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.offcanvas-title {
|
||||
|
||||
26
src/static/scripts/datatables.css
vendored
26
src/static/scripts/datatables.css
vendored
@@ -4,13 +4,12 @@
|
||||
*
|
||||
* To rebuild or modify this file with the latest versions of the included
|
||||
* software please visit:
|
||||
* https://datatables.net/download/#bs5/dt-2.1.8
|
||||
* https://datatables.net/download/#bs5/dt-2.2.2
|
||||
*
|
||||
* Included libraries:
|
||||
* DataTables 2.1.8
|
||||
* DataTables 2.2.2
|
||||
*/
|
||||
|
||||
@charset "UTF-8";
|
||||
:root {
|
||||
--dt-row-selected: 13, 110, 253;
|
||||
--dt-row-selected-text: 255, 255, 255;
|
||||
@@ -43,6 +42,9 @@ table.dataTable tr.dt-hasChild td.dt-control:before {
|
||||
border-bottom: 0px solid transparent;
|
||||
border-right: 5px solid transparent;
|
||||
}
|
||||
table.dataTable tfoot:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
html.dark table.dataTable td.dt-control:before,
|
||||
:root[data-bs-theme=dark] table.dataTable td.dt-control:before,
|
||||
@@ -90,8 +92,8 @@ table.dataTable thead > tr > td.dt-ordering-asc span.dt-column-order:before {
|
||||
position: absolute;
|
||||
display: block;
|
||||
bottom: 50%;
|
||||
content: "▲";
|
||||
content: "▲"/"";
|
||||
content: "\25B2";
|
||||
content: "\25B2"/"";
|
||||
}
|
||||
table.dataTable thead > tr > th.dt-orderable-desc span.dt-column-order:after, table.dataTable thead > tr > th.dt-ordering-desc span.dt-column-order:after,
|
||||
table.dataTable thead > tr > td.dt-orderable-desc span.dt-column-order:after,
|
||||
@@ -99,8 +101,8 @@ table.dataTable thead > tr > td.dt-ordering-desc span.dt-column-order:after {
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: 50%;
|
||||
content: "▼";
|
||||
content: "▼"/"";
|
||||
content: "\25BC";
|
||||
content: "\25BC"/"";
|
||||
}
|
||||
table.dataTable thead > tr > th.dt-orderable-asc, table.dataTable thead > tr > th.dt-orderable-desc, table.dataTable thead > tr > th.dt-ordering-asc, table.dataTable thead > tr > th.dt-ordering-desc,
|
||||
table.dataTable thead > tr > td.dt-orderable-asc,
|
||||
@@ -251,6 +253,11 @@ table.dataTable th,
|
||||
table.dataTable td {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
table.dataTable th.dt-type-numeric, table.dataTable th.dt-type-date,
|
||||
table.dataTable td.dt-type-numeric,
|
||||
table.dataTable td.dt-type-date {
|
||||
text-align: right;
|
||||
}
|
||||
table.dataTable th.dt-left,
|
||||
table.dataTable td.dt-left {
|
||||
text-align: left;
|
||||
@@ -276,11 +283,6 @@ table.dataTable td.dt-empty {
|
||||
text-align: center;
|
||||
vertical-align: top;
|
||||
}
|
||||
table.dataTable th.dt-type-numeric, table.dataTable th.dt-type-date,
|
||||
table.dataTable td.dt-type-numeric,
|
||||
table.dataTable td.dt-type-date {
|
||||
text-align: right;
|
||||
}
|
||||
table.dataTable thead th,
|
||||
table.dataTable thead td,
|
||||
table.dataTable tfoot th,
|
||||
|
||||
503
src/static/scripts/datatables.js
vendored
503
src/static/scripts/datatables.js
vendored
@@ -4,34 +4,16 @@
|
||||
*
|
||||
* To rebuild or modify this file with the latest versions of the included
|
||||
* software please visit:
|
||||
* https://datatables.net/download/#bs5/dt-2.1.8
|
||||
* https://datatables.net/download/#bs5/dt-2.2.2
|
||||
*
|
||||
* Included libraries:
|
||||
* DataTables 2.1.8
|
||||
* DataTables 2.2.2
|
||||
*/
|
||||
|
||||
/*! DataTables 2.1.8
|
||||
/*! DataTables 2.2.2
|
||||
* © SpryMedia Ltd - datatables.net/license
|
||||
*/
|
||||
|
||||
/**
|
||||
* @summary DataTables
|
||||
* @description Paginate, search and order HTML tables
|
||||
* @version 2.1.8
|
||||
* @author SpryMedia Ltd
|
||||
* @contact www.datatables.net
|
||||
* @copyright SpryMedia Ltd.
|
||||
*
|
||||
* This source file is free software, available under the following license:
|
||||
* MIT license - https://datatables.net/license
|
||||
*
|
||||
* This source file is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the license files for details.
|
||||
*
|
||||
* For details please refer to: https://www.datatables.net
|
||||
*/
|
||||
|
||||
(function( factory ) {
|
||||
"use strict";
|
||||
|
||||
@@ -441,7 +423,6 @@
|
||||
thead = $('<thead/>').appendTo($this);
|
||||
}
|
||||
oSettings.nTHead = thead[0];
|
||||
$('tr', thead).addClass(oClasses.thead.row);
|
||||
|
||||
var tbody = $this.children('tbody');
|
||||
if ( tbody.length === 0 ) {
|
||||
@@ -456,7 +437,6 @@
|
||||
tfoot = $('<tfoot/>').appendTo($this);
|
||||
}
|
||||
oSettings.nTFoot = tfoot[0];
|
||||
$('tr', tfoot).addClass(oClasses.tfoot.row);
|
||||
|
||||
// Copy the data index array
|
||||
oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();
|
||||
@@ -539,7 +519,7 @@
|
||||
*
|
||||
* @type string
|
||||
*/
|
||||
builder: "bs5/dt-2.1.8",
|
||||
builder: "bs5/dt-2.2.2",
|
||||
|
||||
|
||||
/**
|
||||
@@ -2173,6 +2153,10 @@
|
||||
var width = _fnColumnsSumWidth(settings, [i], false, false);
|
||||
|
||||
cols[i].colEl.css('width', width);
|
||||
|
||||
if (settings.oScroll.sX) {
|
||||
cols[i].colEl.css('min-width', width);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3240,9 +3224,13 @@
|
||||
|
||||
// Add the number of cells needed to make up to the number of columns
|
||||
if (row.length === 1) {
|
||||
var cells = $('td, th', row);
|
||||
var cellCount = 0;
|
||||
|
||||
$('td, th', row).each(function () {
|
||||
cellCount += this.colSpan;
|
||||
});
|
||||
|
||||
for ( i=cells.length, ien=columns.length ; i<ien ; i++ ) {
|
||||
for ( i=cellCount, ien=columns.length ; i<ien ; i++ ) {
|
||||
$('<th/>')
|
||||
.html( columns[i][titleProp] || '' )
|
||||
.appendTo( row );
|
||||
@@ -3254,9 +3242,11 @@
|
||||
|
||||
if (side === 'header') {
|
||||
settings.aoHeader = detected;
|
||||
$('tr', target).addClass(classes.thead.row);
|
||||
}
|
||||
else {
|
||||
settings.aoFooter = detected;
|
||||
$('tr', target).addClass(classes.tfoot.row);
|
||||
}
|
||||
|
||||
// Every cell needs to be passed through the renderer
|
||||
@@ -4519,7 +4509,7 @@
|
||||
// So the array reference doesn't break set the results into the
|
||||
// existing array
|
||||
displayRows.length = 0;
|
||||
displayRows.push.apply(displayRows, rows);
|
||||
_fnArrayApply(displayRows, rows);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5247,8 +5237,11 @@
|
||||
// [].find, but it wasn't supported in Chrome until Sept 2015, and DT has 10 year
|
||||
// browser support
|
||||
var firstTr = null;
|
||||
var start = _fnDataSource( settings ) !== 'ssp'
|
||||
? settings._iDisplayStart
|
||||
: 0;
|
||||
|
||||
for (i=settings._iDisplayStart ; i<settings.aiDisplay.length ; i++) {
|
||||
for (i=start ; i<start + settings.aiDisplay.length ; i++) {
|
||||
var idx = settings.aiDisplay[i];
|
||||
var tr = settings.aoData[idx].nTr;
|
||||
|
||||
@@ -5263,7 +5256,7 @@
|
||||
return {
|
||||
idx: _fnVisibleToColumnIndex(settings, vis),
|
||||
width: $(this).outerWidth()
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// Check against what the colgroup > col is set to and correct if needed
|
||||
@@ -5273,6 +5266,10 @@
|
||||
|
||||
if (colWidth !== colSizes[i].width) {
|
||||
colEl.style.width = colSizes[i].width + 'px';
|
||||
|
||||
if (scroll.sX) {
|
||||
colEl.style.minWidth = colSizes[i].width + 'px';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5365,6 +5362,14 @@
|
||||
i, column, columnIdx;
|
||||
|
||||
var styleWidth = table.style.width;
|
||||
var containerWidth = _fnWrapperWidth(settings);
|
||||
|
||||
// Don't re-run for the same width as the last time
|
||||
if (containerWidth === settings.containerWidth) {
|
||||
return false;
|
||||
}
|
||||
|
||||
settings.containerWidth = containerWidth;
|
||||
|
||||
// If there is no width applied as a CSS style or as an attribute, we assume that
|
||||
// the width is intended to be 100%, which is usually is in CSS, but it is very
|
||||
@@ -5422,6 +5427,8 @@
|
||||
// browser will collapse it. If this width is smaller than the
|
||||
// width the column requires, then it will have no effect
|
||||
if ( scrollX ) {
|
||||
this.style.minWidth = width;
|
||||
|
||||
$( this ).append( $('<div/>').css( {
|
||||
width: width,
|
||||
margin: 0,
|
||||
@@ -5490,15 +5497,15 @@
|
||||
|
||||
// If there is no width attribute or style, then allow the table to
|
||||
// collapse
|
||||
if ( tmpTable.width() < tableContainer.clientWidth && tableWidthAttr ) {
|
||||
tmpTable.width( tableContainer.clientWidth );
|
||||
if ( tmpTable.outerWidth() < tableContainer.clientWidth && tableWidthAttr ) {
|
||||
tmpTable.outerWidth( tableContainer.clientWidth );
|
||||
}
|
||||
}
|
||||
else if ( scrollY ) {
|
||||
tmpTable.width( tableContainer.clientWidth );
|
||||
tmpTable.outerWidth( tableContainer.clientWidth );
|
||||
}
|
||||
else if ( tableWidthAttr ) {
|
||||
tmpTable.width( tableWidthAttr );
|
||||
tmpTable.outerWidth( tableWidthAttr );
|
||||
}
|
||||
|
||||
// Get the width of each column in the constructed table
|
||||
@@ -5531,20 +5538,64 @@
|
||||
}
|
||||
|
||||
if ( (tableWidthAttr || scrollX) && ! settings._reszEvt ) {
|
||||
var bindResize = function () {
|
||||
$(window).on('resize.DT-'+settings.sInstance, DataTable.util.throttle( function () {
|
||||
if (! settings.bDestroying) {
|
||||
_fnAdjustColumnSizing( settings );
|
||||
}
|
||||
} ) );
|
||||
};
|
||||
var resize = DataTable.util.throttle( function () {
|
||||
var newWidth = _fnWrapperWidth(settings);
|
||||
|
||||
bindResize();
|
||||
// Don't do it if destroying or the container width is 0
|
||||
if (! settings.bDestroying && newWidth !== 0) {
|
||||
_fnAdjustColumnSizing( settings );
|
||||
}
|
||||
} );
|
||||
|
||||
// For browsers that support it (~2020 onwards for wide support) we can watch for the
|
||||
// container changing width.
|
||||
if (window.ResizeObserver) {
|
||||
// This is a tricky beast - if the element is visible when `.observe()` is called,
|
||||
// then the callback is immediately run. Which we don't want. If the element isn't
|
||||
// visible, then it isn't run, but we want it to run when it is then made visible.
|
||||
// This flag allows the above to be satisfied.
|
||||
var first = $(settings.nTableWrapper).is(':visible');
|
||||
|
||||
// Use an empty div to attach the observer so it isn't impacted by height changes
|
||||
var resizer = $('<div>')
|
||||
.css({
|
||||
width: '100%',
|
||||
height: 0
|
||||
})
|
||||
.addClass('dt-autosize')
|
||||
.appendTo(settings.nTableWrapper);
|
||||
|
||||
settings.resizeObserver = new ResizeObserver(function (e) {
|
||||
if (first) {
|
||||
first = false;
|
||||
}
|
||||
else {
|
||||
resize();
|
||||
}
|
||||
});
|
||||
|
||||
settings.resizeObserver.observe(resizer[0]);
|
||||
}
|
||||
else {
|
||||
// For old browsers, the best we can do is listen for a window resize
|
||||
$(window).on('resize.DT-'+settings.sInstance, resize);
|
||||
}
|
||||
|
||||
settings._reszEvt = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the width of the DataTables wrapper element
|
||||
*
|
||||
* @param {*} settings DataTables settings object
|
||||
* @returns Width
|
||||
*/
|
||||
function _fnWrapperWidth(settings) {
|
||||
return $(settings.nTableWrapper).is(':visible')
|
||||
? $(settings.nTableWrapper).width()
|
||||
: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the maximum strlen for each data column
|
||||
@@ -5855,10 +5906,14 @@
|
||||
displayMaster = oSettings.aiDisplayMaster,
|
||||
aSort;
|
||||
|
||||
// Make sure the columns all have types defined
|
||||
_fnColumnTypes(oSettings);
|
||||
|
||||
// Allow a specific column to be sorted, which will _not_ alter the display
|
||||
// master
|
||||
if (col !== undefined) {
|
||||
var srcCol = oSettings.aoColumns[col];
|
||||
|
||||
aSort = [{
|
||||
src: col,
|
||||
col: col,
|
||||
@@ -6153,15 +6208,26 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort state saving uses [[idx, order]] structure.
|
||||
var sorting = [];
|
||||
_fnSortResolve(settings, sorting, settings.aaSorting );
|
||||
|
||||
/* Store the interesting variables */
|
||||
var columns = settings.aoColumns;
|
||||
var state = {
|
||||
time: +new Date(),
|
||||
start: settings._iDisplayStart,
|
||||
length: settings._iDisplayLength,
|
||||
order: $.extend( true, [], settings.aaSorting ),
|
||||
order: sorting.map(function (sort) {
|
||||
// If a column name is available, use it
|
||||
return columns[sort[0]] && columns[sort[0]].sName
|
||||
? [ columns[sort[0]].sName, sort[1] ]
|
||||
: sort.slice();
|
||||
} ),
|
||||
search: $.extend({}, settings.oPreviousSearch),
|
||||
columns: settings.aoColumns.map( function ( col, i ) {
|
||||
return {
|
||||
name: col.sName,
|
||||
visible: col.bVisible,
|
||||
search: $.extend({}, settings.aoPreSearchCols[i])
|
||||
};
|
||||
@@ -6209,6 +6275,8 @@
|
||||
function _fnImplementState ( settings, s, callback) {
|
||||
var i, ien;
|
||||
var columns = settings.aoColumns;
|
||||
var currentNames = _pluck(settings.aoColumns, 'sName');
|
||||
|
||||
settings._bLoadingState = true;
|
||||
|
||||
// When StateRestore was introduced the state could now be implemented at any time
|
||||
@@ -6238,13 +6306,6 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Number of columns have changed - all bets are off, no restore of settings
|
||||
if ( s.columns && columns.length !== s.columns.length ) {
|
||||
settings._bLoadingState = false;
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
// Store the saved state so it might be accessed at any time
|
||||
settings.oLoadedState = $.extend( true, {}, s );
|
||||
|
||||
@@ -6278,10 +6339,23 @@
|
||||
if ( s.order !== undefined ) {
|
||||
settings.aaSorting = [];
|
||||
$.each( s.order, function ( i, col ) {
|
||||
settings.aaSorting.push( col[0] >= columns.length ?
|
||||
[ 0, col[1] ] :
|
||||
col
|
||||
);
|
||||
var set = [ col[0], col[1] ];
|
||||
|
||||
// A column name was stored and should be used for restore
|
||||
if (typeof col[0] === 'string') {
|
||||
var idx = currentNames.indexOf(col[0]);
|
||||
|
||||
// Find the name from the current list of column names, or fallback to index 0
|
||||
set[0] = idx >= 0
|
||||
? idx
|
||||
: 0;
|
||||
}
|
||||
else if (set[0] >= columns.length) {
|
||||
// If a column name, but it is out of bounds, set to 0
|
||||
set[0] = 0;
|
||||
}
|
||||
|
||||
settings.aaSorting.push(set);
|
||||
} );
|
||||
}
|
||||
|
||||
@@ -6292,31 +6366,65 @@
|
||||
|
||||
// Columns
|
||||
if ( s.columns ) {
|
||||
for ( i=0, ien=s.columns.length ; i<ien ; i++ ) {
|
||||
var col = s.columns[i];
|
||||
var set = s.columns;
|
||||
var incoming = _pluck(s.columns, 'name');
|
||||
|
||||
// Visibility
|
||||
if ( col.visible !== undefined ) {
|
||||
// If the api is defined, the table has been initialised so we need to use it rather than internal settings
|
||||
if (api) {
|
||||
// Don't redraw the columns on every iteration of this loop, we will do this at the end instead
|
||||
api.column(i).visible(col.visible, false);
|
||||
// Check if it is a 2.2 style state object with a `name` property for the columns, and if
|
||||
// the name was defined. If so, then create a new array that will map the state object
|
||||
// given, to the current columns (don't bother if they are already matching tho).
|
||||
if (incoming.join('').length && incoming.join('') !== currentNames.join('')) {
|
||||
set = [];
|
||||
|
||||
// For each column, try to find the name in the incoming array
|
||||
for (i=0 ; i<currentNames.length ; i++) {
|
||||
if (currentNames[i] != '') {
|
||||
var idx = incoming.indexOf(currentNames[i]);
|
||||
|
||||
if (idx >= 0) {
|
||||
set.push(s.columns[idx]);
|
||||
}
|
||||
else {
|
||||
// No matching column name in the state's columns, so this might be a new
|
||||
// column and thus can't have a state already.
|
||||
set.push({});
|
||||
}
|
||||
}
|
||||
else {
|
||||
columns[i].bVisible = col.visible;
|
||||
// If no name, but other columns did have a name, then there is no knowing
|
||||
// where this one came from originally so it can't be restored.
|
||||
set.push({});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the number of columns to restore is different from current, then all bets are off.
|
||||
if (set.length === columns.length) {
|
||||
for ( i=0, ien=set.length ; i<ien ; i++ ) {
|
||||
var col = set[i];
|
||||
|
||||
// Visibility
|
||||
if ( col.visible !== undefined ) {
|
||||
// If the api is defined, the table has been initialised so we need to use it rather than internal settings
|
||||
if (api) {
|
||||
// Don't redraw the columns on every iteration of this loop, we will do this at the end instead
|
||||
api.column(i).visible(col.visible, false);
|
||||
}
|
||||
else {
|
||||
columns[i].bVisible = col.visible;
|
||||
}
|
||||
}
|
||||
|
||||
// Search
|
||||
if ( col.search !== undefined ) {
|
||||
$.extend( settings.aoPreSearchCols[i], col.search );
|
||||
}
|
||||
}
|
||||
|
||||
// Search
|
||||
if ( col.search !== undefined ) {
|
||||
$.extend( settings.aoPreSearchCols[i], col.search );
|
||||
// If the api is defined then we need to adjust the columns once the visibility has been changed
|
||||
if (api) {
|
||||
api.columns.adjust();
|
||||
}
|
||||
}
|
||||
|
||||
// If the api is defined then we need to adjust the columns once the visibility has been changed
|
||||
if (api) {
|
||||
api.columns.adjust();
|
||||
}
|
||||
}
|
||||
|
||||
settings._bLoadingState = false;
|
||||
@@ -6633,6 +6741,30 @@
|
||||
replace(/_ENTRIES-TOTAL_/g, settings.api.i18n('entries', '', vis) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add elements to an array as quickly as possible, but stack stafe.
|
||||
*
|
||||
* @param {*} arr Array to add the data to
|
||||
* @param {*} data Data array that is to be added
|
||||
* @returns
|
||||
*/
|
||||
function _fnArrayApply(arr, data) {
|
||||
if (! data) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Chrome can throw a max stack error if apply is called with
|
||||
// too large an array, but apply is faster.
|
||||
if (data.length < 10000) {
|
||||
arr.push.apply(arr, data);
|
||||
}
|
||||
else {
|
||||
for (i=0 ; i<data.length ; i++) {
|
||||
arr.push(data[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
@@ -6825,18 +6957,7 @@
|
||||
: settings;
|
||||
|
||||
// Initial data
|
||||
if ( data ) {
|
||||
// Chrome can throw a max stack error if apply is called with
|
||||
// too large an array, but apply is faster.
|
||||
if (data.length < 10000) {
|
||||
this.push.apply(this, data);
|
||||
}
|
||||
else {
|
||||
for (i=0 ; i<data.length ; i++) {
|
||||
this.push(data[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
_fnArrayApply(this, data);
|
||||
|
||||
// selector
|
||||
this.selector = {
|
||||
@@ -7217,7 +7338,7 @@
|
||||
selector.forEach(function (sel) {
|
||||
var inner = __table_selector(sel, a);
|
||||
|
||||
result.push.apply(result, inner);
|
||||
_fnArrayApply(result, inner);
|
||||
});
|
||||
|
||||
return result.filter( function (item) {
|
||||
@@ -8071,7 +8192,7 @@
|
||||
// Return an Api.rows() extended instance, so rows().nodes() etc can be used
|
||||
var modRows = this.rows( -1 );
|
||||
modRows.pop();
|
||||
modRows.push.apply(modRows, newRows);
|
||||
_fnArrayApply(modRows, newRows);
|
||||
|
||||
return modRows;
|
||||
} );
|
||||
@@ -8584,7 +8705,10 @@
|
||||
.map( function () {
|
||||
return _fnColumnsFromHeader( this ); // `nodes` is column index complete and in order
|
||||
} )
|
||||
.toArray();
|
||||
.toArray()
|
||||
.sort(function (a, b) {
|
||||
return a - b;
|
||||
});
|
||||
|
||||
if ( jqResult.length || ! s.nodeName ) {
|
||||
return jqResult;
|
||||
@@ -8838,6 +8962,10 @@
|
||||
|
||||
_api_register( 'columns.adjust()', function () {
|
||||
return this.iterator( 'table', function ( settings ) {
|
||||
// Force a column sizing to happen with a manual call - otherwise it can skip
|
||||
// if the size hasn't changed
|
||||
settings.containerWidth = -1;
|
||||
|
||||
_fnAdjustColumnSizing( settings );
|
||||
}, 1 );
|
||||
} );
|
||||
@@ -9403,6 +9531,10 @@
|
||||
} );
|
||||
} );
|
||||
|
||||
// Can be assigned in DateTable.use() - note luxon and moment vars are in helpers.js
|
||||
var __bootstrap;
|
||||
var __foundation;
|
||||
|
||||
/**
|
||||
* Set the libraries that DataTables uses, or the global objects.
|
||||
* Note that the arguments can be either way around (legacy support)
|
||||
@@ -9436,6 +9568,14 @@
|
||||
case 'moment':
|
||||
return __moment;
|
||||
|
||||
case 'bootstrap':
|
||||
// Use local if set, otherwise try window, which could be undefined
|
||||
return __bootstrap || window.bootstrap;
|
||||
|
||||
case 'foundation':
|
||||
// Ditto
|
||||
return __foundation || window.Foundation;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -9445,7 +9585,7 @@
|
||||
if (type === 'lib' || type === 'jq' || (module && module.fn && module.fn.jquery)) {
|
||||
$ = module;
|
||||
}
|
||||
else if (type == 'win' || (module && module.document)) {
|
||||
else if (type === 'win' || (module && module.document)) {
|
||||
window = module;
|
||||
document = module.document;
|
||||
}
|
||||
@@ -9458,6 +9598,14 @@
|
||||
else if (type === 'moment' || (module && module.isMoment)) {
|
||||
__moment = module;
|
||||
}
|
||||
else if (type === 'bootstrap' || (module && module.Modal && module.Modal.NAME === 'modal'))
|
||||
{
|
||||
// This is currently for BS5 only. BS3/4 attach to jQuery, so no need to use `.use()`
|
||||
__bootstrap = module;
|
||||
}
|
||||
else if (type === 'foundation' || (module && module.Reveal)) {
|
||||
__foundation = module;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -9709,12 +9857,14 @@
|
||||
// Function to run either once the table becomes ready or
|
||||
// immediately if it is already ready.
|
||||
return this.tables().every(function () {
|
||||
var api = this;
|
||||
|
||||
if (this.context[0]._bInitComplete) {
|
||||
fn.call(this);
|
||||
fn.call(api);
|
||||
}
|
||||
else {
|
||||
this.on('init.dt.DT', function () {
|
||||
fn.call(this);
|
||||
fn.call(api);
|
||||
});
|
||||
}
|
||||
} );
|
||||
@@ -9748,6 +9898,11 @@
|
||||
new _Api( settings ).columns().visible( true );
|
||||
}
|
||||
|
||||
// Container width change listener
|
||||
if (settings.resizeObserver) {
|
||||
settings.resizeObserver.disconnect();
|
||||
}
|
||||
|
||||
// Blitz all `DT` namespaced events (these are internal events, the
|
||||
// lowercase, `dt` events are user subscribed and they are responsible
|
||||
// for removing them
|
||||
@@ -9765,20 +9920,37 @@
|
||||
jqTable.append( tfoot );
|
||||
}
|
||||
|
||||
// Clean up the header
|
||||
$(thead).find('span.dt-column-order').remove();
|
||||
$(thead).find('span.dt-column-title').each(function () {
|
||||
var title = $(this).html();
|
||||
$(this).parent().append(title);
|
||||
$(this).remove();
|
||||
});
|
||||
|
||||
settings.colgroup.remove();
|
||||
|
||||
settings.aaSorting = [];
|
||||
settings.aaSortingFixed = [];
|
||||
_fnSortingClasses( settings );
|
||||
|
||||
$(jqTable).find('th, td').removeClass(
|
||||
$.map(DataTable.ext.type.className, function (v) {
|
||||
return v;
|
||||
}).join(' ')
|
||||
);
|
||||
|
||||
$('th, td', thead)
|
||||
.removeClass(
|
||||
orderClasses.none + ' ' +
|
||||
orderClasses.canAsc + ' ' +
|
||||
orderClasses.canDesc + ' ' +
|
||||
orderClasses.isAsc + ' ' +
|
||||
orderClasses.isDesc
|
||||
)
|
||||
.css('width', '');
|
||||
.css('width', '')
|
||||
.removeAttr('data-dt-column')
|
||||
.removeAttr('aria-sort');
|
||||
|
||||
// Add the TR elements back into the table in their original order
|
||||
jqTbody.children().detach();
|
||||
@@ -9866,7 +10038,7 @@
|
||||
* @type string
|
||||
* @default Version number
|
||||
*/
|
||||
DataTable.version = "2.1.8";
|
||||
DataTable.version = "2.2.2";
|
||||
|
||||
/**
|
||||
* Private data store, containing all of the settings objects that are
|
||||
@@ -11969,7 +12141,13 @@
|
||||
deferLoading: null,
|
||||
|
||||
/** Allow auto type detection */
|
||||
typeDetect: true
|
||||
typeDetect: true,
|
||||
|
||||
/** ResizeObserver for the container div */
|
||||
resizeObserver: null,
|
||||
|
||||
/** Keep a record of the last size of the container, so we can skip duplicates */
|
||||
containerWidth: -1
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -12111,8 +12289,8 @@
|
||||
|
||||
|
||||
var __mlWarning = false;
|
||||
var __luxon; // Can be assigned in DateTeble.use()
|
||||
var __moment; // Can be assigned in DateTeble.use()
|
||||
var __luxon; // Can be assigned in DateTable.use()
|
||||
var __moment; // Can be assigned in DateTable.use()
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -12148,7 +12326,7 @@
|
||||
return null;
|
||||
}
|
||||
|
||||
dt.setLocale(locale);
|
||||
dt = dt.setLocale(locale);
|
||||
}
|
||||
else if (! format) {
|
||||
// No format given, must be ISO
|
||||
@@ -12531,6 +12709,13 @@
|
||||
});
|
||||
}
|
||||
|
||||
var __diacriticHtmlSort = function (a, b) {
|
||||
a = _stripHtml(a);
|
||||
b = _stripHtml(b);
|
||||
|
||||
return __diacriticSort(a, b);
|
||||
}
|
||||
|
||||
//
|
||||
// Built in data types
|
||||
//
|
||||
@@ -12601,6 +12786,31 @@
|
||||
});
|
||||
|
||||
|
||||
DataTable.type('html-utf8', {
|
||||
detect: {
|
||||
allOf: function ( d ) {
|
||||
return _empty( d ) || (typeof d === 'string' && d.indexOf('<') !== -1);
|
||||
},
|
||||
oneOf: function ( d ) {
|
||||
// At least one data point must contain a `<` and a non-ASCII character
|
||||
// eslint-disable-next-line compat/compat
|
||||
return navigator.languages &&
|
||||
! _empty( d ) &&
|
||||
typeof d === 'string' &&
|
||||
d.indexOf('<') !== -1 &&
|
||||
typeof d === 'string' && d.match(/[^\x00-\x7F]/);
|
||||
}
|
||||
},
|
||||
order: {
|
||||
asc: __diacriticHtmlSort,
|
||||
desc: function (a, b) {
|
||||
return __diacriticHtmlSort(a, b) * -1;
|
||||
}
|
||||
},
|
||||
search: _filterString(true, true)
|
||||
});
|
||||
|
||||
|
||||
DataTable.type('date', {
|
||||
className: 'dt-type-date',
|
||||
detect: {
|
||||
@@ -12811,6 +13021,7 @@
|
||||
var indexes = columns.indexes();
|
||||
var sortDirs = columns.orderable(true).flatten();
|
||||
var orderedColumns = _pluck(sorting, 'col');
|
||||
var tabIndex = settings.iTabIndex;
|
||||
|
||||
cell
|
||||
.removeClass(
|
||||
@@ -12868,15 +13079,20 @@
|
||||
cell.removeAttr('aria-sort');
|
||||
}
|
||||
|
||||
cell.attr('aria-label', orderable
|
||||
? col.ariaTitle + ctx.api.i18n('oAria.orderable' + ariaType)
|
||||
: col.ariaTitle
|
||||
);
|
||||
|
||||
// Make the headers tab-able for keyboard navigation
|
||||
if (orderable) {
|
||||
cell.find('.dt-column-title').attr('role', 'button');
|
||||
cell.attr('tabindex', 0)
|
||||
var orderSpan = cell.find('.dt-column-order');
|
||||
|
||||
orderSpan
|
||||
.attr('role', 'button')
|
||||
.attr('aria-label', orderable
|
||||
? col.ariaTitle + ctx.api.i18n('oAria.orderable' + ariaType)
|
||||
: col.ariaTitle
|
||||
);
|
||||
|
||||
if (tabIndex !== -1) {
|
||||
orderSpan.attr('tabindex', tabIndex);
|
||||
}
|
||||
}
|
||||
} );
|
||||
}
|
||||
@@ -12890,7 +13106,7 @@
|
||||
.addClass(items.className || classes.row)
|
||||
.appendTo( container );
|
||||
|
||||
$.each( items, function (key, val) {
|
||||
DataTable.ext.renderer.layout._forLayoutRow(items, function (key, val) {
|
||||
if (key === 'id' || key === 'className') {
|
||||
return;
|
||||
}
|
||||
@@ -12921,7 +13137,31 @@
|
||||
})
|
||||
.append( val.contents )
|
||||
.appendTo( row );
|
||||
} );
|
||||
});
|
||||
},
|
||||
|
||||
// Shared for use by the styling frameworks
|
||||
_forLayoutRow: function (items, fn) {
|
||||
// As we are inserting dom elements, we need start / end in a
|
||||
// specific order, this function is used for sorting the layout
|
||||
// keys.
|
||||
var layoutEnum = function (x) {
|
||||
switch (x) {
|
||||
case '': return 0;
|
||||
case 'start': return 1;
|
||||
case 'end': return 2;
|
||||
default: return 3;
|
||||
}
|
||||
};
|
||||
|
||||
Object
|
||||
.keys(items)
|
||||
.sort(function (a, b) {
|
||||
return layoutEnum(a) - layoutEnum(b);
|
||||
})
|
||||
.forEach(function (key) {
|
||||
fn(key, items[key]);
|
||||
});
|
||||
}
|
||||
}
|
||||
} );
|
||||
@@ -13273,7 +13513,7 @@
|
||||
'data-dt-idx': button,
|
||||
'tabIndex': btnInfo.disabled
|
||||
? -1
|
||||
: settings.iTabIndex
|
||||
: settings.iTabIndex && btn.clicker[0].nodeName.toLowerCase() !== 'span'
|
||||
? settings.iTabIndex
|
||||
: null, // `0` doesn't need a tabIndex since it is the default
|
||||
});
|
||||
@@ -13307,12 +13547,16 @@
|
||||
|
||||
// Responsive - check if the buttons are over two lines based on the
|
||||
// height of the buttons and the container.
|
||||
if (
|
||||
buttonEls.length && // any buttons
|
||||
opts.buttons > 1 && // prevent infinite
|
||||
$(host).height() >= ($(buttonEls[0]).outerHeight() * 2) - 10
|
||||
) {
|
||||
_pagingDraw(settings, host, $.extend({}, opts, { buttons: opts.buttons - 2 }));
|
||||
if (buttonEls.length) {
|
||||
var outerHeight = $(buttonEls[0]).outerHeight();
|
||||
|
||||
if (
|
||||
opts.buttons > 1 && // prevent infinite
|
||||
outerHeight > 0 && // will be 0 if hidden
|
||||
$(host).height() >= (outerHeight * 2) - 10
|
||||
) {
|
||||
_pagingDraw(settings, host, $.extend({}, opts, { buttons: opts.buttons - 2 }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13336,7 +13580,6 @@
|
||||
switch ( button ) {
|
||||
case 'ellipsis':
|
||||
o.display = '…';
|
||||
o.disabled = true;
|
||||
break;
|
||||
|
||||
case 'first':
|
||||
@@ -13523,7 +13766,7 @@
|
||||
|
||||
// Save text node content for macro updating
|
||||
var textNodes = [];
|
||||
Array.from(div.find('label')[0].childNodes).forEach(function (el) {
|
||||
Array.prototype.slice.call(div.find('label')[0].childNodes).forEach(function (el) {
|
||||
if (el.nodeType === Node.TEXT_NODE) {
|
||||
textNodes.push({
|
||||
el: el,
|
||||
@@ -13541,7 +13784,6 @@
|
||||
|
||||
// Next, the select itself, along with the options
|
||||
var select = $('<select/>', {
|
||||
'name': tableId+'_length',
|
||||
'aria-controls': tableId,
|
||||
'class': classes.select
|
||||
} );
|
||||
@@ -13732,41 +13974,6 @@ DataTable.ext.renderer.pagingContainer.bootstrap = function (settings, buttonEls
|
||||
return $('<ul/>').addClass('pagination').append(buttonEls);
|
||||
};
|
||||
|
||||
// DataTable.ext.renderer.layout.bootstrap = function ( settings, container, items ) {
|
||||
// var row = $( '<div/>', {
|
||||
// "class": items.full ?
|
||||
// 'row mt-2 justify-content-md-center' :
|
||||
// 'row mt-2 justify-content-between'
|
||||
// } )
|
||||
// .appendTo( container );
|
||||
|
||||
// $.each( items, function (key, val) {
|
||||
// var klass;
|
||||
// var cellClass = '';
|
||||
|
||||
// // Apply start / end (left / right when ltr) margins
|
||||
// if (val.table) {
|
||||
// klass = 'col-12';
|
||||
// }
|
||||
// else if (key === 'start') {
|
||||
// klass = '' + cellClass;
|
||||
// }
|
||||
// else if (key === 'end') {
|
||||
// klass = '' + cellClass;
|
||||
// }
|
||||
// else {
|
||||
// klass = ' ' + cellClass;
|
||||
// }
|
||||
|
||||
// $( '<div/>', {
|
||||
// id: val.id || null,
|
||||
// "class": klass + ' ' + (val.className || '')
|
||||
// } )
|
||||
// .append( val.contents )
|
||||
// .appendTo( row );
|
||||
// } );
|
||||
// };
|
||||
|
||||
|
||||
return DataTable;
|
||||
}));
|
||||
|
||||
8
src/static/templates/email/register_verify_email.hbs
Normal file
8
src/static/templates/email/register_verify_email.hbs
Normal file
@@ -0,0 +1,8 @@
|
||||
Verify Your Email
|
||||
<!---------------->
|
||||
Verify this email address to finish creating your account by clicking the link below.
|
||||
|
||||
Verify Email Address Now: {{{url}}}
|
||||
|
||||
If you did not request to verify your account, you can safely ignore this email.
|
||||
{{> email/email_footer_text }}
|
||||
24
src/static/templates/email/register_verify_email.html.hbs
Normal file
24
src/static/templates/email/register_verify_email.html.hbs
Normal file
@@ -0,0 +1,24 @@
|
||||
Verify Your Email
|
||||
<!---------------->
|
||||
{{> email/email_header }}
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
|
||||
Verify this email address to finish creating your account by clicking the link below.
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
|
||||
<a href="{{{url}}}"
|
||||
clicktracking=off target="_blank" style="color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #3c8dbc; border-color: #3c8dbc; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
Verify Email Address Now
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
|
||||
If you did not request to verify your account, you can safely ignore this email.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{{> email/email_footer }}
|
||||
@@ -21,7 +21,15 @@ a[href$="/settings/sponsored-families"] {
|
||||
}
|
||||
|
||||
/* Hide the `Enterprise Single Sign-On` button on the login page */
|
||||
a[routerlink="/sso"] {
|
||||
app-root form.ng-untouched button.\!tw-text-primary-600:nth-child(4) {
|
||||
@extend %vw-hide;
|
||||
}
|
||||
/* Hide Log in with passkey on the login page */
|
||||
app-root form.ng-untouched a[routerlink="/login-with-passkey"] {
|
||||
@extend %vw-hide;
|
||||
}
|
||||
/* Hide the or text followed by the two buttons hidden above */
|
||||
app-root form.ng-untouched > div:nth-child(1) > div:nth-child(3) > div:nth-child(2) {
|
||||
@extend %vw-hide;
|
||||
}
|
||||
|
||||
@@ -31,69 +39,84 @@ a[href$="/settings/two-factor"] {
|
||||
@extend %vw-hide;
|
||||
}
|
||||
|
||||
/* Hide Business Owned checkbox */
|
||||
app-org-info > form:nth-child(1) > div:nth-child(3) {
|
||||
@extend %vw-hide;
|
||||
}
|
||||
|
||||
/* Hide the `This account is owned by a business` checkbox and label */
|
||||
#ownedBusiness,
|
||||
label[for^="ownedBusiness"] {
|
||||
@extend %vw-hide;
|
||||
}
|
||||
|
||||
/* Hide the radio button and label for the `Custom` org user type */
|
||||
#userTypeCustom,
|
||||
label[for^="userTypeCustom"] {
|
||||
@extend %vw-hide;
|
||||
}
|
||||
|
||||
/* Hide Business Name */
|
||||
app-org-account form div bit-form-field.tw-block:nth-child(3) {
|
||||
@extend %vw-hide;
|
||||
}
|
||||
|
||||
/* Hide organization plans */
|
||||
app-organization-plans > form > bit-section:nth-child(2) {
|
||||
@extend %vw-hide;
|
||||
}
|
||||
|
||||
/* Hide Collection Management Form */
|
||||
app-org-account form.ng-untouched:nth-child(5) {
|
||||
@extend %vw-hide;
|
||||
}
|
||||
|
||||
/* Hide 'Member Access' Report Card from Org Reports */
|
||||
app-org-reports-home > app-report-list > div.tw-inline-grid > div:nth-child(6) {
|
||||
@extend %vw-hide;
|
||||
}
|
||||
|
||||
/* Hide Device Verification form at the Two Step Login screen */
|
||||
app-security > app-two-factor-setup > form {
|
||||
@extend %vw-hide;
|
||||
}
|
||||
|
||||
/* Hide unsupported Custom Role options */
|
||||
bit-dialog div.tw-ml-4:has(bit-form-control input),
|
||||
bit-dialog div.tw-col-span-4:has(input[formcontrolname*="access"], input[formcontrolname*="manage"]) {
|
||||
@extend %vw-hide;
|
||||
}
|
||||
|
||||
/* Change collapsed menu icon to Vaultwarden */
|
||||
bit-nav-logo bit-nav-item a:before {
|
||||
content: "";
|
||||
background-image: url("../images/icon-white.svg");
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
height: 32px;
|
||||
display: block;
|
||||
}
|
||||
bit-nav-logo bit-nav-item .bwi-shield {
|
||||
@extend %vw-hide;
|
||||
}
|
||||
/**** END Static Vaultwarden Changes ****/
|
||||
/**** START Dynamic Vaultwarden Changes ****/
|
||||
{{#if signup_disabled}}
|
||||
/* From web vault 2025.1.2 and onwards, the signup button is hidden
|
||||
when signups are disabled as the web vault checks the /api/config endpoint.
|
||||
Note that the clients tend to aggressively cache this endpoint, so it might
|
||||
take a while for the change to take effect. To avoid the button appearing
|
||||
when it shouldn't, we'll keep this style in place for a couple of versions */
|
||||
{{#if webver "<2025.3.0"}}
|
||||
/* Hide the register link on the login screen */
|
||||
app-frontend-layout > app-login > form > div > div > div > p {
|
||||
app-login form div + div + div + div + hr,
|
||||
app-login form div + div + div + div + hr + p {
|
||||
@extend %vw-hide;
|
||||
}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
/* Hide `Email` 2FA if mail is not enabled */
|
||||
{{#unless mail_enabled}}
|
||||
app-two-factor-setup ul.list-group.list-group-2fa li.list-group-item:nth-child(5) {
|
||||
/* Hide `Email` 2FA if mail is not enabled */
|
||||
.providers-2fa-1 {
|
||||
@extend %vw-hide;
|
||||
}
|
||||
{{/unless}}
|
||||
|
||||
/* Hide `YubiKey OTP security key` 2FA if it is not enabled */
|
||||
{{#unless yubico_enabled}}
|
||||
app-two-factor-setup ul.list-group.list-group-2fa li.list-group-item:nth-child(2) {
|
||||
/* Hide `YubiKey OTP security key` 2FA if it is not enabled */
|
||||
.providers-2fa-3 {
|
||||
@extend %vw-hide;
|
||||
}
|
||||
{{/unless}}
|
||||
|
||||
/* Hide Emergency Access if not allowed */
|
||||
{{#unless emergency_access_allowed}}
|
||||
/* Hide Emergency Access if not allowed */
|
||||
bit-nav-item[route="settings/emergency-access"] {
|
||||
@extend %vw-hide;
|
||||
}
|
||||
{{/unless}}
|
||||
|
||||
/* Hide Sends if not allowed */
|
||||
{{#unless sends_allowed}}
|
||||
/* Hide Sends if not allowed */
|
||||
bit-nav-item[route="sends"] {
|
||||
@extend %vw-hide;
|
||||
}
|
||||
|
||||
90
src/util.rs
90
src/util.rs
@@ -1,13 +1,12 @@
|
||||
//
|
||||
// Web Headers and caching
|
||||
//
|
||||
use std::{collections::HashMap, io::Cursor, ops::Deref, path::Path};
|
||||
use std::{collections::HashMap, io::Cursor, path::Path};
|
||||
|
||||
use num_traits::ToPrimitive;
|
||||
use rocket::{
|
||||
fairing::{Fairing, Info, Kind},
|
||||
http::{ContentType, Header, HeaderMap, Method, Status},
|
||||
request::FromParam,
|
||||
response::{self, Responder},
|
||||
Data, Orbit, Request, Response, Rocket,
|
||||
};
|
||||
@@ -56,9 +55,18 @@ impl Fairing for AppHeaders {
|
||||
res.set_raw_header("Referrer-Policy", "same-origin");
|
||||
res.set_raw_header("X-Content-Type-Options", "nosniff");
|
||||
res.set_raw_header("X-Robots-Tag", "noindex, nofollow");
|
||||
|
||||
// Obsolete in modern browsers, unsafe (XS-Leak), and largely replaced by CSP
|
||||
res.set_raw_header("X-XSS-Protection", "0");
|
||||
|
||||
// The `Cross-Origin-Resource-Policy` header should not be set on images or on the `icon_external` route.
|
||||
// Otherwise some clients, like the Bitwarden Desktop, will fail to download the icons
|
||||
if !(res.headers().get_one("Content-Type").is_some_and(|v| v.starts_with("image/"))
|
||||
|| req.route().is_some_and(|v| v.name.as_deref() == Some("icon_external")))
|
||||
{
|
||||
res.set_raw_header("Cross-Origin-Resource-Policy", "same-origin");
|
||||
}
|
||||
|
||||
// Do not send the Content-Security-Policy (CSP) Header and X-Frame-Options for the *-connector.html files.
|
||||
// This can cause issues when some MFA requests needs to open a popup or page within the clients like WebAuthn, or Duo.
|
||||
// This is the same behavior as upstream Bitwarden.
|
||||
@@ -75,7 +83,9 @@ impl Fairing for AppHeaders {
|
||||
// # Mail Relay: https://bitwarden.com/blog/add-privacy-and-security-using-email-aliases-with-bitwarden/
|
||||
// app.simplelogin.io, app.addy.io, api.fastmail.com, quack.duckduckgo.com
|
||||
let csp = format!(
|
||||
"default-src 'self'; \
|
||||
"default-src 'none'; \
|
||||
font-src 'self'; \
|
||||
manifest-src 'self'; \
|
||||
base-uri 'self'; \
|
||||
form-action 'self'; \
|
||||
object-src 'self' blob:; \
|
||||
@@ -98,10 +108,11 @@ impl Fairing for AppHeaders {
|
||||
https://app.addy.io/api/ \
|
||||
https://api.fastmail.com/ \
|
||||
https://api.forwardemail.net \
|
||||
;\
|
||||
{allowed_connect_src};\
|
||||
",
|
||||
icon_service_csp = CONFIG._icon_service_csp(),
|
||||
allowed_iframe_ancestors = CONFIG.allowed_iframe_ancestors()
|
||||
allowed_iframe_ancestors = CONFIG.allowed_iframe_ancestors(),
|
||||
allowed_connect_src = CONFIG.allowed_connect_src(),
|
||||
);
|
||||
res.set_raw_header("Content-Security-Policy", csp);
|
||||
res.set_raw_header("X-Frame-Options", "SAMEORIGIN");
|
||||
@@ -222,42 +233,6 @@ impl<'r, R: 'r + Responder<'r, 'static> + Send> Responder<'r, 'static> for Cache
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SafeString(String);
|
||||
|
||||
impl fmt::Display for SafeString {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for SafeString {
|
||||
type Target = String;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<Path> for SafeString {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &Path {
|
||||
Path::new(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'r> FromParam<'r> for SafeString {
|
||||
type Error = ();
|
||||
|
||||
#[inline(always)]
|
||||
fn from_param(param: &'r str) -> Result<Self, Self::Error> {
|
||||
if param.chars().all(|c| matches!(c, 'a'..='z' | 'A'..='Z' |'0'..='9' | '-')) {
|
||||
Ok(SafeString(param.to_string()))
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Log all the routes from the main paths list, and the attachments endpoint
|
||||
// Effectively ignores, any static file route, and the alive endpoint
|
||||
const LOGGED_ROUTES: [&str; 7] = ["/api", "/admin", "/identity", "/icons", "/attachments", "/events", "/notifications"];
|
||||
@@ -293,8 +268,8 @@ impl Fairing for BetterLogging {
|
||||
} else {
|
||||
"http"
|
||||
};
|
||||
let addr = format!("{}://{}:{}", &scheme, &config.address, &config.port);
|
||||
info!(target: "start", "Rocket has launched from {}", addr);
|
||||
let addr = format!("{scheme}://{}:{}", &config.address, &config.port);
|
||||
info!(target: "start", "Rocket has launched from {addr}");
|
||||
}
|
||||
|
||||
async fn on_request(&self, request: &mut Request<'_>, _data: &mut Data<'_>) {
|
||||
@@ -308,8 +283,8 @@ impl Fairing for BetterLogging {
|
||||
let uri_subpath = uri_path_str.strip_prefix(&CONFIG.domain_path()).unwrap_or(&uri_path_str);
|
||||
if self.0 || LOGGED_ROUTES.iter().any(|r| uri_subpath.starts_with(r)) {
|
||||
match uri.query() {
|
||||
Some(q) => info!(target: "request", "{} {}?{}", method, uri_path_str, &q[..q.len().min(30)]),
|
||||
None => info!(target: "request", "{} {}", method, uri_path_str),
|
||||
Some(q) => info!(target: "request", "{method} {uri_path_str}?{}", &q[..q.len().min(30)]),
|
||||
None => info!(target: "request", "{method} {uri_path_str}"),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -324,9 +299,9 @@ impl Fairing for BetterLogging {
|
||||
if self.0 || LOGGED_ROUTES.iter().any(|r| uri_subpath.starts_with(r)) {
|
||||
let status = response.status();
|
||||
if let Some(ref route) = request.route() {
|
||||
info!(target: "response", "{} => {}", route, status)
|
||||
info!(target: "response", "{route} => {status}")
|
||||
} else {
|
||||
info!(target: "response", "{}", status)
|
||||
info!(target: "response", "{status}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -351,7 +326,7 @@ pub fn get_display_size(size: i64) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
format!("{:.2} {}", size, UNITS[unit_counter])
|
||||
format!("{size:.2} {}", UNITS[unit_counter])
|
||||
}
|
||||
|
||||
pub fn get_uuid() -> String {
|
||||
@@ -497,6 +472,23 @@ pub fn parse_date(date: &str) -> NaiveDateTime {
|
||||
DateTime::parse_from_rfc3339(date).unwrap().naive_utc()
|
||||
}
|
||||
|
||||
/// Returns true or false if an email address is valid or not
|
||||
///
|
||||
/// Some extra checks instead of only using email_address
|
||||
/// This prevents from weird email formats still excepted but in the end invalid
|
||||
pub fn is_valid_email(email: &str) -> bool {
|
||||
let Ok(email) = email_address::EmailAddress::from_str(email) else {
|
||||
return false;
|
||||
};
|
||||
let Ok(email_url) = url::Url::parse(&format!("https://{}", email.domain())) else {
|
||||
return false;
|
||||
};
|
||||
if email_url.path().ne("/") || email_url.domain().is_none() || email_url.query().is_some() {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
//
|
||||
// Deployment environment methods
|
||||
//
|
||||
@@ -707,7 +699,7 @@ where
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
warn!("Can't connect to database, retrying: {:?}", e);
|
||||
warn!("Can't connect to database, retrying: {e:?}");
|
||||
|
||||
sleep(Duration::from_millis(1_000)).await;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user