mirror of
https://github.com/dani-garcia/vaultwarden.wiki.git
synced 2026-07-28 12:05:05 +03:00
Compare commits
7
Commits
1.36.0
...
22f5e0496c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22f5e0496c | ||
|
|
70f9dfbe8b | ||
|
|
54895ad4be | ||
|
|
a057c7deae | ||
|
|
2f85b62d2f | ||
|
|
9bc14e6e77 | ||
|
|
cdf711bb30 |
+5
-4
@@ -50,10 +50,11 @@
|
|||||||
#########################
|
#########################
|
||||||
|
|
||||||
## Database URL
|
## Database URL
|
||||||
## When using SQLite, this is the path to the DB file, and it defaults to
|
## When using SQLite, this should use the sqlite:// scheme followed by the path
|
||||||
## %DATA_FOLDER%/db.sqlite3. If DATA_FOLDER is set to an external location, this
|
## to the DB file. It defaults to sqlite://%DATA_FOLDER%/db.sqlite3.
|
||||||
## must be set to a local sqlite3 file path.
|
## Bare paths without the sqlite:// scheme are supported for backwards compatibility,
|
||||||
# DATABASE_URL=data/db.sqlite3
|
## but only if the database file already exists.
|
||||||
|
# DATABASE_URL=sqlite://data/db.sqlite3
|
||||||
## When using MySQL, specify an appropriate connection URI.
|
## When using MySQL, specify an appropriate connection URI.
|
||||||
## Details: https://docs.diesel.rs/2.1.x/diesel/mysql/struct.MysqlConnection.html
|
## Details: https://docs.diesel.rs/2.1.x/diesel/mysql/struct.MysqlConnection.html
|
||||||
# DATABASE_URL=mysql://user:password@host[:port]/database_name
|
# DATABASE_URL=mysql://user:password@host[:port]/database_name
|
||||||
|
|||||||
@@ -50,6 +50,6 @@ jobs:
|
|||||||
severity: CRITICAL,HIGH
|
severity: CRITICAL,HIGH
|
||||||
|
|
||||||
- name: Upload Trivy scan results to GitHub Security tab
|
- name: Upload Trivy scan results to GitHub Security tab
|
||||||
uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
|
uses: github/codeql-action/upload-sarif@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
|
||||||
with:
|
with:
|
||||||
sarif_file: 'trivy-results.sarif'
|
sarif_file: 'trivy-results.sarif'
|
||||||
|
|||||||
@@ -23,4 +23,4 @@ jobs:
|
|||||||
|
|
||||||
# When this version is updated, do not forget to update this in `.pre-commit-config.yaml` too
|
# When this version is updated, do not forget to update this in `.pre-commit-config.yaml` too
|
||||||
- name: Spell Check Repo
|
- name: Spell Check Repo
|
||||||
uses: crate-ci/typos@7c572958218557a3272c2d6719629443b5cc26fd # v1.45.2
|
uses: crate-ci/typos@5374cbf686e897b15713110e233094e2874de7ef # v1.46.1
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Run zizmor
|
- name: Run zizmor
|
||||||
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
|
uses: zizmorcore/zizmor-action@b572f7b1a1c2d41efaab43d504f68d215c3cd727 # v0.5.4
|
||||||
with:
|
with:
|
||||||
# intentionally not scanning the entire repository,
|
# intentionally not scanning the entire repository,
|
||||||
# since it contains integration tests.
|
# since it contains integration tests.
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ repos:
|
|||||||
|
|
||||||
# When this version is updated, do not forget to update this in `.github/workflows/typos.yaml` too
|
# When this version is updated, do not forget to update this in `.github/workflows/typos.yaml` too
|
||||||
- repo: https://github.com/crate-ci/typos
|
- repo: https://github.com/crate-ci/typos
|
||||||
rev: 7c572958218557a3272c2d6719629443b5cc26fd # v1.45.2
|
rev: 5374cbf686e897b15713110e233094e2874de7ef # v1.46.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: typos
|
- id: typos
|
||||||
|
|
||||||
|
|||||||
Generated
+239
-395
File diff suppressed because it is too large
Load Diff
+13
-12
@@ -39,7 +39,7 @@ vendored_openssl = ["openssl/vendored"]
|
|||||||
# Enable MiMalloc memory allocator to replace the default malloc
|
# Enable MiMalloc memory allocator to replace the default malloc
|
||||||
# This can improve performance for Alpine builds
|
# This can improve performance for Alpine builds
|
||||||
enable_mimalloc = ["dep:mimalloc"]
|
enable_mimalloc = ["dep:mimalloc"]
|
||||||
s3 = ["opendal/services-s3", "dep:aws-config", "dep:aws-credential-types", "dep:aws-smithy-runtime-api", "dep:anyhow", "dep:http", "dep:reqsign"]
|
s3 = ["opendal/services-s3", "dep:aws-config", "dep:aws-credential-types", "dep:aws-smithy-runtime-api", "dep:http", "dep:reqsign-aws-v4", "dep:reqsign-core"]
|
||||||
|
|
||||||
# OIDC specific features
|
# OIDC specific features
|
||||||
oidc-accept-rfc3339-timestamps = ["openidconnect/accept-rfc3339-timestamps"]
|
oidc-accept-rfc3339-timestamps = ["openidconnect/accept-rfc3339-timestamps"]
|
||||||
@@ -81,7 +81,7 @@ dashmap = "6.1.0"
|
|||||||
|
|
||||||
# Async futures
|
# Async futures
|
||||||
futures = "0.3.32"
|
futures = "0.3.32"
|
||||||
tokio = { version = "1.52.1", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] }
|
tokio = { version = "1.52.3", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] }
|
||||||
tokio-util = { version = "0.7.18", features = ["compat"]}
|
tokio-util = { version = "0.7.18", features = ["compat"]}
|
||||||
|
|
||||||
# A generic serialization/deserialization framework
|
# A generic serialization/deserialization framework
|
||||||
@@ -102,6 +102,7 @@ libsqlite3-sys = { version = "0.37.0", optional = true }
|
|||||||
# Crypto-related libraries
|
# Crypto-related libraries
|
||||||
rand = "0.10.1"
|
rand = "0.10.1"
|
||||||
ring = "0.17.14"
|
ring = "0.17.14"
|
||||||
|
rustls = { version = "0.23.40", features = ["ring", "std"], default-features = false }
|
||||||
subtle = "2.6.1"
|
subtle = "2.6.1"
|
||||||
|
|
||||||
# UUID generation
|
# UUID generation
|
||||||
@@ -119,13 +120,13 @@ job_scheduler_ng = "2.4.0"
|
|||||||
data-encoding = "2.11.0"
|
data-encoding = "2.11.0"
|
||||||
|
|
||||||
# JWT library
|
# JWT library
|
||||||
jsonwebtoken = { version = "10.3.0", features = ["use_pem", "rust_crypto"], default-features = false }
|
jsonwebtoken = { version = "10.4.0", features = ["use_pem", "rust_crypto"], default-features = false }
|
||||||
|
|
||||||
# TOTP library
|
# TOTP library
|
||||||
totp-lite = "2.0.1"
|
totp-lite = "2.0.1"
|
||||||
|
|
||||||
# Yubico Library
|
# Yubico Library
|
||||||
yubico = { package = "yubico_ng", version = "0.14.1", features = ["online-tokio"], default-features = false }
|
yubico = { package = "yubico_ng", version = "0.15.0", features = ["online-tokio"], default-features = false }
|
||||||
|
|
||||||
# WebAuthn libraries
|
# WebAuthn libraries
|
||||||
# danger-allow-state-serialisation is needed to save the state in the db
|
# danger-allow-state-serialisation is needed to save the state in the db
|
||||||
@@ -138,7 +139,7 @@ webauthn-rs-core = "0.5.5"
|
|||||||
url = "2.5.8"
|
url = "2.5.8"
|
||||||
|
|
||||||
# Email libraries
|
# Email libraries
|
||||||
lettre = { version = "0.11.21", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "hostname", "tracing", "tokio1-rustls", "ring", "rustls-native-certs"], default-features = false }
|
lettre = { version = "0.11.22", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "hostname", "tracing", "tokio1-rustls", "ring", "rustls-native-certs"], default-features = false }
|
||||||
percent-encoding = "2.3.2" # URL encoding library used for URL's in the emails
|
percent-encoding = "2.3.2" # URL encoding library used for URL's in the emails
|
||||||
email_address = "0.2.9"
|
email_address = "0.2.9"
|
||||||
|
|
||||||
@@ -146,7 +147,7 @@ email_address = "0.2.9"
|
|||||||
handlebars = { version = "6.4.0", features = ["dir_source"] }
|
handlebars = { version = "6.4.0", features = ["dir_source"] }
|
||||||
|
|
||||||
# HTTP client (Used for favicons, version check, DUO and HIBP API)
|
# HTTP client (Used for favicons, version check, DUO and HIBP API)
|
||||||
reqwest = { version = "0.12.28", features = ["rustls-tls", "rustls-tls-native-roots", "stream", "json", "deflate", "gzip", "brotli", "zstd", "socks", "cookies", "charset", "http2", "system-proxy"], default-features = false}
|
reqwest = { version = "0.13.3", features = ["rustls-no-provider", "stream", "json", "form", "deflate", "gzip", "brotli", "zstd", "socks", "cookies", "charset", "http2", "system-proxy"], default-features = false}
|
||||||
hickory-resolver = "0.26.1"
|
hickory-resolver = "0.26.1"
|
||||||
|
|
||||||
# Favicon extraction libraries
|
# Favicon extraction libraries
|
||||||
@@ -164,7 +165,7 @@ cookie = "0.18.1"
|
|||||||
cookie_store = "0.22.1"
|
cookie_store = "0.22.1"
|
||||||
|
|
||||||
# Used by U2F, JWT and PostgreSQL
|
# Used by U2F, JWT and PostgreSQL
|
||||||
openssl = "0.10.78"
|
openssl = "0.10.79"
|
||||||
|
|
||||||
# CLI argument parsing
|
# CLI argument parsing
|
||||||
pico-args = "0.5.0"
|
pico-args = "0.5.0"
|
||||||
@@ -174,7 +175,7 @@ pastey = "0.2.2"
|
|||||||
governor = "0.10.4"
|
governor = "0.10.4"
|
||||||
|
|
||||||
# OIDC for SSO
|
# OIDC for SSO
|
||||||
openidconnect = { version = "4.0.1", features = ["reqwest", "rustls-tls"] }
|
openidconnect = { version = "4.0.1", default-features = false }
|
||||||
moka = { version = "0.12.15", features = ["future"] }
|
moka = { version = "0.12.15", features = ["future"] }
|
||||||
|
|
||||||
# Check client versions for specific features.
|
# Check client versions for specific features.
|
||||||
@@ -190,21 +191,21 @@ which = "8.0.2"
|
|||||||
argon2 = "0.5.3"
|
argon2 = "0.5.3"
|
||||||
|
|
||||||
# Reading a password from the cli for generating the Argon2id ADMIN_TOKEN
|
# Reading a password from the cli for generating the Argon2id ADMIN_TOKEN
|
||||||
rpassword = "7.5.1"
|
rpassword = "7.5.2"
|
||||||
|
|
||||||
# Loading a dynamic CSS Stylesheet
|
# Loading a dynamic CSS Stylesheet
|
||||||
grass_compiler = { version = "0.13.4", default-features = false }
|
grass_compiler = { version = "0.13.4", default-features = false }
|
||||||
|
|
||||||
# File are accessed through Apache OpenDAL
|
# File are accessed through Apache OpenDAL
|
||||||
opendal = { version = "0.55.0", features = ["services-fs"], default-features = false }
|
opendal = { version = "0.56.0", features = ["services-fs"], default-features = false }
|
||||||
|
|
||||||
# For retrieving AWS credentials, including temporary SSO credentials
|
# For retrieving AWS credentials, including temporary SSO credentials
|
||||||
anyhow = { version = "1.0.102", optional = true }
|
|
||||||
aws-config = { version = "1.8.16", features = ["behavior-version-latest", "rt-tokio", "credentials-process", "sso"], default-features = false, optional = true }
|
aws-config = { version = "1.8.16", features = ["behavior-version-latest", "rt-tokio", "credentials-process", "sso"], default-features = false, optional = true }
|
||||||
aws-credential-types = { version = "1.2.14", optional = true }
|
aws-credential-types = { version = "1.2.14", optional = true }
|
||||||
aws-smithy-runtime-api = { version = "1.12.0", optional = true }
|
aws-smithy-runtime-api = { version = "1.12.0", optional = true }
|
||||||
http = { version = "1.4.0", optional = true }
|
http = { version = "1.4.0", optional = true }
|
||||||
reqsign = { version = "0.16.5", optional = true }
|
reqsign-aws-v4 = { version = "3.0.0", optional = true }
|
||||||
|
reqsign-core = { version = "3.0.0", optional = true }
|
||||||
|
|
||||||
# Strip debuginfo from the release builds
|
# Strip debuginfo from the release builds
|
||||||
# The debug symbols are to provide better panic traces
|
# The debug symbols are to provide better panic traces
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
|||||||
# Debian Trixie uses libpq v17
|
# Debian Trixie uses libpq v17
|
||||||
PQ_LIB_DIR="/usr/local/musl/pq17/lib"
|
PQ_LIB_DIR="/usr/local/musl/pq17/lib"
|
||||||
|
|
||||||
|
|
||||||
# Create CARGO_HOME folder and don't download rust docs
|
# Create CARGO_HOME folder and don't download rust docs
|
||||||
RUN mkdir -pv "${CARGO_HOME}" && \
|
RUN mkdir -pv "${CARGO_HOME}" && \
|
||||||
rustup set profile minimal
|
rustup set profile minimal
|
||||||
|
|||||||
+11
-33
@@ -51,7 +51,7 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
|||||||
TERM=xterm-256color \
|
TERM=xterm-256color \
|
||||||
CARGO_HOME="/root/.cargo" \
|
CARGO_HOME="/root/.cargo" \
|
||||||
USER="root"
|
USER="root"
|
||||||
# Install clang to get `xx-cargo` working
|
# Install clang && xx-c-essentials to get `xx-cargo` working
|
||||||
# Install pkg-config to allow amd64 builds to find all libraries
|
# Install pkg-config to allow amd64 builds to find all libraries
|
||||||
# Install git so build.rs can determine the correct version
|
# Install git so build.rs can determine the correct version
|
||||||
# Install the libc cross packages based upon the debian-arch
|
# Install the libc cross packages based upon the debian-arch
|
||||||
@@ -59,19 +59,16 @@ RUN apt-get update && \
|
|||||||
apt-get install -y \
|
apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
clang \
|
clang \
|
||||||
pkg-config \
|
git && \
|
||||||
git \
|
|
||||||
"libc6-$(xx-info debian-arch)-cross" \
|
|
||||||
"libc6-dev-$(xx-info debian-arch)-cross" \
|
|
||||||
"linux-libc-dev-$(xx-info debian-arch)-cross" && \
|
|
||||||
xx-apt-get install -y \
|
xx-apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
gcc \
|
|
||||||
libpq-dev \
|
libpq-dev \
|
||||||
libpq5 \
|
libpq5 \
|
||||||
libssl-dev \
|
libssl-dev \
|
||||||
libmariadb-dev \
|
libmariadb-dev \
|
||||||
zlib1g-dev && \
|
pkg-config \
|
||||||
|
zlib1g-dev \
|
||||||
|
xx-c-essentials && \
|
||||||
# Run xx-cargo early, since it sometimes seems to break when run at a later stage
|
# Run xx-cargo early, since it sometimes seems to break when run at a later stage
|
||||||
echo "export CARGO_TARGET=$(xx-cargo --print-target-triple)" >> /env-cargo
|
echo "export CARGO_TARGET=$(xx-cargo --print-target-triple)" >> /env-cargo
|
||||||
|
|
||||||
@@ -83,29 +80,6 @@ RUN mkdir -pv "${CARGO_HOME}" && \
|
|||||||
RUN USER=root cargo new --bin /app
|
RUN USER=root cargo new --bin /app
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Environment variables for Cargo on Debian based builds
|
|
||||||
ARG TARGET_PKG_CONFIG_PATH
|
|
||||||
|
|
||||||
RUN source /env-cargo && \
|
|
||||||
if xx-info is-cross ; then \
|
|
||||||
# We can't use xx-cargo since that uses clang, which doesn't work for our libraries.
|
|
||||||
# Because of this we generate the needed environment variables here which we can load in the needed steps.
|
|
||||||
echo "export CC_$(echo "${CARGO_TARGET}" | tr '[:upper:]' '[:lower:]' | tr - _)=/usr/bin/$(xx-info)-gcc" >> /env-cargo && \
|
|
||||||
echo "export CARGO_TARGET_$(echo "${CARGO_TARGET}" | tr '[:lower:]' '[:upper:]' | tr - _)_LINKER=/usr/bin/$(xx-info)-gcc" >> /env-cargo && \
|
|
||||||
echo "export CROSS_COMPILE=1" >> /env-cargo && \
|
|
||||||
echo "export PKG_CONFIG_ALLOW_CROSS=1" >> /env-cargo && \
|
|
||||||
# For some architectures `xx-info` returns a triple which doesn't matches the path on disk
|
|
||||||
# In those cases you can override this by setting the `TARGET_PKG_CONFIG_PATH` build-arg
|
|
||||||
if [[ -n "${TARGET_PKG_CONFIG_PATH}" ]]; then \
|
|
||||||
echo "export TARGET_PKG_CONFIG_PATH=${TARGET_PKG_CONFIG_PATH}" >> /env-cargo ; \
|
|
||||||
else \
|
|
||||||
echo "export PKG_CONFIG_PATH=/usr/lib/$(xx-info)/pkgconfig" >> /env-cargo ; \
|
|
||||||
fi && \
|
|
||||||
echo "# End of env-cargo" >> /env-cargo ; \
|
|
||||||
fi && \
|
|
||||||
# Output the current contents of the file
|
|
||||||
cat /env-cargo
|
|
||||||
|
|
||||||
RUN source /env-cargo && \
|
RUN source /env-cargo && \
|
||||||
rustup target add "${CARGO_TARGET}"
|
rustup target add "${CARGO_TARGET}"
|
||||||
|
|
||||||
@@ -122,7 +96,9 @@ ARG DB=sqlite,mysql,postgresql
|
|||||||
# dummy project, except the target folder
|
# dummy project, except the target folder
|
||||||
# This folder contains the compiled dependencies
|
# This folder contains the compiled dependencies
|
||||||
RUN source /env-cargo && \
|
RUN source /env-cargo && \
|
||||||
cargo build --features ${DB} --profile "${CARGO_PROFILE}" --target="${CARGO_TARGET}" && \
|
# Workaround for xx related build issues
|
||||||
|
# https://github.com/tonistiigi/xx/pull/108#issuecomment-3700635977
|
||||||
|
PKG_CONFIG="$(command -v "$(xx-info)-pkg-config")" xx-cargo build --features ${DB} --profile "${CARGO_PROFILE}" && \
|
||||||
find . -not -path "./target*" -delete
|
find . -not -path "./target*" -delete
|
||||||
|
|
||||||
# Copies the complete project
|
# Copies the complete project
|
||||||
@@ -137,7 +113,9 @@ RUN source /env-cargo && \
|
|||||||
# Also do this for build.rs to ensure the version is rechecked
|
# Also do this for build.rs to ensure the version is rechecked
|
||||||
touch build.rs src/main.rs && \
|
touch build.rs src/main.rs && \
|
||||||
# Create a symlink to the binary target folder to easy copy the binary in the final stage
|
# Create a symlink to the binary target folder to easy copy the binary in the final stage
|
||||||
cargo build --features ${DB} --profile "${CARGO_PROFILE}" --target="${CARGO_TARGET}" && \
|
# Workaround for xx related build issues
|
||||||
|
# https://github.com/tonistiigi/xx/pull/108#issuecomment-3700635977
|
||||||
|
PKG_CONFIG="$(command -v "$(xx-info)-pkg-config")" xx-cargo build --features ${DB} --profile "${CARGO_PROFILE}" && \
|
||||||
if [[ "${CARGO_PROFILE}" == "dev" ]] ; then \
|
if [[ "${CARGO_PROFILE}" == "dev" ]] ; then \
|
||||||
ln -vfsr "/app/target/${CARGO_TARGET}/debug" /app/target/final ; \
|
ln -vfsr "/app/target/${CARGO_TARGET}/debug" /app/target/final ; \
|
||||||
else \
|
else \
|
||||||
|
|||||||
+20
-34
@@ -27,6 +27,11 @@
|
|||||||
# $ docker image inspect --format "{{ '{{' }}.RepoTags}}" docker.io/vaultwarden/web-vault@{{ vault_image_digest }}
|
# $ docker image inspect --format "{{ '{{' }}.RepoTags}}" docker.io/vaultwarden/web-vault@{{ vault_image_digest }}
|
||||||
# [docker.io/vaultwarden/web-vault:{{ vault_version | replace('+', '_') }}]
|
# [docker.io/vaultwarden/web-vault:{{ vault_version | replace('+', '_') }}]
|
||||||
#
|
#
|
||||||
|
{% macro xx_cargo_config() -%}
|
||||||
|
# Workaround for xx related build issues
|
||||||
|
# https://github.com/tonistiigi/xx/pull/108#issuecomment-3700635977
|
||||||
|
PKG_CONFIG="$(command -v "$(xx-info)-pkg-config")" xx-cargo build --features ${DB} --profile "${CARGO_PROFILE}"
|
||||||
|
{%- endmacro %}
|
||||||
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@{{ vault_image_digest }} AS vault
|
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@{{ vault_image_digest }} AS vault
|
||||||
|
|
||||||
{% if base == "debian" %}
|
{% if base == "debian" %}
|
||||||
@@ -66,10 +71,10 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
|||||||
# Use PostgreSQL v17 during Alpine/MUSL builds instead of the default v16
|
# Use PostgreSQL v17 during Alpine/MUSL builds instead of the default v16
|
||||||
# Debian Trixie uses libpq v17
|
# Debian Trixie uses libpq v17
|
||||||
PQ_LIB_DIR="/usr/local/musl/pq17/lib"
|
PQ_LIB_DIR="/usr/local/musl/pq17/lib"
|
||||||
{% endif %}
|
{%- endif %}
|
||||||
|
|
||||||
{% if base == "debian" %}
|
{% if base == "debian" %}
|
||||||
# Install clang to get `xx-cargo` working
|
# Install clang && xx-c-essentials to get `xx-cargo` working
|
||||||
# Install pkg-config to allow amd64 builds to find all libraries
|
# Install pkg-config to allow amd64 builds to find all libraries
|
||||||
# Install git so build.rs can determine the correct version
|
# Install git so build.rs can determine the correct version
|
||||||
# Install the libc cross packages based upon the debian-arch
|
# Install the libc cross packages based upon the debian-arch
|
||||||
@@ -77,19 +82,16 @@ RUN apt-get update && \
|
|||||||
apt-get install -y \
|
apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
clang \
|
clang \
|
||||||
pkg-config \
|
git && \
|
||||||
git \
|
|
||||||
"libc6-$(xx-info debian-arch)-cross" \
|
|
||||||
"libc6-dev-$(xx-info debian-arch)-cross" \
|
|
||||||
"linux-libc-dev-$(xx-info debian-arch)-cross" && \
|
|
||||||
xx-apt-get install -y \
|
xx-apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
gcc \
|
|
||||||
libpq-dev \
|
libpq-dev \
|
||||||
libpq5 \
|
libpq5 \
|
||||||
libssl-dev \
|
libssl-dev \
|
||||||
libmariadb-dev \
|
libmariadb-dev \
|
||||||
zlib1g-dev && \
|
pkg-config \
|
||||||
|
zlib1g-dev \
|
||||||
|
xx-c-essentials && \
|
||||||
# Run xx-cargo early, since it sometimes seems to break when run at a later stage
|
# Run xx-cargo early, since it sometimes seems to break when run at a later stage
|
||||||
echo "export CARGO_TARGET=$(xx-cargo --print-target-triple)" >> /env-cargo
|
echo "export CARGO_TARGET=$(xx-cargo --print-target-triple)" >> /env-cargo
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -102,31 +104,7 @@ RUN mkdir -pv "${CARGO_HOME}" && \
|
|||||||
RUN USER=root cargo new --bin /app
|
RUN USER=root cargo new --bin /app
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
{% if base == "debian" %}
|
{% if base == "alpine" %}
|
||||||
# Environment variables for Cargo on Debian based builds
|
|
||||||
ARG TARGET_PKG_CONFIG_PATH
|
|
||||||
|
|
||||||
RUN source /env-cargo && \
|
|
||||||
if xx-info is-cross ; then \
|
|
||||||
# We can't use xx-cargo since that uses clang, which doesn't work for our libraries.
|
|
||||||
# Because of this we generate the needed environment variables here which we can load in the needed steps.
|
|
||||||
echo "export CC_$(echo "${CARGO_TARGET}" | tr '[:upper:]' '[:lower:]' | tr - _)=/usr/bin/$(xx-info)-gcc" >> /env-cargo && \
|
|
||||||
echo "export CARGO_TARGET_$(echo "${CARGO_TARGET}" | tr '[:lower:]' '[:upper:]' | tr - _)_LINKER=/usr/bin/$(xx-info)-gcc" >> /env-cargo && \
|
|
||||||
echo "export CROSS_COMPILE=1" >> /env-cargo && \
|
|
||||||
echo "export PKG_CONFIG_ALLOW_CROSS=1" >> /env-cargo && \
|
|
||||||
# For some architectures `xx-info` returns a triple which doesn't matches the path on disk
|
|
||||||
# In those cases you can override this by setting the `TARGET_PKG_CONFIG_PATH` build-arg
|
|
||||||
if [[ -n "${TARGET_PKG_CONFIG_PATH}" ]]; then \
|
|
||||||
echo "export TARGET_PKG_CONFIG_PATH=${TARGET_PKG_CONFIG_PATH}" >> /env-cargo ; \
|
|
||||||
else \
|
|
||||||
echo "export PKG_CONFIG_PATH=/usr/lib/$(xx-info)/pkgconfig" >> /env-cargo ; \
|
|
||||||
fi && \
|
|
||||||
echo "# End of env-cargo" >> /env-cargo ; \
|
|
||||||
fi && \
|
|
||||||
# Output the current contents of the file
|
|
||||||
cat /env-cargo
|
|
||||||
|
|
||||||
{% elif base == "alpine" %}
|
|
||||||
# Environment variables for Cargo on Alpine based builds
|
# Environment variables for Cargo on Alpine based builds
|
||||||
RUN echo "export CARGO_TARGET=${RUST_MUSL_CROSS_TARGET}" >> /env-cargo && \
|
RUN echo "export CARGO_TARGET=${RUST_MUSL_CROSS_TARGET}" >> /env-cargo && \
|
||||||
# Output the current contents of the file
|
# Output the current contents of the file
|
||||||
@@ -154,7 +132,11 @@ ARG DB=sqlite,mysql,postgresql,enable_mimalloc
|
|||||||
# dummy project, except the target folder
|
# dummy project, except the target folder
|
||||||
# This folder contains the compiled dependencies
|
# This folder contains the compiled dependencies
|
||||||
RUN source /env-cargo && \
|
RUN source /env-cargo && \
|
||||||
|
{% if base == "debian" %}
|
||||||
|
{{ xx_cargo_config() }} && \
|
||||||
|
{% elif base == "alpine" %}
|
||||||
cargo build --features ${DB} --profile "${CARGO_PROFILE}" --target="${CARGO_TARGET}" && \
|
cargo build --features ${DB} --profile "${CARGO_PROFILE}" --target="${CARGO_TARGET}" && \
|
||||||
|
{% endif %}
|
||||||
find . -not -path "./target*" -delete
|
find . -not -path "./target*" -delete
|
||||||
|
|
||||||
# Copies the complete project
|
# Copies the complete project
|
||||||
@@ -169,7 +151,11 @@ RUN source /env-cargo && \
|
|||||||
# Also do this for build.rs to ensure the version is rechecked
|
# Also do this for build.rs to ensure the version is rechecked
|
||||||
touch build.rs src/main.rs && \
|
touch build.rs src/main.rs && \
|
||||||
# Create a symlink to the binary target folder to easy copy the binary in the final stage
|
# Create a symlink to the binary target folder to easy copy the binary in the final stage
|
||||||
|
{% if base == "debian" %}
|
||||||
|
{{ xx_cargo_config() }} && \
|
||||||
|
{% elif base == "alpine" %}
|
||||||
cargo build --features ${DB} --profile "${CARGO_PROFILE}" --target="${CARGO_TARGET}" && \
|
cargo build --features ${DB} --profile "${CARGO_PROFILE}" --target="${CARGO_TARGET}" && \
|
||||||
|
{% endif %}
|
||||||
if [[ "${CARGO_PROFILE}" == "dev" ]] ; then \
|
if [[ "${CARGO_PROFILE}" == "dev" ]] ; then \
|
||||||
ln -vfsr "/app/target/${CARGO_TARGET}/debug" /app/target/final ; \
|
ln -vfsr "/app/target/${CARGO_TARGET}/debug" /app/target/final ; \
|
||||||
else \
|
else \
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE sso_auth DROP COLUMN code_response_error;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE sso_auth ADD COLUMN code_response_error TEXT;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE sso_auth DROP COLUMN IF EXISTS code_response_error;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE sso_auth ADD COLUMN IF NOT EXISTS code_response_error TEXT;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE sso_auth DROP COLUMN code_response_error;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE sso_auth ADD COLUMN code_response_error TEXT;
|
||||||
@@ -78,6 +78,7 @@ pub fn routes() -> Vec<Route> {
|
|||||||
revoke_member,
|
revoke_member,
|
||||||
bulk_revoke_members,
|
bulk_revoke_members,
|
||||||
restore_member,
|
restore_member,
|
||||||
|
restore_member_vnext,
|
||||||
bulk_restore_members,
|
bulk_restore_members,
|
||||||
get_groups,
|
get_groups,
|
||||||
get_groups_details,
|
get_groups_details,
|
||||||
@@ -2315,6 +2316,18 @@ async fn _revoke_member(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[put("/organizations/<org_id>/users/<member_id>/restore/vnext")]
|
||||||
|
async fn restore_member_vnext(
|
||||||
|
org_id: OrganizationId,
|
||||||
|
member_id: MembershipId,
|
||||||
|
headers: AdminHeaders,
|
||||||
|
conn: DbConn,
|
||||||
|
) -> EmptyResult {
|
||||||
|
// Vaultwarden does not (yet) support the per User Collection linked to the `Enforce organization data ownership` policy.
|
||||||
|
// Therefor we ignore the `defaultUserCollectionName` data sent and just call restore_member
|
||||||
|
_restore_member(&org_id, &member_id, &headers, &conn).await
|
||||||
|
}
|
||||||
|
|
||||||
#[put("/organizations/<org_id>/users/<member_id>/restore")]
|
#[put("/organizations/<org_id>/users/<member_id>/restore")]
|
||||||
async fn restore_member(
|
async fn restore_member(
|
||||||
org_id: OrganizationId,
|
org_id: OrganizationId,
|
||||||
|
|||||||
@@ -568,7 +568,7 @@ async fn post_access_file(
|
|||||||
async fn download_url(host: &Host, send_id: &SendId, file_id: &SendFileId) -> Result<String, crate::Error> {
|
async fn download_url(host: &Host, send_id: &SendId, file_id: &SendFileId) -> Result<String, crate::Error> {
|
||||||
let operator = CONFIG.opendal_operator_for_path_type(&PathType::Sends)?;
|
let operator = CONFIG.opendal_operator_for_path_type(&PathType::Sends)?;
|
||||||
|
|
||||||
if operator.info().scheme() == <&'static str>::from(opendal::Scheme::Fs) {
|
if crate::storage::is_fs_operator(&operator) {
|
||||||
let token_claims = crate::auth::generate_send_claims(send_id, file_id);
|
let token_claims = crate::auth::generate_send_claims(send_id, file_id);
|
||||||
let token = crate::auth::encode_jwt(&token_claims);
|
let token = crate::auth::encode_jwt(&token_claims);
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ pub fn routes() -> Vec<Route> {
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct SendEmailLoginData {
|
struct SendEmailLoginData {
|
||||||
#[serde(alias = "DeviceIdentifier")]
|
#[serde(alias = "DeviceIdentifier")]
|
||||||
device_identifier: DeviceId,
|
device_identifier: Option<DeviceId>,
|
||||||
#[serde(alias = "Email")]
|
#[serde(alias = "Email")]
|
||||||
email: Option<String>,
|
email: Option<String>,
|
||||||
#[serde(alias = "MasterPasswordHash")]
|
#[serde(alias = "MasterPasswordHash")]
|
||||||
@@ -91,8 +91,11 @@ async fn send_email_login(data: Json<SendEmailLoginData>, client_headers: Client
|
|||||||
|
|
||||||
user
|
user
|
||||||
} else {
|
} else {
|
||||||
|
let Some(device_identifier) = &data.device_identifier else {
|
||||||
|
err!("No device identifier has been submitted.")
|
||||||
|
};
|
||||||
// SSO login only sends device id, so we get the user by the most recently used device
|
// SSO login only sends device id, so we get the user by the most recently used device
|
||||||
let Some(user) = User::find_by_device_for_email2fa(&data.device_identifier, &conn).await else {
|
let Some(user) = User::find_by_device_for_email2fa(device_identifier, &conn).await else {
|
||||||
err!("Username or password is incorrect. Try again.")
|
err!("Username or password is incorrect. Try again.")
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+22
-25
@@ -28,8 +28,9 @@ use crate::{
|
|||||||
crypto,
|
crypto,
|
||||||
db::{
|
db::{
|
||||||
models::{
|
models::{
|
||||||
AuthRequest, AuthRequestId, Device, DeviceId, EventType, Invitation, OIDCCodeWrapper, OrganizationApiKey,
|
AuthRequest, AuthRequestId, Device, DeviceId, EventType, Invitation, OIDCCodeResponseError,
|
||||||
OrganizationId, SsoAuth, SsoUser, TwoFactor, TwoFactorIncomplete, TwoFactorType, User, UserId,
|
OrganizationApiKey, OrganizationId, SsoAuth, SsoUser, TwoFactor, TwoFactorIncomplete, TwoFactorType, User,
|
||||||
|
UserId,
|
||||||
},
|
},
|
||||||
DbConn,
|
DbConn,
|
||||||
},
|
},
|
||||||
@@ -186,7 +187,7 @@ async fn _sso_login(
|
|||||||
// Ratelimit the login
|
// Ratelimit the login
|
||||||
crate::ratelimit::check_limit_login(&ip.ip)?;
|
crate::ratelimit::check_limit_login(&ip.ip)?;
|
||||||
|
|
||||||
let (state, code_verifier) = match (data.code.as_ref(), data.code_verifier.as_ref()) {
|
let (code, code_verifier) = match (data.code.as_ref(), data.code_verifier.as_ref()) {
|
||||||
(None, _) => err!(
|
(None, _) => err!(
|
||||||
"Got no code in OIDC data",
|
"Got no code in OIDC data",
|
||||||
ErrorEvent {
|
ErrorEvent {
|
||||||
@@ -202,7 +203,7 @@ async fn _sso_login(
|
|||||||
(Some(code), Some(code_verifier)) => (code, code_verifier.clone()),
|
(Some(code), Some(code_verifier)) => (code, code_verifier.clone()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let (sso_auth, user_infos) = sso::exchange_code(state, code_verifier, conn).await?;
|
let (sso_auth, user_infos) = sso::exchange_code(code, code_verifier, conn).await?;
|
||||||
let user_with_sso = match SsoUser::find_by_identifier(&user_infos.identifier, conn).await {
|
let user_with_sso = match SsoUser::find_by_identifier(&user_infos.identifier, conn).await {
|
||||||
None => match SsoUser::find_by_mail(&user_infos.email, conn).await {
|
None => match SsoUser::find_by_mail(&user_infos.email, conn).await {
|
||||||
None => None,
|
None => None,
|
||||||
@@ -1138,7 +1139,7 @@ struct ConnectData {
|
|||||||
|
|
||||||
// Needed for authorization code
|
// Needed for authorization code
|
||||||
#[field(name = uncased("code"))]
|
#[field(name = uncased("code"))]
|
||||||
code: Option<OIDCState>,
|
code: Option<OIDCCode>,
|
||||||
#[field(name = uncased("code_verifier"))]
|
#[field(name = uncased("code_verifier"))]
|
||||||
code_verifier: Option<OIDCCodeVerifier>,
|
code_verifier: Option<OIDCCodeVerifier>,
|
||||||
}
|
}
|
||||||
@@ -1165,19 +1166,12 @@ const SSO_BINDING_COOKIE: &str = "VW_SSO_BINDING";
|
|||||||
|
|
||||||
#[get("/connect/oidc-signin?<code>&<state>", rank = 1)]
|
#[get("/connect/oidc-signin?<code>&<state>", rank = 1)]
|
||||||
async fn oidcsignin(code: OIDCCode, state: String, cookies: &CookieJar<'_>, mut conn: DbConn) -> ApiResult<Redirect> {
|
async fn oidcsignin(code: OIDCCode, state: String, cookies: &CookieJar<'_>, mut conn: DbConn) -> ApiResult<Redirect> {
|
||||||
_oidcsignin_redirect(
|
_oidcsignin_redirect(state, code, None, cookies, &mut conn).await
|
||||||
state,
|
|
||||||
OIDCCodeWrapper::Ok {
|
|
||||||
code,
|
|
||||||
},
|
|
||||||
cookies,
|
|
||||||
&mut conn,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bitwarden client appear to only care for code and state so we pipe it through
|
// Bitwarden client appear to only care for code and state
|
||||||
// cf: https://github.com/bitwarden/clients/blob/80b74b3300e15b4ae414dc06044cc9b02b6c10a6/libs/auth/src/angular/sso/sso.component.ts#L141
|
// We save the error in the database and set the encoded state as the code to be able to retrieve them later on
|
||||||
|
// cf: https://github.com/bitwarden/clients/blob/afd36d290ce18fb0048e0575e7d5a8f78b5dbffc/libs/auth/src/angular/sso/sso.component.ts#L156
|
||||||
#[get("/connect/oidc-signin?<state>&<error>&<error_description>", rank = 2)]
|
#[get("/connect/oidc-signin?<state>&<error>&<error_description>", rank = 2)]
|
||||||
async fn oidcsignin_error(
|
async fn oidcsignin_error(
|
||||||
state: String,
|
state: String,
|
||||||
@@ -1187,11 +1181,12 @@ async fn oidcsignin_error(
|
|||||||
mut conn: DbConn,
|
mut conn: DbConn,
|
||||||
) -> ApiResult<Redirect> {
|
) -> ApiResult<Redirect> {
|
||||||
_oidcsignin_redirect(
|
_oidcsignin_redirect(
|
||||||
state,
|
state.clone(),
|
||||||
OIDCCodeWrapper::Error {
|
state.into(),
|
||||||
|
Some(OIDCCodeResponseError {
|
||||||
error,
|
error,
|
||||||
error_description,
|
error_description,
|
||||||
},
|
}),
|
||||||
cookies,
|
cookies,
|
||||||
&mut conn,
|
&mut conn,
|
||||||
)
|
)
|
||||||
@@ -1200,10 +1195,10 @@ async fn oidcsignin_error(
|
|||||||
|
|
||||||
// The state was encoded using Base64 to ensure no issue with providers.
|
// The state was encoded using Base64 to ensure no issue with providers.
|
||||||
// iss and scope parameters are needed for redirection to work on IOS.
|
// iss and scope parameters are needed for redirection to work on IOS.
|
||||||
// We pass the state as the code to get it back later on.
|
|
||||||
async fn _oidcsignin_redirect(
|
async fn _oidcsignin_redirect(
|
||||||
base64_state: String,
|
base64_state: String,
|
||||||
code_response: OIDCCodeWrapper,
|
code: OIDCCode,
|
||||||
|
error: Option<OIDCCodeResponseError>,
|
||||||
cookies: &CookieJar<'_>,
|
cookies: &CookieJar<'_>,
|
||||||
conn: &mut DbConn,
|
conn: &mut DbConn,
|
||||||
) -> ApiResult<Redirect> {
|
) -> ApiResult<Redirect> {
|
||||||
@@ -1222,9 +1217,11 @@ async fn _oidcsignin_redirect(
|
|||||||
(Some(expected), Some(actual)) if crypto::ct_eq(expected, actual) => {}
|
(Some(expected), Some(actual)) if crypto::ct_eq(expected, actual) => {}
|
||||||
_ => err!(format!("SSO session binding mismatch for {state}")),
|
_ => err!(format!("SSO session binding mismatch for {state}")),
|
||||||
}
|
}
|
||||||
cookies.remove(Cookie::build(SSO_BINDING_COOKIE).path("/identity/connect/").build());
|
cookies
|
||||||
|
.remove(Cookie::build(SSO_BINDING_COOKIE).path(format!("{}/identity/connect/", CONFIG.domain_path())).build());
|
||||||
|
|
||||||
sso_auth.code_response = Some(code_response);
|
sso_auth.code_response = Some(code.clone());
|
||||||
|
sso_auth.code_response_error = error;
|
||||||
sso_auth.updated_at = Utc::now().naive_utc();
|
sso_auth.updated_at = Utc::now().naive_utc();
|
||||||
sso_auth.save(conn).await?;
|
sso_auth.save(conn).await?;
|
||||||
|
|
||||||
@@ -1234,7 +1231,7 @@ async fn _oidcsignin_redirect(
|
|||||||
};
|
};
|
||||||
|
|
||||||
url.query_pairs_mut()
|
url.query_pairs_mut()
|
||||||
.append_pair("code", &state)
|
.append_pair("code", &code)
|
||||||
.append_pair("state", &state)
|
.append_pair("state", &state)
|
||||||
.append_pair("scope", &AuthMethod::Sso.scope())
|
.append_pair("scope", &AuthMethod::Sso.scope())
|
||||||
.append_pair("iss", &CONFIG.domain());
|
.append_pair("iss", &CONFIG.domain());
|
||||||
@@ -1294,7 +1291,7 @@ async fn authorize(data: AuthorizeData, cookies: &CookieJar<'_>, secure: Secure,
|
|||||||
|
|
||||||
cookies.add(
|
cookies.add(
|
||||||
Cookie::build((SSO_BINDING_COOKIE, binding_token))
|
Cookie::build((SSO_BINDING_COOKIE, binding_token))
|
||||||
.path("/identity/connect/")
|
.path(format!("{}/identity/connect/", CONFIG.domain_path()))
|
||||||
.max_age(time::Duration::seconds(sso::SSO_AUTH_EXPIRATION.num_seconds()))
|
.max_age(time::Duration::seconds(sso::SSO_AUTH_EXPIRATION.num_seconds()))
|
||||||
.same_site(SameSite::Lax) // Lax is needed because the IdP runs on a different FQDN
|
.same_site(SameSite::Lax) // Lax is needed because the IdP runs on a different FQDN
|
||||||
.http_only(true)
|
.http_only(true)
|
||||||
|
|||||||
+2
-6
@@ -54,12 +54,8 @@ static PUBLIC_RSA_KEY: OnceLock<DecodingKey> = OnceLock::new();
|
|||||||
pub async fn initialize_keys() -> Result<(), Error> {
|
pub async fn initialize_keys() -> Result<(), Error> {
|
||||||
use std::io::Error;
|
use std::io::Error;
|
||||||
|
|
||||||
let rsa_key_filename = std::path::PathBuf::from(CONFIG.private_rsa_key())
|
let rsa_key_filename = crate::storage::file_name(&CONFIG.private_rsa_key())
|
||||||
.file_name()
|
.ok_or_else(|| Error::other("Private RSA key path missing filename"))?;
|
||||||
.ok_or_else(|| Error::other("Private RSA key path missing filename"))?
|
|
||||||
.to_str()
|
|
||||||
.ok_or_else(|| Error::other("Private RSA key path filename is not valid UTF-8"))?
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let operator = CONFIG.opendal_operator_for_path_type(&PathType::RsaKey).map_err(Error::other)?;
|
let operator = CONFIG.opendal_operator_for_path_type(&PathType::RsaKey).map_err(Error::other)?;
|
||||||
|
|
||||||
|
|||||||
+31
-119
@@ -14,6 +14,7 @@ use serde::de::{self, Deserialize, Deserializer, MapAccess, Visitor};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
error::Error,
|
error::Error,
|
||||||
|
storage,
|
||||||
util::{
|
util::{
|
||||||
get_active_web_release, get_env, get_env_bool, is_valid_email, parse_experimental_client_feature_flags,
|
get_active_web_release, get_env, get_env_bool, is_valid_email, parse_experimental_client_feature_flags,
|
||||||
FeatureFlagFilter,
|
FeatureFlagFilter,
|
||||||
@@ -22,18 +23,14 @@ use crate::{
|
|||||||
|
|
||||||
static CONFIG_FILE: LazyLock<String> = LazyLock::new(|| {
|
static CONFIG_FILE: LazyLock<String> = LazyLock::new(|| {
|
||||||
let data_folder = get_env("DATA_FOLDER").unwrap_or_else(|| String::from("data"));
|
let data_folder = get_env("DATA_FOLDER").unwrap_or_else(|| String::from("data"));
|
||||||
get_env("CONFIG_FILE").unwrap_or_else(|| format!("{data_folder}/config.json"))
|
get_env("CONFIG_FILE").unwrap_or_else(|| storage::join_path(&data_folder, "config.json"))
|
||||||
});
|
});
|
||||||
|
|
||||||
static CONFIG_FILE_PARENT_DIR: LazyLock<String> = LazyLock::new(|| {
|
static CONFIG_FILE_PARENT_DIR: LazyLock<String> =
|
||||||
let path = std::path::PathBuf::from(&*CONFIG_FILE);
|
LazyLock::new(|| storage::parent(&CONFIG_FILE).unwrap_or_else(|| "data".to_string()));
|
||||||
path.parent().unwrap_or(std::path::Path::new("data")).to_str().unwrap_or("data").to_string()
|
|
||||||
});
|
|
||||||
|
|
||||||
static CONFIG_FILENAME: LazyLock<String> = LazyLock::new(|| {
|
static CONFIG_FILENAME: LazyLock<String> =
|
||||||
let path = std::path::PathBuf::from(&*CONFIG_FILE);
|
LazyLock::new(|| storage::file_name(&CONFIG_FILE).unwrap_or_else(|| "config.json".to_string()));
|
||||||
path.file_name().unwrap_or(std::ffi::OsStr::new("config.json")).to_str().unwrap_or("config.json").to_string()
|
|
||||||
});
|
|
||||||
|
|
||||||
pub static SKIP_CONFIG_VALIDATION: AtomicBool = AtomicBool::new(false);
|
pub static SKIP_CONFIG_VALIDATION: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
@@ -263,7 +260,7 @@ macro_rules! make_config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn from_file() -> Result<Self, Error> {
|
async fn from_file() -> Result<Self, Error> {
|
||||||
let operator = opendal_operator_for_path(&CONFIG_FILE_PARENT_DIR)?;
|
let operator = storage::operator_for_path(&CONFIG_FILE_PARENT_DIR)?;
|
||||||
let config_bytes = operator.read(&CONFIG_FILENAME).await?;
|
let config_bytes = operator.read(&CONFIG_FILENAME).await?;
|
||||||
println!("[INFO] Using saved config from `{}` for configuration.\n", *CONFIG_FILE);
|
println!("[INFO] Using saved config from `{}` for configuration.\n", *CONFIG_FILE);
|
||||||
serde_json::from_slice(&config_bytes.to_vec()).map_err(Into::into)
|
serde_json::from_slice(&config_bytes.to_vec()).map_err(Into::into)
|
||||||
@@ -507,19 +504,19 @@ make_config! {
|
|||||||
/// Data folder |> Main data folder
|
/// Data folder |> Main data folder
|
||||||
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!("{}/db.sqlite3", c.data_folder);
|
database_url: String, false, auto, |c| format!("sqlite://{}", storage::join_path(&c.data_folder, "db.sqlite3"));
|
||||||
/// Icon cache folder
|
/// Icon cache folder
|
||||||
icon_cache_folder: String, false, auto, |c| format!("{}/icon_cache", c.data_folder);
|
icon_cache_folder: String, false, auto, |c| storage::join_path(&c.data_folder, "icon_cache");
|
||||||
/// Attachments folder
|
/// Attachments folder
|
||||||
attachments_folder: String, false, auto, |c| format!("{}/attachments", c.data_folder);
|
attachments_folder: String, false, auto, |c| storage::join_path(&c.data_folder, "attachments");
|
||||||
/// Sends folder
|
/// Sends folder
|
||||||
sends_folder: String, false, auto, |c| format!("{}/sends", c.data_folder);
|
sends_folder: String, false, auto, |c| storage::join_path(&c.data_folder, "sends");
|
||||||
/// Temp folder |> Used for storing temporary file uploads
|
/// Temp folder |> Used for storing temporary file uploads
|
||||||
tmp_folder: String, false, auto, |c| format!("{}/tmp", c.data_folder);
|
tmp_folder: String, false, auto, |c| storage::join_path(&c.data_folder, "tmp");
|
||||||
/// Templates folder
|
/// Templates folder
|
||||||
templates_folder: String, false, auto, |c| format!("{}/templates", c.data_folder);
|
templates_folder: String, false, auto, |c| storage::join_path(&c.data_folder, "templates");
|
||||||
/// Session JWT key
|
/// Session JWT key
|
||||||
rsa_key_filename: String, false, auto, |c| format!("{}/rsa_key", c.data_folder);
|
rsa_key_filename: String, false, auto, |c| storage::join_path(&c.data_folder, "rsa_key");
|
||||||
/// Web vault folder
|
/// Web vault folder
|
||||||
web_vault_folder: String, false, def, "web-vault/".to_string();
|
web_vault_folder: String, false, def, "web-vault/".to_string();
|
||||||
},
|
},
|
||||||
@@ -929,14 +926,17 @@ fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> {
|
|||||||
{
|
{
|
||||||
use crate::db::DbConnType;
|
use crate::db::DbConnType;
|
||||||
let url = &cfg.database_url;
|
let url = &cfg.database_url;
|
||||||
if DbConnType::from_url(url)? == DbConnType::Sqlite && url.contains('/') {
|
if DbConnType::from_url(url)? == DbConnType::Sqlite {
|
||||||
let path = std::path::Path::new(&url);
|
let file_path = url.strip_prefix("sqlite://").unwrap_or(url);
|
||||||
if let Some(parent) = path.parent() {
|
if file_path.contains('/') {
|
||||||
if !parent.is_dir() {
|
let path = std::path::Path::new(file_path);
|
||||||
err!(format!(
|
if let Some(parent) = path.parent() {
|
||||||
"SQLite database directory `{}` does not exist or is not a directory",
|
if !parent.is_dir() {
|
||||||
parent.display()
|
err!(format!(
|
||||||
));
|
"SQLite database directory `{}` does not exist or is not a directory",
|
||||||
|
parent.display()
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1366,90 +1366,6 @@ fn smtp_convert_deprecated_ssl_options(smtp_ssl: Option<bool>, smtp_explicit_tls
|
|||||||
"starttls".to_string()
|
"starttls".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn opendal_operator_for_path(path: &str) -> Result<opendal::Operator, Error> {
|
|
||||||
// Cache of previously built operators by path
|
|
||||||
static OPERATORS_BY_PATH: LazyLock<dashmap::DashMap<String, opendal::Operator>> =
|
|
||||||
LazyLock::new(dashmap::DashMap::new);
|
|
||||||
|
|
||||||
if let Some(operator) = OPERATORS_BY_PATH.get(path) {
|
|
||||||
return Ok(operator.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
let operator = if path.starts_with("s3://") {
|
|
||||||
#[cfg(not(s3))]
|
|
||||||
return Err(opendal::Error::new(opendal::ErrorKind::ConfigInvalid, "S3 support is not enabled").into());
|
|
||||||
|
|
||||||
#[cfg(s3)]
|
|
||||||
opendal_s3_operator_for_path(path)?
|
|
||||||
} else {
|
|
||||||
let builder = opendal::services::Fs::default().root(path);
|
|
||||||
opendal::Operator::new(builder)?.finish()
|
|
||||||
};
|
|
||||||
|
|
||||||
OPERATORS_BY_PATH.insert(path.to_string(), operator.clone());
|
|
||||||
|
|
||||||
Ok(operator)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(s3)]
|
|
||||||
fn opendal_s3_operator_for_path(path: &str) -> Result<opendal::Operator, Error> {
|
|
||||||
use crate::http_client::aws::AwsReqwestConnector;
|
|
||||||
use aws_config::{default_provider::credentials::DefaultCredentialsChain, provider_config::ProviderConfig};
|
|
||||||
|
|
||||||
// This is a custom AWS credential loader that uses the official AWS Rust
|
|
||||||
// SDK config crate to load credentials. This ensures maximum compatibility
|
|
||||||
// with AWS credential configurations. For example, OpenDAL doesn't support
|
|
||||||
// AWS SSO temporary credentials yet.
|
|
||||||
struct OpenDALS3CredentialLoader {}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl reqsign::AwsCredentialLoad for OpenDALS3CredentialLoader {
|
|
||||||
async fn load_credential(&self, _client: reqwest::Client) -> anyhow::Result<Option<reqsign::AwsCredential>> {
|
|
||||||
use aws_credential_types::provider::ProvideCredentials as _;
|
|
||||||
use tokio::sync::OnceCell;
|
|
||||||
|
|
||||||
static DEFAULT_CREDENTIAL_CHAIN: OnceCell<DefaultCredentialsChain> = OnceCell::const_new();
|
|
||||||
|
|
||||||
let chain = DEFAULT_CREDENTIAL_CHAIN
|
|
||||||
.get_or_init(|| {
|
|
||||||
let reqwest_client = reqwest::Client::builder().build().unwrap();
|
|
||||||
let connector = AwsReqwestConnector {
|
|
||||||
client: reqwest_client,
|
|
||||||
};
|
|
||||||
|
|
||||||
let conf = ProviderConfig::default().with_http_client(connector);
|
|
||||||
|
|
||||||
DefaultCredentialsChain::builder().configure(conf).build()
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let creds = chain.provide_credentials().await?;
|
|
||||||
|
|
||||||
Ok(Some(reqsign::AwsCredential {
|
|
||||||
access_key_id: creds.access_key_id().to_string(),
|
|
||||||
secret_access_key: creds.secret_access_key().to_string(),
|
|
||||||
session_token: creds.session_token().map(|s| s.to_string()),
|
|
||||||
expires_in: creds.expiry().map(|expiration| expiration.into()),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const OPEN_DAL_S3_CREDENTIAL_LOADER: OpenDALS3CredentialLoader = OpenDALS3CredentialLoader {};
|
|
||||||
|
|
||||||
let url = Url::parse(path).map_err(|e| format!("Invalid path S3 URL path {path:?}: {e}"))?;
|
|
||||||
|
|
||||||
let bucket = url.host_str().ok_or_else(|| format!("Missing Bucket name in data folder S3 URL {path:?}"))?;
|
|
||||||
|
|
||||||
let builder = opendal::services::S3::default()
|
|
||||||
.customized_credential_load(Box::new(OPEN_DAL_S3_CREDENTIAL_LOADER))
|
|
||||||
.enable_virtual_host_style()
|
|
||||||
.bucket(bucket)
|
|
||||||
.root(url.path())
|
|
||||||
.default_storage_class("INTELLIGENT_TIERING");
|
|
||||||
|
|
||||||
Ok(opendal::Operator::new(builder)?.finish())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum PathType {
|
pub enum PathType {
|
||||||
Data,
|
Data,
|
||||||
IconCache,
|
IconCache,
|
||||||
@@ -1547,7 +1463,7 @@ impl Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Save to file
|
//Save to file
|
||||||
let operator = opendal_operator_for_path(&CONFIG_FILE_PARENT_DIR)?;
|
let operator = storage::operator_for_path(&CONFIG_FILE_PARENT_DIR)?;
|
||||||
operator.write(&CONFIG_FILENAME, config_str).await?;
|
operator.write(&CONFIG_FILENAME, config_str).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -1612,7 +1528,7 @@ impl Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete_user_config(&self) -> Result<(), Error> {
|
pub async fn delete_user_config(&self) -> Result<(), Error> {
|
||||||
let operator = opendal_operator_for_path(&CONFIG_FILE_PARENT_DIR)?;
|
let operator = storage::operator_for_path(&CONFIG_FILE_PARENT_DIR)?;
|
||||||
operator.delete(&CONFIG_FILENAME).await?;
|
operator.delete(&CONFIG_FILENAME).await?;
|
||||||
|
|
||||||
// Empty user config
|
// Empty user config
|
||||||
@@ -1636,7 +1552,7 @@ impl Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn private_rsa_key(&self) -> String {
|
pub fn private_rsa_key(&self) -> String {
|
||||||
format!("{}.pem", self.rsa_key_filename())
|
storage::with_extension(&self.rsa_key_filename(), "pem")
|
||||||
}
|
}
|
||||||
pub fn mail_enabled(&self) -> bool {
|
pub fn mail_enabled(&self) -> bool {
|
||||||
let inner = &self.inner.read().unwrap().config;
|
let inner = &self.inner.read().unwrap().config;
|
||||||
@@ -1677,15 +1593,11 @@ impl Config {
|
|||||||
PathType::IconCache => self.icon_cache_folder(),
|
PathType::IconCache => self.icon_cache_folder(),
|
||||||
PathType::Attachments => self.attachments_folder(),
|
PathType::Attachments => self.attachments_folder(),
|
||||||
PathType::Sends => self.sends_folder(),
|
PathType::Sends => self.sends_folder(),
|
||||||
PathType::RsaKey => std::path::Path::new(&self.rsa_key_filename())
|
PathType::RsaKey => storage::parent(&self.private_rsa_key())
|
||||||
.parent()
|
.ok_or_else(|| std::io::Error::other("Failed to get directory of RSA key file"))?,
|
||||||
.ok_or_else(|| std::io::Error::other("Failed to get directory of RSA key file"))?
|
|
||||||
.to_str()
|
|
||||||
.ok_or_else(|| std::io::Error::other("Failed to convert RSA key file directory to UTF-8 string"))?
|
|
||||||
.to_string(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
opendal_operator_for_path(&path)
|
storage::operator_for_path(&path)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_template<T: serde::ser::Serialize>(&self, name: &str, data: &T) -> Result<String, Error> {
|
pub fn render_template<T: serde::ser::Serialize>(&self, name: &str, data: &T) -> Result<String, Error> {
|
||||||
|
|||||||
+27
-7
@@ -272,13 +272,32 @@ impl DbConnType {
|
|||||||
#[cfg(not(postgresql))]
|
#[cfg(not(postgresql))]
|
||||||
err!("`DATABASE_URL` is a PostgreSQL URL, but the 'postgresql' feature is not enabled")
|
err!("`DATABASE_URL` is a PostgreSQL URL, but the 'postgresql' feature is not enabled")
|
||||||
|
|
||||||
//Sqlite
|
// Sqlite (explicit)
|
||||||
} else {
|
} else if url.len() > 7 && &url[..7] == "sqlite:" {
|
||||||
#[cfg(sqlite)]
|
#[cfg(sqlite)]
|
||||||
return Ok(DbConnType::Sqlite);
|
return Ok(DbConnType::Sqlite);
|
||||||
|
|
||||||
#[cfg(not(sqlite))]
|
#[cfg(not(sqlite))]
|
||||||
err!("`DATABASE_URL` looks like a SQLite URL, but 'sqlite' feature is not enabled")
|
err!("`DATABASE_URL` is a SQLite URL, but the 'sqlite' feature is not enabled")
|
||||||
|
|
||||||
|
// No recognized scheme — assume legacy bare-path SQLite, but the database file must already exist.
|
||||||
|
// This prevents misconfigured URLs (typos, quoted strings) from silently creating a new empty SQLite database.
|
||||||
|
} else {
|
||||||
|
#[cfg(sqlite)]
|
||||||
|
{
|
||||||
|
if std::path::Path::new(url).exists() {
|
||||||
|
return Ok(DbConnType::Sqlite);
|
||||||
|
}
|
||||||
|
err!(format!(
|
||||||
|
"`DATABASE_URL` does not match any known database scheme (mysql://, postgresql://, sqlite://) \
|
||||||
|
and no existing SQLite database was found at '{url}'. \
|
||||||
|
If you intend to use SQLite, use an explicit `sqlite://` scheme in your `DATABASE_URL`. \
|
||||||
|
Otherwise, check your DATABASE_URL for typos or quoting issues."
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(sqlite))]
|
||||||
|
err!("`DATABASE_URL` does not match any known database scheme (mysql://, postgresql://, sqlite://)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -390,11 +409,12 @@ pub fn backup_sqlite() -> Result<String, Error> {
|
|||||||
|
|
||||||
let db_url = CONFIG.database_url();
|
let db_url = CONFIG.database_url();
|
||||||
if DbConnType::from_url(&CONFIG.database_url()).map(|t| t == DbConnType::Sqlite).unwrap_or(false) {
|
if DbConnType::from_url(&CONFIG.database_url()).map(|t| t == DbConnType::Sqlite).unwrap_or(false) {
|
||||||
// Since we do not allow any schema for sqlite database_url's like `file:` or `sqlite:` to be set, we can assume here it isn't
|
// Strip the sqlite:// prefix if present to get the raw file path
|
||||||
// This way we can set a readonly flag on the opening mode without issues.
|
let file_path = db_url.strip_prefix("sqlite://").unwrap_or(&db_url);
|
||||||
let mut conn = diesel::sqlite::SqliteConnection::establish(&format!("sqlite://{db_url}?mode=ro"))?;
|
// Open a read-only connection for the backup
|
||||||
|
let mut conn = diesel::sqlite::SqliteConnection::establish(&format!("sqlite://{file_path}?mode=ro"))?;
|
||||||
|
|
||||||
let db_path = std::path::Path::new(&db_url).parent().unwrap();
|
let db_path = std::path::Path::new(file_path).parent().unwrap();
|
||||||
let backup_file = db_path
|
let backup_file = db_path
|
||||||
.join(format!("db_{}.sqlite3", chrono::Utc::now().format("%Y%m%d_%H%M%S")))
|
.join(format!("db_{}.sqlite3", chrono::Utc::now().format("%Y%m%d_%H%M%S")))
|
||||||
.to_string_lossy()
|
.to_string_lossy()
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ impl Attachment {
|
|||||||
pub async fn get_url(&self, host: &str) -> Result<String, crate::Error> {
|
pub async fn get_url(&self, host: &str) -> Result<String, crate::Error> {
|
||||||
let operator = CONFIG.opendal_operator_for_path_type(&PathType::Attachments)?;
|
let operator = CONFIG.opendal_operator_for_path_type(&PathType::Attachments)?;
|
||||||
|
|
||||||
if operator.info().scheme() == <&'static str>::from(opendal::Scheme::Fs) {
|
if crate::storage::is_fs_operator(&operator) {
|
||||||
let token = encode_jwt(&generate_file_download_claims(self.cipher_uuid.clone(), self.id.clone()));
|
let token = encode_jwt(&generate_file_download_claims(self.cipher_uuid.clone(), self.id.clone()));
|
||||||
Ok(format!("{host}/attachments/{}/{}?token={token}", self.cipher_uuid, self.id))
|
Ok(format!("{host}/attachments/{}/{}?token={token}", self.cipher_uuid, self.id))
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ pub use self::send::{
|
|||||||
id::{SendFileId, SendId},
|
id::{SendFileId, SendId},
|
||||||
Send, SendType,
|
Send, SendType,
|
||||||
};
|
};
|
||||||
pub use self::sso_auth::{OIDCAuthenticatedUser, OIDCCodeWrapper, SsoAuth};
|
pub use self::sso_auth::{OIDCAuthenticatedUser, OIDCCodeResponseError, SsoAuth};
|
||||||
pub use self::two_factor::{TwoFactor, TwoFactorType};
|
pub use self::two_factor::{TwoFactor, TwoFactorType};
|
||||||
pub use self::two_factor_duo_context::TwoFactorDuoContext;
|
pub use self::two_factor_duo_context::TwoFactorDuoContext;
|
||||||
pub use self::two_factor_incomplete::TwoFactorIncomplete;
|
pub use self::two_factor_incomplete::TwoFactorIncomplete;
|
||||||
|
|||||||
@@ -237,7 +237,7 @@ impl Send {
|
|||||||
|
|
||||||
if self.atype == SendType::File as i32 {
|
if self.atype == SendType::File as i32 {
|
||||||
let operator = CONFIG.opendal_operator_for_path_type(&PathType::Sends)?;
|
let operator = CONFIG.opendal_operator_for_path_type(&PathType::Sends)?;
|
||||||
operator.remove_all(&self.uuid).await.ok();
|
operator.delete_with(&self.uuid).recursive(true).await.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
db_run! { conn: {
|
db_run! { conn: {
|
||||||
|
|||||||
+18
-10
@@ -15,17 +15,12 @@ use diesel::sql_types::Text;
|
|||||||
|
|
||||||
#[derive(AsExpression, Clone, Debug, Serialize, Deserialize, FromSqlRow)]
|
#[derive(AsExpression, Clone, Debug, Serialize, Deserialize, FromSqlRow)]
|
||||||
#[diesel(sql_type = Text)]
|
#[diesel(sql_type = Text)]
|
||||||
pub enum OIDCCodeWrapper {
|
pub struct OIDCCodeResponseError {
|
||||||
Ok {
|
pub error: String,
|
||||||
code: OIDCCode,
|
pub error_description: Option<String>,
|
||||||
},
|
|
||||||
Error {
|
|
||||||
error: String,
|
|
||||||
error_description: Option<String>,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_FromToSqlText!(OIDCCodeWrapper);
|
impl_FromToSqlText!(OIDCCodeResponseError);
|
||||||
|
|
||||||
#[derive(AsExpression, Clone, Debug, Serialize, Deserialize, FromSqlRow)]
|
#[derive(AsExpression, Clone, Debug, Serialize, Deserialize, FromSqlRow)]
|
||||||
#[diesel(sql_type = Text)]
|
#[diesel(sql_type = Text)]
|
||||||
@@ -50,7 +45,8 @@ pub struct SsoAuth {
|
|||||||
pub client_challenge: OIDCCodeChallenge,
|
pub client_challenge: OIDCCodeChallenge,
|
||||||
pub nonce: String,
|
pub nonce: String,
|
||||||
pub redirect_uri: String,
|
pub redirect_uri: String,
|
||||||
pub code_response: Option<OIDCCodeWrapper>,
|
pub code_response: Option<OIDCCode>,
|
||||||
|
pub code_response_error: Option<OIDCCodeResponseError>,
|
||||||
pub auth_response: Option<OIDCAuthenticatedUser>,
|
pub auth_response: Option<OIDCAuthenticatedUser>,
|
||||||
pub created_at: NaiveDateTime,
|
pub created_at: NaiveDateTime,
|
||||||
pub updated_at: NaiveDateTime,
|
pub updated_at: NaiveDateTime,
|
||||||
@@ -76,6 +72,7 @@ impl SsoAuth {
|
|||||||
created_at: now,
|
created_at: now,
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
code_response: None,
|
code_response: None,
|
||||||
|
code_response_error: None,
|
||||||
auth_response: None,
|
auth_response: None,
|
||||||
binding_hash,
|
binding_hash,
|
||||||
}
|
}
|
||||||
@@ -118,6 +115,17 @@ impl SsoAuth {
|
|||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn find_by_code(code: &OIDCCode, conn: &DbConn) -> Option<Self> {
|
||||||
|
let oldest = Utc::now().naive_utc() - *SSO_AUTH_EXPIRATION;
|
||||||
|
db_run! { conn: {
|
||||||
|
sso_auth::table
|
||||||
|
.filter(sso_auth::code_response.eq(code))
|
||||||
|
.filter(sso_auth::created_at.ge(oldest))
|
||||||
|
.first::<Self>(conn)
|
||||||
|
.ok()
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn delete(self, conn: &DbConn) -> EmptyResult {
|
pub async fn delete(self, conn: &DbConn) -> EmptyResult {
|
||||||
db_run! {conn: {
|
db_run! {conn: {
|
||||||
diesel::delete(sso_auth::table.filter(sso_auth::state.eq(self.state)))
|
diesel::delete(sso_auth::table.filter(sso_auth::state.eq(self.state)))
|
||||||
|
|||||||
@@ -262,6 +262,7 @@ table! {
|
|||||||
nonce -> Text,
|
nonce -> Text,
|
||||||
redirect_uri -> Text,
|
redirect_uri -> Text,
|
||||||
code_response -> Nullable<Text>,
|
code_response -> Nullable<Text>,
|
||||||
|
code_response_error -> Nullable<Text>,
|
||||||
auth_response -> Nullable<Text>,
|
auth_response -> Nullable<Text>,
|
||||||
created_at -> Timestamp,
|
created_at -> Timestamp,
|
||||||
updated_at -> Timestamp,
|
updated_at -> Timestamp,
|
||||||
|
|||||||
+10
@@ -57,6 +57,7 @@ mod mail;
|
|||||||
mod ratelimit;
|
mod ratelimit;
|
||||||
mod sso;
|
mod sso;
|
||||||
mod sso_client;
|
mod sso_client;
|
||||||
|
mod storage;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
use crate::api::core::two_factor::duo_oidc::purge_duo_contexts;
|
use crate::api::core::two_factor::duo_oidc::purge_duo_contexts;
|
||||||
@@ -70,6 +71,7 @@ pub use util::is_running_in_container;
|
|||||||
|
|
||||||
#[rocket::main]
|
#[rocket::main]
|
||||||
async fn main() -> Result<(), Error> {
|
async fn main() -> Result<(), Error> {
|
||||||
|
install_rustls_crypto_provider();
|
||||||
parse_args();
|
parse_args();
|
||||||
launch_info();
|
launch_info();
|
||||||
|
|
||||||
@@ -202,6 +204,14 @@ fn parse_args() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn install_rustls_crypto_provider() {
|
||||||
|
if rustls::crypto::CryptoProvider::get_default().is_none() {
|
||||||
|
rustls::crypto::ring::default_provider()
|
||||||
|
.install_default()
|
||||||
|
.expect("failed to install rustls ring crypto provider");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn launch_info() {
|
fn launch_info() {
|
||||||
println!(
|
println!(
|
||||||
"\
|
"\
|
||||||
|
|||||||
+14
-14
@@ -10,7 +10,7 @@ use crate::{
|
|||||||
auth,
|
auth,
|
||||||
auth::{AuthMethod, AuthTokens, TokenWrapper, BW_EXPIRATION, DEFAULT_REFRESH_VALIDITY},
|
auth::{AuthMethod, AuthTokens, TokenWrapper, BW_EXPIRATION, DEFAULT_REFRESH_VALIDITY},
|
||||||
db::{
|
db::{
|
||||||
models::{Device, OIDCAuthenticatedUser, OIDCCodeWrapper, SsoAuth, SsoUser, User},
|
models::{Device, OIDCAuthenticatedUser, SsoAuth, SsoUser, User},
|
||||||
DbConn,
|
DbConn,
|
||||||
},
|
},
|
||||||
sso_client::Client,
|
sso_client::Client,
|
||||||
@@ -240,14 +240,14 @@ impl OIDCIdentifier {
|
|||||||
// - second time we will rely on `SsoAuth.auth_response` since the `code` has already been exchanged.
|
// - second time we will rely on `SsoAuth.auth_response` since the `code` has already been exchanged.
|
||||||
// The `SsoAuth` will ensure that the user is authorized only once.
|
// The `SsoAuth` will ensure that the user is authorized only once.
|
||||||
pub async fn exchange_code(
|
pub async fn exchange_code(
|
||||||
state: &OIDCState,
|
code: &OIDCCode,
|
||||||
client_verifier: OIDCCodeVerifier,
|
client_verifier: OIDCCodeVerifier,
|
||||||
conn: &DbConn,
|
conn: &DbConn,
|
||||||
) -> ApiResult<(SsoAuth, OIDCAuthenticatedUser)> {
|
) -> ApiResult<(SsoAuth, OIDCAuthenticatedUser)> {
|
||||||
use openidconnect::OAuth2TokenResponse;
|
use openidconnect::OAuth2TokenResponse;
|
||||||
|
|
||||||
let mut sso_auth = match SsoAuth::find(state, conn).await {
|
let mut sso_auth = match SsoAuth::find_by_code(code, conn).await {
|
||||||
None => err!(format!("Invalid state cannot retrieve sso auth")),
|
None => err!(format!("Invalid code cannot retrieve sso auth")),
|
||||||
Some(sso_auth) => sso_auth,
|
Some(sso_auth) => sso_auth,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -255,18 +255,18 @@ pub async fn exchange_code(
|
|||||||
return Ok((sso_auth, authenticated_user));
|
return Ok((sso_auth, authenticated_user));
|
||||||
}
|
}
|
||||||
|
|
||||||
let code = match sso_auth.code_response.clone() {
|
let code = match (sso_auth.code_response.clone(), sso_auth.code_response_error.as_ref()) {
|
||||||
Some(OIDCCodeWrapper::Ok {
|
(Some(code), None) => code,
|
||||||
code,
|
(_, Some(re)) => {
|
||||||
}) => code.clone(),
|
let error_msg = format!(
|
||||||
Some(OIDCCodeWrapper::Error {
|
"SSO authorization failed: {}, {}",
|
||||||
error,
|
re.error,
|
||||||
error_description,
|
re.error_description.as_ref().unwrap_or(&String::new())
|
||||||
}) => {
|
);
|
||||||
sso_auth.delete(conn).await?;
|
sso_auth.delete(conn).await?;
|
||||||
err!(format!("SSO authorization failed: {error}, {}", error_description.as_ref().unwrap_or(&String::new())))
|
err!(error_msg);
|
||||||
}
|
}
|
||||||
None => {
|
(None, _) => {
|
||||||
sso_auth.delete(conn).await?;
|
sso_auth.delete(conn).await?;
|
||||||
err!("Missing authorization provider return");
|
err!("Missing authorization provider return");
|
||||||
}
|
}
|
||||||
|
|||||||
+37
-4
@@ -1,12 +1,13 @@
|
|||||||
use std::{borrow::Cow, sync::LazyLock, time::Duration};
|
use std::{borrow::Cow, future::Future, pin::Pin, sync::LazyLock, time::Duration};
|
||||||
|
|
||||||
use openidconnect::{core::*, reqwest, *};
|
use openidconnect::{core::*, *};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{ApiResult, EmptyResult},
|
api::{ApiResult, EmptyResult},
|
||||||
db::models::SsoAuth,
|
db::models::SsoAuth,
|
||||||
|
http_client::get_reqwest_client_builder,
|
||||||
sso::{OIDCCode, OIDCCodeChallenge, OIDCCodeVerifier, OIDCState},
|
sso::{OIDCCode, OIDCCodeChallenge, OIDCCodeVerifier, OIDCState},
|
||||||
CONFIG,
|
CONFIG,
|
||||||
};
|
};
|
||||||
@@ -46,10 +47,42 @@ pub type RefreshTokenResponse = (Option<String>, String, Option<Duration>);
|
|||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Client {
|
pub struct Client {
|
||||||
pub http_client: reqwest::Client,
|
pub http_client: OidcHttpClient,
|
||||||
pub core_client: CustomClient,
|
pub core_client: CustomClient,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct OidcHttpClient {
|
||||||
|
client: reqwest::Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OidcHttpClient {
|
||||||
|
fn new() -> Result<Self, reqwest::Error> {
|
||||||
|
get_reqwest_client_builder().redirect(reqwest::redirect::Policy::none()).build().map(|client| Self {
|
||||||
|
client,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'c> AsyncHttpClient<'c> for OidcHttpClient {
|
||||||
|
type Error = HttpClientError<reqwest::Error>;
|
||||||
|
type Future = Pin<Box<dyn Future<Output = Result<HttpResponse, Self::Error>> + Send + Sync + 'c>>;
|
||||||
|
|
||||||
|
fn call(&'c self, request: HttpRequest) -> Self::Future {
|
||||||
|
Box::pin(async move {
|
||||||
|
let response = self.client.execute(request.try_into().map_err(Box::new)?).await.map_err(Box::new)?;
|
||||||
|
|
||||||
|
let mut builder = http::Response::builder().status(response.status()).version(response.version());
|
||||||
|
|
||||||
|
for (name, value) in response.headers() {
|
||||||
|
builder = builder.header(name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.body(response.bytes().await.map_err(Box::new)?.to_vec()).map_err(HttpClientError::Http)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Client {
|
impl Client {
|
||||||
// Call the OpenId discovery endpoint to retrieve configuration
|
// Call the OpenId discovery endpoint to retrieve configuration
|
||||||
async fn _get_client() -> ApiResult<Self> {
|
async fn _get_client() -> ApiResult<Self> {
|
||||||
@@ -58,7 +91,7 @@ impl Client {
|
|||||||
|
|
||||||
let issuer_url = CONFIG.sso_issuer_url()?;
|
let issuer_url = CONFIG.sso_issuer_url()?;
|
||||||
|
|
||||||
let http_client = match reqwest::ClientBuilder::new().redirect(reqwest::redirect::Policy::none()).build() {
|
let http_client = match OidcHttpClient::new() {
|
||||||
Err(err) => err!(format!("Failed to build http client: {err}")),
|
Err(err) => err!(format!("Failed to build http client: {err}")),
|
||||||
Ok(client) => client,
|
Ok(client) => client,
|
||||||
};
|
};
|
||||||
|
|||||||
Vendored
+2
-2
@@ -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-2.3.7
|
* https://datatables.net/download/#bs5/dt-2.3.8
|
||||||
*
|
*
|
||||||
* Included libraries:
|
* Included libraries:
|
||||||
* DataTables 2.3.7
|
* DataTables 2.3.8
|
||||||
*/
|
*/
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
|
|||||||
Vendored
+41
-11
@@ -4,13 +4,13 @@
|
|||||||
*
|
*
|
||||||
* 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-2.3.7
|
* https://datatables.net/download/#bs5/dt-2.3.8
|
||||||
*
|
*
|
||||||
* Included libraries:
|
* Included libraries:
|
||||||
* DataTables 2.3.7
|
* DataTables 2.3.8
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/*! DataTables 2.3.7
|
/*! DataTables 2.3.8
|
||||||
* © SpryMedia Ltd - datatables.net/license
|
* © SpryMedia Ltd - datatables.net/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -525,7 +525,7 @@
|
|||||||
*
|
*
|
||||||
* @type string
|
* @type string
|
||||||
*/
|
*/
|
||||||
builder: "bs5/dt-2.3.7",
|
builder: "bs5/dt-2.3.8",
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Buttons. For use with the Buttons extension for DataTables. This is
|
* Buttons. For use with the Buttons extension for DataTables. This is
|
||||||
@@ -3607,6 +3607,11 @@
|
|||||||
if ( holdPosition !== true ) {
|
if ( holdPosition !== true ) {
|
||||||
settings._iDisplayStart = 0;
|
settings._iDisplayStart = 0;
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
// Keep position, but make sure that there is actually data to display,
|
||||||
|
// otherwise we need to rewind a bit (e.g. if rows were deleted)
|
||||||
|
_fnLengthOverflow(settings);
|
||||||
|
}
|
||||||
|
|
||||||
// Let any modules know about the draw hold position state (used by
|
// Let any modules know about the draw hold position state (used by
|
||||||
// scrolling internally)
|
// scrolling internally)
|
||||||
@@ -4920,6 +4925,12 @@
|
|||||||
|
|
||||||
var args = [settings, settings.json];
|
var args = [settings, settings.json];
|
||||||
|
|
||||||
|
// If the footer element is empty after initialisation, then remove it
|
||||||
|
let tfoot = $(settings.tfoot);
|
||||||
|
if (tfoot.children().length === 0) {
|
||||||
|
tfoot.remove();
|
||||||
|
}
|
||||||
|
|
||||||
settings._bInitComplete = true;
|
settings._bInitComplete = true;
|
||||||
|
|
||||||
// Table is fully set up and we have data, so calculate the
|
// Table is fully set up and we have data, so calculate the
|
||||||
@@ -5376,12 +5387,12 @@
|
|||||||
// the content of the cell so that the width applied to the header and body
|
// the content of the cell so that the width applied to the header and body
|
||||||
// both match, but we want to hide it completely.
|
// both match, but we want to hide it completely.
|
||||||
$('th, td', headerCopy).each(function () {
|
$('th, td', headerCopy).each(function () {
|
||||||
$(this.childNodes).wrapAll('<div class="dt-scroll-sizing">');
|
$(this.childNodes).wrapAll('<div class="dt-scroll-sizing" />');
|
||||||
});
|
});
|
||||||
|
|
||||||
if ( footer ) {
|
if ( footer ) {
|
||||||
$('th, td', footerCopy).each(function () {
|
$('th, td', footerCopy).each(function () {
|
||||||
$(this.childNodes).wrapAll('<div class="dt-scroll-sizing">');
|
$(this.childNodes).wrapAll('<div class="dt-scroll-sizing" />');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5409,6 +5420,10 @@
|
|||||||
// Correct DOM ordering for colgroup - comes before the thead
|
// Correct DOM ordering for colgroup - comes before the thead
|
||||||
table.children('colgroup').prependTo(table);
|
table.children('colgroup').prependTo(table);
|
||||||
|
|
||||||
|
// Remove tabindex from the hidden row elements
|
||||||
|
table.find('thead, tfoot').find('[tabindex]').removeAttr('tabindex');
|
||||||
|
table.find('thead, tfoot').find('role').removeAttr('role');
|
||||||
|
|
||||||
// Adjust the position of the header in case we loose the y-scrollbar
|
// Adjust the position of the header in case we loose the y-scrollbar
|
||||||
divBody.trigger('scroll');
|
divBody.trigger('scroll');
|
||||||
|
|
||||||
@@ -5732,8 +5747,12 @@
|
|||||||
.replace(/id=".*?"/g, '')
|
.replace(/id=".*?"/g, '')
|
||||||
.replace(/name=".*?"/g, '');
|
.replace(/name=".*?"/g, '');
|
||||||
|
|
||||||
// Don't want Javascript at all in these calculation cells.
|
// Don't want script, dialog or template tags in the width
|
||||||
cellString = cellString.replace(/<script.*?<\/script>/gi, ' ');
|
// calculations as they are hidden content
|
||||||
|
cellString = cellString
|
||||||
|
.replace(/<script[\s\S]*?<\/script>/gi, ' ')
|
||||||
|
.replace(/<dialog[\s\S]*?<\/dialog>/gi, ' ')
|
||||||
|
.replace(/<template[\s\S]*?<\/template>/gi, ' ');
|
||||||
|
|
||||||
var noHtml = _stripHtml(cellString, ' ')
|
var noHtml = _stripHtml(cellString, ' ')
|
||||||
.replace( / /g, ' ' );
|
.replace( / /g, ' ' );
|
||||||
@@ -10304,7 +10323,7 @@
|
|||||||
* @type string
|
* @type string
|
||||||
* @default Version number
|
* @default Version number
|
||||||
*/
|
*/
|
||||||
DataTable.version = "2.3.7";
|
DataTable.version = "2.3.8";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Private data store, containing all of the settings objects that are
|
* Private data store, containing all of the settings objects that are
|
||||||
@@ -12586,6 +12605,7 @@
|
|||||||
var __mlWarning = false;
|
var __mlWarning = false;
|
||||||
var __luxon; // Can be assigned in DateTable.use()
|
var __luxon; // Can be assigned in DateTable.use()
|
||||||
var __moment; // Can be assigned in DateTable.use()
|
var __moment; // Can be assigned in DateTable.use()
|
||||||
|
var __reIsoTimezone = /[T\s]\d{2}.*?(Z|[+-]\d{2}(?::?\d{2})?)$/;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -12606,7 +12626,7 @@
|
|||||||
resolveWindowLibs();
|
resolveWindowLibs();
|
||||||
|
|
||||||
if (__moment) {
|
if (__moment) {
|
||||||
dt = __moment.utc( d, format, locale, true );
|
dt = __moment( d, format, locale, true );
|
||||||
|
|
||||||
if (! dt.isValid()) {
|
if (! dt.isValid()) {
|
||||||
return null;
|
return null;
|
||||||
@@ -12716,6 +12736,16 @@
|
|||||||
return d;
|
return d;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determine if there is a timezone. If there is, we want to reuse
|
||||||
|
// it for the output, so the timezone doesn't change between the
|
||||||
|
// input and output.
|
||||||
|
let options = {};
|
||||||
|
let tzMatch = typeof d === 'string' ? d.match(__reIsoTimezone) : null;
|
||||||
|
|
||||||
|
if (tzMatch) {
|
||||||
|
options.timeZone = tzMatch[1] === 'Z' ? 'UTC' : tzMatch[1];
|
||||||
|
}
|
||||||
|
|
||||||
var dt = __mldObj(d, from, locale);
|
var dt = __mldObj(d, from, locale);
|
||||||
|
|
||||||
if (dt === null) {
|
if (dt === null) {
|
||||||
@@ -12729,7 +12759,7 @@
|
|||||||
var formatted = to === null
|
var formatted = to === null
|
||||||
? __mld(dt, 'toDate', 'toJSDate', '')[localeString](
|
? __mld(dt, 'toDate', 'toJSDate', '')[localeString](
|
||||||
navigator.language,
|
navigator.language,
|
||||||
{ timeZone: "UTC" }
|
options
|
||||||
)
|
)
|
||||||
: __mld(dt, 'format', 'toFormat', 'toISOString', to);
|
: __mld(dt, 'format', 'toFormat', 'toISOString', to);
|
||||||
|
|
||||||
|
|||||||
+297
@@ -0,0 +1,297 @@
|
|||||||
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
|
pub(crate) fn join_path(base: &str, child: &str) -> String {
|
||||||
|
#[cfg(s3)]
|
||||||
|
if s3::is_uri(base) {
|
||||||
|
return s3::join_path(base, child);
|
||||||
|
}
|
||||||
|
|
||||||
|
let base = base.trim_end_matches('/');
|
||||||
|
let child = child.trim_start_matches('/');
|
||||||
|
if base.is_empty() {
|
||||||
|
child.to_string()
|
||||||
|
} else if child.is_empty() {
|
||||||
|
base.to_string()
|
||||||
|
} else {
|
||||||
|
format!("{base}/{child}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn with_extension(path: &str, extension: &str) -> String {
|
||||||
|
let extension = extension.trim_start_matches('.');
|
||||||
|
|
||||||
|
#[cfg(s3)]
|
||||||
|
if s3::is_uri(path) {
|
||||||
|
return s3::with_extension(path, extension);
|
||||||
|
}
|
||||||
|
|
||||||
|
format!("{path}.{extension}")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn parent(path: &str) -> Option<String> {
|
||||||
|
#[cfg(s3)]
|
||||||
|
if s3::is_uri(path) {
|
||||||
|
return s3::parent(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::path::Path::new(path).parent()?.to_str().map(ToString::to_string)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn file_name(path: &str) -> Option<String> {
|
||||||
|
#[cfg(s3)]
|
||||||
|
if s3::is_uri(path) {
|
||||||
|
return s3::file_name(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::path::Path::new(path).file_name()?.to_str().map(ToString::to_string)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_fs_operator(operator: &opendal::Operator) -> bool {
|
||||||
|
operator.info().scheme() == opendal::services::FS_SCHEME
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn operator_for_path(path: &str) -> Result<opendal::Operator, crate::Error> {
|
||||||
|
// Cache of previously built operators by path
|
||||||
|
static OPERATORS_BY_PATH: LazyLock<dashmap::DashMap<String, opendal::Operator>> =
|
||||||
|
LazyLock::new(dashmap::DashMap::new);
|
||||||
|
|
||||||
|
if let Some(operator) = OPERATORS_BY_PATH.get(path) {
|
||||||
|
return Ok(operator.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let operator = if path.starts_with("s3://") {
|
||||||
|
#[cfg(not(s3))]
|
||||||
|
return Err(opendal::Error::new(opendal::ErrorKind::ConfigInvalid, "S3 support is not enabled").into());
|
||||||
|
|
||||||
|
#[cfg(s3)]
|
||||||
|
s3::operator_for_path(path)?
|
||||||
|
} else {
|
||||||
|
let builder = opendal::services::Fs::default().root(path);
|
||||||
|
opendal::Operator::new(builder)?.finish()
|
||||||
|
};
|
||||||
|
|
||||||
|
OPERATORS_BY_PATH.insert(path.to_string(), operator.clone());
|
||||||
|
|
||||||
|
Ok(operator)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(s3)]
|
||||||
|
mod s3 {
|
||||||
|
use reqwest::Url;
|
||||||
|
|
||||||
|
use crate::error::Error;
|
||||||
|
|
||||||
|
pub(super) fn is_uri(path: &str) -> bool {
|
||||||
|
path.starts_with("s3://")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn join_path(base: &str, child: &str) -> String {
|
||||||
|
if let Ok(mut url) = Url::parse(base) {
|
||||||
|
let mut segments = path_segments(&url);
|
||||||
|
segments.extend(child.split('/').filter(|segment| !segment.is_empty()).map(ToString::to_string));
|
||||||
|
set_path_segments(&mut url, &segments);
|
||||||
|
return url.to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
let base = base.trim_end_matches('/');
|
||||||
|
let child = child.trim_start_matches('/');
|
||||||
|
if base.is_empty() {
|
||||||
|
child.to_string()
|
||||||
|
} else if child.is_empty() {
|
||||||
|
base.to_string()
|
||||||
|
} else {
|
||||||
|
format!("{base}/{child}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn with_extension(path: &str, extension: &str) -> String {
|
||||||
|
if let Ok(mut url) = Url::parse(path) {
|
||||||
|
let mut segments = path_segments(&url);
|
||||||
|
if let Some(file_name) = segments.last_mut() {
|
||||||
|
file_name.push('.');
|
||||||
|
file_name.push_str(extension);
|
||||||
|
set_path_segments(&mut url, &segments);
|
||||||
|
return url.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
format!("{path}.{extension}")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn parent(path: &str) -> Option<String> {
|
||||||
|
if let Ok(mut url) = Url::parse(path) {
|
||||||
|
let mut segments = path_segments(&url);
|
||||||
|
segments.pop()?;
|
||||||
|
set_path_segments(&mut url, &segments);
|
||||||
|
return Some(url.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::path::Path::new(path).parent()?.to_str().map(ToString::to_string)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn file_name(path: &str) -> Option<String> {
|
||||||
|
if let Ok(url) = Url::parse(path) {
|
||||||
|
return path_segments(&url).pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::path::Path::new(path).file_name()?.to_str().map(ToString::to_string)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path_segments(url: &Url) -> Vec<String> {
|
||||||
|
url.path_segments()
|
||||||
|
.map(|segments| segments.filter(|segment| !segment.is_empty()).map(ToString::to_string).collect())
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_path_segments(url: &mut Url, segments: &[String]) {
|
||||||
|
if segments.is_empty() {
|
||||||
|
url.set_path("");
|
||||||
|
} else {
|
||||||
|
url.set_path(&format!("/{}", segments.join("/")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn operator_for_path(path: &str) -> Result<opendal::Operator, Error> {
|
||||||
|
use crate::http_client::aws::AwsReqwestConnector;
|
||||||
|
use aws_config::{default_provider::credentials::DefaultCredentialsChain, provider_config::ProviderConfig};
|
||||||
|
use opendal::Configurator;
|
||||||
|
use reqsign_aws_v4::Credential;
|
||||||
|
use reqsign_core::{Context, ProvideCredential, ProvideCredentialChain};
|
||||||
|
|
||||||
|
// This is a custom AWS credential loader that uses the official AWS Rust
|
||||||
|
// SDK config crate to load credentials. This ensures maximum compatibility
|
||||||
|
// with AWS credential configurations. For example, OpenDAL doesn't support
|
||||||
|
// AWS SSO temporary credentials yet.
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct OpenDALS3CredentialProvider;
|
||||||
|
|
||||||
|
impl ProvideCredential for OpenDALS3CredentialProvider {
|
||||||
|
type Credential = Credential;
|
||||||
|
|
||||||
|
async fn provide_credential(&self, _ctx: &Context) -> reqsign_core::Result<Option<Self::Credential>> {
|
||||||
|
use aws_credential_types::provider::ProvideCredentials as _;
|
||||||
|
use reqsign_core::time::Timestamp;
|
||||||
|
use tokio::sync::OnceCell;
|
||||||
|
|
||||||
|
static DEFAULT_CREDENTIAL_CHAIN: OnceCell<DefaultCredentialsChain> = OnceCell::const_new();
|
||||||
|
|
||||||
|
let chain = DEFAULT_CREDENTIAL_CHAIN
|
||||||
|
.get_or_init(|| {
|
||||||
|
let reqwest_client = reqwest::Client::builder().build().unwrap();
|
||||||
|
let connector = AwsReqwestConnector {
|
||||||
|
client: reqwest_client,
|
||||||
|
};
|
||||||
|
|
||||||
|
let conf = ProviderConfig::default().with_http_client(connector);
|
||||||
|
|
||||||
|
DefaultCredentialsChain::builder().configure(conf).build()
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let creds = chain.provide_credentials().await.map_err(|e| {
|
||||||
|
reqsign_core::Error::unexpected("failed to load AWS credentials via AWS SDK").with_source(e)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let expires_in = if let Some(expiration) = creds.expiry() {
|
||||||
|
let duration = expiration.duration_since(std::time::UNIX_EPOCH).map_err(|e| {
|
||||||
|
reqsign_core::Error::unexpected("AWS credential expiration is before the Unix epoch")
|
||||||
|
.with_source(e)
|
||||||
|
})?;
|
||||||
|
let seconds = i64::try_from(duration.as_secs()).map_err(|e| {
|
||||||
|
reqsign_core::Error::unexpected("AWS credential expiration is too large").with_source(e)
|
||||||
|
})?;
|
||||||
|
Some(Timestamp::from_second(seconds)?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Some(Credential {
|
||||||
|
access_key_id: creds.access_key_id().to_string(),
|
||||||
|
secret_access_key: creds.secret_access_key().to_string(),
|
||||||
|
session_token: creds.session_token().map(|s| s.to_string()),
|
||||||
|
expires_in,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let uri = opendal::OperatorUri::new(path, std::iter::empty::<(String, String)>())?;
|
||||||
|
let mut config = opendal::services::S3Config::from_uri(&uri)?;
|
||||||
|
|
||||||
|
if !uri_has_option(&uri, &["default_storage_class"]) {
|
||||||
|
config.default_storage_class = Some("INTELLIGENT_TIERING".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if !uri_has_option(
|
||||||
|
&uri,
|
||||||
|
&["enable_virtual_host_style", "aws_virtual_hosted_style_request", "virtual_hosted_style_request"],
|
||||||
|
) {
|
||||||
|
config.enable_virtual_host_style = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let use_aws_sdk_credentials = !uri_has_credential_options(&uri, &config);
|
||||||
|
let mut builder = config.into_builder();
|
||||||
|
|
||||||
|
if use_aws_sdk_credentials {
|
||||||
|
builder =
|
||||||
|
builder.credential_provider_chain(ProvideCredentialChain::new().push(OpenDALS3CredentialProvider));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(opendal::Operator::new(builder)?.finish())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn uri_has_option(uri: &opendal::OperatorUri, names: &[&str]) -> bool {
|
||||||
|
names.iter().any(|name| uri.options().contains_key(*name))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn uri_has_credential_options(uri: &opendal::OperatorUri, config: &opendal::services::S3Config) -> bool {
|
||||||
|
config.access_key_id.is_some()
|
||||||
|
|| config.secret_access_key.is_some()
|
||||||
|
|| config.session_token.is_some()
|
||||||
|
|| config.role_arn.is_some()
|
||||||
|
|| config.external_id.is_some()
|
||||||
|
|| config.role_session_name.is_some()
|
||||||
|
|| uri_has_option(uri, &["allow_anonymous", "disable_config_load", "disable_ec2_metadata"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn handles_local_paths() {
|
||||||
|
assert_eq!(join_path("data", "attachments"), "data/attachments");
|
||||||
|
assert_eq!(with_extension("data/rsa_key", "pem"), "data/rsa_key.pem");
|
||||||
|
assert_eq!(parent("data/rsa_key.pem").as_deref(), Some("data"));
|
||||||
|
assert_eq!(file_name("data/rsa_key.pem").as_deref(), Some("rsa_key.pem"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(test, s3))]
|
||||||
|
mod s3_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn joins_s3_path_before_query_string() {
|
||||||
|
assert_eq!(
|
||||||
|
join_path("s3://bucket/base?region=us-west-2", "attachments"),
|
||||||
|
"s3://bucket/base/attachments?region=us-west-2"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn appends_extension_before_s3_query_string() {
|
||||||
|
assert_eq!(
|
||||||
|
with_extension("s3://bucket/base/rsa_key?region=us-west-2", "pem"),
|
||||||
|
"s3://bucket/base/rsa_key.pem?region=us-west-2"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn splits_s3_parent_and_file_name_without_query_string() {
|
||||||
|
let path = "s3://bucket/base/config.json?region=us-west-2";
|
||||||
|
|
||||||
|
assert_eq!(parent(path).as_deref(), Some("s3://bucket/base?region=us-west-2"));
|
||||||
|
assert_eq!(file_name(path).as_deref(), Some("config.json"));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user