mirror of
https://github.com/dani-garcia/vaultwarden.wiki.git
synced 2026-07-26 00:35:05 +03:00
Compare commits
34
Commits
602b18fdd6
..
1.34.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0198d8d7c | ||
|
|
dfad931dca | ||
|
|
25865efd79 | ||
|
|
bcf627930e | ||
|
|
ce70cd2cf4 | ||
|
|
2ac589d4b4 | ||
|
|
b2e2aef7de | ||
|
|
0755bb19c0 | ||
|
|
fee0c1c711 | ||
|
|
f58539f0b4 | ||
|
|
e718afb441 | ||
|
|
55945ad793 | ||
|
|
4fd22d8e3b | ||
|
|
d6a8fb8e48 | ||
|
|
3b48e6e903 | ||
|
|
6b9333b33e | ||
|
|
a545636ee5 | ||
|
|
f125d5f1a1 | ||
|
|
ad75ce281e | ||
|
|
9059437c35 | ||
|
|
c84db0daca | ||
|
|
72adc239f5 | ||
|
|
34ebeeca76 | ||
|
|
0469d9ba4c | ||
|
|
eaa6ad06ed | ||
|
|
0d3f283c37 | ||
|
|
51a1d641c5 | ||
|
|
90f7e5ff80 | ||
|
|
200999c94e | ||
|
|
d363e647e9 | ||
|
|
53f58b14d5 | ||
|
|
ef7835d1b0 | ||
|
|
3a44dc963b | ||
|
|
a039e227c7 |
+25
-16
@@ -15,6 +15,14 @@
|
|||||||
####################
|
####################
|
||||||
|
|
||||||
## Main data folder
|
## Main data folder
|
||||||
|
## This can be a path to local folder or a path to an external location
|
||||||
|
## depending on features enabled at build time. Possible external locations:
|
||||||
|
##
|
||||||
|
## - AWS S3 Bucket (via `s3` feature): s3://bucket-name/path/to/folder
|
||||||
|
##
|
||||||
|
## When using an external location, make sure to set TMP_FOLDER,
|
||||||
|
## TEMPLATES_FOLDER, and DATABASE_URL to local paths and/or a remote database
|
||||||
|
## location.
|
||||||
# DATA_FOLDER=data
|
# DATA_FOLDER=data
|
||||||
|
|
||||||
## Individual folders, these override %DATA_FOLDER%
|
## Individual folders, these override %DATA_FOLDER%
|
||||||
@@ -22,10 +30,13 @@
|
|||||||
# ICON_CACHE_FOLDER=data/icon_cache
|
# ICON_CACHE_FOLDER=data/icon_cache
|
||||||
# ATTACHMENTS_FOLDER=data/attachments
|
# ATTACHMENTS_FOLDER=data/attachments
|
||||||
# SENDS_FOLDER=data/sends
|
# SENDS_FOLDER=data/sends
|
||||||
|
|
||||||
|
## Temporary folder used for storing temporary file uploads
|
||||||
|
## Must be a local path.
|
||||||
# TMP_FOLDER=data/tmp
|
# TMP_FOLDER=data/tmp
|
||||||
|
|
||||||
## Templates data folder, by default uses embedded templates
|
## HTML template overrides data folder
|
||||||
## Check source code to see the format
|
## Must be a local path.
|
||||||
# TEMPLATES_FOLDER=data/templates
|
# TEMPLATES_FOLDER=data/templates
|
||||||
## Automatically reload the templates for every request, slow, use only for development
|
## Automatically reload the templates for every request, slow, use only for development
|
||||||
# RELOAD_TEMPLATES=false
|
# RELOAD_TEMPLATES=false
|
||||||
@@ -39,7 +50,9 @@
|
|||||||
#########################
|
#########################
|
||||||
|
|
||||||
## Database URL
|
## Database URL
|
||||||
## When using SQLite, this is the path to the DB file, default to %DATA_FOLDER%/db.sqlite3
|
## When using SQLite, this is the path to the DB file, and it defaults to
|
||||||
|
## %DATA_FOLDER%/db.sqlite3. If DATA_FOLDER is set to an external location, this
|
||||||
|
## must be set to a local sqlite3 file path.
|
||||||
# DATABASE_URL=data/db.sqlite3
|
# DATABASE_URL=data/db.sqlite3
|
||||||
## When using MySQL, specify an appropriate connection URI.
|
## When using MySQL, specify an appropriate connection URI.
|
||||||
## Details: https://docs.diesel.rs/2.1.x/diesel/mysql/struct.MysqlConnection.html
|
## Details: https://docs.diesel.rs/2.1.x/diesel/mysql/struct.MysqlConnection.html
|
||||||
@@ -117,7 +130,7 @@
|
|||||||
## and are always in terms of UTC time (regardless of your local time zone settings).
|
## and are always in terms of UTC time (regardless of your local time zone settings).
|
||||||
##
|
##
|
||||||
## The schedule format is a bit different from crontab as crontab does not contains seconds.
|
## The schedule format is a bit different from crontab as crontab does not contains seconds.
|
||||||
## You can test the the format here: https://crontab.guru, but remove the first digit!
|
## You can test the format here: https://crontab.guru, but remove the first digit!
|
||||||
## SEC MIN HOUR DAY OF MONTH MONTH DAY OF WEEK
|
## SEC MIN HOUR DAY OF MONTH MONTH DAY OF WEEK
|
||||||
## "0 30 9,12,15 1,15 May-Aug Mon,Wed,Fri"
|
## "0 30 9,12,15 1,15 May-Aug Mon,Wed,Fri"
|
||||||
## "0 30 * * * * "
|
## "0 30 * * * * "
|
||||||
@@ -260,7 +273,7 @@
|
|||||||
## A comma-separated list means only those users can create orgs:
|
## A comma-separated list means only those users can create orgs:
|
||||||
# ORG_CREATION_USERS=admin1@example.com,admin2@example.com
|
# ORG_CREATION_USERS=admin1@example.com,admin2@example.com
|
||||||
|
|
||||||
## Invitations org admins to invite users, even when signups are disabled
|
## Allows org admins to invite users, even when signups are disabled
|
||||||
# INVITATIONS_ALLOWED=true
|
# INVITATIONS_ALLOWED=true
|
||||||
## Name shown in the invitation emails that don't come from a specific organization
|
## Name shown in the invitation emails that don't come from a specific organization
|
||||||
# INVITATION_ORG_NAME=Vaultwarden
|
# INVITATION_ORG_NAME=Vaultwarden
|
||||||
@@ -328,37 +341,33 @@
|
|||||||
|
|
||||||
## Icon download timeout
|
## Icon download timeout
|
||||||
## Configure the timeout value when downloading the favicons.
|
## Configure the timeout value when downloading the favicons.
|
||||||
## The default is 10 seconds, but this could be to low on slower network connections
|
## The default is 10 seconds, but this could be too low on slower network connections
|
||||||
# ICON_DOWNLOAD_TIMEOUT=10
|
# ICON_DOWNLOAD_TIMEOUT=10
|
||||||
|
|
||||||
## Block HTTP domains/IPs by Regex
|
## Block HTTP domains/IPs by Regex
|
||||||
## Any domains or IPs that match this regex won't be fetched by the internal HTTP client.
|
## Any domains or IPs that match this regex won't be fetched by the internal HTTP client.
|
||||||
## Useful to hide other servers in the local network. Check the WIKI for more details
|
## Useful to hide other servers in the local network. Check the WIKI for more details
|
||||||
## NOTE: Always enclose this regex withing single quotes!
|
## NOTE: Always enclose this regex within single quotes!
|
||||||
# HTTP_REQUEST_BLOCK_REGEX='^(192\.168\.0\.[0-9]+|192\.168\.1\.[0-9]+)$'
|
# HTTP_REQUEST_BLOCK_REGEX='^(192\.168\.0\.[0-9]+|192\.168\.1\.[0-9]+)$'
|
||||||
|
|
||||||
## Enabling this will cause the internal HTTP client to refuse to connect to any non global IP address.
|
## Enabling this will cause the internal HTTP client to refuse to connect to any non-global IP address.
|
||||||
## Useful to secure your internal environment: See https://en.wikipedia.org/wiki/Reserved_IP_addresses for a list of IPs which it will block
|
## Useful to secure your internal environment: See https://en.wikipedia.org/wiki/Reserved_IP_addresses for a list of IPs which it will block
|
||||||
# HTTP_REQUEST_BLOCK_NON_GLOBAL_IPS=true
|
# HTTP_REQUEST_BLOCK_NON_GLOBAL_IPS=true
|
||||||
|
|
||||||
## Client Settings
|
## Client Settings
|
||||||
## Enable experimental feature flags for clients.
|
## Enable experimental feature flags for clients.
|
||||||
## This is a comma-separated list of flags, e.g. "flag1,flag2,flag3".
|
## This is a comma-separated list of flags, e.g. "flag1,flag2,flag3".
|
||||||
|
## Note that clients cache the /api/config endpoint for about 1 hour and it could take some time before they are enabled or disabled!
|
||||||
##
|
##
|
||||||
## The following flags are available:
|
## The following flags are available:
|
||||||
## - "autofill-overlay": Add an overlay menu to form fields for quick access to credentials.
|
|
||||||
## - "autofill-v2": Use the new autofill implementation.
|
|
||||||
## - "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.
|
## - "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)
|
## - "inline-menu-totp": Enable the use of inline menu TOTP codes in the browser extension.
|
||||||
## - "ssh-agent": Enable SSH agent support on Desktop. (Needs desktop >=2024.12.0)
|
## - "ssh-agent": Enable SSH agent support on Desktop. (Needs desktop >=2024.12.0)
|
||||||
|
## - "ssh-key-vault-item": Enable the creation and use of SSH key vault items. (Needs clients >=2024.12.0)
|
||||||
|
## - "export-attachments": Enable support for exporting attachments (Clients >=2025.4.0)
|
||||||
## - "anon-addy-self-host-alias": Enable configuring self-hosted Anon Addy alias generator. (Needs Android >=2025.3.0, iOS >=2025.4.0)
|
## - "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)
|
## - "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)
|
## - "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
|
# EXPERIMENTAL_CLIENT_FEATURE_FLAGS=fido2-vault-credentials
|
||||||
|
|
||||||
## Require new device emails. When a user logs in an email is required to be sent.
|
## Require new device emails. When a user logs in an email is required to be sent.
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/.github @dani-garcia @BlackDex
|
/.github @dani-garcia @BlackDex
|
||||||
/.github/** @dani-garcia @BlackDex
|
/.github/** @dani-garcia @BlackDex
|
||||||
/.github/CODEOWNERS @dani-garcia @BlackDex
|
/.github/CODEOWNERS @dani-garcia @BlackDex
|
||||||
|
/.github/ISSUE_TEMPLATE/** @dani-garcia @BlackDex
|
||||||
/.github/workflows/** @dani-garcia @BlackDex
|
/.github/workflows/** @dani-garcia @BlackDex
|
||||||
/SECURITY.md @dani-garcia @BlackDex
|
/SECURITY.md @dani-garcia @BlackDex
|
||||||
|
|||||||
@@ -8,15 +8,30 @@ body:
|
|||||||
value: |
|
value: |
|
||||||
Thanks for taking the time to fill out this bug report!
|
Thanks for taking the time to fill out this bug report!
|
||||||
|
|
||||||
Please *do not* submit feature requests or ask for help on how to configure Vaultwarden here.
|
Please **do not** submit feature requests or ask for help on how to configure Vaultwarden here!
|
||||||
|
|
||||||
The [GitHub Discussions](https://github.com/dani-garcia/vaultwarden/discussions/) has sections for Questions and Ideas.
|
The [GitHub Discussions](https://github.com/dani-garcia/vaultwarden/discussions/) has sections for Questions and Ideas.
|
||||||
|
|
||||||
|
Our [Wiki](https://github.com/dani-garcia/vaultwarden/wiki/) has topics on how to configure Vaultwarden.
|
||||||
|
|
||||||
Also, make sure you are running [](https://github.com/dani-garcia/vaultwarden/releases/latest) of Vaultwarden!
|
Also, make sure you are running [](https://github.com/dani-garcia/vaultwarden/releases/latest) of Vaultwarden!
|
||||||
And search for existing open or closed issues or discussions regarding your topic before posting.
|
|
||||||
|
|
||||||
Be sure to check and validate the Vaultwarden Admin Diagnostics (`/admin/diagnostics`) page for any errors!
|
Be sure to check and validate the Vaultwarden Admin Diagnostics (`/admin/diagnostics`) page for any errors!
|
||||||
See here [how to enable the admin page](https://github.com/dani-garcia/vaultwarden/wiki/Enabling-admin-page).
|
See here [how to enable the admin page](https://github.com/dani-garcia/vaultwarden/wiki/Enabling-admin-page).
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> ## :bangbang: Search for existing **Closed _AND_ Open** [Issues](https://github.com/dani-garcia/vaultwarden/issues?q=is%3Aissue%20) **_AND_** [Discussions](https://github.com/dani-garcia/vaultwarden/discussions?discussions_q=) regarding your topic before posting! :bangbang:
|
||||||
|
#
|
||||||
|
- type: checkboxes
|
||||||
|
id: checklist
|
||||||
|
attributes:
|
||||||
|
label: Prerequisites
|
||||||
|
description: Please confirm you have completed the following before submitting an issue!
|
||||||
|
options:
|
||||||
|
- label: I have searched the existing **Closed _AND_ Open** [Issues](https://github.com/dani-garcia/vaultwarden/issues?q=is%3Aissue%20) **_AND_** [Discussions](https://github.com/dani-garcia/vaultwarden/discussions?discussions_q=)
|
||||||
|
required: true
|
||||||
|
- label: I have searched and read the [documentation](https://github.com/dani-garcia/vaultwarden/wiki/)
|
||||||
|
required: true
|
||||||
#
|
#
|
||||||
- id: support-string
|
- id: support-string
|
||||||
type: textarea
|
type: textarea
|
||||||
@@ -36,7 +51,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Vaultwarden Build Version
|
label: Vaultwarden Build Version
|
||||||
description: What version of Vaultwarden are you running?
|
description: What version of Vaultwarden are you running?
|
||||||
placeholder: ex. v1.31.0 or v1.32.0-3466a804
|
placeholder: ex. v1.34.0 or v1.34.1-53f58b14
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
#
|
#
|
||||||
@@ -67,7 +82,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Reverse Proxy
|
label: Reverse Proxy
|
||||||
description: Are you using a reverse proxy, if so which and what version?
|
description: Are you using a reverse proxy, if so which and what version?
|
||||||
placeholder: ex. nginx 1.26.2, caddy 2.8.4, traefik 3.1.2, haproxy 3.0
|
placeholder: ex. nginx 1.29.0, caddy 2.10.0, traefik 3.4.4, haproxy 3.2
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
#
|
#
|
||||||
@@ -115,7 +130,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Client Version
|
label: Client Version
|
||||||
description: What version(s) of the client(s) are you seeing the problem on?
|
description: What version(s) of the client(s) are you seeing the problem on?
|
||||||
placeholder: ex. CLI v2024.7.2, Firefox 130 - v2024.7.0
|
placeholder: ex. CLI v2025.7.0, Firefox 140 - v2025.6.1
|
||||||
#
|
#
|
||||||
- id: reproduce
|
- id: reproduce
|
||||||
type: textarea
|
type: textarea
|
||||||
|
|||||||
@@ -66,13 +66,15 @@ jobs:
|
|||||||
- name: Init Variables
|
- name: Init Variables
|
||||||
id: toolchain
|
id: toolchain
|
||||||
shell: bash
|
shell: bash
|
||||||
|
env:
|
||||||
|
CHANNEL: ${{ matrix.channel }}
|
||||||
run: |
|
run: |
|
||||||
if [[ "${{ matrix.channel }}" == 'rust-toolchain' ]]; then
|
if [[ "${CHANNEL}" == 'rust-toolchain' ]]; then
|
||||||
RUST_TOOLCHAIN="$(grep -oP 'channel.*"(\K.*?)(?=")' rust-toolchain.toml)"
|
RUST_TOOLCHAIN="$(grep -oP 'channel.*"(\K.*?)(?=")' rust-toolchain.toml)"
|
||||||
elif [[ "${{ matrix.channel }}" == 'msrv' ]]; then
|
elif [[ "${CHANNEL}" == 'msrv' ]]; then
|
||||||
RUST_TOOLCHAIN="$(grep -oP 'rust-version.*"(\K.*?)(?=")' Cargo.toml)"
|
RUST_TOOLCHAIN="$(grep -oP 'rust-version.*"(\K.*?)(?=")' Cargo.toml)"
|
||||||
else
|
else
|
||||||
RUST_TOOLCHAIN="${{ matrix.channel }}"
|
RUST_TOOLCHAIN="${CHANNEL}"
|
||||||
fi
|
fi
|
||||||
echo "RUST_TOOLCHAIN=${RUST_TOOLCHAIN}" | tee -a "${GITHUB_OUTPUT}"
|
echo "RUST_TOOLCHAIN=${RUST_TOOLCHAIN}" | tee -a "${GITHUB_OUTPUT}"
|
||||||
# End Determine rust-toolchain version
|
# End Determine rust-toolchain version
|
||||||
@@ -80,7 +82,7 @@ jobs:
|
|||||||
|
|
||||||
# Only install the clippy and rustfmt components on the default rust-toolchain
|
# Only install the clippy and rustfmt components on the default rust-toolchain
|
||||||
- name: "Install rust-toolchain version"
|
- name: "Install rust-toolchain version"
|
||||||
uses: dtolnay/rust-toolchain@56f84321dbccf38fb67ce29ab63e4754056677e0 # master @ Mar 18, 2025, 8:14 PM GMT+1
|
uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # master @ Apr 29, 2025, 9:22 PM GMT+2
|
||||||
if: ${{ matrix.channel == 'rust-toolchain' }}
|
if: ${{ matrix.channel == 'rust-toolchain' }}
|
||||||
with:
|
with:
|
||||||
toolchain: "${{steps.toolchain.outputs.RUST_TOOLCHAIN}}"
|
toolchain: "${{steps.toolchain.outputs.RUST_TOOLCHAIN}}"
|
||||||
@@ -90,7 +92,7 @@ jobs:
|
|||||||
|
|
||||||
# Install the any other channel to be used for which we do not execute clippy and rustfmt
|
# Install the any other channel to be used for which we do not execute clippy and rustfmt
|
||||||
- name: "Install MSRV version"
|
- name: "Install MSRV version"
|
||||||
uses: dtolnay/rust-toolchain@56f84321dbccf38fb67ce29ab63e4754056677e0 # master @ Mar 18, 2025, 8:14 PM GMT+1
|
uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # master @ Apr 29, 2025, 9:22 PM GMT+2
|
||||||
if: ${{ matrix.channel != 'rust-toolchain' }}
|
if: ${{ matrix.channel != 'rust-toolchain' }}
|
||||||
with:
|
with:
|
||||||
toolchain: "${{steps.toolchain.outputs.RUST_TOOLCHAIN}}"
|
toolchain: "${{steps.toolchain.outputs.RUST_TOOLCHAIN}}"
|
||||||
@@ -115,7 +117,7 @@ jobs:
|
|||||||
|
|
||||||
# Enable Rust Caching
|
# Enable Rust Caching
|
||||||
- name: Rust Caching
|
- name: Rust Caching
|
||||||
uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
|
uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||||
with:
|
with:
|
||||||
# Use a custom prefix-key to force a fresh start. This is sometimes needed with bigger changes.
|
# Use a custom prefix-key to force a fresh start. This is sometimes needed with bigger changes.
|
||||||
# Like changing the build host from Ubuntu 20.04 to 22.04 for example.
|
# Like changing the build host from Ubuntu 20.04 to 22.04 for example.
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ permissions: {}
|
|||||||
on: [ push, pull_request ]
|
on: [ push, pull_request ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docker-templates:
|
docker-templates:
|
||||||
|
name: Validate docker templates
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
@@ -20,7 +21,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Run make to rebuild templates
|
- name: Run make to rebuild templates
|
||||||
working-directory: docker
|
working-directory: docker
|
||||||
run: make
|
run: make
|
||||||
|
|
||||||
- name: Check for unstaged changes
|
- name: Check for unstaged changes
|
||||||
working-directory: docker
|
working-directory: docker
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
# Start Docker Buildx
|
# Start Docker Buildx
|
||||||
- name: Setup Docker Buildx
|
- name: Setup Docker Buildx
|
||||||
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
|
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||||
# https://github.com/moby/buildkit/issues/3969
|
# https://github.com/moby/buildkit/issues/3969
|
||||||
# Also set max parallelism to 2, the default of 4 breaks GitHub Actions and causes OOMKills
|
# Also set max parallelism to 2, the default of 4 breaks GitHub Actions and causes OOMKills
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ jobs:
|
|||||||
# Start a local docker registry to extract the compiled binaries to upload as artifacts and attest them
|
# Start a local docker registry to extract the compiled binaries to upload as artifacts and attest them
|
||||||
services:
|
services:
|
||||||
registry:
|
registry:
|
||||||
image: registry:2
|
image: registry@sha256:1fc7de654f2ac1247f0b67e8a459e273b0993be7d2beda1f3f56fbf1001ed3e7 # v3.0.0
|
||||||
ports:
|
ports:
|
||||||
- 5000:5000
|
- 5000:5000
|
||||||
env:
|
env:
|
||||||
@@ -76,7 +76,7 @@ jobs:
|
|||||||
|
|
||||||
# Start Docker Buildx
|
# Start Docker Buildx
|
||||||
- name: Setup Docker Buildx
|
- name: Setup Docker Buildx
|
||||||
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
|
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||||
# https://github.com/moby/buildkit/issues/3969
|
# https://github.com/moby/buildkit/issues/3969
|
||||||
# Also set max parallelism to 2, the default of 4 breaks GitHub Actions and causes OOMKills
|
# Also set max parallelism to 2, the default of 4 breaks GitHub Actions and causes OOMKills
|
||||||
with:
|
with:
|
||||||
@@ -192,7 +192,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Bake ${{ matrix.base_image }} containers
|
- name: Bake ${{ matrix.base_image }} containers
|
||||||
id: bake_vw
|
id: bake_vw
|
||||||
uses: docker/bake-action@4ba453fbc2db7735392b93edf935aaf9b1e8f747 # v6.5.0
|
uses: docker/bake-action@37816e747588cb137173af99ab33873600c46ea8 # v6.8.0
|
||||||
env:
|
env:
|
||||||
BASE_TAGS: "${{ env.BASE_TAGS }}"
|
BASE_TAGS: "${{ env.BASE_TAGS }}"
|
||||||
SOURCE_COMMIT: "${{ env.SOURCE_COMMIT }}"
|
SOURCE_COMMIT: "${{ env.SOURCE_COMMIT }}"
|
||||||
@@ -213,14 +213,15 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
BAKE_METADATA: ${{ steps.bake_vw.outputs.metadata }}
|
BAKE_METADATA: ${{ steps.bake_vw.outputs.metadata }}
|
||||||
|
BASE_IMAGE: ${{ matrix.base_image }}
|
||||||
run: |
|
run: |
|
||||||
GET_DIGEST_SHA="$(jq -r '.["${{ matrix.base_image }}-multi"]."containerimage.digest"' <<< "${BAKE_METADATA}")"
|
GET_DIGEST_SHA="$(jq -r --arg base "$BASE_IMAGE" '.[$base + "-multi"]."containerimage.digest"' <<< "${BAKE_METADATA}")"
|
||||||
echo "DIGEST_SHA=${GET_DIGEST_SHA}" | tee -a "${GITHUB_ENV}"
|
echo "DIGEST_SHA=${GET_DIGEST_SHA}" | tee -a "${GITHUB_ENV}"
|
||||||
|
|
||||||
# Attest container images
|
# Attest container images
|
||||||
- name: Attest - docker.io - ${{ matrix.base_image }}
|
- name: Attest - docker.io - ${{ matrix.base_image }}
|
||||||
if: ${{ env.HAVE_DOCKERHUB_LOGIN == 'true' && steps.bake_vw.outputs.metadata != ''}}
|
if: ${{ env.HAVE_DOCKERHUB_LOGIN == 'true' && steps.bake_vw.outputs.metadata != ''}}
|
||||||
uses: actions/attest-build-provenance@c074443f1aee8d4aeeae555aebba3282517141b2 # v2.2.3
|
uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0
|
||||||
with:
|
with:
|
||||||
subject-name: ${{ vars.DOCKERHUB_REPO }}
|
subject-name: ${{ vars.DOCKERHUB_REPO }}
|
||||||
subject-digest: ${{ env.DIGEST_SHA }}
|
subject-digest: ${{ env.DIGEST_SHA }}
|
||||||
@@ -228,7 +229,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Attest - ghcr.io - ${{ matrix.base_image }}
|
- name: Attest - ghcr.io - ${{ matrix.base_image }}
|
||||||
if: ${{ env.HAVE_GHCR_LOGIN == 'true' && steps.bake_vw.outputs.metadata != ''}}
|
if: ${{ env.HAVE_GHCR_LOGIN == 'true' && steps.bake_vw.outputs.metadata != ''}}
|
||||||
uses: actions/attest-build-provenance@c074443f1aee8d4aeeae555aebba3282517141b2 # v2.2.3
|
uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0
|
||||||
with:
|
with:
|
||||||
subject-name: ${{ vars.GHCR_REPO }}
|
subject-name: ${{ vars.GHCR_REPO }}
|
||||||
subject-digest: ${{ env.DIGEST_SHA }}
|
subject-digest: ${{ env.DIGEST_SHA }}
|
||||||
@@ -236,7 +237,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Attest - quay.io - ${{ matrix.base_image }}
|
- name: Attest - quay.io - ${{ matrix.base_image }}
|
||||||
if: ${{ env.HAVE_QUAY_LOGIN == 'true' && steps.bake_vw.outputs.metadata != ''}}
|
if: ${{ env.HAVE_QUAY_LOGIN == 'true' && steps.bake_vw.outputs.metadata != ''}}
|
||||||
uses: actions/attest-build-provenance@c074443f1aee8d4aeeae555aebba3282517141b2 # v2.2.3
|
uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0
|
||||||
with:
|
with:
|
||||||
subject-name: ${{ vars.QUAY_REPO }}
|
subject-name: ${{ vars.QUAY_REPO }}
|
||||||
subject-digest: ${{ env.DIGEST_SHA }}
|
subject-digest: ${{ env.DIGEST_SHA }}
|
||||||
@@ -248,6 +249,7 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
REF_TYPE: ${{ github.ref_type }}
|
REF_TYPE: ${{ github.ref_type }}
|
||||||
|
BASE_IMAGE: ${{ matrix.base_image }}
|
||||||
run: |
|
run: |
|
||||||
# Check which main tag we are going to build determined by ref_type
|
# Check which main tag we are going to build determined by ref_type
|
||||||
if [[ "${REF_TYPE}" == "tag" ]]; then
|
if [[ "${REF_TYPE}" == "tag" ]]; then
|
||||||
@@ -257,7 +259,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Check which base_image was used and append -alpine if needed
|
# Check which base_image was used and append -alpine if needed
|
||||||
if [[ "${{ matrix.base_image }}" == "alpine" ]]; then
|
if [[ "${BASE_IMAGE}" == "alpine" ]]; then
|
||||||
EXTRACT_TAG="${EXTRACT_TAG}-alpine"
|
EXTRACT_TAG="${EXTRACT_TAG}-alpine"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -266,25 +268,25 @@ jobs:
|
|||||||
|
|
||||||
# Extract amd64 binary
|
# Extract amd64 binary
|
||||||
docker create --name amd64 --platform=linux/amd64 "localhost:5000/vaultwarden/server:${EXTRACT_TAG}"
|
docker create --name amd64 --platform=linux/amd64 "localhost:5000/vaultwarden/server:${EXTRACT_TAG}"
|
||||||
docker cp amd64:/vaultwarden vaultwarden-amd64-${{ matrix.base_image }}
|
docker cp amd64:/vaultwarden vaultwarden-amd64-${BASE_IMAGE}
|
||||||
docker rm --force amd64
|
docker rm --force amd64
|
||||||
docker rmi --force "localhost:5000/vaultwarden/server:${EXTRACT_TAG}"
|
docker rmi --force "localhost:5000/vaultwarden/server:${EXTRACT_TAG}"
|
||||||
|
|
||||||
# Extract arm64 binary
|
# Extract arm64 binary
|
||||||
docker create --name arm64 --platform=linux/arm64 "localhost:5000/vaultwarden/server:${EXTRACT_TAG}"
|
docker create --name arm64 --platform=linux/arm64 "localhost:5000/vaultwarden/server:${EXTRACT_TAG}"
|
||||||
docker cp arm64:/vaultwarden vaultwarden-arm64-${{ matrix.base_image }}
|
docker cp arm64:/vaultwarden vaultwarden-arm64-${BASE_IMAGE}
|
||||||
docker rm --force arm64
|
docker rm --force arm64
|
||||||
docker rmi --force "localhost:5000/vaultwarden/server:${EXTRACT_TAG}"
|
docker rmi --force "localhost:5000/vaultwarden/server:${EXTRACT_TAG}"
|
||||||
|
|
||||||
# Extract armv7 binary
|
# Extract armv7 binary
|
||||||
docker create --name armv7 --platform=linux/arm/v7 "localhost:5000/vaultwarden/server:${EXTRACT_TAG}"
|
docker create --name armv7 --platform=linux/arm/v7 "localhost:5000/vaultwarden/server:${EXTRACT_TAG}"
|
||||||
docker cp armv7:/vaultwarden vaultwarden-armv7-${{ matrix.base_image }}
|
docker cp armv7:/vaultwarden vaultwarden-armv7-${BASE_IMAGE}
|
||||||
docker rm --force armv7
|
docker rm --force armv7
|
||||||
docker rmi --force "localhost:5000/vaultwarden/server:${EXTRACT_TAG}"
|
docker rmi --force "localhost:5000/vaultwarden/server:${EXTRACT_TAG}"
|
||||||
|
|
||||||
# Extract armv6 binary
|
# Extract armv6 binary
|
||||||
docker create --name armv6 --platform=linux/arm/v6 "localhost:5000/vaultwarden/server:${EXTRACT_TAG}"
|
docker create --name armv6 --platform=linux/arm/v6 "localhost:5000/vaultwarden/server:${EXTRACT_TAG}"
|
||||||
docker cp armv6:/vaultwarden vaultwarden-armv6-${{ matrix.base_image }}
|
docker cp armv6:/vaultwarden vaultwarden-armv6-${BASE_IMAGE}
|
||||||
docker rm --force armv6
|
docker rm --force armv6
|
||||||
docker rmi --force "localhost:5000/vaultwarden/server:${EXTRACT_TAG}"
|
docker rmi --force "localhost:5000/vaultwarden/server:${EXTRACT_TAG}"
|
||||||
|
|
||||||
@@ -314,7 +316,7 @@ jobs:
|
|||||||
path: vaultwarden-armv6-${{ matrix.base_image }}
|
path: vaultwarden-armv6-${{ matrix.base_image }}
|
||||||
|
|
||||||
- name: "Attest artifacts ${{ matrix.base_image }}"
|
- name: "Attest artifacts ${{ matrix.base_image }}"
|
||||||
uses: actions/attest-build-provenance@c074443f1aee8d4aeeae555aebba3282517141b2 # v2.2.3
|
uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0
|
||||||
with:
|
with:
|
||||||
subject-path: vaultwarden-*
|
subject-path: vaultwarden-*
|
||||||
# End Upload artifacts to Github Actions
|
# End Upload artifacts to Github Actions
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Run Trivy vulnerability scanner
|
- name: Run Trivy vulnerability scanner
|
||||||
uses: aquasecurity/trivy-action@6c175e9c4083a92bbca2f9724c8a5e33bc2d97a5 # v0.30.0
|
uses: aquasecurity/trivy-action@dc5a429b52fcf669ce959baa2c2dd26090d2a6c4 # v0.32.0
|
||||||
env:
|
env:
|
||||||
TRIVY_DB_REPOSITORY: docker.io/aquasec/trivy-db:2,public.ecr.aws/aquasecurity/trivy-db:2,ghcr.io/aquasecurity/trivy-db:2
|
TRIVY_DB_REPOSITORY: docker.io/aquasec/trivy-db:2,public.ecr.aws/aquasecurity/trivy-db:2,ghcr.io/aquasecurity/trivy-db:2
|
||||||
TRIVY_JAVA_DB_REPOSITORY: docker.io/aquasec/trivy-java-db:1,public.ecr.aws/aquasecurity/trivy-java-db:1,ghcr.io/aquasecurity/trivy-java-db:1
|
TRIVY_JAVA_DB_REPOSITORY: docker.io/aquasec/trivy-java-db:1,public.ecr.aws/aquasecurity/trivy-java-db:1,ghcr.io/aquasecurity/trivy-java-db:1
|
||||||
@@ -48,6 +48,6 @@ jobs:
|
|||||||
severity: CRITICAL,HIGH
|
severity: CRITICAL,HIGH
|
||||||
|
|
||||||
- name: Upload Trivy scan results to GitHub Security tab
|
- name: Upload Trivy scan results to GitHub Security tab
|
||||||
uses: github/codeql-action/upload-sarif@86b04fb0e47484f7282357688f21d5d0e32175fe # v3.27.5
|
uses: github/codeql-action/upload-sarif@4e828ff8d448a8a6e532957b1811f387a63867e8 # v3.29.4
|
||||||
with:
|
with:
|
||||||
sarif_file: 'trivy-results.sarif'
|
sarif_file: 'trivy-results.sarif'
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
name: Security Analysis with zizmor
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["main"]
|
||||||
|
pull_request:
|
||||||
|
branches: ["**"]
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
zizmor:
|
||||||
|
name: Run zizmor
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
security-events: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Run zizmor
|
||||||
|
uses: zizmorcore/zizmor-action@f52a838cfabf134edcbaa7c8b3677dde20045018 # v0.1.1
|
||||||
|
with:
|
||||||
|
# intentionally not scanning the entire repository,
|
||||||
|
# since it contains integration tests.
|
||||||
|
inputs: ./.github/
|
||||||
Generated
+1335
-403
File diff suppressed because it is too large
Load Diff
+28
-15
@@ -6,7 +6,7 @@ name = "vaultwarden"
|
|||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
authors = ["Daniel García <dani-garcia@users.noreply.github.com>"]
|
authors = ["Daniel García <dani-garcia@users.noreply.github.com>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.85.0"
|
rust-version = "1.86.0"
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
repository = "https://github.com/dani-garcia/vaultwarden"
|
repository = "https://github.com/dani-garcia/vaultwarden"
|
||||||
@@ -32,6 +32,7 @@ enable_mimalloc = ["dep:mimalloc"]
|
|||||||
# You also need to set an env variable `QUERY_LOGGER=1` to fully activate this so you do not have to re-compile
|
# You also need to set an env variable `QUERY_LOGGER=1` to fully activate this so you do not have to re-compile
|
||||||
# if you want to turn off the logging for a specific run.
|
# if you want to turn off the logging for a specific run.
|
||||||
query_logger = ["dep:diesel_logger"]
|
query_logger = ["dep:diesel_logger"]
|
||||||
|
s3 = ["opendal/services-s3", "dep:aws-config", "dep:aws-credential-types", "dep:aws-smithy-runtime-api", "dep:anyhow", "dep:http", "dep:reqsign"]
|
||||||
|
|
||||||
# Enable unstable features, requires nightly
|
# Enable unstable features, requires nightly
|
||||||
# Currently only used to enable rusts official ip support
|
# Currently only used to enable rusts official ip support
|
||||||
@@ -72,14 +73,15 @@ dashmap = "6.1.0"
|
|||||||
|
|
||||||
# Async futures
|
# Async futures
|
||||||
futures = "0.3.31"
|
futures = "0.3.31"
|
||||||
tokio = { version = "1.45.0", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] }
|
tokio = { version = "1.46.1", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] }
|
||||||
|
tokio-util = { version = "0.7.15", features = ["compat"]}
|
||||||
|
|
||||||
# A generic serialization/deserialization framework
|
# A generic serialization/deserialization framework
|
||||||
serde = { version = "1.0.219", features = ["derive"] }
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
serde_json = "1.0.140"
|
serde_json = "1.0.141"
|
||||||
|
|
||||||
# A safe, extensible ORM and Query builder
|
# A safe, extensible ORM and Query builder
|
||||||
diesel = { version = "2.2.10", features = ["chrono", "r2d2", "numeric"] }
|
diesel = { version = "2.2.12", features = ["chrono", "r2d2", "numeric"] }
|
||||||
diesel_migrations = "2.2.0"
|
diesel_migrations = "2.2.0"
|
||||||
diesel_logger = { version = "0.4.0", optional = true }
|
diesel_logger = { version = "0.4.0", optional = true }
|
||||||
|
|
||||||
@@ -87,19 +89,19 @@ derive_more = { version = "2.0.1", features = ["from", "into", "as_ref", "deref"
|
|||||||
diesel-derive-newtype = "2.1.2"
|
diesel-derive-newtype = "2.1.2"
|
||||||
|
|
||||||
# Bundled/Static SQLite
|
# Bundled/Static SQLite
|
||||||
libsqlite3-sys = { version = "0.33.0", features = ["bundled"], optional = true }
|
libsqlite3-sys = { version = "0.35.0", features = ["bundled"], optional = true }
|
||||||
|
|
||||||
# Crypto-related libraries
|
# Crypto-related libraries
|
||||||
rand = "0.9.1"
|
rand = "0.9.2"
|
||||||
ring = "0.17.14"
|
ring = "0.17.14"
|
||||||
subtle = "2.6.1"
|
subtle = "2.6.1"
|
||||||
|
|
||||||
# UUID generation
|
# UUID generation
|
||||||
uuid = { version = "1.16.0", features = ["v4"] }
|
uuid = { version = "1.17.0", features = ["v4"] }
|
||||||
|
|
||||||
# Date and time libraries
|
# Date and time libraries
|
||||||
chrono = { version = "0.4.41", features = ["clock", "serde"], default-features = false }
|
chrono = { version = "0.4.41", features = ["clock", "serde"], default-features = false }
|
||||||
chrono-tz = "0.10.3"
|
chrono-tz = "0.10.4"
|
||||||
time = "0.3.41"
|
time = "0.3.41"
|
||||||
|
|
||||||
# Job scheduler
|
# Job scheduler
|
||||||
@@ -124,7 +126,7 @@ webauthn-rs = "0.3.2"
|
|||||||
url = "2.5.4"
|
url = "2.5.4"
|
||||||
|
|
||||||
# Email libraries
|
# Email libraries
|
||||||
lettre = { version = "0.11.16", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "tokio1-native-tls", "hostname", "tracing", "tokio1"], default-features = false }
|
lettre = { version = "0.11.17", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "hostname", "tracing", "tokio1-rustls", "ring", "rustls-native-certs"], default-features = false }
|
||||||
percent-encoding = "2.3.1" # URL encoding library used for URL's in the emails
|
percent-encoding = "2.3.1" # URL encoding library used for URL's in the emails
|
||||||
email_address = "0.2.9"
|
email_address = "0.2.9"
|
||||||
|
|
||||||
@@ -132,7 +134,7 @@ email_address = "0.2.9"
|
|||||||
handlebars = { version = "6.3.2", features = ["dir_source"] }
|
handlebars = { version = "6.3.2", features = ["dir_source"] }
|
||||||
|
|
||||||
# HTTP client (Used for favicons, version check, DUO and HIBP API)
|
# HTTP client (Used for favicons, version check, DUO and HIBP API)
|
||||||
reqwest = { version = "0.12.15", features = ["native-tls-alpn", "stream", "json", "gzip", "brotli", "socks", "cookies"] }
|
reqwest = { version = "0.12.22", features = ["rustls-tls", "rustls-tls-native-roots", "stream", "json", "deflate", "gzip", "brotli", "zstd", "socks", "cookies", "charset", "http2", "system-proxy"], default-features = false}
|
||||||
hickory-resolver = "0.25.2"
|
hickory-resolver = "0.25.2"
|
||||||
|
|
||||||
# Favicon extraction libraries
|
# Favicon extraction libraries
|
||||||
@@ -140,16 +142,17 @@ html5gum = "0.7.0"
|
|||||||
regex = { version = "1.11.1", features = ["std", "perf", "unicode-perl"], default-features = false }
|
regex = { version = "1.11.1", features = ["std", "perf", "unicode-perl"], default-features = false }
|
||||||
data-url = "0.3.1"
|
data-url = "0.3.1"
|
||||||
bytes = "1.10.1"
|
bytes = "1.10.1"
|
||||||
|
svg-hush = "0.9.5"
|
||||||
|
|
||||||
# Cache function results (Used for version check and favicon fetching)
|
# Cache function results (Used for version check and favicon fetching)
|
||||||
cached = { version = "0.55.1", features = ["async"] }
|
cached = { version = "0.56.0", features = ["async"] }
|
||||||
|
|
||||||
# Used for custom short lived cookie jar during favicon extraction
|
# Used for custom short lived cookie jar during favicon extraction
|
||||||
cookie = "0.18.1"
|
cookie = "0.18.1"
|
||||||
cookie_store = "0.21.1"
|
cookie_store = "0.21.1"
|
||||||
|
|
||||||
# Used by U2F, JWT and PostgreSQL
|
# Used by U2F, JWT and PostgreSQL
|
||||||
openssl = "0.10.72"
|
openssl = "0.10.73"
|
||||||
|
|
||||||
# CLI argument parsing
|
# CLI argument parsing
|
||||||
pico-args = "0.5.0"
|
pico-args = "0.5.0"
|
||||||
@@ -163,9 +166,9 @@ semver = "1.0.26"
|
|||||||
|
|
||||||
# Allow overriding the default memory allocator
|
# Allow overriding the default memory allocator
|
||||||
# Mainly used for the musl builds, since the default musl malloc is very slow
|
# Mainly used for the musl builds, since the default musl malloc is very slow
|
||||||
mimalloc = { version = "0.1.46", features = ["secure"], default-features = false, optional = true }
|
mimalloc = { version = "0.1.47", features = ["secure"], default-features = false, optional = true }
|
||||||
|
|
||||||
which = "7.0.3"
|
which = "8.0.0"
|
||||||
|
|
||||||
# Argon2 library with support for the PHC format
|
# Argon2 library with support for the PHC format
|
||||||
argon2 = "0.5.3"
|
argon2 = "0.5.3"
|
||||||
@@ -176,6 +179,17 @@ rpassword = "7.4.0"
|
|||||||
# Loading a dynamic CSS Stylesheet
|
# Loading a dynamic CSS Stylesheet
|
||||||
grass_compiler = { version = "0.13.4", default-features = false }
|
grass_compiler = { version = "0.13.4", default-features = false }
|
||||||
|
|
||||||
|
# File are accessed through Apache OpenDAL
|
||||||
|
opendal = { version = "0.54.0", features = ["services-fs"], default-features = false }
|
||||||
|
|
||||||
|
# For retrieving AWS credentials, including temporary SSO credentials
|
||||||
|
anyhow = { version = "1.0.98", optional = true }
|
||||||
|
aws-config = { version = "1.8.3", features = ["behavior-version-latest", "rt-tokio", "credentials-process", "sso"], default-features = false, optional = true }
|
||||||
|
aws-credential-types = { version = "1.2.4", optional = true }
|
||||||
|
aws-smithy-runtime-api = { version = "1.8.5", optional = true }
|
||||||
|
http = { version = "1.3.1", optional = true }
|
||||||
|
reqsign = { version = "0.16.5", optional = true }
|
||||||
|
|
||||||
# Strip debuginfo from the release builds
|
# Strip debuginfo from the release builds
|
||||||
# The debug symbols are to provide better panic traces
|
# The debug symbols are to provide better panic traces
|
||||||
# Also enable fat LTO and use 1 codegen unit for optimizations
|
# Also enable fat LTO and use 1 codegen unit for optimizations
|
||||||
@@ -265,7 +279,6 @@ macro_use_imports = "deny"
|
|||||||
manual_assert = "deny"
|
manual_assert = "deny"
|
||||||
manual_instant_elapsed = "deny"
|
manual_instant_elapsed = "deny"
|
||||||
manual_string_new = "deny"
|
manual_string_new = "deny"
|
||||||
match_on_vec_items = "deny"
|
|
||||||
match_wildcard_for_single_variants = "deny"
|
match_wildcard_for_single_variants = "deny"
|
||||||
mem_forget = "deny"
|
mem_forget = "deny"
|
||||||
needless_continue = "deny"
|
needless_continue = "deny"
|
||||||
|
|||||||
@@ -59,19 +59,21 @@ A nearly complete implementation of the Bitwarden Client API is provided, includ
|
|||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> Most modern web browsers disallow the use of Web Crypto APIs in insecure contexts. In this case, you might get an error like `Cannot read property 'importKey'`. To solve this problem, you need to access the web vault via HTTPS or localhost.
|
> The web-vault requires the use a secure context for the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API).
|
||||||
>
|
> That means it will only work via `http://localhost:8000` (using the port from the example below) or if you [enable HTTPS](https://github.com/dani-garcia/vaultwarden/wiki/Enabling-HTTPS).
|
||||||
>This can be configured in [Vaultwarden directly](https://github.com/dani-garcia/vaultwarden/wiki/Enabling-HTTPS) or using a third-party reverse proxy ([some examples](https://github.com/dani-garcia/vaultwarden/wiki/Proxy-examples)).
|
|
||||||
>
|
The recommended way to install and use Vaultwarden is via our container images which are published to [ghcr.io](https://github.com/dani-garcia/vaultwarden/pkgs/container/vaultwarden), [docker.io](https://hub.docker.com/r/vaultwarden/server) and [quay.io](https://quay.io/repository/vaultwarden/server).
|
||||||
>If you have an available domain name, you can get HTTPS certificates with [Let's Encrypt](https://letsencrypt.org/), or you can generate self-signed certificates with utilities like [mkcert](https://github.com/FiloSottile/mkcert). Some proxies automatically do this step, like Caddy or Traefik (see examples linked above).
|
See [which container image to use](https://github.com/dani-garcia/vaultwarden/wiki/Which-container-image-to-use) for an explanation of the provided tags.
|
||||||
|
|
||||||
|
There are also [community driven packages](https://github.com/dani-garcia/vaultwarden/wiki/Third-party-packages) which can be used, but those might be lagging behind the latest version or might deviate in the way Vaultwarden is configured, as described in our [Wiki](https://github.com/dani-garcia/vaultwarden/wiki).
|
||||||
|
|
||||||
|
Alternatively, you can also [build Vaultwarden](https://github.com/dani-garcia/vaultwarden/wiki/Building-binary) yourself.
|
||||||
|
|
||||||
|
While Vaultwarden is based upon the [Rocket web framework](https://rocket.rs) which has built-in support for TLS our recommendation would be that you setup a reverse proxy (see [proxy examples](https://github.com/dani-garcia/vaultwarden/wiki/Proxy-examples)).
|
||||||
|
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
>**For more detailed examples on how to install, use and configure Vaultwarden you can check our [Wiki](https://github.com/dani-garcia/vaultwarden/wiki).**
|
>**For more detailed examples on how to install, use and configure Vaultwarden you can check our [Wiki](https://github.com/dani-garcia/vaultwarden/wiki).**
|
||||||
|
|
||||||
The main way to use Vaultwarden is via our container images which are published to [ghcr.io](https://github.com/dani-garcia/vaultwarden/pkgs/container/vaultwarden), [docker.io](https://hub.docker.com/r/vaultwarden/server) and [quay.io](https://quay.io/repository/vaultwarden/server).
|
|
||||||
|
|
||||||
There are also [community driven packages](https://github.com/dani-garcia/vaultwarden/wiki/Third-party-packages) which can be used, but those might be lagging behind the latest version or might deviate in the way Vaultwarden is configured, as described in our [Wiki](https://github.com/dani-garcia/vaultwarden/wiki).
|
|
||||||
|
|
||||||
### Docker/Podman CLI
|
### Docker/Podman CLI
|
||||||
|
|
||||||
Pull the container image and mount a volume from the host for persistent storage.<br>
|
Pull the container image and mount a volume from the host for persistent storage.<br>
|
||||||
@@ -83,7 +85,7 @@ docker run --detach --name vaultwarden \
|
|||||||
--env DOMAIN="https://vw.domain.tld" \
|
--env DOMAIN="https://vw.domain.tld" \
|
||||||
--volume /vw-data/:/data/ \
|
--volume /vw-data/:/data/ \
|
||||||
--restart unless-stopped \
|
--restart unless-stopped \
|
||||||
--publish 80:80 \
|
--publish 127.0.0.1:8000:80 \
|
||||||
vaultwarden/server:latest
|
vaultwarden/server:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -104,7 +106,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./vw-data/:/data/
|
- ./vw-data/:/data/
|
||||||
ports:
|
ports:
|
||||||
- 80:80
|
- 127.0.0.1:8000:80
|
||||||
```
|
```
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ fn main() {
|
|||||||
println!("cargo:rustc-cfg=postgresql");
|
println!("cargo:rustc-cfg=postgresql");
|
||||||
#[cfg(feature = "query_logger")]
|
#[cfg(feature = "query_logger")]
|
||||||
println!("cargo:rustc-cfg=query_logger");
|
println!("cargo:rustc-cfg=query_logger");
|
||||||
|
#[cfg(feature = "s3")]
|
||||||
|
println!("cargo:rustc-cfg=s3");
|
||||||
|
|
||||||
#[cfg(not(any(feature = "sqlite", feature = "mysql", feature = "postgresql")))]
|
#[cfg(not(any(feature = "sqlite", feature = "mysql", feature = "postgresql")))]
|
||||||
compile_error!(
|
compile_error!(
|
||||||
@@ -23,6 +25,7 @@ fn main() {
|
|||||||
println!("cargo::rustc-check-cfg=cfg(mysql)");
|
println!("cargo::rustc-check-cfg=cfg(mysql)");
|
||||||
println!("cargo::rustc-check-cfg=cfg(postgresql)");
|
println!("cargo::rustc-check-cfg=cfg(postgresql)");
|
||||||
println!("cargo::rustc-check-cfg=cfg(query_logger)");
|
println!("cargo::rustc-check-cfg=cfg(query_logger)");
|
||||||
|
println!("cargo::rustc-check-cfg=cfg(s3)");
|
||||||
|
|
||||||
// Rerun when these paths are changed.
|
// Rerun when these paths are changed.
|
||||||
// Someone could have checked-out a tag or specific commit, but no other files changed.
|
// Someone could have checked-out a tag or specific commit, but no other files changed.
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
---
|
---
|
||||||
vault_version: "v2025.5.0"
|
vault_version: "v2025.7.0"
|
||||||
vault_image_digest: "sha256:a0a377b810e66a4ebf1416f732d2be06f3262bf5a5238695af88d3ec6871cc0e"
|
vault_image_digest: "sha256:f6ac819a2cd9e226f2cd2ec26196ede94a41e672e9672a11b5f307a19278b15e"
|
||||||
# Cross Compile Docker Helper Scripts v1.6.1
|
# 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
|
# We use the linux/amd64 platform shell scripts since there is no difference between the different platform scripts
|
||||||
# https://github.com/tonistiigi/xx | https://hub.docker.com/r/tonistiigi/xx/tags
|
# https://github.com/tonistiigi/xx | https://hub.docker.com/r/tonistiigi/xx/tags
|
||||||
xx_image_digest: "sha256:9c207bead753dda9430bdd15425c6518fc7a03d866103c516a2c6889188f5894"
|
xx_image_digest: "sha256:9c207bead753dda9430bdd15425c6518fc7a03d866103c516a2c6889188f5894"
|
||||||
rust_version: 1.87.0 # Rust version to be used
|
rust_version: 1.88.0 # Rust version to be used
|
||||||
debian_version: bookworm # Debian release name to be used
|
debian_version: bookworm # Debian release name to be used
|
||||||
alpine_version: "3.21" # Alpine version to be used
|
alpine_version: "3.22" # Alpine version to be used
|
||||||
# For which platforms/architectures will we try to build images
|
# For which platforms/architectures will we try to build images
|
||||||
platforms: ["linux/amd64", "linux/arm64", "linux/arm/v7", "linux/arm/v6"]
|
platforms: ["linux/amd64", "linux/arm64", "linux/arm/v7", "linux/arm/v6"]
|
||||||
# Determine the build images per OS/Arch
|
# Determine the build images per OS/Arch
|
||||||
|
|||||||
+11
-11
@@ -19,23 +19,23 @@
|
|||||||
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
||||||
# click the tag name to view the digest of the image it currently points to.
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
# - From the command line:
|
# - From the command line:
|
||||||
# $ docker pull docker.io/vaultwarden/web-vault:v2025.5.0
|
# $ docker pull docker.io/vaultwarden/web-vault:v2025.7.0
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.5.0
|
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.7.0
|
||||||
# [docker.io/vaultwarden/web-vault@sha256:a0a377b810e66a4ebf1416f732d2be06f3262bf5a5238695af88d3ec6871cc0e]
|
# [docker.io/vaultwarden/web-vault@sha256:f6ac819a2cd9e226f2cd2ec26196ede94a41e672e9672a11b5f307a19278b15e]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:a0a377b810e66a4ebf1416f732d2be06f3262bf5a5238695af88d3ec6871cc0e
|
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:f6ac819a2cd9e226f2cd2ec26196ede94a41e672e9672a11b5f307a19278b15e
|
||||||
# [docker.io/vaultwarden/web-vault:v2025.5.0]
|
# [docker.io/vaultwarden/web-vault:v2025.7.0]
|
||||||
#
|
#
|
||||||
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:a0a377b810e66a4ebf1416f732d2be06f3262bf5a5238695af88d3ec6871cc0e AS vault
|
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:f6ac819a2cd9e226f2cd2ec26196ede94a41e672e9672a11b5f307a19278b15e AS vault
|
||||||
|
|
||||||
########################## ALPINE BUILD IMAGES ##########################
|
########################## ALPINE BUILD IMAGES ##########################
|
||||||
## NOTE: The Alpine Base Images do not support other platforms then linux/amd64
|
## NOTE: The Alpine Base Images do not support other platforms then linux/amd64
|
||||||
## And for Alpine we define all build images here, they will only be loaded when actually used
|
## And for Alpine we define all build images here, they will only be loaded when actually used
|
||||||
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.87.0 AS build_amd64
|
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.88.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:aarch64-musl-stable-1.88.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:armv7-musleabihf-stable-1.88.0 AS build_armv7
|
||||||
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.87.0 AS build_armv6
|
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.88.0 AS build_armv6
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
# hadolint ignore=DL3006
|
# hadolint ignore=DL3006
|
||||||
@@ -127,7 +127,7 @@ RUN source /env-cargo && \
|
|||||||
# To uninstall: docker run --privileged --rm tonistiigi/binfmt --uninstall 'qemu-*'
|
# To uninstall: docker run --privileged --rm tonistiigi/binfmt --uninstall 'qemu-*'
|
||||||
#
|
#
|
||||||
# We need to add `--platform` here, because of a podman bug: https://github.com/containers/buildah/issues/4742
|
# We need to add `--platform` here, because of a podman bug: https://github.com/containers/buildah/issues/4742
|
||||||
FROM --platform=$TARGETPLATFORM docker.io/library/alpine:3.21
|
FROM --platform=$TARGETPLATFORM docker.io/library/alpine:3.22
|
||||||
|
|
||||||
ENV ROCKET_PROFILE="release" \
|
ENV ROCKET_PROFILE="release" \
|
||||||
ROCKET_ADDRESS=0.0.0.0 \
|
ROCKET_ADDRESS=0.0.0.0 \
|
||||||
|
|||||||
@@ -19,15 +19,15 @@
|
|||||||
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
||||||
# click the tag name to view the digest of the image it currently points to.
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
# - From the command line:
|
# - From the command line:
|
||||||
# $ docker pull docker.io/vaultwarden/web-vault:v2025.5.0
|
# $ docker pull docker.io/vaultwarden/web-vault:v2025.7.0
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.5.0
|
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.7.0
|
||||||
# [docker.io/vaultwarden/web-vault@sha256:a0a377b810e66a4ebf1416f732d2be06f3262bf5a5238695af88d3ec6871cc0e]
|
# [docker.io/vaultwarden/web-vault@sha256:f6ac819a2cd9e226f2cd2ec26196ede94a41e672e9672a11b5f307a19278b15e]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:a0a377b810e66a4ebf1416f732d2be06f3262bf5a5238695af88d3ec6871cc0e
|
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:f6ac819a2cd9e226f2cd2ec26196ede94a41e672e9672a11b5f307a19278b15e
|
||||||
# [docker.io/vaultwarden/web-vault:v2025.5.0]
|
# [docker.io/vaultwarden/web-vault:v2025.7.0]
|
||||||
#
|
#
|
||||||
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:a0a377b810e66a4ebf1416f732d2be06f3262bf5a5238695af88d3ec6871cc0e AS vault
|
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:f6ac819a2cd9e226f2cd2ec26196ede94a41e672e9672a11b5f307a19278b15e AS vault
|
||||||
|
|
||||||
########################## Cross Compile Docker Helper Scripts ##########################
|
########################## Cross Compile Docker Helper Scripts ##########################
|
||||||
## We use the linux/amd64 no matter which Build Platform, since these are all bash scripts
|
## We use the linux/amd64 no matter which Build Platform, since these are all bash scripts
|
||||||
@@ -36,7 +36,7 @@ FROM --platform=linux/amd64 docker.io/tonistiigi/xx@sha256:9c207bead753dda9430bd
|
|||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
# hadolint ignore=DL3006
|
# hadolint ignore=DL3006
|
||||||
FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.87.0-slim-bookworm AS build
|
FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.88.0-slim-bookworm AS build
|
||||||
COPY --from=xx / /
|
COPY --from=xx / /
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
ARG TARGETVARIANT
|
ARG TARGETVARIANT
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@ proc-macro = true
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
quote = "1.0.40"
|
quote = "1.0.40"
|
||||||
syn = "2.0.101"
|
syn = "2.0.104"
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
[toolchain]
|
[toolchain]
|
||||||
channel = "1.87.0"
|
channel = "1.88.0"
|
||||||
components = [ "rustfmt", "clippy" ]
|
components = [ "rustfmt", "clippy" ]
|
||||||
profile = "minimal"
|
profile = "minimal"
|
||||||
|
|||||||
+47
-45
@@ -421,11 +421,11 @@ async fn delete_user(user_id: UserId, token: AdminToken, mut conn: DbConn) -> Em
|
|||||||
async fn deauth_user(user_id: UserId, _token: AdminToken, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
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?;
|
let mut user = get_user_or_404(&user_id, &mut conn).await?;
|
||||||
|
|
||||||
nt.send_logout(&user, None).await;
|
nt.send_logout(&user, None, &mut conn).await;
|
||||||
|
|
||||||
if CONFIG.push_enabled() {
|
if CONFIG.push_enabled() {
|
||||||
for device in Device::find_push_devices_by_user(&user.uuid, &mut conn).await {
|
for device in Device::find_push_devices_by_user(&user.uuid, &mut conn).await {
|
||||||
match unregister_push_device(device.push_uuid).await {
|
match unregister_push_device(&device.push_uuid).await {
|
||||||
Ok(r) => r,
|
Ok(r) => r,
|
||||||
Err(e) => error!("Unable to unregister devices from Bitwarden server: {e}"),
|
Err(e) => error!("Unable to unregister devices from Bitwarden server: {e}"),
|
||||||
};
|
};
|
||||||
@@ -447,7 +447,7 @@ async fn disable_user(user_id: UserId, _token: AdminToken, mut conn: DbConn, nt:
|
|||||||
|
|
||||||
let save_result = user.save(&mut conn).await;
|
let save_result = user.save(&mut conn).await;
|
||||||
|
|
||||||
nt.send_logout(&user, None).await;
|
nt.send_logout(&user, None, &mut conn).await;
|
||||||
|
|
||||||
save_result
|
save_result
|
||||||
}
|
}
|
||||||
@@ -591,20 +591,14 @@ struct GitCommit {
|
|||||||
sha: String,
|
sha: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct TimeApi {
|
|
||||||
year: u16,
|
|
||||||
month: u8,
|
|
||||||
day: u8,
|
|
||||||
hour: u8,
|
|
||||||
minute: u8,
|
|
||||||
seconds: u8,
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_json_api<T: DeserializeOwned>(url: &str) -> Result<T, Error> {
|
async fn get_json_api<T: DeserializeOwned>(url: &str) -> Result<T, Error> {
|
||||||
Ok(make_http_request(Method::GET, url)?.send().await?.error_for_status()?.json::<T>().await?)
|
Ok(make_http_request(Method::GET, url)?.send().await?.error_for_status()?.json::<T>().await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_text_api(url: &str) -> Result<String, Error> {
|
||||||
|
Ok(make_http_request(Method::GET, url)?.send().await?.error_for_status()?.text().await?)
|
||||||
|
}
|
||||||
|
|
||||||
async fn has_http_access() -> bool {
|
async fn has_http_access() -> bool {
|
||||||
let Ok(req) = make_http_request(Method::HEAD, "https://github.com/dani-garcia/vaultwarden") else {
|
let Ok(req) = make_http_request(Method::HEAD, "https://github.com/dani-garcia/vaultwarden") else {
|
||||||
return false;
|
return false;
|
||||||
@@ -616,10 +610,12 @@ async fn has_http_access() -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
use cached::proc_macro::cached;
|
use cached::proc_macro::cached;
|
||||||
/// Cache this function to prevent API call rate limit. Github only allows 60 requests per hour, and we use 3 here already.
|
/// Cache this function to prevent API call rate limit. Github only allows 60 requests per hour, and we use 3 here already
|
||||||
/// It will cache this function for 300 seconds (5 minutes) which should prevent the exhaustion of the rate limit.
|
/// It will cache this function for 600 seconds (10 minutes) which should prevent the exhaustion of the rate limit
|
||||||
#[cached(time = 300, sync_writes = "default")]
|
/// Any cache will be lost if Vaultwarden is restarted
|
||||||
async fn get_release_info(has_http_access: bool, running_within_container: bool) -> (String, String, String) {
|
use std::time::Duration; // Needed for cached
|
||||||
|
#[cached(time = 600, sync_writes = "default")]
|
||||||
|
async fn get_release_info(has_http_access: bool) -> (String, String, String) {
|
||||||
// If the HTTP Check failed, do not even attempt to check for new versions since we were not able to connect with github.com anyway.
|
// If the HTTP Check failed, do not even attempt to check for new versions since we were not able to connect with github.com anyway.
|
||||||
if has_http_access {
|
if has_http_access {
|
||||||
(
|
(
|
||||||
@@ -636,19 +632,13 @@ async fn get_release_info(has_http_access: bool, running_within_container: bool)
|
|||||||
}
|
}
|
||||||
_ => "-".to_string(),
|
_ => "-".to_string(),
|
||||||
},
|
},
|
||||||
// Do not fetch the web-vault version when running within a container.
|
// Do not fetch the web-vault version when running within a container
|
||||||
// The web-vault version is embedded within the container it self, and should not be updated manually
|
// The web-vault version is embedded within the container it self, and should not be updated manually
|
||||||
if running_within_container {
|
match get_json_api::<GitRelease>("https://api.github.com/repos/dani-garcia/bw_web_builds/releases/latest")
|
||||||
"-".to_string()
|
|
||||||
} else {
|
|
||||||
match get_json_api::<GitRelease>(
|
|
||||||
"https://api.github.com/repos/dani-garcia/bw_web_builds/releases/latest",
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(r) => r.tag_name.trim_start_matches('v').to_string(),
|
Ok(r) => r.tag_name.trim_start_matches('v').to_string(),
|
||||||
_ => "-".to_string(),
|
_ => "-".to_string(),
|
||||||
}
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -658,17 +648,18 @@ async fn get_release_info(has_http_access: bool, running_within_container: bool)
|
|||||||
|
|
||||||
async fn get_ntp_time(has_http_access: bool) -> String {
|
async fn get_ntp_time(has_http_access: bool) -> String {
|
||||||
if has_http_access {
|
if has_http_access {
|
||||||
if let Ok(ntp_time) = get_json_api::<TimeApi>("https://www.timeapi.io/api/Time/current/zone?timeZone=UTC").await
|
if let Ok(cf_trace) = get_text_api("https://cloudflare.com/cdn-cgi/trace").await {
|
||||||
{
|
for line in cf_trace.lines() {
|
||||||
return format!(
|
if let Some((key, value)) = line.split_once('=') {
|
||||||
"{year}-{month:02}-{day:02} {hour:02}:{minute:02}:{seconds:02} UTC",
|
if key == "ts" {
|
||||||
year = ntp_time.year,
|
let ts = value.split_once('.').map_or(value, |(s, _)| s);
|
||||||
month = ntp_time.month,
|
if let Ok(dt) = chrono::DateTime::parse_from_str(ts, "%s") {
|
||||||
day = ntp_time.day,
|
return dt.format("%Y-%m-%d %H:%M:%S UTC").to_string();
|
||||||
hour = ntp_time.hour,
|
}
|
||||||
minute = ntp_time.minute,
|
break;
|
||||||
seconds = ntp_time.seconds
|
}
|
||||||
);
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
String::from("Unable to fetch NTP time.")
|
String::from("Unable to fetch NTP time.")
|
||||||
@@ -693,14 +684,23 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn)
|
|||||||
_ => "Unable to resolve domain name.".to_string(),
|
_ => "Unable to resolve domain name.".to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let (latest_release, latest_commit, latest_web_build) =
|
let (latest_release, latest_commit, latest_web_build) = get_release_info(has_http_access).await;
|
||||||
get_release_info(has_http_access, running_within_container).await;
|
|
||||||
|
|
||||||
let ip_header_name = &ip_header.0.unwrap_or_default();
|
let ip_header_name = &ip_header.0.unwrap_or_default();
|
||||||
|
|
||||||
// Get current running versions
|
// Get current running versions
|
||||||
let web_vault_version = get_web_vault_version();
|
let web_vault_version = get_web_vault_version();
|
||||||
|
|
||||||
|
// Check if the running version is newer than the latest stable released version
|
||||||
|
let web_vault_pre_release = if let Ok(web_ver_match) = semver::VersionReq::parse(&format!(">{latest_web_build}")) {
|
||||||
|
web_ver_match.matches(
|
||||||
|
&semver::Version::parse(&web_vault_version).unwrap_or_else(|_| semver::Version::parse("2025.1.1").unwrap()),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
error!("Unable to parse latest_web_build: '{latest_web_build}'");
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
let diagnostics_json = json!({
|
let diagnostics_json = json!({
|
||||||
"dns_resolved": dns_resolved,
|
"dns_resolved": dns_resolved,
|
||||||
"current_release": VERSION,
|
"current_release": VERSION,
|
||||||
@@ -709,6 +709,7 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn)
|
|||||||
"web_vault_enabled": &CONFIG.web_vault_enabled(),
|
"web_vault_enabled": &CONFIG.web_vault_enabled(),
|
||||||
"web_vault_version": web_vault_version,
|
"web_vault_version": web_vault_version,
|
||||||
"latest_web_build": latest_web_build,
|
"latest_web_build": latest_web_build,
|
||||||
|
"web_vault_pre_release": web_vault_pre_release,
|
||||||
"running_within_container": running_within_container,
|
"running_within_container": running_within_container,
|
||||||
"container_base_image": if running_within_container { container_base_image() } else { "Not applicable" },
|
"container_base_image": if running_within_container { container_base_image() } else { "Not applicable" },
|
||||||
"has_http_access": has_http_access,
|
"has_http_access": has_http_access,
|
||||||
@@ -724,6 +725,7 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn)
|
|||||||
"overrides": &CONFIG.get_overrides().join(", "),
|
"overrides": &CONFIG.get_overrides().join(", "),
|
||||||
"host_arch": env::consts::ARCH,
|
"host_arch": env::consts::ARCH,
|
||||||
"host_os": env::consts::OS,
|
"host_os": env::consts::OS,
|
||||||
|
"tz_env": env::var("TZ").unwrap_or_default(),
|
||||||
"server_time_local": Local::now().format("%Y-%m-%d %H:%M:%S %Z").to_string(),
|
"server_time_local": Local::now().format("%Y-%m-%d %H:%M:%S %Z").to_string(),
|
||||||
"server_time": Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string(), // Run the server date/time check as late as possible to minimize the time difference
|
"server_time": Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string(), // Run the server date/time check as late as possible to minimize the time difference
|
||||||
"ntp_time": get_ntp_time(has_http_access).await, // Run the ntp check as late as possible to minimize the time difference
|
"ntp_time": get_ntp_time(has_http_access).await, // Run the ntp check as late as possible to minimize the time difference
|
||||||
@@ -745,17 +747,17 @@ fn get_diagnostics_http(code: u16, _token: AdminToken) -> EmptyResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/config", format = "application/json", data = "<data>")]
|
#[post("/config", format = "application/json", data = "<data>")]
|
||||||
fn post_config(data: Json<ConfigBuilder>, _token: AdminToken) -> EmptyResult {
|
async fn post_config(data: Json<ConfigBuilder>, _token: AdminToken) -> EmptyResult {
|
||||||
let data: ConfigBuilder = data.into_inner();
|
let data: ConfigBuilder = data.into_inner();
|
||||||
if let Err(e) = CONFIG.update_config(data, true) {
|
if let Err(e) = CONFIG.update_config(data, true).await {
|
||||||
err!(format!("Unable to save config: {e:?}"))
|
err!(format!("Unable to save config: {e:?}"))
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/config/delete", format = "application/json")]
|
#[post("/config/delete", format = "application/json")]
|
||||||
fn delete_config(_token: AdminToken) -> EmptyResult {
|
async fn delete_config(_token: AdminToken) -> EmptyResult {
|
||||||
if let Err(e) = CONFIG.delete_user_config() {
|
if let Err(e) = CONFIG.delete_user_config().await {
|
||||||
err!(format!("Unable to delete config: {e:?}"))
|
err!(format!("Unable to delete config: {e:?}"))
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
+120
-56
@@ -8,8 +8,8 @@ use serde_json::Value;
|
|||||||
use crate::{
|
use crate::{
|
||||||
api::{
|
api::{
|
||||||
core::{log_user_event, two_factor::email},
|
core::{log_user_event, two_factor::email},
|
||||||
register_push_device, unregister_push_device, AnonymousNotify, EmptyResult, JsonResult, Notify,
|
master_password_policy, register_push_device, unregister_push_device, AnonymousNotify, EmptyResult, JsonResult,
|
||||||
PasswordOrOtpData, UpdateType,
|
Notify, PasswordOrOtpData, UpdateType,
|
||||||
},
|
},
|
||||||
auth::{decode_delete, decode_invite, decode_verify_email, ClientHeaders, Headers},
|
auth::{decode_delete, decode_invite, decode_verify_email, ClientHeaders, Headers},
|
||||||
crypto,
|
crypto,
|
||||||
@@ -128,9 +128,8 @@ async fn is_email_2fa_required(member_id: Option<MembershipId>, conn: &mut DbCon
|
|||||||
if CONFIG.email_2fa_enforce_on_verified_invite() {
|
if CONFIG.email_2fa_enforce_on_verified_invite() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if member_id.is_some() {
|
if let Some(member_id) = member_id {
|
||||||
return OrgPolicy::is_enabled_for_member(&member_id.unwrap(), OrgPolicyType::TwoFactorAuthentication, conn)
|
return OrgPolicy::is_enabled_for_member(&member_id, OrgPolicyType::TwoFactorAuthentication, conn).await;
|
||||||
.await;
|
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
@@ -337,7 +336,6 @@ async fn profile(headers: Headers, mut conn: DbConn) -> Json<Value> {
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct ProfileData {
|
struct ProfileData {
|
||||||
// culture: String, // Ignored, always use en-US
|
// culture: String, // Ignored, always use en-US
|
||||||
// masterPasswordHint: Option<String>, // Ignored, has been moved to ChangePassData
|
|
||||||
name: String,
|
name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -463,7 +461,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.
|
// Prevent logging out the client where the user requested this endpoint from.
|
||||||
// If you do logout the user it will causes issues at the client side.
|
// If you do logout the user it will causes issues at the client side.
|
||||||
// Adding the device uuid will prevent this.
|
// Adding the device uuid will prevent this.
|
||||||
nt.send_logout(&user, Some(headers.device.uuid.clone())).await;
|
nt.send_logout(&user, Some(headers.device.uuid.clone()), &mut conn).await;
|
||||||
|
|
||||||
save_result
|
save_result
|
||||||
}
|
}
|
||||||
@@ -523,7 +521,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);
|
user.set_password(&data.new_master_password_hash, Some(data.key), true, None);
|
||||||
let save_result = user.save(&mut conn).await;
|
let save_result = user.save(&mut conn).await;
|
||||||
|
|
||||||
nt.send_logout(&user, Some(headers.device.uuid.clone())).await;
|
nt.send_logout(&user, Some(headers.device.uuid.clone()), &mut conn).await;
|
||||||
|
|
||||||
save_result
|
save_result
|
||||||
}
|
}
|
||||||
@@ -558,14 +556,45 @@ use super::sends::{update_send_from_data, SendData};
|
|||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct KeyData {
|
struct KeyData {
|
||||||
|
account_unlock_data: RotateAccountUnlockData,
|
||||||
|
account_keys: RotateAccountKeys,
|
||||||
|
account_data: RotateAccountData,
|
||||||
|
old_master_key_authentication_hash: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct RotateAccountUnlockData {
|
||||||
|
emergency_access_unlock_data: Vec<UpdateEmergencyAccessData>,
|
||||||
|
master_password_unlock_data: MasterPasswordUnlockData,
|
||||||
|
organization_account_recovery_unlock_data: Vec<UpdateResetPasswordData>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct MasterPasswordUnlockData {
|
||||||
|
kdf_type: i32,
|
||||||
|
kdf_iterations: i32,
|
||||||
|
kdf_parallelism: Option<i32>,
|
||||||
|
kdf_memory: Option<i32>,
|
||||||
|
email: String,
|
||||||
|
master_key_authentication_hash: String,
|
||||||
|
master_key_encrypted_user_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct RotateAccountKeys {
|
||||||
|
user_key_encrypted_account_private_key: String,
|
||||||
|
account_public_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct RotateAccountData {
|
||||||
ciphers: Vec<CipherData>,
|
ciphers: Vec<CipherData>,
|
||||||
folders: Vec<UpdateFolderData>,
|
folders: Vec<UpdateFolderData>,
|
||||||
sends: Vec<SendData>,
|
sends: Vec<SendData>,
|
||||||
emergency_access_keys: Vec<UpdateEmergencyAccessData>,
|
|
||||||
reset_password_keys: Vec<UpdateResetPasswordData>,
|
|
||||||
key: String,
|
|
||||||
master_password_hash: String,
|
|
||||||
private_key: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_keydata(
|
fn validate_keydata(
|
||||||
@@ -575,10 +604,24 @@ fn validate_keydata(
|
|||||||
existing_emergency_access: &[EmergencyAccess],
|
existing_emergency_access: &[EmergencyAccess],
|
||||||
existing_memberships: &[Membership],
|
existing_memberships: &[Membership],
|
||||||
existing_sends: &[Send],
|
existing_sends: &[Send],
|
||||||
|
user: &User,
|
||||||
) -> EmptyResult {
|
) -> EmptyResult {
|
||||||
|
if user.client_kdf_type != data.account_unlock_data.master_password_unlock_data.kdf_type
|
||||||
|
|| user.client_kdf_iter != data.account_unlock_data.master_password_unlock_data.kdf_iterations
|
||||||
|
|| user.client_kdf_memory != data.account_unlock_data.master_password_unlock_data.kdf_memory
|
||||||
|
|| user.client_kdf_parallelism != data.account_unlock_data.master_password_unlock_data.kdf_parallelism
|
||||||
|
|| user.email != data.account_unlock_data.master_password_unlock_data.email
|
||||||
|
{
|
||||||
|
err!("Changing the kdf variant or email is not supported during key rotation");
|
||||||
|
}
|
||||||
|
if user.public_key.as_ref() != Some(&data.account_keys.account_public_key) {
|
||||||
|
err!("Changing the asymmetric keypair is not possible during key rotation")
|
||||||
|
}
|
||||||
|
|
||||||
// Check that we're correctly rotating all the user's ciphers
|
// Check that we're correctly rotating all the user's ciphers
|
||||||
let existing_cipher_ids = existing_ciphers.iter().map(|c| &c.uuid).collect::<HashSet<&CipherId>>();
|
let existing_cipher_ids = existing_ciphers.iter().map(|c| &c.uuid).collect::<HashSet<&CipherId>>();
|
||||||
let provided_cipher_ids = data
|
let provided_cipher_ids = data
|
||||||
|
.account_data
|
||||||
.ciphers
|
.ciphers
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|c| c.organization_id.is_none())
|
.filter(|c| c.organization_id.is_none())
|
||||||
@@ -590,7 +633,8 @@ fn validate_keydata(
|
|||||||
|
|
||||||
// Check that we're correctly rotating all the user's folders
|
// Check that we're correctly rotating all the user's folders
|
||||||
let existing_folder_ids = existing_folders.iter().map(|f| &f.uuid).collect::<HashSet<&FolderId>>();
|
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>>();
|
let provided_folder_ids =
|
||||||
|
data.account_data.folders.iter().filter_map(|f| f.id.as_ref()).collect::<HashSet<&FolderId>>();
|
||||||
if !provided_folder_ids.is_superset(&existing_folder_ids) {
|
if !provided_folder_ids.is_superset(&existing_folder_ids) {
|
||||||
err!("All existing folders must be included in the rotation")
|
err!("All existing folders must be included in the rotation")
|
||||||
}
|
}
|
||||||
@@ -598,8 +642,12 @@ fn validate_keydata(
|
|||||||
// Check that we're correctly rotating all the user's emergency access keys
|
// Check that we're correctly rotating all the user's emergency access keys
|
||||||
let existing_emergency_access_ids =
|
let existing_emergency_access_ids =
|
||||||
existing_emergency_access.iter().map(|ea| &ea.uuid).collect::<HashSet<&EmergencyAccessId>>();
|
existing_emergency_access.iter().map(|ea| &ea.uuid).collect::<HashSet<&EmergencyAccessId>>();
|
||||||
let provided_emergency_access_ids =
|
let provided_emergency_access_ids = data
|
||||||
data.emergency_access_keys.iter().map(|ea| &ea.id).collect::<HashSet<&EmergencyAccessId>>();
|
.account_unlock_data
|
||||||
|
.emergency_access_unlock_data
|
||||||
|
.iter()
|
||||||
|
.map(|ea| &ea.id)
|
||||||
|
.collect::<HashSet<&EmergencyAccessId>>();
|
||||||
if !provided_emergency_access_ids.is_superset(&existing_emergency_access_ids) {
|
if !provided_emergency_access_ids.is_superset(&existing_emergency_access_ids) {
|
||||||
err!("All existing emergency access keys must be included in the rotation")
|
err!("All existing emergency access keys must be included in the rotation")
|
||||||
}
|
}
|
||||||
@@ -607,15 +655,19 @@ fn validate_keydata(
|
|||||||
// Check that we're correctly rotating all the user's reset password keys
|
// Check that we're correctly rotating all the user's reset password keys
|
||||||
let existing_reset_password_ids =
|
let existing_reset_password_ids =
|
||||||
existing_memberships.iter().map(|m| &m.org_uuid).collect::<HashSet<&OrganizationId>>();
|
existing_memberships.iter().map(|m| &m.org_uuid).collect::<HashSet<&OrganizationId>>();
|
||||||
let provided_reset_password_ids =
|
let provided_reset_password_ids = data
|
||||||
data.reset_password_keys.iter().map(|rp| &rp.organization_id).collect::<HashSet<&OrganizationId>>();
|
.account_unlock_data
|
||||||
|
.organization_account_recovery_unlock_data
|
||||||
|
.iter()
|
||||||
|
.map(|rp| &rp.organization_id)
|
||||||
|
.collect::<HashSet<&OrganizationId>>();
|
||||||
if !provided_reset_password_ids.is_superset(&existing_reset_password_ids) {
|
if !provided_reset_password_ids.is_superset(&existing_reset_password_ids) {
|
||||||
err!("All existing reset password keys must be included in the rotation")
|
err!("All existing reset password keys must be included in the rotation")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that we're correctly rotating all the user's sends
|
// Check that we're correctly rotating all the user's sends
|
||||||
let existing_send_ids = existing_sends.iter().map(|s| &s.uuid).collect::<HashSet<&SendId>>();
|
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>>();
|
let provided_send_ids = data.account_data.sends.iter().filter_map(|s| s.id.as_ref()).collect::<HashSet<&SendId>>();
|
||||||
if !provided_send_ids.is_superset(&existing_send_ids) {
|
if !provided_send_ids.is_superset(&existing_send_ids) {
|
||||||
err!("All existing sends must be included in the rotation")
|
err!("All existing sends must be included in the rotation")
|
||||||
}
|
}
|
||||||
@@ -623,12 +675,12 @@ fn validate_keydata(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/accounts/key", data = "<data>")]
|
#[post("/accounts/key-management/rotate-user-account-keys", data = "<data>")]
|
||||||
async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
||||||
// TODO: See if we can wrap everything within a SQL Transaction. If something fails it should revert everything.
|
// TODO: See if we can wrap everything within a SQL Transaction. If something fails it should revert everything.
|
||||||
let data: KeyData = data.into_inner();
|
let data: KeyData = data.into_inner();
|
||||||
|
|
||||||
if !headers.user.check_valid_password(&data.master_password_hash) {
|
if !headers.user.check_valid_password(&data.old_master_key_authentication_hash) {
|
||||||
err!("Invalid password")
|
err!("Invalid password")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -636,7 +688,7 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn,
|
|||||||
// Bitwarden does not process the import if there is one item invalid.
|
// Bitwarden does not process the import if there is one item invalid.
|
||||||
// Since we check for the size of the encrypted note length, we need to do that here to pre-validate it.
|
// Since we check for the size of the encrypted note length, we need to do that here to pre-validate it.
|
||||||
// TODO: See if we can optimize the whole cipher adding/importing and prevent duplicate code and checks.
|
// TODO: See if we can optimize the whole cipher adding/importing and prevent duplicate code and checks.
|
||||||
Cipher::validate_cipher_data(&data.ciphers)?;
|
Cipher::validate_cipher_data(&data.account_data.ciphers)?;
|
||||||
|
|
||||||
let user_id = &headers.user.uuid;
|
let user_id = &headers.user.uuid;
|
||||||
|
|
||||||
@@ -657,10 +709,11 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn,
|
|||||||
&existing_emergency_access,
|
&existing_emergency_access,
|
||||||
&existing_memberships,
|
&existing_memberships,
|
||||||
&existing_sends,
|
&existing_sends,
|
||||||
|
&headers.user,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Update folder data
|
// Update folder data
|
||||||
for folder_data in data.folders {
|
for folder_data in data.account_data.folders {
|
||||||
// Skip `null` folder id entries.
|
// Skip `null` folder id entries.
|
||||||
// See: https://github.com/bitwarden/clients/issues/8453
|
// See: https://github.com/bitwarden/clients/issues/8453
|
||||||
if let Some(folder_id) = folder_data.id {
|
if let Some(folder_id) = folder_data.id {
|
||||||
@@ -674,7 +727,7 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update emergency access data
|
// Update emergency access data
|
||||||
for emergency_access_data in data.emergency_access_keys {
|
for emergency_access_data in data.account_unlock_data.emergency_access_unlock_data {
|
||||||
let Some(saved_emergency_access) =
|
let Some(saved_emergency_access) =
|
||||||
existing_emergency_access.iter_mut().find(|ea| ea.uuid == emergency_access_data.id)
|
existing_emergency_access.iter_mut().find(|ea| ea.uuid == emergency_access_data.id)
|
||||||
else {
|
else {
|
||||||
@@ -686,7 +739,7 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update reset password data
|
// Update reset password data
|
||||||
for reset_password_data in data.reset_password_keys {
|
for reset_password_data in data.account_unlock_data.organization_account_recovery_unlock_data {
|
||||||
let Some(membership) =
|
let Some(membership) =
|
||||||
existing_memberships.iter_mut().find(|m| m.org_uuid == reset_password_data.organization_id)
|
existing_memberships.iter_mut().find(|m| m.org_uuid == reset_password_data.organization_id)
|
||||||
else {
|
else {
|
||||||
@@ -698,7 +751,7 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update send data
|
// Update send data
|
||||||
for send_data in data.sends {
|
for send_data in data.account_data.sends {
|
||||||
let Some(send) = existing_sends.iter_mut().find(|s| &s.uuid == send_data.id.as_ref().unwrap()) else {
|
let Some(send) = existing_sends.iter_mut().find(|s| &s.uuid == send_data.id.as_ref().unwrap()) else {
|
||||||
err!("Send doesn't exist")
|
err!("Send doesn't exist")
|
||||||
};
|
};
|
||||||
@@ -709,7 +762,7 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn,
|
|||||||
// Update cipher data
|
// Update cipher data
|
||||||
use super::ciphers::update_cipher_from_data;
|
use super::ciphers::update_cipher_from_data;
|
||||||
|
|
||||||
for cipher_data in data.ciphers {
|
for cipher_data in data.account_data.ciphers {
|
||||||
if cipher_data.organization_id.is_none() {
|
if cipher_data.organization_id.is_none() {
|
||||||
let Some(saved_cipher) = existing_ciphers.iter_mut().find(|c| &c.uuid == cipher_data.id.as_ref().unwrap())
|
let Some(saved_cipher) = existing_ciphers.iter_mut().find(|c| &c.uuid == cipher_data.id.as_ref().unwrap())
|
||||||
else {
|
else {
|
||||||
@@ -726,16 +779,20 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn,
|
|||||||
// Update user data
|
// Update user data
|
||||||
let mut user = headers.user;
|
let mut user = headers.user;
|
||||||
|
|
||||||
user.akey = data.key;
|
user.private_key = Some(data.account_keys.user_key_encrypted_account_private_key);
|
||||||
user.private_key = Some(data.private_key);
|
user.set_password(
|
||||||
user.reset_security_stamp();
|
&data.account_unlock_data.master_password_unlock_data.master_key_authentication_hash,
|
||||||
|
Some(data.account_unlock_data.master_password_unlock_data.master_key_encrypted_user_key),
|
||||||
|
true,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
let save_result = user.save(&mut conn).await;
|
let save_result = user.save(&mut conn).await;
|
||||||
|
|
||||||
// Prevent logging out the client where the user requested this endpoint from.
|
// Prevent logging out the client where the user requested this endpoint from.
|
||||||
// If you do logout the user it will causes issues at the client side.
|
// If you do logout the user it will causes issues at the client side.
|
||||||
// Adding the device uuid will prevent this.
|
// Adding the device uuid will prevent this.
|
||||||
nt.send_logout(&user, Some(headers.device.uuid.clone())).await;
|
nt.send_logout(&user, Some(headers.device.uuid.clone()), &mut conn).await;
|
||||||
|
|
||||||
save_result
|
save_result
|
||||||
}
|
}
|
||||||
@@ -751,7 +808,7 @@ async fn post_sstamp(data: Json<PasswordOrOtpData>, headers: Headers, mut conn:
|
|||||||
user.reset_security_stamp();
|
user.reset_security_stamp();
|
||||||
let save_result = user.save(&mut conn).await;
|
let save_result = user.save(&mut conn).await;
|
||||||
|
|
||||||
nt.send_logout(&user, None).await;
|
nt.send_logout(&user, None, &mut conn).await;
|
||||||
|
|
||||||
save_result
|
save_result
|
||||||
}
|
}
|
||||||
@@ -777,6 +834,11 @@ async fn post_email_token(data: Json<EmailTokenData>, headers: Headers, mut conn
|
|||||||
}
|
}
|
||||||
|
|
||||||
if User::find_by_mail(&data.new_email, &mut conn).await.is_some() {
|
if User::find_by_mail(&data.new_email, &mut conn).await.is_some() {
|
||||||
|
if CONFIG.mail_enabled() {
|
||||||
|
if let Err(e) = mail::send_change_email_existing(&data.new_email, &user.email).await {
|
||||||
|
error!("Error sending change-email-existing email: {e:#?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
err!("Email already in use");
|
err!("Email already in use");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -859,7 +921,7 @@ async fn post_email(data: Json<ChangeEmailData>, headers: Headers, mut conn: DbC
|
|||||||
|
|
||||||
let save_result = user.save(&mut conn).await;
|
let save_result = user.save(&mut conn).await;
|
||||||
|
|
||||||
nt.send_logout(&user, None).await;
|
nt.send_logout(&user, None, &mut conn).await;
|
||||||
|
|
||||||
save_result
|
save_result
|
||||||
}
|
}
|
||||||
@@ -1057,7 +1119,7 @@ pub async fn _prelogin(data: Json<PreloginData>, mut conn: DbConn) -> Json<Value
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://github.com/bitwarden/server/blob/master/src/Api/Models/Request/Accounts/SecretVerificationRequestModel.cs
|
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Auth/Models/Request/Accounts/SecretVerificationRequestModel.cs
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct SecretVerificationRequest {
|
struct SecretVerificationRequest {
|
||||||
@@ -1065,7 +1127,7 @@ struct SecretVerificationRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/accounts/verify-password", data = "<data>")]
|
#[post("/accounts/verify-password", data = "<data>")]
|
||||||
fn verify_password(data: Json<SecretVerificationRequest>, headers: Headers) -> EmptyResult {
|
async fn verify_password(data: Json<SecretVerificationRequest>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||||
let data: SecretVerificationRequest = data.into_inner();
|
let data: SecretVerificationRequest = data.into_inner();
|
||||||
let user = headers.user;
|
let user = headers.user;
|
||||||
|
|
||||||
@@ -1073,7 +1135,7 @@ fn verify_password(data: Json<SecretVerificationRequest>, headers: Headers) -> E
|
|||||||
err!("Invalid password")
|
err!("Invalid password")
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(Json(master_password_policy(&user, &conn).await))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn _api_key(data: Json<PasswordOrOtpData>, rotate: bool, headers: Headers, mut conn: DbConn) -> JsonResult {
|
async fn _api_key(data: Json<PasswordOrOtpData>, rotate: bool, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||||
@@ -1198,19 +1260,14 @@ async fn put_device_token(
|
|||||||
err!(format!("Error: device {device_id} should be present before a token can be assigned"))
|
err!(format!("Error: device {device_id} should be present before a token can be assigned"))
|
||||||
};
|
};
|
||||||
|
|
||||||
// if the device already has been registered
|
// Check if the new token is the same as the registered token
|
||||||
if device.is_registered() {
|
// Although upstream seems to always register a device on login, we do not.
|
||||||
// check if the new token is the same as the registered token
|
// Unless this causes issues, lets keep it this way, else we might need to also register on every login.
|
||||||
if device.push_token.is_some() && device.push_token.unwrap() == token.clone() {
|
if device.push_token.as_ref() == Some(&token) {
|
||||||
debug!("Device {device_id} is already registered and token is the same");
|
debug!("Device {device_id} for user {} is already registered and token is identical", headers.user.uuid);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
} else {
|
|
||||||
// Try to unregister already registered device
|
|
||||||
unregister_push_device(device.push_uuid).await.ok();
|
|
||||||
}
|
|
||||||
// clear the push_uuid
|
|
||||||
device.push_uuid = None;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
device.push_token = Some(token);
|
device.push_token = Some(token);
|
||||||
if let Err(e) = device.save(&mut conn).await {
|
if let Err(e) = device.save(&mut conn).await {
|
||||||
err!(format!("An error occurred while trying to save the device push token: {e}"));
|
err!(format!("An error occurred while trying to save the device push token: {e}"));
|
||||||
@@ -1224,16 +1281,19 @@ async fn put_device_token(
|
|||||||
#[put("/devices/identifier/<device_id>/clear-token")]
|
#[put("/devices/identifier/<device_id>/clear-token")]
|
||||||
async fn put_clear_device_token(device_id: DeviceId, mut conn: DbConn) -> EmptyResult {
|
async fn put_clear_device_token(device_id: DeviceId, mut conn: DbConn) -> EmptyResult {
|
||||||
// This only clears push token
|
// This only clears push token
|
||||||
// https://github.com/bitwarden/core/blob/master/src/Api/Controllers/DevicesController.cs#L109
|
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Controllers/DevicesController.cs#L215
|
||||||
// https://github.com/bitwarden/core/blob/master/src/Core/Services/Implementations/DeviceService.cs#L37
|
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/Services/Implementations/DeviceService.cs#L37
|
||||||
// This is somehow not implemented in any app, added it in case it is required
|
// This is somehow not implemented in any app, added it in case it is required
|
||||||
|
// 2025: Also, it looks like it only clears the first found device upstream, which is probably faulty.
|
||||||
|
// This because currently multiple accounts could be on the same device/app and that would cause issues.
|
||||||
|
// Vaultwarden removes the push-token for all devices, but this probably means we should also unregister all these devices.
|
||||||
if !CONFIG.push_enabled() {
|
if !CONFIG.push_enabled() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(device) = Device::find_by_uuid(&device_id, &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?;
|
Device::clear_push_token_by_uuid(&device_id, &mut conn).await?;
|
||||||
unregister_push_device(device.push_uuid).await?;
|
unregister_push_device(&device.push_uuid).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -1271,10 +1331,10 @@ async fn post_auth_request(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Validate device uuid and type
|
// Validate device uuid and type
|
||||||
match Device::find_by_uuid_and_user(&data.device_identifier, &user.uuid, &mut conn).await {
|
let device = match Device::find_by_uuid_and_user(&data.device_identifier, &user.uuid, &mut conn).await {
|
||||||
Some(device) if device.atype == client_headers.device_type => {}
|
Some(device) if device.atype == client_headers.device_type => device,
|
||||||
_ => err!("AuthRequest doesn't exist", "Device verification failed"),
|
_ => err!("AuthRequest doesn't exist", "Device verification failed"),
|
||||||
}
|
};
|
||||||
|
|
||||||
let mut auth_request = AuthRequest::new(
|
let mut auth_request = AuthRequest::new(
|
||||||
user.uuid.clone(),
|
user.uuid.clone(),
|
||||||
@@ -1286,7 +1346,7 @@ async fn post_auth_request(
|
|||||||
);
|
);
|
||||||
auth_request.save(&mut conn).await?;
|
auth_request.save(&mut conn).await?;
|
||||||
|
|
||||||
nt.send_auth_request(&user.uuid, &auth_request.uuid, &data.device_identifier, &mut conn).await;
|
nt.send_auth_request(&user.uuid, &auth_request.uuid, &device, &mut conn).await;
|
||||||
|
|
||||||
log_user_event(
|
log_user_event(
|
||||||
EventType::UserRequestedDeviceApproval as i32,
|
EventType::UserRequestedDeviceApproval as i32,
|
||||||
@@ -1361,6 +1421,10 @@ async fn put_auth_request(
|
|||||||
err!("AuthRequest doesn't exist", "Record not found or user uuid does not match")
|
err!("AuthRequest doesn't exist", "Record not found or user uuid does not match")
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if headers.device.uuid != data.device_identifier {
|
||||||
|
err!("AuthRequest doesn't exist", "Device verification failed")
|
||||||
|
}
|
||||||
|
|
||||||
if auth_request.approved.is_some() {
|
if auth_request.approved.is_some() {
|
||||||
err!("An authentication request with the same device already exists")
|
err!("An authentication request with the same device already exists")
|
||||||
}
|
}
|
||||||
@@ -1377,7 +1441,7 @@ async fn put_auth_request(
|
|||||||
auth_request.save(&mut conn).await?;
|
auth_request.save(&mut conn).await?;
|
||||||
|
|
||||||
ant.send_auth_response(&auth_request.user_uuid, &auth_request.uuid).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, &headers.device, &mut conn).await;
|
||||||
|
|
||||||
log_user_event(
|
log_user_event(
|
||||||
EventType::OrganizationUserApprovedAuthRequest as i32,
|
EventType::OrganizationUserApprovedAuthRequest as i32,
|
||||||
|
|||||||
+51
-51
@@ -11,10 +11,11 @@ use rocket::{
|
|||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::auth::ClientVersion;
|
use crate::auth::ClientVersion;
|
||||||
use crate::util::NumberOrString;
|
use crate::util::{save_temp_file, NumberOrString};
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{self, core::log_event, EmptyResult, JsonResult, Notify, PasswordOrOtpData, UpdateType},
|
api::{self, core::log_event, EmptyResult, JsonResult, Notify, PasswordOrOtpData, UpdateType},
|
||||||
auth::Headers,
|
auth::Headers,
|
||||||
|
config::PathType,
|
||||||
crypto,
|
crypto,
|
||||||
db::{models::*, DbConn, DbPool},
|
db::{models::*, DbConn, DbPool},
|
||||||
CONFIG,
|
CONFIG,
|
||||||
@@ -105,12 +106,7 @@ struct SyncData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/sync?<data..>")]
|
#[get("/sync?<data..>")]
|
||||||
async fn sync(
|
async fn sync(data: SyncData, headers: Headers, client_version: Option<ClientVersion>, mut conn: DbConn) -> JsonResult {
|
||||||
data: SyncData,
|
|
||||||
headers: Headers,
|
|
||||||
client_version: Option<ClientVersion>,
|
|
||||||
mut conn: DbConn,
|
|
||||||
) -> Json<Value> {
|
|
||||||
let user_json = headers.user.to_json(&mut conn).await;
|
let user_json = headers.user.to_json(&mut conn).await;
|
||||||
|
|
||||||
// Get all ciphers which are visible by the user
|
// Get all ciphers which are visible by the user
|
||||||
@@ -134,7 +130,7 @@ async fn sync(
|
|||||||
for c in ciphers {
|
for c in ciphers {
|
||||||
ciphers_json.push(
|
ciphers_json.push(
|
||||||
c.to_json(&headers.host, &headers.user.uuid, Some(&cipher_sync_data), CipherSyncType::User, &mut conn)
|
c.to_json(&headers.host, &headers.user.uuid, Some(&cipher_sync_data), CipherSyncType::User, &mut conn)
|
||||||
.await,
|
.await?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,7 +155,7 @@ async fn sync(
|
|||||||
api::core::_get_eq_domains(headers, true).into_inner()
|
api::core::_get_eq_domains(headers, true).into_inner()
|
||||||
};
|
};
|
||||||
|
|
||||||
Json(json!({
|
Ok(Json(json!({
|
||||||
"profile": user_json,
|
"profile": user_json,
|
||||||
"folders": folders_json,
|
"folders": folders_json,
|
||||||
"collections": collections_json,
|
"collections": collections_json,
|
||||||
@@ -168,11 +164,11 @@ async fn sync(
|
|||||||
"domains": domains_json,
|
"domains": domains_json,
|
||||||
"sends": sends_json,
|
"sends": sends_json,
|
||||||
"object": "sync"
|
"object": "sync"
|
||||||
}))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/ciphers")]
|
#[get("/ciphers")]
|
||||||
async fn get_ciphers(headers: Headers, mut conn: DbConn) -> Json<Value> {
|
async fn get_ciphers(headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||||
let ciphers = Cipher::find_by_user_visible(&headers.user.uuid, &mut conn).await;
|
let ciphers = Cipher::find_by_user_visible(&headers.user.uuid, &mut conn).await;
|
||||||
let cipher_sync_data = CipherSyncData::new(&headers.user.uuid, CipherSyncType::User, &mut conn).await;
|
let cipher_sync_data = CipherSyncData::new(&headers.user.uuid, CipherSyncType::User, &mut conn).await;
|
||||||
|
|
||||||
@@ -180,15 +176,15 @@ async fn get_ciphers(headers: Headers, mut conn: DbConn) -> Json<Value> {
|
|||||||
for c in ciphers {
|
for c in ciphers {
|
||||||
ciphers_json.push(
|
ciphers_json.push(
|
||||||
c.to_json(&headers.host, &headers.user.uuid, Some(&cipher_sync_data), CipherSyncType::User, &mut conn)
|
c.to_json(&headers.host, &headers.user.uuid, Some(&cipher_sync_data), CipherSyncType::User, &mut conn)
|
||||||
.await,
|
.await?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Json(json!({
|
Ok(Json(json!({
|
||||||
"data": ciphers_json,
|
"data": ciphers_json,
|
||||||
"object": "list",
|
"object": "list",
|
||||||
"continuationToken": null
|
"continuationToken": null
|
||||||
}))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/ciphers/<cipher_id>")]
|
#[get("/ciphers/<cipher_id>")]
|
||||||
@@ -201,7 +197,7 @@ async fn get_cipher(cipher_id: CipherId, headers: Headers, mut conn: DbConn) ->
|
|||||||
err!("Cipher is not owned by user")
|
err!("Cipher is not owned by user")
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await))
|
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await?))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/ciphers/<cipher_id>/admin")]
|
#[get("/ciphers/<cipher_id>/admin")]
|
||||||
@@ -339,7 +335,7 @@ async fn post_ciphers(data: Json<CipherData>, headers: Headers, mut conn: DbConn
|
|||||||
let mut cipher = Cipher::new(data.r#type, data.name.clone());
|
let mut cipher = Cipher::new(data.r#type, data.name.clone());
|
||||||
update_cipher_from_data(&mut cipher, data, &headers, None, &mut conn, &nt, UpdateType::SyncCipherCreate).await?;
|
update_cipher_from_data(&mut cipher, data, &headers, None, &mut conn, &nt, UpdateType::SyncCipherCreate).await?;
|
||||||
|
|
||||||
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await))
|
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await?))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enforces the personal ownership policy on user-owned ciphers, if applicable.
|
/// Enforces the personal ownership policy on user-owned ciphers, if applicable.
|
||||||
@@ -535,7 +531,7 @@ pub async fn update_cipher_from_data(
|
|||||||
ut,
|
ut,
|
||||||
cipher,
|
cipher,
|
||||||
&cipher.update_users_revision(conn).await,
|
&cipher.update_users_revision(conn).await,
|
||||||
&headers.device.uuid,
|
&headers.device,
|
||||||
shared_to_collections,
|
shared_to_collections,
|
||||||
conn,
|
conn,
|
||||||
)
|
)
|
||||||
@@ -612,7 +608,7 @@ async fn post_ciphers_import(
|
|||||||
|
|
||||||
let mut user = headers.user;
|
let mut user = headers.user;
|
||||||
user.update_revision(&mut conn).await?;
|
user.update_revision(&mut conn).await?;
|
||||||
nt.send_user_update(UpdateType::SyncVault, &user).await;
|
nt.send_user_update(UpdateType::SyncVault, &user, &headers.device.push_uuid, &mut conn).await;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -676,7 +672,7 @@ async fn put_cipher(
|
|||||||
|
|
||||||
update_cipher_from_data(&mut cipher, data, &headers, None, &mut conn, &nt, UpdateType::SyncCipherUpdate).await?;
|
update_cipher_from_data(&mut cipher, data, &headers, None, &mut conn, &nt, UpdateType::SyncCipherUpdate).await?;
|
||||||
|
|
||||||
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await))
|
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await?))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/ciphers/<cipher_id>/partial", data = "<data>")]
|
#[post("/ciphers/<cipher_id>/partial", data = "<data>")]
|
||||||
@@ -714,7 +710,7 @@ async fn put_cipher_partial(
|
|||||||
// Update favorite
|
// Update favorite
|
||||||
cipher.set_favorite(Some(data.favorite), &headers.user.uuid, &mut conn).await?;
|
cipher.set_favorite(Some(data.favorite), &headers.user.uuid, &mut conn).await?;
|
||||||
|
|
||||||
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await))
|
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await?))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@@ -808,7 +804,7 @@ async fn post_collections_update(
|
|||||||
UpdateType::SyncCipherUpdate,
|
UpdateType::SyncCipherUpdate,
|
||||||
&cipher,
|
&cipher,
|
||||||
&cipher.update_users_revision(&mut conn).await,
|
&cipher.update_users_revision(&mut conn).await,
|
||||||
&headers.device.uuid,
|
&headers.device,
|
||||||
Some(Vec::from_iter(posted_collections)),
|
Some(Vec::from_iter(posted_collections)),
|
||||||
&mut conn,
|
&mut conn,
|
||||||
)
|
)
|
||||||
@@ -825,7 +821,7 @@ async fn post_collections_update(
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await))
|
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await?))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[put("/ciphers/<cipher_id>/collections-admin", data = "<data>")]
|
#[put("/ciphers/<cipher_id>/collections-admin", data = "<data>")]
|
||||||
@@ -885,7 +881,7 @@ async fn post_collections_admin(
|
|||||||
UpdateType::SyncCipherUpdate,
|
UpdateType::SyncCipherUpdate,
|
||||||
&cipher,
|
&cipher,
|
||||||
&cipher.update_users_revision(&mut conn).await,
|
&cipher.update_users_revision(&mut conn).await,
|
||||||
&headers.device.uuid,
|
&headers.device,
|
||||||
Some(Vec::from_iter(posted_collections)),
|
Some(Vec::from_iter(posted_collections)),
|
||||||
&mut conn,
|
&mut conn,
|
||||||
)
|
)
|
||||||
@@ -1030,7 +1026,7 @@ async fn share_cipher_by_uuid(
|
|||||||
|
|
||||||
update_cipher_from_data(&mut cipher, data.cipher, headers, Some(shared_to_collections), conn, nt, ut).await?;
|
update_cipher_from_data(&mut cipher, data.cipher, headers, Some(shared_to_collections), conn, nt, ut).await?;
|
||||||
|
|
||||||
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, conn).await))
|
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, conn).await?))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// v2 API for downloading an attachment. This just redirects the client to
|
/// v2 API for downloading an attachment. This just redirects the client to
|
||||||
@@ -1055,7 +1051,7 @@ async fn get_attachment(
|
|||||||
}
|
}
|
||||||
|
|
||||||
match Attachment::find_by_id(&attachment_id, &mut conn).await {
|
match Attachment::find_by_id(&attachment_id, &mut conn).await {
|
||||||
Some(attachment) if cipher_id == attachment.cipher_uuid => Ok(Json(attachment.to_json(&headers.host))),
|
Some(attachment) if cipher_id == attachment.cipher_uuid => Ok(Json(attachment.to_json(&headers.host).await?)),
|
||||||
Some(_) => err!("Attachment doesn't belong to cipher"),
|
Some(_) => err!("Attachment doesn't belong to cipher"),
|
||||||
None => err!("Attachment doesn't exist"),
|
None => err!("Attachment doesn't exist"),
|
||||||
}
|
}
|
||||||
@@ -1116,7 +1112,7 @@ async fn post_attachment_v2(
|
|||||||
"attachmentId": attachment_id,
|
"attachmentId": attachment_id,
|
||||||
"url": url,
|
"url": url,
|
||||||
"fileUploadType": FileUploadType::Direct as i32,
|
"fileUploadType": FileUploadType::Direct as i32,
|
||||||
response_key: cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await,
|
response_key: cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await?,
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1142,7 +1138,7 @@ async fn save_attachment(
|
|||||||
mut conn: DbConn,
|
mut conn: DbConn,
|
||||||
nt: Notify<'_>,
|
nt: Notify<'_>,
|
||||||
) -> Result<(Cipher, DbConn), crate::error::Error> {
|
) -> Result<(Cipher, DbConn), crate::error::Error> {
|
||||||
let mut data = data.into_inner();
|
let data = data.into_inner();
|
||||||
|
|
||||||
let Some(size) = data.data.len().to_i64() else {
|
let Some(size) = data.data.len().to_i64() else {
|
||||||
err!("Attachment data size overflow");
|
err!("Attachment data size overflow");
|
||||||
@@ -1269,19 +1265,13 @@ async fn save_attachment(
|
|||||||
attachment.save(&mut conn).await.expect("Error saving attachment");
|
attachment.save(&mut conn).await.expect("Error saving attachment");
|
||||||
}
|
}
|
||||||
|
|
||||||
let folder_path = tokio::fs::canonicalize(&CONFIG.attachments_folder()).await?.join(cipher_id.as_ref());
|
save_temp_file(PathType::Attachments, &format!("{cipher_id}/{file_id}"), data.data, true).await?;
|
||||||
let file_path = folder_path.join(file_id.as_ref());
|
|
||||||
tokio::fs::create_dir_all(&folder_path).await?;
|
|
||||||
|
|
||||||
if let Err(_err) = data.data.persist_to(&file_path).await {
|
|
||||||
data.data.move_copy_to(file_path).await?
|
|
||||||
}
|
|
||||||
|
|
||||||
nt.send_cipher_update(
|
nt.send_cipher_update(
|
||||||
UpdateType::SyncCipherUpdate,
|
UpdateType::SyncCipherUpdate,
|
||||||
&cipher,
|
&cipher,
|
||||||
&cipher.update_users_revision(&mut conn).await,
|
&cipher.update_users_revision(&mut conn).await,
|
||||||
&headers.device.uuid,
|
&headers.device,
|
||||||
None,
|
None,
|
||||||
&mut conn,
|
&mut conn,
|
||||||
)
|
)
|
||||||
@@ -1342,7 +1332,7 @@ async fn post_attachment(
|
|||||||
|
|
||||||
let (cipher, mut conn) = save_attachment(attachment, cipher_id, data, &headers, conn, nt).await?;
|
let (cipher, mut conn) = save_attachment(attachment, cipher_id, data, &headers, conn, nt).await?;
|
||||||
|
|
||||||
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await))
|
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await?))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/ciphers/<cipher_id>/attachment-admin", format = "multipart/form-data", data = "<data>")]
|
#[post("/ciphers/<cipher_id>/attachment-admin", format = "multipart/form-data", data = "<data>")]
|
||||||
@@ -1581,8 +1571,8 @@ async fn move_cipher_selected(
|
|||||||
nt.send_cipher_update(
|
nt.send_cipher_update(
|
||||||
UpdateType::SyncCipherUpdate,
|
UpdateType::SyncCipherUpdate,
|
||||||
&cipher,
|
&cipher,
|
||||||
&[user_id.clone()],
|
std::slice::from_ref(&user_id),
|
||||||
&headers.device.uuid,
|
&headers.device,
|
||||||
None,
|
None,
|
||||||
&mut conn,
|
&mut conn,
|
||||||
)
|
)
|
||||||
@@ -1629,7 +1619,7 @@ async fn delete_all(
|
|||||||
Some(member) => {
|
Some(member) => {
|
||||||
if member.atype == MembershipType::Owner {
|
if member.atype == MembershipType::Owner {
|
||||||
Cipher::delete_all_by_organization(&org_data.org_id, &mut conn).await?;
|
Cipher::delete_all_by_organization(&org_data.org_id, &mut conn).await?;
|
||||||
nt.send_user_update(UpdateType::SyncVault, &user).await;
|
nt.send_user_update(UpdateType::SyncVault, &user, &headers.device.push_uuid, &mut conn).await;
|
||||||
|
|
||||||
log_event(
|
log_event(
|
||||||
EventType::OrganizationPurgedVault as i32,
|
EventType::OrganizationPurgedVault as i32,
|
||||||
@@ -1662,7 +1652,7 @@ async fn delete_all(
|
|||||||
}
|
}
|
||||||
|
|
||||||
user.update_revision(&mut conn).await?;
|
user.update_revision(&mut conn).await?;
|
||||||
nt.send_user_update(UpdateType::SyncVault, &user).await;
|
nt.send_user_update(UpdateType::SyncVault, &user, &headers.device.push_uuid, &mut conn).await;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -1691,7 +1681,7 @@ async fn _delete_cipher_by_uuid(
|
|||||||
UpdateType::SyncCipherUpdate,
|
UpdateType::SyncCipherUpdate,
|
||||||
&cipher,
|
&cipher,
|
||||||
&cipher.update_users_revision(conn).await,
|
&cipher.update_users_revision(conn).await,
|
||||||
&headers.device.uuid,
|
&headers.device,
|
||||||
None,
|
None,
|
||||||
conn,
|
conn,
|
||||||
)
|
)
|
||||||
@@ -1702,7 +1692,7 @@ async fn _delete_cipher_by_uuid(
|
|||||||
UpdateType::SyncCipherDelete,
|
UpdateType::SyncCipherDelete,
|
||||||
&cipher,
|
&cipher,
|
||||||
&cipher.update_users_revision(conn).await,
|
&cipher.update_users_revision(conn).await,
|
||||||
&headers.device.uuid,
|
&headers.device,
|
||||||
None,
|
None,
|
||||||
conn,
|
conn,
|
||||||
)
|
)
|
||||||
@@ -1767,7 +1757,7 @@ async fn _restore_cipher_by_uuid(
|
|||||||
UpdateType::SyncCipherUpdate,
|
UpdateType::SyncCipherUpdate,
|
||||||
&cipher,
|
&cipher,
|
||||||
&cipher.update_users_revision(conn).await,
|
&cipher.update_users_revision(conn).await,
|
||||||
&headers.device.uuid,
|
&headers.device,
|
||||||
None,
|
None,
|
||||||
conn,
|
conn,
|
||||||
)
|
)
|
||||||
@@ -1786,7 +1776,7 @@ async fn _restore_cipher_by_uuid(
|
|||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, conn).await))
|
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, conn).await?))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn _restore_multiple_ciphers(
|
async fn _restore_multiple_ciphers(
|
||||||
@@ -1841,7 +1831,7 @@ async fn _delete_cipher_attachment_by_id(
|
|||||||
UpdateType::SyncCipherUpdate,
|
UpdateType::SyncCipherUpdate,
|
||||||
&cipher,
|
&cipher,
|
||||||
&cipher.update_users_revision(conn).await,
|
&cipher.update_users_revision(conn).await,
|
||||||
&headers.device.uuid,
|
&headers.device,
|
||||||
None,
|
None,
|
||||||
conn,
|
conn,
|
||||||
)
|
)
|
||||||
@@ -1859,7 +1849,7 @@ async fn _delete_cipher_attachment_by_id(
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
let cipher_json = cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, conn).await;
|
let cipher_json = cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, conn).await?;
|
||||||
Ok(Json(json!({"cipher":cipher_json})))
|
Ok(Json(json!({"cipher":cipher_json})))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1934,11 +1924,21 @@ impl CipherSyncData {
|
|||||||
|
|
||||||
// Generate a HashMap with the collections_uuid as key and the CollectionGroup record
|
// Generate a HashMap with the collections_uuid as key and the CollectionGroup record
|
||||||
let user_collections_groups: HashMap<CollectionId, CollectionGroup> = if CONFIG.org_groups_enabled() {
|
let user_collections_groups: HashMap<CollectionId, CollectionGroup> = if CONFIG.org_groups_enabled() {
|
||||||
CollectionGroup::find_by_user(user_id, conn)
|
CollectionGroup::find_by_user(user_id, conn).await.into_iter().fold(
|
||||||
.await
|
HashMap::new(),
|
||||||
.into_iter()
|
|mut combined_permissions, cg| {
|
||||||
.map(|collection_group| (collection_group.collections_uuid.clone(), collection_group))
|
combined_permissions
|
||||||
.collect()
|
.entry(cg.collections_uuid.clone())
|
||||||
|
.and_modify(|existing| {
|
||||||
|
// Combine permissions: take the most permissive settings.
|
||||||
|
existing.read_only &= cg.read_only; // false if ANY group allows write
|
||||||
|
existing.hide_passwords &= cg.hide_passwords; // false if ANY group allows password view
|
||||||
|
existing.manage |= cg.manage; // true if ANY group allows manage
|
||||||
|
})
|
||||||
|
.or_insert(cg);
|
||||||
|
combined_permissions
|
||||||
|
},
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
HashMap::new()
|
HashMap::new()
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -582,7 +582,7 @@ async fn view_emergency_access(emer_id: EmergencyAccessId, headers: Headers, mut
|
|||||||
CipherSyncType::User,
|
CipherSyncType::User,
|
||||||
&mut conn,
|
&mut conn,
|
||||||
)
|
)
|
||||||
.await,
|
.await?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ struct EventRange {
|
|||||||
continuation_token: Option<String>,
|
continuation_token: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upstream: https://github.com/bitwarden/server/blob/9ecf69d9cabce732cf2c57976dd9afa5728578fb/src/Api/Controllers/EventsController.cs#LL84C35-L84C41
|
// Upstream: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/AdminConsole/Controllers/EventsController.cs#L87
|
||||||
#[get("/organizations/<org_id>/events?<data..>")]
|
#[get("/organizations/<org_id>/events?<data..>")]
|
||||||
async fn get_org_events(
|
async fn get_org_events(
|
||||||
org_id: OrganizationId,
|
org_id: OrganizationId,
|
||||||
@@ -169,8 +169,8 @@ struct EventCollection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Upstream:
|
// Upstream:
|
||||||
// https://github.com/bitwarden/server/blob/8a22c0479e987e756ce7412c48a732f9002f0a2d/src/Events/Controllers/CollectController.cs
|
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Events/Controllers/CollectController.cs
|
||||||
// https://github.com/bitwarden/server/blob/8a22c0479e987e756ce7412c48a732f9002f0a2d/src/Core/Services/Implementations/EventService.cs
|
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Services/Implementations/EventService.cs
|
||||||
#[post("/collect", format = "application/json", data = "<data>")]
|
#[post("/collect", format = "application/json", data = "<data>")]
|
||||||
async fn post_events_collect(data: Json<Vec<EventCollection>>, headers: Headers, mut conn: DbConn) -> EmptyResult {
|
async fn post_events_collect(data: Json<Vec<EventCollection>>, headers: Headers, mut conn: DbConn) -> EmptyResult {
|
||||||
if !CONFIG.org_events_enabled() {
|
if !CONFIG.org_events_enabled() {
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ async fn post_folders(data: Json<FolderData>, headers: Headers, mut conn: DbConn
|
|||||||
let mut folder = Folder::new(headers.user.uuid, data.name);
|
let mut folder = Folder::new(headers.user.uuid, data.name);
|
||||||
|
|
||||||
folder.save(&mut conn).await?;
|
folder.save(&mut conn).await?;
|
||||||
nt.send_folder_update(UpdateType::SyncFolderCreate, &folder, &headers.device.uuid, &mut conn).await;
|
nt.send_folder_update(UpdateType::SyncFolderCreate, &folder, &headers.device, &mut conn).await;
|
||||||
|
|
||||||
Ok(Json(folder.to_json()))
|
Ok(Json(folder.to_json()))
|
||||||
}
|
}
|
||||||
@@ -78,7 +78,7 @@ async fn put_folder(
|
|||||||
folder.name = data.name;
|
folder.name = data.name;
|
||||||
|
|
||||||
folder.save(&mut conn).await?;
|
folder.save(&mut conn).await?;
|
||||||
nt.send_folder_update(UpdateType::SyncFolderUpdate, &folder, &headers.device.uuid, &mut conn).await;
|
nt.send_folder_update(UpdateType::SyncFolderUpdate, &folder, &headers.device, &mut conn).await;
|
||||||
|
|
||||||
Ok(Json(folder.to_json()))
|
Ok(Json(folder.to_json()))
|
||||||
}
|
}
|
||||||
@@ -97,6 +97,6 @@ async fn delete_folder(folder_id: FolderId, headers: Headers, mut conn: DbConn,
|
|||||||
// Delete the actual folder entry
|
// Delete the actual folder entry
|
||||||
folder.delete(&mut conn).await?;
|
folder.delete(&mut conn).await?;
|
||||||
|
|
||||||
nt.send_folder_update(UpdateType::SyncFolderDelete, &folder, &headers.device.uuid, &mut conn).await;
|
nt.send_folder_update(UpdateType::SyncFolderDelete, &folder, &headers.device, &mut conn).await;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-6
@@ -124,7 +124,7 @@ async fn post_eq_domains(
|
|||||||
|
|
||||||
user.save(&mut conn).await?;
|
user.save(&mut conn).await?;
|
||||||
|
|
||||||
nt.send_user_update(UpdateType::SyncSettings, &user).await;
|
nt.send_user_update(UpdateType::SyncSettings, &user, &headers.device.push_uuid, &mut conn).await;
|
||||||
|
|
||||||
Ok(Json(json!({})))
|
Ok(Json(json!({})))
|
||||||
}
|
}
|
||||||
@@ -199,14 +199,18 @@ fn get_api_webauthn(_headers: Headers) -> Json<Value> {
|
|||||||
#[get("/config")]
|
#[get("/config")]
|
||||||
fn config() -> Json<Value> {
|
fn config() -> Json<Value> {
|
||||||
let domain = crate::CONFIG.domain();
|
let domain = crate::CONFIG.domain();
|
||||||
|
// Official available feature flags can be found here:
|
||||||
|
// Server (v2025.6.2): https://github.com/bitwarden/server/blob/d094be3267f2030bd0dc62106bc6871cf82682f5/src/Core/Constants.cs#L103
|
||||||
|
// Client (web-v2025.6.1): https://github.com/bitwarden/clients/blob/747c2fd6a1c348a57a76e4a7de8128466ffd3c01/libs/common/src/enums/feature-flag.enum.ts#L12
|
||||||
|
// Android (v2025.6.0): https://github.com/bitwarden/android/blob/b5b022caaad33390c31b3021b2c1205925b0e1a2/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/model/FlagKey.kt#L22
|
||||||
|
// iOS (v2025.6.0): https://github.com/bitwarden/ios/blob/ff06d9c6cc8da89f78f37f376495800201d7261a/BitwardenShared/Core/Platform/Models/Enum/FeatureFlag.swift#L7
|
||||||
let mut feature_states =
|
let mut feature_states =
|
||||||
parse_experimental_client_feature_flags(&crate::CONFIG.experimental_client_feature_flags());
|
parse_experimental_client_feature_flags(&crate::CONFIG.experimental_client_feature_flags());
|
||||||
// Force the new key rotation feature
|
feature_states.insert("duo-redirect".to_string(), true);
|
||||||
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("email-verification".to_string(), true);
|
||||||
feature_states.insert("unauth-ui-refresh".to_string(), true);
|
feature_states.insert("unauth-ui-refresh".to_string(), true);
|
||||||
|
feature_states.insert("enable-pm-flight-recorder".to_string(), true);
|
||||||
|
feature_states.insert("mobile-error-reporting".to_string(), true);
|
||||||
|
|
||||||
Json(json!({
|
Json(json!({
|
||||||
// Note: The clients use this version to handle backwards compatibility concerns
|
// Note: The clients use this version to handle backwards compatibility concerns
|
||||||
@@ -214,7 +218,7 @@ fn config() -> Json<Value> {
|
|||||||
// We should make sure that we keep this updated when we support the new server features
|
// We should make sure that we keep this updated when we support the new server features
|
||||||
// Version history:
|
// Version history:
|
||||||
// - Individual cipher key encryption: 2024.2.0
|
// - Individual cipher key encryption: 2024.2.0
|
||||||
"version": "2025.1.0",
|
"version": "2025.6.0",
|
||||||
"gitHash": option_env!("GIT_REV"),
|
"gitHash": option_env!("GIT_REV"),
|
||||||
"server": {
|
"server": {
|
||||||
"name": "Vaultwarden",
|
"name": "Vaultwarden",
|
||||||
@@ -229,6 +233,12 @@ fn config() -> Json<Value> {
|
|||||||
"identity": format!("{domain}/identity"),
|
"identity": format!("{domain}/identity"),
|
||||||
"notifications": format!("{domain}/notifications"),
|
"notifications": format!("{domain}/notifications"),
|
||||||
"sso": "",
|
"sso": "",
|
||||||
|
"cloudRegion": null,
|
||||||
|
},
|
||||||
|
// Bitwarden uses this for the self-hosted servers to indicate the default push technology
|
||||||
|
"push": {
|
||||||
|
"pushTechnology": 0,
|
||||||
|
"vapidPublicKey": null
|
||||||
},
|
},
|
||||||
"featureStates": feature_states,
|
"featureStates": feature_states,
|
||||||
"object": "config",
|
"object": "config",
|
||||||
|
|||||||
+151
-53
@@ -374,6 +374,21 @@ async fn get_org_collections_details(
|
|||||||
|| (CONFIG.org_groups_enabled()
|
|| (CONFIG.org_groups_enabled()
|
||||||
&& GroupUser::has_full_access_by_member(&org_id, &member.uuid, &mut conn).await);
|
&& GroupUser::has_full_access_by_member(&org_id, &member.uuid, &mut conn).await);
|
||||||
|
|
||||||
|
// Get all admins, owners and managers who can manage/access all
|
||||||
|
// Those are currently not listed in the col_users but need to be listed too.
|
||||||
|
let manage_all_members: Vec<Value> = Membership::find_confirmed_and_manage_all_by_org(&org_id, &mut conn)
|
||||||
|
.await
|
||||||
|
.into_iter()
|
||||||
|
.map(|member| {
|
||||||
|
json!({
|
||||||
|
"id": member.uuid,
|
||||||
|
"readOnly": false,
|
||||||
|
"hidePasswords": false,
|
||||||
|
"manage": true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
for col in Collection::find_by_organization(&org_id, &mut conn).await {
|
for col in Collection::find_by_organization(&org_id, &mut conn).await {
|
||||||
// check whether the current user has access to the given collection
|
// check whether the current user has access to the given collection
|
||||||
let assigned = has_full_access_to_org
|
let assigned = has_full_access_to_org
|
||||||
@@ -382,7 +397,7 @@ async fn get_org_collections_details(
|
|||||||
&& GroupUser::has_access_to_collection_by_member(&col.uuid, &member.uuid, &mut conn).await);
|
&& GroupUser::has_access_to_collection_by_member(&col.uuid, &member.uuid, &mut conn).await);
|
||||||
|
|
||||||
// get the users assigned directly to the given collection
|
// get the users assigned directly to the given collection
|
||||||
let users: Vec<Value> = col_users
|
let mut users: Vec<Value> = col_users
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|collection_member| collection_member.collection_uuid == col.uuid)
|
.filter(|collection_member| collection_member.collection_uuid == col.uuid)
|
||||||
.map(|collection_member| {
|
.map(|collection_member| {
|
||||||
@@ -391,6 +406,7 @@ async fn get_org_collections_details(
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
users.extend_from_slice(&manage_all_members);
|
||||||
|
|
||||||
// get the group details for the given collection
|
// get the group details for the given collection
|
||||||
let groups: Vec<Value> = if CONFIG.org_groups_enabled() {
|
let groups: Vec<Value> = if CONFIG.org_groups_enabled() {
|
||||||
@@ -681,6 +697,9 @@ async fn _delete_organization_collection(
|
|||||||
headers: &ManagerHeaders,
|
headers: &ManagerHeaders,
|
||||||
conn: &mut DbConn,
|
conn: &mut DbConn,
|
||||||
) -> EmptyResult {
|
) -> EmptyResult {
|
||||||
|
if org_id != &headers.org_id {
|
||||||
|
err!("Organization not found", "Organization id's do not match");
|
||||||
|
}
|
||||||
let Some(collection) = Collection::find_by_uuid_and_org(col_id, org_id, conn).await else {
|
let Some(collection) = Collection::find_by_uuid_and_org(col_id, org_id, conn).await else {
|
||||||
err!("Collection not found", "Collection does not exist or does not belong to this organization")
|
err!("Collection not found", "Collection does not exist or does not belong to this organization")
|
||||||
};
|
};
|
||||||
@@ -707,15 +726,6 @@ async fn delete_organization_collection(
|
|||||||
_delete_organization_collection(&org_id, &col_id, &headers, &mut conn).await
|
_delete_organization_collection(&org_id, &col_id, &headers, &mut conn).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
struct DeleteCollectionData {
|
|
||||||
#[allow(dead_code)]
|
|
||||||
id: String,
|
|
||||||
#[allow(dead_code)]
|
|
||||||
org_id: OrganizationId,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/organizations/<org_id>/collections/<col_id>/delete")]
|
#[post("/organizations/<org_id>/collections/<col_id>/delete")]
|
||||||
async fn post_organization_collection_delete(
|
async fn post_organization_collection_delete(
|
||||||
org_id: OrganizationId,
|
org_id: OrganizationId,
|
||||||
@@ -893,26 +903,31 @@ struct OrgIdData {
|
|||||||
|
|
||||||
#[get("/ciphers/organization-details?<data..>")]
|
#[get("/ciphers/organization-details?<data..>")]
|
||||||
async fn get_org_details(data: OrgIdData, headers: OrgMemberHeaders, mut conn: DbConn) -> JsonResult {
|
async fn get_org_details(data: OrgIdData, headers: OrgMemberHeaders, mut conn: DbConn) -> JsonResult {
|
||||||
if data.organization_id != headers.org_id {
|
if data.organization_id != headers.membership.org_uuid {
|
||||||
err_code!("Resource not found.", "Organization id's do not match", rocket::http::Status::NotFound.code);
|
err_code!("Resource not found.", "Organization id's do not match", rocket::http::Status::NotFound.code);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"data": _get_org_details(&data.organization_id, &headers.host, &headers.user.uuid, &mut conn).await,
|
"data": _get_org_details(&data.organization_id, &headers.host, &headers.user.uuid, &mut conn).await?,
|
||||||
"object": "list",
|
"object": "list",
|
||||||
"continuationToken": null,
|
"continuationToken": null,
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn _get_org_details(org_id: &OrganizationId, host: &str, user_id: &UserId, conn: &mut DbConn) -> Value {
|
async fn _get_org_details(
|
||||||
|
org_id: &OrganizationId,
|
||||||
|
host: &str,
|
||||||
|
user_id: &UserId,
|
||||||
|
conn: &mut DbConn,
|
||||||
|
) -> Result<Value, crate::Error> {
|
||||||
let ciphers = Cipher::find_by_org(org_id, conn).await;
|
let ciphers = Cipher::find_by_org(org_id, conn).await;
|
||||||
let cipher_sync_data = CipherSyncData::new(user_id, CipherSyncType::Organization, conn).await;
|
let cipher_sync_data = CipherSyncData::new(user_id, CipherSyncType::Organization, conn).await;
|
||||||
|
|
||||||
let mut ciphers_json = Vec::with_capacity(ciphers.len());
|
let mut ciphers_json = Vec::with_capacity(ciphers.len());
|
||||||
for c in ciphers {
|
for c in ciphers {
|
||||||
ciphers_json.push(c.to_json(host, user_id, Some(&cipher_sync_data), CipherSyncType::Organization, conn).await);
|
ciphers_json.push(c.to_json(host, user_id, Some(&cipher_sync_data), CipherSyncType::Organization, conn).await?);
|
||||||
}
|
}
|
||||||
json!(ciphers_json)
|
Ok(json!(ciphers_json))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(FromForm)]
|
#[derive(FromForm)]
|
||||||
@@ -1180,6 +1195,9 @@ async fn reinvite_member(
|
|||||||
headers: AdminHeaders,
|
headers: AdminHeaders,
|
||||||
mut conn: DbConn,
|
mut conn: DbConn,
|
||||||
) -> EmptyResult {
|
) -> EmptyResult {
|
||||||
|
if org_id != headers.org_id {
|
||||||
|
err!("Organization not found", "Organization id's do not match");
|
||||||
|
}
|
||||||
_reinvite_member(&org_id, &member_id, &headers.user.email, &mut conn).await
|
_reinvite_member(&org_id, &member_id, &headers.user.email, &mut conn).await
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1397,6 +1415,9 @@ async fn _confirm_invite(
|
|||||||
conn: &mut DbConn,
|
conn: &mut DbConn,
|
||||||
nt: &Notify<'_>,
|
nt: &Notify<'_>,
|
||||||
) -> EmptyResult {
|
) -> EmptyResult {
|
||||||
|
if org_id != &headers.org_id {
|
||||||
|
err!("Organization not found", "Organization id's do not match");
|
||||||
|
}
|
||||||
if key.is_empty() || member_id.is_empty() {
|
if key.is_empty() || member_id.is_empty() {
|
||||||
err!("Key or UserId is not set, unable to process request");
|
err!("Key or UserId is not set, unable to process request");
|
||||||
}
|
}
|
||||||
@@ -1460,7 +1481,7 @@ async fn _confirm_invite(
|
|||||||
let save_result = member_to_confirm.save(conn).await;
|
let save_result = member_to_confirm.save(conn).await;
|
||||||
|
|
||||||
if let Some(user) = User::find_by_uuid(&member_to_confirm.user_uuid, conn).await {
|
if let Some(user) = User::find_by_uuid(&member_to_confirm.user_uuid, conn).await {
|
||||||
nt.send_user_update(UpdateType::SyncOrgKeys, &user).await;
|
nt.send_user_update(UpdateType::SyncOrgKeys, &user, &headers.device.push_uuid, conn).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
save_result
|
save_result
|
||||||
@@ -1719,6 +1740,9 @@ async fn _delete_member(
|
|||||||
conn: &mut DbConn,
|
conn: &mut DbConn,
|
||||||
nt: &Notify<'_>,
|
nt: &Notify<'_>,
|
||||||
) -> EmptyResult {
|
) -> EmptyResult {
|
||||||
|
if org_id != &headers.org_id {
|
||||||
|
err!("Organization not found", "Organization id's do not match");
|
||||||
|
}
|
||||||
let Some(member_to_delete) = Membership::find_by_uuid_and_org(member_id, org_id, conn).await else {
|
let Some(member_to_delete) = Membership::find_by_uuid_and_org(member_id, org_id, conn).await else {
|
||||||
err!("User to delete isn't member of the organization")
|
err!("User to delete isn't member of the organization")
|
||||||
};
|
};
|
||||||
@@ -1747,7 +1771,7 @@ async fn _delete_member(
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
if let Some(user) = User::find_by_uuid(&member_to_delete.user_uuid, conn).await {
|
if let Some(user) = User::find_by_uuid(&member_to_delete.user_uuid, conn).await {
|
||||||
nt.send_user_update(UpdateType::SyncOrgKeys, &user).await;
|
nt.send_user_update(UpdateType::SyncOrgKeys, &user, &headers.device.push_uuid, conn).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
member_to_delete.delete(conn).await
|
member_to_delete.delete(conn).await
|
||||||
@@ -1813,16 +1837,20 @@ struct RelationsData {
|
|||||||
value: usize,
|
value: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Tools/Controllers/ImportCiphersController.cs#L62
|
||||||
#[post("/ciphers/import-organization?<query..>", data = "<data>")]
|
#[post("/ciphers/import-organization?<query..>", data = "<data>")]
|
||||||
async fn post_org_import(
|
async fn post_org_import(
|
||||||
query: OrgIdData,
|
query: OrgIdData,
|
||||||
data: Json<ImportData>,
|
data: Json<ImportData>,
|
||||||
headers: AdminHeaders,
|
headers: OrgMemberHeaders,
|
||||||
mut conn: DbConn,
|
mut conn: DbConn,
|
||||||
nt: Notify<'_>,
|
nt: Notify<'_>,
|
||||||
) -> EmptyResult {
|
) -> EmptyResult {
|
||||||
let data: ImportData = data.into_inner();
|
|
||||||
let org_id = query.organization_id;
|
let org_id = query.organization_id;
|
||||||
|
if org_id != headers.membership.org_uuid {
|
||||||
|
err!("Organization not found", "Organization id's do not match");
|
||||||
|
}
|
||||||
|
let data: ImportData = data.into_inner();
|
||||||
|
|
||||||
// Validate the import before continuing
|
// Validate the import before continuing
|
||||||
// Bitwarden does not process the import if there is one item invalid.
|
// Bitwarden does not process the import if there is one item invalid.
|
||||||
@@ -1835,8 +1863,20 @@ async fn post_org_import(
|
|||||||
let mut collections: Vec<CollectionId> = Vec::with_capacity(data.collections.len());
|
let mut collections: Vec<CollectionId> = Vec::with_capacity(data.collections.len());
|
||||||
for col in data.collections {
|
for col in data.collections {
|
||||||
let collection_uuid = if existing_collections.contains(&col.id) {
|
let collection_uuid = if existing_collections.contains(&col.id) {
|
||||||
col.id.unwrap()
|
let col_id = col.id.unwrap();
|
||||||
|
// When not an Owner or Admin, check if the member is allowed to access the collection.
|
||||||
|
if headers.membership.atype < MembershipType::Admin
|
||||||
|
&& !Collection::can_access_collection(&headers.membership, &col_id, &mut conn).await
|
||||||
|
{
|
||||||
|
err!(Compact, "The current user isn't allowed to manage this collection")
|
||||||
|
}
|
||||||
|
col_id
|
||||||
} else {
|
} else {
|
||||||
|
// We do not allow users or managers which can not manage all collections to create new collections
|
||||||
|
// If there is any collection other than an existing import collection, abort the import.
|
||||||
|
if headers.membership.atype <= MembershipType::Manager && !headers.membership.has_full_access() {
|
||||||
|
err!(Compact, "The current user isn't allowed to create new collections")
|
||||||
|
}
|
||||||
let new_collection = Collection::new(org_id.clone(), col.name, col.external_id);
|
let new_collection = Collection::new(org_id.clone(), col.name, col.external_id);
|
||||||
new_collection.save(&mut conn).await?;
|
new_collection.save(&mut conn).await?;
|
||||||
new_collection.uuid
|
new_collection.uuid
|
||||||
@@ -1859,7 +1899,17 @@ async fn post_org_import(
|
|||||||
// Always clear folder_id's via an organization import
|
// Always clear folder_id's via an organization import
|
||||||
cipher_data.folder_id = None;
|
cipher_data.folder_id = None;
|
||||||
let mut cipher = Cipher::new(cipher_data.r#type, cipher_data.name.clone());
|
let mut cipher = Cipher::new(cipher_data.r#type, cipher_data.name.clone());
|
||||||
update_cipher_from_data(&mut cipher, cipher_data, &headers, None, &mut conn, &nt, UpdateType::None).await.ok();
|
update_cipher_from_data(
|
||||||
|
&mut cipher,
|
||||||
|
cipher_data,
|
||||||
|
&headers,
|
||||||
|
Some(collections.clone()),
|
||||||
|
&mut conn,
|
||||||
|
&nt,
|
||||||
|
UpdateType::None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
ciphers.push(cipher.uuid);
|
ciphers.push(cipher.uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1890,12 +1940,6 @@ struct BulkCollectionsData {
|
|||||||
async fn post_bulk_collections(data: Json<BulkCollectionsData>, headers: Headers, mut conn: DbConn) -> EmptyResult {
|
async fn post_bulk_collections(data: Json<BulkCollectionsData>, headers: Headers, mut conn: DbConn) -> EmptyResult {
|
||||||
let data: BulkCollectionsData = data.into_inner();
|
let data: BulkCollectionsData = data.into_inner();
|
||||||
|
|
||||||
// This feature does not seem to be active on all the clients
|
|
||||||
// To prevent future issues, add a check to block a call when this is set to true
|
|
||||||
if data.remove_collections {
|
|
||||||
err!("Bulk removing of collections is not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all the collection available to the user in one query
|
// Get all the collection available to the user in one query
|
||||||
// Also filter based upon the provided collections
|
// Also filter based upon the provided collections
|
||||||
let user_collections: HashMap<CollectionId, Collection> =
|
let user_collections: HashMap<CollectionId, Collection> =
|
||||||
@@ -1924,8 +1968,16 @@ async fn post_bulk_collections(data: Json<BulkCollectionsData>, headers: Headers
|
|||||||
// Do not abort the operation just ignore it, it could be a cipher was just deleted for example
|
// Do not abort the operation just ignore it, it could be a cipher was just deleted for example
|
||||||
if let Some(cipher) = Cipher::find_by_uuid_and_org(cipher_id, &data.organization_id, &mut conn).await {
|
if let Some(cipher) = Cipher::find_by_uuid_and_org(cipher_id, &data.organization_id, &mut conn).await {
|
||||||
if cipher.is_write_accessible_to_user(&headers.user.uuid, &mut conn).await {
|
if cipher.is_write_accessible_to_user(&headers.user.uuid, &mut conn).await {
|
||||||
for collection in &data.collection_ids {
|
// When selecting a specific collection from the left filter list, and use the bulk option, you can remove an item from that collection
|
||||||
CollectionCipher::save(&cipher.uuid, collection, &mut conn).await?;
|
// In these cases the client will call this endpoint twice, once for adding the new collections and a second for deleting.
|
||||||
|
if data.remove_collections {
|
||||||
|
for collection in &data.collection_ids {
|
||||||
|
CollectionCipher::delete(&cipher.uuid, collection, &mut conn).await?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for collection in &data.collection_ids {
|
||||||
|
CollectionCipher::save(&cipher.uuid, collection, &mut conn).await?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -2402,6 +2454,9 @@ async fn _revoke_member(
|
|||||||
headers: &AdminHeaders,
|
headers: &AdminHeaders,
|
||||||
conn: &mut DbConn,
|
conn: &mut DbConn,
|
||||||
) -> EmptyResult {
|
) -> EmptyResult {
|
||||||
|
if org_id != &headers.org_id {
|
||||||
|
err!("Organization not found", "Organization id's do not match");
|
||||||
|
}
|
||||||
match Membership::find_by_uuid_and_org(member_id, org_id, conn).await {
|
match Membership::find_by_uuid_and_org(member_id, org_id, conn).await {
|
||||||
Some(mut member) if member.status > MembershipStatus::Revoked as i32 => {
|
Some(mut member) if member.status > MembershipStatus::Revoked as i32 => {
|
||||||
if member.user_uuid == headers.user.uuid {
|
if member.user_uuid == headers.user.uuid {
|
||||||
@@ -2509,6 +2564,9 @@ async fn _restore_member(
|
|||||||
headers: &AdminHeaders,
|
headers: &AdminHeaders,
|
||||||
conn: &mut DbConn,
|
conn: &mut DbConn,
|
||||||
) -> EmptyResult {
|
) -> EmptyResult {
|
||||||
|
if org_id != &headers.org_id {
|
||||||
|
err!("Organization not found", "Organization id's do not match");
|
||||||
|
}
|
||||||
match Membership::find_by_uuid_and_org(member_id, org_id, conn).await {
|
match Membership::find_by_uuid_and_org(member_id, org_id, conn).await {
|
||||||
Some(mut member) if member.status < MembershipStatus::Accepted as i32 => {
|
Some(mut member) if member.status < MembershipStatus::Accepted as i32 => {
|
||||||
if member.user_uuid == headers.user.uuid {
|
if member.user_uuid == headers.user.uuid {
|
||||||
@@ -2556,18 +2614,27 @@ async fn _restore_member(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/organizations/<org_id>/groups")]
|
async fn get_groups_data(
|
||||||
async fn get_groups(org_id: OrganizationId, headers: ManagerHeadersLoose, mut conn: DbConn) -> JsonResult {
|
details: bool,
|
||||||
|
org_id: OrganizationId,
|
||||||
|
headers: ManagerHeadersLoose,
|
||||||
|
mut conn: DbConn,
|
||||||
|
) -> JsonResult {
|
||||||
if org_id != headers.membership.org_uuid {
|
if org_id != headers.membership.org_uuid {
|
||||||
err!("Organization not found", "Organization id's do not match");
|
err!("Organization not found", "Organization id's do not match");
|
||||||
}
|
}
|
||||||
let groups: Vec<Value> = if CONFIG.org_groups_enabled() {
|
let groups: Vec<Value> = if CONFIG.org_groups_enabled() {
|
||||||
// Group::find_by_organization(&org_id, &mut conn).await.iter().map(Group::to_json).collect::<Value>()
|
|
||||||
let groups = Group::find_by_organization(&org_id, &mut conn).await;
|
let groups = Group::find_by_organization(&org_id, &mut conn).await;
|
||||||
let mut groups_json = Vec::with_capacity(groups.len());
|
let mut groups_json = Vec::with_capacity(groups.len());
|
||||||
|
|
||||||
for g in groups {
|
if details {
|
||||||
groups_json.push(g.to_json_details(&mut conn).await)
|
for g in groups {
|
||||||
|
groups_json.push(g.to_json_details(&mut conn).await)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for g in groups {
|
||||||
|
groups_json.push(g.to_json())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
groups_json
|
groups_json
|
||||||
} else {
|
} else {
|
||||||
@@ -2583,9 +2650,14 @@ async fn get_groups(org_id: OrganizationId, headers: ManagerHeadersLoose, mut co
|
|||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[get("/organizations/<org_id>/groups")]
|
||||||
|
async fn get_groups(org_id: OrganizationId, headers: ManagerHeadersLoose, conn: DbConn) -> JsonResult {
|
||||||
|
get_groups_data(false, org_id, headers, conn).await
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/organizations/<org_id>/groups/details", rank = 1)]
|
#[get("/organizations/<org_id>/groups/details", rank = 1)]
|
||||||
async fn get_groups_details(org_id: OrganizationId, headers: ManagerHeadersLoose, conn: DbConn) -> JsonResult {
|
async fn get_groups_details(org_id: OrganizationId, headers: ManagerHeadersLoose, conn: DbConn) -> JsonResult {
|
||||||
get_groups(org_id, headers, conn).await
|
get_groups_data(true, org_id, headers, conn).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@@ -2647,6 +2719,9 @@ async fn post_groups(
|
|||||||
data: Json<GroupRequest>,
|
data: Json<GroupRequest>,
|
||||||
mut conn: DbConn,
|
mut conn: DbConn,
|
||||||
) -> JsonResult {
|
) -> JsonResult {
|
||||||
|
if org_id != headers.org_id {
|
||||||
|
err!("Organization not found", "Organization id's do not match");
|
||||||
|
}
|
||||||
if !CONFIG.org_groups_enabled() {
|
if !CONFIG.org_groups_enabled() {
|
||||||
err!("Group support is disabled");
|
err!("Group support is disabled");
|
||||||
}
|
}
|
||||||
@@ -2676,6 +2751,9 @@ async fn put_group(
|
|||||||
headers: AdminHeaders,
|
headers: AdminHeaders,
|
||||||
mut conn: DbConn,
|
mut conn: DbConn,
|
||||||
) -> JsonResult {
|
) -> JsonResult {
|
||||||
|
if org_id != headers.org_id {
|
||||||
|
err!("Organization not found", "Organization id's do not match");
|
||||||
|
}
|
||||||
if !CONFIG.org_groups_enabled() {
|
if !CONFIG.org_groups_enabled() {
|
||||||
err!("Group support is disabled");
|
err!("Group support is disabled");
|
||||||
}
|
}
|
||||||
@@ -2740,7 +2818,8 @@ async fn add_update_group(
|
|||||||
"organizationId": group.organizations_uuid,
|
"organizationId": group.organizations_uuid,
|
||||||
"name": group.name,
|
"name": group.name,
|
||||||
"accessAll": group.access_all,
|
"accessAll": group.access_all,
|
||||||
"externalId": group.external_id
|
"externalId": group.external_id,
|
||||||
|
"object": "group"
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2791,6 +2870,9 @@ async fn _delete_group(
|
|||||||
headers: &AdminHeaders,
|
headers: &AdminHeaders,
|
||||||
conn: &mut DbConn,
|
conn: &mut DbConn,
|
||||||
) -> EmptyResult {
|
) -> EmptyResult {
|
||||||
|
if org_id != &headers.org_id {
|
||||||
|
err!("Organization not found", "Organization id's do not match");
|
||||||
|
}
|
||||||
if !CONFIG.org_groups_enabled() {
|
if !CONFIG.org_groups_enabled() {
|
||||||
err!("Group support is disabled");
|
err!("Group support is disabled");
|
||||||
}
|
}
|
||||||
@@ -2820,6 +2902,9 @@ async fn bulk_delete_groups(
|
|||||||
headers: AdminHeaders,
|
headers: AdminHeaders,
|
||||||
mut conn: DbConn,
|
mut conn: DbConn,
|
||||||
) -> EmptyResult {
|
) -> EmptyResult {
|
||||||
|
if org_id != headers.org_id {
|
||||||
|
err!("Organization not found", "Organization id's do not match");
|
||||||
|
}
|
||||||
if !CONFIG.org_groups_enabled() {
|
if !CONFIG.org_groups_enabled() {
|
||||||
err!("Group support is disabled");
|
err!("Group support is disabled");
|
||||||
}
|
}
|
||||||
@@ -2883,6 +2968,9 @@ async fn put_group_members(
|
|||||||
data: Json<Vec<MembershipId>>,
|
data: Json<Vec<MembershipId>>,
|
||||||
mut conn: DbConn,
|
mut conn: DbConn,
|
||||||
) -> EmptyResult {
|
) -> EmptyResult {
|
||||||
|
if org_id != headers.org_id {
|
||||||
|
err!("Organization not found", "Organization id's do not match");
|
||||||
|
}
|
||||||
if !CONFIG.org_groups_enabled() {
|
if !CONFIG.org_groups_enabled() {
|
||||||
err!("Group support is disabled");
|
err!("Group support is disabled");
|
||||||
}
|
}
|
||||||
@@ -3067,7 +3155,7 @@ async fn get_organization_public_key(
|
|||||||
headers: OrgMemberHeaders,
|
headers: OrgMemberHeaders,
|
||||||
mut conn: DbConn,
|
mut conn: DbConn,
|
||||||
) -> JsonResult {
|
) -> JsonResult {
|
||||||
if org_id != headers.org_id {
|
if org_id != headers.membership.org_uuid {
|
||||||
err!("Organization not found", "Organization id's do not match");
|
err!("Organization not found", "Organization id's do not match");
|
||||||
}
|
}
|
||||||
let Some(org) = Organization::find_by_uuid(&org_id, &mut conn).await else {
|
let Some(org) = Organization::find_by_uuid(&org_id, &mut conn).await else {
|
||||||
@@ -3081,7 +3169,7 @@ async fn get_organization_public_key(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Obsolete - Renamed to public-key (2023.8), left for backwards compatibility with older clients
|
// Obsolete - Renamed to public-key (2023.8), left for backwards compatibility with older clients
|
||||||
// https://github.com/bitwarden/server/blob/25dc0c9178e3e3584074bbef0d4be827b7c89415/src/Api/AdminConsole/Controllers/OrganizationsController.cs#L463-L468
|
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/AdminConsole/Controllers/OrganizationsController.cs#L487-L492
|
||||||
#[get("/organizations/<org_id>/keys")]
|
#[get("/organizations/<org_id>/keys")]
|
||||||
async fn get_organization_keys(org_id: OrganizationId, headers: OrgMemberHeaders, conn: DbConn) -> JsonResult {
|
async fn get_organization_keys(org_id: OrganizationId, headers: OrgMemberHeaders, conn: DbConn) -> JsonResult {
|
||||||
get_organization_public_key(org_id, headers, conn).await
|
get_organization_public_key(org_id, headers, conn).await
|
||||||
@@ -3132,7 +3220,7 @@ async fn put_reset_password(
|
|||||||
user.set_password(reset_request.new_master_password_hash.as_str(), Some(reset_request.key), true, None);
|
user.set_password(reset_request.new_master_password_hash.as_str(), Some(reset_request.key), true, None);
|
||||||
user.save(&mut conn).await?;
|
user.save(&mut conn).await?;
|
||||||
|
|
||||||
nt.send_logout(&user, None).await;
|
nt.send_logout(&user, None, &mut conn).await;
|
||||||
|
|
||||||
log_event(
|
log_event(
|
||||||
EventType::OrganizationUserAdminResetPassword as i32,
|
EventType::OrganizationUserAdminResetPassword as i32,
|
||||||
@@ -3172,16 +3260,16 @@ async fn get_reset_password_details(
|
|||||||
|
|
||||||
check_reset_password_applicable_and_permissions(&org_id, &member_id, &headers, &mut conn).await?;
|
check_reset_password_applicable_and_permissions(&org_id, &member_id, &headers, &mut conn).await?;
|
||||||
|
|
||||||
// https://github.com/bitwarden/server/blob/3b50ccb9f804efaacdc46bed5b60e5b28eddefcf/src/Api/Models/Response/Organizations/OrganizationUserResponseModel.cs#L111
|
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/AdminConsole/Models/Response/Organizations/OrganizationUserResponseModel.cs#L190
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"object": "organizationUserResetPasswordDetails",
|
"object": "organizationUserResetPasswordDetails",
|
||||||
"kdf":user.client_kdf_type,
|
"organizationUserId": member_id,
|
||||||
"kdfIterations":user.client_kdf_iter,
|
"kdf": user.client_kdf_type,
|
||||||
"kdfMemory":user.client_kdf_memory,
|
"kdfIterations": user.client_kdf_iter,
|
||||||
"kdfParallelism":user.client_kdf_parallelism,
|
"kdfMemory": user.client_kdf_memory,
|
||||||
"resetPasswordKey":member.reset_password_key,
|
"kdfParallelism": user.client_kdf_parallelism,
|
||||||
"encryptedPrivateKey":org.private_key,
|
"resetPasswordKey": member.reset_password_key,
|
||||||
|
"encryptedPrivateKey": org.private_key,
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3237,13 +3325,17 @@ async fn put_reset_password_enrollment(
|
|||||||
|
|
||||||
let reset_request = data.into_inner();
|
let reset_request = data.into_inner();
|
||||||
|
|
||||||
if reset_request.reset_password_key.is_none()
|
let reset_password_key = match reset_request.reset_password_key {
|
||||||
&& OrgPolicy::org_is_reset_password_auto_enroll(&org_id, &mut conn).await
|
None => None,
|
||||||
{
|
Some(ref key) if key.is_empty() => None,
|
||||||
|
Some(key) => Some(key),
|
||||||
|
};
|
||||||
|
|
||||||
|
if reset_password_key.is_none() && OrgPolicy::org_is_reset_password_auto_enroll(&org_id, &mut conn).await {
|
||||||
err!("Reset password can't be withdrawn due to an enterprise policy");
|
err!("Reset password can't be withdrawn due to an enterprise policy");
|
||||||
}
|
}
|
||||||
|
|
||||||
if reset_request.reset_password_key.is_some() {
|
if reset_password_key.is_some() {
|
||||||
PasswordOrOtpData {
|
PasswordOrOtpData {
|
||||||
master_password_hash: reset_request.master_password_hash,
|
master_password_hash: reset_request.master_password_hash,
|
||||||
otp: reset_request.otp,
|
otp: reset_request.otp,
|
||||||
@@ -3252,7 +3344,7 @@ async fn put_reset_password_enrollment(
|
|||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
member.reset_password_key = reset_request.reset_password_key;
|
member.reset_password_key = reset_password_key;
|
||||||
member.save(&mut conn).await?;
|
member.save(&mut conn).await?;
|
||||||
|
|
||||||
let log_id = if member.reset_password_key.is_some() {
|
let log_id = if member.reset_password_key.is_some() {
|
||||||
@@ -3269,6 +3361,9 @@ async fn put_reset_password_enrollment(
|
|||||||
// NOTE: It seems clients can't handle uppercase-first keys!!
|
// NOTE: It seems clients can't handle uppercase-first keys!!
|
||||||
// We need to convert all keys so they have the first character to be a lowercase.
|
// We need to convert all keys so they have the first character to be a lowercase.
|
||||||
// Else the export will be just an empty JSON file.
|
// Else the export will be just an empty JSON file.
|
||||||
|
// We currently only support exports by members of the Admin or Owner status.
|
||||||
|
// Vaultwarden does not yet support exporting only managed collections!
|
||||||
|
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Tools/Controllers/OrganizationExportController.cs#L52
|
||||||
#[get("/organizations/<org_id>/export")]
|
#[get("/organizations/<org_id>/export")]
|
||||||
async fn get_org_export(org_id: OrganizationId, headers: AdminHeaders, mut conn: DbConn) -> JsonResult {
|
async fn get_org_export(org_id: OrganizationId, headers: AdminHeaders, mut conn: DbConn) -> JsonResult {
|
||||||
if org_id != headers.org_id {
|
if org_id != headers.org_id {
|
||||||
@@ -3277,7 +3372,7 @@ async fn get_org_export(org_id: OrganizationId, headers: AdminHeaders, mut conn:
|
|||||||
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"collections": convert_json_key_lcase_first(_get_org_collections(&org_id, &mut conn).await),
|
"collections": convert_json_key_lcase_first(_get_org_collections(&org_id, &mut conn).await),
|
||||||
"ciphers": convert_json_key_lcase_first(_get_org_details(&org_id, &headers.host, &headers.user.uuid, &mut conn).await),
|
"ciphers": convert_json_key_lcase_first(_get_org_details(&org_id, &headers.host, &headers.user.uuid, &mut conn).await?),
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3288,6 +3383,9 @@ async fn _api_key(
|
|||||||
headers: AdminHeaders,
|
headers: AdminHeaders,
|
||||||
mut conn: DbConn,
|
mut conn: DbConn,
|
||||||
) -> JsonResult {
|
) -> JsonResult {
|
||||||
|
if org_id != &headers.org_id {
|
||||||
|
err!("Organization not found", "Organization id's do not match");
|
||||||
|
}
|
||||||
let data: PasswordOrOtpData = data.into_inner();
|
let data: PasswordOrOtpData = data.into_inner();
|
||||||
let user = headers.user;
|
let user = headers.user;
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ struct OrgImportData {
|
|||||||
#[post("/public/organization/import", data = "<data>")]
|
#[post("/public/organization/import", data = "<data>")]
|
||||||
async fn ldap_import(data: Json<OrgImportData>, token: PublicToken, mut conn: DbConn) -> EmptyResult {
|
async fn ldap_import(data: Json<OrgImportData>, token: PublicToken, mut conn: DbConn) -> EmptyResult {
|
||||||
// Most of the logic for this function can be found here
|
// Most of the logic for this function can be found here
|
||||||
// https://github.com/bitwarden/server/blob/fd892b2ff4547648a276734fb2b14a8abae2c6f5/src/Core/Services/Implementations/OrganizationService.cs#L1797
|
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs#L1203
|
||||||
|
|
||||||
let org_id = token.0;
|
let org_id = token.0;
|
||||||
let data = data.into_inner();
|
let data = data.into_inner();
|
||||||
|
|||||||
+50
-34
@@ -1,7 +1,9 @@
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use chrono::{DateTime, TimeDelta, Utc};
|
use chrono::{DateTime, TimeDelta, Utc};
|
||||||
use num_traits::ToPrimitive;
|
use num_traits::ToPrimitive;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
use rocket::form::Form;
|
use rocket::form::Form;
|
||||||
use rocket::fs::NamedFile;
|
use rocket::fs::NamedFile;
|
||||||
use rocket::fs::TempFile;
|
use rocket::fs::TempFile;
|
||||||
@@ -11,12 +13,28 @@ use serde_json::Value;
|
|||||||
use crate::{
|
use crate::{
|
||||||
api::{ApiResult, EmptyResult, JsonResult, Notify, UpdateType},
|
api::{ApiResult, EmptyResult, JsonResult, Notify, UpdateType},
|
||||||
auth::{ClientIp, Headers, Host},
|
auth::{ClientIp, Headers, Host},
|
||||||
|
config::PathType,
|
||||||
db::{models::*, DbConn, DbPool},
|
db::{models::*, DbConn, DbPool},
|
||||||
util::NumberOrString,
|
util::{save_temp_file, NumberOrString},
|
||||||
CONFIG,
|
CONFIG,
|
||||||
};
|
};
|
||||||
|
|
||||||
const SEND_INACCESSIBLE_MSG: &str = "Send does not exist or is no longer available";
|
const SEND_INACCESSIBLE_MSG: &str = "Send does not exist or is no longer available";
|
||||||
|
static ANON_PUSH_DEVICE: Lazy<Device> = Lazy::new(|| {
|
||||||
|
let dt = crate::util::parse_date("1970-01-01T00:00:00.000000Z");
|
||||||
|
Device {
|
||||||
|
uuid: String::from("00000000-0000-0000-0000-000000000000").into(),
|
||||||
|
created_at: dt,
|
||||||
|
updated_at: dt,
|
||||||
|
user_uuid: String::from("00000000-0000-0000-0000-000000000000").into(),
|
||||||
|
name: String::new(),
|
||||||
|
atype: 14, // 14 == Unknown Browser
|
||||||
|
push_uuid: Some(String::from("00000000-0000-0000-0000-000000000000").into()),
|
||||||
|
push_token: None,
|
||||||
|
refresh_token: String::new(),
|
||||||
|
twofactor_remember: None,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// The max file size allowed by Bitwarden clients and add an extra 5% to avoid issues
|
// The max file size allowed by Bitwarden clients and add an extra 5% to avoid issues
|
||||||
const SIZE_525_MB: i64 = 550_502_400;
|
const SIZE_525_MB: i64 = 550_502_400;
|
||||||
@@ -182,7 +200,7 @@ async fn post_send(data: Json<SendData>, headers: Headers, mut conn: DbConn, nt:
|
|||||||
UpdateType::SyncSendCreate,
|
UpdateType::SyncSendCreate,
|
||||||
&send,
|
&send,
|
||||||
&send.update_users_revision(&mut conn).await,
|
&send.update_users_revision(&mut conn).await,
|
||||||
&headers.device.uuid,
|
&headers.device,
|
||||||
&mut conn,
|
&mut conn,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
@@ -204,13 +222,15 @@ struct UploadDataV2<'f> {
|
|||||||
// @deprecated Mar 25 2021: This method has been deprecated in favor of direct uploads (v2).
|
// @deprecated Mar 25 2021: This method has been deprecated in favor of direct uploads (v2).
|
||||||
// This method still exists to support older clients, probably need to remove it sometime.
|
// This method still exists to support older clients, probably need to remove it sometime.
|
||||||
// Upstream: https://github.com/bitwarden/server/blob/d0c793c95181dfb1b447eb450f85ba0bfd7ef643/src/Api/Controllers/SendsController.cs#L164-L167
|
// Upstream: https://github.com/bitwarden/server/blob/d0c793c95181dfb1b447eb450f85ba0bfd7ef643/src/Api/Controllers/SendsController.cs#L164-L167
|
||||||
|
// 2025: This endpoint doesn't seem to exists anymore in the latest version
|
||||||
|
// See: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Tools/Controllers/SendsController.cs
|
||||||
#[post("/sends/file", format = "multipart/form-data", data = "<data>")]
|
#[post("/sends/file", format = "multipart/form-data", data = "<data>")]
|
||||||
async fn post_send_file(data: Form<UploadData<'_>>, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult {
|
async fn post_send_file(data: Form<UploadData<'_>>, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult {
|
||||||
enforce_disable_send_policy(&headers, &mut conn).await?;
|
enforce_disable_send_policy(&headers, &mut conn).await?;
|
||||||
|
|
||||||
let UploadData {
|
let UploadData {
|
||||||
model,
|
model,
|
||||||
mut data,
|
data,
|
||||||
} = data.into_inner();
|
} = data.into_inner();
|
||||||
let model = model.into_inner();
|
let model = model.into_inner();
|
||||||
|
|
||||||
@@ -250,13 +270,8 @@ async fn post_send_file(data: Form<UploadData<'_>>, headers: Headers, mut conn:
|
|||||||
}
|
}
|
||||||
|
|
||||||
let file_id = crate::crypto::generate_send_file_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?;
|
|
||||||
|
|
||||||
if let Err(_err) = data.persist_to(&file_path).await {
|
save_temp_file(PathType::Sends, &format!("{}/{file_id}", send.uuid), data, true).await?;
|
||||||
data.move_copy_to(file_path).await?
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut data_value: Value = serde_json::from_str(&send.data)?;
|
let mut data_value: Value = serde_json::from_str(&send.data)?;
|
||||||
if let Some(o) = data_value.as_object_mut() {
|
if let Some(o) = data_value.as_object_mut() {
|
||||||
@@ -272,7 +287,7 @@ async fn post_send_file(data: Form<UploadData<'_>>, headers: Headers, mut conn:
|
|||||||
UpdateType::SyncSendCreate,
|
UpdateType::SyncSendCreate,
|
||||||
&send,
|
&send,
|
||||||
&send.update_users_revision(&mut conn).await,
|
&send.update_users_revision(&mut conn).await,
|
||||||
&headers.device.uuid,
|
&headers.device,
|
||||||
&mut conn,
|
&mut conn,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
@@ -280,7 +295,7 @@ async fn post_send_file(data: Form<UploadData<'_>>, headers: Headers, mut conn:
|
|||||||
Ok(Json(send.to_json()))
|
Ok(Json(send.to_json()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upstream: https://github.com/bitwarden/server/blob/d0c793c95181dfb1b447eb450f85ba0bfd7ef643/src/Api/Controllers/SendsController.cs#L190
|
// Upstream: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Tools/Controllers/SendsController.cs#L165
|
||||||
#[post("/sends/file/v2", data = "<data>")]
|
#[post("/sends/file/v2", data = "<data>")]
|
||||||
async fn post_send_file_v2(data: Json<SendData>, headers: Headers, mut conn: DbConn) -> JsonResult {
|
async fn post_send_file_v2(data: Json<SendData>, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||||
enforce_disable_send_policy(&headers, &mut conn).await?;
|
enforce_disable_send_policy(&headers, &mut conn).await?;
|
||||||
@@ -351,7 +366,7 @@ pub struct SendFileData {
|
|||||||
fileName: String,
|
fileName: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://github.com/bitwarden/server/blob/66f95d1c443490b653e5a15d32977e2f5a3f9e32/src/Api/Tools/Controllers/SendsController.cs#L250
|
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Tools/Controllers/SendsController.cs#L195
|
||||||
#[post("/sends/<send_id>/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(
|
async fn post_send_file_v2_data(
|
||||||
send_id: SendId,
|
send_id: SendId,
|
||||||
@@ -363,7 +378,7 @@ async fn post_send_file_v2_data(
|
|||||||
) -> EmptyResult {
|
) -> EmptyResult {
|
||||||
enforce_disable_send_policy(&headers, &mut conn).await?;
|
enforce_disable_send_policy(&headers, &mut conn).await?;
|
||||||
|
|
||||||
let mut data = data.into_inner();
|
let data = data.into_inner();
|
||||||
|
|
||||||
let Some(send) = Send::find_by_uuid_and_user(&send_id, &headers.user.uuid, &mut conn).await else {
|
let Some(send) = Send::find_by_uuid_and_user(&send_id, &headers.user.uuid, &mut conn).await else {
|
||||||
err!("Send not found. Unable to save the file.", "Invalid send uuid or does not belong to user.")
|
err!("Send not found. Unable to save the file.", "Invalid send uuid or does not belong to user.")
|
||||||
@@ -406,25 +421,15 @@ 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));
|
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_id);
|
let file_path = format!("{send_id}/{file_id}");
|
||||||
let file_path = folder_path.join(file_id);
|
|
||||||
|
|
||||||
// Check if the file already exists, if that is the case do not overwrite it
|
save_temp_file(PathType::Sends, &file_path, data.data, false).await?;
|
||||||
if tokio::fs::metadata(&file_path).await.is_ok() {
|
|
||||||
err!("Send file has already been uploaded.", format!("File {file_path:?} already exists"))
|
|
||||||
}
|
|
||||||
|
|
||||||
tokio::fs::create_dir_all(&folder_path).await?;
|
|
||||||
|
|
||||||
if let Err(_err) = data.data.persist_to(&file_path).await {
|
|
||||||
data.data.move_copy_to(file_path).await?
|
|
||||||
}
|
|
||||||
|
|
||||||
nt.send_send_update(
|
nt.send_send_update(
|
||||||
UpdateType::SyncSendCreate,
|
UpdateType::SyncSendCreate,
|
||||||
&send,
|
&send,
|
||||||
&send.update_users_revision(&mut conn).await,
|
&send.update_users_revision(&mut conn).await,
|
||||||
&headers.device.uuid,
|
&headers.device,
|
||||||
&mut conn,
|
&mut conn,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
@@ -489,7 +494,7 @@ async fn post_access(
|
|||||||
UpdateType::SyncSendUpdate,
|
UpdateType::SyncSendUpdate,
|
||||||
&send,
|
&send,
|
||||||
&send.update_users_revision(&mut conn).await,
|
&send.update_users_revision(&mut conn).await,
|
||||||
&String::from("00000000-0000-0000-0000-000000000000").into(),
|
&ANON_PUSH_DEVICE,
|
||||||
&mut conn,
|
&mut conn,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
@@ -546,20 +551,31 @@ async fn post_access_file(
|
|||||||
UpdateType::SyncSendUpdate,
|
UpdateType::SyncSendUpdate,
|
||||||
&send,
|
&send,
|
||||||
&send.update_users_revision(&mut conn).await,
|
&send.update_users_revision(&mut conn).await,
|
||||||
&String::from("00000000-0000-0000-0000-000000000000").into(),
|
&ANON_PUSH_DEVICE,
|
||||||
&mut conn,
|
&mut conn,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let token_claims = crate::auth::generate_send_claims(&send_id, &file_id);
|
|
||||||
let token = crate::auth::encode_jwt(&token_claims);
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"object": "send-fileDownload",
|
"object": "send-fileDownload",
|
||||||
"id": file_id,
|
"id": file_id,
|
||||||
"url": format!("{}/api/sends/{send_id}/{file_id}?t={token}", &host.host)
|
"url": download_url(&host, &send_id, &file_id).await?,
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn download_url(host: &Host, send_id: &SendId, file_id: &SendFileId) -> Result<String, crate::Error> {
|
||||||
|
let operator = CONFIG.opendal_operator_for_path_type(PathType::Sends)?;
|
||||||
|
|
||||||
|
if operator.info().scheme() == opendal::Scheme::Fs {
|
||||||
|
let token_claims = crate::auth::generate_send_claims(send_id, file_id);
|
||||||
|
let token = crate::auth::encode_jwt(&token_claims);
|
||||||
|
|
||||||
|
Ok(format!("{}/api/sends/{send_id}/{file_id}?t={token}", &host.host))
|
||||||
|
} else {
|
||||||
|
Ok(operator.presign_read(&format!("{send_id}/{file_id}"), Duration::from_secs(5 * 60)).await?.uri().to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/sends/<send_id>/<file_id>?<t>")]
|
#[get("/sends/<send_id>/<file_id>?<t>")]
|
||||||
async fn download_send(send_id: SendId, file_id: SendFileId, 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 let Ok(claims) = crate::auth::decode_send(t) {
|
||||||
@@ -645,7 +661,7 @@ pub async fn update_send_from_data(
|
|||||||
|
|
||||||
send.save(conn).await?;
|
send.save(conn).await?;
|
||||||
if ut != UpdateType::None {
|
if ut != UpdateType::None {
|
||||||
nt.send_send_update(ut, send, &send.update_users_revision(conn).await, &headers.device.uuid, conn).await;
|
nt.send_send_update(ut, send, &send.update_users_revision(conn).await, &headers.device, conn).await;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -661,7 +677,7 @@ async fn delete_send(send_id: SendId, headers: Headers, mut conn: DbConn, nt: No
|
|||||||
UpdateType::SyncSendDelete,
|
UpdateType::SyncSendDelete,
|
||||||
&send,
|
&send,
|
||||||
&send.update_users_revision(&mut conn).await,
|
&send.update_users_revision(&mut conn).await,
|
||||||
&headers.device.uuid,
|
&headers.device,
|
||||||
&mut conn,
|
&mut conn,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
@@ -683,7 +699,7 @@ async fn put_remove_password(send_id: SendId, headers: Headers, mut conn: DbConn
|
|||||||
UpdateType::SyncSendUpdate,
|
UpdateType::SyncSendUpdate,
|
||||||
&send,
|
&send,
|
||||||
&send.update_users_revision(&mut conn).await,
|
&send.update_users_revision(&mut conn).await,
|
||||||
&headers.device.uuid,
|
&headers.device,
|
||||||
&mut conn,
|
&mut conn,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ async fn generate_authenticator(data: Json<PasswordOrOtpData>, headers: Headers,
|
|||||||
_ => (false, crypto::encode_random_bytes::<20>(BASE32)),
|
_ => (false, crypto::encode_random_bytes::<20>(BASE32)),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Upstream seems to also return `userVerificationToken`, but doesn't seem to be used at all.
|
||||||
|
// It should help prevent TOTP disclosure if someone keeps their vault unlocked.
|
||||||
|
// Since it doesn't seem to be used, and also does not cause any issues, lets leave it out of the response.
|
||||||
|
// See: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Auth/Controllers/TwoFactorController.cs#L94
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"enabled": enabled,
|
"enabled": enabled,
|
||||||
"key": key,
|
"key": key,
|
||||||
|
|||||||
@@ -118,6 +118,9 @@ async fn get_duo(data: Json<PasswordOrOtpData>, headers: Headers, mut conn: DbCo
|
|||||||
} else {
|
} else {
|
||||||
json!({
|
json!({
|
||||||
"enabled": enabled,
|
"enabled": enabled,
|
||||||
|
"host": null,
|
||||||
|
"clientSecret": null,
|
||||||
|
"clientId": null,
|
||||||
"object": "twoFactorDuo"
|
"object": "twoFactorDuo"
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
@@ -258,7 +261,7 @@ pub(crate) async fn get_duo_keys_email(email: &str, conn: &mut DbConn) -> ApiRes
|
|||||||
}
|
}
|
||||||
.map_res("Can't fetch Duo Keys")?;
|
.map_res("Can't fetch Duo Keys")?;
|
||||||
|
|
||||||
Ok((data.ik, data.sk, CONFIG.get_duo_akey(), data.host))
|
Ok((data.ik, data.sk, CONFIG.get_duo_akey().await, data.host))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn generate_duo_signature(email: &str, conn: &mut DbConn) -> ApiResult<(String, String)> {
|
pub async fn generate_duo_signature(email: &str, conn: &mut DbConn) -> ApiResult<(String, String)> {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ use url::Url;
|
|||||||
|
|
||||||
// The location on this service that Duo should redirect users to. For us, this is a bridge
|
// The location on this service that Duo should redirect users to. For us, this is a bridge
|
||||||
// built in to the Bitwarden clients.
|
// built in to the Bitwarden clients.
|
||||||
// See: https://github.com/bitwarden/clients/blob/main/apps/web/src/connectors/duo-redirect.ts
|
// See: https://github.com/bitwarden/clients/blob/5fb46df3415aefced0b52f2db86c873962255448/apps/web/src/connectors/duo-redirect.ts
|
||||||
const DUO_REDIRECT_LOCATION: &str = "duo-redirect-connector.html";
|
const DUO_REDIRECT_LOCATION: &str = "duo-redirect-connector.html";
|
||||||
|
|
||||||
// Number of seconds that a JWT we generate for Duo should be valid for.
|
// Number of seconds that a JWT we generate for Duo should be valid for.
|
||||||
|
|||||||
@@ -145,15 +145,14 @@ async fn activate_yubikey(data: Json<EnableYubikeyData>, headers: Headers, mut c
|
|||||||
|
|
||||||
// Ensure they are valid OTPs
|
// Ensure they are valid OTPs
|
||||||
for yubikey in &yubikeys {
|
for yubikey in &yubikeys {
|
||||||
if yubikey.len() == 12 {
|
if yubikey.is_empty() || yubikey.len() == 12 {
|
||||||
// YubiKey ID
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
verify_yubikey_otp(yubikey.to_owned()).await.map_res("Invalid Yubikey OTP provided")?;
|
verify_yubikey_otp(yubikey.to_owned()).await.map_res("Invalid Yubikey OTP provided")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let yubikey_ids: Vec<String> = yubikeys.into_iter().map(|x| (x[..12]).to_owned()).collect();
|
let yubikey_ids: Vec<String> = yubikeys.into_iter().filter_map(|x| x.get(..12).map(str::to_owned)).collect();
|
||||||
|
|
||||||
let yubikey_metadata = YubikeyMetadata {
|
let yubikey_metadata = YubikeyMetadata {
|
||||||
keys: yubikey_ids,
|
keys: yubikey_ids,
|
||||||
|
|||||||
+80
-30
@@ -14,14 +14,12 @@ use reqwest::{
|
|||||||
Client, Response,
|
Client, Response,
|
||||||
};
|
};
|
||||||
use rocket::{http::ContentType, response::Redirect, Route};
|
use rocket::{http::ContentType, response::Redirect, Route};
|
||||||
use tokio::{
|
use svg_hush::{data_url_filter, Filter};
|
||||||
fs::{create_dir_all, remove_file, symlink_metadata, File},
|
|
||||||
io::{AsyncReadExt, AsyncWriteExt},
|
|
||||||
};
|
|
||||||
|
|
||||||
use html5gum::{Emitter, HtmlString, Readable, StringReader, Tokenizer};
|
use html5gum::{Emitter, HtmlString, Readable, StringReader, Tokenizer};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
config::PathType,
|
||||||
error::Error,
|
error::Error,
|
||||||
http_client::{get_reqwest_client_builder, should_block_address, CustomHttpClientError},
|
http_client::{get_reqwest_client_builder, should_block_address, CustomHttpClientError},
|
||||||
util::Cached,
|
util::Cached,
|
||||||
@@ -38,11 +36,29 @@ pub fn routes() -> Vec<Route> {
|
|||||||
static CLIENT: Lazy<Client> = Lazy::new(|| {
|
static CLIENT: Lazy<Client> = Lazy::new(|| {
|
||||||
// Generate the default headers
|
// Generate the default headers
|
||||||
let mut default_headers = HeaderMap::new();
|
let mut default_headers = HeaderMap::new();
|
||||||
default_headers.insert(header::USER_AGENT, HeaderValue::from_static("Links (2.22; Linux X86_64; GNU C; text)"));
|
default_headers.insert(
|
||||||
default_headers.insert(header::ACCEPT, HeaderValue::from_static("text/html, text/*;q=0.5, image/*, */*;q=0.1"));
|
header::USER_AGENT,
|
||||||
default_headers.insert(header::ACCEPT_LANGUAGE, HeaderValue::from_static("en,*;q=0.1"));
|
HeaderValue::from_static(
|
||||||
|
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
default_headers.insert(header::ACCEPT, HeaderValue::from_static("text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"));
|
||||||
|
default_headers.insert(header::ACCEPT_LANGUAGE, HeaderValue::from_static("en-US,en;q=0.9"));
|
||||||
default_headers.insert(header::CACHE_CONTROL, HeaderValue::from_static("no-cache"));
|
default_headers.insert(header::CACHE_CONTROL, HeaderValue::from_static("no-cache"));
|
||||||
default_headers.insert(header::PRAGMA, HeaderValue::from_static("no-cache"));
|
default_headers.insert(header::PRAGMA, HeaderValue::from_static("no-cache"));
|
||||||
|
default_headers.insert(header::UPGRADE_INSECURE_REQUESTS, HeaderValue::from_static("1"));
|
||||||
|
|
||||||
|
default_headers.insert("Sec-Ch-Ua-Mobile", HeaderValue::from_static("?0"));
|
||||||
|
default_headers.insert("Sec-Ch-Ua-Platform", HeaderValue::from_static("Linux"));
|
||||||
|
default_headers.insert(
|
||||||
|
"Sec-Ch-Ua",
|
||||||
|
HeaderValue::from_static("\"Not)A;Brand\";v=\"8\", \"Chromium\";v=\"138\", \"Google Chrome\";v=\"138\""),
|
||||||
|
);
|
||||||
|
|
||||||
|
default_headers.insert("Sec-Fetch-Site", HeaderValue::from_static("none"));
|
||||||
|
default_headers.insert("Sec-Fetch-Mode", HeaderValue::from_static("navigate"));
|
||||||
|
default_headers.insert("Sec-Fetch-User", HeaderValue::from_static("?1"));
|
||||||
|
default_headers.insert("Sec-Fetch-Dest", HeaderValue::from_static("document"));
|
||||||
|
|
||||||
// Generate the cookie store
|
// Generate the cookie store
|
||||||
let cookie_store = Arc::new(Jar::default());
|
let cookie_store = Arc::new(Jar::default());
|
||||||
@@ -56,6 +72,7 @@ static CLIENT: Lazy<Client> = Lazy::new(|| {
|
|||||||
.pool_max_idle_per_host(5) // Configure the Hyper Pool to only have max 5 idle connections
|
.pool_max_idle_per_host(5) // Configure the Hyper Pool to only have max 5 idle connections
|
||||||
.pool_idle_timeout(pool_idle_timeout) // Configure the Hyper Pool to timeout after 10 seconds
|
.pool_idle_timeout(pool_idle_timeout) // Configure the Hyper Pool to timeout after 10 seconds
|
||||||
.default_headers(default_headers.clone())
|
.default_headers(default_headers.clone())
|
||||||
|
.http1_title_case_headers()
|
||||||
.build()
|
.build()
|
||||||
.expect("Failed to build client")
|
.expect("Failed to build client")
|
||||||
});
|
});
|
||||||
@@ -158,7 +175,7 @@ fn is_valid_domain(domain: &str) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn get_icon(domain: &str) -> Option<(Vec<u8>, String)> {
|
async fn get_icon(domain: &str) -> Option<(Vec<u8>, String)> {
|
||||||
let path = format!("{}/{domain}.png", CONFIG.icon_cache_folder());
|
let path = format!("{domain}.png");
|
||||||
|
|
||||||
// Check for expiration of negatively cached copy
|
// Check for expiration of negatively cached copy
|
||||||
if icon_is_negcached(&path).await {
|
if icon_is_negcached(&path).await {
|
||||||
@@ -177,7 +194,7 @@ async fn get_icon(domain: &str) -> Option<(Vec<u8>, String)> {
|
|||||||
// Get the icon, or None in case of error
|
// Get the icon, or None in case of error
|
||||||
match download_icon(domain).await {
|
match download_icon(domain).await {
|
||||||
Ok((icon, icon_type)) => {
|
Ok((icon, icon_type)) => {
|
||||||
save_icon(&path, &icon).await;
|
save_icon(&path, icon.to_vec()).await;
|
||||||
Some((icon.to_vec(), icon_type.unwrap_or("x-icon").to_string()))
|
Some((icon.to_vec(), icon_type.unwrap_or("x-icon").to_string()))
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -190,7 +207,7 @@ async fn get_icon(domain: &str) -> Option<(Vec<u8>, String)> {
|
|||||||
|
|
||||||
warn!("Unable to download icon: {e:?}");
|
warn!("Unable to download icon: {e:?}");
|
||||||
let miss_indicator = path + ".miss";
|
let miss_indicator = path + ".miss";
|
||||||
save_icon(&miss_indicator, &[]).await;
|
save_icon(&miss_indicator, vec![]).await;
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -203,11 +220,9 @@ async fn get_cached_icon(path: &str) -> Option<Vec<u8>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Try to read the cached icon, and return it if it exists
|
// Try to read the cached icon, and return it if it exists
|
||||||
if let Ok(mut f) = File::open(path).await {
|
if let Ok(operator) = CONFIG.opendal_operator_for_path_type(PathType::IconCache) {
|
||||||
let mut buffer = Vec::new();
|
if let Ok(buf) = operator.read(path).await {
|
||||||
|
return Some(buf.to_vec());
|
||||||
if f.read_to_end(&mut buffer).await.is_ok() {
|
|
||||||
return Some(buffer);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,9 +230,11 @@ async fn get_cached_icon(path: &str) -> Option<Vec<u8>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn file_is_expired(path: &str, ttl: u64) -> Result<bool, Error> {
|
async fn file_is_expired(path: &str, ttl: u64) -> Result<bool, Error> {
|
||||||
let meta = symlink_metadata(path).await?;
|
let operator = CONFIG.opendal_operator_for_path_type(PathType::IconCache)?;
|
||||||
let modified = meta.modified()?;
|
let meta = operator.stat(path).await?;
|
||||||
let age = SystemTime::now().duration_since(modified)?;
|
let modified =
|
||||||
|
meta.last_modified().ok_or_else(|| std::io::Error::other(format!("No last modified time for `{path}`")))?;
|
||||||
|
let age = SystemTime::now().duration_since(modified.into())?;
|
||||||
|
|
||||||
Ok(ttl > 0 && ttl <= age.as_secs())
|
Ok(ttl > 0 && ttl <= age.as_secs())
|
||||||
}
|
}
|
||||||
@@ -229,8 +246,13 @@ async fn icon_is_negcached(path: &str) -> bool {
|
|||||||
match expired {
|
match expired {
|
||||||
// No longer negatively cached, drop the marker
|
// No longer negatively cached, drop the marker
|
||||||
Ok(true) => {
|
Ok(true) => {
|
||||||
if let Err(e) = remove_file(&miss_indicator).await {
|
match CONFIG.opendal_operator_for_path_type(PathType::IconCache) {
|
||||||
error!("Could not remove negative cache indicator for icon {path:?}: {e:?}");
|
Ok(operator) => {
|
||||||
|
if let Err(e) = operator.delete(&miss_indicator).await {
|
||||||
|
error!("Could not remove negative cache indicator for icon {path:?}: {e:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => error!("Could not remove negative cache indicator for icon {path:?}: {e:?}"),
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
@@ -316,7 +338,7 @@ struct IconUrlResult {
|
|||||||
|
|
||||||
/// Returns a IconUrlResult which holds a Vector IconList and a string which holds the referer.
|
/// Returns a IconUrlResult which holds a Vector IconList and a string which holds the referer.
|
||||||
/// There will always two items within the iconlist which holds http(s)://domain.tld/favicon.ico.
|
/// There will always two items within the iconlist which holds http(s)://domain.tld/favicon.ico.
|
||||||
/// This does not mean that that location does exists, but it is the default location browser use.
|
/// This does not mean that location exists, but (it) is the default location the browser uses.
|
||||||
///
|
///
|
||||||
/// # Argument
|
/// # Argument
|
||||||
/// * `domain` - A string which holds the domain with extension.
|
/// * `domain` - A string which holds the domain with extension.
|
||||||
@@ -559,26 +581,46 @@ async fn download_icon(domain: &str) -> Result<(Bytes, Option<&str>), Error> {
|
|||||||
|
|
||||||
if buffer.is_empty() {
|
if buffer.is_empty() {
|
||||||
err_silent!("Empty response or unable find a valid icon", domain);
|
err_silent!("Empty response or unable find a valid icon", domain);
|
||||||
|
} else if icon_type == Some("svg+xml") {
|
||||||
|
let mut svg_filter = Filter::new();
|
||||||
|
svg_filter.set_data_url_filter(data_url_filter::allow_standard_images);
|
||||||
|
let mut sanitized_svg = Vec::new();
|
||||||
|
if svg_filter.filter(&*buffer, &mut sanitized_svg).is_err() {
|
||||||
|
icon_type = None;
|
||||||
|
buffer.clear();
|
||||||
|
} else {
|
||||||
|
buffer = sanitized_svg.into();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok((buffer, icon_type))
|
Ok((buffer, icon_type))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn save_icon(path: &str, icon: &[u8]) {
|
async fn save_icon(path: &str, icon: Vec<u8>) {
|
||||||
match File::create(path).await {
|
let operator = match CONFIG.opendal_operator_for_path_type(PathType::IconCache) {
|
||||||
Ok(mut f) => {
|
Ok(operator) => operator,
|
||||||
f.write_all(icon).await.expect("Error writing icon file");
|
|
||||||
}
|
|
||||||
Err(ref e) if e.kind() == std::io::ErrorKind::NotFound => {
|
|
||||||
create_dir_all(&CONFIG.icon_cache_folder()).await.expect("Error creating icon cache folder");
|
|
||||||
}
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Unable to save icon: {e:?}");
|
warn!("Failed to get OpenDAL operator while saving icon: {e}");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = operator.write(path, icon).await {
|
||||||
|
warn!("Unable to save icon: {e:?}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_icon_type(bytes: &[u8]) -> Option<&'static str> {
|
fn get_icon_type(bytes: &[u8]) -> Option<&'static str> {
|
||||||
|
fn check_svg_after_xml_declaration(bytes: &[u8]) -> Option<&'static str> {
|
||||||
|
// Look for SVG tag within the first 1KB
|
||||||
|
if let Ok(content) = std::str::from_utf8(&bytes[..bytes.len().min(1024)]) {
|
||||||
|
if content.contains("<svg") || content.contains("<SVG") {
|
||||||
|
return Some("svg+xml");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
match bytes {
|
match bytes {
|
||||||
[137, 80, 78, 71, ..] => Some("png"),
|
[137, 80, 78, 71, ..] => Some("png"),
|
||||||
[0, 0, 1, 0, ..] => Some("x-icon"),
|
[0, 0, 1, 0, ..] => Some("x-icon"),
|
||||||
@@ -586,6 +628,8 @@ fn get_icon_type(bytes: &[u8]) -> Option<&'static str> {
|
|||||||
[255, 216, 255, ..] => Some("jpeg"),
|
[255, 216, 255, ..] => Some("jpeg"),
|
||||||
[71, 73, 70, 56, ..] => Some("gif"),
|
[71, 73, 70, 56, ..] => Some("gif"),
|
||||||
[66, 77, ..] => Some("bmp"),
|
[66, 77, ..] => Some("bmp"),
|
||||||
|
[60, 115, 118, 103, ..] => Some("svg+xml"), // Normal svg
|
||||||
|
[60, 63, 120, 109, 108, ..] => check_svg_after_xml_declaration(bytes), // An svg starting with <?xml
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -597,6 +641,12 @@ async fn stream_to_bytes_limit(res: Response, max_size: usize) -> Result<Bytes,
|
|||||||
let mut buf = BytesMut::new();
|
let mut buf = BytesMut::new();
|
||||||
let mut size = 0;
|
let mut size = 0;
|
||||||
while let Some(chunk) = stream.next().await {
|
while let Some(chunk) = stream.next().await {
|
||||||
|
// It is possible that there might occure UnexpectedEof errors or others
|
||||||
|
// This is most of the time no issue, and if there is no chunked data anymore or at all parsing the HTML will not happen anyway.
|
||||||
|
// Therfore if chunk is an err, just break and continue with the data be have received.
|
||||||
|
if chunk.is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
let chunk = &chunk?;
|
let chunk = &chunk?;
|
||||||
size += chunk.len();
|
size += chunk.len();
|
||||||
buf.extend(chunk);
|
buf.extend(chunk);
|
||||||
|
|||||||
+36
-53
@@ -14,10 +14,11 @@ use crate::{
|
|||||||
log_user_event,
|
log_user_event,
|
||||||
two_factor::{authenticator, duo, duo_oidc, email, enforce_2fa_policy, webauthn, yubikey},
|
two_factor::{authenticator, duo, duo_oidc, email, enforce_2fa_policy, webauthn, yubikey},
|
||||||
},
|
},
|
||||||
|
master_password_policy,
|
||||||
push::register_push_device,
|
push::register_push_device,
|
||||||
ApiResult, EmptyResult, JsonResult,
|
ApiResult, EmptyResult, JsonResult,
|
||||||
},
|
},
|
||||||
auth::{generate_organization_api_key_login_claims, ClientHeaders, ClientIp},
|
auth::{generate_organization_api_key_login_claims, ClientHeaders, ClientIp, ClientVersion},
|
||||||
db::{models::*, DbConn},
|
db::{models::*, DbConn},
|
||||||
error::MapResult,
|
error::MapResult,
|
||||||
mail, util, CONFIG,
|
mail, util, CONFIG,
|
||||||
@@ -28,7 +29,12 @@ pub fn routes() -> Vec<Route> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/connect/token", data = "<data>")]
|
#[post("/connect/token", data = "<data>")]
|
||||||
async fn login(data: Form<ConnectData>, client_header: ClientHeaders, mut conn: DbConn) -> JsonResult {
|
async fn login(
|
||||||
|
data: Form<ConnectData>,
|
||||||
|
client_header: ClientHeaders,
|
||||||
|
client_version: Option<ClientVersion>,
|
||||||
|
mut conn: DbConn,
|
||||||
|
) -> JsonResult {
|
||||||
let data: ConnectData = data.into_inner();
|
let data: ConnectData = data.into_inner();
|
||||||
|
|
||||||
let mut user_id: Option<UserId> = None;
|
let mut user_id: Option<UserId> = None;
|
||||||
@@ -48,7 +54,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_name, "device_name cannot be blank")?;
|
||||||
_check_is_some(&data.device_type, "device_type cannot be blank")?;
|
_check_is_some(&data.device_type, "device_type cannot be blank")?;
|
||||||
|
|
||||||
_password_login(data, &mut user_id, &mut conn, &client_header.ip).await
|
_password_login(data, &mut user_id, &mut conn, &client_header.ip, &client_version).await
|
||||||
}
|
}
|
||||||
"client_credentials" => {
|
"client_credentials" => {
|
||||||
_check_is_some(&data.client_id, "client_id cannot be blank")?;
|
_check_is_some(&data.client_id, "client_id cannot be blank")?;
|
||||||
@@ -112,7 +118,7 @@ async fn _refresh_login(data: ConnectData, conn: &mut DbConn) -> JsonResult {
|
|||||||
// See: https://github.com/dani-garcia/vaultwarden/issues/4156
|
// See: https://github.com/dani-garcia/vaultwarden/issues/4156
|
||||||
// ---
|
// ---
|
||||||
// let members = Membership::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);
|
let (access_token, expires_in) = device.refresh_tokens(&user, scope_vec, data.client_id);
|
||||||
device.save(conn).await?;
|
device.save(conn).await?;
|
||||||
|
|
||||||
let result = json!({
|
let result = json!({
|
||||||
@@ -127,23 +133,12 @@ async fn _refresh_login(data: ConnectData, conn: &mut DbConn) -> JsonResult {
|
|||||||
Ok(Json(result))
|
Ok(Json(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Deserialize, Serialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
struct MasterPasswordPolicy {
|
|
||||||
min_complexity: u8,
|
|
||||||
min_length: u32,
|
|
||||||
require_lower: bool,
|
|
||||||
require_upper: bool,
|
|
||||||
require_numbers: bool,
|
|
||||||
require_special: bool,
|
|
||||||
enforce_on_login: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn _password_login(
|
async fn _password_login(
|
||||||
data: ConnectData,
|
data: ConnectData,
|
||||||
user_id: &mut Option<UserId>,
|
user_id: &mut Option<UserId>,
|
||||||
conn: &mut DbConn,
|
conn: &mut DbConn,
|
||||||
ip: &ClientIp,
|
ip: &ClientIp,
|
||||||
|
client_version: &Option<ClientVersion>,
|
||||||
) -> JsonResult {
|
) -> JsonResult {
|
||||||
// Validate scope
|
// Validate scope
|
||||||
let scope = data.scope.as_ref().unwrap();
|
let scope = data.scope.as_ref().unwrap();
|
||||||
@@ -262,7 +257,7 @@ async fn _password_login(
|
|||||||
|
|
||||||
let (mut device, new_device) = get_device(&data, conn, &user).await;
|
let (mut device, new_device) = get_device(&data, conn, &user).await;
|
||||||
|
|
||||||
let twofactor_token = twofactor_auth(&user, &data, &mut device, ip, conn).await?;
|
let twofactor_token = twofactor_auth(&user, &data, &mut device, ip, client_version, conn).await?;
|
||||||
|
|
||||||
if CONFIG.mail_enabled() && new_device {
|
if CONFIG.mail_enabled() && new_device {
|
||||||
if let Err(e) = mail::send_new_device_logged_in(&user.email, &ip.ip.to_string(), &now, &device).await {
|
if let Err(e) = mail::send_new_device_logged_in(&user.email, &ip.ip.to_string(), &now, &device).await {
|
||||||
@@ -291,38 +286,10 @@ async fn _password_login(
|
|||||||
// See: https://github.com/dani-garcia/vaultwarden/issues/4156
|
// See: https://github.com/dani-garcia/vaultwarden/issues/4156
|
||||||
// ---
|
// ---
|
||||||
// let members = Membership::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);
|
let (access_token, expires_in) = device.refresh_tokens(&user, scope_vec, data.client_id);
|
||||||
device.save(conn).await?;
|
device.save(conn).await?;
|
||||||
|
|
||||||
// Fetch all valid Master Password Policies and merge them into one with all true's and larges numbers as one policy
|
let master_password_policy = master_password_policy(&user, conn).await;
|
||||||
let master_password_policies: Vec<MasterPasswordPolicy> =
|
|
||||||
OrgPolicy::find_accepted_and_confirmed_by_user_and_active_policy(
|
|
||||||
&user.uuid,
|
|
||||||
OrgPolicyType::MasterPassword,
|
|
||||||
conn,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|p| serde_json::from_str(&p.data).ok())
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let master_password_policy = if !master_password_policies.is_empty() {
|
|
||||||
let mut mpp_json = json!(master_password_policies.into_iter().reduce(|acc, policy| {
|
|
||||||
MasterPasswordPolicy {
|
|
||||||
min_complexity: acc.min_complexity.max(policy.min_complexity),
|
|
||||||
min_length: acc.min_length.max(policy.min_length),
|
|
||||||
require_lower: acc.require_lower || policy.require_lower,
|
|
||||||
require_upper: acc.require_upper || policy.require_upper,
|
|
||||||
require_numbers: acc.require_numbers || policy.require_numbers,
|
|
||||||
require_special: acc.require_special || policy.require_special,
|
|
||||||
enforce_on_login: acc.enforce_on_login || policy.enforce_on_login,
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
mpp_json["object"] = json!("masterPasswordPolicy");
|
|
||||||
mpp_json
|
|
||||||
} else {
|
|
||||||
json!({"object": "masterPasswordPolicy"})
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut result = json!({
|
let mut result = json!({
|
||||||
"access_token": access_token,
|
"access_token": access_token,
|
||||||
@@ -441,7 +408,7 @@ async fn _user_api_key_login(
|
|||||||
// See: https://github.com/dani-garcia/vaultwarden/issues/4156
|
// See: https://github.com/dani-garcia/vaultwarden/issues/4156
|
||||||
// ---
|
// ---
|
||||||
// let members = Membership::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);
|
let (access_token, expires_in) = device.refresh_tokens(&user, scope_vec, data.client_id);
|
||||||
device.save(conn).await?;
|
device.save(conn).await?;
|
||||||
|
|
||||||
info!("User {} logged in successfully via API key. IP: {}", user.email, ip.ip);
|
info!("User {} logged in successfully via API key. IP: {}", user.email, ip.ip);
|
||||||
@@ -520,6 +487,7 @@ async fn twofactor_auth(
|
|||||||
data: &ConnectData,
|
data: &ConnectData,
|
||||||
device: &mut Device,
|
device: &mut Device,
|
||||||
ip: &ClientIp,
|
ip: &ClientIp,
|
||||||
|
client_version: &Option<ClientVersion>,
|
||||||
conn: &mut DbConn,
|
conn: &mut DbConn,
|
||||||
) -> ApiResult<Option<String>> {
|
) -> ApiResult<Option<String>> {
|
||||||
let twofactors = TwoFactor::find_by_user(&user.uuid, conn).await;
|
let twofactors = TwoFactor::find_by_user(&user.uuid, conn).await;
|
||||||
@@ -538,7 +506,10 @@ async fn twofactor_auth(
|
|||||||
let twofactor_code = match data.two_factor_token {
|
let twofactor_code = match data.two_factor_token {
|
||||||
Some(ref code) => code,
|
Some(ref code) => code,
|
||||||
None => {
|
None => {
|
||||||
err_json!(_json_err_twofactor(&twofactor_ids, &user.uuid, data, conn).await?, "2FA token not provided")
|
err_json!(
|
||||||
|
_json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, conn).await?,
|
||||||
|
"2FA token not provided"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -585,7 +556,7 @@ async fn twofactor_auth(
|
|||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
err_json!(
|
err_json!(
|
||||||
_json_err_twofactor(&twofactor_ids, &user.uuid, data, conn).await?,
|
_json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, conn).await?,
|
||||||
"2FA Remember token not provided"
|
"2FA Remember token not provided"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -617,6 +588,7 @@ async fn _json_err_twofactor(
|
|||||||
providers: &[i32],
|
providers: &[i32],
|
||||||
user_id: &UserId,
|
user_id: &UserId,
|
||||||
data: &ConnectData,
|
data: &ConnectData,
|
||||||
|
client_version: &Option<ClientVersion>,
|
||||||
conn: &mut DbConn,
|
conn: &mut DbConn,
|
||||||
) -> ApiResult<Value> {
|
) -> ApiResult<Value> {
|
||||||
let mut result = json!({
|
let mut result = json!({
|
||||||
@@ -689,8 +661,16 @@ async fn _json_err_twofactor(
|
|||||||
err!("No twofactor email registered")
|
err!("No twofactor email registered")
|
||||||
};
|
};
|
||||||
|
|
||||||
// Send email immediately if email is the only 2FA option
|
// Starting with version 2025.5.0 the client will call `/api/two-factor/send-email-login`.
|
||||||
if providers.len() == 1 {
|
let disabled_send = if let Some(cv) = client_version {
|
||||||
|
let ver_match = semver::VersionReq::parse(">=2025.5.0").unwrap();
|
||||||
|
ver_match.matches(&cv.0)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send email immediately if email is the only 2FA option.
|
||||||
|
if providers.len() == 1 && !disabled_send {
|
||||||
email::send_token(user_id, conn).await?
|
email::send_token(user_id, conn).await?
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -738,7 +718,10 @@ async fn register_verification_email(
|
|||||||
) -> ApiResult<RegisterVerificationResponse> {
|
) -> ApiResult<RegisterVerificationResponse> {
|
||||||
let data = data.into_inner();
|
let data = data.into_inner();
|
||||||
|
|
||||||
if !CONFIG.is_signup_allowed(&data.email) {
|
// the registration can only continue if signup is allowed or there exists an invitation
|
||||||
|
if !(CONFIG.is_signup_allowed(&data.email)
|
||||||
|
|| (!CONFIG.mail_enabled() && Invitation::find_by_mail(&data.email, &mut conn).await.is_some()))
|
||||||
|
{
|
||||||
err!("Registration not allowed or user already exists")
|
err!("Registration not allowed or user already exists")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+50
-1
@@ -32,7 +32,10 @@ pub use crate::api::{
|
|||||||
web::routes as web_routes,
|
web::routes as web_routes,
|
||||||
web::static_files,
|
web::static_files,
|
||||||
};
|
};
|
||||||
use crate::db::{models::User, DbConn};
|
use crate::db::{
|
||||||
|
models::{OrgPolicy, OrgPolicyType, User},
|
||||||
|
DbConn,
|
||||||
|
};
|
||||||
|
|
||||||
// Type aliases for API methods results
|
// Type aliases for API methods results
|
||||||
type ApiResult<T> = Result<T, crate::error::Error>;
|
type ApiResult<T> = Result<T, crate::error::Error>;
|
||||||
@@ -68,3 +71,49 @@ impl PasswordOrOtpData {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Deserialize, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct MasterPasswordPolicy {
|
||||||
|
min_complexity: Option<u8>,
|
||||||
|
min_length: Option<u32>,
|
||||||
|
require_lower: bool,
|
||||||
|
require_upper: bool,
|
||||||
|
require_numbers: bool,
|
||||||
|
require_special: bool,
|
||||||
|
enforce_on_login: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch all valid Master Password Policies and merge them into one with all trues and largest numbers as one policy
|
||||||
|
async fn master_password_policy(user: &User, conn: &DbConn) -> Value {
|
||||||
|
let master_password_policies: Vec<MasterPasswordPolicy> =
|
||||||
|
OrgPolicy::find_accepted_and_confirmed_by_user_and_active_policy(
|
||||||
|
&user.uuid,
|
||||||
|
OrgPolicyType::MasterPassword,
|
||||||
|
conn,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|p| serde_json::from_str(&p.data).ok())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut mpp_json = if !master_password_policies.is_empty() {
|
||||||
|
json!(master_password_policies.into_iter().reduce(|acc, policy| {
|
||||||
|
MasterPasswordPolicy {
|
||||||
|
min_complexity: acc.min_complexity.max(policy.min_complexity),
|
||||||
|
min_length: acc.min_length.max(policy.min_length),
|
||||||
|
require_lower: acc.require_lower || policy.require_lower,
|
||||||
|
require_upper: acc.require_upper || policy.require_upper,
|
||||||
|
require_numbers: acc.require_numbers || policy.require_numbers,
|
||||||
|
require_special: acc.require_special || policy.require_special,
|
||||||
|
enforce_on_login: acc.enforce_on_login || policy.enforce_on_login,
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
json!({})
|
||||||
|
};
|
||||||
|
|
||||||
|
// NOTE: Upstream still uses PascalCase here for `Object`!
|
||||||
|
mpp_json["Object"] = json!("masterPasswordPolicy");
|
||||||
|
mpp_json
|
||||||
|
}
|
||||||
|
|||||||
+19
-25
@@ -10,7 +10,7 @@ use rocket_ws::{Message, WebSocket};
|
|||||||
use crate::{
|
use crate::{
|
||||||
auth::{ClientIp, WsAccessTokenHeader},
|
auth::{ClientIp, WsAccessTokenHeader},
|
||||||
db::{
|
db::{
|
||||||
models::{AuthRequestId, Cipher, CollectionId, DeviceId, Folder, Send as DbSend, User, UserId},
|
models::{AuthRequestId, Cipher, CollectionId, Device, DeviceId, Folder, PushId, Send as DbSend, User, UserId},
|
||||||
DbConn,
|
DbConn,
|
||||||
},
|
},
|
||||||
Error, CONFIG,
|
Error, CONFIG,
|
||||||
@@ -339,7 +339,7 @@ impl WebSocketUsers {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: The last modified date needs to be updated before calling these methods
|
// NOTE: The last modified date needs to be updated before calling these methods
|
||||||
pub async fn send_user_update(&self, ut: UpdateType, user: &User) {
|
pub async fn send_user_update(&self, ut: UpdateType, user: &User, push_uuid: &Option<PushId>, conn: &mut DbConn) {
|
||||||
// Skip any processing if both WebSockets and Push are not active
|
// Skip any processing if both WebSockets and Push are not active
|
||||||
if *NOTIFICATIONS_DISABLED {
|
if *NOTIFICATIONS_DISABLED {
|
||||||
return;
|
return;
|
||||||
@@ -355,11 +355,11 @@ impl WebSocketUsers {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if CONFIG.push_enabled() {
|
if CONFIG.push_enabled() {
|
||||||
push_user_update(ut, user);
|
push_user_update(ut, user, push_uuid, conn).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn send_logout(&self, user: &User, acting_device_id: Option<DeviceId>) {
|
pub async fn send_logout(&self, user: &User, acting_device_id: Option<DeviceId>, conn: &mut DbConn) {
|
||||||
// Skip any processing if both WebSockets and Push are not active
|
// Skip any processing if both WebSockets and Push are not active
|
||||||
if *NOTIFICATIONS_DISABLED {
|
if *NOTIFICATIONS_DISABLED {
|
||||||
return;
|
return;
|
||||||
@@ -375,17 +375,11 @@ impl WebSocketUsers {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if CONFIG.push_enabled() {
|
if CONFIG.push_enabled() {
|
||||||
push_logout(user, acting_device_id.clone());
|
push_logout(user, acting_device_id.clone(), conn).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn send_folder_update(
|
pub async fn send_folder_update(&self, ut: UpdateType, folder: &Folder, device: &Device, conn: &mut DbConn) {
|
||||||
&self,
|
|
||||||
ut: UpdateType,
|
|
||||||
folder: &Folder,
|
|
||||||
acting_device_id: &DeviceId,
|
|
||||||
conn: &mut DbConn,
|
|
||||||
) {
|
|
||||||
// Skip any processing if both WebSockets and Push are not active
|
// Skip any processing if both WebSockets and Push are not active
|
||||||
if *NOTIFICATIONS_DISABLED {
|
if *NOTIFICATIONS_DISABLED {
|
||||||
return;
|
return;
|
||||||
@@ -397,7 +391,7 @@ impl WebSocketUsers {
|
|||||||
("RevisionDate".into(), serialize_date(folder.updated_at)),
|
("RevisionDate".into(), serialize_date(folder.updated_at)),
|
||||||
],
|
],
|
||||||
ut,
|
ut,
|
||||||
Some(acting_device_id.clone()),
|
Some(device.uuid.clone()),
|
||||||
);
|
);
|
||||||
|
|
||||||
if CONFIG.enable_websocket() {
|
if CONFIG.enable_websocket() {
|
||||||
@@ -405,7 +399,7 @@ impl WebSocketUsers {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if CONFIG.push_enabled() {
|
if CONFIG.push_enabled() {
|
||||||
push_folder_update(ut, folder, acting_device_id, conn).await;
|
push_folder_update(ut, folder, device, conn).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,7 +408,7 @@ impl WebSocketUsers {
|
|||||||
ut: UpdateType,
|
ut: UpdateType,
|
||||||
cipher: &Cipher,
|
cipher: &Cipher,
|
||||||
user_ids: &[UserId],
|
user_ids: &[UserId],
|
||||||
acting_device_id: &DeviceId,
|
device: &Device,
|
||||||
collection_uuids: Option<Vec<CollectionId>>,
|
collection_uuids: Option<Vec<CollectionId>>,
|
||||||
conn: &mut DbConn,
|
conn: &mut DbConn,
|
||||||
) {
|
) {
|
||||||
@@ -444,7 +438,7 @@ impl WebSocketUsers {
|
|||||||
("RevisionDate".into(), revision_date),
|
("RevisionDate".into(), revision_date),
|
||||||
],
|
],
|
||||||
ut,
|
ut,
|
||||||
Some(acting_device_id.clone()),
|
Some(device.uuid.clone()), // Acting device id (unique device/app uuid)
|
||||||
);
|
);
|
||||||
|
|
||||||
if CONFIG.enable_websocket() {
|
if CONFIG.enable_websocket() {
|
||||||
@@ -454,7 +448,7 @@ impl WebSocketUsers {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if CONFIG.push_enabled() && user_ids.len() == 1 {
|
if CONFIG.push_enabled() && user_ids.len() == 1 {
|
||||||
push_cipher_update(ut, cipher, acting_device_id, conn).await;
|
push_cipher_update(ut, cipher, device, conn).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -463,7 +457,7 @@ impl WebSocketUsers {
|
|||||||
ut: UpdateType,
|
ut: UpdateType,
|
||||||
send: &DbSend,
|
send: &DbSend,
|
||||||
user_ids: &[UserId],
|
user_ids: &[UserId],
|
||||||
acting_device_id: &DeviceId,
|
device: &Device,
|
||||||
conn: &mut DbConn,
|
conn: &mut DbConn,
|
||||||
) {
|
) {
|
||||||
// Skip any processing if both WebSockets and Push are not active
|
// Skip any processing if both WebSockets and Push are not active
|
||||||
@@ -488,7 +482,7 @@ impl WebSocketUsers {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if CONFIG.push_enabled() && user_ids.len() == 1 {
|
if CONFIG.push_enabled() && user_ids.len() == 1 {
|
||||||
push_send_update(ut, send, acting_device_id, conn).await;
|
push_send_update(ut, send, device, conn).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -496,7 +490,7 @@ impl WebSocketUsers {
|
|||||||
&self,
|
&self,
|
||||||
user_id: &UserId,
|
user_id: &UserId,
|
||||||
auth_request_uuid: &str,
|
auth_request_uuid: &str,
|
||||||
acting_device_id: &DeviceId,
|
device: &Device,
|
||||||
conn: &mut DbConn,
|
conn: &mut DbConn,
|
||||||
) {
|
) {
|
||||||
// Skip any processing if both WebSockets and Push are not active
|
// Skip any processing if both WebSockets and Push are not active
|
||||||
@@ -506,14 +500,14 @@ impl WebSocketUsers {
|
|||||||
let data = create_update(
|
let data = create_update(
|
||||||
vec![("Id".into(), auth_request_uuid.to_owned().into()), ("UserId".into(), user_id.to_string().into())],
|
vec![("Id".into(), auth_request_uuid.to_owned().into()), ("UserId".into(), user_id.to_string().into())],
|
||||||
UpdateType::AuthRequest,
|
UpdateType::AuthRequest,
|
||||||
Some(acting_device_id.clone()),
|
Some(device.uuid.clone()),
|
||||||
);
|
);
|
||||||
if CONFIG.enable_websocket() {
|
if CONFIG.enable_websocket() {
|
||||||
self.send_update(user_id, &data).await;
|
self.send_update(user_id, &data).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
if CONFIG.push_enabled() {
|
if CONFIG.push_enabled() {
|
||||||
push_auth_request(user_id.clone(), auth_request_uuid.to_owned(), conn).await;
|
push_auth_request(user_id, auth_request_uuid, device, conn).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -521,7 +515,7 @@ impl WebSocketUsers {
|
|||||||
&self,
|
&self,
|
||||||
user_id: &UserId,
|
user_id: &UserId,
|
||||||
auth_request_id: &AuthRequestId,
|
auth_request_id: &AuthRequestId,
|
||||||
approving_device_id: &DeviceId,
|
device: &Device,
|
||||||
conn: &mut DbConn,
|
conn: &mut DbConn,
|
||||||
) {
|
) {
|
||||||
// Skip any processing if both WebSockets and Push are not active
|
// Skip any processing if both WebSockets and Push are not active
|
||||||
@@ -531,14 +525,14 @@ impl WebSocketUsers {
|
|||||||
let data = create_update(
|
let data = create_update(
|
||||||
vec![("Id".into(), auth_request_id.to_string().into()), ("UserId".into(), user_id.to_string().into())],
|
vec![("Id".into(), auth_request_id.to_string().into()), ("UserId".into(), user_id.to_string().into())],
|
||||||
UpdateType::AuthRequestResponse,
|
UpdateType::AuthRequestResponse,
|
||||||
Some(approving_device_id.clone()),
|
Some(device.uuid.clone()),
|
||||||
);
|
);
|
||||||
if CONFIG.enable_websocket() {
|
if CONFIG.enable_websocket() {
|
||||||
self.send_update(user_id, &data).await;
|
self.send_update(user_id, &data).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
if CONFIG.push_enabled() {
|
if CONFIG.push_enabled() {
|
||||||
push_auth_response(user_id, auth_request_id, approving_device_id, conn).await;
|
push_auth_response(user_id, auth_request_id, device, conn).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+127
-111
@@ -7,9 +7,9 @@ use tokio::sync::RwLock;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{ApiResult, EmptyResult, UpdateType},
|
api::{ApiResult, EmptyResult, UpdateType},
|
||||||
db::models::{AuthRequestId, Cipher, Device, DeviceId, Folder, Send, User, UserId},
|
db::models::{AuthRequestId, Cipher, Device, DeviceId, Folder, PushId, Send, User, UserId},
|
||||||
http_client::make_http_request,
|
http_client::make_http_request,
|
||||||
util::format_date,
|
util::{format_date, get_uuid},
|
||||||
CONFIG,
|
CONFIG,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -28,20 +28,20 @@ struct LocalAuthPushToken {
|
|||||||
valid_until: Instant,
|
valid_until: Instant,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_auth_push_token() -> ApiResult<String> {
|
async fn get_auth_api_token() -> ApiResult<String> {
|
||||||
static PUSH_TOKEN: Lazy<RwLock<LocalAuthPushToken>> = Lazy::new(|| {
|
static API_TOKEN: Lazy<RwLock<LocalAuthPushToken>> = Lazy::new(|| {
|
||||||
RwLock::new(LocalAuthPushToken {
|
RwLock::new(LocalAuthPushToken {
|
||||||
access_token: String::new(),
|
access_token: String::new(),
|
||||||
valid_until: Instant::now(),
|
valid_until: Instant::now(),
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
let push_token = PUSH_TOKEN.read().await;
|
let api_token = API_TOKEN.read().await;
|
||||||
|
|
||||||
if push_token.valid_until.saturating_duration_since(Instant::now()).as_secs() > 0 {
|
if api_token.valid_until.saturating_duration_since(Instant::now()).as_secs() > 0 {
|
||||||
debug!("Auth Push token still valid, no need for a new one");
|
debug!("Auth Push token still valid, no need for a new one");
|
||||||
return Ok(push_token.access_token.clone());
|
return Ok(api_token.access_token.clone());
|
||||||
}
|
}
|
||||||
drop(push_token); // Drop the read lock now
|
drop(api_token); // Drop the read lock now
|
||||||
|
|
||||||
let installation_id = CONFIG.push_installation_id();
|
let installation_id = CONFIG.push_installation_id();
|
||||||
let client_id = format!("installation.{installation_id}");
|
let client_id = format!("installation.{installation_id}");
|
||||||
@@ -68,44 +68,48 @@ async fn get_auth_push_token() -> ApiResult<String> {
|
|||||||
Err(e) => err!(format!("Unexpected push token received from bitwarden server: {e}")),
|
Err(e) => err!(format!("Unexpected push token received from bitwarden server: {e}")),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut push_token = PUSH_TOKEN.write().await;
|
let mut api_token = API_TOKEN.write().await;
|
||||||
push_token.valid_until = Instant::now()
|
api_token.valid_until = Instant::now()
|
||||||
.checked_add(Duration::new((json_pushtoken.expires_in / 2) as u64, 0)) // Token valid for half the specified time
|
.checked_add(Duration::new((json_pushtoken.expires_in / 2) as u64, 0)) // Token valid for half the specified time
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
push_token.access_token = json_pushtoken.access_token;
|
api_token.access_token = json_pushtoken.access_token;
|
||||||
|
|
||||||
debug!("Token still valid for {}", push_token.valid_until.saturating_duration_since(Instant::now()).as_secs());
|
debug!("Token still valid for {}", api_token.valid_until.saturating_duration_since(Instant::now()).as_secs());
|
||||||
Ok(push_token.access_token.clone())
|
Ok(api_token.access_token.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn register_push_device(device: &mut Device, conn: &mut crate::db::DbConn) -> EmptyResult {
|
pub async fn register_push_device(device: &mut Device, conn: &mut crate::db::DbConn) -> EmptyResult {
|
||||||
if !CONFIG.push_enabled() || !device.is_push_device() || device.is_registered() {
|
if !CONFIG.push_enabled() || !device.is_push_device() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
if device.push_token.is_none() {
|
if device.push_token.is_none() {
|
||||||
warn!("Skipping the registration of the device {} because the push_token field is empty.", device.uuid);
|
warn!("Skipping the registration of the device {:?} because the push_token field is empty.", device.uuid);
|
||||||
warn!("To get rid of this message you need to clear the app data and reconnect the device.");
|
warn!("To get rid of this message you need to logout, clear the app data and login again on the device.");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
debug!("Registering Device {}", device.uuid);
|
debug!("Registering Device {:?}", device.push_uuid);
|
||||||
|
|
||||||
// generate a random push_uuid so we know the device is registered
|
// Generate a random push_uuid so if it doesn't already have one
|
||||||
device.push_uuid = Some(uuid::Uuid::new_v4().to_string());
|
if device.push_uuid.is_none() {
|
||||||
|
device.push_uuid = Some(PushId(get_uuid()));
|
||||||
|
}
|
||||||
|
|
||||||
//Needed to register a device for push to bitwarden :
|
//Needed to register a device for push to bitwarden :
|
||||||
let data = json!({
|
let data = json!({
|
||||||
|
"deviceId": device.push_uuid, // Unique UUID per user/device
|
||||||
|
"pushToken": device.push_token,
|
||||||
"userId": device.user_uuid,
|
"userId": device.user_uuid,
|
||||||
"deviceId": device.push_uuid,
|
|
||||||
"identifier": device.uuid,
|
|
||||||
"type": device.atype,
|
"type": device.atype,
|
||||||
"pushToken": device.push_token
|
"identifier": device.uuid, // Unique UUID of the device/app, determined by the device/app it self currently registering
|
||||||
|
// "organizationIds:" [] // TODO: This is not yet implemented by Vaultwarden!
|
||||||
|
"installationId": CONFIG.push_installation_id(),
|
||||||
});
|
});
|
||||||
|
|
||||||
let auth_push_token = get_auth_push_token().await?;
|
let auth_api_token = get_auth_api_token().await?;
|
||||||
let auth_header = format!("Bearer {}", &auth_push_token);
|
let auth_header = format!("Bearer {auth_api_token}");
|
||||||
|
|
||||||
if let Err(e) = make_http_request(Method::POST, &(CONFIG.push_relay_uri() + "/push/register"))?
|
if let Err(e) = make_http_request(Method::POST, &(CONFIG.push_relay_uri() + "/push/register"))?
|
||||||
.header(CONTENT_TYPE, "application/json")
|
.header(CONTENT_TYPE, "application/json")
|
||||||
@@ -126,18 +130,21 @@ pub async fn register_push_device(device: &mut Device, conn: &mut crate::db::DbC
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn unregister_push_device(push_id: Option<String>) -> EmptyResult {
|
pub async fn unregister_push_device(push_id: &Option<PushId>) -> EmptyResult {
|
||||||
if !CONFIG.push_enabled() || push_id.is_none() {
|
if !CONFIG.push_enabled() || push_id.is_none() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let auth_push_token = get_auth_push_token().await?;
|
let auth_api_token = get_auth_api_token().await?;
|
||||||
|
|
||||||
let auth_header = format!("Bearer {}", &auth_push_token);
|
let auth_header = format!("Bearer {auth_api_token}");
|
||||||
|
|
||||||
match make_http_request(Method::DELETE, &(CONFIG.push_relay_uri() + "/push/" + &push_id.unwrap()))?
|
match make_http_request(
|
||||||
.header(AUTHORIZATION, auth_header)
|
Method::POST,
|
||||||
.send()
|
&format!("{}/push/delete/{}", CONFIG.push_relay_uri(), push_id.as_ref().unwrap()),
|
||||||
.await
|
)?
|
||||||
|
.header(AUTHORIZATION, auth_header)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
{
|
{
|
||||||
Ok(r) => r,
|
Ok(r) => r,
|
||||||
Err(e) => err!(format!("An error occurred during device unregistration: {e}")),
|
Err(e) => err!(format!("An error occurred during device unregistration: {e}")),
|
||||||
@@ -145,12 +152,7 @@ pub async fn unregister_push_device(push_id: Option<String>) -> EmptyResult {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn push_cipher_update(
|
pub async fn push_cipher_update(ut: UpdateType, cipher: &Cipher, device: &Device, conn: &mut crate::db::DbConn) {
|
||||||
ut: UpdateType,
|
|
||||||
cipher: &Cipher,
|
|
||||||
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.
|
// 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() {
|
if cipher.organization_uuid.is_some() {
|
||||||
return;
|
return;
|
||||||
@@ -163,87 +165,97 @@ pub async fn push_cipher_update(
|
|||||||
if Device::check_user_has_push_device(user_id, conn).await {
|
if Device::check_user_has_push_device(user_id, conn).await {
|
||||||
send_to_push_relay(json!({
|
send_to_push_relay(json!({
|
||||||
"userId": user_id,
|
"userId": user_id,
|
||||||
"organizationId": (),
|
"organizationId": null,
|
||||||
"deviceId": acting_device_id,
|
"deviceId": device.push_uuid, // Should be the records unique uuid of the acting device (unique uuid per user/device)
|
||||||
"identifier": acting_device_id,
|
"identifier": device.uuid, // Should be the acting device id (aka uuid per device/app)
|
||||||
"type": ut as i32,
|
"type": ut as i32,
|
||||||
"payload": {
|
"payload": {
|
||||||
"Id": cipher.uuid,
|
"id": cipher.uuid,
|
||||||
"UserId": cipher.user_uuid,
|
"userId": cipher.user_uuid,
|
||||||
"OrganizationId": (),
|
"organizationId": null,
|
||||||
"RevisionDate": format_date(&cipher.updated_at)
|
"collectionIds": null,
|
||||||
}
|
"revisionDate": format_date(&cipher.updated_at)
|
||||||
|
},
|
||||||
|
"clientType": null,
|
||||||
|
"installationId": null
|
||||||
}))
|
}))
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn push_logout(user: &User, acting_device_id: Option<DeviceId>) {
|
pub async fn push_logout(user: &User, acting_device_id: Option<DeviceId>, conn: &mut crate::db::DbConn) {
|
||||||
let acting_device_id: Value = acting_device_id.map(|v| v.to_string().into()).unwrap_or_else(|| Value::Null);
|
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!({
|
if Device::check_user_has_push_device(&user.uuid, conn).await {
|
||||||
"userId": user.uuid,
|
|
||||||
"organizationId": (),
|
|
||||||
"deviceId": acting_device_id,
|
|
||||||
"identifier": acting_device_id,
|
|
||||||
"type": UpdateType::LogOut as i32,
|
|
||||||
"payload": {
|
|
||||||
"UserId": user.uuid,
|
|
||||||
"Date": format_date(&user.updated_at)
|
|
||||||
}
|
|
||||||
})));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn push_user_update(ut: UpdateType, user: &User) {
|
|
||||||
tokio::task::spawn(send_to_push_relay(json!({
|
|
||||||
"userId": user.uuid,
|
|
||||||
"organizationId": (),
|
|
||||||
"deviceId": (),
|
|
||||||
"identifier": (),
|
|
||||||
"type": ut as i32,
|
|
||||||
"payload": {
|
|
||||||
"UserId": user.uuid,
|
|
||||||
"Date": format_date(&user.updated_at)
|
|
||||||
}
|
|
||||||
})));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn push_folder_update(
|
|
||||||
ut: UpdateType,
|
|
||||||
folder: &Folder,
|
|
||||||
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!({
|
tokio::task::spawn(send_to_push_relay(json!({
|
||||||
"userId": folder.user_uuid,
|
"userId": user.uuid,
|
||||||
"organizationId": (),
|
"organizationId": (),
|
||||||
"deviceId": acting_device_id,
|
"deviceId": acting_device_id,
|
||||||
"identifier": acting_device_id,
|
"identifier": acting_device_id,
|
||||||
"type": ut as i32,
|
"type": UpdateType::LogOut as i32,
|
||||||
"payload": {
|
"payload": {
|
||||||
"Id": folder.uuid,
|
"userId": user.uuid,
|
||||||
"UserId": folder.user_uuid,
|
"date": format_date(&user.updated_at)
|
||||||
"RevisionDate": format_date(&folder.updated_at)
|
},
|
||||||
}
|
"clientType": null,
|
||||||
|
"installationId": null
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn push_send_update(ut: UpdateType, send: &Send, acting_device_id: &DeviceId, conn: &mut crate::db::DbConn) {
|
pub async fn push_user_update(ut: UpdateType, user: &User, push_uuid: &Option<PushId>, conn: &mut crate::db::DbConn) {
|
||||||
|
if Device::check_user_has_push_device(&user.uuid, conn).await {
|
||||||
|
tokio::task::spawn(send_to_push_relay(json!({
|
||||||
|
"userId": user.uuid,
|
||||||
|
"organizationId": null,
|
||||||
|
"deviceId": push_uuid,
|
||||||
|
"identifier": null,
|
||||||
|
"type": ut as i32,
|
||||||
|
"payload": {
|
||||||
|
"userId": user.uuid,
|
||||||
|
"date": format_date(&user.updated_at)
|
||||||
|
},
|
||||||
|
"clientType": null,
|
||||||
|
"installationId": null
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn push_folder_update(ut: UpdateType, folder: &Folder, device: &Device, 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": null,
|
||||||
|
"deviceId": device.push_uuid, // Should be the records unique uuid of the acting device (unique uuid per user/device)
|
||||||
|
"identifier": device.uuid, // Should be the acting device id (aka uuid per device/app)
|
||||||
|
"type": ut as i32,
|
||||||
|
"payload": {
|
||||||
|
"id": folder.uuid,
|
||||||
|
"userId": folder.user_uuid,
|
||||||
|
"revisionDate": format_date(&folder.updated_at)
|
||||||
|
},
|
||||||
|
"clientType": null,
|
||||||
|
"installationId": null
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn push_send_update(ut: UpdateType, send: &Send, device: &Device, conn: &mut crate::db::DbConn) {
|
||||||
if let Some(s) = &send.user_uuid {
|
if let Some(s) = &send.user_uuid {
|
||||||
if Device::check_user_has_push_device(s, conn).await {
|
if Device::check_user_has_push_device(s, conn).await {
|
||||||
tokio::task::spawn(send_to_push_relay(json!({
|
tokio::task::spawn(send_to_push_relay(json!({
|
||||||
"userId": send.user_uuid,
|
"userId": send.user_uuid,
|
||||||
"organizationId": (),
|
"organizationId": null,
|
||||||
"deviceId": acting_device_id,
|
"deviceId": device.push_uuid, // Should be the records unique uuid of the acting device (unique uuid per user/device)
|
||||||
"identifier": acting_device_id,
|
"identifier": device.uuid, // Should be the acting device id (aka uuid per device/app)
|
||||||
"type": ut as i32,
|
"type": ut as i32,
|
||||||
"payload": {
|
"payload": {
|
||||||
"Id": send.uuid,
|
"id": send.uuid,
|
||||||
"UserId": send.user_uuid,
|
"userId": send.user_uuid,
|
||||||
"RevisionDate": format_date(&send.revision_date)
|
"revisionDate": format_date(&send.revision_date)
|
||||||
}
|
},
|
||||||
|
"clientType": null,
|
||||||
|
"installationId": null
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -254,7 +266,7 @@ async fn send_to_push_relay(notification_data: Value) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let auth_push_token = match get_auth_push_token().await {
|
let auth_api_token = match get_auth_api_token().await {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
debug!("Could not get the auth push token: {e}");
|
debug!("Could not get the auth push token: {e}");
|
||||||
@@ -262,7 +274,7 @@ async fn send_to_push_relay(notification_data: Value) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let auth_header = format!("Bearer {}", &auth_push_token);
|
let auth_header = format!("Bearer {auth_api_token}");
|
||||||
|
|
||||||
let req = match make_http_request(Method::POST, &(CONFIG.push_relay_uri() + "/push/send")) {
|
let req = match make_http_request(Method::POST, &(CONFIG.push_relay_uri() + "/push/send")) {
|
||||||
Ok(r) => r,
|
Ok(r) => r,
|
||||||
@@ -284,18 +296,20 @@ async fn send_to_push_relay(notification_data: Value) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn push_auth_request(user_id: UserId, auth_request_id: String, conn: &mut crate::db::DbConn) {
|
pub async fn push_auth_request(user_id: &UserId, auth_request_id: &str, device: &Device, conn: &mut crate::db::DbConn) {
|
||||||
if Device::check_user_has_push_device(&user_id, conn).await {
|
if Device::check_user_has_push_device(user_id, conn).await {
|
||||||
tokio::task::spawn(send_to_push_relay(json!({
|
tokio::task::spawn(send_to_push_relay(json!({
|
||||||
"userId": user_id,
|
"userId": user_id,
|
||||||
"organizationId": (),
|
"organizationId": null,
|
||||||
"deviceId": null,
|
"deviceId": device.push_uuid, // Should be the records unique uuid of the acting device (unique uuid per user/device)
|
||||||
"identifier": null,
|
"identifier": device.uuid, // Should be the acting device id (aka uuid per device/app)
|
||||||
"type": UpdateType::AuthRequest as i32,
|
"type": UpdateType::AuthRequest as i32,
|
||||||
"payload": {
|
"payload": {
|
||||||
"Id": auth_request_id,
|
"userId": user_id,
|
||||||
"UserId": user_id,
|
"id": auth_request_id,
|
||||||
}
|
},
|
||||||
|
"clientType": null,
|
||||||
|
"installationId": null
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -303,20 +317,22 @@ pub async fn push_auth_request(user_id: UserId, auth_request_id: String, conn: &
|
|||||||
pub async fn push_auth_response(
|
pub async fn push_auth_response(
|
||||||
user_id: &UserId,
|
user_id: &UserId,
|
||||||
auth_request_id: &AuthRequestId,
|
auth_request_id: &AuthRequestId,
|
||||||
approving_device_id: &DeviceId,
|
device: &Device,
|
||||||
conn: &mut crate::db::DbConn,
|
conn: &mut crate::db::DbConn,
|
||||||
) {
|
) {
|
||||||
if Device::check_user_has_push_device(user_id, conn).await {
|
if Device::check_user_has_push_device(user_id, conn).await {
|
||||||
tokio::task::spawn(send_to_push_relay(json!({
|
tokio::task::spawn(send_to_push_relay(json!({
|
||||||
"userId": user_id,
|
"userId": user_id,
|
||||||
"organizationId": (),
|
"organizationId": null,
|
||||||
"deviceId": approving_device_id,
|
"deviceId": device.push_uuid, // Should be the records unique uuid of the acting device (unique uuid per user/device)
|
||||||
"identifier": approving_device_id,
|
"identifier": device.uuid, // Should be the acting device id (aka uuid per device/app)
|
||||||
"type": UpdateType::AuthRequestResponse as i32,
|
"type": UpdateType::AuthRequestResponse as i32,
|
||||||
"payload": {
|
"payload": {
|
||||||
"Id": auth_request_id,
|
"userId": user_id,
|
||||||
"UserId": user_id,
|
"id": auth_request_id,
|
||||||
}
|
},
|
||||||
|
"clientType": null,
|
||||||
|
"installationId": null
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ fn vaultwarden_css() -> Cached<Css<String>> {
|
|||||||
let css_options = json!({
|
let css_options = json!({
|
||||||
"signup_disabled": !CONFIG.signups_allowed() && CONFIG.signups_domains_whitelist().is_empty(),
|
"signup_disabled": !CONFIG.signups_allowed() && CONFIG.signups_domains_whitelist().is_empty(),
|
||||||
"mail_enabled": CONFIG.mail_enabled(),
|
"mail_enabled": CONFIG.mail_enabled(),
|
||||||
|
"mail_2fa_enabled": CONFIG._enable_email_2fa(),
|
||||||
"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(),
|
"emergency_access_allowed": CONFIG.emergency_access_allowed(),
|
||||||
"sends_allowed": CONFIG.sends_allowed(),
|
"sends_allowed": CONFIG.sends_allowed(),
|
||||||
|
|||||||
+52
-49
@@ -7,16 +7,14 @@ use once_cell::sync::{Lazy, OnceCell};
|
|||||||
use openssl::rsa::Rsa;
|
use openssl::rsa::Rsa;
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use serde::ser::Serialize;
|
use serde::ser::Serialize;
|
||||||
use std::{
|
use std::{env, net::IpAddr};
|
||||||
env,
|
|
||||||
fs::File,
|
|
||||||
io::{Read, Write},
|
|
||||||
net::IpAddr,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::db::models::{
|
use crate::{
|
||||||
AttachmentId, CipherId, CollectionId, DeviceId, EmergencyAccessId, MembershipId, OrgApiKeyId, OrganizationId,
|
config::PathType,
|
||||||
SendFileId, SendId, UserId,
|
db::models::{
|
||||||
|
AttachmentId, CipherId, CollectionId, DeviceId, EmergencyAccessId, MembershipId, OrgApiKeyId, OrganizationId,
|
||||||
|
SendFileId, SendId, UserId,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
use crate::{error::Error, CONFIG};
|
use crate::{error::Error, CONFIG};
|
||||||
|
|
||||||
@@ -40,37 +38,33 @@ static JWT_REGISTER_VERIFY_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|regis
|
|||||||
static PRIVATE_RSA_KEY: OnceCell<EncodingKey> = OnceCell::new();
|
static PRIVATE_RSA_KEY: OnceCell<EncodingKey> = OnceCell::new();
|
||||||
static PUBLIC_RSA_KEY: OnceCell<DecodingKey> = OnceCell::new();
|
static PUBLIC_RSA_KEY: OnceCell<DecodingKey> = OnceCell::new();
|
||||||
|
|
||||||
pub fn initialize_keys() -> Result<(), Error> {
|
pub async fn initialize_keys() -> Result<(), Error> {
|
||||||
fn read_key(create_if_missing: bool) -> Result<(Rsa<openssl::pkey::Private>, Vec<u8>), Error> {
|
use std::io::Error;
|
||||||
let mut priv_key_buffer = Vec::with_capacity(2048);
|
|
||||||
|
|
||||||
let mut priv_key_file = File::options()
|
let rsa_key_filename = std::path::PathBuf::from(CONFIG.private_rsa_key())
|
||||||
.create(create_if_missing)
|
.file_name()
|
||||||
.truncate(false)
|
.ok_or_else(|| Error::other("Private RSA key path missing filename"))?
|
||||||
.read(true)
|
.to_str()
|
||||||
.write(create_if_missing)
|
.ok_or_else(|| Error::other("Private RSA key path filename is not valid UTF-8"))?
|
||||||
.open(CONFIG.private_rsa_key())?;
|
.to_string();
|
||||||
|
|
||||||
#[allow(clippy::verbose_file_reads)]
|
let operator = CONFIG.opendal_operator_for_path_type(PathType::RsaKey).map_err(Error::other)?;
|
||||||
let bytes_read = priv_key_file.read_to_end(&mut priv_key_buffer)?;
|
|
||||||
|
|
||||||
let rsa_key = if bytes_read > 0 {
|
let priv_key_buffer = match operator.read(&rsa_key_filename).await {
|
||||||
Rsa::private_key_from_pem(&priv_key_buffer[..bytes_read])?
|
Ok(buffer) => Some(buffer),
|
||||||
} else if create_if_missing {
|
Err(e) if e.kind() == opendal::ErrorKind::NotFound => None,
|
||||||
// Only create the key if the file doesn't exist or is empty
|
Err(e) => return Err(e.into()),
|
||||||
let rsa_key = Rsa::generate(2048)?;
|
};
|
||||||
priv_key_buffer = rsa_key.private_key_to_pem()?;
|
|
||||||
priv_key_file.write_all(&priv_key_buffer)?;
|
|
||||||
info!("Private key '{}' created correctly", CONFIG.private_rsa_key());
|
|
||||||
rsa_key
|
|
||||||
} else {
|
|
||||||
err!("Private key does not exist or invalid format", CONFIG.private_rsa_key());
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok((rsa_key, priv_key_buffer))
|
let (priv_key, priv_key_buffer) = if let Some(priv_key_buffer) = priv_key_buffer {
|
||||||
}
|
(Rsa::private_key_from_pem(priv_key_buffer.to_vec().as_slice())?, priv_key_buffer.to_vec())
|
||||||
|
} else {
|
||||||
let (priv_key, priv_key_buffer) = read_key(true).or_else(|_| read_key(false))?;
|
let rsa_key = Rsa::generate(2048)?;
|
||||||
|
let priv_key_buffer = rsa_key.private_key_to_pem()?;
|
||||||
|
operator.write(&rsa_key_filename, priv_key_buffer.clone()).await?;
|
||||||
|
info!("Private key '{}' created correctly", CONFIG.private_rsa_key());
|
||||||
|
(rsa_key, priv_key_buffer)
|
||||||
|
};
|
||||||
let pub_key_buffer = priv_key.public_key_to_pem()?;
|
let pub_key_buffer = priv_key.public_key_to_pem()?;
|
||||||
|
|
||||||
let enc = EncodingKey::from_rsa_pem(&priv_key_buffer)?;
|
let enc = EncodingKey::from_rsa_pem(&priv_key_buffer)?;
|
||||||
@@ -181,6 +175,11 @@ pub struct LoginJwtClaims {
|
|||||||
pub sstamp: String,
|
pub sstamp: String,
|
||||||
// device uuid
|
// device uuid
|
||||||
pub device: DeviceId,
|
pub device: DeviceId,
|
||||||
|
// what kind of device, like FirefoxBrowser or Android derived from DeviceType
|
||||||
|
pub devicetype: String,
|
||||||
|
// the type of client_id, like web, cli, desktop, browser or mobile
|
||||||
|
pub client_id: String,
|
||||||
|
|
||||||
// [ "api", "offline_access" ]
|
// [ "api", "offline_access" ]
|
||||||
pub scope: Vec<String>,
|
pub scope: Vec<String>,
|
||||||
// [ "Application" ]
|
// [ "Application" ]
|
||||||
@@ -689,17 +688,6 @@ impl<'r> FromRequest<'r> for AdminHeaders {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<AdminHeaders> for Headers {
|
|
||||||
fn from(h: AdminHeaders) -> Headers {
|
|
||||||
Headers {
|
|
||||||
host: h.host,
|
|
||||||
device: h.device,
|
|
||||||
user: h.user,
|
|
||||||
ip: h.ip,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// col_id is usually the fourth path param ("/organizations/<org_id>/collections/<col_id>"),
|
// 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.
|
// 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.
|
// First check the path, if this is not a valid uuid, try the query values.
|
||||||
@@ -869,8 +857,10 @@ impl<'r> FromRequest<'r> for OwnerHeaders {
|
|||||||
|
|
||||||
pub struct OrgMemberHeaders {
|
pub struct OrgMemberHeaders {
|
||||||
pub host: String,
|
pub host: String,
|
||||||
|
pub device: Device,
|
||||||
pub user: User,
|
pub user: User,
|
||||||
pub org_id: OrganizationId,
|
pub membership: Membership,
|
||||||
|
pub ip: ClientIp,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rocket::async_trait]
|
#[rocket::async_trait]
|
||||||
@@ -882,8 +872,10 @@ impl<'r> FromRequest<'r> for OrgMemberHeaders {
|
|||||||
if headers.is_member() {
|
if headers.is_member() {
|
||||||
Outcome::Success(Self {
|
Outcome::Success(Self {
|
||||||
host: headers.host,
|
host: headers.host,
|
||||||
|
device: headers.device,
|
||||||
user: headers.user,
|
user: headers.user,
|
||||||
org_id: headers.membership.org_uuid,
|
membership: headers.membership,
|
||||||
|
ip: headers.ip,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
err_handler!("You need to be a Member of the Organization to call this endpoint")
|
err_handler!("You need to be a Member of the Organization to call this endpoint")
|
||||||
@@ -891,6 +883,17 @@ impl<'r> FromRequest<'r> for OrgMemberHeaders {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<OrgMemberHeaders> for Headers {
|
||||||
|
fn from(h: OrgMemberHeaders) -> Headers {
|
||||||
|
Headers {
|
||||||
|
host: h.host,
|
||||||
|
device: h.device,
|
||||||
|
user: h.user,
|
||||||
|
ip: h.ip,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Client IP address detection
|
// Client IP address detection
|
||||||
//
|
//
|
||||||
|
|||||||
+165
-28
@@ -3,7 +3,7 @@ use std::{
|
|||||||
process::exit,
|
process::exit,
|
||||||
sync::{
|
sync::{
|
||||||
atomic::{AtomicBool, Ordering},
|
atomic::{AtomicBool, Ordering},
|
||||||
RwLock,
|
LazyLock, RwLock,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -22,10 +22,32 @@ static CONFIG_FILE: Lazy<String> = Lazy::new(|| {
|
|||||||
get_env("CONFIG_FILE").unwrap_or_else(|| format!("{data_folder}/config.json"))
|
get_env("CONFIG_FILE").unwrap_or_else(|| format!("{data_folder}/config.json"))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
static CONFIG_FILE_PARENT_DIR: LazyLock<String> = LazyLock::new(|| {
|
||||||
|
let path = std::path::PathBuf::from(&*CONFIG_FILE);
|
||||||
|
path.parent().unwrap_or(std::path::Path::new("data")).to_str().unwrap_or("data").to_string()
|
||||||
|
});
|
||||||
|
|
||||||
|
static CONFIG_FILENAME: LazyLock<String> = LazyLock::new(|| {
|
||||||
|
let path = std::path::PathBuf::from(&*CONFIG_FILE);
|
||||||
|
path.file_name().unwrap_or(std::ffi::OsStr::new("config.json")).to_str().unwrap_or("config.json").to_string()
|
||||||
|
});
|
||||||
|
|
||||||
pub static SKIP_CONFIG_VALIDATION: AtomicBool = AtomicBool::new(false);
|
pub static SKIP_CONFIG_VALIDATION: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
pub static CONFIG: Lazy<Config> = Lazy::new(|| {
|
pub static CONFIG: Lazy<Config> = Lazy::new(|| {
|
||||||
Config::load().unwrap_or_else(|e| {
|
std::thread::spawn(|| {
|
||||||
|
let rt = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap_or_else(|e| {
|
||||||
|
println!("Error loading config:\n {e:?}\n");
|
||||||
|
exit(12)
|
||||||
|
});
|
||||||
|
|
||||||
|
rt.block_on(Config::load()).unwrap_or_else(|e| {
|
||||||
|
println!("Error loading config:\n {e:?}\n");
|
||||||
|
exit(12)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.join()
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
println!("Error loading config:\n {e:?}\n");
|
println!("Error loading config:\n {e:?}\n");
|
||||||
exit(12)
|
exit(12)
|
||||||
})
|
})
|
||||||
@@ -110,10 +132,11 @@ macro_rules! make_config {
|
|||||||
builder
|
builder
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_file(path: &str) -> Result<Self, Error> {
|
async fn from_file() -> Result<Self, Error> {
|
||||||
let config_str = std::fs::read_to_string(path)?;
|
let operator = opendal_operator_for_path(&CONFIG_FILE_PARENT_DIR)?;
|
||||||
println!("[INFO] Using saved config from `{path}` for configuration.\n");
|
let config_bytes = operator.read(&CONFIG_FILENAME).await?;
|
||||||
serde_json::from_str(&config_str).map_err(Into::into)
|
println!("[INFO] Using saved config from `{}` for configuration.\n", *CONFIG_FILE);
|
||||||
|
serde_json::from_slice(&config_bytes.to_vec()).map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clear_non_editable(&mut self) {
|
fn clear_non_editable(&mut self) {
|
||||||
@@ -579,7 +602,7 @@ make_config! {
|
|||||||
authenticator_disable_time_drift: bool, true, def, false;
|
authenticator_disable_time_drift: bool, true, def, false;
|
||||||
|
|
||||||
/// Customize the enabled feature flags on the clients |> This is a comma separated list of feature flags to enable.
|
/// Customize the enabled feature flags on the clients |> This is a comma separated list of feature flags to enable.
|
||||||
experimental_client_feature_flags: String, false, def, "fido2-vault-credentials".to_string();
|
experimental_client_feature_flags: String, false, def, String::new();
|
||||||
|
|
||||||
/// Require new device emails |> When a user logs in an email is required to be sent.
|
/// Require new device emails |> When a user logs in an email is required to be sent.
|
||||||
/// If sending the email fails the login attempt will fail.
|
/// If sending the email fails the login attempt will fail.
|
||||||
@@ -833,21 +856,25 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: deal with deprecated flags so they can be removed from this list, cf. #4263
|
// Server (v2025.6.2): https://github.com/bitwarden/server/blob/d094be3267f2030bd0dc62106bc6871cf82682f5/src/Core/Constants.cs#L103
|
||||||
|
// Client (web-v2025.6.1): https://github.com/bitwarden/clients/blob/747c2fd6a1c348a57a76e4a7de8128466ffd3c01/libs/common/src/enums/feature-flag.enum.ts#L12
|
||||||
|
// Android (v2025.6.0): https://github.com/bitwarden/android/blob/b5b022caaad33390c31b3021b2c1205925b0e1a2/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/model/FlagKey.kt#L22
|
||||||
|
// iOS (v2025.6.0): https://github.com/bitwarden/ios/blob/ff06d9c6cc8da89f78f37f376495800201d7261a/BitwardenShared/Core/Platform/Models/Enum/FeatureFlag.swift#L7
|
||||||
|
//
|
||||||
|
// NOTE: Move deprecated flags to the utils::parse_experimental_client_feature_flags() DEPRECATED_FLAGS const!
|
||||||
const KNOWN_FLAGS: &[&str] = &[
|
const KNOWN_FLAGS: &[&str] = &[
|
||||||
"autofill-overlay",
|
// Autofill Team
|
||||||
"autofill-v2",
|
|
||||||
"browser-fileless-import",
|
|
||||||
"extension-refresh",
|
|
||||||
"fido2-vault-credentials",
|
|
||||||
"inline-menu-positioning-improvements",
|
"inline-menu-positioning-improvements",
|
||||||
"ssh-key-vault-item",
|
"inline-menu-totp",
|
||||||
"ssh-agent",
|
"ssh-agent",
|
||||||
|
// Key Management Team
|
||||||
|
"ssh-key-vault-item",
|
||||||
|
// Tools
|
||||||
|
"export-attachments",
|
||||||
|
// Mobile Team
|
||||||
"anon-addy-self-host-alias",
|
"anon-addy-self-host-alias",
|
||||||
"simple-login-self-host-alias",
|
"simple-login-self-host-alias",
|
||||||
"mutual-tls",
|
"mutual-tls",
|
||||||
"export-attachments",
|
|
||||||
"inline-menu-totp",
|
|
||||||
];
|
];
|
||||||
let configured_flags = parse_experimental_client_feature_flags(&cfg.experimental_client_feature_flags);
|
let configured_flags = parse_experimental_client_feature_flags(&cfg.experimental_client_feature_flags);
|
||||||
let invalid_flags: Vec<_> = configured_flags.keys().filter(|flag| !KNOWN_FLAGS.contains(&flag.as_str())).collect();
|
let invalid_flags: Vec<_> = configured_flags.keys().filter(|flag| !KNOWN_FLAGS.contains(&flag.as_str())).collect();
|
||||||
@@ -1134,11 +1161,103 @@ fn smtp_convert_deprecated_ssl_options(smtp_ssl: Option<bool>, smtp_explicit_tls
|
|||||||
"starttls".to_string()
|
"starttls".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn opendal_operator_for_path(path: &str) -> Result<opendal::Operator, Error> {
|
||||||
|
// Cache of previously built operators by path
|
||||||
|
static OPERATORS_BY_PATH: LazyLock<dashmap::DashMap<String, opendal::Operator>> =
|
||||||
|
LazyLock::new(dashmap::DashMap::new);
|
||||||
|
|
||||||
|
if let Some(operator) = OPERATORS_BY_PATH.get(path) {
|
||||||
|
return Ok(operator.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let operator = if path.starts_with("s3://") {
|
||||||
|
#[cfg(not(s3))]
|
||||||
|
return Err(opendal::Error::new(opendal::ErrorKind::ConfigInvalid, "S3 support is not enabled").into());
|
||||||
|
|
||||||
|
#[cfg(s3)]
|
||||||
|
opendal_s3_operator_for_path(path)?
|
||||||
|
} else {
|
||||||
|
let builder = opendal::services::Fs::default().root(path);
|
||||||
|
opendal::Operator::new(builder)?.finish()
|
||||||
|
};
|
||||||
|
|
||||||
|
OPERATORS_BY_PATH.insert(path.to_string(), operator.clone());
|
||||||
|
|
||||||
|
Ok(operator)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(s3)]
|
||||||
|
fn opendal_s3_operator_for_path(path: &str) -> Result<opendal::Operator, Error> {
|
||||||
|
use crate::http_client::aws::AwsReqwestConnector;
|
||||||
|
use aws_config::{default_provider::credentials::DefaultCredentialsChain, provider_config::ProviderConfig};
|
||||||
|
|
||||||
|
// This is a custom AWS credential loader that uses the official AWS Rust
|
||||||
|
// SDK config crate to load credentials. This ensures maximum compatibility
|
||||||
|
// with AWS credential configurations. For example, OpenDAL doesn't support
|
||||||
|
// AWS SSO temporary credentials yet.
|
||||||
|
struct OpenDALS3CredentialLoader {}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl reqsign::AwsCredentialLoad for OpenDALS3CredentialLoader {
|
||||||
|
async fn load_credential(&self, _client: reqwest::Client) -> anyhow::Result<Option<reqsign::AwsCredential>> {
|
||||||
|
use aws_credential_types::provider::ProvideCredentials as _;
|
||||||
|
use tokio::sync::OnceCell;
|
||||||
|
|
||||||
|
static DEFAULT_CREDENTIAL_CHAIN: OnceCell<DefaultCredentialsChain> = OnceCell::const_new();
|
||||||
|
|
||||||
|
let chain = DEFAULT_CREDENTIAL_CHAIN
|
||||||
|
.get_or_init(|| {
|
||||||
|
let reqwest_client = reqwest::Client::builder().build().unwrap();
|
||||||
|
let connector = AwsReqwestConnector {
|
||||||
|
client: reqwest_client,
|
||||||
|
};
|
||||||
|
|
||||||
|
let conf = ProviderConfig::default().with_http_client(connector);
|
||||||
|
|
||||||
|
DefaultCredentialsChain::builder().configure(conf).build()
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let creds = chain.provide_credentials().await?;
|
||||||
|
|
||||||
|
Ok(Some(reqsign::AwsCredential {
|
||||||
|
access_key_id: creds.access_key_id().to_string(),
|
||||||
|
secret_access_key: creds.secret_access_key().to_string(),
|
||||||
|
session_token: creds.session_token().map(|s| s.to_string()),
|
||||||
|
expires_in: creds.expiry().map(|expiration| expiration.into()),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const OPEN_DAL_S3_CREDENTIAL_LOADER: OpenDALS3CredentialLoader = OpenDALS3CredentialLoader {};
|
||||||
|
|
||||||
|
let url = Url::parse(path).map_err(|e| format!("Invalid path S3 URL path {path:?}: {e}"))?;
|
||||||
|
|
||||||
|
let bucket = url.host_str().ok_or_else(|| format!("Missing Bucket name in data folder S3 URL {path:?}"))?;
|
||||||
|
|
||||||
|
let builder = opendal::services::S3::default()
|
||||||
|
.customized_credential_load(Box::new(OPEN_DAL_S3_CREDENTIAL_LOADER))
|
||||||
|
.enable_virtual_host_style()
|
||||||
|
.bucket(bucket)
|
||||||
|
.root(url.path())
|
||||||
|
.default_storage_class("INTELLIGENT_TIERING");
|
||||||
|
|
||||||
|
Ok(opendal::Operator::new(builder)?.finish())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum PathType {
|
||||||
|
Data,
|
||||||
|
IconCache,
|
||||||
|
Attachments,
|
||||||
|
Sends,
|
||||||
|
RsaKey,
|
||||||
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
pub fn load() -> Result<Self, Error> {
|
pub async fn load() -> Result<Self, Error> {
|
||||||
// Loading from env and file
|
// Loading from env and file
|
||||||
let _env = ConfigBuilder::from_env();
|
let _env = ConfigBuilder::from_env();
|
||||||
let _usr = ConfigBuilder::from_file(&CONFIG_FILE).unwrap_or_default();
|
let _usr = ConfigBuilder::from_file().await.unwrap_or_default();
|
||||||
|
|
||||||
// Create merged config, config file overwrites env
|
// Create merged config, config file overwrites env
|
||||||
let mut _overrides = Vec::new();
|
let mut _overrides = Vec::new();
|
||||||
@@ -1162,7 +1281,7 @@ impl Config {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_config(&self, other: ConfigBuilder, ignore_non_editable: bool) -> Result<(), Error> {
|
pub async fn update_config(&self, other: ConfigBuilder, ignore_non_editable: bool) -> Result<(), Error> {
|
||||||
// Remove default values
|
// Remove default values
|
||||||
//let builder = other.remove(&self.inner.read().unwrap()._env);
|
//let builder = other.remove(&self.inner.read().unwrap()._env);
|
||||||
|
|
||||||
@@ -1194,20 +1313,19 @@ impl Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Save to file
|
//Save to file
|
||||||
use std::{fs::File, io::Write};
|
let operator = opendal_operator_for_path(&CONFIG_FILE_PARENT_DIR)?;
|
||||||
let mut file = File::create(&*CONFIG_FILE)?;
|
operator.write(&CONFIG_FILENAME, config_str).await?;
|
||||||
file.write_all(config_str.as_bytes())?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_config_partial(&self, other: ConfigBuilder) -> Result<(), Error> {
|
async fn update_config_partial(&self, other: ConfigBuilder) -> Result<(), Error> {
|
||||||
let builder = {
|
let builder = {
|
||||||
let usr = &self.inner.read().unwrap()._usr;
|
let usr = &self.inner.read().unwrap()._usr;
|
||||||
let mut _overrides = Vec::new();
|
let mut _overrides = Vec::new();
|
||||||
usr.merge(&other, false, &mut _overrides)
|
usr.merge(&other, false, &mut _overrides)
|
||||||
};
|
};
|
||||||
self.update_config(builder, false)
|
self.update_config(builder, false).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tests whether an email's domain is allowed. A domain is allowed if it
|
/// Tests whether an email's domain is allowed. A domain is allowed if it
|
||||||
@@ -1249,8 +1367,9 @@ impl Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_user_config(&self) -> Result<(), Error> {
|
pub async fn delete_user_config(&self) -> Result<(), Error> {
|
||||||
std::fs::remove_file(&*CONFIG_FILE)?;
|
let operator = opendal_operator_for_path(&CONFIG_FILE_PARENT_DIR)?;
|
||||||
|
operator.delete(&CONFIG_FILENAME).await?;
|
||||||
|
|
||||||
// Empty user config
|
// Empty user config
|
||||||
let usr = ConfigBuilder::default();
|
let usr = ConfigBuilder::default();
|
||||||
@@ -1280,7 +1399,7 @@ impl Config {
|
|||||||
inner._enable_smtp && (inner.smtp_host.is_some() || inner.use_sendmail)
|
inner._enable_smtp && (inner.smtp_host.is_some() || inner.use_sendmail)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_duo_akey(&self) -> String {
|
pub async fn get_duo_akey(&self) -> String {
|
||||||
if let Some(akey) = self._duo_akey() {
|
if let Some(akey) = self._duo_akey() {
|
||||||
akey
|
akey
|
||||||
} else {
|
} else {
|
||||||
@@ -1291,7 +1410,7 @@ impl Config {
|
|||||||
_duo_akey: Some(akey_s.clone()),
|
_duo_akey: Some(akey_s.clone()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
self.update_config_partial(builder).ok();
|
self.update_config_partial(builder).await.ok();
|
||||||
|
|
||||||
akey_s
|
akey_s
|
||||||
}
|
}
|
||||||
@@ -1304,6 +1423,23 @@ impl Config {
|
|||||||
token.is_some() && !token.unwrap().trim().is_empty()
|
token.is_some() && !token.unwrap().trim().is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn opendal_operator_for_path_type(&self, path_type: PathType) -> Result<opendal::Operator, Error> {
|
||||||
|
let path = match path_type {
|
||||||
|
PathType::Data => self.data_folder(),
|
||||||
|
PathType::IconCache => self.icon_cache_folder(),
|
||||||
|
PathType::Attachments => self.attachments_folder(),
|
||||||
|
PathType::Sends => self.sends_folder(),
|
||||||
|
PathType::RsaKey => std::path::Path::new(&self.rsa_key_filename())
|
||||||
|
.parent()
|
||||||
|
.ok_or_else(|| std::io::Error::other("Failed to get directory of RSA key file"))?
|
||||||
|
.to_str()
|
||||||
|
.ok_or_else(|| std::io::Error::other("Failed to convert RSA key file directory to UTF-8 string"))?
|
||||||
|
.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
opendal_operator_for_path(&path)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn render_template<T: serde::ser::Serialize>(&self, name: &str, data: &T) -> Result<String, Error> {
|
pub fn render_template<T: serde::ser::Serialize>(&self, name: &str, data: &T) -> Result<String, Error> {
|
||||||
if self.reload_templates() {
|
if self.reload_templates() {
|
||||||
warn!("RELOADING TEMPLATES");
|
warn!("RELOADING TEMPLATES");
|
||||||
@@ -1373,6 +1509,7 @@ where
|
|||||||
reg!("email/email_footer_text");
|
reg!("email/email_footer_text");
|
||||||
|
|
||||||
reg!("email/admin_reset_password", ".html");
|
reg!("email/admin_reset_password", ".html");
|
||||||
|
reg!("email/change_email_existing", ".html");
|
||||||
reg!("email/change_email", ".html");
|
reg!("email/change_email", ".html");
|
||||||
reg!("email/delete_account", ".html");
|
reg!("email/delete_account", ".html");
|
||||||
reg!("email/emergency_access_invite_accepted", ".html");
|
reg!("email/emergency_access_invite_accepted", ".html");
|
||||||
|
|||||||
+30
-24
@@ -1,11 +1,11 @@
|
|||||||
use std::io::ErrorKind;
|
use std::time::Duration;
|
||||||
|
|
||||||
use bigdecimal::{BigDecimal, ToPrimitive};
|
use bigdecimal::{BigDecimal, ToPrimitive};
|
||||||
use derive_more::{AsRef, Deref, Display};
|
use derive_more::{AsRef, Deref, Display};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use super::{CipherId, OrganizationId, UserId};
|
use super::{CipherId, OrganizationId, UserId};
|
||||||
use crate::CONFIG;
|
use crate::{config::PathType, CONFIG};
|
||||||
use macros::IdFromParam;
|
use macros::IdFromParam;
|
||||||
|
|
||||||
db_object! {
|
db_object! {
|
||||||
@@ -41,24 +41,30 @@ impl Attachment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_file_path(&self) -> String {
|
pub fn get_file_path(&self) -> String {
|
||||||
format!("{}/{}/{}", CONFIG.attachments_folder(), self.cipher_uuid, self.id)
|
format!("{}/{}", self.cipher_uuid, self.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_url(&self, host: &str) -> String {
|
pub async fn get_url(&self, host: &str) -> Result<String, crate::Error> {
|
||||||
let token = encode_jwt(&generate_file_download_claims(self.cipher_uuid.clone(), self.id.clone()));
|
let operator = CONFIG.opendal_operator_for_path_type(PathType::Attachments)?;
|
||||||
format!("{host}/attachments/{}/{}?token={token}", self.cipher_uuid, self.id)
|
|
||||||
|
if operator.info().scheme() == opendal::Scheme::Fs {
|
||||||
|
let token = encode_jwt(&generate_file_download_claims(self.cipher_uuid.clone(), self.id.clone()));
|
||||||
|
Ok(format!("{host}/attachments/{}/{}?token={token}", self.cipher_uuid, self.id))
|
||||||
|
} else {
|
||||||
|
Ok(operator.presign_read(&self.get_file_path(), Duration::from_secs(5 * 60)).await?.uri().to_string())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_json(&self, host: &str) -> Value {
|
pub async fn to_json(&self, host: &str) -> Result<Value, crate::Error> {
|
||||||
json!({
|
Ok(json!({
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
"url": self.get_url(host),
|
"url": self.get_url(host).await?,
|
||||||
"fileName": self.file_name,
|
"fileName": self.file_name,
|
||||||
"size": self.file_size.to_string(),
|
"size": self.file_size.to_string(),
|
||||||
"sizeName": crate::util::get_display_size(self.file_size),
|
"sizeName": crate::util::get_display_size(self.file_size),
|
||||||
"key": self.akey,
|
"key": self.akey,
|
||||||
"object": "attachment"
|
"object": "attachment"
|
||||||
})
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,26 +110,26 @@ impl Attachment {
|
|||||||
|
|
||||||
pub async fn delete(&self, conn: &mut DbConn) -> EmptyResult {
|
pub async fn delete(&self, conn: &mut DbConn) -> EmptyResult {
|
||||||
db_run! { conn: {
|
db_run! { conn: {
|
||||||
let _: () = crate::util::retry(
|
crate::util::retry(
|
||||||
|| diesel::delete(attachments::table.filter(attachments::id.eq(&self.id))).execute(conn),
|
|| diesel::delete(attachments::table.filter(attachments::id.eq(&self.id))).execute(conn),
|
||||||
10,
|
10,
|
||||||
)
|
)
|
||||||
.map_res("Error deleting attachment")?;
|
.map(|_| ())
|
||||||
|
.map_res("Error deleting attachment")
|
||||||
|
}}?;
|
||||||
|
|
||||||
let file_path = &self.get_file_path();
|
let operator = CONFIG.opendal_operator_for_path_type(PathType::Attachments)?;
|
||||||
|
let file_path = self.get_file_path();
|
||||||
|
|
||||||
match std::fs::remove_file(file_path) {
|
if let Err(e) = operator.delete(&file_path).await {
|
||||||
// Ignore "file not found" errors. This can happen when the
|
if e.kind() == opendal::ErrorKind::NotFound {
|
||||||
// upstream caller has already cleaned up the file as part of
|
debug!("File '{file_path}' already deleted.");
|
||||||
// its own error handling.
|
} else {
|
||||||
Err(e) if e.kind() == ErrorKind::NotFound => {
|
return Err(e.into());
|
||||||
debug!("File '{file_path}' already deleted.");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Err(e) => Err(e.into()),
|
|
||||||
_ => Ok(()),
|
|
||||||
}
|
}
|
||||||
}}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete_all_by_cipher(cipher_uuid: &CipherId, conn: &mut DbConn) -> EmptyResult {
|
pub async fn delete_all_by_cipher(cipher_uuid: &CipherId, conn: &mut DbConn) -> EmptyResult {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ db_object! {
|
|||||||
pub organization_uuid: Option<OrganizationId>,
|
pub organization_uuid: Option<OrganizationId>,
|
||||||
|
|
||||||
pub request_device_identifier: DeviceId,
|
pub request_device_identifier: DeviceId,
|
||||||
pub device_type: i32, // https://github.com/bitwarden/server/blob/master/src/Core/Enums/DeviceType.cs
|
pub device_type: i32, // https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/Enums/DeviceType.cs
|
||||||
|
|
||||||
pub request_ip: String,
|
pub request_ip: String,
|
||||||
pub response_device_id: Option<DeviceId>,
|
pub response_device_id: Option<DeviceId>,
|
||||||
|
|||||||
+35
-14
@@ -141,18 +141,28 @@ impl Cipher {
|
|||||||
cipher_sync_data: Option<&CipherSyncData>,
|
cipher_sync_data: Option<&CipherSyncData>,
|
||||||
sync_type: CipherSyncType,
|
sync_type: CipherSyncType,
|
||||||
conn: &mut DbConn,
|
conn: &mut DbConn,
|
||||||
) -> Value {
|
) -> Result<Value, crate::Error> {
|
||||||
use crate::util::{format_date, validate_and_format_date};
|
use crate::util::{format_date, validate_and_format_date};
|
||||||
|
|
||||||
let mut attachments_json: Value = Value::Null;
|
let mut attachments_json: Value = Value::Null;
|
||||||
if let Some(cipher_sync_data) = cipher_sync_data {
|
if let Some(cipher_sync_data) = cipher_sync_data {
|
||||||
if let Some(attachments) = cipher_sync_data.cipher_attachments.get(&self.uuid) {
|
if let Some(attachments) = cipher_sync_data.cipher_attachments.get(&self.uuid) {
|
||||||
attachments_json = attachments.iter().map(|c| c.to_json(host)).collect();
|
if !attachments.is_empty() {
|
||||||
|
let mut attachments_json_vec = vec![];
|
||||||
|
for attachment in attachments {
|
||||||
|
attachments_json_vec.push(attachment.to_json(host).await?);
|
||||||
|
}
|
||||||
|
attachments_json = Value::Array(attachments_json_vec);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let attachments = Attachment::find_by_cipher(&self.uuid, conn).await;
|
let attachments = Attachment::find_by_cipher(&self.uuid, conn).await;
|
||||||
if !attachments.is_empty() {
|
if !attachments.is_empty() {
|
||||||
attachments_json = attachments.iter().map(|c| c.to_json(host)).collect()
|
let mut attachments_json_vec = vec![];
|
||||||
|
for attachment in attachments {
|
||||||
|
attachments_json_vec.push(attachment.to_json(host).await?);
|
||||||
|
}
|
||||||
|
attachments_json = Value::Array(attachments_json_vec);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -318,7 +328,7 @@ impl Cipher {
|
|||||||
// supports the "cipherDetails" type, though it seems like the
|
// supports the "cipherDetails" type, though it seems like the
|
||||||
// Bitwarden clients will ignore extra fields.
|
// Bitwarden clients will ignore extra fields.
|
||||||
//
|
//
|
||||||
// Ref: https://github.com/bitwarden/server/blob/master/src/Core/Models/Api/Response/CipherResponseModel.cs
|
// Ref: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Vault/Models/Response/CipherResponseModel.cs#L14
|
||||||
let mut json_object = json!({
|
let mut json_object = json!({
|
||||||
"object": "cipherDetails",
|
"object": "cipherDetails",
|
||||||
"id": self.uuid,
|
"id": self.uuid,
|
||||||
@@ -372,6 +382,11 @@ impl Cipher {
|
|||||||
// the "Read Only" or "Hide Passwords" restrictions for the user.
|
// the "Read Only" or "Hide Passwords" restrictions for the user.
|
||||||
json_object["edit"] = json!(!read_only);
|
json_object["edit"] = json!(!read_only);
|
||||||
json_object["viewPassword"] = json!(!hide_passwords);
|
json_object["viewPassword"] = json!(!hide_passwords);
|
||||||
|
// The new key used by clients since v2025.6.0
|
||||||
|
json_object["permissions"] = json!({
|
||||||
|
"delete": !read_only,
|
||||||
|
"restore": !read_only,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let key = match self.atype {
|
let key = match self.atype {
|
||||||
@@ -384,7 +399,7 @@ impl Cipher {
|
|||||||
};
|
};
|
||||||
|
|
||||||
json_object[key] = type_data_json;
|
json_object[key] = type_data_json;
|
||||||
json_object
|
Ok(json_object)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update_users_revision(&self, conn: &mut DbConn) -> Vec<UserId> {
|
pub async fn update_users_revision(&self, conn: &mut DbConn) -> Vec<UserId> {
|
||||||
@@ -594,22 +609,23 @@ impl Cipher {
|
|||||||
let mut rows: Vec<(bool, 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) {
|
if let Some(collections) = cipher_sync_data.cipher_collections.get(&self.uuid) {
|
||||||
for collection in collections {
|
for collection in collections {
|
||||||
//User permissions
|
// User permissions
|
||||||
if let Some(cu) = cipher_sync_data.user_collections.get(collection) {
|
if let Some(cu) = cipher_sync_data.user_collections.get(collection) {
|
||||||
rows.push((cu.read_only, cu.hide_passwords, cu.manage));
|
rows.push((cu.read_only, cu.hide_passwords, cu.manage));
|
||||||
}
|
// Group permissions
|
||||||
|
} else if let Some(cg) = cipher_sync_data.user_collections_groups.get(collection) {
|
||||||
//Group permissions
|
|
||||||
if let Some(cg) = cipher_sync_data.user_collections_groups.get(collection) {
|
|
||||||
rows.push((cg.read_only, cg.hide_passwords, cg.manage));
|
rows.push((cg.read_only, cg.hide_passwords, cg.manage));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rows
|
rows
|
||||||
} else {
|
} else {
|
||||||
let mut access_flags = self.get_user_collections_access_flags(user_uuid, conn).await;
|
let user_permissions = self.get_user_collections_access_flags(user_uuid, conn).await;
|
||||||
access_flags.append(&mut self.get_group_collections_access_flags(user_uuid, conn).await);
|
if !user_permissions.is_empty() {
|
||||||
access_flags
|
user_permissions
|
||||||
|
} else {
|
||||||
|
self.get_group_collections_access_flags(user_uuid, conn).await
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if rows.is_empty() {
|
if rows.is_empty() {
|
||||||
@@ -618,6 +634,9 @@ impl Cipher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// A cipher can be in multiple collections with inconsistent access flags.
|
// A cipher can be in multiple collections with inconsistent access flags.
|
||||||
|
// Also, user permission overrule group permissions
|
||||||
|
// and only user permissions are returned by the code above.
|
||||||
|
//
|
||||||
// For example, a cipher could be in one collection where the user has
|
// For example, a cipher could be in one collection where the user has
|
||||||
// read-only access, but also in another collection where the user has
|
// read-only access, but also in another collection where the user has
|
||||||
// read/write access. For a flag to be in effect for a cipher, upstream
|
// read/write access. For a flag to be in effect for a cipher, upstream
|
||||||
@@ -626,13 +645,15 @@ impl Cipher {
|
|||||||
// and `hide_passwords` columns. This could ideally be done as part of the
|
// and `hide_passwords` columns. This could ideally be done as part of the
|
||||||
// query, but Diesel doesn't support a min() or bool_and() function on
|
// query, but Diesel doesn't support a min() or bool_and() function on
|
||||||
// booleans and this behavior isn't portable anyway.
|
// booleans and this behavior isn't portable anyway.
|
||||||
|
//
|
||||||
|
// The only exception is for the `manage` flag, that needs a boolean OR!
|
||||||
let mut read_only = true;
|
let mut read_only = true;
|
||||||
let mut hide_passwords = true;
|
let mut hide_passwords = true;
|
||||||
let mut manage = false;
|
let mut manage = false;
|
||||||
for (ro, hp, mn) in rows.iter() {
|
for (ro, hp, mn) in rows.iter() {
|
||||||
read_only &= ro;
|
read_only &= ro;
|
||||||
hide_passwords &= hp;
|
hide_passwords &= hp;
|
||||||
manage &= mn;
|
manage |= mn;
|
||||||
}
|
}
|
||||||
|
|
||||||
Some((read_only, hide_passwords, manage))
|
Some((read_only, hide_passwords, manage))
|
||||||
|
|||||||
@@ -97,13 +97,13 @@ impl Collection {
|
|||||||
(
|
(
|
||||||
cu.read_only,
|
cu.read_only,
|
||||||
cu.hide_passwords,
|
cu.hide_passwords,
|
||||||
cu.manage || (is_manager && !cu.read_only && !cu.hide_passwords),
|
is_manager && (cu.manage || (!cu.read_only && !cu.hide_passwords)),
|
||||||
)
|
)
|
||||||
} else if let Some(cg) = cipher_sync_data.user_collections_groups.get(&self.uuid) {
|
} else if let Some(cg) = cipher_sync_data.user_collections_groups.get(&self.uuid) {
|
||||||
(
|
(
|
||||||
cg.read_only,
|
cg.read_only,
|
||||||
cg.hide_passwords,
|
cg.hide_passwords,
|
||||||
cg.manage || (is_manager && !cg.read_only && !cg.hide_passwords),
|
is_manager && (cg.manage || (!cg.read_only && !cg.hide_passwords)),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
(false, false, false)
|
(false, false, false)
|
||||||
@@ -114,7 +114,9 @@ impl Collection {
|
|||||||
} else {
|
} else {
|
||||||
match Membership::find_confirmed_by_user_and_org(user_uuid, &self.org_uuid, conn).await {
|
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(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) if m.atype == MembershipType::Manager && self.is_manageable_by_user(user_uuid, conn).await => {
|
||||||
|
(false, false, true)
|
||||||
|
}
|
||||||
Some(m) => {
|
Some(m) => {
|
||||||
let is_manager = m.atype == MembershipType::Manager;
|
let is_manager = m.atype == MembershipType::Manager;
|
||||||
let read_only = !self.is_writable_by_user(user_uuid, conn).await;
|
let read_only = !self.is_writable_by_user(user_uuid, conn).await;
|
||||||
|
|||||||
+29
-12
@@ -3,8 +3,12 @@ use derive_more::{Display, From};
|
|||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use super::{AuthRequest, UserId};
|
use super::{AuthRequest, UserId};
|
||||||
use crate::{crypto, util::format_date, CONFIG};
|
use crate::{
|
||||||
use macros::IdFromParam;
|
crypto,
|
||||||
|
util::{format_date, get_uuid},
|
||||||
|
CONFIG,
|
||||||
|
};
|
||||||
|
use macros::{IdFromParam, UuidFromParam};
|
||||||
|
|
||||||
db_object! {
|
db_object! {
|
||||||
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
|
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
|
||||||
@@ -19,8 +23,8 @@ db_object! {
|
|||||||
pub user_uuid: UserId,
|
pub user_uuid: UserId,
|
||||||
|
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub atype: i32, // https://github.com/bitwarden/server/blob/dcc199bcce4aa2d5621f6fab80f1b49d8b143418/src/Core/Enums/DeviceType.cs
|
pub atype: i32, // https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/Enums/DeviceType.cs
|
||||||
pub push_uuid: Option<String>,
|
pub push_uuid: Option<PushId>,
|
||||||
pub push_token: Option<String>,
|
pub push_token: Option<String>,
|
||||||
|
|
||||||
pub refresh_token: String,
|
pub refresh_token: String,
|
||||||
@@ -42,7 +46,7 @@ impl Device {
|
|||||||
name,
|
name,
|
||||||
atype,
|
atype,
|
||||||
|
|
||||||
push_uuid: None,
|
push_uuid: Some(PushId(get_uuid())),
|
||||||
push_token: None,
|
push_token: None,
|
||||||
refresh_token: String::new(),
|
refresh_token: String::new(),
|
||||||
twofactor_remember: None,
|
twofactor_remember: None,
|
||||||
@@ -54,7 +58,7 @@ impl Device {
|
|||||||
"id": self.uuid,
|
"id": self.uuid,
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"type": self.atype,
|
"type": self.atype,
|
||||||
"identifier": self.push_uuid,
|
"identifier": self.uuid,
|
||||||
"creationDate": format_date(&self.created_at),
|
"creationDate": format_date(&self.created_at),
|
||||||
"isTrusted": false,
|
"isTrusted": false,
|
||||||
"object":"device"
|
"object":"device"
|
||||||
@@ -73,7 +77,12 @@ impl Device {
|
|||||||
self.twofactor_remember = None;
|
self.twofactor_remember = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn refresh_tokens(&mut self, user: &super::User, scope: Vec<String>) -> (String, i64) {
|
pub fn refresh_tokens(
|
||||||
|
&mut self,
|
||||||
|
user: &super::User,
|
||||||
|
scope: Vec<String>,
|
||||||
|
client_id: Option<String>,
|
||||||
|
) -> (String, i64) {
|
||||||
// If there is no refresh token, we create one
|
// If there is no refresh token, we create one
|
||||||
if self.refresh_token.is_empty() {
|
if self.refresh_token.is_empty() {
|
||||||
use data_encoding::BASE64URL;
|
use data_encoding::BASE64URL;
|
||||||
@@ -84,6 +93,11 @@ impl Device {
|
|||||||
let time_now = Utc::now();
|
let time_now = Utc::now();
|
||||||
self.updated_at = time_now.naive_utc();
|
self.updated_at = time_now.naive_utc();
|
||||||
|
|
||||||
|
// Generate a random push_uuid so if it doesn't already have one
|
||||||
|
if self.push_uuid.is_none() {
|
||||||
|
self.push_uuid = Some(PushId(get_uuid()));
|
||||||
|
}
|
||||||
|
|
||||||
// ---
|
// ---
|
||||||
// Disabled these keys to be added to the JWT since they could cause the JWT to get too large
|
// Disabled these keys to be added to the JWT since they could cause the JWT to get too large
|
||||||
// Also These key/value pairs are not used anywhere by either Vaultwarden or Bitwarden Clients
|
// Also These key/value pairs are not used anywhere by either Vaultwarden or Bitwarden Clients
|
||||||
@@ -121,6 +135,8 @@ impl Device {
|
|||||||
// orgmanager,
|
// orgmanager,
|
||||||
sstamp: user.security_stamp.clone(),
|
sstamp: user.security_stamp.clone(),
|
||||||
device: self.uuid.clone(),
|
device: self.uuid.clone(),
|
||||||
|
devicetype: DeviceType::from_i32(self.atype).to_string(),
|
||||||
|
client_id: client_id.unwrap_or("undefined".to_string()),
|
||||||
scope,
|
scope,
|
||||||
amr: vec!["Application".into()],
|
amr: vec!["Application".into()],
|
||||||
};
|
};
|
||||||
@@ -132,10 +148,6 @@ impl Device {
|
|||||||
matches!(DeviceType::from_i32(self.atype), DeviceType::Android | DeviceType::Ios)
|
matches!(DeviceType::from_i32(self.atype), DeviceType::Android | DeviceType::Ios)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_registered(&self) -> bool {
|
|
||||||
self.push_uuid.is_some()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_cli(&self) -> bool {
|
pub fn is_cli(&self) -> bool {
|
||||||
matches!(DeviceType::from_i32(self.atype), DeviceType::WindowsCLI | DeviceType::MacOsCLI | DeviceType::LinuxCLI)
|
matches!(DeviceType::from_i32(self.atype), DeviceType::WindowsCLI | DeviceType::MacOsCLI | DeviceType::LinuxCLI)
|
||||||
}
|
}
|
||||||
@@ -156,10 +168,12 @@ impl DeviceWithAuthRequest {
|
|||||||
"id": self.device.uuid,
|
"id": self.device.uuid,
|
||||||
"name": self.device.name,
|
"name": self.device.name,
|
||||||
"type": self.device.atype,
|
"type": self.device.atype,
|
||||||
"identifier": self.device.push_uuid,
|
"identifier": self.device.uuid,
|
||||||
"creationDate": format_date(&self.device.created_at),
|
"creationDate": format_date(&self.device.created_at),
|
||||||
"devicePendingAuthRequest": auth_request,
|
"devicePendingAuthRequest": auth_request,
|
||||||
"isTrusted": false,
|
"isTrusted": false,
|
||||||
|
"encryptedPublicKey": null,
|
||||||
|
"encryptedUserKey": null,
|
||||||
"object": "device",
|
"object": "device",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -395,3 +409,6 @@ impl DeviceType {
|
|||||||
Clone, Debug, DieselNewType, Display, From, FromForm, Hash, PartialEq, Eq, Serialize, Deserialize, IdFromParam,
|
Clone, Debug, DieselNewType, Display, From, FromForm, Hash, PartialEq, Eq, Serialize, Deserialize, IdFromParam,
|
||||||
)]
|
)]
|
||||||
pub struct DeviceId(String);
|
pub struct DeviceId(String);
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, DieselNewType, Display, From, FromForm, Serialize, Deserialize, UuidFromParam)]
|
||||||
|
pub struct PushId(pub String);
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ impl EmergencyAccess {
|
|||||||
"grantorId": grantor_user.uuid,
|
"grantorId": grantor_user.uuid,
|
||||||
"email": grantor_user.email,
|
"email": grantor_user.email,
|
||||||
"name": grantor_user.name,
|
"name": grantor_user.name,
|
||||||
|
"avatarColor": grantor_user.avatar_color,
|
||||||
"object": "emergencyAccessGrantorDetails",
|
"object": "emergencyAccessGrantorDetails",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -106,6 +107,7 @@ impl EmergencyAccess {
|
|||||||
"granteeId": grantee_user.uuid,
|
"granteeId": grantee_user.uuid,
|
||||||
"email": grantee_user.email,
|
"email": grantee_user.email,
|
||||||
"name": grantee_user.name,
|
"name": grantee_user.name,
|
||||||
|
"avatarColor": grantee_user.avatar_color,
|
||||||
"object": "emergencyAccessGranteeDetails",
|
"object": "emergencyAccessGranteeDetails",
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-10
@@ -8,9 +8,9 @@ use crate::{api::EmptyResult, db::DbConn, error::MapResult, CONFIG};
|
|||||||
// https://bitwarden.com/help/event-logs/
|
// https://bitwarden.com/help/event-logs/
|
||||||
|
|
||||||
db_object! {
|
db_object! {
|
||||||
// Upstream: https://github.com/bitwarden/server/blob/8a22c0479e987e756ce7412c48a732f9002f0a2d/src/Core/Services/Implementations/EventService.cs
|
// Upstream: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Services/Implementations/EventService.cs
|
||||||
// Upstream: https://github.com/bitwarden/server/blob/8a22c0479e987e756ce7412c48a732f9002f0a2d/src/Api/Models/Public/Response/EventResponseModel.cs
|
// Upstream: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/AdminConsole/Public/Models/Response/EventResponseModel.cs
|
||||||
// Upstream SQL: https://github.com/bitwarden/server/blob/8a22c0479e987e756ce7412c48a732f9002f0a2d/src/Sql/dbo/Tables/Event.sql
|
// Upstream SQL: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Sql/dbo/Tables/Event.sql
|
||||||
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
|
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
|
||||||
#[diesel(table_name = event)]
|
#[diesel(table_name = event)]
|
||||||
#[diesel(treat_none_as_null = true)]
|
#[diesel(treat_none_as_null = true)]
|
||||||
@@ -25,7 +25,7 @@ db_object! {
|
|||||||
pub group_uuid: Option<GroupId>,
|
pub group_uuid: Option<GroupId>,
|
||||||
pub org_user_uuid: Option<MembershipId>,
|
pub org_user_uuid: Option<MembershipId>,
|
||||||
pub act_user_uuid: Option<UserId>,
|
pub act_user_uuid: Option<UserId>,
|
||||||
// Upstream enum: https://github.com/bitwarden/server/blob/8a22c0479e987e756ce7412c48a732f9002f0a2d/src/Core/Enums/DeviceType.cs
|
// Upstream enum: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/Enums/DeviceType.cs
|
||||||
pub device_type: Option<i32>,
|
pub device_type: Option<i32>,
|
||||||
pub ip_address: Option<String>,
|
pub ip_address: Option<String>,
|
||||||
pub event_date: NaiveDateTime,
|
pub event_date: NaiveDateTime,
|
||||||
@@ -36,7 +36,7 @@ db_object! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upstream enum: https://github.com/bitwarden/server/blob/8a22c0479e987e756ce7412c48a732f9002f0a2d/src/Core/Enums/EventType.cs
|
// Upstream enum: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Enums/EventType.cs
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
pub enum EventType {
|
pub enum EventType {
|
||||||
// User
|
// User
|
||||||
@@ -72,7 +72,6 @@ pub enum EventType {
|
|||||||
CipherSoftDeleted = 1115,
|
CipherSoftDeleted = 1115,
|
||||||
CipherRestored = 1116,
|
CipherRestored = 1116,
|
||||||
CipherClientToggledCardNumberVisible = 1117,
|
CipherClientToggledCardNumberVisible = 1117,
|
||||||
CipherClientToggledTOTPSeedVisible = 1118,
|
|
||||||
|
|
||||||
// Collection
|
// Collection
|
||||||
CollectionCreated = 1300,
|
CollectionCreated = 1300,
|
||||||
@@ -88,7 +87,7 @@ pub enum EventType {
|
|||||||
OrganizationUserInvited = 1500,
|
OrganizationUserInvited = 1500,
|
||||||
OrganizationUserConfirmed = 1501,
|
OrganizationUserConfirmed = 1501,
|
||||||
OrganizationUserUpdated = 1502,
|
OrganizationUserUpdated = 1502,
|
||||||
OrganizationUserRemoved = 1503,
|
OrganizationUserRemoved = 1503, // Organization user data was deleted
|
||||||
OrganizationUserUpdatedGroups = 1504,
|
OrganizationUserUpdatedGroups = 1504,
|
||||||
// OrganizationUserUnlinkedSso = 1505, // Not supported
|
// OrganizationUserUnlinkedSso = 1505, // Not supported
|
||||||
OrganizationUserResetPasswordEnroll = 1506,
|
OrganizationUserResetPasswordEnroll = 1506,
|
||||||
@@ -100,8 +99,8 @@ pub enum EventType {
|
|||||||
OrganizationUserRestored = 1512,
|
OrganizationUserRestored = 1512,
|
||||||
OrganizationUserApprovedAuthRequest = 1513,
|
OrganizationUserApprovedAuthRequest = 1513,
|
||||||
OrganizationUserRejectedAuthRequest = 1514,
|
OrganizationUserRejectedAuthRequest = 1514,
|
||||||
OrganizationUserDeleted = 1515,
|
OrganizationUserDeleted = 1515, // Both user and organization user data were deleted
|
||||||
OrganizationUserLeft = 1516,
|
OrganizationUserLeft = 1516, // User voluntarily left the organization
|
||||||
|
|
||||||
// Organization
|
// Organization
|
||||||
OrganizationUpdated = 1600,
|
OrganizationUpdated = 1600,
|
||||||
@@ -188,7 +187,7 @@ impl Event {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Database methods
|
/// Database methods
|
||||||
/// https://github.com/bitwarden/server/blob/8a22c0479e987e756ce7412c48a732f9002f0a2d/src/Core/Services/Implementations/EventService.cs
|
/// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Services/Implementations/EventService.cs
|
||||||
impl Event {
|
impl Event {
|
||||||
pub const PAGE_SIZE: i64 = 30;
|
pub const PAGE_SIZE: i64 = 30;
|
||||||
|
|
||||||
|
|||||||
@@ -68,16 +68,11 @@ impl Group {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_json(&self) -> Value {
|
pub fn to_json(&self) -> Value {
|
||||||
use crate::util::format_date;
|
|
||||||
|
|
||||||
json!({
|
json!({
|
||||||
"id": self.uuid,
|
"id": self.uuid,
|
||||||
"organizationId": self.organizations_uuid,
|
"organizationId": self.organizations_uuid,
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"accessAll": self.access_all,
|
|
||||||
"externalId": self.external_id,
|
"externalId": self.external_id,
|
||||||
"creationDate": format_date(&self.creation_date),
|
|
||||||
"revisionDate": format_date(&self.revision_date),
|
|
||||||
"object": "group"
|
"object": "group"
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ pub use self::attachment::{Attachment, AttachmentId};
|
|||||||
pub use self::auth_request::{AuthRequest, AuthRequestId};
|
pub use self::auth_request::{AuthRequest, AuthRequestId};
|
||||||
pub use self::cipher::{Cipher, CipherId, RepromptType};
|
pub use self::cipher::{Cipher, CipherId, RepromptType};
|
||||||
pub use self::collection::{Collection, CollectionCipher, CollectionId, CollectionUser};
|
pub use self::collection::{Collection, CollectionCipher, CollectionId, CollectionUser};
|
||||||
pub use self::device::{Device, DeviceId, DeviceType};
|
pub use self::device::{Device, DeviceId, DeviceType, PushId};
|
||||||
pub use self::emergency_access::{EmergencyAccess, EmergencyAccessId, EmergencyAccessStatus, EmergencyAccessType};
|
pub use self::emergency_access::{EmergencyAccess, EmergencyAccessId, EmergencyAccessStatus, EmergencyAccessType};
|
||||||
pub use self::event::{Event, EventType};
|
pub use self::event::{Event, EventType};
|
||||||
pub use self::favorite::Favorite;
|
pub use self::favorite::Favorite;
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ db_object! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://github.com/bitwarden/server/blob/abfdf6f5cb0f1f1504dbaaaa0e04ce9cb60faf19/src/Core/AdminConsole/Enums/PolicyType.cs
|
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Enums/PolicyType.cs
|
||||||
#[derive(Copy, Clone, Eq, PartialEq, num_derive::FromPrimitive)]
|
#[derive(Copy, Clone, Eq, PartialEq, num_derive::FromPrimitive)]
|
||||||
pub enum OrgPolicyType {
|
pub enum OrgPolicyType {
|
||||||
TwoFactorAuthentication = 0,
|
TwoFactorAuthentication = 0,
|
||||||
@@ -41,7 +41,7 @@ pub enum OrgPolicyType {
|
|||||||
RemoveUnlockWithPin = 14,
|
RemoveUnlockWithPin = 14,
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://github.com/bitwarden/server/blob/5cbdee137921a19b1f722920f0fa3cd45af2ef0f/src/Core/Models/Data/Organizations/Policies/SendOptionsPolicyData.cs
|
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Models/Data/Organizations/Policies/SendOptionsPolicyData.cs#L5
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct SendOptionsPolicyData {
|
pub struct SendOptionsPolicyData {
|
||||||
@@ -49,7 +49,7 @@ pub struct SendOptionsPolicyData {
|
|||||||
pub disable_hide_email: bool,
|
pub disable_hide_email: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://github.com/bitwarden/server/blob/5cbdee137921a19b1f722920f0fa3cd45af2ef0f/src/Core/Models/Data/Organizations/Policies/ResetPasswordDataModel.cs
|
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Models/Data/Organizations/Policies/ResetPasswordDataModel.cs
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ResetPasswordDataModel {
|
pub struct ResetPasswordDataModel {
|
||||||
@@ -83,14 +83,24 @@ impl OrgPolicy {
|
|||||||
|
|
||||||
pub fn to_json(&self) -> Value {
|
pub fn to_json(&self) -> Value {
|
||||||
let data_json: Value = serde_json::from_str(&self.data).unwrap_or(Value::Null);
|
let data_json: Value = serde_json::from_str(&self.data).unwrap_or(Value::Null);
|
||||||
json!({
|
let mut policy = json!({
|
||||||
"id": self.uuid,
|
"id": self.uuid,
|
||||||
"organizationId": self.org_uuid,
|
"organizationId": self.org_uuid,
|
||||||
"type": self.atype,
|
"type": self.atype,
|
||||||
"data": data_json,
|
"data": data_json,
|
||||||
"enabled": self.enabled,
|
"enabled": self.enabled,
|
||||||
"object": "policy",
|
"object": "policy",
|
||||||
})
|
});
|
||||||
|
|
||||||
|
// Upstream adds this key/value
|
||||||
|
// Allow enabling Single Org policy when the organization has claimed domains.
|
||||||
|
// See: (https://github.com/bitwarden/server/pull/5565)
|
||||||
|
// We return the same to prevent possible issues
|
||||||
|
if self.atype == 8i32 {
|
||||||
|
policy["canToggleState"] = json!(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
policy
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,7 +211,7 @@ impl OrgPolicy {
|
|||||||
pub async fn find_accepted_and_confirmed_by_user_and_active_policy(
|
pub async fn find_accepted_and_confirmed_by_user_and_active_policy(
|
||||||
user_uuid: &UserId,
|
user_uuid: &UserId,
|
||||||
policy_type: OrgPolicyType,
|
policy_type: OrgPolicyType,
|
||||||
conn: &mut DbConn,
|
conn: &DbConn,
|
||||||
) -> Vec<Self> {
|
) -> Vec<Self> {
|
||||||
db_run! { conn: {
|
db_run! { conn: {
|
||||||
org_policies::table
|
org_policies::table
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ db_object! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://github.com/bitwarden/server/blob/b86a04cef9f1e1b82cf18e49fc94e017c641130c/src/Core/Enums/OrganizationUserStatusType.cs
|
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Enums/OrganizationUserStatusType.cs
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
pub enum MembershipStatus {
|
pub enum MembershipStatus {
|
||||||
Revoked = -1,
|
Revoked = -1,
|
||||||
@@ -177,7 +177,7 @@ impl Organization {
|
|||||||
public_key,
|
public_key,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// https://github.com/bitwarden/server/blob/13d1e74d6960cf0d042620b72d85bf583a4236f7/src/Api/Models/Response/Organizations/OrganizationResponseModel.cs
|
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs
|
||||||
pub fn to_json(&self) -> Value {
|
pub fn to_json(&self) -> Value {
|
||||||
json!({
|
json!({
|
||||||
"id": self.uuid,
|
"id": self.uuid,
|
||||||
@@ -203,7 +203,6 @@ impl Organization {
|
|||||||
"useResetPassword": CONFIG.mail_enabled(),
|
"useResetPassword": CONFIG.mail_enabled(),
|
||||||
"allowAdminAccessToAllCollectionItems": true,
|
"allowAdminAccessToAllCollectionItems": true,
|
||||||
"limitCollectionCreation": true,
|
"limitCollectionCreation": true,
|
||||||
"limitCollectionCreationDeletion": true,
|
|
||||||
"limitCollectionDeletion": true,
|
"limitCollectionDeletion": true,
|
||||||
|
|
||||||
"businessName": self.name,
|
"businessName": self.name,
|
||||||
@@ -424,7 +423,7 @@ impl Membership {
|
|||||||
"manageScim": false // Not supported (Not AGPLv3 Licensed)
|
"manageScim": false // Not supported (Not AGPLv3 Licensed)
|
||||||
});
|
});
|
||||||
|
|
||||||
// https://github.com/bitwarden/server/blob/13d1e74d6960cf0d042620b72d85bf583a4236f7/src/Api/Models/Response/ProfileOrganizationResponseModel.cs
|
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs
|
||||||
json!({
|
json!({
|
||||||
"id": self.org_uuid,
|
"id": self.org_uuid,
|
||||||
"identifier": null, // Not supported
|
"identifier": null, // Not supported
|
||||||
@@ -451,6 +450,8 @@ impl Membership {
|
|||||||
"usePasswordManager": true,
|
"usePasswordManager": true,
|
||||||
"useCustomPermissions": true,
|
"useCustomPermissions": true,
|
||||||
"useActivateAutofillPolicy": false,
|
"useActivateAutofillPolicy": false,
|
||||||
|
"useAdminSponsoredFamilies": false,
|
||||||
|
"useRiskInsights": false, // Not supported (Not AGPLv3 Licensed)
|
||||||
|
|
||||||
"organizationUserId": self.uuid,
|
"organizationUserId": self.uuid,
|
||||||
"providerId": null,
|
"providerId": null,
|
||||||
@@ -458,7 +459,6 @@ impl Membership {
|
|||||||
"providerType": null,
|
"providerType": null,
|
||||||
"familySponsorshipFriendlyName": null,
|
"familySponsorshipFriendlyName": null,
|
||||||
"familySponsorshipAvailable": false,
|
"familySponsorshipAvailable": false,
|
||||||
"planProductType": 3,
|
|
||||||
"productTierType": 3, // Enterprise tier
|
"productTierType": 3, // Enterprise tier
|
||||||
"keyConnectorEnabled": false,
|
"keyConnectorEnabled": false,
|
||||||
"keyConnectorUrl": null,
|
"keyConnectorUrl": null,
|
||||||
@@ -467,10 +467,11 @@ impl Membership {
|
|||||||
"familySponsorshipToDelete": null,
|
"familySponsorshipToDelete": null,
|
||||||
"accessSecretsManager": false,
|
"accessSecretsManager": false,
|
||||||
"limitCollectionCreation": self.atype < MembershipType::Manager, // If less then a manager return true, to limit collection creations
|
"limitCollectionCreation": self.atype < MembershipType::Manager, // If less then a manager return true, to limit collection creations
|
||||||
"limitCollectionCreationDeletion": true,
|
|
||||||
"limitCollectionDeletion": true,
|
"limitCollectionDeletion": true,
|
||||||
|
"limitItemDeletion": false,
|
||||||
"allowAdminAccessToAllCollectionItems": true,
|
"allowAdminAccessToAllCollectionItems": true,
|
||||||
"userIsManagedByOrganization": false, // Means not managed via the Members UI, like SSO
|
"userIsManagedByOrganization": false, // Means not managed via the Members UI, like SSO
|
||||||
|
"userIsClaimedByOrganization": false, // The new key instead of the obsolete userIsManagedByOrganization
|
||||||
|
|
||||||
"permissions": permissions,
|
"permissions": permissions,
|
||||||
|
|
||||||
@@ -616,6 +617,8 @@ impl Membership {
|
|||||||
"permissions": permissions,
|
"permissions": permissions,
|
||||||
|
|
||||||
"ssoBound": false, // Not supported
|
"ssoBound": false, // Not supported
|
||||||
|
"managedByOrganization": false, // This key is obsolete replaced by claimedByOrganization
|
||||||
|
"claimedByOrganization": false, // Means not managed via the Members UI, like SSO
|
||||||
"usesKeyConnector": false, // Not supported
|
"usesKeyConnector": false, // Not supported
|
||||||
"accessSecretsManager": false, // Not supported (Not AGPLv3 Licensed)
|
"accessSecretsManager": false, // Not supported (Not AGPLv3 Licensed)
|
||||||
|
|
||||||
@@ -863,6 +866,21 @@ impl Membership {
|
|||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get all users which are either owner or admin, or a manager which can manage/access all
|
||||||
|
pub async fn find_confirmed_and_manage_all_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> Vec<Self> {
|
||||||
|
db_run! { conn: {
|
||||||
|
users_organizations::table
|
||||||
|
.filter(users_organizations::org_uuid.eq(org_uuid))
|
||||||
|
.filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
|
||||||
|
.filter(
|
||||||
|
users_organizations::atype.eq_any(vec![MembershipType::Owner as i32, MembershipType::Admin as i32])
|
||||||
|
.or(users_organizations::atype.eq(MembershipType::Manager as i32).and(users_organizations::access_all.eq(true)))
|
||||||
|
)
|
||||||
|
.load::<MembershipDb>(conn)
|
||||||
|
.unwrap_or_default().from_db()
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn count_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> i64 {
|
pub async fn count_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> i64 {
|
||||||
db_run! { conn: {
|
db_run! { conn: {
|
||||||
users_organizations::table
|
users_organizations::table
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use chrono::{NaiveDateTime, Utc};
|
use chrono::{NaiveDateTime, Utc};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::util::LowerCase;
|
use crate::{config::PathType, util::LowerCase, CONFIG};
|
||||||
|
|
||||||
use super::{OrganizationId, User, UserId};
|
use super::{OrganizationId, User, UserId};
|
||||||
use id::SendId;
|
use id::SendId;
|
||||||
@@ -226,7 +226,8 @@ impl Send {
|
|||||||
self.update_users_revision(conn).await;
|
self.update_users_revision(conn).await;
|
||||||
|
|
||||||
if self.atype == SendType::File as i32 {
|
if self.atype == SendType::File as i32 {
|
||||||
std::fs::remove_dir_all(std::path::Path::new(&crate::CONFIG.sends_folder()).join(&self.uuid)).ok();
|
let operator = CONFIG.opendal_operator_for_path_type(PathType::Sends)?;
|
||||||
|
operator.remove_all(&self.uuid).await.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
db_run! { conn: {
|
db_run! { conn: {
|
||||||
|
|||||||
@@ -249,7 +249,6 @@ impl User {
|
|||||||
"emailVerified": !CONFIG.mail_enabled() || self.verified_at.is_some(),
|
"emailVerified": !CONFIG.mail_enabled() || self.verified_at.is_some(),
|
||||||
"premium": true,
|
"premium": true,
|
||||||
"premiumFromOrganization": false,
|
"premiumFromOrganization": false,
|
||||||
"masterPasswordHint": self.password_hint,
|
|
||||||
"culture": "en-US",
|
"culture": "en-US",
|
||||||
"twoFactorEnabled": twofactor_enabled,
|
"twoFactorEnabled": twofactor_enabled,
|
||||||
"key": self.akey,
|
"key": self.akey,
|
||||||
|
|||||||
+29
-2
@@ -46,6 +46,7 @@ use jsonwebtoken::errors::Error as JwtErr;
|
|||||||
use lettre::address::AddressError as AddrErr;
|
use lettre::address::AddressError as AddrErr;
|
||||||
use lettre::error::Error as LettreErr;
|
use lettre::error::Error as LettreErr;
|
||||||
use lettre::transport::smtp::Error as SmtpErr;
|
use lettre::transport::smtp::Error as SmtpErr;
|
||||||
|
use opendal::Error as OpenDALErr;
|
||||||
use openssl::error::ErrorStack as SSLErr;
|
use openssl::error::ErrorStack as SSLErr;
|
||||||
use regex::Error as RegexErr;
|
use regex::Error as RegexErr;
|
||||||
use reqwest::Error as ReqErr;
|
use reqwest::Error as ReqErr;
|
||||||
@@ -59,6 +60,8 @@ use yubico::yubicoerror::YubicoError as YubiErr;
|
|||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub struct Empty {}
|
pub struct Empty {}
|
||||||
|
|
||||||
|
pub struct Compact {}
|
||||||
|
|
||||||
// Error struct
|
// Error struct
|
||||||
// Contains a String error message, meant for the user and an enum variant, with an error of different types.
|
// Contains a String error message, meant for the user and an enum variant, with an error of different types.
|
||||||
//
|
//
|
||||||
@@ -69,6 +72,7 @@ make_error! {
|
|||||||
Empty(Empty): _no_source, _serialize,
|
Empty(Empty): _no_source, _serialize,
|
||||||
// Used to represent err! calls
|
// Used to represent err! calls
|
||||||
Simple(String): _no_source, _api_error,
|
Simple(String): _no_source, _api_error,
|
||||||
|
Compact(Compact): _no_source, _api_error_small,
|
||||||
|
|
||||||
// Used in our custom http client to handle non-global IPs and blocked domains
|
// Used in our custom http client to handle non-global IPs and blocked domains
|
||||||
CustomHttpClient(CustomHttpClientError): _has_source, _api_error,
|
CustomHttpClient(CustomHttpClientError): _has_source, _api_error,
|
||||||
@@ -95,6 +99,8 @@ make_error! {
|
|||||||
|
|
||||||
DieselCon(DieselConErr): _has_source, _api_error,
|
DieselCon(DieselConErr): _has_source, _api_error,
|
||||||
Webauthn(WebauthnErr): _has_source, _api_error,
|
Webauthn(WebauthnErr): _has_source, _api_error,
|
||||||
|
|
||||||
|
OpenDAL(OpenDALErr): _has_source, _api_error,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for Error {
|
impl std::fmt::Debug for Error {
|
||||||
@@ -132,6 +138,12 @@ impl Error {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn with_kind(mut self, kind: ErrorKind) -> Self {
|
||||||
|
self.error = kind;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn with_code(mut self, code: u16) -> Self {
|
pub const fn with_code(mut self, code: u16) -> Self {
|
||||||
self.error_code = code;
|
self.error_code = code;
|
||||||
@@ -200,6 +212,18 @@ fn _api_error(_: &impl std::any::Any, msg: &str) -> String {
|
|||||||
_serialize(&json, "")
|
_serialize(&json, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn _api_error_small(_: &impl std::any::Any, msg: &str) -> String {
|
||||||
|
let json = json!({
|
||||||
|
"message": msg,
|
||||||
|
"validationErrors": null,
|
||||||
|
"exceptionMessage": null,
|
||||||
|
"exceptionStackTrace": null,
|
||||||
|
"innerExceptionMessage": null,
|
||||||
|
"object": "error"
|
||||||
|
});
|
||||||
|
_serialize(&json, "")
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Rocket responder impl
|
// Rocket responder impl
|
||||||
//
|
//
|
||||||
@@ -212,8 +236,7 @@ use rocket::response::{self, Responder, Response};
|
|||||||
impl Responder<'_, 'static> for Error {
|
impl Responder<'_, 'static> for Error {
|
||||||
fn respond_to(self, _: &Request<'_>) -> response::Result<'static> {
|
fn respond_to(self, _: &Request<'_>) -> response::Result<'static> {
|
||||||
match self.error {
|
match self.error {
|
||||||
ErrorKind::Empty(_) => {} // Don't print the error in this situation
|
ErrorKind::Empty(_) | ErrorKind::Simple(_) | ErrorKind::Compact(_) => {} // 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:#?}"),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -228,6 +251,10 @@ impl Responder<'_, 'static> for Error {
|
|||||||
//
|
//
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! err {
|
macro_rules! err {
|
||||||
|
($kind:ident, $msg:expr) => {{
|
||||||
|
error!("{}", $msg);
|
||||||
|
return Err($crate::error::Error::new($msg, $msg).with_kind($crate::error::ErrorKind::$kind($crate::error::$kind {})));
|
||||||
|
}};
|
||||||
($msg:expr) => {{
|
($msg:expr) => {{
|
||||||
error!("{}", $msg);
|
error!("{}", $msg);
|
||||||
return Err($crate::error::Error::new($msg, $msg));
|
return Err($crate::error::Error::new($msg, $msg));
|
||||||
|
|||||||
@@ -244,3 +244,61 @@ impl Resolve for CustomDnsResolver {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(s3)]
|
||||||
|
pub(crate) mod aws {
|
||||||
|
use aws_smithy_runtime_api::client::{
|
||||||
|
http::{HttpClient, HttpConnector, HttpConnectorFuture, HttpConnectorSettings, SharedHttpConnector},
|
||||||
|
orchestrator::HttpResponse,
|
||||||
|
result::ConnectorError,
|
||||||
|
runtime_components::RuntimeComponents,
|
||||||
|
};
|
||||||
|
use reqwest::Client;
|
||||||
|
|
||||||
|
// Adapter that wraps reqwest to be compatible with the AWS SDK
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct AwsReqwestConnector {
|
||||||
|
pub(crate) client: Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HttpConnector for AwsReqwestConnector {
|
||||||
|
fn call(&self, request: aws_smithy_runtime_api::client::orchestrator::HttpRequest) -> HttpConnectorFuture {
|
||||||
|
// Convert the AWS-style request to a reqwest request
|
||||||
|
let client = self.client.clone();
|
||||||
|
let future = async move {
|
||||||
|
let method = reqwest::Method::from_bytes(request.method().as_bytes())
|
||||||
|
.map_err(|e| ConnectorError::user(Box::new(e)))?;
|
||||||
|
let mut req_builder = client.request(method, request.uri().to_string());
|
||||||
|
|
||||||
|
for (name, value) in request.headers() {
|
||||||
|
req_builder = req_builder.header(name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(body_bytes) = request.body().bytes() {
|
||||||
|
req_builder = req_builder.body(body_bytes.to_vec());
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = req_builder.send().await.map_err(|e| ConnectorError::io(Box::new(e)))?;
|
||||||
|
|
||||||
|
let status = response.status().into();
|
||||||
|
let bytes = response.bytes().await.map_err(|e| ConnectorError::io(Box::new(e)))?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::new(status, bytes.into()))
|
||||||
|
};
|
||||||
|
|
||||||
|
HttpConnectorFuture::new(Box::pin(future))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HttpClient for AwsReqwestConnector {
|
||||||
|
fn http_connector(
|
||||||
|
&self,
|
||||||
|
_settings: &HttpConnectorSettings,
|
||||||
|
_components: &RuntimeComponents,
|
||||||
|
) -> SharedHttpConnector {
|
||||||
|
SharedHttpConnector::new(AwsReqwestConnector {
|
||||||
|
client: self.client.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+14
@@ -570,6 +570,20 @@ pub async fn send_change_email(address: &str, token: &str) -> EmptyResult {
|
|||||||
send_email(address, &subject, body_html, body_text).await
|
send_email(address, &subject, body_html, body_text).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn send_change_email_existing(address: &str, acting_address: &str) -> EmptyResult {
|
||||||
|
let (subject, body_html, body_text) = get_text(
|
||||||
|
"email/change_email_existing",
|
||||||
|
json!({
|
||||||
|
"url": CONFIG.domain(),
|
||||||
|
"img_src": CONFIG._smtp_img_src(),
|
||||||
|
"existing_address": address,
|
||||||
|
"acting_address": acting_address,
|
||||||
|
}),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
send_email(address, &subject, body_html, body_text).await
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn send_test(address: &str) -> EmptyResult {
|
pub async fn send_test(address: &str) -> EmptyResult {
|
||||||
let (subject, body_html, body_text) = get_text(
|
let (subject, body_html, body_text) = get_text(
|
||||||
"email/smtp_test",
|
"email/smtp_test",
|
||||||
|
|||||||
+20
-5
@@ -61,7 +61,7 @@ mod util;
|
|||||||
use crate::api::core::two_factor::duo_oidc::purge_duo_contexts;
|
use crate::api::core::two_factor::duo_oidc::purge_duo_contexts;
|
||||||
use crate::api::purge_auth_requests;
|
use crate::api::purge_auth_requests;
|
||||||
use crate::api::{WS_ANONYMOUS_SUBSCRIPTIONS, WS_USERS};
|
use crate::api::{WS_ANONYMOUS_SUBSCRIPTIONS, WS_USERS};
|
||||||
pub use config::CONFIG;
|
pub use config::{PathType, CONFIG};
|
||||||
pub use error::{Error, MapResult};
|
pub use error::{Error, MapResult};
|
||||||
use rocket::data::{Limits, ToByteUnit};
|
use rocket::data::{Limits, ToByteUnit};
|
||||||
use std::sync::{atomic::Ordering, Arc};
|
use std::sync::{atomic::Ordering, Arc};
|
||||||
@@ -75,16 +75,13 @@ async fn main() -> Result<(), Error> {
|
|||||||
let level = init_logging()?;
|
let level = init_logging()?;
|
||||||
|
|
||||||
check_data_folder().await;
|
check_data_folder().await;
|
||||||
auth::initialize_keys().unwrap_or_else(|e| {
|
auth::initialize_keys().await.unwrap_or_else(|e| {
|
||||||
error!("Error creating private key '{}'\n{e:?}\nExiting Vaultwarden!", CONFIG.private_rsa_key());
|
error!("Error creating private key '{}'\n{e:?}\nExiting Vaultwarden!", CONFIG.private_rsa_key());
|
||||||
exit(1);
|
exit(1);
|
||||||
});
|
});
|
||||||
check_web_vault();
|
check_web_vault();
|
||||||
|
|
||||||
create_dir(&CONFIG.icon_cache_folder(), "icon cache");
|
|
||||||
create_dir(&CONFIG.tmp_folder(), "tmp folder");
|
create_dir(&CONFIG.tmp_folder(), "tmp folder");
|
||||||
create_dir(&CONFIG.sends_folder(), "sends folder");
|
|
||||||
create_dir(&CONFIG.attachments_folder(), "attachments folder");
|
|
||||||
|
|
||||||
let pool = create_db_pool().await;
|
let pool = create_db_pool().await;
|
||||||
schedule_jobs(pool.clone());
|
schedule_jobs(pool.clone());
|
||||||
@@ -464,6 +461,24 @@ fn create_dir(path: &str, description: &str) {
|
|||||||
|
|
||||||
async fn check_data_folder() {
|
async fn check_data_folder() {
|
||||||
let data_folder = &CONFIG.data_folder();
|
let data_folder = &CONFIG.data_folder();
|
||||||
|
|
||||||
|
if data_folder.starts_with("s3://") {
|
||||||
|
if let Err(e) = CONFIG
|
||||||
|
.opendal_operator_for_path_type(PathType::Data)
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
error!("Failed to create S3 operator for data folder '{data_folder}': {e:?}");
|
||||||
|
exit(1);
|
||||||
|
})
|
||||||
|
.check()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
error!("Could not access S3 data folder '{data_folder}': {e:?}");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let path = Path::new(data_folder);
|
let path = Path::new(data_folder);
|
||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
error!("Data folder '{data_folder}' doesn't exist.");
|
error!("Data folder '{data_folder}' doesn't exist.");
|
||||||
|
|||||||
Vendored
+6
-2
@@ -38,8 +38,8 @@ img {
|
|||||||
max-width: 130px;
|
max-width: 130px;
|
||||||
}
|
}
|
||||||
#users-table .vw-actions, #orgs-table .vw-actions {
|
#users-table .vw-actions, #orgs-table .vw-actions {
|
||||||
min-width: 135px;
|
min-width: 155px;
|
||||||
max-width: 140px;
|
max-width: 160px;
|
||||||
}
|
}
|
||||||
#users-table .vw-org-cell {
|
#users-table .vw-org-cell {
|
||||||
max-height: 120px;
|
max-height: 120px;
|
||||||
@@ -54,3 +54,7 @@ img {
|
|||||||
.vw-copy-toast {
|
.vw-copy-toast {
|
||||||
width: 15rem;
|
width: 15rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.abbr-badge {
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
|
|||||||
+13
-10
@@ -29,7 +29,7 @@ function isValidIp(ip) {
|
|||||||
return ipv4Regex.test(ip) || ipv6Regex.test(ip);
|
return ipv4Regex.test(ip) || ipv6Regex.test(ip);
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkVersions(platform, installed, latest, commit=null) {
|
function checkVersions(platform, installed, latest, commit=null, pre_release=false) {
|
||||||
if (installed === "-" || latest === "-") {
|
if (installed === "-" || latest === "-") {
|
||||||
document.getElementById(`${platform}-failed`).classList.remove("d-none");
|
document.getElementById(`${platform}-failed`).classList.remove("d-none");
|
||||||
return;
|
return;
|
||||||
@@ -37,10 +37,12 @@ function checkVersions(platform, installed, latest, commit=null) {
|
|||||||
|
|
||||||
// Only check basic versions, no commit revisions
|
// Only check basic versions, no commit revisions
|
||||||
if (commit === null || installed.indexOf("-") === -1) {
|
if (commit === null || installed.indexOf("-") === -1) {
|
||||||
if (installed !== latest) {
|
if (platform === "web" && pre_release === true) {
|
||||||
document.getElementById(`${platform}-warning`).classList.remove("d-none");
|
document.getElementById(`${platform}-prerelease`).classList.remove("d-none");
|
||||||
} else {
|
} else if (installed == latest) {
|
||||||
document.getElementById(`${platform}-success`).classList.remove("d-none");
|
document.getElementById(`${platform}-success`).classList.remove("d-none");
|
||||||
|
} else {
|
||||||
|
document.getElementById(`${platform}-warning`).classList.remove("d-none");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Check if this is a branched version.
|
// Check if this is a branched version.
|
||||||
@@ -86,7 +88,7 @@ async function generateSupportString(event, dj) {
|
|||||||
supportString += `* Running within a container: ${dj.running_within_container} (Base: ${dj.container_base_image})\n`;
|
supportString += `* Running within a container: ${dj.running_within_container} (Base: ${dj.container_base_image})\n`;
|
||||||
supportString += `* Database type: ${dj.db_type}\n`;
|
supportString += `* Database type: ${dj.db_type}\n`;
|
||||||
supportString += `* Database version: ${dj.db_version}\n`;
|
supportString += `* Database version: ${dj.db_version}\n`;
|
||||||
supportString += `* Environment settings overridden!: ${dj.overrides !== ""}\n`;
|
supportString += `* Uses config.json: ${dj.overrides !== ""}\n`;
|
||||||
supportString += `* Uses a reverse proxy: ${dj.ip_header_exists}\n`;
|
supportString += `* Uses a reverse proxy: ${dj.ip_header_exists}\n`;
|
||||||
if (dj.ip_header_exists) {
|
if (dj.ip_header_exists) {
|
||||||
supportString += `* IP Header check: ${dj.ip_header_match} (${dj.ip_header_name})\n`;
|
supportString += `* IP Header check: ${dj.ip_header_match} (${dj.ip_header_name})\n`;
|
||||||
@@ -94,6 +96,9 @@ async function generateSupportString(event, dj) {
|
|||||||
supportString += `* Internet access: ${dj.has_http_access}\n`;
|
supportString += `* Internet access: ${dj.has_http_access}\n`;
|
||||||
supportString += `* Internet access via a proxy: ${dj.uses_proxy}\n`;
|
supportString += `* Internet access via a proxy: ${dj.uses_proxy}\n`;
|
||||||
supportString += `* DNS Check: ${dnsCheck}\n`;
|
supportString += `* DNS Check: ${dnsCheck}\n`;
|
||||||
|
if (dj.tz_env !== "") {
|
||||||
|
supportString += `* TZ environment: ${dj.tz_env}\n`;
|
||||||
|
}
|
||||||
supportString += `* Browser/Server Time Check: ${timeCheck}\n`;
|
supportString += `* Browser/Server Time Check: ${timeCheck}\n`;
|
||||||
supportString += `* Server/NTP Time Check: ${ntpTimeCheck}\n`;
|
supportString += `* Server/NTP Time Check: ${ntpTimeCheck}\n`;
|
||||||
supportString += `* Domain Configuration Check: ${domainCheck}\n`;
|
supportString += `* Domain Configuration Check: ${domainCheck}\n`;
|
||||||
@@ -203,11 +208,9 @@ function initVersionCheck(dj) {
|
|||||||
}
|
}
|
||||||
checkVersions("server", serverInstalled, serverLatest, serverLatestCommit);
|
checkVersions("server", serverInstalled, serverLatest, serverLatestCommit);
|
||||||
|
|
||||||
if (!dj.running_within_container) {
|
const webInstalled = dj.web_vault_version;
|
||||||
const webInstalled = dj.web_vault_version;
|
const webLatest = dj.latest_web_build;
|
||||||
const webLatest = dj.latest_web_build;
|
checkVersions("web", webInstalled, webLatest, null, dj.web_vault_pre_release);
|
||||||
checkVersions("web", webInstalled, webLatest);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkDns(dns_resolved) {
|
function checkDns(dns_resolved) {
|
||||||
|
|||||||
+10
-6
@@ -1,5 +1,5 @@
|
|||||||
/*!
|
/*!
|
||||||
* Bootstrap v5.3.4 (https://getbootstrap.com/)
|
* Bootstrap v5.3.7 (https://getbootstrap.com/)
|
||||||
* Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
|
* Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
|
||||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||||
*/
|
*/
|
||||||
@@ -647,7 +647,7 @@
|
|||||||
* Constants
|
* Constants
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const VERSION = '5.3.4';
|
const VERSION = '5.3.7';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class definition
|
* Class definition
|
||||||
@@ -673,6 +673,8 @@
|
|||||||
this[propertyName] = null;
|
this[propertyName] = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Private
|
||||||
_queueCallback(callback, element, isAnimated = true) {
|
_queueCallback(callback, element, isAnimated = true) {
|
||||||
executeAfterTransition(callback, element, isAnimated);
|
executeAfterTransition(callback, element, isAnimated);
|
||||||
}
|
}
|
||||||
@@ -1604,11 +1606,11 @@
|
|||||||
this._element.style[dimension] = '';
|
this._element.style[dimension] = '';
|
||||||
this._queueCallback(complete, this._element, true);
|
this._queueCallback(complete, this._element, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Private
|
||||||
_isShown(element = this._element) {
|
_isShown(element = this._element) {
|
||||||
return element.classList.contains(CLASS_NAME_SHOW$7);
|
return element.classList.contains(CLASS_NAME_SHOW$7);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Private
|
|
||||||
_configAfterMerge(config) {
|
_configAfterMerge(config) {
|
||||||
config.toggle = Boolean(config.toggle); // Coerce string values
|
config.toggle = Boolean(config.toggle); // Coerce string values
|
||||||
config.parent = getElement(config.parent);
|
config.parent = getElement(config.parent);
|
||||||
@@ -3688,6 +3690,9 @@
|
|||||||
this._element.setAttribute('aria-expanded', 'false');
|
this._element.setAttribute('aria-expanded', 'false');
|
||||||
Manipulator.removeDataAttribute(this._menu, 'popper');
|
Manipulator.removeDataAttribute(this._menu, 'popper');
|
||||||
EventHandler.trigger(this._element, EVENT_HIDDEN$5, relatedTarget);
|
EventHandler.trigger(this._element, EVENT_HIDDEN$5, relatedTarget);
|
||||||
|
|
||||||
|
// Explicitly return focus to the trigger element
|
||||||
|
this._element.focus();
|
||||||
}
|
}
|
||||||
_getConfig(config) {
|
_getConfig(config) {
|
||||||
config = super._getConfig(config);
|
config = super._getConfig(config);
|
||||||
@@ -4800,7 +4805,6 @@
|
|||||||
*
|
*
|
||||||
* Shout-out to Angular https://github.com/angular/angular/blob/15.2.8/packages/core/src/sanitization/url_sanitizer.ts#L38
|
* Shout-out to Angular https://github.com/angular/angular/blob/15.2.8/packages/core/src/sanitization/url_sanitizer.ts#L38
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line unicorn/better-regex
|
|
||||||
const SAFE_URL_PATTERN = /^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i;
|
const SAFE_URL_PATTERN = /^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i;
|
||||||
const allowedAttribute = (attribute, allowedAttributeList) => {
|
const allowedAttribute = (attribute, allowedAttributeList) => {
|
||||||
const attributeName = attribute.nodeName.toLowerCase();
|
const attributeName = attribute.nodeName.toLowerCase();
|
||||||
@@ -5344,6 +5348,7 @@
|
|||||||
if (trigger === 'click') {
|
if (trigger === 'click') {
|
||||||
EventHandler.on(this._element, this.constructor.eventName(EVENT_CLICK$1), this._config.selector, event => {
|
EventHandler.on(this._element, this.constructor.eventName(EVENT_CLICK$1), this._config.selector, event => {
|
||||||
const context = this._initializeOnDelegatedTarget(event);
|
const context = this._initializeOnDelegatedTarget(event);
|
||||||
|
context._activeTrigger[TRIGGER_CLICK] = !(context._isShown() && context._activeTrigger[TRIGGER_CLICK]);
|
||||||
context.toggle();
|
context.toggle();
|
||||||
});
|
});
|
||||||
} else if (trigger !== TRIGGER_MANUAL) {
|
} else if (trigger !== TRIGGER_MANUAL) {
|
||||||
@@ -6209,7 +6214,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Private
|
// Private
|
||||||
|
|
||||||
_maybeScheduleHide() {
|
_maybeScheduleHide() {
|
||||||
if (!this._config.autohide) {
|
if (!this._config.autohide) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
Vendored
+13
-32
@@ -1,6 +1,6 @@
|
|||||||
@charset "UTF-8";
|
@charset "UTF-8";
|
||||||
/*!
|
/*!
|
||||||
* Bootstrap v5.3.4 (https://getbootstrap.com/)
|
* Bootstrap v5.3.7 (https://getbootstrap.com/)
|
||||||
* Copyright 2011-2025 The Bootstrap Authors
|
* Copyright 2011-2025 The Bootstrap Authors
|
||||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||||
*/
|
*/
|
||||||
@@ -2156,10 +2156,6 @@ progress {
|
|||||||
display: block;
|
display: block;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
.form-control::-moz-placeholder {
|
|
||||||
color: var(--bs-secondary-color);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
.form-control::placeholder {
|
.form-control::placeholder {
|
||||||
color: var(--bs-secondary-color);
|
color: var(--bs-secondary-color);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
@@ -2629,17 +2625,10 @@ textarea.form-control-lg {
|
|||||||
.form-floating > .form-control-plaintext {
|
.form-floating > .form-control-plaintext {
|
||||||
padding: 1rem 0.75rem;
|
padding: 1rem 0.75rem;
|
||||||
}
|
}
|
||||||
.form-floating > .form-control::-moz-placeholder, .form-floating > .form-control-plaintext::-moz-placeholder {
|
|
||||||
color: transparent;
|
|
||||||
}
|
|
||||||
.form-floating > .form-control::placeholder,
|
.form-floating > .form-control::placeholder,
|
||||||
.form-floating > .form-control-plaintext::placeholder {
|
.form-floating > .form-control-plaintext::placeholder {
|
||||||
color: transparent;
|
color: transparent;
|
||||||
}
|
}
|
||||||
.form-floating > .form-control:not(:-moz-placeholder), .form-floating > .form-control-plaintext:not(:-moz-placeholder) {
|
|
||||||
padding-top: 1.625rem;
|
|
||||||
padding-bottom: 0.625rem;
|
|
||||||
}
|
|
||||||
.form-floating > .form-control:focus, .form-floating > .form-control:not(:placeholder-shown),
|
.form-floating > .form-control:focus, .form-floating > .form-control:not(:placeholder-shown),
|
||||||
.form-floating > .form-control-plaintext:focus,
|
.form-floating > .form-control-plaintext:focus,
|
||||||
.form-floating > .form-control-plaintext:not(:placeholder-shown) {
|
.form-floating > .form-control-plaintext:not(:placeholder-shown) {
|
||||||
@@ -2656,9 +2645,6 @@ textarea.form-control-lg {
|
|||||||
padding-bottom: 0.625rem;
|
padding-bottom: 0.625rem;
|
||||||
padding-left: 0.75rem;
|
padding-left: 0.75rem;
|
||||||
}
|
}
|
||||||
.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:focus ~ label,
|
||||||
.form-floating > .form-control:not(:placeholder-shown) ~ label,
|
.form-floating > .form-control:not(:placeholder-shown) ~ label,
|
||||||
.form-floating > .form-control-plaintext ~ label,
|
.form-floating > .form-control-plaintext ~ label,
|
||||||
@@ -2668,15 +2654,6 @@ textarea.form-control-lg {
|
|||||||
.form-floating > .form-control:-webkit-autofill ~ label {
|
.form-floating > .form-control:-webkit-autofill ~ label {
|
||||||
transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem);
|
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:focus ~ label::after,
|
||||||
.form-floating > textarea:not(:placeholder-shown) ~ label::after {
|
.form-floating > textarea:not(:placeholder-shown) ~ label::after {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -4540,24 +4517,24 @@ textarea.form-control-lg {
|
|||||||
border-top-right-radius: 0;
|
border-top-right-radius: 0;
|
||||||
border-bottom-right-radius: 0;
|
border-bottom-right-radius: 0;
|
||||||
}
|
}
|
||||||
.card-group > .card:not(:last-child) .card-img-top,
|
.card-group > .card:not(:last-child) > .card-img-top,
|
||||||
.card-group > .card:not(:last-child) .card-header {
|
.card-group > .card:not(:last-child) > .card-header {
|
||||||
border-top-right-radius: 0;
|
border-top-right-radius: 0;
|
||||||
}
|
}
|
||||||
.card-group > .card:not(:last-child) .card-img-bottom,
|
.card-group > .card:not(:last-child) > .card-img-bottom,
|
||||||
.card-group > .card:not(:last-child) .card-footer {
|
.card-group > .card:not(:last-child) > .card-footer {
|
||||||
border-bottom-right-radius: 0;
|
border-bottom-right-radius: 0;
|
||||||
}
|
}
|
||||||
.card-group > .card:not(:first-child) {
|
.card-group > .card:not(:first-child) {
|
||||||
border-top-left-radius: 0;
|
border-top-left-radius: 0;
|
||||||
border-bottom-left-radius: 0;
|
border-bottom-left-radius: 0;
|
||||||
}
|
}
|
||||||
.card-group > .card:not(:first-child) .card-img-top,
|
.card-group > .card:not(:first-child) > .card-img-top,
|
||||||
.card-group > .card:not(:first-child) .card-header {
|
.card-group > .card:not(:first-child) > .card-header {
|
||||||
border-top-left-radius: 0;
|
border-top-left-radius: 0;
|
||||||
}
|
}
|
||||||
.card-group > .card:not(:first-child) .card-img-bottom,
|
.card-group > .card:not(:first-child) > .card-img-bottom,
|
||||||
.card-group > .card:not(:first-child) .card-footer {
|
.card-group > .card:not(:first-child) > .card-footer {
|
||||||
border-bottom-left-radius: 0;
|
border-bottom-left-radius: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7179,6 +7156,10 @@ textarea.form-control-lg {
|
|||||||
.visually-hidden-focusable:not(:focus):not(:focus-within):not(caption) {
|
.visually-hidden-focusable:not(:focus):not(:focus-within):not(caption) {
|
||||||
position: absolute !important;
|
position: absolute !important;
|
||||||
}
|
}
|
||||||
|
.visually-hidden *,
|
||||||
|
.visually-hidden-focusable:not(:focus):not(:focus-within) * {
|
||||||
|
overflow: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
.stretched-link::after {
|
.stretched-link::after {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
Vendored
+113
-19
@@ -4,10 +4,10 @@
|
|||||||
*
|
*
|
||||||
* To rebuild or modify this file with the latest versions of the included
|
* To rebuild or modify this file with the latest versions of the included
|
||||||
* software please visit:
|
* software please visit:
|
||||||
* https://datatables.net/download/#bs5/dt-2.2.2
|
* https://datatables.net/download/#bs5/dt-2.3.2
|
||||||
*
|
*
|
||||||
* Included libraries:
|
* Included libraries:
|
||||||
* DataTables 2.2.2
|
* DataTables 2.3.2
|
||||||
*/
|
*/
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
@@ -17,17 +17,18 @@
|
|||||||
--dt-row-stripe: 0, 0, 0;
|
--dt-row-stripe: 0, 0, 0;
|
||||||
--dt-row-hover: 0, 0, 0;
|
--dt-row-hover: 0, 0, 0;
|
||||||
--dt-column-ordering: 0, 0, 0;
|
--dt-column-ordering: 0, 0, 0;
|
||||||
|
--dt-header-align-items: center;
|
||||||
--dt-html-background: white;
|
--dt-html-background: white;
|
||||||
}
|
}
|
||||||
:root.dark {
|
:root.dark {
|
||||||
--dt-html-background: rgb(33, 37, 41);
|
--dt-html-background: rgb(33, 37, 41);
|
||||||
}
|
}
|
||||||
|
|
||||||
table.dataTable td.dt-control {
|
table.dataTable tbody td.dt-control {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
table.dataTable td.dt-control:before {
|
table.dataTable tbody td.dt-control:before {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
content: "";
|
content: "";
|
||||||
@@ -36,7 +37,7 @@ table.dataTable td.dt-control:before {
|
|||||||
border-bottom: 5px solid transparent;
|
border-bottom: 5px solid transparent;
|
||||||
border-right: 0px solid transparent;
|
border-right: 0px solid transparent;
|
||||||
}
|
}
|
||||||
table.dataTable tr.dt-hasChild td.dt-control:before {
|
table.dataTable tbody tr.dt-hasChild td.dt-control:before {
|
||||||
border-top: 10px solid rgba(0, 0, 0, 0.5);
|
border-top: 10px solid rgba(0, 0, 0, 0.5);
|
||||||
border-left: 5px solid transparent;
|
border-left: 5px solid transparent;
|
||||||
border-bottom: 0px solid transparent;
|
border-bottom: 0px solid transparent;
|
||||||
@@ -104,24 +105,14 @@ table.dataTable thead > tr > td.dt-ordering-desc span.dt-column-order:after {
|
|||||||
content: "\25BC";
|
content: "\25BC";
|
||||||
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,
|
|
||||||
table.dataTable thead > tr > td.dt-orderable-desc,
|
|
||||||
table.dataTable thead > tr > td.dt-ordering-asc,
|
|
||||||
table.dataTable thead > tr > td.dt-ordering-desc {
|
|
||||||
position: relative;
|
|
||||||
padding-right: 30px;
|
|
||||||
}
|
|
||||||
table.dataTable thead > tr > th.dt-orderable-asc span.dt-column-order, table.dataTable thead > tr > th.dt-orderable-desc span.dt-column-order, table.dataTable thead > tr > th.dt-ordering-asc span.dt-column-order, table.dataTable thead > tr > th.dt-ordering-desc span.dt-column-order,
|
table.dataTable thead > tr > th.dt-orderable-asc span.dt-column-order, table.dataTable thead > tr > th.dt-orderable-desc span.dt-column-order, table.dataTable thead > tr > th.dt-ordering-asc span.dt-column-order, table.dataTable thead > tr > th.dt-ordering-desc span.dt-column-order,
|
||||||
table.dataTable thead > tr > td.dt-orderable-asc span.dt-column-order,
|
table.dataTable thead > tr > td.dt-orderable-asc span.dt-column-order,
|
||||||
table.dataTable thead > tr > td.dt-orderable-desc span.dt-column-order,
|
table.dataTable thead > tr > td.dt-orderable-desc span.dt-column-order,
|
||||||
table.dataTable thead > tr > td.dt-ordering-asc span.dt-column-order,
|
table.dataTable thead > tr > td.dt-ordering-asc span.dt-column-order,
|
||||||
table.dataTable thead > tr > td.dt-ordering-desc span.dt-column-order {
|
table.dataTable thead > tr > td.dt-ordering-desc span.dt-column-order {
|
||||||
position: absolute;
|
position: relative;
|
||||||
right: 12px;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
width: 12px;
|
width: 12px;
|
||||||
|
height: 20px;
|
||||||
}
|
}
|
||||||
table.dataTable thead > tr > th.dt-orderable-asc span.dt-column-order:before, table.dataTable thead > tr > th.dt-orderable-asc span.dt-column-order:after, table.dataTable thead > tr > th.dt-orderable-desc span.dt-column-order:before, table.dataTable thead > tr > th.dt-orderable-desc span.dt-column-order:after, table.dataTable thead > tr > th.dt-ordering-asc span.dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-asc span.dt-column-order:after, table.dataTable thead > tr > th.dt-ordering-desc span.dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-desc span.dt-column-order:after,
|
table.dataTable thead > tr > th.dt-orderable-asc span.dt-column-order:before, table.dataTable thead > tr > th.dt-orderable-asc span.dt-column-order:after, table.dataTable thead > tr > th.dt-orderable-desc span.dt-column-order:before, table.dataTable thead > tr > th.dt-orderable-desc span.dt-column-order:after, table.dataTable thead > tr > th.dt-ordering-asc span.dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-asc span.dt-column-order:after, table.dataTable thead > tr > th.dt-ordering-desc span.dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-desc span.dt-column-order:after,
|
||||||
table.dataTable thead > tr > td.dt-orderable-asc span.dt-column-order:before,
|
table.dataTable thead > tr > td.dt-orderable-asc span.dt-column-order:before,
|
||||||
@@ -163,6 +154,40 @@ table.dataTable thead > tr > td:active {
|
|||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table.dataTable thead > tr > th div.dt-column-header,
|
||||||
|
table.dataTable thead > tr > th div.dt-column-footer,
|
||||||
|
table.dataTable thead > tr > td div.dt-column-header,
|
||||||
|
table.dataTable thead > tr > td div.dt-column-footer,
|
||||||
|
table.dataTable tfoot > tr > th div.dt-column-header,
|
||||||
|
table.dataTable tfoot > tr > th div.dt-column-footer,
|
||||||
|
table.dataTable tfoot > tr > td div.dt-column-header,
|
||||||
|
table.dataTable tfoot > tr > td div.dt-column-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: var(--dt-header-align-items);
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
table.dataTable thead > tr > th div.dt-column-header span.dt-column-title,
|
||||||
|
table.dataTable thead > tr > th div.dt-column-footer span.dt-column-title,
|
||||||
|
table.dataTable thead > tr > td div.dt-column-header span.dt-column-title,
|
||||||
|
table.dataTable thead > tr > td div.dt-column-footer span.dt-column-title,
|
||||||
|
table.dataTable tfoot > tr > th div.dt-column-header span.dt-column-title,
|
||||||
|
table.dataTable tfoot > tr > th div.dt-column-footer span.dt-column-title,
|
||||||
|
table.dataTable tfoot > tr > td div.dt-column-header span.dt-column-title,
|
||||||
|
table.dataTable tfoot > tr > td div.dt-column-footer span.dt-column-title {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
table.dataTable thead > tr > th div.dt-column-header span.dt-column-title:empty,
|
||||||
|
table.dataTable thead > tr > th div.dt-column-footer span.dt-column-title:empty,
|
||||||
|
table.dataTable thead > tr > td div.dt-column-header span.dt-column-title:empty,
|
||||||
|
table.dataTable thead > tr > td div.dt-column-footer span.dt-column-title:empty,
|
||||||
|
table.dataTable tfoot > tr > th div.dt-column-header span.dt-column-title:empty,
|
||||||
|
table.dataTable tfoot > tr > th div.dt-column-footer span.dt-column-title:empty,
|
||||||
|
table.dataTable tfoot > tr > td div.dt-column-header span.dt-column-title:empty,
|
||||||
|
table.dataTable tfoot > tr > td div.dt-column-footer span.dt-column-title:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
div.dt-scroll-body > table.dataTable > thead > tr > th,
|
div.dt-scroll-body > table.dataTable > thead > tr > th,
|
||||||
div.dt-scroll-body > table.dataTable > thead > tr > td {
|
div.dt-scroll-body > table.dataTable > thead > tr > td {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -258,10 +283,25 @@ table.dataTable td.dt-type-numeric,
|
|||||||
table.dataTable td.dt-type-date {
|
table.dataTable td.dt-type-date {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
table.dataTable th.dt-type-numeric div.dt-column-header,
|
||||||
|
table.dataTable th.dt-type-numeric div.dt-column-footer, table.dataTable th.dt-type-date div.dt-column-header,
|
||||||
|
table.dataTable th.dt-type-date div.dt-column-footer,
|
||||||
|
table.dataTable td.dt-type-numeric div.dt-column-header,
|
||||||
|
table.dataTable td.dt-type-numeric div.dt-column-footer,
|
||||||
|
table.dataTable td.dt-type-date div.dt-column-header,
|
||||||
|
table.dataTable td.dt-type-date div.dt-column-footer {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
table.dataTable th.dt-left,
|
table.dataTable th.dt-left,
|
||||||
table.dataTable td.dt-left {
|
table.dataTable td.dt-left {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
table.dataTable th.dt-left div.dt-column-header,
|
||||||
|
table.dataTable th.dt-left div.dt-column-footer,
|
||||||
|
table.dataTable td.dt-left div.dt-column-header,
|
||||||
|
table.dataTable td.dt-left div.dt-column-footer {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
table.dataTable th.dt-center,
|
table.dataTable th.dt-center,
|
||||||
table.dataTable td.dt-center {
|
table.dataTable td.dt-center {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -270,10 +310,22 @@ table.dataTable th.dt-right,
|
|||||||
table.dataTable td.dt-right {
|
table.dataTable td.dt-right {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
table.dataTable th.dt-right div.dt-column-header,
|
||||||
|
table.dataTable th.dt-right div.dt-column-footer,
|
||||||
|
table.dataTable td.dt-right div.dt-column-header,
|
||||||
|
table.dataTable td.dt-right div.dt-column-footer {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
table.dataTable th.dt-justify,
|
table.dataTable th.dt-justify,
|
||||||
table.dataTable td.dt-justify {
|
table.dataTable td.dt-justify {
|
||||||
text-align: justify;
|
text-align: justify;
|
||||||
}
|
}
|
||||||
|
table.dataTable th.dt-justify div.dt-column-header,
|
||||||
|
table.dataTable th.dt-justify div.dt-column-footer,
|
||||||
|
table.dataTable td.dt-justify div.dt-column-header,
|
||||||
|
table.dataTable td.dt-justify div.dt-column-footer {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
table.dataTable th.dt-nowrap,
|
table.dataTable th.dt-nowrap,
|
||||||
table.dataTable td.dt-nowrap {
|
table.dataTable td.dt-nowrap {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -295,6 +347,16 @@ table.dataTable tfoot th.dt-head-left,
|
|||||||
table.dataTable tfoot td.dt-head-left {
|
table.dataTable tfoot td.dt-head-left {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
table.dataTable thead th.dt-head-left div.dt-column-header,
|
||||||
|
table.dataTable thead th.dt-head-left div.dt-column-footer,
|
||||||
|
table.dataTable thead td.dt-head-left div.dt-column-header,
|
||||||
|
table.dataTable thead td.dt-head-left div.dt-column-footer,
|
||||||
|
table.dataTable tfoot th.dt-head-left div.dt-column-header,
|
||||||
|
table.dataTable tfoot th.dt-head-left div.dt-column-footer,
|
||||||
|
table.dataTable tfoot td.dt-head-left div.dt-column-header,
|
||||||
|
table.dataTable tfoot td.dt-head-left div.dt-column-footer {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
table.dataTable thead th.dt-head-center,
|
table.dataTable thead th.dt-head-center,
|
||||||
table.dataTable thead td.dt-head-center,
|
table.dataTable thead td.dt-head-center,
|
||||||
table.dataTable tfoot th.dt-head-center,
|
table.dataTable tfoot th.dt-head-center,
|
||||||
@@ -307,12 +369,32 @@ table.dataTable tfoot th.dt-head-right,
|
|||||||
table.dataTable tfoot td.dt-head-right {
|
table.dataTable tfoot td.dt-head-right {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
table.dataTable thead th.dt-head-right div.dt-column-header,
|
||||||
|
table.dataTable thead th.dt-head-right div.dt-column-footer,
|
||||||
|
table.dataTable thead td.dt-head-right div.dt-column-header,
|
||||||
|
table.dataTable thead td.dt-head-right div.dt-column-footer,
|
||||||
|
table.dataTable tfoot th.dt-head-right div.dt-column-header,
|
||||||
|
table.dataTable tfoot th.dt-head-right div.dt-column-footer,
|
||||||
|
table.dataTable tfoot td.dt-head-right div.dt-column-header,
|
||||||
|
table.dataTable tfoot td.dt-head-right div.dt-column-footer {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
table.dataTable thead th.dt-head-justify,
|
table.dataTable thead th.dt-head-justify,
|
||||||
table.dataTable thead td.dt-head-justify,
|
table.dataTable thead td.dt-head-justify,
|
||||||
table.dataTable tfoot th.dt-head-justify,
|
table.dataTable tfoot th.dt-head-justify,
|
||||||
table.dataTable tfoot td.dt-head-justify {
|
table.dataTable tfoot td.dt-head-justify {
|
||||||
text-align: justify;
|
text-align: justify;
|
||||||
}
|
}
|
||||||
|
table.dataTable thead th.dt-head-justify div.dt-column-header,
|
||||||
|
table.dataTable thead th.dt-head-justify div.dt-column-footer,
|
||||||
|
table.dataTable thead td.dt-head-justify div.dt-column-header,
|
||||||
|
table.dataTable thead td.dt-head-justify div.dt-column-footer,
|
||||||
|
table.dataTable tfoot th.dt-head-justify div.dt-column-header,
|
||||||
|
table.dataTable tfoot th.dt-head-justify div.dt-column-footer,
|
||||||
|
table.dataTable tfoot td.dt-head-justify div.dt-column-header,
|
||||||
|
table.dataTable tfoot td.dt-head-justify div.dt-column-footer {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
table.dataTable thead th.dt-head-nowrap,
|
table.dataTable thead th.dt-head-nowrap,
|
||||||
table.dataTable thead td.dt-head-nowrap,
|
table.dataTable thead td.dt-head-nowrap,
|
||||||
table.dataTable tfoot th.dt-head-nowrap,
|
table.dataTable tfoot th.dt-head-nowrap,
|
||||||
@@ -340,6 +422,10 @@ table.dataTable tbody td.dt-body-nowrap {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--dt-header-align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
/*! Bootstrap 5 integration for DataTables
|
/*! Bootstrap 5 integration for DataTables
|
||||||
*
|
*
|
||||||
* ©2020 SpryMedia Ltd, all rights reserved.
|
* ©2020 SpryMedia Ltd, all rights reserved.
|
||||||
@@ -410,6 +496,9 @@ div.dt-container div.dt-layout-table > div {
|
|||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
div.dt-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
div.dt-container div.dt-length label {
|
div.dt-container div.dt-length label {
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
@@ -498,14 +587,19 @@ table.dataTable.table-sm > thead > tr td.dt-orderable-asc,
|
|||||||
table.dataTable.table-sm > thead > tr td.dt-orderable-desc,
|
table.dataTable.table-sm > thead > tr td.dt-orderable-desc,
|
||||||
table.dataTable.table-sm > thead > tr td.dt-ordering-asc,
|
table.dataTable.table-sm > thead > tr td.dt-ordering-asc,
|
||||||
table.dataTable.table-sm > thead > tr td.dt-ordering-desc {
|
table.dataTable.table-sm > thead > tr td.dt-ordering-desc {
|
||||||
padding-right: 20px;
|
padding-right: 0.25rem;
|
||||||
}
|
}
|
||||||
table.dataTable.table-sm > thead > tr th.dt-orderable-asc span.dt-column-order, table.dataTable.table-sm > thead > tr th.dt-orderable-desc span.dt-column-order, table.dataTable.table-sm > thead > tr th.dt-ordering-asc span.dt-column-order, table.dataTable.table-sm > thead > tr th.dt-ordering-desc span.dt-column-order,
|
table.dataTable.table-sm > thead > tr th.dt-orderable-asc span.dt-column-order, table.dataTable.table-sm > thead > tr th.dt-orderable-desc span.dt-column-order, table.dataTable.table-sm > thead > tr th.dt-ordering-asc span.dt-column-order, table.dataTable.table-sm > thead > tr th.dt-ordering-desc span.dt-column-order,
|
||||||
table.dataTable.table-sm > thead > tr td.dt-orderable-asc span.dt-column-order,
|
table.dataTable.table-sm > thead > tr td.dt-orderable-asc span.dt-column-order,
|
||||||
table.dataTable.table-sm > thead > tr td.dt-orderable-desc span.dt-column-order,
|
table.dataTable.table-sm > thead > tr td.dt-orderable-desc span.dt-column-order,
|
||||||
table.dataTable.table-sm > thead > tr td.dt-ordering-asc span.dt-column-order,
|
table.dataTable.table-sm > thead > tr td.dt-ordering-asc span.dt-column-order,
|
||||||
table.dataTable.table-sm > thead > tr td.dt-ordering-desc span.dt-column-order {
|
table.dataTable.table-sm > thead > tr td.dt-ordering-desc span.dt-column-order {
|
||||||
right: 5px;
|
right: 0.25rem;
|
||||||
|
}
|
||||||
|
table.dataTable.table-sm > thead > tr th.dt-type-date span.dt-column-order, table.dataTable.table-sm > thead > tr th.dt-type-numeric span.dt-column-order,
|
||||||
|
table.dataTable.table-sm > thead > tr td.dt-type-date span.dt-column-order,
|
||||||
|
table.dataTable.table-sm > thead > tr td.dt-type-numeric span.dt-column-order {
|
||||||
|
left: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.dt-scroll-head table.table-bordered {
|
div.dt-scroll-head table.table-bordered {
|
||||||
|
|||||||
Vendored
+340
-110
@@ -4,13 +4,13 @@
|
|||||||
*
|
*
|
||||||
* To rebuild or modify this file with the latest versions of the included
|
* To rebuild or modify this file with the latest versions of the included
|
||||||
* software please visit:
|
* software please visit:
|
||||||
* https://datatables.net/download/#bs5/dt-2.2.2
|
* https://datatables.net/download/#bs5/dt-2.3.2
|
||||||
*
|
*
|
||||||
* Included libraries:
|
* Included libraries:
|
||||||
* DataTables 2.2.2
|
* DataTables 2.3.2
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/*! DataTables 2.2.2
|
/*! DataTables 2.3.2
|
||||||
* © SpryMedia Ltd - datatables.net/license
|
* © SpryMedia Ltd - datatables.net/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -101,15 +101,19 @@
|
|||||||
var defaults = DataTable.defaults;
|
var defaults = DataTable.defaults;
|
||||||
var $this = $(this);
|
var $this = $(this);
|
||||||
|
|
||||||
|
// Sanity check
|
||||||
/* Sanity check */
|
|
||||||
if ( this.nodeName.toLowerCase() != 'table' )
|
if ( this.nodeName.toLowerCase() != 'table' )
|
||||||
{
|
{
|
||||||
_fnLog( null, 0, 'Non-table node initialisation ('+this.nodeName+')', 2 );
|
_fnLog( null, 0, 'Non-table node initialisation ('+this.nodeName+')', 2 );
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$(this).trigger( 'options.dt', oInit );
|
// Special case for options
|
||||||
|
if (oInit.on && oInit.on.options) {
|
||||||
|
_fnListener($this, 'options', oInit.on.options);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this.trigger( 'options.dt', oInit );
|
||||||
|
|
||||||
/* Backwards compatibility for the defaults */
|
/* Backwards compatibility for the defaults */
|
||||||
_fnCompatOpts( defaults );
|
_fnCompatOpts( defaults );
|
||||||
@@ -120,7 +124,7 @@
|
|||||||
_fnCamelToHungarian( defaults.column, defaults.column, true );
|
_fnCamelToHungarian( defaults.column, defaults.column, true );
|
||||||
|
|
||||||
/* Setting up the initialisation object */
|
/* Setting up the initialisation object */
|
||||||
_fnCamelToHungarian( defaults, $.extend( oInit, $this.data() ), true );
|
_fnCamelToHungarian( defaults, $.extend( oInit, _fnEscapeObject($this.data()) ), true );
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -248,6 +252,9 @@
|
|||||||
"caption",
|
"caption",
|
||||||
"layout",
|
"layout",
|
||||||
"orderDescReverse",
|
"orderDescReverse",
|
||||||
|
"orderIndicators",
|
||||||
|
"orderHandler",
|
||||||
|
"titleRow",
|
||||||
"typeDetect",
|
"typeDetect",
|
||||||
[ "iCookieDuration", "iStateDuration" ], // backwards compat
|
[ "iCookieDuration", "iStateDuration" ], // backwards compat
|
||||||
[ "oSearch", "oPreviousSearch" ],
|
[ "oSearch", "oPreviousSearch" ],
|
||||||
@@ -276,6 +283,13 @@
|
|||||||
|
|
||||||
oSettings.rowIdFn = _fnGetObjectDataFn( oInit.rowId );
|
oSettings.rowIdFn = _fnGetObjectDataFn( oInit.rowId );
|
||||||
|
|
||||||
|
// Add event listeners
|
||||||
|
if (oInit.on) {
|
||||||
|
Object.keys(oInit.on).forEach(function (key) {
|
||||||
|
_fnListener($this, key, oInit.on[key]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/* Browser support detection */
|
/* Browser support detection */
|
||||||
_fnBrowserDetect( oSettings );
|
_fnBrowserDetect( oSettings );
|
||||||
|
|
||||||
@@ -336,7 +350,7 @@
|
|||||||
/* HTML5 attribute detection - build an mData object automatically if the
|
/* HTML5 attribute detection - build an mData object automatically if the
|
||||||
* attributes are found
|
* attributes are found
|
||||||
*/
|
*/
|
||||||
var rowOne = $this.children('tbody').find('tr').eq(0);
|
var rowOne = $this.children('tbody').find('tr:first-child').eq(0);
|
||||||
|
|
||||||
if ( rowOne.length ) {
|
if ( rowOne.length ) {
|
||||||
var a = function ( cell, name ) {
|
var a = function ( cell, name ) {
|
||||||
@@ -494,6 +508,13 @@
|
|||||||
* @namespace
|
* @namespace
|
||||||
*/
|
*/
|
||||||
DataTable.ext = _ext = {
|
DataTable.ext = _ext = {
|
||||||
|
/**
|
||||||
|
* DataTables build type (expanded by the download builder)
|
||||||
|
*
|
||||||
|
* @type string
|
||||||
|
*/
|
||||||
|
builder: "bs5/dt-2.3.2",
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Buttons. For use with the Buttons extension for DataTables. This is
|
* Buttons. For use with the Buttons extension for DataTables. This is
|
||||||
* defined here so other extensions can define buttons regardless of load
|
* defined here so other extensions can define buttons regardless of load
|
||||||
@@ -505,6 +526,14 @@
|
|||||||
buttons: {},
|
buttons: {},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ColumnControl buttons and content
|
||||||
|
*
|
||||||
|
* @type object
|
||||||
|
*/
|
||||||
|
ccContent: {},
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Element class names
|
* Element class names
|
||||||
*
|
*
|
||||||
@@ -514,14 +543,6 @@
|
|||||||
classes: {},
|
classes: {},
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DataTables build type (expanded by the download builder)
|
|
||||||
*
|
|
||||||
* @type string
|
|
||||||
*/
|
|
||||||
builder: "bs5/dt-2.2.2",
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Error reporting.
|
* Error reporting.
|
||||||
*
|
*
|
||||||
@@ -533,6 +554,11 @@
|
|||||||
*/
|
*/
|
||||||
errMode: "alert",
|
errMode: "alert",
|
||||||
|
|
||||||
|
/** HTML entity escaping */
|
||||||
|
escape: {
|
||||||
|
/** When reading data-* attributes for initialisation options */
|
||||||
|
attributes: false
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Legacy so v1 plug-ins don't throw js errors on load
|
* Legacy so v1 plug-ins don't throw js errors on load
|
||||||
@@ -1887,6 +1913,26 @@
|
|||||||
init.scrollX = init.scrollX ? '100%' : '';
|
init.scrollX = init.scrollX ? '100%' : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Objects for ordering
|
||||||
|
if ( typeof init.bSort === 'object' ) {
|
||||||
|
init.orderIndicators = init.bSort.indicators !== undefined ? init.bSort.indicators : true;
|
||||||
|
init.orderHandler = init.bSort.handler !== undefined ? init.bSort.handler : true;
|
||||||
|
init.bSort = true;
|
||||||
|
}
|
||||||
|
else if (init.bSort === false) {
|
||||||
|
init.orderIndicators = false;
|
||||||
|
init.orderHandler = false;
|
||||||
|
}
|
||||||
|
else if (init.bSort === true) {
|
||||||
|
init.orderIndicators = true;
|
||||||
|
init.orderHandler = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Which cells are the title cells?
|
||||||
|
if (typeof init.bSortCellsTop === 'boolean') {
|
||||||
|
init.titleRow = init.bSortCellsTop;
|
||||||
|
}
|
||||||
|
|
||||||
// Column search objects are in an array, so it needs to be converted
|
// Column search objects are in an array, so it needs to be converted
|
||||||
// element by element
|
// element by element
|
||||||
var searchCols = init.aoSearchCols;
|
var searchCols = init.aoSearchCols;
|
||||||
@@ -3264,7 +3310,7 @@
|
|||||||
* @param {*} settings DataTables settings
|
* @param {*} settings DataTables settings
|
||||||
* @param {*} source Source layout array
|
* @param {*} source Source layout array
|
||||||
* @param {*} incColumns What columns should be included
|
* @param {*} incColumns What columns should be included
|
||||||
* @returns Layout array
|
* @returns Layout array in column index order
|
||||||
*/
|
*/
|
||||||
function _fnHeaderLayout( settings, source, incColumns )
|
function _fnHeaderLayout( settings, source, incColumns )
|
||||||
{
|
{
|
||||||
@@ -3548,7 +3594,9 @@
|
|||||||
|
|
||||||
_fnDraw( settings );
|
_fnDraw( settings );
|
||||||
|
|
||||||
settings._drawHold = false;
|
settings.api.one('draw', function () {
|
||||||
|
settings._drawHold = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -3560,10 +3608,9 @@
|
|||||||
var zero = oLang.sZeroRecords;
|
var zero = oLang.sZeroRecords;
|
||||||
var dataSrc = _fnDataSource( settings );
|
var dataSrc = _fnDataSource( settings );
|
||||||
|
|
||||||
if (
|
// Make use of the fact that settings.json is only set once the initial data has
|
||||||
(settings.iDraw < 1 && dataSrc === 'ssp') ||
|
// been loaded. Show loading when that isn't the case
|
||||||
(settings.iDraw <= 1 && dataSrc === 'ajax')
|
if ((dataSrc === 'ssp' || dataSrc === 'ajax') && ! settings.json) {
|
||||||
) {
|
|
||||||
zero = oLang.sLoadingRecords;
|
zero = oLang.sLoadingRecords;
|
||||||
}
|
}
|
||||||
else if ( oLang.sEmptyTable && settings.fnRecordsTotal() === 0 )
|
else if ( oLang.sEmptyTable && settings.fnRecordsTotal() === 0 )
|
||||||
@@ -3933,6 +3980,7 @@
|
|||||||
var rows = $(thead).children('tr');
|
var rows = $(thead).children('tr');
|
||||||
var row, cell;
|
var row, cell;
|
||||||
var i, k, l, iLen, shifted, column, colspan, rowspan;
|
var i, k, l, iLen, shifted, column, colspan, rowspan;
|
||||||
|
var titleRow = settings.titleRow;
|
||||||
var isHeader = thead && thead.nodeName.toLowerCase() === 'thead';
|
var isHeader = thead && thead.nodeName.toLowerCase() === 'thead';
|
||||||
var layout = [];
|
var layout = [];
|
||||||
var unique;
|
var unique;
|
||||||
@@ -3961,6 +4009,7 @@
|
|||||||
cell.nodeName.toUpperCase() == 'TH'
|
cell.nodeName.toUpperCase() == 'TH'
|
||||||
) {
|
) {
|
||||||
var cols = [];
|
var cols = [];
|
||||||
|
var jqCell = $(cell);
|
||||||
|
|
||||||
// Get the col and rowspan attributes from the DOM and sanitise them
|
// Get the col and rowspan attributes from the DOM and sanitise them
|
||||||
colspan = cell.getAttribute('colspan') * 1;
|
colspan = cell.getAttribute('colspan') * 1;
|
||||||
@@ -3981,7 +4030,7 @@
|
|||||||
if ( write ) {
|
if ( write ) {
|
||||||
if (unique) {
|
if (unique) {
|
||||||
// Allow column options to be set from HTML attributes
|
// Allow column options to be set from HTML attributes
|
||||||
_fnColumnOptions( settings, shifted, $(cell).data() );
|
_fnColumnOptions( settings, shifted, _fnEscapeObject(jqCell.data()) );
|
||||||
|
|
||||||
// Get the width for the column. This can be defined from the
|
// Get the width for the column. This can be defined from the
|
||||||
// width attribute, style attribute or `columns.width` option
|
// width attribute, style attribute or `columns.width` option
|
||||||
@@ -3998,7 +4047,14 @@
|
|||||||
// Column title handling - can be user set, or read from the DOM
|
// Column title handling - can be user set, or read from the DOM
|
||||||
// This happens before the render, so the original is still in place
|
// This happens before the render, so the original is still in place
|
||||||
if ( columnDef.sTitle !== null && ! columnDef.autoTitle ) {
|
if ( columnDef.sTitle !== null && ! columnDef.autoTitle ) {
|
||||||
cell.innerHTML = columnDef.sTitle;
|
if (
|
||||||
|
(titleRow === true && i === 0) || // top row
|
||||||
|
(titleRow === false && i === rows.length -1) || // bottom row
|
||||||
|
(titleRow === i) || // specific row
|
||||||
|
(titleRow === null)
|
||||||
|
) {
|
||||||
|
cell.innerHTML = columnDef.sTitle;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! columnDef.sTitle && unique) {
|
if (! columnDef.sTitle && unique) {
|
||||||
@@ -4016,12 +4072,12 @@
|
|||||||
// Fall back to the aria-label attribute on the table header if no ariaTitle is
|
// Fall back to the aria-label attribute on the table header if no ariaTitle is
|
||||||
// provided.
|
// provided.
|
||||||
if (! columnDef.ariaTitle) {
|
if (! columnDef.ariaTitle) {
|
||||||
columnDef.ariaTitle = $(cell).attr("aria-label") || columnDef.sTitle;
|
columnDef.ariaTitle = jqCell.attr("aria-label") || columnDef.sTitle;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Column specific class names
|
// Column specific class names
|
||||||
if ( columnDef.className ) {
|
if ( columnDef.className ) {
|
||||||
$(cell).addClass( columnDef.className );
|
jqCell.addClass( columnDef.className );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4033,11 +4089,28 @@
|
|||||||
.appendTo(cell);
|
.appendTo(cell);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( isHeader && $('span.dt-column-order', cell).length === 0) {
|
if (
|
||||||
|
settings.orderIndicators &&
|
||||||
|
isHeader &&
|
||||||
|
jqCell.filter(':not([data-dt-order=disable])').length !== 0 &&
|
||||||
|
jqCell.parent(':not([data-dt-order=disable])').length !== 0 &&
|
||||||
|
$('span.dt-column-order', cell).length === 0
|
||||||
|
) {
|
||||||
$('<span>')
|
$('<span>')
|
||||||
.addClass('dt-column-order')
|
.addClass('dt-column-order')
|
||||||
.appendTo(cell);
|
.appendTo(cell);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We need to wrap the elements in the header in another element to use flexbox
|
||||||
|
// layout for those elements
|
||||||
|
var headerFooter = isHeader ? 'header' : 'footer';
|
||||||
|
|
||||||
|
if ( $('span.dt-column-' + headerFooter, cell).length === 0) {
|
||||||
|
$('<div>')
|
||||||
|
.addClass('dt-column-' + headerFooter)
|
||||||
|
.append(cell.childNodes)
|
||||||
|
.appendTo(cell);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there is col / rowspan, copy the information into the layout grid
|
// If there is col / rowspan, copy the information into the layout grid
|
||||||
@@ -4188,6 +4261,11 @@
|
|||||||
// Allow plug-ins and external processes to modify the data
|
// Allow plug-ins and external processes to modify the data
|
||||||
_fnCallbackFire( oSettings, null, 'preXhr', [oSettings, data, baseAjax], true );
|
_fnCallbackFire( oSettings, null, 'preXhr', [oSettings, data, baseAjax], true );
|
||||||
|
|
||||||
|
// Custom Ajax option to submit the parameters as a JSON string
|
||||||
|
if (baseAjax.submitAs === 'json' && typeof data === 'object') {
|
||||||
|
baseAjax.data = JSON.stringify(data);
|
||||||
|
}
|
||||||
|
|
||||||
if ( typeof ajax === 'function' )
|
if ( typeof ajax === 'function' )
|
||||||
{
|
{
|
||||||
// Is a function - let the caller define what needs to be done
|
// Is a function - let the caller define what needs to be done
|
||||||
@@ -4198,7 +4276,7 @@
|
|||||||
// to the object for the callback.
|
// to the object for the callback.
|
||||||
var empty = {};
|
var empty = {};
|
||||||
|
|
||||||
DataTable.util.set(ajax.dataSrc)(empty, []);
|
_fnAjaxDataSrc(oSettings, empty, []);
|
||||||
callback(empty);
|
callback(empty);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -5688,24 +5766,30 @@
|
|||||||
function _fnSortInit( settings ) {
|
function _fnSortInit( settings ) {
|
||||||
var target = settings.nTHead;
|
var target = settings.nTHead;
|
||||||
var headerRows = target.querySelectorAll('tr');
|
var headerRows = target.querySelectorAll('tr');
|
||||||
var legacyTop = settings.bSortCellsTop;
|
var titleRow = settings.titleRow;
|
||||||
var notSelector = ':not([data-dt-order="disable"]):not([data-dt-order="icon-only"])';
|
var notSelector = ':not([data-dt-order="disable"]):not([data-dt-order="icon-only"])';
|
||||||
|
|
||||||
// Legacy support for `orderCellsTop`
|
// Legacy support for `orderCellsTop`
|
||||||
if (legacyTop === true) {
|
if (titleRow === true) {
|
||||||
target = headerRows[0];
|
target = headerRows[0];
|
||||||
}
|
}
|
||||||
else if (legacyTop === false) {
|
else if (titleRow === false) {
|
||||||
target = headerRows[ headerRows.length - 1 ];
|
target = headerRows[ headerRows.length - 1 ];
|
||||||
}
|
}
|
||||||
|
else if (titleRow !== null) {
|
||||||
|
target = headerRows[titleRow];
|
||||||
|
}
|
||||||
|
// else - all rows
|
||||||
|
|
||||||
_fnSortAttachListener(
|
if (settings.orderHandler) {
|
||||||
settings,
|
_fnSortAttachListener(
|
||||||
target,
|
settings,
|
||||||
target === settings.nTHead
|
target,
|
||||||
? 'tr'+notSelector+' th'+notSelector+', tr'+notSelector+' td'+notSelector
|
target === settings.nTHead
|
||||||
: 'th'+notSelector+', td'+notSelector
|
? 'tr'+notSelector+' th'+notSelector+', tr'+notSelector+' td'+notSelector
|
||||||
);
|
: 'th'+notSelector+', td'+notSelector
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Need to resolve the user input array into our internal structure
|
// Need to resolve the user input array into our internal structure
|
||||||
var order = [];
|
var order = [];
|
||||||
@@ -5720,7 +5804,11 @@
|
|||||||
var run = false;
|
var run = false;
|
||||||
var columns = column === undefined
|
var columns = column === undefined
|
||||||
? _fnColumnsFromHeader( e.target )
|
? _fnColumnsFromHeader( e.target )
|
||||||
: [column];
|
: typeof column === 'function'
|
||||||
|
? column()
|
||||||
|
: Array.isArray(column)
|
||||||
|
? column
|
||||||
|
: [column];
|
||||||
|
|
||||||
if ( columns.length ) {
|
if ( columns.length ) {
|
||||||
for ( var i=0, ien=columns.length ; i<ien ; i++ ) {
|
for ( var i=0, ien=columns.length ; i<ien ; i++ ) {
|
||||||
@@ -6343,16 +6431,19 @@
|
|||||||
|
|
||||||
// A column name was stored and should be used for restore
|
// A column name was stored and should be used for restore
|
||||||
if (typeof col[0] === 'string') {
|
if (typeof col[0] === 'string') {
|
||||||
|
// Find the name from the current list of column names
|
||||||
var idx = currentNames.indexOf(col[0]);
|
var idx = currentNames.indexOf(col[0]);
|
||||||
|
|
||||||
// Find the name from the current list of column names, or fallback to index 0
|
if (idx < 0) {
|
||||||
set[0] = idx >= 0
|
// If the column was not found ignore it and continue
|
||||||
? idx
|
return;
|
||||||
: 0;
|
}
|
||||||
|
|
||||||
|
set[0] = idx;
|
||||||
}
|
}
|
||||||
else if (set[0] >= columns.length) {
|
else if (set[0] >= columns.length) {
|
||||||
// If a column name, but it is out of bounds, set to 0
|
// If the column index is out of bounds ignore it and continue
|
||||||
set[0] = 0;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
settings.aaSorting.push(set);
|
settings.aaSorting.push(set);
|
||||||
@@ -6765,6 +6856,36 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add one or more listeners to the table
|
||||||
|
*
|
||||||
|
* @param {*} that JQ for the table
|
||||||
|
* @param {*} name Event name
|
||||||
|
* @param {*} src Listener(s)
|
||||||
|
*/
|
||||||
|
function _fnListener(that, name, src) {
|
||||||
|
if (!Array.isArray(src)) {
|
||||||
|
src = [src];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i=0 ; i<src.length ; i++) {
|
||||||
|
that.on(name + '.dt', src[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escape HTML entities in strings, in an object
|
||||||
|
*/
|
||||||
|
function _fnEscapeObject(obj) {
|
||||||
|
if (DataTable.ext.escape.attributes) {
|
||||||
|
$.each(obj, function (key, val) {
|
||||||
|
obj[key] = _escapeHtml(val);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -7421,12 +7542,24 @@
|
|||||||
['footer', 'aoFooter'],
|
['footer', 'aoFooter'],
|
||||||
].forEach(function (item) {
|
].forEach(function (item) {
|
||||||
_api_register( 'table().' + item[0] + '.structure()' , function (selector) {
|
_api_register( 'table().' + item[0] + '.structure()' , function (selector) {
|
||||||
var indexes = this.columns(selector).indexes().flatten();
|
var indexes = this.columns(selector).indexes().flatten().toArray();
|
||||||
var ctx = this.context[0];
|
var ctx = this.context[0];
|
||||||
|
var structure = _fnHeaderLayout(ctx, ctx[item[1]], indexes);
|
||||||
return _fnHeaderLayout(ctx, ctx[item[1]], indexes);
|
|
||||||
} );
|
// The structure is in column index order - but from this method we want the return to be
|
||||||
})
|
// in the columns() selector API order. In order to do that we need to map from one form
|
||||||
|
// to the other
|
||||||
|
var orderedIndexes = indexes.slice().sort(function (a, b) {
|
||||||
|
return a - b;
|
||||||
|
});
|
||||||
|
|
||||||
|
return structure.map(function (row) {
|
||||||
|
return indexes.map(function (colIdx) {
|
||||||
|
return row[orderedIndexes.indexOf(colIdx)];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
_api_registerPlural( 'tables().containers()', 'table().container()' , function () {
|
_api_registerPlural( 'tables().containers()', 'table().container()' , function () {
|
||||||
@@ -7775,7 +7908,7 @@
|
|||||||
{
|
{
|
||||||
var
|
var
|
||||||
out = [], res,
|
out = [], res,
|
||||||
a, i, ien, j, jen,
|
i, ien,
|
||||||
selectorType = typeof selector;
|
selectorType = typeof selector;
|
||||||
|
|
||||||
// Can't just check for isArray here, as an API or jQuery instance might be
|
// Can't just check for isArray here, as an API or jQuery instance might be
|
||||||
@@ -7785,22 +7918,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
for ( i=0, ien=selector.length ; i<ien ; i++ ) {
|
for ( i=0, ien=selector.length ; i<ien ; i++ ) {
|
||||||
// Only split on simple strings - complex expressions will be jQuery selectors
|
res = selectFn( typeof selector[i] === 'string' ? selector[i].trim() : selector[i] );
|
||||||
a = selector[i] && selector[i].split && ! selector[i].match(/[[(:]/) ?
|
|
||||||
selector[i].split(',') :
|
|
||||||
[ selector[i] ];
|
|
||||||
|
|
||||||
for ( j=0, jen=a.length ; j<jen ; j++ ) {
|
// Remove empty items
|
||||||
res = selectFn( typeof a[j] === 'string' ? (a[j]).trim() : a[j] );
|
res = res.filter( function (item) {
|
||||||
|
return item !== null && item !== undefined;
|
||||||
|
});
|
||||||
|
|
||||||
// Remove empty items
|
if ( res && res.length ) {
|
||||||
res = res.filter( function (item) {
|
out = out.concat( res );
|
||||||
return item !== null && item !== undefined;
|
|
||||||
});
|
|
||||||
|
|
||||||
if ( res && res.length ) {
|
|
||||||
out = out.concat( res );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7829,6 +7955,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
return $.extend( {
|
return $.extend( {
|
||||||
|
columnOrder: 'implied',
|
||||||
search: 'none',
|
search: 'none',
|
||||||
order: 'current',
|
order: 'current',
|
||||||
page: 'all'
|
page: 'all'
|
||||||
@@ -8590,23 +8717,60 @@
|
|||||||
|
|
||||||
var __column_header = function ( settings, column, row ) {
|
var __column_header = function ( settings, column, row ) {
|
||||||
var header = settings.aoHeader;
|
var header = settings.aoHeader;
|
||||||
var target = row !== undefined
|
var titleRow = settings.titleRow;
|
||||||
? row
|
var target = null;
|
||||||
: settings.bSortCellsTop // legacy support
|
|
||||||
? 0
|
if (row !== undefined) {
|
||||||
: header.length - 1;
|
target = row;
|
||||||
|
}
|
||||||
|
else if (titleRow === true) { // legacy orderCellsTop support
|
||||||
|
target = 0;
|
||||||
|
}
|
||||||
|
else if (titleRow === false) {
|
||||||
|
target = header.length - 1;
|
||||||
|
}
|
||||||
|
else if (titleRow !== null) {
|
||||||
|
target = titleRow;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Automatic - find the _last_ unique cell from the top that is not empty (last for
|
||||||
|
// backwards compatibility)
|
||||||
|
for (var i=0 ; i<header.length ; i++) {
|
||||||
|
if (header[i][column].unique && $('span.dt-column-title', header[i][column].cell).text()) {
|
||||||
|
target = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target === null) {
|
||||||
|
target = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return header[target][column].cell;
|
return header[target][column].cell;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var __column_header_cells = function (header) {
|
||||||
|
var out = [];
|
||||||
|
|
||||||
|
for (var i=0 ; i<header.length ; i++) {
|
||||||
|
for (var j=0 ; j<header[i].length ; j++) {
|
||||||
|
var cell = header[i][j].cell;
|
||||||
|
|
||||||
|
if (!out.includes(cell)) {
|
||||||
|
out.push(cell);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
var __column_selector = function ( settings, selector, opts )
|
var __column_selector = function ( settings, selector, opts )
|
||||||
{
|
{
|
||||||
var
|
var
|
||||||
columns = settings.aoColumns,
|
columns = settings.aoColumns,
|
||||||
names = _pluck( columns, 'sName' ),
|
names, titles,
|
||||||
titles = _pluck( columns, 'sTitle' ),
|
nodes = __column_header_cells(settings.aoHeader);
|
||||||
cells = DataTable.util.get('[].[].cell')(settings.aoHeader),
|
|
||||||
nodes = _unique( _flatten([], cells) );
|
|
||||||
|
|
||||||
var run = function ( s ) {
|
var run = function ( s ) {
|
||||||
var selInt = _intVal( s );
|
var selInt = _intVal( s );
|
||||||
@@ -8678,12 +8842,21 @@
|
|||||||
} );
|
} );
|
||||||
|
|
||||||
case 'name':
|
case 'name':
|
||||||
|
// Don't get names, unless needed, and only get once if it is
|
||||||
|
if (!names) {
|
||||||
|
names = _pluck( columns, 'sName' );
|
||||||
|
}
|
||||||
|
|
||||||
// match by name. `names` is column index complete and in order
|
// match by name. `names` is column index complete and in order
|
||||||
return names.map( function (name, i) {
|
return names.map( function (name, i) {
|
||||||
return name === match[1] ? i : null;
|
return name === match[1] ? i : null;
|
||||||
} );
|
} );
|
||||||
|
|
||||||
case 'title':
|
case 'title':
|
||||||
|
if (!titles) {
|
||||||
|
titles = _pluck( columns, 'sTitle' );
|
||||||
|
}
|
||||||
|
|
||||||
// match by column title
|
// match by column title
|
||||||
return titles.map( function (title, i) {
|
return titles.map( function (title, i) {
|
||||||
return title === match[1] ? i : null;
|
return title === match[1] ? i : null;
|
||||||
@@ -8722,7 +8895,11 @@
|
|||||||
[];
|
[];
|
||||||
};
|
};
|
||||||
|
|
||||||
return _selector_run( 'column', selector, run, settings, opts );
|
var selected = _selector_run( 'column', selector, run, settings, opts );
|
||||||
|
|
||||||
|
return opts.columnOrder && opts.columnOrder === 'index'
|
||||||
|
? selected.sort(function (a, b) { return a - b; })
|
||||||
|
: selected; // implied
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -8846,6 +9023,12 @@
|
|||||||
}, 1 );
|
}, 1 );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
_api_registerPlural( 'columns().names()', 'column().name()', function () {
|
||||||
|
return this.iterator( 'column', function ( settings, column ) {
|
||||||
|
return settings.aoColumns[column].sName;
|
||||||
|
}, 1 );
|
||||||
|
} );
|
||||||
|
|
||||||
_api_registerPlural( 'columns().nodes()', 'column().nodes()', function () {
|
_api_registerPlural( 'columns().nodes()', 'column().nodes()', function () {
|
||||||
return this.iterator( 'column-rows', function ( settings, column, i, j, rows ) {
|
return this.iterator( 'column-rows', function ( settings, column, i, j, rows ) {
|
||||||
return _pluck_order( settings.aoData, rows, 'anCells', column ) ;
|
return _pluck_order( settings.aoData, rows, 'anCells', column ) ;
|
||||||
@@ -9272,7 +9455,10 @@
|
|||||||
// otherwise a 2D array was passed in
|
// otherwise a 2D array was passed in
|
||||||
|
|
||||||
return this.iterator( 'table', function ( settings ) {
|
return this.iterator( 'table', function ( settings ) {
|
||||||
settings.aaSorting = Array.isArray(order) ? order.slice() : order;
|
var resolved = [];
|
||||||
|
_fnSortResolve(settings, resolved, order);
|
||||||
|
|
||||||
|
settings.aaSorting = resolved;
|
||||||
} );
|
} );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
@@ -9398,7 +9584,7 @@
|
|||||||
var fixed = settings.searchFixed;
|
var fixed = settings.searchFixed;
|
||||||
|
|
||||||
if (! name) {
|
if (! name) {
|
||||||
return Object.keys(fixed)
|
return Object.keys(fixed);
|
||||||
}
|
}
|
||||||
else if (search === undefined) {
|
else if (search === undefined) {
|
||||||
return fixed[name];
|
return fixed[name];
|
||||||
@@ -9465,10 +9651,10 @@
|
|||||||
var fixed = settings.aoColumns[colIdx].searchFixed;
|
var fixed = settings.aoColumns[colIdx].searchFixed;
|
||||||
|
|
||||||
if (! name) {
|
if (! name) {
|
||||||
return Object.keys(fixed)
|
return Object.keys(fixed);
|
||||||
}
|
}
|
||||||
else if (search === undefined) {
|
else if (search === undefined) {
|
||||||
return fixed[name];
|
return fixed[name] || null;
|
||||||
}
|
}
|
||||||
else if (search === null) {
|
else if (search === null) {
|
||||||
delete fixed[name];
|
delete fixed[name];
|
||||||
@@ -9920,14 +10106,9 @@
|
|||||||
jqTable.append( tfoot );
|
jqTable.append( tfoot );
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up the header
|
// Clean up the header / footer
|
||||||
$(thead).find('span.dt-column-order').remove();
|
cleanHeader(thead, 'header');
|
||||||
$(thead).find('span.dt-column-title').each(function () {
|
cleanHeader(tfoot, 'footer');
|
||||||
var title = $(this).html();
|
|
||||||
$(this).parent().append(title);
|
|
||||||
$(this).remove();
|
|
||||||
});
|
|
||||||
|
|
||||||
settings.colgroup.remove();
|
settings.colgroup.remove();
|
||||||
|
|
||||||
settings.aaSorting = [];
|
settings.aaSorting = [];
|
||||||
@@ -9949,7 +10130,6 @@
|
|||||||
orderClasses.isDesc
|
orderClasses.isDesc
|
||||||
)
|
)
|
||||||
.css('width', '')
|
.css('width', '')
|
||||||
.removeAttr('data-dt-column')
|
|
||||||
.removeAttr('aria-sort');
|
.removeAttr('aria-sort');
|
||||||
|
|
||||||
// Add the TR elements back into the table in their original order
|
// Add the TR elements back into the table in their original order
|
||||||
@@ -10030,6 +10210,19 @@
|
|||||||
: resolved;
|
: resolved;
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
// Needed for header and footer, so pulled into its own function
|
||||||
|
function cleanHeader(node, className) {
|
||||||
|
$(node).find('span.dt-column-order').remove();
|
||||||
|
$(node).find('span.dt-column-title').each(function () {
|
||||||
|
var title = $(this).html();
|
||||||
|
$(this).parent().parent().append(title);
|
||||||
|
$(this).remove();
|
||||||
|
});
|
||||||
|
$(node).find('div.dt-column-' + className).remove();
|
||||||
|
|
||||||
|
$('th, td', node).removeAttr('data-dt-column');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Version string for plug-ins to check compatibility. Allowed format is
|
* Version string for plug-ins to check compatibility. Allowed format is
|
||||||
* `a.b.c-d` where: a:int, b:int, c:int, d:string(dev|beta|alpha). `d` is used
|
* `a.b.c-d` where: a:int, b:int, c:int, d:string(dev|beta|alpha). `d` is used
|
||||||
@@ -10038,7 +10231,7 @@
|
|||||||
* @type string
|
* @type string
|
||||||
* @default Version number
|
* @default Version number
|
||||||
*/
|
*/
|
||||||
DataTable.version = "2.2.2";
|
DataTable.version = "2.3.2";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Private data store, containing all of the settings objects that are
|
* Private data store, containing all of the settings objects that are
|
||||||
@@ -10645,6 +10838,10 @@
|
|||||||
"bSortCellsTop": null,
|
"bSortCellsTop": null,
|
||||||
|
|
||||||
|
|
||||||
|
/** Specify which row is the title row in the header. Replacement for bSortCellsTop */
|
||||||
|
titleRow: null,
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enable or disable the addition of the classes `sorting\_1`, `sorting\_2` and
|
* Enable or disable the addition of the classes `sorting\_1`, `sorting\_2` and
|
||||||
* `sorting\_3` to the columns which are currently being sorted on. This is
|
* `sorting\_3` to the columns which are currently being sorted on. This is
|
||||||
@@ -10922,6 +11119,13 @@
|
|||||||
1: "entry"
|
1: "entry"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page length options
|
||||||
|
*/
|
||||||
|
lengthLabels: {
|
||||||
|
'-1': 'All'
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This string is shown in preference to `zeroRecords` when the table is
|
* This string is shown in preference to `zeroRecords` when the table is
|
||||||
* empty of data (regardless of filtering). Note that this is an optional
|
* empty of data (regardless of filtering). Note that this is an optional
|
||||||
@@ -11192,7 +11396,10 @@
|
|||||||
/**
|
/**
|
||||||
* For server-side processing - use the data from the DOM for the first draw
|
* For server-side processing - use the data from the DOM for the first draw
|
||||||
*/
|
*/
|
||||||
iDeferLoading: null
|
iDeferLoading: null,
|
||||||
|
|
||||||
|
/** Event listeners */
|
||||||
|
on: null
|
||||||
};
|
};
|
||||||
|
|
||||||
_fnHungarianMap( DataTable.defaults );
|
_fnHungarianMap( DataTable.defaults );
|
||||||
@@ -12019,10 +12226,7 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicate that if multiple rows are in the header and there is more than
|
* Indicate that if multiple rows are in the header and there is more than
|
||||||
* one unique cell per column, if the top one (true) or bottom one (false)
|
* one unique cell per column. Replaced by titleRow
|
||||||
* should be used for sorting / title by DataTables.
|
|
||||||
* Note that this parameter will be set by the initialisation routine. To
|
|
||||||
* set a default use {@link DataTable.defaults}.
|
|
||||||
*/
|
*/
|
||||||
"bSortCellsTop": null,
|
"bSortCellsTop": null,
|
||||||
|
|
||||||
@@ -12147,7 +12351,19 @@
|
|||||||
resizeObserver: null,
|
resizeObserver: null,
|
||||||
|
|
||||||
/** Keep a record of the last size of the container, so we can skip duplicates */
|
/** Keep a record of the last size of the container, so we can skip duplicates */
|
||||||
containerWidth: -1
|
containerWidth: -1,
|
||||||
|
|
||||||
|
/** Reverse the initial order of the data set on desc ordering */
|
||||||
|
orderDescReverse: null,
|
||||||
|
|
||||||
|
/** Show / hide ordering indicators in headers */
|
||||||
|
orderIndicators: true,
|
||||||
|
|
||||||
|
/** Default ordering listener */
|
||||||
|
orderHandler: true,
|
||||||
|
|
||||||
|
/** Title row indicator */
|
||||||
|
titleRow: null
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -12977,7 +13193,7 @@
|
|||||||
cell.addClass(classes.order.none);
|
cell.addClass(classes.order.none);
|
||||||
}
|
}
|
||||||
|
|
||||||
var legacyTop = settings.bSortCellsTop;
|
var titleRow = settings.titleRow;
|
||||||
var headerRows = cell.closest('thead').find('tr');
|
var headerRows = cell.closest('thead').find('tr');
|
||||||
var rowIdx = cell.parent().index();
|
var rowIdx = cell.parent().index();
|
||||||
|
|
||||||
@@ -12987,11 +13203,10 @@
|
|||||||
cell.attr('data-dt-order') === 'disable' ||
|
cell.attr('data-dt-order') === 'disable' ||
|
||||||
cell.parent().attr('data-dt-order') === 'disable' ||
|
cell.parent().attr('data-dt-order') === 'disable' ||
|
||||||
|
|
||||||
// Legacy support for `orderCellsTop`. If it is set, then cells
|
// titleRow support, for defining a specific row in the header
|
||||||
// which are not in the top or bottom row of the header (depending
|
(titleRow === true && rowIdx !== 0) ||
|
||||||
// on the value) do not get the sorting classes applied to them
|
(titleRow === false && rowIdx !== headerRows.length - 1) ||
|
||||||
(legacyTop === true && rowIdx !== 0) ||
|
(typeof titleRow === 'number' && rowIdx !== titleRow)
|
||||||
(legacyTop === false && rowIdx !== headerRows.length - 1)
|
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -13001,7 +13216,7 @@
|
|||||||
// `DT` namespace will allow the event to be removed automatically
|
// `DT` namespace will allow the event to be removed automatically
|
||||||
// on destroy, while the `dt` namespaced event is the one we are
|
// on destroy, while the `dt` namespaced event is the one we are
|
||||||
// listening for
|
// listening for
|
||||||
$(settings.nTable).on( 'order.dt.DT column-visibility.dt.DT', function ( e, ctx ) {
|
$(settings.nTable).on( 'order.dt.DT column-visibility.dt.DT', function ( e, ctx, column ) {
|
||||||
if ( settings !== ctx ) { // need to check this this is the host
|
if ( settings !== ctx ) { // need to check this this is the host
|
||||||
return; // table, not a nested one
|
return; // table, not a nested one
|
||||||
}
|
}
|
||||||
@@ -13012,6 +13227,16 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var orderedColumns = _pluck(sorting, 'col');
|
||||||
|
|
||||||
|
// This handler is only needed on column visibility if the column is part of the
|
||||||
|
// ordering. If it isn't, then we can bail out to save performance. It could be a
|
||||||
|
// separate event handler, but this is a balance between code reuse / size and performance
|
||||||
|
// console.log(e, e.name, column, orderedColumns, orderedColumns.includes(column))
|
||||||
|
if (e.type === 'column-visibility' && ! orderedColumns.includes(column)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var i;
|
var i;
|
||||||
var orderClasses = classes.order;
|
var orderClasses = classes.order;
|
||||||
var columns = ctx.api.columns( cell );
|
var columns = ctx.api.columns( cell );
|
||||||
@@ -13020,8 +13245,8 @@
|
|||||||
var ariaType = '';
|
var ariaType = '';
|
||||||
var indexes = columns.indexes();
|
var indexes = columns.indexes();
|
||||||
var sortDirs = columns.orderable(true).flatten();
|
var sortDirs = columns.orderable(true).flatten();
|
||||||
var orderedColumns = _pluck(sorting, 'col');
|
|
||||||
var tabIndex = settings.iTabIndex;
|
var tabIndex = settings.iTabIndex;
|
||||||
|
var canOrder = ctx.orderHandler && orderable;
|
||||||
|
|
||||||
cell
|
cell
|
||||||
.removeClass(
|
.removeClass(
|
||||||
@@ -13029,8 +13254,8 @@
|
|||||||
orderClasses.isDesc
|
orderClasses.isDesc
|
||||||
)
|
)
|
||||||
.toggleClass( orderClasses.none, ! orderable )
|
.toggleClass( orderClasses.none, ! orderable )
|
||||||
.toggleClass( orderClasses.canAsc, orderable && sortDirs.includes('asc') )
|
.toggleClass( orderClasses.canAsc, canOrder && sortDirs.includes('asc') )
|
||||||
.toggleClass( orderClasses.canDesc, orderable && sortDirs.includes('desc') );
|
.toggleClass( orderClasses.canDesc, canOrder && sortDirs.includes('desc') );
|
||||||
|
|
||||||
// Determine if all of the columns that this cell covers are included in the
|
// Determine if all of the columns that this cell covers are included in the
|
||||||
// current ordering
|
// current ordering
|
||||||
@@ -13789,12 +14014,17 @@
|
|||||||
} );
|
} );
|
||||||
|
|
||||||
for ( i=0 ; i<lengths.length ; i++ ) {
|
for ( i=0 ; i<lengths.length ; i++ ) {
|
||||||
select[0][ i ] = new Option(
|
// Attempt to look up the length from the i18n options
|
||||||
typeof language[i] === 'number' ?
|
var label = settings.api.i18n('lengthLabels.' + lengths[i], null);
|
||||||
|
|
||||||
|
if (label === null) {
|
||||||
|
// If not present, fallback to old style
|
||||||
|
label = typeof language[i] === 'number' ?
|
||||||
settings.fnFormatNumber( language[i] ) :
|
settings.fnFormatNumber( language[i] ) :
|
||||||
language[i],
|
language[i];
|
||||||
lengths[i]
|
}
|
||||||
);
|
|
||||||
|
select[0][ i ] = new Option(label, lengths[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// add for and id to label and input
|
// add for and id to label and input
|
||||||
|
|||||||
@@ -7,35 +7,34 @@
|
|||||||
<div class="col-md">
|
<div class="col-md">
|
||||||
<dl class="row">
|
<dl class="row">
|
||||||
<dt class="col-sm-5">Server Installed
|
<dt class="col-sm-5">Server Installed
|
||||||
<span class="badge bg-success d-none" id="server-success" title="Latest version is installed.">Ok</span>
|
<span class="badge bg-success d-none abbr-badge" id="server-success" title="Latest version is installed.">Ok</span>
|
||||||
<span class="badge bg-warning text-dark d-none" id="server-warning" title="There seems to be an update available.">Update</span>
|
<span class="badge bg-warning text-dark d-none abbr-badge" id="server-warning" title="There seems to be an update available.">Update</span>
|
||||||
<span class="badge bg-info text-dark d-none" id="server-branch" title="This is a branched version.">Branched</span>
|
<span class="badge bg-info text-dark d-none abbr-badge" id="server-branch" title="This is a branched version.">Branched</span>
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="col-sm-7">
|
<dd class="col-sm-7">
|
||||||
<span id="server-installed">{{page_data.current_release}}</span>
|
<span id="server-installed">{{page_data.current_release}}</span>
|
||||||
</dd>
|
</dd>
|
||||||
<dt class="col-sm-5">Server Latest
|
<dt class="col-sm-5">Server Latest
|
||||||
<span class="badge bg-secondary d-none" id="server-failed" title="Unable to determine latest version.">Unknown</span>
|
<span class="badge bg-secondary d-none abbr-badge" id="server-failed" title="Unable to determine latest version.">Unknown</span>
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="col-sm-7">
|
<dd class="col-sm-7">
|
||||||
<span id="server-latest">{{page_data.latest_release}}<span id="server-latest-commit" class="d-none">-{{page_data.latest_commit}}</span></span>
|
<span id="server-latest">{{page_data.latest_release}}<span id="server-latest-commit" class="d-none">-{{page_data.latest_commit}}</span></span>
|
||||||
</dd>
|
</dd>
|
||||||
{{#if page_data.web_vault_enabled}}
|
{{#if page_data.web_vault_enabled}}
|
||||||
<dt class="col-sm-5">Web Installed
|
<dt class="col-sm-5">Web Installed
|
||||||
<span class="badge bg-success d-none" id="web-success" title="Latest version is installed.">Ok</span>
|
<span class="badge bg-success d-none abbr-badge" id="web-success" title="Latest version is installed.">Ok</span>
|
||||||
<span class="badge bg-warning text-dark d-none" id="web-warning" title="There seems to be an update available.">Update</span>
|
<span class="badge bg-warning text-dark d-none abbr-badge" id="web-warning" title="There seems to be an update available.">Update</span>
|
||||||
|
<span class="badge bg-info text-dark d-none abbr-badge" id="web-prerelease" title="You seem to be using a pre-release version.">Pre-Release</span>
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="col-sm-7">
|
<dd class="col-sm-7">
|
||||||
<span id="web-installed">{{page_data.web_vault_version}}</span>
|
<span id="web-installed">{{page_data.web_vault_version}}</span>
|
||||||
</dd>
|
</dd>
|
||||||
{{#unless page_data.running_within_container}}
|
|
||||||
<dt class="col-sm-5">Web Latest
|
<dt class="col-sm-5">Web Latest
|
||||||
<span class="badge bg-secondary d-none" id="web-failed" title="Unable to determine latest version.">Unknown</span>
|
<span class="badge bg-secondary d-none abbr-badge" id="web-failed" title="Unable to determine latest version.">Unknown</span>
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="col-sm-7">
|
<dd class="col-sm-7">
|
||||||
<span id="web-latest">{{page_data.latest_web_build}}</span>
|
<span id="web-latest">{{page_data.latest_web_build}}</span>
|
||||||
</dd>
|
</dd>
|
||||||
{{/unless}}
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#unless page_data.web_vault_enabled}}
|
{{#unless page_data.web_vault_enabled}}
|
||||||
<dt class="col-sm-5">Web Installed</dt>
|
<dt class="col-sm-5">Web Installed</dt>
|
||||||
@@ -68,10 +67,11 @@
|
|||||||
<span class="d-block"><b>No</b></span>
|
<span class="d-block"><b>No</b></span>
|
||||||
{{/unless}}
|
{{/unless}}
|
||||||
</dd>
|
</dd>
|
||||||
<dt class="col-sm-5">Environment settings overridden</dt>
|
<dt class="col-sm-5">Uses config.json</dt>
|
||||||
<dd class="col-sm-7">
|
<dd class="col-sm-7">
|
||||||
{{#if page_data.overrides}}
|
{{#if page_data.overrides}}
|
||||||
<span class="d-block" title="The following settings are overridden: {{page_data.overrides}}"><b>Yes</b></span>
|
<span class="d-inline"><b>Yes</b></span>
|
||||||
|
<span class="badge bg-info text-dark abbr-badge" title="Environment variables are overwritten by a config.json.
{{page_data.overrides}}">Details</span>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#unless page_data.overrides}}
|
{{#unless page_data.overrides}}
|
||||||
<span class="d-block"><b>No</b></span>
|
<span class="d-block"><b>No</b></span>
|
||||||
@@ -90,10 +90,10 @@
|
|||||||
{{#if page_data.ip_header_exists}}
|
{{#if page_data.ip_header_exists}}
|
||||||
<dt class="col-sm-5">IP header
|
<dt class="col-sm-5">IP header
|
||||||
{{#if page_data.ip_header_match}}
|
{{#if page_data.ip_header_match}}
|
||||||
<span class="badge bg-success" title="IP_HEADER config seems to be valid.">Match</span>
|
<span class="badge bg-success abbr-badge" title="IP_HEADER config seems to be valid.">Match</span>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#unless page_data.ip_header_match}}
|
{{#unless page_data.ip_header_match}}
|
||||||
<span class="badge bg-danger" title="IP_HEADER config seems to be invalid. IP's in the log could be invalid. Please fix.">No Match</span>
|
<span class="badge bg-danger abbr-badge" title="IP_HEADER config seems to be invalid. IP's in the log could be invalid. Please fix.">No Match</span>
|
||||||
{{/unless}}
|
{{/unless}}
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="col-sm-7">
|
<dd class="col-sm-7">
|
||||||
@@ -109,10 +109,10 @@
|
|||||||
{{!-- End if IP Header Exists --}}
|
{{!-- End if IP Header Exists --}}
|
||||||
<dt class="col-sm-5">Internet access
|
<dt class="col-sm-5">Internet access
|
||||||
{{#if page_data.has_http_access}}
|
{{#if page_data.has_http_access}}
|
||||||
<span class="badge bg-success" title="We have internet access!">Ok</span>
|
<span class="badge bg-success abbr-badge" title="We have internet access!">Ok</span>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#unless page_data.has_http_access}}
|
{{#unless page_data.has_http_access}}
|
||||||
<span class="badge bg-danger" title="There seems to be no internet access. Please fix.">Error</span>
|
<span class="badge bg-danger abbr-badge" title="There seems to be no internet access. Please fix.">Error</span>
|
||||||
{{/unless}}
|
{{/unless}}
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="col-sm-7">
|
<dd class="col-sm-7">
|
||||||
@@ -134,8 +134,8 @@
|
|||||||
</dd>
|
</dd>
|
||||||
<dt class="col-sm-5">Websocket enabled
|
<dt class="col-sm-5">Websocket enabled
|
||||||
{{#if page_data.enable_websocket}}
|
{{#if page_data.enable_websocket}}
|
||||||
<span class="badge bg-success d-none" id="websocket-success" title="Websocket connection is working.">Ok</span>
|
<span class="badge bg-success d-none abbr-badge" id="websocket-success" title="Websocket connection is working.">Ok</span>
|
||||||
<span class="badge bg-danger d-none" id="websocket-error" title="Websocket connection error, validate your reverse proxy configuration!">Error</span>
|
<span class="badge bg-danger d-none abbr-badge" id="websocket-error" title="Websocket connection error, validate your reverse proxy configuration!">Error</span>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="col-sm-7">
|
<dd class="col-sm-7">
|
||||||
@@ -148,23 +148,27 @@
|
|||||||
</dd>
|
</dd>
|
||||||
|
|
||||||
<dt class="col-sm-5">DNS (github.com)
|
<dt class="col-sm-5">DNS (github.com)
|
||||||
<span class="badge bg-success d-none" id="dns-success" title="DNS Resolving works!">Ok</span>
|
<span class="badge bg-success d-none abbr-badge" id="dns-success" title="DNS Resolving works!">Ok</span>
|
||||||
<span class="badge bg-danger d-none" id="dns-warning" title="DNS Resolving failed. Please fix.">Error</span>
|
<span class="badge bg-danger d-none abbr-badge" id="dns-warning" title="DNS Resolving failed. Please fix.">Error</span>
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="col-sm-7">
|
<dd class="col-sm-7">
|
||||||
<span id="dns-resolved">{{page_data.dns_resolved}}</span>
|
<span id="dns-resolved">{{page_data.dns_resolved}}</span>
|
||||||
</dd>
|
</dd>
|
||||||
<dt class="col-sm-5">Date & Time (Local)</dt>
|
<dt class="col-sm-5">Date & Time (Local)
|
||||||
|
{{#if page_data.tz_env}}
|
||||||
|
<span class="badge bg-success abbr-badge" title="Configured TZ environment variable">{{page_data.tz_env}}</span>
|
||||||
|
{{/if}}
|
||||||
|
</dt>
|
||||||
<dd class="col-sm-7">
|
<dd class="col-sm-7">
|
||||||
<span><b>Server:</b> {{page_data.server_time_local}}</span>
|
<span><b>Server:</b> {{page_data.server_time_local}}</span>
|
||||||
</dd>
|
</dd>
|
||||||
<dt class="col-sm-5">Date & Time (UTC)
|
<dt class="col-sm-5">Date & Time (UTC)
|
||||||
<span class="badge bg-success d-none" id="time-success" title="Server and browser times are within 15 seconds of each other.">Server/Browser Ok</span>
|
<span class="badge bg-success d-none abbr-badge" id="time-success" title="Server and browser times are within 15 seconds of each other.">Server/Browser Ok</span>
|
||||||
<span class="badge bg-danger d-none" id="time-warning" title="Server and browser times are more than 15 seconds apart.">Server/Browser Error</span>
|
<span class="badge bg-danger d-none abbr-badge" id="time-warning" title="Server and browser times are more than 15 seconds apart.">Server/Browser Error</span>
|
||||||
<span class="badge bg-success d-none" id="ntp-server-success" title="Server and NTP times are within 15 seconds of each other.">Server NTP Ok</span>
|
<span class="badge bg-success d-none abbr-badge" id="ntp-server-success" title="Server and NTP times are within 15 seconds of each other.">Server NTP Ok</span>
|
||||||
<span class="badge bg-danger d-none" id="ntp-server-warning" title="Server and NTP times are more than 15 seconds apart.">Server NTP Error</span>
|
<span class="badge bg-danger d-none abbr-badge" id="ntp-server-warning" title="Server and NTP times are more than 15 seconds apart.">Server NTP Error</span>
|
||||||
<span class="badge bg-success d-none" id="ntp-browser-success" title="Browser and NTP times are within 15 seconds of each other.">Browser NTP Ok</span>
|
<span class="badge bg-success d-none abbr-badge" id="ntp-browser-success" title="Browser and NTP times are within 15 seconds of each other.">Browser NTP Ok</span>
|
||||||
<span class="badge bg-danger d-none" id="ntp-browser-warning" title="Browser and NTP times are more than 15 seconds apart.">Browser NTP Error</span>
|
<span class="badge bg-danger d-none abbr-badge" id="ntp-browser-warning" title="Browser and NTP times are more than 15 seconds apart.">Browser NTP Error</span>
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="col-sm-7">
|
<dd class="col-sm-7">
|
||||||
<span id="ntp-time" class="d-block"><b>NTP:</b> <span id="ntp-server-string">{{page_data.ntp_time}}</span></span>
|
<span id="ntp-time" class="d-block"><b>NTP:</b> <span id="ntp-server-string">{{page_data.ntp_time}}</span></span>
|
||||||
@@ -173,10 +177,10 @@
|
|||||||
</dd>
|
</dd>
|
||||||
|
|
||||||
<dt class="col-sm-5">Domain configuration
|
<dt class="col-sm-5">Domain configuration
|
||||||
<span class="badge bg-success d-none" id="domain-success" title="The domain variable matches the browser location and seems to be configured correctly.">Match</span>
|
<span class="badge bg-success d-none abbr-badge" id="domain-success" title="The domain variable matches the browser location and seems to be configured correctly.">Match</span>
|
||||||
<span class="badge bg-danger d-none" id="domain-warning" title="The domain variable does not match the browser location.
The domain variable does not seem to be configured correctly.
Some features may not work as expected!">No Match</span>
|
<span class="badge bg-danger d-none abbr-badge" id="domain-warning" title="The domain variable does not match the browser location.
The domain variable does not seem to be configured correctly.
Some features may not work as expected!">No Match</span>
|
||||||
<span class="badge bg-success d-none" id="https-success" title="Configured to use HTTPS">HTTPS</span>
|
<span class="badge bg-success d-none abbr-badge" id="https-success" title="Configured to use HTTPS">HTTPS</span>
|
||||||
<span class="badge bg-danger d-none" id="https-warning" title="Not configured to use HTTPS.
Some features may not work as expected!">No HTTPS</span>
|
<span class="badge bg-danger d-none abbr-badge" id="https-warning" title="Not configured to use HTTPS.
Some features may not work as expected!">No HTTPS</span>
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="col-sm-7">
|
<dd class="col-sm-7">
|
||||||
<span id="domain-server" class="d-block"><b>Server:</b> <span id="domain-server-string">{{page_data.admin_url}}</span></span>
|
<span id="domain-server" class="d-block"><b>Server:</b> <span id="domain-server-string">{{page_data.admin_url}}</span></span>
|
||||||
@@ -184,8 +188,8 @@
|
|||||||
</dd>
|
</dd>
|
||||||
|
|
||||||
<dt class="col-sm-5">HTTP Response validation
|
<dt class="col-sm-5">HTTP Response validation
|
||||||
<span class="badge bg-success d-none" id="http-response-success" title="All headers and HTTP request responses seem to be ok.">Ok</span>
|
<span class="badge bg-success d-none abbr-badge" id="http-response-success" title="All headers and HTTP request responses seem to be ok.">Ok</span>
|
||||||
<span class="badge bg-danger d-none" id="http-response-warning" title="Some headers or HTTP request responses return invalid data!">Error</span>
|
<span class="badge bg-danger d-none abbr-badge" id="http-response-warning" title="Some headers or HTTP request responses return invalid data!">Error</span>
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="col-sm-7">
|
<dd class="col-sm-7">
|
||||||
<span id="http-response-errors" class="d-block"></span>
|
<span id="http-response-errors" class="d-block"></span>
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
<span class="d-block"><strong>Groups:</strong> {{group_count}}</span>
|
<span class="d-block"><strong>Groups:</strong> {{group_count}}</span>
|
||||||
<span class="d-block"><strong>Events:</strong> {{event_count}}</span>
|
<span class="d-block"><strong>Events:</strong> {{event_count}}</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end px-0 small">
|
<td class="text-end px-1 small">
|
||||||
<button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-delete-organization data-vw-org-uuid="{{id}}" data-vw-org-name="{{name}}" data-vw-billing-email="{{billingEmail}}">Delete Organization</button><br>
|
<button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-delete-organization data-vw-org-uuid="{{id}}" data-vw-org-name="{{name}}" data-vw-billing-email="{{billingEmail}}">Delete Organization</button><br>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -60,7 +60,7 @@
|
|||||||
{{/each}}
|
{{/each}}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end px-0 small">
|
<td class="text-end px-1 small">
|
||||||
<span data-vw-user-uuid="{{id}}" data-vw-user-email="{{email}}">
|
<span data-vw-user-uuid="{{id}}" data-vw-user-email="{{email}}">
|
||||||
{{#if twoFactorEnabled}}
|
{{#if twoFactorEnabled}}
|
||||||
<button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-remove2fa>Remove all 2FA</button><br>
|
<button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-remove2fa>Remove all 2FA</button><br>
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
Your Email Change
|
||||||
|
<!---------------->
|
||||||
|
A user ({{ acting_address }}) recently tried to change their account to use this email address ({{ existing_address }}). An account already exists with this email ({{ existing_address }}).
|
||||||
|
|
||||||
|
If you did not try to change an email address, contact your administrator.
|
||||||
|
{{> email/email_footer_text }}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
Your Email Change
|
||||||
|
<!---------------->
|
||||||
|
{{> 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">
|
||||||
|
A user ({{ acting_address }}) recently tried to change their account to use this email address ({{ existing_address }}). An account already exists with this email ({{ existing_address }}).
|
||||||
|
</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 try to change an email address, contact your administrator.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
{{> email/email_footer }}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
Removed from {{{org_name}}}
|
Your access to {{{org_name}}} has been revoked.
|
||||||
<!---------------->
|
<!---------------->
|
||||||
You have been removed from organization *{{org_name}}* because your account does not have Two-step Login enabled.
|
Your user account has been removed from the *{{org_name}}* organization because you do not have two-step login configured.
|
||||||
|
Before you can re-join this organization you need to set up two-step login on your user account.
|
||||||
|
|
||||||
|
You can enable two-step login in your account settings.
|
||||||
You can enable Two-step Login in your account settings.
|
|
||||||
{{> email/email_footer_text }}
|
{{> email/email_footer_text }}
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
Removed from {{{org_name}}}
|
Your access to {{{org_name}}} has been revoked.
|
||||||
<!---------------->
|
<!---------------->
|
||||||
{{> email/email_header }}
|
{{> email/email_header }}
|
||||||
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
|
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
|
||||||
You have been removed from organization <b 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;">{{org_name}}</b> because your account does not have Two-step Login enabled.
|
Your user account has been removed from the <b 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;">{{org_name}}</b> organization because you do not have two-step login configured.<br>
|
||||||
|
Before you can re-join this organization you need to set up two-step login on your user account.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
<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">
|
<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">
|
||||||
You can enable Two-step Login in your account settings.
|
You can enable two-step login in your account settings.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -20,16 +20,50 @@ a[href$="/settings/sponsored-families"] {
|
|||||||
@extend %vw-hide;
|
@extend %vw-hide;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Hide the sso `Email` input field */
|
||||||
|
.vw-email-sso {
|
||||||
|
@extend %vw-hide;
|
||||||
|
}
|
||||||
|
|
||||||
/* Hide the `Enterprise Single Sign-On` button on the login page */
|
/* Hide the `Enterprise Single Sign-On` button on the login page */
|
||||||
app-root form.ng-untouched button.\!tw-text-primary-600:nth-child(4) {
|
{{#if (webver ">=2025.5.1")}}
|
||||||
|
.vw-sso-login {
|
||||||
|
@extend %vw-hide;
|
||||||
|
}
|
||||||
|
{{else}}
|
||||||
|
app-root ng-component > form > div:nth-child(1) > div > button[buttontype="secondary"].\!tw-text-primary-600:nth-child(4) {
|
||||||
|
@extend %vw-hide;
|
||||||
|
}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
/* Hide the `Log in with passkey` settings */
|
||||||
|
app-change-password app-webauthn-login-settings {
|
||||||
@extend %vw-hide;
|
@extend %vw-hide;
|
||||||
}
|
}
|
||||||
/* Hide Log in with passkey on the login page */
|
/* Hide Log in with passkey on the login page */
|
||||||
app-root form.ng-untouched a[routerlink="/login-with-passkey"] {
|
{{#if (webver ">=2025.5.1")}}
|
||||||
|
.vw-passkey-login {
|
||||||
@extend %vw-hide;
|
@extend %vw-hide;
|
||||||
}
|
}
|
||||||
|
{{else}}
|
||||||
|
app-root ng-component > form > div:nth-child(1) > div > button[buttontype="secondary"].\!tw-text-primary-600:nth-child(3) {
|
||||||
|
@extend %vw-hide;
|
||||||
|
}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
/* Hide the or text followed by the two buttons hidden above */
|
/* 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) {
|
{{#if (webver ">=2025.5.1")}}
|
||||||
|
.vw-or-text {
|
||||||
|
@extend %vw-hide;
|
||||||
|
}
|
||||||
|
{{else}}
|
||||||
|
app-root ng-component > form > div:nth-child(1) > div:nth-child(3) > div:nth-child(2) {
|
||||||
|
@extend %vw-hide;
|
||||||
|
}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
/* Hide the `Other` button on the login page */
|
||||||
|
.vw-other-login {
|
||||||
@extend %vw-hide;
|
@extend %vw-hide;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,19 +116,23 @@ bit-nav-logo bit-nav-item .bwi-shield {
|
|||||||
{{#if signup_disabled}}
|
{{#if signup_disabled}}
|
||||||
/* From web vault 2025.1.2 and onwards, the signup button is hidden
|
/* 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.
|
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
|
Note that the clients tend to cache this endpoint for about 1 hour, so it might
|
||||||
take a while for the change to take effect. To avoid the button appearing
|
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 */
|
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 */
|
/* Hide the register link on the login screen */
|
||||||
|
{{#if (webver "<2025.3.0")}}
|
||||||
app-login form div + div + div + div + hr,
|
app-login form div + div + div + div + hr,
|
||||||
app-login form div + div + div + div + hr + p {
|
app-login form div + div + div + div + hr + p {
|
||||||
@extend %vw-hide;
|
@extend %vw-hide;
|
||||||
}
|
}
|
||||||
|
{{else}}
|
||||||
|
app-root a[routerlink="/signup"] {
|
||||||
|
@extend %vw-hide;
|
||||||
|
}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#unless mail_enabled}}
|
{{#unless mail_2fa_enabled}}
|
||||||
/* Hide `Email` 2FA if mail is not enabled */
|
/* Hide `Email` 2FA if mail is not enabled */
|
||||||
.providers-2fa-1 {
|
.providers-2fa-1 {
|
||||||
@extend %vw-hide;
|
@extend %vw-hide;
|
||||||
|
|||||||
+87
-47
@@ -16,7 +16,7 @@ use tokio::{
|
|||||||
time::{sleep, Duration},
|
time::{sleep, Duration},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::CONFIG;
|
use crate::{config::PathType, CONFIG};
|
||||||
|
|
||||||
pub struct AppHeaders();
|
pub struct AppHeaders();
|
||||||
|
|
||||||
@@ -61,9 +61,11 @@ impl Fairing for AppHeaders {
|
|||||||
|
|
||||||
// The `Cross-Origin-Resource-Policy` header should not be set on images or on the `icon_external` route.
|
// 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
|
// Otherwise some clients, like the Bitwarden Desktop, will fail to download the icons
|
||||||
|
let mut is_image = true;
|
||||||
if !(res.headers().get_one("Content-Type").is_some_and(|v| v.starts_with("image/"))
|
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")))
|
|| req.route().is_some_and(|v| v.name.as_deref() == Some("icon_external")))
|
||||||
{
|
{
|
||||||
|
is_image = false;
|
||||||
res.set_raw_header("Cross-Origin-Resource-Policy", "same-origin");
|
res.set_raw_header("Cross-Origin-Resource-Policy", "same-origin");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,49 +73,56 @@ impl Fairing for AppHeaders {
|
|||||||
// This can cause issues when some MFA requests needs to open a popup or page within the clients like WebAuthn, or Duo.
|
// 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.
|
// This is the same behavior as upstream Bitwarden.
|
||||||
if !req_uri_path.ends_with("connector.html") {
|
if !req_uri_path.ends_with("connector.html") {
|
||||||
// # Frame Ancestors:
|
let csp = if is_image {
|
||||||
// Chrome Web Store: https://chrome.google.com/webstore/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb
|
// Prevent scripts, frames, objects, etc., from loading with images, mainly for SVG images, since these could contain JavaScript and other unsafe items.
|
||||||
// Edge Add-ons: https://microsoftedge.microsoft.com/addons/detail/bitwarden-free-password/jbkfoedolllekgbhcbcoahefnbanhhlh?hl=en-US
|
// Even though we sanitize SVG images before storing and viewing them, it's better to prevent allowing these elements.
|
||||||
// Firefox Browser Add-ons: https://addons.mozilla.org/en-US/firefox/addon/bitwarden-password-manager/
|
String::from("default-src 'none'; img-src 'self' data:; style-src 'unsafe-inline'; script-src 'none'; frame-src 'none'; object-src 'none")
|
||||||
// # img/child/frame src:
|
} else {
|
||||||
// Have I Been Pwned to allow those calls to work.
|
// # Frame Ancestors:
|
||||||
// # Connect src:
|
// Chrome Web Store: https://chrome.google.com/webstore/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb
|
||||||
// Leaked Passwords check: api.pwnedpasswords.com
|
// Edge Add-ons: https://microsoftedge.microsoft.com/addons/detail/bitwarden-free-password/jbkfoedolllekgbhcbcoahefnbanhhlh?hl=en-US
|
||||||
// 2FA/MFA Site check: api.2fa.directory
|
// Firefox Browser Add-ons: https://addons.mozilla.org/en-US/firefox/addon/bitwarden-password-manager/
|
||||||
// # Mail Relay: https://bitwarden.com/blog/add-privacy-and-security-using-email-aliases-with-bitwarden/
|
// # img/child/frame src:
|
||||||
// app.simplelogin.io, app.addy.io, api.fastmail.com, quack.duckduckgo.com
|
// Have I Been Pwned to allow those calls to work.
|
||||||
let csp = format!(
|
// # Connect src:
|
||||||
"default-src 'none'; \
|
// Leaked Passwords check: api.pwnedpasswords.com
|
||||||
font-src 'self'; \
|
// 2FA/MFA Site check: api.2fa.directory
|
||||||
manifest-src 'self'; \
|
// # Mail Relay: https://bitwarden.com/blog/add-privacy-and-security-using-email-aliases-with-bitwarden/
|
||||||
base-uri 'self'; \
|
// app.simplelogin.io, app.addy.io, api.fastmail.com, api.forwardemail.net
|
||||||
form-action 'self'; \
|
format!(
|
||||||
object-src 'self' blob:; \
|
"default-src 'none'; \
|
||||||
script-src 'self' 'wasm-unsafe-eval'; \
|
font-src 'self'; \
|
||||||
style-src 'self' 'unsafe-inline'; \
|
manifest-src 'self'; \
|
||||||
child-src 'self' https://*.duosecurity.com https://*.duofederal.com; \
|
base-uri 'self'; \
|
||||||
frame-src 'self' https://*.duosecurity.com https://*.duofederal.com; \
|
form-action 'self'; \
|
||||||
frame-ancestors 'self' \
|
object-src 'self' blob:; \
|
||||||
chrome-extension://nngceckbapebfimnlniiiahkandclblb \
|
script-src 'self' 'wasm-unsafe-eval'; \
|
||||||
chrome-extension://jbkfoedolllekgbhcbcoahefnbanhhlh \
|
style-src 'self' 'unsafe-inline'; \
|
||||||
moz-extension://* \
|
child-src 'self' https://*.duosecurity.com https://*.duofederal.com; \
|
||||||
{allowed_iframe_ancestors}; \
|
frame-src 'self' https://*.duosecurity.com https://*.duofederal.com; \
|
||||||
img-src 'self' data: \
|
frame-ancestors 'self' \
|
||||||
https://haveibeenpwned.com \
|
chrome-extension://nngceckbapebfimnlniiiahkandclblb \
|
||||||
{icon_service_csp}; \
|
chrome-extension://jbkfoedolllekgbhcbcoahefnbanhhlh \
|
||||||
connect-src 'self' \
|
moz-extension://* \
|
||||||
https://api.pwnedpasswords.com \
|
{allowed_iframe_ancestors}; \
|
||||||
https://api.2fa.directory \
|
img-src 'self' data: \
|
||||||
https://app.simplelogin.io/api/ \
|
https://haveibeenpwned.com \
|
||||||
https://app.addy.io/api/ \
|
{icon_service_csp}; \
|
||||||
https://api.fastmail.com/ \
|
connect-src 'self' \
|
||||||
https://api.forwardemail.net \
|
https://api.pwnedpasswords.com \
|
||||||
{allowed_connect_src};\
|
https://api.2fa.directory \
|
||||||
",
|
https://app.simplelogin.io/api/ \
|
||||||
icon_service_csp = CONFIG._icon_service_csp(),
|
https://app.addy.io/api/ \
|
||||||
allowed_iframe_ancestors = CONFIG.allowed_iframe_ancestors(),
|
https://api.fastmail.com/ \
|
||||||
allowed_connect_src = CONFIG.allowed_connect_src(),
|
https://api.forwardemail.net \
|
||||||
);
|
{allowed_connect_src};\
|
||||||
|
",
|
||||||
|
icon_service_csp = CONFIG._icon_service_csp(),
|
||||||
|
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("Content-Security-Policy", csp);
|
||||||
res.set_raw_header("X-Frame-Options", "SAMEORIGIN");
|
res.set_raw_header("X-Frame-Options", "SAMEORIGIN");
|
||||||
} else {
|
} else {
|
||||||
@@ -752,9 +761,20 @@ pub fn convert_json_key_lcase_first(src_json: Value) -> Value {
|
|||||||
|
|
||||||
/// Parses the experimental client feature flags string into a HashMap.
|
/// Parses the experimental client feature flags string into a HashMap.
|
||||||
pub fn parse_experimental_client_feature_flags(experimental_client_feature_flags: &str) -> HashMap<String, bool> {
|
pub fn parse_experimental_client_feature_flags(experimental_client_feature_flags: &str) -> HashMap<String, bool> {
|
||||||
let feature_states = experimental_client_feature_flags.split(',').map(|f| (f.trim().to_owned(), true)).collect();
|
// These flags could still be configured, but are deprecated and not used anymore
|
||||||
|
// To prevent old installations from starting filter these out and not error out
|
||||||
feature_states
|
const DEPRECATED_FLAGS: &[&str] =
|
||||||
|
&["autofill-overlay", "autofill-v2", "browser-fileless-import", "extension-refresh", "fido2-vault-credentials"];
|
||||||
|
experimental_client_feature_flags
|
||||||
|
.split(',')
|
||||||
|
.filter_map(|f| {
|
||||||
|
let flag = f.trim();
|
||||||
|
if !flag.is_empty() && !DEPRECATED_FLAGS.contains(&flag) {
|
||||||
|
return Some((flag.to_owned(), true));
|
||||||
|
}
|
||||||
|
None
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// TODO: This is extracted from IpAddr::is_global, which is unstable:
|
/// TODO: This is extracted from IpAddr::is_global, which is unstable:
|
||||||
@@ -816,6 +836,26 @@ pub fn is_global(ip: std::net::IpAddr) -> bool {
|
|||||||
ip.is_global()
|
ip.is_global()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Saves a Rocket temporary file to the OpenDAL Operator at the given path.
|
||||||
|
pub async fn save_temp_file(
|
||||||
|
path_type: PathType,
|
||||||
|
path: &str,
|
||||||
|
temp_file: rocket::fs::TempFile<'_>,
|
||||||
|
overwrite: bool,
|
||||||
|
) -> Result<(), crate::Error> {
|
||||||
|
use futures::AsyncWriteExt as _;
|
||||||
|
use tokio_util::compat::TokioAsyncReadCompatExt as _;
|
||||||
|
|
||||||
|
let operator = CONFIG.opendal_operator_for_path_type(path_type)?;
|
||||||
|
|
||||||
|
let mut read_stream = temp_file.open().await?.compat();
|
||||||
|
let mut writer = operator.writer_with(path).if_not_exists(!overwrite).await?.into_futures_async_write();
|
||||||
|
futures::io::copy(&mut read_stream, &mut writer).await?;
|
||||||
|
writer.close().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// These are some tests to check that the implementations match
|
/// These are some tests to check that the implementations match
|
||||||
/// The IPv4 can be all checked in 30 seconds or so and they are correct as of nightly 2023-07-17
|
/// The IPv4 can be all checked in 30 seconds or so and they are correct as of nightly 2023-07-17
|
||||||
/// The IPV6 can't be checked in a reasonable time, so we check over a hundred billion random ones, so far correct
|
/// The IPV6 can't be checked in a reasonable time, so we check over a hundred billion random ones, so far correct
|
||||||
|
|||||||
Reference in New Issue
Block a user