forked from trashmodern/vaultwarden
Compare commits
12 Commits
test_dylint
...
1.32.5
| Author | SHA1 | Date | |
|---|---|---|---|
| cdfdc6ff4f | |||
| 2393c3f3c0 | |||
| 0d16b38a68 | |||
| ff33534c07 | |||
| adb21d5c1a | |||
| e927b8aa5e | |||
| ba48ca68fc | |||
| 294b429436 | |||
| 37c14c3c69 | |||
| d0581da638 | |||
| 38aad4f7be | |||
| 20d9e885bf |
+6
-3
@@ -280,12 +280,13 @@
|
||||
## The default for new users. If changed, it will be updated during login for existing users.
|
||||
# PASSWORD_ITERATIONS=600000
|
||||
|
||||
## Controls whether users can set password hints. This setting applies globally to all users.
|
||||
## Controls whether users can set or show password hints. This setting applies globally to all users.
|
||||
# PASSWORD_HINTS_ALLOWED=true
|
||||
|
||||
## Controls whether a password hint should be shown directly in the web page if
|
||||
## SMTP service is not configured. Not recommended for publicly-accessible instances
|
||||
## as this provides unauthenticated access to potentially sensitive data.
|
||||
## SMTP service is not configured and password hints are allowed.
|
||||
## Not recommended for publicly-accessible instances because this provides
|
||||
## unauthenticated access to potentially sensitive data.
|
||||
# SHOW_PASSWORD_HINT=false
|
||||
|
||||
#########################
|
||||
@@ -349,6 +350,8 @@
|
||||
## - "browser-fileless-import": Directly import credentials from other providers without a file.
|
||||
## - "extension-refresh": Temporarily enable the new extension design until general availability (should be used with the beta Chrome extension)
|
||||
## - "fido2-vault-credentials": Enable the use of FIDO2 security keys as second factor.
|
||||
## - "ssh-key-vault-item": Enable the creation and use of SSH key vault items. (Needs clients >=2024.12.0)
|
||||
## - "ssh-agent": Enable SSH agent support on Desktop. (Needs desktop >=2024.12.0)
|
||||
# EXPERIMENTAL_CLIENT_FEATURE_FLAGS=fido2-vault-credentials
|
||||
|
||||
## Require new device emails. When a user logs in an email is required to be sent.
|
||||
|
||||
Generated
+152
-78
File diff suppressed because it is too large
Load Diff
+14
-11
@@ -53,7 +53,7 @@ once_cell = "1.20.2"
|
||||
# Numerical libraries
|
||||
num-traits = "0.2.19"
|
||||
num-derive = "0.4.2"
|
||||
bigdecimal = "0.4.5"
|
||||
bigdecimal = "0.4.6"
|
||||
|
||||
# Web framework
|
||||
rocket = { version = "0.5.1", features = ["tls", "json"], default-features = false }
|
||||
@@ -67,10 +67,10 @@ dashmap = "6.1.0"
|
||||
|
||||
# Async futures
|
||||
futures = "0.3.31"
|
||||
tokio = { version = "1.41.0", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] }
|
||||
tokio = { version = "1.41.1", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] }
|
||||
|
||||
# A generic serialization/deserialization framework
|
||||
serde = { version = "1.0.213", features = ["derive"] }
|
||||
serde = { version = "1.0.214", features = ["derive"] }
|
||||
serde_json = "1.0.132"
|
||||
|
||||
# A safe, extensible ORM and Query builder
|
||||
@@ -112,7 +112,7 @@ yubico = { version = "0.11.0", features = ["online-tokio"], default-features = f
|
||||
webauthn-rs = "0.3.2"
|
||||
|
||||
# Handling of URL's for WebAuthn and favicons
|
||||
url = "2.5.2"
|
||||
url = "2.5.3"
|
||||
|
||||
# Email libraries
|
||||
lettre = { version = "0.11.10", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "tokio1-native-tls", "hostname", "tracing", "tokio1"], default-features = false }
|
||||
@@ -120,24 +120,24 @@ percent-encoding = "2.3.1" # URL encoding library used for URL's in the emails
|
||||
email_address = "0.2.9"
|
||||
|
||||
# HTML Template library
|
||||
handlebars = { version = "6.1.0", features = ["dir_source"] }
|
||||
handlebars = { version = "6.2.0", features = ["dir_source"] }
|
||||
|
||||
# HTTP client (Used for favicons, version check, DUO and HIBP API)
|
||||
reqwest = { version = "0.12.8", features = ["native-tls-alpn", "stream", "json", "gzip", "brotli", "socks", "cookies"] }
|
||||
reqwest = { version = "0.12.9", features = ["native-tls-alpn", "stream", "json", "gzip", "brotli", "socks", "cookies"] }
|
||||
hickory-resolver = "0.24.1"
|
||||
|
||||
# Favicon extraction libraries
|
||||
html5gum = "0.5.7"
|
||||
regex = { version = "1.11.0", features = ["std", "perf", "unicode-perl"], default-features = false }
|
||||
html5gum = "0.6.1"
|
||||
regex = { version = "1.11.1", features = ["std", "perf", "unicode-perl"], default-features = false }
|
||||
data-url = "0.3.1"
|
||||
bytes = "1.8.0"
|
||||
|
||||
# Cache function results (Used for version check and favicon fetching)
|
||||
cached = { version = "0.53.1", features = ["async"] }
|
||||
cached = { version = "0.54.0", features = ["async"] }
|
||||
|
||||
# Used for custom short lived cookie jar during favicon extraction
|
||||
cookie = "0.18.1"
|
||||
cookie_store = "0.21.0"
|
||||
cookie_store = "0.21.1"
|
||||
|
||||
# Used by U2F, JWT and PostgreSQL
|
||||
openssl = "0.10.68"
|
||||
@@ -155,7 +155,7 @@ semver = "1.0.23"
|
||||
# Allow overriding the default memory allocator
|
||||
# Mainly used for the musl builds, since the default musl malloc is very slow
|
||||
mimalloc = { version = "0.1.43", features = ["secure"], default-features = false, optional = true }
|
||||
which = "6.0.3"
|
||||
which = "7.0.0"
|
||||
|
||||
# Argon2 library with support for the PHC format
|
||||
argon2 = "0.5.3"
|
||||
@@ -163,6 +163,9 @@ argon2 = "0.5.3"
|
||||
# Reading a password from the cli for generating the Argon2id ADMIN_TOKEN
|
||||
rpassword = "7.3.1"
|
||||
|
||||
# Loading a dynamic CSS Stylesheet
|
||||
grass_compiler = { version = "0.13.4", default-features = false }
|
||||
|
||||
# Strip debuginfo from the release builds
|
||||
# The symbols are the provide better panic traces
|
||||
# Also enable fat LTO and use 1 codegen unit for optimizations
|
||||
|
||||
+192
-118
File diff suppressed because it is too large
Load Diff
+28
-8
@@ -10,6 +10,7 @@ use rocket::{
|
||||
};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::auth::ClientVersion;
|
||||
use crate::util::NumberOrString;
|
||||
use crate::{
|
||||
api::{self, core::log_event, EmptyResult, JsonResult, Notify, PasswordOrOtpData, UpdateType},
|
||||
@@ -104,11 +105,27 @@ struct SyncData {
|
||||
}
|
||||
|
||||
#[get("/sync?<data..>")]
|
||||
async fn sync(data: SyncData, headers: Headers, mut conn: DbConn) -> Json<Value> {
|
||||
async fn sync(
|
||||
data: SyncData,
|
||||
headers: Headers,
|
||||
client_version: Option<ClientVersion>,
|
||||
mut conn: DbConn,
|
||||
) -> Json<Value> {
|
||||
let user_json = headers.user.to_json(&mut conn).await;
|
||||
|
||||
// Get all ciphers which are visible by the user
|
||||
let ciphers = Cipher::find_by_user_visible(&headers.user.uuid, &mut conn).await;
|
||||
let mut ciphers = Cipher::find_by_user_visible(&headers.user.uuid, &mut conn).await;
|
||||
|
||||
// Filter out SSH keys if the client version is less than 2024.12.0
|
||||
let show_ssh_keys = if let Some(client_version) = client_version {
|
||||
let ver_match = semver::VersionReq::parse(">=2024.12.0").unwrap();
|
||||
ver_match.matches(&client_version.0)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if !show_ssh_keys {
|
||||
ciphers.retain(|c| c.atype != 5);
|
||||
}
|
||||
|
||||
let cipher_sync_data = CipherSyncData::new(&headers.user.uuid, CipherSyncType::User, &mut conn).await;
|
||||
|
||||
@@ -205,7 +222,7 @@ pub struct CipherData {
|
||||
// Id is optional as it is included only in bulk share
|
||||
pub id: Option<String>,
|
||||
// Folder id is not included in import
|
||||
folder_id: Option<String>,
|
||||
pub folder_id: Option<String>,
|
||||
// TODO: Some of these might appear all the time, no need for Option
|
||||
#[serde(alias = "organizationID")]
|
||||
pub organization_id: Option<String>,
|
||||
@@ -216,7 +233,8 @@ pub struct CipherData {
|
||||
Login = 1,
|
||||
SecureNote = 2,
|
||||
Card = 3,
|
||||
Identity = 4
|
||||
Identity = 4,
|
||||
SshKey = 5
|
||||
*/
|
||||
pub r#type: i32,
|
||||
pub name: String,
|
||||
@@ -228,6 +246,7 @@ pub struct CipherData {
|
||||
secure_note: Option<Value>,
|
||||
card: Option<Value>,
|
||||
identity: Option<Value>,
|
||||
ssh_key: Option<Value>,
|
||||
|
||||
favorite: Option<bool>,
|
||||
reprompt: Option<i32>,
|
||||
@@ -469,6 +488,7 @@ pub async fn update_cipher_from_data(
|
||||
2 => data.secure_note,
|
||||
3 => data.card,
|
||||
4 => data.identity,
|
||||
5 => data.ssh_key,
|
||||
_ => err!("Invalid type"),
|
||||
};
|
||||
|
||||
@@ -565,11 +585,11 @@ async fn post_ciphers_import(
|
||||
Cipher::validate_cipher_data(&data.ciphers)?;
|
||||
|
||||
// Read and create the folders
|
||||
let existing_folders: Vec<String> =
|
||||
Folder::find_by_user(&headers.user.uuid, &mut conn).await.into_iter().map(|f| f.uuid).collect();
|
||||
let existing_folders: HashSet<Option<String>> =
|
||||
Folder::find_by_user(&headers.user.uuid, &mut conn).await.into_iter().map(|f| Some(f.uuid)).collect();
|
||||
let mut folders: Vec<String> = Vec::with_capacity(data.folders.len());
|
||||
for folder in data.folders.into_iter() {
|
||||
let folder_uuid = if folder.id.is_some() && existing_folders.contains(folder.id.as_ref().unwrap()) {
|
||||
let folder_uuid = if existing_folders.contains(&folder.id) {
|
||||
folder.id.unwrap()
|
||||
} else {
|
||||
let mut new_folder = Folder::new(headers.user.uuid.clone(), folder.name);
|
||||
@@ -581,8 +601,8 @@ async fn post_ciphers_import(
|
||||
}
|
||||
|
||||
// Read the relations between folders and ciphers
|
||||
// Ciphers can only be in one folder at the same time
|
||||
let mut relations_map = HashMap::with_capacity(data.folder_relationships.len());
|
||||
|
||||
for relation in data.folder_relationships {
|
||||
relations_map.insert(relation.key, relation.value);
|
||||
}
|
||||
|
||||
+6
-5
@@ -135,12 +135,13 @@ async fn put_eq_domains(data: Json<EquivDomainData>, headers: Headers, conn: DbC
|
||||
}
|
||||
|
||||
#[get("/hibp/breach?<username>")]
|
||||
async fn hibp_breach(username: &str) -> JsonResult {
|
||||
let url = format!(
|
||||
"https://haveibeenpwned.com/api/v3/breachedaccount/{username}?truncateResponse=false&includeUnverified=false"
|
||||
);
|
||||
|
||||
async fn hibp_breach(username: &str, _headers: Headers) -> JsonResult {
|
||||
let username: String = url::form_urlencoded::byte_serialize(username.as_bytes()).collect();
|
||||
if let Some(api_key) = crate::CONFIG.hibp_api_key() {
|
||||
let url = format!(
|
||||
"https://haveibeenpwned.com/api/v3/breachedaccount/{username}?truncateResponse=false&includeUnverified=false"
|
||||
);
|
||||
|
||||
let res = make_http_request(Method::GET, &url)?.header("hibp-api-key", api_key).send().await?;
|
||||
|
||||
// If we get a 404, return a 404, it means no breached accounts
|
||||
|
||||
@@ -9,9 +9,8 @@ use crate::{
|
||||
core::{log_event, two_factor, CipherSyncData, CipherSyncType},
|
||||
EmptyResult, JsonResult, Notify, PasswordOrOtpData, UpdateType,
|
||||
},
|
||||
auth::{decode_invite, AdminHeaders, Headers, ManagerHeaders, ManagerHeadersLoose, OwnerHeaders},
|
||||
auth::{decode_invite, AdminHeaders, ClientVersion, Headers, ManagerHeaders, ManagerHeadersLoose, OwnerHeaders},
|
||||
db::{models::*, DbConn},
|
||||
error::Error,
|
||||
mail,
|
||||
util::{convert_json_key_lcase_first, NumberOrString},
|
||||
CONFIG,
|
||||
@@ -127,6 +126,7 @@ struct NewCollectionData {
|
||||
name: String,
|
||||
groups: Vec<NewCollectionObjectData>,
|
||||
users: Vec<NewCollectionObjectData>,
|
||||
id: Option<String>,
|
||||
external_id: Option<String>,
|
||||
}
|
||||
|
||||
@@ -1598,40 +1598,43 @@ async fn post_org_import(
|
||||
// TODO: See if we can optimize the whole cipher adding/importing and prevent duplicate code and checks.
|
||||
Cipher::validate_cipher_data(&data.ciphers)?;
|
||||
|
||||
let mut collections = Vec::new();
|
||||
let existing_collections: HashSet<Option<String>> =
|
||||
Collection::find_by_organization(&org_id, &mut conn).await.into_iter().map(|c| (Some(c.uuid))).collect();
|
||||
let mut collections: Vec<String> = Vec::with_capacity(data.collections.len());
|
||||
for coll in data.collections {
|
||||
let collection = Collection::new(org_id.clone(), coll.name, coll.external_id);
|
||||
if collection.save(&mut conn).await.is_err() {
|
||||
collections.push(Err(Error::new("Failed to create Collection", "Failed to create Collection")));
|
||||
let collection_uuid = if existing_collections.contains(&coll.id) {
|
||||
coll.id.unwrap()
|
||||
} else {
|
||||
collections.push(Ok(collection));
|
||||
}
|
||||
let new_collection = Collection::new(org_id.clone(), coll.name, coll.external_id);
|
||||
new_collection.save(&mut conn).await?;
|
||||
new_collection.uuid
|
||||
};
|
||||
|
||||
collections.push(collection_uuid);
|
||||
}
|
||||
|
||||
// Read the relations between collections and ciphers
|
||||
let mut relations = Vec::new();
|
||||
// Ciphers can be in multiple collections at the same time
|
||||
let mut relations = Vec::with_capacity(data.collection_relationships.len());
|
||||
for relation in data.collection_relationships {
|
||||
relations.push((relation.key, relation.value));
|
||||
}
|
||||
|
||||
let headers: Headers = headers.into();
|
||||
|
||||
let mut ciphers = Vec::new();
|
||||
for cipher_data in data.ciphers {
|
||||
let mut ciphers: Vec<String> = Vec::with_capacity(data.ciphers.len());
|
||||
for mut cipher_data in data.ciphers {
|
||||
// Always clear folder_id's via an organization import
|
||||
cipher_data.folder_id = None;
|
||||
let mut cipher = Cipher::new(cipher_data.r#type, cipher_data.name.clone());
|
||||
update_cipher_from_data(&mut cipher, cipher_data, &headers, None, &mut conn, &nt, UpdateType::None).await.ok();
|
||||
ciphers.push(cipher);
|
||||
ciphers.push(cipher.uuid);
|
||||
}
|
||||
|
||||
// Assign the collections
|
||||
for (cipher_index, coll_index) in relations {
|
||||
let cipher_id = &ciphers[cipher_index].uuid;
|
||||
let coll = &collections[coll_index];
|
||||
let coll_id = match coll {
|
||||
Ok(coll) => coll.uuid.as_str(),
|
||||
Err(_) => err!("Failed to assign to collection"),
|
||||
};
|
||||
|
||||
let cipher_id = &ciphers[cipher_index];
|
||||
let coll_id = &collections[coll_index];
|
||||
CollectionCipher::save(cipher_id, coll_id, &mut conn).await?;
|
||||
}
|
||||
|
||||
@@ -2305,14 +2308,14 @@ async fn _restore_organization_user(
|
||||
}
|
||||
|
||||
#[get("/organizations/<org_id>/groups")]
|
||||
async fn get_groups(org_id: &str, headers: ManagerHeadersLoose, mut conn: DbConn) -> JsonResult {
|
||||
async fn get_groups(org_id: &str, _headers: ManagerHeadersLoose, mut conn: DbConn) -> JsonResult {
|
||||
let groups: Vec<Value> = if CONFIG.org_groups_enabled() {
|
||||
// Group::find_by_organization(&org_id, &mut conn).await.iter().map(Group::to_json).collect::<Value>()
|
||||
let groups = Group::find_by_organization(org_id, &mut conn).await;
|
||||
let mut groups_json = Vec::with_capacity(groups.len());
|
||||
|
||||
for g in groups {
|
||||
groups_json.push(g.to_json_details(&headers.org_user.atype, &mut conn).await)
|
||||
groups_json.push(g.to_json_details(&mut conn).await)
|
||||
}
|
||||
groups_json
|
||||
} else {
|
||||
@@ -2500,7 +2503,7 @@ async fn add_update_group(
|
||||
}
|
||||
|
||||
#[get("/organizations/<_org_id>/groups/<group_id>/details")]
|
||||
async fn get_group_details(_org_id: &str, group_id: &str, headers: AdminHeaders, mut conn: DbConn) -> JsonResult {
|
||||
async fn get_group_details(_org_id: &str, group_id: &str, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult {
|
||||
if !CONFIG.org_groups_enabled() {
|
||||
err!("Group support is disabled");
|
||||
}
|
||||
@@ -2510,7 +2513,7 @@ async fn get_group_details(_org_id: &str, group_id: &str, headers: AdminHeaders,
|
||||
_ => err!("Group could not be found!"),
|
||||
};
|
||||
|
||||
Ok(Json(group.to_json_details(&(headers.org_user_type as i32), &mut conn).await))
|
||||
Ok(Json(group.to_json_details(&mut conn).await))
|
||||
}
|
||||
|
||||
#[post("/organizations/<org_id>/groups/<group_id>/delete")]
|
||||
@@ -2999,18 +3002,20 @@ async fn put_reset_password_enrollment(
|
||||
// We need to convert all keys so they have the first character to be a lowercase.
|
||||
// Else the export will be just an empty JSON file.
|
||||
#[get("/organizations/<org_id>/export")]
|
||||
async fn get_org_export(org_id: &str, headers: AdminHeaders, mut conn: DbConn) -> Json<Value> {
|
||||
use semver::{Version, VersionReq};
|
||||
|
||||
async fn get_org_export(
|
||||
org_id: &str,
|
||||
headers: AdminHeaders,
|
||||
client_version: Option<ClientVersion>,
|
||||
mut conn: DbConn,
|
||||
) -> Json<Value> {
|
||||
// Since version v2023.1.0 the format of the export is different.
|
||||
// Also, this endpoint was created since v2022.9.0.
|
||||
// Therefore, we will check for any version smaller then v2023.1.0 and return a different response.
|
||||
// If we can't determine the version, we will use the latest default v2023.1.0 and higher.
|
||||
// https://github.com/bitwarden/server/blob/9ca93381ce416454734418c3a9f99ab49747f1b6/src/Api/Controllers/OrganizationExportController.cs#L44
|
||||
let use_list_response_model = if let Some(client_version) = headers.client_version {
|
||||
let ver_match = VersionReq::parse("<2023.1.0").unwrap();
|
||||
let client_version = Version::parse(&client_version).unwrap();
|
||||
ver_match.matches(&client_version)
|
||||
let use_list_response_model = if let Some(client_version) = client_version {
|
||||
let ver_match = semver::VersionReq::parse("<2023.1.0").unwrap();
|
||||
ver_match.matches(&client_version.0)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
@@ -211,10 +211,7 @@ impl DuoClient {
|
||||
nonce,
|
||||
};
|
||||
|
||||
let token = match self.encode_duo_jwt(jwt_payload) {
|
||||
Ok(token) => token,
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
let token = self.encode_duo_jwt(jwt_payload)?;
|
||||
|
||||
let authz_endpoint = format!("https://{}/oauth/v1/authorize", self.api_host);
|
||||
let mut auth_url = match Url::parse(authz_endpoint.as_str()) {
|
||||
|
||||
+35
-26
@@ -165,20 +165,22 @@ async fn _password_login(
|
||||
// Set the user_uuid here to be passed back used for event logging.
|
||||
*user_uuid = Some(user.uuid.clone());
|
||||
|
||||
// Check password
|
||||
let password = data.password.as_ref().unwrap();
|
||||
if let Some(auth_request_uuid) = data.auth_request.clone() {
|
||||
if let Some(auth_request) = AuthRequest::find_by_uuid(auth_request_uuid.as_str(), conn).await {
|
||||
if !auth_request.check_access_code(password) {
|
||||
err!(
|
||||
"Username or access code is incorrect. Try again",
|
||||
format!("IP: {}. Username: {}.", ip.ip, username),
|
||||
ErrorEvent {
|
||||
event: EventType::UserFailedLogIn,
|
||||
}
|
||||
)
|
||||
// Check if the user is disabled
|
||||
if !user.enabled {
|
||||
err!(
|
||||
"This user has been disabled",
|
||||
format!("IP: {}. Username: {}.", ip.ip, username),
|
||||
ErrorEvent {
|
||||
event: EventType::UserFailedLogIn
|
||||
}
|
||||
} else {
|
||||
)
|
||||
}
|
||||
|
||||
let password = data.password.as_ref().unwrap();
|
||||
|
||||
// If we get an auth request, we don't check the user's password, but the access code of the auth request
|
||||
if let Some(ref auth_request_uuid) = data.auth_request {
|
||||
let Some(auth_request) = AuthRequest::find_by_uuid(auth_request_uuid.as_str(), conn).await else {
|
||||
err!(
|
||||
"Auth request not found. Try again.",
|
||||
format!("IP: {}. Username: {}.", ip.ip, username),
|
||||
@@ -186,6 +188,24 @@ async fn _password_login(
|
||||
event: EventType::UserFailedLogIn,
|
||||
}
|
||||
)
|
||||
};
|
||||
|
||||
let expiration_time = auth_request.creation_date + chrono::Duration::minutes(5);
|
||||
let request_expired = Utc::now().naive_utc() >= expiration_time;
|
||||
|
||||
if auth_request.user_uuid != user.uuid
|
||||
|| !auth_request.approved.unwrap_or(false)
|
||||
|| request_expired
|
||||
|| ip.ip.to_string() != auth_request.request_ip
|
||||
|| !auth_request.check_access_code(password)
|
||||
{
|
||||
err!(
|
||||
"Username or access code is incorrect. Try again",
|
||||
format!("IP: {}. Username: {}.", ip.ip, username),
|
||||
ErrorEvent {
|
||||
event: EventType::UserFailedLogIn,
|
||||
}
|
||||
)
|
||||
}
|
||||
} else if !user.check_valid_password(password) {
|
||||
err!(
|
||||
@@ -197,8 +217,8 @@ async fn _password_login(
|
||||
)
|
||||
}
|
||||
|
||||
// Change the KDF Iterations
|
||||
if user.password_iterations != CONFIG.password_iterations() {
|
||||
// Change the KDF Iterations (only when not logging in with an auth request)
|
||||
if data.auth_request.is_none() && user.password_iterations != CONFIG.password_iterations() {
|
||||
user.password_iterations = CONFIG.password_iterations();
|
||||
user.set_password(password, None, false, None);
|
||||
|
||||
@@ -207,17 +227,6 @@ async fn _password_login(
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the user is disabled
|
||||
if !user.enabled {
|
||||
err!(
|
||||
"This user has been disabled",
|
||||
format!("IP: {}. Username: {}.", ip.ip, username),
|
||||
ErrorEvent {
|
||||
event: EventType::UserFailedLogIn
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
let now = Utc::now().naive_utc();
|
||||
|
||||
if user.verified_at.is_none() && CONFIG.mail_enabled() && CONFIG.signups_verify() {
|
||||
|
||||
+100
-3
@@ -1,13 +1,20 @@
|
||||
use once_cell::sync::Lazy;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use rocket::{fs::NamedFile, http::ContentType, response::content::RawHtml as Html, serde::json::Json, Catcher, Route};
|
||||
use rocket::{
|
||||
fs::NamedFile,
|
||||
http::ContentType,
|
||||
response::{content::RawCss as Css, content::RawHtml as Html, Redirect},
|
||||
serde::json::Json,
|
||||
Catcher, Route,
|
||||
};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{
|
||||
api::{core::now, ApiResult, EmptyResult},
|
||||
auth::decode_file_download,
|
||||
error::Error,
|
||||
util::{Cached, SafeString},
|
||||
util::{get_web_vault_version, Cached, SafeString},
|
||||
CONFIG,
|
||||
};
|
||||
|
||||
@@ -16,7 +23,7 @@ pub fn routes() -> Vec<Route> {
|
||||
// crate::utils::LOGGED_ROUTES to make sure they appear in the log
|
||||
let mut routes = routes![attachments, alive, alive_head, static_files];
|
||||
if CONFIG.web_vault_enabled() {
|
||||
routes.append(&mut routes![web_index, web_index_head, app_id, web_files]);
|
||||
routes.append(&mut routes![web_index, web_index_direct, web_index_head, app_id, web_files, vaultwarden_css]);
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
@@ -45,11 +52,101 @@ fn not_found() -> ApiResult<Html<String>> {
|
||||
Ok(Html(text))
|
||||
}
|
||||
|
||||
#[get("/css/vaultwarden.css")]
|
||||
fn vaultwarden_css() -> Cached<Css<String>> {
|
||||
// Configure the web-vault version as an integer so it can be used as a comparison smaller or greater then.
|
||||
// The default is based upon the version since this feature is added.
|
||||
static WEB_VAULT_VERSION: Lazy<u32> = Lazy::new(|| {
|
||||
let re = regex::Regex::new(r"(\d{4})\.(\d{1,2})\.(\d{1,2})").unwrap();
|
||||
let vault_version = get_web_vault_version();
|
||||
|
||||
let (major, minor, patch) = match re.captures(&vault_version) {
|
||||
Some(c) if c.len() == 4 => (
|
||||
c.get(1).unwrap().as_str().parse().unwrap(),
|
||||
c.get(2).unwrap().as_str().parse().unwrap(),
|
||||
c.get(3).unwrap().as_str().parse().unwrap(),
|
||||
),
|
||||
_ => (2024, 6, 2),
|
||||
};
|
||||
format!("{major}{minor:02}{patch:02}").parse::<u32>().unwrap()
|
||||
});
|
||||
|
||||
// Configure the Vaultwarden version as an integer so it can be used as a comparison smaller or greater then.
|
||||
// The default is based upon the version since this feature is added.
|
||||
static VW_VERSION: Lazy<u32> = Lazy::new(|| {
|
||||
let re = regex::Regex::new(r"(\d{1})\.(\d{1,2})\.(\d{1,2})").unwrap();
|
||||
let vw_version = crate::VERSION.unwrap_or("1.32.1");
|
||||
|
||||
let (major, minor, patch) = match re.captures(vw_version) {
|
||||
Some(c) if c.len() == 4 => (
|
||||
c.get(1).unwrap().as_str().parse().unwrap(),
|
||||
c.get(2).unwrap().as_str().parse().unwrap(),
|
||||
c.get(3).unwrap().as_str().parse().unwrap(),
|
||||
),
|
||||
_ => (1, 32, 1),
|
||||
};
|
||||
format!("{major}{minor:02}{patch:02}").parse::<u32>().unwrap()
|
||||
});
|
||||
|
||||
let css_options = json!({
|
||||
"web_vault_version": *WEB_VAULT_VERSION,
|
||||
"vw_version": *VW_VERSION,
|
||||
"signup_disabled": !CONFIG.signups_allowed() && CONFIG.signups_domains_whitelist().is_empty(),
|
||||
"mail_enabled": CONFIG.mail_enabled(),
|
||||
"yubico_enabled": CONFIG._enable_yubico() && (CONFIG.yubico_client_id().is_some() == CONFIG.yubico_secret_key().is_some()),
|
||||
"emergency_access_allowed": CONFIG.emergency_access_allowed(),
|
||||
"sends_allowed": CONFIG.sends_allowed(),
|
||||
"load_user_scss": true,
|
||||
});
|
||||
|
||||
let scss = match CONFIG.render_template("scss/vaultwarden.scss", &css_options) {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
// Something went wrong loading the template. Use the fallback
|
||||
warn!("Loading scss/vaultwarden.scss.hbs or scss/user.vaultwarden.scss.hbs failed. {e}");
|
||||
CONFIG
|
||||
.render_fallback_template("scss/vaultwarden.scss", &css_options)
|
||||
.expect("Fallback scss/vaultwarden.scss.hbs to render")
|
||||
}
|
||||
};
|
||||
|
||||
let css = match grass_compiler::from_string(
|
||||
scss,
|
||||
&grass_compiler::Options::default().style(grass_compiler::OutputStyle::Compressed),
|
||||
) {
|
||||
Ok(css) => css,
|
||||
Err(e) => {
|
||||
// Something went wrong compiling the scss. Use the fallback
|
||||
warn!("Compiling the Vaultwarden SCSS styles failed. {e}");
|
||||
let mut css_options = css_options;
|
||||
css_options["load_user_scss"] = json!(false);
|
||||
let scss = CONFIG
|
||||
.render_fallback_template("scss/vaultwarden.scss", &css_options)
|
||||
.expect("Fallback scss/vaultwarden.scss.hbs to render");
|
||||
grass_compiler::from_string(
|
||||
scss,
|
||||
&grass_compiler::Options::default().style(grass_compiler::OutputStyle::Compressed),
|
||||
)
|
||||
.expect("SCSS to compile")
|
||||
}
|
||||
};
|
||||
|
||||
// Cache for one day should be enough and not too much
|
||||
Cached::ttl(Css(css), 86_400, false)
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
async fn web_index() -> Cached<Option<NamedFile>> {
|
||||
Cached::short(NamedFile::open(Path::new(&CONFIG.web_vault_folder()).join("index.html")).await.ok(), false)
|
||||
}
|
||||
|
||||
// Make sure that `/index.html` redirect to actual domain path.
|
||||
// If not, this might cause issues with the web-vault
|
||||
#[get("/index.html")]
|
||||
fn web_index_direct() -> Redirect {
|
||||
Redirect::to(format!("{}/", CONFIG.domain_path()))
|
||||
}
|
||||
|
||||
#[head("/")]
|
||||
fn web_index_head() -> EmptyResult {
|
||||
// Add an explicit HEAD route to prevent uptime monitoring services from
|
||||
|
||||
+21
-3
@@ -615,7 +615,6 @@ pub struct AdminHeaders {
|
||||
pub device: Device,
|
||||
pub user: User,
|
||||
pub org_user_type: UserOrgType,
|
||||
pub client_version: Option<String>,
|
||||
pub ip: ClientIp,
|
||||
}
|
||||
|
||||
@@ -625,14 +624,12 @@ impl<'r> FromRequest<'r> for AdminHeaders {
|
||||
|
||||
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||
let headers = try_outcome!(OrgHeaders::from_request(request).await);
|
||||
let client_version = request.headers().get_one("Bitwarden-Client-Version").map(String::from);
|
||||
if headers.org_user_type >= UserOrgType::Admin {
|
||||
Outcome::Success(Self {
|
||||
host: headers.host,
|
||||
device: headers.device,
|
||||
user: headers.user,
|
||||
org_user_type: headers.org_user_type,
|
||||
client_version,
|
||||
ip: headers.ip,
|
||||
})
|
||||
} else {
|
||||
@@ -900,3 +897,24 @@ impl<'r> FromRequest<'r> for WsAccessTokenHeader {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ClientVersion(pub semver::Version);
|
||||
|
||||
#[rocket::async_trait]
|
||||
impl<'r> FromRequest<'r> for ClientVersion {
|
||||
type Error = &'static str;
|
||||
|
||||
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||
let headers = request.headers();
|
||||
|
||||
let Some(version) = headers.get_one("Bitwarden-Client-Version") else {
|
||||
err_handler!("No Bitwarden-Client-Version header provided")
|
||||
};
|
||||
|
||||
let Ok(version) = semver::Version::parse(version) else {
|
||||
err_handler!("Invalid Bitwarden-Client-Version header provided")
|
||||
};
|
||||
|
||||
Outcome::Success(ClientVersion(version))
|
||||
}
|
||||
}
|
||||
|
||||
+27
-7
@@ -497,11 +497,11 @@ make_config! {
|
||||
/// Password iterations |> Number of server-side passwords hashing iterations for the password hash.
|
||||
/// The default for new users. If changed, it will be updated during login for existing users.
|
||||
password_iterations: i32, true, def, 600_000;
|
||||
/// Allow password hints |> Controls whether users can set password hints. This setting applies globally to all users.
|
||||
/// Allow password hints |> Controls whether users can set or show password hints. This setting applies globally to all users.
|
||||
password_hints_allowed: bool, true, def, true;
|
||||
/// Show password hint |> Controls whether a password hint should be shown directly in the web page
|
||||
/// if SMTP service is not configured. Not recommended for publicly-accessible instances as this
|
||||
/// provides unauthenticated access to potentially sensitive data.
|
||||
/// Show password hint (Know the risks!) |> Controls whether a password hint should be shown directly in the web page
|
||||
/// if SMTP service is not configured and password hints are allowed. Not recommended for publicly-accessible instances
|
||||
/// because this provides unauthenticated access to potentially sensitive data.
|
||||
show_password_hint: bool, true, def, false;
|
||||
|
||||
/// Admin token/Argon2 PHC |> The plain text token or Argon2 PHC string used to authenticate in this very same page. Changing it here will not deauthorize the current session!
|
||||
@@ -811,8 +811,15 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
|
||||
}
|
||||
|
||||
// TODO: deal with deprecated flags so they can be removed from this list, cf. #4263
|
||||
const KNOWN_FLAGS: &[&str] =
|
||||
&["autofill-overlay", "autofill-v2", "browser-fileless-import", "extension-refresh", "fido2-vault-credentials"];
|
||||
const KNOWN_FLAGS: &[&str] = &[
|
||||
"autofill-overlay",
|
||||
"autofill-v2",
|
||||
"browser-fileless-import",
|
||||
"extension-refresh",
|
||||
"fido2-vault-credentials",
|
||||
"ssh-key-vault-item",
|
||||
"ssh-agent",
|
||||
];
|
||||
let configured_flags = parse_experimental_client_feature_flags(&cfg.experimental_client_feature_flags);
|
||||
let invalid_flags: Vec<_> = configured_flags.keys().filter(|flag| !KNOWN_FLAGS.contains(&flag.as_str())).collect();
|
||||
if !invalid_flags.is_empty() {
|
||||
@@ -1269,11 +1276,16 @@ impl Config {
|
||||
let hb = load_templates(CONFIG.templates_folder());
|
||||
hb.render(name, data).map_err(Into::into)
|
||||
} else {
|
||||
let hb = &CONFIG.inner.read().unwrap().templates;
|
||||
let hb = &self.inner.read().unwrap().templates;
|
||||
hb.render(name, data).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_fallback_template<T: serde::ser::Serialize>(&self, name: &str, data: &T) -> Result<String, Error> {
|
||||
let hb = &self.inner.read().unwrap().templates;
|
||||
hb.render(&format!("fallback_{name}"), data).map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn set_rocket_shutdown_handle(&self, handle: rocket::Shutdown) {
|
||||
self.inner.write().unwrap().rocket_shutdown_handle = Some(handle);
|
||||
}
|
||||
@@ -1312,6 +1324,11 @@ where
|
||||
reg!($name);
|
||||
reg!(concat!($name, $ext));
|
||||
}};
|
||||
(@withfallback $name:expr) => {{
|
||||
let template = include_str!(concat!("static/templates/", $name, ".hbs"));
|
||||
hb.register_template_string($name, template).unwrap();
|
||||
hb.register_template_string(concat!("fallback_", $name), template).unwrap();
|
||||
}};
|
||||
}
|
||||
|
||||
// First register default templates here
|
||||
@@ -1355,6 +1372,9 @@ where
|
||||
|
||||
reg!("404");
|
||||
|
||||
reg!(@withfallback "scss/vaultwarden.scss");
|
||||
reg!("scss/user.vaultwarden.scss");
|
||||
|
||||
// And then load user templates to overwrite the defaults
|
||||
// Use .hbs extension for the files
|
||||
// Templates get registered with their relative name
|
||||
|
||||
+11
-6
@@ -1,6 +1,6 @@
|
||||
use crate::util::LowerCase;
|
||||
use crate::CONFIG;
|
||||
use chrono::{DateTime, NaiveDateTime, TimeDelta, Utc};
|
||||
use chrono::{NaiveDateTime, TimeDelta, Utc};
|
||||
use serde_json::Value;
|
||||
|
||||
use super::{
|
||||
@@ -30,7 +30,8 @@ db_object! {
|
||||
Login = 1,
|
||||
SecureNote = 2,
|
||||
Card = 3,
|
||||
Identity = 4
|
||||
Identity = 4,
|
||||
SshKey = 5
|
||||
*/
|
||||
pub atype: i32,
|
||||
pub name: String,
|
||||
@@ -216,11 +217,13 @@ impl Cipher {
|
||||
Some(p) if p.is_string() => Some(d.data),
|
||||
_ => None,
|
||||
})
|
||||
.map(|d| match d.get("lastUsedDate").and_then(|l| l.as_str()) {
|
||||
Some(l) if DateTime::parse_from_rfc3339(l).is_ok() => d,
|
||||
.map(|mut d| match d.get("lastUsedDate").and_then(|l| l.as_str()) {
|
||||
Some(l) => {
|
||||
d["lastUsedDate"] = json!(crate::util::validate_and_format_date(l));
|
||||
d
|
||||
}
|
||||
_ => {
|
||||
let mut d = d;
|
||||
d["lastUsedDate"] = json!("1970-01-01T00:00:00.000Z");
|
||||
d["lastUsedDate"] = json!("1970-01-01T00:00:00.000000Z");
|
||||
d
|
||||
}
|
||||
})
|
||||
@@ -317,6 +320,7 @@ impl Cipher {
|
||||
"secureNote": null,
|
||||
"card": null,
|
||||
"identity": null,
|
||||
"sshKey": null,
|
||||
});
|
||||
|
||||
// These values are only needed for user/default syncs
|
||||
@@ -345,6 +349,7 @@ impl Cipher {
|
||||
2 => "secureNote",
|
||||
3 => "card",
|
||||
4 => "identity",
|
||||
5 => "sshKey",
|
||||
_ => panic!("Wrong type"),
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::{User, UserOrgType, UserOrganization};
|
||||
use super::{User, UserOrganization};
|
||||
use crate::api::EmptyResult;
|
||||
use crate::db::DbConn;
|
||||
use crate::error::MapResult;
|
||||
@@ -73,7 +73,7 @@ impl Group {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn to_json_details(&self, user_org_type: &i32, conn: &mut DbConn) -> Value {
|
||||
pub async fn to_json_details(&self, conn: &mut DbConn) -> Value {
|
||||
let collections_groups: Vec<Value> = CollectionGroup::find_by_group(&self.uuid, conn)
|
||||
.await
|
||||
.iter()
|
||||
@@ -82,7 +82,7 @@ impl Group {
|
||||
"id": entry.collections_uuid,
|
||||
"readOnly": entry.read_only,
|
||||
"hidePasswords": entry.hide_passwords,
|
||||
"manage": *user_org_type >= UserOrgType::Admin || (*user_org_type == UserOrgType::Manager && !entry.read_only && !entry.hide_passwords)
|
||||
"manage": false
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
+48
-9
@@ -96,7 +96,31 @@ fn smtp_transport() -> AsyncSmtpTransport<Tokio1Executor> {
|
||||
smtp_client.build()
|
||||
}
|
||||
|
||||
// This will sanitize the string values by stripping all the html tags to prevent XSS and HTML Injections
|
||||
fn sanitize_data(data: &mut serde_json::Value) {
|
||||
use regex::Regex;
|
||||
use std::sync::LazyLock;
|
||||
static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"<[^>]+>").unwrap());
|
||||
|
||||
match data {
|
||||
serde_json::Value::String(s) => *s = RE.replace_all(s, "").to_string(),
|
||||
serde_json::Value::Object(obj) => {
|
||||
for d in obj.values_mut() {
|
||||
sanitize_data(d);
|
||||
}
|
||||
}
|
||||
serde_json::Value::Array(arr) => {
|
||||
for d in arr.iter_mut() {
|
||||
sanitize_data(d);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_text(template_name: &'static str, data: serde_json::Value) -> Result<(String, String, String), Error> {
|
||||
let mut data = data;
|
||||
sanitize_data(&mut data);
|
||||
let (subject_html, body_html) = get_template(&format!("{template_name}.html"), &data)?;
|
||||
let (_subject_text, body_text) = get_template(template_name, &data)?;
|
||||
Ok((subject_html, body_html, body_text))
|
||||
@@ -116,6 +140,10 @@ fn get_template(template_name: &str, data: &serde_json::Value) -> Result<(String
|
||||
None => err!("Template doesn't contain body"),
|
||||
};
|
||||
|
||||
if text_split.next().is_some() {
|
||||
err!("Template contains more than one body");
|
||||
}
|
||||
|
||||
Ok((subject, body))
|
||||
}
|
||||
|
||||
@@ -259,16 +287,15 @@ pub async fn send_invite(
|
||||
}
|
||||
|
||||
let query_string = match query.query() {
|
||||
None => err!(format!("Failed to build invite URL query parameters")),
|
||||
None => err!("Failed to build invite URL query parameters"),
|
||||
Some(query) => query,
|
||||
};
|
||||
|
||||
// `url.Url` would place the anchor `#` after the query parameters
|
||||
let url = format!("{}/#/accept-organization/?{}", CONFIG.domain(), query_string);
|
||||
let (subject, body_html, body_text) = get_text(
|
||||
"email/send_org_invite",
|
||||
json!({
|
||||
"url": url,
|
||||
// `url.Url` would place the anchor `#` after the query parameters
|
||||
"url": format!("{}/#/accept-organization/?{}", CONFIG.domain(), query_string),
|
||||
"img_src": CONFIG._smtp_img_src(),
|
||||
"org_name": org_name,
|
||||
}),
|
||||
@@ -292,17 +319,29 @@ pub async fn send_emergency_access_invite(
|
||||
String::from(grantor_email),
|
||||
);
|
||||
|
||||
let invite_token = encode_jwt(&claims);
|
||||
// Build the query here to ensure proper escaping
|
||||
let mut query = url::Url::parse("https://query.builder").unwrap();
|
||||
{
|
||||
let mut query_params = query.query_pairs_mut();
|
||||
query_params
|
||||
.append_pair("id", emer_id)
|
||||
.append_pair("name", grantor_name)
|
||||
.append_pair("email", address)
|
||||
.append_pair("token", &encode_jwt(&claims));
|
||||
}
|
||||
|
||||
let query_string = match query.query() {
|
||||
None => err!("Failed to build emergency invite URL query parameters"),
|
||||
Some(query) => query,
|
||||
};
|
||||
|
||||
let (subject, body_html, body_text) = get_text(
|
||||
"email/send_emergency_access_invite",
|
||||
json!({
|
||||
"url": CONFIG.domain(),
|
||||
// `url.Url` would place the anchor `#` after the query parameters
|
||||
"url": format!("{}/#/accept-emergency/?{query_string}", CONFIG.domain()),
|
||||
"img_src": CONFIG._smtp_img_src(),
|
||||
"emer_id": emer_id,
|
||||
"email": percent_encode(address.as_bytes(), NON_ALPHANUMERIC).to_string(),
|
||||
"grantor_name": grantor_name,
|
||||
"token": invite_token,
|
||||
}),
|
||||
)?;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ Emergency access for {{{grantor_name}}}
|
||||
<!---------------->
|
||||
You have been invited to become an emergency contact for {{grantor_name}}. To accept this invite, click the following link:
|
||||
|
||||
Click here to join: {{url}}/#/accept-emergency/?id={{emer_id}}&name={{grantor_name}}&email={{email}}&token={{token}}
|
||||
Click here to join: {{{url}}}
|
||||
|
||||
If you do not wish to become an emergency contact for {{grantor_name}}, you can safely ignore this email.
|
||||
{{> email/email_footer_text }}
|
||||
|
||||
@@ -9,7 +9,7 @@ Emergency access for {{{grantor_name}}}
|
||||
</tr>
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
|
||||
<a href="{{url}}/#/accept-emergency/?id={{emer_id}}&name={{grantor_name}}&email={{email}}&token={{token}}"
|
||||
<a href="{{{url}}}"
|
||||
clicktracking=off target="_blank" style="color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #3c8dbc; border-color: #3c8dbc; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
Become emergency contact
|
||||
</a>
|
||||
@@ -21,4 +21,4 @@ Emergency access for {{{grantor_name}}}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{{> email/email_footer }}
|
||||
{{> email/email_footer }}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
/* See the wiki for examples and details: https://github.com/dani-garcia/vaultwarden/wiki/Customize-Vaultwarden-CSS */
|
||||
@@ -0,0 +1,105 @@
|
||||
/**** START Static Vaultwarden changes ****/
|
||||
/* This combines all selectors extending it into one */
|
||||
%vw-hide {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* This allows searching for the combined style in the browsers dev-tools (look into the head tag) */
|
||||
.vw-hide,
|
||||
head {
|
||||
@extend %vw-hide;
|
||||
}
|
||||
|
||||
/* Hide the Subscription Page tab */
|
||||
bit-nav-item[route="settings/subscription"] {
|
||||
@extend %vw-hide;
|
||||
}
|
||||
|
||||
/* Hide any link pointing to Free Bitwarden Families */
|
||||
a[href$="/settings/sponsored-families"] {
|
||||
@extend %vw-hide;
|
||||
}
|
||||
|
||||
/* Hide the `Enterprise Single Sign-On` button on the login page */
|
||||
a[routerlink="/sso"] {
|
||||
@extend %vw-hide;
|
||||
}
|
||||
|
||||
/* Hide Two-Factor menu in Organization settings */
|
||||
bit-nav-item[route="settings/two-factor"],
|
||||
a[href$="/settings/two-factor"] {
|
||||
@extend %vw-hide;
|
||||
}
|
||||
|
||||
/* Hide Business Owned checkbox */
|
||||
app-org-info > form:nth-child(1) > div:nth-child(3) {
|
||||
@extend %vw-hide;
|
||||
}
|
||||
|
||||
/* Hide the `This account is owned by a business` checkbox and label */
|
||||
#ownedBusiness,
|
||||
label[for^="ownedBusiness"] {
|
||||
@extend %vw-hide;
|
||||
}
|
||||
|
||||
/* Hide the radio button and label for the `Custom` org user type */
|
||||
#userTypeCustom,
|
||||
label[for^="userTypeCustom"] {
|
||||
@extend %vw-hide;
|
||||
}
|
||||
|
||||
/* Hide Business Name */
|
||||
app-org-account form div bit-form-field.tw-block:nth-child(3) {
|
||||
@extend %vw-hide;
|
||||
}
|
||||
|
||||
/* Hide organization plans */
|
||||
app-organization-plans > form > bit-section:nth-child(2) {
|
||||
@extend %vw-hide;
|
||||
}
|
||||
|
||||
/* Hide Device Verification form at the Two Step Login screen */
|
||||
app-security > app-two-factor-setup > form {
|
||||
@extend %vw-hide;
|
||||
}
|
||||
/**** END Static Vaultwarden Changes ****/
|
||||
/**** START Dynamic Vaultwarden Changes ****/
|
||||
{{#if signup_disabled}}
|
||||
/* Hide the register link on the login screen */
|
||||
app-frontend-layout > app-login > form > div > div > div > p {
|
||||
@extend %vw-hide;
|
||||
}
|
||||
{{/if}}
|
||||
|
||||
/* Hide `Email` 2FA if mail is not enabled */
|
||||
{{#unless mail_enabled}}
|
||||
app-two-factor-setup ul.list-group.list-group-2fa li.list-group-item:nth-child(5) {
|
||||
@extend %vw-hide;
|
||||
}
|
||||
{{/unless}}
|
||||
|
||||
/* Hide `YubiKey OTP security key` 2FA if it is not enabled */
|
||||
{{#unless yubico_enabled}}
|
||||
app-two-factor-setup ul.list-group.list-group-2fa li.list-group-item:nth-child(2) {
|
||||
@extend %vw-hide;
|
||||
}
|
||||
{{/unless}}
|
||||
|
||||
/* Hide Emergency Access if not allowed */
|
||||
{{#unless emergency_access_allowed}}
|
||||
bit-nav-item[route="settings/emergency-access"] {
|
||||
@extend %vw-hide;
|
||||
}
|
||||
{{/unless}}
|
||||
|
||||
/* Hide Sends if not allowed */
|
||||
{{#unless sends_allowed}}
|
||||
bit-nav-item[route="sends"] {
|
||||
@extend %vw-hide;
|
||||
}
|
||||
{{/unless}}
|
||||
/**** End Dynamic Vaultwarden Changes ****/
|
||||
/**** Include a special user stylesheet for custom changes ****/
|
||||
{{#if load_user_scss}}
|
||||
{{> scss/user.vaultwarden.scss }}
|
||||
{{/if}}
|
||||
+11
-5
@@ -438,13 +438,19 @@ pub fn get_env_bool(key: &str) -> Option<bool> {
|
||||
|
||||
use chrono::{DateTime, Local, NaiveDateTime, TimeZone};
|
||||
|
||||
// Format used by Bitwarden API
|
||||
const DATETIME_FORMAT: &str = "%Y-%m-%dT%H:%M:%S%.6fZ";
|
||||
|
||||
/// Formats a UTC-offset `NaiveDateTime` in the format used by Bitwarden API
|
||||
/// responses with "date" fields (`CreationDate`, `RevisionDate`, etc.).
|
||||
pub fn format_date(dt: &NaiveDateTime) -> String {
|
||||
dt.format(DATETIME_FORMAT).to_string()
|
||||
dt.and_utc().to_rfc3339_opts(chrono::SecondsFormat::Micros, true)
|
||||
}
|
||||
|
||||
/// Validates and formats a RFC3339 timestamp
|
||||
/// If parsing fails it will return the start of the unix datetime
|
||||
pub fn validate_and_format_date(dt: &str) -> String {
|
||||
match DateTime::parse_from_rfc3339(dt) {
|
||||
Ok(dt) => dt.to_rfc3339_opts(chrono::SecondsFormat::Micros, true),
|
||||
_ => String::from("1970-01-01T00:00:00.000000Z"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Formats a `DateTime<Local>` using the specified format string.
|
||||
@@ -486,7 +492,7 @@ pub fn format_datetime_http(dt: &DateTime<Local>) -> String {
|
||||
}
|
||||
|
||||
pub fn parse_date(date: &str) -> NaiveDateTime {
|
||||
NaiveDateTime::parse_from_str(date, DATETIME_FORMAT).unwrap()
|
||||
DateTime::parse_from_rfc3339(date).unwrap().naive_utc()
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user