mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2026-06-08 19:50:17 +03:00
Compare commits
4 Commits
9bc14e6e77
...
70f9dfbe8b
| Author | SHA1 | Date | |
|---|---|---|---|
| 70f9dfbe8b | |||
| 54895ad4be | |||
| a057c7deae | |||
| 2f85b62d2f |
+5
-4
@@ -50,10 +50,11 @@
|
||||
#########################
|
||||
|
||||
## Database URL
|
||||
## When using SQLite, this is the path to the DB file, and it defaults to
|
||||
## %DATA_FOLDER%/db.sqlite3. If DATA_FOLDER is set to an external location, this
|
||||
## must be set to a local sqlite3 file path.
|
||||
# DATABASE_URL=data/db.sqlite3
|
||||
## When using SQLite, this should use the sqlite:// scheme followed by the path
|
||||
## to the DB file. It defaults to sqlite://%DATA_FOLDER%/db.sqlite3.
|
||||
## Bare paths without the sqlite:// scheme are supported for backwards compatibility,
|
||||
## but only if the database file already exists.
|
||||
# DATABASE_URL=sqlite://data/db.sqlite3
|
||||
## When using MySQL, specify an appropriate connection URI.
|
||||
## Details: https://docs.diesel.rs/2.1.x/diesel/mysql/struct.MysqlConnection.html
|
||||
# DATABASE_URL=mysql://user:password@host[:port]/database_name
|
||||
|
||||
@@ -57,7 +57,6 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
||||
# Debian Trixie uses libpq v17
|
||||
PQ_LIB_DIR="/usr/local/musl/pq17/lib"
|
||||
|
||||
|
||||
# Create CARGO_HOME folder and don't download rust docs
|
||||
RUN mkdir -pv "${CARGO_HOME}" && \
|
||||
rustup set profile minimal
|
||||
|
||||
+11
-33
@@ -51,7 +51,7 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
||||
TERM=xterm-256color \
|
||||
CARGO_HOME="/root/.cargo" \
|
||||
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 git so build.rs can determine the correct version
|
||||
# Install the libc cross packages based upon the debian-arch
|
||||
@@ -59,19 +59,16 @@ RUN apt-get update && \
|
||||
apt-get install -y \
|
||||
--no-install-recommends \
|
||||
clang \
|
||||
pkg-config \
|
||||
git \
|
||||
"libc6-$(xx-info debian-arch)-cross" \
|
||||
"libc6-dev-$(xx-info debian-arch)-cross" \
|
||||
"linux-libc-dev-$(xx-info debian-arch)-cross" && \
|
||||
git && \
|
||||
xx-apt-get install -y \
|
||||
--no-install-recommends \
|
||||
gcc \
|
||||
libpq-dev \
|
||||
libpq5 \
|
||||
libssl-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
|
||||
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
|
||||
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 && \
|
||||
rustup target add "${CARGO_TARGET}"
|
||||
|
||||
@@ -122,7 +96,9 @@ ARG DB=sqlite,mysql,postgresql
|
||||
# dummy project, except the target folder
|
||||
# This folder contains the compiled dependencies
|
||||
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
|
||||
|
||||
# Copies the complete project
|
||||
@@ -137,7 +113,9 @@ RUN source /env-cargo && \
|
||||
# Also do this for build.rs to ensure the version is rechecked
|
||||
touch build.rs src/main.rs && \
|
||||
# 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 \
|
||||
ln -vfsr "/app/target/${CARGO_TARGET}/debug" /app/target/final ; \
|
||||
else \
|
||||
|
||||
+20
-34
@@ -27,6 +27,11 @@
|
||||
# $ docker image inspect --format "{{ '{{' }}.RepoTags}}" docker.io/vaultwarden/web-vault@{{ vault_image_digest }}
|
||||
# [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
|
||||
|
||||
{% if base == "debian" %}
|
||||
@@ -66,10 +71,10 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
||||
# Use PostgreSQL v17 during Alpine/MUSL builds instead of the default v16
|
||||
# Debian Trixie uses libpq v17
|
||||
PQ_LIB_DIR="/usr/local/musl/pq17/lib"
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
|
||||
{% 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 git so build.rs can determine the correct version
|
||||
# Install the libc cross packages based upon the debian-arch
|
||||
@@ -77,19 +82,16 @@ RUN apt-get update && \
|
||||
apt-get install -y \
|
||||
--no-install-recommends \
|
||||
clang \
|
||||
pkg-config \
|
||||
git \
|
||||
"libc6-$(xx-info debian-arch)-cross" \
|
||||
"libc6-dev-$(xx-info debian-arch)-cross" \
|
||||
"linux-libc-dev-$(xx-info debian-arch)-cross" && \
|
||||
git && \
|
||||
xx-apt-get install -y \
|
||||
--no-install-recommends \
|
||||
gcc \
|
||||
libpq-dev \
|
||||
libpq5 \
|
||||
libssl-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
|
||||
echo "export CARGO_TARGET=$(xx-cargo --print-target-triple)" >> /env-cargo
|
||||
{% endif %}
|
||||
@@ -102,31 +104,7 @@ RUN mkdir -pv "${CARGO_HOME}" && \
|
||||
RUN USER=root cargo new --bin /app
|
||||
WORKDIR /app
|
||||
|
||||
{% if base == "debian" %}
|
||||
# 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" %}
|
||||
{% if base == "alpine" %}
|
||||
# Environment variables for Cargo on Alpine based builds
|
||||
RUN echo "export CARGO_TARGET=${RUST_MUSL_CROSS_TARGET}" >> /env-cargo && \
|
||||
# Output the current contents of the file
|
||||
@@ -154,7 +132,11 @@ ARG DB=sqlite,mysql,postgresql,enable_mimalloc
|
||||
# dummy project, except the target folder
|
||||
# This folder contains the compiled dependencies
|
||||
RUN source /env-cargo && \
|
||||
{% if base == "debian" %}
|
||||
{{ xx_cargo_config() }} && \
|
||||
{% elif base == "alpine" %}
|
||||
cargo build --features ${DB} --profile "${CARGO_PROFILE}" --target="${CARGO_TARGET}" && \
|
||||
{% endif %}
|
||||
find . -not -path "./target*" -delete
|
||||
|
||||
# Copies the complete project
|
||||
@@ -169,7 +151,11 @@ RUN source /env-cargo && \
|
||||
# Also do this for build.rs to ensure the version is rechecked
|
||||
touch build.rs src/main.rs && \
|
||||
# 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}" && \
|
||||
{% endif %}
|
||||
if [[ "${CARGO_PROFILE}" == "dev" ]] ; then \
|
||||
ln -vfsr "/app/target/${CARGO_TARGET}/debug" /app/target/final ; \
|
||||
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;
|
||||
@@ -25,7 +25,7 @@ pub fn routes() -> Vec<Route> {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SendEmailLoginData {
|
||||
#[serde(alias = "DeviceIdentifier")]
|
||||
device_identifier: DeviceId,
|
||||
device_identifier: Option<DeviceId>,
|
||||
#[serde(alias = "Email")]
|
||||
email: Option<String>,
|
||||
#[serde(alias = "MasterPasswordHash")]
|
||||
@@ -91,8 +91,11 @@ async fn send_email_login(data: Json<SendEmailLoginData>, client_headers: Client
|
||||
|
||||
user
|
||||
} 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
|
||||
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.")
|
||||
};
|
||||
|
||||
|
||||
+19
-23
@@ -28,8 +28,9 @@ use crate::{
|
||||
crypto,
|
||||
db::{
|
||||
models::{
|
||||
AuthRequest, AuthRequestId, Device, DeviceId, EventType, Invitation, OIDCCodeWrapper, OrganizationApiKey,
|
||||
OrganizationId, SsoAuth, SsoUser, TwoFactor, TwoFactorIncomplete, TwoFactorType, User, UserId,
|
||||
AuthRequest, AuthRequestId, Device, DeviceId, EventType, Invitation, OIDCCodeResponseError,
|
||||
OrganizationApiKey, OrganizationId, SsoAuth, SsoUser, TwoFactor, TwoFactorIncomplete, TwoFactorType, User,
|
||||
UserId,
|
||||
},
|
||||
DbConn,
|
||||
},
|
||||
@@ -186,7 +187,7 @@ async fn _sso_login(
|
||||
// Ratelimit the login
|
||||
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!(
|
||||
"Got no code in OIDC data",
|
||||
ErrorEvent {
|
||||
@@ -202,7 +203,7 @@ async fn _sso_login(
|
||||
(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 {
|
||||
None => match SsoUser::find_by_mail(&user_infos.email, conn).await {
|
||||
None => None,
|
||||
@@ -1138,7 +1139,7 @@ struct ConnectData {
|
||||
|
||||
// Needed for authorization code
|
||||
#[field(name = uncased("code"))]
|
||||
code: Option<OIDCState>,
|
||||
code: Option<OIDCCode>,
|
||||
#[field(name = uncased("code_verifier"))]
|
||||
code_verifier: Option<OIDCCodeVerifier>,
|
||||
}
|
||||
@@ -1165,19 +1166,12 @@ const SSO_BINDING_COOKIE: &str = "VW_SSO_BINDING";
|
||||
|
||||
#[get("/connect/oidc-signin?<code>&<state>", rank = 1)]
|
||||
async fn oidcsignin(code: OIDCCode, state: String, cookies: &CookieJar<'_>, mut conn: DbConn) -> ApiResult<Redirect> {
|
||||
_oidcsignin_redirect(
|
||||
state,
|
||||
OIDCCodeWrapper::Ok {
|
||||
code,
|
||||
},
|
||||
cookies,
|
||||
&mut conn,
|
||||
)
|
||||
.await
|
||||
_oidcsignin_redirect(state, code, None, cookies, &mut conn).await
|
||||
}
|
||||
|
||||
// Bitwarden client appear to only care for code and state so we pipe it through
|
||||
// cf: https://github.com/bitwarden/clients/blob/80b74b3300e15b4ae414dc06044cc9b02b6c10a6/libs/auth/src/angular/sso/sso.component.ts#L141
|
||||
// Bitwarden client appear to only care for code and state
|
||||
// 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)]
|
||||
async fn oidcsignin_error(
|
||||
state: String,
|
||||
@@ -1187,11 +1181,12 @@ async fn oidcsignin_error(
|
||||
mut conn: DbConn,
|
||||
) -> ApiResult<Redirect> {
|
||||
_oidcsignin_redirect(
|
||||
state,
|
||||
OIDCCodeWrapper::Error {
|
||||
state.clone(),
|
||||
state.into(),
|
||||
Some(OIDCCodeResponseError {
|
||||
error,
|
||||
error_description,
|
||||
},
|
||||
}),
|
||||
cookies,
|
||||
&mut conn,
|
||||
)
|
||||
@@ -1200,10 +1195,10 @@ async fn oidcsignin_error(
|
||||
|
||||
// The state was encoded using Base64 to ensure no issue with providers.
|
||||
// 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(
|
||||
base64_state: String,
|
||||
code_response: OIDCCodeWrapper,
|
||||
code: OIDCCode,
|
||||
error: Option<OIDCCodeResponseError>,
|
||||
cookies: &CookieJar<'_>,
|
||||
conn: &mut DbConn,
|
||||
) -> ApiResult<Redirect> {
|
||||
@@ -1225,7 +1220,8 @@ async fn _oidcsignin_redirect(
|
||||
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.save(conn).await?;
|
||||
|
||||
@@ -1235,7 +1231,7 @@ async fn _oidcsignin_redirect(
|
||||
};
|
||||
|
||||
url.query_pairs_mut()
|
||||
.append_pair("code", &state)
|
||||
.append_pair("code", &code)
|
||||
.append_pair("state", &state)
|
||||
.append_pair("scope", &AuthMethod::Sso.scope())
|
||||
.append_pair("iss", &CONFIG.domain());
|
||||
|
||||
+12
-9
@@ -504,7 +504,7 @@ make_config! {
|
||||
/// Data folder |> Main data folder
|
||||
data_folder: String, false, def, "data".to_string();
|
||||
/// Database URL
|
||||
database_url: String, false, auto, |c| storage::join_path(&c.data_folder, "db.sqlite3");
|
||||
database_url: String, false, auto, |c| format!("sqlite://{}", storage::join_path(&c.data_folder, "db.sqlite3"));
|
||||
/// Icon cache folder
|
||||
icon_cache_folder: String, false, auto, |c| storage::join_path(&c.data_folder, "icon_cache");
|
||||
/// Attachments folder
|
||||
@@ -926,14 +926,17 @@ fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> {
|
||||
{
|
||||
use crate::db::DbConnType;
|
||||
let url = &cfg.database_url;
|
||||
if DbConnType::from_url(url)? == DbConnType::Sqlite && url.contains('/') {
|
||||
let path = std::path::Path::new(&url);
|
||||
if let Some(parent) = path.parent() {
|
||||
if !parent.is_dir() {
|
||||
err!(format!(
|
||||
"SQLite database directory `{}` does not exist or is not a directory",
|
||||
parent.display()
|
||||
));
|
||||
if DbConnType::from_url(url)? == DbConnType::Sqlite {
|
||||
let file_path = url.strip_prefix("sqlite://").unwrap_or(url);
|
||||
if file_path.contains('/') {
|
||||
let path = std::path::Path::new(file_path);
|
||||
if let Some(parent) = path.parent() {
|
||||
if !parent.is_dir() {
|
||||
err!(format!(
|
||||
"SQLite database directory `{}` does not exist or is not a directory",
|
||||
parent.display()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+27
-7
@@ -272,13 +272,32 @@ impl DbConnType {
|
||||
#[cfg(not(postgresql))]
|
||||
err!("`DATABASE_URL` is a PostgreSQL URL, but the 'postgresql' feature is not enabled")
|
||||
|
||||
//Sqlite
|
||||
} else {
|
||||
// Sqlite (explicit)
|
||||
} else if url.len() > 7 && &url[..7] == "sqlite:" {
|
||||
#[cfg(sqlite)]
|
||||
return Ok(DbConnType::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();
|
||||
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
|
||||
// This way we can set a readonly flag on the opening mode without issues.
|
||||
let mut conn = diesel::sqlite::SqliteConnection::establish(&format!("sqlite://{db_url}?mode=ro"))?;
|
||||
// Strip the sqlite:// prefix if present to get the raw file path
|
||||
let file_path = db_url.strip_prefix("sqlite://").unwrap_or(&db_url);
|
||||
// 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
|
||||
.join(format!("db_{}.sqlite3", chrono::Utc::now().format("%Y%m%d_%H%M%S")))
|
||||
.to_string_lossy()
|
||||
|
||||
@@ -38,7 +38,7 @@ pub use self::send::{
|
||||
id::{SendFileId, SendId},
|
||||
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_duo_context::TwoFactorDuoContext;
|
||||
pub use self::two_factor_incomplete::TwoFactorIncomplete;
|
||||
|
||||
+18
-10
@@ -15,17 +15,12 @@ use diesel::sql_types::Text;
|
||||
|
||||
#[derive(AsExpression, Clone, Debug, Serialize, Deserialize, FromSqlRow)]
|
||||
#[diesel(sql_type = Text)]
|
||||
pub enum OIDCCodeWrapper {
|
||||
Ok {
|
||||
code: OIDCCode,
|
||||
},
|
||||
Error {
|
||||
error: String,
|
||||
error_description: Option<String>,
|
||||
},
|
||||
pub struct OIDCCodeResponseError {
|
||||
pub error: String,
|
||||
pub error_description: Option<String>,
|
||||
}
|
||||
|
||||
impl_FromToSqlText!(OIDCCodeWrapper);
|
||||
impl_FromToSqlText!(OIDCCodeResponseError);
|
||||
|
||||
#[derive(AsExpression, Clone, Debug, Serialize, Deserialize, FromSqlRow)]
|
||||
#[diesel(sql_type = Text)]
|
||||
@@ -50,7 +45,8 @@ pub struct SsoAuth {
|
||||
pub client_challenge: OIDCCodeChallenge,
|
||||
pub nonce: 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 created_at: NaiveDateTime,
|
||||
pub updated_at: NaiveDateTime,
|
||||
@@ -76,6 +72,7 @@ impl SsoAuth {
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
code_response: None,
|
||||
code_response_error: None,
|
||||
auth_response: None,
|
||||
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 {
|
||||
db_run! {conn: {
|
||||
diesel::delete(sso_auth::table.filter(sso_auth::state.eq(self.state)))
|
||||
|
||||
@@ -262,6 +262,7 @@ table! {
|
||||
nonce -> Text,
|
||||
redirect_uri -> Text,
|
||||
code_response -> Nullable<Text>,
|
||||
code_response_error -> Nullable<Text>,
|
||||
auth_response -> Nullable<Text>,
|
||||
created_at -> Timestamp,
|
||||
updated_at -> Timestamp,
|
||||
|
||||
+14
-14
@@ -10,7 +10,7 @@ use crate::{
|
||||
auth,
|
||||
auth::{AuthMethod, AuthTokens, TokenWrapper, BW_EXPIRATION, DEFAULT_REFRESH_VALIDITY},
|
||||
db::{
|
||||
models::{Device, OIDCAuthenticatedUser, OIDCCodeWrapper, SsoAuth, SsoUser, User},
|
||||
models::{Device, OIDCAuthenticatedUser, SsoAuth, SsoUser, User},
|
||||
DbConn,
|
||||
},
|
||||
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.
|
||||
// The `SsoAuth` will ensure that the user is authorized only once.
|
||||
pub async fn exchange_code(
|
||||
state: &OIDCState,
|
||||
code: &OIDCCode,
|
||||
client_verifier: OIDCCodeVerifier,
|
||||
conn: &DbConn,
|
||||
) -> ApiResult<(SsoAuth, OIDCAuthenticatedUser)> {
|
||||
use openidconnect::OAuth2TokenResponse;
|
||||
|
||||
let mut sso_auth = match SsoAuth::find(state, conn).await {
|
||||
None => err!(format!("Invalid state cannot retrieve sso auth")),
|
||||
let mut sso_auth = match SsoAuth::find_by_code(code, conn).await {
|
||||
None => err!(format!("Invalid code cannot retrieve sso auth")),
|
||||
Some(sso_auth) => sso_auth,
|
||||
};
|
||||
|
||||
@@ -255,18 +255,18 @@ pub async fn exchange_code(
|
||||
return Ok((sso_auth, authenticated_user));
|
||||
}
|
||||
|
||||
let code = match sso_auth.code_response.clone() {
|
||||
Some(OIDCCodeWrapper::Ok {
|
||||
code,
|
||||
}) => code.clone(),
|
||||
Some(OIDCCodeWrapper::Error {
|
||||
error,
|
||||
error_description,
|
||||
}) => {
|
||||
let code = match (sso_auth.code_response.clone(), sso_auth.code_response_error.as_ref()) {
|
||||
(Some(code), None) => code,
|
||||
(_, Some(re)) => {
|
||||
let error_msg = format!(
|
||||
"SSO authorization failed: {}, {}",
|
||||
re.error,
|
||||
re.error_description.as_ref().unwrap_or(&String::new())
|
||||
);
|
||||
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?;
|
||||
err!("Missing authorization provider return");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user