mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2025-09-09 18:25:58 +03:00
Compare commits
155 Commits
1.32.3
...
5a8736e116
Author | SHA1 | Date | |
---|---|---|---|
|
5a8736e116 | ||
|
f76362ff89 | ||
|
6db5b7115d | ||
|
3510351f4d | ||
|
7161f612a1 | ||
|
5ee908517f | ||
|
55577fa4eb | ||
|
843c063649 | ||
|
550b670dba | ||
|
de808c5ad9 | ||
|
1f73630136 | ||
|
77008a91e9 | ||
|
7f386d38ae | ||
|
8e7eeab293 | ||
|
e35c6f8705 | ||
|
ae7b725c0f | ||
|
2a5489a4b2 | ||
|
8fd0ee4211 | ||
|
4a5516e150 | ||
|
7fc94516ce | ||
|
5ea0779d6b | ||
|
a133d4e90c | ||
|
49eff787de | ||
|
cff6c2b3af | ||
|
a0c76284fd | ||
|
318653b0e5 | ||
|
5d84f17600 | ||
|
0db4b00007 | ||
|
a0198d8d7c | ||
|
dfad931dca | ||
|
25865efd79 | ||
|
bcf627930e | ||
|
ce70cd2cf4 | ||
|
2ac589d4b4 | ||
|
b2e2aef7de | ||
|
0755bb19c0 | ||
|
fee0c1c711 | ||
|
f58539f0b4 | ||
|
e718afb441 | ||
|
55945ad793 | ||
|
4fd22d8e3b | ||
|
d6a8fb8e48 | ||
|
3b48e6e903 | ||
|
6b9333b33e | ||
|
a545636ee5 | ||
|
f125d5f1a1 | ||
|
ad75ce281e | ||
|
9059437c35 | ||
|
c84db0daca | ||
|
72adc239f5 | ||
|
34ebeeca76 | ||
|
0469d9ba4c | ||
|
eaa6ad06ed | ||
|
0d3f283c37 | ||
|
51a1d641c5 | ||
|
90f7e5ff80 | ||
|
200999c94e | ||
|
d363e647e9 | ||
|
53f58b14d5 | ||
|
ef7835d1b0 | ||
|
3a44dc963b | ||
|
a039e227c7 | ||
|
602b18fdd6 | ||
|
bf04c64759 | ||
|
2f1d86b7f1 | ||
|
ff97bcfdda | ||
|
73f2441d1a | ||
|
ad8484a2d5 | ||
|
9813e480c0 | ||
|
bfe172702a | ||
|
df42b6d6b0 | ||
|
2697fe8aba | ||
|
674e444d67 | ||
|
0d16da440d | ||
|
66cf179bca | ||
|
025bb90f8f | ||
|
d5039d9c17 | ||
|
e7c796a660 | ||
|
bbbd2f6d15 | ||
|
a2d7895586 | ||
|
8a0cb1137e | ||
|
f960bf59bb | ||
|
3a1f1bae00 | ||
|
8dfe805954 | ||
|
07b869b3ef | ||
|
2a18665288 | ||
|
71952a4ab5 | ||
|
994d157064 | ||
|
1dae6093c9 | ||
|
6edceb5f7a | ||
|
359a4a088a | ||
|
3baffeee9a | ||
|
d5c353427d | ||
|
1f868b8d22 | ||
|
8d1df08b81 | ||
|
3b6bccde97 | ||
|
d2b36642a6 | ||
|
a02fb0fd24 | ||
|
1109293992 | ||
|
3c29f82974 | ||
|
663f88e717 | ||
|
a3dccee243 | ||
|
c0ebe0d982 | ||
|
1b46c80389 | ||
|
2c549984c0 | ||
|
ecab7a50ea | ||
|
2903a3a13a | ||
|
952992c85b | ||
|
c0be36a17f | ||
|
d1dee04615 | ||
|
ef2695de0c | ||
|
29f2b433f0 | ||
|
07f80346b4 | ||
|
4f68eafa3e | ||
|
327d369188 | ||
|
ca7483df85 | ||
|
16b6d2a71e | ||
|
871a3f214a | ||
|
10d12676cf | ||
|
dec3a9603a | ||
|
86aaf27659 | ||
|
bc913d1156 | ||
|
ef4bff09eb | ||
|
4816f77fd7 | ||
|
dfd9e65396 | ||
|
b1481c7c1a | ||
|
d9e0d68f20 | ||
|
08183fc999 | ||
|
d9b043d32c | ||
|
ed4ad67e73 | ||
|
a523c82f5f | ||
|
4d6d3443ae | ||
|
9cd400db6c | ||
|
fd51230044 | ||
|
45e5f06b86 | ||
|
620ad92331 | ||
|
c9860af11c | ||
|
d7adce97df | ||
|
71b3d3c818 | ||
|
da3701c0cf | ||
|
96813b1317 | ||
|
b0b953f348 | ||
|
cdfdc6ff4f | ||
|
2393c3f3c0 | ||
|
0d16b38a68 | ||
|
ff33534c07 | ||
|
adb21d5c1a | ||
|
e927b8aa5e | ||
|
ba48ca68fc | ||
|
294b429436 | ||
|
37c14c3c69 | ||
|
d0581da638 | ||
|
38aad4f7be | ||
|
20d9e885bf | ||
|
2f20ad86f9 |
@@ -5,6 +5,7 @@
|
|||||||
!.git
|
!.git
|
||||||
!docker/healthcheck.sh
|
!docker/healthcheck.sh
|
||||||
!docker/start.sh
|
!docker/start.sh
|
||||||
|
!macros
|
||||||
!migrations
|
!migrations
|
||||||
!src
|
!src
|
||||||
|
|
||||||
|
130
.env.template
130
.env.template
@@ -15,6 +15,14 @@
|
|||||||
####################
|
####################
|
||||||
|
|
||||||
## Main data folder
|
## Main data folder
|
||||||
|
## This can be a path to local folder or a path to an external location
|
||||||
|
## depending on features enabled at build time. Possible external locations:
|
||||||
|
##
|
||||||
|
## - AWS S3 Bucket (via `s3` feature): s3://bucket-name/path/to/folder
|
||||||
|
##
|
||||||
|
## When using an external location, make sure to set TMP_FOLDER,
|
||||||
|
## TEMPLATES_FOLDER, and DATABASE_URL to local paths and/or a remote database
|
||||||
|
## location.
|
||||||
# DATA_FOLDER=data
|
# DATA_FOLDER=data
|
||||||
|
|
||||||
## Individual folders, these override %DATA_FOLDER%
|
## Individual folders, these override %DATA_FOLDER%
|
||||||
@@ -22,10 +30,13 @@
|
|||||||
# ICON_CACHE_FOLDER=data/icon_cache
|
# ICON_CACHE_FOLDER=data/icon_cache
|
||||||
# ATTACHMENTS_FOLDER=data/attachments
|
# ATTACHMENTS_FOLDER=data/attachments
|
||||||
# SENDS_FOLDER=data/sends
|
# SENDS_FOLDER=data/sends
|
||||||
|
|
||||||
|
## Temporary folder used for storing temporary file uploads
|
||||||
|
## Must be a local path.
|
||||||
# TMP_FOLDER=data/tmp
|
# TMP_FOLDER=data/tmp
|
||||||
|
|
||||||
## Templates data folder, by default uses embedded templates
|
## HTML template overrides data folder
|
||||||
## Check source code to see the format
|
## Must be a local path.
|
||||||
# TEMPLATES_FOLDER=data/templates
|
# TEMPLATES_FOLDER=data/templates
|
||||||
## Automatically reload the templates for every request, slow, use only for development
|
## Automatically reload the templates for every request, slow, use only for development
|
||||||
# RELOAD_TEMPLATES=false
|
# RELOAD_TEMPLATES=false
|
||||||
@@ -39,7 +50,9 @@
|
|||||||
#########################
|
#########################
|
||||||
|
|
||||||
## Database URL
|
## Database URL
|
||||||
## When using SQLite, this is the path to the DB file, default to %DATA_FOLDER%/db.sqlite3
|
## When using SQLite, this is the path to the DB file, and it defaults to
|
||||||
|
## %DATA_FOLDER%/db.sqlite3. If DATA_FOLDER is set to an external location, this
|
||||||
|
## must be set to a local sqlite3 file path.
|
||||||
# DATABASE_URL=data/db.sqlite3
|
# DATABASE_URL=data/db.sqlite3
|
||||||
## When using MySQL, specify an appropriate connection URI.
|
## When using MySQL, specify an appropriate connection URI.
|
||||||
## Details: https://docs.diesel.rs/2.1.x/diesel/mysql/struct.MysqlConnection.html
|
## Details: https://docs.diesel.rs/2.1.x/diesel/mysql/struct.MysqlConnection.html
|
||||||
@@ -67,8 +80,16 @@
|
|||||||
## Timeout when acquiring database connection
|
## Timeout when acquiring database connection
|
||||||
# DATABASE_TIMEOUT=30
|
# DATABASE_TIMEOUT=30
|
||||||
|
|
||||||
|
## Database idle timeout
|
||||||
|
## Timeout in seconds before idle connections to the database are closed.
|
||||||
|
# DATABASE_IDLE_TIMEOUT=600
|
||||||
|
|
||||||
|
## Database min connections
|
||||||
|
## Define the minimum size of the connection pool used for connecting to the database.
|
||||||
|
# DATABASE_MIN_CONNS=2
|
||||||
|
|
||||||
## Database max connections
|
## Database max connections
|
||||||
## Define the size of the connection pool used for connecting to the database.
|
## Define the maximum size of the connection pool used for connecting to the database.
|
||||||
# DATABASE_MAX_CONNS=10
|
# DATABASE_MAX_CONNS=10
|
||||||
|
|
||||||
## Database connection initialization
|
## Database connection initialization
|
||||||
@@ -117,7 +138,7 @@
|
|||||||
## and are always in terms of UTC time (regardless of your local time zone settings).
|
## and are always in terms of UTC time (regardless of your local time zone settings).
|
||||||
##
|
##
|
||||||
## The schedule format is a bit different from crontab as crontab does not contains seconds.
|
## The schedule format is a bit different from crontab as crontab does not contains seconds.
|
||||||
## You can test the the format here: https://crontab.guru, but remove the first digit!
|
## You can test the format here: https://crontab.guru, but remove the first digit!
|
||||||
## SEC MIN HOUR DAY OF MONTH MONTH DAY OF WEEK
|
## SEC MIN HOUR DAY OF MONTH MONTH DAY OF WEEK
|
||||||
## "0 30 9,12,15 1,15 May-Aug Mon,Wed,Fri"
|
## "0 30 9,12,15 1,15 May-Aug Mon,Wed,Fri"
|
||||||
## "0 30 * * * * "
|
## "0 30 * * * * "
|
||||||
@@ -161,6 +182,10 @@
|
|||||||
## 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 or set to use the legacy iframe prompt.
|
||||||
## 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 * * * * *"
|
||||||
|
#
|
||||||
|
## Cron schedule of the job that cleans sso nonce from incomplete flow
|
||||||
|
## Defaults to daily (20 minutes after midnight). Set blank to disable this job.
|
||||||
|
# PURGE_INCOMPLETE_SSO_NONCE="0 20 0 * * *"
|
||||||
|
|
||||||
########################
|
########################
|
||||||
### General settings ###
|
### General settings ###
|
||||||
@@ -229,7 +254,8 @@
|
|||||||
# SIGNUPS_ALLOWED=true
|
# SIGNUPS_ALLOWED=true
|
||||||
|
|
||||||
## Controls if new users need to verify their email address upon registration
|
## Controls if new users need to verify their email address upon registration
|
||||||
## Note that setting this option to true prevents logins until the email address has been verified!
|
## On new client versions, this will require the user to verify their email at signup time.
|
||||||
|
## On older clients, it will require the user to verify their email before they can log in.
|
||||||
## The welcome email will include a verification link, and login attempts will periodically
|
## The welcome email will include a verification link, and login attempts will periodically
|
||||||
## trigger another verification email to be sent.
|
## trigger another verification email to be sent.
|
||||||
# SIGNUPS_VERIFY=false
|
# SIGNUPS_VERIFY=false
|
||||||
@@ -259,7 +285,7 @@
|
|||||||
## A comma-separated list means only those users can create orgs:
|
## A comma-separated list means only those users can create orgs:
|
||||||
# ORG_CREATION_USERS=admin1@example.com,admin2@example.com
|
# ORG_CREATION_USERS=admin1@example.com,admin2@example.com
|
||||||
|
|
||||||
## Invitations org admins to invite users, even when signups are disabled
|
## Allows org admins to invite users, even when signups are disabled
|
||||||
# INVITATIONS_ALLOWED=true
|
# INVITATIONS_ALLOWED=true
|
||||||
## Name shown in the invitation emails that don't come from a specific organization
|
## Name shown in the invitation emails that don't come from a specific organization
|
||||||
# INVITATION_ORG_NAME=Vaultwarden
|
# INVITATION_ORG_NAME=Vaultwarden
|
||||||
@@ -280,12 +306,13 @@
|
|||||||
## The default for new users. If changed, it will be updated during login for existing users.
|
## The default for new users. If changed, it will be updated during login for existing users.
|
||||||
# PASSWORD_ITERATIONS=600000
|
# PASSWORD_ITERATIONS=600000
|
||||||
|
|
||||||
## Controls whether users can set password hints. This setting applies globally to all users.
|
## Controls whether users can set or show password hints. This setting applies globally to all users.
|
||||||
# PASSWORD_HINTS_ALLOWED=true
|
# PASSWORD_HINTS_ALLOWED=true
|
||||||
|
|
||||||
## Controls whether a password hint should be shown directly in the web page if
|
## Controls whether a password hint should be shown directly in the web page if
|
||||||
## SMTP service is not configured. Not recommended for publicly-accessible instances
|
## SMTP service is not configured and password hints are allowed.
|
||||||
## as this provides unauthenticated access to potentially sensitive data.
|
## Not recommended for publicly-accessible instances because this provides
|
||||||
|
## unauthenticated access to potentially sensitive data.
|
||||||
# SHOW_PASSWORD_HINT=false
|
# SHOW_PASSWORD_HINT=false
|
||||||
|
|
||||||
#########################
|
#########################
|
||||||
@@ -326,29 +353,33 @@
|
|||||||
|
|
||||||
## Icon download timeout
|
## Icon download timeout
|
||||||
## Configure the timeout value when downloading the favicons.
|
## Configure the timeout value when downloading the favicons.
|
||||||
## The default is 10 seconds, but this could be to low on slower network connections
|
## The default is 10 seconds, but this could be too low on slower network connections
|
||||||
# ICON_DOWNLOAD_TIMEOUT=10
|
# ICON_DOWNLOAD_TIMEOUT=10
|
||||||
|
|
||||||
## Block HTTP domains/IPs by Regex
|
## Block HTTP domains/IPs by Regex
|
||||||
## Any domains or IPs that match this regex won't be fetched by the internal HTTP client.
|
## Any domains or IPs that match this regex won't be fetched by the internal HTTP client.
|
||||||
## Useful to hide other servers in the local network. Check the WIKI for more details
|
## Useful to hide other servers in the local network. Check the WIKI for more details
|
||||||
## NOTE: Always enclose this regex withing single quotes!
|
## NOTE: Always enclose this regex within single quotes!
|
||||||
# HTTP_REQUEST_BLOCK_REGEX='^(192\.168\.0\.[0-9]+|192\.168\.1\.[0-9]+)$'
|
# HTTP_REQUEST_BLOCK_REGEX='^(192\.168\.0\.[0-9]+|192\.168\.1\.[0-9]+)$'
|
||||||
|
|
||||||
## Enabling this will cause the internal HTTP client to refuse to connect to any non global IP address.
|
## Enabling this will cause the internal HTTP client to refuse to connect to any non-global IP address.
|
||||||
## Useful to secure your internal environment: See https://en.wikipedia.org/wiki/Reserved_IP_addresses for a list of IPs which it will block
|
## Useful to secure your internal environment: See https://en.wikipedia.org/wiki/Reserved_IP_addresses for a list of IPs which it will block
|
||||||
# HTTP_REQUEST_BLOCK_NON_GLOBAL_IPS=true
|
# HTTP_REQUEST_BLOCK_NON_GLOBAL_IPS=true
|
||||||
|
|
||||||
## Client Settings
|
## Client Settings
|
||||||
## Enable experimental feature flags for clients.
|
## Enable experimental feature flags for clients.
|
||||||
## This is a comma-separated list of flags, e.g. "flag1,flag2,flag3".
|
## This is a comma-separated list of flags, e.g. "flag1,flag2,flag3".
|
||||||
|
## Note that clients cache the /api/config endpoint for about 1 hour and it could take some time before they are enabled or disabled!
|
||||||
##
|
##
|
||||||
## The following flags are available:
|
## The following flags are available:
|
||||||
## - "autofill-overlay": Add an overlay menu to form fields for quick access to credentials.
|
## - "inline-menu-positioning-improvements": Enable the use of inline menu password generator and identity suggestions in the browser extension.
|
||||||
## - "autofill-v2": Use the new autofill implementation.
|
## - "inline-menu-totp": Enable the use of inline menu TOTP codes in the browser extension.
|
||||||
## - "browser-fileless-import": Directly import credentials from other providers without a file.
|
## - "ssh-agent": Enable SSH agent support on Desktop. (Needs desktop >=2024.12.0)
|
||||||
## - "extension-refresh": Temporarily enable the new extension design until general availability (should be used with the beta Chrome extension)
|
## - "ssh-key-vault-item": Enable the creation and use of SSH key vault items. (Needs clients >=2024.12.0)
|
||||||
## - "fido2-vault-credentials": Enable the use of FIDO2 security keys as second factor.
|
## - "export-attachments": Enable support for exporting attachments (Clients >=2025.4.0)
|
||||||
|
## - "anon-addy-self-host-alias": Enable configuring self-hosted Anon Addy alias generator. (Needs Android >=2025.3.0, iOS >=2025.4.0)
|
||||||
|
## - "simple-login-self-host-alias": Enable configuring self-hosted Simple Login alias generator. (Needs Android >=2025.3.0, iOS >=2025.4.0)
|
||||||
|
## - "mutual-tls": Enable the use of mutual TLS on Android (Client >= 2025.2.0)
|
||||||
# EXPERIMENTAL_CLIENT_FEATURE_FLAGS=fido2-vault-credentials
|
# EXPERIMENTAL_CLIENT_FEATURE_FLAGS=fido2-vault-credentials
|
||||||
|
|
||||||
## Require new device emails. When a user logs in an email is required to be sent.
|
## Require new device emails. When a user logs in an email is required to be sent.
|
||||||
@@ -407,6 +438,14 @@
|
|||||||
## Multiple values must be separated with a whitespace.
|
## Multiple values must be separated with a whitespace.
|
||||||
# ALLOWED_IFRAME_ANCESTORS=
|
# ALLOWED_IFRAME_ANCESTORS=
|
||||||
|
|
||||||
|
## Allowed connect-src (Know the risks!)
|
||||||
|
## https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/connect-src
|
||||||
|
## Allows other domains to URLs which can be loaded using script interfaces like the Forwarded email alias feature
|
||||||
|
## This adds the configured value to the 'Content-Security-Policy' headers 'connect-src' value.
|
||||||
|
## Multiple values must be separated with a whitespace. And only HTTPS values are allowed.
|
||||||
|
## Example: "https://my-addy-io.domain.tld https://my-simplelogin.domain.tld"
|
||||||
|
# ALLOWED_CONNECT_SRC=""
|
||||||
|
|
||||||
## Number of seconds, on average, between login requests from the same IP address before rate limiting kicks in.
|
## Number of seconds, on average, between login requests from the same IP address before rate limiting kicks in.
|
||||||
# LOGIN_RATELIMIT_SECONDS=60
|
# LOGIN_RATELIMIT_SECONDS=60
|
||||||
## Allow a burst of requests of up to this size, while maintaining the average indicated by `LOGIN_RATELIMIT_SECONDS`.
|
## Allow a burst of requests of up to this size, while maintaining the average indicated by `LOGIN_RATELIMIT_SECONDS`.
|
||||||
@@ -432,6 +471,55 @@
|
|||||||
## Setting this to true will enforce the Single Org Policy to be enabled before you can enable the Reset Password policy.
|
## Setting this to true will enforce the Single Org Policy to be enabled before you can enable the Reset Password policy.
|
||||||
# ENFORCE_SINGLE_ORG_WITH_RESET_PW_POLICY=false
|
# ENFORCE_SINGLE_ORG_WITH_RESET_PW_POLICY=false
|
||||||
|
|
||||||
|
#####################################
|
||||||
|
### SSO settings (OpenID Connect) ###
|
||||||
|
#####################################
|
||||||
|
|
||||||
|
## Controls whether users can login using an OpenID Connect identity provider
|
||||||
|
# SSO_ENABLED=false
|
||||||
|
|
||||||
|
## Prevent users from logging in directly without going through SSO
|
||||||
|
# SSO_ONLY=false
|
||||||
|
|
||||||
|
## On SSO Signup if a user with a matching email already exists make the association
|
||||||
|
# SSO_SIGNUPS_MATCH_EMAIL=true
|
||||||
|
|
||||||
|
## Allow unknown email verification status. Allowing this with `SSO_SIGNUPS_MATCH_EMAIL=true` open potential account takeover.
|
||||||
|
# SSO_ALLOW_UNKNOWN_EMAIL_VERIFICATION=false
|
||||||
|
|
||||||
|
## Base URL of the OIDC server (auto-discovery is used)
|
||||||
|
## - Should not include the `/.well-known/openid-configuration` part and no trailing `/`
|
||||||
|
## - ${SSO_AUTHORITY}/.well-known/openid-configuration should return a json document: https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse
|
||||||
|
# SSO_AUTHORITY=https://auth.example.com
|
||||||
|
|
||||||
|
## Authorization request scopes. Optional SSO scopes, override if email and profile are not enough (`openid` is implicit).
|
||||||
|
# SSO_SCOPES="email profile"
|
||||||
|
|
||||||
|
## Additional authorization url parameters (ex: to obtain a `refresh_token` with Google Auth).
|
||||||
|
# SSO_AUTHORIZE_EXTRA_PARAMS="access_type=offline&prompt=consent"
|
||||||
|
|
||||||
|
## Activate PKCE for the Auth Code flow.
|
||||||
|
# SSO_PKCE=true
|
||||||
|
|
||||||
|
## Regex for additional trusted Id token audience (by default only the client_id is trusted).
|
||||||
|
# SSO_AUDIENCE_TRUSTED='^$'
|
||||||
|
|
||||||
|
## Set your Client ID and Client Key
|
||||||
|
# SSO_CLIENT_ID=11111
|
||||||
|
# SSO_CLIENT_SECRET=AAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
|
||||||
|
## Optional Master password policy (minComplexity=[0-4]), `enforceOnLogin` is not supported at the moment.
|
||||||
|
# SSO_MASTER_PASSWORD_POLICY='{"enforceOnLogin":false,"minComplexity":3,"minLength":12,"requireLower":false,"requireNumbers":false,"requireSpecial":false,"requireUpper":false}'
|
||||||
|
|
||||||
|
## Use sso only for authentication not the session lifecycle
|
||||||
|
# SSO_AUTH_ONLY_NOT_SESSION=false
|
||||||
|
|
||||||
|
## Client cache for discovery endpoint. Duration in seconds (0 to disable).
|
||||||
|
# SSO_CLIENT_CACHE_EXPIRATION=0
|
||||||
|
|
||||||
|
## Log all the tokens, LOG_LEVEL=debug is required
|
||||||
|
# SSO_DEBUG_TOKENS=false
|
||||||
|
|
||||||
########################
|
########################
|
||||||
### MFA/2FA settings ###
|
### MFA/2FA settings ###
|
||||||
########################
|
########################
|
||||||
@@ -474,7 +562,7 @@
|
|||||||
## Maximum attempts before an email token is reset and a new email will need to be sent.
|
## Maximum attempts before an email token is reset and a new email will need to be sent.
|
||||||
# EMAIL_ATTEMPTS_LIMIT=3
|
# EMAIL_ATTEMPTS_LIMIT=3
|
||||||
##
|
##
|
||||||
## Setup email 2FA regardless of any organization policy
|
## Setup email 2FA on registration regardless of any organization policy
|
||||||
# EMAIL_2FA_ENFORCE_ON_VERIFIED_INVITE=false
|
# EMAIL_2FA_ENFORCE_ON_VERIFIED_INVITE=false
|
||||||
## Automatically setup email 2FA as fallback provider when needed
|
## Automatically setup email 2FA as fallback provider when needed
|
||||||
# EMAIL_2FA_AUTO_FALLBACK=false
|
# EMAIL_2FA_AUTO_FALLBACK=false
|
||||||
@@ -491,7 +579,7 @@
|
|||||||
##
|
##
|
||||||
## According to the RFC6238 (https://tools.ietf.org/html/rfc6238),
|
## According to the RFC6238 (https://tools.ietf.org/html/rfc6238),
|
||||||
## we allow by default the TOTP code which was valid one step back and one in the future.
|
## we allow by default the TOTP code which was valid one step back and one in the future.
|
||||||
## This can however allow attackers to be a bit more lucky with there attempts because there are 3 valid codes.
|
## This can however allow attackers to be a bit more lucky with their attempts because there are 3 valid codes.
|
||||||
## You can disable this, so that only the current TOTP Code is allowed.
|
## You can disable this, so that only the current TOTP Code is allowed.
|
||||||
## Keep in mind that when a sever drifts out of time, valid codes could be marked as invalid.
|
## Keep in mind that when a sever drifts out of time, valid codes could be marked as invalid.
|
||||||
## In any case, if a code has been used it can not be used again, also codes which predates it will be invalid.
|
## In any case, if a code has been used it can not be used again, also codes which predates it will be invalid.
|
||||||
@@ -531,7 +619,7 @@
|
|||||||
# SMTP_AUTH_MECHANISM=
|
# SMTP_AUTH_MECHANISM=
|
||||||
|
|
||||||
## Server name sent during the SMTP HELO
|
## Server name sent during the SMTP HELO
|
||||||
## By default this value should be is on the machine's hostname,
|
## By default this value should be the machine's hostname,
|
||||||
## but might need to be changed in case it trips some anti-spam filters
|
## but might need to be changed in case it trips some anti-spam filters
|
||||||
# HELO_NAME=
|
# HELO_NAME=
|
||||||
|
|
||||||
|
3
.github/CODEOWNERS
vendored
3
.github/CODEOWNERS
vendored
@@ -1,3 +1,6 @@
|
|||||||
/.github @dani-garcia @BlackDex
|
/.github @dani-garcia @BlackDex
|
||||||
|
/.github/** @dani-garcia @BlackDex
|
||||||
/.github/CODEOWNERS @dani-garcia @BlackDex
|
/.github/CODEOWNERS @dani-garcia @BlackDex
|
||||||
|
/.github/ISSUE_TEMPLATE/** @dani-garcia @BlackDex
|
||||||
/.github/workflows/** @dani-garcia @BlackDex
|
/.github/workflows/** @dani-garcia @BlackDex
|
||||||
|
/SECURITY.md @dani-garcia @BlackDex
|
||||||
|
25
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
25
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -8,15 +8,30 @@ body:
|
|||||||
value: |
|
value: |
|
||||||
Thanks for taking the time to fill out this bug report!
|
Thanks for taking the time to fill out this bug report!
|
||||||
|
|
||||||
Please *do not* submit feature requests or ask for help on how to configure Vaultwarden here.
|
Please **do not** submit feature requests or ask for help on how to configure Vaultwarden here!
|
||||||
|
|
||||||
The [GitHub Discussions](https://github.com/dani-garcia/vaultwarden/discussions/) has sections for Questions and Ideas.
|
The [GitHub Discussions](https://github.com/dani-garcia/vaultwarden/discussions/) has sections for Questions and Ideas.
|
||||||
|
|
||||||
|
Our [Wiki](https://github.com/dani-garcia/vaultwarden/wiki/) has topics on how to configure Vaultwarden.
|
||||||
|
|
||||||
Also, make sure you are running [](https://github.com/dani-garcia/vaultwarden/releases/latest) of Vaultwarden!
|
Also, make sure you are running [](https://github.com/dani-garcia/vaultwarden/releases/latest) of Vaultwarden!
|
||||||
And search for existing open or closed issues or discussions regarding your topic before posting.
|
|
||||||
|
|
||||||
Be sure to check and validate the Vaultwarden Admin Diagnostics (`/admin/diagnostics`) page for any errors!
|
Be sure to check and validate the Vaultwarden Admin Diagnostics (`/admin/diagnostics`) page for any errors!
|
||||||
See here [how to enable the admin page](https://github.com/dani-garcia/vaultwarden/wiki/Enabling-admin-page).
|
See here [how to enable the admin page](https://github.com/dani-garcia/vaultwarden/wiki/Enabling-admin-page).
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> ## :bangbang: Search for existing **Closed _AND_ Open** [Issues](https://github.com/dani-garcia/vaultwarden/issues?q=is%3Aissue%20) **_AND_** [Discussions](https://github.com/dani-garcia/vaultwarden/discussions?discussions_q=) regarding your topic before posting! :bangbang:
|
||||||
|
#
|
||||||
|
- type: checkboxes
|
||||||
|
id: checklist
|
||||||
|
attributes:
|
||||||
|
label: Prerequisites
|
||||||
|
description: Please confirm you have completed the following before submitting an issue!
|
||||||
|
options:
|
||||||
|
- label: I have searched the existing **Closed _AND_ Open** [Issues](https://github.com/dani-garcia/vaultwarden/issues?q=is%3Aissue%20) **_AND_** [Discussions](https://github.com/dani-garcia/vaultwarden/discussions?discussions_q=)
|
||||||
|
required: true
|
||||||
|
- label: I have searched and read the [documentation](https://github.com/dani-garcia/vaultwarden/wiki/)
|
||||||
|
required: true
|
||||||
#
|
#
|
||||||
- id: support-string
|
- id: support-string
|
||||||
type: textarea
|
type: textarea
|
||||||
@@ -36,7 +51,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Vaultwarden Build Version
|
label: Vaultwarden Build Version
|
||||||
description: What version of Vaultwarden are you running?
|
description: What version of Vaultwarden are you running?
|
||||||
placeholder: ex. v1.31.0 or v1.32.0-3466a804
|
placeholder: ex. v1.34.0 or v1.34.1-53f58b14
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
#
|
#
|
||||||
@@ -67,7 +82,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Reverse Proxy
|
label: Reverse Proxy
|
||||||
description: Are you using a reverse proxy, if so which and what version?
|
description: Are you using a reverse proxy, if so which and what version?
|
||||||
placeholder: ex. nginx 1.26.2, caddy 2.8.4, traefik 3.1.2, haproxy 3.0
|
placeholder: ex. nginx 1.29.0, caddy 2.10.0, traefik 3.4.4, haproxy 3.2
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
#
|
#
|
||||||
@@ -115,7 +130,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Client Version
|
label: Client Version
|
||||||
description: What version(s) of the client(s) are you seeing the problem on?
|
description: What version(s) of the client(s) are you seeing the problem on?
|
||||||
placeholder: ex. CLI v2024.7.2, Firefox 130 - v2024.7.0
|
placeholder: ex. CLI v2025.7.0, Firefox 140 - v2025.6.1
|
||||||
#
|
#
|
||||||
- id: reproduce
|
- id: reproduce
|
||||||
type: textarea
|
type: textarea
|
||||||
|
109
.github/workflows/build.yml
vendored
109
.github/workflows/build.yml
vendored
@@ -1,4 +1,5 @@
|
|||||||
name: Build
|
name: Build
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -13,6 +14,7 @@ on:
|
|||||||
- "diesel.toml"
|
- "diesel.toml"
|
||||||
- "docker/Dockerfile.j2"
|
- "docker/Dockerfile.j2"
|
||||||
- "docker/DockerSettings.yaml"
|
- "docker/DockerSettings.yaml"
|
||||||
|
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- ".github/workflows/build.yml"
|
- ".github/workflows/build.yml"
|
||||||
@@ -28,13 +30,16 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
# We use Ubuntu 22.04 here because this matches the library versions used within the Debian docker containers
|
name: Build and Test ${{ matrix.channel }}
|
||||||
runs-on: ubuntu-22.04
|
permissions:
|
||||||
|
actions: write
|
||||||
|
contents: read
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
timeout-minutes: 120
|
timeout-minutes: 120
|
||||||
# Make warnings errors, this is to prevent warnings slipping through.
|
# Make warnings errors, this is to prevent warnings slipping through.
|
||||||
# This is done globally to prevent rebuilds when the RUSTFLAGS env variable changes.
|
# This is done globally to prevent rebuilds when the RUSTFLAGS env variable changes.
|
||||||
env:
|
env:
|
||||||
RUSTFLAGS: "-D warnings"
|
RUSTFLAGS: "-Dwarnings"
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -42,32 +47,33 @@ jobs:
|
|||||||
- "rust-toolchain" # The version defined in rust-toolchain
|
- "rust-toolchain" # The version defined in rust-toolchain
|
||||||
- "msrv" # The supported MSRV
|
- "msrv" # The supported MSRV
|
||||||
|
|
||||||
name: Build and Test ${{ matrix.channel }}
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
# Checkout the repo
|
|
||||||
- name: "Checkout"
|
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 #v4.2.1
|
|
||||||
# End Checkout the repo
|
|
||||||
|
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
- name: "Install dependencies Ubuntu"
|
- name: "Install dependencies Ubuntu"
|
||||||
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends openssl build-essential libmariadb-dev-compat libpq-dev libssl-dev pkg-config
|
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends openssl build-essential libmariadb-dev-compat libpq-dev libssl-dev pkg-config
|
||||||
# End Install dependencies
|
# End Install dependencies
|
||||||
|
|
||||||
|
# Checkout the repo
|
||||||
|
- name: "Checkout"
|
||||||
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
fetch-depth: 0
|
||||||
|
# End Checkout the repo
|
||||||
|
|
||||||
# Determine rust-toolchain version
|
# Determine rust-toolchain version
|
||||||
- name: Init Variables
|
- name: Init Variables
|
||||||
id: toolchain
|
id: toolchain
|
||||||
shell: bash
|
shell: bash
|
||||||
|
env:
|
||||||
|
CHANNEL: ${{ matrix.channel }}
|
||||||
run: |
|
run: |
|
||||||
if [[ "${{ matrix.channel }}" == 'rust-toolchain' ]]; then
|
if [[ "${CHANNEL}" == 'rust-toolchain' ]]; then
|
||||||
RUST_TOOLCHAIN="$(grep -oP 'channel.*"(\K.*?)(?=")' rust-toolchain.toml)"
|
RUST_TOOLCHAIN="$(grep -oP 'channel.*"(\K.*?)(?=")' rust-toolchain.toml)"
|
||||||
elif [[ "${{ matrix.channel }}" == 'msrv' ]]; then
|
elif [[ "${CHANNEL}" == 'msrv' ]]; then
|
||||||
RUST_TOOLCHAIN="$(grep -oP 'rust-version.*"(\K.*?)(?=")' Cargo.toml)"
|
RUST_TOOLCHAIN="$(grep -oP 'rust-version.*"(\K.*?)(?=")' Cargo.toml)"
|
||||||
else
|
else
|
||||||
RUST_TOOLCHAIN="${{ matrix.channel }}"
|
RUST_TOOLCHAIN="${CHANNEL}"
|
||||||
fi
|
fi
|
||||||
echo "RUST_TOOLCHAIN=${RUST_TOOLCHAIN}" | tee -a "${GITHUB_OUTPUT}"
|
echo "RUST_TOOLCHAIN=${RUST_TOOLCHAIN}" | tee -a "${GITHUB_OUTPUT}"
|
||||||
# End Determine rust-toolchain version
|
# End Determine rust-toolchain version
|
||||||
@@ -75,7 +81,7 @@ jobs:
|
|||||||
|
|
||||||
# Only install the clippy and rustfmt components on the default rust-toolchain
|
# Only install the clippy and rustfmt components on the default rust-toolchain
|
||||||
- name: "Install rust-toolchain version"
|
- name: "Install rust-toolchain version"
|
||||||
uses: dtolnay/rust-toolchain@7b1c307e0dcbda6122208f10795a713336a9b35a # master @ Aug 8, 2024, 7:36 PM GMT+2
|
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master @ Aug 23, 2025, 3:20 AM GMT+2
|
||||||
if: ${{ matrix.channel == 'rust-toolchain' }}
|
if: ${{ matrix.channel == 'rust-toolchain' }}
|
||||||
with:
|
with:
|
||||||
toolchain: "${{steps.toolchain.outputs.RUST_TOOLCHAIN}}"
|
toolchain: "${{steps.toolchain.outputs.RUST_TOOLCHAIN}}"
|
||||||
@@ -85,7 +91,7 @@ jobs:
|
|||||||
|
|
||||||
# Install the any other channel to be used for which we do not execute clippy and rustfmt
|
# Install the any other channel to be used for which we do not execute clippy and rustfmt
|
||||||
- name: "Install MSRV version"
|
- name: "Install MSRV version"
|
||||||
uses: dtolnay/rust-toolchain@7b1c307e0dcbda6122208f10795a713336a9b35a # master @ Aug 8, 2024, 7:36 PM GMT+2
|
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master @ Aug 23, 2025, 3:20 AM GMT+2
|
||||||
if: ${{ matrix.channel != 'rust-toolchain' }}
|
if: ${{ matrix.channel != 'rust-toolchain' }}
|
||||||
with:
|
with:
|
||||||
toolchain: "${{steps.toolchain.outputs.RUST_TOOLCHAIN}}"
|
toolchain: "${{steps.toolchain.outputs.RUST_TOOLCHAIN}}"
|
||||||
@@ -93,11 +99,13 @@ jobs:
|
|||||||
|
|
||||||
# Set the current matrix toolchain version as default
|
# Set the current matrix toolchain version as default
|
||||||
- name: "Set toolchain ${{steps.toolchain.outputs.RUST_TOOLCHAIN}} as default"
|
- name: "Set toolchain ${{steps.toolchain.outputs.RUST_TOOLCHAIN}} as default"
|
||||||
|
env:
|
||||||
|
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
|
# Set the default
|
||||||
rustup default ${{steps.toolchain.outputs.RUST_TOOLCHAIN}}
|
rustup default "${RUST_TOOLCHAIN}"
|
||||||
|
|
||||||
# Show environment
|
# Show environment
|
||||||
- name: "Show environment"
|
- name: "Show environment"
|
||||||
@@ -107,7 +115,8 @@ jobs:
|
|||||||
# End Show environment
|
# End Show environment
|
||||||
|
|
||||||
# Enable Rust Caching
|
# Enable Rust Caching
|
||||||
- uses: Swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84 # v2.7.3
|
- name: Rust Caching
|
||||||
|
uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||||
with:
|
with:
|
||||||
# Use a custom prefix-key to force a fresh start. This is sometimes needed with bigger changes.
|
# Use a custom prefix-key to force a fresh start. This is sometimes needed with bigger changes.
|
||||||
# Like changing the build host from Ubuntu 20.04 to 22.04 for example.
|
# Like changing the build host from Ubuntu 20.04 to 22.04 for example.
|
||||||
@@ -117,33 +126,39 @@ jobs:
|
|||||||
|
|
||||||
# Run cargo tests
|
# Run cargo tests
|
||||||
# First test all features together, afterwards test them separately.
|
# First test all features together, afterwards test them separately.
|
||||||
|
- name: "test features: sqlite,mysql,postgresql,enable_mimalloc,query_logger"
|
||||||
|
id: test_sqlite_mysql_postgresql_mimalloc_logger
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
run: |
|
||||||
|
cargo test --features sqlite,mysql,postgresql,enable_mimalloc,query_logger
|
||||||
|
|
||||||
- name: "test features: sqlite,mysql,postgresql,enable_mimalloc"
|
- name: "test features: sqlite,mysql,postgresql,enable_mimalloc"
|
||||||
id: test_sqlite_mysql_postgresql_mimalloc
|
id: test_sqlite_mysql_postgresql_mimalloc
|
||||||
if: $${{ always() }}
|
if: ${{ !cancelled() }}
|
||||||
run: |
|
run: |
|
||||||
cargo test --features sqlite,mysql,postgresql,enable_mimalloc
|
cargo test --features sqlite,mysql,postgresql,enable_mimalloc
|
||||||
|
|
||||||
- name: "test features: sqlite,mysql,postgresql"
|
- name: "test features: sqlite,mysql,postgresql"
|
||||||
id: test_sqlite_mysql_postgresql
|
id: test_sqlite_mysql_postgresql
|
||||||
if: $${{ always() }}
|
if: ${{ !cancelled() }}
|
||||||
run: |
|
run: |
|
||||||
cargo test --features sqlite,mysql,postgresql
|
cargo test --features sqlite,mysql,postgresql
|
||||||
|
|
||||||
- name: "test features: sqlite"
|
- name: "test features: sqlite"
|
||||||
id: test_sqlite
|
id: test_sqlite
|
||||||
if: $${{ always() }}
|
if: ${{ !cancelled() }}
|
||||||
run: |
|
run: |
|
||||||
cargo test --features sqlite
|
cargo test --features sqlite
|
||||||
|
|
||||||
- name: "test features: mysql"
|
- name: "test features: mysql"
|
||||||
id: test_mysql
|
id: test_mysql
|
||||||
if: $${{ always() }}
|
if: ${{ !cancelled() }}
|
||||||
run: |
|
run: |
|
||||||
cargo test --features mysql
|
cargo test --features mysql
|
||||||
|
|
||||||
- name: "test features: postgresql"
|
- name: "test features: postgresql"
|
||||||
id: test_postgresql
|
id: test_postgresql
|
||||||
if: $${{ always() }}
|
if: ${{ !cancelled() }}
|
||||||
run: |
|
run: |
|
||||||
cargo test --features postgresql
|
cargo test --features postgresql
|
||||||
# End Run cargo tests
|
# End Run cargo tests
|
||||||
@@ -152,16 +167,16 @@ jobs:
|
|||||||
# Run cargo clippy, and fail on warnings
|
# Run cargo clippy, and fail on warnings
|
||||||
- name: "clippy features: sqlite,mysql,postgresql,enable_mimalloc"
|
- name: "clippy features: sqlite,mysql,postgresql,enable_mimalloc"
|
||||||
id: clippy
|
id: clippy
|
||||||
if: ${{ always() && matrix.channel == 'rust-toolchain' }}
|
if: ${{ !cancelled() && matrix.channel == 'rust-toolchain' }}
|
||||||
run: |
|
run: |
|
||||||
cargo clippy --features sqlite,mysql,postgresql,enable_mimalloc -- -D warnings
|
cargo clippy --features sqlite,mysql,postgresql,enable_mimalloc
|
||||||
# End Run cargo clippy
|
# End Run cargo clippy
|
||||||
|
|
||||||
|
|
||||||
# Run cargo fmt (Only run on rust-toolchain defined version)
|
# Run cargo fmt (Only run on rust-toolchain defined version)
|
||||||
- name: "check formatting"
|
- name: "check formatting"
|
||||||
id: formatting
|
id: formatting
|
||||||
if: ${{ always() && matrix.channel == 'rust-toolchain' }}
|
if: ${{ !cancelled() && matrix.channel == 'rust-toolchain' }}
|
||||||
run: |
|
run: |
|
||||||
cargo fmt --all -- --check
|
cargo fmt --all -- --check
|
||||||
# End Run cargo fmt
|
# End Run cargo fmt
|
||||||
@@ -171,21 +186,31 @@ jobs:
|
|||||||
# This is useful so all test/clippy/fmt actions are done, and they can all be addressed
|
# This is useful so all test/clippy/fmt actions are done, and they can all be addressed
|
||||||
- name: "Some checks failed"
|
- name: "Some checks failed"
|
||||||
if: ${{ failure() }}
|
if: ${{ failure() }}
|
||||||
|
env:
|
||||||
|
TEST_DB_M_L: ${{ steps.test_sqlite_mysql_postgresql_mimalloc_logger.outcome }}
|
||||||
|
TEST_DB_M: ${{ steps.test_sqlite_mysql_postgresql_mimalloc.outcome }}
|
||||||
|
TEST_DB: ${{ steps.test_sqlite_mysql_postgresql.outcome }}
|
||||||
|
TEST_SQLITE: ${{ steps.test_sqlite.outcome }}
|
||||||
|
TEST_MYSQL: ${{ steps.test_mysql.outcome }}
|
||||||
|
TEST_POSTGRESQL: ${{ steps.test_postgresql.outcome }}
|
||||||
|
CLIPPY: ${{ steps.clippy.outcome }}
|
||||||
|
FMT: ${{ steps.formatting.outcome }}
|
||||||
run: |
|
run: |
|
||||||
echo "### :x: Checks Failed!" >> $GITHUB_STEP_SUMMARY
|
echo "### :x: Checks Failed!" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
echo "|Job|Status|" >> $GITHUB_STEP_SUMMARY
|
echo "|Job|Status|" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
echo "|---|------|" >> $GITHUB_STEP_SUMMARY
|
echo "|---|------|" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
echo "|test (sqlite,mysql,postgresql,enable_mimalloc)|${{ steps.test_sqlite_mysql_postgresql_mimalloc.outcome }}|" >> $GITHUB_STEP_SUMMARY
|
echo "|test (sqlite,mysql,postgresql,enable_mimalloc,query_logger)|${TEST_DB_M_L}|" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
echo "|test (sqlite,mysql,postgresql)|${{ steps.test_sqlite_mysql_postgresql.outcome }}|" >> $GITHUB_STEP_SUMMARY
|
echo "|test (sqlite,mysql,postgresql,enable_mimalloc)|${TEST_DB_M}|" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
echo "|test (sqlite)|${{ steps.test_sqlite.outcome }}|" >> $GITHUB_STEP_SUMMARY
|
echo "|test (sqlite,mysql,postgresql)|${TEST_DB}|" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
echo "|test (mysql)|${{ steps.test_mysql.outcome }}|" >> $GITHUB_STEP_SUMMARY
|
echo "|test (sqlite)|${TEST_SQLITE}|" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
echo "|test (postgresql)|${{ steps.test_postgresql.outcome }}|" >> $GITHUB_STEP_SUMMARY
|
echo "|test (mysql)|${TEST_MYSQL}|" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
echo "|clippy (sqlite,mysql,postgresql,enable_mimalloc)|${{ steps.clippy.outcome }}|" >> $GITHUB_STEP_SUMMARY
|
echo "|test (postgresql)|${TEST_POSTGRESQL}|" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
echo "|fmt|${{ steps.formatting.outcome }}|" >> $GITHUB_STEP_SUMMARY
|
echo "|clippy (sqlite,mysql,postgresql,enable_mimalloc)|${CLIPPY}|" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "|fmt|${FMT}|" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
echo "Please check the failed jobs and fix where needed." >> $GITHUB_STEP_SUMMARY
|
echo "" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "Please check the failed jobs and fix where needed." >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
echo "" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
|
|
||||||
@@ -194,5 +219,5 @@ jobs:
|
|||||||
- name: "All checks passed"
|
- name: "All checks passed"
|
||||||
if: ${{ success() }}
|
if: ${{ success() }}
|
||||||
run: |
|
run: |
|
||||||
echo "### :tada: Checks Passed!" >> $GITHUB_STEP_SUMMARY
|
echo "### :tada: Checks Passed!" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
29
.github/workflows/check-templates.yml
vendored
Normal file
29
.github/workflows/check-templates.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
name: Check templates
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
|
on: [ push, pull_request ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
docker-templates:
|
||||||
|
name: Validate docker templates
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
timeout-minutes: 30
|
||||||
|
|
||||||
|
steps:
|
||||||
|
# Checkout the repo
|
||||||
|
- name: "Checkout"
|
||||||
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
# End Checkout the repo
|
||||||
|
|
||||||
|
- name: Run make to rebuild templates
|
||||||
|
working-directory: docker
|
||||||
|
run: make
|
||||||
|
|
||||||
|
- name: Check for unstaged changes
|
||||||
|
working-directory: docker
|
||||||
|
run: git diff --exit-code
|
||||||
|
continue-on-error: false
|
22
.github/workflows/hadolint.yml
vendored
22
.github/workflows/hadolint.yml
vendored
@@ -1,24 +1,20 @@
|
|||||||
name: Hadolint
|
name: Hadolint
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
on: [
|
on: [ push, pull_request ]
|
||||||
push,
|
|
||||||
pull_request
|
|
||||||
]
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
hadolint:
|
hadolint:
|
||||||
name: Validate Dockerfile syntax
|
name: Validate Dockerfile syntax
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
steps:
|
|
||||||
# Checkout the repo
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 #v4.2.1
|
|
||||||
# End Checkout the repo
|
|
||||||
|
|
||||||
|
steps:
|
||||||
# Start Docker Buildx
|
# Start Docker Buildx
|
||||||
- name: Setup Docker Buildx
|
- name: Setup Docker Buildx
|
||||||
uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1
|
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||||
# https://github.com/moby/buildkit/issues/3969
|
# https://github.com/moby/buildkit/issues/3969
|
||||||
# Also set max parallelism to 2, the default of 4 breaks GitHub Actions and causes OOMKills
|
# Also set max parallelism to 2, the default of 4 breaks GitHub Actions and causes OOMKills
|
||||||
with:
|
with:
|
||||||
@@ -37,6 +33,12 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
HADOLINT_VERSION: 2.12.0
|
HADOLINT_VERSION: 2.12.0
|
||||||
# End Download hadolint
|
# End Download hadolint
|
||||||
|
# Checkout the repo
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
# End Checkout the repo
|
||||||
|
|
||||||
# Test Dockerfiles with hadolint
|
# Test Dockerfiles with hadolint
|
||||||
- name: Run hadolint
|
- name: Run hadolint
|
||||||
|
213
.github/workflows/release.yml
vendored
213
.github/workflows/release.yml
vendored
@@ -1,4 +1,5 @@
|
|||||||
name: Release
|
name: Release
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -6,35 +7,30 @@ on:
|
|||||||
- main
|
- main
|
||||||
|
|
||||||
tags:
|
tags:
|
||||||
- '*'
|
# https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet
|
||||||
|
- '[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' }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# https://github.com/marketplace/actions/skip-duplicate-actions
|
|
||||||
# Some checks to determine if we need to continue with building a new docker.
|
|
||||||
# We will skip this check if we are creating a tag, because that has the same hash as a previous run already.
|
|
||||||
skip_check:
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
if: ${{ github.repository == 'dani-garcia/vaultwarden' }}
|
|
||||||
outputs:
|
|
||||||
should_skip: ${{ steps.skip_check.outputs.should_skip }}
|
|
||||||
steps:
|
|
||||||
- name: Skip Duplicates Actions
|
|
||||||
id: skip_check
|
|
||||||
uses: fkirc/skip-duplicate-actions@f75f66ce1886f00957d99748a42c724f4330bdcf # v5.3.1
|
|
||||||
with:
|
|
||||||
cancel_others: 'true'
|
|
||||||
# Only run this when not creating a tag
|
|
||||||
if: ${{ github.ref_type == 'branch' }}
|
|
||||||
|
|
||||||
docker-build:
|
docker-build:
|
||||||
|
name: Build Vaultwarden containers
|
||||||
|
if: ${{ github.repository == 'dani-garcia/vaultwarden' }}
|
||||||
|
permissions:
|
||||||
|
packages: write
|
||||||
|
contents: read
|
||||||
|
attestations: write
|
||||||
|
id-token: write
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
timeout-minutes: 120
|
timeout-minutes: 120
|
||||||
needs: skip_check
|
# Start a local docker registry to extract the compiled binaries to upload as artifacts and attest them
|
||||||
if: ${{ needs.skip_check.outputs.should_skip != 'true' && github.repository == 'dani-garcia/vaultwarden' }}
|
|
||||||
# Start a local docker registry to extract the final Alpine static build binaries
|
|
||||||
services:
|
services:
|
||||||
registry:
|
registry:
|
||||||
image: registry:2
|
image: registry@sha256:1fc7de654f2ac1247f0b67e8a459e273b0993be7d2beda1f3f56fbf1001ed3e7 # v3.0.0
|
||||||
ports:
|
ports:
|
||||||
- 5000:5000
|
- 5000:5000
|
||||||
env:
|
env:
|
||||||
@@ -56,37 +52,42 @@ jobs:
|
|||||||
base_image: ["debian","alpine"]
|
base_image: ["debian","alpine"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
# Checkout the repo
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 #v4.2.1
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Initialize QEMU binfmt support
|
- name: Initialize QEMU binfmt support
|
||||||
uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0
|
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.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@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1
|
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||||
# https://github.com/moby/buildkit/issues/3969
|
# https://github.com/moby/buildkit/issues/3969
|
||||||
# Also set max parallelism to 2, the default of 4 breaks GitHub Actions and causes OOMKills
|
# Also set max parallelism to 2, the default of 4 breaks GitHub Actions and causes OOMKills
|
||||||
with:
|
with:
|
||||||
|
cache-binary: false
|
||||||
buildkitd-config-inline: |
|
buildkitd-config-inline: |
|
||||||
[worker.oci]
|
[worker.oci]
|
||||||
max-parallelism = 2
|
max-parallelism = 2
|
||||||
driver-opts: |
|
driver-opts: |
|
||||||
network=host
|
network=host
|
||||||
|
|
||||||
|
# Checkout the repo
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
|
||||||
|
# We need fetch-depth of 0 so we also get all the tag metadata
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
# Determine Base Tags and Source Version
|
# Determine Base Tags and Source Version
|
||||||
- name: Determine Base Tags and Source Version
|
- name: Determine Base Tags and Source Version
|
||||||
shell: bash
|
shell: bash
|
||||||
|
env:
|
||||||
|
REF_TYPE: ${{ github.ref_type }}
|
||||||
run: |
|
run: |
|
||||||
# Check which main tag we are going to build determined by github.ref_type
|
# Check which main tag we are going to build determined by ref_type
|
||||||
if [[ "${{ github.ref_type }}" == "tag" ]]; then
|
if [[ "${REF_TYPE}" == "tag" ]]; then
|
||||||
echo "BASE_TAGS=latest,${GITHUB_REF#refs/*/}" | tee -a "${GITHUB_ENV}"
|
echo "BASE_TAGS=latest,${GITHUB_REF#refs/*/}" | tee -a "${GITHUB_ENV}"
|
||||||
elif [[ "${{ github.ref_type }}" == "branch" ]]; then
|
elif [[ "${REF_TYPE}" == "branch" ]]; then
|
||||||
echo "BASE_TAGS=testing" | tee -a "${GITHUB_ENV}"
|
echo "BASE_TAGS=testing" | tee -a "${GITHUB_ENV}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -102,7 +103,7 @@ jobs:
|
|||||||
|
|
||||||
# Login to Docker Hub
|
# Login to Docker Hub
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
@@ -111,12 +112,14 @@ jobs:
|
|||||||
- name: Add registry for DockerHub
|
- name: Add registry for DockerHub
|
||||||
if: ${{ env.HAVE_DOCKERHUB_LOGIN == 'true' }}
|
if: ${{ env.HAVE_DOCKERHUB_LOGIN == 'true' }}
|
||||||
shell: bash
|
shell: bash
|
||||||
|
env:
|
||||||
|
DOCKERHUB_REPO: ${{ vars.DOCKERHUB_REPO }}
|
||||||
run: |
|
run: |
|
||||||
echo "CONTAINER_REGISTRIES=${{ vars.DOCKERHUB_REPO }}" | tee -a "${GITHUB_ENV}"
|
echo "CONTAINER_REGISTRIES=${DOCKERHUB_REPO}" | tee -a "${GITHUB_ENV}"
|
||||||
|
|
||||||
# Login to GitHub Container Registry
|
# Login to GitHub Container Registry
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
@@ -126,12 +129,14 @@ jobs:
|
|||||||
- name: Add registry for ghcr.io
|
- name: Add registry for ghcr.io
|
||||||
if: ${{ env.HAVE_GHCR_LOGIN == 'true' }}
|
if: ${{ env.HAVE_GHCR_LOGIN == 'true' }}
|
||||||
shell: bash
|
shell: bash
|
||||||
|
env:
|
||||||
|
GHCR_REPO: ${{ vars.GHCR_REPO }}
|
||||||
run: |
|
run: |
|
||||||
echo "CONTAINER_REGISTRIES=${CONTAINER_REGISTRIES:+${CONTAINER_REGISTRIES},}${{ vars.GHCR_REPO }}" | tee -a "${GITHUB_ENV}"
|
echo "CONTAINER_REGISTRIES=${CONTAINER_REGISTRIES:+${CONTAINER_REGISTRIES},}${GHCR_REPO}" | tee -a "${GITHUB_ENV}"
|
||||||
|
|
||||||
# Login to Quay.io
|
# Login to Quay.io
|
||||||
- name: Login to Quay.io
|
- name: Login to Quay.io
|
||||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||||
with:
|
with:
|
||||||
registry: quay.io
|
registry: quay.io
|
||||||
username: ${{ secrets.QUAY_USERNAME }}
|
username: ${{ secrets.QUAY_USERNAME }}
|
||||||
@@ -141,17 +146,22 @@ jobs:
|
|||||||
- name: Add registry for Quay.io
|
- name: Add registry for Quay.io
|
||||||
if: ${{ env.HAVE_QUAY_LOGIN == 'true' }}
|
if: ${{ env.HAVE_QUAY_LOGIN == 'true' }}
|
||||||
shell: bash
|
shell: bash
|
||||||
|
env:
|
||||||
|
QUAY_REPO: ${{ vars.QUAY_REPO }}
|
||||||
run: |
|
run: |
|
||||||
echo "CONTAINER_REGISTRIES=${CONTAINER_REGISTRIES:+${CONTAINER_REGISTRIES},}${{ vars.QUAY_REPO }}" | tee -a "${GITHUB_ENV}"
|
echo "CONTAINER_REGISTRIES=${CONTAINER_REGISTRIES:+${CONTAINER_REGISTRIES},}${QUAY_REPO}" | tee -a "${GITHUB_ENV}"
|
||||||
|
|
||||||
- name: Configure build cache from/to
|
- name: Configure build cache from/to
|
||||||
shell: bash
|
shell: bash
|
||||||
|
env:
|
||||||
|
GHCR_REPO: ${{ vars.GHCR_REPO }}
|
||||||
|
BASE_IMAGE: ${{ matrix.base_image }}
|
||||||
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 "${HAVE_GHCR_LOGIN}" ]]; then
|
||||||
echo "BAKE_CACHE_FROM=type=registry,ref=${{ vars.GHCR_REPO }}-buildcache:${{ matrix.base_image }}" | tee -a "${GITHUB_ENV}"
|
echo "BAKE_CACHE_FROM=type=registry,ref=${GHCR_REPO}-buildcache:${BASE_IMAGE}" | tee -a "${GITHUB_ENV}"
|
||||||
echo "BAKE_CACHE_TO=type=registry,ref=${{ vars.GHCR_REPO }}-buildcache:${{ matrix.base_image }},compression=zstd,mode=max" | tee -a "${GITHUB_ENV}"
|
echo "BAKE_CACHE_TO=type=registry,ref=${GHCR_REPO}-buildcache:${BASE_IMAGE},compression=zstd,mode=max" | tee -a "${GITHUB_ENV}"
|
||||||
else
|
else
|
||||||
echo "BAKE_CACHE_FROM="
|
echo "BAKE_CACHE_FROM="
|
||||||
echo "BAKE_CACHE_TO="
|
echo "BAKE_CACHE_TO="
|
||||||
@@ -159,13 +169,13 @@ jobs:
|
|||||||
#
|
#
|
||||||
|
|
||||||
- name: Add localhost registry
|
- name: Add localhost registry
|
||||||
if: ${{ matrix.base_image == 'alpine' }}
|
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
echo "CONTAINER_REGISTRIES=${CONTAINER_REGISTRIES:+${CONTAINER_REGISTRIES},}localhost:5000/vaultwarden/server" | tee -a "${GITHUB_ENV}"
|
echo "CONTAINER_REGISTRIES=${CONTAINER_REGISTRIES:+${CONTAINER_REGISTRIES},}localhost:5000/vaultwarden/server" | tee -a "${GITHUB_ENV}"
|
||||||
|
|
||||||
- name: Bake ${{ matrix.base_image }} containers
|
- name: Bake ${{ matrix.base_image }} containers
|
||||||
uses: docker/bake-action@2e3d19baedb14545e5d41222653874f25d5b4dfb # v5.10.0
|
id: bake_vw
|
||||||
|
uses: docker/bake-action@3acf805d94d93a86cce4ca44798a76464a75b88c # v6.9.0
|
||||||
env:
|
env:
|
||||||
BASE_TAGS: "${{ env.BASE_TAGS }}"
|
BASE_TAGS: "${{ env.BASE_TAGS }}"
|
||||||
SOURCE_COMMIT: "${{ env.SOURCE_COMMIT }}"
|
SOURCE_COMMIT: "${{ env.SOURCE_COMMIT }}"
|
||||||
@@ -175,78 +185,121 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
pull: true
|
pull: true
|
||||||
push: true
|
push: true
|
||||||
|
source: .
|
||||||
files: docker/docker-bake.hcl
|
files: docker/docker-bake.hcl
|
||||||
targets: "${{ matrix.base_image }}-multi"
|
targets: "${{ matrix.base_image }}-multi"
|
||||||
set: |
|
set: |
|
||||||
*.cache-from=${{ env.BAKE_CACHE_FROM }}
|
*.cache-from=${{ env.BAKE_CACHE_FROM }}
|
||||||
*.cache-to=${{ env.BAKE_CACHE_TO }}
|
*.cache-to=${{ env.BAKE_CACHE_TO }}
|
||||||
|
|
||||||
|
- name: Extract digest SHA
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
BAKE_METADATA: ${{ steps.bake_vw.outputs.metadata }}
|
||||||
|
BASE_IMAGE: ${{ matrix.base_image }}
|
||||||
|
run: |
|
||||||
|
GET_DIGEST_SHA="$(jq -r --arg base "$BASE_IMAGE" '.[$base + "-multi"]."containerimage.digest"' <<< "${BAKE_METADATA}")"
|
||||||
|
echo "DIGEST_SHA=${GET_DIGEST_SHA}" | tee -a "${GITHUB_ENV}"
|
||||||
|
|
||||||
|
# Attest container images
|
||||||
|
- name: Attest - docker.io - ${{ matrix.base_image }}
|
||||||
|
if: ${{ env.HAVE_DOCKERHUB_LOGIN == 'true' && steps.bake_vw.outputs.metadata != ''}}
|
||||||
|
uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0
|
||||||
|
with:
|
||||||
|
subject-name: ${{ vars.DOCKERHUB_REPO }}
|
||||||
|
subject-digest: ${{ env.DIGEST_SHA }}
|
||||||
|
push-to-registry: true
|
||||||
|
|
||||||
|
- name: Attest - ghcr.io - ${{ matrix.base_image }}
|
||||||
|
if: ${{ env.HAVE_GHCR_LOGIN == 'true' && steps.bake_vw.outputs.metadata != ''}}
|
||||||
|
uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0
|
||||||
|
with:
|
||||||
|
subject-name: ${{ vars.GHCR_REPO }}
|
||||||
|
subject-digest: ${{ env.DIGEST_SHA }}
|
||||||
|
push-to-registry: true
|
||||||
|
|
||||||
|
- name: Attest - quay.io - ${{ matrix.base_image }}
|
||||||
|
if: ${{ env.HAVE_QUAY_LOGIN == 'true' && steps.bake_vw.outputs.metadata != ''}}
|
||||||
|
uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0
|
||||||
|
with:
|
||||||
|
subject-name: ${{ vars.QUAY_REPO }}
|
||||||
|
subject-digest: ${{ env.DIGEST_SHA }}
|
||||||
|
push-to-registry: true
|
||||||
|
|
||||||
|
|
||||||
# Extract the Alpine binaries from the containers
|
# Extract the Alpine binaries from the containers
|
||||||
- name: Extract binaries
|
- name: Extract binaries
|
||||||
if: ${{ matrix.base_image == 'alpine' }}
|
|
||||||
shell: bash
|
shell: bash
|
||||||
|
env:
|
||||||
|
REF_TYPE: ${{ github.ref_type }}
|
||||||
|
BASE_IMAGE: ${{ matrix.base_image }}
|
||||||
run: |
|
run: |
|
||||||
# Check which main tag we are going to build determined by github.ref_type
|
# Check which main tag we are going to build determined by ref_type
|
||||||
if [[ "${{ github.ref_type }}" == "tag" ]]; then
|
if [[ "${REF_TYPE}" == "tag" ]]; then
|
||||||
EXTRACT_TAG="latest"
|
EXTRACT_TAG="latest"
|
||||||
elif [[ "${{ github.ref_type }}" == "branch" ]]; then
|
elif [[ "${REF_TYPE}" == "branch" ]]; then
|
||||||
EXTRACT_TAG="testing"
|
EXTRACT_TAG="testing"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Check which base_image was used and append -alpine if needed
|
||||||
|
if [[ "${BASE_IMAGE}" == "alpine" ]]; then
|
||||||
|
EXTRACT_TAG="${EXTRACT_TAG}-alpine"
|
||||||
|
fi
|
||||||
|
|
||||||
# After each extraction the image is removed.
|
# After each extraction the image is removed.
|
||||||
# This is needed because using different platforms doesn't trigger a new pull/download
|
# This is needed because using different platforms doesn't trigger a new pull/download
|
||||||
|
|
||||||
# Extract amd64 binary
|
# Extract amd64 binary
|
||||||
docker create --name amd64 --platform=linux/amd64 "localhost:5000/vaultwarden/server:${EXTRACT_TAG}-alpine"
|
docker create --name amd64 --platform=linux/amd64 "localhost:5000/vaultwarden/server:${EXTRACT_TAG}"
|
||||||
docker cp amd64:/vaultwarden vaultwarden-amd64
|
docker cp amd64:/vaultwarden vaultwarden-amd64-${BASE_IMAGE}
|
||||||
docker rm --force amd64
|
docker rm --force amd64
|
||||||
docker rmi --force "localhost:5000/vaultwarden/server:${EXTRACT_TAG}-alpine"
|
docker rmi --force "localhost:5000/vaultwarden/server:${EXTRACT_TAG}"
|
||||||
|
|
||||||
# Extract arm64 binary
|
# Extract arm64 binary
|
||||||
docker create --name arm64 --platform=linux/arm64 "localhost:5000/vaultwarden/server:${EXTRACT_TAG}-alpine"
|
docker create --name arm64 --platform=linux/arm64 "localhost:5000/vaultwarden/server:${EXTRACT_TAG}"
|
||||||
docker cp arm64:/vaultwarden vaultwarden-arm64
|
docker cp arm64:/vaultwarden vaultwarden-arm64-${BASE_IMAGE}
|
||||||
docker rm --force arm64
|
docker rm --force arm64
|
||||||
docker rmi --force "localhost:5000/vaultwarden/server:${EXTRACT_TAG}-alpine"
|
docker rmi --force "localhost:5000/vaultwarden/server:${EXTRACT_TAG}"
|
||||||
|
|
||||||
# Extract armv7 binary
|
# Extract armv7 binary
|
||||||
docker create --name armv7 --platform=linux/arm/v7 "localhost:5000/vaultwarden/server:${EXTRACT_TAG}-alpine"
|
docker create --name armv7 --platform=linux/arm/v7 "localhost:5000/vaultwarden/server:${EXTRACT_TAG}"
|
||||||
docker cp armv7:/vaultwarden vaultwarden-armv7
|
docker cp armv7:/vaultwarden vaultwarden-armv7-${BASE_IMAGE}
|
||||||
docker rm --force armv7
|
docker rm --force armv7
|
||||||
docker rmi --force "localhost:5000/vaultwarden/server:${EXTRACT_TAG}-alpine"
|
docker rmi --force "localhost:5000/vaultwarden/server:${EXTRACT_TAG}"
|
||||||
|
|
||||||
# Extract armv6 binary
|
# Extract armv6 binary
|
||||||
docker create --name armv6 --platform=linux/arm/v6 "localhost:5000/vaultwarden/server:${EXTRACT_TAG}-alpine"
|
docker create --name armv6 --platform=linux/arm/v6 "localhost:5000/vaultwarden/server:${EXTRACT_TAG}"
|
||||||
docker cp armv6:/vaultwarden vaultwarden-armv6
|
docker cp armv6:/vaultwarden vaultwarden-armv6-${BASE_IMAGE}
|
||||||
docker rm --force armv6
|
docker rm --force armv6
|
||||||
docker rmi --force "localhost:5000/vaultwarden/server:${EXTRACT_TAG}-alpine"
|
docker rmi --force "localhost:5000/vaultwarden/server:${EXTRACT_TAG}"
|
||||||
|
|
||||||
# Upload artifacts to Github Actions
|
# Upload artifacts to Github Actions and Attest the binaries
|
||||||
- name: "Upload amd64 artifact"
|
- name: "Upload amd64 artifact ${{ matrix.base_image }}"
|
||||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||||
if: ${{ matrix.base_image == 'alpine' }}
|
|
||||||
with:
|
with:
|
||||||
name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-amd64
|
name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-amd64-${{ matrix.base_image }}
|
||||||
path: vaultwarden-amd64
|
path: vaultwarden-amd64-${{ matrix.base_image }}
|
||||||
|
|
||||||
- name: "Upload arm64 artifact"
|
- name: "Upload arm64 artifact ${{ matrix.base_image }}"
|
||||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||||
if: ${{ matrix.base_image == 'alpine' }}
|
|
||||||
with:
|
with:
|
||||||
name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-arm64
|
name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-arm64-${{ matrix.base_image }}
|
||||||
path: vaultwarden-arm64
|
path: vaultwarden-arm64-${{ matrix.base_image }}
|
||||||
|
|
||||||
- name: "Upload armv7 artifact"
|
- name: "Upload armv7 artifact ${{ matrix.base_image }}"
|
||||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||||
if: ${{ matrix.base_image == 'alpine' }}
|
|
||||||
with:
|
with:
|
||||||
name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-armv7
|
name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-armv7-${{ matrix.base_image }}
|
||||||
path: vaultwarden-armv7
|
path: vaultwarden-armv7-${{ matrix.base_image }}
|
||||||
|
|
||||||
- name: "Upload armv6 artifact"
|
- name: "Upload armv6 artifact ${{ matrix.base_image }}"
|
||||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||||
if: ${{ matrix.base_image == 'alpine' }}
|
|
||||||
with:
|
with:
|
||||||
name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-armv6
|
name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-armv6-${{ matrix.base_image }}
|
||||||
path: vaultwarden-armv6
|
path: vaultwarden-armv6-${{ matrix.base_image }}
|
||||||
|
|
||||||
|
- name: "Attest artifacts ${{ matrix.base_image }}"
|
||||||
|
uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0
|
||||||
|
with:
|
||||||
|
subject-path: vaultwarden-*
|
||||||
# End Upload artifacts to Github Actions
|
# End Upload artifacts to Github Actions
|
||||||
|
6
.github/workflows/releasecache-cleanup.yml
vendored
6
.github/workflows/releasecache-cleanup.yml
vendored
@@ -1,3 +1,6 @@
|
|||||||
|
name: Cleanup
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
@@ -9,10 +12,11 @@ on:
|
|||||||
schedule:
|
schedule:
|
||||||
- cron: '0 1 * * FRI'
|
- cron: '0 1 * * FRI'
|
||||||
|
|
||||||
name: Cleanup
|
|
||||||
jobs:
|
jobs:
|
||||||
releasecache-cleanup:
|
releasecache-cleanup:
|
||||||
name: Releasecache Cleanup
|
name: Releasecache Cleanup
|
||||||
|
permissions:
|
||||||
|
packages: write
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
|
34
.github/workflows/trivy.yml
vendored
34
.github/workflows/trivy.yml
vendored
@@ -1,37 +1,45 @@
|
|||||||
name: trivy
|
name: Trivy
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
|
||||||
tags:
|
tags:
|
||||||
- '*'
|
- '*'
|
||||||
|
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ "main" ]
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '08 11 * * *'
|
- cron: '08 11 * * *'
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
trivy-scan:
|
trivy-scan:
|
||||||
# Only run this in the master repo and not on forks
|
# Only run this in the upstream repo and not on forks
|
||||||
# When all forks run this at the same time, it is causing `Too Many Requests` issues
|
# When all forks run this at the same time, it is causing `Too Many Requests` issues
|
||||||
if: ${{ github.repository == 'dani-garcia/vaultwarden' }}
|
if: ${{ github.repository == 'dani-garcia/vaultwarden' }}
|
||||||
name: Check
|
name: Trivy Scan
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
timeout-minutes: 30
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
security-events: write
|
|
||||||
actions: read
|
actions: read
|
||||||
|
security-events: write
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
timeout-minutes: 30
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 #v4.2.1
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Run Trivy vulnerability scanner
|
- name: Run Trivy vulnerability scanner
|
||||||
uses: aquasecurity/trivy-action@5681af892cd0f4997658e2bacc62bd0a894cf564 # v0.27.0
|
uses: aquasecurity/trivy-action@dc5a429b52fcf669ce959baa2c2dd26090d2a6c4 # v0.32.0
|
||||||
|
env:
|
||||||
|
TRIVY_DB_REPOSITORY: docker.io/aquasec/trivy-db:2,public.ecr.aws/aquasecurity/trivy-db:2,ghcr.io/aquasecurity/trivy-db:2
|
||||||
|
TRIVY_JAVA_DB_REPOSITORY: docker.io/aquasec/trivy-java-db:1,public.ecr.aws/aquasecurity/trivy-java-db:1,ghcr.io/aquasecurity/trivy-java-db:1
|
||||||
with:
|
with:
|
||||||
scan-type: repo
|
scan-type: repo
|
||||||
ignore-unfixed: true
|
ignore-unfixed: true
|
||||||
@@ -40,6 +48,6 @@ jobs:
|
|||||||
severity: CRITICAL,HIGH
|
severity: CRITICAL,HIGH
|
||||||
|
|
||||||
- name: Upload Trivy scan results to GitHub Security tab
|
- name: Upload Trivy scan results to GitHub Security tab
|
||||||
uses: github/codeql-action/upload-sarif@2bbafcdd7fbf96243689e764c2f15d9735164f33 # v3.26.6
|
uses: github/codeql-action/upload-sarif@3c3833e0f8c1c83d449a7478aa59c036a9165498 # v3.29.11
|
||||||
with:
|
with:
|
||||||
sarif_file: 'trivy-results.sarif'
|
sarif_file: 'trivy-results.sarif'
|
||||||
|
28
.github/workflows/zizmor.yml
vendored
Normal file
28
.github/workflows/zizmor.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
name: Security Analysis with zizmor
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["main"]
|
||||||
|
pull_request:
|
||||||
|
branches: ["**"]
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
zizmor:
|
||||||
|
name: Run zizmor
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
security-events: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Run zizmor
|
||||||
|
uses: zizmorcore/zizmor-action@5ca5fc7a4779c5263a3ffa0e1f693009994446d1 # v0.1.2
|
||||||
|
with:
|
||||||
|
# intentionally not scanning the entire repository,
|
||||||
|
# since it contains integration tests.
|
||||||
|
inputs: ./.github/
|
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v4.6.0
|
rev: v6.0.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
- id: check-json
|
- id: check-json
|
||||||
@@ -31,7 +31,7 @@ repos:
|
|||||||
language: system
|
language: system
|
||||||
args: ["--features", "sqlite,mysql,postgresql,enable_mimalloc", "--"]
|
args: ["--features", "sqlite,mysql,postgresql,enable_mimalloc", "--"]
|
||||||
types_or: [rust, file]
|
types_or: [rust, file]
|
||||||
files: (Cargo.toml|Cargo.lock|rust-toolchain|.*\.rs$)
|
files: (Cargo.toml|Cargo.lock|rust-toolchain.toml|rustfmt.toml|.*\.rs$)
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
- id: cargo-clippy
|
- id: cargo-clippy
|
||||||
name: cargo clippy
|
name: cargo clippy
|
||||||
@@ -40,5 +40,13 @@ repos:
|
|||||||
language: system
|
language: system
|
||||||
args: ["--features", "sqlite,mysql,postgresql,enable_mimalloc", "--", "-D", "warnings"]
|
args: ["--features", "sqlite,mysql,postgresql,enable_mimalloc", "--", "-D", "warnings"]
|
||||||
types_or: [rust, file]
|
types_or: [rust, file]
|
||||||
files: (Cargo.toml|Cargo.lock|rust-toolchain|clippy.toml|.*\.rs$)
|
files: (Cargo.toml|Cargo.lock|rust-toolchain.toml|rustfmt.toml|.*\.rs$)
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
|
- id: check-docker-templates
|
||||||
|
name: check docker templates
|
||||||
|
description: Check if the Docker templates are updated
|
||||||
|
language: system
|
||||||
|
entry: sh
|
||||||
|
args:
|
||||||
|
- "-c"
|
||||||
|
- "cd docker && make"
|
||||||
|
3884
Cargo.lock
generated
3884
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
150
Cargo.toml
150
Cargo.toml
@@ -1,9 +1,12 @@
|
|||||||
|
[workspace]
|
||||||
|
members = ["macros"]
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
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>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.80.0"
|
rust-version = "1.87.0"
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
repository = "https://github.com/dani-garcia/vaultwarden"
|
repository = "https://github.com/dani-garcia/vaultwarden"
|
||||||
@@ -29,6 +32,11 @@ enable_mimalloc = ["dep:mimalloc"]
|
|||||||
# You also need to set an env variable `QUERY_LOGGER=1` to fully activate this so you do not have to re-compile
|
# You also need to set an env variable `QUERY_LOGGER=1` to fully activate this so you do not have to re-compile
|
||||||
# if you want to turn off the logging for a specific run.
|
# if you want to turn off the logging for a specific run.
|
||||||
query_logger = ["dep:diesel_logger"]
|
query_logger = ["dep:diesel_logger"]
|
||||||
|
s3 = ["opendal/services-s3", "dep:aws-config", "dep:aws-credential-types", "dep:aws-smithy-runtime-api", "dep:anyhow", "dep:http", "dep:reqsign"]
|
||||||
|
|
||||||
|
# OIDC specific features
|
||||||
|
oidc-accept-rfc3339-timestamps = ["openidconnect/accept-rfc3339-timestamps"]
|
||||||
|
oidc-accept-string-booleans = ["openidconnect/accept-string-booleans"]
|
||||||
|
|
||||||
# Enable unstable features, requires nightly
|
# Enable unstable features, requires nightly
|
||||||
# Currently only used to enable rusts official ip support
|
# Currently only used to enable rusts official ip support
|
||||||
@@ -36,24 +44,26 @@ unstable = []
|
|||||||
|
|
||||||
[target."cfg(unix)".dependencies]
|
[target."cfg(unix)".dependencies]
|
||||||
# Logging
|
# Logging
|
||||||
syslog = "6.1.1"
|
syslog = "7.0.0"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
macros = { path = "./macros" }
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
log = "0.4.22"
|
log = "0.4.27"
|
||||||
fern = { version = "0.7.0", features = ["syslog-6", "reopen-1"] }
|
fern = { version = "0.7.1", features = ["syslog-7", "reopen-1"] }
|
||||||
tracing = { version = "0.1.40", features = ["log"] } # Needed to have lettre and webauthn-rs trace logging to work
|
tracing = { version = "0.1.41", features = ["log"] } # Needed to have lettre and webauthn-rs trace logging to work
|
||||||
|
|
||||||
# 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 }
|
||||||
|
|
||||||
# Lazy initialization
|
# Lazy initialization
|
||||||
once_cell = "1.20.2"
|
once_cell = "1.21.3"
|
||||||
|
|
||||||
# Numerical libraries
|
# Numerical libraries
|
||||||
num-traits = "0.2.19"
|
num-traits = "0.2.19"
|
||||||
num-derive = "0.4.2"
|
num-derive = "0.4.2"
|
||||||
bigdecimal = "0.4.5"
|
bigdecimal = "0.4.8"
|
||||||
|
|
||||||
# Web framework
|
# Web framework
|
||||||
rocket = { version = "0.5.1", features = ["tls", "json"], default-features = false }
|
rocket = { version = "0.5.1", features = ["tls", "json"], default-features = false }
|
||||||
@@ -67,104 +77,133 @@ dashmap = "6.1.0"
|
|||||||
|
|
||||||
# Async futures
|
# Async futures
|
||||||
futures = "0.3.31"
|
futures = "0.3.31"
|
||||||
tokio = { version = "1.41.0", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] }
|
tokio = { version = "1.47.1", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] }
|
||||||
|
tokio-util = { version = "0.7.16", features = ["compat"]}
|
||||||
|
|
||||||
# A generic serialization/deserialization framework
|
# A generic serialization/deserialization framework
|
||||||
serde = { version = "1.0.213", features = ["derive"] }
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
serde_json = "1.0.132"
|
serde_json = "1.0.143"
|
||||||
|
|
||||||
# A safe, extensible ORM and Query builder
|
# A safe, extensible ORM and Query builder
|
||||||
diesel = { version = "2.2.4", features = ["chrono", "r2d2", "numeric"] }
|
diesel = { version = "2.2.12", features = ["chrono", "r2d2", "numeric"] }
|
||||||
diesel_migrations = "2.2.0"
|
diesel_migrations = "2.2.0"
|
||||||
diesel_logger = { version = "0.3.0", optional = true }
|
diesel_logger = { version = "0.4.0", optional = true }
|
||||||
|
|
||||||
|
derive_more = { version = "2.0.1", features = ["from", "into", "as_ref", "deref", "display"] }
|
||||||
|
diesel-derive-newtype = "2.1.2"
|
||||||
|
|
||||||
# Bundled/Static SQLite
|
# Bundled/Static SQLite
|
||||||
libsqlite3-sys = { version = "0.30.1", features = ["bundled"], optional = true }
|
libsqlite3-sys = { version = "0.35.0", features = ["bundled"], optional = true }
|
||||||
|
|
||||||
# Crypto-related libraries
|
# Crypto-related libraries
|
||||||
rand = { version = "0.8.5", features = ["small_rng"] }
|
rand = "0.9.2"
|
||||||
ring = "0.17.8"
|
ring = "0.17.14"
|
||||||
|
subtle = "2.6.1"
|
||||||
|
|
||||||
# UUID generation
|
# UUID generation
|
||||||
uuid = { version = "1.11.0", features = ["v4"] }
|
uuid = { version = "1.18.0", features = ["v4"] }
|
||||||
|
|
||||||
# Date and time libraries
|
# Date and time libraries
|
||||||
chrono = { version = "0.4.38", features = ["clock", "serde"], default-features = false }
|
chrono = { version = "0.4.41", features = ["clock", "serde"], default-features = false }
|
||||||
chrono-tz = "0.10.0"
|
chrono-tz = "0.10.4"
|
||||||
time = "0.3.36"
|
time = "0.3.41"
|
||||||
|
|
||||||
# Job scheduler
|
# Job scheduler
|
||||||
job_scheduler_ng = "2.0.5"
|
job_scheduler_ng = "2.3.0"
|
||||||
|
|
||||||
# Data encoding library Hex/Base32/Base64
|
# Data encoding library Hex/Base32/Base64
|
||||||
data-encoding = "2.6.0"
|
data-encoding = "2.9.0"
|
||||||
|
|
||||||
# JWT library
|
# JWT library
|
||||||
jsonwebtoken = "9.3.0"
|
jsonwebtoken = "9.3.1"
|
||||||
|
|
||||||
# TOTP library
|
# TOTP library
|
||||||
totp-lite = "2.0.1"
|
totp-lite = "2.0.1"
|
||||||
|
|
||||||
# Yubico Library
|
# Yubico Library
|
||||||
yubico = { version = "0.11.0", features = ["online-tokio"], default-features = false }
|
yubico = { package = "yubico_ng", version = "0.14.1", features = ["online-tokio"], default-features = false }
|
||||||
|
|
||||||
# WebAuthn libraries
|
# WebAuthn libraries
|
||||||
webauthn-rs = "0.3.2"
|
# danger-allow-state-serialisation is needed to save the state in the db
|
||||||
|
# danger-credential-internals is needed to support U2F to Webauthn migration
|
||||||
|
webauthn-rs = { version = "0.5.2", features = ["danger-allow-state-serialisation", "danger-credential-internals"] }
|
||||||
|
webauthn-rs-proto = "0.5.2"
|
||||||
|
webauthn-rs-core = "0.5.2"
|
||||||
|
|
||||||
# Handling of URL's for WebAuthn and favicons
|
# Handling of URL's for WebAuthn and favicons
|
||||||
url = "2.5.2"
|
url = "2.5.7"
|
||||||
|
|
||||||
# Email libraries
|
# Email libraries
|
||||||
lettre = { version = "0.11.10", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "tokio1-native-tls", "hostname", "tracing", "tokio1"], default-features = false }
|
lettre = { version = "0.11.18", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "hostname", "tracing", "tokio1-rustls", "ring", "rustls-native-certs"], default-features = false }
|
||||||
percent-encoding = "2.3.1" # URL encoding library used for URL's in the emails
|
percent-encoding = "2.3.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.1.0", features = ["dir_source"] }
|
handlebars = { version = "6.3.2", features = ["dir_source"] }
|
||||||
|
|
||||||
# HTTP client (Used for favicons, version check, DUO and HIBP API)
|
# HTTP client (Used for favicons, version check, DUO and HIBP API)
|
||||||
reqwest = { version = "0.12.8", features = ["native-tls-alpn", "stream", "json", "gzip", "brotli", "socks", "cookies"] }
|
reqwest = { version = "0.12.23", features = ["rustls-tls", "rustls-tls-native-roots", "stream", "json", "deflate", "gzip", "brotli", "zstd", "socks", "cookies", "charset", "http2", "system-proxy"], default-features = false}
|
||||||
hickory-resolver = "0.24.1"
|
hickory-resolver = "0.25.2"
|
||||||
|
|
||||||
# Favicon extraction libraries
|
# Favicon extraction libraries
|
||||||
html5gum = "0.5.7"
|
html5gum = "0.8.0"
|
||||||
regex = { version = "1.11.0", features = ["std", "perf", "unicode-perl"], default-features = false }
|
regex = { version = "1.11.2", features = ["std", "perf", "unicode-perl"], default-features = false }
|
||||||
data-url = "0.3.1"
|
data-url = "0.3.2"
|
||||||
bytes = "1.8.0"
|
bytes = "1.10.1"
|
||||||
|
svg-hush = "0.9.5"
|
||||||
|
|
||||||
# Cache function results (Used for version check and favicon fetching)
|
# Cache function results (Used for version check and favicon fetching)
|
||||||
cached = { version = "0.53.1", features = ["async"] }
|
cached = { version = "0.56.0", features = ["async"] }
|
||||||
|
|
||||||
# Used for custom short lived cookie jar during favicon extraction
|
# Used for custom short lived cookie jar during favicon extraction
|
||||||
cookie = "0.18.1"
|
cookie = "0.18.1"
|
||||||
cookie_store = "0.21.0"
|
cookie_store = "0.21.1"
|
||||||
|
|
||||||
# Used by U2F, JWT and PostgreSQL
|
# Used by U2F, JWT and PostgreSQL
|
||||||
openssl = "0.10.68"
|
openssl = "0.10.73"
|
||||||
|
|
||||||
# CLI argument parsing
|
# CLI argument parsing
|
||||||
pico-args = "0.5.0"
|
pico-args = "0.5.0"
|
||||||
|
|
||||||
# Macro ident concatenation
|
# Macro ident concatenation
|
||||||
paste = "1.0.15"
|
pastey = "0.1.1"
|
||||||
governor = "0.7.0"
|
governor = "0.10.1"
|
||||||
|
|
||||||
|
# OIDC for SSO
|
||||||
|
openidconnect = { version = "4.0.1", features = ["reqwest", "native-tls"] }
|
||||||
|
mini-moka = "0.10.3"
|
||||||
|
|
||||||
# Check client versions for specific features.
|
# Check client versions for specific features.
|
||||||
semver = "1.0.23"
|
semver = "1.0.26"
|
||||||
|
|
||||||
# Allow overriding the default memory allocator
|
# Allow overriding the default memory allocator
|
||||||
# Mainly used for the musl builds, since the default musl malloc is very slow
|
# Mainly used for the musl builds, since the default musl malloc is very slow
|
||||||
mimalloc = { version = "0.1.43", features = ["secure"], default-features = false, optional = true }
|
mimalloc = { version = "0.1.48", features = ["secure"], default-features = false, optional = true }
|
||||||
which = "6.0.3"
|
|
||||||
|
which = "8.0.0"
|
||||||
|
|
||||||
# Argon2 library with support for the PHC format
|
# Argon2 library with support for the PHC format
|
||||||
argon2 = "0.5.3"
|
argon2 = "0.5.3"
|
||||||
|
|
||||||
# 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.3.1"
|
rpassword = "7.4.0"
|
||||||
|
|
||||||
|
# Loading a dynamic CSS Stylesheet
|
||||||
|
grass_compiler = { version = "0.13.4", default-features = false }
|
||||||
|
|
||||||
|
# File are accessed through Apache OpenDAL
|
||||||
|
opendal = { version = "0.54.0", features = ["services-fs"], default-features = false }
|
||||||
|
|
||||||
|
# For retrieving AWS credentials, including temporary SSO credentials
|
||||||
|
anyhow = { version = "1.0.99", optional = true }
|
||||||
|
aws-config = { version = "1.8.5", features = ["behavior-version-latest", "rt-tokio", "credentials-process", "sso"], default-features = false, optional = true }
|
||||||
|
aws-credential-types = { version = "1.2.5", optional = true }
|
||||||
|
aws-smithy-runtime-api = { version = "1.9.0", optional = true }
|
||||||
|
http = { version = "1.3.1", optional = true }
|
||||||
|
reqsign = { version = "0.16.5", optional = true }
|
||||||
|
|
||||||
# Strip debuginfo from the release builds
|
# Strip debuginfo from the release builds
|
||||||
# The symbols are the provide better panic traces
|
# The debug symbols are to provide better panic traces
|
||||||
# Also enable fat LTO and use 1 codegen unit for optimizations
|
# Also enable fat LTO and use 1 codegen unit for optimizations
|
||||||
[profile.release]
|
[profile.release]
|
||||||
strip = "debuginfo"
|
strip = "debuginfo"
|
||||||
@@ -199,7 +238,7 @@ codegen-units = 16
|
|||||||
|
|
||||||
# Linting config
|
# Linting config
|
||||||
# https://doc.rust-lang.org/rustc/lints/groups.html
|
# https://doc.rust-lang.org/rustc/lints/groups.html
|
||||||
[lints.rust]
|
[workspace.lints.rust]
|
||||||
# Forbid
|
# Forbid
|
||||||
unsafe_code = "forbid"
|
unsafe_code = "forbid"
|
||||||
non_ascii_idents = "forbid"
|
non_ascii_idents = "forbid"
|
||||||
@@ -213,7 +252,8 @@ 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 } # Enable once we are at MSRV 1.81.0
|
rust_2024_compatibility = { level = "deny", priority = -1 }
|
||||||
|
edition_2024_expr_fragment_specifier = "allow" # Once changed to Rust 2024 this should be removed and macro's should be validated again
|
||||||
single_use_lifetimes = "deny"
|
single_use_lifetimes = "deny"
|
||||||
trivial_casts = "deny"
|
trivial_casts = "deny"
|
||||||
trivial_numeric_casts = "deny"
|
trivial_numeric_casts = "deny"
|
||||||
@@ -222,16 +262,20 @@ unused_import_braces = "deny"
|
|||||||
unused_lifetimes = "deny"
|
unused_lifetimes = "deny"
|
||||||
unused_qualifications = "deny"
|
unused_qualifications = "deny"
|
||||||
variant_size_differences = "deny"
|
variant_size_differences = "deny"
|
||||||
# The lints below are part of the rust_2024_compatibility group
|
# Allow the following lints since these cause issues with Rust v1.84.0 or newer
|
||||||
static-mut-refs = "deny"
|
# Building Vaultwarden with Rust v1.85.0 and edition 2024 also works without issues
|
||||||
unsafe-op-in-unsafe-fn = "deny"
|
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
|
||||||
[lints.clippy]
|
[workspace.lints.clippy]
|
||||||
# Warn
|
# Warn
|
||||||
dbg_macro = "warn"
|
dbg_macro = "warn"
|
||||||
todo = "warn"
|
todo = "warn"
|
||||||
|
|
||||||
|
# Ignore/Allow
|
||||||
|
result_large_err = "allow"
|
||||||
|
|
||||||
# Deny
|
# Deny
|
||||||
case_sensitive_file_extension_comparisons = "deny"
|
case_sensitive_file_extension_comparisons = "deny"
|
||||||
cast_lossless = "deny"
|
cast_lossless = "deny"
|
||||||
@@ -239,6 +283,7 @@ clone_on_ref_ptr = "deny"
|
|||||||
equatable_if_let = "deny"
|
equatable_if_let = "deny"
|
||||||
filter_map_next = "deny"
|
filter_map_next = "deny"
|
||||||
float_cmp_const = "deny"
|
float_cmp_const = "deny"
|
||||||
|
implicit_clone = "deny"
|
||||||
inefficient_to_string = "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"
|
||||||
@@ -247,14 +292,12 @@ macro_use_imports = "deny"
|
|||||||
manual_assert = "deny"
|
manual_assert = "deny"
|
||||||
manual_instant_elapsed = "deny"
|
manual_instant_elapsed = "deny"
|
||||||
manual_string_new = "deny"
|
manual_string_new = "deny"
|
||||||
match_on_vec_items = "deny"
|
|
||||||
match_wildcard_for_single_variants = "deny"
|
match_wildcard_for_single_variants = "deny"
|
||||||
mem_forget = "deny"
|
mem_forget = "deny"
|
||||||
needless_continue = "deny"
|
needless_continue = "deny"
|
||||||
needless_lifetimes = "deny"
|
needless_lifetimes = "deny"
|
||||||
option_option = "deny"
|
option_option = "deny"
|
||||||
string_add_assign = "deny"
|
string_add_assign = "deny"
|
||||||
string_to_string = "deny"
|
|
||||||
unnecessary_join = "deny"
|
unnecessary_join = "deny"
|
||||||
unnecessary_self_imports = "deny"
|
unnecessary_self_imports = "deny"
|
||||||
unnested_or_patterns = "deny"
|
unnested_or_patterns = "deny"
|
||||||
@@ -262,3 +305,6 @@ unused_async = "deny"
|
|||||||
unused_self = "deny"
|
unused_self = "deny"
|
||||||
verbose_file_reads = "deny"
|
verbose_file_reads = "deny"
|
||||||
zero_sized_map_values = "deny"
|
zero_sized_map_values = "deny"
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
204
README.md
204
README.md
@@ -1,102 +1,146 @@
|
|||||||
### Alternative implementation of the Bitwarden server API written in Rust and compatible with [upstream Bitwarden clients](https://bitwarden.com/download/)*, perfect for self-hosted deployment where running the official resource-heavy service might not be ideal.
|

|
||||||
|
|
||||||
📢 Note: This project was known as Bitwarden_RS and has been renamed to separate itself from the official Bitwarden server in the hopes of avoiding confusion and trademark/branding issues. Please see [#1642](https://github.com/dani-garcia/vaultwarden/discussions/1642) for more explanation.
|
An alternative server implementation of the Bitwarden Client API, written in Rust and compatible with [official Bitwarden clients](https://bitwarden.com/download/) [[disclaimer](#disclaimer)], perfect for self-hosted deployment where running the official resource-heavy service might not be ideal.
|
||||||
|
|
||||||
---
|
---
|
||||||
[](https://github.com/dani-garcia/vaultwarden/actions/workflows/build.yml)
|
|
||||||
[](https://github.com/dani-garcia/vaultwarden/pkgs/container/vaultwarden)
|
|
||||||
[](https://hub.docker.com/r/vaultwarden/server)
|
|
||||||
[](https://quay.io/repository/vaultwarden/server)
|
|
||||||
[](https://deps.rs/repo/github/dani-garcia/vaultwarden)
|
|
||||||
[](https://github.com/dani-garcia/vaultwarden/releases/latest)
|
|
||||||
[](https://github.com/dani-garcia/vaultwarden/blob/main/LICENSE.txt)
|
|
||||||
[](https://matrix.to/#/#vaultwarden:matrix.org)
|
|
||||||
|
|
||||||
Image is based on [Rust implementation of Bitwarden API](https://github.com/dani-garcia/vaultwarden).
|
[](https://github.com/dani-garcia/vaultwarden/releases/latest)
|
||||||
|
[](https://github.com/dani-garcia/vaultwarden/pkgs/container/vaultwarden)
|
||||||
|
[](https://hub.docker.com/r/vaultwarden/server)
|
||||||
|
[](https://quay.io/repository/vaultwarden/server) <br>
|
||||||
|
[](https://github.com/dani-garcia/vaultwarden/graphs/contributors)
|
||||||
|
[](https://github.com/dani-garcia/vaultwarden/network/members)
|
||||||
|
[](https://github.com/dani-garcia/vaultwarden/stargazers)
|
||||||
|
[](https://github.com/dani-garcia/vaultwarden/issues)
|
||||||
|
[](https://github.com/dani-garcia/vaultwarden/issues?q=is%3Aissue+is%3Aclosed)
|
||||||
|
[](https://github.com/dani-garcia/vaultwarden/blob/main/LICENSE.txt) <br>
|
||||||
|
[%3D'svg'%5D%2F*%5Blocal-name()%3D'g'%5D%5B2%5D%2F*%5Blocal-name()%3D'text'%5D%5B4%5D&style=flat-square&logo=rust&label=dependencies&color=005AA4)](https://deps.rs/repo/github/dani-garcia/vaultwarden)
|
||||||
|
[](https://github.com/dani-garcia/vaultwarden/actions/workflows/release.yml)
|
||||||
|
[](https://github.com/dani-garcia/vaultwarden/actions/workflows/build.yml) <br>
|
||||||
|
[](https://matrix.to/#/#vaultwarden:matrix.org)
|
||||||
|
[](https://github.com/dani-garcia/vaultwarden/discussions)
|
||||||
|
[](https://vaultwarden.discourse.group/)
|
||||||
|
|
||||||
**This project is not associated with the [Bitwarden](https://bitwarden.com/) project nor Bitwarden, Inc.**
|
> [!IMPORTANT]
|
||||||
|
> **When using this server, please report any bugs or suggestions directly to us (see [Get in touch](#get-in-touch)), regardless of whatever clients you are using (mobile, desktop, browser...). DO NOT use the official Bitwarden support channels.**
|
||||||
|
|
||||||
#### ⚠️**IMPORTANT**⚠️: When using this server, please report any bugs or suggestions to us directly (look at the bottom of this page for ways to get in touch), regardless of whatever clients you are using (mobile, desktop, browser...). DO NOT use the official support channels.
|
<br>
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
Basically full implementation of Bitwarden API is provided including:
|
A nearly complete implementation of the Bitwarden Client API is provided, including:
|
||||||
|
|
||||||
* Organizations support
|
* [Personal Vault](https://bitwarden.com/help/managing-items/)
|
||||||
* Attachments and Send
|
* [Send](https://bitwarden.com/help/about-send/)
|
||||||
* Vault API support
|
* [Attachments](https://bitwarden.com/help/attachments/)
|
||||||
* Serving the static files for Vault interface
|
* [Website icons](https://bitwarden.com/help/website-icons/)
|
||||||
* Website icons API
|
* [Personal API Key](https://bitwarden.com/help/personal-api-key/)
|
||||||
* Authenticator and U2F support
|
* [Organizations](https://bitwarden.com/help/getting-started-organizations/)
|
||||||
* YubiKey and Duo support
|
- [Collections](https://bitwarden.com/help/about-collections/),
|
||||||
* Emergency Access
|
[Password Sharing](https://bitwarden.com/help/sharing/),
|
||||||
|
[Member Roles](https://bitwarden.com/help/user-types-access-control/),
|
||||||
|
[Groups](https://bitwarden.com/help/about-groups/),
|
||||||
|
[Event Logs](https://bitwarden.com/help/event-logs/),
|
||||||
|
[Admin Password Reset](https://bitwarden.com/help/admin-reset/),
|
||||||
|
[Directory Connector](https://bitwarden.com/help/directory-sync/),
|
||||||
|
[Policies](https://bitwarden.com/help/policies/)
|
||||||
|
* [Multi/Two Factor Authentication](https://bitwarden.com/help/bitwarden-field-guide-two-step-login/)
|
||||||
|
- [Authenticator](https://bitwarden.com/help/setup-two-step-login-authenticator/),
|
||||||
|
[Email](https://bitwarden.com/help/setup-two-step-login-email/),
|
||||||
|
[FIDO2 WebAuthn](https://bitwarden.com/help/setup-two-step-login-fido/),
|
||||||
|
[YubiKey](https://bitwarden.com/help/setup-two-step-login-yubikey/),
|
||||||
|
[Duo](https://bitwarden.com/help/setup-two-step-login-duo/)
|
||||||
|
* [Emergency Access](https://bitwarden.com/help/emergency-access/)
|
||||||
|
* [Vaultwarden Admin Backend](https://github.com/dani-garcia/vaultwarden/wiki/Enabling-admin-page)
|
||||||
|
* [Modified Web Vault client](https://github.com/dani-garcia/bw_web_builds) (Bundled within our containers)
|
||||||
|
|
||||||
## Installation
|
<br>
|
||||||
Pull the docker image and mount a volume from the host for persistent storage:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
docker pull vaultwarden/server:latest
|
|
||||||
docker run -d --name vaultwarden -v /vw-data/:/data/ --restart unless-stopped -p 80:80 vaultwarden/server:latest
|
|
||||||
```
|
|
||||||
This will preserve any persistent data under /vw-data/, you can adapt the path to whatever suits you.
|
|
||||||
|
|
||||||
**IMPORTANT**: Most modern web browsers disallow the use of Web Crypto APIs in insecure contexts. In this case, you might get an error like `Cannot read property 'importKey'`. To solve this problem, you need to access the web vault via HTTPS or localhost.
|
|
||||||
|
|
||||||
This can be configured in [vaultwarden directly](https://github.com/dani-garcia/vaultwarden/wiki/Enabling-HTTPS) or using a third-party reverse proxy ([some examples](https://github.com/dani-garcia/vaultwarden/wiki/Proxy-examples)).
|
|
||||||
|
|
||||||
If you have an available domain name, you can get HTTPS certificates with [Let's Encrypt](https://letsencrypt.org/), or you can generate self-signed certificates with utilities like [mkcert](https://github.com/FiloSottile/mkcert). Some proxies automatically do this step, like Caddy (see examples linked above).
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
See the [vaultwarden wiki](https://github.com/dani-garcia/vaultwarden/wiki) for more information on how to configure and run the vaultwarden server.
|
|
||||||
|
> [!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).
|
||||||
|
> 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).
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
There are also [community driven packages](https://github.com/dani-garcia/vaultwarden/wiki/Third-party-packages) which can be used, but those might be lagging behind the latest version or might deviate in the way Vaultwarden is configured, as described in our [Wiki](https://github.com/dani-garcia/vaultwarden/wiki).
|
||||||
|
|
||||||
|
Alternatively, you can also [build Vaultwarden](https://github.com/dani-garcia/vaultwarden/wiki/Building-binary) yourself.
|
||||||
|
|
||||||
|
While Vaultwarden is based upon the [Rocket web framework](https://rocket.rs) which has built-in support for TLS our recommendation would be that you setup a reverse proxy (see [proxy examples](https://github.com/dani-garcia/vaultwarden/wiki/Proxy-examples)).
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
>**For more detailed examples on how to install, use and configure Vaultwarden you can check our [Wiki](https://github.com/dani-garcia/vaultwarden/wiki).**
|
||||||
|
|
||||||
|
### Docker/Podman CLI
|
||||||
|
|
||||||
|
Pull the container image and mount a volume from the host for persistent storage.<br>
|
||||||
|
You can replace `docker` with `podman` if you prefer to use podman.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker pull vaultwarden/server:latest
|
||||||
|
docker run --detach --name vaultwarden \
|
||||||
|
--env DOMAIN="https://vw.domain.tld" \
|
||||||
|
--volume /vw-data/:/data/ \
|
||||||
|
--restart unless-stopped \
|
||||||
|
--publish 127.0.0.1:8000:80 \
|
||||||
|
vaultwarden/server:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
This will preserve any persistent data under `/vw-data/`, you can adapt the path to whatever suits you.
|
||||||
|
|
||||||
|
### Docker Compose
|
||||||
|
|
||||||
|
To use Docker compose you need to create a `compose.yaml` which will hold the configuration to run the Vaultwarden container.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
vaultwarden:
|
||||||
|
image: vaultwarden/server:latest
|
||||||
|
container_name: vaultwarden
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
DOMAIN: "https://vw.domain.tld"
|
||||||
|
volumes:
|
||||||
|
- ./vw-data/:/data/
|
||||||
|
ports:
|
||||||
|
- 127.0.0.1:8000:80
|
||||||
|
```
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
## Get in touch
|
## Get in touch
|
||||||
To ask a question, offer suggestions or new features or to get help configuring or installing the software, please use [GitHub Discussions](https://github.com/dani-garcia/vaultwarden/discussions) or [the forum](https://vaultwarden.discourse.group/).
|
|
||||||
|
|
||||||
If you spot any bugs or crashes with vaultwarden itself, please [create an issue](https://github.com/dani-garcia/vaultwarden/issues/). Make sure you are on the latest version and there aren't any similar issues open, though!
|
Have a question, suggestion or need help? Join our community on [Matrix](https://matrix.to/#/#vaultwarden:matrix.org), [GitHub Discussions](https://github.com/dani-garcia/vaultwarden/discussions) or [Discourse Forums](https://vaultwarden.discourse.group/).
|
||||||
|
|
||||||
If you prefer to chat, we're usually hanging around at [#vaultwarden:matrix.org](https://matrix.to/#/#vaultwarden:matrix.org) room on Matrix. Feel free to join us!
|
Encountered a bug or crash? Please search our issue tracker and discussions to see if it's already been reported. If not, please [start a new discussion](https://github.com/dani-garcia/vaultwarden/discussions) or [create a new issue](https://github.com/dani-garcia/vaultwarden/issues/). Ensure you're using the latest version of Vaultwarden and there aren't any similar issues open or closed!
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
### Sponsors
|
|
||||||
Thanks for your contribution to the project!
|
Thanks for your contribution to the project!
|
||||||
|
|
||||||
<!--
|
[](https://github.com/dani-garcia/vaultwarden/graphs/contributors)<br>
|
||||||
<table>
|
[](https://github.com/dani-garcia/vaultwarden/graphs/contributors)
|
||||||
<tr>
|
|
||||||
<td align="center">
|
|
||||||
<a href="https://github.com/username">
|
|
||||||
<img src="https://avatars.githubusercontent.com/u/725423?s=75&v=4" width="75px;" alt="username"/>
|
|
||||||
<br />
|
|
||||||
<sub><b>username</b></sub>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<br/>
|
<br>
|
||||||
-->
|
|
||||||
|
|
||||||
<table>
|
## Disclaimer
|
||||||
<tr>
|
|
||||||
<td align="center">
|
**This project is not associated with [Bitwarden](https://bitwarden.com/) or Bitwarden, Inc.**
|
||||||
<a href="https://github.com/themightychris" style="width: 75px">
|
|
||||||
<sub><b>Chris Alfano</b></sub>
|
However, one of the active maintainers for Vaultwarden is employed by Bitwarden and is allowed to contribute to the project on their own time. These contributions are independent of Bitwarden and are reviewed by other maintainers.
|
||||||
</a>
|
|
||||||
</td>
|
The maintainers work together to set the direction for the project, focusing on serving the self-hosting community, including individuals, families, and small organizations, while ensuring the project's sustainability.
|
||||||
</tr>
|
|
||||||
<tr>
|
**Please note:** We cannot be held liable for any data loss that may occur while using Vaultwarden. This includes passwords, attachments, and other information handled by the application. We highly recommend performing regular backups of your files and database. However, should you experience data loss, we encourage you to contact us immediately.
|
||||||
<td align="center">
|
|
||||||
<a href="https://github.com/numberly" style="width: 75px">
|
<br>
|
||||||
<sub><b>Numberly</b></sub>
|
|
||||||
</a>
|
## Bitwarden_RS
|
||||||
</td>
|
|
||||||
</tr>
|
This project was known as Bitwarden_RS and has been renamed to separate itself from the official Bitwarden server in the hopes of avoiding confusion and trademark/branding issues.<br>
|
||||||
<tr>
|
Please see [#1642 - v1.21.0 release and project rename to Vaultwarden](https://github.com/dani-garcia/vaultwarden/discussions/1642) for more explanation.
|
||||||
<td align="center">
|
|
||||||
<a href="https://github.com/IQ333777" style="width: 75px">
|
|
||||||
<sub><b>IQ333777</b></sub>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
@@ -21,7 +21,7 @@ notify us. We welcome working with you to resolve the issue promptly. Thanks in
|
|||||||
The following bug classes are out-of scope:
|
The following bug classes are out-of scope:
|
||||||
|
|
||||||
- Bugs that are already reported on Vaultwarden's issue tracker (https://github.com/dani-garcia/vaultwarden/issues)
|
- Bugs that are already reported on Vaultwarden's issue tracker (https://github.com/dani-garcia/vaultwarden/issues)
|
||||||
- Bugs that are not part of Vaultwarden, like on the the web-vault or mobile and desktop clients. These issues need to be reported in the respective project issue tracker at https://github.com/bitwarden to which we are not associated
|
- Bugs that are not part of Vaultwarden, like on the web-vault or mobile and desktop clients. These issues need to be reported in the respective project issue tracker at https://github.com/bitwarden to which we are not associated
|
||||||
- Issues in an upstream software dependency (ex: Rust, or External Libraries) which are already reported to the upstream maintainer
|
- Issues in an upstream software dependency (ex: Rust, or External Libraries) which are already reported to the upstream maintainer
|
||||||
- Attacks requiring physical access to a user's device
|
- Attacks requiring physical access to a user's device
|
||||||
- Issues related to software or protocols not under Vaultwarden's control
|
- Issues related to software or protocols not under Vaultwarden's control
|
||||||
|
7
build.rs
7
build.rs
@@ -11,6 +11,8 @@ fn main() {
|
|||||||
println!("cargo:rustc-cfg=postgresql");
|
println!("cargo:rustc-cfg=postgresql");
|
||||||
#[cfg(feature = "query_logger")]
|
#[cfg(feature = "query_logger")]
|
||||||
println!("cargo:rustc-cfg=query_logger");
|
println!("cargo:rustc-cfg=query_logger");
|
||||||
|
#[cfg(feature = "s3")]
|
||||||
|
println!("cargo:rustc-cfg=s3");
|
||||||
|
|
||||||
#[cfg(not(any(feature = "sqlite", feature = "mysql", feature = "postgresql")))]
|
#[cfg(not(any(feature = "sqlite", feature = "mysql", feature = "postgresql")))]
|
||||||
compile_error!(
|
compile_error!(
|
||||||
@@ -23,6 +25,7 @@ fn main() {
|
|||||||
println!("cargo::rustc-check-cfg=cfg(mysql)");
|
println!("cargo::rustc-check-cfg=cfg(mysql)");
|
||||||
println!("cargo::rustc-check-cfg=cfg(postgresql)");
|
println!("cargo::rustc-check-cfg=cfg(postgresql)");
|
||||||
println!("cargo::rustc-check-cfg=cfg(query_logger)");
|
println!("cargo::rustc-check-cfg=cfg(query_logger)");
|
||||||
|
println!("cargo::rustc-check-cfg=cfg(s3)");
|
||||||
|
|
||||||
// Rerun when these paths are changed.
|
// Rerun when these paths are changed.
|
||||||
// Someone could have checked-out a tag or specific commit, but no other files changed.
|
// Someone could have checked-out a tag or specific commit, but no other files changed.
|
||||||
@@ -48,8 +51,8 @@ fn main() {
|
|||||||
fn run(args: &[&str]) -> Result<String, std::io::Error> {
|
fn run(args: &[&str]) -> Result<String, std::io::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, ErrorKind};
|
use std::io::Error;
|
||||||
return Err(Error::new(ErrorKind::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_string())
|
||||||
}
|
}
|
||||||
|
@@ -1,13 +1,13 @@
|
|||||||
---
|
---
|
||||||
vault_version: "v2024.6.2c"
|
vault_version: "v2025.8.0"
|
||||||
vault_image_digest: "sha256:409ab328ca931439cb916b388a4bb784bd44220717aaf74cf71620c23e34fc2b"
|
vault_image_digest: "sha256:41c2b51c87882248f405d5a0ab37210d2672a312ec5d4f3b9afcdbbe8eb9d57d"
|
||||||
# Cross Compile Docker Helper Scripts v1.5.0
|
# Cross Compile Docker Helper Scripts v1.6.1
|
||||||
# We use the linux/amd64 platform shell scripts since there is no difference between the different platform scripts
|
# We use the linux/amd64 platform shell scripts since there is no difference between the different platform scripts
|
||||||
# https://github.com/tonistiigi/xx | https://hub.docker.com/r/tonistiigi/xx/tags
|
# https://github.com/tonistiigi/xx | https://hub.docker.com/r/tonistiigi/xx/tags
|
||||||
xx_image_digest: "sha256:1978e7a58a1777cb0ef0dde76bad60b7914b21da57cfa88047875e4f364297aa"
|
xx_image_digest: "sha256:9c207bead753dda9430bdd15425c6518fc7a03d866103c516a2c6889188f5894"
|
||||||
rust_version: 1.82.0 # Rust version to be used
|
rust_version: 1.89.0 # Rust version to be used
|
||||||
debian_version: bookworm # Debian release name to be used
|
debian_version: trixie # Debian release name to be used
|
||||||
alpine_version: "3.20" # Alpine version to be used
|
alpine_version: "3.22" # Alpine version to be used
|
||||||
# For which platforms/architectures will we try to build images
|
# For which platforms/architectures will we try to build images
|
||||||
platforms: ["linux/amd64", "linux/arm64", "linux/arm/v7", "linux/arm/v6"]
|
platforms: ["linux/amd64", "linux/arm64", "linux/arm/v7", "linux/arm/v6"]
|
||||||
# Determine the build images per OS/Arch
|
# Determine the build images per OS/Arch
|
||||||
|
@@ -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:v2024.6.2c
|
# $ docker pull docker.io/vaultwarden/web-vault:v2025.8.0
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2024.6.2c
|
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.8.0
|
||||||
# [docker.io/vaultwarden/web-vault@sha256:409ab328ca931439cb916b388a4bb784bd44220717aaf74cf71620c23e34fc2b]
|
# [docker.io/vaultwarden/web-vault@sha256:41c2b51c87882248f405d5a0ab37210d2672a312ec5d4f3b9afcdbbe8eb9d57d]
|
||||||
#
|
#
|
||||||
# - 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:409ab328ca931439cb916b388a4bb784bd44220717aaf74cf71620c23e34fc2b
|
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:41c2b51c87882248f405d5a0ab37210d2672a312ec5d4f3b9afcdbbe8eb9d57d
|
||||||
# [docker.io/vaultwarden/web-vault:v2024.6.2c]
|
# [docker.io/vaultwarden/web-vault:v2025.8.0]
|
||||||
#
|
#
|
||||||
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:409ab328ca931439cb916b388a4bb784bd44220717aaf74cf71620c23e34fc2b AS vault
|
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:41c2b51c87882248f405d5a0ab37210d2672a312ec5d4f3b9afcdbbe8eb9d57d AS vault
|
||||||
|
|
||||||
########################## ALPINE BUILD IMAGES ##########################
|
########################## ALPINE BUILD IMAGES ##########################
|
||||||
## NOTE: The Alpine Base Images do not support other platforms then linux/amd64
|
## NOTE: The Alpine Base Images do not support other platforms then linux/amd64
|
||||||
## And for Alpine we define all build images here, they will only be loaded when actually used
|
## And for Alpine we define all build images here, they will only be loaded when actually used
|
||||||
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.82.0 AS build_amd64
|
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.89.0 AS build_amd64
|
||||||
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.82.0 AS build_arm64
|
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.89.0 AS build_arm64
|
||||||
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.82.0 AS build_armv7
|
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.89.0 AS build_armv7
|
||||||
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.82.0 AS build_armv6
|
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.89.0 AS build_armv6
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
# hadolint ignore=DL3006
|
# hadolint ignore=DL3006
|
||||||
@@ -76,6 +76,7 @@ RUN source /env-cargo && \
|
|||||||
|
|
||||||
# Copies over *only* your manifests and build files
|
# Copies over *only* your manifests and build files
|
||||||
COPY ./Cargo.* ./rust-toolchain.toml ./build.rs ./
|
COPY ./Cargo.* ./rust-toolchain.toml ./build.rs ./
|
||||||
|
COPY ./macros ./macros
|
||||||
|
|
||||||
ARG CARGO_PROFILE=release
|
ARG CARGO_PROFILE=release
|
||||||
|
|
||||||
@@ -126,7 +127,7 @@ RUN source /env-cargo && \
|
|||||||
# To uninstall: docker run --privileged --rm tonistiigi/binfmt --uninstall 'qemu-*'
|
# To uninstall: docker run --privileged --rm tonistiigi/binfmt --uninstall 'qemu-*'
|
||||||
#
|
#
|
||||||
# We need to add `--platform` here, because of a podman bug: https://github.com/containers/buildah/issues/4742
|
# We need to add `--platform` here, because of a podman bug: https://github.com/containers/buildah/issues/4742
|
||||||
FROM --platform=$TARGETPLATFORM docker.io/library/alpine:3.20
|
FROM --platform=$TARGETPLATFORM docker.io/library/alpine:3.22
|
||||||
|
|
||||||
ENV ROCKET_PROFILE="release" \
|
ENV ROCKET_PROFILE="release" \
|
||||||
ROCKET_ADDRESS=0.0.0.0 \
|
ROCKET_ADDRESS=0.0.0.0 \
|
||||||
|
@@ -19,24 +19,24 @@
|
|||||||
# - 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:v2024.6.2c
|
# $ docker pull docker.io/vaultwarden/web-vault:v2025.8.0
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2024.6.2c
|
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.8.0
|
||||||
# [docker.io/vaultwarden/web-vault@sha256:409ab328ca931439cb916b388a4bb784bd44220717aaf74cf71620c23e34fc2b]
|
# [docker.io/vaultwarden/web-vault@sha256:41c2b51c87882248f405d5a0ab37210d2672a312ec5d4f3b9afcdbbe8eb9d57d]
|
||||||
#
|
#
|
||||||
# - 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:409ab328ca931439cb916b388a4bb784bd44220717aaf74cf71620c23e34fc2b
|
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:41c2b51c87882248f405d5a0ab37210d2672a312ec5d4f3b9afcdbbe8eb9d57d
|
||||||
# [docker.io/vaultwarden/web-vault:v2024.6.2c]
|
# [docker.io/vaultwarden/web-vault:v2025.8.0]
|
||||||
#
|
#
|
||||||
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:409ab328ca931439cb916b388a4bb784bd44220717aaf74cf71620c23e34fc2b AS vault
|
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:41c2b51c87882248f405d5a0ab37210d2672a312ec5d4f3b9afcdbbe8eb9d57d 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
|
||||||
## And these bash scripts do not have any significant difference if at all
|
## And these bash scripts do not have any significant difference if at all
|
||||||
FROM --platform=linux/amd64 docker.io/tonistiigi/xx@sha256:1978e7a58a1777cb0ef0dde76bad60b7914b21da57cfa88047875e4f364297aa AS xx
|
FROM --platform=linux/amd64 docker.io/tonistiigi/xx@sha256:9c207bead753dda9430bdd15425c6518fc7a03d866103c516a2c6889188f5894 AS xx
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
# hadolint ignore=DL3006
|
# hadolint ignore=DL3006
|
||||||
FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.82.0-slim-bookworm AS build
|
FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.89.0-slim-trixie AS build
|
||||||
COPY --from=xx / /
|
COPY --from=xx / /
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
ARG TARGETVARIANT
|
ARG TARGETVARIANT
|
||||||
@@ -68,15 +68,11 @@ RUN apt-get update && \
|
|||||||
xx-apt-get install -y \
|
xx-apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
gcc \
|
gcc \
|
||||||
libmariadb3 \
|
|
||||||
libpq-dev \
|
libpq-dev \
|
||||||
libpq5 \
|
libpq5 \
|
||||||
libssl-dev \
|
libssl-dev \
|
||||||
|
libmariadb-dev \
|
||||||
zlib1g-dev && \
|
zlib1g-dev && \
|
||||||
# Force install arch dependend mariadb dev packages
|
|
||||||
# Installing them the normal way breaks several other packages (again)
|
|
||||||
apt-get download "libmariadb-dev-compat:$(xx-info debian-arch)" "libmariadb-dev:$(xx-info debian-arch)" && \
|
|
||||||
dpkg --force-all -i ./libmariadb-dev*.deb && \
|
|
||||||
# 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
|
||||||
|
|
||||||
@@ -89,24 +85,24 @@ RUN USER=root cargo new --bin /app
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Environment variables for Cargo on Debian based builds
|
# Environment variables for Cargo on Debian based builds
|
||||||
ARG ARCH_OPENSSL_LIB_DIR \
|
ARG TARGET_PKG_CONFIG_PATH
|
||||||
ARCH_OPENSSL_INCLUDE_DIR
|
|
||||||
|
|
||||||
RUN source /env-cargo && \
|
RUN source /env-cargo && \
|
||||||
if xx-info is-cross ; then \
|
if xx-info is-cross ; then \
|
||||||
# Some special variables if needed to override some build paths
|
|
||||||
if [[ -n "${ARCH_OPENSSL_LIB_DIR}" && -n "${ARCH_OPENSSL_INCLUDE_DIR}" ]]; then \
|
|
||||||
echo "export $(echo "${CARGO_TARGET}" | tr '[:lower:]' '[:upper:]' | tr - _)_OPENSSL_LIB_DIR=${ARCH_OPENSSL_LIB_DIR}" >> /env-cargo && \
|
|
||||||
echo "export $(echo "${CARGO_TARGET}" | tr '[:lower:]' '[:upper:]' | tr - _)_OPENSSL_INCLUDE_DIR=${ARCH_OPENSSL_INCLUDE_DIR}" >> /env-cargo ; \
|
|
||||||
fi && \
|
|
||||||
# We can't use xx-cargo since that uses clang, which doesn't work for our libraries.
|
# 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.
|
# 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 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 CARGO_TARGET_$(echo "${CARGO_TARGET}" | tr '[:lower:]' '[:upper:]' | tr - _)_LINKER=/usr/bin/$(xx-info)-gcc" >> /env-cargo && \
|
||||||
echo "export PKG_CONFIG=/usr/bin/$(xx-info)-pkg-config" >> /env-cargo && \
|
|
||||||
echo "export CROSS_COMPILE=1" >> /env-cargo && \
|
echo "export CROSS_COMPILE=1" >> /env-cargo && \
|
||||||
echo "export OPENSSL_INCLUDE_DIR=/usr/include/$(xx-info)" >> /env-cargo && \
|
echo "export PKG_CONFIG_ALLOW_CROSS=1" >> /env-cargo && \
|
||||||
echo "export OPENSSL_LIB_DIR=/usr/lib/$(xx-info)" >> /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 && \
|
fi && \
|
||||||
# Output the current contents of the file
|
# Output the current contents of the file
|
||||||
cat /env-cargo
|
cat /env-cargo
|
||||||
@@ -116,6 +112,7 @@ RUN source /env-cargo && \
|
|||||||
|
|
||||||
# Copies over *only* your manifests and build files
|
# Copies over *only* your manifests and build files
|
||||||
COPY ./Cargo.* ./rust-toolchain.toml ./build.rs ./
|
COPY ./Cargo.* ./rust-toolchain.toml ./build.rs ./
|
||||||
|
COPY ./macros ./macros
|
||||||
|
|
||||||
ARG CARGO_PROFILE=release
|
ARG CARGO_PROFILE=release
|
||||||
|
|
||||||
@@ -165,7 +162,7 @@ RUN source /env-cargo && \
|
|||||||
# To uninstall: docker run --privileged --rm tonistiigi/binfmt --uninstall 'qemu-*'
|
# To uninstall: docker run --privileged --rm tonistiigi/binfmt --uninstall 'qemu-*'
|
||||||
#
|
#
|
||||||
# We need to add `--platform` here, because of a podman bug: https://github.com/containers/buildah/issues/4742
|
# We need to add `--platform` here, because of a podman bug: https://github.com/containers/buildah/issues/4742
|
||||||
FROM --platform=$TARGETPLATFORM docker.io/library/debian:bookworm-slim
|
FROM --platform=$TARGETPLATFORM docker.io/library/debian:trixie-slim
|
||||||
|
|
||||||
ENV ROCKET_PROFILE="release" \
|
ENV ROCKET_PROFILE="release" \
|
||||||
ROCKET_ADDRESS=0.0.0.0 \
|
ROCKET_ADDRESS=0.0.0.0 \
|
||||||
@@ -178,7 +175,7 @@ RUN mkdir /data && \
|
|||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
curl \
|
curl \
|
||||||
libmariadb-dev-compat \
|
libmariadb-dev \
|
||||||
libpq5 \
|
libpq5 \
|
||||||
openssl && \
|
openssl && \
|
||||||
apt-get clean && \
|
apt-get clean && \
|
||||||
|
@@ -86,15 +86,11 @@ RUN apt-get update && \
|
|||||||
xx-apt-get install -y \
|
xx-apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
gcc \
|
gcc \
|
||||||
libmariadb3 \
|
|
||||||
libpq-dev \
|
libpq-dev \
|
||||||
libpq5 \
|
libpq5 \
|
||||||
libssl-dev \
|
libssl-dev \
|
||||||
|
libmariadb-dev \
|
||||||
zlib1g-dev && \
|
zlib1g-dev && \
|
||||||
# Force install arch dependend mariadb dev packages
|
|
||||||
# Installing them the normal way breaks several other packages (again)
|
|
||||||
apt-get download "libmariadb-dev-compat:$(xx-info debian-arch)" "libmariadb-dev:$(xx-info debian-arch)" && \
|
|
||||||
dpkg --force-all -i ./libmariadb-dev*.deb && \
|
|
||||||
# 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 %}
|
||||||
@@ -109,24 +105,24 @@ WORKDIR /app
|
|||||||
|
|
||||||
{% if base == "debian" %}
|
{% if base == "debian" %}
|
||||||
# Environment variables for Cargo on Debian based builds
|
# Environment variables for Cargo on Debian based builds
|
||||||
ARG ARCH_OPENSSL_LIB_DIR \
|
ARG TARGET_PKG_CONFIG_PATH
|
||||||
ARCH_OPENSSL_INCLUDE_DIR
|
|
||||||
|
|
||||||
RUN source /env-cargo && \
|
RUN source /env-cargo && \
|
||||||
if xx-info is-cross ; then \
|
if xx-info is-cross ; then \
|
||||||
# Some special variables if needed to override some build paths
|
|
||||||
if [[ -n "${ARCH_OPENSSL_LIB_DIR}" && -n "${ARCH_OPENSSL_INCLUDE_DIR}" ]]; then \
|
|
||||||
echo "export $(echo "${CARGO_TARGET}" | tr '[:lower:]' '[:upper:]' | tr - _)_OPENSSL_LIB_DIR=${ARCH_OPENSSL_LIB_DIR}" >> /env-cargo && \
|
|
||||||
echo "export $(echo "${CARGO_TARGET}" | tr '[:lower:]' '[:upper:]' | tr - _)_OPENSSL_INCLUDE_DIR=${ARCH_OPENSSL_INCLUDE_DIR}" >> /env-cargo ; \
|
|
||||||
fi && \
|
|
||||||
# We can't use xx-cargo since that uses clang, which doesn't work for our libraries.
|
# 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.
|
# 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 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 CARGO_TARGET_$(echo "${CARGO_TARGET}" | tr '[:lower:]' '[:upper:]' | tr - _)_LINKER=/usr/bin/$(xx-info)-gcc" >> /env-cargo && \
|
||||||
echo "export PKG_CONFIG=/usr/bin/$(xx-info)-pkg-config" >> /env-cargo && \
|
|
||||||
echo "export CROSS_COMPILE=1" >> /env-cargo && \
|
echo "export CROSS_COMPILE=1" >> /env-cargo && \
|
||||||
echo "export OPENSSL_INCLUDE_DIR=/usr/include/$(xx-info)" >> /env-cargo && \
|
echo "export PKG_CONFIG_ALLOW_CROSS=1" >> /env-cargo && \
|
||||||
echo "export OPENSSL_LIB_DIR=/usr/lib/$(xx-info)" >> /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 && \
|
fi && \
|
||||||
# Output the current contents of the file
|
# Output the current contents of the file
|
||||||
cat /env-cargo
|
cat /env-cargo
|
||||||
@@ -143,6 +139,7 @@ RUN source /env-cargo && \
|
|||||||
|
|
||||||
# Copies over *only* your manifests and build files
|
# Copies over *only* your manifests and build files
|
||||||
COPY ./Cargo.* ./rust-toolchain.toml ./build.rs ./
|
COPY ./Cargo.* ./rust-toolchain.toml ./build.rs ./
|
||||||
|
COPY ./macros ./macros
|
||||||
|
|
||||||
ARG CARGO_PROFILE=release
|
ARG CARGO_PROFILE=release
|
||||||
|
|
||||||
@@ -215,7 +212,7 @@ RUN mkdir /data && \
|
|||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
curl \
|
curl \
|
||||||
libmariadb-dev-compat \
|
libmariadb-dev \
|
||||||
libpq5 \
|
libpq5 \
|
||||||
openssl && \
|
openssl && \
|
||||||
apt-get clean && \
|
apt-get clean && \
|
||||||
|
@@ -46,7 +46,7 @@ There also is an option to use an other docker container to provide support for
|
|||||||
```bash
|
```bash
|
||||||
# To install and activate
|
# To install and activate
|
||||||
docker run --privileged --rm tonistiigi/binfmt --install arm64,arm
|
docker run --privileged --rm tonistiigi/binfmt --install arm64,arm
|
||||||
# To unistall
|
# To uninstall
|
||||||
docker run --privileged --rm tonistiigi/binfmt --uninstall 'qemu-*'
|
docker run --privileged --rm tonistiigi/binfmt --uninstall 'qemu-*'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@@ -17,7 +17,7 @@ variable "SOURCE_REPOSITORY_URL" {
|
|||||||
default = null
|
default = null
|
||||||
}
|
}
|
||||||
|
|
||||||
// The commit hash of of the current commit this build was triggered on
|
// The commit hash of the current commit this build was triggered on
|
||||||
variable "SOURCE_COMMIT" {
|
variable "SOURCE_COMMIT" {
|
||||||
default = null
|
default = null
|
||||||
}
|
}
|
||||||
@@ -133,8 +133,7 @@ target "debian-386" {
|
|||||||
platforms = ["linux/386"]
|
platforms = ["linux/386"]
|
||||||
tags = generate_tags("", "-386")
|
tags = generate_tags("", "-386")
|
||||||
args = {
|
args = {
|
||||||
ARCH_OPENSSL_LIB_DIR = "/usr/lib/i386-linux-gnu"
|
TARGET_PKG_CONFIG_PATH = "/usr/lib/i386-linux-gnu/pkgconfig"
|
||||||
ARCH_OPENSSL_INCLUDE_DIR = "/usr/include/i386-linux-gnu"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,20 +141,12 @@ target "debian-ppc64le" {
|
|||||||
inherits = ["debian"]
|
inherits = ["debian"]
|
||||||
platforms = ["linux/ppc64le"]
|
platforms = ["linux/ppc64le"]
|
||||||
tags = generate_tags("", "-ppc64le")
|
tags = generate_tags("", "-ppc64le")
|
||||||
args = {
|
|
||||||
ARCH_OPENSSL_LIB_DIR = "/usr/lib/powerpc64le-linux-gnu"
|
|
||||||
ARCH_OPENSSL_INCLUDE_DIR = "/usr/include/powerpc64le-linux-gnu"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
target "debian-s390x" {
|
target "debian-s390x" {
|
||||||
inherits = ["debian"]
|
inherits = ["debian"]
|
||||||
platforms = ["linux/s390x"]
|
platforms = ["linux/s390x"]
|
||||||
tags = generate_tags("", "-s390x")
|
tags = generate_tags("", "-s390x")
|
||||||
args = {
|
|
||||||
ARCH_OPENSSL_LIB_DIR = "/usr/lib/s390x-linux-gnu"
|
|
||||||
ARCH_OPENSSL_INCLUDE_DIR = "/usr/include/s390x-linux-gnu"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// ==== End of unsupported Debian architecture targets ===
|
// ==== End of unsupported Debian architecture targets ===
|
||||||
|
|
||||||
|
16
macros/Cargo.toml
Normal file
16
macros/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "macros"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "macros"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
proc-macro = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
quote = "1.0.40"
|
||||||
|
syn = "2.0.105"
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
56
macros/src/lib.rs
Normal file
56
macros/src/lib.rs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
use proc_macro::TokenStream;
|
||||||
|
use quote::quote;
|
||||||
|
|
||||||
|
#[proc_macro_derive(UuidFromParam)]
|
||||||
|
pub fn derive_uuid_from_param(input: TokenStream) -> TokenStream {
|
||||||
|
let ast = syn::parse(input).unwrap();
|
||||||
|
|
||||||
|
impl_derive_uuid_macro(&ast)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn impl_derive_uuid_macro(ast: &syn::DeriveInput) -> TokenStream {
|
||||||
|
let name = &ast.ident;
|
||||||
|
let gen_derive = quote! {
|
||||||
|
#[automatically_derived]
|
||||||
|
impl<'r> rocket::request::FromParam<'r> for #name {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn from_param(param: &'r str) -> Result<Self, Self::Error> {
|
||||||
|
if uuid::Uuid::parse_str(param).is_ok() {
|
||||||
|
Ok(Self(param.to_string()))
|
||||||
|
} else {
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
gen_derive.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[proc_macro_derive(IdFromParam)]
|
||||||
|
pub fn derive_id_from_param(input: TokenStream) -> TokenStream {
|
||||||
|
let ast = syn::parse(input).unwrap();
|
||||||
|
|
||||||
|
impl_derive_safestring_macro(&ast)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn impl_derive_safestring_macro(ast: &syn::DeriveInput) -> TokenStream {
|
||||||
|
let name = &ast.ident;
|
||||||
|
let gen_derive = quote! {
|
||||||
|
#[automatically_derived]
|
||||||
|
impl<'r> rocket::request::FromParam<'r> for #name {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn from_param(param: &'r str) -> Result<Self, Self::Error> {
|
||||||
|
if param.chars().all(|c| matches!(c, 'a'..='z' | 'A'..='Z' |'0'..='9' | '-')) {
|
||||||
|
Ok(Self(param.to_string()))
|
||||||
|
} else {
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
gen_derive.into()
|
||||||
|
}
|
1
migrations/mysql/2023-09-10-133000_add_sso/down.sql
Normal file
1
migrations/mysql/2023-09-10-133000_add_sso/down.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE sso_nonce;
|
4
migrations/mysql/2023-09-10-133000_add_sso/up.sql
Normal file
4
migrations/mysql/2023-09-10-133000_add_sso/up.sql
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
CREATE TABLE sso_nonce (
|
||||||
|
nonce CHAR(36) NOT NULL PRIMARY KEY,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE users_organizations DROP COLUMN invited_by_email;
|
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE users_organizations ADD COLUMN invited_by_email TEXT DEFAULT NULL;
|
@@ -0,0 +1,6 @@
|
|||||||
|
DROP TABLE IF EXISTS sso_nonce;
|
||||||
|
|
||||||
|
CREATE TABLE sso_nonce (
|
||||||
|
nonce CHAR(36) NOT NULL PRIMARY KEY,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
@@ -0,0 +1,8 @@
|
|||||||
|
DROP TABLE IF EXISTS sso_nonce;
|
||||||
|
|
||||||
|
CREATE TABLE sso_nonce (
|
||||||
|
state VARCHAR(512) NOT NULL PRIMARY KEY,
|
||||||
|
nonce TEXT NOT NULL,
|
||||||
|
redirect_uri TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT now()
|
||||||
|
);
|
@@ -0,0 +1,8 @@
|
|||||||
|
DROP TABLE IF EXISTS sso_nonce;
|
||||||
|
|
||||||
|
CREATE TABLE sso_nonce (
|
||||||
|
state VARCHAR(512) NOT NULL PRIMARY KEY,
|
||||||
|
nonce TEXT NOT NULL,
|
||||||
|
redirect_uri TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT now()
|
||||||
|
);
|
@@ -0,0 +1,9 @@
|
|||||||
|
DROP TABLE IF EXISTS sso_nonce;
|
||||||
|
|
||||||
|
CREATE TABLE sso_nonce (
|
||||||
|
state VARCHAR(512) NOT NULL PRIMARY KEY,
|
||||||
|
nonce TEXT NOT NULL,
|
||||||
|
verifier TEXT,
|
||||||
|
redirect_uri TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT now()
|
||||||
|
);
|
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS sso_users;
|
7
migrations/mysql/2024-03-06-170000_add_sso_users/up.sql
Normal file
7
migrations/mysql/2024-03-06-170000_add_sso_users/up.sql
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
CREATE TABLE sso_users (
|
||||||
|
user_uuid CHAR(36) NOT NULL PRIMARY KEY,
|
||||||
|
identifier VARCHAR(768) NOT NULL UNIQUE,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT now(),
|
||||||
|
|
||||||
|
FOREIGN KEY(user_uuid) REFERENCES users(uuid)
|
||||||
|
);
|
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE sso_users DROP FOREIGN KEY `sso_users_ibfk_1`;
|
||||||
|
ALTER TABLE sso_users ADD FOREIGN KEY(user_uuid) REFERENCES users(uuid) ON UPDATE CASCADE ON DELETE CASCADE;
|
5
migrations/mysql/2025-01-09-172300_add_manage/up.sql
Normal file
5
migrations/mysql/2025-01-09-172300_add_manage/up.sql
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
ALTER TABLE users_collections
|
||||||
|
ADD COLUMN manage BOOLEAN NOT NULL DEFAULT FALSE;
|
||||||
|
|
||||||
|
ALTER TABLE collections_groups
|
||||||
|
ADD COLUMN manage BOOLEAN NOT NULL DEFAULT FALSE;
|
1
migrations/postgresql/2023-09-10-133000_add_sso/down.sql
Normal file
1
migrations/postgresql/2023-09-10-133000_add_sso/down.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE sso_nonce;
|
4
migrations/postgresql/2023-09-10-133000_add_sso/up.sql
Normal file
4
migrations/postgresql/2023-09-10-133000_add_sso/up.sql
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
CREATE TABLE sso_nonce (
|
||||||
|
nonce CHAR(36) NOT NULL PRIMARY KEY,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT now()
|
||||||
|
);
|
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE users_organizations DROP COLUMN invited_by_email;
|
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE users_organizations ADD COLUMN invited_by_email TEXT DEFAULT NULL;
|
@@ -0,0 +1,6 @@
|
|||||||
|
DROP TABLE sso_nonce;
|
||||||
|
|
||||||
|
CREATE TABLE sso_nonce (
|
||||||
|
nonce CHAR(36) NOT NULL PRIMARY KEY,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT now()
|
||||||
|
);
|
@@ -0,0 +1,8 @@
|
|||||||
|
DROP TABLE sso_nonce;
|
||||||
|
|
||||||
|
CREATE TABLE sso_nonce (
|
||||||
|
state TEXT NOT NULL PRIMARY KEY,
|
||||||
|
nonce TEXT NOT NULL,
|
||||||
|
redirect_uri TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT now()
|
||||||
|
);
|
@@ -0,0 +1,8 @@
|
|||||||
|
DROP TABLE IF EXISTS sso_nonce;
|
||||||
|
|
||||||
|
CREATE TABLE sso_nonce (
|
||||||
|
state TEXT NOT NULL PRIMARY KEY,
|
||||||
|
nonce TEXT NOT NULL,
|
||||||
|
redirect_uri TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT now()
|
||||||
|
);
|
@@ -0,0 +1,9 @@
|
|||||||
|
DROP TABLE IF EXISTS sso_nonce;
|
||||||
|
|
||||||
|
CREATE TABLE sso_nonce (
|
||||||
|
state TEXT NOT NULL PRIMARY KEY,
|
||||||
|
nonce TEXT NOT NULL,
|
||||||
|
verifier TEXT,
|
||||||
|
redirect_uri TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT now()
|
||||||
|
);
|
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS sso_users;
|
@@ -0,0 +1,7 @@
|
|||||||
|
CREATE TABLE sso_users (
|
||||||
|
user_uuid CHAR(36) NOT NULL PRIMARY KEY,
|
||||||
|
identifier TEXT NOT NULL UNIQUE,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT now(),
|
||||||
|
|
||||||
|
FOREIGN KEY(user_uuid) REFERENCES users(uuid)
|
||||||
|
);
|
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE sso_users
|
||||||
|
DROP CONSTRAINT "sso_users_user_uuid_fkey",
|
||||||
|
ADD CONSTRAINT "sso_users_user_uuid_fkey" FOREIGN KEY(user_uuid) REFERENCES users(uuid) ON UPDATE CASCADE ON DELETE CASCADE;
|
@@ -0,0 +1,5 @@
|
|||||||
|
ALTER TABLE users_collections
|
||||||
|
ADD COLUMN manage BOOLEAN NOT NULL DEFAULT FALSE;
|
||||||
|
|
||||||
|
ALTER TABLE collections_groups
|
||||||
|
ADD COLUMN manage BOOLEAN NOT NULL DEFAULT FALSE;
|
1
migrations/sqlite/2023-09-10-133000_add_sso/down.sql
Normal file
1
migrations/sqlite/2023-09-10-133000_add_sso/down.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE sso_nonce;
|
4
migrations/sqlite/2023-09-10-133000_add_sso/up.sql
Normal file
4
migrations/sqlite/2023-09-10-133000_add_sso/up.sql
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
CREATE TABLE sso_nonce (
|
||||||
|
nonce CHAR(36) NOT NULL PRIMARY KEY,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE users_organizations DROP COLUMN invited_by_email;
|
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE users_organizations ADD COLUMN invited_by_email TEXT DEFAULT NULL;
|
@@ -0,0 +1,6 @@
|
|||||||
|
DROP TABLE sso_nonce;
|
||||||
|
|
||||||
|
CREATE TABLE sso_nonce (
|
||||||
|
nonce CHAR(36) NOT NULL PRIMARY KEY,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
@@ -0,0 +1,8 @@
|
|||||||
|
DROP TABLE sso_nonce;
|
||||||
|
|
||||||
|
CREATE TABLE sso_nonce (
|
||||||
|
state TEXT NOT NULL PRIMARY KEY,
|
||||||
|
nonce TEXT NOT NULL,
|
||||||
|
redirect_uri TEXT NOT NULL,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
@@ -0,0 +1,8 @@
|
|||||||
|
DROP TABLE IF EXISTS sso_nonce;
|
||||||
|
|
||||||
|
CREATE TABLE sso_nonce (
|
||||||
|
state TEXT NOT NULL PRIMARY KEY,
|
||||||
|
nonce TEXT NOT NULL,
|
||||||
|
redirect_uri TEXT NOT NULL,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
@@ -0,0 +1,9 @@
|
|||||||
|
DROP TABLE IF EXISTS sso_nonce;
|
||||||
|
|
||||||
|
CREATE TABLE sso_nonce (
|
||||||
|
state TEXT NOT NULL PRIMARY KEY,
|
||||||
|
nonce TEXT NOT NULL,
|
||||||
|
verifier TEXT,
|
||||||
|
redirect_uri TEXT NOT NULL,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS sso_users;
|
7
migrations/sqlite/2024-03-06-170000_add_sso_users/up.sql
Normal file
7
migrations/sqlite/2024-03-06-170000_add_sso_users/up.sql
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
CREATE TABLE sso_users (
|
||||||
|
user_uuid CHAR(36) NOT NULL PRIMARY KEY,
|
||||||
|
identifier TEXT NOT NULL UNIQUE,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
FOREIGN KEY(user_uuid) REFERENCES users(uuid)
|
||||||
|
);
|
@@ -0,0 +1,9 @@
|
|||||||
|
DROP TABLE IF EXISTS sso_users;
|
||||||
|
|
||||||
|
CREATE TABLE sso_users (
|
||||||
|
user_uuid CHAR(36) NOT NULL PRIMARY KEY,
|
||||||
|
identifier TEXT NOT NULL UNIQUE,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
FOREIGN KEY(user_uuid) REFERENCES users(uuid) ON UPDATE CASCADE ON DELETE CASCADE
|
||||||
|
);
|
5
migrations/sqlite/2025-01-09-172300_add_manage/up.sql
Normal file
5
migrations/sqlite/2025-01-09-172300_add_manage/up.sql
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
ALTER TABLE users_collections
|
||||||
|
ADD COLUMN manage BOOLEAN NOT NULL DEFAULT 0; -- FALSE
|
||||||
|
|
||||||
|
ALTER TABLE collections_groups
|
||||||
|
ADD COLUMN manage BOOLEAN NOT NULL DEFAULT 0; -- FALSE
|
64
playwright/.env.template
Normal file
64
playwright/.env.template
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
#################################
|
||||||
|
### Conf to run dev instances ###
|
||||||
|
#################################
|
||||||
|
ENV=dev
|
||||||
|
DC_ENV_FILE=.env
|
||||||
|
COMPOSE_IGNORE_ORPHANS=True
|
||||||
|
DOCKER_BUILDKIT=1
|
||||||
|
|
||||||
|
################
|
||||||
|
# Users Config #
|
||||||
|
################
|
||||||
|
TEST_USER=test
|
||||||
|
TEST_USER_PASSWORD=${TEST_USER}
|
||||||
|
TEST_USER_MAIL=${TEST_USER}@yopmail.com
|
||||||
|
|
||||||
|
TEST_USER2=test2
|
||||||
|
TEST_USER2_PASSWORD=${TEST_USER2}
|
||||||
|
TEST_USER2_MAIL=${TEST_USER2}@yopmail.com
|
||||||
|
|
||||||
|
TEST_USER3=test3
|
||||||
|
TEST_USER3_PASSWORD=${TEST_USER3}
|
||||||
|
TEST_USER3_MAIL=${TEST_USER3}@yopmail.com
|
||||||
|
|
||||||
|
###################
|
||||||
|
# Keycloak Config #
|
||||||
|
###################
|
||||||
|
KEYCLOAK_ADMIN=admin
|
||||||
|
KEYCLOAK_ADMIN_PASSWORD=${KEYCLOAK_ADMIN}
|
||||||
|
KC_HTTP_HOST=127.0.0.1
|
||||||
|
KC_HTTP_PORT=8080
|
||||||
|
|
||||||
|
# Script parameters (use Keycloak and Vaultwarden config too)
|
||||||
|
TEST_REALM=test
|
||||||
|
DUMMY_REALM=dummy
|
||||||
|
DUMMY_AUTHORITY=http://${KC_HTTP_HOST}:${KC_HTTP_PORT}/realms/${DUMMY_REALM}
|
||||||
|
|
||||||
|
######################
|
||||||
|
# Vaultwarden Config #
|
||||||
|
######################
|
||||||
|
ROCKET_ADDRESS=0.0.0.0
|
||||||
|
ROCKET_PORT=8000
|
||||||
|
DOMAIN=http://localhost:${ROCKET_PORT}
|
||||||
|
LOG_LEVEL=info,oidcwarden::sso=debug
|
||||||
|
I_REALLY_WANT_VOLATILE_STORAGE=true
|
||||||
|
|
||||||
|
SSO_ENABLED=true
|
||||||
|
SSO_ONLY=false
|
||||||
|
SSO_CLIENT_ID=warden
|
||||||
|
SSO_CLIENT_SECRET=warden
|
||||||
|
SSO_AUTHORITY=http://${KC_HTTP_HOST}:${KC_HTTP_PORT}/realms/${TEST_REALM}
|
||||||
|
|
||||||
|
SMTP_HOST=127.0.0.1
|
||||||
|
SMTP_PORT=1025
|
||||||
|
SMTP_SECURITY=off
|
||||||
|
SMTP_TIMEOUT=5
|
||||||
|
SMTP_FROM=vaultwarden@test
|
||||||
|
SMTP_FROM_NAME=Vaultwarden
|
||||||
|
|
||||||
|
########################################################
|
||||||
|
# DUMMY values for docker-compose to stop bothering us #
|
||||||
|
########################################################
|
||||||
|
MARIADB_PORT=3305
|
||||||
|
MYSQL_PORT=3307
|
||||||
|
POSTGRES_PORT=5432
|
6
playwright/.gitignore
vendored
Normal file
6
playwright/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
logs
|
||||||
|
node_modules/
|
||||||
|
/test-results/
|
||||||
|
/playwright-report/
|
||||||
|
/playwright/.cache/
|
||||||
|
temp
|
177
playwright/README.md
Normal file
177
playwright/README.md
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
# Integration tests
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
This relies on `docker` and the `compose` [plugin](https://docs.docker.com/compose/install/).
|
||||||
|
Databases (`Mariadb`, `Mysql` and `Postgres`) and `Playwright` will run in containers.
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
You will additionally need `nodejs` then run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npx playwright install-deps
|
||||||
|
npx playwright install firefox
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
To run all the tests:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env run Playwright
|
||||||
|
```
|
||||||
|
|
||||||
|
To force a rebuild of the Playwright image:
|
||||||
|
```bash
|
||||||
|
DOCKER_BUILDKIT=1 docker compose --env-file test.env build Playwright
|
||||||
|
```
|
||||||
|
|
||||||
|
To access the UI to easily run test individually and debug if needed (this will not work in docker):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx playwright test --ui
|
||||||
|
```
|
||||||
|
|
||||||
|
### DB
|
||||||
|
|
||||||
|
Projects are configured to allow to run tests only on specific database.
|
||||||
|
|
||||||
|
You can use:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env run Playwright test --project=mariadb
|
||||||
|
DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env run Playwright test --project=mysql
|
||||||
|
DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env run Playwright test --project=postgres
|
||||||
|
DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env run Playwright test --project=sqlite
|
||||||
|
```
|
||||||
|
|
||||||
|
### SSO
|
||||||
|
|
||||||
|
To run the SSO tests:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env run Playwright test --project sso-sqlite
|
||||||
|
```
|
||||||
|
|
||||||
|
### Keep services running
|
||||||
|
|
||||||
|
If you want you can keep the DB and Keycloak runnning (states are not impacted by the tests):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PW_KEEP_SERVICE_RUNNNING=true npx playwright test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running specific tests
|
||||||
|
|
||||||
|
To run a whole file you can :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env run Playwright test --project=sqlite tests/login.spec.ts
|
||||||
|
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):
|
||||||
|
|
||||||
|
```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 tests/login.spec.ts:16
|
||||||
|
```
|
||||||
|
|
||||||
|
## Writing scenario
|
||||||
|
|
||||||
|
When creating new scenario use the recorder to more easily identify elements
|
||||||
|
(in general try to rely on visible hint to identify elements and not hidden IDs).
|
||||||
|
This does not start the server, you will need to start it manually.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env up Vaultwarden
|
||||||
|
npx playwright codegen "http://127.0.0.1:8003"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Override web-vault
|
||||||
|
|
||||||
|
It is possible to change the `web-vault` used by referencing a different `bw_web_builds` commit.
|
||||||
|
|
||||||
|
Simplest is to set and uncomment `PW_WV_REPO_URL` and `PW_WV_COMMIT_HASH` in the `test.env`.
|
||||||
|
Ensure that the image is built with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env build Vaultwarden
|
||||||
|
```
|
||||||
|
|
||||||
|
You can check the result running:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env up Vaultwarden
|
||||||
|
```
|
||||||
|
|
||||||
|
# OpenID Connect test setup
|
||||||
|
|
||||||
|
Additionally this `docker-compose` template allows to run locally Vaultwarden,
|
||||||
|
[Keycloak](https://www.keycloak.org/) and [Maildev](https://github.com/timshel/maildev) to test OIDC.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
This rely on `docker` and the `compose` [plugin](https://docs.docker.com/compose/install/).
|
||||||
|
First create a copy of `.env.template` as `.env` (This is done to prevent committing your custom settings, Ex `SMTP_`).
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Then start the stack (the `profile` is required to run `Vaultwarden`) :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
> 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 | Created new realm with id 'test'
|
||||||
|
keycloakSetup_1 | 74af4933-e386-4e64-ba15-a7b61212c45e
|
||||||
|
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
|
||||||
|
(It is normal for this container to stop once the configuration is done).
|
||||||
|
|
||||||
|
Then you can access :
|
||||||
|
|
||||||
|
- `Vaultwarden` on http://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`
|
||||||
|
- `Maildev` on http://0.0.0.0:1080
|
||||||
|
|
||||||
|
To proceed with an SSO login after you enter the email, on the screen prompting for `Master Password` the SSO button should be visible.
|
||||||
|
To use your computer external ip (for example when testing with a phone) you will have to configure `KC_HTTP_HOST` and `DOMAIN`.
|
||||||
|
|
||||||
|
## Running only Keycloak
|
||||||
|
|
||||||
|
You can run just `Keycloak` with `--profile keycloak`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
> docker compose --profile keycloak --env-file .env up
|
||||||
|
```
|
||||||
|
When running with a local Vaultwarden, you can use a front-end build from [dani-garcia/bw_web_builds](https://github.com/dani-garcia/bw_web_builds/releases).
|
||||||
|
|
||||||
|
## Rebuilding the Vaultwarden
|
||||||
|
|
||||||
|
To force rebuilding the Vaultwarden image you can run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose --profile vaultwarden --env-file .env build VaultwardenPrebuild Vaultwarden
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
- `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)).
|
||||||
|
- All `Vaultwarden` configuration can be set (EX: `SMTP_*`)
|
||||||
|
|
||||||
|
## Cleanup
|
||||||
|
|
||||||
|
Use `docker compose --profile vaultwarden down`.
|
40
playwright/compose/keycloak/Dockerfile
Normal file
40
playwright/compose/keycloak/Dockerfile
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
FROM docker.io/library/debian:bookworm-slim as build
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
ARG KEYCLOAK_VERSION
|
||||||
|
|
||||||
|
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y ca-certificates curl wget \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /
|
||||||
|
|
||||||
|
RUN wget -c https://github.com/keycloak/keycloak/releases/download/${KEYCLOAK_VERSION}/keycloak-${KEYCLOAK_VERSION}.tar.gz -O - | tar -xz
|
||||||
|
|
||||||
|
FROM docker.io/library/debian:bookworm-slim
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
ARG KEYCLOAK_VERSION
|
||||||
|
|
||||||
|
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y ca-certificates curl wget \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
ARG JAVA_URL
|
||||||
|
ARG JAVA_VERSION
|
||||||
|
|
||||||
|
ENV JAVA_VERSION=${JAVA_VERSION}
|
||||||
|
|
||||||
|
RUN mkdir -p /opt/openjdk && cd /opt/openjdk \
|
||||||
|
&& wget -c "${JAVA_URL}" -O - | tar -xz
|
||||||
|
|
||||||
|
WORKDIR /
|
||||||
|
|
||||||
|
COPY setup.sh /setup.sh
|
||||||
|
COPY --from=build /keycloak-${KEYCLOAK_VERSION}/bin /opt/keycloak/bin
|
||||||
|
|
||||||
|
CMD "/setup.sh"
|
36
playwright/compose/keycloak/setup.sh
Executable file
36
playwright/compose/keycloak/setup.sh
Executable file
@@ -0,0 +1,36 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
export PATH=/opt/keycloak/bin:/opt/openjdk/jdk-${JAVA_VERSION}/bin:$PATH
|
||||||
|
export JAVA_HOME=/opt/openjdk/jdk-${JAVA_VERSION}
|
||||||
|
|
||||||
|
STATUS_CODE=0
|
||||||
|
while [[ "$STATUS_CODE" != "404" ]] ; do
|
||||||
|
echo "Will retry in 2 seconds"
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
STATUS_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$DUMMY_AUTHORITY")
|
||||||
|
|
||||||
|
if [[ "$STATUS_CODE" = "200" ]]; then
|
||||||
|
echo "Setup should already be done. Will not run."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
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 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
|
||||||
|
|
||||||
|
TEST_USER_ID=$(kcadm.sh create users -r "$TEST_REALM" -s "username=$TEST_USER" -s "firstName=$TEST_USER" -s "lastName=$TEST_USER" -s "email=$TEST_USER_MAIL" -s emailVerified=true -s enabled=true -i)
|
||||||
|
kcadm.sh update users/$TEST_USER_ID/reset-password -r "$TEST_REALM" -s type=password -s "value=$TEST_USER_PASSWORD" -n
|
||||||
|
|
||||||
|
TEST_USER2_ID=$(kcadm.sh create users -r "$TEST_REALM" -s "username=$TEST_USER2" -s "firstName=$TEST_USER2" -s "lastName=$TEST_USER2" -s "email=$TEST_USER2_MAIL" -s emailVerified=true -s enabled=true -i)
|
||||||
|
kcadm.sh update users/$TEST_USER2_ID/reset-password -r "$TEST_REALM" -s type=password -s "value=$TEST_USER2_PASSWORD" -n
|
||||||
|
|
||||||
|
TEST_USER3_ID=$(kcadm.sh create users -r "$TEST_REALM" -s "username=$TEST_USER3" -s "firstName=$TEST_USER3" -s "lastName=$TEST_USER3" -s "email=$TEST_USER3_MAIL" -s emailVerified=true -s enabled=true -i)
|
||||||
|
kcadm.sh update users/$TEST_USER3_ID/reset-password -r "$TEST_REALM" -s type=password -s "value=$TEST_USER3_PASSWORD" -n
|
||||||
|
|
||||||
|
# Dummy realm to mark end of setup
|
||||||
|
kcadm.sh create realms -s realm="$DUMMY_REALM" -s enabled=true -s "accessTokenLifespan=600"
|
40
playwright/compose/playwright/Dockerfile
Normal file
40
playwright/compose/playwright/Dockerfile
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
FROM docker.io/library/debian:bookworm-slim
|
||||||
|
|
||||||
|
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y ca-certificates curl \
|
||||||
|
&& curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc \
|
||||||
|
&& chmod a+r /etc/apt/keyrings/docker.asc \
|
||||||
|
&& echo "deb [signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian bookworm stable" | tee /etc/apt/sources.list.d/docker.list \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
containerd.io \
|
||||||
|
docker-buildx-plugin \
|
||||||
|
docker-ce \
|
||||||
|
docker-ce-cli \
|
||||||
|
docker-compose-plugin \
|
||||||
|
git \
|
||||||
|
libmariadb-dev-compat \
|
||||||
|
libpq5 \
|
||||||
|
nodejs \
|
||||||
|
npm \
|
||||||
|
openssl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN mkdir /playwright
|
||||||
|
WORKDIR /playwright
|
||||||
|
|
||||||
|
COPY package.json .
|
||||||
|
RUN npm install && npx playwright install-deps && npx playwright install firefox
|
||||||
|
|
||||||
|
COPY docker-compose.yml test.env ./
|
||||||
|
COPY compose ./compose
|
||||||
|
|
||||||
|
COPY *.ts test.env ./
|
||||||
|
COPY tests ./tests
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/bin/npx", "playwright"]
|
||||||
|
CMD ["test"]
|
40
playwright/compose/warden/Dockerfile
Normal file
40
playwright/compose/warden/Dockerfile
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
FROM playwright_oidc_vaultwarden_prebuilt AS prebuilt
|
||||||
|
|
||||||
|
FROM node:22-trixie AS build
|
||||||
|
|
||||||
|
ARG REPO_URL
|
||||||
|
ARG COMMIT_HASH
|
||||||
|
|
||||||
|
ENV REPO_URL=$REPO_URL
|
||||||
|
ENV COMMIT_HASH=$COMMIT_HASH
|
||||||
|
|
||||||
|
COPY --from=prebuilt /web-vault /web-vault
|
||||||
|
|
||||||
|
COPY build.sh /build.sh
|
||||||
|
RUN /build.sh
|
||||||
|
|
||||||
|
######################## RUNTIME IMAGE ########################
|
||||||
|
FROM docker.io/library/debian:trixie-slim
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
# Create data folder and Install needed libraries
|
||||||
|
RUN mkdir /data && \
|
||||||
|
apt-get update && apt-get install -y \
|
||||||
|
--no-install-recommends \
|
||||||
|
ca-certificates \
|
||||||
|
curl \
|
||||||
|
libmariadb-dev \
|
||||||
|
libpq5 \
|
||||||
|
openssl && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copies the files from the context (Rocket.toml file and web-vault)
|
||||||
|
# and the binary from the "build" stage to the current stage
|
||||||
|
WORKDIR /
|
||||||
|
|
||||||
|
COPY --from=prebuilt /start.sh .
|
||||||
|
COPY --from=prebuilt /vaultwarden .
|
||||||
|
COPY --from=build /web-vault ./web-vault
|
||||||
|
|
||||||
|
ENTRYPOINT ["/start.sh"]
|
23
playwright/compose/warden/build.sh
Executable file
23
playwright/compose/warden/build.sh
Executable file
@@ -0,0 +1,23 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo $REPO_URL
|
||||||
|
echo $COMMIT_HASH
|
||||||
|
|
||||||
|
if [[ ! -z "$REPO_URL" ]] && [[ ! -z "$COMMIT_HASH" ]] ; then
|
||||||
|
rm -rf /web-vault
|
||||||
|
|
||||||
|
mkdir bw_web_builds;
|
||||||
|
cd bw_web_builds;
|
||||||
|
|
||||||
|
git -c init.defaultBranch=main init
|
||||||
|
git remote add origin "$REPO_URL"
|
||||||
|
git fetch --depth 1 origin "$COMMIT_HASH"
|
||||||
|
git -c advice.detachedHead=false checkout FETCH_HEAD
|
||||||
|
|
||||||
|
export VAULT_VERSION=$(cat Dockerfile | grep "ARG VAULT_VERSION" | cut -d "=" -f2)
|
||||||
|
./scripts/checkout_web_vault.sh
|
||||||
|
./scripts/build_web_vault.sh
|
||||||
|
printf '{"version":"%s"}' "$COMMIT_HASH" > ./web-vault/apps/web/build/vw-version.json
|
||||||
|
|
||||||
|
mv ./web-vault/apps/web/build /web-vault
|
||||||
|
fi
|
124
playwright/docker-compose.yml
Normal file
124
playwright/docker-compose.yml
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
services:
|
||||||
|
VaultwardenPrebuild:
|
||||||
|
profiles: ["playwright", "vaultwarden"]
|
||||||
|
container_name: playwright_oidc_vaultwarden_prebuilt
|
||||||
|
image: playwright_oidc_vaultwarden_prebuilt
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
entrypoint: /bin/bash
|
||||||
|
restart: "no"
|
||||||
|
|
||||||
|
Vaultwarden:
|
||||||
|
profiles: ["playwright", "vaultwarden"]
|
||||||
|
container_name: playwright_oidc_vaultwarden-${ENV:-dev}
|
||||||
|
image: playwright_oidc_vaultwarden-${ENV:-dev}
|
||||||
|
network_mode: "host"
|
||||||
|
build:
|
||||||
|
context: compose/warden
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
args:
|
||||||
|
REPO_URL: ${PW_WV_REPO_URL:-}
|
||||||
|
COMMIT_HASH: ${PW_WV_COMMIT_HASH:-}
|
||||||
|
env_file: ${DC_ENV_FILE:-.env}
|
||||||
|
environment:
|
||||||
|
- DATABASE_URL
|
||||||
|
- I_REALLY_WANT_VOLATILE_STORAGE
|
||||||
|
- LOG_LEVEL
|
||||||
|
- LOGIN_RATELIMIT_MAX_BURST
|
||||||
|
- SMTP_HOST
|
||||||
|
- SMTP_FROM
|
||||||
|
- SMTP_DEBUG
|
||||||
|
- SSO_DEBUG_TOKENS
|
||||||
|
- SSO_FRONTEND
|
||||||
|
- SSO_ENABLED
|
||||||
|
- SSO_ONLY
|
||||||
|
restart: "no"
|
||||||
|
depends_on:
|
||||||
|
- VaultwardenPrebuild
|
||||||
|
|
||||||
|
Playwright:
|
||||||
|
profiles: ["playwright"]
|
||||||
|
container_name: playwright_oidc_playwright
|
||||||
|
image: playwright_oidc_playwright
|
||||||
|
network_mode: "host"
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: compose/playwright/Dockerfile
|
||||||
|
environment:
|
||||||
|
- PW_WV_REPO_URL
|
||||||
|
- PW_WV_COMMIT_HASH
|
||||||
|
restart: "no"
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
- ..:/project
|
||||||
|
|
||||||
|
Mariadb:
|
||||||
|
profiles: ["playwright"]
|
||||||
|
container_name: playwright_mariadb
|
||||||
|
image: mariadb:11.2.4
|
||||||
|
env_file: test.env
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
|
||||||
|
start_period: 10s
|
||||||
|
interval: 10s
|
||||||
|
ports:
|
||||||
|
- ${MARIADB_PORT}:3306
|
||||||
|
|
||||||
|
Mysql:
|
||||||
|
profiles: ["playwright"]
|
||||||
|
container_name: playwright_mysql
|
||||||
|
image: mysql:8.4.1
|
||||||
|
env_file: test.env
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"]
|
||||||
|
start_period: 10s
|
||||||
|
interval: 10s
|
||||||
|
ports:
|
||||||
|
- ${MYSQL_PORT}:3306
|
||||||
|
|
||||||
|
Postgres:
|
||||||
|
profiles: ["playwright"]
|
||||||
|
container_name: playwright_postgres
|
||||||
|
image: postgres:16.3
|
||||||
|
env_file: test.env
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
|
||||||
|
start_period: 20s
|
||||||
|
interval: 30s
|
||||||
|
ports:
|
||||||
|
- ${POSTGRES_PORT}:5432
|
||||||
|
|
||||||
|
Maildev:
|
||||||
|
profiles: ["vaultwarden", "maildev"]
|
||||||
|
container_name: maildev
|
||||||
|
image: timshel/maildev:3.0.4
|
||||||
|
ports:
|
||||||
|
- ${SMTP_PORT}:1025
|
||||||
|
- 1080:1080
|
||||||
|
|
||||||
|
Keycloak:
|
||||||
|
profiles: ["keycloak", "vaultwarden"]
|
||||||
|
container_name: keycloak-${ENV:-dev}
|
||||||
|
image: quay.io/keycloak/keycloak:25.0.4
|
||||||
|
network_mode: "host"
|
||||||
|
command:
|
||||||
|
- start-dev
|
||||||
|
env_file: ${DC_ENV_FILE:-.env}
|
||||||
|
|
||||||
|
KeycloakSetup:
|
||||||
|
profiles: ["keycloak", "vaultwarden"]
|
||||||
|
container_name: keycloakSetup-${ENV:-dev}
|
||||||
|
image: keycloak_setup-${ENV:-dev}
|
||||||
|
build:
|
||||||
|
context: compose/keycloak
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
args:
|
||||||
|
KEYCLOAK_VERSION: 25.0.4
|
||||||
|
JAVA_URL: https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_linux-x64_bin.tar.gz
|
||||||
|
JAVA_VERSION: 21.0.2
|
||||||
|
network_mode: "host"
|
||||||
|
depends_on:
|
||||||
|
- Keycloak
|
||||||
|
restart: "no"
|
||||||
|
env_file: ${DC_ENV_FILE:-.env}
|
22
playwright/global-setup.ts
Normal file
22
playwright/global-setup.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { firefox, type FullConfig } from '@playwright/test';
|
||||||
|
import { execSync } from 'node:child_process';
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
const utils = require('./global-utils');
|
||||||
|
|
||||||
|
utils.loadEnv();
|
||||||
|
|
||||||
|
async function globalSetup(config: FullConfig) {
|
||||||
|
// Are we running in docker and the project is mounted ?
|
||||||
|
const path = (fs.existsSync("/project/playwright/playwright.config.ts") ? "/project/playwright" : ".");
|
||||||
|
execSync(`docker compose --project-directory ${path} --profile playwright --env-file test.env build VaultwardenPrebuild`, {
|
||||||
|
env: { ...process.env },
|
||||||
|
stdio: "inherit"
|
||||||
|
});
|
||||||
|
execSync(`docker compose --project-directory ${path} --profile playwright --env-file test.env build Vaultwarden`, {
|
||||||
|
env: { ...process.env },
|
||||||
|
stdio: "inherit"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default globalSetup;
|
246
playwright/global-utils.ts
Normal file
246
playwright/global-utils.ts
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
import { expect, type Browser, type TestInfo } from '@playwright/test';
|
||||||
|
import { EventEmitter } from "events";
|
||||||
|
import { type Mail, MailServer } from 'maildev';
|
||||||
|
import { execSync } from 'node:child_process';
|
||||||
|
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
import dotenvExpand from 'dotenv-expand';
|
||||||
|
|
||||||
|
const fs = require("fs");
|
||||||
|
const { spawn } = require('node:child_process');
|
||||||
|
|
||||||
|
export function loadEnv(){
|
||||||
|
var myEnv = dotenv.config({ path: 'test.env' });
|
||||||
|
dotenvExpand.expand(myEnv);
|
||||||
|
|
||||||
|
return {
|
||||||
|
user1: {
|
||||||
|
email: process.env.TEST_USER_MAIL,
|
||||||
|
name: process.env.TEST_USER,
|
||||||
|
password: process.env.TEST_USER_PASSWORD,
|
||||||
|
},
|
||||||
|
user2: {
|
||||||
|
email: process.env.TEST_USER2_MAIL,
|
||||||
|
name: process.env.TEST_USER2,
|
||||||
|
password: process.env.TEST_USER2_PASSWORD,
|
||||||
|
},
|
||||||
|
user3: {
|
||||||
|
email: process.env.TEST_USER3_MAIL,
|
||||||
|
name: process.env.TEST_USER3,
|
||||||
|
password: process.env.TEST_USER3_PASSWORD,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function waitFor(url: String, browser: Browser) {
|
||||||
|
var ready = false;
|
||||||
|
var context;
|
||||||
|
|
||||||
|
do {
|
||||||
|
try {
|
||||||
|
context = await browser.newContext();
|
||||||
|
const page = await context.newPage();
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
const result = await page.goto(url);
|
||||||
|
ready = result.status() === 200;
|
||||||
|
} catch(e) {
|
||||||
|
if( !e.message.includes("CONNECTION_REFUSED") ){
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await context.close();
|
||||||
|
}
|
||||||
|
} while(!ready);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function startComposeService(serviceName: String){
|
||||||
|
console.log(`Starting ${serviceName}`);
|
||||||
|
execSync(`docker compose --profile playwright --env-file test.env up -d ${serviceName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stopComposeService(serviceName: String){
|
||||||
|
console.log(`Stopping ${serviceName}`);
|
||||||
|
execSync(`docker compose --profile playwright --env-file test.env stop ${serviceName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function wipeSqlite(){
|
||||||
|
console.log(`Delete Vaultwarden container to wipe sqlite`);
|
||||||
|
execSync(`docker compose --env-file test.env stop Vaultwarden`);
|
||||||
|
execSync(`docker compose --env-file test.env rm -f Vaultwarden`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function wipeMariaDB(){
|
||||||
|
var mysql = require('mysql2/promise');
|
||||||
|
var ready = false;
|
||||||
|
var connection;
|
||||||
|
|
||||||
|
do {
|
||||||
|
try {
|
||||||
|
connection = await mysql.createConnection({
|
||||||
|
user: process.env.MARIADB_USER,
|
||||||
|
host: "127.0.0.1",
|
||||||
|
database: process.env.MARIADB_DATABASE,
|
||||||
|
password: process.env.MARIADB_PASSWORD,
|
||||||
|
port: process.env.MARIADB_PORT,
|
||||||
|
});
|
||||||
|
|
||||||
|
await connection.execute(`DROP DATABASE ${process.env.MARIADB_DATABASE}`);
|
||||||
|
await connection.execute(`CREATE DATABASE ${process.env.MARIADB_DATABASE}`);
|
||||||
|
console.log('Successfully wiped mariadb');
|
||||||
|
ready = true;
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`Error when wiping mariadb: ${err}`);
|
||||||
|
} finally {
|
||||||
|
if( connection ){
|
||||||
|
connection.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await new Promise(r => setTimeout(r, 1000));
|
||||||
|
} while(!ready);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function wipeMysqlDB(){
|
||||||
|
var mysql = require('mysql2/promise');
|
||||||
|
var ready = false;
|
||||||
|
var connection;
|
||||||
|
|
||||||
|
do{
|
||||||
|
try {
|
||||||
|
connection = await mysql.createConnection({
|
||||||
|
user: process.env.MYSQL_USER,
|
||||||
|
host: "127.0.0.1",
|
||||||
|
database: process.env.MYSQL_DATABASE,
|
||||||
|
password: process.env.MYSQL_PASSWORD,
|
||||||
|
port: process.env.MYSQL_PORT,
|
||||||
|
});
|
||||||
|
|
||||||
|
await connection.execute(`DROP DATABASE ${process.env.MYSQL_DATABASE}`);
|
||||||
|
await connection.execute(`CREATE DATABASE ${process.env.MYSQL_DATABASE}`);
|
||||||
|
console.log('Successfully wiped mysql');
|
||||||
|
ready = true;
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`Error when wiping mysql: ${err}`);
|
||||||
|
} finally {
|
||||||
|
if( connection ){
|
||||||
|
connection.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await new Promise(r => setTimeout(r, 1000));
|
||||||
|
} while(!ready);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function wipePostgres(){
|
||||||
|
const { Client } = require('pg');
|
||||||
|
|
||||||
|
const client = new Client({
|
||||||
|
user: process.env.POSTGRES_USER,
|
||||||
|
host: "127.0.0.1",
|
||||||
|
database: "postgres",
|
||||||
|
password: process.env.POSTGRES_PASSWORD,
|
||||||
|
port: process.env.POSTGRES_PORT,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.connect();
|
||||||
|
await client.query(`DROP DATABASE ${process.env.POSTGRES_DB}`);
|
||||||
|
await client.query(`CREATE DATABASE ${process.env.POSTGRES_DB}`);
|
||||||
|
console.log('Successfully wiped postgres');
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`Error when wiping postgres: ${err}`);
|
||||||
|
} finally {
|
||||||
|
client.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function dbConfig(testInfo: TestInfo){
|
||||||
|
switch(testInfo.project.name) {
|
||||||
|
case "postgres":
|
||||||
|
case "sso-postgres":
|
||||||
|
return { DATABASE_URL: `postgresql://${process.env.POSTGRES_USER}:${process.env.POSTGRES_PASSWORD}@127.0.0.1:${process.env.POSTGRES_PORT}/${process.env.POSTGRES_DB}` };
|
||||||
|
case "mariadb":
|
||||||
|
case "sso-mariadb":
|
||||||
|
return { DATABASE_URL: `mysql://${process.env.MARIADB_USER}:${process.env.MARIADB_PASSWORD}@127.0.0.1:${process.env.MARIADB_PORT}/${process.env.MARIADB_DATABASE}` };
|
||||||
|
case "mysql":
|
||||||
|
case "sso-mysql":
|
||||||
|
return { DATABASE_URL: `mysql://${process.env.MYSQL_USER}:${process.env.MYSQL_PASSWORD}@127.0.0.1:${process.env.MYSQL_PORT}/${process.env.MYSQL_DATABASE}`};
|
||||||
|
case "sqlite":
|
||||||
|
case "sso-sqlite":
|
||||||
|
return { I_REALLY_WANT_VOLATILE_STORAGE: true };
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknow database name: ${testInfo.project.name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All parameters passed in `env` need to be added to the docker-compose.yml
|
||||||
|
**/
|
||||||
|
export async function startVault(browser: Browser, testInfo: TestInfo, env = {}, resetDB: Boolean = true) {
|
||||||
|
if( resetDB ){
|
||||||
|
switch(testInfo.project.name) {
|
||||||
|
case "postgres":
|
||||||
|
case "sso-postgres":
|
||||||
|
await wipePostgres();
|
||||||
|
break;
|
||||||
|
case "mariadb":
|
||||||
|
case "sso-mariadb":
|
||||||
|
await wipeMariaDB();
|
||||||
|
break;
|
||||||
|
case "mysql":
|
||||||
|
case "sso-mysql":
|
||||||
|
await wipeMysqlDB();
|
||||||
|
break;
|
||||||
|
case "sqlite":
|
||||||
|
case "sso-sqlite":
|
||||||
|
wipeSqlite();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknow database name: ${testInfo.project.name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Starting Vaultwarden`);
|
||||||
|
execSync(`docker compose --profile playwright --env-file test.env up -d Vaultwarden`, {
|
||||||
|
env: { ...env, ...dbConfig(testInfo) },
|
||||||
|
});
|
||||||
|
await waitFor("/", browser);
|
||||||
|
console.log(`Vaultwarden running on: ${process.env.DOMAIN}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function stopVault(force: boolean = false) {
|
||||||
|
if( force === false && process.env.PW_KEEP_SERVICE_RUNNNING === "true" ) {
|
||||||
|
console.log(`Keep vaultwarden running on: ${process.env.DOMAIN}`);
|
||||||
|
} else {
|
||||||
|
console.log(`Vaultwarden stopping`);
|
||||||
|
execSync(`docker compose --profile playwright --env-file test.env stop Vaultwarden`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function restartVault(page: Page, testInfo: TestInfo, env, resetDB: Boolean = true) {
|
||||||
|
stopVault(true);
|
||||||
|
return startVault(page.context().browser(), testInfo, env, resetDB);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkNotification(page: Page, hasText: string) {
|
||||||
|
await expect(page.locator('bit-toast').filter({ hasText })).toBeVisible();
|
||||||
|
await page.locator('bit-toast').filter({ hasText }).getByRole('button').click();
|
||||||
|
await expect(page.locator('bit-toast').filter({ hasText })).toHaveCount(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cleanLanding(page: Page) {
|
||||||
|
await page.goto('/', { waitUntil: 'domcontentloaded' });
|
||||||
|
await expect(page.getByRole('button').nth(0)).toBeVisible();
|
||||||
|
|
||||||
|
const logged = await page.getByRole('button', { name: 'Log out' }).count();
|
||||||
|
if( logged > 0 ){
|
||||||
|
await page.getByRole('button', { name: 'Log out' }).click();
|
||||||
|
await page.getByRole('button', { name: 'Log out' }).click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function logout(test: Test, page: Page, user: { name: string }) {
|
||||||
|
await test.step('logout', async () => {
|
||||||
|
await page.getByRole('button', { name: user.name, exact: true }).click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Log out' }).click();
|
||||||
|
await expect(page.getByRole('heading', { name: 'Log in' })).toBeVisible();
|
||||||
|
});
|
||||||
|
}
|
2594
playwright/package-lock.json
generated
Normal file
2594
playwright/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
21
playwright/package.json
Normal file
21
playwright/package.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "scenarios",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.54.2",
|
||||||
|
"dotenv": "^16.6.1",
|
||||||
|
"dotenv-expand": "^12.0.2",
|
||||||
|
"maildev": "npm:@timshel_npm/maildev@^3.2.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"mysql2": "^3.14.3",
|
||||||
|
"otpauth": "^9.4.0",
|
||||||
|
"pg": "^8.16.3"
|
||||||
|
}
|
||||||
|
}
|
143
playwright/playwright.config.ts
Normal file
143
playwright/playwright.config.ts
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
|
import { exec } from 'node:child_process';
|
||||||
|
|
||||||
|
const utils = require('./global-utils');
|
||||||
|
|
||||||
|
utils.loadEnv();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See https://playwright.dev/docs/test-configuration.
|
||||||
|
*/
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './.',
|
||||||
|
/* Run tests in files in parallel */
|
||||||
|
fullyParallel: false,
|
||||||
|
|
||||||
|
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
|
||||||
|
retries: 0,
|
||||||
|
workers: 1,
|
||||||
|
|
||||||
|
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||||
|
reporter: 'html',
|
||||||
|
|
||||||
|
/* Long global timeout for complex tests
|
||||||
|
* But short action/nav/expect timeouts to fail on specific step (raise locally if not enough).
|
||||||
|
*/
|
||||||
|
timeout: 120 * 1000,
|
||||||
|
actionTimeout: 20 * 1000,
|
||||||
|
navigationTimeout: 20 * 1000,
|
||||||
|
expect: { timeout: 20 * 1000 },
|
||||||
|
|
||||||
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||||
|
use: {
|
||||||
|
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||||
|
baseURL: process.env.DOMAIN,
|
||||||
|
browserName: 'firefox',
|
||||||
|
locale: 'en-GB',
|
||||||
|
timezoneId: 'Europe/London',
|
||||||
|
|
||||||
|
/* Always collect trace (other values add random test failures) See https://playwright.dev/docs/trace-viewer */
|
||||||
|
trace: 'on',
|
||||||
|
viewport: {
|
||||||
|
width: 1080,
|
||||||
|
height: 720,
|
||||||
|
},
|
||||||
|
video: "on",
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Configure projects for major browsers */
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: 'mariadb-setup',
|
||||||
|
testMatch: 'tests/setups/db-setup.ts',
|
||||||
|
use: { serviceName: "Mariadb" },
|
||||||
|
teardown: 'mariadb-teardown',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'mysql-setup',
|
||||||
|
testMatch: 'tests/setups/db-setup.ts',
|
||||||
|
use: { serviceName: "Mysql" },
|
||||||
|
teardown: 'mysql-teardown',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'postgres-setup',
|
||||||
|
testMatch: 'tests/setups/db-setup.ts',
|
||||||
|
use: { serviceName: "Postgres" },
|
||||||
|
teardown: 'postgres-teardown',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'sso-setup',
|
||||||
|
testMatch: 'tests/setups/sso-setup.ts',
|
||||||
|
teardown: 'sso-teardown',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'mariadb',
|
||||||
|
testMatch: 'tests/*.spec.ts',
|
||||||
|
testIgnore: 'tests/sso_*.spec.ts',
|
||||||
|
dependencies: ['mariadb-setup'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'mysql',
|
||||||
|
testMatch: 'tests/*.spec.ts',
|
||||||
|
testIgnore: 'tests/sso_*.spec.ts',
|
||||||
|
dependencies: ['mysql-setup'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'postgres',
|
||||||
|
testMatch: 'tests/*.spec.ts',
|
||||||
|
testIgnore: 'tests/sso_*.spec.ts',
|
||||||
|
dependencies: ['postgres-setup'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'sqlite',
|
||||||
|
testMatch: 'tests/*.spec.ts',
|
||||||
|
testIgnore: 'tests/sso_*.spec.ts',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'sso-mariadb',
|
||||||
|
testMatch: 'tests/sso_*.spec.ts',
|
||||||
|
dependencies: ['sso-setup', 'mariadb-setup'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'sso-mysql',
|
||||||
|
testMatch: 'tests/sso_*.spec.ts',
|
||||||
|
dependencies: ['sso-setup', 'mysql-setup'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'sso-postgres',
|
||||||
|
testMatch: 'tests/sso_*.spec.ts',
|
||||||
|
dependencies: ['sso-setup', 'postgres-setup'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'sso-sqlite',
|
||||||
|
testMatch: 'tests/sso_*.spec.ts',
|
||||||
|
dependencies: ['sso-setup'],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'mariadb-teardown',
|
||||||
|
testMatch: 'tests/setups/db-teardown.ts',
|
||||||
|
use: { serviceName: "Mariadb" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'mysql-teardown',
|
||||||
|
testMatch: 'tests/setups/db-teardown.ts',
|
||||||
|
use: { serviceName: "Mysql" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'postgres-teardown',
|
||||||
|
testMatch: 'tests/setups/db-teardown.ts',
|
||||||
|
use: { serviceName: "Postgres" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'sso-teardown',
|
||||||
|
testMatch: 'tests/setups/sso-teardown.ts',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
globalSetup: require.resolve('./global-setup'),
|
||||||
|
});
|
97
playwright/test.env
Normal file
97
playwright/test.env
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
##################################################################
|
||||||
|
### Shared Playwright conf test file Vaultwarden and Databases ###
|
||||||
|
##################################################################
|
||||||
|
|
||||||
|
ENV=test
|
||||||
|
DC_ENV_FILE=test.env
|
||||||
|
COMPOSE_IGNORE_ORPHANS=True
|
||||||
|
DOCKER_BUILDKIT=1
|
||||||
|
|
||||||
|
#####################
|
||||||
|
# Playwright Config #
|
||||||
|
#####################
|
||||||
|
PW_KEEP_SERVICE_RUNNNING=${PW_KEEP_SERVICE_RUNNNING:-false}
|
||||||
|
PW_SMTP_FROM=vaultwarden@playwright.test
|
||||||
|
|
||||||
|
#####################
|
||||||
|
# Maildev Config #
|
||||||
|
#####################
|
||||||
|
MAILDEV_HTTP_PORT=1081
|
||||||
|
MAILDEV_SMTP_PORT=1026
|
||||||
|
MAILDEV_HOST=127.0.0.1
|
||||||
|
|
||||||
|
################
|
||||||
|
# Users Config #
|
||||||
|
################
|
||||||
|
TEST_USER=test
|
||||||
|
TEST_USER_PASSWORD=Master Password
|
||||||
|
TEST_USER_MAIL=${TEST_USER}@example.com
|
||||||
|
|
||||||
|
TEST_USER2=test2
|
||||||
|
TEST_USER2_PASSWORD=Master Password
|
||||||
|
TEST_USER2_MAIL=${TEST_USER2}@example.com
|
||||||
|
|
||||||
|
TEST_USER3=test3
|
||||||
|
TEST_USER3_PASSWORD=Master Password
|
||||||
|
TEST_USER3_MAIL=${TEST_USER3}@example.com
|
||||||
|
|
||||||
|
###################
|
||||||
|
# Keycloak Config #
|
||||||
|
###################
|
||||||
|
KEYCLOAK_ADMIN=admin
|
||||||
|
KEYCLOAK_ADMIN_PASSWORD=${KEYCLOAK_ADMIN}
|
||||||
|
KC_HTTP_HOST=127.0.0.1
|
||||||
|
KC_HTTP_PORT=8081
|
||||||
|
|
||||||
|
# Script parameters (use Keycloak and Vaultwarden config too)
|
||||||
|
TEST_REALM=test
|
||||||
|
DUMMY_REALM=dummy
|
||||||
|
DUMMY_AUTHORITY=http://${KC_HTTP_HOST}:${KC_HTTP_PORT}/realms/${DUMMY_REALM}
|
||||||
|
|
||||||
|
######################
|
||||||
|
# Vaultwarden Config #
|
||||||
|
######################
|
||||||
|
ROCKET_PORT=8003
|
||||||
|
DOMAIN=http://localhost:${ROCKET_PORT}
|
||||||
|
LOG_LEVEL=info,oidcwarden::sso=debug
|
||||||
|
LOGIN_RATELIMIT_MAX_BURST=100
|
||||||
|
|
||||||
|
SMTP_SECURITY=off
|
||||||
|
SMTP_PORT=${MAILDEV_SMTP_PORT}
|
||||||
|
SMTP_FROM_NAME=Vaultwarden
|
||||||
|
SMTP_TIMEOUT=5
|
||||||
|
|
||||||
|
SSO_CLIENT_ID=warden
|
||||||
|
SSO_CLIENT_SECRET=warden
|
||||||
|
SSO_AUTHORITY=http://${KC_HTTP_HOST}:${KC_HTTP_PORT}/realms/${TEST_REALM}
|
||||||
|
SSO_DEBUG_TOKENS=true
|
||||||
|
|
||||||
|
# Custom web-vault build
|
||||||
|
# PW_WV_REPO_URL=https://github.com/dani-garcia/bw_web_builds.git
|
||||||
|
# PW_WV_COMMIT_HASH=a5f5390895516bce2f48b7baadb6dc399e5fe75a
|
||||||
|
|
||||||
|
###########################
|
||||||
|
# Docker MariaDb container#
|
||||||
|
###########################
|
||||||
|
MARIADB_PORT=3307
|
||||||
|
MARIADB_ROOT_PASSWORD=warden
|
||||||
|
MARIADB_USER=warden
|
||||||
|
MARIADB_PASSWORD=warden
|
||||||
|
MARIADB_DATABASE=warden
|
||||||
|
|
||||||
|
###########################
|
||||||
|
# Docker Mysql container#
|
||||||
|
###########################
|
||||||
|
MYSQL_PORT=3309
|
||||||
|
MYSQL_ROOT_PASSWORD=warden
|
||||||
|
MYSQL_USER=warden
|
||||||
|
MYSQL_PASSWORD=warden
|
||||||
|
MYSQL_DATABASE=warden
|
||||||
|
|
||||||
|
############################
|
||||||
|
# Docker Postgres container#
|
||||||
|
############################
|
||||||
|
POSTGRES_PORT=5433
|
||||||
|
POSTGRES_USER=warden
|
||||||
|
POSTGRES_PASSWORD=warden
|
||||||
|
POSTGRES_DB=warden
|
37
playwright/tests/collection.spec.ts
Normal file
37
playwright/tests/collection.spec.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { test, expect, type TestInfo } from '@playwright/test';
|
||||||
|
|
||||||
|
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('Create', async ({ page }) => {
|
||||||
|
await createAccount(test, page, users.user1);
|
||||||
|
|
||||||
|
await test.step('Create Org', async () => {
|
||||||
|
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 page.getByRole('link', { name: 'Collections' }).click();
|
||||||
|
await page.getByRole('button', { name: 'New' }).click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Collection' }).click();
|
||||||
|
await page.getByLabel('Name (required)').fill('RandomCollec');
|
||||||
|
await page.getByRole('button', { name: 'Save' }).click();
|
||||||
|
await utils.checkNotification(page, 'Created collection RandomCollec');
|
||||||
|
await expect(page.getByRole('button', { name: 'RandomCollec' })).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
100
playwright/tests/login.smtp.spec.ts
Normal file
100
playwright/tests/login.smtp.spec.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import { test, expect, type TestInfo } from '@playwright/test';
|
||||||
|
import { MailDev } from 'maildev';
|
||||||
|
|
||||||
|
const utils = require('../global-utils');
|
||||||
|
import { createAccount, logUser } from './setups/user';
|
||||||
|
import { activateEmail, retrieveEmailCode, disableEmail } 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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Account creation', async ({ page }) => {
|
||||||
|
const mailBuffer = mailserver.buffer(users.user1.email);
|
||||||
|
|
||||||
|
await createAccount(test, page, users.user1, mailBuffer);
|
||||||
|
|
||||||
|
mailBuffer.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Login', async ({ context, page }) => {
|
||||||
|
const mailBuffer = mailserver.buffer(users.user1.email);
|
||||||
|
|
||||||
|
await logUser(test, page, users.user1, mailBuffer);
|
||||||
|
|
||||||
|
await test.step('verify email', async () => {
|
||||||
|
await page.getByText('Verify your account\'s 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');
|
||||||
|
|
||||||
|
const verify = await mailBuffer.expect((m) => m.subject === "Verify Your Email");
|
||||||
|
expect(verify.from[0]?.address).toBe(process.env.PW_SMTP_FROM);
|
||||||
|
|
||||||
|
const page2 = await context.newPage();
|
||||||
|
await page2.setContent(verify.html);
|
||||||
|
const link = await page2.getByTestId("verify").getAttribute("href");
|
||||||
|
await page2.close();
|
||||||
|
|
||||||
|
await page.goto(link);
|
||||||
|
await utils.checkNotification(page, 'Account email verified');
|
||||||
|
});
|
||||||
|
|
||||||
|
mailBuffer.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Activate 2fa', async ({ page }) => {
|
||||||
|
const emails = mailserver.buffer(users.user1.email);
|
||||||
|
|
||||||
|
await logUser(test, page, users.user1);
|
||||||
|
|
||||||
|
await activateEmail(test, page, users.user1, emails);
|
||||||
|
|
||||||
|
emails.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('2fa', async ({ page }) => {
|
||||||
|
const emails = mailserver.buffer(users.user1.email);
|
||||||
|
|
||||||
|
await test.step('login', async () => {
|
||||||
|
await page.goto('/');
|
||||||
|
|
||||||
|
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 expect(page).toHaveTitle(/Vaults/);
|
||||||
|
})
|
||||||
|
|
||||||
|
await disableEmail(test, page, users.user1);
|
||||||
|
|
||||||
|
emails.close();
|
||||||
|
});
|
51
playwright/tests/login.spec.ts
Normal file
51
playwright/tests/login.spec.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
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('Account creation', async ({ page }) => {
|
||||||
|
await createAccount(test, page, users.user1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Master password login', async ({ page }) => {
|
||||||
|
await logUser(test, page, users.user1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Authenticator 2fa', async ({ page }) => {
|
||||||
|
await logUser(test, page, users.user1);
|
||||||
|
|
||||||
|
let totp = await activateTOTP(test, page, users.user1);
|
||||||
|
|
||||||
|
await utils.logout(test, page, users.user1);
|
||||||
|
|
||||||
|
await test.step('login', async () => {
|
||||||
|
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);
|
||||||
|
});
|
115
playwright/tests/organization.smtp.spec.ts
Normal file
115
playwright/tests/organization.smtp.spec.ts
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import { test, expect, type TestInfo } from '@playwright/test';
|
||||||
|
import { MailDev } from 'maildev';
|
||||||
|
|
||||||
|
import * as utils from '../global-utils';
|
||||||
|
import * as orgs from './setups/orgs';
|
||||||
|
import { createAccount, logUser } from './setups/user';
|
||||||
|
|
||||||
|
let users = utils.loadEnv();
|
||||||
|
|
||||||
|
let mailServer, mail1Buffer, mail2Buffer, mail3Buffer;
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
|
mail1Buffer = mailServer.buffer(users.user1.email);
|
||||||
|
mail2Buffer = mailServer.buffer(users.user2.email);
|
||||||
|
mail3Buffer = mailServer.buffer(users.user3.email);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterAll('Teardown', async ({}, testInfo: TestInfo) => {
|
||||||
|
utils.stopVault(testInfo);
|
||||||
|
[mail1Buffer, mail2Buffer, mail3Buffer, mailServer].map((m) => m?.close());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Create user3', async ({ page }) => {
|
||||||
|
await createAccount(test, page, users.user3, mail3Buffer);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Invite users', async ({ page }) => {
|
||||||
|
await createAccount(test, page, users.user1, mail1Buffer);
|
||||||
|
|
||||||
|
await orgs.create(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.user3.email, {
|
||||||
|
navigate: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('invited with new account', async ({ page }) => {
|
||||||
|
const invited = await mail2Buffer.expect((mail) => mail.subject === 'Join Test');
|
||||||
|
|
||||||
|
await test.step('Create account', async () => {
|
||||||
|
await page.setContent(invited.html);
|
||||||
|
const link = await page.getByTestId('invite').getAttribute('href');
|
||||||
|
await page.goto(link);
|
||||||
|
await expect(page).toHaveTitle(/Create account | Vaultwarden Web/);
|
||||||
|
|
||||||
|
//await page.getByLabel('Name').fill(users.user2.name);
|
||||||
|
await page.getByLabel('New master password (required)', { exact: true }).fill(users.user2.password);
|
||||||
|
await page.getByLabel('Confirm new master password (').fill(users.user2.password);
|
||||||
|
await page.getByRole('button', { name: 'Create account' }).click();
|
||||||
|
await utils.checkNotification(page, 'Your new account has been created');
|
||||||
|
|
||||||
|
// Redirected to the vault
|
||||||
|
await expect(page).toHaveTitle('Vaults | Vaultwarden Web');
|
||||||
|
await utils.checkNotification(page, 'You have been logged in!');
|
||||||
|
await utils.checkNotification(page, 'Invitation accepted');
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Check mails', async () => {
|
||||||
|
await mail2Buffer.expect((m) => m.subject === 'Welcome');
|
||||||
|
await mail2Buffer.expect((m) => m.subject === 'New Device Logged In From Firefox');
|
||||||
|
await mail1Buffer.expect((m) => m.subject.includes('Invitation to Test accepted'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('invited with existing account', async ({ page }) => {
|
||||||
|
const invited = await mail3Buffer.expect((mail) => mail.subject === 'Join Test');
|
||||||
|
|
||||||
|
await page.setContent(invited.html);
|
||||||
|
const link = await page.getByTestId('invite').getAttribute('href');
|
||||||
|
|
||||||
|
await page.goto(link);
|
||||||
|
|
||||||
|
// We should be on login page with email prefilled
|
||||||
|
await expect(page).toHaveTitle(/Vaultwarden Web/);
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
|
||||||
|
// Unlock page
|
||||||
|
await page.getByLabel('Master password').fill(users.user3.password);
|
||||||
|
await page.getByRole('button', { name: 'Log in with master password' }).click();
|
||||||
|
|
||||||
|
// We are now in the default vault page
|
||||||
|
await expect(page).toHaveTitle(/Vaultwarden Web/);
|
||||||
|
await utils.checkNotification(page, 'Invitation accepted');
|
||||||
|
|
||||||
|
await mail3Buffer.expect((m) => m.subject === 'New Device Logged In From Firefox');
|
||||||
|
await mail1Buffer.expect((m) => m.subject.includes('Invitation to Test accepted'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Confirm invited user', async ({ page }) => {
|
||||||
|
await logUser(test, page, users.user1, mail1Buffer);
|
||||||
|
|
||||||
|
await orgs.members(test, page, 'Test');
|
||||||
|
await orgs.confirm(test, page, 'Test', users.user2.email);
|
||||||
|
|
||||||
|
await mail2Buffer.expect((m) => m.subject.includes('Invitation to Test confirmed'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Organization is visible', async ({ page }) => {
|
||||||
|
await logUser(test, page, users.user2, mail2Buffer);
|
||||||
|
await page.getByRole('button', { name: 'vault: Test', exact: true }).click();
|
||||||
|
await expect(page.getByLabel('Filter: Default collection')).toBeVisible();
|
||||||
|
});
|
54
playwright/tests/organization.spec.ts
Normal file
54
playwright/tests/organization.spec.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { test, expect, type TestInfo } from '@playwright/test';
|
||||||
|
import { MailDev } from 'maildev';
|
||||||
|
|
||||||
|
import * as utils from "../global-utils";
|
||||||
|
import * as orgs from './setups/orgs';
|
||||||
|
import { createAccount, logUser } 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('Invite', async ({ page }) => {
|
||||||
|
await createAccount(test, page, users.user3);
|
||||||
|
await createAccount(test, page, users.user1);
|
||||||
|
|
||||||
|
await orgs.create(test, page, 'New organisation');
|
||||||
|
await orgs.members(test, page, 'New organisation');
|
||||||
|
|
||||||
|
await test.step('missing user2', async () => {
|
||||||
|
await orgs.invite(test, page, 'New organisation', users.user2.email);
|
||||||
|
await expect(page.getByRole('row', { name: users.user2.email })).toHaveText(/Invited/);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('existing user3', async () => {
|
||||||
|
await orgs.invite(test, page, 'New organisation', users.user3.email);
|
||||||
|
await expect(page.getByRole('row', { name: users.user3.email })).toHaveText(/Needs confirmation/);
|
||||||
|
await orgs.confirm(test, page, 'New organisation', users.user3.email);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('confirm user2', async () => {
|
||||||
|
await createAccount(test, page, users.user2);
|
||||||
|
await logUser(test, page, users.user1);
|
||||||
|
await orgs.members(test, page, 'New organisation');
|
||||||
|
await orgs.confirm(test, page, 'New organisation', users.user2.email);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Org visible user2 ', async () => {
|
||||||
|
await logUser(test, page, users.user2);
|
||||||
|
await page.getByRole('button', { name: 'vault: New organisation', exact: true }).click();
|
||||||
|
await expect(page.getByLabel('Filter: Default collection')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Org visible user3 ', async () => {
|
||||||
|
await logUser(test, page, users.user3);
|
||||||
|
await page.getByRole('button', { name: 'vault: New organisation', exact: true }).click();
|
||||||
|
await expect(page.getByLabel('Filter: Default collection')).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
92
playwright/tests/setups/2fa.ts
Normal file
92
playwright/tests/setups/2fa.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { expect, type Page, Test } from '@playwright/test';
|
||||||
|
import { type MailBuffer } from 'maildev';
|
||||||
|
import * as OTPAuth from "otpauth";
|
||||||
|
|
||||||
|
import * as utils from '../../global-utils';
|
||||||
|
|
||||||
|
export async function activateTOTP(test: Test, page: Page, user: { name: string, password: string }): OTPAuth.TOTP {
|
||||||
|
return await test.step('Activate TOTP 2FA', 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.locator('bit-item').filter({ hasText: /Authenticator app/ }).getByRole('button').click();
|
||||||
|
await page.getByLabel('Master password (required)').fill(user.password);
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
|
||||||
|
const secret = await page.getByLabel('Key').innerText();
|
||||||
|
let totp = new OTPAuth.TOTP({ secret, period: 30 });
|
||||||
|
|
||||||
|
await page.getByLabel(/Verification code/).fill(totp.generate());
|
||||||
|
await page.getByRole('button', { name: 'Turn on' }).click();
|
||||||
|
await page.getByRole('heading', { name: 'Turned on', exact: true });
|
||||||
|
await page.getByLabel('Close').click();
|
||||||
|
|
||||||
|
return totp;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function disableTOTP(test: Test, page: Page, user: { password: string }) {
|
||||||
|
await test.step('Disable TOTP 2FA', async () => {
|
||||||
|
await page.getByRole('button', { name: 'Test' }).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.locator('bit-item').filter({ hasText: /Authenticator app/ }).getByRole('button').click();
|
||||||
|
await page.getByLabel('Master password (required)').click();
|
||||||
|
await page.getByLabel('Master password (required)').fill(user.password);
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
await page.getByRole('button', { name: 'Turn off' }).click();
|
||||||
|
await page.getByRole('button', { name: 'Yes' }).click();
|
||||||
|
await utils.checkNotification(page, 'Two-step login provider turned off');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function activateEmail(test: Test, page: Page, user: { name: string, password: string }, mailBuffer: MailBuffer) {
|
||||||
|
await test.step('Activate Email 2FA', 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.locator('bit-item').filter({ hasText: 'Email Email Enter a code sent' }).getByRole('button').click();
|
||||||
|
await page.getByLabel('Master password (required)').fill(user.password);
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
await page.getByRole('button', { name: 'Send email' }).click();
|
||||||
|
});
|
||||||
|
|
||||||
|
let code = await retrieveEmailCode(test, page, mailBuffer);
|
||||||
|
|
||||||
|
await test.step('input code', async () => {
|
||||||
|
await page.getByLabel('2. Enter the resulting 6').fill(code);
|
||||||
|
await page.getByRole('button', { name: 'Turn on' }).click();
|
||||||
|
await page.getByRole('heading', { name: 'Turned on', exact: true });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function retrieveEmailCode(test: Test, page: Page, mailBuffer: MailBuffer): string {
|
||||||
|
return await test.step('retrieve code', async () => {
|
||||||
|
const codeMail = await mailBuffer.expect((mail) => mail.subject.includes("Login Verification Code"));
|
||||||
|
const page2 = await page.context().newPage();
|
||||||
|
await page2.setContent(codeMail.html);
|
||||||
|
const code = await page2.getByTestId("2fa").innerText();
|
||||||
|
await page2.close();
|
||||||
|
return code;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function disableEmail(test: Test, page: Page, user: { password: string }) {
|
||||||
|
await test.step('Disable Email 2FA', async () => {
|
||||||
|
await page.getByRole('button', { name: 'Test' }).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.locator('bit-item').filter({ hasText: 'Email' }).getByRole('button').click();
|
||||||
|
await page.getByLabel('Master password (required)').click();
|
||||||
|
await page.getByLabel('Master password (required)').fill(user.password);
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
await page.getByRole('button', { name: 'Turn off' }).click();
|
||||||
|
await page.getByRole('button', { name: 'Yes' }).click();
|
||||||
|
|
||||||
|
await utils.checkNotification(page, 'Two-step login provider turned off');
|
||||||
|
});
|
||||||
|
}
|
7
playwright/tests/setups/db-setup.ts
Normal file
7
playwright/tests/setups/db-setup.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { test } from './db-test';
|
||||||
|
|
||||||
|
const utils = require('../../global-utils');
|
||||||
|
|
||||||
|
test('DB start', async ({ serviceName }) => {
|
||||||
|
utils.startComposeService(serviceName);
|
||||||
|
});
|
11
playwright/tests/setups/db-teardown.ts
Normal file
11
playwright/tests/setups/db-teardown.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { test } from './db-test';
|
||||||
|
|
||||||
|
const utils = require('../../global-utils');
|
||||||
|
|
||||||
|
utils.loadEnv();
|
||||||
|
|
||||||
|
test('DB teardown ?', async ({ serviceName }) => {
|
||||||
|
if( process.env.PW_KEEP_SERVICE_RUNNNING !== "true" ) {
|
||||||
|
utils.stopComposeService(serviceName);
|
||||||
|
}
|
||||||
|
});
|
9
playwright/tests/setups/db-test.ts
Normal file
9
playwright/tests/setups/db-test.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { test as base } from '@playwright/test';
|
||||||
|
|
||||||
|
export type TestOptions = {
|
||||||
|
serviceName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const test = base.extend<TestOptions>({
|
||||||
|
serviceName: ['', { option: true }],
|
||||||
|
});
|
77
playwright/tests/setups/orgs.ts
Normal file
77
playwright/tests/setups/orgs.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { expect, type Browser,Page } from '@playwright/test';
|
||||||
|
|
||||||
|
import * as utils from '../../global-utils';
|
||||||
|
|
||||||
|
export async function create(test, page: Page, name: string) {
|
||||||
|
await test.step('Create Org', async () => {
|
||||||
|
await page.locator('a').filter({ hasText: 'Password Manager' }).first().click();
|
||||||
|
await expect(page.getByTitle('All vaults', { exact: true })).toBeVisible();
|
||||||
|
await page.getByRole('link', { name: 'New organisation' }).click();
|
||||||
|
await page.getByLabel('Organisation name (required)').fill(name);
|
||||||
|
await page.getByRole('button', { name: 'Submit' }).click();
|
||||||
|
|
||||||
|
await utils.checkNotification(page, 'Organisation created');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function policies(test, page: Page, name: string) {
|
||||||
|
await test.step(`Navigate to ${name} policies`, async () => {
|
||||||
|
await page.locator('a').filter({ hasText: 'Admin Console' }).first().click();
|
||||||
|
await page.locator('org-switcher').getByLabel(/Toggle collapse/).click();
|
||||||
|
await page.locator('org-switcher').getByRole('link', { name: `${name}` }).first().click();
|
||||||
|
await expect(page.getByRole('heading', { name: `${name} collections` })).toBeVisible();
|
||||||
|
await page.getByRole('button', { name: 'Toggle collapse Settings' }).click();
|
||||||
|
await page.getByRole('link', { name: 'Policies' }).click();
|
||||||
|
await expect(page.getByRole('heading', { name: 'Policies' })).toBeVisible();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function members(test, page: Page, name: string) {
|
||||||
|
await test.step(`Navigate to ${name} members`, async () => {
|
||||||
|
await page.locator('a').filter({ hasText: 'Admin Console' }).first().click();
|
||||||
|
await page.locator('org-switcher').getByLabel(/Toggle collapse/).click();
|
||||||
|
await page.locator('org-switcher').getByRole('link', { name: `${name}` }).first().click();
|
||||||
|
await expect(page.getByRole('heading', { name: `${name} collections` })).toBeVisible();
|
||||||
|
await page.locator('div').filter({ hasText: 'Members' }).nth(2).click();
|
||||||
|
await expect(page.getByRole('heading', { name: 'Members' })).toBeVisible();
|
||||||
|
await expect(page.getByRole('cell', { name: 'All' })).toBeVisible();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function invite(test, page: Page, name: string, email: string) {
|
||||||
|
await test.step(`Invite ${email}`, async () => {
|
||||||
|
await expect(page.getByRole('heading', { name: 'Members' })).toBeVisible();
|
||||||
|
await page.getByRole('button', { name: 'Invite member' }).click();
|
||||||
|
await page.getByLabel('Email (required)').fill(email);
|
||||||
|
await page.getByRole('tab', { name: 'Collections' }).click();
|
||||||
|
await page.getByRole('combobox', { name: 'Permission' }).click();
|
||||||
|
await page.getByText('Edit items', { exact: true }).click();
|
||||||
|
await page.getByLabel('Select collections').click();
|
||||||
|
await page.getByText('Default collection').click();
|
||||||
|
await page.getByRole('cell', { name: 'Collection', exact: true }).click();
|
||||||
|
await page.getByRole('button', { name: 'Save' }).click();
|
||||||
|
await utils.checkNotification(page, 'User(s) invited');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function confirm(test, page: Page, name: string, user_email: string) {
|
||||||
|
await test.step(`Confirm ${user_email}`, async () => {
|
||||||
|
await expect(page.getByRole('heading', { name: 'Members' })).toBeVisible();
|
||||||
|
await page.getByRole('row').filter({hasText: user_email}).getByLabel('Options').click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Confirm' }).click();
|
||||||
|
await expect(page.getByRole('heading', { name: 'Confirm user' })).toBeVisible();
|
||||||
|
await page.getByRole('button', { name: 'Confirm' }).click();
|
||||||
|
await utils.checkNotification(page, 'confirmed');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function revoke(test, page: Page, name: string, user_email: string) {
|
||||||
|
await test.step(`Revoke ${user_email}`, async () => {
|
||||||
|
await expect(page.getByRole('heading', { name: 'Members' })).toBeVisible();
|
||||||
|
await page.getByRole('row').filter({hasText: user_email}).getByLabel('Options').click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Revoke access' }).click();
|
||||||
|
await expect(page.getByRole('heading', { name: 'Revoke access' })).toBeVisible();
|
||||||
|
await page.getByRole('button', { name: 'Revoke access' }).click();
|
||||||
|
await utils.checkNotification(page, 'Revoked organisation access');
|
||||||
|
});
|
||||||
|
}
|
18
playwright/tests/setups/sso-setup.ts
Normal file
18
playwright/tests/setups/sso-setup.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { test, expect, type TestInfo } from '@playwright/test';
|
||||||
|
|
||||||
|
const { exec } = require('node:child_process');
|
||||||
|
const utils = require('../../global-utils');
|
||||||
|
|
||||||
|
utils.loadEnv();
|
||||||
|
|
||||||
|
test.beforeAll('Setup', async () => {
|
||||||
|
console.log("Starting Keycloak");
|
||||||
|
exec(`docker compose --profile keycloak --env-file test.env up`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Keycloak is up', async ({ page }) => {
|
||||||
|
await utils.waitFor(process.env.SSO_AUTHORITY, page.context().browser());
|
||||||
|
// Dummy authority is created at the end of the setup
|
||||||
|
await utils.waitFor(process.env.DUMMY_AUTHORITY, page.context().browser());
|
||||||
|
console.log(`Keycloak running on: ${process.env.SSO_AUTHORITY}`);
|
||||||
|
});
|
15
playwright/tests/setups/sso-teardown.ts
Normal file
15
playwright/tests/setups/sso-teardown.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { test, type FullConfig } from '@playwright/test';
|
||||||
|
|
||||||
|
const { execSync } = require('node:child_process');
|
||||||
|
const utils = require('../../global-utils');
|
||||||
|
|
||||||
|
utils.loadEnv();
|
||||||
|
|
||||||
|
test('Keycloak teardown', async () => {
|
||||||
|
if( process.env.PW_KEEP_SERVICE_RUNNNING === "true" ) {
|
||||||
|
console.log("Keep Keycloak running");
|
||||||
|
} else {
|
||||||
|
console.log("Keycloak stopping");
|
||||||
|
execSync(`docker compose --profile keycloak --env-file test.env stop Keycloak`);
|
||||||
|
}
|
||||||
|
});
|
129
playwright/tests/setups/sso.ts
Normal file
129
playwright/tests/setups/sso.ts
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import { expect, type Page, Test } from '@playwright/test';
|
||||||
|
import { type MailBuffer, MailServer } from 'maildev';
|
||||||
|
import * as OTPAuth from "otpauth";
|
||||||
|
|
||||||
|
import * as utils from '../../global-utils';
|
||||||
|
import { retrieveEmailCode } from './2fa';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If a MailBuffer is passed it will be used and consume the expected emails
|
||||||
|
*/
|
||||||
|
export async function logNewUser(
|
||||||
|
test: Test,
|
||||||
|
page: Page,
|
||||||
|
user: { email: string, name: string, password: string },
|
||||||
|
options: { mailBuffer?: MailBuffer } = {}
|
||||||
|
) {
|
||||||
|
await test.step(`Create user ${user.name}`, async () => {
|
||||||
|
await page.context().clearCookies();
|
||||||
|
|
||||||
|
await test.step('Landing page', async () => {
|
||||||
|
await utils.cleanLanding(page);
|
||||||
|
|
||||||
|
await page.locator("input[type=email].vw-email-sso").fill(user.email);
|
||||||
|
await page.getByRole('button', { name: /Use single sign-on/ }).click();
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Keycloak login', async () => {
|
||||||
|
await expect(page.getByRole('heading', { name: 'Sign in to your account' })).toBeVisible();
|
||||||
|
await page.getByLabel(/Username/).fill(user.name);
|
||||||
|
await page.getByLabel('Password', { exact: true }).fill(user.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.getByLabel('New master password (required)', { exact: true }).fill(user.password);
|
||||||
|
await page.getByLabel('Confirm new master password (').fill(user.password);
|
||||||
|
await page.getByRole('button', { name: 'Create account' }).click();
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Default vault page', async () => {
|
||||||
|
await expect(page).toHaveTitle(/Vaultwarden Web/);
|
||||||
|
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 ){
|
||||||
|
let mailBuffer = options.mailBuffer;
|
||||||
|
await test.step('Check emails', async () => {
|
||||||
|
await mailBuffer.expect((m) => m.subject === "Welcome");
|
||||||
|
await mailBuffer.expect((m) => m.subject.includes("New Device Logged"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If a MailBuffer is passed it will be used and consume the expected emails
|
||||||
|
*/
|
||||||
|
export async function logUser(
|
||||||
|
test: Test,
|
||||||
|
page: Page,
|
||||||
|
user: { email: string, password: string },
|
||||||
|
options: {
|
||||||
|
mailBuffer ?: MailBuffer,
|
||||||
|
totp?: OTPAuth.TOTP,
|
||||||
|
mail2fa?: boolean,
|
||||||
|
} = {}
|
||||||
|
) {
|
||||||
|
let mailBuffer = options.mailBuffer;
|
||||||
|
|
||||||
|
await test.step(`Log user ${user.email}`, async () => {
|
||||||
|
await page.context().clearCookies();
|
||||||
|
|
||||||
|
await test.step('Landing page', async () => {
|
||||||
|
await utils.cleanLanding(page);
|
||||||
|
|
||||||
|
await page.locator("input[type=email].vw-email-sso").fill(user.email);
|
||||||
|
await page.getByRole('button', { name: /Use single sign-on/ }).click();
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Keycloak login', async () => {
|
||||||
|
await expect(page.getByRole('heading', { name: 'Sign in to your account' })).toBeVisible();
|
||||||
|
await page.getByLabel(/Username/).fill(user.name);
|
||||||
|
await page.getByLabel('Password', { exact: true }).fill(user.password);
|
||||||
|
await page.getByRole('button', { name: 'Sign In' }).click();
|
||||||
|
});
|
||||||
|
|
||||||
|
if( options.totp || options.mail2fa ){
|
||||||
|
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, mailBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.getByLabel(/Verification code/).fill(code);
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).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(user.password);
|
||||||
|
await page.getByRole('button', { name: 'Unlock' }).click();
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Default vault page', async () => {
|
||||||
|
await expect(page).toHaveTitle(/Vaultwarden Web/);
|
||||||
|
await expect(page.getByTitle('All vaults', { exact: true })).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
if( mailBuffer ){
|
||||||
|
await test.step('Check email', async () => {
|
||||||
|
await mailBuffer.expect((m) => m.subject.includes("New Device Logged"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
55
playwright/tests/setups/user.ts
Normal file
55
playwright/tests/setups/user.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { expect, type Browser, Page } from '@playwright/test';
|
||||||
|
|
||||||
|
import { type MailBuffer } from 'maildev';
|
||||||
|
|
||||||
|
import * as utils from '../../global-utils';
|
||||||
|
|
||||||
|
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 utils.cleanLanding(page);
|
||||||
|
|
||||||
|
await page.getByRole('link', { name: 'Create account' }).click();
|
||||||
|
|
||||||
|
// Back to Vault create account
|
||||||
|
await expect(page).toHaveTitle(/Create account | Vaultwarden Web/);
|
||||||
|
await page.getByLabel(/Email address/).fill(user.email);
|
||||||
|
await page.getByLabel('Name').fill(user.name);
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
|
||||||
|
// Vault finish Creation
|
||||||
|
await page.getByLabel('New master password (required)', { exact: true }).fill(user.password);
|
||||||
|
await page.getByLabel('Confirm new master password (').fill(user.password);
|
||||||
|
await page.getByRole('button', { name: 'Create account' }).click();
|
||||||
|
|
||||||
|
await utils.checkNotification(page, 'Your new account has been created')
|
||||||
|
|
||||||
|
// We are now in the default vault page
|
||||||
|
await expect(page).toHaveTitle('Vaults | Vaultwarden Web');
|
||||||
|
await utils.checkNotification(page, 'You have been logged in!');
|
||||||
|
|
||||||
|
if( mailBuffer ){
|
||||||
|
await mailBuffer.expect((m) => m.subject === "Welcome");
|
||||||
|
await mailBuffer.expect((m) => m.subject === "New Device Logged In From Firefox");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function logUser(test, page: Page, user: { email: string, password: string }, mailBuffer?: MailBuffer) {
|
||||||
|
await test.step(`Log user ${user.email}`, async () => {
|
||||||
|
await utils.cleanLanding(page);
|
||||||
|
|
||||||
|
await page.getByLabel(/Email address/).fill(user.email);
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
|
||||||
|
// Unlock page
|
||||||
|
await page.getByLabel('Master password').fill(user.password);
|
||||||
|
await page.getByRole('button', { name: 'Log in with master password' }).click();
|
||||||
|
|
||||||
|
// We are now in the default vault page
|
||||||
|
await expect(page).toHaveTitle(/Vaultwarden Web/);
|
||||||
|
|
||||||
|
if( mailBuffer ){
|
||||||
|
await mailBuffer.expect((m) => m.subject === "New Device Logged In From Firefox");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
53
playwright/tests/sso_login.smtp.spec.ts
Normal file
53
playwright/tests/sso_login.smtp.spec.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { test, expect, type TestInfo } from '@playwright/test';
|
||||||
|
import { MailDev } from 'maildev';
|
||||||
|
|
||||||
|
import { logNewUser, logUser } from './setups/sso';
|
||||||
|
import { activateEmail, disableEmail } from './setups/2fa';
|
||||||
|
import * as utils from "../global-utils";
|
||||||
|
|
||||||
|
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, {
|
||||||
|
SSO_ENABLED: true,
|
||||||
|
SSO_ONLY: false,
|
||||||
|
SMTP_HOST: process.env.MAILDEV_HOST,
|
||||||
|
SMTP_FROM: process.env.PW_SMTP_FROM,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterAll('Teardown', async ({}) => {
|
||||||
|
utils.stopVault();
|
||||||
|
if( mailserver ){
|
||||||
|
await mailserver.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Create and activate 2FA', async ({ page }) => {
|
||||||
|
const mailBuffer = mailserver.buffer(users.user1.email);
|
||||||
|
|
||||||
|
await logNewUser(test, page, users.user1, {mailBuffer: mailBuffer});
|
||||||
|
|
||||||
|
await activateEmail(test, page, users.user1, mailBuffer);
|
||||||
|
|
||||||
|
mailBuffer.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
mailBuffer.close();
|
||||||
|
});
|
85
playwright/tests/sso_login.spec.ts
Normal file
85
playwright/tests/sso_login.spec.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { test, expect, type TestInfo } from '@playwright/test';
|
||||||
|
|
||||||
|
import { logNewUser, logUser } from './setups/sso';
|
||||||
|
import { activateTOTP, disableTOTP } from './setups/2fa';
|
||||||
|
import * as utils from "../global-utils";
|
||||||
|
|
||||||
|
let users = utils.loadEnv();
|
||||||
|
|
||||||
|
test.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => {
|
||||||
|
await utils.startVault(browser, testInfo, {
|
||||||
|
SSO_ENABLED: true,
|
||||||
|
SSO_ONLY: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterAll('Teardown', async ({}) => {
|
||||||
|
utils.stopVault();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Account creation using SSO', async ({ page }) => {
|
||||||
|
// Landing page
|
||||||
|
await logNewUser(test, page, users.user1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('SSO login', async ({ page }) => {
|
||||||
|
await logUser(test, page, users.user1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Non SSO login', async ({ page }) => {
|
||||||
|
// Landing page
|
||||||
|
await page.goto('/');
|
||||||
|
await page.locator("input[type=email].vw-email-sso").fill(users.user1.email);
|
||||||
|
await page.getByRole('button', { name: 'Other' }).click();
|
||||||
|
|
||||||
|
// Unlock page
|
||||||
|
await page.getByLabel('Master password').fill(users.user1.password);
|
||||||
|
await page.getByRole('button', { name: 'Log in with master password' }).click();
|
||||||
|
|
||||||
|
// We are now in the default vault page
|
||||||
|
await expect(page).toHaveTitle(/Vaultwarden Web/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('SSO login with TOTP 2fa', async ({ page }) => {
|
||||||
|
await logUser(test, page, users.user1);
|
||||||
|
|
||||||
|
let totp = await activateTOTP(test, page, users.user1);
|
||||||
|
|
||||||
|
await logUser(test, page, users.user1, { totp });
|
||||||
|
|
||||||
|
await disableTOTP(test, page, users.user1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Non SSO login impossible', async ({ page, browser }, testInfo: TestInfo) => {
|
||||||
|
await utils.restartVault(page, testInfo, {
|
||||||
|
SSO_ENABLED: true,
|
||||||
|
SSO_ONLY: true
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
// Landing page
|
||||||
|
await page.goto('/');
|
||||||
|
|
||||||
|
// Check that SSO login is available
|
||||||
|
await expect(page.getByRole('button', { name: /Use single sign-on/ })).toHaveCount(1);
|
||||||
|
|
||||||
|
// No Continue/Other
|
||||||
|
await expect(page.getByRole('button', { name: 'Other' })).toHaveCount(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test('No SSO login', async ({ page }, testInfo: TestInfo) => {
|
||||||
|
await utils.restartVault(page, testInfo, {
|
||||||
|
SSO_ENABLED: false
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
// Landing page
|
||||||
|
await page.goto('/');
|
||||||
|
|
||||||
|
// No SSO button (rely on a correct selector checked in previous test)
|
||||||
|
await expect(page.getByRole('button', { name: /Use single sign-on/ })).toHaveCount(0);
|
||||||
|
|
||||||
|
// Can continue to Master password
|
||||||
|
await page.getByLabel(/Email address/).fill(users.user1.email);
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
await expect(page.getByRole('button', { name: 'Log in with master password' })).toHaveCount(1);
|
||||||
|
});
|
121
playwright/tests/sso_organization.smtp.spec.ts
Normal file
121
playwright/tests/sso_organization.smtp.spec.ts
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import { test, expect, type TestInfo } from '@playwright/test';
|
||||||
|
import { MailDev } from 'maildev';
|
||||||
|
|
||||||
|
import * as utils from "../global-utils";
|
||||||
|
import * as orgs from './setups/orgs';
|
||||||
|
import { logNewUser, logUser } from './setups/sso';
|
||||||
|
|
||||||
|
let users = utils.loadEnv();
|
||||||
|
|
||||||
|
let mailServer, mail1Buffer, mail2Buffer, mail3Buffer;
|
||||||
|
|
||||||
|
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,
|
||||||
|
SSO_ENABLED: true,
|
||||||
|
SSO_ONLY: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
mail1Buffer = mailServer.buffer(users.user1.email);
|
||||||
|
mail2Buffer = mailServer.buffer(users.user2.email);
|
||||||
|
mail3Buffer = mailServer.buffer(users.user3.email);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterAll('Teardown', async ({}) => {
|
||||||
|
utils.stopVault();
|
||||||
|
[mail1Buffer, mail2Buffer, mail3Buffer, mailServer].map((m) => m?.close());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Create user3', async ({ page }) => {
|
||||||
|
await logNewUser(test, page, users.user3, { mailBuffer: mail3Buffer });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Invite users', async ({ page }) => {
|
||||||
|
await logNewUser(test, page, users.user1, { mailBuffer: mail1Buffer });
|
||||||
|
|
||||||
|
await orgs.create(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.user3.email);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('invited with new account', async ({ page }) => {
|
||||||
|
const link = await test.step('Extract email link', async () => {
|
||||||
|
const invited = await mail2Buffer.expect((m) => m.subject === "Join /Test");
|
||||||
|
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.getByLabel('New master password (required)', { exact: true }).fill(users.user2.password);
|
||||||
|
await page.getByLabel('Confirm new 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(/Vaultwarden Web/);
|
||||||
|
|
||||||
|
await utils.checkNotification(page, 'Account successfully created!');
|
||||||
|
await utils.checkNotification(page, 'Invitation accepted');
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Check mails', async () => {
|
||||||
|
await mail2Buffer.expect((m) => m.subject.includes("New Device Logged"));
|
||||||
|
await mail1Buffer.expect((m) => m.subject === "Invitation to /Test accepted");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('invited with existing account', async ({ page }) => {
|
||||||
|
const link = await test.step('Extract email link', async () => {
|
||||||
|
const invited = await mail3Buffer.expect((m) => m.subject === "Join /Test");
|
||||||
|
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.user3.name);
|
||||||
|
await page.getByLabel('Password', { exact: true }).fill(users.user3.password);
|
||||||
|
await page.getByRole('button', { name: 'Sign In' }).click();
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Unlock vault', async () => {
|
||||||
|
await expect(page).toHaveTitle('Vaultwarden Web');
|
||||||
|
await page.getByLabel('Master password').fill(users.user3.password);
|
||||||
|
await page.getByRole('button', { name: 'Unlock' }).click();
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Default vault page', async () => {
|
||||||
|
await expect(page).toHaveTitle(/Vaultwarden Web/);
|
||||||
|
await utils.checkNotification(page, 'Invitation accepted');
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Check mails', async () => {
|
||||||
|
await mail3Buffer.expect((m) => m.subject.includes("New Device Logged"));
|
||||||
|
await mail1Buffer.expect((m) => m.subject === "Invitation to /Test accepted");
|
||||||
|
});
|
||||||
|
});
|
76
playwright/tests/sso_organization.spec.ts
Normal file
76
playwright/tests/sso_organization.spec.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { test, expect, type TestInfo } from '@playwright/test';
|
||||||
|
import { MailDev } from 'maildev';
|
||||||
|
|
||||||
|
import * as utils from "../global-utils";
|
||||||
|
import * as orgs from './setups/orgs';
|
||||||
|
import { logNewUser, logUser } from './setups/sso';
|
||||||
|
|
||||||
|
let users = utils.loadEnv();
|
||||||
|
|
||||||
|
test.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => {
|
||||||
|
await utils.startVault(browser, testInfo, {
|
||||||
|
SSO_ENABLED: true,
|
||||||
|
SSO_ONLY: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterAll('Teardown', async ({}) => {
|
||||||
|
utils.stopVault();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Create user3', async ({ page }) => {
|
||||||
|
await logNewUser(test, page, users.user3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Invite users', async ({ page }) => {
|
||||||
|
await logNewUser(test, page, users.user1);
|
||||||
|
|
||||||
|
await orgs.create(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.user3.email);
|
||||||
|
await orgs.confirm(test, page, '/Test', users.user3.email);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Create invited account', async ({ page }) => {
|
||||||
|
await logNewUser(test, page, users.user2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Confirm invited user', async ({ page }) => {
|
||||||
|
await logUser(test, page, users.user1);
|
||||||
|
await orgs.members(test, page, '/Test');
|
||||||
|
await expect(page.getByRole('row', { name: users.user2.name })).toHaveText(/Needs confirmation/);
|
||||||
|
await orgs.confirm(test, page, '/Test', users.user2.email);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Organization is visible', async ({ page }) => {
|
||||||
|
await logUser(test, page, users.user2);
|
||||||
|
await page.getByLabel('vault: /Test').click();
|
||||||
|
await expect(page.getByLabel('Filter: Default collection')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Enforce password policy', async ({ page }) => {
|
||||||
|
await logUser(test, page, users.user1);
|
||||||
|
await orgs.policies(test, page, '/Test');
|
||||||
|
|
||||||
|
await test.step(`Set master password policy`, async () => {
|
||||||
|
await page.getByRole('button', { name: 'Master password requirements' }).click();
|
||||||
|
await page.getByRole('checkbox', { name: 'Turn on' }).check();
|
||||||
|
await page.getByRole('checkbox', { name: 'Require existing members to' }).check();
|
||||||
|
await page.getByRole('spinbutton', { name: 'Minimum length' }).fill('42');
|
||||||
|
await page.getByRole('button', { name: 'Save' }).click();
|
||||||
|
await utils.checkNotification(page, 'Edited policy Master password requirements.');
|
||||||
|
});
|
||||||
|
|
||||||
|
await utils.logout(test, page, users.user1);
|
||||||
|
|
||||||
|
await test.step(`Unlock trigger policy`, async () => {
|
||||||
|
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.getByRole('textbox', { name: 'Master password (required)' }).fill(users.user1.password);
|
||||||
|
await page.getByRole('button', { name: 'Unlock' }).click();
|
||||||
|
|
||||||
|
await expect(page.getByRole('heading', { name: 'Update master password' })).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user