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. ## Defaults to daily (5 minutes after midnight). Set blank to disable this job.
# TRASH_PURGE_SCHEDULE="0 5 0 * * *" # 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. ## 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. ## Defaults to hourly (5 minutes after the hour). Set blank to disable this job.
# EMERGENCY_NOTIFICATION_REMINDER_SCHEDULE="0 5 * * * *" # 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. ## This setting applies globally, so make sure to inform all users of any changes to this setting.
# TRASH_AUTO_DELETE_DAYS= # 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 ## Controls the PBBKDF password iterations to apply on the server
## The change only applies when the password is changed ## The change only applies when the password is changed
# PASSWORD_ITERATIONS=100000 # 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" name = "vaultwarden"
version = "1.0.0" version = "1.0.0"
authors = ["Daniel García <dani-garcia@users.noreply.github.com>"] authors = ["Daniel García <dani-garcia@users.noreply.github.com>"]
edition = "2018" edition = "2021"
rust-version = "1.57" rust-version = "1.57"
resolver = "2" resolver = "2"
@@ -34,11 +34,11 @@ rocket = { version = "=0.5.0-dev", features = ["tls"], default-features = false
rocket_contrib = "=0.5.0-dev" rocket_contrib = "=0.5.0-dev"
# HTTP client # 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 # Used for custom short lived cookie jar
cookie = "0.15.1" cookie = "0.15.1"
cookie_store = "0.15.0" cookie_store = "0.15.1"
bytes = "1.1.0" bytes = "1.1.0"
url = "2.2.2" url = "2.2.2"
@@ -46,7 +46,7 @@ url = "2.2.2"
multipart = { version = "0.18.0", features = ["server"], default-features = false } multipart = { version = "0.18.0", features = ["server"], default-features = false }
# WebSockets library # WebSockets library
ws = { version = "0.11.0", package = "parity-ws" } ws = { version = "0.11.1", package = "parity-ws" }
# MessagePack library # MessagePack library
rmpv = "1.0.0" rmpv = "1.0.0"
@@ -56,7 +56,7 @@ chashmap = "2.2.2"
# A generic serialization/deserialization framework # A generic serialization/deserialization framework
serde = { version = "1.0.130", features = ["derive"] } serde = { version = "1.0.130", features = ["derive"] }
serde_json = "1.0.68" serde_json = "1.0.72"
# Logging # Logging
log = "0.4.14" log = "0.4.14"
@@ -95,7 +95,7 @@ jsonwebtoken = "7.2.0"
# U2F library # U2F library
u2f = "0.2.0" u2f = "0.2.0"
webauthn-rs = "0.3.0-alpha.12" webauthn-rs = "0.3.0"
# Yubico Library # Yubico Library
yubico = { version = "0.10.0", features = ["online-tokio"], default-features = false } yubico = { version = "0.10.0", features = ["online-tokio"], default-features = false }
@@ -112,19 +112,19 @@ num-derive = "0.3.3"
# Email libraries # Email libraries
tracing = { version = "0.1.29", features = ["log"] } # Needed to have lettre trace logging used when SMTP_DEBUG is enabled. 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 # Template library
handlebars = { version = "4.1.3", features = ["dir_source"] } handlebars = { version = "4.1.5", features = ["dir_source"] }
# For favicon extraction from main website # For favicon extraction from main website
html5ever = "0.25.1" html5ever = "0.25.1"
markup5ever_rcdom = "0.1.0" markup5ever_rcdom = "0.1.0"
regex = { version = "1.5.4", features = ["std", "perf", "unicode-perl"], default-features = false } 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 # Used by U2F, JWT and Postgres
openssl = "0.10.36" openssl = "0.10.38"
# URL encoding library # URL encoding library
percent-encoding = "2.1.0" percent-encoding = "2.1.0"
@@ -135,19 +135,16 @@ idna = "0.2.3"
pico-args = "0.4.2" pico-args = "0.4.2"
# Logging panics to logfile instead stderr only # Logging panics to logfile instead stderr only
backtrace = "0.3.61" backtrace = "0.3.63"
# Macro ident concatenation # Macro ident concatenation
paste = "1.0.5" paste = "1.0.6"
[patch.crates-io] [patch.crates-io]
# Use newest ring # Use newest ring
rocket = { git = 'https://github.com/SergioBenitez/Rocket', rev = '263e39b5b429de1913ce7e3036575a7b4d88b6d7' } rocket = { git = 'https://github.com/SergioBenitez/Rocket', rev = '263e39b5b429de1913ce7e3036575a7b4d88b6d7' }
rocket_contrib = { 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 # 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 # 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. # 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" %} {% set build_stage_base_image = "rust:1.55-buster" %}
{% if "alpine" in target_file %} {% if "alpine" in target_file %}
{% if "amd64" 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 runtime_stage_base_image = "alpine:3.14" %}
{% set package_arch_target = "x86_64-unknown-linux-musl" %} {% set package_arch_target = "x86_64-unknown-linux-musl" %}
{% elif "armv7" in target_file %} {% elif "armv7" in target_file %}
@@ -51,8 +51,8 @@
# https://docs.docker.com/develop/develop-images/multistage-build/ # https://docs.docker.com/develop/develop-images/multistage-build/
# https://whitfin.io/speeding-up-rust-docker-builds/ # https://whitfin.io/speeding-up-rust-docker-builds/
####################### VAULT BUILD IMAGE ####################### ####################### VAULT BUILD IMAGE #######################
{% set vault_version = "2.23.0c" %} {% set vault_version = "2.25.0" %}
{% set vault_image_digest = "sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459" %} {% set vault_image_digest = "sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527" %}
# The web-vault digest specifies a particular web-vault build on Docker Hub. # The web-vault digest specifies a particular web-vault build on Docker Hub.
# Using the digest instead of the tag name provides better security, # 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 # 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, # - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
# click the tag name to view the digest of the image it currently points to. # click the tag name to view the digest of the image it currently points to.
# - From the command line: # - From the command line:
# $ docker pull vaultwarden/web-vault:v2.23.0c # $ docker pull vaultwarden/web-vault:v2.25.0
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.23.0c # $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.25.0
# [vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459] # [vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527]
# #
# - Conversely, to get the tag name from the digest: # - Conversely, to get the tag name from the digest:
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459 # $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527
# [vaultwarden/web-vault:v2.23.0c] # [vaultwarden/web-vault:v2.25.0]
# #
FROM vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459 as vault FROM vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527 as vault
########################## BUILD IMAGE ########################## ########################## BUILD IMAGE ##########################
FROM rust:1.55-buster as build FROM rust:1.55-buster as build

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -16,15 +16,15 @@
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags, # - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
# click the tag name to view the digest of the image it currently points to. # click the tag name to view the digest of the image it currently points to.
# - From the command line: # - From the command line:
# $ docker pull vaultwarden/web-vault:v2.23.0c # $ docker pull vaultwarden/web-vault:v2.25.0
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.23.0c # $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.25.0
# [vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459] # [vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527]
# #
# - Conversely, to get the tag name from the digest: # - Conversely, to get the tag name from the digest:
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459 # $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527
# [vaultwarden/web-vault:v2.23.0c] # [vaultwarden/web-vault:v2.25.0]
# #
FROM vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459 as vault FROM vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527 as vault
########################## BUILD IMAGE ########################## ########################## BUILD IMAGE ##########################
FROM messense/rust-musl-cross:armv7-musleabihf as build 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 once_cell::sync::Lazy;
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use serde_json::Value; use serde_json::Value;
use std::{env, time::Duration}; use std::env;
use rocket::{ use rocket::{
http::{Cookie, Cookies, SameSite, Status}, http::{Cookie, Cookies, SameSite, Status},
@@ -236,7 +236,7 @@ impl AdminTemplateData {
} }
#[get("/", rank = 1)] #[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()?; let text = AdminTemplateData::new().render()?;
Ok(Html(text)) Ok(Html(text))
} }
@@ -462,13 +462,13 @@ struct GitCommit {
fn get_github_api<T: DeserializeOwned>(url: &str) -> Result<T, Error> { fn get_github_api<T: DeserializeOwned>(url: &str) -> Result<T, Error> {
let github_api = get_reqwest_client(); 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 { fn has_http_access() -> bool {
let http_access = get_reqwest_client(); 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(), Ok(r) => r.status().is_success(),
_ => false, _ => false,
} }
@@ -494,7 +494,6 @@ fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> ApiResu
// Execute some environment checks // Execute some environment checks
let running_within_docker = is_running_in_docker(); let running_within_docker = is_running_in_docker();
let docker_base_image = docker_base_image();
let has_http_access = has_http_access(); let has_http_access = has_http_access();
let uses_proxy = env::var_os("HTTP_PROXY").is_some() let uses_proxy = env::var_os("HTTP_PROXY").is_some()
|| 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, "web_vault_version": web_vault_version.version,
"latest_web_build": latest_web_build, "latest_web_build": latest_web_build,
"running_within_docker": running_within_docker, "running_within_docker": running_within_docker,
"docker_base_image": docker_base_image, "docker_base_image": docker_base_image(),
"has_http_access": has_http_access, "has_http_access": has_http_access,
"ip_header_exists": &ip_header.0.is_some(), "ip_header_exists": &ip_header.0.is_some(),
"ip_header_match": ip_header_name == CONFIG.ip_header(), "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")] #[post("/accounts/verify-email")]
fn post_verify_email(headers: Headers, _conn: DbConn) -> EmptyResult { fn post_verify_email(headers: Headers) -> EmptyResult {
let user = headers.user; let user = headers.user;
if !CONFIG.mail_enabled() { if !CONFIG.mail_enabled() {
@@ -654,7 +654,7 @@ struct VerifyPasswordData {
} }
#[post("/accounts/verify-password", data = "<data>")] #[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 data: VerifyPasswordData = data.into_inner().data;
let user = headers.user; 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.status = EmergencyAccessStatus::Confirmed as i32;
emergency_access.key_encrypted = None;
emergency_access.save(&conn)?; emergency_access.save(&conn)?;
if CONFIG.mail_enabled() { if CONFIG.mail_enabled() {

View File

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

View File

@@ -1294,71 +1294,43 @@ fn put_policy(
#[allow(unused_variables)] #[allow(unused_variables)]
#[get("/organizations/<org_id>/tax")] #[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. // 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")] #[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!({ Json(json!({
"Object": "list", "Object": "list",
"Data": [ "Data": [{
{
"Object": "plan", "Object": "plan",
"Type": 0, "Type": 0,
"Product": 0, "Product": 0,
"Name": "Free", "Name": "Free",
"IsAnnual": false,
"NameLocalizationKey": "planNameFree", "NameLocalizationKey": "planNameFree",
"DescriptionLocalizationKey": "planDescFree", "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
}
],
"ContinuationToken": null "ContinuationToken": null
})) }))
} }
#[get("/plans/sales-tax-rates")] #[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. // Prevent a 404 error, which also causes Javascript errors.
Json(json!({ Json(_empty_data_json())
}
fn _empty_data_json() -> Value {
json!({
"Object": "list", "Object": "list",
"Data": [], "Data": [],
"ContinuationToken": null "ContinuationToken": null
})) })
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]

View File

@@ -1,3 +1,4 @@
use chrono::{Duration, Utc};
use data_encoding::BASE32; use data_encoding::BASE32;
use rocket::Route; use rocket::Route;
use rocket_contrib::json::Json; use rocket_contrib::json::Json;
@@ -7,7 +8,7 @@ use crate::{
api::{JsonResult, JsonUpcase, NumberOrString, PasswordData}, api::{JsonResult, JsonUpcase, NumberOrString, PasswordData},
auth::Headers, auth::Headers,
crypto, crypto,
db::{models::*, DbConn}, db::{models::*, DbConn, DbPool},
mail, CONFIG, 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 { fn disable_twofactor_put(data: JsonUpcase<DisableTwoFactorData>, headers: Headers, conn: DbConn) -> JsonResult {
disable_twofactor(data, headers, conn) 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::Route;
use rocket_contrib::json::Json; use rocket_contrib::json::Json;
use serde_json::Value; use serde_json::Value;
use url::Url;
use webauthn_rs::{base64_data::Base64UrlSafeData, proto::*, AuthenticationState, RegistrationState, Webauthn}; use webauthn_rs::{base64_data::Base64UrlSafeData, proto::*, AuthenticationState, RegistrationState, Webauthn};
use crate::{ use crate::{
@@ -22,7 +23,7 @@ pub fn routes() -> Vec<Route> {
struct WebauthnConfig { struct WebauthnConfig {
url: String, url: String,
origin: String, origin: Url,
rpid: String, rpid: String,
} }
@@ -31,13 +32,9 @@ impl WebauthnConfig {
let domain = CONFIG.domain(); let domain = CONFIG.domain();
let domain_origin = CONFIG.domain_origin(); let domain_origin = CONFIG.domain_origin();
Webauthn::new(Self { Webauthn::new(Self {
rpid: reqwest::Url::parse(&domain) rpid: Url::parse(&domain).map(|u| u.domain().map(str::to_owned)).ok().flatten().unwrap_or_default(),
.map(|u| u.domain().map(str::to_owned))
.ok()
.flatten()
.unwrap_or_default(),
url: domain, url: domain,
origin: domain_origin, origin: Url::parse(&domain_origin).unwrap(),
}) })
} }
} }
@@ -47,7 +44,7 @@ impl webauthn_rs::WebauthnConfig for WebauthnConfig {
&self.url &self.url
} }
fn get_origin(&self) -> &str { fn get_origin(&self) -> &Url {
&self.origin &self.origin
} }

View File

@@ -1,4 +1,4 @@
use chrono::Local; use chrono::Utc;
use num_traits::FromPrimitive; use num_traits::FromPrimitive;
use rocket::{ use rocket::{
request::{Form, FormItems, FromForm}, 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)) 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() { if user.verified_at.is_none() && CONFIG.mail_enabled() && CONFIG.signups_verify() {
let now = now.naive_utc();
if user.last_verifying_at.is_none() if user.last_verifying_at.is_none()
|| now.signed_duration_since(user.last_verifying_at.unwrap()).num_seconds() || now.signed_duration_since(user.last_verifying_at.unwrap()).num_seconds()
> CONFIG.signups_verify_resend_time() as i64 > CONFIG.signups_verify_resend_time() as i64
@@ -219,6 +218,8 @@ fn twofactor_auth(
return Ok(None); 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 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 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"), _ => err!("Invalid two factor provider"),
} }
TwoFactorIncomplete::mark_complete(user_uuid, &device.uuid, conn)?;
if !CONFIG.disable_2fa_remember() && remember == 1 { if !CONFIG.disable_2fa_remember() && remember == 1 {
Ok(Some(device.refresh_twofactor_remember())) Ok(Some(device.refresh_twofactor_remember()))
} else { } else {

View File

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

View File

@@ -4,7 +4,7 @@ use rocket::Route;
use rocket_contrib::json::Json; use rocket_contrib::json::Json;
use serde_json::Value as JsonValue; 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> { pub fn routes() -> Vec<Route> {
routes![negotiate, websockets_err] routes![negotiate, websockets_err]
@@ -30,7 +30,7 @@ fn websockets_err() -> EmptyResult {
} }
#[post("/hub/negotiate")] #[post("/hub/negotiate")]
fn negotiate(_headers: Headers, _conn: DbConn) -> Json<JsonValue> { fn negotiate(_headers: Headers) -> Json<JsonValue> {
use crate::crypto; use crate::crypto;
use data_encoding::BASE64URL; 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)] #[derive(Debug, Serialize, Deserialize)]
pub struct EmergencyAccessInviteJwtClaims { pub struct EmergencyAccessInviteJwtClaims {
// Not before // Not before

View File

@@ -2,7 +2,6 @@ use std::process::exit;
use std::sync::RwLock; use std::sync::RwLock;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use regex::Regex;
use reqwest::Url; use reqwest::Url;
use crate::{ 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; pub type Pass = String;
macro_rules! make_config { macro_rules! make_config {
@@ -61,7 +45,7 @@ macro_rules! make_config {
_overrides: Vec<String>, _overrides: Vec<String>,
} }
#[derive(Debug, Clone, Default, Deserialize, Serialize)] #[derive(Clone, Default, Deserialize, Serialize)]
pub struct ConfigBuilder { pub struct ConfigBuilder {
$($( $($(
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
@@ -133,19 +117,6 @@ macro_rules! make_config {
builder 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 { fn build(&self) -> ConfigItems {
let mut config = ConfigItems::default(); let mut config = ConfigItems::default();
let _domain_set = self.domain.is_some(); let _domain_set = self.domain.is_some();
@@ -161,12 +132,13 @@ macro_rules! make_config {
} }
} }
#[derive(Debug, Clone, Default)] #[derive(Clone, Default)]
pub struct ConfigItems { $($(pub $name: make_config!{@type $ty, $none_action}, )+)+ } struct ConfigItems { $($( $name: make_config!{@type $ty, $none_action}, )+)+ }
#[allow(unused)] #[allow(unused)]
impl Config { impl Config {
$($( $($(
$(#[doc = $doc])+
pub fn $name(&self) -> make_config!{@type $ty, $none_action} { pub fn $name(&self) -> make_config!{@type $ty, $none_action} {
self.inner.read().unwrap().config.$name.clone() self.inner.read().unwrap().config.$name.clone()
} }
@@ -189,38 +161,91 @@ macro_rules! make_config {
fn _get_doc(doc: &str) -> serde_json::Value { fn _get_doc(doc: &str) -> serde_json::Value {
let mut split = doc.split("|>").map(str::trim); let mut split = doc.split("|>").map(str::trim);
json!({
"name": split.next(), // We do not use the json!() macro here since that causes a lot of macro recursion.
"description": split.next() // 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!([ $({ // We do not use the json!() macro here since that causes a lot of macro recursion.
"group": stringify!($group), // This slows down compile time and it also causes issues with rust-analyzer
"grouptoggle": stringify!($($group_enabled)?), serde_json::Value::Array(<[_]>::into_vec(Box::new([
"groupdoc": make_config!{ @show $($groupdoc)? }, $(
"elements": [ serde_json::Value::Object({
$( { let mut group = serde_json::Map::new();
"editable": $editable, group.insert("group".into(), (stringify!($group)).into());
"name": stringify!($name), group.insert("grouptoggle".into(), (stringify!($($group_enabled)?)).into());
"value": cfg.$name, group.insert("groupdoc".into(), (make_config!{ @show $($groupdoc)? }).into());
"default": def.$name,
"type": _get_form_type(stringify!($ty)), group.insert("elements".into(), serde_json::Value::Array(<[_]>::into_vec(Box::new([
"doc": _get_doc(concat!($($doc),+)), $(
"overridden": overriden.contains(&stringify!($name).to_uppercase()), 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 { 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 cfg = {
let inner = &self.inner.read().unwrap(); let inner = &self.inner.read().unwrap();
inner.config.clone() inner.config.clone()
}; };
json!({ $($( /// We map over the string and remove all alphanumeric, _ and - characters.
stringify!($name): make_config!{ @supportstr $name, cfg.$name, $ty, $none_action }, /// 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> { pub fn get_overrides(&self) -> Vec<String> {
@@ -228,29 +253,30 @@ macro_rules! make_config {
let inner = &self.inner.read().unwrap(); let inner = &self.inner.read().unwrap();
inner._overrides.clone() inner._overrides.clone()
}; };
overrides overrides
} }
} }
}; };
// Support string print // 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, 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 ) => { String::from("***") }; // Required pass, we return "***" ( @supportstr $name:ident, $value:expr, Pass, $none_action:ident ) => { "***".into() }; // 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, String, option ) => { // Optional other value, we return as is or convert to string to apply the privacy config
if PRIVACY_CONFIG.contains(&stringify!($name)) { 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 { } 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)) { if PRIVACY_CONFIG.contains(&stringify!($name)) {
json!(PRIVACY_REGEX.replace_all(&$value.to_string(), "${1}*").to_string()) _privacy_mask(&$value).into()
} else { } else {
json!($value) ($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 // Group or empty string
( @show ) => { "" }; ( @show ) => { "" };
@@ -300,8 +326,6 @@ make_config! {
data_folder: String, false, def, "data".to_string(); data_folder: String, false, def, "data".to_string();
/// Database URL /// Database URL
database_url: String, false, auto, |c| format!("{}/{}", c.data_folder, "db.sqlite3"); 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
icon_cache_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "icon_cache"); icon_cache_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "icon_cache");
/// Attachments folder /// Attachments folder
@@ -333,6 +357,9 @@ make_config! {
/// Trash purge schedule |> Cron schedule of the job that checks for trashed items to delete permanently. /// 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. /// Defaults to daily. Set blank to disable this job.
trash_purge_schedule: String, false, def, "0 5 0 * * *".to_string(); 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. /// 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. /// Defaults to hourly. Set blank to disable this job.
emergency_notification_reminder_schedule: String, false, def, "0 5 * * * *".to_string(); 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. /// sure to inform all users of any changes to this setting.
trash_auto_delete_days: i64, true, option; 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 /// 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, /// $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. /// 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 /// 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; 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 /// 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; 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 // Check if the icon blacklist regex is valid
if let Some(ref r) = cfg.icon_blacklist_regex { 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 { match validate_regex {
Ok(_) => (), Ok(_) => (),
Err(e) => err!(format!("`ICON_BLACKLIST_REGEX` is invalid: {:#?}", e)), Err(e) => err!(format!("`ICON_BLACKLIST_REGEX` is invalid: {:#?}", e)),
@@ -707,7 +744,7 @@ impl Config {
Ok(()) Ok(())
} }
pub fn update_config_partial(&self, other: ConfigBuilder) -> Result<(), Error> { fn update_config_partial(&self, other: ConfigBuilder) -> Result<(), Error> {
let builder = { let builder = {
let usr = &self.inner.read().unwrap()._usr; let usr = &self.inner.read().unwrap()._usr;
let mut _overrides = Vec::new(); let mut _overrides = Vec::new();
@@ -861,8 +898,6 @@ where
reg!("email/change_email", ".html"); reg!("email/change_email", ".html");
reg!("email/delete_account", ".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_accepted", ".html");
reg!("email/emergency_access_invite_confirmed", ".html"); reg!("email/emergency_access_invite_confirmed", ".html");
reg!("email/emergency_access_recovery_approved", ".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_rejected", ".html");
reg!("email/emergency_access_recovery_reminder", ".html"); reg!("email/emergency_access_recovery_reminder", ".html");
reg!("email/emergency_access_recovery_timed_out", ".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/new_device_logged_in", ".html");
reg!("email/pw_hint_none", ".html"); reg!("email/pw_hint_none", ".html");
reg!("email/pw_hint_some", ".html"); reg!("email/pw_hint_some", ".html");

View File

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

View File

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

View File

@@ -9,6 +9,7 @@ mod org_policy;
mod organization; mod organization;
mod send; mod send;
mod two_factor; mod two_factor;
mod two_factor_incomplete;
mod user; mod user;
pub use self::attachment::Attachment; 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::organization::{Organization, UserOrgStatus, UserOrgType, UserOrganization};
pub use self::send::{Send, SendType}; pub use self::send::{Send, SendType};
pub use self::two_factor::{TwoFactor, TwoFactorType}; pub use self::two_factor::{TwoFactor, TwoFactorType};
pub use self::two_factor_incomplete::TwoFactorIncomplete;
pub use self::user::{Invitation, User, UserStampException}; pub use self::user::{Invitation, User, UserStampException};

View File

@@ -1,8 +1,6 @@
use serde_json::Value; use serde_json::Value;
use crate::api::EmptyResult; use crate::{api::EmptyResult, db::DbConn, error::MapResult};
use crate::db::DbConn;
use crate::error::MapResult;
use super::User; use super::User;
@@ -161,7 +159,6 @@ impl TwoFactor {
use crate::api::core::two_factor::u2f::U2FRegistration; use crate::api::core::two_factor::u2f::U2FRegistration;
use crate::api::core::two_factor::webauthn::{get_webauthn_registrations, WebauthnRegistration}; use crate::api::core::two_factor::webauthn::{get_webauthn_registrations, WebauthnRegistration};
use std::convert::TryInto;
use webauthn_rs::proto::*; use webauthn_rs::proto::*;
for mut u2f in u2f_factors { 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::db::DbConn;
use crate::api::EmptyResult; use crate::api::EmptyResult;
@@ -273,6 +276,7 @@ impl User {
Folder::delete_all_by_user(&self.uuid, conn)?; Folder::delete_all_by_user(&self.uuid, conn)?;
Device::delete_all_by_user(&self.uuid, conn)?; Device::delete_all_by_user(&self.uuid, conn)?;
TwoFactor::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 Invitation::take(&self.email, conn); // Delete invitation if any
db_run! {conn: { 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! { table! {
users (uuid) { users (uuid) {
uuid -> Text, 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! { table! {
users (uuid) { users (uuid) {
uuid -> Text, 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! { table! {
users (uuid) { users (uuid) {
uuid -> Text, uuid -> Text,

View File

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

View File

@@ -1,6 +1,6 @@
use std::str::FromStr; use std::str::FromStr;
use chrono::{DateTime, Local}; use chrono::NaiveDateTime;
use percent_encoding::{percent_encode, NON_ALPHANUMERIC}; use percent_encoding::{percent_encode, NON_ALPHANUMERIC};
use lettre::{ 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) 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; use crate::util::upcase_first;
let device = upcase_first(device); 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(), "url": CONFIG.domain(),
"ip": ip, "ip": ip,
"device": device, "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) => { Err(e) => {
if e.is_client() { if e.is_client() {
debug!("SMTP Client error: {:#?}", e); debug!("SMTP Client error: {:#?}", e);
err!(format!("SMTP Client error: {}", e.to_string())); err!(format!("SMTP Client error: {}", e));
} else if e.is_transient() { } else if e.is_transient() {
debug!("SMTP 4xx error: {:#?}", e); debug!("SMTP 4xx error: {:#?}", e);
err!(format!("SMTP 4xx error: {}", e.to_string())); err!(format!("SMTP 4xx error: {}", e));
} else if e.is_permanent() { } else if e.is_permanent() {
debug!("SMTP 5xx error: {:#?}", e); debug!("SMTP 5xx error: {:#?}", e);
let mut msg = e.to_string(); 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)); err!(format!("SMTP 5xx error: {}", msg));
} else if e.is_timeout() { } else if e.is_timeout() {
debug!("SMTP timeout error: {:#?}", e); debug!("SMTP timeout error: {:#?}", e);
err!(format!("SMTP timeout error: {}", e.to_string())); err!(format!("SMTP timeout error: {}", e));
} else if e.is_tls() { } else if e.is_tls() {
debug!("SMTP Encryption error: {:#?}", e); debug!("SMTP Encryption error: {:#?}", e);
err!(format!("SMTP Encryption error: {}", e.to_string())); err!(format!("SMTP Encryption error: {}", e));
} else { } else {
debug!("SMTP {:#?}", e); debug!("SMTP {:#?}", e);
err!(format!("SMTP {}", e.to_string())); err!(format!("SMTP {}", e));
} }
} }
} }

View File

@@ -1,6 +1,10 @@
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
#![cfg_attr(feature = "unstable", feature(ip))] #![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; extern crate openssl;
#[macro_use] #[macro_use]
@@ -104,6 +108,14 @@ fn launch_info() {
} }
fn init_logging(level: log::LevelFilter) -> Result<(), fern::InitError> { 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() let mut logger = fern::Dispatch::new()
.level(level) .level(level)
// Hide unknown certificate errors if using self-signed // 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) .level_for("hyper::client", log::LevelFilter::Off)
// Prevent cookie_store logs // Prevent cookie_store logs
.level_for("cookie_store", log::LevelFilter::Off) .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()); .chain(std::io::stdout());
// Enable smtp debug logging only specifically for smtp when need. // 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. // Grant emergency access requests that have met the required wait time.
// This job should run before the emergency access reminders job to avoid // This job should run before the emergency access reminders job to avoid
// sending reminders for requests that are about to be granted anyway. // 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 * Copyright 2015-2021 © dnp_theme
* Licensed under MIT (https://github.com/thednp/bootstrap.native/blob/master/LICENSE) * 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 exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) : typeof define === 'function' && define.amd ? define(factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.BSN = 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'; const transitionEndEvent = 'webkitTransition' in document.head.style ? 'webkitTransitionEnd' : 'transitionend';
@@ -188,7 +188,7 @@
element.dispatchEvent(closedAlertEvent); element.dispatchEvent(closedAlertEvent);
self.dispose(); self.dispose();
element.parentNode.removeChild(element); element.remove();
} }
// ALERT PRIVATE METHOD // ALERT PRIVATE METHOD
@@ -1022,9 +1022,9 @@
function isEmptyAnchor(elem) { function isEmptyAnchor(elem) {
const parentAnchor = elem.closest('A'); const parentAnchor = elem.closest('A');
// anchor href starts with # // 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 # // 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) { function setFocus(element) {
@@ -1487,7 +1487,7 @@
function appendOverlay(hasFade, isModal) { function appendOverlay(hasFade, isModal) {
toggleOverlayType(isModal); toggleOverlayType(isModal);
document.body.appendChild(overlay); document.body.append(overlay);
if (hasFade) addClass(overlay, fadeClass); if (hasFade) addClass(overlay, fadeClass);
} }
@@ -1501,12 +1501,11 @@
} }
function removeOverlay() { function removeOverlay() {
const bd = document.body;
const currentOpen = getCurrentOpen(); const currentOpen = getCurrentOpen();
if (!currentOpen) { if (!currentOpen) {
removeClass(overlay, fadeClass); removeClass(overlay, fadeClass);
bd.removeChild(overlay); overlay.remove();
resetScrollbar(); resetScrollbar();
} }
} }
@@ -1928,7 +1927,7 @@
if ((!element.contains(target) && options.backdrop if ((!element.contains(target) && options.backdrop
&& (!trigger || (trigger && !triggers.includes(trigger)))) && (!trigger || (trigger && !triggers.includes(trigger))))
|| offCanvasDismiss.contains(target)) { || (offCanvasDismiss && offCanvasDismiss.contains(target))) {
self.relatedTarget = target === offCanvasDismiss ? offCanvasDismiss : null; self.relatedTarget = target === offCanvasDismiss ? offCanvasDismiss : null;
self.hide(); self.hide();
} }
@@ -2122,19 +2121,6 @@
.some((mediaType) => element instanceof mediaType); .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) // both popovers and tooltips (this, event)
function styleTip(self, e) { function styleTip(self, e) {
const tipClasses = /\b(top|bottom|start|end)+/; const tipClasses = /\b(top|bottom|start|end)+/;
@@ -2148,32 +2134,32 @@
let tipDimensions = { w: tip.offsetWidth, h: tip.offsetHeight }; let tipDimensions = { w: tip.offsetWidth, h: tip.offsetHeight };
const windowWidth = (document.documentElement.clientWidth || document.body.clientWidth); const windowWidth = (document.documentElement.clientWidth || document.body.clientWidth);
const windowHeight = (document.documentElement.clientHeight || document.body.clientHeight); const windowHeight = (document.documentElement.clientHeight || document.body.clientHeight);
const { element, options, arrow } = self; const {
element, options, arrow, positions,
} = self;
let { container, placement } = options; let { container, placement } = options;
let parentIsBody = container === document.body; let parentIsBody = container === document.body;
const targetPosition = getComputedStyle(element).position;
const parentPosition = getComputedStyle(container).position; const { elementPosition, containerIsStatic, relContainer } = positions;
const staticParent = !parentIsBody && parentPosition === 'static'; let { containerIsRelative } = positions;
let relativeParent = !parentIsBody && parentPosition === 'relative';
const relContainer = staticParent && closestRelative(container);
// static containers should refer to another relative container or the body // static containers should refer to another relative container or the body
container = relContainer || container; container = relContainer || container;
relativeParent = staticParent && relContainer ? 1 : relativeParent; containerIsRelative = containerIsStatic && relContainer ? 1 : containerIsRelative;
parentIsBody = container === document.body; parentIsBody = container === document.body;
const parentRect = container.getBoundingClientRect(); const parentRect = container.getBoundingClientRect();
const leftBoundry = relativeParent ? parentRect.left : 0; const leftBoundry = containerIsRelative ? parentRect.left : 0;
const rightBoundry = relativeParent ? parentRect.right : windowWidth; const rightBoundry = containerIsRelative ? parentRect.right : windowWidth;
// this case should not be possible // this case should not be possible
// absoluteParent = !parentIsBody && parentPosition === 'absolute', // containerIsAbsolute = !parentIsBody && containerPosition === 'absolute',
// this case requires a container with placement: relative // this case requires a container with position: relative
const absoluteTarget = targetPosition === 'absolute'; const absoluteTarget = elementPosition === 'absolute';
const targetRect = element.getBoundingClientRect(); const targetRect = element.getBoundingClientRect();
const scroll = parentIsBody const scroll = parentIsBody
? { x: window.pageXOffset, y: window.pageYOffset } ? { x: window.pageXOffset, y: window.pageYOffset }
: { x: container.scrollLeft, y: container.scrollTop }; : { x: container.scrollLeft, y: container.scrollTop };
const elemDimensions = { w: element.offsetWidth, h: element.offsetHeight }; const elemDimensions = { w: element.offsetWidth, h: element.offsetHeight };
const top = relativeParent ? element.offsetTop : targetRect.top; const top = containerIsRelative ? element.offsetTop : targetRect.top;
const left = relativeParent ? element.offsetLeft : targetRect.left; const left = containerIsRelative ? element.offsetLeft : targetRect.left;
// reset arrow style // reset arrow style
arrow.style.top = ''; arrow.style.top = '';
arrow.style.left = ''; arrow.style.left = '';
@@ -2245,8 +2231,12 @@
} }
} else if (['top', 'bottom'].includes(placement)) { } else if (['top', 'bottom'].includes(placement)) {
if (e && isMedia(element)) { if (e && isMedia(element)) {
const eX = !relativeParent ? e.pageX : e.layerX + (absoluteTarget ? element.offsetLeft : 0); const eX = !containerIsRelative
const eY = !relativeParent ? e.pageY : e.layerY + (absoluteTarget ? element.offsetTop : 0); ? e.pageX
: e.layerX + (absoluteTarget ? element.offsetLeft : 0);
const eY = !containerIsRelative
? e.pageY
: e.layerY + (absoluteTarget ? element.offsetTop : 0);
if (placement === 'top') { if (placement === 'top') {
topPosition = eY - tipDimensions.h - (isPopover ? arrowWidth : arrowHeight); topPosition = eY - tipDimensions.h - (isPopover ? arrowWidth : arrowHeight);
@@ -2323,6 +2313,36 @@
return modal || navbarFixed || document.body; 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 /* 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 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 title: null, // string
content: null, // string content: null, // string
sanitizeFn: null, // function
customClass: null, // string customClass: null, // string
dismissible: false, // boolean
animation: true, // boolean
trigger: 'hover', // string trigger: 'hover', // string
placement: 'top', // 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 delay: 200, // number
}; };
@@ -2350,11 +2371,8 @@
const isIphone = navigator.userAgentData const isIphone = navigator.userAgentData
? navigator.userAgentData.brands.some((x) => appleBrands.test(x.brand)) ? navigator.userAgentData.brands.some((x) => appleBrands.test(x.brand))
: appleBrands.test(navigator.userAgent); : appleBrands.test(navigator.userAgent);
// popoverArrowClass = `${popoverString}-arrow`,
const popoverHeaderClass = `${popoverString}-header`; const popoverHeaderClass = `${popoverString}-header`;
const popoverBodyClass = `${popoverString}-body`; const popoverBodyClass = `${popoverString}-body`;
// close btn for dissmissible popover
let popoverCloseButton = '<button type="button" class="btn-close"></button>';
// POPOVER CUSTOM EVENTS // POPOVER CUSTOM EVENTS
// ===================== // =====================
@@ -2387,51 +2405,59 @@
const { const {
animation, customClass, sanitizeFn, placement, dismissible, animation, customClass, sanitizeFn, placement, dismissible,
} = options; } = options;
let { title, content, template } = options; let {
title, content,
} = options;
const {
template, btnClose,
} = options;
// set initial popover class // set initial popover class
const placementClass = `bs-${popoverString}-${tipClassPositions[placement]}`; const placementClass = `bs-${popoverString}-${tipClassPositions[placement]}`;
// fixing #233 // load template
title = title ? title.trim() : null; let popoverTemplate;
content = content ? content.trim() : null; if (typeof template === 'object') {
popoverTemplate = template;
// sanitize title && content } else {
if (sanitizeFn) { const htmlMarkup = document.createElement('div');
title = title ? sanitizeFn(title) : null; setHtml(htmlMarkup, template, sanitizeFn);
content = content ? sanitizeFn(content) : null; popoverTemplate = htmlMarkup.firstChild;
template = template ? sanitizeFn(template) : null;
popoverCloseButton = sanitizeFn(popoverCloseButton);
} }
// set popover markup
self.popover = popoverTemplate.cloneNode(true);
self.popover = document.createElement('div');
const { popover } = self; const { popover } = self;
// set id and aria-describedby // set id and role attributes
popover.setAttribute('id', id); popover.setAttribute('id', id);
popover.setAttribute('role', 'tooltip'); 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 popoverHeader = queryElement(`.${popoverHeaderClass}`, popover);
const popoverBody = queryElement(`.${popoverBodyClass}`, popover); const popoverBody = queryElement(`.${popoverBodyClass}`, popover);
// set arrow // set arrow and enable access for styleTip
self.arrow = queryElement(`.${popoverString}-arrow`, popover); self.arrow = queryElement(`.${popoverString}-arrow`, popover);
// set dismissible button // set dismissible button
if (dismissible) { if (dismissible) {
title = title ? title + popoverCloseButton : title; if (title) {
content = title === null ? +popoverCloseButton : content; 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 // fill the template with content from options / data attributes
if (title && popoverHeader) popoverHeader.innerHTML = title.trim(); // also sanitize title && content
if (content && popoverBody) popoverBody.innerHTML = content.trim(); 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 // set popover animation and placement
if (!hasClass(popover, popoverString)) addClass(popover, popoverString); if (!hasClass(popover, popoverString)) addClass(popover, popoverString);
@@ -2443,9 +2469,9 @@
} }
function removePopover(self) { function removePopover(self) {
const { element, popover, options } = self; const { element, popover } = self;
element.removeAttribute(ariaDescribedBy); element.removeAttribute(ariaDescribedBy);
options.container.removeChild(popover); popover.remove();
self.timer = null; self.timer = null;
} }
@@ -2470,12 +2496,11 @@
function dismissHandlerToggle(self, add) { function dismissHandlerToggle(self, add) {
const action = add ? addEventListener : removeEventListener; const action = add ? addEventListener : removeEventListener;
const { options, element, popover } = self; const { options, element, btn } = self;
const { trigger, dismissible } = options; const { trigger, dismissible } = options;
if (dismissible) { if (dismissible) {
const [btnClose] = popover.getElementsByClassName('btn-close'); if (btn) btn[action]('click', self.hide);
if (btnClose) btnClose[action]('click', self.hide);
} else { } else {
if (trigger === 'focus') element[action]('focusout', self.hide); if (trigger === 'focus') element[action]('focusout', self.hide);
if (trigger === 'hover') document[action]('touchstart', popoverTouchHandler, passiveHandler); if (trigger === 'hover') document[action]('touchstart', popoverTouchHandler, passiveHandler);
@@ -2488,12 +2513,10 @@
} }
function popoverShowTrigger(self) { function popoverShowTrigger(self) {
dismissHandlerToggle(self, 1);
self.element.dispatchEvent(shownPopoverEvent); self.element.dispatchEvent(shownPopoverEvent);
} }
function popoverHideTrigger(self) { function popoverHideTrigger(self) {
dismissHandlerToggle(self);
removePopover(self); removePopover(self);
self.element.dispatchEvent(hiddenPopoverEvent); self.element.dispatchEvent(hiddenPopoverEvent);
} }
@@ -2514,6 +2537,7 @@
self.timer = null; self.timer = null;
self.popover = null; self.popover = null;
self.arrow = null; self.arrow = null;
self.btn = null;
self.enabled = false; self.enabled = false;
// set unique ID for aria-describedby // set unique ID for aria-describedby
self.id = `${popoverString}-${getUID(element)}`; self.id = `${popoverString}-${getUID(element)}`;
@@ -2535,6 +2559,21 @@
// crate popover // crate popover
createPopover(self); 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 // bind
self.update = self.update.bind(self); self.update = self.update.bind(self);
@@ -2563,23 +2602,21 @@
const { container } = options; const { container } = options;
clearTimeout(self.timer); clearTimeout(self.timer);
self.timer = setTimeout(() => {
if (!isVisibleTip(popover, container)) { if (!isVisibleTip(popover, container)) {
element.dispatchEvent(showPopoverEvent); element.dispatchEvent(showPopoverEvent);
if (showPopoverEvent.defaultPrevented) return; if (showPopoverEvent.defaultPrevented) return;
// append to the container // append to the container
container.appendChild(popover); container.append(popover);
element.setAttribute(ariaDescribedBy, id); element.setAttribute(ariaDescribedBy, id);
self.update(e); self.update(e);
if (!hasClass(popover, showClass)) addClass(popover, showClass); if (!hasClass(popover, showClass)) addClass(popover, showClass);
dismissHandlerToggle(self, 1);
if (options.animation) emulateTransitionEnd(popover, () => popoverShowTrigger(self)); if (options.animation) emulateTransitionEnd(popover, () => popoverShowTrigger(self));
else popoverShowTrigger(self); else popoverShowTrigger(self);
} }
}, 17);
} }
hide(e) { hide(e) {
@@ -2596,13 +2633,13 @@
const { element, popover, options } = self; const { element, popover, options } = self;
clearTimeout(self.timer); clearTimeout(self.timer);
self.timer = setTimeout(() => { self.timer = setTimeout(() => {
if (isVisibleTip(popover, options.container)) { if (isVisibleTip(popover, options.container)) {
element.dispatchEvent(hidePopoverEvent); element.dispatchEvent(hidePopoverEvent);
if (hidePopoverEvent.defaultPrevented) return; if (hidePopoverEvent.defaultPrevented) return;
removeClass(popover, showClass); removeClass(popover, showClass);
dismissHandlerToggle(self);
if (options.animation) emulateTransitionEnd(popover, () => popoverHideTrigger(self)); if (options.animation) emulateTransitionEnd(popover, () => popoverHideTrigger(self));
else popoverHideTrigger(self); else popoverHideTrigger(self);
@@ -2648,7 +2685,7 @@
const { popover, options } = self; const { popover, options } = self;
const { container, animation } = options; const { container, animation } = options;
if (animation && isVisibleTip(popover, container)) { if (animation && isVisibleTip(popover, container)) {
options.delay = 0; // reset delay self.options.delay = 0; // reset delay
self.hide(); self.hide();
emulateTransitionEnd(popover, () => togglePopoverHandlers(self)); emulateTransitionEnd(popover, () => togglePopoverHandlers(self));
} else { } else {
@@ -3067,7 +3104,7 @@
const toastSelector = `.${toastString}`; const toastSelector = `.${toastString}`;
const toastDismissSelector = `[${dataBsDismiss}="${toastString}"]`; const toastDismissSelector = `[${dataBsDismiss}="${toastString}"]`;
const showingClass = 'showing'; const showingClass = 'showing';
const hideClass = 'hide'; const hideClass = 'hide'; // marked as deprecated
const toastDefaultOptions = { const toastDefaultOptions = {
animation: true, animation: true,
autohide: true, autohide: true,
@@ -3085,10 +3122,7 @@
// ===================== // =====================
function showToastComplete(self) { function showToastComplete(self) {
const { element, options } = self; const { element, options } = self;
if (!options.animation) {
removeClass(element, showingClass); removeClass(element, showingClass);
addClass(element, showClass);
}
element.dispatchEvent(shownToastEvent); element.dispatchEvent(shownToastEvent);
if (options.autohide) self.hide(); if (options.autohide) self.hide();
@@ -3096,13 +3130,15 @@
function hideToastComplete(self) { function hideToastComplete(self) {
const { element } = self; const { element } = self;
addClass(element, hideClass); removeClass(element, showingClass);
removeClass(element, showClass);
addClass(element, hideClass); // B/C
element.dispatchEvent(hiddenToastEvent); element.dispatchEvent(hiddenToastEvent);
} }
function closeToast(self) { function hideToast(self) {
const { element, options } = self; const { element, options } = self;
removeClass(element, showClass); addClass(element, showingClass);
if (options.animation) { if (options.animation) {
reflow(element); reflow(element);
@@ -3112,15 +3148,14 @@
} }
} }
function openToast(self) { function showToast(self) {
const { element, options } = self; const { element, options } = self;
removeClass(element, hideClass); removeClass(element, hideClass); // B/C
reflow(element);
addClass(element, showClass);
addClass(element, showingClass);
if (options.animation) { if (options.animation) {
reflow(element);
addClass(element, showingClass);
addClass(element, showClass);
emulateTransitionEnd(element, () => showToastComplete(self)); emulateTransitionEnd(element, () => showToastComplete(self));
} else { } else {
showToastComplete(self); showToastComplete(self);
@@ -3148,9 +3183,13 @@
super(toastComponent, target, toastDefaultOptions, config); super(toastComponent, target, toastDefaultOptions, config);
// bind // bind
const self = this; 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 // dismiss button
self.dismiss = queryElement(toastDismissSelector, self.element); self.dismiss = queryElement(toastDismissSelector, element);
// bind // bind
self.show = self.show.bind(self); self.show = self.show.bind(self);
@@ -3165,13 +3204,12 @@
show() { show() {
const self = this; const self = this;
const { element } = self; const { element } = self;
if (element && hasClass(element, hideClass)) { if (element && !hasClass(element, showClass)) {
element.dispatchEvent(showToastEvent); element.dispatchEvent(showToastEvent);
if (showToastEvent.defaultPrevented) return; if (showToastEvent.defaultPrevented) return;
addClass(element, fadeClass);
clearTimeout(self.timer); clearTimeout(self.timer);
self.timer = setTimeout(() => openToast(self), 10); self.timer = setTimeout(() => showToast(self), 10);
} }
} }
@@ -3184,7 +3222,7 @@
if (hideToastEvent.defaultPrevented) return; if (hideToastEvent.defaultPrevented) return;
clearTimeout(self.timer); clearTimeout(self.timer);
self.timer = setTimeout(() => closeToast(self), self.timer = setTimeout(() => hideToast(self),
noTimer ? 10 : options.delay); noTimer ? 10 : options.delay);
} }
} }
@@ -3192,7 +3230,7 @@
dispose() { dispose() {
const self = this; const self = this;
const { element, options } = self; const { element, options } = self;
self.hide(); self.hide(1);
if (options.animation) emulateTransitionEnd(element, () => completeDisposeToast(self)); if (options.animation) emulateTransitionEnd(element, () => completeDisposeToast(self));
else completeDisposeToast(self); else completeDisposeToast(self);
@@ -3221,13 +3259,14 @@
const titleAttr = 'title'; const titleAttr = 'title';
const tooltipInnerClass = `${tooltipString}-inner`; const tooltipInnerClass = `${tooltipString}-inner`;
const tooltipDefaultOptions = { const tooltipDefaultOptions = {
title: null,
template: '<div class="tooltip" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>', template: '<div class="tooltip" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',
placement: 'top', title: null, // string
animation: true, customClass: null, // string | null
customClass: null, placement: 'top', // string
delay: 200, sanitizeFn: null, // function
sanitizeFn: null, animation: true, // bool
html: false, // bool
delay: 200, // number
}; };
// TOOLTIP CUSTOM EVENTS // TOOLTIP CUSTOM EVENTS
@@ -3241,51 +3280,48 @@
// ======================= // =======================
function createTooltip(self) { function createTooltip(self) {
const { options, id } = self; const { options, id } = self;
const placementClass = `bs-${tooltipString}-${tipClassPositions[options.placement]}`; const {
let titleString = options.title.trim(); title, template, customClass, animation, placement, sanitizeFn,
} = options;
const placementClass = `bs-${tooltipString}-${tipClassPositions[placement]}`;
// sanitize stuff if (!title) return;
if (options.sanitizeFn) {
titleString = options.sanitizeFn(titleString); // load template
options.template = options.sanitizeFn(options.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 // create tooltip
self.tooltip = document.createElement('div'); self.tooltip = tooltipTemplate.cloneNode(true);
const { tooltip } = self; const { tooltip } = self;
// set title
// set aria setHtml(queryElement(`.${tooltipInnerClass}`, tooltip), title, sanitizeFn);
// set id & role attribute
tooltip.setAttribute('id', id); tooltip.setAttribute('id', id);
tooltip.setAttribute('role', tooltipString);
// 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;
// set arrow // set arrow
self.arrow = queryElement(`.${tooltipString}-arrow`, tooltip); self.arrow = queryElement(`.${tooltipString}-arrow`, tooltip);
// set class and role attribute
tooltip.setAttribute('role', tooltipString);
// set classes // set classes
if (!hasClass(tooltip, tooltipString)) addClass(tooltip, tooltipString); if (!hasClass(tooltip, tooltipString)) addClass(tooltip, tooltipString);
if (options.animation && !hasClass(tooltip, fadeClass)) addClass(tooltip, fadeClass); if (animation && !hasClass(tooltip, fadeClass)) addClass(tooltip, fadeClass);
if (options.customClass && !hasClass(tooltip, options.customClass)) { if (customClass && !hasClass(tooltip, customClass)) {
addClass(tooltip, options.customClass); addClass(tooltip, customClass);
} }
if (!hasClass(tooltip, placementClass)) addClass(tooltip, placementClass); if (!hasClass(tooltip, placementClass)) addClass(tooltip, placementClass);
} }
function removeTooltip(self) { function removeTooltip(self) {
const { element, options, tooltip } = self; const { element, tooltip } = self;
element.removeAttribute(ariaDescribedBy); element.removeAttribute(ariaDescribedBy);
options.container.removeChild(tooltip); tooltip.remove();
self.timer = null; self.timer = null;
} }
@@ -3387,6 +3423,21 @@
self.id = `${tooltipString}-${getUID(element)}`; self.id = `${tooltipString}-${getUID(element)}`;
createTooltip(self); 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 // attach events
toggleTooltipHandlers(self, 1); toggleTooltipHandlers(self, 1);
} }
@@ -3398,22 +3449,23 @@
const { const {
options, tooltip, element, id, options, tooltip, element, id,
} = self; } = self;
const {
container, animation,
} = options;
clearTimeout(self.timer); clearTimeout(self.timer);
self.timer = setTimeout(() => { if (!isVisibleTip(tooltip, container)) {
if (!isVisibleTip(tooltip, options.container)) {
element.dispatchEvent(showTooltipEvent); element.dispatchEvent(showTooltipEvent);
if (showTooltipEvent.defaultPrevented) return; if (showTooltipEvent.defaultPrevented) return;
// append to container // append to container
options.container.appendChild(tooltip); container.append(tooltip);
element.setAttribute(ariaDescribedBy, id); element.setAttribute(ariaDescribedBy, id);
self.update(e); self.update(e);
if (!hasClass(tooltip, showClass)) addClass(tooltip, showClass); if (!hasClass(tooltip, showClass)) addClass(tooltip, showClass);
if (options.animation) emulateTransitionEnd(tooltip, () => tooltipShownAction(self)); if (animation) emulateTransitionEnd(tooltip, () => tooltipShownAction(self));
else tooltipShownAction(self); else tooltipShownAction(self);
} }
}, 20);
} }
hide(e) { hide(e) {
@@ -3498,20 +3550,9 @@
constructor: Tooltip, constructor: Tooltip,
}; };
var version = "4.0.6"; var version = "4.0.8";
// import { alertInit } from '../components/alert-native.js'; const Version = version;
// 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 componentsInit = { const componentsInit = {
Alert: Alert.init, Alert: Alert.init,
@@ -3547,7 +3588,7 @@
document.addEventListener('DOMContentLoaded', () => initCallback(), { once: true }); document.addEventListener('DOMContentLoaded', () => initCallback(), { once: true });
} }
var index = { const BSN = {
Alert, Alert,
Button, Button,
Carousel, Carousel,
@@ -3562,9 +3603,9 @@
Tooltip, Tooltip,
initCallback, 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 * To rebuild or modify this file with the latest versions of the included
* software please visit: * software please visit:
* https://datatables.net/download/#bs5/dt-1.11.2 * https://datatables.net/download/#bs5/dt-1.11.3
* *
* Included libraries: * Included libraries:
* DataTables 1.11.2 * DataTables 1.11.3
*/ */
@charset "UTF-8"; @charset "UTF-8";

View File

@@ -4,20 +4,20 @@
* *
* To rebuild or modify this file with the latest versions of the included * To rebuild or modify this file with the latest versions of the included
* software please visit: * software please visit:
* https://datatables.net/download/#bs5/dt-1.11.2 * https://datatables.net/download/#bs5/dt-1.11.3
* *
* Included libraries: * 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 * ©2008-2021 SpryMedia Ltd - datatables.net/license
*/ */
/** /**
* @summary DataTables * @summary DataTables
* @description Paginate, search and order HTML tables * @description Paginate, search and order HTML tables
* @version 1.11.2 * @version 1.11.3
* @file jquery.dataTables.js * @file jquery.dataTables.js
* @author SpryMedia Ltd * @author SpryMedia Ltd
* @contact www.datatables.net * @contact www.datatables.net
@@ -1626,6 +1626,14 @@
return out; return out;
} }
var _includes = function (search, start) {
if (start === undefined) {
start = 0;
}
return this.indexOf(search, start) !== -1;
};
// Array.isArray polyfill. // Array.isArray polyfill.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray
if (! Array.isArray) { if (! Array.isArray) {
@@ -1634,6 +1642,10 @@
}; };
} }
if (! Array.prototype.includes) {
Array.prototype.includes = _includes;
}
// .trim() polyfill // .trim() polyfill
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trim // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trim
if (!String.prototype.trim) { if (!String.prototype.trim) {
@@ -1642,6 +1654,10 @@
}; };
} }
if (! String.prototype.includes) {
String.prototype.includes = _includes;
}
/** /**
* DataTables utility methods * DataTables utility methods
* *
@@ -2808,9 +2824,18 @@
return cellData.call( rowData ); return cellData.call( rowData );
} }
if ( cellData === null && type == 'display' ) { if ( cellData === null && type === 'display' ) {
return ''; return '';
} }
if ( type === 'filter' ) {
var fomatters = DataTable.ext.type.search;
if ( fomatters[ col.sType ] ) {
cellData = fomatters[ col.sType ]( cellData );
}
}
return cellData; return cellData;
} }
@@ -4565,7 +4590,6 @@
var columns = settings.aoColumns; var columns = settings.aoColumns;
var column; var column;
var i, j, ien, jen, filterData, cellData, row; var i, j, ien, jen, filterData, cellData, row;
var fomatters = DataTable.ext.type.search;
var wasInvalidated = false; var wasInvalidated = false;
for ( i=0, ien=settings.aoData.length ; i<ien ; i++ ) { for ( i=0, ien=settings.aoData.length ; i<ien ; i++ ) {
@@ -4580,10 +4604,6 @@
if ( column.bSearchable ) { if ( column.bSearchable ) {
cellData = _fnGetCellData( settings, i, j, 'filter' ); 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 // Search in DataTables 1.10 is string based. In 1.11 this
// should be altered to also allow strict type checking. // should be altered to also allow strict type checking.
if ( cellData === null ) { if ( cellData === null ) {
@@ -6374,6 +6394,10 @@
*/ */
function _fnSaveState ( settings ) function _fnSaveState ( settings )
{ {
if (settings._bLoadingState) {
return;
}
/* Store the interesting variables */ /* Store the interesting variables */
var state = { var state = {
time: +new Date(), time: +new Date(),
@@ -6408,10 +6432,36 @@
*/ */
function _fnLoadState ( settings, oInit, callback ) function _fnLoadState ( settings, oInit, 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 ) {
_fnImplementState( settings, state, callback );
}
// otherwise, wait for the loaded callback to be executed
return true;
}
function _fnImplementState ( settings, s, callback) {
var i, ien; var i, ien;
var columns = settings.aoColumns; var columns = settings.aoColumns;
var loaded = function ( s ) { 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 ) { if ( ! s || ! s.time ) {
settings._bLoadingState = false;
callback(); callback();
return; return;
} }
@@ -6420,6 +6470,7 @@
// cancelling of loading by returning false // cancelling of loading by returning false
var abStateLoad = _fnCallbackFire( settings, 'aoStateLoadParams', 'stateLoadParams', [settings, s] ); var abStateLoad = _fnCallbackFire( settings, 'aoStateLoadParams', 'stateLoadParams', [settings, s] );
if ( $.inArray( false, abStateLoad ) !== -1 ) { if ( $.inArray( false, abStateLoad ) !== -1 ) {
settings._bLoadingState = false;
callback(); callback();
return; return;
} }
@@ -6427,12 +6478,14 @@
// Reject old data // Reject old data
var duration = settings.iStateDuration; var duration = settings.iStateDuration;
if ( duration > 0 && s.time < +new Date() - (duration*1000) ) { if ( duration > 0 && s.time < +new Date() - (duration*1000) ) {
settings._bLoadingState = false;
callback(); callback();
return; return;
} }
// Number of columns have changed - all bets are off, no restore of settings // Number of columns have changed - all bets are off, no restore of settings
if ( s.columns && columns.length !== s.columns.length ) { if ( s.columns && columns.length !== s.columns.length ) {
settings._bLoadingState = false;
callback(); callback();
return; return;
} }
@@ -6444,8 +6497,10 @@
// subscribed events // subscribed events
if ( s.start !== undefined ) { if ( s.start !== undefined ) {
settings._iDisplayStart = s.start; settings._iDisplayStart = s.start;
if(api === null) {
settings.iInitDisplayStart = s.start; settings.iInitDisplayStart = s.start;
} }
}
if ( s.length !== undefined ) { if ( s.length !== undefined ) {
settings._iDisplayLength = s.length; settings._iDisplayLength = s.length;
} }
@@ -6467,40 +6522,39 @@
} }
// Columns // Columns
//
if ( s.columns ) { if ( s.columns ) {
for ( i=0, ien=s.columns.length ; i<ien ; i++ ) { for ( i=0, ien=s.columns.length ; i<ien ; i++ ) {
var col = s.columns[i]; var col = s.columns[i];
// Visibility // Visibility
if ( col.visible !== undefined ) { 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; columns[i].bVisible = col.visible;
} }
}
// Search // Search
if ( col.search !== undefined ) { if ( col.search !== undefined ) {
$.extend( settings.aoPreSearchCols[i], _fnSearchToHung( col.search ) ); $.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] ); _fnCallbackFire( settings, 'aoStateLoaded', 'stateLoaded', [settings, s] );
callback(); callback();
}; };
if ( ! settings.oFeatures.bStateSave ) {
callback();
return;
}
var state = settings.fnStateLoadCallback.call( settings.oInstance, settings, loaded );
if ( state !== undefined ) {
loaded( state );
}
// otherwise, wait for the loaded callback to be executed
}
/** /**
* Return the settings object for a particular table * Return the settings object for a particular table
@@ -9590,7 +9644,7 @@
* @type string * @type string
* @default Version number * @default Version number
*/ */
DataTable.version = "1.11.2"; DataTable.version = "1.11.3";
/** /**
* Private data store, containing all of the settings objects that are * Private data store, containing all of the settings objects that are
@@ -14015,7 +14069,7 @@
* *
* @type string * @type string
*/ */
build:"bs5/dt-1.11.2", build:"bs5/dt-1.11.3",
/** /**
@@ -15048,6 +15102,10 @@
*/ */
var __htmlEscapeEntities = function ( d ) { var __htmlEscapeEntities = function ( d ) {
if (Array.isArray(d)) {
d = d.join(',');
}
return typeof d === 'string' ? return typeof d === 'string' ?
d d
.replace(/&/g, '&amp;') .replace(/&/g, '&amp;')
@@ -15242,6 +15300,7 @@
_fnSortData: _fnSortData, _fnSortData: _fnSortData,
_fnSaveState: _fnSaveState, _fnSaveState: _fnSaveState,
_fnLoadState: _fnLoadState, _fnLoadState: _fnLoadState,
_fnImplementState: _fnImplementState,
_fnSettingsFromNode: _fnSettingsFromNode, _fnSettingsFromNode: _fnSettingsFromNode,
_fnLog: _fnLog, _fnLog: _fnLog,
_fnMap: _fnMap, _fnMap: _fnMap,

View File

@@ -150,7 +150,7 @@
<dt class="col-sm-5">Domain configuration <dt class="col-sm-5">Domain configuration
<span class="badge bg-success d-none" id="domain-success" title="The domain variable matches the browser location and seems to be configured correctly.">Match</span> <span class="badge bg-success d-none" 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-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> <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> </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 res
} }
pub fn get_display_size(size: i32) -> String {
const UNITS: [&str; 6] = ["bytes", "KB", "MB", "GB", "TB", "PB"]; const UNITS: [&str; 6] = ["bytes", "KB", "MB", "GB", "TB", "PB"];
pub fn get_display_size(size: i32) -> String {
let mut size: f64 = size.into(); let mut size: f64 = size.into();
let mut unit_counter = 0; let mut unit_counter = 0;
@@ -359,10 +359,10 @@ where
try_parse_string(get_env_str_value(key)) try_parse_string(get_env_str_value(key))
} }
pub fn get_env_bool(key: &str) -> Option<bool> {
const TRUE_VALUES: &[&str] = &["true", "t", "yes", "y", "1"]; const TRUE_VALUES: &[&str] = &["true", "t", "yes", "y", "1"];
const FALSE_VALUES: &[&str] = &["false", "f", "no", "n", "0"]; const FALSE_VALUES: &[&str] = &["false", "f", "no", "n", "0"];
pub fn get_env_bool(key: &str) -> Option<bool> {
match get_env_str_value(key) { match get_env_str_value(key) {
Some(val) if TRUE_VALUES.contains(&val.to_lowercase().as_ref()) => Some(true), 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), 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::{DateTime, Local, NaiveDateTime, TimeZone};
use chrono_tz::Tz;
/// Formats a UTC-offset `NaiveDateTime` in the format used by Bitwarden API /// Formats a UTC-offset `NaiveDateTime` in the format used by Bitwarden API
/// responses with "date" fields (`CreationDate`, `RevisionDate`, etc.). /// 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 // Try parsing the `TZ` environment variable to enable formatting `%Z` as
// a time zone abbreviation. // a time zone abbreviation.
if let Ok(tz) = env::var("TZ") { 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(); 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>; pub type JsonMap = serde_json::Map<String, Value>;
#[derive(PartialEq, Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct UpCase<T: DeserializeOwned> { pub struct UpCase<T: DeserializeOwned> {
#[serde(deserialize_with = "upcase_deserialize")] #[serde(deserialize_with = "upcase_deserialize")]
#[serde(flatten)] #[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 { fn _process_key(key: &str) -> String {
match key.to_lowercase().as_ref() { match key.to_lowercase().as_ref() {
"ssn" => "SSN".into(), "ssn" => "SSN".into(),