mirror of
https://github.com/dani-garcia/vaultwarden.wiki.git
synced 2026-10-23 01:18:09 +03:00
Compare commits
157
Commits
1.35.2
...
df2cd3c869
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df2cd3c869 | ||
|
|
0e93d15b73 | ||
|
|
d1cbd027cd | ||
|
|
1f802f8e6a | ||
|
|
a2efadc650 | ||
|
|
acbf49018f | ||
|
|
b8089e31c2 | ||
|
|
8647af8f89 | ||
|
|
c687cacb6b | ||
|
|
8b56926077 | ||
|
|
de6d2066b3 | ||
|
|
415df400f4 | ||
|
|
42aa3ee1c7 | ||
|
|
7c188967e7 | ||
|
|
895ec2b1e7 | ||
|
|
1f99d9d276 | ||
|
|
f4f1a8e105 | ||
|
|
1c89e1772b | ||
|
|
3714b504f6 | ||
|
|
f9f011d50c | ||
|
|
cb89088bc3 | ||
|
|
4444442075 | ||
|
|
061694d0cb | ||
|
|
32098ca7d1 | ||
|
|
d2660324e6 | ||
|
|
cc67d644f6 | ||
|
|
3347698712 | ||
|
|
a24683636a | ||
|
|
64c56411f6 | ||
|
|
c3f5477525 | ||
|
|
9c8aa2359f | ||
|
|
8719f86fcf | ||
|
|
fddc8e71be | ||
|
|
eb212e23fa | ||
|
|
25dfedafd7 | ||
|
|
e992cbb4f5 | ||
|
|
5b51b60f94 | ||
|
|
de7abaaafa | ||
|
|
b7667e27bf | ||
|
|
f1c36b8c1d | ||
|
|
f1ff613008 | ||
|
|
57fbed1bed | ||
|
|
277e1536eb | ||
|
|
2ffad8775d | ||
|
|
32d85d03bb | ||
|
|
a6c3bd6d18 | ||
|
|
6729e83521 | ||
|
|
fdc156b247 | ||
|
|
2073c03092 | ||
|
|
923f5d0b5e | ||
|
|
83724b301e | ||
|
|
10e044f563 | ||
|
|
fa2566d14f | ||
|
|
46d71107f5 | ||
|
|
9e78911a2f | ||
|
|
0cefa4cca7 | ||
|
|
b30cc08562 | ||
|
|
55f883a566 | ||
|
|
74ceaf2354 | ||
|
|
2629bcbe13 | ||
|
|
46ae59eaf4 | ||
|
|
a6a88e7929 | ||
|
|
5040bcb7c0 | ||
|
|
660faee68e | ||
|
|
683a23e43c | ||
|
|
4a9bcb0694 | ||
|
|
169aa5efcc | ||
|
|
64d28ab66e | ||
|
|
4720cdbe86 | ||
|
|
5447ee6af2 | ||
|
|
5c5e8e1a6f | ||
|
|
7320a1db4b | ||
|
|
a058a35ccd | ||
|
|
a16b5afaaa | ||
|
|
fddc16d2b8 | ||
|
|
ec7fa137b7 | ||
|
|
b25f715364 | ||
|
|
d6a3d539ed | ||
|
|
d626ea81ab | ||
|
|
1ba2c6a26c | ||
|
|
22f5e0496c | ||
|
|
70f9dfbe8b | ||
|
|
54895ad4be | ||
|
|
a057c7deae | ||
|
|
2f85b62d2f | ||
|
|
9bc14e6e77 | ||
|
|
cdf711bb30 | ||
|
|
f21a3adae2 | ||
|
|
07aa377af7 | ||
|
|
14258caec9 | ||
|
|
cb46fcb948 | ||
|
|
b89648a136 | ||
|
|
c3bd1eb565 | ||
|
|
8c3c969938 | ||
|
|
38a6850b8d | ||
|
|
d297e274a3 | ||
|
|
a354e57659 | ||
|
|
5cc7360816 | ||
|
|
62748100f0 | ||
|
|
fcbdebd6d7 | ||
|
|
454b8e2a35 | ||
|
|
7883da554e | ||
|
|
fd2b6528a9 | ||
|
|
cc57e60886 | ||
|
|
e5681258f0 | ||
|
|
7cf0c5d67e | ||
|
|
b04ed75f9f | ||
|
|
0ed8ab68f7 | ||
|
|
dfebee57ec | ||
|
|
bfe420a018 | ||
|
|
e7e4b9a86d | ||
|
|
bb549986e6 | ||
|
|
39954af96a | ||
|
|
a6b43651ca | ||
|
|
3f28b583db | ||
|
|
d4f67429d6 | ||
|
|
fc43737868 | ||
|
|
43df0fb7f4 | ||
|
|
d29cd29f55 | ||
|
|
2811df2953 | ||
|
|
8f0e99b875 | ||
|
|
f07a91141a | ||
|
|
787822854c | ||
|
|
f62a7a66c8 | ||
|
|
3a1378f469 | ||
|
|
dde63e209e | ||
|
|
235cf88231 | ||
|
|
c0a78dd55a | ||
|
|
711bb53d3d | ||
|
|
650defac75 | ||
|
|
2b3736802d | ||
|
|
9c7df6412c | ||
|
|
065c1f2cd5 | ||
|
|
1a1d7f578a | ||
|
|
2b16a05e54 | ||
|
|
c6e9948984 | ||
|
|
ecdb18fcde | ||
|
|
df25d316d6 | ||
|
|
747286dccd | ||
|
|
e60105411b | ||
|
|
937857a0bc | ||
|
|
ba55191676 | ||
|
|
c555f7d198 | ||
|
|
74819b95bd | ||
|
|
da2af3d362 | ||
|
|
1583fe4af3 | ||
|
|
36f0620fd1 | ||
|
|
3cd2d4afe7 | ||
|
|
d09c45bb63 | ||
|
|
feecfb20da | ||
|
|
347279a12c | ||
|
|
7f65a254b3 | ||
|
|
cc80f689ed | ||
|
|
4737192853 | ||
|
|
0c6817cb4e | ||
|
|
25a71d913f | ||
|
|
b2cd556f3e |
No files matched your search
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
// Ignore everything
|
# Ignore everything
|
||||||
*
|
*
|
||||||
|
|
||||||
// Allow what is needed
|
# Allow what is needed
|
||||||
!.git
|
!.git
|
||||||
!docker/healthcheck.sh
|
!docker/healthcheck.sh
|
||||||
!docker/start.sh
|
!docker/start.sh
|
||||||
|
|||||||
+47
-20
@@ -50,10 +50,11 @@
|
|||||||
#########################
|
#########################
|
||||||
|
|
||||||
## Database URL
|
## Database URL
|
||||||
## When using SQLite, this is the path to the DB file, and it defaults to
|
## When using SQLite, this should use the sqlite:// scheme followed by the path
|
||||||
## %DATA_FOLDER%/db.sqlite3. If DATA_FOLDER is set to an external location, this
|
## to the DB file. It defaults to sqlite://%DATA_FOLDER%/db.sqlite3.
|
||||||
## must be set to a local sqlite3 file path.
|
## Bare paths without the sqlite:// scheme are supported for backwards compatibility,
|
||||||
# DATABASE_URL=data/db.sqlite3
|
## but only if the database file already exists.
|
||||||
|
# DATABASE_URL=sqlite://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
|
||||||
# DATABASE_URL=mysql://user:password@host[:port]/database_name
|
# DATABASE_URL=mysql://user:password@host[:port]/database_name
|
||||||
@@ -179,7 +180,7 @@
|
|||||||
## Defaults to every minute. Set blank to disable this job.
|
## Defaults to every minute. Set blank to disable this job.
|
||||||
# AUTH_REQUEST_PURGE_SCHEDULE="30 * * * * *"
|
# AUTH_REQUEST_PURGE_SCHEDULE="30 * * * * *"
|
||||||
##
|
##
|
||||||
## Cron schedule of the job that cleans expired Duo contexts from the database. Does nothing if Duo MFA is disabled or set to use the legacy iframe prompt.
|
## Cron schedule of the job that cleans expired Duo contexts from the database. Does nothing if Duo MFA is disabled.
|
||||||
## Defaults to every minute. Set blank to disable this job.
|
## Defaults to every minute. Set blank to disable this job.
|
||||||
# DUO_CONTEXT_PURGE_SCHEDULE="30 * * * * *"
|
# DUO_CONTEXT_PURGE_SCHEDULE="30 * * * * *"
|
||||||
#
|
#
|
||||||
@@ -315,6 +316,14 @@
|
|||||||
## unauthenticated access to potentially sensitive data.
|
## unauthenticated access to potentially sensitive data.
|
||||||
# SHOW_PASSWORD_HINT=false
|
# SHOW_PASSWORD_HINT=false
|
||||||
|
|
||||||
|
#########################
|
||||||
|
### Client settings ###
|
||||||
|
#########################
|
||||||
|
|
||||||
|
## Control whether clients onboarding interstitials are suppressed
|
||||||
|
## (post-login welcome dialogs, extension install prompts, setup extension redirects, and premium upsell modals)
|
||||||
|
# CLIENT_SUPPRESS_ONBOARDING=false
|
||||||
|
|
||||||
#########################
|
#########################
|
||||||
### Advanced settings ###
|
### Advanced settings ###
|
||||||
#########################
|
#########################
|
||||||
@@ -323,6 +332,16 @@
|
|||||||
## Set to the string "none" (without quotes), to disable any headers and just use the remote IP
|
## Set to the string "none" (without quotes), to disable any headers and just use the remote IP
|
||||||
# IP_HEADER=X-Real-IP
|
# IP_HEADER=X-Real-IP
|
||||||
|
|
||||||
|
## Which addresses the header above is accepted from, defaults to "local".
|
||||||
|
## Anyone able to reach Vaultwarden can set the header, and the client IP is used for the login and
|
||||||
|
## admin rate limits, so it is only trusted when the request comes from a proxy listed here.
|
||||||
|
## "local" accepts it from any non global address, which covers a reverse proxy running on the same
|
||||||
|
## host or container network. Use "all" to accept it from anywhere, or list the addresses of your
|
||||||
|
## proxy as IPs and CIDR ranges if it connects from a public address.
|
||||||
|
## For a list header like X-Forwarded-For, the client IP is the rightmost address that isn't one of
|
||||||
|
## these proxies, so with several proxies in a row (a CDN in front of nginx, for example) list all of them.
|
||||||
|
# IP_HEADER_TRUSTED_PROXIES=local
|
||||||
|
|
||||||
## Icon service
|
## Icon service
|
||||||
## The predefined icon services are: internal, bitwarden, duckduckgo, google.
|
## The predefined icon services are: internal, bitwarden, duckduckgo, google.
|
||||||
## To specify a custom icon service, set a URL template with exactly one instance of `{}`,
|
## To specify a custom icon service, set a URL template with exactly one instance of `{}`,
|
||||||
@@ -372,16 +391,19 @@
|
|||||||
## Note that clients cache the /api/config endpoint for about 1 hour and it could take some time before they are enabled or disabled!
|
## Note that clients cache the /api/config endpoint for about 1 hour and it could take some time before they are enabled or disabled!
|
||||||
##
|
##
|
||||||
## The following flags are available:
|
## The following flags are available:
|
||||||
## - "inline-menu-positioning-improvements": Enable the use of inline menu password generator and identity suggestions in the browser extension.
|
## - "pm-5594-safari-account-switching": Enable account switching in Safari. (Safari >= 2026.2.0)
|
||||||
## - "inline-menu-totp": Enable the use of inline menu TOTP codes in the browser extension.
|
## - "pm-32413-multi-client-password-management": Enable changing the master password directly in the client. (Desktop/Extension >= 2026.4.0)
|
||||||
## - "ssh-agent": Enable SSH agent support on Desktop. (Needs desktop >=2024.12.0)
|
## - "ssh-agent-v2": Enable newer SSH agent support. (Desktop >= 2026.2.1)
|
||||||
## - "ssh-key-vault-item": Enable the creation and use of SSH key vault items. (Needs clients >=2024.12.0)
|
## - "windows-desktop-autotype": Enable the autotype feature preview on Windows. (Desktop >= 2025.8.0)
|
||||||
## - "pm-25373-windows-biometrics-v2": Enable the new implementation of biometrics on Windows. (Needs desktop >= 2025.11.0)
|
## - "windows-desktop-autotype-ga": Enable the new, still in development, autotype implementation on Windows. (Desktop >= 2026.9.1)
|
||||||
## - "export-attachments": Enable support for exporting attachments (Clients >=2025.4.0)
|
## Only enable one of the two autotype flags, the clients disable autotype completely when both are enabled.
|
||||||
## - "anon-addy-self-host-alias": Enable configuring self-hosted Anon Addy alias generator. (Needs Android >=2025.3.0, iOS >=2025.4.0)
|
## - "pm-30529-webauthn-related-origins":
|
||||||
## - "simple-login-self-host-alias": Enable configuring self-hosted Simple Login alias generator. (Needs Android >=2025.3.0, iOS >=2025.4.0)
|
## - "pm-32009-new-item-types": Enable new item types: Bank Account, Driver's License, and Passport (Clients >= 2026.4.0)
|
||||||
## - "mutual-tls": Enable the use of mutual TLS on Android (Client >= 2025.2.0)
|
## - "pm-34171-card-scanner": Enable the new card scanner feature on mobile (Android >= 2026.4.1, iOS >= 2026.4.1)
|
||||||
# EXPERIMENTAL_CLIENT_FEATURE_FLAGS=fido2-vault-credentials
|
## - "enable-basic-auth-response": Enable HTTP Basic Auth autofill in the browser extension (Browser >= 2026.9.0)
|
||||||
|
## - "undetermined-cipher-scenario-logic": Enable the rewritten add/update login notification triggering logic in the browser extension (Browser >= 2026.2.0)
|
||||||
|
## - "biometrics-sdk-ipc": Enable biometric unlock over the SDK IPC framework (Desktop >= 2026.9.0, Browser >= 2026.9.0)
|
||||||
|
# EXPERIMENTAL_CLIENT_FEATURE_FLAGS=
|
||||||
|
|
||||||
## Require new device emails. When a user logs in an email is required to be sent.
|
## Require new device emails. When a user logs in an email is required to be sent.
|
||||||
## If sending the email fails the login attempt will fail!!
|
## If sending the email fails the login attempt will fail!!
|
||||||
@@ -453,6 +475,13 @@
|
|||||||
## Note that this applies to both the login and the 2FA, so it's recommended to allow a burst size of at least 2.
|
## Note that this applies to both the login and the 2FA, so it's recommended to allow a burst size of at least 2.
|
||||||
# LOGIN_RATELIMIT_MAX_BURST=10
|
# LOGIN_RATELIMIT_MAX_BURST=10
|
||||||
|
|
||||||
|
## Number of seconds, on average, between requests from the same IP address to one of the rate limited
|
||||||
|
## unauthenticated endpoints, like the password hint, the account recovery mails or accessing a Send.
|
||||||
|
# UNAUTHENTICATED_RATELIMIT_SECONDS=60
|
||||||
|
## Allow a burst of requests of up to this size, while maintaining the average indicated by `UNAUTHENTICATED_RATELIMIT_SECONDS`.
|
||||||
|
## This budget is shared between all of those endpoints, so it is more lenient than the login one.
|
||||||
|
# UNAUTHENTICATED_RATELIMIT_MAX_BURST=50
|
||||||
|
|
||||||
## BETA FEATURE: Groups
|
## BETA FEATURE: Groups
|
||||||
## Controls whether group support is enabled for organizations
|
## Controls whether group support is enabled for organizations
|
||||||
## This setting applies to organizations.
|
## This setting applies to organizations.
|
||||||
@@ -487,6 +516,9 @@
|
|||||||
## Prevent users from logging in directly without going through SSO
|
## Prevent users from logging in directly without going through SSO
|
||||||
# SSO_ONLY=false
|
# SSO_ONLY=false
|
||||||
|
|
||||||
|
## Allow SSO flow to create account. You probably want to disable it when using a public provider.
|
||||||
|
# SSO_SIGNUPS_ALLOWED=true
|
||||||
|
|
||||||
## On SSO Signup if a user with a matching email already exists make the association
|
## On SSO Signup if a user with a matching email already exists make the association
|
||||||
# SSO_SIGNUPS_MATCH_EMAIL=true
|
# SSO_SIGNUPS_MATCH_EMAIL=true
|
||||||
|
|
||||||
@@ -549,11 +581,6 @@
|
|||||||
# DUO_HOST=<API Hostname>
|
# DUO_HOST=<API Hostname>
|
||||||
## After that, you should be able to follow the rest of the guide linked above,
|
## After that, you should be able to follow the rest of the guide linked above,
|
||||||
## ignoring the fields that ask for the values that you already configured beforehand.
|
## ignoring the fields that ask for the values that you already configured beforehand.
|
||||||
##
|
|
||||||
## If you want to attempt to use Duo's 'Traditional Prompt' (deprecated, iframe based) set DUO_USE_IFRAME to 'true'.
|
|
||||||
## Duo no longer supports this, but it still works for some integrations.
|
|
||||||
## If you aren't sure, leave this alone.
|
|
||||||
# DUO_USE_IFRAME=false
|
|
||||||
|
|
||||||
## Email 2FA settings
|
## Email 2FA settings
|
||||||
## Email token size
|
## Email token size
|
||||||
|
|||||||
@@ -1,3 +1,2 @@
|
|||||||
# Ignore vendored scripts in GitHub stats
|
# Ignore vendored scripts in GitHub stats
|
||||||
src/static/scripts/* linguist-vendored
|
src/static/scripts/* linguist-vendored
|
||||||
|
|
||||||
+22
-24
@@ -1,6 +1,10 @@
|
|||||||
name: Build
|
name: Build
|
||||||
permissions: {}
|
permissions: {}
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
paths:
|
paths:
|
||||||
@@ -30,6 +34,10 @@ on:
|
|||||||
- "docker/DockerSettings.yaml"
|
- "docker/DockerSettings.yaml"
|
||||||
- "macros/**"
|
- "macros/**"
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build and Test ${{ matrix.channel }}
|
name: Build and Test ${{ matrix.channel }}
|
||||||
@@ -54,7 +62,7 @@ jobs:
|
|||||||
|
|
||||||
# Checkout the repo
|
# Checkout the repo
|
||||||
- name: "Checkout"
|
- name: "Checkout"
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 #v6.0.0
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -63,7 +71,6 @@ jobs:
|
|||||||
# Determine rust-toolchain version
|
# Determine rust-toolchain version
|
||||||
- name: Init Variables
|
- name: Init Variables
|
||||||
id: toolchain
|
id: toolchain
|
||||||
shell: bash
|
|
||||||
env:
|
env:
|
||||||
CHANNEL: ${{ matrix.channel }}
|
CHANNEL: ${{ matrix.channel }}
|
||||||
run: |
|
run: |
|
||||||
@@ -78,32 +85,23 @@ jobs:
|
|||||||
# End Determine rust-toolchain version
|
# End Determine rust-toolchain version
|
||||||
|
|
||||||
|
|
||||||
# Only install the clippy and rustfmt components on the default rust-toolchain
|
- name: "Install toolchain ${{steps.toolchain.outputs.RUST_TOOLCHAIN}} as default"
|
||||||
- name: "Install rust-toolchain version"
|
|
||||||
uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master @ Dec 16, 2025, 6:11 PM GMT+1
|
|
||||||
if: ${{ matrix.channel == 'rust-toolchain' }}
|
|
||||||
with:
|
|
||||||
toolchain: "${{steps.toolchain.outputs.RUST_TOOLCHAIN}}"
|
|
||||||
components: clippy, rustfmt
|
|
||||||
# End Uses the rust-toolchain file to determine version
|
|
||||||
|
|
||||||
|
|
||||||
# Install the any other channel to be used for which we do not execute clippy and rustfmt
|
|
||||||
- name: "Install MSRV version"
|
|
||||||
uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master @ Dec 16, 2025, 6:11 PM GMT+1
|
|
||||||
if: ${{ matrix.channel != 'rust-toolchain' }}
|
|
||||||
with:
|
|
||||||
toolchain: "${{steps.toolchain.outputs.RUST_TOOLCHAIN}}"
|
|
||||||
# End Install the MSRV channel to be used
|
|
||||||
|
|
||||||
# Set the current matrix toolchain version as default
|
|
||||||
- name: "Set toolchain ${{steps.toolchain.outputs.RUST_TOOLCHAIN}} as default"
|
|
||||||
env:
|
env:
|
||||||
|
CHANNEL: ${{ matrix.channel }}
|
||||||
RUST_TOOLCHAIN: ${{steps.toolchain.outputs.RUST_TOOLCHAIN}}
|
RUST_TOOLCHAIN: ${{steps.toolchain.outputs.RUST_TOOLCHAIN}}
|
||||||
run: |
|
run: |
|
||||||
# Remove the rust-toolchain.toml
|
# Remove the rust-toolchain.toml
|
||||||
rm rust-toolchain.toml
|
rm rust-toolchain.toml
|
||||||
# Set the default
|
|
||||||
|
# Install the correct toolchain version
|
||||||
|
rustup toolchain install "${RUST_TOOLCHAIN}" --profile minimal --no-self-update
|
||||||
|
|
||||||
|
# If this matrix is the `rust-toolchain` flow, also install rustfmt and clippy
|
||||||
|
if [[ "${CHANNEL}" == 'rust-toolchain' ]]; then
|
||||||
|
rustup component add --toolchain "${RUST_TOOLCHAIN}" rustfmt clippy
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Set as the default toolchain
|
||||||
rustup default "${RUST_TOOLCHAIN}"
|
rustup default "${RUST_TOOLCHAIN}"
|
||||||
|
|
||||||
# Show environment
|
# Show environment
|
||||||
@@ -115,7 +113,7 @@ jobs:
|
|||||||
|
|
||||||
# Enable Rust Caching
|
# Enable Rust Caching
|
||||||
- name: Rust Caching
|
- name: Rust Caching
|
||||||
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||||
with:
|
with:
|
||||||
# Use a custom prefix-key to force a fresh start. This is sometimes needed with bigger changes.
|
# Use a custom prefix-key to force a fresh start. This is sometimes needed with bigger changes.
|
||||||
# Like changing the build host from Ubuntu 20.04 to 22.04 for example.
|
# Like changing the build host from Ubuntu 20.04 to 22.04 for example.
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
name: Check templates
|
name: Check templates
|
||||||
permissions: {}
|
permissions: {}
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
on: [ push, pull_request ]
|
on: [ push, pull_request ]
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docker-templates:
|
docker-templates:
|
||||||
name: Validate docker templates
|
name: Validate docker templates
|
||||||
@@ -12,7 +20,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
# Checkout the repo
|
# Checkout the repo
|
||||||
- name: "Checkout"
|
- name: "Checkout"
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 #v6.0.0
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
# End Checkout the repo
|
# End Checkout the repo
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
name: Hadolint
|
name: Hadolint
|
||||||
|
|
||||||
on: [ push, pull_request ]
|
|
||||||
permissions: {}
|
permissions: {}
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
on: [ push, pull_request ]
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
hadolint:
|
hadolint:
|
||||||
@@ -13,7 +20,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
# Start Docker Buildx
|
# Start Docker Buildx
|
||||||
- name: Setup Docker Buildx
|
- name: Setup Docker Buildx
|
||||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.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:
|
||||||
@@ -23,31 +30,29 @@ jobs:
|
|||||||
driver-opts: |
|
driver-opts: |
|
||||||
network=host
|
network=host
|
||||||
|
|
||||||
# Download hadolint - https://github.com/hadolint/hadolint/releases
|
|
||||||
- name: Download hadolint
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
sudo curl -L https://github.com/hadolint/hadolint/releases/download/v${HADOLINT_VERSION}/hadolint-$(uname -s)-$(uname -m) -o /usr/local/bin/hadolint && \
|
|
||||||
sudo chmod +x /usr/local/bin/hadolint
|
|
||||||
env:
|
|
||||||
HADOLINT_VERSION: 2.14.0
|
|
||||||
# End Download hadolint
|
|
||||||
# Checkout the repo
|
# Checkout the repo
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 #v6.0.0
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
# End Checkout the repo
|
# End Checkout the repo
|
||||||
|
|
||||||
# Test Dockerfiles with hadolint
|
# Test Dockerfiles with hadolint
|
||||||
- name: Run hadolint
|
# Uses the Docker-based action (hadolint pre-bundled in ghcr.io/hadolint/hadolint:v2.14.0-debian)
|
||||||
shell: bash
|
# so no binary is downloaded at runtime. Pinned by commit SHA for supply-chain safety.
|
||||||
run: hadolint docker/Dockerfile.{debian,alpine}
|
- name: Run hadolint on Dockerfile.debian
|
||||||
|
uses: hadolint/hadolint-action@06be81baf89a55ffd0e24b8f04a4185738dd3387 # v3.5.0
|
||||||
|
with:
|
||||||
|
dockerfile: docker/Dockerfile.debian
|
||||||
|
|
||||||
|
- name: Run hadolint on Dockerfile.alpine
|
||||||
|
uses: hadolint/hadolint-action@06be81baf89a55ffd0e24b8f04a4185738dd3387 # v3.5.0
|
||||||
|
with:
|
||||||
|
dockerfile: docker/Dockerfile.alpine
|
||||||
# End Test Dockerfiles with hadolint
|
# End Test Dockerfiles with hadolint
|
||||||
|
|
||||||
# Test Dockerfiles with docker build checks
|
# Test Dockerfiles with docker build checks
|
||||||
- name: Run docker build check
|
- name: Run docker build check
|
||||||
shell: bash
|
|
||||||
run: |
|
run: |
|
||||||
echo "Checking docker/Dockerfile.debian"
|
echo "Checking docker/Dockerfile.debian"
|
||||||
docker build --check . -f docker/Dockerfile.debian
|
docker build --check . -f docker/Dockerfile.debian
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
name: Release
|
name: Release
|
||||||
permissions: {}
|
permissions: {}
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
# Apply concurrency control only on the upstream repo
|
||||||
|
group: ${{ github.repository == 'dani-garcia/vaultwarden' && format('{0}-{1}', github.workflow, github.ref) || github.run_id }}
|
||||||
|
# Don't cancel other runs when creating a tag
|
||||||
|
cancel-in-progress: ${{ github.ref_type == 'branch' }}
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
@@ -10,33 +16,31 @@ on:
|
|||||||
# https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet
|
# https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet
|
||||||
- '[1-2].[0-9]+.[0-9]+'
|
- '[1-2].[0-9]+.[0-9]+'
|
||||||
|
|
||||||
concurrency:
|
|
||||||
# Apply concurrency control only on the upstream repo
|
|
||||||
group: ${{ github.repository == 'dani-garcia/vaultwarden' && format('{0}-{1}', github.workflow, github.ref) || github.run_id }}
|
|
||||||
# Don't cancel other runs when creating a tag
|
|
||||||
cancel-in-progress: ${{ github.ref_type == 'branch' }}
|
|
||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
env:
|
# A "release" environment must be created in the repository settings
|
||||||
# The *_REPO variables need to be configured as repository variables
|
# (Settings > Environments > New environment) with the following
|
||||||
# Append `/settings/variables/actions` to your repo url
|
# variables and secrets configured as needed.
|
||||||
# DOCKERHUB_REPO needs to be 'index.docker.io/<user>/<repo>'
|
#
|
||||||
# Check for Docker hub credentials in secrets
|
# Variables (only set the ones for registries you want to push to):
|
||||||
HAVE_DOCKERHUB_LOGIN: ${{ vars.DOCKERHUB_REPO != '' && secrets.DOCKERHUB_USERNAME != '' && secrets.DOCKERHUB_TOKEN != '' }}
|
# DOCKERHUB_REPO: 'index.docker.io/<user>/<repo>'
|
||||||
# GHCR_REPO needs to be 'ghcr.io/<user>/<repo>'
|
# QUAY_REPO: 'quay.io/<user>/<repo>'
|
||||||
# Check for Github credentials in secrets
|
# GHCR_REPO: 'ghcr.io/<user>/<repo>'
|
||||||
HAVE_GHCR_LOGIN: ${{ vars.GHCR_REPO != '' && github.repository_owner != '' && secrets.GITHUB_TOKEN != '' }}
|
#
|
||||||
# QUAY_REPO needs to be 'quay.io/<user>/<repo>'
|
# Secrets (only required when the corresponding *_REPO variable is set):
|
||||||
# Check for Quay.io credentials in secrets
|
# DOCKERHUB_REPO => DOCKERHUB_USERNAME, DOCKERHUB_TOKEN
|
||||||
HAVE_QUAY_LOGIN: ${{ vars.QUAY_REPO != '' && secrets.QUAY_USERNAME != '' && secrets.QUAY_TOKEN != '' }}
|
# QUAY_REPO => QUAY_USERNAME, QUAY_TOKEN
|
||||||
|
# GITHUB_TOKEN is provided automatically
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docker-build:
|
docker-build:
|
||||||
name: Build Vaultwarden containers
|
name: Build Vaultwarden containers
|
||||||
if: ${{ github.repository == 'dani-garcia/vaultwarden' }}
|
if: ${{ github.repository == 'dani-garcia/vaultwarden' }}
|
||||||
|
environment:
|
||||||
|
name: release
|
||||||
|
deployment: false
|
||||||
permissions:
|
permissions:
|
||||||
packages: write # Needed to upload packages and artifacts
|
packages: write # Needed to upload packages and artifacts
|
||||||
contents: read
|
contents: read
|
||||||
@@ -54,13 +58,13 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Initialize QEMU binfmt support
|
- name: Initialize QEMU binfmt support
|
||||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
|
||||||
with:
|
with:
|
||||||
platforms: "arm64,arm"
|
platforms: "arm64,arm"
|
||||||
|
|
||||||
# Start Docker Buildx
|
# Start Docker Buildx
|
||||||
- name: Setup Docker Buildx
|
- name: Setup Docker Buildx
|
||||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.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:
|
||||||
@@ -73,7 +77,7 @@ jobs:
|
|||||||
|
|
||||||
# Checkout the repo
|
# Checkout the repo
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 #v6.0.0
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
# We need fetch-depth of 0 so we also get all the tag metadata
|
# We need fetch-depth of 0 so we also get all the tag metadata
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
@@ -102,14 +106,14 @@ jobs:
|
|||||||
|
|
||||||
# Login to Docker Hub
|
# Login to Docker Hub
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
if: ${{ env.HAVE_DOCKERHUB_LOGIN == 'true' }}
|
if: ${{ vars.DOCKERHUB_REPO != '' }}
|
||||||
|
|
||||||
- name: Add registry for DockerHub
|
- name: Add registry for DockerHub
|
||||||
if: ${{ env.HAVE_DOCKERHUB_LOGIN == 'true' }}
|
if: ${{ vars.DOCKERHUB_REPO != '' }}
|
||||||
env:
|
env:
|
||||||
DOCKERHUB_REPO: ${{ vars.DOCKERHUB_REPO }}
|
DOCKERHUB_REPO: ${{ vars.DOCKERHUB_REPO }}
|
||||||
run: |
|
run: |
|
||||||
@@ -117,15 +121,15 @@ jobs:
|
|||||||
|
|
||||||
# Login to GitHub Container Registry
|
# Login to GitHub Container Registry
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
if: ${{ env.HAVE_GHCR_LOGIN == 'true' }}
|
if: ${{ vars.GHCR_REPO != '' }}
|
||||||
|
|
||||||
- name: Add registry for ghcr.io
|
- name: Add registry for ghcr.io
|
||||||
if: ${{ env.HAVE_GHCR_LOGIN == 'true' }}
|
if: ${{ vars.GHCR_REPO != '' }}
|
||||||
env:
|
env:
|
||||||
GHCR_REPO: ${{ vars.GHCR_REPO }}
|
GHCR_REPO: ${{ vars.GHCR_REPO }}
|
||||||
run: |
|
run: |
|
||||||
@@ -133,15 +137,15 @@ jobs:
|
|||||||
|
|
||||||
# Login to Quay.io
|
# Login to Quay.io
|
||||||
- name: Login to Quay.io
|
- name: Login to Quay.io
|
||||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||||
with:
|
with:
|
||||||
registry: quay.io
|
registry: quay.io
|
||||||
username: ${{ secrets.QUAY_USERNAME }}
|
username: ${{ secrets.QUAY_USERNAME }}
|
||||||
password: ${{ secrets.QUAY_TOKEN }}
|
password: ${{ secrets.QUAY_TOKEN }}
|
||||||
if: ${{ env.HAVE_QUAY_LOGIN == 'true' }}
|
if: ${{ vars.QUAY_REPO != '' }}
|
||||||
|
|
||||||
- name: Add registry for Quay.io
|
- name: Add registry for Quay.io
|
||||||
if: ${{ env.HAVE_QUAY_LOGIN == 'true' }}
|
if: ${{ vars.QUAY_REPO != '' }}
|
||||||
env:
|
env:
|
||||||
QUAY_REPO: ${{ vars.QUAY_REPO }}
|
QUAY_REPO: ${{ vars.QUAY_REPO }}
|
||||||
run: |
|
run: |
|
||||||
@@ -155,7 +159,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
#
|
#
|
||||||
# Check if there is a GitHub Container Registry Login and use it for caching
|
# Check if there is a GitHub Container Registry Login and use it for caching
|
||||||
if [[ -n "${HAVE_GHCR_LOGIN}" ]]; then
|
if [[ -n "${GHCR_REPO}" ]]; then
|
||||||
echo "BAKE_CACHE_FROM=type=registry,ref=${GHCR_REPO}-buildcache:${BASE_IMAGE}-${NORMALIZED_ARCH}" | tee -a "${GITHUB_ENV}"
|
echo "BAKE_CACHE_FROM=type=registry,ref=${GHCR_REPO}-buildcache:${BASE_IMAGE}-${NORMALIZED_ARCH}" | tee -a "${GITHUB_ENV}"
|
||||||
echo "BAKE_CACHE_TO=type=registry,ref=${GHCR_REPO}-buildcache:${BASE_IMAGE}-${NORMALIZED_ARCH},compression=zstd,mode=max" | tee -a "${GITHUB_ENV}"
|
echo "BAKE_CACHE_TO=type=registry,ref=${GHCR_REPO}-buildcache:${BASE_IMAGE}-${NORMALIZED_ARCH},compression=zstd,mode=max" | tee -a "${GITHUB_ENV}"
|
||||||
else
|
else
|
||||||
@@ -181,7 +185,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Bake ${{ matrix.base_image }} containers
|
- name: Bake ${{ matrix.base_image }} containers
|
||||||
id: bake_vw
|
id: bake_vw
|
||||||
uses: docker/bake-action@5be5f02ff8819ecd3092ea6b2e6261c31774f2b4 # v6.10.0
|
uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0
|
||||||
env:
|
env:
|
||||||
BASE_TAGS: "${{ steps.determine-version.outputs.BASE_TAGS }}"
|
BASE_TAGS: "${{ steps.determine-version.outputs.BASE_TAGS }}"
|
||||||
SOURCE_COMMIT: "${{ env.SOURCE_COMMIT }}"
|
SOURCE_COMMIT: "${{ env.SOURCE_COMMIT }}"
|
||||||
@@ -218,7 +222,7 @@ jobs:
|
|||||||
touch "${RUNNER_TEMP}/digests/${digest#sha256:}"
|
touch "${RUNNER_TEMP}/digests/${digest#sha256:}"
|
||||||
|
|
||||||
- name: Upload digest
|
- name: Upload digest
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: digests-${{ env.NORMALIZED_ARCH }}-${{ matrix.base_image }}
|
name: digests-${{ env.NORMALIZED_ARCH }}-${{ matrix.base_image }}
|
||||||
path: ${{ runner.temp }}/digests/*
|
path: ${{ runner.temp }}/digests/*
|
||||||
@@ -233,20 +237,23 @@ jobs:
|
|||||||
|
|
||||||
# Upload artifacts to Github Actions and Attest the binaries
|
# Upload artifacts to Github Actions and Attest the binaries
|
||||||
- name: Attest binaries
|
- name: Attest binaries
|
||||||
uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3.1.0
|
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
|
||||||
with:
|
with:
|
||||||
subject-path: vaultwarden-${{ env.NORMALIZED_ARCH }}
|
subject-path: vaultwarden-${{ env.NORMALIZED_ARCH }}
|
||||||
|
|
||||||
- name: Upload binaries as artifacts
|
- name: Upload binaries as artifacts
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-${{ env.NORMALIZED_ARCH }}-${{ matrix.base_image }}
|
name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-${{ env.NORMALIZED_ARCH }}-${{ matrix.base_image }}
|
||||||
path: vaultwarden-${{ env.NORMALIZED_ARCH }}
|
path: vaultwarden-${{ env.NORMALIZED_ARCH }}
|
||||||
|
|
||||||
merge-manifests:
|
merge-manifests:
|
||||||
name: Merge manifests
|
name: Merge manifests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
needs: docker-build
|
needs: docker-build
|
||||||
|
environment:
|
||||||
|
name: release
|
||||||
|
deployment: false
|
||||||
permissions:
|
permissions:
|
||||||
packages: write # Needed to upload packages and artifacts
|
packages: write # Needed to upload packages and artifacts
|
||||||
attestations: write # Needed to generate an artifact attestation for a build
|
attestations: write # Needed to generate an artifact attestation for a build
|
||||||
@@ -257,7 +264,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Download digests
|
- name: Download digests
|
||||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
with:
|
with:
|
||||||
path: ${{ runner.temp }}/digests
|
path: ${{ runner.temp }}/digests
|
||||||
pattern: digests-*-${{ matrix.base_image }}
|
pattern: digests-*-${{ matrix.base_image }}
|
||||||
@@ -265,14 +272,14 @@ jobs:
|
|||||||
|
|
||||||
# Login to Docker Hub
|
# Login to Docker Hub
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
if: ${{ env.HAVE_DOCKERHUB_LOGIN == 'true' }}
|
if: ${{ vars.DOCKERHUB_REPO != '' }}
|
||||||
|
|
||||||
- name: Add registry for DockerHub
|
- name: Add registry for DockerHub
|
||||||
if: ${{ env.HAVE_DOCKERHUB_LOGIN == 'true' }}
|
if: ${{ vars.DOCKERHUB_REPO != '' }}
|
||||||
env:
|
env:
|
||||||
DOCKERHUB_REPO: ${{ vars.DOCKERHUB_REPO }}
|
DOCKERHUB_REPO: ${{ vars.DOCKERHUB_REPO }}
|
||||||
run: |
|
run: |
|
||||||
@@ -280,15 +287,15 @@ jobs:
|
|||||||
|
|
||||||
# Login to GitHub Container Registry
|
# Login to GitHub Container Registry
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
if: ${{ env.HAVE_GHCR_LOGIN == 'true' }}
|
if: ${{ vars.GHCR_REPO != '' }}
|
||||||
|
|
||||||
- name: Add registry for ghcr.io
|
- name: Add registry for ghcr.io
|
||||||
if: ${{ env.HAVE_GHCR_LOGIN == 'true' }}
|
if: ${{ vars.GHCR_REPO != '' }}
|
||||||
env:
|
env:
|
||||||
GHCR_REPO: ${{ vars.GHCR_REPO }}
|
GHCR_REPO: ${{ vars.GHCR_REPO }}
|
||||||
run: |
|
run: |
|
||||||
@@ -296,15 +303,15 @@ jobs:
|
|||||||
|
|
||||||
# Login to Quay.io
|
# Login to Quay.io
|
||||||
- name: Login to Quay.io
|
- name: Login to Quay.io
|
||||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||||
with:
|
with:
|
||||||
registry: quay.io
|
registry: quay.io
|
||||||
username: ${{ secrets.QUAY_USERNAME }}
|
username: ${{ secrets.QUAY_USERNAME }}
|
||||||
password: ${{ secrets.QUAY_TOKEN }}
|
password: ${{ secrets.QUAY_TOKEN }}
|
||||||
if: ${{ env.HAVE_QUAY_LOGIN == 'true' }}
|
if: ${{ vars.QUAY_REPO != '' }}
|
||||||
|
|
||||||
- name: Add registry for Quay.io
|
- name: Add registry for Quay.io
|
||||||
if: ${{ env.HAVE_QUAY_LOGIN == 'true' }}
|
if: ${{ vars.QUAY_REPO != '' }}
|
||||||
env:
|
env:
|
||||||
QUAY_REPO: ${{ vars.QUAY_REPO }}
|
QUAY_REPO: ${{ vars.QUAY_REPO }}
|
||||||
run: |
|
run: |
|
||||||
@@ -357,24 +364,24 @@ jobs:
|
|||||||
|
|
||||||
# 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' && env.DIGEST_SHA != ''}}
|
if: ${{ vars.DOCKERHUB_REPO != '' && env.DIGEST_SHA != ''}}
|
||||||
uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3.1.0
|
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
|
||||||
with:
|
with:
|
||||||
subject-name: ${{ vars.DOCKERHUB_REPO }}
|
subject-name: ${{ vars.DOCKERHUB_REPO }}
|
||||||
subject-digest: ${{ env.DIGEST_SHA }}
|
subject-digest: ${{ env.DIGEST_SHA }}
|
||||||
push-to-registry: true
|
push-to-registry: true
|
||||||
|
|
||||||
- name: Attest - ghcr.io - ${{ matrix.base_image }}
|
- name: Attest - ghcr.io - ${{ matrix.base_image }}
|
||||||
if: ${{ env.HAVE_GHCR_LOGIN == 'true' && env.DIGEST_SHA != ''}}
|
if: ${{ vars.GHCR_REPO != '' && env.DIGEST_SHA != ''}}
|
||||||
uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3.1.0
|
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
|
||||||
with:
|
with:
|
||||||
subject-name: ${{ vars.GHCR_REPO }}
|
subject-name: ${{ vars.GHCR_REPO }}
|
||||||
subject-digest: ${{ env.DIGEST_SHA }}
|
subject-digest: ${{ env.DIGEST_SHA }}
|
||||||
push-to-registry: true
|
push-to-registry: true
|
||||||
|
|
||||||
- name: Attest - quay.io - ${{ matrix.base_image }}
|
- name: Attest - quay.io - ${{ matrix.base_image }}
|
||||||
if: ${{ env.HAVE_QUAY_LOGIN == 'true' && env.DIGEST_SHA != ''}}
|
if: ${{ vars.QUAY_REPO != '' && env.DIGEST_SHA != ''}}
|
||||||
uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3.1.0
|
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
|
||||||
with:
|
with:
|
||||||
subject-name: ${{ vars.QUAY_REPO }}
|
subject-name: ${{ vars.QUAY_REPO }}
|
||||||
subject-digest: ${{ env.DIGEST_SHA }}
|
subject-digest: ${{ env.DIGEST_SHA }}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
name: Cleanup
|
name: Cleanup
|
||||||
permissions: {}
|
permissions: {}
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
name: Trivy
|
name: Trivy
|
||||||
permissions: {}
|
permissions: {}
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
@@ -29,12 +33,12 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 #v6.0.0
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Run Trivy vulnerability scanner
|
- name: Run Trivy vulnerability scanner
|
||||||
uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # 0.33.1
|
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.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
|
||||||
@@ -46,6 +50,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@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
|
uses: github/codeql-action/upload-sarif@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
|
||||||
with:
|
with:
|
||||||
sarif_file: 'trivy-results.sarif'
|
sarif_file: 'trivy-results.sarif'
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
name: Code Spell Checking
|
name: Code Spell Checking
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
on: [ push, pull_request ]
|
on: [ push, pull_request ]
|
||||||
permissions: {}
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
typos:
|
typos:
|
||||||
@@ -12,11 +16,11 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
# Checkout the repo
|
# Checkout the repo
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 #v6.0.0
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
# End Checkout the repo
|
# End Checkout the repo
|
||||||
|
|
||||||
# When this version is updated, do not forget to update this in `.pre-commit-config.yaml` too
|
# When this version is updated, do not forget to update this in `.pre-commit-config.yaml` too
|
||||||
- name: Spell Check Repo
|
- name: Spell Check Repo
|
||||||
uses: crate-ci/typos@1a319b54cc9e3b333fed6a5c88ba1a90324da514 # v1.40.1
|
uses: crate-ci/typos@00f422f3b19c57bc6338715ebfe3316d38768461 # v1.50.3
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
name: Security Analysis with zizmor
|
name: Security Analysis with zizmor
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -6,22 +11,20 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches: ["**"]
|
branches: ["**"]
|
||||||
|
|
||||||
permissions: {}
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
zizmor:
|
zizmor:
|
||||||
name: Run zizmor
|
name: Run zizmor
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
permissions:
|
permissions:
|
||||||
security-events: write # To write the security report
|
security-events: write # To write the security report
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 #v6.0.0
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Run zizmor
|
- name: Run zizmor
|
||||||
uses: zizmorcore/zizmor-action@e639db99335bc9038abc0e066dfcd72e23d26fb4 # v0.3.0
|
uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482 # v0.6.4
|
||||||
with:
|
with:
|
||||||
# intentionally not scanning the entire repository,
|
# intentionally not scanning the entire repository,
|
||||||
# since it contains integration tests.
|
# since it contains integration tests.
|
||||||
|
|||||||
+54
-53
@@ -1,58 +1,59 @@
|
|||||||
---
|
---
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # v6.0.0
|
rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # v6.0.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
- id: check-json
|
- id: check-json
|
||||||
- id: check-toml
|
- id: check-toml
|
||||||
- id: mixed-line-ending
|
- id: mixed-line-ending
|
||||||
args: ["--fix=no"]
|
args: [ "--fix=no" ]
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
exclude: "(.*js$|.*css$)"
|
exclude: "(.*js$|.*css$)"
|
||||||
- id: check-case-conflict
|
- id: check-case-conflict
|
||||||
- id: check-merge-conflict
|
- id: check-merge-conflict
|
||||||
- id: detect-private-key
|
- id: detect-private-key
|
||||||
- id: check-symlinks
|
- id: check-symlinks
|
||||||
- id: forbid-submodules
|
- id: forbid-submodules
|
||||||
- repo: local
|
|
||||||
|
# When this version is updated, do not forget to update this in `.github/workflows/typos.yaml` too
|
||||||
|
- repo: https://github.com/crate-ci/typos
|
||||||
|
rev: 00f422f3b19c57bc6338715ebfe3316d38768461 # v1.50.3
|
||||||
hooks:
|
hooks:
|
||||||
- id: fmt
|
- id: typos
|
||||||
name: fmt
|
always_run: true
|
||||||
description: Format files with cargo fmt.
|
|
||||||
entry: cargo fmt
|
- repo: local
|
||||||
language: system
|
hooks:
|
||||||
always_run: true
|
- id: fmt
|
||||||
pass_filenames: false
|
name: fmt
|
||||||
args: ["--", "--check"]
|
description: Format files with cargo fmt.
|
||||||
- id: cargo-test
|
entry: cargo fmt
|
||||||
name: cargo test
|
language: system
|
||||||
description: Test the package for errors.
|
always_run: true
|
||||||
entry: cargo test
|
pass_filenames: false
|
||||||
language: system
|
args: [ "--", "--check" ]
|
||||||
args: ["--features", "sqlite,mysql,postgresql", "--"]
|
- id: cargo-test
|
||||||
types_or: [rust, file]
|
name: cargo test
|
||||||
files: (Cargo.toml|Cargo.lock|rust-toolchain.toml|rustfmt.toml|.*\.rs$)
|
description: Test the package for errors.
|
||||||
pass_filenames: false
|
entry: cargo test
|
||||||
- id: cargo-clippy
|
language: system
|
||||||
name: cargo clippy
|
args: [ "--features", "sqlite,mysql,postgresql", "--" ]
|
||||||
description: Lint Rust sources
|
types_or: [ rust, toml ] # Cargo.lock matches toml type which is intended
|
||||||
entry: cargo clippy
|
pass_filenames: false
|
||||||
language: system
|
- id: cargo-clippy
|
||||||
args: ["--features", "sqlite,mysql,postgresql", "--", "-D", "warnings"]
|
name: cargo clippy
|
||||||
types_or: [rust, file]
|
description: Lint Rust sources
|
||||||
files: (Cargo.toml|Cargo.lock|rust-toolchain.toml|rustfmt.toml|.*\.rs$)
|
entry: cargo clippy
|
||||||
pass_filenames: false
|
language: system
|
||||||
- id: check-docker-templates
|
args: [ "--features", "sqlite,mysql,postgresql", "--", "-D", "warnings" ]
|
||||||
name: check docker templates
|
types_or: [ rust, toml ] # Cargo.lock matches toml type which is intended
|
||||||
description: Check if the Docker templates are updated
|
pass_filenames: false
|
||||||
language: system
|
- id: check-docker-templates
|
||||||
entry: sh
|
name: check docker templates
|
||||||
args:
|
description: Check if the Docker templates are updated
|
||||||
- "-c"
|
language: system
|
||||||
- "cd docker && make"
|
entry: sh
|
||||||
# When this version is updated, do not forget to update this in `.github/workflows/typos.yaml` too
|
args:
|
||||||
- repo: https://github.com/crate-ci/typos
|
- "-c"
|
||||||
rev: 1a319b54cc9e3b333fed6a5c88ba1a90324da514 # v1.40.1
|
- "cd docker && make"
|
||||||
hooks:
|
|
||||||
- id: typos
|
|
||||||
@@ -23,4 +23,6 @@ extend-ignore-re = [
|
|||||||
# https://github.com/bitwarden/server/blob/dff9f1cf538198819911cf2c20f8cda3307701c5/src/Notifications/HubHelpers.cs#L86
|
# https://github.com/bitwarden/server/blob/dff9f1cf538198819911cf2c20f8cda3307701c5/src/Notifications/HubHelpers.cs#L86
|
||||||
# https://github.com/bitwarden/clients/blob/9612a4ac45063e372a6fbe87eb253c7cb3c588fb/libs/common/src/auth/services/anonymous-hub.service.ts#L45
|
# https://github.com/bitwarden/clients/blob/9612a4ac45063e372a6fbe87eb253c7cb3c588fb/libs/common/src/auth/services/anonymous-hub.service.ts#L45
|
||||||
"AuthRequestResponseRecieved",
|
"AuthRequestResponseRecieved",
|
||||||
|
# Ignore Punycode/IDN tests
|
||||||
|
"xn--.+"
|
||||||
]
|
]
|
||||||
Generated
+1495
-1419
File diff suppressed because it is too large.
Load diff
+164
-96
@@ -1,6 +1,6 @@
|
|||||||
[workspace.package]
|
[workspace.package]
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
rust-version = "1.90.0"
|
rust-version = "1.97.1"
|
||||||
license = "AGPL-3.0-only"
|
license = "AGPL-3.0-only"
|
||||||
repository = "https://github.com/dani-garcia/vaultwarden"
|
repository = "https://github.com/dani-garcia/vaultwarden"
|
||||||
publish = false
|
publish = false
|
||||||
@@ -12,9 +12,7 @@ members = ["macros"]
|
|||||||
name = "vaultwarden"
|
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>"]
|
||||||
readme = "README.md"
|
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
resolver = "2"
|
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
@@ -24,20 +22,32 @@ publish.workspace = true
|
|||||||
[features]
|
[features]
|
||||||
default = [
|
default = [
|
||||||
# "sqlite",
|
# "sqlite",
|
||||||
|
# "sqlite_system",
|
||||||
# "mysql",
|
# "mysql",
|
||||||
# "postgresql",
|
# "postgresql",
|
||||||
]
|
]
|
||||||
# Empty to keep compatibility, prefer to set USE_SYSLOG=true
|
# Empty to keep compatibility, prefer to set USE_SYSLOG=true
|
||||||
enable_syslog = []
|
enable_syslog = []
|
||||||
|
# Please enable at least one of these DB backends.
|
||||||
mysql = ["diesel/mysql", "diesel_migrations/mysql"]
|
mysql = ["diesel/mysql", "diesel_migrations/mysql"]
|
||||||
postgresql = ["diesel/postgres", "diesel_migrations/postgres"]
|
postgresql = ["diesel/postgres", "diesel_migrations/postgres"]
|
||||||
sqlite = ["diesel/sqlite", "diesel_migrations/sqlite", "dep:libsqlite3-sys"]
|
sqlite_system = ["diesel/sqlite", "diesel_migrations/sqlite"] # Dynamically link SQLite
|
||||||
|
sqlite = ["sqlite_system", "libsqlite3-sys/bundled"] # Statically link SQLite into the binary instead of dynamically.
|
||||||
# Enable to use a vendored and statically linked openssl
|
# Enable to use a vendored and statically linked openssl
|
||||||
vendored_openssl = ["openssl/vendored"]
|
vendored_openssl = ["openssl/vendored"]
|
||||||
# Enable MiMalloc memory allocator to replace the default malloc
|
# Enable MiMalloc memory allocator to replace the default malloc
|
||||||
# This can improve performance for Alpine builds
|
# This can improve performance for Alpine builds
|
||||||
enable_mimalloc = ["dep:mimalloc"]
|
enable_mimalloc = ["dep:mimalloc"]
|
||||||
s3 = ["opendal/services-s3", "dep:aws-config", "dep:aws-credential-types", "dep:aws-smithy-runtime-api", "dep:anyhow", "dep:http", "dep:reqsign"]
|
s3 = [
|
||||||
|
"opendal/services-s3",
|
||||||
|
"dep:opendal-http-transport-reqwest",
|
||||||
|
"dep:aws-config",
|
||||||
|
"dep:aws-credential-types",
|
||||||
|
"dep:aws-smithy-runtime-api",
|
||||||
|
"dep:http",
|
||||||
|
"dep:reqsign-aws-v4",
|
||||||
|
"dep:reqsign-core",
|
||||||
|
]
|
||||||
|
|
||||||
# OIDC specific features
|
# OIDC specific features
|
||||||
oidc-accept-rfc3339-timestamps = ["openidconnect/accept-rfc3339-timestamps"]
|
oidc-accept-rfc3339-timestamps = ["openidconnect/accept-rfc3339-timestamps"]
|
||||||
@@ -48,6 +58,7 @@ oidc-accept-string-booleans = ["openidconnect/accept-string-booleans"]
|
|||||||
unstable = []
|
unstable = []
|
||||||
|
|
||||||
[target."cfg(unix)".dependencies]
|
[target."cfg(unix)".dependencies]
|
||||||
|
nix = { version = "0.31.3", features = ["fs"] }
|
||||||
# Logging
|
# Logging
|
||||||
syslog = "7.0.0"
|
syslog = "7.0.0"
|
||||||
|
|
||||||
@@ -55,154 +66,212 @@ syslog = "7.0.0"
|
|||||||
macros = { path = "./macros" }
|
macros = { path = "./macros" }
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
log = "0.4.29"
|
log = "0.4.34"
|
||||||
fern = { version = "0.7.1", features = ["syslog-7", "reopen-1"] }
|
fern = { version = "0.7.1", features = ["syslog-7", "reopen-1"] }
|
||||||
tracing = { version = "0.1.44", features = ["log"] } # Needed to have lettre and webauthn-rs trace logging to work
|
# We need the `log` feature for `tracing` to enable logging for several crates to work, like lettre or webauthn-rs
|
||||||
|
tracing = { version = "0.1.44", features = ["log"] }
|
||||||
|
|
||||||
# A `dotenv` implementation for Rust
|
# A `dotenv` implementation for Rust
|
||||||
dotenvy = { version = "0.15.7", default-features = false }
|
dotenvy = { version = "0.15.7", default-features = false }
|
||||||
|
|
||||||
# Numerical libraries
|
# Numerical libraries
|
||||||
num-traits = "0.2.19"
|
num-traits = "0.2.19"
|
||||||
num-derive = "0.4.2"
|
num-derive = "0.5.1"
|
||||||
bigdecimal = "0.4.10"
|
bigdecimal = "0.4.11"
|
||||||
|
|
||||||
# Web framework
|
# Web framework
|
||||||
rocket = { version = "0.5.1", features = ["tls", "json"], default-features = false }
|
rocket = { version = "0.5.1", default-features = false, features = ["json", "tls"] }
|
||||||
rocket_ws = { version ="0.1.1" }
|
rocket_ws = { version = "0.1.1" }
|
||||||
|
|
||||||
# WebSockets libraries
|
# WebSockets libraries
|
||||||
rmpv = "1.3.1" # MessagePack library
|
rmpv = "1.3.1" # MessagePack library
|
||||||
|
|
||||||
# Concurrent HashMap used for WebSocket messaging and favicons
|
# Concurrent HashMap used for WebSocket messaging and favicons
|
||||||
dashmap = "6.1.0"
|
dashmap = "6.2.1"
|
||||||
|
|
||||||
# Async futures
|
# Async futures
|
||||||
futures = "0.3.31"
|
futures = "0.3.34"
|
||||||
tokio = { version = "1.48.0", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] }
|
tokio = { version = "1.53.2", features = [
|
||||||
tokio-util = { version = "0.7.17", features = ["compat"]}
|
"fs",
|
||||||
|
"io-util",
|
||||||
|
"net",
|
||||||
|
"parking_lot",
|
||||||
|
"rt-multi-thread",
|
||||||
|
"signal",
|
||||||
|
"time",
|
||||||
|
] }
|
||||||
|
tokio-util = { version = "0.7.19", features = ["compat"] }
|
||||||
|
|
||||||
# A generic serialization/deserialization framework
|
# A generic serialization/deserialization framework
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.229", features = ["derive"] }
|
||||||
serde_json = "1.0.148"
|
serde_json = "1.0.151"
|
||||||
|
|
||||||
# A safe, extensible ORM and Query builder
|
# A safe, extensible ORM and Query builder
|
||||||
# Currently pinned diesel to v2.3.3 as newer version break MySQL/MariaDB compatibility
|
diesel = { version = "2.3.13", features = ["chrono", "r2d2", "numeric"] }
|
||||||
diesel = { version = "2.3.5", features = ["chrono", "r2d2", "numeric"] }
|
diesel_migrations = "2.3.2"
|
||||||
diesel_migrations = "2.3.1"
|
|
||||||
|
|
||||||
derive_more = { version = "2.1.1", features = ["from", "into", "as_ref", "deref", "display"] }
|
derive_more = { version = "2.1.1", features = [
|
||||||
diesel-derive-newtype = "2.1.2"
|
"as_ref",
|
||||||
|
"deref",
|
||||||
|
"display",
|
||||||
|
"from",
|
||||||
|
"into",
|
||||||
|
] }
|
||||||
|
diesel-derive-newtype = "2.1.3"
|
||||||
|
|
||||||
# Bundled/Static SQLite
|
# SQLite, statically bundled unless the `sqlite_system` feature is enabled
|
||||||
libsqlite3-sys = { version = "0.35.0", features = ["bundled"], optional = true }
|
libsqlite3-sys = { version = "0.38.2", optional = true }
|
||||||
|
|
||||||
# Crypto-related libraries
|
# Crypto-related libraries
|
||||||
rand = "0.9.2"
|
rand = "0.10.3"
|
||||||
ring = "0.17.14"
|
ring = "0.17.14"
|
||||||
|
rustls = { version = "0.23.45", features = ["ring", "std"], default-features = false }
|
||||||
subtle = "2.6.1"
|
subtle = "2.6.1"
|
||||||
|
|
||||||
# UUID generation
|
# UUID generation
|
||||||
uuid = { version = "1.19.0", features = ["v4"] }
|
uuid = { version = "1.27.0", features = ["v4"] }
|
||||||
|
|
||||||
# Date and time libraries
|
# Date and time libraries
|
||||||
chrono = { version = "0.4.42", features = ["clock", "serde"], default-features = false }
|
chrono = { version = "0.4.45", default-features = false, features = ["clock", "serde"] }
|
||||||
chrono-tz = "0.10.4"
|
chrono-tz = "0.10.4"
|
||||||
time = "0.3.44"
|
time = "0.3.55"
|
||||||
|
|
||||||
# Job scheduler
|
# Job scheduler
|
||||||
job_scheduler_ng = "2.4.0"
|
job_scheduler_ng = "2.5.0"
|
||||||
|
|
||||||
# Data encoding library Hex/Base32/Base64
|
# Data encoding library Hex/Base32/Base64
|
||||||
data-encoding = "2.9.0"
|
data-encoding = "2.11.1"
|
||||||
|
|
||||||
# JWT library
|
# JWT library
|
||||||
jsonwebtoken = { version = "10.2.0", features = ["use_pem", "rust_crypto"], default-features = false }
|
jsonwebtoken = { version = "11.1.0", default-features = false, features = ["rust_crypto", "use_pem"] }
|
||||||
|
|
||||||
# TOTP library
|
# TOTP library
|
||||||
totp-lite = "2.0.1"
|
totp-lite = "2.0.1"
|
||||||
|
|
||||||
# Yubico Library
|
# Yubico Library
|
||||||
yubico = { package = "yubico_ng", version = "0.14.1", features = ["online-tokio"], default-features = false }
|
yubico_ng = { version = "1.0.0", default-features = false }
|
||||||
|
|
||||||
# WebAuthn libraries
|
# WebAuthn libraries
|
||||||
# danger-allow-state-serialisation is needed to save the state in the db
|
# danger-allow-state-serialisation is needed to save the state in the db
|
||||||
# danger-credential-internals is needed to support U2F to Webauthn migration
|
# danger-credential-internals is needed to support U2F to Webauthn migration
|
||||||
webauthn-rs = { version = "0.5.4", features = ["danger-allow-state-serialisation", "danger-credential-internals"] }
|
webauthn-rs = { version = "0.5.5", features = ["danger-allow-state-serialisation", "danger-credential-internals"] }
|
||||||
webauthn-rs-proto = "0.5.4"
|
webauthn-rs-proto = "0.5.5"
|
||||||
webauthn-rs-core = "0.5.4"
|
webauthn-rs-core = "0.5.5"
|
||||||
|
|
||||||
# Handling of URL's for WebAuthn and favicons
|
# Handling of URL's for WebAuthn and favicons
|
||||||
url = "2.5.7"
|
url = "2.5.8"
|
||||||
|
|
||||||
# Email libraries
|
# Email libraries
|
||||||
lettre = { version = "0.11.19", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "hostname", "tracing", "tokio1-rustls", "ring", "rustls-native-certs"], default-features = false }
|
lettre = { version = "0.11.23", default-features = false, features = [
|
||||||
|
# Misc
|
||||||
|
"tracing",
|
||||||
|
"serde",
|
||||||
|
"builder",
|
||||||
|
"hostname",
|
||||||
|
# TLS/Security
|
||||||
|
"ring",
|
||||||
|
"rustls-native-certs",
|
||||||
|
"tokio1-rustls",
|
||||||
|
# Transport
|
||||||
|
"smtp-transport",
|
||||||
|
"sendmail-transport",
|
||||||
|
] }
|
||||||
percent-encoding = "2.3.2" # URL encoding library used for URL's in the emails
|
percent-encoding = "2.3.2" # URL encoding library used for URL's in the emails
|
||||||
email_address = "0.2.9"
|
email_address = "0.2.9"
|
||||||
|
|
||||||
# HTML Template library
|
# HTML Template library
|
||||||
handlebars = { version = "6.3.2", features = ["dir_source"] }
|
handlebars = { version = "6.4.4", features = ["dir_source"] }
|
||||||
|
|
||||||
# HTTP client (Used for favicons, version check, DUO and HIBP API)
|
# HTTP client (Used for favicons, version check, DUO and HIBP API)
|
||||||
reqwest = { version = "0.12.28", features = ["rustls-tls", "rustls-tls-native-roots", "stream", "json", "deflate", "gzip", "brotli", "zstd", "socks", "cookies", "charset", "http2", "system-proxy"], default-features = false}
|
reqwest = { version = "0.13.5", default-features = false, features = [
|
||||||
hickory-resolver = "0.25.2"
|
# Misc
|
||||||
|
"charset",
|
||||||
|
"cookies",
|
||||||
|
"http2",
|
||||||
|
"json",
|
||||||
|
"form",
|
||||||
|
"rustls-no-provider",
|
||||||
|
"stream",
|
||||||
|
# Compression
|
||||||
|
"brotli",
|
||||||
|
"deflate",
|
||||||
|
"gzip",
|
||||||
|
"zstd",
|
||||||
|
# Proxy
|
||||||
|
"socks",
|
||||||
|
"system-proxy",
|
||||||
|
] }
|
||||||
|
hickory-resolver = "0.26.3"
|
||||||
|
|
||||||
# Favicon extraction libraries
|
# Favicon extraction libraries
|
||||||
html5gum = "0.8.3"
|
html5gum = "0.8.4"
|
||||||
regex = { version = "1.12.2", features = ["std", "perf", "unicode-perl"], default-features = false }
|
regex = { version = "1.13.1", default-features = false, features = [
|
||||||
|
"perf",
|
||||||
|
"std",
|
||||||
|
"unicode-perl",
|
||||||
|
] }
|
||||||
data-url = "0.3.2"
|
data-url = "0.3.2"
|
||||||
bytes = "1.11.0"
|
bytes = "1.12.1"
|
||||||
svg-hush = "0.9.5"
|
svg-hush = "0.9.7"
|
||||||
|
|
||||||
# Cache function results (Used for version check and favicon fetching)
|
# Cache function results (Used for version check and favicon fetching)
|
||||||
cached = { version = "0.56.0", features = ["async"] }
|
cached = { version = "4.0.1", 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.2"
|
||||||
cookie_store = "0.22.0"
|
cookie_store = "0.22.1"
|
||||||
|
|
||||||
# Used by U2F, JWT and PostgreSQL
|
# Used by U2F, JWT and PostgreSQL
|
||||||
openssl = "0.10.75"
|
openssl = "0.10.81"
|
||||||
|
|
||||||
# CLI argument parsing
|
# CLI argument parsing
|
||||||
pico-args = "0.5.0"
|
pico-args = "0.5.0"
|
||||||
|
|
||||||
# Macro ident concatenation
|
# Macro ident concatenation
|
||||||
pastey = "0.2.1"
|
pastey = "0.2.3"
|
||||||
governor = "0.10.4"
|
governor = "0.10.4"
|
||||||
|
|
||||||
|
# CIDR parsing for the trusted proxies of the client IP header
|
||||||
|
ipnet = "2.12.2"
|
||||||
|
|
||||||
# OIDC for SSO
|
# OIDC for SSO
|
||||||
openidconnect = { version = "4.0.1", features = ["reqwest", "native-tls"] }
|
openidconnect = { version = "4.0.1", default-features = false }
|
||||||
mini-moka = "0.10.3"
|
moka = { version = "0.12.16", features = ["future"] }
|
||||||
|
|
||||||
# Check client versions for specific features.
|
# Check client versions for specific features.
|
||||||
semver = "1.0.27"
|
semver = "1.0.28"
|
||||||
|
|
||||||
# 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.48", features = ["secure"], default-features = false, optional = true }
|
mimalloc = { version = "0.1.52", optional = true, default-features = false, features = ["secure"] }
|
||||||
|
|
||||||
which = "8.0.0"
|
which = "8.0.6"
|
||||||
|
|
||||||
# Argon2 library with support for the PHC format
|
# Argon2 library with support for the PHC format
|
||||||
argon2 = "0.5.3"
|
argon2 = "0.6.0"
|
||||||
|
|
||||||
# Reading a password from the cli for generating the Argon2id ADMIN_TOKEN
|
# Reading a password from the cli for generating the Argon2id ADMIN_TOKEN
|
||||||
rpassword = "7.4.0"
|
rpassword = "7.5.4"
|
||||||
|
|
||||||
# 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
|
# File are accessed through Apache OpenDAL
|
||||||
opendal = { version = "0.55.0", features = ["services-fs"], default-features = false }
|
opendal = { version = "0.59.4", default-features = false, features = ["services-fs"] }
|
||||||
|
opendal-http-transport-reqwest = { version = "0.59.4", default-features = false, features = ["rustls-no-provider"], optional = true }
|
||||||
|
|
||||||
# For retrieving AWS credentials, including temporary SSO credentials
|
# For retrieving AWS credentials, including temporary SSO credentials
|
||||||
anyhow = { version = "1.0.100", optional = true }
|
aws-config = { version = "1.12.0", optional = true, default-features = false, features = [
|
||||||
aws-config = { version = "1.8.12", features = ["behavior-version-latest", "rt-tokio", "credentials-process", "sso"], default-features = false, optional = true }
|
"behavior-version-latest",
|
||||||
aws-credential-types = { version = "1.2.11", optional = true }
|
"credentials-process",
|
||||||
aws-smithy-runtime-api = { version = "1.9.3", optional = true }
|
"rt-tokio",
|
||||||
http = { version = "1.4.0", optional = true }
|
"sso",
|
||||||
reqsign = { version = "0.16.5", optional = true }
|
] }
|
||||||
|
aws-credential-types = { version = "1.3.0", optional = true }
|
||||||
|
aws-smithy-runtime-api = { version = "1.19.0", optional = true }
|
||||||
|
http = { version = "1.5.0", optional = true }
|
||||||
|
reqsign-aws-v4 = { version = "3.3.1", optional = true }
|
||||||
|
reqsign-core = { version = "3.3.2", 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
|
||||||
@@ -262,75 +331,74 @@ unsafe_code = "forbid"
|
|||||||
non_ascii_idents = "forbid"
|
non_ascii_idents = "forbid"
|
||||||
|
|
||||||
# Deny
|
# Deny
|
||||||
deprecated_in_future = "deny"
|
warnings = "deny" # Explicitly deny all warnings since we deny all warnings in the end
|
||||||
|
|
||||||
|
# Deny lint groups
|
||||||
deprecated_safe = { level = "deny", priority = -1 }
|
deprecated_safe = { level = "deny", priority = -1 }
|
||||||
future_incompatible = { level = "deny", priority = -1 }
|
future_incompatible = { level = "deny", priority = -1 }
|
||||||
keyword_idents = { level = "deny", priority = -1 }
|
keyword_idents = { level = "deny", priority = -1 }
|
||||||
let_underscore = { level = "deny", priority = -1 }
|
let_underscore = { level = "deny", priority = -1 }
|
||||||
nonstandard_style = { level = "deny", priority = -1 }
|
nonstandard_style = { level = "deny", priority = -1 }
|
||||||
noop_method_call = "deny"
|
|
||||||
refining_impl_trait = { level = "deny", priority = -1 }
|
refining_impl_trait = { level = "deny", priority = -1 }
|
||||||
rust_2018_idioms = { level = "deny", priority = -1 }
|
rust_2018_idioms = { level = "deny", priority = -1 }
|
||||||
rust_2021_compatibility = { level = "deny", priority = -1 }
|
rust_2021_compatibility = { level = "deny", priority = -1 }
|
||||||
rust_2024_compatibility = { level = "deny", priority = -1 }
|
rust_2024_compatibility = { level = "deny", priority = -1 }
|
||||||
|
unused = { level = "deny", priority = -1 }
|
||||||
|
|
||||||
|
# Deny individual lints
|
||||||
|
closure_returning_async_block = "deny"
|
||||||
|
deprecated_in_future = "deny"
|
||||||
single_use_lifetimes = "deny"
|
single_use_lifetimes = "deny"
|
||||||
trivial_casts = "deny"
|
trivial_casts = "deny"
|
||||||
trivial_numeric_casts = "deny"
|
trivial_numeric_casts = "deny"
|
||||||
unused = { level = "deny", priority = -1 }
|
|
||||||
unused_import_braces = "deny"
|
unused_import_braces = "deny"
|
||||||
unused_lifetimes = "deny"
|
unused_lifetimes = "deny"
|
||||||
unused_qualifications = "deny"
|
unused_qualifications = "deny"
|
||||||
variant_size_differences = "deny"
|
variant_size_differences = "deny"
|
||||||
# Allow the following lints since these cause issues with Rust v1.84.0 or newer
|
|
||||||
# Building Vaultwarden with Rust v1.85.0 with edition 2024 also works without issues
|
|
||||||
edition_2024_expr_fragment_specifier = "allow" # Once changed to Rust 2024 this should be removed and macro's should be validated again
|
|
||||||
if_let_rescope = "allow"
|
|
||||||
tail_expr_drop_order = "allow"
|
|
||||||
|
|
||||||
# https://rust-lang.github.io/rust-clippy/stable/index.html
|
# https://rust-lang.github.io/rust-clippy/stable/index.html
|
||||||
[workspace.lints.clippy]
|
[workspace.lints.clippy]
|
||||||
# Warn
|
# Warn only so you can still use these during development, but not in the final code
|
||||||
dbg_macro = "warn"
|
dbg_macro = "warn"
|
||||||
todo = "warn"
|
todo = "warn"
|
||||||
|
|
||||||
# Ignore/Allow
|
# Ignore/Allow
|
||||||
result_large_err = "allow"
|
result_large_err = "allow"
|
||||||
|
|
||||||
# Deny
|
# Warn on these lint group (Some might be warn by default already though)
|
||||||
|
# Will be denied during CI!
|
||||||
|
complexity = { level = "warn", priority = -1 }
|
||||||
|
pedantic = { level = "warn", priority = -1 }
|
||||||
|
perf = { level = "warn", priority = -1 }
|
||||||
|
style = { level = "warn", priority = -1 }
|
||||||
|
suspicious = { level = "warn", priority = -1 }
|
||||||
|
|
||||||
|
# Deny individual lints
|
||||||
branches_sharing_code = "deny"
|
branches_sharing_code = "deny"
|
||||||
case_sensitive_file_extension_comparisons = "deny"
|
|
||||||
cast_lossless = "deny"
|
|
||||||
clone_on_ref_ptr = "deny"
|
clone_on_ref_ptr = "deny"
|
||||||
equatable_if_let = "deny"
|
equatable_if_let = "deny"
|
||||||
excessive_precision = "deny"
|
|
||||||
filter_map_next = "deny"
|
|
||||||
float_cmp_const = "deny"
|
float_cmp_const = "deny"
|
||||||
implicit_clone = "deny"
|
|
||||||
inefficient_to_string = "deny"
|
|
||||||
iter_on_empty_collections = "deny"
|
iter_on_empty_collections = "deny"
|
||||||
iter_on_single_items = "deny"
|
iter_on_single_items = "deny"
|
||||||
linkedlist = "deny"
|
|
||||||
macro_use_imports = "deny"
|
|
||||||
manual_assert = "deny"
|
|
||||||
manual_instant_elapsed = "deny"
|
|
||||||
manual_string_new = "deny"
|
|
||||||
match_wildcard_for_single_variants = "deny"
|
|
||||||
mem_forget = "deny"
|
mem_forget = "deny"
|
||||||
needless_borrow = "deny"
|
|
||||||
needless_collect = "deny"
|
needless_collect = "deny"
|
||||||
needless_continue = "deny"
|
|
||||||
needless_lifetimes = "deny"
|
|
||||||
option_option = "deny"
|
|
||||||
redundant_clone = "deny"
|
redundant_clone = "deny"
|
||||||
string_add_assign = "deny"
|
|
||||||
unnecessary_join = "deny"
|
|
||||||
unnecessary_self_imports = "deny"
|
unnecessary_self_imports = "deny"
|
||||||
unnested_or_patterns = "deny"
|
|
||||||
unused_async = "deny"
|
|
||||||
unused_self = "deny"
|
|
||||||
useless_let_if_seq = "deny"
|
useless_let_if_seq = "deny"
|
||||||
verbose_file_reads = "deny"
|
verbose_file_reads = "deny"
|
||||||
zero_sized_map_values = "deny"
|
str_to_string = "deny"
|
||||||
|
|
||||||
|
# Pedantic Opt-Outs
|
||||||
|
inline_always = "allow" # We use this sparsely
|
||||||
|
struct_field_names = "allow" # Noisy and some items are Bitwarden controlled
|
||||||
|
large_futures = "allow" # Causes a fail in some Rocket macro's, since we experience no issues, allow it
|
||||||
|
too_many_lines = "allow" # For now, allow this, good to enable in the future and see if we can refactor
|
||||||
|
unnecessary_wraps = "allow" # Too much false positives because of Rocket integrations
|
||||||
|
# We do not use these doc items
|
||||||
|
doc_link_with_quotes = "allow"
|
||||||
|
doc_markdown = "allow"
|
||||||
|
missing_errors_doc = "allow"
|
||||||
|
missing_panics_doc = "allow"
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
@@ -59,8 +59,9 @@ A nearly complete implementation of the Bitwarden Client API is provided, includ
|
|||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> 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).
|
> The web-vault requires the use of HTTPS and a secure context for the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API). <br>
|
||||||
> 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).
|
> That means it will only work if you [enable HTTPS](https://github.com/dani-garcia/vaultwarden/wiki/Enabling-HTTPS). <br>
|
||||||
|
> We also suggest to use a [reverse proxy](https://github.com/dani-garcia/vaultwarden/wiki/Proxy-examples).
|
||||||
|
|
||||||
The recommended way to install and use Vaultwarden is via our container images which are published to [ghcr.io](https://github.com/dani-garcia/vaultwarden/pkgs/container/vaultwarden), [docker.io](https://hub.docker.com/r/vaultwarden/server) and [quay.io](https://quay.io/repository/vaultwarden/server).
|
The recommended way to install and use Vaultwarden is via our container images which are published to [ghcr.io](https://github.com/dani-garcia/vaultwarden/pkgs/container/vaultwarden), [docker.io](https://hub.docker.com/r/vaultwarden/server) and [quay.io](https://quay.io/repository/vaultwarden/server).
|
||||||
See [which container image to use](https://github.com/dani-garcia/vaultwarden/wiki/Which-container-image-to-use) for an explanation of the provided tags.
|
See [which container image to use](https://github.com/dani-garcia/vaultwarden/wiki/Which-container-image-to-use) for an explanation of the provided tags.
|
||||||
|
|||||||
@@ -1,22 +1,21 @@
|
|||||||
use std::env;
|
use std::{env, io::Error, process::Command};
|
||||||
use std::process::Command;
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
// This allow using #[cfg(sqlite)] instead of #[cfg(feature = "sqlite")], which helps when trying to add them through macros
|
// These allow using e.g. #[cfg(mysql)] instead of #[cfg(feature = "mysql")], which helps when trying to add them through macros
|
||||||
#[cfg(feature = "sqlite")]
|
#[cfg(feature = "sqlite_system")] // The `sqlite` feature implies this one.
|
||||||
println!("cargo:rustc-cfg=sqlite");
|
println!("cargo:rustc-cfg=sqlite");
|
||||||
#[cfg(feature = "mysql")]
|
#[cfg(feature = "mysql")]
|
||||||
println!("cargo:rustc-cfg=mysql");
|
println!("cargo:rustc-cfg=mysql");
|
||||||
#[cfg(feature = "postgresql")]
|
#[cfg(feature = "postgresql")]
|
||||||
println!("cargo:rustc-cfg=postgresql");
|
println!("cargo:rustc-cfg=postgresql");
|
||||||
#[cfg(feature = "s3")]
|
#[cfg(not(any(feature = "sqlite_system", feature = "mysql", feature = "postgresql")))]
|
||||||
println!("cargo:rustc-cfg=s3");
|
|
||||||
|
|
||||||
#[cfg(not(any(feature = "sqlite", feature = "mysql", feature = "postgresql")))]
|
|
||||||
compile_error!(
|
compile_error!(
|
||||||
"You need to enable one DB backend. To build with previous defaults do: cargo build --features sqlite"
|
"You need to enable one DB backend. To build with previous defaults do: cargo build --features sqlite"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
#[cfg(feature = "s3")]
|
||||||
|
println!("cargo:rustc-cfg=s3");
|
||||||
|
|
||||||
// Use check-cfg to let cargo know which cfg's we define,
|
// Use check-cfg to let cargo know which cfg's we define,
|
||||||
// and avoid warnings when they are used in the code.
|
// and avoid warnings when they are used in the code.
|
||||||
println!("cargo::rustc-check-cfg=cfg(sqlite)");
|
println!("cargo::rustc-check-cfg=cfg(sqlite)");
|
||||||
@@ -42,13 +41,12 @@ fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run(args: &[&str]) -> Result<String, std::io::Error> {
|
fn run(args: &[&str]) -> Result<String, Error> {
|
||||||
let out = Command::new(args[0]).args(&args[1..]).output()?;
|
let out = Command::new(args[0]).args(&args[1..]).output()?;
|
||||||
if !out.status.success() {
|
if !out.status.success() {
|
||||||
use std::io::Error;
|
|
||||||
return Err(Error::other("Command not successful"));
|
return Err(Error::other("Command not successful"));
|
||||||
}
|
}
|
||||||
Ok(String::from_utf8(out.stdout).unwrap().trim().to_string())
|
Ok(String::from_utf8(out.stdout).unwrap().trim().to_owned())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This method reads info from Git, namely tags, branch, and revision
|
/// This method reads info from Git, namely tags, branch, and revision
|
||||||
@@ -58,7 +56,7 @@ fn run(args: &[&str]) -> Result<String, std::io::Error> {
|
|||||||
/// - `env!("GIT_BRANCH")`
|
/// - `env!("GIT_BRANCH")`
|
||||||
/// - `env!("GIT_REV")`
|
/// - `env!("GIT_REV")`
|
||||||
/// - `env!("VW_VERSION")`
|
/// - `env!("VW_VERSION")`
|
||||||
fn version_from_git_info() -> Result<String, std::io::Error> {
|
fn version_from_git_info() -> Result<String, Error> {
|
||||||
// The exact tag for the current commit, can be empty when
|
// The exact tag for the current commit, can be empty when
|
||||||
// the current commit doesn't have an associated tag
|
// the current commit doesn't have an associated tag
|
||||||
let exact_tag = run(&["git", "describe", "--abbrev=0", "--tags", "--exact-match"]).ok();
|
let exact_tag = run(&["git", "describe", "--abbrev=0", "--tags", "--exact-match"]).ok();
|
||||||
|
|||||||
+1
-1
@@ -2,4 +2,4 @@
|
|||||||
# see diesel.rs/guides/configuring-diesel-cli
|
# see diesel.rs/guides/configuring-diesel-cli
|
||||||
|
|
||||||
[print_schema]
|
[print_schema]
|
||||||
file = "src/db/schema.rs"
|
file = "src/db/schema.rs"
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
---
|
---
|
||||||
vault_version: "v2025.12.1+build.3"
|
vault_version: "v2026.8.0"
|
||||||
vault_image_digest: "sha256:bf5aa55dc7bcb99f85d2a88ff44d32cdc832e934a0603fe28e5c3f92904bad42"
|
vault_image_digest: "sha256:632375471c7c06a799335cd74dfeaef63f92f45f9cd322c337ae7ad70081e48a"
|
||||||
# Cross Compile Docker Helper Scripts v1.9.0
|
# Cross Compile Docker Helper Scripts v1.9.0
|
||||||
# We use the linux/amd64 platform shell scripts since there is no difference between the different platform scripts
|
# We use the linux/amd64 platform shell scripts since there is no difference between the different platform scripts
|
||||||
# https://github.com/tonistiigi/xx | https://hub.docker.com/r/tonistiigi/xx/tags
|
# https://github.com/tonistiigi/xx | https://hub.docker.com/r/tonistiigi/xx/tags
|
||||||
xx_image_digest: "sha256:c64defb9ed5a91eacb37f96ccc3d4cd72521c4bd18d5442905b95e2226b0e707"
|
xx_image_digest: "sha256:c64defb9ed5a91eacb37f96ccc3d4cd72521c4bd18d5442905b95e2226b0e707"
|
||||||
rust_version: 1.92.0 # Rust version to be used
|
# The `rust_version` variable is extracted from `rust-toolchain.toml`
|
||||||
|
# rust_version: x.yy.z # Rust version to be used
|
||||||
debian_version: trixie # Debian release name to be used
|
debian_version: trixie # Debian release name to be used
|
||||||
alpine_version: "3.23" # Alpine version to be used
|
alpine_version: "3.24" # 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
|
||||||
|
|||||||
+17
-17
@@ -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.12.1_build.3
|
# $ docker pull docker.io/vaultwarden/web-vault:v2026.8.0
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.12.1_build.3
|
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2026.8.0
|
||||||
# [docker.io/vaultwarden/web-vault@sha256:bf5aa55dc7bcb99f85d2a88ff44d32cdc832e934a0603fe28e5c3f92904bad42]
|
# [docker.io/vaultwarden/web-vault@sha256:632375471c7c06a799335cd74dfeaef63f92f45f9cd322c337ae7ad70081e48a]
|
||||||
#
|
#
|
||||||
# - 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:bf5aa55dc7bcb99f85d2a88ff44d32cdc832e934a0603fe28e5c3f92904bad42
|
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:632375471c7c06a799335cd74dfeaef63f92f45f9cd322c337ae7ad70081e48a
|
||||||
# [docker.io/vaultwarden/web-vault:v2025.12.1_build.3]
|
# [docker.io/vaultwarden/web-vault:v2026.8.0]
|
||||||
#
|
#
|
||||||
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:bf5aa55dc7bcb99f85d2a88ff44d32cdc832e934a0603fe28e5c3f92904bad42 AS vault
|
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:632375471c7c06a799335cd74dfeaef63f92f45f9cd322c337ae7ad70081e48a AS vault
|
||||||
|
|
||||||
########################## ALPINE BUILD IMAGES ##########################
|
########################## ALPINE BUILD IMAGES ##########################
|
||||||
## NOTE: The Alpine Base Images do not support other platforms then linux/amd64 and linux/arm64
|
## NOTE: The Alpine Base Images do not support other platforms then linux/amd64 and linux/arm64
|
||||||
## And for Alpine we define all build images here, they will only be loaded when actually used
|
## And for Alpine we define all build images here, they will only be loaded when actually used
|
||||||
FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.92.0 AS build_amd64
|
FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.99.0 AS build_amd64
|
||||||
FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.92.0 AS build_arm64
|
FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.99.0 AS build_arm64
|
||||||
FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.92.0 AS build_armv7
|
FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.99.0 AS build_armv7
|
||||||
FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.92.0 AS build_armv6
|
FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.99.0 AS build_armv6
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
# hadolint ignore=DL3006
|
# hadolint ignore=DL3006
|
||||||
@@ -57,7 +57,6 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
|||||||
# Debian Trixie uses libpq v17
|
# Debian Trixie uses libpq v17
|
||||||
PQ_LIB_DIR="/usr/local/musl/pq17/lib"
|
PQ_LIB_DIR="/usr/local/musl/pq17/lib"
|
||||||
|
|
||||||
|
|
||||||
# Create CARGO_HOME folder and don't download rust docs
|
# Create CARGO_HOME folder and don't download rust docs
|
||||||
RUN mkdir -pv "${CARGO_HOME}" && \
|
RUN mkdir -pv "${CARGO_HOME}" && \
|
||||||
rustup set profile minimal
|
rustup set profile minimal
|
||||||
@@ -67,11 +66,11 @@ RUN USER=root cargo new --bin /app
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Environment variables for Cargo on Alpine based builds
|
# Environment variables for Cargo on Alpine based builds
|
||||||
RUN echo "export CARGO_TARGET=${RUST_MUSL_CROSS_TARGET}" >> /env-cargo && \
|
RUN echo "export CARGO_TARGET=${CARGO_BUILD_TARGET}" >> /env-cargo && \
|
||||||
# Output the current contents of the file
|
# Output the current contents of the file
|
||||||
cat /env-cargo
|
cat /env-cargo
|
||||||
|
|
||||||
RUN source /env-cargo && \
|
RUN . /env-cargo && \
|
||||||
rustup target add "${CARGO_TARGET}"
|
rustup target add "${CARGO_TARGET}"
|
||||||
|
|
||||||
# Copies over *only* your manifests and build files
|
# Copies over *only* your manifests and build files
|
||||||
@@ -87,7 +86,7 @@ ARG DB=sqlite,mysql,postgresql,enable_mimalloc
|
|||||||
# Builds your dependencies and removes the
|
# Builds your dependencies and removes the
|
||||||
# dummy project, except the target folder
|
# dummy project, except the target folder
|
||||||
# This folder contains the compiled dependencies
|
# This folder contains the compiled dependencies
|
||||||
RUN source /env-cargo && \
|
RUN . /env-cargo && \
|
||||||
cargo build --features ${DB} --profile "${CARGO_PROFILE}" --target="${CARGO_TARGET}" && \
|
cargo build --features ${DB} --profile "${CARGO_PROFILE}" --target="${CARGO_TARGET}" && \
|
||||||
find . -not -path "./target*" -delete
|
find . -not -path "./target*" -delete
|
||||||
|
|
||||||
@@ -98,13 +97,13 @@ COPY . .
|
|||||||
ARG VW_VERSION
|
ARG VW_VERSION
|
||||||
|
|
||||||
# Builds again, this time it will be the actual source files being build
|
# Builds again, this time it will be the actual source files being build
|
||||||
RUN source /env-cargo && \
|
RUN . /env-cargo && \
|
||||||
# Make sure that we actually build the project by updating the src/main.rs timestamp
|
# Make sure that we actually build the project by updating the src/main.rs timestamp
|
||||||
# Also do this for build.rs to ensure the version is rechecked
|
# Also do this for build.rs to ensure the version is rechecked
|
||||||
touch build.rs src/main.rs && \
|
touch build.rs src/main.rs && \
|
||||||
# Create a symlink to the binary target folder to easy copy the binary in the final stage
|
# Create a symlink to the binary target folder to easy copy the binary in the final stage
|
||||||
cargo build --features ${DB} --profile "${CARGO_PROFILE}" --target="${CARGO_TARGET}" && \
|
cargo build --features ${DB} --profile "${CARGO_PROFILE}" --target="${CARGO_TARGET}" && \
|
||||||
if [[ "${CARGO_PROFILE}" == "dev" ]] ; then \
|
if [ "${CARGO_PROFILE}" = "dev" ] ; then \
|
||||||
ln -vfsr "/app/target/${CARGO_TARGET}/debug" /app/target/final ; \
|
ln -vfsr "/app/target/${CARGO_TARGET}/debug" /app/target/final ; \
|
||||||
else \
|
else \
|
||||||
ln -vfsr "/app/target/${CARGO_TARGET}/${CARGO_PROFILE}" /app/target/final ; \
|
ln -vfsr "/app/target/${CARGO_TARGET}/${CARGO_PROFILE}" /app/target/final ; \
|
||||||
@@ -127,7 +126,8 @@ 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.23
|
# hadolint ignore=DL3065
|
||||||
|
FROM --platform=$TARGETPLATFORM docker.io/library/alpine:3.24
|
||||||
|
|
||||||
ENV ROCKET_PROFILE="release" \
|
ENV ROCKET_PROFILE="release" \
|
||||||
ROCKET_ADDRESS=0.0.0.0 \
|
ROCKET_ADDRESS=0.0.0.0 \
|
||||||
|
|||||||
+50
-44
@@ -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.12.1_build.3
|
# $ docker pull docker.io/vaultwarden/web-vault:v2026.8.0
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.12.1_build.3
|
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2026.8.0
|
||||||
# [docker.io/vaultwarden/web-vault@sha256:bf5aa55dc7bcb99f85d2a88ff44d32cdc832e934a0603fe28e5c3f92904bad42]
|
# [docker.io/vaultwarden/web-vault@sha256:632375471c7c06a799335cd74dfeaef63f92f45f9cd322c337ae7ad70081e48a]
|
||||||
#
|
#
|
||||||
# - 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:bf5aa55dc7bcb99f85d2a88ff44d32cdc832e934a0603fe28e5c3f92904bad42
|
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:632375471c7c06a799335cd74dfeaef63f92f45f9cd322c337ae7ad70081e48a
|
||||||
# [docker.io/vaultwarden/web-vault:v2025.12.1_build.3]
|
# [docker.io/vaultwarden/web-vault:v2026.8.0]
|
||||||
#
|
#
|
||||||
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:bf5aa55dc7bcb99f85d2a88ff44d32cdc832e934a0603fe28e5c3f92904bad42 AS vault
|
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:632375471c7c06a799335cd74dfeaef63f92f45f9cd322c337ae7ad70081e48a 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,8 @@ FROM --platform=linux/amd64 docker.io/tonistiigi/xx@sha256:c64defb9ed5a91eacb37f
|
|||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
# hadolint ignore=DL3006
|
# hadolint ignore=DL3006
|
||||||
FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.92.0-slim-trixie AS build
|
FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.99.0-slim-trixie AS build
|
||||||
|
# hadolint ignore=DL3067
|
||||||
COPY --from=xx / /
|
COPY --from=xx / /
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
ARG TARGETVARIANT
|
ARG TARGETVARIANT
|
||||||
@@ -51,7 +52,7 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
|||||||
TERM=xterm-256color \
|
TERM=xterm-256color \
|
||||||
CARGO_HOME="/root/.cargo" \
|
CARGO_HOME="/root/.cargo" \
|
||||||
USER="root"
|
USER="root"
|
||||||
# Install clang to get `xx-cargo` working
|
# Install clang && xx-c-essentials to get `xx-cargo` working
|
||||||
# Install pkg-config to allow amd64 builds to find all libraries
|
# Install pkg-config to allow amd64 builds to find all libraries
|
||||||
# Install git so build.rs can determine the correct version
|
# Install git so build.rs can determine the correct version
|
||||||
# Install the libc cross packages based upon the debian-arch
|
# Install the libc cross packages based upon the debian-arch
|
||||||
@@ -59,19 +60,16 @@ RUN apt-get update && \
|
|||||||
apt-get install -y \
|
apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
clang \
|
clang \
|
||||||
pkg-config \
|
git && \
|
||||||
git \
|
|
||||||
"libc6-$(xx-info debian-arch)-cross" \
|
|
||||||
"libc6-dev-$(xx-info debian-arch)-cross" \
|
|
||||||
"linux-libc-dev-$(xx-info debian-arch)-cross" && \
|
|
||||||
xx-apt-get install -y \
|
xx-apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
gcc \
|
|
||||||
libpq-dev \
|
libpq-dev \
|
||||||
libpq5 \
|
libpq5 \
|
||||||
libssl-dev \
|
libssl-dev \
|
||||||
libmariadb-dev \
|
libmariadb-dev \
|
||||||
zlib1g-dev && \
|
pkg-config \
|
||||||
|
zlib1g-dev \
|
||||||
|
xx-c-essentials && \
|
||||||
# Run xx-cargo early, since it sometimes seems to break when run at a later stage
|
# Run xx-cargo early, since it sometimes seems to break when run at a later stage
|
||||||
echo "export CARGO_TARGET=$(xx-cargo --print-target-triple)" >> /env-cargo
|
echo "export CARGO_TARGET=$(xx-cargo --print-target-triple)" >> /env-cargo
|
||||||
|
|
||||||
@@ -83,30 +81,7 @@ RUN mkdir -pv "${CARGO_HOME}" && \
|
|||||||
RUN USER=root cargo new --bin /app
|
RUN USER=root cargo new --bin /app
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Environment variables for Cargo on Debian based builds
|
RUN . /env-cargo && \
|
||||||
ARG TARGET_PKG_CONFIG_PATH
|
|
||||||
|
|
||||||
RUN source /env-cargo && \
|
|
||||||
if xx-info is-cross ; then \
|
|
||||||
# We can't use xx-cargo since that uses clang, which doesn't work for our libraries.
|
|
||||||
# Because of this we generate the needed environment variables here which we can load in the needed steps.
|
|
||||||
echo "export CC_$(echo "${CARGO_TARGET}" | tr '[:upper:]' '[:lower:]' | tr - _)=/usr/bin/$(xx-info)-gcc" >> /env-cargo && \
|
|
||||||
echo "export CARGO_TARGET_$(echo "${CARGO_TARGET}" | tr '[:lower:]' '[:upper:]' | tr - _)_LINKER=/usr/bin/$(xx-info)-gcc" >> /env-cargo && \
|
|
||||||
echo "export CROSS_COMPILE=1" >> /env-cargo && \
|
|
||||||
echo "export PKG_CONFIG_ALLOW_CROSS=1" >> /env-cargo && \
|
|
||||||
# For some architectures `xx-info` returns a triple which doesn't matches the path on disk
|
|
||||||
# In those cases you can override this by setting the `TARGET_PKG_CONFIG_PATH` build-arg
|
|
||||||
if [[ -n "${TARGET_PKG_CONFIG_PATH}" ]]; then \
|
|
||||||
echo "export TARGET_PKG_CONFIG_PATH=${TARGET_PKG_CONFIG_PATH}" >> /env-cargo ; \
|
|
||||||
else \
|
|
||||||
echo "export PKG_CONFIG_PATH=/usr/lib/$(xx-info)/pkgconfig" >> /env-cargo ; \
|
|
||||||
fi && \
|
|
||||||
echo "# End of env-cargo" >> /env-cargo ; \
|
|
||||||
fi && \
|
|
||||||
# Output the current contents of the file
|
|
||||||
cat /env-cargo
|
|
||||||
|
|
||||||
RUN source /env-cargo && \
|
|
||||||
rustup target add "${CARGO_TARGET}"
|
rustup target add "${CARGO_TARGET}"
|
||||||
|
|
||||||
# Copies over *only* your manifests and build files
|
# Copies over *only* your manifests and build files
|
||||||
@@ -121,8 +96,23 @@ ARG DB=sqlite,mysql,postgresql
|
|||||||
# Builds your dependencies and removes the
|
# Builds your dependencies and removes the
|
||||||
# dummy project, except the target folder
|
# dummy project, except the target folder
|
||||||
# This folder contains the compiled dependencies
|
# This folder contains the compiled dependencies
|
||||||
RUN source /env-cargo && \
|
RUN . /env-cargo && \
|
||||||
cargo build --features ${DB} --profile "${CARGO_PROFILE}" --target="${CARGO_TARGET}" && \
|
# Configure xx-cargo for target pkg-config and Debian transitive library lookup
|
||||||
|
# https://github.com/tonistiigi/xx/pull/108#issuecomment-3700635977
|
||||||
|
# https://github.com/dani-garcia/vaultwarden/discussions/7522
|
||||||
|
if xx-info is-cross; then \
|
||||||
|
XX_RUSTFLAGS="-Clink-arg=-Wl,-rpath-link,/usr/lib/$(xx-info triple)"; \
|
||||||
|
fi && \
|
||||||
|
# Debian's GCC adds a special fix for cortex-a53 arch's which gets losts when using xx-cargo, which uses clang
|
||||||
|
# Checking if the arch is arm64 and adding this extra link-arg should cause this fix to be applied
|
||||||
|
# This should fix issues reported when running on a RPi3 which is a Cortex-A35 chip
|
||||||
|
# See: https://salsa.debian.org/toolchain-team/gcc/-/blob/master/debian/rules2?pow_referer=#L543-548
|
||||||
|
# See: https://github.com/dani-garcia/vaultwarden/issues/7754
|
||||||
|
if [ "$(xx-info arch)" = "arm64" ]; then \
|
||||||
|
XX_RUSTFLAGS="${XX_RUSTFLAGS} -Clink-arg=-Wl,--fix-cortex-a53-843419"; \
|
||||||
|
fi && \
|
||||||
|
export XX_RUSTFLAGS && \
|
||||||
|
PKG_CONFIG="$(command -v "$(xx-info)-pkg-config")" xx-cargo build --features ${DB} --profile "${CARGO_PROFILE}" && \
|
||||||
find . -not -path "./target*" -delete
|
find . -not -path "./target*" -delete
|
||||||
|
|
||||||
# Copies the complete project
|
# Copies the complete project
|
||||||
@@ -132,13 +122,28 @@ COPY . .
|
|||||||
ARG VW_VERSION
|
ARG VW_VERSION
|
||||||
|
|
||||||
# Builds again, this time it will be the actual source files being build
|
# Builds again, this time it will be the actual source files being build
|
||||||
RUN source /env-cargo && \
|
RUN . /env-cargo && \
|
||||||
# Make sure that we actually build the project by updating the src/main.rs timestamp
|
# Make sure that we actually build the project by updating the src/main.rs timestamp
|
||||||
# Also do this for build.rs to ensure the version is rechecked
|
# Also do this for build.rs to ensure the version is rechecked
|
||||||
touch build.rs src/main.rs && \
|
touch build.rs src/main.rs && \
|
||||||
# Create a symlink to the binary target folder to easy copy the binary in the final stage
|
# Create a symlink to the binary target folder to easy copy the binary in the final stage
|
||||||
cargo build --features ${DB} --profile "${CARGO_PROFILE}" --target="${CARGO_TARGET}" && \
|
# Configure xx-cargo for target pkg-config and Debian transitive library lookup
|
||||||
if [[ "${CARGO_PROFILE}" == "dev" ]] ; then \
|
# https://github.com/tonistiigi/xx/pull/108#issuecomment-3700635977
|
||||||
|
# https://github.com/dani-garcia/vaultwarden/discussions/7522
|
||||||
|
if xx-info is-cross; then \
|
||||||
|
XX_RUSTFLAGS="-Clink-arg=-Wl,-rpath-link,/usr/lib/$(xx-info triple)"; \
|
||||||
|
fi && \
|
||||||
|
# Debian's GCC adds a special fix for cortex-a53 arch's which gets losts when using xx-cargo, which uses clang
|
||||||
|
# Checking if the arch is arm64 and adding this extra link-arg should cause this fix to be applied
|
||||||
|
# This should fix issues reported when running on a RPi3 which is a Cortex-A35 chip
|
||||||
|
# See: https://salsa.debian.org/toolchain-team/gcc/-/blob/master/debian/rules2?pow_referer=#L543-548
|
||||||
|
# See: https://github.com/dani-garcia/vaultwarden/issues/7754
|
||||||
|
if [ "$(xx-info arch)" = "arm64" ]; then \
|
||||||
|
XX_RUSTFLAGS="${XX_RUSTFLAGS} -Clink-arg=-Wl,--fix-cortex-a53-843419"; \
|
||||||
|
fi && \
|
||||||
|
export XX_RUSTFLAGS && \
|
||||||
|
PKG_CONFIG="$(command -v "$(xx-info)-pkg-config")" xx-cargo build --features ${DB} --profile "${CARGO_PROFILE}" && \
|
||||||
|
if [ "${CARGO_PROFILE}" = "dev" ] ; then \
|
||||||
ln -vfsr "/app/target/${CARGO_TARGET}/debug" /app/target/final ; \
|
ln -vfsr "/app/target/${CARGO_TARGET}/debug" /app/target/final ; \
|
||||||
else \
|
else \
|
||||||
ln -vfsr "/app/target/${CARGO_TARGET}/${CARGO_PROFILE}" /app/target/final ; \
|
ln -vfsr "/app/target/${CARGO_TARGET}/${CARGO_PROFILE}" /app/target/final ; \
|
||||||
@@ -161,6 +166,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
|
||||||
|
# hadolint ignore=DL3065
|
||||||
FROM --platform=$TARGETPLATFORM docker.io/library/debian:trixie-slim
|
FROM --platform=$TARGETPLATFORM docker.io/library/debian:trixie-slim
|
||||||
|
|
||||||
ENV ROCKET_PROFILE="release" \
|
ENV ROCKET_PROFILE="release" \
|
||||||
|
|||||||
+40
-39
@@ -27,6 +27,24 @@
|
|||||||
# $ docker image inspect --format "{{ '{{' }}.RepoTags}}" docker.io/vaultwarden/web-vault@{{ vault_image_digest }}
|
# $ docker image inspect --format "{{ '{{' }}.RepoTags}}" docker.io/vaultwarden/web-vault@{{ vault_image_digest }}
|
||||||
# [docker.io/vaultwarden/web-vault:{{ vault_version | replace('+', '_') }}]
|
# [docker.io/vaultwarden/web-vault:{{ vault_version | replace('+', '_') }}]
|
||||||
#
|
#
|
||||||
|
{% macro xx_cargo_config() -%}
|
||||||
|
# Configure xx-cargo for target pkg-config and Debian transitive library lookup
|
||||||
|
# https://github.com/tonistiigi/xx/pull/108#issuecomment-3700635977
|
||||||
|
# https://github.com/dani-garcia/vaultwarden/discussions/7522
|
||||||
|
if xx-info is-cross; then \
|
||||||
|
XX_RUSTFLAGS="-Clink-arg=-Wl,-rpath-link,/usr/lib/$(xx-info triple)"; \
|
||||||
|
fi && \
|
||||||
|
# Debian's GCC adds a special fix for cortex-a53 arch's which gets losts when using xx-cargo, which uses clang
|
||||||
|
# Checking if the arch is arm64 and adding this extra link-arg should cause this fix to be applied
|
||||||
|
# This should fix issues reported when running on a RPi3 which is a Cortex-A35 chip
|
||||||
|
# See: https://salsa.debian.org/toolchain-team/gcc/-/blob/master/debian/rules2?pow_referer=#L543-548
|
||||||
|
# See: https://github.com/dani-garcia/vaultwarden/issues/7754
|
||||||
|
if [ "$(xx-info arch)" = "arm64" ]; then \
|
||||||
|
XX_RUSTFLAGS="${XX_RUSTFLAGS} -Clink-arg=-Wl,--fix-cortex-a53-843419"; \
|
||||||
|
fi && \
|
||||||
|
export XX_RUSTFLAGS && \
|
||||||
|
PKG_CONFIG="$(command -v "$(xx-info)-pkg-config")" xx-cargo build --features ${DB} --profile "${CARGO_PROFILE}"
|
||||||
|
{%- endmacro %}
|
||||||
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@{{ vault_image_digest }} AS vault
|
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@{{ vault_image_digest }} AS vault
|
||||||
|
|
||||||
{% if base == "debian" %}
|
{% if base == "debian" %}
|
||||||
@@ -47,6 +65,7 @@ FROM --platform=$BUILDPLATFORM {{ build_stage_image[base].arch_image[arch] }} AS
|
|||||||
# hadolint ignore=DL3006
|
# hadolint ignore=DL3006
|
||||||
FROM --platform=$BUILDPLATFORM {{ build_stage_image[base].image }} AS build
|
FROM --platform=$BUILDPLATFORM {{ build_stage_image[base].image }} AS build
|
||||||
{% if base == "debian" %}
|
{% if base == "debian" %}
|
||||||
|
# hadolint ignore=DL3067
|
||||||
COPY --from=xx / /
|
COPY --from=xx / /
|
||||||
{% endif %}
|
{% endif %}
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
@@ -66,10 +85,10 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
|||||||
# Use PostgreSQL v17 during Alpine/MUSL builds instead of the default v16
|
# Use PostgreSQL v17 during Alpine/MUSL builds instead of the default v16
|
||||||
# Debian Trixie uses libpq v17
|
# Debian Trixie uses libpq v17
|
||||||
PQ_LIB_DIR="/usr/local/musl/pq17/lib"
|
PQ_LIB_DIR="/usr/local/musl/pq17/lib"
|
||||||
{% endif %}
|
{%- endif %}
|
||||||
|
|
||||||
{% if base == "debian" %}
|
{% if base == "debian" %}
|
||||||
# Install clang to get `xx-cargo` working
|
# Install clang && xx-c-essentials to get `xx-cargo` working
|
||||||
# Install pkg-config to allow amd64 builds to find all libraries
|
# Install pkg-config to allow amd64 builds to find all libraries
|
||||||
# Install git so build.rs can determine the correct version
|
# Install git so build.rs can determine the correct version
|
||||||
# Install the libc cross packages based upon the debian-arch
|
# Install the libc cross packages based upon the debian-arch
|
||||||
@@ -77,19 +96,16 @@ RUN apt-get update && \
|
|||||||
apt-get install -y \
|
apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
clang \
|
clang \
|
||||||
pkg-config \
|
git && \
|
||||||
git \
|
|
||||||
"libc6-$(xx-info debian-arch)-cross" \
|
|
||||||
"libc6-dev-$(xx-info debian-arch)-cross" \
|
|
||||||
"linux-libc-dev-$(xx-info debian-arch)-cross" && \
|
|
||||||
xx-apt-get install -y \
|
xx-apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
gcc \
|
|
||||||
libpq-dev \
|
libpq-dev \
|
||||||
libpq5 \
|
libpq5 \
|
||||||
libssl-dev \
|
libssl-dev \
|
||||||
libmariadb-dev \
|
libmariadb-dev \
|
||||||
zlib1g-dev && \
|
pkg-config \
|
||||||
|
zlib1g-dev \
|
||||||
|
xx-c-essentials && \
|
||||||
# Run xx-cargo early, since it sometimes seems to break when run at a later stage
|
# Run xx-cargo early, since it sometimes seems to break when run at a later stage
|
||||||
echo "export CARGO_TARGET=$(xx-cargo --print-target-triple)" >> /env-cargo
|
echo "export CARGO_TARGET=$(xx-cargo --print-target-triple)" >> /env-cargo
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -102,38 +118,14 @@ RUN mkdir -pv "${CARGO_HOME}" && \
|
|||||||
RUN USER=root cargo new --bin /app
|
RUN USER=root cargo new --bin /app
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
{% if base == "debian" %}
|
{% if base == "alpine" %}
|
||||||
# Environment variables for Cargo on Debian based builds
|
|
||||||
ARG TARGET_PKG_CONFIG_PATH
|
|
||||||
|
|
||||||
RUN source /env-cargo && \
|
|
||||||
if xx-info is-cross ; then \
|
|
||||||
# We can't use xx-cargo since that uses clang, which doesn't work for our libraries.
|
|
||||||
# Because of this we generate the needed environment variables here which we can load in the needed steps.
|
|
||||||
echo "export CC_$(echo "${CARGO_TARGET}" | tr '[:upper:]' '[:lower:]' | tr - _)=/usr/bin/$(xx-info)-gcc" >> /env-cargo && \
|
|
||||||
echo "export CARGO_TARGET_$(echo "${CARGO_TARGET}" | tr '[:lower:]' '[:upper:]' | tr - _)_LINKER=/usr/bin/$(xx-info)-gcc" >> /env-cargo && \
|
|
||||||
echo "export CROSS_COMPILE=1" >> /env-cargo && \
|
|
||||||
echo "export PKG_CONFIG_ALLOW_CROSS=1" >> /env-cargo && \
|
|
||||||
# For some architectures `xx-info` returns a triple which doesn't matches the path on disk
|
|
||||||
# In those cases you can override this by setting the `TARGET_PKG_CONFIG_PATH` build-arg
|
|
||||||
if [[ -n "${TARGET_PKG_CONFIG_PATH}" ]]; then \
|
|
||||||
echo "export TARGET_PKG_CONFIG_PATH=${TARGET_PKG_CONFIG_PATH}" >> /env-cargo ; \
|
|
||||||
else \
|
|
||||||
echo "export PKG_CONFIG_PATH=/usr/lib/$(xx-info)/pkgconfig" >> /env-cargo ; \
|
|
||||||
fi && \
|
|
||||||
echo "# End of env-cargo" >> /env-cargo ; \
|
|
||||||
fi && \
|
|
||||||
# Output the current contents of the file
|
|
||||||
cat /env-cargo
|
|
||||||
|
|
||||||
{% elif base == "alpine" %}
|
|
||||||
# Environment variables for Cargo on Alpine based builds
|
# Environment variables for Cargo on Alpine based builds
|
||||||
RUN echo "export CARGO_TARGET=${RUST_MUSL_CROSS_TARGET}" >> /env-cargo && \
|
RUN echo "export CARGO_TARGET=${CARGO_BUILD_TARGET}" >> /env-cargo && \
|
||||||
# Output the current contents of the file
|
# Output the current contents of the file
|
||||||
cat /env-cargo
|
cat /env-cargo
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
RUN source /env-cargo && \
|
RUN . /env-cargo && \
|
||||||
rustup target add "${CARGO_TARGET}"
|
rustup target add "${CARGO_TARGET}"
|
||||||
|
|
||||||
# Copies over *only* your manifests and build files
|
# Copies over *only* your manifests and build files
|
||||||
@@ -153,8 +145,12 @@ ARG DB=sqlite,mysql,postgresql,enable_mimalloc
|
|||||||
# Builds your dependencies and removes the
|
# Builds your dependencies and removes the
|
||||||
# dummy project, except the target folder
|
# dummy project, except the target folder
|
||||||
# This folder contains the compiled dependencies
|
# This folder contains the compiled dependencies
|
||||||
RUN source /env-cargo && \
|
RUN . /env-cargo && \
|
||||||
|
{% if base == "debian" %}
|
||||||
|
{{ xx_cargo_config() }} && \
|
||||||
|
{% elif base == "alpine" %}
|
||||||
cargo build --features ${DB} --profile "${CARGO_PROFILE}" --target="${CARGO_TARGET}" && \
|
cargo build --features ${DB} --profile "${CARGO_PROFILE}" --target="${CARGO_TARGET}" && \
|
||||||
|
{% endif %}
|
||||||
find . -not -path "./target*" -delete
|
find . -not -path "./target*" -delete
|
||||||
|
|
||||||
# Copies the complete project
|
# Copies the complete project
|
||||||
@@ -164,13 +160,17 @@ COPY . .
|
|||||||
ARG VW_VERSION
|
ARG VW_VERSION
|
||||||
|
|
||||||
# Builds again, this time it will be the actual source files being build
|
# Builds again, this time it will be the actual source files being build
|
||||||
RUN source /env-cargo && \
|
RUN . /env-cargo && \
|
||||||
# Make sure that we actually build the project by updating the src/main.rs timestamp
|
# Make sure that we actually build the project by updating the src/main.rs timestamp
|
||||||
# Also do this for build.rs to ensure the version is rechecked
|
# Also do this for build.rs to ensure the version is rechecked
|
||||||
touch build.rs src/main.rs && \
|
touch build.rs src/main.rs && \
|
||||||
# Create a symlink to the binary target folder to easy copy the binary in the final stage
|
# Create a symlink to the binary target folder to easy copy the binary in the final stage
|
||||||
|
{% if base == "debian" %}
|
||||||
|
{{ xx_cargo_config() }} && \
|
||||||
|
{% elif base == "alpine" %}
|
||||||
cargo build --features ${DB} --profile "${CARGO_PROFILE}" --target="${CARGO_TARGET}" && \
|
cargo build --features ${DB} --profile "${CARGO_PROFILE}" --target="${CARGO_TARGET}" && \
|
||||||
if [[ "${CARGO_PROFILE}" == "dev" ]] ; then \
|
{% endif %}
|
||||||
|
if [ "${CARGO_PROFILE}" = "dev" ] ; then \
|
||||||
ln -vfsr "/app/target/${CARGO_TARGET}/debug" /app/target/final ; \
|
ln -vfsr "/app/target/${CARGO_TARGET}/debug" /app/target/final ; \
|
||||||
else \
|
else \
|
||||||
ln -vfsr "/app/target/${CARGO_TARGET}/${CARGO_PROFILE}" /app/target/final ; \
|
ln -vfsr "/app/target/${CARGO_TARGET}/${CARGO_PROFILE}" /app/target/final ; \
|
||||||
@@ -193,6 +193,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
|
||||||
|
# hadolint ignore=DL3065
|
||||||
FROM --platform=$TARGETPLATFORM {{ runtime_stage_image[base] }}
|
FROM --platform=$TARGETPLATFORM {{ runtime_stage_image[base] }}
|
||||||
|
|
||||||
ENV ROCKET_PROFILE="release" \
|
ENV ROCKET_PROFILE="release" \
|
||||||
|
|||||||
@@ -3,17 +3,23 @@
|
|||||||
import os
|
import os
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
|
import tomllib
|
||||||
import yaml
|
import yaml
|
||||||
import jinja2
|
import jinja2
|
||||||
|
|
||||||
# Load settings file
|
# Load settings file
|
||||||
with open("DockerSettings.yaml", 'r') as yaml_file:
|
with open('DockerSettings.yaml', 'r', encoding='utf-8') as yaml_file:
|
||||||
yaml_data = yaml.safe_load(yaml_file)
|
yaml_data = yaml.safe_load(yaml_file)
|
||||||
|
|
||||||
|
# Extract the rust_version from the rust-toolchain.toml file
|
||||||
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
with open(os.path.join(script_dir, '..', 'rust-toolchain.toml'), 'rb') as toolchain_file:
|
||||||
|
yaml_data["rust_version"] = tomllib.load(toolchain_file)["toolchain"]["channel"]
|
||||||
|
|
||||||
settings_env = jinja2.Environment(
|
settings_env = jinja2.Environment(
|
||||||
loader=jinja2.FileSystemLoader(os.getcwd()),
|
loader=jinja2.FileSystemLoader(os.getcwd()),
|
||||||
)
|
)
|
||||||
settings_yaml = yaml.safe_load(settings_env.get_template("DockerSettings.yaml").render(yaml_data))
|
settings_yaml = yaml.safe_load(settings_env.get_template('DockerSettings.yaml').render(yaml_data))
|
||||||
|
|
||||||
args_parser = argparse.ArgumentParser()
|
args_parser = argparse.ArgumentParser()
|
||||||
args_parser.add_argument('template_file', help='Jinja2 template file to render.')
|
args_parser.add_argument('template_file', help='Jinja2 template file to render.')
|
||||||
|
|||||||
+2
-2
@@ -13,8 +13,8 @@ path = "src/lib.rs"
|
|||||||
proc-macro = true
|
proc-macro = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
quote = "1.0.42"
|
quote = "1.0.47"
|
||||||
syn = "2.0.111"
|
syn = "3.0.6"
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
+5
-4
@@ -1,14 +1,15 @@
|
|||||||
use proc_macro::TokenStream;
|
use proc_macro::TokenStream;
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
|
use syn::{DeriveInput, parse_macro_input};
|
||||||
|
|
||||||
#[proc_macro_derive(UuidFromParam)]
|
#[proc_macro_derive(UuidFromParam)]
|
||||||
pub fn derive_uuid_from_param(input: TokenStream) -> TokenStream {
|
pub fn derive_uuid_from_param(input: TokenStream) -> TokenStream {
|
||||||
let ast = syn::parse(input).unwrap();
|
let ast = parse_macro_input!(input as DeriveInput);
|
||||||
|
|
||||||
impl_derive_uuid_macro(&ast)
|
impl_derive_uuid_macro(&ast)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn impl_derive_uuid_macro(ast: &syn::DeriveInput) -> TokenStream {
|
fn impl_derive_uuid_macro(ast: &DeriveInput) -> TokenStream {
|
||||||
let name = &ast.ident;
|
let name = &ast.ident;
|
||||||
let gen_derive = quote! {
|
let gen_derive = quote! {
|
||||||
#[automatically_derived]
|
#[automatically_derived]
|
||||||
@@ -30,12 +31,12 @@ fn impl_derive_uuid_macro(ast: &syn::DeriveInput) -> TokenStream {
|
|||||||
|
|
||||||
#[proc_macro_derive(IdFromParam)]
|
#[proc_macro_derive(IdFromParam)]
|
||||||
pub fn derive_id_from_param(input: TokenStream) -> TokenStream {
|
pub fn derive_id_from_param(input: TokenStream) -> TokenStream {
|
||||||
let ast = syn::parse(input).unwrap();
|
let ast = parse_macro_input!(input as DeriveInput);
|
||||||
|
|
||||||
impl_derive_safestring_macro(&ast)
|
impl_derive_safestring_macro(&ast)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn impl_derive_safestring_macro(ast: &syn::DeriveInput) -> TokenStream {
|
fn impl_derive_safestring_macro(ast: &DeriveInput) -> TokenStream {
|
||||||
let name = &ast.ident;
|
let name = &ast.ident;
|
||||||
let gen_derive = quote! {
|
let gen_derive = quote! {
|
||||||
#[automatically_derived]
|
#[automatically_derived]
|
||||||
|
|||||||
Whitespace-only changes.
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE users ADD COLUMN key_id TEXT;
|
||||||
@@ -1,15 +1,31 @@
|
|||||||
-- Dynamically create DROP FOREIGN KEY
|
SELECT if (
|
||||||
-- Some versions of MySQL or MariaDB might fail if the key doesn't exists
|
EXISTS(
|
||||||
-- This checks if the key exists, and if so, will drop it.
|
SELECT CONSTRAINT_NAME FROM information_schema.table_constraints
|
||||||
SET @drop_sso_fk = IF((SELECT true FROM information_schema.TABLE_CONSTRAINTS WHERE
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
CONSTRAINT_SCHEMA = DATABASE() AND
|
AND TABLE_NAME = 'sso_users'
|
||||||
TABLE_NAME = 'sso_users' AND
|
AND CONSTRAINT_TYPE = 'FOREIGN KEY'
|
||||||
CONSTRAINT_NAME = 'sso_users_ibfk_1' AND
|
AND CONSTRAINT_NAME = 'sso_users_ibfk_1'
|
||||||
CONSTRAINT_TYPE = 'FOREIGN KEY') = true,
|
)
|
||||||
'ALTER TABLE sso_users DROP FOREIGN KEY sso_users_ibfk_1',
|
,'ALTER TABLE sso_users DROP FOREIGN KEY `sso_users_ibfk_1`'
|
||||||
'SELECT 1');
|
,'SELECT "info: FK sso_users_ibfk_1 does not exist."'
|
||||||
PREPARE stmt FROM @drop_sso_fk;
|
) INTO @drop_stmt;
|
||||||
EXECUTE stmt;
|
PREPARE drop_stmt FROM @drop_stmt;
|
||||||
DEALLOCATE PREPARE stmt;
|
EXECUTE drop_stmt;
|
||||||
|
|
||||||
|
SELECT if (
|
||||||
|
EXISTS(
|
||||||
|
SELECT CONSTRAINT_NAME FROM information_schema.table_constraints
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'sso_users'
|
||||||
|
AND CONSTRAINT_TYPE = 'FOREIGN KEY'
|
||||||
|
AND CONSTRAINT_NAME = '1'
|
||||||
|
)
|
||||||
|
,'ALTER TABLE sso_users DROP FOREIGN KEY `1`'
|
||||||
|
,'SELECT "info: FK sso_users 1 does not exist."'
|
||||||
|
) INTO @drop_stmt;
|
||||||
|
PREPARE drop_stmt FROM @drop_stmt;
|
||||||
|
EXECUTE drop_stmt;
|
||||||
|
|
||||||
|
DEALLOCATE PREPARE drop_stmt;
|
||||||
|
|
||||||
ALTER TABLE sso_users ADD FOREIGN KEY(user_uuid) REFERENCES users(uuid) ON UPDATE CASCADE ON DELETE CASCADE;
|
ALTER TABLE sso_users ADD FOREIGN KEY(user_uuid) REFERENCES users(uuid) ON UPDATE CASCADE ON DELETE CASCADE;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS archives;
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
DROP TABLE IF EXISTS archives;
|
||||||
|
|
||||||
|
CREATE TABLE archives (
|
||||||
|
user_uuid CHAR(36) NOT NULL,
|
||||||
|
cipher_uuid CHAR(36) NOT NULL,
|
||||||
|
archived_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (user_uuid, cipher_uuid),
|
||||||
|
FOREIGN KEY (user_uuid) REFERENCES users (uuid) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (cipher_uuid) REFERENCES ciphers (uuid) ON DELETE CASCADE
|
||||||
|
);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE sso_auth DROP COLUMN binding_hash;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE sso_auth ADD COLUMN binding_hash TEXT;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE sso_auth DROP COLUMN code_response_error;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE sso_auth ADD COLUMN code_response_error TEXT;
|
||||||
Whitespace-only changes.
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE users ADD COLUMN key_id TEXT;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE org_policies DROP COLUMN revision_date;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
-- DATETIME (not TIMESTAMP) to match this repo's convention for revision_date
|
||||||
|
-- columns elsewhere, and to avoid MySQL's implicit session-timezone
|
||||||
|
-- conversion and 2038 range limit on TIMESTAMP.
|
||||||
|
ALTER TABLE org_policies
|
||||||
|
ADD COLUMN revision_date DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00';
|
||||||
|
|
||||||
|
UPDATE org_policies SET revision_date = UTC_TIMESTAMP();
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS archives;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
DROP TABLE IF EXISTS archives;
|
||||||
|
|
||||||
|
CREATE TABLE archives (
|
||||||
|
user_uuid CHAR(36) NOT NULL REFERENCES users (uuid) ON DELETE CASCADE,
|
||||||
|
cipher_uuid CHAR(36) NOT NULL REFERENCES ciphers (uuid) ON DELETE CASCADE,
|
||||||
|
archived_at TIMESTAMP NOT NULL DEFAULT now(),
|
||||||
|
PRIMARY KEY (user_uuid, cipher_uuid)
|
||||||
|
);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE sso_auth DROP COLUMN binding_hash;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE sso_auth ADD COLUMN binding_hash TEXT;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE sso_auth DROP COLUMN IF EXISTS code_response_error;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE sso_auth ADD COLUMN IF NOT EXISTS code_response_error TEXT;
|
||||||
Whitespace-only changes.
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE users ADD COLUMN key_id TEXT;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE org_policies DROP COLUMN revision_date;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- Backfill via `now() AT TIME ZONE 'utc'` rather than a DEFAULT of now():
|
||||||
|
-- assigning timestamptz now() into a naive TIMESTAMP column casts through
|
||||||
|
-- the server's TimeZone GUC, so a DEFAULT now() would store local wall-clock
|
||||||
|
-- instead of UTC on non-UTC servers, unlike every other naive-UTC timestamp
|
||||||
|
-- column in this schema.
|
||||||
|
ALTER TABLE org_policies
|
||||||
|
ADD COLUMN revision_date TIMESTAMP NOT NULL DEFAULT '1970-01-01 00:00:00';
|
||||||
|
|
||||||
|
UPDATE org_policies SET revision_date = (now() AT TIME ZONE 'utc');
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS archives;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
DROP TABLE IF EXISTS archives;
|
||||||
|
|
||||||
|
CREATE TABLE archives (
|
||||||
|
user_uuid CHAR(36) NOT NULL REFERENCES users (uuid) ON DELETE CASCADE,
|
||||||
|
cipher_uuid CHAR(36) NOT NULL REFERENCES ciphers (uuid) ON DELETE CASCADE,
|
||||||
|
archived_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (user_uuid, cipher_uuid)
|
||||||
|
);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE sso_auth DROP COLUMN binding_hash;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE sso_auth ADD COLUMN binding_hash TEXT;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE sso_auth DROP COLUMN code_response_error;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE sso_auth ADD COLUMN code_response_error TEXT;
|
||||||
Whitespace-only changes.
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE users ADD COLUMN key_id TEXT;
|
||||||
Whitespace-only changes.
@@ -0,0 +1,6 @@
|
|||||||
|
-- SQLite forbids non-constant defaults in ALTER TABLE ... ADD COLUMN, so add
|
||||||
|
-- the column with a constant placeholder and backfill separately.
|
||||||
|
ALTER TABLE org_policies
|
||||||
|
ADD COLUMN revision_date DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00';
|
||||||
|
|
||||||
|
UPDATE org_policies SET revision_date = CURRENT_TIMESTAMP;
|
||||||
@@ -21,11 +21,19 @@ TEST_USER3=test3
|
|||||||
TEST_USER3_PASSWORD=${TEST_USER3}
|
TEST_USER3_PASSWORD=${TEST_USER3}
|
||||||
TEST_USER3_MAIL=${TEST_USER3}@yopmail.com
|
TEST_USER3_MAIL=${TEST_USER3}@yopmail.com
|
||||||
|
|
||||||
|
TEST_USER4=test4
|
||||||
|
TEST_USER4_PASSWORD=${TEST_USER4}
|
||||||
|
TEST_USER4_MAIL=${TEST_USER4}@yopmail.com
|
||||||
|
|
||||||
|
TEST_USER5=test5
|
||||||
|
TEST_USER5_PASSWORD=${TEST_USER5}
|
||||||
|
TEST_USER5_MAIL=${TEST_USER5}@yopmail.com
|
||||||
|
|
||||||
###################
|
###################
|
||||||
# Keycloak Config #
|
# Keycloak Config #
|
||||||
###################
|
###################
|
||||||
KEYCLOAK_ADMIN=admin
|
KC_BOOTSTRAP_ADMIN_USERNAME=admin
|
||||||
KEYCLOAK_ADMIN_PASSWORD=${KEYCLOAK_ADMIN}
|
KC_BOOTSTRAP_ADMIN_PASSWORD=${KC_BOOTSTRAP_ADMIN_USERNAME}
|
||||||
KC_HTTP_HOST=127.0.0.1
|
KC_HTTP_HOST=127.0.0.1
|
||||||
KC_HTTP_PORT=8080
|
KC_HTTP_PORT=8080
|
||||||
|
|
||||||
@@ -39,8 +47,10 @@ DUMMY_AUTHORITY=http://${KC_HTTP_HOST}:${KC_HTTP_PORT}/realms/${DUMMY_REALM}
|
|||||||
######################
|
######################
|
||||||
ROCKET_ADDRESS=0.0.0.0
|
ROCKET_ADDRESS=0.0.0.0
|
||||||
ROCKET_PORT=8000
|
ROCKET_PORT=8000
|
||||||
DOMAIN=http://localhost:${ROCKET_PORT}
|
ROCKET_TLS={certs="/data/ssl/localhost.crt",key="/data/ssl/localhost.key"}
|
||||||
|
DOMAIN=https://127.0.0.1:${ROCKET_PORT}
|
||||||
LOG_LEVEL=info,oidcwarden::sso=debug
|
LOG_LEVEL=info,oidcwarden::sso=debug
|
||||||
|
SSO_DEBUG_TOKENS=true
|
||||||
I_REALLY_WANT_VOLATILE_STORAGE=true
|
I_REALLY_WANT_VOLATILE_STORAGE=true
|
||||||
|
|
||||||
SSO_ENABLED=true
|
SSO_ENABLED=true
|
||||||
|
|||||||
+15
-17
@@ -1,8 +1,8 @@
|
|||||||
# Integration tests
|
# Integration tests
|
||||||
|
|
||||||
This allows running integration tests using [Playwright](https://playwright.dev/).
|
This allows running integration tests using [Playwright](https://playwright.dev/).
|
||||||
|
\
|
||||||
It uses its own `test.env` with different ports to not collide with a running dev instance.
|
It uses its own [test.env](./test.env) with different ports to not collide with a running dev instance.
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
@@ -11,11 +11,11 @@ Databases (`Mariadb`, `Mysql` and `Postgres`) and `Playwright` will run in conta
|
|||||||
|
|
||||||
### Running Playwright outside docker
|
### Running Playwright outside docker
|
||||||
|
|
||||||
It is possible to run `Playwright` outside of the container, this removes the need to rebuild the image for each change.
|
It's possible to run `Playwright` outside of the container, this remove the need to rebuild the image for each change.
|
||||||
You will additionally need `nodejs` then run:
|
You'll additionally need `nodejs` then run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm ci --ignore-scripts
|
npm ci --ignore-scripts --allow-git=none --allow-remote=none
|
||||||
npx playwright install-deps
|
npx playwright install-deps
|
||||||
npx playwright install firefox
|
npx playwright install firefox
|
||||||
```
|
```
|
||||||
@@ -62,10 +62,10 @@ DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env run Pl
|
|||||||
|
|
||||||
### Keep services running
|
### Keep services running
|
||||||
|
|
||||||
If you want you can keep the DB and Keycloak runnning (states are not impacted by the tests):
|
If you want you can keep the DB and Keycloak running (states are not impacted by the tests):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
PW_KEEP_SERVICE_RUNNNING=true npx playwright test
|
PW_KEEP_SERVICE_RUNNING=true npx playwright test
|
||||||
```
|
```
|
||||||
|
|
||||||
### Running specific tests
|
### Running specific tests
|
||||||
@@ -77,7 +77,7 @@ DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env run Pl
|
|||||||
DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env run Playwright test --project=sqlite login
|
DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env run Playwright test --project=sqlite login
|
||||||
```
|
```
|
||||||
|
|
||||||
To run only a specifc test (It might fail if it has dependency):
|
To run only a specific test (It might fail if it has dependency):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env run Playwright test --project=sqlite -g "Account creation"
|
DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env run Playwright test --project=sqlite -g "Account creation"
|
||||||
@@ -92,7 +92,7 @@ This does not start the server, you will need to start it manually.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env up Vaultwarden
|
DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env up Vaultwarden
|
||||||
npx playwright codegen "http://127.0.0.1:8003"
|
npx playwright codegen "https://127.0.0.1:8000" --ignore-https-errors
|
||||||
```
|
```
|
||||||
|
|
||||||
## Override web-vault
|
## Override web-vault
|
||||||
@@ -112,12 +112,11 @@ You can check the result running:
|
|||||||
DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env up Vaultwarden
|
DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env up Vaultwarden
|
||||||
```
|
```
|
||||||
|
|
||||||
Then check `http://127.0.0.1:8003/admin/diagnostics` with `admin`.
|
Then check `https://127.0.0.1:8003/admin/diagnostics` with `admin`.
|
||||||
|
|
||||||
# OpenID Connect test setup
|
# OpenID Connect test setup
|
||||||
|
|
||||||
Additionally this `docker-compose` template allows to run locally Vaultwarden,
|
Additionally this `docker-compose` template allow to run locally `Vaultwarden`, [Keycloak](https://www.keycloak.org/) and [Maildev](https://github.com/timshel/maildev) to test OIDC.
|
||||||
[Keycloak](https://www.keycloak.org/) and [Maildev](https://github.com/timshel/maildev) to test OIDC.
|
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
@@ -131,18 +130,17 @@ Then start the stack (the `profile` is required to run `Vaultwarden`) :
|
|||||||
```bash
|
```bash
|
||||||
> docker compose --profile vaultwarden --env-file .env up
|
> docker compose --profile vaultwarden --env-file .env up
|
||||||
....
|
....
|
||||||
keycloakSetup_1 | Logging into http://127.0.0.1:8080 as user admin of realm master
|
keycloakSetup_1 | Logging into https://127.0.0.1:8080 as user admin of realm master
|
||||||
keycloakSetup_1 | Created new realm with id 'test'
|
keycloakSetup_1 | Created new realm with id 'test'
|
||||||
keycloakSetup_1 | 74af4933-e386-4e64-ba15-a7b61212c45e
|
keycloakSetup_1 | 74af4933-e386-4e64-ba15-a7b61212c45e
|
||||||
oidc_keycloakSetup_1 exited with code 0
|
oidc_keycloakSetup_1 exited with code 0
|
||||||
```
|
```
|
||||||
|
|
||||||
Wait until `oidc_keycloakSetup_1 exited with code 0` which indicates the correct setup of the Keycloak realm, client and user
|
Wait until `oidc_keycloakSetup_1 exited with code 0` which indicate the correct setup of the Keycloak realm, client and user (It's normal for this container to stop once the configuration is done).
|
||||||
(It is normal for this container to stop once the configuration is done).
|
|
||||||
|
|
||||||
Then you can access :
|
Then you can access :
|
||||||
|
|
||||||
- `Vaultwarden` on http://0.0.0.0:8000 with the default user `test@yopmail.com/test`.
|
- `Vaultwarden` on https://0.0.0.0:8000 with the default user `test@yopmail.com/test`.
|
||||||
- `Keycloak` on http://0.0.0.0:8080/admin/master/console/ with the default user `admin/admin`
|
- `Keycloak` on http://0.0.0.0:8080/admin/master/console/ with the default user `admin/admin`
|
||||||
- `Maildev` on http://0.0.0.0:1080
|
- `Maildev` on http://0.0.0.0:1080
|
||||||
|
|
||||||
@@ -171,7 +169,7 @@ docker compose --profile vaultwarden --env-file .env build VaultwardenPrebuild V
|
|||||||
All configuration for `keycloak` / `Vaultwarden` / `keycloak_setup.sh` can be found in [.env](.env.template).
|
All configuration for `keycloak` / `Vaultwarden` / `keycloak_setup.sh` can be found in [.env](.env.template).
|
||||||
The content of the file will be loaded as environment variables in all containers.
|
The content of the file will be loaded as environment variables in all containers.
|
||||||
|
|
||||||
- `keycloak` [configuration](https://www.keycloak.org/server/all-config) includes `KEYCLOAK_ADMIN` / `KEYCLOAK_ADMIN_PASSWORD` and any variable prefixed `KC_` ([more information](https://www.keycloak.org/server/configuration#_example_configuring_the_db_url_host_parameter)).
|
- `keycloak` [configuration](https://www.keycloak.org/server/all-config) include `KC_BOOTSTRAP_ADMIN_USERNAME` / `KC_BOOTSTRAP_ADMIN_PASSWORD` and any variable prefixed `KC_` ([more information](https://www.keycloak.org/server/configuration#_example_configuring_the_db_url_host_parameter)).
|
||||||
- All `Vaultwarden` configuration can be set (EX: `SMTP_*`)
|
- All `Vaultwarden` configuration can be set (EX: `SMTP_*`)
|
||||||
|
|
||||||
## Cleanup
|
## Cleanup
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ done
|
|||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
kcadm.sh config credentials --server "http://${KC_HTTP_HOST}:${KC_HTTP_PORT}" --realm master --user "$KEYCLOAK_ADMIN" --password "$KEYCLOAK_ADMIN_PASSWORD" --client admin-cli
|
kcadm.sh config credentials --server "http://${KC_HTTP_HOST}:${KC_HTTP_PORT}" --realm master --user "$KC_BOOTSTRAP_ADMIN_USERNAME" --password "$KC_BOOTSTRAP_ADMIN_PASSWORD" --client admin-cli
|
||||||
|
|
||||||
kcadm.sh create realms -s realm="$TEST_REALM" -s enabled=true -s "accessTokenLifespan=600"
|
kcadm.sh create realms -s realm="$TEST_REALM" -s enabled=true -s "accessTokenLifespan=600"
|
||||||
kcadm.sh create clients -r test -s "clientId=$SSO_CLIENT_ID" -s "secret=$SSO_CLIENT_SECRET" -s "redirectUris=[\"$DOMAIN/*\"]" -i
|
kcadm.sh create clients -r test -s "clientId=$SSO_CLIENT_ID" -s "secret=$SSO_CLIENT_SECRET" -s "redirectUris=[\"$DOMAIN/*\"]" -i
|
||||||
@@ -39,6 +39,6 @@ kcadm.sh create realms -s realm="$DUMMY_REALM" -s enabled=true -s "accessTokenLi
|
|||||||
# THEN in another terminal:
|
# THEN in another terminal:
|
||||||
# docker exec -it keycloakSetup-dev /bin/bash
|
# docker exec -it keycloakSetup-dev /bin/bash
|
||||||
# export PATH=$PATH:/opt/keycloak/bin
|
# export PATH=$PATH:/opt/keycloak/bin
|
||||||
# kcadm.sh config credentials --server "http://${KC_HTTP_HOST}:${KC_HTTP_PORT}" --realm master --user "$KEYCLOAK_ADMIN" --password "$KEYCLOAK_ADMIN_PASSWORD" --client admin-cli
|
# kcadm.sh config credentials --server "http://${KC_HTTP_HOST}:${KC_HTTP_PORT}" --realm master --user "$KC_BOOTSTRAP_ADMIN_USERNAME" --password "$KC_BOOTSTRAP_ADMIN_PASSWORD" --client admin-cli
|
||||||
# ENJOY
|
# ENJOY
|
||||||
# Doc: https://wjw465150.gitbooks.io/keycloak-documentation/content/server_admin/topics/admin-cli.html
|
# Doc: https://wjw465150.gitbooks.io/keycloak-documentation/content/server_admin/topics/admin-cli.html
|
||||||
@@ -28,7 +28,7 @@ RUN mkdir /playwright
|
|||||||
WORKDIR /playwright
|
WORKDIR /playwright
|
||||||
|
|
||||||
COPY package.json package-lock.json .
|
COPY package.json package-lock.json .
|
||||||
RUN npm ci --ignore-scripts && npx playwright install-deps && npx playwright install firefox
|
RUN npm ci --ignore-scripts --allow-git=none --allow-remote=none && npx playwright install-deps && npx playwright install firefox
|
||||||
|
|
||||||
COPY docker-compose.yml test.env ./
|
COPY docker-compose.yml test.env ./
|
||||||
COPY compose ./compose
|
COPY compose ./compose
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ WORKDIR /
|
|||||||
|
|
||||||
COPY --from=prebuilt /start.sh .
|
COPY --from=prebuilt /start.sh .
|
||||||
COPY --from=prebuilt /vaultwarden .
|
COPY --from=prebuilt /vaultwarden .
|
||||||
|
COPY --from=build /data ./data
|
||||||
COPY --from=build /web-vault ./web-vault
|
COPY --from=build /web-vault ./web-vault
|
||||||
|
|
||||||
ENTRYPOINT ["/start.sh"]
|
ENTRYPOINT ["/start.sh"]
|
||||||
@@ -22,3 +22,14 @@ if [[ ! -z "$REPO_URL" ]] && [[ ! -z "$COMMIT_HASH" ]] ; then
|
|||||||
|
|
||||||
mv build /web-vault
|
mv build /web-vault
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Lower the KDF iterations default for faster tests.
|
||||||
|
sed -i 's/(6e5,2e6,6e5)/(1e5,2e6,1e5)/' /web-vault/app/main.*.js
|
||||||
|
|
||||||
|
# Generate a self signed cert
|
||||||
|
mkdir -p /data/ssl; cd /data/ssl
|
||||||
|
|
||||||
|
openssl req -x509 -out localhost.crt -keyout localhost.key \
|
||||||
|
-newkey rsa:2048 -nodes -sha256 \
|
||||||
|
-subj '/CN=localhost' -extensions EXT -config <( \
|
||||||
|
printf "[dn]\nCN=localhost\n[req]\ndistinguished_name = dn\n[EXT]\nsubjectAltName=DNS:localhost\nkeyUsage=digitalSignature\nextendedKeyUsage=serverAuth")
|
||||||
@@ -24,12 +24,15 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- ADMIN_TOKEN
|
- ADMIN_TOKEN
|
||||||
- DATABASE_URL
|
- DATABASE_URL
|
||||||
|
- CLIENT_SUPPRESS_ONBOARDING
|
||||||
|
- EMAIL_2FA_AUTO_FALLBACK
|
||||||
- I_REALLY_WANT_VOLATILE_STORAGE
|
- I_REALLY_WANT_VOLATILE_STORAGE
|
||||||
- LOG_LEVEL
|
- LOG_LEVEL
|
||||||
- LOGIN_RATELIMIT_MAX_BURST
|
- LOGIN_RATELIMIT_MAX_BURST
|
||||||
- SMTP_HOST
|
- SMTP_HOST
|
||||||
- SMTP_FROM
|
- SMTP_FROM
|
||||||
- SMTP_DEBUG
|
- SMTP_DEBUG
|
||||||
|
- SSO_AUTH_ONLY_NOT_SESSION
|
||||||
- SSO_DEBUG_TOKENS
|
- SSO_DEBUG_TOKENS
|
||||||
- SSO_ENABLED
|
- SSO_ENABLED
|
||||||
- SSO_FRONTEND
|
- SSO_FRONTEND
|
||||||
@@ -58,7 +61,7 @@ services:
|
|||||||
Mariadb:
|
Mariadb:
|
||||||
profiles: ["playwright"]
|
profiles: ["playwright"]
|
||||||
container_name: playwright_mariadb
|
container_name: playwright_mariadb
|
||||||
image: mariadb:11.2.4
|
image: mariadb:12.2.2
|
||||||
env_file: test.env
|
env_file: test.env
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
|
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
|
||||||
@@ -70,7 +73,7 @@ services:
|
|||||||
Mysql:
|
Mysql:
|
||||||
profiles: ["playwright"]
|
profiles: ["playwright"]
|
||||||
container_name: playwright_mysql
|
container_name: playwright_mysql
|
||||||
image: mysql:8.4.1
|
image: mysql:9.7.0
|
||||||
env_file: test.env
|
env_file: test.env
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"]
|
test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"]
|
||||||
@@ -82,7 +85,7 @@ services:
|
|||||||
Postgres:
|
Postgres:
|
||||||
profiles: ["playwright"]
|
profiles: ["playwright"]
|
||||||
container_name: playwright_postgres
|
container_name: playwright_postgres
|
||||||
image: postgres:16.3
|
image: postgres:18.4
|
||||||
env_file: test.env
|
env_file: test.env
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
|
test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
|
||||||
@@ -94,7 +97,7 @@ services:
|
|||||||
Maildev:
|
Maildev:
|
||||||
profiles: ["vaultwarden", "maildev"]
|
profiles: ["vaultwarden", "maildev"]
|
||||||
container_name: maildev
|
container_name: maildev
|
||||||
image: timshel/maildev:3.0.4
|
image: timshel/maildev:3.2.19
|
||||||
ports:
|
ports:
|
||||||
- ${SMTP_PORT}:1025
|
- ${SMTP_PORT}:1025
|
||||||
- 1080:1080
|
- 1080:1080
|
||||||
@@ -102,7 +105,7 @@ services:
|
|||||||
Keycloak:
|
Keycloak:
|
||||||
profiles: ["keycloak", "vaultwarden"]
|
profiles: ["keycloak", "vaultwarden"]
|
||||||
container_name: keycloak-${ENV:-dev}
|
container_name: keycloak-${ENV:-dev}
|
||||||
image: quay.io/keycloak/keycloak:26.3.4
|
image: quay.io/keycloak/keycloak:26.6.2
|
||||||
network_mode: "host"
|
network_mode: "host"
|
||||||
command:
|
command:
|
||||||
- start-dev
|
- start-dev
|
||||||
@@ -112,12 +115,12 @@ services:
|
|||||||
profiles: ["keycloak", "vaultwarden"]
|
profiles: ["keycloak", "vaultwarden"]
|
||||||
container_name: keycloakSetup-${ENV:-dev}
|
container_name: keycloakSetup-${ENV:-dev}
|
||||||
image: keycloak_setup-${ENV:-dev}
|
image: keycloak_setup-${ENV:-dev}
|
||||||
|
network_mode: "host"
|
||||||
build:
|
build:
|
||||||
context: compose/keycloak
|
context: compose/keycloak
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
args:
|
args:
|
||||||
KEYCLOAK_VERSION: 26.3.4
|
KEYCLOAK_VERSION: 26.6.2
|
||||||
network_mode: "host"
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- Keycloak
|
- Keycloak
|
||||||
restart: "no"
|
restart: "no"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { firefox, type FullConfig } from '@playwright/test';
|
import { type FullConfig } from '@playwright/test';
|
||||||
import { execSync } from 'node:child_process';
|
import { execSync } from 'node:child_process';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ export async function startVault(browser: Browser, testInfo: TestInfo, env = {},
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function stopVault(force: boolean = false) {
|
export async function stopVault(force: boolean = false) {
|
||||||
if( force === false && process.env.PW_KEEP_SERVICE_RUNNNING === "true" ) {
|
if( force === false && process.env.PW_KEEP_SERVICE_RUNNING === "true" ) {
|
||||||
console.log(`Keep vaultwarden running on: ${process.env.DOMAIN}`);
|
console.log(`Keep vaultwarden running on: ${process.env.DOMAIN}`);
|
||||||
} else {
|
} else {
|
||||||
console.log(`Vaultwarden stopping`);
|
console.log(`Vaultwarden stopping`);
|
||||||
@@ -231,6 +231,7 @@ export async function checkNotification(page: Page, hasText: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function cleanLanding(page: Page) {
|
export async function cleanLanding(page: Page) {
|
||||||
|
await page.context().clearCookies();
|
||||||
await page.goto('/', { waitUntil: 'domcontentloaded' });
|
await page.goto('/', { waitUntil: 'domcontentloaded' });
|
||||||
await expect(page.getByRole('button').nth(0)).toBeVisible();
|
await expect(page.getByRole('button').nth(0)).toBeVisible();
|
||||||
|
|
||||||
@@ -248,15 +249,3 @@ export async function logout(test: Test, page: Page, user: { name: string }) {
|
|||||||
await expect(page.getByRole('heading', { name: 'Log in' })).toBeVisible();
|
await expect(page.getByRole('heading', { name: 'Log in' })).toBeVisible();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function ignoreExtension(page: Page) {
|
|
||||||
await page.waitForLoadState('domcontentloaded');
|
|
||||||
|
|
||||||
try {
|
|
||||||
await page.getByRole('button', { name: 'Add it later' }).click({timeout: 5_000});
|
|
||||||
await page.getByRole('link', { name: 'Skip to web app' }).click();
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Extension setup not visible. Continuing');
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
Generated
+582
-580
File diff suppressed because it is too large.
Load diff
@@ -8,14 +8,14 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "1.56.1",
|
"@playwright/test": "1.60.0",
|
||||||
"dotenv": "17.2.3",
|
"dotenv": "17.4.2",
|
||||||
"dotenv-expand": "12.0.3",
|
"dotenv-expand": "13.0.0",
|
||||||
"maildev": "npm:@timshel_npm/maildev@3.2.5"
|
"maildev": "npm:@timshel_npm/maildev@3.2.19"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mysql2": "3.15.3",
|
"mysql2": "3.22.3",
|
||||||
"otpauth": "9.4.1",
|
"otpauth": "9.5.1",
|
||||||
"pg": "8.16.3"
|
"pg": "8.21.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -25,10 +25,12 @@ export default defineConfig({
|
|||||||
/* Long global timeout for complex tests
|
/* Long global timeout for complex tests
|
||||||
* But short action/nav/expect timeouts to fail on specific step (raise locally if not enough).
|
* But short action/nav/expect timeouts to fail on specific step (raise locally if not enough).
|
||||||
*/
|
*/
|
||||||
timeout: 120 * 1000,
|
timeout: 240 * 1000,
|
||||||
actionTimeout: 20 * 1000,
|
actionTimeout: 40 * 1000,
|
||||||
navigationTimeout: 20 * 1000,
|
navigationTimeout: 40 * 1000,
|
||||||
expect: { timeout: 20 * 1000 },
|
expect: { timeout: 40 * 1000 },
|
||||||
|
|
||||||
|
"permissions": ["clipboard-read"],
|
||||||
|
|
||||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||||
use: {
|
use: {
|
||||||
@@ -37,6 +39,10 @@ export default defineConfig({
|
|||||||
browserName: 'firefox',
|
browserName: 'firefox',
|
||||||
locale: 'en-GB',
|
locale: 'en-GB',
|
||||||
timezoneId: 'Europe/London',
|
timezoneId: 'Europe/London',
|
||||||
|
ignoreHTTPSErrors: true,
|
||||||
|
launchOptions: {
|
||||||
|
args: ['--ignore-certificate-errors']
|
||||||
|
},
|
||||||
|
|
||||||
/* Always collect trace (other values add random test failures) See https://playwright.dev/docs/trace-viewer */
|
/* Always collect trace (other values add random test failures) See https://playwright.dev/docs/trace-viewer */
|
||||||
trace: 'on',
|
trace: 'on',
|
||||||
|
|||||||
+6
-4
@@ -10,7 +10,7 @@ DOCKER_BUILDKIT=1
|
|||||||
#####################
|
#####################
|
||||||
# Playwright Config #
|
# Playwright Config #
|
||||||
#####################
|
#####################
|
||||||
PW_KEEP_SERVICE_RUNNNING=${PW_KEEP_SERVICE_RUNNNING:-false}
|
PW_KEEP_SERVICE_RUNNING=${PW_KEEP_SERVICE_RUNNING:-false}
|
||||||
PW_SMTP_FROM=vaultwarden@playwright.test
|
PW_SMTP_FROM=vaultwarden@playwright.test
|
||||||
|
|
||||||
#####################
|
#####################
|
||||||
@@ -38,8 +38,8 @@ TEST_USER3_MAIL=${TEST_USER3}@example.com
|
|||||||
###################
|
###################
|
||||||
# Keycloak Config #
|
# Keycloak Config #
|
||||||
###################
|
###################
|
||||||
KEYCLOAK_ADMIN=admin
|
KC_BOOTSTRAP_ADMIN_USERNAME=admin
|
||||||
KEYCLOAK_ADMIN_PASSWORD=${KEYCLOAK_ADMIN}
|
KC_BOOTSTRAP_ADMIN_PASSWORD=${KC_BOOTSTRAP_ADMIN_USERNAME}
|
||||||
KC_HTTP_HOST=127.0.0.1
|
KC_HTTP_HOST=127.0.0.1
|
||||||
KC_HTTP_PORT=8081
|
KC_HTTP_PORT=8081
|
||||||
|
|
||||||
@@ -52,10 +52,12 @@ DUMMY_AUTHORITY=http://${KC_HTTP_HOST}:${KC_HTTP_PORT}/realms/${DUMMY_REALM}
|
|||||||
# Vaultwarden Config #
|
# Vaultwarden Config #
|
||||||
######################
|
######################
|
||||||
ROCKET_PORT=8003
|
ROCKET_PORT=8003
|
||||||
DOMAIN=http://localhost:${ROCKET_PORT}
|
ROCKET_TLS={certs="/data/ssl/localhost.crt",key="/data/ssl/localhost.key"}
|
||||||
|
DOMAIN=https://127.0.0.1:${ROCKET_PORT}
|
||||||
LOG_LEVEL=info,oidcwarden::sso=debug
|
LOG_LEVEL=info,oidcwarden::sso=debug
|
||||||
LOGIN_RATELIMIT_MAX_BURST=100
|
LOGIN_RATELIMIT_MAX_BURST=100
|
||||||
ADMIN_TOKEN=admin
|
ADMIN_TOKEN=admin
|
||||||
|
CLIENT_SUPPRESS_ONBOARDING=true
|
||||||
|
|
||||||
SMTP_SECURITY=off
|
SMTP_SECURITY=off
|
||||||
SMTP_PORT=${MAILDEV_SMTP_PORT}
|
SMTP_PORT=${MAILDEV_SMTP_PORT}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { test, expect, type TestInfo } from '@playwright/test';
|
import { test, expect, type TestInfo } from '@playwright/test';
|
||||||
|
|
||||||
import * as utils from "../global-utils";
|
import * as utils from "../global-utils";
|
||||||
|
|
||||||
|
import * as orgs from './setups/orgs';
|
||||||
import { createAccount } from './setups/user';
|
import { createAccount } from './setups/user';
|
||||||
|
|
||||||
let users = utils.loadEnv();
|
let users = utils.loadEnv();
|
||||||
@@ -16,20 +18,12 @@ test.afterAll('Teardown', async ({}) => {
|
|||||||
test('Create', async ({ page }) => {
|
test('Create', async ({ page }) => {
|
||||||
await createAccount(test, page, users.user1);
|
await createAccount(test, page, users.user1);
|
||||||
|
|
||||||
await test.step('Create Org', async () => {
|
await orgs.create(test, page, 'New organisation');
|
||||||
await page.getByRole('link', { name: 'New organisation' }).click();
|
|
||||||
await page.getByLabel('Organisation name (required)').fill('Test');
|
|
||||||
await page.getByRole('button', { name: 'Submit' }).click();
|
|
||||||
await page.locator('div').filter({ hasText: 'Members' }).nth(2).click();
|
|
||||||
|
|
||||||
await utils.checkNotification(page, 'Organisation created');
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step('Create Collection', async () => {
|
await test.step('Create Collection', async () => {
|
||||||
await page.getByRole('link', { name: 'Collections' }).click();
|
await page.getByRole('button', { name: 'New', exact: true }).click();
|
||||||
await page.getByRole('button', { name: 'New' }).click();
|
|
||||||
await page.getByRole('menuitem', { name: 'Collection' }).click();
|
await page.getByRole('menuitem', { name: 'Collection' }).click();
|
||||||
await page.getByLabel('Name (required)').fill('RandomCollec');
|
await page.getByRole('textbox', { name: 'Name * (required)', exact: true }).fill('RandomCollec');
|
||||||
await page.getByRole('button', { name: 'Save' }).click();
|
await page.getByRole('button', { name: 'Save' }).click();
|
||||||
await utils.checkNotification(page, 'Created collection RandomCollec');
|
await utils.checkNotification(page, 'Created collection RandomCollec');
|
||||||
await expect(page.getByRole('button', { name: 'RandomCollec' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'RandomCollec' })).toBeVisible();
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { test, expect, type Page, type TestInfo } from '@playwright/test';
|
||||||
|
import * as OTPAuth from "otpauth";
|
||||||
|
|
||||||
|
import * as utils from "../global-utils";
|
||||||
|
import { createAccount, logUser } from './setups/user';
|
||||||
|
import { activateTOTP, disableTOTP } from './setups/2fa';
|
||||||
|
|
||||||
|
let users = utils.loadEnv();
|
||||||
|
let totp;
|
||||||
|
|
||||||
|
test.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => {
|
||||||
|
await utils.startVault(browser, testInfo, {});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterAll('Teardown', async ({}) => {
|
||||||
|
utils.stopVault();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Change Key settings', async ({ page }) => {
|
||||||
|
await createAccount(test, page, users.user1);
|
||||||
|
|
||||||
|
await test.step('Change SHA-256 Iterations', async () => {
|
||||||
|
await page.getByRole('button', { name: 'Toggle collapse Settings' }).click();
|
||||||
|
await page.getByRole('link', { name: 'Security' }).click();
|
||||||
|
await page.getByRole('link', { name: 'Keys' }).click();
|
||||||
|
|
||||||
|
await page.getByRole('spinbutton', { name: 'KDF iterations * (required)'}).fill('700000');
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Update encryption settings' }).click();
|
||||||
|
await page.getByRole('textbox', { name: 'Master password * (required)' }).fill(users.user1.password);
|
||||||
|
await page.getByRole('button', { name: 'Update settings' }).click();
|
||||||
|
await page.getByRole('heading', { name: 'Log in' }).click();
|
||||||
|
});
|
||||||
|
|
||||||
|
await logUser(test, page, users.user1);
|
||||||
|
|
||||||
|
await test.step('Switch to Argon2', async () => {
|
||||||
|
await page.getByRole('button', { name: 'Toggle collapse Settings' }).click();
|
||||||
|
await page.getByRole('link', { name: 'Security' }).click();
|
||||||
|
await page.getByRole('link', { name: 'Keys' }).click();
|
||||||
|
|
||||||
|
await page.locator('.ng-arrow-wrapper').click();
|
||||||
|
await page.getByText('Argon2id').click();
|
||||||
|
|
||||||
|
await page.getByRole('spinbutton', { name: 'KDF memory (MB) * (required)'}).fill('16');
|
||||||
|
await page.getByRole('spinbutton', { name: 'KDF iterations * (required)'}).fill('2');
|
||||||
|
await page.getByRole('spinbutton', { name: 'KDF parallelism * (required)'}).fill('1');
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Update encryption settings' }).click();
|
||||||
|
await page.getByRole('textbox', { name: 'Master password * (required)' }).fill(users.user1.password);
|
||||||
|
await page.getByRole('button', { name: 'Update settings' }).click();
|
||||||
|
await page.getByRole('heading', { name: 'Log in' }).click();
|
||||||
|
});
|
||||||
|
|
||||||
|
await logUser(test, page, users.user1);
|
||||||
|
});
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
import { test, expect, type Page, type TestInfo, Test } from '@playwright/test';
|
||||||
|
import { MailDev } from 'maildev';
|
||||||
|
|
||||||
|
import * as utils from "../global-utils";
|
||||||
|
import { createAccount, logUser } from './setups/user';
|
||||||
|
import { activateTOTP } from './setups/2fa';
|
||||||
|
|
||||||
|
let users = utils.loadEnv();
|
||||||
|
let mailserver;
|
||||||
|
|
||||||
|
test.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => {
|
||||||
|
mailserver = new MailDev({
|
||||||
|
port: process.env.MAILDEV_SMTP_PORT,
|
||||||
|
web: { port: process.env.MAILDEV_HTTP_PORT },
|
||||||
|
})
|
||||||
|
|
||||||
|
await mailserver.listen();
|
||||||
|
|
||||||
|
await utils.startVault(browser, testInfo, {
|
||||||
|
SMTP_HOST: process.env.MAILDEV_HOST,
|
||||||
|
SMTP_FROM: process.env.PW_SMTP_FROM,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterAll('Teardown', async ({}) => {
|
||||||
|
utils.stopVault();
|
||||||
|
if( mailserver ){
|
||||||
|
await mailserver.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function emergencyAccess(test: Test, page: Page, user: { name: string }) {
|
||||||
|
await test.step('Navigate', async () => {
|
||||||
|
await page.getByRole('button', { name: user.name }).click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Account settings' }).click();
|
||||||
|
await page.getByRole('link', { name: 'Emergency access' }).click();
|
||||||
|
await expect(page.locator('#main-content').getByText('Emergency access', { exact: true })).toBeVisible();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test('Emergency access', async ({ browser, page }) => {
|
||||||
|
const context2 = await browser.newContext();
|
||||||
|
const page2 = await context2.newPage();
|
||||||
|
|
||||||
|
const mailBuffer = mailserver.buffer(users.user1.email);
|
||||||
|
const mailBuffer2 = mailserver.buffer(users.user2.email);
|
||||||
|
|
||||||
|
await createAccount(test, page, users.user1);
|
||||||
|
await createAccount(test, page2, users.user2);
|
||||||
|
|
||||||
|
await test.step('Add test2', async () => {
|
||||||
|
await emergencyAccess(test, page, users.user1);
|
||||||
|
await page.getByRole('button', { name: 'Add emergency contact' }).click();
|
||||||
|
await page.getByRole('textbox', { name: 'Email * (required)' }).fill(users.user2.email);
|
||||||
|
await page.getByRole('radio', { name: 'Takeover Can reset your' }).check();
|
||||||
|
await page.getByRole('button', { name: 'Save' }).click();
|
||||||
|
await utils.checkNotification(page, 'User(s) invited');
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Accept', async () => {
|
||||||
|
const email = await mailBuffer2.expect((m) => m.subject === "Emergency access for " + users.user1.name);
|
||||||
|
const pageE = await context2.newPage();
|
||||||
|
await pageE.setContent(email.html);
|
||||||
|
const link = await pageE.getByTestId("emergency").getAttribute("href");
|
||||||
|
await pageE.close();
|
||||||
|
|
||||||
|
await page2.goto(link);
|
||||||
|
await utils.checkNotification(page2, 'Invitation accepted');
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Confirm', async () => {
|
||||||
|
await emergencyAccess(test, page, users.user1);
|
||||||
|
await expect(page.locator('#main-content').getByText('Needs confirmation')).toBeVisible();
|
||||||
|
await page.getByRole('button', { name: 'Options' }).click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Confirm' }).click();
|
||||||
|
await page.getByRole('button', { name: 'Confirm' }).click();
|
||||||
|
await utils.checkNotification(page, users.user2.name + ' confirmed');
|
||||||
|
await mailBuffer2.expect((m) => m.subject === "Emergency access contact for " + users.user1.name + " confirmed");
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Request', async () => {
|
||||||
|
await emergencyAccess(test, page2, users.user2);
|
||||||
|
await page2.getByRole('button', { name: 'Options' }).click();
|
||||||
|
await page2.getByRole('menuitem', { name: 'Request Access' }).click();
|
||||||
|
await page2.getByRole('button', { name: 'Request Access' }).click();
|
||||||
|
await utils.checkNotification(page2, 'Emergency access requested');
|
||||||
|
await mailBuffer.expect((m) => m.subject === "Emergency access request by " + users.user2.name + " initiated");
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Approved', async () => {
|
||||||
|
await emergencyAccess(test, page, users.user1);
|
||||||
|
await page.getByRole('button', { name: 'Options' }).click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Approve' }).click();
|
||||||
|
await page.getByRole('button', { name: 'Approve' }).click();
|
||||||
|
await utils.checkNotification(page, 'Emergency access approved');
|
||||||
|
await mailBuffer2.expect((m) => m.subject === "Emergency access request for " + users.user1.name + " approved");
|
||||||
|
});
|
||||||
|
await activateTOTP(test, page, users.user1);
|
||||||
|
|
||||||
|
let newPassword = "TotoNewPassword";
|
||||||
|
await test.step('Access', async () => {
|
||||||
|
await emergencyAccess(test, page2, users.user2);
|
||||||
|
await page2.getByRole('button', { name: 'Options' }).click();
|
||||||
|
await page2.getByRole('menuitem', { name: 'Takeover' }).click();
|
||||||
|
await page2.getByRole('textbox', { name: 'New master password * (required)', exact: true }).fill(newPassword);
|
||||||
|
await page2.getByRole('textbox', { name: 'Confirm new master password' }).fill(newPassword);
|
||||||
|
await page2.getByRole('button', { name: 'Save' }).click();
|
||||||
|
await utils.checkNotification(page2, 'Password reset for ' + users.user1.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Changed no 2fa', async () => {
|
||||||
|
users.user1.password = newPassword;
|
||||||
|
await logUser(test, page, users.user1);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Reject', async () => {
|
||||||
|
await emergencyAccess(test, page, users.user1);
|
||||||
|
await page.getByRole('button', { name: 'Options' }).click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Reject' }).click();
|
||||||
|
await utils.checkNotification(page, 'Emergency access rejected');
|
||||||
|
await mailBuffer2.expect((m) => m.subject === "Emergency access request to " + users.user1.name + " rejected");
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Remove', async () => {
|
||||||
|
await page.getByRole('button', { name: 'Options' }).click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Remove' }).click();
|
||||||
|
await page.getByRole('button', { name: 'Yes' }).click();
|
||||||
|
await utils.checkNotification(page, 'Removed user ' + users.user2.name);
|
||||||
|
await expect(page.getByText('You have not added any emergency contacts')).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -41,13 +41,10 @@ test('Account creation', async ({ page }) => {
|
|||||||
test('Login', async ({ context, page }) => {
|
test('Login', async ({ context, page }) => {
|
||||||
const mailBuffer = mailserver.buffer(users.user1.email);
|
const mailBuffer = mailserver.buffer(users.user1.email);
|
||||||
|
|
||||||
await logUser(test, page, users.user1, mailBuffer);
|
await logUser(test, page, users.user1, { mailBuffer });
|
||||||
|
|
||||||
await test.step('verify email', async () => {
|
await test.step('verify email', async () => {
|
||||||
await page.getByText('Verify your account\'s email').click();
|
await page.getByRole('button', { name: "Send email" }).click();
|
||||||
await expect(page.getByText('Verify your account\'s email')).toBeVisible();
|
|
||||||
await page.getByRole('button', { name: 'Send email' }).click();
|
|
||||||
|
|
||||||
await utils.checkNotification(page, 'Check your email inbox for a verification link');
|
await utils.checkNotification(page, 'Check your email inbox for a verification link');
|
||||||
|
|
||||||
const verify = await mailBuffer.expect((m) => m.subject === "Verify Your Email");
|
const verify = await mailBuffer.expect((m) => m.subject === "Verify Your Email");
|
||||||
@@ -78,26 +75,10 @@ test('Activate 2fa', async ({ page }) => {
|
|||||||
test('2fa', async ({ page }) => {
|
test('2fa', async ({ page }) => {
|
||||||
const emails = mailserver.buffer(users.user1.email);
|
const emails = mailserver.buffer(users.user1.email);
|
||||||
|
|
||||||
await test.step('login', async () => {
|
await logUser(test, page, users.user1, {
|
||||||
await page.goto('/');
|
mailBuffer: emails,
|
||||||
|
mail2fa: true,
|
||||||
await page.getByLabel(/Email address/).fill(users.user1.email);
|
});
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
|
||||||
await page.getByLabel('Master password').fill(users.user1.password);
|
|
||||||
await page.getByRole('button', { name: 'Log in with master password' }).click();
|
|
||||||
|
|
||||||
await expect(page.getByRole('heading', { name: 'Verify your Identity' })).toBeVisible();
|
|
||||||
const code = await retrieveEmailCode(test, page, emails);
|
|
||||||
await page.getByLabel(/Verification code/).fill(code);
|
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Add it later' }).click();
|
|
||||||
await page.getByRole('link', { name: 'Skip to web app' }).click();
|
|
||||||
|
|
||||||
await expect(page).toHaveTitle(/Vaults/);
|
|
||||||
})
|
|
||||||
|
|
||||||
await disableEmail(test, page, users.user1);
|
|
||||||
|
|
||||||
emails.close();
|
emails.close();
|
||||||
});
|
});
|
||||||
@@ -3,7 +3,7 @@ import * as OTPAuth from "otpauth";
|
|||||||
|
|
||||||
import * as utils from "../global-utils";
|
import * as utils from "../global-utils";
|
||||||
import { createAccount, logUser } from './setups/user';
|
import { createAccount, logUser } from './setups/user';
|
||||||
import { activateTOTP, disableTOTP } from './setups/2fa';
|
import { activateTOTP, disableTOTP, recoveryCodes } from './setups/2fa';
|
||||||
|
|
||||||
let users = utils.loadEnv();
|
let users = utils.loadEnv();
|
||||||
let totp;
|
let totp;
|
||||||
@@ -31,21 +31,42 @@ test('Authenticator 2fa', async ({ page }) => {
|
|||||||
|
|
||||||
await utils.logout(test, page, users.user1);
|
await utils.logout(test, page, users.user1);
|
||||||
|
|
||||||
await test.step('login', async () => {
|
await logUser(test, page, users.user1, { totp });
|
||||||
let timestamp = Date.now(); // Needed to use the next token
|
|
||||||
timestamp = timestamp + (totp.period - (Math.floor(timestamp / 1000) % totp.period) + 1) * 1000;
|
|
||||||
|
|
||||||
await page.getByLabel(/Email address/).fill(users.user1.email);
|
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
|
||||||
await page.getByLabel('Master password').fill(users.user1.password);
|
|
||||||
await page.getByRole('button', { name: 'Log in with master password' }).click();
|
|
||||||
|
|
||||||
await expect(page.getByRole('heading', { name: 'Verify your Identity' })).toBeVisible();
|
|
||||||
await page.getByLabel(/Verification code/).fill(totp.generate({timestamp}));
|
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
|
||||||
|
|
||||||
await expect(page).toHaveTitle(/Vaultwarden Web/);
|
|
||||||
});
|
|
||||||
|
|
||||||
await disableTOTP(test, page, users.user1);
|
await disableTOTP(test, page, users.user1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Recovery codes', async ({ context, page }) => {
|
||||||
|
await logUser(test, page, users.user1);
|
||||||
|
|
||||||
|
await activateTOTP(test, page, users.user1);
|
||||||
|
let recovery = await recoveryCodes(test, page, users.user1);
|
||||||
|
|
||||||
|
await utils.logout(test, page, users.user1);
|
||||||
|
|
||||||
|
await test.step('login', async () => {
|
||||||
|
await page.getByLabel(/Email address/).fill(users.user1.email);
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(users.user1.password);
|
||||||
|
await page.getByRole('button', { name: 'Log in', exact: true }).click();
|
||||||
|
|
||||||
|
await expect(page.getByRole('heading', { name: 'Verify your Identity' })).toBeVisible();
|
||||||
|
|
||||||
|
await expect(page).toHaveTitle(/Vaultwarden Web/);
|
||||||
|
|
||||||
|
const newPagePromise = context.waitForEvent('page');
|
||||||
|
await page.getByRole('button', { name: 'Use your recovery code' }).click();
|
||||||
|
const newPage = await newPagePromise;
|
||||||
|
|
||||||
|
const tabs = context.pages();
|
||||||
|
await tabs[1].bringToFront();
|
||||||
|
await expect(tabs[1].getByRole('heading', { name: 'Recover account two-step login' })).toBeVisible();
|
||||||
|
await tabs[1].getByRole('textbox', { name: 'Email address * (required)' }).fill(users.user1.email);
|
||||||
|
await tabs[1].getByRole('textbox', { name: 'Master password * (required)' }).fill(users.user1.password);
|
||||||
|
await tabs[1].getByRole('textbox', { name: 'Recovery code * (required)' }).fill(recovery);
|
||||||
|
await tabs[1].getByRole('button', { name: 'Submit' }).click();
|
||||||
|
|
||||||
|
await expect(tabs[1]).toHaveTitle(/Two-step login/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
@@ -4,6 +4,7 @@ import { MailDev } from 'maildev';
|
|||||||
import * as utils from '../global-utils';
|
import * as utils from '../global-utils';
|
||||||
import * as orgs from './setups/orgs';
|
import * as orgs from './setups/orgs';
|
||||||
import { createAccount, logUser } from './setups/user';
|
import { createAccount, logUser } from './setups/user';
|
||||||
|
import { activateTOTP } from './setups/2fa';
|
||||||
|
|
||||||
let users = utils.loadEnv();
|
let users = utils.loadEnv();
|
||||||
|
|
||||||
@@ -20,6 +21,7 @@ test.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => {
|
|||||||
await utils.startVault(browser, testInfo, {
|
await utils.startVault(browser, testInfo, {
|
||||||
SMTP_HOST: process.env.MAILDEV_HOST,
|
SMTP_HOST: process.env.MAILDEV_HOST,
|
||||||
SMTP_FROM: process.env.PW_SMTP_FROM,
|
SMTP_FROM: process.env.PW_SMTP_FROM,
|
||||||
|
EMAIL_2FA_AUTO_FALLBACK: "true",
|
||||||
});
|
});
|
||||||
|
|
||||||
mail1Buffer = mailServer.buffer(users.user1.email);
|
mail1Buffer = mailServer.buffer(users.user1.email);
|
||||||
@@ -40,6 +42,16 @@ test('Invite users', async ({ page }) => {
|
|||||||
await createAccount(test, page, users.user1, mail1Buffer);
|
await createAccount(test, page, users.user1, mail1Buffer);
|
||||||
|
|
||||||
await orgs.create(test, page, 'Test');
|
await orgs.create(test, page, 'Test');
|
||||||
|
|
||||||
|
await test.step(`Set account recovery`, async () => {
|
||||||
|
await orgs.policies(test, page, 'Test');
|
||||||
|
await page.getByRole('button', { name: 'Account recovery' }).click();
|
||||||
|
await page.getByRole('checkbox', { name: 'Turn on' }).check();
|
||||||
|
await page.getByRole('checkbox', { name: 'Automatically enroll new' }).check();
|
||||||
|
await page.getByRole('button', { name: 'Save' }).click();
|
||||||
|
await utils.checkNotification(page, 'Edited policy Account recovery');
|
||||||
|
});
|
||||||
|
|
||||||
await orgs.members(test, page, 'Test');
|
await orgs.members(test, page, 'Test');
|
||||||
await orgs.invite(test, page, 'Test', users.user2.email);
|
await orgs.invite(test, page, 'Test', users.user2.email);
|
||||||
await orgs.invite(test, page, 'Test', users.user3.email, {
|
await orgs.invite(test, page, 'Test', users.user3.email, {
|
||||||
@@ -56,18 +68,16 @@ test('invited with new account', async ({ page }) => {
|
|||||||
await page.goto(link);
|
await page.goto(link);
|
||||||
await expect(page).toHaveTitle(/Create account | Vaultwarden Web/);
|
await expect(page).toHaveTitle(/Create account | Vaultwarden Web/);
|
||||||
|
|
||||||
//await page.getByLabel('Name').fill(users.user2.name);
|
// await page.getByLabel('Name').fill(users.user2.name);
|
||||||
await page.getByLabel('Master password (required)', { exact: true }).fill(users.user2.password);
|
await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(users.user2.password);
|
||||||
await page.getByLabel('Confirm master password (').fill(users.user2.password);
|
await page.getByRole('textbox', { name: 'Confirm master password * (' }).fill(users.user2.password);
|
||||||
await page.getByRole('button', { name: 'Create account' }).click();
|
await page.getByRole('button', { name: 'Create account' }).click();
|
||||||
await utils.checkNotification(page, 'Your new account has been created');
|
await utils.checkNotification(page, 'Your new account has been created');
|
||||||
|
|
||||||
await utils.checkNotification(page, 'Invitation accepted');
|
|
||||||
await utils.ignoreExtension(page);
|
|
||||||
|
|
||||||
// Redirected to the vault
|
// Redirected to the vault
|
||||||
await expect(page).toHaveTitle('Vaults | Vaultwarden Web');
|
await expect(page).toHaveTitle('Vaults | Vaultwarden Web');
|
||||||
// await utils.checkNotification(page, 'You have been logged in!');
|
// await utils.checkNotification(page, 'You have been logged in!');
|
||||||
|
await utils.checkNotification(page, 'Successfully accepted your invitation');
|
||||||
});
|
});
|
||||||
|
|
||||||
await test.step('Check mails', async () => {
|
await test.step('Check mails', async () => {
|
||||||
@@ -90,21 +100,19 @@ test('invited with existing account', async ({ page }) => {
|
|||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
|
||||||
// Unlock page
|
// Unlock page
|
||||||
await page.getByLabel('Master password').fill(users.user3.password);
|
await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(users.user3.password);
|
||||||
await page.getByRole('button', { name: 'Log in with master password' }).click();
|
await page.getByRole('button', { name: 'Log in', exact: true }).click();
|
||||||
|
|
||||||
await utils.checkNotification(page, 'Invitation accepted');
|
|
||||||
await utils.ignoreExtension(page);
|
|
||||||
|
|
||||||
// We are now in the default vault page
|
// We are now in the default vault page
|
||||||
await expect(page).toHaveTitle(/Vaultwarden Web/);
|
await expect(page).toHaveTitle(/Vaultwarden Web/);
|
||||||
|
await utils.checkNotification(page, 'Successfully accepted your invitation');
|
||||||
|
|
||||||
await mail3Buffer.expect((m) => m.subject === 'New Device Logged In From Firefox');
|
await mail3Buffer.expect((m) => m.subject === 'New Device Logged In From Firefox');
|
||||||
await mail1Buffer.expect((m) => m.subject.includes('Invitation to Test accepted'));
|
await mail1Buffer.expect((m) => m.subject.includes('Invitation to Test accepted'));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Confirm invited user', async ({ page }) => {
|
test('Confirm invited user', async ({ page }) => {
|
||||||
await logUser(test, page, users.user1, mail1Buffer);
|
await logUser(test, page, users.user1, { mailBuffer: mail1Buffer });
|
||||||
|
|
||||||
await orgs.members(test, page, 'Test');
|
await orgs.members(test, page, 'Test');
|
||||||
await orgs.confirm(test, page, 'Test', users.user2.email);
|
await orgs.confirm(test, page, 'Test', users.user2.email);
|
||||||
@@ -113,7 +121,40 @@ test('Confirm invited user', async ({ page }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Organization is visible', async ({ page }) => {
|
test('Organization is visible', async ({ page }) => {
|
||||||
await logUser(test, page, users.user2, mail2Buffer);
|
await logUser(test, page, users.user2, { mailBuffer: mail2Buffer });
|
||||||
await page.getByRole('button', { name: 'vault: Test', exact: true }).click();
|
await page.getByRole('button', { name: 'vault: Test', exact: true }).click();
|
||||||
await expect(page.getByLabel('Filter: Default collection')).toBeVisible();
|
await expect(page.getByLabel('Filter: Default collection')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Recover user password', async ({ page }) => {
|
||||||
|
await logUser(test, page, users.user2, { mailBuffer: mail2Buffer });
|
||||||
|
await activateTOTP(test, page, users.user2);
|
||||||
|
|
||||||
|
await logUser(test, page, users.user1, { mailBuffer: mail1Buffer });
|
||||||
|
|
||||||
|
let newPassword = "TotoNewPassword";
|
||||||
|
|
||||||
|
await orgs.members(test, page, 'Test');
|
||||||
|
await test.step(`Recover ${users.user2.email}`, async () => {
|
||||||
|
await expect(page.getByRole('heading', { name: 'Members' })).toBeVisible();
|
||||||
|
await page.getByRole('row').filter({hasText: users.user2.email}).getByLabel('Options').click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Recover account' }).click();
|
||||||
|
await page.getByRole('textbox', { name: 'New master password * (required)', exact: true }).fill(newPassword);
|
||||||
|
await page.getByRole('textbox', { name: 'Confirm new master password * (' }).fill(newPassword);
|
||||||
|
await page.getByRole('checkbox', { name: 'Reset two-step login' }).check();
|
||||||
|
await page.getByRole('button', { name: 'Save' }).click();
|
||||||
|
await utils.checkNotification(page, 'Account recovery success');
|
||||||
|
await mail2Buffer.expect((m) => m.subject.includes('Admin account recovery from Test organization'));
|
||||||
|
});
|
||||||
|
|
||||||
|
let user2 = {
|
||||||
|
email: users.user2.email,
|
||||||
|
name: users.user2.name,
|
||||||
|
password: newPassword,
|
||||||
|
};
|
||||||
|
await logUser(test, page, user2, {
|
||||||
|
mailBuffer: mail2Buffer,
|
||||||
|
mail2fa: true,
|
||||||
|
notNewDevice: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import { test, expect, type Page, type TestInfo } from '@playwright/test';
|
||||||
|
import * as OTPAuth from "otpauth";
|
||||||
|
|
||||||
|
import * as utils from "../global-utils";
|
||||||
|
import { createAccount, logUser } from './setups/user';
|
||||||
|
|
||||||
|
let users = utils.loadEnv();
|
||||||
|
let totp;
|
||||||
|
|
||||||
|
test.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => {
|
||||||
|
await utils.startVault(browser, testInfo, {});
|
||||||
|
|
||||||
|
const context = await browser.newContext();
|
||||||
|
const page = await context.newPage();
|
||||||
|
await createAccount(test, page, users.user1);
|
||||||
|
await context.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterAll('Teardown', async ({}) => {
|
||||||
|
utils.stopVault();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Password', async ({ context, page }, testInfo: TestInfo) => {
|
||||||
|
const label = 'Test Password';
|
||||||
|
|
||||||
|
await logUser(test, page, users.user1);
|
||||||
|
|
||||||
|
await test.step('Create password entry', async () => {
|
||||||
|
await page.getByRole('button', { name: 'New item' }).click();
|
||||||
|
await page.getByRole('textbox', { name: 'Item name * (required)' }).fill(label);
|
||||||
|
await page.getByRole('textbox', { name: 'Username' }).fill(users.user1.name);
|
||||||
|
await page.getByRole('textbox', { name: 'Password' }).fill(users.user1.password);
|
||||||
|
await page.getByRole('button', { name: 'Save' }).click();
|
||||||
|
await utils.checkNotification(page, 'Item added');
|
||||||
|
await page.getByRole('button', { name: 'Close' }).click();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Log again
|
||||||
|
await logUser(test, page, users.user1);
|
||||||
|
|
||||||
|
await test.step('Check', async () => {
|
||||||
|
await page.getByRole('row').filter({ hasText: label }).getByRole('button', { name: label }).click();
|
||||||
|
await page.getByTestId('copy-username').click();
|
||||||
|
await utils.checkNotification(page, 'Username copied');
|
||||||
|
expect(await page.evaluate(() => navigator.clipboard.readText())).toBe(users.user1.name)
|
||||||
|
await page.getByTestId('copy-password').click();
|
||||||
|
await utils.checkNotification(page, 'Password copied');
|
||||||
|
expect(await page.evaluate(() => navigator.clipboard.readText())).toBe(users.user1.password)
|
||||||
|
await page.getByRole('button', { name: 'Close' }).click();
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Delete', async () => {
|
||||||
|
await page.getByRole('row').filter({ hasText: label }).getByLabel('Options').click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||||
|
await page.getByRole('button', { name: 'Yes' }).click();
|
||||||
|
await utils.checkNotification(page, 'Item sent to bin');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Log again
|
||||||
|
await logUser(test, page, users.user1);
|
||||||
|
|
||||||
|
await test.step('Deleted', async () => {
|
||||||
|
await expect(page.getByRole('row').filter({ hasText: label })).toHaveCount(0)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test('SSH Key', async ({ context, page }, testInfo: TestInfo) => {
|
||||||
|
const label = 'Test SSH key';
|
||||||
|
|
||||||
|
await logUser(test, page, users.user1);
|
||||||
|
|
||||||
|
const privateKey = await test.step('Create key entry', async () => {
|
||||||
|
await page.getByRole('button', { name: 'New', exact: true }).click();
|
||||||
|
await page.getByRole('menuitem', { name: 'SSH key' }).click();
|
||||||
|
await page.getByRole('textbox', { name: 'Item name * (required)' }).fill('Test SSH key');
|
||||||
|
await page.getByRole('button', { name: 'Save' }).click();
|
||||||
|
await utils.checkNotification(page, 'Item added');
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Copy private key' }).click();
|
||||||
|
await utils.checkNotification(page, 'Private key copied');
|
||||||
|
return await page.evaluate(() => navigator.clipboard.readText());
|
||||||
|
});
|
||||||
|
|
||||||
|
// Log again
|
||||||
|
await logUser(test, page, users.user1);
|
||||||
|
|
||||||
|
await test.step('Check', async () => {
|
||||||
|
await page.getByRole('row').filter({ hasText: label }).getByRole('button', { name: label }).click();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Copy private key' }).click();
|
||||||
|
await utils.checkNotification(page, 'Private key copied');
|
||||||
|
expect(await page.evaluate(() => navigator.clipboard.readText())).toBe(privateKey)
|
||||||
|
await page.getByRole('button', { name: 'Close' }).click();
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Delete', async () => {
|
||||||
|
await page.getByRole('row').filter({ hasText: label }).getByLabel('Options').click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||||
|
await page.getByRole('button', { name: 'Yes' }).click();
|
||||||
|
await utils.checkNotification(page, 'Item sent to bin');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Log again
|
||||||
|
await logUser(test, page, users.user1);
|
||||||
|
|
||||||
|
await test.step('Deleted', async () => {
|
||||||
|
await expect(page.getByRole('row').filter({ hasText: label })).toHaveCount(0)
|
||||||
|
})
|
||||||
|
});
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { test, expect, type Page, type TestInfo } from '@playwright/test';
|
||||||
|
import * as OTPAuth from "otpauth";
|
||||||
|
|
||||||
|
import * as utils from "../global-utils";
|
||||||
|
import { createAccount } from './setups/user';
|
||||||
|
|
||||||
|
let users = utils.loadEnv();
|
||||||
|
|
||||||
|
test.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => {
|
||||||
|
await utils.startVault(browser, testInfo, {});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterAll('Teardown', async ({}) => {
|
||||||
|
utils.stopVault();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Send', async ({ browser, page }) => {
|
||||||
|
await createAccount(test, page, users.user1);
|
||||||
|
|
||||||
|
const send_url = await test.step('Create', async () => {
|
||||||
|
await page.getByRole('link', { name: 'Send' }).click();
|
||||||
|
await expect(page.locator('#main-content').getByText('Send', { exact: true })).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'New Send', exact: true }).click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Text' }).click();
|
||||||
|
|
||||||
|
await page.getByRole('textbox', { name: 'Send name * (required)' }).fill('Test');
|
||||||
|
await page.getByRole('textbox', { name: 'Text to share * (required)' }).fill('test');
|
||||||
|
await page.getByRole('button', { name: 'Save' }).click();
|
||||||
|
|
||||||
|
await page.locator('footer').getByRole('button', { name: 'Copy link' }).click();
|
||||||
|
|
||||||
|
return await page.evaluate(() => navigator.clipboard.readText());
|
||||||
|
});
|
||||||
|
|
||||||
|
const context2 = await browser.newContext();
|
||||||
|
const page2 = await context2.newPage();
|
||||||
|
|
||||||
|
await test.step('View', async () => {
|
||||||
|
await page2.goto(send_url, { waitUntil: 'domcontentloaded' });
|
||||||
|
await expect(page2.getByRole('heading', { name: 'View Send' })).toBeVisible();
|
||||||
|
await expect(await page2.getByRole('paragraph').filter({ hasText: 'Test' })).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
const pwd_url = await test.step('Create with password', async () => {
|
||||||
|
await page.getByRole('link', { name: 'Send' }).click();
|
||||||
|
await expect(page.locator('#main-content').getByText('Send', { exact: true })).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'New' }).click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Text' }).click();
|
||||||
|
|
||||||
|
await page.getByRole('textbox', { name: 'Send name * (required)' }).fill('Password');
|
||||||
|
await page.getByRole('textbox', { name: 'Text to share * (required)' }).fill('password');
|
||||||
|
await page.getByRole('combobox', { name: 'Who can view' }).click();
|
||||||
|
await page.getByText('Anyone with a password set by you').click();
|
||||||
|
await page.getByRole('textbox', { name: 'Password * (required)', exact: true }).fill('password');
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Save' }).click();
|
||||||
|
await page.locator('footer').getByRole('button', { name: 'Copy link' }).click();
|
||||||
|
|
||||||
|
return await page.evaluate(() => navigator.clipboard.readText());
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('View with password', async () => {
|
||||||
|
await page2.goto(pwd_url, { waitUntil: 'domcontentloaded' });
|
||||||
|
await expect(page2.getByRole('heading', { name: 'Enter the password to view' })).toBeVisible();
|
||||||
|
await page2.getByRole('textbox', { name: 'Password * (required)' }).fill('password');
|
||||||
|
await page2.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
await expect(page2.getByRole('heading', { name: 'View Send' })).toBeVisible();
|
||||||
|
await expect(await page2.getByRole('paragraph').filter({ hasText: 'Password' })).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -4,6 +4,24 @@ import * as OTPAuth from "otpauth";
|
|||||||
|
|
||||||
import * as utils from '../../global-utils';
|
import * as utils from '../../global-utils';
|
||||||
|
|
||||||
|
export async function recoveryCodes(test: Test, page: Page, user: { name: string, password: string }): string {
|
||||||
|
return await test.step('Recovery code', async () => {
|
||||||
|
await page.getByRole('button', { name: user.name }).click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Account settings' }).click();
|
||||||
|
await page.getByRole('link', { name: 'Security' }).click();
|
||||||
|
await page.getByRole('link', { name: 'Two-step login' }).click();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'View recovery code' }).click();
|
||||||
|
await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(user.password);
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
|
||||||
|
const recovery = await page.getByRole('code').innerText();
|
||||||
|
await page.getByLabel('Close').click();
|
||||||
|
|
||||||
|
return recovery;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export async function activateTOTP(test: Test, page: Page, user: { name: string, password: string }): OTPAuth.TOTP {
|
export async function activateTOTP(test: Test, page: Page, user: { name: string, password: string }): OTPAuth.TOTP {
|
||||||
return await test.step('Activate TOTP 2FA', async () => {
|
return await test.step('Activate TOTP 2FA', async () => {
|
||||||
await page.getByRole('button', { name: user.name }).click();
|
await page.getByRole('button', { name: user.name }).click();
|
||||||
@@ -11,16 +29,16 @@ export async function activateTOTP(test: Test, page: Page, user: { name: string,
|
|||||||
await page.getByRole('link', { name: 'Security' }).click();
|
await page.getByRole('link', { name: 'Security' }).click();
|
||||||
await page.getByRole('link', { name: 'Two-step login' }).click();
|
await page.getByRole('link', { name: 'Two-step login' }).click();
|
||||||
await page.locator('bit-item').filter({ hasText: /Authenticator app/ }).getByRole('button').click();
|
await page.locator('bit-item').filter({ hasText: /Authenticator app/ }).getByRole('button').click();
|
||||||
await page.getByLabel('Master password (required)').fill(user.password);
|
await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(user.password);
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
|
||||||
const secret = await page.getByLabel('Key').innerText();
|
const secret = await page.getByLabel('Key', { exact: true }).innerText();
|
||||||
|
|
||||||
let totp = new OTPAuth.TOTP({ secret, period: 30 });
|
let totp = new OTPAuth.TOTP({ secret, period: 30 });
|
||||||
|
|
||||||
await page.getByLabel(/Verification code/).fill(totp.generate());
|
await page.getByLabel(/Verification code/).fill(totp.generate());
|
||||||
await page.getByRole('button', { name: 'Turn on' }).click();
|
await page.getByRole('button', { name: 'Turn on' }).click();
|
||||||
await page.getByRole('heading', { name: 'Turned on', exact: true });
|
await page.getByRole('heading', { name: 'Turned on', exact: true });
|
||||||
await page.getByLabel('Close').click();
|
|
||||||
|
|
||||||
return totp;
|
return totp;
|
||||||
})
|
})
|
||||||
@@ -33,8 +51,8 @@ export async function disableTOTP(test: Test, page: Page, user: { password: stri
|
|||||||
await page.getByRole('link', { name: 'Security' }).click();
|
await page.getByRole('link', { name: 'Security' }).click();
|
||||||
await page.getByRole('link', { name: 'Two-step login' }).click();
|
await page.getByRole('link', { name: 'Two-step login' }).click();
|
||||||
await page.locator('bit-item').filter({ hasText: /Authenticator app/ }).getByRole('button').click();
|
await page.locator('bit-item').filter({ hasText: /Authenticator app/ }).getByRole('button').click();
|
||||||
await page.getByLabel('Master password (required)').click();
|
await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).click()
|
||||||
await page.getByLabel('Master password (required)').fill(user.password);
|
await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(user.password);
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
await page.getByRole('button', { name: 'Turn off' }).click();
|
await page.getByRole('button', { name: 'Turn off' }).click();
|
||||||
await page.getByRole('button', { name: 'Yes' }).click();
|
await page.getByRole('button', { name: 'Yes' }).click();
|
||||||
@@ -49,7 +67,7 @@ export async function activateEmail(test: Test, page: Page, user: { name: string
|
|||||||
await page.getByRole('link', { name: 'Security' }).click();
|
await page.getByRole('link', { name: 'Security' }).click();
|
||||||
await page.getByRole('link', { name: 'Two-step login' }).click();
|
await page.getByRole('link', { name: 'Two-step login' }).click();
|
||||||
await page.locator('bit-item').filter({ hasText: 'Enter a code sent to your email' }).getByRole('button').click();
|
await page.locator('bit-item').filter({ hasText: 'Enter a code sent to your email' }).getByRole('button').click();
|
||||||
await page.getByLabel('Master password (required)').fill(user.password);
|
await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(user.password);
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
await page.getByRole('button', { name: 'Send email' }).click();
|
await page.getByRole('button', { name: 'Send email' }).click();
|
||||||
});
|
});
|
||||||
@@ -81,8 +99,8 @@ export async function disableEmail(test: Test, page: Page, user: { password: str
|
|||||||
await page.getByRole('link', { name: 'Security' }).click();
|
await page.getByRole('link', { name: 'Security' }).click();
|
||||||
await page.getByRole('link', { name: 'Two-step login' }).click();
|
await page.getByRole('link', { name: 'Two-step login' }).click();
|
||||||
await page.locator('bit-item').filter({ hasText: 'Email' }).getByRole('button').click();
|
await page.locator('bit-item').filter({ hasText: 'Email' }).getByRole('button').click();
|
||||||
await page.getByLabel('Master password (required)').click();
|
await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).click()
|
||||||
await page.getByLabel('Master password (required)').fill(user.password);
|
await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(user.password);
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
await page.getByRole('button', { name: 'Turn off' }).click();
|
await page.getByRole('button', { name: 'Turn off' }).click();
|
||||||
await page.getByRole('button', { name: 'Yes' }).click();
|
await page.getByRole('button', { name: 'Yes' }).click();
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { expect, type Browser, Page } from '@playwright/test';
|
||||||
|
import * as utils from '../../global-utils';
|
||||||
|
|
||||||
|
utils.loadEnv();
|
||||||
|
|
||||||
|
export async function login(test, page: Page) {
|
||||||
|
await test.step(`Admin login`, async () => {
|
||||||
|
await page.goto('/admin');
|
||||||
|
await page.getByRole('textbox', { name: 'Enter admin token' }).fill(process.env.ADMIN_TOKEN);
|
||||||
|
await page.getByRole('button', { name: 'Enter' }).click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function invite(test, page: Page, email: string) {
|
||||||
|
await test.step(`Invite user with ${email}`, async () => {
|
||||||
|
await page.getByRole('link', { name: 'Users' }).click();
|
||||||
|
await page.getByRole('textbox', { name: 'Enter email' }).fill(email);
|
||||||
|
await page.getByRole('button', { name: 'Invite' }).click();
|
||||||
|
await expect(page.getByRole('row', { name: email })).toHaveText(/Invited/);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ const utils = require('../../global-utils');
|
|||||||
utils.loadEnv();
|
utils.loadEnv();
|
||||||
|
|
||||||
test('DB teardown ?', async ({ serviceName }) => {
|
test('DB teardown ?', async ({ serviceName }) => {
|
||||||
if( process.env.PW_KEEP_SERVICE_RUNNNING !== "true" ) {
|
if( process.env.PW_KEEP_SERVICE_RUNNING !== "true" ) {
|
||||||
utils.stopComposeService(serviceName);
|
utils.stopComposeService(serviceName);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -3,11 +3,14 @@ import { expect, type Browser,Page } from '@playwright/test';
|
|||||||
import * as utils from '../../global-utils';
|
import * as utils from '../../global-utils';
|
||||||
|
|
||||||
export async function create(test, page: Page, name: string) {
|
export async function create(test, page: Page, name: string) {
|
||||||
await test.step('Create Org', async () => {
|
await test.step(`Create Org ${name}`, async () => {
|
||||||
await page.locator('a').filter({ hasText: 'Password Manager' }).first().click();
|
let pm_locator = page.locator('a').filter({ hasText: 'Password Manager' });
|
||||||
|
if( await pm_locator.count() > 0 ){
|
||||||
|
pm_locator.first().click();
|
||||||
|
}
|
||||||
await expect(page.getByTitle('All vaults', { exact: true })).toBeVisible();
|
await expect(page.getByTitle('All vaults', { exact: true })).toBeVisible();
|
||||||
await page.getByRole('link', { name: 'New organisation' }).click();
|
await page.getByRole('link', { name: 'New organisation' }).click();
|
||||||
await page.getByLabel('Organisation name (required)').fill(name);
|
await page.getByRole('textbox', { name: 'Organisation name * (required)', exact: true }).fill(name);
|
||||||
await page.getByRole('button', { name: 'Submit' }).click();
|
await page.getByRole('button', { name: 'Submit' }).click();
|
||||||
|
|
||||||
await utils.checkNotification(page, 'Organisation created');
|
await utils.checkNotification(page, 'Organisation created');
|
||||||
@@ -18,7 +21,7 @@ export async function policies(test, page: Page, name: string) {
|
|||||||
await test.step(`Navigate to ${name} policies`, async () => {
|
await test.step(`Navigate to ${name} policies`, async () => {
|
||||||
await page.locator('a').filter({ hasText: 'Admin Console' }).first().click();
|
await page.locator('a').filter({ hasText: 'Admin Console' }).first().click();
|
||||||
await page.locator('org-switcher').getByLabel(/Toggle collapse/).click();
|
await page.locator('org-switcher').getByLabel(/Toggle collapse/).click();
|
||||||
await page.locator('org-switcher').getByRole('link', { name: `${name}` }).first().click();
|
await page.locator('org-switcher > bit-nav-group > div > bit-nav-item').filter({ hasText: `${name}` }).first().click();
|
||||||
await expect(page.getByRole('heading', { name: `${name} collections` })).toBeVisible();
|
await expect(page.getByRole('heading', { name: `${name} collections` })).toBeVisible();
|
||||||
await page.getByRole('button', { name: 'Toggle collapse Settings' }).click();
|
await page.getByRole('button', { name: 'Toggle collapse Settings' }).click();
|
||||||
await page.getByRole('link', { name: 'Policies' }).click();
|
await page.getByRole('link', { name: 'Policies' }).click();
|
||||||
@@ -30,11 +33,11 @@ export async function members(test, page: Page, name: string) {
|
|||||||
await test.step(`Navigate to ${name} members`, async () => {
|
await test.step(`Navigate to ${name} members`, async () => {
|
||||||
await page.locator('a').filter({ hasText: 'Admin Console' }).first().click();
|
await page.locator('a').filter({ hasText: 'Admin Console' }).first().click();
|
||||||
await page.locator('org-switcher').getByLabel(/Toggle collapse/).click();
|
await page.locator('org-switcher').getByLabel(/Toggle collapse/).click();
|
||||||
await page.locator('org-switcher').getByRole('link', { name: `${name}` }).first().click();
|
await page.locator('org-switcher > bit-nav-group > div > bit-nav-item').filter({ hasText: `${name}` }).first().click();
|
||||||
await expect(page.getByRole('heading', { name: `${name} collections` })).toBeVisible();
|
await expect(page.getByRole('heading', { name: `${name} collections` })).toBeVisible();
|
||||||
await page.locator('div').filter({ hasText: 'Members' }).nth(2).click();
|
await page.getByRole('link', { name: 'Members' }).click();
|
||||||
await expect(page.getByRole('heading', { name: 'Members' })).toBeVisible();
|
await expect(page.getByRole('heading', { name: 'Members' })).toBeVisible();
|
||||||
await expect(page.getByRole('cell', { name: 'All' })).toBeVisible();
|
await expect(page.getByRole('columnheader', { name: 'Select all' })).toBeVisible();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,13 +45,13 @@ export async function invite(test, page: Page, name: string, email: string) {
|
|||||||
await test.step(`Invite ${email}`, async () => {
|
await test.step(`Invite ${email}`, async () => {
|
||||||
await expect(page.getByRole('heading', { name: 'Members' })).toBeVisible();
|
await expect(page.getByRole('heading', { name: 'Members' })).toBeVisible();
|
||||||
await page.getByRole('button', { name: 'Invite member' }).click();
|
await page.getByRole('button', { name: 'Invite member' }).click();
|
||||||
await page.getByLabel('Email (required)').fill(email);
|
await page.getByRole('textbox', { name: 'Email * (required)', exact: true }).fill(email);
|
||||||
await page.getByRole('tab', { name: 'Collections' }).click();
|
await page.getByRole('tab', { name: 'Collections' }).click();
|
||||||
await page.getByRole('combobox', { name: 'Permission' }).click();
|
await page.getByRole('combobox', { name: 'Permission' }).click();
|
||||||
await page.getByText('Edit items', { exact: true }).click();
|
await page.getByText('Edit items', { exact: true }).click();
|
||||||
await page.getByLabel('Select collections').click();
|
await page.getByRole('combobox', { name: 'Select collections' }).click();
|
||||||
await page.getByText('Default collection').click();
|
await page.getByLabel('Options List').getByText('Default collection').click();
|
||||||
await page.getByRole('cell', { name: 'Collection', exact: true }).click();
|
await page.getByRole('columnheader', { name: 'Collection', exact: true }).click();
|
||||||
await page.getByRole('button', { name: 'Save' }).click();
|
await page.getByRole('button', { name: 'Save' }).click();
|
||||||
await utils.checkNotification(page, 'User(s) invited');
|
await utils.checkNotification(page, 'User(s) invited');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ const utils = require('../../global-utils');
|
|||||||
utils.loadEnv();
|
utils.loadEnv();
|
||||||
|
|
||||||
test('Keycloak teardown', async () => {
|
test('Keycloak teardown', async () => {
|
||||||
if( process.env.PW_KEEP_SERVICE_RUNNNING === "true" ) {
|
if( process.env.PW_KEEP_SERVICE_RUNNING === "true" ) {
|
||||||
console.log("Keep Keycloak running");
|
console.log("Keep Keycloak running");
|
||||||
} else {
|
} else {
|
||||||
console.log("Keycloak stopping");
|
console.log("Keycloak stopping");
|
||||||
|
|||||||
@@ -15,11 +15,8 @@ export async function logNewUser(
|
|||||||
options: { mailBuffer?: MailBuffer } = {}
|
options: { mailBuffer?: MailBuffer } = {}
|
||||||
) {
|
) {
|
||||||
await test.step(`Create user ${user.name}`, async () => {
|
await test.step(`Create user ${user.name}`, async () => {
|
||||||
await page.context().clearCookies();
|
|
||||||
|
|
||||||
await test.step('Landing page', async () => {
|
await test.step('Landing page', async () => {
|
||||||
await utils.cleanLanding(page);
|
await utils.cleanLanding(page);
|
||||||
|
|
||||||
await page.locator("input[type=email].vw-email-sso").fill(user.email);
|
await page.locator("input[type=email].vw-email-sso").fill(user.email);
|
||||||
await page.getByRole('button', { name: /Use single sign-on/ }).click();
|
await page.getByRole('button', { name: /Use single sign-on/ }).click();
|
||||||
});
|
});
|
||||||
@@ -33,26 +30,24 @@ export async function logNewUser(
|
|||||||
|
|
||||||
await test.step('Create Vault account', async () => {
|
await test.step('Create Vault account', async () => {
|
||||||
await expect(page.getByRole('heading', { name: 'Join organisation' })).toBeVisible();
|
await expect(page.getByRole('heading', { name: 'Join organisation' })).toBeVisible();
|
||||||
await page.getByLabel('Master password (required)', { exact: true }).fill(user.password);
|
await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(user.password);
|
||||||
await page.getByLabel('Confirm master password (').fill(user.password);
|
await page.getByRole('textbox', { name: 'Confirm master password * (' }).fill(user.password);
|
||||||
await page.getByRole('button', { name: 'Create account' }).click();
|
await page.getByRole('button', { name: 'Create account' }).click();
|
||||||
});
|
});
|
||||||
|
|
||||||
await utils.checkNotification(page, 'Account successfully created!');
|
|
||||||
await utils.checkNotification(page, 'Invitation accepted');
|
|
||||||
|
|
||||||
await utils.ignoreExtension(page);
|
|
||||||
|
|
||||||
await test.step('Default vault page', async () => {
|
await test.step('Default vault page', async () => {
|
||||||
await expect(page).toHaveTitle(/Vaultwarden Web/);
|
await expect(page).toHaveTitle(/Vaultwarden Web/);
|
||||||
await expect(page.getByTitle('All vaults', { exact: true })).toBeVisible();
|
await expect(page.getByTitle('All vaults', { exact: true })).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await utils.checkNotification(page, 'Account successfully created!');
|
||||||
|
await utils.checkNotification(page, 'Invitation accepted');
|
||||||
|
|
||||||
if( options.mailBuffer ){
|
if( options.mailBuffer ){
|
||||||
let mailBuffer = options.mailBuffer;
|
let mailBuffer = options.mailBuffer;
|
||||||
await test.step('Check emails', async () => {
|
await test.step('Check emails', async () => {
|
||||||
await mailBuffer.expect((m) => m.subject === "Welcome");
|
|
||||||
await mailBuffer.expect((m) => m.subject.includes("New Device Logged"));
|
await mailBuffer.expect((m) => m.subject.includes("New Device Logged"));
|
||||||
|
await mailBuffer.expect((m) => m.subject === "Welcome");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -69,16 +64,14 @@ export async function logUser(
|
|||||||
mailBuffer ?: MailBuffer,
|
mailBuffer ?: MailBuffer,
|
||||||
totp?: OTPAuth.TOTP,
|
totp?: OTPAuth.TOTP,
|
||||||
mail2fa?: boolean,
|
mail2fa?: boolean,
|
||||||
|
notNewDevice?: boolean,
|
||||||
} = {}
|
} = {}
|
||||||
) {
|
) {
|
||||||
let mailBuffer = options.mailBuffer;
|
let mailBuffer = options.mailBuffer;
|
||||||
|
|
||||||
await test.step(`Log user ${user.email}`, async () => {
|
await test.step(`Log user ${user.email}`, async () => {
|
||||||
await page.context().clearCookies();
|
|
||||||
|
|
||||||
await test.step('Landing page', async () => {
|
await test.step('Landing page', async () => {
|
||||||
await utils.cleanLanding(page);
|
await utils.cleanLanding(page);
|
||||||
|
|
||||||
await page.locator("input[type=email].vw-email-sso").fill(user.email);
|
await page.locator("input[type=email].vw-email-sso").fill(user.email);
|
||||||
await page.getByRole('button', { name: /Use single sign-on/ }).click();
|
await page.getByRole('button', { name: /Use single sign-on/ }).click();
|
||||||
});
|
});
|
||||||
@@ -117,14 +110,12 @@ export async function logUser(
|
|||||||
await page.getByRole('button', { name: 'Unlock' }).click();
|
await page.getByRole('button', { name: 'Unlock' }).click();
|
||||||
});
|
});
|
||||||
|
|
||||||
await utils.ignoreExtension(page);
|
|
||||||
|
|
||||||
await test.step('Default vault page', async () => {
|
await test.step('Default vault page', async () => {
|
||||||
await expect(page).toHaveTitle(/Vaultwarden Web/);
|
await expect(page).toHaveTitle(/Vaultwarden Web/);
|
||||||
await expect(page.getByTitle('All vaults', { exact: true })).toBeVisible();
|
await expect(page.getByTitle('All vaults', { exact: true })).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
if( mailBuffer ){
|
if( mailBuffer && !options.notNewDevice ){
|
||||||
await test.step('Check email', async () => {
|
await test.step('Check email', async () => {
|
||||||
await mailBuffer.expect((m) => m.subject.includes("New Device Logged"));
|
await mailBuffer.expect((m) => m.subject.includes("New Device Logged"));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import { expect, type Browser, Page } from '@playwright/test';
|
|||||||
|
|
||||||
import { type MailBuffer } from 'maildev';
|
import { type MailBuffer } from 'maildev';
|
||||||
|
|
||||||
|
import * as OTPAuth from "otpauth";
|
||||||
import * as utils from '../../global-utils';
|
import * as utils from '../../global-utils';
|
||||||
|
import { retrieveEmailCode } from './2fa';
|
||||||
|
|
||||||
export async function createAccount(test, page: Page, user: { email: string, name: string, password: string }, mailBuffer?: MailBuffer) {
|
export async function createAccount(test, page: Page, user: { email: string, name: string, password: string }, mailBuffer?: MailBuffer) {
|
||||||
await test.step(`Create user ${user.name}`, async () => {
|
await test.step(`Create user ${user.name}`, async () => {
|
||||||
@@ -17,12 +19,11 @@ export async function createAccount(test, page: Page, user: { email: string, nam
|
|||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
|
||||||
// Vault finish Creation
|
// Vault finish Creation
|
||||||
await page.getByLabel('Master password (required)', { exact: true }).fill(user.password);
|
await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(user.password);
|
||||||
await page.getByLabel('Confirm master password (').fill(user.password);
|
await page.getByRole('textbox', { name: 'Confirm master password * (' }).fill(user.password);
|
||||||
await page.getByRole('button', { name: 'Create account' }).click();
|
await page.getByRole('button', { name: 'Create account' }).click();
|
||||||
|
|
||||||
await utils.checkNotification(page, 'Your new account has been created')
|
await utils.checkNotification(page, 'Your new account has been created')
|
||||||
await utils.ignoreExtension(page);
|
|
||||||
|
|
||||||
// We are now in the default vault page
|
// We are now in the default vault page
|
||||||
await expect(page).toHaveTitle('Vaults | Vaultwarden Web');
|
await expect(page).toHaveTitle('Vaults | Vaultwarden Web');
|
||||||
@@ -35,7 +36,17 @@ export async function createAccount(test, page: Page, user: { email: string, nam
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function logUser(test, page: Page, user: { email: string, password: string }, mailBuffer?: MailBuffer) {
|
export async function logUser(
|
||||||
|
test,
|
||||||
|
page: Page,
|
||||||
|
user: { email: string, password: string },
|
||||||
|
options: {
|
||||||
|
mailBuffer ?: MailBuffer,
|
||||||
|
mail2fa?: boolean,
|
||||||
|
notNewDevice?: boolean,
|
||||||
|
totp?: OTPAuth.TOTP,
|
||||||
|
} = {}
|
||||||
|
) {
|
||||||
await test.step(`Log user ${user.email}`, async () => {
|
await test.step(`Log user ${user.email}`, async () => {
|
||||||
await utils.cleanLanding(page);
|
await utils.cleanLanding(page);
|
||||||
|
|
||||||
@@ -43,16 +54,35 @@ export async function logUser(test, page: Page, user: { email: string, password:
|
|||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
|
||||||
// Unlock page
|
// Unlock page
|
||||||
await page.getByLabel('Master password').fill(user.password);
|
await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(user.password);
|
||||||
await page.getByRole('button', { name: 'Log in with master password' }).click();
|
await page.getByRole('button', { name: 'Log in', exact: true }).click();
|
||||||
|
|
||||||
await utils.ignoreExtension(page);
|
if( options.mail2fa || options.totp ){
|
||||||
|
let code;
|
||||||
|
|
||||||
|
await test.step('2FA check', async () => {
|
||||||
|
await expect(page.getByRole('heading', { name: 'Verify your Identity' })).toBeVisible();
|
||||||
|
|
||||||
|
if( options.totp ) {
|
||||||
|
const totp = options.totp;
|
||||||
|
let timestamp = Date.now(); // Needed to use the next token
|
||||||
|
timestamp = timestamp + (totp.period - (Math.floor(timestamp / 1000) % totp.period) + 1) * 1000;
|
||||||
|
code = totp.generate({timestamp});
|
||||||
|
} else if( options.mail2fa ){
|
||||||
|
code = await retrieveEmailCode(test, page, options.mailBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.getByLabel(/Verification code/).fill(code);
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// We are now in the default vault page
|
// We are now in the default vault page
|
||||||
await expect(page).toHaveTitle(/Vaultwarden Web/);
|
await expect(page).toHaveTitle(/Vaultwarden Web/);
|
||||||
|
|
||||||
if( mailBuffer ){
|
if( options.mailBuffer && !options.notNewDevice ){
|
||||||
await mailBuffer.expect((m) => m.subject === "New Device Logged In From Firefox");
|
await options.mailBuffer.expect((m) => m.subject === "New Device Logged In From Firefox");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { test, expect, type TestInfo } from '@playwright/test';
|
import { test, expect, type TestInfo } from '@playwright/test';
|
||||||
import { MailDev } from 'maildev';
|
import { MailDev } from 'maildev';
|
||||||
|
|
||||||
|
import * as admin from "./setups/admin";
|
||||||
import { logNewUser, logUser } from './setups/sso';
|
import { logNewUser, logUser } from './setups/sso';
|
||||||
import { activateEmail, disableEmail } from './setups/2fa';
|
import { activateEmail, disableEmail } from './setups/2fa';
|
||||||
import * as utils from "../global-utils";
|
import * as utils from "../global-utils";
|
||||||
@@ -19,7 +20,7 @@ test.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => {
|
|||||||
|
|
||||||
await utils.startVault(browser, testInfo, {
|
await utils.startVault(browser, testInfo, {
|
||||||
SSO_ENABLED: true,
|
SSO_ENABLED: true,
|
||||||
SSO_ONLY: false,
|
SSO_ONLY: true,
|
||||||
SMTP_HOST: process.env.MAILDEV_HOST,
|
SMTP_HOST: process.env.MAILDEV_HOST,
|
||||||
SMTP_FROM: process.env.PW_SMTP_FROM,
|
SMTP_FROM: process.env.PW_SMTP_FROM,
|
||||||
});
|
});
|
||||||
@@ -32,22 +33,64 @@ test.afterAll('Teardown', async ({}) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Create and activate 2FA', async ({ page }) => {
|
test('2FA email', async ({ page }) => {
|
||||||
|
|
||||||
const mailBuffer = mailserver.buffer(users.user1.email);
|
const mailBuffer = mailserver.buffer(users.user1.email);
|
||||||
|
|
||||||
await logNewUser(test, page, users.user1, {mailBuffer: mailBuffer});
|
await logNewUser(test, page, users.user1, {mailBuffer: mailBuffer});
|
||||||
|
|
||||||
await activateEmail(test, page, users.user1, mailBuffer);
|
await activateEmail(test, page, users.user1, mailBuffer);
|
||||||
|
|
||||||
mailBuffer.close();
|
await logUser(test, page, users.user1, {mailBuffer: mailBuffer, mail2fa: true, notNewDevice: true});
|
||||||
});
|
|
||||||
|
|
||||||
test('Log and disable', async ({ page }) => {
|
|
||||||
const mailBuffer = mailserver.buffer(users.user1.email);
|
|
||||||
|
|
||||||
await logUser(test, page, users.user1, {mailBuffer: mailBuffer, mail2fa: true});
|
|
||||||
|
|
||||||
await disableEmail(test, page, users.user1);
|
await disableEmail(test, page, users.user1);
|
||||||
|
|
||||||
mailBuffer.close();
|
mailBuffer.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test('Admin invite', async ({ page }) => {
|
||||||
|
const mailBuffer = mailserver.buffer(users.user2.email);
|
||||||
|
|
||||||
|
await admin.login(test, page);
|
||||||
|
await admin.invite(test, page, users.user2.email);
|
||||||
|
|
||||||
|
|
||||||
|
const link = await test.step('Extract email link', async () => {
|
||||||
|
const invited = await mailBuffer.expect((m) => m.subject === "Join Vaultwarden");
|
||||||
|
await page.setContent(invited.html);
|
||||||
|
return await page.getByTestId("invite").getAttribute("href");
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Redirect to Keycloak', async () => {
|
||||||
|
await page.goto(link);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Keycloak login', async () => {
|
||||||
|
await expect(page.getByRole('heading', { name: 'Sign in to your account' })).toBeVisible();
|
||||||
|
await page.getByLabel(/Username/).fill(users.user2.name);
|
||||||
|
await page.getByLabel('Password', { exact: true }).fill(users.user2.password);
|
||||||
|
await page.getByRole('button', { name: 'Sign In' }).click();
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Create Vault account', async () => {
|
||||||
|
await expect(page.getByRole('heading', { name: 'Join organisation' })).toBeVisible();
|
||||||
|
await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(users.user2.password);
|
||||||
|
await page.getByRole('textbox', { name: 'Confirm master password * (' }).fill(users.user2.password);
|
||||||
|
await page.getByRole('button', { name: 'Create account' }).click();
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Default vault page', async () => {
|
||||||
|
await expect(page).toHaveTitle('Vaults | Vaultwarden Web');
|
||||||
|
|
||||||
|
await utils.checkNotification(page, 'Account successfully created!');
|
||||||
|
await utils.checkNotification(page, 'Invitation accepted');
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Check mails', async () => {
|
||||||
|
await mailBuffer.expect((m) => m.subject.includes("New Device Logged"));
|
||||||
|
await mailBuffer.expect((m) => m.subject === "Welcome");
|
||||||
|
});
|
||||||
|
|
||||||
|
mailBuffer.close();
|
||||||
|
});
|
||||||
@@ -33,8 +33,8 @@ test('Non SSO login', async ({ page }) => {
|
|||||||
await page.getByRole('button', { name: 'Other' }).click();
|
await page.getByRole('button', { name: 'Other' }).click();
|
||||||
|
|
||||||
// Unlock page
|
// Unlock page
|
||||||
await page.getByLabel('Master password').fill(users.user1.password);
|
await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(users.user1.password);
|
||||||
await page.getByRole('button', { name: 'Log in with master password' }).click();
|
await page.getByRole('button', { name: 'Log in', exact: true }).click();
|
||||||
|
|
||||||
// We are now in the default vault page
|
// We are now in the default vault page
|
||||||
await expect(page).toHaveTitle(/Vaultwarden Web/);
|
await expect(page).toHaveTitle(/Vaultwarden Web/);
|
||||||
@@ -58,6 +58,7 @@ test('Non SSO login impossible', async ({ page, browser }, testInfo: TestInfo) =
|
|||||||
|
|
||||||
// Landing page
|
// Landing page
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
|
await page.locator("input[type=email].vw-email-sso").fill(users.user1.email);
|
||||||
|
|
||||||
// Check that SSO login is available
|
// Check that SSO login is available
|
||||||
await expect(page.getByRole('button', { name: /Use single sign-on/ })).toHaveCount(1);
|
await expect(page.getByRole('button', { name: /Use single sign-on/ })).toHaveCount(1);
|
||||||
@@ -66,7 +67,6 @@ test('Non SSO login impossible', async ({ page, browser }, testInfo: TestInfo) =
|
|||||||
await expect(page.getByRole('button', { name: 'Other' })).toHaveCount(0);
|
await expect(page.getByRole('button', { name: 'Other' })).toHaveCount(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
test('No SSO login', async ({ page }, testInfo: TestInfo) => {
|
test('No SSO login', async ({ page }, testInfo: TestInfo) => {
|
||||||
await utils.restartVault(page, testInfo, {
|
await utils.restartVault(page, testInfo, {
|
||||||
SSO_ENABLED: false
|
SSO_ENABLED: false
|
||||||
@@ -74,12 +74,14 @@ test('No SSO login', async ({ page }, testInfo: TestInfo) => {
|
|||||||
|
|
||||||
// Landing page
|
// Landing page
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
|
await page.getByLabel(/Email address/).fill(users.user1.email);
|
||||||
|
|
||||||
// No SSO button (rely on a correct selector checked in previous test)
|
// No SSO button (rely on a correct selector checked in previous test)
|
||||||
|
await page.getByLabel('Master password');
|
||||||
await expect(page.getByRole('button', { name: /Use single sign-on/ })).toHaveCount(0);
|
await expect(page.getByRole('button', { name: /Use single sign-on/ })).toHaveCount(0);
|
||||||
|
|
||||||
// Can continue to Master password
|
// Can continue to Master password
|
||||||
await page.getByLabel(/Email address/).fill(users.user1.email);
|
await page.getByLabel(/Email address/).fill(users.user1.email);
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
await expect(page.getByRole('button', { name: 'Log in with master password' })).toHaveCount(1);
|
await expect(page.getByRole('button', { name: 'Log in' })).toHaveCount(1);
|
||||||
});
|
});
|
||||||
@@ -67,17 +67,16 @@ test('invited with new account', async ({ page }) => {
|
|||||||
|
|
||||||
await test.step('Create Vault account', async () => {
|
await test.step('Create Vault account', async () => {
|
||||||
await expect(page.getByRole('heading', { name: 'Join organisation' })).toBeVisible();
|
await expect(page.getByRole('heading', { name: 'Join organisation' })).toBeVisible();
|
||||||
await page.getByLabel('Master password (required)', { exact: true }).fill(users.user2.password);
|
await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(users.user2.password);
|
||||||
await page.getByLabel('Confirm master password (').fill(users.user2.password);
|
await page.getByRole('textbox', { name: 'Confirm master password * (' }).fill(users.user2.password);
|
||||||
await page.getByRole('button', { name: 'Create account' }).click();
|
await page.getByRole('button', { name: 'Create account' }).click();
|
||||||
|
|
||||||
await utils.checkNotification(page, 'Account successfully created!');
|
|
||||||
await utils.checkNotification(page, 'Invitation accepted');
|
|
||||||
await utils.ignoreExtension(page);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await test.step('Default vault page', async () => {
|
await test.step('Default vault page', async () => {
|
||||||
await expect(page).toHaveTitle(/Vaultwarden Web/);
|
await expect(page).toHaveTitle(/Vaultwarden Web/);
|
||||||
|
|
||||||
|
await utils.checkNotification(page, 'Account successfully created!');
|
||||||
|
await utils.checkNotification(page, 'Invitation accepted');
|
||||||
});
|
});
|
||||||
|
|
||||||
await test.step('Check mails', async () => {
|
await test.step('Check mails', async () => {
|
||||||
@@ -95,6 +94,7 @@ test('invited with existing account', async ({ page }) => {
|
|||||||
|
|
||||||
await test.step('Redirect to Keycloak', async () => {
|
await test.step('Redirect to Keycloak', async () => {
|
||||||
await page.goto(link);
|
await page.goto(link);
|
||||||
|
await page.getByRole('button', { name: /Use single sign-on/ }).click();
|
||||||
});
|
});
|
||||||
|
|
||||||
await test.step('Keycloak login', async () => {
|
await test.step('Keycloak login', async () => {
|
||||||
@@ -108,13 +108,11 @@ test('invited with existing account', async ({ page }) => {
|
|||||||
await expect(page).toHaveTitle('Vaultwarden Web');
|
await expect(page).toHaveTitle('Vaultwarden Web');
|
||||||
await page.getByLabel('Master password').fill(users.user3.password);
|
await page.getByLabel('Master password').fill(users.user3.password);
|
||||||
await page.getByRole('button', { name: 'Unlock' }).click();
|
await page.getByRole('button', { name: 'Unlock' }).click();
|
||||||
|
|
||||||
await utils.checkNotification(page, 'Invitation accepted');
|
|
||||||
await utils.ignoreExtension(page);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await test.step('Default vault page', async () => {
|
await test.step('Default vault page', async () => {
|
||||||
await expect(page).toHaveTitle(/Vaultwarden Web/);
|
await expect(page).toHaveTitle(/Vaultwarden Web/);
|
||||||
|
await utils.checkNotification(page, 'Successfully accepted your invitation');
|
||||||
});
|
});
|
||||||
|
|
||||||
await test.step('Check mails', async () => {
|
await test.step('Check mails', async () => {
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ test('Organization is visible', async ({ page }) => {
|
|||||||
await expect(page.getByLabel('Filter: Default collection')).toBeVisible();
|
await expect(page.getByLabel('Filter: Default collection')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Enforce password policy', async ({ page }) => {
|
test('Activate password policy', async ({ page }) => {
|
||||||
await logUser(test, page, users.user1);
|
await logUser(test, page, users.user1);
|
||||||
await orgs.policies(test, page, '/Test');
|
await orgs.policies(test, page, '/Test');
|
||||||
|
|
||||||
@@ -61,16 +61,27 @@ test('Enforce password policy', async ({ page }) => {
|
|||||||
await page.getByRole('button', { name: 'Save' }).click();
|
await page.getByRole('button', { name: 'Save' }).click();
|
||||||
await utils.checkNotification(page, 'Edited policy Master password requirements.');
|
await utils.checkNotification(page, 'Edited policy Master password requirements.');
|
||||||
});
|
});
|
||||||
|
});
|
||||||
await utils.logout(test, page, users.user1);
|
|
||||||
|
test('Unlock trigger policyy', async ({ page }) => {
|
||||||
await test.step(`Unlock trigger policy`, async () => {
|
await page.goto('/', { waitUntil: 'domcontentloaded' });
|
||||||
await page.locator("input[type=email].vw-email-sso").fill(users.user1.email);
|
|
||||||
await page.getByRole('button', { name: 'Use single sign-on' }).click();
|
await page.locator("input[type=email].vw-email-sso").fill(users.user2.email);
|
||||||
|
await page.getByRole('button', { name: /Use single sign-on/ }).click();
|
||||||
await page.getByRole('textbox', { name: 'Master password (required)' }).fill(users.user1.password);
|
|
||||||
await page.getByRole('button', { name: 'Unlock' }).click();
|
await test.step('Keycloak login', async () => {
|
||||||
|
await expect(page.getByRole('heading', { name: 'Sign in to your account' })).toBeVisible();
|
||||||
await expect(page.getByRole('heading', { name: 'Update master password' })).toBeVisible();
|
await page.getByLabel(/Username/).fill(users.user2.name);
|
||||||
});
|
await page.getByLabel('Password', { exact: true }).fill(users.user2.password);
|
||||||
|
await page.getByRole('button', { name: 'Sign In' }).click();
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Unlock vault', async () => {
|
||||||
|
await expect(page).toHaveTitle('Vaultwarden Web');
|
||||||
|
await expect(page.getByRole('heading', { name: 'Your vault is locked' })).toBeVisible();
|
||||||
|
await page.getByLabel('Master password').fill(users.user2.password);
|
||||||
|
await page.getByRole('button', { name: 'Unlock' }).click();
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(page.getByRole('heading', { name: 'Update master password' })).toBeVisible();
|
||||||
});
|
});
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
[toolchain]
|
[toolchain]
|
||||||
channel = "1.92.0"
|
channel = "1.99.0"
|
||||||
components = [ "rustfmt", "clippy" ]
|
components = [ "rustfmt", "clippy" ]
|
||||||
profile = "minimal"
|
profile = "minimal"
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
edition = "2021"
|
edition = "2024"
|
||||||
max_width = 120
|
max_width = 120
|
||||||
newline_style = "Unix"
|
newline_style = "Unix"
|
||||||
use_small_heuristics = "Off"
|
use_small_heuristics = "Off"
|
||||||
+138
-94
@@ -2,39 +2,40 @@ use std::{env, sync::LazyLock};
|
|||||||
|
|
||||||
use reqwest::Method;
|
use reqwest::Method;
|
||||||
use rocket::{
|
use rocket::{
|
||||||
|
Catcher, Route,
|
||||||
form::Form,
|
form::Form,
|
||||||
http::{Cookie, CookieJar, MediaType, SameSite, Status},
|
http::{Cookie, CookieJar, MediaType, SameSite, Status},
|
||||||
request::{FromRequest, Outcome, Request},
|
request::{FromRequest, Outcome, Request},
|
||||||
response::{content::RawHtml as Html, Redirect},
|
response::{Redirect, content::RawHtml as Html},
|
||||||
serde::json::Json,
|
serde::json::Json,
|
||||||
Catcher, Route,
|
|
||||||
};
|
};
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
CONFIG, VERSION,
|
||||||
api::{
|
api::{
|
||||||
|
ApiResult, EmptyResult, JsonResult, Notify,
|
||||||
core::{log_event, two_factor},
|
core::{log_event, two_factor},
|
||||||
unregister_push_device, ApiResult, EmptyResult, JsonResult, Notify,
|
unregister_push_device,
|
||||||
},
|
},
|
||||||
auth::{decode_admin, encode_jwt, generate_admin_claims, ClientIp, Secure},
|
auth::{ClientIp, Secure, decode_admin, encode_jwt, generate_admin_claims},
|
||||||
config::ConfigBuilder,
|
config::ConfigBuilder,
|
||||||
db::{
|
db::{
|
||||||
backup_sqlite, get_sql_server_version,
|
ACTIVE_DB_TYPE, DbConn, DbConnType, backup_sqlite, get_sql_server_version,
|
||||||
models::{
|
models::{
|
||||||
Attachment, Cipher, Collection, Device, Event, EventType, Group, Invitation, Membership, MembershipId,
|
Attachment, Cipher, Collection, Device, Event, EventType, Group, Invitation, Membership, MembershipId,
|
||||||
MembershipType, OrgPolicy, Organization, OrganizationId, SsoUser, TwoFactor, User, UserId,
|
MembershipType, OrgPolicy, Organization, OrganizationId, SsoUser, TwoFactor, User, UserId,
|
||||||
},
|
},
|
||||||
DbConn, DbConnType, ACTIVE_DB_TYPE,
|
|
||||||
},
|
},
|
||||||
error::{Error, MapResult},
|
error::{Error, MapResult},
|
||||||
http_client::make_http_request,
|
http_client::make_http_request,
|
||||||
mail,
|
mail,
|
||||||
|
sso::FAKE_SSO_IDENTIFIER,
|
||||||
util::{
|
util::{
|
||||||
container_base_image, format_naive_datetime_local, get_active_web_release, get_display_size,
|
FeatureFlagFilter, NumberOrString, container_base_image, format_naive_datetime_local, get_active_web_release,
|
||||||
is_running_in_container, NumberOrString,
|
get_display_size, is_running_in_container, parse_experimental_client_feature_flags,
|
||||||
},
|
},
|
||||||
CONFIG, VERSION,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
@@ -92,8 +93,7 @@ static DB_TYPE: LazyLock<&str> = LazyLock::new(|| match ACTIVE_DB_TYPE.get() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
#[cfg(sqlite)]
|
#[cfg(sqlite)]
|
||||||
static CAN_BACKUP: LazyLock<bool> =
|
static CAN_BACKUP: LazyLock<bool> = LazyLock::new(|| ACTIVE_DB_TYPE.get().is_some_and(|t| *t == DbConnType::Sqlite));
|
||||||
LazyLock::new(|| ACTIVE_DB_TYPE.get().map(|t| *t == DbConnType::Sqlite).unwrap_or(false));
|
|
||||||
#[cfg(not(sqlite))]
|
#[cfg(not(sqlite))]
|
||||||
static CAN_BACKUP: LazyLock<bool> = LazyLock::new(|| false);
|
static CAN_BACKUP: LazyLock<bool> = LazyLock::new(|| false);
|
||||||
|
|
||||||
@@ -199,13 +199,7 @@ fn post_admin_login(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If the token is invalid, redirect to login page
|
// If the token is invalid, redirect to login page
|
||||||
if !_validate_token(&data.token) {
|
if validate_token(&data.token) {
|
||||||
error!("Invalid admin token. IP: {}", ip.ip);
|
|
||||||
Err(AdminResponse::Unauthorized(render_admin_login(
|
|
||||||
Some("Invalid admin token, please try again."),
|
|
||||||
redirect.as_deref(),
|
|
||||||
)))
|
|
||||||
} else {
|
|
||||||
// If the token received is valid, generate JWT and save it as a cookie
|
// If the token received is valid, generate JWT and save it as a cookie
|
||||||
let claims = generate_admin_claims();
|
let claims = generate_admin_claims();
|
||||||
let jwt = encode_jwt(&claims);
|
let jwt = encode_jwt(&claims);
|
||||||
@@ -223,15 +217,21 @@ fn post_admin_login(
|
|||||||
} else {
|
} else {
|
||||||
Err(AdminResponse::Ok(render_admin_page()))
|
Err(AdminResponse::Ok(render_admin_page()))
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
error!("Invalid admin token. IP: {}", ip.ip);
|
||||||
|
Err(AdminResponse::Unauthorized(render_admin_login(
|
||||||
|
Some("Invalid admin token, please try again."),
|
||||||
|
redirect.as_deref(),
|
||||||
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _validate_token(token: &str) -> bool {
|
fn validate_token(token: &str) -> bool {
|
||||||
match CONFIG.admin_token().as_ref() {
|
match CONFIG.admin_token().as_ref() {
|
||||||
None => false,
|
None => false,
|
||||||
Some(t) if t.starts_with("$argon2") => {
|
Some(t) if t.starts_with("$argon2") => {
|
||||||
use argon2::password_hash::PasswordVerifier;
|
use argon2::password_hash::PasswordVerifier;
|
||||||
match argon2::password_hash::PasswordHash::new(t) {
|
match argon2::password_hash::phc::PasswordHash::new(t) {
|
||||||
Ok(h) => {
|
Ok(h) => {
|
||||||
// NOTE: hash params from `ADMIN_TOKEN` are used instead of what is configured in the `Argon2` instance.
|
// NOTE: hash params from `ADMIN_TOKEN` are used instead of what is configured in the `Argon2` instance.
|
||||||
argon2::Argon2::default().verify_password(token.trim().as_ref(), &h).is_ok()
|
argon2::Argon2::default().verify_password(token.trim().as_ref(), &h).is_ok()
|
||||||
@@ -306,17 +306,14 @@ async fn get_user_or_404(user_id: &UserId, conn: &DbConn) -> ApiResult<User> {
|
|||||||
|
|
||||||
#[post("/invite", format = "application/json", data = "<data>")]
|
#[post("/invite", format = "application/json", data = "<data>")]
|
||||||
async fn invite_user(data: Json<InviteData>, _token: AdminToken, conn: DbConn) -> JsonResult {
|
async fn invite_user(data: Json<InviteData>, _token: AdminToken, conn: DbConn) -> JsonResult {
|
||||||
let data: InviteData = data.into_inner();
|
async fn generate_invite(user: &User, conn: &DbConn) -> EmptyResult {
|
||||||
if User::find_by_mail(&data.email, &conn).await.is_some() {
|
|
||||||
err_code!("User already exists", Status::Conflict.code)
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut user = User::new(&data.email, None);
|
|
||||||
|
|
||||||
async fn _generate_invite(user: &User, conn: &DbConn) -> EmptyResult {
|
|
||||||
if CONFIG.mail_enabled() {
|
if CONFIG.mail_enabled() {
|
||||||
let org_id: OrganizationId = FAKE_ADMIN_UUID.to_string().into();
|
let org_id: OrganizationId = if CONFIG.sso_enabled() {
|
||||||
let member_id: MembershipId = FAKE_ADMIN_UUID.to_string().into();
|
FAKE_SSO_IDENTIFIER.into()
|
||||||
|
} else {
|
||||||
|
FAKE_ADMIN_UUID.into()
|
||||||
|
};
|
||||||
|
let member_id: MembershipId = FAKE_ADMIN_UUID.to_owned().into();
|
||||||
mail::send_invite(user, org_id, member_id, &CONFIG.invitation_org_name(), None).await
|
mail::send_invite(user, org_id, member_id, &CONFIG.invitation_org_name(), None).await
|
||||||
} else {
|
} else {
|
||||||
let invitation = Invitation::new(&user.email);
|
let invitation = Invitation::new(&user.email);
|
||||||
@@ -324,7 +321,14 @@ async fn invite_user(data: Json<InviteData>, _token: AdminToken, conn: DbConn) -
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_generate_invite(&user, &conn).await.map_err(|e| e.with_code(Status::InternalServerError.code))?;
|
let data: InviteData = data.into_inner();
|
||||||
|
if User::find_by_mail(&data.email, &conn).await.is_some() {
|
||||||
|
err_code!("User already exists", Status::Conflict.code)
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut user = User::new(&data.email, None);
|
||||||
|
|
||||||
|
generate_invite(&user, &conn).await.map_err(|e| e.with_code(Status::InternalServerError.code))?;
|
||||||
user.save(&conn).await.map_err(|e| e.with_code(Status::InternalServerError.code))?;
|
user.save(&conn).await.map_err(|e| e.with_code(Status::InternalServerError.code))?;
|
||||||
|
|
||||||
Ok(Json(user.to_json(&conn).await))
|
Ok(Json(user.to_json(&conn).await))
|
||||||
@@ -381,7 +385,7 @@ async fn users_overview(_token: AdminToken, conn: DbConn) -> ApiResult<Html<Stri
|
|||||||
None => json!("Never"),
|
None => json!("Never"),
|
||||||
};
|
};
|
||||||
|
|
||||||
usr["sso_identifier"] = json!(sso_u.map(|u| u.identifier.to_string()).unwrap_or(String::new()));
|
usr["sso_identifier"] = json!(sso_u.map_or(String::new(), |u| u.identifier.to_string()));
|
||||||
|
|
||||||
users_json.push(usr);
|
users_json.push(usr);
|
||||||
}
|
}
|
||||||
@@ -421,7 +425,7 @@ async fn delete_user(user_id: UserId, token: AdminToken, conn: DbConn) -> EmptyR
|
|||||||
|
|
||||||
for membership in memberships {
|
for membership in memberships {
|
||||||
log_event(
|
log_event(
|
||||||
EventType::OrganizationUserDeleted as i32,
|
EventType::OrganizationUserDeleted,
|
||||||
&membership.uuid,
|
&membership.uuid,
|
||||||
&membership.org_uuid,
|
&membership.org_uuid,
|
||||||
&ACTING_ADMIN_USER.into(),
|
&ACTING_ADMIN_USER.into(),
|
||||||
@@ -442,7 +446,7 @@ async fn delete_sso_user(user_id: UserId, token: AdminToken, conn: DbConn) -> Em
|
|||||||
|
|
||||||
for membership in memberships {
|
for membership in memberships {
|
||||||
log_event(
|
log_event(
|
||||||
EventType::OrganizationUserUnlinkedSso as i32,
|
EventType::OrganizationUserUnlinkedSso,
|
||||||
&membership.uuid,
|
&membership.uuid,
|
||||||
&membership.org_uuid,
|
&membership.org_uuid,
|
||||||
&ACTING_ADMIN_USER.into(),
|
&ACTING_ADMIN_USER.into(),
|
||||||
@@ -464,15 +468,15 @@ async fn deauth_user(user_id: UserId, _token: AdminToken, conn: DbConn, nt: Noti
|
|||||||
|
|
||||||
if CONFIG.push_enabled() {
|
if CONFIG.push_enabled() {
|
||||||
for device in Device::find_push_devices_by_user(&user.uuid, &conn).await {
|
for device in Device::find_push_devices_by_user(&user.uuid, &conn).await {
|
||||||
match unregister_push_device(&device.push_uuid).await {
|
match unregister_push_device(device.push_uuid.as_ref()).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}"),
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Device::delete_all_by_user(&user.uuid, &conn).await?;
|
Device::delete_all_by_user(&user.uuid, &conn).await?;
|
||||||
user.reset_security_stamp();
|
user.reset_security_stamp(&conn).await?;
|
||||||
|
|
||||||
user.save(&conn).await
|
user.save(&conn).await
|
||||||
}
|
}
|
||||||
@@ -480,14 +484,15 @@ async fn deauth_user(user_id: UserId, _token: AdminToken, conn: DbConn, nt: Noti
|
|||||||
#[post("/users/<user_id>/disable", format = "application/json")]
|
#[post("/users/<user_id>/disable", format = "application/json")]
|
||||||
async fn disable_user(user_id: UserId, _token: AdminToken, conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
async fn disable_user(user_id: UserId, _token: AdminToken, conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
||||||
let mut user = get_user_or_404(&user_id, &conn).await?;
|
let mut user = get_user_or_404(&user_id, &conn).await?;
|
||||||
Device::delete_all_by_user(&user.uuid, &conn).await?;
|
user.reset_security_stamp(&conn).await?;
|
||||||
user.reset_security_stamp();
|
|
||||||
user.enabled = false;
|
user.enabled = false;
|
||||||
|
|
||||||
let save_result = user.save(&conn).await;
|
let save_result = user.save(&conn).await;
|
||||||
|
|
||||||
nt.send_logout(&user, None, &conn).await;
|
nt.send_logout(&user, None, &conn).await;
|
||||||
|
|
||||||
|
Device::delete_all_by_user(&user.uuid, &conn).await?;
|
||||||
|
|
||||||
save_result
|
save_result
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -503,6 +508,7 @@ async fn enable_user(user_id: UserId, _token: AdminToken, conn: DbConn) -> Empty
|
|||||||
async fn remove_2fa(user_id: UserId, token: AdminToken, conn: DbConn) -> EmptyResult {
|
async fn remove_2fa(user_id: UserId, token: AdminToken, conn: DbConn) -> EmptyResult {
|
||||||
let mut user = get_user_or_404(&user_id, &conn).await?;
|
let mut user = get_user_or_404(&user_id, &conn).await?;
|
||||||
TwoFactor::delete_all_by_user(&user.uuid, &conn).await?;
|
TwoFactor::delete_all_by_user(&user.uuid, &conn).await?;
|
||||||
|
Device::clear_twofactor_remember_by_user(&user.uuid, &conn).await?;
|
||||||
two_factor::enforce_2fa_policy(&user, &ACTING_ADMIN_USER.into(), 14, &token.ip.ip, &conn).await?;
|
two_factor::enforce_2fa_policy(&user, &ACTING_ADMIN_USER.into(), 14, &token.ip.ip, &conn).await?;
|
||||||
user.totp_recover = None;
|
user.totp_recover = None;
|
||||||
user.save(&conn).await
|
user.save(&conn).await
|
||||||
@@ -517,8 +523,12 @@ async fn resend_user_invite(user_id: UserId, _token: AdminToken, conn: DbConn) -
|
|||||||
}
|
}
|
||||||
|
|
||||||
if CONFIG.mail_enabled() {
|
if CONFIG.mail_enabled() {
|
||||||
let org_id: OrganizationId = FAKE_ADMIN_UUID.to_string().into();
|
let org_id: OrganizationId = if CONFIG.sso_enabled() {
|
||||||
let member_id: MembershipId = FAKE_ADMIN_UUID.to_string().into();
|
FAKE_SSO_IDENTIFIER.into()
|
||||||
|
} else {
|
||||||
|
FAKE_ADMIN_UUID.into()
|
||||||
|
};
|
||||||
|
let member_id: MembershipId = FAKE_ADMIN_UUID.to_owned().into();
|
||||||
mail::send_invite(&user, org_id, member_id, &CONFIG.invitation_org_name(), None).await
|
mail::send_invite(&user, org_id, member_id, &CONFIG.invitation_org_name(), None).await
|
||||||
} else {
|
} else {
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -544,9 +554,10 @@ async fn update_membership_type(data: Json<MembershipTypeData>, token: AdminToke
|
|||||||
err!("The specified user isn't member of the organization")
|
err!("The specified user isn't member of the organization")
|
||||||
};
|
};
|
||||||
|
|
||||||
let new_type = match MembershipType::from_str(&data.user_type.into_string()) {
|
let new_type = if let Some(new_type) = MembershipType::from_str(&data.user_type.into_string()) {
|
||||||
Some(new_type) => new_type as i32,
|
new_type as i32
|
||||||
None => err!("Invalid type"),
|
} else {
|
||||||
|
err!("Invalid type")
|
||||||
};
|
};
|
||||||
|
|
||||||
if member_to_edit.atype == MembershipType::Owner && new_type != MembershipType::Owner {
|
if member_to_edit.atype == MembershipType::Owner && new_type != MembershipType::Owner {
|
||||||
@@ -561,7 +572,7 @@ async fn update_membership_type(data: Json<MembershipTypeData>, token: AdminToke
|
|||||||
OrgPolicy::check_user_allowed(&member_to_edit, "modify", &conn).await?;
|
OrgPolicy::check_user_allowed(&member_to_edit, "modify", &conn).await?;
|
||||||
|
|
||||||
log_event(
|
log_event(
|
||||||
EventType::OrganizationUserUpdated as i32,
|
EventType::OrganizationUserUpdated,
|
||||||
&member_to_edit.uuid,
|
&member_to_edit.uuid,
|
||||||
&data.org_uuid,
|
&data.org_uuid,
|
||||||
&ACTING_ADMIN_USER.into(),
|
&ACTING_ADMIN_USER.into(),
|
||||||
@@ -633,12 +644,11 @@ async fn has_http_access() -> bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
use cached::proc_macro::cached;
|
use cached::macros::cached;
|
||||||
/// Cache this function to prevent API call rate limit. Github only allows 60 requests per hour, and we use 3 here already
|
/// Cache this function to prevent API call rate limit. Github only allows 60 requests per hour, and we use 3 here already
|
||||||
/// It will cache this function for 600 seconds (10 minutes) which should prevent the exhaustion of the rate limit
|
/// It will cache this function for 600 seconds (10 minutes) which should prevent the exhaustion of the rate limit
|
||||||
/// Any cache will be lost if Vaultwarden is restarted
|
/// Any cache will be lost if Vaultwarden is restarted
|
||||||
use std::time::Duration; // Needed for cached
|
#[cached(ttl_secs = 600, sync_writes = "default")]
|
||||||
#[cached(time = 600, sync_writes = "default")]
|
|
||||||
async fn get_release_info(has_http_access: bool) -> (String, String, String) {
|
async fn get_release_info(has_http_access: bool) -> (String, String, String) {
|
||||||
// If the HTTP Check failed, do not even attempt to check for new versions since we were not able to connect with github.com anyway.
|
// If the HTTP Check failed, do not even attempt to check for new versions since we were not able to connect with github.com anyway.
|
||||||
if has_http_access {
|
if has_http_access {
|
||||||
@@ -647,42 +657,40 @@ async fn get_release_info(has_http_access: bool) -> (String, String, String) {
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(r) => r.tag_name,
|
Ok(r) => r.tag_name,
|
||||||
_ => "-".to_string(),
|
_ => "-".to_owned(),
|
||||||
},
|
},
|
||||||
match get_json_api::<GitCommit>("https://api.github.com/repos/dani-garcia/vaultwarden/commits/main").await {
|
match get_json_api::<GitCommit>("https://api.github.com/repos/dani-garcia/vaultwarden/commits/main").await {
|
||||||
Ok(mut c) => {
|
Ok(mut c) => {
|
||||||
c.sha.truncate(8);
|
c.sha.truncate(8);
|
||||||
c.sha
|
c.sha
|
||||||
}
|
}
|
||||||
_ => "-".to_string(),
|
_ => "-".to_owned(),
|
||||||
},
|
},
|
||||||
// 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
|
||||||
match get_json_api::<GitRelease>("https://api.github.com/repos/dani-garcia/bw_web_builds/releases/latest")
|
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_owned(),
|
||||||
_ => "-".to_string(),
|
_ => "-".to_owned(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
("-".to_string(), "-".to_string(), "-".to_string())
|
("-".to_owned(), "-".to_owned(), "-".to_owned())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 && let Ok(cf_trace) = get_text_api("https://cloudflare.com/cdn-cgi/trace").await {
|
||||||
if let Ok(cf_trace) = get_text_api("https://cloudflare.com/cdn-cgi/trace").await {
|
for line in cf_trace.lines() {
|
||||||
for line in cf_trace.lines() {
|
if let Some((key, value)) = line.split_once('=')
|
||||||
if let Some((key, value)) = line.split_once('=') {
|
&& key == "ts"
|
||||||
if key == "ts" {
|
{
|
||||||
let ts = value.split_once('.').map_or(value, |(s, _)| s);
|
let ts = value.split_once('.').map_or(value, |(s, _)| s);
|
||||||
if let Ok(dt) = chrono::DateTime::parse_from_str(ts, "%s") {
|
if let Ok(dt) = chrono::DateTime::parse_from_str(ts, "%s") {
|
||||||
return dt.format("%Y-%m-%d %H:%M:%S UTC").to_string();
|
return dt.format("%Y-%m-%d %H:%M:%S UTC").to_string();
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -709,6 +717,36 @@ fn web_vault_compare(active: &str, latest: &str) -> i8 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn check_template_overrides() -> Vec<&'static str> {
|
||||||
|
let template_folder = std::path::PathBuf::from(CONFIG.templates_folder());
|
||||||
|
let mut overrides = Vec::new();
|
||||||
|
for folder in ["admin", "email", "scss"] {
|
||||||
|
if folder_has_hbs_files(&template_folder.join(folder)) {
|
||||||
|
overrides.push(folder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if folder_has_hbs_files(&template_folder) {
|
||||||
|
overrides.push("other");
|
||||||
|
}
|
||||||
|
|
||||||
|
overrides
|
||||||
|
}
|
||||||
|
|
||||||
|
fn folder_has_hbs_files(dir: &std::path::Path) -> bool {
|
||||||
|
let Ok(files) = std::fs::read_dir(dir) else {
|
||||||
|
// No files in this directory at all, so we can return false
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
files.flatten().any(|f| {
|
||||||
|
// Validate if it is a file and if it has the `.hbs` extension and starts with a-z or 0-9
|
||||||
|
f.file_type().is_ok_and(|t| t.is_file())
|
||||||
|
&& f.path().extension().is_some_and(|e| e.eq_ignore_ascii_case("hbs"))
|
||||||
|
&& f.file_name().to_str().is_some_and(|n| n.starts_with(|c: char| c.is_ascii_alphanumeric()))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/diagnostics")]
|
#[get("/diagnostics")]
|
||||||
async fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> ApiResult<Html<String>> {
|
async fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> ApiResult<Html<String>> {
|
||||||
use chrono::prelude::*;
|
use chrono::prelude::*;
|
||||||
@@ -725,7 +763,7 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> A
|
|||||||
// Check if we are able to resolve DNS entries
|
// Check if we are able to resolve DNS entries
|
||||||
let dns_resolved = match ("github.com", 0).to_socket_addrs().map(|mut i| i.next()) {
|
let dns_resolved = match ("github.com", 0).to_socket_addrs().map(|mut i| i.next()) {
|
||||||
Ok(Some(a)) => a.ip().to_string(),
|
Ok(Some(a)) => a.ip().to_string(),
|
||||||
_ => "Unable to resolve domain name.".to_string(),
|
_ => "Unable to resolve domain name.".to_owned(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let (latest_vw_release, latest_vw_commit, latest_web_release) = get_release_info(has_http_access).await;
|
let (latest_vw_release, latest_vw_commit, latest_web_release) = get_release_info(has_http_access).await;
|
||||||
@@ -734,6 +772,13 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> A
|
|||||||
|
|
||||||
let ip_header_name = &ip_header.0.unwrap_or_default();
|
let ip_header_name = &ip_header.0.unwrap_or_default();
|
||||||
|
|
||||||
|
let invalid_feature_flags: Vec<String> = parse_experimental_client_feature_flags(
|
||||||
|
&CONFIG.experimental_client_feature_flags(),
|
||||||
|
&FeatureFlagFilter::InvalidOnly,
|
||||||
|
)
|
||||||
|
.into_keys()
|
||||||
|
.collect();
|
||||||
|
|
||||||
let diagnostics_json = json!({
|
let diagnostics_json = json!({
|
||||||
"dns_resolved": dns_resolved,
|
"dns_resolved": dns_resolved,
|
||||||
"current_release": VERSION,
|
"current_release": VERSION,
|
||||||
@@ -756,6 +801,8 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> A
|
|||||||
"db_version": get_sql_server_version(&conn).await,
|
"db_version": get_sql_server_version(&conn).await,
|
||||||
"admin_url": format!("{}/diagnostics", admin_url()),
|
"admin_url": format!("{}/diagnostics", admin_url()),
|
||||||
"overrides": &CONFIG.get_overrides().join(", "),
|
"overrides": &CONFIG.get_overrides().join(", "),
|
||||||
|
"template_overrides": check_template_overrides().join(", "),
|
||||||
|
"invalid_feature_flags": invalid_feature_flags,
|
||||||
"host_arch": env::consts::ARCH,
|
"host_arch": env::consts::ARCH,
|
||||||
"host_os": env::consts::OS,
|
"host_os": env::consts::OS,
|
||||||
"tz_env": env::var("TZ").unwrap_or_default(),
|
"tz_env": env::var("TZ").unwrap_or_default(),
|
||||||
@@ -817,33 +864,30 @@ impl<'r> FromRequest<'r> for AdminToken {
|
|||||||
type Error = &'static str;
|
type Error = &'static str;
|
||||||
|
|
||||||
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||||
let ip = match ClientIp::from_request(request).await {
|
let Outcome::Success(ip) = ClientIp::from_request(request).await else {
|
||||||
Outcome::Success(ip) => ip,
|
err_handler!("Error getting Client IP")
|
||||||
_ => err_handler!("Error getting Client IP"),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if !CONFIG.disable_admin_token() {
|
if !CONFIG.disable_admin_token() {
|
||||||
let cookies = request.cookies();
|
let cookies = request.cookies();
|
||||||
|
|
||||||
let access_token = match cookies.get(COOKIE_NAME) {
|
let access_token = if let Some(cookie) = cookies.get(COOKIE_NAME) {
|
||||||
Some(cookie) => cookie.value(),
|
cookie.value()
|
||||||
None => {
|
} else {
|
||||||
let requested_page =
|
let requested_page =
|
||||||
request.segments::<std::path::PathBuf>(0..).unwrap_or_default().display().to_string();
|
request.segments::<std::path::PathBuf>(0..).unwrap_or_default().display().to_string();
|
||||||
// When the requested page is empty, it is `/admin`, in that case, Forward, so it will render the login page
|
// When the requested page is empty, it is `/admin`, in that case, Forward, so it will render the login page
|
||||||
// Else, return a 401 failure, which will be caught
|
// Else, return a 401 failure, which will be caught
|
||||||
if requested_page.is_empty() {
|
if requested_page.is_empty() {
|
||||||
return Outcome::Forward(Status::Unauthorized);
|
return Outcome::Forward(Status::Unauthorized);
|
||||||
} else {
|
|
||||||
return Outcome::Error((Status::Unauthorized, "Unauthorized"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return Outcome::Error((Status::Unauthorized, "Unauthorized"));
|
||||||
};
|
};
|
||||||
|
|
||||||
if decode_admin(access_token).is_err() {
|
if decode_admin(access_token).is_err() {
|
||||||
// Remove admin cookie
|
// Remove admin cookie
|
||||||
cookies.remove(Cookie::build(COOKIE_NAME).path(admin_path()));
|
cookies.remove(Cookie::build(COOKIE_NAME).path(admin_path()));
|
||||||
error!("Invalid or expired admin JWT. IP: {}.", &ip.ip);
|
error!("Invalid or expired admin JWT. IP: {}.", ip.ip);
|
||||||
return Outcome::Error((Status::Unauthorized, "Session expired"));
|
return Outcome::Error((Status::Unauthorized, "Session expired"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -862,23 +906,23 @@ mod tests {
|
|||||||
fn validate_web_vault_compare() {
|
fn validate_web_vault_compare() {
|
||||||
// web_vault_compare(active, latest)
|
// web_vault_compare(active, latest)
|
||||||
// Test normal versions
|
// Test normal versions
|
||||||
assert!(web_vault_compare("2025.12.0", "2025.12.1") == -1);
|
assert_eq!(web_vault_compare("2025.12.0", "2025.12.1"), -1);
|
||||||
assert!(web_vault_compare("2025.12.1", "2025.12.1") == 0);
|
assert_eq!(web_vault_compare("2025.12.1", "2025.12.1"), 0);
|
||||||
assert!(web_vault_compare("2025.12.2", "2025.12.1") == 1);
|
assert_eq!(web_vault_compare("2025.12.2", "2025.12.1"), 1);
|
||||||
|
|
||||||
// Test patched/+build.n versions
|
// Test patched/+build.n versions
|
||||||
// Newer latest version
|
// Newer latest version
|
||||||
assert!(web_vault_compare("2025.12.0+build.1", "2025.12.1") == -1);
|
assert_eq!(web_vault_compare("2025.12.0+build.1", "2025.12.1"), -1);
|
||||||
assert!(web_vault_compare("2025.12.1", "2025.12.1+build.1") == -1);
|
assert_eq!(web_vault_compare("2025.12.1", "2025.12.1+build.1"), -1);
|
||||||
assert!(web_vault_compare("2025.12.0+build.1", "2025.12.1+build.1") == -1);
|
assert_eq!(web_vault_compare("2025.12.0+build.1", "2025.12.1+build.1"), -1);
|
||||||
assert!(web_vault_compare("2025.12.1+build.1", "2025.12.1+build.2") == -1);
|
assert_eq!(web_vault_compare("2025.12.1+build.1", "2025.12.1+build.2"), -1);
|
||||||
// Equal versions
|
// Equal versions
|
||||||
assert!(web_vault_compare("2025.12.1+build.1", "2025.12.1+build.1") == 0);
|
assert_eq!(web_vault_compare("2025.12.1+build.1", "2025.12.1+build.1"), 0);
|
||||||
assert!(web_vault_compare("2025.12.2+build.2", "2025.12.2+build.2") == 0);
|
assert_eq!(web_vault_compare("2025.12.2+build.2", "2025.12.2+build.2"), 0);
|
||||||
// Newer active version
|
// Newer active version
|
||||||
assert!(web_vault_compare("2025.12.1+build.1", "2025.12.1") == 1);
|
assert_eq!(web_vault_compare("2025.12.1+build.1", "2025.12.1"), 1);
|
||||||
assert!(web_vault_compare("2025.12.2", "2025.12.1+build.1") == 1);
|
assert_eq!(web_vault_compare("2025.12.2", "2025.12.1+build.1"), 1);
|
||||||
assert!(web_vault_compare("2025.12.2+build.1", "2025.12.1+build.1") == 1);
|
assert_eq!(web_vault_compare("2025.12.2+build.1", "2025.12.1+build.1"), 1);
|
||||||
assert!(web_vault_compare("2025.12.1+build.3", "2025.12.1+build.2") == 1);
|
assert_eq!(web_vault_compare("2025.12.1+build.3", "2025.12.1+build.2"), 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+505
-248
File diff suppressed because it is too large.
Load diff
+444
-212
File diff suppressed because it is too large.
Load diff
@@ -1,23 +1,26 @@
|
|||||||
use chrono::{TimeDelta, Utc};
|
use chrono::{TimeDelta, Utc};
|
||||||
use rocket::{serde::json::Json, Route};
|
use rocket::{Route, serde::json::Json};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
CONFIG,
|
||||||
api::{
|
api::{
|
||||||
core::{CipherSyncData, CipherSyncType},
|
|
||||||
EmptyResult, JsonResult,
|
EmptyResult, JsonResult,
|
||||||
|
core::{
|
||||||
|
CipherSyncData, CipherSyncType,
|
||||||
|
accounts::{AuthenticationData, UnlockData},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
auth::{decode_emergency_access_invite, Headers},
|
auth::{Headers, decode_emergency_access_invite},
|
||||||
db::{
|
db::{
|
||||||
|
DbConn, DbPool,
|
||||||
models::{
|
models::{
|
||||||
Cipher, EmergencyAccess, EmergencyAccessId, EmergencyAccessStatus, EmergencyAccessType, Invitation,
|
Cipher, EmergencyAccess, EmergencyAccessId, EmergencyAccessStatus, EmergencyAccessType, Invitation,
|
||||||
Membership, MembershipType, OrgPolicy, TwoFactor, User, UserId,
|
Membership, MembershipType, OrgPolicy, TwoFactor, User, UserId,
|
||||||
},
|
},
|
||||||
DbConn, DbPool,
|
|
||||||
},
|
},
|
||||||
mail,
|
mail,
|
||||||
util::NumberOrString,
|
util::NumberOrString,
|
||||||
CONFIG,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
@@ -55,7 +58,7 @@ async fn get_contacts(headers: Headers, conn: DbConn) -> Json<Value> {
|
|||||||
let mut emergency_access_list_json = Vec::with_capacity(emergency_access_list.len());
|
let mut emergency_access_list_json = Vec::with_capacity(emergency_access_list.len());
|
||||||
for ea in emergency_access_list {
|
for ea in emergency_access_list {
|
||||||
if let Some(grantee) = ea.to_json_grantee_details(&conn).await {
|
if let Some(grantee) = ea.to_json_grantee_details(&conn).await {
|
||||||
emergency_access_list_json.push(grantee)
|
emergency_access_list_json.push(grantee);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,11 +92,14 @@ async fn get_grantees(headers: Headers, conn: DbConn) -> Json<Value> {
|
|||||||
async fn get_emergency_access(emer_id: EmergencyAccessId, headers: Headers, conn: DbConn) -> JsonResult {
|
async fn get_emergency_access(emer_id: EmergencyAccessId, headers: Headers, conn: DbConn) -> JsonResult {
|
||||||
check_emergency_access_enabled()?;
|
check_emergency_access_enabled()?;
|
||||||
|
|
||||||
match EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &conn).await {
|
if let Some(emergency_access) =
|
||||||
Some(emergency_access) => Ok(Json(
|
EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &conn).await
|
||||||
|
{
|
||||||
|
Ok(Json(
|
||||||
emergency_access.to_json_grantee_details(&conn).await.expect("Grantee user should exist but does not!"),
|
emergency_access.to_json_grantee_details(&conn).await.expect("Grantee user should exist but does not!"),
|
||||||
)),
|
))
|
||||||
None => err!("Emergency access not valid."),
|
} else {
|
||||||
|
err!("Emergency access not valid.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,9 +142,10 @@ async fn post_emergency_access(
|
|||||||
err!("Emergency access not valid.")
|
err!("Emergency access not valid.")
|
||||||
};
|
};
|
||||||
|
|
||||||
let new_type = match EmergencyAccessType::from_str(&data.r#type.into_string()) {
|
let new_type = if let Some(new_type) = EmergencyAccessType::from_str(&data.r#type.into_string()) {
|
||||||
Some(new_type) => new_type as i32,
|
new_type as i32
|
||||||
None => err!("Invalid emergency access type."),
|
} else {
|
||||||
|
err!("Invalid emergency access type.")
|
||||||
};
|
};
|
||||||
|
|
||||||
emergency_access.atype = new_type;
|
emergency_access.atype = new_type;
|
||||||
@@ -205,9 +212,10 @@ async fn send_invite(data: Json<EmergencyAccessInviteData>, headers: Headers, co
|
|||||||
|
|
||||||
let emergency_access_status = EmergencyAccessStatus::Invited as i32;
|
let emergency_access_status = EmergencyAccessStatus::Invited as i32;
|
||||||
|
|
||||||
let new_type = match EmergencyAccessType::from_str(&data.r#type.into_string()) {
|
let new_type = if let Some(new_type) = EmergencyAccessType::from_str(&data.r#type.into_string()) {
|
||||||
Some(new_type) => new_type as i32,
|
new_type as i32
|
||||||
None => err!("Invalid emergency access type."),
|
} else {
|
||||||
|
err!("Invalid emergency access type.")
|
||||||
};
|
};
|
||||||
|
|
||||||
let grantor_user = headers.user;
|
let grantor_user = headers.user;
|
||||||
@@ -220,7 +228,7 @@ async fn send_invite(data: Json<EmergencyAccessInviteData>, headers: Headers, co
|
|||||||
let (grantee_user, new_user) = match User::find_by_mail(&email, &conn).await {
|
let (grantee_user, new_user) = match User::find_by_mail(&email, &conn).await {
|
||||||
None => {
|
None => {
|
||||||
if !CONFIG.invitations_allowed() {
|
if !CONFIG.invitations_allowed() {
|
||||||
err!(format!("Grantee user does not exist: {email}"))
|
err!(format!("Grantee user does not exist: {}", email.escape_debug()))
|
||||||
}
|
}
|
||||||
|
|
||||||
if !CONFIG.is_email_domain_allowed(&email) {
|
if !CONFIG.is_email_domain_allowed(&email) {
|
||||||
@@ -342,12 +350,11 @@ async fn accept_invite(
|
|||||||
err!("Claim email does not match current users email")
|
err!("Claim email does not match current users email")
|
||||||
}
|
}
|
||||||
|
|
||||||
let grantee_user = match User::find_by_mail(&claims.email, &conn).await {
|
let grantee_user = if let Some(user) = User::find_by_mail(&claims.email, &conn).await {
|
||||||
Some(user) => {
|
Invitation::take(&claims.email, &conn).await;
|
||||||
Invitation::take(&claims.email, &conn).await;
|
user
|
||||||
user
|
} else {
|
||||||
}
|
err!("Invited user not found")
|
||||||
None => err!("Invited user not found"),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// We need to search for the uuid in combination with the email, since we do not yet store the uuid of the grantee in the database.
|
// We need to search for the uuid in combination with the email, since we do not yet store the uuid of the grantee in the database.
|
||||||
@@ -611,6 +618,7 @@ async fn takeover_emergency_access(emer_id: EmergencyAccessId, headers: Headers,
|
|||||||
"kdfMemory": grantor_user.client_kdf_memory,
|
"kdfMemory": grantor_user.client_kdf_memory,
|
||||||
"kdfParallelism": grantor_user.client_kdf_parallelism,
|
"kdfParallelism": grantor_user.client_kdf_parallelism,
|
||||||
"keyEncrypted": &emergency_access.key_encrypted,
|
"keyEncrypted": &emergency_access.key_encrypted,
|
||||||
|
"salt": grantor_user.master_password_salt(),
|
||||||
"object": "emergencyAccessTakeover",
|
"object": "emergencyAccessTakeover",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -620,8 +628,13 @@ async fn takeover_emergency_access(emer_id: EmergencyAccessId, headers: Headers,
|
|||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct EmergencyAccessPasswordData {
|
struct EmergencyAccessPasswordData {
|
||||||
new_master_password_hash: String,
|
// Legacy payload
|
||||||
key: String,
|
new_master_password_hash: Option<String>,
|
||||||
|
key: Option<String>,
|
||||||
|
|
||||||
|
// Current payload
|
||||||
|
authentication_data: Option<AuthenticationData>,
|
||||||
|
unlock_data: Option<UnlockData>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/emergency-access/<emer_id>/password", data = "<data>")]
|
#[post("/emergency-access/<emer_id>/password", data = "<data>")]
|
||||||
@@ -634,8 +647,6 @@ async fn password_emergency_access(
|
|||||||
check_emergency_access_enabled()?;
|
check_emergency_access_enabled()?;
|
||||||
|
|
||||||
let data: EmergencyAccessPasswordData = data.into_inner();
|
let data: EmergencyAccessPasswordData = data.into_inner();
|
||||||
let new_master_password_hash = &data.new_master_password_hash;
|
|
||||||
//let key = &data.Key;
|
|
||||||
|
|
||||||
let requesting_user = headers.user;
|
let requesting_user = headers.user;
|
||||||
let Some(emergency_access) =
|
let Some(emergency_access) =
|
||||||
@@ -652,8 +663,23 @@ async fn password_emergency_access(
|
|||||||
err!("Grantor user not found.")
|
err!("Grantor user not found.")
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let (new_master_password_hash, new_key) =
|
||||||
|
if let (Some(authentication_data), Some(unlock_data)) = (data.authentication_data, data.unlock_data) {
|
||||||
|
authentication_data.check(&grantor_user, &unlock_data)?;
|
||||||
|
|
||||||
|
if !authentication_data.kdf.matches_user(&grantor_user) {
|
||||||
|
err!("KDF settings do not match the grantor account")
|
||||||
|
}
|
||||||
|
|
||||||
|
(authentication_data.master_password_authentication_hash, unlock_data.master_key_wrapped_user_key)
|
||||||
|
} else if let (Some(new_master_password_hash), Some(new_key)) = (data.new_master_password_hash, data.key) {
|
||||||
|
(new_master_password_hash, new_key)
|
||||||
|
} else {
|
||||||
|
err!("Invalid request!")
|
||||||
|
};
|
||||||
|
|
||||||
// change grantor_user password
|
// change grantor_user password
|
||||||
grantor_user.set_password(new_master_password_hash, Some(data.key), true, None);
|
grantor_user.set_password(&new_master_password_hash, Some(new_key), true, None, &conn).await?;
|
||||||
grantor_user.save(&conn).await?;
|
grantor_user.save(&conn).await?;
|
||||||
|
|
||||||
// Disable TwoFactor providers since they will otherwise block logins
|
// Disable TwoFactor providers since they will otherwise block logins
|
||||||
@@ -766,7 +792,7 @@ pub async fn emergency_request_timeout_job(pool: DbPool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
error!("Failed to get DB connection while searching emergency request timed out")
|
error!("Failed to get DB connection while searching emergency request timed out");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -825,6 +851,6 @@ pub async fn emergency_notification_reminder_job(pool: DbPool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
error!("Failed to get DB connection while searching emergency notification reminder")
|
error!("Failed to get DB connection while searching emergency notification reminder");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+98
-81
@@ -1,18 +1,19 @@
|
|||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
|
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use rocket::{form::FromForm, serde::json::Json, Route};
|
use rocket::{Route, form::FromForm, serde::json::Json};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{EmptyResult, JsonResult},
|
CONFIG,
|
||||||
|
api::{ApiResult, EmptyResult, JsonResult},
|
||||||
auth::{AdminHeaders, Headers},
|
auth::{AdminHeaders, Headers},
|
||||||
db::{
|
db::{
|
||||||
models::{Cipher, CipherId, Event, Membership, MembershipId, OrganizationId, UserId},
|
|
||||||
DbConn, DbPool,
|
DbConn, DbPool,
|
||||||
|
models::{Cipher, CipherId, Event, EventType, Membership, MembershipId, OrganizationId, UserId},
|
||||||
},
|
},
|
||||||
|
error::MapResult,
|
||||||
util::parse_date,
|
util::parse_date,
|
||||||
CONFIG,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// ###############################################################################################################
|
/// ###############################################################################################################
|
||||||
@@ -29,6 +30,14 @@ struct EventRange {
|
|||||||
continuation_token: Option<String>,
|
continuation_token: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl EventRange {
|
||||||
|
fn date_range(&self) -> ApiResult<(NaiveDateTime, NaiveDateTime)> {
|
||||||
|
let start_date = parse_date(&self.start).map_res("Invalid start date")?;
|
||||||
|
let end_date = parse_date(self.continuation_token.as_ref().unwrap_or(&self.end)).map_res("Invalid end date")?;
|
||||||
|
Ok((start_date, end_date))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Upstream: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/AdminConsole/Controllers/EventsController.cs#L87
|
// 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(org_id: OrganizationId, data: EventRange, headers: AdminHeaders, conn: DbConn) -> JsonResult {
|
async fn get_org_events(org_id: OrganizationId, data: EventRange, headers: AdminHeaders, conn: DbConn) -> JsonResult {
|
||||||
@@ -38,21 +47,16 @@ async fn get_org_events(org_id: OrganizationId, data: EventRange, headers: Admin
|
|||||||
|
|
||||||
// Return an empty vec when we org events are disabled.
|
// Return an empty vec when we org events are disabled.
|
||||||
// This prevents client errors
|
// This prevents client errors
|
||||||
let events_json: Vec<Value> = if !CONFIG.org_events_enabled() {
|
let events_json: Vec<Value> = if CONFIG.org_events_enabled() {
|
||||||
Vec::with_capacity(0)
|
let (start_date, end_date) = data.date_range()?;
|
||||||
} else {
|
|
||||||
let start_date = parse_date(&data.start);
|
|
||||||
let end_date = if let Some(before_date) = &data.continuation_token {
|
|
||||||
parse_date(before_date)
|
|
||||||
} else {
|
|
||||||
parse_date(&data.end)
|
|
||||||
};
|
|
||||||
|
|
||||||
Event::find_by_organization_uuid(&org_id, &start_date, &end_date, &conn)
|
Event::find_by_organization_uuid(&org_id, &start_date, &end_date, &conn)
|
||||||
.await
|
.await
|
||||||
.iter()
|
.iter()
|
||||||
.map(|e| e.to_json())
|
.map(Event::to_json)
|
||||||
.collect()
|
.collect()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
@@ -64,27 +68,16 @@ async fn get_org_events(org_id: OrganizationId, data: EventRange, headers: Admin
|
|||||||
|
|
||||||
#[get("/ciphers/<cipher_id>/events?<data..>")]
|
#[get("/ciphers/<cipher_id>/events?<data..>")]
|
||||||
async fn get_cipher_events(cipher_id: CipherId, data: EventRange, headers: Headers, conn: DbConn) -> JsonResult {
|
async fn get_cipher_events(cipher_id: CipherId, data: EventRange, headers: Headers, conn: DbConn) -> JsonResult {
|
||||||
// Return an empty vec when we org events are disabled.
|
// Return an empty vec when org events are disabled.
|
||||||
// This prevents client errors
|
// This prevents client errors
|
||||||
let events_json: Vec<Value> = if !CONFIG.org_events_enabled() {
|
let events_json: Vec<Value> = if CONFIG.org_events_enabled()
|
||||||
Vec::with_capacity(0)
|
&& Membership::user_has_ge_admin_access_to_cipher(&headers.user.uuid, &cipher_id, &conn).await
|
||||||
} else {
|
{
|
||||||
let mut events_json = Vec::with_capacity(0);
|
let (start_date, end_date) = data.date_range()?;
|
||||||
if Membership::user_has_ge_admin_access_to_cipher(&headers.user.uuid, &cipher_id, &conn).await {
|
|
||||||
let start_date = parse_date(&data.start);
|
|
||||||
let end_date = if let Some(before_date) = &data.continuation_token {
|
|
||||||
parse_date(before_date)
|
|
||||||
} else {
|
|
||||||
parse_date(&data.end)
|
|
||||||
};
|
|
||||||
|
|
||||||
events_json = Event::find_by_cipher_uuid(&cipher_id, &start_date, &end_date, &conn)
|
Event::find_by_cipher_uuid(&cipher_id, &start_date, &end_date, &conn).await.iter().map(Event::to_json).collect()
|
||||||
.await
|
} else {
|
||||||
.iter()
|
Vec::new()
|
||||||
.map(|e| e.to_json())
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
events_json
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
@@ -107,21 +100,16 @@ async fn get_user_events(
|
|||||||
}
|
}
|
||||||
// Return an empty vec when we org events are disabled.
|
// Return an empty vec when we org events are disabled.
|
||||||
// This prevents client errors
|
// This prevents client errors
|
||||||
let events_json: Vec<Value> = if !CONFIG.org_events_enabled() {
|
let events_json: Vec<Value> = if CONFIG.org_events_enabled() {
|
||||||
Vec::with_capacity(0)
|
let (start_date, end_date) = data.date_range()?;
|
||||||
} else {
|
|
||||||
let start_date = parse_date(&data.start);
|
|
||||||
let end_date = if let Some(before_date) = &data.continuation_token {
|
|
||||||
parse_date(before_date)
|
|
||||||
} else {
|
|
||||||
parse_date(&data.end)
|
|
||||||
};
|
|
||||||
|
|
||||||
Event::find_by_org_and_member(&org_id, &member_id, &start_date, &end_date, &conn)
|
Event::find_by_org_and_member(&org_id, &member_id, &start_date, &end_date, &conn)
|
||||||
.await
|
.await
|
||||||
.iter()
|
.iter()
|
||||||
.map(|e| e.to_json())
|
.map(Event::to_json)
|
||||||
.collect()
|
.collect()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
@@ -134,7 +122,8 @@ async fn get_user_events(
|
|||||||
fn get_continuation_token(events_json: &[Value]) -> Option<&str> {
|
fn get_continuation_token(events_json: &[Value]) -> Option<&str> {
|
||||||
// When the length of the vec equals the max page_size there probably is more data
|
// When the length of the vec equals the max page_size there probably is more data
|
||||||
// When it is less, then all events are loaded.
|
// When it is less, then all events are loaded.
|
||||||
if events_json.len() as i64 == Event::PAGE_SIZE {
|
#[expect(clippy::cast_possible_truncation, reason = "PAGE_SIZE fits within usize")]
|
||||||
|
if events_json.len() == Event::PAGE_SIZE as usize {
|
||||||
if let Some(last_event) = events_json.last() {
|
if let Some(last_event) = events_json.last() {
|
||||||
last_event["date"].as_str()
|
last_event["date"].as_str()
|
||||||
} else {
|
} else {
|
||||||
@@ -173,10 +162,11 @@ async fn post_events_collect(data: Json<Vec<EventCollection>>, headers: Headers,
|
|||||||
}
|
}
|
||||||
|
|
||||||
for event in data.iter() {
|
for event in data.iter() {
|
||||||
let event_date = parse_date(&event.date);
|
let event_date = parse_date(&event.date).map_res("Invalid event date")?;
|
||||||
|
// Only the event types upstream accepts from clients, the rest are written server-side.
|
||||||
match event.r#type {
|
match event.r#type {
|
||||||
1000..=1099 => {
|
t if t == EventType::UserClientExportedVault as i32 => {
|
||||||
_log_user_event(
|
log_user_event_impl(
|
||||||
event.r#type,
|
event.r#type,
|
||||||
&headers.user.uuid,
|
&headers.user.uuid,
|
||||||
headers.device.atype,
|
headers.device.atype,
|
||||||
@@ -186,9 +176,12 @@ async fn post_events_collect(data: Json<Vec<EventCollection>>, headers: Headers,
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
1600..=1699 => {
|
t if t == EventType::OrganizationClientExportedVault as i32 => {
|
||||||
if let Some(org_id) = &event.organization_id {
|
// Only allow logging events for an organization the user is actually a member of.
|
||||||
_log_event(
|
if let Some(org_id) = &event.organization_id
|
||||||
|
&& Membership::find_confirmed_by_user_and_org(&headers.user.uuid, org_id, &conn).await.is_some()
|
||||||
|
{
|
||||||
|
log_event_impl(
|
||||||
event.r#type,
|
event.r#type,
|
||||||
org_id,
|
org_id,
|
||||||
org_id,
|
org_id,
|
||||||
@@ -201,38 +194,62 @@ async fn post_events_collect(data: Json<Vec<EventCollection>>, headers: Headers,
|
|||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
// Only the vault notification banner click is accepted from clients. The rest of
|
||||||
if let Some(cipher_uuid) = &event.cipher_id {
|
// the 1500..=1599 range is written server-side and must not be forgeable by a client.
|
||||||
if let Some(cipher) = Cipher::find_by_uuid(cipher_uuid, &conn).await {
|
t if t == EventType::OrganizationUserNotificationBannerActionClicked as i32 => {
|
||||||
if let Some(org_id) = cipher.organization_uuid {
|
if let Some(org_id) = &event.organization_id
|
||||||
_log_event(
|
&& let Some(membership) =
|
||||||
event.r#type,
|
Membership::find_confirmed_by_user_and_org(&headers.user.uuid, org_id, &conn).await
|
||||||
cipher_uuid,
|
{
|
||||||
&org_id,
|
log_event_impl(
|
||||||
&headers.user.uuid,
|
event.r#type,
|
||||||
headers.device.atype,
|
&membership.uuid,
|
||||||
Some(event_date),
|
org_id,
|
||||||
&headers.ip.ip,
|
&headers.user.uuid,
|
||||||
&conn,
|
headers.device.atype,
|
||||||
)
|
Some(event_date),
|
||||||
.await;
|
&headers.ip.ip,
|
||||||
}
|
&conn,
|
||||||
}
|
)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Client-side cipher events (viewed, copied, toggled, autofilled).
|
||||||
|
1107..=1114 | 1117 | 1119..=1132 => {
|
||||||
|
// The cipher determines the organization the event is logged to, so make sure the
|
||||||
|
// user can actually access it instead of trusting the provided cipher uuid.
|
||||||
|
if let Some(cipher_uuid) = &event.cipher_id
|
||||||
|
&& let Some(cipher) = Cipher::find_by_uuid(cipher_uuid, &conn).await
|
||||||
|
&& cipher.is_accessible_to_user(&headers.user.uuid, &conn).await
|
||||||
|
&& let Some(org_id) = cipher.organization_uuid
|
||||||
|
{
|
||||||
|
log_event_impl(
|
||||||
|
event.r#type,
|
||||||
|
cipher_uuid,
|
||||||
|
&org_id,
|
||||||
|
&headers.user.uuid,
|
||||||
|
headers.device.atype,
|
||||||
|
Some(event_date),
|
||||||
|
&headers.ip.ip,
|
||||||
|
&conn,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn log_user_event(event_type: i32, user_id: &UserId, device_type: i32, ip: &IpAddr, conn: &DbConn) {
|
pub async fn log_user_event(event_type: EventType, user_id: &UserId, device_type: i32, ip: &IpAddr, conn: &DbConn) {
|
||||||
if !CONFIG.org_events_enabled() {
|
if !CONFIG.org_events_enabled() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_log_user_event(event_type, user_id, device_type, None, ip, conn).await;
|
log_user_event_impl(event_type as i32, user_id, device_type, None, ip, conn).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn _log_user_event(
|
async fn log_user_event_impl(
|
||||||
event_type: i32,
|
event_type: i32,
|
||||||
user_id: &UserId,
|
user_id: &UserId,
|
||||||
device_type: i32,
|
device_type: i32,
|
||||||
@@ -240,7 +257,7 @@ async fn _log_user_event(
|
|||||||
ip: &IpAddr,
|
ip: &IpAddr,
|
||||||
conn: &DbConn,
|
conn: &DbConn,
|
||||||
) {
|
) {
|
||||||
let memberships = Membership::find_by_user(user_id, conn).await;
|
let memberships = Membership::find_confirmed_by_user(user_id, conn).await;
|
||||||
let mut events: Vec<Event> = Vec::with_capacity(memberships.len() + 1); // We need an event per org and one without an org
|
let mut events: Vec<Event> = Vec::with_capacity(memberships.len() + 1); // We need an event per org and one without an org
|
||||||
|
|
||||||
// Upstream saves the event also without any org_id.
|
// Upstream saves the event also without any org_id.
|
||||||
@@ -267,7 +284,7 @@ async fn _log_user_event(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn log_event(
|
pub async fn log_event(
|
||||||
event_type: i32,
|
event_type: EventType,
|
||||||
source_uuid: &str,
|
source_uuid: &str,
|
||||||
org_id: &OrganizationId,
|
org_id: &OrganizationId,
|
||||||
act_user_id: &UserId,
|
act_user_id: &UserId,
|
||||||
@@ -278,11 +295,11 @@ pub async fn log_event(
|
|||||||
if !CONFIG.org_events_enabled() {
|
if !CONFIG.org_events_enabled() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_log_event(event_type, source_uuid, org_id, act_user_id, device_type, None, ip, conn).await;
|
log_event_impl(event_type as i32, source_uuid, org_id, act_user_id, device_type, None, ip, conn).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[expect(clippy::too_many_arguments)]
|
||||||
async fn _log_event(
|
async fn log_event_impl(
|
||||||
event_type: i32,
|
event_type: i32,
|
||||||
source_uuid: &str,
|
source_uuid: &str,
|
||||||
org_id: &OrganizationId,
|
org_id: &OrganizationId,
|
||||||
@@ -298,24 +315,24 @@ async fn _log_event(
|
|||||||
// 1000..=1099 Are user events, they need to be logged via log_user_event()
|
// 1000..=1099 Are user events, they need to be logged via log_user_event()
|
||||||
// Cipher Events
|
// Cipher Events
|
||||||
1100..=1199 => {
|
1100..=1199 => {
|
||||||
event.cipher_uuid = Some(source_uuid.to_string().into());
|
event.cipher_uuid = Some(source_uuid.to_owned().into());
|
||||||
}
|
}
|
||||||
// Collection Events
|
// Collection Events
|
||||||
1300..=1399 => {
|
1300..=1399 => {
|
||||||
event.collection_uuid = Some(source_uuid.to_string().into());
|
event.collection_uuid = Some(source_uuid.to_owned().into());
|
||||||
}
|
}
|
||||||
// Group Events
|
// Group Events
|
||||||
1400..=1499 => {
|
1400..=1499 => {
|
||||||
event.group_uuid = Some(source_uuid.to_string().into());
|
event.group_uuid = Some(source_uuid.to_owned().into());
|
||||||
}
|
}
|
||||||
// Org User Events
|
// Org User Events
|
||||||
1500..=1599 => {
|
1500..=1599 => {
|
||||||
event.org_user_uuid = Some(source_uuid.to_string().into());
|
event.org_user_uuid = Some(source_uuid.to_owned().into());
|
||||||
}
|
}
|
||||||
// 1600..=1699 Are organizational events, and they do not need the source_uuid
|
// 1600..=1699 Are organizational events, and they do not need the source_uuid
|
||||||
// Policy Events
|
// Policy Events
|
||||||
1700..=1799 => {
|
1700..=1799 => {
|
||||||
event.policy_uuid = Some(source_uuid.to_string().into());
|
event.policy_uuid = Some(source_uuid.to_owned().into());
|
||||||
}
|
}
|
||||||
// Ignore others
|
// Ignore others
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -338,6 +355,6 @@ pub async fn event_cleanup_job(pool: DbPool) {
|
|||||||
if let Ok(conn) = pool.get().await {
|
if let Ok(conn) = pool.get().await {
|
||||||
Event::clean_events(&conn).await.ok();
|
Event::clean_events(&conn).await.ok();
|
||||||
} else {
|
} else {
|
||||||
error!("Failed to get DB connection while trying to cleanup the events table")
|
error!("Failed to get DB connection while trying to cleanup the events table");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,9 +5,10 @@ use crate::{
|
|||||||
api::{EmptyResult, JsonResult, Notify, UpdateType},
|
api::{EmptyResult, JsonResult, Notify, UpdateType},
|
||||||
auth::Headers,
|
auth::Headers,
|
||||||
db::{
|
db::{
|
||||||
models::{Folder, FolderId},
|
|
||||||
DbConn,
|
DbConn,
|
||||||
|
models::{Folder, FolderId},
|
||||||
},
|
},
|
||||||
|
util::deser_opt_nonempty_str,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn routes() -> Vec<rocket::Route> {
|
pub fn routes() -> Vec<rocket::Route> {
|
||||||
@@ -28,9 +29,10 @@ async fn get_folders(headers: Headers, conn: DbConn) -> Json<Value> {
|
|||||||
|
|
||||||
#[get("/folders/<folder_id>")]
|
#[get("/folders/<folder_id>")]
|
||||||
async fn get_folder(folder_id: FolderId, headers: Headers, conn: DbConn) -> JsonResult {
|
async fn get_folder(folder_id: FolderId, headers: Headers, conn: DbConn) -> JsonResult {
|
||||||
match Folder::find_by_uuid_and_user(&folder_id, &headers.user.uuid, &conn).await {
|
if let Some(folder) = Folder::find_by_uuid_and_user(&folder_id, &headers.user.uuid, &conn).await {
|
||||||
Some(folder) => Ok(Json(folder.to_json())),
|
Ok(Json(folder.to_json()))
|
||||||
_ => err!("Invalid folder", "Folder does not exist or belongs to another user"),
|
} else {
|
||||||
|
err!("Invalid folder", "Folder does not exist or belongs to another user")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,6 +40,7 @@ async fn get_folder(folder_id: FolderId, headers: Headers, conn: DbConn) -> Json
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct FolderData {
|
pub struct FolderData {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
#[serde(default, deserialize_with = "deser_opt_nonempty_str")]
|
||||||
pub id: Option<FolderId>,
|
pub id: Option<FolderId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+68
-52
@@ -1,4 +1,6 @@
|
|||||||
pub mod accounts;
|
pub mod accounts;
|
||||||
|
pub mod two_factor;
|
||||||
|
|
||||||
mod ciphers;
|
mod ciphers;
|
||||||
mod emergency_access;
|
mod emergency_access;
|
||||||
mod events;
|
mod events;
|
||||||
@@ -6,17 +8,32 @@ mod folders;
|
|||||||
mod organizations;
|
mod organizations;
|
||||||
mod public;
|
mod public;
|
||||||
mod sends;
|
mod sends;
|
||||||
pub mod two_factor;
|
|
||||||
|
|
||||||
pub use accounts::purge_auth_requests;
|
pub use accounts::purge_auth_requests;
|
||||||
pub use ciphers::{purge_trashed_ciphers, CipherData, CipherSyncData, CipherSyncType};
|
pub use ciphers::{CipherData, CipherSyncData, CipherSyncType, purge_trashed_ciphers};
|
||||||
pub use emergency_access::{emergency_notification_reminder_job, emergency_request_timeout_job};
|
pub use emergency_access::{emergency_notification_reminder_job, emergency_request_timeout_job};
|
||||||
pub use events::{event_cleanup_job, log_event, log_user_event};
|
pub use events::{event_cleanup_job, log_event, log_user_event};
|
||||||
use reqwest::Method;
|
|
||||||
pub use sends::purge_sends;
|
pub use sends::purge_sends;
|
||||||
|
|
||||||
|
use reqwest::Method;
|
||||||
|
use rocket::{Catcher, Route, serde::json::Json, serde::json::Value};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
CONFIG,
|
||||||
|
api::{EmptyResult, JsonResult, Notify, UpdateType},
|
||||||
|
auth::Headers,
|
||||||
|
db::{
|
||||||
|
DbConn,
|
||||||
|
models::{Membership, MembershipStatus, OrgPolicy, Organization, User},
|
||||||
|
},
|
||||||
|
error::Error,
|
||||||
|
http_client::make_http_request,
|
||||||
|
mail,
|
||||||
|
util::{FeatureFlagFilter, parse_experimental_client_feature_flags},
|
||||||
|
};
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
let mut eq_domains_routes = routes![get_eq_domains, post_eq_domains, put_eq_domains];
|
let mut eq_domains_routes = routes![get_settings_domains, post_settings_domains, put_settings_domains];
|
||||||
let mut hibp_routes = routes![hibp_breach];
|
let mut hibp_routes = routes![hibp_breach];
|
||||||
let mut meta_routes = routes![alive, now, version, config, get_api_webauthn];
|
let mut meta_routes = routes![alive, now, version, config, get_api_webauthn];
|
||||||
|
|
||||||
@@ -44,24 +61,6 @@ pub fn events_routes() -> Vec<Route> {
|
|||||||
routes
|
routes
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
|
||||||
// Move this somewhere else
|
|
||||||
//
|
|
||||||
use rocket::{serde::json::Json, serde::json::Value, Catcher, Route};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
api::{EmptyResult, JsonResult, Notify, UpdateType},
|
|
||||||
auth::Headers,
|
|
||||||
db::{
|
|
||||||
models::{Membership, MembershipStatus, OrgPolicy, Organization, User},
|
|
||||||
DbConn,
|
|
||||||
},
|
|
||||||
error::Error,
|
|
||||||
http_client::make_http_request,
|
|
||||||
mail,
|
|
||||||
util::parse_experimental_client_feature_flags,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct GlobalDomain {
|
struct GlobalDomain {
|
||||||
@@ -72,15 +71,17 @@ struct GlobalDomain {
|
|||||||
|
|
||||||
const GLOBAL_DOMAINS: &str = include_str!("../../static/global_domains.json");
|
const GLOBAL_DOMAINS: &str = include_str!("../../static/global_domains.json");
|
||||||
|
|
||||||
|
#[expect(clippy::needless_pass_by_value, reason = "Not beneficial for Headers")]
|
||||||
#[get("/settings/domains")]
|
#[get("/settings/domains")]
|
||||||
fn get_eq_domains(headers: Headers) -> Json<Value> {
|
fn get_settings_domains(headers: Headers) -> Json<Value> {
|
||||||
_get_eq_domains(&headers, false)
|
get_eq_domains(&headers, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _get_eq_domains(headers: &Headers, no_excluded: bool) -> Json<Value> {
|
fn get_eq_domains(headers: &Headers, no_excluded: bool) -> Json<Value> {
|
||||||
let user = &headers.user;
|
|
||||||
use serde_json::from_str;
|
use serde_json::from_str;
|
||||||
|
|
||||||
|
let user = &headers.user;
|
||||||
|
|
||||||
let equivalent_domains: Vec<Vec<String>> = from_str(&user.equivalent_domains).unwrap();
|
let equivalent_domains: Vec<Vec<String>> = from_str(&user.equivalent_domains).unwrap();
|
||||||
let excluded_globals: Vec<i32> = from_str(&user.excluded_globals).unwrap();
|
let excluded_globals: Vec<i32> = from_str(&user.excluded_globals).unwrap();
|
||||||
|
|
||||||
@@ -109,34 +110,45 @@ struct EquivDomainData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/settings/domains", data = "<data>")]
|
#[post("/settings/domains", data = "<data>")]
|
||||||
async fn post_eq_domains(data: Json<EquivDomainData>, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult {
|
async fn post_settings_domains(
|
||||||
|
data: Json<EquivDomainData>,
|
||||||
|
headers: Headers,
|
||||||
|
conn: DbConn,
|
||||||
|
nt: Notify<'_>,
|
||||||
|
) -> JsonResult {
|
||||||
|
use serde_json::to_string;
|
||||||
|
|
||||||
let data: EquivDomainData = data.into_inner();
|
let data: EquivDomainData = data.into_inner();
|
||||||
|
|
||||||
let excluded_globals = data.excluded_global_equivalent_domains.unwrap_or_default();
|
let excluded_globals = data.excluded_global_equivalent_domains.unwrap_or_default();
|
||||||
let equivalent_domains = data.equivalent_domains.unwrap_or_default();
|
let equivalent_domains = data.equivalent_domains.unwrap_or_default();
|
||||||
|
|
||||||
let mut user = headers.user;
|
let mut user = headers.user;
|
||||||
use serde_json::to_string;
|
|
||||||
|
|
||||||
user.excluded_globals = to_string(&excluded_globals).unwrap_or_else(|_| "[]".to_string());
|
user.excluded_globals = to_string(&excluded_globals).unwrap_or_else(|_| "[]".to_owned());
|
||||||
user.equivalent_domains = to_string(&equivalent_domains).unwrap_or_else(|_| "[]".to_string());
|
user.equivalent_domains = to_string(&equivalent_domains).unwrap_or_else(|_| "[]".to_owned());
|
||||||
|
|
||||||
user.save(&conn).await?;
|
user.save(&conn).await?;
|
||||||
|
|
||||||
nt.send_user_update(UpdateType::SyncSettings, &user, &headers.device.push_uuid, &conn).await;
|
nt.send_user_update(UpdateType::SyncSettings, &user, headers.device.push_uuid.as_ref(), &conn).await;
|
||||||
|
|
||||||
Ok(Json(json!({})))
|
Ok(Json(json!({})))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[put("/settings/domains", data = "<data>")]
|
#[put("/settings/domains", data = "<data>")]
|
||||||
async fn put_eq_domains(data: Json<EquivDomainData>, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult {
|
async fn put_settings_domains(
|
||||||
post_eq_domains(data, headers, conn, nt).await
|
data: Json<EquivDomainData>,
|
||||||
|
headers: Headers,
|
||||||
|
conn: DbConn,
|
||||||
|
nt: Notify<'_>,
|
||||||
|
) -> JsonResult {
|
||||||
|
post_settings_domains(data, headers, conn, nt).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/hibp/breach?<username>")]
|
#[get("/hibp/breach?<username>")]
|
||||||
async fn hibp_breach(username: &str, _headers: Headers) -> JsonResult {
|
async fn hibp_breach(username: &str, _headers: Headers) -> JsonResult {
|
||||||
let username: String = url::form_urlencoded::byte_serialize(username.as_bytes()).collect();
|
let username: String = url::form_urlencoded::byte_serialize(username.as_bytes()).collect();
|
||||||
if let Some(api_key) = crate::CONFIG.hibp_api_key() {
|
if let Some(api_key) = CONFIG.hibp_api_key() {
|
||||||
let url = format!(
|
let url = format!(
|
||||||
"https://haveibeenpwned.com/api/v3/breachedaccount/{username}?truncateResponse=false&includeUnverified=false"
|
"https://haveibeenpwned.com/api/v3/breachedaccount/{username}?truncateResponse=false&includeUnverified=false"
|
||||||
);
|
);
|
||||||
@@ -197,19 +209,17 @@ 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 = CONFIG.domain();
|
||||||
// Official available feature flags can be found here:
|
// Official available feature flags can be found here:
|
||||||
// Server (v2025.6.2): https://github.com/bitwarden/server/blob/d094be3267f2030bd0dc62106bc6871cf82682f5/src/Core/Constants.cs#L103
|
// Server (v2026.2.1): https://github.com/bitwarden/server/blob/0e42725d0837bd1c0dabd864ff621a579959744b/src/Core/Constants.cs#L135
|
||||||
// Client (web-v2025.6.1): https://github.com/bitwarden/clients/blob/747c2fd6a1c348a57a76e4a7de8128466ffd3c01/libs/common/src/enums/feature-flag.enum.ts#L12
|
// Client (v2026.2.1): https://github.com/bitwarden/clients/blob/f96380c3138291a028bdd2c7a5fee540d5c98ba5/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
|
// Android (v2026.2.1): https://github.com/bitwarden/android/blob/6902c19c0093fa476bbf74ccaa70c9f14afbb82f/core/src/main/kotlin/com/bitwarden/core/data/manager/model/FlagKey.kt#L31
|
||||||
// iOS (v2025.6.0): https://github.com/bitwarden/ios/blob/ff06d9c6cc8da89f78f37f376495800201d7261a/BitwardenShared/Core/Platform/Models/Enum/FeatureFlag.swift#L7
|
// iOS (v2026.2.1): https://github.com/bitwarden/ios/blob/cdd9ba1770ca2ffc098d02d12cc3208e3a830454/BitwardenShared/Core/Platform/Models/Enum/FeatureFlag.swift#L7
|
||||||
let mut feature_states =
|
let mut feature_states = parse_experimental_client_feature_flags(
|
||||||
parse_experimental_client_feature_flags(&crate::CONFIG.experimental_client_feature_flags());
|
&CONFIG.experimental_client_feature_flags(),
|
||||||
feature_states.insert("duo-redirect".to_string(), true);
|
&FeatureFlagFilter::ValidOnly,
|
||||||
feature_states.insert("email-verification".to_string(), true);
|
);
|
||||||
feature_states.insert("unauth-ui-refresh".to_string(), true);
|
feature_states.insert("pm-19148-innovation-archive".to_owned(), 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
|
||||||
@@ -218,14 +228,17 @@ fn config() -> Json<Value> {
|
|||||||
// Version history:
|
// Version history:
|
||||||
// - Individual cipher key encryption: 2024.2.0
|
// - Individual cipher key encryption: 2024.2.0
|
||||||
// - Mobile app support for MasterPasswordUnlockData: 2025.8.0
|
// - Mobile app support for MasterPasswordUnlockData: 2025.8.0
|
||||||
"version": "2025.12.0",
|
"version": "2026.6.0",
|
||||||
"gitHash": option_env!("GIT_REV"),
|
"gitHash": option_env!("GIT_REV"),
|
||||||
"server": {
|
"server": {
|
||||||
"name": "Vaultwarden",
|
"name": "Vaultwarden",
|
||||||
"url": "https://github.com/dani-garcia/vaultwarden"
|
"url": "https://github.com/dani-garcia/vaultwarden"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"disableUserRegistration": crate::CONFIG.is_signup_disabled()
|
"disableUserRegistration": CONFIG.is_signup_disabled(),
|
||||||
|
// When enabled, this setting signals to clients that onboarding interstitials
|
||||||
|
// (post-login welcome dialogs, extension install prompts, setup extension redirects, and premium upsell modals) should be suppressed
|
||||||
|
"suppressOnboardingInterstitials": CONFIG.client_suppress_onboarding(),
|
||||||
},
|
},
|
||||||
"environment": {
|
"environment": {
|
||||||
"vault": domain,
|
"vault": domain,
|
||||||
@@ -241,6 +254,10 @@ fn config() -> Json<Value> {
|
|||||||
"vapidPublicKey": null
|
"vapidPublicKey": null
|
||||||
},
|
},
|
||||||
"featureStates": feature_states,
|
"featureStates": feature_states,
|
||||||
|
// Not supported right now
|
||||||
|
// Used for by clients to learn if the server requires extra work to establish a connection.
|
||||||
|
// See: https://github.com/bitwarden/server/pull/6892 | https://github.com/bitwarden/server/commit/52955d1860b4dfb905f67bbe39d9b10bbd61ded0
|
||||||
|
"communication": null,
|
||||||
"object": "config",
|
"object": "config",
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@@ -278,10 +295,9 @@ async fn accept_org_invite(
|
|||||||
|
|
||||||
member.save(conn).await?;
|
member.save(conn).await?;
|
||||||
|
|
||||||
if crate::CONFIG.mail_enabled() {
|
if CONFIG.mail_enabled() {
|
||||||
let org = match Organization::find_by_uuid(&member.org_uuid, conn).await {
|
let Some(org) = Organization::find_by_uuid(&member.org_uuid, conn).await else {
|
||||||
Some(org) => org,
|
err!("Organization not found.")
|
||||||
None => err!("Organization not found."),
|
|
||||||
};
|
};
|
||||||
// User was invited to an organization, so they must be confirmed manually after acceptance
|
// User was invited to an organization, so they must be confirmed manually after acceptance
|
||||||
mail::send_invite_accepted(&user.email, &member.invited_by_email.unwrap_or(org.billing_email), &org.name)
|
mail::send_invite_accepted(&user.email, &member.invited_by_email.unwrap_or(org.billing_email), &org.name)
|
||||||
|
|||||||
+546
-867
File diff suppressed because it is too large.
Load diff
Loaded 100 of 178 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user