Compare commits

..

18 Commits

Author SHA1 Message Date
Daniel García
9e4d372213 Update web vault to 2.25.0 2021-12-13 00:02:13 +01:00
Daniel García
d0bf0ab237 Merge pull request #2125 from BlackDex/trust-dns
Enabled trust-dns and some updates.
2021-12-08 00:26:31 +01:00
BlackDex
e327583aa5 Enabled trust-dns and some updates.
- Enabled trust-dns feature which seems to help a bit when DNS is
causing long timeouts. Though in the blocking version it is less visible
then on the async branch.
- Updated crates
- Removed some redundant code
- Updated javascript/css libraries

Resolves #2118
Resolves #2119
2021-12-01 19:01:55 +01:00
Daniel García
ead2f02cbd Merge pull request #2084 from BlackDex/minimize-macro-recursion
Macro recursion decrease and other optimizations
2021-11-06 22:00:37 +01:00
BlackDex
c453528dc1 Macro recursion decrease and other optimizations
- Decreased `recursion_limit` from 512 to 87
  Mainly done by optimizing the config macro's.
  This fixes an issue with the rust-analyzer which doesn't go beyond 128
- Removed Regex for masking sensitive values and replaced it with a map()
  This is much faster then using a Regex.
- Refactored the get_support_json macro's
- All items above also lowered the binary size and possibly compile-time
- Removed `_conn: DbConn` from several functions, these caused unnecessary database connections for functions who didn't used that at all
- Decreased json response for `/plans`
- Updated libraries and where needed some code changes
  This also fixes some rare issues with SMTP https://github.com/lettre/lettre/issues/678
- Using Rust 2021 instead of 2018
- Updated rust nightly
2021-11-06 17:44:53 +01:00
Daniel García
6ae48aa8c2 Merge pull request #2080 from jjlin/fix-postgres-migration
Fix PostgreSQL migration
2021-11-01 14:33:47 +01:00
Daniel García
88643fd9d5 Merge pull request #2078 from jjlin/fix-ea-reject
Fix missing encrypted key after emergency access reject
2021-11-01 14:33:39 +01:00
Daniel García
73e0002219 Merge pull request #2073 from jjlin/fix-access-logic
Fix conflict resolution logic for `read_only` and `hide_passwords` flags
2021-11-01 14:33:29 +01:00
Jeremy Lin
c49ee47de0 Fix PostgreSQL migration
The PostgreSQL migration should have used `TIMESTAMP` rather than `DATETIME`.
2021-10-31 17:50:00 -07:00
Jeremy Lin
14408396bb Fix missing encrypted key after emergency access reject
Rejecting an emergency access request should transition the grantor/grantee
relationship back into the `Confirmed` state, and the grantor's encrypted key
should remain in escrow rather than being cleared, or else future emergency
access requsts from that grantee will fail.
2021-10-31 02:14:18 -07:00
Jeremy Lin
6cbb724069 Fix conflict resolution logic for read_only and hide_passwords flags
For one of these flags to be in effect for a cipher, upstream requires all of
(rather than any of) the collections the cipher is in to have that flag set.

Also, some of the logic for loading access restrictions was wrong. I think
that only malicious clients that also had knowledge of the UUIDs of ciphers
they didn't have access to would have been able to take advantage of that.
2021-10-29 13:47:56 -07:00
Daniel García
a2316ca091 Merge pull request #2067 from jjlin/incomplete-2fa
Add email notifications for incomplete 2FA logins
2021-10-28 18:00:03 +02:00
Jeremy Lin
c476e19796 Add email notifications for incomplete 2FA logins
An incomplete 2FA login is one where the correct master password was provided,
but the 2FA token or action required to complete the login was not provided
within the configured time limit. This potentially indicates that the user's
master password has been compromised, but the login was blocked by 2FA.

Be aware that the 2FA step can usually still be completed after the email
notification has already been sent out, which could be confusing. Therefore,
the incomplete 2FA time limit should be long enough that this situation would
be unlikely. This feature can also be disabled entirely if desired.
2021-10-28 00:19:43 -07:00
Daniel García
9f393cfd9d Formatting 2021-10-27 23:00:26 +02:00
Daniel García
450c4d4d97 Update web vault to 2.24.1 2021-10-27 22:46:12 +02:00
Daniel García
75e62abed0 Move database_max_conns 2021-10-24 22:22:28 +02:00
Daniel García
97f9eb1320 Update dependencies 2021-10-24 21:50:26 +02:00
Daniel García
53cc8a65af Add doc comments to the functions in Config, and remove some unneeded pubs 2021-10-23 20:47:05 +02:00
55 changed files with 1928 additions and 1013 deletions

View File

@@ -82,6 +82,10 @@
## Defaults to daily (5 minutes after midnight). Set blank to disable this job.
# TRASH_PURGE_SCHEDULE="0 5 0 * * *"
##
## Cron schedule of the job that checks for incomplete 2FA logins.
## Defaults to once every minute. Set blank to disable this job.
# INCOMPLETE_2FA_SCHEDULE="30 * * * * *"
##
## Cron schedule of the job that sends expiration reminders to emergency access grantors.
## Defaults to hourly (5 minutes after the hour). Set blank to disable this job.
# EMERGENCY_NOTIFICATION_REMINDER_SCHEDULE="0 5 * * * *"
@@ -220,6 +224,13 @@
## This setting applies globally, so make sure to inform all users of any changes to this setting.
# TRASH_AUTO_DELETE_DAYS=
## Number of minutes to wait before a 2FA-enabled login is considered incomplete,
## resulting in an email notification. An incomplete 2FA login is one where the correct
## master password was provided but the required 2FA step was not completed, which
## potentially indicates a master password compromise. Set to 0 to disable this check.
## This setting applies globally to all users.
# INCOMPLETE_2FA_TIME_LIMIT=3
## Controls the PBBKDF password iterations to apply on the server
## The change only applies when the password is changed
# PASSWORD_ITERATIONS=100000

502
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
name = "vaultwarden"
version = "1.0.0"
authors = ["Daniel García <dani-garcia@users.noreply.github.com>"]
edition = "2018"
edition = "2021"
rust-version = "1.57"
resolver = "2"
@@ -34,11 +34,11 @@ rocket = { version = "=0.5.0-dev", features = ["tls"], default-features = false
rocket_contrib = "=0.5.0-dev"
# HTTP client
reqwest = { version = "0.11.5", features = ["blocking", "json", "gzip", "brotli", "socks", "cookies"] }
reqwest = { version = "0.11.7", features = ["blocking", "json", "gzip", "brotli", "socks", "cookies", "trust-dns"] }
# Used for custom short lived cookie jar
cookie = "0.15.1"
cookie_store = "0.15.0"
cookie_store = "0.15.1"
bytes = "1.1.0"
url = "2.2.2"
@@ -46,7 +46,7 @@ url = "2.2.2"
multipart = { version = "0.18.0", features = ["server"], default-features = false }
# WebSockets library
ws = { version = "0.11.0", package = "parity-ws" }
ws = { version = "0.11.1", package = "parity-ws" }
# MessagePack library
rmpv = "1.0.0"
@@ -56,7 +56,7 @@ chashmap = "2.2.2"
# A generic serialization/deserialization framework
serde = { version = "1.0.130", features = ["derive"] }
serde_json = "1.0.68"
serde_json = "1.0.72"
# Logging
log = "0.4.14"
@@ -95,7 +95,7 @@ jsonwebtoken = "7.2.0"
# U2F library
u2f = "0.2.0"
webauthn-rs = "0.3.0-alpha.12"
webauthn-rs = "0.3.0"
# Yubico Library
yubico = { version = "0.10.0", features = ["online-tokio"], default-features = false }
@@ -112,19 +112,19 @@ num-derive = "0.3.3"
# Email libraries
tracing = { version = "0.1.29", features = ["log"] } # Needed to have lettre trace logging used when SMTP_DEBUG is enabled.
lettre = { version = "0.10.0-rc.3", features = ["smtp-transport", "builder", "serde", "native-tls", "hostname", "tracing"], default-features = false }
lettre = { version = "0.10.0-rc.4", features = ["smtp-transport", "builder", "serde", "native-tls", "hostname", "tracing"], default-features = false }
# Template library
handlebars = { version = "4.1.3", features = ["dir_source"] }
handlebars = { version = "4.1.5", features = ["dir_source"] }
# For favicon extraction from main website
html5ever = "0.25.1"
markup5ever_rcdom = "0.1.0"
regex = { version = "1.5.4", features = ["std", "perf", "unicode-perl"], default-features = false }
data-url = "0.1.0"
data-url = "0.1.1"
# Used by U2F, JWT and Postgres
openssl = "0.10.36"
openssl = "0.10.38"
# URL encoding library
percent-encoding = "2.1.0"
@@ -135,19 +135,16 @@ idna = "0.2.3"
pico-args = "0.4.2"
# Logging panics to logfile instead stderr only
backtrace = "0.3.61"
backtrace = "0.3.63"
# Macro ident concatenation
paste = "1.0.5"
paste = "1.0.6"
[patch.crates-io]
# Use newest ring
rocket = { git = 'https://github.com/SergioBenitez/Rocket', rev = '263e39b5b429de1913ce7e3036575a7b4d88b6d7' }
rocket_contrib = { git = 'https://github.com/SergioBenitez/Rocket', rev = '263e39b5b429de1913ce7e3036575a7b4d88b6d7' }
# For favicon extraction from main website
data-url = { git = 'https://github.com/servo/rust-url', package="data-url", rev = 'eb7330b5296c0d43816d1346211b74182bb4ae37' }
# The maintainer of the `job_scheduler` crate doesn't seem to have responded
# to any issues or PRs for almost a year (as of April 2021). This hopefully
# temporary fork updates Cargo.toml to use more up-to-date dependencies.

View File

@@ -6,7 +6,7 @@
{% set build_stage_base_image = "rust:1.55-buster" %}
{% if "alpine" in target_file %}
{% if "amd64" in target_file %}
{% set build_stage_base_image = "clux/muslrust:nightly-2021-10-06" %}
{% set build_stage_base_image = "clux/muslrust:nightly-2021-10-23" %}
{% set runtime_stage_base_image = "alpine:3.14" %}
{% set package_arch_target = "x86_64-unknown-linux-musl" %}
{% elif "armv7" in target_file %}
@@ -51,8 +51,8 @@
# https://docs.docker.com/develop/develop-images/multistage-build/
# https://whitfin.io/speeding-up-rust-docker-builds/
####################### VAULT BUILD IMAGE #######################
{% set vault_version = "2.23.0c" %}
{% set vault_image_digest = "sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459" %}
{% set vault_version = "2.25.0" %}
{% set vault_image_digest = "sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527" %}
# The web-vault digest specifies a particular web-vault build on Docker Hub.
# Using the digest instead of the tag name provides better security,
# as the digest of an image is immutable, whereas a tag name can later

View File

@@ -16,15 +16,15 @@
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
# click the tag name to view the digest of the image it currently points to.
# - From the command line:
# $ docker pull vaultwarden/web-vault:v2.23.0c
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.23.0c
# [vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459]
# $ docker pull vaultwarden/web-vault:v2.25.0
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.25.0
# [vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527]
#
# - Conversely, to get the tag name from the digest:
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459
# [vaultwarden/web-vault:v2.23.0c]
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527
# [vaultwarden/web-vault:v2.25.0]
#
FROM vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459 as vault
FROM vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527 as vault
########################## BUILD IMAGE ##########################
FROM rust:1.55-buster as build

View File

@@ -16,18 +16,18 @@
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
# click the tag name to view the digest of the image it currently points to.
# - From the command line:
# $ docker pull vaultwarden/web-vault:v2.23.0c
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.23.0c
# [vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459]
# $ docker pull vaultwarden/web-vault:v2.25.0
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.25.0
# [vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527]
#
# - Conversely, to get the tag name from the digest:
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459
# [vaultwarden/web-vault:v2.23.0c]
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527
# [vaultwarden/web-vault:v2.25.0]
#
FROM vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459 as vault
FROM vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527 as vault
########################## BUILD IMAGE ##########################
FROM clux/muslrust:nightly-2021-10-06 as build
FROM clux/muslrust:nightly-2021-10-23 as build
# Alpine-based AMD64 (musl) does not support mysql/mariadb during compile time.
ARG DB=sqlite,postgresql

View File

@@ -16,15 +16,15 @@
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
# click the tag name to view the digest of the image it currently points to.
# - From the command line:
# $ docker pull vaultwarden/web-vault:v2.23.0c
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.23.0c
# [vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459]
# $ docker pull vaultwarden/web-vault:v2.25.0
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.25.0
# [vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527]
#
# - Conversely, to get the tag name from the digest:
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459
# [vaultwarden/web-vault:v2.23.0c]
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527
# [vaultwarden/web-vault:v2.25.0]
#
FROM vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459 as vault
FROM vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527 as vault
########################## BUILD IMAGE ##########################
FROM rust:1.55-buster as build

View File

@@ -16,18 +16,18 @@
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
# click the tag name to view the digest of the image it currently points to.
# - From the command line:
# $ docker pull vaultwarden/web-vault:v2.23.0c
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.23.0c
# [vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459]
# $ docker pull vaultwarden/web-vault:v2.25.0
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.25.0
# [vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527]
#
# - Conversely, to get the tag name from the digest:
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459
# [vaultwarden/web-vault:v2.23.0c]
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527
# [vaultwarden/web-vault:v2.25.0]
#
FROM vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459 as vault
FROM vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527 as vault
########################## BUILD IMAGE ##########################
FROM clux/muslrust:nightly-2021-10-06 as build
FROM clux/muslrust:nightly-2021-10-23 as build
# Alpine-based AMD64 (musl) does not support mysql/mariadb during compile time.
ARG DB=sqlite,postgresql

View File

@@ -16,15 +16,15 @@
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
# click the tag name to view the digest of the image it currently points to.
# - From the command line:
# $ docker pull vaultwarden/web-vault:v2.23.0c
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.23.0c
# [vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459]
# $ docker pull vaultwarden/web-vault:v2.25.0
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.25.0
# [vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527]
#
# - Conversely, to get the tag name from the digest:
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459
# [vaultwarden/web-vault:v2.23.0c]
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527
# [vaultwarden/web-vault:v2.25.0]
#
FROM vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459 as vault
FROM vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527 as vault
########################## BUILD IMAGE ##########################
FROM rust:1.55-buster as build

View File

@@ -16,15 +16,15 @@
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
# click the tag name to view the digest of the image it currently points to.
# - From the command line:
# $ docker pull vaultwarden/web-vault:v2.23.0c
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.23.0c
# [vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459]
# $ docker pull vaultwarden/web-vault:v2.25.0
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.25.0
# [vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527]
#
# - Conversely, to get the tag name from the digest:
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459
# [vaultwarden/web-vault:v2.23.0c]
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527
# [vaultwarden/web-vault:v2.25.0]
#
FROM vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459 as vault
FROM vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527 as vault
########################## BUILD IMAGE ##########################
FROM rust:1.55-buster as build

View File

@@ -16,15 +16,15 @@
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
# click the tag name to view the digest of the image it currently points to.
# - From the command line:
# $ docker pull vaultwarden/web-vault:v2.23.0c
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.23.0c
# [vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459]
# $ docker pull vaultwarden/web-vault:v2.25.0
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.25.0
# [vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527]
#
# - Conversely, to get the tag name from the digest:
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459
# [vaultwarden/web-vault:v2.23.0c]
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527
# [vaultwarden/web-vault:v2.25.0]
#
FROM vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459 as vault
FROM vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527 as vault
########################## BUILD IMAGE ##########################
FROM rust:1.55-buster as build

View File

@@ -16,15 +16,15 @@
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
# click the tag name to view the digest of the image it currently points to.
# - From the command line:
# $ docker pull vaultwarden/web-vault:v2.23.0c
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.23.0c
# [vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459]
# $ docker pull vaultwarden/web-vault:v2.25.0
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.25.0
# [vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527]
#
# - Conversely, to get the tag name from the digest:
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459
# [vaultwarden/web-vault:v2.23.0c]
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527
# [vaultwarden/web-vault:v2.25.0]
#
FROM vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459 as vault
FROM vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527 as vault
########################## BUILD IMAGE ##########################
FROM rust:1.55-buster as build

View File

@@ -16,15 +16,15 @@
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
# click the tag name to view the digest of the image it currently points to.
# - From the command line:
# $ docker pull vaultwarden/web-vault:v2.23.0c
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.23.0c
# [vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459]
# $ docker pull vaultwarden/web-vault:v2.25.0
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.25.0
# [vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527]
#
# - Conversely, to get the tag name from the digest:
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459
# [vaultwarden/web-vault:v2.23.0c]
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527
# [vaultwarden/web-vault:v2.25.0]
#
FROM vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459 as vault
FROM vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527 as vault
########################## BUILD IMAGE ##########################
FROM rust:1.55-buster as build

View File

@@ -16,15 +16,15 @@
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
# click the tag name to view the digest of the image it currently points to.
# - From the command line:
# $ docker pull vaultwarden/web-vault:v2.23.0c
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.23.0c
# [vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459]
# $ docker pull vaultwarden/web-vault:v2.25.0
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.25.0
# [vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527]
#
# - Conversely, to get the tag name from the digest:
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459
# [vaultwarden/web-vault:v2.23.0c]
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527
# [vaultwarden/web-vault:v2.25.0]
#
FROM vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459 as vault
FROM vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527 as vault
########################## BUILD IMAGE ##########################
FROM messense/rust-musl-cross:armv7-musleabihf as build

View File

@@ -16,15 +16,15 @@
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
# click the tag name to view the digest of the image it currently points to.
# - From the command line:
# $ docker pull vaultwarden/web-vault:v2.23.0c
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.23.0c
# [vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459]
# $ docker pull vaultwarden/web-vault:v2.25.0
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.25.0
# [vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527]
#
# - Conversely, to get the tag name from the digest:
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459
# [vaultwarden/web-vault:v2.23.0c]
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527
# [vaultwarden/web-vault:v2.25.0]
#
FROM vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459 as vault
FROM vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527 as vault
########################## BUILD IMAGE ##########################
FROM rust:1.55-buster as build

View File

@@ -16,15 +16,15 @@
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
# click the tag name to view the digest of the image it currently points to.
# - From the command line:
# $ docker pull vaultwarden/web-vault:v2.23.0c
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.23.0c
# [vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459]
# $ docker pull vaultwarden/web-vault:v2.25.0
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.25.0
# [vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527]
#
# - Conversely, to get the tag name from the digest:
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459
# [vaultwarden/web-vault:v2.23.0c]
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527
# [vaultwarden/web-vault:v2.25.0]
#
FROM vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459 as vault
FROM vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527 as vault
########################## BUILD IMAGE ##########################
FROM messense/rust-musl-cross:armv7-musleabihf as build

View File

@@ -0,0 +1 @@
DROP TABLE twofactor_incomplete;

View File

@@ -0,0 +1,9 @@
CREATE TABLE twofactor_incomplete (
user_uuid CHAR(36) NOT NULL REFERENCES users(uuid),
device_uuid CHAR(36) NOT NULL,
device_name TEXT NOT NULL,
login_time DATETIME NOT NULL,
ip_address TEXT NOT NULL,
PRIMARY KEY (user_uuid, device_uuid)
);

View File

@@ -0,0 +1 @@
DROP TABLE twofactor_incomplete;

View File

@@ -0,0 +1,9 @@
CREATE TABLE twofactor_incomplete (
user_uuid VARCHAR(40) NOT NULL REFERENCES users(uuid),
device_uuid VARCHAR(40) NOT NULL,
device_name TEXT NOT NULL,
login_time TIMESTAMP NOT NULL,
ip_address TEXT NOT NULL,
PRIMARY KEY (user_uuid, device_uuid)
);

View File

@@ -0,0 +1 @@
DROP TABLE twofactor_incomplete;

View File

@@ -0,0 +1,9 @@
CREATE TABLE twofactor_incomplete (
user_uuid TEXT NOT NULL REFERENCES users(uuid),
device_uuid TEXT NOT NULL,
device_name TEXT NOT NULL,
login_time DATETIME NOT NULL,
ip_address TEXT NOT NULL,
PRIMARY KEY (user_uuid, device_uuid)
);

View File

@@ -1 +1 @@
nightly-2021-10-14
nightly-2021-11-05

View File

@@ -1,7 +1,7 @@
use once_cell::sync::Lazy;
use serde::de::DeserializeOwned;
use serde_json::Value;
use std::{env, time::Duration};
use std::env;
use rocket::{
http::{Cookie, Cookies, SameSite, Status},
@@ -236,7 +236,7 @@ impl AdminTemplateData {
}
#[get("/", rank = 1)]
fn admin_page(_token: AdminToken, _conn: DbConn) -> ApiResult<Html<String>> {
fn admin_page(_token: AdminToken) -> ApiResult<Html<String>> {
let text = AdminTemplateData::new().render()?;
Ok(Html(text))
}
@@ -462,13 +462,13 @@ struct GitCommit {
fn get_github_api<T: DeserializeOwned>(url: &str) -> Result<T, Error> {
let github_api = get_reqwest_client();
Ok(github_api.get(url).timeout(Duration::from_secs(10)).send()?.error_for_status()?.json::<T>()?)
Ok(github_api.get(url).send()?.error_for_status()?.json::<T>()?)
}
fn has_http_access() -> bool {
let http_access = get_reqwest_client();
match http_access.head("https://github.com/dani-garcia/vaultwarden").timeout(Duration::from_secs(10)).send() {
match http_access.head("https://github.com/dani-garcia/vaultwarden").send() {
Ok(r) => r.status().is_success(),
_ => false,
}
@@ -494,7 +494,6 @@ fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> ApiResu
// Execute some environment checks
let running_within_docker = is_running_in_docker();
let docker_base_image = docker_base_image();
let has_http_access = has_http_access();
let uses_proxy = env::var_os("HTTP_PROXY").is_some()
|| env::var_os("http_proxy").is_some()
@@ -552,7 +551,7 @@ fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> ApiResu
"web_vault_version": web_vault_version.version,
"latest_web_build": latest_web_build,
"running_within_docker": running_within_docker,
"docker_base_image": docker_base_image,
"docker_base_image": docker_base_image(),
"has_http_access": has_http_access,
"ip_header_exists": &ip_header.0.is_some(),
"ip_header_match": ip_header_name == CONFIG.ip_header(),

View File

@@ -454,7 +454,7 @@ fn post_email(data: JsonUpcase<ChangeEmailData>, headers: Headers, conn: DbConn)
}
#[post("/accounts/verify-email")]
fn post_verify_email(headers: Headers, _conn: DbConn) -> EmptyResult {
fn post_verify_email(headers: Headers) -> EmptyResult {
let user = headers.user;
if !CONFIG.mail_enabled() {
@@ -654,7 +654,7 @@ struct VerifyPasswordData {
}
#[post("/accounts/verify-password", data = "<data>")]
fn verify_password(data: JsonUpcase<VerifyPasswordData>, headers: Headers, _conn: DbConn) -> EmptyResult {
fn verify_password(data: JsonUpcase<VerifyPasswordData>, headers: Headers) -> EmptyResult {
let data: VerifyPasswordData = data.into_inner().data;
let user = headers.user;

View File

@@ -539,7 +539,6 @@ fn reject_emergency_access(emer_id: String, headers: Headers, conn: DbConn) -> J
};
emergency_access.status = EmergencyAccessStatus::Confirmed as i32;
emergency_access.key_encrypted = None;
emergency_access.save(&conn)?;
if CONFIG.mail_enabled() {

View File

@@ -9,6 +9,7 @@ pub mod two_factor;
pub use ciphers::purge_trashed_ciphers;
pub use emergency_access::{emergency_notification_reminder_job, emergency_request_timeout_job};
pub use sends::purge_sends;
pub use two_factor::send_incomplete_2fa_notifications;
pub fn routes() -> Vec<Route> {
let mut mod_routes =

View File

@@ -1294,71 +1294,43 @@ fn put_policy(
#[allow(unused_variables)]
#[get("/organizations/<org_id>/tax")]
fn get_organization_tax(org_id: String, _headers: Headers, _conn: DbConn) -> EmptyResult {
fn get_organization_tax(org_id: String, _headers: Headers) -> Json<Value> {
// Prevent a 404 error, which also causes Javascript errors.
err!("Only allowed when not self hosted.")
// Upstream sends "Only allowed when not self hosted." As an error message.
// If we do the same it will also output this to the log, which is overkill.
// An empty list/data also works fine.
Json(_empty_data_json())
}
#[get("/plans")]
fn get_plans(_headers: Headers, _conn: DbConn) -> Json<Value> {
fn get_plans(_headers: Headers) -> Json<Value> {
// Respond with a minimal json just enough to allow the creation of an new organization.
Json(json!({
"Object": "list",
"Data": [
{
"Data": [{
"Object": "plan",
"Type": 0,
"Product": 0,
"Name": "Free",
"IsAnnual": false,
"NameLocalizationKey": "planNameFree",
"DescriptionLocalizationKey": "planDescFree",
"CanBeUsedByBusiness": false,
"BaseSeats": 2,
"BaseStorageGb": null,
"MaxCollections": 2,
"MaxUsers": 2,
"HasAdditionalSeatsOption": false,
"MaxAdditionalSeats": null,
"HasAdditionalStorageOption": false,
"MaxAdditionalStorage": null,
"HasPremiumAccessOption": false,
"TrialPeriodDays": null,
"HasSelfHost": false,
"HasPolicies": false,
"HasGroups": false,
"HasDirectory": false,
"HasEvents": false,
"HasTotp": false,
"Has2fa": false,
"HasApi": false,
"HasSso": false,
"UsersGetPremium": false,
"UpgradeSortOrder": -1,
"DisplaySortOrder": -1,
"LegacyYear": null,
"Disabled": false,
"StripePlanId": null,
"StripeSeatPlanId": null,
"StripeStoragePlanId": null,
"StripePremiumAccessPlanId": null,
"BasePrice": 0.0,
"SeatPrice": 0.0,
"AdditionalStoragePricePerGb": 0.0,
"PremiumAccessOptionPrice": 0.0
}
],
"DescriptionLocalizationKey": "planDescFree"
}],
"ContinuationToken": null
}))
}
#[get("/plans/sales-tax-rates")]
fn get_plans_tax_rates(_headers: Headers, _conn: DbConn) -> Json<Value> {
fn get_plans_tax_rates(_headers: Headers) -> Json<Value> {
// Prevent a 404 error, which also causes Javascript errors.
Json(json!({
Json(_empty_data_json())
}
fn _empty_data_json() -> Value {
json!({
"Object": "list",
"Data": [],
"ContinuationToken": null
}))
})
}
#[derive(Deserialize, Debug)]

View File

@@ -1,3 +1,4 @@
use chrono::{Duration, Utc};
use data_encoding::BASE32;
use rocket::Route;
use rocket_contrib::json::Json;
@@ -7,7 +8,7 @@ use crate::{
api::{JsonResult, JsonUpcase, NumberOrString, PasswordData},
auth::Headers,
crypto,
db::{models::*, DbConn},
db::{models::*, DbConn, DbPool},
mail, CONFIG,
};
@@ -156,3 +157,33 @@ fn disable_twofactor(data: JsonUpcase<DisableTwoFactorData>, headers: Headers, c
fn disable_twofactor_put(data: JsonUpcase<DisableTwoFactorData>, headers: Headers, conn: DbConn) -> JsonResult {
disable_twofactor(data, headers, conn)
}
pub fn send_incomplete_2fa_notifications(pool: DbPool) {
debug!("Sending notifications for incomplete 2FA logins");
if CONFIG.incomplete_2fa_time_limit() <= 0 || !CONFIG.mail_enabled() {
return;
}
let conn = match pool.get() {
Ok(conn) => conn,
_ => {
error!("Failed to get DB connection in send_incomplete_2fa_notifications()");
return;
}
};
let now = Utc::now().naive_utc();
let time_limit = Duration::minutes(CONFIG.incomplete_2fa_time_limit());
let incomplete_logins = TwoFactorIncomplete::find_logins_before(&(now - time_limit), &conn);
for login in incomplete_logins {
let user = User::find_by_uuid(&login.user_uuid, &conn).expect("User not found");
info!(
"User {} did not complete a 2FA login within the configured time limit. IP: {}",
user.email, login.ip_address
);
mail::send_incomplete_2fa_login(&user.email, &login.ip_address, &login.login_time, &login.device_name)
.expect("Error sending incomplete 2FA email");
login.delete(&conn).expect("Error deleting incomplete 2FA record");
}
}

View File

@@ -1,6 +1,7 @@
use rocket::Route;
use rocket_contrib::json::Json;
use serde_json::Value;
use url::Url;
use webauthn_rs::{base64_data::Base64UrlSafeData, proto::*, AuthenticationState, RegistrationState, Webauthn};
use crate::{
@@ -22,7 +23,7 @@ pub fn routes() -> Vec<Route> {
struct WebauthnConfig {
url: String,
origin: String,
origin: Url,
rpid: String,
}
@@ -31,13 +32,9 @@ impl WebauthnConfig {
let domain = CONFIG.domain();
let domain_origin = CONFIG.domain_origin();
Webauthn::new(Self {
rpid: reqwest::Url::parse(&domain)
.map(|u| u.domain().map(str::to_owned))
.ok()
.flatten()
.unwrap_or_default(),
rpid: Url::parse(&domain).map(|u| u.domain().map(str::to_owned)).ok().flatten().unwrap_or_default(),
url: domain,
origin: domain_origin,
origin: Url::parse(&domain_origin).unwrap(),
})
}
}
@@ -47,7 +44,7 @@ impl webauthn_rs::WebauthnConfig for WebauthnConfig {
&self.url
}
fn get_origin(&self) -> &str {
fn get_origin(&self) -> &Url {
&self.origin
}

View File

@@ -1,4 +1,4 @@
use chrono::Local;
use chrono::Utc;
use num_traits::FromPrimitive;
use rocket::{
request::{Form, FormItems, FromForm},
@@ -102,10 +102,9 @@ fn _password_login(data: ConnectData, conn: DbConn, ip: &ClientIp) -> JsonResult
err!("This user has been disabled", format!("IP: {}. Username: {}.", ip.ip, username))
}
let now = Local::now();
let now = Utc::now().naive_utc();
if user.verified_at.is_none() && CONFIG.mail_enabled() && CONFIG.signups_verify() {
let now = now.naive_utc();
if user.last_verifying_at.is_none()
|| now.signed_duration_since(user.last_verifying_at.unwrap()).num_seconds()
> CONFIG.signups_verify_resend_time() as i64
@@ -219,6 +218,8 @@ fn twofactor_auth(
return Ok(None);
}
TwoFactorIncomplete::mark_incomplete(user_uuid, &device.uuid, &device.name, ip, conn)?;
let twofactor_ids: Vec<_> = twofactors.iter().map(|tf| tf.atype).collect();
let selected_id = data.two_factor_provider.unwrap_or(twofactor_ids[0]); // If we aren't given a two factor provider, asume the first one
@@ -262,6 +263,8 @@ fn twofactor_auth(
_ => err!("Invalid two factor provider"),
}
TwoFactorIncomplete::mark_complete(user_uuid, &device.uuid, conn)?;
if !CONFIG.disable_2fa_remember() && remember == 1 {
Ok(Some(device.refresh_twofactor_remember()))
} else {

View File

@@ -13,6 +13,7 @@ pub use crate::api::{
core::purge_sends,
core::purge_trashed_ciphers,
core::routes as core_routes,
core::two_factor::send_incomplete_2fa_notifications,
core::{emergency_notification_reminder_job, emergency_request_timeout_job},
icons::routes as icons_routes,
identity::routes as identity_routes,

View File

@@ -4,7 +4,7 @@ use rocket::Route;
use rocket_contrib::json::Json;
use serde_json::Value as JsonValue;
use crate::{api::EmptyResult, auth::Headers, db::DbConn, Error, CONFIG};
use crate::{api::EmptyResult, auth::Headers, Error, CONFIG};
pub fn routes() -> Vec<Route> {
routes![negotiate, websockets_err]
@@ -30,7 +30,7 @@ fn websockets_err() -> EmptyResult {
}
#[post("/hub/negotiate")]
fn negotiate(_headers: Headers, _conn: DbConn) -> Json<JsonValue> {
fn negotiate(_headers: Headers) -> Json<JsonValue> {
use crate::crypto;
use data_encoding::BASE64URL;

View File

@@ -165,7 +165,6 @@ pub fn generate_invite_claims(
}
}
// var token = _dataProtector.Protect($"EmergencyAccessInvite {emergencyAccess.Id} {emergencyAccess.Email} {nowMillis}");
#[derive(Debug, Serialize, Deserialize)]
pub struct EmergencyAccessInviteJwtClaims {
// Not before

View File

@@ -2,7 +2,6 @@ use std::process::exit;
use std::sync::RwLock;
use once_cell::sync::Lazy;
use regex::Regex;
use reqwest::Url;
use crate::{
@@ -23,21 +22,6 @@ pub static CONFIG: Lazy<Config> = Lazy::new(|| {
})
});
static PRIVACY_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"[\w]").unwrap());
const PRIVACY_CONFIG: &[&str] = &[
"allowed_iframe_ancestors",
"database_url",
"domain_origin",
"domain_path",
"domain",
"helo_name",
"org_creation_users",
"signups_domains_whitelist",
"smtp_from",
"smtp_host",
"smtp_username",
];
pub type Pass = String;
macro_rules! make_config {
@@ -61,7 +45,7 @@ macro_rules! make_config {
_overrides: Vec<String>,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[derive(Clone, Default, Deserialize, Serialize)]
pub struct ConfigBuilder {
$($(
#[serde(skip_serializing_if = "Option::is_none")]
@@ -133,19 +117,6 @@ macro_rules! make_config {
builder
}
/// Returns a new builder with all the elements from self,
/// except those that are equal in both sides
fn _remove(&self, other: &Self) -> Self {
let mut builder = ConfigBuilder::default();
$($(
if &self.$name != &other.$name {
builder.$name = self.$name.clone();
}
)+)+
builder
}
fn build(&self) -> ConfigItems {
let mut config = ConfigItems::default();
let _domain_set = self.domain.is_some();
@@ -161,12 +132,13 @@ macro_rules! make_config {
}
}
#[derive(Debug, Clone, Default)]
pub struct ConfigItems { $($(pub $name: make_config!{@type $ty, $none_action}, )+)+ }
#[derive(Clone, Default)]
struct ConfigItems { $($( $name: make_config!{@type $ty, $none_action}, )+)+ }
#[allow(unused)]
impl Config {
$($(
$(#[doc = $doc])+
pub fn $name(&self) -> make_config!{@type $ty, $none_action} {
self.inner.read().unwrap().config.$name.clone()
}
@@ -189,38 +161,91 @@ macro_rules! make_config {
fn _get_doc(doc: &str) -> serde_json::Value {
let mut split = doc.split("|>").map(str::trim);
json!({
"name": split.next(),
"description": split.next()
// We do not use the json!() macro here since that causes a lot of macro recursion.
// This slows down compile time and it also causes issues with rust-analyzer
serde_json::Value::Object({
let mut doc_json = serde_json::Map::new();
doc_json.insert("name".into(), serde_json::to_value(split.next()).unwrap());
doc_json.insert("description".into(), serde_json::to_value(split.next()).unwrap());
doc_json
})
}
json!([ $({
"group": stringify!($group),
"grouptoggle": stringify!($($group_enabled)?),
"groupdoc": make_config!{ @show $($groupdoc)? },
"elements": [
$( {
"editable": $editable,
"name": stringify!($name),
"value": cfg.$name,
"default": def.$name,
"type": _get_form_type(stringify!($ty)),
"doc": _get_doc(concat!($($doc),+)),
"overridden": overriden.contains(&stringify!($name).to_uppercase()),
}, )+
]}, )+ ])
// We do not use the json!() macro here since that causes a lot of macro recursion.
// This slows down compile time and it also causes issues with rust-analyzer
serde_json::Value::Array(<[_]>::into_vec(Box::new([
$(
serde_json::Value::Object({
let mut group = serde_json::Map::new();
group.insert("group".into(), (stringify!($group)).into());
group.insert("grouptoggle".into(), (stringify!($($group_enabled)?)).into());
group.insert("groupdoc".into(), (make_config!{ @show $($groupdoc)? }).into());
group.insert("elements".into(), serde_json::Value::Array(<[_]>::into_vec(Box::new([
$(
serde_json::Value::Object({
let mut element = serde_json::Map::new();
element.insert("editable".into(), ($editable).into());
element.insert("name".into(), (stringify!($name)).into());
element.insert("value".into(), serde_json::to_value(cfg.$name).unwrap());
element.insert("default".into(), serde_json::to_value(def.$name).unwrap());
element.insert("type".into(), (_get_form_type(stringify!($ty))).into());
element.insert("doc".into(), (_get_doc(concat!($($doc),+))).into());
element.insert("overridden".into(), (overriden.contains(&stringify!($name).to_uppercase())).into());
element
}),
)+
]))));
group
}),
)+
])))
}
pub fn get_support_json(&self) -> serde_json::Value {
// Define which config keys need to be masked.
// Pass types will always be masked and no need to put them in the list.
// Besides Pass, only String types will be masked via _privacy_mask.
const PRIVACY_CONFIG: &[&str] = &[
"allowed_iframe_ancestors",
"database_url",
"domain_origin",
"domain_path",
"domain",
"helo_name",
"org_creation_users",
"signups_domains_whitelist",
"smtp_from",
"smtp_host",
"smtp_username",
];
let cfg = {
let inner = &self.inner.read().unwrap();
inner.config.clone()
};
json!({ $($(
stringify!($name): make_config!{ @supportstr $name, cfg.$name, $ty, $none_action },
)+)+ })
/// We map over the string and remove all alphanumeric, _ and - characters.
/// This is the fastest way (within micro-seconds) instead of using a regex (which takes mili-seconds)
fn _privacy_mask(value: &str) -> String {
value.chars().map(|c|
match c {
c if c.is_alphanumeric() => '*',
'_' => '*',
'-' => '*',
_ => c
}
).collect::<String>()
}
serde_json::Value::Object({
let mut json = serde_json::Map::new();
$($(
json.insert(stringify!($name).into(), make_config!{ @supportstr $name, cfg.$name, $ty, $none_action });
)+)+;
json
})
}
pub fn get_overrides(&self) -> Vec<String> {
@@ -228,29 +253,30 @@ macro_rules! make_config {
let inner = &self.inner.read().unwrap();
inner._overrides.clone()
};
overrides
}
}
};
// Support string print
( @supportstr $name:ident, $value:expr, Pass, option ) => { $value.as_ref().map(|_| String::from("***")) }; // Optional pass, we map to an Option<String> with "***"
( @supportstr $name:ident, $value:expr, Pass, $none_action:ident ) => { String::from("***") }; // Required pass, we return "***"
( @supportstr $name:ident, $value:expr, $ty:ty, option ) => { // Optional other value, we return as is or convert to string to apply the privacy config
( @supportstr $name:ident, $value:expr, Pass, option ) => { serde_json::to_value($value.as_ref().map(|_| String::from("***"))).unwrap() }; // Optional pass, we map to an Option<String> with "***"
( @supportstr $name:ident, $value:expr, Pass, $none_action:ident ) => { "***".into() }; // Required pass, we return "***"
( @supportstr $name:ident, $value:expr, String, option ) => { // Optional other value, we return as is or convert to string to apply the privacy config
if PRIVACY_CONFIG.contains(&stringify!($name)) {
json!($value.as_ref().map(|x| PRIVACY_REGEX.replace_all(&x.to_string(), "${1}*").to_string()))
serde_json::to_value($value.as_ref().map(|x| _privacy_mask(x) )).unwrap()
} else {
json!($value)
serde_json::to_value($value).unwrap()
}
};
( @supportstr $name:ident, $value:expr, $ty:ty, $none_action:ident ) => { // Required other value, we return as is or convert to string to apply the privacy config
( @supportstr $name:ident, $value:expr, String, $none_action:ident ) => { // Required other value, we return as is or convert to string to apply the privacy config
if PRIVACY_CONFIG.contains(&stringify!($name)) {
json!(PRIVACY_REGEX.replace_all(&$value.to_string(), "${1}*").to_string())
} else {
json!($value)
}
_privacy_mask(&$value).into()
} else {
($value).into()
}
};
( @supportstr $name:ident, $value:expr, $ty:ty, option ) => { serde_json::to_value($value).unwrap() }; // Optional other value, we return as is or convert to string to apply the privacy config
( @supportstr $name:ident, $value:expr, $ty:ty, $none_action:ident ) => { ($value).into() }; // Required other value, we return as is or convert to string to apply the privacy config
// Group or empty string
( @show ) => { "" };
@@ -300,8 +326,6 @@ make_config! {
data_folder: String, false, def, "data".to_string();
/// Database URL
database_url: String, false, auto, |c| format!("{}/{}", c.data_folder, "db.sqlite3");
/// Database connection pool size
database_max_conns: u32, false, def, 10;
/// Icon cache folder
icon_cache_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "icon_cache");
/// Attachments folder
@@ -333,6 +357,9 @@ make_config! {
/// Trash purge schedule |> Cron schedule of the job that checks for trashed items to delete permanently.
/// Defaults to daily. Set blank to disable this job.
trash_purge_schedule: String, false, def, "0 5 0 * * *".to_string();
/// Incomplete 2FA login schedule |> Cron schedule of the job that checks for incomplete 2FA logins.
/// Defaults to once every minute. Set blank to disable this job.
incomplete_2fa_schedule: String, false, def, "30 * * * * *".to_string();
/// Emergency notification reminder schedule |> Cron schedule of the job that sends expiration reminders to emergency access grantors.
/// Defaults to hourly. Set blank to disable this job.
emergency_notification_reminder_schedule: String, false, def, "0 5 * * * *".to_string();
@@ -372,6 +399,13 @@ make_config! {
/// sure to inform all users of any changes to this setting.
trash_auto_delete_days: i64, true, option;
/// Incomplete 2FA time limit |> Number of minutes to wait before a 2FA-enabled login is
/// considered incomplete, resulting in an email notification. An incomplete 2FA login is one
/// where the correct master password was provided but the required 2FA step was not completed,
/// which potentially indicates a master password compromise. Set to 0 to disable this check.
/// This setting applies globally to all users.
incomplete_2fa_time_limit: i64, true, def, 3;
/// Disable icon downloads |> Set to true to disable icon downloading, this would still serve icons from
/// $ICON_CACHE_FOLDER, but it won't produce any external network request. Needs to set $ICON_CACHE_TTL to 0,
/// otherwise it will delete them and they won't be downloaded again.
@@ -461,6 +495,9 @@ make_config! {
/// Max database connection retries |> Number of times to retry the database connection during startup, with 1 second between each retry, set to 0 to retry indefinitely
db_connection_retries: u32, false, def, 15;
/// Database connection pool size
database_max_conns: u32, false, def, 10;
/// Bypass admin page security (Know the risks!) |> Disables the Admin Token for the admin page so you may use your own auth in-front
disable_admin_token: bool, true, def, false;
@@ -615,7 +652,7 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
// Check if the icon blacklist regex is valid
if let Some(ref r) = cfg.icon_blacklist_regex {
let validate_regex = Regex::new(r);
let validate_regex = regex::Regex::new(r);
match validate_regex {
Ok(_) => (),
Err(e) => err!(format!("`ICON_BLACKLIST_REGEX` is invalid: {:#?}", e)),
@@ -707,7 +744,7 @@ impl Config {
Ok(())
}
pub fn update_config_partial(&self, other: ConfigBuilder) -> Result<(), Error> {
fn update_config_partial(&self, other: ConfigBuilder) -> Result<(), Error> {
let builder = {
let usr = &self.inner.read().unwrap()._usr;
let mut _overrides = Vec::new();
@@ -861,8 +898,6 @@ where
reg!("email/change_email", ".html");
reg!("email/delete_account", ".html");
reg!("email/invite_accepted", ".html");
reg!("email/invite_confirmed", ".html");
reg!("email/emergency_access_invite_accepted", ".html");
reg!("email/emergency_access_invite_confirmed", ".html");
reg!("email/emergency_access_recovery_approved", ".html");
@@ -870,6 +905,9 @@ where
reg!("email/emergency_access_recovery_rejected", ".html");
reg!("email/emergency_access_recovery_reminder", ".html");
reg!("email/emergency_access_recovery_timed_out", ".html");
reg!("email/incomplete_2fa_login", ".html");
reg!("email/invite_accepted", ".html");
reg!("email/invite_confirmed", ".html");
reg!("email/new_device_logged_in", ".html");
reg!("email/pw_hint_none", ".html");
reg!("email/pw_hint_some", ".html");

View File

@@ -343,36 +343,39 @@ impl Cipher {
db_run! {conn: {
// Check whether this cipher is in any collections accessible to the
// user. If so, retrieve the access flags for each collection.
let query = ciphers::table
let rows = ciphers::table
.filter(ciphers::uuid.eq(&self.uuid))
.inner_join(ciphers_collections::table.on(
ciphers::uuid.eq(ciphers_collections::cipher_uuid)))
.inner_join(users_collections::table.on(
ciphers_collections::collection_uuid.eq(users_collections::collection_uuid)
.and(users_collections::user_uuid.eq(user_uuid))))
.select((users_collections::read_only, users_collections::hide_passwords));
.select((users_collections::read_only, users_collections::hide_passwords))
.load::<(bool, bool)>(conn)
.expect("Error getting access restrictions");
// There's an edge case where a cipher can be in multiple collections
// with inconsistent access flags. For example, a cipher could be in
// one collection where the user has read-only access, but also in
// another collection where the user has read/write access. To handle
// this, we do a boolean OR of all values in each of the `read_only`
// and `hide_passwords` columns. This could ideally be done as part
// of the query, but Diesel doesn't support a max() or bool_or()
// function on booleans and this behavior isn't portable anyway.
if let Ok(vec) = query.load::<(bool, bool)>(conn) {
let mut read_only = false;
let mut hide_passwords = false;
for (ro, hp) in vec.iter() {
read_only |= ro;
hide_passwords |= hp;
}
Some((read_only, hide_passwords))
} else {
if rows.is_empty() {
// This cipher isn't in any collections accessible to the user.
None
return None;
}
// A cipher can be in multiple collections with inconsistent access flags.
// For example, a cipher could be in one collection where the user has
// read-only access, but also in another collection where the user has
// read/write access. For a flag to be in effect for a cipher, upstream
// requires all collections the cipher is in to have that flag set.
// Therefore, we do a boolean AND of all values in each of the `read_only`
// and `hide_passwords` columns. This could ideally be done as part of the
// query, but Diesel doesn't support a min() or bool_and() function on
// booleans and this behavior isn't portable anyway.
let mut read_only = true;
let mut hide_passwords = true;
for (ro, hp) in rows.iter() {
read_only &= ro;
hide_passwords &= hp;
}
Some((read_only, hide_passwords))
}}
}

View File

@@ -17,8 +17,7 @@ db_object! {
pub user_uuid: String,
pub name: String,
// https://github.com/bitwarden/core/tree/master/src/Core/Enums
pub atype: i32,
pub atype: i32, // https://github.com/bitwarden/server/blob/master/src/Core/Enums/DeviceType.cs
pub push_token: Option<String>,
pub refresh_token: String,

View File

@@ -9,6 +9,7 @@ mod org_policy;
mod organization;
mod send;
mod two_factor;
mod two_factor_incomplete;
mod user;
pub use self::attachment::Attachment;
@@ -22,4 +23,5 @@ pub use self::org_policy::{OrgPolicy, OrgPolicyType};
pub use self::organization::{Organization, UserOrgStatus, UserOrgType, UserOrganization};
pub use self::send::{Send, SendType};
pub use self::two_factor::{TwoFactor, TwoFactorType};
pub use self::two_factor_incomplete::TwoFactorIncomplete;
pub use self::user::{Invitation, User, UserStampException};

View File

@@ -1,8 +1,6 @@
use serde_json::Value;
use crate::api::EmptyResult;
use crate::db::DbConn;
use crate::error::MapResult;
use crate::{api::EmptyResult, db::DbConn, error::MapResult};
use super::User;
@@ -161,7 +159,6 @@ impl TwoFactor {
use crate::api::core::two_factor::u2f::U2FRegistration;
use crate::api::core::two_factor::webauthn::{get_webauthn_registrations, WebauthnRegistration};
use std::convert::TryInto;
use webauthn_rs::proto::*;
for mut u2f in u2f_factors {

View File

@@ -0,0 +1,108 @@
use chrono::{NaiveDateTime, Utc};
use crate::{api::EmptyResult, auth::ClientIp, db::DbConn, error::MapResult, CONFIG};
use super::User;
db_object! {
#[derive(Identifiable, Queryable, Insertable, Associations, AsChangeset)]
#[table_name = "twofactor_incomplete"]
#[belongs_to(User, foreign_key = "user_uuid")]
#[primary_key(user_uuid, device_uuid)]
pub struct TwoFactorIncomplete {
pub user_uuid: String,
// This device UUID is simply what's claimed by the device. It doesn't
// necessarily correspond to any UUID in the devices table, since a device
// must complete 2FA login before being added into the devices table.
pub device_uuid: String,
pub device_name: String,
pub login_time: NaiveDateTime,
pub ip_address: String,
}
}
impl TwoFactorIncomplete {
pub fn mark_incomplete(
user_uuid: &str,
device_uuid: &str,
device_name: &str,
ip: &ClientIp,
conn: &DbConn,
) -> EmptyResult {
if CONFIG.incomplete_2fa_time_limit() <= 0 || !CONFIG.mail_enabled() {
return Ok(());
}
// Don't update the data for an existing user/device pair, since that
// would allow an attacker to arbitrarily delay notifications by
// sending repeated 2FA attempts to reset the timer.
let existing = Self::find_by_user_and_device(user_uuid, device_uuid, conn);
if existing.is_some() {
return Ok(());
}
db_run! { conn: {
diesel::insert_into(twofactor_incomplete::table)
.values((
twofactor_incomplete::user_uuid.eq(user_uuid),
twofactor_incomplete::device_uuid.eq(device_uuid),
twofactor_incomplete::device_name.eq(device_name),
twofactor_incomplete::login_time.eq(Utc::now().naive_utc()),
twofactor_incomplete::ip_address.eq(ip.ip.to_string()),
))
.execute(conn)
.map_res("Error adding twofactor_incomplete record")
}}
}
pub fn mark_complete(user_uuid: &str, device_uuid: &str, conn: &DbConn) -> EmptyResult {
if CONFIG.incomplete_2fa_time_limit() <= 0 || !CONFIG.mail_enabled() {
return Ok(());
}
Self::delete_by_user_and_device(user_uuid, device_uuid, conn)
}
pub fn find_by_user_and_device(user_uuid: &str, device_uuid: &str, conn: &DbConn) -> Option<Self> {
db_run! { conn: {
twofactor_incomplete::table
.filter(twofactor_incomplete::user_uuid.eq(user_uuid))
.filter(twofactor_incomplete::device_uuid.eq(device_uuid))
.first::<TwoFactorIncompleteDb>(conn)
.ok()
.from_db()
}}
}
pub fn find_logins_before(dt: &NaiveDateTime, conn: &DbConn) -> Vec<Self> {
db_run! {conn: {
twofactor_incomplete::table
.filter(twofactor_incomplete::login_time.lt(dt))
.load::<TwoFactorIncompleteDb>(conn)
.expect("Error loading twofactor_incomplete")
.from_db()
}}
}
pub fn delete(self, conn: &DbConn) -> EmptyResult {
Self::delete_by_user_and_device(&self.user_uuid, &self.device_uuid, conn)
}
pub fn delete_by_user_and_device(user_uuid: &str, device_uuid: &str, conn: &DbConn) -> EmptyResult {
db_run! { conn: {
diesel::delete(twofactor_incomplete::table
.filter(twofactor_incomplete::user_uuid.eq(user_uuid))
.filter(twofactor_incomplete::device_uuid.eq(device_uuid)))
.execute(conn)
.map_res("Error in twofactor_incomplete::delete_by_user_and_device()")
}}
}
pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult {
db_run! { conn: {
diesel::delete(twofactor_incomplete::table.filter(twofactor_incomplete::user_uuid.eq(user_uuid)))
.execute(conn)
.map_res("Error in twofactor_incomplete::delete_all_by_user()")
}}
}
}

View File

@@ -176,7 +176,10 @@ impl User {
}
}
use super::{Cipher, Device, EmergencyAccess, Favorite, Folder, Send, TwoFactor, UserOrgType, UserOrganization};
use super::{
Cipher, Device, EmergencyAccess, Favorite, Folder, Send, TwoFactor, TwoFactorIncomplete, UserOrgType,
UserOrganization,
};
use crate::db::DbConn;
use crate::api::EmptyResult;
@@ -273,6 +276,7 @@ impl User {
Folder::delete_all_by_user(&self.uuid, conn)?;
Device::delete_all_by_user(&self.uuid, conn)?;
TwoFactor::delete_all_by_user(&self.uuid, conn)?;
TwoFactorIncomplete::delete_all_by_user(&self.uuid, conn)?;
Invitation::take(&self.email, conn); // Delete invitation if any
db_run! {conn: {

View File

@@ -140,6 +140,16 @@ table! {
}
}
table! {
twofactor_incomplete (user_uuid, device_uuid) {
user_uuid -> Text,
device_uuid -> Text,
device_name -> Text,
login_time -> Timestamp,
ip_address -> Text,
}
}
table! {
users (uuid) {
uuid -> Text,

View File

@@ -140,6 +140,16 @@ table! {
}
}
table! {
twofactor_incomplete (user_uuid, device_uuid) {
user_uuid -> Text,
device_uuid -> Text,
device_name -> Text,
login_time -> Timestamp,
ip_address -> Text,
}
}
table! {
users (uuid) {
uuid -> Text,

View File

@@ -140,6 +140,16 @@ table! {
}
}
table! {
twofactor_incomplete (user_uuid, device_uuid) {
user_uuid -> Text,
device_uuid -> Text,
device_name -> Text,
login_time -> Timestamp,
ip_address -> Text,
}
}
table! {
users (uuid) {
uuid -> Text,

View File

@@ -73,7 +73,7 @@ make_error! {
Serde(SerdeErr): _has_source, _api_error,
JWt(JwtErr): _has_source, _api_error,
Handlebars(HbErr): _has_source, _api_error,
//WsError(ws::Error): _has_source, _api_error,
Io(IoErr): _has_source, _api_error,
Time(TimeErr): _has_source, _api_error,
Req(ReqErr): _has_source, _api_error,

View File

@@ -1,6 +1,6 @@
use std::str::FromStr;
use chrono::{DateTime, Local};
use chrono::NaiveDateTime;
use percent_encoding::{percent_encode, NON_ALPHANUMERIC};
use lettre::{
@@ -394,7 +394,7 @@ pub fn send_invite_confirmed(address: &str, org_name: &str) -> EmptyResult {
send_email(address, &subject, body_html, body_text)
}
pub fn send_new_device_logged_in(address: &str, ip: &str, dt: &DateTime<Local>, device: &str) -> EmptyResult {
pub fn send_new_device_logged_in(address: &str, ip: &str, dt: &NaiveDateTime, device: &str) -> EmptyResult {
use crate::util::upcase_first;
let device = upcase_first(device);
@@ -405,7 +405,26 @@ pub fn send_new_device_logged_in(address: &str, ip: &str, dt: &DateTime<Local>,
"url": CONFIG.domain(),
"ip": ip,
"device": device,
"datetime": crate::util::format_datetime_local(dt, fmt),
"datetime": crate::util::format_naive_datetime_local(dt, fmt),
}),
)?;
send_email(address, &subject, body_html, body_text)
}
pub fn send_incomplete_2fa_login(address: &str, ip: &str, dt: &NaiveDateTime, device: &str) -> EmptyResult {
use crate::util::upcase_first;
let device = upcase_first(device);
let fmt = "%A, %B %_d, %Y at %r %Z";
let (subject, body_html, body_text) = get_text(
"email/incomplete_2fa_login",
json!({
"url": CONFIG.domain(),
"ip": ip,
"device": device,
"datetime": crate::util::format_naive_datetime_local(dt, fmt),
"time_limit": CONFIG.incomplete_2fa_time_limit(),
}),
)?;
@@ -486,10 +505,10 @@ fn send_email(address: &str, subject: &str, body_html: String, body_text: String
Err(e) => {
if e.is_client() {
debug!("SMTP Client error: {:#?}", e);
err!(format!("SMTP Client error: {}", e.to_string()));
err!(format!("SMTP Client error: {}", e));
} else if e.is_transient() {
debug!("SMTP 4xx error: {:#?}", e);
err!(format!("SMTP 4xx error: {}", e.to_string()));
err!(format!("SMTP 4xx error: {}", e));
} else if e.is_permanent() {
debug!("SMTP 5xx error: {:#?}", e);
let mut msg = e.to_string();
@@ -500,13 +519,13 @@ fn send_email(address: &str, subject: &str, body_html: String, body_text: String
err!(format!("SMTP 5xx error: {}", msg));
} else if e.is_timeout() {
debug!("SMTP timeout error: {:#?}", e);
err!(format!("SMTP timeout error: {}", e.to_string()));
err!(format!("SMTP timeout error: {}", e));
} else if e.is_tls() {
debug!("SMTP Encryption error: {:#?}", e);
err!(format!("SMTP Encryption error: {}", e.to_string()));
err!(format!("SMTP Encryption error: {}", e));
} else {
debug!("SMTP {:#?}", e);
err!(format!("SMTP {}", e.to_string()));
err!(format!("SMTP {}", e));
}
}
}

View File

@@ -1,6 +1,10 @@
#![forbid(unsafe_code)]
#![cfg_attr(feature = "unstable", feature(ip))]
#![recursion_limit = "512"]
// The recursion_limit is mainly triggered by the json!() macro.
// The more key/value pairs there are the more recursion occurs.
// We want to keep this as low as possible, but not higher then 128.
// If you go above 128 it will cause rust-analyzer to fail,
#![recursion_limit = "87"]
extern crate openssl;
#[macro_use]
@@ -104,6 +108,14 @@ fn launch_info() {
}
fn init_logging(level: log::LevelFilter) -> Result<(), fern::InitError> {
// Depending on the main log level we either want to disable or enable logging for trust-dns.
// Else if there are timeouts it will clutter the logs since trust-dns uses warn for this.
let trust_dns_level = if level >= log::LevelFilter::Debug {
level
} else {
log::LevelFilter::Off
};
let mut logger = fern::Dispatch::new()
.level(level)
// Hide unknown certificate errors if using self-signed
@@ -122,6 +134,8 @@ fn init_logging(level: log::LevelFilter) -> Result<(), fern::InitError> {
.level_for("hyper::client", log::LevelFilter::Off)
// Prevent cookie_store logs
.level_for("cookie_store", log::LevelFilter::Off)
// Variable level for trust-dns used by reqwest
.level_for("trust_dns_proto", trust_dns_level)
.chain(std::io::stdout());
// Enable smtp debug logging only specifically for smtp when need.
@@ -345,6 +359,14 @@ fn schedule_jobs(pool: db::DbPool) {
}));
}
// Send email notifications about incomplete 2FA logins, which potentially
// indicates that a user's master password has been compromised.
if !CONFIG.incomplete_2fa_schedule().is_empty() {
sched.add(Job::new(CONFIG.incomplete_2fa_schedule().parse().unwrap(), || {
api::send_incomplete_2fa_notifications(pool.clone());
}));
}
// Grant emergency access requests that have met the required wait time.
// This job should run before the emergency access reminders job to avoid
// sending reminders for requests that are about to be granted anyway.

View File

@@ -1,5 +1,5 @@
/*!
* Native JavaScript for Bootstrap v4.0.6 (https://thednp.github.io/bootstrap.native/)
* Native JavaScript for Bootstrap v4.0.8 (https://thednp.github.io/bootstrap.native/)
* Copyright 2015-2021 © dnp_theme
* Licensed under MIT (https://github.com/thednp/bootstrap.native/blob/master/LICENSE)
*/
@@ -7,7 +7,7 @@
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.BSN = factory());
}(this, (function () { 'use strict';
})(this, (function () { 'use strict';
const transitionEndEvent = 'webkitTransition' in document.head.style ? 'webkitTransitionEnd' : 'transitionend';
@@ -188,7 +188,7 @@
element.dispatchEvent(closedAlertEvent);
self.dispose();
element.parentNode.removeChild(element);
element.remove();
}
// ALERT PRIVATE METHOD
@@ -1022,9 +1022,9 @@
function isEmptyAnchor(elem) {
const parentAnchor = elem.closest('A');
// anchor href starts with #
return elem && ((elem.href && elem.href.slice(-1) === '#')
return elem && ((elem.hasAttribute('href') && elem.href.slice(-1) === '#')
// OR a child of an anchor with href starts with #
|| (parentAnchor && parentAnchor.href && parentAnchor.href.slice(-1) === '#'));
|| (parentAnchor && parentAnchor.hasAttribute('href') && parentAnchor.href.slice(-1) === '#'));
}
function setFocus(element) {
@@ -1487,7 +1487,7 @@
function appendOverlay(hasFade, isModal) {
toggleOverlayType(isModal);
document.body.appendChild(overlay);
document.body.append(overlay);
if (hasFade) addClass(overlay, fadeClass);
}
@@ -1501,12 +1501,11 @@
}
function removeOverlay() {
const bd = document.body;
const currentOpen = getCurrentOpen();
if (!currentOpen) {
removeClass(overlay, fadeClass);
bd.removeChild(overlay);
overlay.remove();
resetScrollbar();
}
}
@@ -1928,7 +1927,7 @@
if ((!element.contains(target) && options.backdrop
&& (!trigger || (trigger && !triggers.includes(trigger))))
|| offCanvasDismiss.contains(target)) {
|| (offCanvasDismiss && offCanvasDismiss.contains(target))) {
self.relatedTarget = target === offCanvasDismiss ? offCanvasDismiss : null;
self.hide();
}
@@ -2122,19 +2121,6 @@
.some((mediaType) => element instanceof mediaType);
}
function closestRelative(element) {
let retval = null;
let el = element;
while (el !== document.body) {
el = el.parentElement;
if (getComputedStyle(el).position === 'relative') {
retval = el;
break;
}
}
return retval;
}
// both popovers and tooltips (this, event)
function styleTip(self, e) {
const tipClasses = /\b(top|bottom|start|end)+/;
@@ -2148,32 +2134,32 @@
let tipDimensions = { w: tip.offsetWidth, h: tip.offsetHeight };
const windowWidth = (document.documentElement.clientWidth || document.body.clientWidth);
const windowHeight = (document.documentElement.clientHeight || document.body.clientHeight);
const { element, options, arrow } = self;
const {
element, options, arrow, positions,
} = self;
let { container, placement } = options;
let parentIsBody = container === document.body;
const targetPosition = getComputedStyle(element).position;
const parentPosition = getComputedStyle(container).position;
const staticParent = !parentIsBody && parentPosition === 'static';
let relativeParent = !parentIsBody && parentPosition === 'relative';
const relContainer = staticParent && closestRelative(container);
const { elementPosition, containerIsStatic, relContainer } = positions;
let { containerIsRelative } = positions;
// static containers should refer to another relative container or the body
container = relContainer || container;
relativeParent = staticParent && relContainer ? 1 : relativeParent;
containerIsRelative = containerIsStatic && relContainer ? 1 : containerIsRelative;
parentIsBody = container === document.body;
const parentRect = container.getBoundingClientRect();
const leftBoundry = relativeParent ? parentRect.left : 0;
const rightBoundry = relativeParent ? parentRect.right : windowWidth;
const leftBoundry = containerIsRelative ? parentRect.left : 0;
const rightBoundry = containerIsRelative ? parentRect.right : windowWidth;
// this case should not be possible
// absoluteParent = !parentIsBody && parentPosition === 'absolute',
// this case requires a container with placement: relative
const absoluteTarget = targetPosition === 'absolute';
// containerIsAbsolute = !parentIsBody && containerPosition === 'absolute',
// this case requires a container with position: relative
const absoluteTarget = elementPosition === 'absolute';
const targetRect = element.getBoundingClientRect();
const scroll = parentIsBody
? { x: window.pageXOffset, y: window.pageYOffset }
: { x: container.scrollLeft, y: container.scrollTop };
const elemDimensions = { w: element.offsetWidth, h: element.offsetHeight };
const top = relativeParent ? element.offsetTop : targetRect.top;
const left = relativeParent ? element.offsetLeft : targetRect.left;
const top = containerIsRelative ? element.offsetTop : targetRect.top;
const left = containerIsRelative ? element.offsetLeft : targetRect.left;
// reset arrow style
arrow.style.top = '';
arrow.style.left = '';
@@ -2245,8 +2231,12 @@
}
} else if (['top', 'bottom'].includes(placement)) {
if (e && isMedia(element)) {
const eX = !relativeParent ? e.pageX : e.layerX + (absoluteTarget ? element.offsetLeft : 0);
const eY = !relativeParent ? e.pageY : e.layerY + (absoluteTarget ? element.offsetTop : 0);
const eX = !containerIsRelative
? e.pageX
: e.layerX + (absoluteTarget ? element.offsetLeft : 0);
const eY = !containerIsRelative
? e.pageY
: e.layerY + (absoluteTarget ? element.offsetTop : 0);
if (placement === 'top') {
topPosition = eY - tipDimensions.h - (isPopover ? arrowWidth : arrowHeight);
@@ -2323,6 +2313,36 @@
return modal || navbarFixed || document.body;
}
function closestRelative(element) {
let retval = null;
let el = element;
while (el !== document.body) {
el = el.parentElement;
if (getComputedStyle(el).position === 'relative') {
retval = el;
break;
}
}
return retval;
}
function setHtml(element, content, sanitizeFn) {
if (typeof content === 'string' && !content.length) return;
if (typeof content === 'object') {
element.append(content);
} else {
let dirty = content.trim(); // fixing #233
if (typeof sanitizeFn === 'function') dirty = sanitizeFn(dirty);
const domParser = new DOMParser();
const tempDocument = domParser.parseFromString(dirty, 'text/html');
const method = tempDocument.children.length ? 'innerHTML' : 'innerText';
element[method] = tempDocument.body[method];
}
}
/* Native JavaScript for Bootstrap 5 | Popover
---------------------------------------------- */
@@ -2335,12 +2355,13 @@
template: '<div class="popover" role="tooltip"><div class="popover-arrow"></div><h3 class="popover-header"></h3><div class="popover-body"></div></div>', // string
title: null, // string
content: null, // string
sanitizeFn: null, // function
customClass: null, // string
dismissible: false, // boolean
animation: true, // boolean
trigger: 'hover', // string
placement: 'top', // string
btnClose: '<button class="btn-close" aria-label="Close"></button>', // string
sanitizeFn: null, // function
dismissible: false, // boolean
animation: true, // boolean
delay: 200, // number
};
@@ -2350,11 +2371,8 @@
const isIphone = navigator.userAgentData
? navigator.userAgentData.brands.some((x) => appleBrands.test(x.brand))
: appleBrands.test(navigator.userAgent);
// popoverArrowClass = `${popoverString}-arrow`,
const popoverHeaderClass = `${popoverString}-header`;
const popoverBodyClass = `${popoverString}-body`;
// close btn for dissmissible popover
let popoverCloseButton = '<button type="button" class="btn-close"></button>';
// POPOVER CUSTOM EVENTS
// =====================
@@ -2387,51 +2405,59 @@
const {
animation, customClass, sanitizeFn, placement, dismissible,
} = options;
let { title, content, template } = options;
let {
title, content,
} = options;
const {
template, btnClose,
} = options;
// set initial popover class
const placementClass = `bs-${popoverString}-${tipClassPositions[placement]}`;
// fixing #233
title = title ? title.trim() : null;
content = content ? content.trim() : null;
// sanitize title && content
if (sanitizeFn) {
title = title ? sanitizeFn(title) : null;
content = content ? sanitizeFn(content) : null;
template = template ? sanitizeFn(template) : null;
popoverCloseButton = sanitizeFn(popoverCloseButton);
// load template
let popoverTemplate;
if (typeof template === 'object') {
popoverTemplate = template;
} else {
const htmlMarkup = document.createElement('div');
setHtml(htmlMarkup, template, sanitizeFn);
popoverTemplate = htmlMarkup.firstChild;
}
// set popover markup
self.popover = popoverTemplate.cloneNode(true);
self.popover = document.createElement('div');
const { popover } = self;
// set id and aria-describedby
// set id and role attributes
popover.setAttribute('id', id);
popover.setAttribute('role', 'tooltip');
// load template
const popoverTemplate = document.createElement('div');
popoverTemplate.innerHTML = template.trim();
popover.className = popoverTemplate.firstChild.className;
popover.innerHTML = popoverTemplate.firstChild.innerHTML;
const popoverHeader = queryElement(`.${popoverHeaderClass}`, popover);
const popoverBody = queryElement(`.${popoverBodyClass}`, popover);
// set arrow
// set arrow and enable access for styleTip
self.arrow = queryElement(`.${popoverString}-arrow`, popover);
// set dismissible button
if (dismissible) {
title = title ? title + popoverCloseButton : title;
content = title === null ? +popoverCloseButton : content;
if (title) {
if (title instanceof Element) setHtml(title, btnClose, sanitizeFn);
else title += btnClose;
} else {
if (popoverHeader) popoverHeader.remove();
if (content instanceof Element) setHtml(content, btnClose, sanitizeFn);
else content += btnClose;
}
}
// fill the template with content from data attributes
if (title && popoverHeader) popoverHeader.innerHTML = title.trim();
if (content && popoverBody) popoverBody.innerHTML = content.trim();
// fill the template with content from options / data attributes
// also sanitize title && content
if (title && popoverHeader) setHtml(popoverHeader, title, sanitizeFn);
if (content && popoverBody) setHtml(popoverBody, content, sanitizeFn);
// set btn and enable access for styleTip
[self.btn] = popover.getElementsByClassName('btn-close');
// set popover animation and placement
if (!hasClass(popover, popoverString)) addClass(popover, popoverString);
@@ -2443,9 +2469,9 @@
}
function removePopover(self) {
const { element, popover, options } = self;
const { element, popover } = self;
element.removeAttribute(ariaDescribedBy);
options.container.removeChild(popover);
popover.remove();
self.timer = null;
}
@@ -2470,12 +2496,11 @@
function dismissHandlerToggle(self, add) {
const action = add ? addEventListener : removeEventListener;
const { options, element, popover } = self;
const { options, element, btn } = self;
const { trigger, dismissible } = options;
if (dismissible) {
const [btnClose] = popover.getElementsByClassName('btn-close');
if (btnClose) btnClose[action]('click', self.hide);
if (btn) btn[action]('click', self.hide);
} else {
if (trigger === 'focus') element[action]('focusout', self.hide);
if (trigger === 'hover') document[action]('touchstart', popoverTouchHandler, passiveHandler);
@@ -2488,12 +2513,10 @@
}
function popoverShowTrigger(self) {
dismissHandlerToggle(self, 1);
self.element.dispatchEvent(shownPopoverEvent);
}
function popoverHideTrigger(self) {
dismissHandlerToggle(self);
removePopover(self);
self.element.dispatchEvent(hiddenPopoverEvent);
}
@@ -2514,6 +2537,7 @@
self.timer = null;
self.popover = null;
self.arrow = null;
self.btn = null;
self.enabled = false;
// set unique ID for aria-describedby
self.id = `${popoverString}-${getUID(element)}`;
@@ -2535,6 +2559,21 @@
// crate popover
createPopover(self);
// set positions
const { container } = self.options;
const elementPosition = getComputedStyle(element).position;
const containerPosition = getComputedStyle(container).position;
const parentIsBody = container === document.body;
const containerIsStatic = !parentIsBody && containerPosition === 'static';
const containerIsRelative = !parentIsBody && containerPosition === 'relative';
const relContainer = containerIsStatic && closestRelative(container);
self.positions = {
elementPosition,
containerIsRelative,
containerIsStatic,
relContainer,
};
// bind
self.update = self.update.bind(self);
@@ -2563,23 +2602,21 @@
const { container } = options;
clearTimeout(self.timer);
if (!isVisibleTip(popover, container)) {
element.dispatchEvent(showPopoverEvent);
if (showPopoverEvent.defaultPrevented) return;
self.timer = setTimeout(() => {
if (!isVisibleTip(popover, container)) {
element.dispatchEvent(showPopoverEvent);
if (showPopoverEvent.defaultPrevented) return;
// append to the container
container.append(popover);
element.setAttribute(ariaDescribedBy, id);
// append to the container
container.appendChild(popover);
element.setAttribute(ariaDescribedBy, id);
self.update(e);
if (!hasClass(popover, showClass)) addClass(popover, showClass);
dismissHandlerToggle(self, 1);
self.update(e);
if (!hasClass(popover, showClass)) addClass(popover, showClass);
if (options.animation) emulateTransitionEnd(popover, () => popoverShowTrigger(self));
else popoverShowTrigger(self);
}
}, 17);
if (options.animation) emulateTransitionEnd(popover, () => popoverShowTrigger(self));
else popoverShowTrigger(self);
}
}
hide(e) {
@@ -2596,13 +2633,13 @@
const { element, popover, options } = self;
clearTimeout(self.timer);
self.timer = setTimeout(() => {
if (isVisibleTip(popover, options.container)) {
element.dispatchEvent(hidePopoverEvent);
if (hidePopoverEvent.defaultPrevented) return;
removeClass(popover, showClass);
dismissHandlerToggle(self);
if (options.animation) emulateTransitionEnd(popover, () => popoverHideTrigger(self));
else popoverHideTrigger(self);
@@ -2648,7 +2685,7 @@
const { popover, options } = self;
const { container, animation } = options;
if (animation && isVisibleTip(popover, container)) {
options.delay = 0; // reset delay
self.options.delay = 0; // reset delay
self.hide();
emulateTransitionEnd(popover, () => togglePopoverHandlers(self));
} else {
@@ -3067,7 +3104,7 @@
const toastSelector = `.${toastString}`;
const toastDismissSelector = `[${dataBsDismiss}="${toastString}"]`;
const showingClass = 'showing';
const hideClass = 'hide';
const hideClass = 'hide'; // marked as deprecated
const toastDefaultOptions = {
animation: true,
autohide: true,
@@ -3085,10 +3122,7 @@
// =====================
function showToastComplete(self) {
const { element, options } = self;
if (!options.animation) {
removeClass(element, showingClass);
addClass(element, showClass);
}
removeClass(element, showingClass);
element.dispatchEvent(shownToastEvent);
if (options.autohide) self.hide();
@@ -3096,13 +3130,15 @@
function hideToastComplete(self) {
const { element } = self;
addClass(element, hideClass);
removeClass(element, showingClass);
removeClass(element, showClass);
addClass(element, hideClass); // B/C
element.dispatchEvent(hiddenToastEvent);
}
function closeToast(self) {
function hideToast(self) {
const { element, options } = self;
removeClass(element, showClass);
addClass(element, showingClass);
if (options.animation) {
reflow(element);
@@ -3112,15 +3148,14 @@
}
}
function openToast(self) {
function showToast(self) {
const { element, options } = self;
removeClass(element, hideClass);
removeClass(element, hideClass); // B/C
reflow(element);
addClass(element, showClass);
addClass(element, showingClass);
if (options.animation) {
reflow(element);
addClass(element, showingClass);
addClass(element, showClass);
emulateTransitionEnd(element, () => showToastComplete(self));
} else {
showToastComplete(self);
@@ -3148,9 +3183,13 @@
super(toastComponent, target, toastDefaultOptions, config);
// bind
const self = this;
const { element, options } = self;
// set fadeClass, the options.animation will override the markup
if (options.animation && !hasClass(element, fadeClass)) addClass(element, fadeClass);
else if (!options.animation && hasClass(element, fadeClass)) removeClass(element, fadeClass);
// dismiss button
self.dismiss = queryElement(toastDismissSelector, self.element);
self.dismiss = queryElement(toastDismissSelector, element);
// bind
self.show = self.show.bind(self);
@@ -3165,13 +3204,12 @@
show() {
const self = this;
const { element } = self;
if (element && hasClass(element, hideClass)) {
if (element && !hasClass(element, showClass)) {
element.dispatchEvent(showToastEvent);
if (showToastEvent.defaultPrevented) return;
addClass(element, fadeClass);
clearTimeout(self.timer);
self.timer = setTimeout(() => openToast(self), 10);
self.timer = setTimeout(() => showToast(self), 10);
}
}
@@ -3184,7 +3222,7 @@
if (hideToastEvent.defaultPrevented) return;
clearTimeout(self.timer);
self.timer = setTimeout(() => closeToast(self),
self.timer = setTimeout(() => hideToast(self),
noTimer ? 10 : options.delay);
}
}
@@ -3192,7 +3230,7 @@
dispose() {
const self = this;
const { element, options } = self;
self.hide();
self.hide(1);
if (options.animation) emulateTransitionEnd(element, () => completeDisposeToast(self));
else completeDisposeToast(self);
@@ -3221,13 +3259,14 @@
const titleAttr = 'title';
const tooltipInnerClass = `${tooltipString}-inner`;
const tooltipDefaultOptions = {
title: null,
template: '<div class="tooltip" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',
placement: 'top',
animation: true,
customClass: null,
delay: 200,
sanitizeFn: null,
title: null, // string
customClass: null, // string | null
placement: 'top', // string
sanitizeFn: null, // function
animation: true, // bool
html: false, // bool
delay: 200, // number
};
// TOOLTIP CUSTOM EVENTS
@@ -3241,51 +3280,48 @@
// =======================
function createTooltip(self) {
const { options, id } = self;
const placementClass = `bs-${tooltipString}-${tipClassPositions[options.placement]}`;
let titleString = options.title.trim();
const {
title, template, customClass, animation, placement, sanitizeFn,
} = options;
const placementClass = `bs-${tooltipString}-${tipClassPositions[placement]}`;
// sanitize stuff
if (options.sanitizeFn) {
titleString = options.sanitizeFn(titleString);
options.template = options.sanitizeFn(options.template);
if (!title) return;
// load template
let tooltipTemplate;
if (typeof template === 'object') {
tooltipTemplate = template;
} else {
const htmlMarkup = document.createElement('div');
setHtml(htmlMarkup, template, sanitizeFn);
tooltipTemplate = htmlMarkup.firstChild;
}
if (!titleString) return;
// create tooltip
self.tooltip = document.createElement('div');
self.tooltip = tooltipTemplate.cloneNode(true);
const { tooltip } = self;
// set aria
// set title
setHtml(queryElement(`.${tooltipInnerClass}`, tooltip), title, sanitizeFn);
// set id & role attribute
tooltip.setAttribute('id', id);
// set markup
const tooltipMarkup = document.createElement('div');
tooltipMarkup.innerHTML = options.template.trim();
tooltip.className = tooltipMarkup.firstChild.className;
tooltip.innerHTML = tooltipMarkup.firstChild.innerHTML;
queryElement(`.${tooltipInnerClass}`, tooltip).innerHTML = titleString;
tooltip.setAttribute('role', tooltipString);
// set arrow
self.arrow = queryElement(`.${tooltipString}-arrow`, tooltip);
// set class and role attribute
tooltip.setAttribute('role', tooltipString);
// set classes
if (!hasClass(tooltip, tooltipString)) addClass(tooltip, tooltipString);
if (options.animation && !hasClass(tooltip, fadeClass)) addClass(tooltip, fadeClass);
if (options.customClass && !hasClass(tooltip, options.customClass)) {
addClass(tooltip, options.customClass);
if (animation && !hasClass(tooltip, fadeClass)) addClass(tooltip, fadeClass);
if (customClass && !hasClass(tooltip, customClass)) {
addClass(tooltip, customClass);
}
if (!hasClass(tooltip, placementClass)) addClass(tooltip, placementClass);
}
function removeTooltip(self) {
const { element, options, tooltip } = self;
const { element, tooltip } = self;
element.removeAttribute(ariaDescribedBy);
options.container.removeChild(tooltip);
tooltip.remove();
self.timer = null;
}
@@ -3387,6 +3423,21 @@
self.id = `${tooltipString}-${getUID(element)}`;
createTooltip(self);
// set positions
const { container } = self.options;
const elementPosition = getComputedStyle(element).position;
const containerPosition = getComputedStyle(container).position;
const parentIsBody = container === document.body;
const containerIsStatic = !parentIsBody && containerPosition === 'static';
const containerIsRelative = !parentIsBody && containerPosition === 'relative';
const relContainer = containerIsStatic && closestRelative(container);
self.positions = {
elementPosition,
containerIsRelative,
containerIsStatic,
relContainer,
};
// attach events
toggleTooltipHandlers(self, 1);
}
@@ -3398,22 +3449,23 @@
const {
options, tooltip, element, id,
} = self;
const {
container, animation,
} = options;
clearTimeout(self.timer);
self.timer = setTimeout(() => {
if (!isVisibleTip(tooltip, options.container)) {
element.dispatchEvent(showTooltipEvent);
if (showTooltipEvent.defaultPrevented) return;
if (!isVisibleTip(tooltip, container)) {
element.dispatchEvent(showTooltipEvent);
if (showTooltipEvent.defaultPrevented) return;
// append to container
options.container.appendChild(tooltip);
element.setAttribute(ariaDescribedBy, id);
// append to container
container.append(tooltip);
element.setAttribute(ariaDescribedBy, id);
self.update(e);
if (!hasClass(tooltip, showClass)) addClass(tooltip, showClass);
if (options.animation) emulateTransitionEnd(tooltip, () => tooltipShownAction(self));
else tooltipShownAction(self);
}
}, 20);
self.update(e);
if (!hasClass(tooltip, showClass)) addClass(tooltip, showClass);
if (animation) emulateTransitionEnd(tooltip, () => tooltipShownAction(self));
else tooltipShownAction(self);
}
}
hide(e) {
@@ -3498,20 +3550,9 @@
constructor: Tooltip,
};
var version = "4.0.6";
var version = "4.0.8";
// import { alertInit } from '../components/alert-native.js';
// import { buttonInit } from '../components/button-native.js';
// import { carouselInit } from '../components/carousel-native.js';
// import { collapseInit } from '../components/collapse-native.js';
// import { dropdownInit } from '../components/dropdown-native.js';
// import { modalInit } from '../components/modal-native.js';
// import { offcanvasInit } from '../components/offcanvas-native.js';
// import { popoverInit } from '../components/popover-native.js';
// import { scrollSpyInit } from '../components/scrollspy-native.js';
// import { tabInit } from '../components/tab-native.js';
// import { toastInit } from '../components/toast-native.js';
// import { tooltipInit } from '../components/tooltip-native.js';
const Version = version;
const componentsInit = {
Alert: Alert.init,
@@ -3547,7 +3588,7 @@
document.addEventListener('DOMContentLoaded', () => initCallback(), { once: true });
}
var index = {
const BSN = {
Alert,
Button,
Carousel,
@@ -3562,9 +3603,9 @@
Tooltip,
initCallback,
Version: version,
Version,
};
return index;
return BSN;
})));
}));

File diff suppressed because it is too large Load Diff

View File

@@ -4,10 +4,10 @@
*
* To rebuild or modify this file with the latest versions of the included
* software please visit:
* https://datatables.net/download/#bs5/dt-1.11.2
* https://datatables.net/download/#bs5/dt-1.11.3
*
* Included libraries:
* DataTables 1.11.2
* DataTables 1.11.3
*/
@charset "UTF-8";

View File

@@ -4,20 +4,20 @@
*
* To rebuild or modify this file with the latest versions of the included
* software please visit:
* https://datatables.net/download/#bs5/dt-1.11.2
* https://datatables.net/download/#bs5/dt-1.11.3
*
* Included libraries:
* DataTables 1.11.2
* DataTables 1.11.3
*/
/*! DataTables 1.11.2
/*! DataTables 1.11.3
* ©2008-2021 SpryMedia Ltd - datatables.net/license
*/
/**
* @summary DataTables
* @description Paginate, search and order HTML tables
* @version 1.11.2
* @version 1.11.3
* @file jquery.dataTables.js
* @author SpryMedia Ltd
* @contact www.datatables.net
@@ -1626,6 +1626,14 @@
return out;
}
var _includes = function (search, start) {
if (start === undefined) {
start = 0;
}
return this.indexOf(search, start) !== -1;
};
// Array.isArray polyfill.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray
if (! Array.isArray) {
@@ -1634,6 +1642,10 @@
};
}
if (! Array.prototype.includes) {
Array.prototype.includes = _includes;
}
// .trim() polyfill
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trim
if (!String.prototype.trim) {
@@ -1642,6 +1654,10 @@
};
}
if (! String.prototype.includes) {
String.prototype.includes = _includes;
}
/**
* DataTables utility methods
*
@@ -2808,9 +2824,18 @@
return cellData.call( rowData );
}
if ( cellData === null && type == 'display' ) {
if ( cellData === null && type === 'display' ) {
return '';
}
if ( type === 'filter' ) {
var fomatters = DataTable.ext.type.search;
if ( fomatters[ col.sType ] ) {
cellData = fomatters[ col.sType ]( cellData );
}
}
return cellData;
}
@@ -4565,7 +4590,6 @@
var columns = settings.aoColumns;
var column;
var i, j, ien, jen, filterData, cellData, row;
var fomatters = DataTable.ext.type.search;
var wasInvalidated = false;
for ( i=0, ien=settings.aoData.length ; i<ien ; i++ ) {
@@ -4580,10 +4604,6 @@
if ( column.bSearchable ) {
cellData = _fnGetCellData( settings, i, j, 'filter' );
if ( fomatters[ column.sType ] ) {
cellData = fomatters[ column.sType ]( cellData );
}
// Search in DataTables 1.10 is string based. In 1.11 this
// should be altered to also allow strict type checking.
if ( cellData === null ) {
@@ -6374,6 +6394,10 @@
*/
function _fnSaveState ( settings )
{
if (settings._bLoadingState) {
return;
}
/* Store the interesting variables */
var state = {
time: +new Date(),
@@ -6408,99 +6432,129 @@
*/
function _fnLoadState ( settings, oInit, callback )
{
var i, ien;
var columns = settings.aoColumns;
var loaded = function ( s ) {
if ( ! s || ! s.time ) {
callback();
return;
}
// Allow custom and plug-in manipulation functions to alter the saved data set and
// cancelling of loading by returning false
var abStateLoad = _fnCallbackFire( settings, 'aoStateLoadParams', 'stateLoadParams', [settings, s] );
if ( $.inArray( false, abStateLoad ) !== -1 ) {
callback();
return;
}
// Reject old data
var duration = settings.iStateDuration;
if ( duration > 0 && s.time < +new Date() - (duration*1000) ) {
callback();
return;
}
// Number of columns have changed - all bets are off, no restore of settings
if ( s.columns && columns.length !== s.columns.length ) {
callback();
return;
}
// Store the saved state so it might be accessed at any time
settings.oLoadedState = $.extend( true, {}, s );
// Restore key features - todo - for 1.11 this needs to be done by
// subscribed events
if ( s.start !== undefined ) {
settings._iDisplayStart = s.start;
settings.iInitDisplayStart = s.start;
}
if ( s.length !== undefined ) {
settings._iDisplayLength = s.length;
}
// Order
if ( s.order !== undefined ) {
settings.aaSorting = [];
$.each( s.order, function ( i, col ) {
settings.aaSorting.push( col[0] >= columns.length ?
[ 0, col[1] ] :
col
);
} );
}
// Search
if ( s.search !== undefined ) {
$.extend( settings.oPreviousSearch, _fnSearchToHung( s.search ) );
}
// Columns
//
if ( s.columns ) {
for ( i=0, ien=s.columns.length ; i<ien ; i++ ) {
var col = s.columns[i];
// Visibility
if ( col.visible !== undefined ) {
columns[i].bVisible = col.visible;
}
// Search
if ( col.search !== undefined ) {
$.extend( settings.aoPreSearchCols[i], _fnSearchToHung( col.search ) );
}
}
}
_fnCallbackFire( settings, 'aoStateLoaded', 'stateLoaded', [settings, s] );
callback();
};
if ( ! settings.oFeatures.bStateSave ) {
callback();
return;
}
var loaded = function(state) {
_fnImplementState(settings, state, callback);
}
var state = settings.fnStateLoadCallback.call( settings.oInstance, settings, loaded );
if ( state !== undefined ) {
loaded( state );
_fnImplementState( settings, state, callback );
}
// otherwise, wait for the loaded callback to be executed
return true;
}
function _fnImplementState ( settings, s, callback) {
var i, ien;
var columns = settings.aoColumns;
settings._bLoadingState = true;
// When StateRestore was introduced the state could now be implemented at any time
// Not just initialisation. To do this an api instance is required in some places
var api = settings._bInitComplete ? new DataTable.Api(settings) : null;
if ( ! s || ! s.time ) {
settings._bLoadingState = false;
callback();
return;
}
// Allow custom and plug-in manipulation functions to alter the saved data set and
// cancelling of loading by returning false
var abStateLoad = _fnCallbackFire( settings, 'aoStateLoadParams', 'stateLoadParams', [settings, s] );
if ( $.inArray( false, abStateLoad ) !== -1 ) {
settings._bLoadingState = false;
callback();
return;
}
// Reject old data
var duration = settings.iStateDuration;
if ( duration > 0 && s.time < +new Date() - (duration*1000) ) {
settings._bLoadingState = false;
callback();
return;
}
// Number of columns have changed - all bets are off, no restore of settings
if ( s.columns && columns.length !== s.columns.length ) {
settings._bLoadingState = false;
callback();
return;
}
// Store the saved state so it might be accessed at any time
settings.oLoadedState = $.extend( true, {}, s );
// Restore key features - todo - for 1.11 this needs to be done by
// subscribed events
if ( s.start !== undefined ) {
settings._iDisplayStart = s.start;
if(api === null) {
settings.iInitDisplayStart = s.start;
}
}
if ( s.length !== undefined ) {
settings._iDisplayLength = s.length;
}
// Order
if ( s.order !== undefined ) {
settings.aaSorting = [];
$.each( s.order, function ( i, col ) {
settings.aaSorting.push( col[0] >= columns.length ?
[ 0, col[1] ] :
col
);
} );
}
// Search
if ( s.search !== undefined ) {
$.extend( settings.oPreviousSearch, _fnSearchToHung( s.search ) );
}
// Columns
if ( s.columns ) {
for ( i=0, ien=s.columns.length ; i<ien ; i++ ) {
var col = s.columns[i];
// Visibility
if ( col.visible !== undefined ) {
// If the api is defined, the table has been initialised so we need to use it rather than internal settings
if (api) {
// Don't redraw the columns on every iteration of this loop, we will do this at the end instead
api.column(i).visible(col.visible, false);
}
else {
columns[i].bVisible = col.visible;
}
}
// Search
if ( col.search !== undefined ) {
$.extend( settings.aoPreSearchCols[i], _fnSearchToHung( col.search ) );
}
}
// If the api is defined then we need to adjust the columns once the visibility has been changed
if (api) {
api.columns.adjust();
}
}
settings._bLoadingState = false;
_fnCallbackFire( settings, 'aoStateLoaded', 'stateLoaded', [settings, s] );
callback();
};
/**
* Return the settings object for a particular table
@@ -9590,7 +9644,7 @@
* @type string
* @default Version number
*/
DataTable.version = "1.11.2";
DataTable.version = "1.11.3";
/**
* Private data store, containing all of the settings objects that are
@@ -14015,7 +14069,7 @@
*
* @type string
*/
build:"bs5/dt-1.11.2",
build:"bs5/dt-1.11.3",
/**
@@ -15048,6 +15102,10 @@
*/
var __htmlEscapeEntities = function ( d ) {
if (Array.isArray(d)) {
d = d.join(',');
}
return typeof d === 'string' ?
d
.replace(/&/g, '&amp;')
@@ -15242,6 +15300,7 @@
_fnSortData: _fnSortData,
_fnSaveState: _fnSaveState,
_fnLoadState: _fnLoadState,
_fnImplementState: _fnImplementState,
_fnSettingsFromNode: _fnSettingsFromNode,
_fnLog: _fnLog,
_fnMap: _fnMap,

View File

@@ -150,7 +150,7 @@
<dt class="col-sm-5">Domain configuration
<span class="badge bg-success d-none" id="domain-success" title="The domain variable matches the browser location and seems to be configured correctly.">Match</span>
<span class="badge bg-danger d-none" id="domain-warning" title="The domain variable does not matches the browsers location.&#013;&#010;The domain variable does not seem to be configured correctly.&#013;&#010;Some features may not work as expected!">No Match</span>
<span class="badge bg-danger d-none" id="domain-warning" title="The domain variable does not match the browser location.&#013;&#010;The domain variable does not seem to be configured correctly.&#013;&#010;Some features may not work as expected!">No Match</span>
<span class="badge bg-success d-none" id="https-success" title="Configurued to use HTTPS">HTTPS</span>
<span class="badge bg-danger d-none" id="https-warning" title="Not configured to use HTTPS.&#013;&#010;Some features may not work as expected!">No HTTPS</span>
</dt>

View File

@@ -0,0 +1,10 @@
Incomplete Two-Step Login From {{{device}}}
<!---------------->
Someone attempted to log into your account with the correct master password, but did not provide the correct token or action required to complete the two-step login process within {{time_limit}} minutes of the initial login attempt.
* Date: {{datetime}}
* IP Address: {{ip}}
* Device Type: {{device}}
If this was not you or someone you authorized, then you should change your master password as soon as possible, as it is likely to be compromised.
{{> email/email_footer_text }}

View File

@@ -0,0 +1,31 @@
Incomplete Two-Step Login From {{{device}}}
<!---------------->
{{> email/email_header }}
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
Someone attempted to log into your account with the correct master password, but did not provide the correct token or action required to complete the two-step login process within {{time_limit}} minutes of the initial login attempt.
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
<b>Date</b>: {{datetime}}
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
<b>IP Address:</b> {{ip}}
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
<b>Device Type:</b> {{device}}
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top">
If this was not you or someone you authorized, then you should change your master password as soon as possible, as it is likely to be compromised.
</td>
</tr>
</table>
{{> email/email_footer }}

View File

@@ -282,9 +282,9 @@ pub fn delete_file(path: &str) -> IOResult<()> {
res
}
const UNITS: [&str; 6] = ["bytes", "KB", "MB", "GB", "TB", "PB"];
pub fn get_display_size(size: i32) -> String {
const UNITS: [&str; 6] = ["bytes", "KB", "MB", "GB", "TB", "PB"];
let mut size: f64 = size.into();
let mut unit_counter = 0;
@@ -359,10 +359,10 @@ where
try_parse_string(get_env_str_value(key))
}
const TRUE_VALUES: &[&str] = &["true", "t", "yes", "y", "1"];
const FALSE_VALUES: &[&str] = &["false", "f", "no", "n", "0"];
pub fn get_env_bool(key: &str) -> Option<bool> {
const TRUE_VALUES: &[&str] = &["true", "t", "yes", "y", "1"];
const FALSE_VALUES: &[&str] = &["false", "f", "no", "n", "0"];
match get_env_str_value(key) {
Some(val) if TRUE_VALUES.contains(&val.to_lowercase().as_ref()) => Some(true),
Some(val) if FALSE_VALUES.contains(&val.to_lowercase().as_ref()) => Some(false),
@@ -375,7 +375,6 @@ pub fn get_env_bool(key: &str) -> Option<bool> {
//
use chrono::{DateTime, Local, NaiveDateTime, TimeZone};
use chrono_tz::Tz;
/// Formats a UTC-offset `NaiveDateTime` in the format used by Bitwarden API
/// responses with "date" fields (`CreationDate`, `RevisionDate`, etc.).
@@ -393,7 +392,7 @@ pub fn format_datetime_local(dt: &DateTime<Local>, fmt: &str) -> String {
// Try parsing the `TZ` environment variable to enable formatting `%Z` as
// a time zone abbreviation.
if let Ok(tz) = env::var("TZ") {
if let Ok(tz) = tz.parse::<Tz>() {
if let Ok(tz) = tz.parse::<chrono_tz::Tz>() {
return dt.with_timezone(&tz).format(fmt).to_string();
}
}
@@ -442,7 +441,7 @@ use serde_json::{self, Value};
pub type JsonMap = serde_json::Map<String, Value>;
#[derive(PartialEq, Serialize, Deserialize)]
#[derive(Serialize, Deserialize)]
pub struct UpCase<T: DeserializeOwned> {
#[serde(deserialize_with = "upcase_deserialize")]
#[serde(flatten)]
@@ -517,6 +516,8 @@ fn upcase_value(value: Value) -> Value {
}
}
// Inner function to handle some speciale case for the 'ssn' key.
// This key is part of the Identity Cipher (Social Security Number)
fn _process_key(key: &str) -> String {
match key.to_lowercase().as_ref() {
"ssn" => "SSN".into(),