mirror of
https://github.com/dani-garcia/vaultwarden.wiki.git
synced 2026-10-23 02:08:10 +03:00
Compare commits
167
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58a980f850 | ||
|
|
64895524e4 | ||
|
|
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 | ||
|
|
4352fffeec | ||
|
|
8d08697cf8 | ||
|
|
9f1df42259 | ||
|
|
1e1f9957cd | ||
|
|
bf37657c08 | ||
|
|
3e2cef7e8b | ||
|
|
2af9d21158 | ||
|
|
c4f6c4e63b |
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: |
|
||||||
@@ -313,45 +320,43 @@ jobs:
|
|||||||
# Determine Base Tags
|
# Determine Base Tags
|
||||||
- name: Determine Base Tags
|
- name: Determine Base Tags
|
||||||
env:
|
env:
|
||||||
|
BASE_IMAGE_TAG: "${{ matrix.base_image != 'debian' && format('-{0}', matrix.base_image) || '' }}"
|
||||||
REF_TYPE: ${{ github.ref_type }}
|
REF_TYPE: ${{ github.ref_type }}
|
||||||
run: |
|
run: |
|
||||||
# Check which main tag we are going to build determined by ref_type
|
# Check which main tag we are going to build determined by ref_type
|
||||||
if [[ "${REF_TYPE}" == "tag" ]]; then
|
if [[ "${REF_TYPE}" == "tag" ]]; then
|
||||||
echo "BASE_TAGS=latest,${GITHUB_REF#refs/*/}" | tee -a "${GITHUB_ENV}"
|
echo "BASE_TAGS=latest${BASE_IMAGE_TAG},${GITHUB_REF#refs/*/}${BASE_IMAGE_TAG}${BASE_IMAGE_TAG//-/,}" | tee -a "${GITHUB_ENV}"
|
||||||
elif [[ "${REF_TYPE}" == "branch" ]]; then
|
elif [[ "${REF_TYPE}" == "branch" ]]; then
|
||||||
echo "BASE_TAGS=testing" | tee -a "${GITHUB_ENV}"
|
echo "BASE_TAGS=testing${BASE_IMAGE_TAG}" | tee -a "${GITHUB_ENV}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Create manifest list, push it and extract digest SHA
|
- name: Create manifest list, push it and extract digest SHA
|
||||||
working-directory: ${{ runner.temp }}/digests
|
working-directory: ${{ runner.temp }}/digests
|
||||||
env:
|
env:
|
||||||
BASE_IMAGE_TAG: "${{ matrix.base_image != 'debian' && format('-{0}', matrix.base_image) || '' }}"
|
|
||||||
BASE_TAGS: "${{ env.BASE_TAGS }}"
|
BASE_TAGS: "${{ env.BASE_TAGS }}"
|
||||||
CONTAINER_REGISTRIES: "${{ env.CONTAINER_REGISTRIES }}"
|
CONTAINER_REGISTRIES: "${{ env.CONTAINER_REGISTRIES }}"
|
||||||
run: |
|
run: |
|
||||||
set +e
|
|
||||||
IFS=',' read -ra IMAGES <<< "${CONTAINER_REGISTRIES}"
|
IFS=',' read -ra IMAGES <<< "${CONTAINER_REGISTRIES}"
|
||||||
IFS=',' read -ra TAGS <<< "${BASE_TAGS}"
|
IFS=',' read -ra TAGS <<< "${BASE_TAGS}"
|
||||||
|
|
||||||
|
TAG_ARGS=()
|
||||||
for img in "${IMAGES[@]}"; do
|
for img in "${IMAGES[@]}"; do
|
||||||
for tag in "${TAGS[@]}"; do
|
for tag in "${TAGS[@]}"; do
|
||||||
echo "Creating manifest for ${img}:${tag}${BASE_IMAGE_TAG}"
|
TAG_ARGS+=("-t" "${img}:${tag}")
|
||||||
|
|
||||||
OUTPUT=$(docker buildx imagetools create \
|
|
||||||
-t "${img}:${tag}${BASE_IMAGE_TAG}" \
|
|
||||||
$(printf "${img}@sha256:%s " *) 2>&1)
|
|
||||||
STATUS=$?
|
|
||||||
|
|
||||||
if [ ${STATUS} -ne 0 ]; then
|
|
||||||
echo "Manifest creation failed for ${img}:${tag}${BASE_IMAGE_TAG}"
|
|
||||||
echo "${OUTPUT}"
|
|
||||||
exit ${STATUS}
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Manifest created for ${img}:${tag}${BASE_IMAGE_TAG}"
|
|
||||||
echo "${OUTPUT}"
|
|
||||||
done
|
done
|
||||||
done
|
done
|
||||||
set -e
|
|
||||||
|
echo "Creating manifest"
|
||||||
|
if ! OUTPUT=$(docker buildx imagetools create \
|
||||||
|
"${TAG_ARGS[@]}" \
|
||||||
|
$(printf "${IMAGES[0]}@sha256:%s " *) 2>&1); then
|
||||||
|
echo "Manifest creation failed"
|
||||||
|
echo "${OUTPUT}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Manifest created successfully"
|
||||||
|
echo "${OUTPUT}"
|
||||||
|
|
||||||
# Extract digest SHA for subsequent steps
|
# Extract digest SHA for subsequent steps
|
||||||
GET_DIGEST_SHA="$(echo "${OUTPUT}" | grep -oE 'sha256:[a-f0-9]{64}' | tail -1)"
|
GET_DIGEST_SHA="$(echo "${OUTPUT}" | grep -oE 'sha256:[a-f0-9]{64}' | tail -1)"
|
||||||
@@ -359,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@2d0ce569feab1f8752f1dde43cc2f2aa53236e06 # v1.40.0
|
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: 2d0ce569feab1f8752f1dde43cc2f2aa53236e06 # v1.40.0
|
- "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
+1494
-1412
File diff suppressed because it is too large.
Load diff
+165
-97
@@ -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.9"
|
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.0" # 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.145"
|
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.0"
|
vault_version: "v2026.8.0"
|
||||||
vault_image_digest: "sha256:bb7303efafdb7e2b41bee2c772e14f67676ae2c9047bd7bba80d3544d4162613"
|
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.0
|
# $ docker pull docker.io/vaultwarden/web-vault:v2026.8.0
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.12.0
|
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2026.8.0
|
||||||
# [docker.io/vaultwarden/web-vault@sha256:bb7303efafdb7e2b41bee2c772e14f67676ae2c9047bd7bba80d3544d4162613]
|
# [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:bb7303efafdb7e2b41bee2c772e14f67676ae2c9047bd7bba80d3544d4162613
|
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:632375471c7c06a799335cd74dfeaef63f92f45f9cd322c337ae7ad70081e48a
|
||||||
# [docker.io/vaultwarden/web-vault:v2025.12.0]
|
# [docker.io/vaultwarden/web-vault:v2026.8.0]
|
||||||
#
|
#
|
||||||
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:bb7303efafdb7e2b41bee2c772e14f67676ae2c9047bd7bba80d3544d4162613 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.0
|
# $ docker pull docker.io/vaultwarden/web-vault:v2026.8.0
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.12.0
|
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2026.8.0
|
||||||
# [docker.io/vaultwarden/web-vault@sha256:bb7303efafdb7e2b41bee2c772e14f67676ae2c9047bd7bba80d3544d4162613]
|
# [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:bb7303efafdb7e2b41bee2c772e14f67676ae2c9047bd7bba80d3544d4162613
|
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:632375471c7c06a799335cd74dfeaef63f92f45f9cd322c337ae7ad70081e48a
|
||||||
# [docker.io/vaultwarden/web-vault:v2025.12.0]
|
# [docker.io/vaultwarden/web-vault:v2026.8.0]
|
||||||
#
|
#
|
||||||
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:bb7303efafdb7e2b41bee2c772e14f67676ae2c9047bd7bba80d3544d4162613 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" \
|
||||||
|
|||||||
+43
-42
@@ -19,14 +19,32 @@
|
|||||||
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
||||||
# click the tag name to view the digest of the image it currently points to.
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
# - From the command line:
|
# - From the command line:
|
||||||
# $ docker pull docker.io/vaultwarden/web-vault:{{ vault_version }}
|
# $ docker pull docker.io/vaultwarden/web-vault:{{ vault_version | replace('+', '_') }}
|
||||||
# $ docker image inspect --format "{{ '{{' }}.RepoDigests}}" docker.io/vaultwarden/web-vault:{{ vault_version }}
|
# $ docker image inspect --format "{{ '{{' }}.RepoDigests}}" docker.io/vaultwarden/web-vault:{{ vault_version | replace('+', '_') }}
|
||||||
# [docker.io/vaultwarden/web-vault@{{ vault_image_digest }}]
|
# [docker.io/vaultwarden/web-vault@{{ vault_image_digest }}]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{ '{{' }}.RepoTags}}" docker.io/vaultwarden/web-vault@{{ vault_image_digest }}
|
# $ docker image inspect --format "{{ '{{' }}.RepoTags}}" docker.io/vaultwarden/web-vault@{{ vault_image_digest }}
|
||||||
# [docker.io/vaultwarden/web-vault:{{ vault_version }}]
|
# [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" \
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ Some unsupported platforms for Debian based images. These are not built and test
|
|||||||
- 386 (linux/386)
|
- 386 (linux/386)
|
||||||
- ppc64le (linux/ppc64le)
|
- ppc64le (linux/ppc64le)
|
||||||
- s390x (linux/s390x)
|
- s390x (linux/s390x)
|
||||||
|
- riscv64 (linux/riscv64)
|
||||||
|
|
||||||
To build these containers you need to enable QEMU binfmt support to be able to run/emulate architectures which are different then your host.<br>
|
To build these containers you need to enable QEMU binfmt support to be able to run/emulate architectures which are different then your host.<br>
|
||||||
This ensures the container build process can run binaries from other architectures.<br>
|
This ensures the container build process can run binaries from other architectures.<br>
|
||||||
|
|||||||
@@ -148,6 +148,12 @@ target "debian-s390x" {
|
|||||||
platforms = ["linux/s390x"]
|
platforms = ["linux/s390x"]
|
||||||
tags = generate_tags("", "-s390x")
|
tags = generate_tags("", "-s390x")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
target "debian-riscv64" {
|
||||||
|
inherits = ["debian"]
|
||||||
|
platforms = ["linux/riscv64"]
|
||||||
|
tags = generate_tags("", "-riscv64")
|
||||||
|
}
|
||||||
// ==== End of unsupported Debian architecture targets ===
|
// ==== End of unsupported Debian architecture targets ===
|
||||||
|
|
||||||
// A Group to build all platforms individually for local testing
|
// A Group to build all platforms individually for local testing
|
||||||
|
|||||||
@@ -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"
|
||||||
+181
-99
@@ -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_display_size, get_web_vault_version,
|
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,48 +657,96 @@ 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
String::from("Unable to fetch NTP time.")
|
String::from("Unable to fetch NTP time.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn web_vault_compare(active: &str, latest: &str) -> i8 {
|
||||||
|
use semver::Version;
|
||||||
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
|
let active_semver = Version::parse(active).unwrap_or_else(|e| {
|
||||||
|
warn!("Unable to parse active web-vault version '{active}': {e}");
|
||||||
|
Version::parse("2025.1.1").unwrap()
|
||||||
|
});
|
||||||
|
let latest_semver = Version::parse(latest).unwrap_or_else(|e| {
|
||||||
|
warn!("Unable to parse latest web-vault version '{latest}': {e}");
|
||||||
|
Version::parse("2025.1.1").unwrap()
|
||||||
|
});
|
||||||
|
|
||||||
|
match active_semver.cmp(&latest_semver) {
|
||||||
|
Ordering::Less => -1,
|
||||||
|
Ordering::Equal => 0,
|
||||||
|
Ordering::Greater => 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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::*;
|
||||||
@@ -705,35 +763,31 @@ 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_release, latest_commit, latest_web_build) = get_release_info(has_http_access).await;
|
let (latest_vw_release, latest_vw_commit, latest_web_release) = get_release_info(has_http_access).await;
|
||||||
|
let active_web_release = get_active_web_release();
|
||||||
|
let web_vault_compare = web_vault_compare(&active_web_release, &latest_web_release);
|
||||||
|
|
||||||
let ip_header_name = &ip_header.0.unwrap_or_default();
|
let ip_header_name = &ip_header.0.unwrap_or_default();
|
||||||
|
|
||||||
// Get current running versions
|
let invalid_feature_flags: Vec<String> = parse_experimental_client_feature_flags(
|
||||||
let web_vault_version = get_web_vault_version();
|
&CONFIG.experimental_client_feature_flags(),
|
||||||
|
&FeatureFlagFilter::InvalidOnly,
|
||||||
// Check if the running version is newer than the latest stable released version
|
)
|
||||||
let web_vault_pre_release = if let Ok(web_ver_match) = semver::VersionReq::parse(&format!(">{latest_web_build}")) {
|
.into_keys()
|
||||||
web_ver_match.matches(
|
.collect();
|
||||||
&semver::Version::parse(&web_vault_version).unwrap_or_else(|_| semver::Version::parse("2025.1.1").unwrap()),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
error!("Unable to parse latest_web_build: '{latest_web_build}'");
|
|
||||||
false
|
|
||||||
};
|
|
||||||
|
|
||||||
let diagnostics_json = json!({
|
let diagnostics_json = json!({
|
||||||
"dns_resolved": dns_resolved,
|
"dns_resolved": dns_resolved,
|
||||||
"current_release": VERSION,
|
"current_release": VERSION,
|
||||||
"latest_release": latest_release,
|
"latest_release": latest_vw_release,
|
||||||
"latest_commit": latest_commit,
|
"latest_commit": latest_vw_commit,
|
||||||
"web_vault_enabled": &CONFIG.web_vault_enabled(),
|
"web_vault_enabled": &CONFIG.web_vault_enabled(),
|
||||||
"web_vault_version": web_vault_version,
|
"active_web_release": active_web_release,
|
||||||
"latest_web_build": latest_web_build,
|
"latest_web_release": latest_web_release,
|
||||||
"web_vault_pre_release": web_vault_pre_release,
|
"web_vault_compare": web_vault_compare,
|
||||||
"running_within_container": running_within_container,
|
"running_within_container": running_within_container,
|
||||||
"container_base_image": if running_within_container { container_base_image() } else { "Not applicable" },
|
"container_base_image": if running_within_container { container_base_image() } else { "Not applicable" },
|
||||||
"has_http_access": has_http_access,
|
"has_http_access": has_http_access,
|
||||||
@@ -747,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(),
|
||||||
@@ -808,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"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -844,3 +897,32 @@ impl<'r> FromRequest<'r> for AdminToken {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validate_web_vault_compare() {
|
||||||
|
// web_vault_compare(active, latest)
|
||||||
|
// Test normal versions
|
||||||
|
assert_eq!(web_vault_compare("2025.12.0", "2025.12.1"), -1);
|
||||||
|
assert_eq!(web_vault_compare("2025.12.1", "2025.12.1"), 0);
|
||||||
|
assert_eq!(web_vault_compare("2025.12.2", "2025.12.1"), 1);
|
||||||
|
|
||||||
|
// Test patched/+build.n versions
|
||||||
|
// Newer latest version
|
||||||
|
assert_eq!(web_vault_compare("2025.12.0+build.1", "2025.12.1"), -1);
|
||||||
|
assert_eq!(web_vault_compare("2025.12.1", "2025.12.1+build.1"), -1);
|
||||||
|
assert_eq!(web_vault_compare("2025.12.0+build.1", "2025.12.1+build.1"), -1);
|
||||||
|
assert_eq!(web_vault_compare("2025.12.1+build.1", "2025.12.1+build.2"), -1);
|
||||||
|
// Equal versions
|
||||||
|
assert_eq!(web_vault_compare("2025.12.1+build.1", "2025.12.1+build.1"), 0);
|
||||||
|
assert_eq!(web_vault_compare("2025.12.2+build.2", "2025.12.2+build.2"), 0);
|
||||||
|
// Newer active version
|
||||||
|
assert_eq!(web_vault_compare("2025.12.1+build.1", "2025.12.1"), 1);
|
||||||
|
assert_eq!(web_vault_compare("2025.12.2", "2025.12.1+build.1"), 1);
|
||||||
|
assert_eq!(web_vault_compare("2025.12.2+build.1", "2025.12.1+build.1"), 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>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loaded 100 of 180 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user